[
  {
    "path": ".gitattributes",
    "content": "ui/doppio/** linguist-vendored\nui/javapoly/** linguist-vendored\nui/browserfs/** linguist-vendored\nui/scripts/jquery*.js linguist-vendored\nui/scripts/react*.js linguist-vendored\nui/scripts/JSX*.js linguist-vendored"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: peergos\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: ['https://donorbox.org/peergos']\n"
  },
  {
    "path": ".github/workflows/ant.yml",
    "content": "name: Java CI\n\non: [push]\n\njobs:\n  build:\n\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up JDK 21\n      uses: actions/setup-java@v4\n      with:\n        distribution: temurin\n        java-version: 25\n    - name: Build with Ant\n      run: ant -noinput -buildfile build.xml dist\n    - name: GWT build\n      run: ant -noinput -buildfile build.xml gwtc\n    - name: install fuse 2\n      run: sudo apt install libfuse2\n      if: matrix.os == 'ubuntu-latest'\n    - name: Run tests\n      timeout-minutes: 120\n      run: ant -noinput -buildfile build.xml test\n    - name: Reproducible build\n      run: ./reproducible-test.sh\n"
  },
  {
    "path": ".gitignore",
    "content": "build/*\nlog/*\ndist/*\nout/*\nlog/*\n.idea/*\ngwt-unitCache/*\nwar/*\n*.iml\n*.hprof\n*hs_err_pid*\n*~\npeergos/crypto/RootCertificate.java\npeergos/crypto/DirectoryCertificates.java\npeergos/crypto/CoreCertificates.java\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: java\njdk:\n  - oraclejdk8\n\nafter_failure:\n  - cat TEST-*.txt\n\nnotifications:\n  email:\n    - christoph.boddy@gmail.com\n    - ianopolous@gmail.com\n    - kevodwyer@gmail.com\n\nsudo: required\nbefore_install:\n  - sudo apt-get install -qq pkg-config fuse\n  - sudo modprobe fuse\n  - sudo chmod 666 /dev/fuse\n  - sudo chown root:$USER /etc/fuse.conf\n  - wget https://dist.ipfs.io/go-ipfs/v0.4.1/go-ipfs_v0.4.1_linux-amd64.tar.gz\n  - tar xvfz go-ipfs_v0.4.1_linux-amd64.tar.gz\n  - go-ipfs/ipfs init\n  - go-ipfs/ipfs daemon &"
  },
  {
    "path": "Licence.txt",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<http://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "PrintTestErrors.java",
    "content": "import java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\n/** Parse junit log files and print errors and exceptions including stack traces to the console\n *  Will exit with status 1 if there are any errors, otherwise exit status 0.\n *\n *  Scan all files in \"./test.reports\".\n *  Usage \"java PrintTestErrors.java\n */\npublic class PrintTestErrors {\n\n    public static void main(String[] args) throws IOException {\n        List<Path> reports = Files.list(Path.of(\"test.reports\")).collect(Collectors.toList());\n        boolean anyError = false;\n        for (Path report : reports) {\n            boolean inErr = false;\n            List<String> lines = Files.readAllLines(report);\n            for (int i=0; i < lines.size(); i++) {\n                String line = lines.get(i);\n                if (line.contains(\"<error\") || line.contains(\"<failure\")) {\n                    System.out.println(lines.get(i-1));\n                    inErr = true;\n                    anyError = true;\n                }\n                if (inErr)\n                    System.out.println(line);\n                if (line.contains(\"</error\") || line.contains(\"</failure\"))\n                    inErr = false;\n            }\n        }\n        if (anyError)\n            throw new RuntimeException(\"Test failure(s)!\");\n    }\n}\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://teamcity.jetbrains.com/app/rest/builds/buildType:(id:OpenSourceProjects_Peergos_Build)/statusIcon\"></a>\n\n![Peergos Logo](https://peergos.org/theme/img/peergos/logo-main.svg)\n\nPeergos\n========\n\nPeergos is building the next web - the private web, where end users are in control. Imagine web apps being secure by default and unable to track you. Imagine being able to control exactly what personal data each web app can see. Imagine never having to log in to an app ever again. You own your data and decide where it is stored and who can see it. At Peergos, we believe that privacy is a fundamental human right and we want to make it easy for everyone to interact online in ways that respect this right.\n\nThe foundation of Peergos is a peer-to-peer encrypted global filesystem with fine-grained access control designed to be resistant to surveillance of data content or friendship graphs. It has a secure messenger, an encrypted email client and bridge, and a totally private and secure social network, where users are in control of who sees what (executed cryptographically). Our motto at Peergos is, \"Control your data, control your destiny.\"\n\nThe name Peergos comes from the Greek word Πύργος (Pyrgos), which means stronghold or tower, but phonetically spelt with the nice connection to being peer-to-peer. Pronunciation: peer-goss (as in gossip).\n\nScreenshots\n----\n![Drive](/assets/images.jpg)\n\nSee more screenshots in the web-ui repository [https://github.com/Peergos/web-ui](https://github.com/Peergos/web-ui).\n\nTry it now!\n----\nWant to try it out now? Here's a read-only secret link [to a folder](https://peergos.net/secret/z59vuwzfFDovgun2sU9YF8LqJRVXaoVR39XAkvRR6sNp7CJnseecHhV/3647803968#oqmlU2vyq0Fe);\n\nSign-up\n----\nWe run a paid server at https://peergos.net/. \n\nTech book\n---------\nYou can read more detail about our features and architecture in our [tech book](https://book.peergos.org).\n\nRecent progress\n---------\nTo see recent developments read the latest [release notes](https://peergos.net/public/peergos/releases) or see our [web-ui repo releases](https://github.com/Peergos/web-ui/releases).\n\nMedia\n---------\nThe slides of a talk introducing Peergos are [here](https://speakerdeck.com/ianopolous/peergos-architecture) \n\nDeep dive at IPFS Camp 2024\n\n[![IPFS Camp 2024](https://img.youtube.com/vi/yDU4GHsEo34/0.jpg)](https://www.youtube.com/watch?v=yDU4GHsEo34)\n\nDeep dive at Devstaff Crete:\n\n[![Deep dive](https://img.youtube.com/vi/Po_fdZYcfXo/0.jpg)](https://www.youtube.com/watch?v=Po_fdZYcfXo)\n\nOverview at IPFS Thing:\n\n[![Overview](https://img.youtube.com/vi/g1vzoZjG9Zo/0.jpg)](https://www.youtube.com/watch?v=g1vzoZjG9Zo)\n\n[![Architecture details](https://img.youtube.com/vi/HVyrVUI2-RA/0.jpg)](https://www.youtube.com/watch?v=HVyrVUI2-RA)\n\nApplications on Peergos:\n\n[![Overview](https://img.youtube.com/vi/3i1TtknNw2E/0.jpg)](https://www.youtube.com/watch?v=3i1TtknNw2E)\n\n[![A better web: secure, private p2p apps with user owned data and identity](https://img.youtube.com/vi/mSElk2jcFqY/0.jpg)](https://www.youtube.com/watch?v=mSElk2jcFqY)\n\nApplications deep dive:\n\n[![Overview](https://img.youtube.com/vi/oberD75GU8I/0.jpg)](https://www.youtube.com/watch?v=oberD75GU8I)\n\n\nArchitecture talk at IPFS Lab Day:\n\n[![Architecture Talk](https://img.youtube.com/vi/h54pShffxvI/0.jpg)](https://www.youtube.com/watch?v=h54pShffxvI)\n\nIntroduction and 2020 update:\n\n[![Introduction and 2020 update](https://img.youtube.com/vi/oXMqYDLKWPc/0.jpg)](https://www.youtube.com/watch?v=oXMqYDLKWPc)\n\nIntroduction:\n\n[![Introduction](https://img.youtube.com/vi/dCLboQDlzds/0.jpg)](https://www.youtube.com/watch?v=dCLboQDlzds)\n\n\nSupport\n-------\nIf you would like to support Peergos development, then please make a \n\n[recurring donation less than 100 EUR per week](https://liberapay.com/peergos)\n\nor a \n\n[larger or one off donation](https://donorbox.org/peergos). \n\nAudits\n-----\n### 2024\nhttps://peergos.org/posts/security-audit-2024\n\n### 2019\nhttps://peergos.org/posts/security-audit\n\n### All audit reports\nhttps://github.com/Peergos/Peergos/tree/master/audits\n\nChat room\n---------\nThere is a public chat room for Peergos on [Matrix](https://matrix.to/#/#peergos-chat:matrix.org).\n\nPeergos aims\n------------\n - Allow individuals to securely and privately store files in a peer to peer network which has no central node and is generally difficult to disrupt or surveil\n - Allow secure sharing of files with other users of the network without visible meta-data (who shares with who)\n - Allow web apps to be loaded and run directly from Peergos in a sandbox that prevents data exfiltration and with user granted permissions\n - Have a beautiful user interface that any computer or mobile user can understand\n - Be independent of the central TLS Certificate Authority trust architecture\n - Be self-hostable - A user should be able to easily run Peergos on a machine in their home and get their own Peergos storage space, and social communication platform from it. \n - Have a secure web interface\n\nProject anti-aims\n-----------------\n - Peergos does not provide anonymity, yet. Anonymity can be achieved by creating and only ever accessing a User account over Tor\n\nArchitecture\n------------\n1.0 Layers of architecture\n - 1: Peer-to-peer and data layer - [IPFS](https://ipfs.io) provides the data storage, routing and retrieval. A User must have at least one Peergos instance storing their data for it to be available. \n - 2: Authorization Layer - a key pair controls who is able to modify parts of the file system (every write is signed)\n - 3: Data storage - controlled by a given public key there is a [merkle-champ](https://en.wikipedia.org/wiki/Hash_array_mapped_trie) of encrypted chunks under random labels, without any cross links visible to the server (the server can't deduce the size of files)\n - 4: Encryption - Strong encryption is done on the user's machine using [TweetNaCl](http://tweetnacl.cr.yp.to/), with each 5MiB chunk of a file being encrypted independently. \n - 5: Social layer implementing the concept of following or being friends with another user, without exposing the friend network to anyone.\n - 6: Sharing - Secure cryptographic sharing of files with friends.\n\n2.0 Language\n - The IPFS layer is coded in Java - we have a minimal ipfs implementation - [Nabu](https://github.com/peergos/nabu)\n - The Peergos server is coded to run on JVM to get portability and speed, predominantly Java\n - The web interface is mostly coded in Java and cross compiled to JavaScript, with the exception of the Tweetnacl and scrypt libraries, and a small amount of GUI code in JS for Vue.js. \n - Apps are written in HTML5\n\n3.0 Nodes\n - There is a pki node which ensures unique usernames using a structure similar to certificate transparency. This data is mirrored on every peergos server. \n - A new node contacts any public Peergos server to join the network\n\n4.0 Trust\n - New versions of the software will be delivered through Peergos itself. (Able to be turned off by the user if desired)\n - A user who trusts a public Peergos server (and the SSL Certificate authority chain) can use the web interface over TLS\n - A less trusting user can run a Peergos server/proxy on their own machine and use the web interface over localhost\n - A more paranoid user can run a Peergos server on their own machine and use the CLI or the fuse/webdav binding\n - Servers are trustless - your data and metadata cannot be exposed even if your server is compromised (assuming your client is not compromised)\n - IPFS itself is not trusted and all data stored or retrieved from it is self-certifying. \n - The data store (which may not be ipfs directly, but S3 compatible service for example) is also not trusted\n\n4.0 Logging in\n - A user's username is used along with a random salt and the hash of their password and run through scrypt (with parameters 17, 8, 1, 96, though users can choose harder parameters if desired) to generate a symmetric key and a signing keypair. The signing keypair is then used to auth and retrieve encrypted login data. This login data is then decrypted using the symmetric key to obtain the identity key pair, social keypair and root directory capability. This means that a user can log in from any machine without transferring any keys, and also that their keys are protected from a brute force attack (see slides mentioned above for a cost estimate).\n\n5.0 Encryption\n - Private keys never leave the client node. Two random symmetric keys are generated for every file or directory (explicitly not convergent encryption, which leaks information)\n\n5.1 Post-quantum encryption\n - Files that haven't been shared with another user are already resistant to quantum computer based attacks. This is because the operations to decrypt them from logging in, to seeing plain-text, include only hashing and symmetric encryption, both of which are currently believed to not be significantly weakened with a quantum computer. \n - Files that have been shared between users are, currently, vulnerable to a large enough quantum computer if an attacker is able to log the initial follow requests sent between the users (before the user retrieves and deletes them). This will be replaced with a post-quantum asymmetric algorithm as soon as a clear candidate arrives.  \n\n6.0 Friend network\n - Anyone can send anyone else a \"follow request\". This amounts to \"following\" someone and is a one way protocol. This is stored in the target user's server, but the server cannot see who is sending the friend request (it is cryptographically blinded). \n - The target user can respond to friend requests with their own friend request to make it bi-directional (the usual concept of a friend). \n - Once onion routing is integrated, there will be no way for an attacker (or us) to deduce the friendship graph (who is friends with who). \n \n7.0 Sharing of a file (with another user, through a secret link, or publicly)\n - Once user A is being followed by user B, then A can share files with user B (B can revoke their following at any time)\n - File access control is based on the [cryptree](https://raw.githubusercontent.com/ianopolous/Peergos/master/papers/wuala-cryptree.pdf) system used by Wuala\n - A link can be generated to a file or a folder which can be shared with anyone through any medium. A link is of the form https://demo.peergos.net/#KEY_MATERIAL which has the property that even the link doesn't leak the file contents to the network, as the key material after the # is not sent to the server, but interpreted locally in the browser. We have extended cryptree to protect much more metadata, including file size, names, thumbnails, directory structure and more. \n - A user can publish a capability to a file or folder they control which makes it publicly visible\n\nUsage - running locally to log in to another instance\n-----\nUse this method to login to a Peergos account on another instance without any reliance on DNS or the TLS certificate authorities. \n\n1. Download a release from https://peergos.net/public/peergos/releases\n\n2. If you download the jar, install Java - You will need Java >= 25 installed. \n\n3. Run Peergos with:\n\n```\njava -jar Peergos.jar daemon\n```\nor for the native packages:\n```\npeergos daemon\n```\nAll the Peergos data will be stored in ~/.peergos by default, which can be overridden with the environment var or arg - PEERGOS_PATH. \n\nYou can then access the web interface and login via http://localhost:8000.\n\nIn this mode of operation all your writes are proxied directly to your home server. The local instance caches any blocks you access for faster subsequent access. \n\nIf you are using the packaged desktop app, or the default `peergos` launcher on macOS, you can point it at a self-hosted instance with:\n```bash\npeergos -server-url https://YOUR_PEERGOS_SERVER_DOMAIN\n```\nTo make that persistent for the desktop app, add the following to `~/.peergos/config`:\n```ini\nserver-url = https://YOUR_PEERGOS_SERVER_DOMAIN\n```\nFor security, prefer `https` for remote servers, and only use plain `http` for loopback addresses such as `http://localhost:8000`.\n\nUsage - self hosting\n-----\nUse this method to run a new home-server (which is best with a publicly routable IP, and always on machine) to create accounts on or migrate accounts to.\n\n1. Download a release from https://peergos.net/public/peergos/releases\n\n2. Install Java - You will need Java >= 25 installed. \n\n3. Run Peergos with:\n```\njava -jar Peergos.jar daemon -generate-token true\n```\n\n4. Ensure you can listen on a public IP address\n\n   Some cloud hosts don't add your public ip to the network interfaces by default. For these cases you may need to run something like\n```bash\n   sudo ip address add MY.PUBLIC.IP dev eth0\n```\n\nAll the Peergos data will be stored in ~/.peergos by default, which can be overridden with the environment var or arg - PEERGOS_PATH\n\nYou can then access the web interface and signup via the localhost address printed, which includes a single use signup token.\n\nThe config is stored in $PEERGOS_PATH/config, so for subsequent runs you can just use the following unless you want to override any config\n```\njava -jar Peergos.jar daemon\n```\n\nNote that whichever Peergos server you sign up through (your home server) will be storing your data, so if you don't intend on leaving your Peergos server running permanently, then we recommend signing up on https://peergos.net and then you can log in through a local Peergos instance and all your data will magically end up on the peergos.net server. Peergos can work behind NATs and firewalls, but we recommend using a server with a public IP. If you want to expose your web interface publicly you will need to arrange a domain name and TLS certificates (we recommend using nginx and letsencrypt). \n\nIf you don't set up a domain name and TLS you can still log in to your account from another Peergos instance, e.g. one you run locally on your laptop - connections are routed securely over P2P TLS1.3 streams to your home server. In this case, any writes are proxied to your home server so your data is always persisted there. If you do expose your instance via a DNS name and TLS certificate, you will need to add this parameter:\n> -public-server true\n\nIf you are also using a reverse proxy like nginx to terminate TLS you will need to tell Peergos which domain you are using with the following arg:\n> -public-domain $YOUR_DOMAIN\n\nAnd the TLS certificate will also need to cover the wildcard subdomain for the applications (like the PDF viewer, text editor, calendar, and custom 3rd party apps) to work. For example, it should have A records that cover $YOUR_DOMAIN and *.$YOUR_DOMAIN\n\nIf you are using a reverse proxy like nginx to terminate TLS here is a good example of the nginx config file (replace $YOUR_DOMAIN_NAME) (On SELinux enabled Linux distributions you need to make sure to allow nginx to access port 8000 tcp_socket.):\n```\n# Peergos server config\n\nserver {\n    listen 80 default_server;\n    listen [::]:80 default_server;\n\n    location ^~ /.well-known {\n         allow all;\n         proxy_pass http://127.0.0.1:8888;\n    }\n\n    # redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.\n    return 301 https://$host$request_uri;\n}\n\nserver {\n        # SSL configuration\n        listen 443 ssl http2;\n        ssl_protocols TLSv1.2 TLSv1.3;\n        ssl_prefer_server_ciphers on;\n        ssl_session_cache shared:SSL:10m;\n        ssl_session_timeout 10m;\n\n        ssl_ciphers TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:!TLS_AES_128_GCM_SHA256;\n        ssl_certificate /etc/letsencrypt/live/$YOUR_DOMAIN_NAME/fullchain.pem;\n        ssl_certificate_key /etc/letsencrypt/live/$YOUR_DOMAIN_NAME/privkey.pem;\n\n        add_header Strict-Transport-Security \"max-age=31536000\" always;\n        server_name $YOUR_DOMAIN_NAME;\n\n        client_max_body_size 2M;\n\n        location / {\n                proxy_pass http://127.0.0.1:8000;\n                proxy_set_header X-Real-IP $remote_addr;\n                proxy_set_header Host $http_host;\n                allow all;\n        }\n\n\t# pass through for letsencrypt\n        location ^~ /.well-known {\n                 allow all;\n                 proxy_pass http://127.0.0.1:8888;\n        }\n}\n\n```\n\nUsage - self hosting (with docker)\n-----\nGet a docker image with:\n```bash\ndocker pull ghcr.io/peergos/web-ui:master\n```\n\nRun the Peergos image with:\n```bash\ndocker run --volume $(PEERGOS_PATH):/opt/peergos/data ghcr.io/peergos/web-ui:master daemon -listen-host 0.0.0.0 -public-domain $YOUR_DOMAIN_NAME -public-server true -announce-ipfs-addresses /ip4/$IP/tcp/4001,/ip4/$IP/udp/4001/quic-v1 -log-to-console true\n```\n\nUsage - self hosting (with S3 compatible blockstore)\n-----\nFollow the instructions for self hosting but add the following parameters (either on the command line, or in the .peergos/config file after first run):\n```\n-use-s3 true\n-authed-s3-reads true\n-direct-s3-writes true\n-s3.accessKey $ACCESS_KEY\n-s3.bucket $BUCKET\n-s3.region $REGION\n-s3.region.endpoint $ENDPOINT (e.g. us-east-1.linodeobjects.com)\n-s3.secretKey #SECRET_KEY\n```\nN.B. Minio seems to have some issues, so with Minio use: -authed-s3-reads false -direct-s3-writes false\n\nYou will also need to set the cors.xml for the bucket to the following:\n```\n<CORSConfiguration>\n  <CORSRule>\n    <AllowedOrigin>https://$YOUR_DOMAIN</AllowedOrigin>\n    <AllowedMethod>HEAD</AllowedMethod>\n    <AllowedMethod>GET</AllowedMethod>\n    <AllowedMethod>PUT</AllowedMethod>\n    <AllowedHeader>*</AllowedHeader>\n    <ExposeHeader>ETag</ExposeHeader>\n    <ExposeHeader>Content-Length</ExposeHeader>\n    <MaxAgeSeconds>3600</MaxAgeSeconds>\n  </CORSRule>\n</CORSConfiguration>\n```\n\n\nUsage - self hosting (with Postgres instead of sqlite)\n-----\nFollow the instructions for self hosting but add the following parameters (either on the command line, or in the .peergos/config file after first run):\n```\n-use-postgres true\n-postgres.database $DATABASE\n-postgres.host $HOST\n-postgres.password $PASSWORD\n-postgres.username $USERNAME\n```\n\nUsage - troubleshooting\n-----\n* You can run with \"-log-to-console true\" to also show any logging on the console. \n* The very first run will sync the pki and this takes several minutes. Subsequent runs should start within seconds. \n\n\n### CLI\nThere are a range of commands available from a command line. You can run -help to find the available commands or details on any command or sub-command. Most users should only need the *daemon* and *shell* commands, and maybe *identity* or *fuse*. You can use the *migrate* command to move all your data to a new server (where the command is run). \n\n```\n>> java -Djava.library.path=native-lib -jar Peergos.jar -help\nMain: Run a Peergos command\nSub commands:\n\tdaemon: The user facing Peergos server\n\tshell: An interactive command-line-interface to a Peergos server\n\tfuse: Mount a Peergos user's filesystem natively\n\tquota: Manage quota of users on this server\n\tserver-msg: Send and receive messages to/from users of this server\n\tgateway: Serve websites directly from Peergos\n\tmigrate: Move a Peergos account to this server\n\tidentity: Create or verify an identity proof\n\tipfs: Install, configure and start IPFS daemon\n\tpki: Start the Peergos PKI Server that has already been bootstrapped\n\tpki-init: Bootstrap and start the Peergos PKI Server\n```\nor\n```\n>> java -Djava.library.path=native-lib -jar Peergos.jar identity -help\nidentity: Create or verify an identity proof\n\nSub commands:\n\tlink: Link your Peergos identity to an account on another service.\n\tverify: Verify an identity link post from another service.\n```\n\nMirror\n--------\nTo mirror all of your data on another server first run the following command (on any instance):\n> java -jar Peergos.jar mirror init -username $username -peergos-url https://YOUR_PEERGOS_SERVER_DOMAIN\n\nIt will ask for your password and then print three parameters you need to supply to the mirror daemon.\n\nThen run daemon, on the instance you want to mirror your data, with the following additional args provided by the init command.\n> java -jar Peergos.jar daemon -mirror.username $username -mirror.bat $mirrorBat -login-keypair $loginKeypair\n\nThis will then continuously mirror that user's data on this instance. \n\nMigrate\n--------\nTo migrate to another server first ensure you have sufficient quota on it, then run the migrate command on it. \n> java -jar Peergos.jar migrate\n\nIt will ask for your username and password, mirror all your data locally, and then update the PKI to make this your home server. \n\nAfter migration, your identity is unchanged, all links to your data continue to work, and you keep your social graph without needing to tell anyone. \n\nShell\n--------\nVarious operation can be done using the shell.\n```shell\njava -jar Peergos.jar shell\n```\n\nTo connect to a server you will need to provide the server address (including http/https), username and password.\n```shell\nEnter Server address\n> https://peergos.net\nEnter username\n> demo\nEnter password for 'demo'\n> **************************************\nGenerating keys\nLogging in\nRetrieving Friends\ndemo@https://peergos.net >\n```\n\nTo show all available commands\n```shell\ndemo@https://peergos.net > help\n```\n\nHint: The following command might be useful to do an initial upload for larger folders.\n```shell\nput local_path remote_path \n```\n\nSync\n-----\nThere is a bi-directional sync client that will let you sync a native directory with a Peergos directory (or several pairs of directories). The recommended way to setup syncs is to use the desktop/mobile app. Run the app and login to the browser tab it opens to http://localhost:7777, and click on the sync icon in the left side bar. From there you can choose a host and Peergos folder to sync, and whether or not to sync deletes on either or both ends. \n\nIf for some reason you must use the CLI then continue reading. \n\nTo set this up first run:\n```\n>> java -jar Peergos.jar sync init -peergos-url https://peergos.net\n```\nAnd follow the prompts to enter your username, password and the Peergos dir you want to sync with. This will output something like:\n```\n>> Run the sync dir command with the following args: -links secret/z59vuwzfFDomTEuyeEw7rkofcd2vt5EnVffmAy5fnQe9V9MG36ZiBVY/3615659421#QUq6mf4gz8uk -local-dirs $LOCAL_DIR\n```\n\nThen run the sync client with:\n```\n>> java -jar Peergos.jar sync dir -peergos-url https://peergos.net -links secret/z59vuwzfFDomTEuyeEw7rkofcd2vt5EnVffmAy5fnQe9V9MG36ZiBVY/3615659421#QUq6mf4gz8uk -local-dirs /path/to/local/dir\n```\n\nFUSE (native folder mounting of Peergos)\n--------\nYou can mount your Peergos space with the following command\n```\n>> java -Djava.library.path=native-lib -jar Peergos.jar fuse -peergos-url https://peergos.net -username $username -password $password\n```\n\n### MacOS FUSE prerequisites\nInstall osxfuse with\n```\n>> brew install --cask osxfuse\n```\n\n### Windows FUSE prerequisites\nInstall winfsp with\n```\n>> choco install winfsp\n```\n\nWebdav\n------\nYou can run a local webdav bridge which allows you to access your Peergos files with any webdav compatible client. Run the following command (choose an arbitrary webdav username and password): \n```\n>> peergos webdav -peergos-url https://peergos.net -username $username -PEERGOS_PASSWORD $password -webdav.username $webdav-username -PEERGOS_WEBDAV_PASSWORD $webdav-password\n```\nYou can then browse to your home directory with http://localhost:8090/$YOUR-USERNAME\n\nDevelopment\n--------\n### Dependencies\nRequires jdk17 and ant to build. Use the following to install dependencies:\n#### On debian\n```shell\nsudo apt-get install ant\nsudo apt-get install openjdk-17-jdk\n```\n#### On macOS\n```shell\nbrew install ant # installs openjdk as a dependency\nant -version\nApache Ant(TM) version 1.10.8 compiled on May 10 2020\n```\n### Build\nNote that this doesn't include any web ui. For the full build including the web interface build https://github.com/peergos/web-ui\n```shell\nant dist\n```\n### Cross compile to JS\n```shell\nant gwtc\n```\n### Run tests\nYou need to have ant-optional installed:\n#### On debian\n```shell\nsudo apt-get install ant-optional\n```\n#### On macOS\nNothing additional is needed for the ant package on macOS.\n\nRunning tests will install and configure the correct version of IPFS automatically, run the daemon, and terminate it afterwards. \n```shell\nant test\n```\n\n### Development Notes\nThe ``ant compile`` target will only compile sources in src/peergos/{client,server,shared} folders.\n"
  },
  {
    "path": "ReproducibleJar.java",
    "content": "import java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.nio.file.attribute.*;\nimport java.util.*;\nimport java.util.stream.*;\nimport java.util.zip.*;\n\npublic class ReproducibleJar {\n    public static void main(String[] args) throws Exception {\n        Path inputJar = Paths.get(args[0]);\n        long timeStamp = Long.parseLong(args[1]);\n\n        URI uri = URI.create(\"jar:\" + inputJar.toUri());\n        byte[] newJar;\n        try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) {\n            Iterable<Path> roots = fs.getRootDirectories();\n            Path root = roots.iterator().next();\n            newJar = setAllTimes(root, FileTime.fromMillis(timeStamp));\n        }\n        Files.write(inputJar, newJar, StandardOpenOption.TRUNCATE_EXISTING);\n    }\n\n    private static byte[] setAllTimes(Path root, FileTime lastModified) {\n        Map<Path, byte[]> files = new TreeMap<>();\n        try {\n            Set<Path> all = Files.walk(root).collect(Collectors.toSet());\n            for (Path p: all) {\n                if (! Files.isDirectory(p))\n                    files.put(p, Files.readAllBytes(p));\n            }\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            ZipOutputStream zout = new ZipOutputStream(bout);\n            for (Path p: files.keySet()) {\n                ZipEntry zipEntry = new ZipEntry(p.toString().substring(1));\n                zipEntry.setLastModifiedTime(lastModified);\n                zout.putNextEntry(zipEntry);\n                zout.write(files.get(p));\n                zout.closeEntry();\n            }\n            zout.close();\n            return bout.toByteArray();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "RoadMap.md",
    "content": "![Peergos Logo](https://peergos.org/theme/img/peergos/logo-main.svg)\n\nPeergos Road Map\n========\n\n\nCentralized Alpha\n------------\n - &#10004; Stable web interface and data formats (at least pre-crdt)\n - &#10004; Streaming E2E encrypted video (streaming disabled by default)\n - &#10004; Streaming download of arbitrarily large files (disabled by default)\n - &#10004; Max user count to deny new user signups after some limit (And display error to user)\n - &#10004; Max storage quota per user (enforced on puts) and error shown to user\n - &#10004; Whitelist of users that can write to this server\n - &#10004; Blacklist of users that can't be read from this server (illegal content guard)\n \nDecentralized writes + Self-hostable storage\n------------\n - &#10004;Each user stores an ipfs node id (cid) in their PKI which is the server responsible for storing their data\n - &#10004;Implement MutablePointers and SocialNetwork in terms of ipfs p2p stream\n - &#10004;Mirror core node PKI on every node for private friend lookups\n - &#10004;Implement corenode in terms of ipfs p2p stream to allow self hosting in ipfs itself\n\nKeymail\n------------\n - Initially don't need much UI other than upload text file for keymail an select recipient\n - Application to display and edit text (ideally by granting app write access only to a hidden keymail folder)\n - Decide format, information compatible with email headers\n - Bridge to email - a client that polls an email account, writing newly received emails into peergos, and sending emails from new files in a particular folder\n\nGroup chat\n------------\n - owner invite others\n - owner can grant admin to others (admin => they can invite new users)\n - allow inline pictures\n - be careful about leaking social metadata accidentally\n\nSocial Feed\n------------\n - Essentially just a group chat that is for most (all?) your friends, but similar to a Facebook or Twitter feed\n\nFully Quantum proof\n------------\n - Move asymmetric crypto (follow requests and signing roots) to a post-quantum algorithm\n - http://sphincs.cr.yp.to/software.html\n - https://www.win.tue.nl/~tchou/mcbits/\n  - Zk-SNARKs for follow requests? https://blog.ethereum.org/2016/12/05/zksnarks-in-a-nutshell/\n\nSustainable\n------------\n - Use cryptocurrency to pay for username claims, must be privacy preserving, and post-quantum\n"
  },
  {
    "path": "build.xml",
    "content": "<project name=\"Peergos\" default=\"dist\" basedir=\".\">\n  <description>\n    Building Peergos\n  </description>\n\n  <!-- Need to run \"sudo apt-get install ant-optional\" -->\n  <taskdef resource=\"net/sf/antcontrib/antlib.xml\">\n    <classpath>\n      <fileset dir=\"ant.lib\"/>\n    </classpath>\n  </taskdef>\n  \n  <property name=\"src\" location=\"src\"/>\n  <property name=\"test.sources\" location=\"src\"/>\n  <property name=\"build\" location=\"build\"/>\n  <property name=\"dist\" location=\"dist\"/>\n  <property name=\"test.reports\" location=\"test.reports\"/>\n  <property name=\"timestamp.millis\" value=\"1489731900000\"/>\n\n  <path id=\"dep.runtime\">\n    <fileset dir=\"./lib\">\n        <include name=\"**/*.jar\" />\n    </fileset>\n  </path>\n\n  <path id=\"dep.build\">\n    <fileset dir=\"./lib-build\">\n        <include name=\"**/*.jar\" />\n    </fileset>\n  </path>\n\n  <!-- Arguments to gwtc and devmode targets, set style to Obfuscated for 3X smaller output-->\n  <property name=\"gwt.args\" value=\"-generateJsInteropExports -style Pretty -strict\" />\n\n  <!-- Configure path to GWT SDK -->\n  <!--<property name=\"gwt.sdk\" location=\"/home/ian/gwt-2.8.0-rc1\" />-->\n  <property name=\"gwt.sdk\" location=\"gwt/gwt-2.8.3\" />\n\n\n  <path id=\"project.class.path\">\n    <pathelement location=\"war/WEB-INF/classes\"/>\n    <pathelement location=\"${gwt.sdk}/gwt-user.jar\"/>\n    <pathelement location=\"${gwt.sdk}/gwt-dev.jar\"/>\n    <pathelement location=\"${gwt.sdk}/validation-api-1.0.0.GA.jar\"/>\n    <pathelement location=\"${gwt.sdk}/validation-api-1.0.0.GA-sources.jar\"/>\n    <fileset dir=\"war/WEB-INF/lib\" includes=\"**/*.jar\"/>\n    <!-- Add any additional non-server libs (such as JUnit) here -->\n    <pathelement location=\"lib-build/junit-4.11.jar\"/>\n    <pathelement location=\"lib-build/hamcrest-core-1.3.jar\"/>\n  </path>\n  \n  <target name=\"init\">\n    <mkdir dir=\"${build}\"/>\n    <mkdir dir=\"${test.reports}\"/>\n  </target>\n\n  <target name=\"compile\" depends=\"clean, init\"\n        description=\"compile the source\">\n    <javac source=\"17\" target=\"17\" encoding=\"UTF-8\" includeantruntime=\"false\" destdir=\"${build}\" debug=\"true\" debuglevel=\"lines,vars,source\">\n      <src>\n\t<pathelement location=\"${src}/peergos/shared\"/>\n\t<pathelement location=\"${src}/peergos/server\"/>\n\t<pathelement location=\"${src}/peergos/client\"/>\n      </src>\n      <classpath>\n\t<fileset dir=\"lib\">\n          <include name=\"**/*.jar\" />\n        </fileset>\n\t<fileset dir=\"lib-build\">\n          <include name=\"**/*.jar\" />\n        </fileset>\n      </classpath>\n    </javac>\n  </target>\n\n  <target name=\"dist\" depends=\"compile\" description=\"generate the distribution\">\n    <mkdir dir=\"${dist}/lib\"/>\n    <copy todir=\"${dist}/lib\">\n      <fileset dir=\"lib\"/>\n    </copy>\n    <copy todir=\"${build}/native-lib\">\n      <fileset dir=\"native-lib\"/>\n    </copy>\n    <manifestclasspath property=\"manifest_cp\" jarfile=\"myjar.jar\">\n      <classpath refid=\"dep.runtime\" />\n    </manifestclasspath>\n    <jar jarfile=\"${dist}/Peergos.jar\" basedir=\"${build}\" includes=\"peergos/server/**,peergos/shared/**,peergos/client/**,native-lib/**,pki/**\">\n      <manifest>\n        <attribute name=\"Main-Class\" value=\"peergos.server.Main\"/>\n        <attribute name=\"Class-Path\" value=\"${manifest_cp}\"/>\n        <attribute name=\"Multi-Release\" value=\"true\"/>\n        <attribute name=\"Created-By\" value=\"Java\"/>\n        <attribute name=\"Ant-Version\" value=\"Ant\"/>\n      </manifest>\n    </jar>\n    <exec executable=\"java\">\n      <arg value=\"ReproducibleJar.java\"/>\n      <arg value=\"${dist}/Peergos.jar\"/>\n      <arg value=\"${timestamp.millis}\"/>\n    </exec>\n    <copy todir=\".\">\n      <fileset file=\"${dist}/Peergos.jar\"/>\n    </copy>\n  </target>\n\n  <target name=\"test\" depends=\"parallel_test\">\n    <exec executable=\"java\" failonerror=\"true\">\n      <arg value=\"PrintTestErrors.java\"/>\n    </exec>\n  </target> \n\n  <target name=\"execute.test\">\n    <!-- we need to have relative path -->\n    <pathconvert property=\"test.source.relative\">\n      <fileset file=\"${test.source.absolute}\" />\n      <map from=\"${test.sources}/\" to=\"\" />\n    </pathconvert>\n    <!-- run one particular test -->\n    <junit fork=\"true\" printsummary=\"true\" haltonfailure=\"no\">\n      <jvmarg value=\"-Xmx4g\"/>\n      <jvmarg value=\"-Djava.library.path=native-lib\"/>\n      <jvmarg value=\"--enable-native-access=ALL-UNNAMED\"/>\n      <jvmarg value=\"--sun-misc-unsafe-memory-access=allow\"/>\n      <classpath>\n\t<fileset dir=\"lib-build\">\n\t  <include name=\"**/*.jar\"/>\n\t</fileset>\n\t<fileset dir=\"lib\">\n\t  <include name=\"**/*.jar\"/>\n\t</fileset>\n\t<pathelement location=\"${build}\" />\n      </classpath>\n      <formatter type=\"xml\" />\n      <batchtest todir=\"${test.reports}\" skipNonTests=\"true\">\n\t<fileset dir=\"${test.sources}\">\n          <filename name=\"${test.source.relative}\" />\n\t</fileset>\n      </batchtest>\n    </junit>\n  </target>\n\n  <target name=\"parallel_test\" depends=\"compile,dist,linux_tests\">\n    <for\n\tkeepgoing=\"false\"\n\tthreadCount=\"1\"\n\tparallel=\"true\"\n\tparam=\"test.source.absolute\">\n      <path>\n\t<fileset dir=\"${test.sources}\">\n\t  <include name=\"peergos/server/tests/*.java\"/>\n\t</fileset>\n      </path>\n      <sequential>\n\t<antcall target=\"execute.test\">\n\t  <param name=\"test.source.absolute\" value=\"@{test.source.absolute}\"/>\n\t</antcall>\n      </sequential>\n    </for>\n  </target>\n\n  <condition property=\"isMac\">\n    <os family=\"mac\" />\n  </condition>\n  <condition property=\"isLinux\">\n    <and>\n      <os family=\"unix\"/>\n      <not>\n        <os family=\"mac\"/>\n      </not>\n    </and>\n  </condition>\n  \n  <target name=\"linux_tests\" depends=\"compile,dist\" if=\"isLinux\">\n    <for\n\tkeepgoing=\"false\"\n\tthreadCount=\"1\"\n\tparallel=\"true\"\n\tparam=\"test.source.absolute\">\n      <path>\n\t<fileset dir=\"${test.sources}\">\n\t  <include name=\"peergos/server/tests/linux/*.java\"/>\n\t</fileset>\n      </path>\n      <sequential>\n\t<antcall target=\"execute.test\">\n\t  <param name=\"test.source.absolute\" value=\"@{test.source.absolute}\"/>\n\t</antcall>\n      </sequential>\n    </for>\n  </target>\n\n  <target name=\"ipfs_tests\" depends=\"compile,dist\">\n    <for\n\tkeepgoing=\"false\"\n\tthreadCount=\"1\"\n\tparallel=\"true\"\n\tparam=\"test.source.absolute\">\n      <path>\n\t<fileset dir=\"${test.sources}\">\n\t  <include name=\"peergos/server/tests/IpfsUserTests.java\"/>\n\t</fileset>\n      </path>\n      <sequential>\n\t<antcall target=\"execute.test\">\n\t  <param name=\"test.source.absolute\" value=\"@{test.source.absolute}\"/>\n\t</antcall>\n      </sequential>\n    </for>\n  </target>\n\n  <!-- GWT stuff -->\n\n  \n  <target name=\"libs\" description=\"Copy libs to WEB-INF/lib\">\n    <mkdir dir=\"war/WEB-INF/lib\" />\n    <copy todir=\"war/WEB-INF/lib\" file=\"${gwt.sdk}/gwt-servlet.jar\" />\n    <copy todir=\"war/WEB-INF/lib\" file=\"${gwt.sdk}/gwt-servlet-deps.jar\" />\n    <!-- Add any additional server libs that need to be copied -->\n    <!-- <copy todir=\"war/WEB-INF/lib\" file=\"lib/jnr-fuse-0.5.7-all.jar\" />-->\n  </target>\n\n  <target name=\"javac\" depends=\"libs\" description=\"Compile java source to bytecode\">\n    <mkdir dir=\"war/WEB-INF/classes\"/>\n    <javac srcdir=\"src\" encoding=\"utf-8\"\n        destdir=\"war/WEB-INF/classes\"\n        source=\"17\" target=\"17\" nowarn=\"true\"\n        debug=\"true\" debuglevel=\"lines,vars,source\">\n        <include name=\"peergos/client/**\" />\n        <include name=\"peergos/shared/**\" />\n      <classpath refid=\"project.class.path\"/>\n    </javac>\n    <copy todir=\"war/WEB-INF/classes\">\n      <fileset dir=\"src\" excludes=\"**/*.java\"/>\n    </copy>\n  </target>\n\n  <target name=\"gwtc\" depends=\"javac\" description=\"GWT compile to JavaScript (production mode)\">\n    <java failonerror=\"true\" fork=\"true\" classname=\"com.google.gwt.dev.Compiler\" maxmemory=\"8g\">\n      <classpath>\n        <pathelement location=\"src\"/>\n        <path refid=\"project.class.path\"/>\n      </classpath>\n      <arg value=\"-sourceLevel\"/><arg value=\"11\"/>\n      <arg line=\"-war\"/>\n      <arg value=\"war\"/>\n      <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->\n      <arg line=\"${gwt.args}\"/>\n      <arg value=\"peergos.Peergos\"/>\n    </java>\n  </target>\n\n  <target name=\"devmode\" depends=\"javac\" description=\"Run development mode (pass -Dgwt.args=-nosuperDevMode to fallback to classic DevMode)\">\n    <java failonerror=\"true\" fork=\"true\" classname=\"com.google.gwt.dev.DevMode\" maxmemory=\"1g\">\n      <classpath>\n        <pathelement location=\"src\"/>\n        <path refid=\"project.class.path\"/>\n        <pathelement location=\"${gwt.sdk}/gwt-codeserver.jar\"/>\n      </classpath>\n      <arg value=\"-startupUrl\"/>\n      <arg value=\"Peergos.html\"/>\n      <arg line=\"-war\"/>\n      <arg value=\"war\"/>\n      <!-- Additional arguments like -style PRETTY, -logLevel DEBUG or -nosuperDevMode -->\n      <arg line=\"${gwt.args}\"/>\n      <arg value=\"peergos.Peergos\"/>\n      <arg value=\"peergos.Peergos\"/>\n    </java>\n  </target>\n  \n  <target name=\"clean\" description=\"clean up\">\n    <delete dir=\"${build}\"/>\n    <delete dir=\"${dist}\"/>\n    <delete dir=\"${dist}\"/>\n    <delete dir=\"war\"/>\n  </target>\n</project>\n"
  },
  {
    "path": "feedback/notes.md",
    "content": "# Design patterns for the Dweb\n\nUser testing 2019-06-29\n\n## Who was there?\n\n* Ramine Daribiha (shokunin)\n* Victor Rortvedt\n* Gorka Ludlow (aquigorka)\n* Makoto\n* Akshay (aksanoble)\n\n## Impressions\n\n1. It's a thing for uploading files\n2. It's to upload any file.\n3. Didn't look a photo gallery (not file specific)\n4. Not for music\n5. For very private but somehow also public\n6. For small content (because of small allowance)\n7. Kind of like Pastebin\n8. It's for data that you care about, \n9. No other service needed.\n10. Encrypted, difficult to hack.\n11. Dropbox equivalent.\n12. Is it for sharing or is it for backup?\n\n## How should it work (jobs to be done)?\n\nGorka\n\n1. Should be as seamless as going into any browser and dragging and dropping between file browser.\n2. Context menu -- when you right-click \n3. Open with native application when opening in Peergos.\n4. Moving large files should show a progress bar.\n5. Only people who you've shared the folder with could read/write to the folder.\n6. Dropbox without Dropbox.\n\nAkshay\n\n1. Immediately share content with someone over a LAN (dropbox for LAN)\n2. Find a name for someone nearby, click send, they go to a page and can then download it immediately (encrypted on the way).\n3. Mesh network solution\n\nVictor\n\n1. Sharing securely - it first tells me that I am in a safe environment. Need a visualization of \"safety\". Files are transformed and given a logo. \n2. When it is sent, something that shows it was shared securely. \n3. Visual optionality of being able to retract.\n4. Has the recipient received it?\n5. OK when uploading should be 'Dismiss'\n\nMakoto\n\n1. Some visual handshake to verify that the person you are sharing with is who you think they are. \n2. Some verification task?\n3. Would probably end up verifying over some channel before sharing.\n4. Need a shiboleth\n5. Finds keybase super-annoying (can copy text but can't take a screen shot)\n\nRamine\n\n1. Visualizations of encryption, manifest of what has been shared.\n2. Key folder.\n\n## Solutions\n\n### Triage\n\nFour options:\n\n1. Drag and drop (8)\n2. Mesh network sharing (3)\n3. Everything is visually super-encrypted. (7)\n4. Sharing and interactively verification (people would pay money for this). (9)\n-- Diffie Hellman authentication?\n-- Swap public keys over email, chat\n-- Add and prove.\n\n-- Generate a 4 digit PIN and send it to that person over another channel. Verify that your follower has received it before your confirm your would like o\n\n-- Send a link for the person to accept the follow request.\n\nWhat is the cost of delay?\n"
  },
  {
    "path": "gwt/BUILD-GWT.txt",
    "content": "Built using the instructions on: http://www.gwtproject.org/makinggwtbetter.html#compiling\nbuilt from githash c5fd90c6d 6/nov/2019 with the commit 62e0d9a92 1/nov/2017 reversed\n\nbuild command:\nant -Dgwt.version=2.8.3\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n."
  },
  {
    "path": "gwt/gwt-2.8.3/COPYING",
    "content": "           GWT LICENSE INFORMATION\n\nMarch 3, 2008\n\nThe GWT software and sample code developed by the GWT authors is\nlicensed under the Apache License, v. 2.0. Other software included in this\ndistribution is provided under other licenses, as listed in the Included\nSoftware and Licenses section at the bottom of this page. Source code for\nsoftware included in this distribution is available from the GWT\nproject or as otherwise indicated at the bottom of this page.\n\nPlease note that the executable version of the GWT SDK\ndistributed by Google will communicate with Google's servers to check for\navailable updates. If updates are available, you will receive the option to\ninstall them.\n\n=====\n\nApache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/ \n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and \ndistribution as defined by Sections 1 through 9 of this document. \n\n\"Licensor\" shall mean the copyright owner or entity authorized by the \ncopyright owner that is granting the License. \n\n\"Legal Entity\" shall mean the union of the acting entity and all other \nentities that control, are controlled by, or are under common control with \nthat entity. For the purposes of this definition, \"control\" means (i) the \npower, direct or indirect, to cause the direction or management of such \nentity, whether by contract or otherwise, or (ii) ownership of fifty percent \n(50%) or more of the outstanding shares, or (iii) beneficial ownership of \nsuch entity. \n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising \npermissions granted by this License. \n\n\"Source\" form shall mean the preferred form for making modifications, \nincluding but not limited to software source code, documentation source, and \nconfiguration files. \n\n\"Object\" form shall mean any form resulting from mechanical transformation \nor translation of a Source form, including but not limited to compiled \nobject code, generated documentation, and conversions to other media types. \n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, \nmade available under the License, as indicated by a copyright notice that is \nincluded in or attached to the work (an example is provided in the Appendix \nbelow). \n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, \nthat is based on (or derived from) the Work and for which the editorial \nrevisions, annotations, elaborations, or other modifications represent, as a \nwhole, an original work of authorship. For the purposes of this License, \nDerivative Works shall not include works that remain separable from, or \nmerely link (or bind by name) to the interfaces of, the Work and Derivative \nWorks thereof. \n\n\"Contribution\" shall mean any work of authorship, including the original \nversion of the Work and any modifications or additions to that Work or \nDerivative Works thereof, that is intentionally submitted to Licensor for \ninclusion in the Work by the copyright owner or by an individual or Legal \nEntity authorized to submit on behalf of the copyright owner. For the \npurposes of this definition, \"submitted\" means any form of electronic, \nverbal, or written communication sent to the Licensor or its \nrepresentatives, including but not limited to communication on electronic \nmailing lists, source code control systems, and issue tracking systems that \nare managed by, or on behalf of, the Licensor for the purpose of discussing \nand improving the Work, but excluding communication that is conspicuously \nmarked or otherwise designated in writing by the copyright owner as \"Not a \nContribution.\" \n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on \nbehalf of whom a Contribution has been received by Licensor and subsequently \nincorporated within the Work. \n\n2. Grant of Copyright License. Subject to the terms and conditions of this \nLicense, each Contributor hereby grants to You a perpetual, worldwide, \nnon-exclusive, no-charge, royalty-free, irrevocable copyright license to \nreproduce, prepare Derivative Works of, publicly display, publicly perform, \nsublicense, and distribute the Work and such Derivative Works in Source or \nObject form. \n\n3. Grant of Patent License. Subject to the terms and conditions of this \nLicense, each Contributor hereby grants to You a perpetual, worldwide, \nnon-exclusive, no-charge, royalty-free, irrevocable (except as stated in \nthis section) patent license to make, have made, use, offer to sell, sell, \nimport, and otherwise transfer the Work, where such license applies only to \nthose patent claims licensable by such Contributor that are necessarily \ninfringed by their Contribution(s) alone or by combination of their \nContribution(s) with the Work to which such Contribution(s) was submitted. \nIf You institute patent litigation against any entity (including a \ncross-claim or counterclaim in a lawsuit) alleging that the Work or a \nContribution incorporated within the Work constitutes direct or contributory \npatent infringement, then any patent licenses granted to You under this \nLicense for that Work shall terminate as of the date such litigation is \nfiled. \n\n4. Redistribution. You may reproduce and distribute copies of the Work or \nDerivative Works thereof in any medium, with or without modifications, and \nin Source or Object form, provided that You meet the following conditions: \n\na. You must give any other recipients of the Work or Derivative Works a copy \nof this License; and \n\nb. You must cause any modified files to carry prominent notices stating that \nYou changed the files; and \n\nc. You must retain, in the Source form of any Derivative Works that You \ndistribute, all copyright, patent, trademark, and attribution notices from \nthe Source form of the Work, excluding those notices that do not pertain to \nany part of the Derivative Works; and \n\nd. If the Work includes a \"NOTICE\" text file as part of its distribution, \nthen any Derivative Works that You distribute must include a readable copy \nof the attribution notices contained within such NOTICE file, excluding \nthose notices that do not pertain to any part of the Derivative Works, in at \nleast one of the following places: within a NOTICE text file distributed as \npart of the Derivative Works; within the Source form or documentation, if \nprovided along with the Derivative Works; or, within a display generated by \nthe Derivative Works, if and wherever such third-party notices normally \nappear. The contents of the NOTICE file are for informational purposes only \nand do not modify the License. You may add Your own attribution notices \nwithin Derivative Works that You distribute, alongside or as an addendum to \nthe NOTICE text from the Work, provided that such additional attribution \nnotices cannot be construed as modifying the License.\n\nYou may add Your own copyright statement to Your modifications and may \nprovide additional or different license terms and conditions for use, \nreproduction, or distribution of Your modifications, or for any such \nDerivative Works as a whole, provided Your use, reproduction, and \ndistribution of the Work otherwise complies with the conditions stated in \nthis License. \n\n5. Submission of Contributions. Unless You explicitly state otherwise, any \nContribution intentionally submitted for inclusion in the Work by You to the \nLicensor shall be under the terms and conditions of this License, without \nany additional terms or conditions. Notwithstanding the above, nothing \nherein shall supersede or modify the terms of any separate license agreement \nyou may have executed with Licensor regarding such Contributions. \n\n6. Trademarks. This License does not grant permission to use the trade \nnames, trademarks, service marks, or product names of the Licensor, except \nas required for reasonable and customary use in describing the origin of the \nWork and reproducing the content of the NOTICE file. \n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in \nwriting, Licensor provides the Work (and each Contributor provides its \nContributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY \nKIND, either express or implied, including, without limitation, any \nwarranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or \nFITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining \nthe appropriateness of using or redistributing the Work and assume any risks \nassociated with Your exercise of permissions under this License. \n\n8. Limitation of Liability. In no event and under no legal theory, whether \nin tort (including negligence), contract, or otherwise, unless required by \napplicable law (such as deliberate and grossly negligent acts) or agreed to \nin writing, shall any Contributor be liable to You for damages, including \nany direct, indirect, special, incidental, or consequential damages of any \ncharacter arising as a result of this License or out of the use or inability \nto use the Work (including but not limited to damages for loss of goodwill, \nwork stoppage, computer failure or malfunction, or any and all other \ncommercial damages or losses), even if such Contributor has been advised of \nthe possibility of such damages. \n\n9. Accepting Warranty or Additional Liability. While redistributing the Work \nor Derivative Works thereof, You may choose to offer, and charge a fee for, \nacceptance of support, warranty, indemnity, or other liability obligations \nand/or rights consistent with this License. However, in accepting such \nobligations, You may act only on Your own behalf and on Your sole \nresponsibility, not on behalf of any other Contributor, and only if You \nagree to indemnify, defend, and hold each Contributor harmless for any \nliability incurred by, or claims asserted against, such Contributor by \nreason of your accepting any such warranty or additional liability. \n\n===\n\nLICENSE INFORMATION REGARDING BUNDLED THIRD-PARTY SOFTWARE\n\nThe following third party software is distributed with GWT\nand is provided under other licenses and/or has source \navailable from other locations. Where \"gwt-dev.jar\" is listed, \nsubstitute in the name of the jar corresponding to your platform, \ne.g. \"gwt-dev-linux.jar\".\n\n* Apache Tomcat\n  License: Apache License v. 2.0 (above)\n  Source code availability: http://tomcat.apache.org\n    modifications are at org/apache/tomcat/ within gwt-dev.jar\n\n* Apache Tapestry\n  License: Apache License v. 2.0 (above)\n  Source code availability: http://tapestry.apache.org\n\n* ASM 3.1\n  License: (custom)\n    http://asm.objectweb.org/license.html\n  Source code availability: com/google/gwt/dev/asm/ within gwt-dev.jar\n  \n* Eclipse Java Development Tools (JDT)\n  License: Eclipse Public License v. 1.0 \n    http://www.eclipse.org/legal/epl-v10.html\n  Source code availability:\n    http://archive.eclipse.org/eclipse/downloads/drops/R-3.3.1-200709211145/download.php?dropFile=eclipse-JDT-SDK-3.3.1.zip\n\n* Eclipse Standard Widget Toolkit (SWT)\n  License: Eclipse Public License v. 1.0\n    http://www.eclipse.org/legal/epl-v10.html\n  Source code availability:\n    Linux: http://download.eclipse.org/eclipse/downloads/drops/R-3.2.1-200609210945/download.php?dropFile=swt-3.2.1-gtk-linux-x86.zip\n    Windows: http://download.eclipse.org/eclipse/downloads/drops/R-3.2.1-200609210945/download.php?dropFile=swt-3.2.1-win32-win32-x86.zip\n    Mac: http://download.eclipse.org/eclipse/downloads/drops/R-3.2.1-200609210945/download.php?dropFile=swt-3.2.1-carbon-macosx.zip\n    modifications are at org/eclipse/swt/ within gwt-dev.jar\n\n* Jetty\n  License: Apache License v. 2.0 (above)\n  Source code availability:\n    http://mortbay.org/jetty/\n\n* Mozilla Rhino\n  License: Mozilla Public License v. 1.1\n    http://www.mozilla.org/MPL/MPL-1.1.txt\n  Source code availability: com/google/gwt/dev/js/rhino/ within gwt-dev.jar\n\n* Mozilla 1.7.12 (Linux only)\n  License: Mozilla Public License v. 1.1\n    http://www.mozilla.org/MPL/MPL-1.1.txt\n  Source code availability: \n    http://developer.mozilla.org/en/docs/Download_Mozilla_Source_Code\n\n* Protobuf\n  License: New BSD License\n    http://www.opensource.org/licenses/bsd-license.php\n  Source code availablility:\n    http://code.google.com/p/protobuf/source/checkout\n  Binary modifications consist of package-rebasing. The rebased classes are rooted at com/google/gwt/dev/protobuf/ within gwt-dev.jar\n"
  },
  {
    "path": "gwt/gwt-2.8.3/COPYING.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n\n<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n\n<title>Google Web Toolkit License Information</title>\n\n<style type=\"text/css\">\n\np.heading {\n    font-weight: bold;\n}\n\n.licenses {\n    border-collapse: separate;\n}\n\n.licenses th {\n    text-align: left;\n    background-color: #ccccff;\n    padding: 3px;\n}\n\n.licenses td {\n    background-color: #f4f4f4;\n    padding: 3px;\n    text-align: left;\n    vertical-align: top;\n}\n\n.licenses tr.even td {\n    background-color: #eeeeee;\n}\n\n</style></head>\n\n<body>\n    \n<div id=\"body\">\n\n\n\n<h1>Google Web Toolkit License Information</h1>\n\n<p>March 3, 2008</p>\n\n<p>The Google Web Toolkit software and sample code developed by Google\nis licensed under the Apache License, v. 2.0.  Other software included\nin this distribution is provided under other licenses, as listed in the\nIncluded Software and Licenses section at the bottom of this page.\nSource code for software included in this distribution is available\nfrom the\n<a href=\"http://code.google.com/p/google-web-toolkit/\">Google Web Toolkit</a>\nproject or as otherwise indicated at the bottom of this page.\n</p>\n\n<p>Please note that the executable version of the Google Web Toolkit\ndistributed by Google will communicate with Google's servers to check\nfor available updates.  If updates are available, you will receive the\noption to install them.\n</p>\n\n<h2>Apache License, Version 2.0</h2>\n\n<p>\nApache License<br>\nVersion 2.0, January 2004<br>\n<a href=\"http://www.apache.org/licenses/\">http://www.apache.org/licenses/</a>\n\n</p>\n\n<p><b><a name=\"definitions\">1. Definitions</a></b>.</p>\n<p>\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n</p>\n<p>\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n</p>\n<p>\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n</p>\n<p>\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n</p>\n<p>\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n</p>\n<p>\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n</p>\n<p>\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n</p>\n\n<p>\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n</p>\n<p>\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n</p>\n<p>\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n</p>\n<p><b><a name=\"copyright\">2. Grant of Copyright License</a></b>.\nSubject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n</p>\n<p><b><a name=\"patent\">3. Grant of Patent License</a></b>.\nSubject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n</p>\n<p><b><a name=\"redistribution\">4. Redistribution</a></b>.\nYou may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n</p><ol type=\"a\">\n<li>You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n<br> <br></li>\n\n<li>You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n<br> <br></li>\n\n<li>You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n<br> <br></li>\n\n<li>If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.</li>\n\n</ol>\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n<p><b><a name=\"contributions\">5. Submission of Contributions</a></b>.\nUnless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n</p>\n<p><b><a name=\"trademarks\">6. Trademarks</a></b>.\nThis License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n</p>\n<p><b><a name=\"no-warranty\">7. Disclaimer of Warranty</a></b>.\nUnless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n</p>\n<p><b><a name=\"no-liability\">8. Limitation of Liability</a></b>.\nIn no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n</p>\n<p><b><a name=\"additional\">9. Accepting Warranty or Additional Liability</a></b>.\nWhile redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n</p>\n\n<a name=\"licenses\"></a>\n<h2>License Information regarding Bundled Third-Party Software</h2>\n\n<p>The following third party software is distributed with Google Web\nToolkit and is provided under other licenses and/or has source available\nfrom other locations.  Where \"gwt-dev.jar\" is listed, substitute in the\nname of the jar corresponding to your platform, e.g. \"gwt-dev-linux.jar\".\n</p>\n\n<table class=\"licenses\">\n  <tbody><tr>\n    <th>Package</th>\n    <th>License</th>\n    <th>Source Code Availability</th>\n  </tr>\n  <tr>\n    <td class=\"package\">Apache Tomcat</td>\n    <td class=\"license\">Apache License v. 2.0 (above)</td>\n    <td class=\"location\"><a href=\"http://tomcat.apache.org/\">tomcat.apache.org</a>; modifications are at org/apache/tomcat/ within gwt-dev.jar</td>\n  </tr>\n  <tr class=\"even\">\n    <td class=\"package\">Apache Tapestry</td>\n    <td class=\"license\">Apache License v. 2.0 (above)</td>\n    <td class=\"location\"><a href=\"http://tapestry.apache.org/\">tapestry.apache.org</a></td>\n  </tr>\n  <tr>\n    <td class=\"package\">ASM 3.1</td>\n    <td class=\"license\">(<a href=\"http://asm.objectweb.org/license.html\">custom</a>)</td>\n    <td class=\"location\">com/google/gwt/dev/asm/ within gwt-dev.jar</td>\n  </tr>\n  <tr>\n    <td class=\"package\">Eclipse Java Development Tools (JDT)</td>\n    <td class=\"license\"><a href=\"http://www.eclipse.org/legal/epl-v10.html\">Eclipse Public License v. 1.0</a></td>\n    <td class=\"location\"><a href=\"http://archive.eclipse.org/eclipse/downloads/drops/R-3.3.1-200709211145/download.php?dropFile=eclipse-JDT-SDK-3.3.1.zip\">eclipse.org</a></td>\n  </tr>\n  <tr class=\"even\">\n    <td class=\"package\">Eclipse Standard Widget Toolkit (SWT)</td>\n    <td class=\"license\"><a href=\"http://www.eclipse.org/legal/epl-v10.html\">Eclipse Public License v. 1.0</a></td>\n    <td class=\"location\">\n    <a href=\"http://download.eclipse.org/eclipse/downloads/drops/R-3.2.1-200609210945/download.php?dropFile=swt-3.2.1-gtk-linux-x86.zip\">Linux</a>,\n    <a href=\"http://download.eclipse.org/eclipse/downloads/drops/R-3.2.1-200609210945/download.php?dropFile=swt-3.2.1-win32-win32-x86.zip\">Windows</a>, and\n    <a href=\"http://download.eclipse.org/eclipse/downloads/drops/R-3.2.1-200609210945/download.php?dropFile=swt-3.2.1-carbon-macosx.zip\">Mac</a>;\n    modifications are at org/eclipse/swt/ within gwt-dev.jar\n    </td>\n  </tr>\n  <tr>\n    <td class=\"package\">Jetty 6.1.11</td>\n    <td class=\"license\">Apache License v. 2.0 (above)</td>\n    <td class=\"location\"><a href=\"http://mortbay.org/jetty/\">mortbay.org/jetty</a></td>\n  </tr>\n  <tr class=\"even\">\n    <td class=\"package\">Mozilla Rhino</td>\n    <td class=\"license\"><a href=\"http://www.mozilla.org/MPL/MPL-1.1.txt\">Mozilla Public License v. 1.1</a></td>\n    <td class=\"location\">com/google/gwt/dev/js/rhino/ within gwt-dev.jar</td>\n  </tr>\n  <tr>\n    <td class=\"package\">Mozilla 1.7.12 (Linux only)</td>\n    <td class=\"license\"><a href=\"http://www.mozilla.org/MPL/MPL-1.1.txt\">Mozilla Public License v. 1.1</a></td>\n    <td class=\"location\"><a href=\"http://developer.mozilla.org/en/docs/Download_Mozilla_Source_Code\">mozilla.org</a></td>\n  </tr>\n  <tr class=\"even\">\n    <td class=\"package\">Protobuf</td>\n    <td class=\"license\"><a href=\"http://www.opensource.org/licenses/bsd-license.php\">New BSD License</a></td>\n    <td class=\"location\">\n      <a href=\"http://code.google.com/p/protobuf/source/checkout\">code.google.com/p/protobuf</a>;\n      binary modifications consist of package-rebasing. The rebased classes are rooted at com/google/gwt/dev/protobuf/ within gwt-dev.jar\n    </td>\n  </tr>\n</tbody></table>\n\n</div> \n\n</body></html>\n"
  },
  {
    "path": "gwt/gwt-2.8.3/about.html",
    "content": "<html>\n\n   <head>\n      <title>Google Web Toolkit 2.8.3</title>\n      <style>\n         body {\n            background-color: white;\n            color: black;\n            font-family: Arial, sans-serif;\n            font-size: 10pt;\n            margin: 20px;\n            overflow: hidden;\n         }\n\n         #title {\n            font-size: 14pt;\n            font-weight: bold;\n         }\n\n         #title a {\n            text-decoration: none;\n         }\n\n         #version {\n            font-size: 8pt;\n            font-weight: bold;\n            text-align: right;\n         }\n         \n         #intro {\n            margin: 10px 0px 10px 0px ;\n         }\n         \n         #attribs {\n         }\n         \n      </style>\n   </head>\n\n   <body>\n      <table align=\"center\">\n         <tr>\n            <td id=\"title\"><a href=\"http://code.google.com/webtoolkit/\">Google Web Toolkit</a></td>\n         </tr>\n         <tr>\n            <td id=\"version\">Version 2.8.3 <br> (Git revision 0d0b5d352)</td>\n         </tr>\n      </table>\n\n      <hr/>\n\n      <div id=\"intro\">\n         Copyright &copy; 2009 <a href=\"http://www.google.com/\">Google Inc.</a>\n         All rights reserved.  \n         All other product, service names, brands, or trademarks, are the property of their respective owners.\n      </div>\n\n      <div id=\"attribs\">\n         This product includes software developed by\n         <ul>\n            <li>\n                The <a href=\"http://www.apache.org/\">Apache Software Foundation</a>\n                <ul>\n                    <li><a href=\"http://tomcat.apache.org/\">Tomcat</a> (with modifications)</li>\n                    <li><a href=\"http://tapestry.apache.org/\">Tapestry</a></li>\n                </ul\n            </li>\n            <li>\n                The <a href=\"http://www.eclipse.org/\">Eclipse Foundation</a>\n                <ul>\n                    <li><a href=\"http://www.eclipse.org/jdt/\">Java Development Tools</a></li>\n                    <li><a href=\"http://www.eclipse.org/swt/\">Standard Widget Toolkit</a> (with modifications)</li>\n                </ul\n            </li>\n            <li>The <a href=\"http://www.mortbay.com/\">Mort Bay Consulting</a>\n              <ul>\n                <li><a href=\"http://mortbay.org/jetty/\">Jetty 6.1.11</a> </li>\n              </ul>\n            </li>\n            <li>\n                The <a href=\"http://www.mozilla.org/\">Mozilla Foundation</a>\n                <ul>\n                    <li><a href=\"http://www.mozilla.org/releases/mozilla1.7.12/\">Mozilla 1.7.12</a></li>\n                    <li><a href=\"http://www.mozilla.org/rhino/\">Rhino</a> (with modifications)</li>\n                </ul\n            </li>\n            <li>The <a href=\"http://www.objectweb.org/\">ObjectWeb</a>\n              <ul>\n                <li><a href=\"http://asm.objectweb.org/\">ASM</a> (with modifications)</li>\n              </ul>\n            </li>\n            <li>The <a href=\"http://openqa.org/\">OpenQA Project</a>\n              <ul>\n                <li><a href=\"http://selenium-rc.openqa.org/\">Selenium-RC</a> </li>\n              </ul>\n            </li>\n            <li>The <a href=\"http://code.google.com/p/protobuf/\">Protobuf Project</a> (with modifications)</li>\n         </ul>\n         For source availability and license information see COPYING.html.\n\n      </div>\n      \n   </body>\n\n</html>\n"
  },
  {
    "path": "gwt/gwt-2.8.3/about.txt",
    "content": "GWT 2.8.3\n(git revision 0d0b5d352)\nCopyright (c) Google, Inc. 2009.  All rights reserved.\nVisit http://www.gwtproject.org.\n\nThis product includes software developed by:\n - The Apache Software Foundation (http://www.apache.org/).\n   - Tomcat (http://tomcat.apache.org/) with modifications\n   - Tapestry (http://tapestry.apache.org/)\n - The Eclipse Foundation (http://www.eclipse.org/).\n   - Java Development Tools (http://www.eclipse.org/jdt/)\n   - Standard Widget Toolkit (http://www.eclipse.org/swt/) with modifications\n - Mort Bay Consulting (http://www.mortbay.com/)\n   - Jetty 6.1.11 (http://mortbay.org/jetty/)\n - The Mozilla Foundation (http://www.mozilla.org/).\n   - Mozilla 1.7.12 (http://www.mozilla.org/releases/mozilla1.7.12/)\n   - Rhino (http://www.mozilla.org/rhino/) with modifications\n - ObjectWeb (http://www.objectweb.org/)\n   - ASM (http://asm.objectweb.org/) with modifications\n - The OpenQA Project (http://openqa.org/)\n   - Selenium-RC (http://selenium-rc.openqa.org/)\n - The Protobuf Project (http://code.google.com/p/protobuf/) with modifications\n\nFor source availability and license information see COPYING.\n"
  },
  {
    "path": "gwt/gwt-2.8.3/gwt-module.dtd",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--                                                                        -->\n<!-- Copyright 2008 Google Inc.                                             -->\n<!-- Licensed under the Apache License, Version 2.0 (the \"License\"); you    -->\n<!-- may not use this file except in compliance with the License. You may   -->\n<!-- may obtain a copy of the License at                                    -->\n<!--                                                                        -->\n<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->\n<!--                                                                        -->\n<!-- Unless required by applicable law or agreed to in writing, software    -->\n<!-- distributed under the License is distributed on an \"AS IS\" BASIS,      -->\n<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->\n<!-- implied. License for the specific language governing permissions and   -->\n<!-- limitations under the License.                                         -->\n\n<!-- The module root element -->\n<!ELEMENT module (inherits | source | public | super-source | entry-point | \n  stylesheet | script | servlet | replace-with | generate-with |\n  define-property | extend-property | set-property | set-property-fallback |\n  clear-configuration-property | define-configuration-property |\n  extend-configuration-property | set-configuration-property |\n  property-provider | define-linker | add-linker | collapse-all-properties |\n  collapse-property)*>\n<!ATTLIST module\n\trename-to CDATA #IMPLIED\n>\n<!-- Inherit the contents of another module -->\n<!ELEMENT inherits EMPTY>\n<!ATTLIST inherits\n\tname CDATA #REQUIRED\n>\n<!-- Specify the source path, relative to the classpath location of the module descriptor -->\n<!ELEMENT source (include | exclude)*>\n<!ATTLIST source\n\tpath CDATA #REQUIRED\n\tincludes CDATA #IMPLIED\n\texcludes CDATA #IMPLIED\n\tdefaultexcludes (yes | no) \"yes\"\n\tcasesensitive (true | false) \"true\"\n>\n<!-- Specify the public resource path, relative to the classpath location of the module descriptor -->\n<!ELEMENT public (include | exclude)*>\n<!ATTLIST public\n\tpath CDATA #REQUIRED\n\tincludes CDATA #IMPLIED\n\texcludes CDATA #IMPLIED\n\tdefaultexcludes (yes | no) \"yes\"\n\tcasesensitive (true | false) \"true\"\n>\n<!-- Specify a source path that rebases subpackages into the root namespace -->\n<!ELEMENT super-source (include | exclude)*>\n<!ATTLIST super-source\n\tpath CDATA #REQUIRED\n\tincludes CDATA #IMPLIED\n\texcludes CDATA #IMPLIED\n\tdefaultexcludes (yes | no) \"yes\"\n\tcasesensitive (true | false) \"true\"\n>\n<!ELEMENT include EMPTY>\n<!ATTLIST include\n\tname CDATA #REQUIRED\n>\n<!ELEMENT exclude EMPTY>\n<!ATTLIST exclude\n\tname CDATA #REQUIRED\n>\n\n<!-- Define a module entry point -->\n<!ELEMENT entry-point EMPTY>\n<!ATTLIST entry-point\n\tclass CDATA #REQUIRED\n>\n\n<!-- Preload a stylesheet before executing the GWT application -->\n<!ELEMENT stylesheet EMPTY>\n<!ATTLIST stylesheet\n\tsrc CDATA #REQUIRED\n>\n<!-- Preload an external JavaScript file before executing the GWT application -->\n<!ELEMENT script (#PCDATA)>\n<!ATTLIST script\n\tsrc CDATA #REQUIRED\n>\n<!-- Map a named servlet class to a module-relative path in hosted mode -->\n<!ELEMENT servlet EMPTY>\n<!ATTLIST servlet \n\tpath CDATA #REQUIRED\n\tclass CDATA #REQUIRED\n>\n\n<!-- Adds a Linker to the compilation process -->\n<!ELEMENT add-linker EMPTY>\n<!-- A comma-separated list of linker names -->\n<!ATTLIST add-linker\n\tname CDATA #REQUIRED\n>\n\n<!-- Defines a Linker type to package compiler output -->\n<!ELEMENT define-linker EMPTY>\n<!ATTLIST define-linker\n\tclass CDATA #REQUIRED\n\tname CDATA #REQUIRED\n>\n\n<!--                 ^^^ Commonly-used elements ^^^                -->\n<!--                VVV Deferred binding elements VVV              -->\n\n<!-- All possible predicates -->\n<!ENTITY % predicates \"when-property-is | when-type-assignable | when-type-is | when-linker-added | all | any | none\">\n<!-- Define a property and allowable values (comma-separated identifiers) -->\n<!ELEMENT define-property EMPTY>\n<!ATTLIST define-property\n\tname CDATA #REQUIRED\n\tvalues CDATA #REQUIRED\n>\n<!-- Define a configuration property -->\n<!ELEMENT define-configuration-property EMPTY>\n<!ATTLIST define-configuration-property\n\tname CDATA #REQUIRED\n\tis-multi-valued CDATA #REQUIRED\n>\n<!-- Set the value of a previously-defined property -->\n<!ELEMENT set-property (%predicates;)*>\n<!ATTLIST set-property\n\tname CDATA #REQUIRED\n\tvalue CDATA #REQUIRED\n>\n<!-- Set the value of a previously-defined property -->\n<!ELEMENT set-property-fallback EMPTY>\n<!ATTLIST set-property-fallback\n\tname CDATA #REQUIRED\n\tvalue CDATA #REQUIRED\n>\n<!-- Set the value of a configuration property -->\n<!ELEMENT set-configuration-property EMPTY>\n<!ATTLIST set-configuration-property\n\tname CDATA #REQUIRED\n\tvalue CDATA #REQUIRED\n>\n<!-- Add additional allowable values to a property -->\n<!ELEMENT extend-property EMPTY>\n<!ATTLIST extend-property\n\tname CDATA #REQUIRED\n\tvalues CDATA #REQUIRED\n\tfallback-value CDATA #IMPLIED\n>\n<!-- Collapse property values to produce soft permutations -->\n<!ELEMENT collapse-property EMPTY>\n<!ATTLIST collapse-property\n\tname CDATA #REQUIRED\n\tvalues CDATA #REQUIRED\n>\n<!-- Collapse all deferred-binding properties to produce a single permutation -->\n<!ELEMENT collapse-all-properties EMPTY>\n<!ATTLIST collapse-all-properties\n\tvalue (true | false) \"true\"\n>\n<!-- Add additional allowable values to a configuration property -->\n<!ELEMENT extend-configuration-property EMPTY>\n<!ATTLIST extend-configuration-property\n\tname CDATA #REQUIRED\n\tvalue CDATA #REQUIRED\n>\n<!-- Remove all allowable values from a configuration property -->\n<!ELEMENT clear-configuration-property EMPTY>\n<!ATTLIST clear-configuration-property\n\tname CDATA #REQUIRED\n>\n<!-- Define a JavaScript fragment that will return the value for the named property at runtime -->\n<!ELEMENT property-provider (#PCDATA)>\n<!ATTLIST property-provider\n\tname CDATA #REQUIRED\n\tgenerator CDATA #IMPLIED\n>\n<!-- Deferred binding assignment to substitute a named class -->\n<!ELEMENT replace-with (%predicates;)*>\n<!ATTLIST replace-with\n\tclass CDATA #REQUIRED\n>\n<!-- Deferred binding assignment to substitute a generated class -->\n<!ELEMENT generate-with (%predicates;)*>\n<!ATTLIST generate-with\n\tclass CDATA #REQUIRED\n>\n<!-- Deferred binding predicate that is true when a named property has a given value-->\n<!ELEMENT when-property-is EMPTY>\n<!ATTLIST when-property-is\n\tname CDATA #REQUIRED\n\tvalue CDATA #REQUIRED\n>\n<!-- Deferred binding predicate that is true for types in the type system that are assignable to the specified type -->\n<!ELEMENT when-type-assignable EMPTY>\n<!ATTLIST when-type-assignable\n\tclass CDATA #REQUIRED\n>\n<!-- Deferred binding predicate that is true for exactly one type in the type system -->\n<!ELEMENT when-type-is EMPTY>\n<!ATTLIST when-type-is\n\tclass CDATA #REQUIRED\n>\n<!-- Deferred binding predicate that is true when there are linker with such name -->\n<!ELEMENT when-linker-added EMPTY>\n<!ATTLIST when-linker-added\n\tname CDATA #REQUIRED\n>\n<!-- Predicate that ANDs all child conditions -->\n<!ELEMENT all (%predicates;)*>\n<!-- Predicate that ORs all child conditions -->\n<!ELEMENT any (%predicates;)*>\n<!-- Predicate that NANDs all child conditions -->\n<!ELEMENT none (%predicates;)*>\n"
  },
  {
    "path": "gwt/gwt-2.8.3/i18nCreator",
    "content": "#!/bin/sh\nHOMEDIR=`dirname $0`;\njava -cp $HOMEDIR/gwt-user.jar:$HOMEDIR/gwt-dev.jar com.google.gwt.i18n.tools.I18NCreator \"$@\";\n"
  },
  {
    "path": "gwt/gwt-2.8.3/i18nCreator.cmd",
    "content": "@java -cp \"%~dp0\\gwt-user.jar;%~dp0\\gwt-dev.jar\" com.google.gwt.i18n.tools.I18NCreator %*\n"
  },
  {
    "path": "gwt/gwt-2.8.3/release_notes.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n      <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n      <title>Google Web Toolkit Release Notes</title>\n      \n      <style>\n         body {\n            background-color: white;\n            color: black;\n            font-family: Arial, sans-serif;\n            font-size: large;\n            margin: 20px;\n         }\n         a {\n            color: blue;\n            font-weight: bold;\n         }\n      </style></head>\n\n   <body>\n      <h1 style=\"margin-bottom: 0;\">Google Web Toolkit Release Notes</h1>\n      <p>\n          Release notes can be found on the <a href=\"http://www.gwtproject.org/release-notes.html\">GWT project hosting website</a>.\n      </p>\n   </body>\n</html>\n"
  },
  {
    "path": "gwt/gwt-2.8.3/webAppCreator",
    "content": "#!/bin/sh\nHOMEDIR=`dirname $0`;\njava -cp $HOMEDIR/gwt-user.jar:$HOMEDIR/gwt-dev.jar com.google.gwt.user.tools.WebAppCreator \"$@\";\n"
  },
  {
    "path": "gwt/gwt-2.8.3/webAppCreator.cmd",
    "content": "@java -cp \"%~dp0\\gwt-user.jar;%~dp0\\gwt-dev.jar\" com.google.gwt.user.tools.WebAppCreator %*\n"
  },
  {
    "path": "reproducible-test.sh",
    "content": "#!/bin/bash\n\nant dist\nhash1=`sha256sum Peergos.jar`\nant dist\nhash2=`sha256sum Peergos.jar`\nif [[ $hash1 == $hash2 ]];\nthen\n    exit 0\nelse\n    exit -1\nfi\n"
  },
  {
    "path": "scripts/ensure-compile.sh",
    "content": "#!/bin/bash\n\n##\n## To set this as the pre-commit hook do:\n##\n## ln -s $(git rev-parse --show-toplevel)/scripts/ensure-compile.sh $(git rev-parse --show-toplevel)/.git/hooks/pre-commit\n##\n\necho \"********************************\"\necho \" Checking if project compiles..\"\necho \"********************************\"\n\nif ! ant compile;\nthen \n    echo \"Project failed to compile\"\n    exit 1\nfi\n\n"
  },
  {
    "path": "src/peergos/Peergos.gwt.xml",
    "content": "<module rename-to='peergoslib'>\n\n  <set-property name=\"compiler.useSourceMaps\" value=\"false\"/>\n  <set-property name=\"user.agent\" value=\"safari\"/>\n  \n  <entry-point class='peergos.client.Start'/>\n  <public path=\"gwt/resources\"/>\n  <super-source path=\"gwt/emu\" />\n  <source path=\"gwt\">\n    <exclude name=\"**/emu/**\" />\n  </source>\n  <source path='client'/>\n  <source path='shared'/>\n  \t<add-linker name=\"sso\" />\n</module>\n"
  },
  {
    "path": "src/peergos/client/ConsolePrintStream.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage peergos.client;\n\nimport java.io.OutputStream;\nimport java.io.PrintStream;\n\n/** Print stream for GWT that prints to the browser console.\n * \n * @author Stefan Haustein */\npublic class ConsolePrintStream extends PrintStream {\n\n\tStringBuilder buf = new StringBuilder();\n\n\tpublic ConsolePrintStream () {\n\t\tsuper((OutputStream)null);\n\t}\n\n\tpublic void print (String s) {\n\n\t\twhile (true) {\n\t\t\tint cut = s.indexOf('\\n');\n\t\t\tif (cut == -1) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tprintln(s.substring(0, cut));\n\t\t\ts = s.substring(cut + 1);\n\t\t}\n\n\t\tbuf.append(s);\n\t}\n\n\tpublic native void consoleLog (String msg) /*-{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (window.console) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twindow.console.log(msg);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdocument.title = \"LOG:\" + msg;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}-*/;\n\n\tpublic void print (char c) {\n\t\tif (c == '\\n') {\n\t\t\tprintln(\"\");\n\t\t} else {\n\t\t\tbuf.append(c);\n\t\t}\n\t}\n\n\tpublic void println () {\n\t\tprintln(\"\");\n\t}\n\n\t@Override\n\tpublic void println (String s) {\n\t\tbuf.append(s);\n\t\tconsoleLog(buf.toString());\n\t\tbuf.setLength(0);\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/client/JsUtil.java",
    "content": "package peergos.client;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.login.mfa.MultiFactorAuthResponse;\nimport peergos.shared.login.mfa.WebauthnResponse;\nimport peergos.shared.util.Either;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/** Utility methods to handle conversion between types necessary for Javascript interop\n *\n */\npublic class JsUtil {\n\n    @JsMethod\n    public static LocalDateTime fromUtcMillis(long millis) {\n        return LocalDateTime.ofEpochSecond(millis/1_000, ((int)(millis % 1000)) * 1_000_000, ZoneOffset.UTC);\n    }\n\n    @JsMethod\n    public static CborObject fromByteArray(byte[] cbor) {\n        return CborObject.fromByteArray(cbor);\n    }\n\n    @JsMethod\n    public static <T> List<T> asList(T[] array) {\n        return Arrays.asList(array);\n    }\n\n    @JsMethod\n    public static <T> Set<T> asSet(T[] array) {\n        return Arrays.asList(array).stream().collect(Collectors.toSet());\n    }\n\n    @JsMethod\n    public static <T> List<T> emptyList() {\n        return Collections.emptyList();\n    }\n\n    @JsMethod\n    public static <T> Optional<T> emptyOptional() {\n        return Optional.empty();\n    }\n\n    @JsMethod\n    public static <T> Optional<T> optionalOf(T of) {\n        return Optional.of(of);\n    }\n\n\n    @JsMethod\n    public static LocalDateTime now() {\n        return LocalDateTime.now();\n    }\n\n    @JsMethod\n    public static MultiFactorAuthResponse generateAuthResponse(byte[] credentialId, String code) {\n        return new MultiFactorAuthResponse(credentialId, Either.a(code));\n    }\n\n    @JsMethod\n    public static MultiFactorAuthResponse generateWebAuthnResponse(byte[] credentialId, byte[] authenticatorData,\n                                                                   byte[] clientDataJson, byte[] signature) {\n        WebauthnResponse resp = new WebauthnResponse(authenticatorData, clientDataJson, signature);\n        return new MultiFactorAuthResponse(credentialId, Either.b(resp));\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/client/PathUtils.java",
    "content": "package peergos.client;\n\nimport jsinterop.annotations.JsMethod;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class PathUtils {\n\n\n    @JsMethod\n    public static Path getFileName(Path another) {\n        return another.getFileName();\n    }\n\n    @JsMethod\n    public static Path subpath(Path another, int from, int to) {\n        return another.subpath(from, to);\n    }\n\n    @JsMethod\n    public static int getNameCount(Path another) {\n        return another.getNameCount();\n    }\n\n    @JsMethod\n    public static Path getParent(Path another) {\n        return another.getParent();\n    }\n\n    @JsMethod\n    public static Path directoryToPath(String[] parts) {\n        if (parts == null || parts.length == 0) {\n            throw new IllegalArgumentException(\"Invalid params\");\n        }else if (parts.length == 1) {\n            return Paths.get(parts[0]);\n        } else {\n            List<String> pathFragments = Stream.of(parts).skip(1).collect(Collectors.toList());\n            String[] remainder = pathFragments.toArray(new String[1]);\n            return Paths.get(parts[0], remainder);\n        }\n    }\n\n    @JsMethod\n    public static Path toPath(String[] parts, String filename) {\n        if (parts == null || parts.length == 0 || filename == null) {\n            throw new IllegalArgumentException(\"Invalid params\");\n        }else if (parts.length == 1) {\n            return Paths.get(parts[0], filename);\n        } else {\n            List<String> pathFragments = Stream.of(parts).skip(1).collect(Collectors.toList());\n            pathFragments.add(filename);\n            String[] remainder = pathFragments.toArray(new String[1]);\n            return Paths.get(parts[0], remainder);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/client/Start.java",
    "content": "package peergos.client;\n\nimport com.google.gwt.core.client.EntryPoint;\n\npublic class Start implements EntryPoint {\n\tpublic void onModuleLoad() {\n\t\tSystem.setOut(new ConsolePrintStream());\n\t\tSystem.setErr(new ConsolePrintStream());\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/avian/Utf8.java",
    "content": "/* Copyright (c) 2010, Avian Contributors\r\n\r\n   Permission to use, copy, modify, and/or distribute this software\r\n   for any purpose with or without fee is hereby granted, provided\r\n   that the above copyright notice and this permission notice appear\r\n   in all copies.\r\n\r\n   There is NO WARRANTY for this software.  See license.txt for\r\n   details. */\r\n\r\npackage avian;\r\n\r\nimport java.io.ByteArrayOutputStream;\r\n\r\npublic class Utf8 {\r\n\tpublic static boolean test (Object data) {\r\n\t\tif (!(data instanceof byte[])) return false;\r\n\t\tbyte[] b = (byte[])data;\r\n\t\tfor (int i = 0; i < b.length; ++i) {\r\n\t\t\tif (((int)b[i] & 0x080) != 0) return true;\r\n\t\t}\r\n\t\treturn false;\r\n\t}\r\n\r\n\tpublic static byte[] encode (char[] s16, int offset, int length) {\r\n\t\tByteArrayOutputStream buf = new ByteArrayOutputStream();\r\n\t\tfor (int i = offset; i < offset + length; ++i) {\r\n\t\t\tchar c = s16[i];\r\n\t\t\tif (c == '\\u0000') { // null char\r\n\t\t\t\tbuf.write(0);\r\n\t\t\t\tbuf.write(0);\r\n\t\t\t} else if (c < 0x080) { // 1 byte char\r\n\t\t\t\tbuf.write(c);\r\n\t\t\t} else if (c < 0x0800) { // 2 byte char\r\n\t\t\t\tbuf.write(0x0c0 | (c >>> 6));\r\n\t\t\t\tbuf.write(0x080 | (c & 0x03f));\r\n\t\t\t} else { // 3 byte char\r\n\t\t\t\tbuf.write(0x0e0 | ((c >>> 12) & 0x0f));\r\n\t\t\t\tbuf.write(0x080 | ((c >>> 6) & 0x03f));\r\n\t\t\t\tbuf.write(0x080 | (c & 0x03f));\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn buf.toByteArray();\r\n\t}\r\n\r\n\tpublic static Object decode (byte[] s8, int offset, int length) {\r\n\t\tObject buf = new byte[length];\r\n\t\tboolean isMultiByte = false;\r\n\t\tint i = offset, j = 0;\r\n\t\twhile (i < offset + length) {\r\n\t\t\tint x = s8[i++];\r\n\t\t\tif ((x & 0x080) == 0x0) { // 1 byte char\r\n\t\t\t\tif (x == 0) ++i; // 2 byte null char\r\n\t\t\t\tcram(buf, j++, x);\r\n\t\t\t} else if ((x & 0x0e0) == 0x0c0) { // 2 byte char\r\n\t\t\t\tif (!isMultiByte) {\r\n\t\t\t\t\tbuf = widen(buf, j, length - 1);\r\n\t\t\t\t\tisMultiByte = true;\r\n\t\t\t\t}\r\n\t\t\t\tint y = s8[i++];\r\n\t\t\t\tcram(buf, j++, ((x & 0x1f) << 6) | (y & 0x3f));\r\n\t\t\t} else if ((x & 0x0f0) == 0x0e0) { // 3 byte char\r\n\t\t\t\tif (!isMultiByte) {\r\n\t\t\t\t\tbuf = widen(buf, j, length - 2);\r\n\t\t\t\t\tisMultiByte = true;\r\n\t\t\t\t}\r\n\t\t\t\tint y = s8[i++];\r\n\t\t\t\tint z = s8[i++];\r\n\t\t\t\tcram(buf, j++, ((x & 0xf) << 12) | ((y & 0x3f) << 6) | (z & 0x3f));\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn trim(buf, j);\r\n\t}\r\n\r\n\tpublic static char[] decode16 (byte[] s8, int offset, int length) {\r\n\t\tObject decoded = decode(s8, offset, length);\r\n\t\tif (decoded instanceof char[]) return (char[])decoded;\r\n\t\treturn (char[])widen(decoded, length, length);\r\n\t}\r\n\r\n\tprivate static void cram (Object data, int index, int val) {\r\n\t\tif (data instanceof byte[])\r\n\t\t\t((byte[])data)[index] = (byte)val;\r\n\t\telse\r\n\t\t\t((char[])data)[index] = (char)val;\r\n\t}\r\n\r\n\tprivate static Object widen (Object data, int length, int capacity) {\r\n\t\tbyte[] src = (byte[])data;\r\n\t\tchar[] result = new char[capacity];\r\n\t\tfor (int i = 0; i < length; ++i)\r\n\t\t\tresult[i] = (char)((int)src[i] & 0x0ff);\r\n\t\treturn result;\r\n\t}\r\n\r\n\tprivate static Object trim (Object data, int length) {\r\n\t\tif (data instanceof byte[]) return data;\r\n\t\tif (((char[])data).length == length) return data;\r\n\t\tchar[] result = new char[length];\r\n\t\tSystem.arraycopy(data, 0, result, 0, length);\r\n\t\treturn result;\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/com/badlogic/gdx/utils/Base64Coder.java",
    "content": "//Copyright 2003-2010 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland\r\n// www.source-code.biz, www.inventec.ch/chdh\r\n//\r\n// This module is multi-licensed and may be used under the terms\r\n// of any of the following licenses:\r\n//\r\n//  EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal\r\n//  LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html\r\n//  GPL, GNU General Public License, V2 or later, http://www.gnu.org/licenses/gpl.html\r\n//  AL, Apache License, V2.0 or later, http://www.apache.org/licenses\r\n//  BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php\r\n//\r\n// Please contact the author if you need another license.\r\n// This module is provided \"as is\", without warranties of any kind.\r\n/**\r\n * A Base64 encoder/decoder.\r\n *\r\n * <p>\r\n * This class is used to encode and decode data in Base64 format as described in RFC 1521.\r\n *\r\n * <p>\r\n * Project home page: <a href=\"http://www.source-code.biz/base64coder/java/\">www.source-code.biz/base64coder/java</a><br>\r\n * Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>\r\n * Multi-licensed: EPL / LGPL / GPL / AL / BSD.\r\n *\r\n * @author Christian d'Heureuse\r\n * @author vaxquis\r\n */\r\n\r\npackage com.badlogic.gdx.utils;\r\n\r\npublic class Base64Coder {\r\n\tpublic static class CharMap {\r\n\t\tprotected final char[] encodingMap = new char[64];\r\n\t\tprotected final byte[] decodingMap = new byte[128];\r\n\r\n\t\tpublic CharMap (char char63, char char64) {\r\n\t\t\tint i = 0;\r\n\t\t\tfor (char c = 'A'; c <= 'Z'; c++) {\r\n\t\t\t\tencodingMap[i++] = c;\r\n\t\t\t}\r\n\t\t\tfor (char c = 'a'; c <= 'z'; c++) {\r\n\t\t\t\tencodingMap[i++] = c;\r\n\t\t\t}\r\n\t\t\tfor (char c = '0'; c <= '9'; c++) {\r\n\t\t\t\tencodingMap[i++] = c;\r\n\t\t\t}\r\n\t\t\tencodingMap[i++] = char63;\r\n\t\t\tencodingMap[i++] = char64;\r\n\t\t\tfor (i = 0; i < decodingMap.length; i++) {\r\n\t\t\t\tdecodingMap[i] = -1;\r\n\t\t\t}\r\n\t\t\tfor (i = 0; i < 64; i++) {\r\n\t\t\t\tdecodingMap[encodingMap[i]] = (byte)i;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tpublic byte[] getDecodingMap () {\r\n\t\t\treturn decodingMap;\r\n\t\t}\r\n\r\n\t\tpublic char[] getEncodingMap () {\r\n\t\t\treturn encodingMap;\r\n\t\t}\r\n\t}\r\n\r\n\t// The line separator string of the operating system.\r\n\tprivate static final String systemLineSeparator = \"\\n\";\r\n\r\n\tpublic static final CharMap regularMap = new CharMap('+', '/'), urlsafeMap = new CharMap('-', '_');\r\n\r\n\t/** Encodes a string into Base64 format. No blanks or line breaks are inserted.\r\n\t * @param s A String to be encoded.\r\n\t * @return A String containing the Base64 encoded data. */\r\n\tpublic static String encodeString (String s) {\r\n\t\treturn encodeString(s, false);\r\n\t}\r\n\r\n\tpublic static String encodeString (String s, boolean useUrlsafeEncoding) {\r\n\t\treturn new String(encode(s.getBytes(), useUrlsafeEncoding ? urlsafeMap.encodingMap : regularMap.encodingMap));\r\n\t}\r\n\r\n\t/** Encodes a byte array into Base 64 format and breaks the output into lines of 76 characters. This method is compatible with\r\n\t * <code>sun.misc.BASE64Encoder.encodeBuffer(byte[])</code>.\r\n\t * @param in An array containing the data bytes to be encoded.\r\n\t * @return A String containing the Base64 encoded data, broken into lines. */\r\n\tpublic static String encodeLines (byte[] in) {\r\n\t\treturn encodeLines(in, 0, in.length, 76, systemLineSeparator, regularMap.encodingMap);\r\n\t}\r\n\r\n\tpublic static String encodeLines (byte[] in, int iOff, int iLen, int lineLen, String lineSeparator, CharMap charMap) {\r\n\t\treturn encodeLines(in, iOff, iLen, lineLen, lineSeparator, charMap.encodingMap);\r\n\t}\r\n\r\n\t/** Encodes a byte array into Base 64 format and breaks the output into lines.\r\n\t * @param in An array containing the data bytes to be encoded.\r\n\t * @param iOff Offset of the first byte in <code>in</code> to be processed.\r\n\t * @param iLen Number of bytes to be processed in <code>in</code>, starting at <code>iOff</code>.\r\n\t * @param lineLen Line length for the output data. Should be a multiple of 4.\r\n\t * @param lineSeparator The line separator to be used to separate the output lines.\r\n\t * @param charMap char map to use\r\n\t * @return A String containing the Base64 encoded data, broken into lines. */\r\n\tpublic static String encodeLines (byte[] in, int iOff, int iLen, int lineLen, String lineSeparator, char[] charMap) {\r\n\t\tint blockLen = (lineLen * 3) / 4;\r\n\t\tif (blockLen <= 0) {\r\n\t\t\tthrow new IllegalArgumentException();\r\n\t\t}\r\n\t\tint lines = (iLen + blockLen - 1) / blockLen;\r\n\t\tint bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length();\r\n\t\tStringBuilder buf = new StringBuilder(bufLen);\r\n\t\tint ip = 0;\r\n\t\twhile (ip < iLen) {\r\n\t\t\tint l = Math.min(iLen - ip, blockLen);\r\n\t\t\tbuf.append(encode(in, iOff + ip, l, charMap));\r\n\t\t\tbuf.append(lineSeparator);\r\n\t\t\tip += l;\r\n\t\t}\r\n\t\treturn buf.toString();\r\n\t}\r\n\r\n\t/** Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output.\r\n\t * @param in An array containing the data bytes to be encoded.\r\n\t * @return A character array containing the Base64 encoded data. */\r\n\tpublic static char[] encode (byte[] in) {\r\n\t\treturn encode(in, regularMap.encodingMap);\r\n\t}\r\n\r\n\tpublic static char[] encode (byte[] in, CharMap charMap) {\r\n\t\treturn encode(in, 0, in.length, charMap);\r\n\t}\r\n\r\n    public static char[] encode (byte[] in, char[] charMap) {\r\n\t\treturn encode(in, 0, in.length, charMap);\r\n\t}\r\n\r\n\t/** Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output.\r\n\t * @param in An array containing the data bytes to be encoded.\r\n\t * @param iLen Number of bytes to process in <code>in</code>.\r\n\t * @return A character array containing the Base64 encoded data. */\r\n\tpublic static char[] encode (byte[] in, int iLen) {\r\n\t\treturn encode(in, 0, iLen, regularMap.encodingMap);\r\n\t}\r\n\r\n\tpublic static char[] encode (byte[] in, int iOff, int iLen, CharMap charMap) {\r\n\t\treturn encode(in, iOff, iLen, charMap.encodingMap);\r\n\t}\r\n\r\n\t/** Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output.\r\n\t * @param in An array containing the data bytes to be encoded.\r\n\t * @param iOff Offset of the first byte in <code>in</code> to be processed.\r\n\t * @param iLen Number of bytes to process in <code>in</code>, starting at <code>iOff</code>.\r\n\t * @param charMap char map to use\r\n\t * @return A character array containing the Base64 encoded data. */\r\n\tpublic static char[] encode (byte[] in, int iOff, int iLen, char[] charMap) {\r\n\t\tint oDataLen = (iLen * 4 + 2) / 3; // output length without padding\r\n\t\tint oLen = ((iLen + 2) / 3) * 4; // output length including padding\r\n\t\tchar[] out = new char[oLen];\r\n\t\tint ip = iOff;\r\n\t\tint iEnd = iOff + iLen;\r\n\t\tint op = 0;\r\n\t\twhile (ip < iEnd) {\r\n\t\t\tint i0 = in[ip++] & 0xff;\r\n\t\t\tint i1 = ip < iEnd ? in[ip++] & 0xff : 0;\r\n\t\t\tint i2 = ip < iEnd ? in[ip++] & 0xff : 0;\r\n\t\t\tint o0 = i0 >>> 2;\r\n\t\t\tint o1 = ((i0 & 3) << 4) | (i1 >>> 4);\r\n\t\t\tint o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);\r\n\t\t\tint o3 = i2 & 0x3F;\r\n\t\t\tout[op++] = charMap[o0];\r\n\t\t\tout[op++] = charMap[o1];\r\n\t\t\tout[op] = op < oDataLen ? charMap[o2] : '=';\r\n\t\t\top++;\r\n\t\t\tout[op] = op < oDataLen ? charMap[o3] : '=';\r\n\t\t\top++;\r\n\t\t}\r\n\t\treturn out;\r\n\t}\r\n\r\n\t/** Decodes a string from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data.\r\n\t * @param s A Base64 String to be decoded.\r\n\t * @return A String containing the decoded data.\r\n\t * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */\r\n\tpublic static String decodeString (String s) {\r\n\t\treturn decodeString(s, false);\r\n\t}\r\n\r\n\tpublic static String decodeString (String s, boolean useUrlSafeEncoding) {\r\n\t\treturn new String(decode(s.toCharArray(), useUrlSafeEncoding ? urlsafeMap.decodingMap : regularMap.decodingMap));\r\n\t}\r\n\r\n    public static byte[] decodeLines (String s) {\r\n        return decodeLines(s, regularMap.decodingMap);\r\n    }\r\n\r\n    public static byte[] decodeLines (String s, CharMap inverseCharMap) {\r\n        return decodeLines(s, inverseCharMap.decodingMap);\r\n    }\r\n\r\n\t/** Decodes a byte array from Base64 format and ignores line separators, tabs and blanks. CR, LF, Tab and Space characters are\r\n\t * ignored in the input data. This method is compatible with <code>sun.misc.BASE64Decoder.decodeBuffer(String)</code>.\r\n\t * @param s A Base64 String to be decoded.\r\n     * @param inverseCharMap\r\n\t * @return An array containing the decoded data bytes.\r\n\t * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */\r\n\tpublic static byte[] decodeLines (String s, byte[] inverseCharMap) {\r\n\t\tchar[] buf = new char[s.length()];\r\n\t\tint p = 0;\r\n\t\tfor (int ip = 0; ip < s.length(); ip++) {\r\n\t\t\tchar c = s.charAt(ip);\r\n\t\t\tif (c != ' ' && c != '\\r' && c != '\\n' && c != '\\t') {\r\n\t\t\t\tbuf[p++] = c;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn decode(buf, 0, p, inverseCharMap);\r\n\t}\r\n\t\r\n\t/** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data.\r\n\t * @param s A Base64 String to be decoded.\r\n\t * @return An array containing the decoded data bytes.\r\n\t * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */\r\n\tpublic static byte[] decode (String s) {\r\n\t\treturn decode(s.toCharArray());\r\n\t}\r\n\r\n\t/** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data.\r\n\t * @param s A Base64 String to be decoded.\r\n\t * @param inverseCharMap\r\n\t * @return An array containing the decoded data bytes.\r\n\t * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */\r\n\tpublic static byte[] decode (String s, CharMap inverseCharMap) {\r\n\t\treturn decode(s.toCharArray(), inverseCharMap);\r\n\t}\r\n\r\n    public static byte[] decode (char[] in, byte[] inverseCharMap) {\r\n\t\treturn decode(in, 0, in.length, inverseCharMap);\r\n\t}\r\n\r\n\tpublic static byte[] decode (char[] in, CharMap inverseCharMap) {\r\n\t\treturn decode(in, 0, in.length, inverseCharMap);\r\n\t}\r\n\r\n\t/** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data.\r\n\t * @param in A character array containing the Base64 encoded data.\r\n\t * @return An array containing the decoded data bytes.\r\n\t * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */\r\n\tpublic static byte[] decode (char[] in) {\r\n\t\treturn decode(in, 0, in.length, regularMap.decodingMap);\r\n\t}\r\n\r\n\tpublic static byte[] decode (char[] in, int iOff, int iLen, CharMap inverseCharMap) {\r\n\t\treturn decode(in, iOff, iLen, inverseCharMap.decodingMap);\r\n\t}\r\n\r\n\t/** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data.\r\n\t * @param in A character array containing the Base64 encoded data.\r\n\t * @param iOff Offset of the first character in <code>in</code> to be processed.\r\n\t * @param iLen Number of characters to process in <code>in</code>, starting at <code>iOff</code>.\r\n\t * @param inverseCharMap charMap to use\r\n\t * @return An array containing the decoded data bytes.\r\n\t * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */\r\n\tpublic static byte[] decode (char[] in, int iOff, int iLen, byte[] inverseCharMap) {\r\n\t\tif (iLen % 4 != 0) {\r\n\t\t\tthrow new IllegalArgumentException(\"Length of Base64 encoded input string is not a multiple of 4.\");\r\n\t\t}\r\n\t\twhile (iLen > 0 && in[iOff + iLen - 1] == '=') {\r\n\t\t\tiLen--;\r\n\t\t}\r\n\t\tint oLen = (iLen * 3) / 4;\r\n\t\tbyte[] out = new byte[oLen];\r\n\t\tint ip = iOff;\r\n\t\tint iEnd = iOff + iLen;\r\n\t\tint op = 0;\r\n\t\twhile (ip < iEnd) {\r\n\t\t\tint i0 = in[ip++];\r\n\t\t\tint i1 = in[ip++];\r\n\t\t\tint i2 = ip < iEnd ? in[ip++] : 'A';\r\n\t\t\tint i3 = ip < iEnd ? in[ip++] : 'A';\r\n\t\t\tif (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) {\r\n\t\t\t\tthrow new IllegalArgumentException(\"Illegal character in Base64 encoded data.\");\r\n\t\t\t}\r\n\t\t\tint b0 = inverseCharMap[i0];\r\n\t\t\tint b1 = inverseCharMap[i1];\r\n\t\t\tint b2 = inverseCharMap[i2];\r\n\t\t\tint b3 = inverseCharMap[i3];\r\n\t\t\tif (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) {\r\n\t\t\t\tthrow new IllegalArgumentException(\"Illegal character in Base64 encoded data.\");\r\n\t\t\t}\r\n\t\t\tint o0 = (b0 << 2) | (b1 >>> 4);\r\n\t\t\tint o1 = ((b1 & 0xf) << 4) | (b2 >>> 2);\r\n\t\t\tint o2 = ((b2 & 3) << 6) | b3;\r\n\t\t\tout[op++] = (byte)o0;\r\n\t\t\tif (op < oLen) {\r\n\t\t\t\tout[op++] = (byte)o1;\r\n\t\t\t}\r\n\t\t\tif (op < oLen) {\r\n\t\t\t\tout[op++] = (byte)o2;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn out;\r\n\t}\r\n\r\n\t// Dummy constructor.\r\n\tprivate Base64Coder () {\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/com/badlogic/gdx/utils/Utf8Decoder.java",
    "content": "/*******************************************************************************\n * Copyright 2015 See AUTHORS file.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *   http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n ******************************************************************************/\n\n/*******************************************************************************\n * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>\n * \n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n *  IN THE SOFTWARE.\n ******************************************************************************/\n\npackage com.badlogic.gdx.utils;\n\n/** Utf8Decoder converts UTF-8 encoded bytes into characters properly handling buffer boundaries.\n *\n * This class is stateful and up to 4 calls to {@link #decode(byte)} may be needed before a character is appended to the char\n * buffer.\n *\n * The UTF-8 decoding is done by this class and no additional buffers are created. The UTF-8 code was inspired by\n * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/\n * \n * @author davebaol */\npublic class Utf8Decoder {\n\n\tprivate static final char REPLACEMENT = '\\ufffd';\n\tprivate static final int UTF8_ACCEPT = 0;\n\tprivate static final int UTF8_REJECT = 12;\n\n\t// This table maps bytes to character classes to reduce\n\t// the size of the transition table and create bitmasks.\n\tprivate static final byte[] BYTE_TABLE = {\n\t\t// @off - disable libgdx formatter\n\t\t 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n\t\t 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n\t\t 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n\t\t 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n\t\t 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,  9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,\n\t\t 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,  7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,\n\t\t 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,  2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,\n\t\t10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8\n\t\t// @on - enable libgdx formatter\n\t};\n\n\t// This is a transition table that maps a combination of a\n\t// state of the automaton and a character class to a state.\n\tprivate static final byte[] TRANSITION_TABLE = {\n\t\t// @off - disable libgdx formatter\n\t\t 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,\n\t\t12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,\n\t\t12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,\n\t\t12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,\n\t\t12,36,12,12,12,12,12,12,12,12,12,12\n\t\t// @on - enable libgdx formatter\n\t};\n\n\tprivate int codePoint;\n\tprivate int state;\n\tprivate final char[] utf16Char = new char[2];\n\tprivate char[] charBuffer;\n\tprivate int charOffset;\n\n\tpublic Utf8Decoder () {\n\t\tthis.state = UTF8_ACCEPT;\n\t}\n\n\tprotected void reset () {\n\t\tstate = UTF8_ACCEPT;\n\t}\n\n\tpublic int decode (byte[] b, int offset, int length, char[] charBuffer, int charOffset) {\n\t\tthis.charBuffer = charBuffer;\n\t\tthis.charOffset = charOffset;\n\t\tint end = offset + length;\n\t\tfor (int i = offset; i < end; i++)\n\t\t\tdecode(b[i]);\n\t\treturn this.charOffset - charOffset;\n\t}\n\n\tprivate void decode (byte b) {\n\n\t\tif (b > 0 && state == UTF8_ACCEPT) {\n\t\t\tcharBuffer[charOffset++] = (char)(b & 0xFF);\n\t\t} else {\n\t\t\tint i = b & 0xFF;\n\t\t\tint type = BYTE_TABLE[i];\n\t\t\tcodePoint = state == UTF8_ACCEPT ? (0xFF >> type) & i : (i & 0x3F) | (codePoint << 6);\n\t\t\tint next = TRANSITION_TABLE[state + type];\n\n\t\t\tswitch (next) {\n\t\t\tcase UTF8_ACCEPT:\n\t\t\t\tstate = next;\n\t\t\t\tif (codePoint < Character.MIN_HIGH_SURROGATE) {\n\t\t\t\t\tcharBuffer[charOffset++] = (char)codePoint;\n\t\t\t\t} else {\n\t\t\t\t\t// The code below is equivalent to\n\t\t\t\t\t// for (char c : Character.toChars(codePoint)) charBuffer[charOffset++] = c;\n\t\t\t\t\t// but does not allocate a char array.\n\t\t\t\t\tint codePointLength = Character.toChars(codePoint, utf16Char, 0);\n\t\t\t\t\tcharBuffer[charOffset++] = utf16Char[0];\n\t\t\t\t\tif (codePointLength == 2) charBuffer[charOffset++] = utf16Char[1];\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase UTF8_REJECT:\n\t\t\t\tcodePoint = 0;\n\t\t\t\tstate = UTF8_ACCEPT;\n\t\t\t\tcharBuffer[charOffset++] = REPLACEMENT;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tstate = next;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/AlphaComposite.java",
    "content": "package java.awt;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic class AlphaComposite extends Object implements Composite {\n\n    public static final AlphaComposite Src = new AlphaComposite();\n\n    private AlphaComposite(){}\n\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/Composite.java",
    "content": "package java.awt;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic interface Composite {\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/Graphics2D.java",
    "content": "package java.awt;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic class Graphics2D {\n\n    public void setComposite(Composite comp) {}\n\n    public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {}\n\n    public boolean drawImage(Image img, int x, int y,\n                                      int width, int height,\n                                      ImageObserver observer) { return false; }\n\n    public void dispose() {}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/Image.java",
    "content": "package java.awt;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic class Image {\n    public static final int TYPE_INT_ARGB = 2;\n\n    public synchronized void setRGB(int var1, int var2, int var3) {}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/ImageObserver.java",
    "content": "package java.awt;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic class ImageObserver {\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/RenderingHints.java",
    "content": "package java.awt;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic class RenderingHints {\n    public static class Key {\n\n    }\n    public static final Key KEY_INTERPOLATION = null;\n    public static final Key VALUE_INTERPOLATION_BILINEAR = null;\n    public static final Key KEY_RENDERING = null;\n    public static final Key VALUE_RENDER_QUALITY = null;\n    public static final Key KEY_ANTIALIASING = null;\n    public static final Key VALUE_ANTIALIAS_ON = null;\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/image/BufferedImage.java",
    "content": "package java.awt.image;\n\nimport java.awt.*;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic class BufferedImage extends Image implements RenderedImage {\n\n    public BufferedImage(int width,\n                         int height,\n                         int imageType) {\n\n    }\n\n    public int getType() {\n        return -1;\n    }\n\n    public Graphics2D createGraphics() {\n        return null;\n    }\n\n    public int getWidth() {\n        return -1;\n    }\n\n    public int getHeight() {\n        return -1;\n    }\n\n    public int getRGB(int x, int y) {\n        return -1;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/awt/image/RenderedImage.java",
    "content": "package java.awt.image;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic interface RenderedImage {\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/BufferedReader.java",
    "content": "/* Copyright (c) 2008, Avian Contributors\r\n\r\n   Permission to use, copy, modify, and/or distribute this software\r\n   for any purpose with or without fee is hereby granted, provided\r\n   that the above copyright notice and this permission notice appear\r\n   in all copies.\r\n\r\n   There is NO WARRANTY for this software.  See license.txt for\r\n   details. */\r\n\r\npackage java.io;\r\n\r\npublic class BufferedReader extends Reader {\r\n\tprivate final Reader in;\r\n\tprivate final char[] buffer;\r\n\tprivate int position;\r\n\tprivate int limit;\r\n\r\n\tpublic BufferedReader (Reader in, int bufferSize) {\r\n\t\tthis.in = in;\r\n\t\tthis.buffer = new char[bufferSize];\r\n\t}\r\n\r\n\tpublic BufferedReader (Reader in) {\r\n\t\tthis(in, 8192);\r\n\t}\r\n\r\n\tprivate void fill () throws IOException {\r\n\t\tposition = 0;\r\n\t\tlimit = in.read(buffer);\r\n\t}\r\n\r\n\tpublic String readLine () throws IOException {\r\n\t\tStringBuilder sb = new StringBuilder();\r\n\t\twhile (true) {\r\n\t\t\tif (position >= limit) {\r\n\t\t\t\tfill();\r\n\t\t\t}\r\n\r\n\t\t\tif (position >= limit) {\r\n\t\t\t\treturn sb.length() == 0 ? null : sb.toString();\r\n\t\t\t}\r\n\r\n\t\t\tfor (int i = position; i < limit; ++i) {\r\n\t\t\t\tif (buffer[i] == '\\r') {\r\n\t\t\t\t\tsb.append(buffer, position, i - position);\r\n\t\t\t\t\tposition = i + 1;\r\n\t\t\t\t\tif (i + 1 < limit) {\r\n\t\t\t\t\t\tif (buffer[i + 1] == '\\n') {\r\n\t\t\t\t\t\t\tposition = i + 2;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tfill();\r\n\t\t\t\t\t\tif (buffer[position] == '\\n') {\r\n\t\t\t\t\t\t\tposition += 1;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\treturn sb.toString();\r\n\t\t\t\t} else if (buffer[i] == '\\n') {\r\n\t\t\t\t\tsb.append(buffer, position, i - position);\r\n\t\t\t\t\tposition = i + 1;\r\n\t\t\t\t\treturn sb.toString();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tsb.append(buffer, position, limit - position);\r\n\t\t\tposition = limit;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic int read (char[] b, int offset, int length) throws IOException {\r\n\t\tint count = 0;\r\n\r\n\t\tif (position >= limit && length < buffer.length) {\r\n\t\t\tfill();\r\n\t\t}\r\n\r\n\t\tif (position < limit) {\r\n\t\t\tint remaining = limit - position;\r\n\t\t\tif (remaining > length) {\r\n\t\t\t\tremaining = length;\r\n\t\t\t}\r\n\r\n\t\t\tSystem.arraycopy(buffer, position, b, offset, remaining);\r\n\r\n\t\t\tcount += remaining;\r\n\t\t\tposition += remaining;\r\n\t\t\toffset += remaining;\r\n\t\t\tlength -= remaining;\r\n\t\t}\r\n\r\n\t\tif (length > 0) {\r\n\t\t\tint c = in.read(b, offset, length);\r\n\t\t\tif (c == -1) {\r\n\t\t\t\tif (count == 0) {\r\n\t\t\t\t\tcount = -1;\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\tcount += c;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn count;\r\n\t}\r\n\r\n\tpublic void close () throws IOException {\r\n\t\tin.close();\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/ByteArrayInputStream.java",
    "content": "/*******************************************************************************\n * Copyright 2011 See AUTHORS file.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *   http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n ******************************************************************************/\npackage java.io;\n\npublic class ByteArrayInputStream extends InputStream {\n    byte buf[];\n    int pos;\n    int count;\n    int mark = 0;\n\n    public ByteArrayInputStream(byte buf[]) {\n        this.buf = buf;\n        this.pos = 0;\n        this.count = buf.length;\n    }\n\n    public ByteArrayInputStream(byte buf[], int offset, int length) {\n        this.buf = buf;\n        this.pos = offset;\n        this.count = Math.min(offset + length, buf.length);\n        this.mark = offset;\n    }\n\n    public synchronized int read() {\n        return (pos < count) ? (buf[pos++] & 0xff) : -1;\n    }\n\n    public synchronized int read(byte b[], int off, int len) {\n        if (b == null) {\n            throw new NullPointerException();\n        } else if (off < 0 || len < 0 || len > b.length - off) {\n            throw new IndexOutOfBoundsException();\n        }\n\n        if (pos >= count) {\n            return -1;\n        }\n\n        int avail = count - pos;\n        if (len > avail) {\n            len = avail;\n        }\n        if (len <= 0) {\n            return 0;\n        }\n        System.arraycopy(buf, pos, b, off, len);\n        pos += len;\n        return len;\n    }\n\n    public synchronized long skip(long n) {\n        long k = count - pos;\n        if (n < k) {\n            k = n < 0 ? 0 : n;\n        }\n\n        pos += k;\n        return k;\n    }\n\n    public synchronized int available() {\n        return count - pos;\n    }\n\n    public boolean markSupported() {\n        return true;\n    }\n\n    public void mark(int readAheadLimit) {\n        mark = pos;\n    }\n\n    public synchronized void reset() {\n        pos = mark;\n    }\n\n    public void close() throws IOException {\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/ByteArrayOutputStream.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic class ByteArrayOutputStream extends OutputStream {\n\n\tprotected int count;\n\tprotected byte[] buf;\n\n\tpublic ByteArrayOutputStream () {\n\t\tthis(16);\n\t}\n\n\tpublic ByteArrayOutputStream (int initialSize) {\n\t\tbuf = new byte[initialSize];\n\t}\n\n\t@Override\n\tpublic void write (int b) {\n\t\tif (buf.length == count) {\n\t\t\tgrow(count + 1);\n\t\t}\n\n\t\tbuf[count++] = (byte)b;\n\t}\n\n\t//added\n    public void write(byte b[], int off, int len) {\n        if ((off < 0) || (off > b.length) || (len < 0) ||\n            ((off + len) - b.length > 0)) {\n            throw new IndexOutOfBoundsException();\n        }\n        if (count + len > buf.length) {\n\t\t\tgrow(count + len);\n\t\t}\n        System.arraycopy(b, off, buf, count, len);\n        count += len;\n    }\n\n    private void grow(int required) {\n        int current = this.buf.length;\n        int newSize = current << 1;\n        if(newSize - required < 0) {\n            newSize = required;\n        }\n\n        if(newSize - 2147483639 > 0) {\n            newSize = hugeCapacity(required);\n        }\n\n        byte[] newBuf = new byte[newSize];\n\t\tSystem.arraycopy(buf, 0, newBuf, 0, count);\n        this.buf = newBuf;\n    }\n\n    private static int hugeCapacity(int size) {\n        if(size < 0) {\n            throw new OutOfMemoryError();\n        } else {\n            return size > 2147483639 ? 2147483647 : 2147483639;\n        }\n    }\n    \n\tpublic byte[] toByteArray () {\n\t\tbyte[] result = new byte[count];\n\t\tSystem.arraycopy(buf, 0, result, 0, count);\n\t\treturn result;\n\t}\n\n\tpublic int size () {\n\t\treturn count;\n\t}\n\n\tpublic String toString () {\n\t\treturn new String(buf, 0, count);\n\t}\n\n\tpublic String toString (String enc) throws UnsupportedEncodingException {\n\t\treturn new String(buf, 0, count, enc);\n\t}\n\n\tpublic void close() throws IOException {\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/Closeable.java",
    "content": "/** Licensed to the Apache Software Foundation (ASF) under one or more\r\n * contributor license agreements.  See the NOTICE file distributed with\r\n * this work for additional information regarding copyright ownership.\r\n * The ASF licenses this file to You under the Apache License, Version 2.0\r\n * (the \"License\"); you may not use this file except in compliance with\r\n * the License.  You may obtain a copy of the License at\r\n * \r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n * \r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n\r\npackage java.io;\r\n\r\n/*** Defines an interface for classes that can (or need to) be closed once they are not used any longer. This usually includes all\r\n * sorts of {@link InputStream}s and {@link OutputStream}s. Calling the {@code close} method releases resources that the object\r\n * holds. */\r\npublic interface Closeable extends AutoCloseable {\r\n\r\n\t/*** Closes the object and release any system resources it holds. If the object has already been closed, then invoking this\r\n\t * method has no effect.\r\n\t * \r\n\t * @throws IOException if any error occurs when closing the object. */\r\n\tpublic void close () throws IOException;\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/DataInput.java",
    "content": "package java.io;\n\npublic interface DataInput {\n\n    void readFully(byte b[]) throws IOException;\n\n    void readFully(byte b[], int off, int len) throws IOException;\n\n    int skipBytes(int n) throws IOException;\n\n    boolean readBoolean() throws IOException;\n\n    byte readByte() throws IOException;\n\n    int readUnsignedByte() throws IOException;\n\n    short readShort() throws IOException;\n\n    int readUnsignedShort() throws IOException;\n\n    char readChar() throws IOException;\n\n    int readInt() throws IOException;\n\n    long readLong() throws IOException;\n\n    float readFloat() throws IOException;\n\n    double readDouble() throws IOException;\n\n    String readLine() throws IOException;\n\n    String readUTF() throws IOException;\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/DataInputStream.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic class DataInputStream extends InputStream implements DataInput {\n\n\tprivate final InputStream is;\n\n\tpublic DataInputStream (final InputStream is) {\n\t\tthis.is = is;\n\t}\n\n\t@Override\n\tpublic int read () throws IOException {\n\t\treturn is.read();\n\t}\n\n\tpublic boolean readBoolean () throws IOException {\n\t\treturn readByte() != 0;\n\t}\n\n\tpublic byte readByte () throws IOException {\n\t\tint i = read();\n\t\tif (i == -1) {\n\t\t\tthrow new EOFException();\n\t\t}\n\t\treturn (byte)i;\n\t}\n\n\tpublic char readChar () throws IOException {\n\t\tint a = is.read();\n\t\tint b = readUnsignedByte();\n\t\treturn (char)((a << 8) | b);\n\t}\n\n\tpublic double readDouble () throws IOException {\n\t\treturn Double.longBitsToDouble(readLong());\n\t}\n\n\tpublic float readFloat () throws IOException {\n\t\treturn Float.intBitsToFloat(readInt());\n\t}\n\n\tpublic void readFully (byte[] b) throws IOException {\n\t\treadFully(b, 0, b.length);\n\t}\n\n\tpublic void readFully (byte[] b, int off, int len) throws IOException {\n\t\twhile (len > 0) {\n\t\t\tint count = is.read(b, off, len);\n\t\t\tif (count <= 0) {\n\t\t\t\tthrow new EOFException();\n\t\t\t}\n\t\t\toff += count;\n\t\t\tlen -= count;\n\t\t}\n\t}\n\n\tpublic int readInt () throws IOException {\n\t\tint a = is.read();\n\t\tint b = is.read();\n\t\tint c = is.read();\n\t\tint d = readUnsignedByte();\n\t\treturn (a << 24) | (b << 16) | (c << 8) | d;\n\t}\n\n\tpublic String readLine () throws IOException {\n\t\tthrow new RuntimeException(\"readline NYI\");\n\t}\n\n\tpublic long readLong () throws IOException {\n\t\tlong a = readInt();\n\t\tlong b = readInt() & 0x0ffffffff;\n\t\treturn (a << 32) | b;\n\t}\n\n\tpublic short readShort () throws IOException {\n\t\tint a = is.read();\n\t\tint b = readUnsignedByte();\n\t\treturn (short)((a << 8) | b);\n\t}\n\n\tpublic String readUTF () throws IOException {\n\t\tint bytes = readUnsignedShort();\n\t\tStringBuilder sb = new StringBuilder();\n\n\t\twhile (bytes > 0) {\n\t\t\tbytes -= readUtfChar(sb);\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\n\tprivate int readUtfChar (StringBuilder sb) throws IOException {\n\t\tint a = readUnsignedByte();\n\t\tif ((a & 0x80) == 0) {\n\t\t\tsb.append((char)a);\n\t\t\treturn 1;\n\t\t}\n\t\tif ((a & 0xe0) == 0xc0) {\n\t\t\tint b = readUnsignedByte();\n\t\t\tsb.append((char)(((a & 0x1F) << 6) | (b & 0x3F)));\n\t\t\treturn 2;\n\t\t}\n\t\tif ((a & 0xf0) == 0xe0) {\n\t\t\tint b = readUnsignedByte();\n\t\t\tint c = readUnsignedByte();\n\t\t\tsb.append((char)(((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F)));\n\t\t\treturn 3;\n\t\t}\n\t\tthrow new UTFDataFormatException();\n\t}\n\n\tpublic int readUnsignedByte () throws IOException {\n\t\tint i = read();\n\t\tif (i == -1) {\n\t\t\tthrow new EOFException();\n\t\t}\n\t\treturn i;\n\t}\n\n\tpublic int readUnsignedShort () throws IOException {\n\t\tint a = is.read();\n\t\tint b = readUnsignedByte();\n\t\treturn ((a << 8) | b);\n\t}\n\n\tpublic int skipBytes (int n) throws IOException {\n\t\t// note: This is actually a valid implementation of this method, rendering it quite useless...\n\t\treturn 0;\n\t}\n\n\t@Override\n\tpublic int available () throws IOException {\n\t\treturn is.available();\n\t}\n\t\n\t@Override\n\tpublic void close () throws IOException {\n\t\tis.close();\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/DataOutput.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic interface DataOutput {\n\tpublic void write (byte[] data) throws IOException;\n\n\tpublic void write (byte[] data, int ofs, int len) throws IOException;\n\n\tpublic void write (int v) throws IOException;\n\n\tpublic void writeBoolean (boolean v) throws IOException;\n\n\tpublic void writeByte (int v) throws IOException;\n\n\tpublic void writeBytes (String s) throws IOException;\n\n\tpublic void writeChar (int v) throws IOException;\n\n\tpublic void writeChars (String s) throws IOException;\n\n\tpublic void writeDouble (double v) throws IOException;\n\n\tpublic void writeFloat (float v) throws IOException;\n\n\tpublic void writeInt (int v) throws IOException;\n\n\tpublic void writeLong (long v) throws IOException;\n\n\tpublic void writeShort (int v) throws IOException;\n\n\tpublic void writeUTF (String s) throws IOException;\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/DataOutputStream.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic class DataOutputStream extends OutputStream implements DataOutput {\n\n\tOutputStream os;\n\n\tpublic DataOutputStream (OutputStream os) {\n\t\tthis.os = os;\n\t}\n\n\t@Override\n\tpublic void write (int b) throws IOException {\n\t\tos.write(b);\n\t}\n\n\tpublic void writeBoolean (boolean v) throws IOException {\n\t\tos.write(v ? 1 : 0);\n\t}\n\n\tpublic void writeByte (int v) throws IOException {\n\t\tos.write(v);\n\t}\n\n\tpublic void writeBytes (String s) throws IOException {\n\t\tint len = s.length();\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tos.write(s.charAt(i) & 0xff);\n\t\t}\n\t}\n\n\tpublic void writeChar (int v) throws IOException {\n\t\tos.write(v >> 8);\n\t\tos.write(v);\n\t}\n\n\tpublic void writeChars (String s) throws IOException {\n\t\tthrow new RuntimeException(\"writeChars NYI\");\n\t}\n\n\tpublic void writeDouble (double v) throws IOException {\n\t\twriteLong(Double.doubleToLongBits(v));\n\t}\n\n\tpublic void writeFloat (float v) throws IOException {\n\t\twriteInt(Float.floatToIntBits(v));\n\t}\n\n\tpublic void writeInt (int v) throws IOException {\n\t\tos.write(v >> 24);\n\t\tos.write(v >> 16);\n\t\tos.write(v >> 8);\n\t\tos.write(v);\n\t}\n\n\tpublic void writeLong (long v) throws IOException {\n\t\twriteInt((int)(v >> 32L));\n\t\twriteInt((int)v);\n\t}\n\n\tpublic void writeShort (int v) throws IOException {\n\t\tos.write(v >> 8);\n\t\tos.write(v);\n\t}\n\n\tpublic void writeUTF (String s) throws IOException {\n\t\tByteArrayOutputStream baos = new ByteArrayOutputStream();\n\t\tfor (int i = 0; i < s.length(); i++) {\n\t\t\tchar c = s.charAt(i);\n\t\t\tif (c > 0 && c < 80) {\n\t\t\t\tbaos.write(c);\n\t\t\t} else if (c < '\\u0800') {\n\t\t\t\tbaos.write(0xc0 | (0x1f & (c >> 6)));\n\t\t\t\tbaos.write(0x80 | (0x3f & c));\n\t\t\t} else {\n\t\t\t\tbaos.write(0xe0 | (0x0f & (c >> 12)));\n\t\t\t\tbaos.write(0x80 | (0x3f & (c >> 6)));\n\t\t\t\tbaos.write(0x80 | (0x3f & c));\n\t\t\t}\n\t\t}\n\t\twriteShort(baos.count);\n\t\tos.write(baos.buf, 0, baos.count);\n\t}\n\n\tpublic void flush() throws IOException {\n\t\tos.flush();\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/EOFException.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic class EOFException extends IOException {\n\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/File.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\n\n/** LocalStorage based File implementation for GWT. Should probably have used Harmony as a starting point instead of writing this\n * from scratch.\n * \n * @author Stefan Haustein */\npublic class File {\n\n\tpublic static final File ROOT = new File(\"\");\n\n\tpublic static final char separatorChar = '/';\n\n\tpublic static final String separator = \"\" + separatorChar;\n\n\tpublic static final char pathSeparatorChar = ':';\n\n\tpublic static final String pathSeparator = \"\" + pathSeparatorChar;\n\n\tFile parent;\n\tString name;\n\tboolean absolute;\n\n\tpublic File (String pathname) {\n\t\twhile (pathname.endsWith(separator) && pathname.length() > 0) {\n\t\t\tpathname = pathname.substring(0, pathname.length() - 1);\n\t\t}\n\n\t\tint cut = pathname.lastIndexOf(separatorChar);\n\t\tif (cut == -1) {\n\t\t\tname = pathname;\n\t\t} else if (cut == 0) {\n\t\t\tname = pathname.substring(cut);\n\t\t\tparent = name.equals(\"\") ? null : ROOT;\n\t\t} else {\n\t\t\tname = pathname.substring(cut + 1);\n\t\t\tparent = new File(pathname.substring(0, cut));\n\t\t}\n\n// Compatibility.println(\"new File ('\"+pathname+ \"'); canonical name: '\" + getCanonicalPath() + \"'\");\n\t}\n\n\tpublic File (String parent, String child) {\n\t\tthis(new File(parent), child);\n\t}\n\n\tpublic File (File parent, String child) {\n\t\tthis.parent = parent;\n\t\tthis.name = child;\n\t}\n\n\t/*\n\t * public File(URI uri) { }\n\t */\n\n\tpublic String getName () {\n\t\treturn name;\n\t}\n\n\tpublic String getParent () {\n\t\treturn parent == null ? \"\" : parent.getPath();\n\t}\n\n\tpublic File getParentFile () {\n\t\treturn parent;\n\t}\n\n\tpublic String getPath () {\n\t\treturn parent == null ? name : (parent.getPath() + separatorChar + name);\n\t}\n\n\tprivate boolean isRoot () {\n\t\treturn name.equals(\"\") && parent == null;\n\t}\n\n\tpublic boolean isAbsolute () {\n\t\tif (isRoot()) {\n\t\t\treturn true;\n\t\t}\n\t\tif (parent == null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn parent.isAbsolute();\n\t}\n\n\tpublic String getAbsolutePath () {\n\t\tString path = getAbsoluteFile().getPath();\n\t\treturn path.length() == 0 ? \"/\" : path;\n\t}\n\n\tpublic File getAbsoluteFile () {\n\t\tif (isAbsolute()) {\n\t\t\treturn this;\n\t\t}\n\t\tif (parent == null) {\n\t\t\treturn new File(ROOT, name);\n\t\t}\n\t\treturn new File(parent.getAbsoluteFile(), name);\n\t}\n\n\tpublic String getCanonicalPath () {\n\t\treturn getCanonicalFile().getAbsolutePath();\n\t}\n\n\tpublic File getCanonicalFile () {\n\t\tFile cParent = parent == null ? null : parent.getCanonicalFile();\n\t\tif (name.equals(\".\")) {\n\t\t\treturn cParent == null ? ROOT : cParent;\n\t\t}\n\t\tif (cParent != null && cParent.name.equals(\"\")) {\n\t\t\tcParent = null;\n\t\t}\n\t\tif (name.equals(\"..\")) {\n\t\t\tif (cParent == null) {\n\t\t\t\treturn ROOT;\n\t\t\t}\n\t\t\tif (cParent.parent == null) {\n\t\t\t\treturn ROOT;\n\t\t\t}\n\t\t\treturn cParent.parent;\n\t\t}\n\t\tif (cParent == null && !name.equals(\"\")) {\n\t\t\treturn new File(ROOT, name);\n\t\t}\n\t\treturn new File(cParent, name);\n\t}\n\n\t/*\n\t * public URL toURL() throws MalformedURLException { }\n\t * \n\t * public URI toURI() { }\n\t */\n\n\tpublic boolean canRead () {\n\t\treturn true;\n\t}\n\n\tpublic boolean canWrite () {\n\t\treturn true;\n\t}\n\n\tpublic boolean exists () {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\tpublic boolean isDirectory () {\n\t\tthrow new Error(\"Not implemented\");\n\n\t}\n\n\tpublic boolean isFile () {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\tpublic boolean isHidden () {\n\t\treturn false;\n\t}\n\n\tpublic long lastModified () {\n\t\treturn 0;\n\t}\n\n\tpublic long length () {\n\t\ttry {\n\t\t\tif (!exists()) {\n\t\t\t\treturn 0;\n\t\t\t}\n\n\t\t\tRandomAccessFile raf = new RandomAccessFile(this, \"r\");\n\t\t\tlong len = raf.length();\n\t\t\traf.close();\n\t\t\treturn len;\n\t\t} catch (IOException e) {\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\tpublic boolean createNewFile () throws IOException {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\tpublic boolean delete () {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\tpublic void deleteOnExit () {\n\t\tthrow new RuntimeException(\"NYI: File.deleteOnExit()\");\n\t}\n\n\tpublic String[] list () {\n\t\tthrow new RuntimeException(\"NYI: File.list()\");\n\t}\n\n\t/*\n\t * public String[] list(FilenameFilter filter) { return null; }\n\t */\n\n\tpublic File[] listFiles () {\n\t\treturn listFiles(null);\n\t}\n\n\tpublic File[] listFiles (FilenameFilter filter) {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\t/*\n\t * public File[] listFiles(FileFilter filter) { return null; }\n\t */\n\n\tpublic boolean mkdir () {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\tpublic boolean mkdirs () {\n\t\tif (parent != null) {\n\t\t\tparent.mkdirs();\n\t\t}\n\t\treturn mkdir();\n\t}\n\n\tpublic boolean renameTo (File dest) {\n\t\tthrow new RuntimeException(\"renameTo()\");\n\t}\n\n\tpublic boolean setLastModified (long time) {\n\t\treturn false;\n\t}\n\n\tpublic boolean setReadOnly () {\n\t\treturn false;\n\t}\n\n\tpublic static File[] listRoots () {\n\t\treturn new File[] {ROOT};\n\t}\n\n\tpublic static File createTempFile (String prefix, String suffix, File directory) throws IOException {\n\t\tthrow new RuntimeException(\"NYI: createTempFile\");\n\t}\n\n\tpublic static File createTempFile (String prefix, String suffix) throws IOException {\n\t\tthrow new RuntimeException(\"NYI: createTempFile\");\n\t}\n\n\tpublic int compareTo (File pathname) {\n\t\tthrow new RuntimeException(\"NYI: File.compareTo()\");\n\t}\n\n\tpublic boolean equals (Object obj) {\n\t\tif (!(obj instanceof File)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn getPath().equals(((File)obj).getPath());\n\t}\n\n\tpublic Path toPath() {\n\t\treturn null;\n\t}\n\t\n\tpublic int hashCode () {\n\t\treturn parent != null ? parent.hashCode() + name.hashCode() : name.hashCode();\n\t}\n\n\tpublic String toString () {\n\t\treturn name;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/FileInputStream.java",
    "content": "package java.io;\n\npublic class FileInputStream extends InputStream{\n\tprotected InputStream in;\n\n\tpublic FileInputStream (InputStream in) {\n\t\tthis.in = in;\n\t}\n\n\tpublic FileInputStream (File file) {\n\t}\n\n\tpublic FileInputStream (String filename) {\n\t}\n\t\n\tpublic int read () throws IOException {\n\t\treturn in.read();\n\t}\n\n\tpublic int read (byte b[]) throws IOException {\n\t\treturn read(b, 0, b.length);\n\t}\n\n\tpublic int read (byte b[], int off, int len) throws IOException {\n\t\treturn in.read(b, off, len);\n\t}\n\n\tpublic long skip (long n) throws IOException {\n\t\treturn 0;\n\t}\n\n\t//added\n\tpublic int available () throws IOException{\n\t\treturn in.available();\n\t}\n\n\tpublic void close () throws IOException {\n\t\tin.close();\n\t}\n\n\tpublic synchronized void mark (int readlimit) {\n\t}\n\n\tpublic synchronized void reset () throws IOException {\n\t}\n\n\tpublic boolean markSupported () {\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/FileNotFoundException.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic class FileNotFoundException extends IOException {\n\n\tpublic FileNotFoundException () {\n\t\tsuper();\n\t}\n\n\tpublic FileNotFoundException (String s) {\n\t\tsuper(s);\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/FilenameFilter.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic interface FilenameFilter {\n\n\tboolean accept (File file, String name);\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/Flushable.java",
    "content": "/** Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n * \n *     http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage java.io;\n\n/*** Defines an interface for classes that can (or need to) be flushed, typically before some output processing is considered to be\n * finished and the object gets closed. */\npublic interface Flushable {\n\t/*** Flushes the object by writing out any buffered data to the underlying output.\n\t * \n\t * @throws IOException if there are any issues writing the data. */\n\tvoid flush () throws IOException;\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/InputStreamReader.java",
    "content": "/* Copyright (c) 2008-2010, Avian Contributors\n\n   Permission to use, copy, modify, and/or distribute this software\n   for any purpose with or without fee is hereby granted, provided\n   that the above copyright notice and this permission notice appear\n   in all copies.\n\n   There is NO WARRANTY for this software.  See license.txt for\n   details. */\n\npackage java.io;\n\nimport com.badlogic.gdx.utils.Utf8Decoder;\n\npublic class InputStreamReader extends Reader {\n\tprivate final InputStream in;\n\n\tprivate final Utf8Decoder utf8Decoder;\n\n\tpublic InputStreamReader (InputStream in) {\n\t\tthis.in = in;\n\t\tthis.utf8Decoder = new Utf8Decoder();\n\t}\n\n\tpublic InputStreamReader (InputStream in, String encoding) throws UnsupportedEncodingException {\n\t\tthis(in);\n\n\t\t// FIXME this is bad, but some APIs seem to use \"ISO-8859-1\", fuckers...\n// if (! encoding.equals(\"UTF-8\")) {\n// throw new UnsupportedEncodingException(encoding);\n// }\n\t}\n\n\tpublic int read (char[] b, int offset, int length) throws IOException {\n\t\tbyte[] buffer = new byte[length];\n\t\tint c = in.read(buffer);\n\t\treturn c <= 0 ? c : utf8Decoder.decode(buffer, 0, c, b, offset);\n\t}\n\n\tpublic void close () throws IOException {\n\t\tin.close();\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/OutputStreamWriter.java",
    "content": "/* Copyright (c) 2008-2010, Avian Contributors\r\n\r\n   Permission to use, copy, modify, and/or distribute this software\r\n   for any purpose with or without fee is hereby granted, provided\r\n   that the above copyright notice and this permission notice appear\r\n   in all copies.\r\n\r\n   There is NO WARRANTY for this software.  See license.txt for\r\n   details. */\r\n\r\npackage java.io;\r\n\r\nimport avian.Utf8;\r\n\r\npublic class OutputStreamWriter extends Writer {\r\n\tprivate final OutputStream out;\r\n\r\n\tpublic OutputStreamWriter (OutputStream out, String encoding) {\r\n\t\tthis(out);\r\n\t}\r\n\r\n\tpublic OutputStreamWriter (OutputStream out) {\r\n\t\tthis.out = out;\r\n\t}\r\n\r\n\tpublic void write (char[] b, int offset, int length) throws IOException {\r\n\t\tout.write(Utf8.encode(b, offset, length));\r\n\t}\r\n\r\n\tpublic void flush () throws IOException {\r\n\t\tout.flush();\r\n\t}\r\n\r\n\tpublic void close () throws IOException {\r\n\t\tout.close();\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/PrintWriter.java",
    "content": "package java.io;\n\npublic class PrintWriter extends Writer{\n\n\tpublic PrintWriter() {\n\t}\n\t\n\tpublic PrintWriter(int initialCapacity) {\n\t}\n\tpublic PrintWriter(Writer out,\n            boolean autoFlush) {\n\t\t\n\t}\n\tpublic PrintWriter append(CharSequence csq) {\n\t\treturn null;\n\t}\n\tpublic void write (char[] b, int offset, int length) throws IOException {\n\t}\n\n\tpublic String toString () {\n\t\treturn null;\n\t}\n\n\tpublic void flush () {\n\t}\n\n\tpublic void close () throws IOException {\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/PushbackInputStream.java",
    "content": "package java.io;\n\n/* PushbackInputStream.java -- An input stream that can unread bytes\n    Copyright (C) 1998, 1999, 2001, 2002, 2005  Free Software Foundation, Inc.\n \n This file is part of GNU Classpath.\n \n GNU Classpath is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2, or (at your option)\n any later version.\n  \n GNU Classpath is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n General Public License for more details.\n \n You should have received a copy of the GNU General Public License\n along with GNU Classpath; see the file COPYING.  If not, write to the\n Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n 02110-1301 USA.\n \n Linking this library statically or dynamically with other modules is\n making a combined work based on this library.  Thus, the terms and\n conditions of the GNU General Public License cover the whole\n combination.\n \n As a special exception, the copyright holders of this library give you\n permission to link this library with independent modules to produce an\n executable, regardless of the license terms of these independent\n modules, and to copy and distribute the resulting executable under\n terms of your choice, provided that you also meet, for each linked\n independent module, the terms and conditions of the license of that\n module.  An independent module is a module which is not derived from\n or based on this library.  If you modify this library, you may extend\n this exception to your version of the library, but you are not\n obligated to do so.  If you do not wish to do so, delete this\n exception statement from your version. */\n\n/**\n * This subclass of <code>FilterInputStream</code> provides the ability to\n * unread data from a stream.  It maintains an internal buffer of unread\n * data that is supplied to the next read operation.  This is conceptually\n * similar to mark/reset functionality, except that in this case the\n * position to reset the stream to does not need to be known in advance.\n * <p>\n * The default pushback buffer size one byte, but this can be overridden\n * by the creator of the stream.\n * <p>\n *\n * @author Aaron M. Renn (arenn@urbanophile.com)\n * @author Warren Levy (warrenl@cygnus.com)\n */\npublic class PushbackInputStream extends FilterInputStream\n{\n    /**\n     * This is the default buffer size\n     */\n    private static final int DEFAULT_BUFFER_SIZE = 1;\n\n    /**\n     * This is the buffer that is used to store the pushed back data\n     */\n    protected byte[] buf;\n\n    /**\n     * This is the position in the buffer from which the next byte will be\n     * read.  Bytes are stored in reverse order in the buffer, starting from\n     * <code>buf[buf.length - 1]</code> to <code>buf[0]</code>.  Thus when\n     * <code>pos</code> is 0 the buffer is full and <code>buf.length</code> when\n     * it is empty\n     */\n    protected int pos;\n\n    /**\n     * This method initializes a <code>PushbackInputStream</code> to\n     * read from the specified subordinate <code>InputStream</code>\n     * with a default pushback buffer size of 1.\n     *\n     * @param in The subordinate stream to read from\n     */\n    public PushbackInputStream(InputStream in)\n    {\n        this(in, DEFAULT_BUFFER_SIZE);\n    }\n\n    /**\n     * This method initializes a <code>PushbackInputStream</code> to\n     * read from the specified subordinate <code>InputStream</code> with\n     * the specified buffer size\n     *\n     * @param in The subordinate <code>InputStream</code> to read from\n     * @param size The pushback buffer size to use\n     */\n    public PushbackInputStream(InputStream in, int size)\n    {\n        super(in);\n        if (size < 0)\n            throw new IllegalArgumentException();\n        buf = new byte[size];\n        pos = buf.length;\n    }\n\n    /**\n     * This method returns the number of bytes that can be read from this\n     * stream before a read can block.  A return of 0 indicates that blocking\n     * might (or might not) occur on the very next read attempt.\n     * <p>\n     * This method will return the number of bytes available from the\n     * pushback buffer plus the number of bytes available from the\n     * underlying stream.\n     *\n     * @return The number of bytes that can be read before blocking could occur\n     *\n     * @exception IOException If an error occurs\n     */\n    public int available() throws IOException\n    {\n        try\n        {\n            return (buf.length - pos) + super.available();\n        }\n        catch (NullPointerException npe)\n        {\n            throw new IOException (\"Stream closed\");\n        }\n    }\n\n    /**\n     * This method closes the stream and releases any associated resources.\n     *\n     * @exception IOException If an error occurs.\n     */\n    public synchronized void close() throws IOException\n    {\n        buf = null;\n        super.close();\n    }\n\n    /**\n     * This method returns <code>false</code> to indicate that it does\n     * not support mark/reset functionality.\n     *\n     * @return This method returns <code>false</code> to indicate that\n     * this class does not support mark/reset functionality\n     */\n    public boolean markSupported()\n    {\n        return false;\n    }\n\n    /**\n     * This method always throws an IOException in this class because\n     * mark/reset functionality is not supported.\n     *\n     * @exception IOException Always thrown for this class\n     */\n    public void reset() throws IOException\n    {\n        throw new IOException(\"Mark not supported in this class\");\n    }\n\n    /**\n     * This method reads an unsigned byte from the input stream and returns it\n     * as an int in the range of 0-255.  This method also will return -1 if\n     * the end of the stream has been reached.  The byte returned will be read\n     * from the pushback buffer, unless the buffer is empty, in which case\n     * the byte will be read from the underlying stream.\n     * <p>\n     * This method will block until the byte can be read.\n     *\n     * @return The byte read or -1 if end of stream\n     *\n     * @exception IOException If an error occurs\n     */\n    public synchronized int read() throws IOException\n    {\n        if (pos < buf.length)\n            return ((int) buf[pos++]) & 0xFF;\n\n        return super.read();\n    }\n\n    /**\n     * This method read bytes from a stream and stores them into a\n     * caller supplied buffer.  It starts storing the data at index\n     * <code>offset</code> into the buffer and attempts to read\n     * <code>len</code> bytes.  This method can return before reading the\n     * number of bytes requested.  The actual number of bytes read is\n     * returned as an int.  A -1 is returned to indicate the end of the\n     * stream.\n     *  <p>\n     * This method will block until some data can be read.\n     * <p>\n     * This method first reads bytes from the pushback buffer in order to\n     * satisfy the read request.  If the pushback buffer cannot provide all\n     * of the bytes requested, the remaining bytes are read from the\n     * underlying stream.\n     *\n     * @param b The array into which the bytes read should be stored\n     * @param off The offset into the array to start storing bytes\n     * @param len The requested number of bytes to read\n     *\n     * @return The actual number of bytes read, or -1 if end of stream.\n     *\n     * @exception IOException If an error occurs.\n     */\n    public synchronized int read(byte[] b, int off, int len) throws IOException\n    {\n        int numBytes = Math.min(buf.length - pos, len);\n\n        if (numBytes > 0)\n        {\n            System.arraycopy (buf, pos, b, off, numBytes);\n            pos += numBytes;\n            len -= numBytes;\n            off += numBytes;\n        }\n\n        if (len > 0)\n        {\n            len = super.read(b, off, len);\n            if (len == -1) //EOF\n                return numBytes > 0 ? numBytes : -1;\n            numBytes += len;\n        }\n        return numBytes;\n    }\n\n    /**\n     * This method pushes a single byte of data into the pushback buffer.\n     * The byte pushed back is the one that will be returned as the first byte\n     * of the next read.\n     * <p>\n     * If the pushback buffer is full, this method throws an exception.\n     * <p>\n     * The argument to this method is an <code>int</code>.  Only the low\n     * eight bits of this value are pushed back.\n     *\n     * @param b The byte to be pushed back, passed as an int\n     *\n     * @exception IOException If the pushback buffer is full.\n     */\n    public synchronized void unread(int b) throws IOException\n    {\n        if (pos <= 0)\n            throw new IOException(\"Insufficient space in pushback buffer\");\n\n        buf[--pos] = (byte) b;\n    }\n\n    /**\n     * This method pushes all of the bytes in the passed byte array into\n     * the pushback bfer.  These bytes are pushed in reverse order so that\n     * the next byte read from the stream after this operation will be\n     * <code>b[0]</code> followed by <code>b[1]</code>, etc.\n     * <p>\n     * If the pushback buffer cannot hold all of the requested bytes, an\n     * exception is thrown.\n     *\n     * @param b The byte array to be pushed back\n     *\n     * @exception IOException If the pushback buffer is full\n     */\n    public synchronized void unread(byte[] b) throws IOException\n    {\n        unread(b, 0, b.length);\n    }\n\n    /**\n     * This method pushed back bytes from the passed in array into the\n     * pushback buffer.  The bytes from <code>b[offset]</code> to\n     * <code>b[offset + len]</code> are pushed in reverse order so that\n     * the next byte read from the stream after this operation will be\n     * <code>b[offset]</code> followed by <code>b[offset + 1]</code>,\n     * etc.\n     * <p>\n     * If the pushback buffer cannot hold all of the requested bytes, an\n     * exception is thrown.\n     *\n     * @param b The byte array to be pushed back\n     * @param off The index into the array where the bytes to be push start\n     * @param len The number of bytes to be pushed.\n     *\n     * @exception IOException If the pushback buffer is full\n     */\n    public synchronized void unread(byte[] b, int off, int len)\n            throws IOException\n    {\n        if (pos < len)\n            throw new IOException(\"Insufficient space in pushback buffer\");\n\n        // Note the order that these bytes are being added is the opposite\n        // of what would be done if they were added to the buffer one at a time.\n        // See the Java Class Libraries book p. 1390.\n        System.arraycopy(b, off, buf, pos - len, len);\n\n        // Don't put this into the arraycopy above, an exception might be thrown\n        // and in that case we don't want to modify pos.\n        pos -= len;\n    }\n\n    /**\n     * This method skips the specified number of bytes in the stream.  It\n     * returns the actual number of bytes skipped, which may be less than the\n     * requested amount.\n     * <p>\n     * This method first discards bytes from the buffer, then calls the\n     * <code>skip</code> method on the underlying <code>InputStream</code> to\n     * skip additional bytes if necessary.\n     *\n     * @param n The requested number of bytes to skip\n     *\n     * @return The actual number of bytes skipped.\n     *\n     * @exception IOException If an error occurs\n     *\n     * @since 1.2\n     */\n    public synchronized long skip(long n) throws IOException\n    {\n        final long origN = n;\n\n        if (n > 0L)\n        {\n            int numread = (int) Math.min((long) (buf.length - pos), n);\n            pos += numread;\n            n -= numread;\n            if (n > 0)\n                n -= super.skip(n);\n        }\n\n        return origN - n;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/RandomAccessFile.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\n/** Saves binary data to the local storage; currently using hex encoding. The string is prefixed with \"hex:\"\n * @author haustein */\npublic class RandomAccessFile implements Closeable/* implements DataOutput, DataInput, Closeable */{\n\t\n\tpublic RandomAccessFile (File file, String mode) throws FileNotFoundException {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t}\n\n\tpublic long length () throws IOException {\n\t\treturn -1;\n\t}\n\t\n\tpublic int read () throws IOException {\n\t\treturn -1;\n\t}\n\t\n\tpublic int read (byte b[]) throws IOException {\n\t\treturn -1;\n\t}\n\n\tpublic int read (byte b[], int offset, int len) throws IOException {\n\t\treturn -1;\n\t}\n\n\tpublic void seek (long pos) throws IOException {\n\t}\n\t\n\tpublic void setLength (long newLength) throws IOException {\n\t}\n\t\n\tpublic void write (byte b[]) throws IOException {\n\t}\n\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/Reader.java",
    "content": "/*\n * Copyright 2018 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.io;\n\n/**\n * Reads a stream of characters.\n */\npublic abstract class Reader {\n  /**\n   * The maximum buffer size to incrementally read in {@link #skip}.\n   */\n  private static final int MAX_SKIP_BUFFER_SIZE = 1024;\n\n  /**\n   * Closes the reader, and releases any associated resources.\n   */\n  public abstract void close() throws IOException;\n\n  /**\n   * Marks the present position in the stream. Until {@code readAheadLimit} more\n   * characters have been read, the current point in the stream will be stored\n   * as the mark. Calls to {@link #reset} will reposition the point in the\n   * stream to the mark.\n   *\n   * @throws IOException If the stream does not support mark().\n   */\n  public void mark(int readAheadLimit) throws IOException {\n    throw new IOException(\"Not supported\");\n  }\n\n  /**\n   * Returns whether {@link #mark} is implemented.\n   */\n  public boolean markSupported() {\n    return false;\n  }\n\n  /**\n   * Reads a single character, or -1 if we are at the end of the stream.\n   */\n  public int read() throws IOException {\n    char chr[] = new char[1];\n    return (read(chr) == -1) ? -1 : chr[0];\n  }\n\n  /**\n   * Attempts to fill {@code buf} with characters up to the size of the array.\n   */\n  public int read(char[] buf) throws IOException {\n    return read(buf, 0, buf.length);\n  }\n\n  /**\n   * Attempts to fill {@code buf} with up to {@code len} characters. Characters\n   * will be stored in {@code buf} starting at index {@code off}.\n   */\n  public abstract int read(char[] buf, int off, int len) throws IOException;\n\n  /**\n   * Returns whether the stream is ready for reading characters.\n   */\n  public boolean ready() throws IOException {\n    return false;\n  }\n\n  /**\n   * Attempts to reset the stream to the previous mark.\n   */\n  public void reset() throws IOException {\n    throw new IOException(\"Not supported\");\n  }\n\n  /**\n   * Skips {@code n} characters, returning the number of characters that were actually skipped.\n   */\n  public long skip(long n) throws IOException {\n    long remaining = n;\n    int bufferSize = Math.min((int) n, MAX_SKIP_BUFFER_SIZE);\n    char[] skipBuffer = new char[bufferSize];\n    while (remaining > 0) {\n      long numRead = read(skipBuffer, 0, (int) remaining);\n      if (numRead < 0) {\n        break;\n      }\n      remaining -= numRead;\n    }\n    return n - remaining;\n  }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/UTFDataFormatException.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic class UTFDataFormatException extends IOException {\n\n\tpublic UTFDataFormatException (String msg) {\n\t\tsuper(msg);\n\t}\n\n\tpublic UTFDataFormatException () {\n\t\tsuper();\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/UnsupportedEncodingException.java",
    "content": "/*\n * Copyright 2010 Google Inc.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage java.io;\n\npublic class UnsupportedEncodingException extends IOException {\n\tpublic UnsupportedEncodingException () {\n\t\tsuper();\n\t}\n\n\tpublic UnsupportedEncodingException (String s) {\n\t\tsuper(s);\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/io/Writer.java",
    "content": "/**\n *  Licensed to the Apache Software Foundation (ASF) under one or more\n *  contributor license agreements.  See the NOTICE file distributed with\n *  this work for additional information regarding copyright ownership.\n *  The ASF licenses this file to You under the Apache License, Version 2.0\n *  (the \"License\"); you may not use this file except in compliance with\n *  the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage java.io;\n\n/*** The base class for all writers. A writer is a means of writing data to a target in a character-wise manner. Most output streams\n * expect the {@link #flush()} method to be called before closing the stream, to ensure all data is actually written out.\n * <p>\n * This abstract class does not provide a fully working implementation, so it needs to be subclassed, and at least the\n * {@link #write(char[], int, int)}, {@link #close()} and {@link #flush()} methods needs to be overridden. Overriding some of the\n * non-abstract methods is also often advised, since it might result in higher efficiency.\n * <p>\n * Many specialized readers for purposes like reading from a file already exist in this package.\n * \n * @see Reader */\npublic abstract class Writer implements Appendable, Closeable, Flushable {\n\n\tstatic final String TOKEN_NULL = \"null\"; //$NON-NLS-1$\n\n\t/*** The object used to synchronize access to the writer. */\n\tprotected Object lock;\n\n\t/*** Constructs a new {@code Writer} with {@code this} as the object used to synchronize critical sections. */\n\tprotected Writer () {\n\t\tsuper();\n\t\tlock = this;\n\t}\n\n\t/*** Constructs a new {@code Writer} with {@code lock} used to synchronize critical sections.\n\t * \n\t * @param lock the {@code Object} used to synchronize critical sections.\n\t * @throws NullPointerException if {@code lock} is {@code null}. */\n\tprotected Writer (Object lock) {\n\t\tif (lock == null) {\n\t\t\tthrow new NullPointerException();\n\t\t}\n\t\tthis.lock = lock;\n\t}\n\n\t/*** Closes this writer. Implementations of this method should free any resources associated with the writer.\n\t * \n\t * @throws IOException if an error occurs while closing this writer. */\n\tpublic abstract void close () throws IOException;\n\n\t/*** Flushes this writer. Implementations of this method should ensure that all buffered characters are written to the target.\n\t * \n\t * @throws IOException if an error occurs while flushing this writer. */\n\tpublic abstract void flush () throws IOException;\n\n\t/*** Writes the entire character buffer {@code buf} to the target.\n\t * \n\t * @param buf the non-null array containing characters to write.\n\t * @throws IOException if this writer is closed or another I/O error occurs. */\n\tpublic void write (char buf[]) throws IOException {\n\t\twrite(buf, 0, buf.length);\n\t}\n\n\t/*** Writes {@code count} characters starting at {@code offset} in {@code buf} to the target.\n\t * \n\t * @param buf the non-null character array to write.\n\t * @param offset the index of the first character in {@code buf} to write.\n\t * @param count the maximum number of characters to write.\n\t * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code count < 0}, or if {@code offset + count} is greater than\n\t *            the size of {@code buf}.\n\t * @throws IOException if this writer is closed or another I/O error occurs. */\n\tpublic abstract void write (char buf[], int offset, int count) throws IOException;\n\n\t/*** Writes one character to the target. Only the two least significant bytes of the integer {@code oneChar} are written.\n\t * \n\t * @param oneChar the character to write to the target.\n\t * @throws IOException if this writer is closed or another I/O error occurs. */\n\tpublic void write (int oneChar) throws IOException {\n\t\tsynchronized (lock) {\n\t\t\tchar oneCharArray[] = new char[1];\n\t\t\toneCharArray[0] = (char)oneChar;\n\t\t\twrite(oneCharArray);\n\t\t}\n\t}\n\n\t/*** Writes the characters from the specified string to the target.\n\t * \n\t * @param str the non-null string containing the characters to write.\n\t * @throws IOException if this writer is closed or another I/O error occurs. */\n\tpublic void write (String str) throws IOException {\n\t\twrite(str, 0, str.length());\n\t}\n\n\t/*** Writes {@code count} characters from {@code str} starting at {@code offset} to the target.\n\t * \n\t * @param str the non-null string containing the characters to write.\n\t * @param offset the index of the first character in {@code str} to write.\n\t * @param count the number of characters from {@code str} to write.\n\t * @throws IOException if this writer is closed or another I/O error occurs.\n\t * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code count < 0}, or if {@code offset + count} is greater than\n\t *            the length of {@code str}. */\n\tpublic void write (String str, int offset, int count) throws IOException {\n\t\tif (count < 0) { // other cases tested by getChars()\n\t\t\tthrow new StringIndexOutOfBoundsException();\n\t\t}\n\t\tchar buf[] = new char[count];\n\t\tstr.getChars(offset, offset + count, buf, 0);\n\n\t\tsynchronized (lock) {\n\t\t\twrite(buf, 0, buf.length);\n\t\t}\n\t}\n\n\t/*** Appends the character {@code c} to the target. This method works the same way as {@link #write(int)}.\n\t * \n\t * @param c the character to append to the target stream.\n\t * @return this writer.\n\t * @throws IOException if this writer is closed or another I/O error occurs. */\n\tpublic Writer append (char c) throws IOException {\n\t\twrite(c);\n\t\treturn this;\n\t}\n\n\t/*** Appends the character sequence {@code csq} to the target. This method works the same way as\n\t * {@code Writer.write(csq.toString())}. If {@code csq} is {@code null}, then the string \"null\" is written to the target\n\t * stream.\n\t * \n\t * @param csq the character sequence appended to the target.\n\t * @return this writer.\n\t * @throws IOException if this writer is closed or another I/O error occurs. */\n\tpublic Writer append (CharSequence csq) throws IOException {\n\t\tif (null == csq) {\n\t\t\twrite(TOKEN_NULL);\n\t\t} else {\n\t\t\twrite(csq.toString());\n\t\t}\n\t\treturn this;\n\t}\n\n\t/*** Appends a subsequence of the character sequence {@code csq} to the target. This method works the same way as\n\t * {@code Writer.writer(csq.subsequence(start, end).toString())}. If {@code csq} is {@code null}, then the specified\n\t * subsequence of the string \"null\" will be written to the target.\n\t * \n\t * @param csq the character sequence appended to the target.\n\t * @param start the index of the first char in the character sequence appended to the target.\n\t * @param end the index of the character following the last character of the subsequence appended to the target.\n\t * @return this writer.\n\t * @throws IOException if this writer is closed or another I/O error occurs.\n\t * @throws IndexOutOfBoundsException if {@code start > end}, {@code start < 0}, {@code end < 0} or either {@code start} or\n\t *            {@code end} are greater or equal than the length of {@code csq}. */\n\tpublic Writer append (CharSequence csq, int start, int end) throws IOException {\n\t\tif (null == csq) {\n\t\t\twrite(TOKEN_NULL.substring(start, end));\n\t\t} else {\n\t\t\twrite(csq.subSequence(start, end).toString());\n\t\t}\n\t\treturn this;\n\t}\n\n\t/*** Returns true if this writer has encountered and suppressed an error. Used by PrintWriters as an alternative to checked\n\t * exceptions. */\n\tboolean checkError () {\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/AutoCloseable.java",
    "content": "package java.lang;\n\npublic interface AutoCloseable {\n\tvoid close() throws Exception;\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/ClassLoader.java",
    "content": "package java.lang;\n\nimport java.io.InputStream;\nimport java.net.URL;\n\npublic class ClassLoader {\n\n\tpublic static ClassLoader getSystemClassLoader() {\n\t\treturn null;\n\t}\n\t\n\tpublic InputStream getResourceAsStream(String name) {\n\t\treturn null;\n\t}\n\t\n\tpublic URL getResource(String name) {\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/ClassNotFoundException.java",
    "content": "/*\r\n * @(#)ClassNotFoundException.java\t1.20 04/02/19\r\n *\r\n * Copyright 2004 Sun Microsystems, Inc. All rights reserved.\r\n * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.\r\n */\r\n\r\npackage java.lang;\r\n\r\n/** Thrown when an application tries to load in a class through its string name using:\r\n * <ul>\r\n * <li>The <code>forName</code> method in class <code>Class</code>.\r\n * <li>The <code>findSystemClass</code> method in class <code>ClassLoader</code> .\r\n * <li>The <code>loadClass</code> method in class <code>ClassLoader</code>.\r\n * </ul>\r\n * <p>\r\n * but no definition for the class with the specified name could be found.\r\n * \r\n * <p>\r\n * As of release 1.4, this exception has been retrofitted to conform to the general purpose exception-chaining mechanism. The\r\n * \"optional exception that was raised while loading the class\" that may be provided at construction time and accessed via the\r\n * {@link #getException()} method is now known as the <i>cause</i>, and may be accessed via the {@link Throwable#getCause()}\r\n * method, as well as the aforementioned \"legacy method.\"\r\n * \r\n * @author unascribed\r\n * @version 1.20, 02/19/04\r\n * @see java.lang.Class#forName(java.lang.String)\r\n * @see java.lang.ClassLoader#findSystemClass(java.lang.String)\r\n * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)\r\n * @since JDK1.0 */\r\npublic class ClassNotFoundException extends Exception {\r\n\t/** use serialVersionUID from JDK 1.1.X for interoperability */\r\n\tprivate static final long serialVersionUID = 9176873029745254542L;\r\n\r\n\t/** This field holds the exception ex if the ClassNotFoundException(String s, Throwable ex) constructor was used to instantiate\r\n\t * the object\r\n\t * @serial\r\n\t * @since 1.2 */\r\n\tprivate Throwable ex;\r\n\r\n\t/** Constructs a <code>ClassNotFoundException</code> with no detail message. */\r\n\tpublic ClassNotFoundException () {\r\n\t\tsuper((Throwable)null); // Disallow initCause\r\n\t}\r\n\r\n\t/** Constructs a <code>ClassNotFoundException</code> with the specified detail message.\r\n\t * \r\n\t * @param s the detail message. */\r\n\tpublic ClassNotFoundException (String s) {\r\n\t\tsuper(s, null); // Disallow initCause\r\n\t}\r\n\r\n\t/** Constructs a <code>ClassNotFoundException</code> with the specified detail message and optional exception that was raised\r\n\t * while loading the class.\r\n\t * \r\n\t * @param s the detail message\r\n\t * @param ex the exception that was raised while loading the class\r\n\t * @since 1.2 */\r\n\tpublic ClassNotFoundException (String s, Throwable ex) {\r\n\t\tsuper(s, null); // Disallow initCause\r\n\t\tthis.ex = ex;\r\n\t}\r\n\r\n\t/** Returns the exception that was raised if an error occurred while attempting to load the class. Otherwise, returns\r\n\t * <tt>null</tt>.\r\n\t * \r\n\t * <p>\r\n\t * This method predates the general-purpose exception chaining facility. The {@link Throwable#getCause()} method is now the\r\n\t * preferred means of obtaining this information.\r\n\t * \r\n\t * @return the <code>Exception</code> that was raised while loading a class\r\n\t * @since 1.2 */\r\n\tpublic Throwable getException () {\r\n\t\treturn ex;\r\n\t}\r\n\r\n\t/** Returns the cause of this exception (the exception that was raised if an error occurred while attempting to load the class;\r\n\t * otherwise <tt>null</tt>).\r\n\t * \r\n\t * @return the cause of this exception.\r\n\t * @since 1.4 */\r\n\tpublic Throwable getCause () {\r\n\t\treturn ex;\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/Exception.java",
    "content": "/*\n * Copyright 2007 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.lang;\n\n/**\n * See <a\n * href=\"http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Exception.html\">the\n * official Java API doc</a> for details.\n */\npublic class Exception extends Throwable {\n\n    public Exception() {\n    }\n\n    public Exception(String message) {\n        super(message);\n    }\n\n    public Exception(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n    public Exception(Throwable cause) {\n        super(cause);\n    }\n\n    protected Exception(String message, Throwable cause, boolean enableSuppression,\n                        boolean writableStackTrace) {\n        super(message, cause, enableSuppression, writableStackTrace);\n    }\n\n    Exception(Object backingJsObject) {\n        super(backingJsObject);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/InternalError.java",
    "content": "/*\n    * Copyright (c) 1994, 2008, Oracle and/or its affiliates. All rights reserved.\n    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n    *\n    * This code is free software; you can redistribute it and/or modify it\n    * under the terms of the GNU General Public License version 2 only, as\n    * published by the Free Software Foundation.  Oracle designates this\n    * particular file as subject to the \"Classpath\" exception as provided\n    * by Oracle in the LICENSE file that accompanied this code.\n    *\n    * This code is distributed in the hope that it will be useful, but WITHOUT\n    * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n    * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License\n    * version 2 for more details (a copy is included in the LICENSE file that\n    * accompanied this code).\n    *\n    * You should have received a copy of the GNU General Public License version\n    * 2 along with this work; if not, write to the Free Software Foundation,\n    * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n    *\n    * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n    * or visit www.oracle.com if you need additional information or have any\n    * questions.\n    */\n   \npackage java.lang;\n   \n/**\n * Thrown to indicate some unexpected internal error has occurred in\n * the Java Virtual Machine.\n *\n * @author  unascribed\n * @since   JDK1.0\n */\npublic class InternalError extends Error {\n    private static final long serialVersionUID = -9062593416125562365L;\n   \n    /**\n     * Constructs an <code>InternalError</code> with no detail message.\n     */\n    public InternalError() {\n        super();\n    }\n   \n    /**\n     * Constructs an <code>InternalError</code> with the specified\n     * detail message.\n     *\n     * @param   s   the detail message.\n     */\n    public InternalError(String s) {\n        super(s);\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/InterruptedException.java",
    "content": "/* Copyright (c) 2008, Avian Contributors\r\n\r\n   Permission to use, copy, modify, and/or distribute this software\r\n   for any purpose with or without fee is hereby granted, provided\r\n   that the above copyright notice and this permission notice appear\r\n   in all copies.\r\n\r\n   There is NO WARRANTY for this software.  See license.txt for\r\n   details. */\r\n\r\npackage java.lang;\r\n\r\npublic class InterruptedException extends Exception {\r\n\tpublic InterruptedException (String message, Throwable cause) {\r\n\t\tsuper(message, cause);\r\n\t}\r\n\r\n\tpublic InterruptedException (String message) {\r\n\t\tthis(message, null);\r\n\t}\r\n\r\n\tpublic InterruptedException (Throwable cause) {\r\n\t\tthis(null, cause);\r\n\t}\r\n\r\n\tpublic InterruptedException () {\r\n\t\tthis(null, null);\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/OutOfMemoryError.java",
    "content": "package java.lang;\n\npublic class OutOfMemoryError extends Error {\n\n\tpublic OutOfMemoryError() {}\n\n\tpublic OutOfMemoryError(String msg) {\n\t\tsuper(msg);\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/Readable.java",
    "content": "/*******************************************************************************\r\n * Copyright 2011 See AUTHORS file.\r\n * \r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n * \r\n *   http://www.apache.org/licenses/LICENSE-2.0\r\n * \r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n ******************************************************************************/\r\n\r\npackage java.lang;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.CharBuffer;\r\n\r\npublic interface Readable {\r\n\tint read (CharBuffer cb) throws IOException;\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/Runnable.java",
    "content": "package java.lang;\n\nimport jsinterop.annotations.*;\n\n@FunctionalInterface\npublic interface Runnable {\n\n    @JsMethod\n    void run();\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/RuntimeException.java",
    "content": "/*\n * Copyright 2007 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.lang;\n\n/**\n * See <a\n * href=\"http://java.sun.com/j2se/1.5.0/docs/api/java/lang/RuntimeException.html\">the\n * official Java API doc</a> for details.\n */\npublic class RuntimeException extends Exception {\n\n    public RuntimeException() {\n    }\n\n    public RuntimeException(String message) {\n        super(message);\n    }\n\n    public RuntimeException(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n    public RuntimeException(Throwable cause) {\n        super(cause);\n    }\n\n    protected RuntimeException(String message, Throwable cause, boolean enableSuppression,\n                               boolean writableStackTrace) {\n        super(message, cause, enableSuppression, writableStackTrace);\n    }\n\n    RuntimeException(Object backingJsObject) {\n        super(backingJsObject);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/SecurityException.java",
    "content": "/* Copyright (c) 2008, Avian Contributors\r\n\r\n   Permission to use, copy, modify, and/or distribute this software\r\n   for any purpose with or without fee is hereby granted, provided\r\n   that the above copyright notice and this permission notice appear\r\n   in all copies.\r\n\r\n   There is NO WARRANTY for this software.  See license.txt for\r\n   details. */\r\n\r\npackage java.lang;\r\n\r\npublic class SecurityException extends RuntimeException {\r\n\tpublic SecurityException (String message, Throwable cause) {\r\n\t\tsuper(message, cause);\r\n\t}\r\n\r\n\tpublic SecurityException (String message) {\r\n\t\tthis(message, null);\r\n\t}\r\n\r\n\tpublic SecurityException (Throwable cause) {\r\n\t\tthis(null, cause);\r\n\t}\r\n\r\n\tpublic SecurityException () {\r\n\t\tthis(null, null);\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/lang/Thread.java",
    "content": "/*******************************************************************************\r\n * Copyright 2011 See AUTHORS file.\r\n * \r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n * \r\n *   http://www.apache.org/licenses/LICENSE-2.0\r\n * \r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n ******************************************************************************/\r\n\r\npackage java.lang;\r\n\r\nimport com.google.gwt.core.client.GWT;\r\n\r\npublic class Thread {\r\n\t\r\n\tpublic Thread() {\r\n\t\t\r\n\t}\r\n\t\r\n\tpublic Thread(Runnable runnable) {\r\n\t\t\r\n\t}\r\n\t\r\n\tpublic static Thread currentThread() {\r\n\t\treturn null;\r\n\t}\r\n\t\r\n\tpublic synchronized void start() {\r\n\t\t\r\n\t}\r\n\t\r\n\tpublic ClassLoader getContextClassLoader() {\r\n\t\treturn null;\r\n\t}\r\n\t\r\n\tpublic static void sleep (long millis) throws InterruptedException {\r\n\t\t// noop emu\r\n\t}\r\n\t\r\n\tpublic static void setDefaultUncaughtExceptionHandler(final Thread.UncaughtExceptionHandler javaHandler) {\r\n\t\tGWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() {\r\n\t\t\t@Override\r\n\t\t\tpublic void onUncaughtException (Throwable e) {\r\n\t\t\t\tfinal Thread th = new Thread() {\r\n\t\t\t\t\t@Override\r\n\t\t\t\t\tpublic String toString() {\r\n\t\t\t\t\t\treturn \"The only thread\";\r\n\t\t\t\t\t}\r\n\t\t\t\t};\r\n\t\t\t\tjavaHandler.uncaughtException(th, e);\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\t\r\n\tpublic static interface UncaughtExceptionHandler {\r\n\t\tvoid uncaughtException(Thread t, Throwable e);\r\n\t}\r\n}\r\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/ConnectException.java",
    "content": "package java.net;\n\nimport jsinterop.annotations.*;\n\npublic class ConnectException extends SocketException {\n\n    public ConnectException() {\n        this(\"\");\n    }\n\n    @JsConstructor\n    public ConnectException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/HttpURLConnection.java",
    "content": "package java.net;\n\npublic class HttpURLConnection extends URLConnection{\n\n\tpublic static final int HTTP_OK = 200;\n\tpublic static final int HTTP_NO_CONTENT = 204;\n\n\tpublic void setRequestMethod(String method) throws ProtocolException {\n\t\t\n\t}\n\tpublic void setRequestProperty(String key, String value) {\n\t\t\n\t}\n\tpublic void setUseCaches(boolean b) {\n\n\t}\n\tpublic void setDoInput(boolean b) {\n\t\t\n\t}\n\n\tpublic void setConnectTimeout(int timeout) {}\n\tpublic void setReadTimeout(int timeout) {}\n\tpublic int getResponseCode() {\n\t\treturn HTTP_OK;\n\t}\n\tpublic int getContentLength() {\n\t\treturn -1;\n\t}\n\tpublic void connect() {\n\t}\n\tpublic void disconnect() {\n\t}\n\tpublic String getHeaderField(String name) {\n\t\treturn null;\n\t}\n\n\tpublic String getResponseMessage() {\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/Inet4Address.java",
    "content": "package java.net;\n\npublic class Inet4Address extends InetAddress{\n\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/Inet6Address.java",
    "content": "package java.net;\n\npublic class Inet6Address extends InetAddress{\n\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/InetAddress.java",
    "content": "package java.net;\n\npublic class InetAddress {\n\n\tpublic static InetAddress getByName(String host)\n\t        throws UnknownHostException {\n\t\treturn null;\n\t}\n    public byte[] getAddress() {\n        return null;\n    }\n    public static InetAddress getByAddress(byte[] addr)\n            throws UnknownHostException {\n    \treturn null;\n    }\n    \n    public static InetAddress getLocalHost() throws UnknownHostException {\n    \treturn null;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/MalformedURLException.java",
    "content": "package java.net;\n\nimport java.io.IOException;\n\npublic class MalformedURLException extends IOException {\n\n    public MalformedURLException() {\n    }\n\n    public MalformedURLException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/ProtocolException.java",
    "content": "package java.net;\n\nimport java.io.IOException;\n\npublic class ProtocolException extends IOException{\n    public ProtocolException() {\n    }\n\n    public ProtocolException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/Proxy.java",
    "content": "package java.net;\n\npublic class Proxy {\n    public enum Type {DIRECT, HTTP, SOCKS}\n\n    public Proxy(Type type, SocketAddress addr) {}\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/SocketAddress.java",
    "content": "package java.net;\n\npublic class SocketAddress {\n\t\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/SocketException.java",
    "content": "package java.net;\n\nimport java.io.IOException;\n\npublic class SocketException extends IOException{\n\t\n    public SocketException() {\n    }\n\n    public SocketException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/SocketTimeoutException.java",
    "content": "package java.net;\n\nimport java.io.*;\n\npublic class SocketTimeoutException extends IOException {\n\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/URL.java",
    "content": "package java.net;\n\nimport java.io.InputStream;\n\n\npublic class URL {\n\n\tpublic URL(String protocol, String host, int port, String file) throws MalformedURLException\n\t{\n\t}\n\t\n\tpublic URL(String url) throws MalformedURLException\n\t{\n\t}   \n\t\n\tpublic URL(URL context, String spec) throws MalformedURLException {\n\t\t\n\t}\n\t\n    public URLConnection openConnection() throws java.io.IOException {\n    \treturn null;\n    }\n    \n    public URLConnection openConnection(Proxy proxy) throws java.io.IOException {\n    \treturn null;\n    }\n\n    public final InputStream openStream() throws java.io.IOException {\n    \treturn null;\n    }\n    \n    public String getPath() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/URLConnection.java",
    "content": "package java.net;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic class URLConnection {\n\n\tpublic InputStream getInputStream() throws IOException {\n\t\treturn null;\n\t}\n\tpublic OutputStream getOutputStream() throws IOException {\n\t\treturn null;\n\t}\n    public InputStream getErrorStream() throws IOException {\n\t\treturn null;\n\t}\n\tpublic Map<String,List<String>> getHeaderFields() {\n        return Collections.emptyMap();\n    }\n    public void setDoOutput(boolean dooutput) {\n    \t\n    }\n    public String getContentEncoding() {\n    \treturn null;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/URLEncoder.java",
    "content": "/*\n *  Licensed to the Apache Software Foundation (ASF) under one or more\n *  contributor license agreements.  See the NOTICE file distributed with\n *  this work for additional information regarding copyright ownership.\n *  The ASF licenses this file to You under the Apache License, Version 2.0\n *  (the \"License\"); you may not use this file except in compliance with\n *  the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage java.net;\n\nimport java.io.UnsupportedEncodingException;\n\n/**\n * This class is used to encode a string using the format required by\n * {@code application/x-www-form-urlencoded} MIME content type.\n */\npublic class URLEncoder {\n\n    static final String digits = \"0123456789ABCDEF\";\n\n    /**\n     * Prevents this class from being instantiated.\n     */\n    private URLEncoder() {\n    }\n\n    /**\n     * Encodes a given string {@code s} in a x-www-form-urlencoded string using\n     * the specified encoding scheme {@code enc}.\n     * <p>\n     * All characters except letters ('a'..'z', 'A'..'Z') and numbers ('0'..'9')\n     * and characters '.', '-', '*', '_' are converted into their hexadecimal\n     * value prepended by '%'. For example: '#' -> %23. In addition, spaces are\n     * substituted by '+'\n     *\n     * @param s\n     *            the string to be encoded.\n     * @return the encoded string.\n     * @deprecated use {@link #encode(String, String)} instead.\n     */\n    @Deprecated\n    public static String encode(String s) {\n        // Guess a bit bigger for encoded form\n        StringBuilder buf = new StringBuilder(s.length() + 16);\n        for (int i = 0; i < s.length(); i++) {\n            char ch = s.charAt(i);\n            if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n                    || (ch >= '0' && ch <= '9') || \".-*_\".indexOf(ch) > -1) {\n                buf.append(ch);\n            } else if (ch == ' ') {\n                buf.append('+');\n            } else {\n                byte[] bytes = new String(new char[] { ch }).getBytes();\n                for (int j = 0; j < bytes.length; j++) {\n                    buf.append('%');\n                    buf.append(digits.charAt((bytes[j] & 0xf0) >> 4));\n                    buf.append(digits.charAt(bytes[j] & 0xf));\n                }\n            }\n        }\n        return buf.toString();\n    }\n\n    /**\n     * Encodes the given string {@code s} in a x-www-form-urlencoded string\n     * using the specified encoding scheme {@code enc}.\n     * <p>\n     * All characters except letters ('a'..'z', 'A'..'Z') and numbers ('0'..'9')\n     * and characters '.', '-', '*', '_' are converted into their hexadecimal\n     * value prepended by '%'. For example: '#' -> %23. In addition, spaces are\n     * substituted by '+'\n     *\n     * @param s\n     *            the string to be encoded.\n     * @param enc\n     *            the encoding scheme to be used.\n     * @return the encoded string.\n     * @throws UnsupportedEncodingException\n     *             if the specified encoding scheme is invalid.\n     */\n    public static String encode(String s, String enc) throws UnsupportedEncodingException {\n        if (s == null || enc == null) {\n            throw new NullPointerException();\n        }\n        // check for UnsupportedEncodingException\n        \"\".getBytes(enc);\n\n        // Guess a bit bigger for encoded form\n        StringBuilder buf = new StringBuilder(s.length() + 16);\n        int start = -1;\n        for (int i = 0; i < s.length(); i++) {\n            char ch = s.charAt(i);\n            if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')\n                    || (ch >= '0' && ch <= '9') || \" .-*_\".indexOf(ch) > -1) {\n                if (start >= 0) {\n                    convert(s.substring(start, i), buf, enc);\n                    start = -1;\n                }\n                if (ch != ' ') {\n                    buf.append(ch);\n                } else {\n                    buf.append('+');\n                }\n            } else {\n                if (start < 0) {\n                    start = i;\n                }\n            }\n        }\n        if (start >= 0) {\n            convert(s.substring(start, s.length()), buf, enc);\n        }\n        return buf.toString();\n    }\n\n    private static void convert(String s, StringBuilder buf, String enc)\n            throws UnsupportedEncodingException {\n        byte[] bytes = s.getBytes(enc);\n        for (int j = 0; j < bytes.length; j++) {\n            buf.append('%');\n            buf.append(digits.charAt((bytes[j] & 0xf0) >> 4));\n            buf.append(digits.charAt(bytes[j] & 0xf));\n        }\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/net/UnknownHostException.java",
    "content": "package java.net;\n\nimport java.io.IOException;\n\npublic class UnknownHostException extends IOException{\n\n    public UnknownHostException() {\n    }\n\n    public UnknownHostException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/Buffer.java",
    "content": "/* Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n * \n *     http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage java.nio;\n\n/** A buffer is a list of elements of a specific primitive type.\n * <p>\n * A buffer can be described by the following properties:\n * <ul>\n * <li>Capacity: the number of elements a buffer can hold. Capacity may not be negative and never changes.</li>\n * <li>Position: a cursor of this buffer. Elements are read or written at the position if you do not specify an index explicitly.\n * Position may not be negative and not greater than the limit.</li>\n * <li>Limit: controls the scope of accessible elements. You can only read or write elements from index zero to\n * <code>limit - 1</code>. Accessing elements out of the scope will cause an exception. Limit may not be negative and not greater\n * than capacity.</li>\n * <li>Mark: used to remember the current position, so that you can reset the position later. Mark may not be negative and no\n * greater than position.</li>\n * <li>A buffer can be read-only or read-write. Trying to modify the elements of a read-only buffer will cause a\n * <code>ReadOnlyBufferException</code>, while changing the position, limit and mark of a read-only buffer is OK.</li>\n * <li>A buffer can be direct or indirect. A direct buffer will try its best to take advantage of native memory APIs and it may\n * not stay in the Java heap, thus it is not affected by garbage collection.</li>\n * </ul>\n * </p>\n * <p>\n * Buffers are not thread-safe. If concurrent access to a buffer instance is required, then the callers are responsible to take\n * care of the synchronization issues.\n * </p>\n * \n * @since Android 1.0 */\npublic abstract class Buffer {\n\n\t/** <code>UNSET_MARK</code> means the mark has not been set. */\n\tfinal static int UNSET_MARK = -1;\n\n\t/** The capacity of this buffer, which never change. */\n\tfinal int capacity;\n\n\t/** <code>limit - 1</code> is the last element that can be read or written. Limit must be no less than zero and no greater than\n\t * <code>capacity</code>. */\n\tint limit;\n\n\t/** Mark is where position will be set when <code>reset()</code> is called. Mark is not set by default. Mark is always no less\n\t * than zero and no greater than <code>position</code>. */\n\tint mark = UNSET_MARK;\n\n\t/** The current position of this buffer. Position is always no less than zero and no greater than <code>limit</code>. */\n\tint position = 0;\n\n\t/** Construct a buffer with the specified capacity.\n\t * \n\t * @param capacity the capacity of this buffer. */\n\tBuffer (int capacity) {\n\t\tsuper();\n\t\tif (capacity < 0) {\n\t\t\tthrow new IllegalArgumentException();\n\t\t}\n\t\tthis.capacity = this.limit = capacity;\n\t}\n\n\t/** Returns the capacity of this buffer.\n\t * \n\t * @return the number of elements that are contained in this buffer.\n\t * @since Android 1.0 */\n\tpublic final int capacity () {\n\t\treturn capacity;\n\t}\n\n\t/** Clears this buffer.\n\t * <p>\n\t * While the content of this buffer is not changed, the following internal changes take place: the current position is reset\n\t * back to the start of the buffer, the value of the buffer limit is made equal to the capacity and mark is cleared.\n\t * </p>\n\t * \n\t * @return this buffer.\n\t * @since Android 1.0 */\n\tpublic final Buffer clear () {\n\t\tposition = 0;\n\t\tmark = UNSET_MARK;\n\t\tlimit = capacity;\n\t\treturn this;\n\t}\n\n\t/** Flips this buffer.\n\t * <p>\n\t * The limit is set to the current position, then the position is set to zero, and the mark is cleared.\n\t * </p>\n\t * <p>\n\t * The content of this buffer is not changed.\n\t * </p>\n\t * \n\t * @return this buffer.\n\t * @since Android 1.0 */\n\tpublic final Buffer flip () {\n\t\tlimit = position;\n\t\tposition = 0;\n\t\tmark = UNSET_MARK;\n\t\treturn this;\n\t}\n\n\t/** Indicates if there are elements remaining in this buffer, that is if {@code position < limit}.\n\t * \n\t * @return {@code true} if there are elements remaining in this buffer, {@code false} otherwise.\n\t * @since Android 1.0 */\n\tpublic final boolean hasRemaining () {\n\t\treturn position < limit;\n\t}\n\n\t/** Indicates whether this buffer is read-only.\n\t * \n\t * @return {@code true} if this buffer is read-only, {@code false} otherwise.\n\t * @since Android 1.0 */\n\tpublic abstract boolean isReadOnly ();\n\n\t/** Returns the limit of this buffer.\n\t * \n\t * @return the limit of this buffer.\n\t * @since Android 1.0 */\n\tpublic final int limit () {\n\t\treturn limit;\n\t}\n\n\t/** Sets the limit of this buffer.\n\t * <p>\n\t * If the current position in the buffer is in excess of <code>newLimit</code> then, on returning from this call, it will have\n\t * been adjusted to be equivalent to <code>newLimit</code>. If the mark is set and is greater than the new limit, then it is\n\t * cleared.\n\t * </p>\n\t * \n\t * @param newLimit the new limit, must not be negative and not greater than capacity.\n\t * @return this buffer.\n\t * @exception IllegalArgumentException if <code>newLimit</code> is invalid.\n\t * @since Android 1.0 */\n\tpublic final Buffer limit (int newLimit) {\n\t\tif (newLimit < 0 || newLimit > capacity) {\n\t\t\tthrow new IllegalArgumentException();\n\t\t}\n\n\t\tlimit = newLimit;\n\t\tif (position > newLimit) {\n\t\t\tposition = newLimit;\n\t\t}\n\t\tif ((mark != UNSET_MARK) && (mark > newLimit)) {\n\t\t\tmark = UNSET_MARK;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/** Marks the current position, so that the position may return to this point later by calling <code>reset()</code>.\n\t * \n\t * @return this buffer.\n\t * @since Android 1.0 */\n\tpublic final Buffer mark () {\n\t\tmark = position;\n\t\treturn this;\n\t}\n\n\t/** Returns the position of this buffer.\n\t * \n\t * @return the value of this buffer's current position.\n\t * @since Android 1.0 */\n\tpublic final int position () {\n\t\treturn position;\n\t}\n\n\t/** Sets the position of this buffer.\n\t * <p>\n\t * If the mark is set and it is greater than the new position, then it is cleared.\n\t * </p>\n\t * \n\t * @param newPosition the new position, must be not negative and not greater than limit.\n\t * @return this buffer.\n\t * @exception IllegalArgumentException if <code>newPosition</code> is invalid.\n\t * @since Android 1.0 */\n\tpublic final Buffer position (int newPosition) {\n\t\tif (newPosition < 0 || newPosition > limit) {\n\t\t\tthrow new IllegalArgumentException();\n\t\t}\n\n\t\tposition = newPosition;\n\t\tif ((mark != UNSET_MARK) && (mark > position)) {\n\t\t\tmark = UNSET_MARK;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/** Returns the number of remaining elements in this buffer, that is {@code limit - position}.\n\t * \n\t * @return the number of remaining elements in this buffer.\n\t * @since Android 1.0 */\n\tpublic final int remaining () {\n\t\treturn limit - position;\n\t}\n\n\t/** Resets the position of this buffer to the <code>mark</code>.\n\t * \n\t * @return this buffer.\n\t * @exception InvalidMarkException if the mark is not set.\n\t * @since Android 1.0 */\n\tpublic final Buffer reset () {\n\t\tif (mark == UNSET_MARK) {\n\t\t\tthrow new InvalidMarkException();\n\t\t}\n\t\tposition = mark;\n\t\treturn this;\n\t}\n\n\t/** Rewinds this buffer.\n\t * <p>\n\t * The position is set to zero, and the mark is cleared. The content of this buffer is not changed.\n\t * </p>\n\t * \n\t * @return this buffer.\n\t * @since Android 1.0 */\n\tpublic final Buffer rewind () {\n\t\tposition = 0;\n\t\tmark = UNSET_MARK;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/BufferOverflowException.java",
    "content": "/* Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n * \n *     http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage java.nio;\n\n/** A <code>BufferOverflowException</code> is thrown when elements are written to a buffer but there is not enough remaining space\n * in the buffer.\n * \n * @since Android 1.0 */\npublic class BufferOverflowException extends RuntimeException {\n\n\tprivate static final long serialVersionUID = -5484897634319144535L;\n\n\t/** Constructs a <code>BufferOverflowException</code>.\n\t * \n\t * @since Android 1.0 */\n\tpublic BufferOverflowException () {\n\t\tsuper();\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/CharBuffer.java",
    "content": "/*\n *  Licensed to the Apache Software Foundation (ASF) under one or more\n *  contributor license agreements.  See the NOTICE file distributed with\n *  this work for additional information regarding copyright ownership.\n *  The ASF licenses this file to You under the Apache License, Version 2.0\n *  (the \"License\"); you may not use this file except in compliance with\n *  the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage java.nio;\n\n/** A buffer of chars.\n * <p>\n * A char buffer can be created in either one of the following ways:\n * </p>\n * <ul>\n * <li>{@link #allocate(int) Allocate} a new char array and create a buffer based on it;</li>\n * <li>{@link #wrap(char[]) Wrap} an existing char array to create a new buffer;</li>\n * <li>{@link #wrap(CharSequence) Wrap} an existing char sequence to create a new buffer;</li>\n * <li>Use {@link java.nio.ByteBuffer#asCharBuffer() ByteBuffer.asCharBuffer} to create a char buffer based on a byte buffer.</li>\n * </ul>\n * \n * @since Android 1.0 */\npublic abstract class CharBuffer extends Buffer implements Comparable<CharBuffer>, CharSequence, Appendable {// , Readable {\n\n\t/** Constructs a {@code CharBuffer} with given capacity.\n\t * \n\t * @param capacity the capacity of the buffer.\n\t * @since Android 1.0 */\n\tCharBuffer (int capacity) {\n\t\tsuper(capacity);\n\t}\n\n\t/** Writes the given char to the current position and increases the position by 1.\n\t * \n\t * @param c the char to write.\n\t * @return this buffer.\n\t * @exception BufferOverflowException if position is equal or greater than limit.\n\t * @exception ReadOnlyBufferException if no changes may be made to the contents of this buffer.\n\t * @since Android 1.0 */\n\tpublic abstract CharBuffer put (char c);\n\n\t/** Writes chars from the given char array to the current position and increases the position by the number of chars written.\n\t * <p>\n\t * Calling this method has the same effect as {@code put(src, 0, src.length)}.\n\t * </p>\n\t * \n\t * @param src the source char array.\n\t * @return this buffer.\n\t * @exception BufferOverflowException if {@code remaining()} is less than {@code src.length}.\n\t * @exception ReadOnlyBufferException if no changes may be made to the contents of this buffer.\n\t * @since Android 1.0 */\n\tpublic final CharBuffer put (char[] src) {\n\t\treturn put(src, 0, src.length);\n\t}\n\n\t/** Writes chars from the given char array, starting from the specified offset, to the current position and increases the\n\t * position by the number of chars written.\n\t * \n\t * @param src the source char array.\n\t * @param off the offset of char array, must not be negative and not greater than {@code src.length}.\n\t * @param len the number of chars to write, must be no less than zero and no greater than {@code src.length - off}.\n\t * @return this buffer.\n\t * @exception BufferOverflowException if {@code remaining()} is less than {@code len}.\n\t * @exception IndexOutOfBoundsException if either {@code off} or {@code len} is invalid.\n\t * @exception ReadOnlyBufferException if no changes may be made to the contents of this buffer.\n\t * @since Android 1.0 */\n\tpublic CharBuffer put (char[] src, int off, int len) {\n\t\tint length = src.length;\n\t\tif ((off < 0) || (len < 0) || (long)off + (long)len > length) {\n\t\t\tthrow new IndexOutOfBoundsException();\n\t\t}\n\n\t\tif (len > remaining()) {\n\t\t\tthrow new BufferOverflowException();\n\t\t}\n\t\tfor (int i = off; i < off + len; i++) {\n\t\t\tput(src[i]);\n\t\t}\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/InvalidMarkException.java",
    "content": "/* Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n * \n *     http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage java.nio;\n\n/** An {@code InvalidMarkException} is thrown when {@code reset()} is called on a buffer, but no mark has been set previously.\n * \n * @since Android 1.0 */\npublic class InvalidMarkException extends IllegalStateException {\n\n\tprivate static final long serialVersionUID = 1698329710438510774L;\n\n\t/** Constructs an {@code InvalidMarkException}.\n\t * \n\t * @since Android 1.0 */\n\tpublic InvalidMarkException () {\n\t\tsuper();\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/file/Files.java",
    "content": "package java.nio.file;\n\nimport java.io.IOException;\nimport java.nio.file.attribute.FileAttribute;\nimport java.util.stream.Stream;\n\npublic final class Files {\n\n    public static Path write(Path path, byte[] bytes, OpenOption... options) throws IOException {\n        return null;\n    }\n\n    public static void delete(Path path) throws IOException {\n\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/file/OpenOption.java",
    "content": "package java.nio.file;\n\npublic interface OpenOption {\n\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/file/Path.java",
    "content": "package java.nio.file;\n\nimport java.io.File;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class Path {\n\n    private static final String SEPARATOR = \"/\";\n\n    private final String pathString;\n\n    protected Path (String pathString) {\n        this.pathString = pathString;\n    }\n\n    public String toString()\n    {\n        return pathString;\n    }\n\n    public Path getParent() {\n\n        int index = pathString.lastIndexOf(SEPARATOR);\n        if (index == -1) {\n            return null;\n        } else if (index == 0) {\n            String name = pathString.substring(index);\n            if(name.equals(SEPARATOR)) {\n                return null;\n            }\n            return new Path(SEPARATOR);\n        } else {\n            return new Path(pathString.substring(0, index));\n        }\n    }\n\n    public Path getFileName() {\n        int idx = pathString.lastIndexOf(SEPARATOR);\n        if (idx == -1) {\n            return new Path(pathString);\n        }\n        String filename = pathString.substring(idx+1);\n        return new Path(filename);\n    }\n\n    public Path resolve(String other) {\n        if(other.startsWith(SEPARATOR)) {\n            return new Path(other);\n        }\n        if (pathString.endsWith(SEPARATOR))\n            return new Path(pathString + other);\n        return new Path(pathString + \"/\" + other);\n    }\n\n    public Path relativize(Path other) {\n        return new Path(pathString.substring(other.pathString.length()));\n    }\n\n    public Path resolve(Path other) {\n        return resolve(other.pathString);\n    }\n\n    public boolean isAbsolute() {\n        return pathString.startsWith(\"/\");\n    }\n\n    public boolean startsWith(Path other) {\n        return pathString.startsWith(other.pathString);\n    }\n\n    public File toFile() {\n        return new File(toString());\n    }\n\n    public Path getName(int index) {\n        if (index < 0) {\n            throw new IllegalArgumentException();\n        }\n        String withoutLeadingSlash = pathString.startsWith(SEPARATOR) ? pathString.substring(1)\n                : pathString;\n        String[] parts = withoutLeadingSlash.split(SEPARATOR);\n        if (index >= parts.length) {\n            throw new IllegalArgumentException();\n        }\n        return new Path(parts[index]);\n    }\n\n    public int getNameCount() {\n        if (pathString.length() == 0) {\n            return 1;\n        }\n        String withoutLeadingSlash = pathString.startsWith(SEPARATOR) ? pathString.substring(1)\n                : pathString;\n        return 1 + withoutLeadingSlash.length() - withoutLeadingSlash.replace(SEPARATOR, \"\").length();\n    }\n\n    public Path subpath(int from, int to) {\n        if (from < 0) {\n            throw new IllegalArgumentException();\n        }\n        String withoutLeadingSlash = pathString.startsWith(SEPARATOR) ? pathString.substring(1)\n                : pathString;\n        String[] parts = withoutLeadingSlash.split(SEPARATOR);\n        if (to > parts.length) {\n            throw new IllegalArgumentException();\n        }\n        StringBuilder sb = new StringBuilder();\n        for(int i = from; i < to; i++) {\n            sb.append(parts[i]);\n            if(i < to -1){\n                sb.append(SEPARATOR);\n            }\n        }\n        return new Path(sb.toString());\n    }\n\n    public static Path of(String name) {\n        return new Path(name);\n    }\n\n    public static Path of(String name, String... rest) {\n        if (rest.length == 0)\n            return new Path(name);\n        return new Path(name + \"/\" + Arrays.stream(rest).collect(Collectors.joining(\"/\")));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Path path = (Path) o;\n        return Objects.equals(pathString, path.pathString);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(pathString);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/file/Paths.java",
    "content": "package java.nio.file;\n\n\nimport jsinterop.annotations.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class Paths {\n\n    private static final String ERROR_MSG_NULL_PATH = \"Paths.get() does not support null path\";\n    private static final String ERROR_MSG_VARARGS = \"Paths.get() does not support varargs\";\n    private static final String ERROR_MSG_UNINITIALISED = \"Paths.get() does not support uninitialised path string\";\n\n    @JsMethod\n    public static Path get(String firstPath, String... pathString) {\n        if (firstPath == null) {\n            throw new IllegalArgumentException(ERROR_MSG_NULL_PATH);\n        }\n        if (firstPath.equals(\"\")) {\n            return new Path(\"\");\n        }\n        return new Path(Stream.concat(Stream.of(firstPath), Arrays.stream(pathString)).collect(Collectors.joining(\"/\")));\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/file/StandardOpenOption.java",
    "content": "package java.nio.file;\n\npublic enum StandardOpenOption implements OpenOption {\n    READ, WRITE, APPEND,\n    TRUNCATE_EXISTING, CREATE, CREATE_NEW,\n    DELETE_ON_CLOSE, SPARSE, SYNC, DSYNC;\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/nio/file/attribute/FileAttribute.java",
    "content": "package java.nio.file.attribute;\n\npublic interface FileAttribute<T> {\n    String name();\n    T value();\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/security/SecureRandom.java",
    "content": "package java.security;\n\npublic class SecureRandom extends java.util.Random{\n\n\tpublic SecureRandom()\n\t{\n\t\t\n\t}\n\t\n    public static SecureRandom getInstance(String algorithm) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/Clock.java",
    "content": "package java.time;\n\n\npublic class Clock {\n\n\tprivate final ZoneId zone = new ZoneId();\n\t\n    Clock() {\n    }\n    public ZoneId getZone() {\n    \treturn zone;\n    }\n    public static Clock systemDefaultZone() {\n        return new Clock();\n    }\n    public long millis() {\n        return System.currentTimeMillis();\n    }\n\n    public Instant instant() {\n        return Instant.ofEpochMilli(millis());\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/DateTimeException.java",
    "content": "package java.time;\n\npublic class DateTimeException extends RuntimeException {\n\n    public DateTimeException(String message) {\n        super(message);\n    }\n\n    public DateTimeException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/Instant.java",
    "content": "package java.time;\n\npublic class Instant {\n\n    public static final Instant EPOCH = new Instant(0, 0);\n\n    private static final long MIN_SECOND = -31557014167219200L;\n\n    private static final long MAX_SECOND = 31556889864403199L;\n    \n    private final long seconds;\n    private final int nanos;\n    \n    private Instant(long epochSecond, int nanos) {\n        super();\n        this.seconds = epochSecond;\n        this.nanos = nanos;\n    }\n    public int getNano() {\n    \treturn nanos;\n    }\n    public long getEpochSecond() {\n    \treturn seconds;\n    }\n    \n    private static Instant create(long seconds, int nanoOfSecond) {\n        if ((seconds | nanoOfSecond) == 0) {\n            return EPOCH;\n        }\n        if (seconds < MIN_SECOND || seconds > MAX_SECOND) {\n            throw new DateTimeException(\"Instant exceeds minimum or maximum instant\");\n        }\n        return new Instant(seconds, nanoOfSecond);\n    }\n    \n    public static Instant ofEpochSecond(long epochSecond) {\n        return create(epochSecond, 0);\n    }\n    \n    public static Instant ofEpochMilli(long epochMilli) {\n        long secs = Math.floorDiv(epochMilli, 1000);\n        int mos = (int)Math.floorMod(epochMilli, 1000);\n        return create(secs, mos * 1000_000);\n    }\n    \n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/LocalDate.java",
    "content": "package java.time;\n\nimport jsinterop.annotations.JsType;\n\nimport java.time.chrono.ChronoLocalDate;\nimport java.time.chrono.IsoChronology;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeParseException;\nimport java.util.Objects;\n\n@SuppressWarnings(\"unusable-by-js\")\npublic class LocalDate implements Comparable<LocalDate>{\n\n    private final int year;\n    private final short month;\n    private final short day;\n\t\n\tprivate static final int DAYS_PER_CYCLE = 146097;\n\tstatic final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);\n\tpublic static final int YEAR_MAX_VALUE = 999_999_999;\n\tpublic static final int YEAR_MIN_VALUE = -999_999_999;\n    static final int HOURS_PER_DAY = 24;\n    static final int MINUTES_PER_HOUR = 60;\n    static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY;\n    static final int SECONDS_PER_MINUTE = 60;\n    static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;\n    static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;\n\n\t\n\tpublic static final LocalDate MAX = LocalDate.of(YEAR_MAX_VALUE, 12, 31);\n\tpublic static final LocalDate MIN = LocalDate.of(YEAR_MIN_VALUE, 1, 1);\n\t\n    public LocalDate(int year, int month, int dayOfMonth) {\n        this.year = year;\n        this.month = (short) month;\n        this.day = (short) dayOfMonth;\n    }\n\n    public LocalDate plusDays(int days) {\n        return ofEpochDay(toEpochDay() + days);\n    }\n    \n    public static LocalDate of(int year, int month, int dayOfMonth) {\n    \treturn new LocalDate(year, month, dayOfMonth);\n    }\n    \n    public static LocalDate parse(String text) {\n    \t//ie '2011-12-03'\n    \ttry {\n\t    \tif(text.trim().length() == 10) {\n\t    \t\tString trimmed = text.trim();\n\t    \t\tint year = Integer.valueOf(trimmed.substring(0,4));\n\t    \t\tint month = Integer.valueOf(trimmed.substring(5,7));\n\t    \t\tint dayOfMonth = Integer.valueOf(trimmed.substring(8,10));\n\t    \t\treturn of(year, month, dayOfMonth);\n\t    \t}\n            String[] parts = text.trim().split(\"-\");\n            int year = Integer.valueOf(parts[0]);\n            int month = Integer.valueOf(parts[1]);\n            int dayOfMonth = Integer.valueOf(parts[2]);\n            return of(year, month, dayOfMonth);\n        }catch(Exception e){\n            throw new IllegalStateException(\"Unable to parse:\" + text);\n        }\n    }\n    \n    public static LocalDate now(Clock clock) {\n        final Instant now = clock.instant();  // called once\n        ZoneOffset offset = clock.getZone().getRules().getOffset(now);\n        long epochSec = now.getEpochSecond() + offset.getTotalSeconds();  // overflow caught later\n        long epochDay = Math.floorDiv(epochSec, SECONDS_PER_DAY);\n        return LocalDate.ofEpochDay(epochDay);\n    }\n\n    public int getYear() {\n        return this.year;\n    }\n\n    public int getMonthValue() {\n        return this.month;\n    }\n    \n    public static LocalDate ofEpochDay(long epochDay) {\n        long zeroDay = epochDay + DAYS_0000_TO_1970;\n        // find the march-based year\n        zeroDay -= 60;  // adjust to 0000-03-01 so leap day is at end of four year cycle\n        long adjust = 0;\n        if (zeroDay < 0) {\n            // adjust negative years to positive for calculation\n            long adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1;\n            adjust = adjustCycles * 400;\n            zeroDay += -adjustCycles * DAYS_PER_CYCLE;\n        }\n        long yearEst = (400 * zeroDay + 591) / DAYS_PER_CYCLE;\n        long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);\n        if (doyEst < 0) {\n            // fix estimate\n            yearEst--;\n            doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);\n        }\n        yearEst += adjust;  // reset any negative year\n        int marchDoy0 = (int) doyEst;\n\n        // convert march-based values back to january-based\n        int marchMonth0 = (marchDoy0 * 5 + 2) / 153;\n        int month = (marchMonth0 + 2) % 12 + 1;\n        int dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1;\n        yearEst += marchMonth0 / 10;\n\n        int year = (int)yearEst;\n        return new LocalDate(year, month, dom);\n    }\n    \n    public static LocalDate now() {\n        return now(Clock.systemDefaultZone());\n    }\n    \n    public static boolean isLeapYear(long prolepticYear) {\n        return ((prolepticYear & 3) == 0) && ((prolepticYear % 100) != 0 || (prolepticYear % 400) == 0);\n    }\n    \n    public long toEpochDay() {\n        long y = year;\n        long m = month;\n        long total = 0;\n        total += 365 * y;\n        if (y >= 0) {\n            total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400;\n        } else {\n            total -= y / -4 - y / -100 + y / -400;\n        }\n        total += ((367 * m - 362) / 12);\n        total += day - 1;\n        if (m > 2) {\n            total--;\n            if (isLeapYear(year) == false) {\n                total--;\n            }\n        }\n        return total - DAYS_0000_TO_1970;\n    }\n    \n    private static LocalDate resolvePreviousValid(int year, int month, int day) {\n        switch (month) {\n            case 2:\n                day = Math.min(day, isLeapYear(year) ? 29 : 28);\n                break;\n            case 4:\n            case 6:\n            case 9:\n            case 11:\n                day = Math.min(day, 30);\n                break;\n        }\n        return new LocalDate(year, month, day);\n    }\n    \n    public LocalDate plusMonths(long monthsToAdd) {\n        if (monthsToAdd == 0) {\n            return this;\n        }\n        long monthCount = year * 12L + (month - 1);\n        long calcMonths = monthCount + monthsToAdd;  // safe overflow\n        int newYear = (int)Math.floorDiv(calcMonths, 12);\n        int newMonth = (int)Math.floorMod(calcMonths, 12) + 1;\n        return resolvePreviousValid(newYear, newMonth, day);\n    }\n\n    \n    public boolean isBefore(LocalDate other) {\n        return compareTo((LocalDate) other) < 0;\n    }\n\n    public boolean isAfter(LocalDate other) {\n        return compareTo((LocalDate) other) > 0;\n    }\n    \n    public LocalDate plusWeeks(long weeksToAdd) {\n    \treturn null;\n    }\n    \n    public LocalDate plusYears(long yearsToAdd) {\n    \treturn null;\n    }\n\n    @Override\n    public int compareTo(LocalDate otherDate) {\n        int cmp = (year - otherDate.year);\n        if (cmp == 0) {\n            cmp = (month - otherDate.month);\n            if (cmp == 0) {\n                cmp = (day - otherDate.day);\n            }\n        }\n        return cmp;\n    }\n    \n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (obj instanceof LocalDate) {\n            return compareTo((LocalDate) obj) == 0;\n        }\n        return false;\n    }\n\n    @Override\n    public int hashCode() {\n        int yearValue = year;\n        int monthValue = month;\n        int dayValue = day;\n        return (yearValue & 0xFFFFF800) ^ ((yearValue << 11) + (monthValue << 6) + (dayValue));\n    }\n\n    @Override\n    public String toString() {\n        int yearValue = year;\n        int monthValue = month;\n        int dayValue = day;\n        int absYear = Math.abs(yearValue);\n        StringBuilder buf = new StringBuilder(10);\n        if (absYear < 1000) {\n            if (yearValue < 0) {\n                buf.append(yearValue - 10000).deleteCharAt(1);\n            } else {\n                buf.append(yearValue + 10000).deleteCharAt(0);\n            }\n        } else {\n            if (yearValue > 9999) {\n                buf.append('+');\n            }\n            buf.append(yearValue);\n        }\n        return buf.append(monthValue < 10 ? \"-0\" : \"-\")\n            .append(monthValue)\n            .append(dayValue < 10 ? \"-0\" : \"-\")\n            .append(dayValue)\n            .toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/LocalDateTime.java",
    "content": "package java.time;\n\nimport jsinterop.annotations.*;\n\nimport java.time.chrono.ChronoLocalDateTime;\nimport java.time.format.DateTimeFormatter;\n\npublic class LocalDateTime implements Comparable<LocalDateTime>{\n\n\tprivate LocalDate date;\n\tprivate LocalTime time;\n\t\n\tstatic final long NANOS_PER_SECOND = 1000_000_000L;\n    static final int HOURS_PER_DAY = 24;\n    static final int MINUTES_PER_HOUR = 60;\n    static final int SECONDS_PER_MINUTE = 60;\n    static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;\n    static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;\n\t\n\tpublic static final LocalDateTime MIN = LocalDateTime.of(LocalDate.MIN, LocalTime.MIN);\n\t\n    private LocalDateTime(LocalDate date, LocalTime time) {\n        this.date = date;\n        this.time = time;\n    }\n    \n    public static LocalDateTime of(LocalDate date, LocalTime time) {\n        return new LocalDateTime(date, time);\n    }\n    \n\tpublic LocalTime toLocalTime() {\n\t\treturn time;\n\t}\n\tpublic LocalDate toLocalDate() {\n\t\treturn date;\n\t}\n\n    @JsMethod\n    public long toEpochSecond(ZoneOffset offset) {\n        long epochDay = toLocalDate().toEpochDay();\n        long secs = epochDay * 86400 + toLocalTime().toSecondOfDay();\n        secs -= offset.getTotalSeconds();\n        return secs;\n    }\n\n    public static LocalDateTime ofEpochSecond(long epochSecond, int nanoOfSecond, ZoneOffset offset) {\n        long localSecond = epochSecond + offset.getTotalSeconds();  // overflow caught later\n        long localEpochDay = Math.floorDiv(localSecond, SECONDS_PER_DAY);\n        int secsOfDay = (int)Math.floorMod(localSecond, SECONDS_PER_DAY);\n        LocalDate date = LocalDate.ofEpochDay(localEpochDay);\n        LocalTime time = LocalTime.ofNanoOfDay(secsOfDay * NANOS_PER_SECOND + nanoOfSecond);\n        return new LocalDateTime(date, time);\n    }\n\n    public static LocalDateTime now(Clock clock) {\n        final Instant now = clock.instant();  // called once\n        ZoneOffset offset = clock.getZone().getRules().getOffset(now);\n        return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);\n    }\n\n    public static LocalDateTime now(ZoneId zone) {\n        return now(Clock.systemDefaultZone());\n    }\n    \n    public static LocalDateTime now() {\n        return now(Clock.systemDefaultZone());\n    }\n\n    public int getNano() {\n        return time.getNano();\n    }\n\n    public int getYear() {\n        return date.getYear();\n    }\n\n    public int getMonthValue() {\n        return date.getMonthValue();\n    }\n\n    public LocalDateTime plusNanos(long nanos) {\n        return null;\n    }\n    \n    public static LocalDateTime ofInstant(Instant instant, ZoneId zone) {\n    \treturn null;\n    }\n\n    @JsMethod\n    public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second) {\n    \treturn new LocalDateTime(new LocalDate(year, month, dayOfMonth), new LocalTime(hour, minute, second, 0));\n    }\n\n    public Instant toInstant(ZoneOffset offset) {\n        return null;\n    }\n    public boolean isBefore(LocalDateTime other) {\n    \treturn this.compareTo(other) < 0;\n    }\n\n    @JsMethod\n    @Override\n    public int compareTo(LocalDateTime other) {\n        int cmp = date.compareTo(other.toLocalDate());\n        if (cmp == 0) {\n            cmp = time.compareTo(other.toLocalTime());\n        }\n        return cmp;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (obj instanceof LocalDateTime) {\n            LocalDateTime other = (LocalDateTime) obj;\n            return date.equals(other.date) && time.equals(other.time);\n        }\n        return false;\n    }\n\n    @Override\n    public int hashCode() {\n        return date.hashCode() ^ time.hashCode();\n    }\n\n    @Override\n    public String toString() {\n        return date.toString() +\"T\"+ time.toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/LocalTime.java",
    "content": "package java.time;\n\nimport jsinterop.annotations.JsType;\n\n@SuppressWarnings(\"unusable-by-js\")\npublic class LocalTime implements Comparable<LocalTime>{\n\n\tstatic final int SECONDS_PER_MINUTE = 60;\n\tstatic final int MINUTES_PER_HOUR = 60;\n    static final int HOURS_PER_DAY = 24;\n    static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY;\n    static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;\n    \n    \n    static final long NANOS_PER_SECOND = 1000_000_000L;\n    static final long NANOS_PER_MINUTE = NANOS_PER_SECOND * SECONDS_PER_MINUTE;\n    static final long NANOS_PER_HOUR = NANOS_PER_MINUTE * MINUTES_PER_HOUR;\n    \n    public static final LocalTime MIN;\n    public static final LocalTime MAX;\n    public static final LocalTime MIDNIGHT;\n    public static final LocalTime NOON;\n\n    private static final LocalTime[] HOURS = new LocalTime[24];\n    static {\n        for (int i = 0; i < HOURS.length; i++) {\n            HOURS[i] = new LocalTime(i, 0, 0, 0);\n        }\n        MIDNIGHT = HOURS[0];\n        NOON = HOURS[12];\n        MIN = HOURS[0];\n        MAX = new LocalTime(23, 59, 59, 999_999_999);\n    }\n    \n    private final byte hour;\n    private final byte minute;\n    private final byte second;\n    private final int nano;\n    \n    public LocalTime(int hour, int minute, int second, int nanoOfSecond) {\n        this.hour = (byte) hour;\n        this.minute = (byte) minute;\n        this.second = (byte) second;\n        this.nano = nanoOfSecond;\n    }\n\n    public int getNano() {\n        return nano;\n    }\n\n    public int getSecond() {\n        return second;\n    }\n\n    public int getMinute() {\n        return minute;\n    }\n\n    public int getHour() {\n        return hour;\n    }\n\n    public int toSecondOfDay() {\n        int total = hour * SECONDS_PER_HOUR;\n        total += minute * SECONDS_PER_MINUTE;\n        total += second;\n        return total;\n    }\n    \n    public long toNanoOfDay() {\n        long total = hour * NANOS_PER_HOUR;\n        total += minute * NANOS_PER_MINUTE;\n        total += second * NANOS_PER_SECOND;\n        total += nano;\n        return total;\n    }\n    \n    public static LocalTime ofNanoOfDay(long nanoOfDay) {\n        int hours = (int) (nanoOfDay / NANOS_PER_HOUR);\n        nanoOfDay -= hours * NANOS_PER_HOUR;\n        int minutes = (int) (nanoOfDay / NANOS_PER_MINUTE);\n        nanoOfDay -= minutes * NANOS_PER_MINUTE;\n        int seconds = (int) (nanoOfDay / NANOS_PER_SECOND);\n        nanoOfDay -= seconds * NANOS_PER_SECOND;\n        return create(hours, minutes, seconds, (int) nanoOfDay);\n    }\n    \n    private static LocalTime create(int hour, int minute, int second, int nanoOfSecond) {\n        if ((minute | second | nanoOfSecond) == 0) {\n            return HOURS[hour];\n        }\n        return new LocalTime(hour, minute, second, nanoOfSecond);\n    }\n\n    public static LocalTime of(int hour, int minute) {\n        return create(hour, minute, 0, 0);\n    }\n\n    public static LocalTime of(int hour, int minute, int second) {\n        return create(hour, minute, second, 0);\n    }\n\n    @Override\n    public int compareTo(LocalTime other) {\n        int cmp = Integer.compare(hour, other.hour);\n        if (cmp == 0) {\n            cmp = Integer.compare(minute, other.minute);\n            if (cmp == 0) {\n                cmp = Integer.compare(second, other.second);\n                if (cmp == 0) {\n                    cmp = Integer.compare(nano, other.nano);\n                }\n            }\n        }\n        return cmp;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (obj instanceof LocalTime) {\n            LocalTime other = (LocalTime) obj;\n            return hour == other.hour && minute == other.minute &&\n                    second == other.second && nano == other.nano;\n        }\n        return false;\n    }\n    \n    @Override\n    public int hashCode() {\n        long nod = toNanoOfDay();\n        return (int) (nod ^ (nod >>> 32));\n    }\n\n    @Override\n    public String toString() {\n    \tStringBuilder sb = new StringBuilder();\n    \tif (hour < 10) {\n    \t\tsb.append('0');\n    \t}\n    \tsb.append(hour);\n    \tsb.append(\":\");\n    \tif (minute < 10) {\n    \t\tsb.append('0');\n    \t}\n    \tsb.append(minute);\n    \tsb.append(\":\");\n    \tif (second < 10) {\n    \t\tsb.append('0');\n    \t}\n    \tsb.append(second);\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/ZoneId.java",
    "content": "package java.time;\n\npublic class ZoneId {\n\n\tprivate final ZoneRules zoneRules= new ZoneRules();\n\t\n\tpublic ZoneId()\n\t{\n\t\t\n\t}\n\tpublic ZoneRules getRules() {\n\t\treturn zoneRules;\n\t}\n\t\n    public static ZoneId systemDefault() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/ZoneOffset.java",
    "content": "package java.time;\n\n@SuppressWarnings(\"unusable-by-js\")\npublic class ZoneOffset extends ZoneId{\n\n\tpublic static final ZoneOffset UTC = ZoneOffset.ofTotalSeconds(0);\n\t\n\tprivate int totalSeconds;\n\t\n    private ZoneOffset(int totalSeconds) {\n        this.totalSeconds = totalSeconds;\n    }\n    \n\tpublic static ZoneOffset ofTotalSeconds(int totalSeconds) {\n\t\t\treturn new ZoneOffset(totalSeconds);\n\t}\n\t\n    public int getTotalSeconds() {\n        return totalSeconds;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/ZoneRules.java",
    "content": "package java.time;\n\npublic class ZoneRules {\n\n\tpublic ZoneRules()\n\t{\n\t\t\n\t}\n\tpublic ZoneOffset getOffset(Instant instant) {\n\t\treturn ZoneOffset.UTC; //always\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/time/format/DateTimeParseException.java",
    "content": "package java.time.format;\n\n\npublic class DateTimeParseException extends java.time.DateTimeException {\n\n    public DateTimeParseException(String message) {\n        super(message);\n    }\n\n    public DateTimeParseException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/Base64.java",
    "content": "package java.util;\n\nimport com.badlogic.gdx.utils.Base64Coder;\n\npublic class Base64 {\n\tpublic static Decoder getDecoder() {\n\t\treturn new Decoder();\n\t}\n\tpublic static Encoder getEncoder() {\n\t\treturn new Encoder();\n\t}\n\tpublic static class Decoder {\n\t\tpublic byte[] decode(String src) {\n\t\t\treturn Base64Coder.decode(src);\n\t\t}\n\t}\n\tpublic static class Encoder {\n\t\tpublic byte[] encode(byte[] src) {\n\t\t\tchar[] base64 = Base64Coder.encode(src); \n\t\t\tString str = new String(base64);\n\t\t\treturn str.getBytes();\n\t\t}\n\t\tpublic String encodeToString(byte[] src) {\n\t\t\tchar[] base64 = Base64Coder.encode(src); \n\t\t\treturn new String(base64);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/Optional.java",
    "content": "/*\n * Copyright 2015 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.util;\nimport jsinterop.annotations.*;\n\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport java.util.stream.*;\n\nimport static javaemul.internal.InternalPreconditions.checkCriticalElement;\nimport static javaemul.internal.InternalPreconditions.checkCriticalNotNull;\nimport static javaemul.internal.InternalPreconditions.checkNotNull;\n/**\n * See <a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html\">\n * the official Java API doc</a> for details.\n *\n * @param <T> type of the wrapped reference\n */\npublic final class Optional<T> {\n    @SuppressWarnings(\"unchecked\")\n    @JsMethod\n    public static <T> Optional<T> empty() {\n        return (Optional<T>) EMPTY;\n    }\n    @JsMethod\n    public static <T> Optional<T> of(T value) {\n        return new Optional<>(checkCriticalNotNull(value));\n    }\n    public static <T> Optional<T> ofNullable(T value) {\n        return value == null ? empty() : of(value);\n    }\n    private static final Optional<?> EMPTY = new Optional<>(null);\n    private final T ref;\n    private Optional(T ref) {\n        this.ref = ref;\n    }\n    @JsMethod\n    public boolean isPresent() {\n        return ref != null;\n    }\n    @JsMethod\n    public boolean isEmpty() {\n        return ref == null;\n    }\n    @JsMethod\n    public T get() {\n        checkCriticalElement(isPresent());\n        return ref;\n    }\n    public void ifPresent(Consumer<? super T> consumer) {\n        if (isPresent()) {\n            consumer.accept(ref);\n        }\n    }\n\n    public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {\n        Objects.requireNonNull(supplier);\n        if (this.isPresent()) {\n            return this;\n        } else {\n            Optional<T> r = (Optional)supplier.get();\n            return (Optional)Objects.requireNonNull(r);\n        }\n    }\n\n    public Stream<T> stream() {\n        return ! isPresent() ? Stream.empty() : Stream.of(ref);\n    }\n\n    public Optional<T> filter(Predicate<? super T> predicate) {\n        checkNotNull(predicate);\n        if (!isPresent() || predicate.test(ref)) {\n            return this;\n        }\n        return empty();\n    }\n    public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {\n        checkNotNull(mapper);\n        if (isPresent()) {\n            return ofNullable(mapper.apply(ref));\n        }\n        return empty();\n    }\n    public <U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {\n        checkNotNull(mapper);\n        if (isPresent()) {\n            return checkNotNull(mapper.apply(ref));\n        }\n        return empty();\n    }\n    public T orElse(T other) {\n        return isPresent() ? ref : other;\n    }\n    public T orElseGet(Supplier<? extends T> other) {\n        return isPresent() ? ref : other.get();\n    }\n    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {\n        if (isPresent()) {\n            return ref;\n        }\n        throw exceptionSupplier.get();\n    }\n    @Override\n    public boolean equals(Object obj) {\n        if (obj == this) {\n            return true;\n        }\n        if (!(obj instanceof Optional)) {\n            return false;\n        }\n        Optional<?> other = (Optional<?>) obj;\n        return Objects.equals(ref, other.ref);\n    }\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(ref);\n    }\n    @Override\n    public String toString() {\n        return isPresent() ? \"Optional.of(\" + String.valueOf(ref) + \")\" : \"Optional.empty()\";\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/UUID.java",
    "content": "package java.util;\n\npublic final class UUID {\n\n    private final String uuid;\n    private UUID(String uuid) {\n        this.uuid = uuid;\n    }\n    public static UUID randomUUID() {\n        return new UUID(uuid());\n    }\n\n    public String toString() {\n        return uuid;\n    }\n\n    //https://stackoverflow.com/questions/3759590/generate-uuid-with-gwt\n    //lead to this:\n    //http://web.archive.org/web/20160707055604/http://www.pst.ifi.lmu.de/~rauschma/download/UUID.java\n    /*\nFile: Math.uuid.js\nVersion: 1.3\nChange History:\n  v1.0 - first release\n  v1.1 - less code and 2x performance boost (by minimizing calls to Math.random())\n  v1.2 - Add support for generating non-standard uuids of arbitrary length\n  v1.3 - Fixed IE7 bug (can't use []'s to access string chars.  Thanks, Brian R.)\n  v1.4 - Changed method to be \"Math.uuid\". Added support for radix argument.  Use module pattern for better encapsulation.\n\nLatest version:   http://www.broofa.com/Tools/Math.uuid.js\nInformation:      http://www.broofa.com/blog/?p=151\nContact:          robert@broofa.com\n----\nCopyright (c) 2008, Robert Kieffer\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n    * Neither the name of Robert Kieffer nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n    private static final char[] CHARS = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\".toCharArray();\n    /**\n     * Generate a RFC4122, version 4 ID. Example:\n     * \"92329D39-6F5C-4520-ABFC-AAB64544E172\"\n     */\n    private static String uuid() {\n        char[] uuid = new char[36];\n        int r;\n\n        // rfc4122 requires these characters\n        uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';\n        uuid[14] = '4';\n\n        // Fill in random data.  At i==19 set the high bits of clock sequence as\n        // per rfc4122, sec. 4.1.5\n        for (int i = 0; i < 36; i++) {\n            if (uuid[i] == 0) {\n                r = (int) (Math.random()*16);\n                uuid[i] = CHARS[(i == 19) ? (r & 0x3) | 0x8 : r & 0xf];\n            }\n        }\n        return new String(uuid);\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/CompletableFuture.java",
    "content": "package java.util.concurrent;\n\nimport jsinterop.annotations.*;\n\nimport java.util.*;\nimport java.util.function.*;\n\n/** Emulation of CompletableFuture\n *\n */\npublic class CompletableFuture<T> implements Future<T>, CompletionStage<T> {\n\n    public static <T> CompletableFuture<T> completedFuture(T value) {\n        return new CompletableFuture<T>(value);\n    }\n\n    // holders for all the possible consumers of this future\n    private final List<Consumer<? super T>> consumers = new ArrayList<>();\n    private final List<CompletableFuture<Void>> consumeFutures = new ArrayList<>();\n    private final List<Function<? super T, ? extends Object>> applies = new ArrayList<>();\n    private final List<CompletableFuture> applyFutures = new ArrayList<>();\n    private final List<Function<? super T, ? extends CompletionStage<? extends Object>>> composers = new ArrayList<>();\n    private final List<CompletableFuture<? extends Object>> composeFutures = new ArrayList<>();\n    private final List<Function<? super Throwable, ? extends T>> errors = new ArrayList<>();\n    private final List<CompletableFuture<T>> errorFutures = new ArrayList<>();\n    private T value;\n    private Throwable reason;\n    private boolean isDone;\n\n    private CompletableFuture(T value, Throwable err, boolean isDone) {\n        this.value = value;\n        this.reason = err;\n        this.isDone = isDone;\n    }\n\n    public CompletableFuture() {\n        this(null, null, false);\n    }\n\n    private CompletableFuture(T value) {\n        this(value, null, true);\n    }\n\n    private CompletableFuture(Throwable err) {\n        this(null, err, true);\n    }\n\n    public T join() {\n        throw new IllegalStateException(\"Illegal synchronous call!\");\n    }\n\n    @Override\n    @JsMethod\n    public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {\n        CompletableFuture<U> fut = new CompletableFuture<>();\n        if (isDone()) {\n            if (reason != null) {\n                fut.completeExceptionally(reason);\n            } else {\n                try {\n                    fut.complete(fn.apply(value));\n                } catch(Throwable t) {\n                    fut.completeExceptionally(t);\n                }\n            }\n        } else {\n            applyFutures.add(fut);\n            applies.add(fn);\n        }\n        return fut;\n    }\n\n    @Override\n    public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {\n        CompletableFuture<Void> fut = new CompletableFuture<>();\n        if (isDone()) {\n            if (reason != null) {\n                fut.completeExceptionally(reason);\n            } else {\n                try {\n                    action.accept(value);\n                    fut.complete(null);\n                } catch(Throwable t) {\n                    fut.completeExceptionally(t);\n                }\n            }\n        } else {\n            consumeFutures.add(fut);\n            consumers.add(action);\n        }\n        return fut;\n    }\n\n    @Override\n    public <U, V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    @JsMethod\n    public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) {\n        CompletableFuture<U> fut = new CompletableFuture<>();\n        if (isDone()) {\n            if (reason != null) {\n                fut.completeExceptionally(reason);\n            } else {\n                try {\n                    fn.apply(value)\n                            .thenAccept(fut::complete)\n                            .exceptionally(t -> {\n                                fut.completeExceptionally(t);\n                                return null;\n                            });\n                } catch(Throwable t) {\n                    fut.completeExceptionally(t);\n                }\n            }\n        } else {\n            composeFutures.add(fut);\n            composers.add(fn);\n        }\n        return fut;\n    }\n\n    @JsMethod\n    public boolean complete(T value) {\n        this.value = value;\n        this.isDone = true;\n        for (int i = 0; i < applies.size(); i++) {\n            Function<? super T, ? extends Object> function = applies.get(i);\n            CompletableFuture future = applyFutures.get(i);\n            try {\n                future.complete(function.apply(value));\n            } catch (Throwable t) {\n                future.completeExceptionally(t);\n            }\n        }\n        for (int i = 0; i < consumers.size(); i++) {\n            Consumer<? super T> function = consumers.get(i);\n            CompletableFuture<Void> future = consumeFutures.get(i);\n            try {\n                function.accept(value);\n                future.complete(null);\n            } catch (Throwable t) {\n                future.completeExceptionally(t);\n            }\n        }\n        for (int i = 0; i < composers.size(); i++) {\n            Function<? super T, ? extends CompletionStage> function = composers.get(i);\n            CompletableFuture future = composeFutures.get(i);\n            try {\n                function.apply(value)\n                        .thenAccept(val -> future.complete(val))\n                        .exceptionally(t -> {\n                            future.completeExceptionally((Throwable) t);\n                            return null;\n                        });\n            } catch (Throwable t) {\n                future.completeExceptionally(t);\n            }\n        }\n        for (int i = 0; i < errors.size(); i++) {\n            CompletableFuture<T> future = errorFutures.get(i);\n            Function<? super Throwable, ? extends T> function = errors.get(i);\n            try {\n                future.complete(value);\n            } catch (Throwable t) {\n                future.completeExceptionally(t);\n            }\n        }\n        errors.clear();\n        errorFutures.clear();\n        composers.clear();\n        composeFutures.clear();\n        consumers.clear();\n        consumeFutures.clear();\n        applies.clear();\n        applyFutures.clear();\n        return true;\n    }\n\n    @JsMethod\n    public boolean completeExceptionally(Throwable err) {\n        this.reason = err;\n        this.isDone = true;\n        ForkJoinPool.commonPool().execute(() -> {\n            for (int i = 0; i < applies.size(); i++) {\n                CompletableFuture future = applyFutures.get(i);\n                try {\n                    future.completeExceptionally(err);\n                } catch (Throwable t) {\n                    future.completeExceptionally(t);\n                }\n            }\n            for (int i = 0; i < consumers.size(); i++) {\n                CompletableFuture<Void> future = consumeFutures.get(i);\n                try {\n                    future.completeExceptionally(err);\n                } catch (Throwable t) {\n                    future.completeExceptionally(t);\n                }\n            }\n            for (int i = 0; i < composers.size(); i++) {\n                CompletableFuture future = composeFutures.get(i);\n                try {\n                    future.completeExceptionally(err);\n                } catch (Throwable t) {\n                    future.completeExceptionally(t);\n                }\n            }\n            for (int i = 0; i < errors.size(); i++) {\n                CompletableFuture<T> future = errorFutures.get(i);\n                Function<? super Throwable, ? extends T> function = errors.get(i);\n                try {\n                    future.complete(function.apply(err));\n                } catch (Throwable t) {\n                    future.completeExceptionally(t);\n                }\n            }\n            errors.clear();\n            errorFutures.clear();\n            composers.clear();\n            composeFutures.clear();\n            consumers.clear();\n            consumeFutures.clear();\n            applies.clear();\n            applyFutures.clear();\n        });\n        return true;\n    }\n\n    @Override\n    public boolean isDone() {\n        return isDone;\n    }\n\n    @Override\n    public boolean isCancelled() {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public boolean cancel(boolean cancel) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<T> toCompletableFuture() {\n        return this;\n    }\n\n    @Override\n    @JsMethod\n    public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> handler) {\n        if (isDone()) {\n            if (isCompletedExceptionally()) {\n                try {\n                    return CompletableFuture.completedFuture(handler.apply(reason));\n                } catch (Throwable t) {\n                    CompletableFuture<T> fut = new CompletableFuture<>();\n                    fut.completeExceptionally(t);\n                    return fut;\n                }\n            } else {\n                // no exception occured just return the already completed future\n                return this;\n            }\n        } else {\n            CompletableFuture<T> fut = new CompletableFuture<>();\n            errorFutures.add(fut);\n            errors.add(handler);\n            return fut;\n        }\n    }\n\n    public static <U> java.util.concurrent.CompletableFuture<U> supplyAsync(Supplier<U> var0, Executor var1) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public T get(long t, TimeUnit unit) {\n        throw new IllegalStateException(\"Not possible to call synchronous get() in JS!\");\n    }\n\n    @Override\n    public T get() throws InterruptedException, ExecutionException {\n        throw new IllegalStateException(\"Not possible to call synchronous get() in JS!\");\n    }\n\n    public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> thenRun(Runnable action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> thenRunAsync(Runnable action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> thenRunAsync(Runnable action, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> runAfterBoth(CompletionStage<?> other, Runnable action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> runAfterEither(CompletionStage<?> other, Runnable action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n    public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) {     throw new IllegalStateException(\"Unimplemented!\");   }\n\n    public boolean isCompletedExceptionally() {\n        return isDone() && reason != null;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/CompletionStage.java",
    "content": "/*\n * Copyright 2016 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.util.concurrent;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.BiFunction;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n * Emulation of CompletionStage.\n *\n */\npublic interface CompletionStage<T> {\n\n  <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other,\n      Function<? super T, U> fn);\n\n  <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,\n      Function<? super T, U> fn);\n\n  <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,\n      Function<? super T, U> fn, Executor executor);\n\n  CompletionStage<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action);\n\n  CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,\n      Consumer<? super T> action);\n\n  CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,\n      Consumer<? super T> action, Executor executor);\n\n  CompletionStage<Void> thenAccept(Consumer<? super T> action);\n\n  CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);\n\n  CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor);\n\n  <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,\n      BiConsumer<? super T, ? super U> action);\n\n  <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,\n      BiConsumer<? super T, ? super U> action);\n\n  <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,\n      BiConsumer<? super T, ? super U> action, Executor executor);\n\n  <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);\n\n  <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn);\n\n  <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor);\n\n  <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,\n      BiFunction<? super T,? super U,? extends V> fn);\n\n  <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,\n      BiFunction<? super T,? super U,? extends V> fn);\n\n  <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,\n      BiFunction<? super T,? super U,? extends V> fn, Executor executor);\n\n  <U> CompletionStage<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);\n\n  <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn);\n\n  <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn,\n      Executor executor);\n\n  CompletionStage<Void> thenRun(Runnable action);\n\n  CompletionStage<Void> thenRunAsync(Runnable action);\n\n  CompletionStage<Void> thenRunAsync(Runnable action, Executor executor);\n\n  CompletionStage<Void> runAfterBoth(CompletionStage<?> other, Runnable action);\n\n  CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action);\n\n  CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor);\n\n  CompletionStage<Void> runAfterEither(CompletionStage<?> other, Runnable action);\n\n  CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action);\n\n  CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action,\n      Executor executor);\n\n  CompletionStage<T> whenComplete(BiConsumer<? super T, ? super Throwable> action);\n\n  CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action);\n\n  CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action,\n      Executor executor);\n\n  CompletionStage<T> exceptionally(Function<Throwable, ? extends T> fn);\n\n  <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);\n\n  <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);\n\n  <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,\n      Executor executor);\n\n  CompletableFuture<T> toCompletableFuture();\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/ExecutionException.java",
    "content": "package java.util.concurrent;\n\npublic class ExecutionException extends RuntimeException {\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/ExecutorService.java",
    "content": "package java.util.concurrent;\n\npublic interface ExecutorService {\n    void execute(Runnable var1);\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/ForkJoinPool.java",
    "content": "package java.util.concurrent;\n\nimport jsinterop.annotations.*;\n\npublic class ForkJoinPool implements ExecutorService {\n    private static final ForkJoinPool instance = new ForkJoinPool(new JSForkJoinPool());\n\n    private final JSForkJoinPool pool;\n\n    public ForkJoinPool(JSForkJoinPool pool) {\n        this.pool = pool;\n    }\n\n    public static ForkJoinPool commonPool() {\n        return instance;\n    }\n\n    public void execute(Runnable task) {\n        pool.execute(task);\n    }\n\n    @JsType(namespace = \"ForkJoinJS\", isNative = true)\n    private static class JSForkJoinPool {\n        public native void execute(Runnable task);\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/ScheduledFuture.java",
    "content": "package java.util.concurrent;\n\npublic interface ScheduledFuture<V> {\n\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/ScheduledThreadPoolExecutor.java",
    "content": "package java.util.concurrent;\n\nimport jsinterop.annotations.*;\nimport java.util.function.Supplier;\n\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.Callable;\nimport java.util.function.Supplier;\n\npublic class ScheduledThreadPoolExecutor {\n\n    private NativeJSScheduler scheduler = new NativeJSScheduler();\n\n    public ScheduledThreadPoolExecutor(int corePoolSize) {\n    }\n\n    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {\n        if (! unit.equals(TimeUnit.MILLISECONDS)) {\n            throw new Error(\"only milliseconds supported\");\n        }\n        if (delay < 0 || delay > Integer.MAX_VALUE) {\n            throw new Error(\"invalid delay\");\n        }\n        scheduler.callAfterDelay(new CallableWrapper(callable), (int)delay);\n        return null;\n    }\n    private static class CallableWrapper {\n        private final Callable callable;\n        public CallableWrapper(Callable callable) {\n            this.callable = callable;\n        }\n        @JsMethod\n        public void call() throws Exception {\n            this.callable.call();\n        }\n    }\n    @JsType(namespace = \"callback\", isNative = true)\n    private static class NativeJSScheduler {\n        public native void callAfterDelay(CallableWrapper func, int delayMs);\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/TimeUnit.java",
    "content": "/*\n * Copyright 2016 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.util.concurrent;\n\n/**\n * GWT emulation of TimeUnit, created by removing unsupported operations from\n * Doug Lea's public domain version.\n */\npublic enum TimeUnit {\n  NANOSECONDS {\n    public long toNanos(long d)   { return d; }\n    public long toMicros(long d)  { return d / C1_C0; }\n    public long toMillis(long d)  { return d / C2_C0; }\n    public long toSeconds(long d) { return d / C3_C0; }\n    public long toMinutes(long d) { return d / C4_C0; }\n    public long toHours(long d)   { return d / C5_C0; }\n    public long toDays(long d)    { return d / C6_C0; }\n    public long convert(long d, TimeUnit u) { return u.toNanos(d); }\n    int excessNanos(long d, long m) { return (int)(d - (m*C2)); }\n  },\n  MICROSECONDS {\n    public long toNanos(long d)   { return x(d, C1_C0, MAX_C1_C0); }\n    public long toMicros(long d)  { return d; }\n    public long toMillis(long d)  { return d / C2_C1; }\n    public long toSeconds(long d) { return d / C3_C1; }\n    public long toMinutes(long d) { return d / C4_C1; }\n    public long toHours(long d)   { return d / C5_C1; }\n    public long toDays(long d)    { return d / C6_C1; }\n    public long convert(long d, TimeUnit u) { return u.toMicros(d); }\n    int excessNanos(long d, long m) { return (int)((d*C1) - (m*C2)); }\n  },\n  MILLISECONDS {\n    public long toNanos(long d)   { return x(d, C2_C0, MAX_C2_C0); }\n    public long toMicros(long d)  { return x(d, C2_C1, MAX_C2_C1); }\n    public long toMillis(long d)  { return d; }\n    public long toSeconds(long d) { return d / C3_C2; }\n    public long toMinutes(long d) { return d / C4_C2; }\n    public long toHours(long d)   { return d / C5_C2; }\n    public long toDays(long d)    { return d / C6_C2; }\n    public long convert(long d, TimeUnit u) { return u.toMillis(d); }\n    int excessNanos(long d, long m) { return 0; }\n  },\n  SECONDS {\n    public long toNanos(long d)   { return x(d, C3_C0, MAX_C3_C0); }\n    public long toMicros(long d)  { return x(d, C3_C1, MAX_C3_C1); }\n    public long toMillis(long d)  { return x(d, C3_C2, MAX_C3_C2); }\n    public long toSeconds(long d) { return d; }\n    public long toMinutes(long d) { return d / C4_C3; }\n    public long toHours(long d)   { return d / C5_C3; }\n    public long toDays(long d)    { return d / C6_C3; }\n    public long convert(long d, TimeUnit u) { return u.toSeconds(d); }\n    int excessNanos(long d, long m) { return 0; }\n  },\n  MINUTES {\n    public long toNanos(long d)   { return x(d, C4_C0, MAX_C4_C0); }\n    public long toMicros(long d)  { return x(d, C4_C1, MAX_C4_C1); }\n    public long toMillis(long d)  { return x(d, C4_C2, MAX_C4_C2); }\n    public long toSeconds(long d) { return x(d, C4_C3, MAX_C4_C3); }\n    public long toMinutes(long d) { return d; }\n    public long toHours(long d)   { return d / C5_C4; }\n    public long toDays(long d)    { return d / C6_C4; }\n    public long convert(long d, TimeUnit u) { return u.toMinutes(d); }\n    int excessNanos(long d, long m) { return 0; }\n  },\n  HOURS {\n    public long toNanos(long d)   { return x(d, C5_C0, MAX_C5_C0); }\n    public long toMicros(long d)  { return x(d, C5_C1, MAX_C5_C1); }\n    public long toMillis(long d)  { return x(d, C5_C2, MAX_C5_C2); }\n    public long toSeconds(long d) { return x(d, C5_C3, MAX_C5_C3); }\n    public long toMinutes(long d) { return x(d, C5_C4, MAX_C5_C4); }\n    public long toHours(long d)   { return d; }\n    public long toDays(long d)    { return d / C6_C5; }\n    public long convert(long d, TimeUnit u) { return u.toHours(d); }\n    int excessNanos(long d, long m) { return 0; }\n  },\n  DAYS {\n    public long toNanos(long d)   { return x(d, C6_C0, MAX_C6_C0); }\n    public long toMicros(long d)  { return x(d, C6_C1, MAX_C6_C1); }\n    public long toMillis(long d)  { return x(d, C6_C2, MAX_C6_C2); }\n    public long toSeconds(long d) { return x(d, C6_C3, MAX_C6_C3); }\n    public long toMinutes(long d) { return x(d, C6_C4, MAX_C6_C4); }\n    public long toHours(long d)   { return x(d, C6_C5, MAX_C6_C5); }\n    public long toDays(long d)    { return d; }\n    public long convert(long d, TimeUnit u) { return u.toDays(d); }\n    int excessNanos(long d, long m) { return 0; }\n  };\n\n  // Handy constants for conversion methods\n  static final long C0 = 1L;\n  static final long C1 = C0 * 1000L;\n  static final long C2 = C1 * 1000L;\n  static final long C3 = C2 * 1000L;\n  static final long C4 = C3 * 60L;\n  static final long C5 = C4 * 60L;\n  static final long C6 = C5 * 24L;\n\n  static final long MAX = Long.MAX_VALUE;\n\n  static final long C6_C0 = C6 / C0;\n  static final long C6_C1 = C6 / C1;\n  static final long C6_C2 = C6 / C2;\n  static final long C6_C3 = C6 / C3;\n  static final long C6_C4 = C6 / C4;\n  static final long C6_C5 = C6 / C5;\n\n  static final long C5_C0 = C5 / C0;\n  static final long C5_C1 = C5 / C1;\n  static final long C5_C2 = C5 / C2;\n  static final long C5_C3 = C5 / C3;\n  static final long C5_C4 = C5 / C4;\n\n  static final long C4_C0 = C4 / C0;\n  static final long C4_C1 = C4 / C1;\n  static final long C4_C2 = C4 / C2;\n  static final long C4_C3 = C4 / C3;\n\n  static final long C3_C0 = C3 / C0;\n  static final long C3_C1 = C3 / C1;\n  static final long C3_C2 = C3 / C2;\n\n  static final long C2_C0 = C2 / C0;\n  static final long C2_C1 = C2 / C1;\n\n  static final long C1_C0 = C1 / C0;\n\n  static final long MAX_C6_C0 = MAX / C6_C0;\n  static final long MAX_C6_C1 = MAX / C6_C1;\n  static final long MAX_C6_C2 = MAX / C6_C2;\n  static final long MAX_C6_C3 = MAX / C6_C3;\n  static final long MAX_C6_C4 = MAX / C6_C4;\n  static final long MAX_C6_C5 = MAX / C6_C5;\n\n  static final long MAX_C5_C0 = MAX / C5_C0;\n  static final long MAX_C5_C1 = MAX / C5_C1;\n  static final long MAX_C5_C2 = MAX / C5_C2;\n  static final long MAX_C5_C3 = MAX / C5_C3;\n  static final long MAX_C5_C4 = MAX / C5_C4;\n\n  static final long MAX_C4_C0 = MAX / C4_C0;\n  static final long MAX_C4_C1 = MAX / C4_C1;\n  static final long MAX_C4_C2 = MAX / C4_C2;\n  static final long MAX_C4_C3 = MAX / C4_C3;\n\n  static final long MAX_C3_C0 = MAX / C3_C0;\n  static final long MAX_C3_C1 = MAX / C3_C1;\n  static final long MAX_C3_C2 = MAX / C3_C2;\n\n  static final long MAX_C2_C0 = MAX / C2_C0;\n  static final long MAX_C2_C1 = MAX / C2_C1;\n\n  static final long MAX_C1_C0 = MAX / C1_C0;\n\n  static long x(long d, long m, long over) {\n    if (d >  over) return Long.MAX_VALUE;\n    if (d < -over) return Long.MIN_VALUE;\n    return d * m;\n  }\n\n  // exceptions below changed from AbstractMethodError for GWT\n\n  public long convert(long sourceDuration, TimeUnit sourceUnit) {\n    throw new AssertionError();\n  }\n\n  public long toNanos(long duration) {\n    throw new AssertionError();\n  }\n\n  public long toMicros(long duration) {\n    throw new AssertionError();\n  }\n\n  public long toMillis(long duration) {\n    throw new AssertionError();\n  }\n\n  public long toSeconds(long duration) {\n    throw new AssertionError();\n  }\n\n  public long toMinutes(long duration) {\n    throw new AssertionError();\n  }\n\n  public long toHours(long duration) {\n    throw new AssertionError();\n  }\n\n  public long toDays(long duration) {\n    throw new AssertionError();\n  }\n\n  abstract int excessNanos(long d, long m);\n}\n\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/concurrent/TimeoutException.java",
    "content": "/*\n * Copyright 2016 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.util.concurrent;\n\n/**\n * Emulation of TimeoutException.\n *\n */\npublic class TimeoutException extends Exception {\n  public TimeoutException() {}\n\n  public TimeoutException(String message) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/function/Consumer.java",
    "content": "package java.util.function;\nimport jsinterop.annotations.JsType;\n\nimport java.util.Objects;\n\n@FunctionalInterface\n@JsType\npublic interface Consumer<T> {\n\n    void accept(T t);\n\n    default java.util.function.Consumer<T> andThen(java.util.function.Consumer<? super T> after) {\n        Objects.requireNonNull(after);\n        return (T t) -> { accept(t); after.accept(t); };\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/function/Function.java",
    "content": "/*\n * Copyright 2015 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\npackage java.util.function;\n\nimport jsinterop.annotations.*;\n\nimport static javaemul.internal.InternalPreconditions.checkCriticalNotNull;\n\n/**\n * See <a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html\">\n * the official Java API doc</a> for details.\n *\n * @param <T> type of the argument\n * @param <R> type of the return value\n */\n@FunctionalInterface\n@JsFunction\npublic interface Function<T, R> {\n\n    @JsOverlay\n    static <T> Function<T, T> identity() {\n        return t -> t;\n    }\n\n    R apply(T t);\n\n    @JsOverlay\n    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {\n        checkCriticalNotNull(after);\n        return t -> after.apply(apply(t));\n    }\n\n    @JsOverlay\n    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {\n        checkCriticalNotNull(before);\n        return t -> apply(before.apply(t));\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/Adler32.java",
    "content": " /* Adler32.java - Computes Adler32 data checksum of a data stream\n    Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc.\n \n This file is part of GNU Classpath.\n \n GNU Classpath is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2, or (at your option)\n any later version.\n \n GNU Classpath is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n General Public License for more details.\n \n You should have received a copy of the GNU General Public License\n along with GNU Classpath; see the file COPYING.  If not, write to the\n Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n 02110-1301 USA.\n \n Linking this library statically or dynamically with other modules is\n making a combined work based on this library.  Thus, the terms and\n conditions of the GNU General Public License cover the whole\n combination.\n \n As a special exception, the copyright holders of this library give you\n permission to link this library with independent modules to produce an\n executable, regardless of the license terms of these independent\n modules, and to copy and distribute the resulting executable under\n terms of your choice, provided that you also meet, for each linked\n independent module, the terms and conditions of the license of that\n module.  An independent module is a module which is not derived from\n or based on this library.  If you modify this library, you may extend\n this exception to your version of the library, but you are not\n obligated to do so.  If you do not wish to do so, delete this\n exception statement from your version. */\n \n package java.util.zip;\n \n /*\n  * Written using on-line Java Platform 1.2 API Specification, as well\n  * as \"The Java Class Libraries\", 2nd edition (Addison-Wesley, 1998).\n  * The actual Adler32 algorithm is taken from RFC 1950.\n  * Status:  Believed complete and correct.\n  */\n \n /**\n  * Computes Adler32 checksum for a stream of data. An Adler32 \n  * checksum is not as reliable as a CRC32 checksum, but a lot faster to \n  * compute.\n  *<p>\n  * The specification for Adler32 may be found in RFC 1950.\n  * (ZLIB Compressed Data Format Specification version 3.3)\n  *<p>\n  *<p>\n  * From that document:\n  *<p>\n  *      \"ADLER32 (Adler-32 checksum)\n  *       This contains a checksum value of the uncompressed data\n  *       (excluding any dictionary data) computed according to Adler-32\n  *       algorithm. This algorithm is a 32-bit extension and improvement\n  *       of the Fletcher algorithm, used in the ITU-T X.224 / ISO 8073\n  *       standard. \n  *<p>\n  *       Adler-32 is composed of two sums accumulated per byte: s1 is\n  *       the sum of all bytes, s2 is the sum of all s1 values. Both sums\n  *       are done modulo 65521. s1 is initialized to 1, s2 to zero.  The\n  *       Adler-32 checksum is stored as s2*65536 + s1 in most-\n  *       significant-byte first (network) order.\"\n  *<p>\n  * \"8.2. The Adler-32 algorithm\n  *<p>\n  *    The Adler-32 algorithm is much faster than the CRC32 algorithm yet\n  *    still provides an extremely low probability of undetected errors.\n  *<p>\n  *    The modulo on unsigned long accumulators can be delayed for 5552\n  *    bytes, so the modulo operation time is negligible.  If the bytes\n  *    are a, b, c, the second sum is 3a + 2b + c + 3, and so is position\n  *    and order sensitive, unlike the first sum, which is just a\n  *    checksum.  That 65521 is prime is important to avoid a possible\n  *    large class of two-byte errors that leave the check unchanged.\n  *    (The Fletcher checksum uses 255, which is not prime and which also\n  *    makes the Fletcher check insensitive to single byte changes 0 <->\n  *    255.)\n  *<p>\n  *    The sum s1 is initialized to 1 instead of zero to make the length\n  *    of the sequence part of s2, so that the length does not have to be\n  *   checked separately. (Any sequence of zeroes has a Fletcher\n  *    checksum of zero.)\"\n  *\n  * @author John Leuner, Per Bothner\n  * @since JDK 1.1\n  *\n  * @see InflaterInputStream\n  * @see DeflaterOutputStream\n  */\n public class Adler32 implements Checksum\n {\n \n     /** largest prime smaller than 65536 */\n     private static final int BASE = 65521;\n \n     private int checksum; //we do all in int.\n \n     //Note that java doesn't have unsigned integers,\n     //so we have to be careful with what arithmetic\n     //we do. We return the checksum as a long to\n     //avoid sign confusion.\n \n     /**\n      * Creates a new instance of the <code>Adler32</code> class.\n      * The checksum starts off with a value of 1.\n      */\n     public Adler32 ()\n     {\n         reset();\n     }\n \n     /**\n      * Resets the Adler32 checksum to the initial value.\n      */\n     public void reset ()\n     {\n         checksum = 1; //Initialize to 1\n     }\n \n     /**\n      * Updates the checksum with the byte b.\n      *\n      * @param bval the data value to add. The high byte of the int is ignored.\n      */\n     public void update (int bval)\n     {\n         //We could make a length 1 byte array and call update again, but I\n         //would rather not have that overhead\n         int s1 = checksum & 0xffff;\n         int s2 = checksum >>> 16;\n     \n         s1 = (s1 + (bval & 0xFF)) % BASE;\n         s2 = (s1 + s2) % BASE;\n     \n         checksum = (s2 << 16) + s1;\n     }\n \n     /**\n      * Updates the checksum with the bytes taken from the array.\n      *\n      * @param buffer an array of bytes\n      */\n     public void update (byte[] buffer)\n     {\n         update(buffer, 0, buffer.length);\n     }\n \n     /**\n      * Updates the checksum with the bytes taken from the array.\n      *\n      * @param buf an array of bytes\n      * @param off the start of the data used for this update\n      * @param len the number of bytes to use for this update\n      */\n     public void update (byte[] buf, int off, int len)\n     {\n         //(By Per Bothner)\n         int s1 = checksum & 0xffff;\n         int s2 = checksum >>> 16;\n \n         while (len > 0)\n         {\n             // We can defer the modulo operation:\n             // s1 maximally grows from 65521 to 65521 + 255 * 3800\n             // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31\n             int n = 3800;\n             if (n > len)\n                 n = len;\n             len -= n;\n             while (--n >= 0)\n             {\n                 s1 = s1 + (buf[off++] & 0xFF);\n                 s2 = s2 + s1;\n             }\n             s1 %= BASE;\n             s2 %= BASE;\n         }\n \n         /*Old implementation, borrowed from somewhere:\n     int n;\n     \n     while (len-- > 0) {\n \n       s1 = (s1 + (bs[offset++] & 0xff)) % BASE; \n       s2 = (s2 + s1) % BASE;\n     }*/\n     \n         checksum = (s2 << 16) | s1;\n     }\n \n     /**\n      * Returns the Adler32 data checksum computed so far.\n      */\n     public long getValue()\n     {\n         return (long) checksum & 0xffffffffL;\n     }\n }"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/CRC32.java",
    "content": " /* CRC32.java - Computes CRC32 data checksum of a data stream\n    Copyright (C) 1999. 2000, 2001 Free Software Foundation, Inc.\n \n This file is part of GNU Classpath.\n \n GNU Classpath is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2, or (at your option)\n any later version.\n \n GNU Classpath is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n General Public License for more details.\n \n You should have received a copy of the GNU General Public License\n along with GNU Classpath; see the file COPYING.  If not, write to the\n Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n 02110-1301 USA.\n \n Linking this library statically or dynamically with other modules is\n making a combined work based on this library.  Thus, the terms and\n conditions of the GNU General Public License cover the whole\n combination.\n \n As a special exception, the copyright holders of this library give you\n permission to link this library with independent modules to produce an\n executable, regardless of the license terms of these independent\n modules, and to copy and distribute the resulting executable under\n terms of your choice, provided that you also meet, for each linked\n independent module, the terms and conditions of the license of that\n module.  An independent module is a module which is not derived from\n or based on this library.  If you modify this library, you may extend\n this exception to your version of the library, but you are not\n obligated to do so.  If you do not wish to do so, delete this\n exception statement from your version. */\n \n package java.util.zip;\n \n /*\n  * Written using on-line Java Platform 1.2 API Specification, as well\n  * as \"The Java Class Libraries\", 2nd edition (Addison-Wesley, 1998).\n  * The actual CRC32 algorithm is taken from RFC 1952.\n  * Status:  Believed complete and correct.\n  */\n \n /**\n  * Computes CRC32 data checksum of a data stream.\n  * The actual CRC32 algorithm is described in RFC 1952\n  * (GZIP file format specification version 4.3).\n  * Can be used to get the CRC32 over a stream if used with checked input/output\n  * streams.\n  *\n  * @see InflaterInputStream\n  * @see DeflaterOutputStream\n  *\n  * @author Per Bothner\n  * @date April 1, 1999.\n  */\n public class CRC32 implements Checksum\n {\n     /** The crc data checksum so far. */\n     private int crc = 0;\n \n     /** The fast CRC table. Computed once when the CRC32 class is loaded. */\n     private static int[] crc_table = make_crc_table();\n \n     /** Make the table for a fast CRC. */\n     private static int[] make_crc_table ()\n     {\n         int[] crc_table = new int[256];\n         for (int n = 0; n < 256; n++)\n         {\n             int c = n;\n             for (int k = 8;  --k >= 0; )\n             {\n                 if ((c & 1) != 0)\n                     c = 0xedb88320 ^ (c >>> 1);\n                 else\n                     c = c >>> 1;\n             }\n             crc_table[n] = c;\n         }\n         return crc_table;\n     }\n \n     /**\n      * Returns the CRC32 data checksum computed so far.\n      */\n     public long getValue ()\n     {\n         return (long) crc & 0xffffffffL;\n     }\n \n     /**\n      * Resets the CRC32 data checksum as if no update was ever called.\n      */\n     public void reset () { crc = 0; }\n \n     /**\n      * Updates the checksum with the int bval.\n      *\n      * @param bval (the byte is taken as the lower 8 bits of bval)\n      */\n \n     public void update (int bval)\n     {\n         int c = ~crc;\n         c = crc_table[(c ^ bval) & 0xff] ^ (c >>> 8);\n         crc = ~c;\n     }\n \n     /**\n      * Adds the byte array to the data checksum.\n      *\n      * @param buf the buffer which contains the data\n      * @param off the offset in the buffer where the data starts\n      * @param len the length of the data\n      */\n     public void update (byte[] buf, int off, int len)\n     {\n         int c = ~crc;\n         while (--len >= 0)\n             c = crc_table[(c ^ buf[off++]) & 0xff] ^ (c >>> 8);\n         crc = ~c;\n     }\n \n     /**\n      * Adds the complete byte array to the data checksum.\n      */\n     public void update (byte[] buf) { update(buf, 0, buf.length); }\n }"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/Checksum.java",
    "content": " /* Checksum.java - Interface to compute a data checksum\n    Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc.\n \n This file is part of GNU Classpath.\n \n GNU Classpath is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2, or (at your option)\n any later version.\n \n GNU Classpath is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n General Public License for more details.\n \n You should have received a copy of the GNU General Public License\n along with GNU Classpath; see the file COPYING.  If not, write to the\n Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n 02110-1301 USA.\n \n Linking this library statically or dynamically with other modules is\n making a combined work based on this library.  Thus, the terms and\n conditions of the GNU General Public License cover the whole\n combination.\n \n As a special exception, the copyright holders of this library give you\n permission to link this library with independent modules to produce an\n executable, regardless of the license terms of these independent\n modules, and to copy and distribute the resulting executable under\n terms of your choice, provided that you also meet, for each linked\n independent module, the terms and conditions of the license of that\n module.  An independent module is a module which is not derived from\n or based on this library.  If you modify this library, you may extend\n this exception to your version of the library, but you are not\n obligated to do so.  If you do not wish to do so, delete this\n exception statement from your version. */\n \n package java.util.zip;\n \n /*\n  * Written using on-line Java Platform 1.2 API Specification, as well\n  * as \"The Java Class Libraries\", 2nd edition (Addison-Wesley, 1998).\n  * St  Believed complete and correct.\n  */\n \n /**\n  * Interface to compute a data checksum used by checked input/output streams.\n  * A data checksum can be updated by one byte or with a byte array. After each\n  * update the value of the current checksum can be returned by calling\n  * <code>getValue</code>. The complete checksum object can also be reset\n  * so it can be used again with new data.\n  *\n  * @see CheckedInputStream\n  * @see CheckedOutputStream\n  *\n  * @author Per Bothner\n  * @author Jochen Hoenicke\n  */\n public interface Checksum\n {\n     /**\n      * Returns the data checksum computed so far.\n      */\n     long getValue();\n \n     /**\n      * Resets the data checksum as if no update was ever called.\n      */\n     void reset();\n \n     /**\n      * Adds one byte to the data checksum.\n      *\n      * @param bval the data value to add. The high byte of the int is ignored.\n      */\n     void update (int bval);\n \n     /**\n      * Adds the byte array to the data checksum.\n      *\n      * @param buf the buffer which contains the data\n      * @param off the offset in the buffer where the data starts\n      * @param len the length of the data\n      */\n     void update (byte[] buf, int off, int len);\n }"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/Deflater.java",
    "content": " /* Deflater.java - Compress a data stream\n    Copyright (C) 1999, 2000, 2001, 2004, 2005 Free Software Foundation, Inc.\n\n This file is part of GNU Classpath.\n\n GNU Classpath is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2, or (at your option)\n any later version.\n\n GNU Classpath is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n General Public License for more details.\n\n You should have received a copy of the GNU General Public License\n along with GNU Classpath; see the file COPYING.  If not, write to the\n Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n 02110-1301 USA.\n\n Linking this library statically or dynamically with other modules is\n making a combined work based on this library.  Thus, the terms and\n conditions of the GNU General Public License cover the whole\n combination.\n\n As a special exception, the copyright holders of this library give you\n permission to link this library with independent modules to produce an\n executable, regardless of the license terms of these independent\n modules, and to copy and distribute the resulting executable under\n terms of your choice, provided that you also meet, for each linked\n independent module, the terms and conditions of the license of that\n module.  An independent module is a module which is not derived from\n or based on this library.  If you modify this library, you may extend\n this exception to your version of the library, but you are not\n obligated to do so.  If you do not wish to do so, delete this\n exception statement from your version. */\n\n package java.util.zip;\n\n /**\n  * This is the Deflater class.  The deflater class compresses input\n  * with the deflate algorithm described in RFC 1951.  It has several\n  * compression levels and three different strategies described below.\n  *\n  * This class is <i>not</i> thread safe.  This is inherent in the API, due\n  * to the split of deflate and setInput.\n  *\n  * @author Jochen Hoenicke\n  * @author Tom Tromey\n  */\n public class Deflater\n {\n     /**\n      * The best and slowest compression level.  This tries to find very\n      * long and distant string repetitions.\n      */\n     public static final int BEST_COMPRESSION = 9;\n     /**\n      * The worst but fastest compression level.\n      */\n     public static final int BEST_SPEED = 1;\n     /**\n      * The default compression level.\n      */\n     public static final int DEFAULT_COMPRESSION = -1;\n     /**\n      * This level won't compress at all but output uncompressed blocks.\n      */\n     public static final int NO_COMPRESSION = 0;\n\n     /**\n      * The default strategy.\n      */\n     public static final int DEFAULT_STRATEGY = 0;\n     /**\n      * This strategy will only allow longer string repetitions.  It is\n      * useful for random data with a small character set.\n      */\n     public static final int FILTERED = 1;\n\n     /**\n      * This strategy will not look for string repetitions at all.  It\n      * only encodes with Huffman trees (which means, that more common\n      * characters get a smaller encoding.\n      */\n     public static final int HUFFMAN_ONLY = 2;\n\n     /**\n      * The compression method.  This is the only method supported so far.\n      * There is no need to use this constant at all.\n      */\n     public static final int DEFLATED = 8;\n\n     /*\n    * The Deflater can do the following state transitions:\n    *\n    * (1) -> INIT_STATE   ----> INIT_FINISHING_STATE ---.\n    *        /  | (2)      (5)                         |\n    *       /   v          (5)                         |\n    *   (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3)\n    *       \\   | (3)                 |        ,-------'\n    *        |  |                     | (3)   /\n    *        v  v          (5)        v      v\n    * (1) -> BUSY_STATE   ----> FINISHING_STATE\n    *                                | (6)\n    *                                v\n    *                           FINISHED_STATE\n    *    \\_____________________________________/\n    *          | (7)\n    *          v\n    *        CLOSED_STATE\n    *\n    * (1) If we should produce a header we start in INIT_STATE, otherwise\n    *     we start in BUSY_STATE.\n    * (2) A dictionary may be set only when we are in INIT_STATE, then\n    *     we change the state as indicated.\n    * (3) Whether a dictionary is set or not, on the first call of deflate\n    *     we change to BUSY_STATE.\n    * (4) -- intentionally left blank -- :)\n    * (5) FINISHING_STATE is entered, when flush() is called to indicate that\n    *     there is no more INPUT.  There are also states indicating, that\n    *     the header wasn't written yet.\n    * (6) FINISHED_STATE is entered, when everything has been flushed to the\n    *     internal pending output buffer.\n    * (7) At any time (7)\n    *\n    */\n\n     private static final int IS_SETDICT              = 0x01;\n     private static final int IS_FLUSHING             = 0x04;\n     private static final int IS_FINISHING            = 0x08;\n\n     private static final int INIT_STATE              = 0x00;\n     private static final int SETDICT_STATE           = 0x01;\n     private static final int INIT_FINISHING_STATE    = 0x08;\n     private static final int SETDICT_FINISHING_STATE = 0x09;\n     private static final int BUSY_STATE              = 0x10;\n     private static final int FLUSHING_STATE          = 0x14;\n     private static final int FINISHING_STATE         = 0x1c;\n     private static final int FINISHED_STATE          = 0x1e;\n     private static final int CLOSED_STATE            = 0x7f;\n\n     /** Compression level. */\n     private int level;\n\n     /** should we include a header. */\n     private boolean noHeader;\n\n     /** The current state. */\n     private int state;\n\n     /** The total bytes of output written. */\n     private long totalOut;\n\n     /** The pending output. */\n     private DeflaterPending pending;\n\n     /** The deflater engine. */\n     private DeflaterEngine engine;\n\n     /**\n      * Creates a new deflater with default compression level.\n      */\n     public Deflater()\n     {\n         this(DEFAULT_COMPRESSION, false);\n     }\n\n     /**\n      * Creates a new deflater with given compression level.\n      * @param lvl the compression level, a value between NO_COMPRESSION\n      * and BEST_COMPRESSION, or DEFAULT_COMPRESSION.\n      * @exception IllegalArgumentException if lvl is out of range.\n      */\n     public Deflater(int lvl)\n     {\n         this(lvl, false);\n     }\n\n     /**\n      * Creates a new deflater with given compression level.\n      * @param lvl the compression level, a value between NO_COMPRESSION\n      * and BEST_COMPRESSION.\n      * @param nowrap true, iff we should suppress the deflate header at the\n      * beginning and the adler checksum at the end of the output.  This is\n      * useful for the GZIP format.\n      * @exception IllegalArgumentException if lvl is out of range.\n      */\n     public Deflater(int lvl, boolean nowrap)\n     {\n         if (lvl == DEFAULT_COMPRESSION)\n             lvl = 6;\n         else if (lvl < NO_COMPRESSION || lvl > BEST_COMPRESSION)\n             throw new IllegalArgumentException();\n\n         pending = new DeflaterPending();\n         engine = new DeflaterEngine(pending);\n         this.noHeader = nowrap;\n         setStrategy(DEFAULT_STRATEGY);\n         setLevel(lvl);\n         reset();\n     }\n\n     /**\n      * Resets the deflater.  The deflater acts afterwards as if it was\n      * just created with the same compression level and strategy as it\n      * had before.\n      */\n     public void reset()\n     {\n         state = (noHeader ? BUSY_STATE : INIT_STATE);\n         totalOut = 0;\n         pending.reset();\n         engine.reset();\n     }\n\n     /**\n      * Frees all objects allocated by the compressor.  There's no\n      * reason to call this, since you can just rely on garbage\n      * collection.  Exists only for compatibility against Sun's JDK,\n      * where the compressor allocates native memory.\n      * If you call any method (even reset) afterwards the behaviour is\n      * <i>undefined</i>.\n      */\n     public void end()\n     {\n         engine = null;\n         pending = null;\n         state = CLOSED_STATE;\n     }\n\n     /**\n      * Gets the current adler checksum of the data that was processed so\n      * far.\n      */\n     public int getAdler()\n     {\n         return engine.getAdler();\n     }\n\n     /**\n      * Gets the number of input bytes processed so far.\n      */\n     @Deprecated\n     public int getTotalIn()\n     {\n         return (int) engine.getTotalIn();\n     }\n\n     /**\n      * Gets the number of input bytes processed so far.\n      * @since 1.5\n      */\n     public long getBytesRead()\n     {\n         return engine.getTotalIn();\n     }\n\n     /**\n      * Gets the number of output bytes so far.\n      */\n     @Deprecated\n     public int getTotalOut()\n     {\n         return (int) totalOut;\n     }\n\n     /**\n      * Gets the number of output bytes so far.\n      * @since 1.5\n      */\n     public long getBytesWritten()\n     {\n         return totalOut;\n     }\n\n     /**\n      * Finalizes this object.\n      */\n     protected void finalize()\n     {\n         /* Exists solely for compatibility.  We don't have any native state. */\n     }\n\n     /**\n      * Flushes the current input block.  Further calls to deflate() will\n      * produce enough output to inflate everything in the current input\n      * block.  This is not part of Sun's JDK so I have made it package\n      * private.  It is used by DeflaterOutputStream to implement\n      * flush().\n      */\n     void flush() {\n         state |= IS_FLUSHING;\n     }\n\n     /**\n      * Finishes the deflater with the current input block.  It is an error\n      * to give more input after this method was called.  This method must\n      * be called to force all bytes to be flushed.\n      */\n     public void finish() {\n         state |= IS_FLUSHING | IS_FINISHING;\n     }\n\n     /**\n      * Returns true iff the stream was finished and no more output bytes\n      * are available.\n      */\n     public boolean finished()\n     {\n         return state == FINISHED_STATE && pending.isFlushed();\n     }\n\n     /**\n      * Returns true, if the input buffer is empty.\n      * You should then call setInput(). <br>\n      *\n      * <em>NOTE</em>: This method can also return true when the stream\n      * was finished.\n      */\n     public boolean needsInput()\n     {\n         return engine.needsInput();\n     }\n\n     /**\n      * Sets the data which should be compressed next.  This should be only\n      * called when needsInput indicates that more input is needed.\n      * If you call setInput when needsInput() returns false, the\n      * previous input that is still pending will be thrown away.\n      * The given byte array should not be changed, before needsInput() returns\n      * true again.\n      * This call is equivalent to <code>setInput(input, 0, input.length)</code>.\n      * @param input the buffer containing the input data.\n      * @exception IllegalStateException if the buffer was finished() or ended().\n      */\n     public void setInput(byte[] input)\n     {\n         setInput(input, 0, input.length);\n     }\n\n     /**\n      * Sets the data which should be compressed next.  This should be\n      * only called when needsInput indicates that more input is needed.\n      * The given byte array should not be changed, before needsInput() returns\n      * true again.\n      * @param input the buffer containing the input data.\n      * @param off the start of the data.\n      * @param len the length of the data.\n      * @exception IllegalStateException if the buffer was finished() or ended()\n      * or if previous input is still pending.\n      */\n     public void setInput(byte[] input, int off, int len)\n     {\n         if ((state & IS_FINISHING) != 0)\n             throw new IllegalStateException(\"finish()/end() already called\");\n         engine.setInput(input, off, len);\n     }\n\n     /**\n      * Sets the compression level.  There is no guarantee of the exact\n      * position of the change, but if you call this when needsInput is\n      * true the change of compression level will occur somewhere near\n      * before the end of the so far given input.\n      * @param lvl the new compression level.\n      */\n     public void setLevel(int lvl)\n     {\n         if (lvl == DEFAULT_COMPRESSION)\n             lvl = 6;\n         else if (lvl < NO_COMPRESSION || lvl > BEST_COMPRESSION)\n             throw new IllegalArgumentException();\n\n\n         if (level != lvl)\n         {\n             level = lvl;\n             engine.setLevel(lvl);\n         }\n     }\n\n     /**\n      * Sets the compression strategy. Strategy is one of\n      * DEFAULT_STRATEGY, HUFFMAN_ONLY and FILTERED.  For the exact\n      * position where the strategy is changed, the same as for\n      * setLevel() applies.\n      * @param stgy the new compression strategy.\n      */\n     public void setStrategy(int stgy)\n     {\n         if (stgy != DEFAULT_STRATEGY && stgy != FILTERED\n                 && stgy != HUFFMAN_ONLY)\n             throw new IllegalArgumentException();\n         engine.setStrategy(stgy);\n     }\n\n     /**\n      * Deflates the current input block to the given array.  It returns\n      * the number of bytes compressed, or 0 if either\n      * needsInput() or finished() returns true or length is zero.\n      * @param output the buffer where to write the compressed data.\n      */\n     public int deflate(byte[] output)\n     {\n         return deflate(output, 0, output.length);\n     }\n\n     /**\n      * Deflates the current input block to the given array.  It returns\n      * the number of bytes compressed, or 0 if either\n      * needsInput() or finished() returns true or length is zero.\n      * @param output the buffer where to write the compressed data.\n      * @param offset the offset into the output array.\n      * @param length the maximum number of bytes that may be written.\n      * @exception IllegalStateException if end() was called.\n      * @exception IndexOutOfBoundsException if offset and/or length\n      * don't match the array length.\n      */\n     public int deflate(byte[] output, int offset, int length)\n     {\n         int origLength = length;\n\n         if (state == CLOSED_STATE)\n             throw new IllegalStateException(\"Deflater closed\");\n\n         if (state < BUSY_STATE)\n         {\n             /* output header */\n             int header = (DEFLATED +\n                     ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8;\n             int level_flags = (level - 1) >> 1;\n             if (level_flags < 0 || level_flags > 3)\n                 level_flags = 3;\n             header |= level_flags << 6;\n             if ((state & IS_SETDICT) != 0)\n                 /* Dictionary was set */\n                 header |= DeflaterConstants.PRESET_DICT;\n             header += 31 - (header % 31);\n\n             pending.writeShortMSB(header);\n             if ((state & IS_SETDICT) != 0)\n             {\n                 int chksum = engine.getAdler();\n                 engine.resetAdler();\n                 pending.writeShortMSB(chksum >> 16);\n                 pending.writeShortMSB(chksum & 0xffff);\n             }\n\n             state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING));\n         }\n\n         for (;;)\n         {\n             int count = pending.flush(output, offset, length);\n             offset += count;\n             totalOut += count;\n             length -= count;\n             if (length == 0 || state == FINISHED_STATE)\n                 break;\n\n             if (!engine.deflate((state & IS_FLUSHING) != 0,\n                     (state & IS_FINISHING) != 0))\n             {\n                 if (state == BUSY_STATE)\n                     /* We need more input now */\n                     return origLength - length;\n                 else if (state == FLUSHING_STATE)\n                 {\n                     if (level != NO_COMPRESSION)\n                     {\n                         /* We have to supply some lookahead.  8 bit lookahead\n              * are needed by the zlib inflater, and we must fill\n              * the next byte, so that all bits are flushed.\n              */\n                         int neededbits = 8 + ((-pending.getBitCount()) & 7);\n                         while (neededbits > 0)\n                         {\n                             /* write a static tree block consisting solely of\n              * an EOF:\n              */\n                             pending.writeBits(2, 10);\n                             neededbits -= 10;\n                         }\n                     }\n                     state = BUSY_STATE;\n                 }\n                 else if (state == FINISHING_STATE)\n                 {\n                     pending.alignToByte();\n                     /* We have completed the stream */\n                     if (!noHeader)\n                     {\n                         int adler = engine.getAdler();\n                         pending.writeShortMSB(adler >> 16);\n                         pending.writeShortMSB(adler & 0xffff);\n                     }\n                     state = FINISHED_STATE;\n                 }\n             }\n         }\n\n         return origLength - length;\n     }\n\n     /**\n      * Sets the dictionary which should be used in the deflate process.\n      * This call is equivalent to <code>setDictionary(dict, 0,\n      * dict.length)</code>.\n      * @param dict the dictionary.\n      * @exception IllegalStateException if setInput () or deflate ()\n      * were already called or another dictionary was already set.\n      */\n     public void setDictionary(byte[] dict)\n     {\n         setDictionary(dict, 0, dict.length);\n     }\n\n     /**\n      * Sets the dictionary which should be used in the deflate process.\n      * The dictionary should be a byte array containing strings that are\n      * likely to occur in the data which should be compressed.  The\n      * dictionary is not stored in the compressed output, only a\n      * checksum.  To decompress the output you need to supply the same\n      * dictionary again.\n      * @param dict the dictionary.\n      * @param offset an offset into the dictionary.\n      * @param length the length of the dictionary.\n      * @exception IllegalStateException if setInput () or deflate () were\n      * already called or another dictionary was already set.\n      */\n     public void setDictionary(byte[] dict, int offset, int length)\n     {\n         if (state != INIT_STATE)\n             throw new IllegalStateException();\n\n         state = SETDICT_STATE;\n         engine.setDictionary(dict, offset, length);\n     }\n }"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/DeflaterConstants.java",
    "content": "   /* java.util.zip.DeflaterConstants\n      Copyright (C) 2001 Free Software Foundation, Inc.\n   \n   This file is part of GNU Classpath.\n   \n   GNU Classpath is free software; you can redistribute it and/or modify\n   it under the terms of the GNU General Public License as published by\n   the Free Software Foundation; either version 2, or (at your option)\n   any later version.\n   \n   GNU Classpath is distributed in the hope that it will be useful, but\n   WITHOUT ANY WARRANTY; without even the implied warranty of\n   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n   General Public License for more details.\n   \n   You should have received a copy of the GNU General Public License\n   along with GNU Classpath; see the file COPYING.  If not, write to the\n   Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n   02110-1301 USA.\n   \n   Linking this library statically or dynamically with other modules is\n   making a combined work based on this library.  Thus, the terms and\n   conditions of the GNU General Public License cover the whole\n   combination.\n   \n   As a special exception, the copyright holders of this library give you\n   permission to link this library with independent modules to produce an\n   executable, regardless of the license terms of these independent\n   modules, and to copy and distribute the resulting executable under\n   terms of your choice, provided that you also meet, for each linked\n   independent module, the terms and conditions of the license of that\n   module.  An independent module is a module which is not derived from\n   or based on this library.  If you modify this library, you may extend\n   this exception to your version of the library, but you are not\n   obligated to do so.  If you do not wish to do so, delete this\n   exception statement from your version. */\n\npackage java.util.zip;\n   \ninterface DeflaterConstants\n{\n    boolean DEBUGGING = false;\n   \n    int STORED_BLOCK = 0;\n    int STATIC_TREES = 1;\n    int DYN_TREES    = 2;\n    int PRESET_DICT  = 0x20;\n   \n    int DEFAULT_MEM_LEVEL = 8;\n   \n    int MAX_MATCH = 258;\n    int MIN_MATCH = 3;\n   \n    int MAX_WBITS = 15;\n    int WSIZE = 1 << MAX_WBITS;\n    int WMASK = WSIZE - 1;\n   \n    int HASH_BITS = DEFAULT_MEM_LEVEL + 7;\n    int HASH_SIZE = 1 << HASH_BITS;\n    int HASH_MASK = HASH_SIZE - 1;\n    int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH;\n   \n    int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1;\n    int MAX_DIST = WSIZE - MIN_LOOKAHEAD;\n   \n    int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8);\n    int MAX_BLOCK_SIZE = Math.min(65535, PENDING_BUF_SIZE-5);\n   \n    int DEFLATE_STORED = 0;\n    int DEFLATE_FAST   = 1;\n    int DEFLATE_SLOW   = 2;\n   \n    int GOOD_LENGTH[] = { 0,4, 4, 4, 4, 8,  8,  8,  32,  32 };\n    int MAX_LAZY[]    = { 0,4, 5, 6, 4,16, 16, 32, 128, 258 };\n    int NICE_LENGTH[] = { 0,8,16,32,16,32,128,128, 258, 258 };\n    int MAX_CHAIN[]   = { 0,4, 8,32,16,32,128,256,1024,4096 };\n    int COMPR_FUNC[]  = { 0,1, 1, 1, 1, 2,  2,  2,   2,   2 };\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/DeflaterEngine.java",
    "content": "   /* DeflaterEngine.java --\n      Copyright (C) 2001, 2004  Free Software Foundation, Inc.\n   \n   This file is part of GNU Classpath.\n   \n   GNU Classpath is free software; you can redistribute it and/or modify\n   it under the terms of the GNU General Public License as published by\n   the Free Software Foundation; either version 2, or (at your option)\n   any later version.\n   \n   GNU Classpath is distributed in the hope that it will be useful, but\n   WITHOUT ANY WARRANTY; without even the implied warranty of\n   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n   General Public License for more details.\n   \n   You should have received a copy of the GNU General Public License\n   along with GNU Classpath; see the file COPYING.  If not, write to the\n   Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n   02110-1301 USA.\n   \n   Linking this library statically or dynamically with other modules is\n   making a combined work based on this library.  Thus, the terms and\n   conditions of the GNU General Public License cover the whole\n   combination.\n   \n   As a special exception, the copyright holders of this library give you\n   permission to link this library with independent modules to produce an\n   executable, regardless of the license terms of these independent\n   modules, and to copy and distribute the resulting executable under\n   terms of your choice, provided that you also meet, for each linked\n   independent module, the terms and conditions of the license of that\n   module.  An independent module is a module which is not derived from\n   or based on this library.  If you modify this library, you may extend\n   this exception to your version of the library, but you are not\n   obligated to do so.  If you do not wish to do so, delete this\n   exception statement from your version. */\n\npackage java.util.zip;\n   \nclass DeflaterEngine implements DeflaterConstants\n{\n    private static final int TOO_FAR = 4096;\n   \n    private int ins_h;\n   \n    /**\n     * Hashtable, hashing three characters to an index for window, so\n     * that window[index]..window[index+2] have this hash code.\n     * Note that the array should really be unsigned short, so you need\n     * to and the values with 0xffff.\n     */\n    private short[] head;\n   \n    /**\n     * prev[index & WMASK] points to the previous index that has the\n     * same hash code as the string starting at index.  This way\n     * entries with the same hash code are in a linked list.\n     * Note that the array should really be unsigned short, so you need\n     * to and the values with 0xffff.\n     */\n    private short[] prev;\n   \n    private int matchStart, matchLen;\n    private boolean prevAvailable;\n    private int blockStart;\n   \n    /**\n     * strstart points to the current character in window.\n     */\n    private int strstart;\n   \n    /**\n     * lookahead is the number of characters starting at strstart in\n     * window that are valid.\n     * So window[strstart] until window[strstart+lookahead-1] are valid\n     * characters.\n     */\n    private int lookahead;\n   \n    /**\n     * This array contains the part of the uncompressed stream that\n     * is of relevance.  The current character is indexed by strstart.\n     */\n    private byte[] window;\n   \n    private int strategy, max_chain, max_lazy, niceLength, goodLength;\n   \n    /** The current compression function. */\n    private int comprFunc;\n   \n    /** The input data for compression. */\n    private byte[] inputBuf;\n   \n    /** The total bytes of input read. */\n    private int totalIn;\n   \n    /** The offset into inputBuf, where input data starts. */\n    private int inputOff;\n   \n    /** The end offset of the input data. */\n    private int inputEnd;\n   \n    private DeflaterPending pending;\n    private DeflaterHuffman huffman;\n   \n    /** The adler checksum */\n    private Adler32 adler;\n   \n    /* DEFLATE ALGORITHM:\n      *\n      * The uncompressed stream is inserted into the window array.  When\n      * the window array is full the first half is thrown away and the\n      * second half is copied to the beginning.\n      *\n      * The head array is a hash table.  Three characters build a hash value\n      * and they the value points to the corresponding index in window of \n      * the last string with this hash.  The prev array implements a\n      * linked list of matches with the same hash: prev[index & WMASK] points\n      * to the previous index with the same hash.\n      * \n      * \n      */\n   \n   \n    DeflaterEngine(DeflaterPending pending) {\n        this.pending = pending;\n        huffman = new DeflaterHuffman(pending);\n        adler = new Adler32();\n   \n        window = new byte[2*WSIZE];\n        head   = new short[HASH_SIZE];\n        prev   = new short[WSIZE];\n   \n        /* We start at index 1, to avoid a implementation deficiency, that\n        * we cannot build a repeat pattern at index 0.\n        */\n        blockStart = strstart = 1;\n    }\n   \n    public void reset()\n    {\n        huffman.reset();\n        adler.reset();\n        blockStart = strstart = 1;\n        lookahead = 0;\n        totalIn = 0;\n        prevAvailable = false;\n        matchLen = MIN_MATCH - 1;\n        for (int i = 0; i < HASH_SIZE; i++)\n            head[i] = 0;\n        for (int i = 0; i < WSIZE; i++)\n            prev[i] = 0;\n    }\n   \n    public final void resetAdler()\n    {\n        adler.reset();\n    }\n   \n    public final int getAdler()\n    {\n        int chksum = (int) adler.getValue();\n        return chksum;\n    }\n   \n    public final int getTotalIn()\n    {\n        return totalIn;\n    }\n   \n    public final void setStrategy(int strat)\n    {\n        strategy = strat;\n    }\n   \n    public void setLevel(int lvl)\n    {\n        goodLength = DeflaterConstants.GOOD_LENGTH[lvl];\n        max_lazy    = DeflaterConstants.MAX_LAZY[lvl];\n        niceLength = DeflaterConstants.NICE_LENGTH[lvl];\n        max_chain   = DeflaterConstants.MAX_CHAIN[lvl];\n   \n        if (DeflaterConstants.COMPR_FUNC[lvl] != comprFunc)\n        {\n            if (DeflaterConstants.DEBUGGING)\n                System.err.println(\"Change from \"+comprFunc +\" to \"\n                        + DeflaterConstants.COMPR_FUNC[lvl]);\n            switch (comprFunc)\n            {\n                case DEFLATE_STORED:\n                    if (strstart > blockStart)\n                    {\n                        huffman.flushStoredBlock(window, blockStart,\n                                strstart - blockStart, false);\n                        blockStart = strstart;\n                    }\n                    updateHash();\n                    break;\n                case DEFLATE_FAST:\n                    if (strstart > blockStart)\n                    {\n                        huffman.flushBlock(window, blockStart, strstart - blockStart,\n                                false);\n                        blockStart = strstart;\n                    }\n                    break;\n                case DEFLATE_SLOW:\n                    if (prevAvailable)\n                        huffman.tallyLit(window[strstart-1] & 0xff);\n                    if (strstart > blockStart)\n                    {\n                        huffman.flushBlock(window, blockStart, strstart - blockStart,\n                                false);\n                        blockStart = strstart;\n                    }\n                    prevAvailable = false;\n                    matchLen = MIN_MATCH - 1;\n                    break;\n            }\n            comprFunc = COMPR_FUNC[lvl];\n        }\n    }\n   \n    private void updateHash() {\n        if (DEBUGGING)\n            System.err.println(\"updateHash: \"+strstart);\n        ins_h = (window[strstart] << HASH_SHIFT) ^ window[strstart + 1];\n    }\n   \n    /**\n     * Inserts the current string in the head hash and returns the previous\n     * value for this hash.\n     */\n    private int insertString() {\n        short match;\n        int hash = ((ins_h << HASH_SHIFT) ^ window[strstart + (MIN_MATCH -1)])\n                & HASH_MASK;\n   \n        if (DEBUGGING)\n        {\n            if (hash != (((window[strstart] << (2*HASH_SHIFT))\n                    ^ (window[strstart + 1] << HASH_SHIFT)\n                    ^ (window[strstart + 2])) & HASH_MASK))\n                throw new InternalError(\"hash inconsistent: \"+hash+\"/\"\n                        +window[strstart]+\",\"\n                        +window[strstart+1]+\",\"\n                        +window[strstart+2]+\",\"+HASH_SHIFT);\n        }\n   \n        prev[strstart & WMASK] = match = head[hash];\n        head[hash] = (short) strstart;\n        ins_h = hash;\n        return match & 0xffff;\n    }\n   \n    private void slideWindow()\n    {\n        System.arraycopy(window, WSIZE, window, 0, WSIZE);\n        matchStart -= WSIZE;\n        strstart -= WSIZE;\n        blockStart -= WSIZE;\n       \n        /* Slide the hash table (could be avoided with 32 bit values\n        * at the expense of memory usage).\n        */\n        for (int i = 0; i < HASH_SIZE; i++)\n        {\n            int m = head[i] & 0xffff;\n            head[i] = m >= WSIZE ? (short) (m - WSIZE) : 0;\n        }\n   \n        /* Slide the prev table.\n        */\n        for (int i = 0; i < WSIZE; i++)\n        {\n            int m = prev[i] & 0xffff;\n            prev[i] = m >= WSIZE ? (short) (m - WSIZE) : 0;\n        }\n    }\n   \n    /**\n     * Fill the window when the lookahead becomes insufficient.\n     * Updates strstart and lookahead.\n     *\n     * OUT assertions: strstart + lookahead <= 2*WSIZE\n     *    lookahead >= MIN_LOOKAHEAD or inputOff == inputEnd\n     */\n    private void fillWindow()\n    {\n        /* If the window is almost full and there is insufficient lookahead,\n        * move the upper half to the lower one to make room in the upper half.\n        */\n        if (strstart >= WSIZE + MAX_DIST)\n            slideWindow();\n   \n        /* If there is not enough lookahead, but still some input left,\n        * read in the input\n        */\n        while (lookahead < DeflaterConstants.MIN_LOOKAHEAD && inputOff < inputEnd)\n        {\n            int more = 2*WSIZE - lookahead - strstart;\n   \t\n            if (more > inputEnd - inputOff)\n                more = inputEnd - inputOff;\n   \n            System.arraycopy(inputBuf, inputOff,\n                    window, strstart + lookahead, more);\n            adler.update(inputBuf, inputOff, more);\n            inputOff += more;\n            totalIn  += more;\n            lookahead += more;\n        }\n   \n        if (lookahead >= MIN_MATCH)\n            updateHash();\n    }\n   \n    /**\n     * Find the best (longest) string in the window matching the\n     * string starting at strstart.\n     *\n     * Preconditions:\n     *    strstart + MAX_MATCH <= window.length.\n     *\n     *\n     * @param curMatch\n     */\n    private boolean findLongestMatch(int curMatch) {\n        int chainLength = this.max_chain;\n        int niceLength = this.niceLength;\n        short[] prev = this.prev;\n        int scan  = this.strstart;\n        int match;\n        int best_end = this.strstart + matchLen;\n        int best_len = Math.max(matchLen, MIN_MATCH - 1);\n       \n        int limit = Math.max(strstart - MAX_DIST, 0);\n   \n        int strend = scan + MAX_MATCH - 1;\n        byte scan_end1 = window[best_end - 1];\n        byte scan_end  = window[best_end];\n   \n        /* Do not waste too much time if we already have a good match: */\n        if (best_len >= this.goodLength)\n            chainLength >>= 2;\n   \n        /* Do not look for matches beyond the end of the input. This is necessary\n        * to make deflate deterministic.\n        */\n        if (niceLength > lookahead)\n            niceLength = lookahead;\n   \n        if (DeflaterConstants.DEBUGGING\n                && strstart > 2*WSIZE - MIN_LOOKAHEAD)\n            throw new InternalError(\"need lookahead\");\n       \n        do {\n            if (DeflaterConstants.DEBUGGING && curMatch >= strstart)\n                throw new InternalError(\"future match\");\n            if (window[curMatch + best_len] != scan_end\n                    || window[curMatch + best_len - 1] != scan_end1\n                    || window[curMatch] != window[scan]\n                    || window[curMatch+1] != window[scan + 1])\n                continue;\n   \n            match = curMatch + 2;\n            scan += 2;\n   \n            /* We check for insufficient lookahead only every 8th comparison;\n          * the 256th check will be made at strstart+258.\n          */\n            while (window[++scan] == window[++match]\n                    && window[++scan] == window[++match]\n                    && window[++scan] == window[++match]\n                    && window[++scan] == window[++match]\n                    && window[++scan] == window[++match]\n                    && window[++scan] == window[++match]\n                    && window[++scan] == window[++match]\n                    && window[++scan] == window[++match]\n                    && scan < strend);\n   \n            if (scan > best_end) {\n                //  \tif (DeflaterConstants.DEBUGGING && ins_h == 0)\n                //  \t  System.err.println(\"Found match: \"+curMatch+\"-\"+(scan-strstart));\n                matchStart = curMatch;\n                best_end = scan;\n                best_len = scan - strstart;\n                if (best_len >= niceLength)\n                    break;\n   \n                scan_end1  = window[best_end-1];\n                scan_end   = window[best_end];\n            }\n            scan = strstart;\n        } while ((curMatch = (prev[curMatch & WMASK] & 0xffff)) > limit\n                && --chainLength != 0);\n   \n        matchLen = Math.min(best_len, lookahead);\n        return matchLen >= MIN_MATCH;\n    }\n   \n    void setDictionary(byte[] buffer, int offset, int length) {\n        if (DeflaterConstants.DEBUGGING && strstart != 1)\n            throw new IllegalStateException(\"strstart not 1\");\n        adler.update(buffer, offset, length);\n        if (length < MIN_MATCH)\n            return;\n        if (length > MAX_DIST) {\n            offset += length - MAX_DIST;\n            length = MAX_DIST;\n        }\n   \n        System.arraycopy(buffer, offset, window, strstart, length);\n   \n        updateHash();\n        length--;\n        while (--length > 0)\n        {\n            insertString();\n            strstart++;\n        }\n        strstart += 2;\n        blockStart = strstart;\n    }\n       \n    private boolean deflateStored(boolean flush, boolean finish)\n    {\n        if (!flush && lookahead == 0)\n            return false;\n   \n        strstart += lookahead;\n        lookahead = 0;\n   \n        int storedLen = strstart - blockStart;\n   \n        if ((storedLen >= DeflaterConstants.MAX_BLOCK_SIZE)\n                /* Block is full */\n                || (blockStart < WSIZE && storedLen >= MAX_DIST)\n                /* Block may move out of window */\n                || flush)\n        {\n            boolean lastBlock = finish;\n            if (storedLen > DeflaterConstants.MAX_BLOCK_SIZE)\n            {\n                storedLen = DeflaterConstants.MAX_BLOCK_SIZE;\n                lastBlock = false;\n            }\n   \n            if (DeflaterConstants.DEBUGGING)\n                System.err.println(\"storedBlock[\"+storedLen+\",\"+lastBlock+\"]\");\n   \n            huffman.flushStoredBlock(window, blockStart, storedLen, lastBlock);\n            blockStart += storedLen;\n            return !lastBlock;\n        }\n        return true;\n    }\n   \n    private boolean deflateFast(boolean flush, boolean finish)\n    {\n        if (lookahead < MIN_LOOKAHEAD && !flush)\n            return false;\n   \n        while (lookahead >= MIN_LOOKAHEAD || flush)\n        {\n            if (lookahead == 0)\n            {\n                /* We are flushing everything */\n                huffman.flushBlock(window, blockStart, strstart - blockStart,\n                        finish);\n                blockStart = strstart;\n                return false;\n            }\n   \n            if (strstart > 2 * WSIZE - MIN_LOOKAHEAD)\n            {\n                /* slide window, as findLongestMatch need this.\n   \t     * This should only happen when flushing and the window\n   \t     * is almost full.\n   \t     */\n                slideWindow();\n            }\n   \n            int hashHead;\n            if (lookahead >= MIN_MATCH\n                    && (hashHead = insertString()) != 0\n                    && strategy != Deflater.HUFFMAN_ONLY\n                    && strstart - hashHead <= MAX_DIST\n                    && findLongestMatch(hashHead))\n            {\n                /* longestMatch sets matchStart and matchLen */\n                if (DeflaterConstants.DEBUGGING)\n                {\n                    for (int i = 0 ; i < matchLen; i++)\n                    {\n                        if (window[strstart+i] != window[matchStart + i])\n                            throw new InternalError();\n                    }\n                }\n                boolean full = huffman.tallyDist(strstart - matchStart, matchLen);\n   \t    \n                lookahead -= matchLen;\n                if (matchLen <= max_lazy && lookahead >= MIN_MATCH)\n                {\n                    while (--matchLen > 0)\n                    {\n                        strstart++;\n                        insertString();\n                    }\n                    strstart++;\n                }\n                else\n                {\n                    strstart += matchLen;\n                    if (lookahead >= MIN_MATCH - 1)\n                        updateHash();\n                }\n                matchLen = MIN_MATCH - 1;\n                if (!full)\n                    continue;\n            }\n            else\n            {\n                /* No match found */\n                huffman.tallyLit(window[strstart] & 0xff);\n                strstart++;\n                lookahead--;\n            }\n   \n            if (huffman.isFull())\n            {\n                boolean lastBlock = finish && lookahead == 0;\n                huffman.flushBlock(window, blockStart, strstart - blockStart,\n                        lastBlock);\n                blockStart = strstart;\n                return !lastBlock;\n            }\n        }\n        return true;\n    }\n   \n    private boolean deflateSlow(boolean flush, boolean finish)\n    {\n        if (lookahead < MIN_LOOKAHEAD && !flush)\n            return false;\n   \n        while (lookahead >= MIN_LOOKAHEAD || flush)\n        {\n            if (lookahead == 0)\n            {\n                if (prevAvailable)\n                    huffman.tallyLit(window[strstart-1] & 0xff);\n                prevAvailable = false;\n   \n                /* We are flushing everything */\n                if (DeflaterConstants.DEBUGGING && !flush)\n                    throw new InternalError(\"Not flushing, but no lookahead\");\n                huffman.flushBlock(window, blockStart, strstart - blockStart,\n                        finish);\n                blockStart = strstart;\n                return false;\n            }\n   \n            if (strstart >= 2 * WSIZE - MIN_LOOKAHEAD)\n            {\n                /* slide window, as findLongestMatch need this.\n   \t     * This should only happen when flushing and the window\n   \t     * is almost full.\n   \t     */\n                slideWindow();\n            }\n   \n            int prevMatch = matchStart;\n            int prevLen = matchLen;\n            if (lookahead >= MIN_MATCH)\n            {\n                int hashHead = insertString();\n                if (strategy != Deflater.HUFFMAN_ONLY\n                        && hashHead != 0 && strstart - hashHead <= MAX_DIST\n                        && findLongestMatch(hashHead))\n                {\n                    /* longestMatch sets matchStart and matchLen */\n   \t\t\n                    /* Discard match if too small and too far away */\n                    if (matchLen <= 5\n                            && (strategy == Deflater.FILTERED\n                            || (matchLen == MIN_MATCH\n                            && strstart - matchStart > TOO_FAR))) {\n                        matchLen = MIN_MATCH - 1;\n                    }\n                }\n            }\n   \t\n            /* previous match was better */\n            if (prevLen >= MIN_MATCH && matchLen <= prevLen)\n            {\n                if (DeflaterConstants.DEBUGGING)\n                {\n                    for (int i = 0 ; i < matchLen; i++)\n                    {\n                        if (window[strstart-1+i] != window[prevMatch + i])\n                            throw new InternalError();\n                    }\n                }\n                huffman.tallyDist(strstart - 1 - prevMatch, prevLen);\n                prevLen -= 2;\n                do\n                {\n                    strstart++;\n                    lookahead--;\n                    if (lookahead >= MIN_MATCH)\n                        insertString();\n                }\n                while (--prevLen > 0);\n                strstart ++;\n                lookahead--;\n                prevAvailable = false;\n                matchLen = MIN_MATCH - 1;\n            }\n            else\n            {\n                if (prevAvailable)\n                    huffman.tallyLit(window[strstart-1] & 0xff);\n                prevAvailable = true;\n                strstart++;\n                lookahead--;\n            }\n   \n            if (huffman.isFull())\n            {\n                int len = strstart - blockStart;\n                if (prevAvailable)\n                    len--;\n                boolean lastBlock = (finish && lookahead == 0 && !prevAvailable);\n                huffman.flushBlock(window, blockStart, len, lastBlock);\n                blockStart += len;\n                return !lastBlock;\n            }\n        }\n        return true;\n    }\n   \n    public boolean deflate(boolean flush, boolean finish)\n    {\n        boolean progress;\n        do\n        {\n            fillWindow();\n            boolean canFlush = flush && inputOff == inputEnd;\n            if (DeflaterConstants.DEBUGGING)\n                System.err.println(\"window: [\"+blockStart+\",\"+strstart+\",\"\n                        +lookahead+\"], \"+comprFunc+\",\"+canFlush);\n            switch (comprFunc)\n            {\n                case DEFLATE_STORED:\n                    progress = deflateStored(canFlush, finish);\n                    break;\n                case DEFLATE_FAST:\n                    progress = deflateFast(canFlush, finish);\n                    break;\n                case DEFLATE_SLOW:\n                    progress = deflateSlow(canFlush, finish);\n                    break;\n                default:\n                    throw new InternalError();\n            }\n        }\n        while (pending.isFlushed()  /* repeat while we have no pending output */\n                && progress);        /* and progress was made */\n   \n        return progress;\n    }\n   \n    public void setInput(byte[] buf, int off, int len)\n    {\n        if (inputOff < inputEnd)\n            throw new IllegalStateException\n                    (\"Old input was not completely processed\");\n   \n        int end = off + len;\n   \n        /* We want to throw an ArrayIndexOutOfBoundsException early.  The\n        * check is very tricky: it also handles integer wrap around.  \n        */\n        if (0 > off || off > end || end > buf.length)\n            throw new ArrayIndexOutOfBoundsException();\n   \n        inputBuf = buf;\n        inputOff = off;\n        inputEnd = end;\n    }\n   \n    public final boolean needsInput()\n    {\n        return inputEnd == inputOff;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/DeflaterHuffman.java",
    "content": "/* DeflaterHuffman.java --\n      Copyright (C) 2001, 2004, 2005  Free Software Foundation, Inc.\n   \n   This file is part of GNU Classpath.\n   \n   GNU Classpath is free software; you can redistribute it and/or modify\n   it under the terms of the GNU General Public License as published by\n   the Free Software Foundation; either version 2, or (at your option)\n   any later version.\n   \n   GNU Classpath is distributed in the hope that it will be useful, but\n   WITHOUT ANY WARRANTY; without even the implied warranty of\n   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n   General Public License for more details.\n   \n   You should have received a copy of the GNU General Public License\n   along with GNU Classpath; see the file COPYING.  If not, write to the\n   Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n   02110-1301 USA.\n   \n   Linking this library statically or dynamically with other modules is\n   making a combined work based on this library.  Thus, the terms and\n   conditions of the GNU General Public License cover the whole\n   combination.\n   \n   As a special exception, the copyright holders of this library give you\n   permission to link this library with independent modules to produce an\n   executable, regardless of the license terms of these independent\n   modules, and to copy and distribute the resulting executable under\n   terms of your choice, provided that you also meet, for each linked\n   independent module, the terms and conditions of the license of that\n   module.  An independent module is a module which is not derived from\n   or based on this library.  If you modify this library, you may extend\n   this exception to your version of the library, but you are not\n   obligated to do so.  If you do not wish to do so, delete this\n   exception statement from your version. */\n   \npackage java.util.zip;\n   \n/**\n * This is the DeflaterHuffman class.\n *\n * This class is <i>not</i> thread safe.  This is inherent in the API, due\n * to the split of deflate and setInput.\n *\n * @author Jochen Hoenicke\n * @date Jan 6, 2000\n */\nclass DeflaterHuffman\n{\n    private static final int BUFSIZE = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6);\n    private static final int LITERAL_NUM = 286;\n    private static final int DIST_NUM = 30;\n    private static final int BITLEN_NUM = 19;\n    private static final int REP_3_6    = 16;\n    private static final int REP_3_10   = 17;\n    private static final int REP_11_138 = 18;\n    private static final int EOF_SYMBOL = 256;\n    private static final int[] BL_ORDER =\n            { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };\n   \n    private static final String bit4Reverse =\n            \"\\000\\010\\004\\014\\002\\012\\006\\016\\001\\011\\005\\015\\003\\013\\007\\017\";\n   \n    class Tree {\n        short[] freqs;\n        short[] codes;\n        byte[]  length;\n        int[]   bl_counts;\n        int     minNumCodes, numCodes;\n        int     maxLength;\n   \n        Tree(int elems, int minCodes, int maxLength) {\n            this.minNumCodes = minCodes;\n            this.maxLength  = maxLength;\n            freqs  = new short[elems];\n            bl_counts = new int[maxLength];\n        }\n   \n        void reset() {\n            for (int i = 0; i < freqs.length; i++)\n                freqs[i] = 0;\n            codes = null;\n            length = null;\n        }\n   \n        final void writeSymbol(int code)\n        {\n            if (DeflaterConstants.DEBUGGING)\n            {\n                freqs[code]--;\n                //  \t  System.err.print(\"writeSymbol(\"+freqs.length+\",\"+code+\"): \");\n            }\n            pending.writeBits(codes[code] & 0xffff, length[code]);\n        }\n   \n        final void checkEmpty()\n        {\n            boolean empty = true;\n            for (int i = 0; i < freqs.length; i++)\n                if (freqs[i] != 0)\n                {\n                    System.err.println(\"freqs[\"+i+\"] == \"+freqs[i]);\n                    empty = false;\n                }\n            if (!empty)\n                throw new InternalError();\n            System.err.println(\"checkEmpty suceeded!\");\n        }\n   \n        void setStaticCodes(short[] stCodes, byte[] stLength)\n        {\n            codes = stCodes;\n            length = stLength;\n        }\n   \n        public void buildCodes() {\n            int[] nextCode = new int[maxLength];\n            int code = 0;\n            codes = new short[freqs.length];\n   \n            if (DeflaterConstants.DEBUGGING)\n                System.err.println(\"buildCodes: \"+freqs.length);\n            for (int bits = 0; bits < maxLength; bits++)\n            {\n                nextCode[bits] = code;\n                code += bl_counts[bits] << (15 - bits);\n                if (DeflaterConstants.DEBUGGING)\n                    System.err.println(\"bits: \"+(bits+1)+\" count: \"+bl_counts[bits]\n                            +\" nextCode: \"+Integer.toHexString(code));\n            }\n            if (DeflaterConstants.DEBUGGING && code != 65536)\n                throw new RuntimeException(\"Inconsistent bl_counts!\");\n         \n            for (int i=0; i < numCodes; i++)\n            {\n                int bits = length[i];\n                if (bits > 0)\n                {\n                    if (DeflaterConstants.DEBUGGING)\n                        System.err.println(\"codes[\"+i+\"] = rev(\"\n                                +Integer.toHexString(nextCode[bits-1])+\"),\"\n                                +bits);\n                    codes[i] = bitReverse(nextCode[bits-1]);\n                    nextCode[bits-1] += 1 << (16 - bits);\n                }\n            }\n        }\n   \n        private void buildLength(int childs[])\n        {\n            this.length = new byte [freqs.length];\n            int numNodes = childs.length / 2;\n            int numLeafs = (numNodes + 1) / 2;\n            int overflow = 0;\n         \n            for (int i = 0; i < maxLength; i++)\n                bl_counts[i] = 0;\n   \n            /* First calculate optimal bit lengths */\n            int lengths[] = new int[numNodes];\n            lengths[numNodes-1] = 0;\n            for (int i = numNodes - 1; i >= 0; i--)\n            {\n                if (childs[2*i+1] != -1)\n                {\n                    int bitLength = lengths[i] + 1;\n                    if (bitLength > maxLength)\n                    {\n                        bitLength = maxLength;\n                        overflow++;\n                    }\n                    lengths[childs[2*i]] = lengths[childs[2*i+1]] = bitLength;\n                }\n                else\n                {\n                    /* A leaf node */\n                    int bitLength = lengths[i];\n                    bl_counts[bitLength - 1]++;\n                    this.length[childs[2*i]] = (byte) lengths[i];\n                }\n            }\n         \n            if (DeflaterConstants.DEBUGGING)\n            {\n                System.err.println(\"Tree \"+freqs.length+\" lengths:\");\n                for (int i=0; i < numLeafs; i++)\n                    System.err.println(\"Node \"+childs[2*i]+\" freq: \"+freqs[childs[2*i]]\n                            + \" len: \"+length[childs[2*i]]);\n            }\n         \n            if (overflow == 0)\n                return;\n         \n            int incrBitLen = maxLength - 1;\n            do\n            {\n                /* Find the first bit length which could increase: */\n                while (bl_counts[--incrBitLen] == 0)\n                    ;\n   \t  \n                /* Move this node one down and remove a corresponding\n   \t   * amount of overflow nodes.\n   \t   */\n                do\n                {\n                    bl_counts[incrBitLen]--;\n                    bl_counts[++incrBitLen]++;\n                    overflow -= 1 << (maxLength - 1 - incrBitLen);\n                }\n                while (overflow > 0 && incrBitLen < maxLength - 1);\n            }\n            while (overflow > 0);\n   \n            /* We may have overshot above.  Move some nodes from maxLength to\n          * maxLength-1 in that case.\n          */\n            bl_counts[maxLength-1] += overflow;\n            bl_counts[maxLength-2] -= overflow;\n         \n            /* Now recompute all bit lengths, scanning in increasing\n          * frequency.  It is simpler to reconstruct all lengths instead of\n          * fixing only the wrong ones. This idea is taken from 'ar'\n          * written by Haruhiko Okumura.\n          *\n          * The nodes were inserted with decreasing frequency into the childs\n          * array.\n          */\n            int nodePtr = 2 * numLeafs;\n            for (int bits = maxLength; bits != 0; bits--)\n            {\n                int n = bl_counts[bits-1];\n                while (n > 0)\n                {\n                    int childPtr = 2*childs[nodePtr++];\n                    if (childs[childPtr + 1] == -1)\n                    {\n                        /* We found another leaf */\n                        length[childs[childPtr]] = (byte) bits;\n                        n--;\n                    }\n                }\n            }\n            if (DeflaterConstants.DEBUGGING)\n            {\n                System.err.println(\"*** After overflow elimination. ***\");\n                for (int i=0; i < numLeafs; i++)\n                    System.err.println(\"Node \"+childs[2*i]+\" freq: \"+freqs[childs[2*i]]\n                            + \" len: \"+length[childs[2*i]]);\n            }\n        }\n       \n        void buildTree()\n        {\n            int numSymbols = freqs.length;\n   \n            /* heap is a priority queue, sorted by frequency, least frequent\n          * nodes first.  The heap is a binary tree, with the property, that\n          * the parent node is smaller than both child nodes.  This assures\n          * that the smallest node is the first parent.\n          *\n          * The binary tree is encoded in an array:  0 is root node and\n          * the nodes 2*n+1, 2*n+2 are the child nodes of node n.\n          */\n            int[] heap = new int[numSymbols];\n            int heapLen = 0;\n            int maxCode = 0;\n            for (int n = 0; n < numSymbols; n++)\n            {\n                int freq = freqs[n];\n                if (freq != 0)\n                {\n                    /* Insert n into heap */\n                    int pos = heapLen++;\n                    int ppos;\n                    while (pos > 0 &&\n                            freqs[heap[ppos = (pos - 1) / 2]] > freq) {\n                        heap[pos] = heap[ppos];\n                        pos = ppos;\n                    }\n                    heap[pos] = n;\n                    maxCode = n;\n                }\n            }\n         \n            /* We could encode a single literal with 0 bits but then we\n          * don't see the literals.  Therefore we force at least two\n          * literals to avoid this case.  We don't care about order in\n          * this case, both literals get a 1 bit code.  \n          */\n            while (heapLen < 2)\n            {\n                int node = maxCode < 2 ? ++maxCode : 0;\n                heap[heapLen++] = node;\n            }\n   \n            numCodes = Math.max(maxCode + 1, minNumCodes);\n         \n            int numLeafs = heapLen;\n            int[] childs = new int[4*heapLen - 2];\n            int[] values = new int[2*heapLen - 1];\n            int numNodes = numLeafs;\n            for (int i = 0; i < heapLen; i++)\n            {\n                int node = heap[i];\n                childs[2*i]   = node;\n                childs[2*i+1] = -1;\n                values[i] = freqs[node] << 8;\n                heap[i] = i;\n            }\n         \n            /* Construct the Huffman tree by repeatedly combining the least two\n          * frequent nodes.\n          */\n            do\n            {\n                int first = heap[0];\n                int last  = heap[--heapLen];\n   \t  \n                /* Propagate the hole to the leafs of the heap */\n                int ppos = 0;\n                int path = 1;\n                while (path < heapLen)\n                {\n                    if (path + 1 < heapLen\n                            && values[heap[path]] > values[heap[path+1]])\n                        path++;\n   \t      \n                    heap[ppos] = heap[path];\n                    ppos = path;\n                    path = path * 2 + 1;\n                }\n   \t  \n                /* Now propagate the last element down along path.  Normally\n   \t   * it shouldn't go too deep.\n   \t   */\n                int lastVal = values[last];\n                while ((path = ppos) > 0\n                        && values[heap[ppos = (path - 1)/2]] > lastVal)\n                    heap[path] = heap[ppos];\n                heap[path] = last;\n   \t  \n   \t  \n                int second = heap[0];\n   \t  \n                /* Create a new node father of first and second */\n                last = numNodes++;\n                childs[2*last] = first;\n                childs[2*last+1] = second;\n                int mindepth = Math.min(values[first] & 0xff, values[second] & 0xff);\n                values[last] = lastVal = values[first] + values[second] - mindepth + 1;\n   \t  \n                /* Again, propagate the hole to the leafs */\n                ppos = 0;\n                path = 1;\n                while (path < heapLen)\n                {\n                    if (path + 1 < heapLen\n                            && values[heap[path]] > values[heap[path+1]])\n                        path++;\n   \t      \n                    heap[ppos] = heap[path];\n                    ppos = path;\n                    path = ppos * 2 + 1;\n                }\n   \t  \n                /* Now propagate the new element down along path */\n                while ((path = ppos) > 0\n                        && values[heap[ppos = (path - 1)/2]] > lastVal)\n                    heap[path] = heap[ppos];\n                heap[path] = last;\n            }\n            while (heapLen > 1);\n         \n            if (heap[0] != childs.length / 2 - 1)\n                throw new RuntimeException(\"Weird!\");\n         \n            buildLength(childs);\n        }\n   \n        int getEncodedLength()\n        {\n            int len = 0;\n            for (int i = 0; i < freqs.length; i++)\n                len += freqs[i] * length[i];\n            return len;\n        }\n   \n        void calcBLFreq(Tree blTree) {\n            int max_count;               /* max repeat count */\n            int min_count;               /* min repeat count */\n            int count;                   /* repeat count of the current code */\n            int curlen = -1;             /* length of current code */\n         \n            int i = 0;\n            while (i < numCodes)\n            {\n                count = 1;\n                int nextlen = length[i];\n                if (nextlen == 0)\n                {\n                    max_count = 138;\n                    min_count = 3;\n                }\n                else\n                {\n                    max_count = 6;\n                    min_count = 3;\n                    if (curlen != nextlen)\n                    {\n                        blTree.freqs[nextlen]++;\n                        count = 0;\n                    }\n                }\n                curlen = nextlen;\n                i++;\n   \n                while (i < numCodes && curlen == length[i])\n                {\n                    i++;\n                    if (++count >= max_count)\n                        break;\n                }\n   \n                if (count < min_count)\n                    blTree.freqs[curlen] += count;\n                else if (curlen != 0)\n                    blTree.freqs[REP_3_6]++;\n                else if (count <= 10)\n                    blTree.freqs[REP_3_10]++;\n                else\n                    blTree.freqs[REP_11_138]++;\n            }\n        }\n   \n        void writeTree(Tree blTree)\n        {\n            int max_count;               /* max repeat count */\n            int min_count;               /* min repeat count */\n            int count;                   /* repeat count of the current code */\n            int curlen = -1;             /* length of current code */\n         \n            int i = 0;\n            while (i < numCodes)\n            {\n                count = 1;\n                int nextlen = length[i];\n                if (nextlen == 0)\n                {\n                    max_count = 138;\n                    min_count = 3;\n                }\n                else\n                {\n                    max_count = 6;\n                    min_count = 3;\n                    if (curlen != nextlen)\n                    {\n                        blTree.writeSymbol(nextlen);\n                        count = 0;\n                    }\n                }\n                curlen = nextlen;\n                i++;\n   \n                while (i < numCodes && curlen == length[i])\n                {\n                    i++;\n                    if (++count >= max_count)\n                        break;\n                }\n   \n                if (count < min_count)\n                {\n                    while (count-- > 0)\n                        blTree.writeSymbol(curlen);\n                }\n                else if (curlen != 0)\n                {\n                    blTree.writeSymbol(REP_3_6);\n                    pending.writeBits(count - 3, 2);\n                }\n                else if (count <= 10)\n                {\n                    blTree.writeSymbol(REP_3_10);\n                    pending.writeBits(count - 3, 3);\n                }\n                else\n                {\n                    blTree.writeSymbol(REP_11_138);\n                    pending.writeBits(count - 11, 7);\n                }\n            }\n        }\n    }\n   \n   \n   \n    DeflaterPending pending;\n    private Tree literalTree, distTree, blTree;\n   \n    private short d_buf[];\n    private byte l_buf[];\n    private int last_lit;\n    private int extra_bits;\n   \n    private static short staticLCodes[];\n    private static byte  staticLLength[];\n    private static short staticDCodes[];\n    private static byte  staticDLength[];\n   \n    /**\n     * Reverse the bits of a 16 bit value.\n     */\n    static short bitReverse(int value) {\n        return (short) (bit4Reverse.charAt(value & 0xf) << 12\n                | bit4Reverse.charAt((value >> 4) & 0xf) << 8\n                | bit4Reverse.charAt((value >> 8) & 0xf) << 4\n                | bit4Reverse.charAt(value >> 12));\n    }\n   \n    static {\n        /* See RFC 1951 3.2.6 */\n        /* Literal codes */\n        staticLCodes = new short[LITERAL_NUM];\n        staticLLength = new byte[LITERAL_NUM];\n        int i = 0;\n        while (i < 144) {\n            staticLCodes[i] = bitReverse((0x030 + i) << 8);\n            staticLLength[i++] = 8;\n        }\n        while (i < 256) {\n            staticLCodes[i] = bitReverse((0x190 - 144 + i) << 7);\n            staticLLength[i++] = 9;\n        }\n        while (i < 280) {\n            staticLCodes[i] = bitReverse((0x000 - 256 + i) << 9);\n            staticLLength[i++] = 7;\n        }\n        while (i < LITERAL_NUM) {\n            staticLCodes[i] = bitReverse((0x0c0 - 280 + i)  << 8);\n            staticLLength[i++] = 8;\n        }\n   \n        /* Distant codes */\n        staticDCodes = new short[DIST_NUM];\n        staticDLength = new byte[DIST_NUM];\n        for (i = 0; i < DIST_NUM; i++) {\n            staticDCodes[i] = bitReverse(i << 11);\n            staticDLength[i] = 5;\n        }\n    }\n       \n    public DeflaterHuffman(DeflaterPending pending)\n    {\n        this.pending = pending;\n   \n        literalTree = new Tree(LITERAL_NUM, 257, 15);\n        distTree    = new Tree(DIST_NUM, 1, 15);\n        blTree      = new Tree(BITLEN_NUM, 4, 7);\n   \n        d_buf = new short[BUFSIZE];\n        l_buf = new byte [BUFSIZE];\n    }\n   \n    public final void reset() {\n        last_lit = 0;\n        extra_bits = 0;\n        literalTree.reset();\n        distTree.reset();\n        blTree.reset();\n    }\n   \n    private int l_code(int len) {\n        if (len == 255)\n            return 285;\n   \n        int code = 257;\n        while (len >= 8)\n        {\n            code += 4;\n            len >>= 1;\n        }\n        return code + len;\n    }\n   \n    private int d_code(int distance) {\n        int code = 0;\n        while (distance >= 4)\n        {\n            code += 2;\n            distance >>= 1;\n        }\n        return code + distance;\n    }\n   \n    public void sendAllTrees(int blTreeCodes) {\n        blTree.buildCodes();\n        literalTree.buildCodes();\n        distTree.buildCodes();\n        pending.writeBits(literalTree.numCodes - 257, 5);\n        pending.writeBits(distTree.numCodes - 1, 5);\n        pending.writeBits(blTreeCodes - 4, 4);\n        for (int rank = 0; rank < blTreeCodes; rank++)\n            pending.writeBits(blTree.length[BL_ORDER[rank]], 3);\n        literalTree.writeTree(blTree);\n        distTree.writeTree(blTree);\n        if (DeflaterConstants.DEBUGGING)\n            blTree.checkEmpty();\n    }\n   \n    public void compressBlock() {\n        for (int i = 0; i < last_lit; i++)\n        {\n            int litlen = l_buf[i] & 0xff;\n            int dist = d_buf[i];\n            if (dist-- != 0)\n            {\n                if (DeflaterConstants.DEBUGGING)\n                    System.err.print(\"[\"+(dist+1)+\",\"+(litlen+3)+\"]: \");\n   \n                int lc = l_code(litlen);\n                literalTree.writeSymbol(lc);\n   \n                int bits = (lc - 261) / 4;\n                if (bits > 0 && bits <= 5)\n                    pending.writeBits(litlen & ((1 << bits) - 1), bits);\n   \n                int dc = d_code(dist);\n                distTree.writeSymbol(dc);\n   \n                bits = dc / 2 - 1;\n                if (bits > 0)\n                    pending.writeBits(dist & ((1 << bits) - 1), bits);\n            }\n            else\n            {\n                if (DeflaterConstants.DEBUGGING)\n                {\n                    if (litlen > 32 && litlen < 127)\n                        System.err.print(\"(\"+(char)litlen+\"): \");\n                    else\n                        System.err.print(\"{\"+litlen+\"}: \");\n                }\n                literalTree.writeSymbol(litlen);\n            }\n        }\n        if (DeflaterConstants.DEBUGGING)\n            System.err.print(\"EOF: \");\n        literalTree.writeSymbol(EOF_SYMBOL);\n        if (DeflaterConstants.DEBUGGING)\n        {\n            literalTree.checkEmpty();\n            distTree.checkEmpty();\n        }\n    }\n   \n    public void flushStoredBlock(byte[] stored,\n                                 int stored_offset, int stored_len,\n                                 boolean lastBlock) {\n        if (DeflaterConstants.DEBUGGING)\n            System.err.println(\"Flushing stored block \"+ stored_len);\n        pending.writeBits((DeflaterConstants.STORED_BLOCK << 1)\n                + (lastBlock ? 1 : 0), 3);\n        pending.alignToByte();\n        pending.writeShort(stored_len);\n        pending.writeShort(~stored_len);\n        pending.writeBlock(stored, stored_offset, stored_len);\n        reset();\n    }\n   \n    public void flushBlock(byte[] stored, int stored_offset, int stored_len,\n                           boolean lastBlock) {\n        literalTree.freqs[EOF_SYMBOL]++;\n   \n        /* Build trees */\n        literalTree.buildTree();\n        distTree.buildTree();\n   \n        /* Calculate bitlen frequency */\n        literalTree.calcBLFreq(blTree);\n        distTree.calcBLFreq(blTree);\n   \n        /* Build bitlen tree */\n        blTree.buildTree();\n   \n        int blTreeCodes = 4;\n        for (int i = 18; i > blTreeCodes; i--)\n        {\n            if (blTree.length[BL_ORDER[i]] > 0)\n                blTreeCodes = i+1;\n        }\n        int opt_len = 14 + blTreeCodes * 3 + blTree.getEncodedLength()\n                + literalTree.getEncodedLength() + distTree.getEncodedLength()\n                + extra_bits;\n   \n        int static_len = extra_bits;\n        for (int i = 0; i < LITERAL_NUM; i++)\n            static_len += literalTree.freqs[i] * staticLLength[i];\n        for (int i = 0; i < DIST_NUM; i++)\n            static_len += distTree.freqs[i] * staticDLength[i];\n        if (opt_len >= static_len)\n        {\n            /* Force static trees */\n            opt_len = static_len;\n        }\n   \n        if (stored_offset >= 0 && stored_len+4 < opt_len >> 3)\n        {\n            /* Store Block */\n            if (DeflaterConstants.DEBUGGING)\n                System.err.println(\"Storing, since \" + stored_len + \" < \" + opt_len\n                        + \" <= \" + static_len);\n            flushStoredBlock(stored, stored_offset, stored_len, lastBlock);\n        }\n        else if (opt_len == static_len)\n        {\n            /* Encode with static tree */\n            pending.writeBits((DeflaterConstants.STATIC_TREES << 1)\n                    + (lastBlock ? 1 : 0), 3);\n            literalTree.setStaticCodes(staticLCodes, staticLLength);\n            distTree.setStaticCodes(staticDCodes, staticDLength);\n            compressBlock();\n            reset();\n        }\n        else\n        {\n            /* Encode with dynamic tree */\n            pending.writeBits((DeflaterConstants.DYN_TREES << 1)\n                    + (lastBlock ? 1 : 0), 3);\n            sendAllTrees(blTreeCodes);\n            compressBlock();\n            reset();\n        }\n    }\n   \n    public final boolean isFull()\n    {\n        return last_lit == BUFSIZE;\n    }\n   \n    public final boolean tallyLit(int lit)\n    {\n        if (DeflaterConstants.DEBUGGING)\n        {\n            if (lit > 32 && lit < 127)\n                System.err.println(\"(\"+(char)lit+\")\");\n            else\n                System.err.println(\"{\"+lit+\"}\");\n        }\n        d_buf[last_lit] = 0;\n        l_buf[last_lit++] = (byte) lit;\n        literalTree.freqs[lit]++;\n        return last_lit == BUFSIZE;\n    }\n   \n    public final boolean tallyDist(int dist, int len)\n    {\n        if (DeflaterConstants.DEBUGGING)\n            System.err.println(\"[\"+dist+\",\"+len+\"]\");\n   \n        d_buf[last_lit] = (short) dist;\n        l_buf[last_lit++] = (byte) (len - 3);\n   \n        int lc = l_code(len-3);\n        literalTree.freqs[lc]++;\n        if (lc >= 265 && lc < 285)\n            extra_bits += (lc - 261) / 4;\n   \n        int dc = d_code(dist-1);\n        distTree.freqs[dc]++;\n        if (dc >= 4)\n            extra_bits += dc / 2 - 1;\n        return last_lit == BUFSIZE;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/DeflaterOutputStream.java",
    "content": " /* DeflaterOutputStream.java - Output filter for compressing.\n    Copyright (C) 1999, 2000, 2001, 2004, 2005 Free Software Foundation, Inc.\n \n This file is part of GNU Classpath.\n \n GNU Classpath is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2, or (at your option)\n any later version.\n \n GNU Classpath is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n General Public License for more details.\n \n You should have received a copy of the GNU General Public License\n along with GNU Classpath; see the file COPYING.  If not, write to the\n Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n 02110-1301 USA.\n \n Linking this library statically or dynamically with other modules is\n making a combined work based on this library.  Thus, the terms and\n conditions of the GNU General Public License cover the whole\n combination.\n \n As a special exception, the copyright holders of this library give you\n permission to link this library with independent modules to produce an\n executable, regardless of the license terms of these independent\n modules, and to copy and distribute the resulting executable under\n terms of your choice, provided that you also meet, for each linked\n independent module, the terms and conditions of the license of that\n module.  An independent module is a module which is not derived from\n or based on this library.  If you modify this library, you may extend\n this exception to your version of the library, but you are not\n obligated to do so.  If you do not wish to do so, delete this\n exception statement from your version. */\n \n \n package java.util.zip;\n \n import java.io.FilterOutputStream;\n import java.io.IOException;\n import java.io.OutputStream;\n \n /* Written using on-line Java Platform 1.2 API Specification\n  * and JCL book.\n  * Believed complete and correct.\n  */\n \n /**\n  * This is a special FilterOutputStream deflating the bytes that are\n  * written through it.  It uses the Deflater for deflating.\n  *\n  * A special thing to be noted is that flush() doesn't flush\n  * everything in Sun's JDK, but it does so in jazzlib. This is because\n  * Sun's Deflater doesn't have a way to flush() everything, without\n  * finishing the stream.\n  *\n  * @author Tom Tromey, Jochen Hoenicke\n  * @date Jan 11, 2001 \n  */\n public class DeflaterOutputStream extends FilterOutputStream\n {\n     /**\n      * This buffer is used temporarily to retrieve the bytes from the\n      * deflater and write them to the underlying output stream.\n      */\n     protected byte[] buf;\n \n     /**\n      * The deflater which is used to deflate the stream.\n      */\n     protected Deflater def;\n   \n     /**\n      * Deflates everything in the def's input buffers.  This will call\n      * <code>def.deflate()</code> until all bytes from the input buffers\n      * are processed.\n      */\n     protected void deflate() throws IOException\n     {\n         while (! def.needsInput())\n         {\n             int len = def.deflate(buf, 0, buf.length);\n \n             //    System.err.println(\"DOS deflated \" + len + \" out of \" + buf.length);\n             if (len <= 0)\n                 break;\n             out.write(buf, 0, len);\n         }\n \n         if (! def.needsInput())\n             throw new InternalError(\"Can't deflate all input?\");\n     }\n \n     /**\n      * Creates a new DeflaterOutputStream with a default Deflater and\n      * default buffer size.\n      * @param out the output stream where deflated output should be written.\n      */\n     public DeflaterOutputStream(OutputStream out)\n     {\n         this(out, new Deflater(), 4096);\n     }\n \n     /**\n      * Creates a new DeflaterOutputStream with the given Deflater and\n      * default buffer size.\n      * @param out the output stream where deflated output should be written.\n      * @param defl the underlying deflater.\n      */\n     public DeflaterOutputStream(OutputStream out, Deflater defl)\n     {\n         this(out, defl, 4096);\n     }\n \n     /**\n      * Creates a new DeflaterOutputStream with the given Deflater and\n      * buffer size.\n      * @param out the output stream where deflated output should be written.\n      * @param defl the underlying deflater.\n      * @param bufsize the buffer size.\n      * @exception IllegalArgumentException if bufsize isn't positive.\n      */\n     public DeflaterOutputStream(OutputStream out, Deflater defl, int bufsize)\n     {\n         super(out);\n         if (bufsize <= 0)\n             throw new IllegalArgumentException(\"bufsize <= 0\");\n         buf = new byte[bufsize];\n         def = defl;\n     }\n \n     /**\n      * Flushes the stream by calling flush() on the deflater and then\n      * on the underlying stream.  This ensures that all bytes are\n      * flushed.  This function doesn't work in Sun's JDK, but only in\n      * jazzlib.\n      */\n     public void flush() throws IOException\n     {\n         def.flush();\n         deflate();\n         out.flush();\n     }\n \n     /**\n      * Finishes the stream by calling finish() on the deflater.  This\n      * was the only way to ensure that all bytes are flushed in Sun's\n      * JDK.\n      */\n     public void finish() throws IOException\n     {\n         def.finish();\n         while (! def.finished())\n         {\n             int len = def.deflate(buf, 0, buf.length);\n             if (len <= 0)\n                 break;\n             out.write(buf, 0, len);\n         }\n         if (! def.finished())\n             throw new InternalError(\"Can't deflate all input?\");\n         out.flush();\n     }\n \n     /**\n      * Calls finish() and closes the stream.\n      */\n     public void close() throws IOException\n     {\n         finish();\n         out.close();\n     }\n \n     /**\n      * Writes a single byte to the compressed output stream.\n      * @param bval the byte value.\n      */\n     public void write(int bval) throws IOException\n     {\n         byte[] b = new byte[1];\n         b[0] = (byte) bval;\n         write(b, 0, 1);\n     }\n \n     /**\n      * Writes a len bytes from an array to the compressed stream.\n      * @param buf the byte array.\n      * @param off the offset into the byte array where to start.\n      * @param len the number of bytes to write.\n      */\n     public void write(byte[] buf, int off, int len) throws IOException\n     {\n         def.setInput(buf, off, len);\n         deflate();\n     }\n }"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/DeflaterPending.java",
    "content": "/* java.util.zip.DeflaterPending\n   Copyright (C) 2001 Free Software Foundation, Inc.\n   \n   This file is part of GNU Classpath.\n   \n   GNU Classpath is free software; you can redistribute it and/or modify\n   it under the terms of the GNU General Public License as published by\n   the Free Software Foundation; either version 2, or (at your option)\n   any later version.\n   \n   GNU Classpath is distributed in the hope that it will be useful, but\n   WITHOUT ANY WARRANTY; without even the implied warranty of\n   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n   General Public License for more details.\n   \n   You should have received a copy of the GNU General Public License\n   along with GNU Classpath; see the file COPYING.  If not, write to the\n   Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n   02110-1301 USA.\n   \n   Linking this library statically or dynamically with other modules is\n   making a combined work based on this library.  Thus, the terms and\n   conditions of the GNU General Public License cover the whole\n   combination.\n   \n   As a special exception, the copyright holders of this library give you\n   permission to link this library with independent modules to produce an\n   executable, regardless of the license terms of these independent\n   modules, and to copy and distribute the resulting executable under\n   terms of your choice, provided that you also meet, for each linked\n   independent module, the terms and conditions of the license of that\n   module.  An independent module is a module which is not derived from\n   or based on this library.  If you modify this library, you may extend\n   this exception to your version of the library, but you are not\n   obligated to do so.  If you do not wish to do so, delete this\n   exception statement from your version. */\n\npackage java.util.zip;\n   \n/**\n * This class stores the pending output of the Deflater.\n *\n * @author Jochen Hoenicke\n * @date Jan 5, 2000\n */\n   \nclass DeflaterPending extends PendingBuffer\n{\n    public DeflaterPending()\n    {\n        super(DeflaterConstants.PENDING_BUF_SIZE);\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/GZIPInputStream.java",
    "content": "/*******************************************************************************\r\n * Copyright 2011 See AUTHORS file.\r\n * \r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n * \r\n *   http://www.apache.org/licenses/LICENSE-2.0\r\n * \r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n ******************************************************************************/\n\n\npackage java.util.zip;\n\nimport java.io.IOException;\r\nimport java.io.InputStream;\n\n/** Dummy emulation. Throws a GdxRuntimeException on first read.\n * @author hneuer */\npublic class GZIPInputStream extends InflaterInputStream {\n\tpublic GZIPInputStream (InputStream in, int size) {\n\t\tsuper(in);\n\t}\r\n\tpublic GZIPInputStream(InputStream in) throws IOException {\r\n\t\tsuper(in);\r\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/InflaterInputStream.java",
    "content": "/*******************************************************************************\r\n * Copyright 2011 See AUTHORS file.\r\n * \r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n * \r\n *   http://www.apache.org/licenses/LICENSE-2.0\r\n * \r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n ******************************************************************************/\n\n\npackage java.util.zip;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/** Dummy emulation. Throws a GdxRuntimeException on first read.\n * @author hneuer */\npublic class InflaterInputStream extends InputStream {\n\tprivate InputStream in;\n\n\tpublic InflaterInputStream (InputStream in) {\n\t\tthis.in = in;\n\t}\n\n\t@Override\n\tpublic int read () throws IOException {\n\t\tthrow new IOException(\"InflaterInputStream not supported in GWT\");\n\t}\n\n\t@Override\n\tpublic void close () throws IOException {\n\t\tsuper.close();\n\t\t//StreamUtils.closeQuietly(in);\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/java/util/zip/PendingBuffer.java",
    "content": "/* java.util.zip.PendingBuffer\n      Copyright (C) 2001 Free Software Foundation, Inc.\n   \n   This file is part of GNU Classpath.\n   \n   GNU Classpath is free software; you can redistribute it and/or modify\n   it under the terms of the GNU General Public License as published by\n   the Free Software Foundation; either version 2, or (at your option)\n   any later version.\n   \n   GNU Classpath is distributed in the hope that it will be useful, but\n   WITHOUT ANY WARRANTY; without even the implied warranty of\n   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n   General Public License for more details.\n   \n   You should have received a copy of the GNU General Public License\n   along with GNU Classpath; see the file COPYING.  If not, write to the\n   Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n   02110-1301 USA.\n   \n   Linking this library statically or dynamically with other modules is\n   making a combined work based on this library.  Thus, the terms and\n   conditions of the GNU General Public License cover the whole\n   combination.\n   \n   As a special exception, the copyright holders of this library give you\n   permission to link this library with independent modules to produce an\n   executable, regardless of the license terms of these independent\n   modules, and to copy and distribute the resulting executable under\n   terms of your choice, provided that you also meet, for each linked\n   independent module, the terms and conditions of the license of that\n   module.  An independent module is a module which is not derived from\n   or based on this library.  If you modify this library, you may extend\n   this exception to your version of the library, but you are not\n   obligated to do so.  If you do not wish to do so, delete this\n   exception statement from your version. */\n   \npackage java.util.zip;\n   \n/**\n * This class is general purpose class for writing data to a buffer.\n *\n * It allows you to write bits as well as bytes\n *\n * Based on DeflaterPending.java\n *\n * @author Jochen Hoenicke\n * @date Jan 5, 2000\n */\n   \nclass PendingBuffer\n{\n    protected byte[] buf;\n    int    start;\n    int    end;\n   \n    int    bits;\n    int    bitCount;\n   \n    public PendingBuffer()\n    {\n        this( 4096 );\n    }\n   \n    public PendingBuffer(int bufsize)\n    {\n        buf = new byte[bufsize];\n    }\n   \n    public final void reset() {\n        start = end = bitCount = 0;\n    }\n   \n    public final void writeByte(int b)\n    {\n        if (DeflaterConstants.DEBUGGING && start != 0)\n            throw new IllegalStateException();\n        buf[end++] = (byte) b;\n    }\n   \n    public final void writeShort(int s)\n    {\n        if (DeflaterConstants.DEBUGGING && start != 0)\n            throw new IllegalStateException();\n        buf[end++] = (byte) s;\n        buf[end++] = (byte) (s >> 8);\n    }\n   \n    public final void writeInt(int s)\n    {\n        if (DeflaterConstants.DEBUGGING && start != 0)\n            throw new IllegalStateException();\n        buf[end++] = (byte) s;\n        buf[end++] = (byte) (s >> 8);\n        buf[end++] = (byte) (s >> 16);\n        buf[end++] = (byte) (s >> 24);\n    }\n   \n    public final void writeBlock(byte[] block, int offset, int len)\n    {\n        if (DeflaterConstants.DEBUGGING && start != 0)\n            throw new IllegalStateException();\n        System.arraycopy(block, offset, buf, end, len);\n        end += len;\n    }\n   \n    public final int getBitCount() {\n        return bitCount;\n    }\n   \n    public final void alignToByte() {\n        if (DeflaterConstants.DEBUGGING && start != 0)\n            throw new IllegalStateException();\n        if (bitCount > 0)\n        {\n            buf[end++] = (byte) bits;\n            if (bitCount > 8)\n                buf[end++] = (byte) (bits >>> 8);\n        }\n        bits = 0;\n        bitCount = 0;\n    }\n   \n    public final void writeBits(int b, int count)\n    {\n        if (DeflaterConstants.DEBUGGING && start != 0)\n            throw new IllegalStateException();\n        if (DeflaterConstants.DEBUGGING)\n            System.err.println(\"writeBits(\"+Integer.toHexString(b)+\",\"+count+\")\");\n        bits |= b << bitCount;\n        bitCount += count;\n        if (bitCount >= 16) {\n            buf[end++] = (byte) bits;\n            buf[end++] = (byte) (bits >>> 8);\n            bits >>>= 16;\n            bitCount -= 16;\n        }\n    }\n   \n    public final void writeShortMSB(int s) {\n        if (DeflaterConstants.DEBUGGING && start != 0)\n            throw new IllegalStateException();\n        buf[end++] = (byte) (s >> 8);\n        buf[end++] = (byte) s;\n    }\n   \n    public final boolean isFlushed() {\n        return end == 0;\n    }\n   \n    /**\n     * Flushes the pending buffer into the given output array.  If the\n     * output array is to small, only a partial flush is done.\n     *\n     * @param output the output array;\n     * @param offset the offset into output array;\n     * @param length the maximum number of bytes to store;\n     * @exception IndexOutOfBoundsException if offset or length are\n     * invalid.\n     */\n    public final int flush(byte[] output, int offset, int length) {\n        if (bitCount >= 8)\n        {\n            buf[end++] = (byte) bits;\n            bits >>>= 8;\n            bitCount -= 8;\n        }\n        if (length > end - start)\n        {\n            length = end - start;\n            System.arraycopy(buf, start, output, offset, length);\n            start = 0;\n            end = 0;\n        }\n        else\n        {\n            System.arraycopy(buf, start, output, offset, length);\n            start += length;\n        }\n        return length;\n    }\n   \n    /**\n     * Flushes the pending buffer and returns that data in a new array\n     *\n     * @return the output stream\n     */\n   \n    public final byte[] toByteArray()\n    {\n        byte[] ret = new byte[ end - start ];\n        System.arraycopy(buf, start, ret, 0, ret.length);\n        start = 0;\n        end = 0;\n        return ret;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/application/Platform.java",
    "content": "package javafx.application;\n\npublic final class Platform {\n\n    public static void runLater(Runnable runnable) {};\n\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/embed/swing/JFXPanel.java",
    "content": "package javafx.embed.swing;\n\npublic final class JFXPanel {\n    public JFXPanel(){\n\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/embed/swing/SwingFXUtils.java",
    "content": "package javafx.embed.swing;\n\nimport java.awt.image.BufferedImage;\nimport javafx.scene.image.Image;\n\npublic final class SwingFXUtils {\n    public static BufferedImage fromFXImage(Image img, BufferedImage bimg) {\n        return null;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/scene/SnapshotParameters.java",
    "content": "package javafx.scene;\n\npublic final class SnapshotParameters {\n\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/scene/image/Image.java",
    "content": "package javafx.scene.image;\n\npublic class Image{\n\n    public Image() {\n\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/scene/image/WritableImage.java",
    "content": "package javafx.scene.image;\n\npublic final class WritableImage extends Image {\n    public WritableImage(int width, int height) {\n        super();\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/scene/media/Media.java",
    "content": "package javafx.scene.media;\n\nimport javafx.util.Duration;\n\npublic final class Media {\n    public Media(String source){\n\n    }\n    public final Duration getDuration() {\n        return null;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/scene/media/MediaPlayer.java",
    "content": "package javafx.scene.media;\nimport javafx.util.Duration;\n\npublic final class MediaPlayer{\n    public MediaPlayer(Media media){\n\n    }\n    public void seek(Duration seekTime) {\n\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/scene/media/MediaView.java",
    "content": "package javafx.scene.media;\nimport javafx.scene.image.WritableImage;\nimport javafx.scene.SnapshotParameters;\n\npublic final class MediaView{\n    public MediaView() {\n\n    }\n    public void setFitWidth(int width){\n\n    }\n    public void setFitHeight(int height) {\n\n    }\n    public void setMediaPlayer(MediaPlayer mediaPlayer){\n\n    }\n    public void setPreserveRatio(boolean preserverRatio) {\n\n    }\n    public WritableImage snapshot(SnapshotParameters params, WritableImage image){\n        return null;\n    }\n}"
  },
  {
    "path": "src/peergos/gwt/emu/javafx/util/Duration.java",
    "content": "package javafx.util;\n\npublic final class Duration {\n\n    public static Duration seconds(double s) {\n        return null;\n    }\n\n    public boolean isUnknown() {\n        return false;\n    }\n    public double toSeconds() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/javax/crypto/Mac.java",
    "content": "package javax.crypto;\n\nimport javax.crypto.spec.SecretKeySpec;\n\npublic class Mac {\n\n\tpublic static Mac getInstance(String algo)\n\t{\n\t\treturn null;\n\t}\n\t\n\tpublic void init(SecretKeySpec spec)\n\t{\n\t\t\n\t}\n\t\n\tpublic int getMacLength()\n\t{\n\t\treturn -1;\n\t}\n\t\n\tpublic void update(byte[] b)\n\t{\n\t\t\n\t}\n\t\n\tpublic void doFinal(byte[] a, int i)\n\t{\n\t\t\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/javax/crypto/spec/SecretKeySpec.java",
    "content": "package javax.crypto.spec;\n\npublic class SecretKeySpec {\n\n\tprivate final byte[] key;\n\tprivate final String algorithm;\n\tpublic SecretKeySpec(byte[] key, String algorithm)\n\t{\n\t\tthis.key = key;\n\t\tthis.algorithm = algorithm;\n\t}\n\n\tpublic byte[] getEncoded() {\n\t\treturn this.key;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/gwt/emu/javax/imageio/ImageIO.java",
    "content": "package javax.imageio;\n\nimport java.awt.image.BufferedImage;\nimport java.awt.image.RenderedImage;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.IOException;\n\n/*\n*  Dummy implementation - does nothing\n* */\npublic class ImageIO {\n\n    public static BufferedImage read(InputStream input) throws IOException {\n        return null;\n    }\n\n    public static boolean write(RenderedImage im,\n                                String formatName,\n                                OutputStream output) throws IOException {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/AggregatedMetrics.java",
    "content": "package peergos.server;\n\nimport io.prometheus.client.Counter;\nimport io.prometheus.client.Histogram;\nimport io.prometheus.client.exporter.HTTPServer;\nimport peergos.server.util.*;\n\nimport java.io.IOException;\nimport java.util.*;\n\n/**\n * A wrapper around the prometheus metrics and HTTP exporter.\n */\npublic class AggregatedMetrics {\n    private static Counter build(String name, String help) {\n        return Counter.build()\n                .name(name).help(help).register();\n    }\n\n    public static final Counter FOLLOW_REQUEST_COUNTER = build(\"send_follow_request_total\",\"Total follow requests.\");\n    public static final Counter GET_FOLLOW_REQUEST_COUNTER = build(\"get_follow_request_counter\", \"Total get follow request calls.\");\n    public static final Counter REMOVE_FOLLOW_REQUEST_COUNTER = build(\"remove_follow_request_counter\", \"Total remove follow request calls.\");\n\n    public static final Counter PUBLIC_FILE_COUNTER = build(\"public_file_counter\", \"Total public files.\");\n\n    public static final Counter STORAGE_ID  = build(\"storage_id\", \"Total id calls.\");\n    public static final Counter STORAGE_IDS  = build(\"storage_ids\", \"Total ids calls.\");\n    public static final Counter STORAGE_BLOCK_PUT  = build(\"storage_block_put\", \"Total DHT block puts.\");\n    public static final Counter STORAGE_BLOCK_PUT_BULK  = build(\"storage_block_put_bulk\", \"Total bulk block puts.\");\n    public static final Counter STORAGE_BLOCK_GET  = build(\"storage_block_get\", \"Total DHT block gets.\");\n    public static final Counter STORAGE_BLOCK_STAT  = build(\"storage_block_stat\", \"Total DHT block stats.\");\n    public static final Counter STORAGE_BLOCK_REFS  = build(\"storage_block_refs\", \"Total DHT block refs.\");\n    public static final Counter STORAGE_TRANSACTION_START  = build(\"storage_transaction_start\", \"Total DHT transaction starts.\");\n    public static final Counter STORAGE_TRANSACTION_CLOSE  = build(\"storage_transaction_close\", \"Total DHT transaction closes.\");\n    public static final Counter STORAGE_CHAMP_GET  = build(\"storage_champ_get\", \"Total champ gets\");\n    public static final Counter STORAGE_LINK_GET  = build(\"storage_link_get\", \"Total link gets\");\n    public static final Counter STORAGE_LINK_COUNTS  = build(\"storage_link_counts\", \"Total link counts\");\n    public static final Counter STORAGE_IPNS_GET  = build(\"storage_ipns_get\", \"Total ipns gets\");\n    public static final Histogram STORAGE_CHAMP_GET_DURATION = Histogram.build()\n            .labelNames(\"duration\")\n            .name(\"champ_get_duration\")\n            .help(\"Time to respond to a champ.get call\")\n            .exponentialBuckets(0.01, 2, 16)\n            .register();\n\n    public static final Histogram STORAGE_LINK_GET_DURATION = Histogram.build()\n            .labelNames(\"duration\")\n            .name(\"link_get_duration\")\n            .help(\"Time to respond to a link.get call\")\n            .exponentialBuckets(0.01, 2, 16)\n            .register();\n\n    public static final Counter MUTABLE_POINTERS_SET  = build(\"mutable_pointers_set\", \"Total mutable-pointers set calls.\");\n    public static final Counter MUTABLE_POINTERS_GET  = build(\"mutable_pointers_get\", \"Total mutable-pointers get calls.\");\n\n    public static final Counter LOGIN_SET  = build(\"login_set\", \"Total login set calls.\");\n    public static final Counter LOGIN_GET  = build(\"login_get\", \"Total successful login get calls.\");\n    public static final Counter LOGIN_GET_FAILURE_PASSWORD  = build(\"login_get_failure_password\", \"Total login get calls with incorrect signature.\");\n    public static final Counter LOGIN_GET_FAILURE_EXTERNAL  = build(\"login_get_failure_external\", \"Total failed login get calls for external users.\");\n    public static final Counter LOGIN_GET_MFA  = build(\"login_get_mfa\", \"Total get mfa calls.\");\n    public static final Counter LOGIN_ADD_TOTP  = build(\"login_add_totp\", \"Total add totp calls.\");\n    public static final Counter LOGIN_ENABLE_TOTP  = build(\"login_enable_totp\", \"Total enable totp calls.\");\n    public static final Counter LOGIN_WEBAUTHN_START  = build(\"login_webauthn_start\", \"Total webauthn start calls.\");\n    public static final Counter LOGIN_WEBAUTHN_COMPLETE  = build(\"login_webauthn_complete\", \"Total webauthn complete calls.\");\n    public static final Counter LOGIN_DELETE_MFA  = build(\"login_delete_mfa\", \"Total delete mfa calls.\");\n\n    public static final Counter BAT_ADD  = build(\"bat_add\", \"Total addBat calls.\");\n    public static final Counter BATS_GET  = build(\"bats_get\", \"Total getBats calls.\");\n\n    public static final Counter GET_ALL_USERNAMES  = build(\"core_node_get_all_usernames\", \"Total get-all-usernames calls.\");\n    public static final Counter GET_USERNAME  = build(\"core_node_get_username\", \"Total get-username calls.\");\n    public static final Counter GET_PUBLIC_KEY  = build(\"core_node_get_public_key\", \"Total get-public-key calls.\");\n    public static final Counter GET_PUBLIC_KEY_CHAIN  = build(\"core_node_get_chain\", \"Total get-public-key-chain calls.\");\n    public static final Counter UPDATE_PUBLIC_KEY_CHAIN  = build(\"core_node_update_chain\", \"Total getupdate-public-key-chain calls.\");\n    public static final Counter PKI_RATE_LIMITED  = build(\"pki_rate_limited\", \"Total number of pki updates rate limited.\");\n    public static final Counter SIGNUP  = build(\"core_node_signup\", \"Total signup calls.\");\n    public static final Counter PAID_SIGNUP_START  = build(\"core_node_signup_paid_start\", \"Total start paid signup calls.\");\n    public static final Counter PAID_SIGNUP_COMPLETE  = build(\"core_node_signup_paid_complete\", \"Total complete paid signup calls.\");\n    public static final Counter PAID_SIGNUP_SUCCESS  = build(\"core_node_signup_paid_success\", \"Total successful paid signup calls.\");\n    public static final Counter MIRROR  = build(\"core_node_mirror\", \"Total mirror calls.\");\n    public static final Counter PAID_MIRROR_START  = build(\"core_node_mirror_paid_start\", \"Total start paid mirror calls.\");\n    public static final Counter PAID_MIRROR_COMPLETE  = build(\"core_node_mirror_paid_complete\", \"Total complete paid mirror calls.\");\n    public static final Counter GET_USER_SNAPSHOTS  = build(\"core_node_get_user_snapshots\", \"Total get user snapshots calls.\");\n    public static final Counter PAID_MIRROR_SUCCESS  = build(\"core_node_mirror_paid_success\", \"Total successful paid mirror calls.\");\n    public static final Counter MIGRATE_USER  = build(\"core_node_migrate_user\", \"Total migrate-user calls.\");\n\n    private static Set<String> runningExporters = new HashSet<>();\n\n    public static synchronized void startExporter(String address, int port) throws IOException {\n        String addr = address + \":\" + port;\n        if (runningExporters.contains(addr))\n            return;\n        Logging.LOG().info(\"Starting metrics server at \" + addr);\n        HTTPServer server = new HTTPServer(address, port);\n        runningExporters.add(addr);\n        //shutdown hook on signal\n        Runtime.getRuntime().addShutdownHook(new Thread(() -> server.close()));\n    }\n}\n\n"
  },
  {
    "path": "src/peergos/server/Builder.java",
    "content": "package peergos.server;\n\nimport com.zaxxer.hikari.*;\nimport io.libp2p.core.*;\nimport peergos.server.corenode.*;\nimport peergos.server.crypto.*;\nimport peergos.server.crypto.asymmetric.curve25519.*;\nimport peergos.server.crypto.asymmetric.mlkem.JavaMlkem;\nimport peergos.server.crypto.hash.*;\nimport peergos.server.crypto.random.*;\nimport peergos.server.login.*;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.storage.auth.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.crypto.asymmetric.mlkem.Mlkem;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.password.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.MultiAddress;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.sql.Connection;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\nimport static peergos.server.Main.VERSIONED_S3;\n\npublic class Builder {\n    private static final Logger LOG = Logger.getLogger(Builder.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n\n    public static Crypto initNativeCrypto(Salsa20Poly1305 symmetric, Ed25519 signer, Curve25519 boxer, Mlkem mlkem, Hasher h) {\n        SafeRandomJava random = new SafeRandomJava();\n        return Crypto.init(() -> new Crypto(random, h, symmetric, signer, boxer, mlkem));\n    }\n\n    public static Crypto initCrypto() {\n        return initCrypto(new ScryptJava());\n    }\n\n    public static Crypto initCrypto(Hasher h) {\n        try {\n            if (! \"linux\".equalsIgnoreCase(System.getProperty(\"os.name\")))\n                return JavaCrypto.init();\n            JniTweetNacl nativeNacl = JniTweetNacl.build();\n            Salsa20Poly1305 symmetricProvider = new JniTweetNacl.Symmetric(nativeNacl);\n            Ed25519 signer = new JniTweetNacl.Signer(nativeNacl);\n            Curve25519 boxer = new Curve25519Java();\n            JavaMlkem mlkem = new JavaMlkem();\n            return initNativeCrypto(symmetricProvider, signer, boxer, mlkem, h);\n        } catch (Throwable t) {\n            return JavaCrypto.init();\n        }\n    }\n\n    public static Supplier<Connection> getDBConnector(Args a, String dbName, Supplier<Connection> existing) {\n        boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n        if (usePostgres)\n            return existing;\n        return getDBConnector(a, dbName);\n    }\n\n    public static Supplier<Connection> getPostgresConnector(Args a, String prefix) {\n        String postgresHost = a.getArg(prefix + \"postgres.host\");\n        int postgresPort = a.getInt(prefix + \"postgres.port\", 5432);\n        String databaseName = a.getArg(prefix + \"postgres.database\", \"peergos\");\n        String postgresUsername = a.getArg(prefix + \"postgres.username\");\n        String postgresPassword = a.getArg(prefix + \"postgres.password\");\n\n        Properties props = new Properties();\n        props.setProperty(\"dataSourceClassName\", \"org.postgresql.ds.PGSimpleDataSource\");\n        props.setProperty(\"dataSource.serverName\", postgresHost);\n        props.setProperty(\"dataSource.portNumber\", \"\" + postgresPort);\n        props.setProperty(\"dataSource.user\", postgresUsername);\n        props.setProperty(\"dataSource.password\", postgresPassword);\n        props.setProperty(\"dataSource.databaseName\", databaseName);\n        HikariConfig config = new HikariConfig(props);\n        config.setMaximumPoolSize(5);\n        HikariDataSource ds = new HikariDataSource(config);\n\n        return () -> {\n            try {\n                return ds.getConnection();\n            } catch (SQLException e) {\n                throw new RuntimeException(e);\n            }\n        };\n    }\n\n    public static Supplier<Connection> getDBConnector(Args a, String dbName) {\n        boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n        if (usePostgres) {\n            return getPostgresConnector(a, \"\");\n        } else {\n            String sqlFilePath = Sqlite.getDbPath(a, dbName);\n            if (\":memory:\".equals(sqlFilePath))\n                return buildEphemeralSqlite();\n            try {\n                Connection memory = Sqlite.build(sqlFilePath);\n                // We need a connection that ignores close\n                Connection instance = new Sqlite.UncloseableConnection(memory);\n                return () -> instance;\n            } catch (SQLException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    public static Supplier<Connection> buildEphemeralSqlite() {\n        try {\n            Connection memory = Sqlite.build(\":memory:\");\n            // We need a connection that ignores close\n            Connection instance = new Sqlite.UncloseableConnection(memory);\n            return () -> instance;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static JavaPoster buildIpfsApi(Args a) {\n        URL ipfsApiAddress = AddressUtil.getAddress(new MultiAddress(a.getArg(\"ipfs-api-address\", \"/ip4/127.0.0.1/tcp/5001\")));\n        return new JavaPoster(ipfsApiAddress, false);\n    }\n\n    /**\n     *\n     * @param a\n     * @return This returns the P2P HTTP proxy, which is in the IPFS gateway\n     */\n    public static JavaPoster buildP2pHttpProxy(Args a) {\n        URL ipfsGatewayAddress = AddressUtil.getAddress(new MultiAddress(a.getArg(\"ipfs-gateway-address\")));\n        return new JavaPoster(ipfsGatewayAddress, false);\n    }\n\n    /** A number representing the size in bytes of the blockstore's bloom filter. A value of zero represents the feature is disabled.\n\n     This site generates useful graphs for various bloom filter values: https://hur.st/bloomfilter/?n=1e6&p=0.01&m=&k=7\n     You may use it to find a preferred optimal value, where m is BloomFilterSize in bits. Remember to convert the value\n     m from bits, into bytes for use as BloomFilterSize in the config file. For example, for 1,000,000 blocks, expecting\n     a 1% false-positive rate, you'd end up with a filter size of 9592955 bits, so for BloomFilterSize we'd want to use\n     1199120 bytes. As of writing, 7 hash functions are used, so the constant k is 7 in the formula.\n     *\n     * @param falsePositivesProbability\n     * @param numberOfBlocks\n     * @return\n     */\n    public static int bloomfilterSizeBytes(double falsePositivesProbability, long numberOfBlocks) {\n        int numberOfHashfunctions = 7;\n        return (int)Math.ceil((numberOfBlocks * Math.log(falsePositivesProbability)) / Math.log(1 / Math.pow(2, Math.log(2))))/8;\n\n    }\n\n    /**\n     * Create path to local blockstore directory from Args.\n     *\n     * @param args\n     * @return\n     */\n    public static Path blockstorePath(Args args) {\n        return args.fromPeergosDir(\"blockstore_dir\", \".ipfs/blocks\");\n    }\n\n    private static BlockStoreProperties buildS3Properties(Args a) {\n        S3Config config = S3Config.build(a, Optional.empty());\n        Optional<String> publicReadUrl = S3Config.getPublicReadUrl(a);\n        boolean directWrites = a.getBoolean(\"direct-s3-writes\", false);\n        boolean publicReads = a.getBoolean(\"public-s3-reads\", false);\n        boolean authedReads = a.getBoolean(\"authed-s3-reads\", false);\n        Optional<String> authedUrl = Optional.of(\"https://\" + config.getHost() + \"/\");\n        return new BlockStoreProperties(directWrites, publicReads, authedReads, publicReadUrl, authedUrl);\n    }\n\n    public static BlockMetadataStore buildBlockMetadata(Args a) {\n        try {\n            boolean usePostgres = a.getArg(\"block-metadata-db-type\", \"sqlite\").equals(\"postgres\");\n            if (usePostgres) {\n                return new CachingBlockMetadataStore(new JdbcBlockMetadataStore(getPostgresConnector(a, \"metadb.\"), new PostgresCommands()), 200_000);\n            } else {\n                File metaFile = a.fromPeergosDir(\"block-metadata-sql-file\", \"blockmetadata-v3.sql\").toFile();\n                Connection instance = new Sqlite.UncloseableConnection(Sqlite.build(metaFile.getPath()));\n                return new JdbcBlockMetadataStore(() -> instance, new SqliteCommands());\n            }\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static ContentAddressedStorageProxy buildP2PBlockRetrieverForS3(Args a,\n                                                                           UsageStore usage,\n                                                                           Hasher hasher,\n                                                                           ContentAddressedStorageProxy def) {\n        String remoteS3Prefix = \"remote.\";\n        if (a.hasArg(\"mirror.node.id\") && S3Config.useS3(a, remoteS3Prefix)) {\n            LOG.info(\"Reading directly from remote S3 for mirror\");\n            S3Config remoteConfig = S3Config.build(a, Optional.of(remoteS3Prefix));\n            Cid nodeToMirrorId = Cid.decode(a.getArg(\"mirror.node.id\"));\n            boolean legacyS3Path = a.getBoolean(\"use-legacy-mirror-s3-path\", false);\n            return new DirectS3Proxy(remoteConfig, nodeToMirrorId, usage, legacyS3Path, hasher);\n        }\n        return def;\n    }\n\n    public static DeletableContentAddressedStorage buildLocalStorage(Args a,\n                                                                     BlockMetadataStore meta,\n                                                                     JdbcBatCave bats,\n                                                                     TransactionStore transactions,\n                                                                     BlockRequestAuthoriser authoriser,\n                                                                     ServerIdentityStore ids,\n                                                                     UsageStore usage,\n                                                                     JdbcIpnsAndSocial rawPointers,\n                                                                     PartitionStatus partitionStatus,\n                                                                     Hasher hasher) throws SQLException {\n        boolean useIPFS = a.getBoolean(\"useIPFS\");\n        boolean enableGC = a.getBoolean(\"enable-gc\", false);\n        boolean useS3 = S3Config.useS3(a);\n        boolean versionedS3 = a.getBoolean(VERSIONED_S3.name);\n        JavaPoster ipfsApi = buildIpfsApi(a);\n        JavaPoster p2pHttpProxy = buildP2pHttpProxy(a);\n        DeletableContentAddressedStorage.HTTP http = new DeletableContentAddressedStorage.HTTP(ipfsApi, false, hasher);\n        ContentAddressedStorageProxy p2pGets = new ContentAddressedStorageProxy.HTTP(p2pHttpProxy);\n        List<PeerId> ourIds = ids.getIdentities();\n        if (ourIds.isEmpty())\n            ourIds = Collections.singletonList(new PeerId(http.id().join().bareMultihash().toBytes()));\n        MultiIdStorage ipfs = new MultiIdStorage(new LocalFirstStorage(http, http, p2pGets, ourIds, hasher), ourIds);\n        String linkHost = a.getOptionalArg(\"public-domain\").orElseGet(() -> \"localhost:\" + a.getInt(\"port\"));\n        if (useIPFS) {\n            if (useS3) {\n                // IPFS is already running separately, we can still use an S3BlockStorage\n                S3Config config = S3Config.build(a, Optional.empty());\n                BlockStoreProperties props = buildS3Properties(a);\n                TransactionalIpfs p2pBlockRetriever = new TransactionalIpfs(ipfs, transactions, authoriser, ipfs.id().join(), linkHost, hasher);\n\n                FileBlockCache cborCache = new FileBlockCache(a.fromPeergosDir(\"block-cache-dir\", \"block-cache\"), 1024 * 1024 * 1024L);\n                FileBlockBuffer blockBuffer = new FileBlockBuffer(a.fromPeergosDir(\"s3-block-buffer-dir\", \"block-buffer\"), usage);\n\n                p2pGets = buildP2PBlockRetrieverForS3(a, usage, hasher, p2pGets);\n                S3BlockStorage s3 = new S3BlockStorage(config, ipfs.ids().join(), props, linkHost, transactions, authoriser,\n                        bats, meta, usage, cborCache, blockBuffer,\n                        a.getLong(Main.GLOBAL_DOWNLOAD_BANDWIDTH_LIMIT.name),\n                        a.getLong(Main.GLOBAL_S3_READ_REQUESTS_LIMIT.name),\n                        a.getLong(Main.USER_DOWNLOAD_BANDWIDTH_LIMIT.name),\n                        a.getLong(Main.USER_S3_READ_REQUESTS_LIMIT.name),\n                        versionedS3,\n                        a.getPeergosDir(),\n                        partitionStatus,\n                        hasher, p2pBlockRetriever, p2pGets);\n                return new LocalIpnsStorage(s3, ids);\n            }\n            Multihash peerId = Multihash.decode(ourIds.get(ourIds.size() - 1).getBytes());\n            Cid ourId = new Cid(1, Cid.Codec.LibP2pKey, peerId.type, peerId.getHash());\n            FileContentAddressedStorage files = new FileContentAddressedStorage(blockstorePath(a), ourId,\n                    transactions, authoriser, partitionStatus, hasher);\n            MultiIdStorage blocks = new MultiIdStorage(new LocalFirstStorage(files, http, p2pGets, ourIds, hasher), ourIds);\n            if (enableGC) {\n                TransactionalIpfs txns = new TransactionalIpfs(blocks, transactions, authoriser, ourId, linkHost, hasher);\n                MetadataCachingStorage metabs = new MetadataCachingStorage(txns, meta, usage, hasher);\n                return new LocalIpnsStorage(metabs, ids);\n            } else {\n                AuthedStorage target = new AuthedStorage(blocks, authoriser, ourId, linkHost, hasher);\n                MetadataCachingStorage metabs = new MetadataCachingStorage(target, meta, usage, hasher);\n                return new LocalIpnsStorage(metabs, ids);\n            }\n        } else {\n            // In S3 mode of operation we require the ipfs id to be supplied as we don't have a local ipfs running\n            if (useS3) {\n                if (enableGC)\n                    throw new IllegalStateException(\"GC should be run separately when using S3!\");\n                TransactionalIpfs p2pBlockRetriever = new TransactionalIpfs(ipfs, transactions, authoriser, ipfs.id().join(), linkHost, hasher);\n                S3Config config = S3Config.build(a, Optional.empty());\n                BlockStoreProperties props = buildS3Properties(a);\n\n                FileBlockCache cborCache = new FileBlockCache(a.fromPeergosDir(\"block-cache-dir\", \"block-cache\"), 10 * 1024 * 1024 * 1024L);\n                FileBlockBuffer blockBuffer = new FileBlockBuffer(a.fromPeergosDir(\"s3-block-buffer-dir\", \"block-buffer\"), usage);\n                S3BlockStorage s3 = new S3BlockStorage(config, ipfs.ids().join(), props, linkHost, transactions, authoriser,\n                        bats, meta, usage, cborCache, blockBuffer,\n                        a.getLong(Main.GLOBAL_DOWNLOAD_BANDWIDTH_LIMIT.name),\n                        a.getLong(Main.GLOBAL_S3_READ_REQUESTS_LIMIT.name),\n                        a.getLong(Main.USER_DOWNLOAD_BANDWIDTH_LIMIT.name),\n                        a.getLong(Main.USER_S3_READ_REQUESTS_LIMIT.name),\n                        versionedS3,\n                        a.getPeergosDir(),\n                        partitionStatus,\n                        hasher, p2pBlockRetriever,\n                        new ContentAddressedStorageProxy.HTTP(p2pHttpProxy));\n                return new LocalIpnsStorage(s3, ids);\n            } else {\n                // only used for testing\n                Cid ourId = new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes()));\n                FileContentAddressedStorage fileBacked = new FileContentAddressedStorage(blockstorePath(a), ourId,\n                        transactions, authoriser, partitionStatus, hasher);\n                MetadataCachingStorage metabs = new MetadataCachingStorage(fileBacked, meta, usage, hasher);\n                return new LocalIpnsStorage(metabs, ids);\n            }\n        }\n    }\n\n    public static BlockRequestAuthoriser blockAuthoriser(Args a,\n                                                         BatCave batStore,\n                                                         Hasher hasher) {\n        Optional<BatWithId> instanceBat = a.getOptionalArg(\"instance-bat\").map(BatWithId::decode);\n        return (b, blockBats, s, auth) -> {\n            Optional<BlockAuth> blockAuth = auth.isEmpty() ?\n                    Optional.empty() :\n                    Optional.of(BlockAuth.fromString(auth));\n            return Futures.of(BlockRequestAuthoriser.allowRead(b, blockBats, s, blockAuth, batStore, instanceBat, hasher));\n        };\n    }\n\n    public static SqlSupplier getSqlCommands(Args a) {\n        boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n        return usePostgres ? new PostgresCommands() : new SqliteCommands();\n    }\n\n    public static TransactionStore buildTransactionStore(Args a, Supplier<Connection> transactionsDb) {\n        return JdbcTransactionStore.build(transactionsDb, getSqlCommands(a));\n    }\n\n    public static boolean isPaidInstance(Args a) {\n        return a.hasArg(\"quota-admin-address\");\n    }\n\n    public static QuotaAdmin buildSpaceQuotas(Args a,\n                                              DeletableContentAddressedStorage localDht,\n                                              Supplier<Connection> spaceDb,\n                                              Supplier<Connection> quotasDb,\n                                              boolean isPki,\n                                              boolean localhostApi) {\n        if (isPaidInstance(a))\n            return buildPaidQuotas(a);\n\n        SqlSupplier sqlCommands = getSqlCommands(a);\n        JdbcSpaceRequests spaceRequests = JdbcSpaceRequests.build(spaceDb, sqlCommands);\n        JdbcQuotas quotas = JdbcQuotas.build(quotasDb, sqlCommands);\n        if (a.hasArg(\"quotas-init-file\")) {\n            String quotaFile = a.getArg(\"quotas-init-file\");\n            Map<String, Long> quotaInit = UserQuotas.readUsernamesFromFile(PathUtil.get(quotaFile));\n            quotaInit.forEach(quotas::setQuota);\n        }\n        long defaultQuota = a.getLong(\"default-quota\");\n        long maxUsers = a.getLong(\"max-users\", isPki ? 1 : 0);\n        if (! localhostApi && maxUsers > 0)\n            LOG.warning(\"Anyone can signup to this instance because we are listening on non-localhost addresses and max-users > 0. Using signup tokens is more secure.\");\n        LOG.info(\"Using default user space quota of \" + defaultQuota);\n        return new UserQuotas(quotas, defaultQuota, maxUsers, spaceRequests, localDht, isPki);\n    }\n\n    public static QuotaAdmin buildPaidQuotas(Args a) {\n        JavaPoster poster = new JavaPoster(AddressUtil.getAddress(new MultiAddress(a.getArg(\"quota-admin-address\"))), true);\n        return new HttpQuotaAdmin(poster);\n    }\n\n    public static CoreNode buildPkiCorenode(MutablePointers mutable,\n                                            Account account,\n                                            BatCave batCave,\n                                            DeletableContentAddressedStorage dht,\n                                            Crypto crypto,\n                                            Args a) {\n        try {\n            PublicKeyHash peergosIdentity = PublicKeyHash.fromString(a.getArg(\"peergos.identity.hash\"));\n\n            String pkiSecretKeyfilePassword = a.getArg(\"pki.keyfile.password\");\n\n            PublicSigningKey pkiPublic =\n                    PublicSigningKey.fromByteArray(\n                            Files.readAllBytes(a.fromPeergosDir(\"pki.public.key.path\")));\n            SecretSigningKey pkiSecretKey = SecretSigningKey.fromCbor(CborObject.fromByteArray(\n                    PasswordProtected.decryptWithPassword(\n                            CborObject.fromByteArray(Files.readAllBytes(a.fromPeergosDir(\"pki.secret.key.path\"))),\n                            pkiSecretKeyfilePassword,\n                            crypto.hasher,\n                            crypto.symmetricProvider,\n                            crypto.random\n                    )));\n            SigningKeyPair pkiKeys = new SigningKeyPair(pkiPublic, pkiSecretKey);\n            PublicKeyHash pkiPublicHash = ContentAddressedStorage.hashKey(pkiKeys.publicSigningKey);\n            LOG.info(\"PKI key: \" + pkiPublicHash);\n            SigningPrivateKeyAndPublicHash pkiSigner = new SigningPrivateKeyAndPublicHash(pkiPublicHash, pkiSecretKey);\n\n            return new IpfsCoreNode(pkiSigner, a.getInt(\"max-daily-signups\"), dht, crypto, mutable,\n                    account, batCave, peergosIdentity);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static Multihash getPkiServerId(Args a) {\n        return Cid.decodePeerId(a.getArg(\"pki-node-id\"));\n    }\n\n    public static CoreNode buildCorenode(Args a,\n                                         DeletableContentAddressedStorage localStorage,\n                                         TransactionStore transactions,\n                                         JdbcIpnsAndSocial rawPointers,\n                                         MutablePointers localPointers,\n                                         MutablePointersProxy proxingMutable,\n                                         JdbcIpnsAndSocial localSocial,\n                                         UsageStore usageStore,\n                                         QuotaAdmin quotas,\n                                         JdbcAccount rawAccount,\n                                         BatCave bats,\n                                         Account account,\n                                         LinkRetrievalCounter linkCounts,\n                                         Crypto crypto) {\n        Multihash nodeId = localStorage.id().join();\n        PublicKeyHash peergosId = PublicKeyHash.fromString(a.getArg(\"peergos.identity.hash\"));\n        Multihash pkiServerId = getPkiServerId(a);\n        // build a mirroring proxying corenode, unless we are the pki node\n        boolean isPkiNode = nodeId.bareMultihash().equals(pkiServerId);\n        Optional<BatWithId> instanceBat = a.getOptionalArg(\"instance-bat\").map(BatWithId::decode);\n        if (isPkiNode)\n            return buildPkiCorenode(localPointers, account, bats, localStorage, crypto, a);\n        HTTPCoreNode toPki = new HTTPCoreNode(buildP2pHttpProxy(a), pkiServerId);\n        if (! a.getBoolean(\"mirror-pki\", true)) {\n            LOG.info(\"Not mirroring PKI\");\n            return toPki;\n        }\n        return new MirrorCoreNode(toPki, rawAccount, bats, account, proxingMutable,\n                        localStorage, rawPointers, localPointers, transactions, localSocial, usageStore, quotas, linkCounts, pkiServerId, peergosId,\n                        a.fromPeergosDir(\"pki-mirror-state-path\",\"pki-state.cbor\"), instanceBat,\n                a.getOptionalArg(\"unlisted-usernames\")\n                        .map(arg -> Arrays.asList(arg.split(\",\")))\n                        .orElse(Collections.emptyList()), crypto);\n    }\n\n    public static JdbcIpnsAndSocial buildRawPointers(Args a, Supplier<Connection> dbConnectionPool) {\n        return new JdbcIpnsAndSocial(dbConnectionPool, getSqlCommands(a));\n    }\n\n\n    public static CompletableFuture<NetworkAccess> buildJavaGatewayAccess(URL apiAddress, URL proxyAddress, String pkiNodeId) {\n        Multihash pkiServerNodeId = Cid.decode(pkiNodeId);\n        JavaPoster p2pPoster = new JavaPoster(proxyAddress, false);\n        JavaPoster apiPoster = new JavaPoster(apiAddress, false);\n        ScryptJava hasher = new ScryptJava();\n        return NetworkAccess.buildViaGateway(apiPoster, p2pPoster, pkiServerNodeId, 0, hasher, false);\n    }\n\n    public static CompletableFuture<NetworkAccess> buildJavaNetworkAccess(URL target,\n                                                                          boolean isPublicServer,\n                                                                          Optional<String> userAgent,\n                                                                          Optional<ProxySelector> proxy) {\n        return buildJavaNetworkAccess(target, isPublicServer, Optional.empty(), userAgent, proxy);\n    }\n\n    public static CompletableFuture<NetworkAccess> buildJavaNetworkAccess(URL target,\n                                                                          boolean isPublicServer,\n                                                                          Optional<String> basicAuth,\n                                                                          Optional<String> userAgent,\n                                                                          Optional<ProxySelector> proxy) {\n        return buildNonCachingJavaNetworkAccess(target, isPublicServer, 7_000, basicAuth, userAgent, proxy);\n    }\n\n    public static CompletableFuture<NetworkAccess> buildNonCachingJavaNetworkAccess(URL target,\n                                                                                    boolean isPublicServer,\n                                                                                    int mutableCacheTime,\n                                                                                    Optional<String> basicAuth,\n                                                                                    Optional<String> userAgent,\n                                                                                    Optional<ProxySelector> proxy) {\n        JavaPoster poster = new JavaPoster(target, isPublicServer, basicAuth, userAgent, proxy);\n        ScryptJava hasher = new ScryptJava();\n        ContentAddressedStorage localDht = NetworkAccess.buildLocalDht(poster, true, hasher);\n        return NetworkAccess.buildViaPeergosInstance(poster, poster, localDht, mutableCacheTime, hasher, false);\n    }\n\n    public static CompletableFuture<NetworkAccess> buildLocalJavaNetworkAccess(int targetPort) {\n        try {\n            return buildJavaNetworkAccess(new URL(\"http://localhost:\" + targetPort + \"/\"), false, Optional.empty(), Optional.empty(), Optional.empty());\n        } catch (MalformedURLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/Command.java",
    "content": "package peergos.server;\n\nimport peergos.server.util.Args;\nimport peergos.server.util.Logging;\n\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.Collectors;\n\npublic class Command<V> {\n    public static class Arg {\n        public final String name, description;\n        public final boolean isRequired;\n        public final Optional<String> defaultValue;\n\n        public Arg(String name, String description, boolean isRequired) {\n            this.name = name;\n            this.description = description;\n            this.isRequired = isRequired;\n            this.defaultValue = Optional.empty();\n        }\n\n        public Arg(String name, String description, boolean isRequired, String defaultValue) {\n            this.name = name;\n            this.description = description;\n            this.isRequired = isRequired;\n            this.defaultValue = Optional.of(defaultValue);\n        }\n    }\n\n    public final String name, description;\n    public final Function<Args, V> entryPoint;\n    public final List<Arg> params;// with description\n    public final Map<String, Command> subCommands;\n\n    public Command(String name, String description,\n                   Function<Args, V> entryPoint,\n                   List<Arg> params) {\n        this(name,description, entryPoint, params, Collections.emptyList());\n    }\n\n    public Command(String name, String description,\n                   Function<Args, V> entryPoint,\n                   List<Arg> params,\n                   List<Command> subCommands) {\n        this.name = name;\n        this.description = description;\n        this.entryPoint = entryPoint;\n        this.params = params;\n        this.subCommands = subCommands.stream().collect(Collectors.toMap(c -> c.name, c -> c, (a, b) -> b, LinkedHashMap::new));\n    }\n\n    public V main(Args args) {\n        for (Arg param : params) {\n            if (param.defaultValue.isPresent())\n                args = args.setIfAbsent(param.name, param.defaultValue.get());\n        }\n        Optional<String> headOpt = args.head();\n\n        if (headOpt.isPresent()) {\n            String head = headOpt.get();\n            if (subCommands.containsKey(head)) {\n                subCommands.get(head).main(args.tail());\n                return null;\n            }\n        }\n\n        if (args.hasArg(\"help\")) {\n            System.out.println(helpMessage());\n                return null;\n        }\n\n        ensureArgs(args);\n        Logging.init(args);\n        return entryPoint.apply(args);\n    }\n\n    private void ensureArgs(Args args) {\n        Optional<String> missing = params.stream()\n                .filter(p -> p.isRequired)\n                .map(e -> e.name)\n                .filter(e -> ! args.hasArg(e))\n                .findFirst();\n        if (missing.isPresent()) {\n            System.err.println(name + \" requires argument \" + missing.get() + \" run with -\" + missing.get() + \" $VALUE\");\n            System.exit(-1);\n        }\n    }\n\n    public String helpMessage() {\n        return name  +\": \"+ description +\n                System.lineSeparator()\n                + (params.size() > 0 ? \"Parameters: \" + System.lineSeparator() : \"\")\n                + params.stream()\n                        .map(e -> \"\\t\"+ e.name + \": \"+ e.description)\n                        .collect(Collectors.joining(System.lineSeparator()))\n                + (subCommands.size() > 0 ? System.lineSeparator() + \"Sub commands (run -help on a sub command to see more):\" + System.lineSeparator() : \"\")\n                + subCommands.entrySet().stream()\n                .reduce(\"\",\n                        (acc , e) -> acc + \"\\t\" + e.getKey() + \": \" + e.getValue().description + System.lineSeparator(),\n                        (a, b) -> a + b);\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/DesktopApp.java",
    "content": "package peergos.server;\n\nimport peergos.server.util.Args;\n\nimport java.awt.*;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Path;\n\npublic class DesktopApp {\n\n    public static void launch(Args args, int port, URI api) throws IOException {\n        boolean flatpak = args.hasArg(\"flatpak\");\n        boolean isLinux = \"linux\".equalsIgnoreCase(System.getProperty(\"os.name\"));\n        boolean isWindows = System.getProperty(\"os.name\").toLowerCase().startsWith(\"windows\");\n        boolean isMacOS = System.getProperty(\"os.name\").toLowerCase().startsWith(\"mac\");\n\n        try {\n            if (flatpak) {\n                ProcessBuilder pb = new ProcessBuilder(\n                        \"flatpak.sh\",\n                        Integer.toString(port)\n                );\n                pb.inheritIO();\n                Process p = pb.start();\n                p.onExit().thenAccept(done -> {\n                    System.exit(0);\n                });\n            } else if (isWindows) {\n                String edgePath = \"C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe\";\n                if (!new File(edgePath).exists()) {\n                    edgePath = \"C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe\";\n                }\n\n                ProcessBuilder pb = new ProcessBuilder(\n                        edgePath,\n                        \"--app=http://localhost:\" + port,\n                        \"--disable-extensions\",\n                        \"--user-data-dir=\" + System.getenv(\"APPDATA\") + \"\\\\Peergos\\\\edge-data\"\n                );\n\n                Process edgeProcess = pb.start();\n\n                edgeProcess.onExit().thenAccept(done -> {\n                    System.out.println(\"Edge closed, shutting down...\");\n                    System.exit(0);\n                });\n            } else if (isMacOS) {\n                Path jar = Path.of(Main.class.getProtectionDomain()\n                        .getCodeSource()\n                        .getLocation()\n                        .toURI());\n                Path binary = jar.getParent().resolve(\"PeergosWebView\");\n                ProcessBuilder pb = new ProcessBuilder(\n                        binary.toString()\n                );\n                // pass port via env var\n                pb.environment().put(\"PEERGOS_PORT\", \"\" + port);\n                pb.inheritIO();\n                Process webviewProcess = pb.start();\n\n                webviewProcess.onExit().thenAccept(done -> {\n                    System.out.println(\"Webview closed, shutting down...\");\n                    System.exit(0);\n                });\n            } else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {\n                Desktop.getDesktop().browse(api);\n            } else {\n                if (isLinux) // Fix Snap installer\n                    Runtime.getRuntime().exec(new String[] {\"xdg-open\", \"http://localhost:\" + port});\n                System.out.println(\"Please open http://localhost:\" + port + \" in your browser.\");\n            }\n        } catch (Throwable t) {\n            if (isLinux)\n                Runtime.getRuntime().exec(new String[] {\"xdg-open\", \"http://localhost:\" + port});\n            if (isWindows || isMacOS)\n                if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE))\n                    Desktop.getDesktop().browse(api);\n            t.printStackTrace();\n            System.out.println(\"Please open http://localhost:\" + port + \" in your browser.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/DirectOnlyStorage.java",
    "content": "package peergos.server;\n\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class DirectOnlyStorage extends DelegatingStorage {\n    private final ContentAddressedStorage target;\n\n    public DirectOnlyStorage(ContentAddressedStorage target) {\n        super(target);\n        this.target = target;\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new DirectOnlyStorage(target.directToOrigin());\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return Futures.of(new BlockStoreProperties(false, false, false, Optional.empty(), Optional.empty()));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/HostDirChooser.java",
    "content": "package peergos.server;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\npublic interface HostDirChooser {\n    CompletableFuture<String> chooseDir();\n\n    class Flatpak implements HostDirChooser {\n        @Override\n        public CompletableFuture<String> chooseDir() {\n            CompletableFuture<String> res = new CompletableFuture<>();\n\n            try {\n                ProcessBuilder pb = new ProcessBuilder(\n                        \"zenity\",\n                        \"--file-selection\",\n                        \"--directory\",\n                        \"--title=Select folder to sync with Peergos\"\n                );\n                Process p = pb.start();\n                BufferedReader r = new BufferedReader(\n                        new InputStreamReader(p.getInputStream())\n                );\n\n                String selectedDir = r.readLine();\n                p.waitFor();\n\n                if (selectedDir != null && !selectedDir.isEmpty()) {\n                    persistFlatpakPermission(selectedDir);\n                    res.complete(selectedDir);\n                }\n            } catch (Exception e) {\n                res.completeExceptionally(e);\n            }\n            return res;\n        }\n\n        private static void persistFlatpakPermission(String path) throws Exception {\n            // flatpak override must run on the host, not inside the sandbox.\n            // Requires --talk-name=org.freedesktop.Flatpak in the Flatpak manifest.\n            new ProcessBuilder(\n                    \"flatpak-spawn\", \"--host\",\n                    \"flatpak\", \"override\", \"--user\",\n                    \"--filesystem=\" + path,\n                    getAppId()\n            ).start().waitFor();\n        }\n\n        private static String getAppId() {\n            // FLATPAK_ID is set automatically by the Flatpak runtime inside the sandbox\n            String envId = System.getenv(\"FLATPAK_ID\");\n            if (envId != null && !envId.isEmpty())\n                return envId;\n            // Fallback: parse /.flatpak-info (INI format, [Application] section, name= key)\n            try {\n                List<String> lines = Files.readAllLines(Path.of(\"/.flatpak-info\"));\n                for (String line : lines) {\n                    if (line.startsWith(\"name=\"))\n                        return line.substring(\"name=\".length()).trim();\n                }\n            } catch (Exception ignored) {}\n            throw new IllegalStateException(\"Cannot determine Flatpak app ID\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/HostDirEnumerator.java",
    "content": "package peergos.server;\n\nimport peergos.shared.util.Futures;\n\nimport java.io.File;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic interface HostDirEnumerator {\n\n    CompletableFuture<List<String>> getHostDirs(String prefix, int maxDepth);\n\n    class Java implements HostDirEnumerator {\n        @Override\n        public CompletableFuture<List<String>> getHostDirs(String prefix, int mathDepthFromPrefix) {\n            Set<String> roots = Arrays.stream(File.listRoots())\n                    .map(f -> f.toPath().toString())\n                    .collect(Collectors.toSet());\n            List<String> res = new ArrayList<>();\n            boolean includeAll = prefix.equals(\"/\");\n            for (String root : roots) {\n                if (root.startsWith(prefix)) {\n                    File dir = new File(root);\n                    recurse(dir, res, mathDepthFromPrefix - Paths.get(root).getNameCount());\n                } else if (prefix.startsWith(root)) {\n                    File dir = new File(prefix);\n                    recurse(dir, res, mathDepthFromPrefix);\n                } else if (includeAll)\n                    recurse(new File(root), res, mathDepthFromPrefix - Paths.get(root).getNameCount());\n            }\n            return Futures.of(res);\n        }\n\n        private static Set<String> excludedPaths = Set.of(\n                \"/dev\",\n                \"/proc\",\n                \"/tmp\",\n                \"/run\",\n                \"/usr\",\n                \"/var\"\n        );\n\n        private void recurse(File dir, List<String> res, int maxDepth) {\n            if (maxDepth <= 0)\n                return;\n            File[] kids = dir.listFiles();\n            if (kids != null) {\n                for (File kid : kids) {\n                    if (kid.isDirectory() && ! kid.isHidden()) {\n                        String absolutePath = kid.getAbsolutePath();\n                        if (excludedPaths.contains(absolutePath))\n                            continue;\n                        if (kid.canWrite()) {\n                            res.add(absolutePath);\n                        }\n                        recurse(kid, res, maxDepth - 1);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/JavaCrypto.java",
    "content": "package peergos.server;\n\nimport peergos.server.crypto.asymmetric.curve25519.*;\nimport peergos.server.crypto.asymmetric.mlkem.JavaMlkem;\nimport peergos.server.crypto.hash.*;\nimport peergos.server.crypto.random.*;\nimport peergos.server.crypto.symmetric.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\n\npublic class JavaCrypto {\n\n    public static Crypto init() {\n        SafeRandomJava random = new SafeRandomJava();\n        Salsa20Poly1305Java symmetricProvider = new Salsa20Poly1305Java();\n        Ed25519Java signer = new Ed25519Java();\n        Curve25519 boxer = new Curve25519Java();\n        JavaMlkem javaMlkem = new JavaMlkem();\n        return Crypto.init(() -> new Crypto(random, new ScryptJava(), symmetricProvider, signer, boxer, javaMlkem));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/JdbcAddressLRU.java",
    "content": "package peergos.server;\n\nimport io.libp2p.core.AddressBook;\nimport io.libp2p.core.PeerId;\nimport io.libp2p.core.multiformats.Multiaddr;\nimport org.jetbrains.annotations.NotNull;\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.sql.SqliteCommands;\nimport peergos.server.util.Logging;\nimport peergos.server.util.Sqlite;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.corenode.PkiCache;\nimport peergos.shared.corenode.UserPublicKeyLink;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.util.Futures;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Supplier;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\n\npublic class JdbcAddressLRU implements AddressBook {\n    private static final Logger LOG = Logging.LOG();\n\n    private static final String SET = \"INSERT OR REPLACE INTO addressbook (peerid, addresses, lastaccess) VALUES(?, ?, current_timestamp)\";\n    private static final String GET = \"SELECT addresses FROM addressbook WHERE peerid = ?;\";\n    private static final String TOUCH = \"UPDATE addressbook SET lastaccess=current_timestamp WHERE peerid = ?;\";\n    private static final String COUNT = \"SELECT COUNT(*) FROM addressbook;\";\n    private static final String DELETE = \"DELETE FROM addressbook WHERE peerid IN \" +\n            \"(SELECT peerid FROM addressbook ORDER BY lastaccess ASC LIMIT ?);\";\n\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n    private final int maxSize;\n\n    public JdbcAddressLRU(int maxSize, Supplier<Connection> conn, SqlSupplier commands) {\n        this.maxSize = maxSize;\n        this.conn = conn;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(\"CREATE TABLE IF NOT EXISTS addressbook \" +\n                    \"(peerid text primary key not null, addresses text not null, lastaccess int not null); \" +\n                    \"CREATE UNIQUE INDEX IF NOT EXISTS addressbook_index ON addressbook (peerid);\" +\n                    \"CREATE INDEX IF NOT EXISTS addressbooklru_index ON addressbook (lastaccess);\", conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @NotNull\n    @Override\n    public CompletableFuture<Void> addAddrs(@NotNull PeerId peerId, long ttl, @NotNull Multiaddr... multiaddrs) {\n        Collection<Multiaddr> existing = getAddrs(peerId).join();\n        HashSet<Multiaddr> updated = new HashSet<>();\n        updated.addAll(existing);\n        for (Multiaddr addr : multiaddrs) {\n            updated.add(addr);\n        }\n        setAddrs(peerId, 0L, updated.toArray(Multiaddr[]::new));\n        return Futures.of(null);\n    }\n\n    @NotNull\n    @Override\n    public CompletableFuture<Collection<Multiaddr>> getAddrs(@NotNull PeerId peerId) {\n        try (Connection conn = getConnection();\n             PreparedStatement present = conn.prepareStatement(GET);\n             PreparedStatement touch = conn.prepareStatement(TOUCH)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            present.setString(1, peerId.toBase58());\n            ResultSet rs = present.executeQuery();\n            if (rs.next()) {\n                touch.setString(1, peerId.toBase58());\n                touch.executeUpdate();\n                return Futures.of(Arrays.stream(rs.getString(\"addresses\").split(\",\"))\n                        .map(Multiaddr::new)\n                        .toList());\n            } else\n                return Futures.of(Collections.emptySet());\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public int size() {\n        try (Connection conn = getConnection();\n             PreparedStatement present = conn.prepareStatement(COUNT)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            ResultSet rs = present.executeQuery();\n            return rs.getInt(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private void removeOldest(int toRemove) {\n        try (Connection conn = getConnection();\n             PreparedStatement delete = conn.prepareStatement(DELETE)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            delete.setInt(1, toRemove);\n            int changed = delete.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    @NotNull\n    @Override\n    public CompletableFuture<Void> setAddrs(@NotNull PeerId peerId, long ttl, @NotNull Multiaddr... multiaddrs) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(SET)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n\n            insert.setString(1, peerId.toBase58());\n            insert.setString(2, new String(Arrays.stream(multiaddrs)\n                    .map(a -> a.toString())\n                    .collect(Collectors.joining(\",\"))));\n            int changed = insert.executeUpdate();\n            int size = size();\n            if (size > maxSize) {\n                removeOldest(size - maxSize*8/10);\n            }\n            return Futures.of(null);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            return Futures.of(null);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n\n        isClosed = true;\n    }\n\n    public static JdbcAddressLRU buildSqlite(int maxSize, String db) {\n        try {\n            Connection file = Sqlite.build(db);\n            // We need a connection that ignores close\n            Connection instance = new Sqlite.UncloseableConnection(file);\n            return new JdbcAddressLRU(maxSize, () -> instance, new SqliteCommands());\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/JdbcPkiCache.java",
    "content": "package peergos.server;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.util.*;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class JdbcPkiCache implements PkiCache {\n    private static final Logger LOG = Logging.LOG();\n\n    private static final String CREATE = \"INSERT INTO pkistate (username, chain, pubkey) VALUES(?, ?, ?)\";\n    private static final String UPDATE = \"UPDATE pkistate SET chain=?, pubkey=? WHERE username = ?\";\n    private static final String GET_BY_USERNAME = \"SELECT * FROM pkistate WHERE username = ? LIMIT 1;\";\n    private static final String GET_BY_KEY = \"SELECT * FROM pkistate WHERE pubkey = ? LIMIT 1;\";\n\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n\n    public JdbcPkiCache(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(\"CREATE TABLE IF NOT EXISTS pkistate \" +\n                    \"(username text primary key not null, chain text not null, pubkey text not null); \" +\n                    \"CREATE UNIQUE INDEX IF NOT EXISTS pkistate_index ON pkistate (username);\", conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private boolean hasUser(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement present = conn.prepareStatement(GET_BY_USERNAME)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            present.setString(1, username);\n            ResultSet rs = present.executeQuery();\n            if (rs.next()) {\n                return true;\n            }\n            return false;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement present = conn.prepareStatement(GET_BY_USERNAME)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            present.setString(1, username);\n            ResultSet rs = present.executeQuery();\n            if (rs.next()) {\n                return Futures.of(((CborObject.CborList)CborObject.fromByteArray(rs.getBytes(\"chain\"))).map(UserPublicKeyLink::fromCbor));\n            }\n            throw new IllegalStateException(\"Unknown user \" + username);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public CompletableFuture<Boolean> setChain(String username, List<UserPublicKeyLink> chain) {\n        PublicKeyHash owner = chain.get(chain.size() - 1).owner;\n        if (hasUser(username)) {\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(UPDATE)) {\n                conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n\n                insert.setBytes(1, new CborObject.CborList(chain).serialize());\n                insert.setString(2, new String(Base64.getEncoder().encode(owner.serialize())));\n                insert.setString(3, username);\n                int changed = insert.executeUpdate();\n                return Futures.of(changed > 0);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return Futures.of(false);\n            }\n        } else {\n            try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(CREATE)) {\n                stmt.setString(1, username);\n                stmt.setBytes(2, new CborObject.CborList(chain).serialize());\n                stmt.setString(3, new String(Base64.getEncoder().encode(owner.serialize())));\n                stmt.executeUpdate();\n                return Futures.of(true);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return Futures.of(false);\n            }\n        }\n    }\n\n    public CompletableFuture<String> getUsername(PublicKeyHash identity) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_BY_KEY)) {\n            stmt.setString(1, new String(Base64.getEncoder().encode(identity.serialize())));\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return Futures.of(rs.getString(\"username\"));\n            }\n\n            throw new IllegalStateException(\"Unknown user identity key.\");\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n\n        isClosed = true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/JdbcRecordLRU.java",
    "content": "package peergos.server;\n\nimport io.ipfs.multibase.binary.Base32;\nimport io.ipfs.multihash.Multihash;\nimport org.peergos.protocol.dht.RecordStore;\nimport org.peergos.protocol.ipns.IpnsRecord;\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.sql.SqliteCommands;\nimport peergos.server.util.Logging;\nimport peergos.server.util.Sqlite;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.function.Supplier;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\npublic class JdbcRecordLRU implements RecordStore {\n    private static final Logger LOG = Logging.LOG();\n    private static final String RECORD_TABLE = \"records\";\n    private static final int SIZE_OF_VAL = 10 * 1024; // 10KiB\n    private static final int SIZE_OF_PEERID = 100;\n    private static final String SET = \"INSERT OR REPLACE INTO \" + RECORD_TABLE\n            + \" (peerId, raw, sequence, ttlNanos, expiryUTC, val, lastaccess) VALUES (?, ?, ?, ?, ?, ?, current_timestamp);\";\n    private static final String GET = \"SELECT raw, sequence, ttlNanos, expiryUTC, val FROM \" + RECORD_TABLE + \" WHERE peerId=?;\";\n    private static final String TOUCH = \"UPDATE \" + RECORD_TABLE + \" SET lastaccess=current_timestamp WHERE peerid = ?;\";\n    private static final String DELETE = \"DELETE FROM \" + RECORD_TABLE + \" WHERE peerId=?\";\n    private static final String DELETE_BULK = \"DELETE FROM \" + RECORD_TABLE + \" WHERE peerid IN \" +\n            \"(SELECT peerid FROM \" + RECORD_TABLE + \" ORDER BY lastaccess ASC LIMIT ?);\";\n    private static final String COUNT = \"SELECT COUNT(*) FROM \" + RECORD_TABLE + \";\";\n\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n    private final int maxSize;\n\n    public JdbcRecordLRU(int maxSize, Supplier<Connection> conn, SqlSupplier commands) {\n        this.maxSize = maxSize;\n        this.conn = conn;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(\"create table if not exists \" + RECORD_TABLE\n                    + \" (peerId VARCHAR(\" + SIZE_OF_PEERID + \") primary key not null, raw BLOB not null, \"\n                    + \"sequence BIGINT not null, ttlNanos BIGINT not null, expiryUTC BIGINT not null, \"\n                    + \"val VARCHAR(\" + SIZE_OF_VAL + \") not null, \"\n                    + \"lastaccess int not null);\", conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private String hashToKey(Multihash hash) {\n        String padded = new Base32().encodeAsString(hash.toBytes());\n        int padStart = padded.indexOf(\"=\");\n        return padStart > 0 ? padded.substring(0, padStart) : padded;\n    }\n\n    @Override\n    public void put(Multihash peerId, IpnsRecord record) {\n        try (Connection conn = getConnection();\n             PreparedStatement pstmt = conn.prepareStatement(SET)) {\n            pstmt.setString(1, hashToKey(peerId));\n            pstmt.setBytes(2, record.raw);\n            pstmt.setLong(3, record.sequence);\n            pstmt.setLong(4, record.ttlNanos);\n            pstmt.setLong(5, record.expiry.toEpochSecond(ZoneOffset.UTC));\n            pstmt.setString(6, new String(record.value.length > SIZE_OF_VAL ?\n                    Arrays.copyOfRange(record.value, 0, SIZE_OF_VAL) : record.value));\n            pstmt.executeUpdate();\n            int size = size();\n            if (size > maxSize) {\n                removeOldest(size - maxSize*8/10);\n            }\n        } catch (SQLException ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n\n    @Override\n    public Optional<IpnsRecord> get(Multihash peerId) {\n        try (Connection conn = getConnection();\n             PreparedStatement pstmt = conn.prepareStatement(GET);\n             PreparedStatement touch = conn.prepareStatement(TOUCH)) {\n            pstmt.setString(1, hashToKey(peerId));\n            touch.setString(1, hashToKey(peerId));\n            touch.executeUpdate();\n            try (ResultSet rs = pstmt.executeQuery()) {\n                if (rs.next()) {\n                    try (InputStream input = rs.getBinaryStream(\"raw\")) {\n                        byte[] buffer = new byte[1024];\n                        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                        for (int len; (len = input.read(buffer)) != -1; ) {\n                            bout.write(buffer, 0, len);\n                        }\n                        LocalDateTime expiry = LocalDateTime.ofEpochSecond(rs.getLong(\"expiryUTC\"),\n                                0, ZoneOffset.UTC);\n                        IpnsRecord record = new IpnsRecord(bout.toByteArray(), rs.getLong(\"sequence\"),\n                                rs.getLong(\"ttlNanos\"),  expiry, rs.getString(\"val\").getBytes());\n                        return Optional.of(record);\n                    } catch (IOException readEx) {\n                        throw new IllegalStateException(readEx);\n                    }\n                } else {\n                    return Optional.empty();\n                }\n            } catch (SQLException rsEx) {\n                throw new IllegalStateException(rsEx);\n            }\n        } catch (SQLException sqlEx) {\n            throw new IllegalStateException(sqlEx);\n        }\n    }\n\n    @Override\n    public void remove(Multihash peerId) {\n        try (Connection conn = getConnection();\n             PreparedStatement pstmt = conn.prepareStatement(DELETE)) {\n            pstmt.setString(1, hashToKey(peerId));\n            pstmt.executeUpdate();\n        } catch (SQLException ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n\n    public int size() {\n        try (Connection conn = getConnection();\n             PreparedStatement present = conn.prepareStatement(COUNT)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            ResultSet rs = present.executeQuery();\n            return rs.getInt(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private void removeOldest(int toRemove) {\n        try (Connection conn = getConnection();\n             PreparedStatement delete = conn.prepareStatement(DELETE_BULK)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            delete.setInt(1, toRemove);\n            int changed = delete.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n\n        isClosed = true;\n    }\n\n    public static JdbcRecordLRU buildSqlite(int maxSize, String db) {\n        try {\n            Connection file = Sqlite.build(db);\n            // We need a connection that ignores close\n            Connection instance = new Sqlite.UncloseableConnection(file);\n            return new JdbcRecordLRU(maxSize, () -> instance, new SqliteCommands());\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/LinkIdentity.java",
    "content": "package peergos.server;\n\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.regex.*;\n\npublic class LinkIdentity {\n\n    /** Link a peergos account to an account on an external service,\n     *  where you can post textual content up to 280 characters long.\n     *\n     * @param a\n     * @param network\n     * @param crypto\n     */\n    public static void link(Args a, NetworkAccess network, Crypto crypto) {\n        String username = a.getArg(\"username\");\n        Console console = System.console();\n        String password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n        UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).join();\n        String usernameB = a.getArg(\"service-username\");\n        String serviceB = a.getArg(\"service\");\n        if (! Pattern.compile(IdentityLink.KnownService.Peergos.usernameRegex).matcher(username).matches())\n            throw new IllegalStateException(\"Invalid username for Peergos\");\n        IdentityLink.IdentityService B = IdentityLink.IdentityService.parse(serviceB);\n        if (! Pattern.compile(B.usernameRegex()).matcher(usernameB).matches())\n            throw new IllegalStateException(\"Invalid username for \" + serviceB);\n\n        boolean encrypted = a.getBoolean(\"encrypted\");\n        boolean publish = ! encrypted && a.getBoolean(\"publish\", false);\n\n        IdentityLinkProof proof = IdentityLinkProof.buildAndSign(context.signer, username, usernameB, serviceB).join();\n        if (encrypted)\n            proof = proof.withKey(SymmetricKey.random());\n\n        uploadProof(proof, context, publish);\n\n        System.out.println(\"Successfully generated, signed and uploaded identity link.\");\n        System.out.println(\"Post the following text to the alternative service:\\n\");\n        FileWrapper proofFile = context.getByPath(PathUtil.get(username, \".profile\", \"ids\", proof.getFilename())).join().get();\n        boolean isLocalhost = a.getArg(\"peergos-url\").startsWith(\"http://localhost\");\n        String publicPeergosUrl = isLocalhost ? \"https://peergos.net\" : a.getArg(\"peergos-url\");\n        System.out.println(proof.postText(proof.getUrlToPost(publicPeergosUrl, proofFile, publish)));\n\n        String postUrl = console.readLine(\"\\nEnter the URL for the post on the alternative service:\");\n        proof = proof.withPostUrl(postUrl);\n        uploadProof(proof, context, publish);\n        System.out.println(\"Successfully linked to post on alternative service.\");\n    }\n\n    private static void uploadProof(IdentityLinkProof proof, UserContext context, boolean makePublic) {\n        Path subPath = PathUtil.get(\".profile\", \"ids\");\n        FileWrapper idsDir = context.getUserRoot().join().getOrMkdirs(subPath, context.network, true, context.mirrorBatId(), context.crypto).join();\n        String filename = proof.getFilename();\n\n        byte[] raw = proof.serialize();\n        idsDir.uploadOrReplaceFile(filename, AsyncReader.build(raw), raw.length, context.network, context.crypto, () -> false, x -> {}).join();\n\n        if (makePublic)\n            context.makePublic(context.getByPath(PathUtil.get(context.username).resolve(subPath).resolve(filename)).join().get()).join();\n    }\n\n    public static void verify(Args a, NetworkAccess network) {\n        String username = a.getArg(\"username\");\n        String usernameB = a.getArg(\"service-username\");\n        String serviceB = a.getArg(\"service\");\n        String sigb58 = a.getArg(\"signature\");\n        IdentityLink claim = new IdentityLink(username, IdentityLink.IdentityService.parse(\"Peergos\"),\n                usernameB, IdentityLink.IdentityService.parse(serviceB));\n        IdentityLinkProof proof = new IdentityLinkProof(claim, Base58.decode(sigb58), Optional.empty(), Optional.empty());\n        Optional<PublicKeyHash> idKeyHash = network.coreNode.getPublicKeyHash(username).join();\n        if (idKeyHash.isEmpty())\n            throw new IllegalStateException(\"Unknown user: \" + username);\n        Optional<PublicSigningKey> idKey = network.dhtClient.getSigningKey(idKeyHash.get(), idKeyHash.get()).join();\n        if (idKey.isEmpty())\n            throw new IllegalStateException(\"Couldn't retrieve key for \" + username);\n\n        proof.isValid(idKey.get()).join();\n        System.out.println(\"Identity link proof is correct - it was signed by the Peergos user \" + username);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/Login.java",
    "content": "package peergos.server;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.Optional;\n\npublic class Login {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-login\"), Optional.empty()).get();\n        String username = args[0];\n        Console console = System.console();\n        String password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n        UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).get();\n        System.out.println(\"Logged in \" + username + \" successfully!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/LoginUpdate.java",
    "content": "package peergos.server;\n\nimport peergos.server.util.Args;\nimport peergos.shared.Crypto;\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.corenode.OpLog;\nimport peergos.shared.crypto.BoxingKeyPair;\nimport peergos.shared.crypto.SigningKeyPair;\nimport peergos.shared.crypto.SigningPrivateKeyAndPublicHash;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.mutable.PointerUpdate;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.storage.TransactionId;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.AbsoluteCapability;\nimport peergos.shared.user.fs.WritableAbsoluteCapability;\nimport peergos.shared.util.ArrayOps;\nimport peergos.shared.util.Pair;\n\nimport java.net.URL;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class LoginUpdate {\n\n    public static void main(String[] a) throws Exception {\n        Args args = Args.parse(a);\n        Crypto crypto = JavaCrypto.init();\n        String signerHex = args.getArg(\"signer\");\n        SigningPrivateKeyAndPublicHash identity = SigningPrivateKeyAndPublicHash.fromCbor(CborObject.fromByteArray(ArrayOps.hexToBytes(signerHex)));\n        String homeCapLink = args.getArg(\"home-cap\");\n        AbsoluteCapability home = WritableAbsoluteCapability.fromLink(homeCapLink);\n        String boxerHex = args.getArg(\"boxer\");\n        BoxingKeyPair boxer = BoxingKeyPair.fromCbor(CborObject.fromByteArray(ArrayOps.hexToBytes(boxerHex)));\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-login\"), Optional.empty()).get();\n        String username = args.getArg(\"username\");\n        PublicKeyHash remoteId = network.coreNode.getPublicKeyHash(username).join().get();\n        PublicKeyHash idHash = identity.publicKeyHash;\n        if (! idHash.equals(remoteId))\n            throw new IllegalStateException(\"Supplied identity doesn't match remote! \" + idHash + \" != \" + remoteId);\n\n        PointerUpdate currentPointer = network.mutable.getPointerTarget(idHash, idHash, network.dhtClient).join();\n        WriterData wd = WriterData.getWriterData(idHash, idHash, network.mutable, network.dhtClient).join().props.get();\n\n        byte[] boxerSha256 = crypto.hasher.sha256(boxer.publicBoxingKey.serialize()).join();\n        byte[] signedBoxerSha256 = identity.secret.signMessage(boxerSha256).join();\n        PublicKeyHash boxerHash = network.dhtClient.putBoxingKey(idHash, signedBoxerSha256, boxer.publicBoxingKey, new TransactionId(\"12345\")).join();\n\n        SigningPrivateKeyAndPublicHash signer = new SigningPrivateKeyAndPublicHash(idHash, identity.secret);\n        SecretGenerationAlgorithm alg = wd.generationAlgorithm.get();\n        String password = new String(System.console().readPassword(\"Enter password: \"));\n        UserWithRoot loginAuth = UserUtil.generateUser(username, password, crypto, alg).join();\n        SymmetricKey rootKey = loginAuth.getRoot();\n        System.out.println(\"Setting login data\");\n        EntryPoint entryPoint = new EntryPoint(home, username);\n        PublicSigningKey idPub = network.dhtClient.getSigningKey(idHash, idHash).join().get();\n        SigningKeyPair idPair = new SigningKeyPair(idPub, identity.secret);\n        SigningKeyPair loginSigner = loginAuth.getUser();\n        UserStaticData entryPoints = new UserStaticData(List.of(entryPoint), rootKey, Optional.of(idPair), Optional.of(boxer));\n\n        if (! boxerHash.equals(wd.followRequestReceiver.get())) {\n            System.out.println(\"Supplied social keypair doesn't match remote: \" + boxerHash + \" != \" + wd.followRequestReceiver.get());\n            System.out.println(\"Updating to supplied social keypair and updating login data...\");\n            byte[] signedBoxerHash = identity.secret.signMessage(boxerSha256).join();\n            PublicKeyHash kh = network.dhtClient.putBoxingKey(identity.publicKeyHash, signedBoxerHash, boxer.publicBoxingKey, new TransactionId(\"12345\")).join();\n            WriterData updatedWd = wd.withBoxer(Optional.of(kh));\n            byte[] rawWd = updatedWd.serialize();\n            Cid blockHash = crypto.hasher.hash(rawWd, false).join();\n            byte[] signedHash = identity.secret.signMessage(blockHash.getHash()).join();\n            OpLog.BlockWrite blockWrite = new OpLog.BlockWrite(identity.publicKeyHash, signedHash, rawWd, false, Optional.empty());\n            PointerUpdate pointerCas = new PointerUpdate(currentPointer.updated, MaybeMultihash.of(blockHash), PointerUpdate.increment(currentPointer.sequence));\n            byte[] signedPointer = identity.secret.signMessage(pointerCas.serialize()).join();\n            OpLog.PointerWrite pointerWrite = new OpLog.PointerWrite(identity.publicKeyHash, signedPointer);\n            LoginData newLoginData = new LoginData(username, entryPoints, loginSigner.publicSigningKey, Optional.of(new Pair<>(blockWrite, pointerWrite)));\n            network.account.setLoginData(newLoginData, identity, false).join();\n        } else {\n            LoginData login = new LoginData(username, entryPoints, loginSigner.publicSigningKey, Optional.empty());\n            network.account.setLoginData(login, signer, false).join();\n        }\n        System.out.println(\"Completed update\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/LookupOwner.java",
    "content": "package peergos.server;\n\nimport peergos.server.storage.DelegatingDeletableStorage;\nimport peergos.server.storage.DeletableContentAddressedStorage;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.user.CommittedWriterData;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.WriterData;\n\nimport java.io.Console;\nimport java.net.URL;\nimport java.util.Optional;\nimport java.util.Set;\n\npublic class LookupOwner {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-login\"), Optional.empty()).get();\n        String user = network.coreNode.getUsername(PublicKeyHash.fromString(\"\")).join();\n        System.out.println(user);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/Main.java",
    "content": "package peergos.server;\n\nimport com.luciad.imageio.webp.WebPDecoderOptions;\nimport com.webauthn4j.data.client.*;\nimport io.libp2p.core.PeerId;\nimport io.libp2p.core.crypto.PrivKey;\nimport org.eclipse.jetty.server.Server;\nimport org.peergos.HostBuilder;\nimport org.peergos.RamAddressBook;\nimport org.scijava.nativelib.NativeLibraryUtil;\nimport peergos.server.cli.CLI;\nimport peergos.server.crypto.hash.ScryptJava;\nimport peergos.server.login.*;\nimport peergos.server.messages.*;\nimport peergos.server.net.ProxyChooser;\nimport peergos.server.net.SyncConfigHandler;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.storage.auth.*;\nimport peergos.server.sync.DirectorySync;\nimport peergos.server.sync.SyncConfig;\nimport peergos.server.sync.SyncRunner;\nimport peergos.shared.*;\nimport peergos.server.corenode.*;\nimport peergos.server.fuse.*;\nimport peergos.server.webdav.*;\nimport peergos.server.mutable.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.password.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.MultiAddress;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.login.OfflineAccountStore;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.List;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static peergos.server.net.SyncConfigHandler.SYNC_CONFIG_FILENAME;\n\npublic class Main extends Builder {\n    public static final String PEERGOS_PATH = \"PEERGOS_PATH\";\n    public static final Path DEFAULT_PEERGOS_DIR_PATH =\n            Paths.get(System.getProperty(\"user.home\"), \".peergos\");\n\n    public static final Command.Arg ARG_TRANSACTIONS_SQL_FILE =\n        new Command.Arg(\"transactions-sql-file\", \"The filename for the transactions datastore\", false, \"transactions.sql\");\n    public static final Command.Arg ARG_BAT_STORE =\n                    new Command.Arg(\"bat-store\", \"The filename for the BAT store (or :memory: for ram based)\", true, \"bats.sql\");\n    public static final Command.Arg ARG_PARTITIONED_STATUS =\n                    new Command.Arg(\"partition-status-file\", \"The filename for the partition status db\", true, \"partition-status.sql\");\n    public static final Command.Arg ARG_USE_IPFS =\n        new Command.Arg(\"useIPFS\", \"Use IPFS for storage or a local disk store if not\", false, \"true\");\n    public static final Command.Arg ARG_IPFS_API_ADDRESS =\n        new Command.Arg(\"ipfs-api-address\", \"IPFS API address\", true, \"/ip4/127.0.0.1/tcp/5001\");\n    public static final Command.Arg ARG_IPFS_PROXY_TARGET =\n        new Command.Arg(\"proxy-target\", \"Proxy target for p2p http requests\", false, \"/ip4/127.0.0.1/tcp/8003\");\n\n    public static final Command.Arg ARG_BOOTSTRAP_NODES = new Command.Arg(\"ipfs-config-bootstrap-node-list\",\n            \"Comma separated list of IPFS bootstrap nodes.\", false, IpfsWrapper.DEFAULT_BOOTSTRAP_LIST);\n\n    public static final Command.Arg ARG_ANNOUNCE_ADDRESSES = new Command.Arg(\"ipfs-announce-addresses\",\n            \"Comma separated list of extra announce multi-addresses. e.g. a public NAT address with port forwarding: /ip4/$IP/tcp/4001\", false);\n    public static final Command.Arg ARG_HTTP_PROXY = new Command.Arg(\"http_proxy\", \"Use a http proxy for all requests, format host:port\", false);\n    public static final Command.Arg ARG_SERVER_URL = new Command.Arg(\"server-url\", \"Address of the remote Peergos or self-hosted server to use in app/proxy mode\", false);\n\n    public static final Command.Arg LISTEN_HOST = new Command.Arg(\"listen-host\", \"The hostname/interface to listen on\", true, \"localhost\");\n    public static final Command.Arg QUOTA_UPLOAD_LIMIT_SECONDS = new Command.Arg(\"quota-upload-limit-seconds\", \"The minimum time period during which a user is allowed to upload their total quota, in seconds. Faster uploads will be rejected.\", false, \"86400\");\n    public static final Command.Arg GLOBAL_DOWNLOAD_BANDWIDTH_LIMIT = new Command.Arg(\"global-download-bandwidth-limit\", \"The maximum amount of data allowed to be downloaded from this server in bytes per second.\", false, \"1000000000\");\n    public static final Command.Arg USER_DOWNLOAD_BANDWIDTH_LIMIT = new Command.Arg(\"user-download-bandwidth-limit\", \"The maximum amount of data allowed to be downloaded per user in bytes per second.\", false, \"100000000\");\n    public static final Command.Arg GLOBAL_S3_READ_REQUESTS_LIMIT = new Command.Arg(\"global-s3-read-requests-limit\", \"The maximum number of S3 read requests allowed from this server per second.\", false, \"100000\");\n    public static final Command.Arg USER_S3_READ_REQUESTS_LIMIT = new Command.Arg(\"user-s3-read-requests-limit\", \"The maximum number of S3 read requests allowed from this server per user per second.\", false, \"1000\");\n    public static final Command.Arg VERSIONED_S3 = new Command.Arg(\"s3.versioned-bucket\", \"If the S3 bucket is versioned\", false, \"false\");\n\n    public static Command<IpfsWrapper> IPFS = new Command<>(\"ipfs\",\n            \"Configure and start IPFS daemon\",\n            Main::startIpfs,\n            Arrays.asList(\n                    new Command.Arg(\"IPFS_PATH\", \"Path to IPFS directory. Defaults to $PEERGOS_PATH/.ipfs, or ~/.peergos/.ipfs\", false),\n                    ARG_IPFS_API_ADDRESS,\n                    new Command.Arg(\"ipfs-gateway-address\", \"IPFS Gateway port\", false, \"/ip4/127.0.0.1/tcp/8080\"),\n                    new Command.Arg(\"ipfs-swarm-port\", \"IPFS Swarm port\", false, \"4001\"),\n                    new Command.Arg(\"ipfs-swarm-addrs\", \"IPFS Swarm addresses comma separated (overrides ipfs-swarm-port if present) e.g. /ip4/0.0.0.0/tcp/4001\", false),\n                    ARG_IPFS_PROXY_TARGET,\n                    ARG_BOOTSTRAP_NODES,\n                    ARG_ANNOUNCE_ADDRESSES,\n                    new Command.Arg(\"collect-metrics\", \"Export aggregated metrics\", false, \"false\"),\n                    new Command.Arg(\"metrics.address\", \"Listen address for serving aggregated metrics\", false, \"localhost\"),\n                    new Command.Arg(\"ipfs.metrics.port\", \"Port for serving aggregated ipfs metrics\", false, \"8101\"),\n                    new Command.Arg(\"s3.path\", \"Path of data store in S3\", false),\n                    new Command.Arg(\"s3.bucket\", \"S3 bucket name\", false),\n                    new Command.Arg(\"s3.region\", \"S3 region\", false),\n                    new Command.Arg(\"s3.accessKey\", \"S3 access key\", false),\n                    new Command.Arg(\"s3.secretKey\", \"S3 secret key\", false),\n                    new Command.Arg(\"s3.region.endpoint\", \"Base url for S3 service\", false),\n                    new Command.Arg(\"block-store-filter\", \"Indicate blockstore filter type. Can be 'none', 'bloom', 'infini'\", false),\n                    new Command.Arg(\"block-store-filter-false-positive-rate\", \"The false positive rate to apply to the block-store-filter. \", false),\n                    ARG_BAT_STORE,\n                    ServerIdentity.ARG_SERVERIDS_SQL_FILE\n                    )\n    );\n\n\n    public static final Command<ServerProcesses> PEERGOS = new Command<>(\"daemon\",\n            \"The user facing Peergos server\",\n            Main::startPeergos,\n            Stream.of(\n                    new Command.Arg(\"port\", \"service port\", false, \"8000\"),\n                    new Command.Arg(\"peergos.identity.hash\", \"The hash of peergos user's public key, this is used to bootstrap the pki\", true, \"z59vuwzfFDp3ZA8ZpnnmHEuMtyA1q34m3Th49DYXQVJntWpxdGrRqXi\"),\n                    new Command.Arg(\"pki-node-id\", \"Ipfs node id of the pki node\", true, \"QmVdFZgHnEgcedCS2G2ZNiEN59LuVrnRm7z3yXtEBv2XiF\"),\n                    new Command.Arg(\"pki.node.ipaddress\", \"IP address of the pki node\", true, \"172.104.157.121\"),\n                    ARG_IPFS_API_ADDRESS,\n                    new Command.Arg(\"ipfs-gateway-address\", \"IPFS Gateway address\", false, \"/ip4/127.0.0.1/tcp/8080\"),\n                    ARG_IPFS_PROXY_TARGET,\n                    ARG_BOOTSTRAP_NODES,\n                    ARG_ANNOUNCE_ADDRESSES,\n                    new Command.Arg(\"pki.node.swarm.port\", \"Swarm port of the pki node\", true, \"5001\"),\n                    LISTEN_HOST,\n                    new Command.Arg(\"public-domain\", \"The public domain name for this server (required if TLS is managed upstream)\", false),\n                    ARG_USE_IPFS,\n                    ARG_BAT_STORE,\n                    ARG_PARTITIONED_STATUS,\n                    new Command.Arg(\"allow-external-secret-links\", \"Allow external secret links to be served from this server\", false),\n                    new Command.Arg(\"allow-external-login\", \"Allow users from other servers to login through this server\", false),\n                    new Command.Arg(\"mutable-pointers-file\", \"The filename for the mutable pointers datastore\", true, \"mutable.sql\"),\n                    new Command.Arg(\"social-sql-file\", \"The filename for the follow requests datastore\", true, \"social.sql\"),\n                    new Command.Arg(\"space-requests-sql-file\", \"The filename for the space requests datastore\", true, \"space-requests.sql\"),\n                    new Command.Arg(\"account-sql-file\", \"The filename for the login datastore\", true, \"login.sql\"),\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\"),\n                    new Command.Arg(\"space-usage-sql-file\", \"The filename for the space usage datastore\", true, \"space-usage.sql\"),\n                    new Command.Arg(\"link-counts-sql-file\", \"The filename for the secret link counts datastore\", true, \"link-counts.sql\"),\n                    new Command.Arg(\"server-messages-sql-file\", \"The filename for the server messages datastore\", true, \"server-messages.sql\"),\n                    ARG_TRANSACTIONS_SQL_FILE,\n                    QUOTA_UPLOAD_LIMIT_SECONDS,\n                    GLOBAL_DOWNLOAD_BANDWIDTH_LIMIT,\n                    GLOBAL_S3_READ_REQUESTS_LIMIT,\n                    USER_DOWNLOAD_BANDWIDTH_LIMIT,\n                    USER_S3_READ_REQUESTS_LIMIT,\n                    VERSIONED_S3,\n                    ServerIdentity.ARG_SERVERIDS_SQL_FILE,\n                    new Command.Arg(\"enable-gc\", \"Enable the blockstore garbage collector\", false, \"true\"),\n                    new Command.Arg(\"gc.period.millis\", \"Garbage collect frequency in millis (default 12h)\", false, \"43200000\"),\n                    new Command.Arg(\"webroot\", \"the path to the directory to serve as the web root\", false),\n                    new Command.Arg(\"default-quota\", \"default maximum storage per user\", false, Long.toString(1024L * 1024 * 1024)),\n                    new Command.Arg(\"admin-usernames\", \"A comma separated list of usernames who can approve local space requests\", false),\n                    new Command.Arg(\"mirror.node.id\", \"Mirror a server's data locally\", false),\n                    new Command.Arg(\"mirror.username\", \"Mirror a user's data locally\", false),\n                    new Command.Arg(\"mirror.bat\", \"BatWithId to enable mirroring a user's private data\", false),\n                    new Command.Arg(\"login-keypair\", \"The keypair used to mirror the login data for a user (use with 'mirror.username' arg)\", false),\n                    new Command.Arg(\"public-server\", \"Are we a public server? (allow http GETs to API)\", false, \"false\"),\n                    new Command.Arg(\"run-gateway\", \"Run a local Peergos gateway\", false),\n                    new Command.Arg(\"gateway-port\", \"Port to run a local gateway on\", false, \"9000\"),\n                    new Command.Arg(\"unlisted-usernames\", \"List of usernames removed from the auto-complete\", false),\n                    new Command.Arg(\"app-dev-target\", \"URL for app assets for localhost app development\", false),\n                    new Command.Arg(\"collect-metrics\", \"Export aggregated metrics\", false, \"false\"),\n                    new Command.Arg(\"metrics.address\", \"Listen address for serving aggregated metrics\", false, \"localhost\"),\n                    new Command.Arg(\"metrics.port\", \"Port for serving aggregated metrics\", false, \"8001\"),\n                    new Command.Arg(\"ipfs.metrics.port\", \"Port for serving aggregated ipfs metrics\", false)\n            ).collect(Collectors.toList())\n    );\n\n    private static Args bootstrap(Args args) {\n        try {\n            // This means creating a pki keypair and publishing the public key\n            Crypto crypto = initCrypto();\n            // setup peergos user and pki keys\n            String peergosPassword = args.getArg(\"peergos.password\");\n            String pkiUsername = \"peergos\";\n            UserWithRoot peergos = UserUtil.generateUser(pkiUsername, peergosPassword, crypto, SecretGenerationAlgorithm.getDefaultWithoutExtraSalt()).get();\n\n            boolean useIPFS = args.getBoolean(\"useIPFS\");\n            ContentAddressedStorage dht = useIPFS ?\n                    new ContentAddressedStorage.HTTP(Builder.buildIpfsApi(args), false, crypto.hasher) :\n                    new FileContentAddressedStorage(blockstorePath(args), new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                            JdbcTransactionStore.build(getDBConnector(args, \"transactions-sql-file\"), new SqliteCommands()),\n                            (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n\n            SigningKeyPair peergosIdentityKeys = peergos.getUser();\n            PublicKeyHash peergosPublicHash = ContentAddressedStorage.hashKey(peergosIdentityKeys.publicSigningKey);\n\n            String pkiPassword = args.getArg(\"pki.keygen.password\");\n\n            if (peergosPassword.equals(pkiPassword))\n                throw new IllegalStateException(\"Pki password and peergos password must be different!!\");\n            SigningKeyPair pkiKeys = UserUtil.generateUser(pkiUsername, pkiPassword, crypto, SecretGenerationAlgorithm.getDefaultWithoutExtraSalt()).get().getUser();\n            IpfsTransaction.call(peergosPublicHash,\n                    tid -> dht.putSigningKey(peergosIdentityKeys.secretSigningKey.signMessage(\n                            pkiKeys.publicSigningKey.serialize()).join(),\n                            peergosPublicHash,\n                            pkiKeys.publicSigningKey, tid), dht).get();\n\n            String pkiKeyfilePassword = args.getArg(\"pki.keyfile.password\");\n            Cborable cipherTextCbor = PasswordProtected.encryptWithPassword(pkiKeys.secretSigningKey.toCbor().toByteArray(),\n                    pkiKeyfilePassword,\n                    crypto.hasher,\n                    crypto.symmetricProvider,\n                    crypto.random);\n            Files.write(args.fromPeergosDir(\"pki.secret.key.path\"), cipherTextCbor.serialize());\n            Files.write(args.fromPeergosDir(\"pki.public.key.path\"), pkiKeys.publicSigningKey.toCbor().toByteArray());\n            return args.setIfAbsent(\"peergos.identity.hash\", peergosPublicHash.toString());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static final void poststrap(Args args) {\n        try {\n            // The final step of bootstrapping a new peergos network, which must be run once after network bootstrap\n            // This means signing up the peergos user, and adding the pki public key to the peergos user\n            Crypto crypto = initCrypto();\n            // recreate peergos user and pki keys\n            String password = args.getArg(\"peergos.password\");\n            String pkiUsername = \"peergos\";\n\n            PublicSigningKey pkiPublic =\n                    PublicSigningKey.fromByteArray(\n                            Files.readAllBytes(args.fromPeergosDir(\"pki.public.key.path\")));\n            PublicKeyHash pkiPublicHash = ContentAddressedStorage.hashKey(pkiPublic);\n            int webPort = args.getInt(\"port\");\n            Optional<String> basicAuth = args.getOptionalArg(\"basic-auth\")\n                    .map(a -> \"Basic \" + Base64.getEncoder().encodeToString(a.getBytes()));\n            NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + webPort),\n                    false, basicAuth, Optional.empty(), Optional.empty()).get();\n            String pkiFilePassword = args.getArg(\"pki.keyfile.password\");\n            SecretSigningKey pkiSecret =\n                    SecretSigningKey.fromCbor(CborObject.fromByteArray(PasswordProtected.decryptWithPassword(\n                            CborObject.fromByteArray(Files.readAllBytes(args.fromPeergosDir(\"pki.secret.key.path\"))),\n                            pkiFilePassword, crypto.hasher, crypto.symmetricProvider, crypto.random)));\n\n            // sign up peergos user\n            SecretGenerationAlgorithm algorithm = SecretGenerationAlgorithm.getDefaultWithoutExtraSalt();\n            LocalDate expiry = LocalDate.now().plusMonths(2);\n            UserContext context = UserContext.signUpGeneral(pkiUsername, password, \"\", Optional.empty(),\n                    id -> {}, Optional.empty(), expiry, network, crypto, algorithm, x -> {}).join();\n            Optional<PublicKeyHash> existingPkiKey = context.getNamedKey(\"pki\").get();\n            if (!existingPkiKey.isPresent() || existingPkiKey.get().equals(pkiPublicHash)) {\n                SigningPrivateKeyAndPublicHash pkiKeyPair = new SigningPrivateKeyAndPublicHash(pkiPublicHash, pkiSecret);\n\n                // write pki public key to ipfs\n                IpfsTransaction.call(context.signer.publicKeyHash,\n                        tid -> network.dhtClient.putSigningKey(context.signer.secret\n                                .signMessage(pkiPublic.serialize()).join(), context.signer.publicKeyHash, pkiPublic, tid),\n                        network.dhtClient).join();\n                context.addNamedOwnedKeyAndCommit(\"pki\", pkiKeyPair).join();\n            }\n            System.out.println(\"Peergos user identity hash: \" + context.signer.publicKeyHash);\n            // Create /peergos/releases and make it public\n            Optional<FileWrapper> releaseDir = context.getByPath(PathUtil.get(pkiUsername, \"releases\")).join();\n            if (! releaseDir.isPresent()) {\n                context.getUserRoot().join().mkdir(\"releases\", network, false,\n                        Optional.empty(), crypto).join();\n                FileWrapper releases = context.getByPath(PathUtil.get(pkiUsername, \"releases\")).join().get();\n                context.makePublic(releases).join();\n            }\n\n            // Create /peergos/app-gallery and make it public\n            String appGalleryFolderName = \"recommended-apps\";\n            Optional<FileWrapper> appGalleryDir = context.getByPath(PathUtil.get(pkiUsername, appGalleryFolderName)).join();\n            if (! appGalleryDir.isPresent()) {\n                context.getUserRoot().join().mkdir(appGalleryFolderName, network, false,\n                        Optional.empty(), crypto).join();\n                FileWrapper appGalleryFolder = context.getByPath(PathUtil.get(pkiUsername, appGalleryFolderName)).join().get();\n                String contents = \"<html></html>\";\n                byte[] data = contents.getBytes();\n                appGalleryFolder = appGalleryFolder.uploadFileJS(\"index.html\", new AsyncReader.ArrayBacked(data), 0,data.length, false,\n                        appGalleryFolder.mirrorBatId(), network, crypto, l -> {}, context.getTransactionService(), f -> Futures.of(false)).join();\n                context.makePublic(appGalleryFolder).join();\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n            System.exit(1);\n        }\n    }\n\n    public static final Command<ServerProcesses> PKI_INIT = new Command<>(\"pki-init\",\n            \"Bootstrap and start the Peergos PKI Server\",\n            args -> {\n                try {\n                    Crypto crypto = initCrypto();\n                    PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n\n                    IpfsWrapper ipfs = null;\n                    boolean useIPFS = args.getBoolean(\"useIPFS\");\n                    if (useIPFS) {\n                        ipfs = startIpfs(args);\n                    }\n\n                    args = bootstrap(args);\n\n                    SqlSupplier sqlCommands = getSqlCommands(args);\n                    BatCave batStore = new JdbcBatCave(getDBConnector(args, \"bat-store\"), sqlCommands);\n                    BlockRequestAuthoriser blockRequestAuthoriser = Builder.blockAuthoriser(args, batStore, crypto.hasher);\n                    PartitionStatus partitionStatus = new JdbcPartitionStatus(getDBConnector(args, \"partition-status-file\"), sqlCommands);\n                    Multihash pkiIpfsNodeId = useIPFS ?\n                            new ContentAddressedStorage.HTTP(Builder.buildIpfsApi(args), false, crypto.hasher).id().join() :\n                            new FileContentAddressedStorage(blockstorePath(args), new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                                    JdbcTransactionStore.build(getDBConnector(args, \"transactions-sql-file\"), new SqliteCommands()),\n                                    blockRequestAuthoriser, partitionStatus, crypto.hasher).id().get();\n\n                    if (ipfs != null)\n                        ipfs.stop();\n                    args = args.setIfAbsent(\"pki-node-id\", pkiIpfsNodeId.toString());\n                    if (useIPFS) {\n                        boolean saveConfigFile = !args.hasArg(\"ipfs.identity.peerid\");\n                        args = args.setArg(\"ipfs.identity.peerid\", ipfs.ipfsConfigParams.identity.get().peerId.toBase58());\n                        args = args.setArg(\"ipfs.identity.priv-key\", Base64.getEncoder().encodeToString(ipfs.ipfsConfigParams.identity.get().privKeyProtobuf));\n                        if (saveConfigFile) {\n                            args.saveToFile();\n                        }\n                    }\n\n                    ServerProcesses daemon = PEERGOS.main(args.with(\"partition-blockstore\", \"false\"));\n                    poststrap(args);\n                    return daemon;\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            },\n            Arrays.asList(\n                    new Command.Arg(\"listen-host\", \"The hostname/interface to listen on\", true, \"localhost\"),\n                    new Command.Arg(\"port\", \"The port for the local non tls server to listen on\", true, \"8000\"),\n                    new Command.Arg(\"useIPFS\", \"Whether to use IPFS or a local datastore\", true, \"false\"),\n                    new Command.Arg(\"legacy-raw-blocks-file\", \"The filename for the list of legacy raw blocks (or :memory: for ram based)\", true, \"legacyraw.sql\"),\n                    new Command.Arg(\"bat-store\", \"The filename for the BAT store (or :memory: for ram based)\", true, \"bats.sql\"),\n                    new Command.Arg(\"mutable-pointers-file\", \"The filename for the mutable pointers (or :memory: for ram based)\", true, \"mutable.sql\"),\n                    new Command.Arg(\"social-sql-file\", \"The filename for the follow requests (or :memory: for ram based)\", true, \"social.sql\"),\n                    new Command.Arg(\"transactions-sql-file\", \"The filename for the open transactions datastore\", true, \"transactions.sql\"),\n                    ServerIdentity.ARG_SERVERIDS_SQL_FILE,\n                    new Command.Arg(\"space-requests-sql-file\", \"The filename for the space requests datastore\", true, \"space-requests.sql\"),\n                    new Command.Arg(\"account-sql-file\", \"The filename for the login datastore\", true, \"login.sql\"),\n                    new Command.Arg(\"space-usage-sql-file\", \"The filename for the space usage datastore\", true, \"space-usage.sql\"),\n                    new Command.Arg(\"link-counts-sql-file\", \"The filename for the secret link counts datastore\", true, \"link-counts.sql\"),\n                    new Command.Arg(\"ipfs-api-address\", \"ipfs api port\", true, \"/ip4/127.0.0.1/tcp/5001\"),\n                    new Command.Arg(\"ipfs-gateway-address\", \"ipfs gateway port\", true, \"/ip4/127.0.0.1/tcp/8080\"),\n                    ARG_PARTITIONED_STATUS,\n                    ARG_IPFS_PROXY_TARGET,\n                    new Command.Arg(\"pki.secret.key.path\", \"The path to the pki secret key file\", true, \"test.pki.secret.key\"),\n                    new Command.Arg(\"pki.public.key.path\", \"The path to the pki public key file\", true, \"test.pki.public.key\"),\n                    // Secret parameters\n                    new Command.Arg(\"peergos.password\", \"The password for the 'peergos' user\", true),\n                    new Command.Arg(\"pki.keygen.password\", \"The password to generate the pki key from\", true),\n                    new Command.Arg(\"pki.keyfile.password\", \"The password protecting the pki keyfile\", true)\n            )\n    );\n\n    public static final Command<Boolean> STOP = new Command<>(\"stop\",\n            \"Stop any running Peergos instance\",\n            args -> {\n                try {\n                    int port = args.getInt(\"port\");\n                    new JavaPoster(new URL(\"http://localhost:\" + port), false)\n                            .post(Constants.STOP, new byte[0], false).join();\n                    return true;\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            },\n            Arrays.asList()\n    );\n\n    public static final Command<ServerProcesses> PKI = new Command<>(\"pki\",\n            \"Start the Peergos PKI Server that has already been bootstrapped\",\n            args -> {\n                try {\n                    Crypto crypto = initCrypto();\n                    PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n\n                    IpfsWrapper ipfs = null;\n                    boolean useIPFS = args.getBoolean(\"useIPFS\");\n                    if (useIPFS) {\n                        ipfs = startIpfs(args);\n                    }\n\n                    Supplier<Connection> transactionDb = getDBConnector(args, \"transactions-sql-file\");\n                    SqliteCommands sqlCommands = new SqliteCommands();\n                    JdbcTransactionStore transactions = JdbcTransactionStore.build(transactionDb, sqlCommands);\n                    BatCave batStore = new JdbcBatCave(getDBConnector(args, \"bat-store\", transactionDb), sqlCommands);\n                    BlockRequestAuthoriser authoriser = Builder.blockAuthoriser(args, batStore, crypto.hasher);\n                    PartitionStatus partitionStatus = new JdbcPartitionStatus(getDBConnector(args, \"partition-status-file\", transactionDb), sqlCommands);\n\n                    if (S3Config.useS3(args))\n                        throw new IllegalStateException(\"S3 not supported for PKI!\");\n                    ContentAddressedStorage storage = useIPFS ?\n                            new ContentAddressedStorage.HTTP(Builder.buildIpfsApi(args), false, crypto.hasher) :\n                            new FileContentAddressedStorage(blockstorePath(args), new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                                    transactions, authoriser, partitionStatus, crypto.hasher);\n                    Multihash pkiIpfsNodeId = storage.id().get();\n\n                    if (ipfs != null)\n                        ipfs.stop();\n                    args = args.setIfAbsent(\"pki-node-id\", pkiIpfsNodeId.toString());\n                    return PEERGOS.main(args);\n                } catch (Exception e) {\n                    throw new RuntimeException(e.getMessage(), e);\n                }\n            },\n            Arrays.asList(\n                    new Command.Arg(\"peergos.identity.hash\", \"The identity of the 'peergos' user which hosts the pki\", true),\n                    LISTEN_HOST,\n                    new Command.Arg(\"port\", \"The port for the local non tls server to listen on\", true, \"8000\"),\n                    new Command.Arg(\"useIPFS\", \"Whether to use IPFS or a local datastore\", true, \"false\"),\n                    new Command.Arg(\"legacy-raw-blocks-file\", \"The filename for the list of legacy raw blocks (or :memory: for ram based)\", true, \"legacyraw.sql\"),\n                    new Command.Arg(\"bat-store\", \"The filename for the BAT store (or :memory: for ram based)\", true, \"bats.sql\"),\n                    new Command.Arg(\"mutable-pointers-file\", \"The filename for the mutable pointers (or :memory: for ram based)\", true, \"mutable.sql\"),\n                    new Command.Arg(\"social-sql-file\", \"The filename for the follow requests (or :memory: for ram based)\", true, \"social.sql\"),\n                    new Command.Arg(\"transactions-sql-file\", \"The filename for the open transactions datastore\", true, \"transactions.sql\"),\n                    ServerIdentity.ARG_SERVERIDS_SQL_FILE,\n                    new Command.Arg(\"space-requests-sql-file\", \"The filename for the space requests datastore\", true, \"space-requests.sql\"),\n                    new Command.Arg(\"space-usage-sql-file\", \"The filename for the space usage datastore\", true, \"space-usage.sql\"),\n                    new Command.Arg(\"link-counts-sql-file\", \"The filename for the secret link counts datastore\", true, \"link-counts.sql\"),\n                    ARG_IPFS_API_ADDRESS,\n                    new Command.Arg(\"ipfs-gateway-address\", \"ipfs gateway port\", true, \"/ip4/127.0.0.1/tcp/8080\"),\n                    ARG_PARTITIONED_STATUS,\n                    ARG_IPFS_PROXY_TARGET,\n                    new Command.Arg(\"pki.secret.key.path\", \"The path to the pki secret key file\", true, \"test.pki.secret.key\"),\n                    new Command.Arg(\"pki.public.key.path\", \"The path to the pki public key file\", true, \"test.pki.public.key\"),\n                    // Secret parameters\n                    new Command.Arg(\"pki.keyfile.password\", \"The password protecting the pki keyfile\", true)\n            )\n    );\n\n    public static final Command<FuseProcess> FUSE = new Command<>(\"fuse\",\n            \"Mount a Peergos user's filesystem natively\\n\"+\n                    \"            Password can be set via an environment variable.\",\n            Main::startFuse,\n            Stream.of(\n                    new Command.Arg(\"username\", \"Peergos username\", true),\n                    new Command.Arg(\"PEERGOS_PASSWORD\", \"Peergos password\", true),\n                    new Command.Arg(\"peergos-url\", \"Peergos service address\", false, \"https://peergos.net\"),\n                    new Command.Arg(\"mountPoint\", \"The directory to mount the Peergos filesystem in\", true, \"peergos\")\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Server> WEBDAV = new Command<>(\"webdav\",\n            \"Provide a webdav bridge to a Peergos user's filesystem\\n\" +\n                    \"            Passwords can be set via environment variables.\",\n            WebdavServer::start,\n            Stream.of(\n                    new Command.Arg(\"username\", \"Peergos username\", true),\n                    new Command.Arg(\"PEERGOS_PASSWORD\", \"Peergos password\", true),\n                    new Command.Arg(\"webdav.username\", \"Webdav username\", true),\n                    new Command.Arg(\"PEERGOS_WEBDAV_PASSWORD\", \"Webdav password\", true),\n                    new Command.Arg(\"webdav.authorization.scheme\", \"The auth scheme used in the HTTP Authorization request header. Options are: basic or digest\", false, \"digest\"),\n                    new Command.Arg(\"webdav.port\", \"The listen port for the webdav endpoint\", false, \"8090\"),\n                    new Command.Arg(\"peergos-url\", \"Peergos service address\", false, \"https://peergos.net\")\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> SYNC_DIR = new Command<>(\"dir\",\n            \"Sync a local folder to and from a Peergos folder\",\n            DirectorySync::syncDir,\n            Stream.of(\n                    new Command.Arg(\"links\", \"Writable links (path only) to Peergos directories (comma separated)\", true),\n                    new Command.Arg(\"peergos-url\", \"Peergos service address\", false, \"https://peergos.net\"),\n                    new Command.Arg(\"local-dirs\", \"The directories to sync to and from Peergos (comma separated)\", true),\n                    new Command.Arg(\"max-parallelism\", \"The maximum parallelism to download files with\", false, \"32\"),\n                    new Command.Arg(\"block-cache-size-bytes\", \"The size of the local block cache, e.g. 5g\", false, \"1g\"),\n                    new Command.Arg(\"run-once\", \"Only sync the directory once\", false),\n                    ARG_HTTP_PROXY\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> SYNC_INIT = new Command<>(\"init\",\n            \"Setup a peergos folder for syncing and get required arguments for 'sync dir' command\",\n            DirectorySync::init,\n            Stream.of(\n                    new Command.Arg(\"peergos-url\", \"Peergos service address\", false, \"https://peergos.net\"),\n                    ARG_HTTP_PROXY\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> SYNC = new Command<>(\"sync\",\n            \"Sync a local folder to a Peergos folder\",\n            args -> {\n                System.out.println(\"Run sync init first, then sync dir with the resulting arguments\");\n                return null;\n            },\n            Collections.emptyList(),\n            Arrays.asList(SYNC_INIT, SYNC_DIR)\n    );\n\n    public static final Command<InstanceAdmin.VersionInfo> VERSION = new Command<>(\"version\",\n            \"Print the Peergos version\",\n            a -> {\n                InstanceAdmin.VersionInfo version = new InstanceAdmin.VersionInfo(UserService.CURRENT_VERSION, Admin.getSourceVersion());\n                System.out.println(\"Version: \" + version);\n                return version;\n            },\n            Collections.emptyList()\n    );\n\n    public static final Command<PublicGateway> GATEWAY = new Command<>(\"gateway\",\n            \"Serve websites directly from Peergos\",\n            Main::startGateway,\n            Stream.of(\n                    new Command.Arg(\"port\", \"service port\", false, \"9000\"),\n                    new Command.Arg(\"peergos-url\", \"Address of the Peergos server to connect to\", false, \"http://localhost:8000\"),\n                    new Command.Arg(\"domain-suffix\", \"Domain suffix to accept\", false, \".peergos.localhost:9000\"),\n                    new Command.Arg(\"listen-host\", \"Domain name to bind to\", false, \"localhost\"),\n                    new Command.Arg(\"public-server\", \"Are we a public server? (allow http GETs to API)\", false, \"false\"),\n                    new Command.Arg(\"collect-metrics\", \"Export aggregated metrics\", false, \"false\"),\n                    new Command.Arg(\"metrics.address\", \"Listen address for serving aggregated metrics\", false, \"localhost\"),\n                    new Command.Arg(\"metrics.port\", \"Port for serving aggregated metrics\", false, \"8001\")\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> SHELL = new Command<>(\"shell\",\n            \"An interactive command-line-interface to a Peergos server.\",\n            Main::startShell,\n            Stream.of(\n                    new Command.Arg(\"username\", \"Peergos username\", false),\n                    new Command.Arg(\"PEERGOS_PASSWORD\", \"Peergos password\", false),\n                    new Command.Arg(\"peergos-url\", \"Address of the Peergos server\", false),\n                    ARG_HTTP_PROXY\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> MIGRATE = new Command<>(\"migrate\",\n            \"Move a Peergos account to this server.\",\n            Main::migrate,\n            Stream.of(\n                      new Command.Arg(\"peergos-url\", \"Address of the Peergos server to migrate to\", false, \"http://localhost:8000\")\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> HOME = new Command<>(\"update-home-server-id\",\n            \"Update the home server id for a user\",\n            a -> {\n                try {\n                    Crypto crypto = Main.initCrypto();\n                    NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(a.getArg(\"peergos-url\")),\n                            true, Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-home\"), Optional.empty()).join();\n                    String username = a.getArg(\"username\");\n                    Console console = System.console();\n                    String password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n                    UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).join();\n                    boolean updated = context.ensureCurrentHost().join();\n                    if (updated)\n                        System.out.println(\"Updated home server id successfully!\");\n                    else\n                        System.out.println(\"Home server id already uptodate!\");\n                    return true;\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            },\n            Stream.of(\n                    new Command.Arg(\"peergos-url\", \"Address of the user's home server\", false, \"https://peergos.net\"),\n                    new Command.Arg(\"username\", \"Your Peergos username\", true)\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> LINK_IDENTITY = new Command<>(\"link\",\n            \"Link your Peergos identity to an account on another service.\",\n            a -> {\n                try {\n                    Crypto crypto = Main.initCrypto();\n                    String peergosUrl = a.getArg(\"peergos-url\");\n                    URL api = new URL(peergosUrl);\n                    NetworkAccess network = Builder.buildJavaNetworkAccess(api, peergosUrl.startsWith(\"https\"), Optional.empty(), Optional.empty()).join();\n                    LinkIdentity.link(a, network, crypto);\n                    return true;\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            },\n            Stream.of(\n                      new Command.Arg(\"peergos-url\", \"Address of the Peergos server\", false, \"http://localhost:8000\"),\n                      new Command.Arg(\"username\", \"Your Peergos username\", true),\n                      new Command.Arg(\"service\", \"The other service, e.g. Twitter\", true),\n                      new Command.Arg(\"service-username\", \"Your username on the other service\", true),\n                      new Command.Arg(\"publish\", \"Whether the identity proof file should be made public\", false, \"false\"),\n                      new Command.Arg(\"encrypted\", \"Whether the identity proof should be private\", false, \"false\")\n            ).collect(Collectors.toList())\n    );\n\n    public static final Command<Boolean> VERIFY_IDENTITY = new Command<>(\"verify\",\n            \"Verify an identity link post from another service.\",\n            a -> {\n                try {\n                    Main.initCrypto();\n                    String peergosUrl = a.getArg(\"peergos-url\");\n                    URL api = new URL(peergosUrl);\n                    NetworkAccess network = Builder.buildJavaNetworkAccess(api, peergosUrl.startsWith(\"https\"), Optional.empty(), Optional.empty()).join();\n                    LinkIdentity.verify(a, network);\n                    return true;\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            },\n            Stream.of(\n                      new Command.Arg(\"peergos-url\", \"Address of the Peergos server\", false, \"http://localhost:8000\"),\n                      new Command.Arg(\"username\", \"Your Peergos username\", true),\n                      new Command.Arg(\"service\", \"The other service, e.g. Twitter\", true),\n                      new Command.Arg(\"service-username\", \"Your username on the other service\", true),\n                      new Command.Arg(\"signature\", \"The signature of the link included in the post\", true)\n                              ).collect(Collectors.toList())\n    );\n\n    public static final Command<Void> IDENTITY = new Command<>(\"identity\",\n            \"Create or verify an identity proof\",\n            args -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Collections.emptyList(),\n            Arrays.asList(\n                    LINK_IDENTITY,\n                    VERIFY_IDENTITY\n            )\n    );\n\n    public static final Command<Void> PROXY = new Command<>(\"proxy\",\n            \"Run a local proxy to a peergos server. \\n\" +\n                    \"            This allows you to get the security and caching benefits of localhost, \\n\" +\n                    \"            without running a daemon which exposes your IP address. \",\n            a -> {\n                try {\n                    Crypto crypto = JavaCrypto.init();\n                    PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n                    JvmThumbnailer.initJava();\n                    URL target = new URL(getAppServerUrl(a));\n                    Optional<ProxySelector> proxy = ProxyChooser.build(a);\n                    if (proxy.isPresent())\n                        System.out.println(\"Using http proxy \" + proxy.get());\n                    JavaPoster poster = new JavaPoster(target,\n                            ! isLoopbackHost(target.getHost()),\n                            Optional.empty(),\n                            Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-proxy\"),\n                            proxy);\n                    ScryptJava hasher = new ScryptJava();\n                    ContentAddressedStorage localDht = NetworkAccess.buildLocalDht(poster, true, hasher);\n                    CoreNode core = NetworkAccess.buildDirectCorenode(poster);\n                    ContentAddressedStorage s3 = NetworkAccess.buildDirectS3Blockstore(localDht, core, poster, true, hasher).join();\n                    MutablePointersProxy httpMutable = new HttpMutablePointers(poster, poster);\n                    Account account = new HttpAccount(poster, poster);\n\n                    SocialNetworkProxy httpSocial = new HttpSocialNetwork(poster, poster);\n                    SpaceUsageProxy httpUsage = new HttpSpaceUsage(poster, poster);\n                    ServerMessager serverMessager = new ServerMessager.HTTP(poster);\n                    BatCave batCave = new HttpBatCave(poster, poster);\n                    HttpInstanceAdmin admin = new LocalVersionInstanceAdmin(poster);\n\n                    FileBlockCache blockCache = new FileBlockCache(a.getPeergosDir().resolve(Paths.get(\"blocks\", \"cache\")),\n                            10*1024*1024*1024L);\n                    ContentAddressedStorage locallyCachedStorage = new UnauthedCachingStorage(s3, blockCache, crypto.hasher);\n                    DirectOnlyStorage withoutS3 = new DirectOnlyStorage(locallyCachedStorage);\n\n                    Supplier<Connection> dbConnector = Builder.getDBConnector(a, \"mutable-pointers-cache\");\n                    JdbcIpnsAndSocial rawPointers = Builder.buildRawPointers(a, dbConnector);\n                    OnlineState online = new OnlineState(() -> Futures.of(true));\n                    OfflinePointerCache pointerCache = new OfflinePointerCache(httpMutable, new JdbcPointerCache(rawPointers, locallyCachedStorage), online);\n\n                    SqlSupplier commands = Builder.getSqlCommands(a);\n                    OfflineCorenode offlineCorenode = new OfflineCorenode(core, new JdbcPkiCache(Builder.getDBConnector(a, \"pki-cache-sql-file\", dbConnector), commands), online);\n\n                    int port = a.getInt(\"port\");\n                    Origin origin = new Origin(\"http://localhost:\" + port);\n                    JdbcAccount localAccount = new JdbcAccount(Builder.getDBConnector(a, \"account-cache-sql-file\", dbConnector), commands, origin, \"localhost\");\n                    OfflineAccountStore offlineAccounts = new OfflineAccountStore(account, localAccount, online);\n\n                    OfflineBatCache offlineBats = new OfflineBatCache(batCave, new JdbcBatCave(Builder.getDBConnector(a, \"bat-cache-sql-file\", dbConnector), commands));\n\n                    Path peergosDir = a.getPeergosDir();\n                    Path jsonSyncConfig = peergosDir.resolve(SYNC_CONFIG_FILENAME);\n                    SyncConfig syncConfig = Files.exists(jsonSyncConfig) ?\n                            SyncConfig.fromJson((Map<String, Object>) JSONParser.parse(Files.readString(jsonSyncConfig))) :\n                            SyncConfig.fromArgs(a);\n\n                    SyncRunner.ThreadBased syncer = new SyncRunner.ThreadBased(a, withoutS3, pointerCache, offlineCorenode, crypto);\n                    boolean flatpak = a.hasArg(\"flatpak\");\n                    Either<HostDirEnumerator, HostDirChooser> syncDirChooser = flatpak ?\n                            Either.b(new HostDirChooser.Flatpak()) :\n                            Either.a(new HostDirEnumerator.Java());\n\n                    SyncProperties sync = new SyncProperties(syncConfig, peergosDir, syncer, syncDirChooser);\n                    UserService server = new UserService(withoutS3, offlineBats, crypto, offlineCorenode, offlineAccounts,\n                            httpSocial, pointerCache, admin, httpUsage, serverMessager, null, Optional.of(sync),\n                            Optional.of(new UserService.LocalAppProperties(peergosDir, getAppServerUrl(a))));\n\n                    InetSocketAddress localAPIAddress = new InetSocketAddress(\"localhost\", port);\n                    List<String> appSubdomains = Arrays.asList(\"markup-viewer,calendar,code-editor,pdf\".split(\",\"));\n                    int connectionBacklog = 50;\n                    int handlerPoolSize = 4;\n                    server.initAndStart(localAPIAddress, Arrays.asList(), Optional.empty(), Optional.empty(),\n                            Collections.emptyList(), Collections.emptyList(), appSubdomains, true,\n                            Optional.empty(), Optional.empty(), Optional.empty(), true, false,\n                            connectionBacklog, handlerPoolSize);\n                    return null;\n                } catch (IOException e) {\n                    throw new IllegalStateException(e);\n                }\n            },\n            Arrays.asList(\n                    ARG_SERVER_URL,\n                    new Command.Arg(\"peergos-url\", \"Address of the Peergos server\", false, \"https://peergos.net\"),\n                    ARG_HTTP_PROXY,\n                    new Command.Arg(\"port\", \"Localhost server port\", true, \"7777\"),\n                    new Command.Arg(\"mutable-pointers-cache\", \"The filename for the mutable pointers cache\", true, \"pointer-cache.sqlite\"),\n                    new Command.Arg(\"account-cache-sql-file\", \"The filename for the account cache\", true, \"account-cache.sqlite\"),\n                    new Command.Arg(\"pki-cache-sql-file\", \"The filename for the pki cache\", true, \"pki-cache.sqlite\"),\n                    new Command.Arg(\"bat-cache-sql-file\", \"The filename for the bat cache\", true, \"bat-cache.sqlite\")\n            ),\n            Collections.emptyList()\n    );\n\n    public static boolean isLanIP(String host) {\n        try {\n            if (host.contains(\":\"))\n                host = host.substring(0, host.indexOf(\":\"));\n            InetAddress ip = InetAddress.getByName(host);\n            return ip.isSiteLocalAddress();\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n    private static boolean isLoopbackHost(String host) {\n        if (host == null || host.isEmpty())\n            return false;\n        if (\"localhost\".equalsIgnoreCase(host) || \"127.0.0.1\".equals(host) || \"::1\".equals(host) || \"[::1]\".equals(host))\n            return true;\n        return false;\n    }\n\n    private static String getAppServerUrl(Args args) {\n        String serverUrl = args.getArg(ARG_SERVER_URL.name, args.getArg(\"peergos-url\", null));\n        if (serverUrl == null)\n            serverUrl = readSavedServerUrl(args.getPeergosDir()).orElse(\"https://peergos.net\");\n        try {\n            URL target = new URL(serverUrl);\n            boolean secureLoopback = \"http\".equalsIgnoreCase(target.getProtocol()) && isLoopbackHost(target.getHost());\n            if (! \"https\".equalsIgnoreCase(target.getProtocol()) && ! secureLoopback)\n                throw new IllegalStateException(\"Warning: desktop/proxy mode should use https, or http only for a loopback self-hosted server: \" + serverUrl);\n            return serverUrl;\n        } catch (MalformedURLException e) {\n            throw new IllegalStateException(\"Invalid server-url: \" + serverUrl, e);\n        }\n    }\n\n    private static Optional<String> readSavedServerUrl(Path peergosDir) {\n        try {\n            Path configFile = peergosDir.resolve(\"config\");\n            if (! Files.exists(configFile))\n                return Optional.empty();\n            for (String line : Files.readAllLines(configFile)) {\n                String trimmed = line.trim();\n                if (trimmed.startsWith(\"server-url\") || trimmed.startsWith(\"peergos-url\")) {\n                    String[] parts = trimmed.split(\"=\", 2);\n                    if (parts.length == 2 && ! parts[1].trim().isEmpty())\n                        return Optional.of(parts[1].trim());\n                }\n            }\n        } catch (IOException e) {\n            // Config unreadable — fall through to default\n        }\n        return Optional.empty();\n    }\n\n    private static PublicKeyHash getPkiKey(PublicKeyHash pkiOwnerIdentity,\n                                           Multihash pkiPeerId,\n                                           MutablePointers mutable,\n                                           Function<Cid, Cborable> blockGet,\n                                           Hasher hasher) {\n        Optional<byte[]> pointer = mutable.getPointer(pkiOwnerIdentity, pkiOwnerIdentity).join();\n        byte[] pkiIdPointer = pointer.get();\n        PointerUpdate fresh = MutablePointers.parsePointerTarget(pkiIdPointer, pkiOwnerIdentity, pkiOwnerIdentity, new RAMStorage(hasher)).join();\n        MaybeMultihash newPeergosRoot = fresh.updated;\n\n        CommittedWriterData currentPeergosWd = new CommittedWriterData(MaybeMultihash.of(newPeergosRoot.get()),\n                WriterData.fromCbor(blockGet.apply((Cid)newPeergosRoot.get())), fresh.sequence);\n        return currentPeergosWd.props.get().namedOwnedKeys.get(\"pki\").ownedKey;\n    }\n\n    public static ServerProcesses startPeergos(Args a) {\n        try {\n            Crypto crypto = initCrypto();\n            PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n            JvmThumbnailer.initJava();\n            Hasher hasher = crypto.hasher;\n            PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n\n            System.out.println(\"Starting Peergos daemon version: \" + new InstanceAdmin.VersionInfo(UserService.CURRENT_VERSION, Admin.getSourceVersion()));\n\n            SqlSupplier sqlCommands = getSqlCommands(a);\n\n            boolean useIPFS = a.getBoolean(\"useIPFS\");\n            Supplier<Connection> dbConnectionPool = getDBConnector(a, \"transactions-sql-file\");\n            JdbcBatCave batStore = new JdbcBatCave(getDBConnector(a, \"bat-store\", dbConnectionPool), sqlCommands);\n            BlockRequestAuthoriser blockAuth = blockAuthoriser(a, batStore, hasher);\n            BlockMetadataStore meta = buildBlockMetadata(a);\n            JdbcServerIdentityStore ids = JdbcServerIdentityStore.build(getDBConnector(a, \"serverids-file\", dbConnectionPool), sqlCommands, crypto);\n            IpfsWrapper ipfsWrapper = useIPFS ? IpfsWrapper.launch(a, blockAuth, meta, ids) : null;\n            if (ids.getIdentities().isEmpty()) {\n                // initialise id db with our current peerid and sign an ipns record\n                HostBuilder builder = new HostBuilder(new RamAddressBook()).generateIdentity();\n                PrivKey peerPrivate = builder.getPrivateKey();\n                byte[] signedRecord = ServerIdentity.generateSignedIpnsRecord(peerPrivate, Optional.empty(), false, 1);\n                ids.addIdentity(PeerId.fromPubKey(peerPrivate.publicKey()), signedRecord);\n            }\n\n            boolean doExportAggregatedMetrics = a.getBoolean(\"collect-metrics\");\n            if (doExportAggregatedMetrics) {\n                int exporterPort = a.getInt(\"metrics.port\");\n                String exporterAddress = a.getArg(\"metrics.address\");\n                AggregatedMetrics.startExporter(exporterAddress, exporterPort);\n            }\n            MultiAddress localP2PApi = new MultiAddress(a.getArg(\"proxy-target\"));\n\n            Multihash pkiServerNodeId = getPkiServerId(a);\n            String listeningHost = a.getArg(LISTEN_HOST.name);\n            int webPort = a.getInt(\"port\");\n            InetSocketAddress userAPIAddress = new InetSocketAddress(listeningHost, webPort);\n            boolean localhostApi = userAPIAddress.getHostName().equals(\"localhost\");\n            if (! localhostApi)\n                System.out.println(\"Warning: listening on non localhost address: \" + listeningHost);\n\n            JavaPoster p2pHttpProxy = buildP2pHttpProxy(a);\n\n            TransactionStore transactions = buildTransactionStore(a, dbConnectionPool);\n\n            Supplier<Connection> usageDb = getDBConnector(a, \"space-usage-sql-file\", dbConnectionPool);\n            UsageStore usageStore = new JdbcUsageStore(usageDb, sqlCommands);\n            Supplier<Connection> statusDb = Main.getDBConnector(a, \"partition-status-file\");\n            PartitionStatus partitionStatus = new JdbcPartitionStatus(statusDb, sqlCommands);\n            JdbcIpnsAndSocial rawPointers = buildRawPointers(a,\n                    getDBConnector(a, \"mutable-pointers-file\", dbConnectionPool));\n            DeletableContentAddressedStorage localStorageForLinks = buildLocalStorage(a, meta, batStore, transactions, blockAuth,\n                    ids, usageStore, rawPointers, partitionStatus, crypto.hasher);\n\n            MutablePointers localPointers = UserRepository.build(localStorageForLinks, rawPointers, hasher);\n            MutablePointersProxy proxingMutable = new HttpMutablePointers(p2pHttpProxy, pkiServerNodeId);\n            LinkRetrievalCounter linkCounts = new JdbcLinkRetrievalcounter(getDBConnector(a, \"link-counts-sql-file\", dbConnectionPool), sqlCommands);\n\n            List<Cid> nodeIds = localStorageForLinks.ids().get();\n            Logging.LOG().info(\"Our peerids: \" + nodeIds);\n\n            JdbcIpnsAndSocial rawSocial = new JdbcIpnsAndSocial(getDBConnector(a, \"social-sql-file\", dbConnectionPool), sqlCommands);\n            HttpSpaceUsage httpSpaceUsage = new HttpSpaceUsage(p2pHttpProxy, p2pHttpProxy);\n\n            Optional<String> tlsHostname = a.hasArg(\"tls.keyfile.password\") ? Optional.of(listeningHost) : Optional.empty();\n            Optional<String> publicHostname = tlsHostname.isPresent() ? tlsHostname : a.getOptionalArg(\"public-domain\");\n            Origin origin = new Origin(publicHostname.map(host -> (isLanIP(host) ? \"http://\" : \"https://\") + host).orElse(\"http://localhost:\" + webPort));\n            String rpId = publicHostname.orElse(\"localhost\");\n            JdbcAccount rawAccount = new JdbcAccount(getDBConnector(a, \"account-sql-file\", dbConnectionPool), sqlCommands, origin, rpId);\n            Account account = new AccountWithStorage(localStorageForLinks, localPointers, rawAccount);\n            AccountProxy accountProxy = new HttpAccount(p2pHttpProxy, pkiServerNodeId);\n\n            boolean isPki = nodeIds.stream()\n                    .map(Cid::bareMultihash)\n                    .anyMatch(c -> c.equals(pkiServerNodeId.bareMultihash()));\n            QuotaAdmin userQuotas = buildSpaceQuotas(a, localStorageForLinks,\n                    getDBConnector(a, \"space-requests-sql-file\", dbConnectionPool),\n                    getDBConnector(a, \"quotas-sql-file\", dbConnectionPool), isPki, localhostApi);\n\n            boolean allowNonLocalLinks = a.getBoolean(\"allow-external-secret-links\", \"localhost\".equals(listeningHost));\n            DeletableContentAddressedStorage localStorage = new SecretLinkStorage(localStorageForLinks, localPointers, linkCounts, allowNonLocalLinks, userQuotas, batStore, hasher);\n\n            CoreNode core = buildCorenode(a, localStorage, transactions, rawPointers, localPointers, proxingMutable,\n                    rawSocial, usageStore, userQuotas, rawAccount, batStore, account, linkCounts, crypto);\n            localStorage.setPki(core);\n            userQuotas.setPki(core);\n\n            boolean enableGC = a.getBoolean(\"enable-gc\", false) && partitionStatus.isDone();\n            GarbageCollector gc = null;\n            if (enableGC) {\n                boolean useS3 = S3Config.useS3(a);\n                boolean listRawBlocks = useS3 && a.getBoolean(VERSIONED_S3.name);\n                gc = new GarbageCollector(localStorageForLinks, rawPointers, usageStore, a.fromPeergosDir(\"\", \"\"), (cd, rd, c) -> Futures.of(true), listRawBlocks);\n                Function<Stream<Map.Entry<PublicKeyHash, byte[]>>, CompletableFuture<Boolean>> snapshotSaver = s -> Futures.of(true);\n                int gcInterval = 12 * 60 * 60 * 1000;\n                gc.start(a.getInt(\"gc.period.millis\", gcInterval), snapshotSaver);\n            }\n\n            boolean mirrorUsers = a.getBoolean(\"mirror-users\", true);\n            if (a.hasArg(\"mirror.username\") || isPki) // mirror pki before starting user mirror\n                core.initialize(mirrorUsers);\n            else\n                new Thread(() -> core.initialize(mirrorUsers)).start();\n            if (a.getBoolean(\"partition-blockstore\", true)) {\n                ContentAddressedStorageProxy p2pGets = new ContentAddressedStorageProxy.HTTP(p2pHttpProxy);\n                PublicKeyHash pkiOwnerIdentity = PublicKeyHash.fromString(a.getArg(\"peergos.identity.hash\"));\n                PublicKeyHash pkiKey;\n                if (isPki) {\n                    PublicSigningKey pkiPublic =\n                            PublicSigningKey.fromByteArray(\n                                    Files.readAllBytes(a.fromPeergosDir(\"pki.public.key.path\")));\n                    pkiKey = ContentAddressedStorage.hashKey(pkiPublic);\n                } else {\n                    pkiKey = getPkiKey(pkiOwnerIdentity, pkiServerNodeId, proxingMutable,\n                            cid -> p2pGets.get(pkiServerNodeId, pkiOwnerIdentity, cid, Optional.empty()).join().get(), hasher);\n                }\n                localStorage.partitionByUser(usageStore, rawPointers, pkiKey);\n            }\n\n            CoreNode signupFilter = new SignUpFilter(core, userQuotas, nodeIds.get(nodeIds.size() - 1), httpSpaceUsage, hasher,\n                    a.getInt(\"max-daily-paid-signups\", isPaidInstance(a) ? 10 : 0), isPki);\n\n            if (a.getBoolean(\"update-usage\", true))\n                new Thread(() -> SpaceCheckingKeyFilter.update(usageStore, userQuotas, core, localPointers, localStorage, hasher)).start();\n            SpaceCheckingKeyFilter spaceChecker = new SpaceCheckingKeyFilter(core, localPointers, localStorage,\n                    hasher, userQuotas, usageStore, a.getLong(QUOTA_UPLOAD_LIMIT_SECONDS.name, 3600));\n            CorenodeEventPropagator corePropagator = new CorenodeEventPropagator(signupFilter);\n            corePropagator.addListener(spaceChecker::accept);\n            MutableEventPropagator localMutable = new MutableEventPropagator(localPointers);\n            localMutable.addListener(spaceChecker::accept);\n\n            int blockCacheSize = a.getInt(\"max-cached-blocks\", 1000);\n            int maxCachedBlockSize = a.getInt(\"max-cached-block-size\", 50 * 1024);\n            ContentAddressedStorage filteringDht = new WriteFilter(localStorage, spaceChecker::allowWrite);\n            ContentAddressedStorageProxy proxingDht = new ContentAddressedStorageProxy.HTTP(p2pHttpProxy);\n            LRUCache<PublicKeyHash, Boolean> nonLocal = new LRUCache<>(100);\n            ContentAddressedStorage p2pDht = new ContentAddressedStorage.Proxying(filteringDht, proxingDht, nodeIds,\n                    core, allowNonLocalLinks, owner -> {\n                synchronized (nonLocal) {\n                    if (nonLocal.containsKey(owner))\n                        return true;\n                }\n                boolean isLocal = userQuotas.getQuota(core.getUsername(owner).join()) > 0;\n                if (! isLocal) {\n                    synchronized (nonLocal) {\n                        nonLocal.put(owner, true);\n                    }\n                }\n                return isLocal;\n            });\n\n            Path blacklistPath = a.fromPeergosDir(\"blacklist_file\", \"blacklist.txt\");\n            PublicKeyBlackList blacklist = new UserBasedBlacklist(blacklistPath, core, localMutable, localStorage, hasher);\n            MutablePointers blockingMutablePointers = new BlockingMutablePointers(localMutable, blacklist);\n            MutablePointers p2mMutable = new ProxyingMutablePointers(nodeIds, core, blockingMutablePointers, proxingMutable);\n\n            SocialNetworkProxy httpSocial = new HttpSocialNetwork(p2pHttpProxy, p2pHttpProxy);\n\n            SocialNetwork local = UserRepository.build(localStorage, rawSocial, hasher);\n            SocialNetwork p2pSocial = new ProxyingSocialNetwork(nodeIds, core, local, httpSocial);\n\n            Set<String> adminUsernames = Arrays.asList(a.getArg(\"admin-usernames\", \"\").split(\",\"))\n                    .stream()\n                    .filter(n -> ! n.isEmpty())\n                    .collect(Collectors.toSet());\n            boolean enableWaitlist = a.getBoolean(\"enable-wait-list\", false);\n            Admin storageAdmin = new Admin(adminUsernames, userQuotas, core, localStorage, enableWaitlist);\n            ProxyingSpaceUsage p2pSpaceUsage = new ProxyingSpaceUsage(nodeIds, corePropagator, spaceChecker, httpSpaceUsage);\n\n            Account p2pAccount = new ProxyingAccount(nodeIds, core, account, accountProxy);\n            boolean isPublicServer = a.getBoolean(\"public-server\", false);\n            boolean allowExternalLogin = a.getBoolean(\"allow-external-login\", !isPublicServer);\n            LocalOnlyAccount verifyingAccount = new LocalOnlyAccount(new VerifyingAccount(p2pAccount, core, localStorage), userQuotas, allowExternalLogin);\n            ContentAddressedStorage cachingStorage = new AuthedCachingStorage(p2pDht, blockAuth, hasher, blockCacheSize, maxCachedBlockSize);\n\n            ProxyingBatCave p2pBats = new ProxyingBatCave(nodeIds, core, batStore, new HttpBatCave(p2pHttpProxy, p2pHttpProxy));\n            ServerMessageStore serverMessages = new ServerMessageStore(getDBConnector(a, \"server-messages-sql-file\", dbConnectionPool),\n                    sqlCommands, core, p2pDht);\n            SyncRunner.ThreadBased syncer = new SyncRunner.ThreadBased(a, cachingStorage, p2mMutable, corePropagator, crypto);\n\n            Path jsonSyncConfig = a.getPeergosDir().resolve(SyncConfigHandler.SYNC_CONFIG_FILENAME);\n            Path oldSyncConfig = a.getPeergosDir().resolve(SyncConfigHandler.OLD_SYNC_CONFIG_FILENAME);\n            SyncConfig syncConfig = jsonSyncConfig.toFile().exists() ?\n                    SyncConfig.fromJson((Map<String, Object>)JSONParser.parse(Files.readString(jsonSyncConfig))) :\n                    oldSyncConfig.toFile().exists() ?\n                            SyncConfig.fromArgs(Args.parse(new String[]{\"-run-once\", \"true\"}, Optional.of(oldSyncConfig), false)):\n                            SyncConfig.fromArgs(a);\n            boolean flatpak = a.hasArg(\"flatpak\");\n            Either<HostDirEnumerator, HostDirChooser> syncDirChooser = flatpak ?\n                    Either.b(new HostDirChooser.Flatpak()) :\n                    Either.a(new HostDirEnumerator.Java());\n            SyncProperties sync = new SyncProperties(syncConfig, a.getPeergosDir(), syncer, syncDirChooser);\n            Optional<UserService.LocalAppProperties> noConfigApi = Optional.empty();\n            UserService localAPI = new UserService(cachingStorage, p2pBats, crypto, corePropagator, verifyingAccount,\n                    p2pSocial, p2mMutable, storageAdmin, p2pSpaceUsage, serverMessages, gc, Optional.of(sync), noConfigApi);\n            UserService p2pAPI = new UserService(cachingStorage, p2pBats, crypto, corePropagator, verifyingAccount,\n                    p2pSocial, p2mMutable, storageAdmin, p2pSpaceUsage, serverMessages, gc, Optional.empty(), noConfigApi);\n            InetSocketAddress localAPIAddress = userAPIAddress;\n            InetSocketAddress p2pAPIAddress = new InetSocketAddress(\"localhost\", localP2PApi.getTCPPort());\n\n            Optional<Path> webroot = a.hasArg(\"webroot\") ?\n                    Optional.of(PathUtil.get(a.getArg(\"webroot\"))) :\n                    Optional.empty();\n            Optional<HttpPoster> appDevTarget = a.getOptionalArg(\"app-dev-target\")\n                    .map(url ->  new JavaPoster(HttpUtil.toURL(url),  true));\n            boolean useWebAssetCache = a.getBoolean(\"webcache\", appDevTarget.isEmpty());\n            Optional<UserService.TlsProperties> tlsProps =\n                    tlsHostname.map(host -> new UserService.TlsProperties(host, a.getArg(\"tls.keyfile.password\")));\n            int maxConnectionQueue = a.getInt(\"max-connection-queue\", 500);\n            int handlerThreads = a.getInt(\"handler-threads\", 50);\n            Optional<String> basicAuth = a.getOptionalArg(\"basic-auth\");\n            List<String> blockstoreDomains = S3Config.getBlockstoreDomains(a);\n            Optional<String> paymentDomain = a.getOptionalArg(\"payment-domain\");\n            List<String> appSubdomains = Arrays.asList(a.getArg(\"apps\", \"markup-viewer,email,calendar,code-editor,pdf\").split(\",\"));\n            List<String> frameDomains = paymentDomain.map(Arrays::asList).orElse(Collections.emptyList());\n\n            localAPI.initAndStart(localAPIAddress, nodeIds, tlsProps, publicHostname, blockstoreDomains, frameDomains, appSubdomains,\n                    a.getBoolean(\"include-csp\", true), basicAuth, webroot, appDevTarget, useWebAssetCache, isPublicServer, maxConnectionQueue, handlerThreads);\n            p2pAPI.initAndStart(p2pAPIAddress, nodeIds, Optional.empty(), publicHostname, blockstoreDomains, frameDomains, appSubdomains,\n                    a.getBoolean(\"include-csp\", true), basicAuth, webroot, Optional.empty(), useWebAssetCache, isPublicServer, maxConnectionQueue, handlerThreads);\n\n            if (! isPki) {\n                if (core instanceof MirrorCoreNode)\n                    ((MirrorCoreNode) core).start();\n            }\n            if (a.getBoolean(\"update-usage\", true))\n                spaceChecker.calculateUsage();\n\n            if (a.hasArg(\"mirror.node.id\")) {\n                Multihash nodeToMirrorId = Cid.decode(a.getArg(\"mirror.node.id\"));\n                new Thread(() -> {\n                    while (true) {\n                        try {\n                            BatWithId instanceBat = a.getOptionalArg(\"mirror-instance-bat\").map(BatWithId::decode)\n                                    .orElseThrow(() -> new IllegalStateException(\"No target instance bat supplied\"));\n                            int errors = Mirror.mirrorNode(nodeToMirrorId, instanceBat, core, p2pHttpProxy, p2mMutable, localStorage, rawPointers,\n                                    rawAccount, batStore, transactions, linkCounts, usageStore, hasher);\n                            try {\n                                int periodSeconds = a.getInt(\"server-mirror-period-seconds\", 86400);\n                                if (errors == 0)\n                                    Thread.sleep(periodSeconds * 1_000L);\n                                else\n                                    Thread.sleep(30_000);\n                            } catch (InterruptedException f) {}\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                            try {\n                                Thread.sleep(30_000);\n                            } catch (InterruptedException f) {}\n                        }\n                    }\n                }).start();\n            }\n            if (a.hasArg(\"mirror.username\")) {\n                new Thread(() -> {\n                    while (true) {\n                        try {\n                            Optional<SigningKeyPair> mirrorLoginDataPair = a.getOptionalArg(\"login-keypair\").map(SigningKeyPair::fromString);\n                            if (mirrorLoginDataPair.isEmpty())\n                                System.out.println(\"WARNING: Mirroring users data, but not their login, see option 'login-keypair'\");\n                            String username = a.getArg(\"mirror.username\");\n\n                            Optional<BatWithId> mirrorBat = a.getOptionalArg(\"mirror.bat\").map(BatWithId::decode);\n                            if (mirrorBat.isEmpty())\n                                System.out.println(\"WARNING: Mirroring users public blocks only, see option 'mirror.bat'\");\n                            else {\n                                BatId mirrorId = mirrorBat.get().id();\n                                Optional<Bat> existingMirrorBat = batStore.getBat(mirrorId);\n                                if (existingMirrorBat.isEmpty())\n                                    batStore.addBat(username, mirrorId, mirrorBat.get().bat, new byte[0]).join();\n                            }\n                            Mirror.mirrorUser(username, mirrorLoginDataPair, mirrorBat, core, p2mMutable, p2pAccount, localStorage,\n                                    rawPointers, rawAccount, transactions, linkCounts, usageStore, hasher);\n                            try {\n                                Thread.sleep(60_000);\n                            } catch (InterruptedException f) {}\n                        } catch (Exception e) {\n                            Logging.LOG().log(Level.SEVERE, e, () -> e.getMessage());\n                            try {\n                                Thread.sleep(5_000);\n                            } catch (InterruptedException f) {}\n                        }\n                    }\n                }).start();\n            }\n            if (a.getBoolean(\"run-gateway\", false) && ! isPublicServer) {\n                Args gatewayArgs = a.with(\"port\", a.getArg(\"gateway-port\"))\n                        .with(\"peergos-url\", \"http://localhost:\" + a.getArg(\"port\"));\n                GATEWAY.main(gatewayArgs);\n            }\n\n            if (useIPFS && !a.hasArg(\"ipfs.identity.peerid\")) {\n                Args args = a.with(\"ipfs.identity.peerid\", ipfsWrapper.ipfsConfigParams.identity.get().peerId.toBase58());\n                args = args.with(\"ipfs.identity.priv-key\", Base64.getEncoder().encodeToString(ipfsWrapper.ipfsConfigParams.identity.get().privKeyProtobuf));\n                args.saveToFile();\n            } else {\n                a.saveToFileIfAbsent();\n            }\n            System.out.println(\"\\n\" +\n                    \"█╗█╗█╗█╗   ██████╗ ███████╗███████╗██████╗  ██████╗  ██████╗ ███████╗   █╗█╗█╗█╗\\n\" +\n                    \" █████╔╝   ██╔══██╗██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔═══██╗██╔════╝    █████╔╝\\n\" +\n                    \" ██ ██║    ██████╔╝█████╗  █████╗  ██████╔╝██║  ███╗██║   ██║███████╗    ██ ██║\\n\" +\n                    \" █████║    ██╔═══╝ ██╔══╝  ██╔══╝  ██╔══██╗██║   ██║██║   ██║╚════██║    █████║\\n\" +\n                    \"███████╗   ██║     ███████╗███████╗██║  ██║╚██████╔╝╚██████╔╝███████║   ███████╗\\n\" +\n                    \"╚══════╝   ╚═╝     ╚══════╝╚══════╝╚═╝  ╚═╝ ╚═════╝  ╚═════╝ ╚══════╝   ╚══════╝\");\n            boolean generateToken = a.getBoolean(\"generate-token\", ! localhostApi);\n            String host = (publicHostname.isPresent() ? \"https://\" : \"http://\") +\n                    (publicHostname.orElse(localAPIAddress.getHostString())) +\n                    (webPort == 80 ? \"\" : \":\" + webPort);\n            if (generateToken) {\n                System.out.println(\"Generating signup token...\");\n                String token = userQuotas.generateToken(crypto.random);\n                System.out.println(\"Peergos daemon started. Browse to \" + host + \"/?signup=true&token=\"\n                        + token + \" to sign up, or use the shell command with the token \" + token);\n            } else\n                System.out.println(\"Peergos daemon started. Browse to \" + host + \"/ to sign up or login. \\nRun with -generate-token true to generate a signup token.\");\n            InstanceAdmin.VersionInfo version = storageAdmin.getVersionInfo().join();\n            System.out.println(\"Running version \" + version);\n            return new ServerProcesses(localAPI, p2pAPI, ipfsWrapper);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static PublicGateway startGateway(Args a) {\n        Crypto crypto = initCrypto();\n        String peergosUrl = a.getArg(\"peergos-url\");\n        String domainSuffix = a.getArg(\"domain-suffix\");\n        try {\n            URL api = new URL(peergosUrl);\n            NetworkAccess network = Builder.buildJavaNetworkAccess(api,\n                    ! peergosUrl.startsWith(\"http://localhost\"), Optional.empty(), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-gateway\"), Optional.empty()).join();\n            PublicGateway gateway = new PublicGateway(domainSuffix, crypto, network);\n\n            String domain = a.getArg(\"listen-host\");\n            int webPort = a.getInt(\"port\");\n            InetSocketAddress localAddress = new InetSocketAddress(domain, webPort);\n            boolean isPublicServer = a.getBoolean(\"public-server\", false);\n            int maxConnectionQueue = a.getInt(\"max-connection-queue\", 500);\n            int handlerThreads = a.getInt(\"handler-threads\", 50);\n            gateway.initAndStart(localAddress, isPublicServer, maxConnectionQueue, handlerThreads);\n            return gateway;\n        } catch (Exception ex) {\n            throw new RuntimeException(ex);\n        }\n    }\n\n    public static FuseProcess startFuse(Args a) {\n        String username = a.getArg(\"username\");\n        String password = a.getArg(\"PEERGOS_PASSWORD\");\n\n        try {\n            Files.createTempDirectory(\"peergos\").toString();\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe);\n        }\n        String mountPath = a.getArg(\"mountPoint\");\n        Path path = Paths.get(mountPath);\n\n        path.toFile().mkdirs();\n\n        try {\n            String peergosUrl = a.getArg(\"peergos-url\");\n            URL api = new URL(peergosUrl);\n            NetworkAccess network = buildJavaNetworkAccess(api, peergosUrl.startsWith(\"https\"), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-fuse\"), Optional.empty()).join();\n\n            Crypto crypto = initCrypto();\n            PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n            JvmThumbnailer.initJava();\n            UserContext userContext = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).join();\n            PeergosFS peergosFS = new PeergosFS(userContext);\n            FuseProcess fuseProcess = new FuseProcess(peergosFS, path);\n\n            Runtime.getRuntime().addShutdownHook(new Thread(() -> fuseProcess.close(), \"Fuse shutdown\"));\n\n            fuseProcess.start();\n            System.out.println(\"\\n\\nPeergos mounted at \" + path + \"\\n\\n\");\n            return fuseProcess;\n        } catch (Exception ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n\n    public static CompletableFuture<MultiFactorAuthResponse> getMfaResponseCLI(MultiFactorAuthRequest req) {\n        Optional<MultiFactorAuthMethod> anyTotp = req.methods.stream().filter(m -> m.type == MultiFactorAuthMethod.Type.TOTP).findFirst();\n        if (anyTotp.isEmpty())\n            throw new IllegalStateException(\"No supported 2 factor auth method! \" + req.methods);\n        MultiFactorAuthMethod totp = anyTotp.get();\n        System.out.println(\"Enter TOTP code for login\");\n        Console console = System.console();\n        String code = console.readLine().trim();\n        return Futures.of(new MultiFactorAuthResponse(totp.credentialId, Either.a(code)));\n    }\n\n    public static IpfsWrapper startIpfs(Args a) {\n        // test if ipfs is already running\n        String ipfsApiAddress = a.getArg(\"ipfs-api-address\");\n        if (IpfsWrapper.isHttpApiListening(ipfsApiAddress)) {\n            throw new IllegalStateException(\"IPFS is already running on api \" + ipfsApiAddress);\n        }\n        return IpfsWrapper.launch(a);\n    }\n\n    public static Boolean startShell(Args args) {\n        CLI.buildAndRun(args);\n        return true;\n    }\n\n    /** This should be run on a Peergos server to which a user will be migrated\n     *\n     * @param a\n     * @return\n     */\n    public static boolean migrate(Args a) {\n        Crypto crypto = initCrypto();\n        String peergosUrl = a.getArg(\"peergos-url\");\n        try {\n            URL api = new URL(peergosUrl);\n            NetworkAccess network = buildJavaNetworkAccess(api, ! peergosUrl.startsWith(\"http://localhost\"), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-migrate\"), Optional.empty()).join();\n            Console console = System.console();\n            String username = console.readLine(\"Enter username to migrate to this server: \");\n            String password = new String(console.readPassword(\"Enter password for \" + username + \": \"));\n\n            UserContext user = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).join();\n            List<UserPublicKeyLink> existing = user.network.coreNode.getChain(username).join();\n            Multihash currentStorageNodeId = existing.get(existing.size() - 1).claim.storageProviders.stream().findFirst().get();\n            Multihash newStorageNodeId = network.dhtClient.id().join();\n            if (currentStorageNodeId.equals(newStorageNodeId)) {\n                System.err.println(\"You are trying to migrate a user to their current server. Please supply the url of a different server.\");\n                return false;\n            }\n            System.out.println(\"Migrating user from node \" + currentStorageNodeId + \" to \" + newStorageNodeId);\n            List<UserPublicKeyLink> newChain = Migrate.buildMigrationChain(existing, newStorageNodeId, user.signer.secret).join();\n            user.ensureMirrorId().join().get();\n            Optional<BatWithId> current = user.getMirrorBat().join();\n            long usage = user.getSpaceUsage(false).join();\n            user.network.coreNode.migrateUser(username, newChain, currentStorageNodeId, current, LocalDateTime.MIN, usage, true).join();\n            List<UserPublicKeyLink> updatedChain = user.network.coreNode.getChain(username).join();\n            if (!updatedChain.get(updatedChain.size() - 1).claim.storageProviders.contains(newStorageNodeId))\n                throw new IllegalStateException(\"Migration failed. Please try again later\");\n            System.out.println(\"Migration complete.\");\n            return true;\n        } catch (Exception ex) {\n            ex.printStackTrace();\n            return false;\n        }\n    }\n\n    public static final Command<Void> MAIN = new Command<>(\"Main\",\n            \"Run a Peergos command\",\n            args -> {\n                if (args.hasArg(\"version\")) {\n                    VERSION.main(args);\n                } else\n                    System.out.println(\"Run with -help to show options, \\n\" +\n                            \"or to see options for a sub command run with $command -help\");\n\n                try {\n                    if (args.hasArg(ARG_SERVER_URL.name))\n                        args = args.with(\"peergos-url\", getAppServerUrl(args));\n                    // By default we run a proxy instance and open it in the browser\n                    // Check if proxy is already running and stop it if the version is different\n                    int port = args.getInt(\"port\", 7777);\n                    URI api = new URI(\"http://localhost:\" + port);\n                    Optional<ProxySelector> proxy = ProxyChooser.build(args);\n                    JavaPoster poster = new JavaPoster(api.toURL(), false, Optional.empty(), Optional.empty(), proxy);\n                    ScryptJava hasher = new ScryptJava();\n                    ContentAddressedStorage localDht = NetworkAccess.buildLocalDht(poster, true, hasher);\n                    boolean alreadyRunning = false;\n                    try {\n                        localDht.ids().join();\n                        HttpInstanceAdmin admin = new HttpInstanceAdmin(poster);\n                        InstanceAdmin.VersionInfo running = admin.getVersionInfo().join();\n                        InstanceAdmin.VersionInfo ourVersion = new InstanceAdmin.VersionInfo(UserService.CURRENT_VERSION, Admin.getSourceVersion());\n                        if (! running.equals(ourVersion))\n                            STOP.main(args);\n                        else\n                            alreadyRunning = true;\n                    } catch (Exception e){}\n\n                    if (! alreadyRunning) {\n                        try {\n                            PROXY.main(args);\n                        } catch (IllegalStateException e) {\n                            if (e.getCause() instanceof BindException) {\n                                // try another port if something is already listening on 7777\n                                port = 7007;\n                                args = args.with(\"port\", port + \"\");\n                                api = new URI(\"http://localhost:\" + port);\n                                PROXY.main(args);\n                            } else\n                                throw e;\n                        }\n                    }\n                    DesktopApp.launch(args, port, api);\n                    return null;\n                } catch (URISyntaxException | IOException e) {\n                    throw new RuntimeException(e);\n                }\n            },\n            Arrays.asList(\n                    ARG_SERVER_URL,\n                    new Command.Arg(\"port\", \"Localhost server port for app/proxy mode\", false, \"7777\")\n            ),\n            Arrays.asList(\n                    PEERGOS,\n                    SHELL,\n                    SYNC,\n                    FUSE,\n                    WEBDAV,\n                    QuotaCLI.QUOTA,\n                    UsageCLI.USAGE,\n                    ServerMessages.SERVER_MESSAGES,\n                    ServerIdentity.SERVER_IDENTITY,\n                    GATEWAY,\n                    Mirror.MIRROR,\n                    MIGRATE,\n                    VERSION,\n                    HOME,\n                    IDENTITY,\n                    PROXY,\n                    ServerAdmin.SERVER_ADMIN,\n                    STOP,\n                    PKI,\n                    PKI_INIT,\n                    IPFS\n            )\n    );\n\n    public static MultiAddress getLocalMultiAddress(int port) {\n        return new MultiAddress(\"/ip4/127.0.0.1/tcp/\" + port);\n    }\n\n    public static MultiAddress getLocalBootstrapAddress(int port, Multihash nodeId) {\n        return new MultiAddress(\"/ip4/127.0.0.1/tcp/\" + port + \"/ipfs/\"+ nodeId);\n    }\n\n    public static void main(String[] args) {\n        try {\n            // Load webp native library for native image inclusion\n            if (\"linux\".equalsIgnoreCase(System.getProperty(\"os.name\")))\n                NativeLibraryUtil.loadNativeLibrary(WebPDecoderOptions.class, \"webp-imageio\");\n        } catch (Throwable t) {}\n\n        // Netty uses thread count twice the number of CPUs, this undoes that\n        System.getProperties().setProperty(\"io.netty.eventLoopThreads\", \"2\");\n        try {\n            MAIN.main(Args.parse(args));\n        } catch (Throwable e) {\n            e.printStackTrace();\n            Logging.LOG().log(Level.SEVERE, e, () -> e.getMessage());\n            System.exit(-1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/Mirror.java",
    "content": "package peergos.server;\n\nimport peergos.server.corenode.*;\nimport peergos.server.login.*;\nimport peergos.server.space.UsageStore;\nimport peergos.server.space.UserUsage;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.logging.*;\n\npublic class Mirror {\n\n    public static final Command<Boolean> INIT = new Command<>(\"init\",\n            \"Derive the parameters needed to mirror a user's data\",\n            a -> {\n                Crypto crypto = Main.initCrypto();\n                String peergosUrl = a.getArg(\"peergos-url\");\n                try {\n                    URL api = new URL(peergosUrl);\n                    NetworkAccess network;\n                    try {\n                        network = Main.buildJavaNetworkAccess(api, peergosUrl.startsWith(\"https\"), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-mirror\"), Optional.empty()).join();\n                    } catch (Exception e) {\n                        System.err.println(\"Couldn't connect to peergos server at \" + peergosUrl);\n                        System.err.println(\"To use a different server supply location, e.g. -peergos-url https://peergos.net\");\n                        return false;\n                    }\n                    Console console = System.console();\n                    String username = a.getArg(\"username\");\n                    String password = new String(console.readPassword(\"Enter password for \" + username + \": \"));\n\n                    UserContext user = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).join();\n                    Optional<BatWithId> mirrorBat = user.getMirrorBat().join();\n\n                    WriterData userData = WriterData.fromCbor(UserContext.getWriterDataCbor(network, username).join().right);\n                    boolean legacyAccount = userData.staticData.isPresent();\n                    Optional<SecretGenerationAlgorithm> alg = userData.generationAlgorithm;\n                    Optional<SigningKeyPair> loginKeyPair = legacyAccount ?\n                            Optional.empty() :\n                            Optional.of(UserUtil.generateUser(username, password, crypto, alg.get()).join().getUser());\n\n                    System.out.println(\"To mirror all your data on another server run the \\\"daemon\\\" command with these additional arguments:\");\n                    System.out.println(\"-mirror.username \" + username);\n                    mirrorBat.ifPresent(b -> System.out.println(\"Set daemon arg -mirror.bat \" + b.encode()));\n                    loginKeyPair.ifPresent(login -> System.out.println(\"Set daemon arg -login-keypair \" + login));\n                    return true;\n                } catch (Exception ex) {\n                    ex.printStackTrace();\n                    return false;\n                }\n            },\n            Arrays.asList(\n                    new Command.Arg(\"username\", \"The username whose data you want to mirror\", true),\n                    new Command.Arg(\"peergos-url\", \"Address of the Peergos server to connect to\", false, \"http://localhost:8000\")\n            )\n    );\n\n    public static final Command<Boolean> MIRROR = new Command<>(\"mirror\",\n            \"Commands related to mirroring your data on another server\",\n            args -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"print-log-location\", \"Whether to print the log file location at startup\", false, \"false\"),\n                    new Command.Arg(\"log-to-file\", \"Whether to log to a file\", false, \"false\"),\n                    new Command.Arg(\"log-to-console\", \"Whether to log to the console\", false, \"false\")\n            ),\n            Arrays.asList(INIT)\n    );\n\n    /**\n     *\n     * @param nodeId\n     * @param instanceBat\n     * @param core\n     * @param p2pPoster\n     * @param p2pPointers\n     * @param storage\n     * @param targetPointers\n     * @param targetAccount\n     * @param batStorage\n     * @param transactions\n     * @param linkCounts\n     * @param usage\n     * @param hasher\n     * @return Number of errored users\n     */\n    public static int mirrorNode(Multihash nodeId,\n                                 BatWithId instanceBat,\n                                 CoreNode core,\n                                 HttpPoster p2pPoster,\n                                 MutablePointers p2pPointers,\n                                 DeletableContentAddressedStorage storage,\n                                 JdbcIpnsAndSocial targetPointers,\n                                 JdbcAccount targetAccount,\n                                 BatCave batStorage,\n                                 TransactionStore transactions,\n                                 LinkRetrievalCounter linkCounts,\n                                 UsageStore usage,\n                                 Hasher hasher) {\n        Logging.LOG().log(Level.INFO, \"Mirroring data for node \" + nodeId);\n        Optional<LocalDateTime> latest = linkCounts.getLatestModificationTime();\n        HTTPCoreNode sourceNode = new HTTPCoreNode(p2pPoster, nodeId);\n        int userCount = 0;\n        String cursor = \"\";\n        Set<String> erroredUsers = new TreeSet<>();\n        while (true) {\n            Logging.LOG().log(Level.INFO, \"Mirroring from cursor \" + cursor);\n            try {\n                List<UserSnapshot> snapshots = sourceNode.getSnapshots(cursor, instanceBat, latest.orElse(LocalDateTime.MIN)).join();\n                for (UserSnapshot snapshot : snapshots) {\n                    try {\n                        String username = snapshot.username;\n                        Logging.LOG().log(Level.INFO, \"Mirroring \" + username);\n                        PublicKeyHash owner = snapshot.owner;\n                        snapshot.login.ifPresent(login -> targetAccount.setLoginData(login).join());\n                        linkCounts.setCounts(username, snapshot.linkCounts);\n                        List<BatWithId> localMirrorBats = batStorage.getUserBats(username, new byte[0]).join();\n                        for (BatWithId mirrorBat : snapshot.mirrorBats) {\n                            if (!localMirrorBats.contains(mirrorBat))\n                                batStorage.addBat(username, mirrorBat.id(), mirrorBat.bat, new byte[0]);\n                        }\n                        usage.addUserIfAbsent(username);\n                        usage.addWriter(username, owner);\n                        for (Map.Entry<PublicKeyHash, byte[]> pointer : snapshot.pointerState.entrySet()) {\n                            PublicKeyHash writer = pointer.getKey();\n                            byte[] value = pointer.getValue();\n                            usage.addWriter(username, writer);\n                            mirrorMerkleTree(username, owner, writer, List.of(nodeId), value, Optional.of(instanceBat), storage, targetPointers, transactions, usage, hasher);\n                        }\n                        userCount++;\n                    } catch (Exception e) {\n                        if (e.getCause() instanceof UnknownHostException) {\n                            Logging.LOG().log(Level.WARNING, \"Couldn't mirror user: \" + snapshot.username, e);\n                            throw new Error(e.getCause());\n                        }\n                        erroredUsers.add(snapshot.username);\n                        Logging.LOG().log(Level.WARNING, \"Couldn't mirror user: \" + snapshot.username, e);\n                        Logging.LOG().log(Level.WARNING, \"Errored users so far (\" + erroredUsers.size() + \"): \" + erroredUsers);\n                    }\n                }\n                if (snapshots.isEmpty())\n                    break;\n                cursor = snapshots.get(snapshots.size() - 1).username;\n                if (snapshots.size() < MirrorCoreNode.MAX_SNAPSHOTS)\n                    break;\n            } catch (Exception e) {\n                e.printStackTrace();\n                Threads.sleep(10_000);\n            }\n        }\n        Logging.LOG().log(Level.INFO, \"Finished mirroring data for node \" + nodeId + \", with \" + userCount + \" users.\");\n        if (! erroredUsers.isEmpty())\n            Logging.LOG().log(Level.INFO, \"Errored users (\" + erroredUsers.size() + \"): \" + erroredUsers);\n        return erroredUsers.size();\n    }\n\n    /**\n     *\n     * @param username\n     * @param core\n     * @param p2pPointers\n     * @param storage\n     * @param targetPointers\n     * @param transactions\n     * @param hasher\n     * @return The version mirrored\n     */\n    public static Map<PublicKeyHash, byte[]> mirrorUser(String username,\n                                                        Optional<SigningKeyPair> loginAuth,\n                                                        Optional<BatWithId> mirrorBat,\n                                                        CoreNode core,\n                                                        MutablePointers p2pPointers,\n                                                        Account p2pAccount,\n                                                        DeletableContentAddressedStorage storage,\n                                                        JdbcIpnsAndSocial targetPointers,\n                                                        JdbcAccount targetAccount,\n                                                        TransactionStore transactions,\n                                                        LinkRetrievalCounter linkCounts,\n                                                        UsageStore usage,\n                                                        Hasher hasher) {\n        Logging.LOG().log(Level.INFO, \"Mirroring data for \" + username + loginAuth.map(k -> \" including login data\").orElse(\" excluding login data\"));\n        Optional<PublicKeyHash> identity = core.getPublicKeyHash(username).join();\n        if (! identity.isPresent())\n            return Collections.emptyMap();\n        PublicKeyHash owner = identity.get();\n        Map<PublicKeyHash, byte[]> versions = new HashMap<>();\n        Cid ourId = storage.id().join();\n        List<Multihash> storageProviders = core.getStorageProviders(owner);\n        Set<PublicKeyHash> ownedKeys = DeletableContentAddressedStorage.getOwnedKeysRecursive(owner, owner, p2pPointers,\n                (h, s) -> DeletableContentAddressedStorage.getWriterData(storageProviders, owner, h, s, true, ourId, hasher, storage), storage, hasher).join();\n        for (PublicKeyHash ownedKey : ownedKeys) {\n            Optional<byte[]> version = mirrorMutableSubspace(username, owner, ownedKey, storageProviders, mirrorBat, p2pPointers, storage,\n                    targetPointers, transactions, usage, hasher);\n            if (version.isPresent())\n                versions.put(ownedKey, version.get());\n        }\n        if (loginAuth.isPresent()) {\n            SigningKeyPair login = loginAuth.get();\n            Either<UserStaticData, MultiFactorAuthRequest> loginData = p2pAccount.getLoginData(username, login.publicSigningKey,\n                    TimeLimitedClient.signNow(login.secretSigningKey).join(), Optional.empty(), false, true, true).join();\n            if (loginData.isA()) {\n                UserStaticData entryData = loginData.a();\n                targetAccount.setLoginData(new LoginData(username, entryData, login.publicSigningKey, Optional.empty())).join();\n            } else\n                Logging.LOG().log(Level.WARNING, \"Unable to mirror login data because 2FA is required\");\n        }\n        if (mirrorBat.isPresent()) { // get link count db\n            Optional<LocalDateTime> latest = linkCounts.getLatestModificationTime(username);\n            LinkCounts updatedCounts = storage.getLinkCounts(username, latest.orElse(LocalDateTime.MIN), mirrorBat.get()).join();\n            linkCounts.setCounts(username, updatedCounts);\n        }\n        Logging.LOG().log(Level.INFO, \"Finished mirroring data for \" + username);\n        return versions;\n    }\n\n    /**\n     *\n     * @param owner\n     * @param writer\n     * @param p2pPointers\n     * @param storage\n     * @param targetPointers\n     * @return the version mirrored\n     */\n    public static Optional<byte[]> mirrorMutableSubspace(String username,\n                                                         PublicKeyHash owner,\n                                                         PublicKeyHash writer,\n                                                         List<Multihash> peerIds,\n                                                         Optional<BatWithId> mirrorBat,\n                                                         MutablePointers p2pPointers,\n                                                         DeletableContentAddressedStorage storage,\n                                                         JdbcIpnsAndSocial targetPointers,\n                                                         TransactionStore transactions,\n                                                         UsageStore usage,\n                                                         Hasher hasher) {\n        Optional<byte[]> updated = p2pPointers.getPointer(owner, writer).join();\n        if (! updated.isPresent()) {\n            Logging.LOG().log(Level.WARNING, \"Skipping unretrievable mutable pointer for: \" + writer);\n            return updated;\n        }\n\n        usage.addUserIfAbsent(username);\n        usage.addWriter(username, writer);\n        mirrorMerkleTree(username, owner, writer, peerIds, updated.get(), mirrorBat, storage, targetPointers, transactions, usage, hasher);\n        return updated;\n    }\n\n    public static void mirrorMerkleTree(String username,\n                                        PublicKeyHash owner,\n                                        PublicKeyHash writer,\n                                        List<Multihash> peerIds,\n                                        byte[] newPointer,\n                                        Optional<BatWithId> mirrorBat,\n                                        DeletableContentAddressedStorage storage,\n                                        JdbcIpnsAndSocial targetPointers,\n                                        TransactionStore transactions,\n                                        UsageStore usagedb,\n                                        Hasher hasher) {\n        Optional<byte[]> existing = targetPointers.getPointer(writer).join();\n        // First pin the new root, then commit updated pointer\n        MaybeMultihash existingTarget = existing.isPresent() ?\n                MutablePointers.parsePointerTarget(existing.get(), owner, writer, storage).join().updated :\n                MaybeMultihash.empty();\n        MaybeMultihash updatedTarget = MutablePointers.parsePointerTarget(newPointer, owner, writer, storage).join().updated;\n        // use a mirror call to distinguish from normal pin calls\n        TransactionId tid = transactions.startTransaction(owner);\n        try {\n            // zero pending usage\n            UserUsage usage = usagedb.getUsage(username);\n            if (usage.getPending(writer) != 0)\n                usagedb.resetPendingUsage(username, writer);\n            storage.mirror(username, owner, writer, peerIds,\n                    existingTarget.toOptional().map(c -> (Cid)c),\n                    updatedTarget.toOptional().map(c -> (Cid)c), mirrorBat, storage.id().join(), (x, y, z) -> {}, tid, hasher);\n            targetPointers.setPointer(writer, existing, newPointer).join();\n        } finally {\n            transactions.closeTransaction(owner, tid);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/NonWriteThroughNetwork.java",
    "content": "package peergos.server;\n\nimport peergos.server.corenode.*;\nimport peergos.server.login.*;\nimport peergos.server.mutable.*;\nimport peergos.server.social.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.user.*;\n\nimport java.util.*;\n\npublic class NonWriteThroughNetwork extends NetworkAccess {\n\n    protected NonWriteThroughNetwork(CoreNode coreNode,\n                                     Account account,\n                                     SocialNetwork social,\n                                     ContentAddressedStorage ipfs,\n                                     MutablePointers mutable,\n                                     MutableTree tree,\n                                     WriteSynchronizer synchronizer,\n                                     InstanceAdmin instanceAdmin,\n                                     SpaceUsage spaceUsage,\n                                     Hasher hasher,\n                                     List<String> usernames,\n                                     boolean isJavascript) {\n        super(coreNode, account, social, ipfs, null, Optional.empty(), mutable, tree, synchronizer, instanceAdmin, spaceUsage, null, hasher, usernames, isJavascript);\n    }\n\n    public static NetworkAccess build(NetworkAccess source) {\n        ContentAddressedStorage nonWriteThroughIpfs = new NonWriteThroughStorage(source.dhtClient, source.hasher);\n        MutablePointers nonWriteThroughPointers = new NonWriteThroughMutablePointers(source.mutable, nonWriteThroughIpfs);\n        NonWriteThroughCoreNode nonWriteThroughCoreNode = new NonWriteThroughCoreNode(source.coreNode, nonWriteThroughIpfs);\n        NonWriteThroughAccount nonWriteThroughAccount = new NonWriteThroughAccount(source.account);\n        NonWriteThroughSocialNetwork nonWriteThroughSocial = new NonWriteThroughSocialNetwork(source.social, nonWriteThroughIpfs);\n        WriteSynchronizer synchronizer = new WriteSynchronizer(nonWriteThroughPointers, nonWriteThroughIpfs, source.hasher);\n        MutableTree nonWriteThroughTree = new MutableTreeImpl(nonWriteThroughPointers, nonWriteThroughIpfs, source.hasher, synchronizer);\n        return new NonWriteThroughNetwork(nonWriteThroughCoreNode,\n                nonWriteThroughAccount,\n                nonWriteThroughSocial,\n                nonWriteThroughIpfs,\n                nonWriteThroughPointers,\n                nonWriteThroughTree, synchronizer, source.instanceAdmin, source.spaceUsage, source.hasher, source.usernames, false);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/Playground.java",
    "content": "package peergos.server;\nimport java.util.logging.*;\n\nimport peergos.server.storage.*;\nimport peergos.server.util.Logging;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\n\n/**\n *  Use this class to experiment with existing data without committing any changes or writing any data to disk\n */\npublic class Playground {\n    private static final Logger LOG = Logging.LOG();\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess source = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.empty(), Optional.empty()).get();\n        NetworkAccess nonWriteThrough = NonWriteThroughNetwork.build(source);\n\n        String username = args[0];\n        Console console = System.console();\n        String password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n        UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, nonWriteThrough, crypto).get();\n\n        // invert the following two lines to actually commit the experiment\n        experiment(username, context, nonWriteThrough);\n//        experiment(username, context, source);\n\n        // Can we still log in?\n        UserContext context2 = UserContext.signIn(username, password, Main::getMfaResponseCLI, nonWriteThrough, crypto).get();\n        LOG.info(context2.getUserRoot().get().getName());\n    }\n\n    private static void experiment(String username,\n                                   UserContext context,\n                                   NetworkAccess network) throws Exception {\n        // Do something dangerous (you only live once)\n        Set<PublicKeyHash> ownedKeys = DeletableContentAddressedStorage.getOwnedKeysRecursive(username, network.coreNode,\n                network.mutable, (h, s) -> ContentAddressedStorage.getWriterData(context.signer.publicKeyHash, h, s, network.dhtClient),\n                network.dhtClient, network.hasher).join();\n        for (PublicKeyHash ownedKey : ownedKeys) {\n            if (ownedKey.equals(context.signer.publicKeyHash))\n                continue; // only the writer has a tree\n            CommittedWriterData existing = WriterData.getWriterData(context.signer.publicKeyHash, ownedKey,\n                    network.mutable, network.dhtClient).get();\n            if (existing.props.get().tree.isPresent())\n                continue;\n            // Do something risky\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/PublicGateway.java",
    "content": "package peergos.server;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.net.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.concurrent.*;\nimport java.util.logging.*;\n\n/** This class acts as a public gateway for serving websites from Peergos\n *\n *  A user alice can publish a webroot in Peergos.\n *  This is then served at alice.public.localhost:9000 and alice.peergos.me\n */\npublic class PublicGateway {\n    private static final Logger LOG = Logging.LOG();\n\n    private final String domainSuffix;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private volatile HttpServer localhostServer;\n\n    public PublicGateway(String domainSuffix, Crypto crypto, NetworkAccess network) {\n        this.domainSuffix = domainSuffix;\n        this.crypto = crypto;\n        this.network = network;\n    }\n\n    public void shutdown() {\n        localhostServer.stop(0);\n    }\n\n    public void initAndStart(InetSocketAddress local,\n                             boolean isPublicServer,\n                             int connectionBacklog,\n                             int handlerPoolSize) throws IOException {\n        LOG.info(\"Starting local Peergos gateway at: localhost:\" + local.getPort());\n        localhostServer = HttpServer.create(local, connectionBacklog);\n\n        GatewayHandler publicGateway = new GatewayHandler(domainSuffix, crypto, network);\n        localhostServer.createContext(\"/\", isPublicServer ? new HSTSHandler(publicGateway) : publicGateway);\n\n        localhostServer.setExecutor(Threads.newPool(handlerPoolSize, \"peergos-gateway-handler-\"));\n        localhostServer.start();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/Publisher.java",
    "content": "package peergos.server;\n\nimport peergos.shared.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.Optional;\n\n/**\n * Make a file or directory in Peergos public\n */\npublic class Publisher {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.empty(), Optional.empty()).get();\n        String username = args[0];\n        String pathToMakePublic = args[1];\n        Console console = System.console();\n        String password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n        UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).get();\n        FileWrapper file = context.getByPath(pathToMakePublic).join().get();\n        context.makePublic(file).join();\n        System.out.println(\"Made \" + pathToMakePublic + \" public.\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/Renew.java",
    "content": "package peergos.server;\n\nimport peergos.shared.*;\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.time.*;\nimport java.util.Optional;\n\npublic class Renew {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.empty(), Optional.empty()).get();\n        String username = args[0];\n        LocalDate expiry = LocalDate.parse(args[1]);\n        Console console = System.console();\n        String password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n        UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).get();\n        context.renewUsernameClaim(expiry).get();\n        System.out.println(\"Logged in \" + username + \" successfully!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/RenewUsernameClaim.java",
    "content": "package peergos.server;\n\nimport peergos.shared.*;\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.time.*;\nimport java.util.Optional;\n\npublic class RenewUsernameClaim {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.empty(), Optional.empty()).get();\n        String username = args[0];\n        Console console = System.console();\n        String password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n\n        LocalDate expiry = LocalDate.now().plusMonths(2);\n        UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).get();\n        boolean isExpired = context.usernameIsExpired().get();\n        if (isExpired)\n            System.out.println(context.renewUsernameClaim(expiry).get() ? \"Renewed username\" : \"Failed to renew username\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/ServerAdmin.java",
    "content": "package peergos.server;\n\nimport com.webauthn4j.data.client.Origin;\nimport peergos.server.corenode.IpfsCoreNode;\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.corenode.UserRepository;\nimport peergos.server.crypto.hash.ScryptJava;\nimport peergos.server.login.JdbcAccount;\nimport peergos.server.space.JdbcUsageStore;\nimport peergos.server.space.UsageStore;\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.storage.*;\nimport peergos.server.storage.admin.QuotaAdmin;\nimport peergos.server.storage.auth.*;\nimport peergos.shared.Crypto;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.corenode.HTTPCoreNode;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.login.mfa.MultiFactorAuthMethod;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.storage.auth.Bat;\nimport peergos.shared.storage.auth.BatId;\nimport peergos.shared.storage.auth.BatWithId;\nimport peergos.shared.user.CommittedWriterData;\nimport peergos.shared.util.ArrayOps;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Supplier;\n\npublic class ServerAdmin {\n    public static final Command.Arg ARG_USERNAME =\n            new Command.Arg(\"username\", \"The username to delete\", true);\n\n    private static void deleteOwnedKeys(PublicKeyHash id,\n                                        MutablePointers pointers,\n                                        JdbcIpnsAndSocial rawPointers,\n                                        DeletableContentAddressedStorage storage,\n                                        Crypto crypto) {\n        Set<PublicKeyHash> done = new HashSet<>();\n        deleteOwnedKeysRecurse(id, id, pointers, rawPointers, storage, crypto, done);\n    }\n\n    private static void deleteOwnedKeysRecurse(PublicKeyHash id,\n                                               PublicKeyHash w,\n                                               MutablePointers pointers,\n                                               JdbcIpnsAndSocial rawPointers,\n                                               DeletableContentAddressedStorage storage,\n                                               Crypto crypto,\n                                               Set<PublicKeyHash> done) {\n        done.add(w);\n        Cid ourId = storage.id().join();\n        CommittedWriterData.Retriever retriever = (h, s) -> DeletableContentAddressedStorage.getWriterData(\n                Collections.emptyList(), id, h, s, false, ourId, crypto.hasher, storage);\n        Set<PublicKeyHash> verifiedChildren = DeletableContentAddressedStorage.getDirectOwnedKeys(id, w, pointers,\n                retriever, storage, crypto.hasher).join();\n        for (PublicKeyHash child : verifiedChildren) {\n            if (! done.contains(child))\n                deleteOwnedKeysRecurse(id, child, pointers, rawPointers, storage, crypto, done);\n        }\n        rawPointers.removePointer(w);\n    }\n\n    public static final Command<Boolean> MFA_DELETE = new Command<>(\"delete\",\n            \"Delete a multi-factor auth option for a local user.\",\n            a -> {\n                Builder.disableLog();\n                IpfsCoreNode.disableLog();\n                ScryptJava.disableLog();\n                HTTPCoreNode.disableLog();\n\n                String username = a.getArg(\"username\");\n                byte[] credentialID = ArrayOps.hexToBytes(a.getArg(\"credential-id\"));\n                SqlSupplier sqlCommands = Builder.getSqlCommands(a);\n                Supplier<Connection> dbConnectionPool = Builder.getDBConnector(a, \"serverids-file\");\n\n                String listeningHost = a.getArg(Main.LISTEN_HOST.name);\n                Optional<String> tlsHostname = a.hasArg(\"tls.keyfile.password\") ? Optional.of(listeningHost) : Optional.empty();\n                Optional<String> publicHostname = tlsHostname.isPresent() ? tlsHostname : a.getOptionalArg(\"public-domain\");\n                int webPort = a.getInt(\"port\");\n                Origin origin = new Origin(publicHostname.map(host -> (Main.isLanIP(host) ? \"http://\" : \"https://\") + host).orElse(\"http://localhost:\" + webPort));\n                String rpId = publicHostname.orElse(\"localhost\");\n                JdbcAccount rawAccount = new JdbcAccount(Builder.getDBConnector(a, \"account-sql-file\", dbConnectionPool), sqlCommands, origin, rpId);\n                rawAccount.deleteMfa(username, credentialID);\n                System.out.println(\"Deleted multifactor auth for user \" + username + \" with credential id \" + ArrayOps.bytesToHex(credentialID));\n\n                return true;\n            },\n            Arrays.asList(\n                    ARG_USERNAME,\n                    new Command.Arg(\"log-to-file\", \"Whether to log to a file\", true, \"false\"),\n                    new Command.Arg(\"print-log-location\", \"Whether to print log location\", true, \"false\")\n            )\n    );\n\n    public static final Command<Boolean> MFA = new Command<>(\"mfa\",\n            \"List multi-factor auth options for a local user.\",\n            a -> {\n                Builder.disableLog();\n                IpfsCoreNode.disableLog();\n                ScryptJava.disableLog();\n                HTTPCoreNode.disableLog();\n\n                String username = a.getArg(\"username\");\n                SqlSupplier sqlCommands = Builder.getSqlCommands(a);\n                Supplier<Connection> dbConnectionPool = Builder.getDBConnector(a, \"serverids-file\");\n\n                String listeningHost = a.getArg(Main.LISTEN_HOST.name);\n                Optional<String> tlsHostname = a.hasArg(\"tls.keyfile.password\") ? Optional.of(listeningHost) : Optional.empty();\n                Optional<String> publicHostname = tlsHostname.isPresent() ? tlsHostname : a.getOptionalArg(\"public-domain\");\n                int webPort = a.getInt(\"port\");\n                Origin origin = new Origin(publicHostname.map(host -> (Main.isLanIP(host) ? \"http://\" : \"https://\") + host).orElse(\"http://localhost:\" + webPort));\n                String rpId = publicHostname.orElse(\"localhost\");\n                JdbcAccount rawAccount = new JdbcAccount(Builder.getDBConnector(a, \"account-sql-file\", dbConnectionPool), sqlCommands, origin, rpId);\n                List<MultiFactorAuthMethod> mfas = rawAccount.getSecondAuthMethods(username).join();\n                System.out.println(\"Listing multifactor auth options for \" + username);\n                mfas.forEach(mfa -> {\n                    System.out.println(mfa.name + \", credentialID: \" + ArrayOps.bytesToHex(mfa.credentialId) + \", enabled: \" + mfa.enabled + \", type: \" + mfa.type.name());\n                });\n                return true;\n            },\n            Arrays.asList(\n                    ARG_USERNAME,\n                    new Command.Arg(\"log-to-file\", \"Whether to log to a file\", true, \"false\"),\n                    new Command.Arg(\"print-log-location\", \"Whether to print log location\", true, \"false\")\n            ),\n            Arrays.asList(MFA_DELETE)\n    );\n\n    public static final Command<Boolean> DELETE = new Command<>(\"delete\",\n            \"Delete a user from this server and remove their space quota.\",\n            a -> {\n                try {\n                    Builder.disableLog();\n                    IpfsCoreNode.disableLog();\n                    ScryptJava.disableLog();\n                    HTTPCoreNode.disableLog();\n\n                    Crypto crypto = JavaCrypto.init();\n                    String username = a.getArg(\"username\");\n                    SqlSupplier sqlCommands = Builder.getSqlCommands(a);\n                    BlockMetadataStore meta = Builder.buildBlockMetadata(a);\n                    Supplier<Connection> dbConnectionPool = Builder.getDBConnector(a, \"serverids-file\");\n                    JdbcServerIdentityStore ids = JdbcServerIdentityStore.build(dbConnectionPool, sqlCommands, crypto);\n                    JdbcBatCave batStore = new JdbcBatCave(Builder.getDBConnector(a, \"bat-store\", dbConnectionPool), sqlCommands);\n                    BlockRequestAuthoriser blockAuth = Builder.blockAuthoriser(a, batStore, crypto.hasher);\n                    Supplier<Connection> usageDb = Builder.getDBConnector(a, \"space-usage-sql-file\", dbConnectionPool);\n                    JdbcUsageStore usageStore = new JdbcUsageStore(usageDb, sqlCommands);\n                    JdbcIpnsAndSocial rawPointers = Builder.buildRawPointers(a,\n                            Builder.getDBConnector(a, \"mutable-pointers-file\", dbConnectionPool));\n                    PartitionStatus partitionStatus = new JdbcPartitionStatus(Builder.getDBConnector(a, \"partition-status-file\"), sqlCommands);\n                    DeletableContentAddressedStorage storage = Builder.buildLocalStorage(a, meta, null, null, blockAuth,\n                            ids, usageStore, rawPointers, partitionStatus, crypto.hasher);\n\n                    MutablePointers pointers = UserRepository.build(storage, rawPointers, crypto.hasher);\n                    QuotaAdmin quota = Builder.buildSpaceQuotas(a, storage,\n                            Builder.getDBConnector(a, \"space-requests-sql-file\", dbConnectionPool),\n                            Builder.getDBConnector(a, \"quotas-sql-file\", dbConnectionPool), false, false);\n                    CoreNode core = Builder.buildCorenode(a, storage, null, rawPointers, pointers, null,\n                            null, null, quota, null, null, null, null, crypto);\n                    core.initialize(false);\n                    storage.setPki(core);\n\n                    // set quota to 0\n                    quota.removeQuota(username);\n                    Set<PublicKeyHash> fromUsage = usageStore.getAllWriters(username);\n\n                    // get and verify all owned keys, then set them all to empty targets, children first\n                    Optional<PublicKeyHash> idOpt = core.getPublicKeyHash(username).join();\n                    if (idOpt.isEmpty()) {\n                        for (PublicKeyHash w : fromUsage) {\n                            rawPointers.removePointer(w);\n                        }\n                        usageStore.removeUser(username);\n                        System.out.println(\"User \" + username + \" doesn't exist\");\n                        return true;\n                    }\n                    PublicKeyHash id = idOpt.get();\n                    try {\n                        deleteOwnedKeys(id, pointers, rawPointers, storage, crypto);\n                    } catch (Exception e) {}\n                    for (PublicKeyHash w : fromUsage) {\n                        rawPointers.removePointer(w);\n                    }\n                    // Now delete all usage roots\n                    usageStore.removeUser(username);\n                    System.out.println(\"Finished deleting user \" + username);\n                    return true;\n                } catch (SQLException sqe) {\n                    throw new RuntimeException(sqe);\n                }\n            },\n            Arrays.asList(\n                    ARG_USERNAME,\n                    new Command.Arg(\"log-to-file\", \"Whether to log to a file\", true, \"false\"),\n                    new Command.Arg(\"print-log-location\", \"Whether to print log location\", true, \"false\")\n            )\n    );\n\n    public static final Command<Boolean> STATS = new Command<>(\"stats\",\n            \"Print statistics for storage\",\n            a -> {\n                Builder.disableLog();\n                IpfsCoreNode.disableLog();\n                ScryptJava.disableLog();\n                HTTPCoreNode.disableLog();\n\n                BlockMetadataStore meta = Builder.buildBlockMetadata(a);\n\n                AtomicLong cborSize = new AtomicLong(0);\n                AtomicLong cborCount = new AtomicLong(0);\n                AtomicLong rawSize = new AtomicLong(0);\n                AtomicLong rawCount = new AtomicLong(0);\n                meta.applyToAllSizes((c, s) -> {\n                    if (c.isRaw()) {\n                        rawCount.incrementAndGet();\n                        rawSize.addAndGet(s);\n                    } else {\n                        cborCount.incrementAndGet();\n                        cborSize.addAndGet(s);\n                    }\n                });\n\n                System.out.println(\"Cbor blocks: \" + cborCount.get() + \", raw blocks: \" + rawCount.get() +\n                        \", cbor size: \" + cborSize.get() + \", raw size: \" + rawSize.get());\n                return true;\n            },\n            Arrays.asList(),\n            Arrays.asList()\n    );\n\n    public static final Command<Boolean> GENERATE = new Command<>(\"generate\",\n            \"Generate a new BAT to use with -instance-bat on a server to mirror user data on another server\",\n            a -> {\n                Crypto crypto = Main.initCrypto();\n                Bat bat = Bat.random(crypto.random);\n                BatId id = bat.calculateId(crypto.hasher).join();\n                BatWithId both = new BatWithId(bat, id.id);\n                String arg = both.encode();\n                System.out.println(\"Generate new BAT and id: \" + arg);\n                return true;\n            },\n            Arrays.asList(),\n            Arrays.asList()\n    );\n\n    public static final Command<Boolean> BAT = new Command<>(\"bat\",\n            \"BAT utilities\",\n            a -> true,\n            Arrays.asList(),\n            Arrays.asList(GENERATE)\n    );\n\n    public static final Command<Boolean> STORAGE = new Command<>(\"storage\",\n            \"Operations on storage\",\n            a -> true,\n            Arrays.asList(),\n            Arrays.asList(STATS)\n    );\n\n    public static final Command<Boolean> SERVER_ADMIN = new Command<>(\"admin\",\n            \"Manage users on this server\",\n            args -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Arrays.asList(),\n            Arrays.asList(BAT, STORAGE, MFA, DELETE)\n    );\n}\n"
  },
  {
    "path": "src/peergos/server/ServerIdentity.java",
    "content": "package peergos.server;\n\nimport com.google.protobuf.InvalidProtocolBufferException;\nimport io.libp2p.core.*;\nimport io.libp2p.core.crypto.*;\nimport io.libp2p.crypto.keys.*;\nimport org.peergos.protocol.ipns.*;\nimport org.peergos.protocol.ipns.pb.Ipns;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.resolution.*;\nimport peergos.shared.storage.IpnsEntry;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.nio.ByteBuffer;\nimport java.nio.charset.*;\nimport java.time.*;\nimport java.util.*;\n\npublic class ServerIdentity extends Builder {\n    public static final Command.Arg ARG_SERVERIDS_SQL_FILE =\n            new Command.Arg(\"serverids-file\", \"The filename for the server ids datastore\", false, \"serverids.sql\");\n    public static final Command.Arg ARG_PRIVATE_KEY =\n            new Command.Arg(\"ipfs.identity.priv-key\", \"Basse64 encoded server identity private key protobuf\", true);\n    public static final Command<Boolean> GEN_NEXT = new Command<>(\"gen-next\",\n            \"Generate the next identity of this server. This will allow you to recover if the server is \" +\n                    \"compromised and its identity keypair is stolen. You will be given a password which can be used to \" +\n                    \"regenerate the next server identity and rotate to it.\",\n            a -> {\n                boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n                SqlSupplier sqlCommands = usePostgres ?\n                        new PostgresCommands() :\n                        new SqliteCommands();\n\n                JdbcServerIdentityStore idstore = JdbcServerIdentityStore.build(getDBConnector(a, \"serverids-file\"), sqlCommands, Main.initCrypto());\n                List<PeerId> ids = idstore.getIdentities();\n                PeerId current = ids.get(ids.size() - 1);\n                byte[] currentRecord = idstore.getRecord(current);\n                Optional<IpnsMapping> ipnsMapping = IPNS.parseAndValidateIpnsEntry(\n                        ArrayOps.concat(\"/ipns/\".getBytes(StandardCharsets.UTF_8), current.getBytes()),\n                        currentRecord);\n                if (ipnsMapping.isEmpty())\n                    throw new IllegalStateException(\"Invalid record!\");\n                IpnsEntry entry = new IpnsEntry(ipnsMapping.get().getSignature(), ipnsMapping.get().getData());\n                ResolutionRecord res = entry.getValue();\n                boolean hasNextId = res.host.isPresent();\n                if (hasNextId) {\n                    System.out.println(\"This server has already generated a next identity\");\n                    return true;\n                }\n                Crypto crypto = Main.initCrypto();\n                String password = Passwords.generate();\n                System.out.println(\"Your password for the next server identity is the following. \" +\n                        \"Please write this down and store it in a safe place. you will need this to recover your server \" +\n                        \"identity after a compromise.\");\n                System.out.println(password);\n                System.out.println(\"I have written down the password (Y/N)\");\n                String yes = System.console().readLine();\n                if (!yes.equalsIgnoreCase(\"y\")) {\n                    System.out.println(\"Aborting next identity generation\");\n                    return true;\n                }\n                System.out.println(\"Generating next identity...\");\n                // update ipns record with next peer id\n                PrivKey nextPrivate = generateNextIdentity(password, current, crypto);\n                PeerId nextPeerId = PeerId.fromPubKey(nextPrivate.publicKey());\n\n                PrivKey currentPrivate = KeyKt.unmarshalPrivateKey(Base64.getDecoder().decode(a.getArg(\"ipfs.identity.priv-key\")));\n                idstore.setRecord(current, generateSignedIpnsRecord(currentPrivate, Optional.of(Multihash.decode(nextPeerId.getBytes())), false, res.sequence + 1));\n                System.out.println(\"The next server identity will be \" + nextPeerId.toBase58());\n\n                return true;\n            },\n            Arrays.asList(\n                    ARG_SERVERIDS_SQL_FILE,\n                    ARG_PRIVATE_KEY\n            )\n    );\n\n    public static byte[] generateSignedIpnsRecord(PrivKey peerPrivate, Optional<Multihash> host, boolean moved, long sequence) {\n        int years = 1;\n        LocalDateTime expiry = LocalDateTime.now().plusYears(years);\n        long ttlNanos = years * 365L * 24 * 3600 * 1000_000_000;\n        ResolutionRecord ipnsValue = new ResolutionRecord(host,\n                moved, Optional.empty(), sequence);\n        byte[] rr = ipnsValue.serialize();\n        return IPNS.createSignedRecord(\"/ipfs/bafkqaaa\".getBytes(StandardCharsets.UTF_8), expiry, sequence, ttlNanos,\n                Optional.of(IpnsEntry.RESOLUTION_RECORD_IPNS_SUFFIX), Optional.of(org.peergos.cbor.CborObject.fromByteArray(rr)), peerPrivate);\n    }\n\n    public static PrivKey generateNextIdentity(String password, PeerId current, Crypto crypto) {\n        SecretGenerationAlgorithm alg = SecretGenerationAlgorithm.getDefaultWithoutExtraSalt();\n        byte[] keyBytes = crypto.hasher.hashToKeyBytes(current.toBase58(), password, alg).join();\n        byte[] sk = new byte[64];\n        byte[] pk = new byte[32];\n        System.arraycopy(keyBytes, 0, sk, 32, 32);\n        crypto.signer.crypto_sign_keypair(pk, sk);\n\n        // update ipns record with next peer id\n        byte[] privateProto = new byte[36];\n        System.arraycopy(sk, 32, privateProto, 4, 32);\n        // protobuf header\n        System.arraycopy(new byte[]{8, 1, 18, 32}, 0, privateProto, 0, 4);\n        return Ed25519Kt.unmarshalEd25519PrivateKey(privateProto);\n    }\n\n    public static final Command<Boolean> ROTATE = new Command<>(\"rotate\",\n            \"Rotate the identity of this server. If this server has already generated a next identity,\" +\n                    \"you will need the associated password, otherwise a new key pair will be generated.\",\n            a -> {\n                boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n                SqlSupplier sqlCommands = usePostgres ?\n                        new PostgresCommands() :\n                        new SqliteCommands();\n\n                JdbcServerIdentityStore idstore = JdbcServerIdentityStore.build(getDBConnector(a, \"serverids-file\"), sqlCommands, Main.initCrypto());\n                List<PeerId> ids = idstore.getIdentities();\n                if (ids.isEmpty())\n                    throw new IllegalStateException(\"Please run Peergos once before trying to rotate the server identity!\");\n                PeerId current = ids.get(ids.size() - 1);\n                byte[] currentRecord = idstore.getRecord(current);\n                Optional<IpnsMapping> ipnsMapping = IPNS.parseAndValidateIpnsEntry(\n                        ArrayOps.concat(\"/ipns/\".getBytes(StandardCharsets.UTF_8), current.getBytes()),\n                        currentRecord);\n                if (ipnsMapping.isEmpty())\n                    throw new IllegalStateException(\"Invalid record!\");\n                IpnsEntry ipnsEntry = new IpnsEntry(ipnsMapping.get().getSignature(), ipnsMapping.get().getData());\n                Crypto crypto = Main.initCrypto();\n                ResolutionRecord res = ipnsEntry.getValue(Multihash.decode(ipnsMapping.get().publisher.toBytes()), crypto).join();\n                PrivKey nextPriv;\n                if (res.host.isPresent()) {\n                    // require password to regenerate next identity\n                    System.out.println(\"Please enter password for next identity:\");\n                    String password = System.console().readLine();\n                    nextPriv = generateNextIdentity(password, current, crypto);\n                } else {\n                    // generate a new identity now\n                    nextPriv = Ed25519Kt.generateEd25519KeyPair().getFirst();\n                }\n                PeerId nextPeerId = PeerId.fromPubKey(nextPriv.publicKey());\n                PrivKey currentPrivate = KeyKt.unmarshalPrivateKey(Base64.getDecoder().decode(a.getArg(\"ipfs.identity.priv-key\")));\n                idstore.setPrivateKey(currentPrivate);\n                // update peergos config with new private key\n                String encodedPrivate = Base64.getEncoder().encodeToString(nextPriv.bytes());\n                boolean useIPFS = a.getBoolean(\"useIPFS\", false);\n                a.with(\"ipfs.identity.priv-key\", encodedPrivate)\n                        .with(\"ipfs.identity.peerid\", nextPeerId.toBase58())\n                        .saveToFile();\n\n                idstore.setRecord(current, generateSignedIpnsRecord(currentPrivate, Optional.of(Multihash.decode(nextPeerId.getBytes())), true, res.sequence + 1));\n                idstore.addIdentity(nextPeerId, generateSignedIpnsRecord(nextPriv, Optional.empty(), false, 1));\n                idstore.setPrivateKey(nextPriv);\n                System.out.println(\"Successfully rotated server identity from \" + current + \" to \" + nextPeerId);\n                if (!useIPFS) {\n                    System.out.println(\"You are running a multi-server instance, copy the new identity to the ipfs server start script: \\n\" +\n                            \"-ipfs.identity.peerid \" + nextPeerId.toBase58() +\n                            \" -ipfs.identity.priv-key \" + Base64.getEncoder().encodeToString(nextPriv.bytes()));\n                }\n\n                return true;\n            },\n            Arrays.asList(\n                    ARG_SERVERIDS_SQL_FILE,\n                    ARG_PRIVATE_KEY\n            )\n    );\n\n    public static final Command<Boolean> SERVER_IDENTITY = new Command<>(\"server-identity\",\n            \"Manage the identity of this server\",\n            args -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Arrays.asList(),\n            Arrays.asList(GEN_NEXT, ROTATE)\n    );\n}\n"
  },
  {
    "path": "src/peergos/server/ServerMessages.java",
    "content": "package peergos.server;\n\nimport peergos.server.corenode.*;\nimport peergos.server.login.*;\nimport peergos.server.messages.*;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.storage.auth.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class ServerMessages extends Builder {\n\n    public static final Command<Boolean> SHOW = new Command<>(\"show\",\n            \"Show messages with a user of this server\",\n            a -> {\n                boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n                SqlSupplier sqlCommands = usePostgres ?\n                        new PostgresCommands() :\n                        new SqliteCommands();\n                ServerMessageStore store = new ServerMessageStore(getDBConnector(a, \"server-messages-sql-file\"),\n                        sqlCommands, null, null);\n                List<ServerMessage> messages = store.getMessages(a.getArg(\"username\"));\n                List<ServerConversation> conversations = ServerConversation.combine(messages);\n                for (ServerConversation conv : conversations) {\n                    for (ServerMessage msg : conv.messages) {\n                        System.out.println(msg.summary());\n                        System.out.println(msg.contents);\n                    }\n                    System.out.println();\n                }\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"username\", \"Peergos username\", true),\n                    new Command.Arg(\"server-messages-sql-file\", \"The filename for the server messages datastore\", true, \"server-messages.sql\")\n            )\n    );\n\n    public static final Command<Boolean> SEND = new Command<>(\"send\",\n            \"Send a message to a user of this server\",\n            a -> {\n                boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n                SqlSupplier sqlCommands = usePostgres ?\n                        new PostgresCommands() :\n                        new SqliteCommands();\n                ServerMessageStore store = new ServerMessageStore(getDBConnector(a, \"server-messages-sql-file\"),\n                        sqlCommands, null, null);\n                String message;\n                if (a.hasArg(\"msg-file\")) {\n                    try {\n                        message = Files.readString(PathUtil.get(a.getArg(\"msg-file\")));\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                } else\n                    message = a.getArg(\"msg\");\n                ServerMessage msg = new ServerMessage(-1, ServerMessage.Type.FromServer, System.currentTimeMillis(),\n                        message, a.getOptionalArg(\"reply-to\").map(Long::parseLong), false);\n                if (a.hasArg(\"usernames\")) {\n                    try {\n                        List<String> usernames = Files.readAllLines(PathUtil.get(a.getArg(\"usernames\")));\n                        for (String username : usernames) {\n                            store.addMessage(username, msg);\n                            System.out.println(\"Message sent to \" + username);\n                        }\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                } else {\n                    String username = a.getArg(\"username\");\n                    store.addMessage(username, msg);\n                    System.out.println(\"Message sent to \" + username);\n                }\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"usernames\", \"Path to file containing usernames, one per line\", false),\n                    new Command.Arg(\"username\", \"Peergos username\", false),\n                    new Command.Arg(\"reply-to\", \"Message id to reply to\", false),\n                    new Command.Arg(\"msg\", \"Message to send\", false),\n                    new Command.Arg(\"msg-file\", \"File containing message to send\", false),\n                    new Command.Arg(\"server-messages-sql-file\", \"The filename for the server messages datastore\", true, \"server-messages.sql\")\n            )\n    );\n\n    private static QuotaAdmin buildQuotaStore(Args a) {\n        Supplier<Connection> dbConnectionPool = getDBConnector(a, \"transactions-sql-file\");\n        TransactionStore transactions = buildTransactionStore(a, dbConnectionPool);\n        Crypto crypto = Main.initCrypto();\n        Hasher hasher = crypto.hasher;\n        BlockRequestAuthoriser blockRequestAuthoriser = (b, d, s, auth) -> Futures.of(true); // not relevant for local only use here\n        try {\n            BlockMetadataStore metaDB = buildBlockMetadata(a);\n            SqlSupplier cmds = getSqlCommands(a);\n            JdbcServerIdentityStore ids = JdbcServerIdentityStore.build(getDBConnector(a, \"serverids-file\", dbConnectionPool), cmds, crypto);\n            UsageStore usageStore = new JdbcUsageStore(getDBConnector(a, \"space-usage-sql-file\", dbConnectionPool), cmds);\n            JdbcIpnsAndSocial rawPointers = buildRawPointers(a, getDBConnector(a, \"mutable-pointers-file\", dbConnectionPool));\n            DeletableContentAddressedStorage localStorage = buildLocalStorage(a, metaDB, null, transactions,\n                    blockRequestAuthoriser, ids, usageStore, rawPointers, null, hasher);\n            MutablePointers localPointers = UserRepository.build(localStorage, rawPointers, hasher);\n            MutablePointersProxy proxingMutable = new HttpMutablePointers(buildP2pHttpProxy(a), getPkiServerId(a));\n            JdbcIpnsAndSocial rawSocial = new JdbcIpnsAndSocial(getDBConnector(a, \"social-sql-file\", dbConnectionPool), cmds);\n            JdbcAccount account = new JdbcAccount(getDBConnector(a, \"account-sql-file\", dbConnectionPool),\n                    cmds, new com.webauthn4j.data.client.Origin(\"http://localhost:8000\"), \"localhost\");\n            QuotaAdmin quotas = buildSpaceQuotas(a, localStorage,\n                    getDBConnector(a, \"space-requests-sql-file\", dbConnectionPool),\n                    getDBConnector(a, \"quotas-sql-file\", dbConnectionPool), false, true);\n            CoreNode core = buildCorenode(a, localStorage, transactions, rawPointers, localPointers, proxingMutable,\n                    rawSocial, usageStore, quotas, account, null, new AccountWithStorage(localStorage, localPointers, account), null, crypto);\n            quotas.setPki(core);\n            return quotas;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static final Command<Boolean> NEW = new Command<>(\"new\",\n            \"Show new or recent messages from users of this server\",\n            a -> {\n                ServerMessageStore store = new ServerMessageStore(getDBConnector(a, \"server-messages-sql-file\"),\n                        getSqlCommands(a), null, null);\n\n                if (a.hasArg(\"after\")) {\n                    List<Pair<String, ServerMessage>> after = store.getMessagesAfter(LocalDate.parse(a.getArg(\"after\")).atStartOfDay());\n                    for (Pair<String, ServerMessage> p : after) {\n                        ServerMessage msg = p.right;\n                        System.out.println(\"==================================================\");\n                        System.out.println(\"User: \" + p.left);\n                        System.out.println(msg.summary());\n                        System.out.println(msg.contents);\n                    }\n                    return true;\n                }\n\n                QuotaAdmin quotas = buildQuotaStore(a);\n\n                List<String> usernames = quotas.getLocalUsernames();\n                for (String username : usernames) {\n                    List<ServerMessage> all = store.getMessages(username);\n                    List<ServerConversation> allConvs = ServerConversation.combine(all);\n                    List<ServerConversation> withReply = allConvs.stream()\n                            .filter(c -> c.lastMessage().type == ServerMessage.Type.FromUser)\n                            .collect(Collectors.toList());\n                    if (! withReply.isEmpty()) {\n                        System.out.println(\"==================================================\");\n                        System.out.println(\"Replies from \" + username);\n                    }\n                    for (ServerConversation conv : withReply) {\n                        ServerMessage last = conv.lastMessage();\n                        System.out.println(last.summary());\n                        System.out.println(last.contents);\n                    }\n                }\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"after\", \"The date after which to show messages from (YYYY-MM-DD)\", false),\n                    new Command.Arg(\"server-messages-sql-file\", \"The filename for the server messages datastore\", true, \"server-messages.sql\")\n            )\n    );\n\n    public static final Command<Boolean> NEW_USERS = new Command<>(\"new-users\",\n            \"Show new users of this server (that haven't been sent a welcome message)\",\n            a -> {\n                ServerMessageStore store = new ServerMessageStore(getDBConnector(a, \"server-messages-sql-file\"),\n                        getSqlCommands(a), null, null);\n                QuotaAdmin quotas = buildQuotaStore(a);\n\n                List<String> usernames = quotas.getLocalUsernames();\n                for (String username : usernames) {\n                    List<ServerMessage> all = store.getMessages(username);\n                    if (all.stream().filter(m -> m.type == ServerMessage.Type.FromServer).findAny().isEmpty()) {\n                        System.out.println(username);\n                    }\n                }\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"server-messages-sql-file\", \"The filename for the server messages datastore\", true, \"server-messages.sql\")\n            )\n    );\n\n    public static final Command<Boolean> ACTIVE_USERS = new Command<>(\"active-users\",\n            \"Show users of this server that are active\",\n            a -> {\n                ServerMessageStore store = new ServerMessageStore(getDBConnector(a, \"server-messages-sql-file\"),\n                        getSqlCommands(a), null, null);\n                QuotaAdmin quotas = buildQuotaStore(a);\n\n                List<String> usernames = quotas.getLocalUsernames();\n                LocalDateTime monthAgo = LocalDateTime.now().minusMonths(1);\n                for (String username : usernames) {\n                    List<ServerMessage> all = store.getMessages(username);\n                    List<ServerConversation> allConvs = ServerConversation.combine(all);\n                    boolean recent = allConvs.stream()\n                            .anyMatch(c -> c.messages.stream().anyMatch(m ->\n                                    m.type == ServerMessage.Type.FromUser &&\n                                            m.getSendTime().isAfter(monthAgo)));\n                    if (recent)\n                        System.out.println(username);\n                }\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"server-messages-sql-file\", \"The filename for the server messages datastore\", true, \"server-messages.sql\")\n            )\n    );\n\n    public static final Command<Boolean> SERVER_MESSAGES = new Command<>(\"server-msg\",\n            \"Send and receive messages to/from users of this server\",\n            args -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"print-log-location\", \"Whether to print the log file location at startup\", false, \"false\"),\n                    new Command.Arg(\"log-to-file\", \"Whether to log to a file\", false, \"false\"),\n                    new Command.Arg(\"log-to-console\", \"Whether to log to the console\", false, \"false\")\n            ),\n            Arrays.asList(SHOW, SEND, NEW, NEW_USERS, ACTIVE_USERS)\n    );\n}\n"
  },
  {
    "path": "src/peergos/server/ServerProcesses.java",
    "content": "package peergos.server;\n\nimport peergos.server.storage.*;\n\npublic class ServerProcesses {\n\n    public final UserService localApi, p2pApi;\n    public final IpfsWrapper ipfs;\n\n    public ServerProcesses(UserService localApi, UserService p2pApi, IpfsWrapper ipfs) {\n        this.localApi = localApi;\n        this.p2pApi = p2pApi;\n        this.ipfs = ipfs;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/SyncProperties.java",
    "content": "package peergos.server;\n\nimport peergos.server.sync.SyncConfig;\nimport peergos.server.sync.SyncRunner;\nimport peergos.shared.util.Either;\n\nimport java.nio.file.Path;\n\npublic class SyncProperties {\n\n    public final SyncConfig config;\n    public final Path peergosDir;\n    public final SyncRunner syncer;\n    public final Either<HostDirEnumerator, HostDirChooser> hostDirs;\n\n    public SyncProperties(SyncConfig config, Path peergosDir, SyncRunner syncer, Either<HostDirEnumerator, HostDirChooser> hostDirs) {\n        this.config = config;\n        this.peergosDir = peergosDir;\n        this.syncer = syncer;\n        this.hostDirs = hostDirs;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/UserCleanup.java",
    "content": "package peergos.server;\n\nimport peergos.server.storage.*;\nimport peergos.server.util.Args;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.Console;\nimport java.net.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class UserCleanup {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.empty(), Optional.empty()).get();\n        Args a = Args.parse(args);\n        String username = a.getArg(\"username\");\n        Console console = System.console();\n        String password;\n        if (a.hasArg(\"PEERGOS_PASSWORD\"))\n            password = a.getArg(\"PEERGOS_PASSWORD\");\n        else\n            password = new String(console.readPassword(\"Enter password for \" + username + \":\"));\n        UserContext context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).get();\n        long usage = context.getSpaceUsage(false).join();\n//        checkRawUsage(context);\n        clearUnreachableChampNodes(context);\n    }\n\n    public static void clearUnreachableChampNodes(UserContext c) {\n        //  First clear any failed uploads\n//        c.cleanPartialUploads(t -> true).join();\n\n        ContentAddressedStorage storage = c.network.dhtClient;\n        MutablePointers mutable = c.network.mutable;\n        Hasher hasher = c.crypto.hasher;\n        PublicKeyHash owner = c.signer.publicKeyHash;\n        FileWrapper root = c.getUserRoot().join();\n        List<Multihash> hosts = c.network.coreNode.getStorageProviders(owner);\n        Set<PublicKeyHash> writers = DeletableContentAddressedStorage.getOwnedKeysRecursive(owner, owner, mutable,\n                        (h, s) -> ContentAddressedStorage.getWriterData(owner, h, s, storage), storage, hasher).join()\n                .stream()\n                .filter(w ->  !w.equals(owner))\n                .collect(Collectors.toSet());\n\n        Set<PublicKeyHash> namedOwnedKeys = WriterData.getWriterData(owner, owner, mutable, storage).join()\n                .props.get().namedOwnedKeys.values().stream()\n                .map(p -> p.ownedKey)\n                .collect(Collectors.toSet());\n\n        Map<PublicKeyHash, PublicKeyHash> toParent = new HashMap<>();\n        for (PublicKeyHash writer : writers) {\n            Set<PublicKeyHash> owned = DeletableContentAddressedStorage.getDirectOwnedKeys(owner, writer, mutable,\n                    (h, s) -> ContentAddressedStorage.getWriterData(owner, h, s, storage), storage, hasher).join();\n            for (PublicKeyHash child : owned) {\n                toParent.put(child, writer);\n            }\n        }\n\n        Map<SigningPrivateKeyAndPublicHash, Map<String, ByteArrayWrapper>> reachableKeys = new HashMap<>();\n        BatWithId mirrorBat = c.getMirrorBat().join().get();\n        traverseDescendants(root, \"/\" + c.username, (s, cap, p, fopt) -> {\n            reachableKeys.putIfAbsent(s, new HashMap<>());\n            reachableKeys.get(s).put(p, new ByteArrayWrapper(cap.getMapKey()));\n            fopt.ifPresent(f -> {\n                RetrievedCapability rcap = f.getPointer();\n                int nBats = f.getPointer().fileAccess.bats.size();\n                boolean addToFragmentsOnly = rcap.capability.bat.isEmpty() || nBats == 2;\n                System.out.println(p);\n                f.addMirrorBat(mirrorBat.id(), addToFragmentsOnly, c.network).join();\n            });\n            return true;\n        }, c);\n\n        // handle each writing space separately\n        for (PublicKeyHash writer : writers) {\n            Map<ByteArrayWrapper, CborObject.CborMerkleLink> allKeys = new HashMap<>();\n            Set<ByteArrayWrapper> emptyKeys = new HashSet<>();\n            CommittedWriterData cwd = null;\n            try {\n                cwd = WriterData.getWriterData(owner, writer, mutable, storage).join();\n            } catch (Exception e) {\n                continue;\n            }\n            CommittedWriterData wd = cwd;\n            ChampWrapper<CborObject.CborMerkleLink> champ = ChampWrapper.create(owner, (Cid) wd.props.get().tree.get(),\n                    Optional.empty(), x -> Futures.of(x.data), storage, hasher, b -> (CborObject.CborMerkleLink) b).join();\n            champ.applyToAllMappings(owner, p -> {\n                if (p.right.isPresent())\n                    allKeys.put(p.left, p.right.get());\n                else\n                    emptyKeys.add(p.left);\n                return Futures.of(true);\n            }).join();\n\n            Set<ByteArrayWrapper> unreachableKeys = new HashSet<>(allKeys.keySet());\n            Optional<SigningPrivateKeyAndPublicHash> keypair = reachableKeys.keySet()\n                    .stream()\n                    .filter(s -> s.publicKeyHash.equals(writer))\n                    .findFirst();\n            if (keypair.isEmpty()) {\n                if (namedOwnedKeys.contains(writer))\n                    continue;\n                // writing space is unreachable, but non-empty. Remove it by orphaning it.\n                PublicKeyHash parent = toParent.get(writer);\n                Optional<SigningPrivateKeyAndPublicHash> parentKeypair = reachableKeys.keySet()\n                    .stream()\n                    .filter(s -> s.publicKeyHash.equals(parent))\n                    .findFirst();\n                if (parentKeypair.isEmpty())\n                    continue;\n                CommittedWriterData pwd = WriterData.getWriterData(owner, parent, mutable, storage).join();\n                c.network.synchronizer.applyComplexComputation(owner, parentKeypair.get(), (s, comm) -> {\n                    TransactionId tid = storage.startTransaction(owner).join();\n                    WriterData newPwd = pwd.props.get().removeOwnedKey(owner, parentKeypair.get(), writer, storage, hasher).join();\n                    Snapshot updated = comm.commit(owner, parentKeypair.get(), newPwd, pwd, tid).join();\n                    storage.closeTransaction(owner, tid).join();\n                    return Futures.of(new Pair<>(updated, true));\n                }).join();\n\n                continue;\n            }\n            SigningPrivateKeyAndPublicHash signer = keypair.get();\n            unreachableKeys.removeAll(reachableKeys.get(signer).values());\n\n            if (! emptyKeys.isEmpty()) {\n                c.network.synchronizer.applyComplexComputation(owner, signer, (s, comm) -> {\n                    TransactionId tid = storage.startTransaction(owner).join();\n                    WriterData current = wd.props.get();\n\n                    for (ByteArrayWrapper key : emptyKeys) {\n                        current = c.network.tree.remove(current, owner, signer, key.data, MaybeMultihash.empty(), tid).join();\n                    }\n                    Snapshot updated = comm.commit(owner, signer, current, wd, tid).join();\n                    storage.closeTransaction(owner, tid).join();\n                    return Futures.of(new Pair<>(updated, true));\n                }).join();\n            }\n\n            if (unreachableKeys.isEmpty())\n                continue;\n\n            c.network.synchronizer.applyComplexComputation(owner, signer, (s, comm) -> {\n                TransactionId tid = storage.startTransaction(owner).join();\n                WriterData current = wd.props.get();\n\n                for (ByteArrayWrapper key : unreachableKeys) {\n                    current = c.network.tree.remove(current, owner, signer, key.data, MaybeMultihash.of(allKeys.get(key).target), tid).join();\n                }\n                Snapshot updated = comm.commit(owner, signer, current, wd, tid).join();\n                storage.closeTransaction(owner, tid).join();\n                return Futures.of(new Pair<>(updated, true));\n            }).join();\n        }\n    }\n\n    public static void checkRawUsage(UserContext c) {\n        PublicKeyHash owner = c.signer.publicKeyHash;\n        long serverCalculatedUsage = c.getSpaceUsage(false).join();\n        Optional<BatWithId> mirror = c.getMirrorBat().join();\n        Set<PublicKeyHash> writers = DeletableContentAddressedStorage.getOwnedKeysRecursive(owner, owner, c.network.mutable,\n                (h, s) -> ContentAddressedStorage.getWriterData(owner, h, s, c.network.dhtClient), c.network.dhtClient, c.crypto.hasher).join();\n        checkRawUsage(owner, writers, mirror, serverCalculatedUsage, c.network.dhtClient, c.network.mutable);\n    }\n\n    public static void checkRawUsage(PublicKeyHash owner,\n                                     Set<PublicKeyHash> writers,\n                                     Optional<BatWithId> mirror,\n                                     long serverCalculatedUsage,\n                                     ContentAddressedStorage storage,\n                                     MutablePointers mutable) {\n        Map<Cid, Long> blockSizes = new HashMap<>();\n        Map<Cid, List<Cid>> linkedFrom = new HashMap<>();\n\n        for (PublicKeyHash writer : writers) {\n            getAllBlocksWithSize(owner, (Cid) mutable.getPointerTarget(owner, writer, storage).join().updated.get(),\n                    mirror, storage, blockSizes, linkedFrom);\n        }\n\n        long totalFromBlocks = blockSizes.values().stream().mapToLong(i -> i).sum();\n        if (totalFromBlocks != serverCalculatedUsage)\n            throw new IllegalStateException(\"Incorrect usage! Expected: \" + serverCalculatedUsage + \", actual: \" + totalFromBlocks);\n    }\n\n    private static void getAllBlocksWithSize(PublicKeyHash owner,\n                                             Cid root,\n                                             Optional<BatWithId> mirror,\n                                             ContentAddressedStorage dht,\n                                             Map<Cid,  Long> res,\n                                             Map<Cid, List<Cid>> linkedFrom) {\n        Optional<byte[]> raw = dht.getRaw(owner, root, mirror).join();\n        if (raw.isEmpty())\n            return;\n        byte[] block = raw.get();\n        res.put(root, (long) block.length);\n        if (! root.isRaw()) {\n            List<Cid> children = CborObject.fromByteArray(block).links().stream().map(c -> (Cid) c).collect(Collectors.toList());\n            for (Cid child : children) {\n                if (child.isIdentity())\n                    continue;\n                linkedFrom.putIfAbsent(child, new ArrayList<>());\n                linkedFrom.get(child).add(root);\n                getAllBlocksWithSize(owner, child, mirror, dht, res, linkedFrom);\n            }\n        }\n    }\n\n    interface ChunkProcessor {\n        boolean apply(SigningPrivateKeyAndPublicHash signer, AbsoluteCapability cap, String path, Optional<FileWrapper> f);\n    }\n\n    private static void traverseDescendants(FileWrapper dir,\n                                            String path,\n                                            ChunkProcessor visitor,\n                                            UserContext c) {\n        visitor.apply(dir.signingPair(), dir.writableFilePointer(), path, Optional.of(dir));\n        Set<FileWrapper> children = dir.getChildren(c.crypto.hasher, c.network).join();\n        for (FileWrapper child : children) {\n            if (! child.isDirectory()) {\n                WritableAbsoluteCapability cap = child.writableFilePointer();\n                byte[] firstChunk = cap.getMapKey();\n                Optional<Bat> firstBat = cap.bat;\n                SigningPrivateKeyAndPublicHash childSigner = child.signingPair();\n                visitor.apply(childSigner, cap, path + \"/\" + child.getName(), Optional.of(child));\n                for (int i=0; i < child.getSize()/ (5*1024*1024); i++) {\n                    byte[] streamSecret = child.getFileProperties().streamSecret.get();\n                    Pair<byte[], Optional<Bat>> chunk = FileProperties.calculateMapKey(streamSecret, firstChunk,\n                            firstBat, 5 * 1024 * 1024 * (i + 1), c.crypto.hasher).join();\n                    visitor.apply(childSigner, cap.withMapKey(chunk.left, chunk.right), path + \"/\" + child.getName() + \"[\" + i + \"]\", Optional.empty());\n                }\n            } else\n                traverseDescendants(child, path + \"/\" + child.getName(), visitor, c);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/UserService.java",
    "content": "package peergos.server;\nimport java.util.*;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.logging.Logger;\n\nimport peergos.server.storage.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.util.*;\n\nimport java.util.logging.Level;\n\nimport com.sun.net.httpserver.*;\nimport peergos.shared.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\n\nimport peergos.server.net.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport javax.net.ssl.*;\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.security.*;\nimport java.security.cert.*;\n\npublic class UserService {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    public static final Version CURRENT_VERSION = Version.parse(\"1.25.0\");\n    public static final String UI_URL = \"/\";\n\n    private static void initTLS() {\n        // disable weak algorithms\n        LOG.info(\"\\nInitial security properties:\");\n        printSecurityProperties();\n\n        // The ECDH and RSA key exchange algorithms are disabled because they don't provide forward secrecy\n        Security.setProperty(\"jdk.tls.disabledAlgorithms\",\n                \"SSLv3, TLSv1.3, RC4, MD2, MD4, MD5, SHA1, DES, DSA, MD5withRSA, DH, RSA keySize < 2048, EC keySize < 224, 3DES_EDE_CBC, \" +\n                \"TLS_RSA_WITH_NULL_SHA256,\" +\n                \"TLS_RSA_WITH_AES_128_GCM_SHA256,\" +\n                \"TLS_RSA_WITH_AES_128_CBC_SHA256, \" +\n                \"TLS_RSA_WITH_AES_256_GCM_SHA384, \" +\n                \"TLS_RSA_WITH_AES_256_CBC_SHA256, \" +\n                \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, \" +\n                \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, \" +\n                \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256, \" +\n                \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384,\" +\n                \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384,\" +\n                \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256, \" +\n                \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384,\" +\n                \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384,\" +\n                \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,\" +\n                \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,\" +\n                \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,\" +\n                \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,\"\n        );\n        Security.setProperty(\"jdk.certpath.disabledAlgorithms\",\n                \"RC4, MD2, MD4, MD5, SHA1, DSA, RSA keySize < 2048, EC keySize < 224\");\n        Security.setProperty(\"jdk.tls.rejectClientInitializedRenegotiation\", \"true\");\n\n        LOG.info(\"\\nUpdated security properties:\");\n        printSecurityProperties();\n\n        Security.setProperty(\"jdk.tls.ephemeralDHKeySize\", \"2048\");\n    }\n\n    static void printSecurityProperties() {\n        LOG.info(\"jdk.tls.disabledAlgorithms: \" + Security.getProperty(\"jdk.tls.disabledAlgorithms\"));\n        LOG.info(\"jdk.certpath.disabledAlgorithms: \" + Security.getProperty(\"jdk.certpath.disabledAlgorithms\"));\n        LOG.info(\"jdk.tls.rejectClientInitializedRenegotiation: \"+Security.getProperty(\"jdk.tls.rejectClientInitializedRenegotiation\"));\n    }\n\n    public final ContentAddressedStorage storage;\n    public final BatCave bats;\n    public final Crypto crypto;\n    public final CoreNode coreNode;\n    public final Account account;\n    public final SocialNetwork social;\n    public final MutablePointers mutable;\n    public final InstanceAdmin controller;\n    public final SpaceUsage usage;\n    public final ServerMessager serverMessages;\n    public final GarbageCollector gc; // not exposed\n    private final Optional<BlockCache> blockCache;\n    private final Optional<SyncProperties> syncProps;\n    private final Optional<LocalAppProperties> localAppProps;\n    private HttpServer localhostServer;\n\n    public UserService(ContentAddressedStorage storage,\n                       BatCave bats,\n                       Crypto crypto,\n                       CoreNode coreNode,\n                       Account account,\n                       SocialNetwork social,\n                       MutablePointers mutable,\n                       InstanceAdmin controller,\n                       SpaceUsage usage,\n                       ServerMessager serverMessages,\n                       GarbageCollector gc,\n                       Optional<SyncProperties> syncProps,\n                       Optional<LocalAppProperties> localAppProps) {\n        this.storage = storage;\n        this.bats = bats;\n        this.crypto = crypto;\n        this.coreNode = coreNode;\n        this.account = account;\n        this.social = social;\n        this.mutable = mutable;\n        this.controller = controller;\n        this.usage = usage;\n        this.serverMessages = serverMessages;\n        this.gc = gc;\n        this.blockCache = storage.getBlockCache();\n        this.syncProps = syncProps;\n        this.localAppProps = localAppProps;\n    }\n\n    public static class TlsProperties {\n        public final String hostname, keyfilePassword;\n\n        public TlsProperties(String hostname, String keyfilePassword) {\n            this.hostname = hostname;\n            this.keyfilePassword = keyfilePassword;\n        }\n    }\n\n    public static class LocalAppProperties {\n        public final Path peergosDir;\n        public final String currentServerUrl;\n\n        public LocalAppProperties(Path peergosDir, String currentServerUrl) {\n            this.peergosDir = peergosDir;\n            this.currentServerUrl = currentServerUrl;\n        }\n    }\n\n    public void stop() {\n        if (localhostServer != null)\n            localhostServer.stop(0);\n        if (gc != null)\n            gc.stop();\n        Multipart.ioPool.shutdown();\n    }\n\n    public boolean initAndStart(InetSocketAddress local,\n                                List<Cid> nodeIds,\n                                Optional<TlsProperties> tlsProps,\n                                Optional<String> publicHostname,\n                                List<String> blockstoreDomains,\n                                List<String> frameDomains,\n                                List<String> appSubdomains,\n                                boolean includeCsp,\n                                Optional<String> basicAuth,\n                                Optional<Path> webroot,\n                                Optional<HttpPoster> appDevTarget,\n                                boolean useWebCache,\n                                boolean isPublicServer,\n                                int connectionBacklog,\n                                int handlerPoolSize) throws IOException {\n        InetAddress allInterfaces = InetAddress.getByName(\"::\");\n        if (tlsProps.isPresent())\n            try {\n                HttpServer httpServer = HttpServer.create();\n                httpServer.createContext(\"/\", new RedirectHandler(\"https://\" + tlsProps.get().hostname + \":443/\"));\n                httpServer.bind(new InetSocketAddress(allInterfaces, 80), connectionBacklog);\n                httpServer.start();\n                initTLS();\n            } catch (Exception e) {\n                LOG.log(Level.WARNING, e.getMessage(), e);\n                LOG.info(\"Couldn't start http redirect to https for user server!\");\n            }\n\n        LOG.info(\"Starting local Peergos server at: localhost:\"+local.getPort());\n        if (tlsProps.isPresent())\n            LOG.info(\"Starting Peergos TLS server on all interfaces.\");\n        localhostServer = HttpServer.create(local, connectionBacklog);\n        HttpsServer tlsServer = ! tlsProps.isPresent() ? null :\n                HttpsServer.create(new InetSocketAddress(allInterfaces, 443), connectionBacklog);\n\n        if (tlsProps.isPresent()) {\n            try {\n                SSLContext sslContext = SSLContext.getInstance(\"TLS\");\n\n                char[] password = tlsProps.get().keyfilePassword.toCharArray();\n                KeyStore ks = getKeyStore(\"storage.p12\", password);\n\n                KeyManagerFactory kmf = KeyManagerFactory.getInstance(\"SunX509\");\n                kmf.init(ks, password);\n\n//            TrustManagerFactory tmf = TrustManagerFactory.getInstance(\"SunX509\");\n//            tmf.init(SSL.getTrustedKeyStore());\n\n                // setup the HTTPS context and parameters\n                sslContext.init(kmf.getKeyManagers(), null, null);\n                sslContext.getSupportedSSLParameters().setUseCipherSuitesOrder(true);\n                // set up perfect forward secrecy\n                sslContext.getSupportedSSLParameters().setCipherSuites(new String[]{\n                        \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\",\n                        \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n                        \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\",\n                        \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\"\n                });\n\n                SSLContext.setDefault(sslContext);\n                tlsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {\n                    public void configure(HttpsParameters params) {\n                        try {\n                            // initialise the SSL context\n                            SSLContext c = SSLContext.getDefault();\n                            SSLEngine engine = c.createSSLEngine();\n                            params.setNeedClientAuth(false);\n                            params.setCipherSuites(engine.getEnabledCipherSuites());\n                            params.setProtocols(engine.getEnabledProtocols());\n\n                            // get the default parameters\n                            SSLParameters defaultSSLParameters = c.getDefaultSSLParameters();\n                            params.setSSLParameters(defaultSSLParameters);\n                        } catch (Exception ex) {\n                            LOG.severe(\"Failed to create HTTPS port\");\n                            ex.printStackTrace(System.err);\n                        }\n                    }\n                });\n            }\n            catch (NoSuchAlgorithmException | InvalidKeyException | KeyStoreException | CertificateException |\n                    NoSuchProviderException | SignatureException |\n                    UnrecoverableKeyException | KeyManagementException ex)\n            {\n                LOG.severe(\"Failed to load TLS settings\");\n                throw new RuntimeException(ex);\n            }\n        }\n\n        //define web-root static-handler\n        if (webroot.isPresent())\n            LOG.info(\"Using webroot from local file system: \" + webroot);\n        else\n            LOG.info(\"Using webroot from jar\");\n        if (isPublicServer && publicHostname.isEmpty())\n            throw new IllegalStateException(\"Missing arg public-domain\");\n        CspHost host = tlsProps.map(p -> new CspHost(\"https://\", p.hostname))\n                .orElse(publicHostname.isPresent() ?\n                        new CspHost(CspHost.isLocal(publicHostname.get()) ?\n                                \"http://\" : \"https://\", publicHostname.get())  :\n                        new CspHost(\"http://\",  local.getHostName(), local.getPort()));\n        StaticHandler handler = webroot.map(p -> (StaticHandler) new FileHandler(host, blockstoreDomains, frameDomains, appSubdomains, p, includeCsp, true, appDevTarget))\n                .orElseGet(() -> new JarHandler(host, blockstoreDomains, frameDomains, appSubdomains, includeCsp, true, PathUtil.get(\"/webroot\"), appDevTarget));\n        try {\n            handler.getAsset(\"index.html\");\n        } catch (Exception e) {\n            String msg = \"WARNING: No web assets are present. To include the web-ui you need a jar built from https://github.com/peergos/web-ui\";\n            System.out.println(msg);\n            LOG.warning(msg);\n        }\n\n        if (useWebCache) {\n            LOG.info(\"Caching web-resources\");\n            handler = handler.withCache();\n        }\n\n        addHandler(localhostServer, tlsServer, Constants.DHT_URL,\n                new StorageHandler(storage, crypto.hasher, (h, i) -> true, isPublicServer),\n                basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.BATS_URL,\n                new BatCaveHandler(this.bats, coreNode, storage, isPublicServer), basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.CORE_URL,\n                new CoreNodeHandler(this.coreNode, isPublicServer), basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.SOCIAL_URL,\n                new SocialHandler(this.social, isPublicServer), basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.MUTABLE_POINTERS_URL,\n                new MutationHandler(this.mutable, isPublicServer), basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.LOGIN_URL,\n                new AccountHandler(this.account, isPublicServer), basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.ADMIN_URL,\n                new AdminHandler(this.controller, isPublicServer), basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.SPACE_USAGE_URL,\n                new SpaceHandler(this.usage, isPublicServer), basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.SERVER_MESSAGE_URL,\n                new ServerMessageHandler(this.serverMessages, coreNode, storage, isPublicServer),\n                basicAuth, local, host, nodeIds, false);\n        addHandler(localhostServer, tlsServer, \"/\" + Constants.PUBLIC_FILES_URL,\n                new PublicFileHandler(crypto.hasher, coreNode, mutable, storage),\n                basicAuth, local, host, nodeIds, false);\n        if (! isPublicServer && publicHostname.isEmpty()) {\n            addHandler(localhostServer, null, \"/\" + Constants.ANDROID_FILE_REFLECTOR,\n                    new AndroidFileReflector(crypto, coreNode, mutable, storage),\n                    basicAuth, local, host, nodeIds, false);\n            addHandler(localhostServer, null, \"/\" + Constants.STOP,\n                    new StopHandler(), basicAuth, local, host, nodeIds, false);\n            blockCache.ifPresent(cache -> addHandler(localhostServer, null, \"/\" + Constants.CONFIG,\n                    new ConfigHandler(cache, localAppProps),\n                    basicAuth, local, host, nodeIds, false));\n            syncProps.ifPresent(props -> {\n                SyncConfigHandler sync = new SyncConfigHandler(props.config, props.peergosDir, props.syncer, storage, mutable, props.hostDirs, coreNode, crypto);\n                sync.start();\n                addHandler(localhostServer, null, \"/\" + Constants.SYNC,\n                        sync, basicAuth, local, host, nodeIds, false);\n            });\n        }\n        addHandler(localhostServer, tlsServer, UI_URL, handler, basicAuth, local, host, nodeIds, true);\n\n        ExecutorService executor = Threads.newPool(handlerPoolSize, \"api-handler-\");\n        localhostServer.setExecutor(executor);\n        localhostServer.start();\n\n        if (tlsServer != null) {\n            tlsServer.setExecutor(Threads.newPool(handlerPoolSize, \"api-handler-\"));\n            tlsServer.start();\n        }\n\n        return true;\n    }\n\n    private static void addHandler(HttpServer localhostServer,\n                                   HttpsServer tlsServer,\n                                   String path,\n                                   HttpHandler handler,\n                                   Optional<String> basicAuth,\n                                   InetSocketAddress local,\n                                   CspHost host,\n                                   List<Cid> nodeIds,\n                                   boolean allowSubdomains) {\n        HttpHandler withAuth = basicAuth\n                    .map(ba -> (HttpHandler) new BasicAuthHandler(ba, handler))\n                    .orElse(handler);\n        // Allow local requests, ones to the public host, and p2p reqs to our node\n        List<String> allowedHosts = new ArrayList<>();\n        allowedHosts.add(\"127.0.0.1:\" + local.getPort());\n        allowedHosts.add(host.host());\n        for (Cid nodeId : nodeIds) {\n            String barePeerId = new Multihash(nodeId.type, nodeId.getHash()).toBase58();\n            allowedHosts.add(barePeerId);\n            String wrappedPeerId = nodeId.toBase58();\n            allowedHosts.add(wrappedPeerId);\n        }\n\n        SubdomainHandler subdomainHandler = new SubdomainHandler(allowedHosts, withAuth, allowSubdomains);\n        localhostServer.createContext(path, subdomainHandler);\n        if (tlsServer != null) {\n            tlsServer.createContext(path, new HSTSHandler(subdomainHandler));\n        }\n    }\n\n    public static KeyStore getKeyStore(String filename, char[] password)\n            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, InvalidKeyException,\n            NoSuchProviderException, SignatureException\n    {\n        KeyStore ks = KeyStore.getInstance(\"PKCS12\");\n        if (new File(filename).exists())\n        {\n            ks.load(new FileInputStream(filename), password);\n            return ks;\n        }\n\n        throw new IllegalStateException(\"SSL keystore file doesn't exist: \"+filename);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/UserStats.java",
    "content": "package peergos.server;\n\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMPublicKey;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class UserStats {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.empty(), Optional.empty()).get();\n        List<String> usernames = network.coreNode.getUsernames(\"\").get();\n        ForkJoinPool pool = new ForkJoinPool(20);\n        List<String> errors = Collections.synchronizedList(new ArrayList<>());\n        List<Summary> summaries = pool.submit(() -> usernames.stream().parallel().flatMap(username -> {\n            List<Multihash> hosts = Collections.emptyList();\n            try {\n                List<UserPublicKeyLink> chain = network.coreNode.getChain(username).get();\n                UserPublicKeyLink last = chain.get(chain.size() - 1);\n                LocalDate expiry = last.claim.expiry;\n                hosts = last.claim.storageProviders;\n                PublicKeyHash owner = last.owner;\n                Set<PublicKeyHash> ownedKeysRecursive =\n                        DeletableContentAddressedStorage.getOwnedKeysRecursive(username, network.coreNode, network.mutable,\n                                (h, s) -> ContentAddressedStorage.getWriterData(owner, h, s, network.dhtClient), network.dhtClient, network.hasher).join();\n                CommittedWriterData cwd = WriterData.getWriterData(owner, owner, network.mutable, network.dhtClient).join();\n                boolean isLegacy = cwd.props.map(wd -> wd.staticData.isPresent()).orElse(false);\n                boolean postQuantum = cwd.props.map(wd -> network.dhtClient.getBoxingKey(owner, wd.followRequestReceiver.get())\n                        .join().get() instanceof HybridCurve25519MLKEMPublicKey)\n                        .orElse(false);\n                String summary = \"User: \" + username + \", expiry: \" + expiry\n                        + \", owned keys: \" + ownedKeysRecursive.size() + \", legacy: \" + isLegacy + \", preQuantum: \" + !postQuantum + \"\\n\";\n                System.out.println(summary);\n                return Stream.of(new Summary(username, expiry, hosts, ownedKeysRecursive, isLegacy, ! postQuantum));\n            } catch (Exception e) {\n                String host = hosts.stream().findFirst().map(Object::toString).orElse(\"\");\n                errors.add(username + \": \" + host);\n                System.err.println(\"Error for \" + username + \" on host \" + host);\n                e.printStackTrace();\n                return Stream.empty();\n            }\n        }).collect(Collectors.toList())).join();\n\n        System.out.println(\"Errors: \" + errors.size());\n        errors.forEach(System.out::println);\n\n        // Sort by expiry\n        sortAndPrint(summaries, (a, b) -> a.expiry.compareTo(b.expiry), \"expiry.txt\");\n\n        // Sort by host\n        sortAndPrint(summaries, Comparator.comparing(s -> s.storageProviders.stream()\n                .findFirst()\n                .map(Object::toString)\n                .orElse(\"\")), \"host.txt\");\n        pool.shutdownNow();\n    }\n\n    private static void sortAndPrint(List<Summary> stats,\n                                     Comparator<Summary> order,\n                                     String filename) throws Exception {\n        stats.sort(order);\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        stats.stream()\n                .map(s -> (s.toString() + \"\\n\").getBytes())\n                .forEach(bytes -> bout.write(bytes, 0, bytes.length));\n        Files.write(PathUtil.get(filename), bout.toByteArray());\n    }\n\n    private static class Summary {\n        public final String username;\n        public final LocalDate expiry;\n        public final List<Multihash> storageProviders;\n        public final Set<PublicKeyHash> ownedKeys;\n        public final boolean isLegacy, preQuantum;\n\n        public Summary(String username, LocalDate expiry, List<Multihash> storageProviders, Set<PublicKeyHash> ownedKeys, boolean isLegacy, boolean preQuantum) {\n            this.username = username;\n            this.expiry = expiry;\n            this.storageProviders = storageProviders;\n            this.ownedKeys = ownedKeys;\n            this.isLegacy = isLegacy;\n            this.preQuantum = preQuantum;\n        }\n\n        public String toString() {\n            return \"User: \" + username + \", expiry: \" + expiry + \", hosts: \" + storageProviders\n                    + \", owned keys: \" + ownedKeys.size() + \", legacy: \" + isLegacy + \", preQuantum: \" + preQuantum;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/ValidateUser.java",
    "content": "package peergos.server;\n\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\n\nimport java.net.*;\nimport java.util.*;\nimport java.util.logging.*;\n\n/** This utility check that all of a users mutable pointers and blocks are present as far as the network can tell\n *\n */\npublic class ValidateUser {\n\n    public static void main(String[] args) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"https://peergos.net\"), true, Optional.empty(), Optional.empty()).get();\n        String username = args[0];\n        Optional<PublicKeyHash> identity = network.coreNode.getPublicKeyHash(username).join();\n        Set<PublicKeyHash> ownedKeys = DeletableContentAddressedStorage.getOwnedKeysRecursive(username, network.coreNode, network.mutable,\n                (h, s) -> ContentAddressedStorage.getWriterData(identity.get(), h, s, network.dhtClient), network.dhtClient, network.hasher).join();\n        for (PublicKeyHash ownedKey : ownedKeys) {\n            validateWriter(identity.get(), ownedKey, network);\n        }\n    }\n\n    private static void validateWriter(PublicKeyHash owner, PublicKeyHash writer, NetworkAccess network) {\n        MaybeMultihash target = network.mutable.getPointerTarget(owner, writer, network.dhtClient).join().updated;\n        if (! target.isPresent()) {\n            Logging.LOG().log(Level.WARNING, \"Skipping unretrievable mutable pointer for: \" + writer);\n            return;\n        }\n\n        validateBlock(owner, target.get(), network);\n    }\n\n    private static void validateBlock(PublicKeyHash owner, Multihash target, NetworkAccess network) {\n        Optional<CborObject> block = network.dhtClient.get(owner, (Cid)target, Optional.empty()).join();\n        if (! block.isPresent())\n            throw new IllegalStateException(\"Couldn't retrieve \" + target);\n\n        List<Multihash> links = block.get().links();\n        for (Multihash link : links) {\n            if (link instanceof Cid && ((Cid) link).codec == Cid.Codec.Raw)\n                network.dhtClient.getSize(owner, link).join();\n            else\n                validateBlock(owner, link, network);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/apps/email/EmailBridgeClient.java",
    "content": "package peergos.server.apps.email;\n\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.email.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.*;\n\npublic class EmailBridgeClient {\n\n    private final String clientUsername;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private final UserContext clientWritableContext;\n    private final PublicBoxingKey encryptionTarget;\n    private static final Path emailDataDir = PathUtil.get(\".apps\", \"email\", \"data\");\n\n    public EmailBridgeClient(String clientUsername, UserContext clientWritableContext, PublicBoxingKey encryptionTarget) {\n        this.clientUsername = clientUsername;\n        this.clientWritableContext = clientWritableContext;\n        this.network = clientWritableContext.network;\n        this.crypto = clientWritableContext.crypto;\n        this.encryptionTarget = encryptionTarget;\n    }\n\n    private FileWrapper pendingFolder() {\n        String pendingPath = clientWritableContext.getEntryPath().join();\n        return clientWritableContext.getByPath(pendingPath).join().get();\n    }\n\n    public List<String> listOutbox() {\n        return pendingFolder().getChild(\"outbox\", crypto.hasher, network).join().get()\n                .getChildren(crypto.hasher, network).join()\n                .stream()\n                .filter(f -> ! f.isDirectory())\n                .map(FileWrapper::getName)\n                .collect(Collectors.toList());\n    }\n\n    public Pair<FileWrapper, EmailMessage> getPendingEmail(String filename) {\n        FileWrapper outboxFolder = pendingFolder().getChild(\"outbox\", crypto.hasher, network).join().get();\n        FileWrapper emailFile = outboxFolder.getChild(filename, crypto.hasher, network).join().get();\n        EmailMessage email = Serialize.parse(emailFile, EmailMessage::fromCbor, network, crypto).join();\n        return new Pair<>(emailFile, email);\n    }\n\n    public byte[] getOutgoingAttachment(String filename) {\n        FileWrapper outboxFolder = pendingFolder().getChild(\"outbox\", crypto.hasher, network).join().get();\n        FileWrapper attachmentsFolder = outboxFolder.getChild(\"attachments\", crypto.hasher, network).join().get();\n        FileWrapper file = attachmentsFolder.getChild(filename, crypto.hasher, network).join().get();\n        return Serialize.readFully(file, crypto, network).join();\n    }\n\n    public void encryptAndMoveEmailToSent(FileWrapper file, EmailMessage emailMessage, Map<String, byte[]> attachmentsMap) {\n        Path pendingPath = App.getDataDir(\"email\", clientUsername).resolve(PathUtil.get(\"default\", \"pending\"));\n        Path outboxPath = pendingPath.resolve(\"outbox\");\n\n        FileWrapper outbox = pendingFolder().getChild(\"outbox\", crypto.hasher, network).join().get();\n        FileWrapper sent = pendingFolder().getChild(\"sent\", crypto.hasher, network).join().get();\n        byte[] rawCipherText = encryptEmail(emailMessage).join().serialize();\n        sent.uploadFileSection(file.getName(), AsyncReader.build(rawCipherText), false, 0, rawCipherText.length, Optional.empty(),\n                true, network, crypto, () -> false, x -> {}).join();\n\n        // TODO do this inside the update above and make atomic\n        FileWrapper original = file.getUpdated(network).join();\n        original.remove(outbox, outboxPath.resolve(file.getName()), clientWritableContext).join();\n        //move attachments\n        List<Attachment> allAttachments = new ArrayList(emailMessage.attachments);\n        if (emailMessage.forwardingToEmail.isPresent()) {\n            allAttachments.addAll(emailMessage.forwardingToEmail.get().attachments);\n        }\n        FileWrapper sentAttachments = sent.getUpdated(network).join().getChild(\"attachments\", crypto.hasher, network).join().get();\n        for(Attachment attachment : allAttachments) {\n            byte[] bytes = attachmentsMap.get(attachment.uuid);\n            if (bytes != null) {\n                FileWrapper outboxAttachmentDir = outbox.getChild(\"attachments\", crypto.hasher, network).join().get();\n                FileWrapper attachmentFile = outboxAttachmentDir.getChild(attachment.uuid, crypto.hasher, network).join().get();\n                Path attachmentFilePath = pendingPath.resolve(PathUtil.get(\"outbox\", \"attachments\", attachment.uuid));\n                byte[] rawAttachmentCipherText = encryptAttachment(bytes).join().serialize();\n                sentAttachments.uploadFileSection(attachment.uuid, AsyncReader.build(rawAttachmentCipherText),\n                        false, 0, rawAttachmentCipherText.length, Optional.empty(),\n                        true, network, crypto, () -> false, x -> {}).join();\n                attachmentFile.remove(outboxAttachmentDir, attachmentFilePath, clientWritableContext).join();\n            }\n        }\n    }\n\n    public Attachment uploadAttachment(String filename, int size, String type, byte[] data) {\n        int dotIndex = filename.lastIndexOf('.');\n        String fileExtension = dotIndex > -1 && dotIndex <= filename.length() -1\n                ?  filename.substring(dotIndex + 1) : \"\";\n        byte[] rawCipherText = encryptAttachment(data).join().serialize();\n        AsyncReader.ArrayBacked reader = new AsyncReader.ArrayBacked(rawCipherText);\n        String uuid = uploadAttachment(reader, fileExtension, rawCipherText.length).join();\n        return new Attachment(filename, size, type, uuid);\n    }\n\n    private CompletableFuture<String> uploadAttachment(AsyncReader reader, String fileExtension,\n                                                       int length) {\n        String uuid = UUID.randomUUID().toString() + \".\" + fileExtension;\n\n        FileWrapper inbox = pendingFolder().getChild(\"inbox\", crypto.hasher, network).join().get();\n        FileWrapper baseDir = inbox.getChild(\"attachments\", crypto.hasher, network).join().get();\n        return baseDir.uploadAndReturnFile(uuid, reader, length, false, () -> false, l -> {},\n                        baseDir.mirrorBatId(), network, crypto)\n                        .thenApply(hash -> uuid);\n    }\n\n    public void addToInbox(EmailMessage m) {\n        FileWrapper inbox = pendingFolder().getChild(\"inbox\", crypto.hasher, network).join().get();\n        network.synchronizer.applyComplexUpdate(inbox.owner(), inbox.signingPair(), (s, c) -> {\n            byte[] rawCipherText = encryptEmail(m).join().serialize();\n            return inbox.getUpdated(s, network).join()\n                    .uploadFileSection(s, c, m.id + \".cbor\", AsyncReader.build(rawCipherText), false, 0,\n                            rawCipherText.length, Optional.empty(), false, true, true,\n                            network, crypto, () -> false, x -> {}, crypto.random.randomBytes(32),\n                            Optional.empty(), Optional.of(Bat.random(crypto.random)), inbox.mirrorBatId());\n        }).join();\n    }\n\n    private CompletableFuture<SourcedAsymmetricCipherText> encryptEmail(EmailMessage m) {\n        BoxingKeyPair tmp = BoxingKeyPair.randomCurve25519(crypto.random, crypto.boxer);\n        return SourcedAsymmetricCipherText.build(tmp, encryptionTarget, m);\n    }\n\n    private CompletableFuture<SourcedAsymmetricCipherText> encryptAttachment(byte[] fileData) {\n        BoxingKeyPair tmp = BoxingKeyPair.randomCurve25519(crypto.random, crypto.boxer);\n        return SourcedAsymmetricCipherText.build(tmp, encryptionTarget, new CborObject.CborByteArray(fileData));\n    }\n\n    private static PublicBoxingKey getEncryptionTarget(NetworkAccess network, Crypto crypto, FileWrapper pendingDirectory) {\n        FileWrapper keyFile = pendingDirectory.getChild(\"encryption.publickey.cbor\", crypto.hasher, network).join().get();\n        return Serialize.parse(keyFile, PublicBoxingKey::fromCbor, network, crypto).join();\n    }\n\n    public static EmailBridgeClient build(SecretLink emailConfigFileLink, NetworkAccess network, Crypto crypto, String clientUsername, String clientEmailAddress) {\n\n        UserContext writableContext = UserContext.fromSecretLinkV2(emailConfigFileLink.toLink(), () -> Futures.of(\"\"),\n                network, crypto).join();\n        String pendingPath = writableContext.getEntryPath().join();\n        Optional<FileWrapper> emailFile = writableContext.getByPath(pendingPath + \"/\" + \"email.json\").join();\n        if (emailFile.isEmpty()) {\n            String contents = \"{ \\\"email\\\": \\\"\" + clientEmailAddress + \"\\\"}\";\n            byte[] data = contents.getBytes();\n            FileWrapper pendingDirectory = writableContext.getByPath(pendingPath).join().get();\n            pendingDirectory.uploadOrReplaceFile(\"email.json\", new AsyncReader.ArrayBacked(data), data.length, network,\n                    crypto, () -> false, l -> {}).join();\n        }\n        return new EmailBridgeClient(clientUsername, writableContext, getEncryptionTarget(network, crypto, writableContext.getByPath(pendingPath).join().get()));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/cli/CLI.java",
    "content": "package peergos.server.cli;\n\nimport org.jline.builtins.*;\nimport org.jline.reader.*;\nimport org.jline.reader.impl.*;\nimport org.jline.reader.impl.completer.AggregateCompleter;\nimport org.jline.reader.impl.completer.ArgumentCompleter;\nimport org.jline.reader.impl.completer.NullCompleter;\nimport org.jline.reader.impl.completer.StringsCompleter;\nimport org.jline.terminal.*;\nimport org.jline.utils.*;\n\nimport peergos.server.*;\nimport peergos.server.crypto.hash.ScryptJava;\nimport peergos.server.net.ProxyChooser;\nimport peergos.server.simulation.*;\nimport peergos.server.simulation.FileSystem;\nimport peergos.server.user.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.HTTPCoreNode;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.mutable.HttpMutablePointers;\nimport peergos.shared.social.FollowRequestWithCipherText;\nimport peergos.shared.social.HttpSocialNetwork;\nimport peergos.shared.storage.HttpSpaceUsage;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.Level;\nimport java.util.stream.*;\n\nimport static org.jline.builtins.Completers.TreeCompleter.node;\n\npublic class CLI implements Runnable {\n\n    private static void disableLogSpam() {\n        // disable log spam\n        TrieNodeImpl.disableLog();\n        HttpMutablePointers.disableLog();\n        NetworkAccess.disableLog();\n        HTTPCoreNode.disableLog();\n        HttpSocialNetwork.disableLog();\n        HttpSpaceUsage.disableLog();\n        FileUploader.disableLog();\n        LazyInputStreamCombiner.disableLog();\n    }\n\n    private final CLIContext cliContext;\n    private final FileSystem peergosFileSystem;\n    private final ListFilesCompleter remoteFilesCompleter, localFilesCompleter, remoteDirsCompleter, localDirsCompleter;\n    private final Completer allUsernamesCompleter, followersCompleter, pendingFollowersCompleter, processFollowRequestCompleter;\n    private volatile boolean isFinished;\n\n    public CLI(CLIContext cliContext) {\n        this.cliContext = cliContext;\n        this.peergosFileSystem = new PeergosFileSystemImpl(cliContext.userContext);\n        this.remoteFilesCompleter = new ListFilesCompleter(path -> this.remoteFilesLsFiles(path, false));\n        this.remoteDirsCompleter = new ListFilesCompleter(path -> this.remoteFilesLsFiles(path, true));\n        this.localFilesCompleter = new ListFilesCompleter(path -> this.localFilesLsFiles(path, false));\n        this.localDirsCompleter = new ListFilesCompleter(path -> this.localFilesLsFiles(path, true));\n        this.allUsernamesCompleter = new SupplierCompleter(this::listAllUsernames);\n        this.followersCompleter = new SupplierCompleter(this::listFollowers);\n        this.pendingFollowersCompleter = new SupplierCompleter(this::listPendingFollowers);\n        this.processFollowRequestCompleter = new StringsCompleter(\n                Stream.of(Command.ProcessFollowRequestAction.values())\n                        .map(Command.ProcessFollowRequestAction::altOrName)\n                        .collect(Collectors.toList()));\n    }\n\n    /**\n     * resolve against remote pwd if path is relative\n     *\n     * @param path\n     * @return\n     */\n    public Path resolvedRemotePath(String path) {\n        return resolveToPath(path, cliContext.pwd);\n    }\n\n    public Path resolveToPath(String arg, Path pathToResolveTo) {\n        Path p = Paths.get(arg);\n        if (p.isAbsolute())\n            return p;\n        return pathToResolveTo.resolve(p).normalize();\n    }\n\n    public Path resolveToPath(String arg) {\n        return resolveToPath(arg, cliContext.lpwd);\n    }\n\n    public static ParsedCommand fromLine(String line) {\n        String[] split = line.trim().split(\"\\\\s+\");\n        if (split == null || split.length == 0)\n            throw new IllegalStateException();\n        ArrayList<String> tokens = new ArrayList<>(Arrays.asList(split));\n\n        Command cmd = Command.parse(tokens.remove(0));\n\n        return new ParsedCommand(cmd, line, tokens);\n    }\n\n    private static final char PASSWORD_MASK = '*';\n    private static final String PROMPT = \" > \";\n\n    static String formatHelp() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"Available commands:\");\n        int maxLength = Command.maxLength();\n\n        for (Command cmd : Arrays.asList(Command.values())) {\n            sb.append(\"\\n\").append(cmd.example());\n            for (int i = 0; i < maxLength - cmd.example().length(); i++) {\n                sb.append(\" \");\n            }\n            sb.append(\"\\t\").append(cmd.description);\n        }\n        sb.append(\"\\n\\nNote: <TAB> based autocomplete is available on most commands.\");\n        return sb.toString();\n    }\n\n    private String handle(ParsedCommand parsedCommand, Terminal terminal, LineReader reader) {\n\n\n        try {\n            switch (parsedCommand.cmd) {\n                case ls:\n                    return ls(parsedCommand);\n                case lls:\n                    return lls(parsedCommand);\n                case get:  // download\n                    return get(parsedCommand, terminal.writer());\n                case put:  //upload\n                    return put(parsedCommand, terminal.writer());\n                case mkdir:\n                    return mkdir(parsedCommand);\n                case rm:\n                    return rm(parsedCommand);\n                case exit:\n                case quit:\n                case bye:\n                    return exit(parsedCommand);\n                case help:\n                    return help(parsedCommand);\n                case space:\n                    return space(parsedCommand);\n                case get_follow_requests:\n                    return getFollowRequests(parsedCommand);\n                case process_follow_request:\n                    return processFollowRequest(parsedCommand);\n                case follow:\n                    return follow(parsedCommand);\n                case passwd:\n                    return passwd(parsedCommand, terminal, reader);\n//                case share:\n                case share_read:\n                    return shareReadAccess(parsedCommand);\n                case cd:\n                    return cd(parsedCommand);\n                case lcd:\n                    return lcd(parsedCommand);\n                case pwd:\n                    return pwd(parsedCommand);\n                case lpwd:\n                    return lpwd(parsedCommand);\n                default:\n                    return \"Unexpected cmd '\" + parsedCommand.cmd + \"'\";\n            }\n        } catch (Exception ex) {\n            ex.printStackTrace();\n            return \"Failed to execute \" + parsedCommand;\n\n        }\n\n    }\n\n    public String ls(ParsedCommand cmd) {\n\n        String pathArg = cmd.hasArguments() ? cmd.firstArgument() : \"\";\n        Path path = resolvedRemotePath(pathArg);\n\n        Stat stat = checkPath(path);\n        if (stat.fileProperties().isDirectory)\n            return peergosFileSystem.ls(path, false).stream()\n                .map(Path::toString)\n                .sorted()\n                .collect(Collectors.joining(\"\\n\"));\n\n        return path.toString();\n    }\n\n    public String lls(ParsedCommand cmd) {\n\n        String localPathArg = cmd.hasArguments() ? cmd.firstArgument() : \"\";\n        Path path = resolveToPath(localPathArg).toAbsolutePath().normalize();\n\n        try {\n            if (path.toFile().isDirectory())\n                return Files.list(path)\n                        .map(Path::toString)\n                        .sorted()\n                        .collect(Collectors.joining(\"\\n\"));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return path.toString();\n    }\n\n    private Stat checkPath(Path remotePath) {\n        Stat stat = null;\n        try {\n            return peergosFileSystem.stat(remotePath);\n        } catch (Exception ex) {\n            throw new IllegalStateException(\"Could not find remote specified remote path '\" + remotePath + \"'\", ex);\n        }\n\n    }\n\n    public String get(ParsedCommand cmd, PrintWriter writerForProgress) throws IOException {\n        if (!cmd.hasArguments())\n            throw new IllegalStateException();\n\n        Path remotePath = resolvedRemotePath(cmd.firstArgument()).toAbsolutePath().normalize();\n\n        Stat stat = checkPath(remotePath);\n\n        String localPathArg = cmd.hasSecondArgument() ? cmd.secondArgument() : \"\";\n        Path localPath = resolveToPath(localPathArg).toAbsolutePath();\n\n        if (localPath.toFile().isDirectory())\n            localPath = localPath.resolve(stat.fileProperties().name);\n        else if (!localPath.toFile().getParentFile().isDirectory())\n            throw new IllegalStateException(\"Specified local path '\" + localPath.getParent() + \"' is not a directory or does not exist.\");\n\n        if (stat.fileProperties().isDirectory) {\n            boolean skipExisting = cmd.flags.contains(Command.Flag.SKIP_EXISTING.flag);\n            copyDir(remotePath, localPath.getParent(), skipExisting, writerForProgress);\n            return \"Downloaded \" + remotePath + \" to \" + localPath;\n        } else {\n            ProgressBar pb = new ProgressBar(new AtomicLong(0), new AtomicLong(1), remotePath.getParent(), remotePath.getFileName().toString());\n            BiConsumer<Long, Long> progressConsumer = (bytes, size) -> pb.update(writerForProgress, bytes, size);\n\n            AsyncReader reader = peergosFileSystem.reader(remotePath);\n            byte[] buf = new byte[Chunk.MAX_SIZE];\n            try (FileOutputStream fout = new FileOutputStream(localPath.toFile())) {\n                long fileSize = stat.fileProperties().size;\n                for (long offset = 0; offset < fileSize;) {\n                    int read = reader.readIntoArray(buf, 0, Math.min(buf.length, (int) (fileSize - offset))).join();\n                    fout.write(buf, 0, read);\n                    offset += read;\n                    progressConsumer.accept(offset, fileSize);\n                }\n                writerForProgress.println();\n                writerForProgress.flush();\n                return \"Downloaded \" + remotePath + \" to \" + localPath;\n            }\n        }\n    }\n\n    private void copyDir(Path remote, Path local, boolean skipExisting, PrintWriter writerForProgress) throws IOException {\n        String dirName = remote.getFileName().toString();\n        Path localDir = local.resolve(dirName);\n        if (! localDir.toFile().exists())\n            localDir.toFile().mkdirs();\n        if (! localDir.toFile().isDirectory())\n            throw new IllegalStateException(localDir + \" already exists and is a file not a directory!\");\n        List<Path> remoteChildren = peergosFileSystem.ls(remote);\n        for (Path remoteChild : remoteChildren) {\n            Stat stat = peergosFileSystem.stat(remoteChild);\n            if (stat.fileProperties().isDirectory) {\n                copyDir(remoteChild, localDir, skipExisting, writerForProgress);\n            } else {\n                ProgressBar pb = new ProgressBar(new AtomicLong(0), new AtomicLong(1), remoteChild.getParent(), remoteChild.getFileName().toString());\n                BiConsumer<Long, Long> progressConsumer = (bytes, size) -> pb.update(writerForProgress, bytes, size);\n\n                File localFile = localDir.resolve(remoteChild.getFileName()).toFile();\n                if (localFile.exists() && skipExisting) {\n                    writerForProgress.println(\"Skipping \" + localFile);\n                    continue;\n                }\n                FileOutputStream fout = new FileOutputStream(localFile);\n                long fileSize = stat.fileProperties().size;\n                AsyncReader reader = peergosFileSystem.reader(remoteChild);\n                byte[] buf = new byte[Chunk.MAX_SIZE];\n                for (long offset = 0; offset < fileSize;) {\n                    int read = reader.readIntoArray(buf, 0, Math.min(buf.length, (int) (fileSize - offset))).join();\n                    fout.write(buf, 0, read);\n                    offset += read;\n                    progressConsumer.accept(offset, fileSize);\n                }\n                writerForProgress.println();\n                writerForProgress.flush();\n            }\n        }\n    }\n\n    private static List<String> convert(Path p) {\n        List<String> res = new ArrayList<>();\n        for (int i=0; i < p.getNameCount(); i++)\n            res.add(p.getName(i).toString());\n        return res;\n    }\n\n    private static AsyncReader reader(File f) {\n        return new FileAsyncReader(f);\n    }\n\n    public interface ProgressCreator {\n        ProgressConsumer<Long> create(Path remoteRelativeDir, String filename, Long size);\n    }\n\n    public static Stream<FileWrapper.FolderUploadProperties> parseLocalFolder(Path remoteRelativeDir,\n                                                                        Path localDir,\n                                                                        boolean skipExisting,\n                                                                        AtomicLong fileCount,\n                                                                        Hasher hasher,\n                                                                        ProgressCreator progressCreator) {\n        try {\n            List<FileWrapper.FileUploadProperties> files = Files.list(localDir)\n                    .filter(p -> p.toFile().isFile())\n                    .map(p -> {\n                        long fileSize = p.toFile().length();\n                        LocalDateTime modified = LocalDateTime.ofInstant(Instant.ofEpochSecond(p.toFile().lastModified() / 1000, 0), ZoneOffset.UTC);\n                        return new FileWrapper.FileUploadProperties(p.getFileName().toString(), () -> reader(p.toFile()),\n                                (int) (fileSize >> 32), (int) fileSize, Optional.of(modified), Optional.of(ScryptJava.hashFile(p, hasher)), skipExisting, true,\n                                progressCreator.create(remoteRelativeDir, p.getFileName().toString(), Math.max(4096, fileSize)));\n                    })\n                    .collect(Collectors.toList());\n            fileCount.addAndGet(files.size());\n            FileWrapper.FolderUploadProperties dir = new FileWrapper.FolderUploadProperties(convert(remoteRelativeDir), files);\n            return Stream.concat(Stream.of(dir),\n                    Files.list(localDir)\n                            .filter(p -> p.toFile().isDirectory())\n                            .flatMap(p -> parseLocalFolder(remoteRelativeDir.resolve(p.getFileName()), localDir.resolve(p.getFileName()), skipExisting, fileCount, hasher, progressCreator)));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String put(ParsedCommand cmd, PrintWriter writerForProgress) throws IOException {\n        String localPathArg = cmd.firstArgument();\n        Path localPath = resolveToPath(localPathArg).toAbsolutePath().normalize();\n\n        if (localPath.toFile().isDirectory()) {\n            Path remotePath = cmd.hasSecondArgument() ? cliContext.pwd.resolve(Paths.get(cmd.secondArgument())) : cliContext.pwd;\n            boolean skipExisting = cmd.flags.contains(Command.Flag.SKIP_EXISTING.flag);\n            AtomicLong fileCount = new AtomicLong(0);\n            AtomicLong doneFiles = new AtomicLong(0);\n            peergosFileSystem.writeSubtree(remotePath, parseLocalFolder(localPath.getFileName(), localPath, skipExisting, fileCount,\n                    cliContext.userContext.crypto.hasher, (path, name, size) -> {\n                        ProgressBar pb = new ProgressBar(doneFiles, fileCount, path, name);\n                        return bytesWritten -> pb.update(writerForProgress, bytesWritten, size);\n                    }), f -> Futures.of(true));\n            return \"\\nSuccessfully uploaded \" + localPath + \" to remote \" + remotePath;\n        } else {\n            String remotePathS = cmd.hasSecondArgument() ? cmd.secondArgument() : cliContext.pwd.resolve(localPath.getFileName()).toString();\n            Path remotePath = resolvedRemotePath(remotePathS);\n            ProgressBar pb = new ProgressBar(new AtomicLong(0), new AtomicLong(1), remotePath, localPath.getFileName().toString());\n            File file = localPath.toFile();\n            long size = file.length();\n            Consumer<Long> progressConsumer = bytesSoFar -> pb.update(writerForProgress, bytesSoFar, size);\n            FileAsyncReader reader = new FileAsyncReader(file);\n            boolean resumeUpload = cmd.flags.contains(Command.Flag.RESUME_UPLOAD.flag);\n            peergosFileSystem.write(remotePath, reader, size, progressConsumer, resumeUpload);\n            writerForProgress.println();\n            writerForProgress.flush();\n            return \"Successfully uploaded \" + localPath + \" to remote \" + remotePath;\n        }\n    }\n\n    public String mkdir(ParsedCommand cmd) throws IOException {\n        String remoteDirArg = cmd.firstArgument();\n        Path remoteDirPath = cliContext.pwd.resolve(remoteDirArg);\n        peergosFileSystem.mkdir(remoteDirPath);\n\n        return \"\\nSuccessfully created \" + remoteDirPath;\n    }\n\n    public String rm(ParsedCommand cmd) {\n        if (!cmd.hasArguments())\n            throw new IllegalStateException();\n\n        Path remotePath = resolvedRemotePath(cmd.firstArgument()).toAbsolutePath().normalize();\n\n        Stat stat;\n        try {\n            stat = peergosFileSystem.stat(remotePath);\n        } catch (Exception ex) {\n            throw new IllegalStateException(\"Could not find remote specified remote path '\" + remotePath + \"'\", ex);\n        }\n\n        if (stat.fileProperties().isDirectory) {\n            System.out.println(\"Delete directory and all contents of \" + remotePath + \" (Y/N)\");\n            String res = System.console().readLine().toLowerCase();\n            if (! res.equals(\"y\"))\n                return \"Aborting delete\";\n        }\n\n        peergosFileSystem.delete(remotePath);\n        return \"Deleted \" + remotePath;\n    }\n\n    public String exit(ParsedCommand cmd) {\n        if (cmd.hasArguments())\n            throw new IllegalStateException();\n        this.isFinished = true;\n        return \"Exiting\";\n\n    }\n\n    public String passwd(ParsedCommand cmd, Terminal terminal, LineReader reader) {\n        terminal.writer().println(\"Enter current password:\");\n        String currentPassword = reader.readLine(PROMPT, PASSWORD_MASK);\n        terminal.writer().println(\"Enter new  password:\");\n        String newPassword = reader.readLine(PROMPT, PASSWORD_MASK);\n        try {\n            cliContext.userContext.changePassword(currentPassword, newPassword, methods -> mfa(methods, terminal.writer(), reader)).join();\n        } catch (Exception ex) {\n            ex.printStackTrace();\n            return \"Failed to update password\";\n        }\n        return \"Password updated\";\n    }\n\n    public String space(ParsedCommand cmd) {\n        UserContext uc = cliContext.userContext;\n        long spaceUsed = uc.getSpaceUsage(false).join();\n        long spaceMB = spaceUsed / 1024 / 1024;\n        return \"Total space used: \" + spaceMB + \" MiB.\";\n    }\n\n    public String getFollowRequests(ParsedCommand cmd) {\n\n        List<FollowRequestWithCipherText> followRequests = cliContext.userContext.processFollowRequests().join();\n        List<String> followRequestUsers = followRequests.stream()\n                .map(e -> e.getEntry().ownerName)\n                .collect(Collectors.toList());\n\n        if (followRequests.isEmpty())\n            return \"No pending follow requests.\";\n\n        return followRequestUsers.stream()\n                .collect(Collectors.joining(\"\\n\\t\", \"You have pending follow requests from the following users:\\n\", \"\"));\n    }\n\n    public String processFollowRequest(ParsedCommand cmd) {\n        if (! cmd.hasArguments())\n            return \"Specify a user\";\n        if (! cmd.hasSecondArgument())\n            return \"Cannot process follow request - please specify one of \"+ new ArrayList<>(Arrays.asList(Command.ProcessFollowRequestAction.values()));\n\n        String userThatSentFollowRequest = cmd.firstArgument();\n        Command.ProcessFollowRequestAction processFollowRequestAction = null;\n        try {\n            processFollowRequestAction = Command.ProcessFollowRequestAction.parse(cmd.secondArgument());\n        } catch (IllegalArgumentException | NullPointerException ex) {\n            return \"Could not parse process-action '\"+ cmd.secondArgument() +\"' - please specify one of \"+ new ArrayList<>(Arrays.asList(Command.ProcessFollowRequestAction.values()));\n        }\n\n        List<FollowRequestWithCipherText> followRequests = cliContext.userContext.processFollowRequests().join();\n        Optional<FollowRequestWithCipherText> first = followRequests.stream()\n                .filter(e -> userThatSentFollowRequest.equals(e.getEntry().ownerName))\n                .findFirst();\n\n        if (! first.isPresent())\n            return \"Could not process request from ' \"+ userThatSentFollowRequest +\"' - they haven't sent you a follow-request.\";\n\n        FollowRequestWithCipherText followRequestWithCipherText = first.get();\n        boolean accept =  false;\n        boolean reciprocate = false;\n\n        switch (processFollowRequestAction) {\n            case accept:\n                accept = true;\n                break;\n            case accept_and_reciprocate:\n                accept = true;\n                reciprocate = true;\n                break;\n            case reject:\n                accept = false;\n                reciprocate = false;\n                break;\n            default:\n                throw new IllegalStateException();\n        }\n        cliContext.userContext.sendReplyFollowRequest(followRequestWithCipherText, accept, reciprocate).join();\n        return \"Processed follow request from '\"+ userThatSentFollowRequest +\"' with \"+ processFollowRequestAction +\" action.\";\n    }\n\n\n    public String shareReadAccess(ParsedCommand cmd) {\n\n        if (!cmd.hasSecondArgument())\n            throw new IllegalStateException();\n\n        String pathToShare = cmd.firstArgument();\n        Path remotePath = resolvedRemotePath(pathToShare);\n\n        Stat stat = checkPath(remotePath);\n        // TODO\n        if (stat.fileProperties().isDirectory)\n            throw new IllegalStateException(\"Directory is not supported\");\n\n        String userToGrantReadAccess = cmd.secondArgument();\n        Set<String> followerUsernames = cliContext.userContext.getFollowerNames().join();\n        if (!followerUsernames.contains(userToGrantReadAccess))\n            return \"File not shared: specified-user \" + userToGrantReadAccess + \" is not following you\";\n        try {\n            cliContext.userContext.shareReadAccessWith(remotePath, new HashSet<>(Arrays.asList(userToGrantReadAccess))).join();\n        } catch (Exception ex) {\n            ex.printStackTrace();\n            return \"Failed not share file\";\n        }\n        return \"Shared read-access to '\" + remotePath + \"' with \" + userToGrantReadAccess;\n    }\n\n    public String follow(ParsedCommand cmd) {\n        if (!cmd.hasArguments())\n            throw new IllegalStateException();\n\n        String userToFollow = cmd.firstArgument();\n\n        try {\n            cliContext.userContext.sendInitialFollowRequest(userToFollow).join();\n        } catch (Exception ex) {\n            ex.printStackTrace();\n            return \"Failed to send follow request\";\n        }\n        return \"Sent follow request to '\" + userToFollow + \"'\";\n    }\n\n    public String cd(ParsedCommand cmd) {\n        String remotePathArg = cmd.hasArguments() ? cmd.firstArgument() : \"\";\n        if (! cmd.hasArguments()) // no args goes to home\n            cliContext.pwd = cliContext.home;\n        Path remotePathToCdTo = resolvedRemotePath(remotePathArg).toAbsolutePath().normalize(); // normalize handles \"..\" etc.\n\n        Stat stat = checkPath(remotePathToCdTo);\n        if (!stat.fileProperties().isDirectory)\n            return \"Specified path '\" + remotePathToCdTo + \"' is not a directory\";\n        cliContext.pwd = remotePathToCdTo;\n        return \"Current directory : \" + remotePathToCdTo;\n    }\n\n    public String lcd(ParsedCommand cmd) {\n        String localPathArg = cmd.hasArguments() ? cmd.firstArgument() : \"\";\n        Path localPathToCdTo = resolveToPath(localPathArg).toAbsolutePath().normalize(); // normalize handles \"..\" etc.\n\n        if (!localPathToCdTo.toFile().isDirectory())\n            return \"Specified path '\" + localPathToCdTo + \"' is not a directory\";\n        cliContext.lpwd = localPathToCdTo;\n        return \"Current local directory : \" + localPathToCdTo;\n    }\n\n    public String pwd(ParsedCommand cmd) {\n        return \"Remote working directory: \" + cliContext.pwd.toString();\n    }\n\n    public String lpwd(ParsedCommand cmd) {\n        return \"Local working directory: \" + cliContext.lpwd.toString();\n    }\n\n\n    public String help(ParsedCommand cmd) {\n        return formatHelp();\n    }\n\n\n    public String buildPrompt() {\n        return new AttributedStringBuilder()\n                .style(AttributedStyle.DEFAULT.background(AttributedStyle.BLACK).foreground(AttributedStyle.RED))\n                .append(cliContext.username)\n                .style(AttributedStyle.DEFAULT.background(AttributedStyle.BLACK))\n                .append(\"@\")\n                .style(AttributedStyle.DEFAULT.background(AttributedStyle.BLACK).foreground(AttributedStyle.GREEN))\n                .append(cliContext.serverURL)\n                .style(AttributedStyle.DEFAULT)\n                .append(\" > \").toAnsi();\n    }\n\n    private List<String> localFilesLsFiles(String pathArgument, boolean filterDirs) {\n        try {\n            Path path = resolveToPath(pathArgument).toAbsolutePath();\n            if (path.toFile().isFile() && !filterDirs)\n                return Arrays.asList(path.toString());\n            if (path.toFile().isDirectory() && filterDirs)\n                return Arrays.asList(path.toString());\n            if (path.toFile().isDirectory() && !filterDirs)\n                return Files.list(path)\n                        .map(Path::toString)\n                        .collect(Collectors.toList());\n\n            if (path.getParent().toFile().isDirectory())\n                return Files.list(path.getParent())\n                        .filter(p -> !filterDirs || p.toFile().isDirectory())\n                        .map(Path::toString)\n                        .collect(Collectors.toList());\n\n        } catch (IOException ioe) {\n            ioe.printStackTrace();\n        }\n        return Collections.emptyList();\n    }\n\n    private List<String> listFollowers() {\n        SocialState socialState = cliContext.userContext.getSocialState().join();\n        return new ArrayList<>(socialState.followerRoots.keySet());\n    }\n\n    private List<String> listPendingFollowers() {\n        SocialState socialState = cliContext.userContext.getSocialState().join();\n        List<FollowRequestWithCipherText> pendingIncoming = socialState.pendingIncoming;\n        return pendingIncoming.stream()\n                .map(e -> e.req.entry.get().ownerName)\n                .collect(Collectors.toList());\n    }\n\n    private List<String> listAllUsernames() {\n        return cliContext.userContext.network.coreNode.getUsernames(\"\").join();\n    }\n\n    /**\n     *\n     * @param pathArgument\n     * @param filterDirs only return paths that are directories when true\n     * @return\n     */\n    private List<String> remoteFilesLsFiles(String pathArgument, boolean filterDirs) {\n        Path path = resolvedRemotePath(pathArgument).toAbsolutePath();\n        Stat stat = null;\n        try {\n            stat = peergosFileSystem.stat(path);\n            if (! stat.fileProperties().isDirectory)\n                throw new Exception();\n        } catch (Exception ex) {\n            //try parent\n            path = path.getParent();\n            try {\n                peergosFileSystem.stat(path);\n            } catch (Exception ex2) {\n                return Collections.emptyList();\n            }\n\n        }\n        final Path parentPath = path;\n        List<String> completeOptions = peergosFileSystem.ls(parentPath, false)\n                .stream()\n                .filter(p -> (! filterDirs) || checkPath(p).fileProperties().isDirectory)\n                .map(p -> p.isAbsolute() ? cliContext.pwd.relativize(p): p)\n                .map(Path::toString)\n                .collect(Collectors.toList());\n        return completeOptions;\n    }\n    /**\n     * Build the command completer.\n     *\n     * @return\n     */\n    private Completer getCompleter(Command.Argument arg) {\n        switch (arg) {\n            case REMOTE_FILE:\n                return remoteFilesCompleter;\n            case REMOTE_DIR:\n                return remoteDirsCompleter;\n            case LOCAL_FILE:\n                return localFilesCompleter;\n            case LOCAL_DIR:\n                return localDirsCompleter;\n            case USERNAME:\n                return allUsernamesCompleter;\n            case FOLLOWER:\n                return followersCompleter;\n            case PENDING_FOLLOW_REQUEST:\n                return pendingFollowersCompleter;\n            case PROCESS_FOLLOW_REQUEST:\n                return processFollowRequestCompleter;\n            default:\n                throw new IllegalStateException();\n        }\n    }\n\n    private Completer buildCompletionNode(Command cmd) {\n        if (cmd.secondArg != null) {\n            Completer arg1 = getCompleter(cmd.firstArg);\n            Completer arg2 = getCompleter(cmd.secondArg);\n            return new ArgumentCompleter(\n                    new StringsCompleter(cmd.name()),\n                    new Completers.OptionCompleter(List.of(arg1, arg2, NullCompleter.INSTANCE),\n                            cmd.flags.stream()\n                            .map(f -> new Completers.OptDesc(f.flag, f.flag))\n                            .collect(Collectors.toList()), 1)\n            );\n        }\n        else if (cmd.firstArg !=  null) {\n            return new ArgumentCompleter(\n                    new StringsCompleter(cmd.name()), getCompleter(cmd.firstArg));\n        }\n        else\n            return new ArgumentCompleter(new StringsCompleter(cmd.name()));\n    }\n\n    public Completer buildCompleter() {\n\n        List<Completer> cmds = Stream.of(Command.values())\n                .map(this::buildCompletionNode)\n                .collect(Collectors.toList());\n\n        return new AggregateCompleter(cmds);\n    }\n\n    /**\n     * Build a CLIContext from the CLI - from user interaction.\n     *\n     * @return\n     */\n\n    public static CLIContext buildContextFromCLI(Args args) {\n        Terminal terminal = buildTerminal();\n\n        DefaultParser parser = new DefaultParser();\n        LineReader reader = LineReaderBuilder.builder()\n                .terminal(terminal)\n                .parser(parser)\n                .completer(new StringsCompleter(\n                        \"http://\",\n                        \"https://\",\n                        \"https://peergos.net\",\n                        \"http://localhost:8000\"))\n                .build();\n\n        String address = args.hasArg(\"peergos-url\") ?\n                args.getArg(\"peergos-url\") :\n                reader.readLine(\"Enter Server address \\n > \").trim();\n        URL serverURL = null;\n\n        final PrintWriter writer = terminal.writer();\n        try {\n            serverURL = new URL(address);\n        } catch (MalformedURLException ex) {\n            writer.println(\"Specified server \" + address + \" is not valid!\");\n            writer.flush();\n            System.exit(1);\n        }\n\n        String username = args.hasArg(\"username\") ?\n                args.getArg(\"username\") :\n                reader.readLine(\"Enter username\" + PROMPT).trim();\n\n        Optional<ProxySelector> proxy = ProxyChooser.build(args);\n        NetworkAccess network = Builder.buildJavaNetworkAccess(serverURL, address.startsWith(\"https\"), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-shell\"), proxy).join();\n        Consumer<String> progressConsumer =  msg -> {\n            writer.println(msg);\n            writer.flush();\n            return;\n        };\n\n        boolean isRegistered = network.isUsernameRegistered(username).join();\n        if (! isRegistered) {\n            String password = Passwords.generate();\n            writer.println(\"Generated password: \" + password);\n            writer.println(\"Re-enter password\");\n            String password2 = reader.readLine(PROMPT, PASSWORD_MASK);\n            if (! password.equals(password2)) {\n                writer.println(\"Passwords don't match!\");\n                System.exit(0);\n            }\n            writer.println(\"Enter any signup token (or press enter if none):\");\n            String token = reader.readLine(PROMPT).trim();;\n\n            UserContext userContext = UserContext.signUp(username, password, token, Optional.empty(), s -> {},\n                    Optional.empty(), network, CRYPTO, progressConsumer).join();\n            return new CLIContext(terminal, userContext, serverURL.toString(), username);\n        } else {\n            String password = args.hasArg(\"PEERGOS_PASSWORD\") ?\n                    args.getArg(\"PEERGOS_PASSWORD\") :\n                    reader.readLine(\"Enter password for '\" + username + \"'\" + PROMPT, PASSWORD_MASK);\n            UserContext userContext = UserContext.signIn(username, password,\n                    methods -> mfa(methods, writer, reader), false, false, network, CRYPTO, progressConsumer).join();\n            return new CLIContext(terminal, userContext, serverURL.toString(), username);\n        }\n    }\n\n    private static CompletableFuture<MultiFactorAuthResponse> mfa(MultiFactorAuthRequest req,\n                                                                  PrintWriter writer,\n                                                                  LineReader reader) {\n        Optional<MultiFactorAuthMethod> anyTotp = req.methods.stream().filter(m -> m.type == MultiFactorAuthMethod.Type.TOTP).findFirst();\n        if (anyTotp.isEmpty())\n            throw new IllegalStateException(\"No supported 2 factor auth method! \" + req.methods);\n        MultiFactorAuthMethod totp = anyTotp.get();\n        writer.println(\"Enter TOTP code for login\");\n        String code = reader.readLine(PROMPT).trim();\n        return Futures.of(new MultiFactorAuthResponse(totp.credentialId, Either.a(code)));\n    }\n\n    public static Terminal buildTerminal() {\n        try {\n            return TerminalBuilder.builder()\n                    .system(true)\n                    .build();\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe);\n        }\n\n    }\n\n    @Override\n    public void run() {\n        DefaultParser parser = new DefaultParser();\n        LineReader reader = LineReaderBuilder.builder()\n                .terminal(cliContext.terminal)\n                .completer(buildCompleter())\n                .parser(parser)\n                .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)\n//                .variable(LineReader.SECONDARY_PROMPT_PATTERN, \"%M%P > \")\n                .build();\n        boolean color = true;\n\n        while (!isFinished) {\n            while (!isFinished) {\n                String line = null;\n                try {\n                    line = reader.readLine(buildPrompt(), null, (MaskingCallback) null, null);\n                } catch (UserInterruptException e) {\n                    // Ignore\n                } catch (EndOfFileException e) {\n                    return;\n                }\n                if (line == null) {\n                    continue;\n                }\n\n                ParsedCommand parsedCommand = null;\n                try {\n                    parsedCommand = fromLine(line);\n                } catch (Exception ex) {\n                    System.out.println(\"Could not parse command.\");\n                    continue;\n                }\n\n                String response = handle(parsedCommand, cliContext.terminal, reader);\n//                if (color) {\n//                    terminal.writer().println(\n//                            AttributedString.fromAnsi(\"\\u001B[0m\\\"\" + response + \"\\\"\")\n//                                    .toAnsi(terminal));\n//\n//                } else {\n                cliContext.terminal.writer().println(response);\n//                }\n                cliContext.terminal.flush();\n            }\n        }\n    }\n\n    private static Crypto CRYPTO;\n\n    public static void buildAndRun(Args args) {\n        CRYPTO = Main.initCrypto();\n        PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, CRYPTO.signer);\n        disableLogSpam();\n        ThumbnailGenerator.setInstance(new JavaImageThumbnailer());\n        Logging.LOG().setLevel(Level.WARNING);\n        CLIContext cliContext = buildContextFromCLI(args);\n        new CLI(cliContext).run();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/cli/CLIContext.java",
    "content": "package peergos.server.cli;\n\nimport org.jline.terminal.Terminal;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\n\npublic class CLIContext {\n    public final Terminal terminal;\n    public final UserContext userContext;\n    public final String serverURL, username;\n    public final Path home;\n    public Path pwd, lpwd;\n\n    public CLIContext(Terminal terminal, UserContext userContext, String serverURL, String username) {\n        this.terminal = terminal;\n        this.userContext = userContext;\n        this.serverURL = serverURL;\n        this.username = username;\n        this.home = Paths.get(\"/\" + username);\n        this.pwd = Paths.get(\"/\" + username);\n        this.lpwd = Paths.get(System.getProperty(\"user.dir\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/cli/Command.java",
    "content": "package peergos.server.cli;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Set;\nimport java.util.stream.Stream;\n\npublic enum Command {\n    help(\"Show this help.\"),\n    exit(\"Disconnect.\"),\n    get(\"Download a file.\", \"get remote-path <local path>\", Set.of(Flag.SKIP_EXISTING), Argument.REMOTE_FILE, Argument.LOCAL_FILE),\n    mkdir(\"Create a directory\", \"mkdir dir-name\", Argument.REMOTE_DIR),\n    put(\"Upload a file or folder.\", \"put <--skip-existing> local-path <remote-path> \", Set.of(Flag.SKIP_EXISTING), Argument.LOCAL_FILE, Argument.REMOTE_FILE),\n    ls(\"List contents of a remote directory.\", \"ls <path>\", Argument.REMOTE_FILE),\n    lls(\"List contents of a local directory.\", \"lls <path>\", Argument.LOCAL_FILE),\n    rm(\"Remove a remote-file.\", \"rm remote-path\", Argument.REMOTE_FILE),\n    space(\"Show used remote space.\"),\n    get_follow_requests(\"Show the users that have sent you a follow request.\"),\n    process_follow_request(\"Accept or reject a pending follow-request.\", \"process_follow_request pending-follower accept|accept-and-reciprocate|reject\", Argument.PENDING_FOLLOW_REQUEST, Argument.PROCESS_FOLLOW_REQUEST),\n    follow(\"Send a follow-request to another user.\", \"follow user\", Argument.USERNAME),\n    share_read(\"Grant read access for a file to another user.\", \"share_read remote-path user\", Argument.REMOTE_FILE, Argument.FOLLOWER),\n    passwd(\"Update your password.\"),\n    cd(\"change (remote) directory.\", \"cd <remote-path>\", Argument.REMOTE_DIR),\n    lcd(\"change (local) directory.\", \"lcd local-path\", Argument.LOCAL_DIR),\n    pwd(\"Print (remote) working directory.\"),\n    lpwd(\"Print (local) working directory.\"),\n    quit(\"Disconnect.\"),\n    bye(\"Disconnect.\");\n\n    public final String description, example;\n    public final Set<Flag> flags;\n    public final Argument firstArg, secondArg, thirdArg;\n\n    Command(String description, String example, Set<Flag> flags, Argument firstArg, Argument secondArg, Argument thirdArg) {\n        if (firstArg == null && secondArg != null)\n            throw new IllegalArgumentException();\n        if (secondArg == null && thirdArg != null)\n            throw new IllegalArgumentException();\n\n        this.description = description;\n        this.example = example;\n        this.flags = flags;\n        this.firstArg = firstArg;\n        this.secondArg = secondArg;\n        this.thirdArg = thirdArg;\n    }\n\n    Command(String description, String example, Set<Flag> flags, Argument firstArg, Argument secondArg) {\n        this(description, example, flags, firstArg, secondArg, null);\n    }\n\n    Command(String description, String example, Argument firstArg, Argument secondArg) {\n        this(description, example, Collections.emptySet(), firstArg, secondArg, null);\n    }\n\n    Command(String description, String example, Set<Flag> flags, Argument firstArg) {\n        this(description, example, flags, firstArg,null, null);\n    }\n\n    Command(String description, String example, Argument firstArg) {\n        this(description, example, Collections.emptySet(), firstArg,null, null);\n    }\n\n    Command(String description, String example, Set<Flag> flags) {\n        this(description, example, flags, null,null, null);\n    }\n\n    Command(String description, Set<Flag> flags) {\n        this(description, null, flags);\n    }\n\n    Command(String description) {\n        this(description, Collections.emptySet());\n    }\n\n    public static int maxLength() {\n        return Stream.of(values())\n                .mapToInt(e -> e.example().length())\n                .max()\n                .getAsInt();\n    }\n\n    public String example() {\n        return example == null ? name() : example;\n    }\n\n    public static Command parse(String cmd) {\n        try {\n            return Command.valueOf(cmd);\n        } catch (IllegalArgumentException | NullPointerException ex) {\n            if (\"?\".equals(cmd))\n                return help;\n            throw new IllegalStateException(\"Specified command \" + cmd + \" is not a valid command : \" + new ArrayList<>(Arrays.asList(values())));\n        }\n    }\n\n    public enum Argument {\n        REMOTE_FILE,\n        REMOTE_DIR,\n        LOCAL_FILE,\n        LOCAL_DIR,\n        SKIP_EXISTING,\n        USERNAME,\n        FOLLOWER,\n        PENDING_FOLLOW_REQUEST,\n        PROCESS_FOLLOW_REQUEST;\n    }\n\n    public enum Flag {\n        SKIP_EXISTING(\"--skip-existing\"),\n        RESUME_UPLOAD(\"--resume-upload\");\n\n        public final String flag;\n\n        Flag(String flag) {\n            this.flag = flag;\n        }\n    }\n\n    public enum ProcessFollowRequestAction  {\n        accept,\n        accept_and_reciprocate(\"accept-and-reciprocate\"),\n        reject;\n\n        private String alternative;\n\n        ProcessFollowRequestAction() {\n            this(null);\n        }\n        ProcessFollowRequestAction(String alternative) {\n            this.alternative = alternative;\n        }\n\n        public static ProcessFollowRequestAction parse(String s) {\n            try {\n                return Command.ProcessFollowRequestAction.valueOf(s);\n            } catch (IllegalArgumentException | NullPointerException ex) {\n                // try alternative\n                for (ProcessFollowRequestAction value : ProcessFollowRequestAction.values()) {\n                    if (s.equals(value.alternative))\n                        return value;\n                }\n                throw ex;\n            }\n        }\n\n        public String altOrName() {\n            return alternative == null ? name() : alternative;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/cli/ListFilesCompleter.java",
    "content": "package peergos.server.cli;\n\nimport org.jline.reader.*;\nimport java.util.List;\nimport java.util.function.Function;\n\n\npublic class ListFilesCompleter implements Completer {\n    /**\n     * Auto completer for files/directories in  the pwd of the remote Peergos file-system.\n     * @param lineReader\n     * @param parsedLine\n     * @param list\n     */\n    private final Function<String, List<String>> lsSupplier; //lsSupplier(path) -> children of path\n\n    public ListFilesCompleter(Function<String, List<String>> lsSupplier) {\n        this.lsSupplier = lsSupplier;\n    }\n\n    @Override\n    public void complete(LineReader lineReader, ParsedLine parsedLine, List<Candidate> list) {\n        String remotePathPartialArg = parsedLine.word();\n        List<String> remotePathChildren = lsSupplier.apply(remotePathPartialArg);\n        remotePathChildren.stream()\n                .map(Candidate::new)\n                .forEach(list::add);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/cli/ParsedCommand.java",
    "content": "package peergos.server.cli;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\npublic class ParsedCommand {\n    public final Command cmd;\n    public final String line;\n    public final Set<String> flags;\n    public final List<String> arguments;\n\n    ParsedCommand(Command cmd, String line, List<String> args) {\n        this.cmd = cmd;\n        this.line = line;\n        this.flags = new HashSet<>();\n        this.arguments = new ArrayList<>(); // words without the cmd\n        boolean inEscape = false;\n        for (int i=0; i < args.size(); i++) {\n            String arg = args.get(i);\n            if (arg.startsWith(\"--\")) {\n                flags.add(arg);\n                continue;\n            }\n            if (arg.startsWith(\"\\\"\")) {\n                inEscape = true;\n                arg = arg.substring(1);\n                if (arg.endsWith(\"\\\"\"))\n                    arg = arg.substring(0, arg.length() - 1);\n                arguments.add(arg);\n                continue;\n            }\n            if (arg.endsWith(\"\\\"\")) {\n                inEscape = false;\n                arg = arg.substring(0, arg.length() - 1);\n                arguments.set(arguments.size() - 1, arguments.get(arguments.size() - 1) + \" \" + arg);\n                continue;\n            }\n            while (arg.endsWith(\"\\\\\") && i < args.size() - 1) {\n                arg = arg.substring(0, arg.length() - 1) + \" \" + args.get(i+1);\n                i++;\n            }\n\n            if (inEscape)\n                arguments.set(arguments.size() - 1, arguments.get(arguments.size() - 1) + \" \" + arg);\n            else\n                arguments.add(arg);\n        }\n    }\n\n    public boolean hasArguments() {\n        return !arguments.isEmpty();\n    }\n\n    public boolean hasSecondArgument() {\n        return arguments.size() > 1;\n    }\n\n    public boolean hasThirdArgument() {\n        return arguments.size() > 2;\n    }\n\n    public String firstArgument() {\n        if (arguments.size() < 1)\n            throw new IllegalStateException(\"Specifed command \" + line + \" requires an argument\");\n        return arguments.get(0);\n    }\n\n    public String secondArgument() {\n        if (arguments.size() < 2)\n            throw new IllegalStateException(\"Specifed command \" + line + \" requires a second argument\");\n        return arguments.get(1);\n    }\n\n    public String thirdArgument() {\n        if (arguments.size() < 3)\n            throw new IllegalStateException(\"Specifed command \" + line + \" requires a third argument\");\n        return arguments.get(2);\n    }\n\n    @Override\n    public String toString() {\n        return \"ParsedCommand{\" +\n                \"cmd=\" + cmd +\n                \", line='\" + line + '\\'' +\n                \", arguments=\" + arguments +\n                '}';\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/cli/ProgressBar.java",
    "content": "package peergos.server.cli;\n\nimport java.io.PrintWriter;\nimport java.nio.file.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.stream.*;\n\npublic class ProgressBar {\n\n    private static final String[] ANIM = new String[]{\"|\", \"/\", \"-\", \"\\\\\"};\n    private static final int PROGRESS_BAR_LENGTH = 20;\n    private final AtomicLong totalFiles;\n    private final AtomicLong currentFile;\n    private final Path relativePath;\n    private final String filename;\n    private long accumulatedBytes;\n    private int animationPosition;\n\n    public ProgressBar(AtomicLong currentFile, AtomicLong totalFiles, Path relativePath, String filename) {\n        this.totalFiles = totalFiles;\n        this.currentFile = currentFile;\n        this.relativePath = relativePath;\n        this.filename = filename;\n    }\n\n    private String prefix() {\n        return \"(\" + currentFile + \"/\" + totalFiles + \") \";\n    }\n\n    public void update(PrintWriter writer, long bytesSoFar, long totalBytes) {\n        String msg = format(bytesSoFar, totalBytes);\n        writer.print(msg);\n        writer.flush();\n    }\n\n    private String progressBar(long bytes, long  total) {\n        accumulatedBytes += bytes;\n        int barsProgressed = (int) (accumulatedBytes * PROGRESS_BAR_LENGTH / total);\n\n        StringBuilder sb = new StringBuilder();\n        sb.append('[');\n        for (int i = 0; i < PROGRESS_BAR_LENGTH; i++) {\n            if (i < barsProgressed)\n                sb.append('=');\n            else\n                sb.append(' ');\n        }\n        sb.append(']');\n        return sb.toString();\n    }\n\n    private String format(long bytes, long total) {\n        StringBuilder sb =  new StringBuilder(\"\\r\");\n        if (accumulatedBytes == 0) {\n            sb.append(\"\\n\");\n            currentFile.incrementAndGet();\n        }\n        sb.append(prefix());\n        String path = relativePath.resolve(filename).toString();\n        sb.append(path);\n        int alignChars = 52;\n        int startSize = prefix().length() + path.length();\n        if (startSize < alignChars)\n            sb.append(IntStream.range(0, alignChars - startSize).mapToObj(i -> \" \").collect(Collectors.joining()));\n        sb.append(updateAndGetAnimation());\n        sb.append(\"\\t\");\n        sb.append(progressBar(bytes, total));\n        sb.append(\"\\t\");\n        return sb.toString();\n\n    }\n\n    private String updateAndGetAnimation() {\n        return ANIM[animationPosition++ % ANIM.length];\n    }\n\n    public static void main(String[] args) throws Exception {\n        ProgressBar pb = new ProgressBar(new AtomicLong(0), new AtomicLong(1), Paths.get(\"/home\"), \"somefile\");\n        int size = 1000;\n        PrintWriter writer = new PrintWriter(System.out);\n        for (int i = 10; i <= size; i+=10) {\n            pb.update(writer, 10, size);\n            Thread.sleep(250);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/cli/SupplierCompleter.java",
    "content": "package peergos.server.cli;\n\nimport org.jline.reader.Candidate;\nimport org.jline.reader.Completer;\nimport org.jline.reader.LineReader;\nimport org.jline.reader.ParsedLine;\n\nimport java.util.List;\nimport java.util.function.Supplier;\n\n/**\n * Generalized completer that presents a list of options\n */\npublic class SupplierCompleter implements Completer {\n    Supplier<List<String>> optionsSupplier;\n\n    public SupplierCompleter(Supplier<List<String>> optionsSuppllier) {\n        this.optionsSupplier = optionsSuppllier;\n    }\n\n    @Override\n    public void complete(LineReader lineReader, ParsedLine parsedLine, List<Candidate> list) {\n        optionsSupplier.get()\n                .stream()\n                .map(Candidate::new)\n                .forEach(list::add);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/CorenodeEvent.java",
    "content": "package peergos.server.corenode;\n\nimport peergos.shared.crypto.hash.*;\n\n/** This propagates a user changing their root public key (by signing up, or changing their password)\n *\n */\npublic class CorenodeEvent {\n\n    public final String username;\n    public final PublicKeyHash keyHash;\n\n    public CorenodeEvent(String username, PublicKeyHash keyHash) {\n        this.username = username;\n        this.keyHash = keyHash;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/CorenodeEventPropagator.java",
    "content": "package peergos.server.corenode;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\n/** This class propagates core node writes to\n *\n */\npublic class CorenodeEventPropagator implements CoreNode {\n\n    public final CoreNode target;\n    private final List<Function<? super CorenodeEvent, CompletableFuture<Boolean>>> listeners = new ArrayList<>();\n\n    public CorenodeEventPropagator(CoreNode target) {\n        this.target = target;\n    }\n\n    public void addListener(Function<? super CorenodeEvent, CompletableFuture<Boolean>> listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink chain,\n                                                                  OpLog setupOperations,\n                                                                  ProofOfWork proof,\n                                                                  String token) {\n        return target.signup(username, chain, setupOperations, proof, token)\n                .thenApply(res -> {\n                    if (res.isEmpty()) {\n                        processEvent(Arrays.asList(chain)).join();\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username,\n                                                                                            UserPublicKeyLink chain,\n                                                                                            ProofOfWork proof) {\n        return target.startPaidSignup(username, chain, proof);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username,\n                                                                   UserPublicKeyLink chain,\n                                                                   OpLog setupOperations,\n                                                                   byte[] signedSpaceRequest,\n                                                                   ProofOfWork proof) {\n        return target.completePaidSignup(username, chain, setupOperations, signedSpaceRequest, proof)\n                .thenApply(res -> {\n                    processEvent(Arrays.asList(chain)).join();\n                    return res;\n                });\n\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        return target.startMirror(username, mirrorBat, auth, proof);\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        return target.startPaidMirror(username, auth, proof).thenApply(props -> {\n            if (props.isA() && ! props.a().hasError()) {\n                for (Function<? super CorenodeEvent, CompletableFuture<Boolean>> listener : listeners) {\n                    listener.apply(new CorenodeEvent(username, getPublicKeyHash(username).join().get()));\n                }\n            }\n            return props;\n        });\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        return target.completePaidMirror(username, mirrorBat, signedSpaceRequest, proof)\n                .thenApply(props -> {\n                    if (! props.hasError()) {\n                        for (Function<? super CorenodeEvent, CompletableFuture<Boolean>> listener : listeners) {\n                            listener.apply(new CorenodeEvent(username, getPublicKeyHash(username).join().get()));\n                        }\n                    }\n                    return props;\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        return target.getSnapshots(prefix, instanceBat, lastLinkCountsUpdate);\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                       List<UserPublicKeyLink> chain,\n                                                                       ProofOfWork proof,\n                                                                       String token) {\n        return target.updateChain(username, chain, proof, token)\n                .thenApply(res -> {\n                    if (res.isEmpty()) {\n                        processEvent(chain);\n                    }\n                    return res;\n                });\n    }\n\n    private CompletableFuture<Boolean> processEvent(List<UserPublicKeyLink> chain) {\n        UserPublicKeyLink last = chain.get(chain.size() - 1);\n        CorenodeEvent event = new CorenodeEvent(last.claim.username, last.owner);\n        List<CompletableFuture<Boolean>> all = new ArrayList<>();\n        for (Function<? super CorenodeEvent, CompletableFuture<Boolean>> listener : listeners) {\n            all.add(listener.apply(event));\n        }\n        return Futures.combineAll(all).thenApply(x -> true);\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash key) {\n        return target.getUsername(key);\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        return target.getChain(username);\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        return target.getUsernames(prefix);\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime latestLinkCountUpdate,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        return target.migrateUser(username, newChain, currentStorageId, mirrorBat, latestLinkCountUpdate, currentUsage, commitToPki).thenApply(res -> {\n            processEvent(newChain);\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return target.getNextServerId(serverId);\n    }\n\n    @Override\n    public void close() throws IOException {\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/IpfsCoreNode.java",
    "content": "package peergos.server.corenode;\nimport java.time.*;\nimport java.util.logging.*;\n\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class IpfsCoreNode implements CoreNode {\n\tprivate static final Logger LOG = Logging.LOG();\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n\tpublic static final int MAX_FREE_IDENTITY_CHANGES = 10;\n\n    private final PublicKeyHash peergosIdentity;\n    private final DeletableContentAddressedStorage ipfs;\n    private final Hasher hasher;\n    private final Crypto crypto;\n    private final MutablePointers mutable;\n    private final Account account;\n    private final BatCave batCave;\n    private final SigningPrivateKeyAndPublicHash signer;\n\n    private final Map<String, List<UserPublicKeyLink>> chains = new ConcurrentHashMap<>();\n    private final Map<PublicKeyHash, String> reverseLookup = new ConcurrentHashMap<>();\n    private final List<String> usernames = new ArrayList<>();\n    private final DifficultyGenerator difficultyGenerator;\n    private final Map<String, PublicKeyHash> reservedUsernames = new ConcurrentHashMap<>();\n    private final Cid ourId;\n\n    private MaybeMultihash currentRoot;\n    private Optional<Long> currentSequence;\n\n    public IpfsCoreNode(SigningPrivateKeyAndPublicHash pkiSigner,\n                        int maxSignupsPerDay,\n                        DeletableContentAddressedStorage ipfs,\n                        Crypto crypto,\n                        MutablePointers mutable,\n                        Account account,\n                        BatCave batCave,\n                        PublicKeyHash peergosIdentity) {\n        this.currentRoot = MaybeMultihash.empty();\n        this.currentSequence = Optional.empty();\n        this.ipfs = ipfs;\n        this.ourId = ipfs.id().join();\n        this.hasher = crypto.hasher;\n        this.crypto = crypto;\n        this.mutable = mutable;\n        this.account = account;\n        this.batCave = batCave;\n        this.peergosIdentity = peergosIdentity;\n        this.signer = pkiSigner;\n        this.difficultyGenerator = new DifficultyGenerator(System.currentTimeMillis(), maxSignupsPerDay);\n        reverseLookup.put(peergosIdentity, \"peergos\");\n    }\n\n    @Override\n    public void initialize(boolean mirrorUsers) {\n        PointerUpdate currentPkiPointer = mutable.getPointerTarget(peergosIdentity, signer.publicKeyHash, ipfs).join();\n        Optional<Long> currentPkiSequence = currentPkiPointer.sequence;\n        MaybeMultihash currentPkiRoot = currentPkiPointer.updated;\n        LOG.info(\"Initializing PKI from root \" + currentPkiRoot);\n        update(currentPkiRoot, currentPkiSequence);\n        if (! currentPkiRoot.isPresent()) {\n            CommittedWriterData committed = IpfsTransaction.call(peergosIdentity,\n                    tid -> WriterData.createEmpty(peergosIdentity, signer, ipfs, hasher, tid).join()\n                            .commit(peergosIdentity, signer, MaybeMultihash.empty(), Optional.of(1L), mutable, ipfs, hasher, tid)\n                            .thenApply(version -> version.get(signer)), ipfs).join();\n            currentPkiRoot = committed.hash;\n            currentPkiSequence = committed.sequence;\n            update(currentPkiRoot, currentPkiSequence);\n        }\n    }\n    public static CompletableFuture<byte[]> keyHash(ByteArrayWrapper username) {\n        return Futures.of(Blake2b.Digest.newInstance().digest(username.data));\n    }\n\n    /** Update the existing mappings based on the diff between the current champ and the champ with the supplied root.\n     *\n     * @param newRoot The root of the new champ\n     */\n    private synchronized void update(MaybeMultihash newRoot, Optional<Long> newSequence) {\n        updateAllMappings(Arrays.asList(ipfs.id().join()), peergosIdentity, currentRoot, newRoot,\n                ourId, hasher, ipfs, chains, reverseLookup, usernames);\n        this.currentRoot = newRoot;\n        this.currentSequence = newSequence;\n    }\n\n    public static CompletableFuture<CommittedWriterData> getWriterData(List<Multihash> peerIds,\n                                                                       PublicKeyHash owner,\n                                                                       Cid hash,\n                                                                       Optional<Long> sequence,\n                                                                       Cid ourId,\n                                                                       Hasher hasher,\n                                                                       DeletableContentAddressedStorage dht) {\n        return dht.get(peerIds, owner, hash, Optional.empty(), ourId, hasher, true)\n                .thenApply(cborOpt -> {\n                    if (! cborOpt.isPresent())\n                        throw new IllegalStateException(\"Couldn't retrieve WriterData! \" + hash);\n                    return new CommittedWriterData(MaybeMultihash.of(hash), WriterData.fromCbor(cborOpt.get()), sequence);\n                });\n    }\n\n    public static MaybeMultihash getTreeRoot(List<Multihash> peerIds,\n                                             PublicKeyHash owner,\n                                             MaybeMultihash pointerTarget,\n                                             Cid ourId,\n                                             Hasher hasher,\n                                             DeletableContentAddressedStorage ipfs) {\n        if (! pointerTarget.isPresent())\n            return MaybeMultihash.empty();\n        CommittedWriterData current = getWriterData(peerIds, owner, (Cid)pointerTarget.get(), Optional.empty(), ourId, hasher, ipfs).join();\n        return current.props.get().tree.map(MaybeMultihash::of).orElseGet(MaybeMultihash::empty);\n\n    }\n\n    public static <V extends Cborable> CompletableFuture<Boolean> applyToDiff(\n            List<Multihash> storageProviders,\n            Cid ourId,\n            PublicKeyHash owner,\n            MaybeMultihash original,\n            MaybeMultihash updated,\n            int depth,\n            Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher,\n            List<Champ.KeyElement<V>> higherLeftMappings,\n            List<Champ.KeyElement<V>> higherRightMappings,\n            Consumer<Triple<ByteArrayWrapper, Optional<V>, Optional<V>>> consumer,\n            int bitWidth,\n            DeletableContentAddressedStorage storage,\n            Hasher cryptoHash,\n            Function<Cborable, V> fromCbor) {\n\n        if (updated.equals(original))\n            return CompletableFuture.completedFuture(true);\n        return original.map(h -> storage.get(storageProviders, owner, (Cid)h, Optional.empty(), ourId, cryptoHash, true)).orElseGet(() -> CompletableFuture.completedFuture(Optional.empty()))\n                .thenApply(rawOpt -> rawOpt.map(y -> Champ.fromCbor(y, fromCbor)))\n                .thenCompose(left -> updated.map(h -> storage.get(storageProviders, owner, (Cid)h, Optional.empty(), ourId, cryptoHash, true)).orElseGet(() -> CompletableFuture.completedFuture(Optional.empty()))\n                        .thenApply(rawOpt -> rawOpt.map(y -> Champ.fromCbor(y, fromCbor)))\n                        .thenCompose(right -> Champ.hashAndMaskKeys(higherLeftMappings, depth, bitWidth, hasher)\n                                .thenCompose(leftHigherMappingsByBit -> Champ.hashAndMaskKeys(higherRightMappings, depth, bitWidth, hasher)\n                                        .thenCompose(rightHigherMappingsByBit -> {\n\n                                            int leftMax = left.map(c -> Math.max(c.dataMap.length(), c.nodeMap.length())).orElse(0);\n                                            int rightMax = right.map(c -> Math.max(c.dataMap.length(), c.nodeMap.length())).orElse(0);\n                                            int maxBit = Math.max(leftMax, rightMax);\n                                            int leftDataIndex = 0, rightDataIndex = 0, leftNodeCount = 0, rightNodeCount = 0;\n\n                                            List<CompletableFuture<Boolean>> deeperLayers = new ArrayList<>();\n\n                                            for (int i = 0; i < maxBit; i++) {\n                                                // either the payload is present OR higher mappings are non empty OR the champ is absent\n                                                Optional<Champ.HashPrefixPayload<V>> leftPayload = Champ.getElement(i, leftDataIndex, leftNodeCount, left);\n                                                Optional<Champ.HashPrefixPayload<V>> rightPayload = Champ.getElement(i, rightDataIndex, rightNodeCount, right);\n\n                                                List<Champ.KeyElement<V>> leftHigherMappings = leftHigherMappingsByBit.getOrDefault(i, Collections.emptyList());\n                                                List<Champ.KeyElement<V>> leftMappings = leftPayload\n                                                        .filter(p -> !p.isShard())\n                                                        .map(p -> Arrays.asList(p.mappings))\n                                                        .orElse(leftHigherMappings);\n                                                List<Champ.KeyElement<V>> rightHigherMappings = rightHigherMappingsByBit.getOrDefault(i, Collections.emptyList());\n                                                List<Champ.KeyElement<V>> rightMappings = rightPayload\n                                                        .filter(p -> !p.isShard())\n                                                        .map(p -> Arrays.asList(p.mappings))\n                                                        .orElse(rightHigherMappings);\n\n                                                Optional<MaybeMultihash> leftShard = leftPayload\n                                                        .filter(p -> p.isShard())\n                                                        .map(p -> p.link);\n\n                                                Optional<MaybeMultihash> rightShard = rightPayload\n                                                        .filter(p -> p.isShard())\n                                                        .map(p -> p.link);\n\n                                                if (leftShard.isPresent() || rightShard.isPresent()) {\n                                                    deeperLayers.add(applyToDiff(storageProviders,\n                                                            ourId,\n                                                            owner,\n                                                            leftShard.orElse(MaybeMultihash.empty()),\n                                                            rightShard.orElse(MaybeMultihash.empty()), depth + 1, hasher,\n                                                            leftMappings, rightMappings, consumer, bitWidth, storage, cryptoHash, fromCbor));\n                                                } else {\n                                                    Map<ByteArrayWrapper, Optional<V>> leftMap = leftMappings.stream()\n                                                            .collect(Collectors.toMap(e -> e.key, e -> e.valueHash));\n                                                    Map<ByteArrayWrapper, Optional<V>> rightMap = rightMappings.stream()\n                                                            .collect(Collectors.toMap(e -> e.key, e -> e.valueHash));\n\n                                                    HashSet<ByteArrayWrapper> both = new HashSet<>(leftMap.keySet());\n                                                    both.retainAll(rightMap.keySet());\n\n                                                    for (Map.Entry<ByteArrayWrapper, Optional<V>> entry : leftMap.entrySet()) {\n                                                        if (! both.contains(entry.getKey()))\n                                                            consumer.accept(new Triple<>(entry.getKey(), entry.getValue(), Optional.empty()));\n                                                        else if (! entry.getValue().equals(rightMap.get(entry.getKey())))\n                                                            consumer.accept(new Triple<>(entry.getKey(), entry.getValue(), rightMap.get(entry.getKey())));\n                                                    }\n                                                    for (Map.Entry<ByteArrayWrapper, Optional<V>> entry : rightMap.entrySet()) {\n                                                        if (! both.contains(entry.getKey()))\n                                                            consumer.accept(new Triple<>(entry.getKey(), Optional.empty(), entry.getValue()));\n                                                    }\n                                                }\n\n                                                if (leftPayload.isPresent()) {\n                                                    if (leftPayload.get().isShard())\n                                                        leftNodeCount++;\n                                                    else\n                                                        leftDataIndex++;\n                                                }\n                                                if (rightPayload.isPresent()) {\n                                                    if (rightPayload.get().isShard())\n                                                        rightNodeCount++;\n                                                    else\n                                                        rightDataIndex++;\n                                                }\n                                            }\n\n                                            return Futures.combineAll(deeperLayers).thenApply(x -> true);\n                                        })))\n                );\n    }\n\n    public static void updateAllMappings(List<Multihash> peerIds,\n                                         PublicKeyHash owner,\n                                         MaybeMultihash currentChampRoot,\n                                         MaybeMultihash newChampRoot,\n                                         Cid ourId,\n                                         Hasher hasher,\n                                         DeletableContentAddressedStorage ipfs,\n                                         Map<String, List<UserPublicKeyLink>> chains,\n                                         Map<PublicKeyHash, String> reverseLookup,\n                                         List<String> usernames) {\n        try {\n            MaybeMultihash currentTree = getTreeRoot(peerIds, owner, currentChampRoot, ourId, hasher, ipfs);\n            MaybeMultihash updatedTree = getTreeRoot(peerIds, owner, newChampRoot, ourId, hasher, ipfs);\n            LOG.info(\"Updating pki to new tree root \" + updatedTree);\n            Consumer<Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> consumer =\n                    t -> updateMapping(peerIds, owner, t.left, t.middle, t.right, ipfs, chains, reverseLookup, usernames, ourId, hasher);\n            Function<Cborable, CborObject.CborMerkleLink> fromCbor = c -> (CborObject.CborMerkleLink)c;\n            IpfsCoreNode.applyToDiff(peerIds, ourId, owner, currentTree, updatedTree, 0, IpfsCoreNode::keyHash,\n                    Collections.emptyList(), Collections.emptyList(),\n                    consumer, ChampWrapper.BIT_WIDTH, ipfs, hasher, fromCbor)\n                    .orTimeout(2, TimeUnit.MINUTES).join();\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    public static void updateMapping(List<Multihash> peerIds,\n                                     PublicKeyHash owner,\n                                     ByteArrayWrapper key,\n                                     Optional<CborObject.CborMerkleLink> oldValue,\n                                     Optional<CborObject.CborMerkleLink> newValue,\n                                     DeletableContentAddressedStorage ipfs,\n                                     Map<String, List<UserPublicKeyLink>> chains,\n                                     Map<PublicKeyHash, String> reverseLookup,\n                                     List<String> usernames,\n                                     Cid ourId,\n                                     Hasher hasher) {\n        try {\n            Optional<CborObject> cborOpt = ipfs.get(peerIds, owner, (Cid)newValue.get().target, Optional.empty(), ourId, hasher, true)\n                    .orTimeout(30, TimeUnit.SECONDS).join();\n            if (!cborOpt.isPresent()) {\n                LOG.severe(\"Couldn't retrieve new claim chain from \" + newValue);\n                return;\n            }\n\n            List<UserPublicKeyLink> updatedChain = ((CborObject.CborList) cborOpt.get()).value.stream()\n                    .map(UserPublicKeyLink::fromCbor)\n                    .collect(Collectors.toList());\n\n            String username = new String(key.data);\n\n            if (oldValue.isPresent()) {\n                Optional<CborObject> existingCborOpt = ipfs.get(peerIds, owner, (Cid)oldValue.get().target, Optional.empty(), ourId, hasher, true)\n                        .orTimeout(30, TimeUnit.SECONDS).join();\n                if (!existingCborOpt.isPresent()) {\n                    LOG.severe(\"Couldn't retrieve existing claim chain from \" + newValue);\n                    return;\n                }\n                List<UserPublicKeyLink> existingChain = ((CborObject.CborList) existingCborOpt.get()).value.stream()\n                        .map(UserPublicKeyLink::fromCbor)\n                        .collect(Collectors.toList());\n                // Check legality\n                UserPublicKeyLink.merge(existingChain, updatedChain, ipfs).orTimeout(30, TimeUnit.SECONDS).join();\n            }\n\n            for (UserPublicKeyLink link : updatedChain) {\n                reverseLookup.put(link.owner, username);\n            }\n            chains.put(username, updatedChain);\n            if (! oldValue.isPresent()) {\n                // This is a new user\n                usernames.add(username);\n            }\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n\n    /** Replay a series of block writes and pointer updates that form a signup\n     *\n     * @param owner\n     * @param ops\n     * @param ipfs\n     * @param mutable\n     */\n    public static void applyOpLog(String username,\n                                  PublicKeyHash owner,\n                                  OpLog ops,\n                                  ContentAddressedStorage ipfs,\n                                  MutablePointers mutable,\n                                  Account account,\n                                  BatCave batCave) {\n        TransactionId tid = ipfs.startTransaction(owner).join();\n        for (int i=0; i < ops.operations.size(); i++) {\n            Either<OpLog.PointerWrite, OpLog.BlockWrite> op = ops.operations.get(i);\n            if (op.isA()) {\n                OpLog.PointerWrite pointerUpdate = op.a();\n                mutable.setPointer(owner, pointerUpdate.writer, pointerUpdate.writerSignedChampRootCas).join();\n            } else {\n                OpLog.BlockWrite block = op.b();\n                // Group blocks of same type to do a parallel write (these should all be cbor, not raw)\n                List<byte[]> signatures = new ArrayList<>();\n                List<byte[]> blocks = new ArrayList<>();\n                signatures.add(block.signature);\n                blocks.add(block.block);\n                while (i+1 < ops.operations.size() && ops.operations.get(i + 1).isB() && ops.operations.get(i + 1).b().isRaw == block.isRaw) {\n                    signatures.add(ops.operations.get(i + 1).b().signature);\n                    blocks.add(ops.operations.get(i + 1).b().block);\n                    i++;\n                }\n                if (block.isRaw)\n                    ipfs.putRaw(owner, block.writer, signatures, blocks, tid, x -> {}).join();\n                else\n                    ipfs.put(owner, block.writer, signatures, blocks, tid).join();\n            }\n        }\n        if (ops.loginData != null) {\n            if (! ops.loginData.left.username.equals(username))\n                throw new IllegalStateException(\"Invalid signup data!\");\n            account.setLoginData(ops.loginData.left, ops.loginData.right, false).join();\n        }\n        if (ops.mirrorBat.isPresent()) {\n            Pair<BatWithId, byte[]> p = ops.mirrorBat.get();\n            batCave.addBat(username, p.left.id(), p.left.bat, p.right);\n        }\n        ipfs.closeTransaction(owner, tid).join();\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink chain,\n                                                                  OpLog setupOperations,\n                                                                  ProofOfWork proof,\n                                                                  String token) {\n        Optional<RequiredDifficulty> pkiResult = updateChain(username, Arrays.asList(chain), proof, token).join();\n        if (pkiResult.isPresent())\n            return Futures.of(pkiResult);\n\n        applyOpLog(username, chain.owner, setupOperations, ipfs, mutable, account, batCave);\n\n        return Futures.of(Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username,\n                                                                                            UserPublicKeyLink chain,\n                                                                                            ProofOfWork proof) {\n        // reserve the username after checking proof of work is sufficient\n        Optional<RequiredDifficulty> retry = enforceRateLimit(proof, Arrays.asList(chain));\n        if (retry.isPresent())\n            return Futures.of(Either.b(retry.get()));\n\n        PublicKeyHash identity = reservedUsernames.get(chain.claim.username);\n        if (identity != null && ! identity.equals(chain.owner))\n            return Futures.errored(new IllegalStateException(\"Username already reserved!\"));\n        reservedUsernames.put(username, chain.owner);\n        return Futures.of(Either.a(new PaymentProperties(0)));\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username,\n                                                                   UserPublicKeyLink chain,\n                                                                   OpLog setupOperations,\n                                                                   byte[] signedSpaceRequest,\n                                                                   ProofOfWork proof) {\n        if (! reservedUsernames.containsKey(username))\n            return Futures.errored(new IllegalStateException(\"Username not reserved!\"));\n        if (! chain.claim.username.equals(username))\n            return Futures.errored(new IllegalStateException(\"Username different from that in claim!\"));\n        if (! reservedUsernames.get(username).equals(chain.owner))\n            return Futures.errored(new IllegalStateException(\"Username is reserved by a different key pair!\"));\n\n        updateChain(username, List.of(chain), proof, \"\", false).join();\n        return Futures.of(new PaymentProperties(0));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    private Optional<RequiredDifficulty> enforceRateLimit(ProofOfWork proof, List<UserPublicKeyLink> updatedChain) {\n        byte[] hash = hasher.sha256(ArrayOps.concat(proof.prefix, new CborObject.CborList(updatedChain).serialize())).join();\n        difficultyGenerator.updateTime(System.currentTimeMillis());\n        int requiredDifficulty = difficultyGenerator.currentDifficulty();\n        if (! ProofOfWork.satisfiesDifficulty(requiredDifficulty, hash)) {\n            LOG.log(Level.INFO, \"Rejected request with insufficient proof of work for difficulty: \" +\n                    requiredDifficulty + \" and username \" + updatedChain.get(updatedChain.size() - 1).claim.username);\n            return Optional.of(new RequiredDifficulty(requiredDifficulty));\n        }\n        difficultyGenerator.addEvent();\n        return Optional.empty();\n    }\n\n    /** Update a user's public key chain, keeping the in memory mappings correct and committing the new pki root\n     *\n     * @param username\n     * @param updatedChain\n     * @return\n     */\n    @Override\n    public synchronized CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                                    List<UserPublicKeyLink> updatedChain,\n                                                                                    ProofOfWork proof,\n                                                                                    String token) {\n        return updateChain(username, updatedChain, proof, token, true);\n    }\n\n    synchronized CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                             List<UserPublicKeyLink> updatedChain,\n                                                                             ProofOfWork proof,\n                                                                             String token,\n                                                                             boolean rateLimit) {\n        if (! UsernameValidator.isValidUsername(username))\n            throw new IllegalStateException(\"Invalid username\");\n\n        try {\n            CommittedWriterData current = WriterData.getWriterData(peergosIdentity, (Cid)currentRoot.get(), currentSequence, ipfs).get();\n            MaybeMultihash currentTree = current.props.get().tree.map(MaybeMultihash::of).orElseGet(MaybeMultihash::empty);\n\n            ChampWrapper<CborObject.CborMerkleLink> champ = currentTree.isPresent() ?\n                    ChampWrapper.create(peergosIdentity, (Cid)currentTree.get(), Optional.empty(), IpfsCoreNode::keyHash, ipfs, hasher, c -> (CborObject.CborMerkleLink)c).get() :\n                    IpfsTransaction.call(peergosIdentity,\n                            tid -> ChampWrapper.create(peergosIdentity, signer, IpfsCoreNode::keyHash, tid, ipfs, hasher, c -> (CborObject.CborMerkleLink)c),\n                            ipfs).get();\n            Optional<CborObject.CborMerkleLink> existing = champ.get(username.getBytes()).get();\n            Optional<CborObject> cborOpt = existing.isPresent() ?\n                    ipfs.get(peergosIdentity, (Cid) existing.get().target, Optional.empty()).get() :\n                    Optional.empty();\n            if (! cborOpt.isPresent() && existing.isPresent()) {\n                LOG.severe(\"Couldn't retrieve existing claim chain from \" + existing + \" for \" + username);\n                return Futures.of(Optional.empty());\n            }\n            List<UserPublicKeyLink> existingChain = cborOpt.map(cbor -> ((CborObject.CborList) cbor).value.stream()\n                    .map(UserPublicKeyLink::fromCbor)\n                    .collect(Collectors.toList()))\n                    .orElse(Collections.emptyList());\n\n            // Check proof of work is sufficient, unless it is an identity change\n            if (rateLimit) {\n                if (existingChain.isEmpty() || existingChain.size() > MAX_FREE_IDENTITY_CHANGES) {\n                    Optional<RequiredDifficulty> retry = enforceRateLimit(proof, updatedChain);\n                    if (retry.isPresent())\n                        return Futures.of(retry);\n                }\n            }\n\n            List<UserPublicKeyLink> mergedChain = UserPublicKeyLink.merge(existingChain, updatedChain, ipfs).get();\n            CborObject.CborList mergedChainCbor = new CborObject.CborList(mergedChain.stream()\n                    .map(Cborable::toCbor)\n                    .collect(Collectors.toList()));\n            Multihash mergedChainHash = IpfsTransaction.call(peergosIdentity,\n                    tid -> ipfs.put(peergosIdentity, signer, mergedChainCbor.toByteArray(), hasher, tid),\n                    ipfs).get();\n            synchronized (this) {\n                return IpfsTransaction.call(peergosIdentity,\n                        tid -> champ.put(peergosIdentity, signer, username.getBytes(), existing, new CborObject.CborMerkleLink(mergedChainHash), Optional.empty(), tid)\n                                .thenCompose(newPkiRoot -> current.props.get().withChamp(newPkiRoot)\n                                        .commit(peergosIdentity, signer, currentRoot, currentSequence, mutable, ipfs, hasher, tid)),\n                        ipfs\n                ).thenApply(committed -> {\n                    if (existingChain.isEmpty())\n                        usernames.add(username);\n                    PublicKeyHash owner = updatedChain.get(updatedChain.size() - 1).owner;\n                    reverseLookup.put(owner, username);\n                    chains.put(username, mergedChain);\n                    currentRoot = committed.get(signer).hash;\n                    currentSequence = committed.get(signer).sequence;\n                    return Optional.empty();\n                });\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public synchronized CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        return CompletableFuture.completedFuture(chains.getOrDefault(username, Collections.emptyList()));\n    }\n\n    @Override\n    public synchronized CompletableFuture<String> getUsername(PublicKeyHash key) {\n        return CompletableFuture.completedFuture(Optional.ofNullable(reverseLookup.get(key))\n                .orElseThrow(() -> new IllegalStateException(\"Unknown identity key: \" + key)));\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        return CompletableFuture.completedFuture(usernames);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return ipfs.getIpnsEntry(serverId)\n                .thenApply(e -> e.getValue(serverId, crypto).join().host);\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime latestLinkCountUpdate,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        throw new IllegalStateException(\"Migration from pki node unimplemented!\");\n    }\n\n    @Override\n    public void close() throws IOException {}\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/JdbcIpnsAndSocial.java",
    "content": "package peergos.server.corenode;\nimport java.util.function.*;\nimport java.util.logging.*;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class JdbcIpnsAndSocial {\n\n    private static final Logger LOG = Logging.LOG();\n\n    private static final String FOLLOW_REQUEST_USER_NAME = \"name\";\n    private static final String FOLLOW_REQUEST_DATA_NAME = \"followrequest\";\n    private static final String INSERT_FOLLOW_REQUEST = \"INSERT INTO followrequests (name, followrequest) VALUES(?, ?);\";\n    private static final String SELECT_FOLLOW_REQUESTS = \"SELECT name, followrequest FROM followrequests WHERE name = ?;\";\n    private static final String DELETE_FOLLOW_REQUEST = \"DELETE FROM followrequests WHERE name = ? AND followrequest = ?;\";\n\n    private static final String IPNS_TARGET_NAME = \"hash\";\n    private static final String IPNS_CREATE = \"INSERT INTO metadatablobs (writingkey, hash) VALUES(?, ?)\";\n    private static final String IPNS_UPDATE = \"UPDATE metadatablobs SET hash=? WHERE writingkey = ? AND hash = ?\";\n    private static final String IPNS_DELETE = \"DELETE FROM metadatablobs WHERE writingkey = ?;\";\n    private static final String IPNS_GET = \"SELECT * FROM metadatablobs WHERE writingKey = ? LIMIT 1;\";\n\n    private class FollowRequestData {\n        public final String name;\n        public final byte[] data;\n        public final String b64string;\n\n        FollowRequestData(PublicKeyHash owner, byte[] publicKey) {\n            this(owner.toString(), publicKey);\n        }\n\n        FollowRequestData(String name, byte[] data) {\n            this(name,data,(data == null ? null: new String(Base64.getEncoder().encode(data))));\n        }\n\n        FollowRequestData(String name, String d) {\n            this(name, Base64.getDecoder().decode(d), d);\n        }\n\n        FollowRequestData(String name, byte[] data, String b64string) {\n            this.name = name;\n            this.data = data;\n            this.b64string = b64string;\n        }\n\n        public boolean insert() {\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(INSERT_FOLLOW_REQUEST)) {\n                insert.setString(1,this.name);\n                insert.setString(2,this.b64string);\n                insert.executeUpdate();\n                return true;\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return false;\n            }\n        }\n\n        public FollowRequestData[] select() {\n            try (Connection conn = getConnection();\n                 PreparedStatement select = conn.prepareStatement(SELECT_FOLLOW_REQUESTS)) {\n                select.setString(1, name);\n                ResultSet rs = select.executeQuery();\n                List<FollowRequestData> list = new ArrayList<>();\n                while (rs.next())\n                {\n                    String username = rs.getString(FOLLOW_REQUEST_USER_NAME);\n                    String b64string = rs.getString(FOLLOW_REQUEST_DATA_NAME);\n                    list.add(new FollowRequestData(username, b64string));\n                }\n                return list.toArray(new FollowRequestData[0]);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return null;\n            }\n        }\n\n        public boolean delete() {\n            try (Connection conn = getConnection();\n                 PreparedStatement delete = conn.prepareStatement(DELETE_FOLLOW_REQUEST)) {\n                delete.setString(1, name);\n                delete.setString(2, b64string);\n                delete.executeUpdate();\n                return true;\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return false;\n            }\n        }\n    }\n\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n\n    public JdbcIpnsAndSocial(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        return getConnection(true);\n    }\n\n    private Connection getConnection(boolean autoCommit) {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(autoCommit);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createFollowRequestsTableCommand(), conn);\n            commands.createTable(commands.createMutablePointersTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public CompletableFuture<Boolean> addFollowRequest(PublicKeyHash owner, byte[] encryptedPermission) {\n        byte[] dummy = null;\n        FollowRequestData selector = new FollowRequestData(owner, dummy);\n        FollowRequestData[] requests = selector.select();\n        if (requests != null && requests.length > SocialNetwork.MAX_PENDING_FOLLOWERS)\n            return CompletableFuture.completedFuture(false);\n        // ToDo add a crypto currency transaction to prevent spam\n\n        FollowRequestData request = new FollowRequestData(owner, encryptedPermission);\n        return CompletableFuture.completedFuture(request.insert());\n    }\n\n    public CompletableFuture<Boolean> removeFollowRequest(PublicKeyHash owner, byte[] unsigned) {\n        FollowRequestData request = new FollowRequestData(owner, unsigned);\n        return CompletableFuture.completedFuture(request.delete());\n    }\n\n    public CompletableFuture<byte[]> getFollowRequests(PublicKeyHash owner) {\n        byte[] dummy = null;\n        FollowRequestData request = new FollowRequestData(owner, dummy);\n        FollowRequestData[] requests = request.select();\n        if (requests == null)\n            return CompletableFuture.completedFuture(new byte[4]);\n\n        CborObject.CborList resp = new CborObject.CborList(Arrays.asList(requests).stream()\n                .map(req -> CborObject.fromByteArray(req.data))\n                .collect(Collectors.toList()));\n        return CompletableFuture.completedFuture(resp.serialize());\n    }\n\n    public List<byte[]> getRawFollowRequests(PublicKeyHash owner) {\n        byte[] dummy = null;\n        FollowRequestData request = new FollowRequestData(owner, dummy);\n        FollowRequestData[] requests = request.select();\n        if (requests == null)\n            return Collections.emptyList();\n\n        return Arrays.asList(requests).stream()\n                .map(req -> req.data)\n                .collect(Collectors.toList());\n    }\n\n    public List<BlindFollowRequest> getAndParseFollowRequests(PublicKeyHash owner) {\n        byte[] reqs = getFollowRequests(owner).join();\n        CborObject cbor = CborObject.fromByteArray(reqs);\n        if (!(cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for list of follow requests: \" + cbor);\n        return ((CborObject.CborList) cbor).value.stream()\n                .map(BlindFollowRequest::fromCbor)\n                .collect(Collectors.toList());\n    }\n\n    public void removePointer(PublicKeyHash writingKey) {\n        try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(IPNS_DELETE)) {\n                stmt.setString(1, new String(Base64.getEncoder().encode(writingKey.serialize())));\n                stmt.executeUpdate();\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                throw new RuntimeException(sqe);\n            }\n    }\n\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash writingKey, Optional<byte[]> existingCas, byte[] newCas) {\n        if (existingCas.isPresent()) {\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(IPNS_UPDATE)) {\n                conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n                String key = new String(Base64.getEncoder().encode(writingKey.serialize()));\n\n                insert.setString(1, new String(Base64.getEncoder().encode(newCas)));\n                insert.setString(2, key);\n                insert.setString(3, new String(Base64.getEncoder().encode(existingCas.get())));\n                int changed = insert.executeUpdate();\n                return CompletableFuture.completedFuture(changed > 0);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return CompletableFuture.completedFuture(false);\n            }\n        } else {\n            try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(IPNS_CREATE)) {\n                stmt.setString(1, new String(Base64.getEncoder().encode(writingKey.serialize())));\n                stmt.setString(2, new String(Base64.getEncoder().encode(newCas)));\n                stmt.executeUpdate();\n                return CompletableFuture.completedFuture(true);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return CompletableFuture.completedFuture(false);\n            }\n        }\n    }\n\n    public synchronized CompletableFuture<Boolean> setPointers(List<Optional<byte[]>> existing, List<SignedPointerUpdate> updates) {\n        Connection conn = getConnection(false);\n        try {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            if (existing.size() != updates.size())\n                throw new IllegalStateException(\"Argument mismatch!\");\n            for (int i=0; i < updates.size(); i++) {\n                SignedPointerUpdate u = updates.get(i);\n                String writerKey = new String(Base64.getEncoder().encode(u.writer.serialize()));\n                Optional<byte[]> current = existing.get(i);\n                if (current.isPresent()) {\n                    try (PreparedStatement stmt = conn.prepareStatement(IPNS_UPDATE)) {\n                        stmt.setString(1, new String(Base64.getEncoder().encode(u.signed)));\n                        stmt.setString(2, writerKey);\n                        stmt.setString(3, new String(Base64.getEncoder().encode(current.get())));\n                        int changed = stmt.executeUpdate();\n                        if (changed == 0) {\n                            conn.rollback();\n                            conn.setAutoCommit(true);\n                            return CompletableFuture.completedFuture(false);\n                        }\n                    }\n                } else {\n                    try (PreparedStatement stmt = conn.prepareStatement(IPNS_CREATE)) {\n                        stmt.setString(1, writerKey);\n                        stmt.setString(2, new String(Base64.getEncoder().encode(u.signed)));\n                        stmt.executeUpdate();\n                    }\n                }\n            }\n            conn.commit();\n            conn.setAutoCommit(true);\n            return CompletableFuture.completedFuture(true);\n        } catch (SQLException sqe) {\n            try { conn.rollback(); conn.setAutoCommit(true); } catch (SQLException ignored) {}\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            return Futures.errored(new RuntimeException(sqe));\n        } finally {\n            try { conn.close(); } catch (SQLException ignored) {}\n        }\n    }\n\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash writingKey) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(IPNS_GET)) {\n            stmt.setString(1, new String(Base64.getEncoder().encode(writingKey.serialize())));\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return CompletableFuture.completedFuture(Optional.of(Base64.getDecoder().decode(rs.getString(IPNS_TARGET_NAME))));\n            }\n\n            return CompletableFuture.completedFuture(Optional.empty());\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            return Futures.errored(sqe);\n        }\n    }\n\n    public Map<PublicKeyHash, byte[]> getAllEntries() {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(\"SELECT * FROM metadatablobs\")) {\n            ResultSet rs = stmt.executeQuery();\n            Map<PublicKeyHash, byte[]> results = new HashMap<>();\n            while (rs.next()) {\n                PublicKeyHash writerHash = PublicKeyHash.fromCbor(CborObject.fromByteArray(Base64.getDecoder().decode(rs.getString(\"writingKey\"))));\n                byte[] signedRawCas = Base64.getDecoder().decode(rs.getString(IPNS_TARGET_NAME));\n                results.put(writerHash, signedRawCas);\n            }\n\n            return results;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            return Collections.emptyMap();\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n\n        isClosed = true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/MirrorCoreNode.java",
    "content": "package peergos.server.corenode;\n\nimport peergos.server.*;\nimport peergos.server.login.*;\nimport peergos.server.space.*;\nimport peergos.server.storage.*;\nimport peergos.server.storage.admin.QuotaAdmin;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.resolution.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class MirrorCoreNode implements CoreNode {\n    public static final int MAX_SNAPSHOTS = 10;\n    private static final Logger LOG = Logging.LOG();\n\n    private final CoreNode writeTarget;\n    private final JdbcAccount rawAccount;\n    private final BatCave batCave;\n    private final Account account;\n    private final MutablePointersProxy p2pMutable;\n    private final DeletableContentAddressedStorage ipfs;\n    private final JdbcIpnsAndSocial rawPointers;\n    private final MutablePointers localPointers;\n    private final TransactionStore transactions;\n    private final JdbcIpnsAndSocial localSocial;\n    private final UsageStore usageStore;\n    private final QuotaAdmin quotas;\n    private final LinkRetrievalCounter linkCounts;\n    private final PublicKeyHash pkiOwnerIdentity;\n    private final Cid ourNodeId;\n    private final Multihash pkiPeerId;\n    private final Optional<BatWithId> instanceBat;\n    private final Hasher hasher;\n    private final Crypto crypto;\n    private final ExecutorService mirrorPool = Threads.newPool(1, \"Mirror-\");\n    private final List<String> unlistedUsernames;\n\n    private volatile CorenodeState state;\n    private final Path statePath;\n    private volatile boolean running = true, initialized = false;\n    private final AtomicBoolean updating = new AtomicBoolean(false);\n\n    public MirrorCoreNode(CoreNode writeTarget,\n                          JdbcAccount rawAccount,\n                          BatCave batCave,\n                          Account account,\n                          MutablePointersProxy p2pMutable,\n                          DeletableContentAddressedStorage ipfs,\n                          JdbcIpnsAndSocial rawPointers,\n                          MutablePointers localPointers,\n                          TransactionStore transactions,\n                          JdbcIpnsAndSocial localSocial,\n                          UsageStore usageStore,\n                          QuotaAdmin quotas,\n                          LinkRetrievalCounter linkCounts,\n                          Multihash pkiPeerId,\n                          PublicKeyHash pkiOwnerIdentity,\n                          Path statePath,\n                          Optional<BatWithId> instanceBat,\n                          List<String> unlistedUsernames,\n                          Crypto crypto) {\n        this.writeTarget = writeTarget;\n        this.rawAccount = rawAccount;\n        this.batCave = batCave;\n        this.account = account;\n        this.p2pMutable = p2pMutable;\n        this.ipfs = ipfs;\n        this.rawPointers = rawPointers;\n        this.localPointers = localPointers;\n        this.transactions = transactions;\n        this.localSocial = localSocial;\n        this.usageStore = usageStore;\n        this.quotas = quotas;\n        this.linkCounts = linkCounts;\n        this.pkiPeerId = pkiPeerId;\n        this.pkiOwnerIdentity = pkiOwnerIdentity;\n        this.statePath = statePath;\n        this.ourNodeId = ipfs.id().join();\n        this.instanceBat = instanceBat;\n        this.unlistedUsernames = unlistedUsernames;\n        this.hasher = crypto.hasher;\n        this.crypto = crypto;\n        try {\n            this.state = load(statePath, pkiOwnerIdentity);\n        } catch (IOException e) {\n            Logging.LOG().info(\"No previous pki state file present\");\n            // load empty\n            this.state = CorenodeState.buildEmpty(pkiOwnerIdentity, pkiOwnerIdentity, MaybeMultihash.empty(), MaybeMultihash.empty());\n        }\n        this.state.reverseLookup.put(pkiOwnerIdentity, \"peergos\");\n    }\n\n    @Override\n    public void initialize(boolean mirrorUsers) {\n        try {\n            // first mirror pki blocks locally\n            if (state.usernames.isEmpty()) {\n                long t1 = System.currentTimeMillis();\n                usageStore.addUserIfAbsent(\"peergos\");\n                usageStore.addWriter(\"peergos\", pkiOwnerIdentity);\n                CorenodeRoots remote = getPkiState().left;\n                List<Multihash> pkiStorageProviders = List.of(pkiPeerId);\n                TransactionId tid = transactions.startTransaction(pkiOwnerIdentity);\n                try {\n                    MaybeMultihash currentTree = IpfsCoreNode.getTreeRoot(pkiStorageProviders, pkiOwnerIdentity, remote.pkiKeyTarget, ourNodeId, hasher, ipfs);\n                    usageStore.addWriter(\"peergos\", remote.pkiKey);\n                    ipfs.mirror(\"peergos\", pkiOwnerIdentity, remote.pkiKey, pkiStorageProviders, Optional.empty(), currentTree.toOptional().map(m -> (Cid)m),\n                            Optional.empty(), ipfs.id().join(), (x, y, z) -> {}, tid, hasher).join();\n                } finally {\n                    transactions.closeTransaction(pkiOwnerIdentity, tid);\n                }\n                long t2 = System.currentTimeMillis();\n                System.out.println(\"PKI MIRROR TOOK \" + (t2-t1)/1000);\n            }\n            boolean changed = update();\n            if (changed)\n                saveState();\n            initialized = true;\n            if (mirrorUsers)\n                mirrorPool.submit(() -> mirrorExternalUsers());\n        } catch (Throwable t) {\n            Logging.LOG().log(Level.SEVERE, \"Couldn't update mirror pki state: \" + t.getMessage(), t);\n        }\n    }\n\n    private void mirrorExternalUsers() {\n        while (true) {\n            try {\n                List<String> localQuotaUsernames = quotas.getLocalUsernames();\n                List<Multihash> ourPeerIds = ipfs.ids().join()\n                        .stream()\n                        .map(Cid::bareMultihash)\n                        .toList();\n                List<String> externalUsersToMirror = localQuotaUsernames.stream()\n                        .filter(n -> {\n                            if (! state.chains.containsKey(n))\n                                return false;\n                            List<UserPublicKeyLink> chain = state.chains.get(n);\n                            if (chain.isEmpty())\n                                return false;\n                            List<Multihash> homes = chain.get(chain.size() - 1).claim.storageProviders;\n                            if (homes.isEmpty())\n                                return false;\n                            return !ourPeerIds.contains(homes.get(0).bareMultihash());\n                        }).toList();\n                for (String username : externalUsersToMirror) {\n                    long quota = quotas.getQuota(username);\n                    if (quota <= 1024*1024)\n                        continue;\n                    List<BatWithId> localMirrorBats = batCave.getUserBats(username, new byte[0]).join();\n                    if (localMirrorBats.isEmpty())\n                        continue;\n                    LOG.info(\"Mirroring \" + username + \" data locally..\");\n                    try {\n                        long t0 = System.currentTimeMillis();\n                        PublicKeyHash owner = getPublicKeyHash(username).join().get();\n                        Map<PublicKeyHash, byte[]> pointers = Mirror.mirrorUser(username, Optional.empty(),\n                                Optional.of(localMirrorBats.get(localMirrorBats.size() - 1)), this, p2pMutable,\n                                null, ipfs, rawPointers, rawAccount, transactions, linkCounts, usageStore, hasher);\n                        SpaceCheckingKeyFilter.processCorenodeEvent(username, owner, pointers.keySet(), usageStore, quotas, ipfs, p2pMutable, hasher);\n                        long t1 = System.currentTimeMillis();\n                        LOG.info(\"Finished mirroring \" + username + \" data in \" + (t1 - t0) / 1_000 + \"s\");\n                    } catch (Exception e) {\n                        LOG.log(Level.WARNING, \"Error mirroring user \" + username, e);\n                    }\n                }\n            } catch (Exception e) {\n                LOG.log(Level.WARNING, \"Error mirroring users\", e);\n            } finally {\n                Threads.sleep(60_000);\n            }\n        }\n    }\n\n    private static class CorenodeRoots {\n        final PublicKeyHash pkiOwnerIdentity, pkiKey;\n        final MaybeMultihash pkiOwnerTarget, pkiKeyTarget;\n\n        public CorenodeRoots(PublicKeyHash pkiOwnerIdentity, PublicKeyHash pkiKey, MaybeMultihash pkiOwnerTarget, MaybeMultihash pkiKeyTarget) {\n            this.pkiOwnerIdentity = pkiOwnerIdentity;\n            this.pkiKey = pkiKey;\n            this.pkiOwnerTarget = pkiOwnerTarget;\n            this.pkiKeyTarget = pkiKeyTarget;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            CorenodeRoots that = (CorenodeRoots) o;\n            return pkiOwnerIdentity.equals(that.pkiOwnerIdentity) && pkiKey.equals(that.pkiKey) && pkiOwnerTarget.equals(that.pkiOwnerTarget) && pkiKeyTarget.equals(that.pkiKeyTarget);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(pkiOwnerIdentity, pkiKey, pkiOwnerTarget, pkiKeyTarget);\n        }\n    }\n\n    private static class CorenodeState implements Cborable {\n        private final CorenodeRoots roots;\n\n        private final Map<String, List<UserPublicKeyLink>> chains;\n        private final Map<PublicKeyHash, String> reverseLookup;\n        private final List<String> usernames;\n\n        public CorenodeState(CorenodeRoots roots,\n                             Map<String, List<UserPublicKeyLink>> chains,\n                             Map<PublicKeyHash, String> reverseLookup,\n                             List<String> usernames) {\n            this.roots = roots;\n            this.chains = chains;\n            this.reverseLookup = reverseLookup;\n            this.usernames = usernames;\n        }\n\n        public static CorenodeState buildEmpty(PublicKeyHash pkiOwnerIdentity,\n                                               PublicKeyHash pkiKey,\n                                               MaybeMultihash pkiOwnerTarget,\n                                               MaybeMultihash pkiKeyTarget) {\n            return new CorenodeState(new CorenodeRoots(pkiOwnerIdentity, pkiKey, pkiOwnerTarget, pkiKeyTarget), new HashMap<>(),\n                    new HashMap<>(), new ArrayList<>());\n        }\n\n        public void load(CorenodeState other) {\n            chains.putAll(other.chains);\n            reverseLookup.putAll(other.reverseLookup);\n            usernames.clear();\n            usernames.addAll(other.usernames);\n        }\n\n        @Override\n        public CborObject toCbor() {\n            Map<String, Cborable> res = new TreeMap<>();\n            res.put(\"peergosKey\", roots.pkiOwnerIdentity);\n            res.put(\"peergosTarget\", roots.pkiOwnerTarget);\n            res.put(\"pkiKey\", roots.pkiOwnerIdentity);\n            res.put(\"pkiTarget\", roots.pkiKeyTarget);\n\n            TreeMap<String, Cborable> chainsMap = chains.entrySet()\n                .stream()\n                .collect(Collectors.toMap(\n                    e -> e.getKey(),\n                    e -> new CborObject.CborList(e.getValue()),\n                    (a,b) -> a,\n                    TreeMap::new\n                ));\n            res.put(\"chains\", CborObject.CborMap.build(chainsMap));\n            TreeMap<CborObject, Cborable> reverseMap = reverseLookup.entrySet()\n                .stream()\n                .collect(Collectors.toMap(\n                    e -> e.getKey().toCbor(),\n                    e -> new CborObject.CborString(e.getValue()),\n                    (a,b) -> a,\n                    TreeMap::new\n                ));\n            res.put(\"reverse\", new CborObject.CborList(reverseMap));\n            res.put(\"usernames\", new CborObject.CborList(usernames.stream()\n                    .map(CborObject.CborString::new)\n                    .collect(Collectors.toList())));\n\n            return CborObject.CborMap.build(res);\n        }\n\n        public static CorenodeState fromCbor(CborObject cbor) {\n            CborObject.CborMap map = (CborObject.CborMap) cbor;\n            PublicKeyHash peergosKey = map.get(\"peergosKey\", PublicKeyHash::fromCbor);\n            PublicKeyHash pkiKey = map.get(\"pkiKey\", PublicKeyHash::fromCbor);\n            MaybeMultihash peergosTarget = map.get(\"peergosTarget\", MaybeMultihash::fromCbor);\n            MaybeMultihash pkiTarget = map.get(\"pkiTarget\", MaybeMultihash::fromCbor);\n\n            Function<Cborable, String> fromString = e -> ((CborObject.CborString) e).value;\n            Function<? super Cborable, List<UserPublicKeyLink>> chainParser =\n                    c -> ((CborObject.CborList) c).map(UserPublicKeyLink::fromCbor);\n            Map<String, List<UserPublicKeyLink>> chains = ((CborObject.CborMap)map.get(\"chains\"))\n                    .toMap(fromString, chainParser);\n\n            Map<PublicKeyHash, String> reverse = ((CborObject.CborList)map.get(\"reverse\"))\n                    .getMap(PublicKeyHash::fromCbor, fromString);\n\n            List<String> usernames = map.getList(\"usernames\", fromString);\n            return new CorenodeState(new CorenodeRoots(peergosKey, pkiKey, peergosTarget, pkiTarget), chains, reverse, usernames);\n        }\n    }\n\n    public void start() {\n        running = true;\n        new Thread(() -> {\n            while (running) {\n                try {\n                    Thread.sleep(60_000);\n                    boolean changed = update();\n                    if (changed)\n                        saveState();\n                } catch (Throwable t) {\n                    Logging.LOG().log(Level.SEVERE, t.getMessage(), t);\n                }\n            }\n        }, \"Mirroring PKI node\").start();\n    }\n\n    private synchronized void saveState() {\n        byte[] serialized = state.toCbor().serialize();\n        Logging.LOG().info(\"Writing \"+ serialized.length +\" bytes to \"+ statePath);\n        try {\n            Files.write(\n                    statePath,\n                    serialized,\n                    StandardOpenOption.CREATE);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static CorenodeState load(Path statePath, PublicKeyHash pkiNodeIdentity) throws IOException {\n        boolean exists = Files.exists(statePath);\n        Logging.LOG().info(\"Reading state from \" + statePath + \" which exists ? \" + exists);\n        byte[] data = Files.readAllBytes(statePath);\n        CborObject object = CborObject.fromByteArray(data);\n        return CorenodeState.fromCbor(object);\n    }\n\n    private Pair<CorenodeRoots, byte[]> getPkiState() {\n        byte[] pkiIdPointer = p2pMutable.getPointer(pkiPeerId, pkiOwnerIdentity, pkiOwnerIdentity)\n                .orTimeout(30, TimeUnit.SECONDS).join().get();\n        PointerUpdate fresh = MutablePointers.parsePointerTarget(pkiIdPointer, pkiOwnerIdentity, pkiOwnerIdentity, ipfs).join();\n        MaybeMultihash newPeergosRoot = fresh.updated;\n\n        CommittedWriterData currentPeergosWd = IpfsCoreNode.getWriterData(List.of(pkiPeerId), pkiOwnerIdentity,\n                (Cid)newPeergosRoot.get(), fresh.sequence, ourNodeId, hasher, ipfs)\n                .orTimeout(30, TimeUnit.SECONDS).join();\n        PublicKeyHash pkiKey = currentPeergosWd.props.get().namedOwnedKeys.get(\"pki\").ownedKey;\n        if (pkiKey == null)\n            throw new IllegalStateException(\"No pki key on owner: \" + pkiOwnerIdentity);\n\n        byte[] newPointer = p2pMutable.getPointer(pkiPeerId, pkiOwnerIdentity, pkiKey)\n                .orTimeout(30, TimeUnit.SECONDS).join().get();\n        PointerUpdate pkiUpdateCas = MutablePointers.parsePointerTarget(newPointer, pkiOwnerIdentity, pkiKey, ipfs).join();\n        MaybeMultihash currentPkiRoot = pkiUpdateCas.updated;\n        return new Pair<>(new CorenodeRoots(pkiOwnerIdentity, pkiKey, newPeergosRoot, currentPkiRoot), newPointer);\n    }\n\n    /**\n     *\n     * @return whether there was a change\n     */\n    public boolean update() {\n        if (!updating.compareAndSet(false, true))\n            return false;\n        try {\n            Pair<CorenodeRoots, byte[]> remoteState = getPkiState();\n            CorenodeRoots remote = remoteState.left;\n            CorenodeState current = state;\n            if (remote.pkiOwnerIdentity.equals(current.roots.pkiOwnerIdentity) &&\n                    remote.pkiOwnerTarget.equals(current.roots.pkiOwnerTarget) &&\n                    remote.pkiKey.equals(current.roots.pkiKey) &&\n                    remote.pkiKeyTarget.equals(current.roots.pkiKeyTarget)) {\n                return false;\n            }\n\n            Logging.LOG().info(\"Updating pki mirror state... Please wait. This could take a minute or two\");\n            CorenodeState updated = CorenodeState.buildEmpty(pkiOwnerIdentity, remote.pkiKey, remote.pkiOwnerTarget, remote.pkiKeyTarget);\n            updated.load(current);\n\n            // first retrieve all new blocks to be local\n            TransactionId tid = transactions.startTransaction(pkiOwnerIdentity);\n            List<Multihash> pkiStorageProviders = List.of(pkiPeerId);\n            MaybeMultihash currentTree = IpfsCoreNode.getTreeRoot(pkiStorageProviders, pkiOwnerIdentity,\n                    current.roots.pkiKeyTarget, ourNodeId, hasher, ipfs);\n            MaybeMultihash updatedTree = IpfsCoreNode.getTreeRoot(pkiStorageProviders, pkiOwnerIdentity,\n                    remote.pkiKeyTarget, ourNodeId, hasher, ipfs);\n\n            // explicitly get other direct blocks, in theory need recursive mirror, but this is complete here\n            if (updatedTree.isPresent()) {\n                CommittedWriterData currentWd = IpfsCoreNode.getWriterData(pkiStorageProviders,\n                        pkiOwnerIdentity, (Cid) remote.pkiKeyTarget.get(), Optional.empty(), ourNodeId, hasher, ipfs)\n                        .orTimeout(30, TimeUnit.SECONDS).join();\n                List<Multihash> toAdd = currentWd.props.get().toCbor().links().stream()\n                        .filter(h -> updatedTree.map(m -> !m.equals(h)).orElse(true))\n                        .collect(Collectors.toList());\n                for (Multihash m : toAdd) {\n                    ipfs.get(pkiStorageProviders, pkiOwnerIdentity, (Cid) m, Optional.empty(), ourNodeId, hasher, true)\n                            .orTimeout(30, TimeUnit.SECONDS).join();\n                }\n            }\n\n            Consumer<Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> consumer =\n                    t -> {\n                        Optional<CborObject.CborMerkleLink> newVal = t.right;\n                        if (newVal.isPresent()) {\n                            transactions.addBlock(newVal.get().target, tid, pkiOwnerIdentity);\n                            ipfs.get(pkiStorageProviders, pkiOwnerIdentity, (Cid) newVal.get().target, Optional.empty(), ourNodeId, hasher, true)\n                                    .orTimeout(30, TimeUnit.SECONDS).join();\n                        }\n                    };\n            IpfsCoreNode.applyToDiff(pkiStorageProviders, ourNodeId, pkiOwnerIdentity, currentTree, updatedTree, 0, IpfsCoreNode::keyHash,\n                    Collections.emptyList(), Collections.emptyList(),\n                    consumer, ChampWrapper.BIT_WIDTH, ipfs, hasher, c -> (CborObject.CborMerkleLink)c)\n                    .orTimeout(2, TimeUnit.MINUTES).join();\n\n            // now update the mappings\n            IpfsCoreNode.updateAllMappings(pkiStorageProviders, pkiOwnerIdentity, current.roots.pkiKeyTarget,\n                    remote.pkiKeyTarget, ourNodeId, hasher, ipfs, updated.chains, updated.reverseLookup, updated.usernames);\n            updated.usernames.removeAll(unlistedUsernames);\n\n            // 'pin' the new pki version\n            Optional<byte[]> existingPointer = rawPointers.getPointer(remote.pkiKey)\n                    .orTimeout(30, TimeUnit.SECONDS).join();\n            rawPointers.setPointer(remote.pkiKey, existingPointer, remoteState.right)\n                    .orTimeout(30, TimeUnit.SECONDS).join();\n            transactions.closeTransaction(pkiOwnerIdentity, tid);\n\n            state = updated;\n            Logging.LOG().info(\"... finished updating pki mirror state.\");\n            return true;\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        } finally {\n            updating.set(false);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink chain,\n                                                                  OpLog setupOperations,\n                                                                  ProofOfWork proof,\n                                                                  String token) {\n        if (! chain.claim.storageProviders.get(0).bareMultihash().equals(ourNodeId.bareMultihash()))\n            throw new IllegalStateException(\"Trying to signup to wrong host! \" + chain.claim.storageProviders.get(0));\n        Optional<RequiredDifficulty> pkiResult = writeTarget.updateChain(username, Arrays.asList(chain), proof, token).join();\n        if (pkiResult.isPresent()) { // signup rejected\n            LOG.info(\"Rejecting signup: required \" + pkiResult.get());\n            AggregatedMetrics.PKI_RATE_LIMITED.inc();\n            return Futures.of(pkiResult);\n        }\n\n        usageStore.addUserIfAbsent(username);\n        usageStore.addWriter(username, chain.owner);\n        state.usernames.add(username);\n        state.reverseLookup.put(chain.owner, username);\n        state.chains.put(username, List.of(chain));\n        IpfsCoreNode.applyOpLog(username, chain.owner, setupOperations, ipfs, localPointers, account, batCave);\n        return Futures.of(Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username,\n                                                                                            UserPublicKeyLink chain,\n                                                                                            ProofOfWork proof) {\n        if (! chain.claim.storageProviders.get(0).bareMultihash().equals(ourNodeId.bareMultihash()))\n            throw new IllegalStateException(\"Trying to signup to wrong host! \" + chain.claim.storageProviders.get(0));\n        return writeTarget.startPaidSignup(username, chain, proof);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username,\n                                                                   UserPublicKeyLink chain,\n                                                                   OpLog setupOperations,\n                                                                   byte[] signedSpaceRequest,\n                                                                   ProofOfWork proof) {\n        usageStore.addUserIfAbsent(username);\n        usageStore.addWriter(username, chain.owner);\n        long t0 = System.currentTimeMillis();\n        IpfsCoreNode.applyOpLog(username, chain.owner, setupOperations, ipfs, localPointers, account, batCave);\n        long t1 = System.currentTimeMillis();\n        writeTarget.completePaidSignup(username, chain, OpLog.empty(), new byte[0], proof).join();\n        long t2 = System.currentTimeMillis();\n        LOG.info(\"Complete Paid signup timing - oplog: \" + (t1-t0) + \"ms, pki confirmation: \" + (t2-t1) + \"ms\");\n        state.usernames.add(username);\n        state.reverseLookup.put(chain.owner, username);\n        state.chains.put(username, List.of(chain));\n        new Thread(() -> {\n            try {\n                update();\n            } catch (Throwable t) {\n                LOG.log(Level.WARNING, \"Failed to update local PKI during completePaidSignup\", t);\n            }\n        }).start();\n        return Futures.of(new PaymentProperties(0));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        // verify auth\n        PublicKeyHash identityHash = getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowedTime(auth, 10*60, ipfs, identityHash);\n\n        // double check mirror bat and add to db\n        if (! BatId.sha256(mirrorBat.bat, hasher).join().equals(mirrorBat.id()))\n            throw new IllegalStateException(\"Invalid BAT id for BAT\");\n        List<BatWithId> localMirrorBats = batCave.getUserBats(username, new byte[0]).join();\n        if (! localMirrorBats.contains(mirrorBat))\n            batCave.addBat(username, mirrorBat.id(), mirrorBat.bat, new byte[0]);\n\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        // verify auth\n        PublicKeyHash identityHash = getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowedTime(auth, 10*60, ipfs, identityHash);\n        return Futures.of(Either.a(new PaymentProperties(0)));\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        // double check mirror bat and add to db\n        if (! BatId.sha256(mirrorBat.bat, hasher).join().equals(mirrorBat.id()))\n            throw new IllegalStateException(\"Invalid BAT id for BAT\");\n        List<BatWithId> localMirrorBats = batCave.getUserBats(username, new byte[0]).join();\n        if (! localMirrorBats.contains(mirrorBat))\n            batCave.addBat(username, mirrorBat.id(), mirrorBat.bat, new byte[0]);\n        return Futures.of(new PaymentProperties(0));\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        List<UserPublicKeyLink> chain = state.chains.get(username);\n        if (chain != null)\n            return CompletableFuture.completedFuture(chain);\n        if (! initialized)\n            return writeTarget.getChain(username);\n\n        return Futures.of(Collections.emptyList());\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                       List<UserPublicKeyLink> chain,\n                                                                       ProofOfWork proof,\n                                                                       String token) {\n        return writeTarget.updateChain(username, chain, proof, token)\n                .thenApply(x -> {\n                    if (x.isEmpty())\n                        this.update();\n                    return x;\n                });\n    }\n\n    private UserSnapshot update(UserSnapshot in) {\n        return new UserSnapshot(in.username,\n                in.owner,\n                in.pointerState.entrySet().stream()\n                        .flatMap(e -> rawPointers.getPointer(e.getKey()).join()\n                                .map(v -> new Pair<>(e.getKey(), v))\n                                .stream())\n                        .collect(Collectors.toMap(p -> p.left, p -> p.right)),\n                in.pendingFollowReqs,\n                in.mirrorBats,\n                in.login,\n                in.linkCounts);\n    }\n\n    public static CompletableFuture<Map<PublicKeyHash, byte[]>> getUserSnapshotRecursive(List<Multihash> peerIds,\n                                                                                         PublicKeyHash owner,\n                                                                                         PublicKeyHash writer,\n                                                                                         Map<PublicKeyHash, byte[]> alreadyDone,\n                                                                                         MutablePointers mutable,\n                                                                                         Cid ourId,\n                                                                                         DeletableContentAddressedStorage ipfs,\n                                                                                         Hasher hasher) {\n        return DeletableContentAddressedStorage.getDirectOwnedKeys(owner, writer, mutable,\n                        (h, s) -> DeletableContentAddressedStorage.getWriterData(peerIds, owner, h, s, false, ourId, hasher, ipfs), ipfs, hasher)\n                .thenCompose(directOwned -> {\n                    Set<PublicKeyHash> newKeys = directOwned.stream().\n                            filter(h -> ! alreadyDone.containsKey(h))\n                            .collect(Collectors.toSet());\n                    Map<PublicKeyHash, byte[]> done = new HashMap<>(alreadyDone);\n                    return mutable.getPointer(owner, writer).thenCompose(val -> {\n                        if (val.isPresent())\n                            done.put(writer, val.get());\n                        BiFunction<Map<PublicKeyHash, byte[]>, PublicKeyHash,\n                                CompletableFuture<Map<PublicKeyHash, byte[]>>> composer =\n                                (a, w) -> getUserSnapshotRecursive(peerIds, owner, w, a, mutable, ourId, ipfs, hasher)\n                                        .thenApply(ws ->\n                                                Stream.concat(\n                                                                ws.entrySet().stream().filter(e -> ! a.containsKey(e.getKey())),\n                                                                a.entrySet().stream())\n                                                        .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())));\n                        return Futures.reduceAll(newKeys, done,\n                                composer,\n                                (a, b) -> Stream.concat(\n                                                a.entrySet().stream().filter(e -> ! b.containsKey(e.getKey())),\n                                                b.entrySet().stream())\n                                        .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())));\n                    });\n                });\n    }\n\n    public static CompletableFuture<Map<PublicKeyHash, byte[]>> getUserSnapshot(PublicKeyHash owner,\n                                                                                List<Multihash> peerIds,\n                                                                                MutablePointers mutable,\n                                                                                Cid ourId,\n                                                                                DeletableContentAddressedStorage dht,\n                                                                                Hasher hasher) {\n        return getUserSnapshotRecursive(peerIds, owner, owner, Collections.emptyMap(), mutable, ourId, dht, hasher);\n    }\n\n    private boolean isHome(String username, Set<Multihash> ourIds) {\n        List<UserPublicKeyLink> chain = state.chains.get(username);\n        return chain != null &&\n                chain.get(chain.size() - 1).claim.storageProviders.stream()\n                        .map(Multihash::bareMultihash)\n                        .anyMatch(ourIds::contains);\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId suppliedBat, LocalDateTime latestLinkCountUpdate) {\n        if (instanceBat.isEmpty())\n            throw new IllegalStateException(\"This server does not have an instance bat.\");\n        // defend against timing attacks checking bat\n        byte[] salt = crypto.random.randomBytes(8);\n        byte[] supplied = hasher.sha256(ArrayOps.concat(suppliedBat.serialize(), salt)).join();\n        byte[] expected = hasher.sha256(ArrayOps.concat(instanceBat.get().serialize(), salt)).join();\n        if (! Arrays.equals(supplied, expected))\n            throw new IllegalStateException(\"Unauthorized!\");\n        List<String> localUsernames = quotas.getLocalUsernames().stream().sorted().toList();\n        LOG.info(\"GetSnapshots(\"+prefix+\") got \" + localUsernames.size() + \" local usernames.\");\n        Set<Multihash> ourIds = ipfs.ids().join().stream().map(Cid::bareMultihash).collect(Collectors.toSet());\n        return Futures.of(localUsernames.stream()\n                .filter(n -> n.compareTo(prefix) > 0 && isHome(n, ourIds))\n                .limit(MAX_SNAPSHOTS)\n                .parallel()\n                .map(n -> {\n                    List<UserPublicKeyLink> chain = state.chains.get(n);\n                    PublicKeyHash owner = chain.get(chain.size() - 1).owner;\n                    return getUserSnapshot(owner, List.of(ourNodeId), localPointers, ourNodeId, ipfs, hasher)\n                    .thenApply(pointers -> new UserSnapshot(n, owner, pointers,\n                            localSocial.getAndParseFollowRequests(owner),\n                            batCave.getUserBats(n, new byte[0]).join(),\n                            rawAccount.getLoginData(n), linkCounts.getUpdatedCounts(n, latestLinkCountUpdate)));\n                })\n                .map(CompletableFuture::join)\n                .toList());\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime latestLinkCountUpdate,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        // check chain validity before proceeding further\n        List<UserPublicKeyLink> existingChain = getChain(username).join();\n        UserPublicKeyLink currentLast = existingChain.get(existingChain.size() - 1);\n        UserPublicKeyLink newLast = newChain.get(newChain.size() - 1);\n        if (currentLast.claim.expiry.isAfter(newLast.claim.expiry))\n            throw new IllegalStateException(\"Migration claim has earlier expiry than current one!\");\n        UserPublicKeyLink.merge(existingChain, newChain, ipfs).join();\n        Multihash migrationTargetNode = newLast.claim.storageProviders.get(0);\n        PublicKeyHash owner = newLast.owner;\n\n        if (currentStorageId.equals(ourNodeId)) {\n            // a user is migrating away from this server\n            ProofOfWork work = ProofOfWork.empty();\n            LinkCounts updated = linkCounts.getUpdatedCounts(username, latestLinkCountUpdate);\n            UserSnapshot snapshot = getUserSnapshot(owner, currentLast.claim.storageProviders, p2pMutable, ourNodeId, ipfs, hasher)\n                    .thenApply(pointers -> new UserSnapshot(username,\n                            currentLast.owner,\n                            pointers,\n                            localSocial.getAndParseFollowRequests(owner),\n                            batCave.getUserBats(username, new byte[0]).join(),\n                            rawAccount.getLoginData(username), updated)).join();\n            if (commitToPki)\n                updateChain(username, newChain, work, \"\").join();\n            // from this point on new writes are proxied to the new storage server if we committed to the PKI\n            return Futures.of(update(snapshot));\n        }\n\n        if (migrationTargetNode.equals(ourNodeId)) {\n            long quota = quotas.getQuota(username);\n            if (quota < currentUsage)\n                throw new IllegalStateException(\"Not enough space quota on this server to migrate here!\");\n            // We are copying data to this node\n            // Make sure we have the mirror bat stored in out batStore first\n            if (mirrorBat.isPresent()) {\n                BatWithId bat = mirrorBat.get();\n                // double check it\n                if (! BatId.sha256(bat.bat, hasher).join().equals(bat.id()))\n                    throw new IllegalStateException(\"Invalid BAT id for BAT\");\n                List<BatWithId> localMirrorBats = batCave.getUserBats(username, new byte[0]).join();\n                if (! localMirrorBats.contains(bat))\n                    batCave.addBat(username, bat.id(), bat.bat, new byte[0]);\n            }\n            List<Multihash> storageProviders = getStorageProviders(owner);\n            // Mirror all the data locally\n            Mirror.mirrorUser(username, Optional.empty(), mirrorBat, this, p2pMutable, null,\n                    ipfs, rawPointers, rawAccount, transactions, linkCounts, usageStore, hasher);\n            Map<PublicKeyHash, byte[]> mirrored = Mirror.mirrorUser(username, Optional.empty(), mirrorBat,\n                    this, p2pMutable, null, ipfs, rawPointers, rawAccount, transactions, linkCounts,\n                    usageStore, hasher);\n\n            // Proxy call to their current storage server\n            LocalDateTime localLatestLinkCountTime = linkCounts.getLatestModificationTime(username).orElse(LocalDateTime.MIN);\n            UserSnapshot res = writeTarget.migrateUser(username, newChain, currentStorageId, mirrorBat,\n                    localLatestLinkCountTime, currentUsage, false).join();\n            // make sure login data, link counts, pending follow reqs are present locally before updating PKI via remote server\n            commitUpdate(res, username, storageProviders, owner, mirrorBat, mirrored);\n            mirrored = res.pointerState;\n\n            // now tell remote node to commit to the PKI\n            res = writeTarget.migrateUser(username, newChain, currentStorageId, mirrorBat,\n                    localLatestLinkCountTime, currentUsage, true).join();\n\n            // pick up the new pki data locally\n            update();\n\n            // commit any new diff\n            commitUpdate(res, username, storageProviders, owner, mirrorBat, mirrored);\n\n            // Make sure usage is updated\n            List<Multihash> us = List.of(ourNodeId.bareMultihash());\n            Set<PublicKeyHash> allUserKeys = DeletableContentAddressedStorage.getOwnedKeysRecursive(owner, owner, p2pMutable,\n                    (h, s) -> DeletableContentAddressedStorage.getWriterData(us, owner, h, s, true, ourNodeId, hasher, ipfs), ipfs, hasher).join();\n            SpaceCheckingKeyFilter.processCorenodeEvent(username, owner, allUserKeys, usageStore, quotas, ipfs, p2pMutable, hasher);\n            return Futures.of(res);\n        } else // Proxy call to their target storage server\n            return writeTarget.migrateUser(username, newChain, migrationTargetNode, mirrorBat, latestLinkCountUpdate, currentUsage, commitToPki);\n    }\n\n    private void commitUpdate(UserSnapshot res,\n                              String username,\n                              List<Multihash> storageProviders,\n                              PublicKeyHash owner,\n                              Optional<BatWithId> mirrorBat,\n                              Map<PublicKeyHash, byte[]> mirrored) {\n        List<BatWithId> localMirrorBats = batCave.getUserBats(username, new byte[0]).join();\n        res.mirrorBats.forEach(b -> {\n            if (! localMirrorBats.contains(b))\n                batCave.addBat(username, b.id(), b.bat, new byte[0]);\n        });\n        res.login.ifPresent(rawAccount::setLoginData);\n\n        // commit diff since our mirror above\n        for (Map.Entry<PublicKeyHash, byte[]> e : res.pointerState.entrySet()) {\n            byte[] existingVal = mirrored.get(e.getKey());\n            if (! Arrays.equals(existingVal, e.getValue())) {\n                Mirror.mirrorMerkleTree(username, owner, e.getKey(), storageProviders, e.getValue(), mirrorBat,\n                        ipfs, rawPointers, transactions, usageStore, hasher);\n            }\n        }\n\n        // Copy pending follow requests to local server\n        List<byte[]> existing = localSocial.getRawFollowRequests(owner);\n        for (BlindFollowRequest req : res.pendingFollowReqs) {\n            // write directly to local social database to avoid being redirected to user's current node\n            if (existing.stream().noneMatch(b -> Arrays.equals(b, req.serialize())))\n                localSocial.addFollowRequest(owner, req.serialize()).join();\n        }\n\n        // update local link counts\n        linkCounts.setCounts(username, res.linkCounts);\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash key) {\n        String username = state.reverseLookup.get(key);\n        if (username != null)\n            return CompletableFuture.completedFuture(username);\n        if (! initialized)\n            return writeTarget.getUsername(key);\n        return Futures.errored(new IllegalStateException(\"Unknown identity key: \" + key));\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        if (! state.usernames.isEmpty())\n            return CompletableFuture.completedFuture(state.usernames);\n        if (! initialized)\n            return writeTarget.getUsernames(prefix);\n        return CompletableFuture.completedFuture(state.usernames);\n    }\n\n    @Override\n    public List<Multihash> getStorageProviders(PublicKeyHash owner) {\n        String username = getUsername(owner).join();\n        List<UserPublicKeyLink> chain = getChain(username).join();\n        if (chain.isEmpty())\n            return Collections.emptyList();\n        List<Multihash> fromPki = chain.get(chain.size() - 1).claim.storageProviders;\n        List<Multihash> withIdRotations = fromPki.stream()\n                .map(h -> rotatedServers.getOrDefault(h.bareMultihash(), h))\n                .collect(Collectors.toList());\n        return withIdRotations;\n    }\n\n    private final LRUCache<Multihash, Multihash> rotatedServers = new LRUCache<>(100);\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return ipfs.getIpnsEntry(serverId)\n                .thenApply(e -> {\n                    ResolutionRecord value = e.getValue(serverId, crypto).join();\n                    if (value.moved)\n                        value.host.ifPresent(newHost -> rotatedServers.put(serverId, new Cid(1, Cid.Codec.LibP2pKey, newHost.type, newHost.getHash())));\n                    return value.host;\n                });\n    }\n\n    @Override\n    public void close() {\n        running = false;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/NonWriteThroughCoreNode.java",
    "content": "package peergos.server.corenode;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class NonWriteThroughCoreNode implements CoreNode {\n\n    private final CoreNode source;\n    private final ContentAddressedStorage ipfs;\n    private final Map<String, List<UserPublicKeyLink>> tempChains;\n    private final Map<PublicKeyHash, String> tempOwnerToUsername;\n\n    public NonWriteThroughCoreNode(CoreNode source, ContentAddressedStorage ipfs) {\n        this.source = source;\n        this.ipfs = ipfs;\n        this.tempChains = new ConcurrentHashMap<>();\n        this.tempOwnerToUsername = new ConcurrentHashMap<>();\n    }\n\n    @Override\n    public CompletableFuture<Optional<PublicKeyHash>> getPublicKeyHash(String username) {\n        try {\n            List<UserPublicKeyLink> chain = tempChains.get(username);\n            if (chain != null) {\n                PublicKeyHash modified = chain.get(chain.size() - 1).owner;\n                return CompletableFuture.completedFuture(Optional.of(modified));\n            }\n            return source.getPublicKeyHash(username);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash key) {\n        try {\n            String modified = tempOwnerToUsername.get(key);\n            if (modified != null)\n                return CompletableFuture.completedFuture(modified);\n            return source.getUsername(key);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        try {\n            List<UserPublicKeyLink> modified = tempChains.get(username);\n            if (modified != null)\n                return CompletableFuture.completedFuture(modified);\n            return source.getChain(username);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink chain,\n                                                                  OpLog setupOperations,\n                                                                  ProofOfWork proof,\n                                                                  String token) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username, UserPublicKeyLink chain, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username, UserPublicKeyLink chain, OpLog setupOperations, byte[] signedSpaceRequest, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                       List<UserPublicKeyLink> updated,\n                                                                       ProofOfWork proof,\n                                                                       String token) {\n        try {\n            List<UserPublicKeyLink> modified = tempChains.get(username);\n            if (modified != null)\n                modified = source.getChain(username).get();\n            List<UserPublicKeyLink> mergedChain = UserPublicKeyLink.merge(modified, updated, ipfs).get();\n            tempChains.put(username, mergedChain);\n            UserPublicKeyLink last = mergedChain.get(mergedChain.size() - 1);\n            tempOwnerToUsername.put(last.owner, username);\n            return CompletableFuture.completedFuture(Optional.empty());\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        try {\n            return CompletableFuture.completedFuture(\n                    Stream.concat(\n                            source.getUsernames(prefix).get().stream(),\n                            tempChains.keySet().stream().filter(u -> u.startsWith(prefix)))\n                            .distinct()\n                            .collect(Collectors.toList()));\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime latestLinkCountUpdate,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        throw new IllegalStateException(\"Unimplemented method!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return source.getNextServerId(serverId);\n    }\n\n    @Override\n    public void close() throws IOException {}\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/SignUpFilter.java",
    "content": "package peergos.server.corenode;\n\nimport peergos.server.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.logging.*;\n\npublic class SignUpFilter implements CoreNode {\n    private static final Logger LOG = Logging.LOG();\n\n    public final CoreNode target;\n    private final QuotaAdmin quotaStore;\n    private final Multihash ourNodeId;\n    private final HttpSpaceUsage space;\n    private final Hasher hasher;\n    private final DifficultyGenerator startPaidRateLimiter;\n    private final DifficultyGenerator startPaidMirrorRateLimiter;\n    private final int maxPaidSignupsPerDay;\n    private final boolean isPki;\n\n    public SignUpFilter(CoreNode target,\n                        QuotaAdmin quotaStore,\n                        Multihash ourNodeId,\n                        HttpSpaceUsage space,\n                        Hasher hasher,\n                        int maxPaidSignupsPerDay,\n                        boolean isPki) {\n        this.target = target;\n        this.quotaStore = quotaStore;\n        this.ourNodeId = ourNodeId;\n        this.space = space;\n        this.hasher = hasher;\n        this.maxPaidSignupsPerDay = maxPaidSignupsPerDay;\n        startPaidRateLimiter = new DifficultyGenerator(System.currentTimeMillis(), maxPaidSignupsPerDay);\n        startPaidMirrorRateLimiter = new DifficultyGenerator(System.currentTimeMillis(), maxPaidSignupsPerDay);\n        this.isPki = isPki;\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink chain,\n                                                                  OpLog setupOperations,\n                                                                  ProofOfWork proof,\n                                                                  String token) {\n        if (! forUs(Arrays.asList(chain)))\n            return target.signup(username, chain, setupOperations, proof, token);\n\n        if (quotaStore.allowSignupOrUpdate(username, token)) {\n            return target.signup(username, chain, setupOperations, proof, token).thenApply(res -> {\n                if (res.isEmpty())\n                    quotaStore.consumeToken(username, token);\n                return res;\n            });\n        }\n        if (! token.isEmpty())\n            return Futures.errored(new IllegalStateException(\"Invalid signup token.\"));\n\n        return Futures.errored(new IllegalStateException(\"This server is not currently accepting new sign ups. Please try again later\"));\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username,\n                                                                                            UserPublicKeyLink chain,\n                                                                                            ProofOfWork proof) {\n        if (isPki)\n            return target.startPaidSignup(username, chain, proof);\n        if (maxPaidSignupsPerDay == 0)\n            return Futures.of(Either.b(new RequiredDifficulty(ProofOfWork.MAX_DIFFICULTY)));\n        // Apply rate limiting based on IP, failures, etc.\n        // Check proof of work is sufficient, unless it is a password change\n        byte[] hash = hasher.sha256(ArrayOps.concat(proof.prefix, new CborObject.CborList(Arrays.asList(chain)).serialize())).join();\n        startPaidRateLimiter.updateTime(System.currentTimeMillis());\n        int requiredDifficulty = startPaidRateLimiter.currentDifficulty();\n        if (! ProofOfWork.satisfiesDifficulty(requiredDifficulty, hash)) {\n            LOG.log(Level.INFO, \"Rejected start paid signup request with insufficient proof of work for difficulty: \" +\n                    requiredDifficulty + \" and username \" + username);\n            return Futures.of(Either.b(new RequiredDifficulty(requiredDifficulty)));\n        }\n\n        //  reserve username, then create user and get payment url\n        return target.startPaidSignup(username, chain, proof)\n                .thenApply(res -> {\n                    if (res.isA()) {\n                        return Either.a(quotaStore.createPaidUser(username));\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username,\n                                                                   UserPublicKeyLink chain,\n                                                                   OpLog setupOperations,\n                                                                   byte[] signedSpaceRequest,\n                                                                   ProofOfWork proof) {\n        if (isPki)\n            return target.completePaidSignup(username, chain, setupOperations, signedSpaceRequest, proof);\n        // take payment, and if successful, finalise account creation\n        quotaStore.getQuota(username); // This will throw if the user doesn't exist in quota store\n        PaymentProperties result = quotaStore.requestQuota(chain.owner, signedSpaceRequest, 0).join();\n        long quota = quotaStore.getQuota(username);\n        if (quota == 0 && ! result.hasError()) {// try again if no card showed up yet\n            for (int i = 0; i < 3; i++) {\n                LOG.info(\"Paid signup: no cards available, sleeping for 3s...\");\n                try {Thread.sleep(3_000);} catch (InterruptedException e) {}\n                result = quotaStore.requestQuota(chain.owner, signedSpaceRequest,0).join();\n                quota = quotaStore.getQuota(username);\n                if (quota > 0 || result.hasError())\n                    break;\n            }\n        }\n        LOG.info(\"Paid signup: quota=\"+quota + \", username=\" + username);\n        if (quota >= result.desiredQuota && quota > 1024*1024) {// 1 MiB is the deletion quota\n            LOG.info(\"Successful Paid signup!\");\n            AggregatedMetrics.PAID_SIGNUP_SUCCESS.inc();\n            return target.completePaidSignup(username, chain, setupOperations, signedSpaceRequest, proof);\n        } else if (result.hasError()) {\n            LOG.info(\"Payment error during Paid signup! username=\" + username);\n            // payment failed, set desired quota to 0 to prevent future payment attempts\n            quotaStore.removeDesiredQuota(username);\n        }\n        return Futures.of(result);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        long quota = quotaStore.getQuota(username);\n        if (quota <= 1024*1024)\n            throw new IllegalStateException(\"Not enough space quota!\");\n        return target.startMirror(username, mirrorBat, auth, proof);\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username,\n                                                                                            byte[] auth,\n                                                                                            ProofOfWork proof) {\n        if (maxPaidSignupsPerDay == 0)\n            return Futures.of(Either.b(new RequiredDifficulty(ProofOfWork.MAX_DIFFICULTY)));\n\n        // Apply rate limiting based on IP, failures, etc.\n        // Check proof of work is sufficient, unless it is a password change\n        byte[] data = username.getBytes(StandardCharsets.UTF_8);\n        byte[] hash = hasher.sha256(ArrayOps.concat(proof.prefix, data)).join();\n        startPaidMirrorRateLimiter.updateTime(System.currentTimeMillis());\n        int requiredDifficulty = startPaidMirrorRateLimiter.currentDifficulty();\n        if (! ProofOfWork.satisfiesDifficulty(requiredDifficulty, hash)) {\n            LOG.log(Level.INFO, \"Rejected start paid mirror request with insufficient proof of work for difficulty: \" +\n                    requiredDifficulty + \" and username \" + username);\n            return Futures.of(Either.b(new RequiredDifficulty(requiredDifficulty)));\n        }\n\n        // check auth signature\n        target.startPaidMirror(username, auth, proof).join();\n\n        // create user and get payment url\n        return Futures.of(Either.a(quotaStore.createPaidUser(username)));\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username,\n                                                                   BatWithId mirrorBat,\n                                                                   byte[] signedSpaceRequest,\n                                                                   ProofOfWork proof) {\n        // take payment, and if successful, finalise account creation\n        quotaStore.getQuota(username); // This will throw if the user doesn't exist in quota store\n        PublicKeyHash owner = target.getPublicKeyHash(username).join().get();\n        PaymentProperties result = quotaStore.requestQuota(owner, signedSpaceRequest, 0).join();\n        long quota = quotaStore.getQuota(username);\n        if (quota == 0 && ! result.hasError()) {// try again if no card showed up yet\n            for (int i = 0; i < 3; i++) {\n                LOG.info(\"Paid mirror: no cards available, sleeping for 3s...\");\n                try {Thread.sleep(3_000);} catch (InterruptedException e) {}\n                result = quotaStore.requestQuota(owner, signedSpaceRequest,0).join();\n                quota = quotaStore.getQuota(username);\n                if (quota > 0 || result.hasError())\n                    break;\n            }\n        }\n        LOG.info(\"Paid mirror: quota=\"+quota + \", username=\" + username);\n        if (quota >= result.desiredQuota && quota > 1024*1024) {// 1 MiB is the deletion quota\n            LOG.info(\"Successful Paid mirror!\");\n            AggregatedMetrics.PAID_MIRROR_SUCCESS.inc();\n\n            // start mirror\n            target.completePaidMirror(username, mirrorBat, signedSpaceRequest, proof);\n        } else if (result.hasError()) {\n            LOG.info(\"Payment error during Paid signup! username=\" + username);\n            // payment failed, set desired quota to 0 to prevent future payment attempts\n            quotaStore.removeDesiredQuota(username);\n        }\n        return Futures.of(result);\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        return target.getSnapshots(prefix, instanceBat, lastLinkCountsUpdate);\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        return target.getChain(username);\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                       List<UserPublicKeyLink> chain,\n                                                                       ProofOfWork proof,\n                                                                       String token) {\n        return target.updateChain(username, chain, proof, token);\n    }\n\n    private boolean forUs(List<UserPublicKeyLink> chain) {\n        return chain.get(chain.size() - 1).claim.storageProviders.contains(ourNodeId);\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash key) {\n        return target.getUsername(key);\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        return target.getUsernames(prefix);\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime linkCountsAfter,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        if (forUs(newChain)) {\n            if (! quotaStore.allowSignupOrUpdate(username, \"\"))\n                throw new IllegalStateException(\"This server is not currently accepting new user migrations.\");\n            PublicKeyHash owner = newChain.get(newChain.size() - 1).owner;\n\n            // check we have enough local quota to mirror all user's data\n            long localQuota = quotaStore.getQuota(username);\n            if (localQuota < currentUsage)\n                throw new IllegalStateException(\"Not enough space for user to migrate user to this server!\");\n        }\n\n        return target.migrateUser(username, newChain, currentStorageId, mirrorBat, linkCountsAfter, currentUsage, commitToPki);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return target.getNextServerId(serverId);\n    }\n\n    @Override\n    public void close() throws IOException {\n        target.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/UserRepository.java",
    "content": "package peergos.server.corenode;\n\nimport peergos.server.crypto.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class UserRepository implements SocialNetwork, MutablePointers {\n    public static final int MAX_POINTER_SIZE = TweetNaCl.SIGNATURE_SIZE_BYTES + 2 + 2*36 + 9; // Signature overhead + 2 cids + 2 (cbor list[3]) + cbor long\n\n    private final DeletableContentAddressedStorage ipfs;\n    private final JdbcIpnsAndSocial store;\n    private final List<Multihash> us;\n    private final Hasher hasher;\n\n    public UserRepository(DeletableContentAddressedStorage ipfs, JdbcIpnsAndSocial store, Hasher hasher) {\n        this.ipfs = ipfs;\n        this.store = store;\n        this.us = List.of(ipfs.id().join());\n        this.hasher = hasher;\n    }\n\n    @Override\n    public CompletableFuture<byte[]> getFollowRequests(PublicKeyHash owner, byte[] signedTime) {\n        TimeLimited.isAllowedTime(signedTime, 300, ipfs, owner);\n        return store.getFollowRequests(owner);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> sendFollowRequest(PublicKeyHash target, byte[] encryptedPermission) {\n        return store.addFollowRequest(target, encryptedPermission);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> removeFollowRequest(PublicKeyHash owner, byte[] data) {\n        return ipfs.getSigningKey(owner, owner).thenCompose(signerOpt -> {\n            try {\n                byte[] unsigned = signerOpt.get().unsignMessage(data).join();\n                return store.removeFollowRequest(owner, unsigned);\n            } catch (TweetNaCl.InvalidSignatureException e) {\n                return CompletableFuture.completedFuture(false);\n            }\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        return store.getPointer(writer);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        return getPointer(owner, writer)\n                .thenCompose(current -> ipfs.getSigningKey(owner, writer)\n                        .thenCompose(writerOpt -> {\n                            try {\n                                if (writerSignedBtreeRootHash.length > MAX_POINTER_SIZE)\n                                    throw new IllegalStateException(\"Pointer update too big! \" + writerSignedBtreeRootHash.length);\n                                if (! writerOpt.isPresent())\n                                    throw new IllegalStateException(\"Couldn't retrieve writer key from ipfs with hash \" + writer);\n                                PublicSigningKey writerKey = writerOpt.get();\n                                byte[] bothHashes = writerKey.unsignMessage(writerSignedBtreeRootHash).join();\n                                PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n                                MaybeMultihash claimedCurrentHash = cas.original;\n\n                                return MutablePointers.isValidUpdate(writerKey, current, claimedCurrentHash, cas.sequence)\n                                        .thenCompose(x -> {\n\n                                            // check the new target is valid for this writer (or a deletion)\n                                            if (cas.updated.isPresent()) {\n                                                Multihash newHash = cas.updated.get();\n                                                CommittedWriterData newWriterData = DeletableContentAddressedStorage.getWriterData(us, owner, (Cid) newHash, cas.sequence, false, (Cid)us.get(0), hasher, ipfs).join();\n                                                if (!newWriterData.props.get().controller.equals(writer))\n                                                    return Futures.of(false);\n                                            }\n\n                                            return store.setPointer(writer, current, writerSignedBtreeRootHash);\n                                        });\n                            } catch (TweetNaCl.InvalidSignatureException e) {\n                                System.err.println(\"Invalid signature during setMetadataBlob for sharer: \" + writer);\n                                return Futures.of(false);\n                            }\n                        }));\n\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        // Validate all signatures and CAS constraints concurrently, then write atomically.\n        List<CompletableFuture<Optional<byte[]>>> validations = updates.stream()\n                .map(u -> validateUpdate(owner, u))\n                .collect(Collectors.toList());\n        return Futures.combineAllInOrder(validations)\n                .thenCompose(current -> store.setPointers(current, updates));\n    }\n\n    private CompletableFuture<Optional<byte[]>> validateUpdate(PublicKeyHash owner, SignedPointerUpdate u) {\n        return getPointer(owner, u.writer)\n                .thenCompose(current -> ipfs.getSigningKey(owner, u.writer)\n                        .thenCompose(writerOpt -> {\n                            try {\n                                if (u.signed.length > MAX_POINTER_SIZE)\n                                    throw new IllegalStateException(\"Pointer update too big! \" + u.signed.length);\n                                if (!writerOpt.isPresent())\n                                    throw new IllegalStateException(\"Couldn't retrieve writer key from ipfs with hash \" + u.writer);\n                                PublicSigningKey writerKey = writerOpt.get();\n                                byte[] bothHashes = writerKey.unsignMessage(u.signed).join();\n                                PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n                                return MutablePointers.isValidUpdate(writerKey, current, cas.original, cas.sequence)\n                                        .thenCompose(x -> {\n                                            if (cas.updated.isPresent()) {\n                                                Multihash newHash = cas.updated.get();\n                                                CommittedWriterData newWriterData = DeletableContentAddressedStorage\n                                                        .getWriterData(us, owner, (Cid) newHash, cas.sequence, false, (Cid) us.get(0), hasher, ipfs).join();\n                                                if (!newWriterData.props.get().controller.equals(u.writer))\n                                                    throw new IllegalStateException(\"WriterData controller mismatch for \" + u.writer);\n                                            }\n                                            return Futures.of(current);\n                                        });\n                            } catch (TweetNaCl.InvalidSignatureException e) {\n                                System.err.println(\"Invalid signature during setPointers for writer: \" + u.writer);\n                                return Futures.<Optional<byte[]>>errored(e);\n                            }\n                        }));\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return this;\n    }\n\n    public static UserRepository build(DeletableContentAddressedStorage ipfs, JdbcIpnsAndSocial sqlNode, Hasher h) {\n        return new UserRepository(ipfs, sqlNode, h);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/corenode/UsernameValidator.java",
    "content": "package peergos.server.corenode;\n\nimport peergos.shared.corenode.*;\n\nimport java.util.*;\nimport java.util.regex.Pattern;\nimport java.util.stream.*;\n\n/**\n * Encapsulates CoreNode username rules.\n *\n */\npublic final class UsernameValidator {\n\n    private static final Pattern VALID_USERNAME = Pattern.compile(Usernames.REGEX);\n\n    // These are for potential future interoperability and federation/bridging\n    public static final Set<String> BANNED_USERNAMES =\n            Stream.of(\"ipfs\", \"ipns\", \"root\", \"http\", \"https\", \"dns\", \"admin\", \"administrator\", \"support\", \"email\", \"mail\", \"www\",\n                    \"web\", \"onion\", \"tls\", \"i2p\", \"ftp\", \"sftp\", \"file\", \"mailto\", \"wss\", \"xmpp\", \"ssh\", \"smtp\", \"imap\",\n                    \"irc\", \"matrix\", \"twitter\", \"facebook\", \"instagram\", \"linkedin\", \"wechat\", \"tiktok\", \"reddit\",\n                    \"snapchat\", \"qq\", \"whatsapp\", \"signal\", \"telegram\", \"matrix\", \"briar\", \"ssb\", \"mastodon\",\n                    \"apple\", \"google\", \"pinterest\",\n                    \"constructor\",\n                    \"mls\", \"btc\", \"eth\", \"mnr\", \"zec\", \"friends\", \"followers\", \"username\", \"groups\")\n            .collect(Collectors.toSet());\n\n    /** Username rules:\n     * no _- at the end\n     * allowed characters [a-z0-9_-]\n     * no __ or -- or _- or -_ inside\n     * no _- at the beginning\n     * is 1-32 characters long\n     * @param username\n     * @return true iff username is a valid username.\n     */\n    public static boolean isValidUsername(String username) {\n        return (VALID_USERNAME.matcher(username).find() && ! BANNED_USERNAMES.contains(username))\n                || LEGACY_UNDERSCORE.contains(username);\n    }\n\n    public static final Set<String> LEGACY_UNDERSCORE = Stream.of(\n            \"federal_appeal\",\"elle_esse\",\"xzz_yassin\",\"jonas_lindemann\",\"nicolas_thill\",\"yr_zr0\",\"a_bennassar\",\n            \"the_darktrancer\",\"evans_luke\",\"skater_welladay\",\"abc_007\",\"a_little\",\"narcoleptic_snowman\",\"faraz_55\",\n            \"robert_forestell\",\"dfc_test\",\"ozzy_reijnaert\",\"granite_zl\",\"slash_27\",\"caso_saphy\",\"asc_fer41\",\"tim_chen\",\n            \"bengao_zhou\",\"boy_witch\",\"levanto_0\",\"abis_biso\",\"la_la_land\",\"jeff_systemhouse\",\"yier_fang\",\"femi-s_1986\",\n            \"0_0\",\"jungle_adventure\",\"afx_infamix\",\"statik_ip\",\"the_powerdrift_dabster\",\"edrad_wolf\",\"space_cat\",\n            \"r3dpill_17\",\"chaaava_sulcom\",\"b_kirby_8\",\"tech_digger\",\"computer_killer_9_million\",\"radek_nielek\",\n            \"eara_test\",\"sudo_scientist\",\"troels_a\",\"bkd_5\",\"elvin_arrow\",\"ap_java\",\"mr_benjiworld\",\"shiba_coin\",\n            \"white_cashmere\",\"steinar_ag\",\"scott_davis\",\"6_6\",\"nathaniel_gray\",\"thann_banis\",\"jiang_wei\",\"mike_gale\",\n            \"uppercase_manager\",\"jiang_ziya\",\"winston_smith\",\"tiny_fingers\",\"jared_balser\",\"bama_dan\",\"serene_xp\",\n            \"fallen_melon\",\"void_tux\",\"boogie6_6_6\",\"l1la_siren3\",\"iulia_radu\",\"pagmupka_88pagmupka\",\"just_kush\",\n            \"nesh_collo\",\"kibana_user\",\"giannis_geroulis\",\"magnetic_cactus\",\"edvard_norton\",\"mac_a\",\"electro_cloud\",\n            \"blue_100\",\"alex_renn\",\"tp_voyager\",\"the_taco_truck\",\"nil_float\",\"nifty_nft\",\"coco_carma\",\"maya_n\",\n            \"santhosh_reddy\",\"vini_mendes\",\"warriorsga_legis\",\"player_sgs\",\"abn_anik\",\"iagon_test\",\"soupe_cramee\",\n            \"amir_99\",\"haven_mobius\",\"jackripper_1888\",\"ankush_itsfoss\",\"artz_sam\",\"ernesto_fm\",\"seh_zade\",\"dh_gmbnrc\",\n            \"nesh_kothari\",\"s_k_2003\",\"steph_girow\",\"kalibyr_bbx\",\"ameyyy_7303\",\"john_betong\",\"head_brother\",\n            \"unknown_3301\",\"varun_invent\",\"max_mustermann7\",\"md_parker\",\"n_e_felibata\",\"neo_sunny\",\n            \"sankt-peterburg_2017\",\"ky_hsiao\",\"miklehey_717\",\"jd_online\",\"pradeep_07\",\"ernesto_f_m\",\"public_enemy\",\n            \"capitan_bonaccia\",\"sram_bot19\",\"darth_vader\",\"serg_w\",\"k_engelstad\",\"udon_zud\",\"aksja_01_81\",\"darth_malt\",\n            \"assassin_navod\",\"system_design_public_storage\",\"bobfromaccounting_ut\",\"kris_d\",\n            \"systemdesign_publicstorage\",\"alastor_dk9\",\"nu_ll\",\"monkey_maniu\",\"rahul_kulkarni\",\"m_name\",\"nbk_name\",\n            \"v_142857\",\"prince_156\",\"quantoo_antoo\",\"emanuel_ekstrom5634\",\"tax_protestor\",\"vicky_bs\",\"alex_stap\",\n            \"btenors_storage\",\"faustino_c\",\"ace_crusader\",\"1234567890-qwerty_456\",\"gia_bass_provincia_cremona_it\",\n            \"other_side2\",\"arsalan_daneshvar\",\"art_om\",\"evg01_kurz\",\"margal_user\",\"mr_q\",\"mr_qaz\",\"orca_pg\",\"account_2\",\n            \"drew_meetingav_net\",\"test_37\",\"matcho_a\",\"3_14zdec\",\"test_36\",\"account_1\",\"mr_kovacs\",\"trinity_21\",\"cem_7\",\n            \"i_come_from_the_sky\",\"peergos_32p\",\"toshie_yoshida\",\"gimme_cherry\",\"0_o\",\"fxu_36\",\"michael_philippone\",\n            \"der_crazy\",\"scope_recast\",\"jost_burkardt\",\"patchdrive_admin\",\"patchbay_email\",\"patchbay_boss\",\n            \"element_mae\",\"patchbay_elephant\",\"elegant_whale\",\"o_b_o\",\"saskia_sielias\",\"jmhlbcmtknu_omx\",\"coyote_pinke\",\n            \"time_spirit\",\"black_tourmaline86\",\"talus_sp1ke\",\"mr_mike\",\"x0e_e0x\",\"salted_hashbrown\",\"marchon_gmail_com\",\n            \"wtop_eipp\",\"automuse_amrita\",\"little_cow_cat\",\"gevorg_vardanyan\",\"gift_park\",\"yufeng_chin\",\"jac_cos\",\"g_c\",\n            \"kpt_kloss\",\"smtp_sage3\",\"newmay_admin_shared\",\"patchbaycardano_smtp\",\"normcoin_newmay\",\"bitter_sweet\",\n            \"cguo_zz\",\"ebaiy_trimline\",\"pierre_gronau_ndaal_eu\",\"nikola_tesla\",\"fateev_so\",\"di_rocha27\"\n    ).collect(Collectors.toSet());\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/JniTweetNacl.java",
    "content": "package peergos.server.crypto;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class JniTweetNacl {\n\n    private JniTweetNacl() {}\n\n    public static JniTweetNacl build() {\n        return new JniTweetNacl();\n    }\n\n    private static String canonicaliseArchitecture(String arch) {\n        if (arch.startsWith(\"arm64\"))\n            return \"aarch64\";\n        if (arch.startsWith(\"arm\"))\n            return \"arm\";\n        if (arch.startsWith(\"x86_64\"))\n            return \"amd64\";\n        return arch;\n    }\n\n    static {\n        String absoluteLibPath = \"\";\n        boolean isLinux = \"linux\".equalsIgnoreCase(System.getProperty(\"os.name\"));\n        boolean isAndroid = \"The Android Project\".equals(System.getProperty(\"java.vm.vendor\"));\n        if (isAndroid) {\n            System.loadLibrary(\"tweetnacl\");\n        } else {\n            String arch = canonicaliseArchitecture(System.getProperty(\"os.arch\"));\n            try {\n                new File(\"native-lib\").mkdirs();\n                Path writeLibPath = PathUtil.get(\"native-lib\", \"libtweetnacl.so\");\n                if (! writeLibPath.toFile().exists()) {\n                    Path libPath = PathUtil.get(\"native-lib\", \"linux\", arch, \"libtweetnacl.so\");\n                    byte[] data = Serialize.readFully(JniTweetNacl.class.getResourceAsStream(\"/\" + libPath));\n                    Files.write(writeLibPath, data, StandardOpenOption.CREATE);\n                }\n                absoluteLibPath = writeLibPath.toFile().getAbsolutePath();\n                System.loadLibrary(\"tweetnacl\");\n            } catch (Throwable t) {\n                if (isLinux) {\n                    System.err.println(\"Couldn't load native crypto library at \" + absoluteLibPath + \", using pure Java version...\");\n                    System.err.println(\"To use the native Linux crypto implementation use option -Djava.library.path=native-lib\");\n                }\n                throw new RuntimeException(t);\n            }\n        }\n    }\n\n    public static native int crypto_box_keypair(byte[] y, byte[] x);\n\n    public static native int crypto_box_open(byte[] m, byte[]c, long d, byte[] b, byte[] y, byte[] x);\n\n    public static native int crypto_box(byte[] c, byte[] m, long d, byte[] n, byte[] y, byte[] x);\n\n\n    public static native int crypto_sign_open(byte[] m, long mlen, byte[] sm, long n, byte[] pk);\n\n    public static native int crypto_sign(byte[] sm, long smlen, byte[] m, long n, byte[] sk);\n\n    public static native int crypto_sign_keypair(byte[] pk, byte[] sk);\n\n\n    public static native int crypto_secretbox_open(byte[] m, byte[] c, long d, byte[] n, byte[] k);\n\n    public static native int crypto_secretbox(byte[] c, byte[] m, long d, byte[] n, byte[] k);\n\n\n    public static native int crypto_scalarmult_base(byte[] q, byte[] n);\n\n    public static native int ld32(byte[] b);\n\n    public static class Signer implements Ed25519 {\n\n        private final JniTweetNacl impl;\n\n        public Signer(JniTweetNacl impl) {\n            this.impl = impl;\n        }\n\n        @Override\n        public CompletableFuture<byte[]> crypto_sign_open(byte[] signed, byte[] publicSigningKey) {\n            byte[] message = new byte[signed.length];\n            int res = impl.crypto_sign_open(message, message.length, signed, signed.length, publicSigningKey);\n            if (res != 0)\n                throw new TweetNaCl.InvalidSignatureException();\n            return Futures.of(Arrays.copyOfRange(message, 0, message.length - TweetNaCl.SIGNATURE_SIZE_BYTES));\n        }\n\n        @Override\n        public CompletableFuture<byte[]> crypto_sign(byte[] message, byte[] secretSigningKey) {\n            byte[] signedMessage = new byte[message.length + TweetNaCl.SIGNATURE_SIZE_BYTES];\n            impl.crypto_sign(signedMessage, signedMessage.length, message, message.length, secretSigningKey);\n            return Futures.of(signedMessage);\n        }\n\n        @Override\n        public void crypto_sign_keypair(byte[] pk, byte[] sk) {\n            impl.crypto_sign_keypair(pk, sk);\n        }\n    }\n\n    public static class Symmetric implements peergos.shared.crypto.symmetric.Salsa20Poly1305 {\n\n        private final JniTweetNacl impl;\n\n        public Symmetric(JniTweetNacl impl) {\n            this.impl = impl;\n        }\n\n        @Override\n        public byte[] secretbox(byte[] data, byte[] nonce, byte[] key) {\n            byte[] cipherText = new byte[data.length + 32]; // add secret box internal overhead bytes\n            byte[] expandedData = new byte[cipherText.length];\n            System.arraycopy(data, 0, expandedData, 32, data.length);\n            int res = impl.crypto_secretbox(cipherText, expandedData, cipherText.length, nonce, key);\n            if (res != 0)\n                throw new TweetNaCl.InvalidSignatureException();\n            return Arrays.copyOfRange(cipherText, 16, cipherText.length);\n        }\n\n        @Override\n        public byte[] secretbox_open(byte[] cipher, byte[] nonce, byte[] key) {\n            byte[] message = new byte[cipher.length + TweetNaCl.SECRETBOX_OVERHEAD_BYTES];\n            byte[] expandedCipher = new byte[message.length];\n            System.arraycopy(cipher, 0, expandedCipher, TweetNaCl.SECRETBOX_OVERHEAD_BYTES, cipher.length);\n            int res = impl.crypto_secretbox_open(message, expandedCipher, expandedCipher.length, nonce, key);\n            if (res != 0)\n                throw new InvalidCipherTextException();\n            return Arrays.copyOfRange(message, 32, message.length);\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/TweetNaCl.java",
    "content": "package peergos.server.crypto;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.util.*;\n\nimport java.security.*;\nimport java.util.Arrays;\nimport java.util.Random;\n\n/* Ported from the original C by Ian Preston and Chris Boddy\n * crypto_hash() is ported from TweetNaCl.js\n * Released under GPL 2\n */\npublic class TweetNaCl {\n\n    public static final int crypto_auth_hmacsha512256_tweet_BYTES = 32;\n    public static final int crypto_auth_hmacsha512256_tweet_KEYBYTES = 32;\n    public static final int BOX_PUBLIC_KEY_BYTES = 32;\n    public static final int BOX_SECRET_KEY_BYTES = 32;\n    public static final int BOX_SHARED_KEY_BYTES = 32;\n    public static final int BOX_NONCE_BYTES = 24;\n    public static final int BOX_OVERHEAD_BYTES = 16;\n    public static final int SIGNATURE_SIZE_BYTES = 64;\n    public static final int SIGN_PUBLIC_KEY_BYTES = 32;\n    public static final int SIGN_SECRET_KEY_BYTES = 64;\n    public static final int SIGN_KEYPAIR_SEED_BYTES = 32;\n    public static final int SECRETBOX_KEY_BYTES = 32;\n    public static final int SECRETBOX_NONCE_BYTES = 24;\n    public static final int SECRETBOX_OVERHEAD_BYTES = 16;\n    public static final int HASH_SIZE_BYTES = 64; // SHA-512\n    private static final int SECRETBOX_INTERNAL_OVERHEAD_BYTES = 32;\n\n    public static class InvalidSignatureException extends RuntimeException {}\n\n    public static void crypto_sign_keypair(byte[] pk, byte[] sk, boolean isSeeded)\n    {\n        byte[] d = new byte[64];\n        long[][] /*gf*/ p = new long[4][GF_LEN];\n        int i;\n\n        if (!isSeeded)\n            randombytes(sk, 32);\n        crypto_hash(d, sk, 32);\n        d[0] &= 248;\n        d[31] &= 127;\n        d[31] |= 64;\n\n        scalarbase(p,d, 0);\n        pack(pk,p);\n\n        for (i=0;i < 32;++i)sk[32 + i] = pk[i];\n    }\n\n    public static int crypto_box_keypair(byte[] y,byte[] x, boolean isSeeded)\n    {\n        if (!isSeeded)\n            randombytes(x,32);\n        return crypto_scalarmult_base(y,x);\n    }\n\n    public static int crypto_scalarmult_base(byte[] q,byte[] n)\n    {\n        return crypto_scalarmult(q, n, _9);\n    }\n\n    public static byte[] crypto_sign(byte[] message, byte[] secretSigningKey) {\n        byte[] signedMessage = new byte[message.length + TweetNaCl.SIGNATURE_SIZE_BYTES];\n        TweetNaCl.crypto_sign(signedMessage, message, message.length, secretSigningKey);\n        return signedMessage;\n    }\n\n    public static byte[] crypto_sign_open(byte[] signed, byte[] publicSigningKey) {\n        byte[] message = new byte[signed.length];\n        int res = TweetNaCl.crypto_sign_open(message, signed, signed.length, publicSigningKey);\n        if (res != 0)\n            throw new InvalidSignatureException();\n        return Arrays.copyOfRange(message, 64, message.length);\n    }\n\n    public static byte[] crypto_box(byte[] message, byte[] nonce, byte[] theirPublicBoxingKey, byte[] ourSecretBoxingKey) {\n        if (nonce.length != BOX_NONCE_BYTES)\n            throw new IllegalStateException(\"Illegal nonce length: \"+nonce.length);\n        byte[] cipherText = new byte[SECRETBOX_INTERNAL_OVERHEAD_BYTES + message.length];\n        byte[] paddedMessage = new byte[SECRETBOX_INTERNAL_OVERHEAD_BYTES + message.length];\n        System.arraycopy(message, 0, paddedMessage, SECRETBOX_INTERNAL_OVERHEAD_BYTES, message.length);\n        TweetNaCl.crypto_box(cipherText, paddedMessage, paddedMessage.length, nonce, theirPublicBoxingKey, ourSecretBoxingKey);\n        return Arrays.copyOfRange(cipherText, 16, cipherText.length);\n    }\n\n    public static byte[] crypto_box_open(byte[] cipher, byte[] nonce, byte[] theirPublicBoxingKey, byte[] secretBoxingKey) {\n        byte[] paddedCipher = new byte[cipher.length + 16];\n        System.arraycopy(cipher, 0, paddedCipher, 16, cipher.length);\n        byte[] rawText = new byte[paddedCipher.length];\n        int res = TweetNaCl.crypto_box_open(rawText, paddedCipher, paddedCipher.length, nonce, theirPublicBoxingKey, secretBoxingKey);\n        if (res != 0)\n            throw new InvalidCipherTextException();\n        return Arrays.copyOfRange(rawText, 32, rawText.length);\n    }\n\n    public static byte[] secretbox(byte[] mesage, byte[] nonce, byte[] key) {\n        byte[] m = new byte[SECRETBOX_INTERNAL_OVERHEAD_BYTES + mesage.length];\n        byte[] c = new byte[m.length];\n        System.arraycopy(mesage, 0, m, SECRETBOX_INTERNAL_OVERHEAD_BYTES, mesage.length);\n        crypto_secretbox(c, m, m.length, nonce, key);\n        return Arrays.copyOfRange(c, SECRETBOX_OVERHEAD_BYTES, c.length);\n    }\n\n    private static final InvalidCipherTextException INVALID = new InvalidCipherTextException();\n\n    public static byte[] secretbox_open(byte[] cipher, byte[] nonce, byte[] key) {\n        byte[] c = new byte[SECRETBOX_OVERHEAD_BYTES + cipher.length];\n        byte[] m = new byte[c.length];\n        System.arraycopy(cipher, 0, c, SECRETBOX_OVERHEAD_BYTES, cipher.length);\n        boolean validCipher = c.length >= 32;\n        boolean success = crypto_secretbox_open(m, c, c.length, nonce, key) == 0;\n        boolean isValid = validCipher && success;\n\n        if (! isValid)\n            throw INVALID;\n\n        return Arrays.copyOfRange(m, SECRETBOX_INTERNAL_OVERHEAD_BYTES, m.length);\n    }\n\n    private static byte[] _0 = new byte[16], _9 = new byte[32];\n    static {\n        _9[0] = 9;\n    }\n    private static final int GF_LEN = 16;\n    private static long[]  gf0 = new long[GF_LEN];\n    private static long[] gf1 = new long[GF_LEN]; static{gf1[0] = 1;}\n    private static long[]  _121665 = new long[GF_LEN]; static{_121665[0] = 0xDB41; _121665[1] =1;}\n    private static long[]  D = new long[]{0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203},\n            D2 = new long[]{0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406},\n            X = new long[]{0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169},\n            Y = new long[]{0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666},\n            I = new long[]{0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83};\n\n    private static int L32(int x,int c) { return (x << c) | (x >>> (32 - c)); }\n\n    public static int ld32(byte[] x, int off)\n    {\n        int u = x[off + 3] & 0xff;\n        u = (u<<8)|(x[off + 2] & 0xff);\n        u = (u<<8)|(x[off + 1] & 0xff);\n        return (u<<8)|(x[off + 0] & 0xff);\n    }\n\n    private static void st32(byte[] x, int off, int u)\n    {\n        int i;\n        for (i=0;i < 4;++i){ x[off + i] = (byte)u; u >>= 8; }\n    }\n\n    private static int vn(byte[] x, int xOff, byte[] y,int n)\n    {\n        int i,d = 0;\n        for (i=0;i < n;++i)d |= 0xff & (x[xOff + i]^y[i]);\n        return (1 & ((d - 1) >> 8)) - 1;\n    }\n\n    private static int crypto_verify_16(byte[] x, int xOff, byte[] y)\n    {\n        return vn(x, xOff, y, 16);\n    }\n\n    private static int crypto_verify_32(byte[] x,byte[] y)\n    {\n        return vn(x, 0, y,32);\n    }\n\n    private static void core(byte[] out,byte[] in,byte[] k,byte[] c,int h)\n    {\n        int[] w = new int[16],x = new int[16],y = new int[16],t = new int[4];\n        int i,j,m;\n\n        for (i=0;i < 4;++i){\n            x[5*i] = ld32(c,4*i);\n            x[1+i] = ld32(k,4*i);\n            x[6+i] = ld32(in,4*i);\n            x[11+i] = ld32(k,16+4*i);\n        }\n\n        for (i=0;i < 16;++i)y[i] = x[i];\n\n        for (i=0;i < 20;++i){\n            for (j=0;j < 4;++j){\n                for (m=0;m < 4;++m)t[m] = x[(5*j+4*m)%16];\n                t[1] ^= L32(t[0]+t[3], 7);\n                t[2] ^= L32(t[1]+t[0], 9);\n                t[3] ^= L32(t[2]+t[1],13);\n                t[0] ^= L32(t[3]+t[2],18);\n                for (m=0;m < 4;++m)w[4*j+(j+m)%4] = t[m];\n            }\n            for (m=0;m < 16;++m)x[m] = w[m];\n        }\n\n        if (h != 0) {\n            for (i=0;i < 16;++i)x[i] += y[i];\n            for (i=0;i < 4;++i){\n                x[5*i] -= ld32(c,4*i);\n                x[6+i] -= ld32(in,4*i);\n            }\n            for (i=0;i < 4;++i){\n                st32(out, 4*i,x[5*i]);\n                st32(out, 16+4*i,x[6+i]);\n            }\n        } else\n            for (i=0;i < 16;++i)st32(out, 4 * i,x[i] + y[i]);\n    }\n\n    private static int crypto_core_salsa20(byte[] out,byte[] in,byte[] k,byte[] c)\n    {\n        core(out,in,k,c,0);\n        return 0;\n    }\n\n    private static int crypto_core_hsalsa20(byte[] out,byte[] in,byte[] k,byte[] c)\n    {\n        core(out,in,k,c,1);\n        return 0;\n    }\n\n    private static byte[] sigma = { 101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107 };\n\n    private static int crypto_stream_salsa20_xor(byte[] c,byte[] m,long b,byte[] n, int nOff, byte[] k)\n    {\n        byte[] z = new byte[16],x = new byte[64];\n        int u,i;\n        if (b == 0) return 0;\n        for (i=0;i < 16;++i)z[i] = 0;\n        for (i=0;i < 8;++i)z[i] = n[nOff + i];\n        int cOff = 0;\n        int mOff = 0;\n        while (b >= 64) {\n            crypto_core_salsa20(x,z,k,sigma);\n            for (i=0;i < 64; ++i) c[cOff + i] = (byte)((m != null ? m[mOff + i]:0)^ x[i]);\n            u = 1;\n            for (i = 8;i < 16;++i) {\n                u += 0xff & z[i];\n                z[i] = (byte)u;\n                u >>= 8;\n            }\n            b -= 64;\n            cOff += 64;\n            if (m != null) mOff += 64;\n        }\n        if (b != 0) {\n            crypto_core_salsa20(x,z,k,sigma);\n            for (i=0;i < b; i++) c[cOff + i] = (byte)((m != null ? m[mOff + i]:0)^ x[i]);\n        }\n        return 0;\n    }\n\n    private static int crypto_stream_salsa20(byte[] c,long d,byte[] n, int nOff, byte[] k)\n    {\n        return crypto_stream_salsa20_xor(c,null,d,n, nOff, k);\n    }\n\n    private static int crypto_stream(byte[] c,long d,byte[] n,byte[] k)\n    {\n        byte[] s = new byte[32];\n        crypto_core_hsalsa20(s,n,k,sigma);\n        return crypto_stream_salsa20(c, d, n, 16, s);\n    }\n\n    private static int crypto_stream_xor(byte[] c,byte[] m,long d,byte[] n,byte[] k)\n    {\n        byte[] s = new byte[32];\n        crypto_core_hsalsa20(s,n,k,sigma);\n        return crypto_stream_salsa20_xor(c, m, d, n, 16, s);\n    }\n\n    private static void add1305(int[] h,int[] c)\n    {\n        int j,u = 0;\n        for (j=0;j < 17;++j){\n        u += h[j] + c[j];\n        h[j] = u & 255;\n        u >>= 8;\n    }\n    }\n\n    private static int[] minusp = new int[] {\n        5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252\n    } ;\n\n    private static int crypto_onetimeauth(byte[] out, int outOff, byte[] m, int mOff, long n,byte[] k)\n    {\n        int s,i,j,u;\n        int[] x = new int[17],r = new int[17],h = new int[17],c = new int[17],g = new int[17];\n\n        for (j=0;j < 17;++j)\n            r[j]= h[j] = 0;\n        for (j=0;j < 16;++j)\n            r[j] = 0xff & k[j];\n        r[3]&=15;\n        r[4]&=252;\n        r[7]&=15;\n        r[8]&=252;\n        r[11]&=15;\n        r[12]&=252;\n        r[15]&=15;\n\n        while (n > 0) {\n            for (j=0;j < 17;++j)\n                c[j] = 0;\n            for (j = 0;(j < 16) && (j < n);++j)\n                c[j] = 0xff & m[mOff + j];\n            c[j] = 1;\n            mOff += j; n -= j;\n            add1305(h,c);\n            for (i=0;i < 17;++i){\n                x[i] = 0;\n                for (j=0;j < 17; ++j)\n                    x[i] += h[j] * ((j <= i)? r[i - j] : 320 * r[i + 17 - j]);\n            }\n            for (i=0;i < 17;++i)\n                h[i] = x[i];\n            u = 0;\n            for (j=0;j < 16;++j){\n                u += h[j];\n                h[j] = u & 255;\n                u >>= 8;\n            }\n            u += h[16]; h[16] = u & 3;\n            u = 5 * (u >> 2);\n            for (j=0;j < 16;++j){\n                u += h[j];\n                h[j] = u & 255;\n                u >>= 8;\n            }\n            u += h[16]; h[16] = u;\n        }\n\n        for (j=0;j < 17;++j)g[j] = h[j];\n        add1305(h,minusp);\n        s = -(h[16] >> 7);\n        for (j=0;j < 17;++j)h[j] ^= s & (g[j] ^ h[j]);\n\n        for (j=0;j < 16;++j)\n            c[j] = 0xff & k[j + 16];\n        c[16] = 0;\n        add1305(h,c);\n        for (j=0;j < 16;++j)out[outOff + j] = (byte)h[j];\n        return 0;\n    }\n\n    private static int crypto_onetimeauth_verify(byte[] h, int hOff, byte[] m, int mOff, long n,byte[] k)\n    {\n        byte[] x = new byte[16];\n        crypto_onetimeauth(x, 0, m, mOff, n,k);\n        return crypto_verify_16(h, hOff, x);\n    }\n\n    private static int crypto_secretbox(byte[] c,byte[] m,long d,byte[] n,byte[] k)\n    {\n        int i;\n        if (d < 32) return -1;\n        crypto_stream_xor(c,m,d,n,k);\n        crypto_onetimeauth(c, 16, c, 32, d - 32, c);\n        for (i=0;i < 16;++i)c[i] = 0;\n        return 0;\n    }\n\n    private static int crypto_secretbox_open(byte[] m,byte[] c,long d,byte[] n,byte[] k)\n    {\n        int i;\n        byte[] x = new byte[32];\n        if (d < 32) return -1;\n        crypto_stream(x,32,n,k);\n        if (crypto_onetimeauth_verify(c, 16,c, 32,d - 32,x) != 0) return -1;\n        crypto_stream_xor(m,c,d,n,k);\n        for (i=0;i < 32;++i)m[i] = 0;\n        return 0;\n    }\n\n    private static void set25519(long[] /*gf*/ r, long[] /*gf*/ a)\n    {\n        int i;\n        for (i=0;i < 16;++i)r[i]=a[i];\n    }\n\n    private static void car25519(long[] /*gf*/ o, int oOff)\n    {\n        for (int i=0;i < 16;++i){\n            o[oOff + i]+=(1<<16);\n            long c=o[oOff + i]>>16;\n            o[oOff + (i+1) * (i<15 ? 1:0)] += c - 1 + 37 * (c-1) * (i==15 ? 1 : 0);\n            o[oOff + i]-=c<<16;\n        }\n    }\n\n    private static void sel25519(long[] /*gf*/ p,long[] /*gf*/ q,int b)\n    {\n        long t,c=~(b-1);\n        int i;\n        for (i=0;i < 16;++i){\n        t= c&(p[i]^q[i]);\n        p[i]^=t;\n        q[i]^=t;\n    }\n    }\n\n    private static void pack25519(byte[] o,long[] /*gf*/ n, int nOff)\n    {\n        int i,j,b;\n        long[] /*gf*/ m = new long[GF_LEN],t = new long[GF_LEN];\n        for (i=0;i < 16;++i)t[i]=n[nOff+i];\n        car25519(t, 0);\n        car25519(t, 0);\n        car25519(t, 0);\n        for (j=0;j < 2;++j){\n            m[0]=t[0]-0xffed;\n            for(i=1;i<15;i++) {\n                m[i]=t[i]-0xffff-((m[i-1]>>16)&1);\n                m[i-1]&=0xffff;\n            }\n            m[15]=t[15]-0x7fff-((m[14]>>16)&1);\n            b=(int)((m[15]>>16)&1);\n            m[14]&=0xffff;\n            sel25519(t,m,1-b);\n        }\n        for (i=0;i < 16;++i){\n            o[2*i]=(byte)t[i];\n            o[2*i+1]=(byte)(t[i]>>8);\n        }\n    }\n\n    private static int neq25519(long[] /*gf*/ a, long[] /*gf*/ b)\n    {\n        byte[] c = new byte[32],d = new byte[32];\n        pack25519(c,a, 0);\n        pack25519(d,b, 0);\n        return crypto_verify_32(c,d);\n    }\n\n    private static byte par25519(long[] /*gf*/ a)\n    {\n        byte[] d = new byte[32];\n        pack25519(d,a, 0);\n        return (byte)(d[0]&1);\n    }\n\n    private static void unpack25519(long[] /*gf*/ o, byte[] n)\n    {\n        int i;\n        for (i=0;i < 16;++i)\n            o[i] = (0xff & n[2*i])+((0xffL & n[2*i+1])<<8);\n        o[15]&=0x7fff;\n    }\n\n    private static void A(long[] /*gf*/ o,long[] /*gf*/ a,long[] /*gf*/ b)\n    {\n        int i;\n        for (i=0;i < 16;++i)o[i]=a[i]+b[i];\n    }\n\n    private static void Z(long[] /*gf*/ o,long[] /*gf*/ a,long[] /*gf*/ b)\n    {\n        int i;\n        for (i=0;i < 16;++i)o[i]=a[i]-b[i];\n    }\n\n    private static void M(long[] /*gf*/ o, int oOff, long[] /*gf*/ a, int aOff, long[] /*gf*/ b, int bOff)\n    {\n        long[] t = new long[31];\n        for (int i=0;i < 31;++i)t[i]=0;\n        for (int i=0;i < 16; ++i) for(int j=0; j <16;++j)t[i+j]+=a[aOff + i]*b[bOff + j];\n        for (int i=0;i < 15;++i)t[i]+=38*t[i+16];\n        for (int i=0;i < 16;++i)o[oOff + i]=t[i];\n        car25519(o, oOff);\n        car25519(o, oOff);\n    }\n\n    private static void S(long[] /*gf*/ o,long[] /*gf*/ a)\n    {\n        M(o, 0, a, 0, a, 0);\n    }\n\n    private static void inv25519(long[] /*gf*/ o, int oOff, long[] /*gf*/ i, int iOff)\n    {\n        long[] /*gf*/ c = new long[GF_LEN];\n        int a;\n        for (a=0;a < 16;++a)c[a]=i[iOff + a];\n        for(a=253;a>=0;a--) {\n            S(c,c);\n            if(a!=2&&a!=4) M(c, 0, c, 0, i, iOff);\n        }\n        for (a=0;a < 16;++a)o[oOff + a]=c[a];\n    }\n\n    private static void pow2523(long[] /*gf*/ o,long[] /*gf*/ i)\n    {\n        long[] /*gf*/ c = new long[GF_LEN];\n        int a;\n        for (a=0;a < 16;++a)c[a]=i[a];\n        for(a=250;a>=0;a--) {\n            S(c,c);\n            if(a!=1) M(c, 0, c, 0, i, 0);\n        }\n        for (a=0;a < 16;++a)o[a]=c[a];\n    }\n\n    private static int crypto_scalarmult(byte[] q,byte[] n,byte[] p)\n    {\n        byte[] z = new byte[32];\n        long[] x = new long[80];\n        int r;\n        int i;\n        long[] /*gf*/ a = new long[GF_LEN],b = new long[GF_LEN],c = new long[GF_LEN],\n                d = new long[GF_LEN],e = new long[GF_LEN],f = new long[GF_LEN];\n        for (i=0;i < 31;++i)\n            z[i] = n[i];\n        z[31] = (byte)((n[31]&127)|64);\n        z[0] &= 248;\n        unpack25519(x,p);\n        for (i=0;i < 16;++i){\n            b[i]=x[i];\n            d[i]=a[i]=c[i]=0;\n        }\n        a[0]=d[0]=1;\n\n        for(i=254;i>=0;--i) {\n            r=( (0xff & z[i>>3]) >> (i&7))&1;\n            sel25519(a,b,r);\n            sel25519(c,d,r);\n            A(e,a,c);\n            Z(a,a,c);\n            A(c,b,d);\n            Z(b,b,d);\n            S(d,e);\n            S(f,a);\n            M(a, 0, c, 0, a, 0);\n            M(c, 0, b, 0, e, 0);\n            A(e,a,c);\n            Z(a,a,c);\n            S(b, a);\n            Z(c,d,f);\n            M(a, 0, c, 0, _121665, 0);\n            A(a, a, d);\n            M(c, 0, c, 0, a, 0);\n            M(a, 0, d, 0, f, 0);\n            M(d, 0, b, 0, x, 0);\n            S(b,e);\n            sel25519(a,b,r);\n            sel25519(c,d,r);\n        }\n        for (i=0;i < 16;++i){\n            x[i+16]=a[i];\n            x[i+32]=c[i];\n            x[i+48]=b[i];\n            x[i+64]=d[i];\n        }\n\n        inv25519(x, 32,x, 32);\n\n        M(x, 16,x, 16, x, 32);\n\n        pack25519(q,x, 16);\n        return 0;\n    }\n\n    private static int crypto_box_beforenm(byte[] k,byte[] y,byte[] x)\n    {\n        byte[] s = new byte[32];\n        crypto_scalarmult(s, x, y);\n        return crypto_core_hsalsa20(k,_0,s,sigma);\n    }\n\n    private static int crypto_box_afternm(byte[] c,byte[] m,long d,byte[] n,byte[] k)\n    {\n        return crypto_secretbox(c, m, d, n, k);\n    }\n\n    private static int crypto_box_open_afternm(byte[] m,byte[] c,long d,byte[] n,byte[] k)\n    {\n        return crypto_secretbox_open(m, c, d, n, k);\n    }\n\n    private static int crypto_box(byte[] c,byte[] m,long d,byte[] nonce, byte[] theirPublicBoxingKey, byte[] ourSecretBoxingKey)\n    {\n        byte[] k = new byte[32];\n        crypto_box_beforenm(k, theirPublicBoxingKey, ourSecretBoxingKey);\n        return crypto_box_afternm(c, m, d, nonce, k);\n    }\n\n    private static int crypto_box_open(byte[] m,byte[] c,long d,byte[] n,byte[] y,byte[] x)\n    {\n        byte[] k = new byte[32];\n        crypto_box_beforenm(k,y,x);\n        return crypto_box_open_afternm(m, c, d, n, k);\n    }\n\n    private static int crypto_hash(byte[] out, byte[] m, int n) {\n        int[] hh = new int[8], hl = new int[8];\n        byte[] x = new byte[256];\n        int i, b = n;\n\n        hh[0] = 0x6a09e667;\n        hh[1] = 0xbb67ae85;\n        hh[2] = 0x3c6ef372;\n        hh[3] = 0xa54ff53a;\n        hh[4] = 0x510e527f;\n        hh[5] = 0x9b05688c;\n        hh[6] = 0x1f83d9ab;\n        hh[7] = 0x5be0cd19;\n\n        hl[0] = 0xf3bcc908;\n        hl[1] = 0x84caa73b;\n        hl[2] = 0xfe94f82b;\n        hl[3] = 0x5f1d36f1;\n        hl[4] = 0xade682d1;\n        hl[5] = 0x2b3e6c1f;\n        hl[6] = 0xfb41bd6b;\n        hl[7] = 0x137e2179;\n\n        crypto_hashblocks_hl(hh, hl, m, n);\n        n %= 128;\n\n        for (i = 0; i < n; i++) x[i] = m[b-n+i];\n        x[n] = (byte)128;\n\n        n = 256-128*(n<112?1:0);\n        x[n-9] = 0;\n        jsts64(x, n - 8, (b / 0x20000000), b << 3);\n        crypto_hashblocks_hl(hh, hl, x, n);\n\n        for (i = 0; i < 8; i++) jsts64(out, 8 * i, hh[i], hl[i]);\n\n        return 0;\n    }\n\n    private static void jsts64(byte[] x, int i, int h, int l) {\n        x[i]   = (byte)(h >> 24);\n        x[i+1] = (byte)(h >> 16);\n        x[i+2] = (byte)(h >>  8);\n        x[i+3] = (byte)h;\n        x[i+4] = (byte)(l >> 24);\n        x[i+5] = (byte)(l >> 16);\n        x[i+6] = (byte)(l >>  8);\n        x[i+7] = (byte)l;\n    }\n\n    private static int[] jsK = new int[]{\n            0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd,\n            0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc,\n            0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,\n            0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118,\n            0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe,\n            0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,\n            0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1,\n            0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694,\n            0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,\n            0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65,\n            0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483,\n            0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,\n            0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210,\n            0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4,\n            0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,\n            0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70,\n            0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926,\n            0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,\n            0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8,\n            0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b,\n            0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,\n            0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30,\n            0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910,\n            0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,\n            0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53,\n            0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8,\n            0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,\n            0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3,\n            0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60,\n            0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,\n            0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9,\n            0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b,\n            0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,\n            0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178,\n            0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6,\n            0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,\n            0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493,\n            0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c,\n            0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,\n            0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817\n    };\n\n    private static int crypto_hashblocks_hl(int[] hh, int[] hl, byte[] m, int n) {\n        int[] wh = new int[16], wl = new int[16];\n        int bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7,\n                bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7,\n                th, tl, i, j, h, l, a, b, c, d;\n\n        int ah0 = hh[0],\n                ah1 = hh[1],\n                ah2 = hh[2],\n                ah3 = hh[3],\n                ah4 = hh[4],\n                ah5 = hh[5],\n                ah6 = hh[6],\n                ah7 = hh[7],\n\n                al0 = hl[0],\n                al1 = hl[1],\n                al2 = hl[2],\n                al3 = hl[3],\n                al4 = hl[4],\n                al5 = hl[5],\n                al6 = hl[6],\n                al7 = hl[7];\n\n        int pos = 0;\n        while (n >= 128) {\n            for (i = 0; i < 16; i++) {\n                j = 8 * i + pos;\n                wh[i] = ((m[j+0] & 0xff) << 24) | ((m[j+1] & 0xff) << 16) | ((m[j+2] & 0xff) << 8) | (m[j+3] & 0xff);\n                wl[i] = ((m[j+4] & 0xff) << 24) | ((m[j+5] & 0xff) << 16) | ((m[j+6] & 0xff) << 8) | (m[j+7] & 0xff);\n            }\n            for (i = 0; i < 80; i++) {\n                bh0 = ah0;\n                bh1 = ah1;\n                bh2 = ah2;\n                bh3 = ah3;\n                bh4 = ah4;\n                bh5 = ah5;\n                bh6 = ah6;\n                bh7 = ah7;\n\n                bl0 = al0;\n                bl1 = al1;\n                bl2 = al2;\n                bl3 = al3;\n                bl4 = al4;\n                bl5 = al5;\n                bl6 = al6;\n                bl7 = al7;\n\n                // add\n                h = ah7;\n                l = al7;\n\n                a = l & 0xffff; b = l >>> 16;\n                c = h & 0xffff; d = h >>> 16;\n\n                // Sigma1\n                h = ((ah4 >>> 14) | (al4 << (32-14))) ^ ((ah4 >>> 18) | (al4 << (32-18))) ^ ((al4 >>> (41-32)) | (ah4 << (32-(41-32))));\n                l = ((al4 >>> 14) | (ah4 << (32-14))) ^ ((al4 >>> 18) | (ah4 << (32-18))) ^ ((ah4 >>> (41-32)) | (al4 << (32-(41-32))));\n\n                a += l & 0xffff; b += l >>> 16;\n                c += h & 0xffff; d += h >>> 16;\n\n                // Ch\n                h = (ah4 & ah5) ^ (~ah4 & ah6);\n                l = (al4 & al5) ^ (~al4 & al6);\n\n                a += l & 0xffff; b += l >>> 16;\n                c += h & 0xffff; d += h >>> 16;\n\n                // K\n                h = jsK[i*2];\n                l = jsK[i*2+1];\n\n                a += l & 0xffff; b += l >>> 16;\n                c += h & 0xffff; d += h >>> 16;\n\n                // w\n                h = wh[i%16];\n                l = wl[i%16];\n\n                a += l & 0xffff; b += l >>> 16;\n                c += h & 0xffff; d += h >>> 16;\n\n                b += a >>> 16;\n                c += b >>> 16;\n                d += c >>> 16;\n\n                th = c & 0xffff | d << 16;\n                tl = a & 0xffff | b << 16;\n\n                // add\n                h = th;\n                l = tl;\n\n                a = l & 0xffff; b = l >>> 16;\n                c = h & 0xffff; d = h >>> 16;\n\n                // Sigma0\n                h = ((ah0 >>> 28) | (al0 << (32-28))) ^ ((al0 >>> (34-32)) | (ah0 << (32-(34-32)))) ^ ((al0 >>> (39-32)) | (ah0 << (32-(39-32))));\n                l = ((al0 >>> 28) | (ah0 << (32-28))) ^ ((ah0 >>> (34-32)) | (al0 << (32-(34-32)))) ^ ((ah0 >>> (39-32)) | (al0 << (32-(39-32))));\n\n                a += l & 0xffff; b += l >>> 16;\n                c += h & 0xffff; d += h >>> 16;\n\n                // Maj\n                h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2);\n                l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2);\n\n                a += l & 0xffff; b += l >>> 16;\n                c += h & 0xffff; d += h >>> 16;\n\n                b += a >>> 16;\n                c += b >>> 16;\n                d += c >>> 16;\n\n                bh7 = (c & 0xffff) | (d << 16);\n                bl7 = (a & 0xffff) | (b << 16);\n\n                // add\n                h = bh3;\n                l = bl3;\n\n                a = l & 0xffff; b = l >>> 16;\n                c = h & 0xffff; d = h >>> 16;\n\n                h = th;\n                l = tl;\n\n                a += l & 0xffff; b += l >>> 16;\n                c += h & 0xffff; d += h >>> 16;\n\n                b += a >>> 16;\n                c += b >>> 16;\n                d += c >>> 16;\n\n                bh3 = (c & 0xffff) | (d << 16);\n                bl3 = (a & 0xffff) | (b << 16);\n\n                ah1 = bh0;\n                ah2 = bh1;\n                ah3 = bh2;\n                ah4 = bh3;\n                ah5 = bh4;\n                ah6 = bh5;\n                ah7 = bh6;\n                ah0 = bh7;\n\n                al1 = bl0;\n                al2 = bl1;\n                al3 = bl2;\n                al4 = bl3;\n                al5 = bl4;\n                al6 = bl5;\n                al7 = bl6;\n                al0 = bl7;\n\n                if (i%16 == 15) {\n                    for (j = 0; j < 16; j++) {\n                        // add\n                        h = wh[j];\n                        l = wl[j];\n\n                        a = l & 0xffff; b = l >>> 16;\n                        c = h & 0xffff; d = h >>> 16;\n\n                        h = wh[(j+9)%16];\n                        l = wl[(j+9)%16];\n\n                        a += l & 0xffff; b += l >>> 16;\n                        c += h & 0xffff; d += h >>> 16;\n\n                        // sigma0\n                        th = wh[(j+1)%16];\n                        tl = wl[(j+1)%16];\n                        h = ((th >>> 1) | (tl << (32-1))) ^ ((th >>> 8) | (tl << (32-8))) ^ (th >>> 7);\n                        l = ((tl >>> 1) | (th << (32-1))) ^ ((tl >>> 8) | (th << (32-8))) ^ ((tl >>> 7) | (th << (32-7)));\n\n                        a += l & 0xffff; b += l >>> 16;\n                        c += h & 0xffff; d += h >>> 16;\n\n                        // sigma1\n                        th = wh[(j+14)%16];\n                        tl = wl[(j+14)%16];\n                        h = ((th >>> 19) | (tl << (32-19))) ^ ((tl >>> (61-32)) | (th << (32-(61-32)))) ^ (th >>> 6);\n                        l = ((tl >>> 19) | (th << (32-19))) ^ ((th >>> (61-32)) | (tl << (32-(61-32)))) ^ ((tl >>> 6) | (th << (32-6)));\n\n                        a += l & 0xffff; b += l >>> 16;\n                        c += h & 0xffff; d += h >>> 16;\n\n                        b += a >>> 16;\n                        c += b >>> 16;\n                        d += c >>> 16;\n\n                        wh[j] = (c & 0xffff) | (d << 16);\n                        wl[j] = (a & 0xffff) | (b << 16);\n                    }\n                }\n            }\n\n            // add\n            h = ah0;\n            l = al0;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[0];\n            l = hl[0];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[0] = ah0 = (c & 0xffff) | (d << 16);\n            hl[0] = al0 = (a & 0xffff) | (b << 16);\n\n            h = ah1;\n            l = al1;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[1];\n            l = hl[1];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[1] = ah1 = (c & 0xffff) | (d << 16);\n            hl[1] = al1 = (a & 0xffff) | (b << 16);\n\n            h = ah2;\n            l = al2;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[2];\n            l = hl[2];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[2] = ah2 = (c & 0xffff) | (d << 16);\n            hl[2] = al2 = (a & 0xffff) | (b << 16);\n\n            h = ah3;\n            l = al3;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[3];\n            l = hl[3];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[3] = ah3 = (c & 0xffff) | (d << 16);\n            hl[3] = al3 = (a & 0xffff) | (b << 16);\n\n            h = ah4;\n            l = al4;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[4];\n            l = hl[4];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[4] = ah4 = (c & 0xffff) | (d << 16);\n            hl[4] = al4 = (a & 0xffff) | (b << 16);\n\n            h = ah5;\n            l = al5;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[5];\n            l = hl[5];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[5] = ah5 = (c & 0xffff) | (d << 16);\n            hl[5] = al5 = (a & 0xffff) | (b << 16);\n\n            h = ah6;\n            l = al6;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[6];\n            l = hl[6];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[6] = ah6 = (c & 0xffff) | (d << 16);\n            hl[6] = al6 = (a & 0xffff) | (b << 16);\n\n            h = ah7;\n            l = al7;\n\n            a = l & 0xffff; b = l >>> 16;\n            c = h & 0xffff; d = h >>> 16;\n\n            h = hh[7];\n            l = hl[7];\n\n            a += l & 0xffff; b += l >>> 16;\n            c += h & 0xffff; d += h >>> 16;\n\n            b += a >>> 16;\n            c += b >>> 16;\n            d += c >>> 16;\n\n            hh[7] = ah7 = (c & 0xffff) | (d << 16);\n            hl[7] = al7 = (a & 0xffff) | (b << 16);\n\n            pos += 128;\n            n -= 128;\n        }\n\n        return n;\n    }\n\n    private static void add(long[][] /*gf*/ p/*[4]*/,long[][] /*gf*/ q/*[4]*/)\n    {\n        long[] /*gf*/ a=new long[GF_LEN],b=new long[GF_LEN],c=new long[GF_LEN],\n                d=new long[GF_LEN],t=new long[GF_LEN],e=new long[GF_LEN],\n                f=new long[GF_LEN],g=new long[GF_LEN],h=new long[GF_LEN];\n\n        Z(a, p[1], p[0]);\n        Z(t, q[1], q[0]);\n        M(a, 0, a, 0, t, 0);\n        A(b, p[0], p[1]);\n        A(t, q[0], q[1]);\n        M(b, 0, b, 0, t, 0);\n        M(c, 0, p[3], 0, q[3], 0);\n        M(c, 0, c, 0, D2, 0);\n        M(d, 0, p[2], 0, q[2], 0);\n        A(d, d, d);\n        Z(e, b, a);\n        Z(f, d, c);\n        A(g, d, c);\n        A(h, b, a);\n\n        M(p[0], 0, e, 0, f, 0);\n        M(p[1], 0, h, 0, g, 0);\n        M(p[2], 0, g, 0, f, 0);\n        M(p[3], 0, e, 0, h, 0);\n    }\n\n    private static void cswap(long[][] /*gf*/ p/*[4]*/,long[][] /*gf*/ q/*[4]*/,byte b)\n    {\n        int i;\n        for(i=0; i < 4; i++)\n        sel25519(p[i],q[i],b & 0xff);\n    }\n\n    private static void pack(byte[] r,long[][] /*gf*/ p/*[4]*/)\n    {\n        long[] /*gf*/ tx = new long[GF_LEN], ty = new long[GF_LEN], zi = new long[GF_LEN];\n        inv25519(zi, 0, p[2], 0);\n        M(tx, 0, p[0], 0, zi, 0);\n        M(ty, 0, p[1], 0, zi, 0);\n        pack25519(r, ty, 0);\n        r[31] ^= par25519(tx) << 7;\n    }\n\n    private static void scalarmult(long[][] /*gf*/ p/*[4]*/,long[][] /*gf*/ q/*[4]*/,byte[] s, int sOff)\n    {\n        int i;\n        set25519(p[0], gf0);\n        set25519(p[1], gf1);\n        set25519(p[2], gf1);\n        set25519(p[3], gf0);\n        for (i = 255;i >= 0;--i) {\n            byte b = (byte)(( (0xff & s[sOff + i/8]) >> (i&7))&1);\n            cswap(p,q,b);\n            add(q,p);\n            add(p,p);\n            cswap(p,q,b);\n        }\n    }\n\n    private static void scalarbase(long[][] /*gf*/ p/*[4]*/,byte[] s,  int sOff)\n    {\n        long[][] /*gf*/ q = new long[4][16];\n        set25519(q[0],X);\n        set25519(q[1],Y);\n        set25519(q[2],gf1);\n        M(q[3], 0, X, 0, Y, 0);\n        scalarmult(p,q,s, sOff);\n    }\n\n    private static long[] L = {0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,\n            0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,\n            0, 0, 0, 0, 0, 0, 0, 0,\n            0, 0, 0, 0, 0, 0, 0, 0x10};\n\n    private static void modL(byte[] r, int rOff, long[] x/*[64]*/)\n    {\n        long carry;\n        int i,j;\n        for (i = 63;i >= 32;--i) {\n            carry = 0;\n            for (j = i - 32;j < i - 12;++j) {\n                x[j] += carry - 16 * x[i] * L[j - (i - 32)];\n                carry = (x[j] + 128) >> 8;\n                x[j] -= carry << 8;\n            }\n            x[j] += carry;\n            x[i] = 0;\n        }\n        carry = 0;\n        for (j=0;j < 32;++j){\n        x[j] += carry - (x[31] >> 4) * L[j];\n        carry = x[j] >> 8;\n        x[j] &= 255;\n    }\n        for (j=0;j < 32;++j)x[j] -= carry * L[j];\n        for (i=0;i < 32;++i){\n        x[i+1] += x[i] >> 8;\n        r[rOff + i] = (byte)(x[i] & 255);\n    }\n    }\n\n    private static void reduce(byte[] r)\n    {\n        long[] x = new long[64];\n        for (int i=0;i < 64; i++) x[i] = 0xff & r[i];\n        for (int i=0;i < 64;++i)r[i] = 0;\n        modL(r, 0, x);\n    }\n\n    private static int crypto_sign(byte[] sm, byte[] m,int n,byte[] sk)\n    {\n        byte[] d = new byte[64],h = new byte[64],r = new byte[64];\n        long[] x = new long[64];\n        long[][] /*gf*/ p/*[4]*/ = new long[4][GF_LEN];\n\n        crypto_hash(d, sk, 32);\n        d[0] &= 248;\n        d[31] &= 127;\n        d[31] |= 64;\n\n//        smlen[0] = n+64;\n        for (int i=0;i < n;++i)sm[64 + i] = m[i];\n        for (int i=0;i < 32;++i)sm[32 + i] = d[32 + i];\n        crypto_hash(r, Arrays.copyOfRange(sm, 32, sm.length), n + 32);\n        reduce(r);\n        scalarbase(p, r, 0);\n        pack(sm, p);\n\n        for (int i=0;i < 32;++i)sm[i+32] = sk[i+32];\n        crypto_hash(h, sm, n + 64);\n        reduce(h);\n\n        for (int i=0;i < 64;++i) x[i] = 0;\n        for (int i=0;i < 32; ++i) x[i] = 0xff & r[i];\n        for (int i=0;i < 32; ++i) for(int j=0; j < 32; ++j) x[i+j] += (0xff & h[i]) * (0xff & d[j]);\n        modL(sm, 32,x);\n\n        return 0;\n    }\n\n    private static int unpackneg(long[][] /*gf*/ r/*[4]*/,byte[] p/*[32]*/)\n    {\n        long[] /*gf*/ t = new long[GF_LEN], chk = new long[GF_LEN], num = new long[GF_LEN], den = new long[GF_LEN],\n                den2 = new long[GF_LEN], den4 = new long[GF_LEN], den6 = new long[GF_LEN];\n        set25519(r[2],gf1);\n        unpack25519(r[1],p);\n        S(num,r[1]);\n        M(den, 0, num, 0, D, 0);\n        Z(num,num,r[2]);\n        A(den,r[2],den);\n\n        S(den2,den);\n        S(den4,den2);\n        M(den6, 0, den4, 0, den2, 0);\n        M(t, 0, den6, 0, num, 0);\n        M(t, 0, t, 0, den, 0);\n\n        pow2523(t,t);\n        M(t, 0, t, 0, num, 0);\n        M(t, 0, t, 0, den, 0);\n        M(t, 0, t, 0, den, 0);\n        M(r[0], 0, t, 0, den, 0);\n\n        S(chk,r[0]);\n        M(chk, 0, chk, 0, den, 0);\n        if (neq25519(chk, num) != 0) M(r[0], 0, r[0], 0, I, 0);\n\n        S(chk,r[0]);\n        M(chk, 0, chk, 0, den, 0);\n        if (neq25519(chk, num) != 0) return -1;\n\n        if (par25519(r[0]) == ( (0xff & p[31]) >> 7)) Z(r[0],gf0,r[0]);\n\n        M(r[3], 0, r[0], 0, r[1], 0);\n        return 0;\n    }\n\n    private static int crypto_sign_open(byte[] m, byte[] sm, int n, byte[] pk)\n    {\n        int i;\n        byte[] t = new byte[32],h = new byte[64];\n        long[][] /*gf*/ p = new long[4][GF_LEN],q = new long[4][GF_LEN];\n\n//        mlen[0] = -1;\n        if (n < 64) return -1;\n\n        if (unpackneg(q,pk) != 0) return -1;\n\n        for (i=0;i < n;++i) m[i] = sm[i];\n        for (i=0;i < 32;++i) m[i+32] = pk[i];\n        crypto_hash(h, m, n);\n        reduce(h);\n        scalarmult(p, q, h, 0);\n\n        scalarbase(q, sm, 32);\n        add(p, q);\n        pack(t, p);\n\n        n -= 64;\n        if (crypto_verify_32(sm, t) != 0) {\n            for (i=0;i < n;++i)m[i] = 0;\n            return -1;\n        }\n\n        for (i=0;i < n;++i)m[64 + i] = sm[i + 64];\n//        mlen[0] = n;\n        return 0;\n    }\n\n    private static final Random prng = getSecureRandom();\n\n    private static SecureRandom getSecureRandom() {\n        return new SecureRandom();\n    }\n\n    private static void randombytes(byte[] b, int len) {\n        byte[] r = new byte[len];\n        prng.nextBytes(r);\n        System.arraycopy(r, 0, b, 0, len);\n    }\n\n    public static void randomBytes(byte[] b, int offset, int len) {\n        byte[] r = new byte[len];\n        prng.nextBytes(r);\n        System.arraycopy(r, 0, b, offset, len);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/curve25519/Curve25519Java.java",
    "content": "package peergos.server.crypto.asymmetric.curve25519;\n\nimport peergos.server.crypto.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\n\npublic class Curve25519Java implements Curve25519 {\n\n    @Override\n    public byte[] crypto_box_open(byte[] cipher, byte[] nonce, byte[] theirPublicBoxingKey, byte[] secretBoxingKey) {\n        return TweetNaCl.crypto_box_open(cipher, nonce, theirPublicBoxingKey, secretBoxingKey);\n    }\n\n    @Override\n    public byte[] crypto_box(byte[] message, byte[] nonce, byte[] theirPublicBoxingKey, byte[] ourSecretBoxingKey) {\n        return TweetNaCl.crypto_box(message, nonce, theirPublicBoxingKey, ourSecretBoxingKey);\n    }\n\n    @Override\n    public void crypto_box_keypair(byte[] pk, byte[] sk) {\n        TweetNaCl.crypto_box_keypair(pk, sk, true);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/curve25519/Ed25519Java.java",
    "content": "package peergos.server.crypto.asymmetric.curve25519;\n\nimport peergos.server.crypto.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.util.*;\n\nimport java.util.concurrent.*;\n\npublic class Ed25519Java implements Ed25519 {\n    @Override\n    public CompletableFuture<byte[]> crypto_sign_open(byte[] signed, byte[] publicSigningKey) {\n        return Futures.of(TweetNaCl.crypto_sign_open(signed, publicSigningKey));\n    }\n\n    @Override\n    public CompletableFuture<byte[]> crypto_sign(byte[] message, byte[] secretSigningKey) {\n        return Futures.of(TweetNaCl.crypto_sign(message, secretSigningKey));\n    }\n\n    @Override\n    public void crypto_sign_keypair(byte[] pk, byte[] sk) {\n        TweetNaCl.crypto_sign_keypair(pk, sk, true);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/CryptoUtils.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem;\n\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.Arrays;\n\npublic class CryptoUtils {\n\n    public static final int[] INT_BIT_MASKS = new int[]{\n            0x0, 0x1, 0x3, 0x7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF\n    };\n\n    public static int pow(int base, int exponent) {\n\n        int res = 1;\n        while (exponent > 0)\n        {\n            if ((exponent & 1) == 1)\n                res = res * base;\n\n            // Exponent must be even now\n            exponent = exponent >> 1;\n            base = base * base;\n        }\n        return res;\n    }\n\n    public static int mod(int val, int base) {\n        return (val % base + base) % base;\n    }\n\n    public static long bytesToLong(ByteOrder order, byte[] bytes, int offset) {\n\n        byte[] modBytes = new byte[] {\n                bytes[offset], bytes[offset+1], bytes[offset+2], bytes[offset+3],\n                bytes[offset+4], bytes[offset+5], bytes[offset+6], bytes[offset+7]\n        };\n\n        ByteBuffer buffer = ByteBuffer.wrap(modBytes);\n        buffer.order(order);\n        return buffer.getLong();\n\n    }\n\n    public static void zero(byte[] toZero) {\n        Arrays.fill(toZero, (byte) 0);\n    }\n\n    public static void zero(int[] toZero) {\n        Arrays.fill(toZero, (byte) 0);\n    }\n\n    public static void zero(int[][] toZero) {\n        for (int[] ints: toZero) {\n            Arrays.fill(ints, (byte) 0);\n        }\n    }\n\n    public static void zero(int[][][] toZero) {\n        for (int[][] ints : toZero) {\n            for (int[] anInt : ints) {\n                Arrays.fill(anInt, (byte) 0);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/JavaMlkem.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.FIPS203;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.MimicloneFIPS203;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.EncapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem.MLKEMDecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem.MLKEMEncapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.MLKEMCipherText;\nimport peergos.shared.crypto.asymmetric.mlkem.Mlkem;\nimport peergos.shared.crypto.asymmetric.mlkem.MlkemKeyPair;\nimport peergos.shared.crypto.asymmetric.mlkem.MlkemPublicKey;\nimport peergos.shared.crypto.asymmetric.mlkem.MlkemSecretKey;\nimport peergos.shared.util.Futures;\n\nimport java.util.concurrent.CompletableFuture;\n\npublic class JavaMlkem implements Mlkem {\n\n    private static FIPS203 fips203 = MimicloneFIPS203.create(ParameterSet.ML_KEM_1024);\n\n    @Override\n    public CompletableFuture<MlkemKeyPair> generateKeyPair() {\n        KeyPair keyPair = fips203.generateKeyPair();\n        MlkemPublicKey publicKey = new MlkemPublicKey(keyPair.getEncapsulationKey().getBytes(), this);\n        MlkemSecretKey secretKey = new MlkemSecretKey(keyPair.getDecapsulationKey().getBytes(), this);\n        return Futures.of(new MlkemKeyPair(publicKey, secretKey));\n    }\n\n    @Override\n    public CompletableFuture<Encapsulation> encapsulate(byte[] publicKeyBytes) {\n        EncapsulationKey publicKey = MLKEMEncapsulationKey.create(publicKeyBytes);\n        peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation encapsulated = fips203.encapsulate(publicKey);\n        return Futures.of(new Encapsulation(encapsulated.getSharedSecretKey().getBytes(), encapsulated.getCipherText().getBytes()));\n    }\n\n    /**\n     *\n     * @param cipherTextBytes\n     * @return sharedSecret\n     */\n    @Override\n    public CompletableFuture<byte[]> decapsulate(byte[] cipherTextBytes, byte[] secretKeyBytes) {\n        CipherText cipherText = MLKEMCipherText.create(cipherTextBytes);\n        DecapsulationKey secretKey = MLKEMDecapsulationKey.create(secretKeyBytes);\n        return Futures.of(fips203.decapsulate(secretKey, cipherText).getBytes());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/MlkemSecureRandom.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.FIPS203Exception;\n\nimport java.security.DrbgParameters;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.security.SecureRandomParameters;\n\npublic class MlkemSecureRandom {\n    // Secure RBG algorithm set name\n    private static final String SECURE_RBG_ALGO = \"DRBG\";\n\n    public static SecureRandom getSecureRandom(int minSecurityStrength) {\n        try {\n            try {\n                // Create secure random parameters\n                SecureRandomParameters secureParams = DrbgParameters.instantiation(\n                        minSecurityStrength,\n                        DrbgParameters.Capability.PR_AND_RESEED,\n                        null);\n\n                // Create sure random instance\n                return SecureRandom.getInstance(SECURE_RBG_ALGO, secureParams);\n            } catch (Exception e) {\n                return SecureRandom.getInstanceStrong();\n            }\n        } catch (NoSuchAlgorithmException e) {\n            throw new FIPS203Exception(e.getMessage());\n        } catch (Throwable e) {\n            // Android < 15\n            return new SecureRandom();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/Constants.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202;\n\npublic class Constants {\n\tpublic static final String SHAKE128_STREAM_CIPHER = \"SHAKE128\";\n\tpublic static final String SHAKE256_STREAM_CIPHER = \"SHAKE256\";\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/FIPS202.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips202;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.MimicloneKeccak;\n\npublic interface FIPS202 {\n\n    MimicloneKeccak keccakPermutation(MimicloneKeccak.Permutation permutation);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/MimicloneFIPS202.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips202;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.MimicloneKeccak;\n\npublic class MimicloneFIPS202 implements FIPS202 {\n\n    @Override\n    public MimicloneKeccak keccakPermutation(MimicloneKeccak.Permutation permutation) {\n\n        return new MimicloneKeccak(permutation);\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/MimicloneKeccak.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips202.keccak;\n\nimport java.util.BitSet;\n\n/**\n * There are 7 permutation functions in the Keccak family.\n * <br/>\n * They are:\n * <ul>\n * <li>Keccak-f[25] (b=25, l=0, w=1)</li>\n * <li>Keccak-f[50] (b=50, l=1, w=2)</li>\n * <li>Keccak-f[100] (b=100, l=2, w=4)</li>\n * <li>Keccak-f[200] (b=200, l=3, w=8)</li>\n * <li>Keccak-f[400] (b=400, l=4, w=16)</li>\n * <li>Keccak-f[800] (b=800, l=5, w=32)</li>\n * <li>Keccak-f[1600] (b=1600, l=6, w=64)</li>\n * </ul>\n *\n */\npublic class MimicloneKeccak {\n\n    public enum Permutation {\n        KECCAK_F25,\n        KECCAK_F50,\n        KECCAK_F100,\n        KECCAK_F200,\n        KECCAK_F400,\n        KECCAK_F800,\n        KECCAK_F1600;\n\n        private final int b, l, w, n;\n\n        private Permutation() {\n            l = ordinal();\n            w = 2^l;\n            b = 25 * w;\n            n = 12 + 2*l;\n        }\n    }\n\n    private static final long[] ROUND_CONSTANTS = {\n            0x0000000000000001L, // RC[0]\n            0x0000000000008082L, // RC[1]\n            0x800000000000808AL, // RC[2]\n            0x8000000080008000L, // RC[3]\n            0x000000000000808BL, // RC[4]\n            0x0000000080000001L, // RC[5]\n            0x8000000080008081L, // RC[6]\n            0x8000000000008009L, // RC[7]\n            0x000000000000008AL, // RC[8]\n            0x0000000000000088L, // RC[9]\n            0x0000000080008009L, // RC[10]\n            0x000000008000000AL, // RC[11]\n            0x000000008000808BL, // RC[12]\n            0x800000000000008BL, // RC[13]\n            0x8000000000008089L, // RC[14]\n            0x8000000000008003L, // RC[15]\n            0x8000000000008002L, // RC[16]\n            0x8000000000000080L, // RC[17]\n            0x000000000000800AL, // RC[18]\n            0x800000008000000AL, // RC[19]\n            0x8000000080008081L, // RC[20]\n            0x8000000000008080L, // RC[21]\n            0x0000000080000001L, // RC[22]\n            0x8000000080008008L, // RC[23]\n    };\n\n    private static final int[][] ROTATION_OFFSETS = {\n            {0, 36, 3, 41, 18},\n            {1, 44, 10, 45, 2},\n            {62, 6, 43, 15, 61},\n            {28, 55, 25, 21, 56},\n            {27, 20, 39, 8, 14}\n    };\n\n    private final Permutation permutation;\n\n    public MimicloneKeccak(Permutation permutation) {\n        this.permutation = permutation;\n    }\n\n    int mod(int val, int base) {\n        return (val % base + base) % base;\n    }\n\n    long convert(BitSet bits) {\n        long value = 0L;\n        for (int i = 0; i < bits.length(); ++i) {\n            value += bits.get(i) ? (1L << i) : 0L;\n        }\n        return value;\n    }\n\n    public BitSet[][] permute(BitSet[][] a) {\n\n        // At the moment we are expecting the state space to be passed in directly, which is not how FIPS202 works\n\n        long[][] inputData = new long[5][5];\n        for (int i = 0; i < 5; i++) {\n            for (int j = 0; j < 5; j++) {\n                inputData[i][j] = convert(a[i][j]);\n            }\n        }\n\n        System.out.println(\"INPUT DATA\");\n        for (int i = 0; i < 5; i++) {\n            StringBuilder builder = new StringBuilder();\n            for (int j = 0; j < 5; j++) {\n                builder.append(String.format(\"%016X \", inputData[j][i]));\n            }\n            System.out.println(builder.toString());\n        }\n\n        // Execute the rounds\n        for (int i = 0; i < permutation.n; i++) {\n            inputData = round(inputData, ROUND_CONSTANTS[i]);\n        }\n\n        // Convert result to array of BitSet\n        BitSet[][] outputData = new BitSet[5][5];\n        for (int i = 0; i < 5; i++) {\n            for (int j = 0; j < 5; j++) {\n                outputData[i][j] = BitSet.valueOf(new long[]{inputData[i][j]});\n            }\n        }\n\n        return outputData;\n    }\n\n    private void printState(String label, long[][] data) {\n        System.out.printf(\"After %s%n\", label);\n        for (int y = 0; y < 5; y++) {\n            StringBuilder builder = new StringBuilder();\n            for (int x = 0; x < 5; x++) {\n                builder.append(String.format(\" %016X\", data[x][y]));\n            }\n            System.out.println(builder.toString());\n        }\n        System.out.println();\n    }\n\n    /**\n     * Implements Algorithm 1 (Theta Step) of the NIST FIPS 202 Standard.\n     * This version ignores the bit size as we are only implementing Keccak-f[1600] in support\n     * of the SHAKE256 algorithm needed as an XOF for FIPS 203.  We use {@code long} as the\n     * carrier for bit information since in Java this will always be a 64-bit type.\n     *\n     * @param state A 5x5 array of {@code long} values treated as 64 indexed bits.\n     * @return A reference to the modified state space after XORing with the parities of two columns.\n     */\n    long[][] theta(long[][] state) {\n\n        // Allocate result state\n        long[][] statePrime = new long[5][5];\n\n        // Step 1\n        // First we iterate through all the sheets (2d xz arrays) and XOR together all 5 lanes\n        // The intermediate result C is an array of length 5 that has collapsed each sheet into a single lane\n        long[] c = new long[5];\n        for (int x = 0; x < 5; x++) {\n            c[x] = state[x][0] ^ state[x][1] ^ state[x][2] ^ state[x][3] ^ state[x][4];\n        }\n\n        // Step 2\n        // Next we iterate across the 5 intermediate C lanes and XOR together the lanes to their left and right\n        // (with the right lane bit shifted along the z-axis) to produce a new set of 5 intermediate D lanes\n        // In this case the {@code rotateLeft} is the same thing as taking {@code z-1 mod w} as z index if\n        // c were composed of indexed bits rather than a long.\n        long[] d = new long[5];\n        for (int x = 0; x < 5; x++) {\n            d[x] = c[mod(x-1, 5)] ^ Long.rotateLeft(c[mod(x+1, 5)], 1);\n        }\n\n        // Step 3\n        // XOR each together each lane in a sheet of the state space with each lane of D.  The resulting\n        // state space is referred to as A prime but we do not allocate new memory for it for efficiency.\n        for (int x = 0; x < 5; x++) {\n            for (int y = 0; y < 5; y++) {\n                statePrime[x][y] = state[x][y] ^ d[x];\n            }\n        }\n\n        // Return the new state\n        return statePrime;\n\n    }\n\n    /**\n     * Implements Algorithm 2 (Rho Step) of the NIST FIPS 202 Standard.\n     * This version ignores the bit size as we are only implementing Keccak-f[1600] in support\n     * of the SHAKE256 algorithm needed as an XOF for FIPS 203.  We use {@code long} as the\n     * carrier for bit information since in Java this will always be a 64-bit type.\n     *\n     * Rho takes every lane except for the notionally central lane at (0,0) and shifts the\n     * bits by a pre-determined amount specified in the standard.\n     *\n     * Pi re-organized the lanes by rotating them around the central lane with some variation.\n     *\n     * These two steps are combined for efficiency.  The bit shift is pre-calculated for each\n     * lane and assignments to the modified state space perform the rotation at the same time.\n     *\n     * @param state\n     * @return\n     */\n    long[][] rhopi(long[][] state) {\n\n        // Allocate prime state\n        long[][] statePrime = new long[5][5];\n\n        // Step 1\n        // Copy the (0,0) lane of the original state into the prime state\n        statePrime[0][0] = state[0][0]; // Copy by value because this is a long\n\n        // Rotate the bits in each lane corresponding to pre-calculated values in the spec.\n        for (int x = 0; x < 5; x++) {\n            for (int y = 0; y < 5; y++) {\n                statePrime[y][mod(2*x + 3*y, 5)]\n                        = Long.rotateLeft(state[x][y], ROTATION_OFFSETS[x][y]);\n            }\n        }\n\n        return statePrime;\n    }\n\n    long[][] chi(long[][] state) {\n        long[][] statePrime = new long[5][5];\n\n        for (int x = 0; x < 5; x++) {\n            for (int y = 0; y < 5; y++) {\n                statePrime[x][y] = state[x][y] ^ ((~state[mod(x+1, 5)][y]) & state[mod(x+2, 5)][y]);\n            }\n        }\n\n        return statePrime;\n    }\n\n    public long[][] round(long[][] a, long rc) {\n\n        // STEP 1 (Theta)\n        a = theta(a);\n\n        // STEP 2/3 (Rho / Pi)\n        long[][] b = rhopi(a);\n\n        // STEP 4 (Chi)\n        long[][] c = chi(b);\n\n        // STEP 5 (Iota)\n        a[0][0] = c[0][0] ^ rc;\n\n        return a;\n\n    }\n\n    String sponge(String message, long digestLength) {\n        return \"\";\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/core/AbstractKeccakMessageDigest.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core;\n\nimport java.security.MessageDigest;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.XOFParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitInputStream;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitOutputStream;\n\npublic abstract class AbstractKeccakMessageDigest extends MessageDigest {\n\n\tKeccakSponge keccakSponge;\n\tBitOutputStream absorbStream;\n\tint digestLength;\n\n\t/**\n\t * Security level in bits is min(capacity/2,digestLength*8).\n\t *\n\t * @param params An XOFParameterSet instance with a positive digest length\n\t */\n\tpublic AbstractKeccakMessageDigest(XOFParameterSet params)\n\t{\n\t\tsuper(params.getAlgorithm());\n\t\tthis.keccakSponge = new KeccakSponge(params.getCapacityInBits(), params.getDomainPadding(), params.getDomainPaddingBitLength());\n\n\t\tthis.absorbStream = keccakSponge.getAbsorbStream();\n\t\tthis.digestLength = params.getDigestLength();\n\t}\n\n\t@Override\n\tprotected byte[] engineDigest() {\n\t\tabsorbStream.close();\n\n\t\tbyte[] rv = new byte[digestLength];\n\t\tBitInputStream bis = keccakSponge.getSqueezeStream();\n\t\tbis.read(rv);\n\t\tbis.close();\n\n\t\treturn rv;\n\t}\n\n\t@Override\n\tprotected void engineReset() {\n\t\tkeccakSponge.reset();\n\t}\n\n\tpublic void engineUpdateBits(byte[] bits, long bitOff, long bitLength)\n\t{\n\t\tabsorbStream.writeBits(bits, bitOff, bitLength);\n\t}\n\n\t@Override\n\tprotected void engineUpdate(byte input) {\n\t\tabsorbStream.write(((int) input));\n\t}\n\n\t@Override\n\tprotected void engineUpdate(byte[] input, int offset, int len) {\n\t\tengineUpdateBits(input, ((long) offset)<<3, ((long)len)<<3);\n\t}\n\n\tpublic byte[] getRateBits(int boff, int len)\n\t{\n\t\treturn keccakSponge.getRateBits(boff, len);\n\t}\n\n\tpublic int getRateBits() {\n\t\treturn keccakSponge.getRateBits();\n\t}\n\n\t@Override\n\tprotected int engineGetDigestLength() {\n\t\treturn digestLength;\n\t}\n\n\tpublic KeccakSponge getSponge() {\n\t\treturn keccakSponge;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/core/DuplexRandom.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core;\n\nimport java.io.ByteArrayOutputStream;\nimport java.security.SecureRandom;\nimport java.util.Arrays;\n\n/**\n * A cryptographic random generator based on Keccack-1600\n * and the duplex construction. Supports re-seeding and\n * forward secrecy through forgetting past state.\n *\n * Note that this generator must be synchronized externally\n * if used concurrently from multiple threads.\n *\n */\npublic final class DuplexRandom {\n\tclass ForgettingByteArrayOutputStream extends ByteArrayOutputStream {\n\t\tpublic void forget() {\n\t\t\tArrays.fill(buf, (byte) 0);\n\t\t\treset();\n\t\t}\n\n\t\tpublic byte[] getBuf() {\n\t\t\treturn buf;\n\t\t}\n\t}\n\n\tprivate final int MIN_SEED_LENGTH_BYTES = 16; // 128 bits\n\n\tprivate final Keccak1600 keccak1600;\n\tprivate final int rateBytes;\n\tprivate int pos;\n\n\tprivate boolean seeded;\n\n\tprivate ForgettingByteArrayOutputStream duplexIn = new ForgettingByteArrayOutputStream();\n\n\t// static master seed generator\n\tprivate final static DuplexRandom seedGenerator;\n\n\tstatic {\n\t\t// seed the master seed-generator with 512 bits of random entropy\n\t\tseedGenerator = new DuplexRandom(1085);\n\t\tbyte[] seed = SecureRandom.getSeed(64);\n\t\tseedGenerator.feed(seed, 0, seed.length);\n\t\tArrays.fill(seed, (byte) 0);\n\t\tseed = null;\n\t}\n\n\t/**\n\t * @param capacityInBits This should be approximately 2x the desired\n\t * security level. For efficiency, it should be chosen such that\n\t * we get a multiple of 64-bytes output per permutation. This\n\t * generator used 3 bits of padding so pick capacityInBits = n*64 - 3\n\t * with n between [4, 25]. Higher n gives higher security, but lower\n\t * performance.\n\t */\n\tpublic DuplexRandom(int capacityInBits) {\n\t\tthis.keccak1600 = new Keccak1600(capacityInBits);\n\t\tthis.rateBytes = (keccak1600.getRateBits()-3)>>3;\n\t\tthis.seeded = false;\n\t}\n\n\t/**\n\t * (Re)seed with the supplied seed. If you seed with a static\n\t * value before the first call to getBytes() you will get\n\t * a reproducible random sequence with a security that depends upon\n\t * the security of the seed. Seeds are accumulated and not used\n\t * before you have supplied at least 16 bytes/128 bits.\n\t *\n\t * @param seed Byte array\n\t * @param off Offset in array\n\t * @param len Length in bytes\n\t */\n\tpublic void seed(byte[] seed, int off, int len) {\n\t\tif((duplexIn.size()+len) >= MIN_SEED_LENGTH_BYTES) {\n\t\t\tfeed(duplexIn.getBuf(), 0, duplexIn.size());\n\t\t\tfeed(seed, off, len);\n\t\t\tduplexIn.forget();\n\t\t} else {\n\t\t\tduplexIn.write(seed, off, len);\n\t\t}\n\t}\n\n\t/**\n\t * (Re)seed using the internal seed generator\n\t *\n\t */\n\tpublic void seed() {\n\t\tint seedLength = Math.max((keccak1600.getCapacityBits()>>4) + 1, MIN_SEED_LENGTH_BYTES);\n\t\tbyte[] seed = new byte[seedLength];\n\t\tsynchronized(seedGenerator) {\n\t\t\tseedGenerator.getBytes(seed, 0, seed.length);\n\t\t\tseedGenerator.forget();\n\t\t}\n\t\tfeed(seed, 0, seed.length);\n\t\tArrays.fill(seed, (byte) 0);\n\t\tseed = null;\n\t}\n\n\t/**\n\t * Get seed bytes from the master seed generator\n\t *\n\t * @param buf Byte array\n\t * @param off Offset in array\n\t * @param len Length\n\t */\n\tpublic static void getSeedBytes(byte[] buf, int off, int len) {\n\t\tsynchronized (seedGenerator) {\n\t\t\tseedGenerator.getBytes(buf, off, len);\n\t\t\tseedGenerator.forget();\n\t\t}\n\t}\n\n\t/**\n\t * Generate random bytes\n\t */\n\tpublic void getBytes(byte[] buf, int off, int len) {\n\t\tif(!seeded) {\n\t\t\tseed();\n\t\t}\n\t\twhile(len > 0) {\n\t\t\tint chunk = Math.min(len, rateBytes - pos);\n\t\t\tif(chunk == 0) {\n\t\t\t\t// Output: Append 0-bit and pad\n\t\t\t\tkeccak1600.pad((byte) 0, 1, 0);\n\t\t\t\tkeccak1600.permute();\n\t\t\t\tpos = 0;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tkeccak1600.getBytes(pos, buf, off, chunk);\n\t\t\toff += chunk;\n\t\t\tlen -= chunk;\n\t\t\tpos += chunk;\n\t\t}\n\t}\n\n\t/**\n\t * Feed seed material\n\t *\n\t * @param buf Seed array\n\t * @param off Offset in array\n\t * @param len Length of seed\n\t */\n\tprivate void feed(byte[] buf, int off, int len) {\n\t\tpos = 0;\n\t\twhile(len > 0) {\n\t\t\tint chunk = Math.min(len, rateBytes - pos);\n\t\t\tkeccak1600.setXorBytes(pos, buf, off, chunk);\n\t\t\toff += chunk;\n\t\t\tlen -= chunk;\n\t\t\tpos += chunk;\n\n\t\t\tif(chunk == 0 || len == 0) {\n\t\t\t\t// Consume seed material: Append 1-bit pad and permute\n\t\t\t\tkeccak1600.pad((byte)1, 1, 0);\n\t\t\t\tkeccak1600.permute();\n\t\t\t\tpos = 0;\n\t\t\t}\n\t\t}\n\t\tseeded = true;\n\t}\n\n\t/**\n\t * Forget state providing forward secrecy\n\t */\n\tpublic void forget() {\n\t\tpos = 0;\n\t\tkeccak1600.pad(pos<<3);\n\t\tkeccak1600.permute();\n\n\t\tkeccak1600.zeroBytes(0, rateBytes);\n\t\tkeccak1600.pad(rateBytes<<3);\n\t\tkeccak1600.permute();\n\t}\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/core/Keccak1600.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core;\n\nimport java.util.Arrays;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.KeccakStateUtils.StateOp;\n\n/**\n * Java port of the reference implementation of Keccack-1600 permuation\n * from <a href=\"https://github.com/gvanas/KeccakCodePackage\">HERE</a>\n *\n */\npublic final class Keccak1600 {\n\n\tpublic Keccak1600()\n\t{\n\t\tthis(256, 24);\n\t}\n\n\tpublic Keccak1600(int bitCapacity) {\n\t\tthis(bitCapacity, NR_ROUNDS);\n\t}\n\n\tpublic Keccak1600(int bitCapacity, int rounds) {\n\t\tthis.capacityBits = bitCapacity;\n\t\tthis.rateBits = 1600-bitCapacity;\n\t\tthis.rateBytes = rateBits >> 3;\n\t\tthis.firstRound = NR_ROUNDS - rounds;\n\n\t\tclear();\n\t}\n\n\tbyte byteOp(StateOp stateOp, int stateByteOff, byte in)\n\t{\n\t\treturn KeccakStateUtils.byteOp(stateOp, state, stateByteOff, in);\n\t}\n\n\tvoid bytesOp(StateOp stateOp, int stateByteOff, byte[] out, int outpos, byte[] in, int inpos, int lenBytes)\n\t{\n\t\tKeccakStateUtils.bytesOp(stateOp, state, stateByteOff, out, outpos, in, inpos, lenBytes);\n\t}\n\n\tvoid bitsOp(StateOp stateOp, int stateBitOff, byte[] out, long outpos, byte[] in, long inpos, int lenBits)\n\t{\n\t\tKeccakStateUtils.bitsOp(stateOp, state, stateBitOff, out, outpos, in, inpos, lenBits);\n\t}\n\n\tpublic void validateBytes(int stateByteOff, byte[] buf, int bufByteOff, int lenBytes) {\n\t\tbytesOp(StateOp.VALIDATE, stateByteOff, null, 0, buf, bufByteOff, lenBytes);\n\t}\n\n\tpublic void wrapBytes(int stateByteOff, byte[] outBuf, int outBufOff, byte[] inBuf, int inBufOff, int lenBytes)  {\n\t\tbytesOp(StateOp.WRAP, stateByteOff, outBuf, outBufOff, inBuf, inBufOff, lenBytes);\n\t}\n\n\tpublic void unwrapBytes(int stateByteOff, byte[] outBuf, int outBufOff, byte[] inBuf, int inBufOff, int lenBytes) {\n\t\tbytesOp(StateOp.UNWRAP, stateByteOff, outBuf, outBufOff, inBuf, inBufOff, lenBytes);\n\t}\n\n\tpublic void getBytes(int stateByteOff, byte[] buf, int bufByteOff, int lenBytes) {\n\t\tbytesOp(StateOp.GET, stateByteOff, buf, bufByteOff, null, 0, lenBytes);\n\t}\n\n\tpublic void setXorByte(int stateByteOff, byte val) {\n\t\tbyteOp(StateOp.XOR_IN, stateByteOff, val);\n\t}\n\n\tpublic void setXorBytes(int stateByteOff, byte[] buf, int bufByteOff, int lenBytes) {\n\t\tbytesOp(StateOp.XOR_IN, stateByteOff, null, 0, buf, bufByteOff, lenBytes);\n\t}\n\n\tpublic void zeroBytes(int stateByteOff, int lenBytes) {\n\t\tbytesOp(StateOp.ZERO, stateByteOff, null, 0, null, 0, lenBytes);\n\t}\n\n\tpublic void getBits(int stateBitOff, byte[] buf, long bufBitOff, int lenBits) {\n\t\tbitsOp(StateOp.GET, stateBitOff, buf, bufBitOff, null, 0, lenBits);\n\t}\n\n\tpublic final void setXorBits(int stateBitOff, byte[] buf, long bufBitOff, int lenBits) {\n\t\tbitsOp(StateOp.XOR_IN, stateBitOff, null, 0, buf, bufBitOff, lenBits);\n\t}\n\n\tpublic void zeroBits(int stateBitOff, int lenBits) {\n\t\tbitsOp(StateOp.ZERO, stateBitOff, null, 0, null, 0, lenBits);\n\t}\n\n\tpublic void validateBits(int stateBitOff, byte[] buf, int bufBitOff, int lenBits) {\n\t\tbitsOp(StateOp.VALIDATE, stateBitOff, null, 0, buf, bufBitOff, lenBits);\n\t}\n\n\tpublic void wrapBits(int stateBitOff, byte[] outBuf, int outBufOff, byte[] inBuf, int inBufOff, int lenBits)  {\n\t\tbitsOp(StateOp.WRAP, stateBitOff, outBuf, outBufOff, inBuf, inBufOff, lenBits);\n\t}\n\n\tpublic void unwrapBits(int stateBitOff, byte[] outBuf, int outBufOff, byte[] inBuf, int inBufOff, int lenBits) {\n\t\tbitsOp(StateOp.UNWRAP, stateBitOff, outBuf, outBufOff, inBuf, inBufOff, lenBits);\n\t}\n\n\tpublic int remainingLongs(int longOff) {\n\t\treturn remainingBits(longOff << 6) >> 6;\n\t}\n\n\tpublic int remainingBytes(int byteOff) {\n\t\treturn remainingBits(byteOff << 3) >> 3;\n\t}\n\n\tpublic int remainingBits(int bitOff) {\n\t\treturn rateBits - bitOff;\n\t}\n\n\tpublic void pad(byte domainBits, int domainBitLength, int bitPosition)\n\t{\n\t\tint len = rateBits - bitPosition;\n\n\t\tif(len < 0 || domainBitLength>=7)\n\t\t\tthrow new IndexOutOfBoundsException();\n\n\t\t// add bits for multirate padding\n\t\tdomainBits |= (byte) (1 << domainBitLength);\n\t\t++domainBitLength;\n\n\t\tboolean multirateComplete  = false;\n\t\t// no zeros in multirate padding. add final bit.\n\t\tif(len==domainBitLength+1) {\n\t\t\tdomainBits |= (byte) (1 << domainBitLength);\n\t\t\t++domainBitLength;\n\t\t\tmultirateComplete = true;\n\t\t}\n\n\t\twhile(domainBitLength > 0) {\n\t\t\tint chunk = Math.min(len, domainBitLength);\n\t\t\tif(chunk == 0) {\n\t\t\t\tpermute();\n\t\t\t\tlen = rateBits;\n\t\t\t\tbitPosition = 0;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tKeccakStateUtils.bitsOp(StateOp.XOR_IN, state, bitPosition, domainBits, chunk);\n\n\t\t\tlen -= chunk;\n\t\t\tdomainBits >>= chunk;\n\t\t\tdomainBitLength -= chunk;\n\t\t\tbitPosition += chunk;\n\t\t}\n\t\tif(!multirateComplete) {\n\t\t\tif(len == 0) {\n\t\t\t\tpermute();\n\t\t\t}\n\t\t\tKeccakStateUtils.bitOp(StateOp.XOR_IN, state, rateBits-1, true);\n\t\t}\n\t}\n\n\tpublic void pad(int padBitPosition)\n\t{\n\t\tint len = rateBits - padBitPosition;\n\n\t\tif(len < 0)\n\t\t\tthrow new IndexOutOfBoundsException();\n\n\t\tif(len == 0) {\n\t\t\tpermute();\n\t\t\tpadBitPosition=0;\n\t\t}\n\n\t\tKeccakStateUtils.bitOp(StateOp.XOR_IN, state, padBitPosition, true);\n\n\t\tif(len == 1) {\n\t\t\tpermute();\n\t\t}\n\n\t\tKeccakStateUtils.bitOp(StateOp.XOR_IN, state, rateBits-1, true);\n\t}\n\n\tpublic void permute()\n\t{\n/*\n  for (int i=firstRound; i < NR_ROUNDS; ++i) {\n\t\t\ttheta();\n\t\t\trho();\n\t\t\tpi();\n\t\t\tchi();\n\t\t\tiota(i);\n\t}\n*/\n\t\tlong out0 = state[0];\n\t\tlong out1 = state[1];\n\t\tlong out2 = state[2];\n\t\tlong out3 = state[3];\n\t\tlong out4 = state[4];\n\t\tlong out5 = state[5];\n\t\tlong out6 = state[6];\n\t\tlong out7 = state[7];\n\t\tlong out8 = state[8];\n\t\tlong out9 = state[9];\n\t\tlong out10 = state[10];\n\t\tlong out11 = state[11];\n\t\tlong out12 = state[12];\n\t\tlong out13 = state[13];\n\t\tlong out14 = state[14];\n\t\tlong out15 = state[15];\n\t\tlong out16 = state[16];\n\t\tlong out17 = state[17];\n\t\tlong out18 = state[18];\n\t\tlong out19 = state[19];\n\t\tlong out20 = state[20];\n\t\tlong out21 = state[21];\n\t\tlong out22 = state[22];\n\t\tlong out23 = state[23];\n\t\tlong out24 = state[24];\n\t\tfor (int i=firstRound; i < NR_ROUNDS; ++i) {\n\t\t// Theta\n\t\tlong c0 = out0;\n\t\tlong c1 = out1;\n\t\tlong c2 = out2;\n\t\tlong c3 = out3;\n\t\tlong c4 = out4;\n\t\tc0 ^= out5;\n\t\tc1 ^= out6;\n\t\tc2 ^= out7;\n\t\tc3 ^= out8;\n\t\tc4 ^= out9;\n\t\tc0 ^= out10;\n\t\tc1 ^= out11;\n\t\tc2 ^= out12;\n\t\tc3 ^= out13;\n\t\tc4 ^= out14;\n\t\tc0 ^= out15;\n\t\tc1 ^= out16;\n\t\tc2 ^= out17;\n\t\tc3 ^= out18;\n\t\tc4 ^= out19;\n\t\tc0 ^= out20;\n\t\tc1 ^= out21;\n\t\tc2 ^= out22;\n\t\tc3 ^= out23;\n\t\tc4 ^= out24;\n\t\tlong d0 = Long.rotateLeft(c1, 1) ^ c4;\n\t\tlong d1 = Long.rotateLeft(c2, 1) ^ c0;\n\t\tlong d2 = Long.rotateLeft(c3, 1) ^ c1;\n\t\tlong d3 = Long.rotateLeft(c4, 1) ^ c2;\n\t\tlong d4 = Long.rotateLeft(c0, 1) ^ c3;\n\t\tout0 = out0 ^ d0;\n\t\tout1 = out1 ^ d1;\n\t\tout2 = out2 ^ d2;\n\t\tout3 = out3 ^ d3;\n\t\tout4 = out4 ^ d4;\n\t\tout5 = out5 ^ d0;\n\t\tout6 = out6 ^ d1;\n\t\tout7 = out7 ^ d2;\n\t\tout8 = out8 ^ d3;\n\t\tout9 = out9 ^ d4;\n\t\tout10 = out10 ^ d0;\n\t\tout11 = out11 ^ d1;\n\t\tout12 = out12 ^ d2;\n\t\tout13 = out13 ^ d3;\n\t\tout14 = out14 ^ d4;\n\t\tout15 = out15 ^ d0;\n\t\tout16 = out16 ^ d1;\n\t\tout17 = out17 ^ d2;\n\t\tout18 = out18 ^ d3;\n\t\tout19 = out19 ^ d4;\n\t\tout20 = out20 ^ d0;\n\t\tout21 = out21 ^ d1;\n\t\tout22 = out22 ^ d2;\n\t\tout23 = out23 ^ d3;\n\t\tout24 = out24 ^ d4;\n\t\t// RHO AND PI\n\t\tlong piOut0 = out0;\n\t\tlong piOut16 = Long.rotateLeft(out5, 36);\n\t\tlong piOut7 = Long.rotateLeft(out10, 3);\n\t\tlong piOut23 = Long.rotateLeft(out15, 41);\n\t\tlong piOut14 = Long.rotateLeft(out20, 18);\n\t\tlong piOut10 = Long.rotateLeft(out1, 1);\n\t\tlong piOut1 = Long.rotateLeft(out6, 44);\n\t\tlong piOut17 = Long.rotateLeft(out11, 10);\n\t\tlong piOut8 = Long.rotateLeft(out16, 45);\n\t\tlong piOut24 = Long.rotateLeft(out21, 2);\n\t\tlong piOut20 = Long.rotateLeft(out2, 62);\n\t\tlong piOut11 = Long.rotateLeft(out7, 6);\n\t\tlong piOut2 = Long.rotateLeft(out12, 43);\n\t\tlong piOut18 = Long.rotateLeft(out17, 15);\n\t\tlong piOut9 = Long.rotateLeft(out22, 61);\n\t\tlong piOut5 = Long.rotateLeft(out3, 28);\n\t\tlong piOut21 = Long.rotateLeft(out8, 55);\n\t\tlong piOut12 = Long.rotateLeft(out13, 25);\n\t\tlong piOut3 = Long.rotateLeft(out18, 21);\n\t\tlong piOut19 = Long.rotateLeft(out23, 56);\n\t\tlong piOut15 = Long.rotateLeft(out4, 27);\n\t\tlong piOut6 = Long.rotateLeft(out9, 20);\n\t\tlong piOut22 = Long.rotateLeft(out14, 39);\n\t\tlong piOut13 = Long.rotateLeft(out19, 8);\n\t\tlong piOut4 = Long.rotateLeft(out24, 14);\n\t\t// CHI\n\t\tout0 = piOut0 ^ ((~piOut1) & piOut2);\n\t\tout1 = piOut1 ^ ((~piOut2) & piOut3);\n\t\tout2 = piOut2 ^ ((~piOut3) & piOut4);\n\t\tout3 = piOut3 ^ ((~piOut4) & piOut0);\n\t\tout4 = piOut4 ^ ((~piOut0) & piOut1);\n\t\tout5 = piOut5 ^ ((~piOut6) & piOut7);\n\t\tout6 = piOut6 ^ ((~piOut7) & piOut8);\n\t\tout7 = piOut7 ^ ((~piOut8) & piOut9);\n\t\tout8 = piOut8 ^ ((~piOut9) & piOut5);\n\t\tout9 = piOut9 ^ ((~piOut5) & piOut6);\n\t\tout10 = piOut10 ^ ((~piOut11) & piOut12);\n\t\tout11 = piOut11 ^ ((~piOut12) & piOut13);\n\t\tout12 = piOut12 ^ ((~piOut13) & piOut14);\n\t\tout13 = piOut13 ^ ((~piOut14) & piOut10);\n\t\tout14 = piOut14 ^ ((~piOut10) & piOut11);\n\t\tout15 = piOut15 ^ ((~piOut16) & piOut17);\n\t\tout16 = piOut16 ^ ((~piOut17) & piOut18);\n\t\tout17 = piOut17 ^ ((~piOut18) & piOut19);\n\t\tout18 = piOut18 ^ ((~piOut19) & piOut15);\n\t\tout19 = piOut19 ^ ((~piOut15) & piOut16);\n\t\tout20 = piOut20 ^ ((~piOut21) & piOut22);\n\t\tout21 = piOut21 ^ ((~piOut22) & piOut23);\n\t\tout22 = piOut22 ^ ((~piOut23) & piOut24);\n\t\tout23 = piOut23 ^ ((~piOut24) & piOut20);\n\t\tout24 = piOut24 ^ ((~piOut20) & piOut21);\n\t\t// IOTA\n\t\tout0 ^= KeccackRoundConstants[i];\n\t\t}\n\t\tstate[0] = out0;\n\t\tstate[1] = out1;\n\t\tstate[2] = out2;\n\t\tstate[3] = out3;\n\t\tstate[4] = out4;\n\t\tstate[5] = out5;\n\t\tstate[6] = out6;\n\t\tstate[7] = out7;\n\t\tstate[8] = out8;\n\t\tstate[9] = out9;\n\t\tstate[10] = out10;\n\t\tstate[11] = out11;\n\t\tstate[12] = out12;\n\t\tstate[13] = out13;\n\t\tstate[14] = out14;\n\t\tstate[15] = out15;\n\t\tstate[16] = out16;\n\t\tstate[17] = out17;\n\t\tstate[18] = out18;\n\t\tstate[19] = out19;\n\t\tstate[20] = out20;\n\t\tstate[21] = out21;\n\t\tstate[22] = out22;\n\t\tstate[23] = out23;\n\t\tstate[24] = out24;\n\t}\n\n\tpublic void clear() {\n\t\tArrays.fill(state, 0l);\n\t}\n\n\tfinal static int NR_ROUNDS = 24;\n\tfinal static int NR_LANES = 25;\n\n\tlong[] state = new long[NR_LANES];\n\n\tint rateBytes;\n    int rateBits;\n    int capacityBits;\n\tint firstRound;\n\n\tfinal static int index(int x, int y)\n\t{\n\t\treturn (((x)%5)+5*((y)%5));\n\t}\n\n\tfinal static long rol64(long l, int offset) {\n\t\treturn Long.rotateLeft(l, offset);\n\t}\n\n\tfinal static int[] KeccakRhoOffsets = new int[NR_LANES];\n\tfinal static long[] KeccackRoundConstants = new long [NR_ROUNDS];\n\n\tstatic {\n\t\tKeccakF1600_InitializeRoundConstants();\n\t    KeccakF1600_InitializeRhoOffsets();\n\t}\n\n\tfinal static void KeccakF1600_InitializeRoundConstants()\n\t{\n\t\t byte[] LFSRState= new byte[] { 0x01 } ;\n\t\t int i, j, bitPosition;\n\n\t\t for(i=0; i < NR_ROUNDS; i++) {\n\t\t\t KeccackRoundConstants[i] = 0;\n\t\t\t for(j=0; j<7; j++) {\n\t\t\t\t bitPosition = (1<<j)-1; //2^j-1\n\t\t\t\t if (LFSR86540(LFSRState))\n\t\t\t\t\t KeccackRoundConstants[i] ^= 1l<<bitPosition;\n\t\t\t }\n\t\t }\n\t}\n\n\tfinal static boolean LFSR86540(byte[] LFSR)\n\t{\n\t    boolean result = (LFSR[0] & 0x01) != 0;\n\t    if ((LFSR[0] & 0x80) != 0)\n\t        // Primitive polynomial over GF(2): x^8+x^6+x^5+x^4+1\n\t    \tLFSR[0] = (byte) ((LFSR[0] << 1) ^ 0x71);\n\t    else\n\t    \tLFSR[0] <<= 1;\n\t    return result;\n\t}\n\n\tfinal static void KeccakF1600_InitializeRhoOffsets()\n\t {\n\t\t  int x, y, t, newX, newY;\n\n\t\t  KeccakRhoOffsets[index(0, 0)] = 0;\n\t\t  x = 1;\n\t\t  y = 0;\n\t\t  for(t=0; t<24; t++) {\n\t\t\t  KeccakRhoOffsets[index(x, y)] = ((t+1)*(t+2)/2) % 64;\n\t\t\t  newX = (0*x+1*y) % 5;\n\t\t\t  newY = (2*x+3*y) % 5;\n\t\t\t  x = newX;\n\t\t\t  y = newY;\n\t\t  }\n\t }\n\n\tfinal void theta()\n\t{\n\t\tlong[] tempC = new long[5];\n\t\tlong[] tempD = new long[5];\n\t       int x, y;\n\n\t       for(x=0; x<5; x++) {\n\t           tempC[x] = 0;\n\t           for(y=0; y<5; y++)\n\t               tempC[x] ^= state[index(x, y)];\n\t       }\n\t       for(x=0; x<5; x++)\n\t           tempD[x] = rol64(tempC[((x+1)%5)], 1) ^ tempC[((x+4)%5)];\n\t       for(x=0; x<5; x++)\n\t           for(y=0; y<5; y++)\n\t               state[index(x, y)] ^= tempD[x];\n\t}\n\n\tfinal void rho()\n\t{\n\t    int x, y;\n\n\t    for(x=0; x<5; x++) for(y=0; y<5; y++)\n\t        state[index(x, y)] = rol64(state[index(x, y)], KeccakRhoOffsets[index(x, y)]);\n\t}\n\n\tfinal void pi()\n\t{\n\t\tlong[] tempA = new long[25];\n\t    int x, y;\n\n\t    for(x=0; x<5; x++) for(y=0; y<5; y++)\n\t        tempA[index(x, y)] = state[index(x, y)];\n\t    for(x=0; x<5; x++) for(y=0; y<5; y++)\n\t        state[index(0*x+1*y, 2*x+3*y)] = tempA[index(x, y)];\n\t}\n\n\tfinal void chi()\n\t{\n\t\tlong[] tempC = new long[5];\n\t    int x, y;\n\n\t    for(y=0; y<5; y++) {\n\t        for(x=0; x<5; x++)\n\t            tempC[x] = state[index(x, y)] ^ ((~state[index(x+1, y)]) & state[index(x+2, y)]);\n\t        for(x=0; x<5; x++)\n\t            state[index(x, y)] = tempC[x];\n\t    }\n\t}\n\n\tvoid iota(int indexRound)\n\t{\n\t    state[index(0, 0)] ^= KeccackRoundConstants[indexRound];\n\t}\n\n\tpublic int getRateBits() {\n\t\treturn rateBits;\n\t}\n\n\tpublic int getCapacityBits() {\n\t\treturn capacityBits;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/core/KeccakSponge.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc.\n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core;\n\nimport java.io.FilterOutputStream;\nimport java.io.IOException;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.XOFParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.KeccakStateUtils.StateOp;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitInputStream;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitOutputStream;\n\npublic class KeccakSponge {\n\n\tKeccak1600 keccak1600;\n\n\tint domainPaddingBitLength;\n\tbyte domainPadding;\n\n\tprivate int ratePos;\n\n\tSqueezeStream squeezeStream;\n\tAbsorbStream absorbStream;\n\n\tprivate final class SqueezeStream extends BitInputStream {\n\t\tprivate boolean closed = true;\n\n\t\tpublic SqueezeStream() {\n\n\t\t}\n\n\t\t@Override\n\t\tpublic void close() {\n\t\t\tif(!closed) {\n\t\t\t\tkeccak1600.clear();\n\t\t\t\tclosed = true;\n\t\t\t\tratePos = 0;\n\t\t\t}\n\t\t}\n\n\t\tvoid open() {\n\t\t\tif(closed) {\n\t\t\t\tif(absorbStream != null)\n\t\t\t\t\tabsorbStream.close();\n\n\t\t\t\tratePos = 0;\n\t\t\t\tclosed = false;\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic long readBits(byte[] bits, long bitOff, long bitLen) {\n\t\t\topen();\n\t\t\tlong rv = 0;\n\t\t\twhile(bitLen > 0) {\n\t\t\t\tint remainingBits = keccak1600.remainingBits(ratePos);\n\t\t\t\tif(remainingBits <=  0) {\n\t\t\t\t\tkeccak1600.permute();\n\t\t\t\t\tratePos = 0;\n\t\t\t\t\tremainingBits = keccak1600.remainingBits(ratePos);\n\t\t\t\t}\n\t\t\t\tint chunk = (int) Math.min(bitLen, remainingBits);\n\n\t\t\t\tif((ratePos & 7)==0 && (bitOff&7)==0 && (chunk&7)==0) {\n\t\t\t\t\tkeccak1600.getBytes(ratePos>>3, bits, (int) (bitOff>>3), chunk>>3);\n\t\t\t\t} else {\n\t\t\t\t\tkeccak1600.getBits(ratePos, bits, bitOff, chunk);\n\t\t\t\t}\n\n\t\t\t\tratePos += chunk;\n\t\t\t\tbitLen -= chunk;\n\t\t\t\tbitOff += chunk;\n\t\t\t\trv += chunk;\n\t\t\t}\n\n\t\t\treturn rv;\n\t\t}\n\n\t\t@Override\n\t\tpublic long transformBits(byte[] input, long inputOff, byte[] output, long outputOff, long bitLen) {\n\t\t\tlong rv = 0;\n\t\t\twhile(bitLen > 0) {\n\t\t\t\tint remainingBits = keccak1600.remainingBits(ratePos);\n\t\t\t\tif(remainingBits <=  0) {\n\t\t\t\t\tkeccak1600.permute();\n\t\t\t\t\tratePos = 0;\n\t\t\t\t\tremainingBits = keccak1600.remainingBits(ratePos);\n\t\t\t\t}\n\t\t\t\tint chunk = (int) Math.min(bitLen, remainingBits);\n\n\t\t\t\tif((ratePos & 7)==0 && (inputOff&7)==0 && (outputOff&7)==0 && (chunk&7)==0) {\n\t\t\t\t\tkeccak1600.bytesOp(StateOp.XOR_TRANSFORM, ratePos>>3, output, (int) (outputOff>>3), input, (int) (inputOff>>3), chunk>>3);\n\t\t\t\t} else {\n\t\t\t\t\tkeccak1600.bitsOp(StateOp.XOR_TRANSFORM, ratePos, output, outputOff, input, inputOff, chunk);\n\t\t\t\t}\n\n\t\t\t\tratePos += chunk;\n\t\t\t\tbitLen -= chunk;\n\t\t\t\tinputOff += chunk;\n\t\t\t\toutputOff += chunk;\n\t\t\t\trv += chunk;\n\t\t\t}\n\t\t\treturn rv;\n\t\t}\n\t}\n\n\tprivate final class AbsorbStream extends BitOutputStream {\n\t\tprivate boolean closed = false;\n\n\t\t@Override\n\t\tpublic void close() {\n\t\t\tif(!closed){\n\t\t\t\tkeccak1600.pad(domainPadding, domainPaddingBitLength, ratePos);\n\t\t\t\tkeccak1600.permute();\n\t\t\t\tclosed = true;\n\t\t\t\tratePos = 0;\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic void writeBits(byte[] bits, long bitOff, long bitLen) {\n\t\t\topen();\n\t\t\twhile(bitLen > 0) {\n\t\t\t\tint remainingBits = keccak1600.remainingBits(ratePos);\n\t\t\t\tif(remainingBits <=  0) {\n\t\t\t\t\tkeccak1600.permute();\n\t\t\t\t\tratePos = 0;\n\t\t\t\t\tremainingBits = keccak1600.remainingBits(ratePos);\n\t\t\t\t}\n\t\t\t\tint chunk = (int) Math.min(bitLen, remainingBits);\n\n\t\t\t\tif((ratePos & 7)==0 && (bitOff&7)==0 && (chunk&7)==0) {\n\t\t\t\t\tkeccak1600.setXorBytes(ratePos>>3, bits, (int) (bitOff>>3), chunk>>3);\n\t\t\t\t} else {\n\t\t\t\t\tkeccak1600.setXorBits(ratePos, bits, bitOff, chunk);\n\t\t\t\t}\n\n\t\t\t\tratePos += chunk;\n\t\t\t\tbitLen -= chunk;\n\t\t\t\tbitOff += chunk;\n\t\t\t}\n\t\t}\n\n\t\tpublic void open() {\n\t\t\tif(closed) {\n\t\t\t\tif(squeezeStream != null) {\n\t\t\t\t\tsqueezeStream.close();\n\t\t\t\t} else {\n\t\t\t\t\tkeccak1600.clear();\n\t\t\t\t\tratePos = 0;\n\t\t\t\t}\n\t\t\t\tclosed = false;\n\t\t\t}\n\n\t\t}\n\t}\n\n\tpublic KeccakSponge(XOFParameterSet params) {\n\t\tthis.keccak1600 = new Keccak1600(params.getCapacityInBits());\n\t\tthis.domainPadding = params.getDomainPadding();\n\t\tthis.domainPaddingBitLength = params.getDomainPaddingBitLength();\n\t}\n\n\tpublic KeccakSponge(int capacityInBits, byte domainPadding, int domainPaddingBitLength) {\n\t\tthis.keccak1600 = new Keccak1600(capacityInBits);\n\t\tthis.domainPadding = domainPadding;\n\t\tthis.domainPaddingBitLength = domainPaddingBitLength;\n\t\tthis.ratePos = 0;\n\t}\n\n\tpublic void reset() {\n\t\tif(absorbStream != null) {\n\t\t\tabsorbStream.open();\n\t\t}\n\t}\n\n\tpublic BitInputStream getSqueezeStream() {\n\t\tif(squeezeStream == null) {\n\t\t\tsqueezeStream = new SqueezeStream();\n\t\t}\n\t\tsqueezeStream.open();\n\n\t\treturn squeezeStream;\n\t}\n\n\tpublic BitOutputStream getAbsorbStream() {\n\t\tif(absorbStream == null) {\n\t\t\tabsorbStream = new AbsorbStream();\n\t\t}\n\t\tabsorbStream.open();\n\n\t\treturn absorbStream;\n\t}\n\n\tpublic java.io.FilterOutputStream getTransformingSqueezeStream(final java.io.OutputStream target) {\n\t\treturn new FilterOutputStream(target) {\n\t\t\tbyte[] buf = new byte[4096];\n\n\t\t\t@Override\n\t\t\tpublic void write(byte[] b, int off, int len) throws IOException {\n\t\t\t\twhile(len > 0) {\n\t\t\t\t\tint chunk = Math.min(len, buf.length);\n\t\t\t\t\tgetSqueezeStream().transform(b, off, buf, 0, chunk);\n\t\t\t\t\ttarget.write(buf, 0, chunk);\n\t\t\t\t\toff += chunk;\n\t\t\t\t\tlen -= chunk;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void write(byte[] b) throws IOException {\n\t\t\t\tthis.write(b, 0, b.length);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void write(int b) throws IOException {\n\t\t\t\ttarget.write(b ^ getSqueezeStream().read());\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void close() throws IOException {\n\t\t\t\tbuf = null;\n\t\t\t\tgetSqueezeStream().close();\n\t\t\t\tsuper.close();\n\t\t\t}\n\t\t};\n\n\t}\n\n\tpublic byte[] getRateBits(int boff, int len)\n\t{\n\t\tbyte[] rv = new byte[(len+ (8 - len & 7)) >> 3];\n\t\tkeccak1600.getBits(boff, rv, boff, len);\n\t\treturn rv;\n\t}\n\n\tpublic int getRateBits() {\n\t\treturn keccak1600.getRateBits();\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/core/KeccakStateUtils.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc.\n *\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core;\n\n/**\n * Contains methods to manipulate Keccak 64-bit long state using various-length primitives.\n *\n */\nfinal class KeccakStateUtils {\n\n\tpublic enum StateOp {\n\n\t\tZERO, GET, VALIDATE, XOR_IN, XOR_TRANSFORM, WRAP, UNWRAP;\n\n\t\tpublic boolean isIn() {\n\t\t\treturn (this == XOR_IN || this == XOR_TRANSFORM || this == WRAP || this == UNWRAP || this == VALIDATE);\n\t\t}\n\n\t\tpublic boolean isOut() {\n\t\t\treturn (this == GET || this == XOR_TRANSFORM || this == WRAP || this == UNWRAP);\n\t\t}\n\n\t};\n\n\t/**\n\t * Perform a bitwise state operation on bits packed in a long value, of which we may be using only a part.\n\t *\n\t * @param stateOp Operation to perform\n\t * @param state An array of {@code long} values representing the state being processed\n\t * @param position An {@code int} representing the position in the state array to operate upon\n\t * @param value A {@code long} representing an input value for the operation\n\t * @param bitOffset An {@code int} representing the offset (from zero, in bits) within the long value\n\t * @param bitLength An {@code int} representing the number of bits from the offset\n\t *\n\t * @return A {@code long} with the result value if the operation has one, otherwise 0L.\n\t */\n\tpublic static long longOp(StateOp stateOp, long[] state, int position, long value, int bitOffset, int bitLength) {\n\n\t\t// Declare mask\n\t\t// Java longs are always 64-bit signed twos-compliment numbers (8 bytes, big endian)\n\t\tlong mask = getMask(bitOffset, bitLength);\n\n\t\t// Initialize the result\n\t\tlong result = 0L;\n\t\t\n\t\t// Copy the state value for the given position\n\t\tlong stateValue = state[position];\n\t\t\n\t\t// Switch logic based on state operation\n\t\tswitch (stateOp) {\n\t\t\t\n\t\tcase ZERO:\n\n\t\t\t// Zero out the relevant bits in the state\n\t\t\t// We take the existing value and apply the mask to throw away the most significant bits we don't want.\n\t\t\t// We then invert this operation by performing an XOR (^) with the original state value which will result\n\t\t\t// in 1s only in the positions where the values are different.  This has the effect of retaining the\n\t\t\t// unmasked bits and turning all the masked bits to 0s, which we need to do since the unmasked bits\n\t\t\t// represent data in the bit stream.\n\t\t\t//\n\t\t\t// Operation result ends up in the state array, and 0L is returned to caller\n\t\t\tstate[position] = stateValue ^ (stateValue & mask);\n\t\t\tbreak;\n\t\t\t\n\t\tcase GET:\n\t\t\t\n\t\t\t// Retrieve the masked value (just the bits we care about)\n\t\t\t// Result is returned to caller and not put into the state array\n\t\t\tresult = stateValue & mask;\n\t\t\tbreak;\n\t\t\t\n\t\t\tcase XOR_TRANSFORM:\n\t\t\tcase VALIDATE:\n\t\t\t\n\t\t\t// XOR the provided input value with the existing state value and mask off the bits we care about\n\t\t\t// Result is returned to caller and not put into the state array\n\t\t\tresult = (value ^ stateValue) & mask;\n\t\t\tbreak;\n\t\t\t\n\t\tcase XOR_IN:\n\n\t\t\t// Mask the input value first and then XOR with the existing state value\n\t\t\t// Operation result ends up in the state array, and 0L is returned to caller\n\t\t\tstateValue = stateValue ^ (value & mask);\n\t\t\tstate[position] = stateValue;\n\t\t\tbreak;\n\n\t\tcase UNWRAP:\n\n\t\t\t// XORs the input value with the state value and masks the result, then XORs with the state value again.\n\t\t\t// This has the net effect of decoding a value previously encoded with the same input.\n\t\t\tresult = (value ^ stateValue) & mask;\n\t\t\tstateValue = stateValue ^ result;\n\t\t\tstate[position] = stateValue;\n\t\t\tbreak;\n\n\t\tcase WRAP:\n\n\t\t\t// XORs the input value with the state value and masks the result, then XORs with the masked state value.\n\t\t\t// This has the net effect of encoding the state value with the provided input.\n\t\t\tresult = (value ^ stateValue) & mask;\n\t\t\tstateValue = stateValue ^ (value & mask);\n\t\t\tstate[position] = stateValue;\n\t\t\tbreak;\n\n\t\t}\n\n\t\t// Return result, which may be 0L depending on the operation performed if it updated the state array\n\t\treturn result;\n\n\t}\n\n\t/**\n\t * Calculates a bit mask within a {@code long} for a given bit offset and length.\n\t *\n\t * @param bitOffset An {@code int} representing the 0-indexed start bit\n\t * @param bitLength An {@code int} representing the number of bits from the offset in include\n\t *\n\t * @return A {@code long} value containing 1s between {@code bitOffset} and {@code bitOffset + bitLength} and 0s\n\t * \t\t\tin all the other bits, which can be used to mask off other long values.\n\t */\n\tprivate static long getMask(int bitOffset, int bitLength) {\n\t\tlong mask;\n\n\t\t// Check if operation bit length is less than word size\n\t\tif(bitLength < 64) {\n\n\t\t\t// Fill the mask with 64 ones, then left shift by the bitLength of the operation.\n\t\t\t// This leaves us with all 1s except for the least significant bitLength bits, which will be 0s.\n\t\t\t// We then invert that value again, which gives us all 1s for the least significant bitLength bits.\n\t\t\t// This leaves us with a mask we can bitwise and (&) with another value to easily throw away bits\n\t\t\t// we don't care about.\n\t\t\t// NOTE: ~ is the bitwise complement operator which inverts all 0s and 1s.  ~0L is a shorthand for 64-bits\n\t\t\t// \t\tof 1s because the alternative is using 18446744073709551615L which is the decimal representation of\n\t\t\t//\t\tthe same value.\n\t\t\tmask = ~(~0L << bitLength);\n\t\t\tmask = mask << bitOffset;\n\n\t\t} else {\n\n\t\t\t// Mask is 64 ones (~ is the bitwise inversion operation)\n\t\t\t// In this case we are masking nothing and using the full size of the long value (a no-op).\n\t\t\tmask = ~0L;\n\n\t\t}\n\t\treturn mask;\n\t}\n\n\tpublic static long longOp(StateOp operation, long[] state, int position, long value) {\n\t\treturn longOp(operation, state, position, value, 0, 64);\n\t}\n\n\t/**\n\t * Performs an integer operation on a specific position within a long array state.\n\t *\n\t * @param operation The StateOp enum representing the operation to be performed.\n\t * @param state     The long array representing the current state.\n\t * @param position  The position in the state where the operation should be applied.\n\t * @param value     The integer value to be used in the operation.\n\t * @return          The result of the operation as an integer.\n\t */\n\tpublic static int intOp(StateOp operation, long[] state, int position, int value) {\n\n\t\t// Calculate the index in the long array\n\t\t// Each long can hold two integers, so we divide the position by 2\n\t\tint lpos = position >> 1;  // Equivalent to position / 2\n\n\t\t// Calculate the bit offset within the long\n\t\t// If position is even, bitoff is 0; if odd, bitoff is 32\n\t\tint bitoff = (position & 1) << 5;  // Equivalent to (position % 2) * 32\n\n\t\t// Perform the operation and extract the result:\n\t\t// 1. Cast the input value to long and shift it left by bitoff\n\t\t// 2. Call longOp to perform the operation on the long array\n\t\t// 3. Shift the result right by bitoff to align it\n\t\t// 4. Mask with 0xffffffffL to ensure we only get the lower 32 bits\n\t\t// 5. Cast the result back to int\n\t\treturn (int) ((longOp(operation, state, lpos, ((long)value) << bitoff, bitoff, 32) >>> bitoff) & 0xffffffffL);\n\t}\n\n\t/**\n\t * Performs a short operation on a specific position within a long array state.\n\t *\n\t * @param stateOp The StateOp enum representing the operation to be performed.\n\t * @param state   The long array representing the current state.\n\t * @param pos     The position in the state where the operation should be applied.\n\t * @param val     The short value to be used in the operation.\n\t * @return        The result of the operation as a short.\n\t */\n\tpublic static short shortOp(StateOp stateOp, long[] state, int pos, short val) {\n\t\t// Calculate the index in the long array\n\t\t// Each long can hold four shorts, so we divide the position by 4\n\t\tint lpos = pos >> 2;  // Equivalent to pos / 4\n\n\t\t// Calculate the bit offset within the long\n\t\t// The offset can be 0, 16, 32, or 48 depending on which of the four shorts we're targeting\n\t\tint bitoff = (pos & 3) << 4;  // Equivalent to (pos % 4) * 16\n\n\t\t// Perform the operation and extract the result:\n\t\t// 1. Cast the input value to long and shift it left by bitoff\n\t\t// 2. Call longOp to perform the operation on the long array\n\t\t// 3. Shift the result right by bitoff to align it\n\t\t// 4. Mask with 0xffffL to ensure we only get the lower 16 bits\n\t\t// 5. Cast the result back to short\n\t\treturn (short) ((longOp(stateOp, state, lpos, ((long)val) << bitoff, bitoff, 16) >>> bitoff) & 0xffffL);\n\t}\n\n\t/**\n\t * Performs a byte operation on a specific position within a long array state.\n\t *\n\t * @param stateOp The StateOp enum representing the operation to be performed.\n\t * @param state   The long array representing the current state.\n\t * @param pos     The position in the state where the operation should be applied.\n\t * @param val     The byte value to be used in the operation.\n\t * @return        The result of the operation as a byte.\n\t */\n\tpublic static byte byteOp(StateOp stateOp, long[] state, int pos, byte val) {\n\t\t// Calculate the index in the long array\n\t\t// Each long can hold eight bytes, so we divide the position by 8\n\t\tint lpos = pos >> 3;  // Equivalent to pos / 8\n\n\t\t// Calculate the bit offset within the long\n\t\t// The offset can be 0, 8, 16, 24, 32, 40, 48, or 56 depending on which of the eight bytes we're targeting\n\t\tint bitoff = (pos & 7) << 3;  // Equivalent to (pos % 8) * 8\n\n\t\t// Perform the operation and extract the result:\n\t\t// 1. Cast the input value to long and shift it left by bitoff\n\t\t// 2. Call longOp to perform the operation on the long array\n\t\t// 3. Shift the result right by bitoff to align it\n\t\t// 4. Mask with 0xffL to ensure we only get the lower 8 bits\n\t\t// 5. Cast the result back to byte\n\t\treturn (byte) ((longOp(stateOp, state, lpos, ((long)val) << bitoff, bitoff, 8) >>> bitoff) & 0xffL);\n\t}\n\n\t/**\n\t * Performs a bit operation on a specific position within a long array state.\n\t *\n\t * @param stateOp The StateOp enum representing the operation to be performed.\n\t * @param state   The long array representing the current state.\n\t * @param pos     The position in the state where the operation should be applied.\n\t * @param val     The boolean value to be used in the operation.\n\t * @return        The result of the operation as a boolean.\n\t */\n\tpublic static boolean bitOp(StateOp stateOp, long[] state, int pos, boolean val) {\n\t\t// Calculate the index in the long array\n\t\t// Each long contains 64 bits, so we divide the position by 64\n\t\tint lpos = pos >> 6;  // Equivalent to pos / 64\n\n\t\t// Calculate the bit offset within the long\n\t\t// The offset can be 0 to 63, representing which of the 64 bits we're targeting\n\t\tint bitoff = pos & 63;  // Equivalent to pos % 64\n\n\t\t// Perform the operation and extract the result:\n\t\t// 1. Convert the boolean value to a long (1 for true, 0 for false) and shift it left by bitoff\n\t\t// 2. Call longOp to perform the operation on the long array\n\t\t// 3. Shift the result right by bitoff to align it\n\t\t// 4. Mask with 1L to isolate the least significant bit\n\t\t// 5. Compare the result with 1L to determine the boolean outcome\n\t\treturn ((longOp(stateOp, state, lpos, (val ? 1L : 0L) << bitoff, bitoff, 1) >>> bitoff) & 1L) == 1L;\n\t}\n\n\t/**\n\t * Performs operations on multiple longs within the state array.\n\t *\n\t * @param operation The StateOp enum representing the operation to be performed.\n\t * @param state   The long array representing the current state.\n\t * @param position     The starting position in the state for the operation.\n\t * @param outputs     The output long array (possibly null for non-output operations).\n\t * @param outputPosition  The starting position in the output array.\n\t * @param inputs      The input long array (possibly null for non-input operations).\n\t * @param inputPosition   The starting position in the input array.\n\t * @param length     The number of longs to process.\n\t * @throws KeccakStateValidationFailedException If validation fails during a VALIDATE operation.\n\t */\n\tpublic static void longsOp(StateOp operation, long[] state, int position,\n\t\t\t\t\t\t\t   long[] outputs, int outputPosition, long[] inputs, int inputPosition, int length) {\n\n\t\tlong invalid = 0L;\n\t\tboolean isIn = operation.isIn();   // Check if the operation requires an input\n\t\tboolean isOut = operation.isOut(); // Check if the operation produces an output\n\n\t\twhile (length > 0) {\n\t\t\tlong tmp = 0L;\n\n\t\t\t// If the operation requires an input, read from the input array\n\t\t\tif (isIn) {\n\t\t\t\ttmp = inputs[inputPosition];\n\t\t\t\tinputPosition++;\n\t\t\t}\n\n\t\t\t// Perform the operation on the current long\n\t\t\ttmp = longOp(operation, state, position, tmp);\n\n\t\t\t// If the operation produces output, write to the output array\n\t\t\tif (isOut) {\n\t\t\t\toutputs[outputPosition] = tmp;\n\t\t\t\toutputPosition++;\n\t\t\t}\n\n\t\t\t// For VALIDATE operations, accumulate any non-zero results\n\t\t\tif (operation == StateOp.VALIDATE) {\n\t\t\t\tinvalid |= tmp;\n\t\t\t}\n\n\t\t\tposition++;\n\t\t\tlength--;\n\t\t}\n\n\t\t// If this was a VALIDATE operation and any invalid data was found, throw an exception\n\t\tif (operation == StateOp.VALIDATE && invalid != 0) {\n\t\t\tthrow new KeccakStateValidationFailedException();\n\t\t}\n\t}\n\n\t/**\n\t * Performs operations on multiple integers within the state array.\n\t *\n\t * @param operation The StateOp enum representing the operation to be performed.\n\t * @param state   The long array representing the current state.\n\t * @param position     The starting position in the state for the operation.\n\t * @param outputs     The output int array (possibly null for non-output operations).\n\t * @param outputPosition  The starting position in the output array.\n\t * @param inputs      The input int array (possibly null for non-input operations).\n\t * @param inputPosition   The starting position in the input array.\n\t * @param length     The number of integers to process.\n\t * @throws KeccakStateValidationFailedException If validation fails during a VALIDATE operation.\n\t */\n\tpublic static void intsOp(StateOp operation, long[] state, int position,\n\t\t\t\t\t\t\t  int[] outputs, int outputPosition, int[] inputs, int inputPosition, int length) {\n\t\tlong invalid = 0;\n\t\tboolean isIn = operation.isIn();   // Check if the operation requires an input\n\t\tboolean isOut = operation.isOut(); // Check if the operation produces an output\n\n\t\twhile (length > 0) {\n\t\t\t// Check if we can process two integers at once (64-bit operation)\n\t\t\tif (length > 1 & (position & 1) == 0) {\n\t\t\t\tlong mask = 0xffffffffL;\n\t\t\t\tdo {\n\t\t\t\t\tlong tmp = 0;\n\t\t\t\t\tif (isIn) {\n\t\t\t\t\t\t// Combine two 32-bit integers into one 64-bit long\n\t\t\t\t\t\ttmp = ((long) inputs[inputPosition]) & mask;\n\t\t\t\t\t\t++inputPosition;\n\t\t\t\t\t\ttmp |= (((long) inputs[inputPosition]) & mask) << 32;\n\t\t\t\t\t\t++inputPosition;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the operation on the 64-bit value\n\t\t\t\t\ttmp = longOp(operation, state, position >> 1, tmp);\n\n\t\t\t\t\tif (isOut) {\n\t\t\t\t\t\t// Split the 64-bit result back into two 32-bit integers\n\t\t\t\t\t\toutputs[outputPosition] = (int) (tmp & mask);\n\t\t\t\t\t\t++outputPosition;\n\t\t\t\t\t\ttmp >>>= 32;\n\t\t\t\t\t\toutputs[outputPosition] = (int) (tmp & mask);\n\t\t\t\t\t\t++outputPosition;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (operation == StateOp.VALIDATE) {\n\t\t\t\t\t\tinvalid |= tmp;\n\t\t\t\t\t}\n\n\t\t\t\t\tposition += 2;\n\t\t\t\t\tlength -= 2;\n\t\t\t\t} while (length > 1);\n\t\t\t} else {\n\t\t\t\t// Process a single 32-bit integer\n\t\t\t\tint tmp = 0;\n\n\t\t\t\tif (isIn) {\n\t\t\t\t\ttmp = inputs[inputPosition];\n\t\t\t\t\tinputPosition++;\n\t\t\t\t}\n\n\t\t\t\t// Perform the operation on the 32-bit value\n\t\t\t\ttmp = intOp(operation, state, position, tmp);\n\n\t\t\t\tif (isOut) {\n\t\t\t\t\toutputs[outputPosition] = tmp;\n\t\t\t\t\toutputPosition++;\n\t\t\t\t}\n\n\t\t\t\tif (operation == StateOp.VALIDATE) {\n\t\t\t\t\tinvalid |= tmp;\n\t\t\t\t}\n\n\t\t\t\tposition++;\n\t\t\t\tlength--;\n\t\t\t}\n\t\t}\n\n\t\t// If this was a VALIDATE operation and any invalid data was found, throw an exception\n\t\tif (operation == StateOp.VALIDATE && invalid != 0) {\n\t\t\tthrow new KeccakStateValidationFailedException();\n\t\t}\n\t}\n\n\tpublic static void shortsOp(StateOp stateOp, long[] state, int pos,\n\t\t\tshort[] out, int outpos, short[] in, int inpos, int len)\n\t{\n\t\tlong invalid=0;\n\t\tboolean isIn = stateOp.isIn();\n\t\tboolean isOut = stateOp.isOut();\n\t\twhile(len > 0) {\n\t\t\tif(len > 3 && (pos&3)==0) {\n\t\t\t\tlong mask = 0xffffL;\n\t\t\t\tdo {\n\t\t\t\t\tlong tmp = 0;\n\t\t\t\t\tif(isIn) {\n\t\t\t\t\t\ttmp = ((long) in[inpos]) & mask;\n\t\t\t\t\t\t++inpos;\n\t\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<16;\n\t\t\t\t\t\t++inpos;\n\t\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<32;\n\t\t\t\t\t\t++inpos;\n\t\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<48;\n\t\t\t\t\t\t++inpos;\n\n\t\t\t\t\t}\n\t\t\t\t\ttmp = longOp(stateOp, state, pos>>2, tmp);\n\t\t\t\t\tif(isOut) {\n\t\t\t\t\t\tout[outpos] = (short) (tmp & mask);\n\t\t\t\t\t\t++outpos;\n\t\t\t\t\t\ttmp >>>= 16;\n\t\t\t\t\t\tout[outpos] = (short) (tmp & mask);\n\t\t\t\t\t\t++outpos;\n\t\t\t\t\t\ttmp >>>= 16;\n\t\t\t\t\t\tout[outpos] = (short) (tmp & mask);\n\t\t\t\t\t\t++outpos;\n\t\t\t\t\t\ttmp >>>= 16;\n\t\t\t\t\t\tout[outpos] = (short) (tmp & mask);\n\t\t\t\t\t\t++outpos;\n\t\t\t\t\t}\n\t\t\t\t\tif(stateOp == StateOp.VALIDATE) {\n\t\t\t\t\t\tinvalid |= tmp;\n\t\t\t\t\t}\n\t\t\t\t\tpos += 4;\n\t\t\t\t\tlen -= 4;\n\t\t\t\t} while(len > 3);\n\t\t\t} else {\n\t\t\t\tshort tmp = 0;\n\n\t\t\t\tif(isIn) {\n\t\t\t\t\ttmp = in[inpos];\n\t\t\t\t\tinpos++;\n\t\t\t\t}\n\t\t\t\ttmp = shortOp(stateOp, state, pos, tmp);\n\t\t\t\tif(isOut) {\n\t\t\t\t\tout[outpos] = tmp;\n\t\t\t\t\toutpos++;\n\t\t\t\t}\n\t\t\t\tif(stateOp == StateOp.VALIDATE) {\n\t\t\t\t\tinvalid |= tmp;\n\t\t\t\t}\n\t\t\t\tpos++;\n\t\t\t\tlen--;\n\t\t\t}\n\t\t}\n\t\tif(stateOp == StateOp.VALIDATE && invalid != 0) {\n\t\t\tthrow new KeccakStateValidationFailedException();\n\t\t}\n\t}\n\n\tpublic static void bytesOp(StateOp stateOp, long[] state, int pos,\n\t\t\tbyte[] out, int outpos, byte[] in, int inpos, int len)\n\t{\n\t\tif(len > 7 && (len&7)==0 && (pos&7)==0) {\n\t\t\tlong invalid = 0;\n\t\t\tboolean isIn = stateOp.isIn();\n\t\t\tboolean isOut = stateOp.isOut();\n\t\t\tlong mask = 0xff;\n\t\t\tdo {\n\t\t\t\tlong tmp = 0;\n\t\t\t\tif(isIn) {\n\t\t\t\t\ttmp = ((long) in[inpos]) & mask;\n\t\t\t\t\t++inpos;\n\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<8;\n\t\t\t\t\t++inpos;\n\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<16;\n\t\t\t\t\t++inpos;\n\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<24;\n\t\t\t\t\t++inpos;\n\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<32;\n\t\t\t\t\t++inpos;\n\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<40;\n\t\t\t\t\t++inpos;\n\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<48;\n\t\t\t\t\t++inpos;\n\t\t\t\t\ttmp |= (((long) in[inpos]) & mask)<<56;\n\t\t\t\t\t++inpos;\n\t\t\t\t}\n\t\t\t\ttmp = longOp(stateOp, state, pos>>3, tmp);\n\t\t\t\tif(isOut) {\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t\ttmp >>>= 8;\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t\ttmp >>>= 8;\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t\ttmp >>>= 8;\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t\ttmp >>>= 8;\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t\ttmp >>>= 8;\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t\ttmp >>>= 8;\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t\ttmp >>>= 8;\n\t\t\t\t\tout[outpos] = (byte) (tmp & mask);\n\t\t\t\t\t++outpos;\n\t\t\t\t}\n\t\t\t\tif(stateOp == StateOp.VALIDATE) {\n\t\t\t\t\tinvalid |= tmp;\n\t\t\t\t}\n\n\t\t\t\tpos += 8;\n\t\t\t\tlen -= 8;\n\t\t\t} while(len > 0);\n\t\t\tif(stateOp == StateOp.VALIDATE && invalid != 0) {\n\t\t\t\tthrow new KeccakStateValidationFailedException();\n\t\t\t}\n\t\t} else {\n\t\t\tbitsOp(stateOp, state, pos<<3, out, ((long) outpos)<<3, in, ((long)inpos)<<3, len <<3);\n\t\t}\n\t}\n\n\tpublic static byte bitsOp(StateOp stateOp, long[] state, int pos, byte in, int len)\n\t{\n\t\tboolean isIn = stateOp.isIn();\n\t\tboolean isOut = stateOp.isOut();\n\t\tbyte rv=0;\n\n\t\tint lpos = (pos>>6);\n\t\tint loff = (pos & 63);\n\n\t\tint len1 = Math.min(len, 64-loff);\n\t\tint len2 = len - len1;\n\t\tlong mask1 = ((~(0xff<<len1))& 0xffL);\n\t\tlong mask2 = ((~(0xff<<len2))& 0xffL);\n\n\t\tlong tmp = 0;\n\t\tif(isIn) {\n\t\t\ttmp = ((long) in) & mask1;\n\t\t\ttmp <<= loff;\n\t\t}\n\t\ttmp = longOp(stateOp, state, lpos, tmp, loff, len1);\n\t\tif(isOut) {\n\t\t\ttmp >>= loff;\n\t\t\trv = (byte) (tmp & mask1);\n\t\t}\n\n\t\tif(len2 > 0) {\n\t\t\t++lpos;\n\t\t\tloff = 0;\n\n\t\t\tif(isIn) {\n\t\t\t\ttmp = ((long) (in>>>len1)) & mask2;\n\t\t\t}\n\t\t\ttmp = longOp(stateOp, state, lpos, tmp, loff, len2);\n\t\t\tif(isOut) {\n\t\t\t\trv |= ((byte) (tmp & mask2))<<len1;\n\t\t\t}\n\t\t}\n\n\t\treturn rv;\n\t}\n\n\tpublic static void bitsOp(StateOp stateOp, long[] state, int pos,\n\t\t\tbyte[] out, long outpos, byte[] in, long inpos, int len)\n\t{\n\t\tlong invalid=0;\n\t\tboolean isIn = stateOp.isIn();\n\t\tboolean isOut = stateOp.isOut();\n\t\twhile(len > 0) {\n\t\t\tint bitoff = pos & 63;\n\t\t\tint bitlen = Math.min(64 - bitoff, len);\n\n\t\t\tlong tmp = 0;\n\t\t\tint lpos = pos >> 6;\n\n\t\t\tif(isIn) {\n\t\t\t\ttmp = setBitsInLong(in, inpos, tmp, bitoff, bitlen);\n\t\t\t\tinpos += bitlen;\n\t\t\t}\n\t\t\ttmp = longOp(stateOp, state, lpos, tmp, bitoff, bitlen);\n\t\t\tif(isOut) {\n\t\t\t\tsetBitsFromLong(out, outpos, tmp, bitoff, bitlen);\n\t\t\t\toutpos += bitlen;\n\t\t\t}\n\t\t\tif(stateOp == StateOp.VALIDATE) {\n\t\t\t\tinvalid |= tmp;\n\t\t\t}\n\t\t\tpos += bitlen;\n\t\t\tbitoff += bitlen;\n\t\t\tlen -= bitlen;\n\t\t}\n\t\tif(stateOp == StateOp.VALIDATE && invalid != 0) {\n\t\t\tthrow new KeccakStateValidationFailedException();\n\t\t}\n\t}\n\n\tstatic long setBitsInLong(byte[] src, long srcoff,  long l, int off, int len)\n\t{\n\t\tint shift=off;\n\t\t// clear bits in l\n\t\tlong mask = ~(~0l << len);\n\t\tmask = mask << off;\n\t\tl ^= l & mask;\n\t\twhile(len > 0) {\n\t\t\tint bitoff = (int) (srcoff & 7);\n\t\t\tint srcByteOff = (int) (srcoff >> 3);\n\t\t\tif(bitoff==0 && len >= 8) {\n\t\t\t\tdo {\n\t\t\t\t\t// aligned byte\n\t\t\t\t\tlong val = ((long )(src[srcByteOff])) &0xffl;\n\n\t\t\t\t\tl |= val << shift;\n\t\t\t\t\tshift += 8;\n\t\t\t\t\tlen -= 8;\n\t\t\t\t\tsrcoff += 8;\n\t\t\t\t\t++srcByteOff;\n\t\t\t\t} while(len >= 8);\n\t\t\t} else {\n\t\t\t\tint bitlen = Math.min(8 - bitoff, len);\n\n\t\t\t\tbyte valmask = (byte) ((0xff << bitoff) & (0xff >>> (8-bitlen-bitoff)));\n\t\t\t\tlong lval = ((long )(src[srcByteOff] & valmask)) & 0xffl;\n\t\t\t\tlval >>>= bitoff;\n\n\t\t\t\tl |= lval << shift;\n\n\t\t\t\tsrcoff += bitlen;\n\t\t\t\tlen -= bitlen;\n\t\t\t\tshift += bitlen;\n\t\t\t}\n\t\t}\n\t\treturn l;\n\t}\n\n\tstatic void setBitsFromLong(byte[] dst, long dstoff,  long l, int off, int len)\n\t{\n\t\tint shift=off;\n\t\twhile(len > 0) {\n\t\t\tint bitoff = (int) dstoff & 7;\n\t\t\tint dstByteOff = (int) (dstoff >> 3);\n\n\t\t\tif(bitoff==0 && len >= 8) {\n\t\t\t\tdo {\n\t\t\t\t\t// aligned byte\n\t\t\t\t\tdst[dstByteOff] = (byte) ((l >>> shift) & 0xff);\n\t\t\t\t\tshift += 8;\n\t\t\t\t\tlen -= 8;\n\t\t\t\t\tdstoff += 8;\n\t\t\t\t\t++dstByteOff;\n\t\t\t\t} while(len >= 8);\n\t\t\t} else {\n\t\t\t\tint bitlen = Math.min(8 - bitoff, len);\n\t\t\t\tbyte mask = (byte) ((0xff << bitoff) & (0xff >>> (8-bitlen-bitoff)));\n\t\t\t\tbyte val = dst[dstByteOff];\n\t\t\t\tlong lval = (l >>> shift);\n\n\t\t\t\tval ^= val & mask;\n\t\t\t\tval |= (lval<<bitoff) & mask;\n\n\t\t\t\tdst[dstByteOff] = val;\n\n\t\t\t\tdstoff += bitlen;\n\t\t\t\tlen -= bitlen;\n\t\t\t\tshift += bitlen;\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/core/KeccakStateValidationFailedException.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core;\n\npublic class KeccakStateValidationFailedException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/io/BitInputStream.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io;\n\nimport java.io.InputStream;\n\npublic abstract class BitInputStream extends InputStream {\t\t\n\t@Override\n\tpublic abstract void close();\n\t\n\t\n\t@Override\n\tpublic int read(byte[] b, int off, int len) {\n\t\treturn (int) (readBits(b, ((long)off)<<3, ((long)len)<<3)>>3);\n\t}\n\t\n\t/**\n\t * Transform input to output with the input stream as a key stream\n\t * \n\t * @param input Input byte-array\n\t * @param inputOff Input offset\n\t * @param output Output byte-array\n\t * @param outputOff Output offset\n\t * @param len length in bytes\n\t * @return Number of bytes transformed\n\t */\n\tpublic int transform(byte[] input, int inputOff, byte[] output, int outputOff, int len) {\n\t\treturn (int) (transformBits(input, ((long)inputOff)<<3, output, ((long)outputOff)<<3, ((long)len)<<3)>>3);\n\t}\n\n\n\t@Override\n\tpublic int read(byte[] b)  {\n\t\treturn this.read(b, 0, b.length);\n\t}\n\t\n\t/**\n\t * Transform input to output using the input stream as a key stream\n\t * \n\t * @param input Input byte array\n\t * @param output Output byte array\n\t * \n\t * @return Number of bytes transformed\n\t */\n\tpublic int transform(byte[] input, byte[] output) {\n\t\treturn (int) (transformBits(input, 0L, output, 0L, ((long)input.length)<<3)>>3);\n\t}\n\n\n\t@Override\n\tpublic int read() {\n\t\tbyte[] buf = new byte[1];\n\t\treadBits(buf, 0, 8);\n\t\t\n\t\treturn ((int) buf[0]) & 0xff;\t\t\n\t}\n\n\n\tpublic abstract long readBits(byte[] arg, long bitOff, long bitLen);\n\t\n\t/**\n\t * Transform input to output using the input stream as a keystream \n\t * \n\t * @param input Input byte array\n\t * @param inputOff Input offset in bits\n\t * @param output Output byte array\n\t * @param outputOff Output offset in bits\n\t * @param bitLen Number of bits\n\t * @return Number of bits transformed\n\t */\n\tpublic abstract long transformBits(byte[] input, long inputOff, byte[] output, long outputOff, long bitLen);\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/io/BitOutputStream.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io;\n\nimport java.io.OutputStream;\n\npublic abstract class BitOutputStream extends OutputStream {\n\n\t@Override\n\tpublic abstract void close();\n\n\t@Override\n\tpublic void write(byte[] b, int off, int len) {\n\t\twriteBits(b, ((long) (off))<<3, ((long)len)<<3);\n\t}\n\n\t@Override\n\tpublic void write(byte[] b) {\n\t\twrite(b, 0, b.length);\n\t}\n\t\n\t@Override\n\tpublic void write(int b) {\n\t\twriteBits(new byte[] { (byte) b }, 0, 8);\n\t}\n\n\tpublic abstract void writeBits(byte[] arg, long bitOff, long bitLen);\n\t\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/AbstractCipher.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport java.nio.ByteBuffer;\nimport java.security.AlgorithmParameters;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.Key;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.security.spec.AlgorithmParameterSpec;\nimport java.util.Arrays;\n\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.CipherSpi;\nimport javax.crypto.IllegalBlockSizeException;\nimport javax.crypto.NoSuchPaddingException;\nimport javax.crypto.ShortBufferException;\nimport javax.crypto.spec.IvParameterSpec;\n\npublic abstract class AbstractCipher extends CipherSpi {\n\tprivate byte[] key;\n\tprivate byte[] nonce;\n\tprivate int mode;\n\t\n\tprivate void setKey(Key key) throws InvalidKeyException \n\t{\n\t\tif(key == null || !(key instanceof RawKey))\n\t\t\tthrow new InvalidKeyException();\n\n\t\tRawKey rawKey = (RawKey) key;\n\t\tif(rawKey.getEncoded()==null)\n\t\t\tthrow new InvalidKeyException();\n\t\t\n\t\tthis.key = Arrays.copyOf(rawKey.getEncoded(), rawKey.getEncoded().length);\t\t\n\t\t\n\t}\n\t\t\t\t\n\t@Override\n\tprotected void engineInit(int mode, Key key, AlgorithmParameterSpec params, SecureRandom sr)\n\t\t\tthrows InvalidKeyException, InvalidAlgorithmParameterException {\n\t\tthis.mode = mode;\n\t\tsetKey(key);\n\t\tif(params != null) {\n\t\t\tif(!(params instanceof IvParameterSpec)) {\n\t\t\t\tthrow new InvalidAlgorithmParameterException();\t\n\t\t\t}\n\t\t\tIvParameterSpec spec = (IvParameterSpec) params;\n\t\t\tnonce = Arrays.copyOf(spec.getIV(), spec.getIV().length);\t\t\t\t\t\t\n\t\t}\t\t\t\n\t\tinit();\n\t}\n\n\t@Override\n\tprotected void engineInit(int mode, Key key, SecureRandom sr) throws InvalidKeyException {\n\t\tthis.mode = mode;\n\t\tsetKey(key);\n\t\tinit();\n\t}\n\t\n\tpublic void reset() {\n\t\tthis.key  = null;\n\t\tthis.nonce = null;\n\t}\n\n\tprotected byte[] getKey() {\n\t\treturn key;\n\t}\n\tprotected byte[] getNonce() {\n\t\treturn nonce;\n\t}\n\n\tpublic byte[] update(byte[] input) {\n\t\treturn engineUpdate(input, 0, input.length);\n\t}\n\n\tpublic byte[] update(byte[] input, int inputOffset, int inputLen) {\n\t\treturn engineUpdate(input, inputOffset, input.length);\n\t}\n\n\tpublic int update(byte[] input, int inputOffset, int inputLen, byte[] output) throws ShortBufferException {\n\t\treturn engineUpdate(input, inputOffset, input.length, output, 0);\t\t\n\t}\n\n\tpublic int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException {\n\t\treturn engineUpdate(input, inputOffset, inputLen, output, outputOffset);\n\t}\n\t\n\t@Override\n\tprotected int engineGetBlockSize() {\n\t\treturn 0;\n\t}\n\n\t@Override\n\tprotected byte[] engineGetIV() {\n\t\tif(nonce != null) {\n\t\t\treturn Arrays.copyOf(nonce, nonce.length);\t\t\t\n\t\t}\n\t\treturn null;\n\t}\n\t\n\t@Override\n\tprotected AlgorithmParameters engineGetParameters() {\n\t\treturn null;\n\t}\n\t\n\t@Override\n\tprotected int engineDoFinal(byte[] input, int inputOff, int len, byte[] output, int outputOff) throws ShortBufferException,\n\t\t\tIllegalBlockSizeException, BadPaddingException {\n\t\tint rv = engineUpdate(input, inputOff, len, output, outputOff);\t\t\n\n\t\treturn rv;\n\t}\n\n\t@Override\n\tprotected byte[] engineDoFinal(byte[] input, int off, int len) throws IllegalBlockSizeException, BadPaddingException  {\t\t\n\t\tbyte[] rv = new byte[engineGetOutputSize(len)];\n\t\t\n\t\ttry {\n\t\t\tengineDoFinal(input, off, len, rv, 0);\n\t\t} catch(ShortBufferException ex) {\n\t\t\tthrow new RuntimeException(ex);\n\t\t}\n\t\t\n\t\treturn rv;\n\t}\n\n\n\t@Override\n\tprotected int engineGetOutputSize(int inputLen) {\n\t\treturn inputLen;\n\t}\n\n\t@Override\n\tprotected void engineSetMode(String mode) throws NoSuchAlgorithmException {\n\t}\n\n\t@Override\n\tprotected void engineSetPadding(String padding) throws NoSuchPaddingException {\n\t\tif(padding != null && padding.length() > 0 && !padding.equals(\"NoPadding\"))\n\t\t\t\tthrow new NoSuchPaddingException();\t\t\n\t}\n\t\n\t\n\t@Override\n\tprotected byte[] engineUpdate(byte[] input, int offset, int len)  {\n\t\tbyte[] output = new byte[len];\n\t\ttry { engineUpdate(input, offset, len, output, 0); } catch(ShortBufferException ex) { throw new RuntimeException(ex); }\n\n\t\treturn output;\n\t}\n\t\n\tprotected abstract void init() throws InvalidKeyException;\n\n\tpublic Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) throws InvalidKeyException, NoSuchAlgorithmException {\n\t\treturn engineUnwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType);\n\t}\n\n\tpublic int update(ByteBuffer input, ByteBuffer output) throws ShortBufferException {\n\t\treturn engineUpdate(input, output);\n\t}\n\n\tpublic void updateAAD(byte[] src) {\n\t\tengineUpdateAAD(src, 0, src.length);\t\t\n\t}\n\n\tpublic void updateAAD(byte[] src, int offset, int len) {\n\t\tengineUpdateAAD(src, offset, len);\t\t\n\t}\n\n\tpublic void updateAAD(ByteBuffer src) {\n\t\tengineUpdateAAD(src);\t\t\n\t}\n\n\tpublic byte[] wrap(Key key) throws InvalidKeyException, IllegalBlockSizeException {\n\t\treturn engineWrap(key);\n\t}\n\n\t@Override\n\tprotected void engineInit(int mode, Key key, AlgorithmParameters ap, SecureRandom sr)\n\t\t\tthrows InvalidKeyException, InvalidAlgorithmParameterException {\n\t\tif(ap != null)\n\t\t\tthrow new InvalidAlgorithmParameterException();\n\t\tengineInit(mode, key, sr);\n\t\t\n\t}\n\n\tpublic void init(int opmode, Key key, AlgorithmParameterSpec params) throws InvalidKeyException, InvalidAlgorithmParameterException {\n\t\treset();\n\t\tengineInit(opmode, key, params, null);\t\t\n\t}\n\n\tpublic byte[] doFinal() throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {\n\t\tint len = engineGetOutputSize(0);\n\t\tbyte[] rv= new byte[len];\n\t\tdoFinal(rv, 0);\n\t\treturn rv;\n\t}\n\n\tpublic byte[] doFinal(byte[] input) throws IllegalBlockSizeException, BadPaddingException {\n\t\treturn engineDoFinal(input, 0, input.length);\n\t}\n\n\tpublic int doFinal(byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {\n\t\treturn engineDoFinal(null, 0, 0, output, outputOffset);\n\t}\n\n\tpublic byte[] doFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException {\n\t\treturn engineDoFinal(input, inputOffset, inputLen);\n\n\t}\n\n\tpublic int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {\n\t\treturn engineDoFinal(input, inputOffset, inputLen, output, 0);\n\t}\n\n\tpublic int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {\n\t\treturn engineDoFinal(input, inputOffset, inputLen, output, outputOffset);\n\t}\n\n\tpublic int doFinal(ByteBuffer input, ByteBuffer output) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {\n\t\treturn engineDoFinal(input, output);\n\t}\n\t\n\tprotected int getMode() {\n\t\treturn mode;\n\t}\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/AbstractSpongeStreamCipher.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc.\n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport javax.crypto.ShortBufferException;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.KeccakSponge;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitOutputStream;\n\npublic abstract class AbstractSpongeStreamCipher extends AbstractCipher{\n\n\t@Override\n\tpublic void reset() {\n\t\tsuper.reset();\n\t\tgetSponge().reset();\n\t}\n\n\t@Override\n\tprotected void init() {\n\t\tKeccakSponge sponge = getSponge();\n\t\tBitOutputStream absorbStream = sponge.getAbsorbStream();\n\t\tabsorbStream.write(getKey());\n\t\tif(getNonce() != null)\n\t\t\tsponge.getAbsorbStream().write(getNonce());\n\n\t\tsponge.getAbsorbStream().close();\n\t}\n\n\t@Override\n\tprotected int engineUpdate(byte[] input, int inputOffset, int len, byte[] output, int outputOffset) throws ShortBufferException {\n\t\treturn getSponge().getSqueezeStream().transform(input, inputOffset, output, outputOffset, len);\n\t}\n\n\t@Override\n\tprotected byte[] engineUpdate(byte[] input, int offset, int len) {\n\t\tbyte[] rv = new byte[len];\n\t\tgetSponge().getSqueezeStream().transform(input, offset, rv, 0, len);\n\n\t\treturn rv;\n\t}\n\n\tabstract KeccakSponge getSponge();\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/KeccakRnd128.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport java.security.SecureRandomSpi;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.DuplexRandom;\n\n/**\n * A cryptographic random implementation providing 128-bit \n * security suitable for generating session keys.\n * \n * Forgets the previous state after 2MB of random data.\n * \n * Re-seeding with a small static seed also forgets the state.\n * \n */\npublic final class KeccakRnd128 extends SecureRandomSpi {\n\tprivate final DuplexRandom dr = new DuplexRandom(253);\n\t\n\tprivate final static int FORGET_INTERVAL = 2*1024*1024;\n\t\n\tprivate int forgetCounter;\n\t\n\tpublic KeccakRnd128() {\n\t\tthis.forgetCounter = FORGET_INTERVAL;\n\t}\n\t\n\t@Override\n\tprotected byte[] engineGenerateSeed(int len) {\n\t\tbyte[] rv = new byte[len];\n\t\t\n\t\tDuplexRandom.getSeedBytes(rv, 0, len);\n\t\t\n\t\treturn rv;\n\t\t\n\t}\n\n\t@Override\n\tprotected void engineNextBytes(byte[] buf) {\n\t\tdr.getBytes(buf, 0, buf.length);\n\t\tforgetCounter -= buf.length;\n\t\tif(forgetCounter <= 0)\n\t\t{\n\t\t\tdr.forget();\n\t\t\tforgetCounter = FORGET_INTERVAL;\n\t\t}\n\t}\n\n\t@Override\n\tprotected void engineSetSeed(byte[] seed) {\n\t\tdr.seed(seed, 0, seed.length);\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/KeccakRnd256.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport java.security.SecureRandomSpi;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.DuplexRandom;\n\n/**\n * A cryptographic random implementation providing 256-bit \n * security suitable for generating long term keys.\n * \n * Forgets the previous state after every call to \n * nextBytes. \n */\npublic final class KeccakRnd256 extends SecureRandomSpi {\n\tprivate final DuplexRandom dr = new DuplexRandom(509);\n\t\n\t@Override\n\tprotected byte[] engineGenerateSeed(int len) {\n\t\tbyte[] rv = new byte[len];\n\t\t\n\t\tDuplexRandom.getSeedBytes(rv, 0, len);\n\t\t\n\t\treturn rv;\n\t\t\n\t}\n\n\t@Override\n\tprotected void engineNextBytes(byte[] buf) {\n\t\tdr.getBytes(buf, 0, buf.length);\n\t\tdr.forget();\n\t}\n\n\t@Override\n\tprotected void engineSetSeed(byte[] seed) {\n\t\tdr.seed(seed, 0, seed.length);\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/RawKey.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport java.security.InvalidKeyException;\n\nimport javax.crypto.SecretKey;\n\npublic abstract class RawKey implements SecretKey {\n\n\tprivate byte[] rawKey;\n\t\n\tpublic RawKey() {\n\t\t\n\t}\n\t\n\tpublic RawKey(byte[] rawKey) throws InvalidKeyException {\n\t\tsetRaw(rawKey);\n\t}\n\n\t@Override\n\tpublic byte[] getEncoded() {\n\t\treturn rawKey;\n\t}\n\t\n\t\n\tpublic void setRaw(byte[] rawKey) throws InvalidKeyException {\n\t\tif(rawKey == null || rawKey.length < getMinKeyLength() || rawKey.length >= getMaxKeyLength())\n\t\t\tthrow new InvalidKeyException(\"Key must be between \"+getMinKeyLength() + \" and \" + getMaxKeyLength() + \" bytes. \");\n\t\tthis.rawKey = rawKey;\n\t}\n\n\t@Override\n\tpublic String getFormat() {\n\t\treturn \"RAW\";\n\t}\n\t\n\tpublic int getMinKeyLength() {\n\t\treturn 16;\n\t}\n\t\n\tpublic int getMaxKeyLength() {\n\t\treturn Integer.MAX_VALUE;\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/Shake128Key.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.Constants;\n\npublic class Shake128Key extends RawKey {\n\n\t@Override\n\tpublic String getAlgorithm() {\t\t\n\t\treturn Constants.SHAKE128_STREAM_CIPHER;\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/Shake128StreamCipher.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.XOFParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.KeccakSponge;\n\npublic final class Shake128StreamCipher extends AbstractSpongeStreamCipher {\n\tprivate KeccakSponge sponge;\n\n\t@Override\n\tKeccakSponge getSponge() {\n\t\tif(sponge == null) {\n\t\t\tsponge = new KeccakSponge(XOFParameterSet.SHAKE128);\n\t\t}\n\t\treturn sponge;\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/Shake256Key.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.Constants;\n\npublic class Shake256Key extends RawKey {\n\n\t@Override\n\tpublic String getAlgorithm() {\n\t\treturn Constants.SHAKE256_STREAM_CIPHER;\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/spi/Shake256StreamCipher.java",
    "content": "/*\n * Copyright (c) 2024 - Mimiclone, Inc. \n *\n\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.asymmetric.mlkem.fips202.keccak.spi;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.XOFParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.KeccakSponge;\n\npublic class Shake256StreamCipher extends AbstractSpongeStreamCipher {\n\n\tprivate KeccakSponge sponge;\n\n\t@Override\n\tKeccakSponge getSponge() {\n\t\tif(sponge == null) {\n\t\t\tsponge = new KeccakSponge(XOFParameterSet.SHAKE256);\n\t\t}\n\t\treturn sponge;\n\t}\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips202/keccak/sponge/MimicloneKeccakSponge.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips202.keccak.sponge;\n\nimport peergos.server.crypto.asymmetric.mlkem.CryptoUtils;\n\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\nimport static peergos.server.crypto.asymmetric.mlkem.CryptoUtils.mod;\n\npublic class MimicloneKeccakSponge {\n\n    // NOTE:\n    // We are only implementing SHAKE128 and SHAKE256 XOF function to support FIPS203.\n    // The original spec operates on bit strings, but the specific rate and capacity we\n    // support will only use bit strings that align on byte boundaries.\n\n    final int bitRate;\n    final int byteRate;\n    final int bitCapacity;\n    final int byteCapacity;\n\n    private MimicloneKeccakSponge(int bitLength) {\n        bitRate = 1600 - (bitLength << 1);\n        byteRate = bitRate >> 3;\n        bitCapacity = 1600 - bitRate;\n        byteCapacity = bitCapacity >> 3;\n    }\n\n    private long[] state = new long[25];\n    private long[] absorbedState = new long[25];\n    private byte[] dataQueue = new byte[192];\n    private int bitsInQueue, fixedOutputLength;\n    private boolean squeezing;\n\n    public static MimicloneKeccakSponge create(int bitLength) {\n        return new MimicloneKeccakSponge(bitLength);\n    }\n\n    // NOTE: This implementation assumes we will always pad a message, even if the message\n    // falls exactly on a rate boundary\n    byte[] pad(int messageLengthInBytes) {\n\n        int paddingBytes = byteRate - mod(messageLengthInBytes, byteRate);\n\n        // Message does not need padding\n        if (paddingBytes == 0) {\n            if (messageLengthInBytes >= byteRate) {\n                return new byte[0];\n            } else {\n                return new byte[] {\n                        (byte)0x80L,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                        0x00, 0x00, 0x00, 0x00,\n                        0x01,\n                };\n            }\n\n        }\n\n        // Message needs single padding byte\n        else if (paddingBytes == 1) {\n            return new byte[]{\n                    (byte) 0b10000001L\n            };\n        }\n\n        // Message needs at least two padding bytes\n        else if (paddingBytes > 1) {\n            byte firstByte = (byte) 0b10000000L;\n            byte middleByte = (byte) 0b00000000L;\n            byte lastByte = (byte) 0b00000001L;\n            ByteBuffer buffer = ByteBuffer.allocate(paddingBytes);\n            buffer.put(firstByte);\n            if (paddingBytes > 2) {\n                for (int i = 0; i < paddingBytes - 2; i++) {\n                    buffer.put(middleByte);\n                }\n            }\n            buffer.put(lastByte);\n            return buffer.array();\n        }\n\n        // Something went wonky, our modulus operation returned a negative number\n        else {\n            throw new IllegalStateException(\"Modulus operation returned a negative value: \" + paddingBytes);\n        }\n\n    }\n\n    void absorb(byte[] data, int offset, int length) {\n\n        // Ensure bit queue has even length\n        if ((bitsInQueue & 7) != 0) {\n            throw new IllegalStateException(\"Attempt to absorb with odd length queue\");\n        }\n\n        // Ensure we aren't currently trying to squeeze the sponge\n        if (squeezing) {\n            throw new IllegalStateException(\"Attempt to absorb while squeezing\");\n        }\n\n        // Right shift by 3 is division by 2^3 = 8\n        var bytesInQueue = bitsInQueue >> 3;\n\n        // Determine number of bytes available in the queue\n        var available = byteRate - bytesInQueue;\n\n        // Ensure we have enough room in the data queue\n        if (length < available) {\n\n            // Copy the data into the data queue\n            System.arraycopy(data, offset, dataQueue, bytesInQueue, length);\n\n            // Add 8*length to the bits in the queue\n            bitsInQueue += length << 3;\n\n        }\n\n        var count = 0;\n\n        // If there is any room available in the data queue\n        if (bytesInQueue > 0) {\n\n            // Fill the available space with the data\n            System.arraycopy(data, offset, dataQueue, bytesInQueue, available);\n\n            count += available;\n            // TODO: KeccakAbsorb the data\n        }\n\n        int remaining;\n        while ((remaining = length - count) >= byteRate) {\n            // TODO: KeccakAbsorb the data\n            count += byteRate;\n        }\n\n        // Put the rest of the data into the queue\n        System.arraycopy(data, offset + count, dataQueue, 0, remaining);\n        bitsInQueue = remaining << 3;\n    }\n\n    void padAndSwitchToSqueezingPhase() {\n        dataQueue[bitsInQueue >> 3] |= (byte)(1 << (bitsInQueue & 7));\n\n        if (++bitsInQueue == bitRate) {\n            // TODO: KeccakAbsorb (dataQueue, 0)\n        } else {\n            int full = bitsInQueue >> 6;\n            int partial = bitsInQueue & 63;\n            int off = 0;\n            for (int i = 0; i < full; i++) {\n                state[i] ^= CryptoUtils.bytesToLong(ByteOrder.LITTLE_ENDIAN, dataQueue, off);\n                off += 8;\n            }\n\n            if (partial > 0) {\n                long mask = (1L << partial) - 1L;\n                state[full] ^= CryptoUtils.bytesToLong(ByteOrder.LITTLE_ENDIAN, dataQueue, off) & mask;\n            }\n        }\n\n        // XOR the most significant bit of the state\n        state[(bitRate - 1) >> 6] ^= (1L << 63);\n\n        bitsInQueue = 0;\n        squeezing = true;\n    }\n\n    void squeeze(byte[] output, int offset, long outputBitLength) {\n\n        if (!squeezing) {\n            padAndSwitchToSqueezingPhase();\n\n            // Save the absorbed state so we can squeeze multiple times\n            System.arraycopy(state, 0, absorbedState, 0, 25);\n        }\n\n        // Ensure we are squeezing bytes\n        if ((outputBitLength & 7L) != 0L) {\n            throw new IllegalArgumentException(\"Squeezing requires the outputBitLength be a multiple of 8\");\n        }\n\n        long i = 0;\n        bitsInQueue = 0;\n\n        // Copy the absorbed state into the state\n        System.arraycopy(absorbedState, 0, state, 0, 25);\n\n        while (i < outputBitLength) {\n            if (bitsInQueue == 0) {\n                // TODO: KeccakExtract();\n            }\n\n            int partialBlock = (int) Math.min(bitsInQueue, outputBitLength - i);\n            System.arraycopy(dataQueue, (bitRate - bitsInQueue) >> 3, output, offset + (int)(i >> 3), partialBlock >> 3);\n\n            bitsInQueue -= partialBlock;\n            i += partialBlock;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/FIPS203.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.*;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.check.KeyPairCheckException;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.KeyPairGenerationException;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\n\npublic interface FIPS203 {\n\n    /**\n     * Convenience method to verify the ParameterSet of the underlyig implementation.\n     */\n    ParameterSet getParameterSet();\n\n    /**\n     * Implementation of the KeyGen algorithm as specified in the FIPS203 Specification\n     */\n    KeyPair generateKeyPair() throws KeyPairGenerationException;\n\n    void keyPairCheck(KeyPair keyPair) throws KeyPairCheckException;\n\n    /**\n     * Implementation of the Encaps algorithm as specified in the FIPS203 Specification\n     * @return An array of exactly 32 bytes representing the encapsulated cyphertext\n     */\n    Encapsulation encapsulate(EncapsulationKey key);\n\n    /**\n     * Implementation of the Decaps algorithm as specified in the FIPS203 Specification\n     * @return An array of exactly 32 bytes representing the decapsulated cleartext.\n     */\n    SharedSecretKey decapsulate(DecapsulationKey key, CipherText cipherText);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/FIPS203Exception.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203;\n\npublic class FIPS203Exception extends RuntimeException {\n    public FIPS203Exception(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/MimicloneFIPS203.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203;\n\nimport peergos.server.crypto.asymmetric.mlkem.MlkemSecureRandom;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.Decapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.mlkem.MLKEMDecapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.mlkem.MLKEMEncapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.EncapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.check.KeyPairCheckException;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.KeyPairGeneration;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.KeyPairGenerationException;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.mlkem.MLKEMKeyPairGenerator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\n\nimport java.security.SecureRandom;\n\nimport static peergos.server.crypto.asymmetric.mlkem.CryptoUtils.zero;\n\npublic class MimicloneFIPS203 implements FIPS203 {\n\n    public static FIPS203 create(ParameterSet params) {\n        return new MimicloneFIPS203(params);\n    }\n\n\n\n    // FIPS 203 Parameter Set assigned\n    private final ParameterSet parameterSet;\n    private final SecureRandom secureRandom;\n    private final KeyPairGeneration keyPairGenerator;\n    private final Encapsulator encapsulator;\n    private final Decapsulator decapsulator;\n\n    private MimicloneFIPS203(ParameterSet parameterSet) {\n\n        // Assign the chosen parameter set\n        this.parameterSet = parameterSet;\n\n        secureRandom = MlkemSecureRandom.getSecureRandom(parameterSet.getMinSecurityStrength());\n\n        // Initialize the Key Pair Generator\n        this.keyPairGenerator = MLKEMKeyPairGenerator.create(parameterSet);\n\n        // Initialize the Encapsulator\n        this.encapsulator = MLKEMEncapsulator.create(parameterSet);\n\n        // Initialize the Decapsulator\n        this.decapsulator = MLKEMDecapsulator.create(parameterSet);\n\n    }\n\n    @Override\n    public ParameterSet getParameterSet() {\n        return parameterSet;\n    }\n\n    /**\n     * Implements Algorithm 19 (ML-KEM.KeyGen) of the FIPS203 Specification\n     *\n     * @return A FIPS203KeyPair instance.\n     */\n    @Override\n    public KeyPair generateKeyPair() throws KeyPairGenerationException {\n\n        // FIPS203:Algorithm19:Line1\n        // Generate 'd', a value of 32 random bytes\n        byte[] d = new byte[32];\n        secureRandom.nextBytes(d);\n\n        // FIPS203:Algorithm19:Line2\n        // Generate 'z', a value of 32 random bytes\n        byte[] z = new byte[32];\n        secureRandom.nextBytes(z);\n\n        // FIPS203:Algorithm19:Line3\n        // The spec requires a null check here for d and z, but it isn't possible\n        // for them to be null.  Checking would raise a compiler error\n\n        // Invoke Key Generation\n        KeyPair keyPair = keyPairGenerator.generateKeyPair(d, z);\n\n        // ZERO: d\n        zero(d);\n\n        // ZERO: z\n        zero(z);\n\n        // Return wrapped KeyPair\n        return keyPair;\n\n    }\n\n    /**\n     * Implements Algorithm\n     * @param keyPair\n     * @throws KeyPairCheckException\n     */\n    @Override\n    public void keyPairCheck(KeyPair keyPair) throws KeyPairCheckException {\n        // TODO: Implement key pair checking\n        throw new KeyPairCheckException(\"Key pair checking has not yet been implemented.\");\n    }\n\n    /**\n     * Implements Algorithm 20 (ML-KEM.Encaps) of the FIPS203 Specification.\n     *\n     * This generates 32-bytes of entropy and passes it along to the internal implementation.\n     *\n     * @param key An {@code EncapsulationKey} instance.\n     * @return A {@code SharedSecretKey}\n     */\n    @Override\n    public Encapsulation encapsulate(EncapsulationKey key) {\n\n        // Generate 32 bytes of securely random entropy\n        byte[] m = new byte[32];\n        secureRandom.nextBytes(m);\n\n        // The spec requires a null check here for m, but Java is designed such that this isn't possible.\n\n        // Perform the encapsulation\n        Encapsulation encapsulation = encapsulator.encapsulate(key, m); // LAST USE: m\n\n        // ZERO: m\n        zero(m);\n\n        // Return wrapped result value\n        return encapsulation;\n\n    }\n\n    /**\n     * Implements Algorithm 21 (ML-KEM.Decaps) of the FIPS203 Specification.\n     * No randomness is generated so this is a simple pass-through to the internal implementation.\n     *\n     * @param key\n     * @param cipherText\n     * @return\n     */\n    @Override\n    public SharedSecretKey decapsulate(DecapsulationKey key, CipherText cipherText) {\n        return decapsulator.decapsulate(key, cipherText);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/ParameterSet.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203;\n\npublic enum ParameterSet {\n\n    ML_KEM_512(\n            \"ML-KEM-512\",\n            2,\n            3,\n            2,\n            10,\n            4,\n            128,\n            800,\n            1632,\n            768,\n            32\n    ),\n\n    ML_KEM_768(\n            \"ML-KEM-768\",\n            3,\n            2,\n            2,\n            10,\n            4,\n            192,\n            1184,\n            2400,\n            1088,\n            32\n    ),\n\n    ML_KEM_1024(\n            \"ML-KEM-1024\",\n            4,\n            2,\n            2,\n            11,\n            5,\n            256,\n            1568,\n            3168,\n            1568,\n            32\n    );\n\n    /**\n     * The OID name of the parameter set.  This name is common across all implementations\n     * regardless of the naming conventions of the programming language used.\n     */\n    private final String name;\n\n    /**\n     * The n value is not actually part of the parameter set, it is a global variable\n     * across the entire algorithm.  However, it is useful to have it available in all\n     * the same places the parameters are used, so we set it here with no variance between\n     * enum instances.\n     */\n    private final int n = 256;\n\n    /**\n     * The q value is not actually part of the parameter set, it is a global variable\n     * across the entire algorithm.  However, it is useful to have it available in all\n     * the same places the parameters are used, so we set it here with no variance between\n     * enum instances.\n     */\n    private final int q = 3329;\n\n    /**\n     * From FIPS203 Section 8:\n     * The parameter k determines the dimensions of the matrix (A hat) that appears in\n     * K-PKE.KeyGen and K-PKE.Encrypt.  It also determines the dimensions of vectors s\n     * and e in K-PKE.KeyGen and the dimensions of vectors y and e1 in K-PKE.Encrypt.\n     */\n    private final int k;\n\n    /**\n     * Specifies the distribution of vectors s and e in K-PKE.KeyGen and the vector y\n     * in K-PKE.Encrypt\n     */\n    private final int eta1;\n\n    /**\n     * Specifies the distribution of vectors e1 and e2 in K-PKE.Encrypt\n     */\n    private final int eta2;\n\n    /**\n     * Used in the functions Compress, Decompress, ByteEncode, ByteDecode\n     */\n    private final int du;\n\n    /**\n     * Used in the functions Compress, Decompress, ByteEncode, ByteDecode\n     */\n    private final int dv;\n\n    /**\n     * Minimum security strength for hash and XOF functions\n     */\n    private final int minSecurityStrength;\n\n    /**\n     * Length of the Encapsulation Key in bytes\n     */\n    private final int encapsulationKeyLength;\n\n    /**\n     * Length of the Decapsulation Key in bytes\n     */\n    private final int decapsulationKeyLength;\n\n    /**\n     * Length of the generated Ciphertext in bytes\n     */\n    private final int ciphertextLength;\n\n    /**\n     * Length of the Shared Secret Key in bytes\n     */\n    private final int sharedSecretKeyLength;\n\n    ParameterSet(String name, int k, int eta1, int eta2, int du, int dv, int minSecurityStrength, int encapsulationKeyLength, int decapsulationKeyLength, int ciphertextLength, int sharedSecretKeyLength) {\n        this.name = name;\n        this.k = k;\n        this.eta1 = eta1;\n        this.eta2 = eta2;\n        this.du = du;\n        this.dv = dv;\n        this.minSecurityStrength = minSecurityStrength;\n        this.encapsulationKeyLength = encapsulationKeyLength;\n        this.decapsulationKeyLength = decapsulationKeyLength;\n        this.ciphertextLength = ciphertextLength;\n        this.sharedSecretKeyLength = sharedSecretKeyLength;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public int getK() {\n        return k;\n    }\n\n    public int getMinSecurityStrength() {\n        return minSecurityStrength;\n    }\n\n    public int getQ() {\n        return q;\n    }\n\n    public int getN() {\n        return n;\n    }\n\n    public int getEta1() {\n        return eta1;\n    }\n\n    public int getEta2() {\n        return eta2;\n    }\n\n    public int getDu() {\n        return du;\n    }\n\n    public int getDv() {\n        return dv;\n    }\n\n    public int getSharedSecretKeyLength() {\n        return sharedSecretKeyLength;\n    }\n\n    public int getCiphertextLength() {\n        return ciphertextLength;\n    }\n\n    public int getDecapsulationKeyLength() {\n        return decapsulationKeyLength;\n    }\n\n    public int getEncapsulationKeyLength() {\n        return encapsulationKeyLength;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/codec/Codec.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.codec;\n\npublic interface Codec {\n\n    byte[] byteEncode(int d, int[] f);\n\n    int[] compress(int d, int[] x);\n\n    int[] byteDecode(int d, byte[] f);\n\n    int[] decompress(int d, int[] y);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/codec/MLKEMCodec.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.codec;\n\nimport peergos.server.crypto.asymmetric.mlkem.CryptoUtils;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\n\nimport java.util.BitSet;\n\nimport static peergos.server.crypto.asymmetric.mlkem.CryptoUtils.mod;\nimport static peergos.server.crypto.asymmetric.mlkem.CryptoUtils.pow;\n\npublic class MLKEMCodec implements Codec {\n\n    private final ParameterSet parameterSet;\n\n    public MLKEMCodec(ParameterSet parameterSet) {\n        this.parameterSet = parameterSet;\n    }\n\n    public static MLKEMCodec create(ParameterSet parameterSet) {\n        return new MLKEMCodec(parameterSet);\n    }\n\n\n    /**\n     * Encodes the coefficients of a 256-term polynomial (a module lane) into a packed set of bytes where each\n     * value takes up {@code d} bits that may not align to a byte boundary.\n     * Note that while the algorithm accepts any value {@code d} from 1 to 12, it is realistically only used\n     * for the values 1, 12 and the Du and Dv values from the given parameter set.\n     *\n     * @param d An {@code int} representing the number of digits to encode.\n     * @param f An {@code int} array representing the coefficients of a polynomial (modulo q) in a lane.\n     * @return A {@code byte} array composed of packed polynomial coefficients.\n     */\n    @Override\n    public byte[] byteEncode(int d, int[] f) {\n\n        // Declare bitset\n        int bitCapacity = 256 * d;\n        BitSet b = new BitSet(bitCapacity);\n\n        // Iterate over the input array\n        for (int i = 0; i < 256; i++) {\n\n            // Extract a single integer (modulo m) -> Assumes big endian bit order and 32-bit ints\n            int a = f[i] & CryptoUtils.INT_BIT_MASKS[d];\n\n            // Iterate over the bits in the integer\n            for (int j = 0; j < d; j++) {\n\n                // Calculate the bit index for the operation\n                int bitIndex = i * d + j;\n\n                // Set the bit at the calculated bit index to a mod 2 which is the least significant bit of a\n                b.set(bitIndex, (a & CryptoUtils.INT_BIT_MASKS[1]) != 0);\n\n                // Update a\n                a = (a - (b.get(bitIndex) ? 1 : 0))/2;\n\n            }\n\n        }\n\n        // Convert the bitset to a byte array\n        byte[] result = new byte[bitCapacity/8];\n        byte[] bitsAsBytes = b.toByteArray();\n        System.arraycopy(bitsAsBytes, 0, result, 0, bitsAsBytes.length);\n        return result;\n\n    }\n\n    @Override\n    public int[] compress(int d, int[] x) {\n\n        // return ((x * d.Exp2() + (_param.Q / 2)) / _param.Q);\n\n        int q = parameterSet.getQ();\n        int[] result = new int[x.length];\n        for (int i = 0; i < x.length; i++) {\n            result[i] = ((x[i] * pow(2, d)) + (q >> 1)) / q;\n        }\n\n        return result;\n    }\n\n    @Override\n    public int[] byteDecode(int d, byte[] f) {\n\n        BitSet bits = BitSet.valueOf(f);\n        int[] result = new int[256];\n        int dPow = pow(2, d);\n        int q = parameterSet.getQ();\n        int m = (d == 12) ? q : dPow;\n\n        for (int i = 0; i < 256; i++) {\n            for (int j = 0; j < d; j++) {\n                int jPow = pow(2, j);\n                result[i] = mod(result[i] + (bits.get(i * d + j) ? jPow : 0), m);\n            }\n        }\n\n        return result;\n    }\n\n    @Override\n    public int[] decompress(int d, int[] y) {\n\n        int[] result = new int[y.length];\n\n        int q = parameterSet.getQ();\n        int d2 = pow(2, d);\n        int d2Half = pow(2, d) >>> 1;\n\n        for (int i = 0; i < y.length; i++) {\n            result[i] = (y[i] * q + d2Half) / d2;\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/decaps/DecapsulationException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.decaps;\n\npublic class DecapsulationException extends RuntimeException {\n    public DecapsulationException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/decaps/Decapsulator.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.decaps;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\n\npublic interface Decapsulator {\n\n    SharedSecretKey decapsulate(DecapsulationKey key, CipherText cipherText) throws DecapsulationException;\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/decaps/mlkem/MLKEMDecapsulator.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.decaps.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.DecapsulationException;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decrypt.Decryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decrypt.kpke.KPKEDecryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encrypt.Encryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encrypt.kpke.KPKEEncryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.Hash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.MLKEMHash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem.MLKEMSharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\n\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\n\npublic class MLKEMDecapsulator implements peergos.server.crypto.asymmetric.mlkem.fips203.decaps.Decapsulator {\n\n    private final ParameterSet parameterSet;\n\n    private final Hash hash;\n    private final Encryptor encryptor;\n    private final Decryptor decryptor;\n\n    private MLKEMDecapsulator(ParameterSet parameterSet, Hash hash, Encryptor encryptor, Decryptor decryptor) {\n        this.parameterSet = parameterSet;\n        this.hash = hash;\n        this.encryptor = encryptor;\n        this.decryptor = decryptor;\n    }\n\n    public static MLKEMDecapsulator create(ParameterSet parameterSet) {\n        return new MLKEMDecapsulator(\n                parameterSet,\n                MLKEMHash.create(parameterSet),\n                KPKEEncryptor.create(parameterSet),\n                KPKEDecryptor.create(parameterSet)\n        );\n    }\n\n    @Override\n    public SharedSecretKey decapsulate(DecapsulationKey key, CipherText cipherText) throws DecapsulationException {\n\n        // Wrap a copy of the decapsulation key into a buffer\n        ByteBuffer dkBuffer = ByteBuffer.wrap(key.getBytes());\n\n        // Extract the PKE decryption key (first 384*k bytes)\n        byte[] dkPKE = new byte[384*parameterSet.getK()];\n        dkBuffer.get(dkPKE);\n\n        // Extract the PKE encryption key (next 384*k + 32 bytes)\n        byte[] ekPKE = new byte[384*parameterSet.getK() + 32];\n        dkBuffer.get(ekPKE);\n\n        // Extract the PKE encryption key hash (next 32 bytes)\n        byte[] h = new byte[32];\n        dkBuffer.get(h);\n\n        // Extract the implicit rejection value (next 32 bytes)\n        byte[] z = new byte[32];\n        dkBuffer.get(z);\n\n        // Extract the cipherText bytes\n        byte[] c = cipherText.getBytes();\n\n        // Decrypt the ciphertext\n        byte[] mPrime = decryptor.decrypt(dkPKE, c);\n\n        // Hash the concatenation of the shared secret and its own hash\n        ByteBuffer integrityCheckInputBuffer = ByteBuffer.allocate(mPrime.length + h.length)\n                        .put(mPrime).put(h);\n        ByteBuffer integrityCheckOutputBuffer = ByteBuffer.wrap(hash.gHash(integrityCheckInputBuffer.array().clone()));\n\n        // Split out kPrime\n        byte[] kPrime = new byte[32];\n        integrityCheckOutputBuffer.get(kPrime);\n\n        // Split out rPrime\n        byte[] rPrime = new byte[32];\n        integrityCheckOutputBuffer.get(rPrime);\n\n        // Generate kBar (implicit rejection flag)\n        ByteBuffer implicitRejectionBuffer = ByteBuffer.allocate(z.length + c.length).put(z).put(c);\n        byte[] kBar = hash.jHash(implicitRejectionBuffer.array().clone());\n\n        // K-PKE encrypt the recovered shared secret and the calculated randomness kPrime\n        byte[] cPrime = encryptor.encrypt(ekPKE, mPrime, rPrime);\n\n        // Check integrity of calculated values\n        if (!Arrays.equals(c, cPrime)) {\n\n            // Set the implicit rejection flag\n            kPrime = kBar.clone();\n\n            // Destroy the internal implicit rejection value\n            // NOTE: Java uses JVM-managed garbage collection so we must overwrite the value to guarantee\n            //       value destruction.\n            Arrays.fill(kBar, (byte)0L);\n            kBar = null;\n\n        }\n\n        // Construct and return the calculated shared secret key\n        return MLKEMSharedSecretKey.create(kPrime);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/decaps/provider/MLKEMDecapsulatorProvider.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.decaps.provider;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.Decapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.mlkem.MLKEMDecapsulator;\n\nimport javax.crypto.DecapsulateException;\nimport javax.crypto.KEMSpi;\nimport javax.crypto.SecretKey;\nimport java.security.PrivateKey;\nimport java.security.spec.AlgorithmParameterSpec;\n\npublic class MLKEMDecapsulatorProvider implements KEMSpi.DecapsulatorSpi {\n\n    private final Decapsulator decapsulator;\n\n    public MLKEMDecapsulatorProvider(Decapsulator decapsulator) {\n        this.decapsulator = decapsulator;\n    }\n\n    public static MLKEMDecapsulatorProvider getInstance(PrivateKey privateKey, AlgorithmParameterSpec spec) {\n        return new MLKEMDecapsulatorProvider(MLKEMDecapsulator.create(ParameterSet.ML_KEM_768));\n    }\n\n    @Override\n    public SecretKey engineDecapsulate(byte[] encapsulation, int from, int to, String algorithm) throws DecapsulateException {\n\n        return null;\n\n    }\n\n    @Override\n    public int engineSecretSize() {\n        return 0;\n    }\n\n    @Override\n    public int engineEncapsulationSize() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/decrypt/DecryptionException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.decrypt;\n\npublic class DecryptionException extends RuntimeException {\n    public DecryptionException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/decrypt/Decryptor.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.decrypt;\n\npublic interface Decryptor {\n\n    byte[] decrypt(byte[] dkPKE, byte[] cipherText);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/decrypt/kpke/KPKEDecryptor.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.decrypt.kpke;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.codec.Codec;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.codec.MLKEMCodec;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decrypt.Decryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.transform.MLKEMTransformer;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.transform.Transformer;\n\nimport java.nio.ByteBuffer;\n\npublic class KPKEDecryptor implements Decryptor {\n\n    private final ParameterSet parameterSet;\n    private final Codec codec;\n    private final Transformer ntt;\n\n    private KPKEDecryptor(ParameterSet parameterSet, Codec codec, Transformer ntt) {\n        this.parameterSet = parameterSet;\n        this.codec = codec;\n        this.ntt = ntt;\n    }\n\n    public static KPKEDecryptor create(ParameterSet parameterSet) {\n        return new KPKEDecryptor(\n                parameterSet,\n                MLKEMCodec.create(parameterSet),\n                MLKEMTransformer.create(parameterSet)\n        );\n    }\n\n    @Override\n    public byte[] decrypt(byte[] dkPKE, byte[] cipherText) {\n\n        // Wrap cipherText in a buffer\n        ByteBuffer cipherTextBuffer = ByteBuffer.wrap(cipherText);\n\n        // ALGO 1: Extract c1\n        byte[] c1 = new byte[32 * parameterSet.getDu() * parameterSet.getK()];\n        cipherTextBuffer.get(c1);\n\n        // ALGO 2: Extract c2\n        byte[] c2 = new byte[32 * parameterSet.getDv()];\n        cipherTextBuffer.get(c2);\n\n        // Setup up processing buffer c1\n        ByteBuffer c1ChunkBuffer = ByteBuffer.wrap(c1);\n\n        // ALGO 3: Calculate uPrime\n        int[][] uPrime = new int[parameterSet.getK()][];\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            byte[] c1Chunk = new byte[32 * parameterSet.getDu()];\n            c1ChunkBuffer.get(c1Chunk);\n            uPrime[i] = codec.decompress(\n                    parameterSet.getDu(),\n                    codec.byteDecode(\n                            parameterSet.getDu(),\n                            c1Chunk.clone()\n                    )\n            );\n        }\n\n        // ALGO 4: Calculate vPrime\n        int[] vPrime = codec.decompress(\n                parameterSet.getDv(),\n                codec.byteDecode(\n                        parameterSet.getDv(),\n                        c2.clone()\n                )\n        );\n\n        // Wrap dkPKE in a buffer for chunking\n        ByteBuffer dkPKEChunkBuffer = ByteBuffer.wrap(dkPKE);\n        byte[] dkPKEChunk = new byte[384];\n\n        // ALGO 5: Calculate sHat\n        int[][] sHat = new int[parameterSet.getK()][];\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            dkPKEChunkBuffer.get(dkPKEChunk);\n            sHat[i] = codec.byteDecode(12, dkPKEChunk.clone());\n        }\n\n        // ALGO 6: Calculate w\n        int[][] uPrimeNTT = new int[parameterSet.getK()][];\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            uPrimeNTT[i] = ntt.transform(uPrime[i]);\n        }\n        int[] w = ntt.arraySubtract(vPrime, ntt.inverse(ntt.vectorTransposeMultiply(sHat, uPrimeNTT)));\n\n        // ALGO 7&8: Compress, encode and return plaintext\n        return codec.byteEncode(1, codec.compress(1, w));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encaps/Encapsulation.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encaps;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\n\npublic interface Encapsulation {\n\n    SharedSecretKey getSharedSecretKey();\n\n    CipherText getCipherText();\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encaps/EncapsulationException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encaps;\n\npublic class EncapsulationException extends RuntimeException {\n    public EncapsulationException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encaps/Encapsulator.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encaps;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.EncapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\n\npublic interface Encapsulator {\n\n    Encapsulation encapsulate(EncapsulationKey ek, byte[] entropy) throws EncapsulationException;\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encaps/mlkem/MLKEMEncapsulation.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encaps.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem.MLKEMSharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.MLKEMCipherText;\n\npublic class MLKEMEncapsulation implements Encapsulation {\n\n    private final SharedSecretKey sharedSecretKey;\n    private final CipherText cipherText;\n\n    public MLKEMEncapsulation(SharedSecretKey sharedSecretKey, CipherText cipherText) {\n        this.sharedSecretKey = sharedSecretKey;\n        this.cipherText = cipherText;\n    }\n\n    static MLKEMEncapsulation build(byte[] sharedSecretKeyBytes, byte[] cipherTextBytes) {\n        SharedSecretKey secretKey = MLKEMSharedSecretKey.create(sharedSecretKeyBytes);\n        CipherText cipherText = MLKEMCipherText.create(cipherTextBytes);\n        return new MLKEMEncapsulation(secretKey, cipherText);\n    }\n\n    @Override\n    public SharedSecretKey getSharedSecretKey() {\n        return sharedSecretKey;\n    }\n\n    @Override\n    public CipherText getCipherText() {\n        return cipherText;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encaps/mlkem/MLKEMEncapsulator.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encaps.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.EncapsulationException;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encrypt.Encryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encrypt.kpke.KPKEEncryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.Hash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.MLKEMHash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.EncapsulationKey;\n\nimport java.nio.ByteBuffer;\n\npublic class MLKEMEncapsulator implements peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulator {\n\n    private final ParameterSet parameterSet;\n    private final Hash hash;\n    private final Encryptor encryptor;\n\n    public MLKEMEncapsulator(ParameterSet parameterSet, Hash hash, Encryptor encryptor) {\n        this.parameterSet = parameterSet;\n        this.hash = hash;\n        this.encryptor = encryptor;\n    }\n\n    public static MLKEMEncapsulator create(ParameterSet parameterSet) {\n        return new MLKEMEncapsulator(\n                parameterSet,\n                MLKEMHash.create(parameterSet),\n                KPKEEncryptor.create(parameterSet)\n        );\n    }\n\n    /**\n     * Implements Algorithm 17 (ML-KEM.Encaps_internal) of the FIPS203 Standard\n     * @param ek\n     * @param entropy\n     * @return\n     * @throws EncapsulationException\n     */\n    @Override\n    public Encapsulation encapsulate(EncapsulationKey ek, byte[] entropy) throws EncapsulationException {\n\n        // Derive encapsulation key hash\n        byte[] ekHash = hash.hHash(ek.getBytes());\n\n        // Concatenate entropy and encapsulation key hash\n        byte[] entropyAndKeyHash = ByteBuffer.allocate(64).put(entropy).put(ekHash).array();\n\n        // Generate the shared secret and randomness\n        byte[] sharedSecretAndRandom = hash.gHash(entropyAndKeyHash);\n\n        // Split out the shared secret and randomness\n        ByteBuffer sharedSecretAndRandomBuffer = ByteBuffer.wrap(sharedSecretAndRandom);\n\n        // Split out shared secret\n        byte[] sharedSecretBytes = new byte[32];\n        sharedSecretAndRandomBuffer.get(sharedSecretBytes);\n\n        // Split out random\n        byte[] random = new byte[32];\n        sharedSecretAndRandomBuffer.get(random);\n\n        // Generate cipherText bytes\n        byte[] cipherTextBytes = encryptor.encrypt(ek.getBytes(), entropy, random);\n\n        return MLKEMEncapsulation.build(sharedSecretBytes, cipherTextBytes);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encaps/provider/MLKEMEncapsulationProvider.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encaps.provider;\n\nimport javax.crypto.KEM;\nimport javax.crypto.KEMSpi;\n\npublic class MLKEMEncapsulationProvider implements KEMSpi.EncapsulatorSpi {\n\n    @Override\n    public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) {\n        return null;\n    }\n\n    @Override\n    public int engineSecretSize() {\n        return 0;\n    }\n\n    @Override\n    public int engineEncapsulationSize() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encrypt/EncryptionException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encrypt;\n\npublic class EncryptionException extends RuntimeException {\n    public EncryptionException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encrypt/Encryptor.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encrypt;\n\npublic interface Encryptor {\n\n    byte[] encrypt(byte[] ekPKE, byte[] message, byte[] random);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/encrypt/kpke/KPKEEncryptor.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.encrypt.kpke;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.codec.Codec;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.codec.MLKEMCodec;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encrypt.Encryptor;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.Hash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.MLKEMHash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.sample.MLKEMSampler;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.sample.Sampler;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.transform.MLKEMTransformer;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.transform.Transformer;\n\nimport java.nio.ByteBuffer;\n\npublic class KPKEEncryptor implements Encryptor {\n\n    private final ParameterSet parameterSet;\n    private final Codec codec;\n    private final Hash hash;\n    private final Sampler sampler;\n    private final Transformer ntt;\n\n    public KPKEEncryptor(ParameterSet parameterSet, Codec codec, Hash hash, Sampler sampler, Transformer ntt) {\n        this.parameterSet = parameterSet;\n        this.codec = codec;\n        this.hash = hash;\n        this.sampler = sampler;\n        this.ntt = ntt;\n    }\n\n    public static KPKEEncryptor create(ParameterSet parameterSet) {\n        return new KPKEEncryptor(\n                parameterSet,\n                MLKEMCodec.create(parameterSet),\n                MLKEMHash.create(parameterSet),\n                MLKEMSampler.create(parameterSet),\n                MLKEMTransformer.create(parameterSet)\n        );\n    }\n\n    /**\n     * Implements Algorithm 14  (K-PKE.Encrypt) of the FIPS203 Standard.\n     * @param ekPKE An array of {@code 384*k+32} bytes representing the encryption key.\n     * @param message An array of 32 bytes representing the message to encrypt.\n     * @param random An array of 32 bytes representing the entropy into the system.\n     * @return A {@code 32(du*k + dv)} byte array representing the cipherText.\n     */\n    @Override\n    public byte[] encrypt(byte[] ekPKE, byte[] message, byte[] random) {\n\n        int n = 0;\n\n        // Create a byte buffer to wrap the passed in ekPKE\n        ByteBuffer ekPKEBuffer = ByteBuffer.wrap(ekPKE);\n\n        // Allocate tHat\n        int[][] tHat = new int[parameterSet.getK()][];\n\n        // Iterate over the 384-byte chunks of tHat and perform a byte decode on each chunk\n        // When this operation is complete there will be 32-bytes remaining in the buffer\n        // which are the seed rho.\n        for (int i = 0; i < parameterSet.getK(); i++) {\n\n            // Split off a 384-byte chunk of ekPKE\n            byte[] ekPKEChunk = new byte[384];\n            ekPKEBuffer.get(ekPKEChunk);\n\n            // Fill tHat\n            tHat[i] = codec.byteDecode(12, ekPKEChunk);\n\n        }\n\n        // Split off rho\n        byte[] rho = new byte[32];\n        ekPKEBuffer.get(rho);\n\n        // Regenerate aHatMatrix\n        int[][][] aHatMatrix = new int[parameterSet.getK()][parameterSet.getK()][];\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            for (int j = 0; j < parameterSet.getK(); j++) {\n                aHatMatrix[i][j] = sampler.sampleNTT(rho, (byte) j, (byte) i);\n            }\n        }\n\n        // Generate y\n        int[][] y = new int[parameterSet.getK()][];\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            y[i] = sampler.samplePolyCBDEta1(hash.prfEta1(random, (byte) n));\n            n++;\n        }\n\n        // Generate e1\n        int[][] e1 = new int[parameterSet.getK()][];\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            e1[i] = sampler.samplePolyCBDEta2(hash.prfEta2(random, (byte) n));\n            n++;\n        }\n\n        // Sample e2\n        int[] e2 = sampler.samplePolyCBDEta2(hash.prfEta2(random, (byte) n));\n\n        // Generate yHat\n        int[][] yHat = new int[parameterSet.getK()][];\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            yHat[i] = ntt.transform(y[i]);\n        }\n\n        // Generate u\n        int[][] u = new int[parameterSet.getK()][];\n        int[][] matrixOp = ntt.matrixMultiply(ntt.matrixTranspose(aHatMatrix), yHat);\n        for (int i = 0; i < parameterSet.getK(); i++) {\n            u[i] = ntt.inverse(matrixOp[i]);\n        }\n        u = ntt.matrixAdd(u, e1);\n\n        // Generate mu\n        int[] decodedMessage = codec.byteDecode(1, message);\n        int[] mu = codec.decompress(1, decodedMessage);\n\n        // Generate v\n        int[] v = ntt.arrayAdd(ntt.arrayAdd(ntt.inverse(ntt.vectorTransposeMultiply(tHat, yHat)), e2), mu);\n\n        // Generate result\n        int resultLength = 32 * (parameterSet.getDu() * parameterSet.getK() + parameterSet.getDv());\n        ByteBuffer resultBuffer = ByteBuffer.allocate(resultLength);\n\n        for (int[] ints : u) {\n            resultBuffer.put(codec.byteEncode(parameterSet.getDu(), codec.compress(parameterSet.getDu(), ints)));\n        }\n        resultBuffer.put(codec.byteEncode(parameterSet.getDv(), codec.compress(parameterSet.getDv(), v)));\n\n        return resultBuffer.array().clone();\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/hash/Hash.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.hash;\n\npublic interface Hash {\n\n    byte[] prfEta1(byte[] s, byte b);\n\n    byte[] prfEta2(byte[] s, byte b);\n\n    byte[] gHash(byte[] c);\n\n    byte[] hHash(byte[] s);\n\n    byte[] jHash(byte[] s);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/hash/MLKEMHash.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.hash;\n\nimport org.bouncycastle.jcajce.provider.digest.SHA3;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.KeccakSponge;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitInputStream;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitOutputStream;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.KeyPairGenerationException;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\npublic class MLKEMHash implements Hash {\n\n    private final ParameterSet parameterSet;\n\n    private final MessageDigest sha3Hash256;\n\n    private final MessageDigest sha3Hash512;\n\n    private final KeccakSponge shake128;\n\n    private final KeccakSponge shake256;\n\n    public MLKEMHash(ParameterSet parameterSet, MessageDigest sha3Hash256, MessageDigest sha3Hash512, KeccakSponge shake128, KeccakSponge shake256) {\n        this.parameterSet = parameterSet;\n        this.sha3Hash256 = sha3Hash256;\n        this.sha3Hash512 = sha3Hash512;\n        this.shake128 = shake128;\n        this.shake256 = shake256;\n    }\n\n    public static MLKEMHash create(ParameterSet parameterSet) {\n\n        MessageDigest sha3Hash256;\n        MessageDigest sha3Hash512;\n        KeccakSponge shake128;\n        KeccakSponge shake256;\n\n        // Bootstrap SHA3-256\n        try {\n            sha3Hash256 = MessageDigest.getInstance(\"SHA3-256\");\n        } catch (Exception e) {\n            sha3Hash256 = new SHA3.Digest256();\n        }\n\n        // Bootstrap SHA3-512\n        try {\n            sha3Hash512 = MessageDigest.getInstance(\"SHA3-512\");\n        } catch (Exception e) {\n            sha3Hash512 = new SHA3.Digest512();\n        }\n\n        // Bootstrap SHAKE128\n        shake128 = new KeccakSponge(XOFParameterSet.SHAKE128);\n\n        // Bootstrap SHAKE256\n        shake256 = new KeccakSponge(XOFParameterSet.SHAKE256);\n\n        return new MLKEMHash(parameterSet, sha3Hash256, sha3Hash512, shake128, shake256);\n    }\n\n    @Override\n    public byte[] prfEta1(byte[] s, byte b) {\n\n        int eta = parameterSet.getEta1();\n\n        // Init XOF\n        BitOutputStream absorbStream = shake256.getAbsorbStream();\n        BitInputStream squeezeStream = shake256.getSqueezeStream();\n\n        // Absorb s and b\n        absorbStream.write(s);\n        absorbStream.write(new byte[] {b});\n\n        // Squeeze the result\n        byte[] digest = new byte[64 * eta];\n        if (squeezeStream.read(digest) != digest.length) {\n            throw new KeyPairGenerationException(\"PRF SHAKE256.Squeeze() operation failed\");\n        }\n\n        return digest;\n\n    }\n\n    @Override\n    public byte[] prfEta2(byte[] s, byte b) {\n\n        int eta = parameterSet.getEta2();\n\n        // Init XOF\n        BitOutputStream absorbStream = shake256.getAbsorbStream();\n        BitInputStream squeezeStream = shake256.getSqueezeStream();\n\n        // Absorb s and b\n        absorbStream.write(s);\n        absorbStream.write(new byte[] {b});\n\n        // Squeeze the result\n        byte[] digest = new byte[64 * eta];\n        if (squeezeStream.read(digest) != digest.length) {\n            throw new KeyPairGenerationException(\"PRF SHAKE256.Squeeze() operation failed\");\n        }\n\n        return digest;\n\n    }\n\n    @Override\n    public byte[] gHash(byte[] c) {\n\n        return sha3Hash512.digest(c);\n\n    }\n\n    @Override\n    public byte[] hHash(byte[] s) {\n\n        return sha3Hash256.digest(s);\n\n    }\n\n    @Override\n    public byte[] jHash(byte[] s) {\n\n        // Init XOF\n        BitOutputStream absorbStream = shake256.getAbsorbStream();\n        BitInputStream squeezeStream = shake256.getSqueezeStream();\n\n        // Absorb s\n        absorbStream.write(s);\n\n        // Squeeze the result\n        byte[] digest = new byte[32];\n        if (squeezeStream.read(digest) != digest.length) {\n            throw new KeyPairGenerationException(\"PRF SHAKE256.Squeeze() operation failed\");\n        }\n\n        return digest;\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/hash/XOFParameterSet.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.hash;\n\npublic enum XOFParameterSet {\n\n    SHA3_224(\n            \"SHA3-224\",\n            224*2,\n            224/8,\n            (byte) 2L,\n            2\n    ),\n    SHA3_256(\n            \"SHA3-256\",\n            256*2,\n            256/8,\n            (byte) 2L,\n            2\n    ),\n    SHA3_384(\n            \"SHA3-384\",\n            384*2,\n            384/8,\n            (byte) 2L,\n            2\n    ),\n    SHAKE128(\n            \"SHAKE128\",\n            256,\n            -1,\n            (byte) 0xf,\n            4\n    ),\n    SHAKE256(\n            \"SHAKE256\",\n            512,\n            -1,\n            (byte) 0xf,\n            4\n    );\n\n    private final String algorithm;\n    private final int capacityInBits;\n    private final int digestLength;\n    private final byte domainPadding;\n    private final int domainPaddingBitLength;\n\n    XOFParameterSet(String algorithm, int capacityInBits, int digestLength, byte domainPadding, int domainPaddingBitLength) {\n        this.algorithm = algorithm;\n        this.capacityInBits = capacityInBits;\n        this.digestLength = digestLength;\n        this.domainPadding = domainPadding;\n        this.domainPaddingBitLength = domainPaddingBitLength;\n    }\n\n    public String getAlgorithm() {\n        return algorithm;\n    }\n\n    public int getCapacityInBits() {\n        return capacityInBits;\n    }\n\n    public int getDigestLength() {\n        return digestLength;\n    }\n\n    public byte getDomainPadding() {\n        return domainPadding;\n    }\n\n    public int getDomainPaddingBitLength() {\n        return domainPaddingBitLength;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/DecapsulationKey.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key;\n\npublic interface DecapsulationKey extends Key {\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/EncapsulationKey.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key;\n\npublic interface EncapsulationKey extends Key {\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/Key.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key;\n\npublic interface Key {\n\n    byte[] getBytes();\n\n    /**\n     * Any class which implements this method must zero out memory containing key values.\n     */\n    void destroy();\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/KeyPair.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key;\n\npublic interface KeyPair {\n\n    EncapsulationKey getEncapsulationKey();\n\n    DecapsulationKey getDecapsulationKey();\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/KeyPairException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key;\n\npublic class KeyPairException extends RuntimeException {\n    public KeyPairException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/SharedSecretKey.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key;\n\npublic interface SharedSecretKey extends Key {\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/check/KeyPairCheckException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.check;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPairException;\n\npublic class KeyPairCheckException extends KeyPairException {\n    public KeyPairCheckException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/gen/KeyPairGeneration.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.gen;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\n\npublic interface KeyPairGeneration {\n\n    KeyPair generateKeyPair(byte[] d, byte[] z);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/gen/KeyPairGenerationException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.gen;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPairException;\n\npublic class KeyPairGenerationException extends KeyPairException {\n    public KeyPairGenerationException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/gen/mlkem/MLKEMKeyPairGenerator.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.codec.Codec;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.codec.MLKEMCodec;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.Hash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.MLKEMHash;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.KeyPairGeneration;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.KeyPairGenerationException;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem.MLKEMKeyPair;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.sample.MLKEMSampler;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.sample.Sampler;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.transform.MLKEMTransformer;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.transform.Transformer;\n\nimport java.nio.ByteBuffer;\n\nimport static peergos.server.crypto.asymmetric.mlkem.CryptoUtils.zero;\n\npublic final class MLKEMKeyPairGenerator implements KeyPairGeneration {\n\n    private final ParameterSet parameterSet;\n\n    final Codec codec;\n\n    final Hash hash;\n\n    final Sampler sampler;\n\n    final Transformer ntt;\n\n    private MLKEMKeyPairGenerator(ParameterSet parameterSet, Codec codec, Hash hash, Sampler sampler, Transformer ntt) {\n        this.parameterSet = parameterSet;\n        this.codec = codec;\n        this.hash = hash;\n        this.sampler = sampler;\n        this.ntt = ntt;\n    }\n\n    public static MLKEMKeyPairGenerator create(ParameterSet parameterSet) {\n        return new MLKEMKeyPairGenerator(\n                parameterSet,\n                MLKEMCodec.create(parameterSet),\n                MLKEMHash.create(parameterSet),\n                MLKEMSampler.create(parameterSet),\n                MLKEMTransformer.create(parameterSet)\n        );\n    }\n\n    /**\n     * Implements Algorithm 16 (ML-KEM.KeyGen_internal) of the FIPS203 Specification\n     *\n     * @param d A byte array of exactly length 32 of randomly generated noise\n     * @param z A byte array of exactly length 32 of randomly generated noise\n     *\n     * @return FIPS203KeyPair\n     */\n    @Override\n    public KeyPair generateKeyPair(byte[] d, byte[] z) {\n\n        // Ensure d, z exist and are 32 bytes long\n        if (d == null || d.length != 32 || z == null || z.length != 32) {\n            throw new KeyPairGenerationException(\"Entropy sources 'd' and 'z' must be 32 bytes\");\n        }\n\n        // It is the responsibility of the caller to destroy the memory they passed in by reference.\n        // We will only work on local copies, which will be destroyed after last use.\n        byte[] dLocal = d.clone();\n        byte[] zLocal = z.clone();\n\n        // Call K-PKE.KeyGen\n        KeyPair baseKeyPair = generateKPKE(dLocal); // LAST USE: dLocal\n\n        // ZERO: dLocal\n        zero(dLocal);\n\n        // Retrieve bytes array for the pke keys\n        byte[] ekPKE = baseKeyPair.getEncapsulationKey().getBytes();\n        byte[] dkPKE = baseKeyPair.getDecapsulationKey().getBytes(); // LAST USE: baseKeyPair\n\n        // ZERO: baseKeyPair\n        baseKeyPair.getEncapsulationKey().destroy();\n        baseKeyPair.getDecapsulationKey().destroy();\n\n        // Hash the encapsulation key\n        byte[] ekHash = hash.hHash(ekPKE);\n\n        // Allocate byte array for composite decaps key\n        byte[] dkResult = new byte[dkPKE.length + ekPKE.length + ekHash.length + zLocal.length];\n\n        // Wrap in a buffer to fill and fill the result\n        // It doesn't matter if the ByteBuffer wrapper sticks around in memory as long as the wrapped memory is cleared.\n        ByteBuffer dkResultBuffer = ByteBuffer.wrap(dkResult);\n        dkResultBuffer.put(dkPKE)\n                .put(ekPKE)\n                .put(ekHash) // LAST USE: ekHash\n                .put(zLocal); // LAST USE: zLocal\n\n        // ZERO: ekHash\n        zero(ekHash);\n\n        // ZERO: zLocal\n        zero(zLocal);\n\n        // Create result keypair\n        // The implementation itself will make a copy of the key bytes, so we don't need to\n        // worry about it being modified by outside code.\n        KeyPair keyPair = MLKEMKeyPair.fromBytes(ekPKE, dkResult);  // LAST USE: ekPKE, dkResult\n\n        // ZERO: dkResult\n        zero(dkResult);\n\n        // ZERO: ekPKE\n        zero(ekPKE);\n\n        // ZERO: dkPKE\n        zero(dkPKE);\n\n        // Return result value\n        // Caller is responsible for destroying the KeyPair once they are done\n        return keyPair;\n\n    }\n\n    /**\n     * Implements Algorithm 13 of the FIPS203 Specification.\n     * This is described in Section 5.1 of the August 13 Spec Release starting on Page 28\n     *\n     * @param d An array of exactly 32 randomly generated bytes.\n     *\n     * @return An initial FIPS203KeyPair instance\n     */\n    KeyPair generateKPKE(byte[] d) {\n\n        // Ensure d exists and is 32 bytes long\n        if (d == null || d.length != 32) {\n            throw new KeyPairGenerationException(\"Entropy source 'd' must be 32 bytes\");\n        }\n\n        // It is the responsibility of the caller to destroy the memory they passed in by reference.\n        // We will only work on local copies, which will be destroyed after last use.\n        byte[] dLocal = d.clone();\n\n        // Get k as a byte value from parameter set\n        int k = parameterSet.getK();\n        byte[] kb = { (byte) k };\n\n        // 1: Expand 32 + 1 bytes to two pseudorandom 32-byte seeds\n        byte[] dk = new byte[33];\n        ByteBuffer buffer = ByteBuffer.wrap(dk);\n        buffer.put(dLocal); // LAST USE: dLocal\n        buffer.put(kb); // LAST USE: kb\n\n        // ZERO: dLocal\n        zero(dLocal);\n\n        // ZERO: kb\n        zero(kb);\n\n        // Generate the combined seeds\n        byte[] rhoAndSigma = hash.gHash(dk); // LAST USE: dk\n\n        // ZERO: dk\n        zero(dk);\n\n        if (rhoAndSigma == null || rhoAndSigma.length != 64) {\n            throw new KeyPairGenerationException(\"Unable to generate 'rho' and 'sigma' 32-byte seed values\");\n        }\n\n        // Wrap rhoAndSigma in a ByteBuffer for future reads\n        ByteBuffer rhoAndSigmaBuffer = ByteBuffer.wrap(rhoAndSigma);\n\n        // Split out rho\n        byte[] rho = new byte[32];\n        rhoAndSigmaBuffer.get(rho);\n\n        // Split out sigma\n        byte[] sigma = new byte[32];\n        rhoAndSigmaBuffer.get(sigma); // LAST USE: rhoAndSigma\n\n        // ZERO: rhoAndSigma\n        zero(rhoAndSigma);\n\n        int n = 0;\n\n        int[][][] aHatMatrix = new int[k][k][256];\n\n        // Generate A hat matrix\n        for (int i = 0; i < k; i++) {\n            for (int j = 0; j < k; j++) {\n                aHatMatrix[i][j] = sampler.sampleNTT(rho, (byte) j, (byte) i);\n            }\n        }\n\n        // Generate s\n        int[][] s = new int[k][256];\n        for (int i = 0; i < k; i++) {\n            s[i] = sampler.samplePolyCBDEta1(hash.prfEta1(sigma, (byte) n));\n            n++;\n        }\n\n        // Generate e\n        int[][] e = new int[k][256];\n        for (int i = 0; i < k; i++) {\n            e[i] = sampler.samplePolyCBDEta1(hash.prfEta1(sigma, (byte) n));\n            n++;\n        }\n\n        // Calculate sHat\n        int[][] sHat = new int[k][256];\n        for (int i = 0; i < k; i++) {\n            sHat[i] = ntt.transform(s[i]); // LAST USE: s\n        }\n\n        // ZERO: s\n        zero(s);\n\n        // Calculate eHat\n        int[][] eHat = new int[k][256];\n        for (int i = 0; i < k; i++) {\n            eHat[i] = ntt.transform(e[i]); // LAST USE: e\n        }\n\n        // ZERO: e\n        zero(e);\n\n        // Noisy linear system in NTT domain\n        int[][] tHat = ntt.matrixAdd(ntt.matrixMultiply(aHatMatrix, sHat), eHat); // LAST USE: aHatMatrix, eHat\n\n        // ZERO: aHatMatrix, eHat\n        zero(aHatMatrix);\n        zero(eHat);\n\n        // ByteEncode ekPKE and append rho\n        byte[] ekPKE = new byte[384*k+32];\n        ByteBuffer ekPKEBuffer = ByteBuffer.wrap(ekPKE);\n        for (int i = 0; i < k; i++) {\n            ekPKEBuffer.put(codec.byteEncode(12, tHat[i]));\n        }\n        ekPKEBuffer.put(rho); // LAST USE: rho\n\n        // ZERO: rho\n        zero(rho);\n\n        // ByteEncode dkPKE\n        byte[] dkPKE = new byte[384*k];\n        ByteBuffer dkPKEBuffer = ByteBuffer.wrap(dkPKE);\n        for (int i = 0; i < k; i++) {\n            dkPKEBuffer.put(codec.byteEncode(12, sHat[i])); // LAST USE: sHat\n        }\n\n        // ZERO: sHat\n        zero(sHat);\n\n        // Create and a result object wrapping the keypair\n        // This will make copies of the byte arrays, and we will destroy the originals\n        KeyPair keyPair = MLKEMKeyPair.fromBytes(ekPKE, dkPKE); // LAST USE: ekPKE, dkPKE\n\n        // ZERO: ekPKE\n        zero(ekPKE);\n\n        // ZERO: dkPKE\n        zero(dkPKE);\n\n        // Return result\n        return keyPair;\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/gen/provider/MLKEMKeyGenerationProvider.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.provider;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\n\nimport java.security.KeyPair;\nimport java.security.KeyPairGeneratorSpi;\nimport java.security.SecureRandom;\n\npublic class MLKEMKeyGenerationProvider extends KeyPairGeneratorSpi {\n\n    private final ParameterSet params;\n\n    public MLKEMKeyGenerationProvider(ParameterSet params) {\n        this.params = params;\n    }\n\n    public static MLKEMKeyGenerationProvider getMLKEM512Provider() {\n        return new MLKEMKeyGenerationProvider(ParameterSet.ML_KEM_512);\n    }\n\n    public static MLKEMKeyGenerationProvider getMLKEM768Provider() {\n        return new MLKEMKeyGenerationProvider(ParameterSet.ML_KEM_768);\n    }\n\n    public static MLKEMKeyGenerationProvider getMLKEM1024Provider() {\n        return new MLKEMKeyGenerationProvider(ParameterSet.ML_KEM_1024);\n    }\n\n    @Override\n    public void initialize(int keysize, SecureRandom random) {\n\n    }\n\n    @Override\n    public KeyPair generateKeyPair() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/mlkem/MLKEMDecapsulationKey.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\n\nimport java.util.Arrays;\n\npublic class MLKEMDecapsulationKey implements DecapsulationKey {\n\n    private final byte[] keyBytes;\n\n    private MLKEMDecapsulationKey(byte[] keyBytes) {\n        this.keyBytes = keyBytes;\n    }\n\n    public static MLKEMDecapsulationKey create(byte[] keyBytes) {\n        return new MLKEMDecapsulationKey(keyBytes.clone());\n    }\n\n    @Override\n    public byte[] getBytes() {\n        return keyBytes.clone();\n    }\n\n    @Override\n    public void destroy() {\n        Arrays.fill(keyBytes, (byte)0);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/mlkem/MLKEMEncapsulationKey.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.EncapsulationKey;\n\nimport java.util.Arrays;\n\npublic class MLKEMEncapsulationKey implements EncapsulationKey {\n\n    private final byte[] keyBytes;\n\n    private MLKEMEncapsulationKey(byte[] keyBytes) {\n        this.keyBytes = keyBytes;\n    }\n\n    public static MLKEMEncapsulationKey create(byte[] keyBytes) {\n        return new MLKEMEncapsulationKey(keyBytes.clone());\n    }\n\n    @Override\n    public byte[] getBytes() {\n        return keyBytes.clone();\n    }\n\n    @Override\n    public void destroy() {\n        Arrays.fill(keyBytes, (byte)0);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/mlkem/MLKEMKeyPair.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.EncapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\n\nimport java.util.Objects;\n\npublic class MLKEMKeyPair implements KeyPair {\n    private final EncapsulationKey getEncapsulationKey;\n    private final DecapsulationKey getDecapsulationKey;\n\n    public MLKEMKeyPair(EncapsulationKey getEncapsulationKey, DecapsulationKey getDecapsulationKey) {\n        this.getEncapsulationKey = getEncapsulationKey;\n        this.getDecapsulationKey = getDecapsulationKey;\n    }\n\n    public static KeyPair fromBytes(byte[] ek, byte[] dk) {\n        return new MLKEMKeyPair(MLKEMEncapsulationKey.create(ek), MLKEMDecapsulationKey.create(dk));\n    }\n\n    @Override\n    public EncapsulationKey getEncapsulationKey() {\n        return getEncapsulationKey;\n    }\n\n    @Override\n    public DecapsulationKey getDecapsulationKey() {\n        return getDecapsulationKey;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        MLKEMKeyPair that = (MLKEMKeyPair) o;\n        return Objects.equals(getEncapsulationKey, that.getEncapsulationKey) && Objects.equals(getDecapsulationKey, that.getDecapsulationKey);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(getEncapsulationKey, getDecapsulationKey);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/key/mlkem/MLKEMSharedSecretKey.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\n\nimport java.util.Arrays;\n\npublic class MLKEMSharedSecretKey implements SharedSecretKey {\n\n    private final byte[] sharedSecret;\n\n    public MLKEMSharedSecretKey(byte[] sharedSecret) {\n        this.sharedSecret = sharedSecret;\n    }\n\n    public static MLKEMSharedSecretKey create(byte[] sharedSecret) {\n        return new MLKEMSharedSecretKey(sharedSecret);\n    }\n\n    @Override\n    public byte[] getBytes() {\n        return sharedSecret.clone();\n    }\n\n    @Override\n    public void destroy() {\n        Arrays.fill(sharedSecret, (byte)0);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/message/CipherText.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.message;\n\npublic interface CipherText {\n\n    byte[] getBytes();\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/message/MLKEMCipherText.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.message;\n\npublic class MLKEMCipherText implements CipherText {\n\n    private final byte[] cipherText;\n\n    public MLKEMCipherText(byte[] cipherText) {\n        this.cipherText = cipherText;\n    }\n\n    public static MLKEMCipherText create(byte[] cipherText) {\n        return new MLKEMCipherText(cipherText.clone());\n    }\n\n    @Override\n    public byte[] getBytes() {\n        return cipherText.clone();\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/provider/MLKEMProvider.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.provider;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.provider.MLKEMDecapsulatorProvider;\n\nimport javax.crypto.KEMSpi;\nimport java.security.*;\nimport java.security.spec.AlgorithmParameterSpec;\n\npublic class MLKEMProvider implements KEMSpi {\n\n    private final ParameterSet params;\n\n    public MLKEMProvider(ParameterSet params) {\n        this.params = params;\n    }\n\n    public static MLKEMProvider getMLKEM512Provider() {\n        return new MLKEMProvider(ParameterSet.ML_KEM_512);\n    }\n\n    public static MLKEMProvider getMLKEM768Provider() {\n        return new MLKEMProvider(ParameterSet.ML_KEM_768);\n    }\n\n    public static MLKEMProvider getMLKEM1024Provider() {\n        return new MLKEMProvider(ParameterSet.ML_KEM_1024);\n    }\n\n    @Override\n    public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, AlgorithmParameterSpec spec, SecureRandom secureRandom) throws InvalidAlgorithmParameterException, InvalidKeyException {\n        return null;\n    }\n\n    @Override\n    public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException, InvalidKeyException {\n        return MLKEMDecapsulatorProvider.getInstance(privateKey, spec);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/provider/MimicloneSecurityProvider.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.provider;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.FIPS203;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.provider.MLKEMKeyGenerationProvider;\n\nimport java.security.*;\nimport java.security.spec.AlgorithmParameterSpec;\n\npublic class MimicloneSecurityProvider extends Provider implements javax.crypto.KEMSpi {\n\n    public static final String PROVIDER_NAME = \"Mimiclone\";\n    private static final String PROVIDER_VERSION = \"1.0.0\";\n    private static final String PROVIDER_INFO = \"Mimiclone Provider \" +\n            \"(ML-KEM-512/ML-KEM-768/ML-KEM-1024: Key Pair Generation, Kem/Encaps, KEM/Decaps)\";\n\n    private static final class ProviderService extends Provider.Service {\n        ProviderService(Provider p, String type, String algo, String cn) {\n            super(p, type, algo, cn, null, null);\n        }\n\n        @Override\n        public Object newInstance(Object ctrParamObj)\n                throws NoSuchAlgorithmException {\n            String type = getType();\n            if (ctrParamObj != null) {\n                throw new InvalidParameterException\n                        (\"constructorParameter not used with \" + type + \" engines\");\n            }\n\n            String algo = getAlgorithm();\n            switch (type) {\n                case \"KeyPairGenerator\": {\n                    switch (algo) {\n                        case \"ML-KEM-512\": return MLKEMKeyGenerationProvider.getMLKEM512Provider();\n                        case \"ML-KEM-768\": return MLKEMKeyGenerationProvider.getMLKEM768Provider();\n                        case \"ML-KEM-1024\": return MLKEMKeyGenerationProvider.getMLKEM1024Provider();\n                        default: throw new NoSuchAlgorithmException(\"Algorithm not supported: \" + algo);\n                    }\n                }\n                case \"KEM\": {\n                    switch (algo) {\n                        case \"ML-KEM-512\": return MLKEMProvider.getMLKEM512Provider();\n                        case \"ML-KEM-768\": return MLKEMProvider.getMLKEM768Provider();\n                        case \"ML-KEM-1024\": return MLKEMProvider.getMLKEM1024Provider();\n                        default: throw new NoSuchAlgorithmException(\"Algorithm not supported: \" + algo);\n                    }\n                }\n            }\n            throw new ProviderException(\"No impl for \" + algo +\n                    \" \" + type);\n        }\n    }\n\n    public MimicloneSecurityProvider() {\n        super(PROVIDER_NAME, PROVIDER_VERSION, PROVIDER_INFO);\n\n        put(PROVIDER_NAME, this);\n        putService(new MimicloneSecurityProvider.ProviderService(this,\n                \"KeyPairGenerator\",\n                \"ML-KEM-512\",\n                MLKEMKeyGenerationProvider.class.getName()));\n        putService(new MimicloneSecurityProvider.ProviderService(this,\n                \"KeyPairGenerator\",\n                \"ML-KEM-768\",\n                MLKEMKeyGenerationProvider.class.getName()));\n        putService(new MimicloneSecurityProvider.ProviderService(this,\n                \"KeyPairGenerator\",\n                \"ML-KEM-1024\",\n                MLKEMKeyGenerationProvider.class.getName()));\n        putService(new MimicloneSecurityProvider.ProviderService(this,\n                \"KEM\",\n                \"ML-KEM-512\",\n                MLKEMKeyGenerationProvider.class.getName()));\n        putService(new MimicloneSecurityProvider.ProviderService(this,\n                \"KEM\",\n                \"ML-KEM-768\",\n                MLKEMKeyGenerationProvider.class.getName()));\n        putService(new MimicloneSecurityProvider.ProviderService(this,\n                \"KEM\",\n                \"ML-KEM-1024\",\n                MLKEMKeyGenerationProvider.class.getName()));\n    }\n\n    public void install() {\n        Security.addProvider(this);\n    }\n\n    @Override\n    public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, AlgorithmParameterSpec spec, SecureRandom secureRandom) throws InvalidAlgorithmParameterException, InvalidKeyException {\n        return null;\n    }\n\n    @Override\n    public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException, InvalidKeyException {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/reduce/Reducer.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.reduce;\n\npublic interface Reducer {\n\n    int reduce(int a) throws ReductionException;\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/reduce/ReductionException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.reduce;\n\npublic class ReductionException extends RuntimeException {\n    public ReductionException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/reduce/barrett/BarrettReducer.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.reduce.barrett;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.reduce.Reducer;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.reduce.ReductionException;\n\npublic final class BarrettReducer implements Reducer {\n\n    private final ParameterSet parameterSet;\n    private final int modulus;\n    private final int multiplier;\n    private final long shift = 32;\n\n    public BarrettReducer(ParameterSet parameterSet, int modulus, int multiplier) {\n        this.parameterSet = parameterSet;\n        this.modulus = modulus;\n        this.multiplier = multiplier;\n    }\n\n    private static int calculateMultiplier(int modulus) {\n        return (int) ((1L << 32) / modulus);\n    }\n\n    public static BarrettReducer create(ParameterSet parameterSet) {\n        return new BarrettReducer(\n                parameterSet,\n                parameterSet.getQ(),\n                calculateMultiplier(parameterSet.getQ())\n        );\n    }\n\n    public static BarrettReducer create(ParameterSet parameterSet, int modulus) {\n        return new BarrettReducer(\n                parameterSet,\n                modulus,\n                calculateMultiplier(modulus)\n        );\n    }\n\n    @Override\n    public int reduce(int a) throws ReductionException {\n\n        // Estimate the quotient\n        int quotient = (int) (((long) a * multiplier) >> shift);\n\n        // Calculate the remainder\n        int remainder = a - (quotient * modulus);\n\n        // Final correction\n        int result;\n        int correctedResult = remainder - modulus;\n        if (remainder >= modulus) {\n            result = correctedResult;\n        } else {\n            result = remainder;\n        }\n\n        return result;\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/sample/MLKEMSampler.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.sample;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.core.KeccakSponge;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitInputStream;\nimport peergos.server.crypto.asymmetric.mlkem.fips202.keccak.io.BitOutputStream;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.hash.XOFParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.KeyPairGenerationException;\n\nimport java.util.BitSet;\n\nimport static peergos.server.crypto.asymmetric.mlkem.CryptoUtils.mod;\n\npublic class MLKEMSampler implements Sampler {\n\n    private final ParameterSet parameterSet;\n\n    public MLKEMSampler(ParameterSet parameterSet) {\n        this.parameterSet = parameterSet;\n    }\n\n    public static MLKEMSampler create(ParameterSet parameterSet) {\n        return new MLKEMSampler(parameterSet);\n    }\n\n    @Override\n    public int[] sampleNTT(byte[] seed, byte a, byte b) {\n\n        // Setup internal context vars\n        int j = 0;\n        int[] aHat = new int[256];\n        byte[] sample = new byte[3];\n\n        // Init context for XOF\n        KeccakSponge xof = new KeccakSponge(XOFParameterSet.SHAKE128);\n        BitOutputStream absorbStream = xof.getAbsorbStream();\n        BitInputStream squeezeStream = xof.getSqueezeStream();\n\n        // Absorb the seed rho, and the indices i and j that have been appended as bytes\n        absorbStream.write(seed);\n        absorbStream.write(new byte[] {a, b});\n\n        while (j < 256) {\n\n            if (squeezeStream.read(sample) != 3) {\n                throw new KeyPairGenerationException(\"Unable to squeeze 3 bytes of data\");\n            }\n\n            // Java doesn't have unsigned bytes, but this algorithm treats sampled bytes as if they are integers\n            // which leads to strange behavior with a signed byte type.  So we extract the sample values and convert\n            // them to unsigned integers.\n            int c0 = Byte.toUnsignedInt(sample[0]);\n            int c1 = Byte.toUnsignedInt(sample[1]);\n            int c2 = Byte.toUnsignedInt(sample[2]);\n\n            int d1 = c0 + 256 * (c1 % 16);\n            int d2 = (c1 / 16) + (16 * c2);\n\n            if (d1 < parameterSet.getQ()) {\n                aHat[j] = d1;\n                j++;\n            }\n\n            if (d2 < parameterSet.getQ() && j < 256) {\n                aHat[j] = d2;\n                j++;\n            }\n        }\n\n        return aHat;\n\n    }\n\n    private int[] samplePolyCBD(int eta, byte[] input) {\n\n        // Validate input length\n        if (input == null || input.length != 64*eta) {\n            throw new KeyPairGenerationException(String.format(\"PolyCBD sample input must be %d bytes\", 64*eta));\n        }\n\n        // Declare result array\n        int[] result = new int[256];\n\n        BitSet b = BitSet.valueOf(input);\n        for (int i = 0; i < 256; i++) {\n\n            // Calculate X\n            int x = 0;\n            for (int j = 0; j < eta; j++) {\n                x += b.get(2*i*eta + j) ? 1 : 0;\n            }\n\n            // Calculate Y\n            int y = 0;\n            for (int j = 0; j < eta; j++) {\n                y += b.get(2*i*eta + eta + j) ? 1 : 0;\n            }\n\n            result[i] = mod(x - y, parameterSet.getQ());\n        }\n\n        return result;\n\n    }\n\n    @Override\n    public int[] samplePolyCBDEta1(byte[] input) {\n\n        return samplePolyCBD(parameterSet.getEta1(), input);\n\n    }\n\n    @Override\n    public int[] samplePolyCBDEta2(byte[] input) {\n\n        return samplePolyCBD(parameterSet.getEta2(), input);\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/sample/Sampler.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.sample;\n\npublic interface Sampler {\n\n    int[] sampleNTT(byte[] seed, byte a, byte b);\n\n    int[] samplePolyCBDEta1(byte[] input);\n\n    int[] samplePolyCBDEta2(byte[] input);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/transform/MLKEMTransformer.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.transform;\n\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.reduce.Reducer;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.reduce.barrett.BarrettReducer;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\npublic class MLKEMTransformer implements Transformer {\n\n    private final ParameterSet parameterSet;\n    private final Reducer reducer;\n\n    private static final int INPUT_OUTPUT_LENGTH = 256;\n\n    final int[] transformLenVals = {\n            128, 64, 64, 32, 32, 32, 32, 16, 16, 16, 16, 16, 16, 16, 16, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,\n            8, 8, 8, 4, 4, 4, 4,4, 4, 4, 4, 4, 4, 4, 4,4, 4, 4, 4, 4, 4, 4, 4,4, 4, 4, 4,4, 4, 4, 4,4, 4, 4, 4,\n            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2\n    };\n    final int[] transformStartVals = {\n            0, 0, 128, 0, 64, 128, 192, 0, 32, 64, 96, 128, 160, 192, 224, 0, 16, 32, 48, 64, 80, 96, 112, 128,\n            144, 160, 176, 192, 208, 224, 240, 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120,\n            128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 0, 4, 8, 12, 16, 20,\n            24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116,\n            120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196,\n            200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252\n    };\n    final int[] transformZetaVals = {\n            1729, 2580, 3289, 2642, 630, 1897, 848, 1062, 1919, 193, 797, 2786, 3260, 569, 1746, 296, 2447, 1339,\n            1476, 3046, 56, 2240, 1333, 1426, 2094, 535, 2882, 2393, 2879, 1974, 821, 289, 331, 3253, 1756, 1197,\n            2304, 2277, 2055, 650, 1977, 2513, 632, 2865, 33, 1320, 1915, 2319, 1435, 807, 452, 1438, 2868, 1534,\n            2402, 2647, 2617, 1481, 648, 2474, 3110, 1227, 910, 17, 2761, 583, 2649, 1637, 723, 2288, 1100, 1409,\n            2662, 3281, 233, 756, 2156, 3015, 3050, 1703, 1651, 2789, 1789, 1847, 952, 1461, 2687, 939, 2308, 2437,\n            2388, 733, 2337, 268, 641, 1584, 2298, 2037, 3220, 375, 2549, 2090, 1645, 1063, 319, 2773, 757, 2099,\n            561, 2466, 2594, 2804, 1092, 403, 1026, 1143, 2150, 2775, 886, 1722, 1212, 1874, 1029, 2110, 2935, 885,\n            2154\n    };\n\n    final int[] inverseLenVals = {\n            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4,\n            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8, 8,\n            8, 8, 8, 8, 8, 8, 8, 8, 16, 16, 16, 16, 16, 16, 16, 16, 32, 32, 32, 32, 64, 64, 128\n    };\n    final int[] inverseStartVals = {\n            0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100,\n            104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184,\n            188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 0, 8, 16, 24, 32,\n            40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208,\n            216, 224, 232, 240, 248, 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 0, 32,\n            64, 96, 128, 160, 192, 224, 0, 64, 128, 192, 0, 128, 0\n    };\n    final int[] inverseZetaVals = {\n            2154, 885, 2935, 2110, 1029, 1874, 1212, 1722, 886, 2775, 2150, 1143, 1026, 403, 1092, 2804, 2594, 2466,\n            561, 2099, 757, 2773, 319, 1063, 1645, 2090, 2549, 375, 3220, 2037, 2298, 1584, 641, 268, 2337, 733,\n            2388, 2437, 2308, 939, 2687, 1461, 952, 1847, 1789, 2789, 1651, 1703, 3050, 3015, 2156, 756, 233, 3281,\n            2662, 1409, 1100, 2288, 723, 1637, 2649, 583, 2761, 17, 910, 1227, 3110, 2474, 648, 1481, 2617, 2647,\n            2402, 1534, 2868, 1438, 452, 807, 1435, 2319, 1915, 1320, 33, 2865, 632, 2513, 1977, 650, 2055, 2277,\n            2304, 1197, 1756, 3253, 331, 289, 821, 1974, 2879, 2393, 2882, 535, 2094, 1426, 1333, 2240, 56, 3046,\n            1476, 1339, 2447, 296, 1746, 569, 3260, 2786, 797, 193, 1919, 1062, 848, 1897, 630, 2642, 3289, 2580,\n            1729\n    };\n\n    /**\n     * Precomputed values of gamma = 𝜁^(2BitRev7(𝑖)+1) mod 𝑞 as provided in Appendix A of the FIPS203 Specification\n     * on Page 45.  Computation of these values can overflow built-in data types before being\n     * bounded by the modulus so it is significantly easier and faster to work with precomputed values.\n     * These values are used in the implementation of Algorithm 11 when multiplying polynomial\n     * coefficient matrices in NTT space.\n     */\n    final int[] nttGammaVals = {\n            17, -17, 2761, -2761, 583, -583, 2649, -2649,\n            1637, -1637, 723, -723, 2288, -2288, 1100, -1100,\n            1409, -1409, 2662, -2662, 3281, -3281, 233, -233,\n            756, -756, 2156, -2156, 3015, -3015, 3050, -3050,\n            1703, -1703, 1651, -1651, 2789, -2789, 1789, -1789,\n            1847, -1847, 952, -952, 1461, -1461, 2687, -2687,\n            939, -939, 2308, -2308, 2437, -2437, 2388, -2388,\n            733, -733, 2337, -2337, 268, -268, 641, -641,\n            1584, -1584, 2298, -2298, 2037, -2037, 3220, -3220,\n            375, -375, 2549, -2549, 2090, -2090, 1645, -1645,\n            1063, -1063, 319, -319, 2773, -2773, 757, -757,\n            2099, -2099, 561, -561, 2466, -2466, 2594, -2594,\n            2804, -2804, 1092, -1092, 403, -403, 1026, -1026,\n            1143, -1143, 2150, -2150, 2775, -2775, 886, -886,\n            1722, -1722, 1212, -1212, 1874, -1874, 1029, -1029,\n            2110, -2110, 2935, -2935, 885, -885, 2154, -2154\n    };\n\n    public MLKEMTransformer(ParameterSet parameterSet, Reducer reducer) {\n        this.parameterSet = parameterSet;\n        this.reducer = reducer;\n    }\n\n    public static MLKEMTransformer create(ParameterSet parameterSet) {\n        return new MLKEMTransformer(\n                parameterSet,\n                BarrettReducer.create(parameterSet)\n        );\n    }\n\n    private void validateInput(int[] input) {\n\n        int q = parameterSet.getQ();\n\n        // Validate input is correct length\n        if (input == null || input.length != INPUT_OUTPUT_LENGTH) {\n            throw new IllegalArgumentException(String.format(\"Input must be an array of %d long values\", INPUT_OUTPUT_LENGTH));\n        }\n\n        // Validate input has properly bounded values in modulo q\n        List<Integer> incorrectIndexes = IntStream.range(0, input.length)\n                .filter(i -> input[i] < 0 || input[i] > q)\n                .boxed()\n                .collect(Collectors.toList());;\n        if (!incorrectIndexes.isEmpty()) {\n            throw new IllegalArgumentException(String.format(\"Input values at the following indexes were not in modulo %d: %s\", q, incorrectIndexes));\n        }\n    }\n\n    @Override\n    public int[] transform(int[] input) {\n\n        // Validate the input\n        validateInput(input);\n\n        // Make a copy of the input to operate on\n        // This variable is called f-hat in the FIPS203 spec, Algorithm 9, Line 1\n        int[] result = input.clone();\n\n        // NOTE: The FIPS203 spec has two outer loops that calculate {@code len} and {@code start} values that are used\n        // to modify the inner loop conditions.  It also defines a manually incremented {@code i} loop counter that\n        // is used as input to calculate the zeta values.  To improve performance and readability, we have\n        // pre-calculated these three values for each iteration of the outer loop and ordered them so we can use\n        // a single outer loop indexed on {@code i} from {@code 0} to {@code 126}.\n        for (int i = 0; i < transformLenVals.length; i++) {\n\n            // Retrieve pre-calculated loop values\n            int len = transformLenVals[i];\n            int start = transformStartVals[i];\n            int zeta = transformZetaVals[i];\n\n            // Core transform loop\n            for (int j = start; j < start + len; j++) {\n                int t = reducer.reduce(zeta * result[j + len]);\n                result[j + len] = reducer.reduce(result[j] - t);\n                result[j] = reducer.reduce(result[j] + t);\n            }\n        }\n\n        // Return the resulting transform\n        return result;\n    }\n\n    @Override\n    public int[] inverse(int[] input) {\n\n        // Validate the input\n        validateInput(input);\n\n        // Make a copy of the input to operate on\n        // This variable is called f-hat in the FIPS203 spec, Algorithm 9, Line 1\n        int[] result = input.clone();\n\n        // NOTE: The FIPS203 spec has two outer loops that calculate {@code len} and {@code start} values that are used\n        // to modify the inner loop conditions.  It also defines a manually decremented {@code i} loop counter that\n        // is used as input to calculate the zeta values.  To improve performance and readability, we have\n        // pre-calculated these three values for each iteration of the outer loop and ordered them so we can use\n        // a single outer loop indexed on {@code i} from {@code 0} to {@code 126}.\n        for (int i = 0; i < inverseLenVals.length; i++) {\n\n            // Retrieve pre-calculated loop values\n            int len = inverseLenVals[i];\n            int start = inverseStartVals[i];\n            int zeta = inverseZetaVals[i];\n\n            // Core inverse transform loop\n            for (int j = start; j < start + len; j++) {\n                int t = result[j];\n                result[j] = reducer.reduce(t + result[j + len]);\n                result[j + len] = reducer.reduce(zeta * (result[j + len] - t));\n            }\n        }\n\n        // Multiply all entries\n        for (int i = 0; i < result.length; i++) {\n\n            // NOTE: The magic number 3303 is defined in the FIPS203 spec as 128^-1.\n            result[i] = reducer.reduce(result[i] * 3303);\n\n        }\n\n        // Return the resulting transform\n        return result;\n\n    }\n\n    @Override\n    public int[][] matrixMultiply(int[][][] a, int[][] b) {\n        int aRows = a.length;\n        int aCols = a[0].length;\n\n        int[][] product = new int[aRows][256];\n\n        for (int i = 0; i < aRows; i++) {\n            for (int j = 0; j < aCols; j++) {\n                int[] nttProduct = multiplyNTTs(a[i][j], b[j]);\n                for (int k = 0; k < 256; k++) {\n                    product[i][k] = reducer.reduce(product[i][k] + nttProduct[k]);\n                }\n            }\n        }\n\n        return product;\n    }\n\n    @Override\n    public int[][] matrixAdd(int[][] a, int[][] b) {\n\n        int rows = a.length;\n        int cols = a[0].length;\n\n        int[][] sum = new int[rows][];\n\n        for (int i = 0; i < rows; i++) {\n            sum[i] = new int[cols];\n            for (int j = 0; j < cols; j++) {\n                sum[i][j] = reducer.reduce(a[i][j] + b[i][j]);\n            }\n        }\n\n        return sum;\n\n    }\n\n    @Override\n    public int[][][] matrixTranspose(int[][][] a) {\n\n        int rows = a.length;\n        int cols = a[0].length;\n\n        int[][][] transpose = new int[rows][cols][];\n\n        for (int i = 0; i < rows; i++) {\n            for (int j = 0; j < cols; j++) {\n                transpose[j][i] = a[i][j].clone();\n            }\n        }\n\n        return transpose;\n    }\n\n    @Override\n    public int[] multiplyNTTs(int[] fHat, int[] gHat) {\n\n        // Compiler validation of input\n        assert fHat != null;\n        assert fHat.length == 256;\n\n        // Compiler validation of input\n        assert gHat != null;\n        assert gHat.length == 256;\n\n        int[] hHat = new int[256];\n\n        for (int i = 0; i < 128; i++) {\n            int[] c = baseCaseMultiply(\n                    fHat[2*i],\n                    fHat[2*i+1],\n                    gHat[2*i],\n                    gHat[2*i+1],\n                    nttGammaVals[i]\n            );\n            hHat[2*i] = c[0];\n            hHat[2*i+1] = c[1];\n        }\n\n        // Return the result\n        return hHat;\n\n    }\n\n    @Override\n    public int[] baseCaseMultiply(int a0, int a1, int b0, int b1, int gamma) {\n\n        // Calculate c0\n        int a0b0 = reducer.reduce(a0 * b0);\n        int a1b1 = reducer.reduce(a1 * b1);\n        int a1b1gamma = reducer.reduce(a1b1 * gamma);\n        int c0 = reducer.reduce(a0b0 + a1b1gamma);\n\n        // Calculate c1\n        int a0b1 = reducer.reduce(a0 * b1);\n        int a1b0 = reducer.reduce(a1 * b0);\n        int c1 = reducer.reduce(a0b1 + a1b0);\n\n        // Return compound result\n        return new int[]{c0, c1};\n\n    }\n\n    @Override\n    public int[] vectorTransposeMultiply(int[][] a, int[][] b) {\n        int[] product = new int[parameterSet.getN()];\n        for (var i = 0; i < a.length; i++) {\n            var nttProduct = multiplyNTTs(a[i], b[i]);\n            for (var j = 0; j < parameterSet.getN(); j++) {\n                product[j] = (product[j] + nttProduct[j]) % parameterSet.getQ();\n            }\n        }\n        return product;\n    }\n\n    @Override\n    public int[] arrayAdd(int[] a, int[] b) {\n        int[] sum = new int[a.length];\n        for (int i = 0; i < a.length; i++) {\n            sum[i] = reducer.reduce(a[i] + b[i]);\n        }\n        return sum;\n    }\n\n    @Override\n    public int[] arraySubtract(int[] a, int[] b) {\n        int[] difference = new int[a.length];\n        for (int i = 0; i < a.length; i++) {\n            difference[i] = reducer.reduce(a[i] - b[i]);\n        }\n        return difference;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/transform/Transformer.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.transform;\n\npublic interface Transformer {\n\n    /**\n     * Performs a number theoretic transform of an array of 256 integers in modulo q=3329\n     * The concrete implementation should mimic the output characteristics of Algorithm 9 in the FIP203 Specification\n     *\n     * @param input An array of 256 integers in modulo q\n     * @return An array of 256 integers in modulo q transformed using the NTT algorithm\n     */\n    int[] transform(int[] input);\n\n    /**\n     * Performs the inverse of a number theoretic transform of an array of 256 integers in modulo q=3329\n     * The concrete implementation should mimic the output characteristics of Algorithm 10 in the FIP203 Specification\n     *\n     * @param input An array of 256 integers in modulo q representing a number theoretic transform\n     * @return An array of 256 integers in modulo q with the transform reversed\n     */\n    int[] inverse(int[] input);\n\n    int[][] matrixMultiply(int[][][] a, int[][] b);\n\n    int[][] matrixAdd(int[][] a, int[][] b);\n\n    int[][][] matrixTranspose(int[][][] a);\n\n    int[] multiplyNTTs(int[] fHat, int[] gHat);\n\n    int[] baseCaseMultiply(int a0, int a1, int b0, int b1, int gamma);\n\n    int[] vectorTransposeMultiply(int[][] a, int[][] b);\n\n    int[] arrayAdd(int[] a, int[] b);\n\n    int[] arraySubtract(int[] a, int[] b);\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/fips203/transform/TransformerException.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem.fips203.transform;\n\npublic class TransformerException extends RuntimeException {\n    public TransformerException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/asymmetric/mlkem/package-info.java",
    "content": "package peergos.server.crypto.asymmetric.mlkem;\n\n// This package imported from https://github.com/mimiclone/fips203-java which is MIT licensed\n\n/*\nMIT License\n\nCopyright (c) 2024 Mimiclone, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/"
  },
  {
    "path": "src/peergos/server/crypto/hash/Blake3.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.crypto.hash;\n\nimport java.util.Arrays;\nimport java.util.Objects;\n\n/**\n * Implements the Blake3 algorithm providing a {@linkplain #initHash() hash function} with extensible output (XOF), a\n * {@linkplain #initKeyedHash(byte[]) keyed hash function} (MAC, PRF), and a\n * {@linkplain #initKeyDerivationFunction(byte[]) key derivation function} (KDF). Blake3 has a 128-bit security level\n * and a default output length of 256 bits (32 bytes) which can extended up to 2<sup>64</sup> bytes.\n * <h2>Hashing</h2>\n * <p>Hash mode calculates the same output hash given the same input bytes and can be used as both a message digest and\n * and extensible output function.</p>\n * <pre>{@code\n *      Blake3 hasher = Blake3.initHash();\n *      hasher.update(\"Hello, world!\".getBytes(StandardCharsets.UTF_8));\n *      byte[] hash = new byte[32];\n *      hasher.doFinalize(hash);\n * }</pre>\n * <h2>Keyed Hashing</h2>\n * <p>Keyed hashes take a 32-byte secret key and calculates a message authentication code on some input bytes. These\n * also work as pseudo-random functions (PRFs) with extensible output similar to the extensible hash output. Note that\n * Blake3 keyed hashes have the same performance as plain hashes; the key is used in initialization in place of a\n * standard initialization vector used for plain hashing.</p>\n * <pre>{@code\n *      SecureRandom random = SecureRandom.getInstanceStrong();\n *      byte[] key = new byte[32];\n *      random.nextBytes(key);\n *      Blake3 hasher = Blake3.initKeyedHash(key);\n *      hasher.update(\"Hello, Alice!\".getBytes(StandardCharsets.UTF_8));\n *      byte[] mac = new byte[32];\n *      hasher.doFinalize(mac);\n * }</pre>\n * <h2>Key Derivation</h2>\n * <p>A specific hash mode for deriving session keys and other derived keys in a unique key derivation context\n * identified by some sequence of bytes. These context strings should be unique but do not need to be kept secret.\n * Additional input data is hashed for key material which can be finalized to derive subkeys.</p>\n * <pre>{@code\n *      String context = \"org.apache.commons.codec.digest.Blake3Example\";\n *      byte[] sharedSecret = ...;\n *      byte[] senderId = ...;\n *      byte[] recipientId = ...;\n *      Blake3 kdf = Blake3.initKeyDerivationFunction(context.getBytes(StandardCharsets.UTF_8));\n *      kdf.update(sharedSecret);\n *      kdf.update(senderId);\n *      kdf.update(recipientId);\n *      byte[] txKey = new byte[32];\n *      byte[] rxKey = new byte[32];\n *      kdf.doFinalize(txKey);\n *      kdf.doFinalize(rxKey);\n * }</pre>\n * <p>\n * Adapted from the ISC-licensed O(1) Cryptography library by Matt Sicker and ported from the reference public domain\n * implementation by Jack O'Connor.\n * </p>\n *\n * @see <a href=\"https://github.com/BLAKE3-team/BLAKE3\">BLAKE3 hash function</a>\n * @since 1.16\n */\npublic final class Blake3 {\n\n    private static final class ChunkState {\n\n        private int[] chainingValue;\n        private final long chunkCounter;\n        private final int flags;\n\n        private final byte[] block = new byte[BLOCK_LEN];\n        private int blockLength;\n        private int blocksCompressed;\n\n        private ChunkState(final int[] key, final long chunkCounter, final int flags) {\n            chainingValue = key;\n            this.chunkCounter = chunkCounter;\n            this.flags = flags;\n        }\n\n        private int length() {\n            return BLOCK_LEN * blocksCompressed + blockLength;\n        }\n\n        private Output output() {\n            final int[] blockWords = unpackInts(block, BLOCK_INTS);\n            final int outputFlags = flags | startFlag() | CHUNK_END;\n            return new Output(chainingValue, blockWords, chunkCounter, blockLength, outputFlags);\n        }\n\n        private int startFlag() {\n            return blocksCompressed == 0 ? CHUNK_START : 0;\n        }\n\n        private void update(final byte[] input, int offset, int length) {\n            while (length > 0) {\n                if (blockLength == BLOCK_LEN) {\n                    // If the block buffer is full, compress it and clear it. More\n                    // input is coming, so this compression is not CHUNK_END.\n                    final int[] blockWords = unpackInts(block, BLOCK_INTS);\n                    chainingValue = Arrays.copyOf(\n                            compress(chainingValue, blockWords, BLOCK_LEN, chunkCounter, flags | startFlag()),\n                            CHAINING_VALUE_INTS);\n                    blocksCompressed++;\n                    blockLength = 0;\n                    Arrays.fill(block, (byte) 0);\n                }\n\n                final int want = BLOCK_LEN - blockLength;\n                final int take = Math.min(want, length);\n                System.arraycopy(input, offset, block, blockLength, take);\n                blockLength += take;\n                offset += take;\n                length -= take;\n            }\n        }\n    }\n    private static final class EngineState {\n        private final int[] key;\n        private final int flags;\n        // Space for 54 subtree chaining values: 2^54 * CHUNK_LEN = 2^64\n        // No more than 54 entries can ever be added to this stack (after updating 2^64 bytes and not finalizing any)\n        // so we preallocate the stack here. This can be smaller in environments where the data limit is expected to\n        // be much lower.\n        private final int[][] cvStack = new int[54][];\n        private int stackLen;\n        private ChunkState state;\n\n        private EngineState(final int[] key, final int flags) {\n            this.key = key;\n            this.flags = flags;\n            state = new ChunkState(key, 0, flags);\n        }\n\n        // Section 5.1.2 of the BLAKE3 spec explains this algorithm in more detail.\n        private void addChunkCV(final int[] firstCV, final long totalChunks) {\n            // This chunk might complete some subtrees. For each completed subtree,\n            // its left child will be the current top entry in the CV stack, and\n            // its right child will be the current value of `newCV`. Pop each left\n            // child off the stack, merge it with `newCV`, and overwrite `newCV`\n            // with the result. After all these merges, push the final value of\n            // `newCV` onto the stack. The number of completed subtrees is given\n            // by the number of trailing 0-bits in the new total number of chunks.\n            int[] newCV = firstCV;\n            long chunkCounter = totalChunks;\n            while ((chunkCounter & 1) == 0) {\n                newCV = parentChainingValue(popCV(), newCV, key, flags);\n                chunkCounter >>= 1;\n            }\n            pushCV(newCV);\n        }\n\n        private void inputData(final byte[] in, int offset, int length) {\n            while (length > 0) {\n                // If the current chunk is complete, finalize it and reset the\n                // chunk state. More input is coming, so this chunk is not ROOT.\n                if (state.length() == CHUNK_LEN) {\n                    final int[] chunkCV = state.output().chainingValue();\n                    final long totalChunks = state.chunkCounter + 1;\n                    addChunkCV(chunkCV, totalChunks);\n                    state = new ChunkState(key, totalChunks, flags);\n                }\n\n                // Compress input bytes into the current chunk state.\n                final int want = CHUNK_LEN - state.length();\n                final int take = Math.min(want, length);\n                state.update(in, offset, take);\n                offset += take;\n                length -= take;\n            }\n        }\n\n        private void outputHash(final byte[] out, final int offset, final int length) {\n            // Starting with the Output from the current chunk, compute all the\n            // parent chaining values along the right edge of the tree, until we\n            // have the root Output.\n            Output output = state.output();\n            int parentNodesRemaining = stackLen;\n            while (parentNodesRemaining-- > 0) {\n                final int[] parentCV = cvStack[parentNodesRemaining];\n                output = parentOutput(parentCV, output.chainingValue(), key, flags);\n            }\n            output.rootOutputBytes(out, offset, length);\n        }\n\n        private int[] popCV() {\n            return cvStack[--stackLen];\n        }\n\n        private void pushCV(final int[] cv) {\n            cvStack[stackLen++] = cv;\n        }\n\n        private void reset() {\n            stackLen = 0;\n            Arrays.fill(cvStack, null);\n            state = new ChunkState(key, 0, flags);\n        }\n    }\n\n    /**\n     * Represents the state just prior to either producing an eight word chaining value or any number of output bytes\n     * when the ROOT flag is set.\n     */\n    private static final class Output {\n\n        private final int[] inputChainingValue;\n        private final int[] blockWords;\n        private final long counter;\n        private final int blockLength;\n        private final int flags;\n\n        private Output(final int[] inputChainingValue, final int[] blockWords, final long counter, final int blockLength, final int flags) {\n            this.inputChainingValue = inputChainingValue;\n            this.blockWords = blockWords;\n            this.counter = counter;\n            this.blockLength = blockLength;\n            this.flags = flags;\n        }\n\n        private int[] chainingValue() {\n            return Arrays.copyOf(compress(inputChainingValue, blockWords, blockLength, counter, flags), CHAINING_VALUE_INTS);\n        }\n\n        private void rootOutputBytes(final byte[] out, int offset, int length) {\n            int outputBlockCounter = 0;\n            while (length > 0) {\n                int chunkLength = Math.min(OUT_LEN * 2, length);\n                length -= chunkLength;\n                final int[] words = compress(inputChainingValue, blockWords, blockLength, outputBlockCounter++, flags | ROOT);\n                int wordCounter = 0;\n                while (chunkLength > 0) {\n                    final int wordLength = Math.min(Integer.BYTES, chunkLength);\n                    packInt(words[wordCounter++], out, offset, wordLength);\n                    offset += wordLength;\n                    chunkLength -= wordLength;\n                }\n            }\n        }\n    }\n\n    private static final int BLOCK_LEN = 64;\n    private static final int BLOCK_INTS = BLOCK_LEN / Integer.BYTES;\n    private static final int KEY_LEN = 32;\n    private static final int KEY_INTS = KEY_LEN / Integer.BYTES;\n    private static final int OUT_LEN = 32;\n    private static final int CHUNK_LEN = 1024;\n    private static final int CHAINING_VALUE_INTS = 8;\n\n    /**\n     * Standard hash key used for plain hashes; same initialization vector as Blake2s.\n     */\n    private static final int[] IV = { 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 };\n\n    // domain flags\n    private static final int CHUNK_START = 1;\n    private static final int CHUNK_END = 1 << 1;\n    private static final int PARENT = 1 << 2;\n    private static final int ROOT = 1 << 3;\n    private static final int KEYED_HASH = 1 << 4;\n    private static final int DERIVE_KEY_CONTEXT = 1 << 5;\n    private static final int DERIVE_KEY_MATERIAL = 1 << 6;\n\n    /**\n     * Pre-permuted for all 7 rounds; the second row (2,6,3,...) indicates the base permutation.\n     */\n    // @formatter:off\n    private static final byte[][] MSG_SCHEDULE = {\n            { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },\n            { 2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8 },\n            { 3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1 },\n            { 10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6 },\n            { 12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4 },\n            { 9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7 },\n            { 11, 15, 5, 0, 1, 9, 8, 6, 14, 10, 2, 12, 3, 4, 7, 13 }\n    };\n    // @formatter:on\n\n    private static void checkBufferArgs(final byte[] buffer, final int offset, final int length) {\n        Objects.requireNonNull(buffer);\n        if (offset < 0) {\n            throw new IndexOutOfBoundsException(\"Offset must be non-negative\");\n        }\n        if (length < 0) {\n            throw new IndexOutOfBoundsException(\"Length must be non-negative\");\n        }\n        final int bufferLength = buffer.length;\n        if (offset > bufferLength - length) {\n            throw new IndexOutOfBoundsException(\"Offset \" + offset + \" and length \" + length + \" out of bounds with buffer length \" + bufferLength);\n        }\n    }\n\n    private static int[] compress(final int[] chainingValue, final int[] blockWords, final int blockLength, final long counter, final int flags) {\n        final int[] state = Arrays.copyOf(chainingValue, BLOCK_INTS);\n        System.arraycopy(IV, 0, state, 8, 4);\n        state[12] = (int) counter;\n        state[13] = (int) (counter >> Integer.SIZE);\n        state[14] = blockLength;\n        state[15] = flags;\n        for (int i = 0; i < 7; i++) {\n            final byte[] schedule = MSG_SCHEDULE[i];\n            round(state, blockWords, schedule);\n        }\n        for (int i = 0; i < state.length / 2; i++) {\n            state[i] ^= state[i + 8];\n            state[i + 8] ^= chainingValue[i];\n        }\n        return state;\n    }\n\n    /**\n     * The mixing function, G, which mixes either a column or a diagonal.\n     */\n    private static void g(final int[] state, final int a, final int b, final int c, final int d, final int mx, final int my) {\n        state[a] += state[b] + mx;\n        state[d] = Integer.rotateRight(state[d] ^ state[a], 16);\n        state[c] += state[d];\n        state[b] = Integer.rotateRight(state[b] ^ state[c], 12);\n        state[a] += state[b] + my;\n        state[d] = Integer.rotateRight(state[d] ^ state[a], 8);\n        state[c] += state[d];\n        state[b] = Integer.rotateRight(state[b] ^ state[c], 7);\n    }\n\n    /**\n     * Calculates the Blake3 hash of the provided data.\n     *\n     * @param data source array to absorb data from\n     * @return 32-byte hash squeezed from the provided data\n     * @throws NullPointerException if data is null\n     */\n    public static byte[] hash(final byte[] data) {\n        return Blake3.initHash().update(data).doFinalize(OUT_LEN);\n    }\n\n    /**\n     * Constructs a fresh Blake3 hash function. The instance returned functions as an arbitrary length message digest.\n     *\n     * @return fresh Blake3 instance in hashed mode\n     */\n    public static Blake3 initHash() {\n        return new Blake3(IV, 0);\n    }\n\n    /**\n     * Constructs a fresh Blake3 key derivation function using the provided key derivation context byte string.\n     * The instance returned functions as a key-derivation function which can further absorb additional context data\n     * before squeezing derived key data.\n     *\n     * @param kdfContext a globally unique key-derivation context byte string to separate key derivation contexts from each other\n     * @return fresh Blake3 instance in key derivation mode\n     * @throws NullPointerException if kdfContext is null\n     */\n    public static Blake3 initKeyDerivationFunction(final byte[] kdfContext) {\n        Objects.requireNonNull(kdfContext);\n        final EngineState kdf = new EngineState(IV, DERIVE_KEY_CONTEXT);\n        kdf.inputData(kdfContext, 0, kdfContext.length);\n        final byte[] key = new byte[KEY_LEN];\n        kdf.outputHash(key, 0, key.length);\n        return new Blake3(unpackInts(key, KEY_INTS), DERIVE_KEY_MATERIAL);\n    }\n\n    /**\n     * Constructs a fresh Blake3 keyed hash function. The instance returned functions as a pseudorandom function (PRF) or as a\n     * message authentication code (MAC).\n     *\n     * @param key 32-byte secret key\n     * @return fresh Blake3 instance in keyed mode using the provided key\n     * @throws NullPointerException     if key is null\n     * @throws IllegalArgumentException if key is not 32 bytes\n     */\n    public static Blake3 initKeyedHash(final byte[] key) {\n        Objects.requireNonNull(key);\n        if (key.length != KEY_LEN) {\n            throw new IllegalArgumentException(\"Blake3 keys must be 32 bytes\");\n        }\n        return new Blake3(unpackInts(key, KEY_INTS), KEYED_HASH);\n    }\n\n    /**\n     * Calculates the Blake3 keyed hash (MAC) of the provided data.\n     *\n     * @param key  32-byte secret key\n     * @param data source array to absorb data from\n     * @return 32-byte mac squeezed from the provided data\n     * @throws NullPointerException if key or data are null\n     */\n    public static byte[] keyedHash(final byte[] key, final byte[] data) {\n        return Blake3.initKeyedHash(key).update(data).doFinalize(OUT_LEN);\n    }\n\n    private static void packInt(final int value, final byte[] dst, final int off, final int len) {\n        for (int i = 0; i < len; i++) {\n            dst[off + i] = (byte) (value >>> i * Byte.SIZE);\n        }\n    }\n\n    private static int[] parentChainingValue(final int[] leftChildCV, final int[] rightChildCV, final int[] key, final int flags) {\n        return parentOutput(leftChildCV, rightChildCV, key, flags).chainingValue();\n    }\n\n    private static Output parentOutput(final int[] leftChildCV, final int[] rightChildCV, final int[] key, final int flags) {\n        final int[] blockWords = Arrays.copyOf(leftChildCV, BLOCK_INTS);\n        System.arraycopy(rightChildCV, 0, blockWords, 8, CHAINING_VALUE_INTS);\n        return new Output(key.clone(), blockWords, 0, BLOCK_LEN, flags | PARENT);\n    }\n\n    private static void round(final int[] state, final int[] msg, final byte[] schedule) {\n        // Mix the columns.\n        g(state, 0, 4, 8, 12, msg[schedule[0]], msg[schedule[1]]);\n        g(state, 1, 5, 9, 13, msg[schedule[2]], msg[schedule[3]]);\n        g(state, 2, 6, 10, 14, msg[schedule[4]], msg[schedule[5]]);\n        g(state, 3, 7, 11, 15, msg[schedule[6]], msg[schedule[7]]);\n\n        // Mix the diagonals.\n        g(state, 0, 5, 10, 15, msg[schedule[8]], msg[schedule[9]]);\n        g(state, 1, 6, 11, 12, msg[schedule[10]], msg[schedule[11]]);\n        g(state, 2, 7, 8, 13, msg[schedule[12]], msg[schedule[13]]);\n        g(state, 3, 4, 9, 14, msg[schedule[14]], msg[schedule[15]]);\n    }\n\n    private static int unpackInt(final byte[] buf, final int off) {\n        return buf[off] & 0xFF | (buf[off + 1] & 0xFF) << 8 | (buf[off + 2] & 0xFF) << 16 | (buf[off + 3] & 0xFF) << 24;\n    }\n\n    private static int[] unpackInts(final byte[] buf, final int nrInts) {\n        final int[] values = new int[nrInts];\n        for (int i = 0, off = 0; i < nrInts; i++, off += Integer.BYTES) {\n            values[i] = unpackInt(buf, off);\n        }\n        return values;\n    }\n\n    private final EngineState engineState;\n\n    private Blake3(final int[] key, final int flags) {\n        engineState = new EngineState(key, flags);\n    }\n\n    /**\n     * Finalizes hash output data that depends on the sequence of updated bytes preceding this invocation and any\n     * previously finalized bytes. Note that this can finalize up to 2<sup>64</sup> bytes per instance.\n     *\n     * @param out destination array to finalize bytes into\n     * @return {@code this} instance.\n     * @throws NullPointerException if out is null\n     */\n    public Blake3 doFinalize(final byte[] out) {\n        return doFinalize(out, 0, out.length);\n    }\n\n    /**\n     * Finalizes an arbitrary number of bytes into the provided output array that depends on the sequence of previously\n     * updated and finalized bytes. Note that this can finalize up to 2<sup>64</sup> bytes per instance.\n     *\n     * @param out    destination array to finalize bytes into\n     * @param offset where in the array to begin writing bytes to\n     * @param length number of bytes to finalize\n     * @return {@code this} instance.\n     * @throws NullPointerException      if out is null\n     * @throws IndexOutOfBoundsException if offset or length are negative or if offset + length is greater than the\n     *                                   length of the provided array\n     */\n    public Blake3 doFinalize(final byte[] out, final int offset, final int length) {\n        checkBufferArgs(out, offset, length);\n        engineState.outputHash(out, offset, length);\n        return this;\n    }\n\n    /**\n     * Squeezes and returns an arbitrary number of bytes dependent on the sequence of previously absorbed and squeezed bytes.\n     *\n     * @param nrBytes number of bytes to finalize\n     * @return requested number of finalized bytes\n     * @throws IllegalArgumentException if nrBytes is negative\n     */\n    public byte[] doFinalize(final int nrBytes) {\n        if (nrBytes < 0) {\n            throw new IllegalArgumentException(\"Requested bytes must be non-negative\");\n        }\n        final byte[] hash = new byte[nrBytes];\n        doFinalize(hash);\n        return hash;\n    }\n\n    /**\n     * Resets this instance back to its initial state when it was first constructed.\n     * @return {@code this} instance.\n     */\n    public Blake3 reset() {\n        engineState.reset();\n        return this;\n    }\n\n    /**\n     * Updates this hash state using the provided bytes.\n     *\n     * @param in source array to update data from\n     * @return {@code this} instance.\n     * @throws NullPointerException if in is null\n     */\n    public Blake3 update(final byte[] in) {\n        return update(in, 0, in.length);\n    }\n\n    /**\n     * Updates this hash state using the provided bytes at an offset.\n     *\n     * @param in     source array to update data from\n     * @param offset where in the array to begin reading bytes\n     * @param length number of bytes to update\n     * @return {@code this} instance.\n     * @throws NullPointerException      if in is null\n     * @throws IndexOutOfBoundsException if offset or length are negative or if offset + length is greater than the\n     *                                   length of the provided array\n     */\n    public Blake3 update(final byte[] in, final int offset, final int length) {\n        checkBufferArgs(in, offset, length);\n        engineState.inputData(in, offset, length);\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/ScryptJava.java",
    "content": "package peergos.server.crypto.hash;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.function.Supplier;\nimport java.util.logging.*;\n\nimport java.security.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.server.crypto.hash.lambdaworks.crypto.SCrypt;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport javax.crypto.*;\nimport javax.crypto.spec.*;\n\npublic class ScryptJava implements Hasher {\n\tprivate static final Logger LOG = Logger.getGlobal();\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n    private static final int LOG_2_MIN_RAM = 17;\n\n    @Override\n    public CompletableFuture<byte[]> hashToKeyBytes(String username, String password, SecretGenerationAlgorithm algorithm) {\n        CompletableFuture<byte[]> res = new CompletableFuture<>();\n        if (algorithm.getType() == SecretGenerationAlgorithm.Type.Scrypt) {\n            byte[] hash = Hash.sha256(password.getBytes());\n            byte[] salt = username.getBytes();\n            try {\n                ScryptGenerator params = (ScryptGenerator) algorithm;\n                long t1 = System.currentTimeMillis();\n                int parallelism = params.parallelism;\n                int nOutputBytes = params.outputBytes;\n                int cpuCost = params.cpuCost;\n                int memoryCost = 1 << params.memoryCost; // Amount of ram required to run algorithm in bytes\n                byte[] scryptHash = SCrypt.scrypt(hash, salt, memoryCost, cpuCost, parallelism, nOutputBytes);\n                long t2 = System.currentTimeMillis();\n                LOG.info(\"Scrypt hashing took: \" + (t2 - t1) + \" mS\");\n                res.complete(scryptHash);\n                return res;\n            } catch (GeneralSecurityException gse) {\n                res.completeExceptionally(gse);\n            }\n            return res;\n        }\n        throw new IllegalStateException(\"Unknown user generation algorithm: \" + algorithm);\n    }\n\n    @Override\n    public CompletableFuture<ProofOfWork> generateProofOfWork(int difficulty, byte[] data) {\n        byte[] combined = new byte[data.length + ProofOfWork.PREFIX_BYTES];\n        System.arraycopy(data, 0, combined, ProofOfWork.PREFIX_BYTES, data.length);\n        long counter = 0;\n        while (true) {\n            byte[] hash = Hash.sha256(combined);\n            if (ProofOfWork.satisfiesDifficulty(difficulty, hash)) {\n                byte[] prefix = Arrays.copyOfRange(combined, 0, ProofOfWork.PREFIX_BYTES);\n                return Futures.of(new ProofOfWork(prefix, Multihash.Type.sha2_256));\n            }\n            counter++;\n            combined[0] = (byte) counter;\n            combined[1] = (byte) (counter >> 8);\n            combined[2] = (byte) (counter >> 16);\n            combined[3] = (byte) (counter >> 24);\n            combined[4] = (byte) (counter >> 32);\n            combined[5] = (byte) (counter >> 40);\n            combined[6] = (byte) (counter >> 48);\n            combined[7] = (byte) (counter >> 56);\n        }\n    }\n\n    @Override\n    public CompletableFuture<byte[]> sha256(byte[] input) {\n        return CompletableFuture.completedFuture(Hash.sha256(input));\n    }\n\n    @Override\n    public CompletableFuture<byte[]> hmacSha256(byte[] secretKeyBytes, byte[] message) {\n        try {\n            String algorithm = \"HMACSHA256\";\n            Mac mac = Mac.getInstance(algorithm);\n            SecretKey secretKey = new SecretKeySpec(secretKeyBytes, algorithm);\n            mac.init(secretKey);\n            return Futures.of(mac.doFinal(message));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public byte[] blake2b(byte[] input, int outputBytes) {\n        return Blake2b.Digest.newInstance(outputBytes).digest(input);\n    }\n\n    @Override\n    public CompletableFuture<Multihash> hashFromStream(AsyncReader stream, long length) {\n        return Hash.sha256(stream, length)\n                .thenApply(h -> new Multihash(Multihash.Type.sha2_256, h));\n    }\n\n    public static List<byte[]> hashChunks(InputStream fin, long size) {\n        List<byte[]> chunkHashes = new ArrayList<>();\n        int chunkOffset = 0;\n        byte[] buf = new byte[64 * 1024];\n        try {\n            MessageDigest chunkHash = MessageDigest.getInstance(\"SHA-256\");\n            for (long i = 0; i < size; ) {\n                int read = fin.read(buf);\n                chunkOffset += read;\n                if (chunkOffset >= Chunk.MAX_SIZE) {\n                    int thisChunk = read - chunkOffset + Chunk.MAX_SIZE;\n                    chunkHash.update(buf, 0, thisChunk);\n                    chunkHashes.add(chunkHash.digest());\n                    chunkHash = MessageDigest.getInstance(\"SHA-256\");\n                    int leftover = read - thisChunk;\n                    if (leftover > 0)\n                        chunkHash.update(buf, thisChunk, leftover);\n                    chunkOffset = leftover;\n                } else\n                    chunkHash.update(buf, 0, read);\n                i += read;\n            }\n            if (size == 0 || size % Chunk.MAX_SIZE != 0)\n                chunkHashes.add(chunkHash.digest());\n            return chunkHashes;\n        } catch (IOException | NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static List<byte[]> parallelHashChunks(Supplier<InputStream> fins, int nThreads, long size) {\n        int nChunks = (int) ((size + Chunk.MAX_SIZE - 1)/ Chunk.MAX_SIZE);\n        long chunksPerThread = (nChunks + nThreads - 1) / nThreads;\n        if (size < Chunk.MAX_SIZE)\n            try (InputStream fin = fins.get()) {\n                return hashChunks(fin, size);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        return IntStream.range(0, nThreads)\n                .parallel()\n                .mapToObj(i -> {\n                    try (InputStream fin = fins.get()) {\n                        long start = i * chunksPerThread * Chunk.MAX_SIZE;\n                        long end = Math.min(size, (i + 1) * chunksPerThread * Chunk.MAX_SIZE);\n                        if (start == end || start > size)\n                            return Collections.<byte[]>emptyList();\n                        long skipped = fin.skip(start);\n                        if (skipped != start)\n                            throw new IllegalStateException(\"Skip did not complete!\");\n                        return hashChunks(fin, end - start);\n                    } catch (IOException e) {\n                        throw new IllegalStateException(e);\n                    }\n                })\n                .flatMap(List::stream)\n                .collect(Collectors.toList());\n    }\n\n    public static HashTree hashFile(Path p, Hasher hasher) {\n        long size = p.toFile().length();\n        List<byte[]> chunkHashes = parallelHashChunks(() -> {\n            try {\n                return new FileInputStream(p.toFile());\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }, Runtime.getRuntime().availableProcessors(), size);\n        return HashTree.build(chunkHashes, hasher).join();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/lambdaworks/codec/Base64.java",
    "content": "// Copyright (C) 2011 - Will Glozer.  All rights reserved.\n\npackage peergos.server.crypto.hash.lambdaworks.codec;\n\nimport java.util.Arrays;\n\n/**\n * High-performance base64 codec based on the algorithm used in Mikael Grev's MiG Base64.\n * This implementation is designed to handle base64 without line splitting and with\n * optional padding. Alternative character tables may be supplied to the {@code encode}\n * and {@code decode} methods to implement modified base64 schemes.\n *\n * Decoding assumes correct input, the caller is responsible for ensuring that the input\n * contains no invalid characters.\n *\n * @author Will Glozer\n */\npublic class Base64 {\n    private static final char[] encode = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\".toCharArray();\n    private static final int[]  decode = new int[128];\n    private static final char   pad    = '=';\n\n    static {\n        Arrays.fill(decode, -1);\n        for (int i = 0; i < encode.length; i++) {\n            decode[encode[i]] = i;\n        }\n        decode[pad] = 0;\n    }\n\n    /**\n     * Decode base64 chars to bytes.\n     *\n     * @param chars Chars to encode.\n     *\n     * @return Decoded bytes.\n     */\n    public static byte[] decode(char[] chars) {\n        return decode(chars, decode, pad);\n    }\n\n    /**\n     * Encode bytes to base64 chars, with padding.\n     *\n     * @param bytes Bytes to encode.\n     *\n     * @return Encoded chars.\n     */\n    public static char[] encode(byte[] bytes) {\n        return encode(bytes, encode, pad);\n    }\n\n    /**\n     * Encode bytes to base64 chars, with optional padding.\n     *\n     * @param bytes     Bytes to encode.\n     * @param padded    Add padding to output.\n     *\n     * @return Encoded chars.\n     */\n    public static char[] encode(byte[] bytes, boolean padded) {\n        return encode(bytes, encode, padded ? pad : 0);\n    }\n\n    /**\n     * Decode base64 chars to bytes using the supplied decode table and padding\n     * character.\n     *\n     * @param src   Base64 encoded data.\n     * @param table Decode table.\n     * @param pad   Padding character.\n     *\n     * @return Decoded bytes.\n     */\n    public static byte[] decode(char[] src, int[] table, char pad) {\n        int len = src.length;\n\n        if (len == 0) return new byte[0];\n\n        int padCount = (src[len - 1] == pad ? (src[len - 2] == pad ? 2 : 1) : 0);\n        int bytes    = (len * 6 >> 3) - padCount;\n        int blocks   = (bytes / 3) * 3;\n\n        byte[] dst = new byte[bytes];\n        int si = 0, di = 0;\n\n        while (di < blocks) {\n            int n = table[src[si++]] << 18 | table[src[si++]] << 12 | table[src[si++]] << 6 | table[src[si++]];\n            dst[di++] = (byte) (n >> 16);\n            dst[di++] = (byte) (n >>  8);\n            dst[di++] = (byte) n;\n        }\n\n        if (di < bytes) {\n            int n = 0;\n            switch (len - si) {\n                case 4: n |= table[src[si+3]];\n                case 3: n |= table[src[si+2]] <<  6;\n                case 2: n |= table[src[si+1]] << 12;\n                case 1: n |= table[src[si]]   << 18;\n            }\n            for (int r = 16; di < bytes; r -= 8) {\n                dst[di++] = (byte) (n >> r);\n            }\n        }\n\n        return dst;\n    }\n\n    /**\n     * Encode bytes to base64 chars using the supplied encode table and with\n     * optional padding.\n     *\n     * @param src   Bytes to encode.\n     * @param table Encoding table.\n     * @param pad   Padding character, or 0 for no padding.\n     *\n     * @return Encoded chars.\n     */\n    public static char[] encode(byte[] src, char[] table, char pad) {\n        int len = src.length;\n\n        if (len == 0) return new char[0];\n\n        int blocks = (len / 3) * 3;\n        int chars  = ((len - 1) / 3 + 1) << 2;\n        int tail   = len - blocks;\n        if (pad == 0 && tail > 0) chars -= 3 - tail;\n\n        char[] dst = new char[chars];\n        int si = 0, di = 0;\n\n        while (si < blocks) {\n            int n = (src[si++] & 0xff) << 16 | (src[si++] & 0xff) << 8 | (src[si++] & 0xff);\n            dst[di++] = table[(n >>> 18) & 0x3f];\n            dst[di++] = table[(n >>> 12) & 0x3f];\n            dst[di++] = table[(n >>>  6) & 0x3f];\n            dst[di++] = table[n          & 0x3f];\n        }\n\n        if (tail > 0) {\n            int n = (src[si] & 0xff) << 10;\n            if (tail == 2) n |= (src[++si] & 0xff) << 2;\n\n            dst[di++] = table[(n >>> 12) & 0x3f];\n            dst[di++] = table[(n >>> 6)  & 0x3f];\n            if (tail == 2) dst[di++] = table[n & 0x3f];\n\n            if (pad != 0) {\n                if (tail == 1) dst[di++] = pad;\n                dst[di] = pad;\n            }\n        }\n\n        return dst;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/lambdaworks/crypto/PBKDF.java",
    "content": "// Copyright (C) 2011 - Will Glozer.  All rights reserved.\n\npackage peergos.server.crypto.hash.lambdaworks.crypto;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.security.GeneralSecurityException;\nimport static java.lang.System.arraycopy;\n\n/**\n * An implementation of the Password-Based Key Derivation Function as specified\n * in RFC 2898.\n *\n * @author  Will Glozer\n */\npublic class PBKDF {\n    /**\n     * Implementation of PBKDF2 (RFC2898).\n     *\n     * @param   alg     HMAC algorithm to use.\n     * @param   P       Password.\n     * @param   S       Salt.\n     * @param   c       Iteration count.\n     * @param   dkLen   Intended length, in octets, of the derived key.\n     *\n     * @return  The derived key.\n     *\n     * @throws  GeneralSecurityException\n     */\n    public static byte[] pbkdf2(String alg, byte[] P, byte[] S, int c, int dkLen) throws GeneralSecurityException {\n        Mac mac = Mac.getInstance(alg);\n        mac.init(new SecretKeySpec(P, alg));\n        byte[] DK = new byte[dkLen];\n        pbkdf2(mac, S, c, DK, dkLen);\n        return DK;\n    }\n\n    /**\n     * Implementation of PBKDF2 (RFC2898).\n     *\n     * @param   mac     Pre-initialized {@link Mac} instance to use.\n     * @param   S       Salt.\n     * @param   c       Iteration count.\n     * @param   DK      Byte array that derived key will be placed in.\n     * @param   dkLen   Intended length, in octets, of the derived key.\n     *\n     * @throws  GeneralSecurityException\n     */\n    public static void pbkdf2(Mac mac, byte[] S, int c, byte[] DK, int dkLen) throws GeneralSecurityException {\n        int hLen = mac.getMacLength();\n\n        if (dkLen > (Math.pow(2, 32) - 1) * hLen) {\n            throw new GeneralSecurityException(\"Requested key length too long\");\n        }\n\n        byte[] U      = new byte[hLen];\n        byte[] T      = new byte[hLen];\n        byte[] block1 = new byte[S.length + 4];\n\n        int l = (int) Math.ceil((double) dkLen / hLen);\n        int r = dkLen - (l - 1) * hLen;\n\n        arraycopy(S, 0, block1, 0, S.length);\n\n        for (int i = 1; i <= l; i++) {\n            block1[S.length + 0] = (byte) (i >> 24 & 0xff);\n            block1[S.length + 1] = (byte) (i >> 16 & 0xff);\n            block1[S.length + 2] = (byte) (i >> 8  & 0xff);\n            block1[S.length + 3] = (byte) (i >> 0  & 0xff);\n\n            mac.update(block1);\n            mac.doFinal(U, 0);\n            arraycopy(U, 0, T, 0, hLen);\n\n            for (int j = 1; j < c; j++) {\n                mac.update(U);\n                mac.doFinal(U, 0);\n\n                for (int k = 0; k < hLen; k++) {\n                    T[k] ^= U[k];\n                }\n            }\n\n            arraycopy(T, 0, DK, (i - 1) * hLen, (i == l ? r : hLen));\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/lambdaworks/crypto/SCrypt.java",
    "content": "// Copyright (C) 2011 - Will Glozer.  All rights reserved.\n\npackage peergos.server.crypto.hash.lambdaworks.crypto;\n\nimport peergos.server.crypto.hash.lambdaworks.jni.*;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.security.GeneralSecurityException;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static java.lang.System.arraycopy;\n\n/**\n * An implementation of the <a href=\"http://www.tarsnap.com/scrypt/scrypt.pdf\"/>scrypt</a>\n * key derivation function. This class will attempt to load a native library\n * containing the optimized C implementation from\n * <a href=\"http://www.tarsnap.com/scrypt.html\">http://www.tarsnap.com/scrypt.html<a> and\n * fall back to the pure Java version if that fails.\n *\n * @author  Will Glozer\n */\npublic class SCrypt {\n    private static final boolean native_library_loaded;\n\n    static {\n        LibraryLoader loader = LibraryLoaders.loader();\n        native_library_loaded = loader.load(\"scrypt\", true);\n    }\n\n    /**\n     * Implementation of the <a href=\"http://www.tarsnap.com/scrypt/scrypt.pdf\"/>scrypt KDF</a>.\n     * Calls the native implementation {@link #scryptN} when the native library was successfully\n     * loaded, otherwise calls {@link #scryptJ}.\n     *\n     * @param passwd    Password.\n     * @param salt      Salt.\n     * @param N         CPU cost parameter.\n     * @param r         Memory cost parameter.\n     * @param p         Parallelization parameter.\n     * @param dkLen     Intended length of the derived key.\n     *\n     * @return The derived key.\n     *\n     * @throws GeneralSecurityException when HMAC_SHA256 is not available.\n     */\n    public static byte[] scrypt(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException {\n        return native_library_loaded ? scryptN(passwd, salt, N, r, p, dkLen) : scryptJ(passwd, salt, N, r, p, dkLen);\n    }\n\n    /**\n     * Native C implementation of the <a href=\"http://www.tarsnap.com/scrypt/scrypt.pdf\"/>scrypt KDF</a> using\n     * the code from <a href=\"http://www.tarsnap.com/scrypt.html\">http://www.tarsnap.com/scrypt.html<a>.\n     *\n     * @param passwd    Password.\n     * @param salt      Salt.\n     * @param N         CPU cost parameter.\n     * @param r         Memory cost parameter.\n     * @param p         Parallelization parameter.\n     * @param dkLen     Intended length of the derived key.\n     *\n     * @return The derived key.\n     */\n    public static native byte[] scryptN(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen);\n\n    /**\n     * Pure Java implementation of the <a href=\"http://www.tarsnap.com/scrypt/scrypt.pdf\"/>scrypt KDF</a>.\n     *\n     * @param passwd    Password.\n     * @param salt      Salt.\n     * @param N         CPU cost parameter.\n     * @param r         Memory cost parameter.\n     * @param p         Parallelization parameter.\n     * @param dkLen     Intended length of the derived key.\n     *\n     * @return The derived key.\n     *\n     * @throws GeneralSecurityException when HMAC_SHA256 is not available.\n     */\n    public static byte[] scryptJ(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException {\n        if (N < 2 || (N & (N - 1)) != 0) throw new IllegalArgumentException(\"N must be a power of 2 greater than 1\");\n\n        if (N > MAX_VALUE / 128 / r) throw new IllegalArgumentException(\"Parameter N is too large\");\n        if (r > MAX_VALUE / 128 / p) throw new IllegalArgumentException(\"Parameter r is too large\");\n\n        Mac mac = Mac.getInstance(\"HmacSHA256\");\n        mac.init(new SecretKeySpec(passwd, \"HmacSHA256\"));\n\n        byte[] DK = new byte[dkLen];\n\n        byte[] B  = new byte[128 * r * p];\n        byte[] XY = new byte[256 * r];\n        byte[] V  = new byte[128 * r * N];\n        int i;\n\n        PBKDF.pbkdf2(mac, salt, 1, B, p * 128 * r);\n\n        for (i = 0; i < p; i++) {\n            smix(B, i * 128 * r, r, N, V, XY);\n        }\n\n        PBKDF.pbkdf2(mac, B, 1, DK, dkLen);\n\n        return DK;\n    }\n\n    public static void smix(byte[] B, int Bi, int r, int N, byte[] V, byte[] XY) {\n        int Xi = 0;\n        int Yi = 128 * r;\n        int i;\n\n        arraycopy(B, Bi, XY, Xi, 128 * r);\n\n        for (i = 0; i < N; i++) {\n            arraycopy(XY, Xi, V, i * (128 * r), 128 * r);\n            blockmix_salsa8(XY, Xi, Yi, r);\n        }\n\n        for (i = 0; i < N; i++) {\n            int j = integerify(XY, Xi, r) & (N - 1);\n            blockxor(V, j * (128 * r), XY, Xi, 128 * r);\n            blockmix_salsa8(XY, Xi, Yi, r);\n        }\n\n        arraycopy(XY, Xi, B, Bi, 128 * r);\n    }\n\n    public static void blockmix_salsa8(byte[] BY, int Bi, int Yi, int r) {\n        byte[] X = new byte[64];\n        int i;\n\n        arraycopy(BY, Bi + (2 * r - 1) * 64, X, 0, 64);\n\n        for (i = 0; i < 2 * r; i++) {\n            blockxor(BY, i * 64, X, 0, 64);\n            salsa20_8(X);\n            arraycopy(X, 0, BY, Yi + (i * 64), 64);\n        }\n\n        for (i = 0; i < r; i++) {\n            arraycopy(BY, Yi + (i * 2) * 64, BY, Bi + (i * 64), 64);\n        }\n\n        for (i = 0; i < r; i++) {\n            arraycopy(BY, Yi + (i * 2 + 1) * 64, BY, Bi + (i + r) * 64, 64);\n        }\n    }\n\n    public static int R(int a, int b) {\n        return (a << b) | (a >>> (32 - b));\n    }\n\n    public static void salsa20_8(byte[] B) {\n        int[] B32 = new int[16];\n        int[] x   = new int[16];\n        int i;\n\n        for (i = 0; i < 16; i++) {\n            B32[i]  = (B[i * 4 + 0] & 0xff) << 0;\n            B32[i] |= (B[i * 4 + 1] & 0xff) << 8;\n            B32[i] |= (B[i * 4 + 2] & 0xff) << 16;\n            B32[i] |= (B[i * 4 + 3] & 0xff) << 24;\n        }\n\n        arraycopy(B32, 0, x, 0, 16);\n\n        for (i = 8; i > 0; i -= 2) {\n            x[ 4] ^= R(x[ 0]+x[12], 7);  x[ 8] ^= R(x[ 4]+x[ 0], 9);\n            x[12] ^= R(x[ 8]+x[ 4],13);  x[ 0] ^= R(x[12]+x[ 8],18);\n            x[ 9] ^= R(x[ 5]+x[ 1], 7);  x[13] ^= R(x[ 9]+x[ 5], 9);\n            x[ 1] ^= R(x[13]+x[ 9],13);  x[ 5] ^= R(x[ 1]+x[13],18);\n            x[14] ^= R(x[10]+x[ 6], 7);  x[ 2] ^= R(x[14]+x[10], 9);\n            x[ 6] ^= R(x[ 2]+x[14],13);  x[10] ^= R(x[ 6]+x[ 2],18);\n            x[ 3] ^= R(x[15]+x[11], 7);  x[ 7] ^= R(x[ 3]+x[15], 9);\n            x[11] ^= R(x[ 7]+x[ 3],13);  x[15] ^= R(x[11]+x[ 7],18);\n            x[ 1] ^= R(x[ 0]+x[ 3], 7);  x[ 2] ^= R(x[ 1]+x[ 0], 9);\n            x[ 3] ^= R(x[ 2]+x[ 1],13);  x[ 0] ^= R(x[ 3]+x[ 2],18);\n            x[ 6] ^= R(x[ 5]+x[ 4], 7);  x[ 7] ^= R(x[ 6]+x[ 5], 9);\n            x[ 4] ^= R(x[ 7]+x[ 6],13);  x[ 5] ^= R(x[ 4]+x[ 7],18);\n            x[11] ^= R(x[10]+x[ 9], 7);  x[ 8] ^= R(x[11]+x[10], 9);\n            x[ 9] ^= R(x[ 8]+x[11],13);  x[10] ^= R(x[ 9]+x[ 8],18);\n            x[12] ^= R(x[15]+x[14], 7);  x[13] ^= R(x[12]+x[15], 9);\n            x[14] ^= R(x[13]+x[12],13);  x[15] ^= R(x[14]+x[13],18);\n        }\n\n        for (i = 0; i < 16; ++i) B32[i] = x[i] + B32[i];\n\n        for (i = 0; i < 16; i++) {\n            B[i * 4 + 0] = (byte) (B32[i] >> 0  & 0xff);\n            B[i * 4 + 1] = (byte) (B32[i] >> 8  & 0xff);\n            B[i * 4 + 2] = (byte) (B32[i] >> 16 & 0xff);\n            B[i * 4 + 3] = (byte) (B32[i] >> 24 & 0xff);\n        }\n    }\n\n    public static void blockxor(byte[] S, int Si, byte[] D, int Di, int len) {\n        for (int i = 0; i < len; i++) {\n            D[Di + i] ^= S[Si + i];\n        }\n    }\n\n    public static int integerify(byte[] B, int Bi, int r) {\n        int n;\n\n        Bi += (2 * r - 1) * 64;\n\n        n  = (B[Bi + 0] & 0xff) << 0;\n        n |= (B[Bi + 1] & 0xff) << 8;\n        n |= (B[Bi + 2] & 0xff) << 16;\n        n |= (B[Bi + 3] & 0xff) << 24;\n\n        return n;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/lambdaworks/crypto/SCryptUtil.java",
    "content": "// Copyright (C) 2011 - Will Glozer.  All rights reserved.\n\npackage peergos.server.crypto.hash.lambdaworks.crypto;\n\nimport java.io.UnsupportedEncodingException;\nimport java.security.GeneralSecurityException;\nimport java.security.SecureRandom;\n\nimport static peergos.server.crypto.hash.lambdaworks.codec.Base64.*;\n\n/**\n * Simple {@link SCrypt} interface for hashing passwords using the\n * <a href=\"http://www.tarsnap.com/scrypt.html\">scrypt</a> key derivation function\n * and comparing a plain text password to a hashed one. The hashed output is an\n * extended implementation of the Modular Crypt Format that also includes the scrypt\n * algorithm parameters.\n *\n * Format: <code>$s0$PARAMS$SALT$KEY</code>.\n *\n * <dl>\n * <dd>PARAMS</dd><dt>32-bit hex integer containing log2(N) (16 bits), r (8 bits), and p (8 bits)</dt>\n * <dd>SALT</dd><dt>base64-encoded salt</dt>\n * <dd>KEY</dd><dt>base64-encoded derived key</dt>\n * </dl>\n *\n * <code>s0</code> identifies version 0 of the scrypt format, using a 128-bit salt and 256-bit derived key.\n *\n * @author  Will Glozer\n */\npublic class SCryptUtil {\n    /**\n     * Hash the supplied plaintext password and generate output in the format described\n     * in {@link SCryptUtil}.\n     *\n     * @param passwd    Password.\n     * @param N         CPU cost parameter.\n     * @param r         Memory cost parameter.\n     * @param p         Parallelization parameter.\n     *\n     * @return The hashed password.\n     */\n    public static String scrypt(String passwd, int N, int r, int p) {\n        try {\n            byte[] salt = new byte[16];\n            SecureRandom.getInstance(\"SHA1PRNG\").nextBytes(salt);\n\n            byte[] derived = SCrypt.scrypt(passwd.getBytes(\"UTF-8\"), salt, N, r, p, 32);\n\n            String params = Long.toString(log2(N) << 16L | r << 8 | p, 16);\n\n            StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2);\n            sb.append(\"$s0$\").append(params).append('$');\n            sb.append(encode(salt)).append('$');\n            sb.append(encode(derived));\n\n            return sb.toString();\n        } catch (UnsupportedEncodingException e) {\n            throw new IllegalStateException(\"JVM doesn't support UTF-8?\");\n        } catch (GeneralSecurityException e) {\n            throw new IllegalStateException(\"JVM doesn't support SHA1PRNG or HMAC_SHA256?\");\n        }\n    }\n\n    /**\n     * Compare the supplied plaintext password to a hashed password.\n     *\n     * @param   passwd  Plaintext password.\n     * @param   hashed  scrypt hashed password.\n     *\n     * @return true if passwd matches hashed value.\n     */\n    public static boolean check(String passwd, String hashed) {\n        try {\n            String[] parts = hashed.split(\"\\\\$\");\n\n            if (parts.length != 5 || !parts[1].equals(\"s0\")) {\n                throw new IllegalArgumentException(\"Invalid hashed value\");\n            }\n\n            long params = Long.parseLong(parts[2], 16);\n            byte[] salt = decode(parts[3].toCharArray());\n            byte[] derived0 = decode(parts[4].toCharArray());\n\n            int N = (int) Math.pow(2, params >> 16 & 0xffff);\n            int r = (int) params >> 8 & 0xff;\n            int p = (int) params      & 0xff;\n\n            byte[] derived1 = SCrypt.scrypt(passwd.getBytes(\"UTF-8\"), salt, N, r, p, 32);\n\n            if (derived0.length != derived1.length) return false;\n\n            int result = 0;\n            for (int i = 0; i < derived0.length; i++) {\n                result |= derived0[i] ^ derived1[i];\n            }\n            return result == 0;\n        } catch (UnsupportedEncodingException e) {\n            throw new IllegalStateException(\"JVM doesn't support UTF-8?\");\n        } catch (GeneralSecurityException e) {\n            throw new IllegalStateException(\"JVM doesn't support SHA1PRNG or HMAC_SHA256?\");\n        }\n    }\n\n    private static int log2(int n) {\n        int log = 0;\n        if ((n & 0xffff0000 ) != 0) { n >>>= 16; log = 16; }\n        if (n >= 256) { n >>>= 8; log += 8; }\n        if (n >= 16 ) { n >>>= 4; log += 4; }\n        if (n >= 4  ) { n >>>= 2; log += 2; }\n        return log + (n >>> 1);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/lambdaworks/jni/LibraryLoader.java",
    "content": "// Copyright (C) 2011 - Will Glozer.  All rights reserved.\n\npackage peergos.server.crypto.hash.lambdaworks.jni;\n\n/**\n * A {@code LibraryLoader} attempts to load the appropriate native library\n * for the current platform.\n *\n * @author Will Glozer\n */\npublic interface LibraryLoader {\n    /**\n     * Load a native library, and optionally verify any signatures.\n     *\n     * @param name      Name of the library to load.\n     * @param verify    Verify signatures if signed.\n     *\n     * @return true if the library was successfully loaded.\n     */\n    boolean load(String name, boolean verify);\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/lambdaworks/jni/LibraryLoaders.java",
    "content": "// Copyright (C) 2011 - Will Glozer.  All rights reserved.\n\npackage peergos.server.crypto.hash.lambdaworks.jni;\n\n/**\n * {@code LibraryLoaders} will create the appropriate {@link LibraryLoader} for\n * the VM it is running on.\n *\n * The system property {@code com.lambdaworks.jni.loader} may be used to override\n * loader auto-detection, or to disable loading native libraries entirely via use\n * of the nil loader.\n *\n * @author Will Glozer\n */\npublic class LibraryLoaders {\n    /**\n     * Create a new {@link LibraryLoader} for the current VM.\n     *\n     * @return the loader.\n     */\n    public static LibraryLoader loader() {\n    \treturn new NilLibraryLoader();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/hash/lambdaworks/jni/NilLibraryLoader.java",
    "content": "// Copyright (C) 2013 - Will Glozer.  All rights reserved.\n\npackage peergos.server.crypto.hash.lambdaworks.jni;\n\n/**\n * A native library loader that refuses to load libraries.\n *\n * @author Will Glozer\n */\npublic class NilLibraryLoader implements LibraryLoader {\n    /**\n     * Don't load a shared library.\n     *\n     * @param name      Name of the library to load.\n     * @param verify    Ignored, no verification is done.\n     *\n     * @return false.\n     */\n    public boolean load(String name, boolean verify) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/random/SafeRandomJava.java",
    "content": "package peergos.server.crypto.random;\n\nimport peergos.server.crypto.*;\nimport peergos.shared.crypto.random.*;\n\npublic class SafeRandomJava implements SafeRandom {\n\n    @Override\n    public void randombytes(byte[] b, int offset, int len) {\n        TweetNaCl.randomBytes(b, offset, len);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/crypto/symmetric/Salsa20Poly1305Java.java",
    "content": "package peergos.server.crypto.symmetric;\n\nimport peergos.server.crypto.*;\nimport peergos.shared.crypto.symmetric.*;\n\npublic class Salsa20Poly1305Java implements Salsa20Poly1305 {\n\n    @Override\n    public byte[] secretbox(byte[] data, byte[] nonce, byte[] key) {\n        return TweetNaCl.secretbox(data, nonce, key);\n    }\n\n    @Override\n    public byte[] secretbox_open(byte[] cipher, byte[] nonce, byte[] key) {\n        return TweetNaCl.secretbox_open(cipher, nonce, key);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/fuse/CachingPeergosFS.java",
    "content": "package peergos.server.fuse;\nimport java.util.logging.*;\n\nimport peergos.server.util.Logging;\n\nimport jnr.ffi.Pointer;\nimport jnr.ffi.types.off_t;\nimport jnr.ffi.types.size_t;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.fs.Chunk;\nimport peergos.shared.user.fs.FileProperties;\nimport peergos.shared.util.*;\nimport ru.serce.jnrfuse.ErrorCodes;\nimport ru.serce.jnrfuse.struct.*;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class CachingPeergosFS extends PeergosFS {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final int DEFAULT_SYNC_SLEEP = 1000*30;\n    private static final int DEFAULT_CACHE_SIZE = 1;\n    private static final boolean DEBUG = true;\n\n    private final ConcurrentMap<String, CacheEntryHolder> entryMap;\n    private final int chunkCacheSize, syncSleep;\n//    private final Thread syncRunner;\n\n    public CachingPeergosFS(UserContext userContext) {\n        this(userContext, DEFAULT_CACHE_SIZE, DEFAULT_SYNC_SLEEP);\n    }\n\n    public CachingPeergosFS(UserContext userContext, int chunkCacheSize, int syncSleep) {\n        super(userContext);\n\n        this.chunkCacheSize = chunkCacheSize;\n        this.syncSleep = syncSleep;\n        this.entryMap = new ConcurrentHashMap<>();\n    }\n\n    @Override\n    public int read(String s, Pointer pointer, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) {\n        try {\n            return read(s, pointer, 0, size, offset, fuseFileInfo);\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n            throw t;\n        }\n    }\n\n    public int read(String s, Pointer pointer, int pointerOffset, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) {\n        if (DEBUG)\n            System.out.printf(\"read(%s, offset=%d, size=%d)\\n\", s, offset, size);\n        if (!containedInOneChunk(offset, offset + size)) {\n            long boundary = alignToChunkSize(offset + Chunk.MAX_SIZE);\n            int r1 = read(s, pointer, 0, boundary - offset, offset, fuseFileInfo);\n            if (r1 <= 0)\n                return r1;\n            int r2 = read(s, pointer, (int)(boundary - offset), size + offset - boundary, boundary, fuseFileInfo);\n            if (r2 <= 0)\n                return r2;\n            return r1 + r2;\n        }\n\n        long startPos = alignToChunkSize(offset);\n        int chunkOffset  = intraChunkOffset(offset);\n        int iSize = (int) size;\n\n        CacheEntryHolder cacheEntryHolder = entryMap.computeIfAbsent(s, path -> new CacheEntryHolder(new CacheEntry(path, startPos)));\n        return cacheEntryHolder.apply(c -> c != null && c.offset == startPos, () -> new CacheEntry(s, startPos),\n                ce -> ce.read(pointer, pointerOffset, chunkOffset, iSize));\n    }\n\n    @Override\n    public int write(String s, Pointer pointer, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) {\n        try {\n            return write(s, pointer, 0, size, offset, fuseFileInfo);\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n            throw t;\n        }\n    }\n\n    public int write(String s, Pointer pointer, int pointerOffset, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) {\n        if (DEBUG)\n            System.out.printf(\"write(%s, offset=%d, size=%d)\\n\", s, offset, size);\n        if  (! containedInOneChunk(offset, offset+size)) {\n            long boundary = alignToChunkSize(offset + Chunk.MAX_SIZE);\n            int w1 = write(s, pointer, 0, boundary - offset, offset, fuseFileInfo);\n            if (w1 <= 0)\n                return w1;\n            int w2 = write(s, pointer, (int)(boundary - offset), size + offset - boundary, boundary, fuseFileInfo);\n            if (w2 <= 0)\n                return w2;\n            return w1 + w2;\n        }\n\n        long startPos  = alignToChunkSize(offset);\n        int  chunkOffset  = intraChunkOffset(offset);\n        int iSize = (int) size;\n\n        CacheEntryHolder cacheEntry = entryMap.computeIfAbsent(s, path -> new CacheEntryHolder(new CacheEntry(path, startPos)));\n        return cacheEntry.apply(c -> c != null && c.offset == startPos, () -> new CacheEntry(s, startPos),\n                ce -> ce.write(pointer, pointerOffset, chunkOffset, iSize));\n    }\n\n    @Override\n    public int lock(String s, FuseFileInfo fuseFileInfo, int i, Flock flock) {\n        try {\n            if (DEBUG)\n                System.out.printf(\"lock(%s)\\n\", s);\n            CacheEntryHolder cacheEntryHolder = entryMap.get(s);\n            if (cacheEntryHolder != null)\n                cacheEntryHolder.syncAndClear();\n            return 0;\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n            throw t;\n        }\n    }\n\n    @Override\n    public int flush(String s, FuseFileInfo fuseFileInfo) {\n        try {\n            if (DEBUG)\n                System.out.printf(\"flush(%s)\\n\", s);\n            CacheEntryHolder cacheEntry = entryMap.get(s);\n            if (cacheEntry != null) {\n                cacheEntry.sync();\n            }\n            return super.flush(s, fuseFileInfo);\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n            throw t;\n        }\n    }\n\n    @Override\n    protected int annotateAttributes(String fullPath, PeergosStat peergosStat, FileStat fileStat) {\n        if (DEBUG)\n            System.out.printf(\"annotate(%s)\\n\", fullPath);\n        CacheEntryHolder cacheEntry = entryMap.get(fullPath);\n        Optional<PeergosStat> updatedStat = Optional.empty();\n        if (cacheEntry != null) {\n            updatedStat = cacheEntry.applyIfPresent(ce -> {\n                if (ce == null)\n                    return peergosStat;\n                long maxSize = ce.offset + ce.maxDirtyPos;\n                if (peergosStat.properties.size < maxSize) {\n                    FileProperties updated = peergosStat.properties.withSize(maxSize);\n                    return new PeergosStat(peergosStat.treeNode, updated);\n                }\n                return peergosStat;\n            });\n        }\n        return super.annotateAttributes(fullPath, updatedStat.orElse(peergosStat), fileStat);\n    }\n\n    private boolean containedInOneChunk(long start, long end) {\n        return alignToChunkSize(start) == alignToChunkSize(end-1);\n    }\n\n    private long alignToChunkSize(long pos) {\n        return Math.max(0, pos / Chunk.MAX_SIZE) * Chunk.MAX_SIZE;\n    }\n    private int intraChunkOffset(long  pos) {\n        return (int) pos % Chunk.MAX_SIZE;\n    }\n\n    private class CacheEntryHolder {\n        private CacheEntry entry;\n\n        public CacheEntryHolder(CacheEntry entry) {\n            this.entry = entry;\n        }\n\n        public synchronized <A> A apply(Predicate<CacheEntry> correctChunk, Supplier<CacheEntry> supplier, Function<CacheEntry, A> func) {\n            if (!correctChunk.test(entry)) {\n                long oldOffset = entry != null ? entry.offset : -1;\n                syncAndClear();\n                setEntry(supplier.get());\n                LOG.info(\"Ejecting chunk from \" + entry.path + \" \" + oldOffset + \" -> \"+ entry.offset);\n\n            }\n            return func.apply(entry);\n        }\n\n        public synchronized <A> Optional<A> applyIfPresent(Function<CacheEntry, A> func) {\n            if (entry != null) {\n                return Optional.of(func.apply(entry));\n            }\n            return Optional.empty();\n        }\n\n        public synchronized void setEntry(CacheEntry entry) {\n            this.entry = entry;\n        }\n\n        public synchronized void sync() {\n            if (entry == null)\n                return;\n            if (DEBUG)\n                System.out.printf(\"sync(%s)\\n\", entry.path);\n            entry.sync();\n        }\n\n        public synchronized void syncAndClear() {\n            if (entry == null)\n                return;\n            if (DEBUG)\n                System.out.printf(\"fsync(%s)\\n\", entry.path);\n            entry.sync();\n            entry = null;\n        }\n    }\n\n    private class CacheEntry {\n        private final String path;\n        private final byte[] data;\n        private final long offset;\n        private int maxDirtyPos;\n\n        public CacheEntry(String path, long offset) {\n            this.path = path;\n            this.offset = offset;\n            this.data = new byte[Chunk.MAX_SIZE];\n            //read current data into data view\n            PeergosStat stat = getByPath(path).orElseThrow(() -> new IllegalStateException(\"missing\" + path));\n            byte[] readData = CachingPeergosFS.this.read(stat, data.length, offset)\n                    .orElseThrow(() -> new IllegalStateException(\"missing: \" + path));\n            this.maxDirtyPos = 0;\n            System.arraycopy(readData, 0, data, 0, readData.length);\n\n        }\n\n        private void ensureInBounds(int offset, int length) {\n            if (offset + length > data.length)\n                throw new  IllegalStateException(\"cannot op with offset \"+ offset +\" and length \"+ length +\" with length \"+ data.length);\n        }\n\n        public int read(Pointer pointer, int pointerOffset, int chunkOffset, int length) {\n            ensureInBounds(chunkOffset, length);\n            pointer.put(pointerOffset, data, chunkOffset, length);\n            return length;\n        }\n\n        public int write(Pointer pointer, int pointerOffset, int chunkOffset, int length) {\n            ensureInBounds(chunkOffset, length);\n            pointer.get(pointerOffset, data, chunkOffset, length);\n            maxDirtyPos = Math.max(maxDirtyPos, chunkOffset+length);\n            return length;\n        }\n\n        public void sync() {\n            Path p = PathUtil.get(path);\n\n            String parentPath = p.getParent().toString();\n            String name = p.getFileName().toString();\n\n            if (maxDirtyPos ==0)\n                return;\n            applyIfPresent(parentPath, (parent) -> CachingPeergosFS.this.write(parent, name, data, maxDirtyPos, offset), -ErrorCodes.ENOENT());\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CacheEntry that = (CacheEntry) o;\n\n            return Objects.equals(path, that.path);\n\n        }\n\n        @Override\n        public int hashCode() {\n            return path != null ? path.hashCode() : 0;\n        }\n    }\n\n    @Override\n    public void close() throws Exception {\n\n        super.close();\n    }\n}"
  },
  {
    "path": "src/peergos/server/fuse/FuseProcess.java",
    "content": "package peergos.server.fuse;\nimport java.util.logging.*;\nimport peergos.server.util.Logging;\n\nimport java.nio.file.Path;\n\npublic class FuseProcess implements Runnable, AutoCloseable {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private final PeergosFS peergosFS;\n    private final Path mountPoint;\n    private volatile boolean isFinished;\n    private volatile boolean isClosed;\n\n\n    public FuseProcess(PeergosFS peergosFS, Path mountPoint) {\n        this.peergosFS = peergosFS;\n        this.mountPoint = mountPoint;\n    }\n\n    @Override\n    public void run() {\n        while  (! isFinished) {\n            synchronized (this) {\n                try {\n                    wait(1000);\n                } catch (InterruptedException ie) {}\n            }\n        }\n        isClosed = true;\n    }\n\n    public void start() {\n        ensureNotFinished();\n\n        boolean blocking = false;\n        boolean debug = false;\n        int transferBufferSize = 5*1024*1024;\n        String[] fuseOpts = new String[]{\"-o\", \"big_writes\",\n                \"-o\", \"fsname=Peergos\",\n                \"-o\", \"max_read=\"+transferBufferSize, \"-o\", \"max_write=\"+transferBufferSize};\n        peergosFS.mount(mountPoint, blocking, debug, fuseOpts);\n\n        new Thread(this, \"Fuse process\").start();\n    }\n\n    public void close() {\n        if (isFinished)\n            return;\n        isFinished = true;\n        synchronized (this) {\n            notify();\n        }\n        LOG.info(\"CLOSE\");\n        while (! isClosed) {\n            try {\n                Thread.sleep(1000);\n            }  catch (InterruptedException ie){}\n            LOG.info(\"CALLING UNMOUNT\");\n            peergosFS.umount();\n        }\n        LOG.info(\"DONE\");\n    }\n\n    private void ensureNotFinished() {\n        if (isFinished)\n            throw new IllegalStateException();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/fuse/PeergosFS.java",
    "content": "package peergos.server.fuse;\nimport java.util.logging.*;\n\nimport peergos.server.util.Logging;\n\nimport jnr.ffi.Pointer;\nimport jnr.ffi.types.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport ru.serce.jnrfuse.ErrorCodes;\nimport ru.serce.jnrfuse.FuseFillDir;\nimport ru.serce.jnrfuse.FuseStubFS;\nimport ru.serce.jnrfuse.struct.*;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.*;\nimport java.util.*;\nimport java.util.function.*;\n\n/**\n * Nice FUSE API doc @\n * https://www.cs.hmc.edu/~geoff/classes/hmc.cs135.201001/homework/fuse/fuse_doc.html\n * also\n * https://github.com/libfuse/libfuse/blob/master/include/fuse.h\n */\npublic class PeergosFS extends FuseStubFS implements AutoCloseable {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    protected static class PeergosStat {\n        public final FileWrapper treeNode;\n        public final FileProperties properties;\n\n        public PeergosStat(FileWrapper treeNode, FileProperties properties) {\n            this.treeNode = treeNode;\n            this.properties = properties;\n        }\n    }\n\n\n    private final UserContext context;\n    protected volatile boolean isClosed;\n\n    public PeergosFS(UserContext context) {\n        this.context = context;\n    }\n\n    @Override\n    public void close() throws Exception {\n        ensureNotClosed();\n        this.isClosed = true;\n    }\n\n    private void ensureNotClosed() {\n        if  (isClosed)\n            throw new IllegalStateException(this +\" is closed\");\n    }\n\n    protected int annotateAttributes(String fullPath, PeergosStat peergosStat, FileStat fileStat) {\n        try {\n            FileWrapper fileWrapper = peergosStat.treeNode;\n            FileProperties fileProperties = peergosStat.properties;\n\n            int mode = fileWrapper.isDirectory() ?\n                    FileStat.S_IFDIR | 0755 : FileStat.S_IFREG | 0644;\n\n            fileStat.st_mode.set(mode);\n            fileStat.st_size.set(peergosStat.treeNode.getSize());\n\n            Instant instant = Optional.ofNullable(fileProperties).map(p -> p.modified.toInstant(ZonedDateTime.now().getOffset())).orElse(Instant.EPOCH);\n            long epochSecond = instant.getEpochSecond();\n            long nanoSeconds = instant.getNano();\n\n\n            fileStat.st_mtim.tv_sec.set(epochSecond);\n            fileStat.st_mtim.tv_nsec.set(nanoSeconds);\n\n            fileStat.st_atim.tv_nsec.set(epochSecond);\n            fileStat.st_atim.tv_nsec.set(nanoSeconds);\n            return 0;\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n            return -ErrorCodes.ENOENT();\n        }\n    }\n\n    @Override\n    public int getattr(String s, FileStat fileStat) {\n        ensureNotClosed();\n        int aDefault = -ErrorCodes.ENOENT();\n        return applyIfPresent(s, (peergosStat) -> annotateAttributes(s,\n                peergosStat, fileStat), aDefault);\n    }\n\n    @Override\n    public int readlink(String s, Pointer pointer, @size_t long l) {\n        throw new IllegalStateException(\"Unimplemented\");\n    }\n\n    @Override\n    public int mknod(String s, @mode_t long l, @dev_t long l1) {\n        throw new IllegalStateException(\"Unimplemented\");\n    }\n\n    private Optional<FileWrapper> mkdir(String name, FileWrapper node)  {\n        boolean isSystemFolder = false;\n        try {\n            return Optional.of(node.mkdir(name, context.network, isSystemFolder, context.getMirrorBat().join().map(BatWithId::id), context.crypto).get());\n        } catch (Exception ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Optional.empty();\n        }\n    }\n    @Override\n    public int mkdir(String s, @mode_t long l) {\n        ensureNotClosed();\n        Optional<PeergosStat> current = getByPath(s);\n        if (current.isPresent())\n            return -ErrorCodes.ENOENT();\n        Path path = PathUtil.get(s);\n        String parentPath = path.getParent().toString();\n\n        Optional<PeergosStat> parentOpt = getByPath(parentPath);\n\n        String name = path.getFileName().toString();\n\n        if (! parentOpt.isPresent())\n            return -ErrorCodes.ENOENT();\n\n        PeergosStat parent = parentOpt.get();\n        return mkdir(name, parent.treeNode).isPresent() ? 0 : -ErrorCodes.ENOENT();\n    }\n\n    @Override\n    public int unlink(String s) {\n        ensureNotClosed();\n        try {\n            Path requested = PathUtil.get(s);\n            Optional<FileWrapper> file = context.getByPath(s).get();\n            if (!file.isPresent())\n                return -ErrorCodes.ENOENT();\n\n            Optional<FileWrapper> parent = context.getByPath(requested.getParent()).get();;\n            if (!parent.isPresent())\n                return -ErrorCodes.ENOENT();\n\n            FileWrapper updatedParent = file.get().remove(parent.get(), requested, context).get();\n            return 0;\n        } catch (Exception ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return -ErrorCodes.ENOENT();\n        }\n    }\n\n    @Override\n    public int rmdir(String s) {\n        ensureNotClosed();\n        Path dir = PathUtil.get(s);\n        return applyIfPresent(s, (stat) -> applyIfPresent(dir.getParent().toString(), parentStat -> rmdir(stat, dir, parentStat)));\n    }\n\n    @Override\n    public int symlink(String s, String s1) {\n        return unimp();\n    }\n\n    private int rename(PeergosStat source, PeergosStat sourceParent, String sourcePath, String targetPath) {\n        ensureNotClosed();\n        try {\n            Path requested = PathUtil.get(targetPath);\n            String targetFilename = requested.getFileName().toString();\n            Optional<FileWrapper> newParent = context.getByPath(requested.getParent().toString()).get();\n            if (!newParent.isPresent())\n                return -ErrorCodes.ENOENT();\n\n            FileWrapper parent = sourceParent.treeNode;\n            FileWrapper updatedParent = source.treeNode.rename(targetFilename, parent, PathUtil.get(sourcePath), context).get();\n            // TODO clean up on error conditions\n            if (! parent.equals(newParent.get())) {\n                Path renamedInPlacePath = PathUtil.get(sourcePath).getParent().resolve(requested.getFileName().toString());\n                Optional<FileWrapper> renamedOriginal = context.getByPath(renamedInPlacePath).get();\n                if (! renamedOriginal.isPresent())\n                    return -ErrorCodes.ENOENT();\n                renamedOriginal.get().copyTo(newParent.get(), context).get();\n                FileWrapper updatedParent2 = renamedOriginal.get().getUpdated(context.network).join()\n                        .remove(parent.getUpdated(context.network).join(), renamedInPlacePath, context).get();\n            }\n            return 0;\n        } catch (Exception ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return -ErrorCodes.ENOENT();\n        }\n    }\n    @Override\n    public int rename(String s, String s1) {\n        ensureNotClosed();\n        Path source = PathUtil.get(s);\n        return applyIfPresent(s, (stat) -> applyIfPresent(source.getParent().toString(), parentStat -> rename(stat, parentStat, s, s1)));\n    }\n\n    @Override\n    public int link(String s, String s1) {\n        return unimp();\n    }\n\n    @Override\n    public int chmod(String s, @mode_t long l) {\n        return unimp();\n    }\n\n    @Override\n    public int chown(String s, @uid_t long l, @gid_t long l1) {\n        return unimp();\n    }\n\n    @Override\n    public int truncate(String s, @off_t long l) {\n        ensureNotClosed();\n        //TODO\n        return 0;\n    }\n\n    @Override\n    public int open(String s, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        debug(\"OPEN %s\", s);\n        return 0;\n    }\n\n    @Override\n    public int read(String s, Pointer pointer, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        debug(\"READ_OWN_FILE %s, size %d  offset %d \", s, size, offset);\n        return applyIfPresent(s, (stat) -> read(stat, pointer, size, offset));\n    }\n\n    @Override\n    public int write(String s, Pointer pointer, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        debug(\"WRITE_OWN_FILE %s, size %d  offset %d \", s, size, offset);\n        Path path = PathUtil.get(s);\n        String parentPath = path.getParent().toString();\n        String name = path.getFileName().toString();\n        return applyIfPresent(parentPath, (parent) -> write(parent, name, pointer, size, offset), -ErrorCodes.ENOENT());\n    }\n\n    @Override\n    public int statfs(String s, Statvfs statvfs) {\n        ensureNotClosed();\n        long driveSize = context.getQuota().join();\n        long blocksize = 128*1024;\n        long used = context.getSpaceUsage(false).join();\n        long free = driveSize - used;\n        statvfs.f_bsize.set(blocksize);\n        statvfs.f_frsize.set(blocksize);\n        statvfs.f_blocks.set(driveSize / blocksize);\n        statvfs.f_bfree.set(free / blocksize);\n        statvfs.f_bavail.set(free / blocksize);\n        statvfs.f_files.set(1024*1024);\n        statvfs.f_ffree.set(1024*1024);\n        statvfs.f_fsid.set('p' + 'e' * 256 + 'e' * 256*256 + 'r' * 256*256*256);\n        statvfs.f_namemax.set(FileProperties.MAX_FILE_NAME_SIZE);\n        statvfs.f_flag.set(4096);\n        return 0;\n    }\n\n    @Override\n    public int flush(String s, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        return 0;\n    }\n\n//    @Override\n//    public int release(String s, FuseFileInfo fuseFileInfo) {\n//        return 0;\n//    }\n\n//    @Override\n//    public int fsync(String s, int i, FuseFileInfo fuseFileInfo) {\n//        return unimp();\n//    }\n\n//    @Override\n//    public int setxattr(String s, String s1, Pointer pointer, @size_t long l, int i) {\n//        return unimp();\n//    }\n\n//    @Override\n//    public int getxattr(String s, String s1, Pointer pointer, @size_t long l) {\n//        return 0;\n//    }\n\n//    @Override\n//    public int listxattr(String s, Pointer pointer, @size_t long l) {\n//        return unimp();\n//    }\n\n    @Override\n    public int removexattr(String s, String s1) {\n        return unimp();\n    }\n\n    @Override\n    public int opendir(String s, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        return 0;\n    }\n\n    @Override\n    public int readdir(String s, Pointer pointer, FuseFillDir fuseFillDir, @off_t long l, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        return applyIfPresent(s, (stat) ->readdir(stat, fuseFillDir, pointer));\n    }\n\n    @Override\n    public int releasedir(String s, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        return 0;\n    }\n\n    @Override\n    public int fsyncdir(String s, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        return 0;\n    }\n\n    @Override\n    public Pointer init(Pointer pointer) {\n        ensureNotClosed();\n        return pointer;\n    }\n\n    @Override\n    public void destroy(Pointer pointer) {\n        ensureNotClosed();\n    }\n\n    @Override\n    public int access(String s, int mask) {\n        ensureNotClosed();\n        debug(\"ACCESS %s, mask %d\", s, mask);\n        return 0;\n    }\n\n    @Override\n    public int create(String s, @mode_t long l, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        Path path = PathUtil.get(s);\n        String parentPath = path.getParent().toString();\n        String name = path.getFileName().toString();\n        byte[] emptyData = new byte[0];\n\n        return applyIfPresent(parentPath,\n                (stat) -> write(stat,  name, emptyData, 0, 0));\n    }\n\n    @Override\n    public int ftruncate(String s, @off_t long l, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        Path path = PathUtil.get(s);\n        String parentPath = path.getParent().toString();\n        return applyIfBothPresent(parentPath, s, (parent, file) -> truncate(parent, file, l));\n    }\n\n    @Override\n    public int fgetattr(String s, FileStat fileStat, FuseFileInfo fuseFileInfo) {\n        ensureNotClosed();\n        return getattr(s, fileStat);\n    }\n\n    @Override\n    public int lock(String s, FuseFileInfo fuseFileInfo, int i, Flock flock) {\n        LOG.info(\"LOCK: \"+s);\n        ensureNotClosed();\n        return 0;\n    }\n\n//    @Override\n    public int utimens(String s, Timespec[] timespecs) {\n        ensureNotClosed();\n        int aDefault = -ErrorCodes.ENOENT();\n\n        Optional<PeergosStat> parentOpt = getParentByPath(s);\n        if (! parentOpt.isPresent())\n            return aDefault;\n\n        return applyIfPresent(s, (stat) -> {\n\n            Timespec access = timespecs[0], modified = timespecs[1];\n            long epochSeconds = modified.tv_sec.longValue();\n            long nanos = modified.tv_nsec.longValue();\n            Instant instant = Instant.ofEpochSecond(epochSeconds).plusNanos(nanos);\n            LocalDateTime lastModified = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());\n\n            FileProperties updated = stat.properties.withModified(lastModified);\n\n            /*\n            debug(\"utimens %s, with %s, %d, %s, updated %s\", s,\n                    lastModified.toString(),\n                    epochSeconds,\n                    modified.toString(),\n                    updated.toString());\n                    */\n\n            try {\n                boolean isUpdated = stat.treeNode.setProperties(updated, context.crypto.hasher, context.network, Optional.of(parentOpt.get().treeNode)).get();\n                return isUpdated ? 0 : -ErrorCodes.ENOENT();\n            } catch (Exception ex) {\n                LOG.log(Level.WARNING, ex.getMessage(), ex);\n                return -ErrorCodes.ENOENT();\n            }\n        }, aDefault);\n\n    }\n\n    @Override\n    public int bmap(String s, @size_t long l, long l1) {\n        return unimp();\n    }\n\n    @Override\n    public int ioctl(String s, int i, Pointer pointer, FuseFileInfo fuseFileInfo, @u_int32_t long l, Pointer pointer1) {\n        return unimp();\n    }\n\n    @Override\n    public int poll(String s, FuseFileInfo fuseFileInfo, FusePollhandle fusePollhandle, Pointer pointer) {\n        return unimp();\n    }\n\n//    @Override\n//    public int write_buf(String s, FuseBufvec fuseBufvec, @off_t long l, FuseFileInfo fuseFileInfo) {\n//\n//        return write();\n//    }\n\n//    @Override\n//    public int read_buf(String s, Pointer pointer, @size_t long l, @off_t long l1, FuseFileInfo fuseFileInfo) {\n//        return 0;\n//    }\n\n    @Override\n    public int flock(String s, FuseFileInfo fuseFileInfo, int i) {\n        return unimp();\n    }\n\n    @Override\n    public int fallocate(String s, int i, @off_t long l, @off_t long l1, FuseFileInfo fuseFileInfo) {\n        return unimp();\n    }\n\n    private int unimp() {\n        IllegalStateException ex = new IllegalStateException(\"Unimlemented!\");\n        LOG.log(Level.WARNING, ex.getMessage(), ex);\n        throw ex;\n    }\n\n    protected Optional<PeergosStat> getByPath(String path) {\n        try {\n            Optional<FileWrapper> opt = context.getByPath(path).get();\n            ;\n            if (!opt.isPresent())\n                return Optional.empty();\n            FileWrapper treeNode = opt.get();\n            FileProperties fileProperties = treeNode.getFileProperties();\n\n            return Optional.of(new PeergosStat(treeNode, fileProperties));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private Optional<PeergosStat> getParentByPath(String  path) {\n        String parentPath = PathUtil.get(path).getParent().toString();\n        return getByPath(parentPath);\n    }\n\n    protected int applyIf(String path, boolean isPresent, Function<PeergosStat,  Integer> func, int _default) {\n        Optional<PeergosStat> byPath = getByPath(path);\n        if (byPath.isPresent() && isPresent)\n            return func.apply(byPath.get());\n        return _default;\n    }\n\n    protected int applyIfPresent(String path, Function<PeergosStat,  Integer> func) {\n        int aDefault = -ErrorCodes.ENOENT();\n        return applyIfPresent(path, func, aDefault);\n    }\n\n    protected int applyIfPresent(String path, Function<PeergosStat,  Integer> func, int _default) {\n        boolean isPresent = true;\n        return applyIf(path, isPresent, func, _default);\n    }\n\n    private int applyIfBothPresent(String parentPath, String filePath, BiFunction<PeergosStat, PeergosStat,  Integer> func) {\n        int aDefault = -ErrorCodes.ENOENT();\n        return applyIfPresent(parentPath, parentStat -> applyIfPresent(filePath, fileStat -> func.apply(parentStat, fileStat)), aDefault);\n    }\n\n    private int rmdir(PeergosStat stat, Path dir, PeergosStat parentStat) {\n        FileWrapper treeNode = stat.treeNode;\n        try {\n            FileWrapper updatedParent = treeNode.remove(parentStat.treeNode, dir, context).get();\n            return 0;\n        } catch (Exception ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return -ErrorCodes.ENOENT();\n        }\n    }\n\n    private int readdir(PeergosStat stat, FuseFillDir fuseFillDir, Pointer pointer) {\n        try {\n            Set<FileWrapper> children = stat.treeNode.getChildren(context.crypto.hasher, context.network).get();\n            children.stream()\n                    .map(e -> e.getFileProperties().name)\n                    .forEach(e -> fuseFillDir.apply(pointer, e, null, 0));\n            return 0;\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            return -ErrorCodes.ENOENT();\n        }\n    }\n\n    protected Optional<byte[]> read(PeergosStat stat, long requestedSize, long offset) {\n        long actualSize = stat.properties.size;\n\n        if (offset > actualSize) {\n            Optional.empty();\n        }\n\n        long size = Math.min(actualSize - offset, requestedSize);\n        byte[] data =  new byte[(int) size];\n\n        if (data.length == 0)\n            return Optional.of(data);\n\n        try (AsyncReader asyncReader = stat.treeNode.getInputStream(context.network, context.crypto, actualSize, (l) -> {}).get()){\n            AsyncReader seeked = asyncReader.seekJS((int) (offset >> 32), (int) offset).get();\n\n            // N.B. Fuse seems to assume that a file must be an integral number of disk sectors,\n            // so need to tolerate EOFs up end of last sector (4KiB)\n            if (offset + size > actualSize + 4096)\n                return Optional.empty();\n\n            int sizeToRead = offset + size >= actualSize ? (int) (actualSize - offset) : (int) size;\n            int read = seeked.readIntoArray(data, 0, sizeToRead).get();\n\n            return Optional.of(data);\n        } catch (Exception  ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Optional.empty();\n        }\n    }\n\n    public int read(PeergosStat stat, Pointer pointer, long requestedSize, long offset) {\n        Optional<byte[]> dataOpt = read(stat, requestedSize, offset);\n\n        if  (! dataOpt.isPresent())\n            return -ErrorCodes.ENOENT();\n\n        byte[] data = dataOpt.get();\n        for (int i = 0; i < data.length; i++) {\n            pointer.putByte(i, data[i]);\n        }\n        return data.length;\n    }\n\n    private byte[] getData(Pointer pointer, int size) {\n        if (Integer.MAX_VALUE < size) {\n            throw new IllegalStateException(\"Cannot write more than \" + Integer.MAX_VALUE + \" bytes\");\n        }\n\n        byte[] toWrite = new byte[size];\n        pointer.get(0, toWrite, 0, size);\n        return toWrite;\n    }\n\n    public int truncate(PeergosStat parent, PeergosStat file, long size) {\n\n        debug(\"TRUNCATE file %s, size %d\", file.properties.name, size);\n\n        try {\n            if (size > file.properties.size) {\n                long currentPos = file.properties.size;\n                byte[] fullChunk = new byte[Chunk.MAX_SIZE];\n                FileWrapper parentNode = parent.treeNode;\n                while (currentPos < size) {\n                    long nextBoundary = Math.min(size, currentPos + Chunk.MAX_SIZE - (currentPos % Chunk.MAX_SIZE));\n                    int sizeInChunk = (int) (nextBoundary - currentPos);\n                    byte[] data = sizeInChunk == Chunk.MAX_SIZE ? fullChunk : new byte[sizeInChunk];\n                    parentNode = parentNode.uploadFileSection(file.properties.name, AsyncReader.build(data),\n                            file.properties.isHidden, currentPos, currentPos + sizeInChunk, Optional.empty(),\n                            true, context.network, context.crypto, () -> false, x -> {},\n                            file.treeNode.getLocation().getMapKey(), file.properties.streamSecret, file.treeNode.getPointer().capability.bat,\n                            context.getMirrorBat().join().map(BatWithId::id)).get();\n                    currentPos += sizeInChunk;\n                }\n            } else\n                file.treeNode.truncate(size, context.network, context.crypto).get();\n            return 0;\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n            return -ErrorCodes.ENOENT();\n        }\n    }\n\n    public int write(PeergosStat parent, String name, byte[] toWrite, long size, long offset) {\n\n        try {\n            long updatedLength = size + offset;\n            if (Integer.MAX_VALUE < updatedLength) {\n                throw new IllegalStateException(\"Cannot write more than \" + Integer.MAX_VALUE + \" bytes\");\n            }\n\n            FileWrapper b = parent.treeNode.uploadFileSection(name, new AsyncReader.ArrayBacked(toWrite), false, offset,\n                    offset + size, Optional.empty(), true, context.network,\n                    context.crypto, () -> false, l -> {},\n                    context.crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(context.crypto.random)),\n                    context.getMirrorBat().join().map(BatWithId::id)).get();\n            return (int) size;\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n            return -ErrorCodes.ENOENT();\n        }\n    }\n\n    public int write(PeergosStat parent, String name, Pointer pointer, long size, long offset) {\n        byte[] data = getData(pointer, (int) size);\n        return write(parent, name, data, size, offset);\n    }\n\n    /**\n     * JNR doesn't play nicely with debugger at all => debugging like it's 1990\n     */\n    private void debug(String template, Object... obj) {\n        String msg = String.format(template, obj);\n        LOG.info(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/login/AccountWithStorage.java",
    "content": "package peergos.server.login;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class AccountWithStorage implements Account {\n\n    private final ContentAddressedStorage storage;\n    private final MutablePointers pointers;\n    private final JdbcAccount target;\n\n    public AccountWithStorage(ContentAddressedStorage storage, MutablePointers pointers, JdbcAccount target) {\n        this.storage = storage;\n        this.pointers = pointers;\n        this.target = target;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        if (login.identityUpdate.isPresent()) {\n            Pair<OpLog.BlockWrite, OpLog.PointerWrite> pair = login.identityUpdate.get();\n            OpLog.BlockWrite block = pair.left;\n            OpLog.PointerWrite pointer = pair.right;\n            TransactionId tid = storage.startTransaction(block.writer).join();\n            storage.put(block.writer, block.writer, block.signature, block.block, tid).join();\n            pointers.setPointer(pointer.writer, pointer.writer, pointer.writerSignedChampRootCas).join();\n            storage.closeTransaction(pointer.writer, tid).join();\n        }\n        return target.setLoginData(login);\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse> mfa,\n                                                                                          boolean cacheMfaLoginData,\n                                                                                          boolean forceProxy,\n                                                                                          boolean forceNoCache) {\n        return target.getEntryData(username, authorisedReader, mfa);\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        return target.getSecondAuthMethods(username);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        return target.enableTotpFactor(username, credentialId, code);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        return Futures.of(target.registerSecurityKeyStart(username));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        target.registerSecurityKeyComplete(username, keyName, resp);\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        return target.deleteMfa(username, credentialId);\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        return target.addTotpFactor(username);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/login/JdbcAccount.java",
    "content": "package peergos.server.login;\n\nimport com.eatthepath.otp.*;\nimport com.webauthn4j.*;\nimport com.webauthn4j.data.client.*;\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.login.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport javax.crypto.spec.*;\nimport java.security.*;\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class JdbcAccount implements LoginCache {\n    private static final Logger LOG = Logging.LOG();\n\n    private static final String CREATE = \"INSERT INTO login (username, entry, reader) VALUES(?, ?, ?)\";\n    private static final String REMOVE = \"DELETE FROM login WHERE username=?;\";\n    private static final String UPDATE = \"UPDATE login SET entry=?, reader=? WHERE username = ?\";\n    private static final String GET_LOGIN = \"SELECT * FROM login WHERE username = ? AND reader = ? LIMIT 1;\";\n    private static final String GET = \"SELECT * FROM login WHERE username = ? LIMIT 1;\";\n    private static final String CREATE_MFA = \"INSERT INTO mfa (username, name, credid, type, enabled, created, value) VALUES(?, ?, ?, ?, ?, ?, ?);\";\n    private static final String UPDATE_MFA = \"UPDATE mfa SET value=? WHERE username = ? AND credid = ?;\";\n    private static final String GET_TYPE = \"SELECT type FROM mfa WHERE username = ? AND credid = ?;\";\n    private static final String GET_AUTH = \"SELECT value FROM mfa WHERE username = ? AND credid = ?;\";\n    private static final String CREATE_CHALLENGE = \"INSERT INTO mfa_challenge (challenge, username) VALUES(?, ?);\";\n    private static final String UPDATE_CHALLENGE = \"UPDATE mfa_challenge SET challenge=? WHERE username=?;\";\n    private static final String GET_CHALLENGE = \"SELECT challenge FROM mfa_challenge WHERE username = ?;\";\n    private static final String ENABLE_AUTH = \"UPDATE mfa SET enabled=? WHERE username = ? AND credid = ?;\";\n    private static final String DELETE_AUTH = \"DELETE FROM mfa WHERE username = ? AND credid = ?\";\n    private static final String GET_AUTH_METHODS = \"SELECT name, credid, created, type, enabled FROM mfa WHERE username = ?;\";\n\n    public static final int MAX_MFA = 10;\n\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n    private final SecureRandom rnd = new SecureRandom();\n    private final WebAuthnManager webauthn = WebAuthnManager.createNonStrictWebAuthnManager();\n    private final Origin origin;\n    private final String rpId;\n\n    public JdbcAccount(Supplier<Connection> conn, SqlSupplier commands, Origin origin, String rpId) {\n        this.conn = conn;\n        this.origin = origin;\n        this.rpId = rpId;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createAccountTableCommand(), conn);\n            commands.createTable(commands.createMfaTableCommand(), conn);\n            commands.createTable(commands.createMfaChallengeTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private boolean hasEntry(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement present = conn.prepareStatement(GET)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            present.setString(1, username);\n            ResultSet rs = present.executeQuery();\n            if (rs.next()) {\n                return true;\n            }\n            return false;\n        } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                throw new RuntimeException(sqe);\n            }\n    }\n\n    public CompletableFuture<Boolean> setLoginData(LoginData login) {\n        if (hasEntry(login.username)) {\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(UPDATE)) {\n                conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n\n                insert.setString(1, new String(Base64.getEncoder().encode(login.entryPoints.serialize())));\n                insert.setString(2, new String(Base64.getEncoder().encode(login.authorisedReader.serialize())));\n                insert.setString(3, login.username);\n                int changed = insert.executeUpdate();\n                return CompletableFuture.completedFuture(changed > 0);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return CompletableFuture.completedFuture(false);\n            }\n        } else {\n            try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(CREATE)) {\n                stmt.setString(1, login.username);\n                stmt.setString(2, new String(Base64.getEncoder().encode(login.entryPoints.serialize())));\n                stmt.setString(3, new String(Base64.getEncoder().encode(login.authorisedReader.serialize())));\n                stmt.executeUpdate();\n                return CompletableFuture.completedFuture(true);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return CompletableFuture.completedFuture(false);\n            }\n        }\n    }\n\n    @Override\n    public CompletableFuture<Boolean> removeLoginData(String username) {\n        try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(REMOVE)) {\n                stmt.setString(1, username);\n                stmt.executeUpdate();\n                return CompletableFuture.completedFuture(true);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return CompletableFuture.completedFuture(false);\n            }\n    }\n\n    private MultiFactorAuthMethod.Type getType(String username, byte[] credentialId) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_TYPE)) {\n            stmt.setString(1, username);\n            stmt.setBytes(2, credentialId);\n            ResultSet resultSet = stmt.executeQuery();\n            if (resultSet.next()) {\n                return MultiFactorAuthMethod.Type.byValue(resultSet.getInt(1));\n            }\n            throw new IllegalStateException(\"Unknown credential id for user \" + username);\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            throw new IllegalStateException(e);\n        }\n    }\n\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getEntryData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          Optional<MultiFactorAuthResponse> mfa) {\n        List<MultiFactorAuthMethod> mfas = getSecondAuthMethods(username).join();\n        List<MultiFactorAuthMethod> enabled = mfas.stream().filter(m -> m.enabled).collect(Collectors.toList());\n        if (enabled.isEmpty())\n            return getEntryData(username, authorisedReader).thenApply(Either::a);\n        if (mfa.isEmpty()) {\n            byte[] challenge = createChallenge(username);\n            return Futures.of(Either.b(new MultiFactorAuthRequest(enabled, challenge)));\n        }\n        MultiFactorAuthResponse mfaAuth = mfa.get();\n        byte[] credentialId = mfaAuth.credentialId;\n        if (mfaAuth.response.isB()) {\n            MultiFactorAuthMethod.Type type = getType(username, credentialId);\n            if (type != MultiFactorAuthMethod.Type.WEBAUTHN)\n                throw new IllegalStateException(\"Not a webauthn credential!\");\n            Webauthn.Verifier verifier = Webauthn.Verifier.fromCbor(CborObject.fromByteArray(getMfa(username, credentialId)));\n            byte[] challenge = getChallenge(username);\n            byte[] authenticatorData = mfaAuth.response.b().authenticatorData;\n            byte[] clientDataJson = mfaAuth.response.b().clientDataJson;\n            byte[] signature = mfaAuth.response.b().signature;\n            long newSignCount = Webauthn.validateLogin(webauthn, origin, rpId, challenge, verifier, credentialId, username.getBytes(),\n                    authenticatorData, clientDataJson, signature);\n            // Update counter\n            verifier.setCounter(newSignCount);\n            updateMFA(username, credentialId, verifier.serialize());\n        } else {\n            validateTotpCode(username, credentialId, mfaAuth.response.a());\n        }\n        return getEntryData(username, authorisedReader).thenApply(Either::a);\n    }\n\n    public CompletableFuture<UserStaticData> getEntryData(String username, PublicSigningKey authorisedReader) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_LOGIN)) {\n            stmt.setString(1, username);\n            stmt.setString(2, new String(Base64.getEncoder().encode(authorisedReader.serialize())));\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return CompletableFuture.completedFuture(UserStaticData.fromCbor(CborObject.fromByteArray(Base64.getDecoder().decode(rs.getString(\"entry\")))));\n            }\n\n            return Futures.errored(new IllegalStateException(\"Incorrect username or password\"));\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            return Futures.errored(sqe);\n        }\n    }\n\n    public Optional<LoginData> getLoginData(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET)) {\n            stmt.setString(1, username);\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                UserStaticData entry = UserStaticData.fromCbor(CborObject.fromByteArray(Base64.getDecoder().decode(rs.getString(\"entry\"))));\n                PublicSigningKey authorisedReader = PublicSigningKey.fromCbor(CborObject.fromByteArray(Base64.getDecoder().decode(rs.getString(\"reader\"))));\n                return Optional.of(new LoginData(username, entry, authorisedReader, Optional.empty()));\n            }\n\n            return Optional.empty();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_AUTH_METHODS)) {\n            stmt.setString(1, username);\n            ResultSet rs = stmt.executeQuery();\n            List<MultiFactorAuthMethod> res = new ArrayList<>();\n            while (rs.next()) {\n                boolean enabled = rs.getBoolean(\"enabled\");\n                MultiFactorAuthMethod.Type type = MultiFactorAuthMethod.Type.byValue(rs.getInt(\"type\"));\n                if (type == MultiFactorAuthMethod.Type.TOTP && !enabled)\n                    continue; // Don't return disabled totp\n                res.add(new MultiFactorAuthMethod(\n                        rs.getString(\"name\"),\n                        rs.getBytes(\"credid\"),\n                        LocalDate.ofEpochDay(rs.getInt(\"created\")),\n                        type,\n                        enabled));\n            }\n\n            return Futures.of(res);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public void updateMFA(String username, byte[] credentialId, byte[] value) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(UPDATE_MFA)) {\n            stmt.setBytes(1, value);\n            stmt.setString(2, username);\n            stmt.setBytes(3, credentialId);\n            stmt.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public CompletableFuture<TotpKey> addTotpFactor(String username) {\n        byte[] rawKey = new byte[32];\n        rnd.nextBytes(rawKey);\n        byte[] credId = new byte[32];\n        rnd.nextBytes(credId);\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(CREATE_MFA)) {\n            stmt.setString(1, username);\n            stmt.setString(2, \"\"); // TOTP don't need names as there is only 1 active at a time\n            stmt.setBytes(3, credId);\n            stmt.setInt(4, MultiFactorAuthMethod.Type.TOTP.value);\n            stmt.setBoolean(5, false);\n            stmt.setLong(6, LocalDate.now().toEpochDay());\n            stmt.setBytes(7, rawKey);\n            stmt.executeUpdate();\n            return Futures.of(new TotpKey(credId, rawKey));\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    private byte[] getMfa(String username, byte[] credentialId) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_AUTH)) {\n            stmt.setString(1, username);\n            stmt.setBytes(2, credentialId);\n            ResultSet resultSet = stmt.executeQuery();\n            if (resultSet.next()) {\n                return resultSet.getBytes(\"value\");\n            }\n            throw new IllegalStateException(\"Unknown credential id for user \" + username);\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            throw new IllegalStateException(e);\n        }\n    }\n\n    private void validateTotpCode(String username, byte[] credentialId, String code) {\n        byte[] rawKey = getMfa(username, credentialId);\n\n        TimeBasedOneTimePasswordGenerator totp = new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30L), 6, TotpKey.ALGORITHM);\n        Key key = new SecretKeySpec(rawKey, TotpKey.ALGORITHM);\n        try {\n            Instant now = Instant.now();\n            String serverCode = totp.generateOneTimePasswordString(key, now);\n            if (serverCode.equals(code))\n                return;\n            String previousCode = totp.generateOneTimePasswordString(key, now.minusSeconds(30));\n            if (previousCode.equals(code))\n                return;\n            throw new IllegalStateException(\"Invalid TOTP code for credId \" + ArrayOps.bytesToHex(credentialId));\n        } catch (InvalidKeyException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code) {\n        List<MultiFactorAuthMethod> olderTotp = getSecondAuthMethods(username).join()\n                .stream()\n                .filter(m -> !Arrays.equals(m.credentialId, credentialId) && m.type == MultiFactorAuthMethod.Type.TOTP)\n                .collect(Collectors.toList());\n        validateTotpCode(username, credentialId, code);\n        try (Connection conn = getConnection();\n             PreparedStatement update = conn.prepareStatement(ENABLE_AUTH)) {\n            update.setBoolean(1, true);\n            update.setString(2, username);\n            update.setBytes(3, credentialId);\n            update.executeUpdate();\n            // now delete any existing old ones\n            for (MultiFactorAuthMethod mfa : olderTotp) {\n                deleteMfa(username, mfa.credentialId).join();\n            }\n            return Futures.of(true);\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            throw new IllegalStateException(e);\n        }\n    }\n\n    public byte[] registerSecurityKeyStart(String username) {\n        List<MultiFactorAuthMethod> existing = getSecondAuthMethods(username).join();\n        if (existing.size() > MAX_MFA)\n            throw new IllegalStateException(\"Too many multi factor auth methods. Please delete some.\");\n        return createChallenge(username);\n    }\n\n    private byte[] createChallenge(String username) {\n        byte[] challenge = new byte[32];\n        rnd.nextBytes(challenge);\n        boolean hasChallenge = hasChallenge(username);\n        try (Connection conn = getConnection();\n             PreparedStatement update = hasChallenge ? conn.prepareStatement(UPDATE_CHALLENGE) : conn.prepareStatement(CREATE_CHALLENGE)) {\n            update.setBytes(1, challenge);\n            update.setString(2, username);\n            update.executeUpdate();\n            return challenge;\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            throw new IllegalStateException(e);\n        }\n    }\n\n    private boolean hasChallenge(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_CHALLENGE)) {\n            stmt.setString(1, username);\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return true;\n            }\n\n            return false;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private byte[] getChallenge(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_CHALLENGE)) {\n            stmt.setString(1, username);\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return rs.getBytes(\"challenge\");\n            }\n\n            throw new IllegalStateException(\"No challenge for \" + username);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public void registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp) {\n        if (keyName.length() > 32)\n            throw new IllegalStateException(\"Max second factor name length is 32 characters\");\n        byte[] challenge = getChallenge(username);\n        if (resp.response.isA())\n            throw new IllegalStateException(\"Not MFA response!\");\n        byte[] attestationObject = resp.response.b().authenticatorData;\n        byte[] clientDataJson = resp.response.b().clientDataJson;\n        Webauthn.Verifier authenticator = Webauthn.validateRegistration(webauthn, origin, rpId, challenge,\n                attestationObject, clientDataJson);\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(CREATE_MFA)) {\n            stmt.setString(1, username);\n            stmt.setString(2, keyName);\n            stmt.setBytes(3, resp.credentialId);\n            stmt.setInt(4, MultiFactorAuthMethod.Type.WEBAUTHN.value);\n            stmt.setBoolean(5, true);\n            stmt.setLong(6, LocalDate.now().toEpochDay());\n            stmt.setBytes(7, authenticator.serialize());\n            stmt.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public CompletableFuture<Boolean> deleteMfa(String username, byte[] credentialId) {\n        try (Connection conn = getConnection();\n             PreparedStatement update = conn.prepareStatement(DELETE_AUTH)) {\n            update.setString(1, username);\n            update.setBytes(2, credentialId);\n            update.executeUpdate();\n\n            return Futures.of(true);\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            throw new IllegalStateException(e);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n\n        isClosed = true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/login/LocalOnlyAccount.java",
    "content": "package peergos.server.login;\n\nimport peergos.server.storage.admin.QuotaAdmin;\nimport peergos.server.util.TimeLimited;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.login.mfa.MultiFactorAuthMethod;\nimport peergos.shared.login.mfa.MultiFactorAuthRequest;\nimport peergos.shared.login.mfa.MultiFactorAuthResponse;\nimport peergos.shared.login.mfa.TotpKey;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.user.Account;\nimport peergos.shared.user.LoginData;\nimport peergos.shared.user.UserStaticData;\nimport peergos.shared.util.ArrayOps;\nimport peergos.shared.util.Constants;\nimport peergos.shared.util.Either;\n\nimport java.time.LocalDateTime;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\n\npublic class LocalOnlyAccount implements Account {\n\n    private final Account target;\n    private final QuotaAdmin quotas;\n    private final boolean allowExternalLogin;\n\n    public LocalOnlyAccount(Account target, QuotaAdmin quotas, boolean allowExternalLogin) {\n        this.target = target;\n        this.quotas = quotas;\n        this.allowExternalLogin = allowExternalLogin;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        return target.setLoginData(login, auth, forceLocal);\n    }\n\n    private boolean hasQuota(String username) {\n        try {\n            return quotas.getQuota(username) > 0 || quotas.hadQuota(username, LocalDateTime.now().minusMonths(1));\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse>  mfa,\n                                                                                          boolean cacheMfaLoginData,\n                                                                                          boolean forceProxy,\n                                                                                          boolean forceNoCache) {\n        if (! allowExternalLogin && ! hasQuota(username) && !forceProxy)\n            throw new IllegalStateException(\"Please login on your home server\");\n        return target.getLoginData(username, authorisedReader, auth, mfa, cacheMfaLoginData, forceProxy, forceNoCache).thenApply(res -> {\n            TimeLimited.isAllowedTime(auth, 24*3600, authorisedReader);\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        return target.getSecondAuthMethods(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        return target.addTotpFactor(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        return target.enableTotpFactor(username, credentialId, code, auth);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        return target.registerSecurityKeyStart(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        return target.registerSecurityKeyComplete(username, keyName, resp, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        return target.deleteSecondFactor(username, credentialId, auth);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/login/NonWriteThroughAccount.java",
    "content": "package peergos.server.login;\n\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class NonWriteThroughAccount implements Account {\n\n    private final Account source;\n    private final Map<String, LoginData> modifications;\n\n    public NonWriteThroughAccount(Account source) {\n        this.source = source;\n        this.modifications = new HashMap<>();\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        modifications.put(login.username, login);\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse>  mfa,\n                                                                                          boolean cacheMfaLoginData,\n                                                                                          boolean forceProxy,\n                                                                                          boolean forceNoCache) {\n        LoginData updated = modifications.get(username);\n        if (updated == null)\n            return source.getLoginData(username, authorisedReader, auth, mfa, false, forceProxy, forceNoCache);\n        return Futures.of(Either.a(updated.entryPoints));\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        return source.getSecondAuthMethods(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        throw new IllegalStateException(\"TODO\");\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        throw new IllegalStateException(\"TODO\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        throw new IllegalStateException(\"TODO\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        throw new IllegalStateException(\"TODO\");\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        throw new IllegalStateException(\"TODO\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/login/VerifyingAccount.java",
    "content": "package peergos.server.login;\n\nimport peergos.server.util.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class VerifyingAccount implements Account {\n\n    private final Account target;\n    private final CoreNode core;\n    private final ContentAddressedStorage storage;\n\n    public VerifyingAccount(Account target, CoreNode core, ContentAddressedStorage storage) {\n        this.target = target;\n        this.core = core;\n        this.storage = storage;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        PublicKeyHash identityHash = core.getPublicKeyHash(login.username).join().get();\n        PublicSigningKey identity = storage.getSigningKey(identityHash, identityHash).join().get();\n        identity.unsignMessage(ArrayOps.concat(auth, login.serialize()));\n        return target.setLoginData(login, auth, forceLocal);\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse>  mfa,\n                                                                                          boolean cacheMfaLoginData,\n                                                                                          boolean forceProxy,\n                                                                                          boolean forceNoCache) {\n        return target.getLoginData(username, authorisedReader, auth, mfa, cacheMfaLoginData, forceProxy, forceNoCache).thenApply(res -> {\n            TimeLimited.isAllowedTime(auth, 24*3600, authorisedReader);\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        PublicKeyHash identityHash = core.getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowed(Constants.LOGIN_URL + \"listMfa\", auth, 24*3600, storage, identityHash);\n        return target.getSecondAuthMethods(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        PublicKeyHash identityHash = core.getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowed(Constants.LOGIN_URL + \"addTotp\", auth, 24*3600, storage, identityHash);\n        return target.addTotpFactor(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        PublicKeyHash identityHash = core.getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowed(Constants.LOGIN_URL + \"enableTotp\", auth, 24*3600, storage, identityHash);\n        return target.enableTotpFactor(username, credentialId, code, auth);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        PublicKeyHash identityHash = core.getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowed(Constants.LOGIN_URL + \"registerWebauthnStart\", auth, 24*3600, storage, identityHash);\n        return target.registerSecurityKeyStart(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        PublicKeyHash identityHash = core.getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowed(Constants.LOGIN_URL + \"registerWebauthnComplete\", auth, 24*3600, storage, identityHash);\n        return target.registerSecurityKeyComplete(username, keyName, resp, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        PublicKeyHash identityHash = core.getPublicKeyHash(username).join().get();\n        TimeLimited.isAllowed(Constants.LOGIN_URL + \"deleteMfa\", auth, 24*3600, storage, identityHash);\n        return target.deleteSecondFactor(username, credentialId, auth);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/login/Webauthn.java",
    "content": "package peergos.server.login;\n\nimport com.webauthn4j.*;\nimport com.webauthn4j.authenticator.*;\nimport com.webauthn4j.converter.exception.*;\nimport com.webauthn4j.converter.util.CborConverter;\nimport com.webauthn4j.converter.util.ObjectConverter;\nimport com.webauthn4j.data.*;\nimport com.webauthn4j.data.attestation.authenticator.*;\nimport com.webauthn4j.data.attestation.statement.*;\nimport com.webauthn4j.data.client.*;\nimport com.webauthn4j.data.client.challenge.*;\nimport com.webauthn4j.server.*;\nimport com.webauthn4j.verifier.exception.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.util.ArrayOps;\n\nimport java.io.*;\nimport java.util.*;\n\npublic class Webauthn {\n\n    public static class Verifier implements Authenticator, Cborable {\n\n        private final AttestedCredentialData credData;\n        private final AttestationStatement statement;\n        private long signCount;\n\n        public Verifier(AttestedCredentialData credData, AttestationStatement statement, long signCount) {\n            if (!(statement instanceof NoneAttestationStatement))\n                throw new IllegalStateException(\"Attested keys not supported!\");\n            this.credData = credData;\n            this.statement = statement;\n            this.signCount = signCount;\n        }\n\n        @Override\n        public AttestedCredentialData getAttestedCredentialData() {\n            return credData;\n        }\n\n        @Override\n        public long getCounter() {\n            return signCount;\n        }\n\n        @Override\n        public void setCounter(long count) {\n            signCount = count;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            CborConverter cborConverter = new ObjectConverter().getCborConverter();\n            state.put(\"d\", new CborObject.CborByteArray(cborConverter.writeValueAsBytes(credData)));\n            state.put(\"s\", new CborObject.CborByteArray(cborConverter.writeValueAsBytes((statement))));\n            state.put(\"c\", new CborObject.CborLong(signCount));\n            return CborObject.CborMap.build(state);\n        }\n\n        public static Verifier fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor for Verifier\");\n            CborConverter cborConverter = new ObjectConverter().getCborConverter();\n            try {\n                AttestedCredentialData credData = cborConverter.readValue(((CborObject.CborMap) cbor).getByteArray(\"d\"), AttestedCredentialData.class);\n                byte[] s = ((CborObject.CborMap) cbor).getByteArray(\"s\");\n                CborObject cb = CborObject.fromByteArray(s);\n                if (! (cb instanceof CborObject.CborMap && ((CborObject.CborMap) cb).keySet().isEmpty()))\n                    throw new IllegalStateException(\"Unsupported Webauthn Attestation type\");\n                AttestationStatement statement = new NoneAttestationStatement();\n                long signCount = ((CborObject.CborMap) cbor).getLong(\"c\");\n                return new Verifier(credData, statement, signCount);\n            } catch (Exception e) {\n                return fromLegacyCbor(cbor);\n            }\n        }\n\n        private static byte[] NO_ATTESTATION = ArrayOps.hexToBytes(\"aced000573720042636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e4e6f6e654174746573746174696f6e53746174656d656e746b3b9efd2e6430530200007870\");\n        private static byte[] ED25519 = ArrayOps.hexToBytes(\"aced000573720044636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e417474657374656443726564656e7469616c4461746185df6d0f6d8aacb40200034c00066161677569647400364c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f4141475549443b4c0007636f73654b65797400374c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f434f53454b65793b5b000c63726564656e7469616c49647400025b42787073720034636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e414147554944fb3908248cfbd7d40200014c000576616c75657400104c6a6176612f7574696c2f555549443b78707372000e6a6176612e7574696c2e55554944bc9903f7986d852f0200024a000c6c65617374536967426974734a000b6d6f7374536967426974737870000000000000000000000000000000007372003a636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e4564445341434f53454b65794f34c5f4776431400200034c000563757276657400354c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f43757276653b5b00016471007e00035b00017871007e00037872003d636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e4162737472616374434f53454b657911b0302464c2e4c90200044c0009616c676f726974686d7400434c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f73746174656d656e742f434f5345416c676f726974686d4964656e7469666965723b5b000662617365495671007e00035b00056b6579496471007e00034c00066b65794f70737400104c6a6176612f7574696c2f4c6973743b787073720041636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e434f5345416c676f726974686d4964656e746966696572b5e5a801c4dc74180200014a000576616c75657870fffffffffffffff87070707e720033636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e437572766500000000000000001200007872000e6a6176612e6c616e672e456e756d000000000000000012000078707400074544323535313970757200025b42acf317f8060854e0020000787000000020\");\n        private static byte[] SECP_PREFIX = ArrayOps.hexToBytes(\"aced000573720044636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e417474657374656443726564656e7469616c4461746185df6d0f6d8aacb40200034c00066161677569647400364c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f4141475549443b4c0007636f73654b65797400374c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f434f53454b65793b5b000c63726564656e7469616c49647400025b42787073720034636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e414147554944fb3908248cfbd7d40200014c000576616c75657400104c6a6176612f7574696c2f555549443b78707372000e6a6176612e7574696c2e55554944bc9903f7986d852f0200024a000c6c65617374536967426974734a000b6d6f7374536967426974737870\");\n\n        public static Verifier fromLegacyCbor(Cborable cbor) {\n            // This is a reversing of the default Java object serialisation in an older version of the webauthn library\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor for Verifier\");\n            byte[] rawD = ((CborObject.CborMap) cbor).getByteArray(\"d\");\n            if (! ArrayOps.equalArrays(rawD, 0, 1017, ED25519, 0, 1017)) {\n                if (! ArrayOps.equalArrays(rawD, 0, 399, SECP_PREFIX, 0, 399))\n                    throw new IllegalStateException(\"Unknown passkey type!\");\n                byte[] rawAguid = Arrays.copyOfRange(rawD, 399, 399 + 16);\n                byte[] x = Arrays.copyOfRange(rawD, 1026, 1026 + 32);\n                byte[] y = Arrays.copyOfRange(rawD, 1068, 1068 + 32);\n                byte[] credentialId = Arrays.copyOfRange(rawD, 1110, 1110 + 16 + (rawD.length - 1126));\n                byte[] aguid = ArrayOps.concat(Arrays.copyOfRange(rawAguid, 8, 16), Arrays.copyOfRange(rawAguid, 0, 8));\n                AttestedCredentialData credData = new AttestedCredentialData(\n                        new AAGUID(aguid),\n                        credentialId,\n                        new EC2COSEKey(null, COSEAlgorithmIdentifier.ES256, null, Curve.SECP256R1, x, y, null));\n                AttestationStatement statement = new NoneAttestationStatement();\n                long signCount = ((CborObject.CborMap) cbor).getLong(\"c\");\n                return new Verifier(credData, statement, signCount);\n            }\n            AttestedCredentialData credData = new AttestedCredentialData(AAGUID.ZERO,\n                    Arrays.copyOfRange(rawD, 1059, 1059+128 + (rawD.length - 1187)),\n                    new EdDSACOSEKey(null, COSEAlgorithmIdentifier.EdDSA, null, Curve.ED25519, Arrays.copyOfRange(rawD, 1017, 1017 + 32), null));\n            byte[] rawS = ((CborObject.CborMap) cbor).getByteArray(\"s\");\n            if (! Arrays.equals(rawS, NO_ATTESTATION))\n                throw new IllegalStateException(\"Unknown webauthn attestation type!\");\n            AttestationStatement statement = new NoneAttestationStatement();\n            long signCount = ((CborObject.CborMap) cbor).getLong(\"c\");\n            return new Verifier(credData, statement, signCount);\n        }\n    }\n\n    public static Verifier validateRegistration(WebAuthnManager webAuthnManager,\n                                                Origin origin,\n                                                String rpId,\n                                                byte[] rawChallenge,\n                                                byte[] attestationObject,\n                                                byte[] clientDataJSON) {\n        Challenge challenge = () -> rawChallenge;\n        byte[] tokenBindingId = null;\n        ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId);\n\n        // expectations\n        boolean userVerificationRequired = false;\n        boolean userPresenceRequired = true;\n\n        String clientExtensionsJSON = null;\n        Set<String> transports = null;\n        RegistrationRequest registrationRequest = new RegistrationRequest(attestationObject, clientDataJSON, clientExtensionsJSON, transports);\n        RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, userVerificationRequired, userPresenceRequired);\n        RegistrationData registrationData;\n        try {\n            registrationData = webAuthnManager.parse(registrationRequest);\n        } catch (DataConversionException e) {\n            // If you would like to handle WebAuthn data structure parse error, please catch DataConversionException\n            throw e;\n        }\n        try {\n            webAuthnManager.validate(registrationData, registrationParameters);\n        } catch (VerificationException e) {\n            // If you would like to handle WebAuthn data validation error, please catch ValidationException\n            throw e;\n        }\n\n        return new Verifier(\n                registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData(),\n                registrationData.getAttestationObject().getAttestationStatement(),\n                registrationData.getAttestationObject().getAuthenticatorData().getSignCount()\n        );\n    }\n\n    public static long validateLogin(WebAuthnManager webAuthnManager,\n                                     Origin origin,\n                                     String rpId,\n                                     byte[] rawChallenge,\n                                     Authenticator user,\n                                     byte[] credentialId,\n                                     byte[] userHandle,\n                                     byte[] authenticatorData,\n                                     byte[] clientDataJSON,\n                                     byte[] signature) {\n        String clientExtensionJSON = null /* set clientExtensionJSON */;\n\n        Challenge challenge = () -> rawChallenge;\n        byte[] tokenBindingId = null;\n        ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId);\n\n        // expectations\n        List<byte[]> allowCredentials = null;\n        boolean userVerificationRequired = false;\n        boolean userPresenceRequired = true;\n\n        AuthenticationRequest authenticationRequest =\n                new AuthenticationRequest(\n                        credentialId,\n                        userHandle,\n                        authenticatorData,\n                        clientDataJSON,\n                        clientExtensionJSON,\n                        signature\n                );\n        AuthenticationParameters authenticationParameters =\n                new AuthenticationParameters(\n                        serverProperty,\n                        user,\n                        allowCredentials,\n                        userVerificationRequired,\n                        userPresenceRequired\n                );\n\n        AuthenticationData authenticationData;\n        try {\n            authenticationData = webAuthnManager.parse(authenticationRequest);\n        } catch (DataConversionException e) {\n            // If you would like to handle WebAuthn data structure parse error, please catch DataConversionException\n            throw e;\n        }\n        try {\n            webAuthnManager.validate(authenticationData, authenticationParameters);\n        } catch (VerificationException e) {\n            // If you would like to handle WebAuthn data validation error, please catch ValidationException\n            throw e;\n        }\n        return authenticationData.getAuthenticatorData().getSignCount();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/messages/ServerMessageStore.java",
    "content": "package peergos.server.messages;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class ServerMessageStore implements ServerMessager {\n    private static final Logger LOG = Logging.LOG();\n\n    private static final String SELECT = \"SELECT id, type, sent, body, priorid, dismissed FROM messages WHERE username = ?;\";\n    private static final String SELECT_AFTER = \"SELECT username, id, type, sent, body, priorid, dismissed FROM messages WHERE sent >= ?;\";\n    private static final String ADD = \"INSERT INTO messages (username, sent, type, body, priorid) VALUES(?, ?, ?, ?, ?);\";\n    private static final String DISMISS = \"UPDATE messages SET dismissed = true WHERE id = ? AND username = ?;\";\n    private static final String COUNT = \"SELECT COUNT (username) FROM messages where username = ? AND sent > ?;\";\n\n    private static final int HOUR_MILLIS = 3_600_000;\n\n    private final Supplier<Connection> conn;\n    private final SqlSupplier commands;\n    private final CoreNode pki;\n    private final ContentAddressedStorage ipfs;\n    private volatile boolean isClosed;\n\n    public ServerMessageStore(Supplier<Connection> conn, SqlSupplier commands, CoreNode pki, ContentAddressedStorage ipfs) {\n        this.conn = conn;\n        this.commands = commands;\n        this.pki = pki;\n        this.ipfs = ipfs;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createServerMessageTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<ServerMessage>> getMessages(String username, byte[] auth) {\n        List<ServerMessage> all = getMessages(username);\n        List<ServerConversation> allConvs = ServerConversation.combine(all);\n        List<ServerMessage> live = allConvs.stream()\n                .filter(c -> c.isDisplayable)\n                .flatMap(c -> c.messages.stream())\n                .sorted()\n                .collect(Collectors.toList());\n        return Futures.of(live);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> sendMessage(String username, byte[] signedBody) {\n        PublicKeyHash signerHash = pki.getPublicKeyHash(username).join().get();\n        Optional<PublicSigningKey> signerOpt = ipfs.getSigningKey(signerHash, signerHash).join();\n        if (! signerOpt.isPresent())\n            throw new IllegalStateException(\"Couldn't retrieve signer key!\");\n        byte[] raw = signerOpt.get().unsignMessage(signedBody).join();\n        CborObject cbor = CborObject.fromByteArray(raw);\n        ServerMessage message = ServerMessage.fromCbor(cbor);\n        switch (message.type) {\n            case FromUser:\n                if (Math.abs(message.sentEpochMillis - System.currentTimeMillis()) > HOUR_MILLIS)\n                    return Futures.errored(new IllegalStateException(\"Invalid send time on message!\"));\n                long count = recentMessages(username);\n                if (count > 20)\n                    return Futures.errored(new IllegalStateException(\"Please wait before sending more messages!\"));\n                addMessage(username, message);\n                break;\n            case Dismiss:\n                dismissMessage(username, message);\n                break;\n            default:\n                throw new IllegalStateException(\"Invalid message type sent from user: \" + message.type.name());\n        }\n        return Futures.of(true);\n    }\n\n    public List<Pair<String, ServerMessage>> getMessagesAfter(LocalDateTime after) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(SELECT_AFTER)) {\n            select.clearParameters();\n            select.setLong(1, after.toEpochSecond(ZoneOffset.UTC)* 1_000);\n            List<Pair<String, ServerMessage>> msgs = new ArrayList<>();\n            ResultSet res = select.executeQuery();\n            while (res.next()) {\n                String username = res.getString(1);\n                boolean dismissed = res.getBoolean(7);\n                long priorIdRaw = res.getLong(6);\n                Optional<Long> priorId = priorIdRaw == -1L ? Optional.empty() : Optional.of(priorIdRaw);\n                msgs.add(new Pair<>(username, new ServerMessage(res.getLong(2), ServerMessage.Type.byValue(res.getInt(3)),\n                        res.getLong(4), res.getString(5), priorId, dismissed)));\n            }\n            return msgs;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public List<ServerMessage> getMessages(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(SELECT)) {\n            select.clearParameters();\n            select.setString(1, username);\n            List<ServerMessage> msgs = new ArrayList<>();\n            ResultSet res = select.executeQuery();\n            while (res.next()) {\n                boolean dismissed = res.getBoolean(6);\n                long priorIdRaw = res.getLong(5);\n                Optional<Long> priorId = priorIdRaw == -1L ? Optional.empty() : Optional.of(priorIdRaw);\n                msgs.add(new ServerMessage(res.getLong(1), ServerMessage.Type.byValue(res.getInt(2)),\n                        res.getLong(3), res.getString(4), priorId, dismissed));\n            }\n            return msgs;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public long recentMessages(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement count = conn.prepareStatement(COUNT)) {\n            count.clearParameters();\n            count.setString(1, username);\n            count.setLong(2, System.currentTimeMillis() - HOUR_MILLIS);\n            ResultSet res = count.executeQuery();\n            res.next();\n            return res.getLong(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public void addMessage(String username, ServerMessage message) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(ADD)) {\n            insert.clearParameters();\n            insert.setString(1, username);\n            insert.setLong(2, message.sentEpochMillis);\n            insert.setLong(3, message.type.value);\n            insert.setString(4, message.contents);\n            insert.setLong(5, message.replyToId.orElse(-1L));\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public void dismissMessage(String username, ServerMessage message) {\n        try (Connection conn = getConnection();\n             PreparedStatement dismiss = conn.prepareStatement(DISMISS)) {\n            dismiss.clearParameters();\n            dismiss.setLong(1, message.id);\n            dismiss.setString(2, username);\n            dismiss.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n        isClosed = true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/mutable/BlockingMutablePointers.java",
    "content": "package peergos.server.mutable;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class BlockingMutablePointers implements MutablePointers {\n    private final MutablePointers source;\n    private final PublicKeyBlackList blacklist;\n\n    public BlockingMutablePointers(MutablePointers source, PublicKeyBlackList blacklist) {\n        this.source = source;\n        this.blacklist = blacklist;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        if (blacklist.isAllowed(writer))\n            return source.setPointer(owner, writer, writerSignedBtreeRootHash);\n        CompletableFuture<Boolean> res = new CompletableFuture<>();\n        res.completeExceptionally(new IllegalStateException(\"This Peergos subspace has been banned from this server\"));\n        return res;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        for (SignedPointerUpdate u : updates) {\n            if (!blacklist.isAllowed(u.writer)) {\n                CompletableFuture<Boolean> res = new CompletableFuture<>();\n                res.completeExceptionally(new IllegalStateException(\"This Peergos subspace has been banned from this server\"));\n                return res;\n            }\n        }\n        return source.setPointers(owner, updates);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        if (blacklist.isAllowed(writer))\n            return source.getPointer(owner, writer);\n        CompletableFuture<Optional<byte[]>> res = new CompletableFuture<>();\n        res.completeExceptionally(new IllegalStateException(\"This Peergos subspace has been banned from this server\"));\n        return res;\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/mutable/JdbcPointerCache.java",
    "content": "package peergos.server.mutable;\n\nimport peergos.server.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class JdbcPointerCache implements PointerCache {\n\n    private final JdbcIpnsAndSocial store;\n    private final ContentAddressedStorage storage;\n\n    public JdbcPointerCache(JdbcIpnsAndSocial store, ContentAddressedStorage storage) {\n        this.store = store;\n        this.storage = storage;\n    }\n\n    @Override\n    public synchronized CompletableFuture<Boolean> put(PublicKeyHash owner, PublicKeyHash writer, byte[] signedUpdate) {\n        return store.getPointer(writer)\n                .thenCompose(current -> storage.getSigningKey(owner, writer).thenCompose(signerOpt -> {\n                    if (signerOpt.isEmpty())\n                        throw new IllegalStateException(\"Couldn't retrieve signing key!\");\n                    if (doUpdate(current, signedUpdate, signerOpt.get()).join())\n                        return store.setPointer(writer, current, signedUpdate);\n                    return Futures.of(false);\n                }));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> get(PublicKeyHash owner, PublicKeyHash writer) {\n        return store.getPointer(writer);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/mutable/MutableEvent.java",
    "content": "package peergos.server.mutable;\n\nimport peergos.shared.crypto.hash.*;\n\n/** This propagates a change in a mutable pointer's target\n *\n */\npublic class MutableEvent {\n\n    public final PublicKeyHash owner;\n    public final PublicKeyHash writer;\n    public final byte[] writerSignedBtreeRootHash;\n\n    public MutableEvent(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        this.owner = owner;\n        this.writer = writer;\n        this.writerSignedBtreeRootHash = writerSignedBtreeRootHash;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/mutable/MutableEventPropagator.java",
    "content": "package peergos.server.mutable;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class MutableEventPropagator implements MutablePointers {\n\n    private final MutablePointers target;\n    private final List<Consumer<? super MutableEvent>> listeners = new ArrayList<>();\n\n    public MutableEventPropagator(MutablePointers target) {\n        this.target = target;\n    }\n\n    public void addListener(Consumer<? super MutableEvent> listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        return target.setPointer(owner, writer, writerSignedBtreeRootHash)\n                .thenApply(res -> {\n                    if (res) {\n                        MutableEvent event = new MutableEvent(owner, writer, writerSignedBtreeRootHash);\n                        for (Consumer<? super MutableEvent> listener : listeners) {\n                            listener.accept(event);\n                        }\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        return target.setPointers(owner, updates)\n                .thenApply(res -> {\n                    if (res) {\n                        for (SignedPointerUpdate u : updates) {\n                            MutableEvent event = new MutableEvent(owner, u.writer, u.signed);\n                            for (Consumer<? super MutableEvent> listener : listeners) {\n                                listener.accept(event);\n                            }\n                        }\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        return target.getPointer(owner, writer);\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/mutable/NonWriteThroughMutablePointers.java",
    "content": "package peergos.server.mutable;\n\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class NonWriteThroughMutablePointers implements MutablePointers {\n\n    private final MutablePointers source;\n    private final ContentAddressedStorage storage;\n    private final Map<PublicKeyHash, byte[]> modifications;\n\n    public NonWriteThroughMutablePointers(MutablePointers source, ContentAddressedStorage storage) {\n        this.source = source;\n        this.storage = storage;\n        this.modifications = new HashMap<>();\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        try {\n            if (! modifications.containsKey(writer)) {\n                Optional<byte[]> existing = source.getPointer(owner, writer).get();\n                existing.map(val -> modifications.put(writer, val));\n            }\n            Optional<PublicSigningKey> opt = storage.getSigningKey(owner, writer).get();\n            if (! opt.isPresent())\n                throw new IllegalStateException(\"Couldn't retrieve signing key!\");\n            return MutablePointers.isValidUpdate(opt.get(), Optional.ofNullable(modifications.get(writer)), writerSignedBtreeRootHash)\n                    .thenApply(x -> {\n                        modifications.put(writer, writerSignedBtreeRootHash);\n                        return true;\n                    });\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        try {\n            if (modifications.containsKey(writer))\n                return CompletableFuture.completedFuture(Optional.of(modifications.get(writer)));\n            return source.getPointer(owner, writer);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        // Validate all updates first (all-or-nothing), then apply\n        try {\n            for (SignedPointerUpdate u : updates) {\n                if (!modifications.containsKey(u.writer)) {\n                    Optional<byte[]> existing = source.getPointer(owner, u.writer).get();\n                    existing.ifPresent(val -> modifications.put(u.writer, val));\n                }\n                Optional<PublicSigningKey> opt = storage.getSigningKey(owner, u.writer).get();\n                if (!opt.isPresent())\n                    throw new IllegalStateException(\"Couldn't retrieve signing key!\");\n                MutablePointers.isValidUpdate(opt.get(), Optional.ofNullable(modifications.get(u.writer)), u.signed).get();\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n        updates.forEach(u -> modifications.put(u.writer, u.signed));\n        return Futures.of(true);\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/mutable/PublicKeyBlackList.java",
    "content": "package peergos.server.mutable;\n\nimport peergos.shared.crypto.hash.*;\n\npublic interface PublicKeyBlackList {\n\n    boolean isAllowed(PublicKeyHash keyHash);\n}\n"
  },
  {
    "path": "src/peergos/server/mutable/UserBasedBlacklist.java",
    "content": "package peergos.server.mutable;\nimport java.util.logging.*;\n\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class UserBasedBlacklist implements PublicKeyBlackList {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final long RELOAD_PERIOD_MS = 3_600_000;\n\n    private Map<PublicKeyHash, Boolean> banned = new ConcurrentHashMap<>();\n    private final CoreNode core;\n    private final MutablePointers mutable;\n    private final DeletableContentAddressedStorage dht;\n    private final Hasher hasher;\n    private final Path source;\n    private final ExecutorService pool = Threads.newPool(1, \"User-blocklist-\");\n    private long lastModified, lastReloaded;\n\n    public UserBasedBlacklist(Path source,\n                              CoreNode core,\n                              MutablePointers mutable,\n                              DeletableContentAddressedStorage dht,\n                              Hasher hasher) {\n        this.source = source;\n        this.core = core;\n        this.mutable = mutable;\n        this.dht = dht;\n        this.hasher = hasher;\n        pool.submit(() -> {\n            while (true) {\n                try {\n                    updateBlackList();\n                } catch (Throwable t) {\n                    LOG.log(Level.WARNING, t.getMessage(), t);\n                }\n                try {\n                    Thread.sleep(5000);\n                } catch (InterruptedException e) {}\n            }\n        });\n    }\n\n    private void updateBlackList() {\n        if (! source.toFile().exists())\n            return;\n        long modified = source.toFile().lastModified();\n        long now = System.currentTimeMillis();\n        if (modified != lastModified || (now - lastReloaded > RELOAD_PERIOD_MS)) {\n            LOG.info(\"Updating blacklist...\");\n            lastModified = modified;\n            lastReloaded = now;\n            Set<String> usernames = readUsernamesFromFile();\n            Set<PublicKeyHash> updated = buildBlackList(usernames);\n            banned.clear();\n            for (PublicKeyHash hash : updated) {\n                banned.put(hash, true);\n            }\n        }\n    }\n\n    private Set<String> readUsernamesFromFile() {\n        try {\n            if (! source.toFile().exists())\n                return Collections.emptySet();\n            return Files.lines(source)\n                    .map(String::trim)\n                    .collect(Collectors.toSet());\n        } catch (IOException e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            return Collections.emptySet();\n        }\n    }\n\n    private Set<PublicKeyHash> buildBlackList(Set<String> usernames) {\n        Set<PublicKeyHash> res = new HashSet<>();\n        Cid ourId = dht.id().join();\n        for (String username : usernames) {\n            PublicKeyHash owner = core.getPublicKeyHash(username).join().get();\n            res.addAll(DeletableContentAddressedStorage.getOwnedKeysRecursive(username, core, mutable,\n                    (h, s) -> DeletableContentAddressedStorage.getWriterData(Collections.emptyList(), owner, h, s, false, ourId, hasher, dht), dht, hasher).join());\n        }\n        return res;\n    }\n\n    @Override\n    public boolean isAllowed(PublicKeyHash keyHash) {\n        return ! banned.containsKey(keyHash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/AccountHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.login.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.charset.*;\nimport java.util.*;\nimport java.util.logging.Logger;\n\n/** This is the http endpoint for getting and setting encrypted login blobs\n *\n */\npublic class AccountHandler implements HttpHandler {\n    private static final Logger LOG = Logging.LOG();\n    private final Account account;\n    private final boolean isPublicServer;\n\n    public AccountHandler(Account account, boolean isPublicServer) {\n        this.account = account;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange) throws IOException {\n        long t1 = System.currentTimeMillis();\n        DataInputStream din = new DataInputStream(exchange.getRequestBody());\n\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        DataOutputStream dout = new DataOutputStream(bout);\n\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.LOGIN_URL.length()).split(\"/\");\n        String method = subComponents[0];\n\n        Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n        byte[] auth = ArrayOps.hexToBytes(params.get(\"auth\").get(0));\n        String username = \"\";\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            switch (method) {\n                case \"setLogin\":\n                    AggregatedMetrics.LOGIN_SET.inc();\n                    byte[] payload = Serialize.readFully(din, 16384);\n                    boolean forceLocal = params.containsKey(\"local\") ? Boolean.parseBoolean(params.get(\"local\").get(0)) : false;\n                    boolean isAdded = account.setLoginData(LoginData.fromCbor(CborObject.fromByteArray(payload)), auth, forceLocal).join();\n                    dout.writeBoolean(isAdded);\n                    break;\n                case \"getLogin\": {\n                    try {\n                        username = params.get(\"username\").get(0);\n                        PublicSigningKey authorisedReader = PublicSigningKey.fromByteArray(ArrayOps.hexToBytes(params.get(\"author\").get(0)));\n                        Optional<MultiFactorAuthResponse> mfa = params.containsKey(\"mfa\") ?\n                                Optional.of(MultiFactorAuthResponse.fromCbor(CborObject.fromByteArray(ArrayOps.hexToBytes(params.get(\"mfa\").get(0))))) :\n                                Optional.empty();\n                        boolean forceProxy = params.containsKey(\"proxy\") ? Boolean.parseBoolean(params.get(\"proxy\").get(0)) : false;\n                        Either<UserStaticData, MultiFactorAuthRequest> res = account.getLoginData(username, authorisedReader, auth, mfa, false, forceProxy, false).join();\n                        AggregatedMetrics.LOGIN_GET.inc();\n                        byte[] resBytes = new LoginResponse(res).serialize();\n                        dout.write(resBytes);\n                        byte[] b = bout.toByteArray();\n                        exchange.sendResponseHeaders(200, b.length);\n                        exchange.getResponseBody().write(b);\n                    } catch (Exception e) {\n                        e.printStackTrace();\n                        String msg = e.getMessage();\n                        if (msg != null && msg.contains(\"Incorrect password\")) {\n                            AggregatedMetrics.LOGIN_GET_FAILURE_PASSWORD.inc();\n                        } else if (msg != null && msg.contains(\"home server\")) {\n                            AggregatedMetrics.LOGIN_GET_FAILURE_EXTERNAL.inc();\n                        }\n                        HttpUtil.replyError(exchange, e);\n                    }\n                    return;\n                }\n                case \"listMfa\": {\n                    AggregatedMetrics.LOGIN_GET_MFA.inc();\n                    username = params.get(\"username\").get(0);\n                    List<MultiFactorAuthMethod> res = account.getSecondAuthMethods(username, auth).join();\n                    dout.write(new CborObject.CborList(res).serialize());\n                    break;\n                }\n                case \"addTotp\": {\n                    AggregatedMetrics.LOGIN_ADD_TOTP.inc();\n                    username = params.get(\"username\").get(0);\n                    TotpKey res = account.addTotpFactor(username, auth).join();\n                    dout.write(res.encode().getBytes(StandardCharsets.UTF_8));\n                    break;\n                }\n                case \"enableTotp\": {\n                    AggregatedMetrics.LOGIN_ENABLE_TOTP.inc();\n                    username = params.get(\"username\").get(0);\n                    byte[] credentialId = ArrayOps.hexToBytes(params.get(\"credid\").get(0));\n                    String code = params.get(\"code\").get(0);\n                    boolean res = account.enableTotpFactor(username, credentialId, code, auth).join();\n                    dout.write(new CborObject.CborBoolean(res).serialize());\n                    break;\n                }\n                case \"registerWebauthnStart\": {\n                    AggregatedMetrics.LOGIN_WEBAUTHN_START.inc();\n                    username = params.get(\"username\").get(0);\n                    byte[] res = account.registerSecurityKeyStart(username, auth).join();\n                    dout.write(res);\n                    break;\n                }\n                case \"registerWebauthnComplete\": {\n                    AggregatedMetrics.LOGIN_WEBAUTHN_COMPLETE.inc();\n                    username = params.get(\"username\").get(0);\n                    String keyName = params.get(\"keyname\").get(0);\n                    byte[] rawAttestation = Serialize.readFully(din, 2048);\n                    MultiFactorAuthResponse keyResponse = MultiFactorAuthResponse.fromCbor(CborObject.fromByteArray(rawAttestation));\n                    boolean res = account.registerSecurityKeyComplete(username, keyName, keyResponse, auth).join();\n                    dout.write(new CborObject.CborBoolean(res).serialize());\n                    break;\n                }\n                case \"deleteMfa\": {\n                    AggregatedMetrics.LOGIN_DELETE_MFA.inc();\n                    username = params.get(\"username\").get(0);\n                    byte[] credentialId = ArrayOps.hexToBytes(params.get(\"credid\").get(0));\n                    boolean res = account.deleteSecondFactor(username, credentialId, auth).join();\n                    dout.write(new CborObject.CborBoolean(res).serialize());\n                    break;\n                }\n                default:\n                    throw new IOException(\"Unknown method in AccountHandler!\");\n            }\n\n            byte[] b = bout.toByteArray();\n            exchange.sendResponseHeaders(200, b.length);\n            exchange.getResponseBody().write(b);\n        } catch (Exception e) {\n            e.printStackTrace();\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"AccountsHandler handled \" + method + \" request in: \" + (t2 - t1) + \" mS \" + username);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/AndroidFileReflector.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport peergos.server.util.HttpUtil;\nimport peergos.server.util.Logging;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.user.EntryPoint;\nimport peergos.shared.user.fs.AbsoluteCapability;\nimport peergos.shared.user.fs.AsyncReader;\nimport peergos.shared.user.fs.Chunk;\nimport peergos.shared.user.fs.FileWrapper;\nimport peergos.shared.util.Constants;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\npublic class AndroidFileReflector implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final boolean LOGGING = true;\n\n    private final Crypto crypto;\n    private final CoreNode core;\n    private final MutablePointers mutable;\n    private final ContentAddressedStorage dht;\n\n    public AndroidFileReflector(Crypto crypto, CoreNode core, MutablePointers mutable, ContentAddressedStorage dht) {\n        this.crypto = crypto;\n        this.core = core;\n        this.mutable = mutable;\n        this.dht = dht;\n    }\n\n    @Override\n    public void handle(HttpExchange httpExchange) {\n        long t1 = System.currentTimeMillis();\n        String path = httpExchange.getRequestURI().getPath();\n        try {\n            String host = httpExchange.getRequestHeaders().get(\"Host\").get(0);\n            if (! host.startsWith(\"localhost:\")) {\n                httpExchange.sendResponseHeaders(404, 0);\n                httpExchange.close();\n                return;\n            }\n            if (path.startsWith(\"/\"))\n                path = path.substring(1);\n            String rest = path.substring(Constants.ANDROID_FILE_REFLECTOR.length());\n            String action = rest.split(\"/\")[0];\n            if (action.equals(\"file\")) {\n                String link = rest.substring(action.length() + 1);\n\n                AbsoluteCapability cap = AbsoluteCapability.fromLink(link);\n                NetworkAccess network = NetworkAccess.buildPublicNetworkAccess(crypto.hasher, core, mutable, dht).join();\n                Optional<FileWrapper> file = network.retrieveAll(List.of(new EntryPoint(cap, \"\"))).join().stream().findFirst();\n                if (file.isEmpty()) {\n                    httpExchange.sendResponseHeaders(404, 0);\n                    httpExchange.close();\n                    return;\n                }\n                long fileSize = file.get().getSize();\n//                AsyncReader reader = file.get().getBufferedInputStream(network, crypto, (int)(fileSize >> 32), (int)fileSize, 10, x -> {}).join();\n                AsyncReader reader = file.get().getInputStream(network, crypto, fileSize, x -> {}).join();\n                OutputStream resp = httpExchange.getResponseBody();\n                httpExchange.sendResponseHeaders(200, fileSize);\n                byte[] buf = new byte[5 * 1024 * 1024];\n                for (long offset = 0; offset < fileSize; ) {\n                    int read = reader.readIntoArray(buf, 0, (int) Math.min(Chunk.MAX_SIZE, fileSize - offset)).join();\n                    offset += read;\n                    resp.write(buf, 0, read);\n                    resp.flush();\n                }\n                httpExchange.close();\n            } else if (action.equals(\"zip\")) {\n                List<String> links = Arrays.asList(rest.substring(action.length() + 1).split(\"\\\\$\"));\n                List<AbsoluteCapability> caps = links.stream().map(AbsoluteCapability::fromLink).collect(Collectors.toList());\n                NetworkAccess network = NetworkAccess.buildPublicNetworkAccess(crypto.hasher, core, mutable, dht).join();\n                Set<FileWrapper> files = network.retrieveAll(caps.stream().map(cap -> new EntryPoint(cap, \"\")).collect(Collectors.toList())).join();\n                if (files.isEmpty()) {\n                    httpExchange.sendResponseHeaders(404, 0);\n                    httpExchange.close();\n                    return;\n                }\n\n                OutputStream resp = httpExchange.getResponseBody();\n                ZipOutputStream zout = new ZipOutputStream(resp);\n                httpExchange.sendResponseHeaders(200, 0);\n                for (FileWrapper file : files) {\n                    writeDirToZip(file, zout, network, Paths.get(file.getName()));\n                }\n                zout.finish();\n                zout.flush();\n                httpExchange.close();\n            } else {\n                LOG.info(\"Unknown reflector handler: \" +httpExchange.getRequestURI());\n                httpExchange.sendResponseHeaders(404, 0);\n                httpExchange.close();\n            }\n        } catch (Exception e) {\n            LOG.severe(\"Error handling \" +httpExchange.getRequestURI());\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            HttpUtil.replyError(httpExchange, e);\n        } finally {\n            httpExchange.close();\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"File reflector Handler returned file in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n\n    private void writeDirToZip(FileWrapper dir, ZipOutputStream zout, NetworkAccess network, Path ourZipPath) throws IOException {\n        if (!dir.isDirectory()) {\n            writeFileToZip(dir, ourZipPath, zout, network);\n            return;\n        }\n        Set<FileWrapper> children = dir.getChildren(crypto.hasher, network).join();\n        for (FileWrapper child : children) {\n            Path childZipPath = ourZipPath.resolve(child.getName());\n            if (child.isDirectory()) {\n                writeDirToZip(child, zout, network, childZipPath);\n            } else {\n                writeFileToZip(child, childZipPath, zout, network);\n            }\n        }\n    }\n\n    private void writeFileToZip(FileWrapper f, Path ourZipPath, ZipOutputStream zout, NetworkAccess network) throws IOException {\n        long fileSize = f.getSize();\n        byte[] buf = new byte[(int)Math.min(fileSize, 5 * 1024 * 1024)];\n        AsyncReader reader = f.getInputStream(network, crypto, x -> {}).join();\n        zout.putNextEntry(new ZipEntry(ourZipPath.toString()));\n        for (long offset = 0; offset < fileSize; ) {\n            int read = reader.readIntoArray(buf, 0, (int) Math.min(Chunk.MAX_SIZE, fileSize - offset)).join();\n            offset += read;\n            zout.write(buf, 0, read);\n            zout.flush();\n        }\n        zout.closeEntry();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/BasicAuthHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\n\nimport java.io.*;\nimport java.util.*;\n\npublic class BasicAuthHandler implements HttpHandler\n{\n    private final String auth;\n    private final HttpHandler delegate;\n\n    public BasicAuthHandler(String auth, HttpHandler delegate) {\n        if (auth.split(\":\").length != 2)\n            throw new IllegalStateException(\"Basic auth must be of form username:password\");\n        this.auth = \"Basic \" + Base64.getEncoder().encodeToString(auth.getBytes());\n        this.delegate = delegate;\n    }\n\n    private boolean isAuthenticated(HttpExchange exchange) {\n        List<String> authorization = exchange.getRequestHeaders().get(\"Authorization\");\n        if (authorization == null || authorization.isEmpty())\n            return false;\n        String authHeader = authorization.get(0).trim();\n        return authHeader.equals(auth);\n    }\n\n    @Override\n    public void handle(HttpExchange exchange) throws IOException {\n        if (isAuthenticated(exchange)) {\n            delegate.handle(exchange);\n        } else {\n            exchange.getResponseHeaders().add(\"WWW-Authenticate\", \"Basic realm=\\\"Peergos server access\\\", charset=\\\"UTF-8\\\"\");\n            exchange.sendResponseHeaders(401, 0);\n            exchange.close();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/BatCaveHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.function.*;\n\n/** This is the http endpoint for storing Block Access Tokens (BATs)\n *\n */\npublic class BatCaveHandler implements HttpHandler {\n\n    private final BatCave bats;\n    private final CoreNode pki;\n    private final ContentAddressedStorage ipfs;\n    private final boolean isPublicServer;\n\n    public BatCaveHandler(BatCave bats, CoreNode pki, ContentAddressedStorage ipfs, boolean isPublicServer) {\n        this.bats = bats;\n        this.pki = pki;\n        this.ipfs = ipfs;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange) throws IOException {\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.BATS_URL.length()).split(\"/\");\n        String method = subComponents[0];\n\n        Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n        Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n        byte[] auth = ArrayOps.hexToBytes(last.apply(\"auth\"));\n        String username = last.apply(\"username\");\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            Cborable result;\n            switch (method) {\n                case \"addBat\":\n                    AggregatedMetrics.BAT_ADD.inc();\n                    TimeLimited.isAllowed(path, auth, 300, ipfs, pki.getPublicKeyHash(username).join().get());\n                    BatId batid = new BatId(Cid.decode(last.apply(\"batid\")));\n                    Bat bat = Bat.fromString(last.apply(\"bat\"));\n                    bats.addBat(username, batid, bat, auth);\n                    result = new CborObject.CborBoolean(true);\n                    break;\n                case \"getUserBats\":\n                    AggregatedMetrics.BATS_GET.inc();\n                    TimeLimited.isAllowed(path, auth, 300, ipfs, pki.getPublicKeyHash(username).join().get());\n                    List<BatWithId> userBats = bats.getUserBats(username, auth).join();\n                    result = new CborObject.CborList(userBats);\n                    break;\n                default:\n                    throw new IOException(\"Unknown method in BatsHandler!\");\n            }\n\n            byte[] b = result.serialize();\n            exchange.sendResponseHeaders(200, b.length);\n            exchange.getResponseBody().write(b);\n        } catch (Exception e) {\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/ConfigHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport peergos.server.UserService;\nimport peergos.server.util.HttpUtil;\nimport peergos.server.util.Logging;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.storage.BlockCache;\nimport peergos.shared.util.Constants;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.net.InetAddress;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.net.URLDecoder;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\n\npublic class ConfigHandler implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final boolean LOGGING = true;\n    private static final String LEGACY_SERVER_URL_KEY = \"peergos-url\";\n    private static final String SERVER_URL_KEY = \"server-url\";\n    private final BlockCache cache;\n    private final Optional<UserService.LocalAppProperties> localAppProps;\n\n    public ConfigHandler(BlockCache cache, Optional<UserService.LocalAppProperties> localAppProps) {\n        this.cache = cache;\n        this.localAppProps = localAppProps;\n    }\n\n    private static boolean isLoopbackHost(String host) {\n        if (host == null || host.isEmpty())\n            return false;\n        if (\"localhost\".equalsIgnoreCase(host) || \"127.0.0.1\".equals(host) || \"::1\".equals(host) || \"[::1]\".equals(host))\n            return true;\n            return false;\n    }\n\n    private static String validateServerUrl(String serverUrl) {\n        String trimmed = serverUrl.trim();\n        if (trimmed.isEmpty())\n            return \"\";\n        try {\n            URL target = new URL(trimmed);\n            if (target.getHost() == null || target.getHost().isEmpty())\n                throw new IllegalStateException(\"server-url must include a host\");\n            if (target.getUserInfo() != null)\n                throw new IllegalStateException(\"server-url must not include embedded credentials\");\n            if (target.getQuery() != null || target.getRef() != null)\n                throw new IllegalStateException(\"server-url must not include query parameters or fragments\");\n            boolean secureLoopback = \"http\".equalsIgnoreCase(target.getProtocol()) && isLoopbackHost(target.getHost());\n            if (! \"https\".equalsIgnoreCase(target.getProtocol()) && ! secureLoopback)\n                throw new IllegalStateException(\"desktop/proxy mode requires https, or http only for a loopback self-hosted server\");\n            return target.toString();\n        } catch (MalformedURLException e) {\n            throw new IllegalStateException(\"Invalid server-url: \" + trimmed, e);\n        }\n    }\n\n    private synchronized Map<String, String> readConfig(Path configFile) throws IOException {\n        if (! Files.exists(configFile))\n            return new LinkedHashMap<>();\n        List<String> lines = Files.readAllLines(configFile, StandardCharsets.UTF_8);\n        Map<String, String> config = new LinkedHashMap<>();\n        for (String originalLine : lines) {\n            String line = originalLine.trim();\n            if (line.isEmpty() || line.matches(\"\\\\s+\"))\n                continue;\n            int commentPos = line.indexOf(\"#\");\n            if (commentPos == 0)\n                continue;\n            if (commentPos != -1 && line.charAt(commentPos - 1) == ' ')\n                line = line.substring(0, commentPos).trim();\n            String[] split = line.split(\"=\", 2);\n            if (split.length != 2)\n                throw new IllegalStateException(\"Illegal line '\" + line + \"'\");\n            config.put(split[0].trim(), split[1].trim());\n        }\n        return config;\n    }\n\n    private synchronized void writeConfig(Path configFile, Map<String, String> config) throws IOException {\n        Files.createDirectories(configFile.getParent());\n        String text = new TreeMap<>(config).entrySet().stream()\n                .map(e -> e.getKey() + \" = \" + e.getValue())\n                .collect(Collectors.joining(\"\\n\"));\n        Files.write(configFile, text.getBytes(StandardCharsets.UTF_8),\n                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n    }\n\n    private synchronized Optional<String> getConfiguredServerUrl(Path configFile) throws IOException {\n        Map<String, String> config = readConfig(configFile);\n        if (config.containsKey(SERVER_URL_KEY))\n            return Optional.of(config.get(SERVER_URL_KEY));\n        return Optional.ofNullable(config.get(LEGACY_SERVER_URL_KEY));\n    }\n\n    private synchronized void saveServerUrl(Path configFile, String serverUrl) throws IOException {\n        Map<String, String> config = readConfig(configFile);\n        if (serverUrl.isEmpty()) {\n            config.remove(SERVER_URL_KEY);\n            config.remove(LEGACY_SERVER_URL_KEY);\n        } else {\n            config.put(SERVER_URL_KEY, serverUrl);\n            config.put(LEGACY_SERVER_URL_KEY, serverUrl);\n        }\n        writeConfig(configFile, config);\n    }\n\n    private static void replyJson(HttpExchange exchange, Object payload) throws IOException {\n        exchange.getResponseHeaders().set(\"Content-Type\", \"application/json\");\n        byte[] res = JSONParser.toString(payload).getBytes(StandardCharsets.UTF_8);\n        exchange.sendResponseHeaders(200, res.length);\n        try (OutputStream resp = exchange.getResponseBody()) {\n            resp.write(res);\n        }\n    }\n\n    @Override\n    public void handle(HttpExchange exchange) {\n        long t1 = System.currentTimeMillis();\n        String path = exchange.getRequestURI().getPath();\n        try {\n            if (! HttpUtil.allowedQuery(exchange, false)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n            String host = exchange.getRequestHeaders().get(\"Host\").get(0);\n            if (! host.startsWith(\"localhost:\")) {\n                exchange.sendResponseHeaders(404, 0);\n                exchange.close();\n                return;\n            }\n            if (path.startsWith(\"/\"))\n                path = path.substring(1);\n            String action = path.substring(Constants.CONFIG.length());\n            Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n            Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n\n            if (action.equals(\"cache/set-size-mb\")) {\n                long maxSizeMb = Long.parseLong(last.apply(\"size\"));\n                cache.setMaxSize(maxSizeMb * 1024*1024);\n                exchange.sendResponseHeaders(200, 0);\n                exchange.close();\n            } else if (action.equals(\"cache/get-size\")) {\n                long cacheSizeBytes = cache.getMaxSize();\n                long cacheSizeMB = cacheSizeBytes / (1024 * 1024);\n                Map<String, Object> json = new LinkedHashMap<>();\n                json.put(\"size\", cacheSizeMB);\n                replyJson(exchange, json);\n            } else if (action.equals(\"server-url/get\")) {\n                if (localAppProps.isEmpty()) {\n                    exchange.sendResponseHeaders(404, 0);\n                    exchange.close();\n                    return;\n                }\n                UserService.LocalAppProperties props = localAppProps.get();\n                Path configFile = props.peergosDir.resolve(\"config\");\n                Map<String, Object> json = new LinkedHashMap<>();\n                json.put(\"current\", props.currentServerUrl);\n                json.put(\"configured\", getConfiguredServerUrl(configFile).orElse(\"\"));\n                replyJson(exchange, json);\n            } else if (action.equals(\"server-url/set\")) {\n                if (localAppProps.isEmpty()) {\n                    exchange.sendResponseHeaders(404, 0);\n                    exchange.close();\n                    return;\n                }\n                String encoded = params.containsKey(\"url\") ? last.apply(\"url\") : \"\";\n                String newServerUrl = validateServerUrl(URLDecoder.decode(encoded, StandardCharsets.UTF_8));\n                UserService.LocalAppProperties props = localAppProps.get();\n                saveServerUrl(props.peergosDir.resolve(\"config\"), newServerUrl);\n                Map<String, Object> json = new LinkedHashMap<>();\n                json.put(\"serverUrl\", newServerUrl);\n                json.put(\"restartRequired\", true);\n                replyJson(exchange, json);\n            } else {\n                LOG.info(\"Unknown config handler: \" +exchange.getRequestURI());\n                exchange.sendResponseHeaders(404, 0);\n                exchange.close();\n            }\n        } catch (Exception e) {\n            LOG.severe(\"Error handling \" +exchange.getRequestURI());\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"Config Handler returned in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/CoreNodeHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.*;\nimport peergos.server.util.Logging;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.logging.*;\nimport java.util.zip.*;\n\npublic class CoreNodeHandler implements HttpHandler\n{\n    private static final Logger LOG = Logging.LOG();\n\n    private final CoreNode coreNode;\n    private final boolean isPublicServer;\n\n    public CoreNodeHandler(CoreNode coreNode, boolean isPublicServer) {\n        this.coreNode = coreNode;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange)\n    {\n        long t1 = System.currentTimeMillis();\n        DataInputStream din = new DataInputStream(exchange.getRequestBody());\n\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        DataOutputStream dout = new DataOutputStream(bout);\n\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.CORE_URL.length()).split(\"/\");\n        String method = subComponents[0];\n\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            switch (method)\n            {\n                case \"getChain\":\n                    AggregatedMetrics.GET_PUBLIC_KEY_CHAIN.inc();\n                    getChain(din, dout);\n                    break;\n                case \"signup\":\n                    AggregatedMetrics.SIGNUP.inc();\n                    signup(din, dout);\n                    break;\n                case \"startPaidSignup\":\n                    AggregatedMetrics.PAID_SIGNUP_START.inc();\n                    startPaidSignup(din, dout, exchange);\n                    break;\n                case \"completePaidSignup\":\n                    AggregatedMetrics.PAID_SIGNUP_COMPLETE.inc();\n                    completePaidSignup(din, dout);\n                    break;\n                case \"mirror\":\n                    AggregatedMetrics.MIRROR.inc();\n                    mirror(din, dout);\n                    break;\n                case \"startPaidMirror\":\n                    AggregatedMetrics.PAID_MIRROR_START.inc();\n                    startPaidMirror(din, dout, exchange);\n                    break;\n                case \"completePaidMirror\":\n                    AggregatedMetrics.PAID_MIRROR_COMPLETE.inc();\n                    completePaidMirror(din, dout);\n                    break;\n                case \"getUserSnapshots\":\n                    AggregatedMetrics.GET_USER_SNAPSHOTS.inc();\n                    getUserSnapshots(din, dout);\n                    break;\n                case \"updateChain\":\n                    AggregatedMetrics.UPDATE_PUBLIC_KEY_CHAIN.inc();\n                    updateChain(din, dout);\n                    break;\n                case \"getPublicKey\":\n                    AggregatedMetrics.GET_PUBLIC_KEY.inc();\n                    getPublicKey(din, dout);\n                    break;\n                case \"getUsername\":\n                    AggregatedMetrics.GET_USERNAME.inc();\n                    getUsername(din, dout);\n                    break;\n                case \"getUsernamesGzip\":\n                    AggregatedMetrics.GET_ALL_USERNAMES.inc();\n                    exchange.getResponseHeaders().set(\"Content-Encoding\", \"gzip\");\n                    exchange.getResponseHeaders().set(\"Content-Type\", \"application/json\");\n                    getAllUsernamesGzip(subComponents.length > 1 ? subComponents[1] : \"\", din, dout);\n                    break;\n                case \"migrateUser\":\n                    AggregatedMetrics.MIGRATE_USER.inc();\n                    migrateUser(din, dout);\n                    break;\n                default:\n                    throw new IOException(\"Unknown pkinode method!\");\n            }\n\n            dout.flush();\n            dout.close();\n            byte[] b = bout.toByteArray();\n            exchange.sendResponseHeaders(200, b.length);\n            exchange.getResponseBody().write(b);\n        } catch (Exception e) {\n            e.printStackTrace();\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"Corenode server handled \" + method + \" request in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n\n    void getChain(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n\n        List<UserPublicKeyLink> chain = coreNode.getChain(username).get();\n        dout.write(new CborObject.CborList(chain).serialize());\n    }\n\n    void signup(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        byte[] raw = Serialize.deserializeByteArray(din, 2 * UserPublicKeyLink.MAX_SIZE);\n        UserPublicKeyLink res = UserPublicKeyLink.fromCbor(CborObject.fromByteArray(raw));\n        OpLog ops = OpLog.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 64*1024)));\n        ProofOfWork proof = ProofOfWork.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 100)));\n        String token = CoreNodeUtils.deserializeString(din);\n        Optional<RequiredDifficulty> err = coreNode.signup(username, res, ops, proof, token).get();\n        dout.writeBoolean(err.isEmpty());\n        if (err.isPresent())\n            dout.writeInt(err.get().requiredDifficulty);\n    }\n\n    void startPaidSignup(DataInputStream din, DataOutputStream dout, HttpExchange exchange) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        UserPublicKeyLink chain = UserPublicKeyLink.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 2 * UserPublicKeyLink.MAX_SIZE)));\n        ProofOfWork proof = ProofOfWork.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 100)));\n        Either<PaymentProperties, RequiredDifficulty> res = coreNode.startPaidSignup(username, chain, proof).get();\n        dout.writeBoolean(res.isA());\n        if (res.isA())\n            Serialize.serialize(res.a().serialize(), dout);\n        else\n            dout.writeInt(res.b().requiredDifficulty);\n    }\n\n    void completePaidSignup(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        byte[] raw = Serialize.deserializeByteArray(din, 2 * UserPublicKeyLink.MAX_SIZE);\n        UserPublicKeyLink chain = UserPublicKeyLink.fromCbor(CborObject.fromByteArray(raw));\n        OpLog ops = OpLog.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 64*1024)));\n        ProofOfWork proof = ProofOfWork.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 100)));\n        byte[] signedSpaceRequest = Serialize.deserializeByteArray(din, 64 * 1024);\n        PaymentProperties res = coreNode.completePaidSignup(username, chain, ops, signedSpaceRequest, proof).get();\n        dout.write(res.serialize());\n    }\n\n    void mirror(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        byte[] raw = Serialize.deserializeByteArray(din, 100);\n        BatWithId mirrorBat = BatWithId.fromCbor(CborObject.fromByteArray(raw));\n        byte[] auth = Serialize.deserializeByteArray(din, 4096);\n        ProofOfWork proof = ProofOfWork.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 100)));\n        boolean res = coreNode.startMirror(username, mirrorBat, auth, proof).get();\n        dout.writeBoolean(res);\n    }\n\n    void startPaidMirror(DataInputStream din, DataOutputStream dout, HttpExchange exchange) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        byte[] auth = Serialize.deserializeByteArray(din, 4096);\n        ProofOfWork proof = ProofOfWork.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 100)));\n        Either<PaymentProperties, RequiredDifficulty> res = coreNode.startPaidMirror(username, auth, proof).get();\n        dout.writeBoolean(res.isA());\n        if (res.isA())\n            Serialize.serialize(res.a().serialize(), dout);\n        else\n            dout.writeInt(res.b().requiredDifficulty);\n    }\n\n    void completePaidMirror(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        byte[] raw = Serialize.deserializeByteArray(din, 100);\n        BatWithId mirrorBat = BatWithId.fromCbor(CborObject.fromByteArray(raw));\n        ProofOfWork proof = ProofOfWork.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 100)));\n        byte[] signedSpaceRequest = Serialize.deserializeByteArray(din, 64 * 1024);\n        PaymentProperties res = coreNode.completePaidMirror(username, mirrorBat, signedSpaceRequest, proof).get();\n        dout.write(res.serialize());\n    }\n\n    void getUserSnapshots(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String prefix = CoreNodeUtils.deserializeString(din);\n        byte[] raw = Serialize.deserializeByteArray(din, 100);\n        BatWithId instanceBat = BatWithId.fromCbor(CborObject.fromByteArray(raw));\n        LocalDateTime lastLinkCountsUpdate = LocalDateTime.ofEpochSecond(din.readLong(), 0, ZoneOffset.UTC);\n        List<UserSnapshot> res = coreNode.getSnapshots(prefix, instanceBat, lastLinkCountsUpdate).get();\n        dout.write(new CborObject.CborList(res).serialize());\n    }\n\n    void updateChain(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        byte[] raw = Serialize.deserializeByteArray(din, 2 * UserPublicKeyLink.MAX_SIZE);\n        List<UserPublicKeyLink> res = ((CborObject.CborList)CborObject.fromByteArray(raw)).map(UserPublicKeyLink::fromCbor);\n        ProofOfWork proof = ProofOfWork.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 100)));\n        String token = CoreNodeUtils.deserializeString(din);\n        Optional<RequiredDifficulty> err = coreNode.updateChain(username, res, proof, token).get();\n        dout.writeBoolean(err.isEmpty());\n        if (err.isPresent())\n            dout.writeInt(err.get().requiredDifficulty);\n    }\n\n    void migrateUser(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        byte[] raw = Serialize.deserializeByteArray(din, 4096);\n        List<UserPublicKeyLink> newChain = ((CborObject.CborList)CborObject.fromByteArray(raw)).map(UserPublicKeyLink::fromCbor);\n        Multihash currentStorageId = Cid.cast(Serialize.deserializeByteArray(din, 128));\n        boolean hasBat = din.readBoolean();\n        Optional<BatWithId> mirrorBat = hasBat ?\n                Optional.of(BatWithId.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 128)))) :\n                Optional.empty();\n        long seconds = din.readLong();\n        long currentUsage = din.readLong();\n        boolean commitToPki = din.readBoolean();\n        UserSnapshot state = coreNode.migrateUser(username, newChain, currentStorageId, mirrorBat, LocalDateTime.ofEpochSecond(seconds, 0, ZoneOffset.UTC), currentUsage, commitToPki).join();\n        dout.write(state.serialize());\n    }\n\n    void getPublicKey(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        String username = CoreNodeUtils.deserializeString(din);\n        Optional<PublicKeyHash> k = coreNode.getPublicKeyHash(username).get();\n        dout.writeBoolean(k.isPresent());\n        if (!k.isPresent())\n            return;\n        byte[] b = k.get().serialize();\n        dout.writeInt(b.length);\n        dout.write(b);\n    }\n\n    void getUsername(DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        byte[] publicKey = CoreNodeUtils.deserializeByteArray(din);\n        PublicKeyHash owner = PublicKeyHash.fromCbor(CborObject.fromByteArray(publicKey));\n        String k = coreNode.getUsername(owner).get();\n        if (k == null)\n            throw new IllegalStateException(\"Unknown username for key: \" + owner.toString());\n        Serialize.serialize(k, dout);\n    }\n\n    void getAllUsernamesGzip(String prefix, DataInputStream din, DataOutputStream dout) throws Exception\n    {\n        List<String> res = coreNode.getUsernames(prefix).get();\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        GZIPOutputStream gout = new GZIPOutputStream(bout);\n        gout.write(JSONParser.toString(res).getBytes());\n        gout.flush();\n        gout.close();\n        dout.write(bout.toByteArray());\n    }\n\n    public void close() throws IOException{\n        coreNode.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/CspHost.java",
    "content": "package peergos.server.net;\n\nimport java.util.*;\n\npublic class CspHost {\n\n    public final Optional<String> protocol;\n    public final String domain;\n    public final Optional<Integer> port;\n    private final String portSuffix;\n    private final boolean isWildcard;\n\n    public CspHost(Optional<String> protocol, String domain, Optional<Integer> port) {\n        if (protocol.isPresent() && ! protocol.get().equals(\"http://\") && ! protocol.get().equals(\"https://\"))\n            throw new IllegalStateException(\"Protocol must be http:// or https://\");\n        this.protocol = protocol;\n        this.domain = domain;\n        this.isWildcard = domain.equals(\"0.0.0.0\") || domain.equals(\"[::]\");\n        if (port.isPresent() && port.map(p -> p < 0 || p >= 65536).get())\n            throw new IllegalStateException(\"Invalid port \" + port.get());\n        this.port = port;\n        this.portSuffix = port.map(p -> \":\" + p).orElse(\"\");\n    }\n\n    public CspHost(String protocol, String domain) {\n        this(Optional.of(protocol), domain, Optional.empty());\n    }\n\n    public CspHost(String protocol, String domain, int port) {\n        this(Optional.of(protocol), domain, Optional.of(port));\n    }\n\n    public static boolean isLocal(String host) {\n        return host.startsWith(\"localhost\");\n    }\n\n    public CspHost wildcard() {\n        return new CspHost(protocol, \"*.\" + domain, port);\n    }\n\n    public boolean validSubdomain(String reqHost) {\n        if (! reqHost.contains(\".\"))\n            return false;\n        if (! reqHost.endsWith(portSuffix))\n            return false;\n        if (isWildcard)\n            return true;\n        String reqDomain = reqHost.substring(reqHost.indexOf(\".\") + 1, reqHost.length() - portSuffix.length());\n        return reqDomain.equals(domain);\n    }\n\n\n    public String getSubdomain(String reqHost) {\n        if (! reqHost.contains(\".\") || ! reqHost.endsWith(portSuffix))\n            return \"\";\n        if (isWildcard)\n            return reqHost.substring(0, reqHost.lastIndexOf(\".\"));\n        if (reqHost.equals(domain + portSuffix))\n            return \"\";\n        return reqHost.substring(0, reqHost.length() - (1 + domain.length() + portSuffix.length()));\n    }\n\n    public String host() {\n        return domain + port.map(p -> \":\" + p).orElse(\"\");\n    }\n\n    @Override\n    public String toString() {\n        return protocol.orElse(\"\") + domain + portSuffix;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/FileHandler.java",
    "content": "package peergos.server.net;\n\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class FileHandler extends StaticHandler\n{\n    private final Path root;\n    public FileHandler(CspHost host,\n                       List<String> blockstoreDomain,\n                       List<String> frameDomains,\n                       List<String> appSubdomains,\n                       Path root,\n                       boolean includeCsp,\n                       boolean isGzip,\n                       Optional<HttpPoster> appDevTarget) {\n        super(host, blockstoreDomain, frameDomains, appSubdomains, includeCsp, isGzip, appDevTarget);\n        this.root = root;\n    }\n\n    @Override\n    public Asset getAsset(String resourcePath) throws IOException {\n        String stem = resourcePath.startsWith(\"/\")  ?  resourcePath.substring(1) : resourcePath;\n        Path fullPath = root.resolve(stem).normalize();\n        if (! fullPath.startsWith(root))\n            throw new IllegalStateException(\"Invalid descendant path!\");\n        byte[] bytes = readResource(new FileInputStream(fullPath.toFile()), isGzip());\n        return new Asset(bytes);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/GatewayHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.util.Logging;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.logging.*;\n\npublic class GatewayHandler implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n\tprivate static final boolean LOGGING = true;\n\tprivate static final int MAX_ASSET_SIZE_CACHE = 200*1024;\n\n    private final String domainSuffix;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private final LRUCache<String, WebRootEntry> webRootCache;\n    private final LRUCache<String, Asset> assetCache;\n\n    public GatewayHandler(String domainSuffix, Crypto crypto, NetworkAccess network) {\n        this.domainSuffix = domainSuffix;\n        this.crypto = crypto;\n        this.network = network;\n        this.webRootCache = new LRUCache<>(1000);\n        this.assetCache = new LRUCache<>(1000);\n    }\n\n    private final class WebRootEntry {\n        public final FileWrapper field;\n        public final FileWrapper webRoot;\n        public final Optional<String> cspHeader;\n\n        public WebRootEntry(FileWrapper field, FileWrapper webRoot, Optional<String> cspHeader) {\n            this.field = field;\n            this.webRoot = webRoot;\n            this.cspHeader = cspHeader;\n        }\n    }\n\n    private final class Asset {\n        public final FileWrapper source;\n        public final byte[] data;\n\n        public Asset(FileWrapper source, byte[] data) {\n            this.source = source;\n            this.data = data;\n        }\n    }\n\n    private synchronized WebRootEntry lookupRoot(String owner) {\n        return webRootCache.get(owner);\n    }\n\n    private synchronized void cacheRoot(String owner, WebRootEntry webRoot) {\n        webRootCache.put(owner, webRoot);\n    }\n\n    private synchronized Asset lookupAsset(String owner, String path) {\n        return assetCache.get(owner + \"/\" + path);\n    }\n\n    private synchronized void cacheAsset(String owner, String path, Asset asset) {\n        assetCache.put(owner + \"/\" + path, asset);\n    }\n\n    private synchronized void invalidateAssets(String owner) {\n        assetCache.entrySet().removeIf(entry -> entry.getKey().startsWith(owner + \"/\"));\n    }\n\n    @Override\n    public void handle(HttpExchange httpExchange) {\n        long t1 = System.currentTimeMillis();\n        String path = httpExchange.getRequestURI().getPath();\n        try {\n            path = path.substring(1).replaceAll(\"//\", \"/\");\n            if (path.length() == 0)\n                path = \"index.html\";\n\n            String domain = httpExchange.getRequestHeaders().getFirst(\"Host\");\n            if (! domain.endsWith(domainSuffix))\n                throw new IllegalStateException(\"Incorrect domain! \" + domain);\n            String owner = domain.substring(0, domain.length() - domainSuffix.length());\n\n            WebRootEntry webRootEntry = lookupRoot(owner);\n            if (webRootEntry != null) {\n                FileWrapper updatedField = webRootEntry.field.getUpdated(network).join();\n                if (!updatedField.version.equals(webRootEntry.field.version)) {\n                    webRootEntry = new WebRootEntry(updatedField, null, Optional.empty());\n                    invalidateAssets(owner);\n                }\n            } else {\n                Path toProfileEntry = PathUtil.get(owner).resolve(\".profile\").resolve(\"webroot\");\n                AbsoluteCapability capToWebRootField = UserContext.getPublicCapability(toProfileEntry, network).join();\n                FileWrapper webRootField = network.getFile(capToWebRootField, owner).join().get();\n                webRootEntry = new WebRootEntry(webRootField, null, Optional.empty());\n            }\n\n            if (webRootEntry.webRoot == null) {\n                Path toWebRoot = PathUtil.get(new String(Serialize.readFully(webRootEntry.field, crypto, network).join()));\n                AbsoluteCapability capToWebRoot = UserContext.getPublicCapability(toWebRoot, network).join();\n                Optional<FileWrapper> webRootOpt = network.getFile(capToWebRoot, owner).join();\n                if (webRootOpt.isEmpty())\n                    throw new IllegalStateException(\"web root not present\");\n                FileWrapper webRoot = webRootOpt.get();\n                Optional<FileWrapper> headers = webRoot.getChild(\"headers.json\", crypto.hasher, network).join();\n                Optional<String> csp = headers.flatMap(f -> getCsp(f));\n                webRootEntry = new WebRootEntry(webRootEntry.field, webRoot, csp);\n                cacheRoot(owner, webRootEntry);\n            }\n\n            Asset cached = lookupAsset(owner, path);\n            if (cached != null) {\n                FileWrapper updated = cached.source.getUpdated(network).join();\n                if (updated.version.equals(cached.source.version)) {\n                    serveAsset(AsyncReader.build(cached.data), cached.source.getFileProperties(), cached.data.length,\n                            path, webRootEntry.cspHeader, httpExchange);\n                    return;\n                }\n            }\n\n            Optional<FileWrapper> assetOpt = webRootEntry.webRoot.getDescendentByPath(path, crypto.hasher, network).join();\n            if (assetOpt.isEmpty()) {\n                serve404(httpExchange, webRootEntry.webRoot);\n                return;\n            }\n            FileWrapper asset = assetOpt.get();\n            if (asset.isDirectory()) {\n                Optional<FileWrapper> index = asset.getChild(\"index.html\", crypto.hasher, network).join();\n                if (index.isPresent())\n                    asset = index.get();\n                else {\n                    serve404(httpExchange, webRootEntry.webRoot);\n                    return;\n                }\n            }\n            AsyncReader reader = asset.getInputStream(network, crypto, x -> {}).join();\n            long size = asset.getSize();\n            Optional<byte[]> bodyToCache = serveAsset(reader, asset.getFileProperties(), size, path,\n                    webRootEntry.cspHeader, httpExchange);\n            if (bodyToCache.isPresent()) {\n                cacheAsset(owner, path, new Asset(asset, bodyToCache.get()));\n            }\n        } catch (Exception e) {\n            LOG.severe(\"Error handling \" +httpExchange.getRequestURI());\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            HttpUtil.replyError(httpExchange, e);\n        } finally {\n            httpExchange.close();\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"Public file Handler returned \" + domainSuffix + \"/\" + path + \" query in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n\n    private Optional<String> getCsp(FileWrapper headers) {\n        if (headers.getSize() > 1024)\n            return Optional.empty();\n        try {\n            byte[] body = Serialize.readFully(headers.getInputStream(network, crypto, x -> {}).join(), headers.getSize()).join();\n            Map<String, String> json = (Map)JSONParser.parse(new String(body));\n            return Optional.ofNullable(json.get(\"content-security-policy\"));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private ThreadLocal<byte[]> buffer = ThreadLocal.withInitial(() -> new byte[MAX_ASSET_SIZE_CACHE]);\n\n    private Optional<byte[]> serveAsset(AsyncReader reader,\n                                        FileProperties props,\n                                        long size,\n                                        String path,\n                                        Optional<String> cspHeader,\n                                        HttpExchange httpExchange) throws IOException {\n//            if (isGzip)\n//                httpExchange.getResponseHeaders().set(\"Content-Encoding\", \"gzip\");\n\n        if (httpExchange.getRequestMethod().equals(\"HEAD\")) {\n            httpExchange.getResponseHeaders().set(\"Content-Length\", \"\" + size);\n            httpExchange.getResponseHeaders().set(\"Content-Type\", props.mimeType);\n            httpExchange.sendResponseHeaders(200, -1);\n            return Optional.empty();\n        }\n\n        // Only allow assets to be loaded from the original host\n        httpExchange.getResponseHeaders().set(\"content-security-policy\", cspHeader.orElse(\"default-src 'self'\"));\n        // Don't anyone to load Peergos site in an iframe\n        httpExchange.getResponseHeaders().set(\"x-frame-options\", \"sameorigin\");\n        // Enable cross site scripting protection\n        httpExchange.getResponseHeaders().set(\"x-xss-protection\", \"1; mode=block\");\n        // Don't let browser sniff mime types\n        httpExchange.getResponseHeaders().set(\"x-content-type-options\", \"nosniff\");\n        // Don't send Peergos referrer to anyone\n        httpExchange.getResponseHeaders().set(\"referrer-policy\", \"no-referrer\");\n        // Don't send Peergos referrer to anyone\n        httpExchange.getResponseHeaders().set(\"permissions-policy\", \"interest-cohort=()\");\n\n        if (size < MAX_ASSET_SIZE_CACHE) {\n            byte[] body = Serialize.readFully(reader, size).orTimeout(15, TimeUnit.SECONDS).join();\n            addContentType(httpExchange, path, props, body);\n            httpExchange.sendResponseHeaders(200, size);\n            OutputStream resp = httpExchange.getResponseBody();\n            resp.write(body);\n            httpExchange.close();\n            return Optional.of(body);\n        }\n\n        addContentType(httpExchange, path, props, null);\n        httpExchange.sendResponseHeaders(200, size);\n        OutputStream resp = httpExchange.getResponseBody();\n        byte[] buf = buffer.get();\n        int read;\n        long offset = 0;\n        while (offset < size && (read = reader.readIntoArray(buf, 0, (int) Math.min(size - offset, buf.length)).orTimeout(15, TimeUnit.SECONDS).join()) >= 0) {\n            resp.write(buf, 0, read);\n            offset += read;\n        }\n        httpExchange.close();\n        return Optional.empty();\n    }\n\n    private void addContentType(HttpExchange httpExchange, String path, FileProperties props, byte[] start) {\n        if (! path.endsWith(props.name))\n            path = props.name;\n        if (path.endsWith(\".js\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/javascript\");\n        else if (path.endsWith(\".html\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/html\");\n        else if (path.endsWith(\".css\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/css\");\n        else if (path.endsWith(\".json\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/json\");\n        else if (path.endsWith(\".png\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"image/png\");\n        else if (path.endsWith(\".woff\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/font-woff\");\n        else if (path.endsWith(\".svg\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"image/svg+xml\");\n        else if (path.endsWith(\".wasm\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/wasm\");\n        else if (path.endsWith(\".xml\"))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/xml\");\n        else if (start!= null && start.length > 15 && Arrays.equals(\"<!DOCTYPE html>\".getBytes(), Arrays.copyOfRange(start, 0, 15)))\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/html\");\n        else\n            httpExchange.getResponseHeaders().set(\"Content-Type\", props.mimeType);\n    }\n\n    private void serve404(HttpExchange httpExchange, FileWrapper webroot) throws IOException {\n        if (webroot != null) {\n            Optional<FileWrapper> custom404 = webroot.getChild(\"404.html\", crypto.hasher, network).join();\n            if (custom404.isPresent()) {\n                byte[] data = Serialize.readFully(custom404.get(), crypto, network).join();\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/html\");\n                httpExchange.sendResponseHeaders(404, data.length);\n                httpExchange.getResponseBody().write(data);\n                httpExchange.close();\n                return;\n            }\n        }\n        httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/plain\");\n        httpExchange.sendResponseHeaders(404, 0);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/HSTSHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\n\nimport java.util.*;\n\npublic class HSTSHandler extends ResponseHeaderHandler {\n\n    static Map<String, String> getHeaders() {\n        Map<String, String> res = new HashMap<>();\n        // use https only for at least 1 year\n        res.put(\"Strict-Transport-Security\", \"max-age=31536000\");\n        return res;\n    }\n\n    static final Map<String, String> HSTS = getHeaders();\n\n    public HSTSHandler(HttpHandler handler) {\n        super(HSTS, handler);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/InverseProxyHandler.java",
    "content": "package peergos.server.net;\nimport java.util.logging.*;\n\nimport peergos.server.util.Logging;\n\nimport com.sun.net.httpserver.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class InverseProxyHandler implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n    private final String targetDomain;\n    private final boolean isLocal;\n\n    public InverseProxyHandler(String targetDomain, boolean isLocal) {\n        this.targetDomain = targetDomain;\n        this.isLocal = isLocal;\n    }\n\n    @Override\n    public void handle(HttpExchange httpExchange) throws IOException {\n        try {\n            HttpURLConnection conn;\n            if (!isLocal) {\n                LOG.info(\"Proxying to localhost..\");\n                conn = (HttpURLConnection)new URL(\"http://localhost:8765\" + httpExchange.getRequestURI().getPath()).openConnection();\n            } else {\n                LOG.info(\"Proxying to \" + targetDomain);\n                conn = (HttpURLConnection)new URL(\"https://\" + targetDomain + httpExchange.getRequestURI().getPath()).openConnection();\n            }\n            conn.connect();\n            int respCode = conn.getResponseCode();\n            if (respCode == 500) {\n                httpExchange.sendResponseHeaders(500, 0);\n                return;\n            }\n            InputStream in = conn.getInputStream();\n\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            if (respCode == 200) {\n                byte[] tmp = new byte[256];\n                int r;\n                while ((r = in.read(tmp)) >= 0)\n                    bout.write(tmp, 0, r);\n            }\n            byte[] bytes = bout.toByteArray();\n            LOG.info(new String(bytes));\n\n            Map<String, List<String>> respHeaders = conn.getHeaderFields();\n            // status is returned under a null key, remove it\n            Map<String, List<String>> headers = respHeaders.entrySet().stream().filter(e -> e.getKey() != null)\n                    .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));\n            LOG.info(\"headers: \"+headers);\n            httpExchange.getResponseHeaders().putAll(headers);\n            httpExchange.sendResponseHeaders(respCode, bytes.length);\n            httpExchange.getResponseBody().write(bytes);\n        } catch (Throwable t) {\n            LOG.log(Level.WARNING, t.getMessage(), t);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/JarHandler.java",
    "content": "package peergos.server.net;\n\nimport peergos.shared.user.*;\n\nimport java.io.*;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class JarHandler extends StaticHandler {\n    private final Path root;\n\n    public JarHandler(CspHost host,\n                      List<String> blockstoreDomain,\n                      List<String> frameDomains,\n                      List<String> appSubdomains,\n                      boolean includeCsp,\n                      boolean isGzip,\n                      Path root,\n                      Optional<HttpPoster> appDevTarget) {\n        super(host, blockstoreDomain, frameDomains, appSubdomains, includeCsp, isGzip, appDevTarget);\n        this.root = root;\n    }\n\n    @Override\n    public Asset getAsset(String resourcePath) throws IOException {\n        return getAsset(resourcePath, root, isGzip());\n    }\n\n    public static Asset getAsset(String resourcePath, Path root, boolean gzip) throws IOException {\n        String pathWithinJar = \"/\" + root.resolve(resourcePath).toString()\n                .replaceAll(\"\\\\\\\\\", \"/\"); // needed for Windows!\n        byte[] data = StaticHandler.readResource(JarHandler.class.getResourceAsStream(pathWithinJar), gzip);\n        return new Asset(data);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/Multipart.java",
    "content": "package peergos.server.net;\n\nimport peergos.server.util.JavaPoster;\nimport peergos.shared.io.ipfs.api.NamedStreamable;\nimport peergos.shared.storage.StorageQuotaExceededException;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.net.http.*;\nimport java.nio.channels.Channels;\nimport java.nio.channels.Pipe;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.zip.GZIPInputStream;\n\npublic class Multipart {\n    private static final String LINE_FEED = \"\\r\\n\";\n    public static final ExecutorService ioPool = Executors.newCachedThreadPool();\n    private final String boundary;\n    private final CompletableFuture<byte[]> res;\n    private String charset;\n    private OutputStream out;\n    private PrintWriter writer;\n\n    public Multipart(HttpClient client, String requestURL, String charset, Map<String, String> headers, int readTimeoutMillis) throws IOException {\n        this.charset = charset;\n        this.boundary = createBoundary();\n\n        URI uri = URI.create(new URL(requestURL).toString());\n        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(uri);\n        requestBuilder.setHeader(\"User-Agent\", \"Java Peergos Client\");\n        requestBuilder.setHeader(\"Content-Type\", \"multipart/form-data; boundary=\" + boundary);\n        if (readTimeoutMillis > 0)\n            requestBuilder.timeout(Duration.ofMillis(readTimeoutMillis));\n        for (Map.Entry<String, String> e : headers.entrySet()) {\n            requestBuilder.setHeader(e.getKey(), e.getValue());\n        }\n\n        Pipe pipe = Pipe.open();\n\n        requestBuilder.POST(HttpRequest.BodyPublishers.ofInputStream(() -> Channels.newInputStream(pipe.source())));\n\n        HttpRequest request = requestBuilder.build();\n\n        out = Channels.newOutputStream(pipe.sink());\n        writer = new PrintWriter(new OutputStreamWriter(out, charset), true);\n        res = new CompletableFuture<>();\n        CompletableFuture.runAsync(() -> {\n            HttpResponse<InputStream> response = null;\n            try {\n                response = client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()).join();\n                HttpHeaders responseHeaders = response.headers();\n                Optional<String> contentEncodingOpt = responseHeaders.firstValue(\"content-encoding\");\n                boolean isGzipped = contentEncodingOpt.isPresent() && \"gzip\".equals(contentEncodingOpt.get());\n                DataInputStream din = new DataInputStream(isGzipped ?\n                        new GZIPInputStream(response.body()) :\n                        response.body());\n                byte[] resp = Serialize.readFully(din);\n                din.close();\n                int statusCode = response.statusCode();\n                if (statusCode == 200) {\n                    res.complete(resp);\n                } else if (statusCode == HttpURLConnection.HTTP_NO_CONTENT) {\n                    res.complete(new byte[0]);\n                } else {\n                    List<String> trailers = responseHeaders.map().get(\"trailer\");\n                    String trailer = String.join(\"\", trailers);\n                    if (trailer.contains(\"Storage+quota+reached\"))\n                        res.completeExceptionally(new StorageQuotaExceededException(trailer));\n                    else\n                        res.completeExceptionally(new IOException(trailer));\n                }\n            } catch (HttpTimeoutException e) {\n                res.completeExceptionally(new SocketTimeoutException(\"Socket timeout on: \" + request.uri()));\n            } catch (IOException e) {\n                JavaPoster.handleError(request.uri().toString(), res, response, e);\n            } catch (Exception e) {\n                res.completeExceptionally(e);\n            } finally {\n                writer.close();\n            }\n        }, ioPool);\n    }\n\n    public static String createBoundary() {\n        Random r = new Random();\n        String allowed = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n        StringBuilder b = new StringBuilder();\n        for (int i=0; i < 32; i++)\n            b.append(allowed.charAt(r.nextInt(allowed.length())));\n        return b.toString();\n    }\n\n    public void addFormField(String name, String value) {\n        writer.append(\"--\" + boundary).append(LINE_FEED);\n        writer.append(\"Content-Disposition: form-data; name=\\\"\" + name + \"\\\"\")\n                .append(LINE_FEED);\n        writer.append(\"Content-Type: text/plain; charset=\" + charset).append(\n                LINE_FEED);\n        writer.append(LINE_FEED);\n        writer.append(value).append(LINE_FEED);\n        writer.flush();\n    }\n\n    /** Recursive call to add a subtree to this post\n     *\n     * @param path\n     * @param dir\n     * @throws IOException\n     */\n    public void addSubtree(String path, File dir) throws IOException {\n        String dirPath = path + (path.length() > 0 ? \"/\" : \"\") + dir.getName();\n        addDirectoryPart(dirPath);\n        for (File f: dir.listFiles()) {\n            if (f.isDirectory())\n                addSubtree(dirPath, f);\n            else\n                addFilePart(\"file\", new NamedStreamable.NativeFile(dirPath + \"/\", f));\n        }\n    }\n\n    public void addDirectoryPart(String path) {\n        try {\n            writer.append(\"--\" + boundary).append(LINE_FEED);\n            writer.append(\"Content-Disposition: file; filename=\\\"\" + URLEncoder.encode(path, \"UTF-8\") + \"\\\"\").append(LINE_FEED);\n            writer.append(\"Content-Type: application/x-directory\").append(LINE_FEED);\n            writer.append(\"Content-Transfer-Encoding: binary\").append(LINE_FEED);\n            writer.append(LINE_FEED);\n            writer.append(LINE_FEED);\n            writer.flush();\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void addFilePart(String fieldName, NamedStreamable uploadFile) throws IOException {\n        Optional<String> fileName = uploadFile.getName();\n        writer.append(\"--\" + boundary).append(LINE_FEED);\n        if (!fileName.isPresent())\n            writer.append(\"Content-Disposition: file; name=\\\"\" + fieldName + \"\\\";\").append(LINE_FEED);\n        else\n            writer.append(\"Content-Disposition: file; filename=\\\"\" + fileName.get() + \"\\\"\").append(LINE_FEED);\n        writer.append(\"Content-Type: application/octet-stream\").append(LINE_FEED);\n        writer.append(\"Content-Transfer-Encoding: binary\").append(LINE_FEED);\n        writer.append(LINE_FEED);\n        writer.flush();\n\n        InputStream inputStream = uploadFile.getInputStream();\n        byte[] buffer = new byte[4096];\n        int r;\n        while ((r = inputStream.read(buffer)) != -1)\n            out.write(buffer, 0, r);\n        out.flush();\n        inputStream.close();\n\n        writer.append(LINE_FEED);\n        writer.flush();\n    }\n\n    public void addHeaderField(String name, String value) {\n        writer.append(name + \": \" + value).append(LINE_FEED);\n        writer.flush();\n    }\n\n    public CompletableFuture<byte[]> finish() throws IOException {\n        writer.append(\"--\" + boundary + \"--\").append(LINE_FEED);\n        writer.close();\n\n        return res;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/MultipartReceiver.java",
    "content": "package peergos.server.net;\n\nimport java.io.*;\nimport java.util.*;\n\npublic class MultipartReceiver {\n    private static final byte[] DOUBLE_NEW_LINE = \"\\r\\n\\r\\n\".getBytes();\n\n    public static List<byte[]> extractFiles(InputStream rawIn, String boundary) {\n        try {\n            int maxLineSize = 1024;\n            InputStream in = new BufferedInputStream(rawIn);\n            String first = readLine(in, maxLineSize);\n            if (!first.substring(2).equals(boundary))\n                throw new IllegalStateException(\"Incorrect boundary! \" + boundary + \" != \" + first.substring(2));\n            byte[] firstHeaders = readUntil(DOUBLE_NEW_LINE, in);\n\n            byte[] boundaryBytes = (\"\\r\\n--\" + boundary).getBytes();\n            List<byte[]> files = new ArrayList<>();\n\n            while (true) {\n                byte[] file = readUntil(boundaryBytes, in);\n                files.add(file);\n                byte[] headers = readUntil(DOUBLE_NEW_LINE, in);\n                if (headers.length == 0 || Arrays.equals(headers, \"--\".getBytes()))\n                    return files;\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     *\n     * @param pattern the pattern of bytes to search until\n     * @param in\n     * @return the bytes in this stream until pattern is encountered, or the end of the stream is reached\n     * @throws IOException\n     */\n    private static byte[] readUntil(byte[] pattern, InputStream in) throws IOException {\n        ByteArrayOutputStream prior = new ByteArrayOutputStream();\n        int r;\n        int indexInPattern = 0;\n        while ((r = in.read()) != -1) {\n            if ((byte) r == pattern[indexInPattern]) {\n                indexInPattern++;\n                if (indexInPattern == pattern.length)\n                    return prior.toByteArray();\n            } else {\n                if (indexInPattern > 0)\n                    prior.write(pattern, 0, indexInPattern);\n                indexInPattern = 0;\n                // be careful of case where last byte before pattern == first byte of pattern\n                if ((byte) r == pattern[0]) {\n                    indexInPattern = 1;\n                    if (pattern.length == 1)\n                        return prior.toByteArray();\n                } else\n                    prior.write(r);\n            }\n        }\n        return prior.toByteArray();\n    }\n\n    private static String readLine(InputStream in, int maxSize) throws IOException {\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        int r, total = 0;\n        while ((r = in.read()) >= 0) {\n            total++;\n\n            if (r == '\\r') {\n                int next = in.read();\n                if (next == '\\n')\n                    break;\n                bout.write(r);\n                bout.write(next);\n            } else\n                bout.write(r);\n            if (total > maxSize)\n                break;\n        }\n        return new String(bout.toByteArray());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/MutationHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.logging.*;\n\n/** This is the http endpoint for MutablePointer calls\n *\n */\npublic class MutationHandler implements HttpHandler {\n    private static final Logger LOG = Logging.LOG();\n\n    private final MutablePointers mutable;\n    private final boolean isPublicServer;\n\n    public MutationHandler(MutablePointers mutable, boolean isPublicServer) {\n        this.mutable = mutable;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange) throws IOException\n    {\n        long t1 = System.currentTimeMillis();\n        DataInputStream din = new DataInputStream(exchange.getRequestBody());\n\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        DataOutputStream dout = new DataOutputStream(bout);\n\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.MUTABLE_POINTERS_URL.length()).split(\"/\");\n        String method = subComponents[0];\n//            LOG.info(\"core method \"+ method +\" from path \"+ path);\n\n        Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n        PublicKeyHash owner = PublicKeyHash.fromString(params.get(\"owner\").get(0));\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            switch (method) {\n                case \"setPointer\": {\n                    AggregatedMetrics.MUTABLE_POINTERS_SET.inc();\n                    PublicKeyHash writer = PublicKeyHash.fromString(params.get(\"writer\").get(0));\n                    byte[] signedPayload = Serialize.readFully(din, 1024);\n                    boolean isAdded = mutable.setPointer(owner, writer, signedPayload).get();\n                    dout.writeBoolean(isAdded);\n                    break;\n                }\n                case \"setPointers\": {\n                    AggregatedMetrics.MUTABLE_POINTERS_SET.inc();\n                    MultiWriterCommit updates = MultiWriterCommit.fromCbor(CborObject.read(din, 1024*1024));\n                    boolean isAdded = mutable.setPointers(owner, updates.updates).get();\n                    dout.writeBoolean(isAdded);\n                    break;\n                }\n                case \"getPointer\": {\n                    AggregatedMetrics.MUTABLE_POINTERS_GET.inc();\n                    PublicKeyHash writer = PublicKeyHash.fromString(params.get(\"writer\").get(0));\n                    byte[] metadataBlob = mutable.getPointer(owner, writer).get().orElse(new byte[0]);\n                    dout.write(metadataBlob);\n                    break;\n                }\n                default:\n                    throw new IOException(\"Unknown method in mutable pointers!\");\n            }\n\n            byte[] b = bout.toByteArray();\n            exchange.sendResponseHeaders(200, b.length);\n            exchange.getResponseBody().write(b);\n        } catch (Exception e) {\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"Mutable pointers server handled \" + method + \" request in: \" + (t2 - t1) + \" mS \" + owner);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/ProxyChooser.java",
    "content": "package peergos.server.net;\n\nimport peergos.server.util.Args;\n\nimport java.io.IOException;\nimport java.net.*;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class ProxyChooser extends ProxySelector {\n    private final List<Proxy> proxies;\n\n    public ProxyChooser(Optional<SocketAddress> https, Proxy.Type type) {\n        proxies = https.stream().map(a -> new Proxy(type, a)).toList();\n    }\n\n    @Override\n    public List<Proxy> select(URI uri) {\n        if (uri.getHost().equalsIgnoreCase(\"localhost\"))\n            return List.of(Proxy.NO_PROXY);\n        if (! proxies.isEmpty()) {\n            return proxies;\n        }\n        return List.of(Proxy.NO_PROXY);\n    }\n\n    @Override\n    public void connectFailed(URI uri, SocketAddress socketAddress, IOException e) {}\n\n    @Override\n    public String toString() {\n        return proxies.toString();\n    }\n\n    public static InetSocketAddress parseAddress(String addr) {\n        String host = addr.substring(0, addr.indexOf(\":\"));\n        int port = Integer.parseInt(addr.substring(addr.indexOf(\":\") + 1));\n        return new InetSocketAddress(host, port);\n    }\n\n    public static Optional<ProxySelector> build(Args a) {\n        boolean useHttpProxy = a.hasArg(\"http_proxy\");\n        Optional<ProxySelector> httpProxy = ! useHttpProxy ?\n                Optional.empty() :\n                Optional.of(new ProxyChooser(\n                        useHttpProxy ?\n                                Optional.of(parseAddress(a.getArg(\"http_proxy\"))) :\n                                Optional.empty(),\n                        Proxy.Type.HTTP\n                ));\n        return httpProxy;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/PublicFileHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.AggregatedMetrics;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.util.logging.*;\n\npublic class PublicFileHandler implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final boolean LOGGING = true;\n\n    private final NetworkAccess network;\n    private static final String PATH_PREFIX = \"/public/\";\n\n    public PublicFileHandler(Hasher hasher, CoreNode core, MutablePointers mutable, ContentAddressedStorage dht) {\n        this.network = NetworkAccess.buildPublicNetworkAccess(hasher, core, mutable, dht).join();\n    }\n\n    private static boolean contains(String body, String text) {\n        return body != null && body.contains(text);\n    }\n\n    @Override\n    public void handle(HttpExchange httpExchange) {\n        long t1 = System.currentTimeMillis();\n        String path = httpExchange.getRequestURI().getPath();\n        try {\n            if (! path.startsWith(PATH_PREFIX))\n                throw new IllegalStateException(\"Public file urls must start with /public/\");\n\n            AggregatedMetrics.PUBLIC_FILE_COUNTER.inc();\n\n            path = path.substring(PATH_PREFIX.length());\n            String originalPath = path;\n\n            AbsoluteCapability cap = UserContext.getPublicCapability(PathUtil.get(originalPath), network).join();\n\n            boolean open = contains(httpExchange.getRequestURI().getQuery(), \"open=true\");\n            String link = \"/#{\\\"secretLink\\\":true%2c\\\"path\\\":\\\"\"\n                    + URLEncoder.encode(\"/\" + originalPath, \"UTF-8\")\n                    + (open ? \"\\\"%2c\\\"open\\\":true\" : \"\\\"\")\n                    + \"%2c\\\"link\\\":\\\"\" + cap.toLink() + \"\\\"}\";\n\n            httpExchange.getResponseHeaders().add(\"Location\", link);\n            httpExchange.sendResponseHeaders(302, 0); // temporary redirect\n            httpExchange.close();\n        } catch (Exception e) {\n            LOG.severe(\"Error handling \" +httpExchange.getRequestURI());\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            HttpUtil.replyError(httpExchange, e);\n        } finally {\n            httpExchange.close();\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"Public file Handler returned \" + path + \" query in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/RedirectHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\n\nimport java.io.*;\n\npublic class RedirectHandler implements HttpHandler\n{\n    private final String target;\n\n    public RedirectHandler(String target) {\n        this.target = target;\n    }\n\n    @Override\n    public void handle(HttpExchange exchange) throws IOException {\n        exchange.getResponseHeaders().add(\"Location\", target);\n        exchange.sendResponseHeaders(301, 0);\n        exchange.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/ResponseHeaderHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\n\nimport java.io.*;\nimport java.util.*;\n\npublic class ResponseHeaderHandler implements HttpHandler {\n\n    private final Map<String, String> responseHeaders;\n    private final HttpHandler handler;\n\n    public ResponseHeaderHandler(Map<String, String> responseHeaders, HttpHandler handler) {\n        this.responseHeaders = responseHeaders;\n        this.handler = handler;\n    }\n\n    @Override\n    public void handle(HttpExchange httpExchange) throws IOException {\n        for (String key: responseHeaders.keySet())\n            httpExchange.getResponseHeaders().set(key, responseHeaders.get(key));\n        handler.handle(httpExchange);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/ServerMessageHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.messages.*;\nimport peergos.server.util.Logging;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\n/** This is the http endpoint for ServerMessage calls\n *\n */\npublic class ServerMessageHandler implements HttpHandler {\n    private static final Logger LOG = Logging.LOG();\n\n    private final ServerMessager store;\n    private final CoreNode pki;\n    private final ContentAddressedStorage ipfs;\n    private final boolean isPublicServer;\n\n    public ServerMessageHandler(ServerMessager store, CoreNode pki, ContentAddressedStorage ipfs, boolean isPublicServer) {\n        this.store = store;\n        this.pki = pki;\n        this.ipfs = ipfs;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange)\n    {\n        long t1 = System.currentTimeMillis();\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.SERVER_MESSAGE_URL.length()).split(\"/\");\n        String method = subComponents[0];\n\n        Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n        Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n        String username = params.get(\"username\").get(0);\n\n        Cborable result;\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            switch (method) {\n                case \"retrieve\": {\n                    byte[] auth = ArrayOps.hexToBytes(last.apply(\"auth\"));\n                    TimeLimited.isAllowed(path, auth, 300, ipfs, pki.getPublicKeyHash(username).join().get());\n                    result = new CborObject.CborList(store.getMessages(username, auth).join()\n                            .stream()\n                            .map(Cborable::toCbor)\n                            .collect(Collectors.toList()));\n                    break;\n                }\n                case \"send\": {\n                    byte[] signedReq = Serialize.readFully(exchange.getRequestBody(), 10*1024);\n                    store.sendMessage(username, signedReq).join();\n                    result = new CborObject.CborBoolean(true);\n                    break;\n                }\n                default:\n                    throw new IOException(\"Unknown method in ServerMessageHandler!\");\n            }\n\n            byte[] b = result.serialize();\n            exchange.sendResponseHeaders(200, b.length);\n            exchange.getResponseBody().write(b);\n        } catch (Exception e) {\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"ServerMessage handled \" + method + \" request in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/SocialHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.*;\nimport peergos.server.social.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.social.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\n/** This is the http endpoint for SocialNetwork\n *\n * This receives calls to send, retrieve and remove follow requests.\n *\n */\npublic class SocialHandler implements HttpHandler {\n    private static final Logger LOG = Logging.LOG();\n\n    private final SocialNetwork social;\n    private final boolean isPublicServer;\n\n    public SocialHandler(SocialNetwork social, boolean isPublicServer) {\n        this.social = social;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange)\n    {\n        long t1 = System.currentTimeMillis();\n        DataInputStream din = new DataInputStream(exchange.getRequestBody());\n\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        DataOutputStream dout = new DataOutputStream(bout);\n\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.SOCIAL_URL.length()).split(\"/\");\n        String method = subComponents[0];\n        Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n        Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n//            LOG.info(\"social method \"+ method +\" from path \"+ path);\n\n        PublicKeyHash owner = PublicKeyHash.fromString(last.apply(\"owner\"));\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n            switch (method) {\n                case \"followRequest\":\n                    AggregatedMetrics.FOLLOW_REQUEST_COUNTER.inc();\n                    byte[] encryptedCap = Serialize.readFully(din, 4096);\n                    boolean followRequested = social.sendFollowRequest(owner, encryptedCap).get();\n                    dout.writeBoolean(followRequested);\n                    break;\n                case \"getFollowRequests\":\n                    AggregatedMetrics.GET_FOLLOW_REQUEST_COUNTER.inc();\n                    byte[] signedTime = ArrayOps.hexToBytes(last.apply(\"auth\"));\n                    byte[] res = social.getFollowRequests(owner, signedTime).get();\n                    Serialize.serialize(res, dout);\n                    break;\n                case \"removeFollowRequest\":\n                    AggregatedMetrics.REMOVE_FOLLOW_REQUEST_COUNTER.inc();\n                    byte[] signedFollowRequest = Serialize.readFully(din, 4096);\n                    boolean isRemoved = social.removeFollowRequest(owner, signedFollowRequest).get();\n                    dout.writeBoolean(isRemoved);\n                    break;\n                default:\n                    throw new IOException(\"Unknown method \"+ method);\n            }\n\n            dout.flush();\n            dout.close();\n            byte[] b = bout.toByteArray();\n            exchange.sendResponseHeaders(200, b.length);\n            exchange.getResponseBody().write(b);\n        } catch (Exception e) {\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"Social Network server handled \" + method + \" request in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/SpaceHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.util.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\n/** This is the http endpoint for SpaceUsage calls\n *\n */\npublic class SpaceHandler implements HttpHandler {\n    private static final Logger LOG = Logging.LOG();\n\n    private final SpaceUsage spaceUsage;\n    private final boolean isPublicServer;\n\n    public SpaceHandler(SpaceUsage spaceUsage, boolean isPublicServer) {\n        this.spaceUsage = spaceUsage;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange)\n    {\n        long t1 = System.currentTimeMillis();\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.SPACE_USAGE_URL.length()).split(\"/\");\n        String method = subComponents[0];\n\n        Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n        Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n        PublicKeyHash owner = PublicKeyHash.fromString(params.get(\"owner\").get(0));\n\n        Cborable result;\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            switch (method) {\n                case \"payment-properties\": {\n                    byte[] signedTime = ArrayOps.hexToBytes(last.apply(\"auth\"));\n                    boolean newClientSecret = Boolean.parseBoolean(last.apply(\"new-client-secret\"));\n                    result = spaceUsage.getPaymentProperties(owner, newClientSecret, signedTime).join().toCbor();\n                    break;\n                }\n                case \"usage\": {\n                    byte[] signedTime = ArrayOps.hexToBytes(last.apply(\"auth\"));\n                    boolean localUsage = params.containsKey(\"local\") ?\n                            Boolean.parseBoolean(last.apply(\"local\")) :\n                            false;\n                    long usage = spaceUsage.getUsage(owner, signedTime, localUsage).join();\n                    result = new CborObject.CborLong(usage);\n                    break;\n                }\n                case \"quota\": {\n                    byte[] signedTime = ArrayOps.hexToBytes(last.apply(\"auth\"));\n                    long quota = spaceUsage.getQuota(owner, signedTime).join();\n                    result = new CborObject.CborLong(quota);\n                    break;\n                }\n                case \"request\": {\n                    byte[] signedReq = ArrayOps.hexToBytes(last.apply(\"req\"));\n                    result = spaceUsage.requestQuota(owner, signedReq,  0).join();\n                    break;\n                }\n                default:\n                    throw new IOException(\"Unknown method in StorageHandler!\");\n            }\n\n            byte[] b = result.serialize();\n            exchange.sendResponseHeaders(200, b.length);\n            exchange.getResponseBody().write(b);\n        } catch (Exception e) {\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"SpaceUsage handled \" + method + \" request in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/StaticHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport io.prometheus.client.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.Hash;\nimport peergos.shared.user.*;\nimport peergos.shared.util.ArrayOps;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\nimport java.util.zip.GZIPOutputStream;\n\npublic abstract class StaticHandler implements HttpHandler\n{\n    private static final Counter indexLoads = Counter.build()\n            .name(\"main_page_loads\")\n            .help(\"Number of loads of web root\")\n            .register();\n    private static final Counter signupLoads = Counter.build()\n            .name(\"signup_page_loads\")\n            .help(\"Number of loads of signup page\")\n            .register();\n    private final boolean isGzip;\n    private final boolean includeCsp;\n    private final CspHost host;\n    private final List<String> blockstoreDomain;\n    private final List<String> appsubdomains;\n    private final List<String> frameDomains;\n    private final Map<String, String> appDomains;\n    private final Optional<HttpPoster> appDevTarget;\n\n    public StaticHandler(CspHost host,\n                         List<String> blockstoreDomain,\n                         List<String> frameDomains,\n                         List<String> appSubdomains,\n                         boolean includeCsp,\n                         boolean isGzip,\n                         Optional<HttpPoster> appDevTarget) {\n        this.host = host;\n        this.includeCsp = includeCsp;\n        this.blockstoreDomain = blockstoreDomain;\n        this.frameDomains = frameDomains;\n        this.appsubdomains = appSubdomains;\n        this.appDomains = appSubdomains.stream()\n                .collect(Collectors.toMap(s -> s, s -> s));\n        this.isGzip = isGzip;\n        this.appDevTarget = appDevTarget;\n    }\n\n    public abstract Asset getAsset(String resourcePath) throws IOException;\n\n    public static class Asset {\n        public final byte[] data;\n        public final String hash;\n\n        public Asset(byte[] data) {\n            this.data = data;\n            byte[] digest = Hash.sha256(data);\n            this.hash = ArrayOps.bytesToHex(Arrays.copyOfRange(digest, 0, 8));\n        }\n    }\n\n    protected boolean isGzip() {\n        return isGzip;\n    }\n\n\n    private static boolean isBot(String ua) {\n        if (ua == null)\n            return true;\n        ua = ua.toLowerCase();\n        return ua.contains(\"googlebot\") ||\n                ua.contains(\"bingbot\") ||\n                ua.contains(\"crawler\") ||\n                ua.contains(\"inspect\") ||\n                ua.contains(\"search\") ||\n                ua.contains(\"link-check\") ||\n                ua.contains(\"gpt\") ||\n                ua.contains(\"go-http\") ||\n                ua.contains(\"mastodon\") ||\n                ua.contains(\"curl\") ||\n                ua.contains(\"measurement\") ||\n                ua.contains(\"python\") ||\n                ua.contains(\"googleother\") ||\n                ua.contains(\"bot\") ||\n                ua.contains(\"spider\");\n    }\n\n    @Override\n    public void handle(HttpExchange httpExchange) throws IOException {\n        String path = httpExchange.getRequestURI().getPath();\n        try {\n            path = path.substring(1);\n            path = path.replaceAll(\"//\", \"/\");\n            String userAgent = httpExchange.getRequestHeaders().getFirst(\"User-Agent\");\n            boolean isBot = isBot(userAgent);\n            if (path.length() == 0) {\n                if (! isBot && httpExchange.getRequestMethod().equals(\"GET\"))\n                    indexLoads.inc();\n                path = \"index.html\";\n            }\n            String query = httpExchange.getRequestURI().getQuery();\n            if (! isBot && path.equals(\"index.html\") && query != null && query.contains(\"signup=true\")) {\n                signupLoads.inc();\n            }\n            if (path.startsWith(\"secret/\")) // secret links of form /secret/$owner/$label all get same page\n                path = \"secret.html\";\n\n            String reqHost = httpExchange.getRequestHeaders().get(\"Host\").stream().findFirst().orElse(\"\");\n            boolean isSubdomain = host.validSubdomain(reqHost);\n            String subdomain = host.getSubdomain(reqHost);\n            String app = appDomains.getOrDefault(subdomain, \"sandbox\");\n            if (isSubdomain && app.equals(\"sandbox\")) { // serve sandbox assets from sandbox sub dir for root path\n                path = \"apps/sandbox\" + (path.startsWith(\"/\") ? \"\" : \"/\") + path;\n            }\n\n            boolean isRoot = path.equals(\"index.html\");\n            Asset res;\n            boolean isAppDevResource = false;\n            if (appDevTarget.isEmpty() || ! isSubdomain || ! app.equals(\"sandbox\")) {\n                res = getAsset(path);\n            } else {\n                try {\n                    res = getAsset(path);\n                } catch (Throwable t) {\n                    isAppDevResource = true;\n                    HttpPoster poster = appDevTarget.get();\n                    String urlBase = poster.toString();\n                    String assetPath = path.substring(\"apps/sandbox/\".length());\n                    String fullUrl = urlBase.endsWith(\"/\") ? urlBase + assetPath : urlBase + \"/\" + assetPath;\n                    byte[] data = poster.get(fullUrl).join();\n                    res = new Asset(data);\n                }\n            }\n\n            if (isGzip && !isAppDevResource)\n                httpExchange.getResponseHeaders().set(\"Content-Encoding\", \"gzip\");\n            if (path.endsWith(\".js\") || path.endsWith(\".mjs\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/javascript\");\n            else if (path.endsWith(\".html\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/html\");\n            else if (path.endsWith(\".css\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/css\");\n            else if (path.endsWith(\".json\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/json\");\n            else if (path.endsWith(\".png\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"image/png\");\n            else if (path.endsWith(\".woff\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/font-woff\");\n            else if (path.endsWith(\".svg\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"image/svg+xml\");\n            else if (path.endsWith(\".wasm\"))\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/wasm\");\n\n            if (httpExchange.getRequestMethod().equals(\"HEAD\")) {\n                httpExchange.getResponseHeaders().set(\"Content-Length\", \"\" + res.data.length);\n                httpExchange.sendResponseHeaders(200, -1);\n                return;\n            }\n            if (! isRoot && ! isAppDevResource) {\n                httpExchange.getResponseHeaders().set(\"Cache-Control\", \"public, max-age=600\");\n                httpExchange.getResponseHeaders().set(\"ETag\", res.hash);\n            }\n\n            // only allow app-specific subdomain to access app-specific assets folder, or sandbox for generated subdomains\n            if (isSubdomain ^ path.startsWith(\"apps/\" + app)) {\n                System.err.println(\"404 FileNotFound: \" + path);\n                httpExchange.sendResponseHeaders(404, 0);\n                httpExchange.getResponseBody().close();\n                return;\n            }\n\n            // Only allow assets to be loaded from the original host\n            // Todo work on removing unsafe-inline from sub domains\n            if (includeCsp) {\n                // hashes are for the javascript files used by the Notes viewer\n                String editorJsHashes =\n                        \"'sha256-lc0/PlKl54k8dUpkXRKLbx2Hl3hxZMMfGpEht3SbH+g=' 'sha256-QKId69Dt6HhOzWqRT7lihmUgeIaIoZVkXEpZSzT2VMs=' \" +\n                                \"'sha256-VfwV5wiRRHSi+a/7PQvQ40QYPdHsrFSk3YPu9/q15P8=' 'sha256-hxeU9OdaYw1YqILhTS8VftPFmtyUKnApRljigHwLofw=' \" +\n                                \"'sha256-GSHOUEipdHXwJB2q/Xl3FSnCU3tQCq1kaTrC5tNHZp8=' 'sha256-opUlsM2gWfWKF1JyDAxcDnJb554L1f4wKI3eVvZ+NWA=' \" +\n                                \"'sha256-3bx9DzOjhQcTP7MO5xHfwwdJAseoNNmAydmPB1UQ4AY=' 'sha256-PSMJaPB+J5tqJRUKooSo10C9XxKB9CKdnTs8SNhYdJk=' \" +\n                                \"'sha256-1lmdc0UMkFwk1GFh7Rvp5Uz4ou4T2xDW1v7+rcXU3r8=' 'sha256-C/UKtQRkZji+AzMvJQEUSs9ElDKYVvDrELeKwYanNJk=' \" +\n                                \"'sha256-pREzLwvoUb8brgTXz40H6u2wVsnoVOx6nKm3cHMzCeA=' 'sha256-oEVuyRoTUTkqiMx6eYf/c3fN6QSvvp3isF6sezjEtXU=' \" +\n                                \"'sha256-VKpq7Tv3d7WDY3HqKk+CBx8mgM7XzLNPrMaZ2ywNqJQ=' 'sha256-Srpwsy3vwG/eAJoQeBIuDByNVW8j1gAgEg+nlAj332o=' \" +\n                                \"'sha256-RrUjQF+NhOeE8uVQki4T3Yje+smugBEaN0NGPuR8tBg='\";\n                httpExchange.getResponseHeaders().set(\"content-security-policy\",\n                        \"default-src 'self' \" +  this.host + \";script-src 'self' \" +\n                        this.host +\n                        (isSubdomain ? \" \" + editorJsHashes : \"\") +\n                        \";\" +\n                        \"style-src 'self' \" +\n                        \" \" + this.host +\n                        (isSubdomain ? \" 'unsafe-inline' https://\" + reqHost : \"\") + // calendar, editor, todoboard, pdfviewer\n                        \";\" +\n                        (isSubdomain ? \"sandbox allow-same-origin allow-scripts allow-forms allow-modals;\" : \"\") +\n                        \"frame-src 'self' \" + frameDomains.stream().collect(Collectors.joining(\" \")) + \" \" + (isSubdomain ? \"\" : this.host.wildcard()) + \";\" +\n                        \"frame-ancestors 'self' \" + this.host + \";\" +\n                        \"connect-src 'self' \" + this.host +\n                        (isSubdomain ? \"\" : blockstoreDomain.stream().map(d -> \" https://\" + d).collect(Collectors.joining())) + \";\" +\n                        \"media-src 'self' \" + this.host + \" blob:;\" +\n                        \"img-src 'self' \" + this.host + \" data: blob:;\" +\n                        \"object-src 'none';\"\n                );\n            }\n            // Enable COEP, CORP, COOP\n            httpExchange.getResponseHeaders().set(\"Cross-Origin-Embedder-Policy\", \"require-corp\");\n            httpExchange.getResponseHeaders().set(\"Cross-Origin-Resource-Policy\", isSubdomain ? \"cross-origin\" : \"same-origin\");\n            httpExchange.getResponseHeaders().set(\"Cross-Origin-Opener-Policy\", \"same-origin\");\n\n            // Request same site, cross origin isolation\n            httpExchange.getResponseHeaders().set(\"Origin-Agent-Cluster\", \"?1\");\n\n            // Don't let anyone load main Peergos site in an iframe (legacy header)\n            if (!isSubdomain)\n                httpExchange.getResponseHeaders().set(\"x-frame-options\", \"sameorigin\");\n            // Enable cross site scripting protection\n            httpExchange.getResponseHeaders().set(\"x-xss-protection\", \"1; mode=block\");\n            // Disable prefetch which can be used to exfiltrate data cross domain\n            httpExchange.getResponseHeaders().set(\"x-dns-prefetch-control\", \"off\");\n            // Don't let browser sniff mime types\n            httpExchange.getResponseHeaders().set(\"x-content-type-options\", \"nosniff\");\n            // Don't send Peergos referrer to anyone\n            httpExchange.getResponseHeaders().set(\"referrer-policy\", \"no-referrer\");\n            // allow list of permissions\n            httpExchange.getResponseHeaders().set(\"permissions-policy\",\n                    \"interest-cohort=(), geolocation=(), gyroscope=(), magnetometer=(), accelerometer=(), microphone=(), \" +\n                    \"camera=(self), fullscreen=(self)\");\n            if (! isRoot) {\n                String previousEtag = httpExchange.getRequestHeaders().getFirst(\"If-None-Match\");\n                if (res.hash.equals(previousEtag)) {\n                    httpExchange.sendResponseHeaders(304, -1); // NOT MODIFIED\n                    return;\n                }\n            }\n\n            httpExchange.sendResponseHeaders(200, res.data.length);\n            OutputStream out = httpExchange.getResponseBody();\n            try (out) {\n                out.write(res.data);\n            }\n        } catch (Throwable t) {\n            System.err.println(\"404 FileNotFound: \" + path);\n            httpExchange.sendResponseHeaders(404, 0);\n            httpExchange.getResponseBody().close();\n        }\n    }\n\n\n    protected static byte[] readResource(InputStream in, boolean gzip) throws IOException {\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        OutputStream gout = gzip ? new GZIPOutputStream(bout) : new DataOutputStream(bout);\n        byte[] tmp = new byte[4096];\n        int r;\n        while ((r=in.read(tmp)) >= 0)\n            gout.write(tmp, 0, r);\n        gout.flush();\n        gout.close();\n        in.close();\n        return bout.toByteArray();\n    }\n\n\n    public StaticHandler withCache() {\n        Map<String, Asset> cache = new ConcurrentHashMap<>();\n        StaticHandler that = this;\n\n        return new StaticHandler(host, blockstoreDomain, frameDomains, appsubdomains, includeCsp, isGzip, appDevTarget) {\n            @Override\n            public Asset getAsset(String resourcePath) throws IOException {\n                if (! cache.containsKey(resourcePath))\n                    cache.put(resourcePath, that.getAsset(resourcePath));\n                return cache.get(resourcePath);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/StopHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport peergos.server.util.HttpUtil;\nimport peergos.server.util.Logging;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.user.EntryPoint;\nimport peergos.shared.user.fs.AbsoluteCapability;\nimport peergos.shared.user.fs.AsyncReader;\nimport peergos.shared.user.fs.Chunk;\nimport peergos.shared.user.fs.FileWrapper;\nimport peergos.shared.util.Constants;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\npublic class StopHandler implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final boolean LOGGING = true;\n\n    public StopHandler() {}\n\n    @Override\n    public void handle(HttpExchange exchange) {\n        try {\n            String host = exchange.getRequestHeaders().get(\"Host\").get(0);\n            if (! host.startsWith(\"localhost:\")) {\n                exchange.sendResponseHeaders(404, 0);\n                exchange.close();\n                return;\n            }\n            if (! HttpUtil.allowedQuery(exchange, false)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            exchange.sendResponseHeaders(200, 0);\n            exchange.close();\n            System.exit(0);\n        } catch (Exception e) {\n            LOG.severe(\"Error handling \" + exchange.getRequestURI());\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/StorageHandler.java",
    "content": "package peergos.server.net;\nimport java.time.*;\nimport java.util.function.Supplier;\nimport java.util.logging.*;\n\nimport io.prometheus.client.*;\nimport peergos.server.AggregatedMetrics;\nimport peergos.server.util.*;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.io.ipfs.bases.Multibase;\nimport peergos.shared.storage.*;\nimport com.sun.net.httpserver.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport static peergos.shared.storage.ContentAddressedStorage.HTTP.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class StorageHandler implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final boolean LOGGING = true;\n    private final ContentAddressedStorage dht;\n    private final Hasher hasher;\n    private final BiFunction<PublicKeyHash, Integer, Boolean> keyFilter;\n    private final String apiPrefix;\n    private final boolean isPublicServer;\n\n    public StorageHandler(ContentAddressedStorage dht,\n                          Hasher hasher,\n                          BiFunction<PublicKeyHash, Integer, Boolean> keyFilter,\n                          String apiPrefix,\n                          boolean isPublicServer) {\n        this.dht = dht;\n        this.hasher = hasher;\n        this.keyFilter = keyFilter;\n        this.apiPrefix = apiPrefix;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public StorageHandler(ContentAddressedStorage dht,\n                          Hasher hasher,\n                          BiFunction<PublicKeyHash, Integer, Boolean> keyFilter,\n                          boolean isPublicServer) {\n        this(dht, hasher, keyFilter, \"/api/v0/\", isPublicServer);\n    }\n\n    @Override\n    public void handle(HttpExchange httpExchange) {\n        long t1 = System.currentTimeMillis();\n        String path = httpExchange.getRequestURI().getPath();\n        Optional<PublicKeyHash> ownerHash = Optional.empty();\n        try {\n            if (! HttpUtil.allowedQuery(httpExchange, isPublicServer)) {\n                httpExchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            if (! path.startsWith(apiPrefix))\n                throw new IllegalStateException(\"Unsupported api version, required: \" + apiPrefix);\n            path = path.substring(apiPrefix.length());\n            // N.B. URI.getQuery() decodes the query string\n            Map<String, List<String>> params = HttpUtil.parseQuery(httpExchange.getRequestURI().getQuery());\n            List<String> args = params.get(\"arg\");\n            Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n            ownerHash = params.containsKey(\"owner\") ?\n                    Optional.of(PublicKeyHash.fromString(last.apply(\"owner\"))) :\n                    Optional.empty();\n\n            switch (path) {\n                case BLOCKSTORE_PROPERTIES: {\n                    dht.blockStoreProperties().thenAccept(p -> {\n                        replyBytes(httpExchange, p.serialize(), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case LINK_HOST: {\n                    dht.linkHost(ownerHash.get()).thenAccept(p -> {\n                        replyJson(httpExchange, p, Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case AUTH_WRITES: {\n                    TransactionId tid = new TransactionId(last.apply(\"transaction\"));\n                    PublicKeyHash writerHash = PublicKeyHash.fromString(last.apply(\"writer\"));\n                    byte[] reqBody = Serialize.readFully(httpExchange.getRequestBody());\n                    WriteAuthRequest req = WriteAuthRequest.fromCbor(CborObject.fromByteArray(reqBody));\n                    List<byte[]> signatures = req.signatures;\n                    List<Integer> blockSizes = req.sizes.stream()\n                            .map(x -> {\n                                if (x < 0 || x > Integer.MAX_VALUE)\n                                    throw new IllegalStateException(\"Invalid block size: \" + x);\n                                return x.intValue();\n                            })\n                            .collect(Collectors.toList());\n                    boolean isRaw = Boolean.parseBoolean(last.apply(\"raw\"));\n                    dht.authWrites(ownerHash.get(), writerHash, signatures, blockSizes, req.batIds, isRaw, tid).thenAccept(res -> {\n                        replyBytes(httpExchange, new CborObject.CborList(res).serialize(), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case AUTH_READS: {\n                    CborObject cbor = CborObject.fromByteArray(Serialize.readFully(httpExchange.getRequestBody()));\n                    List<BlockMirrorCap> blockCaps = ((CborObject.CborList) cbor).map(BlockMirrorCap::fromCbor);\n                    PublicKeyHash owner = ownerHash.orElse(null);\n                    dht.authReads(owner, blockCaps).thenAccept(res -> {\n                        replyBytes(httpExchange, new CborObject.CborList(res).serialize(), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case TRANSACTION_START: {\n                    AggregatedMetrics.STORAGE_TRANSACTION_START.inc();\n                    dht.startTransaction(ownerHash.get()).thenAccept(tid -> {\n                        replyJson(httpExchange, tid.toString(), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case TRANSACTION_CLOSE: {\n                    AggregatedMetrics.STORAGE_TRANSACTION_CLOSE.inc();\n                    TransactionId tid = new TransactionId(args.get(0));\n                    dht.closeTransaction(ownerHash.get(), tid).thenAccept(b -> {\n                        replyJson(httpExchange, JSONParser.toString(b ? 1 : 0), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case CHAMP_GET: {\n                    AggregatedMetrics.STORAGE_CHAMP_GET.inc();\n                    Histogram.Timer timer = AggregatedMetrics.STORAGE_CHAMP_GET_DURATION.labels(\"duration\").startTimer();\n                    Cid root = Cid.decode(args.get(0));\n                    byte[] champKey = ArrayOps.hexToBytes(args.get(1));\n                    Optional<BatWithId> bat = params.containsKey(\"bat\") ?\n                            Optional.of(BatWithId.decode(last.apply(\"bat\"))) :\n                            Optional.empty();\n                    try {\n                        dht.getChampLookup(ownerHash.get(), root, Arrays.asList(new ChunkMirrorCap(champKey, bat)), Optional.empty()).thenAccept(blocks -> {\n                            replyBytes(httpExchange, new CborObject.CborList(blocks.stream()\n                                    .map(CborObject.CborByteArray::new).collect(Collectors.toList())).serialize(), Optional.of(root));\n                        }).exceptionally(Futures::logAndThrow).get();\n                    } finally {\n                        timer.observeDuration();\n                    }\n                    break;\n                }\n                case CHAMP_GET_BULK: {\n                    AggregatedMetrics.STORAGE_CHAMP_GET.inc();\n                    Histogram.Timer timer = AggregatedMetrics.STORAGE_CHAMP_GET_DURATION.labels(\"duration\").startTimer();\n                    Cid root = Cid.decode(args.get(0));\n\n                    if (params.containsKey(\"caps\")) {\n                        try {\n                            byte[] capsRaw = Multibase.decode(last.apply(\"caps\"));\n                            List<ChunkMirrorCap> caps = ((CborObject.CborList) CborObject.fromByteArray(capsRaw)).map(ChunkMirrorCap::fromCbor);\n                            if (caps.size() > MAX_CHAMP_GETS)\n                                throw new IllegalStateException(\"Too many caps in bulk champ get call! \" + caps.size());\n                            dht.getChampLookup(ownerHash.get(), root, caps, Optional.empty()).thenAccept(blocks -> {\n                                replyBytes(httpExchange, new CborObject.CborList(blocks.stream()\n                                        .map(CborObject.CborByteArray::new).collect(Collectors.toList())).serialize(), Optional.of(root));\n                            }).exceptionally(Futures::logAndThrow).get();\n                        } finally {\n                            timer.observeDuration();\n                        }\n                        break;\n                    }\n\n                    byte[] champKey = ArrayOps.hexToBytes(args.get(1));\n                    Optional<BatWithId> bat = params.containsKey(\"bat\") ?\n                            Optional.of(BatWithId.decode(last.apply(\"bat\"))) :\n                            Optional.empty();\n                    try {\n                        dht.getChampLookup(ownerHash.get(), root, List.of(new ChunkMirrorCap(champKey, bat)), Optional.empty()).thenAccept(blocks -> {\n                            replyBytes(httpExchange, new CborObject.CborList(blocks.stream()\n                                    .map(CborObject.CborByteArray::new).collect(Collectors.toList())).serialize(), Optional.of(root));\n                        }).exceptionally(Futures::logAndThrow).get();\n                    } finally {\n                        timer.observeDuration();\n                    }\n                    break;\n                }\n                case LINK_GET: {\n                    AggregatedMetrics.STORAGE_LINK_GET.inc();\n                    Histogram.Timer timer = AggregatedMetrics.STORAGE_LINK_GET_DURATION.labels(\"duration\").startTimer();\n                    long label = Long.parseLong(last.apply(\"label\"));\n                    SecretLink lookup = new SecretLink(ownerHash.get(), label, \"\");\n                    try {\n                        dht.getSecretLink(lookup).thenAccept(link -> {\n                            replyBytes(httpExchange, link.serialize(), Optional.empty());\n                        }).exceptionally(Futures::logAndThrow).get();\n                    } finally {\n                        timer.observeDuration();\n                    }\n                    break;\n                }\n                case LINK_COUNTS: {\n                    AggregatedMetrics.STORAGE_LINK_COUNTS.inc();\n                    String owner = last.apply(\"owner\");\n                    long seconds = Long.parseLong(last.apply(\"after\"));\n                    LocalDateTime after = LocalDateTime.ofEpochSecond(seconds, 0, ZoneOffset.UTC);\n                    BatWithId mirrorBat = BatWithId.decode(last.apply(\"bat\"));\n                    dht.getLinkCounts(owner, after, mirrorBat).thenAccept(counts -> {\n                        replyBytes(httpExchange, counts.serialize(), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case BLOCK_PUT: {\n                    AggregatedMetrics.STORAGE_BLOCK_PUT.inc();\n                    TransactionId tid = new TransactionId(last.apply(\"transaction\"));\n                    PublicKeyHash writerHash = PublicKeyHash.fromString(last.apply(\"writer\"));\n                    List<byte[]> signatures = Arrays.stream(last.apply(\"signatures\").split(\",\"))\n                            .map(ArrayOps::hexToBytes)\n                            .collect(Collectors.toList());\n                    String boundary = httpExchange.getRequestHeaders().get(\"Content-Type\")\n                            .stream()\n                            .filter(s -> s.contains(\"boundary=\"))\n                            .map(s -> s.substring(s.indexOf(\"=\") + 1))\n                            .findAny()\n                            .get();\n                    List<byte[]> data = MultipartReceiver.extractFiles(httpExchange.getRequestBody(), boundary);\n                    boolean isRaw = last.apply(\"format\").equals(\"raw\");\n\n                    // check writer is allowed to write to this server, and check their free space\n                    if (! keyFilter.apply(writerHash, data.stream().mapToInt(x -> x.length).sum()))\n                        throw new IllegalStateException(\"Key not allowed to write to this server: \" + writerHash);\n\n                    // Get the actual key, unless this is the initial write of the signing key during sign up\n                    // In the initial put of a signing key during sign up the key signs itself (we still check the hash\n                    // against the core node)\n                    Supplier<PublicSigningKey> fromDht = () -> {\n                        try {\n                            return dht.getSigningKey(writerHash, writerHash).get().get();\n                        } catch (Exception e) {\n                            throw new RuntimeException(e);\n                        }\n                    };\n                    Supplier<PublicSigningKey> inBandOrDht = () -> {\n                        try {\n                            PublicSigningKey candidateKey = PublicSigningKey.fromByteArray(data.get(0));\n                            PublicKeyHash calculatedHash = ContentAddressedStorage.hashKey(candidateKey);\n                            if (calculatedHash.equals(writerHash)) {\n                                candidateKey.unsignMessage(signatures.get(0));\n                                return candidateKey;\n                            }\n                        } catch (Throwable e) {\n                            // If signature is not valid then the signing key has already been written, retrieve it\n                            // This happens for the boxing key during sign up for example\n                        }\n                        return fromDht.get();\n                    };\n                    PublicSigningKey writer = data.size() > 1 ? fromDht.get() : inBandOrDht.get();\n\n                    // verify signatures\n                    for (int i = 0; i < data.size(); i++) {\n                        byte[] signature = signatures.get(i);\n                        byte[] hash = hasher.sha256(data.get(i)).join();\n                        byte[] unsigned = writer.unsignMessage(signature).join();\n                        if (! Arrays.equals(unsigned, hash))\n                            throw new IllegalStateException(\"Invalid signature for block!\");\n                    }\n\n                    List<Cid> hashes = (isRaw ?\n                            dht.putRaw(ownerHash.get(), writerHash, signatures, data, tid, x -> {}) :\n                            dht.put(ownerHash.get(), writerHash, signatures, data, tid)).get();\n                    List<Object> json = hashes.stream()\n                            .map(h -> wrapHash(h))\n                            .collect(Collectors.toList());\n                    // make stream of JSON objects\n                    String jsonStream = json.stream()\n                            .map(m -> JSONParser.toString(m))\n                            .reduce(\"\", (a, b) -> a + b);\n                    replyJson(httpExchange, jsonStream, Optional.empty());\n                    break;\n                }\n                case BLOCK_PUT_BULK: {\n                    AggregatedMetrics.STORAGE_BLOCK_PUT_BULK.inc();\n                    TransactionId tid = new TransactionId(last.apply(\"transaction\"));\n                    PublicKeyHash writerHash = PublicKeyHash.fromString(last.apply(\"writer\"));\n                    BlockWriteGroup writes = BlockWriteGroup.fromCbor(CborObject.read(httpExchange.getRequestBody(), 2 * ContentAddressedStorage.MAX_BLOCK_SIZE));\n                    boolean isRaw = last.apply(\"format\").equals(\"raw\");\n\n                    // check writer is allowed to write to this server, and check their free space\n                    if (! keyFilter.apply(writerHash, writes.blocks.stream().mapToInt(x -> x.length).sum()))\n                        throw new IllegalStateException(\"Key not allowed to write to this server: \" + writerHash);\n\n                    // Get the actual key, unless this is the initial write of the signing key during sign up\n                    // In the initial put of a signing key during sign up the key signs itself (we still check the hash\n                    // against the core node)\n                    Supplier<PublicSigningKey> fromDht = () -> {\n                        try {\n                            return dht.getSigningKey(writerHash, writerHash).get().get();\n                        } catch (Exception e) {\n                            throw new RuntimeException(e);\n                        }\n                    };\n                    Supplier<PublicSigningKey> inBandOrDht = () -> {\n                        try {\n                            PublicSigningKey candidateKey = PublicSigningKey.fromByteArray(writes.blocks.get(0));\n                            PublicKeyHash calculatedHash = ContentAddressedStorage.hashKey(candidateKey);\n                            if (calculatedHash.equals(writerHash)) {\n                                candidateKey.unsignMessage(writes.signatures.get(0));\n                                return candidateKey;\n                            }\n                        } catch (Throwable e) {\n                            // If signature is not valid then the signing key has already been written, retrieve it\n                            // This happens for the boxing key during sign up for example\n                        }\n                        return fromDht.get();\n                    };\n                    PublicSigningKey writer = writes.blocks.size() > 1 ? fromDht.get() : inBandOrDht.get();\n\n                    // verify signatures\n                    for (int i = 0; i < writes.blocks.size(); i++) {\n                        byte[] signature = writes.signatures.get(i);\n                        byte[] hash = hasher.sha256(writes.blocks.get(i)).join();\n                        byte[] unsigned = writer.unsignMessage(signature).join();\n                        if (! Arrays.equals(unsigned, hash))\n                            throw new IllegalStateException(\"Invalid signature for block!\");\n                    }\n\n                    List<Cid> hashes = (isRaw ?\n                            dht.putRaw(ownerHash.get(), writerHash, writes.signatures, writes.blocks, tid, x -> {}) :\n                            dht.put(ownerHash.get(), writerHash, writes.signatures, writes.blocks, tid)).get();\n                    List<Object> json = hashes.stream()\n                            .map(h -> wrapHash(h))\n                            .collect(Collectors.toList());\n                    // make stream of JSON objects\n                    String jsonStream = json.stream()\n                            .map(m -> JSONParser.toString(m))\n                            .reduce(\"\", (a, b) -> a + b);\n                    replyJson(httpExchange, jsonStream, Optional.empty());\n                    break;\n                }\n                case BLOCK_GET:{\n                    AggregatedMetrics.STORAGE_BLOCK_GET.inc();\n                    Cid hash = Cid.decode(args.get(0));\n                    Optional<BatWithId> bat = params.containsKey(\"bat\") ?\n                            Optional.of(BatWithId.decode(last.apply(\"bat\"))) :\n                            Optional.empty();\n                    Optional<byte[]> block = hash.codec == Cid.Codec.Raw ?\n                            dht.getRaw(ownerHash.get(), hash, bat).join() :\n                            dht.get(ownerHash.get(), hash, bat).thenApply(opt -> opt.map(CborObject::toByteArray)).join();\n                    replyBytes(httpExchange,\n                            block.orElse(new byte[0]), block.map(x -> hash));\n                    break;\n                }\n                case BLOCK_STAT: {\n                    AggregatedMetrics.STORAGE_BLOCK_STAT.inc();\n                    Multihash block = Cid.decode(args.get(0));\n                    dht.getSize(ownerHash.get(), block).thenAccept(sizeOpt -> {\n                        Map<String, Object> res = new HashMap<>();\n                        res.put(\"Size\", sizeOpt.orElse(0));\n                        String json = JSONParser.toString(res);\n                        replyJson(httpExchange, json, Optional.of(block));\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case ID: {\n                    AggregatedMetrics.STORAGE_ID.inc();\n                    dht.id().thenAccept(id -> {\n                        Object json = wrapHash(\"ID\", id);\n                        replyJson(httpExchange, JSONParser.toString(json), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case IDS: {\n                    AggregatedMetrics.STORAGE_IDS.inc();\n                    dht.ids().thenAccept(ids -> {\n                        Map<String, Object> json = new TreeMap<>();\n                        json.put(\"IDS\", ids.stream()\n                                .map(x -> x.toString())\n                                .collect(Collectors.toList()));\n                        replyJson(httpExchange, JSONParser.toString(json), Optional.empty());\n                    }).exceptionally(Futures::logAndThrow).get();\n                    break;\n                }\n                case IPNS_GET: {\n                    AggregatedMetrics.STORAGE_IPNS_GET.inc();\n                    Multihash signer = Multihash.fromBase58(args.get(0));\n                    dht.getIpnsEntry(signer).thenAccept(rec -> {\n                        replyJson(httpExchange, JSONParser.toString(rec.toJson()), Optional.empty());\n                    });\n                    break;\n                }\n                default: {\n                    httpExchange.sendResponseHeaders(404, 0);\n                }\n            }\n        } catch (Throwable e) {\n            Throwable t = Exceptions.getRootCause(e);\n            if (t instanceof RateLimitException || t instanceof MajorRateLimitException) {\n                HttpUtil.replyErrorWithCode(httpExchange, 429, Optional.ofNullable(t.getMessage()).orElse(\"Too Many Requests\"));\n            } else {\n                LOG.severe(\"Error handling \" + httpExchange.getRequestURI());\n                LOG.log(Level.WARNING, t.getMessage(), t);\n                HttpUtil.replyError(httpExchange, t);\n            }\n        } finally {\n            httpExchange.close();\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"DHT Handler handled \" + path + \" query in: \" + (t2 - t1) + \" mS \" + ownerHash.map(PublicKeyHash::toString).orElse(\"\"));\n        }\n    }\n\n    private static Map<String, Object> wrapHash(Multihash h) {\n        return wrapHash(\"Hash\", h);\n    }\n\n    private static Map<String, Object> wrapHash(String key, Multihash h) {\n        Map<String, Object> json = new TreeMap<>();\n        json.put(key, h.toString());\n        return json;\n    }\n\n    private static void replyJson(HttpExchange exchange, String json, Optional<Multihash> key) {\n        try {\n            if (key.isPresent()) {\n                exchange.getResponseHeaders().set(\"Cache-Control\", \"public, max-age=31622400 immutable\");\n                exchange.getResponseHeaders().set(\"ETag\", \"\\\"\" + key.get().toString() + \"\\\"\");\n            }\n            byte[] raw = json.getBytes();\n            exchange.sendResponseHeaders(200, raw.length);\n            DataOutputStream dout = new DataOutputStream(exchange.getResponseBody());\n            dout.write(raw);\n            dout.flush();\n            dout.close();\n        } catch (IOException e)\n        {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n\n    private static void replyBytes(HttpExchange exchange, byte[] body, Optional<Multihash> key) {\n        try {\n            if (key.isPresent()) {\n                exchange.getResponseHeaders().set(\"Cache-Control\", \"public, max-age=31622400 immutable\");\n                exchange.getResponseHeaders().set(\"ETag\", \"\\\"\" + key.get().toString() + \"\\\"\");\n            }\n            exchange.sendResponseHeaders(200, body.length);\n            DataOutputStream dout = new DataOutputStream(exchange.getResponseBody());\n            dout.write(body);\n            dout.flush();\n            dout.close();\n        } catch (IOException e)\n        {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/SubdomainHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\nimport java.util.logging.*;\nimport java.util.regex.*;\nimport java.util.stream.*;\n\npublic class SubdomainHandler implements HttpHandler\n{\n    private static final Logger LOG = Logging.LOG();\n    private final List<String> domains;\n    private final HttpHandler handler;\n    private final boolean allowSubdomains, allowAnyIp4, allowAnyIp6;\n    private final Optional<Pattern> IP4Wildcard;\n    private final int ipv6PortSuffixSize;\n\n    public SubdomainHandler(List<String> domains, HttpHandler handler, boolean allowSubdomains) {\n        this.handler = handler;\n        this.allowSubdomains = allowSubdomains;\n        Optional<String> allIp4s = domains.stream().filter(d -> d.startsWith(\"0.0.0.0\")).findAny();\n        this.allowAnyIp4 = allIp4s.isPresent();\n        IP4Wildcard = allIp4s.map(d -> Pattern.compile(\"\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+\"+d.substring(7)));\n        Optional<String> allIp6s = domains.stream().filter(d -> d.startsWith(\"[::]\")).findAny();\n        this.allowAnyIp6 = allIp6s.isPresent();\n        this.ipv6PortSuffixSize = allIp6s.map(d -> d.substring(4).length()).orElse(0);\n        this.domains = allowAnyIp4 ?\n                Stream.concat(domains.stream(), Stream.of(\"localhost\" + allIp4s.get().substring(7))).collect(Collectors.toList()) :\n                domains;\n    }\n\n    private boolean isIPv6(String ip) {\n        try {\n            if (! ip.substring(0, ip.length() - ipv6PortSuffixSize).contains(\":\")) // try to avoid DNS lookups\n                return false;\n            return Inet6Address.getByName(ip) instanceof Inet6Address;\n        } catch (UnknownHostException e) {\n            return false;\n        }\n    }\n\n    @Override\n    public void handle(HttpExchange exchange) throws IOException {\n        List<String> hostHeaders = exchange.getRequestHeaders().get(\"Host\");\n        if (hostHeaders.isEmpty() || (hostHeaders.size() == 1 &&\n                (domains.contains(hostHeaders.get(0))\n                        || (allowAnyIp4 && IP4Wildcard.get().matcher(hostHeaders.get(0)).matches())\n                        || (allowAnyIp6 && isIPv6(hostHeaders.get(0)))\n                ))) {\n            handler.handle(exchange);\n        } else if (allowSubdomains && hostHeaders.size() == 1 &&\n                domains.stream().anyMatch(d -> hostHeaders.get(0).endsWith(d))) {\n            handler.handle(exchange);\n        } else {\n            LOG.severe(\"Subdomain access blocked: \" + hostHeaders + \" not in \" + String.join(\",\", domains));\n            exchange.sendResponseHeaders(404, 0);\n            exchange.close();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/net/SyncConfigHandler.java",
    "content": "package peergos.server.net;\n\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport peergos.server.HostDirChooser;\nimport peergos.server.HostDirEnumerator;\nimport peergos.server.sync.DirectorySync;\nimport peergos.server.sync.SyncConfig;\nimport peergos.server.sync.SyncRunner;\nimport peergos.server.util.Args;\nimport peergos.server.util.HttpUtil;\nimport peergos.server.util.Logging;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.user.MutableTreeImpl;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.WriteSynchronizer;\nimport peergos.shared.util.Constants;\nimport peergos.shared.util.Either;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.Serialize;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileSystemException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.ForkJoinPool;\nimport java.util.function.Function;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class SyncConfigHandler implements HttpHandler {\n\tprivate static final Logger LOG = Logging.LOG();\n    public static final String OLD_SYNC_CONFIG_FILENAME = \"sync-config\";\n    public static final String SYNC_CONFIG_FILENAME = \"sync-config.json\";\n\n    private static final boolean LOGGING = true;\n    private final SyncConfig args;\n    private final Path peergosDir;\n    private final SyncRunner syncer;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private final Either<HostDirEnumerator, HostDirChooser> hostPaths;\n\n    public SyncConfigHandler(SyncConfig a,\n                             Path peergosDir,\n                             SyncRunner syncer,\n                             ContentAddressedStorage storage,\n                             MutablePointers mutable,\n                             Either<HostDirEnumerator, HostDirChooser> hostPaths,\n                             CoreNode core,\n                             Crypto crypto) {\n        this.args = a;\n        this.peergosDir = peergosDir;\n        this.syncer = syncer;\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutable, storage, crypto.hasher);\n        MutableTreeImpl tree = new MutableTreeImpl(mutable, storage, crypto.hasher, synchronizer);\n        this.network = new NetworkAccess(core, null, null, storage, null, Optional.empty(),\n                mutable, tree, synchronizer, null, null, null, crypto.hasher,\n                Collections.emptyList(), false);\n        this.crypto = crypto;\n        this.hostPaths = hostPaths;\n        saveConfigToFile(a);\n    }\n\n    private synchronized void saveConfigToFile(SyncConfig config) {\n        byte[] bytes = org.peergos.util.JSONParser.toString(config.toJson()).getBytes(StandardCharsets.UTF_8);\n        try {\n            Files.write(peergosDir.resolve(SYNC_CONFIG_FILENAME), bytes);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized SyncConfig getUpdatedArgs() {\n        try {\n            String json = new String(Files.readAllBytes(peergosDir.resolve(SYNC_CONFIG_FILENAME)));\n            return SyncConfig.fromJson((Map<String, Object>) org.peergos.util.JSONParser.parse(json));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void updateRemotePaths(SyncConfig updated) {\n        List<String> links = updated.links;\n        List<String> remotePaths = links.stream()\n                .map(this::getRemotePath)\n                .collect(Collectors.toList());\n        saveConfigToFile(new SyncConfig(updated.localDirs, remotePaths, links, updated.syncLocalDeletes, updated.syncRemoteDeletes,\n                updated.maxDownloadParallelism, updated.minFreeSpacePercent));\n    }\n\n    public void start() {\n        if (! getUpdatedArgs().links.isEmpty())\n            syncer.start();\n    }\n\n    private String getRemotePath(String link) {\n        return UserContext.fromSecretLinksV2(Arrays.asList(link),\n                        Arrays.asList(() -> Futures.of(\"\")), network, crypto)\n                .join()\n                .getEntryPath()\n                .join();\n    }\n\n    @Override\n    public void handle(HttpExchange exchange) {\n        long t1 = System.currentTimeMillis();\n        String path = exchange.getRequestURI().getPath();\n        try {\n            if (! HttpUtil.allowedQuery(exchange, false)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n            String host = exchange.getRequestHeaders().get(\"Host\").get(0);\n            if (! host.startsWith(\"localhost:\")) {\n                exchange.sendResponseHeaders(404, 0);\n                exchange.close();\n                return;\n            }\n            if (path.startsWith(\"/\"))\n                path = path.substring(1);\n            String action = path.substring(Constants.SYNC.length());\n            Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n            Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n\n            if (action.equals(\"add-pair\")) {\n                Map<String, Object> json = (Map<String, Object>) JSONParser.parse(new String(Serialize.readFully(exchange.getRequestBody())));\n                String link = (String) json.get(\"link\");\n                String rawLocalDir = (String) json.get(\"dir\");\n                String localDir = isWindows() ? rawLocalDir.replaceAll(\"\\\\\\\\\\\\\\\\\", \"\\\\\\\\\") : rawLocalDir;\n                Boolean newSyncLocalDeletes = (Boolean) json.get(\"syncLocalDeletes\");\n                Boolean newSyncRemoteDeletes = (Boolean) json.get(\"syncRemoteDeletes\");\n                SyncConfig updated = getUpdatedArgs();\n                List<String> links = updated.links;\n                List<String> localDirs = updated.localDirs;\n                List<String> remotePaths = updated.remotePaths;\n                List<Boolean> syncLocalDeletes = updated.syncLocalDeletes;\n                List<Boolean> syncRemoteDeletes = updated.syncRemoteDeletes;\n                int existing = links.indexOf(link);\n                if (existing != -1 && existing == localDirs.indexOf(localDir)) {\n                    exchange.sendResponseHeaders(200, 0);\n                    exchange.close();\n                } else {\n                    links.add(link);\n                    localDirs.add(localDir);\n                    remotePaths.add(getRemotePath(link));\n                    syncLocalDeletes.add(newSyncLocalDeletes);\n                    syncRemoteDeletes.add(newSyncRemoteDeletes);\n                    saveConfigToFile(new SyncConfig(localDirs, remotePaths, links, syncLocalDeletes, syncRemoteDeletes,\n                            updated.maxDownloadParallelism, updated.minFreeSpacePercent));\n                    // run sync client now\n                    syncer.start();\n                    System.out.println(\"Syncing \" + localDir + \" syncLocalDeletes: \" + newSyncLocalDeletes + \", syncRemoteDeletes: \" + newSyncRemoteDeletes);\n                    exchange.sendResponseHeaders(200, 0);\n                    exchange.close();\n                }\n            } else if (action.equals(\"remove-pair\")) {\n                long label = Long.parseLong(last.apply(\"label\"));\n                int toRemove = 0;\n                SyncConfig updated = getUpdatedArgs();\n                List<String> links = updated.links;\n                for (;toRemove < links.size(); toRemove++) {\n                    String link = links.get(toRemove);\n                    if (link.substring(link.lastIndexOf(\"/\", link.indexOf(\"#\")) + 1, link.indexOf(\"#\")).equals(Long.toString(label)))\n                        break;\n                }\n                if (toRemove == links.size())\n                    throw new IllegalArgumentException(\"Unknown label\");\n                String link = links.remove(toRemove);\n                List<String> localDirs = updated.localDirs;\n                String removedLocal = localDirs.remove(toRemove);\n                List<String> remotePaths = updated.remotePaths;\n                remotePaths.remove(toRemove);\n                List<Boolean> syncLocalDeletes = updated.syncLocalDeletes;\n                syncLocalDeletes.remove(toRemove);\n                List<Boolean> syncRemoteDeletes = updated.syncRemoteDeletes;\n                syncRemoteDeletes.remove(toRemove);\n\n                saveConfigToFile(new SyncConfig(localDirs, remotePaths, links, syncLocalDeletes, syncRemoteDeletes,\n                        updated.maxDownloadParallelism, updated.minFreeSpacePercent));\n                // clear sync state db as well\n                String linkPath = UserContext.fromSecretLinksV2(Arrays.asList(link), Arrays.asList(() -> Futures.of(\"\")), network, crypto).join().getEntryPath().join();\n                Path syncDb = DirectorySync.getSyncStateDbPath(peergosDir, linkPath, removedLocal);\n                LOG.info(\"Deleting \" + syncDb);\n                if (Files.exists(syncDb)) {\n                    try {\n                        Files.delete(syncDb);\n                    } catch (FileSystemException e) {\n                        LOG.info(\"Error deleting \" + syncDb);\n                    }\n                }\n                SyncRunner.StatusHolder status = syncer.getStatusHolder();\n                status.setStatus(\"Removed sync of \" + removedLocal);\n                status.cancel();\n                // clear sync state db again if it was recreated by an in progress sync\n                if (Files.exists(syncDb)) {\n                    Files.delete(syncDb);\n                    LOG.info(\"Deleted \" + syncDb);\n                }\n                exchange.sendResponseHeaders(200, 0);\n                exchange.close();\n            } else if (action.equals(\"get-pairs\")) {\n//                PublicKeyHash owner = PublicKeyHash.fromString(params.get(\"owner\").get(0));\n//                TimeLimited.isAllowedTime(ArrayOps.hexToBytes(last.apply(\"sig\")), 30, storage, owner);\n                // TODO filter links by owner\n//                String username = core.getUsername(owner).join();\n\n                SyncConfig updated = getUpdatedArgs();\n                Map<String, Object> json = updated.toJsonWithoutCaps();\n                byte[] res = JSONParser.toString(json).getBytes(StandardCharsets.UTF_8);\n\n                exchange.sendResponseHeaders(200, res.length);\n                OutputStream resp = exchange.getResponseBody();\n                resp.write(res);\n                exchange.close();\n                ForkJoinPool.commonPool().execute(() -> updateRemotePaths(updated));\n            } else if (action.equals(\"use-host-dir-chooser\")) {\n                boolean useHostDirChooser = hostPaths.isB();\n                byte[] res = JSONParser.toString(useHostDirChooser).getBytes(StandardCharsets.UTF_8);\n                exchange.sendResponseHeaders(200, res.length);\n                OutputStream resp = exchange.getResponseBody();\n                resp.write(res);\n                exchange.close();\n            } else if (action.equals(\"get-host-paths\")) {\n                if (hostPaths.isB())\n                    throw new IllegalStateException(\"Use direct dir chooser\");\n                String prefix = last.apply(\"prefix\");\n                List<String> json = hostPaths.a().getHostDirs(prefix, 2).join();\n                Collections.sort(json);\n                byte[] res = JSONParser.toString(json).getBytes(StandardCharsets.UTF_8);\n                exchange.sendResponseHeaders(200, res.length);\n                OutputStream resp = exchange.getResponseBody();\n                resp.write(res);\n                exchange.close();\n            } else if (action.equals(\"get-host-dir\")) {\n                if (hostPaths.isA())\n                    throw new IllegalStateException(\"Use dir lister\");\n                String rootUri = hostPaths.b().chooseDir().join();\n                Map<String, Object> json = new LinkedHashMap<>();\n                json.put(\"root\", rootUri);\n                byte[] res = JSONParser.toString(json).getBytes(StandardCharsets.UTF_8);\n                exchange.sendResponseHeaders(200, res.length);\n                OutputStream resp = exchange.getResponseBody();\n                resp.write(res);\n                exchange.close();\n            } else if (action.equals(\"sync-now\")) {\n                syncer.runNow();\n                byte[] res = JSONParser.toString(new LinkedHashMap<>()).getBytes(StandardCharsets.UTF_8);\n                exchange.sendResponseHeaders(200, res.length);\n                OutputStream resp = exchange.getResponseBody();\n                resp.write(res);\n                exchange.close();\n            } else if (action.equals(\"status\")) {\n                LinkedHashMap<Object, Object> reply = new LinkedHashMap<>();\n                reply.put(\"msg\", syncer.getStatusHolder().getStatusAndTime());\n                Optional<String> error = syncer.getStatusHolder().getError();\n                error.ifPresent(err -> reply.put(\"error\", err));\n                byte[] res = JSONParser.toString(reply).getBytes(StandardCharsets.UTF_8);\n                exchange.sendResponseHeaders(200, res.length);\n                OutputStream resp = exchange.getResponseBody();\n                resp.write(res);\n                exchange.close();\n            } else {\n                LOG.info(\"Unknown sync config handler: \" + action);\n                exchange.sendResponseHeaders(404, 0);\n                exchange.close();\n            }\n        } catch (Exception e) {\n            LOG.severe(\"Error handling \" +exchange.getRequestURI());\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"Sync Config Handler returned in: \" + (t2 - t1) + \" mS\");\n        }\n    }\n\n    private static boolean isWindows() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"windows\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/simulation/AccessControl.java",
    "content": "package peergos.server.simulation;\n\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic interface AccessControl {\n\n\n\n    List<String> get(Path path, FileSystem.Permission permission);\n\n    void add(Path path, String reader, FileSystem.Permission permission);\n\n    void remove(Path path, String reader, FileSystem.Permission permission);\n\n    void remove(Path path);\n\n    Path getRandomSharedPath(Random random, FileSystem.Permission permission, String sharee);\n    /**\n     * Model owners, reader and writers\n     */\n    class AccessControlUnit {\n        private Map<Path, List<String>> allowed = new HashMap<>();\n        private Map<String, Set<Path>> allowedReversed = new HashMap<>();\n\n        List<String> getAllowed(Path path) {\n            return allowed.computeIfAbsent(path, p -> new ArrayList<>());\n        }\n\n        void addAllowed(Path path, String user) {\n            getAllowed(path).add(user);\n            allowedReversed.computeIfAbsent(user, s -> new HashSet<>()).add(path);\n        }\n\n        void removeAllowed(Path path, String user) {\n            getAllowed(path).remove(user);\n            allowedReversed.get(user).remove(path);\n        }\n\n        void remove(Path p) {\n            allowed.remove(p);\n            allowedReversed.forEach((k,v)  -> v.remove(p));\n        }\n\n    }\n\n    static String getOwner(Path path) {\n        /**\n         * Should be \"/owner/...\"\n         */\n        try {\n            return path.getName(0).toString();\n        } catch (Throwable t) {\n            throw t;\n        }\n    }\n\n    default boolean can(Path path, String user, FileSystem.Permission permission) {\n\n        boolean isOwner = getOwner(path).equals(user);\n        if (isOwner)\n            return true;\n        //check for sharing of the path and all of it's parents\n        Path sharePath = path;\n        while (sharePath != null) {\n            boolean isShared = get(sharePath, permission).contains(user);\n            if (isShared)\n                return true;\n            sharePath = sharePath.getParent();\n        }\n        return false;\n    }\n\n    class MemoryImpl implements AccessControl {\n        public AccessControlUnit readers = new AccessControlUnit();\n        public AccessControlUnit writers = new AccessControlUnit();\n\n        private AccessControlUnit getAcu(FileSystem.Permission permission) {\n            switch (permission) {\n                case READ:\n                    return readers;\n                case WRITE:\n                    return writers;\n                default:\n                    throw new IllegalStateException(\"Unimplemented\");\n            }\n        }\n\n        @Override\n        public List<String> get(Path path, FileSystem.Permission permission) {\n            switch (permission) {\n                case WRITE:\n                    return writers.getAllowed(path);\n                case READ:\n                    return readers.getAllowed(path);\n                default:\n                    throw new IllegalStateException(\"Unimplemented\");\n            }\n        }\n\n        @Override\n        public void add(Path path, String reader, FileSystem.Permission permission) {\n            getAcu(permission).addAllowed(path, reader);\n        }\n\n        @Override\n        public void remove(Path path, String reader, FileSystem.Permission permission) {\n            getAcu(permission).removeAllowed(path, reader);\n        }\n\n        @Override\n        public Path getRandomSharedPath(Random random, FileSystem.Permission permission, String sharee) {\n            AccessControlUnit acu = getAcu(permission);\n            Set<Path> sharedPaths  = acu.allowedReversed.get(sharee);\n            if (sharedPaths ==  null || sharedPaths.isEmpty()) {\n                throw new IllegalStateException();\n            }\n            int nSkip = random.nextInt(sharedPaths.size());\n            return sharedPaths.stream()\n                    .skip(nSkip)\n                    .findFirst()\n                    .orElseThrow(() -> new IllegalStateException());\n        }\n\n        @Override\n        public void remove(Path path) {\n            getAcu(FileSystem.Permission.READ).remove(path);\n            getAcu(FileSystem.Permission.WRITE).remove(path);\n\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/simulation/FileAsyncReader.java",
    "content": "package peergos.server.simulation;\n\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.concurrent.*;\n\npublic class FileAsyncReader implements AsyncReader {\n\n    private final RandomAccessFile file;\n\n    public FileAsyncReader(File f) {\n        try {\n            this.file = new RandomAccessFile(f, \"r\");\n        } catch (FileNotFoundException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<AsyncReader> seek(long offset) {\n        try {\n            file.seek(offset);\n            return Futures.of(this);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length) {\n        try {\n            return Futures.of(file.read(res, offset, length));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<AsyncReader> reset() {\n        try {\n            file.seek(0);\n            return Futures.of(this);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void close() {\n        try {\n            file.close();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/simulation/FileSystem.java",
    "content": "package peergos.server.simulation;\n\n\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\n\nimport java.io.FileNotFoundException;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Random;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic interface FileSystem {\n/**\n * Implement the same File System model as the crypt-tree\n * on the native file-system, for testing/fuzzing.\n */\n    public enum Permission {\n        READ, WRITE\n    }\n\n    /**\n     * All operations are done as this user\n     * @return\n     */\n    String user();\n\n    byte[] read(Path path, BiConsumer<Long, Long> progressConsumer);\n\n    AsyncReader reader(Path path) throws FileNotFoundException;\n\n    default byte[] read(Path path) {\n        return read(path, (a,b) -> {});\n    }\n\n    void write(Path path, AsyncReader data, long size, Consumer<Long> progressConsumer, boolean resumUpload);\n\n    void writeSubtree(Path path, Stream<FileWrapper.FolderUploadProperties> folders, Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile);\n\n    void modify(Path path, byte[] data, Consumer<Long> progressConsumer);\n\n    default void write(Path path, byte[] data) {\n        write(path, AsyncReader.build(data), data.length, l -> {}, false);\n    }\n\n    default void modify(Path path, byte[] data) {\n        modify(path, data, l -> {});\n    }\n    void delete(Path path);\n\n    void grant(Path path, String user, Permission permission);\n\n    void revoke(Path path, String user, Permission permission);\n\n    Stat stat(Path path);\n\n    void mkdir(Path path);\n\n    List<Path> ls(Path path, boolean showHidden);\n\n    default List<Path> ls(Path path) {\n        return ls(path, true);\n    }\n\n    default void walk(Path path, Consumer<Path> func)  {\n        FileProperties fileProperties = stat(path).fileProperties();\n\n        // skip hidden\n        if (fileProperties.isHidden)\n            return;\n\n        //DFS\n        if (fileProperties.isDirectory) {\n            List<Path> ls = ls(path);\n            for (Path child : ls) {\n                walk(child, func);\n            }\n        }\n\n        func.accept(path);\n    }\n\n    default void walk(Consumer<Path> func)  {\n        walk(PathUtil.get(\"/\"+  user()), func);\n    }\n\n    /**\n     *\n     * @param other user to follow\n     * @param reciprocate uni-directional following when  true, bi-directional when false\n     */\n    void follow(FileSystem other, boolean reciprocate);\n\n    Path getRandomSharedPath(Random random, FileSystem.Permission permission, String sharee);\n\n    List<String> getSharees(Path path, Permission permission);\n}\n\n"
  },
  {
    "path": "src/peergos/server/simulation/InputStreamAsyncReader.java",
    "content": "package peergos.server.simulation;\n\nimport peergos.shared.user.fs.AsyncReader;\nimport peergos.shared.util.Futures;\n\nimport java.io.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class InputStreamAsyncReader implements AsyncReader {\n\n    private final InputStream is;\n\n    public InputStreamAsyncReader(InputStream is) {\n        this.is = is;\n    }\n\n    @Override\n    public CompletableFuture<AsyncReader> seek(long offset) {\n        throw new IllegalStateException(\"Operation not supported!\");\n    }\n\n    @Override\n    public CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length) {\n        try {\n            int read = is.read(res, offset, length);\n            if (read < 0)\n                throw new EOFException();\n            offset += read;\n            length -= read;\n            while (read >= 0 && length > 0) {\n                read = is.read(res, offset, length);\n                if (read > 0) {\n                    offset += read;\n                    length -= read;\n                } else\n                    throw new EOFException();\n            }\n            return Futures.of(length);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<AsyncReader> reset() {\n        try {\n            is.reset();\n            return Futures.of(this);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void close() {\n        try {\n            is.close();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/simulation/PeergosFileSystemImpl.java",
    "content": "package peergos.server.simulation;\n\nimport peergos.shared.social.FollowRequestWithCipherText;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\n\nimport java.io.FileNotFoundException;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class PeergosFileSystemImpl implements FileSystem {\n\n    private final UserContext userContext;\n\n    public PeergosFileSystemImpl(UserContext userContext) {\n        this.userContext = userContext;\n    }\n\n    @Override\n    public String user() {\n        return userContext.username;\n    }\n\n    private FileWrapper getPath(Path path) {\n        Optional<FileWrapper> res = userContext.getByPath(path).join();\n        if (res.isEmpty()) {\n            userContext.getByPath(path).join();\n            throw new IllegalStateException(\"Unable to retrieve file at \" + path);\n        }\n        return res.get();\n    }\n\n    private FileWrapper getParent(Path path) {\n        return getPath(path.getParent());\n    }\n\n    @Override\n    public byte[] read(Path path, BiConsumer<Long, Long> progressConsumer) {\n        FileWrapper wrapper = getPath(path);\n        long size = wrapper.getFileProperties().size;\n        ProgressConsumer<Long> monitor = (readBytes) -> progressConsumer.accept(readBytes, size);\n        AsyncReader in = wrapper.getInputStream(userContext.network, userContext.crypto, size, 1, monitor).join();\n        return Serialize.readFully(in, size).join();\n    }\n\n    @Override\n    public AsyncReader reader(Path path) throws FileNotFoundException {\n        FileWrapper file = getPath(path);\n        return file.getInputStream(userContext.network, userContext.crypto, x -> {}).join();\n    }\n\n    @Override\n    public void write(Path path, AsyncReader data, long size, Consumer<Long> progressConsumer, boolean resumeUpload) {\n        FileWrapper directory = getParent(path);\n        String fileName = path.getFileName().toString();\n        ProgressConsumer<Long> pc  = l -> progressConsumer.accept(l);\n        FileWrapper fileWrapper = directory.uploadFileJS(fileName, data, (int) (size >> 32), (int) size,\n                true, userContext.mirrorBatId(), userContext.network, userContext.crypto, pc, userContext.getTransactionService(), f -> Futures.of(resumeUpload)).join();\n    }\n\n    @Override\n    public void writeSubtree(Path path, Stream<FileWrapper.FolderUploadProperties> folders, Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile) {\n        FileWrapper parentDir = getPath(path);\n        parentDir.uploadSubtree(folders, userContext.mirrorBatId(), userContext.network, userContext.crypto, userContext.getTransactionService(), resumeFile, f -> Futures.of(true), () -> true).join();\n    }\n\n    @Override\n    public void modify(Path path, byte[] data, Consumer<Long> progressConsumer) {\n        FileWrapper file = getPath(path);\n        file.overwriteFileJS(AsyncReader.build(data), 0, data.length,\n                userContext.network, userContext.crypto, l -> progressConsumer.accept(l)).join();\n    }\n\n    @Override\n    public void delete(Path path) {\n        FileWrapper directory = getParent(path);\n        FileWrapper updatedParent = getPath(path).remove(directory, path, userContext).join();\n    }\n\n    @Override\n    public List<Path> ls(Path path, boolean showHidden) {\n        return getPath(path).getChildren(userContext.crypto.hasher, userContext.network)\n                .join()\n                .stream()\n                .filter(e -> showHidden || ! e.getFileProperties().isHidden)\n                .map(e -> path.resolve(e.getName()))\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public void grant(Path path, String user, Permission permission) {\n        Set<String> userSet = Stream.of(user).collect(Collectors.toSet());\n        switch (permission) {\n            case READ:\n                userContext.shareReadAccessWith(path, userSet).join();\n                return;\n            case WRITE:\n                userContext.shareWriteAccessWith(path, userSet).join();\n                return;\n        }\n        throw new IllegalStateException();\n    }\n\n    @Override\n    public void revoke(Path path, String user, Permission permission) {\n\n        switch (permission) {\n            case READ:\n                userContext.unShareReadAccess(path, user).join();\n                return;\n            case WRITE:\n                userContext.unShareWriteAccess(path, user).join();\n                return;\n        }\n        throw new IllegalStateException();\n    }\n\n    @Override\n    public Stat stat(Path path) {\n        FileWrapper fileWrapper = getPath(path);\n        return new Stat() {\n            @Override\n            public boolean isReadable() {\n                return fileWrapper.isReadable();\n            }\n\n            @Override\n            public boolean isWritable() {\n                return fileWrapper.isWritable();\n            }\n\n            @Override\n            public String user() {\n                return fileWrapper.getOwnerName();\n            }\n\n            @Override\n            public FileProperties fileProperties() {\n                return fileWrapper.getFileProperties();\n            }\n        };\n\n    }\n\n    @Override\n    public void mkdir(Path path) {\n        getParent(path).mkdir(path.getFileName().toString(),\n                userContext.network,\n                false,\n                userContext.mirrorBatId(),\n                userContext.crypto).join();\n    }\n\n    @Override\n    public void follow(FileSystem other, boolean reciprocate) {\n        UserContext otherContext = ((PeergosFileSystemImpl) other).userContext;\n\n        this.userContext.sendInitialFollowRequest(other.user()).join();\n        List<FollowRequestWithCipherText> join = otherContext.processFollowRequests().join();\n        FollowRequestWithCipherText req = join.stream()\n                .filter(e -> e.getEntry().ownerName.equals(user()))\n                .findFirst()\n                .orElseThrow(() -> new IllegalStateException());\n        otherContext.sendReplyFollowRequest(req, true, reciprocate).join();\n        this.userContext.processFollowRequests().join();\n    }\n\n    @Override\n    public Path getRandomSharedPath(Random random, Permission permission, String sharee) {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public List<String> getSharees(Path path, Permission permission) {\n        FileSharedWithState sharing = userContext.sharedWith(path).join();\n        switch (permission) {\n            case READ:\n                return new ArrayList<>(sharing.readAccess);\n            case WRITE:\n                return new ArrayList<>(sharing.writeAccess);\n            default:\n                throw new IllegalStateException();\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/peergos/server/simulation/Stat.java",
    "content": "package peergos.server.simulation;\n\nimport peergos.shared.user.fs.FileProperties;\n\npublic interface Stat {\n    String user();\n    FileProperties fileProperties();\n    boolean isReadable();\n    boolean isWritable();\n}"
  },
  {
    "path": "src/peergos/server/social/NonWriteThroughSocialNetwork.java",
    "content": "package peergos.server.social;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class NonWriteThroughSocialNetwork implements SocialNetwork {\n\n    private final SocialNetwork source;\n    private final ContentAddressedStorage ipfs;\n\n    public NonWriteThroughSocialNetwork(SocialNetwork source, ContentAddressedStorage ipfs) {\n        this.source = source;\n        this.ipfs = ipfs;\n    }\n\n    private final Map<PublicKeyHash, List<ByteArrayWrapper>> removedFollowRequests = new ConcurrentHashMap<>();\n    private final Map<PublicKeyHash, List<ByteArrayWrapper>> newFollowRequests = new ConcurrentHashMap<>();\n\n    @Override\n    public CompletableFuture<Boolean> sendFollowRequest(PublicKeyHash target, byte[] encryptedPermission) {\n        newFollowRequests.putIfAbsent(target, new ArrayList<>());\n        ByteArrayWrapper wrappped = new ByteArrayWrapper(encryptedPermission);\n        newFollowRequests.get(target).add(wrappped);\n        removedFollowRequests.putIfAbsent(target, new ArrayList<>());\n        removedFollowRequests.get(target).remove(wrappped);\n        return CompletableFuture.completedFuture(true);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> getFollowRequests(PublicKeyHash owner, byte[] signedTime) {\n        try {\n            byte[] reqs = source.getFollowRequests(owner, signedTime).join();\n            CborObject cbor = CborObject.fromByteArray(reqs);\n            List<byte[]> notDeleted = new ArrayList<>();\n            List<ByteArrayWrapper> removed = removedFollowRequests.getOrDefault(owner, Collections.emptyList());\n            CborObject.CborList list = (CborObject.CborList) cbor;\n            for (Cborable reqCbor: list.value) {\n                byte[] req = reqCbor.serialize();\n                ByteArrayWrapper wrapped = new ByteArrayWrapper(req);\n                if (! removed.contains(wrapped))\n                    notDeleted.add(req);\n            }\n            notDeleted.addAll(newFollowRequests.getOrDefault(owner, Collections.emptyList()).stream()\n                    .map(w -> w.data)\n                    .collect(Collectors.toList()));\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutput dout = new DataOutputStream(bout);\n            dout.writeInt(notDeleted.size());\n            for (byte[] req : notDeleted) {\n                Serialize.serialize(req, dout);\n            }\n            return CompletableFuture.completedFuture(bout.toByteArray());\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Boolean> removeFollowRequest(PublicKeyHash owner, byte[] signedEncryptedPermission) {\n        try {\n            PublicSigningKey signer = ipfs.getSigningKey(owner, owner).join().get();\n            byte[] unsigned = signer.unsignMessage(signedEncryptedPermission).join();\n\n            newFollowRequests.putIfAbsent(owner, new ArrayList<>());\n            ByteArrayWrapper wrappped = new ByteArrayWrapper(unsigned);\n            newFollowRequests.get(owner).remove(wrappped);\n            removedFollowRequests.putIfAbsent(owner, new ArrayList<>());\n            removedFollowRequests.get(owner).add(wrappped);\n            return CompletableFuture.completedFuture(true);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/space/JdbcQuotas.java",
    "content": "package peergos.server.space;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class JdbcQuotas {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final String QUOTA_USER_NAME = \"name\";\n    private static final String QUOTA_SIZE = \"quota\";\n    private static final String SET_QUOTA = \"UPDATE freequotas SET quota = ? WHERE name = ?;\";\n    private static final String GET_QUOTA = \"SELECT quota FROM freequotas WHERE name = ?;\";\n    private static final String GET_ALL_QUOTAS = \"SELECT name, quota FROM freequotas;\";\n    private static final String COUNT_USERS = \"SELECT COUNT (name) FROM freequotas;\";\n    private static final String REMOVE_USER = \"DELETE FROM freequotas WHERE name = ?;\";\n    private static final String HAS_TOKEN = \"SELECT * FROM signuptokens WHERE token = ?;\";\n    private static final String REMOVE_TOKEN = \"DELETE FROM signuptokens WHERE token = ?;\";\n    private static final String ADD_TOKEN = \"INSERT INTO signuptokens (token) VALUES(?);\";\n    private static final String LIST_TOKENS = \"SELECT token from signuptokens;\";\n\n    private final SqlSupplier commands;\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n\n    public JdbcQuotas(Supplier<Connection> conn, SqlSupplier commands) {\n        this.commands = commands;\n        this.conn = conn;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createQuotasTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public boolean setQuota(String username, long quota) {\n        try (Connection conn = getConnection();\n             PreparedStatement createuser = conn.prepareStatement(commands.insertOrIgnoreCommand(\"INSERT \", \"INTO freequotas (name, quota) VALUES(?, ?)\"));\n             PreparedStatement update = conn.prepareStatement(SET_QUOTA)) {\n            createuser.setString(1, username);\n            createuser.setLong(2, 0);\n            createuser.executeUpdate();\n\n            update.setLong(1, quota);\n            update.setString(2, username);\n            update.executeUpdate();\n            return true;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public long getQuota(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_QUOTA)) {\n            select.setString(1, username);\n            ResultSet rs = select.executeQuery();\n            if (rs.next())\n                return rs.getLong(QUOTA_SIZE);\n\n            throw new IllegalStateException(\"No space quota for \" + username);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public void removeUser(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement delete = conn.prepareStatement(REMOVE_USER)) {\n            delete.setString(1, username);\n            delete.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public Map<String, Long> getQuotas() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_ALL_QUOTAS)) {\n            ResultSet rs = select.executeQuery();\n            Map<String, Long> res = new HashMap<>();\n            while (rs.next()) {\n                String username = rs.getString(QUOTA_USER_NAME);\n                long quota = rs.getLong(QUOTA_SIZE);\n                res.put(username, quota);\n            }\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public boolean hasUser(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_QUOTA)) {\n            select.setString(1, username);\n            ResultSet rs = select.executeQuery();\n            return rs.next();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public boolean hasToken(String token) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(HAS_TOKEN)) {\n            select.setString(1, token);\n            ResultSet res =  select.executeQuery();\n            return res.next();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public boolean removeToken(String token) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(REMOVE_TOKEN)) {\n            select.setString(1, token);\n            int modified = select.executeUpdate();\n            return modified == 1;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public boolean addToken(String token) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(ADD_TOKEN)) {\n            select.setString(1, token);\n            int modified = select.executeUpdate();\n            return modified == 1;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public List<String> listTokens() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(LIST_TOKENS)) {\n            ResultSet res = select.executeQuery();\n            List<String> results = new ArrayList<>();\n            while (res.next())\n                results.add(res.getString(QUOTA_USER_NAME));\n            return results;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public int numberOfUsers() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(COUNT_USERS)) {\n            ResultSet rs = select.executeQuery();\n            rs.next();\n            return rs.getInt(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n        isClosed = true;\n    }\n\n    public static JdbcQuotas build(Supplier<Connection> conn, SqlSupplier commands) {\n        return new JdbcQuotas(conn, commands);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/space/JdbcSpaceRequests.java",
    "content": "package peergos.server.space;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.*;\nimport peergos.shared.storage.*;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class JdbcSpaceRequests {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final String SPACE_REQUEST_USER_NAME = \"name\";\n    private static final String SPACE_REQUEST_DATA_NAME = \"spacerequest\";\n    private static final String INSERT_SPACE_REQUEST = \"INSERT INTO spacerequests (name, spacerequest) VALUES(?, ?);\";\n    private static final String SELECT_SPACE_REQUESTS = \"SELECT name, spacerequest FROM spacerequests;\";\n    private static final String DELETE_SPACE_REQUEST = \"DELETE FROM spacerequests WHERE name = ? AND spacerequest = ?;\";\n\n    private class SpaceRequestData {\n        public final String name;\n        public final byte[] data;\n        public final String b64string;\n\n        SpaceRequestData(String name, byte[] data) {\n            this(name,data,(data == null ? null: new String(Base64.getEncoder().encode(data))));\n        }\n\n        SpaceRequestData(String name, String d) {\n            this(name, Base64.getDecoder().decode(d), d);\n        }\n\n        SpaceRequestData(String name, byte[] data, String b64string) {\n            this.name = name;\n            this.data = data;\n            this.b64string = b64string;\n        }\n\n        public boolean insert() {\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(INSERT_SPACE_REQUEST)) {\n                insert.setString(1,this.name);\n                insert.setString(2,this.b64string);\n                insert.executeUpdate();\n                return true;\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return false;\n            }\n        }\n\n        public SpaceRequestData[] select() {\n            try (Connection conn = getConnection();\n                 PreparedStatement select = conn.prepareStatement(SELECT_SPACE_REQUESTS)) {\n                ResultSet rs = select.executeQuery();\n                List<SpaceRequestData> list = new ArrayList<>();\n                while (rs.next())\n                {\n                    String username = rs.getString(SPACE_REQUEST_USER_NAME);\n                    String b64string = rs.getString(SPACE_REQUEST_DATA_NAME);\n                    list.add(new SpaceRequestData(username, b64string));\n                }\n                return list.toArray(new SpaceRequestData[0]);\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return null;\n            }\n        }\n\n        public boolean delete() {\n            try (Connection conn = getConnection();\n                 PreparedStatement delete = conn.prepareStatement(DELETE_SPACE_REQUEST)) {\n                delete.setString(1, name);\n                delete.setString(2, b64string);\n                delete.executeUpdate();\n                return true;\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                return false;\n            }\n        }\n    }\n\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n\n    public JdbcSpaceRequests(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createSpaceRequestsTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public boolean addSpaceRequest(String username, byte[] signedRequest) {\n        SpaceRequestData request = new SpaceRequestData(username, signedRequest);\n        return request.insert();\n    }\n\n    public boolean removeSpaceRequest(String username, byte[] unsigned) {\n        SpaceRequestData request = new SpaceRequestData(username, unsigned);\n        return request.delete();\n    }\n\n    public List<SpaceUsage.LabelledSignedSpaceRequest> getSpaceRequests() {\n        byte[] dummy = null;\n        SpaceRequestData request = new SpaceRequestData(null, dummy);\n        SpaceRequestData[] requests = request.select();\n        if (requests == null)\n            return Collections.emptyList();\n\n        return Arrays.asList(requests).stream()\n                .map(req -> new SpaceUsage.LabelledSignedSpaceRequest(req.name, req.data))\n                .collect(Collectors.toList());\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n        isClosed = true;\n    }\n\n    public static JdbcSpaceRequests build(Supplier<Connection> conn, SqlSupplier commands) {\n        return new JdbcSpaceRequests(conn, commands);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/space/JdbcUsageStore.java",
    "content": "package peergos.server.space;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.util.*;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class JdbcUsageStore implements UsageStore {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private Supplier<Connection> conn;\n    private final SqlSupplier commands;\n    private volatile boolean isClosed;\n\n    public JdbcUsageStore(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        this.commands = commands;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        return getConnection(true, true);\n    }\n\n    private Connection getConnection(boolean autocommit, boolean serializable) {\n        Connection connection = conn.get();\n        try {\n            if (autocommit)\n                connection.setAutoCommit(true);\n            if (serializable)\n                connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createUsageTablesCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void initialized() {\n        // TODO can we remove this method?\n    }\n\n    public void removeUser(String username) {\n        try (Connection conn = getConnection(true, true);\n             PreparedStatement getUser = conn.prepareStatement(\"SELECT id FROM users WHERE name = ?;\");\n             PreparedStatement deleteUserUsage = conn.prepareStatement(\"DELETE from userusage where user_id=?;\");\n             PreparedStatement deletePendingUsage = conn.prepareStatement(\"DELETE from pendingusage where user_id=?;\");\n             PreparedStatement getWriterIds = conn.prepareStatement(\"SELECT writers.id FROM writers \" +\n                     \"INNER JOIN ownedkeys ON writers.id=ownedkeys.owned_id \" +\n                     \"INNER JOIN writerusage ON ownedkeys.parent_id=writerusage.writer_id \" +\n                     \"INNER JOIN users ON writerusage.user_id=users.id WHERE users.name=?;\");\n             PreparedStatement deleteOwnedKeys = conn.prepareStatement(\"DELETE from ownedkeys WHERE owned_id=?;\");\n             PreparedStatement deleteWriters = conn.prepareStatement(\"DELETE FROM writers WHERE id=?;\");\n             PreparedStatement deleteWriterUsage = conn.prepareStatement(\"DELETE from writerusage where user_id=?;\");\n             PreparedStatement deleteUser = conn.prepareStatement(\"DELETE from users WHERE id=?;\")\n        ) {\n            getUser.setString(1, username);\n            ResultSet uidRes = getUser.executeQuery();\n            if (!uidRes.next())\n                return;\n            long uid = uidRes.getLong(1);\n\n            getWriterIds.setString(1, username);\n            Set<Long> ownedIds = new HashSet<>();\n            ResultSet resultSet = getWriterIds.executeQuery();\n            while (resultSet.next())\n                ownedIds.add(resultSet.getLong(1));\n\n            deleteUserUsage.setLong(1, uid);\n            deleteUserUsage.executeUpdate();\n\n            deletePendingUsage.setLong(1, uid);\n            deletePendingUsage.executeUpdate();\n\n            for (long ownedId : ownedIds) {\n                deleteOwnedKeys.setLong(1, ownedId);\n                deleteOwnedKeys.executeUpdate();\n            }\n\n            deleteWriterUsage.setLong(1, uid);\n            deleteWriterUsage.executeUpdate();\n\n            for (long ownedId : ownedIds) {\n                deleteWriters.setLong(1, ownedId);\n                deleteWriters.executeUpdate();\n            }\n\n            deleteUser.setLong(1, uid);\n            deleteUser.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void addUserIfAbsent(String username) {\n        try (Connection conn = getConnection(true, false);\n             PreparedStatement userInsert = conn.prepareStatement(commands.insertOrIgnoreCommand(\"INSERT \", \"INTO users (name) VALUES(?)\"));\n             PreparedStatement select = conn.prepareStatement(\"SELECT id FROM users WHERE name = ?;\");\n             PreparedStatement usageInsert = conn.prepareStatement(commands.insertOrIgnoreCommand(\"INSERT \", \"INTO userusage (user_id, total_bytes, errored) VALUES(?, ?, ?)\"))) {\n            userInsert.setString(1, username);\n            userInsert.executeUpdate();\n\n            select.setString(1, username);\n            ResultSet resultSet = select.executeQuery();\n            resultSet.next();\n            int userId = resultSet.getInt(1);\n\n            usageInsert.setInt(1, userId);\n            usageInsert.setLong(2, 0);\n            usageInsert.setBoolean(3, false);\n            usageInsert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void confirmUsage(String username, PublicKeyHash writer, long usageDelta, boolean errored) {\n        try (Connection conn = getConnection(true, false);\n             PreparedStatement userSelect = conn.prepareStatement(\"SELECT id FROM users WHERE name = ?;\");\n             PreparedStatement insert = conn.prepareStatement(\n                     \"UPDATE userusage SET total_bytes = total_bytes + ?, errored = ? \" +\n                             \"WHERE user_id = ?;\");\n             PreparedStatement insertPending = conn.prepareStatement(\n                     \"UPDATE pendingusage SET pending_bytes = ? \" +\n                             \"WHERE writer_id = (SELECT id FROM writers WHERE key_hash = ?);\")) {\n            userSelect.setString(1, username);\n            ResultSet userRes = userSelect.executeQuery();\n            userRes.next();\n            int userId = userRes.getInt(1);\n\n            insert.setLong(1, usageDelta);\n            insert.setBoolean(2, errored);\n            insert.setInt(3, userId);\n            int count = insert.executeUpdate();\n            if (count != 1)\n                throw new IllegalStateException(\"Didn't update one record!\");\n            insertPending.setLong(1, 0);\n            insertPending.setBytes(2, writer.toBytes());\n            int count2 = insertPending.executeUpdate();\n            if (count2 != 1)\n                throw new IllegalStateException(\"Didn't update one record!\");\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage() + \": \" + username + \".\" + writer + \"(\" + usageDelta + \")\", sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void addPendingUsage(String username, PublicKeyHash writer, int size) {\n        try (Connection conn = getConnection(true, false);\n             PreparedStatement insert = conn.prepareStatement(\"UPDATE pendingusage SET pending_bytes = pending_bytes + ? \" +\n                     \"WHERE writer_id = ?;\")) {\n            int writerId = getWriterId(writer, conn);\n            insert.setLong(1, size);\n            insert.setInt(2, writerId);\n            int count = insert.executeUpdate();\n            if (count != 1)\n                throw new IllegalStateException(\"Didn't update one record!\");\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage() + \" Username: \" + username, sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void resetPendingUsage(String username, PublicKeyHash writer) {\n        try (Connection conn = getConnection(true, false);\n             PreparedStatement insert = conn.prepareStatement(\"UPDATE pendingusage SET pending_bytes = ? \" +\n                     \"WHERE writer_id = ?;\")) {\n            int writerId = getWriterId(writer, conn);\n            insert.setLong(1, 0);\n            insert.setInt(2, writerId);\n            int count = insert.executeUpdate();\n            if (count != 1)\n                throw new IllegalStateException(\"Didn't update one record!\");\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage() + \" Username: \" + username, sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public Map<String, Long> getAllUsage() {\n        try (Connection conn = getConnection();\n             PreparedStatement search = conn.prepareStatement(\"SELECT u.name, uu.total_bytes \" +\n                     \"FROM users u, userusage uu WHERE u.id = uu.user_id;\")) {\n            Map<String, Long> usage = new HashMap<>();\n            ResultSet resultSet = search.executeQuery();\n            while (resultSet.next()) {\n                String username = resultSet.getString(1);\n                long total = resultSet.getLong(2);\n                usage.put(username, total);\n            }\n            return usage;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public UserUsage getUsage(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement totalSearch = conn.prepareStatement(\n                     \"SELECT uu.total_bytes, uu.errored FROM userusage uu \" +\n                     \"INNER JOIN users u ON uu.user_id = u.id WHERE u.name = ?;\");\n             PreparedStatement pendingSearch = conn.prepareStatement(\n                     \"SELECT w.key_hash, pu.pending_bytes FROM pendingusage pu \" +\n                     \"INNER JOIN writers w ON pu.writer_id = w.id \" +\n                     \"INNER JOIN users u ON pu.user_id = u.id WHERE u.name = ?;\")) {\n            totalSearch.setString(1, username);\n            ResultSet totalRes = totalSearch.executeQuery();\n            if (! totalRes.next())\n                return new UserUsage(0, false, new HashMap<>());\n            long totalBytes = totalRes.getLong(1);\n            boolean errored = totalRes.getBoolean(2);\n\n            pendingSearch.setString(1, username);\n            ResultSet resultSet = pendingSearch.executeQuery();\n            Map<PublicKeyHash, Long> pending = new HashMap<>();\n            while (resultSet.next()) {\n                PublicKeyHash writer = PublicKeyHash.decode(resultSet.getBytes(1));\n                long pendingBytes = resultSet.getLong(2);\n                if (pendingBytes > 0)\n                    pending.put(writer, pendingBytes);\n            }\n            return new UserUsage(totalBytes, errored, pending);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private int getWriterId(PublicKeyHash writer, Connection conn) {\n        try (PreparedStatement writerSelect = conn.prepareStatement(\"SELECT id FROM writers WHERE key_hash = ?;\")) {\n            writerSelect.setBytes(1, writer.toBytes());\n            ResultSet writerRes = writerSelect.executeQuery();\n            if (writerRes.next())\n                return writerRes.getInt(1);\n            throw new IllegalStateException(\"Writer not present on this server: \" + writer);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void addWriter(String owner, PublicKeyHash writer) {\n        try (Connection conn = getConnection(true, false);\n             PreparedStatement writerInsert = conn.prepareStatement(commands.insertOrIgnoreCommand(\"INSERT \", \"INTO writers (key_hash) VALUES(?)\"));\n             PreparedStatement userSelect = conn.prepareStatement(\"SELECT id FROM users WHERE name = ?;\");\n             PreparedStatement writerSelect = conn.prepareStatement(\"SELECT id FROM writers WHERE key_hash = ?;\");\n             PreparedStatement defaultPendingInsert = conn.prepareStatement(commands.insertOrIgnoreCommand(\n                     \"INSERT \", \"INTO pendingusage (user_id, writer_id, pending_bytes) VALUES(?, ?, ?)\"));\n             PreparedStatement usageInsert = conn.prepareStatement(commands.insertOrIgnoreCommand(\"INSERT \", \"INTO writerusage (writer_id, user_id, direct_size) VALUES(?, ?, ?)\"))) {\n            writerInsert.setBytes(1, writer.toBytes());\n            writerInsert.executeUpdate();\n\n            userSelect.setString(1, owner);\n            ResultSet resultSet = userSelect.executeQuery();\n            resultSet.next();\n            int userId = resultSet.getInt(1);\n\n            writerSelect.setBytes(1, writer.toBytes());\n            ResultSet writerRes = writerSelect.executeQuery();\n            writerRes.next();\n            int writerId = writerRes.getInt(1);\n\n            defaultPendingInsert.setInt(1, userId);\n            defaultPendingInsert.setInt(2, writerId);\n            defaultPendingInsert.setInt(3, 0);\n            defaultPendingInsert.executeUpdate();\n\n            usageInsert.setInt(1, writerId);\n            usageInsert.setInt(2, userId);\n            usageInsert.setInt(3, 0);\n            usageInsert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Set<PublicKeyHash> getAllWriters() {\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(\"SELECT key_hash FROM writers;\")) {\n            Set<PublicKeyHash> res = new HashSet<>();\n            ResultSet resultSet = query.executeQuery();\n            while (resultSet.next())\n                res.add(PublicKeyHash.decode(resultSet.getBytes(1)));\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Set<PublicKeyHash> getAllWriters(PublicKeyHash owner) {\n        try (Connection conn = getConnection();\n             PreparedStatement idQuery = conn.prepareStatement(\"SELECT id FROM writers WHERE key_hash=?;\");\n             PreparedStatement query = conn.prepareStatement(\"SELECT writers.key_hash FROM writers \" +\n                     \"INNER JOIN ownedkeys ON writers.id=ownedkeys.owned_id WHERE ownedkeys.parent_id=?;\")) {\n            idQuery.setBytes(1, owner.toBytes());\n            ResultSet idRes = idQuery.executeQuery();\n            if (!idRes.next())\n                return Collections.emptySet();\n            int id = idRes.getInt(1);\n            query.setInt(1, id);\n\n            Set<PublicKeyHash> res = new HashSet<>();\n            res.add(owner);\n            ResultSet resultSet = query.executeQuery();\n            while (resultSet.next())\n                res.add(PublicKeyHash.decode(resultSet.getBytes(1)));\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Set<PublicKeyHash> getAllWriters(String owner) {\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(\"SELECT writers.key_hash FROM writers \" +\n                     \"INNER JOIN writerusage ON writers.id=writerusage.writer_id \" +\n                     \"INNER JOIN users ON writerusage.user_id=users.id WHERE users.name=?;\")) {\n            query.setString(1, owner);\n\n            Set<PublicKeyHash> res = new HashSet<>();\n            ResultSet resultSet = query.executeQuery();\n            while (resultSet.next())\n                res.add(PublicKeyHash.decode(resultSet.getBytes(1)));\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private PublicKeyHash getOwnerKey(PublicKeyHash writer, Connection conn) {\n        try (PreparedStatement search = conn.prepareStatement(\n                \"SELECT parent_id FROM ownedkeys WHERE owned_id = ?;\")) {\n            PublicKeyHash current = writer;\n            while (true) {\n                int writerId = getWriterId(current, conn);\n                search.setInt(1, writerId);\n                ResultSet resultSet = search.executeQuery();\n                if (!resultSet.next())\n                    return current;\n                current = getWriter(resultSet.getInt(1), conn);\n            }\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public List<Triple<Multihash, String, PublicKeyHash>> getAllTargets() {\n        try (Connection conn = getConnection();\n             PreparedStatement get = conn.prepareStatement(\n                     \"SELECT wu.target, u.name, w.key_hash \" +\n                             \"FROM writerusage wu \" +\n                             \"INNER JOIN users u ON wu.user_id = u.id \" +\n                             \"INNER JOIN writers w ON wu.writer_id = w.id \" +\n                             \"WHERE wu.target IS NOT NULL;\")) {\n            List<Triple<Multihash, String, PublicKeyHash>> res = new ArrayList<>();\n            ResultSet resultSet = get.executeQuery();\n            while (resultSet.next()) {\n                Multihash target = Cid.cast(resultSet.getBytes(1));\n                String username = resultSet.getString(2);\n                PublicKeyHash writer = PublicKeyHash.decode(resultSet.getBytes(3));\n                res.add(new Triple<>(target, username, getOwnerKey(writer, conn)));\n            }\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public List<Triple<Multihash, String, PublicKeyHash>> getAllTargets(String username) {\n        try (Connection conn = getConnection();\n             PreparedStatement get = conn.prepareStatement(\n                     \"SELECT wu.target, w.key_hash \" +\n                             \"FROM writerusage wu \" +\n                             \"INNER JOIN users u ON wu.user_id = u.id \" +\n                             \"INNER JOIN writers w ON wu.writer_id = w.id \" +\n                             \"WHERE u.name = ?;\")) {\n            get.setString(1, username);\n            List<Triple<Multihash, String, PublicKeyHash>> res = new ArrayList<>();\n            ResultSet resultSet = get.executeQuery();\n            while (resultSet.next()) {\n                MaybeMultihash target = Optional.ofNullable(resultSet.getBytes(1))\n                        .map(x -> MaybeMultihash.of(Cid.cast(x)))\n                        .orElse(MaybeMultihash.empty());\n                if (target.isPresent()) {\n                    PublicKeyHash writer = PublicKeyHash.decode(resultSet.getBytes(2));\n                    res.add(new Triple<>(target.get(), username, getOwnerKey(writer, conn)));\n                }\n            }\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public List<Pair<String, PublicKeyHash>> getAllOwners() {\n        try (Connection conn = getConnection();\n             PreparedStatement get = conn.prepareStatement(\"SELECT users.name FROM writerusage \" +\n                     \"INNER JOIN users ON writerusage.user_id=users.id;\")) {\n            List<Pair<String, PublicKeyHash>> res = new ArrayList<>();\n            ResultSet resultSet = get.executeQuery();\n            while (resultSet.next()) {\n                String username = resultSet.getString(1);\n                res.add(new Pair<>(username, getOwnerKey(username, conn)));\n            }\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public PublicKeyHash getOwnerKey(PublicKeyHash writer) {\n        try (Connection conn = getConnection()) {\n            return getOwnerKey(writer, conn);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public PublicKeyHash getOwnerKey(String username) {\n        try (Connection conn = getConnection()){\n            return getOwnerKey(username, conn);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private PublicKeyHash getOwnerKey(String username, Connection conn) {\n        try (PreparedStatement search = conn.prepareStatement(\n                     \"SELECT w.key_hash FROM writerusage wu \" +\n                             \"INNER JOIN users u ON wu.user_id = u.id \" +\n                             \"INNER JOIN writers w ON wu.writer_id = w.id \" +\n                             \"WHERE u.name = ?;\")) {\n            search.setString(1, username);\n            ResultSet resultSet = search.executeQuery();\n            if (!resultSet.next())\n                throw new IllegalStateException(\"Couldn't find writer for user!\");\n            PublicKeyHash writer = PublicKeyHash.decode(resultSet.getBytes(1));\n            return getOwnerKey(writer, conn);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private Map<PublicKeyHash, String> owners = new LRUCache<>(100);\n\n    @Override\n    public String getOwner(PublicKeyHash writer) {\n        String cached = owners.get(writer);\n        if (cached != null)\n            return cached;\n        try (Connection conn = getConnection();\n             PreparedStatement search = conn.prepareStatement(\"SELECT u.name FROM users u, writerusage wu WHERE u.id = wu.user_id AND wu.writer_id = ?;\")) {\n            int writerId = getWriterId(writer, conn);\n            search.setInt(1, writerId);\n            ResultSet resultSet = search.executeQuery();\n            if (! resultSet.next())\n                throw new IllegalStateException(\"Unknown writer on this server!\");\n            String owner = resultSet.getString(1);\n            owners.put(writer, owner);\n            return owner;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private PublicKeyHash getWriter(int writerId, Connection conn) {\n        try (PreparedStatement search = conn.prepareStatement(\"SELECT key_hash FROM writers WHERE id = ?;\")) {\n            search.setInt(1, writerId);\n            ResultSet resultSet = search.executeQuery();\n            resultSet.next();\n            return PublicKeyHash.decode(resultSet.getBytes(1));\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public WriterUsage getUsage(PublicKeyHash writer) {\n        String owner = getOwner(writer);\n        Set<PublicKeyHash> owned = new HashSet<>();\n        try (Connection conn = getConnection();\n             PreparedStatement ownedSearch = conn.prepareStatement(\"SELECT owned_id FROM ownedkeys WHERE parent_id = ?;\");\n             PreparedStatement usageSearch = conn.prepareStatement(\"SELECT target, direct_size FROM writerusage WHERE writer_id = ?;\");\n             PreparedStatement search = conn.prepareStatement(\"SELECT key_hash FROM writers WHERE id = ?;\")) {\n            int writerId = getWriterId(writer, conn);\n            ownedSearch.setInt(1, writerId);\n            ResultSet ownedRes = ownedSearch.executeQuery();\n            while (ownedRes.next()) {\n                search.setInt(1, ownedRes.getInt(1));\n                ResultSet resultSet = search.executeQuery();\n                resultSet.next();\n                PublicKeyHash ownedKey = PublicKeyHash.decode(resultSet.getBytes(1));\n                owned.add(ownedKey);\n            }\n            usageSearch.setInt(1, writerId);\n            ResultSet usageRes = usageSearch.executeQuery();\n            usageRes.next();\n            MaybeMultihash target = Optional.ofNullable(usageRes.getBytes(1))\n                    .map(x -> MaybeMultihash.of(Cid.cast(x)))\n                    .orElse(MaybeMultihash.empty());\n            return new WriterUsage(owner, target, usageRes.getLong(2), owned);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n\n    }\n\n    @Override\n    public void updateWriterUsage(PublicKeyHash writer,\n                                  MaybeMultihash target,\n                                  Set<PublicKeyHash> removedOwnedKeys,\n                                  Set<PublicKeyHash> addedOwnedKeys,\n                                  long retainedStorage) {\n        try (Connection conn = getConnection(true, false);\n             PreparedStatement insert = conn.prepareStatement(\"UPDATE writerusage SET target=?, direct_size=? WHERE writer_id = ?;\");\n             PreparedStatement writerSelect = conn.prepareStatement(\"SELECT id FROM writers WHERE key_hash = ?;\");\n             PreparedStatement deleteOwned = conn.prepareStatement(\"DELETE FROM ownedkeys WHERE owned_id = ?;\");\n             PreparedStatement insertOwned = conn.prepareStatement(\"INSERT INTO ownedkeys (parent_id, owned_id) VALUES(?, ?);\")) {\n            int writerId = getWriterId(writer, conn);\n            insert.setBytes(1, target.isPresent() ? target.get().toBytes() : null);\n            insert.setLong(2, retainedStorage);\n            insert.setInt(3, writerId);\n            int count = insert.executeUpdate();\n            if (count != 1)\n                throw new IllegalStateException(\"Didn't update one record!\");\n\n            for (PublicKeyHash removed : removedOwnedKeys) {\n                writerSelect.setBytes(1, removed.toBytes());\n                ResultSet writerRes = writerSelect.executeQuery();\n                writerRes.next();\n                int ownedId = writerRes.getInt(1);\n                deleteOwned.setInt(1, ownedId);\n                deleteOwned.executeUpdate();\n            }\n\n            for (PublicKeyHash added : addedOwnedKeys) {\n                writerSelect.setBytes(1, added.toBytes());\n                ResultSet writerRes = writerSelect.executeQuery();\n                writerRes.next();\n                int ownedId = writerRes.getInt(1);\n                insertOwned.setInt(1, writerId);\n                insertOwned.setInt(2, ownedId);\n                insertOwned.execute();\n            }\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n        isClosed = true;\n    }\n}"
  },
  {
    "path": "src/peergos/server/space/QuotaCLI.java",
    "content": "package peergos.server.space;\n\nimport peergos.server.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.storage.admin.*;\nimport peergos.shared.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class QuotaCLI extends Builder {\n\n    private static void printQuota(String name, long quota) {\n        System.out.println(name + \" \" + formatQuota(quota));\n    }\n\n    private static String formatQuota(long quota) {\n        long mb = quota / 1024 / 1024;\n        if (mb == 0)\n            return quota + \" B\";\n        if (mb < 1024)\n            return mb + \" MiB\";\n        return mb/1024 + \" GiB\";\n    }\n\n    public static final Command<Boolean> SET = new Command<>(\"set\",\n            \"Set free quota for a user on this server\",\n            a -> {\n                boolean paidStorage = a.hasArg(\"quota-admin-address\");\n                if (paidStorage)\n                    throw new IllegalStateException(\"Quota CLI only valid on non paid instances\");\n                SqlSupplier sqlCommands = getSqlCommands(a);\n                Supplier<Connection> quotasDb = getDBConnector(a, \"quotas-sql-file\");\n                JdbcQuotas quotas = JdbcQuotas.build(quotasDb, sqlCommands);\n\n                String name = a.getArg(\"username\");\n                long quota = UserQuotas.parseQuota(a.getArg(\"quota\"));\n                quotas.setQuota(name, quota);\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"username\", \"The username to set the quota of\", true),\n                    new Command.Arg(\"quota\", \"The quota in bytes or (k, m, g, t)\", true),\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    public static final Command<Boolean> REQUESTS = new Command<>(\"requests\",\n            \"Show pending quota requests on this server\",\n            a -> {\n                boolean paidStorage = a.hasArg(\"quota-admin-address\");\n                if (paidStorage)\n                    throw new IllegalStateException(\"Quota CLI only valid on non paid instances\");\n                SqlSupplier sqlCommands = getSqlCommands(a);\n                Supplier<Connection> quotasDb = getDBConnector(a, \"space-requests-sql-file\");\n                JdbcSpaceRequests reqs = JdbcSpaceRequests.build(quotasDb, sqlCommands);\n                List<QuotaControl.LabelledSignedSpaceRequest> raw = reqs.getSpaceRequests();\n\n                NetworkAccess net = Builder.buildLocalJavaNetworkAccess(a.getInt(\"port\")).join();\n                List<DecodedSpaceRequest> allReqs = DecodedSpaceRequest.decodeSpaceRequests(raw, net.coreNode, net.dhtClient).join();\n                System.out.println(\"Quota requests:\");\n                for (DecodedSpaceRequest req : allReqs) {\n                    long utcMillis = req.decoded.utcMillis;\n                    LocalDateTime reqTime = LocalDateTime.ofEpochSecond(utcMillis/1000, ((int)(utcMillis % 1000)) * 1000_000, ZoneOffset.UTC);\n                    String formattedQuota = formatQuota(req.decoded.getSizeInBytes());\n                    System.out.println(req.getUsername() + \" \" + formattedQuota + \" \" + reqTime);\n                }\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    public static final Command<Boolean> SHOW = new Command<>(\"show\",\n            \"Show free quota of all users on this server\",\n            a -> {\n                boolean paidStorage = a.hasArg(\"quota-admin-address\");\n                if (paidStorage)\n                    throw new IllegalStateException(\"Quota CLI only valid on non paid instances\");\n                SqlSupplier sqlCommands = getSqlCommands(a);\n                Supplier<Connection> quotasDb = getDBConnector(a, \"quotas-sql-file\");\n                JdbcQuotas quotas = JdbcQuotas.build(quotasDb, sqlCommands);\n\n                if (a.hasArg(\"username\")) {\n                    String name = a.getArg(\"username\");\n                    printQuota(name, quotas.getQuota(name));\n                    return true;\n                }\n                TreeMap<String, Long> all = new TreeMap<>(quotas.getQuotas());\n                all.forEach(QuotaCLI::printQuota);\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"username\", \"The user whose quota to show (or all users are shown)\", false),\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    public static final Command<Boolean> LOCAL = new Command<>(\"local\",\n            \"Show all users with a space quota on this server\",\n            a -> {\n                boolean paidStorage = a.hasArg(\"quota-admin-address\");\n                if (paidStorage) {\n                    QuotaAdmin quotaAdmin = Builder.buildPaidQuotas(a);\n                    quotaAdmin.getLocalUsernames().stream()\n                            .sorted()\n                            .forEach(System.out::println);\n                    return true;\n                }\n                SqlSupplier sqlCommands = getSqlCommands(a);\n                Supplier<Connection> quotasDb = getDBConnector(a, \"quotas-sql-file\");\n                JdbcQuotas quotas = JdbcQuotas.build(quotasDb, sqlCommands);\n\n                quotas.getQuotas()\n                        .keySet()\n                        .stream()\n                        .sorted()\n                        .forEach(System.out::println);\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"username\", \"The user whose quota to show (or all users are shown)\", false),\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    public static final Command<Boolean> HOME = new Command<>(\"home\",\n            \"Show all users whose home server is the current server\",\n            a -> {\n                boolean paidStorage = a.hasArg(\"quota-admin-address\");\n\n                List<String> candidates;\n                if (paidStorage) {\n                    QuotaAdmin quotaAdmin = Builder.buildPaidQuotas(a);\n                    candidates = quotaAdmin.getLocalUsernames().stream()\n                            .sorted()\n                            .collect(Collectors.toList());\n                } else {\n                    SqlSupplier sqlCommands = getSqlCommands(a);\n                    Supplier<Connection> quotasDb = getDBConnector(a, \"quotas-sql-file\");\n                    JdbcQuotas quotas = JdbcQuotas.build(quotasDb, sqlCommands);\n\n                    candidates = quotas.getQuotas()\n                            .keySet()\n                            .stream()\n                            .sorted()\n                            .collect(Collectors.toList());\n                }\n                Crypto crypto = Main.initCrypto();\n                String peergosUrl = a.getArg(\"peergos-url\");\n                try {\n                    URL api = new URL(peergosUrl);\n                    NetworkAccess network = Main.buildJavaNetworkAccess(api, peergosUrl.startsWith(\"https\"), Optional.empty(), Optional.empty()).join();\n                    Cid us = network.dhtClient.id().join();\n                    candidates.stream()\n                            .filter(username -> network.coreNode.getHomeServer(username).join().map(h -> h.equals(us)).orElse(false))\n                            .forEach(System.out::println);\n                    return true;\n                } catch (Exception ex) {\n                    ex.printStackTrace();\n                    return false;\n                }\n            },\n            Arrays.asList(\n                    new Command.Arg(\"peergos-url\", \"Address of the Peergos server to connect to\", false, \"http://localhost:8000\"),\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    public static final Command<Boolean> TOKEN_CREATE = new Command<>(\"create\",\n            \"Create tokens for signups\",\n            a -> {\n                Crypto crypto = Main.initCrypto();\n                int count = a.getInt(\"count\", 1);\n                System.out.println(\"Created signup tokens:\");\n                boolean paidStorage = a.hasArg(\"quota-admin-address\");\n                if (paidStorage) {\n                    QuotaAdmin quotas = Builder.buildPaidQuotas(a);\n                    for (int i=0; i < count; i++)\n                        System.out.println(quotas.generateToken(crypto.random));\n                    return true;\n                }\n                SqlSupplier sqlCommands = getSqlCommands(a);\n                Supplier<Connection> quotasDb = getDBConnector(a, \"quotas-sql-file\");\n                JdbcQuotas quotas = JdbcQuotas.build(quotasDb, sqlCommands);\n\n                for (int i=0; i < count; i++) {\n                    String token = ArrayOps.bytesToHex(crypto.random.randomBytes(32));\n                    quotas.addToken(token);\n                    System.out.println(token);\n                }\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    public static final Command<Boolean> TOKENS_LIST = new Command<>(\"list\",\n            \"Show tokens for signups\",\n            a -> {\n                boolean paidStorage = a.hasArg(\"quota-admin-address\");\n                if (paidStorage)\n                    throw new IllegalStateException(\"Quota CLI only valid on non paid instances\");\n                SqlSupplier sqlCommands = getSqlCommands(a);\n                Supplier<Connection> quotasDb = getDBConnector(a, \"quotas-sql-file\");\n                JdbcQuotas quotas = JdbcQuotas.build(quotasDb, sqlCommands);\n\n                List<String> tokens = quotas.listTokens();\n                System.out.println(\"Stored tokens:\");\n                tokens.forEach(System.out::println);\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    public static final Command<Boolean> TOKEN = new Command<>(\"token\",\n            \"Create and list tokens for signups\",\n            a -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"quotas-sql-file\", \"The filename for the quotas datastore\", true, \"quotas.sql\")\n            ),\n            Arrays.asList(TOKEN_CREATE, TOKENS_LIST)\n    );\n\n    public static final Command<Boolean> QUOTA = new Command<>(\"quota\",\n            \"Manage quota of users on this server\",\n            args -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"print-log-location\", \"Whether to print the log file location at startup\", false, \"false\"),\n                    new Command.Arg(\"log-to-file\", \"Whether to log to a file\", false, \"false\"),\n                    new Command.Arg(\"log-to-console\", \"Whether to log to the console\", false, \"false\")\n            ),\n            Arrays.asList(LOCAL, HOME, SHOW, SET, REQUESTS, TOKEN)\n    );\n}\n"
  },
  {
    "path": "src/peergos/server/space/SpaceCheckingKeyFilter.java",
    "content": "package peergos.server.space;\n\nimport java.util.concurrent.atomic.*;\nimport java.util.logging.*;\n\nimport peergos.server.storage.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.util.*;\n\nimport peergos.server.corenode.*;\nimport peergos.server.mutable.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.Collectors;\n\n/** This class checks whether a given user is using more storage space than their quota\n *\n */\npublic class SpaceCheckingKeyFilter implements SpaceUsage {\n    private static final Logger LOG = Logging.LOG();\n    private static final long USAGE_TOLERANCE = 1024 * 1024;\n    private final CoreNode core;\n    private final MutablePointers mutable;\n    private final DeletableContentAddressedStorage dht;\n    private final Hasher hasher;\n    private final QuotaAdmin quotaAdmin;\n    private final UsageStore usageStore;\n    private static final ExecutorService VIRTUAL_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();\n    private final AtomicBoolean isRunning = new AtomicBoolean(true);\n    private final BlockingQueue<MutableEvent> mutableQueue = new ArrayBlockingQueue<>(1000);\n    private final long quotaUploadLimitSeconds;\n    private final Map<String, SlidingWindowCounter> writeLimiter = new ConcurrentHashMap<>();\n    private final Cid ourId;\n\n    public SpaceCheckingKeyFilter(CoreNode core,\n                                  MutablePointers mutable,\n                                  DeletableContentAddressedStorage dht,\n                                  Hasher hasher,\n                                  QuotaAdmin quotaAdmin,\n                                  UsageStore usageStore,\n                                  long quotaUploadLimitSeconds) {\n        this.core = core;\n        this.mutable = mutable;\n        this.dht = dht;\n        this.hasher = hasher;\n        this.quotaAdmin = quotaAdmin;\n        this.usageStore = usageStore;\n        this.quotaUploadLimitSeconds = quotaUploadLimitSeconds;\n        this.ourId = dht.id().join();\n        new Thread(() -> {\n            while (isRunning.get()) {\n                try {\n                    MutableEvent event = mutableQueue.take();\n                    processMutablePointerEvent(event);\n                } catch (InterruptedException e) {}\n                catch (Exception e) {\n                    e.printStackTrace();\n                }\n            }\n        }, \"SpaceCheckingKeyFilter\").start();\n        //add shutdown-hook to call close\n        Runtime.getRuntime().addShutdownHook(new Thread(this::close, \"SpaceChecker shutdown\"));\n    }\n\n    /**\n     * Write current view of usages to this.statePath, completing any pending operations\n     */\n    private synchronized void close() {\n        isRunning.set(false);\n        usageStore.close();\n    }\n\n    /**\n     * Walk the virtual file-system to calculate space used by each owner not already checked\n     */\n    public void calculateUsage() {\n        try {\n            List<String> usernames = quotaAdmin.getLocalUsernames();\n            long t0 = System.currentTimeMillis();\n            Logging.LOG().info(\"Calculating space usage for \" + usernames.size() + \" local users...\");\n            long done = 0;\n            for (String username : usernames) {\n                Logging.LOG().info(\"Calculating space usage of \" + username + \" (\" + done++ + \"/\" + usernames.size() + \")\");\n                try {\n                    Optional<PublicKeyHash> identity = core.getPublicKeyHash(username).get();\n                    if (identity.isPresent()) {\n                        long prior = usageStore.getUsage(username).totalUsage();\n                        processCorenodeEvent(username, identity.get());\n                        long after = usageStore.getUsage(username).totalUsage();\n                        if (after != prior)\n                            LOG.info(\"Updated space usage of user: \" + username + \" to \" + after);\n                    } else\n                        LOG.info(\"Identity key absent in pki for user: \" + username);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                    LOG.log(Level.WARNING, \"ERROR calculating usage for user: \" + username + \"\\n\" + e.getMessage(), e);\n                }\n            }\n            usageStore.initialized();\n            long t1 = System.currentTimeMillis();\n            Logging.LOG().info(\"Finished calculating space usage for \" + usernames.size() + \" local users in \" + (t1-t0)/1_000 + \"s\");\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n\n    public static void update(UsageStore store,\n                              QuotaAdmin quotas,\n                              CoreNode core,\n                              MutablePointers mutable,\n                              DeletableContentAddressedStorage dht,\n                              Hasher hasher) {\n        Logging.LOG().info(\"Checking for updated usage for users...\");\n        Cid ourId = dht.id().join();\n        List<String> localUsernames = quotas.getLocalUsernames();\n        for (String username : localUsernames) {\n            store.addUserIfAbsent(username);\n            Optional<PublicKeyHash> identity = core.getPublicKeyHash(username).join();\n            if (identity.isPresent())\n                store.addWriter(username, identity.get());\n        }\n\n        Logging.LOG().info(\"Checking for updated mutable pointers...\");\n        long t1 = System.currentTimeMillis();\n        Set<PublicKeyHash> writers = store.getAllWriters();\n        List<Multihash> us = List.of(ourId.bareMultihash());\n        for (PublicKeyHash writerKey : writers) {\n            WriterUsage tmpUsage = null;\n            try {\n                tmpUsage = store.getUsage(writerKey);\n                WriterUsage writerUsage = tmpUsage;\n                Logging.LOG().info(\"Checking for updates from user: \" + writerUsage.owner + \", writer key: \" + writerKey);\n\n                PublicKeyHash owner = writerKey; //NB: owner is a dummy value\n                MaybeMultihash rootHash = mutable.getPointerTarget(owner, writerKey, dht).join().updated;\n                boolean isChanged = ! writerUsage.target().equals(rootHash);\n                if (isChanged) {\n                    Logging.LOG().info(\"Root hash changed from \" + writerUsage.target() + \" to \" + rootHash);\n                    long updatedSize = dht.getRecursiveBlockSize(owner, (Cid)rootHash.get(), us).get();\n                    long deltaUsage = updatedSize - writerUsage.directRetainedStorage();\n                    store.confirmUsage(writerUsage.owner, writerKey, deltaUsage, store.getUsage(writerUsage.owner).isErrored());\n                    Set<PublicKeyHash> directOwnedKeys = DeletableContentAddressedStorage.getDirectOwnedKeys(owner, writerKey, mutable,\n                            (h, s) -> DeletableContentAddressedStorage.getWriterData(us, owner, h, s, false, ourId, hasher, dht), dht, hasher).join();\n                    List<PublicKeyHash> newOwnedKeys = directOwnedKeys.stream()\n                            .filter(key -> !writerUsage.ownedKeys().contains(key))\n                            .collect(Collectors.toList());\n                    for (PublicKeyHash newOwnedKey : newOwnedKeys) {\n                        store.addWriter(writerUsage.owner, newOwnedKey);\n                        processMutablePointerEvent(store, owner, newOwnedKey, MaybeMultihash.empty(),\n                                mutable.getPointerTarget(owner, newOwnedKey, dht).get().updated, mutable, quotas, dht, hasher);\n                    }\n                    HashSet<PublicKeyHash> removedOwnedKeys = new HashSet<>(writerUsage.ownedKeys());\n                    removedOwnedKeys.removeAll(directOwnedKeys);\n                    store.updateWriterUsage(writerKey, rootHash, removedOwnedKeys, new HashSet<>(newOwnedKeys), updatedSize);\n                    Logging.LOG().info(\"Updated space used by \" + writerKey + \" to \" + updatedSize);\n                }\n            } catch (Throwable t) {\n                Logging.LOG().log(Level.WARNING, \"Failed calculating usage for \" + (tmpUsage == null ? writerKey : tmpUsage.owner), t);\n            }\n        }\n        long t2 = System.currentTimeMillis();\n        Logging.LOG().info(LocalDateTime.now() + \" Finished updating space usage for all usernames in \" + (t2 - t1)/1000 + \" s\");\n    }\n\n    public CompletableFuture<Boolean> accept(CorenodeEvent event) {\n        usageStore.addUserIfAbsent(event.username);\n        usageStore.addWriter(event.username, event.keyHash);\n        return CompletableFuture.supplyAsync(() -> processCorenodeEvent(event.username, event.keyHash), VIRTUAL_EXECUTOR);\n    }\n\n    /** Update our view of the world because a user has changed their public key (or registered)\n     *\n     * @param username\n     * @param writer\n     */\n    private boolean processCorenodeEvent(String username, PublicKeyHash writer) {\n        try {\n            processCorenodeEvent(username, writer, usageStore, quotaAdmin, dht, mutable, hasher);\n            return true;\n        } catch (Throwable e) {\n            LOG.severe(\"Error loading storage for user: \" + username);\n            Exceptions.getRootCause(e).printStackTrace();\n            return false;\n        }\n    }\n\n    public void accept(MutableEvent event) {\n        mutableQueue.add(event);\n        try {\n            prepareMutablePointerChange(event, dht, usageStore, hasher);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    public static void processCorenodeEvent(String username,\n                                            PublicKeyHash owner,\n                                            UsageStore usageStore,\n                                            QuotaAdmin quotaAdmin,\n                                            DeletableContentAddressedStorage dht,\n                                            MutablePointers mutable,\n                                            Hasher hasher) {\n        // get current set of owned keys from usage db, and traverse filesystem\n        // only if a pointer has changed since last usage update\n        Set<PublicKeyHash> allUserKeys = usageStore.getAllWriters(owner);\n        processCorenodeEvent(username, owner, allUserKeys, usageStore, quotaAdmin, dht, mutable, hasher);\n    }\n    public static void processCorenodeEvent(String username,\n                                            PublicKeyHash owner,\n                                            Set<PublicKeyHash> allUserKeys,\n                                            UsageStore usageStore,\n                                            QuotaAdmin quotaAdmin,\n                                            DeletableContentAddressedStorage dht,\n                                            MutablePointers mutable,\n                                            Hasher hasher) {\n        usageStore.addUserIfAbsent(username);\n\n        for (PublicKeyHash writerKey : allUserKeys) {\n            usageStore.addWriter(username, writerKey);\n            WriterUsage current = usageStore.getUsage(writerKey);\n            MaybeMultihash updatedRoot = mutable.getPointerTarget(owner, writerKey, dht).join().updated;\n            processMutablePointerEvent(usageStore, owner, writerKey, current.target(), updatedRoot, mutable, quotaAdmin, dht, hasher);\n        }\n    }\n\n    private static void prepareMutablePointerChange(MutableEvent event,\n                                                    DeletableContentAddressedStorage dht,\n                                                    UsageStore usageStore,\n                                                    Hasher hasher) {\n        Cid ourId = dht.id().join();\n        List<Multihash> us = List.of(ourId.bareMultihash());\n        PointerUpdate pointerUpdate = dht.getSigningKey(event.owner, event.writer)\n                .thenApply(signer -> PointerUpdate.fromCbor(CborObject.fromByteArray(signer.get()\n                        .unsignMessage(event.writerSignedBtreeRootHash).join()))).join();\n        Set<PublicKeyHash> updatedOwned =\n                DeletableContentAddressedStorage.getDirectOwnedKeys(event.owner, event.writer, pointerUpdate.updated,\n                        (h, s) -> DeletableContentAddressedStorage.getWriterData(us, event.owner, h, s, false, ourId, hasher, dht), dht, hasher).join();\n        String owner = usageStore.getOwner(event.writer);\n        for (PublicKeyHash owned : updatedOwned) {\n            usageStore.addWriter(owner, owned);\n        }\n    }\n\n    private void processMutablePointerEvent(MutableEvent event) {\n        try {\n            PointerUpdate pointerUpdate = dht.getSigningKey(event.owner, event.writer)\n                    .thenApply(signer -> PointerUpdate.fromCbor(CborObject.fromByteArray(signer.get()\n                            .unsignMessage(event.writerSignedBtreeRootHash).join()))).join();\n            processMutablePointerEvent(usageStore, event.owner, event.writer, pointerUpdate.original, pointerUpdate.updated,\n                    mutable, quotaAdmin, dht, hasher);\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n\n    private static void processMutablePointerEvent(UsageStore state,\n                                                   PublicKeyHash owner,\n                                                   PublicKeyHash writer,\n                                                   MaybeMultihash existingRoot,\n                                                   MaybeMultihash newRoot,\n                                                   MutablePointers mutable,\n                                                   QuotaAdmin quotaAdmin,\n                                                   DeletableContentAddressedStorage dht,\n                                                   Hasher hasher) {\n        if (existingRoot.equals(newRoot))\n            return;\n        Cid ourId = dht.id().join();\n        List<Multihash> us = List.of(ourId.bareMultihash());\n        synchronized (getWriterLock(writer)) {\n            // Re-read inside the lock to get a consistent view; another thread may have already processed this change\n            WriterUsage current = state.getUsage(writer);\n            if (current == null)\n                throw new IllegalStateException(\"Unknown writer key hash: \" + writer);\n            if (current.target().equals(newRoot))\n                return; // already processed by another thread\n            if (! newRoot.isPresent()) {\n                LOG.info(\"Removing usage for (\" + owner + \", \" + writer + \") from \" + current.directRetainedStorage());\n                state.confirmUsage(current.owner, writer, -current.directRetainedStorage(), state.getUsage(current.owner).isErrored());\n                state.updateWriterUsage(writer, MaybeMultihash.empty(), Collections.emptySet(), Collections.emptySet(), 0);\n                if (existingRoot.isPresent()) {\n                    try {\n                        // subtract data size from orphaned child keys (this assumes the keys form a tree without dupes)\n                        Set<PublicKeyHash> updatedOwned =\n                                DeletableContentAddressedStorage.getDirectOwnedKeys(owner, writer, existingRoot,\n                                        (h, s) -> DeletableContentAddressedStorage.getWriterData(us, owner, h, s, false, ourId, hasher, dht),  dht, hasher).join();\n                        processRemovedOwnedKeys(state, owner, updatedOwned, mutable, quotaAdmin, dht, hasher);\n                    } catch (Exception e) {\n                        LOG.log(Level.WARNING, e.getMessage(), e);\n                    }\n                }\n                return;\n            }\n\n            try {\n                long t0 = System.nanoTime();\n                long changeInStorage = dht.getChangeInContainedSize(owner, current.target().toOptional().map(c -> (Cid) c), (Cid) newRoot.get()).get();\n                long t1 = System.nanoTime();\n                LOG.info(\"Calculating change in used space for (\" + owner + \", \" + writer + \") took \" + (t1-t0)/1_000_000 + \"mS\");\n                Set<PublicKeyHash> updatedOwned =\n                        DeletableContentAddressedStorage.getDirectOwnedKeys(owner, writer, newRoot,\n                                (h, s) -> DeletableContentAddressedStorage.getWriterData(us, owner, h, s, false, ourId, hasher, dht), dht, hasher).join();\n                for (PublicKeyHash owned : updatedOwned) {\n                    state.addWriter(current.owner, owned);\n                }\n                UserUsage usage = state.getUsage(current.owner);\n                boolean initialErrored = usage.isErrored();\n                String username = current.owner;\n                long quota = getQuota(username, quotaAdmin);\n                boolean errored = initialErrored && usage.totalUsage() > quota;\n                state.confirmUsage(current.owner, writer, changeInStorage, errored);\n                UserUsage cached = getUsage(username, state);\n                cached.confirmUsage(writer, changeInStorage);\n                cached.setErrored(errored);\n\n                HashSet<PublicKeyHash> removedChildren = new HashSet<>(current.ownedKeys());\n                removedChildren.removeAll(updatedOwned);\n                processRemovedOwnedKeys(state, owner, removedChildren, mutable, quotaAdmin, dht, hasher);\n                HashSet<PublicKeyHash> addedOwnedKeys = new HashSet<>(updatedOwned);\n                addedOwnedKeys.removeAll(current.ownedKeys());\n                state.updateWriterUsage(writer, newRoot, removedChildren, addedOwnedKeys, current.directRetainedStorage() + changeInStorage);\n                for (PublicKeyHash added : addedOwnedKeys) {\n                    state.addWriter(current.owner, added);\n                    WriterUsage currentAdded = state.getUsage(added);\n                    MaybeMultihash updatedRoot = mutable.getPointerTarget(owner, added, dht).join().updated;\n                    processMutablePointerEvent(state, owner, added, currentAdded.target(), updatedRoot, mutable, quotaAdmin, dht, hasher);\n                }\n                LOG.info(\"Updated usage for (\" + owner + \", \" + writer + \") from \" + current.directRetainedStorage() + \", adding \" + changeInStorage);\n            } catch (Exception e) {\n                Exceptions.getRootCause(e).printStackTrace();\n            }\n        }\n    }\n\n    private static void processRemovedOwnedKeys(UsageStore state,\n                                                PublicKeyHash owner,\n                                                Set<PublicKeyHash> removed,\n                                                MutablePointers mutable,\n                                                QuotaAdmin quotaAdmin,\n                                                DeletableContentAddressedStorage dht,\n                                                Hasher hasher) {\n        for (PublicKeyHash ownedKey : removed) {\n            try {\n                MaybeMultihash currentTarget = mutable.getPointerTarget(owner, ownedKey, dht).get().updated;\n                processMutablePointerEvent(state, owner, ownedKey, currentTarget, MaybeMultihash.empty(), mutable, quotaAdmin, dht, hasher);\n            } catch (Exception e) {\n                LOG.log(Level.WARNING, e.getMessage(), e);\n            }\n        }\n    }\n\n    @Override\n    public CompletableFuture<Long> getUsage(PublicKeyHash owner, byte[] signedTime, boolean local) {\n        TimeLimited.isAllowedTime(signedTime, 300, dht, owner);\n        String user = usageStore.getOwner(owner);\n        UserUsage usage = usageStore.getUsage(user);\n        if (usage == null)\n            return Futures.errored(new IllegalStateException(\"No usage present for user: \" + user));\n        return CompletableFuture.completedFuture(local ? usage.expectedUsage() : usage.totalUsage());\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> getPaymentProperties(PublicKeyHash owner, boolean newClientSecret, byte[] signedTime) {\n        return quotaAdmin.getPaymentProperties(owner, newClientSecret, signedTime);\n    }\n\n    @Override\n    public CompletableFuture<Long> getQuota(PublicKeyHash owner, byte[] signedTime) {\n        TimeLimited.isAllowedTime(signedTime, 24*3600, dht, owner);\n        String user = usageStore.getOwner(owner);\n        return quotaAdmin.getQuota(owner, signedTime);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> requestQuota(PublicKeyHash owner, byte[] signedRequest, long claimedUsage) {\n        String username = core.getUsername(owner).join();\n        UserUsage usage = usageStore.getUsage(username);\n        return quotaAdmin.requestQuota(owner, signedRequest,  usage.totalUsage());\n    }\n\n    private static final LRUCache<Long, Map<String, Long>> quotas = new LRUCache<>(2);\n    private static final LRUCache<Long, Map<String, UserUsage>> usageCache = new LRUCache<>(2);\n    private static final ConcurrentHashMap<PublicKeyHash, Object> writerLocks = new ConcurrentHashMap<>();\n\n    private static Object getWriterLock(PublicKeyHash writer) {\n        return writerLocks.computeIfAbsent(writer, k -> new Object());\n    }\n\n    private static long getQuota(String owner, QuotaAdmin quotaAdmin) {\n        long timeKey = System.currentTimeMillis() / 3_600_000;\n        Map<String, Long> cachedQuotas;\n        synchronized (quotas) {\n            cachedQuotas = quotas.computeIfAbsent(timeKey, k -> new ConcurrentHashMap<>());\n        }\n        Long cachedQuota = cachedQuotas.get(owner);\n        long quota = cachedQuota != null ? cachedQuota : quotaAdmin.getQuota(owner);\n        if (cachedQuota == null)\n            cachedQuotas.put(owner, quota);\n        return quota;\n    }\n\n    private static UserUsage getUsage(String username, UsageStore usageStore) {\n        UserUsage usage;\n        Map<String, UserUsage> usageByHour;\n        long tenMinuteKey = System.currentTimeMillis() / 600_000;\n        synchronized (usageCache) {\n            usageByHour = usageCache.computeIfAbsent(tenMinuteKey, k -> new ConcurrentHashMap<>());\n        }\n        usage = usageByHour.get(username);\n        if (usage == null) {\n            usage = usageStore.getUsage(username);\n            usageByHour.put(username, usage);\n        } else if (usage.isErrored()) {\n            usage = usageStore.getUsage(username);\n            usageByHour.put(username, usage);\n        }\n        return usage;\n    }\n\n    public boolean allowWrite(PublicKeyHash writer, int size) {\n        String username = usageStore.getOwner(writer);\n        long quota = getQuota(username, quotaAdmin);\n\n        UserUsage usage = getUsage(username, usageStore);\n\n        long expectedUsage = usage.expectedUsage();\n        boolean errored = usage.isErrored();\n        if ((! errored && expectedUsage + size > quota) || (errored && expectedUsage + size > quota + USAGE_TOLERANCE)) {\n            long pending = usage.getPending(writer);\n            usageStore.confirmUsage(username, writer, 0, true);\n            usage.confirmUsage(writer, 0);\n            usage.setErrored(true);\n            LOG.info(\"Rejecting write for \" + username);\n            throw new IllegalStateException(\"Storage quota reached! \\nUsed \"\n                    + usage.totalUsage() + \" out of \" + quota + \" bytes. Rejecting write of size \" + (size + pending) + \". \\n\" +\n                    \"Please delete some files or request more space.\");\n        }\n        SlidingWindowCounter writeLimit = writeLimiter.get(username);\n        if (writeLimit == null) {\n            writeLimit = new SlidingWindowCounter(quotaUploadLimitSeconds, quota);\n            writeLimiter.put(username, writeLimit);\n        }\n        if (! writeLimit.allowRequest(size))\n            throw new IllegalStateException(\"Upload bandwidth exceeded please try again tomorrow\");\n        try {\n            usage.addPending(writer, size);\n        } catch (Exception e) {\n            throw new IllegalStateException(\"Couldn't update pending usage for user \" + username, e);\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/space/UsageCLI.java",
    "content": "package peergos.server.space;\n\nimport peergos.server.*;\nimport peergos.server.sql.*;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.function.*;\n\npublic class UsageCLI extends Builder {\n\n    public static final Command<Boolean> SHOW = new Command<>(\"show\",\n            \"Show usage for a user(s) on this server\",\n            a -> {\n                SqlSupplier sqlCommands = getSqlCommands(a);\n                Supplier<Connection> usageConn = getDBConnector(a, \"space-usage-sql-file\");\n                JdbcUsageStore usageDb = new JdbcUsageStore(usageConn, sqlCommands);\n\n                if (a.hasArg(\"username\")) {\n                    String name = a.getArg(\"username\");\n                    UserUsage usage = usageDb.getUsage(name);\n                    printFullUsage(name, usage.totalUsage(), usage.expectedUsage() - usage.totalUsage());\n                    return true;\n                }\n                usageDb.getAllUsage()\n                        .entrySet()\n                        .stream()\n                        .sorted(Comparator.comparingLong(Map.Entry::getValue))\n                        .forEach(e -> printUsage(e.getKey(), e.getValue()));\n                return true;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"username\", \"The user whose quota to show (or all users are shown)\", false),\n                    new Command.Arg(\"space-usage-sql-file\", \"The filename for the space usage datastore\", true, \"quotas.sql\")\n            )\n    );\n\n    private static void printFullUsage(String name, long usage, long pendingUsage) {\n        System.out.println(name + \" \" + formatUsage(usage) + \" pending: \" + formatUsage(pendingUsage));\n    }\n\n    private static void printUsage(String name, long usage) {\n        System.out.println(name + \" \" + formatUsage(usage));\n    }\n\n    private static String formatUsage(long quota) {\n        long mb = quota / 1000_000;\n        if (mb == 0)\n            return quota + \" B\";\n        if (mb < 1000)\n            return mb + \" MB\";\n        return mb/1000 + \" GB\";\n    }\n\n    public static final Command<Boolean> USAGE = new Command<>(\"usage\",\n            \"Show usage on this server\",\n            args -> {\n                System.out.println(\"Run with -help to show options\");\n                return null;\n            },\n            Arrays.asList(\n                    new Command.Arg(\"print-log-location\", \"Whether to print the log file location at startup\", false, \"false\"),\n                    new Command.Arg(\"log-to-file\", \"Whether to log to a file\", false, \"false\"),\n                    new Command.Arg(\"log-to-console\", \"Whether to log to the console\", false, \"false\")\n            ),\n            Arrays.asList(SHOW)\n    );\n}\n"
  },
  {
    "path": "src/peergos/server/space/UsageStore.java",
    "content": "package peergos.server.space;\n\npublic interface UsageStore extends WriterUsageStore, UserUsageStore {\n\n    void initialized();\n\n    void close();\n}\n"
  },
  {
    "path": "src/peergos/server/space/UserUsage.java",
    "content": "package peergos.server.space;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.*;\n\npublic class UserUsage implements Cborable {\n    private long totalBytes;\n    private boolean errored;\n    private Map<PublicKeyHash, Long> pending;\n\n    public UserUsage(long totalBytes) {\n        this.totalBytes = totalBytes;\n        this.pending = new HashMap<>();\n        this.errored = false;\n    }\n\n    public UserUsage(long totalBytes, boolean errored, Map<PublicKeyHash, Long> pending) {\n        this.totalBytes = totalBytes;\n        this.pending = pending;\n        this.errored = errored;\n    }\n\n    public long totalUsage() {\n        return totalBytes;\n    }\n\n    protected synchronized void confirmUsage(PublicKeyHash writer, long usageDelta) {\n        pending.remove(writer);\n        totalBytes += usageDelta;\n        errored = false;\n    }\n\n    protected synchronized void addPending(PublicKeyHash writer, long usageDelta) {\n        pending.put(writer, pending.getOrDefault(writer, 0L) + usageDelta);\n    }\n\n    protected synchronized void clearPending(PublicKeyHash writer) {\n        pending.remove(writer);\n    }\n\n    public synchronized long getPending(PublicKeyHash writer) {\n        return pending.getOrDefault(writer, 0L);\n    }\n\n    protected synchronized long expectedUsage() {\n        return totalBytes + pending.values().stream().mapToLong(x -> x).sum();\n    }\n\n    protected synchronized void setErrored(boolean errored) {\n        this.errored = errored;\n    }\n\n    protected synchronized boolean isErrored() {\n        return errored;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborLong(totalBytes);\n    }\n\n    public static UserUsage fromCbor(Cborable cborable) {\n        long usage = ((CborObject.CborLong) cborable).value;\n        return new UserUsage(usage);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        UserUsage usage1 = (UserUsage) o;\n\n        if (totalBytes != usage1.totalBytes) return false;\n        return pending != null ? pending.equals(usage1.pending) : usage1.pending == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = (int) (totalBytes ^ (totalBytes >>> 32));\n        result = 31 * result + (pending != null ? pending.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/space/UserUsageStore.java",
    "content": "package peergos.server.space;\n\nimport peergos.shared.crypto.hash.*;\n\npublic interface UserUsageStore extends WriterUsageStore {\n\n    void addUserIfAbsent(String username);\n\n    UserUsage getUsage(String username);\n\n    void confirmUsage(String username, PublicKeyHash writer, long usageDelta, boolean errored);\n\n    void addPendingUsage(String username, PublicKeyHash writer, int size);\n\n    void resetPendingUsage(String username, PublicKeyHash writer);\n}\n"
  },
  {
    "path": "src/peergos/server/space/WriterUsage.java",
    "content": "package peergos.server.space;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class WriterUsage implements Cborable {\n    public final String owner;\n    private MaybeMultihash target;\n    private long directRetainedStorage;\n    private Set<PublicKeyHash> ownedKeys;\n\n    public WriterUsage(String owner, MaybeMultihash target, long directRetainedStorage, Set<PublicKeyHash> ownedKeys) {\n        this.owner = owner;\n        this.target = target;\n        this.directRetainedStorage = directRetainedStorage;\n        this.ownedKeys = ownedKeys;\n    }\n\n    public synchronized void update(MaybeMultihash target,\n                                    Set<PublicKeyHash> removedOwnedKeys,\n                                    Set<PublicKeyHash> addedOwnedKeys,\n                                    long retainedStorage) {\n        this.target = target;\n        HashSet<PublicKeyHash> updated = new HashSet<>(ownedKeys);\n        updated.removeAll(removedOwnedKeys);\n        updated.addAll(addedOwnedKeys);\n        this.ownedKeys = Collections.unmodifiableSet(updated);\n        this.directRetainedStorage = retainedStorage;\n    }\n\n    public MaybeMultihash target() {\n        return target;\n    }\n\n    public synchronized long directRetainedStorage() {\n        return directRetainedStorage;\n    }\n\n    public synchronized MaybeMultihash getRoot() {\n        return target;\n    }\n\n    public synchronized Set<PublicKeyHash> ownedKeys() {\n        return Collections.unmodifiableSet(ownedKeys);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> map = new HashMap<>();\n        map.put(\"owner\", new CborObject.CborString(owner));\n        map.put(\"target\", target);\n        map.put(\"storage\", new CborObject.CborLong(directRetainedStorage));\n        map.put(\"ownedKey\", new CborObject.CborList(ownedKeys.stream().collect(Collectors.toList())));\n        return CborObject.CborMap.build(map);\n    }\n\n    public static WriterUsage fromCbor(Cborable cbor) {\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        String owner = map.getString(\"owner\");\n        MaybeMultihash target = map.get(\"target\", MaybeMultihash::fromCbor);\n        long storage  = map.getLong(\"storage\");\n        List<PublicKeyHash> ownedKeys = map.getList(\"ownedKey\").map(PublicKeyHash::fromCbor);\n        return new WriterUsage(owner, target, storage, new HashSet<>(ownedKeys));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        WriterUsage writerUsage = (WriterUsage) o;\n\n        if (directRetainedStorage != writerUsage.directRetainedStorage) return false;\n        if (owner != null ? !owner.equals(writerUsage.owner) : writerUsage.owner != null) return false;\n        if (target != null ? !target.equals(writerUsage.target) : writerUsage.target != null) return false;\n        return ownedKeys != null ? ownedKeys.equals(writerUsage.ownedKeys) : writerUsage.ownedKeys == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = owner != null ? owner.hashCode() : 0;\n        result = 31 * result + (target != null ? target.hashCode() : 0);\n        result = 31 * result + (int) (directRetainedStorage ^ (directRetainedStorage >>> 32));\n        result = 31 * result + (ownedKeys != null ? ownedKeys.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/space/WriterUsageStore.java",
    "content": "package peergos.server.space;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic interface WriterUsageStore {\n\n    void addWriter(String owner, PublicKeyHash writer);\n\n    Set<PublicKeyHash> getAllWriters();\n\n    Set<PublicKeyHash> getAllWriters(PublicKeyHash owner);\n\n    Set<PublicKeyHash> getAllWriters(String owner);\n\n    WriterUsage getUsage(PublicKeyHash writer);\n\n    PublicKeyHash getOwnerKey(PublicKeyHash writer);\n\n    PublicKeyHash getOwnerKey(String username);\n\n    String getOwner(PublicKeyHash writer);\n\n    void updateWriterUsage(PublicKeyHash writer,\n                           MaybeMultihash target,\n                           Set<PublicKeyHash> removedOwnedKeys,\n                           Set<PublicKeyHash> addedOwnedKeys,\n                           long retainedStorage);\n\n    // return current usage root, and username\n    List<Triple<Multihash, String, PublicKeyHash>> getAllTargets();\n\n    // return current usage root, and username\n    List<Triple<Multihash, String, PublicKeyHash>> getAllTargets(String username);\n\n    /**\n     *\n     * @return All usernames and owner keys using space locally\n     */\n    List<Pair<String, PublicKeyHash>> getAllOwners();\n}\n"
  },
  {
    "path": "src/peergos/server/sql/PostgresCommands.java",
    "content": "package peergos.server.sql;\n\npublic class PostgresCommands implements SqlSupplier {\n\n    @Override\n    public String vacuumCommand() {\n        return \"\";\n    }\n\n    @Override\n    public String listTablesCommand() {\n        return \"SELECT tablename FROM pg_catalog.pg_tables \" +\n                \"WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema';\";\n    }\n\n    @Override\n    public String tableExistsCommand() {\n        return \"SELECT table_name FROM information_schema.tables WHERE table_schema LIKE 'public' AND table_type LIKE 'BASE TABLE' AND table_name = ?;\";\n    }\n\n    @Override\n    public String addMetadataCommand() {\n        return \"INSERT INTO blockmetadata (owner, cid, version, size, links, batids) VALUES(?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING;\";\n    }\n\n    @Override\n    public String updateMetadataCommand() {\n        return \"UPDATE blockmetadata SET owner=? WHERE cid=?;\";\n    }\n\n    @Override\n    public String setMetadataVersionAndOwnerCommand() {\n        return \"UPDATE blockmetadata SET version=?, owner=? WHERE cid=?;\";\n    }\n\n    @Override\n    public String createFollowRequestsTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS followrequests (id serial primary key, \" +\n                \"name text not null, followrequest text not null);\";\n    }\n\n    @Override\n    public String ensureColumnExistsCommand(String table, String column, String type) {\n        return \"ALTER TABLE \" + table + \" ADD COLUMN IF NOT EXISTS \" + column + \" \" + type + \";\";\n    }\n\n    @Override\n    public String insertTransactionCommand() {\n        return \"INSERT INTO transactions (tid, owner, hash, time) VALUES(?, ?, ?, ?) ON CONFLICT DO NOTHING;\";\n    }\n\n    @Override\n    public String insertServerIdCommand() {\n        return \"INSERT INTO serverids (peerid, record) VALUES(?, ?) ON CONFLICT DO NOTHING;\";\n    }\n\n    @Override\n    public String insertOrIgnoreCommand(String prefix, String suffix) {\n        return prefix + suffix + \" ON CONFLICT DO NOTHING;\";\n    }\n\n    @Override\n    public String getByteArrayType() {\n        return \"BYTEA\";\n    }\n\n    @Override\n    public String getSerialIdType() {\n        return \"SERIAL\";\n    }\n\n    @Override\n    public String sqlInteger() {\n        return \"BIGINT\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sql/SqlSupplier.java",
    "content": "package peergos.server.sql;\n\nimport java.sql.*;\n\npublic interface SqlSupplier {\n\n    String listTablesCommand();\n\n    String tableExistsCommand();\n\n    String createFollowRequestsTableCommand();\n\n    String insertTransactionCommand();\n\n    String insertServerIdCommand();\n\n    String getByteArrayType();\n\n    String getSerialIdType();\n\n    String sqlInteger();\n\n    String ensureColumnExistsCommand(String table, String column, String type);\n\n    String addMetadataCommand();\n\n    String updateMetadataCommand();\n\n    String setMetadataVersionAndOwnerCommand();\n\n    String vacuumCommand();\n\n    default String createMutablePointersTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS metadatablobs (writingkey text primary key not null, hash text not null); \" +\n                \"CREATE UNIQUE INDEX IF NOT EXISTS index_name ON metadatablobs (writingkey);\";\n    }\n\n    default String createAccountTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS login (username text primary key not null, entry text not null, reader text not null); \" +\n                \"CREATE UNIQUE INDEX IF NOT EXISTS login_index ON login (username);\";\n    }\n\n    // credid is <= 1023 bytes\n    default String createMfaTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS mfa (username text not null, name text not null, credid \" + getByteArrayType() + \" not null, \" +\n                \"type \" + sqlInteger() + \" not null, \" +\n                \"enabled boolean not null, \" +\n                \"created \" + sqlInteger() + \" not null, \" +\n                \"value \" + getByteArrayType() + \" not null); \" +\n                \"CREATE INDEX IF NOT EXISTS mfa_index ON mfa (username);\";\n    }\n\n    default String createMfaChallengeTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS mfa_challenge (username text primary key not null, challenge \" + getByteArrayType() + \" not null); \" +\n                \"CREATE UNIQUE INDEX IF NOT EXISTS mfa_challenge_index ON mfa_challenge (username);\";\n    }\n\n    default String createBatStoreTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS bats (username text not null, id text primary key not null, bat text not null); \" +\n                \"CREATE UNIQUE INDEX IF NOT EXISTS bat_index ON bats (id);\";\n    }\n\n    default String createLinkCountTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS linkcounts (username text not null, label \"+sqlInteger()+\" not null, count \"+\n                sqlInteger()+\" not null, modified \"+sqlInteger()+\" not null, PRIMARY KEY (username, label)); \";\n    }\n\n    default String createSpaceRequestsTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS spacerequests (name text primary key not null, spacerequest text not null);\";\n    }\n\n    default String createQuotasTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS freequotas (name text primary key not null, quota BIGINT not null);\" +\n                \"CREATE TABLE IF NOT EXISTS signuptokens (token varchar(64) primary key not null);\";\n    }\n\n    default String createTransactionsTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS transactions (\" +\n                \"tid varchar(64) not null, owner varchar(64) not null, hash varchar(64) not null, time \" + sqlInteger()+\");\";\n    }\n\n    default String createServerIdentitiesTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS serverids (\" +\n                \"id \" + getSerialIdType() + \" primary key not null,\" +\n                \"peerid \" + getByteArrayType() + \" not null, \" +\n                \"private \" + getByteArrayType() + \", \" +\n                \"record \" + getByteArrayType() + \" not null);\";\n    }\n\n    default String createBlockMetadataStoreTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS blockmetadata (owner \" + getByteArrayType() + \", \" +\n                \"cid \" + getByteArrayType() + \" primary key not null, \" +\n                \"version varchar(160),\" +\n                \"size \" + sqlInteger() + \" not null, \" +\n                \"links \" + getByteArrayType() + \" not null, \" +\n                \"batids \" + getByteArrayType() + \" not null);\";\n    }\n\n    default String createPartitionStatusTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS partitioned (done boolean);\";\n    }\n\n    default String createServerMessageTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS messages (\" +\n                \"id \" + getSerialIdType() + \" PRIMARY KEY NOT NULL,\" +\n                \"username VARCHAR(32) NOT NULL,\" +\n                \"type \" + sqlInteger() + \" NOT NULL,\" +\n                \"sent \" + sqlInteger() + \" NOT NULL,\" +\n                \"body text NOT NULL,\" +\n                \"priorid \" + sqlInteger() + \",\" +\n                \"dismissed boolean\" +\n                \");\";\n    }\n\n    String insertOrIgnoreCommand(String prefix, String suffix);\n\n    default String createUsageTablesCommand() {\n        return \"CREATE TABLE IF NOT EXISTS users (\" +\n                \"id \" + getSerialIdType() + \" PRIMARY KEY NOT NULL,\" +\n                \"name VARCHAR(32) NOT NULL,\" +\n                \"CONSTRAINT uniq_users UNIQUE (name)\" +\n                \");\" +\n                \"CREATE TABLE IF NOT EXISTS userusage (\" +\n                \"user_id INTEGER REFERENCES users(id) PRIMARY KEY,\" +\n                \"total_bytes BIGINT NOT NULL,\" +\n                \"errored BOOLEAN NOT NULL,\" +\n                \"CONSTRAINT uniq_usage UNIQUE (user_id)\" +\n                \");\" +\n                \"CREATE TABLE IF NOT EXISTS writers (\" +\n                \"id \" + getSerialIdType() + \" PRIMARY KEY NOT NULL,\" +\n                \"key_hash \" + getByteArrayType() + \" NOT NULL,\" +\n                \"CONSTRAINT uniq_writers UNIQUE (key_hash)\" +\n                \");\" +\n                \"CREATE TABLE IF NOT EXISTS pendingusage (\" +\n                \"user_id INTEGER REFERENCES users(id),\" +\n                \"writer_id INTEGER REFERENCES writers(id) PRIMARY KEY,\" +\n                \"pending_bytes BIGINT NOT NULL\" +\n                \");\" +\n                \"CREATE TABLE IF NOT EXISTS writerusage (\" +\n                \"writer_id INTEGER REFERENCES writers(id),\" +\n                \"user_id INTEGER REFERENCES users(id),\" +\n                \"target \" + getByteArrayType() + \",\" +\n                \"direct_size BIGINT NOT NULL,\" +\n                \"CONSTRAINT uniq UNIQUE (writer_id)\" +\n                \");\"+\n                \"CREATE TABLE IF NOT EXISTS ownedkeys (\" +\n                \"parent_id INTEGER REFERENCES writers(id),\" +\n                \"owned_id INTEGER REFERENCES writers(id),\" +\n                \"PRIMARY KEY (parent_id, owned_id)\" +\n                \");\";\n    }\n\n    default void createTable(String sqlTableCreate, Connection conn) throws SQLException {\n        Statement createStmt = conn.createStatement();\n        createStmt.executeUpdate(sqlTableCreate);\n        createStmt.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sql/SqliteCommands.java",
    "content": "package peergos.server.sql;\n\npublic class SqliteCommands implements SqlSupplier {\n\n    @Override\n    public String vacuumCommand() {\n        return \"VACUUM;\";\n    }\n\n    @Override\n    public String listTablesCommand() {\n        return \"SELECT NAME FROM sqlite_master WHERE type='table';\";\n    }\n\n    @Override\n    public String tableExistsCommand() {\n        return \"SELECT name FROM sqlite_master WHERE type='table' AND name=?;\";\n    }\n\n    @Override\n    public String addMetadataCommand() {\n        return \"INSERT OR IGNORE INTO blockmetadata (owner, cid, version, size, links, batids) VALUES(?, ?, ?, ?, ?, ?);\";\n    }\n\n    @Override\n    public String updateMetadataCommand() {\n        return \"UPDATE blockmetadata SET owner=? WHERE cid=?;\";\n    }\n\n    @Override\n    public String setMetadataVersionAndOwnerCommand() {\n        return \"UPDATE blockmetadata SET version=?, owner=? WHERE cid=?;\";\n    }\n\n    @Override\n    public String createFollowRequestsTableCommand() {\n        return \"CREATE TABLE IF NOT EXISTS followrequests (id integer primary key autoincrement, \" +\n                \"name text not null, followrequest text not null);\";\n    }\n\n    @Override\n    public String ensureColumnExistsCommand(String table, String column, String type) {\n        return \"ALTER TABLE \" + table + \" ADD COLUMN \" + column + \" \" + type + \";\";\n    }\n\n    @Override\n    public String insertTransactionCommand() {\n        return \"INSERT OR IGNORE INTO transactions (tid, owner, hash, time) VALUES (?, ?, ?, ?);\";\n    }\n\n    @Override\n    public String insertServerIdCommand() {\n        return \"INSERT OR IGNORE INTO serverids (peerid, record) VALUES (?, ?);\";\n    }\n\n    @Override\n    public String insertOrIgnoreCommand(String prefix, String suffix) {\n        return prefix + \"OR IGNORE \" + suffix + \";\";\n    }\n\n    @Override\n    public String getByteArrayType() {\n        return \"blob\";\n    }\n\n    @Override\n    public String getSerialIdType() {\n        return \"INTEGER\";\n    }\n\n    @Override\n    public String sqlInteger() {\n        return \"INTEGER\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/AuthedCachingStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.storage.auth.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class AuthedCachingStorage extends DelegatingStorage {\n    private final ContentAddressedStorage target;\n    private final Map<Multihash, byte[]> cache;\n    private final Map<Multihash, Boolean> legacyBlocks;\n    private final Map<Multihash, CompletableFuture<Optional<CborObject>>> pending;\n    private final Map<Multihash, CompletableFuture<Optional<byte[]>>> pendingRaw;\n    private final BlockRequestAuthoriser authoriser;\n    private final Hasher h;\n    private final Cid ourNodeId;\n    private final int maxValueSize, cacheSize;\n\n    public AuthedCachingStorage(ContentAddressedStorage target,\n                                BlockRequestAuthoriser authoriser,\n                                Hasher h,\n                                int cacheSize,\n                                int maxValueSize) {\n        super(target);\n        this.target = target;\n        this.ourNodeId = target.id().join();\n        this.authoriser = authoriser;\n        this.h = h;\n        this.cache = Collections.synchronizedMap(new LRUCache<>(cacheSize));\n        this.legacyBlocks = Collections.synchronizedMap(new LRUCache<>(cacheSize));\n        this.maxValueSize = maxValueSize;\n        this.cacheSize = cacheSize;\n        this.pending = Collections.synchronizedMap(new LRUCache<>(100));\n        this.pendingRaw = Collections.synchronizedMap(new LRUCache<>(100));\n    }\n\n    public Collection<byte[]> getCached() {\n        return cache.values();\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return target.blockStoreProperties();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new AuthedCachingStorage(target.directToOrigin(), authoriser, h, cacheSize, maxValueSize);\n    }\n\n    @Override\n    public void clearBlockCache() {\n        cache.clear();\n        target.clearBlockCache();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return target.put(owner, writer, signedHashes, blocks, tid)\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        if (block.length < maxValueSize)\n                            cache.put(res.get(i), block);\n                    }\n                    return res;\n                });\n    }\n\n    private CompletableFuture<byte[]> authoriseGet(Cid key, byte[] block, Optional<BatWithId> bat) {\n        if (key.isRaw() && bat.isEmpty() && legacyBlocks.containsKey(key))\n            return Futures.of(block);\n        return bat.map(b -> b.bat.generateAuth(key, ourNodeId, 300, S3Request.currentDatetime(), bat.get().id, h)\n                .thenApply(BlockAuth::encode)).orElse(Futures.of(\"\"))\n                .thenCompose(auth -> authoriser.allowRead(key, block, ourNodeId, auth))\n                .thenCompose(allow -> allow ? Futures.of(block) : Futures.errored(new Throwable(\"Unauthorised!\")));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        byte[] cachedBlock = cache.get(key);\n        if (cachedBlock != null)\n            return authoriseGet(key, cachedBlock, bat)\n                    .thenApply(res -> Optional.of(CborObject.fromByteArray(res)));\n\n        CompletableFuture<Optional<CborObject>> inProgress;\n        synchronized (pending) {\n            inProgress = pending.get(key);\n        }\n        if (inProgress != null) {\n            return inProgress\n                    .thenCompose(copt -> copt.isEmpty() ?\n                            Futures.of(Optional.empty()) :\n                            authoriseGet(key, copt.get().serialize(), bat)\n                                    .thenApply(b -> copt));\n        }\n\n        CompletableFuture<Optional<CborObject>> pipe = new CompletableFuture<>();\n        synchronized (pending) {\n            pending.put(key, pipe);\n        }\n\n        CompletableFuture<Optional<CborObject>> result = new CompletableFuture<>();\n        CompletableFuture<Optional<CborObject>> targetFuture;\n        try {\n            targetFuture = target.get(owner, key, bat);\n        } catch (Throwable t) {\n            synchronized (pending) {\n                pending.remove(key);\n            }\n            pipe.completeExceptionally(t);\n            result.completeExceptionally(t);\n            return result;\n        }\n        targetFuture.thenAccept(cborOpt -> {\n            if (cborOpt.isPresent()) {\n                byte[] value = cborOpt.get().toByteArray();\n                if (value.length > 0 && value.length < maxValueSize)\n                    cache.put(key, value);\n            }\n            synchronized (pending) {\n                pending.remove(key);\n            }\n            pipe.complete(cborOpt);\n            result.complete(cborOpt);\n        }).exceptionally(t -> {\n            synchronized (pending) {\n                pending.remove(key);\n            }\n            pipe.completeExceptionally(t);\n            result.completeExceptionally(t);\n            return null;\n        });\n        return result;\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return target.putRaw(owner, writer, signatures, blocks, tid, progressConsumer)\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        if (block.length < maxValueSize)\n                            cache.put(res.get(i), block);\n                        progressConsumer.accept((long)block.length);\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        byte[] cached = cache.get(key);\n        if (cached != null) {\n            return authoriseGet(key, cached, bat)\n                    .thenApply(res -> Optional.of(res));\n        }\n\n        CompletableFuture<Optional<byte[]>> inProgress;\n        synchronized (pendingRaw) {\n            inProgress = pendingRaw.get(key);\n        }\n        if (inProgress != null) {\n            return inProgress\n                    .thenCompose(opt -> opt.isEmpty() ?\n                            Futures.of(Optional.empty()) :\n                            authoriseGet(key, opt.get(), bat)\n                                    .thenApply(b -> opt));\n        }\n\n        CompletableFuture<Optional<byte[]>> pipe = new CompletableFuture<>();\n        synchronized (pendingRaw) {\n            pendingRaw.put(key, pipe);\n        }\n        CompletableFuture<Optional<byte[]>> targetFuture;\n        try {\n            targetFuture = target.getRaw(owner, key, bat);\n        } catch (Throwable t) {\n            synchronized (pendingRaw) {\n                pendingRaw.remove(key);\n            }\n            pipe.completeExceptionally(t);\n            return Futures.errored(t);\n        }\n        CompletableFuture<Optional<byte[]>> result = new CompletableFuture<>();\n        targetFuture.thenAccept(rawOpt -> {\n            if (rawOpt.isPresent()) {\n                byte[] value = rawOpt.get();\n                if (value.length > 0 && value.length < maxValueSize) {\n                    cache.put(key, value);\n                    if (bat.isEmpty() && key.isRaw())\n                        legacyBlocks.put(key, true);\n                }\n            }\n            synchronized (pendingRaw) {\n                pendingRaw.remove(key);\n            }\n            pipe.complete(rawOpt);\n            result.complete(rawOpt);\n        }).exceptionally(t -> {\n            synchronized (pendingRaw) {\n                pendingRaw.remove(key);\n            }\n            pipe.completeExceptionally(t);\n            result.completeExceptionally(t);\n            return null;\n        });\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/AuthedStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.storage.auth.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class AuthedStorage extends DelegatingDeletableStorage {\n    private final DeletableContentAddressedStorage target;\n    private final BlockRequestAuthoriser authoriser;\n    private final Hasher h;\n    private final Cid ourNodeId;\n    private final String linkHost;\n    private CoreNode pki;\n\n    public AuthedStorage(DeletableContentAddressedStorage target,\n                         BlockRequestAuthoriser authoriser,\n                         Cid ourNodeId,\n                         String linkHost,\n                         Hasher h) {\n        super(target);\n        this.target = target;\n        this.ourNodeId = ourNodeId;\n        this.linkHost = linkHost;\n        this.authoriser = authoriser;\n        this.h = h;\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(ourNodeId);\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return Futures.of(linkHost);\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n        target.setPki(pki);\n        this.pki = pki;\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        List<Multihash> peerIds = hasBlock(owner, hash) ?\n                Arrays.asList(ourNodeId) :\n                pki.getStorageProviders(owner);\n        return get(peerIds, owner, hash, bat, ourNodeId, h, true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, auth, persistBlock).thenApply(bopt -> bopt.map(CborObject::fromByteArray));\n    }\n\n    public static CompletableFuture<Optional<CborObject>> getWithAbsentMirrorBat(Throwable t,\n                                                                                 List<Multihash> peerIds,\n                                                                                 PublicKeyHash owner,\n                                                                                 Cid hash,\n                                                                                 Optional<BatWithId> bat,\n                                                                                 Cid ourId,\n                                                                                 Hasher h,\n                                                                                 DeletableContentAddressedStorage target) {\n        if (t.getMessage().contains(\"Unauthorised\")) {\n            if (! bat.get().id().isInline() && target.hasBlock(owner, hash)) {\n                // we are dealing with a mirror bat that we likely don't have locally, we can check the hash to verify it\n                return target.getRaw(peerIds, owner, hash, bat, ourId, h, false, false)\n                        .thenCompose(rawOpt -> {\n                            if (rawOpt.isEmpty())\n                                return Futures.errored(t);\n                            return BatId.sha256(bat.get().bat, h).thenCompose(hashedBat -> {\n                                List<BatId> blockBats = hash.isRaw() ?\n                                        Bat.getRawBlockBats(rawOpt.get()) :\n                                        Bat.getCborBlockBats(rawOpt.get());\n                                boolean correctMirrorBat = blockBats.stream().anyMatch(b -> b.equals(hashedBat));\n                                if (correctMirrorBat)\n                                    return Futures.of(Optional.of(CborObject.fromByteArray(rawOpt.get())));\n                                return Futures.errored(t);\n                            });\n                        });\n            }\n        }\n        return Futures.errored(t);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean  persistblock) {\n        if (bat.isEmpty())\n            return get(peerIds, owner, hash, \"\", persistblock);\n        return Futures.asyncExceptionally(() -> bat.get().bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, h)\n                .thenApply(BlockAuth::encode)\n                .thenCompose(auth -> get(peerIds, owner, hash, auth, persistblock)),\n                t -> getWithAbsentMirrorBat(t, peerIds, owner, hash, bat, ourId, h, this));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return getRaw(pki.getStorageProviders(owner), owner, hash, bat, ourNodeId, h, true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, auth, true, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        return getRaw(Arrays.asList(ourNodeId), owner, block, Optional.empty(), ourNodeId, h, false, true)\n                .thenApply(rawOpt -> BlockMetadataStore.extractMetadata(block, rawOpt.get()));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean doAuth, boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, auth, persistBlock).thenApply(bopt -> {\n            if (bopt.isEmpty())\n                return Optional.empty();\n            byte[] block = bopt.get();\n            if (doAuth && ! authoriser.allowRead(hash, block, id().join(), auth).join())\n                throw new IllegalStateException(\"Unauthorised!\");\n            return bopt;\n        });\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        return target.hasBlock(owner, hash);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        if (root.codec == Cid.Codec.Raw)\n            return CompletableFuture.completedFuture(Collections.emptyList());\n        return getRaw(Arrays.asList(ourNodeId), owner, root, Optional.empty(), ourNodeId, h, false, true)\n                .thenApply(opt -> opt.map(CborObject::fromByteArray))\n                .thenApply(opt -> opt\n                        .map(cbor -> cbor.links().stream().map(c -> (Cid) c).collect(Collectors.toList()))\n                        .orElse(Collections.emptyList())\n                );\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        if (! hasBlock(owner, root))\n            return Futures.errored(new IllegalStateException(\"Champ root not present locally: \" + root));\n        return getChampLookup(owner, root, caps, committedRoot, h);\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        return target.getAllBlockHashes(owner, useBlockstore);\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        target.getAllBlockHashVersions(owner, res);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return target.getOpenTransactionBlocks(owner);\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n        target.clearOldTransactions(owner, cutoffMillis);\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid hash) {\n        target.delete(owner, hash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/BlockBuffer.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic interface BlockBuffer {\n\n    CompletableFuture<Boolean> put(PublicKeyHash owner, Cid hash, byte[] data);\n\n    CompletableFuture<Optional<byte[]>> get(PublicKeyHash owner, Cid hash);\n\n    boolean hasBlock(PublicKeyHash owner, Cid hash);\n\n    CompletableFuture<Boolean> delete(PublicKeyHash owner, Cid hash);\n\n    void applyToAll(BiConsumer<PublicKeyHash, Cid> action);\n\n    void setPki(CoreNode pki);\n}\n"
  },
  {
    "path": "src/peergos/server/storage/BlockMetadata.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.io.ipfs.Cid;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class BlockMetadata {\n\n    public final int size;\n    public final List<Cid> links;\n    public final List<BatId> batids;\n\n    public BlockMetadata(int size, List<Cid> links, List<BatId> batids) {\n        this.size = size;\n        this.links = links;\n        this.batids = batids;\n    }\n\n    public static BlockMetadata fromJSON(Map<String, Object> json) {\n        int size = (Integer) json.get(\"size\");\n        List<Cid> links = ((List<String>) json.get(\"links\"))\n                .stream()\n                .map(Cid::decode)\n                .collect(Collectors.toList());\n        List<BatId> bats = ((List<String>) json.get(\"links\"))\n                .stream()\n                .map(Cid::decode)\n                .map(BatId::new)\n                .collect(Collectors.toList());;\n        return new BlockMetadata(size, links, bats);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/BlockMetadataStore.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic interface BlockMetadataStore {\n\n    Optional<BlockMetadata> get(Cid block);\n\n    List<Cid> hasBlocks(List<Cid> blocks);\n\n    Map<Cid, BlockMetadata> getAll(List<Cid> blocks);\n\n    /**\n     *\n     * @param block\n     * @return The owner for a block or empty if it is a legacy block\n     */\n    Optional<PublicKeyHash> getOwner(Cid block);\n\n    void setOwner(PublicKeyHash owner, Cid block);\n\n    void setOwnerAndVersion(PublicKeyHash owner, Cid block, String version);\n\n    void put(PublicKeyHash owner, Cid block, String version, BlockMetadata meta);\n\n    void remove(Cid block);\n\n    long size(PublicKeyHash owner);\n\n    boolean isEmpty();\n\n    void applyToAll(Consumer<Cid> consumer);\n\n    void applyToAllSizes(BiConsumer<Cid, Long> action);\n\n    Stream<BlockVersion> list(PublicKeyHash owner);\n\n    void listCbor(PublicKeyHash owner, Consumer<List<BlockVersion>> res);\n\n    default BlockMetadata put(PublicKeyHash owner, Cid block, String version, byte[] data) {\n        BlockMetadata meta = extractMetadata(block, data);\n        put(owner, block, version, meta);\n        return meta;\n    }\n\n    static BlockMetadata extractMetadata(Cid block, byte[] data) {\n        if (block.isRaw()) {\n            BlockMetadata meta = new BlockMetadata(data.length, Collections.emptyList(), Bat.getRawBlockBats(data));\n            return meta;\n        } else {\n            CborObject cbor = CborObject.fromByteArray(data);\n            List<Cid> links = cbor\n                    .links().stream()\n                    .map(h -> (Cid) h)\n                    .collect(Collectors.toList());\n            List<BatId> batIds = cbor instanceof CborObject.CborMap ?\n                    ((CborObject.CborMap) cbor).getList(\"bats\", BatId::fromCbor) :\n                    Collections.emptyList();\n            BlockMetadata meta = new BlockMetadata(data.length, links, batIds);\n            return meta;\n        }\n    }\n\n    void compact();\n}\n"
  },
  {
    "path": "src/peergos/server/storage/BlockVersion.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.*;\n\npublic class BlockVersion {\n    public final Cid cid;\n    public final String version;\n    public final boolean isLatest;\n\n    public BlockVersion(Cid cid, String version, boolean isLatest) {\n        this.cid = cid;\n        this.version = version;\n        this.isLatest = isLatest;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        BlockVersion that = (BlockVersion) o;\n        return Objects.equals(cid, that.cid) && Objects.equals(version, that.version);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(cid, version);\n    }\n\n    @Override\n    public String toString() {\n        if (version == null)\n            return cid.toString();\n        return cid.toString() + \":\" + version;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/CachingBlockMetadataStore.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.util.LRUCache;\n\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/**\n * An in-memory LRU cache over a BlockMetadataStore.\n * Avoids hitting the database for frequently-read block metadata (e.g. during authReads).\n */\npublic class CachingBlockMetadataStore implements BlockMetadataStore {\n\n    private final BlockMetadataStore target;\n    private final Map<Cid, BlockMetadata> cache;\n\n    public CachingBlockMetadataStore(BlockMetadataStore target, int cacheSize) {\n        this.target = target;\n        this.cache = Collections.synchronizedMap(new LRUCache<>(cacheSize));\n    }\n\n    @Override\n    public Optional<BlockMetadata> get(Cid block) {\n        BlockMetadata cached = cache.get(block);\n        if (cached != null)\n            return Optional.of(cached);\n        Optional<BlockMetadata> result = target.get(block);\n        result.ifPresent(m -> cache.put(block, m));\n        return result;\n    }\n\n    @Override\n    public Map<Cid, BlockMetadata> getAll(List<Cid> blocks) {\n        Map<Cid, BlockMetadata> result = new HashMap<>();\n        List<Cid> misses = new ArrayList<>();\n        for (Cid block : blocks) {\n            BlockMetadata cached = cache.get(block);\n            if (cached != null)\n                result.put(block, cached);\n            else\n                misses.add(block);\n        }\n        if (!misses.isEmpty()) {\n            Map<Cid, BlockMetadata> fromDb = target.getAll(misses);\n            cache.putAll(fromDb);\n            result.putAll(fromDb);\n        }\n        return result;\n    }\n\n    @Override\n    public List<Cid> hasBlocks(List<Cid> blocks) {\n        return target.hasBlocks(blocks);\n    }\n\n    @Override\n    public Optional<PublicKeyHash> getOwner(Cid block) {\n        return target.getOwner(block);\n    }\n\n    @Override\n    public void setOwner(PublicKeyHash owner, Cid block) {\n        target.setOwner(owner, block);\n    }\n\n    @Override\n    public void setOwnerAndVersion(PublicKeyHash owner, Cid block, String version) {\n        target.setOwnerAndVersion(owner, block, version);\n    }\n\n    @Override\n    public void put(PublicKeyHash owner, Cid block, String version, BlockMetadata meta) {\n        target.put(owner, block, version, meta);\n        cache.put(block, meta);\n    }\n\n    @Override\n    public void remove(Cid block) {\n        cache.remove(block);\n        target.remove(block);\n    }\n\n    @Override\n    public long size(PublicKeyHash owner) {\n        return target.size(owner);\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return target.isEmpty();\n    }\n\n    @Override\n    public void applyToAll(Consumer<Cid> action) {\n        target.applyToAll(action);\n    }\n\n    @Override\n    public void applyToAllSizes(BiConsumer<Cid, Long> action) {\n        target.applyToAllSizes(action);\n    }\n\n    @Override\n    public Stream<BlockVersion> list(PublicKeyHash owner) {\n        return target.list(owner);\n    }\n\n    @Override\n    public void listCbor(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        target.listCbor(owner, res);\n    }\n\n    @Override\n    public void compact() {\n        target.compact();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/CidVersionInfiniFilter.java",
    "content": "package peergos.server.storage;\n\nimport org.peergos.blockstore.filters.ChainedInfiniFilter;\nimport org.peergos.util.Logging;\n\nimport java.util.List;\nimport java.util.logging.Logger;\nimport peergos.shared.cbor.CborObject;\n\npublic class CidVersionInfiniFilter implements VersionFilter {\n\n    private static final Logger LOG = Logging.LOG();\n\n    private final ChainedInfiniFilter filter;\n\n    private CidVersionInfiniFilter(ChainedInfiniFilter filter) {\n        this.filter = filter;\n    }\n\n    private byte[] toBytes(BlockVersion v) {\n        return new CborObject.CborList(List.of(\n                new CborObject.CborByteArray(v.cid.toBytes()),\n                new CborObject.CborString(v.version)\n        )).serialize();\n    }\n\n    @Override\n    public boolean has(BlockVersion v) {\n        return filter.search(toBytes(v));\n    }\n\n    @Override\n    public BlockVersion add(BlockVersion v) {\n        filter.insert(toBytes(v), true);\n        return v;\n    }\n\n    public static CidVersionInfiniFilter build(long nBlocks, double falsePositiveRate) {\n        int nextPowerOfTwo = Math.max(17, (int) (1 + Math.log(nBlocks) / Math.log(2)));\n        double expansionAlpha = 0.8;\n        int bitsPerEntry = (int)(4 - Math.log(falsePositiveRate / expansionAlpha) / Math.log(2) + 1);\n        LOG.fine(\"Using infini filter of initial size \" + ((double)(bitsPerEntry * (1 << nextPowerOfTwo) / 8) / 1024 / 1024) + \" MiB\");\n        ChainedInfiniFilter infini = new ChainedInfiniFilter(nextPowerOfTwo, bitsPerEntry);\n        infini.set_expand_autonomously(true);\n        return new CidVersionInfiniFilter(infini);\n    }\n}\n\n"
  },
  {
    "path": "src/peergos/server/storage/DelayingStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class DelayingStorage implements ContentAddressedStorage {\n    private final ContentAddressedStorage source;\n    private final int readDelay, writeDelay;\n\n    public DelayingStorage(ContentAddressedStorage source, int readDelay, int writeDelay) {\n        this.source = source;\n        this.readDelay = readDelay;\n        this.writeDelay = writeDelay;\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return source.id();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return source.ids();\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return source.linkHost(owner);\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        sleep(writeDelay);\n        return source.startTransaction(owner);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        sleep(writeDelay);\n        return source.closeTransaction(owner, tid);\n    }\n\n    private static void sleep(int millis) {\n        try {\n            Thread.sleep(millis);\n        } catch (InterruptedException e) {}\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        sleep(writeDelay);\n        return source.put(owner, writer, signedHashes, blocks, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        sleep(writeDelay);\n        return source.putRaw(owner, writer, signatures, blocks, tid, progressConsumer);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid object, Optional<BatWithId> bat) {\n        try {\n            sleep(readDelay);\n            return source.getRaw(owner, object, bat);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        try {\n            sleep(readDelay);\n            return source.get(owner, hash, bat);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        sleep(4*readDelay);\n        return source.getChampLookup(owner, root, caps, committedRoot);\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        try {\n            sleep(4*readDelay);\n            return source.getSecretLink(link);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        return source.getLinkCounts(owner, after, mirrorBat);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        try {\n            return source.getSize(owner, block);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return source.getBlockCache();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/DelegatingDeletableStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.space.UsageStore;\nimport peergos.server.storage.auth.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class DelegatingDeletableStorage implements DeletableContentAddressedStorage {\n\n    private final DeletableContentAddressedStorage target;\n\n    public DelegatingDeletableStorage(DeletableContentAddressedStorage target) {\n        this.target = target;\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n        target.setPki(pki);\n    }\n\n    @Override\n    public void partitionByUser(UsageStore usage,\n                                JdbcIpnsAndSocial mutable,\n                                PublicKeyHash pkiKey) {\n        target.partitionByUser(usage, mutable, pkiKey);\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        return target.getAllBlockHashes(owner, useBlockstore);\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore) {\n        return target.getAllBlockHashes(useBlockstore);\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        target.getAllBlockHashVersions(owner, res);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return target.getOpenTransactionBlocks(owner);\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n        target.clearOldTransactions(owner, cutoffMillis);\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        return target.hasBlock(owner, hash);\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid hash) {\n        target.delete(owner, hash);\n    }\n\n    @Override\n    public void bulkDelete(PublicKeyHash owner, List<BlockVersion> blocks) {\n        target.bulkDelete(owner, blocks);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        return target.get(peerIds, owner, hash, auth, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, auth, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, bat, ourId, h, doAuth, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> mirror(String username, PublicKeyHash owner, PublicKeyHash writer, List<Multihash> peerIds, Optional<Cid> existing,\n                                               Optional<Cid> updated, Optional<BatWithId> mirrorBat, Cid ourNodeId,\n                                               NewBlocksProcessor newBlockProcessor, TransactionId tid, Hasher hasher) {\n        return target.mirror(username, owner, writer, peerIds, existing, updated, mirrorBat, ourNodeId, newBlockProcessor, tid, hasher);\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return target.blockStoreProperties();\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return target.linkHost(owner);\n    }\n\n    @Override\n    public void clearBlockCache() {\n        target.clearBlockCache();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return target.directToOrigin();\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n        return target.authReads(owner, blocks);\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                            PublicKeyHash writer,\n                                                            List<byte[]> signedHashes,\n                                                            List<Integer> blockSizes,\n                                                            List<List<BatId>> batIds,\n                                                            boolean isRaw,\n                                                            TransactionId tid) {\n        return target.authWrites(owner, writer, signedHashes, blockSizes, batIds, isRaw, tid);\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return target.id();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return target.ids();\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return target.startTransaction(owner);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return target.closeTransaction(owner, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return target.put(owner, writer, signedHashes, blocks, tid);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return target.get(owner, hash, bat);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signedHashes,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressCounter) {\n        return target.putRaw(owner, writer, signedHashes, blocks, tid, progressCounter);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return target.getRaw(owner, hash, bat);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean persistBlock) {\n        return target.get(peerIds, owner, hash, bat, ourId, h, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return target.getChampLookup(owner, root, caps, committedRoot);\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        return target.getSecretLink(link);\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        return target.getLinkCounts(owner, after, mirrorBat);\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        return target.downloadFragments(owner, hashes, bats, h, monitor, spaceIncreaseFactor);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        return target.getSize(owner, block);\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        return target.getIpnsEntry(signer);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        return target.getLinks(owner, root, peerids);\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        return target.getBlockMetadata(owner, block);\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return target.getBlockCache();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/DeletableContentAddressedStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.space.UsageStore;\nimport peergos.server.storage.auth.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** This interface is only used locally on a server and never exposed.\n *  These methods allow garbage collection and local mirroring to be implemented.\n *\n */\npublic interface DeletableContentAddressedStorage extends ContentAddressedStorage {\n\n    ExecutorService usagePool = Executors.newVirtualThreadPerTaskExecutor();\n\n    Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore);\n\n    Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore);\n\n    void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res);\n\n    default void getAllRawBlockVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        getAllBlockHashVersions(owner, all -> res.accept(all.stream().filter(v -> v.cid.isRaw()).collect(Collectors.toList())));\n    }\n\n    List<Cid> getOpenTransactionBlocks(PublicKeyHash owner);\n\n    void clearOldTransactions(PublicKeyHash owner, long cutoffMillis);\n\n    boolean hasBlock(PublicKeyHash owner, Cid hash);\n\n    void delete(PublicKeyHash owner, Cid block);\n\n    default void delete(PublicKeyHash owner, BlockVersion blockVersion) {\n        delete(owner, blockVersion.cid);\n    }\n\n    default Optional<BlockMetadataStore> getBlockMetadataStore() {\n        return Optional.empty();\n    }\n\n    default void bulkDelete(PublicKeyHash owner, List<BlockVersion> blockVersions) {\n        for (BlockVersion version : blockVersions) {\n            delete(owner, version);\n        }\n    }\n\n    void setPki(CoreNode pki);\n\n    void partitionByUser(UsageStore usage,\n                         JdbcIpnsAndSocial mutable,\n                         PublicKeyHash pkiKey);\n\n    /**\n     * @param peerIds\n     * @param hash\n     * @param persistBlock\n     * @return The data with the requested hash, deserialized into cbor, or Optional.empty() if no object can be found\n     */\n    CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock);\n\n    default CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds,\n                                                        PublicKeyHash owner,\n                                                        Cid hash,\n                                                        Optional<BatWithId> bat,\n                                                        Cid ourId,\n                                                        Hasher h,\n                                                        boolean persistBlock) {\n        if (bat.isEmpty())\n            return get(peerIds, owner, hash, \"\", persistBlock);\n        return bat.get().bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, h)\n                .thenApply(BlockAuth::encode)\n                .thenCompose(auth -> get(peerIds, owner, hash, auth, persistBlock));\n    }\n\n    /**\n     * Get a block of data that is not in ipld cbor format, just raw bytes\n     *\n     * @param peerIds\n     * @param hash\n     * @param persistBlock\n     * @return\n     */\n    CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                               PublicKeyHash owner,\n                                               Cid hash,\n                                               String auth,\n                                               boolean persistBlock);\n\n    default CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                       PublicKeyHash owner,\n                                                       Cid hash,\n                                                       String auth,\n                                                       boolean doAuth,\n                                                       boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, auth, persistBlock);\n    }\n\n    default CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                       PublicKeyHash owner,\n                                                       Cid hash,\n                                                       Optional<BatWithId> bat,\n                                                       Cid ourId,\n                                                       Hasher h,\n                                                       boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, bat, ourId, h, true, persistBlock);\n    }\n\n    default String generateAuth(Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h) {\n        return bat.isEmpty() ? \"\" : bat.get().bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, h)\n                .thenApply(BlockAuth::encode).join();\n    }\n\n    CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                               PublicKeyHash owner,\n                                               Cid hash,\n                                               Optional<BatWithId> bat,\n                                               Cid ourId,\n                                               Hasher h,\n                                               boolean doAuth,\n                                               boolean persistBlock);\n\n    default CompletableFuture<List<Cid>> mirror(String username,\n                                                PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<Multihash> peerIds,\n                                               Optional<Cid> existing,\n                                               Optional<Cid> updated,\n                                               Optional<BatWithId> mirrorBat,\n                                               Cid ourNodeId,\n                                               NewBlocksProcessor newBlockProcessor,\n                                               TransactionId tid,\n                                               Hasher hasher) {\n        return mirror(username, owner, writer, peerIds, existing, updated, mirrorBat, ourNodeId, newBlockProcessor, tid, hasher, this);\n    }\n\n    /**\n     * Ensure that local copies of all blocks in merkle tree referenced are present locally\n     *\n     * @param owner\n     * @param peerIds\n     * @param existing\n     * @param updated\n     * @return\n     */\n    static CompletableFuture<List<Cid>> mirror(String username,\n                                               PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<Multihash> peerIds,\n                                               Optional<Cid> existing,\n                                               Optional<Cid> updated,\n                                               Optional<BatWithId> mirrorBat,\n                                               Cid ourNodeId,\n                                               NewBlocksProcessor newBlockProcessor,\n                                               TransactionId tid,\n                                               Hasher hasher,\n                                               DeletableContentAddressedStorage storage) {\n        if (updated.isEmpty())\n            return Futures.of(Collections.emptyList());\n        Cid newRoot = updated.get();\n        if (existing.equals(updated))\n            return Futures.of(Collections.singletonList(newRoot));\n        boolean isRaw = newRoot.isRaw();\n\n        Optional<byte[]> newVal = RetryStorage.runWithRetry(3, () -> storage.getRaw(peerIds, owner, newRoot, mirrorBat, ourNodeId, hasher, false, true)).join();\n        if (newVal.isEmpty())\n            throw new IllegalStateException(\"Couldn't retrieve block: \" + newRoot);\n        newBlockProcessor.process(writer, List.of(newRoot), newVal.get().length);\n        if (isRaw)\n            return Futures.of(Collections.singletonList(newRoot));\n\n        CborObject newBlock = CborObject.fromByteArray(newVal.get());\n        List<Cid> newLinks = newBlock.links().stream()\n                .filter(h -> !h.isIdentity())\n                .map(m -> (Cid) m)\n                .collect(Collectors.toList());\n        List<Cid> existingLinks = existing.map(h -> storage.getLinks(owner, h, Arrays.asList(ourNodeId)).join()\n                        .stream()\n                        .filter(c -> ! c.isIdentity())\n                        .collect(Collectors.toList()))\n                .orElse(Collections.emptyList());\n\n        return storage.bulkMirror(owner, writer, peerIds, existingLinks, newLinks, mirrorBat, ourNodeId,\n                (peers, o, cs, b) -> storage.bulkGetLinks(peerIds, owner, ourNodeId, cs, mirrorBat, hasher),\n                newBlockProcessor, tid, hasher);\n    }\n\n    default CompletableFuture<List<Cid>> bulkMirror(PublicKeyHash owner,\n                                                    PublicKeyHash writer,\n                                                    List<Multihash> peerIds,\n                                                    List<Cid> existing,\n                                                    List<Cid> updated,\n                                                    Optional<BatWithId> mirrorBat,\n                                                    Cid ourNodeId,\n                                                    P2pBlockGet retriever,\n                                                    NewBlocksProcessor newBlockProcessor,\n                                                    TransactionId tid,\n                                                    Hasher hasher) {\n        if (updated.isEmpty())\n            return Futures.of(updated);\n        Set<Cid> common = new HashSet<>(existing);\n        common.retainAll(updated);\n\n        List<Cid> removed = existing.stream()\n                .filter(x -> ! common.contains(x))\n                .filter(c -> ! c.isIdentity())\n                .collect(Collectors.toList());\n        List<Cid> added = updated.stream()\n                .filter(x -> ! common.contains(x))\n                .filter(c -> ! c.isIdentity())\n                .collect(Collectors.toList());\n\n        List<BlockMetadata> addedLinks = retriever.bulkGet(peerIds, owner, added, mirrorBat);\n        newBlockProcessor.process(writer, added, addedLinks.stream().mapToInt(p -> p.size).sum());\n        if (removed.isEmpty()) {\n            List<Cid> allCbor = addedLinks.stream()\n                    .map(p -> p.links)\n                    .flatMap(Collection::stream)\n                    .filter(c -> !c.isIdentity() && !c.isRaw())\n                    .collect(Collectors.toList());\n            for (int i=0; i < allCbor.size();) {\n                int end = Math.min(allCbor.size(), i + 100);\n                bulkMirror(owner, writer, peerIds, Collections.emptyList(), allCbor.subList(i, end), mirrorBat, ourNodeId, retriever, newBlockProcessor, tid, hasher);\n                i = end;\n            }\n            List<Cid> allRaw = addedLinks.stream()\n                    .map(p -> p.links)\n                    .flatMap(Collection::stream)\n                    .filter(c -> !c.isIdentity() && c.isRaw())\n                    .collect(Collectors.toList());\n            for (int i=0; i < allRaw.size();i++) {\n                bulkMirror(owner, writer, peerIds, Collections.emptyList(), allRaw.subList(i, i+1), mirrorBat, ourNodeId, retriever, newBlockProcessor, tid, hasher);\n            }\n        } else {\n            for (int i = 0; i < added.size(); i++) {\n                List<Cid> newLinks = addedLinks.get(i).links;\n                List<Cid> existingLinks = i >= removed.size() ?\n                        Collections.emptyList() :\n                        getLinks(owner, removed.get(i), Arrays.asList(ourNodeId)).join().stream()\n                                .filter(c -> !c.isIdentity())\n                                .collect(Collectors.toList());\n                bulkMirror(owner, writer, peerIds, existingLinks, newLinks, mirrorBat, ourNodeId, retriever, newBlockProcessor, tid, hasher);\n            }\n        }\n        return Futures.of(updated);\n    }\n\n    default List<BlockMetadata> bulkGetLinks(List<Multihash> peerIds,\n                                             PublicKeyHash owner,\n                                             Cid ourId,\n                                             List<Cid> blocks,\n                                             Optional<BatWithId> mirrorBat,\n                                             Hasher h) {\n        return blocks.stream()\n                .map(c -> BlockMetadataStore.extractMetadata(c, getRaw(peerIds, owner, c, mirrorBat, ourId, h, true).join().get()))\n                .collect(Collectors.toList());\n    }\n\n    /**\n     * Get all the merkle-links referenced directly from this object\n     * @param root The hash of the object whose links we want\n     * @return A list of the multihashes referenced with ipld links in this object\n     */\n    CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids);\n\n    default CompletableFuture<Long> getRecursiveBlockSize(PublicKeyHash owner, Cid block, List<Multihash> peerids) {\n        return getLinks(owner, block, peerids).thenCompose(links -> {\n            List<CompletableFuture<Long>> subtrees = links.stream()\n                    .filter(m -> ! m.isIdentity())\n                    .map(c -> Futures.runAsync(() -> getRecursiveBlockSize(owner, c, peerids), usagePool))\n                    .collect(Collectors.toList());\n            return getSize(owner, block)\n                    .thenCompose(sizeOpt -> {\n                        CompletableFuture<Long> reduced = Futures.reduceAll(subtrees,\n                                0L, (t, fut) -> fut.thenApply(x -> x + t), (a, b) -> a + b);\n                        return reduced.thenApply(sum -> sum + sizeOpt.orElse(0));\n                    });\n        });\n    }\n\n    default CompletableFuture<Long> getChangeInContainedSize(PublicKeyHash owner, Optional<Cid> original, Cid updated) {\n        if (! original.isPresent())\n            return getRecursiveBlockSize(owner, updated, Arrays.asList(id().join()));\n        return getChangeInContainedSize(owner, original.get(), updated);\n    }\n\n    CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block);\n\n    default CompletableFuture<Long> getChangeInContainedSize(PublicKeyHash owner, Cid original, Cid updated) {\n        return getBlockMetadata(owner, original)\n                .thenCompose(before -> getBlockMetadata(owner, updated).thenCompose(after -> {\n                    int objectDelta = after.size - before.size;\n                    List<Cid> beforeLinks = before.links.stream().filter(c -> !c.isIdentity()).collect(Collectors.toList());\n                    List<Cid> onlyBefore = new ArrayList<>(beforeLinks);\n                    onlyBefore.removeAll(after.links);\n                    List<Cid> afterLinks = after.links.stream().filter(c -> !c.isIdentity()).collect(Collectors.toList());\n                    List<Cid> onlyAfter = new ArrayList<>(afterLinks);\n                    onlyAfter.removeAll(before.links);\n\n                    int nPairs = Math.min(onlyBefore.size(), onlyAfter.size());\n                    List<Pair<Cid, Cid>> pairs = IntStream.range(0, nPairs)\n                            .mapToObj(i -> new Pair<>(onlyBefore.get(i), onlyAfter.get(i)))\n                            .collect(Collectors.toList());\n\n                    List<Cid> extraBefore = onlyBefore.subList(nPairs, onlyBefore.size());\n                    List<Cid> extraAfter = onlyAfter.subList(nPairs, onlyAfter.size());\n\n                    CompletableFuture<Long> beforeRes = Futures.runAsync(() -> getAllRecursiveSizes(owner, extraBefore), usagePool);\n                    CompletableFuture<Long> afterRes = Futures.runAsync(() -> getAllRecursiveSizes(owner, extraAfter), usagePool);\n                    CompletableFuture<Long> pairsRes = Futures.runAsync(() -> getSizeDiff(owner, pairs), usagePool);\n                    return beforeRes.thenCompose(priorSize -> afterRes.thenApply(postSize -> postSize - priorSize + objectDelta))\n                            .thenCompose(total -> pairsRes.thenApply(res -> res + total));\n                }));\n    }\n\n    private CompletableFuture<Long> getAllRecursiveSizes(PublicKeyHash owner, List<Cid> roots) {\n        List<CompletableFuture<Long>> allSizes = roots.stream()\n                .map(c -> Futures.runAsync(() -> getRecursiveBlockSize(owner, c, Arrays.asList(id().join())), usagePool))\n                .collect(Collectors.toList());\n        return Futures.reduceAll(allSizes,\n                0L,\n                (s, f) -> f.thenApply(size -> size + s),\n                (a, b) -> a + b);\n    }\n\n    private CompletableFuture<Long> getSizeDiff(PublicKeyHash owner, List<Pair<Cid, Cid>> pairs) {\n        List<CompletableFuture<Long>> pairDiffs = pairs.stream()\n                .map(p -> Futures.runAsync(() -> getChangeInContainedSize(owner, p.left, p.right), usagePool))\n                .collect(Collectors.toList());\n        return Futures.reduceAll(pairDiffs,\n                0L,\n                (s, f) -> f.thenApply(size -> size + s),\n                (a, b) -> a + b);\n    }\n\n    class HTTP extends ContentAddressedStorage.HTTP implements DeletableContentAddressedStorage {\n\n        private final HttpPoster poster;\n\n        public HTTP(HttpPoster poster, boolean isPeergosServer, Hasher hasher) {\n            super(poster, isPeergosServer, hasher);\n            this.poster = poster;\n        }\n\n        @Override\n        public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n            String jsonStream = new String(poster.postUnzip(apiPrefix + REFS_LOCAL +\n                    \"?use-block-store=\" + useBlockstore + \"&owner=\" + owner, new byte[0], -1).join());\n            return JSONParser.parseStream(jsonStream).stream()\n                    .map(m -> (String) (((Map) m).get(\"Ref\")))\n                    .map(Cid::decode).map(c -> new Pair<>(owner, c));\n        }\n\n        @Override\n        public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore) {\n            String jsonStream = new String(poster.postUnzip(apiPrefix + REFS_LOCAL +\n                    \"?use-block-store=\" + useBlockstore, new byte[0], -1).join());\n            return JSONParser.parseStream(jsonStream).stream()\n                    .map(m -> new Pair<>(\n                            PublicKeyHash.fromString((String) (((Map) m).get(\"Owner\"))),\n                            Cid.decode((String) (((Map) m).get(\"Ref\")))));\n        }\n\n        @Override\n        public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n            res.accept(getAllBlockHashes(owner, false)\n                    .map(p -> new BlockVersion(p.right, null, true))\n                    .collect(Collectors.toList()));\n        }\n\n        @Override\n        public void delete(PublicKeyHash owner, Cid hash) {\n            poster.get(apiPrefix + BLOCK_RM + \"?arg=\" + hash.toString() + \"&owner=\" + owner).join();\n        }\n\n        @Override\n        public void bulkDelete(PublicKeyHash owner, List<BlockVersion> blocks) {\n            Map<String, Object> json = new HashMap<>();\n            json.put(\"cids\", blocks.stream()\n                    .map(v -> v.cid.toString())\n                    .collect(Collectors.toList()));\n            json.put(\"owner\", owner.toString());\n            poster.postUnzip(apiPrefix + BLOCK_RM_BULK, JSONParser.toString(json).getBytes(), -1);\n        }\n\n        @Override\n        public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n            throw new IllegalStateException(\"Unimplemented!\");\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n            throw new IllegalStateException(\"Unimplemented!\");\n        }\n\n        @Override\n        public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n            return poster.get(apiPrefix + BLOCK_PRESENT + \"?arg=\" + hash.toString() +\n                            \"&owner=\" + owner)\n                    .thenApply(raw -> new String(raw).equals(\"true\")).join();\n        }\n\n        @Override\n        public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n            throw new IllegalStateException(\"Unimplemented!\");\n        }\n\n        @Override\n        public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n            throw new IllegalStateException(\"Unimplemented!\");\n        }\n\n        @Override\n        public void setPki(CoreNode pki) {}\n\n        @Override\n        public void partitionByUser(UsageStore usage,\n                                    JdbcIpnsAndSocial mutable,\n                                    PublicKeyHash pkiKey) {}\n\n        @Override\n        public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n            if (hash.isIdentity())\n                return CompletableFuture.completedFuture(Optional.of(CborObject.fromByteArray(hash.getHash())));\n            if (peerIds.isEmpty())\n                throw new IllegalStateException(\"Empty peer list for block \"+hash+\"!\");\n            return poster.get(apiPrefix + BLOCK_GET + \"?stream-channels=true&arg=\" + hash\n                            + (peerIds.isEmpty() ? \"\" : \"&peers=\" + peerIds.stream().map(p -> p.bareMultihash().toBase58()).collect(Collectors.joining(\",\")))\n                            + \"&owner=\" + owner\n                            + \"&auth=\" + auth\n                            + \"&persist=\" + persistBlock)\n                    .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(CborObject.fromByteArray(raw)));\n        }\n\n        @Override\n        public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                          PublicKeyHash owner,\n                                                          Cid hash,\n                                                          Optional<BatWithId> bat,\n                                                          Cid ourId,\n                                                          Hasher h,\n                                                          boolean doAuth,\n                                                          boolean persistBlock) {\n            if (hash.isIdentity())\n                return CompletableFuture.completedFuture(Optional.of(hash.getHash()));\n            if (peerIds.isEmpty())\n                throw new IllegalStateException(\"Empty peer list for block \"+hash+\"!\");\n            return poster.get(apiPrefix + BLOCK_GET + \"?stream-channels=true&arg=\" + hash\n                            + (peerIds.isEmpty() ? \"\" : \"&peers=\" + peerIds.stream().map(p -> p.bareMultihash().toBase58()).collect(Collectors.joining(\",\")))\n                            + \"&owner=\" + owner\n                            + bat.map(b -> \"&bat=\" + b.encode()).orElse(\"\")\n                            + \"&persist=\" + persistBlock)\n                    .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(raw));\n        }\n\n        @Override\n        public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n            if (hash.isIdentity())\n                return CompletableFuture.completedFuture(Optional.of(hash.getHash()));\n            if (peerIds.isEmpty())\n                throw new IllegalStateException(\"Empty peer list for block \"+hash+\"!\");\n            return poster.get(apiPrefix + BLOCK_GET + \"?stream-channels=true&arg=\" + hash\n                            + (peerIds.isEmpty() ? \"\" : \"&peers=\" + peerIds.stream().map(p -> p.bareMultihash().toBase58()).collect(Collectors.joining(\",\")))\n                            + \"&owner=\" + owner\n                            + \"&auth=\" + auth\n                            + \"&persist=\" + persistBlock)\n                    .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(raw));\n        }\n    }\n\n    public static CompletableFuture<Set<PublicKeyHash>> getOwnedKeysRecursive(String username,\n                                                                              CoreNode core,\n                                                                              MutablePointers mutable,\n                                                                              CommittedWriterData.Retriever retriever,\n                                                                              ContentAddressedStorage dht,\n                                                                              Hasher hasher) {\n        List<UserPublicKeyLink> chain = core.getChain(username).join();\n        if (chain.isEmpty())\n            return Futures.of(Collections.emptySet());\n        UserPublicKeyLink last = chain.get(chain.size() - 1);\n        PublicKeyHash owner = last.owner;\n        return getOwnedKeysRecursive(owner, owner, mutable, retriever, dht, hasher);\n    }\n\n    public static CompletableFuture<Set<PublicKeyHash>> getOwnedKeysRecursive(PublicKeyHash owner,\n                                                                              PublicKeyHash writer,\n                                                                              MutablePointers mutable,\n                                                                              CommittedWriterData.Retriever retriever,\n                                                                              ContentAddressedStorage ipfs,\n                                                                              Hasher hasher) {\n        return getOwnedKeysRecursive(owner, writer, Collections.emptySet(), mutable, retriever, ipfs, hasher);\n    }\n\n    private static CompletableFuture<Set<PublicKeyHash>> getOwnedKeysRecursive(PublicKeyHash owner,\n                                                                               PublicKeyHash writer,\n                                                                               Set<PublicKeyHash> alreadyDone,\n                                                                               MutablePointers mutable,\n                                                                               CommittedWriterData.Retriever retriever,\n                                                                               ContentAddressedStorage ipfs,\n                                                                               Hasher hasher) {\n        return getDirectOwnedKeys(owner, writer, mutable, retriever, ipfs, hasher)\n                .thenCompose(directOwned -> {\n                    Set<PublicKeyHash> newKeys = directOwned.stream().\n                            filter(h -> ! alreadyDone.contains(h))\n                            .collect(Collectors.toSet());\n                    Set<PublicKeyHash> done = new HashSet<>(alreadyDone);\n                    done.add(writer);\n                    BiFunction<Set<PublicKeyHash>, PublicKeyHash, CompletableFuture<Set<PublicKeyHash>>> composer =\n                            (a, w) -> getOwnedKeysRecursive(owner, w, a, mutable, retriever, ipfs, hasher)\n                                    .thenApply(ws ->\n                                            Stream.concat(ws.stream(), a.stream())\n                                                    .collect(Collectors.toSet()));\n                    return Futures.reduceAll(newKeys, done,\n                            composer,\n                            (a, b) -> Stream.concat(a.stream(), b.stream())\n                                    .collect(Collectors.toSet()));\n                });\n    }\n\n    public static CompletableFuture<Set<PublicKeyHash>> getDirectOwnedKeys(PublicKeyHash owner,\n                                                                           PublicKeyHash writer,\n                                                                           MutablePointers mutable,\n                                                                           CommittedWriterData.Retriever retriever,\n                                                                           ContentAddressedStorage ipfs,\n                                                                           Hasher hasher) {\n        return mutable.getPointerTarget(owner, writer, ipfs)\n                .thenCompose(h -> getDirectOwnedKeys(owner, writer, h.updated, retriever, ipfs, hasher));\n    }\n\n    public static CompletableFuture<Set<PublicKeyHash>> getDirectOwnedKeys(PublicKeyHash owner,\n                                                                           PublicKeyHash writer,\n                                                                           MaybeMultihash root,\n                                                                           CommittedWriterData.Retriever retriever,\n                                                                           ContentAddressedStorage ipfs,\n                                                                           Hasher hasher) {\n        if (! root.isPresent())\n            return CompletableFuture.completedFuture(Collections.emptySet());\n\n        BiFunction<Set<OwnerProof>, Pair<PublicKeyHash, OwnerProof>, CompletableFuture<Set<OwnerProof>>>\n                composer = (acc, pair) -> CompletableFuture.completedFuture(Stream.concat(acc.stream(), Stream.of(pair.right))\n                .collect(Collectors.toSet()));\n\n        BiFunction<Set<PublicKeyHash>, OwnerProof, CompletableFuture<Set<PublicKeyHash>>> proofComposer =\n                (acc, proof) -> proof.getAndVerifyOwner(owner, ipfs)\n                        .thenApply(claimedWriter -> Stream.concat(acc.stream(), claimedWriter.equals(writer) ?\n                                Stream.of(proof.ownedKey) :\n                                Stream.empty()).collect(Collectors.toSet()));\n\n        return retriever.getWriterData((Cid)root.get(), Optional.empty())\n                .thenCompose(wd -> wd.props.get().applyToOwnedKeys(owner, owned ->\n                                owned.applyToAllMappings(owner, Collections.emptySet(), composer, ipfs), ipfs, hasher)\n                        .thenApply(owned -> Stream.concat(owned.stream(),\n                                wd.props.get().namedOwnedKeys.values().stream()).collect(Collectors.toSet())))\n                .thenCompose(all -> Futures.reduceAll(all, Collections.emptySet(),\n                        proofComposer,\n                        (a, b) -> Stream.concat(a.stream(), b.stream())\n                                .collect(Collectors.toSet())));\n    }\n\n    static CompletableFuture<CommittedWriterData> getWriterData(List<Multihash> peerIds,\n                                                                PublicKeyHash owner,\n                                                                Cid hash,\n                                                                Optional<Long> sequence,\n                                                                boolean persistBlock,\n                                                                Cid ourId,\n                                                                Hasher h,\n                                                                DeletableContentAddressedStorage dht) {\n        return dht.get(peerIds, owner, hash, Optional.empty(), ourId, h, persistBlock)\n                .thenApply(cborOpt -> {\n                    if (! cborOpt.isPresent())\n                        throw new IllegalStateException(\"Couldn't retrieve WriterData from dht! \" + hash);\n                    return new CommittedWriterData(MaybeMultihash.of(hash), WriterData.fromCbor(cborOpt.get()), sequence);\n                });\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/DirectS3Proxy.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.space.UsageStore;\nimport peergos.server.util.HttpUtil;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.BatWithId;\nimport peergos.shared.storage.auth.S3Request;\nimport peergos.shared.user.fs.EncryptedCapability;\nimport peergos.shared.user.fs.SecretLink;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.LRUCache;\nimport peergos.shared.util.Pair;\nimport peergos.shared.util.ProgressConsumer;\n\nimport javax.net.ssl.SSLException;\nimport java.io.IOException;\nimport java.net.SocketException;\nimport java.net.SocketTimeoutException;\nimport java.net.http.HttpClient;\nimport java.time.Duration;\nimport java.time.LocalDateTime;\nimport java.time.ZonedDateTime;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Random;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Supplier;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\nimport static peergos.server.storage.S3BlockStorage.isRateLimitedException;\n\n/** This can be used for direct (read-only) blockstore access when mirroring a server from S3 to glacier\n *\n */\npublic class DirectS3Proxy implements ContentAddressedStorageProxy {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    private final String region, bucket, host, folder;\n    private final String accessKeyId, secretKey;\n    private final Optional<String> storageClass;\n    private final boolean useHttps, startWithLegacyPath;\n    private final Cid remoteId;\n    private final Hasher h;\n    private final UsageStore usage;\n    private final Random rnd = new Random();\n\n    public DirectS3Proxy(S3Config config,\n                         Cid remoteId,\n                         UsageStore usage,\n                         boolean startWithLegacyPath,\n                         Hasher h) {\n        this.remoteId = remoteId;\n        this.usage = usage;\n        this.h = h;\n        this.region = config.region;\n        this.bucket = config.bucket;\n        this.host = config.getHost();\n        this.useHttps = ! host.endsWith(\"localhost\") && ! host.contains(\"localhost:\");\n        this.folder = (useHttps ? \"\" : bucket + \"/\") + (config.path.isEmpty() || config.path.endsWith(\"/\") ? config.path : config.path + \"/\");\n        this.startWithLegacyPath = startWithLegacyPath;\n        this.storageClass = config.storageClass;\n        this.accessKeyId = config.accessKey;\n        this.secretKey = config.secretKey;\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(Multihash targetServerId, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        if (! remoteId.equals(targetServerId))\n            throw new IllegalStateException(\"Can only proxy to the target instance!\");\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(hash.getHash()));\n        return getWithBackoff(() -> getRawWithoutBackoff(List.of(targetServerId), owner, hash, startWithLegacyPath))\n                .thenApply(p -> p.map(v -> v.left));\n    }\n\n    private final LRUCache<PublicKeyHash, String> ownerToUser = new LRUCache<>(1000);\n\n    private String ownerToPrefix(PublicKeyHash owner) {\n        // legacy data all starts with AFK or AFY, usernames start with a lowercase letter\n        // We want to be able to efficiently list all legacy blocks\n        // Achieve this by listing from B which is after A and before lowercase\n        if (owner == null)\n            return \"\";\n        String cached = ownerToUser.get(owner);\n        if (cached != null)\n            return cached + \"/\";\n        String username = usage.getOwner(owner);\n        ownerToUser.put(owner, username);\n        return username + \"/\";\n    }\n\n    private static String legacyHashToKey(Multihash hash) {\n        return DirectS3BlockStore.hashToKey(hash);\n    }\n\n    private String hashToKey(PublicKeyHash owner, Multihash hash) {\n        if (owner == null)\n            return legacyHashToKey(hash);\n        return ownerToPrefix(owner) + DirectS3BlockStore.hashToKey(hash);\n    }\n\n    private CompletableFuture<Optional<Pair<byte[], String>>> getRawWithoutBackoff(List<Multihash> peerIds,\n                                                                                   PublicKeyHash owner,\n                                                                                   Cid hash,\n                                                                                   boolean useLegacyPath) {\n        String path = folder + (useLegacyPath ? legacyHashToKey(hash) : hashToKey(owner, hash));\n        PresignedUrl getUrl = S3Request.preSignGet(path, Optional.of(600), Optional.empty(),\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, storageClass, accessKeyId, secretKey, useHttps, h).join();\n        try {\n            Pair<byte[], String> blockAndVersion = HttpUtil.getWithVersion(getUrl);\n            return Futures.of(Optional.of(blockAndVersion));\n        } catch (SocketTimeoutException | SSLException | SocketException e) {\n            // S3 can't handle the load so treat this as a rate limit and slow down\n            throw new RateLimitException();\n        } catch (IOException e) {\n            String msg = e.getMessage();\n            boolean rateLimited = isRateLimitedException(e);\n            if (rateLimited) {\n                throw new RateLimitException();\n            }\n            boolean notFound = msg.contains(\"<Code>NoSuchKey</Code>\");\n            if (!notFound) {\n                LOG.warning(\"Remote S3 error reading \" + path);\n                LOG.log(Level.WARNING, msg, e);\n            }\n            if (notFound && ! useLegacyPath)\n                return getRawWithoutBackoff(peerIds, owner, hash, true);\n            throw new RuntimeException(e);\n        }\n    }\n\n    private <V> V getWithBackoff(Supplier<V> req) {\n        long sleep = 100 + rnd.nextInt(100);\n        for (int i=0; i < 10; i++) {\n            try {\n                return req.get();\n            } catch (RateLimitException e) {\n                LOG.info(e.getMessage());\n                try {\n                    Thread.sleep(sleep + rnd.nextInt(1_000));\n                } catch (InterruptedException f) {}\n                sleep *= 2;\n            }\n        }\n        throw new IllegalStateException(\"Couldn't process request because of rate limit!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(Multihash targetServerId, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return getRaw(targetServerId, owner, hash, bat)\n                .thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(Multihash targetServerId, PublicKeyHash owner) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(Multihash targetServerId, PublicKeyHash owner) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(Multihash targetServerId, PublicKeyHash owner, TransactionId tid) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(Multihash targetServerId, PublicKeyHash owner, Multihash root, List<ChunkMirrorCap> caps) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(Multihash targetServerId, SecretLink link) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(Multihash targetServerId, String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(Multihash targetServerId, PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signatures, List<byte[]> blocks, TransactionId tid) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(Multihash targetServerId, PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signatures, List<byte[]> blocks, TransactionId tid, ProgressConsumer<Long> progressConsumer) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/FileBlockBuffer.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.space.UsageStore;\nimport peergos.server.util.Logging;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\n/** A local file based block cache LRU\n *\n */\npublic class FileBlockBuffer implements BlockBuffer {\n    private static final Logger LOG = Logging.LOG();\n    private final Path root;\n    private final UsageStore usage;\n\n    public FileBlockBuffer(Path root, UsageStore usage) {\n        this.root = root;\n        this.usage = usage;\n        File rootDir = root.toFile();\n        if (!rootDir.exists()) {\n            final boolean mkdirs = root.toFile().mkdirs();\n            if (!mkdirs)\n                throw new IllegalStateException(\"Unable to create directory \" + root);\n        }\n        if (!rootDir.isDirectory())\n            throw new IllegalStateException(\"File store path must be a directory! \" + root);\n    }\n\n    public void setPki(CoreNode pki) {\n    }\n\n    private Path getFilePath(PublicKeyHash owner, Cid h) {\n        String key = DirectS3BlockStore.hashToKey(h);\n\n        Path path = PathUtil.get(\"\")\n                .resolve(usage.getOwner(owner))\n                .resolve(key.substring(key.length() - 3, key.length() - 1))\n                .resolve(key + \".data\");\n        return path;\n    }\n\n    private Path getLegacyFilePath(Cid h) {\n        String key = DirectS3BlockStore.hashToKey(h);\n\n        Path path = PathUtil.get(\"\")\n                .resolve(key.substring(key.length() - 3, key.length() - 1))\n                .resolve(key + \".data\");\n        return path;\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> get(PublicKeyHash owner, Cid hash) {\n        try {\n            if (hash.isIdentity())\n                return Futures.of(Optional.of(hash.getHash()));\n            File file;\n            try {\n                Path path = owner == null ?\n                        getLegacyFilePath(hash) :\n                        getFilePath(owner, hash);\n                file = root.resolve(path).toFile();\n                if (!file.exists()) {\n                    return CompletableFuture.completedFuture(Optional.empty());\n                }\n            } catch (Exception e) {\n                // writer not present on this server\n                return CompletableFuture.completedFuture(Optional.empty());\n            }\n            try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {\n                byte[] block = Serialize.readFully(din);\n                return CompletableFuture.completedFuture(Optional.of(block));\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        try {\n            Path path = getFilePath(owner, hash);\n            File file = root.resolve(path).toFile();\n            return file.exists();\n        } catch (IllegalStateException e) {\n            return false;\n        }\n    }\n\n    @Override\n    public CompletableFuture<Boolean> put(PublicKeyHash owner, Cid hash, byte[] data) {\n        try {\n            Path filePath = getFilePath(owner, hash);\n            Path target = root.resolve(filePath);\n            Path parent = target.getParent();\n            File parentDir = parent.toFile();\n\n            if (! parentDir.exists())\n                Files.createDirectories(parent);\n\n            for (Path someParent = parent; !someParent.equals(root); someParent = someParent.getParent()) {\n                File someParentFile = someParent.toFile();\n                if (! someParentFile.canWrite()) {\n                    final boolean b = someParentFile.setWritable(true, false);\n                    if (!b)\n                        throw new IllegalStateException(\"Could not make \" + someParent + \", ancestor of \" + parentDir + \" writable\");\n                }\n            }\n            Path tmp = root.resolve(filePath.getFileName().toString() + \".tmp\");\n            Files.write(tmp, data, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE);\n            Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE);\n            return Futures.of(true);\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash h) {\n        Path path = getFilePath(owner, (Cid)h);\n        File file = root.resolve(path).toFile();\n        return CompletableFuture.completedFuture(file.exists() ? Optional.of((int) file.length()) : Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<Boolean> delete(PublicKeyHash owner, Cid h) {\n        Path path = getFilePath(owner, h);\n        File file = root.resolve(path).toFile();\n        if (file.exists())\n            file.delete();\n        return Futures.of(true);\n    }\n\n    public void applyToAll(BiConsumer<PublicKeyHash, Cid> processor) {\n        // FileContentAddressedStorage.getFilesRecursive expects paths with PublicKeyHash components,\n        // but FileBlockBuffer uses root/username/shard/HASH.data. Walk the structure directly.\n        String[] topEntries = root.toFile().list();\n        if (topEntries == null) return;\n        for (String topName : topEntries) {\n            Path topPath = root.resolve(topName);\n            File topDir = topPath.toFile();\n            if (!topDir.isDirectory()) continue;\n            try {\n                // Try resolving topName as a username to get the owner's PublicKeyHash\n                PublicKeyHash ownerKey = usage.getOwnerKey(topName);\n                // Walk shard subdirectories: root/username/shard/HASH.data\n                String[] shards = topDir.list();\n                if (shards == null) continue;\n                for (String shard : shards) {\n                    File shardDir = topPath.resolve(shard).toFile();\n                    if (!shardDir.isDirectory()) continue;\n                    String[] files = shardDir.list();\n                    if (files == null) continue;\n                    for (String filename : files) {\n                        if (filename.endsWith(\".data\")) {\n                            try {\n                                Cid cid = DirectS3BlockStore.keyToHash(filename.substring(0, filename.length() - 5));\n                                processor.accept(ownerKey, cid);\n                            } catch (Exception e) { /* skip unrecognised files */ }\n                        }\n                    }\n                }\n            } catch (Exception e) {\n                // Not a known username — treat as legacy shard directory: root/shard/HASH.data\n                String[] files = topDir.list();\n                if (files == null) continue;\n                for (String filename : files) {\n                    if (filename.endsWith(\".data\")) {\n                        try {\n                            Cid cid = DirectS3BlockStore.keyToHash(filename.substring(0, filename.length() - 5));\n                            processor.accept(null, cid);\n                        } catch (Exception ex) { /* skip unrecognised files */ }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/FileBlockCache.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.util.Logging;\nimport peergos.server.util.Threads;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\nimport java.nio.file.*;\nimport java.nio.file.attribute.*;\nimport java.security.SecureRandom;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\n/** A local file based block cache LRU\n *\n */\npublic class FileBlockCache implements BlockCache {\n    private static final Logger LOG = Logging.LOG();\n    private final Path root;\n    private volatile long maxSizeBytes;\n    private long lastSizeCheckTime = 0;\n    private AtomicLong totalSize = new AtomicLong(0);\n    private AtomicBoolean needToCommitSize = new AtomicBoolean(true);\n    private final SecureRandom rnd = new SecureRandom();\n\n    public FileBlockCache(Path root, long maxSizeBytes) {\n        this.root = root;\n        this.maxSizeBytes = getOrSetMaxSize(maxSizeBytes);\n        File rootDir = root.toFile();\n        if (!rootDir.exists()) {\n            final boolean mkdirs = root.toFile().mkdirs();\n            if (!mkdirs)\n                throw new IllegalStateException(\"Unable to create directory \" + root);\n        }\n        if (!rootDir.isDirectory())\n            throw new IllegalStateException(\"File store path must be a directory! \" + root);\n\n        File sizeFile = root.resolve(\"size.bin\").toFile();\n        if (sizeFile.exists()) {\n            try {\n                DataInputStream din = new DataInputStream(new FileInputStream(sizeFile));\n                long size = din.readLong();\n                din.close();\n                totalSize.set(size);\n                LOG.info(\"Loaded file block cache size from disk: \" + totalSize.get() / 1024 / 1024 + \" MiB\");\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        } else {\n            LOG.info(\"Listing file block cache...\");\n            long t0 = System.currentTimeMillis();\n            applyToAll((p, a) -> totalSize.addAndGet(a.size()));\n            long t1 = System.currentTimeMillis();\n            LOG.info(\"Finished listing file block cache in \" + (t1 - t0) / 1000 + \"s, total size \" + totalSize.get() / 1024 / 1024 + \" MiB\");\n        }\n        ForkJoinPool.commonPool().submit(() -> ensureWithinSizeLimit(maxSizeBytes));\n        Thread sizeCommitter = new Thread(this::sizeCommitter, \"FileBlockCache size\");\n        sizeCommitter.setDaemon(true);\n        sizeCommitter.start();\n    }\n\n    private long getOrSetMaxSize(long maxSizeBytes) {\n        Path json = root.resolve(\"config.json\");\n        try {\n            if (json.toFile().exists()) {\n                Map<String, Object> decoded = (Map<String, Object>) JSONParser.parse(new String(Files.readAllBytes(json)));\n                Object maxsize = decoded.get(\"maxsize\");\n                if (maxsize instanceof Integer)\n                    return (Integer) maxsize;\n                return (Long) maxsize;\n            } else {\n                json.getParent().toFile().mkdirs();\n                Files.write(json, (\"{\\\"maxsize\\\":\" + maxSizeBytes + \"}\").getBytes(\"UTF-8\"), StandardOpenOption.CREATE);\n                return maxSizeBytes;\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public long getMaxSize() {\n        return maxSizeBytes;\n    }\n\n    @Override\n    public void setMaxSize(long maxSizeBytes) {\n        this.maxSizeBytes = maxSizeBytes;\n        Path json = root.resolve(\"config.json\");\n        try {\n            Files.write(json, (\"{\\\"maxsize\\\":\" + maxSizeBytes + \"}\").getBytes(\"UTF-8\"));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private void sizeCommitter() {\n        while (true) {\n            if (needToCommitSize.get()) {\n                try {\n                    File sizeFile = root.resolve(\"size.bin\").toFile();\n                    DataOutputStream dout = new DataOutputStream(new FileOutputStream(sizeFile));\n                    dout.writeLong(totalSize.get());\n                    dout.flush();\n                    dout.close();\n                    needToCommitSize.set(false);\n                } catch (Exception e) {\n                    LOG.log(Level.WARNING, e, () -> e.getMessage());\n                }\n            }\n            Threads.sleep(30_000);\n        }\n    }\n\n    private Path getFilePath(Cid h) {\n        String key = DirectS3BlockStore.hashToKey(h);\n\n        Path path = PathUtil.get(\"\")\n                .resolve(key.substring(key.length() - 3, key.length() - 1))\n                .resolve(key + \".data\");\n        return path;\n    }\n\n    /**\n     * Remove all files stored as part of this FileContentAddressedStorage.\n     */\n    public void remove() {\n        root.toFile().delete();\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> get(Cid hash) {\n        try {\n            if (hash.isIdentity())\n                return Futures.of(Optional.of(hash.getHash()));\n            Path path = getFilePath(hash);\n            File file = root.resolve(path).toFile();\n            if (! file.exists()){\n                return CompletableFuture.completedFuture(Optional.empty());\n            }\n            try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {\n                byte[] block = Serialize.readFully(din);\n                return CompletableFuture.completedFuture(Optional.of(block));\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public boolean hasBlock(Cid hash) {\n        Path path = getFilePath(hash);\n        File file = root.resolve(path).toFile();\n        return file.exists();\n    }\n\n    public CompletableFuture<Boolean> put(Cid hash, byte[] data) {\n        try {\n            Path filePath = getFilePath(hash);\n            Path target = root.resolve(filePath);\n            if (target.toFile().exists())\n                return Futures.of(true);\n            Path parent = target.getParent();\n            File parentDir = parent.toFile();\n\n            if (! parentDir.exists())\n                Files.createDirectories(parent);\n\n            for (Path someParent = parent; !someParent.equals(root); someParent = someParent.getParent()) {\n                File someParentFile = someParent.toFile();\n                if (! someParentFile.canWrite()) {\n                    final boolean b = someParentFile.setWritable(true, false);\n                    if (!b)\n                        throw new IllegalStateException(\"Could not make \" + someParent + \", ancestor of \" + parentDir + \" writable\");\n                }\n            }\n            Path tmp = target.getParent().resolve(target.getFileName() + \"-\" + rnd.nextInt(Integer.MAX_VALUE) + \".tmp\");\n            Files.write(tmp, data, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE);\n            Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE);\n            totalSize.addAndGet(data.length);\n            if (lastSizeCheckTime < System.currentTimeMillis() - 30_000) {\n                lastSizeCheckTime = System.currentTimeMillis();\n                ForkJoinPool.commonPool().submit(() -> ensureWithinSizeLimit(maxSizeBytes));\n            }\n            needToCommitSize.set(true);\n            return Futures.of(true);\n        } catch (IOException e) {\n            String msg = e.getMessage();\n            // handle running out of inodes by clearing cache\n            if (msg != null && msg.toLowerCase(Locale.ROOT).contains(\"space\")) {\n                LOG.info(\"Clearing block cache after running out of space/inodes...\");\n                clear();\n                return Futures.of(false);\n            }\n            throw new RuntimeException(msg, e);\n        }\n    }\n\n    public CompletableFuture<Optional<Integer>> getSize(Multihash h) {\n        Path path = getFilePath((Cid)h);\n        File file = root.resolve(path).toFile();\n        return CompletableFuture.completedFuture(file.exists() ? Optional.of((int) file.length()) : Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<Boolean> clear() {\n        applyToAll((p, a) -> delete(p.toFile()));\n        return Futures.of(true);\n    }\n\n    public void delete(Multihash h) {\n        Path path = getFilePath((Cid)h);\n        File file = root.resolve(path).toFile();\n        delete(file);\n    }\n\n    private void delete(File file) {\n        if (file.exists()) {\n            long size = file.length();\n            file.delete();\n            totalSize.addAndGet(-size);\n            Path parent = file.toPath().getParent();\n            File[] files = parent.toFile().listFiles();\n            if (files != null && files.length == 0) {\n                parent.toFile().delete();\n            }\n        }\n    }\n\n    public Optional<Long> getLastAccessTimeMillis(Cid h) {\n        Path path = getFilePath(h);\n        File file = root.resolve(path).toFile();\n        if (! file.exists())\n            return Optional.empty();\n        try {\n            BasicFileAttributes attrs = Files.readAttributes(root.resolve(path), BasicFileAttributes.class);\n            FileTime time = attrs.lastAccessTime();\n            return Optional.of(time.toMillis());\n        } catch (NoSuchFileException nope) {\n            return Optional.empty();\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    public void applyToAll(BiConsumer<Path, BasicFileAttributes> processor) {\n        try {\n            Files.walkFileTree(root, new FileVisitor<Path>() {\n                @Override\n                public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attr) throws IOException {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                @Override\n                public FileVisitResult visitFile(Path path, BasicFileAttributes attr) throws IOException {\n                    if (path.getFileName().toString().endsWith(\".data\")) {\n                        processor.accept(path, attr);\n                    }\n                    return FileVisitResult.CONTINUE;\n                }\n\n                @Override\n                public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                @Override\n                public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException {\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private AtomicBoolean cleaning = new AtomicBoolean(false);\n\n    public void ensureWithinSizeLimit(long maxSize) {\n        if (totalSize.get() <= maxSize || cleaning.get())\n            return;\n        if (! cleaning.compareAndSet(false, true))\n            return;\n        // delete files randomly, don't bother trying to sort by access time\n        Logging.LOG().info(\"Starting FileBlockCache reduction from \" + totalSize.get());\n        applyToAll((p, a) -> {\n            if (totalSize.get() > maxSize / 2) {\n                delete(p.toFile());\n                totalSize.addAndGet(-a.size());\n            }\n        });\n        Logging.LOG().info(\"Reduced FileBlockCache down to \" + totalSize.get());\n        cleaning.set(false);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/FileContentAddressedStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.corenode.IpfsCoreNode;\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.space.UsageStore;\nimport peergos.server.storage.auth.*;\nimport peergos.server.util.Logging;\nimport peergos.server.util.Threads;\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.mutable.PointerUpdate;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.CommittedWriterData;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.nio.file.attribute.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\n/** A local directory implementation of ContentAddressedStorage.\n *\n */\npublic class FileContentAddressedStorage implements DeletableContentAddressedStorage {\n    private static final Logger LOG = Logging.LOG();\n    private static final int CID_V1 = 1;\n    private final Path root;\n    private final TransactionStore transactions;\n    private final BlockRequestAuthoriser authoriser;\n    private final PartitionStatus partitionStatus;\n    private final Hasher hasher;\n    private final Cid ourId;\n    private volatile CoreNode pki;\n\n    public FileContentAddressedStorage(Path root,\n                                       Cid ourId,\n                                       TransactionStore transactions,\n                                       BlockRequestAuthoriser authoriser,\n                                       PartitionStatus partitioned,\n                                       Hasher hasher) {\n        this.root = root;\n        this.ourId = ourId;\n        this.transactions = transactions;\n        this.authoriser = authoriser;\n        this.partitionStatus = partitioned;\n        this.hasher = hasher;\n        File rootDir = root.toFile();\n        if (!rootDir.exists()) {\n            final boolean mkdirs = root.toFile().mkdirs();\n            if (!mkdirs)\n                throw new IllegalStateException(\"Unable to create directory \" + root);\n            partitionStatus.complete();\n        }\n        if (!rootDir.isDirectory())\n            throw new IllegalStateException(\"File store path must be a directory! \" + root);\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n        this.pki = pki;\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    private boolean userPartitioningComplete() {\n        return partitionStatus.isDone();\n    }\n\n    @Override\n    public void partitionByUser(UsageStore usage,\n                                JdbcIpnsAndSocial mutable,\n                                PublicKeyHash pkiKey) {\n        if (userPartitioningComplete()) {\n            LOG.info(\"Blockstore already partitioned.\");\n            return;\n        }\n        new Thread(() -> {\n            LOG.info(\"Partitioning blockstore by user. Please wait...\");\n            System.out.println(\"Partitioning blockstore by user. Please wait...\");\n            List<Triple<Multihash, String, PublicKeyHash>> allTargets = usage.getAllTargets();\n            for (Triple<Multihash, String, PublicKeyHash> target : allTargets) {\n                moveSubtreeToOwner(target.right, (Cid) target.left, List.of(ourId));\n            }\n            Map<PublicKeyHash, byte[]> allPointers = mutable.getAllEntries();\n            PublicKeyHash pkiOwner = pki.getPublicKeyHash(\"peergos\").join().get();\n\n            allPointers.forEach((writerHash, rawPointer) -> {\n                PublicKeyHash owner = writerHash.equals(pkiKey) ? pkiOwner : usage.getOwnerKey(writerHash);\n                PublicSigningKey writer = getSigningKey(null, writerHash).join().get();\n                byte[] bothHashes = writer.unsignMessage(rawPointer).join();\n                PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n                MaybeMultihash updated = cas.updated;\n\n                if (updated.isPresent())\n                    moveSubtreeToOwner(owner, (Cid) updated.get(), List.of(ourId));\n            });\n\n            LOG.info(\"Partitioning blockstore completed.\");\n            System.out.println(\"Partitioning blockstore completed.\");\n            partitionStatus.complete();\n        }).start();\n    }\n\n    private void moveSubtreeToOwner(PublicKeyHash owner, Cid root, List<Multihash> ourIds) {\n        moveLegacyBlockToOwner(owner, root);\n        List<Cid> links = getLinks(owner, root, ourIds).join();\n        for (Cid link : links) {\n            moveSubtreeToOwner(owner, link, ourIds);\n        }\n    }\n\n    private void moveLegacyBlockToOwner(PublicKeyHash owner, Cid block) {\n        if (block.isIdentity())\n            return;\n        Path oldPath = root.resolve(getLegacyFilePath(block));\n        File oldFile = oldPath.toFile();\n        if (oldFile.exists()) {\n            Path newPath = root.resolve(getFilePath(owner, block));\n            newPath.getParent().toFile().mkdirs();\n            try {\n                Files.move(oldPath, newPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return CompletableFuture.completedFuture(ourId);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return CompletableFuture.completedFuture(List.of(ourId));\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return Futures.of(\"localhost:8000\");\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return CompletableFuture.completedFuture(transactions.startTransaction(owner));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        transactions.closeTransaction(owner, tid);\n        return CompletableFuture.completedFuture(true);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        if (! hasBlock(owner, root))\n            return Futures.errored(new IllegalStateException(\"Champ root not present locally: \" + root));\n        return getChampLookup(owner, root, caps, committedRoot, hasher);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return transactions.getOpenTransactionBlocks(owner);\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n        transactions.clearOldTransactions(owner, cutoffMillis);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return put(owner, writer, signedHashes, blocks, false, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return put(owner, writer, signatures, blocks, true, tid);\n    }\n\n    private CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                             PublicKeyHash writer,\n                                             List<byte[]> signatures,\n                                             List<byte[]> blocks,\n                                             boolean isRaw,\n                                             TransactionId tid) {\n        return CompletableFuture.completedFuture(blocks.stream()\n                .map(b -> put(b, isRaw, tid, owner))\n                .collect(Collectors.toList()));\n    }\n\n    private Path getFilePath(PublicKeyHash owner, Cid h) {\n        String key = DirectS3BlockStore.hashToKey(h);\n        if (owner == null) { // legacy block lookup\n            return PathUtil.get(\"\")\n                .resolve(key.substring(key.length() - 3, key.length() - 1))\n                .resolve(key + \".data\");\n        }\n\n        Path path = PathUtil.get(\"\")\n                .resolve(pki.getUsername(owner).join())\n                .resolve(key.substring(key.length() - 3, key.length() - 1))\n                .resolve(key + \".data\");\n        return path;\n    }\n\n    private static Path getLegacyFilePath(Cid h) {\n        String key = DirectS3BlockStore.hashToKey(h);\n\n        Path path = PathUtil.get(\"\")\n                .resolve(key.substring(key.length() - 3, key.length() - 1))\n                .resolve(key + \".data\");\n        return path;\n    }\n\n    /**\n     * Remove all files stored as part of this FileContentAddressedStorage.\n     */\n    public void remove() {\n        root.toFile().delete();\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        if (hash.codec == Cid.Codec.Raw)\n            throw new IllegalStateException(\"Need to call getRaw if cid is not cbor!\");\n        return getRaw(Collections.emptyList(), owner, hash, auth, persistBlock).thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return get(Collections.emptyList(), owner, hash, bat, id().join(), hasher, false);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(hash.getHash()));\n        Path path = getFilePath(owner, hash);\n        File file = root.resolve(path).toFile();\n        if (! file.exists()) {\n            if (userPartitioningComplete())\n                return CompletableFuture.completedFuture(Optional.empty());\n            file = root.resolve(getLegacyFilePath(hash)).toFile();\n            if (! file.exists())\n                return CompletableFuture.completedFuture(Optional.empty());\n        }\n        try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {\n            byte[] block = Serialize.readFully(din);\n\n            String auth = bat.isEmpty() ? \"\" :\n                    bat.get().bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, h)\n                    .thenApply(BlockAuth::encode).join();\n            if (doAuth && ! authoriser.allowRead(hash, block, id().join(), auth).join())\n                return Futures.errored(new IllegalStateException(\"Unauthorised!\"));\n            return CompletableFuture.completedFuture(Optional.of(block));\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        try {\n            if (hash.isIdentity())\n                return Futures.of(Optional.of(hash.getHash()));\n            Path path = getFilePath(owner, hash);\n            File file = root.resolve(path).toFile();\n            if (! file.exists()){\n                boolean isPartitioned = userPartitioningComplete();\n                if (isPartitioned)\n                    return CompletableFuture.completedFuture(Optional.empty());\n                file = root.resolve(getLegacyFilePath(hash)).toFile();\n                if (! file.exists())\n                    return CompletableFuture.completedFuture(Optional.empty());\n            }\n            try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {\n                byte[] block = Serialize.readFully(din);\n\n                String auth = bat.isEmpty() ? \"\" :\n                        bat.get().bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, hasher)\n                                .thenApply(BlockAuth::encode).join();\n                if (! authoriser.allowRead(hash, block, id().join(), auth).join())\n                    return Futures.errored(new IllegalStateException(\"Unauthorised!\"));\n                return CompletableFuture.completedFuture(Optional.of(block));\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, auth, true, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean doAuth, boolean persistBlock) {\n        try {\n            if (hash.isIdentity())\n                return Futures.of(Optional.of(hash.getHash()));\n            Path path = getFilePath(owner, hash);\n            File file = root.resolve(path).toFile();\n            if (! file.exists()) {\n                boolean isPartitioned = userPartitioningComplete();\n                if (isPartitioned)\n                    return CompletableFuture.completedFuture(Optional.empty());\n                file = root.resolve(getLegacyFilePath(hash)).toFile();\n                if (! file.exists())\n                    return CompletableFuture.completedFuture(Optional.empty());\n            }\n            try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {\n                byte[] block = Serialize.readFully(din);\n                if (doAuth && ! authoriser.allowRead(hash, block, id().join(), auth).join())\n                    return Futures.errored(new IllegalStateException(\"Unauthorised!\"));\n                return CompletableFuture.completedFuture(Optional.of(block));\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        Path path = getFilePath(owner, hash);\n        File file = root.resolve(path).toFile();\n        boolean isPartitioned = userPartitioningComplete();\n        boolean exists = file.exists();\n        if (isPartitioned) {\n            return exists;\n        }\n        if (exists)\n            return true;\n        Path legacyFilePath = getLegacyFilePath(hash);\n        File legacyFile = root.resolve(legacyFilePath).toFile();\n        return legacyFile.exists();\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        return getRaw(Arrays.asList(id().join()), owner, block, Optional.empty(), ourId, hasher, true)\n                .thenApply(rawOpt -> BlockMetadataStore.extractMetadata(block, rawOpt.get()));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        if (root.codec == Cid.Codec.Raw)\n            return CompletableFuture.completedFuture(Collections.emptyList());\n        return getRaw(peerids, owner, root, Optional.empty(), ourId, hasher, false, false)\n                .thenApply(opt -> opt.map(CborObject::fromByteArray))\n                .thenApply(opt -> opt\n                        .map(cbor -> cbor.links().stream().map(c -> (Cid) c).collect(Collectors.toList()))\n                        .orElse(Collections.emptyList())\n                );\n    }\n\n    public Cid put(byte[] data, boolean isRaw, TransactionId tid, PublicKeyHash owner) {\n        try {\n            Cid cid = new Cid(CID_V1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor,\n                    Multihash.Type.sha2_256, RAMStorage.hash(data));\n            Path filePath = getFilePath(owner, cid);\n            Path target = root.resolve(filePath);\n            Path parent = target.getParent();\n            File parentDir = parent.toFile();\n\n            if (! parentDir.exists())\n                Files.createDirectories(parent);\n\n            for (Path someParent = parent; !someParent.equals(root); someParent = someParent.getParent()) {\n                File someParentFile = someParent.toFile();\n                if (! someParentFile.canWrite()) {\n                    final boolean b = someParentFile.setWritable(true, false);\n                    if (!b)\n                        throw new IllegalStateException(\"Could not make \" + someParent.toString() + \", ancestor of \" + parentDir.toString() + \" writable\");\n                }\n            }\n            transactions.addBlock(cid, tid, owner);\n            Files.write(target, data, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE);\n            return cid;\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    protected List<Pair<PublicKeyHash, Cid>> getFiles(Optional<PublicKeyHash> owner) {\n        List<Pair<PublicKeyHash, Cid>> existing = new ArrayList<>();\n        Path base = owner.map(o -> root.resolve(pki.getUsername(o).join())).orElse(root);\n        if (! base.toFile().exists())\n            return existing;\n        getFilesRecursive(base, (o, c) -> existing.add(new Pair<>(o, c)), root);\n        return existing;\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash h) {\n        Path path = getFilePath(owner, (Cid)h);\n        File file = root.resolve(path).toFile();\n        return CompletableFuture.completedFuture(file.exists() ? Optional.of((int) file.length()) : Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore) {\n        return getFiles(Optional.empty()).stream();\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        return getFiles(Optional.of(owner)).stream();\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        res.accept(getAllBlockHashes(owner, false)\n                .map(p -> new BlockVersion(p.right, null, true))\n                .collect(Collectors.toList()));\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid h) {\n        Path path = getFilePath(owner, h);\n        File file = root.resolve(path).toFile();\n        if (file.exists())\n            file.delete();\n    }\n\n    public void applyToAll(BiConsumer<PublicKeyHash, Cid> processor) {\n        getFilesRecursive(root, processor, root);\n    }\n\n    public static void getFilesRecursive(Path path, BiConsumer<PublicKeyHash, Cid> accumulator, Path root) {\n        File pathFile = path.toFile();\n        if (pathFile.isFile()) {\n            if (pathFile.getName().endsWith(\".data\")) {\n                String name = pathFile.getName();\n                Path fromRoot = path.relativize(root);\n                int nameCount = fromRoot.getNameCount();\n                PublicKeyHash owner = nameCount > 2 ?\n                        PublicKeyHash.fromString(fromRoot.getName(0).toString()) :\n                        null;\n                accumulator.accept(owner, DirectS3BlockStore.keyToHash(name.substring(0, name.length() - 5)));\n            }\n            return;\n        }\n        else if (!  pathFile.isDirectory())\n            throw new IllegalStateException(\"Specified path \"+ path +\" is not a file or directory\");\n\n        String[] filenames = pathFile.list();\n        if (filenames == null)\n            throw new IllegalStateException(\"Couldn't retrieve children of directory: \" + path);\n        for (String filename : filenames) {\n            Path child = path.resolve(filename);\n            if (child.toFile().isDirectory()) {\n                getFilesRecursive(child, accumulator, root);\n            } else if (filename.endsWith(\".data\")) {\n                try {\n                    String name = child.toFile().getName();\n                    Path fromRoot = path.relativize(root);\n                    int nameCount = fromRoot.getNameCount();\n                    PublicKeyHash owner = nameCount > 2 ?\n                            PublicKeyHash.fromString(fromRoot.getName(0).toString()) :\n                            null;\n                    accumulator.accept(owner, DirectS3BlockStore.keyToHash(name.substring(0, name.length() - 5)));\n                } catch (IllegalStateException e) {\n                    // ignore files who's name isn't a valid multihash\n                    LOG.info(\"Ignoring file \"+ child +\" since name is not of form $cid.data\");\n                }\n            }\n        }\n    }\n\n    public void migrateToOwnerPartitionedStore() {\n\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Shouldn't get here.\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Shouldn't get here.\");\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return Optional.empty();\n    }\n\n    @Override\n    public String toString() {\n        return \"FileContentAddressedStorage \" + root;\n    }\n\n    public static void main(String[] a) throws Exception {\n        // run this within the .peergos dir\n        Path root = Paths.get(\".ipfs/blocks\");\n        Path targetDir = Paths.get(\"protobuf-blocks\");\n        if (! targetDir.toFile().mkdir())\n            throw new IllegalStateException(\"Couldn't create target dir!\");\n        moveProtobufBlocks(root, targetDir);\n    }\n    public static void moveProtobufBlocks(Path root, Path targetDir) {\n        getFilesRecursive(root,\n                (Owner, cid) -> {\n                    if (cid.codec == Cid.Codec.DagProtobuf) {\n                        // move block\n                        String filename = DirectS3BlockStore.hashToKey(cid) + \".data\";\n                        Path path = getLegacyFilePath(cid);\n                        try {\n                            Files.move(root.resolve(path), targetDir.resolve(filename), StandardCopyOption.REPLACE_EXISTING);\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    }\n                },\n                root);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/GarbageCollector.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.corenode.*;\nimport peergos.server.space.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class GarbageCollector {\n    private static final Logger LOG = Logger.getGlobal();\n\n    private final DeletableContentAddressedStorage storage;\n    private final JdbcIpnsAndSocial pointers;\n    private final UsageStore usage;\n    private final BlockMetadataStore metadata;\n    private final boolean listRawFromBlockstore;\n    private final AtomicBoolean running = new AtomicBoolean(false);\n    private final Path reachabilityDbDir;\n    private final TriFunction<Long, Long, Long, CompletableFuture<Boolean>> deleteConfirm;\n\n    public GarbageCollector(DeletableContentAddressedStorage storage,\n                            JdbcIpnsAndSocial pointers,\n                            UsageStore usage,\n                            Path reachabilityDbDir,\n                            TriFunction<Long, Long, Long, CompletableFuture<Boolean>> deleteConfirm,\n                            boolean listRawFromBlockstore) {\n        this.storage = storage;\n        this.pointers = pointers;\n        this.usage = usage;\n        this.reachabilityDbDir = reachabilityDbDir;\n        this.deleteConfirm = deleteConfirm;\n        this.listRawFromBlockstore = listRawFromBlockstore;\n        this.metadata = storage.getBlockMetadataStore().orElseGet(RamBlockMetadataStore::new);\n    }\n\n    public synchronized void collect(Function<Stream<Map.Entry<PublicKeyHash, byte[]>>, CompletableFuture<Boolean>> snapshotSaver) {\n        collect(storage, pointers, usage, reachabilityDbDir, snapshotSaver, metadata, deleteConfirm, listRawFromBlockstore);\n    }\n\n    public void stop() {\n        running.set(false);\n    }\n\n    public void start(long periodMillis, Function<Stream<Map.Entry<PublicKeyHash, byte[]>>, CompletableFuture<Boolean>> snapshotSaver) {\n        running.set(true);\n        Thread garbageCollector = new Thread(() -> {\n            while (running.get()) {\n                try {\n                    collect(snapshotSaver);\n                } catch (Exception e) {\n                    LOG.log(Level.SEVERE, e, e::getMessage);\n                }\n                try {\n                    Thread.sleep(periodMillis);\n                } catch (InterruptedException ex) {\n                    Thread.currentThread().interrupt();\n                }\n            }\n        }, \"Garbage Collector\");\n        Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n            garbageCollector.interrupt();\n        }, \"Garbage Collector - shutdown\"));\n        garbageCollector.setDaemon(true);\n        garbageCollector.start();\n    }\n\n    private static void listBlocks(PublicKeyHash owner,\n                                   SqliteBlockReachability reachability,\n                                   CidVersionInfiniFilter inRdb,\n                                   boolean listFromBlockstore,\n                                   DeletableContentAddressedStorage storage,\n                                   BlockMetadataStore metadata) {\n        // the reachability store dedupes on cid + version to guarantee no duplicates which would result in data loss\n        if (listFromBlockstore)\n            storage.getAllBlockHashVersions(owner, versions -> reachability.addBlocks(versions.stream()\n                    .filter(v ->  !inRdb.has(v))\n                    .toList()));\n        else {\n            storage.getAllRawBlockVersions(owner, versions -> reachability.addBlocks(versions.stream()\n                    .filter(v ->  !inRdb.has(v))\n                    .toList()));\n            metadata.listCbor(owner, versions -> reachability.addBlocks(versions.stream()\n                    .filter(v ->  !inRdb.has(v))\n                    .toList()));\n        }\n    }\n\n    public static void checkIntegrity(DeletableContentAddressedStorage storage,\n                                      BlockMetadataStore metadata,\n                                      JdbcIpnsAndSocial pointers,\n                                      UsageStore usage,\n                                      CoreNode pki,\n                                      boolean fixMetadata,\n                                      Hasher h) {\n        Map<PublicKeyHash, byte[]> allPointers = pointers.getAllEntries();\n\n        List<Triple<Multihash, String, PublicKeyHash>> usageRoots = usage.getAllTargets();\n        Set<Multihash> done = new HashSet<>();\n        System.out.println(\"Checking integrity from pointer targets...\");\n        allPointers.forEach((writerHash, signedRawCas) -> {\n            PublicKeyHash owner = usage.getOwnerKey(writerHash);\n            PublicSigningKey writer = getWithBackoff(() -> storage.getSigningKey(owner, writerHash).join().get());\n            byte[] bothHashes = writer.unsignMessage(signedRawCas).join();\n            PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n            MaybeMultihash updated = cas.updated;\n            if (updated.isPresent() && !done.contains(updated.get())) {\n                done.add(updated.get());\n                try {\n                    String username = usage.getOwner(writerHash);\n                    Cid homeServer = (Cid) pki.getHomeServer(username).join().get();\n                    traverseDag(owner, updated.get(), homeServer, metadata, done, fixMetadata, storage, h);\n                } catch (Exception e) {\n                    try {\n                        String username = usage.getOwner(writerHash);\n                        String msg = \"Error marking reachable for user: \" + username + \", writer \" + writerHash + \" \" + e.getMessage();\n                        System.err.println(msg);\n                    } catch (Exception f) {\n//                        System.err.println(\"Error processing writer: \" + e.getMessage() + \" \" + f.getMessage());\n                    }\n                }\n            }\n        });\n        System.out.println(\"Checking integrity from usage roots...\");\n\n        for (Triple<Multihash, String, PublicKeyHash> usageRoot : usageRoots) {\n            if (! done.contains(usageRoot.left)) {\n                try {\n                    Cid homeServer = (Cid) pki.getHomeServer(usageRoot.middle).join().get();\n                    traverseDag(usageRoot.right, usageRoot.left, homeServer, metadata, done, fixMetadata, storage, h);\n                } catch (Exception e) {\n                    String username = usageRoot.middle;\n                    String msg = \"Error marking reachable for user: \" + username + \", from usage root \" + usageRoot.left;\n                    System.err.println(msg);\n                }\n            }\n        }\n        System.out.println(\"Finished checking block DAG integrity\");\n    }\n\n    public static void checkUserIntegrity(String username,\n                                          DeletableContentAddressedStorage storage,\n                                          BlockMetadataStore metadata,\n                                          JdbcIpnsAndSocial pointers,\n                                          UsageStore usage,\n                                          CoreNode pki,\n                                          boolean fixMetadata,\n                                          Hasher h) {\n        Set<PublicKeyHash> writers = usage.getAllWriters(username);\n        Set<Multihash> done = new HashSet<>();\n        System.out.println(\"Checking integrity for user \" + username);\n        Cid homeServer = (Cid) pki.getHomeServer(username).join().get();\n        PublicKeyHash owner = usage.getOwnerKey(writers.stream().findAny().get());\n        PublicKeyHash fromPki = pki.getPublicKeyHash(username).join().get();\n        if (! fromPki.equals(owner)) {\n            List<PublicKeyHash> identityKeys = pki.getChain(username).join()\n                    .stream()\n                    .map(c -> c.owner)\n                    .toList();\n            System.out.println(\"Owner mismatch! \" + owner + \" != \" + fromPki);\n            System.out.println(\"PKI chain \" + identityKeys);\n        }\n\n        Map<PublicKeyHash, byte[]> userPointers = writers.stream()\n                .map(w -> new Pair<>(w, pointers.getPointer(w).join()))\n                .filter(p -> p.right.isPresent())\n                .collect(Collectors.toMap(p -> p.left, p -> p.right.get()));\n        userPointers.forEach((writerHash, signedRawCas) -> {\n            PublicSigningKey writer = getWithBackoff(() -> storage.getSigningKey(null, writerHash).join().get());\n            byte[] bothHashes = writer.unsignMessage(signedRawCas).join();\n            PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n            MaybeMultihash updated = cas.updated;\n            if (updated.isPresent() && !done.contains(updated.get())) {\n                done.add(updated.get());\n                try {\n                    traverseDag(owner, updated.get(), homeServer, metadata, done, fixMetadata, storage, h);\n                } catch (Exception e) {\n                    String msg = \"Error marking reachable for user: \" + username + \", writer \" + writerHash + \" \" + e.getMessage();\n                    System.err.println(msg);\n                }\n            }\n        });\n        System.out.println(\"Finished checking block DAG integrity\");\n    }\n\n    private static void traverseDag(PublicKeyHash owner,\n                                    Multihash cid,\n                                    Cid homeServer,\n                                    BlockMetadataStore metadata,\n                                    Set<Multihash> done,\n                                    boolean fixMetadata,\n                                    DeletableContentAddressedStorage storage,\n                                    Hasher h) {\n        if (cid.isIdentity())\n            return;\n        Optional<BlockMetadata> meta = metadata.get((Cid) cid);\n        Cid ourId = storage.id().join();\n        if (meta.isEmpty() && fixMetadata) {\n            // retrieving the block should add it to the metadata store\n            Optional<byte[]> block = storage.getRaw(Arrays.asList(homeServer), owner, (Cid) cid, Optional.empty(), ourId, h, false, true).join();\n            meta = metadata.get((Cid) cid);\n            if (meta.isPresent())\n                System.out.println(\"Fixed block metadata for \" + cid);\n        }\n        Optional<PublicKeyHash> fromMeta = metadata.getOwner((Cid) cid);\n        if (fromMeta.isPresent() && ! fromMeta.get().equals(owner) && fixMetadata) {\n            metadata.setOwner(owner, (Cid) cid);\n        }\n\n        if (meta.isEmpty())\n            throw new IllegalStateException(\"Absent block! \" + cid + \", key: \" + DirectS3BlockStore.hashToKey(cid));\n        for (Cid link : meta.get().links) {\n            done.add(link);\n            traverseDag(owner, link, homeServer, metadata, done, fixMetadata, storage, h);\n        }\n    }\n\n    private static class PointerSnapshot implements Cborable {\n\n        public final Map<PublicKeyHash, Multihash> roots;\n\n        public PointerSnapshot(Map<PublicKeyHash, Multihash> roots) {\n            this.roots = roots;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (o == null || getClass() != o.getClass()) return false;\n            PointerSnapshot that = (PointerSnapshot) o;\n            return Objects.equals(roots, that.roots);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hashCode(roots);\n        }\n\n        @Override\n        public CborObject toCbor() {\n            return new CborObject.CborList(roots.entrySet()\n                .stream()\n                .sorted(Comparator.comparing(Map.Entry::getKey))\n                .flatMap(e -> Stream.of(e.getKey().toCbor(), new CborObject.CborMerkleLink(e.getValue())))\n                .collect(Collectors.toList()));\n        }\n\n        public static PointerSnapshot fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborList))\n                throw new IllegalStateException(\"Invalid cbor for PointerSnapshot!\");\n            CborObject.CborList list = (CborObject.CborList) cbor;\n            if (list.value.size() % 2 != 0)\n                throw new IllegalStateException(\"Invalid cbor list length for PointerSnapshot!\");\n            HashMap<PublicKeyHash, Multihash> res = new HashMap<>();\n            for (int i=0; i < list.value.size()/2; i++)\n                res.put(list.get(2*i, PublicKeyHash::fromCbor), list.get(2*i + 1, c -> ((CborObject.CborMerkleLink)c).target));\n            return new PointerSnapshot(res);\n        }\n    }\n\n    /** The result of this method is a snapshot of the mutable pointers that is consistent with the blocks store\n     * after GC has completed (saved to a file which can be independently backed up).\n     *\n     * @param storage\n     * @param pointers\n     * @param snapshotSaver\n     * @return\n     */\n    public static void collect(DeletableContentAddressedStorage storage,\n                               JdbcIpnsAndSocial pointers,\n                               UsageStore usage,\n                               Path reachabilityDbDir,\n                               Function<Stream<Map.Entry<PublicKeyHash, byte[]>>, CompletableFuture<Boolean>> snapshotSaver,\n                               BlockMetadataStore metadata,\n                               TriFunction<Long, Long, Long, CompletableFuture<Boolean>> deleteConfirm,\n                               boolean listFromBlockstore) {\n        long ts0 = System.currentTimeMillis();\n        LOG.info(\"Starting blockstore garbage collection on node \" + storage.id().join() + \"...\");\n        List<Pair<String, PublicKeyHash>> allUsers = usage.getAllOwners()\n                .stream()\n                .sorted(Comparator.comparing(a -> a.left))\n                .distinct()\n                .collect(Collectors.toList());\n        Set<String> currentUsers = allUsers.stream().map(p -> p.left).collect(Collectors.toSet());\n        if (currentUsers.size() != allUsers.size())\n            throw new IllegalStateException(\"Duplicate username getting all owners!\");\n        for (Pair<String, PublicKeyHash> p : allUsers) {\n            PublicKeyHash owner = p.right;\n            String username = p.left;\n            LOG.info(\"Starting GC for \" + username);\n            // TODO check if user snapshot hasn't changed and short circuit\n\n            // TODO: do GC in O(1) RAM with a bloom filter?: mark into bloom. Then list and check bloom to delete.\n            storage.clearOldTransactions(owner, System.currentTimeMillis() - 24*3600*1000L);\n            long t0 = System.nanoTime();\n            Path reachabilityDbFile = reachabilityDbDir.resolve(\"reachability\")\n                    .resolve(\"reachability-\" + username + \".sqlite\");\n            reachabilityDbFile.getParent().toFile().mkdirs();\n            Path snapshotFile = reachabilityDbDir.resolve(\"pointer-snapshots\")\n                    .resolve(username + \".cbor\");\n            snapshotFile.getParent().toFile().mkdirs();\n            // load snapshot\n            if (snapshotFile.toFile().exists()) {\n                try {\n                    PointerSnapshot loaded = PointerSnapshot.fromCbor(CborObject.fromByteArray(Files.readAllBytes(snapshotFile)));\n                    Set<PublicKeyHash> initialWriters = usage.getAllWriters(username);\n                    Map<PublicKeyHash, Multihash> allPointers = initialWriters.stream()\n                            .flatMap(w -> pointers.getPointer(w).join()\n                                    .flatMap(d -> parsePointerTarget(owner, w, d, storage)\n                                            .map(m -> Map.entry(w, m))).stream())\n                            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n                    PointerSnapshot initialUsageRoots = new PointerSnapshot(allPointers);\n                    if (loaded.equals(initialUsageRoots)) {\n                        LOG.info(\"No changes for \" + username);\n                        continue;\n                    }\n                } catch (IOException e) {\n                    LOG.log(Level.WARNING, e, e::getMessage);\n                }\n            }\n            SqliteBlockReachability reachability = SqliteBlockReachability.createReachabilityDb(reachabilityDbFile);\n            // First build a bloom (infini) filter of the block versions in RDB\n            // then use this to efficiently filter the blockstore listing\n            long nMetaBlocks = reachability.size();\n            reachability.clearReachable();\n            CidVersionInfiniFilter inRdb = CidVersionInfiniFilter.build(nMetaBlocks, 0.0001);\n            reachability.applyToAllVersions(versions -> versions.forEach(inRdb::add));\n            // Versions are only relevant for versioned S3 buckets, otherwise version is null\n            // For S3, clients write raw blocks directly, we need to get their version directly from S3\n            listBlocks(owner, reachability, inRdb, listFromBlockstore, storage, metadata);\n            long t1 = System.nanoTime();\n            long nBlocks = reachability.size();\n            LOG.info(\"Listing \" + nBlocks + \" blocks took \" + (t1 - t0) / 1_000_000_000 + \"s\");\n\n            List<Cid> pending = storage.getOpenTransactionBlocks(owner);\n            long t2 = System.nanoTime();\n            if (! pending.isEmpty())\n                LOG.info(\"Listing \" + pending.size() + \" pending blocks took \" + (t2 - t1) / 1_000_000_000 + \"s\");\n\n            // This pointers call must happen AFTER the block and pending listing for correctness\n            Set<PublicKeyHash> writers = usage.getAllWriters(username);\n            Map<PublicKeyHash, byte[]> allPointers = writers.stream()\n                    .flatMap(w -> pointers.getPointer(w).join()\n                            .map(d -> Map.entry(w, d)).stream())\n                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n            long t3 = System.nanoTime();\n            LOG.info(\"Listing \" + allPointers.size() + \" pointers took \" + (t3 - t2) / 1_000_000_000 + \"s\");\n\n            // Get the current roots from the usage store which shouldn't be GC'd until usage has been updated\n            List<Triple<Multihash, String, PublicKeyHash>> usageRoots = usage.getAllTargets(username);\n\n            int markParallelism = 10;\n            ForkJoinPool markPool = Threads.newFJPool(markParallelism, \"GC-mark-\");\n            AtomicLong totalReachable = new AtomicLong(0);\n            List<ForkJoinTask<Boolean>> usageMarked = usageRoots.stream()\n                    .map(r -> markPool.submit(() -> markReachable(owner, storage, (Cid) r.left, r.middle, reachability, metadata, totalReachable)))\n                    .collect(Collectors.toList());\n            usageMarked.forEach(f -> f.join());\n            long t4 = System.nanoTime();\n            long reachableAfterUsage = totalReachable.get();\n            LOG.info(\"Marking \" + reachableAfterUsage + \" reachable from \" + usageRoots.size() + \" usage roots took \" + (t4 - t3) / 1_000_000_000 + \"s\");\n\n            Set<Multihash> fromUsage = new HashSet<>(usageRoots.size());\n            fromUsage.addAll(usageRoots.stream().map(r -> r.left).collect(Collectors.toSet()));\n            List<ForkJoinTask<Boolean>> marked = allPointers.entrySet().stream()\n                    .map(e -> markPool.submit(() -> markReachable(owner, e.getKey(), e.getValue(), reachability, storage, usage, fromUsage, metadata, totalReachable)))\n                    .collect(Collectors.toList());\n            long rootsProcessed = marked.stream().filter(ForkJoinTask::join).count();\n            markPool.shutdown();\n\n            long t5 = System.nanoTime();\n            if (totalReachable.get() - reachableAfterUsage > 0)\n                LOG.info(\"Marking \" + (totalReachable.get() - reachableAfterUsage) + \" reachable from \" + rootsProcessed + \" pointers took \" + (t5 - t4) / 1_000_000_000 + \"s\");\n            reachability.setReachable(pending, totalReachable);\n\n            long t6 = System.nanoTime();\n            if (! pending.isEmpty())\n                LOG.info(\"Marking \" + pending.size() + \" pending blocks reachable took \" + (t6 - t5) / 1_000_000_000 + \"s\");\n\n            // Save pointers snapshot\n            snapshotSaver.apply(allPointers.entrySet().stream()).join();\n\n            AtomicLong cborDelCount = new AtomicLong(0);\n            AtomicLong rawDelCount = new AtomicLong(0);\n            reachability.getUnreachable(del -> {\n                cborDelCount.addAndGet(del.stream().filter(v -> ! v.cid.isRaw()).count());\n                rawDelCount.addAndGet(del.stream().filter(v -> v.cid.isRaw()).count());\n            });\n            boolean delete = deleteConfirm.apply(cborDelCount.get(), rawDelCount.get(), nBlocks).join();\n            if (! delete)\n                continue;\n\n            int deleteParallelism = 4;\n            long t7 = System.nanoTime();\n            ForkJoinPool pool = Threads.newFJPool(deleteParallelism, \"GC-delete-\");\n            AtomicLong progressCounter = new AtomicLong(0);\n            List<ForkJoinTask<Pair<Long, Long>>> futures = new ArrayList<>();\n            reachability.getUnreachable(toDel -> futures.add(pool.submit(() ->\n                    deleteUnreachableBlocks(owner, toDel, progressCounter, cborDelCount.get() + rawDelCount.get(), storage, metadata, reachability))));\n            Pair<Long, Long> deleted = futures.stream()\n                    .map(ForkJoinTask::join)\n                    .reduce((a, b) -> new Pair<>(a.left + b.left, a.right + b.right))\n                    .orElse(new Pair<>(0L, 0L));\n            pool.shutdown();\n            long deletedCborBlocks = deleted.left;\n            long deletedRawBlocks = deleted.right;\n            long t8 = System.nanoTime();\n            metadata.compact();\n            reachability.compact();\n            long t9 = System.nanoTime();\n            if (cborDelCount.get() + rawDelCount.get() > 0) {\n                LOG.info(\"Deleting blocks took \" + (t8 - t7) / 1_000_000_000 + \"s\");\n            }\n            // save snapshot to file\n            Map<PublicKeyHash, Multihash> allPointerTargets = allPointers.entrySet()\n                    .stream()\n                    .flatMap(e -> parsePointerTarget(owner, e.getKey(), e.getValue(), storage).map(m -> Map.entry(e.getKey(), m)).stream())\n                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n            PointerSnapshot gcedVersion = new PointerSnapshot(allPointerTargets);\n            try {\n                Files.write(snapshotFile, gcedVersion.serialize(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n            } catch (IOException e) {\n                LOG.log(Level.WARNING, e, e::getMessage);\n            }\n            LOG.info(\"GC complete. Freed \" + deletedCborBlocks + \" cbor blocks and \" + deletedRawBlocks +\n                    \" raw blocks, total duration: \" + (t8 - t7 + t6 - t0) / 1_000_000_000 + \"s, metadata.compact took \" + (t9 - t8) / 1_000_000_000 + \"s\");\n        }\n        long ts1 = System.currentTimeMillis();\n        LOG.info(\"Finished complete GC in \" + (ts1 - ts0)/1_000 + \"s\");\n    }\n\n    private static MaybeMultihash parsePointerTarget(PublicKeyHash owner,\n                                                     PublicKeyHash writerHash,\n                                                     byte[] signedRawCas,\n                                                     DeletableContentAddressedStorage storage) {\n        PublicSigningKey writer = getWithBackoff(() -> storage.getSigningKey(owner, writerHash).join().get());\n        byte[] bothHashes = writer.unsignMessage(signedRawCas).join();\n        PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n        return cas.updated;\n    }\n\n    private static boolean markReachable(PublicKeyHash owner,\n                                         PublicKeyHash writerHash,\n                                         byte[] signedRawCas,\n                                         SqliteBlockReachability reachability,\n                                         DeletableContentAddressedStorage storage,\n                                         UsageStore usage,\n                                         Set<Multihash> done,\n                                         BlockMetadataStore metadata,\n                                         AtomicLong totalReachable) {\n        try {\n            MaybeMultihash updated = parsePointerTarget(owner, writerHash, signedRawCas, storage);\n            if (updated.isPresent() && !done.contains(updated.get())) {\n                markReachable(owner, storage, true, new ArrayList<>(1000), (Cid) updated.get(), reachability, metadata, () -> getUsername(writerHash, usage), totalReachable);\n                return true;\n            }\n            return false;\n        } catch (Exception e) {\n            LOG.info(\"Error processing user \" + getUsername(writerHash, usage) + \" \" + e.getMessage());\n            LOG.log(Level.SEVERE, e, e::getMessage);\n            return false;\n        }\n    }\n\n    private static String getUsername(PublicKeyHash writer, UsageStore usage) {\n        try {\n            return usage.getOwner(writer);\n        } catch (Exception e) {\n            return \"Orphaned writer: \" + writer;\n        }\n    }\n\n    private static Pair<Long, Long> deleteUnreachableBlocks(PublicKeyHash owner,\n                                                            List<BlockVersion> toDelete,\n                                                            AtomicLong progress,\n                                                            long totalBlocksToDelete,\n                                                            DeletableContentAddressedStorage storage,\n                                                            BlockMetadataStore metadata,\n                                                            SqliteBlockReachability reachability) {\n        if (toDelete.isEmpty())\n            return new Pair<>(0L, 0L);\n        long deletedCborBlocks = toDelete.stream().filter(v -> ! v.cid.isRaw()).count();\n        long deletedRawBlocks = toDelete.size() - deletedCborBlocks;\n        for (BlockVersion block : toDelete) {\n            metadata.remove(block.cid);\n            reachability.removeBlock(block);\n        }\n        getWithBackoff(() -> {storage.bulkDelete(owner, toDelete); return true;});\n\n        long logEvery = Math.max(1_000, totalBlocksToDelete / 10);\n        long updatedProgress = progress.addAndGet(toDelete.size());\n        if (updatedProgress / logEvery > (updatedProgress - toDelete.size()) / logEvery)\n            System.out.println(\"Deleting unreachable blocks: \" + updatedProgress * 100 / totalBlocksToDelete + \"% done\");\n\n        return new Pair<>(deletedCborBlocks, deletedRawBlocks);\n    }\n\n    public static boolean markReachable(PublicKeyHash owner,\n                                        DeletableContentAddressedStorage storage,\n                                        Cid root,\n                                        String username,\n                                        SqliteBlockReachability reachability,\n                                        BlockMetadataStore metadata,\n                                        AtomicLong totalReachable) {\n        return markReachable(owner, storage, true, new ArrayList<>(1000), root, reachability, metadata, () -> username, totalReachable);\n    }\n\n    private static boolean markReachable(PublicKeyHash owner,\n                                         DeletableContentAddressedStorage storage,\n                                         boolean isRoot,\n                                         List<Cid> queue,\n                                         Cid block,\n                                         SqliteBlockReachability reachability,\n                                         BlockMetadataStore metadata,\n                                         Supplier<String> username,\n                                         AtomicLong totalReachable) {\n        if (isRoot)\n            queue.add(block);\n\n        try {\n            Optional<List<Cid>> fromRdb = block.isRaw() ?\n                    Optional.of(Collections.emptyList()) :\n                    reachability.getLinks(block);\n            List<Cid> newLinks = fromRdb\n                    .orElseGet(() -> metadata.get(block).map(m -> m.links)\n                            .orElseGet(() -> getWithBackoff(() -> storage.getLinks(owner, block, Arrays.asList(storage.id().join())).join())));\n\n            if (fromRdb.isEmpty() && ! block.isRaw()) {\n                try {\n                    reachability.setLinks(block, newLinks);\n                } catch (Exception e) {\n                    // Can hit this for new blocks that are not in the block\n                    // list in the db and thus don't have an index\n                }\n            }\n            queue.addAll(newLinks);\n            if (queue.size() > 1000) {\n                reachability.setReachable(queue, totalReachable);\n                queue.clear();\n            }\n            for (Cid link : newLinks) {\n                markReachable(owner, storage, false, queue, link, reachability, metadata, username, totalReachable);\n            }\n        } catch (Exception e) {\n            LOG.info(\"Error processing user \" + username.get() + \" \" + e.getMessage());\n            LOG.log(Level.SEVERE, e, e::getMessage);\n        }\n        if (isRoot)\n            reachability.setReachable(queue, totalReachable);\n        return true;\n    }\n\n    private static <V> V getWithBackoff(Supplier<V> req) {\n        long sleep = 1000;\n        for (int i=0; i < 20; i++) {\n            try {\n                return req.get();\n            } catch (RateLimitException e) {\n                try {\n                    Thread.sleep(sleep);\n                } catch (InterruptedException f) {}\n                sleep *= 2;\n            }\n        }\n        throw new IllegalStateException(\"Couldn't process request because of rate limit!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/GetBlockingStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class GetBlockingStorage extends DelegatingStorage {\n    private final ContentAddressedStorage target;\n\n    public GetBlockingStorage(ContentAddressedStorage target) {\n        super(target);\n        this.target = target;\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return target.blockStoreProperties();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new GetBlockingStorage(target.directToOrigin());\n    }\n\n    @Override\n    public void clearBlockCache() {\n        target.clearBlockCache();\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        throw new IllegalStateException(\"P2P block gets are not allowed, use bitswap!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        throw new IllegalStateException(\"P2P block gets are not allowed, use bitswap!\");\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"P2P IPNS gets are not allowed, use the DHT!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/IpfsWrapper.java",
    "content": "package peergos.server.storage;\n\nimport com.sun.net.httpserver.HttpServer;\nimport io.ipfs.cid.Cid;\nimport io.ipfs.multihash.Multihash;\nimport io.libp2p.core.PeerId;\nimport io.libp2p.core.crypto.*;\nimport org.peergos.*;\nimport org.peergos.config.*;\nimport org.peergos.config.Filter;\nimport org.peergos.net.*;\nimport org.peergos.protocol.dht.DatabaseRecordStore;\nimport org.peergos.protocol.dht.RecordStore;\nimport org.peergos.protocol.http.HttpProtocol;\nimport org.peergos.protocol.ipns.IPNS;\nimport org.peergos.protocol.ipns.IpnsMapping;\nimport org.peergos.util.JSONParser;\nimport peergos.server.*;\nimport peergos.server.AggregatedMetrics;\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.storage.auth.JdbcBatCave;\nimport peergos.server.util.*;\nimport peergos.server.util.Args;\nimport peergos.server.storage.auth.BlockRequestAuthoriser;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.resolution.ResolutionRecord;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.sql.Connection;\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.function.Supplier;\nimport java.util.logging.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static peergos.server.util.Logging.LOG;\nimport static peergos.server.util.AddressUtil.getAddress;\n\npublic class IpfsWrapper implements AutoCloseable {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    public static final String IPFS_BOOTSTRAP_NODES = \"ipfs-config-bootstrap-node-list\";\n    public static final String DEFAULT_BOOTSTRAP_LIST = Stream.of(\n            \"/ip4/172.104.157.121/tcp/4001/p2p/QmVdFZgHnEgcedCS2G2ZNiEN59LuVrnRm7z3yXtEBv2XiF\",\n            \"/ip6/2a01:7e01::f03c:92ff:fe26:f671/tcp/4001/p2p/QmVdFZgHnEgcedCS2G2ZNiEN59LuVrnRm7z3yXtEBv2XiF\",\n            \"/ip4/172.104.143.23/tcp/4001/p2p/12D3KooWFv6ZcoUKyaDBB7nR5SQg6HpmEbDXad48WyFSyEk7xrSR\",\n            \"/ip6/2a01:7e01::f03c:92ff:fee5:154a/tcp/4001/p2p/12D3KooWFv6ZcoUKyaDBB7nR5SQg6HpmEbDXad48WyFSyEk7xrSR\",\n            \"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN\",\n            \"/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa\",\n            \"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb\",\n            \"/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt\",\n            \"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ\",\n            \"/ip4/104.236.179.241/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM\",\n            \"/ip4/128.199.219.111/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu\",\n            \"/ip4/104.236.76.40/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64\",\n            \"/ip4/178.62.158.247/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd\",\n            \"/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM\",\n            \"/ip6/2400:6180:0:d0::151:6001/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu\",\n            \"/ip6/2604:a880:800:10::4a:5001/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64\",\n            \"/ip6/2a03:b0c0:0:1010::23:1001/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd\",\n            \"/ip4/104.131.131.82/udp/4001/quic/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ\"\n    ).collect(Collectors.joining(\",\"));\n\n    private static HttpProtocol.HttpRequestProcessor proxyHandler(io.ipfs.multiaddr.MultiAddress target) {\n        return (s, req, h) -> HttpProtocol.proxyRequest(req, convert(target), h);\n    }\n\n    private static SocketAddress convert(io.ipfs.multiaddr.MultiAddress target) {\n        return new InetSocketAddress(target.getHost(), target.getPort());\n    }\n\n    public static class S3ConfigParams {\n        public final String s3Path, s3Bucket, s3Region, s3AccessKey, s3SecretKey, s3RegionEndpoint;\n        public final boolean useGlacier;\n\n        private S3ConfigParams(String s3Path,\n                               String s3Bucket,\n                               String s3Region,\n                               String s3AccessKey,\n                               String s3SecretKey,\n                               String s3RegionEndpoint,\n                               boolean useGlacier) {\n            this.s3Path = s3Path;\n            this.s3Bucket = s3Bucket;\n            this.s3Region = s3Region;\n            this.s3AccessKey = s3AccessKey;\n            this.s3SecretKey = s3SecretKey;\n            this.s3RegionEndpoint = s3RegionEndpoint;\n            this.useGlacier = useGlacier;\n        }\n        public static S3ConfigParams build(Optional<String> s3Path,\n            Optional<String> s3Bucket,\n            Optional<String> s3Region,\n            Optional<String> s3AccessKey,\n            Optional<String> s3SecretKey,\n            Optional<String> s3RegionEndpoint,\n            Optional<String> useGlacier) {\n            S3ConfigParams params = new S3ConfigParams(s3Path.orElse(\"\"), s3Bucket.orElse(\"\"), s3Region.orElse(\"\"),\n                        s3AccessKey.orElse(\"\"), s3SecretKey.orElse(\"\"), s3RegionEndpoint.orElse(\"\"), Boolean.parseBoolean(useGlacier.orElse(\"false\")));\n            return params;\n        }\n    }\n    public static class IpfsConfigParams {\n        /**\n         * Encapsulate IPFS configuration state.\n         */\n        public final List<MultiAddress> bootstrapNode;\n        public final int swarmPort;\n        public final List<MultiAddress> swarmAddrs;\n        public final String apiAddress, gatewayAddress, proxyTarget;\n        public final boolean enableMetrics;\n        public final Optional<String> metricsAddress;\n        public final Optional<Integer> metricsPort;\n        public final Optional<S3ConfigParams> s3ConfigParams;\n        public final Optional<IdentitySection> identity;\n        public final Filter blockFilter;\n\n        public IpfsConfigParams(List<MultiAddress> bootstrapNode,\n                                String apiAddress,\n                                String gatewayAddress,\n                                String proxyTarget,\n                                int swarmPort,\n                                List<MultiAddress> swarmAddrs,\n                                boolean enableMetrics,\n                                Optional<String> metricsAddress,\n                                Optional<Integer> metricsPort,\n                                Optional<S3ConfigParams> s3ConfigParams,\n                                Filter blockFilter,\n                                Optional<IdentitySection> identity) {\n            this.bootstrapNode = bootstrapNode;\n            this.apiAddress = apiAddress;\n            this.gatewayAddress = gatewayAddress;\n            this.proxyTarget = proxyTarget;\n            this.swarmPort = swarmPort;\n            this.swarmAddrs = swarmAddrs;\n            this.enableMetrics = enableMetrics;\n            this.metricsAddress = metricsAddress;\n            this.metricsPort = metricsPort;\n            this.s3ConfigParams = s3ConfigParams;\n            this.blockFilter = blockFilter;\n            this.identity = identity;\n        }\n        public IpfsConfigParams withIdentity(Optional<IdentitySection> identity) {\n            return new IpfsConfigParams(this.bootstrapNode, this.apiAddress, this.gatewayAddress, this.proxyTarget,\n                    this.swarmPort, this.swarmAddrs, this.enableMetrics, this.metricsAddress, this.metricsPort, this.s3ConfigParams, this.blockFilter,\n                    identity);\n        }\n    }\n\n    private static List<MultiAddress> parseMultiAddresses(String s) {\n        return Stream.of(s.split(\",\"))\n                .filter(e -> ! e.isEmpty())\n                .map(MultiAddress::new)\n                .collect(Collectors.toList());\n    }\n\n    public static IpfsConfigParams buildConfig(Args args) {\n\n        List<MultiAddress> bootstrapNodes = args.hasArg(IPFS_BOOTSTRAP_NODES)\n                && args.getArg(IPFS_BOOTSTRAP_NODES).trim().length() > 0 ?\n                new ArrayList<>(parseMultiAddresses(args.getArg(IPFS_BOOTSTRAP_NODES))) :\n                parseMultiAddresses(DEFAULT_BOOTSTRAP_LIST);\n\n        int swarmPort = args.getInt(\"ipfs-swarm-port\", 4001);\n        List<MultiAddress> swarmAddrs = args.getOptionalArg(\"ipfs-swarm-addrs\")\n                .map(as -> Arrays.stream(as.split(\",\")).map(MultiAddress::new).collect(Collectors.toList()))\n                .orElse(Collections.emptyList());\n\n        String apiAddress = args.getArg(\"ipfs-api-address\");\n        String gatewayAddress = args.getArg(\"ipfs-gateway-address\");\n\n        String proxyTarget = args.getArg(\"proxy-target\");\n        boolean enableMetrics = args.getBoolean(\"collect-metrics\", false);\n        Optional<String> metricsAddress = args.getOptionalArg(\"metrics.address\");\n        Optional<Integer> metricsPort = args.getOptionalArg(\"ipfs.metrics.port\").map(Integer::parseInt);\n\n        Optional<S3ConfigParams> s3Params = S3Config.useS3(args) ?\n            Optional.of(\n                S3ConfigParams.build(args.getOptionalArg(\"s3.path\") , args.getOptionalArg(\"s3.bucket\"),\n                args.getOptionalArg(\"s3.region\"), args.getOptionalArg(\"s3.accessKey\"), args.getOptionalArg(\"s3.secretKey\"),\n                args.getOptionalArg(\"s3.region.endpoint\"), args.getOptionalArg(\"use-glacier\"))\n            ) : Optional.empty();\n        Optional<IdentitySection> peergosIdentity = Optional.empty();\n        if (args.hasArg(\"ipfs.identity.priv-key\") && args.hasArg(\"ipfs.identity.peerid\")) {\n            LOG.info(\"Using identity provided via command arguments\");\n            peergosIdentity = Optional.of(new IdentitySection(\n                    io.ipfs.multibase.binary.Base64.decodeBase64(args.getArg(\"ipfs.identity.priv-key\")),\n                    PeerId.fromBase58(args.getArg(\"ipfs.identity.peerid\")))\n            );\n        }\n\n        Optional<String> blockStoreFilterOpt = args.getOptionalArg(\"block-store-filter\");\n        Filter filter = new Filter(FilterType.NONE, 0.0);\n        if (blockStoreFilterOpt.isPresent()) {\n            String blockStoreFilterName = blockStoreFilterOpt.get().toLowerCase().trim();\n            FilterType type = FilterType.NONE;\n            try {\n                type = FilterType.lookup(blockStoreFilterName);\n            } catch (IllegalArgumentException iae) {\n                LOG.warning(\"Provided block-store-filter parameter is invalid. Defaulting to no filter\");\n            }\n            Optional<String> blockStoreFilterFalsePositiveRateOpt = args.getOptionalArg(\"block-store-filter-false-positive-rate\");\n            Double falsePositiveRate = 0.0;\n            if (blockStoreFilterFalsePositiveRateOpt.isPresent()) {\n                String blockStoreFilterFalsePositiveRateStr = blockStoreFilterFalsePositiveRateOpt.get().trim();\n                try {\n                    falsePositiveRate = Double.parseDouble(blockStoreFilterFalsePositiveRateStr);\n                } catch (NumberFormatException nfe) {\n                    LOG.warning(\"Provided block-store-filter-false-positive-rate parameter is invalid. Defaulting to no filter\");\n                    type = FilterType.NONE;\n                }\n            }\n            filter = new Filter(type, falsePositiveRate);\n        }\n        return new IpfsConfigParams(bootstrapNodes, apiAddress, gatewayAddress,\n                proxyTarget, swarmPort, swarmAddrs, enableMetrics, metricsAddress, metricsPort, s3Params, filter, peergosIdentity);\n    }\n\n    private static final String IPFS_DIR = \"IPFS_PATH\";\n    private static final String DEFAULT_DIR_NAME = \".ipfs\";\n\n    public final Path ipfsDir;\n    public final IpfsConfigParams ipfsConfigParams;\n\n    private EmbeddedPeer embeddedIpfs;\n    private HttpServer apiServer;\n    private HttpServer p2pServer;\n    private volatile boolean running = true;\n\n    public IpfsWrapper(Path ipfsDir, IpfsConfigParams ipfsConfigParams) {\n        File ipfsDirF = ipfsDir.toFile();\n        if (! ipfsDirF.isDirectory() && ! ipfsDirF.mkdirs()) {\n            throw new IllegalStateException(\"Specified IPFS_PATH '\" + ipfsDir + \" is not a directory and/or could not be created\");\n        }\n        this.ipfsDir = ipfsDir;\n        Optional<IdentitySection> identityOpt = Optional.empty();\n        if (ipfsConfigParams.identity.isPresent()) {\n            identityOpt = ipfsConfigParams.identity;\n        } else {\n            identityOpt = readIPFSIdentity(ipfsDir);\n            if (identityOpt.isEmpty()) {\n                LOG.info(\"Creating new identity\");\n                HostBuilder builder = new HostBuilder(new RamAddressBook()).generateIdentity();\n                PrivKey privKey = builder.getPrivateKey();\n                PeerId peerId = builder.getPeerId();\n                identityOpt = Optional.of(new IdentitySection(privKey.bytes(), peerId));\n            }\n        }\n        this.ipfsConfigParams= ipfsConfigParams.withIdentity(identityOpt);\n    }\n\n    public static boolean isHttpApiListening(String ipfsApiAddress) {\n        try {\n            MultiAddress ipfsApi = new MultiAddress(ipfsApiAddress);\n            ContentAddressedStorage.HTTP api = new ContentAddressedStorage.HTTP(new JavaPoster(getAddress(ipfsApi), false), false, null);\n            api.id().get();\n            return true;\n        } catch (Exception e) {\n            if (!(e.getCause() instanceof ConnectException))\n                e.printStackTrace();\n        }\n        return false;\n    }\n\n    @Override\n    public synchronized void close() {\n        stop();\n    }\n\n    public synchronized void stop() {\n        LOG.info(\"Stopping server...\");\n        try {\n            embeddedIpfs.stop().join();\n            apiServer.stop(0);\n            p2pServer.stop(0);\n            running = false;\n        } catch (Exception ex) {\n            ex.printStackTrace();\n        }\n    }\n\n    public static Path getIpfsDir(Args args) {\n        //$IPFS_DIR, defaults to $PEERGOS_PATH/.ipfs\n        return args.hasArg(IPFS_DIR) ?\n                PathUtil.get(args.getArg(IPFS_DIR)) :\n                args.fromPeergosDir(IPFS_DIR, DEFAULT_DIR_NAME);\n    }\n\n    private static Optional<IdentitySection> readIPFSIdentity(Path ipfsDir) {\n        Path configFilePath = ipfsDir.resolve(\"config\");\n        File configFile = configFilePath.toFile();\n        if (!configFile.exists()) {\n            return Optional.empty();\n        }\n        try {\n            Map<String, Object> json = (Map) JSONParser.parse(Files.readString(configFilePath));\n            IdentitySection identitySection = Jsonable.parse(json, p -> IdentitySection.fromJson(p));\n            LOG.info(\"Using identity found in config file from folder: \" + ipfsDir);\n            return Optional.of(identitySection);\n        }  catch (IOException ioe) {\n            return Optional.empty();\n        }\n    }\n\n    public static IpfsWrapper launch(Args args) {\n        SqlSupplier sqlCommands = Builder.getSqlCommands(args);\n        Supplier<Connection> dbConn = Builder.getDBConnector(args, \"bat-store\");\n        BatCave batStore = new JdbcBatCave(dbConn, sqlCommands);\n        Crypto crypto = Builder.initCrypto();\n        Hasher hasher = crypto.hasher;\n        BlockRequestAuthoriser blockAuth = Builder.blockAuthoriser(args, batStore, hasher);\n        BlockMetadataStore metaDB = Builder.buildBlockMetadata(args);\n        JdbcServerIdentityStore ids = JdbcServerIdentityStore.build(Builder.getDBConnector(args, \"serverids-file\"), sqlCommands, crypto);\n        return launch(args, blockAuth, metaDB, ids);\n    }\n\n    private void startIdPublisher(ServerIdentityStore ids) {\n        Thread publisher = new Thread(() -> {\n            while (running) {\n                try {\n                    List<PeerId> all = ids.getIdentities();\n                    Map<PeerId, byte[]> presignedRecords = new LinkedHashMap<>();\n                    for (PeerId id : all) {\n                        byte[] signedIpnsRecord = ids.getRecord(id);\n                        presignedRecords.put(id, signedIpnsRecord);\n                    }\n                    if (! presignedRecords.isEmpty()) {\n                        LOG.info(\"Publishing \" + presignedRecords.size() + \" pre-signed ipns records for server identity changes\");\n                        presignedRecords.forEach((id, rec) -> {\n                            LOG.info(\"Publishing ipns record for \" + id);\n                            // renew record if it expires within 6 months\n                            Multihash key = Multihash.deserialize(id.getBytes());\n                            byte[] ipnsKey = IPNS.getKey(key);\n                            IpnsMapping currentMapping = IPNS.parseAndValidateIpnsEntry(ipnsKey, rec).get();\n                            if (currentMapping.value.expiry.isBefore(LocalDateTime.now().plusMonths(6))) {\n                                LOG.info(\"Updating ipns record for \" + id);\n                                IpnsEntry entry = new IpnsEntry(currentMapping.getSignature(), currentMapping.getData());\n                                ResolutionRecord currentRR = entry.getValue();\n                                long newSequence = currentRR.sequence + 1;\n                                byte[] privPb = ids.getPrivateKey(id);\n                                PrivKey priv;\n                                // handle current peer key being supplied on command line\n                                if (privPb == null && id.equals(embeddedIpfs.node.getPeerId())) {\n                                    priv = embeddedIpfs.node.getPrivKey();\n                                } else\n                                    priv = KeyKt.unmarshalPrivateKey(privPb);\n                                byte[] newRecord = ServerIdentity.generateSignedIpnsRecord(priv, currentRR.host, currentRR.moved, newSequence);\n                                IPNS.parseAndValidateIpnsEntry(ipnsKey, newRecord).get();\n                                ids.setRecord(id, newRecord);\n                                rec = newRecord;\n                            }\n                            embeddedIpfs.publishPresignedRecord(key, rec).join();\n                        });\n                    }\n                } catch (Exception e) {\n                    LOG.log(Level.SEVERE, e, e::getMessage);\n                }\n                try {\n                    Thread.sleep(6 * 3600 * 1000);\n                } catch (InterruptedException ex) {\n                    Thread.currentThread().interrupt();\n                }\n            }\n        }, \"Server Identity IPNS Publisher\");\n        Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n            publisher.interrupt();\n        }, \"Server Identity IPNS Publisher - shutdown\"));\n        publisher.setDaemon(true);\n        publisher.start();\n    }\n\n    public static IpfsWrapper launch(Args args,\n                                     BlockRequestAuthoriser blockAuth,\n                                     BlockMetadataStore metaDB,\n                                     ServerIdentityStore ids) {\n        Path ipfsDir = getIpfsDir(args);\n        LOG.info(\"Using IPFS dir \" + ipfsDir);\n\n        IpfsConfigParams ipfsConfigParams = buildConfig(args);\n\n        IpfsWrapper ipfsWrapper = new IpfsWrapper(ipfsDir, ipfsConfigParams);\n        Config config = ipfsWrapper.configure();\n        // use identity from db if present, otherwise move to db\n        List<PeerId> ourIds = ids.getIdentities();\n        if (ourIds.isEmpty()) {\n            // initialise id db with our current peerid and sign an ipns record\n            PrivKey peerPrivate = KeyKt.unmarshalPrivateKey(config.identity.privKeyProtobuf);\n            byte[] signedRecord = ServerIdentity.generateSignedIpnsRecord(peerPrivate, Optional.empty(), false, 1);\n            ids.addIdentity(PeerId.fromPubKey(peerPrivate.publicKey()), signedRecord);\n        } else {\n            // make sure we are using the latest identity\n            PeerId current = ourIds.get(ourIds.size() - 1);\n            if (! current.equals(config.identity.peerId))\n                throw new IllegalStateException(\"Supplied peerid (\"+config.identity.peerId.toBase58()+\n                        \") doesn't match latest in server identity db (\"+current.toBase58()+\")!\");\n        }\n\n        LOG.info(\"Starting Nabu version: \" + APIHandler.CURRENT_VERSION + \", peerid: \" + config.identity.peerId);\n        org.peergos.BlockRequestAuthoriser authoriser = (c, p, auth) -> {\n            peergos.shared.io.ipfs.Cid source = peergos.shared.io.ipfs.Cid.decodePeerId(p.toString());\n            peergos.shared.io.ipfs.Cid cid = peergos.shared.io.ipfs.Cid.decode(c.toString());\n            Optional<BlockMetadata> blockMetadata = metaDB.get(cid);\n            if (blockMetadata.isEmpty())\n                return Futures.of(false);\n            List<BatId> bats = blockMetadata.get().batids;\n            return blockAuth.allowRead(cid, bats, source, auth)\n                .exceptionally(ex -> false);\n        };\n\n        Path datastorePath = ipfsWrapper.ipfsDir.resolve(\"datastore\").resolve(\"records.sqlite\");\n        datastorePath.getParent().toFile().mkdirs();\n        RecordStore records = JdbcRecordLRU.buildSqlite(1_000, datastorePath.toAbsolutePath().toString());\n\n        ipfsWrapper.embeddedIpfs = EmbeddedPeer.build(records,\n                config.addresses.getSwarmAddresses(),\n                config.bootstrap.getBootstrapAddresses(),\n                config.identity,\n                args.getOptionalArg(\"ipfs-announce-addresses\")\n                        .map(addrs -> Arrays.stream(addrs.split(\",\"))\n                                .map(io.ipfs.multiaddr.MultiAddress::new)\n                                .collect(Collectors.toList()))\n                        .orElse(Collections.emptyList()),\n                JdbcAddressLRU.buildSqlite(1000, args.fromPeergosDir(\"address-book\", \"address-book.sqlite\").toString()),\n                config.addresses.proxyTargetAddress.map(IpfsWrapper::proxyHandler)\n        );\n        ipfsWrapper.embeddedIpfs.start(args.getBoolean(\"async-bootstrap\", false));\n        io.ipfs.multiaddr.MultiAddress apiAddress = config.addresses.apiAddress;\n        InetSocketAddress localAPIAddress = new InetSocketAddress(apiAddress.getHost(), apiAddress.getPort());\n\n        int maxConnectionQueue = 500;\n        int handlerThreads = 50;\n        LOG.info(\"Starting Nabu API server at \" + apiAddress.getHost() + \":\" + localAPIAddress.getPort());\n        try {\n            if (config.metrics.enabled) {\n                LOG.info(\"Starting ipfs metrics endpoint at \" + config.metrics.address + \":\" + config.metrics.port);\n                AggregatedMetrics.startExporter(config.metrics.address, config.metrics.port);\n            }\n\n            ipfsWrapper.apiServer = HttpServer.create(localAPIAddress, maxConnectionQueue);\n            ipfsWrapper.apiServer.createContext(APIHandler.API_URL, new PeerAPIHandler(ipfsWrapper.embeddedIpfs));\n            ipfsWrapper.apiServer.setExecutor(Threads.newPool(handlerThreads, \"Nabu-api-handler-\"));\n            ipfsWrapper.apiServer.start();\n\n            io.ipfs.multiaddr.MultiAddress p2pAddress = config.addresses.gatewayAddress;\n            InetSocketAddress localP2pAddress = new InetSocketAddress(p2pAddress.getHost(), p2pAddress.getPort());\n            ipfsWrapper.p2pServer = HttpServer.create(localP2pAddress, maxConnectionQueue);\n\n            ipfsWrapper.p2pServer.createContext(HttpProxyService.API_URL, new HttpProxyHandler(\n                    new HttpProxyService(ipfsWrapper.embeddedIpfs.node, ipfsWrapper.embeddedIpfs.p2pHttp.get(),\n                            ipfsWrapper.embeddedIpfs.dht)));\n            ipfsWrapper.p2pServer.setExecutor(Threads.newPool(handlerThreads, \"Nabu-proxy-handler-\"));\n            ipfsWrapper.p2pServer.start();\n        } catch (IOException ioe) {\n            throw new IllegalStateException(\"Unable to start Server: \" + ioe);\n        }\n        Thread shutdownHook = new Thread(ipfsWrapper::stop);\n        Runtime.getRuntime().addShutdownHook(shutdownHook);\n        ipfsWrapper.startIdPublisher(ids);\n        return ipfsWrapper;\n    }\n\n    private Config configure() {\n\n        LOG().info(\"Initializing ipfs\");\n        IdentitySection identity = ipfsConfigParams.identity.get();\n\n        List<io.ipfs.multiaddr.MultiAddress> swarmAddresses = ipfsConfigParams.swarmAddrs.isEmpty() ?\n                List.of(\n                        new io.ipfs.multiaddr.MultiAddress(\"/ip6/::/tcp/\" + ipfsConfigParams.swarmPort),\n                        new io.ipfs.multiaddr.MultiAddress(\"/ip6/::/udp/\" + ipfsConfigParams.swarmPort + \"/quic-v1\")\n                        ) :\n                ipfsConfigParams.swarmAddrs.stream()\n                        .map(x -> x.toString())\n                        .map(io.ipfs.multiaddr.MultiAddress::new)\n                        .collect(Collectors.toList());\n        io.ipfs.multiaddr.MultiAddress apiAddress = new io.ipfs.multiaddr.MultiAddress(ipfsConfigParams.apiAddress);\n        io.ipfs.multiaddr.MultiAddress gatewayAddress = new io.ipfs.multiaddr.MultiAddress(ipfsConfigParams.gatewayAddress);\n        Optional<io.ipfs.multiaddr.MultiAddress> proxyTargetAddress = Optional.of(new io.ipfs.multiaddr.MultiAddress(ipfsConfigParams.proxyTarget));\n\n        List<io.ipfs.multiaddr.MultiAddress> bootstrapNodes = ipfsConfigParams.bootstrapNode.stream()\n                .map(b -> new io.ipfs.multiaddr.MultiAddress(b.toString()))\n                .collect(Collectors.toList());\n\n        Map<String, Object> blockChildMap = new LinkedHashMap<>();\n        if (ipfsConfigParams.s3ConfigParams.isPresent()) {\n            S3ConfigParams s3Params = ipfsConfigParams.s3ConfigParams.get();\n            blockChildMap.put(\"region\", s3Params.s3Region);\n            blockChildMap.put(\"bucket\", s3Params.s3Bucket);\n            blockChildMap.put(\"rootDirectory\", s3Params.s3Path);\n            blockChildMap.put(\"regionEndpoint\", s3Params.s3RegionEndpoint);\n            blockChildMap.put(\"accessKey\", s3Params.s3AccessKey);\n            blockChildMap.put(\"secretKey\", s3Params.s3SecretKey);\n            blockChildMap.put(\"use-glacier\", Boolean.toString(s3Params.useGlacier));\n            blockChildMap.put(\"type\", \"s3ds\");\n        } else {\n            blockChildMap.put(\"path\", \"blocks\");\n            blockChildMap.put(\"shardFunc\", \"/repo/flatfs/shard/v1/next-to-last/2\");\n            blockChildMap.put(\"sync\", \"true\");\n            blockChildMap.put(\"type\", \"flatfs\");\n        }\n        String prefix = ipfsConfigParams.s3ConfigParams.isPresent() ? \"s3.datastore\" : \"flatfs.datastore\";\n        Mount blockMount = new Mount(\"/blocks\", prefix, \"measure\", blockChildMap);;\n\n        Map<String, Object> dataChildMap = new LinkedHashMap<>();\n        dataChildMap.put(\"compression\", \"none\");\n        dataChildMap.put(\"path\", \"datastore\");\n        dataChildMap.put(\"type\", \"h2\");\n        Mount rootMount = new Mount(\"/\", \"h2.datastore\", \"measure\", dataChildMap);\n\n        AddressesSection addressesSection = new AddressesSection(swarmAddresses, apiAddress, gatewayAddress,\n                proxyTargetAddress, Optional.empty());\n\n        org.peergos.config.Filter filter = ipfsConfigParams.blockFilter;\n\n        CodecSet codecSet = new CodecSet(Set.of(Cid.Codec.DagCbor, Cid.Codec.Raw));\n        DatastoreSection datastoreSection = new DatastoreSection(blockMount, rootMount, filter, codecSet);\n        BootstrapSection bootstrapSection = new BootstrapSection(bootstrapNodes);\n        // ipfs metrics are merged with peergos metrics, unless running the IPFS standalone command.\n        boolean separateIpfsMetrics = ipfsConfigParams.enableMetrics && ipfsConfigParams.metricsPort.isPresent();\n        MetricsSection metrics = new MetricsSection(separateIpfsMetrics, ipfsConfigParams.metricsAddress.orElse(\"localhost\"), ipfsConfigParams.metricsPort.orElse(8101));\n        Config config = new org.peergos.config.Config(addressesSection, bootstrapSection, datastoreSection,\n                identity, metrics);\n        return config;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/JdbcBlockMetadataStore.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class JdbcBlockMetadataStore implements BlockMetadataStore {\n\n    private static final Logger LOG = Logging.LOG();\n    private static final String GET_INFO = \"SELECT * FROM blockmetadata WHERE cid = ?;\";\n    private static final String GET_OWNER = \"SELECT owner FROM blockmetadata WHERE cid = ?;\";\n    private static final String REMOVE = \"DELETE FROM blockmetadata where cid = ?;\";\n    public static final int PAGE_LIMIT = 100_000;\n    private static final String LIST_PAGINATED_FIRST = \"SELECT cid, version FROM blockmetadata ORDER BY cid LIMIT \" + PAGE_LIMIT + \";\";\n    private static final String LIST_PAGINATED = \"SELECT cid, version FROM blockmetadata WHERE cid > ? ORDER BY cid LIMIT \" + PAGE_LIMIT + \";\";\n    private static final String LIST_SIZE_PAGINATED_FIRST = \"SELECT cid, size FROM blockmetadata ORDER BY cid LIMIT \" + PAGE_LIMIT + \";\";\n    private static final String LIST_SIZE_PAGINATED = \"SELECT cid, size FROM blockmetadata WHERE cid > ? ORDER BY cid LIMIT \" + PAGE_LIMIT + \";\";\n    private static final String LIST_ALL = \"SELECT cid, version FROM blockmetadata WHERE owner=?;\";\n    private static final String SIZE = \"SELECT COUNT(*) FROM blockmetadata WHERE owner=?;\";\n    private static final String EMPTY = \"SELECT * FROM blockmetadata LIMIT 1;\";\n    private Supplier<Connection> conn;\n    private final SqlSupplier commands;\n\n    public JdbcBlockMetadataStore(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        this.commands = commands;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        return getConnection(true, true);\n    }\n\n    private Connection getConnection(boolean autocommit, boolean serializable) {\n        Connection connection = conn.get();\n        try {\n            if (autocommit)\n                connection.setAutoCommit(true);\n            if (serializable)\n                connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            else\n                connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createBlockMetadataStoreTableCommand(), conn);\n            try { // sqlite doesn't have an \"if not exists\" modifier on \"add column\"\n                commands.createTable(commands.ensureColumnExistsCommand(\"blockmetadata\", \"owner\", commands.getByteArrayType() + \" DEFAULT null\"), conn);\n            } catch (SQLException f) {\n                if (!f.getMessage().contains(\"duplicate column\"))\n                    throw new RuntimeException(f);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void compact() {\n        String vacuum = commands.vacuumCommand();\n        if (vacuum.isEmpty())\n            return;\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(vacuum)) {\n            stmt.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public void remove(Cid block) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(REMOVE)) {\n\n            insert.setBytes(1, block.toBytes());\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Optional<BlockMetadata> get(Cid block) {\n        try (Connection conn = getConnection(false, false);\n             PreparedStatement stmt = conn.prepareStatement(GET_INFO)) {\n            stmt.setBytes(1, block.toBytes());\n            ResultSet rs = stmt.executeQuery();\n            while (rs.next()) {\n                List<Cid> links = ((CborObject.CborList) CborObject.fromByteArray(rs.getBytes(\"links\")))\n                        .map(cbor -> Cid.cast(((CborObject.CborByteArray)cbor).value));\n                List<BatId> batIds = ((CborObject.CborList) CborObject.fromByteArray(rs.getBytes(\"batids\")))\n                        .map(BatId::fromCbor);\n                return Optional.of(new BlockMetadata(rs.getInt(\"size\"), links, batIds));\n            }\n            return Optional.empty();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public List<Cid> hasBlocks(List<Cid> blocks) {\n        String placeholders = blocks.stream()\n                .map(b -> \"?\")\n                .collect(Collectors.joining(\",\"));\n\n        String sql = \"SELECT cid FROM blockmetadata WHERE cid IN (\" + placeholders + \");\";\n\n        try (Connection conn = getConnection(false, false);\n             PreparedStatement stmt = conn.prepareStatement(sql)) {\n\n            for (int i = 0; i < blocks.size(); i++) {\n                stmt.setBytes(i + 1, blocks.get(i).toBytes());\n            }\n\n            ResultSet rs = stmt.executeQuery();\n            List<Cid> present = new ArrayList<>();\n\n            while (rs.next()) {\n                present.add(Cid.cast(rs.getBytes(\"cid\")));\n            }\n\n            return present;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Map<Cid, BlockMetadata> getAll(List<Cid> blocks) {\n        if (blocks.isEmpty())\n            return Collections.emptyMap();\n\n        String placeholders = blocks.stream()\n                .map(b -> \"?\")\n                .collect(Collectors.joining(\",\"));\n\n        String sql = \"SELECT cid, links, batids, size FROM blockmetadata WHERE cid IN (\" + placeholders + \");\";\n\n        try (Connection conn = getConnection(false, false);\n             PreparedStatement stmt = conn.prepareStatement(sql)) {\n\n            for (int i = 0; i < blocks.size(); i++) {\n                stmt.setBytes(i + 1, blocks.get(i).toBytes());\n            }\n\n            ResultSet rs = stmt.executeQuery();\n            Map<Cid, BlockMetadata> present = new HashMap<>();\n\n            while (rs.next()) {\n                Cid h = Cid.cast(rs.getBytes(\"cid\"));\n                List<Cid> links = ((CborObject.CborList) CborObject.fromByteArray(rs.getBytes(\"links\")))\n                        .map(cbor -> Cid.cast(((CborObject.CborByteArray)cbor).value));\n                List<BatId> batIds = ((CborObject.CborList) CborObject.fromByteArray(rs.getBytes(\"batids\")))\n                        .map(BatId::fromCbor);\n                present.put(h, new BlockMetadata(rs.getInt(\"size\"), links, batIds));\n            }\n\n            return present;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Optional<PublicKeyHash> getOwner(Cid block) {\n        try (Connection conn = getConnection(false, false);\n             PreparedStatement stmt = conn.prepareStatement(GET_OWNER)) {\n            stmt.setBytes(1, block.toBytes());\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return Optional.ofNullable(rs.getBytes(\"owner\"))\n                        .map(PublicKeyHash::decode);\n            }\n            return Optional.empty();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void setOwner(PublicKeyHash owner, Cid block) {\n        try (Connection conn = getConnection();\n             PreparedStatement update = conn.prepareStatement(commands.updateMetadataCommand())) {\n\n            update.setBytes(1, owner.toBytes());\n            update.setBytes(2, block.toBytes());\n            update.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void setOwnerAndVersion(PublicKeyHash owner, Cid block, String version) {\n        try (Connection conn = getConnection();\n             PreparedStatement update = conn.prepareStatement(commands.setMetadataVersionAndOwnerCommand())) {\n\n            update.setString(1, version);\n            update.setBytes(2, owner.toBytes());\n            update.setBytes(3, block.toBytes());\n            update.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void put(PublicKeyHash owner, Cid block, String version, BlockMetadata meta) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(commands.addMetadataCommand())) {\n\n            insert.setBytes(1, owner != null ? owner.toBytes() : null);\n            insert.setBytes(2, block.toBytes());\n            insert.setString(3, version);\n            insert.setLong(4, meta.size);\n            insert.setBytes(5, new CborObject.CborList(meta.links.stream()\n                    .map(Cid::toBytes)\n                    .map(CborObject.CborByteArray::new)\n                    .collect(Collectors.toList()))\n                    .toByteArray());\n            insert.setBytes(6, new CborObject.CborList(meta.batids)\n                    .toByteArray());\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public long size(PublicKeyHash owner) {\n        try (Connection conn = getConnection();\n             PreparedStatement size = conn.prepareStatement(SIZE)) {\n            size.setBytes(1, owner != null ? owner.toBytes() : null);\n            ResultSet rs = size.executeQuery();\n            rs.next();\n            return rs.getInt(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public boolean isEmpty() {\n        try (Connection conn = getConnection();\n             PreparedStatement size = conn.prepareStatement(EMPTY)) {\n            ResultSet rs = size.executeQuery();\n            return ! rs.next();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void applyToAll(Consumer<Cid> action) {\n        Cid prevLast = null;\n        while (true) {\n            try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(prevLast == null ? LIST_PAGINATED_FIRST : LIST_PAGINATED)) {\n                if (prevLast != null)\n                    stmt.setBytes(1, prevLast.toBytes());\n                ResultSet rs = stmt.executeQuery();\n                int added = 0;\n                while (rs.next()) {\n                    Cid cid = Cid.cast(rs.getBytes(\"cid\"));\n                    action.accept(cid);\n                    added++;\n                    prevLast = cid;\n                }\n                if (added == 0)\n                    break;\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                throw new RuntimeException(sqe);\n            }\n        }\n    }\n\n    @Override\n    public void applyToAllSizes(BiConsumer<Cid, Long> action) {\n        Cid prevLast = null;\n        while (true) {\n            try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(prevLast == null ? LIST_SIZE_PAGINATED_FIRST : LIST_SIZE_PAGINATED)) {\n                if (prevLast != null)\n                    stmt.setBytes(1, prevLast.toBytes());\n                ResultSet rs = stmt.executeQuery();\n                int added = 0;\n                while (rs.next()) {\n                    Cid cid = Cid.cast(rs.getBytes(\"cid\"));\n                    long size = rs.getLong(\"size\");\n                    action.accept(cid, size);\n                    added++;\n                    prevLast = cid;\n                }\n                if (added == 0)\n                    break;\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                throw new RuntimeException(sqe);\n            }\n        }\n    }\n\n    @Override\n    public Stream<BlockVersion> list(PublicKeyHash owner) {\n        try (Connection conn = getConnection();\n             PreparedStatement list = conn.prepareStatement(LIST_ALL)) {\n            list.setBytes(1, owner == null ? null : owner.toBytes());\n            ResultSet rs = list.executeQuery();\n            List<BlockVersion> res = new ArrayList<>();\n            while (rs.next()) {\n                res.add(new BlockVersion(Cid.cast(rs.getBytes(\"cid\")), rs.getString(\"version\"), true));\n            }\n            return res.stream();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void listCbor(PublicKeyHash owner, Consumer<List<BlockVersion>> results) {\n        try (Connection conn = getConnection();\n             PreparedStatement list = conn.prepareStatement(LIST_ALL)) {\n            list.setBytes(1, owner == null ? null : owner.toBytes());\n            ResultSet rs = list.executeQuery();\n            List<BlockVersion> res = new ArrayList<>();\n            while (rs.next()) {\n                Cid cid = Cid.cast(rs.getBytes(\"cid\"));\n                String version = rs.getString(\"version\");\n                if (! cid.isRaw()) {\n                    res.add(new BlockVersion(cid, version, true));\n                    if (res.size() == 1000) {\n                        results.accept(res);\n                        res = new ArrayList<>(1000);\n                    }\n                }\n            }\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/JdbcLinkRetrievalcounter.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class JdbcLinkRetrievalcounter implements LinkRetrievalCounter {\n\n    private static final Logger LOG = Logging.LOG();\n\n    private static final String GET = \"SELECT count FROM linkcounts WHERE username = ? AND label = ?;\";\n    private static final String LATEST = \"SELECT MAX(modified) FROM linkcounts WHERE username = ?;\";\n    private static final String LATEST_ALL = \"SELECT MAX(modified) FROM linkcounts;\";\n    private static final String AFTER = \"SELECT label, count, modified FROM linkcounts WHERE username = ? AND modified > ?;\";\n\n    private Supplier<Connection> conn;\n    private final SqlSupplier commands;\n\n    public JdbcLinkRetrievalcounter(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        this.commands = commands;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createLinkCountTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n    @Override\n    public void increment(String owner, long label) {\n        try (Connection conn = getConnection();\n             PreparedStatement linkInsert = conn.prepareStatement(commands.insertOrIgnoreCommand(\"INSERT \", \"INTO linkcounts (username, label, count, modified) VALUES(?, ?, ?, ?)\"));\n             PreparedStatement increment = conn.prepareStatement(\"UPDATE linkcounts SET count = count + 1, modified=? where username=? AND label=?;\")\n             ) {\n            linkInsert.setString(1, owner);\n            linkInsert.setLong(2, label);\n            linkInsert.setLong(3, 0);\n            long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);\n            linkInsert.setLong(4, now);\n            linkInsert.executeUpdate();\n\n            increment.setLong(1, now);\n            increment.setString(2, owner);\n            increment.setLong(3, label);\n            int modified = increment.executeUpdate();\n            if (modified != 1)\n                throw new IllegalStateException(\"No rows modified!\");\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public long getCount(String owner, long label) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET)) {\n            stmt.setString(1, owner);\n            stmt.setLong(2, label);\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return rs.getLong(1);\n            }\n            return 0;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Optional<LocalDateTime> getLatestModificationTime(String owner) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(LATEST)) {\n            stmt.setString(1, owner);\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return Optional.of(LocalDateTime.ofEpochSecond(rs.getLong(1), 0, ZoneOffset.UTC));\n            }\n            return Optional.empty();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Optional<LocalDateTime> getLatestModificationTime() {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(LATEST_ALL)) {\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return Optional.of(LocalDateTime.ofEpochSecond(rs.getLong(1), 0, ZoneOffset.UTC));\n            }\n            return Optional.empty();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void setCounts(String owner, LinkCounts counts) {\n        counts.counts.forEach((k, v) -> {\n            try (Connection conn = getConnection();\n                 PreparedStatement linkInsert = conn.prepareStatement(commands.insertOrIgnoreCommand(\"INSERT \", \"INTO linkcounts (username, label, count, modified) VALUES(?, ?, ?, ?)\"));\n                 PreparedStatement increment = conn.prepareStatement(\"UPDATE linkcounts SET count = ? where username=? AND label=?;\")\n            ) {\n                linkInsert.setString(1, owner);\n                linkInsert.setLong(2, k);\n                linkInsert.setLong(3, 0);\n                linkInsert.setLong(4, v.right.toEpochSecond(ZoneOffset.UTC));\n                linkInsert.executeUpdate();\n\n                increment.setLong(1, v.left);\n                increment.setString(2, owner);\n                increment.setLong(3, k);\n                increment.executeUpdate();\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                throw new RuntimeException(sqe);\n            }\n        });\n    }\n\n    @Override\n    public LinkCounts getUpdatedCounts(String owner, LocalDateTime after) {\n        long seconds = after.toEpochSecond(ZoneOffset.UTC);\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(AFTER)) {\n            stmt.setString(1, owner);\n            stmt.setLong(2, seconds);\n            ResultSet rs = stmt.executeQuery();\n            Map<Long, Pair<Long, LocalDateTime>> res = new HashMap<>();\n            while (rs.next()) {\n                res.put(rs.getLong(1), new Pair<>(rs.getLong(2), LocalDateTime.ofEpochSecond(rs.getLong(3), 0, ZoneOffset.UTC)));\n            }\n\n            return new LinkCounts(res);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/JdbcPartitionStatus.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.util.Logging;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.function.Supplier;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\npublic class JdbcPartitionStatus implements PartitionStatus {\n\n    private static final Logger LOG = Logging.LOG();\n    private static final String GET = \"SELECT * FROM partitioned;\";\n    private static final String SET_DONE = \"INSERT INTO partitioned (done) VALUES(true);\";\n\n    private Supplier<Connection> conn;\n    private final SqlSupplier commands;\n\n    public JdbcPartitionStatus(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        this.commands = commands;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        return getConnection(true, true);\n    }\n\n    private Connection getConnection(boolean autocommit, boolean serializable) {\n        Connection connection = conn.get();\n        try {\n            if (autocommit)\n                connection.setAutoCommit(true);\n            if (serializable)\n                connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            else\n                connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createPartitionStatusTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public boolean isDone() {\n        try (Connection conn = getConnection(false, false);\n             PreparedStatement stmt = conn.prepareStatement(GET)) {\n            ResultSet rs = stmt.executeQuery();\n            return rs.next();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void complete() {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(SET_DONE)) {\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/JdbcServerIdentityStore.java",
    "content": "package peergos.server.storage;\n\nimport io.libp2p.core.*;\nimport io.libp2p.core.crypto.*;\nimport org.peergos.protocol.ipns.pb.*;\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.resolution.*;\nimport peergos.shared.storage.*;\n\nimport java.sql.*;\nimport java.sql.Connection;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\nimport static peergos.shared.storage.IpnsEntry.RESOLUTION_RECORD_IPNS_SUFFIX;\n\npublic class JdbcServerIdentityStore implements ServerIdentityStore {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final String SELECT_PEERIDS = \"SELECT peerid FROM serverids ORDER BY id;\";\n    private static final String SELECT_PRIVATE = \"SELECT private FROM serverids WHERE peerid=?;\";\n    private static final String GET_RECORD = \"SELECT record FROM serverids WHERE peerid=?;\";\n    private static final String SET_RECORD = \"UPDATE serverids SET record=? WHERE peerid = ?;\";\n    private static final String SET_PRIVATE = \"UPDATE serverids SET private=? WHERE peerid = ?;\";\n\n    private Supplier<Connection> conn;\n    private final SqlSupplier commands;\n    private final Crypto crypto;\n    private volatile boolean isClosed;\n\n    public JdbcServerIdentityStore(Supplier<Connection> conn, SqlSupplier commands, Crypto crypto) {\n        this.conn = conn;\n        this.commands = commands;\n        this.crypto = crypto;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createServerIdentitiesTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void addIdentity(PeerId id, byte[] signedIpnsRecord) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(commands.insertServerIdCommand())) {\n            insert.setBytes(1, id.getBytes());\n            insert.setBytes(2, signedIpnsRecord);\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    @Override\n    public void setPrivateKey(PrivKey privateKey) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(SET_PRIVATE)) {\n            insert.setBytes(1, privateKey.bytes());\n            insert.setBytes(2, PeerId.fromPubKey(privateKey.publicKey()).getBytes());\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    @Override\n    public List<PeerId> getIdentities() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(SELECT_PEERIDS)) {\n            ResultSet qres = select.executeQuery();\n            List<PeerId> res = new ArrayList<>();\n            while (qres.next()) {\n                res.add(new PeerId(qres.getBytes(1)));\n            }\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public byte[] getPrivateKey(PeerId peerId) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(SELECT_PRIVATE)) {\n            select.setBytes(1, peerId.getBytes());\n            ResultSet qres = select.executeQuery();\n            while (qres.next()) {\n                return qres.getBytes(1);\n            }\n            throw new IllegalStateException(\"No id record for \" + peerId);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public byte[] getRecord(PeerId peerId) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_RECORD)) {\n            select.setBytes(1, peerId.getBytes());\n            ResultSet qres = select.executeQuery();\n            while (qres.next()) {\n                return qres.getBytes(1);\n            }\n            throw new IllegalStateException(\"No ipns record for \" + peerId);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public void setRecord(PeerId peerId, byte[] newRecord) {\n        byte[] currentRaw = getRecord(peerId);\n        try {\n            Ipns.IpnsEntry currentEntry = Ipns.IpnsEntry.parseFrom(currentRaw);\n            Ipns.IpnsEntry newEntry = Ipns.IpnsEntry.parseFrom(newRecord);\n            IpnsEntry existing = new IpnsEntry(currentEntry.getSignatureV2().toByteArray(), currentEntry.getData().toByteArray());\n            IpnsEntry updated = new IpnsEntry(newEntry.getSignatureV2().toByteArray(), newEntry.getData().toByteArray());\n            ResolutionRecord existingValue = existing.getValue();\n            ResolutionRecord updatedValue = updated.getValue();\n\n            if (updatedValue.sequence != newEntry.getSequence())\n                throw new IllegalStateException(\"Non matching sequence!\");\n            if (updated.getIpnsSequence() != newEntry.getSequence())\n                throw new IllegalStateException(\"Non matching sequence!\");\n            updatedValue.ensureValidUpdateTo(existingValue);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(SET_RECORD)) {\n            select.setBytes(1, newRecord);\n            select.setBytes(2, peerId.getBytes());\n            int updatedRows = select.executeUpdate();\n            if (updatedRows != 1)\n                throw new IllegalStateException(\"Set record failed for \" + peerId);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n        isClosed = true;\n    }\n\n    public static JdbcServerIdentityStore build(Supplier<Connection> conn, SqlSupplier commands, Crypto crypto) {\n        return new JdbcServerIdentityStore(conn, commands, crypto);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/JdbcTransactionStore.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class JdbcTransactionStore implements TransactionStore {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final String SELECT_TRANSACTIONS_BLOCKS = \"SELECT tid, hash, time FROM transactions WHERE owner=?;\";\n    private static final String DELETE_TRANSACTION = \"DELETE FROM transactions WHERE tid = ? AND owner = ?;\";\n    private static final String DELETE_OLD_TRANSACTIONS = \"DELETE FROM transactions WHERE time < ? AND owner = ?;\";\n\n    private Supplier<Connection> conn;\n    private final SqlSupplier commands;\n    private volatile boolean isClosed;\n\n    public JdbcTransactionStore(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        this.commands = commands;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createTransactionsTableCommand(), conn);\n            try { // sqlite doesn't have an \"if not exists\" modifier on \"add column\"\n                commands.createTable(commands.ensureColumnExistsCommand(\"transactions\", \"time\", commands.sqlInteger() + \" DEFAULT 0\"), conn);\n            } catch (SQLException f) {\n                if (!f.getMessage().contains(\"duplicate column\"))\n                    throw new RuntimeException(f);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public TransactionId startTransaction(PublicKeyHash owner) {\n        return new TransactionId(UUID.randomUUID().toString());\n    }\n\n    @Override\n    public void addBlock(Multihash hash, TransactionId tid, PublicKeyHash owner) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(commands.insertTransactionCommand())) {\n            insert.clearParameters();\n            insert.setString(1, tid.toString());\n            insert.setString(2, owner.toString());\n            insert.setString(3, hash.toString());\n            insert.setLong(4, System.currentTimeMillis());\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    @Override\n    public void closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        try (Connection conn = getConnection();\n             PreparedStatement delete = conn.prepareStatement(DELETE_TRANSACTION)) {\n            delete.setString(1, tid.toString());\n            delete.setString(2, owner.toString());\n            delete.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n        try (Connection conn = getConnection();\n             PreparedStatement delete = conn.prepareStatement(DELETE_OLD_TRANSACTIONS)) {\n            delete.setLong(1, cutoffMillis);\n            delete.setString(2, owner.toString());\n            delete.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(SELECT_TRANSACTIONS_BLOCKS)) {\n            select.setString(1, owner.toString());\n            ResultSet rs = select.executeQuery();\n            List<Cid> results = new ArrayList<>();\n            while (rs.next())\n            {\n                String tid = rs.getString(\"tid\");\n                String hash = rs.getString(\"hash\");\n                results.add(Cid.decode(hash));\n            }\n            return results;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    public synchronized void close() {\n        if (isClosed)\n            return;\n        isClosed = true;\n    }\n\n    public static JdbcTransactionStore build(Supplier<Connection> conn, SqlSupplier commands) {\n        return new JdbcTransactionStore(conn, commands);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/LinkRetrievalCounter.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.storage.*;\n\nimport java.time.*;\nimport java.util.*;\n\npublic interface LinkRetrievalCounter {\n\n    void increment(String owner, long label);\n\n    long getCount(String owner, long label);\n\n    Optional<LocalDateTime> getLatestModificationTime(String owner);\n\n    Optional<LocalDateTime> getLatestModificationTime();\n\n    void setCounts(String owner, LinkCounts counts);\n\n    LinkCounts getUpdatedCounts(String owner, LocalDateTime after);\n\n}\n"
  },
  {
    "path": "src/peergos/server/storage/LocalFirstStorage.java",
    "content": "package peergos.server.storage;\n\nimport io.libp2p.core.PeerId;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.ContentAddressedStorageProxy;\nimport peergos.shared.storage.IpnsEntry;\nimport peergos.shared.storage.TransactionId;\nimport peergos.shared.storage.auth.BatWithId;\nimport peergos.shared.util.Futures;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class LocalFirstStorage extends DelegatingDeletableStorage {\n\n    private final DeletableContentAddressedStorage local, dhtLookup;\n    private final ContentAddressedStorageProxy p2pGets;\n    private final PeerId ourId;\n    private final Cid ourNodeId;\n    private final Hasher hasher;\n\n    public LocalFirstStorage(DeletableContentAddressedStorage local,\n                             DeletableContentAddressedStorage dhtLookup,\n                             ContentAddressedStorageProxy p2pGets,\n                             List<PeerId> ourIds,\n                             Hasher hasher) {\n        super(local);\n        this.local = local;\n        this.dhtLookup = dhtLookup;\n        this.p2pGets = p2pGets;\n        this.ourId = ourIds.get(ourIds.size() - 1);\n        Multihash barePeerId = Multihash.decode(ourId.getBytes());\n        this.ourNodeId = new Cid(1, Cid.Codec.LibP2pKey, barePeerId.type, barePeerId.getHash());\n        this.hasher = hasher;\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean persistBlock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(hash.getHash()));\n        boolean localBlock = local.hasBlock(owner, hash);\n        if (localBlock)\n            return local.getRaw(peerIds, owner, hash, bat, ourId, h, persistBlock);\n        return p2pGets.getRaw(peerIds.get(0), owner, hash, bat).thenCompose(res -> {\n            if (res.isPresent() && persistBlock) {\n                return (hash.isRaw() ?\n                        local.putRaw(owner, owner, new byte[0], res.get(), new TransactionId(\"\"), x -> {}) :\n                        local.put(owner, owner, new byte[0], res.get(), new TransactionId(\"\")))\n                        .thenApply(x -> res);\n            }\n            return Futures.of(res);\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(hash.getHash()));\n        boolean localBlock = local.hasBlock(owner, hash);\n        if (localBlock)\n            return local.getRaw(peerIds, owner, hash, bat, ourId, h, doAuth, persistBlock);\n        if (peerIds.get(0).equals(ourId))\n            throw new IllegalStateException(\"We should have this block!\");\n        return p2pGets.getRaw(peerIds.get(0), owner, hash, bat).thenCompose(res -> {\n            if (res.isPresent() && persistBlock) {\n                return (hash.isRaw() ?\n                        local.putRaw(owner, owner, new byte[0], res.get(), new TransactionId(\"\"), x -> {}) :\n                        local.put(owner, owner, new byte[0], res.get(), new TransactionId(\"\")))\n                        .thenApply(x -> res);\n            }\n            return Futures.of(res);\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds,\n                                                       PublicKeyHash owner,\n                                                       Cid hash,\n                                                       String auth,\n                                                       boolean persistBlock) {\n        if (! auth.isEmpty())\n            throw new IllegalStateException(\"Can't retrieve private block!\");\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(CborObject.fromByteArray(hash.getHash())));\n        boolean localBlock = local.hasBlock(owner, hash);\n        if (localBlock)\n            return local.get(peerIds, owner, hash, auth, persistBlock);\n        return p2pGets.get(peerIds.get(0), owner, hash, Optional.empty()).thenCompose(res -> {\n            if (res.isPresent() && persistBlock) {\n                return (hash.isRaw() ?\n                        local.putRaw(owner, owner, new byte[0], res.get().serialize(), new TransactionId(\"\"), x -> {}) :\n                        local.put(owner, owner, new byte[0], res.get().serialize(), new TransactionId(\"\")))\n                        .thenApply(x -> res);\n            }\n            return Futures.of(res);\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds,\n                                                       PublicKeyHash owner,\n                                                       Cid hash,\n                                                       Optional<BatWithId> bat,\n                                                       Cid ourId,\n                                                       Hasher h,\n                                                       boolean persistblock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(CborObject.fromByteArray(hash.getHash())));\n        boolean localBlock = local.hasBlock(owner, hash);\n        if (localBlock)\n            return local.get(owner, hash, bat);\n        return p2pGets.get(peerIds.get(0), owner, hash, bat).thenCompose(res -> {\n            if (res.isPresent() && persistblock) {\n                return (hash.isRaw() ?\n                        local.putRaw(owner, owner, new byte[0], res.get().serialize(), new TransactionId(\"\"), x -> {}) :\n                        local.put(owner, owner, new byte[0], res.get().serialize(), new TransactionId(\"\")))\n                        .thenApply(x -> res);\n            }\n            return Futures.of(res);\n        });\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        return getRaw(Arrays.asList(ourNodeId), owner, block, Optional.empty(), ourNodeId, hasher, true)\n                .thenApply(rawOpt -> BlockMetadataStore.extractMetadata(block, rawOpt.get()));\n    }\n\n    @Override\n    public List<BlockMetadata> bulkGetLinks(List<Multihash> peerIds,\n                                            PublicKeyHash owner,\n                                            Cid ourId,\n                                            List<Cid> blocks,\n                                            Optional<BatWithId> mirrorBat,\n                                            Hasher h) {\n        List<Cid> localHashes = blocks.stream()\n                .filter(c -> local.hasBlock(owner, c))\n                .collect(Collectors.toList());\n        List<BlockMetadata> localMeta = localHashes.stream()\n                .map(c -> getBlockMetadata(owner, c).join())\n                .collect(Collectors.toList());\n        List<Cid> remoteHashes = blocks.stream()\n                .filter(c -> !localHashes.contains(c))\n                .collect(Collectors.toList());\n        List<BlockMetadata> remoteMeta = remoteHashes.stream()\n                .map(c -> p2pGets.get(peerIds.get(0), owner, c, mirrorBat).thenApply(res -> {\n                    if (res.isPresent()) {\n                        return (c.isRaw() ?\n                                local.putRaw(owner, owner, new byte[0], res.get().serialize(), new TransactionId(\"\"), x -> {}) :\n                                local.put(owner, owner, new byte[0], res.get().serialize(), new TransactionId(\"\")))\n                                .thenApply(x -> BlockMetadataStore.extractMetadata(c, res.get().serialize())).join();\n                    }\n                    throw new IllegalStateException(\"Couldn't retrieve \" + c);\n                }).join())\n                .collect(Collectors.toList());\n        return Stream.concat(localMeta.stream(), remoteMeta.stream())\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> mirror(String username, PublicKeyHash owner, PublicKeyHash writer, List<Multihash> peerIds, Optional<Cid> existing,\n                                               Optional<Cid> updated, Optional<BatWithId> mirrorBat, Cid ourNodeId,\n                                               NewBlocksProcessor newBlockProcessor, TransactionId tid, Hasher hasher) {\n        return DeletableContentAddressedStorage.mirror(username, owner, writer, peerIds, existing, updated, mirrorBat, ourNodeId, newBlockProcessor, tid, hasher, this);\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        return dhtLookup.getIpnsEntry(signer);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/LocalIpnsStorage.java",
    "content": "package peergos.server.storage;\n\nimport io.libp2p.core.*;\nimport org.peergos.protocol.ipns.pb.*;\nimport org.peergos.util.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.storage.*;\n\nimport java.nio.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class LocalIpnsStorage extends DelegatingDeletableStorage {\n\n    private final ServerIdentityStore ids;\n    private final List<Multihash> localIds;\n\n    public LocalIpnsStorage(DeletableContentAddressedStorage target, ServerIdentityStore ids) {\n        super(target);\n        this.ids = ids;\n        this.localIds = ids.getIdentities().stream()\n                .map(PeerId::getBytes)\n                .map(Multihash::decode)\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        if (localIds.contains(signer)) {\n            try {\n                Ipns.IpnsEntry proto = Ipns.IpnsEntry.parseFrom(ByteBuffer.wrap(ids.getRecord(new PeerId(signer.toBytes()))));\n                return Futures.of(new IpnsEntry(proto.getSignatureV2().toByteArray(), proto.getData().toByteArray()));\n            } catch (Exception e) {\n                return CompletableFuture.failedFuture(e);\n            }\n        }\n        return super.getIpnsEntry(signer);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/LocalS3Handler.java",
    "content": "package peergos.server.storage;\n\nimport com.sun.net.httpserver.*;\nimport org.w3c.dom.*;\n\nimport javax.crypto.*;\nimport javax.crypto.spec.*;\nimport javax.xml.parsers.*;\nimport java.io.*;\nimport java.net.*;\nimport java.nio.charset.*;\nimport java.nio.file.*;\nimport java.security.*;\nimport java.time.*;\nimport java.time.format.*;\nimport java.time.temporal.*;\nimport java.util.*;\nimport java.util.stream.*;\n\nclass LocalS3Handler implements HttpHandler {\n    private static final String ALGORITHM = \"AWS4-HMAC-SHA256\";\n    private static final String UNSIGNED = \"UNSIGNED-PAYLOAD\";\n\n    private final Path storageRoot;\n    private final String bucket;\n    private final String accessKey;\n    private final String secretKey;\n    private static final DateTimeFormatter S3_DATE = DateTimeFormatter.ofPattern(\"uuuu-MM-dd'T'HH:mm:ss'Z'\");\n\n    LocalS3Handler(Path storageRoot, String bucket, String accessKey, String secretKey) {\n        this.storageRoot = storageRoot;\n        this.bucket = bucket;\n        this.accessKey = accessKey;\n        this.secretKey = secretKey;\n    }\n\n    @Override\n    public void handle(HttpExchange exchange) throws IOException {\n        try {\n            verifySignature(exchange);\n            String method = exchange.getRequestMethod().toUpperCase();\n            String rawPath = exchange.getRequestURI().getRawPath();\n            Map<String, String> qp = parseQueryParams(exchange.getRequestURI().getRawQuery());\n\n            switch (method) {\n                case \"GET\":\n                    if (qp.containsKey(\"versions\"))\n                        handleListVersions(exchange, qp);\n                    else if (qp.containsKey(\"list-type\"))\n                        handleListObjects(exchange, qp);\n                    else\n                        handleGet(exchange, rawPath);\n                    break;\n                case \"HEAD\":\n                    handleHead(exchange, rawPath);\n                    break;\n                case \"PUT\":\n                    String copySource = firstHeader(exchange, \"x-amz-copy-source\");\n                    if (copySource != null)\n                        handleCopy(exchange, rawPath, copySource);\n                    else\n                        handlePut(exchange, rawPath);\n                    break;\n                case \"DELETE\":\n                    handleDelete(exchange, rawPath);\n                    break;\n                case \"POST\":\n                    if (qp.containsKey(\"delete\"))\n                        handleBulkDelete(exchange);\n                    else\n                        sendXmlError(exchange, 400, \"InvalidRequest\", \"Unknown POST\");\n                    break;\n                default:\n                    sendXmlError(exchange, 405, \"MethodNotAllowed\", method);\n            }\n        } catch (SignatureException e) {\n            sendXmlError(exchange, 403, \"SignatureDoesNotMatch\", e.getMessage());\n        } catch (FileNotFoundException e) {\n            sendXmlError(exchange, 404, \"NoSuchKey\", \"The specified key does not exist.\");\n        } catch (Exception e) {\n            sendXmlError(exchange, 500, \"InternalError\", e.getMessage() != null ? e.getMessage() : e.getClass().getName());\n        }\n    }\n\n    // ── Storage helpers ──────────────────────────────────────────────────────\n\n    private Path keyToPath(String rawPath) throws IOException {\n        String key = rawPath.startsWith(\"/\") ? rawPath.substring(1) : rawPath;\n        key = URLDecoder.decode(key, \"UTF-8\");\n        Path target = storageRoot.resolve(key).normalize();\n        if (!target.startsWith(storageRoot))\n            throw new IOException(\"Path traversal: \" + rawPath);\n        return target;\n    }\n\n    private void handleGet(HttpExchange exchange, String rawPath) throws IOException {\n        Path file = keyToPath(rawPath);\n        if (!Files.exists(file)) throw new FileNotFoundException(rawPath);\n        byte[] data = Files.readAllBytes(file);\n        exchange.getResponseHeaders().set(\"Content-Type\", \"application/octet-stream\");\n        exchange.getResponseHeaders().set(\"ETag\", \"\\\"\" + etag(data) + \"\\\"\");\n        exchange.sendResponseHeaders(200, data.length);\n        try (OutputStream os = exchange.getResponseBody()) { os.write(data); }\n    }\n\n    private void handleHead(HttpExchange exchange, String rawPath) throws IOException {\n        Path file = keyToPath(rawPath);\n        if (!Files.exists(file)) throw new FileNotFoundException(rawPath);\n        exchange.getResponseHeaders().set(\"Content-Length\", String.valueOf(Files.size(file)));\n        exchange.sendResponseHeaders(200, -1);\n    }\n\n    private void handlePut(HttpExchange exchange, String rawPath) throws IOException {\n        Path file = keyToPath(rawPath);\n        Files.createDirectories(file.getParent());\n        byte[] body = exchange.getRequestBody().readAllBytes();\n        Files.write(file, body);\n        exchange.getResponseHeaders().set(\"ETag\", \"\\\"\" + etag(body) + \"\\\"\");\n        exchange.sendResponseHeaders(200, 0);\n        exchange.getResponseBody().close();\n    }\n\n    private void handleCopy(HttpExchange exchange, String rawPath, String copySource) throws IOException {\n        // copySource is /bucket/key or bucket/key\n        String srcPath = copySource.startsWith(\"/\") ? copySource : \"/\" + copySource;\n        Path src = keyToPath(srcPath);\n        if (!Files.exists(src)) throw new FileNotFoundException(copySource);\n        Path dst = keyToPath(rawPath);\n        Files.createDirectories(dst.getParent());\n        Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);\n        String modified = Instant.now().truncatedTo(ChronoUnit.SECONDS)\n                .atOffset(ZoneOffset.UTC).format(S3_DATE);\n        sendXml(exchange, 200,\n                \"<CopyObjectResult><LastModified>\" + modified + \"Z</LastModified><ETag>\\\"copied\\\"</ETag></CopyObjectResult>\");\n    }\n\n    private void handleDelete(HttpExchange exchange, String rawPath) throws IOException {\n        Path file = keyToPath(rawPath);\n        Files.deleteIfExists(file);\n        exchange.sendResponseHeaders(204, -1);\n    }\n\n    private void handleBulkDelete(HttpExchange exchange) throws IOException {\n        byte[] body = exchange.getRequestBody().readAllBytes();\n        List<String> keys = parseDeleteXml(body);\n        StringBuilder sb = new StringBuilder(\"<DeleteResult>\");\n        for (String key : keys) {\n            Files.deleteIfExists(keyToPath(\"/\" + key));\n            sb.append(\"<Deleted><Key>\").append(escapeXml(key)).append(\"</Key></Deleted>\");\n        }\n        sb.append(\"</DeleteResult>\");\n        sendXml(exchange, 200, sb.toString());\n    }\n\n    private void handleListVersions(HttpExchange exchange, Map<String, String> qp) throws IOException {\n        String prefix = qp.getOrDefault(\"prefix\", \"\");\n        int maxKeys = Integer.parseInt(qp.getOrDefault(\"max-keys\", \"1000\"));\n        String keyMarker = qp.getOrDefault(\"key-marker\", \"\");\n\n        List<String> keys = listKeysWithPrefix(prefix).stream()\n                .filter(k -> keyMarker.isEmpty() || k.compareTo(keyMarker) > 0)\n                .collect(Collectors.toList());\n\n        boolean truncated = keys.size() > maxKeys;\n        List<String> page = truncated ? keys.subList(0, maxKeys) : keys;\n\n        StringBuilder sb = new StringBuilder(\"<ListVersionsResult>\");\n        sb.append(\"<IsTruncated>\").append(truncated).append(\"</IsTruncated>\");\n        if (truncated) {\n            String last = page.get(page.size() - 1);\n            sb.append(\"<NextKeyMarker>\").append(escapeXml(last)).append(\"</NextKeyMarker>\");\n            sb.append(\"<NextVersionIdMarker>null</NextVersionIdMarker>\");\n        }\n        for (String key : page) {\n            Path file = storageRoot.resolve(key);\n            long size = Files.size(file);\n            String modified = Files.getLastModifiedTime(file).toInstant()\n                    .truncatedTo(ChronoUnit.SECONDS).atOffset(ZoneOffset.UTC)\n                    .format(S3_DATE);\n            sb.append(\"<Version>\");\n            sb.append(\"<Key>\").append(escapeXml(key)).append(\"</Key>\");\n            sb.append(\"<VersionId>null</VersionId>\");\n            sb.append(\"<IsLatest>true</IsLatest>\");\n            sb.append(\"<LastModified>\").append(modified).append(\"</LastModified>\");\n            sb.append(\"<ETag>\\\"etag\\\"</ETag>\");\n            sb.append(\"<Size>\").append(size).append(\"</Size>\");\n            sb.append(\"</Version>\");\n        }\n        sb.append(\"</ListVersionsResult>\");\n        sendXml(exchange, 200, sb.toString());\n    }\n\n    private void handleListObjects(HttpExchange exchange, Map<String, String> qp) throws IOException {\n        String prefix = qp.getOrDefault(\"prefix\", \"\");\n        int maxKeys = Integer.parseInt(qp.getOrDefault(\"max-keys\", \"1000\"));\n        String contToken = qp.getOrDefault(\"continuation-token\", \"\");\n\n        List<String> keys = listKeysWithPrefix(prefix).stream()\n                .filter(k -> contToken.isEmpty() || k.compareTo(contToken) > 0)\n                .collect(Collectors.toList());\n\n        boolean truncated = keys.size() > maxKeys;\n        List<String> page = truncated ? keys.subList(0, maxKeys) : keys;\n\n        StringBuilder sb = new StringBuilder(\"<ListBucketResult>\");\n        sb.append(\"<IsTruncated>\").append(truncated).append(\"</IsTruncated>\");\n        if (truncated)\n            sb.append(\"<NextContinuationToken>\").append(escapeXml(page.get(page.size() - 1))).append(\"</NextContinuationToken>\");\n        for (String key : page) {\n            Path file = storageRoot.resolve(key);\n            long size = Files.size(file);\n            String modified = Files.getLastModifiedTime(file).toInstant()\n                    .truncatedTo(ChronoUnit.SECONDS).atOffset(ZoneOffset.UTC)\n                    .format(S3_DATE);\n            sb.append(\"<Contents>\");\n            sb.append(\"<Key>\").append(escapeXml(key)).append(\"</Key>\");\n            sb.append(\"<LastModified>\").append(modified).append(\"</LastModified>\");\n            sb.append(\"<ETag>\\\"etag\\\"</ETag>\");\n            sb.append(\"<Size>\").append(size).append(\"</Size>\");\n            sb.append(\"</Contents>\");\n        }\n        sb.append(\"</ListBucketResult>\");\n        sendXml(exchange, 200, sb.toString());\n    }\n\n    private List<String> listKeysWithPrefix(String prefix) throws IOException {\n        Path prefixPath = storageRoot.resolve(prefix).normalize();\n        Path walkFrom = Files.isDirectory(prefixPath) ? prefixPath : prefixPath.getParent();\n        if (walkFrom == null || !walkFrom.startsWith(storageRoot) || !Files.exists(walkFrom))\n            return Collections.emptyList();\n        try (Stream<Path> stream = Files.walk(walkFrom)) {\n            return stream\n                    .filter(Files::isRegularFile)\n                    .map(p -> storageRoot.relativize(p).toString().replace(File.separatorChar, '/'))\n                    .filter(k -> k.startsWith(prefix))\n                    .sorted()\n                    .collect(Collectors.toList());\n        }\n    }\n\n    private static List<String> parseDeleteXml(byte[] body) {\n        try {\n            DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();\n            Document doc = db.parse(new ByteArrayInputStream(body));\n            NodeList objects = doc.getElementsByTagName(\"Object\");\n            List<String> keys = new ArrayList<>();\n            for (int i = 0; i < objects.getLength(); i++) {\n                NodeList children = objects.item(i).getChildNodes();\n                for (int j = 0; j < children.getLength(); j++) {\n                    Node child = children.item(j);\n                    if (\"Key\".equals(child.getNodeName()))\n                        keys.add(child.getTextContent());\n                }\n            }\n            return keys;\n        } catch (Exception e) {\n            throw new RuntimeException(\"Failed to parse delete XML\", e);\n        }\n    }\n\n    // ── Signature verification ───────────────────────────────────────────────\n\n    private void verifySignature(HttpExchange exchange) throws SignatureException {\n        try {\n            URI uri = exchange.getRequestURI();\n            String rawQuery = uri.getRawQuery();\n            Map<String, String> qp = parseQueryParams(rawQuery);\n            Headers headers = exchange.getRequestHeaders();\n\n            boolean queryParamAuth = qp.containsKey(\"X-Amz-Signature\");\n            String authHeader = firstHeader(exchange, \"authorization\");\n            boolean headerAuth = authHeader != null && authHeader.startsWith(ALGORITHM);\n\n            if (!queryParamAuth && !headerAuth)\n                throw new SignatureException(\"No AWS authentication found\");\n\n            String providedSig, datetime, signedHeaders, credentialScope, payloadHash;\n\n            if (queryParamAuth) {\n                providedSig = urlDecode(qp.get(\"X-Amz-Signature\"));\n                datetime = urlDecode(qp.getOrDefault(\"X-Amz-Date\", \"\"));\n                signedHeaders = urlDecode(qp.getOrDefault(\"X-Amz-SignedHeaders\", \"host\"));\n                String cred = urlDecode(qp.getOrDefault(\"X-Amz-Credential\", \"\"));\n                int slash = cred.indexOf('/');\n                credentialScope = slash >= 0 ? cred.substring(slash + 1) : cred;\n                payloadHash = UNSIGNED;\n            } else {\n                Map<String, String> authParts = parseAuthorization(authHeader);\n                providedSig = authParts.getOrDefault(\"Signature\", \"\");\n                String cred = authParts.getOrDefault(\"Credential\", \"\");\n                int slash = cred.indexOf('/');\n                credentialScope = slash >= 0 ? cred.substring(slash + 1) : cred;\n                signedHeaders = authParts.getOrDefault(\"SignedHeaders\", \"\");\n                datetime = firstHeader(exchange, \"x-amz-date\");\n                if (datetime == null) datetime = \"\";\n                payloadHash = firstHeader(exchange, \"x-amz-content-sha256\");\n                if (payloadHash == null) payloadHash = UNSIGNED;\n            }\n\n            if (datetime.length() < 8)\n                throw new SignatureException(\"Missing or invalid x-amz-date\");\n            String shortDate = datetime.substring(0, 8);\n\n            // Extract region from credentialScope: DATE/REGION/s3/aws4_request\n            String[] scopeParts = credentialScope.split(\"/\");\n            String region = scopeParts.length > 1 ? scopeParts[1] : \"us-east-1\";\n\n            // Canonical request\n            String method = exchange.getRequestMethod().toUpperCase();\n            String canonicalUri = uri.getRawPath();\n            if (canonicalUri == null || canonicalUri.isEmpty()) canonicalUri = \"/\";\n            String canonicalQS = buildCanonicalQueryString(rawQuery, queryParamAuth);\n            String canonicalHeaders = buildCanonicalHeaders(exchange, signedHeaders);\n\n            String canonicalRequest = method + \"\\n\" + canonicalUri + \"\\n\" + canonicalQS + \"\\n\" +\n                    canonicalHeaders + \"\\n\" + signedHeaders + \"\\n\" + payloadHash;\n\n            String stringToSign = ALGORITHM + \"\\n\" + datetime + \"\\n\" + credentialScope + \"\\n\" +\n                    hex(sha256(canonicalRequest.getBytes(StandardCharsets.UTF_8)));\n\n            byte[] signingKey = deriveSigningKey(secretKey, shortDate, region);\n            String expectedSig = hex(hmac(signingKey, stringToSign.getBytes(StandardCharsets.UTF_8)));\n\n            if (!MessageDigest.isEqual(expectedSig.getBytes(StandardCharsets.UTF_8),\n                    providedSig.getBytes(StandardCharsets.UTF_8)))\n                throw new SignatureException(\"Signature mismatch\");\n\n        } catch (SignatureException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new SignatureException(\"Signature verification error: \" + e.getMessage());\n        }\n    }\n\n    private static String buildCanonicalQueryString(String rawQuery, boolean queryParamAuth) {\n        if (rawQuery == null || rawQuery.isEmpty()) return \"\";\n        List<String[]> params = new ArrayList<>();\n        for (String part : rawQuery.split(\"&\")) {\n            int eq = part.indexOf('=');\n            String k = eq < 0 ? part : part.substring(0, eq);\n            String v = eq < 0 ? \"\" : part.substring(eq + 1);\n            String dk = urlDecode(k);\n            if (queryParamAuth && \"X-Amz-Signature\".equals(dk)) continue;\n            params.add(new String[]{dk, urlDecode(v)});\n        }\n        return params.stream()\n                .sorted(Comparator.comparing(p -> urlEncode(p[0])))\n                .map(p -> urlEncode(p[0]) + \"=\" + urlEncode(p[1]))\n                .collect(Collectors.joining(\"&\"));\n    }\n\n    private static String buildCanonicalHeaders(HttpExchange exchange, String signedHeaders) {\n        StringBuilder sb = new StringBuilder();\n        for (String name : signedHeaders.split(\";\")) {\n            String value;\n            if (\"host\".equalsIgnoreCase(name)) {\n                value = exchange.getRequestHeaders().getFirst(\"Host\");\n                if (value == null) {\n                    URI uri = exchange.getRequestURI();\n                    value = uri.getHost();\n                    if (uri.getPort() != -1) value += \":\" + uri.getPort();\n                }\n            } else {\n                value = firstHeader(exchange, name);\n            }\n            sb.append(name.toLowerCase()).append(\":\").append(value != null ? value.trim() : \"\").append(\"\\n\");\n        }\n        return sb.toString();\n    }\n\n    private static byte[] deriveSigningKey(String secret, String shortDate, String region) {\n        byte[] kDate   = hmac((\"AWS4\" + secret).getBytes(StandardCharsets.UTF_8), shortDate.getBytes(StandardCharsets.UTF_8));\n        byte[] kRegion = hmac(kDate, region.getBytes(StandardCharsets.UTF_8));\n        byte[] kSvc    = hmac(kRegion, \"s3\".getBytes(StandardCharsets.UTF_8));\n        return hmac(kSvc, \"aws4_request\".getBytes(StandardCharsets.UTF_8));\n    }\n\n    // ── Crypto utils ─────────────────────────────────────────────────────────\n\n    private static byte[] hmac(byte[] key, byte[] data) {\n        try {\n            Mac mac = Mac.getInstance(\"HmacSHA256\");\n            mac.init(new SecretKeySpec(key, \"HmacSHA256\"));\n            return mac.doFinal(data);\n        } catch (GeneralSecurityException e) { throw new RuntimeException(e); }\n    }\n\n    private static byte[] sha256(byte[] data) {\n        try {\n            return MessageDigest.getInstance(\"SHA-256\").digest(data);\n        } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); }\n    }\n\n    private static String hex(byte[] b) {\n        StringBuilder sb = new StringBuilder(b.length * 2);\n        for (byte v : b) sb.append(String.format(\"%02x\", v & 0xff));\n        return sb.toString();\n    }\n\n    private static String etag(byte[] data) {\n        return hex(sha256(data));\n    }\n\n    // ── HTTP / string utils ──────────────────────────────────────────────────\n\n    private static Map<String, String> parseQueryParams(String rawQuery) {\n        if (rawQuery == null || rawQuery.isEmpty()) return Collections.emptyMap();\n        Map<String, String> result = new LinkedHashMap<>();\n        for (String part : rawQuery.split(\"&\")) {\n            int eq = part.indexOf('=');\n            if (eq < 0) result.put(urlDecode(part), \"\");\n            else result.put(urlDecode(part.substring(0, eq)), urlDecode(part.substring(eq + 1)));\n        }\n        return result;\n    }\n\n    private static Map<String, String> parseAuthorization(String auth) {\n        // \"AWS4-HMAC-SHA256 Credential=X,SignedHeaders=Y,Signature=Z\"\n        Map<String, String> result = new HashMap<>();\n        int space = auth.indexOf(' ');\n        if (space < 0) return result;\n        for (String part : auth.substring(space + 1).split(\",\")) {\n            int eq = part.indexOf('=');\n            if (eq > 0) result.put(part.substring(0, eq).trim(), part.substring(eq + 1).trim());\n        }\n        return result;\n    }\n\n    private static String firstHeader(HttpExchange exchange, String name) {\n        for (Map.Entry<String, List<String>> e : exchange.getRequestHeaders().entrySet()) {\n            if (e.getKey() != null && e.getKey().equalsIgnoreCase(name)) {\n                List<String> vals = e.getValue();\n                return vals.isEmpty() ? null : vals.get(0);\n            }\n        }\n        return null;\n    }\n\n    private static String urlDecode(String s) {\n        try { return URLDecoder.decode(s, \"UTF-8\"); }\n        catch (Exception e) { return s; }\n    }\n\n    private static String urlEncode(String s) {\n        try { return URLEncoder.encode(s, \"UTF-8\"); }\n        catch (Exception e) { return s; }\n    }\n\n    private static String escapeXml(String s) {\n        return s.replace(\"&\", \"&amp;\").replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\");\n    }\n\n    private static void sendXml(HttpExchange exchange, int code, String body) throws IOException {\n        byte[] bytes = (\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\" + body).getBytes(StandardCharsets.UTF_8);\n        exchange.getResponseHeaders().set(\"Content-Type\", \"application/xml\");\n        exchange.sendResponseHeaders(code, bytes.length);\n        try (OutputStream os = exchange.getResponseBody()) { os.write(bytes); }\n    }\n\n    private static void sendXmlError(HttpExchange exchange, int code, String error, String message) throws IOException {\n        sendXml(exchange, code, \"<Error><Code>\" + error + \"</Code><Message>\" + escapeXml(message != null ? message : \"\") + \"</Message></Error>\");\n    }\n\n    private static class SignatureException extends Exception {\n        SignatureException(String msg) { super(msg); }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/LocalS3Server.java",
    "content": "package peergos.server.storage;\n\nimport com.sun.net.httpserver.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class LocalS3Server {\n    private final HttpServer server;\n\n    public LocalS3Server(Path storageRoot, String bucket, String accessKey, String secretKey, int port) throws IOException {\n        server = HttpServer.create(new InetSocketAddress(port), 128);\n        server.createContext(\"/\", new LocalS3Handler(storageRoot, bucket, accessKey, secretKey));\n        server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());\n    }\n\n    public void start() {\n        server.start();\n    }\n\n    public void stop() {\n        server.stop(0);\n    }\n\n    /**\n     * Returns an S3Config wired to this local server.\n     * S3BlockStorage in non-HTTPS mode prepends the bucket to all paths, so\n     * a block stored as \"key\" will live at storageRoot/bucket/key.\n     */\n    public static S3Config getConfig(String bucket, String accessKey, String secretKey, int port) {\n        return new S3Config(\"\", bucket, \"us-east-1\", accessKey, secretKey, \"localhost:\" + port, Optional.empty());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/LocalVersionInstanceAdmin.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.UserService;\nimport peergos.server.storage.admin.Admin;\nimport peergos.shared.storage.controller.HttpInstanceAdmin;\nimport peergos.shared.user.HttpPoster;\nimport peergos.shared.util.Futures;\n\nimport java.util.concurrent.CompletableFuture;\n\npublic class LocalVersionInstanceAdmin extends HttpInstanceAdmin {\n\n    public LocalVersionInstanceAdmin(HttpPoster poster) {\n        super(poster);\n    }\n\n    @Override\n    public CompletableFuture<VersionInfo> getVersionInfo() {\n        return Futures.of(new VersionInfo(UserService.CURRENT_VERSION, Admin.getSourceVersion()));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/MetadataCachingStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.space.UsageStore;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.logging.*;\n\npublic class MetadataCachingStorage extends DelegatingDeletableStorage {\n\n    private static final Logger LOG = Logger.getGlobal();\n    private final DeletableContentAddressedStorage target;\n    private final BlockMetadataStore metadata;\n    private final UsageStore usage;\n    private final Hasher hasher;\n\n    public MetadataCachingStorage(DeletableContentAddressedStorage target,\n                                  BlockMetadataStore metadata,\n                                  UsageStore usage,\n                                  Hasher hasher) {\n        super(target);\n        this.target = target;\n        this.metadata = metadata;\n        this.usage = usage;\n        this.hasher = hasher;\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n        super.setPki(pki);\n        updateMetadataStoreIfEmpty();\n    }\n\n    public void updateMetadataStoreIfEmpty() {\n        if (! metadata.isEmpty())\n            return;\n        LOG.info(\"Populating block metadata db..\");\n        target.getAllBlockHashes(true).forEach(p -> {\n            Optional<BlockMetadata> existing = metadata.get(p.right);\n            if (existing.isEmpty())\n                metadata.put(p.left, p.right, null, target.getBlockMetadata(p.left, p.right).join());\n        });\n    }\n\n    @Override\n    public Optional<BlockMetadataStore> getBlockMetadataStore() {\n        return Optional.of(metadata);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid) {\n        return target.put(owner, writer, signedHashes, blocks, tid)\n                .thenApply(cids -> {\n                    for (int i=0; i < cids.size(); i++)\n                        metadata.put(owner, cids.get(i), null, blocks.get(i));\n                    return cids;\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid, ProgressConsumer<Long> progressCounter) {\n        return target.putRaw(owner, writer, signedHashes, blocks, tid, progressCounter)\n                .thenApply(cids -> {\n                    for (int i=0; i < cids.size(); i++)\n                        metadata.put(owner, cids.get(i), null, blocks.get(i));\n                    return cids;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        Optional<BlockMetadata> meta = metadata.get((Cid) block);\n        if (meta.isPresent())\n            return Futures.of(Optional.of(meta.get().size));\n        return target.getSize(owner, block);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid block, List<Multihash> peerids) {\n        if (block.isRaw())\n            return Futures.of(Collections.emptyList());\n        if (block.isIdentity())\n            return Futures.of(CborObject.getLinks(block, block.getHash()));\n        Optional<BlockMetadata> meta = metadata.get(block);\n        if (meta.isPresent())\n            return Futures.of(meta.get().links);\n        return getBlockMetadata(owner, block).thenApply(res -> res.links);\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        if (block.isIdentity())\n            return Futures.of(BlockMetadataStore.extractMetadata(block, block.getHash()));\n        Optional<BlockMetadata> meta = metadata.get(block);\n        if (meta.isPresent())\n            return Futures.of(meta.get());\n        return target.getBlockMetadata(owner, block);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return target.getChampLookup(owner, root, caps, committedRoot);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return target.get(owner, hash, bat);\n    }\n\n    private BlockMetadata writeBlockMetadata(PublicKeyHash owner, byte[] block, boolean isRaw) {\n        Cid cid = hashToCid(block, isRaw, hasher).join();\n        return metadata.put(owner, cid, null, block);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        return target.get(peerIds, owner, hash, auth, persistBlock).thenApply(res -> {\n            if (persistBlock)\n                res.ifPresent(cbor -> writeBlockMetadata(owner, cbor.toByteArray(), hash.isRaw()));\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean persistBlock) {\n        return target.get(peerIds, owner, hash, bat, ourId, h, persistBlock).thenApply(res -> {\n            if (persistBlock)\n                res.ifPresent(cbor -> writeBlockMetadata(owner, cbor.toByteArray(), hash.isRaw()));\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean doAuth, boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, auth, doAuth, persistBlock).thenApply(bopt -> {\n            if (persistBlock)\n                bopt.ifPresent(b -> writeBlockMetadata(owner, b, hash.isRaw()));\n            return bopt;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, bat, ourId, h, persistBlock).thenApply(bopt -> {\n            if (persistBlock)\n                bopt.ifPresent(b -> writeBlockMetadata(owner, b, hash.isRaw()));\n            return bopt;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean doAuth, boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, bat, ourId, h, doAuth, persistBlock).thenApply(bopt -> {\n            if (persistBlock)\n                bopt.ifPresent(b -> writeBlockMetadata(owner, b, hash.isRaw()));\n            return bopt;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, auth, persistBlock).thenApply(bopt -> {\n            if (persistBlock)\n                bopt.ifPresent(b -> writeBlockMetadata(owner, b, hash.isRaw()));\n            return bopt;\n        });\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> mirror(String username, PublicKeyHash owner, PublicKeyHash writer, List<Multihash> peerIds, Optional<Cid> existing,\n                                               Optional<Cid> updated, Optional<BatWithId> mirrorBat, Cid ourNodeId,\n                                               NewBlocksProcessor newBlockProcessor, TransactionId tid, Hasher hasher) {\n        return target.mirror(username, owner, writer, peerIds, existing, updated, mirrorBat, ourNodeId,\n                (w, bs, size) -> usage.addPendingUsage(username, w, addMetadata(peerIds, owner, bs, mirrorBat, hasher)), tid, hasher);\n    }\n\n    private int addMetadata(List<Multihash> peerIds, PublicKeyHash owner, List<Cid> hashes, Optional<BatWithId> mirrorBat, Hasher h) {\n        int totalSize = 0;\n        Cid us = id().join();\n        for (Cid c : hashes) {\n            totalSize += target.getRaw(peerIds, owner, c, mirrorBat, us, h, false)\n                    .thenApply(bopt -> bopt.map(b -> writeBlockMetadata(owner, b, c.isRaw()).size)\n                            .orElse(0))\n                    .join();\n        }\n        return totalSize;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/MultiIdStorage.java",
    "content": "package peergos.server.storage;\n\nimport io.libp2p.core.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class MultiIdStorage extends DelegatingDeletableStorage {\n\n    private final List<Cid> ourIds;\n\n    public MultiIdStorage(DeletableContentAddressedStorage target, List<PeerId> ourIds) {\n        super(target);\n        this.ourIds = ourIds.stream()\n                .map(PeerId::getBytes)\n                .map(Multihash::decode)\n                .map(m -> new Cid(1, Cid.Codec.LibP2pKey, m.type, m.getHash()))\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(ourIds.get(ourIds.size() - 1));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return Futures.of(ourIds);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/NewBlocksProcessor.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.List;\n\npublic interface NewBlocksProcessor {\n\n    void process(PublicKeyHash writer, List<Cid> blocks, int totalSize);\n}\n"
  },
  {
    "path": "src/peergos/server/storage/NonWriteThroughStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class NonWriteThroughStorage implements ContentAddressedStorage {\n    private final ContentAddressedStorage source;\n    private final ContentAddressedStorage modifications;\n\n    public NonWriteThroughStorage(ContentAddressedStorage source, Hasher hasher) {\n        this.source = source;\n        this.modifications = new RAMStorage(hasher);\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return source.id();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return source.ids();\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return source.linkHost(owner);\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return modifications.startTransaction(owner);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return modifications.closeTransaction(owner, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return modifications.getChampLookup(owner, root, caps, committedRoot);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return modifications.put(owner, writer, signedHashes, blocks, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return modifications.putRaw(owner, writer, signatures, blocks, tid, progressConsumer);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid object, Optional<BatWithId> bat) {\n        try {\n            Optional<byte[]> modified = modifications.getRaw(owner, object, bat).get();\n            if ( modified.isPresent())\n                return CompletableFuture.completedFuture(modified);\n            return source.getRaw(owner, object, bat);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        try {\n            Optional<CborObject> modified = modifications.get(owner, hash, bat).get();\n            if ( modified.isPresent())\n                return CompletableFuture.completedFuture(modified);\n            return source.get(owner, hash, bat);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        try {\n            Optional<CborObject> modified = modifications.get(null, (Cid)block, Optional.empty()).get();\n            if (modified.isPresent())\n                return CompletableFuture.completedFuture(modified.map(cbor -> cbor.toByteArray().length));\n            return source.getSize(owner, block);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return source.getBlockCache();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/P2pBlockGet.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.BatWithId;\n\nimport java.util.List;\nimport java.util.Optional;\n\npublic interface P2pBlockGet {\n\n    List<BlockMetadata> bulkGet(List<Multihash> peerid, PublicKeyHash owner, List<Cid> blocks, Optional<BatWithId> mirrorBat);\n}\n"
  },
  {
    "path": "src/peergos/server/storage/PartitionStatus.java",
    "content": "package peergos.server.storage;\n\npublic interface PartitionStatus {\n\n    boolean isDone();\n\n    void complete();\n\n    PartitionStatus DONE = new PartitionStatus() {\n        @Override\n        public boolean isDone() {\n            return true;\n        }\n\n        @Override\n        public void complete() {\n\n        }\n    };\n}\n"
  },
  {
    "path": "src/peergos/server/storage/RAMStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.space.UsageStore;\nimport peergos.server.storage.auth.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.security.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class RAMStorage implements DeletableContentAddressedStorage {\n    private static final int CID_V1 = 1;\n\n    private Map<PublicKeyHash, Map<Cid, byte[]>> storage = new EfficientHashMap<>();\n    private Map<TransactionId, List<Cid>> openTransactions = new ConcurrentHashMap<>();\n    private final Set<Cid> pinnedRoots = new HashSet<>();\n    private final Hasher hasher;\n\n    public RAMStorage(Hasher hasher) {\n        this.hasher = hasher;\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {}\n\n    @Override\n    public void partitionByUser(UsageStore usage,\n                                JdbcIpnsAndSocial mutable,\n                                PublicKeyHash pkiKey) {}\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return CompletableFuture.completedFuture(new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, new byte[32]));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return CompletableFuture.completedFuture(List.of(new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, new byte[32])));\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return Futures.of(\"localhost:8000\");\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        TransactionId tid = new TransactionId(Long.toString(System.currentTimeMillis()));\n        openTransactions.put(tid, new ArrayList<>());\n        return CompletableFuture.completedFuture(tid);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        openTransactions.remove(tid);\n        return CompletableFuture.completedFuture(true);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return getChampLookup(owner, root, caps, committedRoot, hasher);\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        return storage.getOrDefault(owner, Collections.emptyMap()).keySet()\n                .stream()\n                .map(c -> new Pair<>(owner, c));\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore) {\n        return storage.entrySet().stream()\n                .flatMap(e -> e.getValue()\n                        .keySet()\n                        .stream()\n                        .map(c -> new Pair<>(e.getKey(), c)));\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        res.accept(getAllBlockHashes(owner, false)\n                .map(p -> new BlockVersion(p.right, null, true))\n                .collect(Collectors.toList()));\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid hash) {\n        storage.get(owner).remove(hash);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return openTransactions.values()\n                .stream()\n                .flatMap(List::stream)\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return put(owner, blocks, false, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return put(owner, blocks, true, tid);\n    }\n\n    private CompletableFuture<List<Cid>> put(PublicKeyHash owner, List<byte[]> blocks, boolean isRaw, TransactionId tid) {\n        return CompletableFuture.completedFuture(blocks.stream()\n                .map(b -> {\n                    Cid cid = hashToCid(b, isRaw);\n                    put(owner, cid, b);\n                    openTransactions.get(tid).add(cid);\n                    return cid;\n                }).collect(Collectors.toList()));\n    }\n\n    private synchronized void put(PublicKeyHash owner, Cid cid, byte[] data) {\n        Map<Cid, byte[]> userStorage = forUser(owner);\n        userStorage.put(cid, data);\n    }\n\n    private Map<Cid, byte[]> forUser(PublicKeyHash owner) {\n        Map<Cid, byte[]> res = storage.get(owner);\n        if (res != null)\n            return res;\n        EfficientHashMap<Cid, byte[]> val = new EfficientHashMap<>();\n        storage.put(owner, val);\n        return val;\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        Map<Cid, byte[]> userStorage = forUser(owner);\n        return CompletableFuture.completedFuture(userStorage.containsKey(hash) ?\n                Optional.of(userStorage.get(hash)) :\n                Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid object, String auth, boolean persistBlock) {\n        Map<Cid, byte[]> userStorage = forUser(owner);\n        return CompletableFuture.completedFuture(userStorage.containsKey(object) ?\n                Optional.of(userStorage.get(object)) :\n                Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return getRaw(Collections.emptyList(), owner, hash, bat, id().join(), hasher, false);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        if (hash.codec == Cid.Codec.Raw)\n            throw new IllegalStateException(\"Need to call getRaw if cid is not cbor!\");\n        return CompletableFuture.completedFuture(getAndParseObject(owner, hash));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return get(Collections.emptyList(), owner, hash, bat, id().join(), hasher, false);\n    }\n\n    private synchronized Optional<CborObject> getAndParseObject(PublicKeyHash owner, Multihash hash) {\n        Map<Cid, byte[]> userStorage = forUser(owner);\n        if (! userStorage.containsKey(hash))\n            return Optional.empty();\n        return Optional.of(CborObject.fromByteArray(userStorage.get(hash)));\n    }\n\n    public synchronized void clear() {\n        storage.clear();\n    }\n\n    public synchronized int size() {\n        return storage.values().stream().mapToInt(Map::size).sum();\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        return getRaw(Arrays.asList(id().join()), owner, block, Optional.empty(), id().join(), hasher, true)\n                .thenApply(rawOpt -> BlockMetadataStore.extractMetadata(block, rawOpt.get()));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        if (root.codec == Cid.Codec.Raw)\n            return CompletableFuture.completedFuture(Collections.emptyList());\n        return get(peerids, owner, root, \"\", false).thenApply(opt -> opt\n                .map(cbor -> cbor.links().stream().map(c -> (Cid)c).collect(Collectors.toList()))\n                .orElse(Collections.emptyList())\n        );\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        return storage.get(owner).containsKey(hash);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        Map<Cid, byte[]> userStorage = forUser(owner);\n        if (!userStorage.containsKey(block))\n            return CompletableFuture.completedFuture(Optional.empty());\n        return CompletableFuture.completedFuture(Optional.of(userStorage.get(block).length));\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Shouldn't get here.\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Shouldn't get here.\");\n    }\n\n    public static Cid hashToCid(byte[] input, boolean isRaw) {\n        byte[] hash = hash(input);\n        return new Cid(CID_V1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n\n    public static byte[] hash(byte[] input)\n    {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"SHA-256\");\n            md.update(input);\n            return md.digest();\n        } catch (NoSuchAlgorithmException e)\n        {\n            throw new IllegalStateException(\"couldn't find hash algorithm\");\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        RAMStorage that = (RAMStorage) o;\n        return Objects.equals(storage, that.storage) &&\n                Objects.equals(openTransactions, that.openTransactions) &&\n                Objects.equals(pinnedRoots, that.pinnedRoots) &&\n                Objects.equals(hasher, that.hasher);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(storage, openTransactions, pinnedRoots, hasher);\n    }\n\n    public int totalSize() {\n        return storage.values().stream()\n                .flatMap(m -> m.values().stream())\n                .mapToInt(a -> a.length).sum();\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/RamBlockMetadataStore.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class RamBlockMetadataStore implements BlockMetadataStore {\n\n    private final Map<Cid, BlockMetadata> store;\n\n    public RamBlockMetadataStore() {\n        this.store = new HashMap<>(50_000);\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return store.isEmpty();\n    }\n\n    @Override\n    public Optional<BlockMetadata> get(Cid block) {\n        return Optional.ofNullable(store.get(block));\n    }\n\n    @Override\n    public List<Cid> hasBlocks(List<Cid> blocks) {\n        return blocks.stream()\n                .filter(store::containsKey)\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public Map<Cid, BlockMetadata> getAll(List<Cid> blocks) {\n        return blocks.stream()\n                .filter(store::containsKey)\n                .collect(Collectors.toMap(h -> h, store::get));\n    }\n\n    @Override\n    public Optional<PublicKeyHash> getOwner(Cid block) {\n        return Optional.empty();\n    }\n\n    @Override\n    public void setOwner(PublicKeyHash owner, Cid block) {\n\n    }\n\n    @Override\n    public void setOwnerAndVersion(PublicKeyHash owner, Cid block, String version) {\n\n    }\n\n    @Override\n    public void put(PublicKeyHash owner, Cid block, String version, BlockMetadata meta) {\n        store.put(block, meta);\n    }\n\n    @Override\n    public void remove(Cid block) {\n        store.remove(block);\n    }\n\n    @Override\n    public void applyToAll(Consumer<Cid> action) {\n        store.keySet().stream().forEach(action);\n    }\n\n    @Override\n    public void applyToAllSizes(BiConsumer<Cid, Long> action) {\n        store.entrySet().stream().forEach(e -> action.accept(e.getKey(), (long) e.getValue().size));\n    }\n\n    @Override\n    public Stream<BlockVersion> list(PublicKeyHash owner) {\n        return store.keySet().stream().map(c -> new BlockVersion(c, null, true));\n    }\n\n    @Override\n    public void listCbor(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        res.accept(store.keySet()\n                .stream()\n                .filter(c -> ! c.isRaw())\n                .map(c -> new BlockVersion(c, null, true))\n                .collect(Collectors.toList()));\n    }\n\n    @Override\n    public long size(PublicKeyHash owner) {\n        return store.size();\n    }\n\n    @Override\n    public void compact() {}\n}\n"
  },
  {
    "path": "src/peergos/server/storage/RamLinkRetrievalCounter.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class RamLinkRetrievalCounter implements LinkRetrievalCounter {\n\n    private final Map<Pair<String, Long>, Pair<Long, LocalDateTime>> counts = new HashMap<>();\n\n    @Override\n    public synchronized void increment(String owner, long label) {\n        Pair<String, Long> key = new Pair<>(owner, label);\n        LocalDateTime now = LocalDateTime.now();\n        counts.putIfAbsent(key, new Pair<>(0L, now));\n        Pair<Long, LocalDateTime> current = counts.get(key);\n        counts.put(key, new Pair<>(current.left+1, now));\n    }\n\n    @Override\n    public long getCount(String owner, long label) {\n        return Optional.ofNullable(counts.get(new Pair<>(owner, label))).map(p -> p.left).orElse(0L);\n    }\n\n    @Override\n    public Optional<LocalDateTime> getLatestModificationTime(String owner) {\n        return counts.entrySet().stream()\n                .filter(e -> e.getKey().left.equals(owner))\n                .map(e -> e.getValue().right)\n                .sorted((a, b) -> -a.compareTo(b))\n                .findFirst();\n    }\n\n    @Override\n    public Optional<LocalDateTime> getLatestModificationTime() {\n        return counts.entrySet().stream()\n                .map(e -> e.getValue().right)\n                .sorted((a, b) -> -a.compareTo(b))\n                .findFirst();\n    }\n\n    @Override\n    public void setCounts(String owner, LinkCounts counts) {\n        counts.counts.forEach((k, v) -> {\n            this.counts.put(new Pair<>(owner, k), v);\n        });\n    }\n\n    @Override\n    public LinkCounts getUpdatedCounts(String owner, LocalDateTime after) {\n        return new LinkCounts(counts.entrySet()\n                .stream()\n                .filter(e -> e.getKey().left.equals(owner) && e.getValue().right.isAfter(after))\n                .collect(Collectors.toMap(e -> e.getKey().right, Map.Entry::getValue)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/RequestCountingStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\n\npublic class RequestCountingStorage extends DelegatingStorage {\n\n    private final ContentAddressedStorage target;\n    public AtomicInteger get = new AtomicInteger(0);\n    public AtomicInteger put = new AtomicInteger(0);\n    public AtomicInteger getRaw = new AtomicInteger(0);\n    public AtomicInteger putRaw = new AtomicInteger(0);\n    public AtomicInteger start = new AtomicInteger(0);\n    public AtomicInteger close = new AtomicInteger(0);\n    public AtomicInteger champGet = new AtomicInteger(0);\n\n    public RequestCountingStorage(ContentAddressedStorage target) {\n        super(target);\n        this.target = target;\n    }\n\n    public void reset() {\n        get.set(0);\n        put.set(0);\n        getRaw.set(0);\n        putRaw.set(0);\n        start.set(0);\n        close.set(0);\n        champGet.set(0);\n    }\n\n    public int requestTotal() {\n        return get.get() + put.get() + getRaw.get() + putRaw.get() + start.get() + close.get() + champGet.get();\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return target.startTransaction(owner).thenApply(res -> {\n            start.incrementAndGet();\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return target.closeTransaction(owner, tid).thenApply(res -> {\n            close.incrementAndGet();\n            return res;\n        });\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return target.directToOrigin();\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return target.getChampLookup(owner, root, caps, committedRoot)\n                .thenApply(blocks -> {\n                    champGet.incrementAndGet();\n                    return blocks;\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return target.put(owner, writer, signedHashes, blocks, tid)\n                .thenApply(res -> {\n                    put.incrementAndGet();\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        return target.get(owner, key, bat).thenApply(cborOpt -> {\n            get.incrementAndGet();\n            return cborOpt;\n        });\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return target.putRaw(owner, writer, signatures, blocks, tid, progressConsumer)\n                .thenApply(res -> {\n                    putRaw.incrementAndGet();\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        return target.getRaw(owner, key, bat).thenApply(rawOpt -> {\n            getRaw.incrementAndGet();\n            return rawOpt;\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/ResetableFileInputStream.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.user.fs.*;\n\nimport java.io.*;\nimport java.util.concurrent.*;\n\npublic class ResetableFileInputStream implements AsyncReader {\n\n    private final RandomAccessFile raf;\n\n    public ResetableFileInputStream(RandomAccessFile raf) {\n        this.raf = raf;\n    }\n\n    public ResetableFileInputStream(File f) throws IOException {\n        this(new RandomAccessFile(f, \"r\"));\n    }\n\n    @Override\n    public CompletableFuture<AsyncReader> seekJS(int high32, int low32) {\n        try {\n            raf.seek((low32 & 0xFFFFFFFFL) + (high32 & 0xFFFFFFFFL) << 32);\n            return CompletableFuture.completedFuture(this);\n        } catch (IOException e) {\n            CompletableFuture<AsyncReader> err = new CompletableFuture<>();\n            err.completeExceptionally(e);\n            return err;\n        }\n    }\n\n    @Override\n    public CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length) {\n        try {\n            int read = raf.read(res, offset, length);\n            return CompletableFuture.completedFuture(read);\n        } catch (IOException e) {\n            CompletableFuture<Integer> err = new CompletableFuture<>();\n            err.completeExceptionally(e);\n            return err;\n        }\n    }\n\n    @Override\n    public synchronized CompletableFuture<AsyncReader> reset() {\n        try {\n            raf.seek(0);\n            return CompletableFuture.completedFuture(this);\n        } catch (IOException e) {\n            CompletableFuture<AsyncReader> err = new CompletableFuture<>();\n            err.completeExceptionally(e);\n            return err;\n        }\n    }\n\n    @Override\n    public void close() {\n        try {\n            raf.close();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3AdminRequests.java",
    "content": "package peergos.server.storage;\n\nimport org.w3c.dom.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport javax.xml.parsers.*;\nimport javax.xml.xpath.*;\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class S3AdminRequests {\n\n    public static S3Request buildReq(String verb,\n                                     String host,\n                                     String key,\n                                     String contentSha256,\n                                     Optional<String> storageClass,\n                                     Optional<Integer> expiresSeconds,\n                                     boolean allowPublicReads,\n                                     boolean useAuthHeader,\n                                     Map<String, String> extraQueryParameters,\n                                     Map<String, String> extraHeaders,\n                                     String accessKeyId,\n                                     String region,\n                                     ZonedDateTime timestamp) {\n        return new S3Request(verb, host, key, contentSha256, storageClass, expiresSeconds, allowPublicReads, useAuthHeader, extraQueryParameters,\n                extraHeaders, accessKeyId, region, asAwsDate(normaliseDate(timestamp)));\n    }\n    public static String asAwsTime(ZonedDateTime timestamp) {\n        return asAwsDate(normaliseDate(timestamp));\n    }\n\n    private static Instant normaliseDate(ZonedDateTime timestamp) {\n        return timestamp.withNano(0).withZoneSameInstant(ZoneId.of(\"UTC\")).toInstant();\n    }\n\n    public static String asAwsDate(Instant instant) {\n        return instant.toString()\n                .replaceAll(\"[:\\\\-]|\\\\.\\\\d{3}\", \"\");\n    }\n\n    public static String asAwsDate(ZonedDateTime time) {\n        return asAwsDate(normaliseDate(time));\n    }\n\n    private static String asAwsShortDate(Instant instant) {\n        return asAwsDate(instant).substring(0, 8);\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignDelete(String key,\n                                                                Optional<String> versionId,\n                                                                String datetime,\n                                                                String host,\n                                                                String region,\n                                                                Optional<String> storageClass,\n                                                                String accessKeyId,\n                                                                String s3SecretKey,\n                                                                boolean useHttps,\n                                                                Hasher h) {\n        Map<String, String> extraQueryParameters = versionId.map(s -> Map.of(\"versionId\", s)).orElse(Collections.emptyMap());\n        S3Request policy = new S3Request(\"DELETE\", host, key, S3Request.UNSIGNED, storageClass, Optional.empty(), false, true,\n                extraQueryParameters, new HashMap<>(), accessKeyId, region, datetime);\n        return S3Request.preSignRequest(policy, key, host, s3SecretKey, useHttps, h);\n    }\n\n    private static XPathFactory xPathFactory = XPathFactory.newInstance();\n    public static final ThreadLocal<DocumentBuilder> builder =\n            new ThreadLocal<>() {\n                @Override\n                protected DocumentBuilder initialValue() {\n                    try {\n                        return DocumentBuilderFactory.newInstance().newDocumentBuilder();\n                    } catch (ParserConfigurationException exc) {\n                        throw new IllegalArgumentException(exc);\n                    }\n                }\n            };\n\n    public static class ObjectMetadata {\n        public final String key, etag;\n        public final LocalDateTime lastModified;\n        public final long size;\n\n        public ObjectMetadata(String key, String etag, LocalDateTime lastModified, long size) {\n            this.key = key;\n            this.etag = etag;\n            this.lastModified = lastModified;\n            this.size = size;\n        }\n    }\n\n    public static class ListObjectsReply {\n        public final String prefix;\n        public final boolean isTruncated;\n        public final List<ObjectMetadata> objects;\n        public final Optional<String> continuationToken;\n\n        public ListObjectsReply(String prefix, boolean isTruncated, List<ObjectMetadata> objects, Optional<String> continuationToken) {\n            this.prefix = prefix;\n            this.isTruncated = isTruncated;\n            this.objects = objects;\n            this.continuationToken = continuationToken;\n        }\n    }\n\n    public static class ObjectMetadataVersion {\n        public final String key, etag, version;\n        public final LocalDateTime lastModified;\n        public final long size;\n        public final boolean isLatest;\n\n        public ObjectMetadataVersion(String key, String etag, String version, LocalDateTime lastModified, long size, boolean isLatest) {\n            this.key = key;\n            this.etag = etag;\n            this.version = version;\n            this.lastModified = lastModified;\n            this.size = size;\n            this.isLatest = isLatest;\n        }\n    }\n\n    public static class DeleteMarker {\n        public final String key, version;\n        public final LocalDateTime lastModified;\n        public final boolean isLatest;\n\n        public DeleteMarker(String key, String version, LocalDateTime lastModified, boolean isLatest) {\n            this.key = key;\n            this.version = version;\n            this.lastModified = lastModified;\n            this.isLatest = isLatest;\n        }\n    }\n\n    public static class ListObjectVersionsReply {\n        public final String prefix;\n        public final boolean isTruncated;\n        public final List<ObjectMetadataVersion> versions;\n        public final List<DeleteMarker> deletes;\n        public final Optional<String> nextKeyMarker, nextVersionIdMarker;\n\n        public ListObjectVersionsReply(String prefix, boolean isTruncated, List<ObjectMetadataVersion> versions,\n                                       List<DeleteMarker> deletes, Optional<String> nextKeyMarker, Optional<String> nextVersionIdMarker) {\n            this.prefix = prefix;\n            this.isTruncated = isTruncated;\n            this.versions = versions;\n            this.deletes = deletes;\n            this.nextKeyMarker = nextKeyMarker;\n            this.nextVersionIdMarker = nextVersionIdMarker;\n        }\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignList(String prefix,\n                                                              int maxKeys,\n                                                              Optional<String> continuationToken,\n                                                              ZonedDateTime now,\n                                                              String host,\n                                                              String region,\n                                                              Optional<String> storageClass,\n                                                              String accessKeyId,\n                                                              String s3SecretKey,\n                                                              boolean useHttps,\n                                                              Hasher h) {\n        Map<String, String> extraQueryParameters = new LinkedHashMap<>();\n        extraQueryParameters.put(\"list-type\", \"2\");\n        extraQueryParameters.put(\"max-keys\", \"\" + maxKeys);\n        extraQueryParameters.put(\"fetch-owner\", \"false\");\n        extraQueryParameters.put(\"prefix\", prefix);\n        continuationToken.ifPresent(t -> extraQueryParameters.put(\"continuation-token\", t));\n\n        Instant normalised = normaliseDate(now);\n        S3Request policy = new S3Request(\"GET\", host, \"\", S3Request.UNSIGNED, storageClass, Optional.empty(), false, true,\n                extraQueryParameters, new HashMap<>(), accessKeyId, region, asAwsDate(normalised));\n        return S3Request.preSignRequest(policy, \"\", host, s3SecretKey, useHttps, h);\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignListVersions(String prefix,\n                                                                      int maxKeys,\n                                                                      Optional<String> keyMarker,\n                                                                      Optional<String> versionIdMarker,\n                                                                      ZonedDateTime now,\n                                                                      String host,\n                                                                      String region,\n                                                                      Optional<String> storageClass,\n                                                                      String accessKeyId,\n                                                                      String s3SecretKey,\n                                                                      boolean useHttps,\n                                                                      Hasher h) {\n        Map<String, String> extraQueryParameters = new LinkedHashMap<>();\n        extraQueryParameters.put(\"versions\", \"true\");\n        extraQueryParameters.put(\"max-keys\", \"\" + maxKeys);\n        extraQueryParameters.put(\"fetch-owner\", \"false\");\n        extraQueryParameters.put(\"prefix\", prefix);\n        keyMarker.ifPresent(t -> extraQueryParameters.put(\"key-marker\", t));\n        versionIdMarker.ifPresent(t -> extraQueryParameters.put(\"version-id-marker\", t));\n\n        Instant normalised = normaliseDate(now);\n        S3Request policy = new S3Request(\"GET\", host, \"\", S3Request.UNSIGNED, storageClass, Optional.empty(), false, true,\n                extraQueryParameters, new HashMap<>(), accessKeyId, region, asAwsDate(normalised));\n        return S3Request.preSignRequest(policy, \"\", host, s3SecretKey, useHttps, h);\n    }\n\n    public static ListObjectVersionsReply listObjectVersions(String prefix,\n                                                             int maxKeys,\n                                                             Optional<String> keyMarker,\n                                                             Optional<String> versionIdMarker,\n                                                             ZonedDateTime now,\n                                                             String host,\n                                                             String region,\n                                                             Optional<String> storageClass,\n                                                             String accessKeyId,\n                                                             String s3SecretKey,\n                                                             Function<PresignedUrl, byte[]> getter,\n                                                             Supplier<DocumentBuilder> builder,\n                                                             boolean useHttps,\n                                                             Hasher h) {\n        PresignedUrl listReq = preSignListVersions(prefix, maxKeys, keyMarker, versionIdMarker, now, host, region,\n                storageClass, accessKeyId, s3SecretKey, useHttps, h).join();\n        try {\n            Document xml = builder.get().parse(new ByteArrayInputStream(getter.apply(listReq)));\n            List<ObjectMetadataVersion> res = new ArrayList<>();\n            List<DeleteMarker> deletes = new ArrayList<>();\n            Node root = xml.getFirstChild();\n            NodeList topLevel = root.getChildNodes();\n            boolean isTruncated = false;\n            Optional<String> nextKeyMarker = Optional.empty();\n            Optional<String> nextVersionIdMarker = Optional.empty();\n            for (int t=0; t < topLevel.getLength(); t++) {\n                Node top = topLevel.item(t);\n                if (\"IsTruncated\".equals(top.getNodeName())) {\n                    String val = top.getTextContent();\n                    isTruncated = \"true\".equals(val);\n                }\n                if (\"NextKeyMarker\".equals(top.getNodeName())) {\n                    String val = top.getTextContent();\n                    nextKeyMarker = Optional.of(val);\n                }\n                if (\"NextVersionIdMarker\".equals(top.getNodeName())) {\n                    String val = top.getTextContent();\n                    nextVersionIdMarker = Optional.of(val);\n                }\n                if (\"Version\".equals(top.getNodeName())) {\n                    NodeList childNodes = top.getChildNodes();\n                    String key=null, etag=null, modified=null, versionId=null;\n                    long size=0;\n                    boolean isLatest = false;\n                    for (int i = 0; i < childNodes.getLength(); i++) {\n                        Node n = childNodes.item(i);\n                        if (\"Key\".equals(n.getNodeName())) {\n                            key = n.getTextContent();\n                        } else if (\"LastModified\".equals(n.getNodeName())) {\n                            modified = n.getTextContent();\n                        } else if (\"ETag\".equals(n.getNodeName())) {\n                            etag = n.getTextContent();\n                        } else if (\"Size\".equals(n.getNodeName())) {\n                            size = Long.parseLong(n.getTextContent());\n                        } else if (\"VersionId\".equals(n.getNodeName())) {\n                            versionId = n.getTextContent();\n                        } else if (\"IsLatest\".equals(n.getNodeName())) {\n                            isLatest = Boolean.parseBoolean(n.getTextContent());\n                        }\n                    }\n                    if (versionId.equals(\"null\"))\n                        versionId = null;\n                    res.add(new ObjectMetadataVersion(key, etag, versionId,\n                            LocalDateTime.parse(modified.substring(0, modified.length() - 1)), size, isLatest));\n                }\n                if (\"DeleteMarker\".equals(top.getNodeName())) {\n                    NodeList childNodes = top.getChildNodes();\n                    String key=null, modified=null, versionId=null;\n                    boolean isLatest = false;\n                    for (int i = 0; i < childNodes.getLength(); i++) {\n                        Node n = childNodes.item(i);\n                        if (\"Key\".equals(n.getNodeName())) {\n                            key = n.getTextContent();\n                        } else if (\"LastModified\".equals(n.getNodeName())) {\n                            modified = n.getTextContent();\n                        } else if (\"VersionId\".equals(n.getNodeName())) {\n                            versionId = n.getTextContent();\n                        } else if (\"IsLatest\".equals(n.getNodeName())) {\n                            isLatest = Boolean.parseBoolean(n.getTextContent());\n                        }\n                    }\n                    deletes.add(new DeleteMarker(key, versionId, LocalDateTime.parse(modified.substring(0, modified.length() - 1)), isLatest));\n                }\n            }\n            return new ListObjectVersionsReply(prefix, isTruncated, res, deletes, nextKeyMarker, nextVersionIdMarker);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static ListObjectsReply listObjects(String prefix,\n                                               int maxKeys,\n                                               Optional<String> continuationToken,\n                                               ZonedDateTime now,\n                                               String host,\n                                               String region,\n                                               Optional<String> storageClass,\n                                               String accessKeyId,\n                                               String s3SecretKey,\n                                               Function<PresignedUrl, byte[]> getter,\n                                               Supplier<DocumentBuilder> builder,\n                                               boolean useHttps,\n                                               Hasher h) {\n        PresignedUrl listReq = preSignList(prefix, maxKeys, continuationToken, now, host, region, storageClass, accessKeyId, s3SecretKey, useHttps, h).join();\n        try {\n            Document xml = builder.get().parse(new ByteArrayInputStream(getter.apply(listReq)));\n            List<ObjectMetadata> res = new ArrayList<>();\n            Node root = xml.getFirstChild();\n            NodeList topLevel = root.getChildNodes();\n            boolean isTruncated = false;\n            Optional<String> nextContinuationToken = Optional.empty();\n            for (int t=0; t < topLevel.getLength(); t++) {\n                Node top = topLevel.item(t);\n                if (\"IsTruncated\".equals(top.getNodeName())) {\n                    String val = top.getTextContent();\n                    isTruncated = \"true\".equals(val);\n                }\n                if (\"NextContinuationToken\".equals(top.getNodeName())) {\n                    String val = top.getTextContent();\n                    nextContinuationToken = Optional.of(val);\n                }\n                if (\"Contents\".equals(top.getNodeName())) {\n                    NodeList childNodes = top.getChildNodes();\n                    String key=null, etag=null, modified=null;\n                    long size=0;\n                    for (int i = 0; i < childNodes.getLength(); i++) {\n                        Node n = childNodes.item(i);\n                        if (\"Key\".equals(n.getNodeName())) {\n                            key = n.getTextContent();\n                        } else if (\"LastModified\".equals(n.getNodeName())) {\n                            modified = n.getTextContent();\n                        } else if (\"ETag\".equals(n.getNodeName())) {\n                            etag = n.getTextContent();\n                        } else if (\"Size\".equals(n.getNodeName())) {\n                            size = Long.parseLong(n.getTextContent());\n                        }\n                    }\n                    res.add(new ObjectMetadata(key, etag, LocalDateTime.parse(modified.substring(0, modified.length() - 1)), size));\n                }\n            }\n            return new ListObjectsReply(prefix, isTruncated, res, nextContinuationToken);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static class BulkDeleteReply {\n        public final List<String> deletedKeys;\n\n        public BulkDeleteReply(List<String> deletedKeys) {\n            this.deletedKeys = deletedKeys;\n        }\n    }\n\n    public static BulkDeleteReply bulkDelete(List<Pair<String, String>> keyVersions,\n                                             ZonedDateTime now,\n                                             String host,\n                                             String region,\n                                             Optional<String> storageClass,\n                                             String accessKeyId,\n                                             String s3SecretKey,\n                                             Function<byte[], String> sha256,\n                                             Function<byte[], String> sha256toBase64,\n                                             BiFunction<PresignedUrl, byte[], byte[]> poster,\n                                             Supplier<DocumentBuilder> builder,\n                                             boolean useHttps,\n                                             Hasher h) {\n        StringBuilder xmlBuilder = new StringBuilder();\n        xmlBuilder.append(\"<Delete>\");\n        for (Pair<String, String> v : keyVersions) {\n            xmlBuilder.append(\"<Object><Key>\");\n            xmlBuilder.append(v.left);\n            xmlBuilder.append(\"</Key>\");\n            if (v.right != null) {\n                xmlBuilder.append(\"<VersionId>\");\n                xmlBuilder.append(v.right);\n                xmlBuilder.append(\"</VersionId>\");\n            }\n            xmlBuilder.append( \"</Object>\");\n        }\n        xmlBuilder.append(\"</Delete>\");\n        String reqXml = xmlBuilder.toString();\n        byte[] body = reqXml.getBytes();\n        String contentSha256 = sha256.apply(body);\n        Map<String, String> extraHeaders = new TreeMap<>();\n        extraHeaders.put(\"Content-Length\", \"\" + body.length);\n        extraHeaders.put(\"x-amz-checksum-sha256\", sha256toBase64.apply(body));\n        extraHeaders.put(\"Content-Type\", \"text/xml\");\n        Map<String, String> extraQueryParameters = new TreeMap<>();\n        extraQueryParameters.put(\"delete\", \"true\");\n        Instant normalised = normaliseDate(now);\n        String key = \"\";\n        S3Request policy = new S3Request(\"POST\", host, key, contentSha256, storageClass, Optional.empty(), false, true,\n                extraQueryParameters, extraHeaders, accessKeyId, region, asAwsDate(normalised));\n        PresignedUrl reqUrl = S3Request.preSignRequest(policy, key, host, s3SecretKey, useHttps, h).join();\n        byte[] respBytes = poster.apply(reqUrl, body);\n        try {\n            Document xml = builder.get().parse(new ByteArrayInputStream(respBytes));\n            List<String> deleted = new ArrayList<>();\n            Node root = xml.getFirstChild();\n            NodeList topLevel = root.getChildNodes();\n            for (int t=0; t < topLevel.getLength(); t++) {\n                Node top = topLevel.item(t);\n                if (\"Deleted\".equals(top.getNodeName())) {\n                    NodeList childNodes = top.getChildNodes();\n                    for (int i = 0; i < childNodes.getLength(); i++) {\n                        Node n = childNodes.item(i);\n                        if (\"Key\".equals(n.getNodeName())) {\n                            deleted.add(n.getTextContent());\n                        }\n                    }\n                }\n            }\n            return new BulkDeleteReply(deleted);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3BlockStorage.java",
    "content": "package peergos.server.storage;\n\nimport com.webauthn4j.data.client.Origin;\nimport io.netty.handler.ssl.SslClosedEngineException;\nimport io.prometheus.client.Histogram;\nimport io.prometheus.client.Counter;\nimport peergos.server.*;\nimport peergos.server.corenode.*;\nimport peergos.server.login.AccountWithStorage;\nimport peergos.server.login.JdbcAccount;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.admin.QuotaAdmin;\nimport peergos.server.storage.auth.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.bases.Base64;\nimport peergos.shared.mutable.HttpMutablePointers;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.mutable.MutablePointersProxy;\nimport peergos.shared.mutable.PointerUpdate;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.Account;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport javax.net.ssl.*;\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.*;\n\npublic class S3BlockStorage implements DeletableContentAddressedStorage {\n\n    private static final Logger LOG = Logger.getGlobal();\n    private static final int LIST_PARALLELISM = 64;\n    private static final List<String> KEY_FIRST_CHARS;\n    static {\n        List<String> chars = new ArrayList<>(62);\n        for (char c = '0'; c <= '9'; c++) chars.add(String.valueOf(c));\n        for (char c = 'A'; c <= 'Z'; c++) chars.add(String.valueOf(c));\n        for (char c = 'a'; c <= 'z'; c++) chars.add(String.valueOf(c));\n        KEY_FIRST_CHARS = Collections.unmodifiableList(chars);\n    }\n    private static final List<String> RETRY_S3_CODES = List.of(\"RequestError\",\"RequestTimeout\",\"Throttling\"\n            ,\"ThrottlingException\",\"RequestLimitExceeded\",\"RequestThrottled\",\"InternalError\",\"ExpiredToken\",\"ExpiredTokenException\",\"SlowDown\");\n    \n    private static final Histogram CborReadTimerLog = Histogram.build()\n            .labelNames(\"filesize\")\n            .name(\"cbor_block_read_seconds\")\n            .help(\"Time to read a cbor block from immutable storage\")\n            .exponentialBuckets(0.01, 2, 16)\n            .register();\n    private static final Histogram RawReadTimerLog = Histogram.build()\n            .labelNames(\"filesize\")\n            .name(\"raw_block_read_seconds\")\n            .help(\"Time to read a raw block from immutable storage\")\n            .exponentialBuckets(0.01, 2, 16)\n            .register();\n    private static final Histogram HeadTimerLog = Histogram.build()\n            .labelNames(\"filesize\")\n            .name(\"block_head_seconds\")\n            .help(\"Time to get a blocks size from immutable storage\")\n            .exponentialBuckets(0.01, 2, 16)\n            .register();\n    private static final Histogram writeTimerLog = Histogram.build()\n            .labelNames(\"filesize\")\n            .name(\"s3_block_write_seconds\")\n            .help(\"Time to write a block to immutable storage\")\n            .exponentialBuckets(0.01, 2, 16)\n            .register();\n    private static final Counter blockHeads = Counter.build()\n            .name(\"s3_block_heads\")\n            .help(\"Number of block heads to S3\")\n            .register();\n    private static final Counter blockSize = Counter.build()\n            .name(\"s3_block_size_heads\")\n            .help(\"Number of block size head requests to S3\")\n            .register();\n    private static final Counter blockGets = Counter.build()\n            .name(\"s3_block_gets\")\n            .help(\"Number of block gets to S3\")\n            .register();\n    private static final Counter blockGetAuths = Counter.build()\n            .name(\"s3_block_get_auths\")\n            .help(\"Number of authed block gets to S3\")\n            .register();\n    private static final Counter failedBlockGets = Counter.build()\n            .name(\"s3_block_get_failures\")\n            .help(\"Number of failed block gets to S3\")\n            .register();\n    private static final Counter blockPuts = Counter.build()\n            .name(\"s3_block_puts\")\n            .help(\"Number of block puts to S3\")\n            .register();\n    private static final Counter blockPutAuths = Counter.build()\n            .name(\"s3_block_put_auths\")\n            .help(\"Number of authed block puts to S3\")\n            .register();\n    private static final Histogram blockPutBytes = Histogram.build()\n            .labelNames(\"size\")\n            .name(\"s3_block_put_bytes\")\n            .help(\"Number of bytes written to S3\")\n            .exponentialBuckets(0.01, 2, 16)\n            .register();\n    private static final Counter nonLocalGets = Counter.build()\n            .name(\"p2p_block_gets\")\n            .help(\"Number of block gets which fell back to p2p retrieval\")\n            .register();\n\n    private static final Counter getRateLimited = Counter.build()\n            .name(\"s3_get_rate_limited\")\n            .help(\"Number of times we get a http 429 rate limit response during a block get\")\n            .register();\n\n    private static final Counter rateLimited = Counter.build()\n            .name(\"s3_rate_limited\")\n            .help(\"Number of times we get a http 429 rate limit response\")\n            .register();\n\n    private final Cid id;\n    private final List<Cid> ids;\n    private final List<Multihash> peerIds;\n    private final String region, bucket, folder, host;\n    private final boolean useHttps;\n    private final String accessKeyId, secretKey;\n    private final Optional<String> storageClass;\n    private final boolean noReads;\n    private final BlockStoreProperties props;\n    private final String linkHost;\n    private final TransactionStore transactions;\n    private final BlockRequestAuthoriser authoriser;\n    private final BlockMetadataStore blockMetadata;\n    private final UsageStore usage;\n    private final BlockCache cborCache;\n    private final BlockBuffer blockBuffer;\n    private final Hasher hasher;\n    private final DeletableContentAddressedStorage ipnsHandler;\n    private final ContentAddressedStorageProxy p2pHttpFallback;\n\n    private final LinkedBlockingQueue<Pair<PublicKeyHash, Cid>> blocksToFlush = new LinkedBlockingQueue<>();\n    private final ConcurrentHashMap<PublicKeyHash, SlidingWindowCounter> userReadReqRateLimits = new ConcurrentHashMap();\n    private final ConcurrentHashMap<PublicKeyHash, SlidingWindowCounter> userReadSizeRateLimits = new ConcurrentHashMap();\n    private final SlidingWindowCounter globalReadReqCount;\n    private final SlidingWindowCounter globalReadBandwidth;\n    private final long maxUserBandwidthPerMinute, maxUserReadRequestsPerMinute;\n    private final boolean isVersioned;\n    private final Path peergosDir;\n    private final PartitionStatus partitionStatus;\n    private final boolean partitionComplete;\n    private final JdbcBatCave bats;\n    private CoreNode pki;\n    private static final int MIRROR_PARALLELISM = 30;\n    private final Semaphore mirrorSemaphore = new Semaphore(MIRROR_PARALLELISM);\n    private final Semaphore mirrorTreeSemaphore = new Semaphore(MIRROR_PARALLELISM);\n\n    public S3BlockStorage(S3Config config,\n                          List<Cid> ids,\n                          BlockStoreProperties props,\n                          String linkHost,\n                          TransactionStore transactions,\n                          BlockRequestAuthoriser authoriser,\n                          JdbcBatCave bats,\n                          BlockMetadataStore blockMetadata,\n                          UsageStore usage,\n                          BlockCache cborCache,\n                          BlockBuffer blockBuffer,\n                          long maxReadBandwidthPerSecond,\n                          long maxReadReqsPerSecond,\n                          long maxUserBandwidthPerSecond,\n                          long maxUserReadRequestsPerSecond,\n                          boolean isVersioned,\n                          Path peergosDir,\n                          PartitionStatus partitioned,\n                          Hasher hasher,\n                          DeletableContentAddressedStorage ipnsHandler,\n                          ContentAddressedStorageProxy p2pHttpFallback) {\n        this.ids = ids;\n        this.peerIds = ids.stream()\n                .map(Cid::bareMultihash)\n                .collect(Collectors.toList());\n        this.id = ids.get(ids.size() - 1);\n        this.region = config.region;\n        this.bucket = config.bucket;\n        this.host = config.getHost();\n        this.useHttps = ! host.endsWith(\"localhost\") && ! host.contains(\"localhost:\");\n        this.folder = (useHttps ? \"\" : bucket + \"/\") + (config.path.isEmpty() || config.path.endsWith(\"/\") ? config.path : config.path + \"/\");\n        this.storageClass = config.storageClass;\n        this.noReads = storageClass.isPresent() && storageClass.get().equals(\"GLACIER\");\n        this.accessKeyId = config.accessKey;\n        this.secretKey = config.secretKey;\n        LOG.info(\"Using S3 Block Storage at \" + config.regionEndpoint + \", bucket \" + config.bucket\n                + \", path: \" + config.path + \", peerids: \"+peerIds);\n        this.props = props;\n        this.linkHost = linkHost;\n        this.transactions = transactions;\n        this.authoriser = authoriser;\n        this.bats = bats;\n        this.blockMetadata = blockMetadata;\n        this.usage = usage;\n        this.cborCache = cborCache;\n        this.blockBuffer = blockBuffer;\n        this.hasher = hasher;\n        this.ipnsHandler = ipnsHandler;\n        this.p2pHttpFallback = p2pHttpFallback;\n        globalReadReqCount = new SlidingWindowCounter(60*60, 60*60 * maxReadReqsPerSecond);\n        globalReadBandwidth = new SlidingWindowCounter(60*60, 60*60 * maxReadBandwidthPerSecond);\n        this.maxUserBandwidthPerMinute = 60 * maxUserBandwidthPerSecond;\n        this.maxUserReadRequestsPerMinute = 60 * maxUserReadRequestsPerSecond;\n        this.isVersioned = isVersioned;\n        this.peergosDir = peergosDir;\n        this.partitionStatus = partitioned;\n        this.partitionComplete = partitionStatus.isDone();\n        startFlusherThread();\n        new Thread(() -> blockBuffer.applyToAll((o, c) -> blocksToFlush.add(new Pair<>(o, c)))).start();\n    }\n\n    private boolean userPartitioningComplete() {\n        return partitionStatus.isDone();\n    }\n\n    @Override\n    public void partitionByUser(UsageStore usage,\n                                JdbcIpnsAndSocial mutable,\n                                PublicKeyHash pkiKey) {\n        partitionByUser(mutable, pkiKey);\n    }\n\n    public void partitionByUser(JdbcIpnsAndSocial mutable,\n                                PublicKeyHash pkiKey) {\n        if (userPartitioningComplete()) {\n            LOG.info(\"S3 blockstore already partitioned\");\n            return;\n        }\n        LOG.info(\"Partitioning S3 blockstore...\");\n        new Thread(() -> {\n            ForkJoinPool partitionPool = new ForkJoinPool(30);\n            while (true) {\n                try {\n                    Path legacyBlockListFile = peergosDir.resolve(\"legacy-versions.sqlite\");\n                    if (legacyBlockListFile.toFile().exists())\n                        Files.delete(legacyBlockListFile);\n                    Path partitionedBlockListFile = peergosDir.resolve(\"partitioned-versions.sqlite\");\n                    if (partitionedBlockListFile.toFile().exists())\n                        Files.delete(partitionedBlockListFile);\n                    SqliteBlockList legacyBlocklist = SqliteBlockList.createBlockListDb(legacyBlockListFile);\n                    SqliteBlockList partitionedBlocklist = SqliteBlockList.createBlockListDb(partitionedBlockListFile);\n                    // This will only list legacy block versions\n                    LOG.info(\"Listing legacy blocks\");\n                    getAllBlockHashVersions(null, vs -> legacyBlocklist.addBlocks(vs.stream()\n                            .map(v -> new UserBlockVersion(null, v.cid, v.version, v.isLatest))\n                            .collect(Collectors.toList())));\n                    LOG.info(legacyBlocklist.size() + \" legacy blocks remaining\");\n                    LOG.info(\"Listing partitioned blocks\");\n                    applyToAllVersionsParallel(\"\", Optional.empty(), vs -> partitionedBlocklist.addBlocks(vs.stream()\n                            .filter(v -> v.username != null)\n                            .collect(Collectors.toList())), x -> {});\n                    LOG.info(partitionedBlocklist.size() + \" partitioned blocks.\");\n                    List<Triple<Multihash, String, PublicKeyHash>> allTargets = usage.getAllTargets()\n                            .stream()\n                            .sorted(Comparator.comparing(a -> a.middle))\n                            .collect(Collectors.toList());\n                    Set<Multihash> doneRoots = new HashSet<>();\n                    for (Triple<Multihash, String, PublicKeyHash> target : allTargets) {\n                        LOG.info(\"Partitioning user \" + target.middle);\n                        moveSubtreeToOwner(target.right, target.middle, (Cid) target.left, List.of(id), legacyBlocklist, partitionedBlocklist, partitionPool);\n                        doneRoots.add(target.left);\n                    }\n                    Map<PublicKeyHash, byte[]> allPointers = mutable.getAllEntries();\n                    PublicKeyHash pkiOwner = pki.getPublicKeyHash(\"peergos\").join().get();\n\n                    LOG.info(\"Partitioning blocks from remaining pointers..\");\n                    allPointers.forEach((writerHash, rawPointer) -> {\n                        PublicSigningKey writer = getSigningKey(null, writerHash).join().get();\n                        byte[] bothHashes = writer.unsignMessage(rawPointer).join();\n                        PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n                        MaybeMultihash updated = cas.updated;\n                        if (updated.isPresent() && doneRoots.contains(updated.get()))\n                            return;\n                        PublicKeyHash owner;\n                        try {\n                            owner = writerHash.equals(pkiKey) ? pkiOwner : usage.getOwnerKey(writerHash);\n                        } catch (Exception e) {\n                            LOG.warning(\"Couldn't partition key with unknown owner: \" + writerHash);\n                            return;\n                        }\n                        String username = pki.getUsername(owner).join();\n\n                        if (updated.isPresent())\n                            moveSubtreeToOwner(owner, username, (Cid) updated.get(), List.of(id), legacyBlocklist, partitionedBlocklist, partitionPool);\n                    });\n                    LOG.info(\"S3 blockstore partitioning complete\");\n                    partitionStatus.complete();\n                    return;\n                } catch (Exception e) {\n                    LOG.log(Level.SEVERE, e, e::getMessage);\n                    Threads.sleep(120 * 60_000);\n                }\n            }\n        }).start();\n    }\n\n    private void moveSubtreeToOwner(PublicKeyHash owner,\n                                    String username,\n                                    Cid root,\n                                    List<Multihash> ourIds,\n                                    SqliteBlockList reachability,\n                                    SqliteBlockList partitionedBlocklist,\n                                    ForkJoinPool pool) {\n        while (pool.getQueuedSubmissionCount() > 100)\n            try {Thread.sleep(100);} catch (InterruptedException e) {}\n        pool.submit(() -> moveLegacyBlockToOwner(owner, username, root, reachability, partitionedBlocklist));\n        try {\n            List<Cid> links = getLinks(owner, root, ourIds).join();\n            for (Cid link : links) {\n                moveSubtreeToOwner(owner, username, link, ourIds, reachability, partitionedBlocklist, pool);\n            }\n        } catch (Exception e) {\n            LOG.log(Level.SEVERE, e, () -> \"Error getting links for \" + root);\n        }\n    }\n\n    private void moveLegacyBlockToOwner(PublicKeyHash owner,\n                                        String username,\n                                        Cid block,\n                                        SqliteBlockList legacyBlockList,\n                                        SqliteBlockList partitionedBlocklist) {\n        if (block.isIdentity())\n            return;\n        // Unfortunately we have to copy, then delete, then update metadata\n        // Check if the copy target already exists first\n        boolean alreadyMoved = partitionedBlocklist.hasBlock(username, block);\n        boolean legacyPresent = legacyBlockList.hasBlock(null, block);\n        String newVersion;\n        try {\n            if (alreadyMoved) {\n                List<String> versions = partitionedBlocklist.getVersions(username, block);\n                newVersion = versions.get(versions.size() - 1);\n            } else if (legacyPresent) {\n                newVersion = copyObject(legacyHashToKey(block), hashToKey(owner, block), hasher);\n            } else {\n                if (block.equals(Cid.decode(\"zdpuAy36qRsvJq5Rdqt8YMMjrDfnAw2ETyVqakwaMXupLqR7X\"))) {\n                    // Ask for a war story\n                    byte[] emptyChampRootBlock = {(byte) 0x83, 0x40, 0x40, (byte) 0x80};\n                    put(owner, block, emptyChampRootBlock, false);\n                    return;\n                }\n\n                // check if it is a new block written since the listing\n                boolean newlyWrittenBlock = getWithBackoff(() -> hasBlockWithoutBackoff(owner, block, false), 1000);\n                if (newlyWrittenBlock)\n                    return;\n                throw new IllegalStateException(\"User \" + username + \" missing block: \" + legacyHashToKey(block));\n            }\n            if (legacyPresent) {\n                if (isVersioned) {\n                    List<String> versions = legacyBlockList.getVersions(null, block);\n                    for (String version : versions) {\n                        LOG.info(\"S3 delete version of s3://\" + bucket + \"/\" + legacyHashToKey(block));\n                        delete(null, new BlockVersion(block, version, true));\n                    }\n                } else {\n                    delete(null, block);\n                }\n            }\n\n            if (isVersioned)\n                blockMetadata.setOwnerAndVersion(owner, block, newVersion);\n            else\n                blockMetadata.setOwner(owner, block);\n        } catch (Exception e) {\n            String msg = e.getMessage();\n            // tolerate missing blocks\n            if (msg == null || (!msg.contains(\"NoSuchKey\") && !msg.contains(\"missing block\")))\n                throw new RuntimeException(e);\n            LOG.log(Level.SEVERE, e, e::getMessage);\n            LOG.info(\"S3 partition missing block (user \" + username + \") \" + block);\n        }\n    }\n\n    private String copyObject(String sourceKey,\n                              String destKey,\n                              Hasher h) {\n        return getWithBackoff(() -> copyObjectWithoutBackoff(sourceKey, destKey, h));\n    }\n\n    private String copyObjectWithoutBackoff(String sourceKey,\n                                            String destKey,\n                                            Hasher h) {\n        PresignedUrl copyUrl = S3Request.preSignCopy(bucket, sourceKey, destKey, S3AdminRequests.asAwsDate(ZonedDateTime.now()), host,\n                storageClass, Collections.emptyMap(), region, accessKeyId, secretKey, true, h).join();\n        try {\n            LOG.info(\"Copying s3://\" + bucket + \"/\" + sourceKey + \" to s3://\" + bucket + \"/\" + destKey);\n            Pair<byte[], String> reply = HttpUtil.putWithVersion(copyUrl, new byte[0]);\n            String res = new String(reply.left);\n            if (! res.startsWith(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"\") ||\n                    ! res.contains(\"<CopyObjectResult\") ||\n                    ! res.contains(\"<LastModified>\") ||\n                    ! res.contains(\"<ETag>\"))\n                throw new IllegalStateException(res);\n            return reply.right;\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        } catch (Exception e) {\n            boolean rateLimited = isRateLimitedException(e)\n                    || isCausedBy(e, RateLimitException.class, SocketTimeoutException.class,\n                    SSLException.class, SslClosedEngineException.class,\n                    SocketException.class)\n                    || isRateLimitedException(e);\n            if (rateLimited) {\n                throw new RateLimitException();\n            }\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static boolean isCausedBy(Throwable t, Class<?>... types) {\n        Set<Throwable> visited = Collections.newSetFromMap(new IdentityHashMap<>());\n        Deque<Throwable> queue = new ArrayDeque<>();\n        queue.add(t);\n        while (!queue.isEmpty()) {\n            Throwable current = queue.poll();\n            if (!visited.add(current))\n                continue;\n            for (Class<?> type : types)\n                if (type.isInstance(current))\n                    return true;\n            if (current.getCause() != null)\n                queue.add(current.getCause());\n            queue.addAll(Arrays.asList(current.getSuppressed()));\n        }\n        return false;\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n        this.pki = pki;\n        this.blockBuffer.setPki(pki);\n        updateMetadataStoreIfEmpty();\n    }\n\n    private void startFlusherThread() {\n        Thread flusher = new Thread(() -> {\n            while (true) {\n                try {\n                    Pair<PublicKeyHash, Cid> p = blocksToFlush.peek();\n                    if (p == null) {\n                        Thread.sleep(1_000);\n                        continue;\n                    }\n                    PublicKeyHash owner = p.left;\n                    Cid h = p.right;\n                    flushConcurrencyLimit.acquireUninterruptibly();\n                    flusherPool.submit(() -> getWithBackoff(() -> {\n                        try {\n                            Optional<byte[]> block = blockBuffer.get(owner, h).join();\n                            if (block.isPresent()) {\n                                Pair<Cid, BlockMetadata> res = getWithBackoff(() -> put(owner, h, block.get(), true), 1_000);\n                                blockBuffer.delete(owner, h);\n                            } else {\n                                // Block not in buffer: already flushed, nothing to do\n                                LOG.info(\"Block \" + h + \" not found in buffer during flush, skipping\");\n                            }\n                            return true;\n                        } catch (Exception e) {\n                            LOG.info(\"Error flushing block \" + h + \" \" + e.getMessage());\n                            try { Thread.sleep(5_000); } catch (InterruptedException ignored) {}\n                            blocksToFlush.add(new Pair<>(owner, h));\n                            throw new RuntimeException(e);\n                        } finally {\n                            flushConcurrencyLimit.release();\n                        }\n                    }));\n                    blocksToFlush.poll();\n                } catch (Exception e) {\n                    LOG.log(Level.INFO, e.getMessage(), e);\n                }\n            }\n        });\n        flusher.setDaemon(true);\n        flusher.start();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    private static String legacyHashToKey(Multihash hash) {\n        return DirectS3BlockStore.hashToKey(hash);\n    }\n\n    private String hashToKey(PublicKeyHash owner, Multihash hash) {\n        if (owner == null)\n            return legacyHashToKey(hash);\n        return ownerToPrefix(owner) + DirectS3BlockStore.hashToKey(hash);\n    }\n\n    private Pair<String, Cid> keyToHash(String key) {\n        String userAndKey = key.substring(folder.length());\n        if (userAndKey.contains(\"/\")) {\n            int slash = userAndKey.indexOf(\"/\");\n            return new Pair<>(userAndKey.substring(0, slash), DirectS3BlockStore.keyToHash(userAndKey.substring(slash + 1)));\n        }\n        return new Pair<>(null, DirectS3BlockStore.keyToHash(userAndKey));\n    }\n\n    private Pair<PublicKeyHash, Cid> keyToOwnerAndHash(Optional<PublicKeyHash> owner, String key) {\n        String path = key.substring(folder.length());\n        if (path.contains(\"/\")) {\n            int slash = path.indexOf(\"/\");\n            Cid hash = DirectS3BlockStore.keyToHash(path.substring(slash + 1));\n            if (owner.isPresent())\n                return new Pair<>(owner.get(), hash);\n            String username = path.substring(0, slash);\n            PublicKeyHash parsedOwner = pki.getPublicKeyHash(username).join().get();\n            return new Pair<>(parsedOwner, hash);\n        }\n        // legacy path without owner\n        return new Pair<>(null, DirectS3BlockStore.keyToHash(path));\n    }\n\n    private final LRUCache<PublicKeyHash, String> ownerToUser = new LRUCache<>(1000);\n\n    private String ownerToPrefix(PublicKeyHash owner) {\n        // legacy data all starts with AFK or AFY, usernames start with a lowercase letter\n        // We want to be able to efficiently list all legacy blocks\n        // Achieve this by listing from B which is after A and before lowercase\n        if (owner == null)\n            return \"\";\n        String cached = ownerToUser.get(owner);\n        if (cached != null)\n            return cached + \"/\";\n        String username = usage.getOwner(owner);\n        if (username == null) {\n            try {\n                username = pki.getUsername(owner).join();\n            } catch (Exception e) {\n                // key not in PKI\n            }\n        }\n        if (username == null || username.isEmpty())\n            return \"\";\n        ownerToUser.put(owner, username);\n        return username + \"/\";\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return Futures.of(props);\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return Futures.of(linkHost);\n    }\n\n    private void enforceGlobalRequestRateLimit() {\n        if (! globalReadReqCount.allowRequest(1))\n            throw new MajorRateLimitException(\"Rate Limit: Server S3 request limit exceeded. Please try again later\");\n    }\n\n    private void enforceGlobalBandwidthLimit(long readSize) {\n        if (! globalReadBandwidth.allowRequest(readSize))\n            throw new MajorRateLimitException(\"Rate Limit: Server bandwidth limit exceeded. Please try again later\");\n    }\n\n    private void enforceUserRequestRateLimits(PublicKeyHash owner, long readRequests) {\n        if (owner == null) // GC until we move to user partitioned blockstores\n            return;\n        if (! userReadReqRateLimits.computeIfAbsent(owner, o -> new SlidingWindowCounter(60, maxUserReadRequestsPerMinute))\n                .allowRequest(readRequests)) {\n            LOG.info(\"User read request rate limit hit: \" + owner);\n            throw new MajorRateLimitException(\"Rate Limit: User request limit exceeded. Please try again later.\");\n        }\n    }\n\n    private void enforceUserBandwidthRateLimits(PublicKeyHash owner, long readSize) {\n        if (owner == null) // GC until we move to user partitioned blockstores\n            return;\n        if (! userReadSizeRateLimits.computeIfAbsent(owner, o -> new SlidingWindowCounter(60, maxUserBandwidthPerMinute))\n                .allowRequest(readSize)) {\n            LOG.info(\"User bandwidth rate limit hit: \" + owner);\n            throw new MajorRateLimitException(\"Rate Limit: User bandwidth limit exceeded. Please try again later.\");\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n        if (noReads)\n            throw new IllegalStateException(\"Reads from Glacier are disabled!\");\n        if (blocks.size() > MAX_BLOCK_AUTHS)\n            throw new IllegalStateException(\"Too many reads to auth!\");\n        List<PresignedUrl> res = new ArrayList<>();\n\n        if (! blocks.stream().allMatch(c -> c.hash.isRaw()))\n            return Futures.errored(new IllegalStateException(\"Can only auth read for raw blocks, not cbor!\"));\n\n        // bulk-fetch all metadata in a single query, then verify BATs concurrently\n        List<Cid> hashes = blocks.stream().map(b -> b.hash).collect(Collectors.toList());\n        Map<Cid, BlockMetadata> allMeta = blockMetadata.getAll(hashes);\n        List<CompletableFuture<BlockMetadata>> auths = blocks.stream()\n                .map(b -> {\n                    BlockMetadata meta = allMeta.get(b.hash);\n                    CompletableFuture<BlockMetadata> metaFuture = meta != null ?\n                            Futures.of(meta) : getBlockMetadata(owner, b.hash);\n                    return metaFuture.thenCompose(m -> {\n                        CompletableFuture<String> authFuture = b.bat\n                                .map(bat -> bat.bat.generateAuth(b.hash, id, 300, S3Request.currentDatetime(), bat.id, hasher)\n                                        .thenApply(BlockAuth::encode))\n                                .orElseGet(() -> Futures.of(\"\"));\n                        return authFuture.thenCompose(auth ->\n                                authoriser.allowRead(b.hash, m.batids, id, auth)\n                                        .thenApply(allowed -> {\n                                            if (!allowed)\n                                                throw new IllegalStateException(\"Unauthorised!\");\n                                            return m;\n                                        }));\n                    });\n                })\n                .collect(Collectors.toList());\n\n        for (BlockMirrorCap block : blocks) {\n            String s3Key = hashToKey(partitionComplete ?\n                    owner :\n                    blockMetadata.getOwner(block.hash).orElse(null), block.hash);\n            res.add(S3Request.preSignGet(folder + s3Key, Optional.of(600), Optional.empty(), S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, storageClass, accessKeyId, secretKey, useHttps, hasher).join());\n        }\n        long byteCount = 0;\n        long reqCount = 0;\n        for (CompletableFuture<BlockMetadata> fut : auths) {\n            BlockMetadata m = fut.join();// Any invalids BATs will cause this to throw\n            byteCount += m.size;\n            reqCount++;\n        }\n        Optional<BatId> mirrorBat = auths.stream()\n                .flatMap(f -> f.join().batids.stream().filter(b -> !b.isInline()))\n                .findFirst();\n        if (mirrorBat.isPresent()) {\n            String username = bats.getOwner(mirrorBat.get());\n            PublicKeyHash verifiedOwner = usage.getOwnerKey(username);\n            // check rate limits\n            enforceGlobalRequestRateLimit();\n            enforceGlobalBandwidthLimit(byteCount);\n            enforceUserRequestRateLimits(verifiedOwner, reqCount);\n            enforceUserBandwidthRateLimits(verifiedOwner, byteCount);\n        }\n        for (int i=0; i < blocks.size(); i++)\n            blockGetAuths.inc();\n        return Futures.of(res);\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                            PublicKeyHash writerHash,\n                                                            List<byte[]> signedHashes,\n                                                            List<Integer> blockSizes,\n                                                            List<List<BatId>> batIds,\n                                                            boolean isRaw,\n                                                            TransactionId tid) {\n        try {\n            if (signedHashes.size() > MAX_BLOCK_AUTHS)\n                throw new IllegalStateException(\"Too many writes to auth!\");\n            if (blockSizes.size() != signedHashes.size())\n                throw new IllegalStateException(\"Number of sizes doesn't match number of signed hashes!\");\n            if (blockSizes.size() != batIds.size())\n                throw new IllegalStateException(\"Number of sizes doesn't match number of bats!\");\n            PublicSigningKey writer = getSigningKey(owner, writerHash).get().get();\n            List<Pair<Cid, BlockMetadata>> blockProps = new ArrayList<>();\n            for (int i=0; i < signedHashes.size(); i++) {\n                Cid.Codec codec = isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor;\n                if (! isRaw)\n                    throw new IllegalStateException(\"Only raw blocks can be pre-authed for writes\");\n                Cid cid = new Cid(1, codec, Multihash.Type.sha2_256, writer.unsignMessage(signedHashes.get(i)).join());\n                blockProps.add(new Pair<>(cid, new BlockMetadata(blockSizes.get(i), Collections.emptyList(), batIds.get(i))));\n            }\n            List<PresignedUrl> res = new ArrayList<>();\n            for (Pair<Cid, BlockMetadata> props : blockProps) {\n                if (props.left.type != Multihash.Type.sha2_256)\n                    throw new IllegalStateException(\"Can only pre-auth writes of sha256 hashed blocks!\");\n                transactions.addBlock(props.left, tid, owner);\n                String s3Key = hashToKey(owner, props.left);\n                String contentSha256 = ArrayOps.bytesToHex(props.left.getHash());\n                Map<String, String> extraHeaders = new LinkedHashMap<>();\n                extraHeaders.put(\"Content-Type\", \"application/octet-stream\");\n                res.add(S3Request.preSignPut(folder + s3Key, props.right.size, contentSha256, storageClass, false,\n                        S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, extraHeaders, region, accessKeyId, secretKey, useHttps, hasher).join());\n                blockPutAuths.inc();\n                if (isRaw)\n                    blockMetadata.put(owner, props.left, null, props.right);\n            }\n            return Futures.of(res);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid object, Optional<BatWithId> bat) {\n        return getRaw(pki.getStorageProviders(owner), owner, object, bat, id, hasher, true)\n                .thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        if (hash.isRaw())\n            throw new IllegalStateException(\"Need to call getRaw if cid is not cbor!\");\n        return getRaw(peerIds, owner, hash, auth, persistBlock).thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds,\n                                                       PublicKeyHash owner,\n                                                       Cid hash,\n                                                       Optional<BatWithId> bat,\n                                                       Cid ourId,\n                                                       Hasher h,\n                                                       boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, bat, ourId, h, persistBlock).thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid object, Optional<BatWithId> bat) {\n        return getRaw(pki.getStorageProviders(owner), owner, object, bat, id, hasher, true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, bat, ourId, h, true, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean doAuth, boolean persistBlock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(hash.getHash()));\n        if (noReads) {\n            if (peerIds.stream().anyMatch(p -> ids.stream().anyMatch(us -> us.bareMultihash().equals(p.bareMultihash()))))\n                throw new IllegalStateException(\"Reads from Glacier are disabled!\");\n            return p2pHttpFallback.getRaw(peerIds.get(0), owner, hash, bat).thenApply(res -> {\n                if (res.isPresent() && persistBlock) {\n                    if (hash.isRaw())\n                        putRaw(owner, owner, new byte[0], res.get(), new TransactionId(\"\"), x -> {});\n                    else\n                        put(owner, owner, new byte[0], res.get(), new TransactionId(\"\"));\n                }\n                return res;\n            });\n        }\n        return getRaw(peerIds, owner, hash, Optional.empty(), doAuth, bat, persistBlock)\n                .thenApply(p -> p.map(v -> v.left));\n    }\n\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, List<Multihash> peerIds, Cid hash, Optional<BatWithId> bat, Cid ourId, Hasher h, boolean persistBlock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(hash.getHash()));\n        if (noReads) {\n            if (peerIds.stream().anyMatch(p -> ids.stream().anyMatch(us -> us.bareMultihash().equals(p.bareMultihash()))))\n                throw new IllegalStateException(\"Reads from Glacier are disabled!\");\n            return p2pHttpFallback.getRaw(peerIds.get(0), owner, hash, bat);\n        }\n        return getRaw(peerIds, owner, hash, Optional.empty(), true, bat, persistBlock)\n                .thenApply(p -> p.map(v -> v.left));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      String auth,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    /** Get raw block data and version\n     *\n     * @param hash\n     * @param range\n     * @param enforceAuth\n     * @param bat\n     * @return\n     */\n    private CompletableFuture<Optional<Pair<byte[], String>>> getRaw(List<Multihash> peerIds,\n                                                                     PublicKeyHash owner,\n                                                                     Cid hash,\n                                                                     Optional<Pair<Integer, Integer>> range,\n                                                                     boolean enforceAuth,\n                                                                     Optional<BatWithId> bat,\n                                                                     boolean persistP2pBlock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(new Pair<>(hash.getHash(), null)));\n        if (noReads)\n            throw new IllegalStateException(\"Reads from Glacier are disabled!\");\n        if (! hash.isRaw()) {\n            Optional<byte[]> cached = cborCache.get(hash).join();\n            if (cached.isPresent()) {\n                if (enforceAuth && ! authoriser.allowRead(hash, cached.get(), id, generateAuth(hash, bat, id, hasher)).join())\n                    throw new IllegalStateException(\"Unauthorised!\");\n                return Futures.of(Optional.of(new Pair<>(cached.get(), null)));\n            }\n        }\n        Optional<byte[]> buffered = blockBuffer.get(owner, hash).join();\n        if (buffered.isPresent()) {\n            if (enforceAuth && ! authoriser.allowRead(hash, buffered.get(), id, generateAuth(hash, bat, id, hasher)).join())\n                    throw new IllegalStateException(\"Unauthorised!\");\n                return Futures.of(Optional.of(new Pair<>(buffered.get(), null)));\n        }\n        return getWithBackoff(() -> getRawWithoutBackoff(peerIds, owner, hash, range, enforceAuth, bat, persistP2pBlock, false))\n                .thenApply(res -> {\n                    if (hash.isRaw())\n                        return res;\n                    if (res.isPresent())\n                        cborCache.put(hash, res.get().left);\n                    return res;\n                });\n    }\n\n    public static Throwable getRootCause(Throwable t) {\n        Throwable cause = t.getCause();\n        if (t instanceof CompletionException)\n            return getRootCause(cause);\n        if (t instanceof ExecutionException)\n            return getRootCause(cause);\n        if (t instanceof RuntimeException && cause != null && cause != t)\n            return getRootCause(cause);\n        return t;\n    }\n\n    private CompletableFuture<Optional<Pair<byte[], String>>> getRawWithoutBackoff(List<Multihash> peerIds,\n                                                                                   PublicKeyHash owner,\n                                                                                   Cid hash,\n                                                                                   Optional<Pair<Integer, Integer>> range,\n                                                                                   boolean enforceAuth,\n                                                                                   Optional<BatWithId> bat,\n                                                                                   boolean persistP2pBlock,\n                                                                                   boolean useLegacyPath) {\n        enforceGlobalRequestRateLimit();\n        enforceUserRequestRateLimits(owner, 1);\n\n        String path = folder + (useLegacyPath ? legacyHashToKey(hash) : hashToKey(owner, hash));\n        PresignedUrl getUrl = S3Request.preSignGet(path, Optional.of(600), range,\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, storageClass, accessKeyId, secretKey, useHttps, hasher).join();\n        Histogram.Timer readTimer = hash.isRaw() ?\n                RawReadTimerLog.labels(\"read\").startTimer() :\n                CborReadTimerLog.labels(\"read\").startTimer();\n        try {\n            Pair<byte[], String> blockAndVersion = HttpUtil.getWithVersion(getUrl);\n            blockGets.inc();\n            enforceUserBandwidthRateLimits(owner, blockAndVersion.left.length);\n            enforceGlobalBandwidthLimit(blockAndVersion.left.length);\n            // validate auth, unless this is an internal query\n            if (enforceAuth && ! authoriser.allowRead(hash, blockAndVersion.left, id, generateAuth(hash, bat, id, hasher)).join())\n                throw new IllegalStateException(\"Unauthorised!\");\n            if (range.isEmpty())\n                blockMetadata.put(owner, hash, blockAndVersion.right, blockAndVersion.left);\n            return Futures.of(Optional.of(blockAndVersion));\n        } catch (Exception e) {\n            String msg = e.getMessage();\n            Throwable cause = getRootCause(e);\n            boolean rateLimited = cause instanceof RateLimitException\n                    || cause instanceof SocketTimeoutException\n                    || cause instanceof SSLException\n                    || cause instanceof SocketException\n                    || isRateLimitedException(e);\n            if (rateLimited) {\n                getRateLimited.inc();\n                S3BlockStorage.rateLimited.inc();\n                throw new RateLimitException();\n            }\n\n            boolean notFound = cause instanceof FileNotFoundException || (msg != null && msg.contains(\"<Code>NoSuchKey</Code>\"));\n            if (! notFound) {\n                LOG.warning(\"S3 error reading \" + path);\n                LOG.log(Level.WARNING, msg, e);\n            } else {\n                if (! useLegacyPath)\n                    return getRawWithoutBackoff(peerIds, owner, hash, range, enforceAuth, bat, persistP2pBlock, true);\n            }\n            failedBlockGets.inc();\n\n            if (peerIds.stream().map(Multihash::bareMultihash).anyMatch(this.peerIds::contains)) {\n                // This is the owner's home server, we should have the block!\n                if (! notFound)\n                    LOG.log(Level.SEVERE, cause, cause::getMessage);\n                else\n                    LOG.log(Level.SEVERE, \"Missing block for \" + owner + \" - \" + hash);\n                throw new IllegalStateException(\"Missing block \" + hash);\n            }\n\n            nonLocalGets.inc();\n            return p2pHttpFallback.getRaw(peerIds.get(0), owner, hash, bat)\n                    .thenApply(res -> {\n                        if (res.isPresent() && persistP2pBlock) {\n                            if (hash.isRaw())\n                                putRaw(owner, owner, new byte[0], res.get(), new TransactionId(\"\"), x -> {});\n                            else\n                                put(owner, owner, new byte[0], res.get(), new TransactionId(\"\"));\n                        }\n                        return res;\n                    })\n                    .thenApply(dopt -> dopt.map(b -> new Pair<>(b, null)));\n        } finally {\n            readTimer.observeDuration();\n        }\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        if (blockBuffer.hasBlock(owner, hash))\n            return true;\n        if (! hash.isRaw() && cborCache.hasBlock(hash))\n            return true;\n        Optional<BlockMetadata> meta = blockMetadata.get(hash);\n        if (meta.isPresent())\n            return true;\n        return getWithBackoff(() -> hasBlockWithoutBackoff(owner, hash, false));\n    }\n\n    public boolean hasBlockWithoutBackoff(PublicKeyHash owner, Cid hash, boolean useLegacyPath) {\n        try {\n            String key;\n            try {\n                key = (useLegacyPath ? legacyHashToKey(hash) : hashToKey(owner, hash));\n            } catch (Exception e) {\n                return false;\n            }\n            PresignedUrl headUrl = S3Request.preSignHead(folder + key, Optional.of(60),\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, storageClass, accessKeyId, secretKey, useHttps, hasher).join();\n            Map<String, List<String>> headRes = HttpUtil.head(headUrl);\n            blockHeads.inc();\n            return true;\n        } catch (FileNotFoundException e) {\n            if (! useLegacyPath && ! userPartitioningComplete())\n                return hasBlockWithoutBackoff(owner, hash, true);\n            return false;\n        } catch (Exception e) {\n            Throwable cause = getRootCause(e);\n            String msg = cause.getMessage();\n            if (msg == null && ! (cause instanceof FileNotFoundException)) {\n                LOG.info(\"Error checking for \" + hash + \": \" + e);\n                return false;\n            }\n            boolean rateLimited = isRateLimitedException(cause);\n            if (rateLimited) {\n                S3BlockStorage.rateLimited.inc();\n                throw new RateLimitException();\n            }\n            boolean notFound = cause instanceof FileNotFoundException || msg.contains(\"<Code>NoSuchKey</Code>\");\n            if (! notFound) {\n                LOG.warning(\"S3 error reading \" + hash);\n                LOG.log(Level.WARNING, msg, e);\n            }\n            return false;\n        }\n    }\n\n    public static boolean isRateLimitedException(Throwable e) {\n        String msg = e.getMessage();\n        if (msg == null) {\n            return false;\n        }\n        if (msg.contains(\"Connection reset\"))\n            return true;\n        int startIndex = msg.indexOf(\"<Code>\");\n        int endIndex = msg.indexOf(\"</Code>\");\n        if (startIndex >=0 && endIndex >=0 && startIndex < endIndex) {\n            String code = msg.substring(startIndex + 6, endIndex).trim();\n            return RETRY_S3_CODES.contains(code);\n        } else {\n            return e instanceof ConnectException;\n        }\n    }\n\n    private BlockMetadata checkAndAddBlock(PublicKeyHash owner, Cid expected, byte[] raw) {\n        Cid res = hasher.hash(raw, expected.isRaw()).join();\n        if (! res.equals(expected))\n            throw new IllegalStateException(\"Received block with incorrect hash!\");\n        return getWithBackoff(() -> put(owner, expected, raw, false)).right;\n    }\n\n    private List<BlockMetadata> bulkGetBlocks(List<Multihash> peers,\n                                              String username,\n                                              PublicKeyHash owner,\n                                              List<Cid> hashes,\n                                              Optional<BatWithId> mirrorBat,\n                                              AtomicLong skippedCount,\n                                              AtomicLong retrievalCount,\n                                              AtomicLong retrievalSize) {\n        Map<Cid, BlockMetadata> present = blockMetadata.getAll(hashes);\n        long original = skippedCount.get();\n        if (original/100 != (original + present.size())/100) {\n            long skipped = (original + present.size())/100 * 100;\n            LOG.info(\"User \" + username + \": skipped \" + String.format(\"%,d\", skipped) + \" blocks already present.\");\n        }\n        skippedCount.addAndGet(present.size());\n\n        List<Cid> missing = hashes.stream().filter(h -> !present.containsKey(h)).toList();\n        ConcurrentHashMap<Cid, BlockMetadata> fetched = new ConcurrentHashMap<>();\n        AtomicReference<Throwable> firstError = new AtomicReference<>();\n\n        List<Thread> threads = missing.stream().map(c -> Thread.ofVirtual().start(() -> {\n            try {\n                mirrorSemaphore.acquire();\n                try {\n                    long count = retrievalCount.incrementAndGet();\n                    if (count % 100 == 0 || count == 1)\n                        LOG.info(\"User \" + username + \": retrieved \" + String.format(\"%,d\", count) + \" blocks, of total size \" + String.format(\"%,d\", retrievalSize.get()) + (count == 1 ? \" \" + c : \"\"));\n                    Optional<byte[]> bo = RetryStorage.runWithRetry(10, () ->\n                            p2pHttpFallback.getRaw(peers.get(0), owner, c, mirrorBat)).join();\n                    if (bo.isEmpty())\n                        throw new IllegalStateException(\"Block not available from remote: \" + c);\n                    byte[] b = bo.get();\n                    retrievalSize.addAndGet(b.length);\n                    BlockMetadata meta = RetryStorage.runWithRetry(5,\n                            () -> Futures.of(checkAndAddBlock(owner, c, b))).join();\n                    fetched.put(c, meta);\n                } finally {\n                    mirrorSemaphore.release();\n                }\n            } catch (Throwable t) {\n                firstError.compareAndSet(null, t);\n            }\n        })).toList();\n\n        for (Thread t : threads) {\n            try { t.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }\n        }\n\n        if (firstError.get() != null)\n            throw new RuntimeException(\"Mirror block fetch failed\", firstError.get());\n\n        return Stream.concat(present.values().stream(), missing.stream().map(fetched::get))\n                .toList();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> mirror(String username,\n                                               PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<Multihash> peerIds,\n                                               Optional<Cid> existing,\n                                               Optional<Cid> updated,\n                                               Optional<BatWithId> mirrorBat,\n                                               Cid ourNodeId,\n                                               NewBlocksProcessor newBlockProcessor,\n                                               TransactionId tid,\n                                               Hasher hasher) {\n        if (updated.isEmpty())\n            return Futures.of(Collections.emptyList());\n        Cid newRoot = updated.get();\n        if (existing.equals(updated))\n            return Futures.of(Collections.singletonList(newRoot));\n\n        // This call will not verify the auth as we might not have the mirror bat present locally\n        boolean hasBlock = hasBlock(owner, newRoot);\n        if (! hasBlock) {\n            Optional<byte[]> newBlock = getRaw(peerIds, owner, newRoot, mirrorBat, ourNodeId, hasher, false, true).join();\n            if (newBlock.isEmpty())\n                throw new IllegalStateException(\"Couldn't retrieve block: \" + newRoot);\n            getWithBackoff(() -> put(newBlock.get(), newRoot.isRaw(), tid, owner, false));\n            usage.addPendingUsage(username, writer, newBlock.get().length);\n        }\n        if (newRoot.isRaw())\n            return Futures.of(Collections.singletonList(newRoot));\n        BlockMetadata meta = getBlockMetadata(owner, newRoot).join();\n\n        List<Cid> newLinks = meta.links\n                .stream()\n                .filter(h -> !h.isIdentity())\n                .collect(Collectors.toList());\n        List<Cid> existingLinks = existing.map(c -> getLinks(owner, c, peerIds).join().stream()\n                        .filter(h -> !h.isIdentity())\n                        .collect(Collectors.toList()))\n                .orElse(Collections.emptyList());\n\n        AtomicLong skippedBlockCount = new AtomicLong(0);\n        AtomicLong blockCount = new AtomicLong(0);\n        AtomicLong totalSize = new AtomicLong(0);\n        bulkMirror(owner, writer, peerIds, existingLinks, newLinks, mirrorBat, ourNodeId,\n                (p, o, h, m) -> bulkGetBlocks(p, username, o, h, m, skippedBlockCount, blockCount, totalSize),\n                (w, bs, size) -> usage.addPendingUsage(username, writer, size));\n        if (blockCount.get() > 0) {\n            LOG.info(\"Mirrored \" + String.format(\"%,d\", blockCount.get()) + \" blocks, taking \" +\n                    String.format(\"%,d\", totalSize.get()) + \" bytes\");\n        }\n        return Futures.of(newLinks);\n    }\n\n    private void bulkMirror(PublicKeyHash owner,\n                            PublicKeyHash writer,\n                            List<Multihash> peerIds,\n                            List<Cid> existing,\n                            List<Cid> updated,\n                            Optional<BatWithId> mirrorBat,\n                            Cid ourNodeId,\n                            P2pBlockGet retriever,\n                            NewBlocksProcessor newBlockProcessor) {\n        if (updated.isEmpty())\n            return;\n        Set<Cid> common = new HashSet<>(existing);\n        common.retainAll(updated);\n\n        List<Cid> removed = existing.stream()\n                .filter(x -> !common.contains(x))\n                .filter(c -> !c.isIdentity())\n                .collect(Collectors.toList());\n        List<Cid> added = updated.stream()\n                .filter(x -> !common.contains(x))\n                .filter(c -> !c.isIdentity())\n                .collect(Collectors.toList());\n\n        List<BlockMetadata> addedLinks = retriever.bulkGet(peerIds, owner, added, mirrorBat);\n        newBlockProcessor.process(writer, added, addedLinks.stream().mapToInt(p -> p.size).sum());\n\n        List<Thread> childThreads = new ArrayList<>();\n        AtomicReference<Throwable> firstError = new AtomicReference<>();\n\n        if (removed.isEmpty()) {\n            List<Cid> allCbor = addedLinks.stream()\n                    .map(p -> p.links)\n                    .flatMap(Collection::stream)\n                    .filter(c -> !c.isIdentity() && !c.isRaw())\n                    .collect(Collectors.toList());\n            for (int i = 0; i < allCbor.size();) {\n                int end = Math.min(allCbor.size(), i + 100);\n                List<Cid> batch = allCbor.subList(i, end);\n                if (mirrorTreeSemaphore.tryAcquire()) {\n                    childThreads.add(Thread.ofVirtual().start(() -> {\n                        try {\n                            bulkMirror(owner, writer, peerIds, Collections.emptyList(),\n                                    batch, mirrorBat, ourNodeId, retriever, newBlockProcessor);\n                        } catch (Throwable t) {\n                            firstError.compareAndSet(null, t);\n                        } finally {\n                            mirrorTreeSemaphore.release();\n                        }\n                    }));\n                } else {\n                    try {\n                        bulkMirror(owner, writer, peerIds, Collections.emptyList(),\n                                batch, mirrorBat, ourNodeId, retriever, newBlockProcessor);\n                    } catch (Throwable t) {\n                        firstError.compareAndSet(null, t);\n                    }\n                }\n                i = end;\n            }\n            List<Cid> allRaw = addedLinks.stream()\n                    .map(p -> p.links)\n                    .flatMap(Collection::stream)\n                    .filter(c -> !c.isIdentity() && c.isRaw())\n                    .collect(Collectors.toList());\n            for (int i = 0; i < allRaw.size(); i++) {\n                List<Cid> single = allRaw.subList(i, i + 1);\n                if (mirrorTreeSemaphore.tryAcquire()) {\n                    childThreads.add(Thread.ofVirtual().start(() -> {\n                        try {\n                            bulkMirror(owner, writer, peerIds, Collections.emptyList(),\n                                    single, mirrorBat, ourNodeId, retriever, newBlockProcessor);\n                        } catch (Throwable t) {\n                            firstError.compareAndSet(null, t);\n                        } finally {\n                            mirrorTreeSemaphore.release();\n                        }\n                    }));\n                } else {\n                    try {\n                        bulkMirror(owner, writer, peerIds, Collections.emptyList(),\n                                single, mirrorBat, ourNodeId, retriever, newBlockProcessor);\n                    } catch (Throwable t) {\n                        firstError.compareAndSet(null, t);\n                    }\n                }\n            }\n        } else {\n            for (int i = 0; i < added.size(); i++) {\n                List<Cid> newLinks = addedLinks.get(i).links;\n                List<Cid> existingLinks = i >= removed.size() ?\n                        Collections.emptyList() :\n                        getLinks(owner, removed.get(i), peerIds).join().stream()\n                                .filter(c -> !c.isIdentity())\n                                .collect(Collectors.toList());\n                if (mirrorTreeSemaphore.tryAcquire()) {\n                    childThreads.add(Thread.ofVirtual().start(() -> {\n                        try {\n                            bulkMirror(owner, writer, peerIds, existingLinks, newLinks,\n                                    mirrorBat, ourNodeId, retriever, newBlockProcessor);\n                        } catch (Throwable t) {\n                            firstError.compareAndSet(null, t);\n                        } finally {\n                            mirrorTreeSemaphore.release();\n                        }\n                    }));\n                } else {\n                    try {\n                        bulkMirror(owner, writer, peerIds, existingLinks, newLinks,\n                                mirrorBat, ourNodeId, retriever, newBlockProcessor);\n                    } catch (Throwable t) {\n                        firstError.compareAndSet(null, t);\n                    }\n                }\n            }\n        }\n\n        for (Thread t : childThreads) {\n            try { t.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }\n        }\n        if (firstError.get() != null)\n            throw new RuntimeException(\"Mirror subtree failed\", firstError.get());\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        if (noReads)\n            throw new IllegalStateException(\"Reads from Glacier are disabled!\");\n        if (! hasBlock(owner, root))\n            return Futures.errored(new IllegalStateException(\"Champ root not present locally: \" + root + \" for owner: \" + owner));\n        return getChampLookup(owner, root, caps, committedRoot, hasher);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return transactions.getOpenTransactionBlocks(owner);\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n        transactions.clearOldTransactions(owner, cutoffMillis);\n    }\n\n    private void collectGarbage(JdbcIpnsAndSocial pointers, UsageStore usage, BlockMetadataStore metadata, boolean listFromBlockstore) {\n        GarbageCollector.collect(this, pointers, usage, Paths.get(\"\"),\n                x -> Futures.of(true), metadata, this::confirmDeleteBlocks, listFromBlockstore);\n    }\n\n    private CompletableFuture<Boolean> confirmDeleteBlocks(long cborCount, long rawCount, long total) {\n        if (cborCount == 0 && rawCount == 0) {\n            System.out.println(\"0 blocks to delete\");\n            return Futures.of(true);\n        }\n        if ((cborCount + rawCount)*100/total < 5) {\n            System.out.println(\"Deleting \" + cborCount + \" cbor blocks and \" + rawCount + \" raw blocks out of \" + total + \", \" + ((cborCount + rawCount) * 100 / total) + \"%\");\n            return Futures.of(true);\n        }\n        System.out.println(\"Delete \" + cborCount + \" cbor blocks and \" + rawCount + \" raw blocks out of \" + total + \", \" + ((cborCount + rawCount) * 100 / total) + \"% (Y/N)\");\n        String confirm = System.console().readLine();\n        if (confirm.equals(\"Y\"))\n            return Futures.of(true);\n        return Futures.of(false);\n    }\n\n    private static <V> V getWithBackoff(Supplier<V> req) {\n        return getWithBackoff(req, 100);\n    }\n\n    private static <V> V getWithBackoff(Supplier<V> req, long initialSleep) {\n        long sleep = initialSleep;\n        for (int i=0; i < 4; i++) {\n            try {\n                return req.get();\n            } catch (RateLimitException e) {\n                try {\n                    Thread.sleep(sleep);\n                } catch (InterruptedException f) {}\n                sleep *= 2;\n            }\n        }\n        throw new RateLimitException();\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash hash) {\n        Optional<BlockMetadata> meta = blockMetadata.get((Cid) hash);\n        if (meta.isPresent())\n            return Futures.of(Optional.of(meta.get().size));\n        Optional<byte[]> buffered = blockBuffer.get(owner, (Cid) hash).join();\n        if (buffered.isPresent())\n            return Futures.of(Optional.of(buffered.get().length));\n        return getBlockMetadata(owner, (Cid)hash)\n                .thenApply(m -> Optional.of(m.size));\n    }\n\n    private CompletableFuture<Optional<Integer>> getSizeOnly(PublicKeyHash owner, Multihash hash) {\n        Optional<BlockMetadata> meta = blockMetadata.get((Cid) hash);\n        if (meta.isPresent())\n            return Futures.of(Optional.of(meta.get().size));\n        Optional<byte[]> buffered = blockBuffer.get(owner, (Cid) hash).join();\n        if (buffered.isPresent())\n            return Futures.of(Optional.of(buffered.get().length));\n        return getWithBackoff(() -> getSizeWithoutRetry(owner, hash));\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        return ipnsHandler.getIpnsEntry(signer);\n    }\n\n    private CompletableFuture<Optional<Integer>> getSizeWithoutRetry(PublicKeyHash owner, Multihash hash) {\n        if (noReads)\n            throw new IllegalStateException(\"Reads from Glacier are disabled!\");\n        if (hash.isIdentity()) // Identity hashes are not actually stored explicitly\n            return Futures.of(Optional.of(0));\n        Histogram.Timer readTimer = HeadTimerLog.labels(\"size\").startTimer();\n        try {\n            PresignedUrl headUrl = S3Request.preSignHead(folder + hashToKey(owner, hash), Optional.of(60),\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, storageClass, accessKeyId, secretKey, useHttps, hasher).join();\n            Map<String, List<String>> headRes = HttpUtil.head(headUrl);\n            blockHeads.inc();\n            blockSize.inc();\n            long size = Long.parseLong(headRes.get(\"Content-Length\").get(0));\n            return Futures.of(Optional.of((int)size));\n        } catch (FileNotFoundException f) {\n            LOG.warning(\"S3 404 error reading \" + hash);\n            return Futures.of(Optional.empty());\n        } catch (IOException e) {\n            String msg = e.getMessage();\n            boolean rateLimited = isRateLimitedException(e);\n            if (rateLimited) {\n                S3BlockStorage.rateLimited.inc();\n                throw new RateLimitException();\n            }\n            boolean notFound = msg.contains(\"<Code>NoSuchKey</Code>\");\n            if (! notFound) {\n                LOG.warning(\"S3 error reading \" + hash);\n                LOG.log(Level.WARNING, msg, e);\n            }\n            return Futures.of(Optional.empty());\n        } finally {\n            readTimer.observeDuration();\n        }\n    }\n\n    public boolean contains(PublicKeyHash owner, Multihash hash) {\n        try {\n            PresignedUrl headUrl = S3Request.preSignHead(folder + hashToKey(owner, hash), Optional.of(60),\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, storageClass, accessKeyId, secretKey, useHttps, hasher).join();\n            Map<String, List<String>> headRes = HttpUtil.head(headUrl);\n            blockHeads.inc();\n            return true;\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(id);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return Futures.of(ids);\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return CompletableFuture.completedFuture(transactions.startTransaction(owner));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        transactions.closeTransaction(owner, tid);\n        return CompletableFuture.completedFuture(true);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return put(owner, blocks, false, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signedHashes,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return put(owner, blocks, true, tid);\n    }\n\n    private final ExecutorService flusherPool = Executors.newVirtualThreadPerTaskExecutor();\n    private final Semaphore flushConcurrencyLimit = new Semaphore(32);\n\n    private CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                             List<byte[]> blocks,\n                                             boolean isRaw,\n                                             TransactionId tid) {\n        if (blocksToFlush.size() > 1_000)\n            throw new RateLimitException();\n        return Futures.of(blocks.stream()\n                .parallel()\n                .map(b -> getWithBackoff(() -> b.length > DirectS3BlockStore.MAX_SMALL_BLOCK_SIZE ?\n                        put(b, isRaw, tid, owner, true) : // This should only happen from p2p requests that can't use DirectS3Blockstore\n                        putToBuffer(b, isRaw, tid, owner).join()))\n                .collect(Collectors.toList()));\n    }\n\n    private CompletableFuture<Cid> putToBuffer(byte[] data, boolean isRaw, TransactionId tid, PublicKeyHash owner) {\n        if (data.length > DirectS3BlockStore.MAX_SMALL_BLOCK_SIZE)\n            throw new IllegalStateException(\"Block too big for block buffer!\");\n        Multihash hash = new Multihash(Multihash.Type.sha2_256, Hash.sha256(data));\n        Cid h = new Cid(1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, hash.type, hash.getHash());\n        if (! isRaw)\n            cborCache.put(h, data);\n        transactions.addBlock(h, tid, owner);\n        return blockBuffer.put(owner, h, data).thenApply(x -> {\n            blocksToFlush.add(new Pair<>(owner, h));\n            return h;\n        });\n    }\n\n    /** Must be atomic relative to reads of the same key\n     *\n     * @param data\n     */\n    public Cid put(byte[] data, boolean isRaw, TransactionId tid, PublicKeyHash owner, boolean cacheCbor) {\n        Multihash hash = new Multihash(Multihash.Type.sha2_256, Hash.sha256(data));\n        Cid cid = new Cid(1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, hash.type, hash.getHash());\n        transactions.addBlock(cid, tid, owner);\n        return put(owner, cid, data, cacheCbor).left;\n    }\n\n    public Pair<Cid, BlockMetadata> put(PublicKeyHash owner, Cid cid, byte[] data, boolean cacheCbor) {\n        Histogram.Timer writeTimer = writeTimerLog.labels(\"write\").startTimer();\n        String key = hashToKey(owner, cid);\n        try {\n            String s3Key = folder + key;\n            Map<String, String> extraHeaders = new TreeMap<>();\n            extraHeaders.put(\"Content-Type\", \"application/octet-stream\");\n            boolean hashContent = true;\n            String contentHash = hashContent ? ArrayOps.bytesToHex(cid.getHash()) : \"UNSIGNED-PAYLOAD\";\n            PresignedUrl putUrl = S3Request.preSignPut(s3Key, data.length, contentHash, storageClass, false,\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, extraHeaders, region, accessKeyId, secretKey, useHttps, hasher).join();\n            String version = HttpUtil.putWithVersion(putUrl, data).right;\n            BlockMetadata meta = blockMetadata.put(owner, cid, version, data);\n            blockPuts.inc();\n            blockPutBytes.labels(\"size\").observe(data.length);\n            if (cacheCbor && ! cid.isRaw())\n                cborCache.put(cid, data);\n            return new Pair<>(cid, meta);\n        } catch (IOException e) {\n            boolean rateLimited = isRateLimitedException(e)\n                    || isCausedBy(e, RateLimitException.class, SocketTimeoutException.class,\n                    SSLException.class, SslClosedEngineException.class,\n                    SocketException.class);\n            if (rateLimited) {\n                S3BlockStorage.rateLimited.inc();\n                throw new RateLimitException();\n            }\n            LOG.log(Level.SEVERE, e.getMessage(), e);\n            throw new RuntimeException(e.getMessage(), e);\n        } finally {\n            writeTimer.observeDuration();\n        }\n    }\n\n    @Override\n    public List<BlockMetadata> bulkGetLinks(List<Multihash> peerIds,\n                                            PublicKeyHash owner,\n                                            Cid ourId,\n                                            List<Cid> blocks,\n                                            Optional<BatWithId> mirrorBat,\n                                            Hasher h) {\n        List<Optional<byte[]>> rawOpts = blocks.stream()\n                .parallel()\n                .map(b -> RetryStorage.runWithRetry(2, () -> p2pHttpFallback.getRaw(peerIds.get(0), owner, b, mirrorBat)).join())\n                .toList();\n        if (rawOpts.size() != blocks.size())\n            throw new IllegalStateException(\"Incorrect number of blocks returned!\");\n        List<byte[]> raw = rawOpts.stream().map(Optional::get).toList();\n        List<Pair<Cid, byte[]>> hashed = new ArrayList<>();\n        for (int i=0; i < blocks.size(); i++) {\n            Cid c = blocks.get(i);\n            byte[] bytes = raw.get(i);\n            Cid res = h.hash(bytes, c.isRaw()).join();\n            if (! res.equals(c))\n                throw new IllegalStateException(\"Received block with incorrect hash!\");\n            hashed.add(new Pair<>(c, bytes));\n        }\n        return hashed.stream().map(p -> put(owner, p.left, p.right, false).right).toList();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        if (root.isRaw())\n            return CompletableFuture.completedFuture(Collections.emptyList());\n        Optional<BlockMetadata> meta = blockMetadata.get(root);\n        if (meta.isPresent())\n            return Futures.of(meta.get().links);\n        return getBlockMetadata(owner, root)\n                .thenApply(res -> res.links);\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid h) {\n        if (h.isIdentity())\n            return Futures.of(new BlockMetadata(0, CborObject.getLinks(h, h.getHash()), Bat.getBlockBats(h, h.getHash())));\n        Optional<BlockMetadata> cached = blockMetadata.get(h);\n        if (cached.isPresent())\n            return Futures.of(cached.get());\n        Optional<Pair<byte[], String>> data = getRaw(peerIds, owner, h, h.isRaw() ?\n                Optional.of(new Pair<>(0, Bat.MAX_RAW_BLOCK_PREFIX_SIZE - 1)) :\n                Optional.empty(), false, Optional.empty(), false).join();\n        if (data.isEmpty())\n            throw new IllegalStateException(\"Block not present locally: \" + h);\n        byte[] bloc = data.get().left;\n        String version = data.get().right;\n        if (h.isRaw()) {\n            // we should avoid this by populating the metadata store, as it means two S3 calls, a ranged GET and a HEAD\n            int size = getSizeOnly(owner, h).join().get();\n            BlockMetadata meta = new BlockMetadata(size, Collections.emptyList(), Bat.getRawBlockBats(bloc));\n            blockMetadata.put(owner, h, version, meta);\n            return Futures.of(meta);\n        }\n        return Futures.of(blockMetadata.put(owner, h, version, bloc));\n    }\n\n    public void updateMetadataStoreIfEmpty() {\n        if (! blockMetadata.isEmpty())\n            return;\n        LOG.info(\"Updating block metadata store from S3. Listing blocks...\");\n        List<Pair<PublicKeyHash, Cid>> all = getAllBlockHashes(true).collect(Collectors.toList());\n        LOG.info(\"Updating block metadata store from S3. Updating db with \" + all.size() + \" blocks...\");\n\n        int updateParallelism = 10;\n        ForkJoinPool pool = new ForkJoinPool(updateParallelism);\n        int batchSize = all.size() / updateParallelism;\n        AtomicLong progress = new AtomicLong(0);\n        int tenth = batchSize/10;\n\n        List<ForkJoinTask<Optional<BlockMetadata>>> futures = IntStream.range(0, updateParallelism)\n                .mapToObj(b -> pool.submit(() -> IntStream.range(b * batchSize, (b + 1) * batchSize)\n                        .mapToObj(i -> {\n                            BlockMetadata res = getBlockMetadata(all.get(i).left, all.get(i).right).join();\n                            if (i % (batchSize / 10) == 0) {\n                                long updatedProgress = progress.addAndGet(tenth);\n                                if (updatedProgress * 10 / all.size() > (updatedProgress - tenth) * 10 / all.size())\n                                    LOG.info(\"Populating block metadata: \" + updatedProgress * 100 / all.size() + \"% done\");\n                            }\n                            return res;\n                        })\n                        .reduce((x, y) -> y)))\n                .collect(Collectors.toList());\n        futures.stream()\n                .map(ForkJoinTask::join)\n                .collect(Collectors.toList());\n        LOG.info(\"Finished updating block metadata store from S3.\");\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore) {\n        // todo make this actually streaming\n        return getFiles(Optional.empty(), Long.MAX_VALUE).stream();\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        // todo make this actually streaming\n        return getFiles(Optional.of(owner), Long.MAX_VALUE).stream();\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        getFileVersions(owner, res);\n    }\n\n    @Override\n    public void getAllRawBlockVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        applyToAllVersionsParallel(ownerToPrefix(owner) + \"AFK\", Optional.empty(),\n                vs -> res.accept(vs.stream().map(v -> new BlockVersion(v.cid, v.version, v.isLatest))\n                        .collect(Collectors.toList())),\n                vs -> res.accept(vs.stream().map(v -> new BlockVersion(v.cid, v.version, v.isLatest))\n                        .collect(Collectors.toList())));\n    }\n\n    private void getFileVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        applyToAllVersionsParallel(ownerToPrefix(owner) + (owner == null ? \"A\" : \"\"), Optional.empty(),\n                vs -> res.accept(vs.stream().map(v -> new BlockVersion(v.cid, v.version, v.isLatest))\n                        .collect(Collectors.toList())),\n                vs -> res.accept(vs.stream().map(v -> new BlockVersion(v.cid, v.version, v.isLatest))\n                        .collect(Collectors.toList())));\n    }\n\n    private List<Pair<PublicKeyHash, Cid>> getFiles(Optional<PublicKeyHash> owner, long maxReturned) {\n        List<Pair<PublicKeyHash, Cid>> results = new ArrayList<>();\n        applyToAll(owner.map(this::ownerToPrefix), obj -> {\n            try {\n                results.add(keyToOwnerAndHash(owner, obj.key));\n            } catch (Exception e) {\n                LOG.warning(\"Couldn't parse S3 key to Cid: \" + obj.key);\n            }\n        }, maxReturned);\n        return results;\n    }\n\n    private void applyToAll(Optional<String> prefix, Consumer<S3AdminRequests.ObjectMetadata> processor, long maxObjects) {\n        try {\n            Optional<String> continuationToken = Optional.empty();\n            S3AdminRequests.ListObjectsReply result;\n            long processedObjects = 0;\n            do {\n                result = S3AdminRequests.listObjects(folder + prefix.orElse(\"\"), 1_000, continuationToken,\n                        ZonedDateTime.now(), host, region, storageClass, accessKeyId, secretKey, url -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }, S3AdminRequests.builder::get, useHttps, hasher);\n\n                for (S3AdminRequests.ObjectMetadata objectSummary : result.objects) {\n                    if (objectSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + objectSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    processor.accept(objectSummary);\n                    processedObjects++;\n                    if (processedObjects >= maxObjects)\n                        return;\n                }\n                LOG.log(Level.FINE, \"Next Continuation Token : \" + result.continuationToken);\n                continuationToken = result.continuationToken;\n            } while (result.isTruncated);\n\n        } catch (Exception e) {\n            LOG.log(Level.SEVERE, e.getMessage(), e);\n        }\n    }\n\n    private void applyToAllVersions(String prefix,\n                                    Optional<String> startKey,\n                                    Consumer<List<UserBlockVersion>> processor,\n                                    Consumer<List<UserBlockVersion>> deleteProcessor) {\n        Optional<String> keyMarker = startKey;\n        Optional<String> versionIdMarker = Optional.empty();\n        S3AdminRequests.ListObjectVersionsReply result = null;\n        int sleep = 100;\n        do {\n            try {\n                result = S3AdminRequests.listObjectVersions(folder + prefix, 1_000, keyMarker, versionIdMarker,\n                        ZonedDateTime.now(), host, region, storageClass, accessKeyId, secretKey, url -> getWithBackoff(() -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }), S3AdminRequests.builder::get, useHttps, hasher);\n\n                List<UserBlockVersion> versions = result.versions.stream()\n                        .filter(omv -> !omv.key.endsWith(\"/\"))\n                        .map(omv -> {\n                            Pair<String, Cid> userBlock = keyToHash(omv.key);\n                            return new UserBlockVersion(userBlock.left, userBlock.right, omv.version, omv.isLatest);\n                        })\n                        .collect(Collectors.toList());\n                processor.accept(versions);\n\n                List<UserBlockVersion> deletes = result.deletes.stream()\n                        .filter(dm -> !dm.key.endsWith(\"/\"))\n                        .map(dm -> {\n                            Pair<String, Cid> userBlock = keyToHash(dm.key);\n                            return new UserBlockVersion(userBlock.left, userBlock.right, dm.version, dm.isLatest);\n                        })\n                        .collect(Collectors.toList());\n                deleteProcessor.accept(deletes);\n                LOG.log(Level.FINE, \"Next key marker : \" + result.nextKeyMarker);\n                LOG.log(Level.FINE, \"Next version id marker : \" + result.nextVersionIdMarker);\n                keyMarker = result.nextKeyMarker;\n                versionIdMarker = result.nextVersionIdMarker;\n                sleep = Math.max(100, sleep / 2);\n            } catch (Exception e) {\n                Throwable cause = getRootCause(e);\n                boolean rateLimited = cause instanceof RateLimitException\n                        || cause instanceof SocketTimeoutException\n                        || cause instanceof SSLException\n                        || cause instanceof SocketException\n                        || isRateLimitedException(e);\n                if (rateLimited) {\n                    Threads.sleep(sleep);\n                    sleep *= 2;\n                } else {\n                    LOG.log(Level.SEVERE, e.getMessage(), e);\n                }\n            }\n        } while (result == null || result.isTruncated);\n    }\n\n    public interface S3VersionLister {\n        S3AdminRequests.ListObjectVersionsReply list(Optional<String> keyMarker, Optional<String> versionIdMarker);\n    }\n\n    /** Compute the initial (keyMarker, endKey) ranges that together cover exactly the keys starting with\n     *  folder+prefix, with no leakage beyond the prefix. Each range is seeded at a different first character\n     *  so workers can list in parallel without overlap.\n     */\n    public static List<Pair<Optional<String>, Optional<String>>> computeInitialRanges(String folder, String prefix, String minFirst) {\n        // The first key that sorts after everything starting with (folder+prefix).\n        // For an empty prefix, there is no upper bound (list the whole bucket).\n        String fullPrefix = folder + prefix;\n        Optional<String> prefixEndKey = prefix.isEmpty() ? Optional.empty() :\n                Optional.of(fullPrefix.substring(0, fullPrefix.length() - 1) +\n                        (char)(fullPrefix.charAt(fullPrefix.length() - 1) + 1));\n\n        List<String> bounds = KEY_FIRST_CHARS.stream()\n                .filter(c -> minFirst.isEmpty() || c.compareTo(minFirst) >= 0)\n                .map(c -> folder + prefix + c)\n                .collect(Collectors.toList());\n        List<Pair<Optional<String>, Optional<String>>> ranges = new ArrayList<>(bounds.size());\n        for (int i = 0; i < bounds.size(); i++) {\n            Optional<String> end = (i + 1 < bounds.size()) ? Optional.of(bounds.get(i + 1)) : prefixEndKey;\n            ranges.add(new Pair<>(Optional.of(bounds.get(i)), end));\n        }\n        return ranges;\n    }\n\n    private void applyToAllVersionsParallel(String prefix,\n                                            Optional<String> startKey,\n                                            Consumer<List<UserBlockVersion>> processor,\n                                            Consumer<List<UserBlockVersion>> deleteProcessor) {\n        applyToAllVersionsParallel(prefix, startKey, processor, deleteProcessor,\n                (keyMarker, versionIdMarker) -> S3AdminRequests.listObjectVersions(folder, 1_000, keyMarker, versionIdMarker,\n                        ZonedDateTime.now(), host, region, storageClass, accessKeyId, secretKey, url -> getWithBackoff(() -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }), S3AdminRequests.builder::get, useHttps, hasher));\n    }\n\n    void applyToAllVersionsParallel(String prefix,\n                                    Optional<String> startKey,\n                                    Consumer<List<UserBlockVersion>> processor,\n                                    Consumer<List<UserBlockVersion>> deleteProcessor,\n                                    S3VersionLister lister) {\n        String minFirst = startKey.map(k -> k.isEmpty() ? \"\" : k.substring(0, 1)).orElse(\"\");\n\n        // Work queue: each entry is (keyMarker, versionIdMarker, endKey) using full S3 keys.\n        // endKey is exclusive and enforced client-side; Optional.empty() means scan to end of folder.\n        LinkedBlockingQueue<Triple<Optional<String>, Optional<String>, Optional<String>>> workQueue = new LinkedBlockingQueue<>();\n\n        List<Pair<Optional<String>, Optional<String>>> ranges = computeInitialRanges(folder, prefix, minFirst);\n        for (Pair<Optional<String>, Optional<String>> range : ranges)\n            workQueue.add(new Triple<>(range.left, Optional.empty(), range.right));\n\n        AtomicInteger inFlight = new AtomicInteger(workQueue.size());\n        CompletableFuture<Void> done = new CompletableFuture<>();\n\n        ForkJoinPool pool = Threads.newFJPool(LIST_PARALLELISM, \"S3-List-\");\n        try {\n            for (int i = 0; i < LIST_PARALLELISM; i++)\n                pool.execute(() -> listWorker(workQueue, inFlight, done, processor, deleteProcessor, lister));\n            done.join();\n        } finally {\n            pool.shutdown();\n        }\n    }\n\n    /** Returns a key strictly between start and end, or empty if no such key can be constructed. */\n    private static Optional<String> lexMidpoint(Optional<String> start, Optional<String> end) {\n        String a = start.orElse(\"\");\n        if (end.isEmpty()) {\n            return Optional.of(a + KEY_FIRST_CHARS.get(KEY_FIRST_CHARS.size() / 2));\n        }\n        String b = end.get();\n        if (a.compareTo(b) >= 0) return Optional.empty();\n        int i = 0;\n        while (i < a.length() && i < b.length() && a.charAt(i) == b.charAt(i))\n            i++;\n        if (i < b.length()) {\n            char ce = b.charAt(i);\n            char cs = (i < a.length()) ? a.charAt(i) : (char)(ce - 1);\n            if (ce > cs + 1)\n                return Optional.of(a.substring(0, i) + (char)((cs + ce) / 2));\n        }\n        // Characters are adjacent or a is a prefix of b: append a middle character to a\n        String candidate = a + KEY_FIRST_CHARS.get(KEY_FIRST_CHARS.size() / 2);\n        return (candidate.compareTo(b) < 0) ? Optional.of(candidate) : Optional.empty();\n    }\n\n    private void listWorker(\n            LinkedBlockingQueue<Triple<Optional<String>, Optional<String>, Optional<String>>> workQueue,\n            AtomicInteger inFlight,\n            CompletableFuture<Void> done,\n            Consumer<List<UserBlockVersion>> processor,\n            Consumer<List<UserBlockVersion>> deleteProcessor,\n            S3VersionLister lister) {\n        while (!done.isDone()) {\n            Triple<Optional<String>, Optional<String>, Optional<String>> task;\n            try {\n                task = workQueue.poll(100, TimeUnit.MILLISECONDS);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n                return;\n            }\n            if (task == null) {\n                if (inFlight.get() == 0) break;\n                continue;\n            }\n            Optional<String> keyMarker = task.left;\n            Optional<String> versionIdMarker = task.middle;\n            Optional<String> endKey = task.right;\n\n            S3AdminRequests.ListObjectVersionsReply result = null;\n            try {\n                int sleep = 100;\n                while (result == null) {\n                    try {\n                        result = lister.list(keyMarker, versionIdMarker);\n                    } catch (Exception e) {\n                        Throwable cause = getRootCause(e);\n                        boolean rateLimited = cause instanceof RateLimitException\n                                || cause instanceof SocketTimeoutException\n                                || cause instanceof SSLException\n                                || cause instanceof SocketException\n                                || isRateLimitedException(e);\n                        if (rateLimited) {\n                            Threads.sleep(sleep);\n                            sleep *= 2;\n                        } else {\n                            done.completeExceptionally(e);\n                            return;\n                        }\n                    }\n                }\n\n                final Optional<String> finalEndKey = endKey;\n                boolean rangeHasMore = result.isTruncated\n                        && result.nextKeyMarker.isPresent()\n                        && (endKey.isEmpty() || result.nextKeyMarker.get().compareTo(endKey.get()) < 0);\n\n                if (rangeHasMore) {\n                    Optional<String> nextKey = result.nextKeyMarker;\n                    Optional<String> nextVid = result.nextVersionIdMarker;\n                    Optional<String> continuationEnd = endKey;\n\n                    // When the queue is empty, workers may be idle: split the remaining range\n                    // at a lexicographic midpoint and donate the far half to the queue\n                    if (workQueue.isEmpty()) {\n                        Optional<String> mid = lexMidpoint(nextKey, endKey);\n                        if (mid.isPresent()) {\n                            inFlight.incrementAndGet();\n                            workQueue.add(new Triple<>(mid, Optional.empty(), endKey));\n                            continuationEnd = mid;\n                        }\n                    }\n\n                    // Enqueue continuation BEFORE processing data so another worker can immediately\n                    // start the next S3 request, overlapping with this worker's data processing\n                    inFlight.incrementAndGet();\n                    workQueue.add(new Triple<>(nextKey, nextVid, continuationEnd));\n                }\n\n                List<UserBlockVersion> versions = result.versions.stream()\n                        .filter(omv -> !omv.key.endsWith(\"/\"))\n                        .filter(omv -> finalEndKey.isEmpty() || omv.key.compareTo(finalEndKey.get()) < 0)\n                        .map(omv -> {\n                            Pair<String, Cid> userBlock = keyToHash(omv.key);\n                            return new UserBlockVersion(userBlock.left, userBlock.right, omv.version, omv.isLatest);\n                        })\n                        .collect(Collectors.toList());\n                processor.accept(versions);\n                List<UserBlockVersion> deletes = result.deletes.stream()\n                        .filter(dm -> !dm.key.endsWith(\"/\"))\n                        .filter(dm -> finalEndKey.isEmpty() || dm.key.compareTo(finalEndKey.get()) < 0)\n                        .map(dm -> {\n                            Pair<String, Cid> userBlock = keyToHash(dm.key);\n                            return new UserBlockVersion(userBlock.left, userBlock.right, dm.version, dm.isLatest);\n                        })\n                        .collect(Collectors.toList());\n                deleteProcessor.accept(deletes);\n                LOG.log(Level.FINE, \"Next key marker: \" + result.nextKeyMarker);\n            } catch (Exception e) {\n                if (!done.isDone()) done.completeExceptionally(e);\n                return;\n            } finally {\n                if (inFlight.decrementAndGet() == 0)\n                    done.complete(null);\n            }\n        }\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid hash) {\n        delete(owner, new BlockVersion(hash, null, true));\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, BlockVersion version) {\n        try {\n            PresignedUrl delUrl = S3AdminRequests.preSignDelete(folder + hashToKey(owner, version.cid), Optional.ofNullable(version.version),\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, storageClass, accessKeyId, secretKey, useHttps, hasher).join();\n            HttpUtil.delete(delUrl);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static AtomicBoolean hasBulkDeleteError = new AtomicBoolean(false);\n\n    @Override\n    public void bulkDelete(PublicKeyHash owner, List<BlockVersion> versions) {\n        List<Pair<String, String>> keyVersions = versions.stream()\n                .map(v -> new Pair<>(folder + hashToKey(owner, v.cid), v.version))\n                .collect(Collectors.toList());\n        try {\n            S3AdminRequests.bulkDelete(keyVersions, ZonedDateTime.now(), host, region, storageClass, accessKeyId, secretKey,\n                    b -> ArrayOps.bytesToHex(Hash.sha256(b)),\n                    b -> Base64.encodeBase64String(Hash.sha256(b)),\n                    (url, body) -> {\n                        try {\n                            return HttpUtil.post(url, body);\n                        } catch (IOException e) {\n                            boolean rateLimited = isRateLimitedException(e);\n                            if (rateLimited) {\n                                S3BlockStorage.rateLimited.inc();\n                                throw new RateLimitException();\n                            }\n                            throw new RuntimeException(e);\n                        }\n                    }, S3AdminRequests.builder::get, useHttps, hasher);\n        } catch (Exception e) {\n            // fallback to doing deletes with parallel single calls\n            // This is necessary because B2 doesn't implement the bulk delete call!!\n            if (! hasBulkDeleteError.get())\n                System.out.println(\"Falling back to parallel individual block deletes... (B2 doesn't implement bulk delete)\" + e.getMessage());\n            hasBulkDeleteError.set(true);\n            for (BlockVersion version : versions) {\n                new Thread(() -> delete(owner, version)).start();\n            }\n        }\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return Optional.of(cborCache);\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Shouldn't get here.\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Shouldn't get here.\");\n    }\n\n    public static void main(String[] args) throws Exception {\n        Args a = Args.parse(args);\n        Logging.init(a.with(\"log-to-console\", \"true\"));\n        Crypto crypto = Main.initCrypto();\n        Hasher hasher = crypto.hasher;\n        S3Config config = S3Config.build(a, Optional.empty());\n        boolean versioned = a.getBoolean(\"s3.versioned-bucket\");\n        boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n        SqlSupplier sqlCommands = usePostgres ?\n                new PostgresCommands() :\n                new SqliteCommands();\n        Supplier<Connection> database = Main.getDBConnector(a, \"mutable-pointers-file\");\n        Supplier<Connection> transactionsDb = Main.getDBConnector(a, \"transactions-sql-file\");\n        TransactionStore transactions = JdbcTransactionStore.build(transactionsDb, sqlCommands);\n        BlockRequestAuthoriser authoriser = (c, b, s, auth) -> Futures.of(true);\n        BlockMetadataStore meta = Builder.buildBlockMetadata(a);\n        Supplier<Connection> usageDb = Main.getDBConnector(a, \"space-usage-sql-file\");\n        UsageStore usage = new JdbcUsageStore(usageDb, sqlCommands);\n        Supplier<Connection> statusDb = Main.getDBConnector(a, \"partition-status-file\");\n        PartitionStatus partitioned = new JdbcPartitionStatus(statusDb, sqlCommands);\n        JavaPoster p2pHttpProxy = Builder.buildP2pHttpProxy(a);\n        ContentAddressedStorageProxy p2pHttpFallback = new ContentAddressedStorageProxy.HTTP(p2pHttpProxy);\n        p2pHttpFallback = Builder.buildP2PBlockRetrieverForS3(a, usage, hasher, p2pHttpFallback);\n        S3BlockStorage s3 = new S3BlockStorage(config, List.of(Cid.decode(a.getArg(\"ipfs.id\"))),\n                BlockStoreProperties.empty(), \"localhost:8000\", transactions, authoriser, null, meta, usage,\n                new RamBlockCache(1024, 100),\n                new FileBlockBuffer(a.fromPeergosDir(\"s3-block-buffer-dir\", \"block-buffer\"), usage),\n                Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,\n                versioned, a.getPeergosDir(), partitioned, hasher,\n                new RAMStorage(hasher), p2pHttpFallback);\n        JdbcIpnsAndSocial rawPointers = new JdbcIpnsAndSocial(database, sqlCommands);\n\n        MutablePointers localPointers = UserRepository.build(s3, rawPointers, hasher);\n        Multihash pkiServerNodeId = Builder.getPkiServerId(a);\n        MutablePointersProxy proxingMutable = new HttpMutablePointers(p2pHttpProxy, pkiServerNodeId);\n        LinkRetrievalCounter linkCounts = new JdbcLinkRetrievalcounter(Main.getDBConnector(a, \"link-counts-sql-file\", database), sqlCommands);\n        JdbcIpnsAndSocial rawSocial = new JdbcIpnsAndSocial(Builder.getDBConnector(a, \"social-sql-file\", database), sqlCommands);\n        String listeningHost = a.getArg(Main.LISTEN_HOST.name, \"localhost\");\n        int webPort = a.getInt(\"port\");\n        Optional<String> tlsHostname = a.hasArg(\"tls.keyfile.password\") ? Optional.of(listeningHost) : Optional.empty();\n        Optional<String> publicHostname = tlsHostname.isPresent() ? tlsHostname : a.getOptionalArg(\"public-domain\");\n        Origin origin = new Origin(publicHostname.map(host -> (Main.isLanIP(host) ? \"http://\" : \"https://\") + host).orElse(\"http://localhost:\" + webPort));\n        String rpId = publicHostname.orElse(\"localhost\");\n        JdbcAccount rawAccount = new JdbcAccount(Builder.getDBConnector(a, \"account-sql-file\", database), sqlCommands, origin, rpId);\n        Account account = new AccountWithStorage(s3, localPointers, rawAccount);\n        boolean isPki = false;\n        InetSocketAddress userAPIAddress = new InetSocketAddress(listeningHost, webPort);\n        boolean localhostApi = userAPIAddress.getHostName().equals(\"localhost\");\n        QuotaAdmin userQuotas = Main.buildSpaceQuotas(a, s3,\n                Main.getDBConnector(a, \"space-requests-sql-file\", database),\n                Main.getDBConnector(a, \"quotas-sql-file\", database), isPki, localhostApi);\n        JdbcBatCave batStore = new JdbcBatCave(Main.getDBConnector(a, \"bat-store\", database), sqlCommands);\n\n        CoreNode core = Builder.buildCorenode(a, s3, transactions, rawPointers, localPointers, proxingMutable,\n                rawSocial, usage, userQuotas, rawAccount, batStore, account, linkCounts, crypto);\n\n        s3.setPki(core);\n        if (a.hasArg(\"integrity-check\")) {\n            if (a.hasArg(\"username\"))\n                GarbageCollector.checkUserIntegrity(a.getArg(\"username\"), s3, meta, rawPointers, usage, core, a.getBoolean(\"fix-metadata\", false), hasher);\n            else\n                GarbageCollector.checkIntegrity(s3, meta, rawPointers, usage, core, a.getBoolean(\"fix-metadata\", false), hasher);\n            return;\n        }\n        System.out.println(\"Performing GC on S3 block store...\");\n        s3.collectGarbage(rawPointers, usage, meta, versioned);\n    }\n\n    @Override\n    public String toString() {\n        return \"S3BlockStore[\" + bucket + \":\" + folder + \"]\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3BucketCopy.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.http.HttpClient;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class S3BucketCopy {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    private static void applyToAllInRange(Consumer<S3AdminRequests.ObjectMetadata> processor,\n                                          String startPrefix,\n                                          Optional<String> endPrefix,\n                                          S3Config config,\n                                          AtomicLong counter,\n                                          Hasher h) {\n        Optional<String> continuationToken = Optional.empty();\n        S3AdminRequests.ListObjectsReply result;\n        while (true) {\n            try {\n                result = S3AdminRequests.listObjects(startPrefix, 1_000, continuationToken,\n                        ZonedDateTime.now(), config.getHost(), config.region, config.storageClass, config.accessKey, config.secretKey, url -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }, S3AdminRequests.builder::get, true, h);\n\n                for (S3AdminRequests.ObjectMetadata objectSummary : result.objects) {\n                    if (objectSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + objectSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    if (endPrefix.isPresent() && objectSummary.key.compareTo(endPrefix.get()) >= 0)\n                        return;\n                    processor.accept(objectSummary);\n                }\n                long done = counter.addAndGet(result.objects.size());\n                if ((done / 1000) % 10 == 0)\n                    System.out.println(\"Objects processed: \" + done);\n                LOG.log(Level.FINE, \"Next Continuation Token : \" + result.continuationToken);\n                continuationToken = result.continuationToken;\n                if (! result.isTruncated)\n                    break;\n            } catch (RateLimitException r) {\n                Threads.sleep(5_000);\n            } catch (Exception e) {\n                LOG.log(Level.SEVERE, e.getMessage(), e);\n            }\n        }\n    }\n\n    private static Set<String> getFilenames(S3Config config,\n                                            HttpClient client,\n                                            Hasher h) {\n        Set<String> results = new HashSet<>();\n        applyToAllInRange(obj -> results.add(obj.key), \"\", Optional.empty(), config, new AtomicLong(0), h);\n        return results;\n    }\n\n    private static void copyObject(String key,\n                                   String sourceBucket,\n                                   S3Config config,\n                                   Hasher h) {\n        PresignedUrl copyUrl = S3Request.preSignCopy(sourceBucket, key, key, S3AdminRequests.asAwsDate(ZonedDateTime.now()), config.getHost(),\n                config.storageClass, Collections.emptyMap(), config.region, config.accessKey, config.secretKey, true, h).join();\n        try {\n            System.out.println(\"Copying s3://\"+sourceBucket + \"/\" + key + \" to s3://\" + config.bucket);\n            String res = new String(HttpUtil.putWithVersion(copyUrl, new byte[0]).left);\n            if (! res.startsWith(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?><CopyObjectResult\") || !res.contains(\"</LastModified><ETag>\"))\n                throw new IllegalStateException(res);\n        } catch (IOException e) {\n            System.err.println(e.getMessage());\n        }\n    }\n\n    private static void copyObjectInterProvider(String key,\n                                                S3Config source,\n                                                S3Config target,\n                                                Hasher h) {\n        PresignedUrl getUrl = S3Request.preSignGet(key, Optional.of(600), Optional.empty(),\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), source.getHost(),\n                source.region, source.storageClass, source.accessKey, source.secretKey, true, h).join();\n        try {\n            System.out.println(\"Copying s3://\"+source.getHost() + \"/\" + source.bucket + \"/\" + key + \" to s3://\" + target.getHost() + \"/\" + target.bucket);\n            byte[] res = HttpUtil.get(getUrl);\n            Map<String, String> extraHeaders = new TreeMap<>();\n            extraHeaders.put(\"Content-Type\", \"application/octet-stream\");\n            boolean hashContent = true;\n            String contentHash = hashContent ? ArrayOps.bytesToHex(DirectS3BlockStore.keyToHash(key).getHash()) : \"UNSIGNED-PAYLOAD\";\n            HttpUtil.putWithVersion(S3Request.preSignPut(key, res.length, contentHash, target.storageClass, false,\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), target.getHost(), extraHeaders, target.region, target.accessKey, target.secretKey,  true,h).join(), res);\n        } catch (IOException e) {\n            System.err.println(e.getMessage());\n        }\n    }\n\n    private static void copyRange(String startPrefix,\n                                  Optional<String> endPrefix,\n                                  S3Config source,\n                                  S3Config dest,\n                                  AtomicLong counter,\n                                  AtomicLong copyCounter,\n                                  int parallelism,\n                                  HttpClient client,\n                                  Hasher h) {\n        System.out.println(\"Listing destination bucket...\");\n        Set<String> targetKeys = getFilenames(dest, client, h);\n        ForkJoinPool pool = Threads.newFJPool(parallelism, \"S3-copy-\");\n        boolean sameHost = source.regionEndpoint.equals(dest.regionEndpoint);\n        System.out.println(\"Copying objects...\");\n        applyToAllInRange(obj -> {\n            if (!targetKeys.contains(obj.key)) {\n                while (pool.getQueuedSubmissionCount() > 100)\n                    try {Thread.sleep(100);} catch (InterruptedException e) {}\n                copyCounter.incrementAndGet();\n                pool.submit(() -> {\n                    if (sameHost)\n                        copyObject(obj.key, source.bucket, dest, h);\n                    else\n                        copyObjectInterProvider(obj.key, source, dest, h);\n                });\n            }\n        }, startPrefix, endPrefix, source, counter, h);\n        while (! pool.isQuiescent())\n            try {Thread.sleep(100);} catch (InterruptedException e) {}\n        System.out.println(\"Objects copied: \" + copyCounter.get());\n    }\n\n    public static void main(String[] args) {\n        Args a = Args.parse(args);\n        S3Config source = S3Config.build(a, Optional.empty());\n        S3Config dest = S3Config.build(a, Optional.of(\"dest.\"));\n\n        String startPrefix = \"\";\n        Optional<String> endPrefix = Optional.empty();\n\n        System.out.println(\"Copying S3 bucket \" + source.getHost() + \"/\" + source.bucket + \" to \" + dest.getHost() + \"/\" + dest.bucket);\n        HttpClient client = HttpClient.newBuilder()\n                .connectTimeout(Duration.ofMillis(10_000))\n                .build();\n        copyRange(startPrefix, endPrefix, source, dest, new AtomicLong(0),\n                new AtomicLong(0), a.getInt(\"parallelism\"), client, Main.initCrypto().hasher);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3BucketStats.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\n\nimport java.io.*;\nimport java.net.http.HttpClient;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class S3BucketStats {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    private static void applyToAllInRange(Consumer<S3AdminRequests.ObjectMetadata> processor,\n                                          String startPrefix,\n                                          Optional<String> endPrefix,\n                                          S3Config config,\n                                          AtomicLong counter,\n                                          Hasher h) {\n        try {\n            Optional<String> continuationToken = Optional.empty();\n            S3AdminRequests.ListObjectsReply result;\n            do {\n                result = S3AdminRequests.listObjects(startPrefix, 1_000, continuationToken,\n                        ZonedDateTime.now(), config.getHost(), config.region, config.storageClass, config.accessKey, config.secretKey, url -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }, S3AdminRequests.builder::get, true, h);\n\n                for (S3AdminRequests.ObjectMetadata objectSummary : result.objects) {\n                    if (objectSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + objectSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    if (endPrefix.isPresent() && objectSummary.key.compareTo(endPrefix.get()) >= 0)\n                        return;\n                    processor.accept(objectSummary);\n                }\n                long done = counter.addAndGet(result.objects.size());\n                if ((done / 1000) % 10 == 0)\n                    System.out.println(\"Objects processed: \" + done);\n                LOG.log(Level.FINE, \"Next Continuation Token : \" + result.continuationToken);\n                continuationToken = result.continuationToken;\n            } while (result.isTruncated);\n\n        } catch (Exception e) {\n            LOG.log(Level.SEVERE, e.getMessage(), e);\n        }\n    }\n\n    private static void analyseRange(String startPrefix,\n                                     Optional<String> endPrefix,\n                                     S3Config source,\n                                     AtomicLong counter,\n                                     HttpClient client,\n                                     Hasher h) {\n        AtomicLong rawBlocks = new AtomicLong(0);\n        AtomicLong cborBlocks = new AtomicLong(0);\n        AtomicLong cborBlocksSize = new AtomicLong(0);\n        AtomicLong rawBlocksSize = new AtomicLong(0);\n        applyToAllInRange(obj -> {\n            boolean isRaw = DirectS3BlockStore.keyToHash(obj.key).isRaw();\n            if (isRaw) {\n                rawBlocks.incrementAndGet();\n                rawBlocksSize.addAndGet(obj.size);\n            } else {\n                cborBlocks.incrementAndGet();\n                cborBlocksSize.addAndGet(obj.size);\n            }\n        }, startPrefix, endPrefix, source, counter, h);\n        System.out.println(\"Raw blocks: \" + rawBlocks.get() + \",  size: \" + rawBlocksSize.get() + \",  average size: \" + (rawBlocksSize.get()/rawBlocks.get()));\n        System.out.println(\"Cbor blocks: \" + cborBlocks.get() + \",  size: \" + cborBlocksSize.get() + \",  average size: \" + (cborBlocksSize.get()/cborBlocks.get()));\n    }\n    public static void main(String[] args) {\n        Args a = Args.parse(args);\n        S3Config source = S3Config.build(a, Optional.empty());\n\n        String startPrefix = \"\";\n        Optional<String> endPrefix = Optional.empty();\n\n        System.out.println(\"Analysing S3 bucket \" + source.getHost() + \"/\" + source.bucket);\n        HttpClient client = HttpClient.newBuilder()\n                .connectTimeout(Duration.ofMillis(10_000))\n                .build();\n        analyseRange(startPrefix, endPrefix, source, new AtomicLong(0), client, Main.initCrypto().hasher);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3BucketSync.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.Main;\nimport peergos.server.util.Args;\nimport peergos.server.util.HttpUtil;\nimport peergos.server.util.Threads;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.storage.PresignedUrl;\nimport peergos.shared.storage.RateLimitException;\nimport peergos.shared.storage.auth.S3Request;\nimport peergos.shared.util.ArrayOps;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.time.ZonedDateTime;\nimport java.util.*;\nimport java.util.concurrent.ForkJoinPool;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Consumer;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\npublic class S3BucketSync {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    private static void applyToAllInRange(Consumer<S3AdminRequests.ObjectMetadata> processor,\n                                          String startPrefix,\n                                          Optional<String> endPrefix,\n                                          S3Config config,\n                                          AtomicLong counter,\n                                          Hasher h) {\n        Optional<String> continuationToken = Optional.empty();\n        S3AdminRequests.ListObjectsReply result;\n        while (true) {\n            try {\n                result = S3AdminRequests.listObjects(startPrefix, 1_000, continuationToken,\n                        ZonedDateTime.now(), config.getHost(), config.region, config.storageClass, config.accessKey, config.secretKey, url -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }, S3AdminRequests.builder::get, true, h);\n\n                for (S3AdminRequests.ObjectMetadata objectSummary : result.objects) {\n                    if (objectSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + objectSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    if (endPrefix.isPresent() && objectSummary.key.compareTo(endPrefix.get()) >= 0)\n                        return;\n                    processor.accept(objectSummary);\n                }\n                long done = counter.addAndGet(result.objects.size());\n                if ((done / 1000) % 10 == 0)\n                    System.out.println(\"Objects processed: \" + done);\n                LOG.log(Level.FINE, \"Next Continuation Token : \" + result.continuationToken);\n                continuationToken = result.continuationToken;\n                if (! result.isTruncated)\n                    break;\n            } catch (RateLimitException r) {\n                Threads.sleep(5_000);\n            } catch (Exception e) {\n                LOG.log(Level.SEVERE, e.getMessage(), e);\n            }\n        }\n    }\n\n    private static Map<String, String> getFileHashes(S3Config config, Hasher h) {\n        Map<String, String> results = new HashMap<>();\n        applyToAllInRange(obj -> {\n            results.put(obj.key, obj.etag.substring(1, obj.etag.length() - 1)); // strip \"'s\n        }, \"\", Optional.empty(), config, new AtomicLong(0), h);\n        return results;\n    }\n\n    private static void uploadFile(String key,\n                                   Path source,\n                                   S3Config target,\n                                   Hasher h) {\n        try {\n            System.out.println(\"Copying \" + source + \" to s3://\" + target.getHost() + \"/\" + key);\n            byte[] res = Files.readAllBytes(source);\n            Map<String, String> extraHeaders = new TreeMap<>();\n            boolean hashContent = true;\n            String contentHash = hashContent ? ArrayOps.bytesToHex(h.sha256(res).join()) : \"UNSIGNED-PAYLOAD\";\n            HttpUtil.putWithVersion(S3Request.preSignPut(key, res.length, contentHash, target.storageClass, false,\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), target.getHost(), extraHeaders, target.region, target.accessKey, target.secretKey,  true,h).join(), res);\n        } catch (IOException e) {\n            e.printStackTrace();\n            System.err.println(e.getMessage());\n        }\n    }\n\n    private static String md5(Path file) throws IOException {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"MD5\");\n\n            try (InputStream is = Files.newInputStream(file)) {\n                byte[] buffer = new byte[8192];\n                int bytesRead;\n\n                while ((bytesRead = is.read(buffer)) != -1) {\n                    md.update(buffer, 0, bytesRead);\n                }\n            }\n\n            return ArrayOps.bytesToHex(md.digest());\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static void syncFrom(String startPrefix,\n                                 Optional<String> endPrefix,\n                                 Path source,\n                                 S3Config dest,\n                                 AtomicLong counter,\n                                 AtomicLong copyCounter,\n                                 int parallelism,\n                                 Hasher h) throws Exception {\n        System.out.println(\"Listing destination bucket...\");\n        Map<String, String> targetKeys = getFileHashes(dest, h);\n        AtomicInteger skipped = new AtomicInteger(0);\n        Set<String> done = new HashSet<>();\n        ForkJoinPool pool = Threads.newFJPool(parallelism, \"S3-copy-\");\n        System.out.println(\"Syncing objects...\");\n        Files.walkFileTree(source, new FileVisitor<>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes basicFileAttributes) throws IOException {\n                if (basicFileAttributes.isDirectory())\n                    return FileVisitResult.CONTINUE;\n                String filepath = source.relativize(file).toString();\n                String localMd5 = md5(file);\n                String remoteMd5 = targetKeys.get(filepath);\n                if (localMd5.equals(remoteMd5)) {\n                    done.add(filepath);\n                    skipped.incrementAndGet();\n                    return FileVisitResult.CONTINUE;\n                }\n                System.out.println(\"Remote md5: \" + remoteMd5 + \", local: \" + localMd5);\n                while (pool.getQueuedSubmissionCount() > 100)\n                    try {Thread.sleep(100);} catch (InterruptedException e) {}\n                copyCounter.incrementAndGet();\n                pool.submit(() -> {\n                    System.out.println(\"Uploading \" + filepath);\n                    uploadFile(filepath, file, dest, h);\n                });\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n        });\n        HashSet<String> toDelete = new HashSet<>(targetKeys.keySet());\n        toDelete.removeAll(done);\n        System.out.println(\"Deleting \" + toDelete.size() + \" remote files...\");\n        for (String key : toDelete) {\n            PresignedUrl delUrl = S3AdminRequests.preSignDelete(key, Optional.empty(),\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), dest.getHost(), dest.region, dest.storageClass,\n                    dest.accessKey, dest.secretKey, true, h).join();\n            HttpUtil.delete(delUrl);\n        }\n\n        while (! pool.isQuiescent())\n            try {Thread.sleep(100);} catch (InterruptedException e) {}\n        System.out.println(\"Objects copied: \" + copyCounter.get());\n        System.out.println(\"Objects skipped: \" + skipped.get());\n    }\n\n    public static void main(String[] args) throws Exception {\n        Args a = Args.parse(args);\n        S3Config dest = S3Config.build(a, Optional.empty());\n        Path source = Paths.get(a.getArg(\"source-dir\"));\n\n        String startPrefix = \"\";\n        Optional<String> endPrefix = Optional.empty();\n\n        System.out.println(\"Sync S3 bucket \" + dest.getHost() + \"/\" + dest.bucket + \" from \" + source);\n        syncFrom(startPrefix, endPrefix, source, dest, new AtomicLong(0),\n                new AtomicLong(0), a.getInt(\"parallelism\"), Main.initCrypto().hasher);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3CanonicaliseVersionedBucket.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.*;\n\nimport java.io.*;\nimport java.net.http.HttpClient;\nimport java.time.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class S3CanonicaliseVersionedBucket {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    private static void applyToAllVersions(String prefix,\n                                           Consumer<S3AdminRequests.ObjectMetadataVersion> processor,\n                                           Consumer<S3AdminRequests.DeleteMarker> deleteProcessor,\n                                           long maxObjects,\n                                           S3Config config,\n                                           Hasher h) {\n        try {\n            Optional<String> keyMarker = Optional.empty();\n            Optional<String> versionIdMarker = Optional.empty();\n            S3AdminRequests.ListObjectVersionsReply result;\n            long processedObjects = 0;\n            do {\n                result = S3AdminRequests.listObjectVersions(prefix, 1_000, keyMarker, versionIdMarker,\n                        ZonedDateTime.now(), config.getHost(), config.region, config.storageClass, config.accessKey, config.secretKey, url -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }, S3AdminRequests.builder::get, true, h);\n\n                for (S3AdminRequests.ObjectMetadataVersion objectSummary : result.versions) {\n                    if (objectSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + objectSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    processor.accept(objectSummary);\n                    processedObjects++;\n                    if (processedObjects >= maxObjects)\n                        return;\n                }\n                for (S3AdminRequests.DeleteMarker deleteSummary : result.deletes) {\n                    if (deleteSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + deleteSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    deleteProcessor.accept(deleteSummary);\n                    processedObjects++;\n                    if (processedObjects >= maxObjects)\n                        return;\n                }\n                LOG.log(Level.FINE, \"Next key marker : \" + result.nextKeyMarker);\n                LOG.log(Level.FINE, \"Next version id marker : \" + result.nextVersionIdMarker);\n                keyMarker = result.nextKeyMarker;\n                versionIdMarker = result.nextVersionIdMarker;\n            } while (result.isTruncated);\n\n        } catch (Exception e) {\n            LOG.log(Level.SEVERE, e.getMessage(), e);\n        }\n    }\n\n    private static void processFileVersions(long maxReturned,\n                                            S3Config config,\n                                            HttpClient client,\n                                            Hasher h) {\n        applyToAllVersions(\"\", obj -> {\n            try {\n                if (! obj.isLatest) {\n                    System.out.println(\"Old version of \" + obj.key);\n                    // TODO delete\n                }\n            } catch (Exception e) {\n                LOG.warning(\"Couldn't parse S3 key to Cid: \" + obj.key);\n            }\n        }, del -> {\n            try {\n                if (! del.isLatest) {\n                    System.out.println(\"Old delete marker version of \" + del.key);\n                    // TODO delete\n                }\n            } catch (Exception e) {\n                LOG.warning(\"Couldn't parse S3 key to Cid: \" + del.key);\n            }\n        }, maxReturned, config, h);\n    }\n\n    public static void main(String[] args) {\n        Args a = Args.parse(args);\n        S3Config config = S3Config.build(a, Optional.empty());\n\n        System.out.println(\"Listing old versions in S3 bucket \" + config.getHost() + \"/\" + config.bucket);\n        HttpClient client = HttpClient.newBuilder()\n                .connectTimeout(Duration.ofMillis(10_000))\n                .build();\n        processFileVersions(Long.MAX_VALUE, config, client, Main.initCrypto().hasher);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3Config.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.util.*;\n\nimport java.util.*;\n\npublic class S3Config {\n    public final String path, bucket, region, accessKey, secretKey, regionEndpoint;\n    public final Optional<String> storageClass;\n\n    /**\n     *\n     * @param path The root path to store blocks within the bucket\n     * @param bucket The bucket name\n     * @param region The S3 region e.g. eu-east-1\n     * @param accessKey The S3 access key\n     * @param secretKey The S3 secret key\n     * @param regionEndpoint The location of the S3 endpoint e.g. us-east-1.linodeobjects.com\n     */\n    public S3Config(String path, String bucket, String region, String accessKey, String secretKey, String regionEndpoint, Optional<String> storageClass) {\n        this.path = path;\n        this.bucket = bucket;\n        this.region = region;\n        this.accessKey = accessKey;\n        this.secretKey = secretKey;\n        this.regionEndpoint = regionEndpoint;\n        this.storageClass = storageClass;\n    }\n\n    public String getHost() {\n        return bucket + \".\" + regionEndpoint;\n    }\n\n    public static boolean useS3(Args a) {\n        return a.hasArg(\"s3.bucket\");\n    }\n\n    public static boolean useS3(Args a, String prefix) {\n        return a.hasArg(prefix + \"s3.bucket\");\n    }\n\n    public static S3Config build(Args a, Optional<String> prefix) {\n        String path = a.getArg(prefix.orElse(\"\") + \"s3.path\", \"\");\n        String bucket = a.getArg(prefix.orElse(\"\") + \"s3.bucket\");\n        String region = a.getArg(prefix.orElse(\"\") + \"s3.region\");\n        String accessKey = a.getArg(prefix.orElse(\"\") + \"s3.accessKey\", \"\");\n        String secretKey = a.getArg(prefix.orElse(\"\") + \"s3.secretKey\", \"\");\n        String regionEndpoint = a.getArg(prefix.orElse(\"\") + \"s3.region.endpoint\", bucket + \".amazonaws.com\");\n        boolean glacier = a.getBoolean(prefix.orElse(\"\") + \"use-glacier\", false);\n        Optional<String> storageClass = glacier ? Optional.of(\"GLACIER\") : Optional.empty();\n        return new S3Config(path, bucket, region, accessKey, secretKey, regionEndpoint, storageClass);\n    }\n\n    public static Optional<String> getPublicReadUrl(Args a) {\n        return Optional.ofNullable(a.getArg(\"blockstore-url\", null));\n    }\n\n    public static List<String> getBlockstoreDomains(Args a) {\n        if (! useS3(a))\n            return Collections.emptyList();\n        Optional<String> publicReads = getPublicReadUrl(a);\n        String authedHost = S3Config.build(a,  Optional.empty()).getHost();\n        if (publicReads.isPresent())\n            return Arrays.asList(authedHost, publicReads.get());\n        return Arrays.asList(authedHost);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3DeleteOld.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.bases.Base64;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.http.HttpClient;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class S3DeleteOld {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    private static void applyToAllInRange(Consumer<List<S3AdminRequests.ObjectMetadataVersion>> processor,\n                                          Consumer<List<S3AdminRequests.DeleteMarker>> deleteProcessor,\n                                          String startPrefix,\n                                          Optional<String> endPrefix,\n                                          S3Config config,\n                                          AtomicLong counter,\n                                          Hasher h) {\n        try {\n            Optional<String> keyMarker = Optional.empty();\n            Optional<String> versionIdMarker = Optional.empty();\n            S3AdminRequests.ListObjectVersionsReply result;\n            do {\n                result = S3AdminRequests.listObjectVersions(startPrefix, 1_000, keyMarker, versionIdMarker,\n                        ZonedDateTime.now(), config.getHost(), config.region, config.storageClass, config.accessKey, config.secretKey, url -> {\n                            try {\n                                return HttpUtil.get(url);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        }, S3AdminRequests.builder::get, true, h);\n\n                List<S3AdminRequests.ObjectMetadataVersion> toProcess = new ArrayList<>();\n                List<S3AdminRequests.DeleteMarker> deletesToProcess = new ArrayList<>();\n                for (S3AdminRequests.ObjectMetadataVersion objectSummary : result.versions) {\n                    if (objectSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + objectSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    if (endPrefix.isPresent() && objectSummary.key.compareTo(endPrefix.get()) >= 0)\n                        return;\n                    toProcess.add(objectSummary);\n                }\n                for (S3AdminRequests.DeleteMarker deleteSummary : result.deletes) {\n                    if (deleteSummary.key.endsWith(\"/\")) {\n                        LOG.fine(\" - \" + deleteSummary.key + \"  \" + \"(directory)\");\n                        continue;\n                    }\n                    if (endPrefix.isPresent() && deleteSummary.key.compareTo(endPrefix.get()) >= 0)\n                        return;\n                    deletesToProcess.add(deleteSummary);\n                }\n                processor.accept(toProcess);\n                deleteProcessor.accept(deletesToProcess);\n                long done = counter.addAndGet(result.versions.size());\n                if ((done / 1000) % 10 == 0)\n                    System.out.println(\"Objects processed: \" + done);\n                LOG.log(Level.FINE, \"Next Key Marker : \" + result.nextKeyMarker);\n                LOG.log(Level.FINE, \"Next Version Id Marker : \" + result.nextVersionIdMarker);\n                keyMarker = result.nextKeyMarker;\n                versionIdMarker = result.nextVersionIdMarker;\n            } while (result.isTruncated);\n\n        } catch (Exception e) {\n            LOG.log(Level.SEVERE, e.getMessage(), e);\n        }\n    }\n\n    private static void applyToRange(String startPrefix,\n                                     Optional<String> endPrefix,\n                                     Consumer<List<S3AdminRequests.ObjectMetadataVersion>> processor,\n                                     Consumer<List<S3AdminRequests.DeleteMarker>> deleteProcessor,\n                                     S3Config config,\n                                     AtomicLong counter,\n                                     AtomicLong doneCounter,\n                                     int parallelism,\n                                     Hasher h) {\n        ForkJoinPool pool = Threads.newFJPool(parallelism, \"S3-delete-\");\n        System.out.println(\"Processing objects...\");\n        applyToAllInRange(obj -> {\n                while (pool.getQueuedSubmissionCount() > 100)\n                    try {Thread.sleep(100);} catch (InterruptedException e) {}\n                doneCounter.addAndGet(obj.size());\n                pool.submit(() -> processor.accept(obj));\n        }, del -> {\n                while (pool.getQueuedSubmissionCount() > 100)\n                    try {Thread.sleep(100);} catch (InterruptedException e) {}\n                doneCounter.addAndGet(del.size());\n                pool.submit(() -> deleteProcessor.accept(del));\n        }, startPrefix, endPrefix, config, counter, h);\n        while (! pool.isQuiescent())\n            try {Thread.sleep(100);} catch (InterruptedException e) {}\n        System.out.println(\"Objects processed: \" + doneCounter.get());\n    }\n\n    public static void delete(Pair<String, String> version, S3Config config, Hasher hasher) {\n        try {\n            PresignedUrl delUrl = S3AdminRequests.preSignDelete(version.left, Optional.ofNullable(version.right),\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), config.getHost(), config.region, config.storageClass, config.accessKey,\n                    config.secretKey, true, hasher).join();\n            HttpUtil.delete(delUrl);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void bulkDelete(List<Pair<String, String>> keyVersions, S3Config config, Hasher hasher) {\n        try {\n            S3AdminRequests.bulkDelete(keyVersions, ZonedDateTime.now(), config.getHost(), config.region, config.storageClass, config.accessKey, config.secretKey,\n                    b -> ArrayOps.bytesToHex(Hash.sha256(b)),\n                    b -> Base64.encodeBase64String(Hash.sha256(b)),\n                    (url, body) -> {\n                        try {\n                            return HttpUtil.post(url, body);\n                        } catch (IOException e) {\n                            String msg = e.getMessage();\n                            boolean rateLimited = msg.startsWith(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?><Error><Code>SlowDown</Code>\");\n                            if (rateLimited) {\n                                throw new RateLimitException();\n                            }\n                            throw new RuntimeException(e);\n                        }\n                    }, S3AdminRequests.builder::get, true, hasher);\n        } catch (Exception e) {\n            // fallback to doing deletes with parallel single calls\n            // This is necessary because B2 doesn't implement the bulk delete call!!\n            System.out.println(\"Falling back to parallel individual block deletes...\");\n            for (Pair<String, String> version : keyVersions) {\n                new Thread(() -> delete(version, config, hasher)).start();\n            }\n        }\n    }\n\n    public static void main(String[] args) {\n        Crypto crypto = JavaCrypto.init();\n        Args a = Args.parse(args);\n        S3Config config = S3Config.build(a, Optional.empty());\n\n        String startPrefix = \"\";\n        Optional<String> endPrefix = Optional.empty();\n        LocalDateTime cutoff = LocalDate.parse(a.getArg(\"delete-before-date\")).atStartOfDay();\n\n        Consumer<List<S3AdminRequests.ObjectMetadataVersion>> processor = objs -> {\n            List<Pair<String, String>> toDelete = new ArrayList<>();\n            for (S3AdminRequests.ObjectMetadataVersion m : objs) {\n                try {\n                    if (m.lastModified.isBefore(cutoff)) {\n                        System.out.println(\"Deleting \" + m.key);\n                        toDelete.add(new Pair<>(m.key, m.version));\n                        if (toDelete.size() > 1_000) {\n                            bulkDelete(toDelete, config, crypto.hasher);\n                            toDelete.clear();\n                        }\n                    }\n                } catch (Exception e) {\n                    System.err.println(e.getMessage());\n                }\n            }\n            if (! toDelete.isEmpty())\n                bulkDelete(toDelete, config, crypto.hasher);\n        };\n        Consumer<List<S3AdminRequests.DeleteMarker>> deleteProcessor = dels -> {\n            List<Pair<String, String>> toDelete = new ArrayList<>();\n            for (S3AdminRequests.DeleteMarker m : dels) {\n                try {\n                    if (m.lastModified.isBefore(cutoff)) {\n                        System.out.println(\"Deleting \" + m.key);\n                        toDelete.add(new Pair<>(m.key, m.version));\n                        if (toDelete.size() > 1_000) {\n                            bulkDelete(toDelete, config, crypto.hasher);\n                            toDelete.clear();\n                        }\n                    }\n                } catch (Exception e) {\n                    System.err.println(e.getMessage());\n                }\n            }\n            if (! toDelete.isEmpty())\n                bulkDelete(toDelete, config, crypto.hasher);\n        };\n\n        System.out.println(\"Deleting objects in S3 bucket \" + config.bucket + \" older than \" + cutoff);\n        HttpClient client = HttpClient.newBuilder()\n                .connectTimeout(Duration.ofMillis(10_000))\n                .build();\n        applyToRange(startPrefix, endPrefix, processor, deleteProcessor, config, new AtomicLong(0),\n                new AtomicLong(0), a.getInt(\"parallelism\"), Main.initCrypto().hasher);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3Exploration.java",
    "content": "package peergos.server.storage;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.bases.Base64;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.stream.*;\n\nclass S3Exploration {\n    public static void main(String[] a) throws Exception {\n        testVersionedBucket(a);\n    }\n\n    public static void testVersionedBucket(String[] a) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        String accessKey = a[0];\n        String secretKey = a[1];\n        String bucketName = a[2];\n        String region = \"us-east-1\";\n        String regionEndpoint = \"s3.\" + region + \".linodeobjects.com\";\n\n        String host = bucketName + \".\" + regionEndpoint;\n\n        byte[] payload = new byte[4096];\n        new Random(1).nextBytes(payload);\n        Hasher h = crypto.hasher;\n        RAMStorage ram = new RAMStorage(h);\n        TransactionId tid = ram.startTransaction(null).join();\n        Multihash content = ram.put(null, null, null, Collections.singletonList(payload), tid).join().get(0);\n        String s3Key = DirectS3BlockStore.hashToKey(content);// \"AFYREIBF5Y4OUJXNGRCHBAR2ZMPQBSW62SZDHFNX2GA6V4J3W7I63LA4UQ\"\n        boolean useHttps = true;\n\n        // test an authed PUT\n        Map<String, String> extraHeaders = new TreeMap<>();\n        extraHeaders.put(\"Content-Type\", \"application/octet-stream\");\n        extraHeaders.put(\"User-Agent\", \"Bond, James Bond\");\n        boolean useIllegalPayload = false;\n        boolean hashContent = true;\n        String contentHash = hashContent ? ArrayOps.bytesToHex(content.getHash()) : \"UNSIGNED-PAYLOAD\";\n        PresignedUrl putUrl = S3Request.preSignPut(s3Key, payload.length, contentHash, Optional.empty(), false,\n                S3AdminRequests.asAwsDate(ZonedDateTime.now().minusMinutes(14)), host, extraHeaders, region, accessKey, secretKey, useHttps, h).join();\n        // put same object twice to create two identical versions\n        new String(write(new URI(putUrl.base).toURL(), \"PUT\", putUrl.fields, useIllegalPayload ? new byte[payload.length] : payload));\n        new String(write(new URI(putUrl.base).toURL(), \"PUT\", putUrl.fields, useIllegalPayload ? new byte[payload.length] : payload));\n\n        // list bucket to get all files and latest versions\n        S3AdminRequests.ListObjectVersionsReply listing = S3AdminRequests.listObjectVersions(s3Key, 20, Optional.empty(),\n                Optional.empty(), ZonedDateTime.now(), host, region, Optional.empty(), accessKey, secretKey, url -> get(url),\n                S3AdminRequests.builder::get, useHttps, h);\n        System.out.println();\n\n        // do a normal delete (adds a delete marker, leaving old versions)\n        PresignedUrl delUrl = S3AdminRequests.preSignDelete(s3Key, Optional.empty(), S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, Optional.empty(), accessKey, secretKey, useHttps, h).join();\n        delete(new URI(delUrl.base).toURL(), delUrl.fields);\n\n        // check versions include delete version\n        S3AdminRequests.ListObjectVersionsReply listing2 = S3AdminRequests.listObjectVersions(s3Key, 20, Optional.empty(),\n                Optional.empty(), ZonedDateTime.now(), host, region, Optional.empty(), accessKey, secretKey, url -> get(url),\n                S3AdminRequests.builder::get, useHttps, h);\n        if (listing2.deletes.size() != listing.deletes.size() + 1)\n            throw new IllegalStateException(\"Where's delete?\");\n\n        List<Pair<String, String>> versionsToDelete = new ArrayList<>();\n        versionsToDelete.addAll(listing2.versions.stream()\n                .filter(m -> m.key.equals(s3Key))\n                .map(m -> new Pair<>(m.key, m.version))\n                .collect(Collectors.toList()));\n        versionsToDelete.addAll(listing2.deletes.stream()\n                .filter(m -> m.key.equals(s3Key))\n                .map(m -> new Pair<>(m.key, m.version))\n                .collect(Collectors.toList()));\n        // delete all versions of the key and delete markers using a bulk delete call\n        S3AdminRequests.BulkDeleteReply bulkDelete = S3AdminRequests.bulkDelete(\n                versionsToDelete, ZonedDateTime.now(), host, region, Optional.empty(), accessKey, secretKey,\n                b -> ArrayOps.bytesToHex(Hash.sha256(b)),\n                b -> Base64.encodeBase64String(Hash.sha256(b)),\n                (url, body) -> {\n                    try {\n                        System.out.println(\"URL: \" + url.base);\n                        url.fields.entrySet().forEach(e -> System.out.println(\"HEADER: \" + e.getKey() + \": \" + e.getValue()));\n                        System.out.println(\"BODY: \" + new String(body));\n                        return write(toURL(url.base), \"POST\", url.fields, body);\n                    } catch (Exception e) {\n                        throw new RuntimeException(e);\n                    }\n                }, S3AdminRequests.builder::get, useHttps, h);\n\n        S3AdminRequests.ListObjectVersionsReply afterDelete = S3AdminRequests.listObjectVersions(s3Key, 20, Optional.empty(),\n                Optional.empty(), ZonedDateTime.now(), host, region, Optional.empty(), accessKey, secretKey, url -> get(url),\n                S3AdminRequests.builder::get, useHttps, h);\n        if (afterDelete.versions.stream().anyMatch(m -> m.key.equals(s3Key)) || afterDelete.deletes.stream().anyMatch(d -> d.key.equals(s3Key)))\n            throw new IllegalStateException(\"Bulk delete failed\");\n\n        // delete all versions and delete markers of the key\n//        for (S3AdminRequests.ObjectMetadataVersion version : listing2.versions) {\n//            if (version.key.equals(s3Key)) {\n//                PresignedUrl delUrl2 = S3AdminRequests.preSignDelete(version.key, Optional.of(version.version), S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, accessKey, secretKey, useHttps, h).join();\n//                delete(new URI(delUrl2.base).toURL(), delUrl2.fields);\n//            }\n//        }\n//        for (S3AdminRequests.DeleteMarker delete : listing2.deletes) {\n//            if (delete.key.equals(s3Key)) {\n//                PresignedUrl delUrl2 = S3AdminRequests.preSignDelete(delete.key, Optional.of(delete.version), S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, accessKey, secretKey, useHttps, h).join();\n//                delete(new URI(delUrl2.base).toURL(), delUrl2.fields);\n//            }\n//        }\n//\n//        // check bucket is empty\n//        S3AdminRequests.ListObjectVersionsReply listing3 = S3AdminRequests.listObjectVersions(\"\", 20, Optional.empty(),\n//                Optional.empty(), ZonedDateTime.now(), host, region, accessKey, secretKey, url -> get(url),\n//                S3AdminRequests.builder::get, useHttps, h);\n//        if (! listing3.versions.isEmpty() || ! listing3.deletes.isEmpty())\n//            throw new IllegalStateException(\"Not all versions deleted!\");\n    }\n\n    public static void explore(String[] a) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        String accessKey = a[0];\n        String secretKey = a[1];\n        String bucketName = a[2];\n        String region = \"us-east-1\";\n        String regionEndpoint = region + \".linodeobjects.com\";\n        String host = bucketName + \".\" + regionEndpoint;\n\n        byte[] payload = new byte[4096];\n        new Random(1).nextBytes(payload);\n        Hasher h = crypto.hasher;\n        RAMStorage ram = new RAMStorage(h);\n        TransactionId tid = ram.startTransaction(null).join();\n        Multihash content = ram.put(null, null, null, Collections.singletonList(payload), tid).join().get(0);\n        String s3Key = DirectS3BlockStore.hashToKey(content);// \"AFYREIBF5Y4OUJXNGRCHBAR2ZMPQBSW62SZDHFNX2GA6V4J3W7I63LA4UQ\"\n        boolean useHttps = true;\n        {\n            // test an authed PUT\n            Map<String, String> extraHeaders = new TreeMap<>();\n            extraHeaders.put(\"Content-Type\", \"application/octet-stream\");\n            extraHeaders.put(\"User-Agent\", \"Bond, James Bond\");\n            boolean useIllegalPayload = false;\n            boolean hashContent = true;\n            String contentHash = hashContent ? ArrayOps.bytesToHex(content.getHash()) : \"UNSIGNED-PAYLOAD\";\n            PresignedUrl putUrl = S3Request.preSignPut(s3Key, payload.length, contentHash, Optional.empty(), false,\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now().minusMinutes(14)), host, extraHeaders, region, accessKey, secretKey, useHttps, h).join();\n            String putRes = new String(write(new URI(putUrl.base).toURL(), \"PUT\", putUrl.fields, useIllegalPayload ? new byte[payload.length] : payload));\n            System.out.println(putRes);\n\n            // test copying over to reset modified time\n            PresignedUrl getaUrl = S3Request.preSignGet(s3Key, Optional.of(600), Optional.of(new Pair<>(0, Bat.MAX_RAW_BLOCK_PREFIX_SIZE - 1)),\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, Optional.empty(), accessKey, secretKey, useHttps, h).join();\n            byte[] prefix = get(new URI(getaUrl.base).toURL(), getaUrl.fields);\n            Assert.assertTrue(prefix.length == Bat.MAX_RAW_BLOCK_PREFIX_SIZE);\n            String tempKey = s3Key + \"Z\";\n            {\n                PresignedUrl copyUrl = S3Request.preSignCopy(bucketName, s3Key, tempKey,\n                        S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, Optional.empty(), Collections.emptyMap(), region, accessKey, secretKey, useHttps, h).join();\n                String res = new String(write(new URI(copyUrl.base).toURL(), \"PUT\", copyUrl.fields, new byte[0]));\n                System.out.println(res);\n            }\n            {\n                PresignedUrl copyUrl = S3Request.preSignCopy(bucketName, tempKey, s3Key,\n                        S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, Optional.empty(), Collections.emptyMap(), region, accessKey, secretKey, useHttps, h).join();\n                String res = new String(write(new URI(copyUrl.base).toURL(), \"PUT\", copyUrl.fields, new byte[0]));\n                System.out.println(res);\n            }\n            get(new URI(getaUrl.base).toURL(), getaUrl.fields);\n\n            // test a delete\n            PresignedUrl delUrl = S3AdminRequests.preSignDelete(tempKey, Optional.empty(), S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, Optional.empty(), accessKey, secretKey, useHttps, h).join();\n            delete(new URI(delUrl.base).toURL(), delUrl.fields);\n            System.out.println();\n\n            // test bulk delete of two copies\n            {\n                PresignedUrl copyUrl = S3Request.preSignCopy(bucketName, s3Key, tempKey,\n                        S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, Optional.empty(), Collections.emptyMap(), region, accessKey, secretKey, useHttps, h).join();\n                String res = new String(write(new URI(copyUrl.base).toURL(), \"PUT\", copyUrl.fields, new byte[0]));\n            }\n            String tempKey2 = s3Key + \"ZZ\";\n            {\n                PresignedUrl copyUrl = S3Request.preSignCopy(bucketName, s3Key, tempKey2,\n                        S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, Optional.empty(), Collections.emptyMap(), region, accessKey, secretKey, useHttps, h).join();\n                String res = new String(write(new URI(copyUrl.base).toURL(), \"PUT\", copyUrl.fields, new byte[0]));\n                System.out.println(res);\n            }\n            String nonExistentKey = tempKey2 + \"ZZ\";\n            S3AdminRequests.BulkDeleteReply bulkDelete = S3AdminRequests.bulkDelete(\n                    Arrays.asList(new Pair<>(tempKey, null), new Pair<>(tempKey2, null), new Pair<>(nonExistentKey, null)),\n                    ZonedDateTime.now(), host, region, Optional.empty(), accessKey, secretKey,\n                    b -> ArrayOps.bytesToHex(Hash.sha256(b)),\n                    b -> Base64.encodeBase64String(Hash.sha256(b)),\n                    (url, body) -> {\n                        try {\n                            System.out.println(\"URL: \" + url.base);\n                            url.fields.entrySet().forEach(e -> System.out.println(\"HEADER: \" + e.getKey() + \": \" + e.getValue()));\n                            System.out.println(\"BODY: \" + new String(body));\n                            return write(toURL(url.base), \"POST\", url.fields, body);\n                        } catch (Exception e) {\n                            throw new RuntimeException(e);\n                        }\n                    }, S3AdminRequests.builder::get, useHttps, h);\n            if (! bulkDelete.deletedKeys.containsAll(Arrays.asList(tempKey, tempKey2)))\n                throw new IllegalStateException(\"Delete failed\");\n        }\n\n        // Test a list objects GET\n        S3AdminRequests.ListObjectsReply listing = S3AdminRequests.listObjects(\"\", 10, Optional.empty(),\n                ZonedDateTime.now(), host, region, Optional.empty(), accessKey, secretKey, url -> get(url), S3AdminRequests.builder::get, useHttps, h);\n\n        // Test a list objects GET continuation\n        S3AdminRequests.ListObjectsReply listing2 = S3AdminRequests.listObjects(\"\", 10, Optional.of(s3Key),\n                ZonedDateTime.now(), host, region, Optional.empty(), accessKey, secretKey, url -> get(url), S3AdminRequests.builder::get, useHttps, h);\n        if (listing2.objects.get(0).key.equals(listing.objects.get(0).key))\n            throw new IllegalStateException(\"Incorrect listing!\");\n\n        // test an authed HEAD\n        PresignedUrl headUrl = S3Request.preSignHead(s3Key, Optional.of(600), S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, Optional.empty(), accessKey, secretKey, useHttps, h).join();\n        Map<String, List<String>> headRes = HttpUtil.head(headUrl);\n        int size = Integer.parseInt(headRes.get(\"Content-Length\").get(0));\n        if (size != payload.length)\n            throw new IllegalStateException(\"Incorrect size: \" + size);\n\n        // test an authed read\n        PresignedUrl getUrl = S3Request.preSignGet(s3Key, Optional.of(600), Optional.empty(), S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, region, Optional.empty(), accessKey, secretKey, useHttps, h).join();\n        byte[] authReadBytes = get(new URI(getUrl.base).toURL(), getUrl.fields);\n        if (! Arrays.equals(authReadBytes, payload))\n            throw new IllegalStateException(\"Incorrect contents: \" + new String(authReadBytes));\n\n        // test an authed read which has expired\n        PresignedUrl failGetUrl = S3Request.preSignGet(s3Key, Optional.of(600), Optional.empty(), S3AdminRequests.asAwsDate(ZonedDateTime.now().minusMinutes(11)), host, region, Optional.empty(), accessKey, secretKey, useHttps, h).join();\n        String failReadRes = new String(get(new URI(failGetUrl.base).toURL(), failGetUrl.fields));\n        System.out.println(failReadRes);\n\n        // test a public read\n        String webUrl = \"https://\" + bucketName + \".website-\" + regionEndpoint + \"/\" + s3Key;\n        byte[] getResult = get(new URI(webUrl).toURL(), Collections.emptyMap());\n        if (! Arrays.equals(getResult, payload))\n            System.out.println(\"Incorrect contents!\");\n    }\n\n    private static URL toURL(String url) {\n        try {\n            return new URI(url).toURL();\n        } catch (URISyntaxException | MalformedURLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static byte[] write(URL target, String method, Map<String, String> headers, byte[] body) throws Exception {\n        HttpURLConnection conn = (HttpURLConnection) target.openConnection();\n        conn.setRequestMethod(method);\n        for (Map.Entry<String, String> e : headers.entrySet()) {\n            conn.setRequestProperty(e.getKey(), e.getValue());\n        }\n        conn.setDoOutput(true);\n        OutputStream out = conn.getOutputStream();\n        out.write(body);\n        out.flush();\n        out.close();\n\n        try {\n            InputStream in = conn.getInputStream();\n            ByteArrayOutputStream resp = new ByteArrayOutputStream();\n            byte[] buf = new byte[4096];\n            int r;\n            while ((r = in.read(buf)) >= 0)\n                resp.write(buf, 0, r);\n            return resp.toByteArray();\n        } catch (IOException e) {\n            InputStream err = conn.getErrorStream();\n            ByteArrayOutputStream resp = new ByteArrayOutputStream();\n            byte[] buf = new byte[4096];\n            int r;\n            while ((r = err.read(buf)) >= 0)\n                resp.write(buf, 0, r);\n            throw new IOException(\"HTTP \" + conn.getResponseCode() + \": \" + conn.getResponseMessage() + \"\\nbody:\\n\" + new String(resp.toByteArray()));\n        }\n    }\n\n    private static byte[] get(PresignedUrl url) {\n        try {\n            return get(new URI(url.base).toURL(), url.fields);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static byte[] get(URL target, Map<String, String> headers) throws Exception {\n        HttpURLConnection conn = (HttpURLConnection) target.openConnection();\n        conn.setRequestMethod(\"GET\");\n        for (Map.Entry<String, String> e : headers.entrySet()) {\n            conn.setRequestProperty(e.getKey(), e.getValue());\n        }\n\n        try {\n            InputStream in = conn.getInputStream();\n            ByteArrayOutputStream resp = new ByteArrayOutputStream();\n            byte[] buf = new byte[4096];\n            int r;\n            while ((r = in.read(buf)) >= 0)\n                resp.write(buf, 0, r);\n            return resp.toByteArray();\n        } catch (IOException e) {\n            InputStream err = conn.getErrorStream();\n            ByteArrayOutputStream resp = new ByteArrayOutputStream();\n            byte[] buf = new byte[4096];\n            int r;\n            while ((r = err.read(buf)) >= 0)\n                resp.write(buf, 0, r);\n            return resp.toByteArray();\n        }\n    }\n\n    private static void delete(URL target, Map<String, String> headers) throws Exception {\n        HttpURLConnection conn = (HttpURLConnection) target.openConnection();\n        conn.setRequestMethod(\"DELETE\");\n        for (Map.Entry<String, String> e : headers.entrySet()) {\n            conn.setRequestProperty(e.getKey(), e.getValue());\n        }\n\n        try {\n            int code = conn.getResponseCode();\n            if (code == 204)\n                return;\n            InputStream in = conn.getInputStream();\n            ByteArrayOutputStream resp = new ByteArrayOutputStream();\n            byte[] buf = new byte[4096];\n            int r;\n            while ((r = in.read(buf)) >= 0)\n                resp.write(buf, 0, r);\n            throw new IllegalStateException(\"HTTP \" + code + \"-\" + new String(resp.toByteArray()));\n        } catch (IOException e) {\n            InputStream err = conn.getErrorStream();\n            ByteArrayOutputStream resp = new ByteArrayOutputStream();\n            byte[] buf = new byte[4096];\n            int r;\n            while ((r = err.read(buf)) >= 0)\n                resp.write(buf, 0, r);\n            throw new IllegalStateException(new String(resp.toByteArray()), e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/S3HasBlock.java",
    "content": "package peergos.server.storage;\n\nimport com.webauthn4j.data.client.Origin;\nimport peergos.server.Builder;\nimport peergos.server.Main;\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.corenode.UserRepository;\nimport peergos.server.login.AccountWithStorage;\nimport peergos.server.login.JdbcAccount;\nimport peergos.server.space.JdbcUsageStore;\nimport peergos.server.space.UsageStore;\nimport peergos.server.sql.PostgresCommands;\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.sql.SqliteCommands;\nimport peergos.server.storage.admin.QuotaAdmin;\nimport peergos.server.storage.auth.BlockRequestAuthoriser;\nimport peergos.server.storage.auth.JdbcBatCave;\nimport peergos.server.util.Args;\nimport peergos.server.util.JavaPoster;\nimport peergos.server.util.Logging;\nimport peergos.shared.Crypto;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.HttpMutablePointers;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.mutable.MutablePointersProxy;\nimport peergos.shared.storage.BlockStoreProperties;\nimport peergos.shared.storage.ContentAddressedStorageProxy;\nimport peergos.shared.storage.RamBlockCache;\nimport peergos.shared.user.Account;\nimport peergos.shared.util.Futures;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.sql.Connection;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class S3HasBlock {\n    public static void main(String[] args) throws IOException {\n        Args a = Args.parse(args);\n        Logging.init(a.with(\"log-to-console\", \"true\"));\n        Crypto crypto = Main.initCrypto();\n        Hasher hasher = crypto.hasher;\n        S3Config config = S3Config.build(a, Optional.empty());\n        boolean versioned = a.getBoolean(\"s3.versioned-bucket\");\n        boolean usePostgres = a.getBoolean(\"use-postgres\", false);\n        SqlSupplier sqlCommands = usePostgres ?\n                new PostgresCommands() :\n                new SqliteCommands();\n        Supplier<Connection> database = Main.getDBConnector(a, \"mutable-pointers-file\");\n        Supplier<Connection> transactionsDb = Main.getDBConnector(a, \"transactions-sql-file\");\n        TransactionStore transactions = JdbcTransactionStore.build(transactionsDb, sqlCommands);\n        BlockRequestAuthoriser authoriser = (c, b, s, auth) -> Futures.of(true);\n        BlockMetadataStore meta = Builder.buildBlockMetadata(a);\n        Supplier<Connection> usageDb = Main.getDBConnector(a, \"space-usage-sql-file\");\n        UsageStore usage = new JdbcUsageStore(usageDb, sqlCommands);\n        Supplier<Connection> statusDb = Main.getDBConnector(a, \"partition-status-file\");\n        PartitionStatus partitioned = new JdbcPartitionStatus(statusDb, sqlCommands);\n        JavaPoster p2pHttpProxy = Builder.buildP2pHttpProxy(a);\n        ContentAddressedStorageProxy p2pHttpFallback = new ContentAddressedStorageProxy.HTTP(p2pHttpProxy);\n        p2pHttpFallback = Builder.buildP2PBlockRetrieverForS3(a, usage, hasher, p2pHttpFallback);\n        List<Cid> ids = List.of(Cid.decode(a.getArg(\"ipfs.id\")));\n        S3BlockStorage s3 = new S3BlockStorage(config, ids,\n                BlockStoreProperties.empty(), \"localhost:8000\", transactions, authoriser, null, meta, usage,\n                new RamBlockCache(1024, 100),\n                new FileBlockBuffer(a.fromPeergosDir(\"s3-block-buffer-dir\", \"block-buffer\"), usage),\n                Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,\n                versioned, a.getPeergosDir(), partitioned, hasher,\n                new RAMStorage(hasher), p2pHttpFallback);\n        JdbcIpnsAndSocial rawPointers = new JdbcIpnsAndSocial(database, sqlCommands);\n\n        MutablePointers localPointers = UserRepository.build(s3, rawPointers, hasher);\n        Multihash pkiServerNodeId = Builder.getPkiServerId(a);\n        MutablePointersProxy proxingMutable = new HttpMutablePointers(p2pHttpProxy, pkiServerNodeId);\n        LinkRetrievalCounter linkCounts = new JdbcLinkRetrievalcounter(Main.getDBConnector(a, \"link-counts-sql-file\", database), sqlCommands);\n        JdbcIpnsAndSocial rawSocial = new JdbcIpnsAndSocial(Builder.getDBConnector(a, \"social-sql-file\", database), sqlCommands);\n        String listeningHost = a.getArg(Main.LISTEN_HOST.name, \"localhost\");\n        int webPort = a.getInt(\"port\");\n        Optional<String> tlsHostname = a.hasArg(\"tls.keyfile.password\") ? Optional.of(listeningHost) : Optional.empty();\n        Optional<String> publicHostname = tlsHostname.isPresent() ? tlsHostname : a.getOptionalArg(\"public-domain\");\n        Origin origin = new Origin(publicHostname.map(host -> (Main.isLanIP(host) ? \"http://\" : \"https://\") + host).orElse(\"http://localhost:\" + webPort));\n        String rpId = publicHostname.orElse(\"localhost\");\n        JdbcAccount rawAccount = new JdbcAccount(Builder.getDBConnector(a, \"account-sql-file\", database), sqlCommands, origin, rpId);\n        Account account = new AccountWithStorage(s3, localPointers, rawAccount);\n        boolean isPki = false;\n        InetSocketAddress userAPIAddress = new InetSocketAddress(listeningHost, webPort);\n        boolean localhostApi = userAPIAddress.getHostName().equals(\"localhost\");\n        QuotaAdmin userQuotas = Main.buildSpaceQuotas(a, s3,\n                Main.getDBConnector(a, \"space-requests-sql-file\", database),\n                Main.getDBConnector(a, \"quotas-sql-file\", database), isPki, localhostApi);\n        JdbcBatCave batStore = new JdbcBatCave(Main.getDBConnector(a, \"bat-store\", database), sqlCommands);\n\n        CoreNode core = Builder.buildCorenode(a, s3, transactions, rawPointers, localPointers, proxingMutable,\n                rawSocial, usage, userQuotas, rawAccount, batStore, account, linkCounts, crypto);\n\n        s3.setPki(core);\n        String username = a.getArg(\"username\");\n        PublicKeyHash owner = core.getPublicKeyHash(username).join().get();\n        Cid hash = Cid.decode(a.getArg(\"hash\"));\n\n        Optional<BlockMetadata> blockMetadata = meta.get(hash);\n        System.out.println(\"Block present in metadb \" + owner + \": \" + hash + \" \" + blockMetadata.isPresent());\n        System.out.println(\"Stored owner \" + meta.getOwner(hash));\n        System.out.println(\"Block present \" + owner + \": \" + hash + \" \" + s3.hasBlock(owner, hash));\n        List<Multihash> peerIds = ids.stream()\n                .map(c -> (Multihash) c)\n                .collect(Collectors.toList());\n        boolean useOwner = a.getBoolean(\"use-owner\");\n        Optional<byte[]> block = s3.getRaw(peerIds, useOwner ? owner : null, hash, Optional.empty(), ids.get(0), hasher, false, false).join();\n        Files.write(Paths.get(hash + \".data\"), block.get());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/SecretLinkStorage.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.storage.admin.QuotaAdmin;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.logging.*;\n\npublic class SecretLinkStorage extends DelegatingDeletableStorage {\n\n    private static final Logger LOG = Logger.getGlobal();\n\n    private final DeletableContentAddressedStorage target;\n    private final MutablePointers pointers;\n    private final Hasher hasher;\n    private final LinkRetrievalCounter counter;\n    private final BatCave batstore;\n    private final boolean allowNonLocalLinks;\n    private final QuotaAdmin quota;\n    private CoreNode pki;\n\n    public SecretLinkStorage(DeletableContentAddressedStorage target,\n                             MutablePointers pointers,\n                             LinkRetrievalCounter counter,\n                             boolean allowNonLocalLinks,\n                             QuotaAdmin quota,\n                             BatCave batStore,\n                             Hasher hasher) {\n        super(target);\n        this.target = target;\n        this.pointers = pointers;\n        this.hasher = hasher;\n        this.counter = counter;\n        this.allowNonLocalLinks = allowNonLocalLinks;\n        this.quota = quota;\n        this.batstore = batStore;\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n        this.pki = pki;\n        target.setPki(pki);\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        PublicKeyHash owner = link.owner;\n        String username = pki.getUsername(owner).join();\n\n        WriterData wd = WriterData.getWriterData(owner, owner, pointers, target).join().props.get();\n        if (wd.secretLinks.isEmpty())\n            throw new IllegalStateException(\"No secret link published!\");\n        List<BatWithId> mirrorBats = batstore.getUserBats(username, new byte[0]).join();\n        Optional<BatWithId> mirrorBat = mirrorBats.isEmpty() ? Optional.empty() : Optional.of(mirrorBats.get(mirrorBats.size() - 1));\n        SecretLinkChamp champ = SecretLinkChamp.build(owner, (Cid) wd.secretLinks.get(), mirrorBat, this, hasher).join();\n        Optional<SecretLinkTarget> res = champ.get(owner, link.label).join();\n        if (res.isEmpty())\n            throw new IllegalStateException(\"No secret link present!\");\n        SecretLinkTarget target = res.get();\n        if (target.expiry.isPresent()) {\n            LocalDateTime now = LocalDateTime.now();\n            if (target.expiry.get().isBefore(now)) {\n                LOG.info(\"Expired secret link: \" + owner + \"-\" + link.label + \" \" + target.expiry.get() + \" < \" + now);\n                throw new IllegalStateException(\"Secret link expired!\");\n            }\n        }\n\n        if (target.maxRetrievals.isPresent()) {\n            long retrievals = counter.getCount(username, link.label);\n            if (retrievals >= target.maxRetrievals.get()) {\n                LOG.info(\"Unavailable secret link: \" + owner + \"-\" + link.label + \" \" + target.maxRetrievals.get() + \" >= \" + retrievals);\n                throw new IllegalStateException(\"Maximum link retrievals exceed!\");\n            }\n        }\n        counter.increment(username, link.label);\n        return Futures.of(target.cap);\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner,\n                                                       LocalDateTime after,\n                                                       BatWithId mirrorBat) {\n        byte[] supplied = hasher.sha256(mirrorBat.serialize()).join();\n        List<BatWithId> mirrorBats = batstore.getUserBats(owner, new byte[0]).join();\n        byte[] expected = hasher.sha256(mirrorBats.get(mirrorBats.size() - 1).serialize()).join();\n        if (! Arrays.equals(expected, supplied))\n            throw new IllegalStateException(\"Unauthorized!\");\n        return Futures.of(counter.getUpdatedCounts(owner, after));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/ServerIdentityStore.java",
    "content": "package peergos.server.storage;\n\nimport io.libp2p.core.*;\nimport io.libp2p.core.crypto.*;\n\nimport java.util.*;\n\npublic interface ServerIdentityStore {\n\n    List<PeerId> getIdentities();\n\n    void addIdentity(PeerId id, byte[] signedIpnsRecord);\n\n    void setPrivateKey(PrivKey privateKey);\n\n    byte[] getPrivateKey(PeerId peerId);\n\n    byte[] getRecord(PeerId peerId);\n\n    void setRecord(PeerId peerId, byte[] newRecord);\n}\n"
  },
  {
    "path": "src/peergos/server/storage/SqliteBlockList.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.sql.SqliteCommands;\nimport peergos.server.util.Logging;\nimport peergos.server.util.Sqlite;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.util.ArrayOps;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\n\npublic class SqliteBlockList {\n    private static final Logger LOG = Logging.LOG();\n    private static final String CREATE_TABLE = \"CREATE TABLE IF NOT EXISTS blocks (\" +\n            \"username string, \" +\n            \"hash bytes primary key not null, \" +\n            \"version text); \" +\n                \"CREATE UNIQUE INDEX IF NOT EXISTS hash_reachable_index ON blocks (hash, version);\";\n\n    private static final String INSERT_SUFFIX = \"INTO blocks (username, hash, version) VALUES(?, ?, ?)\";\n    private static final String APPLY_TO_ALL_VERSIONS = \"SELECT hash, version FROM blocks\";\n    private static final String HAS_BLOCK = \"SELECT hash, version FROM blocks WHERE hash=? AND username=?;\";\n    private static final String HAS_LEGACY_BLOCK = \"SELECT hash, version FROM blocks WHERE hash=? AND username IS NULL;\";\n    private static final String GET_VERSIONS = \"SELECT version FROM blocks WHERE hash=? AND username=?;\";\n    private static final String GET_LEGACY_VERSIONS = \"SELECT version FROM blocks WHERE hash=? AND username IS NULL;\";\n    private static final String COUNT = \"SELECT COUNT(*) FROM blocks\";\n\n    private final Supplier<Connection> conn;\n    private final SqlSupplier cmds;\n    public SqliteBlockList(Supplier<Connection> conn, SqlSupplier cmds) {\n        this.conn = conn;\n        this.cmds = cmds;\n        init(cmds);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private Connection getNonCommittingConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(false);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        try (Connection conn = getConnection()) {\n            commands.createTable(CREATE_TABLE, conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public synchronized void addBlocks(List<UserBlockVersion> versions) {\n        if (versions.isEmpty())\n            return;\n        try (Connection conn = getNonCommittingConnection();\n             PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", INSERT_SUFFIX));\n        ) {\n            List<UserBlockVersion> distinct = versions.stream()\n                    .distinct()\n                    .collect(Collectors.toList());\n\n            for (UserBlockVersion version : distinct) {\n                insert.setString(1, version.username);\n                insert.setBytes(2, version.cid.toBytes());\n                insert.setString(3, version.version);\n                insert.addBatch();\n            }\n            int[] inserted = insert.executeBatch();\n            conn.commit();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    public synchronized boolean hasBlock(String username, Cid block) {\n        if (username == null) {\n            try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(HAS_LEGACY_BLOCK)) {\n                stmt.setBytes(1, block.toBytes());\n                ResultSet rs = stmt.executeQuery();\n                return rs.next();\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                throw new RuntimeException(sqe);\n            }\n        }\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(HAS_BLOCK)) {\n            stmt.setBytes(1, block.toBytes());\n            stmt.setString(2, username);\n            ResultSet rs = stmt.executeQuery();\n            return rs.next();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized List<String> getVersions(String username, Cid block) {\n        if (username == null) {\n            try (Connection conn = getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(GET_LEGACY_VERSIONS)) {\n                stmt.setBytes(1, block.toBytes());\n                ResultSet rs = stmt.executeQuery();\n                List<String> res = new ArrayList<>();\n                while (rs.next()) {\n                    res.add(rs.getString(1));\n                }\n                return res;\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n                throw new RuntimeException(sqe);\n            }\n        }\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_VERSIONS)) {\n            stmt.setBytes(1, block.toBytes());\n            stmt.setString(2, username);\n            ResultSet rs = stmt.executeQuery();\n            List<String> res = new ArrayList<>();\n            while (rs.next()) {\n                res.add(rs.getString(1));\n            }\n            return res;\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void applyToAllVersions(Consumer<List<BlockVersion>> out) {\n        try (Connection conn = getConnection();\n             PreparedStatement update = conn.prepareStatement(APPLY_TO_ALL_VERSIONS)) {\n            ResultSet res = update.executeQuery();\n            ArrayList<BlockVersion> buf = new ArrayList<>();\n            while (res.next()) {\n                buf.add(new BlockVersion(Cid.cast(res.getBytes(1)), res.getString(2), false));\n                if (buf.size() == 1000) {\n                    out.accept(buf);\n                    buf.clear();\n                }\n            }\n            out.accept(buf);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized long size() {\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(COUNT)) {\n            ResultSet res = query.executeQuery();\n            res.next();\n            return res.getLong(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public static SqliteBlockList createBlockListDb(Path dbFile) {\n        try {\n            Connection file = Sqlite.build(dbFile.toString());\n            // We need a connection that ignores close\n            Connection instance = new Sqlite.UncloseableConnection(file);\n            return new SqliteBlockList(() -> instance, new SqliteCommands());\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void main(String[] a) throws IOException {\n        // This is a benchmark to test baseline speed of a blockstore partition\n        String filename = System.nanoTime() + \"temp.sql\";\n        Path file = Path.of(filename);\n        SqliteBlockList blocksDb = createBlockListDb(file);\n        List<UserBlockVersion> versions = new ArrayList<>();\n        int count = 1_000_000;\n        boolean versioned = true;\n        Random rnd = new Random(28);\n\n        for (int i = 0; i < count; i++) {\n            byte[] hash = new byte[32];\n            rnd.nextBytes(hash);\n            Cid cid = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n            if (versioned)\n                versions.add(new UserBlockVersion(null, cid, ArrayOps.bytesToHex(hash), true));\n            else\n                versions.add(new UserBlockVersion(null, cid, null, true));\n        }\n        System.out.println(\"Starting Db load...\");\n        long t0 = System.nanoTime();\n        int batchSize = 10_000;\n        for (int i = 0; i < count / batchSize; i++) {\n            blocksDb.addBlocks(versions.subList(i * batchSize, (i+1)* batchSize));\n            long size = blocksDb.size();\n            if (size != (i +1) * batchSize)\n                throw new IllegalStateException(\"Incorrect size: \" + size + \", expected \" + (i+1)*batchSize);\n        }\n        long t1 = System.nanoTime();\n        System.out.println(\"Load duration \" + (t1-t0)/1_000_000_000 + \"s, batch size = \" + batchSize);\n\n        long size = blocksDb.size();\n        if (size != count)\n            throw new IllegalStateException(\"Missing rows! \" + size + \", expected \" + count);\n\n        // put the same block version in multiple times (should be idempotent)\n        long priorSize = blocksDb.size();\n        byte[] hash1 = new byte[32];\n        rnd.nextBytes(hash1);\n        Cid cid1 = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash1);\n        UserBlockVersion v1 = new UserBlockVersion(null, cid1, ArrayOps.bytesToHex(hash1), true);\n        blocksDb.addBlocks(Arrays.asList(v1, v1, v1, v1, v1, v1, v1, v1, v1, v1));\n\n        try {\n            blocksDb.addBlocks(Arrays.asList(v1));\n        } catch (Exception e) {}\n        long with1Block = blocksDb.size();\n        if (with1Block != priorSize + 1)\n            throw new IllegalStateException(\"Adding not idempotent!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/SqliteBlockReachability.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.util.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class SqliteBlockReachability {\n    private static final Logger LOG = peergos.server.util.Logging.LOG();\n    private static final String CREATE_TABLE = \"CREATE TABLE IF NOT EXISTS reachability (\" +\n            \"idx integer primary key,\"+\n            \"hash bytes not null, \" +\n            \"version text, \" +\n            \"latest boolean not null,\" +\n            \"reachable boolean not null); \" +\n                \"CREATE UNIQUE INDEX IF NOT EXISTS hash_reachable_index ON reachability (hash, version);\" +\n            \"CREATE TABLE IF NOT EXISTS links (\" +\n            \"parent integer references reachability(idx) not null,\" +\n            \"child integer references reachability(idx) not null\" +\n            \");\" +\n            \"CREATE UNIQUE INDEX IF NOT EXISTS links_index ON links (parent, child);\" +\n            \"CREATE TABLE IF NOT EXISTS emptylinks (\" +\n            \"parent integer references reachability(idx) not null primary key\" +\n            \");\";\n\n    private static final String CLEAR_REACHABLE = \"UPDATE reachability SET reachable=false\";\n    private static final String SET_REACHABLE = \"UPDATE reachability SET reachable=true WHERE hash = ? AND latest = true\";\n    private static final String INSERT_SUFFIX = \"INTO reachability (hash, version, latest, reachable) VALUES(?, ?, ?, false)\";\n    private static final String NOT_LATEST = \"update reachability set latest=false WHERE hash=? AND version!=?\";\n    private static final String INSERT_LINK_SUFFIX = \"INTO links (parent, child) VALUES(?, ?)\";\n    private static final String INSERT_EMPTY_LINKS_SUFFIX = \"INTO emptylinks (parent) VALUES(?)\";\n    private static final String UNREACHABLE = \"SELECT hash, version FROM reachability WHERE reachable = false\";\n    private static final String APPLY_TO_ALL_VERSIONS = \"SELECT hash, version FROM reachability\";\n    private static final String COUNT = \"SELECT COUNT(*) FROM reachability\";\n    private static final String BLOCK_INDEX = \"SELECT idx FROM reachability WHERE hash=? AND latest=true\";\n    private static final String BLOCK_VERSION_INDEX = \"SELECT idx FROM reachability WHERE hash=? AND VERSION=?\";\n    private static final String OLD_BLOCK_INDEX = \"SELECT idx FROM reachability WHERE hash=? AND latest=false\";\n    private static final String BLOCK_BY_INDEX = \"SELECT hash FROM reachability WHERE idx=?\";\n    private static final String LINKS = \"SELECT child FROM links WHERE parent=?\";\n    private static final String UPDATE_LINK_PARENTS = \"UPDATE links SET parent=? WHERE parent=?\";\n    private static final String UPDATE_LINK_KIDS = \"UPDATE links SET child=? WHERE child=?\";\n    private static final String UPDATE_OLD_EMPTY_LINKS = \"UPDATE emptylinks SET parent=? WHERE parent=?\";\n    private static final String DELETE_LINKS = \"DELETE FROM links WHERE parent=?\";\n    private static final String DELETE_EMPTY_LINKS = \"DELETE FROM emptylinks WHERE parent=?\";\n    private static final String DELETE_BLOCK = \"DELETE FROM reachability WHERE hash=? AND version=?\";\n    private static final String EMPTY_LINKS = \"SELECT COUNT(*) FROM emptylinks WHERE parent=?\";\n\n    private final Supplier<Connection> conn;\n    private final SqlSupplier cmds;\n    public SqliteBlockReachability(Supplier<Connection> conn, SqlSupplier cmds) {\n        this.conn = conn;\n        this.cmds = cmds;\n        init(cmds);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private Connection getNonCommittingConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(false);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        try (Connection conn = getConnection()) {\n            commands.createTable(CREATE_TABLE, conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public synchronized void addBlocks(List<BlockVersion> versions) {\n        if (versions.isEmpty())\n            return;\n        try (Connection conn = getNonCommittingConnection();\n             PreparedStatement oldlatest = conn.prepareStatement(NOT_LATEST);\n             PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", INSERT_SUFFIX));\n             PreparedStatement linkParents = conn.prepareStatement(UPDATE_LINK_PARENTS);\n             PreparedStatement linkKids = conn.prepareStatement(UPDATE_LINK_KIDS);\n             PreparedStatement oldBlockIndices = conn.prepareStatement(OLD_BLOCK_INDEX);\n             PreparedStatement blockIndex = conn.prepareStatement(BLOCK_INDEX);\n             PreparedStatement updateOldEmptyLinkIndices = conn.prepareStatement(UPDATE_OLD_EMPTY_LINKS);\n        ) {\n            List<BlockVersion> distinct = versions.stream()\n                    .distinct()\n                    .collect(Collectors.toList());\n\n            for (BlockVersion version : distinct) {\n                insert.setBytes(1, version.cid.toBytes());\n                insert.setString(2, version.version);\n                insert.setBoolean(3, version.isLatest);\n                insert.addBatch();\n            }\n            int[] inserted = insert.executeBatch();\n            conn.commit();\n            List<BlockVersion> newVersions = new ArrayList<>();\n            for (int i=0; i < inserted.length; i++)\n                if (inserted[i] == 1)\n                    newVersions.add(distinct.get(i));\n            Set<BlockVersion> newLatestVersions = newVersions.stream()\n                    .filter(v -> v.isLatest && v.version != null)\n                    .collect(Collectors.toSet());\n            if (! newLatestVersions.isEmpty()) {\n                for (BlockVersion latest : newLatestVersions) {\n                    oldlatest.setBytes(1, latest.cid.toBytes());\n                    oldlatest.setString(2, latest.version);\n                    oldlatest.addBatch();\n                }\n                int[] changed = oldlatest.executeBatch();\n                conn.commit();\n            }\n            for (BlockVersion latest : newLatestVersions) {\n                blockIndex.setBytes(1, latest.cid.toBytes());\n                ResultSet latestIdRes = blockIndex.executeQuery();\n                if (!latestIdRes.next())\n                    throw new IllegalStateException(\"Latest block not present: \" + latest.cid);\n                long latestId = latestIdRes.getLong(1);\n\n                oldBlockIndices.setBytes(1, latest.cid.toBytes());\n                ResultSet res = oldBlockIndices.executeQuery();\n                Set<Long> oldVersionIndices = new HashSet<>();\n                while (res.next())\n                    oldVersionIndices.add(res.getLong(1));\n\n                if (!oldVersionIndices.isEmpty()) {\n                    // update all links that use old indices\n                    for (long old : oldVersionIndices) {\n                        linkParents.setLong(1, latestId);\n                        linkParents.setLong(2, old);\n                        linkParents.addBatch();\n                    }\n                    int[] changedLinkParents = linkParents.executeBatch();\n                    conn.commit();\n                    for (long old : oldVersionIndices) {\n                        linkKids.setLong(1, latestId);\n                        linkKids.setLong(2, old);\n                        linkKids.addBatch();\n                    }\n                    int[] changedLinkKids = linkKids.executeBatch();\n                    conn.commit();\n                    for (long old : oldVersionIndices) {\n                        updateOldEmptyLinkIndices.setLong(1, latestId);\n                        updateOldEmptyLinkIndices.setLong(2, old);\n                        updateOldEmptyLinkIndices.addBatch();\n                    }\n                    int[] changedEmptyLinks = updateOldEmptyLinkIndices.executeBatch();\n                    conn.commit();\n                }\n            }\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    public synchronized void applyToAllVersions(Consumer<List<BlockVersion>> out) {\n        try (Connection conn = getConnection();\n             PreparedStatement update = conn.prepareStatement(APPLY_TO_ALL_VERSIONS)) {\n            ResultSet res = update.executeQuery();\n            ArrayList<BlockVersion> buf = new ArrayList<>();\n            while (res.next()) {\n                buf.add(new BlockVersion(Cid.cast(res.getBytes(1)), res.getString(2), false));\n                if (buf.size() == 1000) {\n                    out.accept(buf);\n                    buf.clear();\n                }\n            }\n            out.accept(buf);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void clearReachable() {\n        try (Connection conn = getConnection();\n             PreparedStatement update = conn.prepareStatement(CLEAR_REACHABLE)) {\n            update.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void setReachable(List<Cid> blocks, AtomicLong totalReachable) {\n        if (blocks.isEmpty())\n            return;\n        try (Connection conn = getNonCommittingConnection();\n             PreparedStatement update = conn.prepareStatement(SET_REACHABLE)) {\n            for (Cid block : blocks) {\n                update.setBytes(1, block.toBytes());\n                update.addBatch();\n            }\n            int[] res = update.executeBatch();\n            int changed = IntStream.of(res).sum();\n            totalReachable.addAndGet(changed);\n            if (changed > 0)\n                conn.commit();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized long size() {\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(COUNT)) {\n            ResultSet res = query.executeQuery();\n            res.next();\n            return res.getLong(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void getUnreachable(Consumer<List<BlockVersion>> toDelete) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(UNREACHABLE)) {\n            ResultSet res = select.executeQuery();\n            int batchSize = 1000;\n            List<BlockVersion> tmp = new ArrayList<>(batchSize);\n            while (res.next()) {\n                tmp.add(new BlockVersion(Cid.cast(res.getBytes(1)), res.getString(2), false));\n                if (tmp.size() % batchSize == 0) {\n                    toDelete.accept(tmp);\n                    tmp = new ArrayList<>(batchSize);\n                }\n            }\n            if (! tmp.isEmpty())\n                toDelete.accept(tmp);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    public void compact() {\n        String vacuum = cmds.vacuumCommand();\n        if (vacuum.isEmpty())\n            return;\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(vacuum)) {\n            stmt.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private long getBlockIndex(Cid block) {\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(BLOCK_INDEX)) {\n            query.setBytes(1, block.toBytes());\n            ResultSet res = query.executeQuery();\n            if (!res.next())\n                throw new IllegalStateException(\"Block not present: \" + block);\n            return res.getLong(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private long getBlockVersionIndex(BlockVersion block) {\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(BLOCK_VERSION_INDEX)) {\n            query.setBytes(1, block.cid.toBytes());\n            query.setString(2, block.version);\n            ResultSet res = query.executeQuery();\n            if (!res.next())\n                throw new IllegalStateException(\"Block version not present: \" + block);\n            return res.getLong(1);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    private Cid getBlock(long index) {\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(BLOCK_BY_INDEX)) {\n            query.setLong(1, index);\n            ResultSet res = query.executeQuery();\n            if (! res.next())\n                throw new IllegalStateException(\"Could get block for index \" + index);\n            return Cid.cast(res.getBytes(1));\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void setLinks(Cid block, List<Cid> links) {\n        long parentIndex = getBlockIndex(block);\n        if (links.isEmpty()) {\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", INSERT_EMPTY_LINKS_SUFFIX))) {\n                insert.setLong(1, parentIndex);\n                int updated = insert.executeUpdate();\n                if (updated != 1)\n                    throw new IllegalStateException(\"Couldn't insert links!\");\n            } catch (SQLException sqe) {\n                LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            }\n            return;\n        }\n        List<Long> linkIndices = links.stream()\n                .map(this::getBlockIndex)\n                .toList();\n        try (Connection conn = getNonCommittingConnection();\n             PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", INSERT_LINK_SUFFIX))) {\n            for (Long linkIndex : linkIndices) {\n                insert.setLong(1, parentIndex);\n                insert.setLong(2, linkIndex);\n                insert.addBatch();\n            }\n            int[] changed = insert.executeBatch();\n            if (IntStream.of(changed).sum() < links.size()) {\n                conn.rollback();\n            } else\n                conn.commit();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized Optional<List<Cid>> getLinks(Cid block) {\n        long index;\n        try {\n            index = getBlockIndex(block);\n        } catch (Exception e) {\n            return Optional.empty();\n        }\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(EMPTY_LINKS)) {\n            query.setLong(1, index);\n            ResultSet res = query.executeQuery();\n            res.next();\n            if (res.getLong(1) > 0)\n                return Optional.of(Collections.emptyList());\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n\n        try (Connection conn = getConnection();\n             PreparedStatement query = conn.prepareStatement(LINKS)) {\n            query.setLong(1, index);\n            ResultSet res = query.executeQuery();\n            List<Cid> links = new ArrayList<>();\n            while (res.next()) {\n                links.add(getBlock(res.getLong(1)));\n            }\n            if (links.isEmpty())\n                return Optional.empty();\n            return Optional.of(links);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    public synchronized void removeBlock(BlockVersion block) {\n        long index;\n        try {\n            index = getBlockVersionIndex(block);\n        } catch (Exception e) {\n            return;\n        }\n        try (Connection conn = getConnection();\n             PreparedStatement delete = conn.prepareStatement(DELETE_BLOCK);\n             PreparedStatement deleteLinks = conn.prepareStatement(DELETE_LINKS);\n             PreparedStatement deleteEmptyLinks = conn.prepareStatement(DELETE_EMPTY_LINKS)) {\n            deleteLinks.setLong(1, index);\n            deleteLinks.executeUpdate();\n            deleteEmptyLinks.setLong(1, index);\n            deleteEmptyLinks.executeUpdate();\n            delete.setBytes(1, block.cid.toBytes());\n            delete.setString(2, block.version);\n            delete.executeUpdate();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n        }\n    }\n\n    public static SqliteBlockReachability createReachabilityDb(Path dbFile) {\n        try {\n            Connection file = Sqlite.build(dbFile.toString());\n            // We need a connection that ignores close\n            Connection instance = new Sqlite.UncloseableConnection(file);\n            return new SqliteBlockReachability(() -> instance, new SqliteCommands());\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void main(String[] a) throws IOException {\n        // This is a benchmark to test baseline speed of a blockstore GC\n        String filename = System.nanoTime() + \"temp.sql\";\n        Path file = Path.of(filename);\n        SqliteBlockReachability reachabilityDb = createReachabilityDb(file);\n        List<BlockVersion> versions = new ArrayList<>();\n        int count = 1_000_000;\n        boolean versioned = true;\n        Random rnd = new Random(28);\n\n        for (int i = 0; i < count; i++) {\n            byte[] hash = new byte[32];\n            rnd.nextBytes(hash);\n            Cid cid = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n            if (versioned)\n                versions.add(new BlockVersion(cid, ArrayOps.bytesToHex(hash), true));\n            else\n                versions.add(new BlockVersion(cid, null, true));\n        }\n        System.out.println(\"Starting Db load...\");\n        long t0 = System.nanoTime();\n        int batchSize = 10_000;\n        for (int i = 0; i < count / batchSize; i++) {\n            reachabilityDb.addBlocks(versions.subList(i * batchSize, (i+1)* batchSize));\n            long size = reachabilityDb.size();\n            if (size != (i +1) * batchSize)\n                throw new IllegalStateException(\"Incorrect size: \" + size + \", expected \" + (i+1)*batchSize);\n        }\n        long t1 = System.nanoTime();\n        System.out.println(\"Load duration \" + (t1-t0)/1_000_000_000 + \"s, batch size = \" + batchSize);\n        int markBatchSize = 1000;\n        AtomicLong totalReachable = new AtomicLong();\n        for (int i = 0; i < count / markBatchSize; i++) {\n            List<Cid> batch = versions.subList(i * markBatchSize, (i + 1) * markBatchSize)\n                    .stream()\n                    .map(v -> v.cid)\n                    .collect(Collectors.toList());\n            reachabilityDb.setReachable(batch, totalReachable);\n        }\n        long t2 = System.nanoTime();\n        System.out.println(\"Marking reachable took \" + (t2-t1)/1_000_000_000 + \"s, batch size = \" + markBatchSize);\n        reachabilityDb.setReachable(Arrays.asList(versions.get(0).cid), totalReachable);\n\n        List<BlockVersion> unreachable = new ArrayList<>();\n        long t3 = System.nanoTime();\n        reachabilityDb.getUnreachable(unreachable::addAll);\n        if (!unreachable.isEmpty())\n            throw new IllegalStateException(\"Incorrect garbage! This would lose data!\");\n        long t4 = System.nanoTime();\n        System.out.println(\"Listing garbage took \" + (t4-t3)/1_000_000 + \"ms\");\n\n        long size = reachabilityDb.size();\n        if (size != count)\n            throw new IllegalStateException(\"Missing rows! \" + size + \", expected \" + count);\n\n        // Now double the size with unreachable blocks\n        for (int i = 0; i < count; i++) {\n            byte[] hash = new byte[32];\n            rnd.nextBytes(hash);\n            Cid cid = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n            if (versioned)\n                versions.add(new BlockVersion(cid, ArrayOps.bytesToHex(hash), true));\n            else\n                versions.add(new BlockVersion(cid, null, true));\n        }\n        for (int i = 0; i < count / batchSize; i++) {\n            reachabilityDb.addBlocks(versions.subList(count + i * batchSize, count + (i+1)* batchSize));\n        }\n        long t5 = System.nanoTime();\n        reachabilityDb.getUnreachable(unreachable::addAll);\n        long t6 = System.nanoTime();\n        if (unreachable.size() != count)\n            throw new IllegalStateException(\"Incorrect garbage!\");\n        System.out.println(\"Listing garbage took \" + (t6-t5)/1_000_000 + \"ms\");\n\n        // put the same block version in multiple times (should be idempotent)\n        long priorSize = reachabilityDb.size();\n        byte[] hash1 = new byte[32];\n        rnd.nextBytes(hash1);\n        Cid cid1 = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash1);\n        BlockVersion v1 = new BlockVersion(cid1, ArrayOps.bytesToHex(hash1), true);\n        reachabilityDb.addBlocks(Arrays.asList(v1, v1, v1, v1, v1, v1, v1, v1, v1, v1));\n\n        reachabilityDb.setReachable(versions.subList(0, 10).stream().map(v ->v.cid).collect(Collectors.toList()), totalReachable);\n        reachabilityDb.setReachable(versions.subList(0, 10).stream().map(v ->v.cid).collect(Collectors.toList()), totalReachable);\n        try {\n            reachabilityDb.addBlocks(Arrays.asList(v1));\n        } catch (Exception e) {}\n        long with1Block = reachabilityDb.size();\n        if (with1Block != priorSize + 1)\n            throw new IllegalStateException(\"Adding not idempotent!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/TransactionStore.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.storage.*;\n\nimport java.util.*;\n\npublic interface TransactionStore {\n\n    TransactionId startTransaction(PublicKeyHash owner);\n\n    void addBlock(Multihash hash, TransactionId tid, PublicKeyHash owner);\n\n    void closeTransaction(PublicKeyHash owner, TransactionId tid);\n\n    List<Cid> getOpenTransactionBlocks(PublicKeyHash owner);\n\n    void clearOldTransactions(PublicKeyHash owner, long cutoffUtcMillis);\n}\n"
  },
  {
    "path": "src/peergos/server/storage/TransactionalIpfs.java",
    "content": "package peergos.server.storage;\n\nimport peergos.server.storage.auth.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class TransactionalIpfs extends DelegatingDeletableStorage {\n\n    private final DeletableContentAddressedStorage target;\n    private final TransactionStore transactions;\n    private final BlockRequestAuthoriser authoriser;\n    private final Cid id;\n    private final String linkHost;\n    private final Hasher hasher;\n    private CoreNode pki;\n\n    public TransactionalIpfs(DeletableContentAddressedStorage target,\n                             TransactionStore transactions,\n                             BlockRequestAuthoriser authoriser,\n                             Cid id,\n                             String linkHost,\n                             Hasher hasher) {\n        super(target);\n        this.target = target;\n        this.transactions = transactions;\n        this.authoriser = authoriser;\n        this.id = id;\n        this.linkHost = linkHost;\n        this.hasher = hasher;\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(id);\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return Futures.of(linkHost);\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n        this.pki = pki;\n        target.setPki(pki);\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return Futures.of(transactions.startTransaction(owner));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        transactions.closeTransaction(owner, tid);\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        List<Multihash> providers = hasBlock(owner, hash) ? List.of(id) : pki.getStorageProviders(owner);\n        return get(providers, owner, hash, bat, id, hasher, true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds,\n                                                       PublicKeyHash owner,\n                                                       Cid hash,\n                                                       String auth,\n                                                       boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, auth, persistBlock).thenApply(bopt -> bopt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds,\n                                                       PublicKeyHash owner,\n                                                       Cid hash,\n                                                       Optional<BatWithId> bat,\n                                                       Cid ourId,\n                                                       Hasher h,\n                                                       boolean persistBlock) {\n        if (bat.isEmpty())\n            return getRaw(peerIds, owner, hash, bat, ourId, hasher, true, persistBlock)\n                    .thenApply(opt -> opt.map(CborObject::fromByteArray));\n        return Futures.asyncExceptionally(() -> bat.get().bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, h)\n                        .thenApply(BlockAuth::encode)\n                        .thenCompose(auth -> get(peerIds, owner, hash, auth, persistBlock)),\n                t -> AuthedStorage.getWithAbsentMirrorBat(t, peerIds, owner, hash, bat, ourId, h, this)\n        );\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat) {\n        return getRaw(pki.getStorageProviders(owner), owner, hash, bat, id, hasher, true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        return target.getRaw(peerIds, owner, hash, bat, ourId, h, doAuth, persistBlock).thenApply(bopt -> {\n            if (bopt.isEmpty())\n                return Optional.empty();\n            byte[] block = bopt.get();\n            if (doAuth) {\n                String auth = bat.isEmpty() ? \"\" : bat.get().bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, h)\n                        .thenApply(BlockAuth::encode).join();\n                if (! authoriser.allowRead(hash, block, id().join(), auth).join()) {\n                    throw new IllegalStateException(\"Unauthorised!\");\n                }\n            }\n            return bopt;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      String auth,\n                                                      boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, auth, true, persistBlock);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      String auth,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        if (hash.isIdentity())\n            return Futures.of(Optional.of(hash.getHash()));\n        return target.getRaw(peerIds, owner, hash, auth, persistBlock).thenApply(bopt -> {\n            if (bopt.isEmpty())\n                return Optional.empty();\n            byte[] block = bopt.get();\n            if (doAuth && ! authoriser.allowRead(hash, block, id().join(), auth).join())\n                throw new IllegalStateException(\"Unauthorised!\");\n            return bopt;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean persistBlock) {\n        return getRaw(peerIds, owner, hash, bat, ourId, h, true, persistBlock);\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        return target.hasBlock(owner, hash);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        if (root.isRaw())\n            return CompletableFuture.completedFuture(Collections.emptyList());\n        return getRaw(Arrays.asList(id), owner, root, Optional.empty(), id, hasher, false, true)\n                .thenApply(opt -> opt.map(CborObject::fromByteArray))\n                .thenApply(opt -> opt\n                        .map(cbor -> cbor.links().stream().map(c -> (Cid) c).collect(Collectors.toList()))\n                        .orElse(Collections.emptyList())\n                );\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        return getRaw(List.of(id), owner, block, Optional.empty(), id, hasher, false, true)\n                .thenApply(data -> BlockMetadataStore.extractMetadata(block, data.get()));\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner,\n                                                          Cid root,\n                                                          List<ChunkMirrorCap> caps,\n                                                          Optional<Cid> committedRoot) {\n        if (! hasBlock(owner, root))\n            return Futures.errored(new IllegalStateException(\"Champ root not present locally: \" + root));\n        return getChampLookup(owner, root, caps, committedRoot, hasher);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        for (byte[] signedHash : signedHashes) {\n            Multihash hash = new Multihash(Multihash.Type.sha2_256, Arrays.copyOfRange(signedHash, signedHash.length - 32, signedHash.length));\n            Cid cid = new Cid(1, Cid.Codec.DagCbor, hash.type, hash.getHash());\n            transactions.addBlock(cid, tid, owner);\n        }\n        return target.put(owner, writer, signedHashes, blocks, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signedHashes,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        for (byte[] signedHash : signedHashes) {\n            Multihash hash = new Multihash(Multihash.Type.sha2_256, Arrays.copyOfRange(signedHash, signedHash.length - 32, signedHash.length));\n            Cid cid = new Cid(1, Cid.Codec.Raw, hash.type, hash.getHash());\n            transactions.addBlock(cid, tid, owner);\n        }\n        return target.putRaw(owner, writer, signedHashes, blocks, tid, progressConsumer);\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        return target.getAllBlockHashes(owner, useBlockstore);\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        target.getAllBlockHashVersions(owner, res);\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid hash) {\n        target.delete(owner, hash);\n    }\n\n    @Override\n    public void bulkDelete(PublicKeyHash owner, List<BlockVersion> blocks) {\n        target.bulkDelete(owner, blocks);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return transactions.getOpenTransactionBlocks(owner);\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n        transactions.clearOldTransactions(owner, cutoffMillis);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/UserBlockVersion.java",
    "content": "package peergos.server.storage;\n\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.Objects;\n\npublic class UserBlockVersion {\n    public final String username;\n    public final Cid cid;\n    public final String version;\n    public final boolean isLatest;\n\n    public UserBlockVersion(String username, Cid cid, String version, boolean isLatest) {\n        this.username = username;\n        this.cid = cid;\n        this.version = version;\n        this.isLatest = isLatest;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        UserBlockVersion that = (UserBlockVersion) o;\n        return isLatest == that.isLatest && Objects.equals(username, that.username) && Objects.equals(cid, that.cid) && Objects.equals(version, that.version);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(username, cid, version, isLatest);\n    }\n\n    @Override\n    public String toString() {\n        if (version == null)\n            return (username == null ? \"\" : username + \"/\") + cid.toString();\n        if (username == null)\n            return cid.toString() + \":\" + version;\n        return username + \"/\" + cid.toString() + \":\" + version;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/UserQuotas.java",
    "content": "package peergos.server.storage;\nimport java.time.LocalDateTime;\nimport java.util.logging.*;\n\nimport peergos.server.space.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/** This implements a quota manager for Peergos instances that are not charging for storage\n */\npublic class UserQuotas implements QuotaAdmin {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private final long defaultQuota, maxUsers;\n    private final JdbcQuotas quotas;\n    private final JdbcSpaceRequests spaceRequests;\n    private final ContentAddressedStorage dht;\n    private CoreNode core;\n    private final boolean isPki;\n\n    public UserQuotas(JdbcQuotas quotas,\n                      long defaultQuota,\n                      long maxUsers,\n                      JdbcSpaceRequests spaceRequests,\n                      ContentAddressedStorage dht,\n                      boolean isPki) {\n        this.quotas = quotas;\n        this.defaultQuota = defaultQuota;\n        this.maxUsers = maxUsers;\n        this.spaceRequests = spaceRequests;\n        this.dht = dht;\n        this.isPki = isPki;\n    }\n\n    @Override\n    public void setPki(CoreNode core) {\n        this.core = core;\n    }\n\n    @Override\n    public List<QuotaControl.LabelledSignedSpaceRequest> getSpaceRequests() {\n        return spaceRequests.getSpaceRequests();\n    }\n\n    @Override\n    public CompletableFuture<Long> getQuota(PublicKeyHash owner, byte[] signedTime) {\n        TimeLimited.isAllowedTime(signedTime, 300, dht, owner);\n        String username = core.getUsername(owner).join();\n        return Futures.of(getQuota(username));\n    }\n\n    @Override\n    public boolean hadQuota(String username, LocalDateTime time) {\n        return false;\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> requestQuota(PublicKeyHash owner, byte[] signedRequest, long usage) {\n        SpaceUsage.SpaceRequest req = QuotaAdmin.parseQuotaRequest(owner, signedRequest, dht);\n        // TODO check user is signed up to this server\n        boolean added = spaceRequests.addSpaceRequest(req.username, signedRequest);\n        String username = core.getUsername(owner).join();\n        return Futures.of(new PaymentProperties(getQuota(username)));\n    }\n\n    @Override\n    public void approveSpaceRequest(PublicKeyHash adminIdentity, Multihash instanceIdentity, byte[] signedRequest) {\n        try {\n            Optional<PublicSigningKey> adminOpt = dht.getSigningKey(adminIdentity, adminIdentity).join();\n            if (!adminOpt.isPresent())\n                throw new IllegalStateException(\"Couldn't retrieve admin key!\");\n            byte[] rawFromAdmin = adminOpt.get().unsignMessage(signedRequest).join();\n            SpaceUsage.LabelledSignedSpaceRequest withName = QuotaControl.LabelledSignedSpaceRequest\n                    .fromCbor(CborObject.fromByteArray(rawFromAdmin));\n\n            Optional<PublicKeyHash> userOpt = core.getPublicKeyHash(withName.username).join();\n            if (! userOpt.isPresent())\n                throw new IllegalStateException(\"Couldn't lookup user key!\");\n\n            Optional<PublicSigningKey> userKey = dht.getSigningKey(userOpt.get(), userOpt.get()).join();\n            if (! userKey.isPresent())\n                throw new IllegalStateException(\"Couldn't retrieve user key!\");\n\n            CborObject cbor = CborObject.fromByteArray(userKey.get().unsignMessage(withName.signedRequest).join());\n            SpaceUsage.SpaceRequest req = QuotaControl.SpaceRequest.fromCbor(cbor);\n            setQuota(req.username, req.bytes);\n            removeSpaceRequest(req.username, withName.signedRequest);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void removeSpaceRequest(String username, byte[] unsigned) {\n        spaceRequests.removeSpaceRequest(username, unsigned);\n    }\n\n    @Override\n    public AllowedSignups acceptingSignups() {\n        return new AllowedSignups(quotas.numberOfUsers() < maxUsers, false);\n    }\n\n    @Override\n    public List<String> getLocalUsernames() {\n        return new ArrayList<>(quotas.getQuotas().keySet());\n    }\n\n    @Override\n    public boolean allowSignupOrUpdate(String username, String token) {\n        if (quotas.hasUser(username))\n            return true;\n        if (quotas.hasToken(token))\n            return true;\n        if (quotas.numberOfUsers() >= maxUsers)\n            return false;\n        quotas.setQuota(username, defaultQuota);\n        return true;\n    }\n\n    @Override\n    public PaymentProperties createPaidUser(String username) {\n        if (isPki)\n            return new PaymentProperties(0);\n        throw new IllegalStateException(\"Cannot create a paid user on an unpaid server!\");\n    }\n\n    @Override\n    public void removeDesiredQuota(String username) {}\n\n    @Override\n    public boolean consumeToken(String username, String token) {\n        if (! token.isEmpty()) {\n            return quotas.removeToken(token) && quotas.setQuota(username, defaultQuota);\n        }\n        return false;\n    }\n\n    @Override\n    public boolean addToken(String token) {\n        return quotas.addToken(token);\n    }\n\n    @Override\n    public long getQuota(String username) {\n        return quotas.getQuota(username);\n    }\n\n    @Override\n    public void removeQuota(String username) {\n        quotas.removeUser(username);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> getPaymentProperties(PublicKeyHash owner,\n                                                                     boolean newClientSecret,\n                                                                     byte[] signedTime) {\n        TimeLimited.isAllowedTime(signedTime, 300, dht, owner);\n        String username = core.getUsername(owner).join();\n        return Futures.of(new PaymentProperties(getQuota(username)));\n    }\n\n    public void setQuota(String username, long quota) {\n        quotas.setQuota(username, quota);\n    }\n\n    private static String getUsername(String line) {\n        return line.split(\" \")[0];\n    }\n\n    private static long parseQuotaLine(String line) {\n        return parseQuota(line.split(\" \")[1]);\n    }\n\n    public static long parseQuota(String quota) {\n        if (quota.endsWith(\"t\"))\n            return Long.parseLong(quota.substring(0, quota.length() - 1)) * 1024 * 1024 * 1024 * 1024;\n        if (quota.endsWith(\"g\"))\n            return Long.parseLong(quota.substring(0, quota.length() - 1)) * 1024 * 1024 * 1024;\n        if (quota.endsWith(\"m\"))\n            return Long.parseLong(quota.substring(0, quota.length() - 1)) * 1024 * 1024;\n        if (quota.endsWith(\"k\"))\n            return Long.parseLong(quota.substring(0, quota.length() - 1)) * 1024;\n        return Long.parseLong(quota);\n    }\n\n    public static Map<String, Long> readUsernamesFromFile(Path source) {\n        try {\n            if (! source.toFile().exists())\n                return Collections.emptyMap();\n            return Files.lines(source)\n                    .map(String::trim)\n                    .collect(Collectors.toMap(UserQuotas::getUsername, UserQuotas::parseQuotaLine));\n        } catch (IOException e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n            return Collections.emptyMap();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/VersionFilter.java",
    "content": "package peergos.server.storage;\n\npublic interface VersionFilter {\n\n    boolean has(BlockVersion v);\n\n    BlockVersion add(BlockVersion v);\n}\n"
  },
  {
    "path": "src/peergos/server/storage/admin/Admin.java",
    "content": "package peergos.server.storage.admin;\n\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.regex.*;\n\npublic class Admin implements InstanceAdmin {\n\n    private static final Path waitingList = PathUtil.get(\"waiting-list.txt\");\n    private static final int MAX_WAITING = 1_000_000;\n\n    private final Set<String> adminUsernames;\n    private final QuotaAdmin quotas;\n    private final CoreNode core;\n    private final ContentAddressedStorage ipfs;\n    private final AtomicLong lastPendingRequestTime = new AtomicLong(System.currentTimeMillis());\n    private final boolean enableWaitList;\n    private int numberWaiting;\n    private final String sourceVersion;\n\n    public Admin(Set<String> adminUsernames,\n                 QuotaAdmin quotas,\n                 CoreNode core,\n                 ContentAddressedStorage ipfs,\n                 boolean enableWaitList) {\n        this.adminUsernames = adminUsernames;\n        this.quotas = quotas;\n        this.core = core;\n        this.ipfs = ipfs;\n        this.enableWaitList = enableWaitList;\n        try {\n            this.numberWaiting = Files.readAllLines(waitingList).size();\n        } catch (IOException e) {\n            this.numberWaiting = 0;\n        }\n\n        this.sourceVersion = getSourceVersion();\n    }\n\n    public static String getSourceVersion() {\n        return Optional.ofNullable(Admin.class.getPackage().getImplementationVersion()).orElse(\"\");\n    }\n\n    @Override\n    public CompletableFuture<VersionInfo> getVersionInfo() {\n        return CompletableFuture.completedFuture(new VersionInfo(UserService.CURRENT_VERSION, sourceVersion));\n    }\n\n    @Override\n    public synchronized CompletableFuture<List<SpaceUsage.LabelledSignedSpaceRequest>> getPendingSpaceRequests(\n            PublicKeyHash adminIdentity,\n            Multihash instanceIdentity,\n            byte[] signedTime) {\n        String username = core.getUsername(adminIdentity).join();\n        if (! adminUsernames.contains(username))\n            return Futures.of(Collections.emptyList());\n        long time = TimeLimited.isAllowedTime(signedTime, 60, ipfs, adminIdentity);\n        if (lastPendingRequestTime.get() >= time)\n            throw new IllegalStateException(\"Replay attack? Stale auth time for getPendingSpaceRequests\");\n        lastPendingRequestTime.set(time);\n        return CompletableFuture.completedFuture(quotas.getSpaceRequests());\n    }\n\n    @Override\n    public CompletableFuture<Boolean> approveSpaceRequest(PublicKeyHash adminIdentity,\n                                                          Multihash instanceIdentity,\n                                                          byte[] signedRequest) {\n            // check admin key is from an admin\n            String username = core.getUsername(adminIdentity).join();\n            if (! adminUsernames.contains(username))\n                throw new IllegalStateException(\"User is not an admin on this instance!\");\n            quotas.approveSpaceRequest(adminIdentity, instanceIdentity, signedRequest);\n            return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<AllowedSignups> acceptingSignups() {\n        return Futures.of(quotas.acceptingSignups());\n    }\n\n    public String generateSignupToken(SafeRandom rnd) {\n        return quotas.generateToken(rnd);\n    }\n\n    private static Pattern VALID_EMAIL = Pattern.compile(\"^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\\\.[A-Z]{2,6}$\", Pattern.CASE_INSENSITIVE);\n    private static final int MAX_EMAIL_LENGTH = 256;\n\n    @Override\n    public synchronized CompletableFuture<Boolean> addToWaitList(String email) {\n        if (! enableWaitList\n                || numberWaiting >= MAX_WAITING\n                || ! VALID_EMAIL.matcher(email).matches()\n                || email.length() > MAX_EMAIL_LENGTH)\n            return Futures.of(false);\n        try {\n            Files.write(waitingList, (email + \"\\n\").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);\n            numberWaiting++;\n            return Futures.of(true);\n        } catch (IOException e) {\n            e.printStackTrace();\n            return Futures.of(false);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/admin/AdminHandler.java",
    "content": "package peergos.server.storage.admin;\n\nimport com.sun.net.httpserver.*;\nimport peergos.server.util.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.function.*;\n\npublic class AdminHandler implements HttpHandler {\n\n    private final InstanceAdmin target;\n    private final boolean isPublicServer;\n\n    public AdminHandler(InstanceAdmin target, boolean isPublicServer) {\n        this.target = target;\n        this.isPublicServer = isPublicServer;\n    }\n\n    public void handle(HttpExchange exchange) {\n        String path = exchange.getRequestURI().getPath();\n        if (path.startsWith(\"/\"))\n            path = path.substring(1);\n        String[] subComponents = path.substring(Constants.ADMIN_URL.length()).split(\"/\");\n        String method = subComponents[0];\n\n        Map<String, List<String>> params = HttpUtil.parseQuery(exchange.getRequestURI().getQuery());\n        Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1);\n\n        Cborable reply;\n        try {\n            if (! HttpUtil.allowedQuery(exchange, isPublicServer)) {\n                exchange.sendResponseHeaders(405, 0);\n                return;\n            }\n\n            switch (method) {\n                case HttpInstanceAdmin.VERSION:\n                    InstanceAdmin.VersionInfo res = target.getVersionInfo().join();\n                    reply = res.toCbor();\n                    break;\n                case HttpInstanceAdmin.PENDING: {\n                    PublicKeyHash admin = PublicKeyHash.fromString(params.get(\"admin\").get(0));\n                    Multihash instance = Cid.decode(params.get(\"instance\").get(0));\n                    byte[] signedTime = ArrayOps.hexToBytes(last.apply(\"auth\"));\n                    List<SpaceUsage.LabelledSignedSpaceRequest> pending = target\n                            .getPendingSpaceRequests(admin, instance, signedTime).join();\n                    reply = new CborObject.CborList(pending);\n                    break;\n                }\n                case HttpInstanceAdmin.APPROVE: {\n                    PublicKeyHash admin = PublicKeyHash.fromString(params.get(\"admin\").get(0));\n                    Multihash instance = Cid.decode(params.get(\"instance\").get(0));\n                    byte[] signedReq = ArrayOps.hexToBytes(last.apply(\"req\"));\n                    boolean result = target.approveSpaceRequest(admin, instance, signedReq).join();\n                    reply = new CborObject.CborBoolean(result);\n                    break;\n                }\n                case HttpInstanceAdmin.WAIT_LIST: {\n                    String email = params.get(\"email\").get(0);\n                    boolean result = target.addToWaitList(email).join();\n                    reply = new CborObject.CborBoolean(result);\n                    break;\n                }\n                case HttpInstanceAdmin.SIGNUPS: {\n                    reply = target.acceptingSignups().join();\n                    break;\n                }\n                default:\n                    throw new IOException(\"Unknown method in admin handler!\");\n            }\n\n            byte[] res = reply.serialize();\n            exchange.sendResponseHeaders(200, res.length);\n            exchange.getResponseBody().write(res);\n        } catch (Exception e) {\n            HttpUtil.replyError(exchange, e);\n        } finally {\n            exchange.close();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/admin/HttpQuotaAdmin.java",
    "content": "package peergos.server.storage.admin;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class HttpQuotaAdmin implements QuotaAdmin {\n    public static final String QUOTA_URL = \"quota-admin/\";\n\n    public static final String SIGNUPS = \"signups\";\n    public static final String USERNAMES = \"usernames\";\n    public static final String ALLOWED = \"allowed\";\n    public static final String CREATE_PAID = \"create-paid\";\n    public static final String REMOVE_DESIRED = \"remove-desired-quota\";\n    public static final String TOKEN_ADD = \"token-add\";\n    public static final String TOKEN_REMOVE = \"token-remove\";\n    public static final String QUOTA_PRIVATE = \"quota-by-name\";\n    public static final String QUOTA_PRIVATE_REMOVE = \"quota-remove\";\n    public static final String QUOTA_PRIVATE_TIME = \"quota-by-name-time\";\n    public static final String PAYMENT_PROPERTIES = \"payment-properties\";\n    public static final String QUOTA_PUBLIC = \"quota\";\n    public static final String REQUEST_QUOTA = \"request\";\n\n    private final HttpPoster poster;\n\n    public HttpQuotaAdmin(HttpPoster poster) {\n        this.poster = poster;\n    }\n\n    @Override\n    public AllowedSignups acceptingSignups() {\n        return poster.get(QUOTA_URL + SIGNUPS)\n                .thenApply(res -> AllowedSignups.fromCbor(CborObject.fromByteArray(res))).join();\n    }\n\n    @Override\n    public boolean allowSignupOrUpdate(String username, String token) {\n        return poster.get(QUOTA_URL + ALLOWED + \"?username=\" + username + \"&token=\"+token)\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value).join();\n    }\n\n    @Override\n    public PaymentProperties createPaidUser(String username) {\n        return poster.get(QUOTA_URL + CREATE_PAID + \"?username=\" + username)\n                .thenApply(res -> PaymentProperties.fromCbor(CborObject.fromByteArray(res))).join();\n    }\n\n    @Override\n    public void removeDesiredQuota(String username) {\n        poster.get(QUOTA_URL + REMOVE_DESIRED + \"?username=\" + username).join();\n    }\n\n    @Override\n    public long getQuota(String username) {\n        return poster.get(QUOTA_URL + QUOTA_PRIVATE + \"?username=\" + username)\n                .thenApply(res -> ((CborObject.CborLong)CborObject.fromByteArray(res)).value).join();\n    }\n\n    @Override\n    public void removeQuota(String username) {\n        poster.get(QUOTA_URL + QUOTA_PRIVATE_REMOVE + \"?username=\" + username).join();\n    }\n\n    @Override\n    public boolean hadQuota(String username, LocalDateTime time) {\n        return poster.get(QUOTA_URL + QUOTA_PRIVATE_TIME + \"?username=\" + username + \"&time=\" + time)\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value).join();\n    }\n\n    @Override\n    public boolean addToken(String token) {\n        return poster.get(QUOTA_URL + TOKEN_ADD + \"?token=\" + token)\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value).join();\n    }\n\n    @Override\n    public boolean consumeToken(String username, String token) {\n        return poster.get(QUOTA_URL + TOKEN_REMOVE + \"?username=\" + username + \"&token=\" + token)\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value).join();\n    }\n\n    @Override\n    public List<String> getLocalUsernames() {\n        return poster.get(QUOTA_URL + USERNAMES)\n                .thenApply(res -> ((CborObject.CborList)CborObject.fromByteArray(res)).value\n                        .stream()\n                        .map(x -> ((CborObject.CborString)x).value)\n                        .collect(Collectors.toList()))\n                .join();\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> getPaymentProperties(PublicKeyHash owner, boolean newClientSecret, byte[] signedTime) {\n        return poster.get(QUOTA_URL + PAYMENT_PROPERTIES + \"?owner=\"+owner\n                + \"&new-client-secret=\" + newClientSecret\n                + \"&auth=\" + ArrayOps.bytesToHex(signedTime))\n                .thenApply(res -> PaymentProperties.fromCbor(CborObject.fromByteArray(res)));\n    }\n\n    @Override\n    public CompletableFuture<Long> getQuota(PublicKeyHash owner, byte[] signedTime) {\n        return poster.get(QUOTA_URL + QUOTA_PUBLIC + \"?owner=\"+owner + \"&auth=\" + ArrayOps.bytesToHex(signedTime))\n                .thenApply(res -> ((CborObject.CborLong)CborObject.fromByteArray(res)).value);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> requestQuota(PublicKeyHash owner, byte[] signedRequest, long usage) {\n        return poster.get(QUOTA_URL + REQUEST_QUOTA + \"?owner=\"+owner + \"&req=\" + ArrayOps.bytesToHex(signedRequest) + \"&usage=\" + usage)\n                .thenApply(res -> PaymentProperties.fromCbor(CborObject.fromByteArray(res)));\n    }\n\n   @Override\n    public List<LabelledSignedSpaceRequest> getSpaceRequests() {\n        return Collections.emptyList();\n    }\n\n    @Override\n    public void approveSpaceRequest(PublicKeyHash adminIdentity, Multihash instanceIdentity, byte[] signedRequest) {}\n\n    @Override\n    public void removeSpaceRequest(String username, byte[] unsigned) {}\n\n}\n"
  },
  {
    "path": "src/peergos/server/storage/admin/QuotaAdmin.java",
    "content": "package peergos.server.storage.admin;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.util.*;\n\nimport java.time.LocalDateTime;\nimport java.util.*;\n\n/** The api for administrators approving pending space requests\n *\n */\npublic interface QuotaAdmin extends QuotaControl {\n\n    AllowedSignups acceptingSignups();\n\n    boolean allowSignupOrUpdate(String username, String token);\n\n    PaymentProperties createPaidUser(String username);\n\n    void removeDesiredQuota(String username);\n\n    boolean addToken(String token);\n\n    boolean consumeToken(String username, String token);\n\n    default void setPki(CoreNode core) {}\n\n    default String generateToken(SafeRandom rnd) {\n        String token = ArrayOps.bytesToHex(rnd.randomBytes(32));\n        addToken(token);\n        return token;\n    }\n\n    long getQuota(String username);\n\n    void removeQuota(String username);\n\n    boolean hadQuota(String username, LocalDateTime time);\n\n    List<String> getLocalUsernames();\n\n    List<SpaceUsage.LabelledSignedSpaceRequest> getSpaceRequests();\n\n    void approveSpaceRequest(PublicKeyHash adminIdentity, Multihash instanceIdentity, byte[] signedRequest);\n\n    void removeSpaceRequest(String username, byte[] unsigned);\n\n    static QuotaControl.SpaceRequest parseQuotaRequest(PublicKeyHash owner, byte[] signedRequest, ContentAddressedStorage dht) {\n        // check request is valid\n        Optional<PublicSigningKey> ownerOpt = dht.getSigningKey(owner, owner).join();\n        if (!ownerOpt.isPresent())\n            throw new IllegalStateException(\"Couldn't retrieve owner key!\");\n        byte[] raw = ownerOpt.get().unsignMessage(signedRequest).join();\n        CborObject cbor = CborObject.fromByteArray(raw);\n        QuotaControl.SpaceRequest req = QuotaControl.SpaceRequest.fromCbor(cbor);\n        long now = System.currentTimeMillis();\n        if (req.utcMillis < now - 300_000)\n            throw new IllegalStateException(\"Stale auth time in space request! \" + req.utcMillis + \" !< \" + now + \" - 30000\");\n        return req;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/auth/BlockRequestAuthoriser.java",
    "content": "package peergos.server.storage.auth;\n\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface BlockRequestAuthoriser {\n\n    default CompletableFuture<Boolean> allowRead(Cid block, byte[] blockData, Cid sourceNodeId, String auth) {\n        List<BatId> batIds = block.isRaw() ?\n                Bat.getRawBlockBats(blockData) :\n                block.codec == Cid.Codec.DagCbor ?\n                        Bat.getCborBlockBats(blockData) :\n                        Collections.emptyList();\n        return allowRead(block, batIds, sourceNodeId, auth);\n    }\n\n    CompletableFuture<Boolean> allowRead(Cid block, List<BatId> blockBats, Cid sourceNodeId, String auth);\n\n    static boolean allowRead(Cid b,\n                             List<BatId> batids,\n                             Cid s,\n                             Optional<BlockAuth> auth,\n                             BatCave batStore,\n                             Optional<BatWithId> instanceBat,\n                             Hasher h) {\n        Logging.LOG().fine(\"Allow: \" + b + \", auth=\" + auth + \", from: \" + s);\n        if (batids.isEmpty()) // public block\n            return true;\n        if (auth.isEmpty()) {\n            Logging.LOG().info(\"INVALID AUTH: EMPTY\");\n            return false;\n        }\n        BlockAuth blockAuth = auth.get();\n        if (b.isRaw()) {\n            for (BatId bid : batids) {\n                Optional<Bat> bat = bid.getInline()\n                        .or(() -> bid.id.equals(blockAuth.batId) ?\n                                batStore.getBat(bid) :\n                                Optional.empty());\n                if (bat.isPresent() && BlockRequestAuthoriser.isValidAuth(blockAuth, b, s, bat.get(), h))\n                    return true;\n            }\n            if (instanceBat.isPresent()) {\n                if (BlockRequestAuthoriser.isValidAuth(blockAuth, b, s, instanceBat.get().bat, h))\n                    return true;\n            }\n            String reason = BlockRequestAuthoriser.invalidReason(blockAuth, b, s, batids, h);\n            Logging.LOG().info(\"INVALID RAW BLOCK AUTH: source: \" + s + \", cid: \" + b + \" reason: \" + reason);\n            return false;\n        } else if (b.codec == Cid.Codec.DagCbor) {\n            for (BatId bid : batids) {\n                Optional<Bat> bat = bid.getInline()\n                        .or(() -> bid.id.equals(blockAuth.batId) ?\n                                batStore.getBat(bid) :\n                                Optional.empty());\n                if (bat.isPresent() && BlockRequestAuthoriser.isValidAuth(blockAuth, b, s, bat.get(), h))\n                    return true;\n            }\n            if (instanceBat.isPresent()) {\n                if (BlockRequestAuthoriser.isValidAuth(blockAuth, b, s, instanceBat.get().bat, h))\n                    return true;\n            }\n            if (! batids.isEmpty()) {\n                String reason = BlockRequestAuthoriser.invalidReason(blockAuth, b, s, batids, h);\n                Logging.LOG().info(\"INVALID AUTH: source: \" + s + \", cid: \" + b + \" reason: \" + reason);\n            }\n            return false;\n        }\n        return false;\n    }\n\n    static boolean isValidAuth(BlockAuth auth, Cid block, Cid sourceNode, Bat bat, Hasher h) {\n        S3Request req = new S3Request(\"GET\", sourceNode.bareMultihash().toBase58(), \"api/v0/block/get?arg=\" + block.toBase58(), S3Request.UNSIGNED,\n                Optional.empty(), Optional.of(auth.expirySeconds), false, true,\n                Collections.emptyMap(), Collections.emptyMap(), auth.batId.toBase58(), \"eu-central-1\", auth.awsDatetime);\n        LocalDateTime timestamp = auth.timestamp();\n        LocalDateTime expiry = timestamp.plusSeconds(auth.expirySeconds);\n        LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);\n        if (expiry.isBefore(now))\n            return false;\n        String signature = S3Request.computeSignature(req, bat.encodeSecret(), h).join();\n        String expected = ArrayOps.bytesToHex(auth.signature);\n        return java.security.MessageDigest.isEqual(signature.getBytes(), expected.getBytes());\n    }\n\n    static String invalidReason(BlockAuth auth, Cid block, Cid sourceNode, List<BatId> batids, Hasher h) {\n        // careful here to avoid a timing attack on inline bats\n        Optional<BatId> match = batids.stream()\n                .filter(bid -> (! bid.isInline() && bid.id.equals(auth.batId)) ||\n                        (bid.isInline() && auth.batId.equals(h.hash(bid.getInline().get().serialize(), false).join())))\n                .findFirst();\n        if (match.isEmpty())\n            return \"No matching BAT ID in block for \" + auth.batId;\n\n        LocalDateTime timestamp = auth.timestamp();\n        LocalDateTime expiry = timestamp.plusSeconds(auth.expirySeconds);\n        LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);\n        // INVALID AUTH: Expired: 2022-04-19T08:05:34Z is before now: 2022-04-19T13:00:34.679482Z\n        if (expiry.isBefore(now))\n            return \"Expired: \" + expiry + \" is before now: \" + now;\n        return \"Invalid signature\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/auth/JdbcBatCave.java",
    "content": "package peergos.server.storage.auth;\n\nimport peergos.server.sql.*;\nimport peergos.server.util.Logging;\nimport peergos.shared.cbor.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.sql.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.logging.*;\n\npublic class JdbcBatCave implements BatCave, BatCache {\n\n    private static final Logger LOG = Logging.LOG();\n\n    private static final String CREATE = \"INSERT INTO bats (username, id, bat) VALUES(?, ?, ?)\";\n    private static final String GET = \"SELECT bat FROM bats WHERE id = ? LIMIT 1;\";\n    private static final String GET_OWNER = \"SELECT username FROM bats WHERE id = ? LIMIT 1;\";\n    private static final String GET_USER = \"SELECT * FROM bats WHERE username = ?;\";\n\n    private volatile boolean isClosed;\n    private Supplier<Connection> conn;\n\n    public JdbcBatCave(Supplier<Connection> conn, SqlSupplier commands) {\n        this.conn = conn;\n        init(commands);\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init(SqlSupplier commands) {\n        if (isClosed)\n            return;\n\n        try (Connection conn = getConnection()) {\n            commands.createTable(commands.createBatStoreTableCommand(), conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String getOwner(BatId id) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_OWNER)) {\n            stmt.setString(1, new String(Base64.getEncoder().encode(id.serialize())));\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                return rs.getString(\"username\");\n            }\n            throw new IllegalStateException(\"Unknonw bat!\");\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public Optional<Bat> getBat(BatId id) {\n        if (id.isInline())\n            throw new IllegalStateException(\"Stored BATs cannot be inline.\");\n\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET)) {\n            stmt.setString(1, new String(Base64.getEncoder().encode(id.serialize())));\n            ResultSet rs = stmt.executeQuery();\n            if (rs.next()) {\n                Bat bat = Bat.fromCbor(CborObject.fromByteArray(Base64.getDecoder().decode(rs.getString(\"bat\"))));\n                return Optional.of(bat);\n            }\n\n            return Optional.empty();\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            throw new RuntimeException(sqe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username, byte[] auth) {\n        try (Connection conn = getConnection();\n             PreparedStatement stmt = conn.prepareStatement(GET_USER)) {\n            stmt.setString(1, username);\n            ResultSet rs = stmt.executeQuery();\n            List<BatWithId> res = new ArrayList<>();\n            while (rs.next()) {\n                res.add(new BatWithId(Bat.fromCbor(CborObject.fromByteArray(Base64.getDecoder().decode(rs.getString(\"bat\")))),\n                        BatId.fromCbor(CborObject.fromByteArray(Base64.getDecoder().decode(rs.getString(\"id\")))).id));\n            }\n            return Futures.of(res);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            return Futures.errored(sqe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, byte[] auth) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(CREATE)) {\n            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n\n            insert.setString(1, username);\n            insert.setString(2, new String(Base64.getEncoder().encode(id.serialize())));\n            insert.setString(3, new String(Base64.getEncoder().encode(bat.serialize())));\n            int changed = insert.executeUpdate();\n            return CompletableFuture.completedFuture(changed > 0);\n        } catch (SQLException sqe) {\n            LOG.log(Level.WARNING, sqe.getMessage(), sqe);\n            return CompletableFuture.completedFuture(false);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username) {\n        return getUserBats(username, (byte[])null);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setUserBats(String username, List<BatWithId> bats) {\n        List<BatWithId> existing = getUserBats(username).join();\n        for (BatWithId bat : bats) {\n            if (! existing.contains(bat))\n                addBat(username, bat.id(), bat.bat, (byte[])null).join();\n        }\n        return Futures.of(true);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/auth/RamBatCave.java",
    "content": "package peergos.server.storage.auth;\n\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class RamBatCave implements BatCave {\n\n    private final Map<BatId, Bat> bats = new HashMap<>();\n    private final Map<String, List<BatWithId>> byUser = new HashMap<>();\n\n    @Override\n    public Optional<Bat> getBat(BatId id) {\n        return Optional.ofNullable(bats.get(id));\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username, byte[] auth) {\n        return Futures.of(byUser.getOrDefault(username, Collections.emptyList()));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, byte[] auth) {\n        if (id.isInline())\n            throw new IllegalStateException(\"Cannot store an inline batId!\");\n        bats.put(id, bat);\n        byUser.putIfAbsent(username, new ArrayList<>());\n        byUser.get(username).add(new BatWithId(bat, id.id));\n        return Futures.of(true);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/storage/auth/Want.java",
    "content": "package peergos.server.storage.auth;\n\nimport org.peergos.config.*;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\n\npublic class Want implements Jsonable {\n\n    public final Cid cid;\n    public final Optional<String> authHex;\n    public Want(Cid cid, Optional<String> authHex) {\n        this.cid = cid;\n        this.authHex = authHex.flatMap(a -> a.isEmpty() ? Optional.empty() : Optional.of(a));\n    }\n\n    public Want(Cid h) {\n        this(h, Optional.empty());\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Want want = (Want) o;\n        return cid.equals(want.cid) && authHex.equals(want.authHex);\n    }\n\n    public Map<String, Object> toJson() {\n        Map<String, Object> m = new LinkedHashMap<>();\n        m.put(\"c\", cid.toString());\n        authHex.ifPresent(h -> m.put(\"a\", h));\n        return m;\n    }\n\n    public static Want fromJson(Map<String, String> m) {\n        return new Want(Cid.decode(m.get(\"c\")), Optional.ofNullable(m.get(\"a\")));\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(cid, authHex);\n    }\n\n    @Override\n    public String toString() {\n        return cid.toString() + \"(\" + authHex.orElse(\"\") + \")\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/CopyOp.java",
    "content": "package peergos.server.sync;\n\nimport peergos.shared.user.fs.ResumeUploadProps;\n\nimport java.nio.file.Path;\nimport java.util.Objects;\n\nclass CopyOp {\n    public final boolean isLocalTarget;\n    public final Path source, target;\n    public final FileState sourceState, targetState;\n    public final long diffStart, diffEnd;\n    public final ResumeUploadProps props;\n\n    public CopyOp(boolean isLocalTarget,\n                  Path source,\n                  Path target,\n                  FileState sourceState,\n                  FileState targetState,\n                  long diffStart,\n                  long diffEnd,\n                  ResumeUploadProps props) {\n        if (hasComponent(source, \"..\"))\n            throw new IllegalStateException();\n        if (hasComponent(target, \"..\"))\n            throw new IllegalStateException();\n        this.isLocalTarget = isLocalTarget;\n        this.source = source;\n        this.target = target;\n        this.sourceState = sourceState;\n        this.targetState = targetState;\n        this.diffStart = diffStart;\n        this.diffEnd = diffEnd;\n        this.props = props;\n    }\n\n    private boolean hasComponent(Path p, String name) {\n        for (int i=0; i < p.getNameCount(); i++)\n            if (p.getName(i).toString().equals(name))\n                return true;\n        return false;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        CopyOp copyOp = (CopyOp) o;\n        return isLocalTarget == copyOp.isLocalTarget && diffStart == copyOp.diffStart && diffEnd == copyOp.diffEnd &&\n                Objects.equals(source, copyOp.source) && Objects.equals(target, copyOp.target) && Objects.equals(sourceState, copyOp.sourceState) && Objects.equals(targetState, copyOp.targetState);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(isLocalTarget, source, target, sourceState, targetState, diffStart, diffEnd);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/DirectorySync.java",
    "content": "package peergos.server.sync;\n\nimport peergos.server.Builder;\nimport peergos.server.Main;\nimport peergos.server.UserService;\nimport peergos.server.net.ProxyChooser;\nimport peergos.server.storage.FileBlockCache;\nimport peergos.server.user.JavaImageThumbnailer;\nimport peergos.server.util.Args;\nimport peergos.server.util.Logging;\nimport peergos.server.util.Threads;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.corenode.HTTPCoreNode;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.hash.Hash;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.login.mfa.MultiFactorAuthMethod;\nimport peergos.shared.login.mfa.MultiFactorAuthRequest;\nimport peergos.shared.login.mfa.MultiFactorAuthResponse;\nimport peergos.shared.mutable.HttpMutablePointers;\nimport peergos.shared.social.HttpSocialNetwork;\nimport peergos.shared.storage.HttpSpaceUsage;\nimport peergos.shared.storage.UnauthedCachingStorage;\nimport peergos.shared.user.LinkProperties;\nimport peergos.shared.user.Snapshot;\nimport peergos.shared.user.TrieNodeImpl;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.ProxySelector;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ForkJoinPool;\nimport java.util.concurrent.ForkJoinTask;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.*;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\npublic class DirectorySync {\n    private static final Logger LOG = Logging.LOG();\n    private static Set<String> IGNORED_FILENAMES = Stream.of(\".DS_Store\")\n            .collect(Collectors.toSet());\n\n    private static void disableLogSpam() {\n        // disable log spam\n        TrieNodeImpl.disableLog();\n        HttpMutablePointers.disableLog();\n        NetworkAccess.disableLog();\n        HTTPCoreNode.disableLog();\n        HttpSocialNetwork.disableLog();\n        HttpSpaceUsage.disableLog();\n        FileUploader.disableLog();\n        LazyInputStreamCombiner.disableLog();\n    }\n\n    public static boolean syncDir(Args args) {\n        try {\n            disableLogSpam();\n\n            String address = args.getArg(\"peergos-url\");\n            URL serverURL = new URL(address);\n            Crypto crypto = Main.initCrypto();\n            PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n            String cacheSize = args.getArg(\"block-cache-size-bytes\");\n            long blockCacheSizeBytes = cacheSize.endsWith(\"g\") ?\n                    Long.parseLong(cacheSize.substring(0, cacheSize.length() - 1)) * 1024L * 1024 * 1024 :\n                    Long.parseLong(cacheSize);\n            Optional<ProxySelector> proxy = ProxyChooser.build(args);\n            NetworkAccess network = Builder.buildJavaNetworkAccess(serverURL, address.startsWith(\"https\"), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-sync\"), proxy).join()\n                    .withStorage(s -> new UnauthedCachingStorage(s, new FileBlockCache(args.fromPeergosDir(\"block-cache-dir\", \"block-cache\"), blockCacheSizeBytes), crypto.hasher));\n            ThumbnailGenerator.setInstance(new JavaImageThumbnailer());\n            List<String> links = new ArrayList<>(Arrays.asList(args.getArg(\"links\").split(\",\")));\n            List<String> localDirs = new ArrayList<>(Arrays.asList(args.getArg(\"local-dirs\").split(\",\")));\n            List<Boolean> syncLocalDeletes = args.hasArg(\"sync-local-deletes\") ?\n                    new ArrayList<>(Arrays.stream(args.getArg(\"sync-local-deletes\").split(\",\"))\n                            .map(Boolean::parseBoolean)\n                            .collect(Collectors.toList())) :\n                    IntStream.range(0, links.size())\n                            .mapToObj(x -> true)\n                            .collect(Collectors.toList());\n            List<Boolean> syncRemoteDeletes = args.hasArg(\"sync-remote-deletes\") ?\n                    new ArrayList<>(Arrays.stream(args.getArg(\"sync-remote-deletes\").split(\",\"))\n                            .map(Boolean::parseBoolean)\n                            .collect(Collectors.toList())) :\n                    IntStream.range(0, links.size())\n                            .mapToObj(x -> true)\n                            .collect(Collectors.toList());\n            int maxDownloadParallelism = args.getInt(\"max-parallelism\", 32);\n            int minFreeSpacePercent = args.getInt(\"min-free-space-percent\", 5);\n            boolean oneRun = args.getBoolean(\"run-once\", false);\n            Path peergosDir = args.getPeergosDir();\n            return syncDirs(links, localDirs, syncLocalDeletes, syncRemoteDeletes, maxDownloadParallelism,\n                    minFreeSpacePercent, oneRun, root -> new LocalFileSystem(Paths.get(root), crypto.hasher),\n                    peergosDir, new SyncRunner.StatusHolder(), m -> log(m), e -> {if (e != null) log(e.getMessage());}, network, crypto);\n        } catch (Exception e) {\n            LOG.log(Level.SEVERE, e, e == null ? () -> \"\" : e::getMessage);\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static PeergosSyncFS buildRemote(String link,\n                                            NetworkAccess network,\n                                            Crypto crypto) {\n        Supplier<CompletableFuture<String>> linkUserPassword = () -> Futures.of(\"\");\n        List<Supplier<CompletableFuture<String>>> linkPasswords = List.of(linkUserPassword);\n        UserContext context = UserContext.fromSecretLinksV2(List.of(link), linkPasswords, network, crypto).join();\n        Path path = PathUtil.get(context.getEntryPath().join());\n        return new PeergosSyncFS(context, path);\n    }\n\n    public static Path getSyncStateDbPath(Path peergosDir, String linkPath, String localDir) {\n        return peergosDir.resolve(\"dir-sync-state-v3-\" + ArrayOps.bytesToHex(Hash.sha256(linkPath + \"///\" + localDir)) + \".sqlite\");\n    }\n\n    public static boolean syncDirs(List<String> links,\n                                   List<String> localDirs, //could be paths or URIs\n                                   List<Boolean> syncLocalDeletes,\n                                   List<Boolean> syncRemoteDeletes,\n                                   int maxDownloadParallelism,\n                                   int minFreeSpacePercent,\n                                   boolean oneRun,\n                                   Function<String, SyncFilesystem> localBuilder,\n                                   Path peergosDir,\n                                   SyncRunner.StatusHolder status,\n                                   Consumer<String> LOG,\n                                   Consumer<Throwable> ERROR,\n                                   NetworkAccess network,\n                                   Crypto crypto) {\n        if (syncLocalDeletes.size() != links.size())\n            throw new IllegalStateException(\"Incorrect number of sync-local-deletes!\");\n        if (syncRemoteDeletes.size() != links.size())\n            throw new IllegalStateException(\"Incorrect number of sync-remote-deletes!\");\n\n        List<String> linkPaths = links.stream()\n                .map(link -> UserContext.fromSecretLinksV2(Arrays.asList(link), Arrays.asList(() -> Futures.of(\"\")), network, crypto).join().getEntryPath().join())\n                .collect(Collectors.toList());\n\n        List<Path> syncDbPaths = IntStream.range(0, linkPaths.size())\n                .mapToObj(i -> getSyncStateDbPath(peergosDir, linkPaths.get(i), localDirs.get(i)))\n                .collect(Collectors.toList());\n\n        // delete any old sync dbs that are no longer referenced\n        try (Stream<Path> kids = Files.list(peergosDir)) {\n            kids\n                    .filter(p -> p.getFileName().endsWith(\".sqlite\"))\n                    .filter(p -> p.getFileName().startsWith(\"dir-sync-state-v3-\"))\n                    .filter(p -> ! syncDbPaths.contains(p))\n                    .forEach(p -> {\n                        try {\n                            Files.delete(p);\n                        } catch (IOException e) {\n                            e.printStackTrace();\n                        }\n                    });\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n\n        List<Supplier<SyncState>> syncedStates = syncDbPaths.stream()\n                .<Supplier<SyncState>>map(p -> () -> new JdbcTreeState(p.toString()))\n                .collect(Collectors.toList());\n        if (links.size() != localDirs.size())\n            throw new IllegalArgumentException(\"Mismatched number of local dirs and links\");\n\n        while (true) {\n            LOG.accept(\"Syncing \" + links.size() + \" pairs of directories: \" + IntStream.range(0, links.size()).mapToObj(i -> Arrays.asList(localDirs.get(i), linkPaths.get(i))).collect(Collectors.toList()));\n            boolean errored = false;\n            for (int i=0; i < links.size(); i++) {\n                SyncState syncedState = null;\n                try {\n                    if (status.isCancelled()) {\n                        status.resume();\n                        return false;\n                    }\n                    Path localDir = Paths.get(localDirs.get(i));\n                    Path remoteDir = PathUtil.get(linkPaths.get(i));\n                    syncedState = syncedStates.get(i).get();\n                    log(\"Syncing \" + localDir + \" to+from \" + remoteDir);\n                    long t0 = System.currentTimeMillis();\n                    String username = remoteDir.getName(0).toString();\n                    PublicKeyHash owner = network.coreNode.getPublicKeyHash(username).join().get();\n                    PeergosSyncFS remote = buildRemote(links.get(i), network, crypto);\n                    boolean isAndroid = \"The Android Project\".equals(System.getProperty(\"java.vm.vendor\"));\n                    if (! isAndroid && ! Paths.get(localDirs.get(i)).toFile().exists()) {\n                        LOG.accept(\"Local dir does not exist! Please remove and recreate the sync.\");\n                        errored = true;\n                    } else {\n                        SyncFilesystem local = localBuilder.apply(localDirs.get(i));\n                        syncDir(local, remote, syncLocalDeletes.get(i), syncRemoteDeletes.get(i),\n                                owner, network, syncedState, maxDownloadParallelism, minFreeSpacePercent, crypto, status::isCancelled, LOG);\n                        long t1 = System.currentTimeMillis();\n                        LOG.accept(\"Dir sync took \" + (t1 - t0) / 1000 + \"s\");\n                    }\n                } catch (Exception e) {\n                    errored = true;\n                    ERROR.accept(e);\n                    e.printStackTrace();\n                    DirectorySync.LOG.log(Level.WARNING, e, e::getMessage);\n                } finally {\n                    if (syncedState != null)\n                        try {\n                            syncedState.close();\n                        } catch (IOException e) {\n                            DirectorySync.LOG.log(Level.WARNING, e, e::getMessage);\n                        }\n                }\n            }\n            if (!errored)\n                ERROR.accept(null);\n            if (oneRun)\n                break;\n            Threads.sleep(30_000);\n        }\n        return true;\n    }\n\n    public static boolean init(Args args) {\n        disableLogSpam();\n        Console console = System.console();\n        String username = new String(console.readLine(\"Enter username:\"));\n        String password = new String(console.readPassword(\"Enter password:\"));\n        String address = args.getArg(\"peergos-url\");\n        try {\n            URL serverURL = new URL(address);\n            Optional<ProxySelector> proxy = ProxyChooser.build(args);\n            NetworkAccess network = Builder.buildJavaNetworkAccess(serverURL, address.startsWith(\"https\"), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-sync\"), proxy).join();\n            Crypto crypto = Main.initCrypto();\n            UserContext context = UserContext.signIn(username, password, mfar -> mfa(mfar), network, crypto).join();\n            String peergosPath = new String(console.readLine(\"Enter the peergos path you want to sync to (e.g. /$username/media/images):\"));\n            init(context, peergosPath);\n            return true;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static LinkProperties init(UserContext context, String peergosPath) {\n        try {\n            Path toSync = PathUtil.get(peergosPath).normalize();\n            if (toSync.getNameCount() < 2)\n                throw new IllegalArgumentException(\"You cannot sync to your Peergos home directory, please make a sub-directory.\");\n            Optional<FileWrapper> dir = context.getByPath(toSync).join();\n            if (dir.isEmpty())\n                throw new IllegalArgumentException(\"Directory \"+toSync+\" does not exist in Peergos!\");\n            // ensure directory is in its own writing space\n            if (dir.get().owner().equals(context.signer.publicKeyHash)) {\n                // our file\n                context.shareWriteAccessWith(toSync, Collections.emptySet()).join();\n            } else {\n                // something we have write access to\n                if (! dir.get().isWritable())\n                    throw new IllegalArgumentException(\"You do not have write access to this directory!\");\n            }\n            LinkProperties link = context.createSecretLink(toSync.toString(), true, Optional.empty(), Optional.empty(), \"\", false).join();\n\n            String cap = link.toLinkString(context.signer.publicKeyHash);\n\n            System.out.println(\"Run the sync dir command on all devices you want to sync using the following args: -links \" + cap + \" -local-dirs $LOCAL_DIR\");\n            return link;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static CompletableFuture<MultiFactorAuthResponse> mfa(MultiFactorAuthRequest req) {\n        Optional<MultiFactorAuthMethod> anyTotp = req.methods.stream().filter(m -> m.type == MultiFactorAuthMethod.Type.TOTP).findFirst();\n        if (anyTotp.isEmpty())\n            throw new IllegalStateException(\"No supported 2 factor auth method! \" + req.methods);\n        MultiFactorAuthMethod totp = anyTotp.get();\n        Console console = System.console();\n        String code = new String(console.readLine(\"Enter TOTP code for login:\"));\n        return Futures.of(new MultiFactorAuthResponse(totp.credentialId, Either.a(code)));\n    }\n\n    public static void syncDir(SyncFilesystem localFS,\n                               SyncFilesystem remoteFS,\n                               boolean syncLocalDeletes,\n                               boolean syncRemoteDeletes,\n                               PublicKeyHash owner,\n                               NetworkAccess network,\n                               SyncState syncedVersions,\n                               int maxParallelism,\n                               int minPercentFreeSpace,\n                               Crypto crypto,\n                               Supplier<Boolean> isCancelled,\n                               Consumer<String> LOG) throws IOException {\n        // first complete any failed in progress copy ops\n        List<CopyOp> ops = syncedVersions.getInProgressCopies();\n        if (! ops.isEmpty())\n            log(\"Rerunning failed copy operations...\");\n        for (CopyOp op : ops) {\n            try {\n                applyCopyOp(op.isLocalTarget ? remoteFS : localFS, op.isLocalTarget ? localFS : remoteFS, op, isCancelled, LOG);\n            } catch (FileNotFoundException e) {\n                // A local file has been added and removed concurrently with us trying to copy it, ignore the copy op now\n            }\n            syncedVersions.finishCopies(List.of(op));\n        }\n        if (! syncedVersions.hasCompletedSync()) {\n            // Do an incremental sync of only files to make progress quicker\n            // We don't need to prehash entire dirs here,\n            // because we are not trying to detect moves/renames\n            SyncProgress progress = new SyncProgress(localFS.filesCount() - syncedVersions.filesCount());\n\n            localFS.applyToSubtree(file -> {\n                if (file.size > 1024*1024) { // avoid doing many small files in non bulk uploads\n                    FileState synced = syncedVersions.byPath(file.relPath);\n                    if (synced != null)\n                        return;\n                    Path p = PathUtil.get(file.relPath);\n                    if (IGNORED_FILENAMES.contains(p.getFileName().toString()))\n                        return;\n                    if (! remoteFS.exists(p)) {\n                        try {\n                            LOG.accept(\"REMOTE: Uploading \" + file.relPath + \" \" + progress);\n                            HashTree hashTree = localFS.hashFile(p, Optional.empty(), file.relPath, syncedVersions);\n                            LocalDateTime modified = LocalDateTime.ofInstant(Instant.ofEpochSecond(file.modifiedTime / 1000, 0), ZoneOffset.UTC);\n                            CopyOp op = new CopyOp(false, localFS.resolve(file.relPath),\n                                    remoteFS.resolve(file.relPath), new FileState(file.relPath, file.modifiedTime, file.size, hashTree), null,\n                                    0, file.size, ResumeUploadProps.random(crypto));\n                            syncedVersions.startCopies(List.of(op));\n                            remoteFS.setBytes(p, 0, localFS.getBytes(p, 0), file.size, Optional.of(hashTree),\n                                    Optional.of(modified), localFS.getThumbnail(p), op.props, isCancelled, LOG);\n                            syncedVersions.finishCopies(List.of(op));\n                            syncedVersions.add(new FileState(file.relPath, file.modifiedTime, file.size, hashTree));\n                            progress.doneFile();\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    } else {\n                        HashTree remoteHash = remoteFS.hashFile(p, Optional.empty(), file.relPath, syncedVersions);\n                        HashTree localHash = localFS.hashFile(p, Optional.empty(), file.relPath, syncedVersions);\n                        if (localHash.equals(remoteHash)) {\n                            syncedVersions.add(new FileState(file.relPath, file.modifiedTime, file.size, localHash));\n                            progress.doneFile();\n                            LOG.accept(\"Skipping identical remote file in initial sync: \" + file.relPath + \" \" + progress);\n                        }\n                    }\n                }\n            }, dir -> {});\n        }\n\n        SyncState localState = new RamTreeState();\n        long t1 = System.currentTimeMillis();\n        buildDirState(localFS, localState, syncedVersions);\n        long t2 = System.currentTimeMillis();\n        LOG.accept(\"Found \" + localState.filesCount() + \" local files in \" + (t2-t1)/1_000 + \"s\");\n\n        Snapshot syncedVersion = syncedVersions.getSnapshot(remoteFS.getRoot());\n        Snapshot remoteVersion = network == null ?\n                new Snapshot(new HashMap<>()) :\n                Futures.reduceAll(syncedVersion.versions.keySet(),\n                        new Snapshot(new HashMap<>()),\n                        (v, w) -> v.withWriter(owner, w, network),\n                        Snapshot::mergeAndOverwriteWith).join();\n\n        boolean remoteChange = ! remoteVersion.equals(syncedVersion) || remoteVersion.versions.isEmpty();\n        SyncState remoteState = remoteChange ? new RamTreeState() : syncedVersions;\n        long t3 = System.currentTimeMillis();\n        if (remoteChange)\n            remoteVersion = buildDirState(remoteFS, remoteState, syncedVersions);\n        long t4 = System.currentTimeMillis();\n        LOG.accept(\"Found \" + remoteState.filesCount() + \" remote files in \" + (t4-t3)/1_000 + \"s\");\n\n        TreeSet<String> allPaths = new TreeSet<>(localState.allFilePaths());\n        allPaths.addAll(remoteState.allFilePaths());\n        TreeSet<String> allChangedPaths = new TreeSet<>();\n\n        // remove identical paths\n        for (String path : allPaths) {\n            FileState local = localState.byPath(path);\n            FileState remote = remoteState.byPath(path);\n            FileState synced = syncedVersions.byPath(path);\n            if (Objects.equals(local, remote)) {\n                if (synced == null)\n                    syncedVersions.add(local);\n                // already synced\n                if (! syncLocalDeletes && syncedVersions.hasLocalDelete(path))\n                    syncedVersions.removeLocalDelete(path);\n                if (! syncRemoteDeletes && syncedVersions.hasRemoteDelete(path))\n                    syncedVersions.removeRemoteDelete(path);\n            } else if (synced != null && remote != null && local != null &&\n                    remote.hashTree.rootHash.equals(local.hashTree.rootHash) &&\n                    (synced.equalsIgnoreModtime(local) || synced.equalsIgnoreModtime(remote))) {\n                // already synced\n                if (! syncLocalDeletes && syncedVersions.hasLocalDelete(path))\n                    syncedVersions.removeLocalDelete(path);\n                if (! syncRemoteDeletes && syncedVersions.hasRemoteDelete(path))\n                    syncedVersions.removeRemoteDelete(path);\n            } else if (! IGNORED_FILENAMES.contains(PathUtil.get(path).getFileName().toString()))\n                allChangedPaths.add(path);\n        }\n        Set<String> doneFiles = Collections.synchronizedSet(new HashSet<>());\n        SyncProgress progress = new SyncProgress(allChangedPaths.size());\n\n        // upload new small files in a single bulk operation\n        Set<String> smallFiles = new HashSet<>();\n        Set<String> localDeletes = new HashSet<>();\n        for (String relativePath : allChangedPaths) {\n            FileState synced = syncedVersions.byPath(relativePath);\n            FileState local = localState.byPath(relativePath);\n            FileState remote = remoteState.byPath(relativePath);\n            boolean isSmallRemoteCopy = synced == null && remote == null && local.size < Chunk.MAX_SIZE;\n            if (isSmallRemoteCopy) {\n                List<FileState> remoteByHash = remoteState.byHash(local.hashTree.rootHash);\n                List<FileState> localByHash = localState.byHash(local.hashTree.rootHash);\n                List<FileState> extraRemote = remoteByHash.stream()\n                        .filter(f -> ! localByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                List<FileState> extraLocal = localByHash.stream()\n                        .filter(f -> ! remoteByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                // This index is deterministic because of the sorting\n                int index = extraLocal.indexOf(local);\n\n                Optional<FileState> localAtHashedPath = extraRemote.size() == extraLocal.size() ?\n                        Optional.ofNullable(localState.byPath(extraRemote.get(index).relPath)) :\n                        Optional.empty();\n                if (extraRemote.size() != extraLocal.size() || localAtHashedPath.isPresent())\n                    smallFiles.add(relativePath);\n            }\n\n            boolean isLocalDelete = local == null &&\n                    remote != null &&\n                    synced != null &&\n                    remote.equalsIgnoreModtime(synced);\n            if (isLocalDelete ) {\n                List<FileState> remoteByHash = remoteState.byHash(remote.hashTree.rootHash);\n                List<FileState> localByHash = localState.byHash(remote.hashTree.rootHash);\n                List<FileState> extraLocal = localByHash.stream()\n                        .filter(f -> ! remoteByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                List<FileState> extraRemote = remoteByHash.stream()\n                        .filter(f -> ! localByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                int index = extraRemote.indexOf(remote);\n\n                Optional<FileState> remoteAtHashedPath = extraLocal.size() == extraRemote.size() ?\n                        Optional.ofNullable(remoteState.byPath(extraLocal.get(index).relPath)) :\n                        Optional.empty();\n                if (extraLocal.size() != extraRemote.size() || remoteAtHashedPath.isPresent())\n                    localDeletes.add(relativePath);\n            }\n        }\n        if (isCancelled.get())\n            return;\n\n        if (! smallFiles.isEmpty()) {\n\n            LOG.accept(\"Remote: bulk uploading \" + smallFiles.size() + \" small files\");\n            Map<String, FileWrapper.FolderUploadProperties> folders = new HashMap<>();\n            for (String relPath : smallFiles) {\n                doneFiles.add(relPath);\n                String folderPath = relPath.contains(\"/\") ? relPath.substring(0, relPath.lastIndexOf(\"/\")) : \"\";\n                FileWrapper.FolderUploadProperties folder = folders.get(folderPath);\n                if (folder == null) {\n                    List<String> relativePath = folderPath.isEmpty() ? Collections.emptyList() : Arrays.asList(folderPath.split(\"/\"));\n                    folder = new FileWrapper.FolderUploadProperties(relativePath, new ArrayList<>());\n                    folders.put(folderPath, folder);\n                }\n                String filename = relPath.substring(relPath.lastIndexOf(\"/\") + 1);\n                FileState local = localState.byPath(relPath);\n                if (relPath.contains(\"/\"))\n                    remoteState.addDir(relPath.substring(0, relPath.length() - filename.length() - 1));\n                AtomicBoolean uploadStarted = new AtomicBoolean(false);\n                AtomicBoolean uploadEnded = new AtomicBoolean(false);\n                LocalDateTime modificationTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(local.modificationTime / 1000, 0), ZoneOffset.UTC);\n                folder.files.add(new FileWrapper.FileUploadProperties(filename,\n                        () -> {\n                            try {\n                                return localFS.getBytes(localFS.resolve(relPath), 0);\n                            } catch (IOException e) {\n                                throw new RuntimeException(e);\n                            }\n                        },\n                        (int) (local.size >> 32), (int) local.size, Optional.of(modificationTime), Optional.of(local.hashTree), false, true,\n                        x -> {\n                            if (!uploadStarted.get()) {\n                                LOG.accept(\"REMOTE: Uploading \" + relPath + \" \" + progress);\n                                uploadStarted.set(true);\n                            }\n                            if (! uploadEnded.get()) {\n                                progress.doneFile();\n                                uploadEnded.set(true);\n                            }\n                        }));\n            }\n            remoteFS.uploadSubtree(folders.values().stream());\n            for (String relPath : smallFiles) {\n                syncedVersions.add(localState.byPath(relPath));\n            }\n        }\n        if (isCancelled.get())\n            return;\n\n        // do deletes in bulk\n        if (! localDeletes.isEmpty()) {\n            Map<String, Set<String>> byFolder = new HashMap<>();\n            for (String relPath : localDeletes) {\n                doneFiles.add(relPath);\n                if (! syncLocalDeletes) {\n                    LOG.accept(\"Sync ignore local delete \" + relPath + \" \" + progress);\n                    syncedVersions.addLocalDelete(relPath);\n                } else {\n                    String folderPath = relPath.contains(\"/\") ? relPath.substring(0, relPath.lastIndexOf(\"/\")) : \"\";\n                    Set<String> folder = byFolder.get(folderPath);\n                    if (folder == null) {\n                        folder = new HashSet<>();\n                        byFolder.put(folderPath, folder);\n                    }\n                    String filename = relPath.substring(relPath.lastIndexOf(\"/\") + 1);\n                    folder.add(filename);\n                }\n            }\n            byFolder.forEach((dir, files) -> {\n                LOG.accept(\"REMOTE: bulk deleting \" + files.size() + \" from \" + remoteFS.resolve(dir));\n                remoteFS.bulkDelete(remoteFS.resolve(dir), files);\n                for (String file : files) {\n                    String path = dir + (dir.isEmpty() ? \"\" : \"/\") + file;\n                    progress.doneFile();\n                    LOG.accept(\"REMOTE: deleted \" + path + \" \" + progress);\n                    syncedVersions.remove(path);\n                }\n            });\n        }\n\n        List<ForkJoinTask<?>> downloads = new ArrayList<>();\n        AtomicInteger maxDownloadConcurrency = new AtomicInteger(maxParallelism);\n\n        for (String relativePath : allChangedPaths) {\n            if (doneFiles.contains(relativePath))\n                continue;\n            if (isCancelled.get())\n                return;\n            FileState synced = syncedVersions.byPath(relativePath);\n            FileState local = localState.byPath(relativePath);\n            FileState remote = remoteState.byPath(relativePath);\n            boolean isLocalCopy = synced == null && local == null;\n            if (isLocalCopy) {\n                while (maxDownloadConcurrency.get() == 0) {\n                    try {\n                        Thread.sleep(100);\n                    } catch (InterruptedException e) {\n                    }\n                }\n                maxDownloadConcurrency.decrementAndGet();\n                downloads.add(ForkJoinPool.commonPool().submit(() -> {\n                    try {\n                        syncFile(synced, local, remote, localFS, remoteFS, syncedVersions,\n                                localState, remoteState, syncLocalDeletes, syncRemoteDeletes, doneFiles, minPercentFreeSpace, crypto, isCancelled, LOG, progress);\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    } finally {\n                        maxDownloadConcurrency.incrementAndGet();\n                    }\n                }));\n            } else\n                syncFile(synced, local, remote, localFS, remoteFS, syncedVersions, localState,\n                        remoteState, syncLocalDeletes, syncRemoteDeletes, doneFiles, minPercentFreeSpace, crypto, isCancelled, LOG, progress);\n        }\n\n        for (ForkJoinTask<?> download : downloads) {\n            download.join();\n        }\n\n        // all files are in sync, now sync dirs\n        Comparator<String> longestFirst = (a, b) -> -a.compareTo(b);\n        SortedSet<String> allDirs = new TreeSet<>(longestFirst);\n        allDirs.addAll(remoteState.getDirs());\n        allDirs.addAll(localState.getDirs());\n        allDirs.addAll(syncedVersions.getDirs());\n        for (String dirPath : allDirs) {\n            boolean hasLocal = localState.hasDir(dirPath);\n            boolean hasRemote = remoteState.hasDir(dirPath);\n            boolean hasSynced = syncedVersions.hasDir(dirPath);\n            if (hasLocal && hasRemote) {\n                syncedVersions.addDir(dirPath);\n            } else if (!hasLocal && !hasRemote) {\n                syncedVersions.removeDir(dirPath);\n            } else if (hasLocal) {\n                if (hasSynced) { // delete\n                    if (syncRemoteDeletes) {\n                        LOG.accept(\"Sync local: delete dir \" + dirPath);\n                        localFS.delete(localFS.resolve(dirPath));\n                        syncedVersions.removeDir(dirPath);\n                    }\n                } else {\n                    LOG.accept(\"Sync Remote: mkdir \" + dirPath);\n                    remoteFS.mkdirs(remoteFS.resolve(dirPath));\n                    syncedVersions.addDir(dirPath);\n                }\n            } else {\n                if (hasSynced) { // delete\n                    if (syncLocalDeletes) {\n                        LOG.accept(\"Sync Remote: delete dir \" + dirPath);\n                        remoteFS.delete(remoteFS.resolve(dirPath));\n                        syncedVersions.removeDir(dirPath);\n                    }\n                } else {\n                    LOG.accept(\"Sync Local: mkdir \" + dirPath);\n                    localFS.mkdirs(localFS.resolve(dirPath));\n                    syncedVersions.addDir(dirPath);\n                }\n            }\n        }\n\n        syncedVersions.setSnapshot(remoteFS.getRoot(), remoteVersion);\n        syncedVersions.setCompletedSync(true);\n    }\n\n    public static void log(String msg) {\n        System.out.println(msg);\n        LOG.info(msg);\n    }\n\n    public static void syncFile(FileState synced, FileState local, FileState remote,\n                                SyncFilesystem localFs,\n                                SyncFilesystem remoteFs,\n                                SyncState syncedVersions, SyncState localTree, SyncState remoteTree,\n                                boolean syncLocalDeletes,\n                                boolean syncRemoteDeletes,\n                                Set<String> doneFiles,\n                                int minPercentFree,\n                                Crypto crypto,\n                                Supplier<Boolean> isCancelled,\n                                Consumer<String> LOG,\n                                SyncProgress progress) throws IOException {\n        long totalSpace = localFs.totalSpace();\n        long freeSpace = localFs.freeSpace();\n\n        if (synced == null) {\n            if (local == null) { // remotely added or renamed\n                List<FileState> remoteByHash = remoteTree.byHash(remote.hashTree.rootHash);\n                List<FileState> localByHash = localTree.byHash(remote.hashTree.rootHash);\n                List<FileState> extraLocal = localByHash.stream()\n                        .filter(f -> ! remoteByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                List<FileState> extraRemote = remoteByHash.stream()\n                        .filter(f -> ! localByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                int index = extraRemote.indexOf(remote);\n\n                Optional<FileState> remoteAtHashedPath = extraLocal.size() == extraRemote.size() ?\n                        Optional.ofNullable(remoteTree.byPath(extraLocal.get(index).relPath)) :\n                        Optional.empty();\n\n                if (extraLocal.size() == extraRemote.size() && remoteAtHashedPath.isEmpty()) {// rename\n                    FileState toMove = extraLocal.get(index);\n                    LOG.accept(\"Sync Local: Moving \" + toMove.relPath + \" ==> \" + remote.relPath + \" \" + progress);\n                    localFs.moveTo(localFs.resolve(toMove.relPath), localFs.resolve(remote.relPath));\n                    syncedVersions.remove(toMove.relPath);\n                    doneFiles.add(toMove.relPath);\n                    syncedVersions.add(remote);\n                    progress.doneFile();\n                    progress.doneFile();\n                } else {\n                    if ((freeSpace - remote.size) * 100 / totalSpace < minPercentFree && (freeSpace - remote.size < 5L * 1024*1024*1024))\n                        throw new IllegalStateException(\"Not enough local free space to sync and keep \" + minPercentFree + \"% free or 5 GB free\");\n                    LOG.accept(\"Sync Local: Copying \" + remote.relPath + \" \" + progress);\n                    List<Pair<Long, Long>> diffs = remote.diffRanges(null);\n                    List<CopyOp> ops = diffs.stream()\n                            .map(d -> new CopyOp(true, remoteFs.resolve(remote.relPath), localFs.resolve(remote.relPath), remote, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(remoteFs, localFs, ops, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(remote.withModtime(actualModTime));\n                    progress.doneFile();\n                }\n            } else if (remote == null) { // locally added or renamed\n                List<FileState> remoteByHash = remoteTree.byHash(local.hashTree.rootHash);\n                List<FileState> localByHash = localTree.byHash(local.hashTree.rootHash);\n                List<FileState> extraRemote = remoteByHash.stream()\n                        .filter(f -> ! localByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                List<FileState> extraLocal = localByHash.stream()\n                        .filter(f -> ! remoteByHash.contains(f))\n                        .sorted(Comparator.comparing(f -> f.relPath))\n                        .collect(Collectors.toList());\n                int index = extraLocal.indexOf(local);\n\n                Optional<FileState> localAtHashedPath = extraRemote.size() == extraLocal.size() ?\n                        Optional.ofNullable(localTree.byPath(extraRemote.get(index).relPath)) :\n                        Optional.empty();\n\n                if (extraRemote.size() == extraLocal.size() && localAtHashedPath.isEmpty()) {// rename\n                    FileState toMove = extraRemote.get(index);\n                    LOG.accept(\"Sync Remote: Moving \" + toMove.relPath + \" ==> \" + local.relPath + \" \" + progress);\n                    remoteFs.moveTo(remoteFs.resolve(toMove.relPath), Paths.get(local.relPath));\n                    syncedVersions.remove(toMove.relPath);\n                    doneFiles.add(toMove.relPath);\n                    syncedVersions.add(local);\n                    progress.doneFile();\n                    progress.doneFile();\n                } else {\n                    LOG.accept(\"Sync Remote: Copying \" + local.relPath + \" \" + progress);\n                    List<Pair<Long, Long>> diffs = local.diffRanges(null);\n                    List<CopyOp> ops = diffs.stream()\n                            .map(d -> new CopyOp(false, localFs.resolve(local.relPath), remoteFs.resolve(local.relPath), local, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(localFs, remoteFs, ops, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(local.withModtime(actualModTime));\n                    progress.doneFile();\n                }\n            } else {\n                // concurrent addition, rename 1 if contents are different\n                if (remote.hashTree.rootHash.equals(local.hashTree.rootHash)) {\n                    if (local.modificationTime > remote.modificationTime) {\n                        LOG.accept(\"Remote: Set mod time \" + local.relPath + \" \" + progress);\n                        remoteFs.setModificationTime(remoteFs.resolve(local.relPath), local.modificationTime);\n                        syncedVersions.add(local);\n                        progress.doneFile();\n                        return;\n                    } else if (remote.modificationTime > local.modificationTime) {\n                        LOG.accept(\"Sync Local: Set mod time \" + local.relPath + \" \" + progress);\n                        localFs.setModificationTime(localFs.resolve(local.relPath), remote.modificationTime);\n                        syncedVersions.add(remote);\n                        progress.doneFile();\n                        return;\n                    }\n                    syncedVersions.add(local);\n                } else {\n                    if ((freeSpace - remote.size) * 100 / totalSpace < minPercentFree && (freeSpace - remote.size < 5L * 1024*1024*1024))\n                        throw new IllegalStateException(\"Not enough local free space to sync and keep \" + minPercentFree + \"% free or 5GB free. Conflict on \" + local.relPath);\n                    LOG.accept(\"Sync Remote: Concurrent file addition: \" + local.relPath + \" renaming local version\" + \" \" + progress);\n                    FileState renamed = renameOnConflict(localFs, localFs.resolve(local.relPath), local);\n                    List<Pair<Long, Long>> diffs = remote.diffRanges(null);\n                    List<CopyOp> ops = diffs.stream()\n                            .map(d -> new CopyOp(true, remoteFs.resolve(remote.relPath), localFs.resolve(remote.relPath), remote, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(remoteFs, localFs, ops, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(remote.withModtime(actualModTime));\n\n                    List<Pair<Long, Long>> diffs2 = local.diffRanges(null);\n                    List<CopyOp> ops2 = diffs2.stream()\n                            .map(d -> new CopyOp(false, localFs.resolve(renamed.relPath), remoteFs.resolve(renamed.relPath), renamed, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime2 = copyFileDiffAndTruncate(localFs, remoteFs, ops2, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(renamed.withModtime(actualModTime2));\n                    progress.doneFile();\n                }\n            }\n        } else { // synced != null\n            if (synced.equalsIgnoreModtime(local)) { // remote change only\n                if (remote == null) { // deletion or rename\n                    List<FileState> remoteByHash = remoteTree.byHash(local.hashTree.rootHash);\n                    List<FileState> localByHash = localTree.byHash(local.hashTree.rootHash);\n                    List<FileState> extraRemote = remoteByHash.stream()\n                            .filter(f -> ! localByHash.contains(f))\n                            .sorted(Comparator.comparing(f -> f.relPath))\n                            .collect(Collectors.toList());\n                    List<FileState> extraLocal = localByHash.stream()\n                            .filter(f -> ! remoteByHash.contains(f))\n                            .sorted(Comparator.comparing(f -> f.relPath))\n                            .collect(Collectors.toList());\n                    int index = extraLocal.indexOf(local);\n\n                    Optional<FileState> localAtHashedPath = extraRemote.size() == extraLocal.size() ?\n                            Optional.ofNullable(localTree.byPath(extraRemote.get(index).relPath)) :\n                            Optional.empty();\n                    if (extraRemote.size() == extraLocal.size() && localAtHashedPath.isEmpty()) {// rename\n                        // we will do the local rename when we process the new remote entry\n                    } else {\n                        if (syncRemoteDeletes) {\n                            LOG.accept(\"Sync Local: delete \" + local.relPath + \" \" + progress);\n                            localFs.delete(localFs.resolve(local.relPath));\n                            syncedVersions.remove(local.relPath);\n                        } else {\n                            LOG.accept(\"Sync ignore remote delete \" + local.relPath + \" \" + progress);\n                            syncedVersions.addRemoteDelete(local.relPath);\n                        }\n                        progress.doneFile();\n                    }\n                } else if (remote.hashTree.rootHash.equals(local.hashTree.rootHash)) {\n                    // already synced\n                    if (! syncLocalDeletes && syncedVersions.hasLocalDelete(remote.relPath))\n                        syncedVersions.removeLocalDelete(remote.relPath);\n                    if (! syncRemoteDeletes && syncedVersions.hasRemoteDelete(remote.relPath))\n                        syncedVersions.removeRemoteDelete(remote.relPath);\n                    progress.doneFile();\n                } else {\n                    if (syncedVersions.hasRemoteDelete(remote.relPath)) {\n                        // remote file was deleted, then a different file with same path was added. Rename local and copy remote\n                        LOG.accept(\"Sync Remote: Concurrent change: \" + local.relPath + \" renaming local version\" + \" \" + progress);\n                        FileState renamed = renameOnConflict(localFs, localFs.resolve(local.relPath), local);\n                        List<Pair<Long, Long>> diffs = remote.diffRanges(null);\n                        List<CopyOp> ops = diffs.stream()\n                                .map(d -> new CopyOp(true, remoteFs.resolve(remote.relPath), localFs.resolve(remote.relPath), remote, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                                .collect(Collectors.toList());\n                        Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(remoteFs, localFs, ops, syncedVersions, isCancelled, LOG);\n                        syncedVersions.add(remote.withModtime(actualModTime));\n                        progress.doneFile();\n\n                        List<Pair<Long, Long>> diffs2 = local.diffRanges(null);\n                        List<CopyOp> ops2 = diffs2.stream()\n                                .map(d -> new CopyOp(false, localFs.resolve(renamed.relPath), remoteFs.resolve(renamed.relPath), renamed, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                                .collect(Collectors.toList());\n                        Optional<LocalDateTime> actualModTime2 = copyFileDiffAndTruncate(localFs, remoteFs, ops2, syncedVersions, isCancelled, LOG);\n                        syncedVersions.add(renamed.withModtime(actualModTime2));\n                        syncedVersions.removeRemoteDelete(remote.relPath);\n                        progress.doneFile();\n                    } else {\n                        if (remote.size > local.size && (freeSpace + local.size - remote.size) * 100 / totalSpace < minPercentFree &&\n                                (freeSpace + local.size - remote.size < 5L * 1024*1024*1024))\n                            throw new IllegalStateException(\"Not enough local free space to sync and keep \" + minPercentFree + \"% free or 5 GiB free\");\n                        LOG.accept(\"Sync Local: Copying changes to \" + remote.relPath + \" \" + progress);\n                        List<Pair<Long, Long>> diffs = remote.diffRanges(local);\n                        List<CopyOp> ops = diffs.stream()\n                                .map(d -> new CopyOp(true, remoteFs.resolve(remote.relPath), localFs.resolve(remote.relPath), remote, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                                .collect(Collectors.toList());\n                        Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(remoteFs, localFs, ops, syncedVersions, isCancelled, LOG);\n                        syncedVersions.add(remote.withModtime(actualModTime));\n                        progress.doneFile();\n                    }\n                }\n            } else if (synced.equalsIgnoreModtime(remote)) { // local only change\n                if (local == null) { // deletion or rename\n                    List<FileState> remoteByHash = remoteTree.byHash(remote.hashTree.rootHash);\n                    List<FileState> localByHash = localTree.byHash(remote.hashTree.rootHash);\n                    List<FileState> extraLocal = localByHash.stream()\n                            .filter(f -> ! remoteByHash.contains(f))\n                            .sorted(Comparator.comparing(f -> f.relPath))\n                            .collect(Collectors.toList());\n                    List<FileState> extraRemote = remoteByHash.stream()\n                            .filter(f -> ! localByHash.contains(f))\n                            .sorted(Comparator.comparing(f -> f.relPath))\n                            .collect(Collectors.toList());\n                    int index = extraRemote.indexOf(remote);\n\n                    Optional<FileState> remoteAtHashedPath = extraLocal.size() == extraRemote.size() ?\n                            Optional.ofNullable(remoteTree.byPath(extraLocal.get(index).relPath)) :\n                            Optional.empty();\n                    if (extraLocal.size() == extraRemote.size() && remoteAtHashedPath.isEmpty()) {// rename\n                        // we will do the local rename when we process the new remote entry\n                    } else {\n                        if (syncLocalDeletes) {\n                            LOG.accept(\"Sync Remote: delete \" + remote.relPath + \" \" + progress);\n                            remoteFs.delete(remoteFs.resolve(remote.relPath));\n                            syncedVersions.remove(remote.relPath);\n                        } else {\n                            LOG.accept(\"Sync ignore local delete \" + remote.relPath + \" \" + progress);\n                            syncedVersions.addLocalDelete(remote.relPath);\n                        }\n                        progress.doneFile();\n                    }\n                } else if (remote.hashTree.rootHash.equals(local.hashTree.rootHash)) {\n                    // already synced\n                    if (! syncLocalDeletes && syncedVersions.hasLocalDelete(remote.relPath))\n                        syncedVersions.removeLocalDelete(remote.relPath);\n                    if (! syncRemoteDeletes && syncedVersions.hasRemoteDelete(remote.relPath))\n                        syncedVersions.removeRemoteDelete(remote.relPath);\n                    progress.doneFile();\n                } else {\n                    if (syncedVersions.hasLocalDelete(local.relPath)) {\n                        // local file was deleted, then a different file with same path was added. Keep remote, rename local.\n                        LOG.accept(\"Sync Remote: Concurrent change: \" + local.relPath + \" renaming different local version after local delete\" + \" \" + progress);\n                        FileState renamed = renameOnConflict(localFs, localFs.resolve(local.relPath), local);\n                        List<Pair<Long, Long>> diffs = remote.diffRanges(null);\n                        List<CopyOp> ops = diffs.stream()\n                                .map(d -> new CopyOp(true, remoteFs.resolve(remote.relPath), localFs.resolve(remote.relPath), remote, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                                .collect(Collectors.toList());\n                        Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(remoteFs, localFs, ops, syncedVersions, isCancelled, LOG);\n                        syncedVersions.add(remote.withModtime(actualModTime));\n                        progress.doneFile();\n\n                        List<Pair<Long, Long>> diffs2 = local.diffRanges(null);\n                        List<CopyOp> ops2 = diffs2.stream()\n                                .map(d -> new CopyOp(false, localFs.resolve(renamed.relPath), remoteFs.resolve(renamed.relPath), renamed, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                                .collect(Collectors.toList());\n                        Optional<LocalDateTime> actualModTime2 = copyFileDiffAndTruncate(localFs, remoteFs, ops2, syncedVersions, isCancelled, LOG);\n                        syncedVersions.add(renamed.withModtime(actualModTime2));\n                        syncedVersions.removeLocalDelete(local.relPath);\n                        progress.doneFile();\n                    } else {\n                        LOG.accept(\"Sync Remote: Copying changes to \" + local.relPath + \" \" + progress);\n                        List<Pair<Long, Long>> diffs = local.diffRanges(remote);\n                        List<CopyOp> ops = diffs.stream()\n                                .map(d -> new CopyOp(false, localFs.resolve(local.relPath), remoteFs.resolve(local.relPath), local, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                                .collect(Collectors.toList());\n                        Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(localFs, remoteFs, ops, syncedVersions, isCancelled, LOG);\n                        remoteFs.setHash(remoteFs.resolve(local.relPath), local.hashTree, local.size);\n                        syncedVersions.add(local.withModtime(actualModTime));\n                        progress.doneFile();\n                    }\n                }\n            } else { // concurrent change/deletion\n                if (local == null && remote == null) {// concurrent deletes\n                    LOG.accept(\"Sync Concurrent delete on \" + synced.relPath + \" \" + progress);\n                    syncedVersions.remove(synced.relPath);\n                    progress.doneFile();\n                    return;\n                }\n                if (local == null) { // local delete, copy changed remote\n                    if ((freeSpace - remote.size) * 100 / totalSpace < minPercentFree && (freeSpace - remote.size < 5L * 1024*1024*1024))\n                        throw new IllegalStateException(\"Not enough local free space to sync and keep \" + minPercentFree + \"% free\");\n                    LOG.accept(\"Sync Local: deleted, copying changed remote \" + remote.relPath + \", Synced: \" + synced.prettyPrint() + \", remote: \" + remote.prettyPrint() + \" \" + progress);\n                    List<Pair<Long, Long>> diffs = remote.diffRanges(null);\n                    List<CopyOp> ops = diffs.stream()\n                            .map(d -> new CopyOp(true, remoteFs.resolve(remote.relPath), localFs.resolve(remote.relPath), remote, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(remoteFs, localFs, ops, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(remote.withModtime(actualModTime));\n                    progress.doneFile();\n                    if (! syncLocalDeletes && syncedVersions.hasLocalDelete(remote.relPath))\n                        syncedVersions.removeLocalDelete(remote.relPath);\n                    return;\n                }\n                if (remote == null) { // remote delete, copy changed local\n                    LOG.accept(\"Sync Remote: deleted, copying changed local \" + local.relPath + \" \" + progress);\n                    List<Pair<Long, Long>> diffs = local.diffRanges(null);\n                    List<CopyOp> ops = diffs.stream()\n                            .map(d -> new CopyOp(false, localFs.resolve(local.relPath), remoteFs.resolve(local.relPath), local, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(localFs, remoteFs, ops, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(local.withModtime(actualModTime));\n                    progress.doneFile();\n                    if (! syncRemoteDeletes && syncedVersions.hasRemoteDelete(local.relPath))\n                        syncedVersions.removeRemoteDelete(local.relPath);\n                    return;\n                }\n                // concurrent change, rename one sync the other\n                if ((freeSpace - remote.size) * 100 / totalSpace < minPercentFree && (freeSpace - remote.size < 5L * 1024*1024*1024))\n                    throw new IllegalStateException(\"Not enough local free space to sync and keep \" + minPercentFree + \"% free or 5 GiB free\");\n                // if local and remote are the same, update sync and return\n                if (local.equals(remote)) {\n                    syncedVersions.add(local);\n                } else if (synced.hashTree.rootHash.equals(remote.hashTree.rootHash)) {\n                    // synced content is same as remote, so just a local change\n                    LOG.accept(\"Sync Remote: Copying changes to \" + local.relPath + \" \" + progress);\n                    List<Pair<Long, Long>> diffs = local.diffRanges(remote);\n                    List<CopyOp> ops = diffs.stream()\n                            .map(d -> new CopyOp(false, localFs.resolve(local.relPath), remoteFs.resolve(local.relPath), local, remote, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(localFs, remoteFs, ops, syncedVersions, isCancelled, LOG);\n                    remoteFs.setHash(remoteFs.resolve(local.relPath), local.hashTree, local.size);\n                    syncedVersions.add(local.withModtime(actualModTime));\n                    progress.doneFile();\n                } else {\n                    LOG.accept(\"Sync Remote: Concurrent change: \" + local.relPath + \" renaming local version\" + \" \" + progress);\n                    FileState renamed = renameOnConflict(localFs, localFs.resolve(local.relPath), local);\n                    List<Pair<Long, Long>> diffs = remote.diffRanges(null);\n                    List<CopyOp> ops = diffs.stream()\n                            .map(d -> new CopyOp(true, remoteFs.resolve(remote.relPath), localFs.resolve(remote.relPath), remote, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime = copyFileDiffAndTruncate(remoteFs, localFs, ops, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(remote.withModtime(actualModTime));\n                    progress.doneFile();\n\n                    List<Pair<Long, Long>> diffs2 = local.diffRanges(null);\n                    List<CopyOp> ops2 = diffs2.stream()\n                            .map(d -> new CopyOp(false, localFs.resolve(renamed.relPath), remoteFs.resolve(renamed.relPath), renamed, null, d.left, d.right, ResumeUploadProps.random(crypto)))\n                            .collect(Collectors.toList());\n                    Optional<LocalDateTime> actualModTime2 = copyFileDiffAndTruncate(localFs, remoteFs, ops2, syncedVersions, isCancelled, LOG);\n                    syncedVersions.add(renamed.withModtime(actualModTime2));\n                    progress.doneFile();\n                }\n            }\n        }\n    }\n\n    public static FileState renameOnConflict(SyncFilesystem fs, Path f, FileState s) {\n        String name = f.getFileName().toString();\n        String newName;\n        if (name.contains(\"[conflict-\")) {\n            int start = name.lastIndexOf(\"[conflict-\");\n            int end = name.indexOf(\"]\", start);\n            int version = Integer.parseInt(name.substring(start + \"[conflict-\".length(), end));\n            while (true) {\n                newName = name.substring(0, start) + \"[conflict-\" + (version + 1) + \"]\" + name.substring(end + 1);\n                if (! fs.exists(getParent(f).resolve(newName)))\n                    break;\n                version++;\n            }\n        } else {\n            int version = 0;\n            while (true) {\n                if (name.contains(\".\")) {\n                    int dot = name.lastIndexOf(\".\");\n                    newName = name.substring(0, dot) + \"[conflict-\" + version + \"]\" + name.substring(dot);\n                } else\n                    newName = name + \"[conflict-\" + version + \"]\";\n                if (! fs.exists(getParent(f).resolve(newName)))\n                    break;\n                version++;\n            }\n        }\n        Path newFile = getParent(f).resolve(newName);\n        fs.moveTo(f, newFile);\n        long newModified = fs.getLastModified(newFile);\n        return new FileState(s.relPath.substring(0, s.relPath.length() - name.length()) + newName, newModified, s.size, s.hashTree);\n    }\n\n    public static Optional<LocalDateTime> copyFileDiffAndTruncate(SyncFilesystem srcFs,\n                                                                  SyncFilesystem targetFs,\n                                                                  List<CopyOp> ops,\n                                                                  SyncState syncDb,\n                                                                  Supplier<Boolean> isCancelled,\n                                                                  Consumer<String> progress) throws IOException {\n        // first write the operation to the db\n        syncDb.startCopies(ops);\n\n        Optional<LocalDateTime> res = Optional.empty();\n\n        for (CopyOp op : ops) {\n            Optional<LocalDateTime> mod = applyCopyOp(srcFs, targetFs, op, isCancelled, progress);\n            res = res.isEmpty() ? mod : mod.isEmpty() ? res : res.map(t -> mod.get().isAfter(t) ? mod.get() : t);\n        }\n\n        // now remove the operation from the db\n        syncDb.finishCopies(ops);\n        return res;\n    }\n\n    public static Optional<LocalDateTime> applyCopyOp(SyncFilesystem srcFs, SyncFilesystem targetFs, CopyOp op, Supplier<Boolean> isCancelled, Consumer<String> progress) throws IOException {\n        if (isCancelled.get())\n            return Optional.empty();\n        log(\"COPY from \" + op.source + \" to \" + op.target + \" range=[\" + op.diffStart +\", \" + op.diffEnd+\"]\");\n        targetFs.mkdirs(getParent(op.target));\n        long priorSize = op.targetState != null ? op.targetState.size : 0;\n        long size = op.sourceState.size;\n        long lastModified = op.sourceState.modificationTime;\n\n        long start = op.diffStart;\n        long end = op.diffEnd;\n        Optional<LocalDateTime> res;\n        try (AsyncReader fin = srcFs.getBytes(op.source, start)) {\n            Optional<Thumbnail> thumbnail = srcFs.getThumbnail(op.source);\n            LocalDateTime modified = LocalDateTime.ofInstant(Instant.ofEpochSecond(lastModified / 1000, 0), ZoneOffset.UTC);\n            res = targetFs.setBytes(op.target, start, fin, end - start, Optional.of(op.sourceState.hashTree),\n                    Optional.of(modified), thumbnail, op.props, isCancelled, progress);\n        }\n        if (isCancelled.get())\n            return res;\n        if (priorSize > size) {\n            log(\"Sync Truncating file \" + op.sourceState.relPath + \" from \" + priorSize + \" to \" + size);\n            targetFs.truncate(op.target, size);\n        }\n        return res;\n    }\n\n    private static class SnapshotTracker {\n        private Snapshot s;\n\n        public SnapshotTracker(Snapshot s) {\n            this.s = s;\n        }\n\n        public synchronized void update(Snapshot s2) {\n            s = s.mergeAndOverwriteWith(s2);\n        }\n\n        public synchronized Snapshot get() {\n            return s;\n        }\n    }\n\n    public static Snapshot buildDirState(SyncFilesystem fs, SyncState res, SyncState synced) throws IOException {\n        SnapshotTracker version = new SnapshotTracker(new Snapshot(new HashMap<>()));\n        List<Triple<String, FileWrapper, HashTree>> toUpdate = new ArrayList<>();\n        AtomicLong downloadedSize = new AtomicLong(0);\n\n        Optional<PublicKeyHash> baseDirWriter = fs.applyToSubtree(props -> {\n            String relPath = props.relPath;\n            FileState atSync = synced.byPath(relPath);\n            if (atSync != null && atSync.modificationTime == props.modifiedTime && atSync.size == props.size) {\n                res.add(atSync);\n                if (props.meta.isPresent())\n                    version.update(props.meta.get().version);\n            } else {\n                HashTree hashTree = fs.hashFile(PathUtil.get(props.relPath), props.meta, relPath, synced);\n                if (props.meta.isPresent()) {\n                    version.update(props.meta.get().version);\n                    Optional<HashBranch> remoteHash = props.meta.get().getFileProperties().treeHash;\n                    if (!remoteHash.isPresent()) {\n                        // collect new hashes to set in bulk later\n                        toUpdate.add(new Triple<>(relPath, props.meta.get(), hashTree));\n                        downloadedSize.addAndGet(props.size);\n                        if (downloadedSize.get() > 100 * 1024 * 1024L) {\n                            // set hashes inline if we've downloaded a lot of data to avoid cache thrashing if there is\n                            // an exception. This way we continue to make progress.\n                            log(\"REMOTE: Updating \" + toUpdate.size() + \" hashes: \" + toUpdate.stream().limit(10).map(p -> p.left).collect(Collectors.toList()));\n                            fs.setHashes(toUpdate);\n                            toUpdate.clear();\n                            downloadedSize.set(0);\n                        }\n                    }\n                }\n                FileState fstat = new FileState(relPath, props.modifiedTime, props.size, hashTree);\n                if (atSync != null && atSync.equalsIgnoreModtime(fstat)) {\n                    res.add(atSync);\n                } else\n                    res.add(fstat);\n            }\n        }, p -> {\n            String relPath = p.relPath;\n            p.meta.ifPresent(d -> version.update(d.version));\n            res.addDir(relPath);\n        });\n        if (! toUpdate.isEmpty()) {\n            log(\"REMOTE: Updating \" + toUpdate.size() + \" hashes: \" + toUpdate.stream().limit(10).map(p -> p.left).collect(Collectors.toList()));\n            fs.setHashes(toUpdate);\n        }\n        // don't track the entry point writer which we only have read access to\n        if (baseDirWriter.isEmpty())\n            return version.get();\n        return version.get().remove(baseDirWriter.get());\n    }\n\n    private static Path getParent(Path p) {\n        Path parent = p.getParent();\n        if (parent == null)\n            return Paths.get(\"\");\n        return parent;\n    }\n}"
  },
  {
    "path": "src/peergos/server/sync/FileState.java",
    "content": "package peergos.server.sync;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.ArrayOps;\nimport peergos.shared.util.Pair;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.LongStream;\n\npublic class FileState implements Cborable {\n    public final String relPath;\n    public final long modificationTime;\n    public final long size;\n    public final HashTree hashTree;\n\n    public FileState(String relPath, long modificationTime, long size, HashTree hashTree) {\n        if (relPath.contains(\"..\")) {\n            if (Arrays.asList(relPath.split(\"/\")).contains(\"..\"))\n                throw new IllegalStateException(\"Invalid path: \" + relPath);\n        }\n        this.relPath = relPath;\n        this.modificationTime = modificationTime;\n        this.size = size;\n        this.hashTree = hashTree;\n    }\n\n    public FileState withModtime(Optional<LocalDateTime> modtime) {\n        return new FileState(relPath, modtime.map(t -> t.toInstant(ZoneOffset.UTC).toEpochMilli() / 1000 * 1000).orElse(modificationTime), size, hashTree);\n    }\n\n    public String prettyPrint() {\n        return \"[\" + relPath + \", size: \" + size + \", modTime: \" + modificationTime + \", hash: \" + ArrayOps.bytesToHex(hashTree.rootHash.hash)+\"]\";\n    }\n\n    public List<Pair<Long, Long>> diffRanges(FileState other) {\n        if (other == null)\n            return List.of(new Pair<>(0L, size));\n        if (hashTree.rootHash.equals(other.hashTree.rootHash))\n            return Collections.emptyList();\n\n        List<ChunkHashList> a = hashTree.level1;\n        List<ChunkHashList> b = other.hashTree.level1;\n        List<Long> diffChunks = new ArrayList<>();\n        for (int i=0; i < a.size(); i++) {\n            ChunkHashList aList = a.get(i);\n            ChunkHashList bList = b.get(i);\n            if (bList == null) {\n                diffChunks.addAll(LongStream.range(0, aList.nChunks())\n                        .mapToObj(x -> x)\n                        .collect(Collectors.toList()));\n            } else {\n                for (int j=0; j < aList.nChunks(); j++){\n                    if (! aList.equalAt(j, bList))\n                        diffChunks.add(i * 1024L + j);\n                }\n                for (int j= aList.nChunks(); j < bList.nChunks(); j++)\n                    diffChunks.add(i * 1024L + j);\n            }\n        }\n        return diffChunks.stream()\n                .map(c -> new Pair<>(c * Chunk.MAX_SIZE, Math.min((c + 1) * Chunk.MAX_SIZE, size)))\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"r\", new CborObject.CborString(relPath));\n        state.put(\"m\", new CborObject.CborLong(modificationTime));\n        state.put(\"s\", new CborObject.CborLong(size));\n        state.put(\"h\", hashTree.toCbor());\n\n        return CborObject.CborMap.build(state);\n    }\n\n    public static FileState fromCbor(Cborable c) {\n        CborObject.CborMap map = (CborObject.CborMap) c;\n        String relPath = map.getString(\"r\");\n        long modTime = map.getLong(\"m\");\n        long size = map.getLong(\"s\");\n        HashTree hash = map.get(\"h\", HashTree::fromCbor);\n        return new FileState(relPath, modTime, size, hash);\n    }\n\n    public boolean equalsIgnoreModtime(FileState other) {\n        return other != null && relPath.equals(other.relPath) && size == other.size && hashTree.equals(other.hashTree);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        FileState fileState = (FileState) o;\n        return modificationTime == fileState.modificationTime && size == fileState.size && Objects.equals(relPath, fileState.relPath) && Objects.equals(hashTree.rootHash, fileState.hashTree.rootHash);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(relPath, modificationTime, size, hashTree.rootHash);\n    }\n\n    @Override\n    public String toString() {\n        return relPath;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/JdbcTreeState.java",
    "content": "package peergos.server.sync;\n\nimport peergos.server.sql.SqlSupplier;\nimport peergos.server.sql.SqliteCommands;\nimport peergos.server.util.Sqlite;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.user.Snapshot;\nimport peergos.shared.user.fs.HashTree;\nimport peergos.shared.user.fs.ResumeUploadProps;\nimport peergos.shared.user.fs.RootHash;\n\nimport java.io.IOException;\nimport java.nio.file.Paths;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.*;\nimport java.util.function.Supplier;\n\npublic class JdbcTreeState implements SyncState {\n    private static final String INSERT_SNAPSHOT = \"INSERT INTO snapshots (path, snapshot) VALUES(?, ?);\";\n    private static final String UPDATE_SNAPSHOT = \"UPDATE snapshots SET snapshot=? WHERE path=?;\";\n    private static final String INSERT = \"INSERT INTO syncstate (path, roothash, modtime, size, hashtree) VALUES(?, ?, ?, ?, ?);\";\n    private static final String INSERT_DIR_SUFFIX = \"INTO syncdirs (path) VALUES(?);\";\n    private static final String SET_DONE_SUFFIX = \"INTO syncdone (key, done) VALUES(?, ?);\";\n    private static final String INSERT_LOCAL_DELETE_SUFFIX = \"INTO synclocaldeletes (path) VALUES(?);\";\n    private static final String INSERT_REMOTE_DELETE_SUFFIX = \"INTO syncremotedeletes (path) VALUES(?);\";\n    private static final String UPDATE = \"UPDATE syncstate SET roothash=?, hashtree=?, modtime=?, size=? WHERE path=?;\";\n    private static final String DELETE = \"DELETE from syncstate WHERE path = ?;\";\n    private static final String DELETE_DIR = \"DELETE from syncdirs WHERE path = ?;\";\n    private static final String DELETE_LOCAL_DELETE = \"DELETE from synclocaldeletes WHERE path = ?;\";\n    private static final String DELETE_REMOTE_DELETE = \"DELETE from syncremotedeletes WHERE path = ?;\";\n    private static final String GET_BY_PATH = \"SELECT path, modtime, size, hashtree FROM syncstate WHERE path = ?;\";\n    private static final String GET_SNAPSHOT = \"SELECT snapshot FROM snapshots WHERE path = ?;\";\n    private static final String COUNT_FILES = \"SELECT COUNT(*) FROM syncstate;\";\n    private static final String ALL_FILE_PATHS = \"SELECT path FROM syncstate;\";\n    private static final String GET_BY_HASH = \"SELECT path, modtime, size, hashtree FROM syncstate WHERE roothash = ?;\";\n    private static final String GET_DIRS = \"SELECT path FROM syncdirs;\";\n    private static final String HAS_DIR = \"SELECT path FROM syncdirs WHERE path=?;\";\n    private static final String DONE_SYNC = \"SELECT done FROM syncdone WHERE key=?;\";\n    private static final String HAS_LOCAL_DELETE = \"SELECT path FROM synclocaldeletes WHERE path=?;\";\n    private static final String HAS_REMOTE_DELETE = \"SELECT path FROM syncremotedeletes WHERE path=?;\";\n    private static final String INSERT_COPY_OP = \"INSERT INTO copyops2 (islocal, source, target, start, end, sourcestate, targetstate, props) VALUES(?, ?, ?, ?, ?, ?, ?, ?);\";\n    private static final String REMOVE_COPY_OP = \"DELETE FROM copyops2 WHERE source=? AND target=? AND start=? AND end=?\";\n    private static final String LIST_COPY_OPS = \"SELECT islocal, source, target, start, end, sourcestate, targetstate, props FROM copyops2;\";\n\n    private final Supplier<Sqlite.UncloseableConnection> conn;\n    private final SqlSupplier cmds = new SqliteCommands();\n\n    public JdbcTreeState(String sqlFile) {\n        try {\n            Connection db = Sqlite.build(sqlFile);\n            // We need a connection that ignores close\n            Sqlite.UncloseableConnection instance = new Sqlite.UncloseableConnection(db);\n            this.conn = () -> instance;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        init();\n    }\n\n    @Override\n    public void close() throws IOException {\n        try {\n            conn.get().closeTarget();\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private Connection getConnection() {\n        Connection connection = conn.get();\n        try {\n            connection.setAutoCommit(true);\n            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);\n            return connection;\n        } catch (SQLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private synchronized void init() {\n        try (Connection conn = getConnection()) {\n            cmds.createTable(\"CREATE TABLE IF NOT EXISTS syncstate (path text primary key not null, roothash blob, modtime bigint not null, size bigint not null, hashtree blob); \" +\n                    \"CREATE INDEX IF NOT EXISTS sync_hash_index ON syncstate (roothash);\" +\n                    \"CREATE INDEX IF NOT EXISTS sync_path_index ON syncstate (path);\", conn);\n            cmds.createTable(\"CREATE TABLE IF NOT EXISTS syncdone (key text primary key not null, done bool not null);\", conn);\n            cmds.createTable(\"CREATE TABLE IF NOT EXISTS syncdirs (path text primary key not null);\", conn);\n            cmds.createTable(\"CREATE TABLE IF NOT EXISTS synclocaldeletes (path text primary key not null);\", conn);\n            cmds.createTable(\"CREATE TABLE IF NOT EXISTS syncremotedeletes (path text primary key not null);\", conn);\n            cmds.createTable(\"CREATE TABLE IF NOT EXISTS snapshots (path text primary key not null, snapshot blob);\", conn);\n            cmds.createTable(\"CREATE TABLE IF NOT EXISTS copyops2 (islocal bool not null, source text not null, target text not null, props blob not null,\" +\n                    \"start \"+cmds.sqlInteger()+\" not null, end \" + cmds.sqlInteger() + \" not null, sourcestate blob, targetstate blob);\", conn);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public boolean hasCompletedSync() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(DONE_SYNC)) {\n            select.setString(1, \"done\");\n            ResultSet rs = select.executeQuery();\n            rs.next();\n            return rs.getBoolean(1);\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void setCompletedSync(boolean done) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", SET_DONE_SUFFIX))) {\n            insert.setString(1, \"done\");\n            insert.setBoolean(2, done);\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public long filesCount() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(COUNT_FILES)) {\n            ResultSet rs = select.executeQuery();\n            rs.next();\n            return rs.getLong(1);\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public Set<String> allFilePaths() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(ALL_FILE_PATHS)) {\n            ResultSet rs = select.executeQuery();\n            Set<String> res = new HashSet<>();\n            while (rs.next())\n                res.add(rs.getString(1));\n            return res;\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void setSnapshot(String basePath, Snapshot s) {\n        if (s.versions.isEmpty())\n            return;\n        Snapshot existing = getSnapshot(basePath);\n        if (existing != null && ! existing.versions.isEmpty()) {\n            try (Connection conn = getConnection();\n                 PreparedStatement update = conn.prepareStatement(UPDATE_SNAPSHOT)) {\n                update.setBytes(1, s.serialize());\n                update.setString(2, basePath);\n                update.executeUpdate();\n            } catch (SQLException sqe) {\n                throw new IllegalStateException(sqe);\n            }\n        } else\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(INSERT_SNAPSHOT)) {\n                insert.setString(1, basePath);\n                insert.setBytes(2, s.serialize());\n                insert.executeUpdate();\n            } catch (SQLException sqe) {\n                throw new IllegalStateException(sqe);\n            }\n    }\n\n    @Override\n    public Snapshot getSnapshot(String basePath) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_SNAPSHOT)) {\n            select.setString(1, basePath);\n            ResultSet rs = select.executeQuery();\n            if (rs.next())\n                return Snapshot.fromCbor(CborObject.fromByteArray(rs.getBytes(1)));\n            return new Snapshot(new HashMap<>());\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void addDir(String relPath) {\n        if (relPath.contains(\"\\\\\"))\n            throw new IllegalStateException(\"Relative paths must be normalised to use /'s not \\\\'s!\");\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", INSERT_DIR_SUFFIX))) {\n            insert.setString(1, relPath);\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void removeDir(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement remove = conn.prepareStatement(DELETE_DIR)) {\n            remove.setString(1, path);\n            remove.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public boolean hasDir(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(HAS_DIR)) {\n            select.setString(1, path);\n            ResultSet rs = select.executeQuery();\n            return (rs.next());\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public Set<String> getDirs() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_DIRS)) {\n            ResultSet rs = select.executeQuery();\n            Set<String> res = new HashSet<>();\n            while (rs.next())\n                res.add(rs.getString(1));\n\n            return res;\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void addLocalDelete(String relPath) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", INSERT_LOCAL_DELETE_SUFFIX))) {\n            insert.setString(1, relPath);\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void removeLocalDelete(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement remove = conn.prepareStatement(DELETE_LOCAL_DELETE)) {\n            remove.setString(1, path);\n            remove.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public boolean hasLocalDelete(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(HAS_LOCAL_DELETE)) {\n            select.setString(1, path);\n            ResultSet rs = select.executeQuery();\n            return (rs.next());\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void addRemoteDelete(String relPath) {\n        try (Connection conn = getConnection();\n             PreparedStatement insert = conn.prepareStatement(cmds.insertOrIgnoreCommand(\"INSERT \", INSERT_REMOTE_DELETE_SUFFIX))) {\n            insert.setString(1, relPath);\n            insert.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void removeRemoteDelete(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement remove = conn.prepareStatement(DELETE_REMOTE_DELETE)) {\n            remove.setString(1, path);\n            remove.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public boolean hasRemoteDelete(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(HAS_REMOTE_DELETE)) {\n            select.setString(1, path);\n            ResultSet rs = select.executeQuery();\n            return (rs.next());\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public FileState byPath(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_BY_PATH)) {\n            select.setString(1, path);\n            ResultSet rs = select.executeQuery();\n            if (rs.next())\n                return new FileState(rs.getString(1), rs.getLong(2), rs.getLong(3), HashTree.fromCbor(CborObject.fromByteArray(rs.getBytes(4))));\n\n            return null;\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void add(FileState fs) {\n        if (fs.relPath.contains(\"\\\\\"))\n            throw new IllegalStateException(\"Relative paths must be normalised to use /'s not \\\\'s!\");\n        FileState existing = byPath(fs.relPath);\n        if (existing != null) {\n            try (Connection conn = getConnection();\n                 PreparedStatement update = conn.prepareStatement(UPDATE)) {\n                update.setBytes(1, fs.hashTree.rootHash.serialize());\n                update.setBytes(2, fs.hashTree.serialize());\n                update.setLong(3, fs.modificationTime);\n                update.setLong(4, fs.size);\n                update.setString(5, fs.relPath);\n                update.executeUpdate();\n            } catch (SQLException sqe) {\n                throw new IllegalStateException(sqe);\n            }\n        } else\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(INSERT)) {\n                insert.setString(1, fs.relPath);\n                insert.setBytes(2, fs.hashTree.rootHash.serialize());\n                insert.setLong(3, fs.modificationTime);\n                insert.setLong(4, fs.size);\n                insert.setBytes(5, fs.hashTree.serialize());\n                insert.executeUpdate();\n            } catch (SQLException sqe) {\n                throw new IllegalStateException(sqe);\n            }\n    }\n\n    @Override\n    public void remove(String path) {\n        try (Connection conn = getConnection();\n             PreparedStatement remove = conn.prepareStatement(DELETE)) {\n            remove.setString(1, path);\n            remove.executeUpdate();\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public List<FileState> byHash(RootHash hash) {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(GET_BY_HASH)) {\n            select.setBytes(1, hash.serialize());\n            ResultSet rs = select.executeQuery();\n            List<FileState> res = new ArrayList<>();\n            while (rs.next())\n                res.add(new FileState(rs.getString(1), rs.getLong(2), rs.getLong(3), HashTree.fromCbor(CborObject.fromByteArray(rs.getBytes(4)))));\n\n            return res;\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n\n    @Override\n    public void startCopies(List<CopyOp> ops) {\n        for (CopyOp op : ops) {\n            try (Connection conn = getConnection();\n                 PreparedStatement insert = conn.prepareStatement(INSERT_COPY_OP)) {\n                insert.setBoolean(1, op.isLocalTarget);\n                insert.setString(2, op.source.toString());\n                insert.setString(3, op.target.toString());\n                insert.setLong(4, op.diffStart);\n                insert.setLong(5, op.diffEnd);\n                insert.setBytes(6, Optional.ofNullable(op.sourceState).map(Cborable::serialize).orElse(null));\n                insert.setBytes(7, Optional.ofNullable(op.targetState).map(Cborable::serialize).orElse(null));\n                insert.setBytes(8, op.props.serialize());\n                insert.executeUpdate();\n            } catch (SQLException sqe) {\n                throw new IllegalStateException(sqe);\n            }\n        }\n    }\n\n    @Override\n    public void finishCopies(List<CopyOp> ops) {\n        for (CopyOp op : ops) {\n            try (Connection conn = getConnection();\n                 PreparedStatement remove = conn.prepareStatement(REMOVE_COPY_OP)) {\n                remove.setString(1, op.source.toString());\n                remove.setString(2, op.target.toString());\n                remove.setLong(3, op.diffStart);\n                remove.setLong(4, op.diffEnd);\n                remove.executeUpdate();\n            } catch (SQLException sqe) {\n                throw new IllegalStateException(sqe);\n            }\n        }\n    }\n\n    @Override\n    public List<CopyOp> getInProgressCopies() {\n        try (Connection conn = getConnection();\n             PreparedStatement select = conn.prepareStatement(LIST_COPY_OPS)) {\n            ResultSet rs = select.executeQuery();\n            List<CopyOp> res = new ArrayList<>();\n            while (rs.next())\n                res.add(new CopyOp(rs.getBoolean(1), Paths.get(rs.getString(2)), Paths.get(rs.getString(3)),\n                        Optional.ofNullable(rs.getBytes(6)).map(b -> FileState.fromCbor(CborObject.fromByteArray(b))).orElse(null),\n                        Optional.ofNullable(rs.getBytes(7)).map(b -> FileState.fromCbor(CborObject.fromByteArray(b))).orElse(null),\n                        rs.getLong(4),\n                        rs.getLong(5),\n                        ResumeUploadProps.fromCbor(CborObject.fromByteArray(rs.getBytes(8)))\n                ));\n\n            return res;\n        } catch (SQLException sqe) {\n            throw new IllegalStateException(sqe);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/LocalFileSystem.java",
    "content": "package peergos.server.sync;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport peergos.server.crypto.hash.ScryptJava;\nimport peergos.server.simulation.FileAsyncReader;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.PathUtil;\nimport peergos.shared.util.Triple;\n\nimport java.io.*;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\n\npublic class LocalFileSystem implements SyncFilesystem {\n\n    private static final Logger LOG = LoggerFactory.getLogger(LocalFileSystem.class);\n    private final Hasher hasher;\n    private final Path root;\n    private final boolean hasBackSlashes;\n\n    public LocalFileSystem(Path root, Hasher hasher) {\n        this.root = root;\n        this.hasher = hasher;\n        if (! root.toFile().exists())\n            throw new IllegalStateException(\"Dir does not exist: \" + root);\n        this.hasBackSlashes = ! root.getFileSystem().getSeparator().equals(\"/\");\n    }\n\n    @Override\n    public long totalSpace() throws IOException {\n        try {\n            return Files.getFileStore(root).getTotalSpace();\n        } catch (Exception e) {\n            return Long.MAX_VALUE;\n        }\n    }\n\n    @Override\n    public long freeSpace() throws IOException {\n        try {\n            return Files.getFileStore(root).getUsableSpace();\n        } catch (Exception e) {\n            return Long.MAX_VALUE;\n        }\n    }\n\n    @Override\n    public String getRoot() {\n        return root.toString();\n    }\n\n    @Override\n    public Path resolve(String p) {\n        return PathUtil.get(p);\n    }\n\n    @Override\n    public boolean exists(Path p) {\n        return root.resolve(p).toFile().exists();\n    }\n\n    @Override\n    public void mkdirs(Path p) {\n        File f = root.resolve(p).toFile();\n        if (f.exists() && f.isDirectory())\n            return;\n        if (!f.mkdirs() && ! f.exists())\n            throw new IllegalStateException(\"Couldn't create \" + root.resolve(p));\n    }\n\n    @Override\n    public void delete(Path p) {\n        p = root.resolve(p);\n        try {\n            if (Files.isDirectory(p))\n                try (Stream<Path> stream = Files.list(p)) {\n                    if (stream.anyMatch(f -> true))\n                        throw new IllegalStateException(\"Trying to delete non empty directory: \" + p);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n        try {\n            Files.delete(p);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void bulkDelete(Path dir, Set<String> children) {\n        children.forEach(kid -> delete(dir.resolve(kid)));\n    }\n\n    @Override\n    public void moveTo(Path src, Path target) {\n        try {\n            Files.createDirectories(root.resolve(target).getParent());\n            Files.move(root.resolve(src), root.resolve(target));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public long getLastModified(Path p) {\n        long millis = root.resolve(p).toFile().lastModified();\n        return millis / 1000 * 1000;\n    }\n\n    @Override\n    public void setModificationTime(Path p, long modificationTime) {\n        root.resolve(p).toFile().setLastModified(modificationTime / 1000 * 1000);\n    }\n\n    @Override\n    public void setHash(Path p, HashTree hashTree, long fileSize) {}\n\n    @Override\n    public void setHashes(List<Triple<String, FileWrapper, HashTree>> toUpdate) {}\n\n    @Override\n    public long size(Path p) {\n        return root.resolve(p).toFile().length();\n    }\n\n    @Override\n    public void truncate(Path p, long size) throws IOException {\n        try (RandomAccessFile raf = new RandomAccessFile(root.resolve(p).toFile(), \"rw\")) {\n            raf.setLength(size);\n        }\n    }\n\n    @Override\n    public Optional<LocalDateTime> setBytes(Path p,\n                                            long fileOffset,\n                                            AsyncReader fin,\n                                            long size,\n                                            Optional<HashTree> hash,\n                                            Optional<LocalDateTime> modificationTime,\n                                            Optional<Thumbnail> thumbnail,\n                                            ResumeUploadProps props,\n                                            Supplier<Boolean> isCancelled,\n                                            Consumer<String> progress) throws IOException {\n        try (RandomAccessFile raf = new RandomAccessFile(root.resolve(p).toFile(), \"rw\")) {\n            raf.seek(fileOffset);\n            byte[] buf = new byte[4096];\n            long done = 0;\n            while (done < size) {\n                int read = fin.readIntoArray(buf, 0, (int) Math.min(buf.length, size - done)).join();\n                raf.write(buf, 0, read);\n                done += read;\n                if (done >= 1024*1024)\n                    progress.accept(\"Downloaded \" + (done/1024/1024) + \" / \" + (size / 1024/1024) + \" MiB of \" + p.getFileName().toString());\n            }\n            if (modificationTime.isPresent()) {\n                long time = modificationTime.get().toInstant(ZoneOffset.UTC).toEpochMilli() / 1000 * 1000;\n                if (time >= 0) {\n                    root.resolve(p).toFile().setLastModified(time);\n                    return modificationTime;\n                } else\n                    return Optional.empty();\n            }\n            return modificationTime;\n        }\n    }\n\n    @Override\n    public AsyncReader getBytes(Path p, long fileOffset) throws IOException {\n        FileAsyncReader reader = new FileAsyncReader(root.resolve(p).toFile());\n        return reader.seek(fileOffset).join();\n    }\n\n    @Override\n    public void uploadSubtree(Stream<FileWrapper.FolderUploadProperties> directories) {\n        byte[] buf = new byte[5*1024*1024];\n        directories.forEach(folder -> {\n            Path dir = root.resolve(folder.path());\n            dir.toFile().mkdirs();\n            for (FileWrapper.FileUploadProperties file : folder.files) {\n                try (AsyncReader reader = file.fileData.get()) {\n                    long written = 0;\n                    try (FileOutputStream fout = new FileOutputStream(dir.resolve(file.filename).toFile())) {\n                        while (written < file.length) {\n                            int read = reader.readIntoArray(buf, 0, (int) Math.min(buf.length, file.length - written)).join();\n                            fout.write(buf, 0, read);\n                            written += read;\n                        }\n                        fout.flush();\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                }\n            }\n        });\n    }\n\n    @Override\n    public Optional<Thumbnail> getThumbnail(Path p) {\n        return ThumbnailGenerator.getVideo().generateVideoThumbnail(root.resolve(p).toFile());\n    }\n\n    @Override\n    public HashTree hashFile(Path p, Optional<FileWrapper> meta, String relPath, SyncState syncedVersions) {\n        return ScryptJava.hashFile(root.resolve(p), hasher);\n    }\n\n    @Override\n    public long filesCount() throws IOException {\n        AtomicLong count = new AtomicLong(0);\n        try (Stream<Path> stream = Files.list(root)) {\n            stream.forEach(p -> {\n                if (Files.isRegularFile(p))\n                    count.incrementAndGet();\n            });\n        }\n        return count.get();\n    }\n\n    @Override\n    public Optional<PublicKeyHash> applyToSubtree(Consumer<FileProps> file, Consumer<FileProps> dir) throws IOException {\n        applyToSubtree(root, file, dir);\n        return Optional.empty();\n    }\n\n    private void applyToSubtree(Path start, Consumer<FileProps> file, Consumer<FileProps> dir) throws IOException {\n        try (Stream<Path> stream = Files.list(start)) {\n            stream.forEach(c -> {\n                String relPath = root.relativize(start.resolve(c.getFileName())).normalize().toString();\n                String canonicalRelPath = hasBackSlashes ? relPath.replaceAll(\"\\\\\\\\\", \"/\") : relPath;\n                FileProps props = new FileProps(canonicalRelPath, c.toFile().lastModified() / 1000 * 1000, c.toFile().length(), Optional.empty());\n                if (Files.isRegularFile(c)) {\n                    file.accept(props);\n                } else if (Files.isDirectory(c)) {\n                    dir.accept(props);\n                    try {\n                        applyToSubtree(start.resolve(c.getFileName()), file, dir);\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/PeergosSyncFS.java",
    "content": "package peergos.server.sync;\n\nimport peergos.server.util.Logging;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.storage.auth.Bat;\nimport peergos.shared.storage.auth.BatId;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.CryptreeNode;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.Pair;\nimport peergos.shared.util.PathUtil;\nimport peergos.shared.util.Triple;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class PeergosSyncFS implements SyncFilesystem {\n    private static final Logger LOG = Logging.LOG();\n    private static final int MAX_CHAMP_GETS = peergos.shared.storage.ContentAddressedStorage.MAX_CHAMP_GETS;\n    private static final int MAX_CONCURRENT_BATCH_FETCHES = 20;\n\n    private final UserContext context;\n    private final Path root;\n    private final Semaphore fetchSemaphore = new Semaphore(MAX_CONCURRENT_BATCH_FETCHES);\n\n    public PeergosSyncFS(UserContext context, Path root) {\n        this.context = context;\n        this.root = root;\n    }\n\n    @Override\n    public long totalSpace() throws IOException {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public long freeSpace() throws IOException {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public String getRoot() {\n        return root.toString();\n    }\n\n    @Override\n    public Path resolve(String p) {\n        return PathUtil.get(p);\n    }\n\n    @Override\n    public boolean exists(Path p) {\n        return context.getByPath(root.resolve(p)).join().isPresent();\n    }\n\n    @Override\n    public void mkdirs(Path p) {\n        if (p == null) // base dir\n            return;\n        Optional<BatId> mirrorBat = context.mirrorBatId();\n        if (exists(p))\n            return;\n        mkdirs(p.getParent());\n        FileWrapper parent = context.getByPath(root.resolve(p).getParent()).join().get();\n        parent.mkdir(p.getFileName().toString(), context.network, false, mirrorBat, context.crypto).join();\n    }\n\n    @Override\n    public void delete(Path p) {\n        Optional<FileWrapper> parentOpt = context.getByPath(root.resolve(p)).join();\n        if (parentOpt.isEmpty())\n            return;\n        FileWrapper f = parentOpt.get();\n        if (f.isDirectory() && f.hasChildren(context.network).join())\n            throw new IllegalStateException(\"Trying to delete non empty directory: \" + p);\n        FileWrapper parent = context.getByPath(root.resolve(p).getParent()).join().get();\n        f.remove(parent, root.resolve(p), context).join();\n    }\n\n    @Override\n    public void bulkDelete(Path dir, Set<String> children) {\n        Optional<FileWrapper> parentOpt = context.getByPath(root.resolve(dir)).join();\n        if (parentOpt.isEmpty())\n            return;\n        FileWrapper parent = parentOpt.get();\n        Set<FileWrapper> kids = parent.getChildren(children, context.crypto.hasher, context.network, false).join();\n        FileWrapper.deleteChildren(parent, kids, dir, context).join();\n    }\n\n    @Override\n    public void moveTo(Path src, Path target) {\n        if (Objects.equals(target.getParent(), src.getParent())) { // rename\n            Optional<FileWrapper> parentOpt = context.getByPath(root.resolve(src).getParent()).join();\n            if (parentOpt.isEmpty())\n                throw new IllegalStateException(\"Couldn't retrieve \" + root.resolve(src).getParent());\n            FileWrapper parent = parentOpt.get();\n            Optional<FileWrapper> srcOpt = context.getByPath(root.resolve(src)).join();\n            if (srcOpt.isEmpty())\n                throw new IllegalStateException(\"Couldn't retrieve \" + root.resolve(src));\n            FileWrapper from = srcOpt.get();\n            from.rename(target.getFileName().toString(), parent, root.resolve(src), context).join();\n        } else {\n            Optional<FileWrapper> newParent = context.getByPath(root.resolve(target).getParent()).join();\n            if (newParent.isEmpty()) {\n                mkdirs(target.getParent());\n                newParent = context.getByPath(root.resolve(target).getParent()).join();\n            }\n            Optional<FileWrapper> srcOpt = context.getByPath(root.resolve(src)).join();\n            if (srcOpt.isEmpty())\n                throw new IllegalStateException(\"Couldn't retrieve \" + root.resolve(src));\n            FileWrapper from = srcOpt.get();\n            Optional<FileWrapper> parentOpt = context.getByPath(root.resolve(src).getParent()).join();\n            if (parentOpt.isEmpty())\n                throw new IllegalStateException(\"Couldn't retrieve \" + root.resolve(src).getParent());\n            FileWrapper parent = parentOpt.get();\n            from.moveTo(newParent.get(), parent, root.resolve(src), context, () -> Futures.of(true));\n        }\n    }\n\n    @Override\n    public long getLastModified(Path p) {\n        Optional<FileWrapper> file = context.getByPath(root.resolve(p)).join();\n        if (file.isEmpty())\n            throw new IllegalStateException(\"Couldn't retrieve file modification time for \" + root.resolve(p));\n        LocalDateTime modified = file.get().getFileProperties().modified;\n        return modified.toInstant(ZoneOffset.UTC).toEpochMilli() / 1000 * 1000;\n    }\n\n    @Override\n    public void setModificationTime(Path p, long t) {\n        FileWrapper f = context.getByPath(root.resolve(p)).join().get();\n        LocalDateTime newModified = LocalDateTime.ofInstant(Instant.ofEpochSecond(t / 1000, 0), ZoneOffset.UTC);\n        Optional<FileWrapper> parent = context.getByPath(root.resolve(p).getParent()).join();\n        f.setProperties(f.getFileProperties().withModified(newModified), context.crypto.hasher, context.network, parent).join();\n    }\n\n    @Override\n    public void setHash(Path p, HashTree hashTree, long fileSize) {\n        FileWrapper f = context.getByPath(root.resolve(p)).join().get();\n        Optional<FileWrapper> parent = context.getByPath(root.resolve(p).getParent()).join();\n        FileProperties withHash = f.getFileProperties().withHash(Optional.of(hashTree.branch(0)));\n        f.setProperties(withHash, context.crypto.hasher, context.network, parent).join();\n        long nBranches = (fileSize + 1024 * Chunk.MAX_SIZE - 1) / (1024 * Chunk.MAX_SIZE);\n        for (long b = 1; b < nBranches; b++) {\n            WritableAbsoluteCapability cap = f.writableFilePointer();\n            Pair<byte[], Optional<Bat>> loc = FileProperties.calculateMapKey(withHash.streamSecret.get(),\n                    cap.getMapKey(), cap.bat, b * 1024 * Chunk.MAX_SIZE, context.crypto.hasher).join();\n            WritableAbsoluteCapability chunkCap = cap.withMapKey(loc.left, loc.right);\n            long chunkIndex = b * 1024;\n            context.network.synchronizer.applyComplexUpdate(f.owner(), f.signingPair(),\n                    (s, c) -> {\n                        CryptreeNode meta = context.network.getMetadata(s.get(f.writer()), chunkCap).join().get();\n                        return meta.updateProperties(s, c, chunkCap, Optional.of(f.signingPair()), meta.getProperties(chunkCap.rBaseKey)\n                                .withHash(Optional.of(hashTree.branch(chunkIndex))), context.network);\n                    }, () -> true).join();\n        }\n    }\n\n    @Override\n    public void setHashes(List<Triple<String, FileWrapper, HashTree>> toUpdate) {\n        List<FileWrapper.PropsUpdate> hashUpdates = toUpdate.stream()\n                .flatMap(p -> p.middle.getHashUpdates(p.right, context.network, context.crypto.hasher).join().stream())\n                .collect(Collectors.toList());\n        FileWrapper.bulkSetSameNameProperties(hashUpdates, context.network).join();\n    }\n\n    @Override\n    public long size(Path p) {\n        Optional<FileWrapper> file = context.getByPath(root.resolve(p)).join();\n        if (file.isEmpty())\n            throw new IllegalStateException(\"Couldn't retrieve file size for \" + p);\n        return file.get().getFileProperties().size;\n    }\n\n    @Override\n    public void truncate(Path p, long size) throws IOException {\n        FileWrapper f = context.getByPath(root.resolve(p)).join().get();\n        f.truncate(size, context.network, context.crypto).join();\n    }\n\n    @Override\n    public Optional<LocalDateTime> setBytes(Path p,\n                                            long fileOffset,\n                                            AsyncReader data,\n                                            long size,\n                                            Optional<HashTree> hash,\n                                            Optional<LocalDateTime> modificationTime,\n                                            Optional<Thumbnail> thumbnail,\n                                            ResumeUploadProps props,\n                                            Supplier<Boolean> isCancelled,\n                                            Consumer<String> progress) throws IOException {\n        Optional<FileWrapper> existing = context.getByPath(root.resolve(p)).join();\n        String filename = p.getFileName().toString();\n        if (existing.isEmpty() && fileOffset == 0) {\n            Optional<FileWrapper> parentOpt = context.getByPath(root.resolve(p).getParent()).join();\n            if (parentOpt.isEmpty()) {\n                mkdirs(p.getParent());\n                parentOpt = context.getByPath(root.resolve(p).getParent()).join();\n            }\n            FileWrapper parent = parentOpt.get();\n            AtomicLong done = new AtomicLong(0);\n            parent.uploadFileWithHash(filename, data, size, hash, modificationTime, thumbnail,\n                    Optional.of(props),\n                    context.network, context.crypto, isCancelled, x -> {\n                        long total = done.addAndGet(x);\n                        if (total >= 1024*1024)\n                            progress.accept(\"Uploaded \" + (total/1024/1024) + \" / \" + (size / 1024/1024) + \" MiB of \" + filename);\n                    }).join();\n        } else {\n            FileWrapper f = existing.get();\n            if (f.isDirty()) {\n                FileWrapper ff = f;\n                context.network.synchronizer.applyComplexUpdate(f.owner(), f.signingPair(), (v, c) -> ff.clean(v, c, context.network, context.crypto)\n                        .thenApply(r -> r.right)).join();\n                f = context.getByPath(root.resolve(p)).join().get();\n            }\n\n            long end = fileOffset + size;\n            AtomicLong done = new AtomicLong(0);\n            f.overwriteSectionJS(data, (int) (fileOffset >>> 32), (int) fileOffset, (int) (end >>> 32), (int) end, modificationTime, context.network, context.crypto, x -> {\n                long total = done.addAndGet(x);\n                if (total >= 1024*1024)\n                    progress.accept(\"Uploaded \" + (total/1024/1024) + \" / \" + (size / 1024/1024) + \" MiB of \" + filename);\n            }).join();\n        }\n        return modificationTime;\n    }\n\n    @Override\n    public AsyncReader getBytes(Path p, long fileOffset) throws IOException {\n        Optional<FileWrapper> file = context.getByPath(root.resolve(p)).join();\n        if (file.isEmpty())\n            throw new IllegalStateException(\"Couldn't retrieve \" + root.resolve(p));\n        FileWrapper f = file.get();\n        AsyncReader reader = f.getInputStream(context.network, context.crypto, x -> {}).join();\n        return reader.seek(fileOffset).join();\n    }\n\n    @Override\n    public void uploadSubtree(Stream<FileWrapper.FolderUploadProperties> directories) {\n        FileWrapper base = context.getByPath(root).join().get();\n        Optional<BatId> mirrorBat = base.mirrorBatId();\n        base.uploadSubtree(directories, mirrorBat, context.network, context.crypto, context.getTransactionService(), x -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n    }\n\n    @Override\n    public Optional<Thumbnail> getThumbnail(Path p) {\n        return Optional.empty();\n    }\n\n    @Override\n    public HashTree hashFile(Path p, Optional<FileWrapper> meta, String relativePath, SyncState syncedVersions) {\n        FileWrapper f = meta.orElseGet(() -> context.getByPath(root.resolve(p)).join().get());\n        FileProperties props = f.getFileProperties();\n        if (props.treeHash.isPresent()) {\n            FileState synced = syncedVersions.byPath(relativePath);\n            HashBranch branch = props.treeHash.get();\n            if (synced != null && synced.hashTree.rootHash.equals(branch.rootHash))\n                return synced.hashTree;\n            if (props.size < 1024L * Chunk.MAX_SIZE)\n                return new HashTree(branch.rootHash, branch.level1.map(List::of)\n                        .orElseThrow(() -> new IllegalStateException(\"Invalid hash branch\")),\n                        Collections.emptyList(),\n                        Collections.emptyList());\n        }\n\n        byte[] buf = new byte[4 * 1024];\n\n        long size = f.getSize();\n        AsyncReader reader = f.getInputStream(context.network, context.crypto, x -> {}).join();\n        int chunkOffset = 0;\n        List<byte[]> chunkHashes = new ArrayList<>();\n        try {\n            MessageDigest chunkHash = MessageDigest.getInstance(\"SHA-256\");\n\n            for (long i = 0; i < size; ) {\n                int read = reader.readIntoArray(buf, 0, (int) Math.min(buf.length, size - i)).join();\n                chunkOffset += read;\n                if (chunkOffset >= Chunk.MAX_SIZE) {\n                    int thisChunk = read - chunkOffset + Chunk.MAX_SIZE;\n                    chunkHash.update(buf, 0, thisChunk);\n                    chunkHashes.add(chunkHash.digest());\n                    chunkHash = MessageDigest.getInstance(\"SHA-256\");\n                    int leftover = read - thisChunk;\n                    if (leftover > 0)\n                        chunkHash.update(buf, thisChunk, leftover);\n                    chunkOffset = leftover;\n                } else\n                    chunkHash.update(buf, 0, read);\n                i += read;\n            }\n            if (size == 0 || chunkOffset % Chunk.MAX_SIZE != 0)\n                chunkHashes.add(chunkHash.digest());\n\n            return HashTree.build(chunkHashes, context.crypto.hasher).join();\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public long filesCount() throws IOException {\n        throw new IllegalStateException(\"Unimplemented file count.\");\n    }\n\n    @Override\n    public Optional<PublicKeyHash> applyToSubtree(Consumer<FileProps> onFile, Consumer<FileProps> onDir) {\n        return applyToSubtree(root, onFile, onDir);\n    }\n\n    private Optional<PublicKeyHash> applyToSubtree(Path start, Consumer<FileProps> onFile, Consumer<FileProps> onDir) {\n        Optional<FileWrapper> baseDir = context.getByPath(start).join();\n        if (baseDir.isEmpty())\n            throw new IllegalStateException(\"Couldn't retrieve Peergos base directory!\");\n        applyToSubtree(start, baseDir.get(), onFile, onDir);\n        return Optional.of(baseDir.get().getLinkPointer().capability.writer);\n    }\n\n    private void applyToSubtree(Path basePath, FileWrapper base, Consumer<FileProps> onFile, Consumer<FileProps> onDir) {\n        Set<NamedAbsoluteCapability> childCaps = base.getChildrenCapabilities(context.crypto.hasher, context.network).join();\n        AtomicLong directChildCount = new AtomicLong(0);\n        List<Pair<Path, FileWrapper>> subdirs = Collections.synchronizedList(new ArrayList<>());\n        Consumer<Set<FileWrapper>> collector = children -> {\n            directChildCount.addAndGet(children.size());\n            for (FileWrapper child : children) {\n                Path childPath = basePath.resolve(child.getName());\n                FileProps childProps = new FileProps(root.relativize(childPath).normalize().toString().replaceAll(\"\\\\\\\\\", \"/\"),\n                        child.getFileProperties().modified.toInstant(ZoneOffset.UTC).toEpochMilli() / 1000 * 1000,\n                        child.getSize(), Optional.of(child));\n                if (!child.isDirectory()) {\n                    onFile.accept(childProps);\n                } else {\n                    onDir.accept(childProps);\n                    subdirs.add(new Pair<>(childPath, child));\n                }\n            }\n        };\n        // Partition caps into batches of MAX_CHAMP_GETS so each getChildrenFromCaps call\n        // makes exactly one network request, and bound concurrency with the semaphore.\n        List<NamedAbsoluteCapability> capList = new ArrayList<>(childCaps);\n        List<CompletableFuture<Boolean>> futures = new ArrayList<>();\n        for (int i = 0; i < capList.size(); i += MAX_CHAMP_GETS) {\n            Set<NamedAbsoluteCapability> batch = new HashSet<>(capList.subList(i, Math.min(i + MAX_CHAMP_GETS, capList.size())));\n            try {\n                fetchSemaphore.acquire();\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n                throw new RuntimeException(e);\n            }\n            futures.add(base.getChildrenFromCaps(batch, collector, context.crypto.hasher, context.network)\n                    .whenComplete((r, e) -> fetchSemaphore.release()));\n        }\n        Futures.combineAllInOrder(futures).join();\n        if (directChildCount.get() != childCaps.size())\n            throw new IllegalStateException(\"Couldn't retrieve all \" + childCaps.size() + \" children for \" + basePath);\n        for (Pair<Path, FileWrapper> subdir : subdirs)\n            applyToSubtree(subdir.left, subdir.right, onFile, onDir);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/RamTreeState.java",
    "content": "package peergos.server.sync;\n\nimport peergos.shared.user.Snapshot;\nimport peergos.shared.user.fs.RootHash;\n\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nclass RamTreeState implements SyncState {\n    public final Map<String, FileState> filesByPath = new HashMap<>();\n    public final Map<RootHash, List<FileState>> fileByHash = new HashMap<>();\n    private final Set<String> dirs = new HashSet<>();\n    private final Set<String> localDeletes = new HashSet<>();\n    private final Set<String> remoteDeletes = new HashSet<>();\n    private final List<CopyOp> inProgress = new ArrayList<>();\n    private final Map<String, Snapshot> versions = new HashMap<>();\n    private final AtomicBoolean completedSync = new AtomicBoolean(false);\n\n    @Override\n    public boolean hasCompletedSync() {\n        return completedSync.get();\n    }\n\n    @Override\n    public void setCompletedSync(boolean done) {\n        completedSync.set(done);\n    }\n\n    @Override\n    public long filesCount() {\n        return filesByPath.size();\n    }\n\n    @Override\n    public Set<String> allFilePaths() {\n        return filesByPath.keySet();\n    }\n\n    @Override\n    public synchronized void setSnapshot(String basePath, Snapshot s) {\n        versions.put(basePath, s);\n    }\n\n    @Override\n    public synchronized Snapshot getSnapshot(String basePath) {\n        return versions.getOrDefault(basePath, new Snapshot(new HashMap<>()));\n    }\n\n    @Override\n    public synchronized void add(FileState fs) {\n        filesByPath.put(fs.relPath, fs);\n        fileByHash.putIfAbsent(fs.hashTree.rootHash, new ArrayList<>());\n        fileByHash.get(fs.hashTree.rootHash).add(fs);\n    }\n\n    public synchronized void addDir(String path) {\n        dirs.add(path);\n    }\n\n    public synchronized void removeDir(String path) {\n        dirs.remove(path);\n    }\n\n    public synchronized boolean hasDir(String path) {\n        return dirs.contains(path);\n    }\n\n    public synchronized Set<String> getDirs() {\n        return dirs;\n    }\n\n    @Override\n    public void addLocalDelete(String path) {\n        localDeletes.add(path);\n    }\n\n    @Override\n    public void removeLocalDelete(String path) {\n        localDeletes.remove(path);\n    }\n\n    @Override\n    public boolean hasLocalDelete(String p) {\n        return localDeletes.contains(p);\n    }\n\n    @Override\n    public void addRemoteDelete(String path) {\n        remoteDeletes.add(path);\n    }\n\n    @Override\n    public void removeRemoteDelete(String path) {\n        remoteDeletes.remove(path);\n    }\n\n    @Override\n    public boolean hasRemoteDelete(String p) {\n        return remoteDeletes.contains(p);\n    }\n\n    @Override\n    public synchronized void remove(String path) {\n        FileState v = filesByPath.remove(path);\n        if (v != null) {\n            List<FileState> byHash = fileByHash.get(v.hashTree.rootHash);\n            if (byHash.size() == 1)\n                fileByHash.remove(v.hashTree.rootHash);\n            else\n                byHash.remove(v);\n        }\n    }\n\n    @Override\n    public synchronized FileState byPath(String path) {\n        return filesByPath.get(path);\n    }\n\n    public List<FileState> byHash(RootHash b3) {\n        return fileByHash.getOrDefault(b3, Collections.emptyList());\n    }\n\n    @Override\n    public synchronized void startCopies(List<CopyOp> ops) {\n        inProgress.addAll(ops);\n    }\n\n    @Override\n    public synchronized void finishCopies(List<CopyOp> ops) {\n        inProgress.removeAll(ops);\n    }\n\n    @Override\n    public synchronized List<CopyOp> getInProgressCopies() {\n        return inProgress;\n    }\n\n    @Override\n    public void close() throws IOException {}\n}\n"
  },
  {
    "path": "src/peergos/server/sync/SyncConfig.java",
    "content": "package peergos.server.sync;\n\nimport org.peergos.config.Jsonable;\nimport peergos.server.net.SyncConfigHandler;\nimport peergos.server.util.Args;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class SyncConfig implements Jsonable {\n    public final List<String> localDirs, remotePaths, links;\n    public final List<Boolean> syncLocalDeletes, syncRemoteDeletes;\n    public final int maxDownloadParallelism, minFreeSpacePercent;\n\n    public SyncConfig(List<String> localDirs,\n                      List<String> remotePaths,\n                      List<String> links,\n                      List<Boolean> syncLocalDeletes,\n                      List<Boolean> syncRemoteDeletes,\n                      int maxDownloadParallelism,\n                      int minFreeSpacePercent) {\n        if (localDirs.size() != remotePaths.size())\n            throw new IllegalStateException(\"Invalid SyncConfig!\");\n        if (localDirs.size() != links.size())\n            throw new IllegalStateException(\"Invalid SyncConfig!\");\n        if (localDirs.size() != syncLocalDeletes.size())\n            throw new IllegalStateException(\"Invalid SyncConfig!\");\n        if (localDirs.size() != syncRemoteDeletes.size())\n            throw new IllegalStateException(\"Invalid SyncConfig!\");\n        this.localDirs = localDirs;\n        this.remotePaths = remotePaths;\n        this.links = links;\n        this.syncLocalDeletes = syncLocalDeletes;\n        this.syncRemoteDeletes = syncRemoteDeletes;\n        this.maxDownloadParallelism = maxDownloadParallelism;\n        this.minFreeSpacePercent = minFreeSpacePercent;\n    }\n\n    public Map<String, Object> toJsonWithoutCaps() {\n        LinkedHashMap<String, Object> res = new LinkedHashMap<>();\n        List<Object> pairs = new ArrayList<>();\n        for (int i = 0; i < localDirs.size(); i++) {\n            LinkedHashMap<String, Object> pair = new LinkedHashMap<>();\n            pair.put(\"localpath\", localDirs.get(i));\n            pair.put(\"remotepath\", remotePaths.get(i));\n            String link = links.get(i);\n            // only return the link champ label, which is not sensitive, but enough for the owner to delete it\n            pair.put(\"label\", link.substring(link.lastIndexOf(\"/\", link.indexOf(\"#\")) + 1, link.indexOf(\"#\")));\n            pair.put(\"syncLocalDeletes\", syncLocalDeletes.get(i));\n            pair.put(\"syncRemoteDeletes\", syncRemoteDeletes.get(i));\n            pairs.add(pair);\n        }\n        res.put(\"pairs\", pairs);\n        return res;\n    }\n\n    @Override\n    public Map<String, Object> toJson() {\n        LinkedHashMap<String, Object> res = new LinkedHashMap<>();\n        List<Object> pairs = new ArrayList<>();\n        for (int i = 0; i < localDirs.size(); i++) {\n            LinkedHashMap<String, Object> pair = new LinkedHashMap<>();\n            String rawLocalDir = localDirs.get(i);\n            String localDir = isWindows() ? rawLocalDir.replaceAll(\"\\\\\\\\\\\\\\\\\", \"\\\\\\\\\") : rawLocalDir;\n            pair.put(\"localpath\", localDir);\n            pair.put(\"remotepath\", remotePaths.get(i));\n            pair.put(\"link\", links.get(i));\n            pair.put(\"syncLocalDeletes\", syncLocalDeletes.get(i));\n            pair.put(\"syncRemoteDeletes\", syncRemoteDeletes.get(i));\n            pairs.add(pair);\n        }\n        res.put(\"pairs\", pairs);\n        res.put(\"maxParallelism\", maxDownloadParallelism);\n        res.put(\"minPercentFreeSpace\", minFreeSpacePercent);\n        return res;\n    }\n\n    public static SyncConfig fromJson(Map<String, Object> json) {\n        List<Map<String, Object>> jsonList = (List<Map<String, Object>>) json.get(\"pairs\");\n        List<String> localDirs = new ArrayList<>();\n        List<String> links = new ArrayList<>();\n        List<String> remoteDirs = new ArrayList<>();\n        List<Boolean> syncLocalDeletes= new ArrayList<>();\n        List<Boolean> syncRemoteDeletes= new ArrayList<>();\n        for (Map<String, Object> pair : jsonList) {\n            localDirs.add((String)pair.get(\"localpath\"));\n            links.add((String)pair.get(\"link\"));\n            remoteDirs.add((String)pair.get(\"remotepath\"));\n            syncLocalDeletes.add((Boolean)pair.get(\"syncLocalDeletes\"));\n            syncRemoteDeletes.add((Boolean)pair.get(\"syncRemoteDeletes\"));\n        }\n        return new SyncConfig(localDirs, remoteDirs, links, syncLocalDeletes, syncRemoteDeletes, (Integer)json.get(\"maxParallelism\"), (Integer)json.get(\"minPercentFreeSpace\"));\n    }\n\n    public static List<String> getLinks(Args updated) {\n        if (! updated.hasArg(\"links\"))\n            return new ArrayList<>();\n        return new ArrayList<>(Arrays.asList(updated.getArg(\"links\").split(\",\")));\n    }\n\n    public static List<String> getLocalDirs(Args updated) {\n        if (! updated.hasArg(\"local-dirs\"))\n            return new ArrayList<>();\n        return new ArrayList<>(Arrays.asList(updated.getArg(\"local-dirs\").split(\",\")));\n    }\n\n    public static List<String> getRemotePaths(Args updated) {\n        if (updated.hasArg(\"remote-paths\")) {\n            return new ArrayList<>(Arrays.asList(updated.getArg(\"remote-paths\").split(\"//\")));\n        }\n        return Collections.emptyList();\n    }\n\n    public static List<Boolean> getSyncLocalDeletes(Args updated) {\n        if (! updated.hasArg(\"sync-local-deletes\"))\n            return new ArrayList<>();\n        return new ArrayList<>(Stream.of(updated.getArg(\"sync-local-deletes\").split(\",\"))\n                .map(Boolean::parseBoolean)\n                .collect(Collectors.toList()));\n    }\n\n    public static List<Boolean> getSyncRemoteDeletes(Args updated) {\n        if (! updated.hasArg(\"sync-remote-deletes\"))\n            return new ArrayList<>();\n        return new ArrayList<>(Stream.of(updated.getArg(\"sync-remote-deletes\").split(\",\"))\n                .map(Boolean::parseBoolean)\n                .collect(Collectors.toList()));\n    }\n\n    public static SyncConfig fromArgs(Args a) {\n        return new SyncConfig(getLocalDirs(a),\n                getRemotePaths(a),\n                getLinks(a),\n                getSyncLocalDeletes(a),\n                getSyncRemoteDeletes(a),\n                a.getInt(\"max-parallelism\", 32),\n                a.getInt(\"min-free-space-percent\", 5));\n    }\n\n    private static boolean isWindows() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"windows\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/SyncFilesystem.java",
    "content": "package peergos.server.sync;\n\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.Triple;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.time.LocalDateTime;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\n\npublic interface SyncFilesystem {\n\n    long totalSpace() throws IOException;\n\n    long freeSpace() throws IOException;\n\n    String getRoot();\n\n    Path resolve(String p);\n\n    boolean exists(Path p);\n\n    void mkdirs(Path p);\n\n    void delete(Path p);\n\n    void bulkDelete(Path dir, Set<String> children);\n\n    void moveTo(Path src, Path target);\n\n    long getLastModified(Path p);\n\n    void setModificationTime(Path p, long t);\n\n    void setHash(Path p, HashTree hashTree, long fileSize);\n\n    void setHashes(List<Triple<String, FileWrapper, HashTree>> toUpdate);\n\n    long size(Path p);\n\n    void truncate(Path p, long size) throws IOException;\n\n    /**\n     *\n     * @param p\n     * @param fileOffset\n     * @param data\n     * @param size\n     * @param hash\n     * @param modificationTime\n     * @param thumbnail\n     * @param props\n     * @param isCancelled\n     * @param progress\n     * @return The actual modification time the filesystem returns after write\n     * @throws IOException\n     */\n    Optional<LocalDateTime> setBytes(Path p,\n                                     long fileOffset,\n                                     AsyncReader data,\n                                     long size,\n                                     Optional<HashTree> hash,\n                                     Optional<LocalDateTime> modificationTime,\n                                     Optional<Thumbnail> thumbnail,\n                                     ResumeUploadProps props,\n                                     Supplier<Boolean> isCancelled,\n                                     Consumer<String> progress) throws IOException;\n\n    AsyncReader getBytes(Path p, long fileOffset) throws IOException;\n\n    void uploadSubtree(Stream<FileWrapper.FolderUploadProperties> directories);\n\n    Optional<Thumbnail> getThumbnail(Path p);\n\n    HashTree hashFile(Path p, Optional<FileWrapper> meta, String relativePath, SyncState syncedState);\n\n    /**\n     *\n     * @param file\n     * @param dir\n     * @return the writer to ignore from snapshots (we only have read access to it as the entry point)\n     * @throws IOException\n     */\n    Optional<PublicKeyHash> applyToSubtree(Consumer<FileProps> file, Consumer<FileProps> dir) throws IOException;\n\n    long filesCount() throws IOException;\n\n    class FileProps {\n        public final String relPath;\n        public final long modifiedTime;\n        public final long size;\n        public final Optional<FileWrapper> meta;\n\n        public FileProps(String relPath, long modifiedTime, long size, Optional<FileWrapper> meta) {\n            this.relPath = relPath;\n            this.modifiedTime = modifiedTime;\n            this.size = size;\n            this.meta = meta;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/SyncProgress.java",
    "content": "package peergos.server.sync;\n\nimport java.util.concurrent.atomic.AtomicLong;\n\npublic class SyncProgress {\n\n    private final AtomicLong done = new AtomicLong(0);\n    private final long total;\n\n    public SyncProgress(long total) {\n        this.total = total;\n    }\n\n    public void doneFile() {\n        done.incrementAndGet();\n    }\n\n    public void doneFiles(int count) {\n        done.addAndGet(count);\n    }\n\n    @Override\n    public String toString() {\n        return \"(\"+done.get() + \"/\" + total+\") files synced\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/SyncRunner.java",
    "content": "package peergos.server.sync;\n\nimport peergos.server.util.Args;\nimport peergos.server.util.Logging;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.storage.RetryStorage;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Consumer;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Stream;\n\nimport static peergos.server.net.SyncConfigHandler.OLD_SYNC_CONFIG_FILENAME;\nimport static peergos.server.net.SyncConfigHandler.SYNC_CONFIG_FILENAME;\n\npublic interface SyncRunner {\n\n    void start();\n\n    void runNow();\n\n    StatusHolder getStatusHolder();\n\n    class StatusHolder {\n        private String status;\n        private LocalDateTime updateTime;\n        private Optional<String> error = Optional.empty();\n        private final AtomicBoolean cancelled = new AtomicBoolean(false);\n\n        public synchronized void cancel() {\n            cancelled.set(true);\n        }\n\n        public synchronized void resume() {\n            cancelled.set(false);\n        }\n\n        public synchronized boolean isCancelled() {\n            return cancelled.get();\n        }\n\n        public synchronized void setStatus(String newStatus) {\n            status = newStatus;\n            updateTime = LocalDateTime.now();\n        }\n\n        public synchronized void setError(String error) {\n            this.error = error == null || error.isEmpty() ?\n                    Optional.empty() :\n                    Optional.of(error);\n        }\n\n        public synchronized String getStatusAndTime() {\n            if (status == null)\n                return \"\";\n            return status + \" at \" + updateTime.toLocalDate() + \" \" + updateTime.toLocalTime().withNano(0);\n        }\n\n        public synchronized Optional<String> getError() {\n            return error;\n        }\n    }\n\n    class ThreadBased implements SyncRunner {\n        private static final Logger LOG = Logging.LOG();\n        private final Thread runner;\n        private final AtomicBoolean started = new AtomicBoolean(false);\n        private final StatusHolder status = new StatusHolder();\n\n        public ThreadBased(Args args,\n                           ContentAddressedStorage storage,\n                           MutablePointers mutable,\n                           CoreNode core,\n                           Crypto crypto) {\n\n            NetworkAccess network = NetworkAccess.buildBuffered(new RetryStorage(storage, 5), null, core, null,\n                    mutable, 5_000, null, null, null, null,\n                    crypto.hasher, Collections.emptyList(), false);\n            this.runner = new Thread(() -> {\n                while (true) {\n                    try {\n                        Path peergosDir = args.getPeergosDir();\n                        Path jsonSyncConfig = peergosDir.resolve(SYNC_CONFIG_FILENAME);\n                        Path oldSyncConfig = peergosDir.resolve(OLD_SYNC_CONFIG_FILENAME);\n                        SyncConfig syncConfig = Files.exists(jsonSyncConfig) ?\n                                SyncConfig.fromJson((Map<String, Object>) JSONParser.parse(Files.readString(jsonSyncConfig))) :\n                                SyncConfig.fromArgs(Args.parse(new String[]{\"-run-once\", \"true\"}, Optional.of(oldSyncConfig), false));\n                        if (! syncConfig.links.isEmpty()) {\n                            List<String> links = syncConfig.links;\n                            List<String> localDirs = syncConfig.localDirs;\n                            List<Boolean> syncLocalDeletes = syncConfig.syncLocalDeletes;\n                            List<Boolean> syncRemoteDeletes = syncConfig.syncRemoteDeletes;\n                            int maxDownloadParallelism = syncConfig.maxDownloadParallelism;\n                            int minFreeSpacePercent = syncConfig.minFreeSpacePercent;\n                            Consumer<String> statusUpdater = msg -> {\n                                status.setStatus(msg);\n                                DirectorySync.log(msg);\n                            };\n                            Consumer<Throwable> errorUpdater = e -> {\n                                if (e != null) {\n                                    status.setError(e.getMessage());\n                                    DirectorySync.log(e.getMessage());\n                                }\n                            };\n                            DirectorySync.syncDirs(links, localDirs, syncLocalDeletes, syncRemoteDeletes,\n                                    maxDownloadParallelism, minFreeSpacePercent, true,\n                                    root -> new LocalFileSystem(Paths.get(root), crypto.hasher),\n                                    peergosDir, status, statusUpdater, errorUpdater, network.clear(), crypto);\n                        } else {\n                            // delete stale async state dbs\n                            try (Stream<Path> kids = Files.list(peergosDir)) {\n                                kids\n                                        .filter(p -> p.getFileName().endsWith(\".sqlite\"))\n                                        .filter(p -> p.getFileName().startsWith(\"dir-sync-state-v3-\"))\n                                        .forEach(p -> {\n                                            try {\n                                                Files.delete(p);\n                                            } catch (IOException e) {\n                                                e.printStackTrace();\n                                            }\n                                        });\n                            } catch (IOException e) {\n                                e.printStackTrace();\n                            }\n                        }\n                    } catch (Exception e) {\n                        LOG.log(Level.WARNING, e.getMessage(), e);\n                    }\n                    try {\n                        Thread.sleep(30_000);\n                    } catch (InterruptedException e) {}\n                }\n            });\n        }\n\n        @Override\n        public void start() {\n            if (! started.get()) {\n                runner.start();\n                started.set(true);\n            } else\n                runner.interrupt();\n        }\n\n        @Override\n        public void runNow() {\n            runner.interrupt();\n        }\n\n        @Override\n        public StatusHolder getStatusHolder() {\n            return status;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/sync/SyncState.java",
    "content": "package peergos.server.sync;\n\nimport peergos.shared.user.Snapshot;\nimport peergos.shared.user.fs.RootHash;\n\nimport java.io.Closeable;\nimport java.util.List;\nimport java.util.Set;\n\npublic interface SyncState extends Closeable {\n\n    boolean hasCompletedSync();\n\n    void setCompletedSync(boolean done);\n\n    long filesCount();\n\n    Set<String> allFilePaths();\n\n    void add(FileState fs);\n\n    void remove(String path);\n\n    FileState byPath(String path);\n\n    List<FileState> byHash(RootHash b3);\n\n    void addDir(String path);\n\n    void removeDir(String path);\n\n    boolean hasDir(String path);\n\n    Set<String> getDirs();\n\n    void addLocalDelete(String path);\n\n    void removeLocalDelete(String path);\n\n    boolean hasLocalDelete(String p);\n\n    void addRemoteDelete(String path);\n\n    void removeRemoteDelete(String path);\n\n    boolean hasRemoteDelete(String p);\n\n    void startCopies(List<CopyOp> ops);\n\n    void finishCopies(List<CopyOp> ops);\n\n    List<CopyOp> getInProgressCopies();\n\n    void setSnapshot(String basePath, Snapshot s);\n\n    Snapshot getSnapshot(String basePath);\n}\n"
  },
  {
    "path": "src/peergos/server/tests/ArgsTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.util.*;\n\nimport java.util.*;\n\npublic class ArgsTests {\n\n    @Test\n    public void parse() {\n        Args daemon = Args.parse(new String[]{\"daemon\", \"-useIPFS\", \"true\"});\n        Assert.assertTrue(\"command correct\", daemon.commands().equals(Arrays.asList(\"daemon\")));\n\n        Args subcommand = Args.parse(new String[]{\"server-msg\", \"show\", \"-useIPFS\", \"true\"});\n        Assert.assertTrue(\"command correct\", subcommand.commands().equals(Arrays.asList(\"server-msg\", \"show\")));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/BatTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\n\nimport java.time.*;\n\nimport static peergos.server.storage.auth.BlockRequestAuthoriser.isValidAuth;\n\npublic class BatTests {\n    private static final Crypto crypto = Main.initCrypto();\n    private static final SafeRandom rnd = crypto.random;\n    private static final Hasher h = crypto.hasher;\n\n    @Test\n    public void roundtripAndValidity() {\n        Cid block = h.hash(rnd.randomBytes(100), false).join();\n        Bat bat = Bat.random(rnd);\n        Cid batId = h.hash(bat.serialize(), false).join();\n        Cid nodeId = new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.id, rnd.randomBytes(32));\n\n        ZonedDateTime now = ZonedDateTime.now();\n        String awsNow = S3AdminRequests.asAwsDate(now);\n        BlockAuth auth = bat.generateAuth(block, nodeId, 300, awsNow, batId, h).join();\n\n        String forBitswap = auth.encode();\n        BlockAuth receivedAuth = BlockAuth.fromString(forBitswap);\n        boolean validAuth = isValidAuth(receivedAuth, block, nodeId, bat, h);\n        Assert.assertTrue(validAuth);\n\n        // invalid signature\n        BlockAuth invalidSig = new BlockAuth(new byte[32], auth.expirySeconds, auth.awsDatetime, batId);\n        Assert.assertTrue(! isValidAuth(invalidSig, block, nodeId, bat, h));\n\n        // invalid date\n        String differentDatetime = bat.generateAuth(block, nodeId, 300,\n                S3AdminRequests.asAwsDate(now.plusSeconds(1)), batId, h).join().awsDatetime;\n        BlockAuth invalidDate = new BlockAuth(auth.signature, 300, differentDatetime, batId);\n        Assert.assertTrue(! isValidAuth(invalidDate, block, nodeId, bat, h));\n\n        // expired sig\n        BlockAuth expired = bat.generateAuth(block, nodeId, 300,\n                S3AdminRequests.asAwsDate(now.minusSeconds(301)), batId, h).join();\n        Assert.assertTrue(! isValidAuth(expired, block, nodeId, bat, h));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/Blake3Tests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.crypto.hash.Blake3;\nimport peergos.shared.util.ArrayOps;\n\nimport java.util.Random;\n\npublic class Blake3Tests {\n\n    @Test\n    public void correct() {\n        Blake3 b3 = Blake3.initHash();\n        byte[] data = new byte[4096];\n        new Random(42).nextBytes(data);\n        b3.update(data);\n        byte[] hash = b3.doFinalize(32);\n        Assert.assertEquals(ArrayOps.bytesToHex(hash), \"3393625f68437730188ea2f582ac38f9ec6ead68ea6351caf36030d4a7b94ac5\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/BlockSizeTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class BlockSizeTests {\n    private static Crypto crypto = Main.initCrypto();\n\n    @Test\n    public void largeDirectoryCryptreeNode() {\n        SymmetricKey rBase = SymmetricKey.random();\n        SymmetricKey wBase = SymmetricKey.random();\n        SymmetricKey parent = SymmetricKey.random();\n        SymmetricKey parentParent = SymmetricKey.random();\n        Optional<BatId> mirrorBatId = Optional.of(BatId.sha256(Bat.random(crypto.random), crypto.hasher).join());\n        FileProperties props = new FileProperties(\"a-directory\", true, false, \"\", 0, 0,\n                LocalDateTime.now(), LocalDateTime.now(), false, Optional.empty(), Optional.empty(), Optional.empty());\n        SigningPrivateKeyAndPublicHash signingPair = ChampTests.createUser(new RAMStorage(crypto.hasher), crypto);\n\n        Optional<RelativeCapability> parentCap = Optional.of(new RelativeCapability(Optional.empty(), crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), parentParent, Optional.empty()));\n        RelativeCapability nextChunk = new RelativeCapability(Optional.empty(), crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), parentParent, Optional.empty());\n\n        String nameBase = IntStream.range(0, 252).mapToObj(i -> \"A\").collect(Collectors.joining());\n        List<NamedRelativeCapability> children = IntStream.range(0, 500)\n                .mapToObj(i -> new NamedRelativeCapability(nameBase + String.format(\"%03d\", i), nextChunk, Optional.of(true), Optional.of(\"appliction/x-someapp-somebig-string-too\"), Optional.of(LocalDateTime.MAX)))\n                .collect(Collectors.toList());\n        CryptreeNode.ChildrenLinks childrenLinks = new CryptreeNode.ChildrenLinks(children);\n        CryptreeNode.DirAndChildren dir = CryptreeNode.createDir(MaybeMultihash.empty(), rBase,\n                wBase, Optional.of(signingPair), props, parentCap, parent, nextChunk, childrenLinks, Optional.of(Bat.random(crypto.random)), mirrorBatId, crypto.random, crypto.hasher).join();\n\n        byte[] raw = dir.dir.serialize();\n        Assert.assertTrue(raw.length < Fragment.MAX_LENGTH);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/CLITests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.cli.CLI;\nimport peergos.server.cli.ParsedCommand;\n\npublic class CLITests {\n\n    @Test\n    public void quoting() {\n        CLI.fromLine(\"put dir\\\\ with\\\\ spaces.txt /me/target\");\n        CLI.fromLine(\"put \\\"dir with spaces\\\" /me/target\");\n        ParsedCommand cmd = CLI.fromLine(\"mkdir \\\"quotedpathwithnospaces\\\"\");\n        Assert.assertEquals(1, cmd.arguments.size());\n        Assert.assertEquals(\"quotedpathwithnospaces\", cmd.arguments.get(0));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/CborObjects.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic class CborObjects {\n    private final Random rnd = new Random();\n\n    private byte[] random(int len) {\n        byte[] res = new byte[len];\n        rnd.nextBytes(res);\n        return res;\n    }\n\n    @Test\n    public void dosCborObject() throws Throwable {\n        // make a header for a byte[] that is 2^50 long\n        byte[] raw = ArrayOps.hexToBytes(\"5b0004000000000000\");\n        try {\n            CborObject.fromByteArray(raw);\n            Assert.fail(\"Should have failed!\");\n        } catch (RuntimeException e) {}\n    }\n\n    @Test\n    public void cborNull() {\n        CborObject.CborNull cbor = new CborObject.CborNull();\n        compatibleAndIdempotentSerialization(cbor);\n    }\n\n    @Test\n    public void cborString() {\n        String value = \"G'day mate!\";\n        CborObject.CborString cbor = new CborObject.CborString(value);\n        compatibleAndIdempotentSerialization(cbor);\n    }\n\n    @Test\n    public void cborByteArray() {\n        byte[] value = random(32);\n        CborObject.CborByteArray cbor = new CborObject.CborByteArray(value);\n        compatibleAndIdempotentSerialization(cbor);\n    }\n\n    @Test\n    public void cborBoolean() {\n        compatibleAndIdempotentSerialization(new CborObject.CborBoolean(true));\n        compatibleAndIdempotentSerialization(new CborObject.CborBoolean(false));\n    }\n\n    @Test\n    public void cborLongs() {\n        cborLong(rnd.nextLong());\n        cborLong(Long.MAX_VALUE);\n        cborLong(Long.MIN_VALUE);\n        cborLong(Integer.MAX_VALUE);\n        cborLong(Integer.MIN_VALUE);\n        cborLong(0);\n        cborLong(100);\n        cborLong(-100);\n    }\n\n    private void cborLong(long value) {\n        CborObject.CborLong cbor = new CborObject.CborLong(value);\n        compatibleAndIdempotentSerialization(cbor);\n    }\n\n    @Test\n    public void cborMerkleLink() {\n        Multihash hash = Multihash.fromBase58(\"QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB\");\n        CborObject.CborMerkleLink link = new CborObject.CborMerkleLink(hash);\n        compatibleAndIdempotentSerialization(link);\n    }\n\n    @Test\n    public void cborMap() {\n        SortedMap<String, Cborable> map = new TreeMap<>();\n        map.put(\"KEY 1\", new CborObject.CborString(\"A value\"));\n        map.put(\"KEY 2\", new CborObject.CborByteArray(\"Another value\".getBytes()));\n        map.put(\"KEY 3\", new CborObject.CborNull());\n        map.put(\"KEY 4\", new CborObject.CborBoolean(true));\n        Multihash hash = Multihash.fromBase58(\"QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB\");\n        CborObject.CborMerkleLink link = new CborObject.CborMerkleLink(hash);\n        map.put(\"Key 5\", link);\n        List<CborObject> list = new ArrayList<>();\n        list.add(new CborObject.CborBoolean(true));\n        list.add(new CborObject.CborNull());\n        list.add(new CborObject.CborLong(256));\n        map.put(\"KEY 6\", new CborObject.CborList(list));\n        CborObject.CborMap cborMap = CborObject.CborMap.build(map);\n        compatibleAndIdempotentSerialization(cborMap);\n    }\n\n    @Test\n    public void cborList() {\n        List<CborObject> list = new ArrayList<>();\n        list.add(new CborObject.CborString(\"A value\"));\n        list.add(new CborObject.CborByteArray(\"A value\".getBytes()));\n        list.add(new CborObject.CborNull());\n        list.add(new CborObject.CborBoolean(true));\n        CborObject.CborList cborList = new CborObject.CborList(list);\n        compatibleAndIdempotentSerialization(cborList);\n    }\n\n    public void compatibleAndIdempotentSerialization(CborObject value) {\n        byte[] raw = value.toByteArray();\n        CborObject deserialized = CborObject.fromByteArray(raw);\n\n        boolean equals = deserialized.equals(value);\n        Assert.assertTrue(\"Equal objects\", equals);\n        byte[] raw2 = deserialized.toByteArray();\n        boolean sameRaw = Arrays.equals(raw, raw2);\n        Assert.assertTrue(\"Idempotent serialization\", sameRaw);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/ChampTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.corenode.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class ChampTests {\n\n    private static final Crypto crypto = Main.initCrypto();\n    private static final Hasher writeHasher = crypto.hasher;\n    private final Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher;\n\n    public ChampTests(Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher) {\n        this.hasher = hasher;\n    }\n\n    public static byte[] identityHash(ByteArrayWrapper key) {\n        return Arrays.copyOfRange(key.data, 0, 32);\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        Function<ByteArrayWrapper, CompletableFuture<byte[]>> identityHash = x -> Futures.of(ChampTests.identityHash(x));\n        Function<ByteArrayWrapper, CompletableFuture<byte[]>> blake2b = IpfsCoreNode::keyHash;\n        return Arrays.asList(new Object[][] {\n                {identityHash},\n                {blake2b}\n        });\n    }\n\n    @Test\n    public void insertAndRetrieve() throws Exception {\n        DeletableContentAddressedStorage storage = new FileContentAddressedStorage(Files.createTempDirectory(\"peergos-tmp\"),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n        RamPki pki = new RamPki();\n        storage.setPki(pki);\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        PublicKeyHash owner = user.publicKeyHash;\n        pki.reverseLookup.put(owner, \"alice\");\n        Random r = new Random(28);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n\n        Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> state = new HashMap<>();\n\n        Champ<CborObject.CborMerkleLink> current = Champ.empty(c -> (CborObject.CborMerkleLink)c);\n        TransactionId tid = storage.startTransaction(owner).get();\n        Multihash currentHash = storage.put(owner, user, current.serialize(), writeHasher, tid).get();\n        int bitWidth = 5;\n        int maxCollisions = 3;\n        // build a random tree and keep track of the state\n        int nKeys = 1000;\n        for (int i = 0; i < nKeys; i++) {\n            ByteArrayWrapper key = new ByteArrayWrapper(randomHash.get().toBytes());\n            Multihash value = randomHash.get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(owner, user, key, hasher.apply(key).join(), 0,\n                    Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage, writeHasher, currentHash).get();\n            Optional<CborObject.CborMerkleLink> result = updated.left.get(owner, key, hasher.apply(key).join(), 0, bitWidth, storage).get();\n            if (! result.equals(Optional.of(new CborObject.CborMerkleLink(value))))\n                throw new IllegalStateException(\"Incorrect result!\");\n            current = updated.left;\n            currentHash = updated.right;\n            state.put(key, Optional.of(new CborObject.CborMerkleLink(value)));\n        }\n\n        // check every mapping\n        for (Map.Entry<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> e : state.entrySet()) {\n            Optional<CborObject.CborMerkleLink> res = current.get(owner, e.getKey(), hasher.apply(e.getKey()).join(), 0, bitWidth, storage).get();\n            if (! res.equals(e.getValue()))\n                throw new IllegalStateException(\"Incorrect state!\");\n        }\n\n        long size = current.size(owner, 0, storage).get();\n        if (size != nKeys)\n            throw new IllegalStateException(\"Incorrect number of mappings! \" + size);\n\n        // change the value for every key and check\n        for (Map.Entry<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> e : state.entrySet()) {\n            ByteArrayWrapper key = e.getKey();\n            Multihash value = randomHash.get();\n            Optional<CborObject.CborMerkleLink> currentValue = current.get(owner, e.getKey(), hasher.apply(e.getKey()).join(), 0, bitWidth, storage).get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(owner, user, key, hasher.apply(key).join(), 0, currentValue,\n                    Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage, writeHasher, currentHash).get();\n            Optional<CborObject.CborMerkleLink> result = updated.left.get(owner, key, hasher.apply(key).join(), 0, bitWidth, storage).get();\n            if (! result.equals(Optional.of(new CborObject.CborMerkleLink(value))))\n                throw new IllegalStateException(\"Incorrect result!\");\n            state.put(key, Optional.of(new CborObject.CborMerkleLink(value)));\n            current = updated.left;\n            currentHash = updated.right;\n        }\n\n        // remove each key and check the mapping is gone\n        for (Map.Entry<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> e : state.entrySet()) {\n            ByteArrayWrapper key = e.getKey();\n            Optional<CborObject.CborMerkleLink> currentValue = current.get(owner, e.getKey(), hasher.apply(e.getKey()).join(), 0, bitWidth, storage).get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.remove(owner, user, key, hasher.apply(key).join(), 0, currentValue,\n                    bitWidth, maxCollisions, Optional.empty(), tid, storage, writeHasher, currentHash).get();\n            Optional<CborObject.CborMerkleLink> result = updated.left.get(owner, key, hasher.apply(key).join(), 0, bitWidth, storage).get();\n            if (! result.equals(Optional.empty()))\n                throw new IllegalStateException(\"Incorrect state!\");\n        }\n\n        // add a random mapping, then remove it, and check we return to the canonical state\n        for (int i = 0; i < 100; i++) {\n            ByteArrayWrapper key = new ByteArrayWrapper(randomHash.get().toBytes());\n            Multihash value = randomHash.get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(owner, user, key, hasher.apply(key).join(), 0, Optional.empty(),\n                    Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage, writeHasher, currentHash).get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> removed = updated.left.remove(owner, user, key, hasher.apply(key).join(), 0,\n                    Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), tid, storage, writeHasher, updated.right).get();\n            if (! removed.right.equals(currentHash))\n                throw new IllegalStateException(\"Non canonical state!\");\n        }\n\n        testInsertionOrderIndependence(state, currentHash, bitWidth, maxCollisions, storage, user);\n    }\n\n    private void testInsertionOrderIndependence(Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> state,\n                                                Multihash expectedRoot,\n                                                int bitWidth,\n                                                int maxCollisions,\n                                                ContentAddressedStorage storage,\n                                                SigningPrivateKeyAndPublicHash user) {\n        ArrayList<Map.Entry<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>>> mappings = new ArrayList<>(state.entrySet());\n        Random r = new Random();\n        int orderings = 1;\n        for (int i = 0; i < orderings; i++) {\n            Collections.shuffle(mappings, r);\n            Champ<CborObject.CborMerkleLink> current = Champ.empty(c -> (CborObject.CborMerkleLink)c);\n            TransactionId tid = storage.startTransaction(user.publicKeyHash).join();\n            Multihash currentHash = storage.put(user.publicKeyHash, user, current.serialize(), writeHasher, tid).join();\n            for (int k=0; k < mappings.size(); k++) {\n                ByteArrayWrapper key = mappings.get(k).getKey();\n                Multihash value = mappings.get(k).getValue().get().target;\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                        Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage,\n                        writeHasher, currentHash).join();\n                current = updated.left;\n                currentHash = updated.right;\n            }\n            if (! currentHash.equals(expectedRoot))\n                throw new IllegalStateException(\"Champ is not insertion order independent!\");\n        }\n    }\n\n    @Test\n    public void diff() throws Exception {\n        DeletableContentAddressedStorage storage = new FileContentAddressedStorage(Files.createTempDirectory(\"peergos-tmp\"),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n        RamPki pki = new RamPki();\n        storage.setPki(pki);\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        pki.reverseLookup.put(user.publicKeyHash, \"eve\");\n        Random r = new Random(28);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n\n        Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> state = new HashMap<>();\n\n        Champ<CborObject.CborMerkleLink> current = Champ.empty(c -> (CborObject.CborMerkleLink)c);\n        TransactionId tid = storage.startTransaction(user.publicKeyHash).get();\n        Multihash currentHash = storage.put(user.publicKeyHash, user, current.serialize(), writeHasher, tid).get();\n        int bitWidth = 4;\n        int maxCollisions = 2;\n        // build a random tree and keep track of the state\n        for (int i = 0; i < 100; i++) {\n            ByteArrayWrapper key = new ByteArrayWrapper(new byte[]{0, (byte)i, 0});\n            Multihash value = randomHash.get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                    Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage,\n                    writeHasher, currentHash).get();\n            current = updated.left;\n            currentHash = updated.right;\n            state.put(key, Optional.of(new CborObject.CborMerkleLink(value)));\n        }\n        Cid ourId = new Cid(1, Cid.Codec.Raw, Multihash.Type.sha2_256, new byte[32]);\n\n        List<ByteArrayWrapper> keys = state.keySet().stream().collect(Collectors.toList());\n        // update random entries\n        for (int i=0; i < 100; i++) {\n            ByteArrayWrapper key = keys.get(r.nextInt(keys.size()));\n            Optional<CborObject.CborMerkleLink> currentValue = state.get(key);\n            Optional<CborObject.CborMerkleLink> newValue = r.nextBoolean() ? Optional.of(new CborObject.CborMerkleLink(randomHash.get())) : Optional.empty();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0, currentValue,\n                    newValue, bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage, writeHasher, currentHash).get();\n            List<Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> diffs = new ArrayList<>();\n\n            IpfsCoreNode.applyToDiff(Collections.emptyList(), ourId, user.publicKeyHash, MaybeMultihash.of(currentHash), MaybeMultihash.of(updated.right), 0, hasher,\n                    Collections.emptyList(), Collections.emptyList(), diffs::add, bitWidth, storage, crypto.hasher, c -> (CborObject.CborMerkleLink)c).join();\n            if (diffs.size() != 1 || ! diffs.get(0).equals(new Triple<>(key, currentValue, newValue)))\n                throw new IllegalStateException(\"Incorrect champ diff updating element!\");\n        }\n\n        // add a random entry\n        for (int i=0; i < 100; i++) {\n            byte[] keyBytes = new byte[32];\n            r.nextBytes(keyBytes);\n            ByteArrayWrapper key = new ByteArrayWrapper(keyBytes);\n            Optional<CborObject.CborMerkleLink> currentValue = Optional.ofNullable(state.get(key))\n                    .orElse(Optional.empty());\n            Optional<CborObject.CborMerkleLink> newValue = Optional.of(new CborObject.CborMerkleLink(randomHash.get()));\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0, currentValue,\n                    newValue, bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage, writeHasher, currentHash).get();\n            List<Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> diffs = new ArrayList<>();\n\n            IpfsCoreNode.applyToDiff(Collections.emptyList(), ourId, user.publicKeyHash, MaybeMultihash.of(currentHash), MaybeMultihash.of(updated.right), 0, hasher,\n                    Collections.emptyList(), Collections.emptyList(), diffs::add, bitWidth, storage, crypto.hasher, c -> (CborObject.CborMerkleLink)c).join();\n            if (diffs.size() != 1 || ! diffs.get(0).equals(new Triple<>(key, currentValue, newValue)))\n                throw new IllegalStateException(\"Incorrect champ diff updating element!\");\n        }\n\n        // add random entries which share a key prefix\n        for (int i=0; i < 100; i++) {\n            ByteArrayWrapper keyPrefix = keys.get(r.nextInt(keys.size()));\n            byte[] longerKey = Arrays.copyOfRange(keyPrefix.data, 0, keyPrefix.data.length + 1);\n            longerKey[longerKey.length - 1] = (byte) r.nextInt();\n            ByteArrayWrapper key = new ByteArrayWrapper(longerKey);\n            Optional<CborObject.CborMerkleLink> currentValue = Optional.empty();\n            Optional<CborObject.CborMerkleLink> newValue = Optional.of(new CborObject.CborMerkleLink(randomHash.get()));\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0, currentValue,\n                    newValue, bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage, writeHasher, currentHash).get();\n            List<Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> diffs = new ArrayList<>();\n            IpfsCoreNode.applyToDiff(Collections.emptyList(), ourId, user.publicKeyHash, MaybeMultihash.of(currentHash), MaybeMultihash.of(updated.right), 0, hasher,\n                    Collections.emptyList(), Collections.emptyList(), diffs::add, bitWidth, storage, crypto.hasher, c -> (CborObject.CborMerkleLink)c).join();\n            if (diffs.size() != 1 || ! diffs.get(0).equals(new Triple<>(key, currentValue, newValue)))\n                throw new IllegalStateException(\"Incorrect champ diff updating element!\");\n        }\n    }\n\n    @Test\n    public void canonicalDelete() throws Exception {\n        RAMStorage storage = new RAMStorage(crypto.hasher);\n        int bitWidth = 5;\n        int maxCollisions = 3;\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        Random r = new Random(28);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n        TransactionId tid = storage.startTransaction(user.publicKeyHash).get();\n\n        for (int prefixLen = 0; prefixLen < 5; prefixLen++)\n            for (int i=0; i < 100; i++) {\n                int suffixLen = 5;\n                int nKeys = r.nextInt(10);\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> root = randomTree(user, r, prefixLen, suffixLen, nKeys, bitWidth, maxCollisions,\n                        Optional.empty(), hasher, randomHash, storage);\n                byte[] keyBytes = new byte[prefixLen + suffixLen];\n                r.nextBytes(keyBytes);\n                ByteArrayWrapper key = new ByteArrayWrapper(keyBytes);\n                Multihash value = randomHash.get();\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> added = root.left.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                        Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage,\n                        writeHasher, root.right).get();\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> removed = added.left.remove(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                        Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), tid, storage, writeHasher, added.right).get();\n                if (! removed.right.equals(root.right))\n                    throw new IllegalStateException(\"Non canonical delete!\");\n            }\n    }\n\n    @Test\n    public void merge() throws Exception {\n        RAMStorage storage = new RAMStorage(crypto.hasher);\n        int bitWidth = 5;\n        int maxCollisions = 3;\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        PublicKeyHash owner = user.publicKeyHash;\n        Random r = new Random(28);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n        TransactionId tid = storage.startTransaction(user.publicKeyHash).get();\n\n        for (int prefixLen = 0; prefixLen < 5; prefixLen++)\n            for (int i=0; i < 100; i++) {\n                int suffixLen = 5;\n                int nKeys = r.nextInt(10);\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> root = randomTree(user, r, prefixLen, suffixLen, nKeys, bitWidth, maxCollisions,\n                        Optional.empty(), hasher, randomHash, storage);\n\n                Map<ByteArrayWrapper, Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> original = new HashMap<>();\n                Function<Cborable, CborObject.CborMerkleLink> fromCbor = c -> (CborObject.CborMerkleLink) c;\n                ChampUtil.applyToDiff(owner, MaybeMultihash.empty(), MaybeMultihash.of(root.right), 0, hasher,\n                        Collections.emptyList(), Collections.emptyList(), t -> original.put(t.left, t), bitWidth, storage, fromCbor).join();\n\n                Map<ByteArrayWrapper, Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> remoteUpdated = new HashMap<>(original);\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> remoteRoot = root;\n                for (int j=0; j < 10; j++) {\n                    byte[] keyBytes = new byte[prefixLen + suffixLen];\n                    r.nextBytes(keyBytes);\n                    ByteArrayWrapper key = new ByteArrayWrapper(keyBytes);\n                    Multihash value = randomHash.get();\n                    if (original.containsKey(key) || remoteUpdated.containsKey(key))\n                        continue;\n                    remoteUpdated.put(key, new Triple<>(key, Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value))));\n                    remoteRoot = remoteRoot.left.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                            Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage,\n                            writeHasher, root.right).get();\n                }\n\n                Map<ByteArrayWrapper, Triple<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>, Optional<CborObject.CborMerkleLink>>> localUpdated = new HashMap<>(original);\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> localRoot = root;\n                for (int j=0; j < 10; j++) {\n                    byte[] keyBytes = new byte[prefixLen + suffixLen];\n                    r.nextBytes(keyBytes);\n                    ByteArrayWrapper key = new ByteArrayWrapper(keyBytes);\n                    Multihash value = randomHash.get();\n                    if (remoteUpdated.containsKey(key) || localUpdated.containsKey(key))\n                        continue;\n                    localUpdated.put(key, new Triple<>(key, Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value))));\n                    localRoot = localRoot.left.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                            Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage,\n                            writeHasher, root.right).get();\n                }\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> merged = ChampUtil.merge(owner, user, MaybeMultihash.of(root.right), MaybeMultihash.of(localRoot.right),\n                        MaybeMultihash.of(remoteRoot.right), Optional.empty(), tid, bitWidth, maxCollisions, hasher, fromCbor, storage, writeHasher).join();\n\n                Map<ByteArrayWrapper, Multihash> newMappings = new HashMap<>();\n                ChampUtil.applyToDiff(owner, MaybeMultihash.empty(), MaybeMultihash.of(merged.right), 0,\n                        hasher, Collections.emptyList(), Collections.emptyList(), t -> {\n                            newMappings.put(t.left, t.right.get().target);\n                        }, bitWidth, storage, fromCbor).join();\n                Assert.assertTrue(newMappings.keySet().containsAll(localUpdated.keySet()));\n                Assert.assertTrue(newMappings.keySet().containsAll(remoteUpdated.keySet()));\n            }\n    }\n\n    @Test\n    public void mirrorOnRoot() throws Exception {\n        RAMStorage storage = new RAMStorage(crypto.hasher);\n        int bitWidth = 5;\n        int maxCollisions = 3;\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        Random r = new Random(28);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n        TransactionId tid = storage.startTransaction(user.publicKeyHash).get();\n        Optional<BatId> mirrorBat = Optional.of(Bat.random(crypto.random).calculateId(crypto.hasher).join());\n\n        for (int prefixLen = 0; prefixLen < 5; prefixLen++)\n            for (int i=0; i < 100; i++) {\n                int suffixLen = 5;\n                int nKeys = r.nextInt(10);\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> root = randomTree(user, r, prefixLen, suffixLen, nKeys, bitWidth, maxCollisions,\n                        mirrorBat, hasher, randomHash, storage);\n                byte[] keyBytes = new byte[prefixLen + suffixLen];\n                r.nextBytes(keyBytes);\n                ByteArrayWrapper key = new ByteArrayWrapper(keyBytes);\n                Multihash value = randomHash.get();\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> added = root.left.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                        Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, mirrorBat, hasher, tid, storage,\n                        writeHasher, root.right).get();\n                Assert.assertTrue(added.left.mirrorBat.isPresent());\n\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> removed = added.left.remove(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                        Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, mirrorBat, tid, storage, writeHasher, added.right).get();\n                Assert.assertTrue(removed.left.mirrorBat.isPresent());\n                if (! removed.right.equals(root.right))\n                    throw new IllegalStateException(\"Non canonical delete!\");\n            }\n    }\n\n    @Test\n    public void correctDelete() throws Exception {\n        DeletableContentAddressedStorage storage = new FileContentAddressedStorage(Files.createTempDirectory(\"peergos-tmp\"),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n        RamPki pki = new RamPki();\n        storage.setPki(pki);\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        PublicKeyHash owner = user.publicKeyHash;\n        pki.reverseLookup.put(owner, \"bob\");\n        Random r = new Random(28);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n\n        Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> state = new HashMap<>();\n\n        Champ<CborObject.CborMerkleLink> current = Champ.empty(c -> (CborObject.CborMerkleLink)c);\n        TransactionId tid = storage.startTransaction(user.publicKeyHash).get();\n        Multihash currentHash = storage.put(user.publicKeyHash, user, current.serialize(), writeHasher, tid).get();\n        int bitWidth = 4;\n        int maxCollisions = 2;\n        // build a random tree and keep track of the state\n        for (int i = 0; i < 3; i++) {\n            ByteArrayWrapper key = new ByteArrayWrapper(new byte[]{0, (byte)i, 0});\n            Multihash value = randomHash.get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(user.publicKeyHash, user, key, hasher.apply(key).join(), 0,\n                    Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)), bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage,\n                    writeHasher, currentHash).get();\n            current = updated.left;\n            currentHash = updated.right;\n            state.put(key, Optional.of(new CborObject.CborMerkleLink(value)));\n        }\n\n        // check every mapping\n        for (Map.Entry<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> e : state.entrySet()) {\n            Optional<CborObject.CborMerkleLink> res = current.get(owner, e.getKey(), hasher.apply(e.getKey()).join(), 0, bitWidth, storage).get();\n            if (! res.equals(e.getValue()))\n                throw new IllegalStateException(\"Incorrect state!\");\n        }\n\n        long size = current.size(owner, 0, storage).get();\n        if (size != 3)\n            throw new IllegalStateException(\"Incorrect number of mappings! \" + size);\n\n        // delete one entry\n        ByteArrayWrapper key = new ByteArrayWrapper(new byte[]{0, 1, 0});\n        Optional<CborObject.CborMerkleLink> currentValue = current.get(owner, key, hasher.apply(key).join(), 0, bitWidth, storage).get();\n        Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.remove(user.publicKeyHash, user, key, hasher.apply(key).join(), 0, currentValue,\n                bitWidth, maxCollisions, Optional.empty(), tid, storage, writeHasher, currentHash).get();\n        current = updated.left;\n        state.remove(key);\n        Optional<CborObject.CborMerkleLink> result = updated.left.get(owner, key, hasher.apply(key).join(), 0, bitWidth, storage).get();\n        if (! result.equals(Optional.empty()))\n            throw new IllegalStateException(\"Incorrect state!\");\n\n        long size_after_delete = current.size(owner, 0, storage).get();\n        if (size_after_delete != 2)\n            throw new IllegalStateException(\"Incorrect number of mappings! \" + size);\n\n        // check every mapping\n        for (Map.Entry<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> e : state.entrySet()) {\n            Optional<CborObject.CborMerkleLink> res = current.get(owner, e.getKey(), hasher.apply(e.getKey()).join(), 0, bitWidth, storage).get();\n            if (! res.equals(e.getValue()))\n                throw new IllegalStateException(\"Incorrect state!\");\n        }\n    }\n\n    @Test\n    public void bulkDelete() throws Exception {\n        RAMStorage storage = new RAMStorage(crypto.hasher);\n        int bitWidth = 5;\n        int maxCollisions = 3;\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        PublicKeyHash owner = user.publicKeyHash;\n        Random r = new Random(42);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n        TransactionId tid = storage.startTransaction(owner).get();\n\n        int nKeys = 1000;\n        List<ByteArrayWrapper> keys = new ArrayList<>();\n        List<CborObject.CborMerkleLink> values = new ArrayList<>();\n\n        Champ<CborObject.CborMerkleLink> current = Champ.empty(c -> (CborObject.CborMerkleLink) c);\n        Multihash currentHash = storage.put(owner, user, current.serialize(), writeHasher, tid).get();\n\n        for (int i = 0; i < nKeys; i++) {\n            ByteArrayWrapper key = new ByteArrayWrapper(randomHash.get().toBytes());\n            CborObject.CborMerkleLink value = new CborObject.CborMerkleLink(randomHash.get());\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(owner, user, key,\n                    hasher.apply(key).join(), 0, Optional.empty(), Optional.of(value),\n                    bitWidth, maxCollisions, Optional.empty(), hasher, tid, storage, writeHasher, currentHash).get();\n            current = updated.left;\n            currentHash = updated.right;\n            keys.add(key);\n            values.add(value);\n        }\n        Assert.assertEquals(nKeys, (long) current.size(owner, 0, storage).get());\n\n        Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> expectedAll = new HashMap<>();\n        for (int i = 0; i < nKeys; i++) expectedAll.put(keys.get(i), Optional.of(values.get(i)));\n\n        // --- Full bulk delete ---\n        List<Pair<ByteArrayWrapper, byte[]>> allKeysAndHashes = keys.stream()\n                .map(k -> new Pair<>(k, hasher.apply(k).join()))\n                .collect(Collectors.toList());\n\n        Pair<Champ<CborObject.CborMerkleLink>, Multihash> afterBulk = current.removeAll(owner, user,\n                allKeysAndHashes, expectedAll, 0, bitWidth, maxCollisions, Optional.empty(),\n                tid, storage, writeHasher, currentHash).get();\n\n        Assert.assertEquals(\"tree must be empty after full bulk delete\", 0L,\n                (long) afterBulk.left.size(owner, 0, storage).get());\n        for (ByteArrayWrapper key : keys) {\n            Optional<CborObject.CborMerkleLink> res = afterBulk.left.get(owner, key,\n                    hasher.apply(key).join(), 0, bitWidth, storage).get();\n            Assert.assertEquals(Optional.empty(), res);\n        }\n\n        // Root must match the result of sequential removes (canonical structure)\n        Champ<CborObject.CborMerkleLink> seqCurrent = current;\n        Multihash seqHash = currentHash;\n        for (int i = 0; i < nKeys; i++) {\n            ByteArrayWrapper key = keys.get(i);\n            seqCurrent = seqCurrent.remove(owner, user, key, hasher.apply(key).join(), 0,\n                    Optional.of(values.get(i)), bitWidth, maxCollisions, Optional.empty(),\n                    tid, storage, writeHasher, seqHash).get().left;\n            seqHash = seqCurrent.equals(Champ.empty(c -> (CborObject.CborMerkleLink) c))\n                    ? seqHash  // will be recomputed below\n                    : seqHash; // placeholder — we just need the final seqHash\n        }\n        // Recompute seqHash properly\n        seqCurrent = current;\n        seqHash = currentHash;\n        for (int i = 0; i < nKeys; i++) {\n            ByteArrayWrapper key = keys.get(i);\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> step = seqCurrent.remove(owner, user,\n                    key, hasher.apply(key).join(), 0, Optional.of(values.get(i)),\n                    bitWidth, maxCollisions, Optional.empty(), tid, storage, writeHasher, seqHash).get();\n            seqCurrent = step.left;\n            seqHash = step.right;\n        }\n        Assert.assertEquals(\"full bulkDelete root must match sequential remove\", seqHash, afterBulk.right);\n\n        // --- Partial bulk delete: remove the first half, keep the second half ---\n        int half = nKeys / 2;\n        List<Pair<ByteArrayWrapper, byte[]>> partialKeysAndHashes = keys.subList(0, half).stream()\n                .map(k -> new Pair<>(k, hasher.apply(k).join()))\n                .collect(Collectors.toList());\n        Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> partialExpected = new HashMap<>();\n        for (int i = 0; i < half; i++) partialExpected.put(keys.get(i), Optional.of(values.get(i)));\n\n        Pair<Champ<CborObject.CborMerkleLink>, Multihash> afterPartial = current.removeAll(owner, user,\n                partialKeysAndHashes, partialExpected, 0, bitWidth, maxCollisions, Optional.empty(),\n                tid, storage, writeHasher, currentHash).get();\n\n        Assert.assertEquals(\"size must be half after partial bulk delete\", (long) half,\n                (long) afterPartial.left.size(owner, 0, storage).get());\n\n        for (int i = 0; i < half; i++) {\n            Optional<CborObject.CborMerkleLink> res = afterPartial.left.get(owner, keys.get(i),\n                    hasher.apply(keys.get(i)).join(), 0, bitWidth, storage).get();\n            Assert.assertEquals(\"deleted key must be absent\", Optional.empty(), res);\n        }\n        for (int i = half; i < nKeys; i++) {\n            Optional<CborObject.CborMerkleLink> res = afterPartial.left.get(owner, keys.get(i),\n                    hasher.apply(keys.get(i)).join(), 0, bitWidth, storage).get();\n            Assert.assertEquals(\"kept key must still be present\", Optional.of(values.get(i)), res);\n        }\n\n        // Partial bulk-remove root must match sequential removes of the same keys\n        Champ<CborObject.CborMerkleLink> seqPartial = current;\n        Multihash seqPartialHash = currentHash;\n        for (int i = 0; i < half; i++) {\n            ByteArrayWrapper key = keys.get(i);\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> step = seqPartial.remove(owner, user,\n                    key, hasher.apply(key).join(), 0, Optional.of(values.get(i)),\n                    bitWidth, maxCollisions, Optional.empty(), tid, storage, writeHasher, seqPartialHash).get();\n            seqPartial = step.left;\n            seqPartialHash = step.right;\n        }\n        Assert.assertEquals(\"partial bulkDelete root must match sequential remove\", seqPartialHash, afterPartial.right);\n    }\n\n    private static byte[] randomKey(byte[] startingWith, int extraBytes, Random r) {\n        byte[] suffix = new byte[extraBytes];\n        r.nextBytes(suffix);\n        byte[] res = new byte[startingWith.length + extraBytes];\n        System.arraycopy(startingWith, 0, res, 0, startingWith.length);\n        System.arraycopy(suffix, 0, res, startingWith.length, suffix.length);\n        return res;\n    }\n\n    private static Pair<Champ<CborObject.CborMerkleLink>, Multihash> randomTree(SigningPrivateKeyAndPublicHash user,\n                                                     Random r,\n                                                     int prefixLen,\n                                                     int suffixLen,\n                                                     int nKeys,\n                                                     int bitWidth,\n                                                     int maxCollisions,\n                                                     Optional<BatId> mirrorBat,\n                                                     Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher,\n                                                     Supplier<Multihash> randomHash,\n                                                     RAMStorage storage) throws Exception {\n        Champ<CborObject.CborMerkleLink> current = Champ.empty(c -> (CborObject.CborMerkleLink)c).withBat(mirrorBat);\n        TransactionId tid = storage.startTransaction(user.publicKeyHash).get();\n        Multihash currentHash = storage.put(user.publicKeyHash, user, current.serialize(), writeHasher, tid).get();\n        // build a random tree and keep track of the state\n        byte[] prefix = new byte[prefixLen];\n        r.nextBytes(prefix);\n        for (int i = 0; i < nKeys; i++) {\n            ByteArrayWrapper key = new ByteArrayWrapper(randomKey(prefix, suffixLen, r));\n            Multihash value = randomHash.get();\n            Pair<Champ<CborObject.CborMerkleLink>, Multihash> updated = current.put(user.publicKeyHash, user, key,\n                    hasher.apply(key).join(), 0, Optional.empty(), Optional.of(new CborObject.CborMerkleLink(value)),\n                    bitWidth, maxCollisions, mirrorBat, hasher, tid, storage, writeHasher, currentHash).get();\n            current = updated.left;\n            currentHash = updated.right;\n        }\n        return new Pair<>(current, currentHash);\n    }\n\n    public static SigningPrivateKeyAndPublicHash createUser(ContentAddressedStorage storage, Crypto crypto) {\n        SigningKeyPair random = SigningKeyPair.random(crypto.random, crypto.signer);\n        try {\n            PublicKeyHash ownerHash = ContentAddressedStorage.hashKey(random.publicSigningKey);\n            TransactionId tid = storage.startTransaction(ownerHash).get();\n            PublicKeyHash publicHash = storage.putSigningKey(\n                    random.secretSigningKey.signMessage(random.publicSigningKey.serialize()).join(),\n                    ownerHash,\n                    random.publicSigningKey, tid).get();\n            return new SigningPrivateKeyAndPublicHash(publicHash, random.secretSigningKey);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/CidTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.bases.*;\n\nimport java.util.*;\n\npublic class CidTests {\n\n    @Test\n    public void nodeid() {\n        byte[] rawPublicKey = Multibase.decode(\"mCAESIA2UgZjCUFpg3P2C4EC+kFNq9KwzTVTpTHu51Y7fQAFg\");\n        String peerID = \"12D3KooWAjNorDWXZJx8Jhzrq9onKbrmYf9XSmteb4yKnbXfSD8K\";\n        byte[] rawPeerId = Base58.decode(peerID);\n        Assert.assertTrue(Arrays.equals(rawPublicKey, Arrays.copyOfRange(rawPeerId, 2, rawPeerId.length)));\n\n        // convert identity multihash to cidV1\n        Multihash hash = Multihash.decode(Base58.decode(peerID));\n        Cid cid = new Cid(1, Cid.Codec.LibP2pKey, hash.type, hash.getHash());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/CorenodeTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.corenode.UsernameValidator;\n\nimport java.util.*;\n\npublic class CorenodeTests {\n\n    @Test\n    public void isValidUsernameTest() {\n        List<String> areValid = Arrays.asList(\n                \"chris\",\n                \"super-califragilistic-ex\",\n                \"z\",\n                \"ch-ris\",\n                \"123456789012345678901234567890ab\",\n                \"1337\",\n                \"alpha-beta\",\n                \"the-god-father\");\n\n        List<String> areNotValid = Arrays.asList(\n                \"123456789012345678901234567890abc\",\n                \"\",\n                \" \",\n                \"super_califragilistic_expialidocious\",\n                \"\\n\",\n                \"\\r\",\n                \"\\tted\",\n                \"-ted\",\n                \"_ted\",\n                \"t__ed\",\n                \"ted_\",\n                \" ted\",\n                \"<ted>\",\n                \"ted-\",\n                \"a-_b\",\n                \"a_-b\",\n                \"a--b\",\n                \"a_b\",\n                \"fred--flinstone\",\n                \"peter-_pan\",\n                \"_hello\",\n                \"hello.\",\n                \"\\b0\");\n\n        areValid.forEach(username -> Assert.assertTrue(username + \" is valid\", UsernameValidator.isValidUsername(username)));\n        areNotValid.forEach(username -> Assert.assertFalse(username +\" is not valid\", UsernameValidator.isValidUsername(username)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/DifficultyGeneratorTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.*;\n\nimport java.util.*;\n\npublic class DifficultyGeneratorTests {\n\n    @Test\n    public void rateLimited() {\n        int maxPerDay = 1000;\n        long startTime = System.currentTimeMillis();\n        long currentTime = startTime;\n        DifficultyGenerator gen = new DifficultyGenerator(currentTime, maxPerDay);\n        int events = 100;\n        for (int i = 0; i < events; i++) {\n            gen.addEvent();\n            int diff = gen.currentDifficulty();\n            System.out.println(\"Difficulty \" + diff);\n            long toSleep = 1L << diff;\n            currentTime += toSleep;\n            gen.updateTime(currentTime);\n        }\n        long minDuration = maxPerDay * 86400_000L / events;\n        long duration = currentTime - startTime;\n        Assert.assertTrue(\"Won't exceed daily limit\", duration > minDuration);\n\n        // Check recovery after a delay\n        gen.updateTime(currentTime + 86400_000L * events / maxPerDay);\n        int afterDelay = gen.currentDifficulty();\n        Assert.assertTrue(\"Reduce difficulty after period of no queries\", afterDelay == ProofOfWork.MIN_DIFFICULTY);\n    }\n\n    /** Simulate different hardware which gives a different constant factor in proof of work slow down\n     *\n     */\n    @Test\n    public void differentHardware() {\n        for (int i: List.of(10_000, 1_000, 1_000_000))\n            differentHardware(i);\n    }\n\n    public void differentHardware(int hardwareSpeedup) {\n        int maxPerDay = 1000;\n        long startTime = System.currentTimeMillis();\n        long currentTime = startTime;\n        DifficultyGenerator gen = new DifficultyGenerator(currentTime, maxPerDay);\n        int events = 1000;\n        for (int i = 0; i < events; i++) {\n            gen.addEvent();\n            int diff = gen.currentDifficulty();\n            System.out.println(\"Difficulty \" + diff);\n            long toSleep = (1L << diff) / hardwareSpeedup;\n            currentTime += toSleep;\n            gen.updateTime(currentTime);\n        }\n        long minDuration = maxPerDay * 86400_000L / events;\n        long duration = currentTime - startTime;\n        Assert.assertTrue(\"Won't exceed daily limit\", duration > minDuration);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/Exceptions.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\n\npublic class Exceptions {\n\n\n    @Test\n    public void cause() {\n        try {\n            rethrows();\n        } catch (Throwable t) {\n            Throwable cause = peergos.shared.util.Exceptions.getRootCause(t);\n            if (! (cause instanceof IllegalStateException))\n                throw new IllegalStateException(\"Fail\");\n        }\n    }\n\n    private static void rethrows() {\n        try {\n            throwsIllegal();\n        } catch (Throwable t) {\n            throw new RuntimeException(t);\n        }\n    }\n\n    private static void throwsIllegal() {\n        throw new IllegalStateException(\"Bob\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/FileBlockBufferTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.Sqlite;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class FileBlockBufferTests {\n\n    /**\n     * Regression test for the startup-flush bug where FileBlockBuffer.applyToAll() silently\n     * skipped all user-partitioned blocks (root/username/shard/HASH.data).\n     *\n     * The old implementation delegated to FileContentAddressedStorage.getFilesRecursive(),\n     * which expected paths with PublicKeyHash components (root/PublicKeyHash/shard/HASH.data).\n     * For the username-based layout it computed path.relativize(root) = \"../..\", giving\n     * nameCount=2, so owner was always set to null.  The startup flush then called\n     * blockBuffer.get(null, cid), which looked for the file at the legacy path\n     * root/shard/HASH.data, found nothing, and silently skipped the block.\n     *\n     * After the fix, applyToAll() resolves each top-level directory name as a username via\n     * usage.getOwnerKey(username), yielding the correct PublicKeyHash owner.\n     */\n    @Test\n    public void applyToAllFindsUserPartitionedBlocks() throws Exception {\n        Crypto crypto = Main.initCrypto();\n\n        // Set up an in-memory usage store with a registered user and owner key.\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        JdbcUsageStore usageStore = new JdbcUsageStore(() -> db, new SqliteCommands());\n        String username = \"alice\";\n        usageStore.addUserIfAbsent(username);\n        PublicKeyHash ownerKey = new PublicKeyHash(\n                Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, crypto.random.randomBytes(36)));\n        usageStore.addWriter(username, ownerKey);\n\n        Path tmpDir = Files.createTempDirectory(\"peergos-block-buffer-test\");\n        try {\n            FileBlockBuffer buffer = new FileBlockBuffer(tmpDir, usageStore);\n\n            // Write a dag-cbor block into the buffer.\n            byte[] data = \"test dag-cbor block content\".getBytes();\n            Multihash hash = new Multihash(Multihash.Type.sha2_256, crypto.hasher.sha256(data).join());\n            Cid cid = new Cid(1, Cid.Codec.DagCbor, hash.type, hash.getHash());\n\n            buffer.put(ownerKey, cid, data).join();\n\n            // applyToAll simulates what the startup flush does to re-populate blocksToFlush.\n            List<Pair<PublicKeyHash, Cid>> found = new ArrayList<>();\n            buffer.applyToAll((owner, c) -> found.add(new Pair<>(owner, c)));\n\n            Assert.assertEquals(\"applyToAll should find exactly one block\", 1, found.size());\n\n            // Before the fix this was null; after the fix it is ownerKey.\n            Assert.assertEquals(\n                    \"applyToAll must return the correct owner PublicKeyHash, not null\",\n                    ownerKey, found.get(0).left);\n            Assert.assertEquals(cid, found.get(0).right);\n\n            // Verify the block is actually retrievable using the owner returned by applyToAll.\n            // Before the fix, found.get(0).left was null so get() used the legacy path and\n            // returned Optional.empty(), meaning the startup flush would skip the block entirely.\n            Optional<byte[]> retrieved = buffer.get(found.get(0).left, found.get(0).right).join();\n            Assert.assertTrue(\n                    \"Block must be retrievable via the owner returned by applyToAll\",\n                    retrieved.isPresent());\n            Assert.assertArrayEquals(data, retrieved.get());\n        } finally {\n            try (Stream<Path> paths = Files.walk(tmpDir)) {\n                paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/FileChunkBinarySearchTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/**\n * Fast unit tests for FileWrapper.binarySearchAbsentChunk.\n *\n * These tests bypass all network/storage layers: they pre-derive the real map-key+bat\n * chain for N chunks, then inject a synthetic lookup function that answers\n * \"present if index < K\" to verify that the binary search returns K in\n * O(log₈ N) CHAMP round-trips rather than O(N).\n */\npublic class FileChunkBinarySearchTests {\n\n    private static final Crypto crypto = Main.initCrypto();\n    private static final Hasher hasher = crypto.hasher;\n    private static final Random random = new Random(0xdeadbeef);\n\n    /**\n     * Pre-computes all N (mapKey, bat) pairs for the given stream secret, first key and first bat.\n     * Uses the cumulative chain so each step is just one hash.\n     */\n    private static List<Pair<byte[], Optional<Bat>>> computeAllProbes(\n            byte[] streamSecret, byte[] firstKey, Optional<Bat> firstBat, int n) {\n        List<Pair<byte[], Optional<Bat>>> probes = new ArrayList<>(n);\n        if (n == 0) return probes;\n        probes.add(new Pair<>(firstKey, firstBat));\n        for (int i = 1; i < n; i++) {\n            Pair<byte[], Optional<Bat>> prev = probes.get(i - 1);\n            probes.add(FileProperties.calculateMapKey(streamSecret, prev.left, prev.right,\n                    Chunk.MAX_SIZE, hasher).join());\n        }\n        return probes;\n    }\n\n    /**\n     * Builds a lookup function over pre-computed probes.\n     * Probes with index < k are \"present\"; index >= k are \"absent\".\n     * Also counts the number of CHAMP round-trips (each call = one bulk server request).\n     */\n    private static Function<List<Pair<byte[], Optional<Bat>>>, CompletableFuture<List<Boolean>>> buildLookup(\n            List<Pair<byte[], Optional<Bat>>> allProbes, int k, int[] callCount) {\n        Set<ByteArrayWrapper> presentSet = new HashSet<>();\n        for (int i = 0; i < k; i++)\n            presentSet.add(new ByteArrayWrapper(allProbes.get(i).left));\n        return ps -> {\n            callCount[0]++;\n            return Futures.of(ps.stream()\n                    .map(p -> presentSet.contains(new ByteArrayWrapper(p.left)))\n                    .collect(Collectors.toList()));\n        };\n    }\n\n    private void runSearch(int n, int k) {\n        byte[] streamSecret = new byte[32];\n        random.nextBytes(streamSecret);\n        byte[] firstKey = new byte[32];\n        random.nextBytes(firstKey);\n        Optional<Bat> firstBat = Optional.of(new Bat(new byte[32]));\n        random.nextBytes(firstBat.get().secret);\n\n        List<Pair<byte[], Optional<Bat>>> allProbes = computeAllProbes(streamSecret, firstKey, firstBat, Math.max(n, 1));\n\n        int[] callCount = {0};\n        Function<List<Pair<byte[], Optional<Bat>>>, CompletableFuture<List<Boolean>>> lookup =\n                buildLookup(allProbes, k, callCount);\n\n        long result = FileWrapper.binarySearchAbsentChunk(\n                streamSecret, 0L, n, firstKey, firstBat, lookup, hasher).join();\n\n        Assert.assertEquals(\"N=\" + n + \" k=\" + k + \": wrong first absent chunk\", (long) k, result);\n\n        // Verify O(log₈ N) round-trips.\n        int maxExpectedCalls = (int) Math.ceil(Math.log(Math.max(n, 1)) / Math.log(8)) + 2;\n        Assert.assertTrue(\"N=\" + n + \" k=\" + k + \": too many CHAMP calls: \" + callCount[0]\n                + \" (expected ≤ \" + maxExpectedCalls + \")\", callCount[0] <= maxExpectedCalls);\n    }\n\n    @Test\n    public void zeroChunks() {\n        byte[] streamSecret = new byte[32];\n        byte[] firstKey = new byte[32];\n        int[] calls = {0};\n        Function<List<Pair<byte[], Optional<Bat>>>, CompletableFuture<List<Boolean>>> lookup =\n                buildLookup(Collections.emptyList(), 0, calls);\n        long result = FileWrapper.binarySearchAbsentChunk(\n                streamSecret, 0L, 0L, firstKey, Optional.empty(), lookup, hasher).join();\n        Assert.assertEquals(0L, result);\n        Assert.assertEquals(\"no lookups needed\", 0, calls[0]);\n    }\n\n    @Test\n    public void singleChunkAbsent() {\n        runSearch(1, 0);\n    }\n\n    @Test\n    public void singleChunkPresent() {\n        runSearch(1, 1);\n    }\n\n    @Test\n    public void smallFileAllPresent() {\n        for (int n = 1; n <= 10; n++)\n            runSearch(n, n);\n    }\n\n    @Test\n    public void smallFileVariousK() {\n        int n = 8;\n        for (int k = 0; k <= n; k++)\n            runSearch(n, k);\n    }\n\n    @Test\n    public void largeFileFirstChunkAbsent() {\n        runSearch(10_000, 0);\n    }\n\n    @Test\n    public void largeFileLastChunkAbsent() {\n        runSearch(10_000, 9_999);\n    }\n\n    @Test\n    public void largeFileAllPresent() {\n        runSearch(10_000, 10_000);\n    }\n\n    @Test\n    public void largeFileHalfPresent() {\n        runSearch(10_000, 5_000);\n    }\n\n    @Test\n    public void logarithmicRoundTrips() {\n        // Verify the number of CHAMP round-trips grows as O(log₈ N) not O(N).\n        int[] ns = {8, 64, 512, 4096, 32768};\n        int[] previousCalls = {0};\n        for (int n : ns) {\n            byte[] streamSecret = new byte[32];\n            random.nextBytes(streamSecret);\n            byte[] firstKey = new byte[32];\n            random.nextBytes(firstKey);\n            Optional<Bat> firstBat = Optional.of(new Bat(new byte[32]));\n            random.nextBytes(firstBat.get().secret);\n\n            List<Pair<byte[], Optional<Bat>>> allProbes = computeAllProbes(streamSecret, firstKey, firstBat, n);\n\n            int k = n / 2;\n            int[] callCount = {0};\n            Function<List<Pair<byte[], Optional<Bat>>>, CompletableFuture<List<Boolean>>> lookup =\n                    buildLookup(allProbes, k, callCount);\n            FileWrapper.binarySearchAbsentChunk(streamSecret, 0L, n, firstKey, firstBat, lookup, hasher).join();\n\n            // Each 8x growth in N adds at most 1 more round-trip.\n            if (previousCalls[0] > 0)\n                Assert.assertTrue(\"N=\" + n + \": calls=\" + callCount[0] + \" grew too fast from \" + previousCalls[0],\n                        callCount[0] <= previousCalls[0] + 2);\n            previousCalls[0] = callCount[0];\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/FileHandlerTests.java",
    "content": "package peergos.server.tests;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.net.*;\nimport peergos.shared.util.*;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class FileHandlerTests {\n    static final Path TEST_ROOT = PathUtil.get(\"test\", \"resources\", \"static_handler\");\n    @Test\n    public  void  test_read() throws IOException {\n        FileHandler fileHandler = new FileHandler(new CspHost(\"http://\", \"localhost\"),\n                Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), TEST_ROOT, false, false, Optional.empty());\n        for (String path : Arrays.asList(\"something.txt\", \"/something.txt\")) {\n            StaticHandler.Asset asset = fileHandler.getAsset(path);\n            Assert.assertEquals(new String(asset.data), \"The thing!\");\n        }\n\n        StaticHandler.Asset hello = fileHandler.getAsset(\"test/hello.txt\");\n        Assert.assertEquals(new String(hello.data), \"Hello, Peergos!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/FragmentedPaddedCipherTextTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic class FragmentedPaddedCipherTextTests {\n\n    private static final Crypto crypto = JavaCrypto.init();\n\n    @Test\n    public void legacyFormatWithinlinedIdentityMultihash() {\n        byte[] raw = ArrayOps.hexToBytes(\"a2616681d82a591016000155009020426139995416ed3d728693c3438c4269418752de1392aeb3aa6cb57ec4974b46a596d37967e961636a4a74e5c7d8c929097e18145b76a4d4cea156034dda281da91ecb2eae053cd441000dc0cc44ca7365cc491b9ca16cf2dde4ff95aa5c94079172d826a3eaab5db2d09125a8b1cba4319dfe793c339a4f1265e9339ebc53e224dcbeaf70dc5b3e01f462bf7efe8b0aad9c03d776a270c52dca62446739d4a0bb73adda1253354f1c7b0e2daa5a9b8062cf267188ac51d4860399e0e9a93e762dbd97bd0f96eff8b53d9346563e514071dd361577e7b5d041bddb9b8ec9c1bab602356f1c3f8acd4119fff32ddd3241fff978c54e00926a9696dd638bb89e8012e43755a6f9401ab4cf61459ee6785176ac78778859292cd5e8b2d2ee4cea46ed2ebd334b463119cf144de67bfd70b0bf9798ae636ce028ba6bcf701dff2f3edfef0d50fb8ba83c868fe0ae0fc5a4423b24b92227b71800710d7eebe0820332cafda64253dd004d7fc165d05dd1b1a7024f8ce307b33de3a2a3ea4b9a1767ae6ec6ad3de9c3c0c345baacd81c9a4958db1adb39fbacb9f84d6ee22b9191589483510148e1037a6811728fb4b94b9a707f0ee078aaabad9986d502842a22feb37894fa4d3c675a508d00a0256cd132e06f973c6d56c8d22e327a535af4688586ee96db4520ec1e356a3954af8068fbea5db2aea1c4783eb1f39c3cfd66f2db39357d2ccfd2aa9d52adb01fd28db7c374583a3409ef81846b209089618c18703187ff13cbc2ea8319d8bc5214849e96e035bcfbd35b3c91bf260f07ea35a28fde793f85fd5f2680f2821a0f0b24380d5add993ea63edaa6ff9275c8ca228959b7a2939dfeca9f64c448241d2aa0ce1bef92155363eeaf36f94455688e8f7c75614b75bb0eb99e8860cef3ffb8aba37a5ec624dc5708807925cf886846336cb8722d424d7b7e369e94e584c4bb1a3a6558cae771b46d6c3c2b0c954b98419982a4a6919f37f09608c6ff3d8ae17274fe9671ca23aac2c2ff6fc1058994379706347d66baa72ebeaa00c840dba6e97b194e1c26ca143527fd09f62da58f4546f45970e01de0c2b416ac4a1c5e2f2c6da411f5bb8ba1c7796b46f689c95696a40dda60f49d3694914853453bdb5cf62a5c76f39c4764adca9ec5336711714ed15ff5ef8dbf92d2921fa0a3c1b6ecd793ffa8cc37f8465e40ea9c130fe0b06a67c491cf869a6991fbbe7034b2ce19965890c37c35abf4348171cc9faa1bb3d6f6f7e9b5970f48f3878383cb1c5eea862172fda59f6b93a27697e6f1bbb8da161d1fa1798e5d40bcb654876884fb7cdd0358eca2a29d4b2bbf2f6e2b67aa38b32a1e6622ce1a27531b9110fd2bc0dce02f9b69175a39dfda0838496c03e44cd28ad2760781de44a7190e41333e87e2fcd0b1769092d1535d13b63fb9fd27c9e6653e19ebc2c6214b20dc7c2b607ba6c7465ca9434c99db482db695cea8cd33ccdd2197abab290eb46fac60c37a8ff42f761eb13a494c2a6ef18dcb23162c232cb386d97510560d31357f5fde35f097f988d25ddf7e8679d268e61f5b6bd32c924038e7ef971b91241905d050507cf19ff28164f7ed11a6a76c589071a5416eb048fe57cb9fe9f0d9d534fb916af0b22a21fa6785a8176f4d1424b1442d2d635c156e5c6746668244ee5b88c1bd1e50a821572a09d957c37bf8e3a5c831fb98dd6607149fb58791524e6569ab97185dec6b81532ce456f388d2d62f136e208faaa5e5d63e63066078cc45569615c149581669d3744310dba61464554153d6b9620f97c38893fe6b7ce3e6b964770e77730e3844753efffea82b0936382af790bd13aee4f605f150296f41a98dc3876a20ddf33a4ed1ff312f20731ca685c927f6f64643e4dc31fc5d1192ec35ed7f88673e69fd330a004d0e50cc07137686edbb7a86f01fc274b363482694a512ce3a9754cbd23f324a71a822d173d5a3124f3b432783856cc73044f86bb8d141da775b7227e817d963db8c0ce6b8207710492b95623c693e06a285ea34c265fb0bf270e670b8d923797fa11994c8743c3209ecfe9579b7c51c7d69a132b6bbf77562a7241cbc8441caf30477fa81c9b2ed9b9b08420975b668654c2152c6d83ee7fbe49d9e5884336aeb2eef65af615f617133210ae2e22f1642f66997b64adb022ed8b413613b9a4e9f3a1ae6f5385f33b4575e77c5d2c8fc13ccc93c02cb33f71b5eaf0f86aa374ab0272ce998b538906f8bec182d38faa1ef0f4b14d29b84f8ce27cea4e717b7bcc10ea462828a685e42dd56c1886ab371bbb03ef7b8beea55c216679ec8aaa548aa01c8af4d47890f437e6682329900bfe5eb9d4a420c47ee4838cb220199b934ed9e58ae174b0bd9a89ac9dea59291f3ccd98a5fab93560662cf9477c152efbc3ef84a807d089130c1d570146a5879d4a94d96975c4e60e99ae3b27d68e43843ce33ebc399626819615a5c31d877bd8e372df1ba6c7ffcbf2f1cbb965b2c9045e905a97f6ebdf6a03ebaf329b2edc249c61ea2b38526e1daf2814621a89ec0e21fe9d544bddf6c60666042465f68363f938b76f786df6fb51ac59e1080f642186ddd340017efb920ae5e9458de2c764685702e367b49a732bf66d928d2df237205bbe96c5e0586fc4e8a63aa8dc501940c8ace556992e361d2c408a0ef7c54bde34a81431da532231e187a0ca2ecf17b0ce55bc14f6a6d04ac72e510246c46b7a105ccbc9973684387a5b3d8e79f69bebae8d268dbe5ab8d69e44bb852a77aa240facf0b095acb280ef2c015cd723620568220f97e3f87909d206ac4b0bfa2203e98675dc7161fe25f19fc74856a40b7068bb09f66ffdf9cc81bec6d414158d0ec8d06ffb4da1674c9349c06e70d858957e03ba3ab3908ebc306a46366f6aeda040571bdbb794e5fd2e9ac79c86a9450bceae55d383eab1a408ea522d570ec9357cfb58d2cdaf81745865f2310da8d9ebd0d9a3ac1a1e36296772204b9c6dc9ac4cada5e44336390936a7543d37008335dba382fc82990595b4a0f102c9a59ab74df6364567e3af1e7401dcc1e19f4f2bdc7013dcf01c9fa7e0ff7bff687e09720facc129746dec9c73bb1a63302de3925b07e703d9f75b54f9bf6de76f53c8f07ee47eb157e07f6582027e951a1f40c017906770193e320895166e711bd956aa83a81a77258c1e4b06533fc0cd208877d4b56c7250cca0420c5edba7e4bbb2eb4e44b93e6a6788a262b1c4b18fabd641d4ce2a5cbf1c029c663c94447d617357b36574f3bdbd18d411f52c21f0def02d3c4c43faa99918ff9f7de26f7b510c7d690c14f083e1d7aee8176754ca2140b7f2c938a8fe777f50cd83604450fb1c39bc4ab87e5584ca38994442f4a38c791ed24e275ee7755dfc16fda062aeae761e4d1e28d9eb0c370e1b86250a657b76a2c1c514a1ad21df990b082f3d9b69ad0e8cfcde4b435ff3b55ac481bff1b02cea0077f6cee2a0ad6e1c8ab8b390d7249ab19f9d65da00f2837b04d1684f079bd998d2563036bbcfb4303d0f62a7cd60b2b45b4aeeeb71508aac6c32960a9881ac061b67757e226fc6f0957928372b88ae5a1d216f7147ff8c6283ede139003f4dccf39cd4b19ac838b7da2748c8fdb193388b67af15c59c6e460050c087316cd1acaba946d626e532326e0f577ce908ba3cc687e2b911983124a35952027ecc92cf1ae319663a1433c3910f1132ab5df7aba0f3f88c0087ba99c1872c18b6295479d7290b10a94d9b5b9e144e8320605e49af4b0455d7f65b05b605861a4de5242912970bc0fe0b05b0646ccb01083127b20d843338558d338a518538aae33ea4734d8b92221bc821a18c1b3fb75de30692a39485e29d1dad3952be86251218dfeaeac330dafd164470ff5c1b892c598fdcff7ab2c89fdbb4af1349b9a3098ee9e2db34c16e8eb044a71951bc2464ae90b4a12df576dcd71e2c7c41a4107ce52911826d4815ae499391b869cfba72a13dfae2d146d666d1c94f2bde16063f62129c8c12c9622ae926b61c000d40ef16b7279c1b44add8c8060087930abd691080d645b1df59f9ef83e803c7f7b36f666fe017749874aa237b066847c6993734fd6d934329b0cb3d8ecbcd794298c06cb4de370f78c9f6b82df25c440181d0341cc9a58d4b47b9f9b07733313c2b98438d0763d8420bfbe40ff8808e5e9c01da43608e395112fe3aa9dc17c71eef3c5c56bd6be2a0fae4fa00fb8cfa56461e182ac2a2067bc94eb35ff596ba71b64a7de063e5be2eb2e0606b86d2777b6b00e2d700bbeaa36efeff51495d92238f068414fc69b8391039f72335ccf997dff25a988fbf1c7ce60a05c5d49fe6b7554c04ca40e0eca32817a480f547e9315e1cb4743552a1076f02a49590ec0f7a58f122c7c58c9ad5e3ccad5aadbac0fe16795017d013c446b7368605966582819141ec43db37b3e645947a3f101c76b231c38fdfd6da9e2e2a3b46039c9dbeb2ff2dc8bde8d79420211dd34a177fbc02a5f565c6acfeabc4137db9c8e0590580c94cfbb27eedd19c26817bbb4cc9d77b09e7a4ad8f70f19a633513ae3e55cd193328ea2a32c1f9f2e0b90da8b3cbab0df63eb5cecf2aa35b8bc9dc654452c928a36e352671d2a80d36603200d0ae38fc3dc74635cc6930717548f1355a49ecdf445fd87a543dda919fc641681e4cb83b049463d0c217cbb9b0b69663a32cdc3f149d5e80191e05b8052da771dfc9b05a842cacd47d35e2161cbdeb8ba99c295d1631adc42c658020282e6a2ecad001f3302766058e9a514d15932ad8994ef6f1c83f525e12b63b1abd4729b490a36b4d98684f0916826e35257b1f913f2bf275d69a0b84afe40734ac5d6ff51b3a2e3431fa1de1c0ae69739e015738e3349e1ba47885f6e2a4001f335a2cd3e49637664d8ae192737b8348e2de97c2e8d6860be054b0675a2d5272c669800c5e01088f4c9e29deafa254f263d221688dcce2d454e349f9602b004806e1c6992987bcb80c0ae07d7649c3bd563de0191827945237caec16e4a92730ad94346849c062b1fc3ef2fad3556c82269e694d2efc70f3c143d95d2301dbbea5ea0db35fb9d2c58c89cfcde170408ba59517b654109b0dbc9a62cff073b3d6261b644d95433ded05e28246fe434b5e2c0172cba09adfc46ef12076635c7cfdc1354189c9d12ab58843cf6d498abc7d963733ad966be1301d3fe4ea84102d6710c9b370ff3788182b24273d17f7382e96ec8a78bf6b4d22eb7301f5e04b6e0db2e79c6ffe6b274f0b928b4a7c8859891b0289bb7831155408c76a7b717bc65ad9ea9341b642e431152520884c2e511bdcdd4104d9dd2c6dbf631db2fadcc6ef658ff6da09e0f5d6c094253cefe6637d31130187bce44701a2d6cbdd71b77179a9bf55490855e4784c140a5c1baca15781a7cae6e33069c395534a636de8f05fc5406fbcb8a3929b402f5a332a7d0d44ab1abebfc912dd3ab5f571debfbfeab4a8fc740fee7726fd2cc5acb1f741cfd28cc478c1c7cc7a52dbf0a7dba2763743c893481af53bc5be2dd2595a7040858d6a5479bba4676b51f7e50071fa4caf0ed39f56fe3a96e2f56e56b84d6b8095ec0a669b6f579ae9ae1bf9dc54845edb41a84bbd43b4c92b8f209c3395f4ff1406714b348cf593fc34032be0f55e4b8604a40030c2b543db929524f23ea7eae4fd4b8ba6bb49dd7cc0c6453a1ee4933d1e059c11d05b851cd7ec4964e827ce67df111814b8d1c95e3a81e78011f68a6ff21a0845ff17622de27469f9c938af0e71e8c50acdcb0652c40616e581881c0e4b17db05731488b56c75275c5f58302167c1a783b3b\");\n        // test we can parse this legacy format correctly\n        FragmentedPaddedCipherText.fromCbor(CborObject.fromByteArray(raw));\n    }\n\n    @Test\n    public void fragmentCountAndAlignment() {\n        SymmetricKey from = SymmetricKey.random();\n        for (int len: List.of(0, 4000, 4093, 4096, 4099,\n                Fragment.MAX_LENGTH - 3, Fragment.MAX_LENGTH, Fragment.MAX_LENGTH + 3,\n                Chunk.MAX_SIZE - 4, Chunk.MAX_SIZE)) {\n            byte[] data = new byte[len];\n            int paddingBlockSize = 4096;\n            Optional<BatId> mirrorBat = Optional.of(Bat.random(crypto.random).calculateId(crypto.hasher).join());\n            Pair<FragmentedPaddedCipherText, List<FragmentWithHash>> p = FragmentedPaddedCipherText.build(from,\n                    new CborObject.CborByteArray(data), paddingBlockSize, Fragment.MAX_LENGTH, mirrorBat, crypto.random, crypto.hasher, false).join();\n\n            Assert.assertTrue(\"block sizes, len: \" + len, p.right.stream()\n                    .allMatch(f -> Bat.removeRawBlockBatPrefix(f.fragment.data).length % paddingBlockSize == 0));\n            Assert.assertTrue(\"# blocks, len: \" + len, p.right.size() <= Chunk.MAX_SIZE / Fragment.MAX_LENGTH);\n            int maxInlineSize = 4096 + 6;\n            if (data.length > maxInlineSize)\n                Assert.assertTrue(\"# blocks exact, len: \" + len, p.right.size() == (data.length + Fragment.MAX_LENGTH - 1) / Fragment.MAX_LENGTH);\n            if (data.length <= maxInlineSize)\n                Assert.assertTrue(\"len: \" + len, p.right.size() == 0);\n        }\n    }\n\n    @Test\n    public void directorySmallFileEquality() {\n        SymmetricKey from = SymmetricKey.random();\n        byte[] data = new byte[0];\n        int paddingBlockSize = 4096;\n        Optional<BatId> mirrorBat = Optional.of(Bat.random(crypto.random).calculateId(crypto.hasher).join());\n        Pair<FragmentedPaddedCipherText, List<FragmentWithHash>> file = FragmentedPaddedCipherText.build(from,\n                new CborObject.CborByteArray(data), paddingBlockSize, Fragment.MAX_LENGTH, mirrorBat, crypto.random, crypto.hasher, false).join();\n\n        Pair<FragmentedPaddedCipherText, List<FragmentWithHash>> dir = FragmentedPaddedCipherText.build(from,\n                CryptreeNode.ChildrenLinks.empty(), paddingBlockSize, Fragment.MAX_LENGTH, mirrorBat, crypto.random, crypto.hasher, false).join();\n\n        Assert.assertTrue(\"cbor length\", file.left.serialize().length == dir.left.serialize().length);\n        CborObject fileCbor = file.left.toCbor();\n        CborObject dirCbor = dir.left.toCbor();\n        Assert.assertTrue(\"cbor stuctural equality\", structurallyEqual(fileCbor, dirCbor));\n    }\n\n    private static boolean structurallyEqual(Cborable a, Cborable b) {\n        if (!a.getClass().equals(b.getClass()))\n            return false;\n        if (a instanceof CborObject.CborByteArray)\n            return ((CborObject.CborByteArray)b).value.length == ((CborObject.CborByteArray) a).value.length;\n        if (a instanceof CborObject.CborList){\n            List<? extends Cborable> aVals = ((CborObject.CborList) a).value;\n            for (int i=0; i < aVals.size(); i++)\n                if (! structurallyEqual(aVals.get(i), ((CborObject.CborList)b).value.get(i)))\n                    return false;\n            return true;\n        }\n        if (a instanceof CborObject.CborMap) {\n            CborObject.CborMap aMap = (CborObject.CborMap) a;\n            for (String key : aMap.keySet()) {\n                if (! structurallyEqual(aMap.get(key), ((CborObject.CborMap)b).get(key)))\n                    return false;\n            }\n            return true;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/FragmenterTest.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\n\n@RunWith(Parameterized.class)\npublic class FragmenterTest {\n    private static Random random = new Random(666);\n\n    private final Fragmenter fragmenter;\n\n    public FragmenterTest(Fragmenter fragmenter) {\n        this.fragmenter = fragmenter;\n    }\n\n    @Parameterized.Parameters(name = \"{0}\")\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][]{\n                {new SplitFragmenter()},\n                {new ErasureFragmenter(ErasureFragmenter.ERASURE_ORIGINAL, ErasureFragmenter.ERASURE_ALLOWED_FAILURES)}\n        });\n    }\n\n    @Test\n    public void testSeries() throws IOException {\n        for (int i = 1; i < 10; i++) {\n            int length = random.nextInt(Chunk.MAX_SIZE);\n            byte[] b = new byte[length];\n            test(b);\n        }\n    }\n\n    @Test\n    public void testBoundary() throws IOException {\n        List<Integer> sizes = Arrays.asList(Fragment.MAX_LENGTH, 2 * Fragment.MAX_LENGTH);\n        for (Integer size : sizes) {\n            byte[] b = new byte[size];\n            test(b);\n        }\n    }\n\n    private void test(byte[] input) throws IOException {\n        random.nextBytes(input);\n\n\n        byte[][] split = fragmenter.split(input);\n\n        for (byte[] bytes : split) {\n            int length = bytes.length;\n            assertTrue(length > 0);\n            assertTrue(length <= Fragment.MAX_LENGTH);\n        }\n\n        byte[] recombine = fragmenter.recombine(split, 0, input.length);\n\n        assertTrue(\"recombine(split(input)) = input\", Arrays.equals(input, recombine));\n    }\n\n\n    @Test\n    public void serializationTest() throws IOException {\n        byte[] raw = fragmenter.serialize();\n\n        Fragmenter deserialize = Fragmenter.fromCbor(CborObject.fromByteArray(raw));\n\n        assertEquals(fragmenter, deserialize);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/FuseTests.java",
    "content": "package peergos.server.tests;\nimport java.util.logging.*;\n\nimport peergos.server.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.Args;\nimport peergos.server.util.Logging;\n\nimport org.junit.*;\nimport static org.junit.Assert.*;\nimport static java.util.UUID.*;\n\nimport peergos.shared.*;\nimport peergos.server.fuse.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.lang.*;\nimport java.nio.file.*;\nimport java.nio.file.attribute.FileTime;\nimport java.time.ZonedDateTime;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class FuseTests {\n    private static final Logger LOG = Logging.LOG();\n    public static int WEB_PORT = 8888;\n    public static String username = \"test02\";\n    public static String password = randomUUID().toString();\n    public static Path mountPoint, home;\n    public static FuseProcess fuseProcess;\n    public static Random RANDOM = new Random(666);\n\n    public static void setWebPort(int webPort) {\n        WEB_PORT = webPort;\n    }\n\n    private static boolean isWindows() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"windows\");\n    }\n    private static boolean isMacos() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"mac\");\n    }\n\n    @BeforeClass\n    public static void init() throws Exception {\n        if (isWindows() || isMacos())\n            return;\n        Args args = UserTests.buildArgs()\n                .with(\"async-bootstrap\", \"true\")\n                .with(\"useIPFS\", \"false\");\n        setWebPort(args.getInt(\"port\"));\n        LOG.info(\"Using web-port \" + WEB_PORT);\n        System.out.flush();\n\n        Main.PKI_INIT.main(args);\n        NetworkAccess network = Builder.buildLocalJavaNetworkAccess(WEB_PORT).get();\n        UserContext userContext = PeergosNetworkUtils.ensureSignedUp(username, password, network, Main.initCrypto());\n\n        Path mount = Files.createTempDirectory(\"peergos\");\n        String mountPath = args.getArg(\"mountPoint\", mount.toString());\n\n        mountPoint = PathUtil.get(mountPath);\n        mountPoint = mountPoint.resolve(UUID.randomUUID().toString());\n        mountPoint.toFile().mkdirs();\n        home = mountPoint.resolve(username);\n\n        LOG.info(\"\\n\\nMountpoint \"+ mountPoint +\"\\n\\n\");\n//        PeergosFS peergosFS = new PeergosFS(userContext);\n        PeergosFS peergosFS = new CachingPeergosFS(userContext);\n        fuseProcess = new FuseProcess(peergosFS, mountPoint);\n\n        Runtime.getRuntime().addShutdownHook(new Thread(() -> fuseProcess.close(), \"Fuse shutdown\"));\n\n        fuseProcess.start();\n    }\n\n    public static String readStdout(Process p) throws IOException {\n        return new String(Serialize.readFully(p.getInputStream())).trim();\n    }\n\n    @Test\n    public void globalRoot() throws IOException  {\n        if (isWindows() || isMacos())\n            return;\n        Path root = home;\n        assertTrue(root.toFile().length() >= 0);\n        assertTrue(root.toFile().lastModified() >= 0);\n        File[] listing = root.toFile().listFiles();\n        assertNotNull(listing);\n    }\n\n    @Test\n    public void createFileTest() throws IOException  {\n        if (isWindows() || isMacos())\n            return;\n        Path resolve = home.resolve(UUID.randomUUID().toString());\n        assertFalse(\"file already exists\", resolve.toFile().exists());\n        resolve.toFile().createNewFile();\n        assertTrue(\"file exists after creation\", resolve.toFile().exists());\n    }\n\n    @Test public void moveTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        Path initial = createRandomFile(0x1000);\n\n        byte[] initialData = Files.readAllBytes(initial);\n\n        String[] stem = Stream.generate(() -> randomUUID().toString())\n                .limit(2)\n                .toArray(String[]::new);\n\n        Path targetDir = PathUtil.get(initial.getParent().toString(), stem);\n        targetDir.toFile().mkdirs();\n        assertTrue(\"target dir exists\", targetDir.toFile().isDirectory());\n\n        Path target = targetDir.resolve(randomUUID().toString());\n        assertFalse(\"target exists before move\", target.toFile().exists());\n\n        Files.move(initial, target);\n\n        assertFalse(\"initial still exists\", initial.toFile().exists());\n        assertTrue(\"target exists after move\", target.toFile().exists());\n        byte[] targetData = Files.readAllBytes(target);\n\n        assertTrue(\"target contents equal to initial contents\", Arrays.equals(initialData, targetData));\n    }\n\n    @Test public void copyFileTest() throws IOException  {\n        if (isWindows() || isMacos())\n            return;\n        Path initial = createRandomFile(1024*1024*10);\n        Path target = initial.getParent().resolve(randomUUID().toString());\n\n        assertFalse(\"target exists\", target.toFile().exists());\n        Files.copy(initial, target);\n\n        assertTrue(\"initial exists\", initial.toFile().exists());\n        assertTrue(\"target exists\", target.toFile().exists());\n\n        byte[] original = Files.readAllBytes(initial);\n        byte[] copy = Files.readAllBytes(target);\n        boolean contentEquals = Arrays.equals(original, copy);\n\n        int firstDifferentIndex = firstDifferentindex(original, copy, 0);\n        int lastDifferentIndex = lastDifferentindex(original, copy, original.length);\n\n        byte[] diff = firstDifferentIndex > 0 ? Arrays.copyOfRange(copy, firstDifferentIndex, lastDifferentIndex) : new byte[0];\n\n        assertTrue(\"initial and target contents equal\", contentEquals);\n    }\n\n    @Test public void copyFileFromHostTest() throws IOException  {\n        if (isWindows() || isMacos())\n            return;\n        Path initial = Files.createTempFile(UUID.randomUUID().toString(), \"rw\");\n        byte[] data = new byte[6*1024*1024];\n        new Random(0).nextBytes(data);\n        Files.write(initial, data);\n\n        Path target = home.resolve(randomUUID().toString());\n\n        assertFalse(\"target exists\", target.toFile().exists());\n        Files.copy(initial, target);\n\n        assertTrue(\"initial exists\", initial.toFile().exists());\n        assertTrue(\"target exists\", target.toFile().exists());\n\n        boolean contentEquals = Arrays.equals(\n                Files.readAllBytes(initial),\n                Files.readAllBytes(target));\n\n        assertTrue(\"initial and target contents equal\", contentEquals);\n    }\n\n    @Test public void randomReadTest() throws IOException  {\n        if (isWindows() || isMacos())\n            return;\n        Path initial = Files.createTempFile(UUID.randomUUID().toString(), \"rw\");\n        byte[] data = new byte[6*1024*1024];\n        new Random(0).nextBytes(data);\n        Files.write(initial, data);\n\n        Path target = home.resolve(UUID.randomUUID().toString());\n\n        assertFalse(\"target exists\", target.toFile().exists());\n        Files.copy(initial, target);\n\n        Random r = new Random(0);\n        for (int i=0; i < 20; i++) {\n            int size = r.nextInt(100*1024);\n            int offset = r.nextInt(data.length - size);\n\n            byte[] original = readBytes(initial, offset, size);\n            byte[] copy = readBytes(target, offset, size);\n\n            boolean equal = Arrays.equals(original, copy);\n\n            Assert.assertTrue(\"Same contents from \" + offset, equal);\n        }\n    }\n\n    private byte[] readBytes(Path file, long offset, int size) throws IOException {\n        try (RandomAccessFile raf = new RandomAccessFile(file.toFile(), \"r\")) {\n            raf.seek(offset);\n            byte[] res = new byte[size];\n            int read = raf.read(res);\n            if (read != size)\n                throw new IllegalStateException(\"Only read \" + read + \" not \" + size);\n            return res;\n        }\n    }\n\n    @Test public void removeTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        Path path = createRandomFile();\n        assertTrue(\"path exists before delete\", path.toFile().exists());\n        Files.delete(path);\n        assertFalse(\"path exists after delete\", path.toFile().exists());\n    }\n\n    @Test public void writePastEnd() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        int length = 10 * 1024;\n        Path path = createRandomFile(length);\n        byte[] initial = Files.readAllBytes(path);\n        RandomAccessFile raf = new RandomAccessFile(path.toFile(), \"rw\");\n        raf.seek(2*length);\n        byte[] tmp = new byte[length];\n        Random rnd = new Random(666);\n        rnd.nextBytes(tmp);\n        raf.write(tmp);\n        raf.close();\n        byte[] expected = Arrays.copyOfRange(initial, 0, 3*length);\n        System.arraycopy(tmp, 0, expected, 2*length, length);\n        byte[] extendedContents = Files.readAllBytes(path);\n        assertTrue(\"Correct contents\", Arrays.equals(expected, extendedContents));\n    }\n\n    @Test \n    public void truncateTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        int initialLength = 0x1000;\n        Path path = createRandomFile(initialLength);\n\n        try (RandomAccessFile raf = new RandomAccessFile(path.toFile(), \"rws\")) {\n            assertEquals(\"initial size\", initialLength, raf.length());\n\n            for (int pow = -1; pow < 4; pow++) {\n                long newSize = (long) (Math.pow(2, pow) * initialLength);\n                raf.setLength(newSize);\n                assertEquals(\"truncated size equals\", newSize, raf.length());\n            }\n        }\n    }\n\n    @Test\n    public void anotherTruncateTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        long kiloByte = 1024; // 1KB\n        int initialLength = (int) (4 * kiloByte);\n        long testLengthThree = 8 * kiloByte;\n\n        Path path = createRandomFile(initialLength);\n        assertTrue(\"file exists after creation\", path.toFile().exists());\n        assertEquals(\"file length equals initial length\", path.toFile().length(), initialLength);\n\n        RandomAccessFile testFile = new RandomAccessFile(path.toFile(), \"rws\");\n        testFile.setLength(testLengthThree);\n        testFile.close();\n\n        long truncatedFileLength = path.toFile().length();\n        assertEquals(\"truncated size equals\", testLengthThree, truncatedFileLength);\n\n    }\n\n    @Test\n    public void lastModifiedTimeTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        Path path = createRandomFile();\n\n        ZonedDateTime now = ZonedDateTime.now();\n        ZonedDateTime limit =  now.minusMonths(1);\n\n\n        for (ZonedDateTime zdt = now; zdt.isAfter(limit); zdt =  zdt.minusDays(1)) {\n            FileTime fileTime = FileTime.from(zdt.toInstant());\n            Path path1 = Files.setLastModifiedTime(path, fileTime);\n            FileTime found = Files.getLastModifiedTime(path);\n            int timeZoneDiff = zdt.getOffset().getTotalSeconds() - now.getOffset().getTotalSeconds();\n            assertEquals(\"get(set(time)) = time for time = \"+ zdt, fileTime.toMillis() + 1000 * timeZoneDiff, found.toMillis());\n        }\n    }\n\n    @Test public void mkdirsTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n\n        String[] stem = Stream.generate(() -> randomUUID().toString())\n                .limit(10)\n                .toArray(String[]::new);\n\n        Path path = PathUtil.get(home.toString(), stem);\n        assertFalse(\"path exists initially\", path.toFile().exists());\n\n        path.toFile().mkdirs();\n\n        assertTrue(\"path exists\", path.toFile().exists());\n        assertTrue(\"path is directory\", path.toFile().isDirectory());\n    }\n\n    @Test\n    public  void rmdirTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        Path path = home\n                .resolve(randomUUID().toString())\n                .resolve(randomUUID().toString());\n\n\n        assertFalse(\"dir exists initially\",\n                path.toFile().exists());\n\n        path.toFile().mkdirs();\n\n        assertTrue(\"dir exists after creation\",\n                path.toFile().exists());\n\n        path.toFile().delete();\n\n        assertFalse(\"dir exists after deletion\",\n                path.toFile().exists());\n    }\n\n\n    private Path createRandomFile() throws IOException {\n        return createRandomFile(0);\n    }\n\n    private Path createRandomFile(int length) throws IOException {\n        Path resolve = home.resolve(UUID.randomUUID().toString());\n        resolve.toFile().createNewFile();\n\n        if (length > 0) {\n            byte[] data = new byte[length];\n            RANDOM.nextBytes(data);\n            Files.write(resolve, data);\n        }\n\n        return resolve;\n    }\n\n    private void fileTest(int length, Random random)  throws IOException {\n        byte[] data = new byte[length];\n        random.nextBytes(data);\n\n        String filename = randomUUID().toString();\n        Path path = home.resolve(filename);\n\n        Files.write(path, data);\n\n        byte[] contents = Files.readAllBytes(path);\n\n        boolean equals = Arrays.equals(data, contents);\n        String diff = equals ? \"\" : \"Different at index \" + firstDifferentindex(data, contents, 0);\n        Assert.assertTrue(\"Correct file contents: length(\"+ contents.length +\") expected(\"+length+\") \"+ diff, equals);\n    }\n\n    public static int lastDifferentindex(byte[] src, byte[] target, int start) {\n        for (int i=start-1; i >= 0; i--) {\n            if (i >= target.length)\n                return i;\n            if (src[i] != target[i])\n                return i;\n        }\n        return -1;\n    }\n\n    public static int firstDifferentindex(byte[] src, byte[] target, int start) {\n        for (int i=start; i < src.length; i++) {\n            if (i >= target.length)\n                return i;\n            if (src[i] != target[i])\n                return i;\n        }\n        return -1;\n    }\n\n    @Test\n    public void readWriteTest() throws IOException {\n        if (isWindows() || isMacos())\n            return;\n        Random random = new Random(3); // repeatable with same seed 3 leads to failure with bulk upload at size of 137\n        for (int power = 5; power < 20; power++) {\n            int length =  (int) Math.pow(2, power);\n            length += random.nextInt(length);\n            fileTest(length, random);\n        }\n    }\n\n    @AfterClass\n    public static void shutdown() {\n        if (fuseProcess != null)\n            fuseProcess.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/GCTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.peergos.*;\nimport peergos.server.JavaCrypto;\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.space.JdbcUsageStore;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.Crypto;\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.SigningKeyPair;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.mutable.PointerUpdate;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.Pair;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class GCTests {\n    private static final Crypto crypto = JavaCrypto.init();\n\n    private Supplier<Connection> getDb(Path file) throws Exception {\n        File storeFile = file.toFile();\n        String sqlFilePath = storeFile.getPath();\n        Connection db = Sqlite.build(sqlFilePath);\n        Connection instance = new Sqlite.UncloseableConnection(db);\n        return () -> instance;\n    }\n\n    @Test\n    public void linksInDb() throws Exception {\n        Path dir = Files.createTempDirectory(\"peergos-gc-test\");\n        SqliteBlockReachability rdb = SqliteBlockReachability.createReachabilityDb(dir.resolve(\"reachability.sqlite\"));\n        Cid block = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, crypto.random.randomBytes(32));\n        List<Cid> links = new ArrayList<>();\n        for (int i=0; i<10; i++)\n            links.add(new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, crypto.random.randomBytes(32)));\n        rdb.addBlocks(Stream.concat(Stream.of(block), links.stream()).map(c -> new BlockVersion(c, null, true)).toList());\n\n        rdb.setLinks(block, links);\n\n        Optional<List<Cid>> fromDb = rdb.getLinks(block);\n        Assert.assertEquals(fromDb.get(), links);\n    }\n\n    @Test\n    public void versionedGC() throws Exception {\n        Path dir = Files.createTempDirectory(\"peergos-gc-test\");\n        SqliteCommands cmds = new SqliteCommands();\n        RequestCountingBlockMetadataStore metadb = new RequestCountingBlockMetadataStore(new JdbcBlockMetadataStore(getDb(dir.resolve(\"metadata.sqlite\")), cmds));\n\n        VersionedWriteOnlyStorage storage = new VersionedWriteOnlyStorage(metadb);\n        JdbcIpnsAndSocial pointers = new JdbcIpnsAndSocial(getDb(dir.resolve(\"mutable.sqlite\")), cmds);\n        JdbcUsageStore usage = new JdbcUsageStore(getDb(dir.resolve(\"usage.sqlite\")), cmds);\n\n        GarbageCollector gc = new GarbageCollector(storage, pointers, usage, dir, (x, y, z) -> Futures.of(true), true);\n        gc.collect(s -> Futures.of(true));\n\n        verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n\n        // write a 2 block tree, gc and verify again\n        SigningKeyPair signer = SigningKeyPair.random(crypto.random, crypto.signer);\n        PublicKeyHash writer = ContentAddressedStorage.hashKey(signer.publicSigningKey);\n        Random r = new Random(42);\n        Cid leaf = randomRaw(r);\n        byte[] raw = new CborObject.CborList(Stream.of(leaf)\n                .map(CborObject.CborMerkleLink::new).toList()\n        ).serialize();\n        Cid root = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, Hash.sha256(raw));\n        storage.add(writer, leaf);\n        BlockVersion rootV1 = storage.add(writer, root);\n        metadb.put(writer, root, rootV1.version, raw);\n        byte[] signedCas = signer.signMessage(new PointerUpdate(MaybeMultihash.empty(), MaybeMultihash.of(root), Optional.of(1L)).serialize()).join();\n        pointers.setPointer(writer, Optional.empty(), signedCas).join();\n        String username = \"user\";\n        usage.addUserIfAbsent(username);\n        usage.addWriter(username, writer);\n        usage.updateWriterUsage(writer, MaybeMultihash.of(randomCbor(new Random(42))), Collections.emptySet(), Collections.emptySet(), 1024*1024);\n        usage.confirmUsage(username, writer, 10*1024*1024, false);\n\n        verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n        Assert.assertEquals(2, storage.storage.values().stream().map(Map::size).mapToInt(i -> i).sum());\n        List<BlockVersion> versions1 = new ArrayList<>();\n        storage.getAllBlockHashVersions(writer, versions1::addAll);\n        Assert.assertEquals(2, versions1.size());\n        gc.collect(s -> Futures.of(true));\n\n        Path dbFile = dir.resolve(\"reachability\").resolve(\"reachability-\" + username + \".sqlite\");\n        Assert.assertTrue(Files.exists(dbFile));\n        SqliteBlockReachability rdb = SqliteBlockReachability.createReachabilityDb(dbFile);\n        Optional<List<Cid>> links = rdb.getLinks(root);\n        Assert.assertTrue(links.isPresent());\n        Assert.assertTrue(links.get().contains(leaf));\n\n        // Add a new leaf\n        Cid leaf2 = randomRaw(r);\n        byte[] raw2 = new CborObject.CborList(Stream.of(leaf, leaf2)\n                .map(CborObject.CborMerkleLink::new).toList()\n        ).serialize();\n        Cid root2 = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, Hash.sha256(raw2));\n        storage.add(writer, leaf2);\n        BlockVersion rootV1b = storage.add(writer, root2);\n        metadb.put(writer, root2, rootV1b.version, raw2);\n        byte[] signedCas2 = signer.signMessage(new PointerUpdate(MaybeMultihash.of(root), MaybeMultihash.of(root2), Optional.of(1L)).serialize()).join();\n        pointers.setPointer(writer, Optional.of(signedCas), signedCas2).join();\n        // add a new version of the original leaf block, and check it is kept and the original is deleted\n        BlockVersion leafV2 = storage.add(writer, leaf);\n        gc.collect(s -> Futures.of(true));\n        Assert.assertEquals(4, storage.storage.values().stream().map(Map::size).mapToInt(i -> i).sum());\n        List<BlockVersion> versions2 = new ArrayList<>();\n        storage.getAllBlockHashVersions(writer, versions2::addAll);\n        Assert.assertEquals(3, versions2.size());\n        Assert.assertTrue(versions2.contains(leafV2));\n        Optional<List<Cid>> links2 = rdb.getLinks(root2);\n        Assert.assertTrue(links2.isPresent());\n        Assert.assertTrue(links2.get().contains(leaf));\n\n        // now add a new version of the root\n        BlockVersion rootV2 = storage.add(writer, root2);\n        // delete snapshot to force a GC for user\n        Files.delete(dir.resolve(\"pointer-snapshots\")\n                .resolve(username + \".cbor\"));\n        gc.collect(s -> Futures.of(true));\n        Assert.assertEquals(4, storage.storage.values().stream().map(Map::size).mapToInt(i -> i).sum());\n        List<BlockVersion> versions3 = new ArrayList<>();\n        storage.getAllBlockHashVersions(writer, versions3::addAll);\n        Assert.assertEquals(3, versions3.size());\n        Assert.assertTrue(versions3.contains(rootV2));\n        Optional<List<Cid>> links3 = rdb.getLinks(root2);\n        Assert.assertTrue(links3.isPresent());\n        Assert.assertTrue(links3.get().contains(leaf));\n\n        gc.collect(s -> Futures.of(true));\n        Assert.assertEquals(4, storage.storage.values().stream().map(Map::size).mapToInt(i -> i).sum());\n        List<BlockVersion> versions4 = new ArrayList<>();\n        storage.getAllBlockHashVersions(writer, versions4::addAll);\n        Assert.assertEquals(3, versions4.size());\n        Assert.assertTrue(versions4.contains(rootV2));\n    }\n\n    @Test\n    public void fullGC() throws Exception {\n        Path dir = Files.createTempDirectory(\"peergos-gc-test\");\n        SqliteCommands cmds = new SqliteCommands();\n        RequestCountingBlockMetadataStore metadb = new RequestCountingBlockMetadataStore(new JdbcBlockMetadataStore(getDb(dir.resolve(\"metadata.sqlite\")), cmds));\n\n        WriteOnlyStorage storage = new WriteOnlyStorage(metadb);\n        JdbcIpnsAndSocial pointers = new JdbcIpnsAndSocial(getDb(dir.resolve(\"mutable.sqlite\")), cmds);\n        JdbcUsageStore usage = new JdbcUsageStore(getDb(dir.resolve(\"usage.sqlite\")), cmds);\n\n        GarbageCollector gc = new GarbageCollector(storage, pointers, usage, dir, (x, y, z) -> Futures.of(true), true);\n        gc.collect(s -> Futures.of(true));\n\n        verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n\n        // write a tree, gc and verify again\n        SigningKeyPair signer = SigningKeyPair.random(crypto.random, crypto.signer);\n        PublicKeyHash writer = ContentAddressedStorage.hashKey(signer.publicSigningKey);\n        String username = \"user\";\n        usage.addUserIfAbsent(username);\n        usage.addWriter(username, writer);\n        usage.updateWriterUsage(writer, MaybeMultihash.of(randomCbor(new Random(42))), Collections.emptySet(), Collections.emptySet(), 1024*1024);\n        usage.confirmUsage(username, writer, 10*1024*1024, false);\n        List<Pair<String, PublicKeyHash>> owners = usage.getAllOwners();\n        Assert.assertTrue(! owners.isEmpty());\n\n        storage.storage.put(writer, new HashMap<>());\n        Cid root = generateTree(42, 1000, blocks -> blocks\n                        .forEach(b -> storage.storage.get(writer).put(b, true)),\n                (b, kids) -> metadb.put(writer, b, null, new BlockMetadata(10, kids, Collections.emptyList())));\n        byte[] signedCas = signer.signMessage(new PointerUpdate(MaybeMultihash.empty(), MaybeMultihash.of(root), Optional.of(1L)).serialize()).join();\n        pointers.setPointer(writer, Optional.empty(), signedCas).join();\n        usage.updateWriterUsage(writer, MaybeMultihash.of(root), Collections.emptySet(), Collections.emptySet(), 1024*1024);\n\n        verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n\n        // test deleting a block fails to verify\n        Cid toRemove = storage.storage.get(writer).keySet().stream().findAny().get();\n        storage.storage.get(writer).remove(toRemove);\n        try {\n            verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n            throw new RuntimeException(\"Should not get here\");\n        } catch (IllegalStateException expected) {}\n        storage.storage.get(writer).put(toRemove, true);\n\n        metadb.resetRequestCount();\n        gc.collect(s -> Futures.of(true));\n        long gcMetadbGets = metadb.getRequestCount();\n        verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n        Assert.assertTrue(gcMetadbGets < 2000);\n\n        Path dbFile = dir.resolve(\"reachability\").resolve(\"reachability-\"+username+\".sqlite\");\n        Assert.assertTrue(Files.exists(dbFile));\n        SqliteBlockReachability rdb = SqliteBlockReachability.createReachabilityDb(dbFile);\n        Optional<List<Cid>> links = rdb.getLinks(root);\n        Assert.assertTrue(rdb.size() < 2000);\n        Assert.assertTrue(links.isPresent());\n\n        metadb.resetRequestCount();\n        gc.collect(s -> Futures.of(true));\n        long gcMetadbGets2 = metadb.getRequestCount();\n        verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n        Assert.assertTrue(gcMetadbGets2 == 0);\n\n        // test size is stable after repeated GCs\n        long bigSize = Files.size(dbFile);\n        gc.collect(s -> Futures.of(true));\n        gc.collect(s -> Futures.of(true));\n        Assert.assertEquals(bigSize, Files.size(dbFile));\n\n        // Remove root so everything is GC'd and test db file size decreases\n        usage.updateWriterUsage(writer, MaybeMultihash.empty(), Collections.emptySet(), Collections.emptySet(), 1024*1024);\n        boolean setPointer = pointers.setPointer(writer, Optional.of(signedCas), signer.signMessage(new PointerUpdate(MaybeMultihash.of(root), MaybeMultihash.empty(), Optional.of(2L)).serialize()).join()).join();\n        gc.collect(s -> Futures.of(true));\n        long emptySize = Files.size(dbFile);\n        Assert.assertTrue(emptySize < 32*1024);\n\n        // Add a new tree\n        SigningKeyPair signer2 = SigningKeyPair.random(crypto.random, crypto.signer);\n        PublicKeyHash writer2 = ContentAddressedStorage.hashKey(signer2.publicSigningKey);\n        storage.storage.put(writer2, new HashMap<>());\n        Cid root2 = generateTree(42, 1000, blocks -> blocks\n                        .forEach(b -> storage.storage.get(writer2).put(b, true)),\n                (b, kids) -> metadb.put(writer2, b, null, new BlockMetadata(10, kids, Collections.emptyList())));\n        byte[] signedCas2 = signer2.signMessage(new PointerUpdate(MaybeMultihash.empty(), MaybeMultihash.of(root2), Optional.of(1L)).serialize()).join();\n        pointers.setPointer(writer2, Optional.empty(), signedCas2).join();\n        usage.addWriter(\"user2\", writer2);\n        usage.updateWriterUsage(writer2, MaybeMultihash.of(root2), Collections.emptySet(), Collections.emptySet(), 1024*1024);\n\n        gc.collect(s -> Futures.of(true));\n        verifyAllReachableBlocksArePresent(pointers, metadb, storage);\n    }\n\n    public void verifyAllReachableBlocksArePresent(JdbcIpnsAndSocial pointers,\n                                                   BlockMetadataStore meta,\n                                                   DeletableContentAddressedStorage storage) {\n        Map<PublicKeyHash, byte[]> roots = pointers.getAllEntries();\n        for (Map.Entry<PublicKeyHash, byte[]> e : roots.entrySet()) {\n            PublicKeyHash writerHash = e.getKey();\n            PublicSigningKey writer = storage.getSigningKey(null, writerHash).join().get();\n            byte[] bothHashes = writer.unsignMessage(e.getValue()).join();\n            PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n            if (cas.updated.isPresent()) {\n                Cid root = (Cid) cas.updated.get();\n                verifySubtreePresent(writerHash, root, meta, storage);\n            }\n        }\n    }\n\n    public void verifySubtreePresent(PublicKeyHash owner,\n                                     Cid block,\n                                     BlockMetadataStore meta,\n                                     DeletableContentAddressedStorage storage) {\n        if (! storage.hasBlock(owner, block))\n            throw new IllegalStateException(\"Absent block \" + block);\n        if (block.isRaw())\n            return;\n        BlockMetadata m = meta.get(block).get();\n        for (Cid link : m.links) {\n            verifySubtreePresent(owner, link, meta, storage);\n        }\n    }\n\n    @Test\n    public void correctMarkPhase() throws IOException, SQLException {\n        Path dir = Files.createTempDirectory(\"peergos-block-metadata\");\n        File storeFile = dir.resolve(\"metadata.sql\" + System.currentTimeMillis()).toFile();\n        String sqlFilePath = storeFile.getPath();\n        Connection db = Sqlite.build(sqlFilePath);\n        Connection instance = new Sqlite.UncloseableConnection(db);\n        BlockMetadataStore metadb = new JdbcBlockMetadataStore(() -> instance, new SqliteCommands());\n\n        String filename = \"temp.sql\";\n        Path file = Path.of(filename);\n        SqliteBlockReachability reachability = SqliteBlockReachability.createReachabilityDb(file);\n        PublicKeyHash owner = new PublicKeyHash(randomCbor(new Random(42)));\n\n        int nUsers = 1;\n        int nRawBlocks = 1 << 9;\n        ForkJoinPool listPool = Threads.newFJPool(2, \"GC-list-\");\n        List<ForkJoinTask<Cid>> futs = IntStream.range(0, nUsers)\n                .mapToObj(i -> listPool.submit(() -> generateTree(i, nRawBlocks,\n                        blocks ->  reachability.addBlocks(blocks.stream().map(c ->  new BlockVersion(c, null, true)).collect(Collectors.toList())),\n                        (b, links) -> metadb.put(owner, b, null, new BlockMetadata(0, links, Collections.emptyList()))\n                        )))\n                .collect(Collectors.toList());\n        List<Cid> roots = futs.stream()\n                .map(ForkJoinTask::join)\n                .collect(Collectors.toList());\n\n        long size = reachability.size();\n        Assert.assertTrue(size > 0);\n\n        int markParallelism = 10;\n        ForkJoinPool markPool = Threads.newFJPool(markParallelism, \"GC-mark-\");\n        AtomicLong totalReachable = new AtomicLong(0);\n        List<ForkJoinTask<Boolean>> usageMarked = roots.stream()\n                .map(r -> markPool.submit(() -> {\n                    try {\n                        return GarbageCollector.markReachable(owner, null, r,\n                                \"user-\" + r, reachability, metadb, totalReachable);\n                    } catch (Exception e) {\n                        e.printStackTrace();\n                        throw new RuntimeException(e);\n                    }\n                }))\n                .collect(Collectors.toList());\n        usageMarked.forEach(ForkJoinTask::join);\n\n        List<BlockVersion> garbage = new ArrayList<>();\n        reachability.getUnreachable(garbage::addAll);\n        Assert.assertTrue(garbage.isEmpty());\n    }\n\n    private Cid generateTree(int seed, int nLeafBlocksLeft, Consumer<List<Cid>> listConsumer, BiConsumer<Cid, List<Cid>> linksConsumer) {\n        Random r = new Random(seed);\n        List<Cid> buffer = new ArrayList<>(1000);\n        Cid root = generateTree(r, nLeafBlocksLeft, buffer, listConsumer, linksConsumer);\n        listConsumer.accept(List.of(root));\n        listConsumer.accept(buffer);\n        System.out.println(\"Generated tree \" + seed);\n        return root;\n    }\n\n    private Cid generateTree(Random r, int nLeafBlocksLeft, List<Cid> buffer, Consumer<List<Cid>> listConsumer, BiConsumer<Cid, List<Cid>> linksConsumer) {\n        if (nLeafBlocksLeft <= 5) {\n            List<Cid> leaves = IntStream.range(0, nLeafBlocksLeft)\n                    .mapToObj(i -> Math.random() < 0.5 ?\n                            randomRaw(r) :\n                            randomCbor(r))\n                    .toList();\n            for (Cid leaf : leaves) {\n                linksConsumer.accept(leaf, Collections.emptyList());\n            }\n            byte[] raw = new CborObject.CborList(leaves.stream()\n                    .map(CborObject.CborMerkleLink::new).toList()\n            ).serialize();\n            Cid parent = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, Hash.sha256(raw));\n\n            linksConsumer.accept(parent, leaves);\n            buffer.addAll(leaves);\n            buffer.add(parent);\n            return parent;\n        }\n        int nLeft = nLeafBlocksLeft / 2;\n        Cid left = generateTree(r, nLeft, buffer, listConsumer, linksConsumer);\n        Cid right = generateTree(r, nLeafBlocksLeft - nLeft, buffer, listConsumer, linksConsumer);\n        byte[] raw = new CborObject.CborList(List.of(\n                new CborObject.CborMerkleLink(left),\n                new CborObject.CborMerkleLink(right)\n        )).serialize();\n        Cid root = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, Hash.sha256(raw));\n        linksConsumer.accept(root, List.of(left, right));\n        buffer.add(left);\n        buffer.add(right);\n        if (buffer.size() > 1000) {\n            listConsumer.accept(buffer);\n            buffer.clear();\n        }\n        return root;\n    }\n\n    private Cid randomRaw(Random r) {\n        byte[] hash = new byte[32];\n        r.nextBytes(hash);\n        return new Cid(1, Cid.Codec.Raw, Multihash.Type.sha2_256, hash);\n    }\n\n    private Cid randomCbor(Random r) {\n        byte[] hash = new byte[32];\n        r.nextBytes(hash);\n        return new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n}"
  },
  {
    "path": "src/peergos/server/tests/IdentityProofTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.regex.*;\n\npublic class IdentityProofTests {\n    private static final Crypto crypto = Main.initCrypto();\n\n    @Test\n    public void identityProof() {\n        String username = \"long-username-with-maxximum-size\";\n        SigningKeyPair pair = SigningKeyPair.random(crypto.random, crypto.signer);\n        PublicSigningKey peergosIdentityKey = pair.publicSigningKey;\n        PublicKeyHash publicHash = ContentAddressedStorage.hashKey(pair.publicSigningKey);\n        SigningPrivateKeyAndPublicHash signer = new SigningPrivateKeyAndPublicHash(publicHash, pair.secretSigningKey);\n\n        IdentityLinkProof proof = IdentityLinkProof.buildAndSign(signer, username, \"twitterusername\", \"Twitter\").join();\n        Assert.assertTrue(proof.isValid(peergosIdentityKey).join());\n\n        String toPost = proof.postText(\"https://peergos.net/public/\" + username + \"/.profile/ids/\" + proof.getFilename() + \"?open=true\");\n        int twitterCharacterCount = toPost.substring(0, toPost.indexOf(\"https://\")).length() + 23;\n        Assert.assertTrue(twitterCharacterCount < 280);\n\n        IdentityLinkProof parsed = IdentityLinkProof.parse(toPost);\n        Assert.assertTrue(parsed.isValid(peergosIdentityKey).join());\n\n        // Now do an encrypted version\n        SymmetricKey key = SymmetricKey.random();\n        IdentityLinkProof withKey = proof.withKey(key);\n        String encrypted = withKey.encryptedPostText();\n        Assert.assertTrue(encrypted.length() < 280);\n\n        IdentityLink decrypted = IdentityLink.decrypt(encrypted, key, peergosIdentityKey).join();\n        Assert.assertTrue(decrypted.equals(proof.claim));\n\n        // test mimetype detection\n        String mimeType = MimeTypes.calculateMimeType(withKey.serialize(), proof.getFilename());\n        Assert.assertTrue(mimeType.equals(MimeTypes.PEERGOS_IDENTITY));\n    }\n\n    @Test\n    public void usernameRegexes() {\n        Assert.assertTrue(Pattern.compile(IdentityLink.KnownService.Website.usernameRegex).matcher(\"example.com\").matches());\n        Assert.assertTrue(Pattern.compile(IdentityLink.KnownService.Website.usernameRegex).matcher(\"cool.example.com\").matches());\n        Assert.assertFalse(Pattern.compile(IdentityLink.KnownService.Website.usernameRegex).matcher(\"example-.com\").matches());\n\n        Assert.assertTrue(Pattern.compile(IdentityLink.KnownService.Mastodon.usernameRegex).matcher(\"peergos@mastodon.social\").matches());\n        Assert.assertFalse(Pattern.compile(IdentityLink.KnownService.Mastodon.usernameRegex).matcher(\"first.last@mastodon.social\").matches());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/InodeFilesystemTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.corenode.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.inode.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class InodeFilesystemTests {\n\n    private static final Crypto crypto = Main.initCrypto();\n\n    public InodeFilesystemTests() {}\n\n    @Test\n    public void deleteExample() throws IOException {\n        DeletableContentAddressedStorage storage = new FileContentAddressedStorage(Files.createTempDirectory(\"peergos-tmp\"),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n        RamPki pki = new RamPki();\n        storage.setPki(pki);\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        pki.reverseLookup.put(user.publicKeyHash, \"bob\");\n        Random r = new Random(28);\n\n        Map<String, AbsoluteCapability> state = new HashMap<>();\n\n        PublicKeyHash owner = user.publicKeyHash;\n        TransactionId tid = storage.startTransaction(owner).join();\n        InodeFileSystem current = InodeFileSystem.createEmpty(owner, user, storage, crypto.hasher, tid).join();\n\n        String path1 = \"username/webroot\";\n        AbsoluteCapability cap = randomCap(owner, r);\n        current = current.addCap(owner, user, path1, cap, tid).join();\n        state.put(path1, cap);\n\n        String profileElement = \"username/.profile/webroot\";\n        AbsoluteCapability cap2 = randomCap(owner, r);\n        current = current.addCap(owner, user, profileElement, cap2, tid).join();\n        state.put(profileElement, cap2);\n\n        checkAllMappings(state, current);\n        Assert.assertTrue(current.inodeCount == 3);\n\n        current = current.removeCap(owner, user, path1, tid).join();\n        state.remove(path1);\n        checkAllMappings(state, current);\n\n        String p3 = \"username/.profile/webroot/somedir\";\n        AbsoluteCapability cap3 = randomCap(owner, r);\n        current = current.addCap(owner, user, p3, cap3, tid).join();\n        state.put(p3, cap3);\n        checkAllMappings(state, current);\n        Assert.assertTrue(current.inodeCount == 4);\n    }\n\n    @Test\n    public void nameClash() throws Exception {\n        DeletableContentAddressedStorage storage = new FileContentAddressedStorage(Files.createTempDirectory(\"peergos-tmp\"),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n        RamPki pki = new RamPki();\n        storage.setPki(pki);\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        pki.reverseLookup.put(user.publicKeyHash, \"bob\");\n        Random r = new Random(28);\n\n        Map<String, AbsoluteCapability> state = new HashMap<>();\n\n        PublicKeyHash owner = user.publicKeyHash;\n        TransactionId tid = storage.startTransaction(owner).join();\n        InodeFileSystem current = InodeFileSystem.createEmpty(owner, user, storage, crypto.hasher, tid).join();\n\n        String path = randomPath(r, 3);\n        AbsoluteCapability cap = randomCap(owner, r);\n        current = current.addCap(owner, user, path, cap, tid).join();\n        state.put(path, cap);\n        checkAllMappings(state, current);\n\n        // Update the mapping to a new cap\n        AbsoluteCapability newCap = randomCap(owner, r);\n        current = current.addCap(owner, user, path, newCap, tid).join();\n        state.put(path, newCap);\n        checkAllMappings(state, current);\n\n        // remove the mapping\n        InodeFileSystem removed = current.removeCap(owner, user, path, tid).join();\n        state.remove(path);\n        checkAllMappings(state, removed);\n        if (removed.inodeCount != current.inodeCount)\n            throw new IllegalStateException(\"Incorrect inode count!\");\n    }\n\n    @Test\n    public void insertAndRetrieve() throws Exception {\n        DeletableContentAddressedStorage storage = new FileContentAddressedStorage(Files.createTempDirectory(\"peergos-tmp\"),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n        RamPki pki = new RamPki();\n        storage.setPki(pki);\n        SigningPrivateKeyAndPublicHash user = createUser(storage, crypto);\n        pki.reverseLookup.put(user.publicKeyHash, \"bob\");\n        Random r = new Random(28);\n\n        Map<String, AbsoluteCapability> state = new HashMap<>();\n\n        PublicKeyHash owner = user.publicKeyHash;\n        TransactionId tid = storage.startTransaction(owner).join();\n        InodeFileSystem current = InodeFileSystem.createEmpty(owner, user, storage, crypto.hasher, tid).join();\n\n        // build a random tree and keep track of the state\n        int nKeys = 1000;\n        for (int i = 0; i < nKeys; i++) {\n            String path = randomPath(r, 3);\n            AbsoluteCapability cap = randomCap(owner, r);\n            current = current.addCap(owner, user, path, cap, tid).join();\n            state.put(path, cap);\n        }\n\n        checkAllMappings(state, current);\n\n        // add and remove a mapping and check result is canonical\n        for (int i = 0; i < 100; i++) {\n            String path = randomPath(r, 3);\n            AbsoluteCapability cap = randomCap(owner, r);\n            InodeFileSystem added = current.addCap(owner, user, path, cap, tid).join();\n            InodeFileSystem removed = added.removeCap(owner, user, path, tid).join();\n            checkAllMappings(state, removed);\n            if (! removed.getRoot().equals(current.getRoot()))\n                throw new IllegalStateException(\"Non canonical after delete!\");\n            if (removed.inodeCount != current.inodeCount + 1)\n                throw new IllegalStateException(\"Incorrect inode count!\");\n        }\n\n        // add a huge directory\n        Set<String> dirContents = new HashSet<>();\n        for (int i = 0; i < 1000; i++) {\n            String child = randomPathElement(r);\n            dirContents.add(child);\n            String path = \"user/dir/\" + child;\n            AbsoluteCapability cap = randomCap(owner, r);\n            current = current.addCap(owner, user, path, cap, tid).join();\n            state.put(path, cap);\n        }\n        checkAllMappings(state, current);\n\n        List<InodeCap> dir = current.listDirectory(\"user/dir\").join();\n        Assert.assertTrue(dir.size() == 1000);\n        Assert.assertTrue(dir.stream().map(i -> i.inode.name.name).collect(Collectors.toSet()).equals(dirContents));\n    }\n\n    private static void checkAllMappings(Map<String, AbsoluteCapability> state, InodeFileSystem current) {\n        for (Map.Entry<String, AbsoluteCapability> e : state.entrySet()) {\n            Pair<InodeCap, String> access = current.getByPath(e.getKey()).join().get();\n            AbsoluteCapability res = access.left.cap.get();\n            // If a higher privilege cap is published it will be returned with the remaining path\n            if (! res.equals(e.getValue()) && access.right.isEmpty())\n                throw new IllegalStateException(\"Incorrect state!\");\n        }\n        Map<String, AbsoluteCapability> allCaps = getAllCaps(current, \"/\");\n        for (String key : allCaps.keySet()) {\n            if (! state.containsKey(key.substring(1)))\n                throw new IllegalStateException(\"Unexpected published cap for \" + key);\n        }\n    }\n\n    private static Map<String, AbsoluteCapability> getAllCaps(InodeFileSystem infs, String path) {\n        Map<String, AbsoluteCapability> res = new HashMap<>();\n        List<InodeCap> children = infs.listDirectory(path).join();\n        for (InodeCap child : children) {\n            String childPath = path + (path.endsWith(\"/\") ? \"\" : \"/\") + child.inode.name.name;\n            res.putAll(getAllCaps(infs, childPath));\n            Optional<Pair<InodeCap, String>> cap = infs.getByPath(childPath).join();\n            if (cap.isPresent() && cap.get().left.cap.isPresent())\n                res.put(childPath, cap.get().left.cap.get());\n        }\n        return res;\n    }\n\n    private static AbsoluteCapability randomCap(PublicKeyHash owner, Random r) {\n        byte[] mapKey = new byte[32];\n        r.nextBytes(mapKey);\n        SymmetricKey readKey = SymmetricKey.random();\n        return new AbsoluteCapability(owner, owner, mapKey, Optional.of(Bat.random(crypto.random)), readKey);\n    }\n\n    private static String randomPath(Random r, int maxDepth) {\n        int depth = 2 + r.nextInt(maxDepth - 2);\n        return IntStream.range(0, depth)\n                .mapToObj(i -> randomPathElement(r))\n                .collect(Collectors.joining(\"/\"));\n    }\n\n    private static String randomPathElement(Random r) {\n        int length = 1 + r.nextInt(255);\n        return IntStream.range(0, length)\n                .mapToObj(x -> randomChar(r))\n                .collect(Collectors.joining());\n    }\n\n    private static String[] chars = IntStream.concat(IntStream.range(97, 97 + 26), IntStream.range(48, 58))\n            .mapToObj(i -> String.valueOf((char)i))\n            .toArray(String[]::new);\n    private static String randomChar(Random r) {\n        return chars[r.nextInt(chars.length)];\n    }\n\n    public static SigningPrivateKeyAndPublicHash createUser(ContentAddressedStorage storage, Crypto crypto) {\n        SigningKeyPair random = SigningKeyPair.random(crypto.random, crypto.signer);\n        try {\n            PublicKeyHash ownerHash = ContentAddressedStorage.hashKey(random.publicSigningKey);\n            TransactionId tid = storage.startTransaction(ownerHash).join();\n            PublicKeyHash publicHash = storage.putSigningKey(\n                    random.secretSigningKey.signMessage(random.publicSigningKey.serialize()).join(),\n                    ownerHash,\n                    random.publicSigningKey, tid).join();\n            return new SigningPrivateKeyAndPublicHash(publicHash, random.secretSigningKey);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/IpfsMetricsTest.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\n\nimport java.util.*;\n\npublic class IpfsMetricsTest {\n\n    @Test\n    public void enableMetrics() {\n        Args a = Args.parse(new String[]{\"-collect-metrics\", \"true\",\n                \"-metrics.address\", \"192.168.10.1\",\n                \"-ipfs.metrics.port\", \"9000\",\n                \"-proxy-target\", \"/ip4/127.0.0.1/tcp/8000\",\n                \"-ipfs-api-address\", \"/ip4/127.0.0.1/tcp/5001\",\n                \"ipfs-gateway-address\", \"/ip4/127.0.0.1/tcp/8080\",\n        }, Optional.empty(), false);\n        IpfsWrapper.IpfsConfigParams conf = IpfsWrapper.buildConfig(a);\n        Assert.assertTrue(conf.enableMetrics);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/IpfsUserTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\n\n@RunWith(Parameterized.class)\npublic class IpfsUserTests extends UserTests {\n\n    private static Args args = buildArgs()\n            .with(\"useIPFS\", \"true\")\n            .with(\"async-bootstrap\", \"true\")\n            .with(\"enable-gc\", \"true\")\n//            .with(\"gc.period.millis\", \"10000\")\n            .with(\"collect-metrics\", \"true\")\n            .with(\"metrics.address\", \"localhost\")\n            .removeArg(IpfsWrapper.IPFS_BOOTSTRAP_NODES); // no bootstrapping\n\n    public IpfsUserTests(NetworkAccess network, UserService service) {\n        super(network, service);\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() throws Exception {\n        UserService service = Main.PKI_INIT.main(args).localApi;\n        return Arrays.asList(new Object[][] {\n                {\n                        Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).join(),\n                        service\n                }\n        });\n    }\n\n    @AfterClass\n    public static void cleanup() {\n        try {Thread.sleep(2000);}catch (InterruptedException e) {}\n        Path peergosDir = args.fromPeergosDir(\"\", \"\");\n        System.out.println(\"Deleting \" + peergosDir);\n        deleteFiles(peergosDir.toFile());\n    }\n\n    @Override\n    public Args getArgs() {\n        return args;\n    }\n\n    public long getBlockstoreSize() {\n        Path ipfsDir = args.fromPeergosDir(\"\", \".ipfs\").resolve(\"blocks\");\n        try {\n            return Files.walk(ipfsDir)\n                    .filter(p -> p.toFile().isFile() && p.getFileName().toString().endsWith(\".data\"))\n                    .mapToLong(p -> p.toFile().length())\n                    .sum();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n    @Test\n    public void gcReclaimsSpace() {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        // need to clear transactions otherwise blocks won't be GC'd for a day\n        TransactionStore transactionStore = Builder.buildTransactionStore(args, Builder.getDBConnector(args.with(\"transactions-sql-file\", \"transactions.sql\"),\n                \"transactions-sql-file\"));\n        transactionStore.clearOldTransactions(context.signer.publicKeyHash, System.currentTimeMillis());\n        gc();\n        long sizeBefore = getBlockstoreSize();\n        long usageBefore = context.getSpaceUsage(false).join();\n        int filesize = 10 * 1024 * 1024;\n        String filename = \"file.bin\";\n        context.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(new byte[filesize]),\n                filesize, network, crypto, () -> false, x -> {}).join();\n        long sizeWithFile = getBlockstoreSize();\n        Assert.assertTrue(sizeWithFile > sizeBefore + filesize);\n        Threads.sleep(2_000); // Allow time for server to update usage\n        long usageWithFile = context.getSpaceUsage(false).join();\n        Assert.assertTrue(usageWithFile > usageBefore + filesize);\n\n        Path filePath = PathUtil.get(username, filename);\n        context.getByPath(filePath).join().get()\n                .remove(context.getUserRoot().join(), filePath, context).join();\n        transactionStore.clearOldTransactions(context.signer.publicKeyHash, System.currentTimeMillis());\n        List<Cid> open = transactionStore.getOpenTransactionBlocks(context.signer.publicKeyHash);\n        Assert.assertTrue(open.isEmpty());\n        long usageAfter = context.getSpaceUsage(false).join();\n        Assert.assertTrue(usageAfter == usageBefore);\n        gc();\n        long sizeAfterDelete = getBlockstoreSize();\n        long diff = sizeAfterDelete - sizeBefore;\n        Assert.assertTrue(diff < 20*1024); // Why not equal?\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/JdbcAddressBookTest.java",
    "content": "package peergos.server.tests;\n\nimport io.libp2p.core.PeerId;\nimport io.libp2p.core.multiformats.Multiaddr;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.JdbcAddressLRU;\n\nimport java.util.HashMap;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\npublic class JdbcAddressBookTest {\n\n    @Test\n    public void lru() {\n        JdbcAddressLRU lru = JdbcAddressLRU.buildSqlite(10, \":memory:\");\n        Multiaddr addr = new Multiaddr(\"/ip4/127.0.0.0/tcp/8000\");\n        HashMap<PeerId, Multiaddr> ram = new HashMap<>();\n        for (int i=0; i < 10; i++) {\n            Assert.assertEquals(i, lru.size());\n            PeerId peer = PeerId.random();\n            lru.setAddrs(peer, 0, addr);\n            ram.put(peer, addr);\n        }\n        lru.setAddrs(PeerId.random(), 0, addr);\n        Assert.assertEquals(8, lru.size());\n    }\n\n    @Test\n    public void add() {\n        JdbcAddressLRU lru = JdbcAddressLRU.buildSqlite(10, \":memory:\");\n        Multiaddr addr1 = new Multiaddr(\"/ip4/127.0.0.0/tcp/8000\");\n        PeerId peer = PeerId.random();\n        lru.setAddrs(peer, 0, addr1);\n        Assert.assertEquals(Set.of(addr1), lru.getAddrs(peer).join().stream().collect(Collectors.toSet()));\n        Multiaddr addr2 = new Multiaddr(\"/ip4/127.0.0.0/tcp/9000\");\n        lru.addAddrs(peer, 0, addr2);\n        Assert.assertEquals(Set.of(addr1, addr2), lru.getAddrs(peer).join().stream().collect(Collectors.toSet()));\n    }\n\n    @Test\n    public void set() {\n        JdbcAddressLRU lru = JdbcAddressLRU.buildSqlite(10, \":memory:\");\n        Multiaddr addr1 = new Multiaddr(\"/ip4/127.0.0.0/tcp/8000\");\n        PeerId peer = PeerId.random();\n        lru.setAddrs(peer, 0, addr1);\n        Assert.assertEquals(Set.of(addr1), lru.getAddrs(peer).join().stream().collect(Collectors.toSet()));\n        Multiaddr addr2 = new Multiaddr(\"/ip4/127.0.0.0/tcp/9000\");\n        lru.setAddrs(peer, 0, addr2);\n        Assert.assertEquals(Set.of(addr2), lru.getAddrs(peer).join().stream().collect(Collectors.toSet()));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/JdbcLinkRetrievalCounterTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.storage.*;\n\nimport java.sql.*;\nimport java.time.*;\nimport java.util.*;\n\npublic class JdbcLinkRetrievalCounterTests {\n\n    @Test\n    public void basicUsage() throws Exception {\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        JdbcLinkRetrievalcounter store = new JdbcLinkRetrievalcounter(() -> db, new SqliteCommands());\n        String name = \"bob\";\n        LocalDateTime start = LocalDateTime.now();\n        Thread.sleep(2_000);\n        store.increment(name, 1);\n\n        Assert.assertTrue(1 == store.getCount(name, 1));\n        Assert.assertTrue(store.getLatestModificationTime(name).get().isBefore(LocalDateTime.now()));\n        for (int i=0; i < 100; i++) {\n            store.increment(name, 1);\n            long count = store.getCount(name, 1);\n            Assert.assertTrue(2 + i == count);\n        }\n\n        LinkCounts updatedCounts = store.getUpdatedCounts(name, start);\n        Assert.assertTrue(updatedCounts.counts.containsKey(1L));\n\n//        store.setCounts();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/JdbcRecordStoreTests.java",
    "content": "package peergos.server.tests;\n\nimport io.ipfs.multihash.Multihash;\nimport io.libp2p.core.PeerId;\nimport io.libp2p.core.crypto.PrivKey;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.peergos.HostBuilder;\nimport org.peergos.RamAddressBook;\nimport org.peergos.protocol.ipns.IPNS;\nimport org.peergos.protocol.ipns.IpnsMapping;\nimport org.peergos.protocol.ipns.IpnsRecord;\nimport peergos.server.JdbcRecordLRU;\nimport peergos.shared.util.Triple;\n\nimport java.time.LocalDateTime;\nimport java.util.Optional;\n\npublic class JdbcRecordStoreTests {\n\n    private Triple<PrivKey, PeerId, Multihash> randomId() {\n        PrivKey priv = new HostBuilder(new RamAddressBook()).generateIdentity().getPrivateKey();\n        PeerId peerId = PeerId.fromPubKey(priv.publicKey());\n        Multihash publisher = Multihash.deserialize(peerId.getBytes());\n        return new Triple<>(priv, peerId, publisher);\n    }\n\n    private IpnsRecord createRecord(PrivKey priv, Multihash publisher) {\n        byte[] signedRecord = IPNS.createSignedRecord(\"G'day mate!\".getBytes(),\n                LocalDateTime.now().plusMonths(1), 56,\n                365*86400_000_000_000L,\n                Optional.empty(),\n                Optional.empty(),\n                priv);\n        byte[] key = IPNS.getKey(publisher);\n        Optional<IpnsMapping> parsed = IPNS.parseAndValidateIpnsEntry(key, signedRecord);\n        return parsed.get().value;\n    }\n\n    @Test\n    public void lru() {\n        JdbcRecordLRU lru = JdbcRecordLRU.buildSqlite(10, \":memory:\");\n        Triple<PrivKey, PeerId, Multihash> id = randomId();\n        PrivKey priv = id.left;\n        Multihash publisher = id.right;\n        IpnsRecord record = createRecord(priv, publisher);\n\n        for (int i=0; i < 10; i++) {\n            Assert.assertEquals(i, lru.size());\n            io.ipfs.multihash.Multihash peer = randomId().right;\n            lru.put(peer, record);\n        }\n        lru.put(randomId().right, record);\n        Assert.assertEquals(8, lru.size());\n    }\n\n    @Test\n    public void overwrite() {\n        JdbcRecordLRU lru = JdbcRecordLRU.buildSqlite(10, \":memory:\");\n        Triple<PrivKey, PeerId, Multihash> id = randomId();\n        PrivKey priv = id.left;\n        Multihash publisher = id.right;\n        IpnsRecord record = createRecord(priv, publisher);\n        lru.put(publisher, record);\n        Assert.assertArrayEquals(record.raw, lru.get(publisher).get().raw);\n        IpnsRecord record2 = createRecord(priv, publisher);\n        lru.put(publisher, record2);\n        Assert.assertArrayEquals(record2.raw, lru.get(publisher).get().raw);\n    }\n\n    @Test\n    public void remove() {\n        JdbcRecordLRU lru = JdbcRecordLRU.buildSqlite(10, \":memory:\");\n        Triple<PrivKey, PeerId, Multihash> id = randomId();\n        PrivKey priv = id.left;\n        Multihash publisher = id.right;\n        IpnsRecord record = createRecord(priv, publisher);\n        lru.put(publisher, record);\n        Assert.assertArrayEquals(record.raw, lru.get(publisher).get().raw);\n        lru.remove(publisher);\n        Assert.assertEquals(Optional.empty(), lru.get(publisher));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/JdbcUsageStoreTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\n\nimport java.sql.*;\nimport java.util.*;\n\npublic class JdbcUsageStoreTests {\n\n    @Test\n    public void ownedKeys() throws Exception {\n        Crypto crypto = Main.initCrypto();\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        JdbcUsageStore store = new JdbcUsageStore(() -> db, new SqliteCommands());\n        String username = \"bob\";\n        store.addUserIfAbsent(username);\n        PublicKeyHash owner = new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, crypto.random.randomBytes(36)));\n        store.addWriter(username, owner);\n        PublicKeyHash writer = new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, crypto.random.randomBytes(36)));\n        store.addWriter(username, writer);\n        store.updateWriterUsage(owner, MaybeMultihash.empty(), Collections.emptySet(), Set.of(writer), 0);\n        store.updateWriterUsage(owner, MaybeMultihash.empty(), Collections.emptySet(), Set.of(owner), 0);\n\n        Set<PublicKeyHash> allWriters = store.getAllWriters(owner);\n        Assert.assertTrue(allWriters.size() == 2);\n        Assert.assertTrue(allWriters.contains(writer));\n\n        Set<PublicKeyHash> byName = store.getAllWriters(username);\n        Assert.assertTrue(byName.size() == 2);\n        Assert.assertTrue(byName.contains(owner));\n        Assert.assertTrue(byName.contains(writer));\n\n        int usageDelta = 1_000_000_000;\n        store.confirmUsage(username, owner, usageDelta, false);\n        Map<String, Long> allUsage = store.getAllUsage();\n        Assert.assertTrue(allUsage.get(username) == usageDelta);\n\n        // Now delete the user\n        Assert.assertFalse(store.getAllWriters().isEmpty());\n        store.removeUser(username);\n        Set<PublicKeyHash> empty = store.getAllWriters();\n        Assert.assertTrue(empty.isEmpty());\n    }\n\n    /** A user added with addUserIfAbsent but no writers yet should have usage 0, not -1 */\n    @Test\n    public void usageNotNegativeForUserWithNoWriters() throws Exception {\n        Crypto crypto = Main.initCrypto();\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        JdbcUsageStore store = new JdbcUsageStore(() -> db, new SqliteCommands());\n        String username = \"alice\";\n        store.addUserIfAbsent(username);\n        // No addWriter call - user has a userusage row but no pendingusage rows.\n        // The INNER JOIN in getUsage() returns no rows so totalBytes stays at -1 sentinel.\n        UserUsage usage = store.getUsage(username);\n        Assert.assertFalse(\"Usage should not be negative for a new user with no writers; was: \" + usage.totalUsage(),\n                usage.totalUsage() < 0);\n        Assert.assertEquals(\"Expected 0 bytes used for new user\", 0, usage.totalUsage());\n    }\n\n    /** Confirmed usage stored in userusage must be readable even after pending bytes are reset to zero */\n    @Test\n    public void storedUsageVisibleWithZeroPending() throws Exception {\n        Crypto crypto = Main.initCrypto();\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        JdbcUsageStore store = new JdbcUsageStore(() -> db, new SqliteCommands());\n        String username = \"carol\";\n        store.addUserIfAbsent(username);\n        PublicKeyHash owner = new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, crypto.random.randomBytes(36)));\n        store.addWriter(username, owner);\n        long size = 5_000_000L;\n        store.confirmUsage(username, owner, size, false);\n        // confirmUsage resets pending to 0 for this writer; getUsage must still return total_bytes\n        UserUsage usage = store.getUsage(username);\n        Assert.assertEquals(\"Confirmed usage should be returned correctly\", size, usage.totalUsage());\n        Assert.assertFalse(\"Usage must not be negative\", usage.totalUsage() < 0);\n    }\n\n    /** Multiple sequential confirmUsage calls accumulate correctly */\n    @Test\n    public void usageSummedAcrossWriters() throws Exception {\n        Crypto crypto = Main.initCrypto();\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        JdbcUsageStore store = new JdbcUsageStore(() -> db, new SqliteCommands());\n        String username = \"dave\";\n        store.addUserIfAbsent(username);\n        PublicKeyHash owner = new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, crypto.random.randomBytes(36)));\n        PublicKeyHash writer = new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, crypto.random.randomBytes(36)));\n        store.addWriter(username, owner);\n        store.addWriter(username, writer);\n        long ownerSize = 3_000_000L;\n        long writerSize = 2_000_000L;\n        store.confirmUsage(username, owner, ownerSize, false);\n        store.confirmUsage(username, writer, writerSize, false);\n        UserUsage usage = store.getUsage(username);\n        Assert.assertEquals(\"Total usage should be sum of all writers\", ownerSize + writerSize, usage.totalUsage());\n    }\n\n    /** Deleting all data (applying negative delta equal to stored size) should give 0, not negative */\n    @Test\n    public void deletingDataDoesNotMakeUsageNegative() throws Exception {\n        Crypto crypto = Main.initCrypto();\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        JdbcUsageStore store = new JdbcUsageStore(() -> db, new SqliteCommands());\n        String username = \"eve\";\n        store.addUserIfAbsent(username);\n        PublicKeyHash owner = new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, crypto.random.randomBytes(36)));\n        store.addWriter(username, owner);\n        long size = 4_000_000L;\n        store.confirmUsage(username, owner, size, false);\n        store.confirmUsage(username, owner, -size, false);\n        UserUsage usage = store.getUsage(username);\n        Assert.assertEquals(\"Usage should be 0 after deleting all data\", 0, usage.totalUsage());\n        Assert.assertFalse(\"Usage must not be negative after delete\", usage.totalUsage() < 0);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/LegacyWebauthn.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.login.Webauthn;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.util.ArrayOps;\n\npublic class LegacyWebauthn {\n\n    @Test\n    public void modernEncodedSecp256() {\n        byte[] raw = ArrayOps.hexToBytes(\"a361630061645871586f50726f746f6e5061737350726f746f6e0010181dfbb184591e6359a8f3a1c08d1507a5010203262001215820bd631ab6e9f96915b56c1645a1081529d7390172d623bd427458d25c2ce63d5f225820285a80bae02c73e7723e244db7d0070e1e576415c6fa09ca8f5a4a7a277cfa8d617341a0\");\n        CborObject cbor1 = CborObject.fromByteArray(raw);\n        Webauthn.Verifier verifier = Webauthn.Verifier.fromCbor(cbor1);\n        byte[] credentialId = verifier.getAttestedCredentialData().getCredentialId();\n        Assert.assertArrayEquals(ArrayOps.hexToBytes(\"181dfbb184591e6359a8f3a1c08d1507\"), credentialId);\n        byte[] pubKey = verifier.getAttestedCredentialData().getCOSEKey().getPublicKey().getEncoded();\n        Assert.assertArrayEquals(pubKey, ArrayOps.hexToBytes(\"3059301306072a8648ce3d020106082a8648ce3d03010703420004bd631ab6e9f96915b56c1645a1081529d7390172d623bd427458d25c2ce63d5f285a80bae02c73e7723e244db7d0070e1e576415c6fa09ca8f5a4a7a277cfa8d\"));\n        byte[] aguid = verifier.getAttestedCredentialData().getAaguid().getBytes();\n        Assert.assertArrayEquals(aguid, ArrayOps.hexToBytes(\"50726f746f6e5061737350726f746f6e\"));\n    }\n\n    @Test\n    public void secp256r1() {\n        byte[] raw = ArrayOps.hexToBytes(\"a36163006164590466aced000573720044636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e417474657374656443726564656e7469616c4461746185df6d0f6d8aacb40200034c00066161677569647400364c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f4141475549443b4c0007636f73654b65797400374c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f434f53454b65793b5b000c63726564656e7469616c49647400025b42787073720034636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e414147554944fb3908248cfbd7d40200014c000576616c75657400104c6a6176612f7574696c2f555549443b78707372000e6a6176612e7574696c2e55554944bc9903f7986d852f0200024a000c6c65617374536967426974734a000b6d6f7374536967426974737870a3d811116f7e8349d548826e79b4db4073720038636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e454332434f53454b6579cfb7122e4f2fdb2a0200044c000563757276657400354c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f43757276653b5b00016471007e00035b00017871007e00035b00017971007e00037872003d636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e4162737472616374434f53454b657911b0302464c2e4c90200044c0009616c676f726974686d7400434c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f73746174656d656e742f434f5345416c676f726974686d4964656e7469666965723b5b000662617365495671007e00035b00056b6579496471007e00034c00066b65794f70737400104c6a6176612f7574696c2f4c6973743b787073720041636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e434f5345416c676f726974686d4964656e746966696572b5e5a801c4dc74180200014a000576616c75657870fffffffffffffff97070707e720033636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e437572766500000000000000001200007872000e6a6176612e6c616e672e456e756d0000000000000000120000787074000953454350323536523170757200025b42acf317f8060854e002000078700000002092070d0adffa715b57673bc2608c25df1d0a0ec1a685e052467aef41f72bd9777571007e0016000000203b13b8aa0e363020418bb382e37d9221db4b716dd89ee8519d72d77942fd5d3e7571007e001600000010637b7a5418144829bd5a10b981bd828f61735857aced000573720042636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e4e6f6e654174746573746174696f6e53746174656d656e746b3b9efd2e6430530200007870\");\n        CborObject cbor1 = CborObject.fromByteArray(raw);\n        Webauthn.Verifier verifier = Webauthn.Verifier.fromCbor(cbor1);\n        byte[] credentialId = verifier.getAttestedCredentialData().getCredentialId();\n        Assert.assertArrayEquals(ArrayOps.hexToBytes(\"637b7a5418144829bd5a10b981bd828f\"), credentialId);\n        byte[] pubKey = verifier.getAttestedCredentialData().getCOSEKey().getPublicKey().getEncoded();\n        Assert.assertArrayEquals(pubKey, ArrayOps.hexToBytes(\"3059301306072a8648ce3d020106082a8648ce3d0301070342000492070d0adffa715b57673bc2608c25df1d0a0ec1a685e052467aef41f72bd9773b13b8aa0e363020418bb382e37d9221db4b716dd89ee8519d72d77942fd5d3e\"));\n        byte[] aguid = verifier.getAttestedCredentialData().getAaguid().getBytes();\n        Assert.assertArrayEquals(aguid, ArrayOps.hexToBytes(\"d548826e79b4db40a3d811116f7e8349\"));\n    }\n\n    @Test\n    public void largeCredIdSecp256r1() {\n        byte[] raw = ArrayOps.hexToBytes(\"a3616300616459046aaced000573720044636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e417474657374656443726564656e7469616c4461746185df6d0f6d8aacb40200034c00066161677569647400364c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f4141475549443b4c0007636f73654b65797400374c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f434f53454b65793b5b000c63726564656e7469616c49647400025b42787073720034636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e414147554944fb3908248cfbd7d40200014c000576616c75657400104c6a6176612f7574696c2f555549443b78707372000e6a6176612e7574696c2e55554944bc9903f7986d852f0200024a000c6c65617374536967426974734a000b6d6f73745369674269747378708c0b6e020557d7bdfbfc3007154e4ecc73720038636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e454332434f53454b6579cfb7122e4f2fdb2a0200044c000563757276657400354c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f43757276653b5b00016471007e00035b00017871007e00035b00017971007e00037872003d636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e4162737472616374434f53454b657911b0302464c2e4c90200044c0009616c676f726974686d7400434c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f73746174656d656e742f434f5345416c676f726974686d4964656e7469666965723b5b000662617365495671007e00035b00056b6579496471007e00034c00066b65794f70737400104c6a6176612f7574696c2f4c6973743b787073720041636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e434f5345416c676f726974686d4964656e746966696572b5e5a801c4dc74180200014a000576616c75657870fffffffffffffff97070707e720033636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e437572766500000000000000001200007872000e6a6176612e6c616e672e456e756d0000000000000000120000787074000953454350323536523170757200025b42acf317f8060854e002000078700000002010f41543ac784c1eef9526cb93e96fedebc40a147841c074705f7ec07e7db2917571007e00160000002078cae801b9d632d1646ff47b8b41bd28fc14c336ab53bd3959a3d1c5e5d61fdd7571007e0016000000141795e64a8ebbf695bde128ca4bbfba4687fc7ce061735857aced000573720042636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e4e6f6e654174746573746174696f6e53746174656d656e746b3b9efd2e6430530200007870\");\n        CborObject cbor1 = CborObject.fromByteArray(raw);\n        Webauthn.Verifier verifier = Webauthn.Verifier.fromCbor(cbor1);\n        byte[] credentialId = verifier.getAttestedCredentialData().getCredentialId();\n        Assert.assertArrayEquals(ArrayOps.hexToBytes(\"1795e64a8ebbf695bde128ca4bbfba4687fc7ce0\"), credentialId);\n        byte[] pubKey = verifier.getAttestedCredentialData().getCOSEKey().getPublicKey().getEncoded();\n        Assert.assertArrayEquals(pubKey, ArrayOps.hexToBytes(\"3059301306072a8648ce3d020106082a8648ce3d0301070342000410f41543ac784c1eef9526cb93e96fedebc40a147841c074705f7ec07e7db29178cae801b9d632d1646ff47b8b41bd28fc14c336ab53bd3959a3d1c5e5d61fdd\"));\n        byte[] aguid = verifier.getAttestedCredentialData().getAaguid().getBytes();\n        Assert.assertArrayEquals(aguid, ArrayOps.hexToBytes(\"fbfc3007154e4ecc8c0b6e020557d7bd\"));\n    }\n\n    @Test\n    public void largeCredIdEd25519() {\n        byte[] raw = ArrayOps.hexToBytes(\"a36163186061645904c0aced000573720044636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e417474657374656443726564656e7469616c4461746185df6d0f6d8aacb40200034c00066161677569647400364c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f4141475549443b4c0007636f73654b65797400374c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f434f53454b65793b5b000c63726564656e7469616c49647400025b42787073720034636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e414147554944fb3908248cfbd7d40200014c000576616c75657400104c6a6176612f7574696c2f555549443b78707372000e6a6176612e7574696c2e55554944bc9903f7986d852f0200024a000c6c65617374536967426974734a000b6d6f7374536967426974737870000000000000000000000000000000007372003a636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e4564445341434f53454b65794f34c5f4776431400200034c000563757276657400354c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f61757468656e74696361746f722f43757276653b5b00016471007e00035b00017871007e00037872003d636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e4162737472616374434f53454b657911b0302464c2e4c90200044c0009616c676f726974686d7400434c636f6d2f776562617574686e346a2f646174612f6174746573746174696f6e2f73746174656d656e742f434f5345416c676f726974686d4964656e7469666965723b5b000662617365495671007e00035b00056b6579496471007e00034c00066b65794f70737400104c6a6176612f7574696c2f4c6973743b787073720041636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e434f5345416c676f726974686d4964656e746966696572b5e5a801c4dc74180200014a000576616c75657870fffffffffffffff87070707e720033636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e61757468656e74696361746f722e437572766500000000000000001200007872000e6a6176612e6c616e672e456e756d000000000000000012000078707400074544323535313970757200025b42acf317f8060854e00200007870000000206c2925dd69f0fca5419bc0281fe8d07f38ca28f34299c0d4d5f3e98db2fb66a27571007e00160000009d9cb9c8e97e790014aac29762e945196de4d112df0f8937163d642a55aba2c81a8babbb584ec6aded0265183357f98fa8bd9b9a7e27da0a9846e8b4dc5753ac0a3bac5357004f61e07a01aff051f1777182a6b94d0ca71fbdefad37d39cbbabab2d2da314e103cce6e026e748efc562359787adb548331a17b5cf16ce5bda25d4c403ee11533595288644f5527a3f308a80123d10b58f1271dd4cee04e861735857aced000573720042636f6d2e776562617574686e346a2e646174612e6174746573746174696f6e2e73746174656d656e742e4e6f6e654174746573746174696f6e53746174656d656e746b3b9efd2e6430530200007870\");\n        CborObject cbor1 = CborObject.fromByteArray(raw);\n        Webauthn.Verifier verifier = Webauthn.Verifier.fromCbor(cbor1);\n        Assert.assertEquals(157, verifier.getAttestedCredentialData().getCredentialId().length);\n        byte[] pubKey = verifier.getAttestedCredentialData().getCOSEKey().getPublicKey().getEncoded();\n        Assert.assertArrayEquals(pubKey, ArrayOps.hexToBytes(\"302a300506032b65700321006c2925dd69f0fca5419bc0281fe8d07f38ca28f34299c0d4d5f3e98db2fb66a2\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MLKEMTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.Main;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.FIPS203;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.MimicloneFIPS203;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.MLKEMCipherText;\nimport peergos.shared.Crypto;\nimport peergos.shared.crypto.BoxingKeyPair;\nimport peergos.shared.crypto.InvalidCipherTextException;\n\nimport java.nio.charset.StandardCharsets;\n\npublic class MLKEMTests {\n    private static Crypto crypto = Main.initCrypto();\n\n    @Test\n    public void usage() {\n        FIPS203 fips203 = MimicloneFIPS203.create(ParameterSet.ML_KEM_1024);\n        KeyPair bobKeys = fips203.generateKeyPair();\n\n        // alice's keys are not involved\n        Encapsulation alice = fips203.encapsulate(bobKeys.getEncapsulationKey());\n        byte[] aliceSharedSecret = alice.getSharedSecretKey().getBytes();\n        CipherText sentCipherText = alice.getCipherText();\n\n        // bob\n        CipherText cipherText = MLKEMCipherText.create(sentCipherText.getBytes());\n        byte[] bobSharedSecret = fips203.decapsulate(bobKeys.getDecapsulationKey(), cipherText).getBytes();\n\n        Assert.assertArrayEquals(aliceSharedSecret, bobSharedSecret);\n    }\n\n    @Test\n    public void hybrid() {\n        BoxingKeyPair alice = BoxingKeyPair.randomHybrid(crypto).join();\n        BoxingKeyPair bob = BoxingKeyPair.randomHybrid(crypto).join();\n\n        byte[] msg = \"G'day mate! This is hopefully post quantum secure!\".getBytes(StandardCharsets.UTF_8);\n\n        byte[] toSend = bob.publicBoxingKey.encryptMessageFor(msg, alice.secretBoxingKey).join();\n\n        byte[] decrypted = bob.secretBoxingKey.decryptMessage(toSend, alice.publicBoxingKey).join();\n        Assert.assertArrayEquals(decrypted, msg);\n    }\n\n    @Test\n    public void hybridMsgTamper() {\n        BoxingKeyPair alice = BoxingKeyPair.randomHybrid(crypto).join();\n        BoxingKeyPair bob = BoxingKeyPair.randomHybrid(crypto).join();\n\n        byte[] msg = \"G'day mate! This is hopefully post quantum secure!\".getBytes(StandardCharsets.UTF_8);\n\n        byte[] toSend = bob.publicBoxingKey.encryptMessageFor(msg, alice.secretBoxingKey).join();\n        for (int i=20; i < toSend.length; i++) {\n            toSend[i] ^= 1;\n\n            try {\n                byte[] decrypted = bob.secretBoxingKey.decryptMessage(toSend, alice.publicBoxingKey).join();\n            } catch (RuntimeException e) {}\n            toSend[i] ^= 1;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MessagingTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.display.*;\nimport peergos.shared.messaging.*;\nimport peergos.shared.messaging.messages.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\nimport static peergos.shared.messaging.messages.ApplicationMessage.text;\n\npublic class MessagingTests {\n    private static final Crypto crypto = Main.initCrypto();\n    private static final Hasher hasher = crypto.hasher;\n    private static final ContentAddressedStorage ipfs = new RAMStorage(crypto.hasher);\n    private static final Function<Chat, CompletableFuture<Boolean>> NO_OP = c -> Futures.of(true);\n    private static final Function<FileRef, CompletableFuture<Boolean>> NO_OP2 = r -> Futures.of(true);\n\n    @Test\n    public void basicChat() {\n        List<SigningPrivateKeyAndPublicHash> identities = generateUsers(2);\n        List<PrivateChatState> chatIdentities = generateChatIdentities(2);\n        List<RamMessageStore> stores = IntStream.range(0, 2).mapToObj(i -> new RamMessageStore()).collect(Collectors.toList());\n\n        Chat chat1 = Chat.createNew(\"uid\", \"user1\", identities.get(0).publicKeyHash);\n        OwnerProof user1ChatId = OwnerProof.build(identities.get(0), chatIdentities.get(0).chatIdentity.publicKeyHash).join();\n        ChatUpdate u1_1 = chat1.join(chat1.host(), user1ChatId, chatIdentities.get(0).chatIdPublic, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_1);\n\n        ChatUpdate u1_2 = u1_1.state.inviteMember(\"user2\", identities.get(1).publicKeyHash,\n                chatIdentities.get(0).chatIdentity, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_2);\n        Member user2 = u1_2.state.getMember(\"user2\");\n        Chat chat2 = u1_2.state.copy(user2);\n        stores.get(1).mirror(stores.get(0));\n        OwnerProof user2ChatId = OwnerProof.build(identities.get(1), chatIdentities.get(1).chatIdentity.publicKeyHash).join();\n        ChatUpdate u2_1 = chat2.join(user2, user2ChatId, chatIdentities.get(1).chatIdPublic, identities.get(1), stores.get(1), ipfs, crypto).join();\n        stores.get(1).apply(u2_1);\n\n        ChatUpdate u1_3 = u1_2.state.sendMessage(text(\"Welcome!\"), chatIdentities.get(0).chatIdentity, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_3);\n        MessageEnvelope msg1 = u1_3.newMessages.get(u1_3.newMessages.size() - 1).msg;\n        ChatUpdate u2_2 = u2_1.state.merge(\"chat-uid\", chat1.host, identities.get(1), stores.get(0), ipfs, crypto).join();\n        stores.get(1).apply(u2_2);\n        Assert.assertTrue(stores.get(1).messages.get(3).msg.equals(msg1));\n\n        ReplyTo reply = ReplyTo.build(msg1, text(\"This is cool!\"), hasher).join();\n        ChatUpdate u2_3 = u2_2.state.sendMessage(reply, chatIdentities.get(1).chatIdentity, identities.get(1), stores.get(1), ipfs, crypto).join();\n        stores.get(1).apply(u2_3);\n        MessageEnvelope msg2 = u2_3.newMessages.get(u2_3.newMessages.size() - 1).msg;;\n\n        ChatUpdate u1_4 = u1_3.state.merge(\"chat-uid\", chat2.host, identities.get(0), stores.get(1), ipfs, crypto).join();\n        stores.get(0).apply(u1_4);\n        Assert.assertTrue(stores.get(0).messages.get(4).msg.equals(msg2));\n    }\n\n    @Test\n    public void multipleInvites() {\n        List<SigningPrivateKeyAndPublicHash> identities = generateUsers(3);\n        List<PrivateChatState> chatIdentities = generateChatIdentities(3);\n        List<RamMessageStore> stores = IntStream.range(0, 3).mapToObj(i -> new RamMessageStore()).collect(Collectors.toList());\n\n        Chat chat1 = Chat.createNew(\"uid\", \"user1\", identities.get(0).publicKeyHash);\n        OwnerProof user1ChatId = OwnerProof.build(identities.get(0), chatIdentities.get(0).chatIdentity.publicKeyHash).join();\n        ChatUpdate u1_1 = chat1.join(chat1.host(), user1ChatId, chatIdentities.get(0).chatIdPublic, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_1);\n\n        ChatUpdate u1_2 = u1_1.state.inviteMember(\"user2\", identities.get(1).publicKeyHash,\n                chatIdentities.get(0).chatIdentity, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_2);\n        Member user2 = u1_2.state.getMember(\"user2\");\n\n        Chat chat2 = u1_2.state.copy(user2);\n        stores.get(1).mirror(stores.get(0));\n        OwnerProof user2ChatId = OwnerProof.build(identities.get(1), chatIdentities.get(1).chatIdentity.publicKeyHash).join();\n        chat2.join(user2, user2ChatId, chatIdentities.get(1).chatIdPublic, identities.get(1), stores.get(1), ipfs, crypto).join();\n\n        ChatUpdate u1_3 = u1_2.state.inviteMember(\"user3\", identities.get(2).publicKeyHash,\n                chatIdentities.get(0).chatIdentity, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_3);\n        Member user3 = u1_3.state.getMember(\"user3\");\n\n        Chat chat3 = u1_3.state.copy(user3);\n        stores.get(2).mirror(stores.get(0));\n        OwnerProof user3ChatId = OwnerProof.build(identities.get(2), chatIdentities.get(2).chatIdentity.publicKeyHash).join();\n        chat3.join(user3, user3ChatId, chatIdentities.get(2).chatIdPublic, identities.get(2), stores.get(0), ipfs, crypto).join();\n\n        Assert.assertTrue(! user2.id.equals(user3.id));\n    }\n\n    @Test\n    public void messagePropagation() {\n        List<SigningPrivateKeyAndPublicHash> identities = generateUsers(3);\n        List<PrivateChatState> chatIdentities = generateChatIdentities(3);\n        List<RamMessageStore> stores = IntStream.range(0, 3).mapToObj(i -> new RamMessageStore()).collect(Collectors.toList());\n\n        Chat chat1 = Chat.createNew(\"uid\", \"user1\", identities.get(0).publicKeyHash);\n        OwnerProof user1ChatId = OwnerProof.build(identities.get(0), chatIdentities.get(0).chatIdentity.publicKeyHash).join();\n        ChatUpdate u1_1 = chat1.join(chat1.host(), user1ChatId, chatIdentities.get(0).chatIdPublic, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_1);\n\n        ChatUpdate u1_2 = u1_1.state.inviteMember(\"user2\", identities.get(1).publicKeyHash,\n                chatIdentities.get(0).chatIdentity, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_2);\n        Member user2 = u1_2.state.getMember(\"user2\");\n        Chat chat2 = u1_2.state.copy(user2);\n        stores.get(1).mirror(stores.get(0));\n        OwnerProof user2ChatId = OwnerProof.build(identities.get(1), chatIdentities.get(1).chatIdentity.publicKeyHash).join();\n        ChatUpdate u2_1 = chat2.join(user2, user2ChatId, chatIdentities.get(1).chatIdPublic, identities.get(1), stores.get(1), ipfs, crypto).join();\n        stores.get(1).apply(u2_1);\n\n        ChatUpdate u2_2 = u2_1.state.inviteMember(\"user3\", identities.get(2).publicKeyHash,\n                chatIdentities.get(1).chatIdentity, identities.get(1), stores.get(1), ipfs, crypto).join();\n        stores.get(1).apply(u2_2);\n        Member user3 = u2_2.state.getMember(\"user3\");\n        Chat chat3 = u2_2.state.copy(user3);\n        stores.get(2).mirror(stores.get(1));\n        OwnerProof user3ChatId = OwnerProof.build(identities.get(2), chatIdentities.get(2).chatIdentity.publicKeyHash).join();\n        ChatUpdate u3_1 = chat3.join(user3, user3ChatId, chatIdentities.get(2).chatIdPublic, identities.get(2), stores.get(2), ipfs, crypto).join();\n        stores.get(2).apply(u3_1);\n\n        ChatUpdate u3_2 = u3_1.state.sendMessage(text(\"Hey All!\"), chatIdentities.get(2).chatIdentity, identities.get(2), stores.get(2), ipfs, crypto).join();\n        stores.get(2).apply(u3_2);\n        MessageEnvelope msg1 = u3_2.newMessages.get(u3_2.newMessages.size() - 1).msg;\n        ChatUpdate u2_3 = u2_2.state.merge(\"chat-uid\", chat3.host, identities.get(1), stores.get(2), ipfs, crypto).join();\n        stores.get(1).apply(u2_3);\n        Assert.assertTrue(stores.get(1).messages.get(5).msg.equals(msg1));\n\n        ChatUpdate u1_3 = u1_2.state.merge(\"chat-uid\", chat2.host, identities.get(0), stores.get(1), ipfs, crypto).join();\n        stores.get(0).apply(u1_3);\n        Assert.assertTrue(stores.get(0).messages.get(5).msg.equals(msg1));\n    }\n\n    @Test\n    public void partitionAndJoin() {\n        List<SigningPrivateKeyAndPublicHash> identities = generateUsers(4);\n        List<PrivateChatState> chatIdentities = generateChatIdentities(4);\n        List<RamMessageStore> stores = IntStream.range(0, 4).mapToObj(i -> new RamMessageStore()).collect(Collectors.toList());\n\n        List<Chat> chats = Chat.createNew(\"uid\",\n                Arrays.asList(\"user1\", \"user2\", \"user3\", \"user4\"),\n                identities.stream().map(p -> p.publicKeyHash).collect(Collectors.toList()));\n        TreeClock genesis = chats.get(0).current;\n        Chat chat1 = chats.get(0);\n        Chat chat2 = chats.get(1);\n        Chat chat3 = chats.get(2);\n        Chat chat4 = chats.get(3);\n        OwnerProof user1ChatId = OwnerProof.build(identities.get(0), chatIdentities.get(0).chatIdentity.publicKeyHash).join();\n        ChatUpdate u1_1 = chat1.join(chat1.host(), user1ChatId, chatIdentities.get(0).chatIdPublic, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_1);\n\n        OwnerProof user2ChatId = OwnerProof.build(identities.get(1), chatIdentities.get(1).chatIdentity.publicKeyHash).join();\n        ChatUpdate u2_1 = chat2.join(chat2.host(), user2ChatId, chatIdentities.get(1).chatIdPublic, identities.get(1), stores.get(1), ipfs, crypto).join();\n        stores.get(1).apply(u2_1);\n\n        OwnerProof user3ChatId = OwnerProof.build(identities.get(2), chatIdentities.get(2).chatIdentity.publicKeyHash).join();\n        ChatUpdate u3_1 = chat3.join(chat3.host(), user3ChatId, chatIdentities.get(2).chatIdPublic, identities.get(2), stores.get(2), ipfs, crypto).join();\n        stores.get(2).apply(u3_1);\n\n        OwnerProof user4ChatId = OwnerProof.build(identities.get(3), chatIdentities.get(3).chatIdentity.publicKeyHash).join();\n        ChatUpdate u4_1 = chat4.join(chat4.host(), user4ChatId, chatIdentities.get(3).chatIdPublic, identities.get(3), stores.get(3), ipfs, crypto).join();\n        stores.get(3).apply(u4_1);\n\n        // partition and chat between user1 and user2\n        TreeClock t1_0 = u1_1.state.current;\n        ChatUpdate u1_2 = u1_1.state.sendMessage(text(\"Hey All, I'm user1!\"), chatIdentities.get(0).chatIdentity, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_2);\n        MessageEnvelope msg1 = u1_2.newMessages.get(u1_2.newMessages.size() - 1).msg;\n        Assert.assertTrue(msg1.timestamp.isIncrementOf(t1_0));\n\n        String chatUid = \"chat-uid\";\n        ChatUpdate u2_2 = u2_1.state.merge(chatUid, chat1.host, identities.get(1), stores.get(0), ipfs, crypto).join();\n        stores.get(1).apply(u2_2);\n        TreeClock t2_0 = u2_2.state.current;\n        ChatUpdate u2_3 = u2_2.state.sendMessage(text(\"Hey user1! I'm user2.\"), chatIdentities.get(1).chatIdentity, identities.get(1), stores.get(1), ipfs, crypto).join();\n        stores.get(1).apply(u2_3);\n        MessageEnvelope msg2 = u2_3.newMessages.get(u2_3.newMessages.size() - 1).msg;\n        Assert.assertTrue(msg2.timestamp.isIncrementOf(t2_0));\n\n        ChatUpdate u1_3 = u1_2.state.merge(chatUid, chat2.host, identities.get(0), stores.get(1), ipfs, crypto).join();\n        stores.get(0).apply(u1_3);\n        TreeClock t1_1 = u1_3.state.current;\n        ChatUpdate u1_4 = u1_3.state.sendMessage(text(\"Hey user2, whats up?\"), chatIdentities.get(0).chatIdentity, identities.get(0), stores.get(0), ipfs, crypto).join();\n        stores.get(0).apply(u1_4);\n        MessageEnvelope msg3 = u1_4.newMessages.get(u1_4.newMessages.size() - 1).msg;\n        Assert.assertTrue(msg3.timestamp.isIncrementOf(t1_1));\n\n        ChatUpdate u2_4 = u2_3.state.merge(chatUid, chat1.host, identities.get(1), stores.get(0), ipfs, crypto).join();\n        stores.get(1).apply(u2_4);\n        TreeClock t2_1 = u2_4.state.current;\n        ChatUpdate u2_5 = u2_4.state.sendMessage(text(\"Just saving the world one decentralized chat at a time..\"),\n                chatIdentities.get(1).chatIdentity, identities.get(1), stores.get(1), ipfs, crypto).join();\n        stores.get(1).apply(u2_5);\n        MessageEnvelope msg4 = u2_5.newMessages.get(u2_5.newMessages.size() - 1).msg;\n        Assert.assertTrue(msg4.timestamp.isIncrementOf(t2_1));\n\n        ChatUpdate u1_5 = u1_4.state.merge(chatUid, chat2.host, identities.get(0), stores.get(1), ipfs, crypto).join();\n        stores.get(0).apply(u1_5);\n        Assert.assertTrue(stores.get(1).messages.containsAll(stores.get(0).messages));\n        Assert.assertEquals(stores.get(1).messages.size(), 6);\n\n        // also between user3 and user4\n        ChatUpdate u3_2 = u3_1.state.sendMessage(text(\"Hey All, I'm user3!\"), chatIdentities.get(2).chatIdentity, identities.get(2), stores.get(2), ipfs, crypto).join();\n        stores.get(2).apply(u3_2);\n\n        ChatUpdate u4_2 = u4_1.state.merge(chatUid, chat3.host, identities.get(3), stores.get(2), ipfs, crypto).join();\n        stores.get(3).apply(u4_2);\n        ChatUpdate u4_3 = u4_2.state.sendMessage(text(\"Hey user3! I'm user4.\"), chatIdentities.get(3).chatIdentity, identities.get(3), stores.get(3), ipfs, crypto).join();\n        stores.get(3).apply(u4_3);\n\n        ChatUpdate u3_3 = u3_2.state.merge(chatUid, chat4.host, identities.get(2), stores.get(3), ipfs, crypto).join();\n        stores.get(2).apply(u3_3);\n        ChatUpdate u3_4 = u3_3.state.sendMessage(text(\"Hey user4, whats up?\"), chatIdentities.get(2).chatIdentity, identities.get(2), stores.get(2), ipfs, crypto).join();\n        stores.get(2).apply(u3_4);\n\n        ChatUpdate u4_4 = u4_3.state.merge(chatUid, chat3.host, identities.get(3), stores.get(2), ipfs, crypto).join();\n        stores.get(3).apply(u4_4);\n        ChatUpdate u4_5 = u4_4.state.sendMessage(text(\"Just saving the world one encrypted chat at a time..\"), chatIdentities.get(3).chatIdentity, identities.get(3), stores.get(3), ipfs, crypto).join();\n        stores.get(3).apply(u4_5);\n\n        ChatUpdate u3_5 = u3_4.state.merge(chatUid, chat4.host, identities.get(2), stores.get(3), ipfs, crypto).join();\n        stores.get(2).apply(u3_5);\n        Assert.assertTrue(stores.get(3).messages.containsAll(stores.get(2).messages));\n        Assert.assertEquals(stores.get(3).messages.size(), 6);\n\n        // now resolve the partition and merge states\n        ChatUpdate u1_6 = u1_5.state.merge(chatUid, chat4.host, identities.get(0), stores.get(3), ipfs, crypto).join();\n        stores.get(0).apply(u1_6);\n        Assert.assertEquals(stores.get(0).messages.size(), 12);\n        ChatUpdate u2_6 = u2_5.state.merge(chatUid, chat1.host, identities.get(1), stores.get(0), ipfs, crypto).join();\n        stores.get(1).apply(u2_6);\n        Assert.assertTrue(stores.get(1).messages.containsAll(stores.get(0).messages));\n\n        // check ordering\n        for (int i=0; i < stores.size(); i++)\n            validateMessageLog(stores.get(i).getMessagesFrom(0).join(), genesis);\n    }\n\n    private static void validateMessageLog(List<SignedMessage> msgs, TreeClock genesis) {\n        TreeClock current = genesis;\n        for (int i=0; i < msgs.size(); i++) {\n            SignedMessage signed = msgs.get(i);\n            MessageEnvelope msg = signed.msg;\n            TreeClock t = msg.timestamp;\n            if (t.isBeforeOrEqual(current))\n                throw new IllegalStateException(\"Invalid timestamp ordering!\");\n            current = current.merge(t);\n        }\n    }\n\n    @Test\n    public void clockSize() {\n        List<Id> ids = IntStream.range(0, 100).mapToObj(Id::new).collect(Collectors.toList());\n        TreeClock clock = TreeClock.init(ids);\n        byte[] raw = clock.serialize();\n        Assert.assertTrue(raw.length < 400);\n    }\n\n    private static class RamMessageStore implements MessageStore {\n        public final List<SignedMessage> messages;\n\n        public RamMessageStore() {\n            this.messages = new ArrayList<>();\n        }\n\n        @Override\n        public CompletableFuture<List<SignedMessage>> getMessagesFrom(long index) {\n            return Futures.of(messages.subList((int) index, messages.size()));\n        }\n\n        @Override\n        public CompletableFuture<List<SignedMessage>> getMessages(long fromIndex, long toIndex) {\n            return Futures.of(messages.subList((int) fromIndex, (int) toIndex));\n        }\n\n        @Override\n        public CompletableFuture<Snapshot> addMessages(Snapshot initialVersion, Committer committer, long msgIndex, List<SignedMessage> msgs) {\n            if (messages.size() != msgIndex)\n                throw new IllegalStateException();\n            messages.addAll(msgs);\n            return Futures.of(initialVersion);\n        }\n\n        @Override\n        public CompletableFuture<Snapshot> revokeAccess(Set<String> usernames, Snapshot initialVersion, Committer committer) {\n            return Futures.of(new Snapshot(Collections.emptyMap()));\n        }\n\n        public void apply(ChatUpdate u) {\n            messages.addAll(u.newMessages);\n        }\n\n        public void mirror(RamMessageStore other) {\n            messages.addAll(other.messages);\n        }\n    }\n\n    private static List<SigningPrivateKeyAndPublicHash> generateUsers(int count) {\n        return IntStream.range(0, count)\n                .mapToObj(i -> SigningKeyPair.random(crypto.random, crypto.signer))\n                .map(p -> new SigningPrivateKeyAndPublicHash(ContentAddressedStorage.hashKey(p.publicSigningKey), p.secretSigningKey))\n                .collect(Collectors.toList());\n    }\n\n    private static List<PrivateChatState> generateChatIdentities(int count) {\n        return IntStream.range(0, count)\n                .mapToObj(i -> SigningKeyPair.random(crypto.random, crypto.signer))\n                .map(p -> new PrivateChatState(new SigningPrivateKeyAndPublicHash(\n                        ContentAddressedStorage.hashKey(p.publicSigningKey), p.secretSigningKey),\n                        p.publicSigningKey, Collections.emptySet()))\n                .collect(Collectors.toList());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MimeTypeTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.charset.*;\nimport java.time.LocalDateTime;\nimport java.util.*;\n\npublic class MimeTypeTests {\n\n    @Test\n    public void smallTextFile() {\n        String mime = MimeTypes.calculateMimeType(\"G'day Peergos!\".getBytes(), \"data.txt\");\n        Assert.assertTrue(mime.equals(\"text/plain\"));\n    }\n\n    @Test\n    public void utf8() {\n        byte[] utf8 = ArrayOps.concat(\"<!DOCTYPE html>\\n<html>\".getBytes(StandardCharsets.UTF_8), new byte[]{(byte)0xe2, (byte)0x80, (byte)0x9c});\n        String mime = MimeTypes.calculateMimeType(utf8, \"surreal\");\n        Assert.assertTrue(mime.equals(\"text/html\"));\n    }\n\n    @Test\n    public void truncatedUtf8() {\n        byte[] utf8 = ArrayOps.concat(\"<!DOCTYPE html>\\n<html>\".getBytes(StandardCharsets.UTF_8), new byte[]{(byte)0xe2, (byte)0x80});\n        String mime = MimeTypes.calculateMimeType(utf8, \"surreal\");\n        Assert.assertTrue(mime.equals(\"text/html\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MirrorTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.Main;\nimport peergos.server.storage.BlockMetadata;\nimport peergos.server.storage.NewBlocksProcessor;\nimport peergos.server.storage.RAMStorage;\nimport peergos.shared.Crypto;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.TransactionId;\nimport peergos.shared.storage.auth.BatWithId;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.stream.IntStream;\n\npublic class MirrorTests {\n    private static Random r = new Random(42);\n\n    private static byte[] randomBlock() {\n        byte[] bytes = new byte[32];\n        r.nextBytes(bytes);\n        return bytes;\n    }\n\n    @Test\n    public void test() {\n        Crypto crypto = Main.initCrypto();\n        // Build a 10 deep binary tree\n        TestMirrorStorage s = new TestMirrorStorage(crypto.hasher);\n\n        int depth = 6;\n        int branches = 3;\n        List<byte[]> leaves = IntStream.range(0, (int) Math.pow(branches, depth - 1))\n                .mapToObj(i -> randomBlock())\n                .toList();\n        TransactionId tid = s.startTransaction(null).join();\n        List<Cid> leafCids = leaves.stream()\n                .map(b -> s.putRaw(null, null, null, b, tid, x -> {}).join())\n                .toList();\n\n        List<Cid> level = leafCids;\n        while (level.size() > 1) {\n            List<Cid> nextLevel = new ArrayList<>();\n            for (int i=0; i < level.size(); i+= branches)\n                nextLevel.add(s.put(null, null, null, new CborObject.CborList(level.subList(i, Math.min(level.size(), i + branches))\n                        .stream().map(CborObject.CborMerkleLink::new).toList()).toByteArray(), tid).join());\n            level = nextLevel;\n        }\n\n        Cid root = level.get(0);\n        AtomicLong count = new AtomicLong(0);\n        NewBlocksProcessor p = (w, bs, size) -> count.addAndGet(bs.size());\n        s.mirror(\"a\", null, null, Collections.emptyList(), Optional.empty(),\n                Optional.of(root), Optional.empty(), null, p, tid, crypto.hasher).join();\n        int nBlocks = ((int) Math.pow(branches, depth) - 1) / (branches - 1);\n        Assert.assertEquals(nBlocks, count.get());\n        // Ths is necessary for mirror to be fast in a p2p setting when getLinks retrieves the blocks\n        // Leaves are retrieved 1 at a time as they are large (up to 1 Mib).\n        // Minus 2 for the leaf layer and the root\n        Assert.assertEquals(depth - 2 + leaves.size(), s.highLatencyCalls.get());\n    }\n\n    public static class TestMirrorStorage extends RAMStorage {\n        public final AtomicLong highLatencyCalls = new AtomicLong(0);\n\n        public TestMirrorStorage(Hasher hasher) {\n            super(hasher);\n        }\n\n        @Override\n        public List<BlockMetadata> bulkGetLinks(List<Multihash> peerIds,\n                                                PublicKeyHash owner,\n                                                Cid ourId,\n                                                List<Cid> blocks,\n                                                Optional<BatWithId> mirrorBat,\n                                                Hasher h) {\n            highLatencyCalls.incrementAndGet();\n            return super.bulkGetLinks(peerIds, owner, ourId, blocks, mirrorBat, h);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MultiNodeNetworkTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\nimport org.junit.Assume;\nimport peergos.server.*;\nimport peergos.server.corenode.CorenodeEventPropagator;\nimport peergos.server.corenode.MirrorCoreNode;\nimport peergos.server.corenode.SignUpFilter;\nimport peergos.server.storage.*;\nimport peergos.server.tests.util.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\nimport static org.junit.Assert.*;\nimport static peergos.server.tests.UserTests.deleteFiles;\nimport static peergos.server.tests.UserTests.randomString;\nimport static peergos.server.tests.PeergosNetworkUtils.ensureSignedUp;\nimport static peergos.server.tests.PeergosNetworkUtils.generateUsername;\nimport static peergos.server.tests.PeergosNetworkUtils.*;\n\n@RunWith(Parameterized.class)\npublic class MultiNodeNetworkTests {\n    private static Args args = UserTests.buildArgs()\n            .with(\"useIPFS\", \"true\")\n            .with(\"async-bootstrap\", \"true\")\n            .with(\"allow-external-secret-links\", \"true\")\n            .with(\"enable-gc\", \"true\")\n            .with(\"allow-external-login\", \"true\")\n            .removeArg(IpfsWrapper.IPFS_BOOTSTRAP_NODES); // no bootstrapping\n    private static Random random = new Random(0);\n    private static List<NetworkAccess> nodes = new ArrayList<>();\n    private static List<ServerProcesses> services = new ArrayList<>();\n    private static final List<Args> argsToCleanUp = new ArrayList<>();\n    private final Crypto crypto = Main.initCrypto();\n\n    private final int iNode1, iNode2;\n\n//    @Parameterized.Parameters()\n    private final String node1Name;\n\n//    @Parameterized.Parameter()\n    private final String node2Name;\n\n\n    public MultiNodeNetworkTests(int iNode1, int iNode2) {\n        this.iNode1 = iNode1;\n        this.iNode2 = iNode2;\n        this.node1Name = iNode1 == 0 ? \"PKI-node\" : String.format(\"normal-node %d\", iNode1);\n        this.node2Name = iNode2 == 0 ? \"PKI-node\" : String.format(\"normal-node %d\", iNode2);\n    }\n\n\n    @Parameterized.Parameters(name=\"nodes: {0}, {1} (0 == PKI, > 0 normal)\")\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n                {0, 1}, // PKI, normal-1\n                {1, 0}, // normal-2, PKI\n                {2, 1}  // normal-1, normal-2\n        });\n    }\n\n    @AfterClass\n    public static void cleanup() {\n        try {Thread.sleep(2000);}catch (InterruptedException e) {}\n        for (ServerProcesses service : services) {\n            try { service.localApi.stop(); } catch (Exception e) {}\n            try { if (service.p2pApi != null) service.p2pApi.stop(); } catch (Exception e) {}\n            try { if (service.ipfs != null) service.ipfs.stop(); } catch (Exception e) {}\n        }\n        for (Args toClean : argsToCleanUp) {\n            Path peergosDir = toClean.fromPeergosDir(\"\", \"\");\n            System.out.println(\"Deleting \" + peergosDir);\n            deleteFiles(peergosDir.toFile());\n        }\n    }\n\n    private NetworkAccess getNode(int i)  {\n        return nodes.get(i);\n    }\n\n    private void stopServer(int i)  {\n        ServerProcesses server = services.get(i);\n        server.localApi.stop();\n        server.p2pApi.stop();\n        if (server.ipfs != null)\n            server.ipfs.stop();\n    }\n\n    private void startServer(int i) throws Exception  {\n        if (i == 0)\n            throw new IllegalStateException(\"Restarting PKI not yet supported in test\");\n        Args startArgs = argsToCleanUp.get(i).with(IpfsWrapper.IPFS_BOOTSTRAP_NODES, buildBootstrapList(i));\n        ServerProcesses service = Main.PEERGOS.main(startArgs);\n        service.localApi.gc.stop();\n        services.set(i, service);\n        nodes.set(i, buildApi(startArgs));\n    }\n\n    private static String buildBootstrapList(int exclude) {\n        StringBuilder res = new StringBuilder();\n        for (int i=0; i < 3; i++) {\n            if (i != exclude && services.size() > i) {\n                res.append(\",\"+Main.getLocalBootstrapAddress(\n                        argsToCleanUp.get(i).getInt(\"ipfs-swarm-port\"),\n                        services.get(i).localApi.storage.id().join().bareMultihash()));\n            }\n        }\n        return res.toString().substring(1);\n    }\n\n    private void rotateServerIdentity(int i)  {\n        if (i == 0)\n            throw new IllegalStateException(\"Rotating PKI identity not yet supported in test\");\n        Optional<Path> config = Optional.of(argsToCleanUp.get(i).getPeergosDirChild(\"config\"));\n        Args withPrivKey = Args.parse(new String[0], config, false);\n        ServerIdentity.ROTATE.main(withPrivKey);\n        Args withNewPrivKey = Args.parse(new String[0], config, false);\n        String bootstrapList = Main.getLocalBootstrapAddress(argsToCleanUp.get(0).getInt(\"ipfs-swarm-port\"), services.get(0).localApi.storage.id().join().bareMultihash()).toString();\n        for (int n = 1; n < 3; n++)\n            if (n != i)\n                bootstrapList += \",\" + Main.getLocalBootstrapAddress(argsToCleanUp.get(n).getInt(\"ipfs-swarm-port\"), services.get(n).localApi.storage.id().join().bareMultihash());\n        argsToCleanUp.set(i, withNewPrivKey.with(IpfsWrapper.IPFS_BOOTSTRAP_NODES, bootstrapList));\n    }\n\n    private UserService getService(int i)  {\n        return services.get(i).localApi;\n    }\n\n    @BeforeClass\n    public static void init() throws Exception {\n        long t0 = System.currentTimeMillis();\n        System.getProperties().setProperty(\"io.netty.eventLoopThreads\", \"1\");\n        // start pki node\n        ServerProcesses pki = Main.PKI_INIT.main(args);\n        PublicKeyHash peergosId = pki.localApi.coreNode.getPublicKeyHash(\"peergos\").join().get();\n        args = args.setArg(\"peergos.identity.hash\", peergosId.toString());\n        NetworkAccess toPki = buildApi(args);\n        Multihash pkiNodeId = toPki.dhtClient.id().get();\n        nodes.add(toPki);\n        services.add(pki);\n        pki.localApi.gc.stop();\n        argsToCleanUp.add(args);\n        int bootstrapSwarmPort = args.getInt(\"ipfs-swarm-port\");\n        String bootstrapList = Main.getLocalBootstrapAddress(bootstrapSwarmPort, pkiNodeId).toString();\n        long t1 = System.currentTimeMillis();\n        System.out.println(\"MNNT starting PKI took \" + (t1 - t0)/1000 + \"s\");\n\n        // create two other nodes that use the first as a PKI-node\n        for (int i = 0; i < 2; i++) {\n            long n0 = System.currentTimeMillis();\n            int ipfsApiPort = TestPorts.getPort();System.out.println(\"node\" + (i+1) + \" base port: \" + ipfsApiPort);\n            int ipfsGatewayPort = TestPorts.getPort();\n            int ipfsSwarmPort = TestPorts.getPort();\n            int peergosPort = TestPorts.getPort();\n            int proxyTargetPort = TestPorts.getPort();\n            Args normalNode = UserTests.buildArgs()\n                    .with(Main.PEERGOS_PATH, Files.createTempDirectory(\"peergos-mnnt-\" + System.nanoTime()).toString())\n                    .with(\"useIPFS\", \"true\")\n                    .with(\"async-bootstrap\", \"false\")\n                    .with(\"enable-gc\", \"true\")\n                    .with(\"port\", \"\" + peergosPort)\n                    .with(\"pki-node-id\", pkiNodeId.toString())\n                    .with(\"peergos.identity.hash\", peergosId.toString())\n                    .with(\"ipfs-api-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsApiPort)\n                    .with(\"ipfs-gateway-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsGatewayPort)\n                    .with(\"ipfs-swarm-port\", \"\" + ipfsSwarmPort)\n                    .with(IpfsWrapper.IPFS_BOOTSTRAP_NODES, bootstrapList)\n                    .with(\"proxy-target\", Main.getLocalMultiAddress(proxyTargetPort).toString())\n                    .with(\"ipfs-api-address\", Main.getLocalMultiAddress(ipfsApiPort).toString());\n            argsToCleanUp.add(normalNode);\n            ServerProcesses service = Main.PEERGOS.main(normalNode);\n            services.add(service);\n\n            service.localApi.gc.stop();\n            Multihash ourId = service.localApi.storage.id().get();\n            bootstrapList += \",\" + Main.getLocalBootstrapAddress(ipfsSwarmPort, ourId);\n\n            nodes.add(buildApi(normalNode));\n            long n1 = System.currentTimeMillis();\n            System.out.println(\"MNNT::start peer took \" + (n1-n0)/1000 + \"s\");\n        }\n        long t2 = System.currentTimeMillis();\n        System.out.println(\"MNNT::init took \" + (t2-t0)/1000 + \"s\");\n    }\n\n    private static NetworkAccess buildApi(Args args) throws Exception {\n        URL local = new URL(\"http://localhost:\" + args.getInt(\"port\"));\n        return Builder.buildNonCachingJavaNetworkAccess(local, false, 1_000, Optional.empty(), Optional.empty(), Optional.empty()).get();\n    }\n\n    @Before\n    public void gc() {\n        for (ServerProcesses service : services) {\n            service.localApi.gc.collect(s -> Futures.of(true));\n        }\n    }\n\n    private void updatePkis() {\n        for (ServerProcesses service : services) {\n            CoreNode core = service.localApi.coreNode;\n            CoreNode target = ((SignUpFilter) ((CorenodeEventPropagator) core).target).target;\n            if (target instanceof MirrorCoreNode)\n                ((MirrorCoreNode)target).update();\n        }\n    }\n\n    @Test\n    public void signUp() {\n        UserContext context = ensureSignedUp(generateUsername(random), randomString(), getNode(iNode1), crypto);\n        updatePkis();\n\n        for (NetworkAccess node: nodes) {\n            long usage = node.spaceUsage.getUsage(context.signer.publicKeyHash,\n                    TimeLimitedClient.signNow(context.signer.secret).join(), false).join();\n            byte[] signedTime = TimeLimitedClient.signNow(context.signer.secret).join();\n            long quota = node.spaceUsage.getQuota(context.signer.publicKeyHash, signedTime).join();\n            Assert.assertTrue(usage >0 && quota > 0);\n        }\n    }\n\n    @Test\n    public void migrateWithZeroPwdChanges() {\n        migrate(0);\n    }\n\n    @Test\n    public void migrateWith1PwdChanges() {\n        migrate(1);\n    }\n\n    public void migrate(int nPasswordChanges) {\n        if (iNode1 == 0 || iNode2 == 0)\n            return; // Don't test migration to/from pki node\n        String username = generateUsername(random);\n        String password = randomString();\n        NetworkAccess node1 = getNode(iNode1);\n        Multihash originalNodeId = node1.dhtClient.id().join();\n        NetworkAccess node2 = getNode(iNode2);\n        Multihash newStorageNodeId = node2.dhtClient.id().join();\n\n        UserContext user = ensureSignedUp(username, password, node1, crypto);\n        updatePkis();\n        for (int i=0; i < nPasswordChanges; i++) {\n            String newPassword = randomString();\n            user = ensureSignedUp(username, password, node2, crypto).changePassword(password, newPassword, UserTests::noMfa).join();\n            password = newPassword;\n        }\n        // make sure we have some raw fragments\n        String filename = \"somedata.bin\";\n        user.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(new byte[10*1024*1024]),\n                10*1024*1024, user.network, crypto, () -> false, x -> {}).join();\n\n        // check retrieval of cryptree node or data both fail without bat\n        FileWrapper file = user.getByPath(\"/\" + username + \"/\" + filename).join().get();\n        WritableAbsoluteCapability cap = file.writableFilePointer();\n        WritableAbsoluteCapability badCap = cap.withMapKey(cap.getMapKey(), Optional.empty());\n        Assert.assertTrue(node1.clear().getFile(badCap, username).join().isEmpty());\n\n        Multihash fragment = file.getPointer().fileAccess.toCbor().links().get(0);\n        CompletableFuture<Optional<byte[]>> raw = node1.clear().dhtClient.getRaw(user.signer.publicKeyHash, (Cid) fragment, Optional.empty());\n        try {\n            raw.join();\n        } catch (Exception e) {\n            // unauthorized\n        }\n        Assert.assertTrue(raw.isCompletedExceptionally());\n\n        UserContext friend = ensureSignedUp(generateUsername(random), password, node1, crypto);\n        friend.sendInitialFollowRequest(username).join();\n\n        // migrate to node2\n        List<UserPublicKeyLink> existing = user.network.coreNode.getChain(username).join();\n        List<UserPublicKeyLink> newChain = Migrate.buildMigrationChain(existing, newStorageNodeId, user.signer.secret).join();\n        UserContext userViaNewServer = ensureSignedUp(username, password, node2, crypto);\n\n        List<BatWithId> bats = node1.batCave.getUserBats(username, userViaNewServer.signer).join();\n        List<BatWithId> batsViaNewNode = node2.batCave.getUserBats(username, userViaNewServer.signer).join();\n        Assert.assertTrue(bats.equals(batsViaNewNode));\n        Optional<BatWithId> mirrorBat = Optional.of(bats.get(bats.size() - 1));\n        long usageVia1 = user.getSpaceUsage(false).join();\n        userViaNewServer.network.coreNode.migrateUser(username, newChain, originalNodeId, mirrorBat, LocalDateTime.now(), usageVia1, true).join();\n\n        List<UserPublicKeyLink> chain = userViaNewServer.network.coreNode.getChain(username).join();\n        Multihash storageNode = chain.get(chain.size() - 1).claim.storageProviders.stream().findFirst().get();\n        Assert.assertTrue(storageNode.equals(newStorageNodeId));\n\n        // test a fresh login on the new storage node\n        UserContext postMigration = ensureSignedUp(username, password, node2.clear(), crypto);\n        long usageVia2 = postMigration.getSpaceUsage(false).join();\n        // Note we currently don't remove the old pointer after changing password,\n        // so there is a 5kib reduction after migration per password change\n        Assert.assertTrue(\"Usage after migrate: \" + usageVia2 + \", usage before: \" + usageVia1,\n                usageVia2 == usageVia1 || (nPasswordChanges > 0 && usageVia2 < usageVia1));\n\n        // check pending followRequest was transferred\n        List<FollowRequestWithCipherText> followRequests = postMigration.processFollowRequests().join();\n        Assert.assertTrue(followRequests.size() == 1);\n\n        // check bats were transferred\n        List<BatWithId> postBats = postMigration.network.batCave.getUserBats(username, postMigration.signer).join();\n        Assert.assertTrue(\"mirror bats transferred\", postBats.equals(bats));\n\n        // check a reverse migration can't be triggered by anyone else\n        try {\n            node1.coreNode.migrateUser(username, existing, newStorageNodeId, mirrorBat, LocalDateTime.now(), usageVia2, true).join();\n            throw new RuntimeException(\"Shouldn't get here!\");\n        } catch (CompletionException e) {\n            if (! e.getCause().getMessage().startsWith(\"Migration claim has earlier expiry than current one\"))\n                throw new RuntimeException(e.getCause());\n        }\n\n        try { // check a direct update call with old chain also fails\n            ProofOfWork work = crypto.hasher.generateProofOfWork(ProofOfWork.DEFAULT_DIFFICULTY,\n                    new CborObject.CborList(existing).serialize()).join();\n            node1.coreNode.updateChain(username, existing, work, \"\").join();\n            throw new RuntimeException(\"Shouldn't get here!\");\n        } catch (CompletionException e) {\n            if (! e.getCause().getMessage().startsWith(\"New claim chain expiry before existing\"))\n                throw new RuntimeException(e.getCause());\n        }\n    }\n\n    @Test\n    public void largeP2pWrites() {\n        if (iNode1 == 0 || iNode2 == 0)\n            return; // Don't test to/from pki node\n        String username = generateUsername(random);\n        String password = randomString();\n        NetworkAccess node1 = getNode(iNode1);\n        NetworkAccess node2 = getNode(iNode2);\n        UserContext user = ensureSignedUp(username, password, node1, crypto);\n        updatePkis();\n\n        UserContext viaProxy = ensureSignedUp(username, password, node2, crypto);\n\n        byte[] data = new byte[2 * 1024 * 1024];\n\n        for (int i=0; i < 100; i++) {\n            FileWrapper home = viaProxy.getUserRoot().join();\n            home.uploadFileJS(i + \"\", AsyncReader.build(data), 0, data.length, false, home.mirrorBatId(), node2, crypto, x -> {}, viaProxy.getTransactionService(), f -> Futures.of(true)).join();\n        }\n    }\n\n    @Test\n    public void invalidMigrate() {\n        if (iNode1 == 0 || iNode2 == 0)\n            return; // Don't test migration to/from pki node\n        String username = generateUsername(random);\n        String password = randomString();\n        NetworkAccess node1 = getNode(iNode1);\n        Multihash originalNodeId = node1.dhtClient.id().join();\n        UserContext user = ensureSignedUp(username, password, node1, crypto);\n        String evilusername = randomUsername(\"evil\", new Random());\n        UserContext evil = ensureSignedUp(evilusername, password, node1, crypto);\n        updatePkis();\n\n        // try to migrate with an invalid claim chain\n        UserService node2 = getService(iNode2);\n        List<UserPublicKeyLink> existing = user.network.coreNode.getChain(username).join();\n        Multihash newStorageNodeId = node2.storage.id().join();\n\n        List<UserPublicKeyLink> evilChain = evil.network.coreNode.getChain(evilusername).join();\n        UserPublicKeyLink evilLast = evilChain.get(0);\n        UserPublicKeyLink.Claim newClaim = UserPublicKeyLink.Claim.build(username, evil.signer.secret,\n                LocalDate.now().plusMonths(2), Arrays.asList(newStorageNodeId)).join();\n        UserPublicKeyLink evilUpdate = evilLast.withClaim(newClaim);\n        List<UserPublicKeyLink> newChain = Arrays.asList(evilUpdate);\n        UserContext userViaNewServer = ensureSignedUp(username, password, getNode(iNode2), crypto);\n        List<BatWithId> bats = user.network.batCave.getUserBats(username, userViaNewServer.signer).join();\n        Optional<BatWithId> mirrorBat = Optional.of(bats.get(bats.size() - 1));\n        try {\n            userViaNewServer.network.coreNode.migrateUser(username, newChain, originalNodeId, mirrorBat, LocalDateTime.now(), 1_000_000, true).join();\n            throw new RuntimeException(\"Shouldn't get here!\");\n        } catch (CompletionException e) {}\n\n        List<UserPublicKeyLink> chain = userViaNewServer.network.coreNode.getChain(username).join();\n        Multihash storageNode = chain.get(chain.size() - 1).claim.storageProviders.stream().findFirst().get();\n        Assert.assertTrue(storageNode.equals(originalNodeId));\n    }\n\n    @Test\n    public void internodeFriends() throws Exception {\n        String username1 = generateUsername(random);\n        String password1 = randomString();\n        UserContext u1 = ensureSignedUp(username1, password1, getNode(iNode2), crypto);\n        String username2 = generateUsername(random);\n        String password2 = randomString();\n        UserContext u2 = ensureSignedUp(username2, password2, getNode(iNode1), crypto);\n        updatePkis();\n\n        u2.sendFollowRequest(username1, SymmetricKey.random()).get();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", u1Requests.size() > 0);\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, true).get();\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().get();\n        Optional<FileWrapper> u1ToU2 = u2.getByPath(\"/\" + u1.username).get();\n        assertTrue(\"Friend root present after accepted follow request\", u1ToU2.isPresent());\n\n        Optional<FileWrapper> u2ToU1 = u1.getByPath(\"/\" + u2.username).get();\n        assertTrue(\"Friend root present after accepted follow request\", u2ToU1.isPresent());\n\n        Set<String> u1Following = ensureSignedUp(username1, password1, getNode(iNode2).clear(), crypto).getSocialState().get()\n                .followingRoots.stream().map(f -> f.getName())\n                .collect(Collectors.toSet());\n        assertTrue(\"Following correct\", u1Following.contains(u2.username));\n\n        Set<String> u2Following = ensureSignedUp(username2, password2, getNode(iNode1).clear(), crypto).getSocialState().get()\n                .followingRoots.stream().map(f -> f.getName())\n                .collect(Collectors.toSet());\n        assertTrue(\"Following correct\", u2Following.contains(u1.username));\n    }\n\n    @Test\n    public void writeViaUnrelatedNode() throws Exception {\n        String username1 = generateUsername(random);\n        String password1 = randomString();\n        UserContext u1 = ensureSignedUp(username1, password1, getNode(iNode2), crypto);\n        updatePkis();\n\n        byte[] data = \"G'day mate!\".getBytes();\n        String filename = \"hey.txt\";\n        FileWrapper root = u1.getUserRoot().get();\n        FileWrapper upload = root.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                getNode(iNode1), crypto, () -> false, x -> {}).get();\n        Thread.sleep(10_000); // make sure pointer cache is invalidated\n        Optional<FileWrapper> file = u1.getByPath(\"/\" + username1 + \"/\" + filename).get();\n        Assert.assertTrue(file.isPresent());\n    }\n\n    @Test\n    public void grantAndRevokeFileReadAccess() throws Exception {\n        int shareeCount = 2;\n        PeergosNetworkUtils.grantAndRevokeFileReadAccess(getNode(iNode1), getNode(iNode2), shareeCount, random, this::updatePkis);\n    }\n\n    @Test\n    public void grantAndRevokeDirReadAccess() throws Exception {\n        int shareeCount = 2;\n        PeergosNetworkUtils.grantAndRevokeDirReadAccess(getNode(iNode1), getNode(iNode2), shareeCount, random, this::updatePkis);\n    }\n\n    @Test\n    public void publicLinkToFile() throws Exception {\n        PeergosNetworkUtils.publicLinkToFile(random, getNode(iNode1), getNode(iNode2), this::updatePkis);\n    }\n\n    @Ignore\n    @Test\n    public void serverIdentityRotation() throws Exception {\n        if (iNode1 == 0 || iNode2 == 0)\n            return; // Don't test migration to/from pki node\n\n        String password = randomString();\n        String username = generateUsername(random);\n        UserContext context = ensureSignedUp(username, password, getNode(iNode1), crypto);\n        byte[] fileData = new byte[6*1024*1024];\n        new Random(28).nextBytes(fileData);\n        String filename = \"somefile.bin\";\n        context.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(fileData),\n                fileData.length, context.network, crypto,  () -> false, x -> {}).join();\n        FileWrapper file = context.getByPath(PathUtil.get(context.username + \"/\" + filename)).join().get();\n        AbsoluteCapability cap = file.getPointer().capability.readOnly();\n\n        context.getUserRoot().join().uploadOrReplaceFile(filename+\"2\", AsyncReader.build(fileData),\n                fileData.length, context.network, crypto,  () -> false, x -> {}).join();\n        FileWrapper file2 = context.getByPath(PathUtil.get(context.username + \"/\" + filename + \"2\")).join().get();\n        AbsoluteCapability cap2 = file2.getPointer().capability.readOnly();\n\n        Multihash originalHost = context.network.coreNode.getHomeServer(context.username).join().get();\n\n        // rotate server identity, and check file cap works from other server\n        stopServer(iNode1);\n        rotateServerIdentity(iNode1);\n        startServer(iNode1);\n        Thread.sleep(60_000); // wait one DNS cycle\n\n        // login through other server\n        ensureSignedUp(username, password, getNode(iNode2), crypto);\n\n        FileWrapper fromOtherServer = getNode(iNode2).getFile(cap, context.username).join().get();\n\n        // update owner host and check cap still works from other server\n        context = ensureSignedUp(username, password, getNode(iNode1), crypto);\n        context.ensureCurrentHost().join();\n        Multihash updatedHost = context.network.coreNode.getHomeServer(context.username).join().get();\n        Assert.assertTrue(! updatedHost.equals(originalHost));\n\n        // login again through other server\n        ensureSignedUp(username, password, getNode(iNode2), crypto);\n        FileWrapper afterRotation = getNode(iNode2).getFile(cap2, context.username).join().get();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MultiUserTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.Args;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.fingerprint.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.TriFunction;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.server.*;\nimport peergos.shared.social.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Predicate;\nimport java.util.stream.*;\n\nimport static org.junit.Assert.assertTrue;\nimport static peergos.server.tests.PeergosNetworkUtils.ensureSignedUp;\nimport static peergos.server.tests.PeergosNetworkUtils.getUserContextsForNode;\n\npublic class MultiUserTests {\n\n    private static Args args = UserTests.buildArgs()\n            .with(\"enable-gc\", \"false\")\n            .with(\"log-to-console\", \"true\");\n    private static UserService service;\n    private Random random = new Random();\n    private final NetworkAccess network;\n    private static final Crypto crypto = Main.initCrypto();\n    private final int userCount;\n\n    public MultiUserTests() {\n        this.userCount = 2;\n        this.network = NetworkAccess.buildBuffered(new CachingStorage(service.storage, 1_000, 50 * 1024),\n                service.bats, service.coreNode, service.account, service.mutable, 0, service.social,\n                service.controller, service.usage, service.serverMessages, crypto.hasher, Arrays.asList(\"peergos\"), false);\n    }\n\n    @BeforeClass\n    public static void init() {\n        service = Main.PKI_INIT.main(args).localApi;\n    }\n\n    public static void checkUserValidity(NetworkAccess network, String username) {\n        PublicKeyHash identity = network.coreNode.getPublicKeyHash(username).join().get();\n        checkUserValidity(1, identity, identity, Collections.emptySet(), network);\n    }\n\n    public static void checkUserValidity(int maxClaims,\n                                         PublicKeyHash owner,\n                                         PublicKeyHash writer,\n                                         Set<PublicKeyHash> ancestors,\n                                         NetworkAccess network) {\n        WriterData props = WriterData.getWriterData(owner, writer, network.mutable, network.dhtClient).join().props.get();\n        if (! props.ownedKeys.isPresent())\n            return;\n        OwnedKeyChamp ownedChamp = props.getOwnedKeyChamp(owner, network.dhtClient, network.hasher).join();\n        Set<OwnerProof> empty = Collections.emptySet();\n        Set<OwnerProof> claims = ownedChamp.applyToAllMappings(owner, empty,\n                (a, b) -> CompletableFuture.completedFuture(Stream.concat(a.stream(), Stream.of(b.right)).collect(Collectors.toSet())),\n                network.dhtClient).join();\n        Set<PublicKeyHash> ownedKeys = claims.stream()\n                .map(p -> p.ownedKey)\n                .collect(Collectors.toSet());\n        Set<Pair<PublicKeyHash, PublicKeyHash>> pairs = claims.stream()\n                .map(p -> new Pair<>(p.getAndVerifyOwner(owner, network.dhtClient).join(), p.ownedKey))\n                .collect(Collectors.toSet());\n        Set<PublicKeyHash> ownerKeys = pairs.stream()\n                .map(p -> p.left)\n                .collect(Collectors.toSet());\n        if (claims.size() > maxClaims)\n            throw new IllegalStateException(\"Too many owned keys on identity key pair for \" + writer);\n        if (! ownerKeys.isEmpty() && ownerKeys.size() != 1)\n            throw new IllegalStateException(\"More than 1 owner key on writer data for \" + writer);\n        if (! ownerKeys.isEmpty() && ! ownerKeys.contains(writer))\n            throw new IllegalStateException(\"WriterData contains claims with wrong owner for \" + writer);\n        if (ownedKeys.contains(writer))\n            throw new IllegalStateException(\"Identity key pair owns itself!\");\n        HashSet<PublicKeyHash> withCurrent = new HashSet<>(ancestors);\n        withCurrent.add(writer);\n        for (PublicKeyHash ownedKey : ownedKeys) {\n            if (! withCurrent.contains(ownedKey))\n                checkUserValidity(Integer.MAX_VALUE, owner, ownedKey, withCurrent, network);\n\n        }\n    }\n\n    private List<UserContext> getUserContexts(int size, List<String> passwords) {\n        return getUserContextsForNode(network.clear(), random, size, passwords);\n    }\n\n    @Test\n    public void copyDirFromFriend() {\n        PeergosNetworkUtils.copyDirFromFriend(network, random);\n    }\n\n    @Test\n    public void copyDirToFriend() {\n        PeergosNetworkUtils.copyDirToFriend(network, random);\n    }\n\n    @Test\n    public void copySubDirFromFriend() {\n        PeergosNetworkUtils.copySubdirFromFriend(network, random);\n    }\n\n    @Test\n    public void grantAndRevokeFileReadAccess() throws Exception {\n        PeergosNetworkUtils.grantAndRevokeFileReadAccess(network, network, userCount, random, () -> {});\n    }\n\n    @Test\n    public void sharedwithPermutations() throws Exception {\n        PeergosNetworkUtils.sharedwithPermutations(network, random);\n    }\n\n    @Test\n    public void sharedWriteableAndTruncate() throws Exception {\n        PeergosNetworkUtils.sharedWriteableAndTruncate(network, random);\n    }\n\n    @Test\n    public void renameSharedwithFolder() throws Exception {\n        PeergosNetworkUtils.renameSharedwithFolder(network, random);\n    }\n\n    @Test\n    public void grantAndRevokeFileWriteAccess() throws Exception {\n        PeergosNetworkUtils.grantAndRevokeFileWriteAccess(network, network, userCount, random);\n    }\n\n    @Test\n    public void shareAFileWithDifferentSigner() {\n        PeergosNetworkUtils.shareFileWithDifferentSigner(network, network, random);\n    }\n\n    @Test\n    public void grantAndRevokeDirReadAccess() throws Exception {\n        PeergosNetworkUtils.grantAndRevokeDirReadAccess(network, network, 2, random, () -> {});\n    }\n\n    @Test\n    public void grantAndRevokeDirWriteAccess() throws Exception {\n        PeergosNetworkUtils.grantAndRevokeDirWriteAccess(network, network, 2, random);\n    }\n\n    @Test\n    public void grantAndRevokeNestedDirWriteAccess() {\n        PeergosNetworkUtils.grantAndRevokeNestedDirWriteAccess(network, random);\n    }\n\n    @Test\n    public void grantAndRevokeParentNestedWriteAccess() {\n        PeergosNetworkUtils.grantAndRevokeParentNestedWriteAccess(network, random);\n    }\n\n    @Test\n    public void grantAndRevokeDirWriteAccessWithNestedWriteAccess() {\n        PeergosNetworkUtils.grantAndRevokeDirWriteAccessWithNestedWriteAccess(network, random);\n    }\n\n    @Test\n    public void grantAndRevokeReadAccessToFileInFolder() throws IOException {\n        PeergosNetworkUtils.grantAndRevokeReadAccessToFileInFolder(network, random);\n    }\n\n    @Test\n    public void grantWriteToFileAndDeleteParent() throws IOException {\n        PeergosNetworkUtils.grantWriteToFileAndDeleteParent(network, random);\n    }\n\n    @Test\n    public void grantAndRevokeWriteThenReadAccessToFolder() {\n        PeergosNetworkUtils.grantAndRevokeWriteThenReadAccessToFolder(network, random);\n    }\n\n    @Test\n    public void socialFeed() {\n        PeergosNetworkUtils.socialFeed(network, random);\n    }\n\n    @Test\n    public void socialPostPropagation() {\n        PeergosNetworkUtils.socialPostPropagation(network, random);\n    }\n\n    @Test\n    public void socialFeedBug() {\n        PeergosNetworkUtils.socialFeedBug(network, random);\n    }\n\n    @Test\n    public void socialFeedAndUnfriending() {\n        PeergosNetworkUtils.socialFeedAndUnfriending(network, random);\n    }\n\n    @Test\n    public void socialFeedCommentOnSharedFile() throws Exception {\n        PeergosNetworkUtils.socialFeedCommentOnSharedFile(network, network, random);\n    }\n\n    @Test\n    public void socialFeedCASExceptionOnUpdate() throws Exception {\n        PeergosNetworkUtils.socialFeedCASExceptionOnUpdate(network, network, random);\n    }\n\n    @Test\n    public void socialFeedVariations() {\n        PeergosNetworkUtils.socialFeedVariations(network, random);\n    }\n\n    @Test\n    public void socialFeedVariations2() {\n        PeergosNetworkUtils.socialFeedVariations2(network, random);\n    }\n\n    @Test\n    public void socialFeedFailsInUI() {\n        PeergosNetworkUtils.socialFeedFailsInUI(network, random);\n    }\n\n    @Test\n    public void socialFeedEmpty() {\n        PeergosNetworkUtils.socialFeedEmpty(network, random);\n    }\n\n    @Test\n    public void groupSharing() {\n        PeergosNetworkUtils.groupSharing(network, random);\n    }\n\n    @Test\n    public void email() {\n        PeergosNetworkUtils.email(network, random);\n    }\n\n    @Test\n    public void deleteEmailApp() {\n        PeergosNetworkUtils.deleteEmailApp(network, random);\n    }\n\n    @Test\n    public void chatMultipleInvites() {\n        PeergosNetworkUtils.chatMultipleInvites(network, random);\n    }\n\n    @Test\n    public void chat() {\n        PeergosNetworkUtils.chat(network, random);\n    }\n\n    @Test\n    public void chatReplyWithAttachment() {\n        PeergosNetworkUtils.chatReplyWithAttachment(network, random);\n    }\n\n    @Test\n    public void concurrentChatMerges() {\n        PeergosNetworkUtils.concurrentChatMerges(network, random);\n    }\n\n    @Test\n    public void chatLeaveAndDelete() {\n        PeergosNetworkUtils.memberLeaveAndDeleteChat(network, random);\n    }\n\n    @Test\n    public void editChatMessage() {\n        PeergosNetworkUtils.editChatMessage(network, random);\n    }\n\n    @Test\n    public void groupSharingToFollowers() {\n        PeergosNetworkUtils.groupSharingToFollowers(network, random);\n    }\n\n    @Test\n    public void groupReadIndividualWrite() {\n        PeergosNetworkUtils.groupReadIndividualWrite(network, random);\n    }\n\n    @Test\n    public void groupAwareSharingReadAccess() {\n        TriFunction<UserContext, Path, Set<String>, CompletableFuture<Snapshot>> shareFunction =\n                (u1, dirToShare, usersToAdd) ->\n                        u1.shareReadAccessWith(dirToShare, usersToAdd);\n\n        TriFunction<UserContext, Path, Set<String>, CompletableFuture<Snapshot>> unshareFunction =\n                (u1, dirToShare, usersToRemove) -> u1.unShareReadAccessWith(dirToShare, usersToRemove);\n\n        TriFunction<UserContext, Path, FileSharedWithState, Integer> resultFunc =\n                (u1, dirToShare, fileSharedWithState) -> u1.sharedWith(dirToShare).join().readAccess.size();\n\n        PeergosNetworkUtils.groupAwareSharing(network, random, shareFunction, unshareFunction, resultFunc);\n    }\n\n    @Test\n    public void groupAwareSharingWriteAccess() {\n        TriFunction<UserContext, Path, Set<String>, CompletableFuture<Snapshot>> shareFunction =\n                (u1, dirToShare, usersToAdd) ->\n                        u1.shareWriteAccessWith(dirToShare, usersToAdd);\n        TriFunction<UserContext, Path, Set<String>, CompletableFuture<Snapshot>> unshareFunction =\n                (u1, dirToShare, usersToRemove) ->\n                        u1.unShareWriteAccessWith(dirToShare, usersToRemove);\n        TriFunction<UserContext, Path, FileSharedWithState, Integer> resultFunc =\n                (u1, dirToShare, fileSharedWithState) -> u1.sharedWith(dirToShare).join().writeAccess.size();\n\n        PeergosNetworkUtils.groupAwareSharing(network, random, shareFunction, unshareFunction, resultFunc);\n    }\n\n    @Test\n    public void safeCopyOfFriendsReadAccess() throws Exception {\n        TriFunction<UserContext, UserContext, String, CompletableFuture<Snapshot>> readAccessSharingFunction =\n                (u1, u2, filename) ->\n        u1.shareReadAccessWith(PathUtil.get(u1.username, filename), Collections.singleton(u2.username));\n        safeCopyOfFriends(readAccessSharingFunction);\n    }\n\n    @Test\n    public void safeCopyOfFriendsWriteAccess() throws Exception {\n        TriFunction<UserContext, UserContext, String, CompletableFuture<Snapshot>> writeAccessSharingFunction =\n                (u1, u2, filename) ->\n                        u1.shareWriteAccessWith(PathUtil.get(u1.username, filename), Collections.singleton(u2.username));\n        safeCopyOfFriends(writeAccessSharingFunction);\n    }\n\n    private void safeCopyOfFriends(TriFunction<UserContext, UserContext, String, CompletableFuture<Snapshot>> sharingFunction) throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(random(), \"b\", network.clear(), crypto);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), Arrays.asList(u2));\n\n        // upload a file to u1's space\n        FileWrapper u1Root = u1.getUserRoot().get();\n        String filename = \"somefile.txt\";\n        byte[] data = UserTests.randomData(10*1024*1024);\n\n        FileWrapper uploaded = u1Root.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                u1.network, crypto, () -> false, l -> {}).get();\n\n        // share the file from u1 to each of the others\n        FileWrapper u1File = u1.getByPath(u1.username + \"/\" + filename).get().get();\n        sharingFunction.apply(u1, u2, filename).get();\n\n        // check other user can read the file\n        FileWrapper sharedFile = u2.getByPath(u1.username + \"/\" + filename).get().get();\n        String dirname = \"adir\";\n        u2.getUserRoot().get().mkdir(dirname, network, false, u2.mirrorBatId(), crypto).get();\n        FileWrapper targetDir = u2.getByPath(PathUtil.get(u2.username, dirname).toString()).get().get();\n\n        // copy the friend's file to our own space, this should reupload the file encrypted with a new key\n        // this prevents us exposing to the network our social graph by the fact that we pin the same file fragments\n        sharedFile.copyTo(targetDir, u2).get();\n        FileWrapper copy = u2.getByPath(PathUtil.get(u2.username, dirname, filename).toString()).get().get();\n\n        // check that the copied file has the correct contents\n        UserTests.checkFileContents(data, copy, u2);\n        Assert.assertTrue(\"Different base key\", ! copy.getPointer().capability.rBaseKey.equals(u1File.getPointer().capability.rBaseKey));\n        Assert.assertTrue(\"Different metadata key\", ! UserTests.getMetaKey(copy).equals(UserTests.getMetaKey(u1File)));\n        Assert.assertTrue(\"Different data key\", ! UserTests.getDataKey(copy).equals(UserTests.getDataKey(u1File)));\n    }\n\n    @Test\n    public void revokeReadAccessToWritableFile() {\n\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n        UserContext u3 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n        UserContext u4 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        List<UserContext> all = Arrays.asList(u1, u2, u3, u4);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), Arrays.asList(u2, u3, u4));\n\n        u1.getUserRoot().join().mkdir(\"subdir\", u1.network, false, u1.mirrorBatId(), crypto).join();\n        byte[] fileData = \"file data\".getBytes();\n        AsyncReader reader = AsyncReader.build(fileData);\n        u1.getByPath(PathUtil.get(u1.username, \"subdir\")).join().get().uploadOrReplaceFile(\"file.txt\",\n                reader, fileData.length, u1.network, crypto, () -> false, x -> {}).join();\n        Path filePath = PathUtil.get(u1.username, \"subdir\", \"file.txt\");\n        FileWrapper file = u1.getByPath(filePath).join().get();\n        u1.shareWriteAccessWith(filePath, Collections.singleton(u2.username)).join();\n        u1.shareWriteAccessWith(filePath, Collections.singleton(u3.username)).join();\n        u1.shareReadAccessWith(filePath, Collections.singleton(u4.username)).join();\n        u1.unShareReadAccess(filePath, u4.username).join();\n\n        // check u1 can log in\n        UserContext freshContext = PeergosNetworkUtils.ensureSignedUp(u1.username, \"a\", network.clear(), crypto);\n        freshContext.getUserRoot().join().mkdir(\"Adir\", network, false, u1.mirrorBatId(), crypto).join();\n        checkUserValidity(network, u1.username);\n    }\n    \n    @Test\n    public void shareTwoFilesWithSameNameReadAccess() throws Exception {\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> readAccessSharingFunction =\n                (u1, userContexts, path) ->\n                        u1.shareReadAccessWith(path, userContexts.stream().map(u -> u.username).collect(Collectors.toSet()));\n        shareTwoFilesWithSameName(readAccessSharingFunction);\n    }\n\n    @Test\n    public void shareTwoFilesWithSameNameWriteAccess() throws Exception {\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> writeAccessSharingFunction =\n                (u1, userContexts, path) ->\n                        u1.shareWriteAccessWith(path, userContexts.stream().map(u -> u.username).collect(Collectors.toSet()));\n        shareTwoFilesWithSameName(writeAccessSharingFunction);\n    }\n\n    private void shareTwoFilesWithSameName(TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> sharingFunction) throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        // send follow requests from each other user to u1\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        FileWrapper u1Root = u1.getUserRoot().get();\n        String filename = \"somefile.txt\";\n        byte[] data1 = \"Hello Peergos friend!\".getBytes();\n        AsyncReader file1Reader = new AsyncReader.ArrayBacked(data1);\n        FileWrapper uploaded = u1Root.uploadOrReplaceFile(filename, file1Reader, data1.length,\n                u1.network, u1.crypto, () -> false, l -> {}).get();\n\n        // upload a different file with the same name in a sub folder\n        uploaded.mkdir(\"subdir\", u1.network, false, u1.mirrorBatId(), crypto).get();\n        FileWrapper subdir = u1.getByPath(\"/\" + u1.username + \"/subdir\").get().get();\n        byte[] data2 = \"Goodbye Peergos friend!\".getBytes();\n        AsyncReader file2Reader = new AsyncReader.ArrayBacked(data2);\n        subdir.uploadOrReplaceFile(filename, file2Reader, data2.length,\n                u1.network, u1.crypto, () -> false, l -> {}).get();\n\n        // share the file from \"a\" to each of the others\n        //        sharingFunction.apply(u1, u2, filenameu1.shareReadAccessWith(PathUtil.get(u1.username, filename), userContexts.stream().map(u -> u.username).collect(Collectors.toSet())).get();\n\n        sharingFunction.apply(u1, userContexts, PathUtil.get(u1.username, filename)).get();\n\n        sharingFunction.apply(u1, userContexts, PathUtil.get(u1.username, \"subdir\", filename)).get();\n\n        // check other users can read the file\n        for (UserContext userContext : userContexts) {\n            Optional<FileWrapper> sharedFile = userContext.getByPath(u1.username + \"/\" + filename).get();\n            Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n\n            AsyncReader inputStream = sharedFile.get().getInputStream(userContext.network,\n                    userContext.crypto, l -> {}).get();\n\n            byte[] fileContents = Serialize.readFully(inputStream, sharedFile.get().getFileProperties().size).get();\n            Assert.assertTrue(\"shared file contents correct\", Arrays.equals(data1, fileContents));\n        }\n\n        // check other users can read the file\n        for (UserContext userContext : userContexts) {\n            String expectedPath = PathUtil.get(u1.username, \"subdir\", filename).toString();\n            Optional<FileWrapper> sharedFile = userContext.getByPath(expectedPath).get();\n            Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n\n            AsyncReader inputStream = sharedFile.get().getInputStream(userContext.network,\n                    userContext.crypto, l -> {}).get();\n\n            byte[] fileContents = Serialize.readFully(inputStream, sharedFile.get().getFileProperties().size).get();\n            Assert.assertTrue(\"shared file contents correct\", Arrays.equals(data2, fileContents));\n        }\n    }\n\n    @Test\n    public void deleteFileSharedWithWriteAccess() throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        // send follow requests from each other user to u1\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        FileWrapper u1Root = u1.getUserRoot().get();\n        String filename = \"somefile.txt\";\n        byte[] data1 = \"Hello Peergos friend!\".getBytes();\n        AsyncReader file1Reader = new AsyncReader.ArrayBacked(data1);\n        String subdirName = \"subdir\";\n        u1Root.mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        Path subdirPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper subdir = u1.getByPath(subdirPath).get().get();\n        FileWrapper uploaded = subdir.uploadOrReplaceFile(filename, file1Reader, data1.length,\n                u1.network, u1.crypto, () -> false, l -> {}).get();\n\n        Path filePath = PathUtil.get(u1.username, subdirName, filename);\n        u1.shareWriteAccessWith(filePath, userContexts.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n\n        // check other users can read the file\n        for (UserContext userContext : userContexts) {\n            Optional<FileWrapper> sharedFile = userContext.getByPath(filePath).get();\n            Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n\n            AsyncReader inputStream = sharedFile.get().getInputStream(userContext.network,\n                    userContext.crypto, l -> {}).get();\n\n            byte[] fileContents = Serialize.readFully(inputStream, sharedFile.get().getFileProperties().size).get();\n            Assert.assertTrue(\"shared file contents correct\", Arrays.equals(data1, fileContents));\n        }\n        //delete file\n        FileWrapper theFile = u1.getByPath(filePath).get().get();\n        FileWrapper parentFolder = u1.getByPath(subdirPath).get().get();\n        FileWrapper metaOnlyParent = theFile.retrieveParent(u1.network).get().get();\n\n        Assert.assertTrue(\"Following parent link results in read only parent\",\n                ! metaOnlyParent.isWritable() && ! metaOnlyParent.isReadable());\n\n        Set<PublicKeyHash> keysOwnedByRootSigner = DeletableContentAddressedStorage.getDirectOwnedKeys(theFile.owner(), parentFolder.writer(),\n                u1.network.mutable, (h, s) -> ContentAddressedStorage.getWriterData(parentFolder.writer(), h,s, u1.network.dhtClient),\n                u1.network.dhtClient, u1.network.hasher).join();\n        Assert.assertTrue(\"New writer key present\", keysOwnedByRootSigner.contains(theFile.writer()));\n\n        Set<String> sharedWriteAccessWithBefore = u1.sharedWith(filePath).join().get(SharedWithCache.Access.WRITE);\n        Assert.assertTrue(\"file shared\", ! sharedWriteAccessWithBefore.isEmpty());\n\n        theFile.remove(parentFolder, filePath, u1).get();\n        Assert.assertTrue(\"file removed\", u1.getByPath(filePath).join().isEmpty());\n\n        Set<String> sharedWriteAccessWithAfter = u1.sharedWith(filePath).join().get(SharedWithCache.Access.WRITE);\n        Assert.assertTrue(\"file shared\", sharedWriteAccessWithAfter.isEmpty());\n\n        for (UserContext userContext : userContexts) {\n            Optional<FileWrapper> sharedFile = userContext.getByPath(filePath).get();\n            Assert.assertTrue(\"shared file removed\", ! sharedFile.isPresent());\n        }\n        Set<PublicKeyHash> updatedKeysOwnedByRootSigner = DeletableContentAddressedStorage.getDirectOwnedKeys(theFile.owner(),\n                parentFolder.writer(), u1.network.mutable,\n                (h, s) -> ContentAddressedStorage.getWriterData(theFile.owner(), h,s, u1.network.dhtClient),\n                u1.network.dhtClient, u1.network.hasher).join();\n        Assert.assertTrue(\"New writer key not present\", ! updatedKeysOwnedByRootSigner.contains(theFile.writer()));\n    }\n\n    @Test\n    public void deleteFolderSharedWithWriteAccess() throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        // send follow requests from each other user to u1\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space in a subdir\n        FileWrapper u1Root = u1.getUserRoot().get();\n        String filename = \"somefile.txt\";\n        byte[] data1 = \"Hello Peergos friend!\".getBytes();\n        AsyncReader file1Reader = new AsyncReader.ArrayBacked(data1);\n        String subdirName = \"subdir\";\n        u1Root.mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        Path subdirPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper subdir = u1.getByPath(subdirPath).get().get();\n        FileWrapper uploaded = subdir.uploadOrReplaceFile(filename, file1Reader, data1.length,\n                u1.network, u1.crypto, () -> false, l -> {}).get();\n        u1.getByPath(subdirPath).get().get().mkdir(\"another-dir\",  u1.network,false, u1.mirrorBatId(), crypto).join();\n\n        Path dirPath = PathUtil.get(u1.username, subdirName);\n        u1.shareWriteAccessWith(dirPath, userContexts.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n\n        // check other users can read the file\n        for (UserContext userContext : userContexts) {\n            Optional<FileWrapper> sharedFile = userContext.getByPath(dirPath.resolve(filename)).get();\n            Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n\n            AsyncReader inputStream = sharedFile.get().getInputStream(userContext.network,\n                    userContext.crypto, l -> {}).get();\n\n            byte[] fileContents = Serialize.readFully(inputStream, sharedFile.get().getFileProperties().size).get();\n            Assert.assertTrue(\"shared file contents correct\", Arrays.equals(data1, fileContents));\n        }\n        //delete folder\n        FileWrapper theDir = u1.getByPath(dirPath).get().get();\n        FileWrapper parentFolder = u1.getUserRoot().join();\n        FileWrapper metaOnlyParent = theDir.retrieveParent(u1.network).get().get();\n\n        Assert.assertTrue(\"Following parent link results in read only parent\",\n                ! metaOnlyParent.isWritable() && ! metaOnlyParent.isReadable());\n\n        Set<PublicKeyHash> keysOwnedByRootSigner = DeletableContentAddressedStorage.getDirectOwnedKeys(theDir.owner(), parentFolder.writer(),\n                u1.network.mutable, (h, s) -> ContentAddressedStorage.getWriterData(parentFolder.writer(), h,s, u1.network.dhtClient),\n                u1.network.dhtClient, u1.network.hasher).join();\n        Assert.assertTrue(\"New writer key present\", keysOwnedByRootSigner.contains(theDir.writer()));\n\n        Set<String> sharedWriteAccessWithBefore = u1.sharedWith(dirPath).join().get(SharedWithCache.Access.WRITE);\n        Assert.assertTrue(\"file shared\", ! sharedWriteAccessWithBefore.isEmpty());\n        System.out.println(\"Start DELETE\");\n        theDir.remove(parentFolder, dirPath, u1).get();\n        Assert.assertTrue(\"dir removed\", u1.getByPath(dirPath).join().isEmpty());\n\n        Set<String> sharedWriteAccessWithAfter = u1.sharedWith(dirPath).join().get(SharedWithCache.Access.WRITE);\n        Assert.assertTrue(\"dir unshared\", sharedWriteAccessWithAfter.isEmpty());\n\n        for (UserContext userContext : userContexts) {\n            Optional<FileWrapper> sharedDir = userContext.getByPath(dirPath).get();\n            Assert.assertTrue(\"shared dir removed\", sharedDir.isEmpty());\n        }\n        Set<PublicKeyHash> updatedKeysOwnedByRootSigner = DeletableContentAddressedStorage.getDirectOwnedKeys(theDir.owner(),\n                parentFolder.writer(), u1.network.mutable,\n                (h, s) -> ContentAddressedStorage.getWriterData(theDir.owner(), h,s, u1.network.dhtClient),\n                u1.network.dhtClient, u1.network.hasher).join();\n        Assert.assertTrue(\"New writer key not present\", ! updatedKeysOwnedByRootSigner.contains(theDir.writer()));\n    }\n\n    @Test\n    public void renamedFileSharedWith() throws Exception {\n        //read access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> readAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareReadAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n\n        renamedFileSharedWith(readAccessSharingFunction, SharedWithCache.Access.READ);\n        //write access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> writeAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareWriteAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n        renamedFileSharedWith(writeAccessSharingFunction, SharedWithCache.Access.WRITE);\n    }\n\n    private void renamedFileSharedWith(\n            TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> shareFunction,\n            SharedWithCache.Access sharedWithAccess)\n            throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        // send follow requests from each other user to u1\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        FileWrapper u1Root = u1.getUserRoot().get();\n        String filename = \"somefile.txt\";\n        byte[] data1 = \"Hello Peergos friend!\".getBytes();\n        AsyncReader file1Reader = new AsyncReader.ArrayBacked(data1);\n        String subdirName = \"subdir\";\n        u1Root.mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        Path subdirPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper subdir = u1.getByPath(subdirPath).get().get();\n        FileWrapper uploaded = subdir.uploadOrReplaceFile(filename, file1Reader, data1.length,\n                u1.network, u1.crypto, () -> false, l -> {}).get();\n\n        Path filePath = PathUtil.get(u1.username, subdirName, filename);\n\n        shareFunction.apply(u1, userContexts, filePath).join();\n\n        //rename file\n        FileWrapper theFile = u1.getByPath(filePath).get().get();\n        FileWrapper parentFolder = u1.getByPath(subdirPath).get().get();\n\n        Set<String> sharedAccessWithBefore = u1.sharedWith(filePath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"file shared\", ! sharedAccessWithBefore.isEmpty());\n\n        String newFilename = \"newfilename.txt\";\n        theFile.rename(newFilename, parentFolder, filePath, u1).get();\n\n        filePath = PathUtil.get(u1.username, subdirName, newFilename);\n\n        Set<String> sharedAccessWithAfter = u1.sharedWith(filePath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"file shared\", ! sharedAccessWithAfter.isEmpty());\n    }\n\n    @Test\n    public void renamedDirectorySharedWith() throws Exception {\n        //read access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> readAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareReadAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n\n        renamedDirectorySharedWith(readAccessSharingFunction, SharedWithCache.Access.READ);\n        //write access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> writeAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareWriteAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n        renamedDirectorySharedWith(writeAccessSharingFunction, SharedWithCache.Access.WRITE);\n    }\n\n    private void renamedDirectorySharedWith(\n            TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> shareFunction,\n            SharedWithCache.Access sharedWithAccess)\n            throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        // send follow requests from each other user to u1\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        FileWrapper u1Root = u1.getUserRoot().get();\n        String subdirName = \"subdir\";\n        u1Root.mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n\n        Path filePath = PathUtil.get(u1.username, subdirName);\n\n        shareFunction.apply(u1, userContexts, filePath).join();\n\n        //rename directory\n        FileWrapper theDir = u1.getByPath(filePath).get().get();\n        FileWrapper parentFolder = u1.getUserRoot().get();\n\n        Set<String> sharedAccessWithBefore = u1.sharedWith(filePath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"directory shared\", ! sharedAccessWithBefore.isEmpty());\n\n        String newDirectoryName = \"newDir\";\n        theDir.rename(newDirectoryName, parentFolder, filePath, u1).get();\n\n        filePath = PathUtil.get(u1.username, newDirectoryName);\n\n        Set<String> sharedAccessWithAfter = u1.sharedWith(filePath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"directory shared\", ! sharedAccessWithAfter.isEmpty());\n    }\n\n    @Test\n    public void copyToFileSharedWith() throws Exception {\n        //read access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> readAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareReadAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n\n        copyToFileSharedWith(readAccessSharingFunction, SharedWithCache.Access.READ);\n        //write access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> writeAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareWriteAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n        copyToFileSharedWith(writeAccessSharingFunction, SharedWithCache.Access.WRITE);\n    }\n\n    private void copyToFileSharedWith(\n            TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> shareFunction,\n            SharedWithCache.Access sharedWithAccess)\n            throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        // send follow requests from each other user to u1\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        String filename = \"somefile.txt\";\n        byte[] data1 = \"Hello Peergos friend!\".getBytes();\n        AsyncReader file1Reader = new AsyncReader.ArrayBacked(data1);\n        String subdirName = \"subdir\";\n        String destinationSubdirName = \"destdir\";\n        u1.getUserRoot().get().mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        u1.getUserRoot().get().mkdir(destinationSubdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        Path subdirPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper subdir = u1.getByPath(subdirPath).get().get();\n        FileWrapper uploaded = subdir.uploadOrReplaceFile(filename, file1Reader, data1.length,\n                u1.network, u1.crypto, () -> false, l -> {}).get();\n\n        Path filePath = PathUtil.get(u1.username, subdirName, filename);\n\n        shareFunction.apply(u1, userContexts, filePath);\n\n        FileWrapper theFile = u1.getByPath(filePath).get().get();\n        Set<String> sharedWriteAccessWithBefore = u1.sharedWith(filePath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"file shared\", ! sharedWriteAccessWithBefore.isEmpty());\n\n        //copy file\n        Path destSubdirPath = PathUtil.get(u1.username, destinationSubdirName);\n        FileWrapper destSubdir = u1.getByPath(destSubdirPath).get().get();\n        theFile.copyTo(destSubdir, u1);\n\n        //old copy should retain sharedWith entries\n        Set<String> sharedWriteAccessWithOriginal = u1.sharedWith(filePath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"file shared\", ! sharedWriteAccessWithOriginal.isEmpty());\n\n        filePath = PathUtil.get(u1.username, destinationSubdirName, filename);\n\n        Set<String> sharedWriteAccessWithNewCopy = u1.sharedWith(filePath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"file shared\", sharedWriteAccessWithNewCopy.isEmpty());\n    }\n\n    @Test\n    public void copyToDirectorySharedWith() throws Exception {\n        //read access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> readAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareReadAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n\n        copyToDirectorySharedWith(readAccessSharingFunction, SharedWithCache.Access.READ);\n        //write access\n        TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> writeAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareWriteAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n        copyToDirectorySharedWith(writeAccessSharingFunction, SharedWithCache.Access.WRITE);\n    }\n\n    private void copyToDirectorySharedWith(\n            TriFunction<UserContext, List<UserContext>, Path, CompletableFuture<Snapshot>> shareFunction,\n            SharedWithCache.Access sharedWithAccess)\n            throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        String subdirName = \"subdir\";\n        String destinationDirName = \"destdir\";\n        u1.getUserRoot().get().mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        u1.getUserRoot().get().mkdir(destinationDirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n\n        Path dirPath = PathUtil.get(u1.username, subdirName);\n\n        shareFunction.apply(u1, userContexts, dirPath);\n\n        FileWrapper theDir = u1.getByPath(dirPath).get().get();\n        Set<String> sharedWriteAccessWithBefore = u1.sharedWith(dirPath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"directory shared\", ! sharedWriteAccessWithBefore.isEmpty());\n\n        //copy file\n        Path destDirPath = PathUtil.get(u1.username, destinationDirName);\n        FileWrapper destDir = u1.getByPath(destDirPath).get().get();\n        theDir.copyTo(destDir, u1).join();\n\n        //old copy should retain sharedWith entries\n        Set<String> sharedWriteAccessWithOriginal = u1.sharedWith(dirPath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"directory shared\", ! sharedWriteAccessWithOriginal.isEmpty());\n\n        dirPath = PathUtil.get(u1.username, destinationDirName, subdirName);\n\n        Set<String> sharedWriteAccessWithNewCopy = u1.sharedWith(dirPath).join().get(sharedWithAccess);\n        Assert.assertTrue(\"directory shared\", sharedWriteAccessWithNewCopy.isEmpty());\n    }\n\n    @Test\n    public void moveToFileSharedWith()\n            throws Exception {\n        // Secret link\n        TriFunction<UserContext, List<UserContext>, Path, Object> linkSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.createSecretLink(filePath.toString(), false, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        moveToFileSharedWith(linkSharingFunction, s -> ! s.links.isEmpty());\n\n        //read access\n        TriFunction<UserContext, List<UserContext>, Path, Object> readAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareReadAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n\n        moveToFileSharedWith(readAccessSharingFunction, s -> ! s.readAccess.isEmpty());\n        //write access\n        TriFunction<UserContext, List<UserContext>, Path, Object> writeAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareWriteAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n        moveToFileSharedWith(writeAccessSharingFunction, s -> ! s.writeAccess.isEmpty());\n    }\n\n    private void moveToFileSharedWith(TriFunction<UserContext, List<UserContext>, Path, Object> shareFunction,\n                                      Predicate<FileSharedWithState> isShared)\n            throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        String filename = \"somefile.txt\";\n        byte[] data1 = \"Hello Peergos friend!\".getBytes();\n        AsyncReader file1Reader = new AsyncReader.ArrayBacked(data1);\n        String subdirName = \"subdir\";\n        String destinationSubdirName = \"destdir\";\n        u1.getUserRoot().get().mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        u1.getUserRoot().get().mkdir(destinationSubdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        Path subdirPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper subdir = u1.getByPath(subdirPath).get().get();\n        FileWrapper uploaded = subdir.uploadOrReplaceFile(filename, file1Reader, data1.length,\n                u1.network, u1.crypto, () -> false, l -> {}).join();\n\n        Path filePath = PathUtil.get(u1.username, subdirName, filename);\n        shareFunction.apply(u1, userContexts, filePath);\n\n        FileWrapper theFile = u1.getByPath(filePath).get().get();\n        Path parentPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper theParent = u1.getByPath(parentPath).get().get();\n        FileSharedWithState shared = u1.sharedWith(filePath).join();\n        Assert.assertTrue(\"file shared\", isShared.test(shared));\n\n        //move file\n        Path destSubdirPath = PathUtil.get(u1.username, destinationSubdirName);\n        FileWrapper destSubdir = u1.getByPath(destSubdirPath).get().get();\n\n        theFile.moveTo(destSubdir, theParent, filePath, u1, () -> Futures.of(true)).join();\n\n        //old copy sharedWith entries should be removed\n        FileSharedWithState oldShared = u1.sharedWith(filePath).join();\n        Assert.assertTrue(\"file shared\", !isShared.test(oldShared));\n\n        filePath = PathUtil.get(u1.username, destinationSubdirName, filename);\n\n        //new copy sharedWith entry should not be empty\n        FileSharedWithState newShared = u1.sharedWith(filePath).join();\n        Assert.assertTrue(\"file shared\", isShared.test(newShared));\n    }\n\n    @Test\n    public void moveFileToDifferentWriter()\n            throws Exception {\n        // Secret link\n        TriFunction<UserContext, List<UserContext>, Path, Object> linkSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.createSecretLink(filePath.toString(), false, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        moveFileToDifferentWriter(linkSharingFunction, s -> ! s.links.isEmpty());\n\n        //read access\n        TriFunction<UserContext, List<UserContext>, Path, Object> readAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareReadAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet()));\n\n        moveFileToDifferentWriter(readAccessSharingFunction, s -> ! s.readAccess.isEmpty());\n        //write access\n        TriFunction<UserContext, List<UserContext>, Path, Object> writeAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareWriteAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n        moveFileToDifferentWriter(writeAccessSharingFunction, s -> ! s.writeAccess.isEmpty());\n    }\n\n    private void moveFileToDifferentWriter(TriFunction<UserContext, List<UserContext>, Path, Object> shareFunction,\n                                      Predicate<FileSharedWithState> isShared)\n            throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n        // make u1 friend all users\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        // upload a file to u1's space\n        String filename = \"somefile.txt\";\n        byte[] data1 = \"Hello Peergos friend!\".getBytes();\n        AsyncReader file1Reader = new AsyncReader.ArrayBacked(data1);\n        String subdirName = \"subdir\";\n        String destinationSubdirName = \"destdir\";\n        u1.getUserRoot().get().mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        u1.getUserRoot().get().mkdir(destinationSubdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n\n        // put new directory in a different writing space\n        u1.createSecretLink(PathUtil.get(u1.username, destinationSubdirName).toString(), true, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        Path subdirPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper subdir = u1.getByPath(subdirPath).get().get();\n        FileWrapper uploaded = subdir.uploadOrReplaceFile(filename, file1Reader, data1.length,\n                u1.network, u1.crypto, () -> false, l -> {}).join();\n\n        Path filePath = PathUtil.get(u1.username, subdirName, filename);\n        shareFunction.apply(u1, userContexts, filePath);\n\n        FileWrapper theFile = u1.getByPath(filePath).get().get();\n        Path parentPath = PathUtil.get(u1.username, subdirName);\n        FileWrapper theParent = u1.getByPath(parentPath).get().get();\n        FileSharedWithState shared = u1.sharedWith(filePath).join();\n        Assert.assertTrue(\"file shared\", isShared.test(shared));\n\n        //move file\n        Path destSubdirPath = PathUtil.get(u1.username, destinationSubdirName);\n        FileWrapper destSubdir = u1.getByPath(destSubdirPath).get().get();\n\n        theFile.moveTo(destSubdir, theParent, filePath, u1, () -> Futures.of(true)).join();\n\n        //old copy sharedWith entries should be removed\n        FileSharedWithState oldShared = u1.sharedWith(filePath).join();\n        Assert.assertTrue(\"file shared\", !isShared.test(oldShared));\n\n        filePath = PathUtil.get(u1.username, destinationSubdirName, filename);\n\n        //new copy sharedWith entry should be empty\n        FileSharedWithState newShared = u1.sharedWith(filePath).join();\n        Assert.assertTrue(\"file shared\", ! isShared.test(newShared));\n    }\n\n    @Test\n    public void moveToDirectorySharedWith()\n            throws Exception {\n        // Secret link\n        TriFunction<UserContext, List<UserContext>, Path, Object> linkSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.createSecretLink(filePath.toString(), false, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        moveToDirectorySharedWith(linkSharingFunction, s -> ! s.links.isEmpty());\n\n        //read access\n        TriFunction<UserContext, List<UserContext>, Path, Object> readAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareReadAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n\n        moveToDirectorySharedWith(readAccessSharingFunction, s -> ! s.readAccess.isEmpty());\n        //write access\n        TriFunction<UserContext, List<UserContext>, Path, Object> writeAccessSharingFunction =\n                (u1, u2List, filePath) ->\n                        u1.shareWriteAccessWith(filePath, u2List.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n        moveToDirectorySharedWith(writeAccessSharingFunction, s -> ! s.writeAccess.isEmpty());\n    }\n\n    private void moveToDirectorySharedWith(TriFunction<UserContext, List<UserContext>, Path, Object> shareFunction,\n                                           Predicate<FileSharedWithState> isShared)\n            throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), \"a\", network.clear(), crypto);\n\n        // send follow requests from each other user to \"a\"\n        List<String> shareePasswords = IntStream.range(0, 1)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> userContexts = getUserContexts(1, shareePasswords);\n\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), userContexts);\n\n        String subdirName = \"subdir\";\n        String destinationSubdirName = \"destdir\";\n        u1.getUserRoot().get().mkdir(subdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n        u1.getUserRoot().get().mkdir(destinationSubdirName, u1.network, false, u1.mirrorBatId(), crypto).get();\n\n        Path dirPath = PathUtil.get(u1.username, subdirName);\n        shareFunction.apply(u1, userContexts, dirPath);\n\n        FileWrapper theDir = u1.getByPath(dirPath).get().get();\n        Path parentPath = PathUtil.get(u1.username);\n        FileWrapper theParent = u1.getByPath(parentPath).get().get();\n\n        //move directory\n        Path destSubdirPath = PathUtil.get(u1.username, destinationSubdirName);\n        FileWrapper destSubdir = u1.getByPath(destSubdirPath).get().get();\n\n        theDir.moveTo(destSubdir, theParent, dirPath, u1, () -> Futures.of(true)).join();\n\n        //old copy sharedWith entries should be removed\n        FileSharedWithState shared = u1.sharedWith(dirPath).join();\n        Assert.assertTrue(\"original directory not shared\", ! isShared.test(shared));\n\n        dirPath = PathUtil.get(u1.username, destinationSubdirName, subdirName);\n\n        //new copy sharedWith entry should not be empty\n        FileSharedWithState newShare = u1.sharedWith(dirPath).join();\n        Assert.assertTrue(\"new directory shared\", isShared.test(newShare));\n    }\n\n    @Test\n    public void cleanRenamedFilesReadAccess() throws Exception {\n        String username = random();\n        String password = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n\n        List<String> shareePasswords = IntStream.range(0, userCount)\n                .mapToObj(i -> PeergosNetworkUtils.generatePassword())\n                .collect(Collectors.toList());\n        // make u1 friend others\n        List<UserContext> friends = getUserContexts(userCount, shareePasswords);\n\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), friends);\n\n        // upload a file to u1's space\n        FileWrapper u1Root = u1.getUserRoot().get();\n        String filename = \"somefile.txt\";\n        File f = File.createTempFile(\"peergos\", \"\");\n        byte[] originalFileContents = \"Hello Peergos friend!\".getBytes();\n        Files.write(f.toPath(), originalFileContents);\n        ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(f);\n        FileWrapper uploaded = u1Root.uploadOrReplaceFile(filename, resetableFileInputStream, f.length(),\n                u1.network, u1.crypto, () -> false, l -> {}).get();\n\n        // share the file from \"a\" to each of the others\n        String originalPath = u1.username + \"/\" + filename;\n        FileWrapper u1File = u1.getByPath(originalPath).get().get();\n        u1.shareReadAccessWith(PathUtil.get(u1.username, filename), friends.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n\n        // check other users can read the file\n        for (UserContext friend : friends) {\n            Optional<FileWrapper> sharedFile = friend.getByPath(u1.username + \"/\" + filename).get();\n            Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n\n            AsyncReader inputStream = sharedFile.get().getInputStream(friend.network,\n                    friend.crypto, l -> {}).get();\n\n            byte[] fileContents = Serialize.readFully(inputStream, sharedFile.get().getFileProperties().size).get();\n            Assert.assertTrue(\"shared file contents correct\", Arrays.equals(originalFileContents, fileContents));\n        }\n\n        UserContext userToUnshareWith = friends.stream().findFirst().get();\n        String friendsPathToFile = u1.username + \"/\" + filename;\n        Optional<FileWrapper> priorUnsharedView = userToUnshareWith.getByPath(friendsPathToFile).get();\n        AbsoluteCapability priorPointer = priorUnsharedView.get().getPointer().capability;\n        CommittedWriterData cwd = network.synchronizer.getValue(priorPointer.owner, priorPointer.writer).join().get(priorPointer.writer);\n        CryptreeNode priorFileAccess = network.getMetadata(cwd, priorPointer).get().get();\n        SymmetricKey priorMetaKey = priorFileAccess.getParentKey(priorPointer.rBaseKey);\n\n        // unshare with a single user\n        u1.unShareReadAccess(PathUtil.get(u1.username, filename), userToUnshareWith.username).join();\n\n        String newname = \"newname.txt\";\n        FileWrapper updatedParent = u1.getByPath(originalPath).get().get()\n                .rename(newname, u1.getUserRoot().get(), PathUtil.get(originalPath), u1).get();\n        Path newPath = PathUtil.get(u1.username, newname);\n        AbsoluteCapability newCap = u1.getByPath(newPath).join().get().getPointer().capability;\n\n        // check still logged in user can't read the new name\n        Optional<FileWrapper> unsharedView = userToUnshareWith.getByPath(friendsPathToFile).get();\n        String friendsNewPathToFile = u1.username + \"/\" + newname;\n        Optional<FileWrapper> unsharedView2 = userToUnshareWith.getByPath(friendsNewPathToFile).get();\n        CommittedWriterData cwd2 = network.synchronizer.getValue(priorPointer.owner, priorPointer.writer).join().get(priorPointer.writer);\n        CryptreeNode fileAccess = network.getMetadata(cwd2, priorPointer.withMapKey(newCap.getMapKey(), newCap.bat)).get().get();\n        // check we are trying to decrypt the correct thing\n        PaddedCipherText priorPropsCipherText = ((CborObject.CborMap) priorFileAccess.toCbor()).getObject(\"p\", PaddedCipherText::fromCbor);\n        CborObject.CborMap priorFromParent = priorPropsCipherText.decrypt(priorMetaKey, x -> (CborObject.CborMap)x);\n        FileProperties priorProps = FileProperties.fromCbor(priorFromParent.get(\"s\"));\n        try {\n            // Try decrypting the new metadata with the old key\n            PaddedCipherText propsCipherText = ((CborObject.CborMap) fileAccess.toCbor()).getObject(\"p\", PaddedCipherText::fromCbor);\n            CborObject.CborMap fromParent = propsCipherText.decrypt(priorMetaKey, x -> (CborObject.CborMap)x);\n            FileProperties props = FileProperties.fromCbor(fromParent.get(\"s\"));\n            throw new IllegalStateException(\"We shouldn't be able to decrypt this after a rename! new name = \" + props.name);\n        } catch (InvalidCipherTextException e) {}\n        try {\n            FileProperties freshProperties = fileAccess.getProperties(priorPointer.rBaseKey);\n            throw new IllegalStateException(\"We shouldn't be able to decrypt this after a rename!\");\n        } catch (InvalidCipherTextException e) {}\n\n        Assert.assertTrue(\"target can't read through original path\", ! unsharedView.isPresent());\n        Assert.assertTrue(\"target can't read through new path\", ! unsharedView2.isPresent());\n\n        List<UserContext> updatedUserContexts = friends.stream()\n                .map(e -> {\n                    try {\n                        return ensureSignedUp(e.username, shareePasswords.get(friends.indexOf(e)), network.clear(), crypto);\n                    } catch (Exception ex) {\n                        throw new IllegalStateException(ex.getMessage(), ex);\n                    }\n                })\n                .collect(Collectors.toList());\n\n        List<UserContext> remainingUsers = updatedUserContexts.stream()\n                .skip(1)\n                .collect(Collectors.toList());\n\n        UserContext u1New = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n\n        // check remaining users can still read it\n        for (UserContext userContext : remainingUsers) {\n            String path = u1.username + \"/\" + newname;\n            Optional<FileWrapper> sharedFile = userContext.getByPath(path).get();\n            Assert.assertTrue(\"path '\"+ path +\"' is still available\", sharedFile.isPresent());\n        }\n\n        // test that u1 can still access the original file\n        Optional<FileWrapper> fileWithNewBaseKey = u1New.getByPath(u1.username + \"/\" + newname).get();\n        Assert.assertTrue(fileWithNewBaseKey.isPresent());\n\n        // Now modify the file\n        byte[] suffix = \"Some new data at the end\".getBytes();\n        AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);\n        FileWrapper parent = u1New.getByPath(u1New.username).get().get();\n        parent.uploadFileSection(newname, suffixStream, false, originalFileContents.length,\n                originalFileContents.length + suffix.length, Optional.empty(), true,\n                u1New.network, crypto, () -> false, l -> {}, null, Optional.empty(), null, null).get();\n        AsyncReader extendedContents = u1New.getByPath(u1.username + \"/\" + newname).get().get()\n                .getInputStream(u1New.network, crypto, l -> {}).get();\n        byte[] newFileContents = Serialize.readFully(extendedContents, originalFileContents.length + suffix.length).get();\n\n        Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(originalFileContents, suffix)));\n    }\n\n    private String random() {\n        return ArrayOps.bytesToHex(crypto.random.randomBytes(15));\n    }\n\n    @Test\n    public void shareFolderForWriteAccess() throws Exception {\n        PeergosNetworkUtils.shareFolderForWriteAccess(network, network, 2, random);\n    }\n\n    @Test\n    public void friendDeletesAccount() {\n        String username1 = random();\n        String password1 = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username1, password1, network, crypto);\n        String username2 = random();\n        String password2 = random();\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(u1), Arrays.asList(u2));\n        // Add file bigger than the 1MiB final quota\n        u1.getUserRoot().join().uploadOrReplaceFile(\"afile.bin\", AsyncReader.build(new byte[2*1024*1024]),\n                2*1024*1024, u1.network, crypto, () -> false, x -> {}).join();\n        u1.deleteAccount(password1, UserTests::noMfa).join();\n\n        // Check u2 can still log in\n        UserContext u2Refresh = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n    }\n\n    @Test\n    public void acceptAndReciprocateFollowRequest() throws Exception {\n        String username1 = random();\n        String password1 = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username1, password1, network, crypto);\n        String username2 = random();\n        String password2 = random();\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).get();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", u1Requests.size() > 0);\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, true).get();\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().get();\n        Optional<FileWrapper> u1ToU2 = u2.getByPath(\"/\" + u1.username).get();\n        assertTrue(\"Friend root present after accepted follow request\", u1ToU2.isPresent());\n\n        Optional<FileWrapper> u2ToU1 = u1.getByPath(\"/\" + u2.username).get();\n        assertTrue(\"Friend root present after accepted follow request\", u2ToU1.isPresent());\n\n        Set<FileWrapper> children = u2ToU1.get().getChildren(crypto.hasher, u2.network).get();\n\n        assertTrue(\"Browse to friend root\", children.size() == 1);\n\n        SocialState u1Social = PeergosNetworkUtils.ensureSignedUp(username1, password1, network.clear(), crypto)\n                .getSocialState().get();\n\n        Set<String> u1Following = u1Social.followingRoots.stream().map(f -> f.getName())\n                .collect(Collectors.toSet());\n        assertTrue(\"Following correct\", u1Following.contains(u2.username));\n        assertTrue(\"Followers correct\", u1Social.followerRoots.containsKey(username2));\n\n        SocialState u2Social = PeergosNetworkUtils.ensureSignedUp(username2, password2, network.clear(), crypto)\n                .getSocialState().get();\n\n        Set<String> u2Following = u2Social\n                .followingRoots.stream().map(f -> f.getName())\n                .collect(Collectors.toSet());\n        assertTrue(\"Following correct\", u2Following.contains(u1.username));\n        assertTrue(\"Followers correct\", u2Social.followerRoots.containsKey(username1));\n    }\n\n    @Test\n    public void verifyFriend() {\n        String username1 = random();\n        String password1 = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username1, password1, network, crypto);\n        String username2 = random();\n        String password2 = random();\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).join();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().join();\n        assertTrue(\"Receive a follow request\", u1Requests.size() > 0);\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, true).join();\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().join();\n\n        // verify a friend and persist the result\n        Pair<List<PublicKeyHash>, FingerPrint> u2FingerPrint = u2.generateFingerPrint(username1).join();\n        Pair<List<PublicKeyHash>, FingerPrint> u1FingerPrint = u1.generateFingerPrint(username2).join();\n\n        Assert.assertTrue(\"Verify fingerprint\", u1FingerPrint.right.matches(u2FingerPrint.right));\n\n        u1.addFriendAnnotation(new FriendAnnotation(username2, true, u1FingerPrint.left)).join();\n\n        SocialState u1Social = PeergosNetworkUtils.ensureSignedUp(username1, password1, network.clear(), crypto)\n                .getSocialState().join();\n        FriendAnnotation annotation = u1Social.friendAnnotations.get(username2);\n        Assert.assertTrue(\"Annotation persisted\", annotation != null && annotation.isVerified());\n    }\n\n    @Test\n    public void acceptButNotReciprocateFollowRequest() throws Exception {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).get();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", u1Requests.size() > 0);\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, false).get();\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().get();\n        Optional<FileWrapper> u1Tou2 = u2.getByPath(\"/\" + u1.username).get();\n        Optional<FileWrapper> u2Tou1 = u1.getByPath(\"/\" + u2.username).get();\n\n        assertTrue(\"Friend root present after accepted follow request\", u1Tou2.isPresent());\n        assertTrue(\"Friend root not present after non reciprocated follow request\", !u2Tou1.isPresent());\n\n        Set<String> followers = u1.getFollowerNames().join();\n        assertTrue(followers.contains(u2.username));\n\n        // Now test them trying to become full friends after u1 unfollowing u2\n        u1.unfollow(u2.username).join();\n        u1.sendInitialFollowRequest(u2.username).join();\n        List<FollowRequestWithCipherText> reqs = u2.processFollowRequests().get();\n        u2.sendReplyFollowRequest(reqs.get(0), true, true).join();\n        SocialState u1Social = u1.getSocialState().join();\n        Assert.assertTrue(u1Social.followerRoots.containsKey(u2.username));\n        Assert.assertTrue(u1Social.followingRoots.stream().anyMatch(f -> f.getName().equals(u2.username)));\n\n        SocialState u2Social = u2.getSocialState().join();\n        Assert.assertTrue(u2Social.followerRoots.containsKey(u1.username));\n        Assert.assertTrue(u2Social.followingRoots.stream().anyMatch(f -> f.getName().equals(u1.username)));\n    }\n\n    @Test\n    public void acceptThenCompleteFollowRequest() throws Exception {\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        UserContext b = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        b.sendFollowRequest(a.username, SymmetricKey.random()).get();\n        assertTrue(! b.getSocialState().join().getFollowing().contains(a.username));\n        assertTrue(! b.getSocialState().join().getFollowers().contains(a.username));\n\n        List<FollowRequestWithCipherText> aRequests = a.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", aRequests.size() > 0);\n        a.sendReplyFollowRequest(aRequests.get(0), true, false).get();\n        List<FollowRequestWithCipherText> bFollowRequests = b.processFollowRequests().get();\n        Optional<FileWrapper> aTob = b.getByPath(\"/\" + a.username).get();\n        Optional<FileWrapper> bToa = a.getByPath(\"/\" + b.username).get();\n\n        assertTrue(\"Friend root present after accepted follow request\", aTob.isPresent());\n        assertTrue(\"Friend root not present after non reciprocated follow request\", bToa.isEmpty());\n        assertTrue(a.getSocialState().join().getFollowers().contains(b.username));\n        assertTrue(b.getSocialState().join().getFollowing().contains(a.username));\n\n        // Now test them trying to become full friends\n        a.sendInitialFollowRequest(b.username).join();\n        List<FollowRequestWithCipherText> reqs = b.processFollowRequests().get();\n        b.sendReplyFollowRequest(reqs.get(0), true, true).join();\n        SocialState aSocial = a.getSocialState().join();\n        Assert.assertTrue(aSocial.followerRoots.containsKey(b.username));\n        Assert.assertTrue(aSocial.followingRoots.stream().anyMatch(f -> f.getName().equals(b.username)));\n\n        SocialState bSocial = b.getSocialState().join();\n        Assert.assertTrue(bSocial.followerRoots.containsKey(a.username));\n        Assert.assertTrue(bSocial.followingRoots.stream().anyMatch(f -> f.getName().equals(a.username)));\n\n        assertTrue(b.getByPath(\"/\" + a.username).join().isPresent());\n        assertTrue(a.getByPath(\"/\" + b.username).join().isPresent());\n    }\n\n    @Test\n    public void denyThenSubsequentFollowRequest() throws Exception {\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + random(), random(), network, crypto);\n        UserContext b = PeergosNetworkUtils.ensureSignedUp(\"b-\" + random(), random(), network, crypto);\n        UserContext c = PeergosNetworkUtils.ensureSignedUp(\"c-\" + random(), random(), network, crypto);\n        b.sendFollowRequest(a.username, SymmetricKey.random()).get();\n        assertTrue(! b.getSocialState().join().getFollowing().contains(a.username));\n        assertTrue(! b.getSocialState().join().getFollowers().contains(a.username));\n\n        List<FollowRequestWithCipherText> aRequests = a.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", aRequests.size() > 0);\n        a.sendReplyFollowRequest(aRequests.get(0), false, false).get();\n        List<FollowRequestWithCipherText> bFollowRequests = b.processFollowRequests().get();\n        assertTrue(bFollowRequests.isEmpty());\n        //b.sendFollowRequest(a.username, SymmetricKey.random()).get();\n\n        b.sendFollowRequest(c.username, SymmetricKey.random()).get();\n        SocialState bState = b.getSocialState().join();\n        assertTrue(bState.pendingIncoming.size() == 0);\n        aRequests = b.processFollowRequests().get();\n        bState = b.getSocialState().join();\n        assertTrue(bState.pendingIncoming.size() == 0);\n    }\n\n    @Test\n    public void acceptThenSubsequentFollowRequest() throws Exception {\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + random(), random(), network, crypto);\n        UserContext b = PeergosNetworkUtils.ensureSignedUp(\"b-\" + random(), random(), network, crypto);\n        b.sendFollowRequest(a.username, SymmetricKey.random()).get();\n        assertTrue(! b.getSocialState().join().getFollowing().contains(a.username));\n        assertTrue(! b.getSocialState().join().getFollowers().contains(a.username));\n\n        List<FollowRequestWithCipherText> aRequests = a.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", aRequests.size() > 0);\n        a.sendReplyFollowRequest(aRequests.get(0), true, false).get();\n        List<FollowRequestWithCipherText> bFollowRequests = b.processFollowRequests().join();\n        assertTrue(bFollowRequests.isEmpty());\n        List<FollowRequestWithCipherText> aFollowRequests = a.processFollowRequests().join();\n\n        b.unfollow(a.username).join();\n        b.sendFollowRequest(a.username, SymmetricKey.random()).get();\n        SocialState aState = a.getSocialState().join();\n\n        Set<String> followerNames = aState.followerRoots.keySet();\n        Set<String> followeeNames = aState.followingRoots.stream().map(f -> f.getFileProperties().name).collect(Collectors.toSet());\n        Set<String> friendNames = followerNames.stream().filter(x -> followeeNames.contains(x)).collect(Collectors.toSet());\n        assertTrue(friendNames.isEmpty());\n    }\n\n    @Test\n    public void friendshipRestoration() throws Exception {\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + random(), \"password\", network, crypto);\n        UserContext b = PeergosNetworkUtils.ensureSignedUp(\"b-\" + random(), \"password\", network, crypto);\n        b.sendFollowRequest(a.username, SymmetricKey.random()).get();\n        List<FollowRequestWithCipherText> aRequests = a.processFollowRequests().get();\n        a.sendReplyFollowRequest(aRequests.get(0), true, true).get();\n        b.processFollowRequests().join();\n        a.processFollowRequests().join();\n        assertTrue(a.getSocialState().join().getFriends().contains(b.username));\n        assertTrue(b.getSocialState().join().getFriends().contains(a.username));\n\n        a.unfollow(b.username).join();\n        SocialState aState = a.getSocialState().join();\n        assertTrue(aState.getFriends().isEmpty());\n        assertTrue(aState.getFollowing().isEmpty());\n        assertTrue(aState.getFollowers().contains(b.username));\n\n        // unfollow is local only - b still thinks they are friends\n        assertTrue(b.getSocialState().join().getFriends().contains(a.username));\n\n        a.unblock(b.username).join();\n        assertTrue(a.getSocialState().join().getFriends().contains(b.username));\n\n        // now remove as follower, then reunite\n        a.removeFollower(b.username).join();\n        aState = a.getSocialState().join();\n        assertTrue(aState.getFriends().isEmpty());\n        assertTrue(aState.getFollowing().contains(b.username));\n        assertTrue(aState.getFollowers().isEmpty());\n\n        SocialState bState = b.getSocialState().join();\n        assertTrue(bState.getFriends().isEmpty());\n        assertTrue(bState.getFollowing().isEmpty());\n        assertTrue(bState.getFollowers().contains(a.username));\n\n        b.sendInitialFollowRequest(a.username).join();\n        aRequests = a.processFollowRequests().get();\n        a.sendReplyFollowRequest(aRequests.get(0), true, true).get();\n        b.processFollowRequests().join();\n        assertTrue(a.getSocialState().join().getFriends().contains(b.username));\n        assertTrue(b.getSocialState().join().getFriends().contains(a.username));\n    }\n\n    @Test\n    public void concurrentMutualFollowRequests() throws Exception {\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        UserContext b = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        b.sendFollowRequest(a.username, SymmetricKey.random()).get();\n        a.sendFollowRequest(b.username, SymmetricKey.random()).get();\n\n        // they should be friends now\n        List<FollowRequestWithCipherText> bFollowRequests = b.processFollowRequests().get();\n        List<FollowRequestWithCipherText> aFollowRequests = a.processFollowRequests().get();\n        Optional<FileWrapper> aTob = b.getByPath(\"/\" + a.username).get();\n        Optional<FileWrapper> bToa = a.getByPath(\"/\" + b.username).get();\n\n        assertTrue(\"Friend root present after accepted follow request\", aTob.isPresent());\n        assertTrue(\"Friend root present after accepted follow request\", bToa.isPresent());\n        assertTrue(a.getSocialState().join().getFriends().contains(b.username));\n        assertTrue(b.getSocialState().join().getFriends().contains(a.username));\n    }\n\n    @Test\n    public void rejectFollowRequest() throws Exception {\n        String username1 = random();\n        String password1 = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username1, password1, network, crypto);\n        String username2 = random();\n        String password2 = random();\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random());\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", u1Requests.size() > 0);\n        u1.sendReplyFollowRequest(u1Requests.get(0), false, false);\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().get();\n        Optional<FileWrapper> u1Tou2 = u2.getByPath(\"/\" + u1.username).get();\n        assertTrue(\"Friend root not present after rejected follow request\", ! u1Tou2.isPresent());\n\n        Optional<FileWrapper> u2Tou1 = u1.getByPath(\"/\" + u2.username).get();\n        assertTrue(\"Friend root not present after non reciprocated follow request\", !u2Tou1.isPresent());\n    }\n\n    @Test\n    public void acceptAndReciprocateFollowRequestThenRemoveFollowRequest() throws Exception {\n        String username1 = random();\n        String password1 = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username1, password1, network, crypto);\n        String username2 = random();\n        String password2 = random();\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).get();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", u1Requests.size() > 0);\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, true).get();\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().get();\n        Optional<FileWrapper> u1ToU2 = u2.getByPath(\"/\" + u1.username).get();\n        assertTrue(\"Friend root present after accepted follow request\", u1ToU2.isPresent());\n\n        Optional<FileWrapper> u2ToU1 = u1.getByPath(\"/\" + u2.username).get();\n        assertTrue(\"Friend root present after accepted follow request\", u2ToU1.isPresent());\n\n        Set<String> u1Following = PeergosNetworkUtils.ensureSignedUp(username1, password1, network.clear(), crypto).getSocialState().get()\n                .followingRoots.stream().map(f -> f.getName())\n                .collect(Collectors.toSet());\n        assertTrue(\"Following correct\", u1Following.contains(u2.username));\n\n        Set<String> u2Following = PeergosNetworkUtils.ensureSignedUp(username2, password2, network.clear(), crypto).getSocialState().get()\n                .followingRoots.stream().map(f -> f.getName())\n                .collect(Collectors.toSet());\n        assertTrue(\"Following correct\", u2Following.contains(u1.username));\n\n        UserContext q = u1;\n        UserContext w = u2;\n\n        q.removeFollower(username2).get();\n\n        Optional<FileWrapper> u2ToU1Again = q.getByPath(\"/\" + u2.username).get();\n        assertTrue(\"Friend root present after unfollow request\", u2ToU1Again.isPresent());\n\n        w = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n\n        Optional<FileWrapper> u1ToU2Again = w.getByPath(\"/\" + u1.username).get();\n        assertTrue(\"Friend root NOT present after unfollow\", !u1ToU2Again.isPresent());\n    }\n\n    @Test\n    public void reciprocateButNotAcceptFollowRequest() throws Exception {\n        String username1 = random();\n        String password1 = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username1, password1, network, crypto);\n        String username2 = random();\n        String password2 = random();\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).join();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().get();\n        assertTrue(\"Receive a follow request\", u1Requests.size() > 0);\n        u1.sendReplyFollowRequest(u1Requests.get(0), false, true);\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().get();\n        Optional<FileWrapper> u1Tou2 = u2.getByPath(\"/\" + u1.username).get();\n        assertTrue(\"Friend root not present after rejected follow request\", ! u1Tou2.isPresent());\n\n        Optional<FileWrapper> u2Tou1 = u1.getByPath(\"/\" + u2.username).get();\n        assertTrue(\"Friend root present after reciprocated follow request\", u2Tou1.isPresent());\n\n        // Now test them trying to become full friends after u2 removes u1 as a follower\n        u2.removeFollower(u1.username).join();\n        Assert.assertTrue(u2.getSocialState().join().pendingIncoming.isEmpty());\n        u2.sendInitialFollowRequest(u1.username).join();\n        List<FollowRequestWithCipherText> reqs = u1.processFollowRequests().get();\n        u1.sendReplyFollowRequest(reqs.get(0), true, true).join();\n\n        SocialState u2Social = u2.getSocialState().join();\n        Assert.assertTrue(u2Social.followerRoots.containsKey(u1.username));\n        Assert.assertTrue(u2Social.followingRoots.stream().anyMatch(f -> f.getName().equals(u1.username)));\n\n        SocialState u1Social = u1.getSocialState().join();\n        Assert.assertTrue(u1Social.followerRoots.containsKey(u2.username));\n        Assert.assertTrue(u1Social.followingRoots.stream().anyMatch(f -> f.getName().equals(u2.username)));\n    }\n\n    @Test\n    public void unfollow() {\n        String pw = \"pw\";\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), pw, network, crypto);\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(random(), pw, network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).join();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().join();\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, true).join();\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().join();\n\n        Set<String> u1Following = u1.getFollowing().join();\n        Assert.assertTrue(\"u1 following u2\", u1Following.contains(u2.username));\n\n        u1.unfollow(u2.username).join();\n\n        Set<String> newU1Following = u1.getFollowing().join();\n        Assert.assertTrue(\"u1 no longer following u2\", !newU1Following.contains(u2.username));\n\n        Optional<FileWrapper> u2Tou1 = u1.getByPath(\"/\" + u2.username).join();\n        assertTrue(\"u1 can no longer see u2's root\", u2Tou1.isEmpty());\n\n        Optional<FileWrapper> u1Tou2 = u2.getByPath(\"/\" + u1.username).join();\n        assertTrue(\"u2 can still see u1's root\", u1Tou2.isPresent());\n\n        // now re-follow to become friends again\n        u1.sendInitialFollowRequest(u2.username).join();\n        u1.unblock(u2.username).join();\n        u2.processFollowRequests().join();\n\n        Optional<FileWrapper> u1Tou2again = u2.getByPath(\"/\" + u1.username).join();\n        assertTrue(\"u2 can still see u1's root\", u1Tou2again.isPresent());\n\n        Optional<FileWrapper> u2Tou1again = u1.getByPath(\"/\" + u2.username).join();\n        assertTrue(\"u1 can see u2's root again\", u2Tou1again.isPresent());\n    }\n\n    @Test\n    public void removeFollower() {\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(random(), random(), network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).join();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().join();\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, true).join();\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().join();\n\n        Set<String> u1Followers = u1.getFollowerNames().join();\n        Assert.assertTrue(\"u1 following u2\", u1Followers.contains(u2.username));\n\n        u1.removeFollower(u2.username).join();\n\n        Set<String> newU1Followers = u1.getFollowerNames().join();\n        Assert.assertTrue(\"u1 no longer has u2 as follower\", !newU1Followers.contains(u2.username));\n\n        Set<String> u2Following = u2.getFollowing().join();\n        Assert.assertTrue(\"u2 is no longer following u1\", !u2Following.contains(u1.username));\n\n        Optional<FileWrapper> u2Tou1 = u1.getByPath(\"/\" + u2.username).join();\n        assertTrue(\"u1 can still see u2's root\", u2Tou1.isPresent());\n\n        Optional<FileWrapper> u1Tou2 = u2.getByPath(\"/\" + u1.username).join();\n        assertTrue(\"u2 can no longer see u1's root\", u1Tou2.isEmpty());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MultibaseTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotEquals;\n\npublic class MultibaseTests {\n\n    @Test\n    public void base58Test() {\n        List<String> examples = Arrays.asList(\n                \"zQmdM1TrjBJnYzzESATtrrMNPAtjJdqfcV2vF1kM39DY7cc\",\n                \"zQmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB\",\n                \"zQmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy\",\n                \"z11\");\n        for (String example: examples) {\n            byte[] output = Multibase.decode(example);\n            String encoded = Multibase.encode(Multibase.Base.Base58BTC, output);\n            assertEquals(example, encoded);\n        }\n    }\n\n    @Test\n    public void zeroBytesBase58() {\n        for (int i=0; i < 32; i++) {\n            String encoded = Multibase.encode(Multibase.Base.Base58BTC, new byte[i]);\n            byte[] output = Multibase.decode(encoded);\n            if (! Arrays.equals(output, new byte[i]))\n                throw new IllegalStateException(\"Failed to round trip zero array of length \" + i);\n        }\n    }\n\n    @Test\n    public void base16Test() {\n        List<String> examples = Arrays.asList(\"f234abed8debede\",\n                \"f87ad873defc2b288\",\n                \"f\",\n                \"f01\",\n                \"f0123456789abcdef\");\n        for (String example: examples) {\n            byte[] output = Multibase.decode(example);\n            String encoded = Multibase.encode(Multibase.Base.Base16, output);\n            assertEquals(example, encoded);\n        }\n    }\n\n    @Test\n    public void base32Test() {\n        List<String> examples = Arrays.asList(\"G'day mate!\", \"How's it going?\");\n        for (String example: examples) {\n            String encoded = Multibase.encode(Multibase.Base.Base32, example.getBytes());\n            byte[] output = Multibase.decode(encoded);\n            assertArrayEquals(example.getBytes(), output);\n        }\n\n        List<Pair<String, byte[]>> fullExamples = Arrays.asList(\n                new Pair<>(\"baaaaaaaa\", ArrayOps.hexToBytes(\"0000000000\")),\n                new Pair<>(\"bjv2wy5djmjqxgzjanfzsaylxmvzw63lfeeqfy3zp\", ArrayOps.hexToBytes(\"4D756C74696261736520697320617765736F6D6521205C6F2F\")),\n                new Pair<>(\"birswgzloorzgc3djpjssazlwmvzhs5dinfxgoijbee\", ArrayOps.hexToBytes(\"446563656e7472616c697a652065766572797468696e67212121\")),\n                new Pair<>(\"bafyreif3n3yb2jkftteahuegjtpeej6nfn3zszpldxzuvpvoyiwcb6sc5i\", ArrayOps.hexToBytes(\"01711220bb6ef01d25459cc803d0864cde4227cd2b779965eb1df34abeaec22c20fa42ea\"))\n        );\n        for (Pair<String, byte[]> fullExample : fullExamples) {\n            byte[] output = Multibase.decode(fullExample.left);\n            assertArrayEquals(output, fullExample.right);\n            String encoded = Multibase.encode(Multibase.Base.Base32, fullExample.right);\n            assertEquals(fullExample.left, encoded);\n        }\n    }\n\n    @Test\n    public void invalidBase16Test() {\n        String example = \"f012\"; // hex string of odd length\n        byte[] output = Multibase.decode(example);\n        String encoded = Multibase.encode(Multibase.Base.Base16, output);\n        assertNotEquals(example, encoded);\n\n    }\n\n    @Test (expected = NumberFormatException.class)\n    public void invalidWithExceptionBase16Test() {\n        String example = \"f0g\"; // g char is not allowed in hex\n        Multibase.decode(example);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/MultipartTests.java",
    "content": "package peergos.server.tests;\n\nimport com.sun.net.httpserver.*;\nimport org.junit.*;\nimport peergos.server.net.*;\nimport peergos.shared.io.ipfs.api.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.net.http.HttpClient;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class MultipartTests {\n\n    private final int port;\n    private final HttpServer server;\n    private final Queue<List<byte[]>> received = new LinkedBlockingQueue<>();\n    private final Random r = new Random(1);\n\n    public MultipartTests() throws IOException {\n        this.port = 5679;\n        InetSocketAddress localhost = new InetSocketAddress(\"localhost\", port);\n        this.server = HttpServer.create(localhost, 10);\n        server.createContext(\"/multipart\", this::handle);\n        server.setExecutor(Executors.newFixedThreadPool(1));\n        server.start();\n    }\n\n    @After\n    public void finish() {\n        server.stop(0);\n    }\n\n    public void handle(HttpExchange httpExchange) throws IOException {\n        try {\n            String boundary = httpExchange.getRequestHeaders().get(\"Content-Type\")\n                    .stream()\n                    .filter(s -> s.contains(\"boundary=\"))\n                    .map(s -> s.substring(s.indexOf(\"=\") + 1))\n                    .findAny()\n                    .get();\n            List<byte[]> data = MultipartReceiver.extractFiles(httpExchange.getRequestBody(), boundary);\n            received.add(data);\n            httpExchange.sendResponseHeaders(200, 0);\n            DataOutputStream dout = new DataOutputStream(httpExchange.getResponseBody());\n            dout.write(\"true\".getBytes());\n            dout.flush();\n            dout.close();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    private byte[] randomArray(int len) {\n        byte[] res = new byte[len];\n        r.nextBytes(res);\n        return res;\n    }\n\n    @Test\n    public void random() throws IOException {\n        for (int power = 5; power < 20; power++) {\n            int base =  (int) Math.pow(2, power);\n            int length = base + r.nextInt(base);\n            try {\n                test(IntStream.range(0, 60)\n                        .mapToObj(i -> randomArray(length))\n                        .collect(Collectors.toList()));\n            } catch (AssertionError e) {\n                System.err.println(\"Failed on power: \" + power + \" and length: \" + length);\n                throw e;\n            }\n        }\n    }\n\n    private void test(List<byte[]> input) throws IOException {\n        HttpClient client = HttpClient.newBuilder()\n                .connectTimeout(Duration.ofMillis(1_000))\n                .build();\n        Multipart sender = new Multipart(client, \"http://localhost:\" + port + \"/multipart\", \"UTF-8\", Collections.emptyMap(), 0);\n        for (byte[] in : input)\n            sender.addFilePart(\"file\", new NamedStreamable.ByteArrayWrapper(in));\n\n        String res = new String(sender.finish().join());\n\n        List<byte[]> result = received.poll();\n\n        boolean sameLength = result.size() == input.size();\n\n        Assert.assertTrue(\"Same length on other end\", sameLength);\n\n        List<Integer> differences = IntStream.range(0, input.size())\n                .filter(i -> !Arrays.equals(input.get(i), result.get(i)))\n                .mapToObj(Integer::valueOf)\n                .collect(Collectors.toList());\n        Assert.assertTrue(\"Same result on other end: \" + differences, differences.size() == 0);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/P2pStreamNetworkTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.BeforeClass;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.tests.util.TestPorts;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.io.ipfs.MultiAddress;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.fs.*;\n\nimport java.net.URL;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\nimport static peergos.server.Main.IPFS;\nimport static peergos.server.tests.UserTests.randomString;\n\npublic class P2pStreamNetworkTests {\n    private static Args args = UserTests\n            .buildArgs()\n            .with(\"useIPFS\", \"true\")\n            .with(\"async-bootstrap\", \"true\")\n            .removeArg(IpfsWrapper.IPFS_BOOTSTRAP_NODES); // no bootstrapping\n\n    private static Random random = new Random(0);\n    private static List<NetworkAccess> nodes = new ArrayList<>();\n\n    private final Crypto crypto = Main.initCrypto();\n\n    @BeforeClass\n    public static void init() throws Exception {\n        // start pki node\n        Main.PKI_INIT.main(args);\n        NetworkAccess toPki = buildApi(args);\n        Multihash pkiNodeId = toPki.dhtClient.id().get();\n        nodes.add(toPki);\n        int bootstrapSwarmPort = args.getInt(\"ipfs-swarm-port\");\n\n        // other nodes\n        int ipfsApiPort = TestPorts.getPort();\n        int ipfsGatewayPort = TestPorts.getPort();\n        int ipfsSwarmPort = TestPorts.getPort();\n        int allowPort = TestPorts.getPort();\n        Args normalNode = UserTests.buildArgs()\n                .with(\"ipfs-api-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsApiPort)\n                .with(\"ipfs-gateway-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsGatewayPort)\n                .with(\"allow-target\", \"/ip4/127.0.0.1/tcp/\" + allowPort)\n                .with(\"ipfs-swarm-port\", \"\" + ipfsSwarmPort)\n                .with(IpfsWrapper.IPFS_BOOTSTRAP_NODES, \"\" + Main.getLocalBootstrapAddress(bootstrapSwarmPort, pkiNodeId))\n                .with(\"proxy-target\", new MultiAddress(args.getArg(\"ipfs-gateway-address\")).toString())\n                .with(\"ipfs-api-address\", Main.getLocalMultiAddress(ipfsApiPort).toString());\n\n        IPFS.main(normalNode);\n\n//        IPFS node2 = new IPFS(Main.getLocalMultiAddress(ipfsApiPort));\n//        node2.swarm.connect(Main.getLocalBootstrapAddress(bootstrapSwarmPort, pkiNodeId).toString());\n\n        nodes.add(buildProxiedApi(ipfsApiPort, ipfsGatewayPort, pkiNodeId));\n    }\n\n    private static NetworkAccess buildApi(Args args) throws Exception {\n        return Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    private static NetworkAccess buildProxiedApi(int ipfsApiPort, int ipfsGatewayPort, Multihash pkinodeId) throws Exception {\n        return Builder.buildJavaGatewayAccess(new URL(\"http://localhost:\" + ipfsApiPort), new URL(\"http://localhost:\" + ipfsGatewayPort), pkinodeId.toString()).get();\n    }\n\n    @Test\n    public void writeViaUnrelatedNode() throws Exception {\n        String username1 = generateUsername();\n        String password1 = randomString();\n        UserContext u1 = ensureSignedUp(username1, password1, nodes.get(0), crypto);\n\n        byte[] data = \"G'day mate!\".getBytes();\n        String filename = \"hey.txt\";\n        FileWrapper root = u1.getUserRoot().get();\n        FileWrapper upload = root.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                nodes.get(1), crypto, () -> false, x -> {}).get();\n        Thread.sleep(7000);\n        Optional<FileWrapper> file = ensureSignedUp(username1, password1, nodes.get(0), crypto)\n                .getByPath(\"/\" + username1 + \"/\" + filename).orTimeout(10, TimeUnit.SECONDS).join();\n        Assert.assertTrue(file.isPresent());\n    }\n\n    private String generateUsername() {\n        return \"test\" + Math.abs(random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/PasswordProtection.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.password.*;\n\nimport java.util.*;\n\npublic class PasswordProtection {\n\n    private static Crypto crypto = Main.initCrypto();\n\n    @Test\n    public void invertible() {\n        byte[] secret = \"Some secret data\".getBytes();\n\n        String password = \"notagoodpassword\";\n        Cborable cbor = PasswordProtected.encryptWithPassword(secret, password, crypto.hasher, crypto.symmetricProvider, crypto.random);\n\n        byte[] retrieved = PasswordProtected.decryptWithPassword(cbor, password, crypto.hasher, crypto.symmetricProvider, crypto.random);\n        Assert.assertTrue(\"invertible\", Arrays.equals(retrieved, secret));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/PeergosNetworkUtils.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport peergos.server.*;\nimport peergos.server.apps.email.*;\nimport peergos.server.storage.ResetableFileInputStream;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\nimport peergos.shared.display.*;\nimport peergos.shared.email.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.messaging.*;\nimport peergos.shared.messaging.messages.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.BufferedStorage;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.FileWrapper;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\n\npublic class PeergosNetworkUtils {\n\n    public static String generateUsername(Random random) {\n        return \"username-\" + Math.abs(random.nextInt() % 1_000_000_000);\n    }\n\n    public static String generatePassword() {\n        return ArrayOps.bytesToHex(crypto.random.randomBytes(32));\n    }\n\n    public static final Crypto crypto = Main.initCrypto();\n    public static final Hasher hasher = crypto.hasher;\n\n    public static String randomString() {\n        return UUID.randomUUID().toString();\n    }\n\n    public static byte[] randomData(Random random, int length) {\n        byte[] data = new byte[length];\n        random.nextBytes(data);\n        return data;\n    }\n\n    public static String randomUsername(String prefix, Random rnd) {\n        byte[] suffix = new byte[(30 - prefix.length()) / 2];\n        rnd.nextBytes(suffix);\n        return prefix + ArrayOps.bytesToHex(suffix);\n    }\n\n    public static void checkFileContents(byte[] expected, FileWrapper f, UserContext context) {\n        long size = f.getFileProperties().size;\n        byte[] retrievedData = Serialize.readFully(f.getInputStream(context.network, context.crypto,\n                size, l -> {}).join(), f.getSize()).join();\n        assertEquals(expected.length, size);\n        assertTrue(\"Correct contents\", Arrays.equals(retrievedData, expected));\n    }\n\n    public static List<UserContext> getUserContextsForNode(NetworkAccess network, Random random, int size, List<String> passwords) {\n        return IntStream.range(0, size)\n                .mapToObj(e -> {\n                    String username = generateUsername(random);\n                    String password = passwords.get(e);\n                    try {\n                        return ensureSignedUp(username, password, network.clear(), crypto);\n                    } catch (Exception ioe) {\n                        throw new IllegalStateException(ioe);\n                    }\n                }).collect(Collectors.toList());\n    }\n\n    public static void copyDirFromFriend(NetworkAccess network, Random random) {\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharerUser = ensureSignedUp(sharerUsername, sharerPassword, network, crypto);\n\n        //sign up some users on shareeNode\n        String shareeUsername = generateUsername(random);\n        String shareePassword = generatePassword();\n        UserContext shareeUser = ensureSignedUp(shareeUsername, shareePassword, network, crypto);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharerUser), Arrays.asList(shareeUser));\n\n        // upload a file to \"a\"'s space\n        FileWrapper u1Root = sharerUser.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, network, false, u1Root.mirrorBatId(), crypto).join();\n        byte[] data = \"Some text\".getBytes();\n        String filename = \"Afile.txt\";\n        sharerUser.getByPath(PathUtil.get(sharerUsername, folderName)).join().get()\n                .uploadOrReplaceFile(filename, AsyncReader.build(data), data.length, sharerUser.network, crypto,\n                        () -> false, x -> {}).join();\n        String subdirName = \"subdir\";\n        sharerUser.getByPath(PathUtil.get(sharerUsername, folderName)).join().get()\n                .mkdir(subdirName, sharerUser.network, false, sharerUser.mirrorBatId(), crypto).join();\n\n        // share\n        Set<String> shareeNames = new HashSet();\n        shareeNames.add(shareeUser.username);\n        sharerUser.shareReadAccessWith(PathUtil.get(sharerUser.username, folderName), shareeNames).join();\n\n        Optional<FileWrapper> sharedFile = shareeUser.getByPath(sharerUser.username + \"/\" + folderName).join();\n        Assert.assertTrue(\"shared folder present\", sharedFile.isPresent());\n        Assert.assertTrue(\"Folder is read only\", !sharedFile.get().isWritable());\n\n        Optional<FileWrapper> destFolder = shareeUser.getByPath(shareeUser.username).join();\n        sharedFile.get().copyTo(destFolder.get(), shareeUser).join();\n        //Assert.assertTrue(\"Folder not copied\", res);\n        Optional<FileWrapper> foundFolder = shareeUser.getByPath(shareeUser.username + \"/\" + folderName).join();\n        Assert.assertTrue(\"Folder accessible\", foundFolder.isPresent());\n\n        Set<FileWrapper> receivedChildren = foundFolder.get().getChildren(crypto.hasher, shareeUser.network).join();\n        Assert.assertTrue(receivedChildren.stream().map(FileWrapper::getName).collect(Collectors.toSet()).equals(Set.of(filename, subdirName)));\n    }\n\n    public static void copyDirToFriend(NetworkAccess network, Random random) {\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharerUser = ensureSignedUp(sharerUsername, sharerPassword, network, crypto);\n\n        //sign up some users on shareeNode\n        String shareeUsername = generateUsername(random);\n        String shareePassword = generatePassword();\n        UserContext shareeUser = ensureSignedUp(shareeUsername, shareePassword, network, crypto);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharerUser), Arrays.asList(shareeUser));\n\n        // upload a file to \"a\"'s space\n        FileWrapper u1Root = sharerUser.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, network, false, u1Root.mirrorBatId(), crypto).join();\n\n        // share folder\n        Set<String> shareeNames = new HashSet();\n        shareeNames.add(shareeUser.username);\n        Path sharedPath = PathUtil.get(sharerUser.username, folderName);\n        sharerUser.shareWriteAccessWith(sharedPath, shareeNames).join();\n\n        Optional<FileWrapper> sharedFolder = shareeUser.getByPath(sharerUser.username + \"/\" + folderName).join();\n        Assert.assertTrue(\"shared folder present\", sharedFolder.isPresent());\n        Assert.assertTrue(\"Folder is writable only\", sharedFolder.get().isWritable());\n\n        // sharee uploads a folder to the shared dir\n        Optional<FileWrapper> destFolder = shareeUser.getByPath(sharedPath).join();\n        byte[] fileData = new byte[20*1024*1024];\n        List<FileWrapper.FileUploadProperties> files = List.of(new FileWrapper.FileUploadProperties(\"afile.txt\",\n                () -> AsyncReader.build(fileData), 0, fileData.length, Optional.empty(), Optional.empty(), false, false, x -> {}));\n        String subdirName = \"subdir\";\n        Stream<FileWrapper.FolderUploadProperties> upload = Stream.of(\n                new FileWrapper.FolderUploadProperties(List.of(subdirName), files),\n                new FileWrapper.FolderUploadProperties(List.of(subdirName, \"nested\"), files)\n        );\n        destFolder.get().uploadSubtree(upload,\n                Optional.empty(), network, crypto, shareeUser.getTransactionService(), x -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n\n        // check sharer can see folder\n        Optional<FileWrapper> foundFolder = sharerUser.getByPath(sharedPath.resolve(subdirName)).join();\n        Assert.assertTrue(\"Folder accessible\", foundFolder.isPresent());\n    }\n\n    public static void copySubdirFromFriend(NetworkAccess network, Random random) {\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharerUser = ensureSignedUp(sharerUsername, sharerPassword, network, crypto);\n\n        //sign up some users on shareeNode\n        String shareeUsername = generateUsername(random);\n        String shareePassword = generatePassword();\n        UserContext shareeUser = ensureSignedUp(shareeUsername, shareePassword, network, crypto);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharerUser), Arrays.asList(shareeUser));\n\n        // upload a file to /a/folder/subdir/file.txt\n        FileWrapper u1Root = sharerUser.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, network, false, u1Root.mirrorBatId(), crypto).join();\n        String subdirName = \"subdir\";\n        sharerUser.getByPath(PathUtil.get(sharerUsername, folderName)).join().get()\n                .mkdir(subdirName, sharerUser.network, false, sharerUser.mirrorBatId(), crypto).join();\n        byte[] data = \"Some text\".getBytes();\n        String filename = \"file.txt\";\n        sharerUser.getByPath(PathUtil.get(sharerUsername, folderName, subdirName)).join().get()\n                .uploadOrReplaceFile(filename, AsyncReader.build(data), data.length, sharerUser.network, crypto,\n                        () -> false, x -> {}).join();\n\n        // share\n        Set<String> shareeNames = new HashSet<>();\n        shareeNames.add(shareeUser.username);\n        sharerUser.shareReadAccessWith(PathUtil.get(sharerUser.username, folderName), shareeNames).join();\n\n        Path subFolder = PathUtil.get(sharerUser.username, folderName, subdirName);\n        Optional<FileWrapper> sharedFile = shareeUser.getByPath(sharerUser.username + \"/\" + folderName + \"/\").join().get()\n                .getChildren(crypto.hasher, sharerUser.network).join().stream().findAny();\n        Assert.assertTrue(\"shared subfolder present\", sharedFile.isPresent());\n        Assert.assertTrue(\"Folder is read only\", !sharedFile.get().isWritable());\n\n        Optional<FileWrapper> destFolder = shareeUser.getByPath(shareeUser.username).join();\n        sharedFile.get().copyTo(destFolder.get(), shareeUser).join();\n        Optional<FileWrapper> foundFolder = shareeUser.getByPath(shareeUser.username + \"/\" + subdirName).join();\n        Assert.assertTrue(\"Folder accessible\", foundFolder.isPresent());\n\n        Set<FileWrapper> receivedChildren = foundFolder.get().getChildren(crypto.hasher, shareeUser.network).join();\n        Assert.assertTrue(receivedChildren.stream().map(FileWrapper::getName).collect(Collectors.toSet()).equals(Set.of(filename)));\n    }\n\n    public static void grantAndRevokeFileReadAccess(NetworkAccess sharerNode,\n                                                    NetworkAccess shareeNode,\n                                                    int shareeCount,\n                                                    Random random,\n                                                    Runnable updatePkis) throws Exception {\n        Assert.assertTrue(0 < shareeCount);\n        //sign up a user on sharerNode\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharerUser = ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        //sign up some users on shareeNode\n        List<String> shareePasswords = IntStream.range(0, shareeCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(shareeNode, random, shareeCount, shareePasswords);\n\n        // friend sharer with others\n        updatePkis.run();\n        friendBetweenGroups(Arrays.asList(sharerUser), shareeUsers);\n\n        // upload a file to \"a\"'s space\n        FileWrapper u1Root = sharerUser.getUserRoot().join();\n        String filename = \"somefile.txt\";\n        File f = File.createTempFile(\"peergos\", \"\");\n        byte[] originalFileContents = new byte[10*1024*1024];\n        random.nextBytes(originalFileContents);\n        Files.write(f.toPath(), originalFileContents);\n        ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(f);\n        FileWrapper uploaded = u1Root.uploadOrReplaceFile(filename, resetableFileInputStream, f.length(),\n                sharerUser.network, crypto, () -> false, l -> {}).join();\n        Optional<Bat> originalBat = uploaded.writableFilePointer().bat;\n\n        // create a secret link to the file\n        String userLinkPassword = \"forbob\";\n        LinkProperties link = sharerUser.createSecretLink(Paths.get(sharerUser.username, filename).toString(), false, Optional.empty(), Optional.empty(), userLinkPassword, false).join();\n\n        // share the file from sharer to each of the sharees\n        Set<String> shareeNames = shareeUsers.stream()\n                .map(u -> u.username)\n                .collect(Collectors.toSet());\n        sharerUser.shareReadAccessWith(PathUtil.get(sharerUser.username, filename), shareeNames).join();\n\n        // check other users can read the file\n        for (UserContext userContext : shareeUsers) {\n            Optional<FileWrapper> sharedFile = userContext.getByPath(sharerUser.username + \"/\" + filename).join();\n            Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n            Assert.assertTrue(\"File is read only\", ! sharedFile.get().isWritable());\n            checkFileContents(originalFileContents, sharedFile.get(), userContext);\n        }\n\n        // check secret link works\n        UserContext.fromSecretLinkV2(link.toLinkString(uploaded.owner()), () -> Futures.of(userLinkPassword), shareeNode, crypto).join();\n\n        // check other users can browse to the friend's root\n        for (UserContext userContext : shareeUsers) {\n            Optional<FileWrapper> friendRoot = userContext.getByPath(sharerUser.username).join();\n            assertTrue(\"friend root present\", friendRoot.isPresent());\n            Set<FileWrapper> children = friendRoot.get().getChildren(crypto.hasher, userContext.network).join();\n            Optional<FileWrapper> sharedFile = children.stream()\n                    .filter(file -> file.getName().equals(filename))\n                    .findAny();\n            assertTrue(\"Shared file present via root.getChildren()\", sharedFile.isPresent());\n        }\n\n        UserContext userToUnshareWith = shareeUsers.stream().findFirst().get();\n\n        // unshare with a single user\n        sharerUser.unShareReadAccess(PathUtil.get(sharerUser.username, filename), userToUnshareWith.username).join();\n\n        List<UserContext> updatedShareeUsers = shareeUsers.stream()\n                .map(e -> {\n                    try {\n                        return ensureSignedUp(e.username, shareePasswords.get(shareeUsers.indexOf(e)), shareeNode.clear(), crypto);\n                    } catch (Exception ex) {\n                        throw new IllegalStateException(ex.getMessage(), ex);\n\n                    }\n                }).collect(Collectors.toList());\n\n        // check secret link works\n        UserContext.fromSecretLinkV2(link.toLinkString(uploaded.owner()), () -> Futures.of(userLinkPassword), shareeNode, crypto).join();\n\n        //test that the other user cannot access it from scratch\n        Optional<FileWrapper> otherUserView = updatedShareeUsers.get(0).getByPath(sharerUser.username + \"/\" + filename).join();\n        Assert.assertTrue(!otherUserView.isPresent());\n\n        List<UserContext> remainingUsers = updatedShareeUsers.stream()\n                .skip(1)\n                .collect(Collectors.toList());\n\n        UserContext updatedSharerUser = ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        // check remaining users can still read it\n        for (UserContext userContext : remainingUsers) {\n            String path = sharerUser.username + \"/\" + filename;\n            Optional<FileWrapper> sharedFile = userContext.getByPath(path).join();\n            Assert.assertTrue(\"path '\" + path + \"' is still available\", sharedFile.isPresent());\n            checkFileContents(originalFileContents, sharedFile.get(), userContext);\n            Optional<Bat> newBat = sharedFile.get().readOnlyPointer().bat;\n            Assert.assertTrue(! newBat.equals(originalBat));\n        }\n\n        // test that u1 can still access the original file\n        Optional<FileWrapper> fileWithNewBaseKey = updatedSharerUser.getByPath(sharerUser.username + \"/\" + filename).join();\n        Assert.assertTrue(fileWithNewBaseKey.isPresent());\n\n        // Now modify the file\n        byte[] suffix = \"Some new data at the end\".getBytes();\n        AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);\n        FileWrapper parent = updatedSharerUser.getByPath(updatedSharerUser.username).join().get();\n        parent.uploadFileSection(filename, suffixStream, false, originalFileContents.length, originalFileContents.length + suffix.length,\n                Optional.empty(), true, updatedSharerUser.network, crypto, () -> false, l -> {},\n                null, Optional.empty(), null, parent.mirrorBatId()).join();\n        AsyncReader extendedContents = updatedSharerUser.getByPath(sharerUser.username + \"/\" + filename).join().get()\n                .getInputStream(updatedSharerUser.network, crypto, l -> {}).join();\n        byte[] newFileContents = Serialize.readFully(extendedContents, originalFileContents.length + suffix.length).join();\n\n        Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(originalFileContents, suffix)));\n    }\n\n    public static void socialFeedCommentOnSharedFile(NetworkAccess sharerNode, NetworkAccess shareeNode, Random random) throws Exception {\n        //sign up a user on sharerNode\n        String sharerUsername = randomUsername(\"sharer-\", random);\n        String sharerPassword = generatePassword();\n        UserContext sharer = ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        //sign up some users on shareeNode\n        int shareeCount = 1;\n        List<String> shareePasswords = IntStream.range(0, shareeCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(shareeNode, random, shareeCount, shareePasswords);\n        UserContext sharee = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // upload a file to \"a\"'s space\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String filename = \"somefile.txt\";\n        File f = File.createTempFile(\"peergos\", \"\");\n        byte[] originalFileContents = new byte[10*1024*1024];\n        random.nextBytes(originalFileContents);\n        Files.write(f.toPath(), originalFileContents);\n        ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(f);\n        FileWrapper uploaded = u1Root.uploadOrReplaceFile(filename, resetableFileInputStream, f.length(),\n                sharer.network, crypto, () -> false, l -> {}).join();\n\n        // share the file from sharer to each of the sharees\n        Set<String> shareeNames = shareeUsers.stream()\n                .map(u -> u.username)\n                .collect(Collectors.toSet());\n        sharer.shareReadAccessWith(PathUtil.get(sharer.username, filename), shareeNames).join();\n\n        SocialFeed receiverFeed = sharee.getSocialFeed().join().update().join();\n        List<Pair<SharedItem, FileWrapper>> files = receiverFeed.getSharedFiles(0, 100).join();\n        assertTrue(files.size() == 3);\n        FileWrapper sharedFile = files.get(files.size() -1).right;\n        SharedItem sharedItem = files.get(files.size() -1).left;\n\n        Multihash hash = sharedFile.getContentHash(sharee.network, sharee.crypto).join();\n        String replyText = \"reply\";\n        SocialPost.Resharing resharingType = SocialPost.Resharing.Friends;\n        FileRef parent = new FileRef(sharedItem.path, sharedItem.cap, hash);\n        SocialPost replySocialPost = SocialPost.createComment(parent, resharingType, sharee.username,\n                Arrays.asList(new Text(replyText)));\n        Pair<Path, FileWrapper> result = receiverFeed.createNewPost(replySocialPost).join();\n        String friendGroup = SocialState.FRIENDS_GROUP_NAME;\n        String receiverGroupUid = sharee.getSocialState().join().groupNameToUid.get(friendGroup);\n        sharee.shareReadAccessWith(result.left, Set.of(receiverGroupUid)).join();\n\n        //now sharer should see the reply\n        SocialFeed feed = sharer.getSocialFeed().join().update().join();\n        files = feed.getSharedFiles(0, 100).join();\n        //assertTrue(files.size() == 5);\n\n    }\n\n    public static void socialFeedCASExceptionOnUpdate(NetworkAccess sharerNode, NetworkAccess shareeNode, Random random) {\n        //sign up a user on sharerNode\n        String sharerUsername = randomUsername(\"sharer-\", random);\n        String sharerPassword = generatePassword();\n        UserContext sharer = ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        //sign up some users on shareeNode\n        int shareeCount = 1;\n        List<String> shareePasswords = IntStream.range(0, shareeCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(shareeNode, random, shareeCount, shareePasswords);\n        UserContext sharee = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        SocialFeed senderFeed = sharer.getSocialFeed().join().update().join();\n        List<peergos.shared.display.Text> body = new ArrayList<>();\n        body.add(new peergos.shared.display.Text(\"msg!\"));\n        SocialPost socialPost = peergos.shared.social.SocialPost.createInitialPost(sharerUsername, body, SocialPost.Resharing.Friends);\n\n        Pair<Path, FileWrapper> result = senderFeed.createNewPost(socialPost).join();\n        Set<String> readers = Set.of(sharee.username);\n        sharer.shareReadAccessWith(result.left, readers).join();\n\n        int startIndex = senderFeed.getLastSeenIndex();\n        SocialFeed updatedSenderFeed = senderFeed.update().join();\n\n        int requestSize = 100;\n        List<SharedItem> items = updatedSenderFeed.getShared(startIndex, startIndex + requestSize, sharer.crypto, sharer.network).join();\n\n        int newIndex = startIndex + items.size();\n        updatedSenderFeed.setLastSeenIndex(newIndex).join();\n    }\n    public static void grantAndRevokeFileWriteAccess(NetworkAccess sharerNode, NetworkAccess shareeNode, int shareeCount, Random random) throws Exception {\n        Assert.assertTrue(0 < shareeCount);\n        //sign up a user on sharerNode\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharerUser = ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        //sign up some users on shareeNode\n        List<String> shareePasswords = IntStream.range(0, shareeCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(shareeNode, random, shareeCount, shareePasswords);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharerUser), shareeUsers);\n\n        // upload a file to \"a\"'s space\n        FileWrapper u1Root = sharerUser.getUserRoot().join();\n        String filename = \"somefile.txt\";\n        byte[] originalFileContents = new byte[10*1024*1024];\n        random.nextBytes(originalFileContents);\n        AsyncReader resetableFileInputStream = AsyncReader.build(originalFileContents);\n        FileWrapper uploaded = u1Root.uploadOrReplaceFile(filename, resetableFileInputStream, originalFileContents.length,\n                sharerUser.network, crypto, () -> false, l -> {}).join();\n\n        // share the file from sharer to each of the sharees\n        String filePath = sharerUser.username + \"/\" + filename;\n        FileWrapper u1File = sharerUser.getByPath(filePath).join().get();\n        byte[] originalStreamSecret = u1File.getFileProperties().streamSecret.get();\n        sharerUser.shareWriteAccessWith(PathUtil.get(sharerUser.username, filename), shareeUsers.stream().map(u -> u.username).collect(Collectors.toSet())).join();\n\n        // check other users can read the file\n        for (UserContext userContext : shareeUsers) {\n            Optional<FileWrapper> sharedFile = userContext.getByPath(filePath).join();\n            Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n            Assert.assertTrue(\"File is writable\", sharedFile.get().isWritable());\n            checkFileContents(originalFileContents, sharedFile.get(), userContext);\n            // check the other user can't rename the file\n            FileWrapper parent = userContext.getByPath(sharerUser.username).join().get();\n            CompletableFuture<FileWrapper> rename = sharedFile.get()\n                    .rename(\"Somenew name.dat\", parent, PathUtil.get(filePath), userContext);\n            assertTrue(\"Cannot rename\", rename.isCompletedExceptionally());\n        }\n\n        // check other users can browser to the friend's root\n        for (UserContext userContext : shareeUsers) {\n            Optional<FileWrapper> friendRoot = userContext.getByPath(sharerUser.username).join();\n            assertTrue(\"friend root present\", friendRoot.isPresent());\n            Set<FileWrapper> children = friendRoot.get().getChildren(crypto.hasher, userContext.network).join();\n            Optional<FileWrapper> sharedFile = children.stream()\n                    .filter(file -> file.getName().equals(filename))\n                    .findAny();\n            assertTrue(\"Shared file present via root.getChildren()\", sharedFile.isPresent());\n        }\n        MultiUserTests.checkUserValidity(sharerNode, sharerUsername);\n\n        UserContext userToUnshareWith = shareeUsers.stream().findFirst().get();\n\n        // unshare with a single user\n        sharerUser.unShareWriteAccess(PathUtil.get(sharerUser.username, filename), userToUnshareWith.username).join();\n\n        List<UserContext> updatedShareeUsers = shareeUsers.stream()\n                .map(e -> ensureSignedUp(e.username, shareePasswords.get(shareeUsers.indexOf(e)), shareeNode, crypto))\n                .collect(Collectors.toList());\n\n        //test that the other user cannot access it from scratch\n        Optional<FileWrapper> otherUserView = updatedShareeUsers.get(0).getByPath(filePath).join();\n        Assert.assertTrue(!otherUserView.isPresent());\n\n        List<UserContext> remainingUsers = updatedShareeUsers.stream()\n                .skip(1)\n                .collect(Collectors.toList());\n\n        UserContext updatedSharerUser = ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        FileWrapper theFile = updatedSharerUser.getByPath(filePath).join().get();\n        byte[] newStreamSecret = theFile.getFileProperties().streamSecret.get();\n        boolean sameStreams = Arrays.equals(originalStreamSecret, newStreamSecret);\n        Assert.assertTrue(\"Stream secret should change on revocation\", ! sameStreams);\n\n        String retrievedPath = theFile.getPath(sharerNode).join();\n        Assert.assertTrue(\"File has correct path\", retrievedPath.equals(\"/\" + filePath));\n\n        // check remaining users can still read it\n        for (UserContext userContext : remainingUsers) {\n            String path = filePath;\n            Optional<FileWrapper> sharedFile = userContext.getByPath(path).join();\n            Assert.assertTrue(\"path '\" + path + \"' is still available\", sharedFile.isPresent());\n            checkFileContents(originalFileContents, sharedFile.get(), userContext);\n        }\n\n        // test that u1 can still access the original file\n        Optional<FileWrapper> fileWithNewBaseKey = updatedSharerUser.getByPath(filePath).join();\n        Assert.assertTrue(fileWithNewBaseKey.isPresent());\n\n        // Now modify the file from the sharer\n        byte[] suffix = \"Some new data at the end\".getBytes();\n        AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);\n        FileWrapper parent = updatedSharerUser.getByPath(updatedSharerUser.username).join().get();\n        parent.uploadFileSection(filename, suffixStream, false, originalFileContents.length, originalFileContents.length + suffix.length,\n                Optional.empty(), true, updatedSharerUser.network, crypto, () -> false, l -> {},\n                null, Optional.empty(), null, parent.mirrorBatId()).join();\n        AsyncReader extendedContents = updatedSharerUser.getByPath(filePath).join().get().getInputStream(updatedSharerUser.network,\n                updatedSharerUser.crypto, l -> {}).join();\n        byte[] newFileContents = Serialize.readFully(extendedContents, originalFileContents.length + suffix.length).join();\n\n        Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(originalFileContents, suffix)));\n\n        // Now modify the file from the sharee\n        byte[] suffix2 = \"Some more data\".getBytes();\n        AsyncReader suffixStream2 = new AsyncReader.ArrayBacked(suffix2);\n        UserContext sharee = remainingUsers.get(0);\n        FileWrapper parent2 = sharee.getByPath(updatedSharerUser.username).join().get();\n        parent2.uploadFileSection(filename, suffixStream2, false,\n                originalFileContents.length + suffix.length,\n                originalFileContents.length + suffix.length + suffix2.length,\n                Optional.empty(), true, shareeNode, crypto, () -> false, l -> {},\n                null, Optional.empty(), null, parent.mirrorBatId()).join();\n        AsyncReader extendedContents2 = sharee.getByPath(filePath).join().get()\n                .getInputStream(updatedSharerUser.network,\n                updatedSharerUser.crypto, l -> {}).join();\n        byte[] newFileContents2 = Serialize.readFully(extendedContents2,\n                originalFileContents.length + suffix.length + suffix2.length).join();\n\n        byte[] expected = ArrayOps.concat(ArrayOps.concat(originalFileContents, suffix), suffix2);\n        equalArrays(newFileContents2, expected);\n        MultiUserTests.checkUserValidity(sharerNode, sharerUsername);\n    }\n\n    public static void equalArrays(byte[] a, byte[] b) {\n        if (a.length != b.length)\n            throw new IllegalStateException(\"Different length arrays!\");\n        for (int i=0; i < a.length; i++)\n            if (a[i] != b[i])\n                throw new IllegalStateException(\"Different at index \" + i);\n    }\n\n    public static void shareFileWithDifferentSigner(NetworkAccess sharerNode,\n                                                    NetworkAccess shareeNode,\n                                                    Random random) {\n        // sign up the sharer\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharer = ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        // sign up the sharee\n        String shareeUsername = generateUsername(random);\n        String shareePassword = generatePassword();\n        UserContext sharee = ensureSignedUp(shareeUsername, shareePassword, shareeNode.clear(), crypto);\n\n        // friend users\n        friendBetweenGroups(Arrays.asList(sharer), Arrays.asList(sharee));\n\n        // make directory /sharer/dir and grant write access to it to a friend\n        String dirName = \"dir\";\n        sharer.getUserRoot().join().mkdir(dirName, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n        Path dirPath = PathUtil.get(sharerUsername, dirName);\n        sharer.shareWriteAccessWith(dirPath, Collections.singleton(sharee.username)).join();\n\n        // no revoke write access to dir\n        sharer.unShareWriteAccess(dirPath, sharee.username).join();\n\n        // check sharee can't read the dir\n        Optional<FileWrapper> sharedDir = sharee.getByPath(dirPath).join();\n        Assert.assertTrue(\"unshared dir not present\", ! sharedDir.isPresent());\n\n        // upload a file to the dir\n        FileWrapper dir = sharer.getByPath(dirPath).join().get();\n        String filename = \"somefile.txt\";\n        byte[] originalFileContents = new byte[10*1024*1024];\n        random.nextBytes(originalFileContents);\n        AsyncReader resetableFileInputStream = AsyncReader.build(originalFileContents);\n        FileWrapper uploaded = dir.uploadOrReplaceFile(filename, resetableFileInputStream, originalFileContents.length,\n                sharer.network, crypto, () -> false, l -> {}).join();\n\n        // share the file read only with the sharee\n        Path filePath = dirPath.resolve(filename);\n        FileWrapper u1File = sharer.getByPath(filePath).join().get();\n        sharer.shareWriteAccessWith(filePath, Collections.singleton(sharee.username)).join();\n\n        // check other user can read the file directly\n        Optional<FileWrapper> sharedFile = sharee.getByPath(filePath).join();\n        Assert.assertTrue(\"shared file present\", sharedFile.isPresent());\n        checkFileContents(originalFileContents, sharedFile.get(), sharee);\n        // check other user can read the file via its parent\n        Optional<FileWrapper> sharedDirViaFile = sharee.getByPath(dirPath.toString()).join();\n        Set<FileWrapper> children = sharedDirViaFile.get().getChildren(crypto.hasher, sharee.network).join();\n        Assert.assertTrue(\"shared file present via parent\", children.size() == 1);\n\n        FileWrapper friend = sharee.getByPath(PathUtil.get(sharer.username)).join().get();\n        Set<FileWrapper> friendChildren = friend.getChildren(crypto.hasher, sharee.network).join();\n        Assert.assertEquals(friendChildren.size(), 2);\n    }\n\n    public static void sharedwithPermutations(NetworkAccess sharerNode, Random rnd) throws Exception {\n        String sharerUsername = randomUsername(\"sharer-\", rnd);\n        String password = \"terriblepassword\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, password, sharerNode, crypto);\n\n        String shareeUsername = randomUsername(\"sharee-\", rnd);\n        UserContext sharee = PeergosNetworkUtils.ensureSignedUp(shareeUsername, password, sharerNode, crypto);\n\n        String shareeUsername2 = randomUsername(\"sharee2-\", rnd);\n        UserContext sharee2 = PeergosNetworkUtils.ensureSignedUp(shareeUsername2, password, sharerNode, crypto);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), Arrays.asList(sharee));\n        friendBetweenGroups(Arrays.asList(sharer), Arrays.asList(sharee2));\n\n        // friends are now connected\n        // share a file from u1 to the others\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"afolder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path p = PathUtil.get(sharerUsername, folderName);\n\n        FileSharedWithState result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 0 && result.writeAccess.size() == 0);\n\n        sharer.shareReadAccessWith(p, Collections.singleton(sharee.username)).join();\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 1);\n\n        sharer.shareWriteAccessWith(p, Collections.singleton(sharee2.username)).join();\n\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 1 && result.writeAccess.size() == 1);\n\n        sharer.unShareReadAccess(p, sharee.username).join();\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 0 && result.writeAccess.size() == 1);\n\n        sharer.unShareWriteAccess(p, sharee2.username).join();\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 0 && result.writeAccess.size() == 0);\n\n        // now try again, but after adding read, write sharees, remove the write sharee\n        sharer.shareReadAccessWith(p, Collections.singleton(sharee.username)).join();\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 1);\n\n        sharer.shareWriteAccessWith(p, Collections.singleton(sharee2.username)).join();\n\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 1 && result.writeAccess.size() == 1);\n\n        sharer.unShareWriteAccess(p, sharee2.username).join();\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 1 && result.writeAccess.size() == 0);\n\n    }\n\n\n    public static void sharedWriteableAndTruncate(NetworkAccess sharerNode, Random rnd) throws Exception {\n\n        String sharerUsername = randomUsername(\"sharer\", rnd);\n        String sharerPassword = \"sharer1\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, sharerPassword, sharerNode, crypto);\n\n        String shareeUsername = randomUsername(\"sharee\", rnd);\n        String shareePassword = \"sharee1\";\n        UserContext sharee = PeergosNetworkUtils.ensureSignedUp(shareeUsername, shareePassword, sharerNode, crypto);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), Arrays.asList(sharee));\n\n        // friends are now connected\n        // share a file from u1 to the others\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String dirName = \"afolder\";\n        u1Root.mkdir(dirName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n\n        Path dirPath = PathUtil.get(sharerUsername, dirName);\n        FileWrapper dir = sharer.getByPath(dirPath).join().get();\n        String filename = \"somefile.txt\";\n        byte[] originalFileContents = sharer.crypto.random.randomBytes(409);\n        AsyncReader resetableFileInputStream = AsyncReader.build(originalFileContents);\n        FileWrapper uploaded = dir.uploadOrReplaceFile(filename, resetableFileInputStream, originalFileContents.length,\n                sharer.network, crypto, () -> false, l -> {}).join();\n\n        Path filePath = PathUtil.get(sharerUsername, dirName, filename);\n        FileWrapper file = sharer.getByPath(filePath).join().get();\n        long originalfileSize = file.getFileProperties().size;\n        System.out.println(\"filesize=\" + originalfileSize);\n\n        sharer.shareWriteAccessWith(filePath, Collections.singleton(sharee.username)).join();\n\n        dir = sharer.getByPath(dirPath).join().get();\n        byte[] updatedFileContents = sharer.crypto.random.randomBytes(255);\n        resetableFileInputStream = AsyncReader.build(updatedFileContents);\n\n        uploaded = dir.uploadOrReplaceFile(filename, resetableFileInputStream, updatedFileContents.length,\n                sharer.network, crypto, () -> false, l -> {}).join();\n        file = sharer.getByPath(filePath).join().get();\n        long newFileSize = file.getFileProperties().size;\n        System.out.println(\"filesize=\" + newFileSize);\n        Assert.assertTrue(newFileSize == 255);\n\n        //sharee now attempts to modify file\n        FileWrapper sharedFile = sharee.getByPath(filePath).join().get();\n        byte[] modifiedFileContents = sharer.crypto.random.randomBytes(255);\n        sharedFile.overwriteFileJS(AsyncReader.build(modifiedFileContents), 0, modifiedFileContents.length,\n                sharee.network, sharee.crypto, len -> {}).join();\n        FileWrapper sharedFileUpdated = sharee.getByPath(filePath).join().get();\n        checkFileContents(modifiedFileContents, sharedFileUpdated, sharee);\n    }\n\n    public static void renameSharedwithFolder(NetworkAccess sharerNode, Random rnd) throws Exception {\n        String sharerUsername = randomUsername(\"sharer-\", rnd);\n        String password = \"terriblepassword\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, password, sharerNode, crypto);\n\n        String shareeUsername = randomUsername(\"sharee-\", rnd);\n        UserContext sharee = PeergosNetworkUtils.ensureSignedUp(shareeUsername, password, sharerNode, crypto);\n\n        friendBetweenGroups(Arrays.asList(sharer), Arrays.asList(sharee));\n\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"afolder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path p = PathUtil.get(sharerUsername, folderName);\n\n        sharer.shareReadAccessWith(p, Set.of(shareeUsername)).join();\n        FileSharedWithState result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 1);\n\n        u1Root = sharer.getUserRoot().join();\n        FileWrapper file = sharer.getByPath(p).join().get();\n        String renamedFolderName= \"renamed\";\n        file.rename(renamedFolderName, u1Root, p, sharer).join();\n        p = PathUtil.get(sharerUsername, renamedFolderName);\n\n        sharer.unShareReadAccess(p, sharee.username).join();\n        result = sharer.sharedWith(p).join();\n        Assert.assertTrue(result.readAccess.size() == 0 && result.writeAccess.size() == 0);\n\n    }\n\n    public static void grantAndRevokeDirReadAccess(NetworkAccess sharerNode,\n                                                   NetworkAccess shareeNode,\n                                                   int shareeCount,\n                                                   Random random,\n                                                   Runnable updatePkis) throws Exception {\n        Assert.assertTrue(0 < shareeCount);\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, sharerPassword, sharerNode, crypto);\n\n        List<String> shareePasswords = IntStream.range(0, shareeCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(shareeNode, random, shareeCount, shareePasswords);\n\n        // friend sharer with others\n        updatePkis.run();\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a file from u1 to the others\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"afolder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        String path = PathUtil.get(sharerUsername, folderName).toString();\n        System.out.println(\"PATH \"+ path);\n        FileWrapper folder = sharer.getByPath(path).join().get();\n        String filename = \"somefile.txt\";\n        byte[] originalFileContents = \"Hello Peergos friend!\".getBytes();\n        AsyncReader resetableFileInputStream = new AsyncReader.ArrayBacked(originalFileContents);\n        FileWrapper updatedFolder = folder.uploadOrReplaceFile(filename, resetableFileInputStream,\n                originalFileContents.length, sharer.network, crypto, () -> false, l -> {}).join();\n        String originalFilePath = sharer.username + \"/\" + folderName + \"/\" + filename;\n\n        for (int i=0; i< 20; i++) {\n            sharer.getByPath(path).join().get()\n                    .mkdir(\"subdir\"+i, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n        }\n\n        Set<String> childNames = sharer.getByPath(path).join().get().getChildren(crypto.hasher, sharer.network).join()\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n\n        // file is uploaded, do the actual sharing\n        sharer.shareReadAccessWith(PathUtil.get(path),\n                shareeUsers.stream()\n                        .map(c -> c.username)\n                        .collect(Collectors.toSet())).join();\n\n        // check each user can see the shared folder and directory\n        for (UserContext sharee : shareeUsers) {\n            // test retrieval via getChildren() which is used by the web-ui\n            Set<FileWrapper> children = sharee.getByPath(sharer.username).join().get()\n                    .getChildren(crypto.hasher, sharee.network).join();\n            Assert.assertTrue(children.stream()\n                    .filter(f -> f.getName().equals(folderName))\n                    .findAny()\n                    .isPresent());\n\n            FileWrapper sharedFolder = sharee.getByPath(sharer.username + \"/\" + folderName).join().orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n            Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n            FileWrapper sharedFile = sharee.getByPath(sharer.username + \"/\" + folderName + \"/\" + filename).join().get();\n            checkFileContents(originalFileContents, sharedFile, sharee);\n        }\n\n        UserContext updatedSharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        List<UserContext> updatedSharees = shareeUsers.stream()\n                .map(e -> {\n                    try {\n                        return ensureSignedUp(e.username, shareePasswords.get(shareeUsers.indexOf(e)), e.network.clear(), crypto);\n                    } catch (Exception ex) {\n                        throw new IllegalStateException(ex.getMessage(), ex);\n                    }\n                }).collect(Collectors.toList());\n\n\n        for (int i = 0; i < updatedSharees.size(); i++) {\n            UserContext user = updatedSharees.get(i);\n            updatedSharer.unShareReadAccess(PathUtil.get(updatedSharer.username, folderName), user.username).join();\n            Thread.sleep(7_000); // make sure old pointers aren't cached\n\n            Optional<FileWrapper> updatedSharedFolder = user.getByPath(updatedSharer.username + \"/\" + folderName).join();\n\n            // test that u1 can still access the original file, and user cannot\n            Optional<FileWrapper> fileWithNewBaseKey = updatedSharer.getByPath(updatedSharer.username + \"/\" + folderName + \"/\" + filename).join();\n            Assert.assertTrue(!updatedSharedFolder.isPresent());\n            Assert.assertTrue(fileWithNewBaseKey.isPresent());\n\n            // Now modify the file\n            byte[] suffix = \"Some new data at the end\".getBytes();\n            AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);\n            FileWrapper parent = updatedSharer.getByPath(updatedSharer.username + \"/\" + folderName).join().get();\n            parent.uploadFileSection(filename, suffixStream, false, originalFileContents.length,\n                    originalFileContents.length + suffix.length, Optional.empty(), true,\n                    updatedSharer.network, crypto, () -> false, l -> {},\n                    null, Optional.empty(), null, parent.mirrorBatId()).join();\n            FileWrapper extendedFile = updatedSharer.getByPath(originalFilePath).join().get();\n            AsyncReader extendedContents = extendedFile.getInputStream(updatedSharer.network, crypto, l -> {}).join();\n            byte[] newFileContents = Serialize.readFully(extendedContents, extendedFile.getSize()).join();\n\n            Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(originalFileContents, suffix)));\n\n            Thread.sleep(10_000); // let all pointer caches invalidate\n            // test remaining users can still see shared file and folder\n            for (int j = i + 1; j < updatedSharees.size(); j++) {\n                UserContext otherUser = updatedSharees.get(j);\n\n                Optional<FileWrapper> sharedFolder = otherUser.getByPath(updatedSharer.username + \"/\" + folderName).join();\n                Assert.assertTrue(\"Shared folder present via direct path\", sharedFolder.isPresent() && sharedFolder.get().getName().equals(folderName));\n\n                FileWrapper sharedFile = otherUser.getByPath(updatedSharer.username + \"/\" + folderName + \"/\" + filename).join().get();\n                checkFileContents(newFileContents, sharedFile, otherUser);\n                Set<String> sharedChildNames = sharedFolder.get().getChildren(crypto.hasher, otherUser.network).join()\n                        .stream()\n                        .map(f -> f.getName())\n                        .collect(Collectors.toSet());\n                Assert.assertTrue(\"Correct children\", sharedChildNames.equals(childNames));\n            }\n        }\n    }\n\n    public static void grantAndRevokeDirWriteAccess(NetworkAccess sharerNode,\n                                                    NetworkAccess shareeNode,\n                                                    int shareeCount,\n                                                    Random random) throws Exception {\n        Assert.assertTrue(0 < shareeCount);\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, sharerPassword, sharerNode, crypto);\n\n        List<String> shareePasswords = IntStream.range(0, shareeCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(shareeNode, random, shareeCount, shareePasswords);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a file from u1 to the others\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"afolder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        String path = PathUtil.get(sharerUsername, folderName).toString();\n        System.out.println(\"PATH \"+ path);\n        FileWrapper folder = sharer.getByPath(path).join().get();\n        String filename = \"somefile.txt\";\n        byte[] originalFileContents = \"Hello Peergos friend!\".getBytes();\n        AsyncReader resetableFileInputStream = new AsyncReader.ArrayBacked(originalFileContents);\n        folder.uploadOrReplaceFile(filename, resetableFileInputStream,\n                originalFileContents.length, sharer.network, crypto, () -> false, l -> {}).join();\n        String originalFilePath = sharer.username + \"/\" + folderName + \"/\" + filename;\n\n        for (int i=0; i< 20; i++) {\n            sharer.getByPath(path).join().get()\n                    .mkdir(\"subdir\"+i, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n        }\n\n        // file is uploaded, do the actual sharing\n        sharer.shareWriteAccessWith(PathUtil.get(path), shareeUsers.stream()\n                .map(c -> c.username)\n                .collect(Collectors.toSet())).join();\n\n        // upload a image\n        String imagename = \"small.png\";\n        byte[] data = Files.readAllBytes(PathUtil.get(\"assets\", \"logo.png\"));\n        FileWrapper sharedFolderv0 = sharer.getByPath(path).join().get();\n        sharedFolderv0.uploadOrReplaceFile(imagename, AsyncReader.build(data), data.length,\n                sharer.network, crypto, () -> false, x -> {}).join();\n\n        // create a directory\n        FileWrapper sharedFolderv1 = sharer.getByPath(path).join().get();\n        sharedFolderv1.mkdir(\"asubdir\", sharer.network, false, sharer.mirrorBatId(), crypto).join();\n\n        UserContext shareeUploader = shareeUsers.get(0);\n        // check sharee can see folder via getChildren() which is used by the web-ui\n        Set<FileWrapper> children = shareeUploader.getByPath(sharer.username).join().get()\n                .getChildren(crypto.hasher, shareeUploader.network).join();\n        Assert.assertTrue(children.stream()\n                .filter(f -> f.getName().equals(folderName))\n                .findAny()\n                .isPresent());\n\n        // check a sharee can upload a file\n        FileWrapper sharedDir = shareeUploader.getByPath(path).join().get();\n        String shareeFilename = \"a-new-file.png\";\n        sharedDir.uploadFileJS(shareeFilename, AsyncReader.build(data), 0, data.length,\n                false, shareeUploader.mirrorBatId(), shareeUploader.network, crypto, x -> {}, shareeUploader.getTransactionService(), f -> Futures.of(false)).join();\n        FileWrapper newFile = shareeUploader.getByPath(path + \"/\" + shareeFilename).join().get();\n        Assert.assertTrue(newFile.mirrorBatId().equals(sharer.mirrorBatId()));\n\n        Set<String> childNames = sharer.getByPath(path).join().get().getChildren(crypto.hasher, sharer.network).join()\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n\n        // check each user can see the shared folder and directory\n        for (UserContext sharee : shareeUsers) {\n            FileWrapper sharedFolder = sharee.getByPath(sharer.username + \"/\" + folderName).join().orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n            Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n            FileWrapper sharedFile = sharee.getByPath(sharer.username + \"/\" + folderName + \"/\" + filename).join().get();\n            checkFileContents(originalFileContents, sharedFile, sharee);\n            Set<String> sharedChildNames = sharedFolder.getChildren(crypto.hasher, sharee.network).join()\n                    .stream()\n                    .map(f -> f.getName())\n                    .collect(Collectors.toSet());\n            Assert.assertTrue(\"Correct children\", sharedChildNames.equals(childNames));\n        }\n\n        MultiUserTests.checkUserValidity(sharerNode, sharerUsername);\n\n        UserContext updatedSharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, sharerPassword, sharerNode.clear(), crypto);\n\n        List<UserContext> updatedSharees = shareeUsers.stream()\n                .map(e -> ensureSignedUp(e.username, shareePasswords.get(shareeUsers.indexOf(e)), shareeNode.clear(), crypto))\n                .collect(Collectors.toList());\n\n\n        for (int i = 0; i < updatedSharees.size(); i++) {\n            UserContext user = updatedSharees.get(i);\n            updatedSharer.unShareWriteAccess(PathUtil.get(updatedSharer.username, folderName), user.username).join();\n\n            Optional<FileWrapper> updatedSharedFolder = user.getByPath(updatedSharer.username + \"/\" + folderName).join();\n\n            // test that u1 can still access the original file, and user cannot\n            Optional<FileWrapper> fileWithNewBaseKey = updatedSharer.getByPath(updatedSharer.username + \"/\" + folderName + \"/\" + filename).join();\n            Assert.assertTrue(!updatedSharedFolder.isPresent());\n            Assert.assertTrue(fileWithNewBaseKey.isPresent());\n\n            // Now modify the file\n            byte[] suffix = \"Some new data at the end\".getBytes();\n            AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);\n            FileWrapper parent = updatedSharer.getByPath(updatedSharer.username + \"/\" + folderName).join().get();\n            parent.uploadFileSection(filename, suffixStream, false, originalFileContents.length,\n                    originalFileContents.length + suffix.length, Optional.empty(), true,\n                    updatedSharer.network, crypto, () -> false, l -> {},\n                    null, Optional.empty(), null, parent.mirrorBatId()).join();\n            FileWrapper extendedFile = updatedSharer.getByPath(originalFilePath).join().get();\n            AsyncReader extendedContents = extendedFile.getInputStream(updatedSharer.network, updatedSharer.crypto, l -> {\n            }).join();\n            byte[] newFileContents = Serialize.readFully(extendedContents, extendedFile.getSize()).join();\n\n            Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(originalFileContents, suffix)));\n\n            // test remaining users can still see shared file and folder\n            for (int j = i + 1; j < updatedSharees.size(); j++) {\n                UserContext otherUser = updatedSharees.get(j);\n\n                Optional<FileWrapper> sharedFolder = otherUser.getByPath(updatedSharer.username + \"/\" + folderName).join();\n                Assert.assertTrue(\"Shared folder present via direct path\", sharedFolder.isPresent() && sharedFolder.get().getName().equals(folderName));\n\n                FileWrapper sharedFile = otherUser.getByPath(updatedSharer.username + \"/\" + folderName + \"/\" + filename).join().get();\n                checkFileContents(newFileContents, sharedFile, otherUser);\n                Set<String> sharedChildNames = sharedFolder.get().getChildren(crypto.hasher, otherUser.network).join()\n                        .stream()\n                        .map(f -> f.getName())\n                        .collect(Collectors.toSet());\n                Assert.assertTrue(\"Correct children\", sharedChildNames.equals(childNames));\n            }\n        }\n        MultiUserTests.checkUserValidity(sharerNode, sharerUsername);\n    }\n\n    public static void grantAndRevokeNestedDirWriteAccess(NetworkAccess network,\n                                                          Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 2, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n        UserContext b = shareeUsers.get(1);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a directory from u1 to u2\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path dirPath = PathUtil.get(sharer.username, folderName);\n\n        FileWrapper folder = sharer.getByPath(dirPath).join().get();\n        String filename = \"somefile.txt\";\n        byte[] data = \"Hello Peergos friend!\".getBytes();\n        AsyncReader resetableFileInputStream = new AsyncReader.ArrayBacked(data);\n        folder.uploadOrReplaceFile(filename, resetableFileInputStream,\n                data.length, sharer.network, crypto, () -> false, l -> {}).join();\n        String originalFilePath = sharer.username + \"/\" + folderName + \"/\" + filename;\n\n        for (int i=0; i< 20; i++) {\n            sharer.getByPath(dirPath).join().get()\n                    .mkdir(\"subdir\"+i, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n        }\n\n        // share /u1/folder with 'a'\n        sharer.shareWriteAccessWith(dirPath, Collections.singleton(a.username)).join();\n\n        // create a directory\n        FileWrapper sharedFolderv1 = sharer.getByPath(dirPath).join().get();\n        String subdirName = \"subdir\";\n        sharedFolderv1.mkdir(subdirName, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n\n        // share /u1/folder with 'b'\n        Path subdirPath = PathUtil.get(sharer.username, folderName, subdirName);\n        sharer.shareWriteAccessWith(subdirPath, Collections.singleton(b.username)).join();\n\n        // check 'b' can upload a file\n        UserContext shareeUploader = shareeUsers.get(0);\n        FileWrapper sharedDir = shareeUploader.getByPath(subdirPath).join().get();\n        sharedDir.uploadFileJS(\"a-new-file.png\", AsyncReader.build(data), 0, data.length,\n                false, sharedDir.mirrorBatId(), shareeUploader.network, crypto, x -> {}, shareeUploader.getTransactionService(), f -> Futures.of(false)).join();\n\n        Set<String> childNames = sharer.getByPath(dirPath).join().get().getChildren(crypto.hasher, sharer.network).join()\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n\n        // check 'a' can see the shared directory\n        FileWrapper sharedFolder = a.getByPath(sharer.username + \"/\" + folderName).join()\n                .orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n        Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n        FileWrapper sharedFile = a.getByPath(sharer.username + \"/\" + folderName + \"/\" + filename).join().get();\n        checkFileContents(data, sharedFile, a);\n        Set<String> sharedChildNames = sharedFolder.getChildren(crypto.hasher, a.network).join()\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n        Assert.assertTrue(\"Correct children\", sharedChildNames.equals(childNames));\n\n        MultiUserTests.checkUserValidity(network, sharer.username);\n\n        UserContext updatedSharer = PeergosNetworkUtils.ensureSignedUp(sharer.username, password, network.clear(), crypto);\n\n        List<UserContext> updatedSharees = shareeUsers.stream()\n                .map(e -> ensureSignedUp(e.username, password, network.clear(), crypto))\n                .collect(Collectors.toList());\n\n        // unshare subdir from 'b'\n        UserContext user = updatedSharees.get(1);\n        updatedSharer.unShareWriteAccess(subdirPath, b.username).join();\n\n        Optional<FileWrapper> updatedSharedFolder = user.getByPath(updatedSharer.username + \"/\" + folderName).join();\n\n        // test that u1 can still access the original file, and user cannot\n        Optional<FileWrapper> fileWithNewBaseKey = updatedSharer.getByPath(updatedSharer.username + \"/\" + folderName + \"/\" + filename).join();\n        Assert.assertTrue(!updatedSharedFolder.isPresent());\n        Assert.assertTrue(fileWithNewBaseKey.isPresent());\n\n        // Now modify the file\n        byte[] suffix = \"Some new data at the end\".getBytes();\n        AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);\n        FileWrapper parent = updatedSharer.getByPath(updatedSharer.username + \"/\" + folderName).join().get();\n        parent.uploadFileSection(filename, suffixStream, false, data.length,\n                data.length + suffix.length, Optional.empty(), true,\n                updatedSharer.network, crypto, () -> false, l -> {},\n                null, Optional.empty(), null, parent.mirrorBatId()).join();\n        FileWrapper extendedFile = updatedSharer.getByPath(originalFilePath).join().get();\n        AsyncReader extendedContents = extendedFile.getInputStream(updatedSharer.network, updatedSharer.crypto, l -> {}).join();\n        byte[] newFileContents = Serialize.readFully(extendedContents, extendedFile.getSize()).join();\n\n        Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(data, suffix)));\n\n        // test 'a' can still see shared file and folder\n        UserContext otherUser = updatedSharees.get(0);\n\n        Optional<FileWrapper> folderAgain = otherUser.getByPath(updatedSharer.username + \"/\" + folderName).join();\n        Assert.assertTrue(\"Shared folder present via direct path\", folderAgain.isPresent() && folderAgain.get().getName().equals(folderName));\n\n        FileWrapper sharedFileAgain = otherUser.getByPath(updatedSharer.username + \"/\" + folderName + \"/\" + filename).join().get();\n        checkFileContents(newFileContents, sharedFileAgain, otherUser);\n        Set<String> childNamesAgain = folderAgain.get().getChildren(crypto.hasher, otherUser.network).join()\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n        Assert.assertTrue(\"Correct children\", childNamesAgain.equals(childNames));\n\n        MultiUserTests.checkUserValidity(network, sharer.username);\n    }\n\n    public static void grantAndRevokeParentNestedWriteAccess(NetworkAccess network,\n                                                    Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 2, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n        UserContext b = shareeUsers.get(1);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a directory from u1 to u2\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path dirPath = PathUtil.get(sharer.username, folderName);\n\n        // create a directory\n        String subdirName = \"subdir\";\n        sharer.getByPath(dirPath).join().get()\n                .mkdir(subdirName, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n\n        // share /u1/folder/subdir with 'b'\n        Path subdirPath = PathUtil.get(sharer.username, folderName, subdirName);\n        sharer.shareWriteAccessWith(subdirPath, Collections.singleton(b.username)).join();\n\n        // share /u1/folder with 'a'\n        sharer.shareWriteAccessWith(dirPath, Collections.singleton(a.username)).join();\n\n        // check sharer can still see /u1/folder/subdir\n        Assert.assertTrue(\"subdir still present\", sharer.getByPath(subdirPath).join().isPresent());\n\n        // check 'a' can see the shared directory\n        FileWrapper sharedFolder = a.getByPath(sharer.username + \"/\" + folderName).join()\n                .orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n        Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n        // revoke access to /u1/folder from 'a'\n        sharer.unShareWriteAccess(dirPath, a.username).join();\n        // check 'a' can't see the shared directory\n        Optional<FileWrapper> unsharedFolder = a.getByPath(sharer.username + \"/\" + folderName).join();\n        Assert.assertTrue(\"a can't see unshared folder\", ! unsharedFolder.isPresent());\n    }\n\n    public static void grantAndRevokeReadAccessToFileInFolder(NetworkAccess network, Random random) throws IOException {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a directory from u1 to u2\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path dirPath = PathUtil.get(sharer.username, folderName);\n\n\n        String filename = \"somefile.txt\";\n        File f = File.createTempFile(\"peergos\", \"\");\n        byte[] originalFileContents = new byte[1*1024*1024];\n        random.nextBytes(originalFileContents);\n        Files.write(f.toPath(), originalFileContents);\n        ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(f);\n\n        FileWrapper dir = sharer.getByPath(dirPath).join().get();\n\n        FileWrapper uploaded = dir.uploadOrReplaceFile(filename, resetableFileInputStream, f.length(),\n                sharer.network, crypto, () -> false, l -> {}).join();\n\n\n        Path fileToShare = PathUtil.get(sharer.username, folderName, filename);\n        sharer.shareReadAccessWith(fileToShare, Collections.singleton(a.username)).join();\n\n        // check 'a' can see the shared file\n        FileWrapper sharedFolder = a.getByPath(sharer.username + \"/\" + folderName + \"/\" + filename).join()\n                .orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n        Assert.assertEquals(sharedFolder.getFileProperties().name, filename);\n\n\n        sharer.unShareReadAccess(fileToShare, a.username).join();\n        // check 'a' can't see the shared directory\n        FileWrapper unsharedLocation = a.getByPath(sharer.username).join().get();\n        Set<FileWrapper> children = unsharedLocation.getChildren(crypto.hasher, a.network).join();\n        Assert.assertTrue(\"a can't see unshared folder\", children.stream().filter(c -> c.getName().equals(folderName)).findFirst().isEmpty());\n    }\n\n    public static void grantWriteToFileAndDeleteParent(NetworkAccess network, Random random) throws IOException {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network.clear(), random, 1, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a directory from u1 to u2\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path dirPath = PathUtil.get(sharer.username, folderName);\n\n\n        String filename = \"somefile.txt\";\n        File f = File.createTempFile(\"peergos\", \"\");\n        byte[] originalFileContents = new byte[1*1024*1024];\n        random.nextBytes(originalFileContents);\n        Files.write(f.toPath(), originalFileContents);\n        ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(f);\n\n        FileWrapper dir = sharer.getByPath(dirPath).join().get();\n\n        FileWrapper uploaded = dir.uploadOrReplaceFile(filename, resetableFileInputStream, f.length(),\n                sharer.network, crypto, () -> false, l -> {}).join();\n\n\n        Path fileToShare = PathUtil.get(sharer.username, folderName, filename);\n        sharer.shareWriteAccessWith(fileToShare, Collections.singleton(a.username)).join();\n\n        // check 'a' can see the shared file\n        FileWrapper sharedFolder = a.getByPath(sharer.username + \"/\" + folderName + \"/\" + filename).join()\n                .orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n        Assert.assertEquals(sharedFolder.getFileProperties().name, filename);\n\n        // delete the parent folder\n        FileWrapper parent = sharer.getByPath(dirPath).join().get();\n        parent.remove(sharer.getUserRoot().join(), dirPath, sharer).join();\n        // check 'a' can't see the shared directory\n        FileWrapper unsharedLocation = a.getByPath(sharer.username).join().get();\n        Set<FileWrapper> children = unsharedLocation.getChildren(crypto.hasher, a.network).join();\n        Assert.assertTrue(\"a can't see unshared folder\", children.stream().filter(c -> c.getName().equals(folderName)).findFirst().isEmpty());\n    }\n\n    public static void grantAndRevokeWriteThenReadAccessToFolder(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a directory from u1 to u2\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path dirPath = PathUtil.get(sharer.username, folderName);\n\n        // share /u1/folder with 'a'\n        sharer.shareWriteAccessWith(dirPath, Collections.singleton(a.username)).join();\n\n        // check 'a' can see the shared file\n        FileWrapper sharedFolder = a.getByPath(sharer.username + \"/\" + folderName).join()\n                .orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n        Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n        sharer.unShareWriteAccess(dirPath, a.username).join();\n\n        // check 'a' can't see the shared directory\n        FileWrapper unsharedLocation = a.getByPath(sharer.username).join().get();\n        Set<FileWrapper> children = unsharedLocation.getChildren(crypto.hasher, a.network).join();\n        Assert.assertTrue(\"a can't see unshared folder\", children.stream().filter(c -> c.getName().equals(folderName)).findFirst().isEmpty());\n\n        sharer.shareReadAccessWith(dirPath, Collections.singleton(a.username)).join();\n\n        // check 'a' can see the shared file\n        sharedFolder = a.getByPath(sharer.username + \"/\" + folderName).join()\n                .orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n        Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n        sharer.unShareReadAccess(dirPath, a.username).join();\n        // check 'a' can't see the shared directory\n        unsharedLocation = a.getByPath(sharer.username).join().get();\n        children = unsharedLocation.getChildren(crypto.hasher, a.network).join();\n        Assert.assertTrue(\"a can't see unshared folder\", children.stream().filter(c -> c.getName().equals(folderName)).findFirst().isEmpty());\n    }\n\n    \n    public static void grantAndRevokeDirWriteAccessWithNestedWriteAccess(NetworkAccess network,\n                                                                         Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 2, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n        UserContext b = shareeUsers.get(1);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"folder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        Path dirPath = PathUtil.get(sharer.username, folderName);\n\n        // put a file and some sub-dirs into the dir\n        FileWrapper folder = sharer.getByPath(dirPath).join().get();\n        String filename = \"somefile.txt\";\n        byte[] data = \"Hello Peergos friend!\".getBytes();\n        AsyncReader resetableFileInputStream = new AsyncReader.ArrayBacked(data);\n        folder.uploadOrReplaceFile(filename, resetableFileInputStream,\n                data.length, sharer.network, crypto, () -> false, l -> {}).join();\n\n        for (int i=0; i< 20; i++) {\n            sharer.getByPath(dirPath).join().get()\n                    .mkdir(\"subdir\"+i, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n        }\n\n        // grant write access to a directory to user 'a'\n        sharer.shareWriteAccessWith(dirPath, Collections.singleton(a.username)).join();\n\n        // create another sub-directory\n        FileWrapper sharedFolderv1 = sharer.getByPath(dirPath).join().get();\n        String subdirName = \"subdir\";\n        sharedFolderv1.mkdir(subdirName, sharer.network, false, sharer.mirrorBatId(), crypto).join();\n\n        // grant write access to a sub-directory to user 'b'\n        Path subdirPath = PathUtil.get(sharer.username, folderName, subdirName);\n        sharer.shareWriteAccessWith(subdirPath, Collections.singleton(b.username)).join();\n\n        List<Set<AbsoluteCapability>> childCapsByChunk0 = getAllChildCapsByChunk(sharer.getByPath(dirPath).join().get(), network);\n        Assert.assertTrue(\"Correct links per chunk, without duplicates\",\n                childCapsByChunk0.stream().map(x -> x.size()).collect(Collectors.toList())\n                        .equals(Arrays.asList(10, 10, 2)));\n\n        // check 'b' can upload a file\n        UserContext shareeUploader = shareeUsers.get(0);\n        FileWrapper sharedDir = shareeUploader.getByPath(subdirPath).join().get();\n        sharedDir.uploadFileJS(\"a-new-file.png\", AsyncReader.build(data), 0, data.length,\n                false, sharedDir.mirrorBatId(), shareeUploader.network, crypto, x -> {}, shareeUploader.getTransactionService(), f -> Futures.of(false)).join();\n\n        // check 'a' can see the shared directory\n        FileWrapper sharedFolder = a.getByPath(sharer.username + \"/\" + folderName).join()\n                .orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n        Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n        FileWrapper sharedFile = a.getByPath(sharer.username + \"/\" + folderName + \"/\" + filename).join().get();\n        checkFileContents(data, sharedFile, a);\n        Set<String> sharedChildNames = sharedFolder.getChildren(crypto.hasher, a.network).join()\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n        Set<String> childNames = sharer.getByPath(dirPath).join().get().getChildren(crypto.hasher, sharer.network).join()\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n        Assert.assertTrue(\"Correct children\", sharedChildNames.equals(childNames));\n\n        MultiUserTests.checkUserValidity(network, sharer.username);\n\n        Set<AbsoluteCapability> childCaps = getAllChildCaps(sharer.getByPath(dirPath).join().get(), network);\n        Assert.assertTrue(\"Correct number of child caps on dir\", childCaps.size() == 22);\n\n        UserContext updatedSharer = PeergosNetworkUtils.ensureSignedUp(sharer.username, password, network.clear(), crypto);\n\n        List<UserContext> updatedSharees = shareeUsers.stream()\n                .map(e -> ensureSignedUp(e.username, password, network.clear(), crypto))\n                .collect(Collectors.toList());\n\n        // revoke write access to top level dir from 'a'\n        UserContext user = updatedSharees.get(0);\n\n        List<Set<AbsoluteCapability>> childCapsByChunk1 = getAllChildCapsByChunk(updatedSharer.getByPath(dirPath).join().get(), network);\n        Assert.assertTrue(\"Correct links per chunk, without duplicates\",\n                childCapsByChunk1.stream().map(x -> x.size()).collect(Collectors.toList())\n                        .equals(Arrays.asList(10, 10, 2)));\n\n        updatedSharer.unShareWriteAccess(dirPath, a.username).join();\n\n        List<Set<AbsoluteCapability>> childCapsByChunk2 = getAllChildCapsByChunk(sharer.getByPath(dirPath).join().get(), network);\n        Assert.assertTrue(\"Correct links per chunk, without duplicates\",\n                childCapsByChunk2.stream().map(x -> x.size()).collect(Collectors.toList())\n                        .equals(Arrays.asList(10, 10, 2)));\n\n        Optional<FileWrapper> updatedSharedFolder = user.getByPath(dirPath).join();\n\n        // test that sharer can still access the sub-dir, and 'a' cannot access the top level dir\n        Optional<FileWrapper> updatedSubdir = updatedSharer.getByPath(subdirPath).join();\n        Assert.assertTrue(! updatedSharedFolder.isPresent());\n        Assert.assertTrue(updatedSubdir.isPresent());\n\n        // test 'b' can still see shared sub-dir\n        UserContext otherUser = updatedSharees.get(1);\n\n        Optional<FileWrapper> subdirAgain = otherUser.getByPath(subdirPath).join();\n        Assert.assertTrue(\"Shared folder present via direct path\", subdirAgain.isPresent());\n\n        MultiUserTests.checkUserValidity(network, sharer.username);\n    }\n\n    public static void socialFeed(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a file from u1 to u2\n        byte[] fileData = new byte[1*1024*1024];\n        random.nextBytes(fileData);\n        Path file1 = PathUtil.get(sharer.username, \"first-file.txt\");\n        uploadAndShare(fileData, file1, sharer, a.username);\n\n        // check 'a' can see the shared file in their social feed\n        SocialFeed feed = a.getSocialFeed().join();\n        int feedSize = 2;\n        List<SharedItem> items = feed.getShared(feedSize, feedSize + 1, a.crypto, a.network).join();\n        Assert.assertTrue(items.size() > 0);\n        SharedItem item = items.get(0);\n        Assert.assertTrue(item.owner.equals(sharer.username));\n        Assert.assertTrue(item.sharer.equals(sharer.username));\n        AbsoluteCapability readCap = sharer.getByPath(file1).join().get().getPointer().capability.readOnly();\n        Assert.assertTrue(item.cap.equals(readCap));\n        Assert.assertTrue(PathUtil.get(item.path).equals(file1));\n\n        // Test the feed after a fresh login\n        UserContext freshA = PeergosNetworkUtils.ensureSignedUp(a.username, password, network, crypto);\n        SocialFeed freshFeed = freshA.getSocialFeed().join();\n        List<SharedItem> freshItems = freshFeed.getShared(feedSize, feedSize + 1, a.crypto, a.network).join();\n        Assert.assertTrue(freshItems.size() > 0);\n        SharedItem freshItem = freshItems.get(0);\n        Assert.assertTrue(freshItem.equals(item));\n\n        // Test sharing a new item after construction\n        Path file2 = PathUtil.get(sharer.username, \"second-file.txt\");\n        uploadAndShare(fileData, file2, sharer, a.username);\n\n        SocialFeed updatedFeed = freshFeed.update().join();\n        List<SharedItem> items2 = updatedFeed.getShared(feedSize + 1, feedSize + 2, a.crypto, a.network).join();\n        Assert.assertTrue(items2.size() > 0);\n        SharedItem item2 = items2.get(0);\n        Assert.assertTrue(item2.owner.equals(sharer.username));\n        Assert.assertTrue(item2.sharer.equals(sharer.username));\n        AbsoluteCapability readCap2 = sharer.getByPath(file2).join().get().getPointer().capability.readOnly();\n        Assert.assertTrue(item2.cap.equals(readCap2));\n\n        // check accessing the files normally\n        UserContext fresherA = PeergosNetworkUtils.ensureSignedUp(a.username, password, network, crypto);\n        Optional<FileWrapper> directFile1 = fresherA.getByPath(file1).join();\n        Assert.assertTrue(directFile1.isPresent());\n        Optional<FileWrapper> directFile2 = fresherA.getByPath(file2).join();\n        Assert.assertTrue(directFile2.isPresent());\n\n        // check feed after browsing to the senders home\n        Path file3 = PathUtil.get(sharer.username, \"third-file.txt\");\n        uploadAndShare(fileData, file3, sharer, a.username);\n\n        // browse to sender home\n        freshA.getByPath(PathUtil.get(sharer.username)).join();\n\n        Path file4 = PathUtil.get(sharer.username, \"fourth-file.txt\");\n        uploadAndShare(fileData, file4, sharer, a.username);\n\n        // now check feed\n        SocialFeed updatedFeed3 = freshFeed.update().join();\n        List<SharedItem> items3 = updatedFeed3.getShared(feedSize + 2, feedSize + 4, a.crypto, a.network).join();\n        Assert.assertTrue(items3.size() > 0);\n        SharedItem item3 = items3.get(0);\n        Assert.assertTrue(item3.owner.equals(sharer.username));\n        Assert.assertTrue(item3.sharer.equals(sharer.username));\n        AbsoluteCapability readCap3 = sharer.getByPath(file3).join().get().getPointer().capability.readOnly();\n        Assert.assertTrue(item3.cap.equals(readCap3));\n\n        // social post\n        List<Text> postBody = Arrays.asList(new Text(\"G'day, skip!\"));\n        SocialPost post = new SocialPost(sharer.username, postBody, LocalDateTime.now(),\n                SocialPost.Resharing.Friends, Optional.empty(), Collections.emptyList(), Collections.emptyList());\n        SocialFeed sharerFeed = sharer.getSocialFeed().join();\n        Pair<Path, FileWrapper> p = sharerFeed.createNewPost(post).join();\n        sharer.shareReadAccessWith(p.left, Set.of(a.username)).join();\n        List<SharedItem> withPost = freshFeed.update().join().getShared(0, feedSize + 5, crypto, fresherA.network).join();\n        SharedItem sharedPost = withPost.get(withPost.size() - 1);\n        FileWrapper postFile = fresherA.getByPath(sharedPost.path).join().get();\n        assertTrue(postFile.getFileProperties().isSocialPost());\n        SocialPost receivedPost = Serialize.parse(postFile.getInputStream(network, crypto, x -> {}).join(),\n                postFile.getSize(), SocialPost::fromCbor).join();\n        assertTrue(receivedPost.body.equals(post.body));\n\n        // 2 comments on post\n        SocialPost comment;\n        for (int i=0; i < 2; i++) {\n            List<Text> commentBody = Arrays.asList(new Text(\"Amazing! \" + i));\n            byte[] postBytes = post.serialize();\n            Multihash postHash = crypto.hasher.hashFromStream(AsyncReader.build(postBytes), postBytes.length).join();\n            comment = new SocialPost(a.username, commentBody, LocalDateTime.now(),\n                    SocialPost.Resharing.Friends, Optional.of(new FileRef(sharedPost.path, sharedPost.cap, postHash)), Collections.emptyList(), Collections.emptyList());\n            Pair<Path, FileWrapper> com = a.getSocialFeed().join().createNewPost(comment).join();\n            a.shareReadAccessWith(com.left, Set.of(sharer.username)).join();\n        }\n\n        SocialFeed withComment = sharer.getSocialFeed().join().update().join();\n        List<SharedItem> comments = withComment.getShared(0, 10, crypto, sharer.network).join();\n        SharedItem c = comments.get(comments.size() - 1);\n        FileWrapper comFile = sharer.getByPath(c.path).join().get();\n        SocialPost receivedComment = Serialize.parse(comFile.getInputStream(network, crypto, x -> {}).join(),\n                comFile.getSize(), SocialPost::fromCbor).join();\n        System.out.println();\n    }\n\n    public static void socialPostPropagation(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a\"+generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 2, Arrays.asList(password, password));\n        UserContext b = shareeUsers.get(0);\n        UserContext c = shareeUsers.get(1);\n\n        // friend a with others, b and c are not friends\n        friendBetweenGroups(Arrays.asList(a), shareeUsers);\n\n        // friends are now connected\n        // test social post propagation (comment from b on post from a gets to c)\n        SocialPost post = new SocialPost(a.username,\n                Arrays.asList(new Text(\"G'day, skip!\")), LocalDateTime.now(),\n                SocialPost.Resharing.Friends, Optional.empty(),\n                Collections.emptyList(), Collections.emptyList());\n        SocialFeed feed = a.getSocialFeed().join();\n        Pair<Path, FileWrapper> p = feed.createNewPost(post).join();\n        String aFriendsUid = a.getGroupUid(SocialState.FRIENDS_GROUP_NAME).join().get();\n        a.shareReadAccessWith(p.left, Set.of(aFriendsUid)).join();\n\n        // b receives the post\n        SocialFeed bFeed = b.getSocialFeed().join().update().join();\n        List<Pair<SharedItem, FileWrapper>> bPosts = bFeed.getSharedFiles(0, 25).join();\n        Pair<SharedItem, FileWrapper> sharedPost = bPosts.get(bPosts.size() - 1);\n\n        // b now comments on post from a\n        SocialPost reply = new SocialPost(b.username,\n                Arrays.asList(new Text(\"What an entrance!\")), LocalDateTime.now(),\n                SocialPost.Resharing.Friends,\n                Optional.of(new FileRef(sharedPost.left.path, sharedPost.left.cap, post.contentHash(hasher).join())),\n                Collections.emptyList(), Collections.emptyList());\n        Pair<Path, FileWrapper> replyFromB = bFeed.createNewPost(reply).join();\n        String bFriendsUid = b.getGroupUid(SocialState.FRIENDS_GROUP_NAME).join().get();\n        b.shareReadAccessWith(replyFromB.left, Set.of(bFriendsUid)).join();\n\n        // make sure a includes a ref to the comment on the original\n        a.getSocialFeed().join().update().join();\n\n        // check c gets the post and it references the comment\n        List<Pair<SharedItem, FileWrapper>> cPosts = c.getSocialFeed().join().update().join().getSharedFiles(0, 25).join();\n        Pair<SharedItem, FileWrapper> cPost = cPosts.get(cPosts.size() - 1);\n        SocialPost receivedPost = Serialize.parse(cPost.right.getInputStream(network, crypto, x -> {}).join(),\n                cPost.right.getSize(), SocialPost::fromCbor).join();\n        Assert.assertTrue(receivedPost.author.equals(a.username));\n        Assert.assertTrue(receivedPost.comments.get(0).cap.equals(replyFromB.right.readOnlyPointer()));\n    }\n\n    public static void socialFeedBug(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(randomUsername(\"sharer-\", random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext sharee = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        byte[] fileData = new byte[1*1024*1024];\n        random.nextBytes(fileData);\n        AsyncReader reader = new AsyncReader.ArrayBacked(fileData);\n\n        SocialFeed feed = sharer.getSocialFeed().join();\n        FileRef ref = feed.uploadMediaForPost(reader, fileData.length, LocalDateTime.now(), c -> {}).join().right;\n        SocialPost.Resharing resharingType = SocialPost.Resharing.Friends;\n        List<? extends Content> body = Arrays.asList(new Text(\"aaaa\"), new Reference(ref));\n        SocialPost socialPost = SocialPost.createInitialPost(sharer.username, body, resharingType);\n\n        Pair<Path, FileWrapper> result = feed.createNewPost(socialPost).join();\n\n        LocalDateTime postTime = LocalDateTime.now();\n        String updatedBody = \"bbbbb\";\n        socialPost = socialPost.edit(Arrays.asList(new Text(updatedBody), new Reference(ref)), postTime);\n\n        String uuid = result.left.getFileName().toString();\n        result = feed.updatePost(uuid, socialPost).join();\n\n        String friendGroup = SocialState.FRIENDS_GROUP_NAME;\n        SocialState state = sharer.getSocialState().join();\n        String groupUid = state.groupNameToUid.get(friendGroup);\n        // was Set.of(groupUid)\n        //boolean res = sharer.shareReadAccessWith(result.left, Set.of(sharee.username)).join();\n        sharer.shareReadAccessWith(result.left, Set.of(groupUid)).join();\n\n        SocialFeed receiverFeed = sharee.getSocialFeed().join().update().join();\n        List<Pair<SharedItem, FileWrapper>> files = receiverFeed.getSharedFiles(0, 100).join();\n        assertTrue(files.size() == 3);\n        FileWrapper socialFile = files.get(files.size() -1).right;\n        SharedItem sharedItem = files.get(files.size() -1).left;\n        FileProperties props = socialFile.getFileProperties();\n        SocialPost loadedSocialPost = Serialize.parse(socialFile, SocialPost::fromCbor, sharee.network, crypto).join();\n        assertTrue(loadedSocialPost.body.get(0).inlineText().equals(updatedBody));\n\n        FileRef mediaRef = ((Reference)loadedSocialPost.body.get(1)).ref;\n        Optional<FileWrapper> optFile = sharee.network.getFile(mediaRef.cap, sharer.username).join();\n        assertTrue(optFile.isPresent());\n\n        //create a reply\n        String replyText = \"reply\";\n        Multihash hash = loadedSocialPost.contentHash(sharee.crypto.hasher).join();\n        FileRef parent = new FileRef(sharedItem.path, sharedItem.cap, hash);\n        SocialPost replySocialPost = SocialPost.createComment(parent, resharingType, sharee.username, Arrays.asList(new Text(replyText)));\n        result = receiverFeed.createNewPost(replySocialPost).join();\n        String receiverGroupUid = sharee.getSocialState().join().groupNameToUid.get(friendGroup);\n        sharee.shareReadAccessWith(result.left, Set.of(receiverGroupUid)).join();\n\n        //now sharer should see the reply\n        sharer = UserContext.signIn(sharer.username, password, UserTests::noMfa, false, false, sharer.network, sharer.crypto, c -> {}).join();\n        feed = sharer.getSocialFeed().join().update().join();\n        files = feed.getSharedFiles(0, 100).join();\n        assertTrue(files.size() == 5);\n        socialFile = files.get(files.size() -1).right;\n        loadedSocialPost = Serialize.parse(socialFile, SocialPost::fromCbor, sharer.network, crypto).join();\n        assertTrue(loadedSocialPost.body.get(0).inlineText().equals(replyText));\n    }\n\n    public static void socialFeedAndUnfriending(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(randomUsername(\"sharer-\", random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext sharee = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        SocialFeed feed = sharer.getSocialFeed().join();\n        SocialPost.Resharing resharingType = SocialPost.Resharing.Friends;\n        String bodyText = \"aaaa\";\n        List<Text> body = Arrays.asList(new Text(bodyText));\n        SocialPost socialPost = SocialPost.createInitialPost(sharer.username, body, resharingType);\n        Pair<Path, FileWrapper> result = feed.createNewPost(socialPost).join();\n\n        String friendGroup = SocialState.FRIENDS_GROUP_NAME;\n        SocialState state = sharer.getSocialState().join();\n        String groupUid = state.groupNameToUid.get(friendGroup);\n        sharer.shareReadAccessWith(result.left, Set.of(groupUid)).join();\n\n        SocialFeed receiverFeed = sharee.getSocialFeed().join().update().join();\n        List<Pair<SharedItem, FileWrapper>> files = receiverFeed.getSharedFiles(0, 100).join();\n        assertTrue(files.size() == 3);\n        FileWrapper socialFile = files.get(files.size() - 1).right;\n        SharedItem sharedItem = files.get(files.size() - 1).left;\n        FileProperties props = socialFile.getFileProperties();\n        SocialPost loadedSocialPost = Serialize.parse(socialFile, SocialPost::fromCbor, sharee.network, crypto).join();\n        assertTrue(loadedSocialPost.body.get(0).inlineText().equals(bodyText));\n\n        //create a reply\n        String replyText = \"reply\";\n        Multihash hash = loadedSocialPost.contentHash(sharee.crypto.hasher).join();\n        FileRef parent = new FileRef(sharedItem.path, sharedItem.cap, hash);\n        SocialPost replySocialPost = SocialPost.createComment(parent, resharingType, sharee.username,\n                Arrays.asList(new Text(replyText)));\n        result = receiverFeed.createNewPost(replySocialPost).join();\n        String receiverGroupUid = sharee.getSocialState().join().groupNameToUid.get(friendGroup);\n        sharee.shareReadAccessWith(result.left, Set.of(receiverGroupUid)).join();\n\n        //now sharer should see the reply\n        feed = sharer.getSocialFeed().join().update().join();\n        files = feed.getSharedFiles(0, 100).join();\n        assertTrue(files.size() == 5);\n        FileWrapper original = files.get(files.size() - 2).right;\n        FileWrapper reply = files.get(files.size() - 1).right;\n        SocialPost originalPost = Serialize.parse(original, SocialPost::fromCbor, sharer.network, crypto).join();\n        SocialPost replyPost = Serialize.parse(reply, SocialPost::fromCbor, sharer.network, crypto).join();\n        assertTrue(originalPost.body.get(0).inlineText().equals(bodyText));\n        assertTrue(replyPost.body.get(0).inlineText().equals(replyText));\n\n        sharer.removeFollower(sharee.username).join();\n        feed = sharer.getSocialFeed().join().update().join();\n        files = feed.getSharedFiles(0, 100).join();\n        assertTrue(files.size() == 5);\n        FileWrapper post = files.get(files.size() - 2).right;\n        SocialPost remainingSocialPost = Serialize.parse(post, SocialPost::fromCbor, sharer.network, crypto).join();\n        assertTrue(remainingSocialPost.body.get(0).inlineText().equals(bodyText));\n\n    }\n\n    private static void uploadAndShare(byte[] data, Path file, UserContext sharer, String sharee) {\n        String filename = file.getFileName().toString();\n        sharer.getByPath(file.getParent()).join().get()\n                .uploadOrReplaceFile(filename, AsyncReader.build(data), data.length,\n                        sharer.network, crypto, () -> false, l -> {}).join();\n        sharer.shareReadAccessWith(file, Set.of(sharee)).join();\n    }\n\n    public static void socialFeedVariations2(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext sharee = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n        String dir1 = \"one\";\n        String dir2 = \"two\";\n        sharer.getUserRoot().join().mkdir(dir1, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n        sharer.getUserRoot().join().mkdir(dir2, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        Path dirToShare1 = PathUtil.get(sharer.username, dir1);\n        Path dirToShare2 = PathUtil.get(sharer.username, dir2);\n        sharer.shareReadAccessWith(dirToShare1, Set.of(sharee.username)).join();\n        sharer.shareReadAccessWith(dirToShare2, Set.of(sharee.username)).join();\n\n        SocialFeed feed = sharee.getSocialFeed().join();\n        List<SharedItem> items = feed.getShared(0, 1000, sharee.crypto, sharee.network).join();\n        Assert.assertTrue(items.size() == 2 + 2);\n\n        sharee.getUserRoot().join().mkdir(\"mine\", sharee.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n    }\n\n    public static void socialFeedFailsInUI(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext sharee = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n        String dir1 = \"one\";\n        String dir2 = \"two\";\n        sharer.getUserRoot().join().mkdir(dir1, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n        sharer.getUserRoot().join().mkdir(dir2, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        Path dirToShare1 = PathUtil.get(sharer.username, dir1);\n        Path dirToShare2 = PathUtil.get(sharer.username, dir2);\n        sharer.shareReadAccessWith(dirToShare1, Set.of(sharee.username)).join();\n        sharer.shareReadAccessWith(dirToShare2, Set.of(sharee.username)).join();\n\n        SocialFeed feed = sharee.getSocialFeed().join();\n        int initialFeedSize = 2;\n        List<SharedItem> items = feed.getShared(0, 1000, sharee.crypto, sharee.network).join();\n        Assert.assertTrue(items.size() == initialFeedSize + 2);\n\n        sharee = PeergosNetworkUtils.ensureSignedUp(sharee.username, password, network, crypto);\n        sharee.getUserRoot().join().mkdir(\"mine\", sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        feed = sharee.getSocialFeed().join();\n        items = feed.getShared(0, 1000, sharee.crypto, sharee.network).join();\n        Assert.assertTrue(items.size() == initialFeedSize + 2);\n\n        //When attempting this in the web-ui the below call results in a failure when loading timeline entry\n        //Cannot seek to position 680 in file of length 340\n        feed = sharee.getSocialFeed().join();\n        items = feed.getShared(0, 1000, sharee.crypto, sharee.network).join();\n        Assert.assertTrue(items.size() == initialFeedSize + 2);\n    }\n\n    public static void socialFeedEmpty(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        SocialFeed feed = sharer.getSocialFeed().join();\n        List<SharedItem> items = feed.getShared(0, 1, sharer.crypto, sharer.network).join();\n        Assert.assertTrue(items.size() == 0);\n    }\n\n    public static void socialFeedVariations(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n        String dir1 = \"one\";\n        String dir2 = \"two\";\n        sharer.getUserRoot().join().mkdir(dir1, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n        sharer.getUserRoot().join().mkdir(dir2, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        Path dirToShare1 = PathUtil.get(sharer.username, dir1);\n        Path dirToShare2 = PathUtil.get(sharer.username, dir2);\n        sharer.shareReadAccessWith(dirToShare1, Set.of(a.username)).join();\n        sharer.shareReadAccessWith(dirToShare2, Set.of(a.username)).join();\n\n        SocialFeed feed = a.getSocialFeed().join();\n        int initialFeedSize = 2;\n        List<SharedItem> items = feed.getShared(0, 1000, a.crypto, a.network).join();\n        Assert.assertTrue(items.size() == initialFeedSize + 2);\n\n        //Add another file and share\n        String dir3 = \"three\";\n        sharer.getUserRoot().join().mkdir(dir3, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        Path dirToShare3 = PathUtil.get(sharer.username, dir3);\n        sharer.shareReadAccessWith(dirToShare3, Set.of(a.username)).join();\n\n        feed = a.getSocialFeed().join().update().join();\n        items = feed.getShared(0, 1000, a.crypto, a.network).join();\n        Assert.assertTrue(items.size() == initialFeedSize + 3);\n    }\n\n    public static void chatReplyWithAttachment(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password));\n        UserContext b = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(a), shareeUsers);\n\n        Messenger msgA = new Messenger(a);\n        ChatController controllerA = msgA.createChat().join();\n        controllerA = msgA.invite(controllerA, Arrays.asList(b.username), Arrays.asList(b.signer.publicKeyHash)).join();\n        List<Pair<SharedItem, FileWrapper>> feed = b.getSocialFeed().join().update().join().getSharedFiles(0, 10).join();\n\n        ApplicationMessage msg1 = ApplicationMessage.text(\"G'day mate!\");\n        controllerA = msgA.sendMessage(controllerA, msg1).join();\n        List<MessageEnvelope> initialMessages = controllerA.getMessages(0, 10).join();\n        MessageEnvelope lastMessage = initialMessages.get(initialMessages.size() - 1);\n\n        byte[] media = \"Some media data\".getBytes();\n        AsyncReader reader = AsyncReader.build(media);\n        Pair<String, FileRef> mediaRef = msgA.uploadMedia(controllerA, reader, \"txt\", media.length,\n                LocalDateTime.now(), x -> {\n                }).join();\n        ReplyTo msg2 = ReplyTo.build(lastMessage, ApplicationMessage.attachment(\"Isn't this cool!!\",\n                Arrays.asList(mediaRef.right)), hasher).join();\n        controllerA = msgA.sendMessage(controllerA, msg2).join();\n    }\n\n    public static void deleteEmailApp(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext user = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(random), password, network, crypto);\n        UserContext email = PeergosNetworkUtils.ensureSignedUp(\"email-\"+ generateUsername(random), password, network, crypto);\n\n        App emailApp = App.init(user, \"email\").join();\n        EmailClient client = EmailClient.load(emailApp, crypto).join();\n        client.connectToBridge(user).join();\n\n        Path path = new File(user.username + \"/.apps/email\").toPath();\n        FileWrapper parentDir = user.getByPath(path.getParent().toString()).join().get();\n        FileWrapper appDir = user.getByPath(path.toString()).join().get();\n        FileWrapper parent = appDir.remove(parentDir, path, user).join();\n        Assert.assertTrue(\"App removal worked\", parent != null);\n    }\n\n    public static void email(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext user = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(random), password, network, crypto);\n        UserContext email = PeergosNetworkUtils.ensureSignedUp(\"email-\"+ generateUsername(random), password, network, crypto);\n\n        App emailApp = App.init(user, \"email\").join();\n        EmailClient client = EmailClient.load(emailApp, crypto).join();\n        client = EmailClient.load(emailApp, crypto).join(); //test that keys are found and loaded\n        SecretLink writableLink = client.connectToBridge(user).join();\n\n        Optional<String> emailAddress =  client.getEmailAddress().join();\n        Assert.assertTrue(\"email address\", emailAddress.isEmpty());\n\n        //email bridge setup\n        EmailBridgeClient bridge = EmailBridgeClient.build(writableLink, network, crypto, user.username, user.username + \"@example.com\");\n\n        emailAddress =  client.getEmailAddress().join();\n        Assert.assertTrue(\"email address\", emailAddress.isPresent());\n\n        // send email to bridge\n        String attachmentFilename = \"text\";\n        String attachmentContent = \"this is an attachment!\";\n        byte[] data = attachmentContent.getBytes();\n        Map<String, byte[]> attachmentsMap = new HashMap<>();\n        String uuid = client.uploadAttachment(data).join();\n        attachmentsMap.put(uuid, data);\n        List<Attachment> outGoingAttachments = Arrays.asList(new Attachment(attachmentFilename, data.length, \"text/plain\", uuid));\n        EmailMessage msg = new EmailMessage(\"id\", \"msgid\", user.username, \"subject\",\n            LocalDateTime.now(), Arrays.asList(\"a@example.com\"), Collections.emptyList(), Collections.emptyList(),\n            \"content\", true, true, outGoingAttachments, null,\n            Optional.empty(), Optional.empty(), Optional.empty());\n        boolean sentEmail = client.send(msg).join();\n        Assert.assertTrue(\"email sent\", sentEmail);\n\n        // Receive sent email in bridge\n        List<String> filenames = bridge.listOutbox();\n        Assert.assertTrue(\"bridge received email\", ! filenames.isEmpty());\n        Pair<FileWrapper, EmailMessage> pendingEmail = bridge.getPendingEmail(filenames.get(0));\n        Assert.assertTrue(Arrays.equals(msg.serialize(), pendingEmail.right.serialize()));\n\n        Map<String, byte[]> receivedAttachmentsMap = new HashMap<>();\n        Attachment attachment = pendingEmail.right.attachments.get(0);\n        receivedAttachmentsMap.put(attachment.uuid, bridge.getOutgoingAttachment(attachment.uuid));\n        bridge.encryptAndMoveEmailToSent(pendingEmail.left, pendingEmail.right, receivedAttachmentsMap);\n\n        // detect that email's been sent and move to private folder\n        List<EmailMessage> sent = client.getNewSent().join();\n        Assert.assertTrue(! sent.isEmpty());\n        EmailMessage sentEmail2 = sent.get(0);\n        client.moveToPrivateSent(sentEmail2).join();\n        byte[] attachmentRetrieved =  client.getAttachment(sentEmail2.attachments.get(0).uuid).join();\n        String retrievedContent = new String(attachmentRetrieved);\n        Assert.assertTrue(retrievedContent.equals(attachmentContent));\n        Assert.assertTrue(client.getNewSent().join().isEmpty());\n\n        // receive an inbound email in bridge\n        String content2 = \"Inbound attachment text\";\n        byte[] content2Bytes = content2.getBytes();\n        Attachment attachment2 = bridge.uploadAttachment(\"inbound.txt\", content2Bytes.length, \"text/plain\", content2Bytes);\n\n        List<Attachment> inboundAttachments = Arrays.asList(attachment2);\n        EmailMessage inMsg = new EmailMessage(\"id2\", \"msgid\", \"alice@crypto.net\", \"what's up?\",\n                LocalDateTime.now(), Arrays.asList(\"ouremail@example.com\"), Collections.emptyList(), Collections.emptyList(),\n                \"content\", true, true, inboundAttachments, null,\n                Optional.empty(), Optional.empty(), Optional.empty());\n        bridge.addToInbox(inMsg);\n\n        // retrieve new message in client\n        List<EmailMessage> incoming = client.getNewIncoming().join();\n        Assert.assertTrue(\"received email\", ! incoming.isEmpty());\n        Assert.assertTrue(Arrays.equals(inMsg.serialize(), incoming.get(0).serialize()));\n\n        // decrypt and move incoming email to private folder\n        client.moveToPrivateInbox(incoming.get(0)).join();\n        byte[] attachmentRetrieved2 =  client.getAttachment(incoming.get(0).attachments.get(0).uuid).join();\n        String retrievedContent2 = new String(attachmentRetrieved2);\n        Assert.assertTrue(retrievedContent2.equals(content2));\n        Assert.assertTrue(client.getNewIncoming().join().isEmpty());\n    }\n\n    public static void chatMultipleInvites(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(random), password, network, crypto);\n        int otherMembersCount = 5;\n        List<String> passwords = IntStream.range(0, otherMembersCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, otherMembersCount, passwords);\n        UserContext b = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(a), shareeUsers);\n\n        Messenger msgA = new Messenger(a);\n        ChatController controllerA = msgA.createChat().join();\n        List<String> otherMembersUsernames = shareeUsers.stream().map(u -> u.username).collect(Collectors.toList());\n        List<PublicKeyHash> otherMembersPublicKeyHash = shareeUsers.stream().map(u -> u.signer.publicKeyHash).collect(Collectors.toList());\n\n        controllerA = msgA.invite(controllerA, otherMembersUsernames, otherMembersPublicKeyHash).join();\n        Set<String> allMemberNames = controllerA.getMemberNames();\n        Assert.assertTrue(\"all members\", allMemberNames.size() == otherMembersCount + 1);\n\n        List<MessageEnvelope> messages = controllerA.getMessages(0, 10).join();\n        List<MessageEnvelope> inviteMessages = messages.stream().filter(m -> m.payload.type() == Message.Type.Invite).collect(Collectors.toList());\n        Assert.assertTrue(\"all invites\", inviteMessages.size() == otherMembersCount);\n    }\n\n    public static void chat(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password));\n        UserContext b = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(a), shareeUsers);\n\n        Messenger msgA = new Messenger(a);\n        ChatController controllerA = msgA.createChat().join();\n        Assert.assertTrue(\"creator is admin\", controllerA.getAdmins().equals(Collections.singleton(a.username)));\n        controllerA = msgA.invite(controllerA, Arrays.asList(b.username), Arrays.asList(b.signer.publicKeyHash)).join();\n        List<Pair<SharedItem, FileWrapper>> feed = b.getSocialFeed().join().update().join().getSharedFiles(0, 10).join();\n        FileWrapper chatSharedDir = feed.get(feed.size() - 1).right;\n\n        Messenger msgB = new Messenger(b);\n        ChatController controllerB = msgB.cloneLocallyAndJoin(chatSharedDir).join();\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n\n        List<MessageEnvelope> initialMessages = controllerB.getMessages(0, 10).join();\n        Assert.assertEquals(initialMessages.size(), 4);\n        Assert.assertEquals(controllerA.host().messagesMergedUpto, 3);\n        Assert.assertEquals(controllerB.host().messagesMergedUpto, 4);\n\n        ApplicationMessage msg1 = ApplicationMessage.text(\"G'day mate!\");\n        controllerA = msgA.sendMessage(controllerA, msg1).join();\n        Assert.assertEquals(controllerA.host().messagesMergedUpto, 4);\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        List<MessageEnvelope> messages = controllerB.getMessages(0, 10).join();\n        Assert.assertEquals(messages.size(), 5);\n        MessageEnvelope fromA = messages.get(messages.size() - 1);\n        Assert.assertEquals(fromA.payload, msg1);\n        Assert.assertEquals(controllerB.host().messagesMergedUpto, 5);\n\n        ReplyTo msg2 = ReplyTo.build(fromA, ApplicationMessage.text(\"Isn't this cool!!\"), hasher).join();\n        controllerB = msgB.sendMessage(controllerB, msg2).join();\n        controllerA = msgA.mergeMessages(controllerA, b.username).join();\n        List<MessageEnvelope> messagesA = controllerA.getMessages(0, 10).join();\n        MessageEnvelope fromB = messagesA.get(5);\n        Assert.assertEquals(messagesA.size(), 6);\n        Assert.assertEquals(messagesA.get(messagesA.size() - 1).payload, msg2);\n        Assert.assertEquals(controllerA.host().messagesMergedUpto, 6);\n        Assert.assertEquals(controllerB.host().messagesMergedUpto, 6);\n        Assert.assertTrue(fromB.payload instanceof ReplyTo);\n        MessageRef parentRef = ((ReplyTo) fromB.payload).parent;\n        MessageEnvelope parent = controllerA.getMessageFromRef(parentRef, 4).join();\n        Assert.assertTrue(parent.equals(fromA));\n\n        // test setting group properties\n        String random_chat = \"Random chat\";\n        controllerA = msgA.setGroupProperty(controllerA, \"name\", random_chat).join();\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        String groupName = controllerB.getGroupProperty(\"name\");\n        Assert.assertTrue(groupName.equals(random_chat));\n        Assert.assertEquals(controllerA.host().messagesMergedUpto, 7);\n        Assert.assertEquals(controllerB.host().messagesMergedUpto, 7);\n\n        // make message log multi chunk\n        for (int i=0; i < 6; i++) {\n            ApplicationMessage msgn = ApplicationMessage.text(new String(new byte[1024 * 1024]));\n            controllerA = msgA.sendMessage(controllerA, msgn).join();\n        }\n\n        List<MessageEnvelope> last = controllerA.getMessages(12, 13).join();\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        controllerB.getMessages(12, 13).join();\n\n        // share a media file\n        byte[] media = \"Some media data\".getBytes();\n        AsyncReader reader = AsyncReader.build(media);\n        Pair<String, FileRef> mediaRef = msgA.uploadMedia(controllerA, reader, \"txt\", media.length, LocalDateTime.now(), x -> {}).join();\n        List<Content> content = Arrays.asList(new Reference(mediaRef.right), new Text(\"Check out this sunset!\"));\n        controllerA = msgA.sendMessage(controllerA, new ApplicationMessage(content)).join();\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        List<MessageEnvelope> withMediaMessage = controllerB.getMessages(0, 50).join();\n        MessageEnvelope mediaMessage = withMediaMessage.get(withMediaMessage.size() - 1);\n        Assert.assertTrue(mediaMessage.payload instanceof ApplicationMessage);\n        Optional<FileRef> ref = ((ApplicationMessage) mediaMessage.payload).body.stream().flatMap(c -> c.reference().stream()).findFirst();\n        Assert.assertTrue(\"Message with media ref present\", ref.isPresent());\n        FileRef fileRef = ref.get();\n        Optional<FileWrapper> mediaFile = a.getByPath(fileRef.path).join();\n        Assert.assertTrue(mediaFile.isPresent());\n\n        // remove member from chat\n        controllerA = msgA.removeMember(controllerA, b.username).join();\n        controllerA = msgA.sendMessage(controllerA, new ApplicationMessage(Arrays.asList(new Text(\"B shouldn't see this!\")))).join();\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        List<MessageEnvelope> all = controllerB.getMessages(0, 50).join();\n        Assert.assertEquals(all.size(), withMediaMessage.size());\n        Assert.assertTrue(controllerB.getMemberNames().size() == 1);\n\n        // recent messages\n        List<MessageEnvelope> recentA = controllerA.getRecent();\n        Assert.assertTrue(recentA.size() > 0);\n\n        // removal status\n        Member originalB = controllerA.getMember(b.username);\n        Id originalBId = originalB.id;\n        Assert.assertTrue(originalB.removed);\n\n        // reinvite member\n        controllerA = msgA.invite(controllerA, Arrays.asList(b.username), Arrays.asList(b.signer.publicKeyHash)).join();\n        Assert.assertTrue(! controllerA.getMember(b.username).removed);\n\n        // rejoin chat\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        Member newB = controllerB.getMember(b.username);\n        boolean bRemoved2 = newB.removed;\n        Assert.assertTrue(! bRemoved2);\n\n        Id newBId = newB.id;\n        Assert.assertTrue(! originalBId.equals(newBId));\n        PublicKeyHash newChatId = newB.chatIdentity.get().getAndVerifyOwner(b.signer.publicKeyHash, network.dhtClient).join();\n        PublicKeyHash oldChatId = originalB.chatIdentity.get().getAndVerifyOwner(b.signer.publicKeyHash, network.dhtClient).join();\n        Assert.assertTrue(\"New chat identity\", !newChatId.equals(oldChatId));\n    }\n\n    public static void concurrentChatMerges(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(random), password, network, crypto);\n        UserContext b = PeergosNetworkUtils.ensureSignedUp(\"b-\" + generateUsername(random), password, network, crypto);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(a), Arrays.asList(b));\n\n        Messenger msgA = new Messenger(a);\n        ChatController controllerA = msgA.createChat().join();\n        controllerA = msgA.invite(controllerA, Arrays.asList(b.username), Arrays.asList(b.signer.publicKeyHash)).join();\n\n        Pair<Messenger, ChatController> bInit = joinChat(b);\n        Messenger msgB = bInit.left;\n        ChatController controllerB = bInit.right;\n\n        controllerA = msgA.mergeAllUpdates(controllerA, a.getSocialState().join()).join();\n\n        ApplicationMessage msg1 = ApplicationMessage.text(\"G'day mate!\");\n        ApplicationMessage msg2 = ApplicationMessage.text(\"G'day again!\");\n        controllerA = msgA.sendMessage(controllerA, msg1).join();\n        controllerA = msgA.sendMessage(controllerA, msg2).join();\n\n        // B merges ne messages concurrently in two places\n        UserContext b2 = PeergosNetworkUtils.ensureSignedUp(b.username, password, network.clear(), crypto);\n        Messenger msgB2 = new Messenger(b2);\n        ChatController controllerB2 = msgB2.getChat(controllerB.chatUuid).join();\n        ForkJoinTask<?> concurrent = ForkJoinPool.commonPool().submit(() -> {\n            msgB2.mergeMessages(controllerB2, a.username).join();\n        });\n\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        concurrent.join();\n\n        if (network.dhtClient instanceof BufferedStorage)\n            ((BufferedStorage)network.dhtClient).clear();\n        UserContext b3 = PeergosNetworkUtils.ensureSignedUp(b.username, password, network.clear(), crypto);\n        Messenger msgB3 = new Messenger(b3);\n        ChatController controllerB3 = msgB3.getChat(controllerB.chatUuid).join();\n        List<MessageEnvelope> messages = controllerB3.getMessages(0, 20).join();\n        int msgCount = messages.stream().filter(m -> m.payload.equals(msg1)).collect(Collectors.toList()).size();\n        Assert.assertTrue(msgCount <= 1);\n    }\n\n    private static Pair<Messenger, ChatController> joinChat(UserContext c) {\n        List<Pair<SharedItem, FileWrapper>> feed = c.getSocialFeed().join().update().join().getSharedFiles(0, 10).join();\n        FileWrapper chatSharedDir = feed.stream()\n                .filter(p -> p.left.path.contains(\"/.messaging/\"))\n                .findAny().get().right;\n        Messenger msg = new Messenger(c);\n        ChatController controller = msg.cloneLocallyAndJoin(chatSharedDir).join();\n        return new Pair<>(msg, controller);\n    }\n\n    public static void memberLeaveAndDeleteChat(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(random), password, network, crypto);\n        UserContext b = PeergosNetworkUtils.ensureSignedUp(\"b-\" + generateUsername(random), password, network, crypto);\n        UserContext c = PeergosNetworkUtils.ensureSignedUp(\"c-\" + generateUsername(random), password, network, crypto);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(a, b), Arrays.asList(c));\n        friendBetweenGroups(Arrays.asList(a), Arrays.asList(b));\n\n        Messenger msgA = new Messenger(a);\n        ChatController controllerA = msgA.createChat().join();\n        controllerA = msgA.invite(controllerA, Arrays.asList(b.username), Arrays.asList(b.signer.publicKeyHash)).join();\n        controllerA = msgA.invite(controllerA, Arrays.asList(c.username), Arrays.asList(c.signer.publicKeyHash)).join();\n\n        Pair<Messenger, ChatController> bInit = joinChat(b);\n        Messenger msgB = bInit.left;\n        ChatController controllerB = bInit.right;\n\n        Pair<Messenger, ChatController> cInit = joinChat(c);\n        Messenger msgC = cInit.left;\n        ChatController controllerC = cInit.right;\n\n        controllerA = msgA.mergeAllUpdates(controllerA, a.getSocialState().join()).join();\n\n        ApplicationMessage msg1 = ApplicationMessage.text(\"G'day mate!\");\n        controllerA = msgA.sendMessage(controllerA, msg1).join();\n\n        controllerB = msgB.mergeMessages(controllerB, a.username).join();\n        List<MessageEnvelope> messages = controllerB.getMessages(0, 10).join();\n        MessageEnvelope fromA = messages.get(messages.size() - 1);\n        Assert.assertEquals(fromA.payload, msg1);\n\n        // C deletes their mirror\n        msgC.deleteChat(controllerC).join();\n\n        // B sends a message\n        ApplicationMessage msg2 = ApplicationMessage.text(\"You still here, A?\");\n        controllerB = msgB.sendMessage(controllerB, msg2).join();\n\n        controllerB = msgB.mergeAllUpdates(controllerB, b.getSocialState().join()).join();\n        Assert.assertTrue(controllerB.deletedMemberNames().contains(c.username));\n        controllerA = msgA.mergeAllUpdates(controllerA, a.getSocialState().join()).join();\n        List<MessageEnvelope> recentA = controllerA.getRecent();\n        Assert.assertTrue(recentA.stream().anyMatch(m -> m.payload.equals(msg2)));\n        Assert.assertEquals(controllerA.getMemberNames(), Stream.of(a.username, b.username).collect(Collectors.toSet()));\n    }\n\n    public static void editChatMessage(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n\n        UserContext a = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password));\n        UserContext b = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(a), shareeUsers);\n\n        Messenger msgA = new Messenger(a);\n        ChatController controllerA = msgA.createChat().join();\n        controllerA = msgA.invite(controllerA, Arrays.asList(b.username), Arrays.asList(b.signer.publicKeyHash)).join();\n\n        ApplicationMessage msg1 = ApplicationMessage.text(\"G'day mate!\");\n        controllerA = msgA.sendMessage(controllerA, msg1).join();\n\n        controllerA = msgA.mergeMessages(controllerA, a.username).join();\n        List<MessageEnvelope> messages = controllerA.getMessages(0, 10).join();\n        Assert.assertEquals(messages.size(), 4);\n        MessageEnvelope envelope = messages.get(messages.size()-1);\n\n        MessageRef messageRef = controllerA.generateHash(envelope).join();\n        String changedContent = \"edited\";\n        EditMessage editMessage = new EditMessage(messageRef, ApplicationMessage.text(changedContent));\n        controllerA = msgA.sendMessage(controllerA, editMessage).join();\n        controllerA = msgA.mergeMessages(controllerA, a.username).join();\n        messages = controllerA.getMessages(0, 10).join();\n        Assert.assertEquals(messages.size(), 5);\n        envelope = messages.get(messages.size()-1);\n        EditMessage appMsg = (EditMessage) envelope.payload;\n        String msgContent = appMsg.content.body.get(0).inlineText();\n        Assert.assertEquals(msgContent, changedContent);\n\n        messageRef = controllerA.generateHash(envelope).join();\n        DeleteMessage delMessage = new DeleteMessage(messageRef);\n        controllerA = msgA.sendMessage(controllerA, delMessage).join();\n        controllerA = msgA.mergeMessages(controllerA, a.username).join();\n        messages = controllerA.getMessages(0, 10).join();\n        Assert.assertEquals(messages.size(), 6);\n    }\n\n    public static void groupSharing(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network.clear(), random, 2, Arrays.asList(password, password));\n        UserContext friend = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n        String dirName = \"one\";\n        sharer.getUserRoot().join().mkdir(dirName, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        Path dirToShare1 = PathUtil.get(sharer.username, dirName);\n        SocialState social = sharer.getSocialState().join();\n        String friends = social.getFriendsGroupUid();\n        sharer.shareReadAccessWith(dirToShare1, Set.of(friends)).join();\n\n        FileSharedWithState fileSharedWithState = sharer.sharedWith(dirToShare1).join();\n        Assert.assertTrue(fileSharedWithState.readAccess.size() == 1);\n        Assert.assertTrue(fileSharedWithState.readAccess.contains(friends));\n\n        Optional<FileWrapper> dir = friend.getByPath(dirToShare1).join();\n        Assert.assertTrue(dir.isPresent());\n\n        Optional<FileWrapper> home = friend.getByPath(PathUtil.get(sharer.username)).join();\n        Assert.assertTrue(home.isPresent());\n\n        Optional<FileWrapper> dirViaGetChild = home.get().getChild(dirName, sharer.crypto.hasher, sharer.network).join();\n        Assert.assertTrue(dirViaGetChild.isPresent());\n\n        Set<FileWrapper> children = home.get().getChildren(sharer.crypto.hasher, friend.network).join();\n        Assert.assertTrue(children.size() > 1);\n\n        // remove friend, which should rotate all keys of things shared with the friends group\n        sharer.removeFollower(friend.username).join();\n\n        Optional<FileWrapper> dir2 = friend.getByPath(dirToShare1).join();\n        Assert.assertTrue(dir2.isEmpty());\n\n        // new friends\n        List<UserContext> newFriends = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext newFriend = newFriends.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), newFriends);\n\n        Optional<FileWrapper> dirForNewFriend = newFriend.getByPath(dirToShare1).join();\n        Assert.assertTrue(dirForNewFriend.isPresent());\n\n        UserContext oldFriend = shareeUsers.get(1);\n        Optional<FileWrapper> dirForOldFriend = oldFriend.getByPath(dirToShare1).join();\n        Assert.assertTrue(dirForOldFriend.isPresent());\n    }\n\n    public static void groupSharingToFollowers(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 2, Arrays.asList(password, password));\n        UserContext friend = shareeUsers.get(0);\n\n        // make others follow sharer\n        followBetweenGroups(Arrays.asList(sharer), shareeUsers);\n        String dir1 = \"one\";\n        sharer.getUserRoot().join().mkdir(dir1, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        Path dirToShare1 = PathUtil.get(sharer.username, dir1);\n        SocialState social = sharer.getSocialState().join();\n        String followers = social.getFollowersGroupUid();\n        sharer.shareReadAccessWith(dirToShare1, Set.of(followers)).join();\n\n        FileSharedWithState fileSharedWithState = sharer.sharedWith(dirToShare1).join();\n        Assert.assertTrue(fileSharedWithState.readAccess.size() == 1);\n        Assert.assertTrue(fileSharedWithState.readAccess.contains(followers));\n\n        Optional<FileWrapper> dir = friend.getByPath(dirToShare1).join();\n        Assert.assertTrue(dir.isPresent());\n\n        // remove friend, which should rotate all keys of things shared with the friends group\n        sharer.removeFollower(friend.username).join();\n\n        Optional<FileWrapper> dir2 = friend.getByPath(dirToShare1).join();\n        Assert.assertTrue(dir2.isEmpty());\n    }\n\n    public static void groupReadIndividualWrite(NetworkAccess network, Random random) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        String password = \"notagoodone\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network.clear(), random, 2, Arrays.asList(password, password));\n        UserContext friend = shareeUsers.get(0);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n        String dirName = \"one\";\n        sharer.getUserRoot().join().mkdir(dirName, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n\n        Path dirToShare1 = PathUtil.get(sharer.username, dirName);\n        SocialState social = sharer.getSocialState().join();\n        String friends = social.getFriendsGroupUid();\n        sharer.shareReadAccessWith(dirToShare1, Set.of(friends)).join();\n\n        FileSharedWithState fileSharedWithState = sharer.sharedWith(dirToShare1).join();\n        Assert.assertTrue(fileSharedWithState.readAccess.size() == 1);\n        Assert.assertTrue(fileSharedWithState.readAccess.contains(friends));\n\n        sharer.shareWriteAccessWith(dirToShare1, Set.of(friend.username)).join();\n\n        Optional<FileWrapper> dir = friend.getByPath(dirToShare1).join();\n        Assert.assertTrue(dir.isPresent() && dir.get().isWritable());\n\n        Optional<FileWrapper> home = friend.getByPath(PathUtil.get(sharer.username)).join();\n        Assert.assertTrue(home.isPresent());\n\n        Optional<FileWrapper> dirViaGetChild = home.get().getChild(dirName, sharer.crypto.hasher, sharer.network).join();\n        Assert.assertTrue(dirViaGetChild.isPresent() && dirViaGetChild.get().isWritable());\n    }\n\n    public static void groupAwareSharing(NetworkAccess network, Random random,\n                                         TriFunction<UserContext, Path, Set<String>, CompletableFuture<Snapshot>> shareFunction,\n                                         TriFunction<UserContext, Path, Set<String>, CompletableFuture<Snapshot>> unshareFunction,\n                                         TriFunction<UserContext, Path, FileSharedWithState, Integer> resultFunc) {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n        String password = \"notagoodone\";\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n        UserContext shareeFriend = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n        UserContext shareeFollower = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n\n        followBetweenGroups(Arrays.asList(sharer), Arrays.asList(shareeFollower));\n        friendBetweenGroups(Arrays.asList(sharer), Arrays.asList(shareeFriend));\n\n        String dir1 = \"one\";\n        sharer.getUserRoot().join().mkdir(dir1, sharer.network, false, sharer.mirrorBatId(), sharer.crypto).join();\n        Path dirToShare1 = PathUtil.get(sharer.username, dir1);\n        SocialState social = sharer.getSocialState().join();\n        String followers = social.getFollowersGroupUid();\n        String friends = social.getFriendsGroupUid();\n\n        shareFunction.apply(sharer, dirToShare1, Set.of(shareeFriend.username)).join();\n        shareFunction.apply(sharer, dirToShare1, Set.of(shareeFollower.username)).join();\n        shareFunction.apply(sharer, dirToShare1, Set.of(followers)).join();\n        shareFunction.apply(sharer, dirToShare1, Set.of(friends)).join();\n\n        FileSharedWithState fileSharedWithState = sharer.sharedWith(dirToShare1).join();\n        Assert.assertTrue(resultFunc.apply(sharer, dirToShare1, fileSharedWithState) == 4);\n\n        unshareFunction.apply(sharer, dirToShare1, Set.of(friends, followers)).join();\n\n        fileSharedWithState = sharer.sharedWith(dirToShare1).join();\n        Assert.assertTrue(resultFunc.apply(sharer, dirToShare1, fileSharedWithState) == 0);\n    }\n\n    public static List<Set<AbsoluteCapability>> getAllChildCapsByChunk(FileWrapper dir, NetworkAccess network) {\n        return getAllChildCapsByChunk(dir.getPointer().capability, dir.getPointer().fileAccess, dir.version, network);\n    }\n\n    public static List<Set<AbsoluteCapability>> getAllChildCapsByChunk(AbsoluteCapability cap,\n                                                                       CryptreeNode dir,\n                                                                       Snapshot inVersion,\n                                                                       NetworkAccess network) {\n        Set<NamedAbsoluteCapability> direct = dir.getDirectChildrenCapabilities(cap, inVersion, network).join();\n\n        Pair<byte[], Optional<Bat>> nextLoc = dir.getNextChunkLocation(cap.rBaseKey, Optional.empty(), cap.getMapKey(), cap.bat, null).join();\n        AbsoluteCapability nextChunkCap = cap.withMapKey(nextLoc.left, nextLoc.right);\n\n        PointerUpdate pointer = network.mutable.getPointerTarget(cap.owner, cap.writer,\n                network.dhtClient).join();\n        Snapshot version = new Snapshot(cap.writer,\n                WriterData.getWriterData(cap.owner, (Cid) pointer.updated.get(), pointer.sequence, network.dhtClient).join());\n\n        Optional<CryptreeNode> next = network.getMetadata(version.get(nextChunkCap.writer), nextChunkCap).join();\n        Set<AbsoluteCapability> directUnnamed = direct.stream().map(n -> n.cap).collect(Collectors.toSet());\n        if (! next.isPresent())\n            return Arrays.asList(directUnnamed);\n        return Stream.concat(Stream.of(directUnnamed), getAllChildCapsByChunk(nextChunkCap, next.get(), inVersion, network).stream())\n                .collect(Collectors.toList());\n    }\n\n    public static Set<AbsoluteCapability> getAllChildCaps(FileWrapper dir, NetworkAccess network) {\n        RetrievedCapability p = dir.getPointer();\n        AbsoluteCapability cap = p.capability;\n        return getAllChildCaps(cap, p.fileAccess, network);\n    }\n\n    public static Set<AbsoluteCapability> getAllChildCaps(AbsoluteCapability cap, CryptreeNode dir, NetworkAccess network) {\n        PointerUpdate pointer = network.mutable.getPointerTarget(cap.owner, cap.writer,\n                network.dhtClient).join();\n        return dir.getAllChildrenCapabilities(new Snapshot(cap.writer,\n                    WriterData.getWriterData(cap.owner, (Cid) pointer.updated.get(), pointer.sequence, network.dhtClient).join()), cap, crypto.hasher, network).join()\n                    .stream().map(n -> n.cap).collect(Collectors.toSet());\n    }\n\n    public static void shareFolderForWriteAccess(NetworkAccess sharerNode, NetworkAccess shareeNode, int shareeCount, Random random) throws Exception {\n        Assert.assertTrue(0 < shareeCount);\n\n        String sharerUsername = generateUsername(random);\n        String sharerPassword = generatePassword();\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(sharerUsername, sharerPassword, sharerNode, crypto);\n\n        List<String> shareePasswords = IntStream.range(0, shareeCount)\n                .mapToObj(i -> generatePassword())\n                .collect(Collectors.toList());\n        List<UserContext> shareeUsers = getUserContextsForNode(shareeNode, random, shareeCount, shareePasswords);\n\n        // friend sharer with others\n        friendBetweenGroups(Arrays.asList(sharer), shareeUsers);\n\n        // friends are now connected\n        // share a directory from u1 to the others\n        FileWrapper u1Root = sharer.getUserRoot().join();\n        String folderName = \"awritefolder\";\n        u1Root.mkdir(folderName, sharer.network, SymmetricKey.random(), Optional.of(Bat.random(crypto.random)), false, sharer.mirrorBatId(), crypto).join();\n        String path = PathUtil.get(sharerUsername, folderName).toString();\n        System.out.println(\"PATH \"+ path);\n\n        // file is uploaded, do the actual sharing\n        sharer.shareWriteAccessWith(PathUtil.get(path),\n                shareeUsers.stream()\n                        .map(c -> c.username)\n                        .collect(Collectors.toSet())).join();\n\n        // check each user can see the shared folder, and write to it\n        for (UserContext sharee : shareeUsers) {\n            FileWrapper sharedFolder = sharee.getByPath(sharer.username + \"/\" + folderName).join().orElseThrow(() -> new AssertionError(\"shared folder is present after sharing\"));\n            Assert.assertEquals(sharedFolder.getFileProperties().name, folderName);\n\n            sharedFolder.mkdir(sharee.username, shareeNode, false, sharedFolder.mirrorBatId(), crypto).join();\n        }\n\n        Set<FileWrapper> children = sharer.getByPath(path).join().get().getChildren(crypto.hasher, sharerNode).get();\n        Assert.assertTrue(children.size() == shareeCount);\n    }\n\n    public static void publicLinkToFile(Random random,\n                                        NetworkAccess writerNode,\n                                        NetworkAccess readerNode,\n                                        Runnable updatePkis) throws Exception {\n        String username = generateUsername(random);\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, writerNode, crypto);\n        updatePkis.run();\n        FileWrapper userRoot = context.getUserRoot().join();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[128*1024];\n        random.nextBytes(data);\n        long t1 = System.currentTimeMillis();\n        userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(data), false, 0, data.length, Optional.empty(),\n                true, context.network, crypto, () -> false, l -> {}).join();\n        long t2 = System.currentTimeMillis();\n        String path = \"/\" + username + \"/\" + filename;\n        FileWrapper file = context.getByPath(path).join().get();\n        String link = file.toLink();\n        UserContext linkContext = UserContext.fromSecretLink(link, readerNode, crypto).join();\n        String entryPath = linkContext.getEntryPath().join();\n        Assert.assertTrue(\"Correct entry path\", entryPath.equals(\"/\" + username));\n        Optional<FileWrapper> fileThroughLink = linkContext.getByPath(path).join();\n        Assert.assertTrue(\"File present through link\", fileThroughLink.isPresent());\n    }\n\n    public static void friendBetweenGroups(List<UserContext> a, List<UserContext> b) {\n        for (UserContext userA : a) {\n            for (UserContext userB : b) {\n                // send initial request\n                userA.sendFollowRequest(userB.username, SymmetricKey.random()).join();\n\n                // make sharer reciprocate all the follow requests\n                List<FollowRequestWithCipherText> sharerRequests = userB.processFollowRequests().join();\n                for (FollowRequestWithCipherText u1Request : sharerRequests) {\n                    AbsoluteCapability pointer = u1Request.req.entry.get().pointer;\n                    Assert.assertTrue(\"Read only capabilities are shared\", ! pointer.wBaseKey.isPresent());\n                    boolean accept = true;\n                    boolean reciprocate = true;\n                    userB.sendReplyFollowRequest(u1Request, accept, reciprocate).join();\n                }\n\n                // complete the friendship connection\n                userA.processFollowRequests().join();\n            }\n        }\n    }\n\n    public static void followBetweenGroups(List<UserContext> sharers, List<UserContext> followers) {\n        for (UserContext userA : sharers) {\n            for (UserContext userB : followers) {\n                // send initial request\n                userB.sendFollowRequest(userA.username, SymmetricKey.random()).join();\n\n                // make sharer reciprocate all the follow requests\n                List<FollowRequestWithCipherText> sharerRequests = userA.processFollowRequests().join();\n                for (FollowRequestWithCipherText u1Request : sharerRequests) {\n                    AbsoluteCapability pointer = u1Request.req.entry.get().pointer;\n                    Assert.assertTrue(\"Read only capabilities are shared\", ! pointer.wBaseKey.isPresent());\n                    boolean accept = true;\n                    boolean reciprocate = false;\n                    userA.sendReplyFollowRequest(u1Request, accept, reciprocate).join();\n                }\n\n                // complete the friendship connection\n                userB.processFollowRequests().join();\n            }\n        }\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) {\n        boolean isRegistered = network.isUsernameRegistered(username).join();\n        if (isRegistered)\n            return UserContext.signIn(username, password, UserTests::noMfa, network, crypto).join();\n        return UserContext.signUp(username, password, \"\", network, crypto).join();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/ProofOfWorkTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.util.*;\n\npublic class ProofOfWorkTests {\n    private static final Crypto crypto = Main.initCrypto();\n\n    @Test\n    public void validity() {\n        byte[] data = crypto.random.randomBytes(100);\n        // d<=21 takes < 1s, 22-24 take ~12s, 25 takes ~ 85s (all with native sha256, and single threaded)\n        // so probably 11 should be the default\n        for (int d=0; d < 25; d++) {\n            long t0 = System.currentTimeMillis();\n            ProofOfWork work = crypto.hasher.generateProofOfWork(d, data).join();\n            long t1 = System.currentTimeMillis();\n            System.out.println(\"Difficulty: \" + d + \" took \" + (t1 - t0) + \"ms\");\n            byte[] hash = crypto.hasher.sha256(ArrayOps.concat(work.prefix, data)).join();\n            Assert.assertTrue(ProofOfWork.satisfiesDifficulty(d, hash));\n        }\n    }\n\n    @Test\n    public void infiniteDifficulty() {\n        DifficultyGenerator rateLimiter = new DifficultyGenerator(System.currentTimeMillis(), 0);\n        int diff = rateLimiter.currentDifficulty();\n        Assert.assertTrue(diff == ProofOfWork.MAX_DIFFICULTY);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/QrCodeTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.fingerprint.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.util.*;\nimport peergos.shared.zxing.*;\nimport peergos.shared.zxing.common.*;\nimport peergos.shared.zxing.qrcode.*;\n\nimport javax.imageio.*;\nimport java.awt.geom.*;\nimport java.awt.image.*;\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\n\npublic class QrCodeTests {\n\n    private static final Crypto crypto = Main.initCrypto();\n\n    @Test\n    public void testSecretLinkQRCode() throws Exception {\n        String originalText = \"http://too.cool:8000/for/school.html\";\n        SecretLinkQRCode link = SecretLinkQRCode.generate(originalText);\n        byte[] bytes = link.getQrCodeData();\n\n        File file = new File(\"secret-link-qr-code.png\");\n        try(FileOutputStream fos = new FileOutputStream(file);\n            BufferedOutputStream bos = new BufferedOutputStream(fos)) {\n            bos.write(bytes);\n        }\n        BufferedImage bufferedImage = ImageIO.read(file);\n        int width = bufferedImage.getWidth();\n        int height = bufferedImage.getHeight();\n        int i=0;\n        int[] result = new int[width * height];\n        for (int row = 0; row < height; row++) {\n            for (int col = 0; col < width; col++) {\n                result[i++] = bufferedImage.getRGB(col, row);\n            }\n        }\n        String text = SecretLinkQRCode.decodeFromPixels(result, bufferedImage.getWidth(), bufferedImage.getHeight());\n        Assert.assertTrue(\"Round trip perfect scan\", text.equals(originalText));\n    }\n\n    @Test\n    public void invertable() throws Exception {\n        String originalText = \"Peergos is amazing! So many cool features!\";\n\n        QRCodeWriter writer = new QRCodeWriter();\n        BitMatrix result = writer.encode(originalText, BarcodeFormat.QR_CODE, 512, 512);\n        BufferedImage original = new BufferedImage(result.getWidth(), result.getHeight(), BufferedImage.TYPE_INT_ARGB);\n\n        for (int y = 0; y < result.getHeight(); y++) {\n            for (int x = 0; x < result.getWidth(); x++) {\n                if (result.get(x, y))\n                    original.setRGB(x, y, 0xff000000);\n                else\n                    original.setRGB(x, y, 0xffffffff);\n            }\n        }\n        File file = new File(\"qr-code.png\");\n        ImageIO.write(original, \"png\", file);\n\n        // now read back in\n        BufferedImage read = ImageIO.read(file);\n\n        int width = read.getWidth();\n        int height = read.getHeight();\n\n        Result decoded = decodeRGB(read);\n        String text = decoded.getText();\n        Assert.assertTrue(\"Round trip perfect scan\", text.equals(originalText));\n\n        // Now try a rotated and dilated image\n        int scaledHeight = (int) (height * 0.9);\n        int scaledWidth = (int) (width * 0.9);\n        AffineTransform transform = AffineTransform.getTranslateInstance((scaledHeight-scaledWidth)/2, (scaledWidth-scaledHeight)/2);\n        int degrees = 10;\n        transform.rotate(Math.toRadians(degrees), scaledWidth/2, scaledHeight/2);\n        transform.scale(.9, .9);\n        AffineTransformOp operation = new AffineTransformOp(transform, AffineTransformOp.TYPE_BICUBIC);\n        BufferedImage transformedImage = operation.createCompatibleDestImage(original, original.getColorModel());\n        BufferedImage transformed = operation.filter(original, transformedImage);\n        ImageIO.write(transformed, \"png\", new File(\"qr-code-transformed.png\"));\n\n        // now decode\n        Result fromTransform = decodeRGB(transformed);\n        Assert.assertTrue(\"Decode transformed scan\", fromTransform.getText().equals(originalText));\n    }\n\n    private static Result decodeRGB(BufferedImage in) throws Exception {\n        int width = in.getWidth();\n        int height = in.getHeight();\n        int[] pixels = in.getRGB(0, 0, width, height, null, 0, width);\n        // This source doesn't handle rotations or dilations\n        RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels);\n\n        BinaryBitmap readBitmap = new BinaryBitmap(new HybridBinarizer(source));\n        QRCodeReader reader = new QRCodeReader();\n        return reader.decode(readBitmap);\n    }\n\n    private static PublicKeyHash randomKeyHash(Random rnd) {\n        byte[] keyHash = new byte[32];\n        rnd.nextBytes(keyHash);\n        return new PublicKeyHash(new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, keyHash));\n    }\n\n    @Test\n    public void roundTripFingerPrint() throws Exception {\n        Random rnd = new Random(1);\n        String name1 = \"alice\";\n        String name2 = \"bob\";\n        PublicKeyHash aliceId = randomKeyHash(rnd);\n        PublicKeyHash aliceBox = randomKeyHash(rnd);\n        List<PublicKeyHash> aliceKeys = Arrays.asList(aliceId, aliceBox);\n        PublicKeyHash bobId = randomKeyHash(rnd);\n        PublicKeyHash bobBox = randomKeyHash(rnd);\n        List<PublicKeyHash> bobKeys = Arrays.asList(bobId, bobBox);\n\n        FingerPrint fingerPrint1 = FingerPrint.generate(name1, aliceKeys, name2, bobKeys, crypto.hasher);\n        FingerPrint fingerPrint2 = FingerPrint.generate(name2, bobKeys, name1, aliceKeys, crypto.hasher);\n\n        File file = new File(\"qr-code-bin.png\");\n        Files.write(PathUtil.get(\"qr-code-bin.png\"), fingerPrint1.getQrCodeData());\n\n        // now read back in\n        BufferedImage read = ImageIO.read(file);\n\n        int width = read.getWidth();\n        int height = read.getHeight();\n\n        // Now try a rotated and dilated image\n        int scaledHeight = (int) (height * 0.9);\n        int scaledWidth = (int) (width * 0.9);\n        AffineTransform transform = AffineTransform.getTranslateInstance((scaledHeight-scaledWidth)/2, (scaledWidth-scaledHeight)/2);\n        int degrees = 10;\n        transform.rotate(Math.toRadians(degrees), scaledWidth/2, scaledHeight/2);\n        transform.scale(.9, .9);\n        AffineTransformOp operation = new AffineTransformOp(transform, AffineTransformOp.TYPE_BICUBIC);\n        BufferedImage transformedImage = operation.createCompatibleDestImage(read, read.getColorModel());\n        BufferedImage transformed = operation.filter(read, transformedImage);\n        ImageIO.write(transformed, \"png\", new File(\"qr-code-transformed.png\"));\n\n        // now decode\n        Result fromTransform = decodeRGB(transformed);\n        FingerPrint decoded = FingerPrint.fromString(fromTransform.getText());\n\n        Assert.assertTrue(\"Fingerprints match\", fingerPrint2.matches(decoded));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/QuotaStoreTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\n\nimport java.util.*;\n\n@RunWith(Parameterized.class)\npublic class QuotaStoreTests {\n\n    private final JdbcQuotas store;\n\n    public QuotaStoreTests(JdbcQuotas store) {\n        this.store = store;\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        JdbcQuotas ram = JdbcQuotas.build(Main.buildEphemeralSqlite(), new SqliteCommands());\n        return Arrays.asList(new Object[][] {\n                {ram}\n        });\n    }\n\n    @Test\n    public void updatesAndCount() {\n        String bob = \"bob\";\n        long oneG = 1024 * 1024 * 1024L;\n        store.setQuota(bob, oneG);\n        Assert.assertTrue(store.getQuota(bob) == oneG);\n        store.setQuota(bob, 50 * oneG);\n        Assert.assertTrue(store.getQuota(bob) == 50 * oneG);\n\n        Assert.assertTrue(store.numberOfUsers() == 1);\n        store.setQuota(\"fred\", oneG);\n        Assert.assertTrue(store.numberOfUsers() == 2);\n    }\n\n    @Test\n    public void tokens() {\n        String token = \"bob\";\n        store.addToken(token);\n        Assert.assertTrue(store.hasToken(token));\n        store.removeToken(token);\n        Assert.assertFalse(store.hasToken(token));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/QuotaTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.util.Args;\nimport peergos.server.util.Threads;\nimport peergos.shared.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\n\nimport static peergos.server.tests.PeergosNetworkUtils.ensureSignedUp;\n\n@RunWith(Parameterized.class)\npublic class QuotaTests {\n\n    private static Args args = UserTests.buildArgs()\n            .with(\"useIPFS\", \"false\")\n            .with(\"quota-upload-limit-seconds\", \"1\")\n            .with(\"default-quota\", Long.toString(2 * 1024 * 1024));\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n    private static ServerProcesses server;\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public QuotaTests(Args args) throws Exception {\n        this.network = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][]{\n                {args}\n        });\n    }\n\n    @BeforeClass\n    public static void init() {\n        server = Main.PKI_INIT.main(args);\n    }\n\n    private String generateUsername() {\n        return \"test\" + Math.abs(random.nextInt() % 10000);\n    }\n\n    @Test\n    public void quota() throws Exception {\n        String username = generateUsername();\n        String password = \"badpassword\";\n\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper home = context.getByPath(PathUtil.get(username).toString()).get().get();\n        byte[] data = new byte[1024*1024];\n        random.nextBytes(data);\n        FileWrapper newHome = home.uploadOrReplaceFile(\"file-1\", new AsyncReader.ArrayBacked(data), data.length,\n                network, crypto, () -> false, x -> {}).get();\n\n        try {\n            byte[] bigger = new byte[3 * 1024 * 1024];\n            newHome.uploadOrReplaceFile(\"file-2\", new AsyncReader.ArrayBacked(bigger), bigger.length, network,\n                    crypto, () -> false, x -> {}).get();\n            Assert.fail(\"Quota wasn't enforced\");\n        } catch (Exception e) {}\n    }\n\n    @Test\n    public void deletionsReduceUsage() throws Exception {\n        String username = generateUsername();\n        String password = \"badpassword\";\n\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        byte[] data = new byte[1024 * 1024];\n        random.nextBytes(data);\n        for (int i=0; i < 5; i++) {\n            String filename = \"file-1\";\n            context.getUserRoot().join().uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                    network, crypto, () -> false, x -> {}).get();\n            Path filePath = PathUtil.get(username, filename);\n            FileWrapper file = context.getByPath(filePath).get().get();\n            file.remove(context.getUserRoot().join(), filePath, context).get();\n            Thread.sleep(2_000);\n        }\n    }\n\n    @Test\n    public void deletionAtQuota() throws Exception {\n        String username = generateUsername();\n        String password = \"badpassword\";\n\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper home = context.getByPath(PathUtil.get(username).toString()).get().get();\n        int used = context.getSpaceUsage(false).join().intValue();\n        // use within a few KiB of our quota, before deletion\n        byte[] data = new byte[2 * 1024 * 1024 - used - 16 * 1024];\n        random.nextBytes(data);\n        String filename = \"file-1\";\n        home = home.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                network, crypto, () -> false, x -> {}).join();\n        Path filePath = PathUtil.get(username, filename);\n        FileWrapper file = context.getByPath(filePath).join().get();\n        Thread.sleep(2_000);\n        file.remove(home, filePath, context).join();\n    }\n\n    @Ignore // Can always just increae their quota for now\n    @Test\n    public void deletionAfterExceedingQuota() throws Exception {\n        String username = generateUsername();\n        String password = \"badpassword\";\n\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper home = context.getByPath(PathUtil.get(username).toString()).get().get();\n        // signing up uses just under 32k and the quota is 2 MiB, so use close to our quota\n        int used = context.getSpaceUsage(false).join().intValue();\n        byte[] data = new byte[2 * 1024 * 1024 - used - 16 * 1024];\n        random.nextBytes(data);\n        String filename = \"file-1\";\n        home = home.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                network, crypto, () -> false, x -> {}).get();\n        Path filePath = PathUtil.get(username, filename);\n        FileWrapper file = context.getByPath(filePath).get().get();\n        Threads.sleep(2_000);\n        try {\n            home = home.uploadOrReplaceFile(\"file-2\", new AsyncReader.ArrayBacked(data), data.length,\n                    network, crypto, () -> false, x -> {}).get();\n            Assert.fail();\n        } catch (Exception e) {}\n        if (server.localApi.gc != null) {\n            server.localApi.gc.collect(x -> Futures.of(true));\n            server.localApi.gc.collect(x -> Futures.of(true));\n        }\n        file.remove(home, filePath, context).get();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RamPki.java",
    "content": "package peergos.server.tests;\n\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.corenode.OpLog;\nimport peergos.shared.corenode.UserPublicKeyLink;\nimport peergos.shared.crypto.ProofOfWork;\nimport peergos.shared.crypto.RequiredDifficulty;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.PaymentProperties;\nimport peergos.shared.storage.auth.BatWithId;\nimport peergos.shared.user.UserSnapshot;\nimport peergos.shared.util.Either;\nimport peergos.shared.util.Futures;\n\nimport java.io.IOException;\nimport java.time.LocalDateTime;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\n\nclass RamPki implements CoreNode {\n    final Map<PublicKeyHash, String> reverseLookup = new HashMap<>();\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username, UserPublicKeyLink chain, OpLog setupOperations, ProofOfWork proof, String token) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username, UserPublicKeyLink chain, ProofOfWork proof) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username, UserPublicKeyLink chain, OpLog setupOperations, byte[] signedSpaceRequest, ProofOfWork proof) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username, List<UserPublicKeyLink> chain, ProofOfWork proof, String token) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash owner) {\n        return Futures.of(reverseLookup.get(owner));\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username, List<UserPublicKeyLink> newChain, Multihash currentStorageId, Optional<BatWithId> mirrorBat, LocalDateTime latestLinkCountUpdate, long usage, boolean commitToPki) {\n        return null;\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return null;\n    }\n\n    @Override\n    public void close() throws IOException {\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RamUserTests.java",
    "content": "package peergos.server.tests;\n\nimport com.eatthepath.otp.*;\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.cli.CLI;\nimport peergos.server.crypto.hash.ScryptJava;\nimport peergos.server.tests.util.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.BlockCache;\nimport peergos.shared.storage.UnauthedCachingStorage;\nimport peergos.shared.storage.auth.BatId;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\n\nimport javax.crypto.spec.*;\nimport java.io.*;\nimport java.net.*;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.*;\nimport java.security.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.Supplier;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class RamUserTests extends UserTests {\n    private static Args args = buildArgs().with(\"useIPFS\", \"false\");\n    private final NetworkAccess alternativeNet1, alternativeNet2;\n\n    public RamUserTests(NetworkAccess network, UserService service, NetworkAccess alternativeNet1, NetworkAccess alternativeNet2) {\n        super(network, service);\n        this.alternativeNet1 = alternativeNet1;\n        this.alternativeNet2 = alternativeNet2;\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() throws Exception {\n        UserService service = Main.PKI_INIT.main(args).localApi;\n        // use actual http messager\n        ServerMessager.HTTP serverMessager = new ServerMessager.HTTP(new JavaPoster(new URI(\"http://localhost:\" + args.getArg(\"port\")).toURL(), false));\n        NetworkAccess network = NetworkAccess.buildBuffered(service.storage, service.bats, service.coreNode, service.account, service.mutable,\n                        5_000, service.social, service.controller, service.usage, serverMessager, crypto.hasher, Arrays.asList(\"peergos\"), false)\n                .withStorage(s -> new UnauthedCachingStorage(s, new NoopCache(), crypto.hasher));\n        NetworkAccess altNetwork1 = NetworkAccess.buildBuffered(service.storage, service.bats, service.coreNode, service.account, service.mutable,\n                        0, service.social, service.controller, service.usage, serverMessager, crypto.hasher, Arrays.asList(\"peergos\"), false)\n                .withStorage(s -> new UnauthedCachingStorage(s, new NoopCache(), crypto.hasher));\n        NetworkAccess altNetwork2 = NetworkAccess.buildBuffered(service.storage, service.bats, service.coreNode, service.account, service.mutable,\n                        0, service.social, service.controller, service.usage, serverMessager, crypto.hasher, Arrays.asList(\"peergos\"), false)\n                .withStorage(s -> new UnauthedCachingStorage(s, new NoopCache(), crypto.hasher));\n        return Arrays.asList(new Object[][] {\n                {network, service, altNetwork1, altNetwork2}\n        });\n    }\n\n    public static class NoopCache implements BlockCache {\n        @Override\n        public CompletableFuture<Boolean> put(Cid hash, byte[] data) {\n            return CompletableFuture.supplyAsync(() -> true);\n        }\n\n        @Override\n        public CompletableFuture<Optional<byte[]>> get(Cid hash) {\n            return CompletableFuture.supplyAsync(Optional::empty);\n        }\n\n        @Override\n        public boolean hasBlock(Cid hash) {\n            return false;\n        }\n\n        @Override\n        public CompletableFuture<Boolean> clear() {\n            return Futures.of(true);\n        }\n\n        @Override\n        public long getMaxSize() {\n            return 0;\n        }\n\n        @Override\n        public void setMaxSize(long maxSizeBytes) {\n\n        }\n    }\n\n    @Override\n    public Args getArgs() {\n        return args;\n    }\n\n    @AfterClass\n    public static void cleanup() {\n        try {Thread.sleep(2000);}catch (InterruptedException e) {}\n        Path peergosDir = args.fromPeergosDir(\"\", \"\");\n        System.out.println(\"Deleting \" + peergosDir);\n        deleteFiles(peergosDir.toFile());\n    }\n\n    private static boolean isWindows() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"windows\");\n    }\n    private static boolean isMacos() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"mac\");\n    }\n\n    @Test\n    public void mfa() throws Throwable {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        Assert.assertTrue(context.network.account.getSecondAuthMethods(username, context.signer).join().isEmpty());\n\n        TimeBasedOneTimePasswordGenerator totp = new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30L), 6, TotpKey.ALGORITHM);\n        TotpKey key = addTotpKey(context, totp);\n\n        List<MultiFactorAuthMethod> enabled = context.network.account.getSecondAuthMethods(username, context.signer).join();\n        Assert.assertTrue(enabled.size() == 1 && enabled.get(0).enabled);\n\n        // now try logging in again, now with mfa\n        testLoginRequiresTotp(username, password, network, totp, key);\n\n        // Now delete the second factor and login again without MFA\n        context.network.account.deleteSecondFactor(username, enabled.get(0).credentialId, context.signer).join();\n        Assert.assertTrue(context.network.account.getSecondAuthMethods(username, context.signer).join().isEmpty());\n        context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        // now add a new totp key\n        TotpKey key2 = addTotpKey(context, totp);\n        testLoginRequiresTotp(username, password, network, totp, key2);\n\n        // Now add a 3rd which should delete the old one\n        TotpKey key3 = addTotpKey(context, totp);\n        testLoginRequiresTotp(username, password, network, totp, key3);\n        // logging in with old totp key should fail\n        try {\n            testLoginRequiresTotp(username, password, network, totp, key2);\n            throw new Throwable(\"Shouldn't get here!\");\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        // test that the old totp is deleted when new one is enabled\n        Assert.assertTrue(context.network.account.getSecondAuthMethods(username, context.signer).join().size() == 1);\n    }\n\n    private static void testLoginRequiresTotp(String username,\n                                              String password,\n                                              NetworkAccess network,\n                                              TimeBasedOneTimePasswordGenerator totp,\n                                              TotpKey totpKey) {\n        AtomicBoolean usedMfa = new AtomicBoolean(false);\n        UserContext freshLogin = UserContext.signIn(username, password, req -> {\n            List<MultiFactorAuthMethod> totps = req.methods.stream().filter(m -> m.type == MultiFactorAuthMethod.Type.TOTP).collect(Collectors.toList());\n            if (totps.isEmpty())\n                throw new IllegalStateException(\"No supported 2 factor auth method! \" + req.methods);\n            MultiFactorAuthMethod method = totps.get(totps.size() - 1);\n            usedMfa.set(true);\n            try {\n                return Futures.of(new MultiFactorAuthResponse(method.credentialId, Either.a(totp.generateOneTimePasswordString(new SecretKeySpec(totpKey.key, TotpKey.ALGORITHM), Instant.now()))));\n            } catch (InvalidKeyException e) {\n                throw new RuntimeException(e);\n            }\n        }, network, crypto).join();\n        Assert.assertTrue(usedMfa.get());\n    }\n\n    @Test\n    public void appWriteInSecretLink() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        String dirName = \"someapp\";\n        context.getUserRoot().join()\n                .mkdir(\".apps\", network, false, context.mirrorBatId(), crypto).join();\n        context.getByPath(username + \"/.apps\").join().get()\n                .mkdir(dirName, network, false, context.mirrorBatId(), crypto).join();\n        LinkProperties link = context.createSecretLink(username + \"/.apps/\" + dirName, true, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        UserContext fromLink = UserContext.fromSecretLinkV2(link.toLinkString(context.signer.publicKeyHash), () -> Futures.of(\"\"), network, crypto).join();\n        App app = App.init(fromLink, dirName).join();\n        app.writeInternal(Paths.get(dirName), \"G'day mate!\".getBytes(StandardCharsets.UTF_8), null).join();\n    }\n\n    @Test\n    public void copybug() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        Path remoteRelativeDir = Paths.get(\"pandoc\",\"assets\");\n        String filename = \"data.dat\";\n        CLI.ProgressCreator progressCreator = (a, b, c) -> x -> {};\n        long fileSize1 = 31*1024*1024;\n        AsyncReader.ArrayBacked data1 = new AsyncReader.ArrayBacked(new byte[(int)fileSize1]);\n\n        FileWrapper.FileUploadProperties props1 = new FileWrapper.FileUploadProperties(filename, () -> data1,\n                (int) (fileSize1 >> 32), (int) fileSize1, Optional.empty(), Optional.empty(), true, true,\n                progressCreator.create(remoteRelativeDir, filename, Math.max(4096, fileSize1)));\n\n        String filename2 = \"index.html\";\n        long fileSize2 = 4294;\n        AsyncReader.ArrayBacked data2 = new AsyncReader.ArrayBacked(new byte[(int)fileSize2]);\n\n        FileWrapper.FileUploadProperties props2 = new FileWrapper.FileUploadProperties(filename2, () -> data2,\n                (int) (fileSize2 >> 32), (int) fileSize2, Optional.empty(), Optional.empty(), true, true,\n                progressCreator.create(remoteRelativeDir, filename2, Math.max(4096, fileSize2)));\n\n\n        List<FileWrapper.FileUploadProperties> files = new ArrayList<>();\n        files.add(props2);\n        files.add(props1);\n        FileWrapper.FolderUploadProperties folderProps = new FileWrapper.FolderUploadProperties(convert(remoteRelativeDir), files);\n        List<FileWrapper.FolderUploadProperties> folders = new ArrayList<>();\n        folders.add(folderProps);\n        context.getUserRoot().join().uploadSubtree(folders.stream(), context.mirrorBatId(), context.network, crypto, context.getTransactionService(), x -> Futures.of(true), f -> Futures.of(true), () -> true).join();\n\n        String appName = \"pandoc\";\n        String installAppFromFolder = context.username + \"/\" + appName;\n        peergos.shared.user.App.init(context, appName).join();\n        boolean result = copyAssetsFolder(context, appName, installAppFromFolder).join();\n        Assert.assertTrue(result);\n    }\n    private static CompletableFuture<Boolean> copyAssetsFolder(UserContext context, String appName, String installAppFromFolder) {\n        CompletableFuture<Boolean> future = peergos.shared.util.Futures.incomplete();\n        String appFolderPath = \"/\" + context.username + \"/.apps/\" + appName;\n        context.getByPath(installAppFromFolder + \"/assets\").thenApply(srcAssetsDirOpt -> {\n            if (srcAssetsDirOpt.isPresent()) {\n                context.getByPath(appFolderPath).thenApply(destAppDirOpt -> {\n                    srcAssetsDirOpt.get().copyTo(destAppDirOpt.get(), context)\n                            .thenApply(res -> {\n                                future.complete(true);\n                                return true;\n                            }).exceptionally(throwable -> {\n                                System.out.println(\"unable to copy app assets. error: \" + throwable.getMessage());\n                                future.complete(false);\n                                return false;\n                            });\n                    return null;\n                });\n            }else {\n                future.complete(false);\n            }\n            return null;\n        });\n        return future;\n    }\n\n    private static List<String> convert(Path p) {\n        List<String> res = new ArrayList<>();\n        for (int i=0; i < p.getNameCount(); i++)\n            res.add(p.getName(i).toString());\n        return res;\n    }\n\n    private static TotpKey addTotpKey(UserContext context, TimeBasedOneTimePasswordGenerator totp) throws Exception {\n        TotpKey totpKey = context.network.account.addTotpFactor(context.username, context.signer).join();\n        // User stores totp key in authenticator app via QR code\n\n        List<MultiFactorAuthMethod> disabled = context.network.account.getSecondAuthMethods(context.username, context.signer)\n                .join()\n                .stream()\n                .filter(t -> !t.enabled)\n                .collect(Collectors.toList());\n        Assert.assertTrue(disabled.isEmpty());\n\n        // need to verify once to enable the second factor\n        // (to guard against things like google authenticator which silently ignore the algorithm)\n        Key key = new SecretKeySpec(totpKey.key, TotpKey.ALGORITHM);\n\n        Instant now = Instant.now();\n        String clientCode = totp.generateOneTimePasswordString(key, now);\n        context.network.account.enableTotpFactor(context.username, totpKey.credentialId, clientCode, context.signer).join();\n        return totpKey;\n    }\n\n    @Test\n    public void concurrentModification() throws Exception {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context1 = PeergosNetworkUtils.ensureSignedUp(username, password, alternativeNet1, crypto);\n        Optional<BatId> mirrorBat = context1.mirrorBatId();\n        UserContext context2 = PeergosNetworkUtils.ensureSignedUp(username, password, alternativeNet2, crypto);\n\n        context1.getUserRoot().join().mkdir(\"dir1\", context1.network, false, mirrorBat, crypto).join();\n        context1.getUserRoot().join().mkdir(\"dir2\", context1.network, false, mirrorBat, crypto).join();\n\n        FileWrapper dir1 = context1.getByPath(Paths.get(username, \"dir1\")).join().get();\n\n        FileWrapper dir2 = context2.getByPath(Paths.get(username, \"dir2\")).join().get();\n\n        int KB = 1024;\n        dir1.uploadOrReplaceFile(\"file1\", AsyncReader.build(new byte[KB]), KB, context1.network,\n                crypto, () -> false, x -> {}).join();\n\n        dir2.uploadOrReplaceFile(\"file2\", AsyncReader.build(new byte[KB]), KB, context1.network,\n                crypto, () -> false, x -> {}).join();\n\n        FileWrapper file1 = context1.getByPath(Paths.get(username, \"dir1\", \"file1\")).join().get();\n        FileWrapper file2 = context2.getByPath(Paths.get(username, \"dir2\", \"file2\")).join().get();\n\n        int MB = 1024 * 1024;\n        CompletableFuture<FileWrapper> future = CompletableFuture.supplyAsync(() -> file1.overwriteFile(AsyncReader.build(new byte[MB]), MB, context1.network, crypto, x -> {Threads.sleep(1_000);}).join());\n        FileWrapper f2 = file2.overwriteFile(AsyncReader.build(new byte[MB]), MB, context2.network, crypto, x -> {Threads.sleep(1_000);}).join();\n        FileWrapper f1 = future.join();\n\n        FileWrapper updatedFile1 = context1.getByPath(username + \"/dir1/file1\", f1.version).join().get();\n        FileWrapper updatedFile2 = context2.getByPath(username + \"/dir2/file2\", f2.version).join().get();\n        Assert.assertEquals(MB, updatedFile1.getSize());\n        Assert.assertEquals(MB, updatedFile2.getSize());\n    }\n\n    @Test\n    public void publicWebHosting() throws Exception {\n        if (isWindows() || isMacos()) // Windows/MacOS doesn't allow localhost domains natively\n            return;\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        String dirName = \"website\";\n        context.getUserRoot().join().mkdir(dirName, context.network, false, context.mirrorBatId(), crypto).join();\n        byte[] data = \"<html><body><h1>You are AWESOME!</h1></body></html>\".getBytes();\n        context.getByPath(username + \"/\" + dirName).join().get()\n                .uploadOrReplaceFile(\"index.html\", AsyncReader.build(data), data.length, network, crypto, () -> false, x -> {}).join();\n        ProfilePaths.setWebRoot(context, \"/\" + username + \"/\" + dirName).join();\n        ProfilePaths.publishWebroot(context).join();\n\n        // start a gateway\n        Args a = Args.parse(new String[]{\n                \"-peergos-url\", \"http://localhost:\" + args.getInt(\"port\"),\n                \"-port\", \"9002\",\n                \"-listen-host\", \"localhost\",\n                \"-domain-suffix\", \".peergos.localhost:9002\"\n        });\n        PublicGateway publicGateway = Main.startGateway(a);\n\n        // retrieve website\n        byte[] retrieved = get(new URI(\"http://\" + username + \".peergos.localhost:9002\").toURL());\n        Assert.assertTrue(Arrays.equals(retrieved, data));\n\n        publicGateway.shutdown();\n    }\n\n    @Test\n    public void cleanupFailedUploads() throws Exception {\n        String username = generateUsername();\n        String password = \"terriblepassword\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        long initialUsage = context.getSpaceUsage(false).join();\n        int size = 100*1024*1024;\n        byte[] data = new byte[size];\n        int bufferSize = 20*1024*1024;\n        int throwAtIndex = size / bufferSize / 2 * bufferSize; // needs to be a multiple of the buffer size\n        AsyncReader thrower = new ThrowingStream(data, throwAtIndex);\n        FileWrapper txnDir = context.getByPath(Paths.get(username, UserContext.TRANSACTIONS_DIR_NAME)).join().get();\n        TransactionService txns = new NonClosingTransactionService(network, crypto, txnDir);\n        try {\n            FileWrapper.FileUploadProperties fileUpload = new FileWrapper.FileUploadProperties(\"somefile\", () -> thrower, 0, size, Optional.empty(), Optional.empty(), false, false, x -> {});\n            FileWrapper.FolderUploadProperties dirUploads = new FileWrapper.FolderUploadProperties(Arrays.asList(username), Arrays.asList(fileUpload));\n            userRoot.uploadSubtree(Stream.of(dirUploads), context.mirrorBatId(), network, crypto, txns, f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        } catch (Exception e) {}\n        try {\n            context.getUserRoot().join().uploadFileJS(\"anotherfile\", thrower, 0, size, false,\n                    context.mirrorBatId(), network, crypto, x -> {}, txns, f -> Futures.of(false)).join();\n        } catch (Exception e) {}\n        long usageAfterFail = context.getSpaceUsage(false).join();\n        while (usageAfterFail <= throwAtIndex) { // give server a chance to recalculate usage\n            Thread.sleep(2_000);\n            usageAfterFail = context.getSpaceUsage(false).join();\n        }\n        Assert.assertTrue(usageAfterFail > throwAtIndex);\n        context.cleanPartialUploads(t -> true).join();\n        long usageAfterCleanup = context.getSpaceUsage(false).join();\n        while (usageAfterCleanup >= initialUsage + 5000) {\n            Thread.sleep(1_000);\n            usageAfterCleanup = context.getSpaceUsage(false).join();\n        }\n        Assert.assertTrue(usageAfterCleanup < initialUsage + 5000); // TODO: investigate why 5000 more (open transactions in db referencing blocks?)\n    }\n\n    @Test\n    public void cleanupFailedUploadsInDifferentWritingSpace() throws Exception {\n        String username = generateUsername();\n        String password = \"terriblepassword\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        String subdir = \"subdir\";\n        userRoot.mkdir(subdir, network, false, Optional.empty(), crypto).join();\n        Path subdirPath = PathUtil.get(username, subdir);\n        FileWrapper subdirectory = context.getByPath(subdirPath).join().get();\n        // put sub directory in a new writing space\n        context.shareWriteAccessWith(subdirPath, Collections.emptySet()).join();\n\n        userRoot = context.getUserRoot().join();\n\n        long initialUsage = context.getSpaceUsage(false).join();\n        int size = 100*1024*1024;\n        byte[] data = new byte[size];\n        int bufferSize = 20*1024*1024;\n        int throwAtIndex = size / bufferSize / 2 * bufferSize; // needs to be a multiple of the buffer size\n        AsyncReader thrower = new ThrowingStream(data, throwAtIndex);\n        FileWrapper txnDir = context.getByPath(Paths.get(username, UserContext.TRANSACTIONS_DIR_NAME)).join().get();\n        TransactionService txns = new NonClosingTransactionService(network, crypto, txnDir);\n        try {\n            FileWrapper.FileUploadProperties fileUpload = new FileWrapper.FileUploadProperties(\"somefile\", () -> thrower, 0, size, Optional.empty(), Optional.empty(), false, false, x -> {});\n            FileWrapper.FolderUploadProperties dirUploads = new FileWrapper.FolderUploadProperties(Arrays.asList(subdir), Arrays.asList(fileUpload));\n            userRoot.uploadSubtree(Stream.of(dirUploads), context.mirrorBatId(), network, crypto, txns, f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        } catch (Exception e) {}\n        long usageAfterFail = context.getSpaceUsage(false).join();\n        if (usageAfterFail <= throwAtIndex) { // give server a chance to recalculate usage\n            Thread.sleep(2_000);\n            usageAfterFail = context.getSpaceUsage(false).join();\n        }\n        Assert.assertTrue(usageAfterFail > throwAtIndex);\n\n        // delete the new writing space\n        FileWrapper sub = context.getByPath(subdirPath).join().get();\n\n        sub.remove(context.getUserRoot().get(), subdirPath, context).join();\n        long usageAfterDelete = context.getSpaceUsage(false).join();\n        while (usageAfterDelete >= throwAtIndex) { // give server a chance to recalculate usage\n            Thread.sleep(2_000);\n            usageAfterDelete = context.getSpaceUsage(false).join();\n        }\n        Assert.assertTrue(usageAfterDelete < initialUsage);\n\n        // clean the partial upload\n        context.cleanPartialUploads(t -> true).join();\n        long usageAfterCleanup = context.getSpaceUsage(false).join();\n        Assert.assertTrue(usageAfterCleanup < usageAfterDelete);\n    }\n\n    @Test\n    public void moveToDescendant() throws Exception {\n        String username = generateUsername();\n        String password = \"terriblepassword\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        String parentName = \"parent\";\n        userRoot.mkdir(parentName, network, false, context.mirrorBatId(), crypto).join();\n        Path parentPath = Paths.get(username, parentName);\n        FileWrapper parent = context.getByPath(parentPath).join().get();\n        String childName = \"child\";\n        parent.mkdir(childName, network, false, context.mirrorBatId(), crypto).join();\n        parent = context.getByPath(parentPath).join().get();\n        FileWrapper child = context.getByPath(parentPath.resolve(childName)).join().get();\n        try {\n            parent.moveTo(child, parent, parentPath, context, () -> Futures.of(true)).join();\n            throw new RuntimeException(\"Should fail before here\");\n        } catch (CompletionException e) {}\n        context.getByPath(parentPath.resolve(childName)).join().get();\n    }\n\n    @Test\n    public void duplicateNameCutAndPaste() throws Exception {\n        String username = generateUsername();\n        String password = \"terriblepassword\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        String targetName = \"target\";\n        userRoot.mkdir(targetName, network, false, context.mirrorBatId(), crypto).join();\n        Path targetPath = Paths.get(username, targetName);\n        FileWrapper target = context.getByPath(targetPath).join().get();\n        byte[] orig = \"Some words are here\".getBytes();\n        String filename = \"test.txt\";\n        target.uploadOrReplaceFile(filename, AsyncReader.build(orig), orig.length, network, crypto, () -> false, x -> {}).join();\n\n        String sourceName = \"source\";\n        context.getUserRoot().join().mkdir(sourceName, network, false, context.mirrorBatId(), crypto).join();\n        FileWrapper source = context.getByPath(Paths.get(username, sourceName)).join().get();\n        byte[] different = \"hi\".getBytes();\n        source.uploadOrReplaceFile(filename, AsyncReader.build(different), different.length, network, crypto, () -> false, x -> {}).join();\n\n        FileWrapper toMove = context.getByPath(Paths.get(username, sourceName, filename)).join().get();\n        try {\n            target = context.getByPath(targetPath).join().get();\n            FileWrapper parent = context.getByPath(Paths.get(username, sourceName)).join().get();\n            toMove.moveTo(target, parent, Paths.get(username, sourceName, filename), context, () -> Futures.of(true)).join();\n            throw new RuntimeException(\"Should fail before here\");\n        } catch (CompletionException e) {}\n        target = context.getByPath(targetPath).join().get();\n        Set<FileWrapper> kids = target.getChildren(crypto.hasher, network).join();\n        Assert.assertTrue(kids.size() == 1);\n        byte[] data = Serialize.readFully(kids.stream().findFirst().get(), crypto, network).join();\n        Assert.assertArrayEquals(data, orig);\n    }\n\n    private static byte[] get(URL target) throws IOException {\n        HttpURLConnection conn = (HttpURLConnection) target.openConnection();\n        conn.setRequestMethod(\"GET\");\n        conn.setRequestProperty(\"Host\", target.getHost());\n\n        InputStream in = conn.getInputStream();\n        ByteArrayOutputStream resp = new ByteArrayOutputStream();\n\n        byte[] buf = new byte[4096];\n        int r;\n        while ((r = in.read(buf)) >= 0)\n            resp.write(buf, 0, r);\n        return resp.toByteArray();\n    }\n\n    @Test\n    public void bufferedReaderTest() throws Exception {\n\n        String username = \"test\";\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n\n        String filename = \"sintel.mp4\";\n        Random random = new Random(666);\n        byte[] fileData = new byte[14621544];\n        random.nextBytes(fileData);\n\n        FileWrapper userRoot2 = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(fileData), fileData.length,\n                context.network, context.crypto, () -> false, l -> {}).join();\n\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename)).join().get();\n        FileProperties props = file.getFileProperties();\n        int sizeHigh = props.sizeHigh();\n        int sizeLow = props.sizeLow();\n\n        int seekHi = 0;\n        //int seekLo = 0;\n        //int length = 1048576;\n\n        int seekLo = 786432;\n        int length = 5242880;\n        //file length = 14,621,544\n        final int maxBlockSize = 1024 * 1024 * 5;\n\n        List<byte[]> resultBytes = new ArrayList<>();\n        boolean result = file.getBufferedInputStream(network, crypto, sizeHigh, sizeLow, 4, l -> {}).thenCompose(reader -> {\n            return reader.seekJS(seekHi, seekLo).thenApply(seekReader -> {\n                final int blockSize = length > maxBlockSize ? maxBlockSize : length;\n                return pump(seekReader, length, blockSize, resultBytes);\n            });\n        }).join().join();\n\n        List<byte[]> resultBytes2 = new ArrayList<>();\n        boolean result2 = file.getInputStream(network, crypto, sizeHigh, sizeLow, l -> {}).thenCompose(reader -> {\n            return reader.seekJS(seekHi, seekLo).thenApply(seekReader -> {\n                final int blockSize = length > maxBlockSize ? maxBlockSize : length;\n                return pump(seekReader, length, blockSize, resultBytes2);\n            });\n        }).join().join();\n        compare(resultBytes, resultBytes2);\n    }\n\n    @Test\n    public void bufferedReaderSeek() {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n\n        String filename = \"data.bin\";\n        Random random = new Random(666);\n        byte[] fileData = new byte[20*1024*1024];\n        random.nextBytes(fileData);\n\n        FileWrapper userRoot2 = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(fileData), fileData.length,\n                context.network, context.crypto, () -> false, l -> {}).join();\n\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename)).join().get();\n        FileProperties props = file.getFileProperties();\n        int sizeHigh = props.sizeHigh();\n        int sizeLow = props.sizeLow();\n\n        int seekHi = 0;\n        int seekLo = 10*1024*1024;\n        int length = 5242880;\n        final int maxBlockSize = 1024 * 1024 * 5;\n\n        List<byte[]> resultBytes = new ArrayList<>();\n        AsyncReader reader = file.getBufferedInputStream(network, crypto, sizeHigh, sizeLow, 4, l -> {}).join();\n        reader.readIntoArray(new byte[1024*1024], 0, 1024*1024).join();\n        reader.seekJS(seekHi, seekLo).thenApply(seekReader -> {\n            final int blockSize = length > maxBlockSize ? maxBlockSize : length;\n            return pump(seekReader, length, blockSize, resultBytes);\n        }).join();\n\n        List<byte[]> resultBytes2 = new ArrayList<>();\n        resultBytes2.add(Arrays.copyOfRange(fileData, seekLo, seekLo + maxBlockSize));\n        compare(resultBytes, resultBytes2);\n    }\n\n    @Test\n    public void testReuseOfAsyncReader() throws Exception {\n\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n\n        String filename = \"sintel.mp4\";\n        Random random = new Random(666);\n        byte[] fileData = new byte[14621544];\n        random.nextBytes(fileData);\n        FileWrapper userRoot2 = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(fileData), fileData.length,\n                context.network, context.crypto, () -> false, l -> {}).join();\n\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename)).join().get();\n        FileProperties props = file.getFileProperties();\n        int sizeHigh = props.sizeHigh();\n        int sizeLow = props.sizeLow();\n\n        final int maxBlockSize = 1024 * 1024 * 5;\n        final int fileLength = sizeLow;\n        AsyncReader reader = file.getBufferedInputStream(network, crypto, sizeHigh, sizeLow, 2, l -> {}).join();\n        int seekHi = 0;\n        int seekLo = 0;\n        int length = 1 * 1024 * 1024;\n        reader = reuseExistingReader(reader, file, sizeHigh, sizeLow, seekHi, seekLo, length, maxBlockSize, false);\n\n        seekLo = fileLength - (1024 * 1024 * 1);\n        length = fileLength - seekLo;\n        reader = reuseExistingReader(reader, file, sizeHigh, sizeLow, seekHi, seekLo, length, maxBlockSize, false);\n        System.currentTimeMillis();\n\n        seekHi = 0;\n        seekLo = 0;\n        length = fileLength;\n        reader = reuseExistingReader(reader, file, sizeHigh, sizeLow, seekHi, seekLo, length, maxBlockSize, false);\n        System.currentTimeMillis();\n    }\n\n    @Test\n    public void testReuseOfAsyncReaderSerialRead() throws Exception {\n\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n\n        String filename = \"sintel.mp4\";\n        Random random = new Random(666);\n        byte[] fileData = new byte[14621544];\n        random.nextBytes(fileData);\n\n        FileWrapper userRoot2 = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(fileData), fileData.length,\n                context.network, context.crypto, () -> false, l -> {}).join();\n\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename)).join().get();\n        FileProperties props = file.getFileProperties();\n        int sizeHigh = props.sizeHigh();\n        int sizeLow = props.sizeLow();\n\n        final int maxBlockSize = 1024 * 1024 * 5;\n        AsyncReader reader = file.getBufferedInputStream(network, crypto, sizeHigh, sizeLow, 2, l -> {}).join();\n        int seekHi = 0;\n        int seekLo = 0;\n        int length = maxBlockSize;\n        reader = reuseExistingReader(reader, file, sizeHigh, sizeLow, seekHi, seekLo, length, maxBlockSize, false);\n\n        seekLo = maxBlockSize;\n        length = maxBlockSize;\n        reader = reuseExistingReader(reader, file, sizeHigh, sizeLow, seekHi, seekLo, length, maxBlockSize, true);\n        System.currentTimeMillis();\n    }\n\n    private AsyncReader reuseExistingReader(AsyncReader reader, FileWrapper file, int sizeHigh, int sizeLow,\n                                           int seekHi, int seekLo, int length, int maxBlockSize, boolean serialAccess) throws Exception {\n        List<AsyncReader> currentAsyncReader = new ArrayList<>();\n        currentAsyncReader.add(reader);\n        List<byte[]> resultBytes2 = new ArrayList<>();\n        boolean result2 = file.getInputStream(network, crypto, sizeHigh, sizeLow, l -> {\n        }).thenCompose(reader2 -> {\n            return reader2.seekJS(seekHi, seekLo).thenApply(seekReader -> {\n                final int blockSize = length > maxBlockSize ? maxBlockSize : length;\n                return pump(seekReader, length, blockSize, resultBytes2);\n            });\n        }).join().join();\n\n        List<byte[]> resultBytes3 = new ArrayList<>();\n\n        boolean result3 = reader.seekJS(seekHi, seekLo).thenApply(seekReader -> {\n            if(serialAccess && reader != seekReader) {\n                throw new Error(\"Expecting reader reuse!\");\n            }\n            currentAsyncReader.remove(0);\n            currentAsyncReader.add(seekReader);\n            final int blockSize = length > maxBlockSize ? maxBlockSize : length;\n            return pump(currentAsyncReader.get(0), length, blockSize, resultBytes3);\n        }).join().join();\n\n        compare(resultBytes2, resultBytes3);\n        return currentAsyncReader.get(0);\n    }\n\n    private void compare(List<byte[]> resultBytes, List<byte[]> resultBytes2 ) {\n        if(resultBytes.size() != resultBytes2.size()) {\n            throw new Error(\"wrong!\");\n        }\n        for(int i=0; i < resultBytes.size(); i++) {\n            byte[] result1 = resultBytes.get(i);\n            byte[] result2 = resultBytes2.get(i);\n            if(result1.length != result2.length) {\n                throw new Error(\"wrong!\");\n            }\n            for(int j=0; j < result1.length; j++) {\n                if(result1[j] != result2[j]) {\n                    throw new Error(\"wrong!\");\n                }\n            }\n        }\n        System.currentTimeMillis();\n    }\n\n    private CompletableFuture<Boolean> pump(AsyncReader reader, Integer currentSize, Integer blockSize, List<byte[]> resultBytes) {\n        final int maxBlockSize = 1024 * 1024 * 5;\n        if(blockSize > 0) {\n            byte[] data = new byte[blockSize];\n            return reader.readIntoArray(data, 0, blockSize).thenCompose(read -> {\n                int newCurrentSize = currentSize - read;\n                int newBlockSize = newCurrentSize > maxBlockSize ? maxBlockSize : newCurrentSize;\n                resultBytes.add(data);\n                return pump(reader, newCurrentSize, newBlockSize, resultBytes);\n            });\n        } else {\n            CompletableFuture<Boolean> future = Futures.incomplete();\n            future.complete(true);\n            return future;\n        }\n    }\n\n    @Test\n    public void revokeWriteAccessToTree() throws Exception {\n        String username1 = generateUsername();\n        String password = \"test\";\n        UserContext user1 = PeergosNetworkUtils.ensureSignedUp(username1, password, network, crypto);\n        FileWrapper user1Root = user1.getUserRoot().join();\n\n        String folder1 = \"folder1\";\n        user1Root.mkdir(folder1, user1.network, false, user1.mirrorBatId(), crypto).join();\n\n        String folder11 = \"folder1.1\";\n        user1.getByPath(PathUtil.get(username1, folder1)).join().get()\n                .mkdir(folder11, user1.network, false, user1.mirrorBatId(), crypto).join();\n\n        String filename = \"somedata.txt\";\n        // write empty file\n        byte[] data = new byte[0];\n        user1.getByPath(PathUtil.get(username1, folder1, folder11)).join().get()\n                .uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, user1.network,\n                crypto, () -> false, l -> {}).join();\n\n        // create 2nd user and friend user1\n        String username2 = generateUsername();\n        UserContext user2 = PeergosNetworkUtils.ensureSignedUp(username2, password, network, crypto);\n        user2.sendInitialFollowRequest(username1).join();\n        List<FollowRequestWithCipherText> incoming = user1.getSocialState().join().pendingIncoming;\n        user1.sendReplyFollowRequest(incoming.get(0), true, true).join();\n        user2.getSocialState().join();\n\n        user1.shareWriteAccessWith(PathUtil.get(username1, folder1), Collections.singleton(username2)).join();\n\n        user1.unShareWriteAccess(PathUtil.get(username1, folder1), username2).join();\n        // check user1 can still log in\n        UserContext freshUser1 = PeergosNetworkUtils.ensureSignedUp(username1, password, network, crypto);\n    }\n\n    @Test\n    public void secretLinkV2() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext user = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        boolean writable = false;\n        String filename = \"somedata.txt\";\n        Path filePath = null;\n        SecretLink link = null;\n\n        for (int i=0; i < 3; i++) {\n            FileWrapper userRoot = user.getUserRoot().join();\n\n            String subdir1 = \"subdir\" + i;\n            userRoot.mkdir(subdir1, network, false, user.mirrorBatId(), crypto).join();\n\n            // write empty file\n            byte[] data = new byte[1025 * 1024 * 5];\n            user.getByPath(Paths.get(username, subdir1)).join().get().uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, user.network,\n                    crypto, () -> false, l -> {}).join();\n\n            filePath = PathUtil.get(username, subdir1, filename);\n\n            Optional<LocalDateTime> expiry = Optional.of(LocalDateTime.now().plusDays(1));\n            Optional<Integer> maxRetrievals = Optional.of(2);\n\n            String userPassword = \"youre-terrible-muriel\";\n            LinkProperties linkProps = user.createSecretLink(filePath.toString(), writable, expiry, maxRetrievals, userPassword, false).join();\n            link = linkProps.toLink(userRoot.owner());\n\n            EncryptedCapability retrieved = network.getSecretLink(link).join();\n            AbsoluteCapability cap = retrieved.decryptFromPassword(link.labelString(), link.linkPassword + userPassword, crypto).join();\n            FileWrapper resolvedFile = network.getFile(cap, username).join().get();\n            Assert.assertTrue(resolvedFile.isWritable() == writable);\n        }\n\n        SharedWithState sharingState = user.getDirectorySharingState(filePath.getParent()).join();\n        Assert.assertTrue(sharingState.hasLink(filename));\n        LinkProperties props = sharingState.get(filename).links.stream().findFirst().get();\n\n        // try changing the password\n        String newPass = \"different\";\n        user.updateSecretLink(filePath.toString(), new LinkProperties(props.label, props.linkPassword, newPass, writable, props.maxRetrievals, props.expiry, props.open, props.existing)).join();\n\n        UserContext.fromSecretLinkV2(link.toLink(), () -> Futures.of(newPass), network, crypto).join();\n        try {\n            UserContext.fromSecretLinkV2(link.toLink(), () -> Futures.of(newPass), network, crypto).join();\n            throw new RuntimeException(\"Shouldn't get here\");\n        } catch (IllegalStateException expected) {}\n\n        user.deleteSecretLink(link.label, filePath, writable).join();\n\n        try {\n            network.getSecretLink(link).join();\n            throw new RuntimeException(\"Shouldn't get here\");\n        } catch (IllegalStateException expected) {}\n\n        // now a writable secret link\n        String wpass = \"modifyme\";\n        LinkProperties writeLink = user.createSecretLink(filePath.toString(), true, Optional.empty(), Optional.empty(), wpass, false).join();\n        UserContext writableContext = UserContext.fromSecretLinkV2(writeLink.toLinkString(user.signer.publicKeyHash), () -> Futures.of(wpass), network, crypto).join();\n        FileWrapper wf = writableContext.getByPath(filePath).join().get();\n        Assert.assertTrue(wf.isWritable());\n\n        // test creating a secret link from a fresh login\n        LinkProperties dirlink = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto)\n                .createSecretLink(filePath.getParent().toString(), writable, Optional.empty(), Optional.empty(), \"\", false).join();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RateLimitTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.util.SlidingWindowCounter;\n\npublic class RateLimitTests {\n\n    @Test\n    public void maxRequestsInWindow() {\n        Clock clock = new Clock(System.currentTimeMillis());\n        SlidingWindowCounter limiter = new SlidingWindowCounter(20, 100, clock::now);\n        Assert.assertTrue(limiter.allowRequest(100));\n        Assert.assertFalse(limiter.allowRequest(1));\n    }\n\n    @Test\n    public void previousWindow() {\n        Clock clock = new Clock(System.currentTimeMillis());\n        int windowSizeInSeconds = 20;\n        SlidingWindowCounter limiter = new SlidingWindowCounter(windowSizeInSeconds, 100, clock::now);\n        Assert.assertTrue(limiter.allowRequest(100));\n        clock.addTime(windowSizeInSeconds * 3 / 2 * 1000);\n        Assert.assertTrue(limiter.allowRequest(50));\n        Assert.assertFalse(limiter.allowRequest(1));\n        clock.addTime(windowSizeInSeconds / 2 * 1000);\n        Assert.assertTrue(limiter.allowRequest(50));\n        Assert.assertFalse(limiter.allowRequest(1));\n    }\n\n    static class Clock {\n        private long time;\n\n        public Clock(long time) {\n            this.time = time;\n        }\n\n        public synchronized long now() {\n            return time/1000;\n        }\n\n        public synchronized void addTime(long delta) {\n            time += delta;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RateMonitorTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.util.*;\n\npublic class RateMonitorTests {\n\n    @Test\n    public void linear() {\n        RateMonitor rates = new RateMonitor(10);\n        for (int i=0; i < 1L << 20; i++) {\n            rates.addEvent();\n            rates.timeStep();\n        }\n        long[] linear = rates.getRates();\n        Assert.assertTrue(\"Powers of two\", linear[0] == 0);\n        for (int i=1; i < 10; i++)\n            Assert.assertTrue(\"Powers of two\", linear[i] == 1L << i - 1);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RequestCountTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.display.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\n\nimport static peergos.server.tests.PeergosNetworkUtils.*;\n\npublic class RequestCountTests {\n\n    private static final Crypto crypto = Main.initCrypto();\n    private static Args args = UserTests.buildArgs();\n    private static UserService service;\n    private Random random = new Random();\n    private final NetworkAccess network;\n    private final RequestCountingStorage storageCounter;\n\n    public RequestCountTests() {\n        RequestCountingStorage requestCounter = new RequestCountingStorage(service.storage);\n        this.storageCounter = requestCounter;\n        CachingVerifyingStorage dhtClient = new CachingVerifyingStorage(requestCounter, 50 * 1024, 1_000, service.storage.ids().join(), crypto.hasher);\n\n        BufferedStorage blockBuffer = new BufferedStorage(dhtClient, hasher);\n        MutablePointers unbufferedMutable = new CachingPointers(service.mutable, 7_000);\n        BufferedPointers mutableBuffer = new BufferedPointers(unbufferedMutable);\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutableBuffer, blockBuffer, hasher);\n        MutableTree tree = new MutableTreeImpl(mutableBuffer, blockBuffer, hasher, synchronizer);\n\n        int bufferSize = 20 * 1024 * 1024;\n        this.network = new BufferedNetworkAccess(blockBuffer, mutableBuffer, bufferSize, service.coreNode, service.account, service.social,\n                unbufferedMutable, service.bats, Optional.empty(), tree, synchronizer, service.controller, service.usage,\n                service.serverMessages, hasher, Arrays.asList(\"peergos\"), false);\n    }\n\n    @BeforeClass\n    public static void init() {\n        service = Main.PKI_INIT.main(args).localApi;\n    }\n\n    @Test\n    public void socialFeedRequestCount() {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n        String password = \"notagoodone\";\n\n        storageCounter.reset();\n        UserContext sharer = PeergosNetworkUtils.ensureSignedUp(generateUsername(random), password, network, crypto);\n        Assert.assertTrue(\"signup request count: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 32);\n\n        storageCounter.reset();\n        PeergosNetworkUtils.ensureSignedUp(sharer.username, password, network, crypto);\n        Assert.assertTrue(\"login request count: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 5);\n\n        List<UserContext> shareeUsers = getUserContextsForNode(network, random, 1, Arrays.asList(password, password));\n        UserContext a = shareeUsers.get(0);\n\n        // initialize friend and follower groups\n        a.getGroupNameMappings().join();\n        // friend sharer with other user\n        storageCounter.reset();\n        // send initial request\n        sharer.sendFollowRequest(a.username, SymmetricKey.random()).join();\n        Assert.assertTrue(\"send initial followrequest: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 25);\n\n        // make sharer reciprocate all the follow requests\n        storageCounter.reset();\n        List<FollowRequestWithCipherText> sharerRequests = a.processFollowRequests().join();\n        Assert.assertTrue(\"friending 2 users: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 3);\n        storageCounter.reset();\n        for (FollowRequestWithCipherText u1Request : sharerRequests) {\n            AbsoluteCapability pointer = u1Request.req.entry.get().pointer;\n            Assert.assertTrue(\"Read only capabilities are shared\", ! pointer.wBaseKey.isPresent());\n            boolean accept = true;\n            boolean reciprocate = true;\n            a.sendReplyFollowRequest(u1Request, accept, reciprocate).join();\n        }\n        Assert.assertTrue(\"send reply follow request: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 33);\n\n        // complete the friendship connection\n        storageCounter.reset();\n        sharer.processFollowRequests().join();\n        Assert.assertTrue(\"friending complete: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 47);\n\n        // friends are now connected\n        // share a file from u1 to u2\n        byte[] fileData = new byte[1*1024*1024];\n        random.nextBytes(fileData);\n        Path file1 = PathUtil.get(sharer.username, \"first-file.txt\");\n        uploadAndShare(fileData, file1, sharer, a.username);\n\n        // check 'a' can see the shared file in their social feed\n        storageCounter.reset();\n        SocialFeed feed = a.getSocialFeed().join();\n        Assert.assertTrue(\"initialise social feed: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 38);\n        int feedSize = 2;\n\n        storageCounter.reset();\n        List<SharedItem> items = feed.getShared(feedSize, feedSize + 1, a.crypto, a.network).join();\n        Assert.assertTrue(storageCounter.requestTotal() <= 2);\n\n        storageCounter.reset();\n        a.getFiles(items).join();\n        Assert.assertTrue(storageCounter.requestTotal() <= 0);\n\n        SocialState social = sharer.getSocialState().join();\n        String friends = social.getFriendsGroupUid();\n        SocialFeed sharerFeed = sharer.getSocialFeed().join().update().join();\n        { // Do an initial post to ensure all directories are created\n            List<Text> postBody = Arrays.asList(new Text(\"Initial post.\"));\n            SocialPost post = SocialPost.createInitialPost(sharer.username, postBody, SocialPost.Resharing.Friends);\n            Pair<Path, FileWrapper> p = sharerFeed.createNewPost(post).join();\n            sharer.shareReadAccessWith(p.left, Set.of(friends)).join();\n        }\n        storageCounter.reset();\n        {\n            List<Text> postBody = Arrays.asList(new Text(\"G'day, skip!\"));\n            SocialPost post = SocialPost.createInitialPost(sharer.username, postBody, SocialPost.Resharing.Friends);\n            Pair<Path, FileWrapper> p = sharerFeed.createNewPost(post).join();\n            sharer.shareReadAccessWith(p.left, Set.of(friends)).join();\n        }\n        Assert.assertTrue(\"Adding a post to social feed: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 10);\n        a.getSocialFeed().join().update().join();\n\n        // share more items\n        for (int i=0; i < 5; i++) {\n            byte[] data = new byte[1*1024*1024];\n            random.nextBytes(data);\n            Path file = PathUtil.get(sharer.username, random.nextInt() + \"first-file.txt\");\n            uploadAndShare(data, file, sharer, a.username);\n        }\n\n        storageCounter.reset();\n        SocialFeed feed2 = a.getSocialFeed().join().update().join();\n        Assert.assertTrue(\"load 5 items in social feed: \" + storageCounter.requestTotal(), storageCounter.requestTotal() <= 26);\n\n        storageCounter.reset();\n        List<SharedItem> items2 = feed2.getShared(feedSize + 1, feedSize + 6, a.crypto, a.network).join();\n        Assert.assertTrue(storageCounter.requestTotal() <= 1);\n    }\n\n    private static void uploadAndShare(byte[] data, Path file, UserContext sharer, String sharee) {\n        String filename = file.getFileName().toString();\n        sharer.getByPath(file.getParent()).join().get()\n                .uploadOrReplaceFile(filename, AsyncReader.build(data), data.length, sharer.network, crypto, () -> false, l -> {}).join();\n        sharer.shareReadAccessWith(file, Set.of(sharee)).join();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RequestCountingBlockMetadataStore.java",
    "content": "package peergos.server.tests;\n\nimport peergos.server.storage.BlockMetadata;\nimport peergos.server.storage.BlockMetadataStore;\nimport peergos.server.storage.BlockVersion;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.util.Pair;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nclass RequestCountingBlockMetadataStore implements BlockMetadataStore {\n    private final BlockMetadataStore target;\n    private final AtomicLong count = new AtomicLong(0);\n\n    public RequestCountingBlockMetadataStore(BlockMetadataStore target) {\n        this.target = target;\n    }\n\n    public long getRequestCount() {\n        return count.get();\n    }\n\n    public void resetRequestCount() {\n        count.set(0);\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return target.isEmpty();\n    }\n\n    @Override\n    public Optional<BlockMetadata> get(Cid block) {\n        count.incrementAndGet();\n        return target.get(block);\n    }\n\n    @Override\n    public List<Cid> hasBlocks(List<Cid> blocks) {\n        count.incrementAndGet();\n        return blocks.stream()\n                .filter(h -> target.get(h).isPresent())\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public Map<Cid, BlockMetadata> getAll(List<Cid> blocks) {\n        return blocks.stream()\n                .map(h -> new Pair<>(h, target.get(h)))\n                .filter(p -> p.right.isPresent())\n                .collect(Collectors.toMap(p -> p.left, p -> p.right.get()));\n    }\n\n    @Override\n    public Optional<PublicKeyHash> getOwner(Cid block) {\n        return target.getOwner(block);\n    }\n\n    @Override\n    public void setOwner(PublicKeyHash owner, Cid block) {\n        target.setOwner(owner, block);\n    }\n\n    @Override\n    public void setOwnerAndVersion(PublicKeyHash owner, Cid block, String version) {\n        target.setOwnerAndVersion(owner, block, version);\n    }\n\n    @Override\n    public void put(PublicKeyHash owner, Cid block, String version, BlockMetadata meta) {\n        target.put(owner, block, version, meta);\n    }\n\n    @Override\n    public void remove(Cid block) {\n        target.remove(block);\n    }\n\n    @Override\n    public long size(PublicKeyHash owner) {\n        return target.size(owner);\n    }\n\n    @Override\n    public void applyToAll(Consumer<Cid> consumer) {\n        target.applyToAll(consumer);\n    }\n\n    @Override\n    public void applyToAllSizes(BiConsumer<Cid, Long> action) {\n        target.applyToAllSizes(action);\n    }\n\n    @Override\n    public Stream<BlockVersion> list(PublicKeyHash owner) {\n        return target.list(owner);\n    }\n\n    @Override\n    public void listCbor(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        target.listCbor(owner, res);\n    }\n\n    @Override\n    public void compact() {\n        target.compact();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RestartTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.social.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\nimport static org.junit.Assert.*;\n\npublic class RestartTests {\n\n    private static Args args = UserTests.buildArgs()\n            .with(\"mutable-pointers-file\", \"mutable.sql\")\n            .with(\"social-sql-file\", \"social.sql\")\n            .with(\"useIPFS\", \"true\")\n            .with(IpfsWrapper.IPFS_BOOTSTRAP_NODES, \"\"); // no bootstrapping;\n    private final NetworkAccess network;\n    private static final Crypto crypto = Main.initCrypto();\n    private static Process server;\n\n    public RestartTests() throws Exception {\n        this.network = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).join();\n    }\n\n    @BeforeClass\n    public static void init() throws Exception {\n        Files.copy(PathUtil.get(\"Peergos.jar\"), args.getPeergosDirChild(\"Peergos.jar\"));\n        Files.copy(PathUtil.get(\"lib\"), args.getPeergosDirChild(\"lib\"));\n        for (Path file : Files.list(PathUtil.get(\"lib\")).collect(Collectors.toList()))\n            Files.copy(file, args.getPeergosDirChild(\"lib\").resolve(file.getFileName()));\n\n        server = start(\"pki-init\");\n        waitUntilReady();\n    }\n\n    @AfterClass\n    public static void shutdown() {\n        server.destroy();\n    }\n\n    private static void restart() throws Exception {\n        NetworkAccess network = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty())\n                .join();\n        Multihash pkiNodeId = network.dhtClient.id().join();\n        PublicKeyHash peergosId = network.coreNode.getPublicKeyHash(\"peergos\").join().get();\n        args = args.setArg(\"pki-node-id\", pkiNodeId.toString());\n        args = args.setArg(\"peergos.identity.hash\", peergosId.toString());\n        server.destroy();\n        server.destroyForcibly();\n        server.waitFor();\n        try {Thread.sleep(30_000);} catch (InterruptedException e) {}\n        server = start(\"pki\");\n        waitUntilReady();\n    }\n\n    private static void waitUntilReady() {\n        for (int i=0; i < 30; i++) {\n            try {\n                Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).join();\n                return;\n            } catch (Exception e) {\n                try {\n                    Thread.sleep(5000);\n                } catch (InterruptedException f) {}\n            }\n        }\n    }\n\n    public static Process start(String command) throws IOException {\n        Stream<String> classPath = Stream.concat(Stream.of(\"Peergos.jar\"),\n                Files.list(args.getPeergosDirChild(\"lib\")).map(Path::toString));\n        List<String> peergosArgs = Stream.concat(\n                Stream.of(\"java\", \"-cp\", classPath.collect(Collectors.joining(System.getProperty(\"path.separator\"))), \"peergos.server.Main\", \"-\" + command),\n                args.getAllArgs().stream())\n                .collect(Collectors.toList());\n\n        ProcessBuilder pb = new ProcessBuilder(peergosArgs);\n        pb.directory(args.getPeergosDir().toFile());\n        try {\n            Process started = pb.start();\n            new Thread(() -> Logging.log(started.getInputStream(),\n                    \"$(peergos server) out: \"), \"Peergos output stream\").start();\n            new Thread(() -> Logging.log(started.getErrorStream(),\n                    \"$(peergos server) err: \"), \"Peergos error stream\").start();\n            return started;\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe.getMessage(), ioe);\n        }\n    }\n\n    private String random() {\n        return ArrayOps.bytesToHex(crypto.random.randomBytes(15));\n    }\n\n    @Ignore\n    @Test\n    public void friendPasswordChange() throws Exception {\n        String username1 = random();\n        String password1 = random();\n        UserContext u1 = PeergosNetworkUtils.ensureSignedUp(username1, password1, network, crypto);\n        String username2 = random();\n        String password2 = random();\n        UserContext u2 = PeergosNetworkUtils.ensureSignedUp(username2, password2, network, crypto);\n        u2.sendFollowRequest(u1.username, SymmetricKey.random()).join();\n        List<FollowRequestWithCipherText> u1Requests = u1.processFollowRequests().join();\n        u1.sendReplyFollowRequest(u1Requests.get(0), true, true).join();\n        // complete connection\n        List<FollowRequestWithCipherText> u2FollowRequests = u2.processFollowRequests().join();\n\n        // change password for u2\n        String password3 = random();\n        u2.changePassword(password2, password3, UserTests::noMfa).join();\n\n        // restart the server\n        restart();\n\n        UserContext freshU1 = UserContext.signIn(username1, password1, UserTests::noMfa, network.clear(), crypto).join();\n        Optional<FileWrapper> u2ToU1 = freshU1.getByPath(\"/\" + u2.username).join();\n        assertTrue(\"Friend root present after their password change\", u2ToU1.isPresent());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/RetryStorageTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.ProgressConsumer;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class RetryStorageTests {\n\n    public class FailingStorage implements ContentAddressedStorage {\n        private int counter = 1;\n        private final int retryLimit;\n        public FailingStorage(int retryLimit) {\n            this.retryLimit = retryLimit;\n        }\n\n        @Override\n        public ContentAddressedStorage directToOrigin() {\n            return this;\n        }\n\n        @Override\n        public CompletableFuture<Cid> id() {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else{\n                counter=1;\n                return CompletableFuture.completedFuture(new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, new byte[32]));\n            }\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> ids() {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else{\n                counter=1;\n                return CompletableFuture.completedFuture(List.of(new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, new byte[32])));\n            }\n        }\n\n        @Override\n        public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n            return Futures.of(\"localhost\");\n        }\n\n        @Override\n        public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else {\n                counter=1;\n                return CompletableFuture.completedFuture(new TransactionId(Long.toString(System.currentTimeMillis())));\n            }\n        }\n\n        @Override\n        public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else {\n                counter=1;\n                return CompletableFuture.completedFuture(true);\n            }\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                                PublicKeyHash writer,\n                                                List<byte[]> signatures,\n                                                List<byte[]> blocks,\n                                                TransactionId tid) {\n            return put(writer, blocks, false);\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                                   PublicKeyHash writer,\n                                                   List<byte[]> signatures,\n                                                   List<byte[]> blocks,\n                                                   TransactionId tid,\n                                                   ProgressConsumer<Long> progressConsumer) {\n            return put(writer, blocks, true);\n        }\n\n        private CompletableFuture<List<Cid>> put(PublicKeyHash writer, List<byte[]> blocks, boolean isRaw) {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else{\n                counter=1;\n                return CompletableFuture.completedFuture(new ArrayList<>());\n            }\n        }\n\n\n        @Override\n        public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid object, Optional<BatWithId> bat) {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else {\n                counter=1;\n                return CompletableFuture.completedFuture(Optional.empty());\n            }\n        }\n\n        @Override\n        public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else {\n                counter=1;\n                return CompletableFuture.completedFuture(Optional.empty());\n            }\n        }\n\n        @Override\n        public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else {\n                counter=1;\n                return CompletableFuture.completedFuture(Collections.emptyList());\n            }\n        }\n\n        @Override\n        public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n            throw new IllegalStateException(\"Unimplemented!\");\n        }\n\n        @Override\n        public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n            throw new IllegalStateException(\"Unimplemented!\");\n        }\n\n        @Override\n        public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n            if(counter++ % retryLimit != 0) {\n                return CompletableFuture.failedFuture(new Error(\"failure!\"));\n            }else {\n                counter=1;\n                return CompletableFuture.completedFuture(Optional.empty());\n            }\n        }\n\n        @Override\n        public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n            throw new IllegalStateException(\"Unimplemented!\");\n        }\n\n        @Override\n        public Optional<BlockCache> getBlockCache() {\n            return Optional.empty();\n        }\n    }\n\n    @Test\n    public void callMethod() {\n        ContentAddressedStorage storage = new RetryStorage(new RAMStorage(Main.initCrypto().hasher), 3);\n\n        BlockStoreProperties props = storage.blockStoreProperties().join();\n        Assert.assertNotNull(\"props should not be null\", props);\n    }\n    @Test\n    public void retryMethodSuccess() {\n        ContentAddressedStorage storage = new RetryStorage(new FailingStorage(3), 3);\n\n        Cid result = storage.id().join();\n        Assert.assertNotNull(\"Retry should succeed\", result);\n    }\n    @Test\n    public void retryMethodFailure() {\n        ContentAddressedStorage storage = new RetryStorage(new FailingStorage(4), 3);\n\n        try {\n            Cid result = storage.id().join();\n            Assert.assertTrue(\"Should throw exception\", false);\n        } catch (Exception e) {\n            System.currentTimeMillis();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/S3ParallelListingTest.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.storage.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.DirectS3BlockStore;\nimport peergos.shared.util.Pair;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class S3ParallelListingTest {\n\n    private static String makeKey(String folder, String username, Cid cid) {\n        return folder + username + \"/\" + DirectS3BlockStore.hashToKey(cid);\n    }\n\n    private static Cid randomCid(Random r) {\n        byte[] hash = new byte[32];\n        r.nextBytes(hash);\n        return new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n\n    @Test\n    public void parallelListingRangesDoNotLeakBeyondPrefix() {\n        String folder = \"blocks/\";\n        Random r = new Random(42);\n\n        String ada = \"ada\"; // sorts before alice\n        String alice = \"alice\";\n        String bob = \"bob\"; // sorts after alice\n\n        List<Cid> adaBlocks = IntStream.range(0, 50).mapToObj(i -> randomCid(r)).collect(Collectors.toList());\n        List<Cid> aliceBlocks = IntStream.range(0, 50).mapToObj(i -> randomCid(r)).collect(Collectors.toList());\n        List<Cid> bobBlocks   = IntStream.range(0, 50).mapToObj(i -> randomCid(r)).collect(Collectors.toList());\n\n        Set<String> adaKeys = adaBlocks.stream().map(c -> makeKey(folder, ada, c)).collect(Collectors.toSet());\n        Set<String> aliceKeys = aliceBlocks.stream().map(c -> makeKey(folder, alice, c)).collect(Collectors.toSet());\n        Set<String> bobKeys   = bobBlocks.stream().map(c -> makeKey(folder, bob, c)).collect(Collectors.toSet());\n\n        // All S3 keys sorted, as S3 would return them\n        List<String> allKeys = new ArrayList<>();\n        allKeys.addAll(adaKeys);\n        allKeys.addAll(aliceKeys);\n        allKeys.addAll(bobKeys);\n        Collections.sort(allKeys);\n\n        // Compute the ranges that applyToAllVersionsParallel seeds for alice's prefix\n        String alicePrefix = alice + \"/\";\n        List<Pair<Optional<String>, Optional<String>>> ranges =\n                S3BlockStorage.computeInitialRanges(folder, alicePrefix, \"\");\n\n        // Collect every key that falls within ANY of the seeded ranges\n        Set<String> coveredByRanges = new HashSet<>();\n        for (String key : allKeys) {\n            for (Pair<Optional<String>, Optional<String>> range : ranges) {\n                String startMarker = range.left.get(); // exclusive lower bound\n                boolean afterStart = key.compareTo(startMarker) > 0;\n                boolean beforeEnd  = range.right.isEmpty() || key.compareTo(range.right.get()) < 0;\n                if (afterStart && beforeEnd) {\n                    coveredByRanges.add(key);\n                    break;\n                }\n            }\n        }\n\n        // All of alice's keys must be covered\n        for (String key : aliceKeys)\n            Assert.assertTrue(\"Alice's key missing from ranges: \" + key, coveredByRanges.contains(key));\n\n        // None of bob's keys should be covered\n        List<String> leaked = bobKeys.stream().filter(coveredByRanges::contains).collect(Collectors.toList());\n        Assert.assertTrue(\n                \"Bug: \" + leaked.size() + \" of bob's keys fall within alice's listing ranges: \" + leaked,\n                leaked.isEmpty());\n\n        // None of ada's keys should be covered\n        List<String> leakedAda = adaKeys.stream().filter(coveredByRanges::contains).collect(Collectors.toList());\n        Assert.assertTrue(\n                \"Bug: \" + leaked.size() + \" of ada's keys fall within alice's listing ranges: \" + leaked,\n                leaked.isEmpty());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/S3V4SignatureTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\n\npublic class S3V4SignatureTests {\n    private static final Crypto crypto = Main.initCrypto();\n    private static final Hasher h = crypto.hasher;\n\n    @Test\n    public void validPutSignature() {\n        String accessKey = \"AKIAIOSFODNN7EXAMPLE\";\n        String secretKey = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\";\n        byte[] payload = \"Welcome to Amazon S3.\".getBytes();\n        String s3Key = \"test%24file.text\";\n        String bucketName = \"examplebucket\";\n        String region = \"us-east-1\";\n        String host = bucketName + \".s3.amazonaws.com\";\n        Map<String, String> extraHeaders = new TreeMap<>();\n        extraHeaders.put(\"date\", \"Fri, 24 May 2013 00:00:00 GMT\");\n        extraHeaders.put(\"x-amz-storage-class\", \"REDUCED_REDUNDANCY\");\n        String timestamp = S3AdminRequests.asAwsDate(LocalDate.of(2013, Month.MAY, 24)\n                .atStartOfDay()\n                .toInstant(ZoneOffset.UTC)\n                .atZone(ZoneId.of(\"UTC\")));\n        String contentSha256 = ArrayOps.bytesToHex(Hash.sha256(payload));\n\n        S3Request policy = new S3Request(\"PUT\", host, s3Key, contentSha256, Optional.empty(), Optional.empty(), false, true,\n                Collections.emptyMap(), extraHeaders, accessKey, region, timestamp);\n        String toSign = policy.stringToSign();\n        Assert.assertTrue(toSign.equals(\"AWS4-HMAC-SHA256\\n\" +\n                \"20130524T000000Z\\n\" +\n                \"20130524/us-east-1/s3/aws4_request\\n\" +\n                \"9e0e90d9c76de8fa5b200d8c849cd5b8dc7a3be3951ddb7f6a76b4158342019d\"));\n\n        String signature = S3Request.computeSignature(policy, secretKey, h).join();\n        Assert.assertTrue(signature.equals(\"98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd\"));\n    }\n\n    @Test\n    public void validGetSignature() {\n        String accessKey = \"AKIAIOSFODNN7EXAMPLE\";\n        String secretKey = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\";\n        String s3Key = \"test.txt\";\n        String bucketName = \"examplebucket\";\n        String region = \"us-east-1\";\n        String host = bucketName + \".s3.amazonaws.com\";\n        String timestamp = S3AdminRequests.asAwsDate(LocalDate.of(2013, Month.MAY, 24)\n                .atStartOfDay()\n                .toInstant(ZoneOffset.UTC)\n                .atZone(ZoneId.of(\"UTC\")));\n        String contentSha256 = \"UNSIGNED-PAYLOAD\";\n\n        S3Request policy = new S3Request(\"GET\", host, s3Key, contentSha256, Optional.empty(), Optional.of(86400), false, false,\n                Collections.emptyMap(), Collections.emptyMap(), accessKey, region, timestamp);\n        String toSign = policy.stringToSign();\n        Assert.assertTrue(toSign.equals(\"AWS4-HMAC-SHA256\\n\" +\n                \"20130524T000000Z\\n\" +\n                \"20130524/us-east-1/s3/aws4_request\\n\" +\n                \"3bfa292879f6447bbcda7001decf97f4a54dc650c8942174ae0a9121cf58ad04\"));\n\n        String signature = S3Request.computeSignature(policy, secretKey, h).join();\n        Assert.assertTrue(signature.equals(\"aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404\"));\n    }\n\n    @Test\n    public void linodeSignature() {\n        String accessKey = \"AKIAIOSFODNN7EXAMPLE\";\n        String secretKey = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\";\n        byte[] payload = \"Hi Linode!\".getBytes();\n        String s3Key = \"AFKREIGBKDTWGUVD3UTHMNCKKLMIL6YE5MI4XSEYCZOWP7WTOUJ6XNMASU\";\n        String bucketName = \"peergos-test\";\n        String region = \"us-east-1\";\n        String host = bucketName + \".\" +region + \".linodeobjects.com\";\n        Map<String, String> extraHeaders = new TreeMap<>();\n        extraHeaders.put(\"amz-sdk-invocation-id\", \"e84d52c4-953e-e1d2-d442-f05ca7067524\");\n        extraHeaders.put(\"amz-sdk-retry\", \"0/0/500\");\n        extraHeaders.put(\"Content-Length\", \"\" + payload.length);\n        extraHeaders.put(\"Content-Type\", \"application/octet-stream\");\n        extraHeaders.put(\"User-Agent\", \"aws-sdk-java/1.11.705 Linux/5.3.0-46-generic OpenJDK_64-Bit_Server_VM/11.0.7+10-post-Ubuntu-2ubuntu218.04 java/11.0.7 vendor/Ubuntu\");\n        String timestamp = S3AdminRequests.asAwsDate(LocalDate.of(2020, Month.APRIL, 25)\n                .atStartOfDay()\n                .withHour(20)\n                .withMinute(41)\n                .withSecond(56)\n                .withNano(0)\n                .toInstant(ZoneOffset.UTC)\n                .atZone(ZoneId.of(\"UTC\")));\n        String contentSha256 = \"UNSIGNED-PAYLOAD\";\n\n        S3Request policy = new S3Request(\"PUT\", host, s3Key, contentSha256, Optional.empty(), Optional.empty(), false, true,\n                Collections.emptyMap(), extraHeaders, accessKey, region, timestamp);\n\n        String canonicalRequest = policy.toCanonicalRequest();\n        Assert.assertTrue(canonicalRequest.equals(\"PUT\\n\" +\n                \"/AFKREIGBKDTWGUVD3UTHMNCKKLMIL6YE5MI4XSEYCZOWP7WTOUJ6XNMASU\\n\" +\n                \"\\n\" +\n                \"amz-sdk-invocation-id:e84d52c4-953e-e1d2-d442-f05ca7067524\\n\" +\n                \"amz-sdk-retry:0/0/500\\n\" +\n                \"content-length:10\\n\" +\n                \"content-type:application/octet-stream\\n\" +\n                \"host:peergos-test.us-east-1.linodeobjects.com\\n\" +\n                \"user-agent:aws-sdk-java/1.11.705 Linux/5.3.0-46-generic OpenJDK_64-Bit_Server_VM/11.0.7+10-post-Ubuntu-2ubuntu218.04 java/11.0.7 vendor/Ubuntu\\n\" +\n                \"x-amz-content-sha256:UNSIGNED-PAYLOAD\\n\" +\n                \"x-amz-date:20200425T204156Z\\n\" +\n                \"\\n\" +\n                \"amz-sdk-invocation-id;amz-sdk-retry;content-length;content-type;host;user-agent;x-amz-content-sha256;x-amz-date\\n\" +\n                \"UNSIGNED-PAYLOAD\"));\n\n        String toSign = policy.stringToSign();\n        Assert.assertTrue(toSign.equals(\"AWS4-HMAC-SHA256\\n\" +\n                \"20200425T204156Z\\n\" +\n                \"20200425/us-east-1/s3/aws4_request\\n\" +\n                \"8dc8ddc0eef8bc2f62ef0ae12a89df788b73780404358bfaba915d58096b9cec\"));\n\n        String signature = S3Request.computeSignature(policy, secretKey, h).join();\n        Assert.assertTrue(signature.equals(\"5cc3daea623ac6d43b482209892cc6eb95e46b068e232eabd85343caf79bb17e\"));\n\n        PresignedUrl url = S3Request.preSignPut(s3Key, payload.length, contentSha256, Optional.empty(), false, timestamp,\n                host, extraHeaders, region, accessKey, secretKey, true, h).join();\n        Assert.assertTrue((\"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20200425/us-east-1/s3/aws4_request,\" +\n                \"SignedHeaders=amz-sdk-invocation-id;amz-sdk-retry;content-length;content-type;host;user-agent;x-amz-content-sha256;x-amz-date,\" +\n                \"Signature=5cc3daea623ac6d43b482209892cc6eb95e46b068e232eabd85343caf79bb17e\")\n                .equals(url.fields.get(\"Authorization\")));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/ServerIdentityTests.java",
    "content": "package peergos.server.tests;\n\nimport io.libp2p.core.*;\nimport io.libp2p.core.crypto.*;\nimport io.libp2p.crypto.keys.*;\nimport org.junit.*;\nimport org.peergos.protocol.ipns.*;\nimport peergos.server.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.resolution.*;\nimport peergos.shared.storage.IpnsEntry;\nimport peergos.shared.util.*;\n\nimport java.nio.charset.*;\nimport java.util.*;\n\npublic class ServerIdentityTests {\n\n    @Test\n    public void rotation() {\n        JdbcServerIdentityStore idstore = JdbcServerIdentityStore.build(Builder.buildEphemeralSqlite(), new SqliteCommands(), Main.initCrypto());\n        // create a new identity\n        PrivKey currentPrivate = Ed25519Kt.generateEd25519KeyPair().getFirst();\n        byte[] signedRecord = ServerIdentity.generateSignedIpnsRecord(currentPrivate, Optional.empty(), false,  1);\n        idstore.addIdentity(PeerId.fromPubKey(currentPrivate.publicKey()), signedRecord);\n\n        List<PeerId> ids = idstore.getIdentities();\n        Assert.assertEquals(1, ids.size());\n        PeerId current = ids.get(0);\n        Assert.assertEquals(current, PeerId.fromPubKey(currentPrivate.publicKey()));\n\n        String password = Passwords.generate();\n        Crypto crypto = Main.initCrypto();\n        PrivKey nextPriv = ServerIdentity.generateNextIdentity(password, current, crypto);\n        PeerId nextPeerId = PeerId.fromPubKey(nextPriv.publicKey());\n        idstore.setPrivateKey(currentPrivate);\n        idstore.setRecord(current, ServerIdentity.generateSignedIpnsRecord(currentPrivate, Optional.of(Multihash.decode(nextPeerId.getBytes())), true,2));\n        idstore.addIdentity(nextPeerId, ServerIdentity.generateSignedIpnsRecord(nextPriv, Optional.empty(), false, 1));\n\n        List<PeerId> updated = idstore.getIdentities();\n        Assert.assertEquals(2, updated.size());\n        Assert.assertEquals(nextPeerId, updated.get(1));\n        byte[] prevRecord = idstore.getRecord(current);\n        Optional<IpnsMapping> prevIpnsMapping = IPNS.parseAndValidateIpnsEntry(\n                ArrayOps.concat(\"/ipns/\".getBytes(StandardCharsets.UTF_8), current.getBytes()),\n                prevRecord);\n        IpnsEntry ipnsData = new IpnsEntry(prevIpnsMapping.get().getSignature(), prevIpnsMapping.get().getData());\n        ResolutionRecord prevRes = ipnsData.getValue();\n        Assert.assertEquals(prevRes.moved, true);\n        Assert.assertEquals(prevRes.host, Optional.of(Multihash.fromBase58(nextPeerId.toBase58())));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/SqliteBlockMetadataTest.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.sql.*;\nimport java.util.*;\nimport java.util.stream.*;\n\nimport static org.junit.Assert.assertTrue;\n\npublic class SqliteBlockMetadataTest {\n\n    private static final Random r = new Random(666);\n    private static Crypto crypto = Main.initCrypto();\n\n    private static Cid randomCid() {\n        byte[] hash = new byte[32];\n        r.nextBytes(hash);\n        return new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n\n    private static List<Cid> randomCids(int count) {\n        byte[] hash = new byte[32];\n        r.nextBytes(hash);\n        return IntStream.range(0, count).mapToObj(i -> randomCid()).collect(Collectors.toList());\n    }\n\n    @Test\n    public void basicUsage() throws Exception {\n        Path dir = Files.createTempDirectory(\"peergos-block-metadata\");\n        File storeFile = dir.resolve(\"metadata.sql\" + System.currentTimeMillis()).toFile();\n        String sqlFilePath = storeFile.getPath();\n        Connection memory = Sqlite.build(sqlFilePath);\n        Connection instance = new Sqlite.UncloseableConnection(memory);\n        BlockMetadataStore store = new JdbcBlockMetadataStore(() -> instance, new SqliteCommands());\n\n        Cid cid = randomCid();\n        PublicKeyHash owner = new PublicKeyHash(cid);\n        BlockMetadata meta = new BlockMetadata(10240, randomCids(20), Collections.emptyList());\n        store.put(owner, cid, \"alpha\", meta);\n\n        // add same cid again\n        store.put(owner, cid, \"beta\", meta);\n        Cid cid2 = randomCid();\n        BlockMetadata meta2 = new BlockMetadata(10240, randomCids(20),\n                List.of(BatId.inline(Bat.random(crypto.random)), BatId.inline(Bat.random(crypto.random))));\n        store.put(owner, cid2, \"gammaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", meta2);\n\n        List<BlockVersion> ls = store.list(owner).collect(Collectors.toList());\n        Assert.assertTrue(ls.size() == 2);\n\n        long size = store.size(owner);\n        Assert.assertTrue(size == 2);\n\n        // null versions\n        Cid cid3 = randomCid();\n        BlockMetadata meta3 = new BlockMetadata(10240, randomCids(20),\n                List.of(BatId.inline(Bat.random(crypto.random)), BatId.inline(Bat.random(crypto.random))));\n        store.put(owner, cid3, null, meta3);\n        Assert.assertTrue(store.list(owner).filter(v -> v.cid.equals(cid3)).findFirst().get().version == null);\n\n        List<Cid> all = new ArrayList<>();\n        store.applyToAll(all::add);\n        Assert.assertTrue(all.size() == 3);\n    }\n\n    @Ignore\n    @Test\n    public void pagination() throws Exception {\n        Path dir = Files.createTempDirectory(\"peergos-block-metadata\");\n        File storeFile = dir.resolve(\"metadata.sql\" + System.currentTimeMillis()).toFile();\n        String sqlFilePath = storeFile.getPath();\n        Connection memory = Sqlite.build(sqlFilePath);\n        Connection instance = new Sqlite.UncloseableConnection(memory);\n        BlockMetadataStore store = new JdbcBlockMetadataStore(() -> instance, new SqliteCommands());\n        PublicKeyHash owner = new PublicKeyHash(randomCid());\n\n        for (int i=0; i < 5 * JdbcBlockMetadataStore.PAGE_LIMIT/2; i++) {\n            Cid cid = randomCid();\n            BlockMetadata meta = new BlockMetadata(10240, randomCids(20), Collections.emptyList());\n            store.put(owner, cid, \"alpha\", meta);\n        }\n\n        List<Cid> all = new ArrayList<>();\n        store.applyToAll(all::add);\n        Assert.assertTrue(all.size() == 5 * JdbcBlockMetadataStore.PAGE_LIMIT/2);\n        HashSet<Cid> cidSet = new HashSet<>(all);\n        Assert.assertTrue(cidSet.size() == 5 * JdbcBlockMetadataStore.PAGE_LIMIT/2);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/SqliteTableTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\n\nimport java.sql.*;\nimport java.util.*;\n\npublic class SqliteTableTests {\n\n    @Test\n    public void modifyTransactionsTable() throws Exception {\n        long start = System.currentTimeMillis();\n        Connection db = new Sqlite.UncloseableConnection(Sqlite.build(\":memory:\"));\n        String legacyTableCreate = \"CREATE TABLE IF NOT EXISTS transactions (\" +\n                \"tid varchar(64) not null, owner varchar(64) not null, hash varchar(64) not null);\";\n        db.createStatement().executeUpdate(legacyTableCreate);\n\n        PublicKeyHash owner = new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, new byte[36]));\n        SqliteCommands cmds = new SqliteCommands();\n        {\n            PreparedStatement insert = db.prepareStatement(\"INSERT OR IGNORE INTO transactions (tid, owner, hash) VALUES (?, ?, ?);\");\n            insert.clearParameters();\n            insert.setString(1, \"tid0\");\n            insert.setString(2, owner.toString());\n            insert.setString(3, new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, new byte[32]).toString());\n            insert.executeUpdate();\n        }\n\n        // now add a column\n        JdbcTransactionStore txns = new JdbcTransactionStore(() -> db, cmds);\n\n        Multihash hash1 = new Cid(1, Cid.Codec.Raw, Multihash.Type.sha2_256, new byte[32]);\n        txns.addBlock(hash1, TransactionId.build(\"tid1\"), owner);\n\n        // check both entries are correct\n        List<Cid> open = txns.getOpenTransactionBlocks(owner);\n        Assert.assertTrue(open.size() == 2);\n\n        // check an immediate GC doesn't clear the new block\n        txns.clearOldTransactions(owner, start);\n        Assert.assertTrue(txns.getOpenTransactionBlocks(owner).size() == 1);\n\n        // clear both entries\n        txns.clearOldTransactions(owner, System.currentTimeMillis() + 1000);\n\n        // check there are no entries left\n        List<Cid> empty = txns.getOpenTransactionBlocks(owner);\n        Assert.assertTrue(empty.isEmpty());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/SqliteblockListTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.storage.SqliteBlockList;\nimport peergos.server.storage.UserBlockVersion;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Random;\n\npublic class SqliteblockListTests {\n\n    @Test\n    public void legacy() throws IOException {\n        Path dir = Files.createTempDirectory(\"block-list\");\n        SqliteBlockList blocks = SqliteBlockList.createBlockListDb(dir.resolve(\"blocks.sqlite\"));\n        Cid h = randomCbor(new Random(42));\n        String version = \"version\";\n        blocks.addBlocks(List.of(new UserBlockVersion(null, h, version, true)));\n        List<String> versions = blocks.getVersions(null, h);\n        Assert.assertEquals(List.of(version), versions);\n\n        Assert.assertTrue(blocks.hasBlock(null, h));\n    }\n\n    private Cid randomCbor(Random r) {\n        byte[] hash = new byte[32];\n        r.nextBytes(hash);\n        return new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/StandaloneWebauthnDemo.java",
    "content": "package peergos.server.tests;\n\nimport com.sun.net.httpserver.*;\nimport com.webauthn4j.*;\nimport com.webauthn4j.authenticator.*;\nimport com.webauthn4j.authenticator.Authenticator;\nimport com.webauthn4j.converter.exception.*;\nimport com.webauthn4j.data.*;\nimport com.webauthn4j.data.client.*;\nimport com.webauthn4j.data.client.challenge.*;\nimport com.webauthn4j.server.*;\nimport com.webauthn4j.verifier.exception.*;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.security.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class StandaloneWebauthnDemo {\n\n    public static void main(String[] args) throws Exception {\n        // demo webauthn server and client\n        // only the most recent user to register can login here\n        // requires java 17 for Ed25519\n        SecureRandom rnd = new SecureRandom();\n        WebAuthnManager webAuthnManager = WebAuthnManager.createNonStrictWebAuthnManager();\n        List<Authenticator> users = new ArrayList<>();\n        List<byte[]> registerChallenges = new ArrayList<>();\n        List<byte[]> loginChallenges = new ArrayList<>();\n        String domain = args.length == 0 ? \"localhost\" : args[0];\n        String origin = domain.equals(\"localhost\") ? \"http://localhost:9999\" : \"https://\" + domain;\n\n        HttpServer server = HttpServer.create(new InetSocketAddress(\"localhost\", 9999), 10);\n        server.setExecutor(Executors.newFixedThreadPool(10));\n        server.createContext(\"/\", httpExchange -> {\n            String path = httpExchange.getRequestURI().getPath();\n            byte[] html = (\"<!DOCTYPE html><html><body>\\n\" +\n                    \"<h1>Webauthn test</h1>\\n\" +\n                    \"<button onclick=\\\"register()\\\">Register</button><label id='register'></label><br/>\\n\" +\n                    \"<button onclick=\\\"login()\\\">Login</button><label id='login'></label>\\n\" +\n                    \"<script type=\\\"text/javascript\\\" src=\\\"webauthn.js\\\"></script>\\n\" +\n                    \"</body></html>\").getBytes();\n            byte[] js = (\n                    \"function toHexString(byteArray) {\\n\" +\n                            \"   return Array.prototype.map.call(new Uint8Array(byteArray), x => ('00' + x.toString(16)).slice(-2)).join('')\\n\" +\n                            \"}\\n\" +\n                            \"function hexToBytes(hex) {\\n\" +\n                            \"   let res = new Uint8Array(hex.length/2);\\n\" +\n                            \"   for (var i=0; i < hex.length/2; i++)\\n\" +\n                            \"      res[i] = parseInt(hex.substring(2*i, 2*(i+1)), 16);\\n\" +\n                            \"   return res;\\n\" +\n                            \"}\\n\" +\n                            \"async function register() {\\n\" +\n                            \"   let init = await fetch(\\\"/registerStart\\\").then(response=>response.json());\\n\" +\n                            \"   let challenge = hexToBytes(init);\\n\" +\n                            \"   let username = 'peergosuser'\\n\" +\n                            \"   let enc = new TextEncoder()\\n\" +\n                            \"   let userId = new Uint8Array(username.length)\\n\" +\n                            \"   enc.encodeInto(username, userId);\\n\" +\n                            \"   let credential = await navigator.credentials.create({\\n\" +\n                            \"      publicKey: {\\n\" +\n                            \"         challenge: challenge,\\n\" +\n                            \"         rp: { name: \\\"Peergos\\\" },\\n\" +\n                            \"         user: {\\n\" +\n                            \"            id: userId,\\n\" +\n                            \"            name: username,\\n\" +\n                            \"            displayName: username,\\n\" +\n                            \"         },\\n\" +\n                            \"         timeout: 60000,\\n\" +\n                            \"         pubKeyCredParams: [ {type: \\\"public-key\\\", alg: -8}, {type: \\\"public-key\\\", alg: -7}, {type: \\\"public-key\\\", alg: -257}]\\n\" +\n                            \"   }\\n\" +\n                            \"});\\n\" +\n                            \"let res = await fetch(\\\"/registerComplete\\\", {'method':'POST','body':JSON.stringify({\" +\n                            \"      'attestationObject':toHexString(credential.response.attestationObject),\\n\"+\n                            \"      'clientDataJSON': toHexString(credential.response.clientDataJSON)\" +\n                            \"   })\\n\" +\n                            \"}).then(response=>response.json());\\n\" +\n                            \"document.getElementById(\\\"register\\\").textContent = res.status;\\n\" +\n                            \"}\\n\" +\n                            \"async function login() {\\n\" +\n                            \"   let init = await fetch(\\\"/loginStart\\\").then(response=>response.json());\\n\" +\n                            \"   let challenge = hexToBytes(init.challenge);\\n\" +\n                            \"   let id = hexToBytes(init.id);\\n\" +\n                            \"   let credential = await navigator.credentials.get({\\n\" +\n                            \"      publicKey: {\\n\" +\n                            \"         challenge: challenge,\\n\" +\n                            \"         allowCredentials: [{\\n\" +\n                            \"            type: \\\"public-key\\\",\\n\" +\n                            \"            id: id\\n\" +\n                            \"         }],\\n\" +\n                            \"         timeout: 60000,\\n\" +\n                            \"         userVerification: \\\"preferred\\\",\\n\" +\n                            \"      }\\n\" +\n                            \"   });\\n\" +\n                            \"   let res = await fetch(\\\"/loginComplete\\\", {'method':'POST','body':JSON.stringify({\" +\n                            \"         'authenticatorData':toHexString(credential.response.authenticatorData),\\n\"+\n                            \"         'signature':toHexString(credential.response.signature),\\n\"+\n                            \"         'clientDataJSON': toHexString(credential.response.clientDataJSON)\" +\n                            \"      })\" +\n                            \"   }).then(response=>response.json());\\n\" +\n                            \"   document.getElementById(\\\"login\\\").textContent = res.status;\\n\" +\n                            \"}\").getBytes();\n            boolean isHTML = ! path.equals(\"/webauthn.js\");\n            byte[] res = isHTML ? html : js;\n            if (isHTML)\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/html\");\n            else\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/javascript\");\n            httpExchange.sendResponseHeaders(200, res.length);\n            httpExchange.getResponseBody().write(res);\n            httpExchange.getResponseBody().close();\n        });\n        server.createContext(\"/registerStart\", httpExchange -> {\n            byte[] rawChallenge = new byte[32];\n            rnd.nextBytes(rawChallenge);\n            registerChallenges.add(rawChallenge);\n            byte[] res = (\"\\\"\"+ArrayOps.bytesToHex(rawChallenge)+\"\\\"\").getBytes();\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/json\");\n            httpExchange.sendResponseHeaders(200, res.length);\n            httpExchange.getResponseBody().write(res);\n            httpExchange.getResponseBody().close();\n        });\n        server.createContext(\"/registerComplete\", httpExchange -> {\n            String req = new String(Serialize.readFully(httpExchange.getRequestBody()));\n            Object json = JSONParser.parse(req);\n            // Client properties\n            byte[] attestationObject = ArrayOps.hexToBytes((String)((Map)json).get(\"attestationObject\"));\n            byte[] clientDataJSON = ArrayOps.hexToBytes((String)((Map)json).get(\"clientDataJSON\"));\n            String clientExtensionJSON = null;  /* set clientExtensionJSON */\n            Set<String> transports = null /* set transports */;\n            try {\n                Authenticator registered = register(webAuthnManager, registerChallenges.get(registerChallenges.size() - 1),\n                        attestationObject, clientDataJSON, clientExtensionJSON, transports, origin, domain);\n                users.add(registered);\n                byte[] res = \"{\\\"status\\\":\\\"success\\\"}\".getBytes();\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/json\");\n                httpExchange.sendResponseHeaders(200, res.length);\n                httpExchange.getResponseBody().write(res);\n                httpExchange.getResponseBody().close();\n            } catch (Throwable e) {\n                e.printStackTrace();\n                byte[] res = \"{\\\"status\\\":\\\"error\\\"}\".getBytes();\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/json\");\n                httpExchange.sendResponseHeaders(200, res.length);\n                httpExchange.getResponseBody().write(res);\n                httpExchange.getResponseBody().close();\n            }\n        });\n        server.createContext(\"/loginStart\", httpExchange -> {\n            byte[] rawChallenge = new byte[32];\n            rnd.nextBytes(rawChallenge);\n            loginChallenges.add(rawChallenge);\n            Authenticator user = users.get(users.size() - 1);\n            byte[] res = (\"{\\\"challenge\\\":\\\"\"+ArrayOps.bytesToHex(rawChallenge)+\"\\\",\" +\n                    \"\\\"id\\\":\\\"\"+ArrayOps.bytesToHex(user.getAttestedCredentialData().getCredentialId())+\"\\\"}\").getBytes();\n            httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/json\");\n            httpExchange.sendResponseHeaders(200, res.length);\n            httpExchange.getResponseBody().write(res);\n            httpExchange.getResponseBody().close();\n        });\n        server.createContext(\"/loginComplete\", httpExchange -> {\n            String req = new String(Serialize.readFully(httpExchange.getRequestBody()));\n            Object json = JSONParser.parse(req);\n            byte[] authenticatorData = ArrayOps.hexToBytes((String)((Map)json).get(\"authenticatorData\"));\n            byte[] clientDataJSON = ArrayOps.hexToBytes((String)((Map)json).get(\"clientDataJSON\"));\n            byte[] signature = ArrayOps.hexToBytes((String)((Map)json).get(\"signature\"));\n            Authenticator user = users.get(users.size() - 1);\n            try {\n                login(webAuthnManager, loginChallenges.get(loginChallenges.size() - 1),\n                        user.getAttestedCredentialData().getCredentialId(), \"peergosuser\".getBytes(), user,\n                        authenticatorData, clientDataJSON, signature, origin, domain);\n                byte[] res = \"{\\\"status\\\":\\\"success\\\"}\".getBytes();\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/json\");\n                httpExchange.sendResponseHeaders(200, res.length);\n                httpExchange.getResponseBody().write(res);\n                httpExchange.getResponseBody().close();\n            } catch (Throwable e) {\n                e.printStackTrace();\n                byte[] res = \"{\\\"status\\\":\\\"error\\\"}\".getBytes();\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"text/json\");\n                httpExchange.sendResponseHeaders(200, res.length);\n                httpExchange.getResponseBody().write(res);\n                httpExchange.getResponseBody().close();\n            }\n        });\n        server.start();\n    }\n\n    private static void login(WebAuthnManager webAuthnManager,\n                              byte[] rawChallenge,\n                              byte[] credentialId,\n                              byte[] userHandle,\n                              Authenticator user,\n                              byte[] authenticatorData,\n                              byte[] clientDataJSON,\n                              byte[] signature,\n                              String originString,\n                              String host) {\n        // Client properties\n        String clientExtensionJSON = null /* set clientExtensionJSON */;\n\n        // Server properties\n        Origin origin = new Origin(originString);\n        String rpId = host;\n        Challenge challenge = () -> rawChallenge;\n        byte[] tokenBindingId = null /* set tokenBindingId */;\n        ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId);\n\n        // expectations\n        List<byte[]> allowCredentials = null;\n        boolean userVerificationRequired = false;\n        boolean userPresenceRequired = true;\n\n        AuthenticationRequest authenticationRequest =\n                new AuthenticationRequest(\n                        credentialId,\n                        userHandle,\n                        authenticatorData,\n                        clientDataJSON,\n                        clientExtensionJSON,\n                        signature\n                );\n        AuthenticationParameters authenticationParameters =\n                new AuthenticationParameters(\n                        serverProperty,\n                        user,\n                        allowCredentials,\n                        userVerificationRequired,\n                        userPresenceRequired\n                );\n\n        AuthenticationData authenticationData;\n        try {\n            authenticationData = webAuthnManager.parse(authenticationRequest);\n        } catch (DataConversionException e) {\n            // If you would like to handle WebAuthn data structure parse error, please catch DataConversionException\n            throw e;\n        }\n        try {\n            webAuthnManager.validate(authenticationData, authenticationParameters);\n        } catch (VerificationException e) {\n            // If you would like to handle WebAuthn data validation error, please catch ValidationException\n            throw e;\n        }\n        // please update the counter of the authenticator record\n//        updateCounter(\n//                authenticationData.getCredentialId(),\n//                authenticationData.getAuthenticatorData().getSignCount()\n//        );\n    }\n    private static Authenticator register(WebAuthnManager webAuthnManager,\n                                          byte[] rawChallenge,\n                                          byte[] attestationObject,\n                                          byte[] clientDataJSON,\n                                          String clientExtensionJSON,\n                                          Set<String> transports,\n                                          String originString,\n                                          String domain) {\n        // Server properties\n        Origin origin = new Origin(originString);\n        String rpId = domain;\n\n        Challenge challenge = () -> rawChallenge;\n        byte[] tokenBindingId = null /* set tokenBindingId */;\n        ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId);\n\n        // expectations\n        boolean userVerificationRequired = false;\n        boolean userPresenceRequired = true;\n\n        RegistrationRequest registrationRequest = new RegistrationRequest(attestationObject, clientDataJSON, clientExtensionJSON, transports);\n        RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, userVerificationRequired, userPresenceRequired);\n        RegistrationData registrationData;\n        try {\n            registrationData = webAuthnManager.parse(registrationRequest);\n        } catch (DataConversionException e) {\n            // If you would like to handle WebAuthn data structure parse error, please catch DataConversionException\n            throw e;\n        }\n        try {\n            webAuthnManager.validate(registrationData, registrationParameters);\n        } catch (VerificationException e) {\n            // If you would like to handle WebAuthn data validation error, please catch ValidationException\n            throw e;\n        }\n\n        // please persist Authenticator object, which will be used in the authentication process.\n        return new AuthenticatorImpl(\n                registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData(),\n                registrationData.getAttestationObject().getAttestationStatement(),\n                registrationData.getAttestationObject().getAuthenticatorData().getSignCount()\n        );\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/SyncTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.JavaCrypto;\nimport peergos.server.Main;\nimport peergos.server.sync.*;\nimport peergos.shared.Crypto;\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.CommittedWriterData;\nimport peergos.shared.user.Snapshot;\nimport peergos.shared.user.fs.HashTree;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.time.LocalDateTime;\nimport java.util.*;\n\npublic class SyncTests {\n\n    private static Crypto crypto = JavaCrypto.init();\n\n    @Test\n    public void rename() throws Exception {\n        LocalDateTime.now();\n        for (int filesize : List.of(1024, 6 * 1024 * 1024)) {\n            rename(\"file.bin\", \"newfile.bin\", true, true, filesize);\n            rename(\"file.bin\", \"newfile.bin\", false, false, filesize);\n            rename(\"newfile.bin\", \"file.bin\", true, true, filesize);\n            rename(\"newfile.bin\", \"file.bin\", false, false, filesize);\n        }\n    }\n\n    public void rename(String originalFilename,\n                       String newFilename,\n                       boolean syncLocalDeletes,\n                       boolean syncRemoteDeletes,\n                       int filesize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[filesize];\n        new Random(42).nextBytes(data);\n        Files.write(base1.resolve(originalFilename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(originalFilename));\n\n        // rename file\n        Files.move(base1.resolve(originalFilename), base1.resolve(newFilename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNull(syncedState.byPath(originalFilename));\n        Assert.assertNotNull(syncedState.byPath(newFilename));\n\n        // sync should be stable\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNull(syncedState.byPath(originalFilename));\n        Assert.assertNotNull(syncedState.byPath(newFilename));\n    }\n\n    @Test\n    public void renamesWithDuplicates() throws Exception {\n        for (int copies=2; copies < 15; copies++)\n            for (int renames=1; renames <= copies; renames++)\n                renameDupe(\"file.bin\", \"newfile.bin\", true, true, 1024, copies, renames);\n    }\n\n    public void renameDupe(String originalFilename,\n                           String newFilename,\n                           boolean syncLocalDeletes,\n                           boolean syncRemoteDeletes,\n                           int filesize,\n                           int nCopies,\n                           int nRenames) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[filesize];\n        new Random(42).nextBytes(data);\n        for (int i=0; i < nCopies; i++)\n            Files.write(base1.resolve(i + \"_\" + originalFilename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        for (int i=0; i < nCopies; i++)\n            Assert.assertNotNull(syncedState.byPath(i + \"_\" + originalFilename));\n        Assert.assertEquals(syncedState.allFilePaths().size(), nCopies);\n\n        // rename file\n        for (int i=0; i < nRenames; i++)\n            Files.move(base1.resolve(i + \"_\" + originalFilename), base1.resolve(i + \"_\" + newFilename));\n        List<String> ops = new ArrayList<>();\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, ops::add);\n        for (int i=0; i < nRenames; i++) {\n            Assert.assertNull(syncedState.byPath(i + \"_\" + originalFilename));\n            Assert.assertNotNull(syncedState.byPath(i + \"_\" + newFilename));\n        }\n        Assert.assertTrue(ops.stream().noneMatch(op -> op.contains(\"upload\")));\n        Assert.assertTrue(ops.stream().anyMatch(op -> op.contains(\"Moving\")));\n\n        // sync should be stable\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        for (int i=0; i < nRenames; i++) {\n            Assert.assertNull(syncedState.byPath(i + \"_\" + originalFilename));\n            Assert.assertNotNull(syncedState.byPath(i + \"_\" + newFilename));\n        }\n    }\n\n    @Test\n    public void renameIgnoringDeletes() throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = false;\n        boolean syncRemoteDeletes = false;\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[6 * 1024 * 1024];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // rename file\n        String filename2 = \"newfile.bin\";\n        Files.move(base1.resolve(filename), base1.resolve(filename2));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNull(syncedState.byPath(filename));\n        Assert.assertNotNull(syncedState.byPath(filename2));\n\n        // sync should be stable\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNull(syncedState.byPath(filename));\n        Assert.assertNotNull(syncedState.byPath(filename2));\n    }\n\n    @Test\n    public void moves() throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[6 * 1024 * 1024];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // move file to a subdir\n        Path subdir = base1.resolve(\"subdir\");\n        subdir.toFile().mkdirs();\n        Files.move(base1.resolve(filename), subdir.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNull(syncedState.byPath(filename));\n        String fileRelPath = subdir.getFileName().resolve(filename).toString().replaceAll(\"\\\\\\\\\", \"/\");\n        Assert.assertNotNull(syncedState.byPath(fileRelPath));\n\n        // sync should be stable\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNull(syncedState.byPath(filename));\n        Assert.assertNotNull(syncedState.byPath(fileRelPath));\n\n        // move the file back\n        Files.move(subdir.resolve(filename), base1.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertNull(syncedState.byPath(fileRelPath));\n\n        // check stability\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertNull(syncedState.byPath(fileRelPath));\n\n        Assert.assertTrue(syncedState.getInProgressCopies().isEmpty());\n    }\n\n    @Test\n    public void androidModTime() throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[6 * 1024];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base2.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // simulate Android (base1) not being able to set mod time, and a modification on original source (base2)\n        boolean modTimeSet = base1.resolve(filename).toFile().setLastModified(System.currentTimeMillis() + 10_000);\n        Files.write(base2.resolve(filename), \"add to end\".getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);\n\n        // check stability\n        List<String> ops = new ArrayList<>();\n        DirectorySync.syncDir(localFs, remoteFs, true, true, null, null, syncedState, 32, 5, crypto, () -> false, ops::add);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Set<String> all = syncedState.allFilePaths();\n        Assert.assertEquals(1, all.size());\n    }\n\n    @Test\n    public void ignoreLocalDeleteBeforeConflict() throws Exception {\n        ignoreLocalDeleteBeforeConflict(6 * 1024 * 1024);\n        ignoreLocalDeleteBeforeConflict(1024);\n    }\n\n    public void ignoreLocalDeleteBeforeConflict(int fileSize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = false;\n        boolean syncRemoteDeletes = true;\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[fileSize];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // delete local file and check remote is not deleted\n        Files.delete(base1.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(base2.resolve(filename).toFile().exists());\n        Assert.assertFalse(base1.resolve(filename).toFile().exists());\n        Assert.assertTrue(syncedState.hasLocalDelete(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(syncedState.hasLocalDelete(filename));\n        Assert.assertTrue(base2.resolve(filename).toFile().exists());\n        Assert.assertFalse(base1.resolve(filename).toFile().exists());\n\n        // add a different local file with the same name (it should be renamed, and then synced)\n        byte[] data2 = new byte[fileSize + 1024 * 1024];\n        new Random(28).nextBytes(data2);\n        Files.write(base1.resolve(filename), data2, StandardOpenOption.CREATE);\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertArrayEquals(Files.readAllBytes(base2.resolve(filename)), data);\n        Assert.assertArrayEquals(Files.readAllBytes(base1.resolve(filename)), data);\n        Assert.assertFalse(syncedState.hasLocalDelete(filename));\n        Assert.assertEquals(2, syncedState.allFilePaths().size());\n    }\n\n    @Test\n    public void ignoreLocalDeleteBeforeRestore() throws Exception {\n        ignoreLocalDeleteBeforeRestore(6 * 1024 * 1024);\n        ignoreLocalDeleteBeforeRestore(1024);\n    }\n\n    public void ignoreLocalDeleteBeforeRestore(int fileSize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = false;\n        boolean syncRemoteDeletes = true;\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[fileSize];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // delete local file and check remote is not deleted\n        Files.delete(base1.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(base2.resolve(filename).toFile().exists());\n        Assert.assertTrue(syncedState.hasLocalDelete(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(syncedState.hasLocalDelete(filename));\n        Assert.assertTrue(base2.resolve(filename).toFile().exists());\n\n        // restore the local file (it should be removed from the delete list)\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertArrayEquals(Files.readAllBytes(base2.resolve(filename)), data);\n        Assert.assertArrayEquals(Files.readAllBytes(base1.resolve(filename)), data);\n        Assert.assertFalse(syncedState.hasLocalDelete(filename));\n        Assert.assertEquals(1, syncedState.allFilePaths().size());\n    }\n\n    @Test\n    public void ignoreLocalDeleteBeforeRemoteModification() throws Exception {\n        ignoreLocalDeleteBeforeRemoteModification(6 * 1024 * 1024);\n        ignoreLocalDeleteBeforeRemoteModification(1024);\n    }\n\n    public void ignoreLocalDeleteBeforeRemoteModification(int fileSize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = false;\n        boolean syncRemoteDeletes = true;\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[fileSize];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // delete local file and check remote is not deleted\n        Files.delete(base1.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(base2.resolve(filename).toFile().exists());\n        Assert.assertTrue(syncedState.hasLocalDelete(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(syncedState.hasLocalDelete(filename));\n        Assert.assertTrue(base2.resolve(filename).toFile().exists());\n\n        // modify the remote file (it should be copied to local)\n        byte[] data2 = new byte[fileSize + 1024 * 1024];\n        new Random(28).nextBytes(data2);\n        Files.delete(base2.resolve(filename));\n        Files.write(base2.resolve(filename), data2, StandardOpenOption.CREATE);\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertArrayEquals(Files.readAllBytes(base2.resolve(filename)), data2);\n        Assert.assertArrayEquals(Files.readAllBytes(base1.resolve(filename)), data2);\n        Assert.assertFalse(syncedState.hasLocalDelete(filename));\n        Assert.assertEquals(1, syncedState.allFilePaths().size());\n    }\n\n    @Test\n    public void ignoreRemoteDeleteBeforeConflict() throws Exception {\n        ignoreRemoteDeleteBeforeConflict(6 * 1024 * 1024);\n        ignoreRemoteDeleteBeforeConflict(1024);\n    }\n\n    public void ignoreRemoteDeleteBeforeConflict(int fileSize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = true;\n        boolean syncRemoteDeletes = false;\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[fileSize];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // delete remote file and check local is not deleted\n        Files.delete(base2.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(base1.resolve(filename).toFile().exists());\n        Assert.assertTrue(syncedState.hasRemoteDelete(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(syncedState.hasRemoteDelete(filename));\n        Assert.assertTrue(base1.resolve(filename).toFile().exists());\n\n        // add a different remote file with the same name (local should be renamed, and then new remote synced)\n        byte[] data2 = new byte[fileSize + 1024 * 1024];\n        new Random(28).nextBytes(data2);\n        Files.write(base2.resolve(filename), data2, StandardOpenOption.CREATE);\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertArrayEquals(Files.readAllBytes(base2.resolve(filename)), data2);\n        Assert.assertArrayEquals(Files.readAllBytes(base1.resolve(filename)), data2);\n        Assert.assertFalse(syncedState.hasRemoteDelete(filename));\n        Set<String> paths = syncedState.allFilePaths();\n        Assert.assertEquals(2, paths.size());\n        String renamed = paths.stream().filter(p -> !p.equals(filename)).findFirst().get();\n        Assert.assertArrayEquals(Files.readAllBytes(base1.resolve(renamed)), data);\n        Assert.assertArrayEquals(Files.readAllBytes(base2.resolve(renamed)), data);\n    }\n\n    @Test\n    public void ignoreRemoteDeleteBeforeRestore() throws Exception {\n        ignoreRemoteDeleteBeforeRestore(6 * 1024 * 1024);\n        ignoreRemoteDeleteBeforeRestore(1024);\n    }\n\n    public void ignoreRemoteDeleteBeforeRestore(int fileSize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = true;\n        boolean syncRemoteDeletes = false;\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[fileSize];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // delete remote file and check local is not deleted\n        Files.delete(base2.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(base1.resolve(filename).toFile().exists());\n        Assert.assertTrue(syncedState.hasRemoteDelete(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(syncedState.hasRemoteDelete(filename));\n        Assert.assertTrue(base1.resolve(filename).toFile().exists());\n\n        // restore the remote file (it should be removed from the delete list)\n        Files.write(base2.resolve(filename), data, StandardOpenOption.CREATE);\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertArrayEquals(Files.readAllBytes(base2.resolve(filename)), data);\n        Assert.assertArrayEquals(Files.readAllBytes(base1.resolve(filename)), data);\n        Assert.assertFalse(syncedState.hasRemoteDelete(filename));\n        Assert.assertEquals(1, syncedState.allFilePaths().size());\n    }\n\n    @Test\n    public void ignoreRemoteDeleteBeforeRemoteModification() throws Exception {\n        ignoreRemoteDeleteBeforeRemoteModification(6 * 1024 * 1024);\n        ignoreRemoteDeleteBeforeRemoteModification(1024);\n    }\n\n    public void ignoreRemoteDeleteBeforeRemoteModification(int fileSize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = true;\n        boolean syncRemoteDeletes = false;\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        byte[] data = new byte[fileSize];\n        new Random(42).nextBytes(data);\n        String filename = \"file.bin\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n\n        // delete remote file and check local is not deleted\n        Files.delete(base2.resolve(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(base1.resolve(filename).toFile().exists());\n        Assert.assertTrue(syncedState.hasRemoteDelete(filename));\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertNotNull(syncedState.byPath(filename));\n        Assert.assertTrue(syncedState.hasRemoteDelete(filename));\n        Assert.assertTrue(base1.resolve(filename).toFile().exists());\n\n        // modify the local file (it should be copied to remote)\n        byte[] data2 = new byte[fileSize + 1024 * 1024];\n        new Random(28).nextBytes(data2);\n        Files.delete(base1.resolve(filename));\n        Files.write(base1.resolve(filename), data2, StandardOpenOption.CREATE);\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n        Assert.assertArrayEquals(Files.readAllBytes(base2.resolve(filename)), data2);\n        Assert.assertArrayEquals(Files.readAllBytes(base1.resolve(filename)), data2);\n        Assert.assertFalse(syncedState.hasRemoteDelete(filename));\n        Assert.assertEquals(1, syncedState.allFilePaths().size());\n    }\n\n    @Test\n    public void modifyLargeFile() throws Exception {\n        modifyLargeFile(6 * 1024 * 1024);\n    }\n\n    public void modifyLargeFile(int fileSize) throws Exception {\n        Path base1 = Files.createTempDirectory(\"peergos-sync\");\n        Path base2 = Files.createTempDirectory(\"peergos-sync\");\n\n        LocalFileSystem localFs = new LocalFileSystem(base1, Main.initCrypto().hasher);\n        LocalFileSystem remoteFs = new LocalFileSystem(base2, Main.initCrypto().hasher);\n        SyncState syncedState = new JdbcTreeState(\":memory:\");\n\n        boolean syncLocalDeletes = true;\n        boolean syncRemoteDeletes = true;\n\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        // Create and sync a file\n        byte[] data = new byte[fileSize];\n        new Random(42).nextBytes(data);\n        String filename = \"document.txt\";\n        Files.write(base1.resolve(filename), data, StandardOpenOption.CREATE);\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        FileState synced1 = syncedState.byPath(filename);\n        Assert.assertNotNull(synced1);\n\n        // User edits the file locally (real content change)\n        Thread.sleep(10);\n        byte[] newData = new byte[fileSize + 1024];\n        new Random(99).nextBytes(newData);\n        Files.write(base1.resolve(filename), newData, StandardOpenOption.CREATE);\n\n        // Sync the change\n        DirectorySync.syncDir(localFs, remoteFs, syncLocalDeletes, syncRemoteDeletes, null, null, syncedState, 32, 5, crypto, () -> false, DirectorySync::log);\n\n        // Verify the file is synced correctly\n        Assert.assertArrayEquals(newData, Files.readAllBytes(base1.resolve(filename)));\n        Assert.assertArrayEquals(newData, Files.readAllBytes(base2.resolve(filename)));\n    }\n\n    @Test\n    public void treeStateStore() throws IOException {\n        Crypto crypto = Main.initCrypto();\n        Path tmp = Files.createTempDirectory(\"peergos-sync-test\");\n        JdbcTreeState synced = new JdbcTreeState(tmp.resolve(\"syndb.sql\").toString());\n        Assert.assertFalse(synced.hasCompletedSync());\n        synced.setCompletedSync(true);\n        Assert.assertTrue(synced.hasCompletedSync());\n        HashTree hash = HashTree.build(Arrays.asList(new byte[32]), crypto.hasher).join();\n        String path = \"some-path\";\n        FileState state1 = new FileState(path, 12345000, 12345, hash);\n        synced.add(state1);\n        FileState retrieved = synced.byPath(path);\n        Assert.assertEquals(retrieved.modificationTime, state1.modificationTime);\n        Assert.assertEquals(retrieved.size, state1.size);\n        FileState state2 = new FileState(path, 12346000, 12346, hash);\n        synced.add(state2);\n        retrieved = synced.byPath(path);\n        Assert.assertEquals(retrieved.modificationTime, state2.modificationTime);\n        Assert.assertEquals(retrieved.size, state2.size);\n        Cid c = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, new byte[32]);\n        PublicKeyHash writer = new PublicKeyHash(c);\n        CommittedWriterData base = new CommittedWriterData(MaybeMultihash.of(c), Optional.empty(), Optional.of(3L));\n        Snapshot original = new Snapshot(writer, base);\n        synced.setSnapshot(\"/some/dir\", original);\n        synced.setSnapshot(\"/some/dir\", original);\n        Snapshot s = synced.getSnapshot(\"/some/dir\");\n        Assert.assertTrue(s.equals(original));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/TestJniTweetNacl.java",
    "content": "package peergos.server.tests;\nimport org.junit.*;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\nimport peergos.server.crypto.JniTweetNacl;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@RunWith(Parameterized.class)\npublic class TestJniTweetNacl {\n\n\n    private static JniTweetNacl.Signer signer;\n    private static JniTweetNacl.Symmetric symmetric;\n\n    private static Random random = new Random(1337);\n    private static boolean isWindows() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"windows\");\n    }\n    private static boolean isMacos() {\n        return System.getProperty(\"os.name\").toLowerCase().startsWith(\"mac\");\n    }\n\n    @BeforeClass public static void init() {\n        if (isWindows() || isMacos())\n            return;\n        JniTweetNacl instance = JniTweetNacl.build();\n\n        signer = new JniTweetNacl.Signer(instance);\n        symmetric = new JniTweetNacl.Symmetric(instance);\n        random.setSeed(1337);\n    }\n\n\n    public final int messageLength;\n\n    public TestJniTweetNacl(int messageLength) {\n        this.messageLength = messageLength;\n    }\n\n    @Parameterized.Parameters(name = \"{0}\")\n    public static Collection<Object[]> parameters() {\n        //spiral out\n        int  i=1, j=1;\n        int cutoff = 1024 * 1024;\n        List<Integer> fibs  = new ArrayList<>();\n\n        while  (i < cutoff) {\n            int k = j;\n            j = j+i;\n            i = k;\n            fibs.add(i);\n        }\n        return fibs.stream().map(e -> new Object[]{e})\n                .collect(Collectors.toList());\n    }\n\n    @Test\n    public void testSigningIdentity() {\n        if (isWindows() || isMacos())\n            return;\n        byte[] secretSignBytes = new byte[64];\n        byte[] publicSignBytes = new byte[32];\n        signer.crypto_sign_keypair(publicSignBytes, secretSignBytes);\n        byte[] message = new byte[messageLength];\n        random.nextBytes(message);\n        byte[] signed = signer.crypto_sign(message, secretSignBytes).join();\n        byte[] unsigned = signer.crypto_sign_open(signed, publicSignBytes).join();\n\n        Assert.assertArrayEquals(message, unsigned);\n        Assert.assertFalse(Arrays.equals(message, signed));\n        Assert.assertFalse(Arrays.equals(signed, unsigned));\n    }\n\n    @Test\n    public void testSecretboxIdentity() {\n        if (isWindows() || isMacos())\n            return;\n        byte[] key = new byte[32];\n        byte[] nonce = new byte[32];\n        random.nextBytes(key);\n        random.nextBytes(nonce);\n\n        byte[] message = new byte[messageLength];\n        random.nextBytes(message);\n\n        byte[] boxed = symmetric.secretbox(message, nonce, key);\n        byte[] unboxed = symmetric.secretbox_open(boxed, nonce, key);\n        Assert.assertArrayEquals(message, unboxed);\n        Assert.assertFalse(Arrays.equals(message, boxed));\n        Assert.assertFalse(Arrays.equals(boxed, unboxed));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/TokenSignupTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.storage.admin.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.user.*;\n\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\n@RunWith(Parameterized.class)\npublic class TokenSignupTests {\n\n    private static Args args = UserTests.buildArgs()\n            .with(\"useIPFS\", \"false\")\n            .with(\"max-users\", \"1\");\n    protected static final Crypto crypto = Main.initCrypto();\n\n    private final NetworkAccess network;\n    private final UserService service;\n\n    public TokenSignupTests(NetworkAccess network, UserService service) {\n        this.network = network;\n        this.service = service;\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() throws Exception {\n        UserService service = Main.PKI_INIT.main(args).localApi;\n        WriteSynchronizer synchronizer = new WriteSynchronizer(service.mutable, service.storage, crypto.hasher);\n        MutableTree mutableTree = new MutableTreeImpl(service.mutable, service.storage, crypto.hasher, synchronizer);\n        // use actual http messager\n        ServerMessager.HTTP serverMessager = new ServerMessager.HTTP(new JavaPoster(new URI(\"http://localhost:\" + args.getArg(\"port\")).toURL(), false));\n        NetworkAccess network = new NetworkAccess(service.coreNode, service.account, service.social, service.storage,\n                service.bats, Optional.empty(), service.mutable, mutableTree, synchronizer, service.controller, service.usage,\n                serverMessager, service.crypto.hasher,\n                Arrays.asList(\"peergos\"), false);\n        return Arrays.asList(new Object[][] {\n                {network, service}\n        });\n    }\n\n    @AfterClass\n    public static void cleanup() {\n        try {Thread.sleep(2000);}catch (InterruptedException e) {}\n        Path peergosDir = args.fromPeergosDir(\"\", \"\");\n        System.out.println(\"Deleting \" + peergosDir);\n        UserTests.deleteFiles(peergosDir.toFile());\n    }\n\n    @Test\n    public void signupWithToken() {\n        String username = \"q\";\n        String password = \"test\";\n        String badtoken = \"notvalid\";\n        // invalid token fails\n        try {\n            UserContext.signUp(username, password, badtoken, network, crypto).join();\n            throw new RuntimeException(\"Shouldn't get here!\");\n        } catch (CompletionException e) {}\n\n        String token = ((Admin)service.controller).generateSignupToken(crypto.random);\n        UserContext.signUp(username, password, token, network, crypto).join();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/TorTest.java",
    "content": "package peergos.server.tests;\nimport java.util.logging.*;\nimport peergos.server.util.Logging;\n\nimport org.junit.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\n\npublic class TorTest {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    @Ignore\n    @Test\n    public void connectlToHiddenServiceNatively() throws Exception {\n//        boolean tor = true;\n        boolean tor = false;\n        boolean smallFile = true;\n        SocketAddress addr = tor ?\n                new InetSocketAddress(\"localhost\", 9050) :\n                new InetSocketAddress(\"localhost\", 4444);\n        Proxy proxy = tor ?\n                new Proxy(Proxy.Type.SOCKS, addr) :\n                new Proxy(Proxy.Type.HTTP, addr);\n        long t0 = System.currentTimeMillis();\n\n        int respSize = smallFile ?\n                128 * 1024 :\n                100 * 1024 * 1024;\n        int threads = 2;\n        AtomicLong requests = new AtomicLong(0);\n        ExecutorService pool = Executors.newFixedThreadPool(threads);\n        List<Future> futs = new ArrayList<>();\n        for (int t=0; t < threads; t++)\n            futs.add(pool.submit(() -> {\n                List<String> files = smallFile ?\n                        Arrays.asList(\n                                \"api/v0/block/get?stream-channels=true&arg=zb2rhZ2ME5SAUFnqe8b6sgkPnSAMBUruQBjayJo1p7kE7gdsc\",\n                                \"api/v0/block/get?stream-channels=true&arg=zb2rhfT1FuzXQodxCLYkR76mxnpm2xLtMrTJzYTWvoe9ARZ96\"\n                                ) :\n                        Arrays.asList(\"big.tar.gz\");\n                for (int i = 0; i < 10000; i++) {\n                    try {\n                        URL url = tor ?\n                                new URL(\"http://s2prds2oc2ujvnmm.onion/\" + files.get(i % files.size())) :\n                                new URL(\"http://cpozng3fspyr7vg5i3ebqlncnzfn2lmxfkfkqbcyg52xnrs2vydq.b32.i2p/\" + files.get(i % files.size()));\n                        HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy);\n                        conn.connect();\n                        InputStream in = conn.getInputStream();\n                        int size = conn.getContentLength();\n                        if (size < 0) {\n                            LOG.severe(\"negative body size!\");\n                            continue;\n                        }\n                        byte[] bytes = Serialize.read(in, size);\n                        long now = System.currentTimeMillis();\n                        long reqs = requests.incrementAndGet();\n                        if (reqs % 1 == 0) {\n                            LOG.info(\"Average bandwidth: \" + reqs * respSize * 1000/ (now - t0)/1024 + \" kiB/S, Average \" + reqs * 1000.0/ (now - t0) + \" requests/s\");\n                        }\n                    } catch (IOException e) {\n                        LOG.log(Level.WARNING, e.getMessage(), e);\n                    }\n                }\n            }));\n        for (Future fut : futs) {\n            fut.get();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/TotpTest.java",
    "content": "package peergos.server.tests;\n\nimport com.eatthepath.otp.*;\nimport org.junit.*;\nimport peergos.shared.QRCodeEncoder;\nimport peergos.shared.io.ipfs.bases.Base32;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.zxing.*;\nimport peergos.shared.zxing.common.*;\nimport peergos.shared.zxing.qrcode.*;\n\nimport javax.crypto.spec.*;\nimport java.nio.file.*;\nimport java.security.*;\nimport java.time.*;\n\npublic class TotpTest {\n\n    @Test\n    public void roundtrip() throws Exception {\n        // Google authenticator is hard coded to this and will silently ignore attempts to use HmacSha256. Thanks Google!\n        TimeBasedOneTimePasswordGenerator totp = new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30L), 6, TotpKey.ALGORITHM);\n        byte[] rawKey = new Base32().decode(\"W6AWIT4QDM3BIHILNZUHRLAQ7DWAC4IODJBUZWHQGKVKA3DVJQ6Q\");\n        Key key = new SecretKeySpec(rawKey, TotpKey.ALGORITHM);\n\n        byte[] encoded = key.getEncoded();\n        Instant now = Instant.now();\n        Duration timeStep = totp.getTimeStep();\n        String clientCode = totp.generateOneTimePasswordString(key, now);\n        String serverCode = totp.generateOneTimePasswordString(key, now.plus(timeStep.dividedBy(2)));\n        String serverCode2 = totp.generateOneTimePasswordString(key, now.minus(timeStep.dividedBy(2)));\n        Assert.assertTrue(serverCode.equals(clientCode) || serverCode2.equals(clientCode));\n\n        //  generate a QR code\n        QRCodeWriter writer = new QRCodeWriter();\n        String issuer = \"peergos\";\n        String label = issuer + \":demo@peergos\";\n        String originalText = \"otpauth://totp/\" + label + \"?secret=\" + new Base32().encodeToString(encoded).replaceAll(\"=\",\"\")\n                + \"&issuer=\" + issuer;\n\n        BitMatrix result = writer.encode(originalText, BarcodeFormat.QR_CODE, 512, 512);\n        byte[] png = QRCodeEncoder.encodeToPng(0, result.getWidth(), result.getHeight(), result);\n        Files.write(Paths.get(\"totp-qr-code.png\"), png);\n        System.out.println(\"Try scanning QR code with an app...\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/TransactionsStoreTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\n\nimport java.security.*;\nimport java.util.*;\n\n@RunWith(Parameterized.class)\npublic class TransactionsStoreTests {\n\n    private final TransactionStore store;\n\n    public TransactionsStoreTests(TransactionStore store) {\n        this.store = store;\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() throws Exception {\n        TransactionStore ram = JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands());\n        return Arrays.asList(new Object[][] {\n                {ram}\n        });\n    }\n\n    public static byte[] hash(byte[] input) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"SHA-256\");\n            md.update(input);\n            return md.digest();\n        } catch (NoSuchAlgorithmException e) {\n            throw new IllegalStateException(\"couldn't find hash algorithm\");\n        }\n    }\n\n    public static Cid hashToCid(byte[] input, boolean isRaw) {\n        byte[] hash = hash(input);\n        return new Cid(1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n\n    @Test\n    public void singleTransaction() {\n        Cid multihash = hashToCid(new byte[2], true);\n        PublicKeyHash owner = new PublicKeyHash(multihash);\n        TransactionId tid = store.startTransaction(owner);\n        List<Multihash> pending = new ArrayList<>();\n        for (int i=0; i < 20; i++) {\n            Cid block = hashToCid(new byte[]{(byte) i}, false);\n            pending.add(block);\n            store.addBlock(block, tid, owner);\n        }\n        List<Cid> uncommitted = store.getOpenTransactionBlocks(owner);\n        Assert.assertTrue(\"All blocks present\", uncommitted.containsAll(pending));\n\n        store.closeTransaction(owner, tid);\n        List<Cid> empty = store.getOpenTransactionBlocks(owner);\n        Assert.assertTrue(\"All blocks removed\", empty.isEmpty());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/TreeHash.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.crypto.hash.ScryptJava;\nimport peergos.shared.user.fs.Chunk;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.List;\nimport java.util.Random;\n\npublic class TreeHash {\n\n    @Test\n    public void parallelTreeHash() {\n        for (long chunks=0; chunks < 1024; chunks += 200) {\n            for (long size : List.of(\n                    chunks * Chunk.MAX_SIZE + 1024,\n                    Math.max(0, chunks * Chunk.MAX_SIZE - 1024))) {\n                long t0 = System.nanoTime();\n                List<byte[]> parallel = ScryptJava.parallelHashChunks(() -> new RandomStream(size), 8, size);\n                long t1 = System.nanoTime();\n\n                List<byte[]> serial = ScryptJava.hashChunks(new RandomStream(size), size);\n                long t2 = System.nanoTime();\n                long sizeMiB = size / 1024 / 1024;\n                if (sizeMiB > 0) {\n                    System.out.println(\"parallel took \" + (t1 - t0) / 1_000_000_000 + \", serial took \" + (t2 - t1) / 1_000_000_000);\n                    System.out.println(\"Speed up \" + (t2 - t1) / (t1 - t0) + \" for file size \" + sizeMiB + \" MiB\");\n                }\n                long expectedChunks = Math.max(1, (size + Chunk.MAX_SIZE - 1) / Chunk.MAX_SIZE);\n                Assert.assertEquals(parallel.size(), expectedChunks);\n                Assert.assertEquals(parallel.size(), serial.size());\n                for (int i = 0; i < parallel.size(); i++)\n                    Assert.assertArrayEquals(parallel.get(i), serial.get(i));\n            }\n        }\n    }\n\n    static class RandomStream extends InputStream {\n        final int val = new Random(42).nextInt() & 0xFF;\n        private final long size;\n        private long read = 0;\n\n        public RandomStream(long size) {\n            this.size = size;\n        }\n\n        @Override\n        public int read() throws IOException {\n            if (read >= size)\n                return -1;\n            read++;\n            return val;\n        }\n\n        @Override\n        public long skip(long n) throws IOException {\n            read += n;\n            return n;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/UserPublicKeyLinkTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.crypto.asymmetric.curve25519.*;\nimport peergos.server.crypto.random.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.UserPublicKeyLink;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.time.LocalDate;\nimport java.util.*;\n\n\npublic class UserPublicKeyLinkTests {\n    private final ContentAddressedStorage ipfs;\n\n    {\n        ipfs = new FileContentAddressedStorage(PathUtil.get(\"blockstore\"),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                    JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, Main.initCrypto().hasher);\n    }\n\n    private final List<Multihash> id;\n\n    public UserPublicKeyLinkTests() throws Exception {\n        id = Arrays.asList(ipfs.id().join());\n    }\n\n    @BeforeClass\n    public static void init() {\n        PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, new Ed25519Java());\n    }\n\n    private PublicKeyHash putPublicSigningKey(SigningKeyPair user) throws Exception {\n        PublicKeyHash owner = ContentAddressedStorage.hashKey(user.publicSigningKey);\n        return ipfs.putSigningKey(\n                user.secretSigningKey.signMessage(user.publicSigningKey.serialize()).join(),\n                owner,\n                user.publicSigningKey, ipfs.startTransaction(owner).join()).get();\n    }\n\n    @Test\n    public void createInitial() throws Exception {\n        SigningKeyPair user = SigningKeyPair.random(new SafeRandomJava(), new Ed25519Java());\n        UserPublicKeyLink.Claim node = UserPublicKeyLink.Claim.build(\"someuser\", user.secretSigningKey, LocalDate.now().plusYears(2), id).join();\n\n        PublicKeyHash owner = putPublicSigningKey(user);\n        UserPublicKeyLink upl = new UserPublicKeyLink(owner, node);\n        testSerialization(upl);\n    }\n\n    public void testSerialization(UserPublicKeyLink link) {\n        byte[] serialized1 = link.serialize();\n        UserPublicKeyLink upl2 = UserPublicKeyLink.fromCbor(CborObject.fromByteArray(serialized1));\n        byte[] serialized2 = upl2.serialize();\n        if (!Arrays.equals(serialized1, serialized2))\n            throw new IllegalStateException(\"toByteArray not inverse of fromByteArray!\");\n    }\n\n    @Test\n    public void createChain() throws Exception {\n        SigningKeyPair oldUser = SigningKeyPair.random(new SafeRandomJava(), new Ed25519Java());\n        SigningKeyPair newUser = SigningKeyPair.random(new SafeRandomJava(), new Ed25519Java());\n        PublicKeyHash oldHash = putPublicSigningKey(oldUser);\n        PublicKeyHash newHash = putPublicSigningKey(newUser);\n\n        SigningPrivateKeyAndPublicHash oldSigner = new SigningPrivateKeyAndPublicHash(oldHash, oldUser.secretSigningKey);\n        SigningPrivateKeyAndPublicHash newSigner = new SigningPrivateKeyAndPublicHash(newHash, newUser.secretSigningKey);\n\n        List<UserPublicKeyLink> links = UserPublicKeyLink.createChain(oldSigner, newSigner, \"someuser\", LocalDate.now().plusYears(2), id).join();\n        links.forEach(link -> testSerialization(link));\n    }\n\n    @Test\n    public void repeatedPassword() throws Exception {\n        SigningKeyPair oldUser = SigningKeyPair.random(new SafeRandomJava(), new Ed25519Java());\n        SigningKeyPair newUser = SigningKeyPair.random(new SafeRandomJava(), new Ed25519Java());\n        PublicKeyHash oldHash = putPublicSigningKey(oldUser);\n        PublicKeyHash newHash = putPublicSigningKey(newUser);\n\n        SigningPrivateKeyAndPublicHash oldSigner = new SigningPrivateKeyAndPublicHash(oldHash, oldUser.secretSigningKey);\n        SigningPrivateKeyAndPublicHash newSigner = new SigningPrivateKeyAndPublicHash(newHash, newUser.secretSigningKey);\n\n        String username = \"someuser\";\n        LocalDate expiry = LocalDate.now().plusYears(2);\n        List<UserPublicKeyLink> initial = UserPublicKeyLink.createInitial(oldSigner, username, expiry, id).join();\n        List<UserPublicKeyLink> newPassword = UserPublicKeyLink.createChain(oldSigner, newSigner, username, expiry, id).join();\n        List<UserPublicKeyLink> changed = UserPublicKeyLink.merge(initial, newPassword, ipfs).join();\n        List<UserPublicKeyLink> backToOldPassword = UserPublicKeyLink.createChain(newSigner, oldSigner, username, expiry, id).join();\n        List<UserPublicKeyLink> finalChain = Arrays.asList(changed.get(0), backToOldPassword.get(0), backToOldPassword.get(1));\n        try {\n            UserPublicKeyLink.merge(changed, finalChain, ipfs).join();\n        } catch (Exception e) {\n            return;\n        }\n        throw new IllegalStateException(\"Should have failed!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/UserTests.java",
    "content": "package peergos.server.tests;\nimport java.net.URLDecoder;\nimport java.time.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.logging.*;\n\nimport peergos.server.crypto.asymmetric.curve25519.*;\nimport peergos.server.crypto.hash.*;\nimport peergos.server.crypto.random.*;\nimport peergos.server.crypto.symmetric.*;\nimport peergos.server.messages.*;\nimport peergos.server.sync.DirectorySync;\nimport peergos.server.tests.util.*;\nimport peergos.server.user.*;\nimport peergos.server.util.*;\n\nimport org.junit.*;\nimport static org.junit.Assert.*;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.asymmetric.curve25519.Curve25519PublicKey;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMPublicKey;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.server.*;\nimport peergos.shared.hamt.ChampWrapper;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.bases.Charsets;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\nimport peergos.shared.util.Exceptions;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic abstract class UserTests {\n\tprivate static final Logger LOG = Logging.LOG();\n    static {\n        ThumbnailGenerator.setInstance(new JavaImageThumbnailer());\n    }\n\n    public static int RANDOM_SEED = 666;\n    protected final NetworkAccess network;\n    protected final UserService service;\n    protected static final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public UserTests(NetworkAccess network, UserService service) {\n        this.network = network;\n        this.service = service;\n    }\n\n    public abstract Args getArgs();\n\n    public static Args buildArgs() {\n        try {\n            Path peergosDir = Files.createTempDirectory(\"peergos\");\n            int port = TestPorts.getPort();\n            int proxyPort = TestPorts.getPort();\n            int gatewayPort = TestPorts.getPort();\n            int ipfsApiPort = TestPorts.getPort();\n            int ipfsGatewayPort = TestPorts.getPort();\n            int ipfsSwarmPort = TestPorts.getPort();\n            return Args.parse(new String[]{\n                    \"-port\", Integer.toString(port),\n                    \"-proxy-target\", \"/ip4/127.0.0.1/tcp/\" + proxyPort,\n                    \"-gateway-port\", Integer.toString(gatewayPort),\n                    \"-ipfs-api-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsApiPort,\n                    \"-ipfs-gateway-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsGatewayPort,\n                    \"-ipfs-swarm-port\", Integer.toString(ipfsSwarmPort),\n                    \"-ipfs.metrics.port\", Integer.toString(TestPorts.getPort()),\n                    \"-admin-usernames\", \"peergos\",\n                    \"-logToConsole\", \"true\",\n                    \"-enable-gc\", \"true\",\n                    \"-gc.period.millis\", \"60000\",\n                    \"max-users\", \"10000\",\n                    \"max-daily-signups\", \"20000\",\n                    Main.PEERGOS_PATH, peergosDir.toString(),\n                    \"peergos.password\", \"testpassword\",\n                    \"pki.keygen.password\", \"testpkipassword\",\n                    \"pki.keyfile.password\", \"testpassword\",\n            });\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void deleteFiles(File f) {\n        if (! f.exists())\n            return;\n        if (f.isDirectory()) {\n            for (File child : f.listFiles()) {\n                deleteFiles(child);\n            }\n        }\n        f.delete();\n    }\n\n    public void gc() {\n        service.gc.collect(e -> Futures.of(true));\n    }\n\n    protected String generateUsername() {\n        return \"test\" + Math.abs(random.nextInt() % 1_000_000);\n    }\n\n    private static CompletableFuture<FileWrapper> uploadFileSection(FileWrapper parent,\n                                                                    String filename,\n                                                                    AsyncReader fileData,\n                                                                    long startIndex,\n                                                                    long endIndex,\n                                                                    NetworkAccess network,\n                                                                    Crypto crypto,\n                                                                    ProgressConsumer<Long> monitor) {\n        return parent.uploadFileSection(filename, fileData, false, startIndex, endIndex, Optional.empty(),\n                true, network, crypto, () -> false, monitor, crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)),\n                parent.mirrorBatId());\n    }\n\n    @After\n    public void clearBuffer() {\n        if (network instanceof BufferedNetworkAccess) {\n            ((BufferedNetworkAccess) network).forceClear();\n        } else\n            network.clear();\n    }\n\n    @Test\n    public void serializationSizesSmall() {\n        SigningKeyPair signer = SigningKeyPair.random(crypto.random, crypto.signer);\n        byte[] rawSignPub = signer.publicSigningKey.serialize(); // 36\n        byte[] rawSignSecret = signer.secretSigningKey.serialize(); // 68\n        byte[] rawSignBoth = signer.serialize(); // 105\n        BoxingKeyPair boxer = BoxingKeyPair.randomCurve25519(crypto.random, crypto.boxer);\n        byte[] rawBoxPub = boxer.publicBoxingKey.serialize(); // 36\n        byte[] rawBoxSecret = boxer.secretBoxingKey.serialize(); // 36\n        byte[] rawBoxBoth = boxer.serialize(); // 73\n        SymmetricKey sym = SymmetricKey.random();\n        byte[] rawSym = sym.serialize(); // 37\n        Assert.assertTrue(\"Serialization overhead isn't too much\", rawSignPub.length <= 32 + 4);\n        Assert.assertTrue(\"Serialization overhead isn't too much\", rawSignSecret.length <= 64 + 4);\n        Assert.assertTrue(\"Serialization overhead isn't too much\", rawSignBoth.length <= 96 + 9);\n        Assert.assertTrue(\"Serialization overhead isn't too much\", rawBoxPub.length <= 32 + 4);\n        Assert.assertTrue(\"Serialization overhead isn't too much\", rawBoxSecret.length <= 32 + 4);\n        Assert.assertTrue(\"Serialization overhead isn't too much\", rawBoxBoth.length <= 64 + 9);\n        Assert.assertTrue(\"Serialization overhead isn't too much\", rawSym.length <= 33 + 4);\n    }\n\n    @Test\n    public void differentLoginTypes() throws Exception {\n        String username = generateUsername();\n        String password = \"letmein\";\n        String extraSalt = ArrayOps.bytesToHex(crypto.random.randomBytes(32));\n        List<ScryptGenerator> params = Arrays.asList(\n                new ScryptGenerator(17, 8, 1, 96, extraSalt),\n                new ScryptGenerator(17, 8, 1, 64, extraSalt),\n                new ScryptGenerator(18, 8, 1, 96, extraSalt),\n                new ScryptGenerator(19, 8, 1, 96, extraSalt),\n                new ScryptGenerator(17, 9, 1, 96, extraSalt)\n        );\n        for (ScryptGenerator p: params) {\n            long t1 = System.currentTimeMillis();\n            UserUtil.generateUser(username, password, crypto, p).get();\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"User gen took \" + (t2 - t1) + \" mS\");\n            System.gc();\n        }\n    }\n\n    @Test\n    public void javascriptCompatible() {\n        String username = generateUsername();\n        String password = \"test01\";\n\n        SafeRandomJava random = new SafeRandomJava();\n        UserUtil.generateUser(username, password, crypto, SecretGenerationAlgorithm.getLegacy(random)).thenAccept(userWithRoot -> {\n\t\t    PublicSigningKey expected = PublicSigningKey.fromString(\"7HvEWP6yd1UD8rOorfFrieJ8S7yC8+l3VisV9kXNiHmI7Eav7+3GTRSVBRCymItrzebUUoCi39M6rdgeOU9sXXFD\");\n\t\t    if (! expected.equals(userWithRoot.getUser().publicSigningKey))\n\t\t        throw new IllegalStateException(\"Generated user different from the Javascript! \\n\"+userWithRoot.getUser().publicSigningKey + \" != \\n\"+expected);\n        });\n    }\n\n    @Test\n    public void randomSignup() {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        InstanceAdmin.VersionInfo version = network.instanceAdmin.getVersionInfo().join();\n        Assert.assertTrue(! version.version.isBefore(Version.parse(\"0.0.0\")));\n\n        FileWrapper userRoot = context.getUserRoot().join();\n        Assert.assertTrue(\"owner uses identity multihash\", userRoot.getPointer().capability.owner.isIdentity());\n        Assert.assertTrue(\"signer uses identity multihash\", userRoot.getPointer().capability.writer.isIdentity());\n        Assert.assertTrue(\"user root does not have a retrievable parent\",\n                ! userRoot.retrieveParent(network).join().isPresent());\n\n        String someUrlFragment = \"Somedata\";\n        UserContext.EncryptedURL encryptedURL = context.encryptURL(someUrlFragment);\n        String decryptedUrl = context.decryptURL(encryptedURL.base64Ciphertext, encryptedURL.base64Nonce);\n        Assert.assertTrue(decryptedUrl.equalsIgnoreCase(someUrlFragment));\n    }\n\n    @Test\n    public void singleSignUp() {\n        // This is to ensure a user can't accidentally sign up rather than login and overwrite all their data\n        String username = generateUsername();\n        String password = \"password\";\n        PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        CompletableFuture<UserContext> secondSignup = UserContext.signUp(username, password, \"\", network, crypto);\n\n        Assert.assertTrue(\"Second sign up fails\", secondSignup.isCompletedExceptionally());\n    }\n\n    public static CompletableFuture<MultiFactorAuthResponse> noMfa(MultiFactorAuthRequest req) {\n        throw new IllegalStateException(\"Unsupported!\");\n    }\n\n    @Test\n    public void errorLoggingInToDeletedAccont() {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        context.deleteAccount(password, UserTests::noMfa).join();\n\n        try {\n            PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        } catch (Exception e) {\n            if (! e.getMessage().contains(\"User has been deleted\"))\n                throw new RuntimeException(\"Incorrect error message\");\n        }\n    }\n\n    @Test\n    public void expiredSignin() {\n        String username = generateUsername();\n        String password = \"password\";\n        // set username claim to an expiry in the past\n        UserContext context = UserContext.signUpGeneral(username, password, \"\", Optional.empty(), id -> {},\n                Optional.empty(), LocalDate.now().minusDays(1),\n                network, crypto, SecretGenerationAlgorithm.getDefault(crypto.random), t -> {}).join();\n\n        LocalDate expiry = context.getUsernameClaimExpiry().join();\n        Assert.assertTrue(expiry.isBefore(LocalDate.now()));\n\n        context.ensureUsernameClaimRenewed().join();\n        UserContext context2 = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        LocalDate expiry2 = context2.getUsernameClaimExpiry().join();\n        Assert.assertTrue(expiry2.isAfter(LocalDate.now()));\n    }\n\n    @Test\n    public void expiredSigninAfterPasswordChange() {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = UserContext.signUpGeneral(username, password, \"\", Optional.empty(), id -> {},\n                Optional.empty(), LocalDate.now().minusDays(2),\n                network, crypto, SecretGenerationAlgorithm.getDefault(crypto.random), x -> {}).join();\n        String newPassword = \"G'day mate!\";\n\n        // change password and set username claim to an expiry in the past\n        SecretGenerationAlgorithm alg = context.getKeyGenAlgorithm().join();\n        SecretGenerationAlgorithm newAlg = SecretGenerationAlgorithm.withNewSalt(alg, crypto.random);\n        context = context.changePassword(password, newPassword, alg, newAlg, LocalDate.now().minusDays(1), UserTests::noMfa).join();\n\n        LocalDate expiry = context.getUsernameClaimExpiry().join();\n        Assert.assertTrue(expiry.isBefore(LocalDate.now()));\n\n        context.ensureUsernameClaimRenewed().join();\n        UserContext context2 = PeergosNetworkUtils.ensureSignedUp(username, newPassword, network, crypto);\n        LocalDate expiry2 = context2.getUsernameClaimExpiry().join();\n        Assert.assertTrue(expiry2.isAfter(LocalDate.now()));\n    }\n\n    @Test\n    public void noRepeatedPassword() {\n        // This is to ensure a user can't change their password to a previously used password\n        String username = generateUsername();\n        String password1 = \"pass1\";\n        String password2 = \"pass2\";\n        UserContext context1 = PeergosNetworkUtils.ensureSignedUp(username, password1, network, crypto);\n        UserContext context2 = context1.changePassword(password1, password2, UserTests::noMfa).join();\n        try {\n            context2.changePassword(password2, password1, UserTests::noMfa).join();\n        } catch (Throwable t) {\n            Assert.assertTrue(t.getMessage().contains(\"You must change to a different password.\"));\n        }\n    }\n\n    @Test\n    public void duplicateSignUp() {\n        String username = generateUsername();\n        String password1 = \"password1\";\n        String password2 = \"password2\";\n        PeergosNetworkUtils.ensureSignedUp(username, password1, network, crypto);\n        try {\n            UserContext.signUp(username, password2, \"\", network, crypto).get();\n        } catch (Exception e) {\n            if (! e.getMessage().contains(\"User already exists\"))\n                Assert.fail(\"Incorrect error message\");\n        }\n    }\n\n    @Test\n    public void repeatedSignUp() {\n        String username = generateUsername();\n        String password = \"password\";\n        PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        try {\n            UserContext.signUp(username, password, \"\", network, crypto).get();\n        } catch (Exception e) {\n            if (!Exceptions.getRootCause(e).getMessage().contains(\"User already exists\"))\n                Assert.fail(\"Incorrect error message\");\n        }\n    }\n\n    @Test\n    public void deleteWritableFolderWithSecretLinkToDescendant() {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        context.getUserRoot().join().mkdir(\"sync\", network, false, context.mirrorBatId(), crypto).join();\n\n        LinkProperties syncLink = DirectorySync.init(context, username + \"/sync\");\n        // create a secret link to a subdir of sync, then delete sync dir\n        Path syncPath = PathUtil.get(username, \"sync\");\n        context.getByPath(syncPath).join().get()\n                .mkdir(\"subdir\", network, false, context.mirrorBatId(), crypto).join();\n        LinkProperties subdirLink = context.createSecretLink(syncPath.resolve(\"subdir\").toString(), false, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        FileWrapper syncDir = context.getByPath(syncPath).join().get();\n        FileWrapper deleted = syncDir.remove(context.getUserRoot().join(), syncPath, context).join();\n\n        Map<Path, SharedWithState> sharedWith = context.sharedWithCache.getAllDescendantShares(syncPath, deleted.version).join();\n        Assert.assertTrue(sharedWith.isEmpty());\n\n        try {\n            // test that the secret link itself has been removed\n            EncryptedCapability ecap = network.dhtClient.getSecretLink(syncLink.toLink(context.signer.publicKeyHash)).join();\n        } catch (Exception e) {\n            if (!URLDecoder.decode(e.getMessage(), Charsets.UTF_8).contains(\"No secret link\"))\n                throw new RuntimeException(\"Failed\");\n        }\n\n        try {\n            // test that the secret link itself has been removed\n            EncryptedCapability ecap = network.dhtClient.getSecretLink(subdirLink.toLink(context.signer.publicKeyHash)).join();\n        } catch (Exception e) {\n            if (! URLDecoder.decode(e.getMessage(), Charsets.UTF_8).contains(\"No secret link\"))\n                throw new RuntimeException(\"Failed\");\n        }\n\n        // now try to init sync dir again\n        context.getUserRoot().join().mkdir(\"sync\", network, false, context.mirrorBatId(), crypto).join();\n        DirectorySync.init(context, username + \"/sync\");\n    }\n\n    @Test\n    public void webdavHashesSet() {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        byte[] contents = new byte[0];\n        HashTreeBuilder hash = new HashTreeBuilder(0);\n        hash.setChunk(0, contents, context.crypto.hasher).join();\n        HashTree h = hash.complete(context.crypto.hasher).join();\n        String filename = \"afile.bin\";\n        context.getUserRoot().join()\n                .uploadFileWithHash(filename, AsyncReader.build(contents), 0, Optional.of(h), Optional.empty(), Optional.empty(), network, crypto, x -> {}).join();\n        FileWrapper emptyFile = context.getByPath(Paths.get(username, filename)).join().get();\n        HashBranch expected = emptyFile.getFileProperties().treeHash.get();\n        HashBranch empty = h.branch(0);\n        Assert.assertEquals(expected, empty);\n\n        byte[] data = new byte[1024];\n        context.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(data), data.length, context.network, context.crypto, () -> false, l -> {}).join();\n        FileWrapper updatedFile = context.getByPath(Paths.get(username, filename)).join().get();\n        HashBranch nonEmpty = updatedFile.getFileProperties().treeHash.get();\n        Assert.assertNotEquals(nonEmpty, empty);\n\n        Assert.assertTrue(updatedFile.getFileProperties().modified.isAfter(emptyFile.getFileProperties().modified));\n    }\n\n    @Test\n    public void changePassword() throws Exception {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext userContext = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        Pair<PublicKeyHash, PublicBoxingKey> keyPairs = userContext.getPublicKeys(username).join().get();\n        PublicBoxingKey initialBoxer = keyPairs.right;\n        PublicKeyHash initialIdentity = keyPairs.left;\n        String newPassword = \"newPassword\";\n        UserContext updated = userContext.changePassword(password, newPassword, UserTests::noMfa).join();\n        MultiUserTests.checkUserValidity(network, username);\n\n        Pair<PublicKeyHash, PublicBoxingKey> updatedPairs = updated.getPublicKeys(username).join().get();\n        PublicBoxingKey newBoxer = updatedPairs.right;\n        PublicKeyHash newIdentity = updatedPairs.left;\n        Assert.assertTrue(newBoxer.equals(initialBoxer));\n        Assert.assertTrue(newIdentity.equals(initialIdentity));\n        UserContext changedPassword = PeergosNetworkUtils.ensureSignedUp(username, newPassword, network, crypto);\n\n        // change it again\n        String password3 = \"pass3\";\n        changedPassword.changePassword(newPassword, password3, UserTests::noMfa).get();\n        MultiUserTests.checkUserValidity(network, username);\n        PeergosNetworkUtils.ensureSignedUp(username, password3, network, crypto);\n    }\n\n    @Test\n    public void legacyLogin() throws Exception {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext userContext = UserContext.signUpGeneral(username, password, \"\", Optional.empty(), id -> {},\n                Optional.empty(), LocalDate.now().plusMonths(2),\n                network, crypto, SecretGenerationAlgorithm.getLegacy(crypto.random), x -> {}).join();\n        SecretGenerationAlgorithm originalAlg = WriterData.fromCbor(UserContext.getWriterDataCbor(network, username).join().right).generationAlgorithm.get();\n        Assert.assertTrue(\"legacy accounts generate boxer\", originalAlg.generateBoxerAndIdentity());\n        Pair<PublicKeyHash, PublicBoxingKey> keyPairs = userContext.getPublicKeys(username).join().get();\n        PublicBoxingKey initialBoxer = keyPairs.right;\n        PublicKeyHash initialIdentity = keyPairs.left;\n        WriterData initialWd = WriterData.getWriterData(initialIdentity, initialIdentity, network.mutable, network.dhtClient).join().props.get();\n        Assert.assertTrue(initialWd.staticData.isPresent());\n\n        List<String> progress = new ArrayList<>();\n        UserContext login = UserContext.signIn(username, password, UserTests::noMfa, false, false, network, crypto, progress::add).join();\n        WriterData afterPqUpgrade = WriterData.getWriterData(login.signer.publicKeyHash, login.signer.publicKeyHash, network.mutable, network.dhtClient).join().props.get();\n        Pair<PublicKeyHash, PublicBoxingKey> pqKeys = login.getPublicKeys(username).join().get();\n        Assert.assertTrue(afterPqUpgrade.staticData.isPresent());\n        Assert.assertTrue(pqKeys.right instanceof HybridCurve25519MLKEMPublicKey);\n        Assert.assertTrue(login.isPostQuantum());\n\n        String newPassword = \"newPassword\";\n        login.changePassword(password, newPassword, UserTests::noMfa).get();\n        MultiUserTests.checkUserValidity(network, username);\n\n        List<String> progress2 = new ArrayList<>();\n        UserContext changedPassword = UserContext.signIn(username, newPassword, UserTests::noMfa, false, false, network, crypto, progress2::add).join();\n        Pair<PublicKeyHash, PublicBoxingKey> newKeyPairs = changedPassword.getPublicKeys(username).join().get();\n        PublicBoxingKey newBoxer = newKeyPairs.right;\n        PublicKeyHash newIdentity = newKeyPairs.left;\n        Assert.assertTrue(! newBoxer.equals(initialBoxer));\n        Assert.assertTrue(newBoxer.equals(pqKeys.right));\n        Assert.assertTrue(! newIdentity.equals(initialIdentity));\n\n        SecretGenerationAlgorithm alg = WriterData.fromCbor(UserContext.getWriterDataCbor(network, username).join().right).generationAlgorithm.get();\n        Assert.assertTrue(\"password change upgrades legacy accounts\", ! alg.generateBoxerAndIdentity());\n        WriterData finalWd = WriterData.getWriterData(newIdentity, newIdentity, network.mutable, network.dhtClient).join().props.get();\n        Assert.assertTrue(finalWd.staticData.isEmpty());\n\n        UserContext pq = UserContext.signIn(username, newPassword, UserTests::noMfa, network, crypto).join();\n        PublicBoxingKey pqBoxer = pq.getPublicKeys(username).join().get().right;\n        Assert.assertTrue(pqBoxer instanceof HybridCurve25519MLKEMPublicKey);\n        Assert.assertTrue(pqBoxer.equals(pqKeys.right));\n    }\n\n    @Test\n    public void legacyToPQ() throws Exception {\n        String username = generateUsername();\n        String password = \"password\";\n        List<String> progress = new ArrayList<>();\n        UserContext userContext = UserContext.signUpGeneral(username, password, \"\", Optional.empty(), id -> {},\n                Optional.empty(), LocalDate.now().plusMonths(2),\n                network, crypto, SecretGenerationAlgorithm.getLegacy(crypto.random), progress::add).join();\n        SecretGenerationAlgorithm originalAlg = WriterData.fromCbor(UserContext.getWriterDataCbor(network, username).join().right).generationAlgorithm.get();\n        Assert.assertTrue(\"legacy accounts generate boxer\", originalAlg.generateBoxerAndIdentity());\n        Pair<PublicKeyHash, PublicBoxingKey> keyPairs = userContext.getPublicKeys(username).join().get();\n        PublicBoxingKey initialBoxer = keyPairs.right;\n        PublicKeyHash initialIdentity = keyPairs.left;\n        WriterData initialWd = WriterData.getWriterData(initialIdentity, initialIdentity, network.mutable, network.dhtClient).join().props.get();\n        Assert.assertTrue(initialWd.staticData.isPresent());\n        Assert.assertTrue(initialBoxer instanceof Curve25519PublicKey);\n\n        String newPassword = \"newPassword\";\n        userContext.changePassword(password, newPassword, UserTests::noMfa).get();\n        MultiUserTests.checkUserValidity(network, username);\n\n        List<String> progress2 = new ArrayList<>();\n        // changing password also upgrade to PQ\n        UserContext changedPassword = UserContext.signIn(username, newPassword, UserTests::noMfa, false, false, network, crypto, progress2::add).join();\n        Pair<PublicKeyHash, PublicBoxingKey> newKeyPairs = changedPassword.getPublicKeys(username).join().get();\n        PublicBoxingKey newBoxer = newKeyPairs.right;\n        PublicKeyHash newIdentity = newKeyPairs.left;\n        Assert.assertTrue(! newBoxer.equals(initialBoxer));\n        Assert.assertTrue(! newIdentity.equals(initialIdentity));\n\n        SecretGenerationAlgorithm alg = WriterData.fromCbor(UserContext.getWriterDataCbor(network, username).join().right).generationAlgorithm.get();\n        Assert.assertTrue(\"password change upgrades legacy accounts\", ! alg.generateBoxerAndIdentity());\n        WriterData finalWd = WriterData.getWriterData(newIdentity, newIdentity, network.mutable, network.dhtClient).join().props.get();\n        Assert.assertTrue(finalWd.staticData.isEmpty());\n\n        UserContext pq = UserContext.signIn(username, newPassword, UserTests::noMfa, network, crypto).join();\n        PublicBoxingKey pqBoxer = pq.getPublicKeys(username).join().get().right;\n        Assert.assertTrue(pqBoxer instanceof HybridCurve25519MLKEMPublicKey);\n        Assert.assertTrue(pqBoxer.equals(newKeyPairs.right));\n\n        // change password again\n        String newerPassword = \"greentrees\";\n        UserContext secondPassChange = pq.changePassword(newPassword, newerPassword, UserTests::noMfa).join();\n        UserContext relogin = UserContext.signIn(username, newerPassword, UserTests::noMfa, network, crypto).join();\n        PublicBoxingKey finalPqBoxer = relogin.getPublicKeys(username).join().get().right;\n        Assert.assertTrue(finalPqBoxer.equals(pqBoxer));\n    }\n\n    @Test\n    public void changePasswordFAIL() throws Exception {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext userContext = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        String newPassword = \"passwordtest\";\n        UserContext newContext = userContext.changePassword(password, newPassword, UserTests::noMfa).get();\n\n        try {\n            UserContext oldContext = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        } catch (Exception e) {\n            if (! e.getMessage().contains(\"Incorrect username or password\") && ! e.getMessage().contains(\"Incorrect+username+or+password\"))\n                throw e;\n        }\n    }\n\n    @Test\n    public void changeLoginAlgorithm() throws Exception {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        SecretGenerationAlgorithm algo = context.getKeyGenAlgorithm().get();\n        ScryptGenerator newAlgo = new ScryptGenerator(19, 8, 1, 64, algo.getExtraSalt());\n        context.changePassword(password, password, algo, newAlgo, LocalDate.now().plusMonths(2), UserTests::noMfa).get();\n        PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    @Test\n    public void maliciousPointerClone() throws Throwable {\n        String a = generateUsername();\n        String b = generateUsername();\n        String password = \"password\";\n        UserContext aContext = PeergosNetworkUtils.ensureSignedUp(a, password, network, crypto);\n        UserContext bContext = PeergosNetworkUtils.ensureSignedUp(b, password, network, crypto);\n\n        FileWrapper aRoot = aContext.getUserRoot().join();\n        FileWrapper bRoot = bContext.getUserRoot().join();\n        MaybeMultihash target = network.mutable.getPointerTarget(aContext.signer.publicKeyHash, aRoot.writer(), network.dhtClient).join().updated;\n        MaybeMultihash current = network.mutable.getPointerTarget(bContext.signer.publicKeyHash, bRoot.writer(), network.dhtClient).join().updated;\n        PointerUpdate cas = new PointerUpdate(current, target, Optional.of(1000L));\n        byte[] rootBlock = network.dhtClient.getRaw(aContext.signer.publicKeyHash, (Cid) target.get(), Optional.empty()).join().get();\n        Cid root2 = ((BufferedStorage)network.dhtClient).target().put(bContext.signer.publicKeyHash, bContext.signer.publicKeyHash,\n                bContext.signer.secret.signMessage(crypto.hasher.sha256(rootBlock).join()).join(), rootBlock, TransactionId.build(\"hey\")).join();\n        Assert.assertFalse(network.mutable.setPointer(bContext.signer.publicKeyHash, bRoot.writer(),\n                bRoot.signingPair().secret.signMessage(cas.serialize()).join()).join());\n        MaybeMultihash updated = network.mutable.getPointerTarget(bContext.signer.publicKeyHash, bRoot.writer(), network.dhtClient).join().updated;\n        Assert.assertTrue(\"Malicious pointer update failed\", updated.equals(current));\n    }\n\n    @Test\n    public void downloadSmallFile() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedata.txt\";\n        Path filePath = PathUtil.get(username, filename);\n        byte[] data = new byte[3];\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        checkFileContents(data, context.getByPath(filePath).join().get(), context);\n        FileWrapper file = context.getByPath(filePath).join().get();\n        FileProperties props = file.getFileProperties();\n        List<Boolean> progressUpdate = new ArrayList<>();\n        file.getInputStream(context.network, context.crypto, props.sizeHigh(), props.sizeLow(), read -> {\n            progressUpdate.add(true);\n        }).join();\n        assertTrue(progressUpdate.size() == 1 && progressUpdate.get(0));\n    }\n    @Test\n    public void concurrentFileModificationFailure() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        NetworkAccess network = this.network.clear();\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedata.txt\";\n        Path filePath = PathUtil.get(username, filename);\n        // write empty file\n        byte[] data = new byte[120*1024];\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        checkFileContents(data, context.getByPath(filePath).join().get(), context);\n\n        FileWrapper fileV1 = context.getByPath(filePath).join().get();\n        FileWrapper fileV2 = context.getByPath(filePath).join().get();\n        byte[] section1 = \"11111111\".getBytes();\n        PublicKeyHash owner = fileV2.owner();\n        SigningPrivateKeyAndPublicHash writer = fileV2.signingPair();\n        network.synchronizer.applyComplexUpdate(owner, writer,\n                (v, c) -> fileV2.overwriteSection(v, c, AsyncReader.build(section1),\n                        1024, 1024 + section1.length, Optional.empty(), network, crypto, x -> {})).join();\n        byte[] data1 = Arrays.copyOfRange(data, 0, data.length);\n        System.arraycopy(section1, 0, data1, 1024, section1.length);\n        checkFileContents(data1, context.getByPath(filePath).join().get(), context);\n        byte[] section2 = \"22222222\".getBytes();\n        try {\n            network.synchronizer.applyComplexUpdate(owner, writer,\n                    (v, c) -> fileV1.overwriteSection(v, c, AsyncReader.build(section2),\n                            1024, 1024 + section2.length, Optional.empty(), network, crypto, x -> {})).join();\n            throw new RuntimeException(\"Concurrentmodification should have failed!\");\n        } catch (CompletionException c) {\n            if (!(c.getCause() instanceof CasException))\n                throw new RuntimeException(\"Failure!\");\n        }\n    }\n\n    @Test\n    public void writeReadVariations() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedata.txt\";\n        // write empty file\n        byte[] data = new byte[0];\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        checkFileContents(data, context.getUserRoot().get().getDescendentByPath(filename, crypto.hasher, context.network).get().get(), context);\n\n        // write small 1 chunk file\n        byte[] data2 = \"This is a small amount of data\".getBytes();\n        FileWrapper updatedRoot = uploadFileSection(context.getUserRoot().get(), filename, new AsyncReader.ArrayBacked(data2), 0, data2.length, context.network,\n                context.crypto, l -> {}).get();\n        checkFileContents(data2, updatedRoot.getDescendentByPath(filename, crypto.hasher, context.network).get().get(), context);\n\n        // check multiple read calls  in one chunk\n        checkFileContentsChunked(data2, updatedRoot.getDescendentByPath(filename, crypto.hasher, context.network).get().get(), context, 3);\n        // check file size\n        // assertTrue(\"File size\", data2.length == userRoot.getDescendentByPath(filename,context.network).get().get().getFileProperties().size);\n\n\n        // check multiple read calls in multiple chunks\n        int bigLength = Chunk.MAX_SIZE * 3;\n        byte[] bigData = new byte[bigLength];\n        random.nextBytes(bigData);\n        FileWrapper updatedRoot2 = uploadFileSection(updatedRoot, filename, new AsyncReader.ArrayBacked(bigData), 0, bigData.length, context.network,\n                context.crypto, l -> {}).get();\n        checkFileContentsChunked(bigData,\n                updatedRoot2.getDescendentByPath(filename, crypto.hasher, context.network).get().get(),\n                context,\n                5);\n        assertTrue(\"File size\", bigData.length == context.getByPath(username + \"/\" + filename).get().get().getFileProperties().size);\n\n        // extend file within existing chunk\n        byte[] data3 = new byte[128 * 1024];\n        new Random().nextBytes(data3);\n        String otherName = \"other\"+filename;\n        FileWrapper updatedRoot3 = uploadFileSection(updatedRoot2, otherName, new AsyncReader.ArrayBacked(data3), 0, data3.length, context.network,\n                context.crypto, l -> {}).get();\n        assertTrue(\"File size\", data3.length == context.getByPath(username + \"/\" + otherName).get().get().getFileProperties().size);\n        checkFileContents(data3, updatedRoot3.getDescendentByPath(otherName, crypto.hasher, context.network).get().get(), context);\n\n        // insert data in the middle\n        byte[] data4 = \"some data to insert somewhere\".getBytes();\n        int startIndex = 100 * 1024;\n        FileWrapper updatedRoot4 = uploadFileSection(updatedRoot3, otherName, new AsyncReader.ArrayBacked(data4), startIndex, startIndex + data4.length,\n                context.network, context.crypto, l -> {}).get();\n        System.arraycopy(data4, 0, data3, startIndex, data4.length);\n        checkFileContents(data3, updatedRoot4.getDescendentByPath(otherName, crypto.hasher, context.network).get().get(), context);\n\n        //rename\n        String newname = \"newname.txt\";\n        FileWrapper updatedRoot5 = updatedRoot4.getDescendentByPath(otherName, crypto.hasher, context.network).join().get()\n                .rename(newname, updatedRoot4, PathUtil.get(username, otherName), context).join();\n        checkFileContents(data3, updatedRoot5.getDescendentByPath(newname, crypto.hasher, context.network).join().get(), context);\n        // check from the root as well\n        checkFileContents(data3, context.getByPath(username + \"/\" + newname).get().get(), context);\n        // check from a fresh log in too\n        UserContext context2 = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        Optional<FileWrapper> renamed = context2.getByPath(username + \"/\" + newname).get();\n        checkFileContents(data3, renamed.get(), context);\n    }\n\n    @Test\n    public void bulkUpload() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        List<Integer> fileSizes = List.of(1024, 4096, 8192, 1024*1024, 6*1024*1024, 15*1024*1024);\n        List<Integer> countForSize = List.of(10, 5, 4, 2, 1, 1);\n\n        Map<List<String>, byte[]> subtree = new HashMap<>();\n\n        String subdir = randomString();\n        String subsubdir = randomString();\n        for (int i=0; i < countForSize.size(); i++) {\n            for (int j=0; j < countForSize.get(i); j++) {\n                byte[] data = new byte[fileSizes.get(i)];\n                random.nextBytes(data);\n                subtree.put(Arrays.asList(randomString()), data);\n                subtree.put(Arrays.asList(subdir, randomString()), data);\n                subtree.put(Arrays.asList(subdir, subsubdir, randomString()), data);\n            }\n        }\n\n        Stream<FileWrapper.FolderUploadProperties> byFolder = subtree.entrySet()\n                .stream()\n                .collect(Collectors.groupingBy(e -> e.getKey().subList(0, e.getKey().size() - 1)))\n                .entrySet()\n                .stream()\n                .map(e -> new FileWrapper.FolderUploadProperties(e.getKey(),\n                        e.getValue()\n                                .stream()\n                                .map(f -> new FileWrapper.FileUploadProperties(f.getKey().get(f.getKey().size() - 1),\n                                        () -> AsyncReader.build(f.getValue()), 0, f.getValue().length, Optional.empty(), Optional.empty(), false, false, x -> {}))\n                                .collect(Collectors.toList())));\n\n        int priorChildren = userRoot.getChildren(crypto.hasher, network).join().size();\n\n        userRoot.uploadSubtree(byFolder, Optional.empty(), network, crypto, context.getTransactionService(), f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n\n        userRoot = context.getUserRoot().join();\n        int postChildren = userRoot.getChildren(crypto.hasher, network).join().size();\n        Assert.assertTrue(\"uploaded dir present\", postChildren > priorChildren);\n        for (Map.Entry<List<String>, byte[]> e : subtree.entrySet()) {\n            String path = e.getKey().stream().collect(Collectors.joining(\"/\"));\n            Optional<FileWrapper> fileOpt = userRoot.getDescendentByPath(path, crypto.hasher, context.network).join();\n            Assert.assertTrue(path + \"is present\", fileOpt.isPresent());\n            FileWrapper file = fileOpt.get();\n            checkFileContentsChunked(e.getValue(), file, context, 5);\n        }\n    }\n\n    @Test\n    public void resumeFailedUploads() {\n        String username = generateUsername();\n        String password = \"terriblepassword\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        String filename = \"somefile\";\n        int size = 50 * 1024 * 1024;\n        byte[] data = new byte[size];\n        random.nextBytes(data);\n        AsyncReader thrower = new ThrowingStream(data, size / 2);\n        FileWrapper txnDir = context.getByPath(Paths.get(username, UserContext.TRANSACTIONS_DIR_NAME)).join().get();\n        TransactionService txns = new NonClosingTransactionService(network, crypto, txnDir);\n        String subdir = \"dir\";\n        try {\n            FileWrapper.FileUploadProperties fileUpload = new FileWrapper.FileUploadProperties(filename, () -> thrower, 0, size, Optional.empty(), Optional.empty(), false, false, x -> {\n            });\n            FileWrapper.FolderUploadProperties dirUploads = new FileWrapper.FolderUploadProperties(Arrays.asList(subdir), Arrays.asList(fileUpload));\n            context.getUserRoot().join().uploadSubtree(Stream.of(dirUploads), context.mirrorBatId(), network, crypto, txns, f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        } catch (Exception e) {\n        }\n        FileWrapper home = context.getUserRoot().join();\n        Set<Transaction> open = context.getTransactionService().getOpenTransactions(home.version).join();\n        Assert.assertTrue(open.size() > 0);\n        // Now try again, with confirmation from the user to resume upload\n        FileWrapper parent = context.getByPath(Paths.get(username, subdir)).join().get();\n        parent.uploadFileJS(filename, AsyncReader.build(data), 0, size, false, context.mirrorBatId(),\n                network, crypto, x -> {}, context.getTransactionService(), f -> Futures.of(true)).join();\n        checkFileContents(data, context.getByPath(Paths.get(username, subdir, filename)).join().get(), context);\n    }\n\n    @Test\n    public void replaceFileBulkUpload() {\n        String username = generateUsername();\n        String password = \"terriblepassword\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        String filename = \"somefile.txt\";\n        int size = 1000;\n        byte[] data = new byte[size];\n        random.nextBytes(data);\n        AsyncReader reader = new AsyncReader.ArrayBacked(data);\n        FileWrapper txnDir = context.getByPath(Paths.get(username, UserContext.TRANSACTIONS_DIR_NAME)).join().get();\n        TransactionService txns = new NonClosingTransactionService(network, crypto, txnDir);\n        String subdir = \"dir\";\n        long[] monitorVal = new long[1];\n        ProgressConsumer<Long> monitor = val -> {\n            monitorVal[0] = val;\n        };\n        try {\n            FileWrapper.FileUploadProperties fileUpload = new FileWrapper.FileUploadProperties(filename, () -> reader, 0, size, Optional.empty(), Optional.empty(), false, false, monitor);\n            FileWrapper.FolderUploadProperties dirUploads = new FileWrapper.FolderUploadProperties(Arrays.asList(subdir), Arrays.asList(fileUpload));\n            context.getUserRoot().join().uploadSubtree(Stream.of(dirUploads), context.mirrorBatId(), network, crypto, txns, f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        } catch (Exception e) {\n        }\n        long[] monitorUploadJSVal = new long[1];\n        ProgressConsumer<Long> monitorUploadJS = val -> {\n            monitorUploadJSVal[0] = val;\n        };\n        FileWrapper parent = context.getByPath(Paths.get(username, subdir)).join().get();\n        parent.uploadFileJS(filename, AsyncReader.build(data), 0, size, true, context.mirrorBatId(), network, crypto, monitorUploadJS, txns, f -> Futures.of(true)).join();\n        Assert.assertTrue(\"monitorJS\", monitorUploadJSVal[0] == monitorVal[0]);\n\n        random.nextBytes(data);\n        AsyncReader reader2 = new AsyncReader.ArrayBacked(data);\n        FileWrapper txnDir2 = context.getByPath(Paths.get(username, UserContext.TRANSACTIONS_DIR_NAME)).join().get();\n        TransactionService txns2 = new NonClosingTransactionService(network, crypto, txnDir2);\n        long[] monitor2Val = new long[1];\n        ProgressConsumer<Long> monitor2 = val -> {\n            monitor2Val[0] = val;\n        };\n        try {\n            FileWrapper.FileUploadProperties fileUpload = new FileWrapper.FileUploadProperties(filename, () -> reader2, 0, size, Optional.empty(), Optional.empty(), false, true, monitor2);\n            FileWrapper.FolderUploadProperties dirUploads = new FileWrapper.FolderUploadProperties(Arrays.asList(subdir), Arrays.asList(fileUpload));\n            context.getUserRoot().join().uploadSubtree(Stream.of(dirUploads), context.mirrorBatId(), network, crypto, txns2, f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        } catch (Exception e) {\n        }\n        Assert.assertTrue(\"bulkMonitor\", monitor2Val[0] == monitorVal[0]);\n    }\n\n    @Test\n    public void appendToFile() {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        FileWrapper userRootCopy = context.getUserRoot().join();\n\n        String filename = \"file1.txt\";\n        String contents = \"Hello \";\n        byte[] data = contents.getBytes();\n        userRootCopy = userRoot.uploadFileJS(filename, new AsyncReader.ArrayBacked(data), 0,data.length, false,\n                userRoot.mirrorBatId(), network, crypto, l -> {}, context.getTransactionService(), f -> Futures.of(false)).join();\n        checkFileContents(data, context.getUserRoot().join().getDescendentByPath(filename, crypto.hasher, context.network).join().get(), context);\n\n        contents = \"World!\";\n        data = contents.getBytes();\n        userRootCopy = userRootCopy.appendFileJS(filename, new AsyncReader.ArrayBacked(data), 0,data.length, network, crypto, l -> {}).join();\n        checkFileContents(\"Hello World!\".getBytes(), context.getUserRoot().join().getDescendentByPath(filename, crypto.hasher, context.network).join().get(), context);\n\n    }\n\n    @Test\n    public void concurrentUploadSucceeds() {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        FileWrapper userRootCopy = context.getUserRoot().join();\n\n        String filename = \"file1.bin\";\n        byte[] data = randomData(6*1024*1024);\n        userRoot.uploadFileJS(filename, new AsyncReader.ArrayBacked(data), 0,data.length, false,\n                userRoot.mirrorBatId(), network, crypto, l -> {}, context.getTransactionService(), f -> Futures.of(false)).join();\n        checkFileContents(data, context.getUserRoot().join().getDescendentByPath(filename, crypto.hasher, context.network).join().get(), context);\n\n        String file2name = \"file2.bin\";\n        byte[] data2 = randomData(6*1024*1024);\n        userRootCopy.uploadFileJS(file2name, new AsyncReader.ArrayBacked(data2), 0,data2.length, false,\n                userRootCopy.mirrorBatId(), network, crypto, l -> {}, context.getTransactionService(), f -> Futures.of(false)).join();\n        checkFileContents(data2, context.getUserRoot().join().getDescendentByPath(file2name, crypto.hasher, context.network).join().get(), context);\n    }\n\n    @Test\n    public void renameFile() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedata.txt\";\n        // write empty file\n        byte[] data = new byte[0];\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        checkFileContents(data, context.getUserRoot().get().getDescendentByPath(filename, crypto.hasher, context.network).get().get(), context);\n\n        //rename\n        String newname = \"newname.txt\";\n        FileWrapper parent = context.getUserRoot().get();\n        FileWrapper file = context.getByPath(username + \"/\" + filename).get().get();\n\n        file.rename(newname, parent, PathUtil.get(username, filename), context).get();\n\n        FileWrapper updatedRoot = context.getUserRoot().get();\n        FileWrapper updatedFile = context.getByPath(updatedRoot.getName() + \"/\" + newname).get().get();\n        checkFileContents(data, updatedFile, context);\n    }\n\n    @Test\n    public void renameWriteSharedDir() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String dirName = \"dir\";\n        userRoot.mkdir(dirName, context.network, false, context.mirrorBatId(), crypto).join();\n\n        // share write access\n        LinkProperties link = context.createSecretLink(username + \"/\" + dirName, true, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        //rename\n        String newname = \"dir2\";\n        FileWrapper parent = context.getUserRoot().get();\n        FileWrapper file = context.getByPath(username + \"/\" + dirName).join().get();\n\n        file.rename(newname, parent, PathUtil.get(username, dirName), context).get();\n\n        FileWrapper updatedRoot = context.getUserRoot().get();\n        FileWrapper updatedDir = context.getByPath(updatedRoot.getName() + \"/\" + newname).get().get();\n        FileProperties linkProps = updatedDir.getLinkPointer().getProperties();\n        Assert.assertTrue(linkProps.isLink);\n        PublicKeyHash linkWriter = updatedDir.getLinkPointer().capability.writer;\n        PublicKeyHash rootWriter = parent.writer();\n        Assert.assertEquals(linkWriter, rootWriter);\n        PublicKeyHash dirWriter = updatedDir.getPointer().capability.writer;\n        Assert.assertNotEquals(dirWriter, rootWriter);\n        SecretLink thelink = SecretLink.fromLink(link.toLinkString(file.owner()));\n        AbsoluteCapability linkCap = network.getSecretLink(thelink)\n                .thenCompose(retrieved -> retrieved.decryptFromPassword(thelink.labelString(), link.linkPassword, crypto)).join();\n        Assert.assertEquals(linkCap.writer, rootWriter);\n\n        UserContext fromLink = UserContext.fromSecretLinkV2(link.toLinkString(file.owner()), () -> Futures.of(\"\"), network.clear(), crypto).join();\n        String entryPath = fromLink.getEntryPath().join();\n        assertEquals(entryPath, \"/\" + username + \"/\" + newname);\n        Optional<FileWrapper> oldPathFromLink = fromLink.getByPath(username + \"/\" + dirName).join();\n        Optional<FileWrapper> dirFromLink = fromLink.getByPath(username + \"/\" + newname).join();\n        Assert.assertTrue(oldPathFromLink.isEmpty());\n        Assert.assertTrue(dirFromLink.isPresent());\n        assertTrue(dirFromLink.get().isWritable());\n\n        FileProperties dirFromLinkProps = dirFromLink.get().getLinkPointer().getProperties();\n        assertEquals(dirFromLinkProps.name, newname);\n        FileProperties props = dirFromLink.get().getPointer().getProperties();\n        assertEquals(props.name, newname);\n    }\n\n    @Test\n    public void fileModifiedDateShouldChangeAfterOverwrite() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedata.txt\";\n        byte[] data = randomData(1024);\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n\n        userRoot = context.getUserRoot().get();\n\n        userRoot.getChild(filename, context.crypto.hasher, context.network).thenCompose(file -> {\n            FileWrapper existingFile = file.get();\n            LocalDateTime modified = existingFile.getFileProperties().modified;\n            byte[] bytes = randomData(1024 * 2);\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException ie) {\n                ie.printStackTrace();\n            }\n            return file.get().overwriteFile(AsyncReader.build(bytes), bytes.length, context.network, context.crypto,\n                    x -> {})\n                    .thenApply(updatedFile -> {\n                        LocalDateTime newTimestamp = updatedFile.getFileProperties().modified;\n                        Assert.assertTrue(\"modified date\", ! newTimestamp.equals(modified));\n                        return updatedFile;\n                    });\n        }).join();\n    }\n\n    @Test\n    public void directoryEncryptionKey() throws Exception {\n        // ensure that a directory's child links are encrypted with the base key, not the parent key\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String dirname = \"somedir\";\n        userRoot.mkdir(dirname, network, false, userRoot.mirrorBatId(), crypto).join();\n\n        FileWrapper dir = context.getByPath(\"/\" + username + \"/\" + dirname).get().get();\n        RetrievedCapability pointer = dir.getPointer();\n        SymmetricKey baseKey = pointer.capability.rBaseKey;\n        SymmetricKey parentKey = dir.getParentKey();\n        Assert.assertTrue(\"parent key different from base key\", ! parentKey.equals(baseKey));\n        pointer.fileAccess.getDirectChildren(pointer.capability, dir.version, network).join();\n    }\n\n    @Test\n    public void fileEncryptionKey() throws Exception {\n        // ensure that a directory's child links are encrypted with the base key, not the parent key\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedir\";\n        byte[] data = new byte[200*1024];\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).join();\n\n        FileWrapper file = context.getByPath(\"/\" + username + \"/\" + filename).join().get();\n        RetrievedCapability pointer = file.getPointer();\n        SymmetricKey baseKey = pointer.capability.rBaseKey;\n        SymmetricKey dataKey = pointer.fileAccess.getDataKey(baseKey);\n        Assert.assertTrue(\"data key different from base key\", ! dataKey.equals(baseKey));\n        pointer.fileAccess.getLinkedData(file.owner(), dataKey, c -> ((CborObject.CborByteArray)c).value, crypto.hasher, network, x -> {}).join();\n    }\n\n    @Test\n    public void concurrentWritesToDir() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        // write empty file\n        int concurrency = 8;\n        int fileSize = 1024;\n        ForkJoinPool pool = new ForkJoinPool(concurrency);\n        Set<CompletableFuture<Boolean>> futs = IntStream.range(0, concurrency)\n                .mapToObj(i -> CompletableFuture.supplyAsync(() -> {\n                    byte[] data = randomData(fileSize);\n                    String filename = i + \".bin\";\n                    FileWrapper userRoot = context.getUserRoot().join();\n                    FileWrapper result = userRoot.uploadOrReplaceFile(filename,\n                            new AsyncReader.ArrayBacked(data),\n                            data.length, context.network, context.crypto, () -> false, l -> {}).join();\n                    Optional<FileWrapper> childOpt = result.getChild(filename, crypto.hasher, network).join();\n                    checkFileContents(data, childOpt.get(), context);\n                    LOG.info(\"Finished a file\");\n                    return true;\n                }, pool)).collect(Collectors.toSet());\n\n        boolean success = Futures.combineAll(futs).get().stream().reduce(true, (a, b) -> a && b);\n\n        Set<FileWrapper> files = context.getUserRoot().get().getChildren(crypto.hasher, context.network).get();\n        Set<String> names = files.stream().filter(f -> ! f.getFileProperties().isHidden).map(f -> f.getName()).collect(Collectors.toSet());\n        Set<String> expectedNames = IntStream.range(0, concurrency).mapToObj(i -> i + \".bin\").collect(Collectors.toSet());\n        Assert.assertTrue(\"All children present and accounted for: \" + names, names.equals(expectedNames));\n    }\n\n    @Test\n    public void concurrentMkdirs() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        // write empty file\n        int concurrency = 8;\n        int fileSize = 1024;\n        ForkJoinPool pool = new ForkJoinPool(concurrency);\n        Set<CompletableFuture<Boolean>> futs = IntStream.range(0, concurrency)\n                .mapToObj(i -> CompletableFuture.supplyAsync(() -> {\n                    byte[] data = randomData(fileSize);\n                    String filename = \"folder\" + i;\n                        FileWrapper userRoot = context.getUserRoot().join();\n                        FileWrapper result = userRoot.uploadOrReplaceFile(filename,\n                                new AsyncReader.ArrayBacked(data), data.length, context.network, context.crypto, () -> false, l -> {}).join();\n                        return true;\n                }, pool)).collect(Collectors.toSet());\n\n        boolean success = Futures.combineAll(futs).get().stream().reduce(true, (a, b) -> a && b);\n\n        Set<FileWrapper> files = context.getUserRoot().get().getChildren(crypto.hasher, context.network).get();\n        Set<String> names = files.stream().filter(f -> ! f.getFileProperties().isHidden).map(f -> f.getName()).collect(Collectors.toSet());\n        Set<String> expectedNames = IntStream.range(0, concurrency).mapToObj(i -> \"folder\" + i).collect(Collectors.toSet());\n        Assert.assertTrue(\"All children present and accounted for: \" + names, names.equals(expectedNames));\n    }\n\n    @Test\n    public void concurrentWritesToFile() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        // write a n chunk file, then concurrently modify each of the chunks\n        int concurrency = 2;\n        int CHUNK_SIZE = 5 * 1024 * 1024;\n        int fileSize = concurrency * CHUNK_SIZE;\n        String filename = \"afile.bin\";\n        FileWrapper userRoot = context.getUserRoot().get();\n        FileWrapper newRoot = userRoot.uploadOrReplaceFile(filename,\n                new AsyncReader.ArrayBacked(randomData(fileSize)),\n                fileSize, context.network, context.crypto, () -> false, l -> {}).get();\n\n        List<byte[]> sections = Collections.synchronizedList(new ArrayList<>(concurrency));\n        for (int i=0; i < concurrency; i++)\n            sections.add(null);\n\n        ForkJoinPool pool = new ForkJoinPool(concurrency);\n        Set<CompletableFuture<Boolean>> futs = IntStream.range(0, concurrency)\n                .mapToObj(i -> CompletableFuture.supplyAsync(() -> {\n                    FileWrapper root = context.getUserRoot().join();\n\n                    byte[] data = randomData(CHUNK_SIZE);\n                    FileWrapper result = uploadFileSection(root, filename,\n                            new AsyncReader.ArrayBacked(data),\n                            i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE,\n                            context.network, context.crypto, l -> {}).join();\n                    Optional<FileWrapper> childOpt = result.getChild(filename, crypto.hasher, network).join();\n                    sections.set(i, data);\n                    return true;\n                }, pool)).collect(Collectors.toSet());\n\n        boolean success = Futures.combineAll(futs).get().stream().reduce(true, (a, b) -> a && b);\n\n        FileWrapper file = context.getByPath(\"/\" + username + \"/\" + filename).get().get();\n        byte[] all = new byte[concurrency * CHUNK_SIZE];\n        for (int i=0; i < concurrency; i++)\n            System.arraycopy(sections.get(i), 0, all, i * CHUNK_SIZE, CHUNK_SIZE);\n        checkFileContents(all, file, context);\n    }\n\n    @Test\n    public void smallFileWrite() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"small.txt\";\n        byte[] data = \"G'day mate\".getBytes();\n        AtomicLong writeCount = new AtomicLong(0);\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, writeCount::addAndGet).get();\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename).toString()).get().get();\n        String mimeType = file.getFileProperties().mimeType;\n        Assert.assertTrue(\"Incorrect mimetype: \" + mimeType, mimeType.equals(\"text/plain\"));\n        Assert.assertTrue(\"No thumbnail\", ! file.getFileProperties().thumbnail.isPresent());\n        Assert.assertTrue(\"Completed progress monitor\", writeCount.get() == data.length + FileWrapper.THUMBNAIL_PROGRESS_OFFSET);\n        AbsoluteCapability cap = file.getPointer().capability;\n        CryptreeNode fileAccess = file.getPointer().fileAccess;\n        RelativeCapability toParent = fileAccess.getParentCapability(fileAccess.getParentKey(cap.rBaseKey)).get();\n        Assert.assertTrue(\"parent link shouldn't include write access\",\n                ! toParent.wBaseKeyLink.isPresent());\n        Assert.assertTrue(\"parent link shouldn't include public write key\",\n                ! toParent.writer.isPresent());\n\n        FileWrapper home = context.getByPath(PathUtil.get(username).toString()).get().get();\n        RetrievedCapability homePointer = home.getPointer();\n        List<NamedRelativeCapability> children = homePointer.fileAccess.getDirectChildren(homePointer.capability, home.version, network).get();\n        for (NamedRelativeCapability child : children) {\n            Assert.assertTrue(\"child pointer is minimal\",\n                    ! child.cap.writer.isPresent() && child.cap.wBaseKeyLink.isPresent());\n        }\n    }\n\n    static class ThrowingStream implements AsyncReader {\n        private final byte[] data;\n        private int index = 0;\n        private final int throwAtIndex;\n\n        public ThrowingStream(byte[] data, int throwAtIndex) {\n            this.data = data;\n            this.throwAtIndex = throwAtIndex;\n        }\n\n        @Override\n        public CompletableFuture<AsyncReader> seekJS(int high32, int low32) {\n            if (high32 != 0)\n                throw new IllegalArgumentException(\"Cannot have arrays larger than 4GiB!\");\n            if (index + low32 > throwAtIndex)\n                throw new RuntimeException(\"Simulated IO Error\");\n            index += low32;\n            return CompletableFuture.completedFuture(this);\n        }\n\n        @Override\n        public CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length) {\n            if (index + length > throwAtIndex)\n                throw new RuntimeException(\"Simulated IO Error\");\n            System.arraycopy(data, index, res, offset, length);\n            index += length;\n            return CompletableFuture.completedFuture(length);\n        }\n\n        @Override\n        public CompletableFuture<AsyncReader> reset() {\n            index = 0;\n            return CompletableFuture.completedFuture(this);\n        }\n\n        @Override\n        public void close() {}\n    }\n\n    @Test\n    public void repeatFailedUpload() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"small.txt\";\n        byte[] data = new byte[2*5*1024*1024];\n        ThrowingStream throwingReader = new ThrowingStream(data, 5 * 1024 * 1024);\n\n        TransactionService transactions = context.getTransactionService();\n        try {\n            userRoot.uploadFileJS(filename, throwingReader, 0, data.length, false,\n                    userRoot.mirrorBatId(), context.network, context.crypto, l -> {}, transactions, f -> Futures.of(false)).join();\n        } catch (Exception e) {}\n\n        userRoot.uploadFileJS(filename, AsyncReader.build(data), 0, data.length, false,\n                userRoot.mirrorBatId(), context.network, context.crypto, l -> {}, transactions, f -> Futures.of(true)).join();\n    }\n\n    @Test\n    public void javaThumbnail() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"small.png\";\n        byte[] data = Files.readAllBytes(PathUtil.get(\"assets\", \"logo.png\"));\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename).toString()).get().get();\n        String thumbnail = file.getBase64Thumbnail();\n        Assert.assertTrue(\"Has thumbnail\", thumbnail.length() > 0);\n\n        data = Files.readAllBytes(PathUtil.get(\"assets\", \"logos\", \"peergos-logo.png\"));\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        file = context.getByPath(PathUtil.get(username, filename).toString()).get().get();\n        boolean res = file.calculateAndUpdateThumbnail(context.network, context.crypto).join();\n        Assert.assertTrue(\"Has updated Thumbnail\", res);\n        file = context.getByPath(PathUtil.get(username, filename).toString()).get().get();\n        String thumbnailAfter = file.getBase64Thumbnail();\n        Assert.assertTrue(\"Thumbnail NOT changed\", !thumbnail.equals(thumbnailAfter));\n    }\n\n    @Test\n    public void copyFileWithThumbnail() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        Path home = PathUtil.get(username);\n\n        String filename = \"logo.png\";\n        byte[] data = Files.readAllBytes(PathUtil.get(\"assets\", filename));\n\n        FileWrapper updatedUserRoot = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data),\n                data.length, context.network, crypto, () -> false, x -> {}).join();\n        // copy the file\n        String foldername = \"afolder\";\n        updatedUserRoot.mkdir(foldername, context.network, false, userRoot.mirrorBatId(), crypto).join();\n        FileWrapper subfolder = context.getByPath(home.resolve(foldername)).join().get();\n        FileWrapper original = context.getByPath(home.resolve(filename)).join().get();\n        Boolean res = original.copyTo(subfolder, context).join();\n        Assert.assertTrue(\"Copied\", res);\n        FileWrapper copy = context.getByPath(home.resolve(foldername).resolve(filename)).join().get();\n        String thumbnail = copy.getBase64Thumbnail();\n        Assert.assertTrue(\"Has thumbnail\", thumbnail.length() > 0);\n        checkFileContents(data, copy, context);\n    }\n\n    @Ignore // until we figure out how to manage javafx in tests\n    @Test\n    public void javaVideoThumbnail() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"trailer.mp4\";\n        byte[] data = Files.readAllBytes(PathUtil.get(\"assets\", filename));\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename).toString()).get().get();\n        String thumbnail = file.getBase64Thumbnail();\n        Assert.assertTrue(\"Has thumbnail\", thumbnail.length() > 0);\n    }\n\n    @Test\n    public void legacyFileModification() throws Exception {\n        // test that a legacy file without BATs can be modified and extended, and all new fragments have BATs\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        PublicKeyHash owner = context.signer.publicKeyHash;\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[4*1024*1024];\n        random.nextBytes(data);\n        FileWrapper userRoot2 = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                context.network, context.crypto, () -> false, l -> {}, crypto.random.randomBytes(32), Optional.empty(), Optional.empty()).get();\n\n        // Remove BATs from cryptree node and fragments in file\n        FileWrapper file = context.getByPath(PathUtil.get(username, filename)).join().get();\n        CborObject.CborMap cbor = (CborObject.CborMap) file.getPointer().fileAccess.toCbor();\n        // add blocks without bat prefix\n        List<Multihash> rawBlockLinks = cbor.links();\n        List<BatWithId> bats = (List<BatWithId>)((CborObject.CborList)((CborObject.CborMap)cbor.get(\"d\")).get(\"bats\")).value;\n        List<Multihash> newFragmentCids = IntStream.range(0, rawBlockLinks.size()).mapToObj(i -> {\n            Cid original = (Cid) rawBlockLinks.get(i);\n            BatWithId bat = bats.get(i);\n            byte[] originalBlock = context.network.dhtClient.getRaw(owner, original, Optional.of(bat)).join().get();\n            byte[] newBlock = Bat.removeRawBlockBatPrefix(originalBlock);\n            return context.network.uploadFragments(Arrays.asList(new Fragment(newBlock)), userRoot.owner(),\n                    userRoot.signingPair(), x -> {}, TransactionId.build(\"tid\")).join().get(0);\n        }).collect(Collectors.toList());\n\n        Map<String, Cborable> modified = new LinkedHashMap<>();\n        cbor.applyToAll(modified::put);\n        CborObject.CborMap d = (CborObject.CborMap) modified.get(\"d\");\n        d.put(\"bats\", new CborObject.CborList(Collections.emptyList()));\n        d.put(\"f\", new CborObject.CborList(newFragmentCids\n                .stream()\n                .map(CborObject.CborMerkleLink::new)\n                .collect(Collectors.toList())));\n        modified.put(\"d\", d);\n        modified.remove(\"bats\");\n        CborObject.CborMap noBats = CborObject.CborMap.build(modified);\n        CommittedWriterData cwd = WriterData.getWriterData(owner, file.writer(), network.mutable, network.dhtClient).join();\n        WriterData wd = cwd.props.get();\n        SigningPrivateKeyAndPublicHash signingPair = file.signingPair();\n        Cid cryptreeCid = network.dhtClient.put(owner, signingPair, noBats.serialize(), crypto.hasher,\n                TransactionId.build(\"hey\")).join();\n        byte[] mapKey = file.readOnlyPointer().getMapKey();\n\n        // also update child pointer in parent\n        FileWrapper root = context.getUserRoot().join();\n        FileWrapper cFile = file;\n        network.synchronizer.applyComplexUpdate(owner, signingPair,\n                (s, c) -> {\n                    WriterData newWd = network.tree.put(wd, owner, signingPair, mapKey,\n                            cFile.getPointer().fileAccess.committedHash(), cryptreeCid, TransactionId.build(\"hey\")).join();\n                    return c.commit(owner, signingPair, newWd, cwd, TransactionId.build(\"123\"));\n                }).join();\n        WritableAbsoluteCapability cap = cFile.writableFilePointer();\n        WritableAbsoluteCapability batlessCap = cap.withMapKey(cFile.readOnlyPointer().getMapKey(), Optional.empty());\n        network.synchronizer.applyComplexUpdate(owner, signingPair,\n                (s, c) -> root.getPointer().fileAccess.updateChildLink(s, c, root.writableFilePointer(), root.signingPair(),\n                        cap, new NamedAbsoluteCapability(filename, batlessCap, Optional.empty(), Optional.empty(), Optional.empty()), network, crypto.random, crypto.hasher)).join();\n\n        // check there are no BATs for this file\n        UserContext newContext = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        file = newContext.getByPath(PathUtil.get(username, filename)).join().get();\n        Assert.assertTrue(file.writableFilePointer().bat.isEmpty());\n\n        // Check fragments are retrievable without a BAT\n        Multihash originalFragmentWithoutBatPrefix = file.getPointer().fileAccess.toCbor().links().get(0);\n        CompletableFuture<Optional<byte[]>> originalRaw = network.clear().dhtClient.getRaw(owner, (Cid) originalFragmentWithoutBatPrefix, Optional.empty());\n        Assert.assertTrue(originalRaw.join().isPresent());\n\n        //overwrite with 2 chunk file\n        byte[] threeChunkData = new byte[11*1024*1024];\n        random.nextBytes(threeChunkData);\n        FileWrapper userRoot3 = uploadFileSection(userRoot2, filename, new AsyncReader.ArrayBacked(threeChunkData), 0,\n                threeChunkData.length, newContext.network, newContext.crypto, l -> {}).join();\n        FileWrapper updatedFile = newContext.getByPath(PathUtil.get(username, filename)).join().get();\n        checkFileContents(threeChunkData, updatedFile, newContext);\n        assertTrue(\"10MiB file size\", threeChunkData.length == updatedFile.getFileProperties().size);\n\n        WritableAbsoluteCapability newcap = updatedFile.writableFilePointer();\n        Assert.assertTrue(newcap.bat.isEmpty());\n        // check later chunks don't have BATs\n        Pair<byte[], Optional<Bat>> nextChunkRel = updatedFile.getPointer().fileAccess.getNextChunkLocation(updatedFile.getKey(),\n                updatedFile.getFileProperties().streamSecret, newcap.getMapKey(), Optional.empty(), crypto.hasher).join();\n        Assert.assertTrue(nextChunkRel.right.isEmpty());\n        NetworkAccess cleared = network.clear();\n        CommittedWriterData uwd = WriterData.getWriterData(owner, updatedFile.writer(), network.mutable, network.dhtClient).join();\n        Optional<CryptreeNode> secondChunk = cleared.getMetadata(uwd, newcap.withMapKey(nextChunkRel.left, Optional.empty())).join();\n        Assert.assertTrue(secondChunk.isPresent());\n        // now the third chunk\n        Pair<byte[], Optional<Bat>> thirdChunkRel = secondChunk.get().getNextChunkLocation(updatedFile.getKey(),\n                updatedFile.getFileProperties().streamSecret, newcap.getMapKey(), Optional.empty(), crypto.hasher).join();\n        Assert.assertTrue(thirdChunkRel.right.isEmpty());\n        Optional<CryptreeNode> thirdChunk = cleared.getMetadata(uwd, newcap.withMapKey(thirdChunkRel.left, Optional.empty())).join();\n        Assert.assertTrue(thirdChunk.isPresent());\n\n        // check cryptree node can still be retrieved without a BAT\n        Assert.assertTrue(network.clear().getFile(updatedFile.version, newcap, Optional.of(updatedFile.signingPair()), username).join().isPresent());\n\n        // check retrieval of fragments fail without bat\n        Multihash fragment = updatedFile.getPointer().fileAccess.toCbor().links().get(0);\n        Optional<byte[]> raw = network.clear().dhtClient.getRaw(owner, (Cid) fragment, Optional.empty()).exceptionally(e -> Optional.empty()).join();\n        Assert.assertTrue(raw.isEmpty());\n    }\n\n    @Test\n    public void mediumFileWrite() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[0];\n        FileWrapper userRoot2 = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                context.network, context.crypto, () -> false, l -> {}).get();\n\n        //overwrite with 2 chunk file\n        byte[] data5 = new byte[10*1024*1024];\n        random.nextBytes(data5);\n        FileWrapper userRoot3 = uploadFileSection(userRoot2, filename, new AsyncReader.ArrayBacked(data5), 0, data5.length, context.network,\n                context.crypto, l -> {}).join();\n        checkFileContents(data5, userRoot3.getDescendentByPath(filename, crypto.hasher, context.network).get().get(), context);\n        assertTrue(\"10MiB file size\", data5.length == userRoot3.getDescendentByPath(filename,\n                crypto.hasher, context.network).get().get().getFileProperties().size);\n\n        // insert data in the middle of second chunk\n        LOG.info(\"\\n***** Mid 2nd chunk write test\");\n        byte[] dataInsert = \"some data to insert somewhere else\".getBytes();\n        int start = 5*1024*1024 + 4*1024;\n        FileWrapper userRoot4 = uploadFileSection(userRoot3, filename, new AsyncReader.ArrayBacked(dataInsert), start, start + dataInsert.length,\n                context.network, context.crypto, l -> {}).get();\n        System.arraycopy(dataInsert, 0, data5, start, dataInsert.length);\n        FileWrapper file = userRoot4.getDescendentByPath(filename, crypto.hasher, context.network).get().get();\n        checkFileContents(data5, file, context);\n\n        // check used space\n        long totalSpaceUsed = context.getSpaceUsage(false).get();\n        while (totalSpaceUsed < 10*1024*1024) {\n            Thread.sleep(1_000);\n            totalSpaceUsed = context.getSpaceUsage(false).get();\n        }\n        Assert.assertTrue(\"Correct used space\", totalSpaceUsed > 10*1024*1024);\n\n        // check second chunk BAT is different from first\n        Pair<byte[], Optional<Bat>> nextChunkRel = file.getPointer().fileAccess.getNextChunkLocation(file.getKey(),\n                file.getFileProperties().streamSecret, file.writableFilePointer().getMapKey(), file.writableFilePointer().bat, crypto.hasher).join();\n        Assert.assertTrue(! nextChunkRel.right.get().equals(file.writableFilePointer().bat.get()));\n\n        // check retrieval of cryptree node or data both fail without bat\n        WritableAbsoluteCapability cap = file.writableFilePointer();\n        WritableAbsoluteCapability badCap = cap.withMapKey(cap.getMapKey(), Optional.empty());\n        NetworkAccess cleared = network.clear();\n        CompletableFuture<Optional<FileWrapper>> badFileGet = cleared.getFile(file.version, badCap, Optional.of(file.signingPair()), username);\n        Assert.assertTrue(badFileGet.exceptionally(t -> Optional.empty()).join().isEmpty());\n\n        Multihash fragment = file.getPointer().fileAccess.toCbor().links().get(0);\n        CompletableFuture<Optional<byte[]>> raw = cleared.dhtClient.getRaw(context.signer.publicKeyHash, (Cid) fragment, Optional.empty());\n        Assert.assertTrue(raw.exceptionally(t -> Optional.empty()).join().isEmpty());\n    }\n\n    @Test\n    public void truncate() {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[15*1024*1024];\n        random.nextBytes(data);\n        FileWrapper userRoot2 = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                context.network, context.crypto, () -> false, l -> {}).join();\n\n        FileWrapper original = context.getByPath(PathUtil.get(username, filename)).join().get();\n        Pair<byte[], Optional<Bat>> thirdChunkLabel = original.getMapKey(12 * 1024 * 1024, network, crypto).join();\n\n        int truncateLength = 7 * 1024 * 1024;\n        FileWrapper truncated = original.truncate(truncateLength, network, crypto).join();\n        checkFileContents(Arrays.copyOfRange(data, 0, truncateLength), truncated, context);\n        // check we can't get the third chunk any more\n        WritableAbsoluteCapability pointer = original.writableFilePointer();\n        CommittedWriterData cwd = network.synchronizer.getValue(pointer.owner, pointer.writer).join().get(pointer.writer);\n        Optional<CryptreeNode> thirdChunk = network.getMetadata(cwd, pointer.withMapKey(thirdChunkLabel.left, thirdChunkLabel.right)).join();\n        Assert.assertTrue(\"File is truncated\", ! thirdChunk.isPresent());\n        Assert.assertTrue(\"File has correct size\", truncated.getFileProperties().size == truncateLength);\n\n        // truncate to first chunk\n        int truncateLength2 = 1 * 1024 * 1024;\n        FileWrapper truncated2 = truncated.truncate(truncateLength2, network, crypto).join();\n        checkFileContents(Arrays.copyOfRange(data, 0, truncateLength2), truncated2, context);\n        Assert.assertTrue(\"File has correct size\", truncated2.getFileProperties().size == truncateLength2);\n\n        // truncate within first chunk\n        int truncateLength3 = 1024 * 1024 / 2;\n        FileWrapper truncated3 = truncated2.truncate(truncateLength3, network, crypto).join();\n        checkFileContents(Arrays.copyOfRange(data, 0, truncateLength2), truncated2, context);\n        Assert.assertTrue(\"File has correct size\", truncated3.getFileProperties().size == truncateLength3);\n    }\n\n    @Test\n    public void fileSeek() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"mediumfile.bin\";\n\n        int MB = 1024*1024;\n        byte[] data = new byte[15 * MB];\n        random.nextBytes(data);\n        uploadFileSection(userRoot, filename, new AsyncReader.ArrayBacked(data), 0, data.length, context.network,\n                context.crypto, l -> {}).join();\n        byte[] buf = new byte[2 * MB];\n\n        for (int offset: Arrays.asList(10, 4*MB, 6*MB, 11*MB)) {\n            AsyncReader reader = context.getByPath(PathUtil.get(username, filename)).join()\n                    .get().getInputStream(network, crypto, x -> { }).join();\n            AsyncReader seeked = reader.seek(offset).join();\n            seeked.readIntoArray(buf, 0, buf.length).join();\n            if (! Arrays.equals(buf, Arrays.copyOfRange(data, offset, offset + buf.length)))\n                throw new IllegalStateException(\"Seeked data incorrect! Offset: \" + offset);\n        }\n\n        for (int mb = 0; mb < 13; mb++) {\n            AsyncReader reader = context.getByPath(PathUtil.get(username, filename)).join()\n                    .get().getInputStream(network, crypto, x -> { }).join();\n            for (int count = 0; count < mb; count++) {\n                reader = reader.seek(count * MB).join();\n                reader.readIntoArray(buf, 0, buf.length).join();\n                if (!Arrays.equals(buf, Arrays.copyOfRange(data, count * MB, count * MB + buf.length)))\n                    throw new IllegalStateException(\"Seeked data incorrect! Offset: \" + count * MB);\n            }\n        }\n    }\n\n    @Test\n    public void writeTiming() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[0];\n        FileWrapper updatedRoot = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length,\n                context.network, context.crypto, () -> false, l -> {}).get();\n\n        //overwrite with 2 chunk file\n        byte[] data5 = new byte[10*1024*1024];\n        random.nextBytes(data5);\n        long t1 = System.currentTimeMillis();\n        uploadFileSection(updatedRoot, filename, new AsyncReader.ArrayBacked(data5), 0, data5.length,\n                context.network, context.crypto, l -> {}).get();\n        long t2 = System.currentTimeMillis();\n        LOG.info(\"Write time per chunk \" + (t2-t1)/2 + \"mS\");\n        Assert.assertTrue(\"Timely write\", (t2-t1)/2 < 20000);\n    }\n\n    @Test\n    public void publiclySharedFile() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"afile.bin\";\n        byte[] data = new byte[128*1024];\n        random.nextBytes(data);\n        uploadFileSection(userRoot, filename, new AsyncReader.ArrayBacked(data), 0, data.length,\n                context.network, context.crypto, l -> {}).get();\n        String path = \"/\" + username + \"/\" + filename;\n        FileWrapper file = context.getByPath(path).get().get();\n        context.makePublic(file).get();\n\n        FileWrapper publicFile = context.getPublicFile(PathUtil.get(username, filename)).join().get();\n        byte[] returnedData = Serialize.readFully(publicFile.getInputStream(context.network, crypto, x -> {}).join(), data.length).join();\n        Assert.assertTrue(\"Correct data returned for publicly shared file\", Arrays.equals(data, returnedData));\n    }\n\n    @Test\n    public void publiclySharedDirectory() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"afile.bin\";\n        byte[] data = new byte[128*1024];\n        random.nextBytes(data);\n        String dirName = \"subdir\";\n        userRoot.mkdir(dirName, network, false, userRoot.mirrorBatId(), crypto).get();\n        String dirPath = \"/\" + username + \"/\" + dirName;\n        FileWrapper subdir = context.getByPath(dirPath).get().get();\n        FileWrapper updatedSubdir = uploadFileSection(subdir, filename, new AsyncReader.ArrayBacked(data), 0,\n                data.length, context.network, context.crypto, l -> {}).get();\n        context.makePublic(updatedSubdir).get();\n\n        FileWrapper publicFile = context.getPublicFile(PathUtil.get(username, dirName, filename)).join().get();\n        byte[] returnedData = Serialize.readFully(publicFile.getInputStream(context.network, crypto, x -> {}).join(), data.length).join();\n        Assert.assertTrue(\"Correct data returned for publicly shared file\", Arrays.equals(data, returnedData));\n    }\n\n    @Test\n    public void publicLinkToFile() throws Exception {\n        PeergosNetworkUtils.publicLinkToFile(random, network, network, () -> {});\n    }\n\n    @Test\n    public void publicLinkToDir() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[128*1024];\n        random.nextBytes(data);\n        String dirName = \"subdir\";\n        userRoot.mkdir(dirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        FileWrapper subdir = context.getByPath(\"/\" + username + \"/\" + dirName).get().get();\n        String anotherDirName = \"anotherDir\";\n        subdir.mkdir(anotherDirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        FileWrapper anotherDir = context.getByPath(\"/\" + username + \"/\" + dirName + \"/\" + anotherDirName).get().get();\n        uploadFileSection(anotherDir, filename, new AsyncReader.ArrayBacked(data), 0, data.length, context.network,\n                context.crypto, l -> {}).get();\n\n        String path = \"/\" + username + \"/\" + dirName + \"/\" + anotherDirName;\n        FileWrapper theDir = context.getByPath(path).get().get();\n        String link = theDir.toLink();\n        UserContext linkContext = UserContext.fromSecretLink(link, network, crypto).get();\n        String entryPath = linkContext.getEntryPath().get();\n        Assert.assertTrue(\"public link to folder has correct entry path\", entryPath.equals(path));\n\n        Optional<FileWrapper> fileThroughLink = linkContext.getByPath(path + \"/\" + filename).get();\n        Assert.assertTrue(\"File present through link\", fileThroughLink.isPresent());\n\n        SharedWithState sharing = context.getDirectorySharingState(PathUtil.get(path)).join();\n        Assert.assertTrue(\"Can retrieve (empty) sharing state in secret link\", sharing.isEmpty());\n    }\n\n    @Test\n    public void writablePublicLinkToDir() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[128*1024];\n        random.nextBytes(data);\n        String dirName = \"subdir\";\n        userRoot.mkdir(dirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        FileWrapper subdir = context.getByPath(\"/\" + username + \"/\" + dirName).get().get();\n        String anotherDirName = \"anotherDir\";\n        subdir.mkdir(anotherDirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        FileWrapper anotherDir = context.getByPath(\"/\" + username + \"/\" + dirName + \"/\" + anotherDirName).get().get();\n        uploadFileSection(anotherDir, filename, new AsyncReader.ArrayBacked(data), 0, data.length, context.network,\n                context.crypto, l -> {}).get();\n\n        String path = \"/\" + username + \"/\" + dirName + \"/\" + anotherDirName;\n        // move folder to new signing subspace\n        context.shareWriteAccessWith(PathUtil.get(path), Collections.emptySet()).join();\n\n        FileWrapper theDir = context.getByPath(path).get().get();\n        String link = theDir.toWritableLink();\n        UserContext linkContext = UserContext.fromSecretLink(link, network, crypto).get();\n        String entryPath = linkContext.getEntryPath().get();\n        Assert.assertTrue(\"public link to folder has correct entry path\", entryPath.equals(path));\n\n        Optional<FileWrapper> fileThroughLink = linkContext.getByPath(path + \"/\" + filename).get();\n        Assert.assertTrue(\"File present through link\", fileThroughLink.isPresent());\n\n        Optional<FileWrapper> dirThroughLink = linkContext.getByPath(path).get();\n        Assert.assertTrue(\"dir is writable\", dirThroughLink.isPresent() && dirThroughLink.get().isWritable());\n\n        byte[] newData = \"Some dataaa\".getBytes();\n        dirThroughLink.get().uploadFileJS(\"anoterfile\", AsyncReader.build(newData), 0, newData.length,\n                false, dirThroughLink.get().mirrorBatId(), linkContext.network, linkContext.crypto, x -> {}, null, f -> Futures.of(false)).join();\n    }\n\n    @Test\n    public void writableSecretLinkMoveTo() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"mediumfile.bin\";\n        byte[] data = new byte[128*1024];\n        random.nextBytes(data);\n        String dirName = \"subdir\";\n        userRoot.mkdir(dirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        FileWrapper subdir = context.getByPath(\"/\" + username + \"/\" + dirName).get().get();\n        String anotherDirName = \"anotherDir\";\n        subdir.mkdir(anotherDirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        FileWrapper anotherDir = context.getByPath(\"/\" + username + \"/\" + dirName + \"/\" + anotherDirName).get().get();\n        uploadFileSection(anotherDir, filename, new AsyncReader.ArrayBacked(data), 0, data.length, context.network,\n                context.crypto, l -> {}).get();\n\n        String path = \"/\" + username + \"/\" + dirName;\n        // move folder to new signing subspace\n        LinkProperties link = context.createSecretLink(path, true, Optional.empty(), Optional.empty(), \"\", false).join();\n\n        UserContext linkContext = UserContext.fromSecretLinkV2(link.toLinkString(context.signer.publicKeyHash), () -> Futures.of(\"\"), network, crypto).get();\n        String entryPath = linkContext.getEntryPath().get();\n        Assert.assertTrue(\"public link to folder has correct entry path\", entryPath.equals(path));\n\n        String filePath = \"/\" + username + \"/\" + dirName + \"/\" + anotherDirName + \"/\" + filename;\n        FileWrapper file = linkContext.getByPath(filePath).join().get();\n        FileWrapper target = linkContext.getByPath(\"/\" + username + \"/\" + dirName).join().get();\n        FileWrapper parent = linkContext.getByPath(\"/\" + username + \"/\" + dirName+ \"/\" + anotherDirName).join().get();\n        file.moveTo(target, parent, PathUtil.get(filePath), linkContext, () -> Futures.of(true)).join();\n    }\n\n    @Test\n    public void recursiveDelete() {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        Path home = PathUtil.get(username);\n\n        String foldername = \"afolder\";\n        userRoot.mkdir(foldername, context.network, false, userRoot.mirrorBatId(), crypto).join();\n        FileWrapper folder = context.getByPath(home.resolve(foldername)).join().get();\n\n        String subfoldername = \"subfolder\";\n        folder = folder.mkdir(subfoldername, context.network, false, userRoot.mirrorBatId(), crypto).join();\n        Path subfolderPath = PathUtil.get(username, foldername, subfoldername);\n        FileWrapper subfolder = context.getByPath(subfolderPath).join().get();\n\n        folder.remove(context.getUserRoot().join(), subfolderPath, context).join();\n\n        AbsoluteCapability pointer = subfolder.getPointer().capability;\n        CommittedWriterData cwd = network.synchronizer.getValue(pointer.owner, pointer.writer).join().get(pointer.writer);\n        Optional<CryptreeNode> subdir = network.getMetadata(cwd, pointer).join();\n        Assert.assertTrue(\"Child deleted\", ! subdir.isPresent());\n    }\n\n    @Test\n    public void profileTest() {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        String firstName = \"john\";\n        String lastName = \"doe\";\n        String bio = \"asleep\";\n        String phone = \"555 5555\";\n        String email = \"joe@doesnt.exist\";\n        String status = \"busy\";\n        String dir = \"webroot\";\n        Path webroot = PathUtil.get(username, dir);\n        String webrootString = \"/\" + username + \"/\" + dir;\n        context.getUserRoot().join().mkdir(dir, context.network, false, context.mirrorBatId(), crypto).join();\n\n        byte[] thumbnail = randomData(100);\n        byte[] hires = randomData(1000);\n\n        ProfilePaths.setFirstName(context, firstName).join();\n        ProfilePaths.setLastName(context, lastName).join();\n        ProfilePaths.setBio(context, bio).join();\n        ProfilePaths.setPhone(context, phone).join();\n        ProfilePaths.setEmail(context, email).join();\n        ProfilePaths.setStatus(context, status).join();\n        ProfilePaths.setWebRoot(context, webrootString).join();\n        ProfilePaths.setProfilePhoto(context, thumbnail).join();\n        ProfilePaths.setHighResProfilePhoto(context, hires).join();\n\n        assertTrue(\"Correct value\", ProfilePaths.getFirstName(username, context).join().get().equals(firstName));\n        assertTrue(\"Correct value\", ProfilePaths.getLastName(username, context).join().get().equals(lastName));\n        assertTrue(\"Correct value\", ProfilePaths.getBio(username, context).join().get().equals(bio));\n        assertTrue(\"Correct value\", ProfilePaths.getPhone(username, context).join().get().equals(phone));\n        assertTrue(\"Correct value\", ProfilePaths.getEmail(username, context).join().get().equals(email));\n        assertTrue(\"Correct value\", ProfilePaths.getStatus(username, context).join().get().equals(status));\n        assertTrue(\"Correct value\", ProfilePaths.getWebRoot(username, context).join().get().equals(webrootString));\n        assertTrue(\"Correct value\", Arrays.equals(ProfilePaths.getProfilePhoto(username, context).join().get(), thumbnail));\n        assertTrue(\"Correct value\", Arrays.equals(ProfilePaths.getHighResProfilePhoto(username, context).join().get(), hires));\n\n        Profile profile = ProfilePaths.getProfile(username, context).join();\n        assertTrue(\"Correct value\", profile.firstName.get().equals(firstName));\n        assertTrue(\"Correct value\", profile.lastName.get().equals(lastName));\n        assertTrue(\"Correct value\", profile.bio.get().equals(bio));\n        assertTrue(\"Correct value\", profile.phone.get().equals(phone));\n        assertTrue(\"Correct value\", profile.email.get().equals(email));\n        assertTrue(\"Correct value\", profile.status.get().equals(status));\n        assertTrue(\"Correct value\", profile.webRoot.get().equals(webrootString));\n        assertTrue(\"Correct value\", Arrays.equals(profile.profilePhoto.get(), thumbnail));\n\n        ProfilePaths.publishWebroot(context).join();\n        Optional<FileWrapper> fw = context.getPublicFile(webroot).join();\n        assertTrue(\"webroot\", fw.isPresent());\n\n        ProfilePaths.unpublishWebRoot(context).join();\n        Optional<FileWrapper> currentCap = context.getPublicFile(webroot).join();\n        assertTrue(currentCap.isEmpty());\n        ProfilePaths.publishWebroot(context).join();\n        Optional<FileWrapper> fw2 = context.getPublicFile(webroot).join();\n        assertTrue(\"webroot\", fw2.isPresent());\n    }\n\n    @Test\n    public void rename() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String dirName = \"subdir\";\n        userRoot.mkdir(dirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        FileWrapper subdir = context.getByPath(\"/\" + username + \"/\" + dirName).get().get();\n        String anotherDirName = \"anotherDir\";\n        subdir.mkdir(anotherDirName, context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n\n        String path = \"/\" + username + \"/\" + dirName;\n        FileWrapper theDir = context.getByPath(path).get().get();\n        FileWrapper userRoot2 = context.getByPath(\"/\" + username).get().get();\n        FileWrapper renamed = theDir.rename(\"subdir2\", userRoot2, PathUtil.get(username, dirName), context).get();\n    }\n\n    // This one takes a while, so disable most of the time\n//    @Test\n    public void hugeFolder() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        List<String> names = new ArrayList<>();\n        int nChildren = 2000;\n        IntStream.range(0, nChildren).forEach(i -> names.add(randomString()));\n\n        for (int i=0; i < names.size(); i++) {\n            String filename = names.get(i);\n            context.getUserRoot().get().mkdir(filename, context.network, false, context.mirrorBatId(), context.crypto);\n            Set<FileWrapper> children = context.getUserRoot().get().getChildren(crypto.hasher, context.network).get();\n            Assert.assertTrue(\"All children present\", children.size() == i + 3); // 3 due to .keystore and shared\n        }\n    }\n\n    public static void checkFileContents(byte[] expected, FileWrapper f, UserContext context) {\n        long size = f.getFileProperties().size;\n        byte[] retrievedData = Serialize.readFully(f.getInputStream(context.network, context.crypto,\n            size, l-> {}).join(), f.getSize()).join();\n        assertEquals(expected.length, size);\n        assertTrue(\"Correct contents\", Arrays.equals(retrievedData, expected));\n    }\n\n    private static void checkFileContentsChunked(byte[] expected, FileWrapper f, UserContext context, int  nReads) throws Exception {\n\n        AsyncReader in = f.getInputStream(context.network, context.crypto,\n                f.getFileProperties().size, l -> {}).get();\n        assertTrue(nReads > 1);\n\n        long size = f.getSize();\n        long readLength = size/nReads;\n\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n\n        for (int i = 0; i < nReads; i++) {\n            long pos = i * readLength;\n            long len = i < nReads - 1 ? readLength : expected.length - pos;\n            LOG.info(\"Reading from \"+ pos +\" to \"+ (pos + len) +\" with total \"+ expected.length);\n            byte[] retrievedData = Serialize.readFully(in, len).get();\n            bout.write(retrievedData);\n        }\n        byte[] readBytes = bout.toByteArray();\n        assertEquals(\"Lengths correct\", readBytes.length, expected.length);\n\n        String start = ArrayOps.bytesToHex(Arrays.copyOfRange(expected, 0, 10));\n\n        for (int i = 0; i < readBytes.length; i++)\n            assertEquals(\"position  \" + i + \" out of \" + readBytes.length + \", start of file \" + start,\n                    ArrayOps.byteToHex(readBytes[i] & 0xFF),\n                    ArrayOps.byteToHex(expected[i] & 0xFF));\n\n        assertTrue(\"Correct contents\", Arrays.equals(readBytes, expected));\n    }\n\n\n    @Test\n    public void readWriteTest() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        Set<FileWrapper> children = userRoot.getChildren(crypto.hasher, context.network).get();\n\n        children.stream()\n                .map(FileWrapper::toString)\n                .forEach(System.out::println);\n\n        String name = randomString();\n        byte[] data = randomData(10*1024*1024); // 2 chunks to test block chaining\n\n        AsyncReader resetableFileInputStream = new AsyncReader.ArrayBacked(data);\n        FileWrapper updatedRoot = userRoot.uploadOrReplaceFile(name, resetableFileInputStream, data.length,\n                context.network, context.crypto, () -> false, l -> {}).get();\n\n        Optional<FileWrapper> opt = updatedRoot.getChildren(crypto.hasher, context.network).get()\n                .stream()\n                .filter(e -> e.getFileProperties().name.equals(name))\n                .findFirst();\n\n        assertTrue(\"found uploaded file\", opt.isPresent());\n\n        FileWrapper fileWrapper = opt.get();\n        long size = fileWrapper.getFileProperties().size;\n        AsyncReader in = fileWrapper.getInputStream(context.network, context.crypto, size, (l) -> {}).get();\n        byte[] retrievedData = Serialize.readFully(in, fileWrapper.getSize()).get();\n\n        boolean  dataEquals = Arrays.equals(data, retrievedData);\n\n        assertTrue(\"retrieved same data\", dataEquals);\n    }\n\n    @Test\n    public void deleteTest() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String name = randomString();\n        byte[] data = randomData(10*1024*1024); // 2 chunks to test block chaining\n\n        AsyncReader fileData = new AsyncReader.ArrayBacked(data);\n\n        FileWrapper updatedRoot = userRoot.uploadOrReplaceFile(name, fileData, data.length,\n                context.network, context.crypto, () -> false, l -> {}).get();\n        String otherName = name + \".other\";\n\n        FileWrapper updatedRoot2 = updatedRoot.uploadOrReplaceFile(otherName, fileData.reset().join(),\n                data.length, context.network, context.crypto, () -> false, l -> {}).get();\n\n        Optional<FileWrapper> opt = updatedRoot2.getChildren(crypto.hasher, context.network).get()\n                        .stream()\n                        .filter(e -> e.getFileProperties().name.equals(name))\n                        .findFirst();\n\n        assertTrue(\"found uploaded file\", opt.isPresent());\n\n        FileWrapper fileWrapper = opt.get();\n        long size = fileWrapper.getFileProperties().size;\n        AsyncReader in = fileWrapper.getInputStream(context.network, context.crypto, size, (l) -> {}).get();\n        byte[] retrievedData = Serialize.readFully(in, fileWrapper.getSize()).get();\n\n        boolean  dataEquals = Arrays.equals(data, retrievedData);\n\n        assertTrue(\"retrieved same data\", dataEquals);\n\n        //delete the file\n        fileWrapper.remove(updatedRoot2, PathUtil.get(username, name), context).get();\n\n        //re-create user-context\n        UserContext context2 = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot2 = context2.getUserRoot().get();\n\n\n        //check the file is no longer present\n        boolean isPresent = userRoot2.getChildren(crypto.hasher, context2.network).get()\n                .stream()\n                .anyMatch(e -> e.getFileProperties().name.equals(name));\n\n        Assert.assertFalse(\"uploaded file is deleted\", isPresent);\n\n\n        //check content of other file in same directory that was not removed\n        FileWrapper otherFileWrapper = userRoot2.getChildren(crypto.hasher, context2.network).get()\n                .stream()\n                .filter(e -> e.getFileProperties().name.equals(otherName))\n                .findFirst()\n                .orElseThrow(() -> new IllegalStateException(\"Missing other file\"));\n\n        AsyncReader asyncReader = otherFileWrapper.getInputStream(context2.network, context2.crypto, l -> {}).get();\n\n        byte[] otherRetrievedData = Serialize.readFully(asyncReader, otherFileWrapper.getSize()).get();\n        boolean  otherDataEquals = Arrays.equals(data, otherRetrievedData);\n        Assert.assertTrue(\"other file data is  intact\", otherDataEquals);\n    }\n\n    @Test\n    public void bulkDeleteTest() {\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n\n        Set<String> filenames = new HashSet<>();\n\n        for (int i=0; i < 20; i++) {\n            String name = randomString();\n            byte[] data = randomData(8 * 1024);\n\n            AsyncReader fileData = new AsyncReader.ArrayBacked(data);\n            userRoot = userRoot.uploadOrReplaceFile(name, fileData, data.length,\n                    context.network, context.crypto, () -> false, l -> {}).join();\n            filenames.add(name);\n        }\n\n        Set<FileWrapper> kids = userRoot.getChildren(crypto.hasher, context.network).join();\n        Set<String> kidNames = kids\n                .stream()\n                .map(f -> f.getName())\n                .collect(Collectors.toSet());\n\n        assertTrue(\"found uploaded files\", kidNames.containsAll(filenames));\n\n        //delete the files\n        List<FileWrapper> toDelete = kids.stream()\n                .filter(f -> filenames.contains(f.getName()))\n                .collect(Collectors.toList());\n        FileWrapper deleted = FileWrapper.deleteChildren(userRoot, toDelete, PathUtil.get(username), context).join();\n\n        //re-create user-context\n        UserContext context2 = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot2 = context2.getUserRoot().join();\n\n        //check the files are no longer present\n        List<FileWrapper> remaining = userRoot2.getChildren(crypto.hasher, context2.network).join().stream()\n                .filter(f -> filenames.contains(f.getName()))\n                .collect(Collectors.toList());\n        Assert.assertTrue(\"uploaded files are deleted\", remaining.isEmpty());\n    }\n\n    @Test\n    public void internalCopy() {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        Path home = PathUtil.get(username);\n\n        String filename = \"initialfile.bin\";\n        byte[] data = randomData(10*1024*1024); // 2 chunks to test block chaining\n\n        FileWrapper updatedUserRoot = userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data),\n                data.length, context.network, crypto, () -> false, x -> {}).join();\n\n        // copy the file\n        String foldername = \"afolder\";\n        updatedUserRoot.mkdir(foldername, context.network, false, userRoot.mirrorBatId(), crypto).join();\n        FileWrapper subfolder = context.getByPath(home.resolve(foldername)).join().get();\n        FileWrapper original = context.getByPath(home.resolve(filename)).join().get();\n        Boolean res = original.copyTo(subfolder, context).join();\n        FileWrapper copy = context.getByPath(home.resolve(foldername).resolve(filename)).join().get();\n        Assert.assertTrue(\"Different base key\", ! copy.getPointer().capability.rBaseKey.equals(original.getPointer().capability.rBaseKey));\n        Assert.assertTrue(\"Different metadata key\", ! getMetaKey(copy).equals(getMetaKey(original)));\n        Assert.assertTrue(\"Different data key\", ! getDataKey(copy).equals(getDataKey(original)));\n        checkFileContents(data, copy, context);\n    }\n\n    @Test\n    public void internalCopyDirToDir() {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network.clear(), crypto);\n        FileWrapper userRoot = context.getUserRoot().join();\n        Path home = PathUtil.get(username);\n\n        String filename = \"initialfile.bin\";\n        byte[] data = randomData(10*1024*1024); // 2 chunks to test block chaining\n\n        String foldername = \"afolder\";\n        userRoot = userRoot.mkdir(foldername, context.network, false, userRoot.mirrorBatId(), crypto).join();\n        String foldername2 = \"bfolder\";\n        userRoot.mkdir(foldername2, context.network, false, userRoot.mirrorBatId(), crypto).join();\n        FileWrapper folder2 = context.getByPath(home.resolve(foldername2)).join().get();\n\n\n        FileWrapper subfolder = context.getByPath(home.resolve(foldername)).join().get();\n        subfolder = subfolder.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data),\n                data.length, context.network, crypto, () -> false, x -> {}).join();\n\n        subfolder.copyTo(folder2, context).join();\n        Optional<FileWrapper> file = context.getByPath(PathUtil.get(username, foldername2, foldername, filename)).join();\n        Assert.assertTrue(\"File copied in dir\", file.isPresent());\n    }\n\n    @Test\n    public void usage() {\n        String username = generateUsername();\n        String password = \"password\";\n        Assert.assertTrue(network.instanceAdmin.acceptingSignups().join().free);\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        long quota = context.getQuota().join();\n        long usage = context.getSpaceUsage(false).join();\n        Assert.assertTrue(\"non zero quota\", quota > 0);\n        Assert.assertTrue(\"non zero space usage\", usage > 0);\n\n        CompletableFuture<List<SpaceUsage.LabelledSignedSpaceRequest>> nonAdmin = context.getPendingSpaceRequests();\n\n        Assert.assertTrue(\"Non admins get an empty list\", nonAdmin.join().isEmpty());\n\n        // Now let's request some more quota and get it approved by an admin\n        context.requestSpace(quota * 2, false).join();\n\n        // retrieve, decode and approve request as admin\n        UserContext admin = PeergosNetworkUtils.ensureSignedUp(\"peergos\", \"testpassword\", network.clear(), crypto);\n        List<SpaceUsage.LabelledSignedSpaceRequest> spaceReqs = admin.getPendingSpaceRequests().join();\n        try {\n            List<DecodedSpaceRequest> parsed = admin.decodeSpaceRequests(spaceReqs).join();\n            DecodedSpaceRequest req = parsed.stream().filter(r -> r.getUsername().equals(username)).findFirst().get();\n            admin.approveSpaceRequest(req).join();\n        } catch (Exception e) {\n            List<DecodedSpaceRequest> parsed = admin.decodeSpaceRequests(spaceReqs).join();\n            DecodedSpaceRequest req = parsed.stream().filter(r -> r.getUsername().equals(username)).findFirst().get();\n            admin.approveSpaceRequest(req).join();\n        }\n\n        long updatedQuota = context.getQuota().join();\n        Assert.assertTrue(\"Quota updated \" + updatedQuota + \" != 2 * \" + quota, updatedQuota == 2 * quota);\n    }\n\n    @Test\n    public void correctUsageAndSpaceRecovery() throws Exception {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        long initialUsage = context.getSpaceUsage(false).join();\n\n        UserCleanup.checkRawUsage(context);\n        String filename = \"test.bin\";\n        context.getUserRoot().join().uploadFileJS(filename, AsyncReader.build(new byte[10*1024*1024]),\n                0, 10*1024*1024, true, context.mirrorBatId(), network, crypto, x-> {},\n                context.getTransactionService(), f -> Futures.of(true)).join();\n        String dirName = \"subdir\";\n        context.getUserRoot().join().mkdir(dirName, network, false, context.mirrorBatId(), crypto).join();\n        Thread.sleep(5_000); // Allow time for space usage recalculation\n        UserCleanup.checkRawUsage(context);\n\n        // now delete the file and dir\n        Path filePath = PathUtil.get(username, filename);\n        context.getByPath(filePath).join().get().remove(context.getUserRoot().join(), filePath, context).join();\n        Path dirPath = PathUtil.get(username, dirName);\n        context.getByPath(dirPath).join().get().remove(context.getUserRoot().join(), dirPath, context).join();\n        try {Thread.sleep(2000);} catch (InterruptedException e) {}\n        UserCleanup.checkRawUsage(context);\n\n        long finalUsage = context.getSpaceUsage(false).join();\n        long diff = finalUsage - initialUsage;\n        Assert.assertTrue(diff == 0);\n    }\n\n    @Test\n    public void serverMessaging() {\n        String username = generateUsername();\n        String password = \"password\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n\n        String serverMsgBody = \"Welcome to the world of Peergos!\";\n        ServerMessageStore msgStore = (ServerMessageStore) service.serverMessages;\n        msgStore.addMessage(username, new ServerMessage(1, ServerMessage.Type.FromServer,\n                System.currentTimeMillis(), serverMsgBody, Optional.empty(), false));\n\n        String replyBody = \"Thanks for making Peergos awesome!\";\n        context.sendReply(context.getNewMessages().join().get(0), replyBody).join();\n        List<ServerMessage> afterReply = context.getNewMessages().join();\n        Assert.assertTrue(afterReply.size() == 0);\n\n        String msgBody = \"Peergos really is amazing! I love it!\";\n        context.sendFeedback(msgBody).join();\n        List<ServerMessage> messages = context.getNewMessages().join();\n        Assert.assertTrue(messages.size() == 0);\n\n        List<ServerMessage> onServer = msgStore.getMessages(username);\n        Assert.assertTrue(onServer.size() == 3);\n        ServerMessage reply = onServer.get(2);\n        Assert.assertTrue(reply.contents.equals(msgBody));\n\n        List<ServerConversation> convs = context.getServerConversations().join();\n        Assert.assertTrue(convs.size() == 0);\n\n        msgStore.addMessage(username, new ServerMessage(1, ServerMessage.Type.FromServer,\n                System.currentTimeMillis(), \"Thank you for supporting Peergos.\", Optional.empty(), false));\n        context.dismissMessage(context.getNewMessages().join().get(0)).join();\n        List<ServerMessage> updatedMessages = context.getNewMessages().join();\n        Assert.assertTrue(updatedMessages.size() == 0);\n        // Test that we get rate limited\n        try {\n            for (int i = 0; i < 20; i++)\n                context.sendFeedback(\"SPAM \" + i).join();\n            Assert.fail();\n        } catch (RuntimeException e) {}\n    }\n\n    public static SymmetricKey getDataKey(FileWrapper file) {\n        return file.getPointer().fileAccess.getDataKey(file.getPointer().capability.rBaseKey);\n    }\n\n    public static SymmetricKey getMetaKey(FileWrapper file) {\n        return file.getPointer().capability.rBaseKey;\n    }\n\n    @Test\n    public void deleteDirectoryTest() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        Set<FileWrapper> children = userRoot.getChildren(crypto.hasher, context.network).get();\n\n        children.stream()\n                .map(FileWrapper::toString)\n                .forEach(System.out::println);\n\n        String folderName = \"a_folder\";\n        boolean isSystemFolder = false;\n\n        //create the directory\n        userRoot.mkdir(folderName, context.network, isSystemFolder, userRoot.mirrorBatId(), context.crypto).get();\n\n        FileWrapper updatedUserRoot = context.getUserRoot().get();\n        FileWrapper directory = updatedUserRoot.getChildren(crypto.hasher, context.network)\n                .get()\n                .stream()\n                .filter(e -> e.getFileProperties().name.equals(folderName))\n                .findFirst()\n                .orElseThrow(() -> new IllegalStateException(\"Missing created folder \" + folderName));\n\n        // check the parent link doesn't include write access\n        AbsoluteCapability cap = directory.getPointer().capability;\n        CryptreeNode fileAccess = directory.getPointer().fileAccess;\n        RelativeCapability toParent = fileAccess.getParentCapability(fileAccess.getParentKey(cap.rBaseKey)).get();\n        Assert.assertTrue(\"parent link shouldn't include write access\",\n                ! toParent.wBaseKeyLink.isPresent());\n        Assert.assertTrue(\"parent link shouldn't include public write key\",\n                ! toParent.writer.isPresent());\n\n        //remove the directory\n        directory.remove(updatedUserRoot, PathUtil.get(username, folderName), context).get();\n\n        //ensure folder directory not  present\n        boolean isPresent = context.getUserRoot().get().getChildren(crypto.hasher, context.network)\n                .get()\n                .stream()\n                .filter(e -> e.getFileProperties().name.equals(folderName))\n                .findFirst()\n                .isPresent();\n\n        Assert.assertFalse(\"folder not present after remove\", isPresent);\n\n        //can sign-in again\n        try {\n            UserContext context2 = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n            FileWrapper userRoot2 = context2.getUserRoot().get();\n        } catch (Exception ex) {\n            fail(\"Failed to log-in and see user-root \" + ex.getMessage());\n        }\n\n    }\n\n    @Test\n    public void overwriteContentsOfFileGrowFile() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedata.txt\";\n        Path filePath = PathUtil.get(username, filename);\n        byte[] data = randomData(6);\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        checkFileContents(data, context.getByPath(filePath).join().get(), context);\n\n        FileWrapper fileV2 = context.getByPath(filePath).join().get();\n\n        byte[] bytes = \"11111111\".getBytes();\n        AsyncReader java_reader = peergos.shared.user.fs.AsyncReader.build(bytes);\n        int newSizeLo = bytes.length;\n        fileV2.overwriteFileJS(java_reader, 0, newSizeLo,\n                context.network, context.crypto, len -> {}).join();\n\n        checkFileContents(bytes, context.getByPath(filePath).join().get(), context);\n    }\n\n    @Test\n    public void overwriteContentsOfFileShrinkFile() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        String filename = \"somedata.txt\";\n        Path filePath = PathUtil.get(username, filename);\n        byte[] data = randomData(6000);\n        for(int i=0; i < data.length; i++) {\n            if(data[i] == 0) {\n                data[i] = 1;\n            }\n        }\n        userRoot.uploadOrReplaceFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,\n                context.crypto, () -> false, l -> {}).get();\n        checkFileContents(data, context.getByPath(filePath).join().get(), context);\n\n        FileWrapper fileV2 = context.getByPath(filePath).join().get();\n\n        byte[] bytes = \"11111111\".getBytes();\n        AsyncReader java_reader = peergos.shared.user.fs.AsyncReader.build(bytes);\n        int newSizeLo = bytes.length;\n        fileV2.overwriteFileJS(java_reader, 0, newSizeLo,\n                context.network, context.crypto, len -> {}).join();\n\n        checkFileContents(bytes, context.getByPath(filePath).join().get(), context);\n\n        FileWrapper fileV3 = context.getByPath(filePath).join().get();\n        byte[] retrievedData = Serialize.readFully(fileV3.getInputStream(context.network, context.crypto,\n                6000, l-> {}).join(), 6000).join();\n        int nonZeroBytes = (int)IntStream.range(0, retrievedData.length).map(i-> retrievedData[i]).filter(a -> a != 0).count();\n        Assert.assertTrue(\"File truncated\", nonZeroBytes == bytes.length);\n\n\n    }\n\n    @Test\n    public void deleteWithOwnSubtreeWriterLeavesNoOrphanedChampEntries() throws Exception {\n        String username = generateUsername();\n        UserContext context = PeergosNetworkUtils.ensureSignedUp(username, \"test01\", network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n\n        // Create toDelete/ and give it its own signing key (writer W) so this becomes a\n        // multi-writer delete\n        userRoot.mkdir(\"toDelete\", context.network, false, userRoot.mirrorBatId(), context.crypto).get();\n        String toDeletePath = \"/\" + username + \"/toDelete\";\n        context.shareWriteAccessWith(PathUtil.get(toDeletePath), Collections.emptySet()).join();\n\n        // Upload files after shareWriteAccessWith so they live in W's CHAMP, not P's.\n        FileWrapper freshToDelete = context.getByPath(toDeletePath).get().get();\n        byte[] data = \"hello\".getBytes();\n        uploadFileSection(freshToDelete, \"a.txt\", new AsyncReader.ArrayBacked(data), 0, data.length,\n                context.network, context.crypto, l -> {}).get();\n        uploadFileSection(freshToDelete, \"b.txt\", new AsyncReader.ArrayBacked(data), 0, data.length,\n                context.network, context.crypto, l -> {}).get();\n\n        // Build a network backed by the same storage but with a mutable-pointer layer that\n        // throws after the FIRST successful setPointer call, simulating a crash between the\n        // two sequential writer pointer updates in BufferedNetworkAccess.commit().\n        AtomicInteger pointerCommits = new AtomicInteger(0);\n        MutablePointers failAfterFirst = new MutablePointers() {\n            @Override\n            public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n                return service.mutable.getPointer(owner, writer);\n            }\n            @Override\n            public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] signed) {\n                if (pointerCommits.incrementAndGet() > 1)\n                    return Futures.errored(new RuntimeException(\"Simulated crash after 1st pointer commit\"));\n                return service.mutable.setPointer(owner, writer, signed);\n            }\n            @Override\n            public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n                // Simulate an old server: apply sequentially, crashing after the first commit\n                return Futures.reduceAll(updates, true,\n                        (b, u) -> setPointer(owner, u.writer, u.signed),\n                        (a, b) -> a && b);\n            }\n            @Override\n            public MutablePointers clearCache() {\n                return this;\n            }\n        };\n        NetworkAccess deleteNetwork = NetworkAccess.buildBuffered(service.storage, service.bats,\n                service.coreNode, service.account, failAfterFirst, 0, service.social,\n                service.controller, service.usage, service.serverMessages,\n                crypto.hasher, Arrays.asList(\"peergos\"), false);\n        UserContext deleteContext = PeergosNetworkUtils.ensureSignedUp(username, \"test01\", deleteNetwork, crypto);\n\n        FileWrapper deleteParent = deleteContext.getByPath(\"/\" + username).get().get();\n        FileWrapper deleteToDelete = deleteContext.getByPath(toDeletePath).get().get();\n        PublicKeyHash owner = deleteParent.owner();\n        PublicKeyHash writerW = deleteToDelete.writer();\n\n        // Run the actual delete — it must throw after the first pointer commit.\n        try {\n            FileWrapper.deleteChildren(deleteParent, Collections.singleton(deleteToDelete),\n                    PathUtil.get(\"/\" + username), deleteContext).get();\n            Assert.fail(\"Expected delete to fail after first pointer commit\");\n        } catch (ExecutionException expected) { }\n\n        // Enumerate all CHAMP entries committed for writer W\n        Set<ByteArrayWrapper> champKeysW = getAllChampKeys(owner, writerW, deleteContext);\n\n        // Enumerate every map key reachable by DFS from the filesystem root\n        FileWrapper updatedRoot = deleteContext.getByPath(\"/\" + username).get().get();\n        Set<ByteArrayWrapper> reachableKeys = collectReachableMapKeys(updatedRoot,\n                deleteContext.crypto.hasher, deleteContext.network);\n\n        // Any key in W's CHAMP not reachable from the filesystem root is an orphan\n        Set<ByteArrayWrapper> orphaned = new HashSet<>(champKeysW);\n        orphaned.removeAll(reachableKeys);\n\n        Assert.assertTrue(\"No orphaned CHAMP entries in W's tree after partial delete\",\n                orphaned.isEmpty());\n    }\n\n    private static Set<ByteArrayWrapper> getAllChampKeys(PublicKeyHash owner, PublicKeyHash writer, UserContext ctx) {\n        WriterData wd = WriterData.getWriterData(owner, writer, ctx.network.mutable, ctx.network.dhtClient).join().props.get();\n        if (!wd.tree.isPresent())\n            return Collections.emptySet();\n        Set<ByteArrayWrapper> keys = new HashSet<>();\n        ChampWrapper.create(owner, (Cid) wd.tree.get(), Optional.empty(),\n                k -> Futures.of(k.data),\n                ctx.network.dhtClient, ctx.crypto.hasher,\n                c -> (CborObject.CborMerkleLink) c).join()\n                .applyToAllMappings(owner, pair -> {\n                    keys.add(pair.left);\n                    return Futures.of(true);\n                }).join();\n        return keys;\n    }\n\n    private static Set<ByteArrayWrapper> collectReachableMapKeys(FileWrapper f, Hasher hasher, NetworkAccess network) {\n        Set<ByteArrayWrapper> result = new HashSet<>();\n        result.add(new ByteArrayWrapper(f.getPointer().capability.getMapKey()));\n        if (f.isDirectory()) {\n            try {\n                f.getChildren(hasher, network).join()\n                        .forEach(child -> result.addAll(collectReachableMapKeys(child, hasher, network)));\n            } catch (Exception e) {\n                // directory entry exists in listing but its blocks are gone — dead reference, not orphan\n            }\n        }\n        return result;\n    }\n\n    public static String randomString() {\n        return UUID.randomUUID().toString();\n    }\n\n    public static byte[] randomData(int length) {\n        byte[] data = new byte[length];\n        random.nextBytes(data);\n        return data;\n    }\n\n    private static Path TMP_DIR = PathUtil.get(\"test\",\"resources\",\"tmp\");\n\n    private static void ensureTmpDir() {\n        File dir = TMP_DIR.toFile();\n        if (! dir.isDirectory() &&  ! dir.mkdirs())\n            throw new IllegalStateException(\"Could not find or create specified tmp directory \"+ TMP_DIR);\n    }\n\n    private static Path createTmpFile(String filename) throws IOException {\n        ensureTmpDir();\n        Path resolve = TMP_DIR.resolve(filename);\n        File file = resolve.toFile();\n        file.createNewFile();\n        file.deleteOnExit();\n        return resolve;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/VarintTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.io.*;\n\npublic class VarintTests {\n\n    @Test\n    public void minimalEncoding() throws IOException {\n\n        try {\n            Multihash.readVarint(new ByteArrayInputStream(new byte[]{(byte) 0x81, 0x00}));\n            throw new RuntimeException(\"Should throw for non minimal encoding\");\n        } catch (IllegalStateException e) {}\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/VersionedWriteOnlyStorage.java",
    "content": "package peergos.server.tests;\n\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.space.UsageStore;\nimport peergos.server.storage.*;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.BatWithId;\nimport peergos.shared.user.fs.EncryptedCapability;\nimport peergos.shared.user.fs.SecretLink;\nimport peergos.shared.util.EfficientHashMap;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.Pair;\nimport peergos.shared.util.ProgressConsumer;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Consumer;\nimport java.util.stream.Stream;\n\npublic class VersionedWriteOnlyStorage implements DeletableContentAddressedStorage {\n    public final Map<PublicKeyHash, Map<Cid, Boolean>> storage = new EfficientHashMap<>();\n    private final BlockMetadataStore metadb;\n    private final AtomicLong nextVersion = new AtomicLong(0);\n    private final Map<Cid, Set<String>> versions = new HashMap<>();\n\n    public VersionedWriteOnlyStorage(BlockMetadataStore metadb) {\n        this.metadb = metadb;\n    }\n\n    public BlockVersion add(PublicKeyHash owner, Cid c) {\n        Set<String> versions = this.versions.computeIfAbsent(c, x -> new HashSet<>());\n        String version = Long.toString(nextVersion.incrementAndGet());\n        versions.add(version);\n        storage.computeIfAbsent(owner, o -> new HashMap<>()).put(c, true);\n        return new BlockVersion(c, version, true);\n    }\n\n    public Optional<BlockMetadataStore> getBlockMetadataStore() {\n        return Optional.of(metadb);\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        return storage.getOrDefault(owner, Collections.emptyMap()).keySet()\n                .stream()\n                .map(c -> new Pair<>(owner, c));\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore) {\n        return storage.entrySet().stream()\n                .flatMap(e -> e.getValue()\n                        .keySet()\n                        .stream()\n                        .map(c -> new Pair<>(e.getKey(), c)));\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        List<BlockVersion> batch = new ArrayList<>();\n        for (PublicKeyHash writer : storage.keySet()) {\n            for (Cid cid : storage.get(writer).keySet()) {\n                Set<String> versions = this.versions.getOrDefault(cid, Collections.emptySet());\n                if (!versions.isEmpty()) {\n                    String latest = versions.stream().sorted(Comparator.reverseOrder()).findFirst().get();\n                    for (String version : versions) {\n                        batch.add(new BlockVersion(cid, version, version.equals(latest)));\n                    }\n                    if (batch.size() >= 1000) {\n                        res.accept(batch);\n                        batch.clear();\n                    }\n                }\n            }\n        }\n        res.accept(batch);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return List.of();\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        return storage.get(owner).containsKey(hash);\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid block) {\n        throw new IllegalStateException();\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, BlockVersion v) {\n        Set<String> versions = this.versions.get(v.cid);\n        if (versions == null || versions.isEmpty())\n            return;\n        versions.remove(v.version);\n        if (versions.isEmpty())\n            storage.remove(v.cid);\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, new byte[32]));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid) {\n        return Futures.of(blocks.stream().map(b -> hashToCid(b, false)).toList());\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid, ProgressConsumer<Long> progressCounter) {\n        return Futures.of(blocks.stream().map(b -> hashToCid(b, true)).toList());\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public void partitionByUser(UsageStore usage,\n                                JdbcIpnsAndSocial mutable,\n                                PublicKeyHash pkiKey) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    public static Cid hashToCid(byte[] input, boolean isRaw) {\n        byte[] hash = hash(input);\n        return new Cid(1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n\n    public static byte[] hash(byte[] input) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"SHA-256\");\n            md.update(input);\n            return md.digest();\n        } catch (NoSuchAlgorithmException e) {\n            throw new IllegalStateException(\"couldn't find hash algorithm\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/WriteOnlyStorage.java",
    "content": "package peergos.server.tests;\n\nimport peergos.server.corenode.JdbcIpnsAndSocial;\nimport peergos.server.space.UsageStore;\nimport peergos.server.storage.*;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.corenode.CoreNode;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.BatWithId;\nimport peergos.shared.user.fs.EncryptedCapability;\nimport peergos.shared.user.fs.SecretLink;\nimport peergos.shared.util.EfficientHashMap;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.Pair;\nimport peergos.shared.util.ProgressConsumer;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Consumer;\nimport java.util.stream.Stream;\n\npublic class WriteOnlyStorage implements DeletableContentAddressedStorage {\n    public final Map<PublicKeyHash, Map<Cid, Boolean>> storage = new EfficientHashMap<>();\n    private final BlockMetadataStore metadb;\n\n    public WriteOnlyStorage(BlockMetadataStore metadb) {\n        this.metadb = metadb;\n    }\n\n    public Optional<BlockMetadataStore> getBlockMetadataStore() {\n        return Optional.of(metadb);\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(PublicKeyHash owner, boolean useBlockstore) {\n        return storage.getOrDefault(owner, Collections.emptyMap()).keySet()\n                .stream()\n                .map(c -> new Pair<>(owner, c));\n    }\n\n    @Override\n    public Stream<Pair<PublicKeyHash, Cid>> getAllBlockHashes(boolean useBlockstore) {\n        return storage.entrySet().stream()\n                .flatMap(e -> e.getValue()\n                        .keySet()\n                        .stream()\n                        .map(c -> new Pair<>(e.getKey(), c)));\n    }\n\n    @Override\n    public void getAllBlockHashVersions(PublicKeyHash owner, Consumer<List<BlockVersion>> res) {\n        List<BlockVersion> batch = new ArrayList<>();\n        for (Cid cid : storage.getOrDefault(owner, Collections.emptyMap()).keySet()) {\n            batch.add(new BlockVersion(cid, \"hey\", true));\n            if (batch.size() == 1000) {\n                res.accept(batch);\n                batch.clear();\n            }\n        }\n        res.accept(batch);\n    }\n\n    @Override\n    public List<Cid> getOpenTransactionBlocks(PublicKeyHash owner) {\n        return List.of();\n    }\n\n    @Override\n    public void clearOldTransactions(PublicKeyHash owner, long cutoffMillis) {\n\n    }\n\n    @Override\n    public boolean hasBlock(PublicKeyHash owner, Cid hash) {\n        return storage.containsKey(owner) && storage.get(owner).containsKey(hash);\n    }\n\n    @Override\n    public void delete(PublicKeyHash owner, Cid block) {\n        storage.getOrDefault(owner, Collections.emptyMap()).remove(block);\n    }\n\n    @Override\n    public void setPki(CoreNode pki) {\n\n    }\n\n    @Override\n    public void partitionByUser(UsageStore usage,\n                                JdbcIpnsAndSocial mutable,\n                                PublicKeyHash pkiKey) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds,\n                                                      PublicKeyHash owner,\n                                                      Cid hash,\n                                                      Optional<BatWithId> bat,\n                                                      Cid ourId,\n                                                      Hasher h,\n                                                      boolean doAuth,\n                                                      boolean persistBlock) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(List<Multihash> peerIds, PublicKeyHash owner, Cid hash, String auth, boolean persistBlock) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, new byte[32]));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid) {\n        return Futures.of(blocks.stream().map(b -> hashToCid(b, false)).toList());\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid, ProgressConsumer<Long> progressCounter) {\n        return Futures.of(blocks.stream().map(b -> hashToCid(b, true)).toList());\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> getLinks(PublicKeyHash owner, Cid root, List<Multihash> peerids) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<BlockMetadata> getBlockMetadata(PublicKeyHash owner, Cid block) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Not implemented!\");\n    }\n\n    public static Cid hashToCid(byte[] input, boolean isRaw) {\n        byte[] hash = hash(input);\n        return new Cid(1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n    }\n\n    public static byte[] hash(byte[] input) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"SHA-256\");\n            md.update(input);\n            return md.digest();\n        } catch (NoSuchAlgorithmException e) {\n            throw new IllegalStateException(\"couldn't find hash algorithm\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/WriterDataTests.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.corenode.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\n\nimport java.sql.*;\nimport java.util.*;\n\npublic class WriterDataTests {\n\n    @Test\n    public void tolerateLoopsInOwnedKeys() {\n        Crypto crypto = Main.initCrypto();\n        Hasher hasher = crypto.hasher;\n        DeletableContentAddressedStorage dht = new RAMStorage(hasher);\n        MutablePointers mutable = UserRepository.build(dht, new JdbcIpnsAndSocial(Main.buildEphemeralSqlite(), new SqliteCommands()), hasher);\n\n        SigningKeyPair pairA = SigningKeyPair.random(crypto.random, crypto.signer);\n        PublicKeyHash pubA = ContentAddressedStorage.hashKey(pairA.publicSigningKey);\n        TransactionId test = dht.startTransaction(pubA).join();\n        SigningPrivateKeyAndPublicHash signerA = new SigningPrivateKeyAndPublicHash(pubA, pairA.secretSigningKey);\n\n        SigningKeyPair pairB = SigningKeyPair.random(crypto.random, crypto.signer);\n        PublicKeyHash pubB = ContentAddressedStorage.hashKey(pairB.publicSigningKey);\n        SigningPrivateKeyAndPublicHash signerB = new SigningPrivateKeyAndPublicHash(pubB, pairB.secretSigningKey);\n\n        WriterData wdA = IpfsTransaction.call(pubA, tid -> WriterData.createEmpty(pubA, signerA, dht, hasher, tid), dht).join();\n        WriterData wdB = IpfsTransaction.call(pubA, tid -> WriterData.createEmpty(pubA, signerB, dht, hasher, tid), dht).join();\n\n        WriterData wdA2 = wdA.addOwnedKey(pubA, signerA, OwnerProof.build(signerB, pubA).join(), dht, hasher).join();\n        wdA2.commit(pubA, signerA, MaybeMultihash.empty(), Optional.empty(), mutable, dht, hasher, test).join();\n        CommittedWriterData bCurrentCwd = wdB.commit(pubA, signerB, MaybeMultihash.empty(), Optional.empty(), mutable, dht, hasher, test).join().get(pubB);\n\n        CommittedWriterData.Retriever retriever = (h, s) -> DeletableContentAddressedStorage.getWriterData(Collections.emptyList(), pubA, h, s, false, dht.id().join(), hasher, dht);\n        Set<PublicKeyHash> ownedByA1 = DeletableContentAddressedStorage.getOwnedKeysRecursive(pubA, pubA, mutable, retriever, dht, hasher).join();\n        Set<PublicKeyHash> ownedByB1 = DeletableContentAddressedStorage.getOwnedKeysRecursive(pubA, pubB, mutable, retriever, dht, hasher).join();\n\n        Assert.assertTrue(ownedByA1.size() == 2);\n        Assert.assertTrue(ownedByB1.size() == 1);\n\n        MaybeMultihash bCurrent = bCurrentCwd.hash;\n        WriterData wdB2 = wdB.addOwnedKey(pubA, signerB, OwnerProof.build(signerA, pubB).join(), dht, hasher).join();\n        wdB2.commit(pubA, signerB, bCurrent, bCurrentCwd.sequence, mutable, dht, hasher, test).join();\n\n        Set<PublicKeyHash> ownedByA2 = DeletableContentAddressedStorage.getOwnedKeysRecursive(pubA, pubA, mutable, retriever, dht, hasher).join();\n        Set<PublicKeyHash> ownedByB2 = DeletableContentAddressedStorage.getOwnedKeysRecursive(pubA, pubB, mutable, retriever, dht, hasher).join();\n\n        Assert.assertTrue(ownedByA2.size() == 2);\n        Assert.assertTrue(ownedByB2.size() == 2);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/WriterTest.java",
    "content": "package peergos.server.tests;\n\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport peergos.server.Builder;\nimport peergos.server.Main;\nimport peergos.server.util.Args;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.user.App;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.fs.FileWrapper;\nimport peergos.shared.util.*;\n\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class WriterTest {\n    private static Args args = UserTests.buildArgs().with(\"useIPFS\", \"false\");\n    private static Random random = new Random();\n    private static final Crypto crypto = Main.initCrypto();\n    private NetworkAccess network = null;\n    private UserContext emailBridgeContext = null;\n    private static final String emailBridgeUsername = \"bridge\";\n    private static final String emailBridgePassword = \"notagoodone\";\n    private static final String url = \"http://localhost:\" + args.getArg(\"port\");\n    private static final boolean isPublicServer = false;\n\n    public WriterTest() throws Exception{\n        network = Builder.buildJavaNetworkAccess(new URL(url), isPublicServer, Optional.empty(), Optional.empty()).get();\n        emailBridgeContext = PeergosNetworkUtils.ensureSignedUp(emailBridgeUsername, emailBridgePassword, network, crypto);\n    }\n\n    @BeforeClass\n    public static void init() {\n        Main.PKI_INIT.main(args);\n    }\n\n    protected String generateUsername() {\n        return \"test\" + Math.abs(random.nextInt() % 1_000_000);\n    }\n\n    @Test\n    public void sendTest() throws Exception {\n        String password = \"notagoodone\";\n        UserContext userContext = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(), password, network, crypto);\n\n        List<UserContext> shareeUsers = Arrays.asList(emailBridgeContext);\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(userContext), shareeUsers);\n\n        App emailApp = App.init(userContext, \"email\").join();\n        List<String> dirs = Arrays.asList(\"pending\");// if i add this then it works , \"pending/output\");\n        Path attachmentsDir = PathUtil.get(\".apps\", \"email\", \"data\");\n        for(String dir : dirs) {\n            Path dirFromHome = attachmentsDir.resolve(PathUtil.get(dir));\n            Optional<FileWrapper> homeOpt = userContext.getByPath(userContext.username).join();\n            homeOpt.get().getOrMkdirs(dirFromHome, userContext.network, true, userContext.mirrorBatId(), userContext.crypto).join();\n        }\n        \n        //the shareWriteAccessWith call leads to the problem...\n        List<String> sharees = Arrays.asList(emailBridgeContext.username);\n        String dirStr = userContext.username + \"/.apps/email/data/pending\";\n        Path directoryPath = peergos.client.PathUtils.directoryToPath(dirStr.split(\"/\"));\n        userContext.shareWriteAccessWith(directoryPath, sharees.stream().collect(Collectors.toSet())).join();\n\n        byte[] bytes = randomData(10);\n        Path filePath = PathUtil.get(\"pending\", \"output\", \"data.dat\");\n        emailApp.writeInternal(filePath, bytes, null).join();\n    }\n\n    @Test\n    public void deleteTest() throws Exception {\n        String password = \"notagoodone\";\n        UserContext userContext = PeergosNetworkUtils.ensureSignedUp(\"a-\" + generateUsername(), password, network, crypto);\n\n        List<UserContext> shareeUsers = Arrays.asList(emailBridgeContext);\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(userContext), shareeUsers);\n\n        App emailApp = App.init(userContext, \"email\").join();\n        List<String> dirs = Arrays.asList(\"pending\", \"pending/inbox\");\n        Path attachmentsDir = PathUtil.get(\".apps\", \"email\", \"data\");\n        for(String dir : dirs) {\n            Path dirFromHome = attachmentsDir.resolve(PathUtil.get(dir));\n            Optional<FileWrapper> homeOpt = userContext.getByPath(userContext.username).join();\n            homeOpt.get().getOrMkdirs(dirFromHome, userContext.network, true, userContext.mirrorBatId(), userContext.crypto).join();\n        }\n        List<String> sharees = Arrays.asList(emailBridgeContext.username);\n        String dirStr = userContext.username + \"/.apps/email/data/pending\";\n        Path directoryPath = peergos.client.PathUtils.directoryToPath(dirStr.split(\"/\"));\n        userContext.shareWriteAccessWith(directoryPath, sharees.stream().collect(Collectors.toSet())).join();\n\n        byte[] bytes = randomData(100);\n        Path filePath = PathUtil.get(\"pending/outbox/data.id\");\n        emailApp.writeInternal(filePath, bytes, null).join();\n\n        String path = userContext.username + \"/.apps/email/data/pending/outbox\";\n        Optional<FileWrapper> directory = emailBridgeContext.getByPath(path).get();\n\n        Set<FileWrapper> files = directory.get().getChildren(emailBridgeContext.crypto.hasher, emailBridgeContext.network).get();\n        for (FileWrapper file : files) {\n            Path pathToFile = PathUtil.get(path).resolve(file.getName());\n            file.remove(directory.get(), pathToFile, emailBridgeContext).get();\n        }\n    }\n\n    public static byte[] randomData(int length) {\n        byte[] data = new byte[length];\n        random.nextBytes(data);\n        return data;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/FIPS203Tests.java",
    "content": "package peergos.server.tests.fips203;\n\nimport org.junit.Test;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.FIPS203;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.MimicloneFIPS203;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.EncapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\n\nimport javax.crypto.Cipher;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.GCMParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.security.SecureRandom;\nimport java.util.Base64;\n\nimport static org.junit.Assert.*;\n\npublic class FIPS203Tests {\n\n    private static final String CIPHER_ALGORITHM = \"AES/GCM/NoPadding\";\n    private static final String SECURE_RANDOM_ALGORITHM = \"DRBG\";\n    private static final int GCM_IV_LENGTH = 12;\n    private static final int GCM_TAG_LENGTH = 16;\n\n    /**\n     * Validates that an invocation of {@link FIPS203#generateKeyPair()} with the provided instance creates a\n     * {@link KeyPair} instance that meets all the invariants in terms of existence and size.  Note that because\n     * the concrete implementation of {@link FIPS203} must generate random values as part of the process it is\n     * not possible to provide static test vector validations at this level.\n     *\n     * @param fips203MlKem An instance of the {@link FIPS203} interface using one of the defined {@link ParameterSet}s.\n     * @return A {@link KeyPair} object which has passed all checks relating to existing and appropriate length.\n     */\n    private KeyPair validateKeyPairGeneration(\n            FIPS203 fips203MlKem\n    ) {\n\n        // Retrieve the parameter set\n        ParameterSet parameterSet = fips203MlKem.getParameterSet();\n\n        // Generate the key pair\n        KeyPair keyPair = fips203MlKem.generateKeyPair();\n\n        // Ensure the KeyPair object is not null\n        assertNotNull(keyPair);\n\n        // Get the EncapsulationKey\n        EncapsulationKey encapsulationKey = keyPair.getEncapsulationKey();\n        assertNotNull(encapsulationKey);\n\n        // Validate the encapsulation key bytes\n        byte[] encapsulationKeyBytes = encapsulationKey.getBytes();\n        assertNotNull(encapsulationKeyBytes);\n        assertEquals(parameterSet.getEncapsulationKeyLength(), encapsulationKeyBytes.length);\n\n        // Validate the decapsulation key\n        DecapsulationKey decapsulationKey = keyPair.getDecapsulationKey();\n        assertNotNull(decapsulationKey);\n\n        // Validate the decapsulation key bytes\n        byte[] decapsulationKeyBytes = decapsulationKey.getBytes();\n        assertNotNull(decapsulationKeyBytes);\n        assertEquals(parameterSet.getDecapsulationKeyLength(), decapsulationKeyBytes.length);\n\n        return keyPair;\n    }\n\n    private Encapsulation validateEncapsulation(\n            FIPS203 fips203MlKem,\n            EncapsulationKey encapsulationKey\n    ) {\n\n        // Retrieve the parameter set\n        ParameterSet parameterSet = fips203MlKem.getParameterSet();\n\n        // Encapsulate the shared secret\n        Encapsulation encapsulation = fips203MlKem.encapsulate(encapsulationKey);\n\n        // Validate that the encapsulation is not null\n        assertNotNull(encapsulation);\n\n        // Retrieve and validate the Shared Secret Key\n        SharedSecretKey sharedSecretKey = encapsulation.getSharedSecretKey();\n        assertNotNull(sharedSecretKey);\n\n        // Retrieve and validate the raw shared secret bytes\n        byte[] sharedSecretKeyBytes = sharedSecretKey.getBytes();\n        assertNotNull(sharedSecretKeyBytes);\n        assertEquals(parameterSet.getSharedSecretKeyLength(), sharedSecretKeyBytes.length);\n\n        // Retrieve the cipher text\n        CipherText cipherText = encapsulation.getCipherText();\n        assertNotNull(cipherText);\n\n        // Retrieve and validate the raw ciphertext bytes\n        byte[] cipherTextBytes = cipherText.getBytes();\n        assertNotNull(cipherTextBytes);\n        assertEquals(parameterSet.getCiphertextLength(), cipherTextBytes.length);\n\n        return encapsulation;\n\n    }\n\n    private SharedSecretKey validateDecapsulation(\n            FIPS203 fips203MlKem,\n            DecapsulationKey decapsulationKey,\n            CipherText cipherText\n    ) {\n\n        // Retrieve the parameter set\n        ParameterSet parameterSet = fips203MlKem.getParameterSet();\n\n        // Decapsulate the shared secret\n        SharedSecretKey sharedSecretKey = fips203MlKem.decapsulate(decapsulationKey, cipherText);\n        assertNotNull(sharedSecretKey);\n\n        // Validate the bytes\n        byte[] sharedSecretKeyBytes = sharedSecretKey.getBytes();\n        assertNotNull(sharedSecretKeyBytes);\n        assertEquals(parameterSet.getSharedSecretKeyLength(), sharedSecretKeyBytes.length);\n\n        return sharedSecretKey;\n\n    }\n\n    private void validateAES256CipherCompatability(byte[] sharedSecretKey) throws Exception {\n\n        // Define a plaintext message\n        String message = \"Validation of Mimiclone FIPS203 Implementation\";\n\n        // Build a Java SecretKey from the bytes\n        SecretKey secretKey = new SecretKeySpec(sharedSecretKey, \"AES\");\n\n        // Create initialization vector\n        byte[] iv = new byte[GCM_IV_LENGTH];\n        SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM).nextBytes(iv);\n\n        // Get an instance of the AES cipher\n        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);\n        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);\n\n        // Encrypt the value\n        cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);\n        byte[] cipherText = cipher.doFinal(message.getBytes());\n        byte[] encryptedData = new byte[iv.length + cipherText.length];\n        System.arraycopy(iv, 0, encryptedData, 0, iv.length);\n        System.arraycopy(cipherText, 0, encryptedData, iv.length, cipherText.length);\n\n        // Print out the results for visual inspection\n        String base64EncryptedMessage = Base64.getEncoder().encodeToString(encryptedData);\n        System.out.println(\"Base64 Encrypted Message: \" + base64EncryptedMessage);\n\n        // Decrypt the value\n        cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);\n\n        byte[] decodedData = Base64.getDecoder().decode(base64EncryptedMessage);\n        byte[] ivDecode = new byte[GCM_IV_LENGTH];\n        System.arraycopy(decodedData, 0, ivDecode, 0, ivDecode.length);\n\n        Cipher decipher = Cipher.getInstance(CIPHER_ALGORITHM);\n        GCMParameterSpec despec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, ivDecode);\n        decipher.init(Cipher.DECRYPT_MODE, secretKey, despec);\n        byte[] decrypted = cipher.doFinal(decodedData, GCM_IV_LENGTH, decodedData.length - GCM_IV_LENGTH);\n\n        String restoredMessage = new String(decrypted);\n        assertEquals(message, restoredMessage);\n        assertArrayEquals(iv, ivDecode);\n        assertArrayEquals(encryptedData, decodedData);\n\n    }\n\n    @Test\n    public void testMLKEM512Interface() {\n\n        // Validate the Parameter Set\n        ParameterSet parameterSet = ParameterSet.ML_KEM_512;\n        FIPS203 fips203MlKem512 = MimicloneFIPS203.create(parameterSet);\n        assertEquals(parameterSet, fips203MlKem512.getParameterSet());\n\n        // Validate Key Pair Generation\n        KeyPair keyPair = validateKeyPairGeneration(fips203MlKem512);\n\n        // Validate Encapsulation\n        Encapsulation encapsulation = validateEncapsulation(\n                fips203MlKem512,\n                keyPair.getEncapsulationKey()\n        );\n\n        // Validate Decapsulation\n        SharedSecretKey sharedSecretKey = validateDecapsulation(\n                fips203MlKem512,\n                keyPair.getDecapsulationKey(),\n                encapsulation.getCipherText()\n        );\n\n        // Validate shared secret was unchanged at the byte level\n        assertArrayEquals(encapsulation.getSharedSecretKey().getBytes(), sharedSecretKey.getBytes());\n\n        try {\n            validateAES256CipherCompatability(sharedSecretKey.getBytes());\n        } catch (Exception e) {\n            fail(e.getMessage());\n        }\n\n    }\n\n    @Test\n    public void testMLKEM768Interface() {\n\n        // Validate the Parameter Set\n        ParameterSet parameterSet = ParameterSet.ML_KEM_768;\n        FIPS203 fips203MlKem768 = MimicloneFIPS203.create(parameterSet);\n        assertEquals(parameterSet, fips203MlKem768.getParameterSet());\n\n        // Validate Key Pair Generation\n        KeyPair keyPair = validateKeyPairGeneration(fips203MlKem768);\n\n        // Validate Encapsulation\n        Encapsulation encapsulation = validateEncapsulation(fips203MlKem768, keyPair.getEncapsulationKey());\n\n        // Validate Decapsulation\n        SharedSecretKey sharedSecretKey = validateDecapsulation(\n                fips203MlKem768,\n                keyPair.getDecapsulationKey(),\n                encapsulation.getCipherText()\n        );\n\n        // Validate shared secret was unchanged at the byte level\n        assertArrayEquals(encapsulation.getSharedSecretKey().getBytes(), sharedSecretKey.getBytes());\n\n        try {\n            validateAES256CipherCompatability(sharedSecretKey.getBytes());\n        } catch (Exception e) {\n            fail(e.getMessage());\n        }\n\n    }\n\n    @Test\n    public void testMLKEM1024Interface() {\n\n        // Validate the Parameter Set\n        ParameterSet parameterSet = ParameterSet.ML_KEM_1024;\n        FIPS203 fips203MlKem1024 = MimicloneFIPS203.create(parameterSet);\n        assertEquals(parameterSet, fips203MlKem1024.getParameterSet());\n\n        // Validate Key Pair Generation\n        KeyPair keyPair = validateKeyPairGeneration(fips203MlKem1024);\n\n        // Base64 Encode\n        Base64.Encoder encoder = Base64.getEncoder();\n        String encodedEncapsKey = encoder.encodeToString(keyPair.getEncapsulationKey().getBytes());\n        String encodedDecapsKey = encoder.encodeToString(keyPair.getDecapsulationKey().getBytes());\n        System.out.println(\"Base64 Encoded EncapsKey Length: \" + encodedEncapsKey.length());\n        System.out.println(\"Base64 Encoded DecapsKey Length: \" + encodedDecapsKey.length());\n\n        // Validate Encapsulation\n        Encapsulation encapsulation = validateEncapsulation(fips203MlKem1024, keyPair.getEncapsulationKey());\n\n        // Validate Decapsulation\n        SharedSecretKey sharedSecretKey = validateDecapsulation(\n                fips203MlKem1024,\n                keyPair.getDecapsulationKey(),\n                encapsulation.getCipherText()\n        );\n\n        String encodedSharedSecretKey = encoder.encodeToString(sharedSecretKey.getBytes());\n        System.out.println(\"Base64 Encoded SharedSecretKey Length: \" + encodedSharedSecretKey.length());\n\n        // Validate shared secret was unchanged at the byte level\n        assertArrayEquals(encapsulation.getSharedSecretKey().getBytes(), sharedSecretKey.getBytes());\n\n        try {\n            validateAES256CipherCompatability(sharedSecretKey.getBytes());\n        } catch (Exception e) {\n            fail(e.getMessage());\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/ParameterSetTests.java",
    "content": "package peergos.server.tests.fips203;\n\nimport org.junit.Test;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\npublic class ParameterSetTests {\n\n    @Test\n    public void testForHiddenParamSets() {\n        for (ParameterSet params : ParameterSet.values()) {\n            switch (params) {\n                case ML_KEM_512, ML_KEM_768, ML_KEM_1024 -> {\n                    // These are expected\n                }\n                default -> throw new IllegalStateException(\"Unexpected value: \" + params);\n            }\n        }\n    }\n\n    @Test\n    public void testMLKEM512ParameterSet() {\n\n        ParameterSet params = ParameterSet.ML_KEM_512;\n\n        assertNotNull(params);\n        assertEquals(\"ML-KEM-512\", params.getName());\n        assertEquals(256, params.getN());\n        assertEquals(3329, params.getQ());\n        assertEquals(2, params.getK());\n        assertEquals(3, params.getEta1());\n        assertEquals(2, params.getEta2());\n        assertEquals(10, params.getDu());\n        assertEquals(4, params.getDv());\n        assertEquals(128, params.getMinSecurityStrength());\n        assertEquals(800, params.getEncapsulationKeyLength());\n        assertEquals(1632, params.getDecapsulationKeyLength());\n        assertEquals(768, params.getCiphertextLength());\n        assertEquals(32, params.getSharedSecretKeyLength());\n\n    }\n\n    @Test\n    public void testMLKEM768ParameterSet() {\n\n        ParameterSet params = ParameterSet.ML_KEM_768;\n\n        assertNotNull(params);\n        assertEquals(\"ML-KEM-768\", params.getName());\n        assertEquals(256, params.getN());\n        assertEquals(3329, params.getQ());\n        assertEquals(3, params.getK());\n        assertEquals(2, params.getEta1());\n        assertEquals(2, params.getEta2());\n        assertEquals(10, params.getDu());\n        assertEquals(4, params.getDv());\n        assertEquals(192, params.getMinSecurityStrength());\n        assertEquals(1184, params.getEncapsulationKeyLength());\n        assertEquals(2400, params.getDecapsulationKeyLength());\n        assertEquals(1088, params.getCiphertextLength());\n        assertEquals(32, params.getSharedSecretKeyLength());\n\n    }\n\n    @Test\n    public void testMLKEM1024ParameterSet() {\n\n        ParameterSet params = ParameterSet.ML_KEM_1024;\n\n        assertNotNull(params);\n        assertEquals(\"ML-KEM-1024\", params.getName());\n        assertEquals(256, params.getN());\n        assertEquals(3329, params.getQ());\n        assertEquals(4, params.getK());\n        assertEquals(2, params.getEta1());\n        assertEquals(2, params.getEta2());\n        assertEquals(11, params.getDu());\n        assertEquals(5, params.getDv());\n        assertEquals(256, params.getMinSecurityStrength());\n        assertEquals(1568, params.getEncapsulationKeyLength());\n        assertEquals(3168, params.getDecapsulationKeyLength());\n        assertEquals(1568, params.getCiphertextLength());\n        assertEquals(32, params.getSharedSecretKeyLength());\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/decaps/mlkem/MLKEMDecapsulatorTests.java",
    "content": "package peergos.server.tests.fips203.decaps.mlkem;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.Before;\nimport org.junit.Test;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.Decapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.decaps.mlkem.MLKEMDecapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.DecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.SharedSecretKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem.MLKEMDecapsulationKey;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.CipherText;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.message.MLKEMCipherText;\nimport peergos.server.tests.fips203.harness.TestCase;\nimport peergos.server.tests.fips203.harness.TestGroup;\nimport peergos.server.tests.fips203.harness.TestPrompt;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.HexFormat;\nimport java.util.Objects;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\npublic class MLKEMDecapsulatorTests {\n\n    private TestPrompt prompt;\n\n    private TestPrompt loadTestPrompt(String fromResource) throws IOException {\n\n        // Create an ObjectMapper instance\n        var objectMapper = new ObjectMapper();\n\n        // Load the JSON test prompts\n        try (InputStream inputStream = MLKEMDecapsulatorTests.class.getResourceAsStream(fromResource)) {\n            if (inputStream == null) {\n                throw new IOException(\"Could not find \" + fromResource);\n            }\n\n            // Deserialize JSON into POJO\n            return objectMapper.readValue(inputStream, TestPrompt.class);\n        }\n    }\n\n    @Before\n    public void setUpTest() throws IOException {\n\n        prompt = loadTestPrompt(\"internalProjection.json\");\n\n    }\n\n    private void execTestCase(ParameterSet params, TestCase testCase) {\n\n        // Create keygen under test\n        Decapsulator mlKemDecapsulator = MLKEMDecapsulator.create(params);\n\n        // Print header\n        System.out.printf(\"%n[Test Case %d] using %s Parameter Set:%n\", testCase.getTcId(), params.getName());\n\n        // Input Bytes\n        byte[] inputDK = HexFormat.of().parseHex((String) testCase.getValues().get(\"dk\"));\n        byte[] inputC = HexFormat.of().parseHex((String) testCase.getValues().get(\"c\"));\n\n        // Output Bytes\n        byte[] expectedK = HexFormat.of().parseHex((String) testCase.getValues().get(\"k\"));\n\n        // Wrap raw inputs\n        DecapsulationKey decapsulationKey = MLKEMDecapsulationKey.create(inputDK);\n        CipherText cipherText = MLKEMCipherText.create(inputC);\n\n        // Generate the encapsulation\n        SharedSecretKey sharedSecretKey = mlKemDecapsulator.decapsulate(decapsulationKey, cipherText);\n\n        assertNotNull(sharedSecretKey);\n        assertNotNull(sharedSecretKey.getBytes());\n\n        // Extract the shared secret\n        byte[] sharedSecret = sharedSecretKey.getBytes();\n\n        // Verify it is the expected length\n        assertEquals(params.getSharedSecretKeyLength(), sharedSecret.length);\n\n        // Iterate through each byte and validate they are the same\n        System.out.printf(\" -- Shared Secret Key%n\");\n        System.out.printf(\"   --> Expect: %s%n\", HexFormat.of().formatHex(expectedK));\n        System.out.printf(\"   --> Actual: %s%n\", HexFormat.of().formatHex(sharedSecret));\n        for (int i = 0; i < params.getSharedSecretKeyLength(); i++) {\n            assertEquals(expectedK[i], sharedSecret[i]);\n        }\n    }\n\n    @Test\n    public void mlKem512DecapsTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_512;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())\n                && Objects.equals(testGroup.getFunction(), \"decapsulation\")\n            ) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase : testGroup.getTests()) {\n\n                    // Load shared ek and dk from group level\n                    testCase.getValues().put(\"ek\", testGroup.getEk());\n                    testCase.getValues().put(\"dk\", testGroup.getDk());\n\n                    // Execute test case\n                    execTestCase(params, testCase);\n\n                }\n            }\n        }\n\n    }\n\n    @Test\n    public void mlKem768DecapsTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_768;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())\n                    && Objects.equals(testGroup.getFunction(), \"decapsulation\")\n            ) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase : testGroup.getTests()) {\n\n                    // Load shared ek and dk from group level\n                    testCase.getValues().put(\"ek\", testGroup.getEk());\n                    testCase.getValues().put(\"dk\", testGroup.getDk());\n\n                    // Execute test case\n                    execTestCase(params, testCase);\n\n                }\n            }\n        }\n\n    }\n\n    @Test\n    public void mlKem1024DecapsTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_1024;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())\n                    && Objects.equals(testGroup.getFunction(), \"decapsulation\")\n            ) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase : testGroup.getTests()) {\n\n                    // Load shared ek and dk from group level\n                    testCase.getValues().put(\"ek\", testGroup.getEk());\n                    testCase.getValues().put(\"dk\", testGroup.getDk());\n\n                    // Execute test case\n                    execTestCase(params, testCase);\n\n                }\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/decaps/mlkem/internalProjection.json",
    "content": "{\n  \"vsId\": 42,\n  \"algorithm\": \"ML-KEM\",\n  \"mode\": \"encapDecap\",\n  \"revision\": \"FIPS203\",\n  \"isSample\": true,\n  \"testGroups\": [\n    {\n      \"tgId\": 1,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-512\",\n      \"function\": \"encapsulation\",\n      \"tests\": [\n        {\n          \"tcId\": 1,\n          \"deferred\": false,\n          \"ek\": \"DD1924935AA8E617AF18B5A065AC45727767EE897CF4F9442B2ACE30C0237B307D3E76BF8EEB78ADDC4AACD16463D8602FD5487B63C88BB66027F37D0D614D6F9C24603C42947664AC4398C6C52383469B4F9777E5EC7206210F3E5A796BF45C53268E25F39AC261AF3BFA2EE755BEB8B67AB3AC8DF6C629C1176E9E3B965E9369F9B3B92AD7C20955641D99526FE7B9FE8C850820275CD964849250090733CE124ECF316624374BD18B7C358C06E9C136EE1259A9245ABC55B964D689F5A08292D28265658EBB40CBFE488A2228275590AB9F32A34109709C1C291D4A23337274C7A5A5991C7A87B81C974AB18CE77859E4995E7C14F0371748B7712FB52C5966CD63063C4F3B81B47C45DDE83FB3A2724029B10B3230214C04FA0577FC29AC9086AE18C53B3ED44E507412FCA04B4F538A51588EC1F1029D152D9AE7735F76A077AA9484380AED9189E5912487FCC5B7C7012D9223DD967EECDAC3008A8931B648243537F548C171698C5B381D846A72E5C92D4226C5A8909884F1C4A3404C1720A5279414D7F27B2B982652B6740219C56D217780D7A5E5BA59836349F726881DEA18EF75C0772A8B922766953718CACC14CCBACB5FC412A2D0BE521817645AB2BF6A4785E92BC94CAF477A967876796C0A5190315AC0885671A4C749564C3B2C7AED9064EBA299EF214BA2F40493667C8BD032AEC5621711B41A3852C5C2BAB4A349CE4B7F085A812BBBC820B81BEFE63A05B8BCDFE9C2A70A8B1ACA9BF9816481907FF4432461111287303F0BD817C05726BFA18A2E24C7724921028032F622BD960A317D83B356B57F4A8004499CBC73C97D1EB7745972631C0561C1A3AB6EF91BD363280A10545DA693E6D58AED6845E7CC5F0D08CA7905052C77366D1972CCFCC1A27610CB543665AA798E20940128B9567A7EDB7A900407C70D359438435E13961608D552A94C5CDA7859220509B483C5C52A210E9C812BC0C2328CA00E789A56B2606B90292E3543DACAA2431841D61A22CA90C1CCF0B5B4E0A6F640536D1A26AB5B8D2151327928CE02904CF1D15E32788A95F62D3C270B6FA1508F97B9155A2726D80A1AFA3C5387A276A4D031A08ABF4F2E74F1A0BB8A0FD3CB\",\n          \"dk\": \"A5E26E1B2360203944ACFC2D7C376780E55B5A5CA38674919437C794F54B8217BB0629C84C692EF7827EED864D0C508990CA4553F16F4720CB75368C1B8CA9DBC175F51BBEBAA456F36611A2364775D248C0F4C40B342608F7370A983CF75C915570248E367375B665D9357CE4A8553E659BE4A60CA68B58724689C23B74D34C9E78E168E7CB0DF84641E41B6E6807BE6CF4CF8F338525D57090B08AAB5721216395C49147F6E817B117B129987317A7A5FF15A279F86AF93C6A4995954000C3D4D8B0A07499A95A5C98D0B8303702DFD801B67C37268904C96ABC462750384BAEA767A5AD30C5D452682B3AC864D1671DB38F1CF2CE6E6C901D39C144DA3D93B863F95717C3C585AB876D3EF2B10AFA0B8142164C3C27FB179A923A3F924B15CEBB22EC762907324F1CD4C47573CA1F103CA88844F3B86687280B3B5BB569B1C118B63565055834F39F320CB88C05C199E29684D7802CF45D8DA342CC444D91A84D6D9461C873B66F9785488723A167412019077C9A7FCF4C7BD028BE3007B3483026A442A095124C9607C950443FD69993615697E9AC1CB9D380437B85EB300CE4D9B5A5BC2132660DA3527031A1057A565F2C76775565B0088637707410F2E955355425EFE496113149CF52C901BCCC48864C8AA4262367213602B63AA1A8BED77826C0C476152AB3464A20C9CD73F17A1D019466F2AE37859E6E5A8BB8862A480C1B12D6797B79663ED2333F188F34E6CF6EC87E43979F88787CE35877DDF0B689547BF5BA9EEBB2659D76354EBC39EE83975310ACA4F8867FF290793CC08BF29E60A97C28A71EA3084FE27845AB3664E80592412043B03056FDD5744BD74C9584094C2B75C689ACA8E4B3D3F91994E4722B9B331399310975275A0065935B6CDF5A6A8216188452394238BC82736488A84A0C96C580A81C69032AD5E96F4C3061DF5AB246C258CBA0B68A32916BFC6686730B3FF0944A070F535A113FC349CDDB0B67B40DEBFB5215167090F9891365BB3D87639FDA05843A079A430FD5892F57AC4510450DEC00B7905A3A14442231919F9ED4A76B2B159A6CCC3685B3DD1924935AA8E617AF18B5A065AC45727767EE897CF4F9442B2ACE30C0237B307D3E76BF8EEB78ADDC4AACD16463D8602FD5487B63C88BB66027F37D0D614D6F9C24603C42947664AC4398C6C52383469B4F9777E5EC7206210F3E5A796BF45C53268E25F39AC261AF3BFA2EE755BEB8B67AB3AC8DF6C629C1176E9E3B965E9369F9B3B92AD7C20955641D99526FE7B9FE8C850820275CD964849250090733CE124ECF316624374BD18B7C358C06E9C136EE1259A9245ABC55B964D689F5A08292D28265658EBB40CBFE488A2228275590AB9F32A34109709C1C291D4A23337274C7A5A5991C7A87B81C974AB18CE77859E4995E7C14F0371748B7712FB52C5966CD63063C4F3B81B47C45DDE83FB3A2724029B10B3230214C04FA0577FC29AC9086AE18C53B3ED44E507412FCA04B4F538A51588EC1F1029D152D9AE7735F76A077AA9484380AED9189E5912487FCC5B7C7012D9223DD967EECDAC3008A8931B648243537F548C171698C5B381D846A72E5C92D4226C5A8909884F1C4A3404C1720A5279414D7F27B2B982652B6740219C56D217780D7A5E5BA59836349F726881DEA18EF75C0772A8B922766953718CACC14CCBACB5FC412A2D0BE521817645AB2BF6A4785E92BC94CAF477A967876796C0A5190315AC0885671A4C749564C3B2C7AED9064EBA299EF214BA2F40493667C8BD032AEC5621711B41A3852C5C2BAB4A349CE4B7F085A812BBBC820B81BEFE63A05B8BCDFE9C2A70A8B1ACA9BF9816481907FF4432461111287303F0BD817C05726BFA18A2E24C7724921028032F622BD960A317D83B356B57F4A8004499CBC73C97D1EB7745972631C0561C1A3AB6EF91BD363280A10545DA693E6D58AED6845E7CC5F0D08CA7905052C77366D1972CCFCC1A27610CB543665AA798E20940128B9567A7EDB7A900407C70D359438435E13961608D552A94C5CDA7859220509B483C5C52A210E9C812BC0C2328CA00E789A56B2606B90292E3543DACAA2431841D61A22CA90C1CCF0B5B4E0A6F640536D1A26AB5B8D2151327928CE02904CF1D15E32788A95F62D3C270B6FA1508F97B9155A2726D80A1AFA3C5387A276A4D031A08ABF4F2E74F1A0BB8A0FD3CB0AC923A76D541CA65FDEC9C788A407326C7DB508119F617F43B6E8A6F48A398702E051C20C31DE77A1BA6777829F5539C886E3E14DED294D56AE5E88AC06AB09\",\n          \"c\": \"19C592505907C24C5FA2EBFA932D2CBB48F3E4340A28F7EBA5D068FCACABEDF77784E2B24D7961775F0BF1A997AE8BA9FC4311BE63716779C2B788F812CBB78C74E7517E22E910EFF5F38D44469C50DE1675AE198FD6A289AE7E6C30A9D4351B3D1F4C36EFF9C68DA91C40B82DC9B2799A33A26B60A4E70D7101862779469F3A9DAEC8E3E8F8C6A16BF092FBA5866186B8D208FDEB274AC1F829659DC2BE4AC4F306CB5584BAD1936A92C9B76819234281BB395841C25756086EA564CA3E227E3D9F1052C0766D2EB79A47C150721E0DEA7C0069D551B264801B7727ECAF82EECB99A876FDA090BF6C3FC6B109F1701485F03CE66274B8435B0A014CFB3E79CCED67057B5AE2AD7F5279EB714942E4C1CCFF7E85C0DB43E5D41289207363B444BB51BB8AB0371E70CBD55F0F3DAD403E105176E3E8A225D84AC8BEE38C821EE0F547431145DCB3139286ABB11794A43A3C1B5229E4BCFE959C78ADAEE2D5F2497B5D24BC21FA03A9A58C2455373EC89583E7E588D7FE67991EE93783ED4A6F9EEAE04E64E2E1E0E699F6DC9C5D39EF9278C985E7FDF2A764FFD1A0B95792AD681E930D76DF4EFE5D65DBBD0F1438481ED833AD4946AD1C69AD21DD7C86185774426F3FCF53B52AD4B40D228CE124072F592C7DAA057F17D790A5BD5B93834D58C08C88DC8F0EF488156425B744654EACA9D64858A4D6CEB478795194BFADB18DC0EA054F9771215AD3CB1FD031D7BE4598621926478D375A1845AA91D7C733F8F0E188C83896EDF83B8646C99E29C0DA2290E71C3D2E970720C97B5B7F950486033C6A2571DDF2BCCDABB2DFA5FCE4C3A1884606041D181C728794AE0E806ECB49AF16756A4CE73C87BD4234E60F05535FA5929FD5A34473266401F63BBD6B90E003472AC0CE88F1B666597279D056A632C8D6B790FD411767848A69E37A8A839BC766A02CA2F695EC63F056A4E2A114CACF9FD90D730C970DB387F6DE73395F701A1D953B2A89DD7EDAD439FC205A54A481E889B098D5255670F026B4A2BF02D2BDDE87C766B25FC5E0FD453757E756D18C8CD912F9A77F8E6BF0205374B462\",\n          \"k\": \"0BF323338D6F0A21D5514B673CD10B714CE6E36F35BCD1BF544196368EE51A13\",\n          \"m\": \"6FF02E1DC7FD911BEEE0C692C8BD100C3E5C48964D31DF92994218E80664A6CA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 2,\n          \"deferred\": false,\n          \"ek\": \"49469911485CB1C06A48A449F1A43B0456406243AF447A7CECD5467DF322A159AF32B6C59CB05D200CAC34DA66D8DBCFF8326FCCC08A77C9286F590F33C06AC36049B91442F18AC6C00C240E713D387C8BB2BA3780E6BBFE90A4B1D7B155360ED9ACBD63205BC3482B8953B3490427F28392B083A47BE5B18EF6AB51B9859FDB659B8424BA93A8F470014FCB6AB9E61FACC61311BC1BCB098469D9702FE54F8F931DE7B2B57543750A346367371F3724384261B569AB5D8C870A01822BB4E6C617F17DB6EB0D0989C5644459281828EF4AC11A119EF794530436CAA0E28B8CB5365E4854E1AB4BF87CA018AC6A8E62C36C97117014A569AB472DEAC7E7B1108BB8BDBD710AE8D033A1961917E171AAC6841B5A9C5D869E68974D79B8C70775955520CEC21B5EDB05B60FE230EC143BFCC15B550370E1D58E76D1B5BDB0747412B952131DB306A4DB2395CA69CB9912C0A1660517553C92A7210056C4B6F347CDF33C326A27155CA1A7BB809F8A4A46703537B6530E5987E02A9EDD83297A1218C68699E5A898F9487279D35BD01B57B57A30E85483194C68069BAB0FC4BA0F48880DD1089DD21ABCD07228D9B213C32EA184291A2B12E3E19B4E4B7CA51879C46AB93057AE748236265935F0B16CB456256419AFD82147C9247CE6A7CEE4F349AAA59DA7A4AEBB2B0ED225775CE085A9AABBF49B756CF833CAF22B39AB4D9AC4A26758430866C9D76C5EA68300AF90697FD304AD715C591798C7948FC1A954FFB40301A432B29726884266A3D8B8EED13F3A33A106349FF58075C9D67923A59E06322FC7258017179E7859ABB2A83234D81330B1297AE842AA1C0C8543972E5B3AEA4490D4B0CCC1F9681266A6A0F96DD955924EF25E39642556F18443497D2EAB77AFC01B1D2003F1981C92060743141458FC606BA85DE9DBB1BAC78D6ABBBC73BC7F4E0C6BCE13C8B49BA367E60929C61C34FC7BFF013D4743BF92A70B4683C724B06717237A76D58F81EA39A9F96F6D627162299BC82A355D06C766318BBFEA3386860D683AB75FA03A87B1B2C1D99A30EAAD78A79CEEB43C1621C54F79AEDF82C7F8CD5FB11F119FA35CCA71B4CCF3E1D83F07C3A66C3E82F2D84F640DE5\",\n          \"dk\": \"5639898D7A061A47880E01A1DD869A4393A58EC5811124A22AF2CAB3C61BE3E70492F22EACBA0B52E2C2F31914A466C647379957EAA4F16C9A10440BD25B6A6260626C76C2B5B7391A16897681535B1A279C38CD4661CC04044425297E47F6827BB8258C246894DB965EDCBD6C864D7ED91DC3A12FE0A86C6D2C40EB5812FAF8421F447DAD43BF2140AB9F473C1ED23EFEE952DC0ACB86C068CADCCCC32BB22117B6F8F9B6184BBEE13184B21BB78B87B33879147ED01BE9441D6D240019C2AF998C771A51B616983627354BF0EA1A4C73B8E2A092AB1772CA7CA194666DBD5C06A3464F3A55946C8B816147103AC24934A3488B95185414AB05FAA360C63FBB00A915271C50F25987E817CF849463B8C65CA76F71A83676D75436D27F2362294EB8C55017BAF3A77C57064586D0BC08AA4906E86DDD13830E2C21716258DB81592814681C823DC499752D62B414925B76B5A4BCA94EA30B2B20D489D887B63BFA72D9A327C121C635904800D68F3BF50A01E765E86894D802C1679964A0CC528CA558F960A5A9D8243CF01D94A99E68E4C2BDAB0576A1780ABC3BB8783E35F7441F395F7A1134C4016200E32602640D42252AFE3CBB3A624724066D5644BF93B8BE699B42B052591FC685D00A289AE76766C5A8A384034F5274E4B81ACC6A52EC0A33EDA0201FB600C702606D5806E95C2610C367426A02A7F1808B09B0119B2D71B7A1D9E93F9339309B941C67C855236761B7DCBDCD501AD736C15C0609C4141D1920651A631C39EBB1ABF4AFF51A56F3BC11BCAB098E960DFF115DB306121F19723A3CA5B9838349DC25781CC9804551C8B80FE9897216D72E1036CDB1C11D6BD710BB60C110507392B32DCB3A1ABCA4B67AD5A88DF76399E5134B8063CA068736E626C0CAC3655711D7004E19D56FF0C11F18417061E70C7F2B9A2DD920E297284EF21CF30BA29E1124C3945357C0AF8EF060D949613417A6B35305239BAC986B8E51468655744CD31BC9BAEA4E0BEA33CF665EDE79BAFEE8102A68AD5891AF29FC0409C00AEB904458FC73ED36460D191003A707EA81CBD3ABC5C2C24049469911485CB1C06A48A449F1A43B0456406243AF447A7CECD5467DF322A159AF32B6C59CB05D200CAC34DA66D8DBCFF8326FCCC08A77C9286F590F33C06AC36049B91442F18AC6C00C240E713D387C8BB2BA3780E6BBFE90A4B1D7B155360ED9ACBD63205BC3482B8953B3490427F28392B083A47BE5B18EF6AB51B9859FDB659B8424BA93A8F470014FCB6AB9E61FACC61311BC1BCB098469D9702FE54F8F931DE7B2B57543750A346367371F3724384261B569AB5D8C870A01822BB4E6C617F17DB6EB0D0989C5644459281828EF4AC11A119EF794530436CAA0E28B8CB5365E4854E1AB4BF87CA018AC6A8E62C36C97117014A569AB472DEAC7E7B1108BB8BDBD710AE8D033A1961917E171AAC6841B5A9C5D869E68974D79B8C70775955520CEC21B5EDB05B60FE230EC143BFCC15B550370E1D58E76D1B5BDB0747412B952131DB306A4DB2395CA69CB9912C0A1660517553C92A7210056C4B6F347CDF33C326A27155CA1A7BB809F8A4A46703537B6530E5987E02A9EDD83297A1218C68699E5A898F9487279D35BD01B57B57A30E85483194C68069BAB0FC4BA0F48880DD1089DD21ABCD07228D9B213C32EA184291A2B12E3E19B4E4B7CA51879C46AB93057AE748236265935F0B16CB456256419AFD82147C9247CE6A7CEE4F349AAA59DA7A4AEBB2B0ED225775CE085A9AABBF49B756CF833CAF22B39AB4D9AC4A26758430866C9D76C5EA68300AF90697FD304AD715C591798C7948FC1A954FFB40301A432B29726884266A3D8B8EED13F3A33A106349FF58075C9D67923A59E06322FC7258017179E7859ABB2A83234D81330B1297AE842AA1C0C8543972E5B3AEA4490D4B0CCC1F9681266A6A0F96DD955924EF25E39642556F18443497D2EAB77AFC01B1D2003F1981C92060743141458FC606BA85DE9DBB1BAC78D6ABBBC73BC7F4E0C6BCE13C8B49BA367E60929C61C34FC7BFF013D4743BF92A70B4683C724B06717237A76D58F81EA39A9F96F6D627162299BC82A355D06C766318BBFEA3386860D683AB75FA03A87B1B2C1D99A30EAAD78A79CEEB43C1621C54F79AEDF82C7F8CD5FB11F119FA35CCA71B4CCF3E1D83F07C3A66C3E82F2D84F640DE5B2F75D3486FE6CEBB15F8E0CB70ED8950970C944912A03717D9D168C7B589DB71AC2F3294D2ED2611E9CB1E07CD9684148F13E9ACEB931C8CDA7427873B44B37\",\n          \"c\": \"FEB020751BCADF864161AFEA7B63E63088517A5EADBC52F0833E6DE2E03C66EF3F71F92FB61B277A26D6C2D9E01B88F0738E1A7409CEBFC9D7230C69E02D3BC7F0403B01512F0E082CB9023C7174623478CDBD6CC5E6D65A09AFA63C8B2686234DAC6FAA19A82087F0847B40AB47ABDE90108A13F3AB3601B7EA70B766F1645E7B4428C4AF8CCC19B62C057C8EE9F41E77DC00E5FF5F4ADD0E8EDAD2CC9D6DB40015E5207E7CDAFC915B8FDB1FACD6415FE3E8EB4DACADD7742560C3E2D3EE0EFCD306FAC97A8FE45CFEBEF1AC1B2F5D5316A4EF9C7D3DB6582354680E8932079567D148473CCDFCF32FD7E6D7F3226EC30EF2792F819229B55C5CB8514A77CFA44F6E8531102D073E8CEC089B4268C86B759F043A0E9D8EC8D57EB5618C6D0ABD44D64E9CA25913588F6CB4CF7D0BC914625737140C0C7E559BD00B2C448886B983893FF7F18ADEA02E41A07117644D5A208928892B20685F7A8476C641B25C48EF63551712AB97B0FB759431B287DFD1488EA11CEF813240E2F4E1F5619084B5D7EFFDE09ECE072614BDF6A970168F8D6628FBD521F1431C9ADC46CE31281AB6D6E762E8A7779FDE0F5CFCC36AA677E1F032010F110FA6B460F7294545DE7BB28D6D5CEDA5D8832EFD3E32F9605A7BD43673A00EFD0CCAF63BDEB49C9AC1872B9D3C8CF7A9C81936F18667D15261DECB9A354144D7C3F2BE726FD0A83B1C27E8C6AD66B1AB77425541EBE321EED279526C79154FF27C1B5306414E60684ED42DB69BC2785A1CBE14CD0AEE879173CD3C94020210CAB4BABC531C058857522D9ADF25FC5C3F2FB0A0DB8BD15F23807AC84C7319EC29FABCA43C99F72354415ECA9844D06D299C9F4AE49E0BD8CA7623B8FC9D2DAFFF3F368E4BBA32489F3EC202A9799094469C674CC216750AE88F387A6C030D94BD8E870706D2D74A27A27CEC92CFF8CFD9E09BE6FEF40F807CACAB3628262C501ECAABCE9CBDDE8B3766813BBB8189CAE1AB38A3605F6354F95432E8EBB8B3A5768D2B816E2F7D22139C0135D0070CC41D40ACEECC4CA16636F08C404E3E5E6F6C7D8D652F6084F6477729\",\n          \"k\": \"9183CD7EF4AAF2F21E2E852771F524B10CB2BDB8C0BA1DD36EF48AB391DC7307\",\n          \"m\": \"4660985A5838041F2E50381CB4E7AC908BAC83CC1E074220C6705E3F5FBFC2EF\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 3,\n          \"deferred\": false,\n          \"ek\": \"C70876917C9AC2A2CF2110000910898E21804D940D6F761A525578E2B0399F1C0800B62C8720060F88746122C948446472E73AFE78A205857C6D32B6C01CA0EC742311840A36A8BD157B1C50A944B4C213FCF9558E3068D9714ACDBBC88A1538B59764C1F59C079C5BDA31BF55EB731BF69717A9CA63DC316636461CE2CC42201C359B88D044156DA30AAB932F902030BEEA6D58998FB2363C8223BE3DDC437AAB4D1BA27FCE058E701B10FD211E5862542C058F3DE33D5561B8BB3A83D6C55377862181150173D2135CA3C8084615668C38ABE8B72F193FDF96ACFA3470FAA53450017EFC36882C5AC722659D229225E01C5F976171E19172BCE5290073AA08276FFC85218F208BA3A1108B62545941C8991C4CE279C697D8CD06436134511D300C3A5FEB963D36CF29D6065E085C9663443342A8388763B16566D99C5C5DA4A451C2688E10936293948A57C4CE7B73BCBB698D9B71E2974638D5433E56A411E4089F966F603120D56A48DB653A5CD373D42A6AF3117D71AA4D26A1639D63A305B8BE1DE15C6F678519FA671A91483E46AEA26C0C59872CDAF57B3A6453CD660A0D9A0A562BA23678CDCDEAAD1CDA5820C65EC480347FF2872EA9574C4148C7D59980C584646409793190BC721CA6EA9BF029C695563B0598BCB357A1A221CEB3AB96F2269F45C24108E137D1BC34708B1B66352627340CCAE6CB22F41A75A0A55F58701DC4157A1316FF968CEE300FD55A37B5957F41220BF547933C67201D67B1AE278D9649BCAFC68F082B7D1C6016EF360D200CA0EBC76046DC5AAB171A0D544719E3411B587DCE380D467BC13368A25FA51A3E9B4EDC77561408B776F83A76B24B489310E3B608F1B40EB7395470F86C1D0622C1545065707462D89B364CAB2CEAC9501157F91BC52A2816E9598846B56C1F7B1A6D06195F92BD53931078E923C9D1552782325878832224B21A1C26562245B636943803ACBADC559D3BB6DEC54ED360239F74695ED3364DE562751892F2B2936F6895C166697A4A44D10A49C59C53DDE4B2631705440B957100AF4E1BBD47F2AADF22780AE0255D0AC18A87A28D282DD1C59C0C51D373C8F4A912400D5C87B499E0BE8CA991939F160B\",\n          \"dk\": \"AC789443C51AB5E60BFE7B56D8418AFB874C4C563C88C60B77A1A1516B01CCBC36445B90EAFA778CAA4EFD759982D2313EC061F1E259827214E415A583A911A68581E8B228DA8C30182B3157B50742F835121BB501F7B96947330867BABC615D1044BEFC0467D8B88E78FCC2089C9C8E563DD56BB08FDBB8A5451EDBDBB7ABCA20573B7AA8BA48BD5BCA558CC7B2E512A314CBD5D6A4CECBAEED533216007A7AD988B0D06A37C1C443F16B5B246C2F7C38BF68CDA1DA8098C6307C60658D6C596EF67759923FFAD16EBCC4912010785B7798AF299296653E2D865A12631C2BB49C417C19A655897CA6491CB6ABBCA64B6BFA5D811096D2F99773888153590068F8220534705C7C0E0B1C2538D31A7F0C70ABA02B50B9449D111792372B17C11F85AA67202017A2D07CDB6B25C1D97ECE11991E0C0374167E82419802BBB45F87899F262CBA0C5C74C993849B4CF412C858700B4BC9BBAC82907EA400118C59289CC07BE9CCB265B85B7B2294944F8E50465FAA496FF3865EE52CF6A07492231FBEC32064B30B5E6672616650983B730CC19F0C3BA4B354CB7A1B77A9AAC32D30712EE302CF131B6E38C40A9CC40C3C661848690710BC4DAB94D484B4D7EA7596C380F6C6AC023C01CD773260C95DB6A317F56C62CE0B62A0A59D5BFB99A77B6533C026F4FC25AB3221C643C44C75AB5F70A73A40487AAB651CB62D6E0B7AE24A2D736BBB26777417328078B6A16E151966A72DCEDC8E3C309F99A3989FC61722CB5EB6EB8D3C8993FA57B487D8B6D9C82D37D865FFC172D5478262C3AD8498593E2CC6E9D559F3356DA36CADD4560356351DC40466FB2B840048C4FC857F323126C5944C5C762E5835A90629C0155428B9F99472555FAAD44131E0193021C40DB0A9006543965B8CC1FC0B80092C69938DE4874EC92BB3207AA76CF32ABDF57848BCAFB6F242BF7905C782B38AE1B77DD2787FAA90112768544AB9A8354B9AD41304B15167747A1DE82B52B7CC55CC0D6A59140AC29FDA1654C628492AF47DBE02C8D809189B098D9F8C90C57A91CF853BDE416F6D09638AD2AFF2A0A7987C25C70876917C9AC2A2CF2110000910898E21804D940D6F761A525578E2B0399F1C0800B62C8720060F88746122C948446472E73AFE78A205857C6D32B6C01CA0EC742311840A36A8BD157B1C50A944B4C213FCF9558E3068D9714ACDBBC88A1538B59764C1F59C079C5BDA31BF55EB731BF69717A9CA63DC316636461CE2CC42201C359B88D044156DA30AAB932F902030BEEA6D58998FB2363C8223BE3DDC437AAB4D1BA27FCE058E701B10FD211E5862542C058F3DE33D5561B8BB3A83D6C55377862181150173D2135CA3C8084615668C38ABE8B72F193FDF96ACFA3470FAA53450017EFC36882C5AC722659D229225E01C5F976171E19172BCE5290073AA08276FFC85218F208BA3A1108B62545941C8991C4CE279C697D8CD06436134511D300C3A5FEB963D36CF29D6065E085C9663443342A8388763B16566D99C5C5DA4A451C2688E10936293948A57C4CE7B73BCBB698D9B71E2974638D5433E56A411E4089F966F603120D56A48DB653A5CD373D42A6AF3117D71AA4D26A1639D63A305B8BE1DE15C6F678519FA671A91483E46AEA26C0C59872CDAF57B3A6453CD660A0D9A0A562BA23678CDCDEAAD1CDA5820C65EC480347FF2872EA9574C4148C7D59980C584646409793190BC721CA6EA9BF029C695563B0598BCB357A1A221CEB3AB96F2269F45C24108E137D1BC34708B1B66352627340CCAE6CB22F41A75A0A55F58701DC4157A1316FF968CEE300FD55A37B5957F41220BF547933C67201D67B1AE278D9649BCAFC68F082B7D1C6016EF360D200CA0EBC76046DC5AAB171A0D544719E3411B587DCE380D467BC13368A25FA51A3E9B4EDC77561408B776F83A76B24B489310E3B608F1B40EB7395470F86C1D0622C1545065707462D89B364CAB2CEAC9501157F91BC52A2816E9598846B56C1F7B1A6D06195F92BD53931078E923C9D1552782325878832224B21A1C26562245B636943803ACBADC559D3BB6DEC54ED360239F74695ED3364DE562751892F2B2936F6895C166697A4A44D10A49C59C53DDE4B2631705440B957100AF4E1BBD47F2AADF22780AE0255D0AC18A87A28D282DD1C59C0C51D373C8F4A912400D5C87B499E0BE8CA991939F160BBD4FAFE5DBA7B6E6DEE2892A0D23D7FF97262CF3BE7D86976521FD0E33969DD804179FA8AF901D178A41C1E9F51DBADF03A4393E002689723B0C5963C5EE326E\",\n          \"c\": \"7E132EADB0E35C2A8E0916939F5BD7EA42DF683EE4E64D0512D75B2882FB6372A5233B6BA26A9A1C418171EC4C3EF24E98FD578C87396DB28E35C980A6B3DEC1F772086EDD53126C46A82C6D4F51C1B57F49CF1487D188336CBF99F740EDF5A01D30729FA486B551E0B236D5E08C56C80FFDB2D1CA10040C6435A1E0711E2ED6FBC1A48AEEB6A5D8A59D036D9B702EB3A884476C781BF2996F9BF27C79648552F2A150BDAFBF8E134D44B4B4558EFD9D92F2289CA975E65B601BE687B31D6F028A51B16A5C83B0C20DF3F279C9EFCB66060330854355C02405CD9690BA6F8942918CA5F7C37CE3BA8BBF1F285A4ECBEEEF53BE3366D4AE61377BA5A1730CC82444753A11931790D1228E8CB87F7BC9CA71E6E871351EB81A332D33EE06E83048F84169BE950991863814917D56F97F8A0896B8D8A4725CA965C726BD3C1EF3892175B19D8B9CF83AB82CF55B02558BD255A35085E88D3E3B6185537D8559A6675F8773EC7775FF6518E281701D50A450009B845327D2FA5FEF85FD5B5F6F3BC3895742FF16E483CAF3356B4AFA6ADE61C96D09AE63AC9715B4C0D4AD64072EFEE7B70D7BE0E3AD7D84CAF9A8439AABBFCEF44B624DAB8ED6A4AA57E2AADB22AE7D42DEF201862DADEECBF0BA88EE5D4BB723EC35F99A8556E67DD592A04920B8B228D0460ECDD389EACD55BE9F77EFE906176C5C9A1C3D3B4788410CB4E7035260FA2A2E6E3906E5BA6F3C4CF5AD16098392A0F3EAB85FBC59673BB49B3231229963647402533FE2A8E6EE7B85110300B7E20955974347CE5547521032BD57D8D7A1B202220142FE22676239F8C84BF4ECC2A9A18482EFEF216A232E7ED7583E090DB56F61AAE17B6755506D366ACC6516CCB54537D45B10324A46282E4CD881AC40B45BECDC5238A9605BC722FDA5332E2596049AC12DF07FD8011AD170E12CD8B05E261BC4DAC52D7B0BF42B88267D0AA310F480D67022C740666F9101701ED5319C1DBDEF15F6AAC5E0173CC5436B28848443C7099324F78FB2363CE5BA841DB75C3987C3840659FCE7C675BC5047F9F5C6BBC0057F28B32DFAF9A09E969A\",\n          \"k\": \"941DA82106D0DB42FFCB4EEDBC4123DF57BF0DF2A4E9119969872904FDC0B9B5\",\n          \"m\": \"0D643FF311D83CEDCB3A95BA0F76216A49BCA389A225396F708EC9A51BF18517\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 4,\n          \"deferred\": false,\n          \"ek\": \"E868A60B3888165A293FDBAB8B75C008E2695AE85A1C839CB08290AE87200868623049839674CF8E87C8D20C5C369B17ADDB344E263045017370D6885DD25785A0C430250795FC4AA1AB215109692B44C530005054020B32D73DED4B06AC32CC1AC47571B7244F2A05AD063DB57BB285C03C465A5FECB4489F82908EC72908B942153C318509C1508556B15B3BC7A671F6A69EDE283C5B87C2383324C3183B645086261B7CC4B53CEC6C5694106142A12A108A29D749AD47B93583020B3CFC328CCCA1611338A8576BA98659D9181607C42D7EE9B0C3174309FA7B3EDBC6BE422515C203B208C9AA1B3C387909EF511FF2A47AB03A747E0A83326B894168136BD6361D170B8D4087C99552D992929D832D58F26BF3D707B6D302F741456DE30FA6509A1314773FFC079AC94AAA10376ADBB70D857E556272F2A32E68485F84E28AD9326F656A57CE234AD2826298BA4C1CD4960BE4523126A405BC3E772121B1F7AB256239E9761B0EDB5C8D746B4D3323134742D69182091170E08C6E0D3B1862E202F332C7A70A25A3E2482113A5A2C257BDC6AF93C981E5ACC95118CCBBDB5B6244C1D060C9288BB2BB1A6791F748C4182B4AE6A68E060E2028690DEACB1E887A9213C693D57971FB6EDF6A95BA3821D445479B142737887BAD938B944376EAF87B15B210CF273B0BE07707FC52475C01B4485893710D84F5132D025E82630F8415330D257E1642ABFD8CC878E8B199C44107672FBDB8330F3C8CE8154E4E8036E86CCDBCE4B693B0C513132B8B740FFDFC51341BCFFA0BC204D63D6696B2271BABA633A572143E74382346C701F92C099B4371D18269A32769889C61AD428FBC110D450ACC95892791656A7DB39868E03FE6F97420910AE8476D43851AEC9A03734AB4E8E4087DDB9626F36ABF7480705C0E81C3887A64794CCB29587593BBB435C2377477B7A0E6A3C9050A84A99936B94C09DE28B018E73CA7C48B083C5BC1E73A24E20D16771711A863D1423E884808E0C8B6E250986AD4BF1B8592F8EB1549DACB4117747F2994D1808B65DB2AA5B05EBD7A0E614888EF86494B93822876C351BA487DB0D0C6F33C353B368BDEA3BC149CB74DAFCBEE209C50B88354\",\n          \"dk\": \"F9D66260ABC38576A4B54864B8CB3C1380365EEB412D896570BB243C29359D36366CE050D839462A5CCFD56BC4B28224715C39E0848C72094B5DE99C63252F20BB07D1AC305F34845A914684E0B03CEB6F0DDCCBC118CE06E7708CDC1E7F660824F7AA21487B7CFC1D6B788428FAB2F5062223CC6EC6285B70D7AAFD50832970525B7406F59C2A974AC1A6C0C813B4A71EA45371B1A7E2168F3E972F9EBBA639236E9947482ED75891FC834FC8CB44C51AC272A8CA2A0058F57A05C22D9166028D063BF5EB3576467368024C739514DB517D4E1B1CE2AAB8004D4987A96444B4B14CE633A4793D2B40071A77373D630A8CFAA884B074AFA03152C882E9C14806E65797340F0ED100F9D161ED46581B7C750926C009861C3CC89894258E9CF2032283708D1B83AFFBB7A6B890D9F9C725A1CF2790ACB1424C5BAA05C0B3CDBDFC78808695D6531877254B85819370F971CD196080B70AA1AAA8C691A8BA40C44FB60A898543979B5C29C3A40C0117C0162BC06A816626152B472F57178050E1BE9D66CB6C30584912C39FC45D60F26689696836FC60521298F4529888286636C5435A36BF6F2841D058AC19AAB8789924778B473A567189B4711F655BF5D25871D548AF92CC5392C30D2B50EB3C3381033D00C48B89C58E8EC81FA9942A2FB11A79549487F602FD8A85D6439C54C34493966AC4B3C04BB670A2AC537DE04E52218DE78ABF019085E470949565C111106F0C321620A65133439180783BC7261D0F47024423C43A2038544A2C72D480AFD1C219EAC504AB4003D11FAFDA4173B4A4E7E038AA659A3309AD9D1B2489C270F88CA659547F49F45E56FA149603B54A1611061762DB1B4D8AF629D1DC1396069BF1B9324195B5DA92ADE118588A664389BABDC35B92E5919DA2BA30254521DA834DD728749F9814D6777EB4A24AB4BC7BE5910EB39331CF3461D7B56392F99AFCD2B25E4A2948925C7BB744FB05C3BACAA53500B25109B4881C5BCC28661BA6626D648F3396450692503624737D5496C68BB7A83801534A9F86285EAB94B80536292F0A2FFD59647428CB15DA481DCB2DE868A60B3888165A293FDBAB8B75C008E2695AE85A1C839CB08290AE87200868623049839674CF8E87C8D20C5C369B17ADDB344E263045017370D6885DD25785A0C430250795FC4AA1AB215109692B44C530005054020B32D73DED4B06AC32CC1AC47571B7244F2A05AD063DB57BB285C03C465A5FECB4489F82908EC72908B942153C318509C1508556B15B3BC7A671F6A69EDE283C5B87C2383324C3183B645086261B7CC4B53CEC6C5694106142A12A108A29D749AD47B93583020B3CFC328CCCA1611338A8576BA98659D9181607C42D7EE9B0C3174309FA7B3EDBC6BE422515C203B208C9AA1B3C387909EF511FF2A47AB03A747E0A83326B894168136BD6361D170B8D4087C99552D992929D832D58F26BF3D707B6D302F741456DE30FA6509A1314773FFC079AC94AAA10376ADBB70D857E556272F2A32E68485F84E28AD9326F656A57CE234AD2826298BA4C1CD4960BE4523126A405BC3E772121B1F7AB256239E9761B0EDB5C8D746B4D3323134742D69182091170E08C6E0D3B1862E202F332C7A70A25A3E2482113A5A2C257BDC6AF93C981E5ACC95118CCBBDB5B6244C1D060C9288BB2BB1A6791F748C4182B4AE6A68E060E2028690DEACB1E887A9213C693D57971FB6EDF6A95BA3821D445479B142737887BAD938B944376EAF87B15B210CF273B0BE07707FC52475C01B4485893710D84F5132D025E82630F8415330D257E1642ABFD8CC878E8B199C44107672FBDB8330F3C8CE8154E4E8036E86CCDBCE4B693B0C513132B8B740FFDFC51341BCFFA0BC204D63D6696B2271BABA633A572143E74382346C701F92C099B4371D18269A32769889C61AD428FBC110D450ACC95892791656A7DB39868E03FE6F97420910AE8476D43851AEC9A03734AB4E8E4087DDB9626F36ABF7480705C0E81C3887A64794CCB29587593BBB435C2377477B7A0E6A3C9050A84A99936B94C09DE28B018E73CA7C48B083C5BC1E73A24E20D16771711A863D1423E884808E0C8B6E250986AD4BF1B8592F8EB1549DACB4117747F2994D1808B65DB2AA5B05EBD7A0E614888EF86494B93822876C351BA487DB0D0C6F33C353B368BDEA3BC149CB74DAFCBEE209C50B88354804C79976E41410C336BD08C60D65A16BAABB81987C8E6C6716060488905CF550084F403AAD82B09B96BB6C85D25165EB9E5BDFE784F096522D8BEA8007E19F1\",\n          \"c\": \"0756A8BA612C014FECBE00AC1E49271C6FDD87EF587C9879D6F8159E10982B920D3D1F477D9DCA08619185AD10D802A9C4C68D4E68F54E7A530126EEAB93386AE1F184843D90C34A1FCF7E9F54EE61B40A6DD52BD091A4E44DED49D8E8C9B63A395FA22ED602D03E399755F49AD766C49E24994969CCDA54C986DD47DE9646BF5D1DD5AC0EF7B10F86837C7EF18A05E6AEA2254A956D06062EE2B9FB3640C60E8F5907E99524B4B0EC608B7A0FA698B78B7485B6C07DB74897290659A99FBBA2D8CE4FBA364F4A5978984D1ACF0C175B90B1A04C14DEC67561FD9FA789D418EB8033C99863CF52E6653CC4C951F95D718D77088CBBC36A9B520156803B85F3BAA123008C4B2CBEB52C47D790BDEDFD7E7E8B7693DA18AE01A034781DF62CE288DDD29949DCCF28D0B2FD712C4A28282FCBADAF8CC9F811C1D81893161EE2ACEF36D0C3BF128FDFC77C514DE6CE81A9762E7CFDD53D90FA643F64C68969B2076A259BE106D4C5E5DE63461F48C50ECA1AACCF7EB56C288672FD5E7F2EE3167FD01C3A0FE73BDC97BA69474ADD93203BC01BC1D9F8DEFD59433CE26D117C66692149CC8610C89B5DF5B6741AF6C4BDA814657321AE6CEBCCBD5169B32C70083E2A52D6135FB4262862E080888716245CCEEFD6243F0C2C757A3C2915ED58F2D0C82CA5F9C24547601DDAD0EE1832DF0D779D001C28AE57845444F9A527F32ABA50318135931D3991E629B575762B7DDD0239A9E9EBA94603EB5CAA3B2245A608D84325D5D8093DC4136AA736F5D70E581ABA35EFE446E0FFF5F727D413A9C5C5ABAE83AB0F27A710D32BAEF300BA753C4DC7A997AC24A3E8908F1A705DF7D16876DC3854FD25D747CB8FCFD9202EA8DE379891B98A1859CE74F035BCC28ACE3EFD85B7AE2AD566CF64B91B9808686A4D4ECF498F53DA2F514A34F45EE14E28CECFC12BBB34A08F5FD68D6A856C039102C298F40787D64786085255A855965AE4A4B61C7CC395B4E04CE55BC9875F6C179F707008544901D64C3A3C987C88525323D075586A3FDC6FEEA867F0161F619F8004E4093D40FAB63AF2737C51C9637B5F\",\n          \"k\": \"8A77C7C5D298C5A9724AC05B9FE0D8843A7D859CC40E07C786F1F96F921F76C3\",\n          \"m\": \"AA28DCC71FA83D9997DD733D8B0D0394D84D33A3D3E1B74CB74DC6049628F861\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 5,\n          \"deferred\": false,\n          \"ek\": \"76608E0B9539C1B6470F9ACCCF5465C6B355CFA9424DA750CC085477B4A0E9F671DE185995FA38088506A8524D6384A8F505058800B3F14019DF2977E79436D81B8653B844920BB927D2CC7657666DE99C4D865FCAEA3F18302A36961ECA216740075244215625419758B8B8A82C46FBB79333E56D8076BC2F98610FB3474E75A471AA6D746708037020D3578381CC7C959459FDF21839E8413B44C218643CAC49A0179B2DCCB11235345641A10F3D369FD03C8FF733589607616A50926C176CEB45217A0A62A1F4AD3D06669EEC8C947372E8D082E34789A8D9867C0198E5B06934D904AC5318B58A2A5F3AB2522C611F7BA5A1351231F836881116847551D834A310EB82E0B2C6E1728196B7206D00094AA928AD8138F97879B56B55FCDC0D35C76C336526CF52C6192B0793745658E53155241F46663A8DC153ED12862942674B029492D701BAB56D2468C4B165B588D2A6A94C515E9BCB91A55BD372A613590CD76416C6A0AFC40A60669C90BC3A228F05A6DCEA473688CCB80A4CFA3C9BC5157CE4D37BCED562D5B52B70A48294912BD127BE37C39EC4F23F7C3C01A521C56D121D6829A76B7235C2BAAA2BC841634521D8F360D5B7B31B0648FA442C546878A7CB2FCBA5559F94A90308CBDB72C60551409E364CEC93CE24D16593609159735111F918FEBA4B35465BA14AB8AA1A705AA9ABDFC596864347C6E445F23B64B81AB330B454D14413ED47CCAFA49946B828AC3056CB4195C5351FD65A4BAA57262F9A85A4DB552D939BF539B8C4C547B801C1D99C7044393A08143C332AA440477258E04A83F6089C864B1A69418D0B4B739A4BA729B3FCC9C10F9834BB9409950449A8EC09ACAC79BA0681B31A2C7E687793C352F496010AF4A30CFBBB89CB3012216D8E2B489676B05E4B8F142825C4C4552FF7A48802C3E8040C4B2C744AC477EA84901A27BACA79635BA6C0ED28947BBA8B788ACBD17CBB01CA931E4BCF2BC15EC01476AF0A5F8672897AB5A2F9F850BB0ABE6B61A2A4C800F5671F13D712E8686C780677952A0AF65B463CD0289FE3B56F89A6F7D1643F0823093589FC76619ECC2590076DEAA2D895CF1A81924A0490D99446E364BBA45C3BAC1D40\",\n          \"dk\": \"7880A126F08E51F0A6E89329E38741B15C1D4F2410131BA19CD49B9BD32A71487081E4B0E4692CBFC01CCEFA210BA23303DC9C05E317A9F5A51C535BDB6BBE7F1043A5F841E04B7F70E566AD3C28A84352B5064B25DA223B16A8478BB038E221E2F853A1BC07E8940F97175292C6BB8236661D587F6F539BF0616642B82456F7839BF0A9A393919E1356E070058E0442A61CA6F9019240194E61C453420181F8D0A108F87BAAB970AB0B6DC5A7909AF7193C975BE88849641341E42C6F92851EB8885A951779D606AE9CF7271732B728C35B5FD7611FC7C44362A14B16B16F779CEBDC1740F6A8CD9540CFF05BCB95B1C10044EA14256C34B4DF46C2B5363B33390728C152A6CC9778E21A0C909D83001608C773F4DB30E6774DC7F6B80F966BB1494764E8983EE62F35B14C977C1B05C43734528B4281412738C3C10CB4A1016C5EA5B220A4899D91311B786706285804038EB83357DB1B4FF3B12C71850F33733A52582707351EDEA8336C30A326C66805638318E32E4027C97485A77FE4817AA65A48085824D24ABED83443296EE2A306D4642F71FA98C2647124022812C5652FE4AD7B6892E2239FAAA9261F9B2374F242900461A469AE7F2259CDF527F062C1D55C3948840E4FA7305A088B126C27F40C6A9FE0938879A5C7F56157C32E77E417E5356BA200CAC228C647244CFC03479373C40EEC90CB115D4C723D1057233D42AEF5E7557B0713E29700A246BD168272EB0348D282408FF66B6DC16968E7AB493A64EE626F96436D996A181FD5583BDC2F8609B9A6736FAFF56C80375055A8992EEC85FF6575CCAC18C2B763F1E745691259BF4695324C0A42C3C87B7639CD45352A7603730A44FC7473C79A3C72F18D77CCCCE489C897FB3C7204145DA416C214818EDC7AAFA0262D5A330A4C4FDD72C03DD5568AD1620F6564F19A98630C90482C7C064A9788F0A227F7332B901BC359A6FB2ABB30C5131B98429DF96C35A2A81359C0D9426B516847375B8AF236687A071179EC53B54C1736918599C193FBF5C6B421CF7F029B691482DD2A539C0268E590A3CA563FA88674A6238576608E0B9539C1B6470F9ACCCF5465C6B355CFA9424DA750CC085477B4A0E9F671DE185995FA38088506A8524D6384A8F505058800B3F14019DF2977E79436D81B8653B844920BB927D2CC7657666DE99C4D865FCAEA3F18302A36961ECA216740075244215625419758B8B8A82C46FBB79333E56D8076BC2F98610FB3474E75A471AA6D746708037020D3578381CC7C959459FDF21839E8413B44C218643CAC49A0179B2DCCB11235345641A10F3D369FD03C8FF733589607616A50926C176CEB45217A0A62A1F4AD3D06669EEC8C947372E8D082E34789A8D9867C0198E5B06934D904AC5318B58A2A5F3AB2522C611F7BA5A1351231F836881116847551D834A310EB82E0B2C6E1728196B7206D00094AA928AD8138F97879B56B55FCDC0D35C76C336526CF52C6192B0793745658E53155241F46663A8DC153ED12862942674B029492D701BAB56D2468C4B165B588D2A6A94C515E9BCB91A55BD372A613590CD76416C6A0AFC40A60669C90BC3A228F05A6DCEA473688CCB80A4CFA3C9BC5157CE4D37BCED562D5B52B70A48294912BD127BE37C39EC4F23F7C3C01A521C56D121D6829A76B7235C2BAAA2BC841634521D8F360D5B7B31B0648FA442C546878A7CB2FCBA5559F94A90308CBDB72C60551409E364CEC93CE24D16593609159735111F918FEBA4B35465BA14AB8AA1A705AA9ABDFC596864347C6E445F23B64B81AB330B454D14413ED47CCAFA49946B828AC3056CB4195C5351FD65A4BAA57262F9A85A4DB552D939BF539B8C4C547B801C1D99C7044393A08143C332AA440477258E04A83F6089C864B1A69418D0B4B739A4BA729B3FCC9C10F9834BB9409950449A8EC09ACAC79BA0681B31A2C7E687793C352F496010AF4A30CFBBB89CB3012216D8E2B489676B05E4B8F142825C4C4552FF7A48802C3E8040C4B2C744AC477EA84901A27BACA79635BA6C0ED28947BBA8B788ACBD17CBB01CA931E4BCF2BC15EC01476AF0A5F8672897AB5A2F9F850BB0ABE6B61A2A4C800F5671F13D712E8686C780677952A0AF65B463CD0289FE3B56F89A6F7D1643F0823093589FC76619ECC2590076DEAA2D895CF1A81924A0490D99446E364BBA45C3BAC1D405D5240D40DACB83C6E97603086982B2BF96DC0108BE0A5C76AB85AD6985BD6891BBEDEA993E606D87B101D21308B55560DA8BC3F7C7AED02BFD2D42E4D722BF4\",\n          \"c\": \"371904CA3678917FEE951EF2AF3C21CFFAE77DBD02E836EABA8AFF8B687D8FC28E2443E2F6FA020FDD2962976C6D8062FB57F22A3ADB52EB9AC8472EE2A08C4F4D98632D1D752AB7BE7D57F5FDFCF6355E1AECF7C68DA4FAA809177F9C8A749CB779F49F97B65E3467DDE74CE2C68590465B53E91F2C5C988AF7A0BEEC8090E3E3C60441FEB212D2602CFA3AFC27EAA686EB92CC5BF7E914489A33646FC6A63AF1284C108CD287001CC9867A70E3D62B7A437B1B87C095A26543F1B7EF16EA944F7D4FB382AD3329121281FD6CE8EF3EC215C7C13C2FDBD971CD5599ECF5ACD46616B1C911F03AD284826291F56BB412467143D392C07AA0A50BCF9E7B66CB9FDB25041CA81413B9B392C2F937F033D34A8A293E737D7487E2BCFC2C48E101878E03B1EB0CDEB3BC55BC318286521347828C9A5B4B12AF365EE268A743AE1F12B4A145264E628FC9D35DEE5ACB6122996E01E78221E46AD966B53C122EFD33774C7908BCAC16D79BCF41CB6F648DA913293434B237DD06511D2A5A05E8E5C01C44408E53CB4DB6FB22874D492362AE1D2BF16170690392D979A76627C3D5F5347EBAD4315A76649BF09C0DA85A741CC72C5D63A71E3F7BAFA97337FEE2C8C5EB5583257AC1A94C601AC42071FC3ACCA48A8C42ED3667847B3D938BCEC9AB45C8B150E35F2323127583DFBEEF5C1E8CD0E1333B7F4204FED4B5C2FBFD978B3D42352BAC156AD32916B2C22C76C10BEC77BD31046AD7AEACC2A55DAB6E5DFBF9212FD45A763E125762D6E6D35B253F05E18656DF844594987ACC39A34CA480497CCB2981207E16A2FB2F4FA75C8F603CEF272CC63E771EA6F45AF51B18AFFBBB7B2D3C8E31260D11CE04224FFFEE8E665D20977C3C30FE1C9E34495C3AA47019020B2B4582F8CCA36D34D5D27904A769DFD6C6A224B7B7AD693A7C24D04E04063D5B0CC653047895DF676CADCA882DB762FC3E432171E358B2E2C24BF514FDB6303635C6D3F3CEBDFB711E97A730D63D8C71A6DBD0349041BCE85864CF021EC2D335E48647DDB8EDC5C2DA942D87B922FCE5BCBA8E4E59DDF31EAB6A97D6F01049\",\n          \"k\": \"7251DB9D63102DAC680DD894609F12B795371A4012BBD22C05AF846E5D43E884\",\n          \"m\": \"A4BAA4C603DA1368C1F2AC552A331F77BF1D598C6BCB540D43CA1E6D4B8BDE77\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 6,\n          \"deferred\": false,\n          \"ek\": \"E331342D560ED6120D12B8044C013C5A8A1204164B00130354015213361BFB439417F5AB35A18A4D1600ADE0163F6A1760B08B3BD8A16C3A82D253428508216E3952923070BCBC922FDA6D0E1A734232194EF979790828A110B9F096CD37B5C07D758D3D68CDB9642FAF100D732031DEA07BA884ADA7D4436C490924920D3B3B7B67297E5D8A984DB9136F44557C6BAA5023219048600B8550DEE669A9F38858D79BAB8682FFAC37B0CB9362723B7A1087A21C477513BD386A30D92A5F52F008A32784019B61E0C8C22B096B356C65CEA631CAB14632A2B978A40A91BA7544559A116CC74C28332DC745EB19AC5EF50425E59FE2C777EB0AB7F4EB49A3393CEFCBC846B63D399A6B86CB6168958E71045208C81F07F62CE826729BA2162C2B835185759CF4AEA00025F4AB46EB56AEDD408B0E016C6ED538A1280F6A0CAB2CE7B573258937F0A265D663F0F33495BC4852D73C774061DC388C3E75ACC69A84DE303EDB605CDBB238DB265AA26A8267FCB4A0922A5413CB65783CF37CA2051071FF00CF57BB6000C6057BD93D4DF31EF35CA5A4327CA8AA2EF7D33A04E5261E732EE8FB819AA446E02CB4370B2934140EDF17BAF6040F71B34F251C6C65B766D317C4DD181AC04103409B65C6A7ADA248549BE72A2B43B2E4B85B58CB5FFC0B33C675C0E0D50B2020BED86005BDE97B9862436CF7A7ED687591F9719E4803254BCE195185CB3C9A48DB6F148A0876B00D1132AFDFBA1C91F57E980B96F1906439489093194F2CEA56C46942796749926B36D498B026353CCBAC0BFDC84C208502E0518512D427DB058219B35DC793B789F5C579D9A5E7285B3AE90F71FBB40FA39C5AACAE582211D9130B7D309AB3C34FC279AD6016C611F902AF959C984564BFA565891A5D1FE13C9D27181A512EE1CA31BCB55114C316BE427B01B6A519E441F66B926CE959A90A712DA1B81DCB0AE6D67749987B3E541A59D2680B11AF845234937B63065B32BFBBC1C9086C7DCA12E258C21E7AC52F812CE4A16E347C5CC115C2A16355F4745B810A4B5140CAE72248B554725D9C186523CF625013D5CA11B7951425CCC18E014EE5E94C2695BB469BD83646256DE038ADDF203E0B60B1F6\",\n          \"dk\": \"C97A2268733ED7822CD3B809F395637709A15E02CA5CD7381F27B78D9060BF67B334E193372029FA6601FA5BCAD83429A1F22ED0291AEED22790F62F27A57AE87B81D2A7B2BE812B6988400A62156FDC6DA8261C81283DCEA41C0ED48291128E38007784F65A2B39681BEA58945971551BB873869DFD5C093EF64172583AC2CA8A4646B81786A0A5B039C1F00D3503B31D352A92C2AA7880945924AF63D872E8C85BA6D499B22B17682B508CF981F9C9A8B0DB33B0E7917F9197D0D05FF4A5C38A5102E20317DED4837F46A88A41A1A9D588EBCA92440651A1F5774DC7803D3C88547C57A297A4086855AF7C197254CBEC831A0A4666E265C68CA35678068600303AA17B49A4893B0ED913BBEC197E323DD48B94893AAA0999096E7791F9E628C2E26A9E70C8B6B6AC3D1C6B61943E0220BF30D3A70BEA3272069DAAE676CEC68405A6821E0918B0730CD8D282AC733E4749C05CC382D5E727567A5B8F7258E3358831B160D9F90DC76856A0F462D2D6C66B9266C487131F37634B276E2817A2A946B71B7B84F93C64495CA2BA2947F731442B38BA9A954676580B1CB504E3903AE2B8B02CBB1FCF19A6F37342CB5A13FC923529C0AA1B18B16CC1C7AF1946A41950FB81617C369555F5CBB720971DC8076764A20AE54557AB2208684426F25B0CB0ACC6B75D0ACA76713495528461449C2DDA394FEF871B81D38D651529D7BACADFA3CC05ECB0C27444E1A53C3822641BD0B43220A682772D67D7AFEEF60EAB2B3AD3A87DB56391E471968A65261CB3B219CABFE9975A71645A7A2C9E2A234AB49B77EB689063ACA410134AA8570B687767D8F50687F4844A2B3C1C5B05B188005A0B17D4710086D283F6834EBA728F12996841350507CB5BBE247355A464AC7A008D4B9A07240475C2BD3FE1C469788F51003762F7AB89A9C980F4ABBE76782C084BB8F106AB453A7B7A16047222BB4484F992472ABB62D4EBC053BAC367EBB80579A31F2C949D53AACBCB8ECD9AAB2FF07D86388B26E47AEDE4971A4290DCD7358D306FDD951681513FFD5054BB568534D188D608870ED2028DA36B65A37AE331342D560ED6120D12B8044C013C5A8A1204164B00130354015213361BFB439417F5AB35A18A4D1600ADE0163F6A1760B08B3BD8A16C3A82D253428508216E3952923070BCBC922FDA6D0E1A734232194EF979790828A110B9F096CD37B5C07D758D3D68CDB9642FAF100D732031DEA07BA884ADA7D4436C490924920D3B3B7B67297E5D8A984DB9136F44557C6BAA5023219048600B8550DEE669A9F38858D79BAB8682FFAC37B0CB9362723B7A1087A21C477513BD386A30D92A5F52F008A32784019B61E0C8C22B096B356C65CEA631CAB14632A2B978A40A91BA7544559A116CC74C28332DC745EB19AC5EF50425E59FE2C777EB0AB7F4EB49A3393CEFCBC846B63D399A6B86CB6168958E71045208C81F07F62CE826729BA2162C2B835185759CF4AEA00025F4AB46EB56AEDD408B0E016C6ED538A1280F6A0CAB2CE7B573258937F0A265D663F0F33495BC4852D73C774061DC388C3E75ACC69A84DE303EDB605CDBB238DB265AA26A8267FCB4A0922A5413CB65783CF37CA2051071FF00CF57BB6000C6057BD93D4DF31EF35CA5A4327CA8AA2EF7D33A04E5261E732EE8FB819AA446E02CB4370B2934140EDF17BAF6040F71B34F251C6C65B766D317C4DD181AC04103409B65C6A7ADA248549BE72A2B43B2E4B85B58CB5FFC0B33C675C0E0D50B2020BED86005BDE97B9862436CF7A7ED687591F9719E4803254BCE195185CB3C9A48DB6F148A0876B00D1132AFDFBA1C91F57E980B96F1906439489093194F2CEA56C46942796749926B36D498B026353CCBAC0BFDC84C208502E0518512D427DB058219B35DC793B789F5C579D9A5E7285B3AE90F71FBB40FA39C5AACAE582211D9130B7D309AB3C34FC279AD6016C611F902AF959C984564BFA565891A5D1FE13C9D27181A512EE1CA31BCB55114C316BE427B01B6A519E441F66B926CE959A90A712DA1B81DCB0AE6D67749987B3E541A59D2680B11AF845234937B63065B32BFBBC1C9086C7DCA12E258C21E7AC52F812CE4A16E347C5CC115C2A16355F4745B810A4B5140CAE72248B554725D9C186523CF625013D5CA11B7951425CCC18E014EE5E94C2695BB469BD83646256DE038ADDF203E0B60B1F6A1E6FF9222F4F2C0B6E2F4CE6BCCB009EFFC6A423DF374F485EDBC06000C8FBAF7868913CFD39EE71033FD55572599095F2E641FFD2175F6472AD7E38809A25E\",\n          \"c\": \"A5A62163CA438B8A067E66246A18B815146656D4015E6CF9A1FF0EA73BAF7FEC4B3E177D850822CCAA0EC3191B18CBC05EFF51C78947E4565E105DC3570946E1CD76EC2AAF0AA18FC41D8C8F74A1FA602891DBF82FA7CBDA9E0235A35E9256DBEE2A4708C7472AA5E55F8AB1362883C267D1629163E5BF048056BC8D1C67D934B274C4CC0A486BCAEE2B8BE3FB21126643417607393E57A93483BF37A3091CE196D4FB3F1B645A17B8CD6259301BBE4FDAE4174512690D68CA888DBE194E3E2F2B7AFC4C43B6AF0EF99BD4A9CFB5114A178F501BF2ABAFBD74230C9BD549D91165E96D0B19BBF96C3A938B8E6F0C30EE148933399F0FB13B70F606094EF9B02C526BD66B6E1C2FCAFAB16E0A24911B7F3BC7904FBA00C27A752072CD94E9DC7A894BAAB5E4118AA74A32B3F8668A4C5098B466746B99008A979670572944122DFD32807564C4B56D387B7C48F727D121CC34365BA85FAFE27793EFDDD70E5B0183CF9E8BE4E9B92276E49DC675001E0CC8D061CCC36845C05833308CB99C9FDCA57CB8AF659E30BE417B776D31DD99835373396E7F58A9D07D301525DCA367C1FC39C228BDDC630E0FD76D651558B220891B209DC7AF154E2C51A254B088F083A1099F80CDE8274C6FCA19CAF00338D02208327967537F8FCE0CAD2F37CB90F10DB8FEAF457A25E049D85165433115787CD7487D8ABBF1BAC4A1D694715FDA4E145BE3D9F68E18551C2A8EA31163A6407AB6968FBAA88A0CBD30870CA3DE1D61BF4F72E582B9045C83BDD3E2E26276C9A3E0D81FD9E9BBFEDE81C047E2B3F3445AA5BDE4FF909160181B1F8089F759AE9CB206C5027E04991ACBA93A098585857CB1A983DF67F8E543B626449D7F2A52B64296B2DFEB1673FFDE4CDA17F62AA035A909FF44853AE23DBABF048248C1333BA6E6BE74D2EAFAB8FF52AB31CC47CBE84A2221D4CCC498D670C8BCF382ECCEDAE8599C4FDBE7F1B328A4AC91EAC2CA326D216BC904EA0AC019DAD008ACEAFCC6CC71C97A8AB70DDCB16761EFC8ED656CA72E0385E97F14F971132370DE24A682764A88B2BAD33C56E095C7DDC6F355\",\n          \"k\": \"F8F9921AC3524E9AF70CAAFCE21A20ED5FC76DC988625CA9465A257D43A6FBBB\",\n          \"m\": \"C08584D2F5C950E371668A4FC8F527E20AF1532CC28EE6B5620729155B06389F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 7,\n          \"deferred\": false,\n          \"ek\": \"83B08010002CFAE6362BC02EA2C08809E5031C213F78795391E4390F4A4F7D8403B79458B792B3B2A0BA0EA82811912CEA1C2553B264C03992D28A29AFC0C50FCC0343DCA764457D7F1A73A975044752CFAE0101AA47A3B706594B403B5561A451013EDAD45B60E42905A97D2A0288510B66E9E75CCA8A6B2E5504A9380785DB36B21B5B1839111A543B18E3B41F30232E1BC816A1992A01C51C32CEEA8B4CDBD4C30CF792AAA5BFF2D54DA6B988FD8C14534B2D24334625A68CACF5897B495D55A873AAD557C29957DA2644714303CCC8744640C932B8307B8A08C2403A94E17387F4BDF786B38276756D95C0B9FC4E56C7AB925643A21A9BB7BC6AC58262DAAB33AD09348F7C569147359DCC794EA4673ED1ABDFB71EE199B31FE1CADD2579F94682FB013039B935A3167FF8460EB3006DC7EA297B643B150C5CE360BB197732B7F9198F4A7479EBB8CB97A22010618E22200E896C807C5056D78CE088A8478A751B95750A849D12677C6DCA08CDA502EF603FB5C67566338EB13114FD10622BF8B0212C70E92C68827999ED7177CEA44A7A055F12A00231F3C5FEC1BC07812EA902CAEE99B899B39E88FC7FF6973311483F2A9BAB7917A56D2CBB549AA887E545A7554910947CB9F09423C75EE63B626E8BAAA7A00890340DA33B7C2CB6052B5CC47CE65A0873CE9126816D8BCB6A1C8D39265BD1797F610244C565670DF05E58760690D52B6882A9A303582AA03ACB3B2D857CA0D7B8A2BF859860DB729D2740FD87AF798497D558042319638A33326E3C5CB0D3CD6E71A346365B308221A1DC2E49F51E818B22ACF66FA0C586958CA78204A1FAF135F17977ED2801C4591F2893A756A0C4988BCCB011B389D0656A717A069BBBF1F893A3BB64FF28B66BD2106FC23C2BAA2E4495580D08C896A92F2AF36CBB13CEF4AB94E4CC9D866459BF6CCCDCE7912B3119EEF0AD4CF5093CD85674679A298272EAD6B14F2950F18C602C52AB821234F655419B843C31491C47C596596967F1EB036550A941A03790874FD8498DAFDA127C8B0561116F9D414171778C7C36012DA9BC543158A00352A2E39D802CA5254FB4A43FF40242ACA967C85D45EE0F8E13DDD9951336DADD5E\",\n          \"dk\": \"2DE77A797897F74413868189055980F0040EE0C0917405662CE681BC387D80339E8F5190B63CC50F66867CE78841379DE141141EB15A06E3948439A0F438BC56656A5846C98B21C037A888D1D23DE23A1C945B8B2691CAB7B1570606149A930200BC23B2EC335542619619BF8CB6C7C1A0B8F60C4EFF721ADE91C4AAC36E17894E54D3A006B60FDC62616666927C90A5BFD633F124BBCCB1355A901AE432CB344A72B525B58BD3C78ACBB919C3118E1346631B7E8A514F19006CC3D93AA693296EDABB57D37B61A863D3E6BC95F9C5BF114124FA5BE9425679CCA54BF316C97392BB5A7A16BCBD00953BA7650BB73BCDA3278E1EFC1C7D07B73F942326D871FFD470006657AD2B26E930759C474FEFC2CE64F631C7A39017C9B07D41434925740F03CB10335543D17BEFD57C5312A4C8609503924A4880A7664339FBCAAC331863E88C21F1109740CC8516201A20F349CF62CD56E3004DD1C56B255918699B3DBB3F262724C3F75AE212C9B0093781222B10EB9953806644F958702B80FF6274169979ED5C02ED270634B68F435433B6A36F49731C216C7C3E58BBD58ABC299C14CD1AADD4E9098F7C14354702194412A7F5341A451B6672AD433BA3A7331A006D781CC66BDD8680F5A201FA71B3B9D69FE81A624491CE09D461A8B50558B66C72F198B650A329E729C0753FF7E33E6DD41161AAA43B881DA83CB862BA187FD9AFF7885DCBA80C18C730F51360AA08CABA71C88DA42587600E59B97EDFDA010308A2E8483C71F92C3DE34625DB9E09540A8EDB1696E09C97EBB86507BABB3A901186815128A3A331CE51042E03875278048D4B085DB51094D967CD6BF6A872641E3DFA0A5DD7ADC582A303F3CC5C032C89C6B841397FE901702682939452C88D615BF488C7AF3CB433B72163436DC4E7112EF62F82D91C783ACEEB047D6F46036B70578F79634C98B5962C0EBE98AA97401ACA4B19E6A86D451206E9B06E4CB774F392AF4E1C587AA415B191818E438D0585459E43A50F61AAD3E934FCB09D6C78B43DC7BA6DFA50236984C6992149900843959AE8A7A9AA35146F78AABE2C2983B08010002CFAE6362BC02EA2C08809E5031C213F78795391E4390F4A4F7D8403B79458B792B3B2A0BA0EA82811912CEA1C2553B264C03992D28A29AFC0C50FCC0343DCA764457D7F1A73A975044752CFAE0101AA47A3B706594B403B5561A451013EDAD45B60E42905A97D2A0288510B66E9E75CCA8A6B2E5504A9380785DB36B21B5B1839111A543B18E3B41F30232E1BC816A1992A01C51C32CEEA8B4CDBD4C30CF792AAA5BFF2D54DA6B988FD8C14534B2D24334625A68CACF5897B495D55A873AAD557C29957DA2644714303CCC8744640C932B8307B8A08C2403A94E17387F4BDF786B38276756D95C0B9FC4E56C7AB925643A21A9BB7BC6AC58262DAAB33AD09348F7C569147359DCC794EA4673ED1ABDFB71EE199B31FE1CADD2579F94682FB013039B935A3167FF8460EB3006DC7EA297B643B150C5CE360BB197732B7F9198F4A7479EBB8CB97A22010618E22200E896C807C5056D78CE088A8478A751B95750A849D12677C6DCA08CDA502EF603FB5C67566338EB13114FD10622BF8B0212C70E92C68827999ED7177CEA44A7A055F12A00231F3C5FEC1BC07812EA902CAEE99B899B39E88FC7FF6973311483F2A9BAB7917A56D2CBB549AA887E545A7554910947CB9F09423C75EE63B626E8BAAA7A00890340DA33B7C2CB6052B5CC47CE65A0873CE9126816D8BCB6A1C8D39265BD1797F610244C565670DF05E58760690D52B6882A9A303582AA03ACB3B2D857CA0D7B8A2BF859860DB729D2740FD87AF798497D558042319638A33326E3C5CB0D3CD6E71A346365B308221A1DC2E49F51E818B22ACF66FA0C586958CA78204A1FAF135F17977ED2801C4591F2893A756A0C4988BCCB011B389D0656A717A069BBBF1F893A3BB64FF28B66BD2106FC23C2BAA2E4495580D08C896A92F2AF36CBB13CEF4AB94E4CC9D866459BF6CCCDCE7912B3119EEF0AD4CF5093CD85674679A298272EAD6B14F2950F18C602C52AB821234F655419B843C31491C47C596596967F1EB036550A941A03790874FD8498DAFDA127C8B0561116F9D414171778C7C36012DA9BC543158A00352A2E39D802CA5254FB4A43FF40242ACA967C85D45EE0F8E13DDD9951336DADD5ECD8D9B32FE7AB08059F4D70A3AB29FDAA5385C32E8F39DA46953FF323FAF2E6A0E461934F91330CFBCBD4CF4142F5CDF2065476376506BA36FA778DBFB29077A\",\n          \"c\": \"A07E5CA46B6B8A0370B19BEAC4FD58C994AF463C5F773D1638C3A296CF17CA8C18F3A0AB8E1DEBAB9E42995471B0EC8B473AD1F54EDDC84F48DA0EB534C567A73775CFE32F81C94246D991FA1E05EC6C31AA0B802949D5D7D8E5C4D7EF65E3080C01946F02CBE93F65BBAC03898FD25CDC32010EC4BB0119E30BF07F71A38E30FB5091F17D9F856653263F1982F526855324B6898C2671751DF332E58EC54C903A6BB6BEA0C96263913025DFB386651E6187BBDDB1FFE726C0DE8266FAF77384D2992E5EE8DCB31F41044754839C4525B9DB85B57E13F8C02120816D98B1C220687287CD7192C4DF31327676DE1D94C4EFEBEF3628E5E444386ADC087773ECF0FC79306828E58CD5A64CEB419EF383CE920A6FBB59ED2D2C86A78A069C90F9D52BAEBF4007AFA02C1D541BCAF0C8379D1788AD0AAFD6AAD91F4AFDF9C1C165CAB4EEC304DF6FF9F4E40E18F20FB78B3669DE6C0EDD35A38DA399BCD513C49A07F517AB446B19F4A0D13905C3D496CCCE68E8E778DAAB503CADD99B10951D417B5B6A3753CF9189C2DB624C39D1913F97C8ACC47A399DC2DD3539B083A7EDC3F1B7968B2D342BB78B0D8D9B2D026273D8CA46930A98C113E515F9FF779D10AFC857E44A0E190F90DF1E9B2AF4F5EAFCB451535AA8046CC7338722C29E729E93976D097C0BA766C1C977E20796472770BA41F4964107FE11F9412EA5846E512A7FFD42E71BF50DE6D8D86BBF01EC2A867006A0F881AE97104F2E476244A869C1FF895DF12FC04DB5BF2830191E1CF58CDED8EF7494C9E532282B36C6E72D1F961ECABE75CE5A572E30250E73CE74FA5A2D3C9E5DDB5DBA93865BAD0A219A3A8670D3EFFC7CA1119F383F36768CAD4B514E0644DF95D2E7EF768D487FB98C73EB489D79EB8849A96AB0E84B8B4C97464E7E1BE4F0CCA859BDDC3881DB30E333B68CFC90D8B472E577983544EBB38B729CA073FFA80DE085C861668B7843E3576BF89579A1B9FE0CC7884675A2530D5BCC38E88136A50BB28C491BB6579D789106315C91AF1F0465FF5853D1D1F9D762514523A80559A90DFBD682C4B0E1F522D855\",\n          \"k\": \"70D18ECFFEA01D8C2D4BA32516A042A925618FE4A3A69FF7B932361EAE5C6B47\",\n          \"m\": \"1D51A0CC52E85972001B77047D97DF5F47AE11FFC6C31B4AF42FB0791A3DB40F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 8,\n          \"deferred\": false,\n          \"ek\": \"28D9320FC6A5EC10059D3A531C364A99CA5A13CA98FCA773FE3B4EC6851E29EB8C0CC19440922CBF26859DAC0801FB0D0868B7C6C2917C51B7F644B4CE2C9C8AD9697D1102496B9B11B012105915C1A897065B05E6E7713740A391FC2B486249779864C74A1820F34C44063E47C87278C696DE5260270901190A5A618CAED0DB8B21D15C2D0552678833C1793070128DE0E43257413E99A1A58BC50323B0C722D69C36E82659410683CA4107C51D2D2126156C7357586EA553CD88CAC0E4530EA66B1FFBA700EF3A85259B5FABA131517CAAA0383028E125DC1670FF7C62D74775EC58A5B2B6074794225BAB8BEE7C14FE950AFDBC33B9A6CBE8492B3EC2BC16734EEADA20A45C07ADF81FE3401547F06603018BF5C4AD908AADCD9B5FA1D08244D922EE3AB1B3C693335AB64060369E328D31EBB3699BC3F390363610B2333A61BD67CB7DE4C6050C47D6D1402E7B997EC2C2E9B76AE5C77DF610A893183A1117747B3927BA7BB9B9ABB00E0B7E376C3310C3A63CBB03A2CC4BE1265CA35BC4D5C0CEB4187E9F686290472E9B0264B110C80F7A5D5C34678A746A5C4623CF8B54021A725F2C96E4923C2F531A7AC1405E275C44697A9C794625878F4CCB5D9DB9CF903C4F9DD180D0CB25D09AA389675A972223DBDA0435169C8652967C74C583223398F34E86D46DA0D026F5D661A4C4A85A85AD3DA358ACBA0B7E6C9C25D2CCB1A89B5D35A4F6516349CB1E983B793A04BC495311FF7433CFB8BE2722965308014D914014DA39322C0844F3C657804F9561274936B6556A639903808F8495F13C1138DBA65B573327490D8A772961B70B5C6CB4F8906B1B92639AE1AD25045CE5401AAE924491108B81044ED9E6B996A991AF83011B39021FB177FC8834A244B38C850C6FFCA4BD2323F896431FE64658B45299C280CF02894DFC03593135E172CF5B523734153E3CA24D160CCD64340AE6D021C21B8D5B98AEA0D9187F85AE1E5B0D9B06B1D1B12EC223B35FB2B07D81A42B11120A869C0B873831A41632CB0C1D819418647BBD92712920CFC2F24DC7871A194860105A6F0E10302C391E1DC2FA4BAA0C8576BC6E55F40A12DE2944202C00C192B497300E587946F1FACB\",\n          \"dk\": \"DBB9466138876E21BE1B9311F2D37C4BC447ACE23FED0137DF657CF6A396BAAB372552BB03B6AB73F6B534DCCC9CC9A40740BB0D833D64220340661BE56346C9A6A6B2E9787D5A588D1B8721218075048146F75434936CB948A8A66B5DB020C54B542D03BB4C0F352E0377B171AACD222259AEF33D2DD0A341C2BFF5BAC187391800363AF7EA21A6E535C6E226FC50661B85CF44BC1782E74963C4329D785C8661CBFA4B402CC9493A2042CA0743B156C11DDBCC2B1667EDD786C3C73C83C793A069ACC9F9CD353B94F88222A732575937B5BE0B23FAF5BD1BE1A958D77DEAA54C8AD5845E712241D92F8D1218E4070E527771495277FF0828E3C8BFFEC984832141A49264BF146FC9CC9676C59A8EA66C223888A3835932C39C7DDC15D2862C5EC54F125C799C804E6839309FD446C65A1285547662E79E54389030874A55EB6AC6D488E45863E6259D0473366E7A228EEC9A2BFB747378146010B78A712098E81F3FE0666A4669DC2B1BF9FB2BE6444AAC940427D764153575B82A143DCBB78B2097953731618A947F7057949A2B9890A43B702C052C61A5F35BA53825C02C1685A0ABCFD378F232A5251C38444B8F8349A860D67499B832271457940C8B437529D92A92B7676380628394244A8D55B4B845BBA7A674675289BEF6B1DCD532F42C6C9CE199D2AB0CF0AC4FDE7BA0B85266E540A651D6264B0A071C3C3DBBB0BBA579B587A0CEAA6C4E30305888C3BB9D073427173672063D8D5751BE6133996332846264DFC5092DA38246A74BE3A415895CC978C7A890961487650177EC32F6E30F5BFC928C2B36D7181D8F925266355A48247C4DB35E099363D303C157122F5606180C8AB08200CBBBD19B8240CDF7110C5F558F61565DD920213C60A906370D9D5820987AA2D0F00264221B5FCA7EB791A9075552FB015CFE3582B0A37F8D392D8FA3371531B9AEB54B07C0521CD9BD625669D26AB998E354CB1A5FE0D058FC866D1C67080FF99F1163917FFBCBA3D16D1071A895FC87967230A40BA46610AAB97169085700BE733346758E9B567C11C93C334145A37BB65F55CF09849128D9320FC6A5EC10059D3A531C364A99CA5A13CA98FCA773FE3B4EC6851E29EB8C0CC19440922CBF26859DAC0801FB0D0868B7C6C2917C51B7F644B4CE2C9C8AD9697D1102496B9B11B012105915C1A897065B05E6E7713740A391FC2B486249779864C74A1820F34C44063E47C87278C696DE5260270901190A5A618CAED0DB8B21D15C2D0552678833C1793070128DE0E43257413E99A1A58BC50323B0C722D69C36E82659410683CA4107C51D2D2126156C7357586EA553CD88CAC0E4530EA66B1FFBA700EF3A85259B5FABA131517CAAA0383028E125DC1670FF7C62D74775EC58A5B2B6074794225BAB8BEE7C14FE950AFDBC33B9A6CBE8492B3EC2BC16734EEADA20A45C07ADF81FE3401547F06603018BF5C4AD908AADCD9B5FA1D08244D922EE3AB1B3C693335AB64060369E328D31EBB3699BC3F390363610B2333A61BD67CB7DE4C6050C47D6D1402E7B997EC2C2E9B76AE5C77DF610A893183A1117747B3927BA7BB9B9ABB00E0B7E376C3310C3A63CBB03A2CC4BE1265CA35BC4D5C0CEB4187E9F686290472E9B0264B110C80F7A5D5C34678A746A5C4623CF8B54021A725F2C96E4923C2F531A7AC1405E275C44697A9C794625878F4CCB5D9DB9CF903C4F9DD180D0CB25D09AA389675A972223DBDA0435169C8652967C74C583223398F34E86D46DA0D026F5D661A4C4A85A85AD3DA358ACBA0B7E6C9C25D2CCB1A89B5D35A4F6516349CB1E983B793A04BC495311FF7433CFB8BE2722965308014D914014DA39322C0844F3C657804F9561274936B6556A639903808F8495F13C1138DBA65B573327490D8A772961B70B5C6CB4F8906B1B92639AE1AD25045CE5401AAE924491108B81044ED9E6B996A991AF83011B39021FB177FC8834A244B38C850C6FFCA4BD2323F896431FE64658B45299C280CF02894DFC03593135E172CF5B523734153E3CA24D160CCD64340AE6D021C21B8D5B98AEA0D9187F85AE1E5B0D9B06B1D1B12EC223B35FB2B07D81A42B11120A869C0B873831A41632CB0C1D819418647BBD92712920CFC2F24DC7871A194860105A6F0E10302C391E1DC2FA4BAA0C8576BC6E55F40A12DE2944202C00C192B497300E587946F1FACBFA704DBD0B4F1351219286AA8A868F5F17C01DAFF0AA77B36857D416B7CCD47EA078DF2CFDAAC3393EB22F912F4DD6B49366F5C33F3FFACBD766EA7DC8E2EA48\",\n          \"c\": \"B687F42683C3C4EC4D178FC0B437B20E0612D06B76E78D3F74CDD1A3FFC75D5CAD7271CBF01C65BFC917B214CFFE0041AD9E0ABAAAD326746159E02A81567075CD4EA0B3ADC31DFD8F7D85099E2C5E43CECC717C9D9AC27530EBF7FF76D529A499CE1DA92A15AF94261076A42696A24708C8314E9707D14969BC20F0FE15CD26BD53793A24220DC346526884027C2EB342C680DE9F6CCC816035B9263F8CA25F47E5FFBF564C08CCD4C2CBFD7A53C68BA6C8429093C0474D9840734838664C7250D1A19DCC381434BDDE0DCF8403E7C5FB4F79DD595DC601BAA787173F5946F9594379C2D81DCA8E460D46A19E5C6881607BB08DD66DAB954DC5650EB18ADCD3AD5C4E50DD88EB8CD224159748EE0921EBEBF569C91C0CA37151BFE3688049F791D7389E7E8356611E6FB2221C407F3AB2C8DAFEC6B7336BDF115BE3F2A6D22A852FFDFFC258DA596A1C760672708D16A0DEF4902538EA39FD8D34D79B43F45236D265DDE44B64AC3A6106652FA301F2A5E8AE8E5D181812EE0EF7039EB6C34E954E85568BC882F0AB4EAE260621FE45B79C2A71421A3CC73576439F9B15410A62DEDB1E1C1DAD45AEBD86C6B91E0C6600D28590BCF8DFCB5222890DC48AB7931136AA5793998C1C7C97267B460B5E7726EC03287BCE6A815A5CCB408E2C945BA6E0BA9539C7DA8182478F2F466661B5780FA99C875D9B0FBA379E43526B479B202313728EECE94B3EFDCA70696AC99EE56237C3E5665A4495AC4BAC8B9E2DC1386CB2FCAD904EA3BA78B9053E631D8F84B34BDAAA590D74705911E27D14B012BD85364E2CC2B92E11852B0AEFB3CE7082998C7AEF3B376AC05984091BBFD25697F4F1161C7379B84C8F0E84435D3023782BF65BB2DE49B32A7D432310C87AF0D79B1CB59D86EEB8EA100C17CF92EB85881E2A29D17363EE263F787D8BB054079161ABF904717024B40293B1E9064CA9937805BAC81B4ED9809557CFFBDB1E68F39E4176046769C85124A66A78671B9BB2B105560883521E2B000B423D5AA9D94945BC0480500BE1BD0F2F13214AC13189CCF95A6EC0E825389D4462AE9B7E7B\",\n          \"k\": \"82D886E17A88F82C66E8B1E7E329CC61EB0EA64EE63FA02676B362F8DFF29D51\",\n          \"m\": \"BC2D661E6283B835BAEE160D1448957AC2366DCD087176E252F81F1D11E28781\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 9,\n          \"deferred\": false,\n          \"ek\": \"70A72E79BA178F8951F762242A240036093906A0AC1F2501402779AC6315AE8A0BBDD47A8F347A7E8840269157C93A00CCE5A27CBB16AD9179798376BE8638FD97C976D3513F96B2F0542B78893644793185425864C7950E23102BBA9A4BC9B14F2B4981B673F8C55480FCB4B834490C9BCBCAC7566747A38777CAA5FCADFE9710D8627236E52D9B5463456586E4F963A7308040EA243E62A8EF87B418D92F95D41A57AC468ED5CD51A2B643D11E8F03AAB4F43F2C8160A89B5ACA63C7C9B59E2602458AB103AC58085EF7A98465884BF16A662342BAC721C1E59EBEBCB2CCB9A4FB8A3A650C52B612243D133F870613BF5A9F30095456247C34C779C899B6CD17A181903E0BD1A13AC73BD2848724F25EC2EC037852CD6A960E2A3509E982513FBA840D28BB3B732D76C5909B6612A6650548C709961368F8116236B6709AEC97F7C625094B93BE5B11E9217C8E845ACEF8ABE3742724969C74622C5CFA7776135CC2616B5079AE4535C39C1A3154866AD548C950052B88888761D846C6240C9B3BC0EECAA284ECC4499660DD2BC1B3959AB943612F35610EF1688C41105693061BF16D6E290BFD4941C0460FC173694538604CEC6774C9614EF16B08666ADD3916CFBB39B4BB75EFC166C7E8BF70AB8B872630AC554028E8C147BBB56D3BA312029A68300CC743B81BB35878B26892696F538334BB0A22DC91391D71CFDE723D9E813B2EB844B6A534280880C37B9421572A6B85AFB8AAAF5AF0BACEF356B7F32636B50B3336B7636ACD6C685ACB178AF9986BCB93BBF1C4C69AF1AB569B60B133A480D39B4BA319A6535CA92293D5082ABE76204C7420CEE02C1D372D0413B9F8368098104120D432CD184550667A976C9DDFE0916D423EA2221E64FA95B3F254F59359BE711087732B7D8336B99CB95C7BAC714742D0595BE953273E551D1122AF9B49495DD1B935255726A643D2F8A4EC9A6F573A97C8507FBE6754179A9439C33B0CA5BE3DBC9F19A18C3253A6830C6B69821C86E38A71CAB632E0A578F57A5377015FF43DEE85BF9C67591A1CCAF2C6917387C4B5193404C87AEED269310ABA894FCF51793DDA786F80CA209AE909B8147FAF316B06E4AD8C516BC83B\",\n          \"dk\": \"E2FA96EA25A100737368D8A9D7C3B3CE4C0085DC98854BB29306C45025C0CEFB64EFC64688D46485F8CEF9A81C8D4B53A3BC04DE05BA93EA006A760F55FC46FFE1B659A14FBB16AA00CC1EC3ECBD6C55CB64055F95B8308B690488B146669308C3C3B82D59736CA2A204F07D7984BAAC0109868C666D8644D4A29F0CB02F9C50853428A2FF773E7534989DA2567D044AD9A353628C107D9635637AC330C5A5ABFA1D548C6D552199746C0770B433848258FD185834C309DB32588E57250E206998C8783C177D8DEA3DDBE3473FA8688D7065E1BBB3F5EA79DE04AA4EC5AB16609FB3C4CFEF1659AC60B00765B38106A860506DF15C20E3D474546559822B91D5208CC03931B2C81F54122E9AC68115FA5F608CC0BFBBA7CA4211FF330154D591A48859F39B68BA36BE285B1147DC45FBCC5147A84C42315E4A412BA3D28D537C54AE295CF3400AD400807A38270578692DB112BE01359BC1CFBC001A6AC9AA070B024DD2C6E16B10C3987118F680389A34DC05606CAC4192599D2360125093905FF719A3792A1D83AAC0370A2286959C0B00A7371FF96B9035415DFCD6A0433B19D36800489C4636469BB195C4EA2563B2131B22AAB5059A30A4648E2A956535073E08376650024E52A72F1303BFD1393C7B7C2D320C74DDDB36EF035DDB3CB81F74ABCB2616EDB97FA96AAF8AC65311F790518636D89A80CA289FBFFB2B9E899A8169B0F855BA584263031C8CB5D7598AA43FBA3214FF490D56223168F6BE27557277734CEB673EFED604EF647990A27C0D8B5AB126649E3590A6F5588C7468AAFA7E4AE1C01BD96F66A645B88565310C3CF917B512C22A19589929D867D977B5D33A55DBE92C13F28FF92C6866CC4E75528CC7F5AC58FA4CE523435E08CAACA26CB71B9564C1B167BA48D9E724032368FFC29D8B15A695903210F2300B5415AA25A7034BC5E35B7C91670164DC5559185D489CCF9F168F4F175AB1F2B42249B95589474D8C9FC7EC1D15449C233A41F7A0CBE9284866A7B47170BFCCD2052F5838391A15DB0018F96CBAE3356356BB00B963ACA97A1E15757FCA358E63453F70A72E79BA178F8951F762242A240036093906A0AC1F2501402779AC6315AE8A0BBDD47A8F347A7E8840269157C93A00CCE5A27CBB16AD9179798376BE8638FD97C976D3513F96B2F0542B78893644793185425864C7950E23102BBA9A4BC9B14F2B4981B673F8C55480FCB4B834490C9BCBCAC7566747A38777CAA5FCADFE9710D8627236E52D9B5463456586E4F963A7308040EA243E62A8EF87B418D92F95D41A57AC468ED5CD51A2B643D11E8F03AAB4F43F2C8160A89B5ACA63C7C9B59E2602458AB103AC58085EF7A98465884BF16A662342BAC721C1E59EBEBCB2CCB9A4FB8A3A650C52B612243D133F870613BF5A9F30095456247C34C779C899B6CD17A181903E0BD1A13AC73BD2848724F25EC2EC037852CD6A960E2A3509E982513FBA840D28BB3B732D76C5909B6612A6650548C709961368F8116236B6709AEC97F7C625094B93BE5B11E9217C8E845ACEF8ABE3742724969C74622C5CFA7776135CC2616B5079AE4535C39C1A3154866AD548C950052B88888761D846C6240C9B3BC0EECAA284ECC4499660DD2BC1B3959AB943612F35610EF1688C41105693061BF16D6E290BFD4941C0460FC173694538604CEC6774C9614EF16B08666ADD3916CFBB39B4BB75EFC166C7E8BF70AB8B872630AC554028E8C147BBB56D3BA312029A68300CC743B81BB35878B26892696F538334BB0A22DC91391D71CFDE723D9E813B2EB844B6A534280880C37B9421572A6B85AFB8AAAF5AF0BACEF356B7F32636B50B3336B7636ACD6C685ACB178AF9986BCB93BBF1C4C69AF1AB569B60B133A480D39B4BA319A6535CA92293D5082ABE76204C7420CEE02C1D372D0413B9F8368098104120D432CD184550667A976C9DDFE0916D423EA2221E64FA95B3F254F59359BE711087732B7D8336B99CB95C7BAC714742D0595BE953273E551D1122AF9B49495DD1B935255726A643D2F8A4EC9A6F573A97C8507FBE6754179A9439C33B0CA5BE3DBC9F19A18C3253A6830C6B69821C86E38A71CAB632E0A578F57A5377015FF43DEE85BF9C67591A1CCAF2C6917387C4B5193404C87AEED269310ABA894FCF51793DDA786F80CA209AE909B8147FAF316B06E4AD8C516BC83BB3E7410628F44018D9DFC0EEDB18DCFAC7847E688013C039343B7FA08E0F9E191F64385D36D685D9D38D2A68F5825A84B881DECD0CE337355956C68C7F2B32EC\",\n          \"c\": \"FEBB296071C87A2541D8C0BBEF2F132BC433D608E04E65C035055494F9D3AEE01231784514801870A66357792C0F73238C18B99DEB53522AB3DE54A40EA37D24D62EB782187CCAE51E9DEBE131910ECABE37F312D6FAFCC8A5C1091C0C80769CDF6ADFC3A1C1F3F11DAEAAB65966885B193ABEB6D2B1A81082BB171713A983F073346E672D9F51ED6F1F1D71DDAC85B3A8188B37956709240C78D1EF276E6F534BFA98C52DBDD43E0506F665319506D11642110BA872A9DF8C197ADA9575980048639C930F29C9C45BCD7BE9774B49C2FBD7954ECBE0158D1B6911ED7FDB4EA3FA92F63BBBC34DAB800B2843B5BDC15B2EDECF6DC700A304B31C8E19049EE0371BC9A22E3F6B1C710BCC3AC662148FD9FD729DC3C339E17C4123EEBE60A36269AF28F8A81136379E76C35903C3E017B40E38F273D1B95238F71FB2D2FE6C880307762CB855C0C1951DD2C2779DCEC5285052D60CFCDE76C73B3E95F1D4868C491C71928A3DC04455F0B7F10564D4D65F358DE0AEF7C27D25E89B89E85F6A0B3C34C8AEAC06276F93E4E631EA6120F4E0130F1617891F67731075F6438DF717A4208E45DE930CDA28B737F902C3CC1592CDDF805FA269BC0DC98C40CB9DDD24AF71EAFC6B0B10C9EF2CE262F6D4A22F3C9FAF2553638DE522E5207570248FB87AE1C3DF5144F8EC2DBB4DC57F1C5F74D401D92D0F9E1D7AB98A6BE2090169FAC2C9FC9C6CFF726BD87C3FF2565052C85478FF53CE69EAE1700254AAA94125FA1B7236F4D9258987257B57988D8091AE2B0C06732C8C9FA35C2BC0896EE39825CB2C1B889EC496CE290DB565F403107E58F3DB1B2D40261EBB52492F11E3AEE9B755332B1A000595AF766AAA3D15116865BD3C6C1FDE48A149766BBE0381498B5B2BED28E4E8AA2C87FBA08D28AC8AE64ED47E8796D006169A90CEBDAA2DF63C8E809F169ABEAA9662349D740312B7ED2F26B7762352DDF8BCFF3E545DE5CAB29B5057086438944128DDA68C36C937ADA250A1826532231082E7CEEBB082C62E0BC2E1424D14FC40E057A1591886A6141C49EE309E97AF0C64D1F70FCF8BE9EBEF\",\n          \"k\": \"21CFA40FDE8834A21A9E419B7AD8B9E1F59B7CB184A0CC18932523CF45A1CA75\",\n          \"m\": \"6745F4F0730AE3F14A428A95C9CDFE82717EAA94F65B00A01566A4DCC9ED1E5E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 10,\n          \"deferred\": false,\n          \"ek\": \"5E101D0614A50B307E1AEABE717721690422A1A97C1D05713ED3B206D57919AAB32F6096A503C5C1C1BA332063489038FD1BC7F7B816F6F343274C19E01B1F9C088B311A0C8C09864BEA142BA2C829C5B74C988266BC9F12D37EEAD97FFD48AC5DF0C7C595A5626A23E20663E8F11A6BB66BEFF4A91D6C7CF551B34DD541D3DBA1F16BAB95312A60F9625DDC4FF1C6690E9B05FCC5CE05742D65D5915740AD9A7B60F9D098AA62A6DD3968DFAB5ED93A54E305596AC8B1E4967A5AAC15BB4B540C421571EA324FB422990C2B9B82A12915745C24BFF841731C6C07FE705CCC9B4C15EC42193472A4542ED5FC0B0CA98A91B7CDEF68A6E7ABBCD07B984795151D72365F31BFC76793FE485FDC663C6B7CC43F5A9D410BAAFFBA43CA968988337C9BF802CA221B3C366A22E66F703C695B178830473D7C372D338A4548E7C88A9C4837860419A2B60A3CA9AA19A21B570BD22CA228C019EC6420E77B340F32265BF86A666C9FFC75B64FAA22860B205DE33492B1720BE514BCD5A47496BDEC246D97ABC377430528940BA447579A90C9E18152F69BAD12CB7C51C48CAA8BAD1EEB2867F31691B32B75528056E56955BC4400A249550569C722410FA2573007BCD2B7331648AC9B424667582131473C2E8B11D1299DB6927384B257A81B6E7868AD00216C8548739EC83485CB330B03BE99DB38EDA338C6240828D7A5FF463B1C0C88092CBFEF3A2598993C72F922F2EBA31FEC317570B73DBC53C7F04810878FC9718552CA2C5B7220B6B7298886C94D9C8762A4BD05D8BDE7E9483850C0F75617686174D42CABA69B2968004992467D1CE078B03884F9E00B901473E7059195487C5E3BB0B938049A257C8046B92D129A906669BB1B92B1A83FAF951A9E8332A1833D665A720D0451801874036A7A5133318CA94015A63FEB52C40158644C3869F2E37BC18160C8ACA2381BB9B20B5A58961AF2E8593884ABB47A6D968A8C6BBB83CF089C6B75B6522C77B2B4064EFC50C2948A18C85B1436787C3A076547271F33B864D5CCC585B317193572428FEDD54B923973A11287B6527F68535DA629C904AFED0646115879DB4F48777D2CCDC3784E28834C7E503964FBD58C3652152D\",\n          \"dk\": \"09035BBB278F4BBB6709E427B18149B8D60E9697BA4DEB42DFF673B87682244B907B476132C8914F26045F1794ABF41234F829E4C38A3E1007FF6C74CDD639057185AF8A419B6C56129101D4A428CA92BB42481F23F5888C596ADF8A0F51BC42BBCB7D93305D072832251466D65C864B48794D126F6EFA34FC22592FA2A843D1AEAFC5B7B84CC982CA54B0C62A4C083EF545BB8B38B7F3C97BF9067277A5CB65B0618192091159200A321948415C387B7E4D65C7061331C65A29FC323E76E11E2811A76788B1B811BF7B17952820C077954A6F731A1209A4F5F68A2059C3E7969B01FB3A2FEB7EBE0C304F2C604DC0B1628A839478059674574DE82A35B0B55A22818399247DFA2363161EF981A2E2E117B3B1A61EA42ACA4A6030E5C139D663171867AA364E7DD904AC9BAFD69B480CD35D1A9964CA5C96E3D0660E10B51E805605968AF4747D1414B258190CEBC6C87B1C476AC9B48A0A8161F66E10BBA0D2FC619C415A0846346C9AABFD2B25773A40B3A242DE25004001CD409B8E5B99C0408ACCD8127355790E0D9B03A7AC3BF9F54E1B4466DA83CC0CC7920BF583DCA743568907AF8B381B4553EFA608204A86FD59A03B333F8E4049D3FC36E04A89EF2091169BCE577368043C20725861E7D76C68497D9EAC7F2838B0DF8CB44A829C1A89A7030A3D5BE81F480275B2DBCE88BB08435446EBEB5236C1A38ABA7B1F64304B289AE2073A414434F7521E44B8CBCCCC4784B16649F5B63812AA5C723B6C4024FA8BA361E44DBF21684FCC9FE99BAF2B71BE70AB358A867D85628FA4402A97B836713036AED2A8C14141424581917B6A88A48B9037CE5AD49A4B3CB86972BE77A83A773BB64E63B8AD552E97D9604F596E1B64A891B4526BA6B9C31226C7B491F0C529BC93A2E5651FC1F5C02F27A800683E52ECC087121D4A515864A8602E3432761A385D7355B20475804B98A193BF503326EE3163821677FBC371DBD76502398199D1577C576DD7128BE9E7621AA7BE650A1A4841262BE23A6946A4809875279A93107589C8821DDD23B0C0EBB16FB047364221E3C1671F32325575555E101D0614A50B307E1AEABE717721690422A1A97C1D05713ED3B206D57919AAB32F6096A503C5C1C1BA332063489038FD1BC7F7B816F6F343274C19E01B1F9C088B311A0C8C09864BEA142BA2C829C5B74C988266BC9F12D37EEAD97FFD48AC5DF0C7C595A5626A23E20663E8F11A6BB66BEFF4A91D6C7CF551B34DD541D3DBA1F16BAB95312A60F9625DDC4FF1C6690E9B05FCC5CE05742D65D5915740AD9A7B60F9D098AA62A6DD3968DFAB5ED93A54E305596AC8B1E4967A5AAC15BB4B540C421571EA324FB422990C2B9B82A12915745C24BFF841731C6C07FE705CCC9B4C15EC42193472A4542ED5FC0B0CA98A91B7CDEF68A6E7ABBCD07B984795151D72365F31BFC76793FE485FDC663C6B7CC43F5A9D410BAAFFBA43CA968988337C9BF802CA221B3C366A22E66F703C695B178830473D7C372D338A4548E7C88A9C4837860419A2B60A3CA9AA19A21B570BD22CA228C019EC6420E77B340F32265BF86A666C9FFC75B64FAA22860B205DE33492B1720BE514BCD5A47496BDEC246D97ABC377430528940BA447579A90C9E18152F69BAD12CB7C51C48CAA8BAD1EEB2867F31691B32B75528056E56955BC4400A249550569C722410FA2573007BCD2B7331648AC9B424667582131473C2E8B11D1299DB6927384B257A81B6E7868AD00216C8548739EC83485CB330B03BE99DB38EDA338C6240828D7A5FF463B1C0C88092CBFEF3A2598993C72F922F2EBA31FEC317570B73DBC53C7F04810878FC9718552CA2C5B7220B6B7298886C94D9C8762A4BD05D8BDE7E9483850C0F75617686174D42CABA69B2968004992467D1CE078B03884F9E00B901473E7059195487C5E3BB0B938049A257C8046B92D129A906669BB1B92B1A83FAF951A9E8332A1833D665A720D0451801874036A7A5133318CA94015A63FEB52C40158644C3869F2E37BC18160C8ACA2381BB9B20B5A58961AF2E8593884ABB47A6D968A8C6BBB83CF089C6B75B6522C77B2B4064EFC50C2948A18C85B1436787C3A076547271F33B864D5CCC585B317193572428FEDD54B923973A11287B6527F68535DA629C904AFED0646115879DB4F48777D2CCDC3784E28834C7E503964FBD58C3652152D01E6E2FFEC99716B96F8708E8702954EEF4142F1526CE74057D0049AF5D0376D8846E9C3DB8A50D814B91408C2FF732842F8D8DCAB2AA5CBD2848D44A65C056A\",\n          \"c\": \"8830427E2A9F37CCFBE39067C9D14B9404B83F9DE1BD9AF3E167D2053FD526F8534FB8960B8425CBA720065307602B8E89EB9810D7436CD44C4ADF87EB25F8B8F87865325383238931ABB418580D4774D645C71ECDDAC6B9F4EBAF3410DC142ECFDC357CB3521E62EBE0EF28BD41DF94A593374D8B9EF362D71A7D5AD6300E9C31514B5DE5AAB25E421646E152D5EE9A530F8BE6D0FF5D77DDB93827E525862437A9B6593ED284AFCC8453B409745DE7AB21FABC824307CEACF7D68D9E0EB54E69C98E3B94C61D9B0B84EAF064A966A7F99746EDC93F36DDF7826FB08C635891861FEE8D72A4FDE67F5BE139044BC775E73E7CB2695E24D81D84B2274461EC66E6A7E62571D306A667BB7C6F53AA1C3D403E2C6D48E03B29A164DB2AB7ACBB7F955F1E8CA6F836125B386453E047CB800F65656684FDD5BE79A8F12A2C90839B6EE89D73EFE016C09D878F16B92D62819B85E4275637305BBCD4FB25C578FB5CEDEFB3F9DB6165B5211623B2E53B53A71C5C2EF62A4255BB2E5AB6B9743353D0013760F89CF8A07140EC75D6BB8335DC3E1D2DC1393E43535119F3661F476168522CC25C7A702B58967113771FC6EC6B0F133DD349209C35012AA380819450487670359A906529490000F7B7179C8B6B44ED64C5700B190FD2B80E089F81E724B560E2F9479F8CA9C325B2D0E3873458E9B387BD1B2D84BF4CADF8924F55DC9C410871157B9999E0F580DEF7449F4CAF080028DED23F5437ADD8C3BF004268C9E6BAA21EF9F9C117A543E946D469A9FED47AE20524C3110D1F968A02A8C1DB24DE10316D5C2C0C28A10A043A1FC6393D7C0D6F2AA5DC379D64C1B870A1FA8D543FB17F0E5CF8F174208B370C6A4C44FA851CBA345EF09C70DCF5CE5412BC11A56E4FCC38A48D9BFF662DAFEBE105DDD686575DB01A1AA327A35E64B1DE9F55D1B3C6439E5A0396DC60A2BEF31D52ACEAB818B7068456FAA775F3F3D0BFDB4E78A3E3D38FF6162AC0AACEEFDB04474C93F071833BFC0DF73E0EB4B3A6AE04B87EB3490151A6A1DDE59BB286D449347ED0370929059D775E7909E8F35470DBDC6C\",\n          \"k\": \"CEC6DF7A0A9B79894EE00697CA123B88C4CF94EDAE8514C8A024498E909C72D9\",\n          \"m\": \"C3ED79224CB07A8D37DC9C789BC7AC8E278968E429087E5B2C0E878934DAA53F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 11,\n          \"deferred\": false,\n          \"ek\": \"F3007CA415887CB8294B07BEDD802012CAB18EAC1E58268C821BA9A63739E64AC4ECA1A1CFCC7E61E73E72214981ACC725B3040205134A998E3B0168F46901788054FB20C88EA08F8A96B1AA86CCF2D778E3363F4202CD8C655A14C13D8F954709F46FB78175DA1C839C612CC94984D33A40A2B3CE56C28302E9C849CB51F8C60B614924E21B374A381F473344847CA1E4C610A538645EBB7CF3E1894CB2AA2EF2884F1CBF7734426AA315BD52569E533D374A91EE6A8BF8E05393EBC1506BA0718C40465A7F24483D933926EBF49E9A65C56A1B25F1A122D03A14F150C62EF186EE39335DB1A99D2498F8C552F32568CF714C678943D74651AE345513A4B4D1460040831603874CB281CFF1F49A3FD43EC80724560136C07C3392D4B4493726AB25CC00585B1966B8E73246ABF05D78C43293670F18B02EC5D107A8518F014ABC1108B60B3781AAD6BB5152B228A5BAD5D81FBF0ABADFC1B89047C3EFFC77FA5ACF0FE88CFBD40A5270408F20601A981FA666B1FF9B6324A47D67DA9621F60042C593725A604CA25483714AEE23A38B01AE7EEC4BD2422D7231157533B819F554BB76BF26D9A2EFC10C758B584B1B663CB91023C1869E5283FE13144CC3149F6A2037D53008F720337A58534360765038AC222F97B9C7551004740A5F526597712504FB2B102906A35375943C011C8BC4B44323CBC690A5814A385CAAC581A8530CB201FDC902546C924E1A51B1154EA2D56C3D898394A722FB3B962102262B9A410B12AFDCE8250947BC13A88B40399E05B025A9770D6381206D895E2D319D6C2A237A26B06C7302B61C1288EBABCC439A2B8284B81407BE18674102AA5569A10461C8B1E550ABF41F5773C7EBC8B2E2B968EDD56800B9AD3D95057CCCBEF4CB143489C219A6570B9A4CE23B6044580F7AD2CD2597B8EB5300AFC63C4DA2A98E1C7ACB57C2D7A06E836781F5CB86BDB82AE4CB1577A0AE5540B4D23B8EB100B02966C440B68CAFF36801B99F38649D3D8A49DA61C719D196888481C798B76CB0BEC1AA037AE4271E04BE54075AA51569D700A95254A8CFF1874522935259AF76CC7555A056505D0ED973B075E185A37AE9EC366F52023FE381ED83FB42486B\",\n          \"dk\": \"8B177D624AC1BC09657FF155207B487BE10A043371240026BB1578A6B96017F0BD124038F96422C1898C85C2B5ACA61292D39C1E4A472E99B5EA32631364C6B509A54CF0810FAA061F668199F32673EC6BCDE700894236B64B1F01A4A9599529E6950748830E3E64B9C1E12EAFD26147A86D585C35F56BB1B2689B30614185DC9E2EB96A72FA560EBBA991C36BD5D5B75CF2A7E217CB07AC500FCA1314470B99977248475BA90504EFD64D9BE624C3C13FF9DB9BF3E85EC9B70536451C2D2C993455C8F1061C53A404328788A80877D6BB2CD2432375B37D9DA1146A0904F7263D7E01429DC1767274A654699E862C4064F6C165C6440B3A9393315E8B74493A0C430DEA414C5AA1166B47CD611DD072A0E78A64E191B8C369809F644603C2C92527B3328B9C4C491CB92364E0B632A1F86052302DF0CC196D259828113977347AB59528C91409C2B15E9262966B6980A4FC3A472318FDA787A94B52D9499076D118D02259DC5A6A16D3B90EDB5AA88C40315C703EA52A4B227A2D7103C2C0BF0EB455442A692B3BB9DE56B6528B8DCF383569610872C9469B21BE123C7B30B251310BAD364272C4D7691514755C149EDB752A67854A7830A2B5E6C7913549A4B78709007F30178C832BB9BEB8072FE42B96AC73C17A55ED10CFAB0690AEE71739A8828EF27D3B356156461A9E406E864A931906AF731A241B8B55D344B7ADF93C0477B4A8EC41E82457798666DFB96BAE33876E384CFE3AAB021622599078F15CAD6FD5A7829BC87D20595A5430BFC6AB2B138D54BC36A6B58BE1148906C2566C5347BE340C71949DC849A0F38081DC0B7B7CAC8DBBA5341EC61A1DD889D262BDA734A0EE691063D0CB08045414E37548195C6CA41B1F69BE334638582441E3A3CAF965962277466CC1A5056608B72C5B6232B2901A93D615CBADB57F2265917A330EC8208BAF046BA2960B80E502B4F48ACCA6AA972528D3504C1696C6D4D116325361CEB15F9CEAC560019C3B3A8F374397DD17AE23588D40779473684DE24768EB628D38F336C075A2CD56402783863FA2827AE03B74A2B3B0C4AE36300FF3007CA415887CB8294B07BEDD802012CAB18EAC1E58268C821BA9A63739E64AC4ECA1A1CFCC7E61E73E72214981ACC725B3040205134A998E3B0168F46901788054FB20C88EA08F8A96B1AA86CCF2D778E3363F4202CD8C655A14C13D8F954709F46FB78175DA1C839C612CC94984D33A40A2B3CE56C28302E9C849CB51F8C60B614924E21B374A381F473344847CA1E4C610A538645EBB7CF3E1894CB2AA2EF2884F1CBF7734426AA315BD52569E533D374A91EE6A8BF8E05393EBC1506BA0718C40465A7F24483D933926EBF49E9A65C56A1B25F1A122D03A14F150C62EF186EE39335DB1A99D2498F8C552F32568CF714C678943D74651AE345513A4B4D1460040831603874CB281CFF1F49A3FD43EC80724560136C07C3392D4B4493726AB25CC00585B1966B8E73246ABF05D78C43293670F18B02EC5D107A8518F014ABC1108B60B3781AAD6BB5152B228A5BAD5D81FBF0ABADFC1B89047C3EFFC77FA5ACF0FE88CFBD40A5270408F20601A981FA666B1FF9B6324A47D67DA9621F60042C593725A604CA25483714AEE23A38B01AE7EEC4BD2422D7231157533B819F554BB76BF26D9A2EFC10C758B584B1B663CB91023C1869E5283FE13144CC3149F6A2037D53008F720337A58534360765038AC222F97B9C7551004740A5F526597712504FB2B102906A35375943C011C8BC4B44323CBC690A5814A385CAAC581A8530CB201FDC902546C924E1A51B1154EA2D56C3D898394A722FB3B962102262B9A410B12AFDCE8250947BC13A88B40399E05B025A9770D6381206D895E2D319D6C2A237A26B06C7302B61C1288EBABCC439A2B8284B81407BE18674102AA5569A10461C8B1E550ABF41F5773C7EBC8B2E2B968EDD56800B9AD3D95057CCCBEF4CB143489C219A6570B9A4CE23B6044580F7AD2CD2597B8EB5300AFC63C4DA2A98E1C7ACB57C2D7A06E836781F5CB86BDB82AE4CB1577A0AE5540B4D23B8EB100B02966C440B68CAFF36801B99F38649D3D8A49DA61C719D196888481C798B76CB0BEC1AA037AE4271E04BE54075AA51569D700A95254A8CFF1874522935259AF76CC7555A056505D0ED973B075E185A37AE9EC366F52023FE381ED83FB42486B5FA88098305912B30A55D51412219D3B2A6271BD46046F454AA4AE238431115880FCD17FAB3E190E96CE2AB5E42ADCEE8E516644801B0C0D42BA08B82F5E6E9A\",\n          \"c\": \"128FEFB85AF81CD2D9BE101E5B2C6D4D10C43C870A5E180D9E811541F16875B9D1D4842CD6B2A9555D16C7C47A1A30647BECC8628788194ACA88048B291A3A83CB5D4346C5D741CAA1AE631B59020795049046BA09C262D50896BC4D390F4963970FECB91DF2DE283EAD7CBA46F8DEF0AF5C9819B3F76B7EA1C653584911809310ECF9CB171F1B0C83F147D70996F57B18D0D7BF596983017E02AA7B465210B5BF402444167831D409D2D9A7CE9C24D3DC6CE7A3F71DD0E7F13F66214B29753D625A7874D4606B3688D8FDFDF0459034C4B61794EB476D02C375DE54E543F4C5CE160D0764AD5F001B4CAC7FEFE69B06B5DD4188D6A75DA0EFE81C8BF2B378F888BBD41F9976B56CEE9B6A30AC1F7DAED843FF1A6C209CBA6AB8CFA42E0270817C7CD1A8EE1D8E5552A5771A95B0C621666AAB4738897A5C35F54618D41E4BE592EB6E530228B21A09D56A86039DADA8A8D530E7D95658CF9C3AD3E2476FA037B38F8730EBA96423F5CDF884E9F707B18326A9BC9EE51072AEF8096B9D2CA9D2347E4981AE99ABAB9A2DEE0DBFE2AEE8BBFF5F2EBACE1899089B2AF44318F1530E2DB95F6A6004BEA7BA1643801C2384E254A4E42372E74B30CDAABA3A5A7868A43A91F58503C7DF9FEA920D8C29EECFCCD6D42332D2DF6E2A689865BA65A03B65F0E9338BCCE725BB3E50B28FBCF0F194E24D7EBF89FE4B7B546014667962D92FB7F33681110958A6F5AA0129717FE505C5EB2A009E641FFA73AAC6F214F9B75EA658D012FC638D7C607D6C8292140D856BC2FF86E5DCA2B357C6C92934E342C84AB22374E2C65C5071F1A29E21A3D346A5F4F2B6EDFC1985CEDAFD9F62BC44B07C42E4C34B24450FA07394FE067804775F98846E5F72977CA6B58484D2EC6A5634B2C11485DA0D4AC1F96226EC3920A3FCE229E801F2C9F175C56DC03059B00154A7540CBC4A538B700DD948C19EAF88EF6C206B5F58EB6538DEE0C87E132C62086F38C8F1E9A799E845E2A7472DD393A3FD455617A9C687C1503ED17D8E01C992F503A699249B81BCBA9A9F7606217BAFABAC90995A85DF6663A7BB6370DD\",\n          \"k\": \"9015AB2A00F4E86BC82E6B3F5208D45BA0A725876A9E19D52C9A43332554D3CB\",\n          \"m\": \"41C74E66327238C6F7B2ED2683FC5E88CC35083512BC285CCB7165499F34A0B8\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 12,\n          \"deferred\": false,\n          \"ek\": \"A4F9A7CDCC7E700196FFF92085458D7D6855DCEBC5D98260E4C32C94DCAA47606619C6C02752333D30BCB0E82909D7657FF199668B8824C7B323A50D51203E97A944467930FEDC000C69496751C3C69A4FA4492CA6454665E56C9B677845D6CEF70A21DC7028C4D487629B9738C300D17995F839B59793A0A8813BF9012E0139BDD0EA31582BA31E070BC7F2B44FE57CF5F36CB9D92C6926B271C290CA44BC3068C2981C6CC7932A82E69C1AA9616D99C36CF37D8DE7642F2A0A615B6B85660224ECA4AA6B4779A20A2DC93BF31BB25582705B56B04FC1898CAB5FF97B6934158B4BD9994B4A1643F8A185483F93AB349536A29A3445250575CFE07122021AEB450DF71943E0818B664951137880AA5586C66260B835BAC540378374869084B8E3CC91B9C12248E2A75C060EFB6925C35A3DF34BB5A8E76B4FC4706DA00C2C0331502A3B4030A8CA25AC07EA139FB8B4733957784551208662250B542FFB6B6985A130C109FEF00E30D6C238FC0A10E567737B8776B94D3FEAC0FD9B055FEC78BBC4961015033CF78805872F1F9899F69492AF6A35180C8F8393071BF020F746506FA50E5AE7A675542610F883B24A103A32CC407933CC2160FBEB0375A514A2D405992BCC8FCB82F3799365D238E02C1886B9499CA2C90D8B33FF32AF51F5933F721FAD5CBF78EC78802C7DD06777DB8C0FA3C0CF09790F65630E772177D4D9A35718ADEA8758057601C249C92876799167604664BFAA93537572A6A1425BC0AC53C85AC64DD9A7BE436F110C3AA68BA0552994DC019F9AD80B2767B879C8B7D4664AFA6C937276AB40417AFDC2895FA756909864C4E1867A537B42E271C86953B16279843C88DB9385EFF67FCB8A6BA13025B259398F149B49E1A4A9271CD7C1B5F0C6BD7AEA36CC6739C2660E262A3189E9AD342215FB5A95DB2B179CA763A8AC500EF59A4A243F6E6C96C278426D472161C1069A31992ED9AF9B88275D21815F1584CE7381D487457CC15E04E13FB4B93F309588D238BA0B59867680585C043857516395BACC287491B0E65B1C175B41045F2B94202F888B103A6683BA958FFE411135C3552BD546F9E3AAECB8C783AB074E809056545A8F7B89E7BD8DF0\",\n          \"dk\": \"5260A5E1F6990DCC65328126BAE9BABC0B28E7010A2F9244362218C5E9074A331179B16A07676727966476FBBFACA90119722A168C6CD5A1619A0548C15144FB408C1099BEE94C25DCB119EC6A677F3B0D0D705BF8B7047C564BA5C1843378903CA64D1EE7357D0A2102865417865CC97C93EA5362B4BACB5E5604ADE8CF16E6A70F455A91D104E98C9D63273476C067AAF45B8739099C527B7ABBA56E9B1D939C8A776C4931A14B9B34907EB309FED94406145274738D7C061F5F7038CC2AB9AF6243C913A3CE212ADD4B983FB646A7732BF1B30BF2759CB52936C325CF475C47A6A54979BA2A17D4066D52A728BA6198CB52762829D85C56F0DC1B4352A191AAACE6CB1C26A348E45A25BE0C86A66B16DEF9AC236C2C03BA883CC80908F8586501C5C8D02F0EFAB2D9924CBC2B2935CC73D946A8A370362FAB0C8D94891D980CC8665290B7471D5477902B8A83A0C5B5B2A8BBA4622E272AA9FCA8AF59C40E109C682AAE7BA0C61B49C127629AB0628E402828E7246219AC368063660B1910C7D10CC5992501841612C44F4056B319044721EC9550E235BBFC6D384A9013C52C61257BB9D315F8C383DA7C66A4690C29930F6912665BB494CE63A96185B4DA193C1D58CD79A312ED57BD4E4A86FDBA31D87995C9409A6D03924D6C35145BB0233BBDE8A717E2291F673903839047C23B401FA680EFFA41869664F7F3612C552B01C9CB05D518E6010A744CB5756A31A50BB965AB3ACD62781955295D7C4A07FA13EF911DE93AA477952D99CBCCB3A83FB0E50046917506352B34158E8E451AFFCB9E31F7B800D00D23CA1A927C4AD9B57E0DC7B69EE270A063B3B286ABDAA22C946AC6C436233C6979C3570BB5889F39264017A63E09FB54547C5711B0B6C8576A8F1C8B013A0EF8190795F42E376C803EA4A8908605C8F51E07C3983A357423D594D79080C71AB3EF930901D31D6539328BA5B18725BD47E6AE466C2891F4C889426D5BD267A9B696EC4739C5F978619C786333472EF241353688A02C1154D4650C0C5E22C61AB77B0FE1D193573760720CB2E633868B0B114DD8CD03316DA4F9A7CDCC7E700196FFF92085458D7D6855DCEBC5D98260E4C32C94DCAA47606619C6C02752333D30BCB0E82909D7657FF199668B8824C7B323A50D51203E97A944467930FEDC000C69496751C3C69A4FA4492CA6454665E56C9B677845D6CEF70A21DC7028C4D487629B9738C300D17995F839B59793A0A8813BF9012E0139BDD0EA31582BA31E070BC7F2B44FE57CF5F36CB9D92C6926B271C290CA44BC3068C2981C6CC7932A82E69C1AA9616D99C36CF37D8DE7642F2A0A615B6B85660224ECA4AA6B4779A20A2DC93BF31BB25582705B56B04FC1898CAB5FF97B6934158B4BD9994B4A1643F8A185483F93AB349536A29A3445250575CFE07122021AEB450DF71943E0818B664951137880AA5586C66260B835BAC540378374869084B8E3CC91B9C12248E2A75C060EFB6925C35A3DF34BB5A8E76B4FC4706DA00C2C0331502A3B4030A8CA25AC07EA139FB8B4733957784551208662250B542FFB6B6985A130C109FEF00E30D6C238FC0A10E567737B8776B94D3FEAC0FD9B055FEC78BBC4961015033CF78805872F1F9899F69492AF6A35180C8F8393071BF020F746506FA50E5AE7A675542610F883B24A103A32CC407933CC2160FBEB0375A514A2D405992BCC8FCB82F3799365D238E02C1886B9499CA2C90D8B33FF32AF51F5933F721FAD5CBF78EC78802C7DD06777DB8C0FA3C0CF09790F65630E772177D4D9A35718ADEA8758057601C249C92876799167604664BFAA93537572A6A1425BC0AC53C85AC64DD9A7BE436F110C3AA68BA0552994DC019F9AD80B2767B879C8B7D4664AFA6C937276AB40417AFDC2895FA756909864C4E1867A537B42E271C86953B16279843C88DB9385EFF67FCB8A6BA13025B259398F149B49E1A4A9271CD7C1B5F0C6BD7AEA36CC6739C2660E262A3189E9AD342215FB5A95DB2B179CA763A8AC500EF59A4A243F6E6C96C278426D472161C1069A31992ED9AF9B88275D21815F1584CE7381D487457CC15E04E13FB4B93F309588D238BA0B59867680585C043857516395BACC287491B0E65B1C175B41045F2B94202F888B103A6683BA958FFE411135C3552BD546F9E3AAECB8C783AB074E809056545A8F7B89E7BD8DF0AE9CB398180B4EFE7B808B5881B8F0E5F9A8C23F7FF068DF3BD63457D3B48469DFD461BAA311495C347EFC0C40ACCA288BED6D4DBCF3BEA45D5AFCB6E7FFBC2D\",\n          \"c\": \"D9B7CFCCD8D7790A264374AD1ACF09AAAEEEF36B2AE84D657C05C697901FCC6C6B6F31BE49D729E31FBE760A93D9BF54D0FC37B81F6240D3BCBD911142EE7C330A570CED051BA7DE20810F59D6A2BB0B00F7525F071EDFB8B9DCAD854C70FD454784EB8F68638A1880D468FEA90EF517EA77594B53E901A2BD3FE2BA66F69B6F644FA0556D43FD799145B389CDDCEBAFB1A84B9C6F34231D0028584A8FFD70E69E1C84F33884ED6D95793803281561ADABF1EEEE72C22790558F3A6B7F0A54FCB96BFAB67314951158CE54880D201E9E8B0E76A47DB6FE8E7C767A4F604ACA6A598A25233440687ACEE588E5085B7A28C09E01E4906F3D834938833F165CD6FDAB1524F7FAA64C0B44C1691DA39FE88C19548F9D3ED4EAB68E853CAE954C7749AD6C55383E254E7FFC9662D500AFB1FF1A6A0312D7FDA9606C9E3665C46D0F7DA6C3B3F61EFB25DF574126D3843FFFE720651A06ABF241A68702B7B9A07648AD17E5238FD29D1CCF781605FF482857F1B10E36CE1BCFDEC8D8A0AEFFE0643E65E1BF0B060FCDC5C591CA15B645B701D33D1FB4ADEA2D13562D73CF68361BED92FF108BCC5C31B8E9AA913C112EF54C529BE6A4D2CC64808DFB5CD5EA8499007FA9F156CFA686248FD7232E797E4944E433FCE98B3864B46751D2C55FBC1C4C71FE96A875CBEA1F47F1D6A3C98E80876780B95936EB0368CD56284B211E670BE4ABB5536E6F9C7BB1D2A23C04705BA1D851408A2C566E9893B5C9EC60245EA2174016096B9FB8E8476A94E174E7CD68C66AA805512A5D851ADB17A99B49C33754BAA091E834A09C90880C95032D385458BF514A3B88C67FBA9E93317998AB39F712A5C38F1BB51BD9FDC0B538189580DFDAB817341145752F840FB4207EDB939855745977A65B27642F7C28C91FA7D78075ADB813D896DBBD57DF60EEB46A9C00E07F1F63867978B61AE357F695A1E2D415496773CB52258E94012527DD1FBD35B0A239A48894C4F54FD2606C9B5919BDD52C671D9FD169D8F4C6FE9D01E19B358A84876D303AF979222BAB23CEECE34209D8C1F890091B547374FEFEA7E5A6B8\",\n          \"k\": \"6D339C7DE13DA2BC3F672AEC4DDE931C811FAA91A8E91182DE4F94F2009EF16B\",\n          \"m\": \"6DB6A3F134471A89ABEC3384BB48A3C405DD3B2A5EF53821A3C1EA74DD562799\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 13,\n          \"deferred\": false,\n          \"ek\": \"34166E98297165A69FE3818A3857B7D6E97180D938B8B56ADE8C361F010DA4E933F85A911214465BA7A0432AB99A519BDC81647CF72C4498A220C5B5CFD28566F4AE2CDB025E6270668153AE8B54C22BA5270A1DDB5660984A9812C32E6AF44385C494D55013814900FDC0CCECD7963D9945C4A7329FD5038F382181507D43A96402A973EF683499854D29BA964A530763A09DFCF4B97EF549E5A60C1EC921F8A2C9745B364316784AB27D96F8A9980727E5AB1FEC832DCD023D1774807BAB6D02E898EB0CCDC0EA26DCD16518089BAFE96AE395348481427788794D357B59E4421355656318B3984623C472A01C215CC13B74A3A154FBFC4DE4877ACE367B20397F6FD29E757354E6542A31E48E01B43EFED71BC6AA3540D3093E5794D889B74CF73C6096AD1CE46F4D5A912BB77C10C8CD2F92A6A952B7EE647D7E73C73185942CC331AABC3CAEAB4E0F86776DE4B5EC53B571B818B6F9BB31937EEBFA22C6E9B486E0CBC8658CF2537B6128C939A477E5376C59024440C56E853994FC588EE3D883B3A369D6CA24F228412140BEE37C11E8718831B24C91C46A54D09F75B99015A92AC7049697260652A4288E49A25F8C223709A035F36DAED2749DF387F6C53DB022535E729A10C8542D84B79AC5B44029BAAA360EAFC90CDCC754835A207378309045B7DAF6B8C5B6462A3B777150BA65C73B68F75089E79F1E820889A7705F4B6C7E404436F68DE4B50F0A082C0A8258E80B22646C29E7C732D9489AEFC11AC788593EA8C865970F83C3957829A818D1734DA277871493B45B93DBFBCDF0366C3635C0247358B1C7B8EF81B4B2EC8B4AFAC90E1983FB1C70121691E5913AC3B2689EB28773179E7225788FC319490393149A352EEC56732BB61B1C086CAC906D422D05917F3F1C6F3A57A7B61BAF9A852A8859B5B4F183B36715E578C63DFC347F86005FA11B407494AC4B19A7FABF59D10A34C58C6308768CF88BA463AE8BC243405277AC935D1C5238335B47066910917183444C79E959AAB64915C9609A5A12461F9125062C2536B73C5A5C1002269B0C023E7228AD1084918A247167D0CA5D87F83ED7B3EF523CA41BB22FA002ADD4DCDB3E7B68C892797481BC0B\",\n          \"dk\": \"BE01752AEA3528793D84B87D34010B58DA56FA4219F62A2BF66530B4115EA494A0AEC432AB2A2F6624C984E522BBEB6B34B1329D1501BF41B48CE820D99B3476413A87FC1055EA373BF32DCEE2573F13B14AA43A441BB2FBD97CB863B1D2CB0B60078552E8941F7A9A4703C6ED6621CB055C5C2CA4BE869887140746131CEDD20FCE9C38AE2221E49C384D76A9CD6141EAB46CDEE4B8F9EB734C0A698A038E1F14C4EB92A526711EE058502F54005DF57A443C11806496F14756687136F44B80FAB94E5A145F6A1A8224886DECB1B5C872202190106A95C588C0BC8ABA28EA5BB4C3D39330D17FA78098E1BA7550EA3CE47516F91801D01BC09AB8C9897495DCE68E45366A8BE5962612A4B0E6943DA72EBC87270A64B083225C2D408EDB1596B0FAB8BE0540098B0BF32C18B7401AE1F65C14F4C81337AB8F64A4A7818127AC10195C929C51B85001498D9CA7BB5A13D2FA515CDA75D6E795611B1ABE18981B972D3086136E5841EADCC52C2162B1461250C9111A3016B3B78BADDC6F59E4BEDF989DE0DC03D1555B98CAB2CDD150ACE44B241B6C4938002EE610910A22126B94937A9A7DB1BCB2079DB81B0BAF0A2A1AA80D609B76381888E0FA05329C49C379BA71300A9171B0227B442C610FDC2995C9778CE990B3C1DA83F3C8C1F2C499A1B9A1D936538C9C61B71C83524747C4775FADC055F08AC79622B4CFAC6996C5602C992713607F2D59621A9B1BCE67517A26BFFD636922362E96DA43B5C72BB0697F24167DF97B724971AA5C8B5011C02DE7857F761154E24B6D1741C49616BD17E360A9586D612589FA81625A5C55083092550C8F69B883E6C1510A122C94860EA277274BE1028353219F7B0CFE837B59D895F0A0597BB1AB93D39C6EEA6FE41A70BA395B97FA20B7161437543CBD107BA552CC13BA389F985622C5B4BE7C7F600A6C45196DF7B66BC1053C5A4C7EE2027B49E6ADBC100996717936C89B1F878ED03C4254E84FCD3251129A078749AFB1713635A83FAAE1C201F1012D68940BFB0B44BB87D43344FBC1C2F58B29B2943B148BCD9D4C368837B0BB6A0AEF083134166E98297165A69FE3818A3857B7D6E97180D938B8B56ADE8C361F010DA4E933F85A911214465BA7A0432AB99A519BDC81647CF72C4498A220C5B5CFD28566F4AE2CDB025E6270668153AE8B54C22BA5270A1DDB5660984A9812C32E6AF44385C494D55013814900FDC0CCECD7963D9945C4A7329FD5038F382181507D43A96402A973EF683499854D29BA964A530763A09DFCF4B97EF549E5A60C1EC921F8A2C9745B364316784AB27D96F8A9980727E5AB1FEC832DCD023D1774807BAB6D02E898EB0CCDC0EA26DCD16518089BAFE96AE395348481427788794D357B59E4421355656318B3984623C472A01C215CC13B74A3A154FBFC4DE4877ACE367B20397F6FD29E757354E6542A31E48E01B43EFED71BC6AA3540D3093E5794D889B74CF73C6096AD1CE46F4D5A912BB77C10C8CD2F92A6A952B7EE647D7E73C73185942CC331AABC3CAEAB4E0F86776DE4B5EC53B571B818B6F9BB31937EEBFA22C6E9B486E0CBC8658CF2537B6128C939A477E5376C59024440C56E853994FC588EE3D883B3A369D6CA24F228412140BEE37C11E8718831B24C91C46A54D09F75B99015A92AC7049697260652A4288E49A25F8C223709A035F36DAED2749DF387F6C53DB022535E729A10C8542D84B79AC5B44029BAAA360EAFC90CDCC754835A207378309045B7DAF6B8C5B6462A3B777150BA65C73B68F75089E79F1E820889A7705F4B6C7E404436F68DE4B50F0A082C0A8258E80B22646C29E7C732D9489AEFC11AC788593EA8C865970F83C3957829A818D1734DA277871493B45B93DBFBCDF0366C3635C0247358B1C7B8EF81B4B2EC8B4AFAC90E1983FB1C70121691E5913AC3B2689EB28773179E7225788FC319490393149A352EEC56732BB61B1C086CAC906D422D05917F3F1C6F3A57A7B61BAF9A852A8859B5B4F183B36715E578C63DFC347F86005FA11B407494AC4B19A7FABF59D10A34C58C6308768CF88BA463AE8BC243405277AC935D1C5238335B47066910917183444C79E959AAB64915C9609A5A12461F9125062C2536B73C5A5C1002269B0C023E7228AD1084918A247167D0CA5D87F83ED7B3EF523CA41BB22FA002ADD4DCDB3E7B68C892797481BC0B25939AD5E8DF2448392861CD66369376BE1E6828D87503F46841BB7682A42BA34940BEAD249B04A55DC051633480E518638E7792F57535B3FAC26F0A535A9494\",\n          \"c\": \"3BCD7972030D4F3414C2D151C52BDB8F96500ADB92F89A721A305EA938987F4B0314F093FAE503D8375C134046365443E3E000E19984777AE9189169E20AEC928F3DF3E1CCD2963BAEF94436E3D8116721413C7254F90208C788644A3AD90AECA2814526EEE017E07E222ED0987E5693C2C4EBD524F2B79772B974FD738C59D18FDC9E091F32351F86C57F57A21BE5706C6394D06253FB4526FEB48ACD18668324B7E662E5909CD76F160FE8C562975789F6C7290D1BD167E647FA2FA61FC753D5AA6FF7C62BCF3D7144D3EC02AFCB3E162C3D47F268D78F08FD3B621F66970C9A2A95C003092C3246DFDB1104AB31FAF7FC140D7AA39AF34F429C51041AE7BEDC26608A8BF52D43901BE92E65DCB87B832442ABC64A9F61745F70596A148D3EB7E40E7C8A49B24155BDE63635FD26FDB6458145D06FBA000E577073A407B36D4CD898A312285871487B50B25589D39BB453521F8436DB251710CA8F6F3E5D6EEF56F52291F7AC3DB7520E03DD95058C5CC4AE39E35FCAEC9C7E0284A9483C09D473EA173BAED7BAE5E6397F128C872469CB092A65FD1D2CEDA8E659CA97E7781EDDE6EDA94E68746182FB5A44BF7951C4768F66532445F577950642756BB1FD08448128CAE0D819BDB41DA547914CE892963F64C609C44170AED7918B3192EFCAB9AFB493CEEB327A4A6D21F7FDA7ABAEFED12FCE2F180C8A01FB905482B8BD65859CBED2FB8D13C65CFF497D8C9E0621DFCF8ABA62FB0FF0DE460C04313127031FF4883E9077A4A3FFF4D21740E02563F9595E2DB7B5867A7D5AAC7D7CC2E6206B9DD07CA8D2743F69D3FD0D5C00EB16E55827EE917205816DB1C6ED1BEECD4D529C9A1FA1C9115312B3C9392790BDBDE5EAE4078C90D7CA55CDC4021EEE7D488949AFCC05F2D7F4AD5505B3613983A87ED316B0D16443CBACD8206B593D86ED37B4C884B7C1124B74F30C6F2FEC61AA6EB96CA206EED58164F87D5849814F793FC54BFE5D5AF81E497F60E3C1CBBE2FA1FA8A602A4A36F60A567E9605CD439A2096B2EB051F3F9DD901BF119E172F52D617B6E5EC6422B0B05C030C20649C\",\n          \"k\": \"63AC7D8750E143131B3FE26C0FD5484F5D60DC8D22B542EBFF0D5D8B54F34EEF\",\n          \"m\": \"121DC782B740EAE666E709EA6E3CC6CEB8EAD204CD7D85D2256839E98CA57003\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 14,\n          \"deferred\": false,\n          \"ek\": \"F3BACC6A8C9D390141981325C2DBA6EEF3805950199468661ED4BA300174FCE35411963B2B07599E14524446892603BC300AA3A25B45B41A857D50C06595B40B80159A477D8AD628F455038960BB23B0BC32D90D48E014BBF0AC12F93F17E36976A8BFD1E7B682212815A18B10B5301E0B88A7C17C850AAEC549B98195532C9323559591F9DA52703CA2EF921B1DA82D46A5B522F69109AC83351C46BBC375B61A8E7FEB0161423D5246362DBC8C866A899FE42F3C3C52EF82AF2CD05C11913B87E67B060A2200BB60D8B94E58C1B609DC9FE36A147B5C83CBFAB3C5F59F2032307E5745B7C68E324CA91EC987AF771D9D774B49E18E1F828E3D17641C51CFAAC9450D3A4404141187546AEA64939D94B884868876C86348E27DB0F66BD1A06ABD331B8999A446370982EB25979C35733791A49608D412500E9A144036970C086F15AC90F1699CA79928416858389908F1300356B9C382C94A2AD5491E86C51905BC398CA41FCBB05951A54A3681C806CC4868266E100EC2A334DDEAB98BCC5699E192AEE81DAB58C5C3D24C564C9821C7C0F981A500D91E66108C52013134B9440591AAC5A9BDC6208B22BA88F269B823DC08BD0C4067B7BDE01B5B12985CFA30C9172C9A2D95A33D4BC4EC181F17CA7A294989F0306FE2A7C7F16BCF301752BDE3CD3F2CB81460C0E3B0373EB8B79FD84B05A783E14454A2F2ABC693AF0AB78A0095A3695158A2B6783D32BFF25A4F6EBC4AB5010C0E394437D208ED001CB3B2C92A794607A6BC5EB580E7D52342B004B3A17E20FB70A4C5C2B8CB39C9E265E012C7D03540E7B47681409E30D169AE1277F1230D97B432E4276406D0C4FE52661E69ABD03472F2A56F52B51A5A317B57D229EE0898DE0CB2FEF71DFB71A85ADA97E93B9F2CE3113E838AB458A9FE826090369EA0F496DBB2A21D98981AC4410CF698CCBB6EC6724E498ACD1AF8C480A6AB1A67907D0492098015DB80A98FA8CFD79A74AFD439B4327C07A03C71AA7589D757CAD8BF86DCAC20B133A5511A47DC74B12317FB305098069DFE360250AB29A4239B1792352C3571978B708323BD2CCE72D18392A9C3CB7F504A14014231E9B4F65FEC62AD3125780D51E256CFE1\",\n          \"dk\": \"DB859103F7ADE02C4713C26A95447BCB6BC359F21EE93B845C5C0A145A9841BB576D206B60E46417AB9163A86BE30AC95FE3C9F9EB21DE282FF8D61A68CC43EFFA3FE3BBC9A5D56C304194DADCB94BDAAEF8CA74857C6C5C58A35D415D4E23BEBB602ECCE89B376CB61365C5014C128D090BD3F1CD51380A7D63965C7B143A19663F007CDCC5736BE624ED2721B5F915B8AB4405E5B44C3CAEA181B60A6347A1F0AD7278A879C75829935B056600D078133361A495539E612B9F677263A4A808A0851C54F65D54E8195BD4313D76A9590213EEC21B8FF2561E191DF5B8227D03632782B8A539524E6349D1693B61C0BD4F47B7CAC2AD539A69C609BD6146400F9590A0796DA89ABD99B62C30756CDA7854A792AD8DFC8FC695CE68FAB4AFE88134090AAECB29C0D75C1916A3B6225A76CB6C0E530BE46A60A3A2C8C07427ECB7C090002378B98F03450AA7AA0D8AE5226967C7E9F43AB50C098EB80FC72347E631120B8BBBA3E5C731A7ADE238B55A1763772A86C1159F96EB0B96D5CE0D3C9BA68B9D099A7184A461F91010B8B98C08E0B67A67CC6D4414390A0009493955172B85076F17BAA9A9645C21101ED962574E7C5AADA2B1EA401D05C92494A07664E6521219CDB248CF2D7B76E4D8BC77E30A6CC80017B282F6F55DD81355A8D296D4F0C71D95C14D9031510B012C989BCF959B51B44913B0808C786467AACB0E870C4297034F47AF6226BF8C3527D83B15C547CB94070739367CD48949DC7126E3672068C09C55C48BFB610AC87B81CDE4619221656C624DC1C24C6A038EEF5B58A09A801B225A132C85EAD459DB0A49DEC270B2F046C9482D41B20699E97C11905D4333ADD8139C659289537A411E3BA8FEB1072DB3C723551C7057BA55ABBF0F6755A465B73A93312D4C0BE8C9BAEFC872B48131B6920CBB48114846436E091554F2C352C919B6B16BABAABAB0D1424A5AA5CDD32FFF661DFF15A5BE1A83A0E517FEA9CE2462554F3C64A21C2769A18206F34071C1B2AFA03403B814CFB90DAB476F29B6ABE9DA68F4AB42A10C11C54A54CE51BD03B23DF85072B333114589BCF3BACC6A8C9D390141981325C2DBA6EEF3805950199468661ED4BA300174FCE35411963B2B07599E14524446892603BC300AA3A25B45B41A857D50C06595B40B80159A477D8AD628F455038960BB23B0BC32D90D48E014BBF0AC12F93F17E36976A8BFD1E7B682212815A18B10B5301E0B88A7C17C850AAEC549B98195532C9323559591F9DA52703CA2EF921B1DA82D46A5B522F69109AC83351C46BBC375B61A8E7FEB0161423D5246362DBC8C866A899FE42F3C3C52EF82AF2CD05C11913B87E67B060A2200BB60D8B94E58C1B609DC9FE36A147B5C83CBFAB3C5F59F2032307E5745B7C68E324CA91EC987AF771D9D774B49E18E1F828E3D17641C51CFAAC9450D3A4404141187546AEA64939D94B884868876C86348E27DB0F66BD1A06ABD331B8999A446370982EB25979C35733791A49608D412500E9A144036970C086F15AC90F1699CA79928416858389908F1300356B9C382C94A2AD5491E86C51905BC398CA41FCBB05951A54A3681C806CC4868266E100EC2A334DDEAB98BCC5699E192AEE81DAB58C5C3D24C564C9821C7C0F981A500D91E66108C52013134B9440591AAC5A9BDC6208B22BA88F269B823DC08BD0C4067B7BDE01B5B12985CFA30C9172C9A2D95A33D4BC4EC181F17CA7A294989F0306FE2A7C7F16BCF301752BDE3CD3F2CB81460C0E3B0373EB8B79FD84B05A783E14454A2F2ABC693AF0AB78A0095A3695158A2B6783D32BFF25A4F6EBC4AB5010C0E394437D208ED001CB3B2C92A794607A6BC5EB580E7D52342B004B3A17E20FB70A4C5C2B8CB39C9E265E012C7D03540E7B47681409E30D169AE1277F1230D97B432E4276406D0C4FE52661E69ABD03472F2A56F52B51A5A317B57D229EE0898DE0CB2FEF71DFB71A85ADA97E93B9F2CE3113E838AB458A9FE826090369EA0F496DBB2A21D98981AC4410CF698CCBB6EC6724E498ACD1AF8C480A6AB1A67907D0492098015DB80A98FA8CFD79A74AFD439B4327C07A03C71AA7589D757CAD8BF86DCAC20B133A5511A47DC74B12317FB305098069DFE360250AB29A4239B1792352C3571978B708323BD2CCE72D18392A9C3CB7F504A14014231E9B4F65FEC62AD3125780D51E256CFE1C9EFC09C35370E689B7071A0232850E93F30C5E774FCD37BEE6F8DB91C039E822D53C0A2C522F3E692881F0A4C65BBA41050E7D310898A6747509513A03A418C\",\n          \"c\": \"DC29B9910BC0978FDB8F6D7C215C91E003C550A7244B5D98509145A3544C5CB2AD40A332B2817D15182C8A31108109073D4416992C99149241C5FD147A48F23981C2B69C34E7A72D11B7DA6ED9973AA55A4812239BF8E0DD193E1EDC85635D31FCB26F094E23D47C84F805755D58D3FF7B30E81B8066986D2DC94210778F2F52F94C569AEF36A35F4AAA445B54180F703C28684D842763C9C1C0AFBFED51895B06670D97F68548E40202BF56A1BE5F874A6A440E4673E4A3E4095DB97FD9D36B30F4BE492FC957FA898E4E9BDE175C91927B058D0A1E10A500DA733B640B08DEE07AB4ABA009DC5B5E300F477E6E34431CC8A5DD699EC6D7C509637B6475C5D28DDB73E790F7EB60F7398303B4501D56E2161755D3E43B24AB4C1B391E4FA041C8FE0153BAD4CB6072213EFCE733FD9490583B93DB3D319B51E8DD497A1CFCCBFCC3B227747A9B86B2C5DA52F36894450B2750CEF3B425671EF059C0C4BAE8CB25E6CD626409F79E63CE4262B2275A45D18618DB57E9CF3D8CEA6B22B69340B9807B1DD696B9CDBEA445BD0E1FBDE9D86C92265C808B677A10EEAEDBB71E04949A2FDCC3094ECCD5C37C08A9B3636AAD670356633BAE9B7C16B5A8C4AC79C2873B4056C65E1CBDF13F7A55FA5EC9C4E530B3F13479D8435AC937C165C1269AA8AA7D939433AB0EF01BB87D2FC4D9B9405545D59FAFE004DF0A8086F486B04B1105DF829840CE198BCCE0F55C7486572891E31EFAE42785A2557CA8A685AC8A2655D71E4266D3418BAF29728193C3C5C22E7BF12A933CBEA2D3665C8A155B7B8B8E9EFFC7F99B580393AF3F76ECF9D731D5A299E8D8B2ACD2EDF58F6AE336D3DEE5D7258DE80C86B0E311536F8FAB511574504DB04B4E8326176C15DE143E5DA575C027585E1C8DA38CB50A2A72DAE9D5E266F106261395CC2F9575C6E59B7C73476A65BDCEDDB9E03CF299EEB5089683043FC97A8EB247CD46656A7CC5812DEB8E111CA1040E9BD541CD8AD4786FFB04ED643456E72F3C6DF4D595B1ECB097D6564D42D5915BE55D339D7583AC55B4C1D8221258C0A5BA1443F9BFFFDD2AEE31\",\n          \"k\": \"CEC93D98469424039335CB12FD0ABA4CAAFF3E3B99E55A53507F2CD3458536F4\",\n          \"m\": \"307C7DF0692D264A8186B8D844C7287B236D0FC7EC148BCFBF261A16B0FB7B61\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 15,\n          \"deferred\": false,\n          \"ek\": \"E3F49934F999E0884E019B9299EA5DED8B5374851D9BC24079FA481C534793D328BC3B3563D0922098B98AE26406BB81A48AC0D24102E1D3624F974FCE49786890AAF9EA2E9CC058133C66847B67EFD61C7B469DC8E983ACB6ACB98C25D2A84A6EA82DB780531D0A4FA3A84400C70DDB48CC52406CB3155EC059894A4C01902129D1614C3BC25372C730CDD6CA8B204B9D18793ECA8B080881119549779A67FD456520B1011C6609CD4329C1832F7ABA7997D79B9510409C920E8EBA32A476B72061CE58A8A0A8B369315A814982A519E88E8FB25B3C74B527776C671C7B48C3C9EEB76F2A60BB39D5722A71459B180BCA9A9FBA14B3AB764E5F931DA0A55FB247A93ABB8509334050A029CB7145D4141990929A7D224FDC905419D62828625E316778E1517B151CAFB1B8B8E5E96958447944E266F808017C7CCAED571C64D9BA153709C303C92DC69B808941A8C3C760E52EC7388E800159CBF8626F953E356A827AB8692FE1982F07884FE3B094ACC0075128B3D54D85C39C08D065B6E40B5475CE1F269A2227AD4B368C683A7E797A7857C26EBC1C5207E6113C8B17F1A77186549FB63617611C33CCF38102062094F12EFD292A7B0B98A1423B92DC318E919B3DF441E58A36C04C7E60B270A453707DE23958974EC3A455294168D1265EC6E606AC40C79B690E5829A0F72910A0F5031ED1A5AEF841C58A036A10025B419E0E2BB4C6782EC317786BC1CD538C18B7223345699D44860D0BA04413E70B7D5126D2237545E550893427E34C6FFA40200AFB5D527B80C4A3A32B42B49FF5043B14AAF6A69144D58388A254F7F05F1DDA51F980A3D5DA436540A67569042E6B42337A712FB65A208392016710B8FAA763321DAC533BD06219AC6C48B6929D54D7B6A4A46280D2B242A8C1BA5B2BB42487AB031558BAC899EC3188A1459DB434C5463DEB43BEA974C1167B83C657C93A9604114128256156EDFBC604000C05C84FDEFB2218203131F40483F9C3B06C0FD524129FD65D0AB55714052A24588184164AD4B5929668A5A6F26F9CF44BA946874A21827E5B68A71B21F20785308A3F8DD6EFE22012FA9F25E348661EA987E6455F85D1A368EF1789708DC7AA8E849A\",\n          \"dk\": \"372270C8D59DBC34CF01B107E7E711D265669C5C01EE729926C033D53B0790D05F97DBB75059B3BBB122B73309ACA75D91582C409B39886416F5D97219872FD564935DF0B30F0A5BE2B3B6D08AC5193283E70C93C25C558BE17A93020F375C44B640AE41D974EC2020640515D74CC083143677B32C73D688088B03E8455DF248212B7AA82FC01F1BA0AD96B9430711BF05DBCCB075732F700B124C6B298B171D84C4F338994E0611FEA9AD4244C968DB44AB2967AA7666B8652C12588A3E728235C7A9B88A4D3D7A6C05B46A0D53C31744ACED6BC02B372B364085DE5CC13B2B4B7BBC36C0B06127A67AB170B9BC7C7F938A0A26254808E35A6024920B463EC8D223ECACA90327488EE8246EE5B076CCB5A734B197D61676576A2A4714FE0B3D69B87B581660981CCEA31923A1FB3498802BEA542FF3F1078529161291B014B94785F6BDE14595ED2B0B0560614C547F6952AEAF44974B6365C488235CA23A93759C9F707CDAFB8D81D701B31B08E473852605A493B8CE06B15BB95641069C85B0BB7D826A6F77D743B4051210C742314B787C77AB9193A9765256402C4575F3910C63C92EB816DFB9821C324D6852A5792041FEB441102839DDCB4D49E6ADFB291395B12DD409756E7CCB67192A3137CDAB072DD5004C7742C60A68CB45D63EC2A85F4452A9DD1A06781200D9D65A049BA37A7349402175E6D82AE4F16F802598A1463B46C2A45F583C8F051A84C399F227797F0A0A5051A3B13171C47A8A2CBA99EDBA992E58A5B4E544D19944DF60A1E7A2CFAD69216F4377B48B4B04C206BE5C6F83EA47F702620015029AD3AEF31970EA739ED5E887195C8DC94CAA7B199AB88634326A8303F535B71BCD514C44FFA33F5613388D32778B3C91BACC9F5E659CD170CEFD174D76B096AA2923E99C1AB288CE5897C9066631E1D82267212FAE1221905A7454D753FA916F85094B9DD87972F49D832C19351125BD754047F7200FE049868185658318BEFB8D913A41C6C1887F79C02567AB9236B485148D736B063B6B1EEA0C5F9D8491D279264261B375A799144C9BA787B5DEA211BB85A2E3F49934F999E0884E019B9299EA5DED8B5374851D9BC24079FA481C534793D328BC3B3563D0922098B98AE26406BB81A48AC0D24102E1D3624F974FCE49786890AAF9EA2E9CC058133C66847B67EFD61C7B469DC8E983ACB6ACB98C25D2A84A6EA82DB780531D0A4FA3A84400C70DDB48CC52406CB3155EC059894A4C01902129D1614C3BC25372C730CDD6CA8B204B9D18793ECA8B080881119549779A67FD456520B1011C6609CD4329C1832F7ABA7997D79B9510409C920E8EBA32A476B72061CE58A8A0A8B369315A814982A519E88E8FB25B3C74B527776C671C7B48C3C9EEB76F2A60BB39D5722A71459B180BCA9A9FBA14B3AB764E5F931DA0A55FB247A93ABB8509334050A029CB7145D4141990929A7D224FDC905419D62828625E316778E1517B151CAFB1B8B8E5E96958447944E266F808017C7CCAED571C64D9BA153709C303C92DC69B808941A8C3C760E52EC7388E800159CBF8626F953E356A827AB8692FE1982F07884FE3B094ACC0075128B3D54D85C39C08D065B6E40B5475CE1F269A2227AD4B368C683A7E797A7857C26EBC1C5207E6113C8B17F1A77186549FB63617611C33CCF38102062094F12EFD292A7B0B98A1423B92DC318E919B3DF441E58A36C04C7E60B270A453707DE23958974EC3A455294168D1265EC6E606AC40C79B690E5829A0F72910A0F5031ED1A5AEF841C58A036A10025B419E0E2BB4C6782EC317786BC1CD538C18B7223345699D44860D0BA04413E70B7D5126D2237545E550893427E34C6FFA40200AFB5D527B80C4A3A32B42B49FF5043B14AAF6A69144D58388A254F7F05F1DDA51F980A3D5DA436540A67569042E6B42337A712FB65A208392016710B8FAA763321DAC533BD06219AC6C48B6929D54D7B6A4A46280D2B242A8C1BA5B2BB42487AB031558BAC899EC3188A1459DB434C5463DEB43BEA974C1167B83C657C93A9604114128256156EDFBC604000C05C84FDEFB2218203131F40483F9C3B06C0FD524129FD65D0AB55714052A24588184164AD4B5929668A5A6F26F9CF44BA946874A21827E5B68A71B21F20785308A3F8DD6EFE22012FA9F25E348661EA987E6455F85D1A368EF1789708DC7AA8E849A8034B3E7058DC6E140C0F4220A80B43CACD9758598E55F2931A11A2F5C9026556B16944CAA344BD9BB904392078ADBE511660D4F9228446D69DF2A20A6CEE850\",\n          \"c\": \"7122A73DFE33E937B2D3350EADC73B3EE70C3BC5E9C4B2E7DC590A491CB7DA736B3B37294B0F13013BA8FFD8B25C2164E8EE528044A230220D8203AC4D2ED48FF05C479762CE72DC62957E839580C7FAFA23556119AFA66A53655C48E6193E1B386E4689821F5FA81643B22A7455A8BF30523098721042830259D90B69E21F038607140030A9EEAF30EBC813835AF12CAC2E018F7EF30473D235E6631ECA0306D6AB9E45608DEB559416CC92A7B4D465CD56184B0C4353D8A8D96C257FFAD6A90E090C8D735FD32A14849DCA6B383ACA3FE0A9F482A5A5069AE3B9542E83BD873C3D3C0B052C5DF69D267DB237D65EAB2E84F38B4272F079F84FB6D64F17D864464522E6F79D2BA9C4F1C2E6A0EB8FAEA6CF8AFC71E79B084E77D7BDBAAEB233F107697D245EF9BA19E142C73C9513E711621530B040DCC9B70088436DA2564F97FDC79E8D062ED490778BE78BBAB0B9E71559C6F5A7A314D73C4E16CE627E88F27F1502BD90C001607214772DDEA44C59040DCE7051F0BF2BBD712EC82CEE54A6F41E19DFEB32AF373BFAC06469346AF5CD7B32A15B66A5147E0D880FC180C228ADDC6755C3957740CF7A41F83B3DB58D23B19A33A4275EC795FD1EA20CF6BE52F5070B261A68AA0504CDCF3391A84EAB931FB14EAB16BDA72F69E15364962DA5988E4F0C37C715E5805DDC9674CC1E44449A0E534FB5E4499B32B6B959DFC0E937F40C4F0922EC6B29D8D4C8676F2555FC43B6D860696294A0FE7776D4944CDDCE8133646A0DCFD9D9117595E542E8F82BDFFDD969DA7736724A7FF71E322333EFE3CFD97BC04E80968248BAD48BDFF5F8AA6B0BC1C5141A7B70755199F83B0A896B2FE547F68CBEDAFFCB101A1520E1AC1E6FC364078D626FF53140271CF9E6FE8299D58DF313C50D082D7EEF995DC34BB98BE6026EB87EBB6AE6B776F72C71030DA2DE3F9A84B45953F80EB0A5F06B7408E7F9BC9EBE7064AD8BD9DA234CF4F29DED508106416A6B39131862D4DE2774663658F02F53D85206C8A9A3B78AF18623574E109DBF54EE08659ED285543ACEA5DD2533045FB16EDFF387612CB0\",\n          \"k\": \"BD132E98714A75116BB032DFA0C7B0C34EAD0780C576DF9EC11200256B4BDA87\",\n          \"m\": \"60363F5CDB16BC516A1367DFCE1B72926FB2189B88AA1DEBFD22F440B9CAF0C2\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 16,\n          \"deferred\": false,\n          \"ek\": \"964B324032B0EA05C8042A49F702AE0FCB19257B1EB97079994A9C67043D99CB08708A2F64E694CCA51B71431DD20A77BAC80AE5A259EB99517020CC8CD53D525454A1B6052D4C598D9C504795835D9A45CB5CC7BC6313B857B1884067B0D97961108E57AB00DDD8AA5A69356093348948208947ABEFE117AD92C8562B5DB7DC037AE89C76D00C4315548372A272C32B30168620380CA0A697724913C173AE31AAB4B3B54F3760246A310F199027A72BACAB7046AA702710C145EB9A6F312B3B8FC291762BC301C900E7AB41D298A01D890AC3C85A40D0BE406000C6537BF9503E70986861D90B2EE3C8B0047E66B8B88CAB3AE9FAC382C23D06B835789BBB47A94823180859969535D058E7946C69438F89236AEEA110E404B51C1C22ED04CBB2EA32C957795797806F179A38642023E8AF6516A11393A0256A7EA3C27A8600A669FABB89E144E30A839D391E6C1A1F7F96A1E5FCCA889C8B09C3779B407075D97EE893AA4CAA1F5E5CCF3C5CC4DC361C8C80525BA81C05488012CB61D3EC87720A2CF0977BBA49B8F9D81D3EBB50FBFAA420E27AFABC387EDB87F114C1688830D4137BAAB10D1A6554179398B2D939C8F4387AF0007CEA2AFA9BC56E2766182B0C8D876123FB1A64C405CCD7A9869466B5825E23F41C4BD1747D5CC95C27912DA559535B195B96879F8C14F6030020278DBA6CA371C76F6FB7059F0B7E1EC67AD410B12F36878274C0B19A2F571A49A0358B945BBCAC5142F131C94E05C6A96C08DCA52819640431930D4089B2CA85A4FBA62F2B2614ADC7C4C80B06745320F837CFDBCB0A2885C321738C2B4233085A1B5F4AAAA658354078CCD008C496F0006AE08FE917BBE35433570B4C1838AB48593466D58F18A43A025355ADD4B333EB8146251E32FAAD8742BC847A7916212D5DC69329D0A3AE57147628928FD0B08752A21AC308515229EDC169FB6141FFAB3347F427ABC759C40C8D29448A273441A9C53654042967087F470024EC714B0708C033436995AC4FA678A9FCD724F703411454C04C21764C1B243146587C774897C6752BF79868846811106552C8AA574AD8DB7372836844DA6BABBFE62263107076C4CB48CB256D359D08F68375BC\",\n          \"dk\": \"879036ADEB2F85A51E3AB14979E71A494A8D3DC4CCB6B6A35F826CB03761A1A799F621957DB260133988DD12600DC33602226CEC50434BF059E3B11FE5F19A42BA1DF9164EA118090FB7A1AE857AB7E8ADBAF66DCE7CCC01B8BFA93153D3E5A644C56B1116058F47C663CCA1F54C3184BBB0FE8CBD05E53265ACBE72CB37286929D85B856CC8587DB46DBB3622930465775893FB7401E9E98430FCBDB9966DB60280C8319349C18C62E56DC9BB538DD296A47169753A1394680D202A7EC33ACAA12B315BD69F93A07F77B9C6396209FF4198FA88508A4548F16503B1C4079F7086CEB9BB4F751A0B8175B3F193805685F098A45E0BA16EF3828599842A240B05A201ACFB45B63C4C2D6BC35EC36D69E4142666B8600AAA123B6E6D7C15171BA2FDD94B0205BBD4963A45CC46BC089BDF817C52933E36DBC911174EB62CC4AE4AC8C4E45782736125948A97A2C66E7C9A756CA146253D8C07349A6C4D4D97BE122BA433DC388F972CBE79695C878A23537F33C3B339879BA0481A41032E961618198529DD154FB38B918A019A0FB75827084881871176E02C1E8ACB746832F44191C1B36D2B96CB630499E83007636AC656707998F436C1472EC3406515276E7A4840DC509123D2907AC7424416A656A74E49EB478A01750470ABA7F5AA416BB99A50CC6FD823AC1B9178376A8DDACCDDD2975CD68724FBA3970216983219B70A4ECE563A00C44CF121880FC8C5936124DA62008BAC6319806E1368B4B8467CA8980962C826FC20761D131D6692C2F1907BDDC83F2CEBCDE692C8CFE6880BA357D1D056A022A1D3A4C0D7F638161011AA83B18500096591A1D6B55110976CBE0A03BB78AC7CC34FC5774D794B3871B94BC7C081D5895CB44C0EC7E9B2782004C5C61564F582C537C6B72B5B0B924799181D6D012EA130B2D5E0C998F0C3CD829BBE48645A6905F6D240AA518CE42851989C987CB9CCE9F71C43328703F5882DD13EA0841251FA4177B593BFD58AFD146631D876C1D96310CB2C6850783C5939A431483DA78E09C3041880B1C3686B0C45B442427B61443D957C01A580B287B1AE964B324032B0EA05C8042A49F702AE0FCB19257B1EB97079994A9C67043D99CB08708A2F64E694CCA51B71431DD20A77BAC80AE5A259EB99517020CC8CD53D525454A1B6052D4C598D9C504795835D9A45CB5CC7BC6313B857B1884067B0D97961108E57AB00DDD8AA5A69356093348948208947ABEFE117AD92C8562B5DB7DC037AE89C76D00C4315548372A272C32B30168620380CA0A697724913C173AE31AAB4B3B54F3760246A310F199027A72BACAB7046AA702710C145EB9A6F312B3B8FC291762BC301C900E7AB41D298A01D890AC3C85A40D0BE406000C6537BF9503E70986861D90B2EE3C8B0047E66B8B88CAB3AE9FAC382C23D06B835789BBB47A94823180859969535D058E7946C69438F89236AEEA110E404B51C1C22ED04CBB2EA32C957795797806F179A38642023E8AF6516A11393A0256A7EA3C27A8600A669FABB89E144E30A839D391E6C1A1F7F96A1E5FCCA889C8B09C3779B407075D97EE893AA4CAA1F5E5CCF3C5CC4DC361C8C80525BA81C05488012CB61D3EC87720A2CF0977BBA49B8F9D81D3EBB50FBFAA420E27AFABC387EDB87F114C1688830D4137BAAB10D1A6554179398B2D939C8F4387AF0007CEA2AFA9BC56E2766182B0C8D876123FB1A64C405CCD7A9869466B5825E23F41C4BD1747D5CC95C27912DA559535B195B96879F8C14F6030020278DBA6CA371C76F6FB7059F0B7E1EC67AD410B12F36878274C0B19A2F571A49A0358B945BBCAC5142F131C94E05C6A96C08DCA52819640431930D4089B2CA85A4FBA62F2B2614ADC7C4C80B06745320F837CFDBCB0A2885C321738C2B4233085A1B5F4AAAA658354078CCD008C496F0006AE08FE917BBE35433570B4C1838AB48593466D58F18A43A025355ADD4B333EB8146251E32FAAD8742BC847A7916212D5DC69329D0A3AE57147628928FD0B08752A21AC308515229EDC169FB6141FFAB3347F427ABC759C40C8D29448A273441A9C53654042967087F470024EC714B0708C033436995AC4FA678A9FCD724F703411454C04C21764C1B243146587C774897C6752BF79868846811106552C8AA574AD8DB7372836844DA6BABBFE62263107076C4CB48CB256D359D08F68375BC6D63B8A32687E3ADAB407548CE8B83437F355FCE2D96C1BCEC6C006F7E493B743ABD1651588386750AD3B35DADE74C328DF82778D99596561ABD71F194AAD28C\",\n          \"c\": \"4DBE5E9CC3989D6CCE8D9F491B94985E770AA2ED9D214D45B03832D90EC0817A9F06856EB4D6AD8BEB6B64B7F1892EF9E13C594C7BC1B9222C655F259603868047D35ABAA67FE36816BDC493502E0B3E4129DD58A8971590E182B71386F6BA4F4580618EF186668C0579109E6DED3DC1A8F22EF6FEAD9D20D9435C144CA8D46367F178CD0957A638AFCDA69C96C59B3FE03557107915629E21F148603EF68FFDE3327FDCA00A6B1A3E498F90FE7634E56AEF588DE8E9C567D3C9F8E5E66A7DA505DEB1AF29DB37CBC5089C27BDCAF1E06614D796B89B3C1D9B40294FF479D6E64BEA14520E734600CCAEF6AFB0DFE7B66291F79859E988A4FC78431EBAEF90F511273B16DA1FB42C404D793EB36414A40DAFEFCA07F1787FF9454FCE27EB142B985D21D8E086ECDF1B2DB9B7DB12F1BA442BA3C8C16613A8BB7D4F155155F6AED9196E54BA77027B8C040E6A047AD6F41FF174FF3CA4F38A167221B8715FF60661E744EA43F49335C3E2197E57D7A1DD5CF53DA8D3ABF359251675461EFEF9CC2F033B6ABD73B35A8CF740D65089CCF5FBB94DFE1CCAF26AE43F7E5F630209C7A6EC93753E808345AAD1068D18DE64BFC4A8BAA6EB6E72D97824216F922729D9FE73401EABD11113FD7B69AB97FB10525F273EE35B2390446EFFF9BDEF7FEBC416E6668B06FF5AA7A5C50DDE17554606228B8586BC93E6A7B9B239D8DCFF2E4E55ECBC75C9E449F25E48CC2100C3F130429EB3F841EC2C1A8297769AE8CA81DE932833DB2CF10C9EA57A682A48E26E3D42D9BA9A6D61C82F4014B804B8AD8619CF84AFD3645976ACD87D1B3B7D40CDD87C35C2E3973296D22851C80F84B792FDBD81F549968E2BC6DF18C37F50805D29CB657DCD9C392070FF3EB4EDEBA8DB298447A713E517B94B1CA4C11CD462B2ACED35C4AEA82D65E5D5F050CBD7DD8B40BE07628025F8A352CE8A15D8FCA7FEB78F9D6A1D179FF4AA336CDF3925FDE9631BB53649DC65BAA226CDA770E50ADB9E2E3651A4E6776748D76F9444960FD14B9116AD419F6E5A4D74116890F05AC8ED0BE52A1C70DDECDEEE8C6914B40B1BCF\",\n          \"k\": \"CAA24999EFE659AEFCF18FC9C722FAC1D5DACE583B716AD3828B15C7DF5D94DF\",\n          \"m\": \"579474C123B3381801867203E0021E2B7F15E5F9426D75A3EDA6CBCAECECCF43\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 17,\n          \"deferred\": false,\n          \"ek\": \"87EB24113349A40704B7243F2AE3653F24B87EF50E2220924BE06BC1E86E8AD9011768B255969F4F29C21BD38D6687756D795F07286B4EB04C30143C4F76810CD3233E630417F86EC751CAA87806CDD93EC0B5332758CDEC3A37B4F84878F89A4E687F0E457E45602517B676FC944E0BF9A9E9903EF895800D019304AC223E37623EE0C7DB798AA1A7452D298036A4197E89B0F279A385DC19D20BCED386352FE512A9EA90F973B6890964490538FD82A1748C98384C38C1434EECDB6EBF561366C228AD0086D142966BE1B8E1025097509E460850087935CF543A7309342424394267032A84358CD76E4F98C34B0005B39811233CC425A237AB49CDA9A884DEF6832B1A2031F5AF07DA4C23681B561C7D91A22C65129206187EFD2926016717F1F318EA95CC037208D2B313E0BBC59257710EE99F9B81187837861404CF4E930D0A4185CD3779CD4830CE2A02049AA0BBA0BF06003DF8A358CFA4CB2D3B7C7A508FEA1B930D1BA379E246B9BB793D1C2548F8BD908315BF521200261E42C843D963074CE4CBB3A5898976815CF3CB0DCC137F0CC414ECAA8883CDE897649E81BF707216FFF23B124B3FE6A39A45EA317BA5ACF8116E9825BE6AC518B4658D5D23CAFA600F94A9ACDDD38F303978BD64B4CDE34E73334C1B212060E29085925E6A3319EAF72097826F4233CEF2F4AB1BC9CD99B9338FF43BB9EAB9723140CCB86741C44E5D083CF6FC97D071CAB3135F6B358172468DAD6834FF3A1AD0750F06837EC25C6AA4F179D259A0FFBCB4E6756B627BC1F9F76DFCE2712F7340F6AB2109F023D6BAC5F0A6A63063BDA11625FD93B3E8C7315D387AABBCCF763237D785A89130A3DF63AB2F587D8AC4BC90B795C3639EF2352D0314AD638A532E625A78F296B5C5A38BE65995F8859F279795D0795433BDDACA847140650A72637DEB3529E8179CC056FF58C3FA094678526E12E791272BB623367D0103A68F6811B0636AE101090B2480EF8C6C59B51426E6A3E8C5303C3200EB783A9A133AD2E5261B5371FA338B6D016367650BE0D4A93BF3CD9C48629F8A448AD9AE86258F454BB2F60B1BCA578FE20B26DF0732C3222BD4B8A71A2F5038031BEF9EB0DFEFB2666C\",\n          \"dk\": \"77E8C08006BBA3A7423AE68A67F48336EB0F5FA3B14F931F8B88AAAD384FBCC3CC291480228079D4179A3B6A0E555A4D3E6A5C8BD72A5C0779E770C56B53BAE1974EE047AC3DC953525C03C5ACCC0164C02C65B10407CBEA6AB0F9A413F0E169CCE31446BA219F765B90353F8FB327F9F8C767F6BEA749666FEA81D1186AFEB5170977BADAC98E0BE57854431756C042DB86430C27A358A3BBCF823261C8017FC8B81F9970BF9B73433531C13AAE0A960D82C06CC491865CF4450BA6641D1729B0F7769D073FFE99BF2A8C8D40538CA08318FD163FC5F12264266710D7259F765DA6786EA386C5E9543BEAA54FC785C1C83940FEBB2C866B737DB93D2E798E03A52645330901D118D0C973A1E997E5BC39967577E51C752DE187EAAC8E5C167BEF272F79483A658B6382B93F67E07E3182A183AB9842951FBEFC8CA6525D571B9B7C995D89366AF8E62B371B6A5A0990F3B7AFA054B997E921AD708623950EE7A2C6AED3316580735002C48A298AAD0174DB794A5C862092061E0C25824AE84B6BE824A33911A112AD51509E29424B4D933946528224E64B4BF924552706E7307B4FB7B8C2D083B2245A5787A19100CE78465C3B663330006C8BD0B626CBB9ABFAA3F551ABF18AC146081ED1AC99A37944915CB6E1713DDA4049C3F0CCE4611E6A21458124A850119CD5B846867C6DF0E9592B0A1AF7F1C5210C6CC2E5192A44778AF59BF90935AB8812E811B909528A402174B3747ECF6CB6DBC58DFAF15CE849BE309760A0F9274409C8EB25A679764090E59F27553AE4B31A00DA84B25A843F23C6D9024B23CA942EFC05753123C703391C21B539C9973E14611562036C793CCEA69B0A51785664C0367B94BBDC45833BBD6F132F1A34A31134A934E72A6F988A36A9BA6516981F242F67C33C9F71C113D40380147274427F429A1F26FC5C5EAB9379F0A73DF442B4B2A299E827B0A522DA24690DE17F72A547E9E228D98541C5C02058B1879D5946AC83B338277B6362C72371A6F866C2C72481024A4ACCD0B91AA560249CB5B01B0BABEC227D1AA1D6FB5E878C66214A9DFF0BA989C78987EB24113349A40704B7243F2AE3653F24B87EF50E2220924BE06BC1E86E8AD9011768B255969F4F29C21BD38D6687756D795F07286B4EB04C30143C4F76810CD3233E630417F86EC751CAA87806CDD93EC0B5332758CDEC3A37B4F84878F89A4E687F0E457E45602517B676FC944E0BF9A9E9903EF895800D019304AC223E37623EE0C7DB798AA1A7452D298036A4197E89B0F279A385DC19D20BCED386352FE512A9EA90F973B6890964490538FD82A1748C98384C38C1434EECDB6EBF561366C228AD0086D142966BE1B8E1025097509E460850087935CF543A7309342424394267032A84358CD76E4F98C34B0005B39811233CC425A237AB49CDA9A884DEF6832B1A2031F5AF07DA4C23681B561C7D91A22C65129206187EFD2926016717F1F318EA95CC037208D2B313E0BBC59257710EE99F9B81187837861404CF4E930D0A4185CD3779CD4830CE2A02049AA0BBA0BF06003DF8A358CFA4CB2D3B7C7A508FEA1B930D1BA379E246B9BB793D1C2548F8BD908315BF521200261E42C843D963074CE4CBB3A5898976815CF3CB0DCC137F0CC414ECAA8883CDE897649E81BF707216FFF23B124B3FE6A39A45EA317BA5ACF8116E9825BE6AC518B4658D5D23CAFA600F94A9ACDDD38F303978BD64B4CDE34E73334C1B212060E29085925E6A3319EAF72097826F4233CEF2F4AB1BC9CD99B9338FF43BB9EAB9723140CCB86741C44E5D083CF6FC97D071CAB3135F6B358172468DAD6834FF3A1AD0750F06837EC25C6AA4F179D259A0FFBCB4E6756B627BC1F9F76DFCE2712F7340F6AB2109F023D6BAC5F0A6A63063BDA11625FD93B3E8C7315D387AABBCCF763237D785A89130A3DF63AB2F587D8AC4BC90B795C3639EF2352D0314AD638A532E625A78F296B5C5A38BE65995F8859F279795D0795433BDDACA847140650A72637DEB3529E8179CC056FF58C3FA094678526E12E791272BB623367D0103A68F6811B0636AE101090B2480EF8C6C59B51426E6A3E8C5303C3200EB783A9A133AD2E5261B5371FA338B6D016367650BE0D4A93BF3CD9C48629F8A448AD9AE86258F454BB2F60B1BCA578FE20B26DF0732C3222BD4B8A71A2F5038031BEF9EB0DFEFB2666C29148150CC61C6C8B7FD408B3C9B21B6BF530E9D9AB72573FC6DED2E4A10C4C0D01000728E8DA5326C713E45EDF82C441D51791E0AE7663DF7E931EA208B7313\",\n          \"c\": \"A707FA4EE57BCFA296EF6D10B848DEE8A48BBF84A465529F837977FFACB3E429D0D2C58BA2A10406995B6328AE91728087648B7F018FF9E570E533F982EB58FFECF6BB104CF2E6D819E3E17CFEC29F31F275C64450B5D8C14E231C563F03FB978B214A51DAE1118D8BD235920B401A706F8C3917D3CA066CF0D1D6591893B244ADF6F0E514575E67BCE3CA8217330153EFF5F91FBA1C23CAB3906F2DB8BEFE01742EA2DBBDC0D3B7127BCA805430792E30CC2CC7C1108EE02CF429820C232A65E4A3CED4949FA184A8B624EB4C4B72DE88750FF7565E35A54E71DC289AE7E59F9F05A915BB0B35C7FD36967EBEFAAD806779A116DEEB462306E3757C94F2B6EEC836DDF1CAE12E3AA58F44AF495F410321661E5451ACCE0365C74EE70A630E59A16CB48532A8A7DC7B2276120450820CDE94FB32DE747E643176FB02ED2BB06111E56627E790C304AB163AA0B424C280940459900F51B95FFFAEB244B31873A5C452508E354FD8C7B1C9DFCD73B79F9D1D5A76413CF25C1E378461F075990599F452E0221C0C8ACE25BF0227632C667D8930D12E5A136F8EE42E19677FC3A1ED91D88238527F4F5B8DDCA69A9E25B2AA84719F9D6550D57D8B2BF8B42C3B46D760694A15FB894155F75FAF61E67672A5BD8AC7C1A2F82812944558701181A8F7AD48E1C5E3048F1DEDF19BF5DDB322B0A5559616DABFFDABA2AF717EEA488379E75446524023563FB1CB34102715A63F1E2966C72EBD6A7C590174699BCF325C627970DFA7DB82D8FA9C39D82E412FA7827CD6CC04D23607985B97E5E5E368A23F25FC516BF771DFA1AF4CD65794F72FF61DEB1541001C08C8038E633800967F6AAE7F9CFED288921DB4A8CBF0BA1562B93016DCD051444F4E23817EB081E23309B044390E33F8D73AADC7ED244239B090740C30C73663170E0703DF06B886BBE4735BCA02E44E25382A18D6F18C0CF9CD452DA692C5958CFBA89F84E4BF5DBFDF2B3002318E08E8215BE0E5D770439BECB122A8F8A93CD93D3E8CEB6E89F83B224CF6D7F7526B1181E4D7781FED7A0177BAC4FD82FC229E6A8A61FD4A958A2D\",\n          \"k\": \"F9F4E46B44C781A74DC60E149C81047C89C75469123ABC787DEAB36EE769102C\",\n          \"m\": \"E2F0D46B6C4A43E94CF967EF2BAC7B68C6E0424A37DB52F2BC0C1695D1A66B67\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 18,\n          \"deferred\": false,\n          \"ek\": \"FC93AF4B238968BC258C1CB5B94592501A51C3368DC9041A56DC5E5D230343380E287396E9B821270633DAD37C920182C82C9236FB28BA025DE94A8E552A605AF1393C988E08419BAB6C8709EBB41076192435699F1C48DC149D3F040A5A36ACC7C869FAA5AB3507B8B32A6847EC901DC8B9B19A04B70865955C4B3B01676DBA6B463C36397B2A42218BE0DA0B55E1A5ED8975C002A9520C114CB672DF5B3A7F838911F7869F148D51DA2584AA0C15F4C2A8C95653C95970D2BB4805928DC02D3DB050D6338A50B6C76332911E639DE912656B72619DC66790F491E0DB15C258C000650DE47268303BA52A49A9179A4DBECB45E7B47A74A35DAB781F52D27CCD2B6C75474474DA0374E25D21AC152E5AAB5225BC7EC5A16F5A1FEE930C300A8A15C090CEEA999D045D80CA73B5484C9E8823604AB464404F9A9609AE920ECDC688E17090CA6B7DF2108151E04BEB6A27F7EC8ABF95BF84D23CAA254A93F89DBBE133A6B1862A20B2B7241ED0695C31B17A7AF1A6A557BDB431A3D2DC06580A8DFB52C91592A80E3C1995E5B69D03AE3B8C13F37AC4852C888E077C290928420A4170A377F0A519DEE8789FB7BFE6A214C394038E281B56AC36B5474286A23142E14561D494AC2CCF95322919A34369B94D79BA787194A471F3A3271BAEFB4019FFC040C831447FDC9BC19B948A0710471A233F0B7104826A83B881CBF4764B1BB4441A843255A8445A034AD6972F4C66C906550C1C5E0D30B718C339618B3564456EFDD0AF36A14D7B705E62153F5C880F017740288545A6D51B13A671EE00970217BB3B33B69B56C823148A60499CFA960B06EACB3F9224275BC907D89E4722C1E35C425DB0ACE65B7929F66F82204AA2F0148BC0B7126B4EB13C4692EB3732E3B2559AB0B833AECF072C2060B1DAB6A9F8BACE386A098CB9B7B7447E6957B6219C6607B3CD83BB64A4B12C973C28DD7933A4B95AC3C7C59824A32E271007ABB369FB06FCC82B20D38BD436B54B155A5FC25381361D13B41D0C4B309E3A4C295C18B4F845573051C81B0E6E37A0D7869D23412F9CF55B3AF5121E491E45292535577EBB8EB2CFF39D783AE72D468F687BBFD838E6A61F5C5B95FF1F20379091\",\n          \"dk\": \"F38BB156C26C61164521D1A109AA9B8108251877AE7E9878ACD67C3B497E40323A94D891BF700CA9E1A2219C7BFFEC710F575AA6A3BBCF9090FAAA82D2F0544F325A8B72A5B2F616CF87A9B698C7D5790974547EC5A8963D9649B598370861AB3C1628DB704368A256542B944BE03E28BAB87B6C28AB08ABFAA65827C5045871CB9F7911B81A0D577C1EFE3AB8FA5671A1565AEB286CE9C2175E60CFCDF809AC7384CE260D904C56CF6036AEB18A7B14B3B09C902337707BF739C365748B08BE40885DA99AB3F909C6C9D401ACE3C1816B2D2C8731BF46910A636D5882460C061DDC175290290B40B2AF1CC80CBD1C3BC669339CB95FD3D320A663C5937CC6D0C98D7D211D9CAB2D9D26764DD446893646569A14DE84A7BD7037DD4C644B633D4BC23BACE8308811BD0232A8737A011E98C2F82627BBE173640BC6A22AC957C5BBED4433F802908ECC8837152152C167C296C9254695523AA427A919D7942AC5952EC691697E0216F7A4C0A8C3A6D9B9CFBAC6A47EBB6C80C05BA64B2C94CA8A99F717B1E503C8185A3103A6A6C229D34753ABA4A2D1E9C977EAB176EA1B3882051ABC9C37ABC0D9F29F01753730C90E811391FB75B8EBDC3A9AC525BA3A02B54378BEF835EC51A824C63C5D8509A394183176004A2A0D0A565B439A909A5271D1A3C7CE3A9C09F809C1E2220D28AC38B52ED6F93D73D7A97779B61433CC709323EB8349D5902242861F99E261744A4EDC58073E676D1D84BA5C070A87240F6E82BD700B7AE7EA08E720815FE820A5629CBB498509AB53F359526977B09EC7A33C78CF3985BDDDCC86770106F834900E6A41622AB6A4C2B02F8A7AB54A53A0AC112180B5CA02452056A421F140C5B1BAF5EA0BA5B34B95B7CC71678F31E97C95056C1E4569B3359261A1AB1DF4749358468526CFC922C499C97888588EEF14BC31DB595B71742BB038CAC479B79B3083E77460C9CD860C1AB4E2595A99942E97A40CB0743E5403A9749B5DE2ADDB5A1A1A850353B213F4B6667A561A1E25266773577BD2367193A156DA92C9E72E036BA63D103BE6E0AAD5325F9F5A55E1D749FC93AF4B238968BC258C1CB5B94592501A51C3368DC9041A56DC5E5D230343380E287396E9B821270633DAD37C920182C82C9236FB28BA025DE94A8E552A605AF1393C988E08419BAB6C8709EBB41076192435699F1C48DC149D3F040A5A36ACC7C869FAA5AB3507B8B32A6847EC901DC8B9B19A04B70865955C4B3B01676DBA6B463C36397B2A42218BE0DA0B55E1A5ED8975C002A9520C114CB672DF5B3A7F838911F7869F148D51DA2584AA0C15F4C2A8C95653C95970D2BB4805928DC02D3DB050D6338A50B6C76332911E639DE912656B72619DC66790F491E0DB15C258C000650DE47268303BA52A49A9179A4DBECB45E7B47A74A35DAB781F52D27CCD2B6C75474474DA0374E25D21AC152E5AAB5225BC7EC5A16F5A1FEE930C300A8A15C090CEEA999D045D80CA73B5484C9E8823604AB464404F9A9609AE920ECDC688E17090CA6B7DF2108151E04BEB6A27F7EC8ABF95BF84D23CAA254A93F89DBBE133A6B1862A20B2B7241ED0695C31B17A7AF1A6A557BDB431A3D2DC06580A8DFB52C91592A80E3C1995E5B69D03AE3B8C13F37AC4852C888E077C290928420A4170A377F0A519DEE8789FB7BFE6A214C394038E281B56AC36B5474286A23142E14561D494AC2CCF95322919A34369B94D79BA787194A471F3A3271BAEFB4019FFC040C831447FDC9BC19B948A0710471A233F0B7104826A83B881CBF4764B1BB4441A843255A8445A034AD6972F4C66C906550C1C5E0D30B718C339618B3564456EFDD0AF36A14D7B705E62153F5C880F017740288545A6D51B13A671EE00970217BB3B33B69B56C823148A60499CFA960B06EACB3F9224275BC907D89E4722C1E35C425DB0ACE65B7929F66F82204AA2F0148BC0B7126B4EB13C4692EB3732E3B2559AB0B833AECF072C2060B1DAB6A9F8BACE386A098CB9B7B7447E6957B6219C6607B3CD83BB64A4B12C973C28DD7933A4B95AC3C7C59824A32E271007ABB369FB06FCC82B20D38BD436B54B155A5FC25381361D13B41D0C4B309E3A4C295C18B4F845573051C81B0E6E37A0D7869D23412F9CF55B3AF5121E491E45292535577EBB8EB2CFF39D783AE72D468F687BBFD838E6A61F5C5B95FF1F20379091CF21077C6E3D08D75668EB9DE6088C89F26636404240ED78CF9683E58F178427D527C588E4CBF3A4A4F983B4DFEFB28FAAD96A659A16B403180DDC7E49391AE6\",\n          \"c\": \"34F172C9C056D82BD5DA9A1EBEF6241212452C78A2FB05DBC7C234F46847B3C3B8A1DD0B3316D4C96F84FF3F45B9A8E2BE97417A58946A83892A39C553C59B20164F64C37A3BEA9A14913A6F384AE5FE4B3E00861B903FDA24D740C29F086D1A517B24FB1A101F5855A9D2FA1237472595889F9826C6C5DC0F0FD14A359B2FC4A39A49BE7095E9CDD57D112BF4792433078CB93FF7A36BF5500B61E94545E1578C3817D81AC2E86414BE0339E26E9395E65957370762A5AD089FDB6C74960E7D6AAD7FBCA78833E69F0FCD60A581E836EC41CCDAB3659E422CD2EA42F95D86D79A5974DDF913E6E85061C29467BA1610B5C81E5A5E527F7B7BD1E2B1A21F64E00E11D7ADD5EDCD8898CA3CF5E497DB64DC68502D6183F583FB4BBF7826F8ED843F99634FD6E00DC4E9A87E0271777C7980FD2E72ED83B253B6F0BFE363413E9FEBCDD261ADED6822EBD9501A0C10EF825D4D20D6DAF36068AE03C9B8426939B81761689A6EC6019389B99BFD1DA02D3B0725FAD3DB4B9DF9FE5F291E91414B81B3E64680CF7DA55CFEF76C14D883C7A85299971F328402CFA1EF2064737AFECC27E4A49074C47F08DFFCD4E3AE86062BE0802F7F0FC1BA9C4791BEDEDB83BF432D9B81925C968467A42CDB2C7CF581C2B645933CBC5B03C9B285B6C559BB7985C0CDF7C242A908F0B78DE6DEEBB9BA848F8B3BBAD7A4663BBD26540660E1160C918EA19DF06C64395BE4A439F9963F4982A6EC981F0FD844F1C6FB5507B54618ED1491710ABE264339AB866D393C0FD953AC8B38AEE24AFFE1988F988982506E5D7CAACD8B5A78E13F68321C77F8AEF760B8D45CE5307CF6A3DF13B2D77C6901847E7E9715D1B84DD43CD7A806F8D0DF99B257F8F34C1F2E7891B226F54562FF3C48A05728020E768B863FBF5A2331CA967D55DC8F3468CE8BF5ED401D0E98159C5882720CC34F61FF9076256371377A179A25228AA5450C28AE27826491ECBF5174D70D94C5B6E4AF04853DD89003F7FBBBC241CE87B96AD6F6BB0C3407E448F2E75D2A040F7978B8FC717F69B3C1124FF46667234B2D7EE8946FE63BB18E19\",\n          \"k\": \"52C8EBA213E652AC3F3CDDACCC5586E3C26332A4BF5E57B69421E6DD45C5B873\",\n          \"m\": \"7B34969C65DB28996B6F9C440DE09074CC98DB4F08BD43E4CD948EE4ECFDE8CA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 19,\n          \"deferred\": false,\n          \"ek\": \"894C58C872A67C630143FB29F5C2533614AC1ED6CA436463EFB38C7BF8A8DA348DAF4BBEF00A8E61E650CA9C6BCAD30CD330CC2D032ED7E42F41A9967C3AB568A0301A5A12EF70A2A6EA97B187691AEAC151029CBE350065B0C2239B67E303050D92A692237E21900BF2901A69231B14F04901ABA8D22A881365B943BA429AE31A72B8701CB07029E129B5D3BECF9090A70A5D3A86BE1BBABCBEF35592A7A74C2CC0560C0EC2ABA3095589C7DB2A5849CBCD483730F479ACD55B212C942F900407E46C0C584200C1CAD7D05C7DF467FD31C31E266A50C375689CAB1BE672774C77062018E74CA1BF6BC5F223802EF93CF7A1383F548196A9B4DE958C49014EDA8501BC8A56B2E1A2B0D46E18101D68816EFCE2297CAA5CC509399A0152232026B754A90FF104B9189426742C2FD8055191BC162B04A262C3D23B996C2AA867F3154521BB19E0A69647718FB76EA5B487C930A454943F0FECA6FE163F40557AAFB9785DA0B6450B0BB4095BA23AAFE83B731356B290DB64B4C39DEB5B30FEF38086E6841B660D41EA593E1C24028C60BC44A6D83696F88772425235A4F05A1847AFD5C84DEDC0B4C1D003F29AAC29710AEDA5053A4C0AF556C4F5508C45FB20141B1FB641AAA6EC6E88D0859553896C31A0206631750C954EE73B415CA163D30FBB75CFEB15714E3330AA346F7DE02014214C2EC5AE550906EC5632CF429BF3467FCB0327017703D08A7F3E9A7785D0C9D6377FAFE527259A0ED5894E2A8C766F688FE2574DF57108C81C60A8705DC7D102891437CB2B9988C879D7403967A83A70CA48D6F1B18A3408BAA13BAD9C26297CC10D920F0C53CAE4D85C6EE0C95DF22321F0A05894CB97C4A45F79CC002DA373AA857B65C726F54B03184934B11376852E9FF58099C72BBF531943EB5626C2C3EF6BCA145A2896B4C1BD29A353F49EC2FBB746134C1F50AE6D771160E64848396A5DA65EC32014F9669BB83AC52298B24DC2AB0BA2C82C0B4017B34F9D979D7D07659C4B76160999C47221DEE91521418948CB2EA72532E6A1C273B764D3160EA072A48782336B648C12B6AE93B5AFF289F81236AF0F9A338E8EC35154DB40386949D6E32A71D635053D5F55990C92\",\n          \"dk\": \"62BA496B01A577F941C19004EACA6997F59F417945028925FFBBC684CA3BA6D1A1B9427387E7B7E84A1AD2E00418D471BFE304428AB3D0F1587411BA3971679FD42DDF418546B6681AC8AE63774E62F16460402A3A4730505A650D23B2E14534A1D00CF974A0BA035DA2E29A4A616F1E7832B3C37181967DDB5C37A18023EEB07B0E645FED3A34EF4544F336960CCA7B83A2139DD671C8369016A1BDFDB6980184B98E376A92701D36D07E6D0964ADEC327EFA08D0F84283A58895466A47EB084E042938450D5A2209962358A719B305844210971766C2A756D372B1228302601853C37B3F1AB45571C6F714528F223BB6F8A74F91BCD4840E61924872F7B7FF7469BE8935EB743BED787E4FD753D56494C44591B15990A4F9445856C7938CBD38D546CAD98179C03DB1973CB4BBC3DBD0AF79B28386240FC008357F346552F21ADB551219433E3950675F100E10345B942672205924A80607D6160BDE629EA14C012EC62817D28527A80BA5081F7867AD3516193E80B6EE726428F6409BC6B0A391A6DDA32839A9B6B9F23625DC1FE2CA6FA021BC1A44CE6DABB640E480CB0B5306E5A466BC4C16BB51CFA79AA5467753A5A47873A11B0C670F1C584C4AA39C91A76BC361F1DB598BA63FD3490517DA397F36C669686BC6122131E713AB8083C3F10869C80689B5141BD4BF1C92C22D9ACE8651AF5A236132428E2C2A064C977143528CF9233DBE4B8B913A69E8C34D97D5309A9B1EDC5915EBC42138C93005C2C4BF7C83E857379529A6FC124C4297568EDB7EE47C031308A26B8A17F3F0BA1D217142E300340945BE8B0A85035C5F926ACD21066B8248EFD730E842772996259BE076BDFB1398424360EB38DF029E9893A274E20A44A87CB4FA5E90A55099F96961AA5BB6E69913C7B9117697570A261A4A24C3862198D359114C767092824BB7B7684BCAB4B7017CA3172DE25C187B4728792A305C966BE64B7F2A8A22432136B864E75A14AFA433B7A1C617D80C3F20707124BB3B53CE17E605DAEA53A6677530305C56927D5F5C8493E33B1C37B4D796955CD4A7DC60C56A44703AAAA0894C58C872A67C630143FB29F5C2533614AC1ED6CA436463EFB38C7BF8A8DA348DAF4BBEF00A8E61E650CA9C6BCAD30CD330CC2D032ED7E42F41A9967C3AB568A0301A5A12EF70A2A6EA97B187691AEAC151029CBE350065B0C2239B67E303050D92A692237E21900BF2901A69231B14F04901ABA8D22A881365B943BA429AE31A72B8701CB07029E129B5D3BECF9090A70A5D3A86BE1BBABCBEF35592A7A74C2CC0560C0EC2ABA3095589C7DB2A5849CBCD483730F479ACD55B212C942F900407E46C0C584200C1CAD7D05C7DF467FD31C31E266A50C375689CAB1BE672774C77062018E74CA1BF6BC5F223802EF93CF7A1383F548196A9B4DE958C49014EDA8501BC8A56B2E1A2B0D46E18101D68816EFCE2297CAA5CC509399A0152232026B754A90FF104B9189426742C2FD8055191BC162B04A262C3D23B996C2AA867F3154521BB19E0A69647718FB76EA5B487C930A454943F0FECA6FE163F40557AAFB9785DA0B6450B0BB4095BA23AAFE83B731356B290DB64B4C39DEB5B30FEF38086E6841B660D41EA593E1C24028C60BC44A6D83696F88772425235A4F05A1847AFD5C84DEDC0B4C1D003F29AAC29710AEDA5053A4C0AF556C4F5508C45FB20141B1FB641AAA6EC6E88D0859553896C31A0206631750C954EE73B415CA163D30FBB75CFEB15714E3330AA346F7DE02014214C2EC5AE550906EC5632CF429BF3467FCB0327017703D08A7F3E9A7785D0C9D6377FAFE527259A0ED5894E2A8C766F688FE2574DF57108C81C60A8705DC7D102891437CB2B9988C879D7403967A83A70CA48D6F1B18A3408BAA13BAD9C26297CC10D920F0C53CAE4D85C6EE0C95DF22321F0A05894CB97C4A45F79CC002DA373AA857B65C726F54B03184934B11376852E9FF58099C72BBF531943EB5626C2C3EF6BCA145A2896B4C1BD29A353F49EC2FBB746134C1F50AE6D771160E64848396A5DA65EC32014F9669BB83AC52298B24DC2AB0BA2C82C0B4017B34F9D979D7D07659C4B76160999C47221DEE91521418948CB2EA72532E6A1C273B764D3160EA072A48782336B648C12B6AE93B5AFF289F81236AF0F9A338E8EC35154DB40386949D6E32A71D635053D5F55990C929492E80637971E303800ADF446B35E44F02C1B2936E5381CAAA9738F9F9F79B077A1D61917B642825666A5D08C5DCD13E5ADC0A5E248F28DA3A32BF1188864A4\",\n          \"c\": \"1077E1871719ACE56B2178A208B3F891C187DA970A51633C9996D278EB738375627AF9052866F15ADB21D21B8D0070A19A3024893FA32773D2E832DDC2480070FCDC03A61504857CC40E0E024AF04532E288F5F37F3877303263F4C66848ABD68E5D7FFBBA91B8BE624B63019D69088ACC1C37E79AFEDD5D1D2CD7721A0E5328AF2081C19417873E2B29794A2D2BAEAF67783B64BFD6E473B27E6B05EACC6079F4B8EE61C07FF13060DA3DC04B556307A1D6A7B896AB496CCC52C94897885E59061E70D12B4A9EB0B4C81F331CE3AE2B47B753CFD5CADF96E9C81EC90021F28F3FD33E2EA2EDB61B87D9B7894EFDDD968DE92A232A148AF1F0C31CA9419DE93ECDCB06055FCDAFBF655F2FEED26D3A6316BA259F7AC18A15893A95A635E364A1338C4EAF1F480B6E6DF424584F8DF7EF411902CA5A9A12FC440EC4E2E9CF0E1C3631EECE02A5134B793EC9C8EFFEA700CB8F6729C413AD1432EE4C8A92AE41D9FCA9D19D7871ED136EE3E0B8ADAAE428F0D4BCFCED8C107040D53C858DD2167E0415C98F46FB6327FE7D1B0359D8B3F3A491F4C708CB46064BF872D8830580D41AA8BA93E40570307A21554E5204284974F23287BD6A92D8A2C64A6F1687FBE7AEF7E224455F639BBE235F027DCF160D7249FF010F2BF6E1E358DA17314399C4B5129741E1171B0BAFF8E5BDD4A7BB8DA81F8D387BB32B8A3192136231C49D9A5B88BD1B6A30A9DA7B508893152FC5FCA58592F808E98914781D48D0CA7314A9A166F5154F9354D060BEFFDCCA00227F3BDAA59672820803FA83720D5BA5035B78E02E3ACA332DD0427CA7B2075D6770D8DE005947EC6E82389117D51C7186ECBA9F0BA3C81BF927AC6D75ACAA9826C612328A908E47684D7A97D49673261A7794EE63B9E99F378FDDE9580492FFFE99535CFC76C4D57CDAD1B5C51E751FFEFABE8772F6CCC1634808E2A0C9E09548AA41A267CE0086BB78B14163AFF59E45D603C495E3B1EBB4F2527A5B1DD4638A5DA9CF1429370E1A2F886EE35AC4287FDDC19297F7F96C223191698B35C4C78E1E4A46FFBD3B3CCE38FE63199BB8D46B1E\",\n          \"k\": \"F7AE95AAB26A52F3E8976BEEC50476D3B5FBD7ECF1A610054DC199A99497A1B9\",\n          \"m\": \"4F7798D88974637071717FCAD2C0ED5333945D51341FBA4BF1962A3915D986DA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 20,\n          \"deferred\": false,\n          \"ek\": \"42803BF789094EEA7D875BC10D50C7D3947CB77CA48A301323B54FDBC94E480372E54AAB12C9680765A3E22988FF42184741A440E348C8D19EE8E7CF87B23CE6944663A4B28937160B0C25943A8742F811D0E417CABA034628A11FA2A06E3A5F51E47EC26A9AA5209582F73F1C705F9CAA74F1F300FE9049030061B9C61DB9999E1498BDC9250EBD324F61827463A6A5104796EFF25405AC687092B78248A2F9171E891729CFC11F86316AD6754E8AD249E7849958B2AB380783CC4123BBC087EDF1075BA8304E339F01242C15385A4007616D873587FCAA5AC786731447DB8C537F0CC64731A790E1BF23412AD9342F9654B87DB07E8EA506C534611F48A9453855CA669EC8AB548F648F8AA9A715D0BC09757A285320DC666637950A691AB8E2630A08B59258205BFCAB9DD37C94B9F4706A1114FCCB806E54751A12576063666252CE7F5041432C095CB469B5A1A634508CF1E2398716272E9570C1790FB53A3100D57B5EBCAECC6CCA50B4656F5219DF477C8AD4B7B727BC2BE25246C526D06B8DCC30423421667E554DB0ECC21837539F43A719CA8952A95B143745F79899F7D1C79BD546B6559B1109BF6D60275CBA5DBDCA608FE34CD96369D67B880CCB00F4B3CE6D3C6CD3A35DB44A6F8E440215B027E03109A6546662789B6A096784A0C64A1A4F07F981CFD201F3687691955184684E22C872400749A180CB5B6317599316790A7ECAB711861469EC794FC894AEDEDB90E45164413B82F18CB162769EB92A47630A98FDFCB09C94456E74C0FBE345ED88A43466860CF0472B6C9D4780AC4CC7189F77A19684C54789A9CB99CDD46534914B1990659E71F2662933148E13272E723C46B4809CB32A42D378DA7688F21CB53DA08AD3897ACBA940640089C73049B1E92C94C943D5EA32EAA19AE7EC072527912643ADCB591CC86B97696B98386615AABC1D32D23D4D22919E35786E77A4E7E30C2F6730DF129089943FFCC1678AA600AF4C3F40741867DAA06701A8DFE6421CD0A5DF34C32C933C9AC2B141C5134A7BBD469266D952AFF44995F8904A148B363E3C2A13978A049B968E24051FF6E312DF77FC5663502B0187A5588C1D84149B2DB835045F9BDC1F70\",\n          \"dk\": \"DDBA891C987B8439ADA0114885B54CB210ADE141C92E3226F4B25E1303B93A68092FC95B5FD9BB80805D8EB2869AE6320A90458E4432E6510ABC3C478C291B63F02E476235390C0162468145234744C6208D642DE2E56031A95B83843B7FBC2E20A83AE5229A4ADA7DEF77864B975D475132CFE0C94BCA0762E369ACD52BC8C1135BACC67083819EC229D8962E989C1FF548A127F86E17C60C8695B592D1C9C3689EC3A9096970931BEC56E99499FB3AA982F663F67CA547115D320ACFECE132B3CA9CD5D223EF20B39F7B78E1526E82C816436BCB6FA822C105319AEB9D2B5450129ABD8D1971A043AC7BFB129B5504F35BCB13DA4E1700CB215A3B7ED6512F558D62EC83DAB765D0816B4D085C09E0035E830996EBAE1475B86F39B23613BE9E403A12A7ACE2DCA602722C593043301A35B4DA84A3BC1D53107F3C1C6260323739C27CE09566882BBE3B31C86CF8381EEB10D86167F300A6E510A70E0A881F282FD8C4A1DCE069F417753F069EDFE46A91837C2FA57D737B425B7A4D0544B6DA399368926296C98F7C5C2532374B95C03F3EE15E526B98E2C48EEB3718DCE2C9FB760147434D6FD1BF040AA5A329867DE87094BABEFA563759E49E1A94A737D6488E2280DE5BC827A90A39930BB78CA70D33A476C65CC9F6CB2F8BBB7024652E43212821568288533A51627F8765E1B64AACB86F08333A428AAD4902518DCC3F77563BAD858CA32515BFA8992A5304D8B69E5A6C79E40B7D361B70C2762BC359CB08401AF7967D8C3186EA826A3C4C0748F67E471B92D3216D39091F26EC477051CCACB27696D9AB918904D75743A709CDD09460AC04863B5412E6E935CCE23B3F6217FBF2CEAA6CC37B8C2B6719C4661510A88BBB74220622B589EBA490309A0FC8E7998739CF6205883BC64B86A317A3E1A86F0C65348C2D62A741E80A8AC416467133581BAB2EAA7B5B88025F4711914ED6996DA29E429B5A03C358192C049AD17E6280CCC231AC0C075E3396AD9258AEFAF757FA23259CEC41F5E7CD7AF7B12DD8219AAA5272E7CC30C240B2E2A7B92CBEC265149EAC2A42E3B92C2C0242803BF789094EEA7D875BC10D50C7D3947CB77CA48A301323B54FDBC94E480372E54AAB12C9680765A3E22988FF42184741A440E348C8D19EE8E7CF87B23CE6944663A4B28937160B0C25943A8742F811D0E417CABA034628A11FA2A06E3A5F51E47EC26A9AA5209582F73F1C705F9CAA74F1F300FE9049030061B9C61DB9999E1498BDC9250EBD324F61827463A6A5104796EFF25405AC687092B78248A2F9171E891729CFC11F86316AD6754E8AD249E7849958B2AB380783CC4123BBC087EDF1075BA8304E339F01242C15385A4007616D873587FCAA5AC786731447DB8C537F0CC64731A790E1BF23412AD9342F9654B87DB07E8EA506C534611F48A9453855CA669EC8AB548F648F8AA9A715D0BC09757A285320DC666637950A691AB8E2630A08B59258205BFCAB9DD37C94B9F4706A1114FCCB806E54751A12576063666252CE7F5041432C095CB469B5A1A634508CF1E2398716272E9570C1790FB53A3100D57B5EBCAECC6CCA50B4656F5219DF477C8AD4B7B727BC2BE25246C526D06B8DCC30423421667E554DB0ECC21837539F43A719CA8952A95B143745F79899F7D1C79BD546B6559B1109BF6D60275CBA5DBDCA608FE34CD96369D67B880CCB00F4B3CE6D3C6CD3A35DB44A6F8E440215B027E03109A6546662789B6A096784A0C64A1A4F07F981CFD201F3687691955184684E22C872400749A180CB5B6317599316790A7ECAB711861469EC794FC894AEDEDB90E45164413B82F18CB162769EB92A47630A98FDFCB09C94456E74C0FBE345ED88A43466860CF0472B6C9D4780AC4CC7189F77A19684C54789A9CB99CDD46534914B1990659E71F2662933148E13272E723C46B4809CB32A42D378DA7688F21CB53DA08AD3897ACBA940640089C73049B1E92C94C943D5EA32EAA19AE7EC072527912643ADCB591CC86B97696B98386615AABC1D32D23D4D22919E35786E77A4E7E30C2F6730DF129089943FFCC1678AA600AF4C3F40741867DAA06701A8DFE6421CD0A5DF34C32C933C9AC2B141C5134A7BBD469266D952AFF44995F8904A148B363E3C2A13978A049B968E24051FF6E312DF77FC5663502B0187A5588C1D84149B2DB835045F9BDC1F700F0ED35733D6D2807A9D1358FDEA6AAB613409738917AEA1C9F4D0CBAC25D0298A10A703C91D253B506276C2E15E683FF297EE8713F9AA8F400F73AFB9DBB392\",\n          \"c\": \"4391421C7C0C25DA903B2A944EC32FAEC0E88682FB3146AA621952E3219016F2FFCF97EBB7C7D6EB95891350EE783147BD5B0B1B089743DFEB15C4D81D6BA42B119A7765A73F19EBB39C565D2564EDFF9D57B2C48E8F42DC891315198D9EB17A9C5B5A9FCC169EA8695D1FCB82A96F79BB5432D47BB06106A9AC0D0AC91C3A23D28FFC19971041716D6688759DA314D6DFD40D087489E85780D7BA66D9D526E70038A5DEDFE6576DD240E7C3E3A629606632B71CA08CDC9206F593B51B80190364FDE88448EF5F110E650DE902C27E48BB82E9F2A007610B671AE048F29119FA07A98C86A46174598E0DFD6BD21C8D59C95408600D5181D600EF0BC302ACDF00F99E6D391257432D314696E4E12F2FEE1334574773F28EFFD813F70E9327D83FC239D04315B1F9C95C4C214B71946A733503064F3171C17DCD219DAD8BAF21A31EC0F9817B6A8B3C4B73C43B70357DBC771955F797F8BA28B56F31032376044F3BB33EBCAAE4AF9A93E2584A142008AE3A9CF75ADC2B3AED29ABCB8D03B28DA272AA5E4A695F9E6CFED430EFF445881F9208913A2E0CD61FDB5BA029D3228AB334EE9CBFC730AF95161ECDD1852E52C41291E0CF8ADE3790DA710C5307B5EFFF0E528A9FE2F6C2027E52501244A3E29CBA29E6AD9447AB43F5B4FCFFF9F3A8E7AC090CF1C6D2BC85B39DE79153E7BC36EF2D37FFC98D9BB21D37AD41E457D5D4E36E7C128DEC48422DB0E26D3E76823687F39D43ACDA2F77531612D449295D1740EFC6AD532C233F2CE6A14121C62171DF4B7166355E1F1E939FD597B3038F54AA056BDEEDB25026998E1D0C047C78D648C2D3373782E1862C8BC0D9BCDA9FBBEAAACD80104122091B3AEA9EB113533C75F1C2FDBA188A08DC549D229F408B592CFDF438B61E8321A367F6956CCA81F0E13DFC3CBD1DC9FC1504307A3F14843B4B3E09571E26FF61F69F2775BEEBBEDB5059A3333D5BFD5A7DECCE8FBD89E50B8CB5C52779A9B9CB19866560DB4EE457E3D18991A561268869E47DAA00C59445ECC7B683171CE81E58CD4FEFFD93E31D5EFAE77CCCECFC995A16DC190F59F91A\",\n          \"k\": \"5418AE44ED01EC65F14D5CDB12AB6004B35744E935AC8A9C3D8D607F946BB706\",\n          \"m\": \"E20AC1D70FA6A2C8A286EF0E3665C79668A5E6AE80197BBF13A0D0EF553ACF1F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 21,\n          \"deferred\": false,\n          \"ek\": \"39B893D833BC95E37373D2C71FCA4336AC84DFB6460F156174B7B95DA92351468EC97439F2B88656F9B0B944850F499415981A66B76333402D43EC30F511AFDE7623BC00A1C58136484350A15824AB0A434561CDA2305C815318D204CA0315C5C9703954224522E76DAB0716719A06F723006D66A2916941139B1F29E53783E6BCC568628D6B0A0E72A44D3C19BAF35FD6FAAA9B2396CABA9B3BEA5C901B836758671391304E5330448A62CBAB155811C5018064FA002A73A71EE6146458DAB896B716560649BB823BA9F73ABFE44E80E1C4D0D58E8C9B397A5841FF9C93CB8488851C482C534BB710B27C6C9CF7A15356427EE8F1B40815A9416B233F4A05065495D4B88BD2A42A36DAA392A9CE49D67F4017484368017FB91ECDC34488F463B99A6CE60359A7F3C32D3A3DFAB70817726B73BC1C36FA4CAC3A0EEC9C6BD3A63DF9957502076150C4759F5362BA493F71D669A724C79788532DA797445942FDD214C3A9103738C00AB54CDE3639497A951C03A70A0B01182B2B9C8C889052857A2A541EBBBB77C78F90377EADA6CF21796E44EC4D5BF03BBDE6773E571785F1847252B1D7182BB4B08B41C54C889072CA0922A56C19CAA890D7F899EA21C9186A3A7106458BDA086FE017640B77FF554FEA32A2C2297D82144EFFE20D7836180CE46FDBDC18D357B6FA7ABDDB2858770598C03169BFCAB9B8794BCC48B779E87672D83F62E04D2B42B7E31451273BA0362B682C955C382442C010005E66A2644550BF9390FB427E4839616AD8C8342ABB0944B3ACE932FC134BC6EC7C16D35427E22DBC4A454D9B09FB49AA496BAC88909F54CA88E1747E1081AEDB23BF370394D3492F5B6AB02C721FE3E0A199AAAEA03A91EA1C44FCB70BD4140912AAAEC9F779DCB4A8C0CA9D903C0FA15BB7E5335ECB3724CE621D602772255775EEF07ED060734E7365ACBAA557E25AEC10A27EF405E1A431AD8052E0B752A08346D1961134C77CE4FA356D66094856141EC05FD7385D009000DC6C4F97A91B074CA154773498F61C646951BA5B6B5D50C4E35C6206A6C33043308B474A7CF587C0F07578DB4D6FF52DC654A5BCD997296C79A97F8C16E5667F527DAA3ABEC018CF1671\",\n          \"dk\": \"F5A19CCA8A702ECA16162166BE451187A9300AB182DEF2987683383B3861ADC83BFFB41FCED714706AB26B1751B53B09282500F3320776A4C75F22C7710636C21B008988203599BCE9D53EA8E044E9F37720D2C1E34101186B896FC590A6593AC6B2131A093185C82D61D58405780D16927BF7514B9814BA5CB42B4AE66A37C55E06044CC0CAC774B01CEC02554CC40FE55326A612AE1DC76D7BD6418FD01C7ED25A15AC16F5B8129265C54B0065758390EAEA0F69B212D33B94A4854E912172BEF1AED684AEAF1560DB303AB1FA0146C715EEA9950D317A87953730B32886C164FD7845DDF6B3894A7460509DD9122B12A3938EC40649537EF21592BAB202500331ECC1A39C5AC2D63A1139AC93E18886F68189078C7090593584D59C50D12A2B5CC08EC6A9FD33BC3C2391A7C3BB4FC51F6A536D962C6AB3EA7978D57290719A80A8926A812CD034422CB0CB49416ABE61B03B041E88882059624843679AB699ABEC6988D918710F5B3FDB188B6031C7FEA47C292A04E05CA2961BA4263C298A06956631705254AAB817C46903B537328B6233B06E9530F0AAB1F3F9C16BC6276B746DC0F44FD9A476CBF23DFD27C45B4B56F8B5BF9AE0092028CE72819DA1C6A5FA48942CFC0A24B04ADE63B33642B5B112CECD370C3F3930FF6305B1B0A1D2C490DFF73706A8AC5D34B21CD8AB8D3111559B915634430D9C78460961F805214DD2434411C1C28C769DFAA9FF19C3E240638181B5C83422E5431028420F74A707914354881938BA842228D2C19440CA99DA68AF9372B8AC6D68A34CC75832C41B1406C66EFD1161E8D0B42CD27EFB18A162006375D40E13E24FAA32489DD91E0DB8A32F11A4BB9139E0123C5DB00D5CB1BD4617809CF3214C581276312BA0819699841C6072128A8C3046656AE6815B78F61AB3890FD7E2BAE1A2A37CB27D3CFC3E4EAA84F65350E5DB36DF499FBA17839192901FA7570EC72381A86581B69760A277A8E63EE3C16D0FC45EEDF4270EEC32733B6D0DA8BFC0539D64136E4E260725191B78831F37F942018946C5113807F9161BB89F6A168C6CE2991DEA0F39B893D833BC95E37373D2C71FCA4336AC84DFB6460F156174B7B95DA92351468EC97439F2B88656F9B0B944850F499415981A66B76333402D43EC30F511AFDE7623BC00A1C58136484350A15824AB0A434561CDA2305C815318D204CA0315C5C9703954224522E76DAB0716719A06F723006D66A2916941139B1F29E53783E6BCC568628D6B0A0E72A44D3C19BAF35FD6FAAA9B2396CABA9B3BEA5C901B836758671391304E5330448A62CBAB155811C5018064FA002A73A71EE6146458DAB896B716560649BB823BA9F73ABFE44E80E1C4D0D58E8C9B397A5841FF9C93CB8488851C482C534BB710B27C6C9CF7A15356427EE8F1B40815A9416B233F4A05065495D4B88BD2A42A36DAA392A9CE49D67F4017484368017FB91ECDC34488F463B99A6CE60359A7F3C32D3A3DFAB70817726B73BC1C36FA4CAC3A0EEC9C6BD3A63DF9957502076150C4759F5362BA493F71D669A724C79788532DA797445942FDD214C3A9103738C00AB54CDE3639497A951C03A70A0B01182B2B9C8C889052857A2A541EBBBB77C78F90377EADA6CF21796E44EC4D5BF03BBDE6773E571785F1847252B1D7182BB4B08B41C54C889072CA0922A56C19CAA890D7F899EA21C9186A3A7106458BDA086FE017640B77FF554FEA32A2C2297D82144EFFE20D7836180CE46FDBDC18D357B6FA7ABDDB2858770598C03169BFCAB9B8794BCC48B779E87672D83F62E04D2B42B7E31451273BA0362B682C955C382442C010005E66A2644550BF9390FB427E4839616AD8C8342ABB0944B3ACE932FC134BC6EC7C16D35427E22DBC4A454D9B09FB49AA496BAC88909F54CA88E1747E1081AEDB23BF370394D3492F5B6AB02C721FE3E0A199AAAEA03A91EA1C44FCB70BD4140912AAAEC9F779DCB4A8C0CA9D903C0FA15BB7E5335ECB3724CE621D602772255775EEF07ED060734E7365ACBAA557E25AEC10A27EF405E1A431AD8052E0B752A08346D1961134C77CE4FA356D66094856141EC05FD7385D009000DC6C4F97A91B074CA154773498F61C646951BA5B6B5D50C4E35C6206A6C33043308B474A7CF587C0F07578DB4D6FF52DC654A5BCD997296C79A97F8C16E5667F527DAA3ABEC018CF16717815E832D512DB0C38A08C78F9C7D3CD3010367902146A12A335AD3148A3C8BBDBEC6DE5ABF972F91D59054FDF0B3F927DDD6EC3477C162C2294048A4E6C3FE2\",\n          \"c\": \"9CA67EEE0B5C186A08356C38E33B9E7317637F3CDE3EE9D6E04C1208F9B9CF63386553425BD51F35E523180B29E3DDB8161F1FC632528A5D5AF0418F5C32B767106A774E5D97047B0A49F6C9FE2ACD3C12A6D45B49BAB8A4C95958507BDEEE88B4659373F8A1D605744F5B65AD2E5A5EA081AD2C55670793CB78691B2BF2CFBA1FD1BD6AD4D9E87FBB64A52CADAB26B4D66684AB2FCAE330173F864FBC3B6461ED1B4EF1BB054D59F2CE2B8C62CA06808B99AA29AB2BC941026494B3233FB5AC8B5E200DE2F2F40DB93C0F567348033C1CBF08D491F3CDF59835791BF4751B4A22AB312A7A9C6FAA6B3FD5021F10F8F3D5C0CCC40483CA28322CB75A80E0DA05BE5D848F43CB3473864B26591C27DAC580D354A9D2DB35C9BFF76B42DA9675A2CD63075F33C2A1D1626992D5ACFAB3E7DAFB8F017F54757C26074DBFD523F18C7757ADDB23476528D540A96EC669E6BF0C1DDA200BABEDB965511546F2C96024D344EF0E17A4481ABFB5C2C07C8D23757321BFC9A58529D5B71428D08A056083E5A027BB059E2813AA9A015BDF7C941DDA306B3C54A08D1613109716F11FCA932EC55A4D31806BE21C47CB1B10A88587276C57CF389CA28AD40EACEEC94A57A5361007B5A85F0A44E5B5E8E354B2EC791F42BDD1830CACF5722788A48837AAD2A2DB34E33B56F9C986DA6E9C485FA96487C1AB608CE903B6D335C47B1ECB129D39194B99DD369A122C6A16948F689C94C8A54D6CB4C5E39D073570460BBE04AEDDB0D0432B69E1724DD61A8941B9C2D26B49ABE6CB87FA1D093CD6DE08033C77C11808B0315D8B347E7EBA33537F99F64250C65690F8AC19951679C580CBE6E36A7FF0A624FADFC84B220FF5EB7B9BA306B4A03DCC305669C1DF2210FE76024E21904E1950446EC85FD5A04580CBD9843D5BE7F90F82A901BDFEED370AB83D416F92C58B5CF143D4306C9FDF43FEE62B6D1A0248C2B6F305331F4159382D92AC6388614EC84729450B85B7DDCCBFF9A97403B186DA21480DFD1DDA6499600C326B3A813AE123F8175D2AFCBCD5A519BF706CFCDDD6F36A2DEE5FC3F34263D8CC\",\n          \"k\": \"89D60F46DC4A11DD81C284E97631F08DE239C06B157529A15BF9B53C9EFBF9DE\",\n          \"m\": \"AC25F29AF8D8A2DBD359600C8A500144D6C0236D729DA016C3F116CBBF621002\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 22,\n          \"deferred\": false,\n          \"ek\": \"0AF62D21757B6AF2C9853421D2D74E0DD86A416A228A9035299C27B32528CC526B371C56ECE17C93B7CBBDBAC84D35441E6B3A17811F3C0A6556825174AC3C2BA51D53F176F027C20FEC424F7539ED3C6C7B964260489C3D8BBA94323630B87E856064E3BA81D3F018516971DFA54AC903C9AD9747AD8C5DB35780A033C385C23CBCB21D26153F93626C41237A02185C41C3935B306FDF568F453B255072348D9716AE9CB9B9ECA6FB0B78DF7927D093A3BCCC7F7BA711B27CCDC0051BE4A31BF3A0B224E0770BFAAF26F599AD190173A4A012A52C29DB006CD37076A12CBA65B679C10455E5442D92609DF209F6556CA36BC852D3A282F14AF3EB2A852AA268B1AA92346631953204F4ABD9726338B8B275A26ECFB80428881613C952545B082AC2BC75699F3285AC10905A00449AE6A42B43D1A4890155A9080A587090F7C91FD97833E13B601EEA06104CA41C408EE73C055EB1CD9FB0934E25660381518758CF3A8C95BB0397931427FCBA515D320D423A7440459C614B9ACDB25A9C143A6F704541A339D9811409D6BE200429B33674F61CA6626C7FB879001AC65F08C18310A4BCE685C37944079DA907316060A64069F87CC5D9FB3B56604AD8A52E72A1405AF5C549498F147CCA4A7749A37B5D96084B01AA3241288BD22592AEEC778FAC1C4A2BBF6085C2B5DB7D9C00B9864C480D8826C2E02B246AA945EB61F9425DE8E604B7D76486418FF0F5AB59BC6EEBB4932D6075F667A6AFA74BB66255F03C6F15A63818E759A4B05F480A4C0CD1072E26627513BCB85474E89676501AB2ED21A8F487780CA8CB8560B27A766213092DCA0B02ED3A24C8351EAE88947A764F142C0E24B192A2421367265C0B37093E0A69AE31756C5906845A8C7D01550E07C72FBB18DEF56E1E8B320F688110C4C4DC7680C9FA05D5B1AB63EB65450283ED6A52F9F535FDEC31C207A483413725022A2256663E2657977660AE63CA8671A8C343AFEFA8C43D9B8D0B93B458416CA4C5B87D121469EA5B520ABC0C079546BC1353757AE2BB1D301974BA03C86F624F34A4B481B29036F59F37DC2D5C1721EFBE0A0C22A966C895E5198A91F916DE62C0FB3A769806AE5827AE6F358D8CD6\",\n          \"dk\": \"ECC90EA4A07BB0F318F976967D17B40206A6D06C00FC1CA6AF95AA38712DB92093E8E94CFF6450BE071127ACB7AF76A5FE8A71FB511488E57DBEE396D65807A5D0A86BAB9DFAF69DF38A362E62ACE8F7AE7CC638931716F2411ECCB621CDF181C4677D66793AA969BBEAE71211991DAF14968C95754B2BAFC6779BA5E4697094B28C1340276322527C7F259B4F39CA48AB2AAD90B2959BF1B12F983C0B3B9014A04392902C5C47A92987ADFBC654BB87C4DE63AAC4B2C45A3BC37972A607F491C0D891FD7BB9FC96032850BD110A2B09EACC134B9E99B5A09BB11D9D5163B880CBD73817085C68E571AAB45439A79117F7DA5E81735808F97A42249728A0B63834001D1C008AABAFD72A068309BE74D49C7BEB0EA9251BE6C1C7323B84402C725C212D361BBA03385FB7641AB1D74B39690DD1448B6B01A85F82A7F43C22B0E3962214246133750D23CFD1776FF64B4D1FE17B8257670379320FAA81447395D7069A74254156E900C9278B9789629EB33CE7C0C867992596966ED9875579CC2A525BAD71E920AB5B0A69E69995152BABEB1D4272A04F3283D988826AD0AB24E24C0B505EA5AA02EAD063D7B587603383148521005034E6162106E29A7EC3926EEC024DAB8AA0A5768C7142EF56A5AF17829E505224A2C3F8526DAB6A76E714766D2450D60A68D23AA4FA392A2903C276A965FD1A15693644515A336F6115CB050709855E95035564157308F3062FA03B0DE2C00269BD8D414C6A574D485061A6A28245375513009F205CC1402694BE76338D0795849AB011205E880537E0696E6F4636CBD9ACE8E279A8015D57108BE0A638B3D8516487BF763257C00A6175407433A204C099BBA4972EDE1763EA5889FBFC25742CB66B9360A6A5661DE7BD4DEC856E55A5BD354E01DC6843B4C306B957E72CA528188403E324AC69AF09820C74E6A389881CE882B86517BCFA881435BC8817A1489250A35EA19B80C62CCE52A0A6935A89E47F658041BC93B2B979A7DE815D97D15E5B55310568BBDCE78C6437C848D479E65C60B75391FEE94683BBC379EB83A545AD98A070F281837E24090AF62D21757B6AF2C9853421D2D74E0DD86A416A228A9035299C27B32528CC526B371C56ECE17C93B7CBBDBAC84D35441E6B3A17811F3C0A6556825174AC3C2BA51D53F176F027C20FEC424F7539ED3C6C7B964260489C3D8BBA94323630B87E856064E3BA81D3F018516971DFA54AC903C9AD9747AD8C5DB35780A033C385C23CBCB21D26153F93626C41237A02185C41C3935B306FDF568F453B255072348D9716AE9CB9B9ECA6FB0B78DF7927D093A3BCCC7F7BA711B27CCDC0051BE4A31BF3A0B224E0770BFAAF26F599AD190173A4A012A52C29DB006CD37076A12CBA65B679C10455E5442D92609DF209F6556CA36BC852D3A282F14AF3EB2A852AA268B1AA92346631953204F4ABD9726338B8B275A26ECFB80428881613C952545B082AC2BC75699F3285AC10905A00449AE6A42B43D1A4890155A9080A587090F7C91FD97833E13B601EEA06104CA41C408EE73C055EB1CD9FB0934E25660381518758CF3A8C95BB0397931427FCBA515D320D423A7440459C614B9ACDB25A9C143A6F704541A339D9811409D6BE200429B33674F61CA6626C7FB879001AC65F08C18310A4BCE685C37944079DA907316060A64069F87CC5D9FB3B56604AD8A52E72A1405AF5C549498F147CCA4A7749A37B5D96084B01AA3241288BD22592AEEC778FAC1C4A2BBF6085C2B5DB7D9C00B9864C480D8826C2E02B246AA945EB61F9425DE8E604B7D76486418FF0F5AB59BC6EEBB4932D6075F667A6AFA74BB66255F03C6F15A63818E759A4B05F480A4C0CD1072E26627513BCB85474E89676501AB2ED21A8F487780CA8CB8560B27A766213092DCA0B02ED3A24C8351EAE88947A764F142C0E24B192A2421367265C0B37093E0A69AE31756C5906845A8C7D01550E07C72FBB18DEF56E1E8B320F688110C4C4DC7680C9FA05D5B1AB63EB65450283ED6A52F9F535FDEC31C207A483413725022A2256663E2657977660AE63CA8671A8C343AFEFA8C43D9B8D0B93B458416CA4C5B87D121469EA5B520ABC0C079546BC1353757AE2BB1D301974BA03C86F624F34A4B481B29036F59F37DC2D5C1721EFBE0A0C22A966C895E5198A91F916DE62C0FB3A769806AE5827AE6F358D8CD6CD2DB91B660C482E6C8B2AAB016B0354DC138DC2BF97D5F960E1D8CC51F09806BF737AC0198871CA09B8C1E4928C4F51B47816A69F4174A4BC9A274F2E10D051\",\n          \"c\": \"398189254F2C82F3B9F6826C377BE31222C4E199954CE883CD44E135BE51E8B1A767969BAAC6FB3DFBF59BF38F2A005798D45B1032FF660C37E1AB24E629D84F79B0673E44D12359CD6632BF4AFDB2ECDB2A1BC960E7B7E12ED89116AC5423ADE1AF5CB43FFD173D2878F11BFF604E8D2B59FF847B570F52D5A5048D16038FFD3A6A86F00513C8394434DB5D87019D6CC46738678A45577698DA6E13B466504DCAE736EB36C83369ABC434B8296C3D9BAC5C46C700F5D0CB0EA37A64017E0DCD82A1301649ADBB8339E7F7C0D6CC42B1EF2690F769BBBFFD50AC546447858CD1B46A31E43CD1133691C4600D745BE6BAFD4E9A4B08E4147DEF63E52516FDC2AAD98A77011876DB533374A85805AAE72B25F0A1B30331750914E79570ADEC5D20B391EEED8C235D295C5C7B3A6FC9C6F8D46EF0F2288785BDB4A99BA461BF2EEE99E58BF46A34989DD128062B511A4724FA7A528CAD251A3D4144E0CC39B89DB093A07FF65204B3A44FD20079ADFE17AAE7AC3306C79495338B73D711C11ECD0BF5BACA4F51BCC6A8CD54EE1D339C146241344433B91436E54E17B7999C3101F4FAC0C6D765407A8F7357DB41C43E1E899C5A786ABA6FA1CF216D8C795A98A9A4E6F4FCB6BD38D82A4AE26E556D672504CB8C33ED921B6CE69FF9B7E1F29FEDB7926956278C1010375360E9F149CDEDC4F44C69D18940E85EFDB467C6D7979549882B94BA635694EC91AB5D3459F244DF94863C180BC623C6FCD5297A1797A272F6CCE06EFCDC1F24E6FEEF30C30D50605D7D7FEB2886854281F573B0ECA200739B307706ACB22B05A6755C50FDD9DDED42442990E9F34778B6615DB04A3F39EE3959C0407AEAB90B580ECEC910836C6E2C30561B056BBCF04EB576284314135CA48630155915195B36039B52CD4B546882F536E2B71E5E952AD560059AFA6DEB52305DC8923FCB52E5C8031596E9596BCFF1F0D05CE5106969532F040BCEF32A7FFAEED70A12050FB21835E3BBEF84C548830C9CDEAC86BC6D3AFACDA53CCA62ACC28ACF22089C70014469D22E967C81D3D7B8BD77F50CA03930B7099801CECB\",\n          \"k\": \"66D121707FFB368BC5D4C73FD24DC2DFB742419B203DED2B3E157EE56044C128\",\n          \"m\": \"7114A4B4195826CFF174FCB75336B25D4D1BF2224D585014CBADB0C4CFBF7729\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 23,\n          \"deferred\": false,\n          \"ek\": \"F568158F1981259934435BC2BFD38A4A100BB9826E429312EA3CBF04A5397A723E740112859C74CA0C2783D5CC9798AB3C273D3AD6911FF76BF7235AEA38C4459761FD4262F483AC5D081900FC1EA3337439A3B4A2787E0D5763DDD2100E6A60DEC12B07B84FA8226017456B7F65679FFC31518C172F93895CFCB75FB74054116CA5662886BBCAD5F6B63534703E8B0DC807952EA32577389F94A7355DB079639AAF39B13D9F95CEF9AC9BC4B3AAB0F7070358644ED38C401A73BC2B2270D01D1C207D7FAA5233D74296302A90E4551D380C715265DD0B22F94681EE6093450186738B32BAA1B1FCC3C1F0A309312B662D25120F6B6D31B2B29809537C4A220D303DB4E5CE51B606DB64CA4B86947056015E8857220B8AE4822F28D31A37F63A89644C4E3A797FD7236A52640CDA3DA6D1782E933361A74B2CF665D04A07DC3843A40A0D8C526BE790721FBA16492557F51B6B6904998422B6848C93EA5043A782312C1250D81C5BD0407F5FFC99DF3993D91A71AD8499BDA29427E51BBC93AF43AB49E9062397F58CFD4B8048392AED850D95BB2F26DC55D3D8AA2A7249DFC35DD145569BA276ABA90B78BA143A876C3D97A66E02BDA3A34BD5DC4918F776E2E2C09E46047771B8C6FAB8C5215BE7514900A77567896899E94282E326D4848606FA8F2D350509B19251B4CA4B5B016C4181E8091D501327CCB9AFACD14C6FE4808E6802A8875FB786A52B70359845634BF1C185C887F1BC6FB8221347DB1DEB6C183C412C6D3684A3CC244D37C1C503B69DE67D4B02773DB8096DE18BC717CB3FD9CBA06A370EE84B9ED13597E6BC4CF88474E6956370C88078A56383799DF76621183C70D522DCB8BF07A9546D9CB9873997AFB51F94B49EDE924643900E201097D9853593F2A7E430AF5C57C754142AB462A12D327D04115540B505B8146BB64347A22BC71C02BD2E9C8CB360AAE5C9B173BC2E844CABA9659C2CC695D0019C135963987A5F019989B3064F8A0A8E4CAC8C7909AC4BC028C4AC4750E8C4FBC1433826AEFDDB2315002A27A2B43C539935649890B31710C56BCDDB64E7866BD07009CF11E676BD64E680645DFC88E803063DFE292C2047525EE37B4F3BF7AC\",\n          \"dk\": \"9A6264C4F2200B6775B81585D2665C5FB3711C7CA596C23FA6883AF47C8FF44158278A7437A32488F5C2AAA9A763674ABD01056A767E1CC48D327B0639E85148C8A60F97AC7EB55BDDF389057C72EAB6419640C4EC7174E420BBEAC4C4B9B8A1A726CAA6A27D226529245A6BE1496303395ADD725CCFB43FAA37206011931852B2E117AA93CC99EBECA070B6B9F5C28B60A3469A6497423A638D6C03E711AE1B64CF613824957973F0E30253B355545206585113A4921DBC32336E750CB940C574F7740E145CDD41C6FB1AB4C7186A3BE12026F1CDA6D80A15916CEB8C78A95A094B330C83111F6EB50A6EC17129363F6684443C09716D129ECF0A097DAA50CB021014228E77F6826350C03F16388BFB7578DB3B368591397650ABF39C947736D2D49E1D4625A9DC3CFF2B14520282FE9AC9E92C9C72745DFD1550474AB13510BB0709A69BFBBC7CB2BB1C4BADB45709F093AA24D09CE75BC8DAC99FF5243E98872233E0B51BB87C7785B5BF558DB3CC5CBEDA8D710039D1E260C878836E4A493EF67534F4C269D090F5F0906BE7117F6B848AA72187DB8DDBB62728143A4F569D31439D72D7085C299CD2F77ACB2C8D8C7C33D4860BD9C5AAE7821D1D67BA3FB81FAC8AB513F035816B019B5C464DCA4A289C1E503654E0A8160D1C37174C565EB876644A6EE3FA6107369A08D27EEA331614406ABA1B7EBD91AF36ABB2A7F9870272CEB7EA69DC0431CA1960A8352BDEB4526E729F49AB6428C15C00EC30D6A9757A953AC4D51063973BC954BB30CA1A37C57B1E402C9E7AA653679F88B6CA0BEA8EBF992F7EB422E90697E6EBB1AD72C754130088759E95091AD4DA61B5350138D9387E5744D2B8049457A0E07265A42961ECFA043498ADD3FA517518C8FD8C0C16A059B4E02BBCA459E3FCABC68C0E656C2AADF50DEC8624E14AB964174393F0A152952069160F1A3724234316C290C398F4AE3F7C3D8720464A4125E3F831B3ACAC315B2DF6F58160F98AFEBCBBDC85702584442DDC9306E66BF234B5017B9AD9B51D95C7AB7431428E36C31F8288DB21B8771B12784866A4189FFDA8B7F568158F1981259934435BC2BFD38A4A100BB9826E429312EA3CBF04A5397A723E740112859C74CA0C2783D5CC9798AB3C273D3AD6911FF76BF7235AEA38C4459761FD4262F483AC5D081900FC1EA3337439A3B4A2787E0D5763DDD2100E6A60DEC12B07B84FA8226017456B7F65679FFC31518C172F93895CFCB75FB74054116CA5662886BBCAD5F6B63534703E8B0DC807952EA32577389F94A7355DB079639AAF39B13D9F95CEF9AC9BC4B3AAB0F7070358644ED38C401A73BC2B2270D01D1C207D7FAA5233D74296302A90E4551D380C715265DD0B22F94681EE6093450186738B32BAA1B1FCC3C1F0A309312B662D25120F6B6D31B2B29809537C4A220D303DB4E5CE51B606DB64CA4B86947056015E8857220B8AE4822F28D31A37F63A89644C4E3A797FD7236A52640CDA3DA6D1782E933361A74B2CF665D04A07DC3843A40A0D8C526BE790721FBA16492557F51B6B6904998422B6848C93EA5043A782312C1250D81C5BD0407F5FFC99DF3993D91A71AD8499BDA29427E51BBC93AF43AB49E9062397F58CFD4B8048392AED850D95BB2F26DC55D3D8AA2A7249DFC35DD145569BA276ABA90B78BA143A876C3D97A66E02BDA3A34BD5DC4918F776E2E2C09E46047771B8C6FAB8C5215BE7514900A77567896899E94282E326D4848606FA8F2D350509B19251B4CA4B5B016C4181E8091D501327CCB9AFACD14C6FE4808E6802A8875FB786A52B70359845634BF1C185C887F1BC6FB8221347DB1DEB6C183C412C6D3684A3CC244D37C1C503B69DE67D4B02773DB8096DE18BC717CB3FD9CBA06A370EE84B9ED13597E6BC4CF88474E6956370C88078A56383799DF76621183C70D522DCB8BF07A9546D9CB9873997AFB51F94B49EDE924643900E201097D9853593F2A7E430AF5C57C754142AB462A12D327D04115540B505B8146BB64347A22BC71C02BD2E9C8CB360AAE5C9B173BC2E844CABA9659C2CC695D0019C135963987A5F019989B3064F8A0A8E4CAC8C7909AC4BC028C4AC4750E8C4FBC1433826AEFDDB2315002A27A2B43C539935649890B31710C56BCDDB64E7866BD07009CF11E676BD64E680645DFC88E803063DFE292C2047525EE37B4F3BF7ACCBD82243C1021E9F731F11A6853EABBA8F4E69636C67C2FA6A4718AA4B2BEBB16CC4395DB6F56E75AEC04D1DDE60A119BD846E85AFA528388FF76A185EF98201\",\n          \"c\": \"6DB2BA6A74409C3771A865799F60210A98E0FC38795DE8978FFCF49CDCF97CD68942C89386E5EEBC6273E1C61223BD2BBBE096B43A45E9585076D2A522B2D14FB24A60164B5D49BD4C648CEA83059D12344E32AE6807AC1BF67C7CEEB08C23AC7A0E379FBE383C0986C3B93AD367CEBEE306082B1B26CE6C47EF6F1ECE2CF6EBF836AB453D1A574E7931E1E1DCDF709DED62B534D84BEA05BDB0C6EE0FED3A8465EC43AE00766E4BE8FFA01AFFE5B40165140D1723F3456FE95F62FB4E299295E417F1EA19DF70E45B17FF5951F2D68C87D16FF823FFD6DB683C0E0D89280BFCB6E0E273705230CB70BE7E1890C461A534DCC73C94A2430190E6380A0F48919D7349327E0514F53D4E10677C8FAF771590C9A6E4F8F5443527275962686BACDA101701B399D6BB2911460F84B636C6B1FF92A5D3141CD28A00B1E2484D9A708A1B2BE85C4FBDEF8939634D3DD1B9C9AFF193D9D97850B92880AC8E859C0328551BE21CEB3D553339A5FE9D450F08087465F333FFECE8472AD6C0DD4E41F1C2189178952DEE12A444E1346F744A3A315FF524F41339A0395F65FD97DB4211106118CBCC438BE76087E7E04F47F8C999A8AF661D652FB4EAABC82DC3718739C5D5106C3A85CAB0EC34FB53913000DACE82573FC1682BCE19BF8816B075DDBC871D8DECF5D2350FBB1392A54E94222C9A038AFFEC64ECA6AE2B963D5D45E82DA816B893B4327E0A8A7F11C5D4A2E153F3B4FB1226F707DFA65409439B152B65E38256D8288DE3339BCE574747E5AB26F5B0B114B29A3503DA863D32E3193434ABC77F35807386EAFB37959E9F18C8A0FE654062ED0A589B71C6539A1251B00E816DAAC71F63D35CA189893E0A95D9205A2FE5DA7CD9408EFA51EC442B6EDD8DC1666BE3B222A7429D76A1B70F39A291948D47ACBD8CE0D581F6A8984407377F0CF3A2D7C23A62351B8151AC0FDCB7CF5C3458CE9F69F5DB1E57EE177B46AD28306E1701F91C8BA0864BF447C0E5CB39FFF907EA79B92E86672CE8CB4CC3639FD95EFEC48EB59F855AEE1417ED920BFDA282EC36A2035FD0D7FC64F1B74BB300AA05\",\n          \"k\": \"5E95F007FFA0F4C822238DE22203E3ECCF50020594E1A8D993E8026FE9039159\",\n          \"m\": \"C78E7B1E5EE8F20EF0B67089306E1ABAFD15760B2DD2D7A59D2C00D496FA0FE0\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 24,\n          \"deferred\": false,\n          \"ek\": \"F9238B1B1744FCE415242CB135300B13831D95854E3CEB49E72074B4C09B64859AF81A0C078418C79150D0083B8F9834B14116AF308BC7194C92575119371AA9C74C83350519C0108FC18620453E4820810B5A89D42B5B84FC134F0741E4C0466B2C4B58758A3D561EEA095CB3F469C3A34F19A29F3D20C1381669A6AB1857D3859561096BD8B7A917B40D4B754AE5CDDA47002CD870867203D9901AC5B852EB778705360DF4D35DA7B217951CBD2C026A4AE15221368DA5635D17EA307EFABED74342A6C387D29B59868C4955F6A5B0786AF5CB03F130AC7420663E343BF00998D83B9BFD6458DD63A3B5045C467957A729417CC133FB2048B8DB71BE36905B213A1D8C1BEB76BD27A7CBE281C305F44769746F90307B01613080D50F96CB451D717EF5F372D488B6FDCB762927B910936F161764131C80F09316526CBE62EBB07DB7300D7A81890557000046FD7C098999994FB87691A59F437A7F6D3690075921E973805693AC1CCBC6637471B2C561437A8E30220BDA078435455FBC368DFAD026F85417A5DA96D8FB6272D06F785929B38529D5990DAE3C7596AA72B9381121C6C94A7A593A5AA62BE830BFC0BF5B34C57221AE288302559045F3686D8941576EB41F32516763A90553A5727697AAD39B46C5D84A2382980FE3AC0F904DD10C97078B197B0BC4CB8764F22943F584BFA9F3609D417D65B3C367DA544FAB84AD16025CBA7CE7794CB3ACA32F3042CBB4C25605998A74BC16F80B91C5635DABA3B8AA0D90B1B59CC042BEE19709A747666348577088DC4C5C3168CEFCC5C12C3B4A06C08CFE4CBE21359F03FB99E1C6CF8C84ABDADB1DA905C4EB605FB9C8CE7979458080AC594759CDD82A6E164A9EB781F4BB24147039CFC98889C077698B2471417504E99BE7F283B0929A35E14E3670291DD728599181A6911EA2912B48515F88689A73E9688957473B0CAEDE73937C7BC385D6B2FCF6B76017A8F23A2A7CB54E8A98004E0C97EBC826D1A1142D8162EBC9A311C325AAD94900D9B115276E91DCABA8D015D8A549F7F56A4B182CB3304FCFFB3232F64C2FCB839420663E9391828968D6FEA820C57B8816E1F5D3B414481523D24B81E1E2C429FFF401\",\n          \"dk\": \"DA28574C93C4660286CCC27A47B8437391BE1A3774E46106D689A28AAB03C3BC86FD066384FCB69B51840B1647730A63B7A44D338C7DCAACC24DDA232615962E027E4F5A6638A222EF484826EBAEB557801EC3C0E597BC9653863C534A5232024DBB234F6014E940346B27CB391B87DB64CDF6C642FE87C534D554212C4A8B531AF5F13F1BF1B49E837A2080908C802567C23C7FC1C4274152C8E3A11AC92D9C71639590007C2175E3396339A55F9181AB21525E1EF64EF64B08C3739EF003A6312A439D18AADB1673665B3E154241708C88551941EE9C6DD1126E3C6294026476586907AEBA78A9D04F3E885EBE6B1413CB01BE3383A3DB7626500D8A95C076A7CA5F0CCB7CF785C6939C7E67A16E015583433739D588C0420D57F4719539722B3B4A1FC8A89F93C5766280C2DB4BE16296DB8236A4F7AD2F963A24E9CBF10659E4F7332EBC549471CF84A0A7A17A3942947E4C807C11B37F2144431955C9C4BCC6A9D87DE013408D6C735F9C150F51290254C70D1573838271039661D9860805148B8721A0D73C6589A49DB56C5BC524418DB313972A0277773EFE82139F800501ACA4295A2E95933D0C97A38B27814775C951C0585413CCD8F2197BA169A599A0F8429279584D38D569FF4B3F8B8B40D2C11DFFF524E490490CA21E7312A5B4423D6AA46907B9C1FF46173F3A0C257A80A8E9601C5C4551EC28F52433B62A1391A87EFC466C2AD564EA4B84D8A50128533EA32663A0969CA22A4E34622F1EC1659A2884D76267F904A7D1BBACE0FCCF8340B1680A3FFCB1A53B3213E59C5A7245467CFBC5C1C27910E97F455B8B4B73C04DB36DDE46C063F9AF178853DE82A83C2AA453286B71E14BF6372D4FF46EDD207962079C1E46CDDBF5C40135765465AEA5B5A2BD9059C32A530A8816A63126651AA0A446AA6EA30FC6B09E0F6350EE4C4BD2A750020083B1F60AF0B13702DB7B69693D2F00A06710B7C59710FB813C24E27767E28A1422B266D18A29664B6CF1A179735640EACD3C0A8FFBBB9498D57C4F399DF0F638537A6815024C04D70DF7378DA0910590E58F59977B858465F9238B1B1744FCE415242CB135300B13831D95854E3CEB49E72074B4C09B64859AF81A0C078418C79150D0083B8F9834B14116AF308BC7194C92575119371AA9C74C83350519C0108FC18620453E4820810B5A89D42B5B84FC134F0741E4C0466B2C4B58758A3D561EEA095CB3F469C3A34F19A29F3D20C1381669A6AB1857D3859561096BD8B7A917B40D4B754AE5CDDA47002CD870867203D9901AC5B852EB778705360DF4D35DA7B217951CBD2C026A4AE15221368DA5635D17EA307EFABED74342A6C387D29B59868C4955F6A5B0786AF5CB03F130AC7420663E343BF00998D83B9BFD6458DD63A3B5045C467957A729417CC133FB2048B8DB71BE36905B213A1D8C1BEB76BD27A7CBE281C305F44769746F90307B01613080D50F96CB451D717EF5F372D488B6FDCB762927B910936F161764131C80F09316526CBE62EBB07DB7300D7A81890557000046FD7C098999994FB87691A59F437A7F6D3690075921E973805693AC1CCBC6637471B2C561437A8E30220BDA078435455FBC368DFAD026F85417A5DA96D8FB6272D06F785929B38529D5990DAE3C7596AA72B9381121C6C94A7A593A5AA62BE830BFC0BF5B34C57221AE288302559045F3686D8941576EB41F32516763A90553A5727697AAD39B46C5D84A2382980FE3AC0F904DD10C97078B197B0BC4CB8764F22943F584BFA9F3609D417D65B3C367DA544FAB84AD16025CBA7CE7794CB3ACA32F3042CBB4C25605998A74BC16F80B91C5635DABA3B8AA0D90B1B59CC042BEE19709A747666348577088DC4C5C3168CEFCC5C12C3B4A06C08CFE4CBE21359F03FB99E1C6CF8C84ABDADB1DA905C4EB605FB9C8CE7979458080AC594759CDD82A6E164A9EB781F4BB24147039CFC98889C077698B2471417504E99BE7F283B0929A35E14E3670291DD728599181A6911EA2912B48515F88689A73E9688957473B0CAEDE73937C7BC385D6B2FCF6B76017A8F23A2A7CB54E8A98004E0C97EBC826D1A1142D8162EBC9A311C325AAD94900D9B115276E91DCABA8D015D8A549F7F56A4B182CB3304FCFFB3232F64C2FCB839420663E9391828968D6FEA820C57B8816E1F5D3B414481523D24B81E1E2C429FFF40142C07F795BB51073526A3ACAC38565B3001A89053744886DEBA29F978A97E55A5E31EBD9243C452668809BE6A57BD4E87955928132F1C0AE88233769F141957F\",\n          \"c\": \"B098B60E7D24AFD22B6D949017D7EB64F5B22E09486CDABBF5C968A552E570814AA7EE78C4812AE1F9A62DFEE18AE28C460FAF64B34DF838C868D9F68605E6B174DB175DB8703BB461228725743526B4746CF3196BF15980B6D765D0C70E0435D06EB99DE367CCDBA94ED3062E793DA70678CF40581F1510A715971231429E4CBB97BB68442147ABCD0604D77D1B086F224039B81289C4BB649427BF1509A72FC94F5D239D45DEF93CB926E031049BCAC7E75EEC5689D731EA0A619BB91EDE099252EC631FECA51583C80EA01271310DEB2B075080D7E57141536CE566CC42BDA1EE5D57783C47460597D6919E6993FD57E0C35C612182C6EF8F6924273E1749C7BF6963F37C5A0CE92473A69487A5E40E29339920376F369BCDE9C3CD87A1FBC5E204CAF004372C5839BB725DFD16ED3311898CA15F05BFCD53429074679D0A40FFC162409B339ADF37877343F18C6658ACC96A451940FC09CB7441E0C8A6D309C2223D69095CD8409AF38557836AB5F1DED6B0CB8B3EC30E4C18C15FE1B764A7B932DC3831E91BB5DC62E50A880E1E1F6FA94EA688994E682E6EB28958367456BEFBF61D4CE5B84F64CE980AA2D6AEB4685188E1EA1844292912E5E00D89CA39B11C326BFB076688FB2F03E6BF6EBE8CDD381B5A3776771BA80D88C2625B357815925235B111AA823980512103ED6C861ACC918FC9EA208F08D0923E2CE6A168B13597D91C2F05A9FE7649BB37018922C700C90C5E467DC58E4E51EF87FBEEFBCC8D64E9C4DCA60C4F32B250FD19A0DC8D9159FC936082175C52E0A73953A0E9B8A1000C9F87F0A6E49D271F053D8549FD1A014BEBE89405A54A3F77DE7CB136BEA832E94E18B8BABE44F11CA6E798B1827AF292235A896D865CB3CECD98F8F6AED3952CB33C85F6D1156E1B16481DBB8D74158A1F84A403764BB120F4853D17167E176CFEF7787826DD9A1281E269A7418CB87D80485FDB0D1B73C0DBAC76F5E07FEED9511090B303B4785A72BF77A9445C512703E1942F2E72BDED8508DD4C1B5D4C21F76D0B535ED7915B8AB709521F85814F1AE3F0FF3F4357DCC94216\",\n          \"k\": \"759E8EB2831DCCEE0EADA89C237570E11A9419694AD1CF4474892DFC6877AB16\",\n          \"m\": \"D23A22F6DE6C0F3C28F5A7A8E54581BDB312A56BC90CF3B22A5BB39C9ABF420E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 25,\n          \"deferred\": false,\n          \"ek\": \"305988FF211E278150CE00B65C2669A14830AAA1A4EF2973101443E2A73A1BDC2CFD5AB88C54539573A7A5FA705C573693599C850370CA4A66E853CC283CCA0D2B52084C054C420ABC71ACC2C10D34C61C9CB0459331511107832B0A3BDEC7A0CD941D15A13F193162477211F29345414987ADD22C0FE95AA734745FB32F1114081957017B479108F626EF004D08AA327F0274B1DC2AC9963D8F557BBB58A9E16B7613DB8E379679006119B2DCC441DA80F9AA7F0BC2A8456CB78713A13985625AAA3AA9C6375BA06395F66EC3B63D4EAB2524322979A9B9E1178C4A966FB808C75416338237B3165AB20135ACB0437ABCD96251218F0C382731C45C9FA8141943743C1B1F5D77A064FC7968CA1F26B2B756F0B623323A01D8B0E8DCCB714441D7D5647F6C4338477926B248FAFC997B77C4A2BA625A3A5AAAC60C7A57682DE39D91D46778790CC5B45738D7866F7A909FEB27F70C9C4445A534C0AAF7A487CA8499BA372D3380A13C87978D82677A37C5C7A4490B716CCA2BC262C8853CB60CFA571D2DFAC83D428A010C760FC291E390102E3B6384C35067886940336AF5751E8BA399D11CC9788228E1E21A61F69E2AF7409CABB70DA8C775AA217EACABA3B2BFC7D608B374A47A2C96F56C57748B5137F8093121C0150C1A61803D5C6B42CA439E81A8C9926084F5B98931B7079DD44E2C9376DE77626EB543DF071C7A2C630A995502535FE8570EF7C987675C70DF8525D495C5F93B0B5EC7CF59154CE08A84ECCA36FADA9962160A1C2CCD3A728B148CCC9DE733719387D6C166BD9691E3EA6B22550A85A09989A49C90D68A51F195561A4F424CBE3152652272200FD642DE292A2061065EE1B962964BA33281B9B88222B122A3247AB9C247A9A6342F5082A685C8C355A1F96A0277BB8A97979937C17A47A2C36B3040C6B437BFC28B8DECB59DD9AF1B6818C146B74B0AAFFDCCB710E6BB3AD5A5F6AB7F8B2C2AD8980D6B569A5DD523E20379A6005695033260ECB823C7579E610F00A30EF745811DE05762A874A1986764A399B5CB1212403F6E7184028C5B2C47BCBA1A6537F180F096BAD9FA53AA495443314B91B46600EC339B950E9C4F1B1AD5E92385E3F7CA\",\n          \"dk\": \"CB402B0AB79835868924D80496343B0AC32FD1A13D7B39BF75893DE4529754B68219C58ACFDAA4CD964316AB05C170249A763416DA3DB9449BF32B0D41F7B53C406C62C24C33E2820A6C818B93992A1CAF55F155370BB18FA415DBC45977A335A0D03C0C9757FD26A4CC00980A2C35D699B96838525867A8A17719A109BA6FDAC6D187671F065BA38255D2824FDF5C66D0065858942982EB5C9839181170C633D76D123918D30A23A3A8884B7073C1E2C914FC0117F190C6C85DD327886A8CAAF01A6F9DD537E7797DE0D906DF15CB305765AE6BC724B7B911D293CF8AB7A9C9119D4B09A1794D388347A492CEB7C0664427320537B6CBA990F76581856A2AB6534AAC19B3015132EDB1023F68BD4A935C293168DFDB96C49A6C40B0C86592CFCFF74F59781C6264528A63653649A021568E71290EF4BB3FA445A925682CC04A161549696F3A984BE61B3CA46D3615983F50987E049A5DD1B0BC885F44F0AAE5E836E468789BEB6F7DB10F2AE07C479607AD156C96E49A790078D13C8205B366974AA32A696962434CB972BB78724123E33396463E83A30BD4265CAB598BCC61895D6BC4443C8D3B1B5D27B607599707E3C0454BC2A84BB53FDE1678FE905A0EC7A9458C2540E7741A10AA079802BAA2BBE90C03C3C80EA6178989CC6A3B9488551252B6828B9138B8C259A38DF9412ABCC01A4183BFD19545216E0FC1532119AF0019603F3C247D408ADBC9215E3614832BCEDA531F63A0729552B2EC99B120B8722275440B0C764E7A6BF0A10E73ABC0098761C45068E7E1549611822ED923131A91DA17B523309A1B59525CE427359B6BF54250B282C56985519C151FBD788F9D4737D8EC1968E79B0BB8CFC9E012C26AA66DA8284EAA3E9770B1D0D760A68254D8A111CD722362BA6E4969422B21BBC41838B3171046E87277CC400120C293417E411673373B98303BC533540192E256BAC46E57BB96F6D906FE291FBD843899A3212A894709349C3A22084A3CC4DE7B86CFF66B841583991747C36CAD49667AE4EC295D76765B401B78E59B68B0B0208A6D12FB3720BC9B77FCC3E56287305988FF211E278150CE00B65C2669A14830AAA1A4EF2973101443E2A73A1BDC2CFD5AB88C54539573A7A5FA705C573693599C850370CA4A66E853CC283CCA0D2B52084C054C420ABC71ACC2C10D34C61C9CB0459331511107832B0A3BDEC7A0CD941D15A13F193162477211F29345414987ADD22C0FE95AA734745FB32F1114081957017B479108F626EF004D08AA327F0274B1DC2AC9963D8F557BBB58A9E16B7613DB8E379679006119B2DCC441DA80F9AA7F0BC2A8456CB78713A13985625AAA3AA9C6375BA06395F66EC3B63D4EAB2524322979A9B9E1178C4A966FB808C75416338237B3165AB20135ACB0437ABCD96251218F0C382731C45C9FA8141943743C1B1F5D77A064FC7968CA1F26B2B756F0B623323A01D8B0E8DCCB714441D7D5647F6C4338477926B248FAFC997B77C4A2BA625A3A5AAAC60C7A57682DE39D91D46778790CC5B45738D7866F7A909FEB27F70C9C4445A534C0AAF7A487CA8499BA372D3380A13C87978D82677A37C5C7A4490B716CCA2BC262C8853CB60CFA571D2DFAC83D428A010C760FC291E390102E3B6384C35067886940336AF5751E8BA399D11CC9788228E1E21A61F69E2AF7409CABB70DA8C775AA217EACABA3B2BFC7D608B374A47A2C96F56C57748B5137F8093121C0150C1A61803D5C6B42CA439E81A8C9926084F5B98931B7079DD44E2C9376DE77626EB543DF071C7A2C630A995502535FE8570EF7C987675C70DF8525D495C5F93B0B5EC7CF59154CE08A84ECCA36FADA9962160A1C2CCD3A728B148CCC9DE733719387D6C166BD9691E3EA6B22550A85A09989A49C90D68A51F195561A4F424CBE3152652272200FD642DE292A2061065EE1B962964BA33281B9B88222B122A3247AB9C247A9A6342F5082A685C8C355A1F96A0277BB8A97979937C17A47A2C36B3040C6B437BFC28B8DECB59DD9AF1B6818C146B74B0AAFFDCCB710E6BB3AD5A5F6AB7F8B2C2AD8980D6B569A5DD523E20379A6005695033260ECB823C7579E610F00A30EF745811DE05762A874A1986764A399B5CB1212403F6E7184028C5B2C47BCBA1A6537F180F096BAD9FA53AA495443314B91B46600EC339B950E9C4F1B1AD5E92385E3F7CA320C1B0462C9C95B0367A4A13BEE7F2574BFBDF01921E7C2BA5AD3D6954E8334C39524D35D19623E3F4B21EA8BFFAFE599515D49A90278F7529215781B9A9F82\",\n          \"c\": \"BC00B2A45132B099533C3441157FE9E260F7B47CFA31730421FC913920B72A7AF375DAA469C22A17E8A4EBACB8ACB89D1DC841028190538BCACF028B7709D14E38DE97A99004F54B8D84A1372C250185895486C5426E6AD1D4C42F69D4902DF59A2ECEF40979E6C240EBA46FC0ED0788CD75B1B6BA6F382950BBA1C2A0F779B3100C0A26639A9733F3B912FB1CAD4DDD118D4AE13198204FAE7EE59277315662B9CBC9EFBC1D756127525B4996CFDCDF9B7DF7E9A2E71B9BA72650370DBF75A2F39D0004CA6F7FA59C8951FAA76091362C8938D5EC82E6EDAA06BFDA4852DB9F11EAB5C659D21777AD6365AFC524FD0090551535A6DD2EBB8E5F8A2D1C1DDA87655BC1038C6501610291382969EF3CA1730947DEBFCD5B95B68D63750E77A59CCBB328D57347824D6FF2F09B0152F0B404DD023A6F2DF7E61030BAEDC765500ED03A81237FEEBCC3022403D17EF9398296B0AF4747209E0CFE925DCD71B70DD71DFD96181CA30129EC97A21C0D18E3B6315CDA1E88DCBCDDD1912A4947E6E1BAE6250CBDD931CA1B7D146041E973AC0139FF6A23107D44E61293D1AC9E249B5F4E3CF69E55361440DDF9B2558EC793F8968CC09716CD9DEC2BAE26A0A5587BE97CEE4B9CBD3506794559C2D7D3550011CA37424CCBDA8BF479098A5E76031D729EDE3B67C6E5A0A2AF11627C1AAFD3C16F548C4841AD9307096AE806210CC0173429C9699F5D95162B9B56D7199D4809A294579905E2C5D3BE1F890F65727F92D97CEF4915724FE3CEBF00E3A01336FAC1C86ADF6A8ED654256DEAC45464E537EAB98A918C69CC6AD91A53E69158DBD71A18A83DA3EACD67A65F7DAB277E82B5F9535E61448A1AEE1F52FAD989E14332EFFE97D3309CC2BD58E45AFB5A7056C20AEAF1E4D5A0EF5B0C1507923CAF7937657109A83A437EF10CC035BDF983F88FA04EE6C338346FABEAD3413DF0071F960AEB121FECDE71BEF8800142CC159F9A6EB729205D3A980F11B5960699EB3A9394237B96E58F141058F8057A4D15895D4F77C49BBC021B452FFBACFF2C74279EA83D0C57EA4CE5D7952314206A3FABC3\",\n          \"k\": \"2239EC88DA575EBB9329448904221C63CDF517DBE3029713E3840CF4C54819E3\",\n          \"m\": \"C0A5ECA859643D0134F2231C8F3764044B7E6073C92C9CDF71BD64FBC59ADDB9\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 2,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-768\",\n      \"function\": \"encapsulation\",\n      \"tests\": [\n        {\n          \"tcId\": 26,\n          \"deferred\": false,\n          \"ek\": \"89D2CB65F94DCBFC890EFC7D0E5A7A38344D1641A3D0B024D50797A5F23C3A18B3101A1269069F43A842BACC098A8821271C673DB1BEB33034E4D7774D16635C7C2C3C2763453538BC1632E1851591A51642974E5928ABB8E55FE55612F9B141AFF015545394B2092E590970EC29A7B7E7AA1FB4493BF7CB731906C2A5CB49E6614859064E19B8FA26AF51C44B5E7535BFDAC072B646D3EA490D277F0D97CED47395FED91E8F2BCE0E3CA122C2025F74067AB928A822B35653A74F06757629AFB1A1CAF237100EA935E793C8F58A71B3D6AE2C8658B10150D4A38F572A0D49D28AE89451D338326FDB3B4350036C1081117740EDB86B12081C5C1223DBB5660D5B3CB3787D481849304C68BE875466F14EE5495C2BD795AE412D09002D65B8719B90CBA3603AC4958EA03CC138C86F7851593125334701B677F82F4952A4C93B5B4C134BB42A857FD15C650864A6AA94EB691C0B691BE4684C1F5B7490467FC01B1D1FDA4DDA35C4ECC231BC73A6FEF42C99D34EB82A4D014987B3E386910C62679A118F3C5BD9F467E4162042424357DB92EF484A4A1798C1257E870A30CB20AAA0335D83314FE0AA7E63A862648041A72A6321523220B1ACE9BB701B21AC1253CB812C15575A9085EABEADE73A4AE76E6A7B158A20586D78A5AC620A5C9ABCC9C043350A73656B0ABE822DA5E0BA76045FAD75401D7A3B703791B7E99261710F86B72421D240A347638377205A152C794130A4E047742B888303BDDC309116764DE7424CEBEA6DB65348AC537E01A9CC56EA667D5AA87AC9AAA4317D262C10143050B8D07A728CA633C13E468ABCEAD372C77B8ECF3B986B98C1E55860B2B4216766AD874C35ED7205068739230220B5A2317D102C598356F168ACBE80608DE4C9A710B8DD07078CD7C671058AF1B0B8304A314F7B29BE78A933C7B9294424954A1BF8BC745DE86198659E0E1225A910726074969C39A97C19240601A46E013DCDCB677A8CBD2C95A40629C256F24A328951DF57502AB30772CC7E5B850027C8551781CE4985BDACF6B865C104E8A4BC65C41694D456B7169E45AB3D7ACABEAFE23AD6A7B94D1979A2F4C1CAE7CD77D681D290B5D8E451BFDCCCF5310B9D12A88EC29B10255D5E17A192670AA9731C5CA67EC784C502781BE8527D6FC003C6701B3632284B40307A527C7620377FEB0B73F722C9E3CD4DEC64876B93AB5B7CFC4A657F852B659282864384F442B22E8A21109387B8B47585FC680D0BA45C7A8B1D7274BDA57845D100D0F42A3B74628773351FD7AC305B2497639BE90B3F4F71A6AA3561EECC6A691BB5CB3914D8634CA1E1AF543C049A8C6E868C51F0423BD2D5AE09B79E57C27F3FE3AE2B26A441BABFC6718CE8C05B4FE793B910B8FBCBBE7F1013242B40E0514D0BDC5C88BAC594C794CE5122FBF34896819147B928381587963B0B90034AA07A10BE176E01C80AD6A4B71B10AF4241400A2A4CBBC05961A15EC1474ED51A3CC6D35800679A462809CAA3AB4F7094CD6610B4A700CBA939E7EAC93E38C99755908727619ED76A34E53C4FA25BFC97008206697DD145E5B9188E5B014E941681E15FE3E132B8A3903474148BA28B987111C9BCB3989BBBC671C581B44A492845F288E62196E471FED3C39C1BBDDB0837D0D4706B0922C4\",\n          \"dk\": \"B09125AFB3CFB5295581373AB6885284D9706318280D223EDC987FD14410DBE82E6AC89ADFAB70E67CA4B1C641AD037FD8C47870F159EC79CDCD52605B9890499BB6DBD8347F342C61436B642C0DDF4617DB06198B8285DCE4C09D9775A2F41C8CD18AF8E75F57D4127DF94D901AC83BACBD584CC50C43750F49B357F59350875C9B475480A8AAA168592DDB158614A639813566D205368C6C39F0413CA3230DF60D44008282B682AC66B76C3C95F00B2A555035529C86EF3905B4A3968FEA7802B6C5EECB08E8F0C42D7AB7CD21A62FB136412A1840B52C99970CCF51892F73497C3775BE2189F7FC25E7C74D81FC217683292AA4866DDB04469855323A0810F0893DE5C7F94A9C0B5337DB83C44891B2E694695B76575032BF51761682958BD4F97BE9A355B4A85BB6858B7E5A5EF653AB781056AF9187D811C3A8936E5706503DB57062410BCC9421F1AB867A657856C411C4E025ECB3C387729AE8E112F330B988E22F47C35C280750D21B107687AF7B329EF3CB5289F06FB7D44548391E97BA6DD499B5907C54958413D92AA99D5646CF47A8F48CB70A07AD056B4EEFE6C8C46645F7028A32410558638C48E83AC1570160C3833BF64052F5B7DF4364D3E0B24E790AA7C98CEE0441E6731D9DE22D156C61E1C740397672EF54724F01B9D49923AA321F86B98823F21360138392B90C69434635275F9BFBB9B8A99E8E1B7F4EC25F75DBCE33C13F750170BD6722EFE496E7463E16AAA5867B869A96AD41B22BD2556C924596FD778D79A102F6E46D8EB18FEFAC8DB19993E5414AC816705286892492C8C9E852D6145DFF0C10E4A6703A459E7E732A6DFA2766A622B0622BFEDB8F41C125F61B2EC264853B9CCC165979F6A263BEB148905AAC7618A70E829E23F28696F92EF6FA07C102CDBDB1288BA5CFF3A81ABBA15974535FE3106A80068F14E98964572350A7112B1601C196710C096CCF164FBCE1AABAC9C5B9535070E61AB8068D611CA765FABB6412607DAB30C4FC6AD073731FDC4C48B88E267C47B439AD2560C30561815CEB1F52C896489944BBBAB52B1B1D1680A1057964DAFA600C93A39A447DDBB0ADF911AFE3E823D8ACC7CC04659F625F2C1837BB175282542CD22601F621581AB5A6C0384E087CCD32A5380B522FDD3A4202B5B41C85CAFF2903B2DC2645703D9BC711FBB404C0C0376187AC588AAF5718522D2273A9408DABCBC9701698D2DA172AA6267A4C9693A24011C2265A2B6DC8E96304A98DDC5319A3140C399A08412C20F48537870BB84C32A094457895511FF7EC421DE01A64B78534653F78327441B90CD115939DFAAFA95B40D0A63D62D12EB5C9096018CC83871E44E6CD0BE26D16B7B5A209B8E6471D2954ADF9FABD0153707C9CAA2BCC38DED841C791A0EB597EEEE2C518D926EDB28AB53CAA5B7746466931B0AC9150688BF37049C1F82BCF648332434CD0A92FD2C958353A26CB65CB499057109B2D688CC43C4B385DA7C50868AF1B8075E57088F5DB12DFA493EACB6DC4EC6E205BAA2A89858EC2823C00553714CDE47A96E36C7C198B3EC57CCF74D92CDDB86AA0A8B8B5CA9D52BB60ABA79F4F72B0125532CEB7A9077480D2BB60DF51A989D2CB65F94DCBFC890EFC7D0E5A7A38344D1641A3D0B024D50797A5F23C3A18B3101A1269069F43A842BACC098A8821271C673DB1BEB33034E4D7774D16635C7C2C3C2763453538BC1632E1851591A51642974E5928ABB8E55FE55612F9B141AFF015545394B2092E590970EC29A7B7E7AA1FB4493BF7CB731906C2A5CB49E6614859064E19B8FA26AF51C44B5E7535BFDAC072B646D3EA490D277F0D97CED47395FED91E8F2BCE0E3CA122C2025F74067AB928A822B35653A74F06757629AFB1A1CAF237100EA935E793C8F58A71B3D6AE2C8658B10150D4A38F572A0D49D28AE89451D338326FDB3B4350036C1081117740EDB86B12081C5C1223DBB5660D5B3CB3787D481849304C68BE875466F14EE5495C2BD795AE412D09002D65B8719B90CBA3603AC4958EA03CC138C86F7851593125334701B677F82F4952A4C93B5B4C134BB42A857FD15C650864A6AA94EB691C0B691BE4684C1F5B7490467FC01B1D1FDA4DDA35C4ECC231BC73A6FEF42C99D34EB82A4D014987B3E386910C62679A118F3C5BD9F467E4162042424357DB92EF484A4A1798C1257E870A30CB20AAA0335D83314FE0AA7E63A862648041A72A6321523220B1ACE9BB701B21AC1253CB812C15575A9085EABEADE73A4AE76E6A7B158A20586D78A5AC620A5C9ABCC9C043350A73656B0ABE822DA5E0BA76045FAD75401D7A3B703791B7E99261710F86B72421D240A347638377205A152C794130A4E047742B888303BDDC309116764DE7424CEBEA6DB65348AC537E01A9CC56EA667D5AA87AC9AAA4317D262C10143050B8D07A728CA633C13E468ABCEAD372C77B8ECF3B986B98C1E55860B2B4216766AD874C35ED7205068739230220B5A2317D102C598356F168ACBE80608DE4C9A710B8DD07078CD7C671058AF1B0B8304A314F7B29BE78A933C7B9294424954A1BF8BC745DE86198659E0E1225A910726074969C39A97C19240601A46E013DCDCB677A8CBD2C95A40629C256F24A328951DF57502AB30772CC7E5B850027C8551781CE4985BDACF6B865C104E8A4BC65C41694D456B7169E45AB3D7ACABEAFE23AD6A7B94D1979A2F4C1CAE7CD77D681D290B5D8E451BFDCCCF5310B9D12A88EC29B10255D5E17A192670AA9731C5CA67EC784C502781BE8527D6FC003C6701B3632284B40307A527C7620377FEB0B73F722C9E3CD4DEC64876B93AB5B7CFC4A657F852B659282864384F442B22E8A21109387B8B47585FC680D0BA45C7A8B1D7274BDA57845D100D0F42A3B74628773351FD7AC305B2497639BE90B3F4F71A6AA3561EECC6A691BB5CB3914D8634CA1E1AF543C049A8C6E868C51F0423BD2D5AE09B79E57C27F3FE3AE2B26A441BABFC6718CE8C05B4FE793B910B8FBCBBE7F1013242B40E0514D0BDC5C88BAC594C794CE5122FBF34896819147B928381587963B0B90034AA07A10BE176E01C80AD6A4B71B10AF4241400A2A4CBBC05961A15EC1474ED51A3CC6D35800679A462809CAA3AB4F7094CD6610B4A700CBA939E7EAC93E38C99755908727619ED76A34E53C4FA25BFC97008206697DD145E5B9188E5B014E941681E15FE3E132B8A3903474148BA28B987111C9BCB3989BBBC671C581B44A492845F288E62196E471FED3C39C1BBDDB0837D0D4706B0922C472E31DF613DA9A1DD33B5D2D8939684B89F7649E1C59B959FFBE972786C477F66177DBF3B059173FD06AFCD90E80E862174FC57F97607BBFF5B73D6360FB5C37\",\n          \"c\": \"56B42D593AAB8E8773BD92D76EABDDF3B1546F8326F57A7B773764B6C0DD30470F68DFF82E0DCA92509274ECFE83A954735FDE6E14676DAAA3680C30D524F4EFA79ED6A1F9ED7E1C00560E8683538C3105AB931BE0D2B249B38CB9B13AF5CEAF7887A59DBA16688A7F28DE0B14D19F391EB41832A56479416CCF94E997390ED7878EEAFF49328A70E0AB5FCE6C63C09B35F4E45994DE615B88BB722F70E87D2BBD72AE71E1EE9008E459D8E743039A8DDEB874FCE5301A2F8C0EE8C2FEE7A4EE68B5ED6A6D9AB74F98BB3BA0FE89E82BD5A525C5E8790F818CCC605877D46C8BDB5C337B025BB840FF471896E43BFA99D73DBE31805C27A43E57F0618B3AE522A4644E0D4E4C1C548489431BE558F3BFC50E16617E110DD7AF9A6FD83E3FBB68C304D15F6CB700D61D7AA915A6751EA3BA80223E654132A20999A43BF408592730B9A9499636C09FA729F9CB1F9D3442F47357A2B9CF15D3103B9BF396C23088F118EDE346B5C03891CFA5D517CEF8471322E7E31087C4B036ABAD784BFF72A9B11FA198FACBCB91F067FEAF76FCFE5327C1070B3DA6988400756760D2D1F060298F1683D51E3616E98C51C9C03AA42F2E633651A47AD3CC2AB4A852AE0C4B04B4E1C3DD944445A2B12B4F42A6435105C04122FC3587AFE409A00B308D63C5DD8163654504EEDBB7B5329577C35FBEB3F463872CAC28142B3C12A740EC6EA7CE9AD78C6FC8FE1B4DF5FC55C1667F31F2312DA07799DC870A478608549FEDAFE021F1CF2984180364E90AD98D845652AA3CDD7A8EB09F5E51423FAB42A7B7BB4D514864BE8D71297E9C3B17A993F0AE62E8EF52637BD1B885BD9B6AB727854D703D8DC478F96CB81FCE4C60383AC01FCF0F971D4C8F352B7A82E218652F2C106CA92AE686BACFCEF5D327347A97A9B375D67341552BC2C538778E0F9801823CCDFCD1EAADED55B18C9757E3F212B2889D3857DB51F981D16185FD0F900853A75005E3020A8B95B7D8F2F2631C70D78A957C7A62E1B3719070ACD1FD480C25B83847DA027B6EBBC2EEC2DF22C87F9B46D5D7BAF156B53CEE929572B92C4784C4E829F3446A1FFE47F99DECD0436029DDEBD3ED8E87E5E73D123DBE8A4DDACF2ABDE87F33AE2B621C0EC5D5CAD1259DEEC2AEFF6088F04F27A20338B5762543E5100899A4CBFB7B3CA456B3A19B83A4C432230C23E1C7F107C4CB112152F1C0F30DA0BB33F4F11F47EEA43872BAFA84AE22256D708E0604DADE4B2A4DDE8CCCF11930E13553934AE3ECE52F3D7CCC00287377879FE6B8ECE7EF79423507C9DA339559C20DE1C51955999BAE47401DC3CDFAA1B256D09C7DB9FC8698BFCEFA7302D56FBCDE1FBAAA1C653454E6FD3D84E4F79A931C681CBB6CB462B10DAE112BDFB7F65C7FDF6E5FC594EC3A474A94BD97E6EC81F71C230BF70CA0F13CE3DFFBD9FF9804EFD8F37A4D3629B43A8F55544EBC5AC0ABD9A33D79699068346A0F1A3A96E115A5D80BE165B562D082984D5AACC3A2301981A6418F8BA7D7B0D7CA5875C6\",\n          \"k\": \"2696D28E9C61C2A01CE9B1608DCB9D292785A0CD58EFB7FE13B1DE95F0DB55B3\",\n          \"m\": \"2CE74AD291133518FE60C7DF5D251B9D82ADD48462FF505C6E547E949E6B6BF7\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 27,\n          \"deferred\": false,\n          \"ek\": \"F5841D6AEA683FDBA16308BDAB828DDDD7735B8B7A0DAC6A57EB5134B91D8D6CBD989580411144E1FB5A6A559A7056376210A8284742D22A5881C5214C90023FC910D5D02A869087557900273BB875420B5717CD0B23064AA820CDF372F3E4778D70AEB5D02B6182C4D37110D782B6E80303332697B4C610A384A0C632C0D9484A1D3B5EA921525BEC5755C839DF942F24A027DB50B2D760066D10A117BC9A1B65C448CB9ACF3B4F644316E8941C449803F6851A74D832A739B2C0EA9258C7258E98BD3E833D879A6845EC4ECC44B6FA699388135F5E4830F2625E9FA5CC982C578B2593D350B06288A854D3349C24586D3AA2E68726A873B1E5AAA3B22671D8C69AEB180718CB456B942E4B6678E620A00BCA310C722DDD499EAD9C6B66666A3DE39A45D7AF0BBB7AB6A0BEAF8BBCBBA17B1D097ABB09A70E410352D2084423AC53ECBB4C196021F01E662A60C68B3BF48A5F0864A25577912F52620CE6347BD27FF68A17D4B92CD7D01B89E3487A5BC2859781F3EBB8B5B4C2D682636C486A000A576A4B63AFFC05082B5ABE3CC0B37B1E586C2107D97157E325A067BB86453414A15594A510DCFB2FE1A0074483120FB83440DB1B8C3B41E36364F92056083CB9CF91B39F28CF00F6AD098AA10FDB4B4D9B64ED1338E0D5B7A5169C3D8C0184B19966E54272F765C0337BBD307F8C97369A7A87DA44A5BF468DB8A9AA5EA598F885AB50174B0F9025A4EB53D2323D202A05265331FD836DF8E02B4595458551ABED8A3875B83BF976942372CB37296C813ACD2C27B41A5514B66AB25759009DB38A9D0473D5B7A9A7D6795F1188A079B1792A01141347AF2194CA681055D36E954C02D6935BBA7C2EF7F4B5E47C8B0A0069F29575E863967CE4C53105230472172FB79E69089D5A7BCAA95784BFA279EFE67DA145308BAAA1A5A303757946C2866B4841660A99C1968B8F7DE799ABD71806EB9F091397C1CC4171152A6AFC36BD733FC6C53545361AB6258CB45C9F1331BAEA85BE4558935984C081F73E4B377E0251CA7C396BBBB81D271BB9F0589E1BE3218B0B5840372253AA80A5DB79E11199C0832B2433880B68BD84FC02AA3CBBEC205EBBC7B050967B4DFB11E2FA63BCF6B7656A8028AB607CB084C21747ED573A055166F82215D7201D5D439A19F584F470B4272962C137B38545309547CEC25B09C96459AB7B4DA69C8D7B9277BBC4B5568813DA904141A011D9B45AC1F181273149F3C46F45CA9735221B97CB528E8AB59C5711A57C603F7A91803254E8CC4A37D84D1F6535E5A791A50145E1E073430810B3AB79DF4053538C7DB4826A1B428A84553BB881A23507385271B32F854706BB2D3E884E7B391985B39B7BA373071455187B3DD7DA75F6988BBD6BC39EF2808C245AEC9C024CA16546A16F63831A7B6797951A40894A5E38422F30B87E70355CCBE960B216592D0073F1240C21BB109AE76C9DE5B7835BC08AC6601C314A82232FA6F6896BD7834F0254BF112602022844F0CBA9FC3D2E3A58EDD56DDC498ADC9A03FCB43CA138640F85397FD5731F537D6BDC3AC76563D6516F1CF24F84B7C957635DEFBBB70071621C8B2585380A63660EF2CB6CA5910BAD42A1B621CAB8C26780D4251DFD1C6370EF12193C3CEF0223187A4557BC08F4ADD382\",\n          \"dk\": \"62DC65F32C94A1365605B30807CA5A34996AC9311532B3A23906683B44A9D9B6136BD9AA72F369E77C701E72086E5137EC7350DF480B6437B31B2863DAEBBFC08B9C27D00CE7F6349A971ED1E505039B73EDA8C7614334216A5F2719587DA3CE79B92D14581BF58590BBB3C01108C21F9B6933E4128B28A7834B2DA4922431F921ABD52EBB5295AC4957EFD662ADF97265E912A3D115F6695536AA2D6AD15E74D58C29FC0ACB536C2B43157FF21C57AC1B600979FFB223DDB70E76EB8D56502921654BBCE29B30E5C7E359B86E25A5EB018B0428BA5D53B23F6BA37E3265854C1A58DA09B9FC3DE4B98906E3A8E9C575DC82AD2389CF2E9C32D3BB8337A8221C59077722C7D39C9F536B3CB523C8756250176155907B20F5BB2C7F3A9B85E5A527FB0FCBC01D7EA971162789BDBC840ED68D24AB7F626B0E401BCA03A225F5E4AD2C17B8C87893683B7D1CA83B1AE0B3D0CBC34FE793A07469049C60E9169A47701828B395ACB3BE3364CC5C2A5892713137B04AD0720FF7307D917C61A269CD5B583F4CB65BA2FB562AFC535117264CD46AB03637D832A37585640379A2C3C80D4CD621D4638D3F6972B1EBBEF61A3CE5461B6F1A253AC2C150255B188656CCF492BDD41A921BA7CEF679C8D33265FC0A8AEB6988E2B813561EE6C4A9A8297E935008173C58112756E5288A4ADB6FD192A478546A41D88041038289A4708704BEDA425988D493DF57044E6304E89896CE25C4978A6278AC70EECC27EFC271F1E4A9C0F5AF17E1441357B8EDC88799421FA56A8731063776322837E01FED1AA6B2E4C86A5C55FC5B735E315C22284B37C238FAE93916188247815554DA511AFB634C75A1B8E783F5530BD38A57B4F77E48C812DE683507F5A24CA867AA3A96440678F21404D9D6B182259190C87ED86C6F632C90D03070F7761ED85C7AC22261C6566C76B66C2E800DC427C07633C90A61077B078E2DB251527835239957DFC2055F176FF758506983135D437650E71D77957A8A48B56CE59C566395078C2E7008A4D7604BAAB7AB78C461B7718D6BB27C55206630C331823619A08773DC562392F817D73B350421929D83BC49C6C8DD5C38F6D46F08755C316093F5C5454280A3DBDA4E5B508E40E0ADCC80AD27112E2D2C9DA3392A034B6277F8157FA1BC0ACA1C1F857CC4D6A6EB880C667164F6AB886B5A1D84C6321D735A5A1047AD985FA0DC317B22BB54E0C04FD3C27FA976F2EC633DC1B14A502E5A02275AE67E6B68583B8C7FF97827059BAE1CC3765A5C7202227AB640C1BB68924B928B489A3F6BAB68E2F0C9D338589443C3909353FB98487BFA6A91B71A62A9CDD39AC6065668680388E1A33467A3C32EC61A1D605E6AC92751D05BDE0930867A96E2713D933582D1BA4A85434590324B33A522A5B2BE13F667A96914D2F6289CE735BD3CAD15F6ADDB4B4BC7AC47C88A2C52FAA7CF434E7B8995A4F54A7043273E357FD6B37F3EC924457913C351BADB0B415CE60754BA7621A8C85FD0C678E1C9BF4C3283749A33784098B9065B4724A6A4A5A155751E0845F908B67C673C0B6C278C35CFC86A450A8526608683DFE69D59E1C905D4A3EF95B9E134AF8E6A54FDCBAF3E028EF5841D6AEA683FDBA16308BDAB828DDDD7735B8B7A0DAC6A57EB5134B91D8D6CBD989580411144E1FB5A6A559A7056376210A8284742D22A5881C5214C90023FC910D5D02A869087557900273BB875420B5717CD0B23064AA820CDF372F3E4778D70AEB5D02B6182C4D37110D782B6E80303332697B4C610A384A0C632C0D9484A1D3B5EA921525BEC5755C839DF942F24A027DB50B2D760066D10A117BC9A1B65C448CB9ACF3B4F644316E8941C449803F6851A74D832A739B2C0EA9258C7258E98BD3E833D879A6845EC4ECC44B6FA699388135F5E4830F2625E9FA5CC982C578B2593D350B06288A854D3349C24586D3AA2E68726A873B1E5AAA3B22671D8C69AEB180718CB456B942E4B6678E620A00BCA310C722DDD499EAD9C6B66666A3DE39A45D7AF0BBB7AB6A0BEAF8BBCBBA17B1D097ABB09A70E410352D2084423AC53ECBB4C196021F01E662A60C68B3BF48A5F0864A25577912F52620CE6347BD27FF68A17D4B92CD7D01B89E3487A5BC2859781F3EBB8B5B4C2D682636C486A000A576A4B63AFFC05082B5ABE3CC0B37B1E586C2107D97157E325A067BB86453414A15594A510DCFB2FE1A0074483120FB83440DB1B8C3B41E36364F92056083CB9CF91B39F28CF00F6AD098AA10FDB4B4D9B64ED1338E0D5B7A5169C3D8C0184B19966E54272F765C0337BBD307F8C97369A7A87DA44A5BF468DB8A9AA5EA598F885AB50174B0F9025A4EB53D2323D202A05265331FD836DF8E02B4595458551ABED8A3875B83BF976942372CB37296C813ACD2C27B41A5514B66AB25759009DB38A9D0473D5B7A9A7D6795F1188A079B1792A01141347AF2194CA681055D36E954C02D6935BBA7C2EF7F4B5E47C8B0A0069F29575E863967CE4C53105230472172FB79E69089D5A7BCAA95784BFA279EFE67DA145308BAAA1A5A303757946C2866B4841660A99C1968B8F7DE799ABD71806EB9F091397C1CC4171152A6AFC36BD733FC6C53545361AB6258CB45C9F1331BAEA85BE4558935984C081F73E4B377E0251CA7C396BBBB81D271BB9F0589E1BE3218B0B5840372253AA80A5DB79E11199C0832B2433880B68BD84FC02AA3CBBEC205EBBC7B050967B4DFB11E2FA63BCF6B7656A8028AB607CB084C21747ED573A055166F82215D7201D5D439A19F584F470B4272962C137B38545309547CEC25B09C96459AB7B4DA69C8D7B9277BBC4B5568813DA904141A011D9B45AC1F181273149F3C46F45CA9735221B97CB528E8AB59C5711A57C603F7A91803254E8CC4A37D84D1F6535E5A791A50145E1E073430810B3AB79DF4053538C7DB4826A1B428A84553BB881A23507385271B32F854706BB2D3E884E7B391985B39B7BA373071455187B3DD7DA75F6988BBD6BC39EF2808C245AEC9C024CA16546A16F63831A7B6797951A40894A5E38422F30B87E70355CCBE960B216592D0073F1240C21BB109AE76C9DE5B7835BC08AC6601C314A82232FA6F6896BD7834F0254BF112602022844F0CBA9FC3D2E3A58EDD56DDC498ADC9A03FCB43CA138640F85397FD5731F537D6BDC3AC76563D6516F1CF24F84B7C957635DEFBBB70071621C8B2585380A63660EF2CB6CA5910BAD42A1B621CAB8C26780D4251DFD1C6370EF12193C3CEF0223187A4557BC08F4ADD38239082384D084D2B67B5956A1463685AAA7BDE716AC1791935C47504893E18F24866531E34AD01E68FD6CE8DEE12B40398FFC74FDC4A8DA6785A966640FCC4F85\",\n          \"c\": \"BE483938DAC565B129658D168D494E522B52D031DE7FCC2FC6D52BDCE3F649AB140ECE5B25486B5F85D43ED6D85F6BBDC4141DCFA6C03F680C7B6D51484B461F700E207E2E281070DD48AED510A64E6849C462705AE29C566E6F2461F90387DAA3108FE9372A2B8D11CC2CD6CA20D9D1CEBC31C12B3DAF01F9CB67A4DB488DAF1760A48A29BB4E25A26752FF161B94DFC82A9773A8E5B9F761DA751FBBA982FEAB1A7FA3460CF669D5B8B3BEF8EDA6310009EE7130478222FBCC59CCCC248FBA6384DB7BF5D3B553C8ED134135F09DECA3877C9C4B22A478F892317841DE917E642B966906886358B09E8761E98EED4EC8309C578502C070E7C4E43CF2FFDDF1E4CED37762FC8D5D5C65348FDF01A0CC85314C022040982B94F4CC7FB565EB00C218CC61740062F896E992038F58D02B170DC903BB665B2A6CD724E201C17E646816E2AD528BAA20C43BC8ECC090F644256AA22FA3365820FE7C8AA5D168D67A21785D4BB2BEEE4FD3943FE351A0E94AACF9A5B4859EA97F3A5AECD213169356876B756137697F4C40A567CD960AA0436E61986407B2B88839FA226966271004C1445E057F932BBDE1274757A55F2AC8846FF770B1565C746814276487A9D3E454F5FAB0D77C82723A114BDE9882911A02192DA811D9B3DD2B2C7255C15E3346D6ED745C28A1F3C7BF4CE2DF9213E6FAB9CE90D7941C86E5EBA1CD90C9D12B94274D2D2C3AF727690A425BA8DF2527B26071D5A4C969EA61B646773810513A1AEF7F7E6AD5C5922569611CE5E94B674069C7914EB0CCB3DD03842A9C32302EFD8CAF9A1E4094339D7E857C994FB30C01D7F116EF66D8A502267848E38B080F0E5206DA26549FC7EC8F3D713F1241A09941CD7EA71DD86044F909A0D8C67361996D12E2D42C16E08CA7F789DF296C00393BFC83E47AA8130454F78DE07149D4FBCB304810BEDF462542B4B24A1A1D0A9F2B5B8706431287BA88B026E329E8865AB4F0AAD74D849F34945EDF6B3719E8103B110404A8FBC300592807851C442B506295B2FC76A600A0F9C3B3D796CDCD3C27B10FEB1BBBB462BBCE0BDD33292CD873D2396B0924BDDF8DA7408C4E680956DAD992E45925E9721985D4547BBE2684F4D4FD220FA87773447BF7A620F979FD529D86D2753F0E77C498E02B1EB55812D9E19EE6C99A61543EEF1C124716448FDDB46EB2D460179148DA2F01AA91C9B9B04A350A63D98B8CEB6005A39734C8F3CF9094D650812E1707CAAA98EC35D4ACFE425C48E4D8A1BF190DA3438684A27564255C8E5D1A97033F87077429711128BDF396DEB75E304376FAB9CC33EBA906D3804819534817EA309E3C260F9697F55BF4AA5C08A8A59EAB27BFCA0C2301434D7B490312CFB5095BF9948E3554E5409AA74EA7BFEFB9BC7CA61FAC565F2F7384F5832C2C29FC9F5D1EBAB56612C6696DC93FF21DB4DCD87F09705EE062DB948F68C6D5F7D1886059C87604089ADADA5DB49EA2BF3C3813A71018F1F559B2D72E35A013E3D9CBFDA480B43E616B9C7A\",\n          \"k\": \"44263624052C18E3AA23310697414499F1C0EAE45A1060D84EEB65FCDBCB5733\",\n          \"m\": \"76D04F481E68B2F901ECAB58B6369A2CC31A9DCCED82A1BBD426BE0AEE266AEE\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 28,\n          \"deferred\": false,\n          \"ek\": \"92D1A81751C40C606885C737EFD2B599413311EAAC707939B37500699131A44535F21C5AE596741F7668525108B4B7AFBA814FAC8AB0063B6A9060CED936CC6DA2CE4131695A89C35F2BA2F39A27D3925775FA9F43486E4C95C165A666FC3305AF30B419611D291775E0F08F34A65EFA146E46D207533B908F744BD246A94A4A35137731D02AC43E779E262A66F668784B30B231D83E4369400248AF3EE28432821F07B5020725C8D769B305B3AFA685A42E28C4F0E35BF407549361A67D7B6699CA0F293CCB776019585759502792F8D76A3698872F817A0C621084E53695701795ABBE16C466017BCC02B518EA387103C59D17127B844350AE428929810559A08BC91C2A29DAC3D6C14DA0979DCB4210142C6CD5B7CF18CB77E2E13029C3C23D2089C295411560024AC2AF25B94FBC14796652CFD8A524B6ACB8D9A262B7C26A279BBA7D4995A92A5500E081864200BFB51D46686AE14130E3C5A728FBB76944BA658718FC041DFD3A2480B9B6658A9D595BC4CBDC105BE019E128909978240EA29DA7C66664E17183E0B44969B284DB06D4311751DA4ECC6CC75C06395D5B9537078D24E2091AA45A92D18378415F1183C6B4E7546A1A1792CC07384106A5D5C8B1A369D3D6A8C83B927B72C1FDC7CE27449A5228C85BB6B0CFA85954C0CE5A5BB947F68C8107C1FF3B7D3D4900FEE59206B4CCAC5A1B4E65465609692F76227EEC0721A59B92262DB0F735E391343DC5836BBA779D6A558F8BC0001388E8363E3CB63CE49C4C7669C82B2B650B4611D094707571065B943F2108BCA33747367AB953D9423AFC5609591BF49B8A99650E4D8010617CC58645080DC0A141C34DE1D69E5932032E7B1BAB0CB2A8BAC3506B7D5E713DA79CA4E177A6CB27A545C9A80B3A489941A47AF84F59F292E314302ACB8EF0006F50A539E319951F6CCEE9F478773A8B0AE73C14B729EF4C0B89A99B87F4C9B8BAC735D31BB833342BD501EF458F955496138A6D07D1777A9489A24C74A5799A70C942FC839D20A8C228F7453BC29C02BBA3B0827801143F67691EC3481F9609BF79D9A8B7A1A7A610B05856B5FC8C3521968ED9695A00D71FE8C390C60A59D6734C608B7AC0B4643F7BA1DFD05B5BD853C9432269A9555E3912E9B263C7B939384A1794A50F8688296869AADB4B853091A291E42F485A6F93547E03BC1B57A603C81B7897198DC59252F9805A6266435EB2A26B6300D22667A878C3401800E6612C026C4F0FF99C889531D637036126227B674B95A38A2A93497FF83C8D3143A5398BE9C59909800B02C677B27A42621C190D865AFAC05513F72758B494585435F2357B97342D951A2AB23A1CF8A229BE909487BB2B8F521B09E0C4849632BFCC821CE30025B837A455B2D7D58EE4B0AAE1A25F8A5693F62B1AB77C229890899264BF63189ABBCC80AD1B8ADFDB21B0C2481342A137FCAE8A64B1E21C805B187AB7C1B637D57FCD8811E49C1D2A065848A769B7F02D99E40F4BE3783DE3AE4FE97E23CA716AFC0814C935293641D7C40EA1088EE89C2A43505237A593565A05065081F6181F35C55338C427CA628727DAAF8F5B5322E34488904949E45C61BB915525676ED2659EFC97C6A53376478B629FB32D49047412A49E98F186564A36EEF1CA4920C912B1211B\",\n          \"dk\": \"96B453C51B12941167F1E155BF08755F122716B9C82F92B94DA348B5BA4D8FD517A1756B82522444DB1F28172BB0C37658351DC545053D7995D6591897B39B3A7478FFCAA784D4210B0B291D81AD3921BF093B4329C505C63C1ED5B481F30CA2CB3A9951F00F5091BFBC21319132A0C4B46980248014967E687102D689B0CC04813503988F2C1FDC6CB62AD7616C1A85A12134A6F57F00052AABCBC990CCA1FFB91D14344E4A193F9F6346E8455221830D14B9216AD54E82432402CB51AC44A988A6A2D6035BDAA69FD46206C83B8FF43C33EBCCC47B22B3E57151E09C803869709A5A9597E1830B0C050F87580C23A64BE7C82506CFD238A36F34CF613CBA3AAC9358B84043D9A1938CA244454D7F6753E7A2012D5497F437662B57791189C7878B04617A719B659416BB22ABA04E01159CC7745802DB76F7334CE0715956065F01F8BB2CCA7F9787271CD75E4A918B0462B715ECCBEAC34ACE288A0A23C98924289DF32F13B04831798D9DFA64B229CFEB380F4F70A598A93A8C3A68AA7469C2027101B30A26298099A8CD456826EB5B21826A0C56F4C619938E5150523BA79595BA73B8460A681391B052CA2060AD68344A8A2CCD2F64A211C3A3802336FC583B7C21C53311CB89793D7DB5C68EA15FE49158F5E73E065574EB537D382C7D82B14231732CC1633686E53C723A1F43874AC6B543C4D99335B327DDA3BFC49119B00B3576321513D156E415A2D85689641A7A782800DC234BC8D589F5A384ADCA2A91053E41E18F2AB97F89E98B0C7984C0497EED8B2FD9D828C4BC7676C54C009D994E1786A4B177B7557508947BC8C706FCE8271D4A9630F3BA9D993ED9E82C5CE4A4F2BA83225C08E37A43116708A7B27C1B3620A539334F846FEF6084F8193071BA34CC9B919A0311FD7526C647A38674C81871B4F6D49DCBF83D46C545E5C543D26472EE9BBD04628B0E5434A2B342FF3A950DFCBD9EF078EE8BABBA09BC420967EA06832E78C2946746224B1FB539BF1E44CD67A9BD42F9C319EC7244EA5088949925778304E1BE08A2A9048B37B225325B22A3FCF2BAFF45C55110175E930BB7F6603F95577325B809179A09662E948453DF33313620BF195155C2D36E2B7B4BB4E6AE680B5C9A050A341B25D8BBA007B98C4AB554169952ACEC26864B9CD62B20A43BA2221903516BA174D25143D872B6D5C1C3961950D08EE1350E5700A111289987619E8FD5A18067A94D0B16D27318E4623BC80312F5017FFD241EA1A58E8F4A19EC0B4B7CE44E832A80D1C8C39FE7A535838179789115E83873CAB5BAA2BFEDA8CB07414C3BB0BC44C09D5765911F3233A7C32216914C520A5BBD63944C0153A8054B2E825A83ABBD3C564E7FAB19D081AB59AB138D1B250455B5FDDC16A8D080339182C1540BDC615D0B1C9EC1DC01455B4E59E5917D26BC0EC746A7D300D836B74D0BA16099BF4D87A9C6526BB8A5A9115C59B5F538CC0A17B3965513D92335487A4263C17AD4BA9C6144A0A4AA4DB83EFFEA9970B96C01260E91CAC3D652C88375C50CD0516929B364AB3BBEB15D68FA83C4F72449C47112557741058FDDD3CBD6898241E27FCB16090280002AD686F43A91F9186DC6898292D1A81751C40C606885C737EFD2B599413311EAAC707939B37500699131A44535F21C5AE596741F7668525108B4B7AFBA814FAC8AB0063B6A9060CED936CC6DA2CE4131695A89C35F2BA2F39A27D3925775FA9F43486E4C95C165A666FC3305AF30B419611D291775E0F08F34A65EFA146E46D207533B908F744BD246A94A4A35137731D02AC43E779E262A66F668784B30B231D83E4369400248AF3EE28432821F07B5020725C8D769B305B3AFA685A42E28C4F0E35BF407549361A67D7B6699CA0F293CCB776019585759502792F8D76A3698872F817A0C621084E53695701795ABBE16C466017BCC02B518EA387103C59D17127B844350AE428929810559A08BC91C2A29DAC3D6C14DA0979DCB4210142C6CD5B7CF18CB77E2E13029C3C23D2089C295411560024AC2AF25B94FBC14796652CFD8A524B6ACB8D9A262B7C26A279BBA7D4995A92A5500E081864200BFB51D46686AE14130E3C5A728FBB76944BA658718FC041DFD3A2480B9B6658A9D595BC4CBDC105BE019E128909978240EA29DA7C66664E17183E0B44969B284DB06D4311751DA4ECC6CC75C06395D5B9537078D24E2091AA45A92D18378415F1183C6B4E7546A1A1792CC07384106A5D5C8B1A369D3D6A8C83B927B72C1FDC7CE27449A5228C85BB6B0CFA85954C0CE5A5BB947F68C8107C1FF3B7D3D4900FEE59206B4CCAC5A1B4E65465609692F76227EEC0721A59B92262DB0F735E391343DC5836BBA779D6A558F8BC0001388E8363E3CB63CE49C4C7669C82B2B650B4611D094707571065B943F2108BCA33747367AB953D9423AFC5609591BF49B8A99650E4D8010617CC58645080DC0A141C34DE1D69E5932032E7B1BAB0CB2A8BAC3506B7D5E713DA79CA4E177A6CB27A545C9A80B3A489941A47AF84F59F292E314302ACB8EF0006F50A539E319951F6CCEE9F478773A8B0AE73C14B729EF4C0B89A99B87F4C9B8BAC735D31BB833342BD501EF458F955496138A6D07D1777A9489A24C74A5799A70C942FC839D20A8C228F7453BC29C02BBA3B0827801143F67691EC3481F9609BF79D9A8B7A1A7A610B05856B5FC8C3521968ED9695A00D71FE8C390C60A59D6734C608B7AC0B4643F7BA1DFD05B5BD853C9432269A9555E3912E9B263C7B939384A1794A50F8688296869AADB4B853091A291E42F485A6F93547E03BC1B57A603C81B7897198DC59252F9805A6266435EB2A26B6300D22667A878C3401800E6612C026C4F0FF99C889531D637036126227B674B95A38A2A93497FF83C8D3143A5398BE9C59909800B02C677B27A42621C190D865AFAC05513F72758B494585435F2357B97342D951A2AB23A1CF8A229BE909487BB2B8F521B09E0C4849632BFCC821CE30025B837A455B2D7D58EE4B0AAE1A25F8A5693F62B1AB77C229890899264BF63189ABBCC80AD1B8ADFDB21B0C2481342A137FCAE8A64B1E21C805B187AB7C1B637D57FCD8811E49C1D2A065848A769B7F02D99E40F4BE3783DE3AE4FE97E23CA716AFC0814C935293641D7C40EA1088EE89C2A43505237A593565A05065081F6181F35C55338C427CA628727DAAF8F5B5322E34488904949E45C61BB915525676ED2659EFC97C6A53376478B629FB32D49047412A49E98F186564A36EEF1CA4920C912B1211B1EAAA1990D4FB6A021D7CF8F417D45FE1F49BA84E111A448E8B7DEBBEF902A966BD5EEEA2E6D38487D083F30093ECF02E7EDC4FD4585C73EF6E71FAD24E15E2F\",\n          \"c\": \"2E7CDA2E97146A7BB3C33C5EF76D1A4F4D93A59F1B8441BF6A32D88EBA5609490CB3283DE2C43E4D1DFF2DB55E4DB9B4C3A377B3E9B33FF1CD3D6A2047C7FE0B6D8155DBD4C0296E8CE60C74DCC82080E31AF13169D638EE6396439F49AE426BBE5AC6BEF9B2BFF423AA24BD2C168E0F4F2078419A5865F1808B866FBD19CC221791952D9C2101C3EC3A6F597F97C2268F8F6FF273E4B443B8E95D93B6AEED85F71509ACA3F366938E6BFECC3B0A35F859D3EB486BF321A1F3A7350B39F7A89773DA2C5B235132C9580380DDCDA3A910E89734F03F871FE504BEA38918299DFE7C9F60A6E4CB607768F0A3338910D45612B31BFB6A0424489E0A4E514D2F41C3B4A0001E794A5275F8D047C892870E647BBED53BEE167BE27EC2A43D2D7DC10982F96E3B586119D27EEA5909A18800B79644FC9D15CD7D2200229C1380FE2E939DF89FEACF4834DFD1D3C8ADDB8F365BB94359C4698AF15AAFD4F3289233701C217CB4FF979EE781C8420ED9EEFF53D58F046B774B821EA3021F7DFE33A79F882C955C86FED0702AEABDCC6D32186B7D40DD325B9FB7BFDFB1D34C63B19433F0D80739765EB9D8BD210669675DB3F4349BBF23B49B7A967CA2304ED8F143D27981C26FECCB1658B5BE11DD858BEFC3DEDE25DBA9FD22341E63A5884C41A0ECB68C543E0B021135BE381D42DDB9F67CE1473D8840E00138B39998018E0E869FB0F94823A5191B928C7D13F157318901EA8F8E5A5A0DF0ED71FD2CBC6489A46E5171FD14A09F73420C77947941DCCB4F122866E93D94A9DB0030A20663705B11C93E89396F1B7E7728B6B450AB5DCA0932850190D712E3F27EB207473D18E29B20B433F4E6BBC99B28AEFB5ED0DC74BA529377F0F8A93BB7208CE98049A862FD513E81290187A5B2765E4EC5B4F211058310D0396CFCEB90B9E86E681AEC3D3D81C787A3BF16A412329AE643576A50F2A72E59165AA357ADE9C194A4DE0ED5254FD206D05BC375D1B5E8960B7293C768B7A66796DE0D5587752CDF7921C2053A5A970B9FEBD7A20F336C93839D567D1CE241F061565A893A409EAC2645C02D3FF00AA024F31E50946A8CEC435508486AD757114FC138E57B42F2CE12A248355CF35191341892DD910DF5528306C947B0ADFC0AFAD68DE715E8B2D9A43B8858BFC04F73B44A04C4E0D331DEFD57587276B188965C5924BF1118713C05E975090C52C4DC2BF7BCAF47E4E274DEEF4FEF3D91EBA65F616B8C476FB9EFCE61CB8A0524D97C27491A0C9BD7D99B0EDDB2A3E50248793FEF1C248C15301A3B765E9AE21FEA0AF86F09A5BF42D21638FF6D169D6127463962D3BA17F5CA63ADF63F317CE2B7CED21311A05CA842E0DD6664953DA479851E80F270B4A7FD11C3FD6A52862716AF8A67FEC893BBD104F5394F118D579B787730D6C37AC242A328F724DE9C0AC6E091A3E4CE01E29400836ABB6D1363E049C3CDFF2048F0FB1D36FA1B70070576B8A14E766CC098989EA9C624446DA2D4D45E7381AF63041EDAC0197149AA0E\",\n          \"k\": \"69B8F091A450890C0DCCE0120E9BAB05054C7785A797C93B6FA39FF5E0BC5A70\",\n          \"m\": \"FD3C91294D8C974930B4B6135AB647D4A7885C83FCDCB30CBD38332E14094491\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 29,\n          \"deferred\": false,\n          \"ek\": \"CB2468A0185567F8A60ADB33CA5239C11C4A3E0C031D385DCFA28C3AE2A9F71904BF379CB9E0BEDEAC82B2A537357A9C3AD33362602A1CF6458A745ABED9233A092B962BBA2B0A66DD4A85DAE11B0C28230AA44C40F2B68687B7D54833062CBB5B0233E25496D1C84204B2BAB06050C00C308EF53DD8345143BA810AAC477C8119B1C595D964B13F837849B58D8E1BCE56437F2ECA84B86C91173670E0995D9769642B0AB0BB0713D313AEB7C41C9B2CC411B3B62B110308D94F0DA1BB69D406DFE7286519692502BC83E15BD50494C1047980AC6043B18D7CB72EECB00EEDA515C97C0ED08B5D4BB001BBF08D9821B9A75C02666C357F00278279348462759A602359F5A953247C1928172A013E3C53B7E95B81A64A6DB3AA86CD7670CF7C38AF357A317B71671B950E2BB85EAA6138A5AD93A1640618BD98092D85D36015C0B0DFF6059F3B09AE5007F89AB551230D8B9A2BCA59238405372BAB7F4D8069FF7457EF3720A171C3A6312CC8D69191CA909A13946CF8577A96086893C0C59B026A21835543B8583A05F569977E072299BA580C179EA2976810AAAA93F57DE223B97CBB4CBE559E18139372EB1A2095B8B509AE2CEA5B4944335ED385B23B5EB7D0847A557AAAE89611937495C825B7236280ABB6D409CD513A03C91139A251B3278C929DB23BD815BF68007D50B8356C03BE88F86E410ABFF6CCB3B11BC5D41C7839D48F365AB4E9E29F0B0094A4C4ABBD6761527850286432530241968122D2BC877DF5781D17AE12095C177B69BE3B989E81CB539A2FF2E28D623303FED5750C58B9CE051A6B813899BB3C74D82B2F127E51030BCEB396EF547ED37ACAF1D46A77F7B2B24B48AD399B53FAA69C079FFEFCB9F8367F7C513D142C8392A8BE6089CC2301685460BCCB19BFD6AC821DD840A3DA30C3208668C7103EAB78C6520C1291239DF8217C25450B70302020A4BFF384F0619142A04C769C9B18D27E42FA30BCE60A1EC2AFB618752A3917EEDC37BD15C44D0ABA1FA3A40C23AA14C98E016A592B459BF4C13C07354A1DE0539445882E21B97B1767015810325086AE71CDC0B5C0D7287BBD99B381680BD6559B04FB84835C4419316FCD223B84D03816D86C738334FDC894BC81B1E26813A2159D426AB1FB70A88FFC1782E649BD860D148B33F7607083A5928F1C835F880DC9E3235EA51E78A403F7BA726D17951CB7AE7630AA7394A80D95A1EDB1059F140F3EBC8D1C9BA9F43B75ED90538CB219F8E4A2B202084E049473D57D987CAAD2B51DB009CF8A538F8205B0B4049F41ECB344F84C538CA49F5A8FF309D036C6C520932F08072F5678CA68568A41330D9BEB34BD308F963224F80B5DB9B10E54D146CFAB5AEF84A7F9C62B7BC24A26A578FCC51E36738BDCC8AD24410155AAB74D8691C1E699E3722481BA074EEC75DAFB723863C308F60E10D195617BCDB877153D9CAA775647A83B1DE113019CD6C3A7CA38554023A6BC7EEC594FA6C14DA8353737F0A549715E39C49FD9625F187C3BE1602B6BA178D1784B52690B1E1380847203C13624418C1C1DAA0B231AC0FB39293843CC3D6B48C32B15098748DB0B3672377407EB7441B82371B56EB3E90C983A895AF85D57E76C53088D944840CC309853814266D66DCE88915049579CC45CD602\",\n          \"dk\": \"58C845051321CDE3A3C19C0D3BDA49688751B7C113CAA67BDAF87204D20ED9D79E39D135E64708827BAF19D25807B35F76500445B971DEB6A7D2680D46032490D042FD59B963291B5BA589776499DF1635B1249A12C3CF42322EFEECADEC1A243738260F2503364087A6AC839D1B5B85DA2A0B337BEE74761683A0ED501F67869FA587CEAD708A2309329A7483A8A3C895241A15732C46E2A864C94A33C36C6A71400E70678863B89A5B6BB8B0129679591A87127518BC2D903F06826605D7CC0DEB7E5A95B5BCB691ED852C94CA95683025771235156B406DB024CCD430822C785C3B265633AAE6BC18E4BBC4C6C983F1F0B0F514001E4106F36CAECE1380D661242E43B585A8B1203431BF46BE69567B9A883E2F67539B4C62EA6294B7365796578FAACA0F212B8B87B2AEE6BA128A07A03F20085D8122182465D46A8C227363EA7B4400920CA22C454CEBA538E5CAA110B7762087B86B37CC1932A2B5595D265F3326335D407712D80EA34C4ABAF0BF9D4429300B14BCD2B9E186301BF25C43EB544FB32D30BC6C643BB028577142761C33243D9C038BA0BCBD7502B63196AEE79A4D8DB630BC748D7220C43F76C2A77A7D60F728EE1133DB0C7AA8F0CD1DC0100B85460EB31D0BC9658021443D6564C0D3AFFC6CA1E59C5DCCAA454156923CE7B2E8B123B1EB39EFBAA7EE0CB17F627C79B690816B5EB317210C16A1BB13A803C2453197A19E2728C9F61017D20D6C19C48D21566A583A848A3BDE79628CD4150A30AC1C44AA2673C1810B89545971D9E4199A12B82FC05774475797C6CCD0A9C699F50F0CF663DFA14FD809437E0889B285959DFA29A08040802BA77E3C3CE275CDC3A92388518C9B70BD66BC3424F67FC9FB924F59C500D2C138E7C06179A3EBD800D6F96490DB82C041AE9C958D5015A66D91320D86A243AABB8BE17260D86AE4F5A6229859811552E0BC1000F3C47543698DDC7A61CA5F68E68D47A96733F32C8437B893B10FCA1B0A6866ADDA2CC610E22D155984855A9CB99339FBCCC972F62867D7BA04DA7C10377419E36D8E3876B2C5ACE1380A505CBC418A181196B150B53F5FA3B9AC08CF34E78A39A9B66559A859EC22461C9D836CCC433B8342C94712EC46BF716D0628AA3D421AF6A24B02F38DD4C54E3CDAC15AE47863B0AF21214723FB9592893825031DA00664C1CBAC1585B60E7A71022A35B3E23063CB49B278CA82B51EB9E010E6A3B7C3227356733A6ED0BD3D84A6B7C1C0BFF42842168F9703B1636698B171902AC4775E1A15F0A44B39E9247FB61C0BC90BBE2AB156B11EB2CA98BAB75DB92B844154258FC6BB29C164DA177441968F094941C3FB417C078654CC6358C6143FC32BC1518059105EA8DCA282F138C3EB61A48B2D63266012966BA12654C7B4142129AA0D5841BE0B5784A94CF1007FFD5112EE21AE82D7BF3FE0B1509710569CA6749A0B706C5B79A883BA985967731276FC2B8431BCAB9B1E2E9330FFAC9DF591AB5B17317AA93D8903708CA8929EFB8DBB2861E067CF57A2606F174C5DC80FB26A70EDEA0D849A6977D39866A78F3AA417B3B87D1EF00A7AE2734F249D64C68A84F016DFA57C8FE72443848EB058A5D5B784CB2468A0185567F8A60ADB33CA5239C11C4A3E0C031D385DCFA28C3AE2A9F71904BF379CB9E0BEDEAC82B2A537357A9C3AD33362602A1CF6458A745ABED9233A092B962BBA2B0A66DD4A85DAE11B0C28230AA44C40F2B68687B7D54833062CBB5B0233E25496D1C84204B2BAB06050C00C308EF53DD8345143BA810AAC477C8119B1C595D964B13F837849B58D8E1BCE56437F2ECA84B86C91173670E0995D9769642B0AB0BB0713D313AEB7C41C9B2CC411B3B62B110308D94F0DA1BB69D406DFE7286519692502BC83E15BD50494C1047980AC6043B18D7CB72EECB00EEDA515C97C0ED08B5D4BB001BBF08D9821B9A75C02666C357F00278279348462759A602359F5A953247C1928172A013E3C53B7E95B81A64A6DB3AA86CD7670CF7C38AF357A317B71671B950E2BB85EAA6138A5AD93A1640618BD98092D85D36015C0B0DFF6059F3B09AE5007F89AB551230D8B9A2BCA59238405372BAB7F4D8069FF7457EF3720A171C3A6312CC8D69191CA909A13946CF8577A96086893C0C59B026A21835543B8583A05F569977E072299BA580C179EA2976810AAAA93F57DE223B97CBB4CBE559E18139372EB1A2095B8B509AE2CEA5B4944335ED385B23B5EB7D0847A557AAAE89611937495C825B7236280ABB6D409CD513A03C91139A251B3278C929DB23BD815BF68007D50B8356C03BE88F86E410ABFF6CCB3B11BC5D41C7839D48F365AB4E9E29F0B0094A4C4ABBD6761527850286432530241968122D2BC877DF5781D17AE12095C177B69BE3B989E81CB539A2FF2E28D623303FED5750C58B9CE051A6B813899BB3C74D82B2F127E51030BCEB396EF547ED37ACAF1D46A77F7B2B24B48AD399B53FAA69C079FFEFCB9F8367F7C513D142C8392A8BE6089CC2301685460BCCB19BFD6AC821DD840A3DA30C3208668C7103EAB78C6520C1291239DF8217C25450B70302020A4BFF384F0619142A04C769C9B18D27E42FA30BCE60A1EC2AFB618752A3917EEDC37BD15C44D0ABA1FA3A40C23AA14C98E016A592B459BF4C13C07354A1DE0539445882E21B97B1767015810325086AE71CDC0B5C0D7287BBD99B381680BD6559B04FB84835C4419316FCD223B84D03816D86C738334FDC894BC81B1E26813A2159D426AB1FB70A88FFC1782E649BD860D148B33F7607083A5928F1C835F880DC9E3235EA51E78A403F7BA726D17951CB7AE7630AA7394A80D95A1EDB1059F140F3EBC8D1C9BA9F43B75ED90538CB219F8E4A2B202084E049473D57D987CAAD2B51DB009CF8A538F8205B0B4049F41ECB344F84C538CA49F5A8FF309D036C6C520932F08072F5678CA68568A41330D9BEB34BD308F963224F80B5DB9B10E54D146CFAB5AEF84A7F9C62B7BC24A26A578FCC51E36738BDCC8AD24410155AAB74D8691C1E699E3722481BA074EEC75DAFB723863C308F60E10D195617BCDB877153D9CAA775647A83B1DE113019CD6C3A7CA38554023A6BC7EEC594FA6C14DA8353737F0A549715E39C49FD9625F187C3BE1602B6BA178D1784B52690B1E1380847203C13624418C1C1DAA0B231AC0FB39293843CC3D6B48C32B15098748DB0B3672377407EB7441B82371B56EB3E90C983A895AF85D57E76C53088D944840CC309853814266D66DCE88915049579CC45CD60235DC954C8CA6DC15AB79B2C974D77BA09F049C2007BFD5F81A2BE06F178A0EA5FA1646B083D3C34FDC56A8B5797E26890EC84F86E18EAA17ED3DDC78300313E9\",\n          \"c\": \"1DA1EF5325F46C686D3AB385F8AA79758CA0E6C0092265C636DEDF9C5F34A0F7A36783AED59E21EFF5A8CEA55439E5B13C42AA68E1C19BCD0CA8C629FFF79198673D416A9CE82DCB80D7905968B02E84EB04005D0AD971700B87A023708F169369DED4833B8C13C8C277CC1CD7EF32488DB63E5C1058CCDB73F88A679C41A36144EC2866130D68914503889E783A5E28A1E701B0C198AAB245E6F61337CC9B1CE2CE8B8CD6EB106B969E120CD09EE174E458AAB80ABB5795B091E07166A39F15349C0EE271D063100D07E46E9AA07DE76DF152753EE298930E0172900F7A4E47128E5BE9CE81A317B07282E3735AFA02FC0F89A6561F5B4275E3DBD31FFE2A04947F8CC6067C3A8E8FB625E6BD23BC20F63DB535FAB0E2C44CCD50339959D3A83AF0FD57AFB2C6BBEE6B9920D56A805447CBF7ADB6F957B9DDE850044E7DC47ADA07BAAC747069241FE4B46F1F1DD8DC2E4BF52ED6792ACB987A1528B89213E10EAC95D86519A95EF6EF5D9701971AEC0608EFA2A51A5D0127B3BDFED8E8107FDA600D17D913ECDD8D9860C16E8788CC9CBBC99EEA2A7AD8CF35B85670A0B15607F3ED98D88AB1A6585E1E0561C37DCE34AA00757BC1F6CFC81C7BC2EDC7A011FF12C1C35CEF9D1F8BE5B80860B5ED0707A04472E94D2C3D7C1B1BA4611EFE6D023CEED3B486A066E3B0121687DD9AFE0C4771678EB7B0D85D249C77BE8721B89DC086C4C5F14D9851C51D51CA2646A32929E36A33A35EFA58B0978B2DEE5CBFCD23F3B830CF1AF3EE6743538F82E246F7A9F76B6B8E43C84C9539EAA2A0DAB6EDECB4061B0B211C5547574088B8EC42BF6F21FCF299BEEC8CFF41CFD1B49639032F4ACAC92251B9F37CBF51098F4DEE7D88363A1910C9A6BF689E8DB93EEDBFBE8FACC4D1707686E1BF9E5E790DDBC6874218FBB43128783F611D1EBAE677D526057A87FD33AF449648EDF506E93342CDD38AFB6EC3FAE952101B384E841D889C025FAD91099F2AE41EC3E3DEC70252663C01B4B04EC1501422A97B5AD5AA27CC9EBBE2C22BC22B8C706F04FEE274764F1DBC4CA60DC56631BB2CADDD5399A2F061FCC21541D2595D15CFB6DB464775D4ED48559CCC97DD25F64CF2FCD30013EC35AFA96C1E3368CEAB29AD03FDD5B9BDF1FA1356132210702466719DF52FC34A0A1479FB913B6DFD9CCAA0F9D672AC618591B808B4315A5E17889D99E271FCBBC3C4B496DE8179A74C1293468392E2B592F3E6925B9F81604790DDC3EC0D1056F31F3184FC0330497961EC8E2737FE866AE4C262E5218E06EA7C24B464AC7D5FBB44069B9BDAFC96E014DDCD168C457140078B0A7DEAABFE04773BB1335497CBCCF4083E6D41288B3901029F1B266AA938A9F14763C679DF1E1C58EF406BC2ACE2A236B37557219DA24812036E557ED6B6C1A3A1776C5C0E64E1AE1A2A0747CF2CC55E32D48A7B1387FC9158222AB2582AF43580043F858E527B25379081B97CF0BE6AA5653E186CA066BB7D57B6C4AB8131C68423B12622CFE234696D761E\",\n          \"k\": \"C21C8C4B59906D0C4ADB1F3CAF47F9EB326B8A62B3392407211D502F40C7E07A\",\n          \"m\": \"7DB18CA35A53AB3A65E4C17FA096DDECB19FC7747E657B49D1C1710DBD1D197B\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 30,\n          \"deferred\": false,\n          \"ek\": \"0F613B04128F82A73867D9185891C29D6C3E1381843BD502D86099A740BAD5BAC68C590510CA3F6F2B5463B264BED34952F10C784A92AB7696268410F1F28C06DA7A18416A7B1B5FD23393C33592248C9A8B3956A999483E000F2A2C6F796052FBB22F7F182A191602FD93AC066355B71B7E6BE36E531B1D34F0382E34A4CF623B7B1127519A7BC4EA3FB0D1C91626A417B6129DECAFF2865273F759B4DA1A95F79FAD0CAB09FB61D34B9AD78B8046F5601FB53D28951AB73842B8921B2FF10417DB4F9964A41EE820FBA83D61D30D0EF2C2CBCB1F6CCB9E77523DCCBC37ADB8B9E31A0F0E1C4192911060E8677140690EC671A5445F42FB1A68DB4D1678BF9B60129D98A859837599ABA0DF465DAE76972946C73E8343A19A3C03657806574F59D2611163334FDB0EE8B0C13C679C6D175C22807C86F0C199C89CC43C0DBE6587F0A36199143EAE30116B3B0D49839AA18CA49E2992740B5DBF1C91ADA352D39AB7D0C23FCCFC41E783CA0A333FFE00074E72BC834669931630898B718CD5304E253071E730F1EB067F94861DD98FD9A262FAC919FE870E3D21BCFBF67180C57A5D2797F6C7B96F544CDF92C3FA8B49EB366C02885140D128B88225750733E6105868AC48A468002F35C34C38AD70A7BEC9C37713B98D9EA8A716CBC85D0C3EF5B4BF8F20BC6BBC61CE8150B4F842CEE0A40E0A7FBADB76BE9B5CF2B39749F51BEED8400822044E2CB01C021B0D9B7FE67A8AFF9B227C120F643B85152B40EC4A1734E2A141159A5A96C8B74115A0C92E6B913464994E980A8A304194A595BAB892801C15CBC5033FFAE70F368A62C65A598773C199E20EC07B73B1ACAAF48CC963A6986D9A3D2D236352F66065025751B9884D7CBE3A5538A9B90372D4C22797659B5CA8D357CF00B27C5714344374A5131B261D948DE22390D0521F50553E78223388903CCF241FD608CB4FC1AC7C44A2D3141F0B998B99E704ABEA1E634C1F44231D347254DC2CCDDC114AC4488A549BC4696400F0BABA18198F46E8CE43C93581FB661C745AB5A5520D4450982569BA1B192FEACF37EA8EC8B8B267271A042022DFA177A7C1CC1CF6765492AEC59729D6F61690F27B5D010F22C30E44D15864395BF01771158BCC5F8B48F656CE5D9049D6F1C11CDA96F8C724E0460BDAAA1DD9CCAF0C46162B943461B729AF3543BC9A8CE3B34971B39DD25A6051D41A248BBCD5555D7E2028A5B453FC4062DAB1337B134009C71BDDD705C2E44C64E15C18F26361D8A7A2F7A13D3C31CB3147ED777024B8A90B558812B47AC861BE5B894B989200BEDB242E559801C164003C6EFA61AC34649993F782CCA150DEE1443892BA3DB87B094C491C798B5FA1AA30491B7C7CC3769476D28AA17E754773390316647CF33076CC6CA84C804B26E75295D2A4754B505A3831DC793ABD874C6C1451911B97AD39825EA42D65DA63C42CCE1EFB1397167F1E8933399C2E288AA57E000406A4C66B5A357AC59036F37A52516701B52934C6A1109593809C0F72170A0EC9AD22EA6DA64B0028680B405924287280A8C49B2D516DC9D6B93D42A9B544C6833C0340865905EB00F4661B200686A0A47FE280937BB00F8022B8F0E64AC251BB62D09FBAB3E7C79CCD450EECA94120B05A0B071588E2150EDA6B14150F\",\n          \"dk\": \"E434A051F4253EDCA7BAF48841E07438B892F47ACBE249CA763061CD3B6C9112629CCB760107BA96C260793CBDA5BB4EFA184DE27432683C96A20B39D1B72C4FEA3D05D299EC16CB7BCC7C853B83E92AC8064C38DBE256C86C711ACCA9B0C480A0444C6F8C8EB187C32BE0C5A1F20EED5365C32656AF7335275920091A38EFB8BAF8629E87928E51B2370FEA2B66D188A09264D21CB0AE66B5590A051CA16CE77CAE914003AB994691DC0A09FBCA27644A7E48405AFC5F4CCA9525C113DFF133C1BC057E742FCEC38531EBA8E2D252C543172459817C18774CACCFEBC227D267B5684C325E17C0EAC74B34D22E136B98C9A8532139ADB4ACCA2F3A63A21751D0324CDA8A15230918E3856A07C4A51813569B76093A14771E3606A33C389ACC42453C05027501E9B5AE311C9D2D640E165816FCA60BF6C5423C9C585D642151F7314C9C656E8C3572C407C6DC0EEF1169AF4B5B130ABC0507ABBB52681135984BAB688AF080F567B93F1C2C37E231828A3106600FE11429DDA8C673380105E26423A06BC97BBB7B619A5E54429EF549BD9120BE23892073777F8C21E3700C90751BE53658BA9035380ABAD8A6522DA19ED801646A772353DA9355A75C65164940189A79439985545863DB9896F95F7553BE184ABD982C4EE692CC08046A8951C95EDCCC3B5B942359B82FF4C3ACD86AB0AAA5DEF49ADBD344291A8A15F613A1A8591710BC1B854682659D7FABA1D05924F063942E384953EB59CCD55F3EA4081AB38E13AA12DCDA528CD06907018E9C287042583191B8BC75767266A0C9AE074F0CC4B946906941D17591014ED2FB4A0A3A01179C26BFC85A5EB9A859284E13F04F65F589FCB9557EE5095AE79036033E2D5336A0AB191776A4CF01A6F45B62F3B8767A8413DF6539A5B1781DD30C2BB41A940A4E2D209526E600C713AC522C104BC8C428895F35547E4DF1214804869EC5B92BF9C2FC28A361E050182805E4919BB4EAB368EA0417E03D8611CB09F884C05B3E66294A93EC62D694BD5393AB3114046E29B3B177B997F7CE9E2305BA257A923B10C7A3BE7FA881DC1376FA879BFBDBBF0B8000DEF00DB26A522A96598B450D5B39B943553291D42AC6EA0D1FD04247A41420DB7314F676557765FBB15BBE314EFC968C76074BC3B609268C264B0441EAF92E09E59793273DD8B50ECFFC103DF067496C8015893D05DC28ED3445D726BF0936145720CF7794290FF189B8C38AAE3BC55D2167BAA3AE39BB90D19948DD439C7D827CB2F37C17B0A0D8A74D6CA82D4448114DC72D91991972F122018197F192B4ADC4AC41EB0F2C220DF2A94E61463BCC105A174C985933B09D3326AA2989E2CB4BBD7C5B557C46AD9990BDE8183CA63CDFE98F51070D97C7AA635C8149276C58E9CFC314BE31455939C854B03055436650D3651C3D566F8129648749499AC1946F120D02A6CD230100D5A2788FC35D4C47B7CF086363193843523A81221FD09461F06919DBB5287D727957EA4A2E448D0D8CB3A8860E71E8C60EEA37885878B7E07DE6323908830491DACCA88B50CB901B384ABC4B33236F727D1A5466E5506F6100AB16AB4567937E39A2605A52783D574DA5C961254704795C450F613B04128F82A73867D9185891C29D6C3E1381843BD502D86099A740BAD5BAC68C590510CA3F6F2B5463B264BED34952F10C784A92AB7696268410F1F28C06DA7A18416A7B1B5FD23393C33592248C9A8B3956A999483E000F2A2C6F796052FBB22F7F182A191602FD93AC066355B71B7E6BE36E531B1D34F0382E34A4CF623B7B1127519A7BC4EA3FB0D1C91626A417B6129DECAFF2865273F759B4DA1A95F79FAD0CAB09FB61D34B9AD78B8046F5601FB53D28951AB73842B8921B2FF10417DB4F9964A41EE820FBA83D61D30D0EF2C2CBCB1F6CCB9E77523DCCBC37ADB8B9E31A0F0E1C4192911060E8677140690EC671A5445F42FB1A68DB4D1678BF9B60129D98A859837599ABA0DF465DAE76972946C73E8343A19A3C03657806574F59D2611163334FDB0EE8B0C13C679C6D175C22807C86F0C199C89CC43C0DBE6587F0A36199143EAE30116B3B0D49839AA18CA49E2992740B5DBF1C91ADA352D39AB7D0C23FCCFC41E783CA0A333FFE00074E72BC834669931630898B718CD5304E253071E730F1EB067F94861DD98FD9A262FAC919FE870E3D21BCFBF67180C57A5D2797F6C7B96F544CDF92C3FA8B49EB366C02885140D128B88225750733E6105868AC48A468002F35C34C38AD70A7BEC9C37713B98D9EA8A716CBC85D0C3EF5B4BF8F20BC6BBC61CE8150B4F842CEE0A40E0A7FBADB76BE9B5CF2B39749F51BEED8400822044E2CB01C021B0D9B7FE67A8AFF9B227C120F643B85152B40EC4A1734E2A141159A5A96C8B74115A0C92E6B913464994E980A8A304194A595BAB892801C15CBC5033FFAE70F368A62C65A598773C199E20EC07B73B1ACAAF48CC963A6986D9A3D2D236352F66065025751B9884D7CBE3A5538A9B90372D4C22797659B5CA8D357CF00B27C5714344374A5131B261D948DE22390D0521F50553E78223388903CCF241FD608CB4FC1AC7C44A2D3141F0B998B99E704ABEA1E634C1F44231D347254DC2CCDDC114AC4488A549BC4696400F0BABA18198F46E8CE43C93581FB661C745AB5A5520D4450982569BA1B192FEACF37EA8EC8B8B267271A042022DFA177A7C1CC1CF6765492AEC59729D6F61690F27B5D010F22C30E44D15864395BF01771158BCC5F8B48F656CE5D9049D6F1C11CDA96F8C724E0460BDAAA1DD9CCAF0C46162B943461B729AF3543BC9A8CE3B34971B39DD25A6051D41A248BBCD5555D7E2028A5B453FC4062DAB1337B134009C71BDDD705C2E44C64E15C18F26361D8A7A2F7A13D3C31CB3147ED777024B8A90B558812B47AC861BE5B894B989200BEDB242E559801C164003C6EFA61AC34649993F782CCA150DEE1443892BA3DB87B094C491C798B5FA1AA30491B7C7CC3769476D28AA17E754773390316647CF33076CC6CA84C804B26E75295D2A4754B505A3831DC793ABD874C6C1451911B97AD39825EA42D65DA63C42CCE1EFB1397167F1E8933399C2E288AA57E000406A4C66B5A357AC59036F37A52516701B52934C6A1109593809C0F72170A0EC9AD22EA6DA64B0028680B405924287280A8C49B2D516DC9D6B93D42A9B544C6833C0340865905EB00F4661B200686A0A47FE280937BB00F8022B8F0E64AC251BB62D09FBAB3E7C79CCD450EECA94120B05A0B071588E2150EDA6B14150F81E7F3A4D5E46DC6FA36B4C63BAC9B8DB69CD90E250ACFD99280A13C10C4F6F9B02201A4ED8B58B3F3F38D4B28A3C6E87D6AAB11566531DEA6FC00781E6216E1\",\n          \"c\": \"A0C773196F91C0A7A3CD3BA0764E4FFA331F6962116C3B9FFF775F47A02AE2B0BE69FB89CAD33F5E059E051B92FA124FA25810EDA08AA89F4E5838A250315952E85BF73246C4019DC0F8DC7E6FAC2C0BD1E0191EA0032221F4C5549D914145B3BE2AF25886DB7526439BB9ABB6EF57C959D9CC76404FA02206B5CD4A2EDFA23B9F137729E7FDFD46CE8CB326CC04E73EAD7DBDA6C76EC19972E10049394E03BE7933315AD8B4DF72D0582EA9E36205F07A5B3A0B007A683D677D4571B907F0F967227E5562873D45F96FF2A117040EF2AC2026BF1B6470FF40F50D0A2E53979F3F61AE0E041EFA26E058F753D2436AE9DF06E70252268CA9502859C291BFED18AB563C2A5A74EB4E572E1A916C75E8E7C6B31FF39EC44D29B581598439F8A5E7FDD72C720E703FA24FD10FDEA3A43B06CE30B5C41D2C891724C7872642381860252B20345665038444E8E1167D5CCA83FF29A40C306E18CEE1B22CC583E26B8FAD23D2FA3862CAA0A21F6EF090959982E0E6B1E58413969892A41614A76325DA18C7FE4A73F27FCF275A3495134DF24D94AA1D4DD96174922360F39441F69F8F07EC5B060178AAE1CA5D00F3DD37ED4B55DABFD28203E65205AAAE8B2A885E211A5E35B9FE23CCF991A7A5C156FF8E1B0253B42AE6A4605A55C1A0F47E8054D9195D4F2496458EE4FC64DFA8FC4D2BAEE710900120ABDFD16E6AEA23550BB1D33175B9441E04BA281998D89CF3DE29184CC2EBD2626A1FEA4BA05D1867AD3FEE28C7154EA5FB0E463268B2AD17DF5388C2A7259F044CAE83D51894D61FBB4690E6DCAE9822D63A39B2D887BB2D81E8D57085C8D52773ABE2AA9B8AB04735669311655A3B6AEA829AB486B54E54F6E2AC63FDED2FF90C8EF88C9E2812218433B59677DD9FBAB3E15558DF418DA6F3D61ED45900FB3415B5F1A2A7C600DB715EFD6A17F729FA317086180BB126BF26FDB8637B8D7C2523651A70E233C5B180403B507C04D93DD01A60591BCC77D60FA874CD3DDBCCF7246ECC63FF447DB9CF31C114D3A9E4518D672E803E0D5A9A7821855340DD65FB91157FBA1005EA5EB64E4AB762210EA81E7F93F8F3DCFA6164C5F68060FCA6E3D52F83E6FBABC47B3684C96F5F718EC731CD5CE61A3C368AF5E68EF74020A2B143D37AB268AA4DAF203EE27702DA2446915D000F55B90E2C67B01D1B2ADC07D613B6C88760E6AF7B50C08B88446B43FF419ED8994B9E380A8E35C60AB495DBFD8424F50BB85B1A4DA62597B630811EC6DEACCAB4B049AD3C99DFB034A91DD144D757D88B6DFD0E1F62F4ECFCDD76A40F6B01C6CF27FD2B3B8656CE335A194A9A7E04FCC08F234BE1ED5DFF2A73650EFE7A65A8F9298F522926275FCBA55BF167DF37CD48208F37894949973E3A3E8FA7DE0F45317C01B5A5139BB30BA10767DDAD39864C7DE034601577C600C929245C97016E82534B74575135F2326303B1532DAB96500D314B41903C22D79F7067FE2BBB33424238AC1BAA581BBC6E0DDF3A0162A8066D3D88CED57736\",\n          \"k\": \"7265696182169279EF65779A021AC0A0E0E7E4CFD37C8546D4DCB1BF08572AA3\",\n          \"m\": \"876B17263B409171B746C6936EC65FC94137F958DC974BF98110A1D07F6D95F9\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 31,\n          \"deferred\": false,\n          \"ek\": \"C9CB9FD04057EB96006455C062E3C0722346ADB366DA0AB980C782C417B360EB1C1F6762EBF967D713A0D93A28CF9206E95451E91373805047D8A14FFD2041B4468B26C79B697A14EA75A33876BB865096C0289C1AC4B91A4399821349DC66496DF02B15CA433FD97D96F46F72E0B23EB561E809601C053A35F4171A963EC3F542B423BAFC56134B7C0C5927C746E6C8055B3B70B31CAD6A168DD78F63C64FE3044280297C6630562C48A822B570E3FA8A76995BEFE67734274337F14FE00C723AF55D596BBD0B2C04E5AC6D52A0AEDB04B2BF09B1F9736E9456C40D5976B1EC2CA21C28AC761E39C583CDF256AA3262C3264250B1C19B00849FA83BCB6614DA0BB19752AE9BCB7BD16A71017066F73804AEB3C7F9213BE4634401A7AAE7BC56E603516FA2839A791EA89C58052030DB4AA2887737D9BC9AD21ABB94246546892785CBBBFBD45102C24D2F2370BE91923E732892E599D3154502E878CCB990CE8383D2F5ABF021BA424008D07685B8B54C6A6550D9CCBD93066A5A651A9B2710D25B382E72CC57096900E16B7B9868A06909F441104860BF03134316382F64D4CDDB596C57866E0776C04F8C0F67F2286B7242477059EBE022BDA200B6271AF875A829FC368E1574F4F3C56BB39D7FF61131F014FC59870FE45466D968C109339F0CCC67F6A7092372CB977C75998EBECB68A46059D7146A2EEAADDB258FCDD404D6E95E5400C8432535EFD635AEE5235D3B8ABA4153B46C7D234AA94DDB66B229456C41B2FBF93F39AA048C158E50C312FD623DC1E818A494980A34C41568942C30373E430DD5250D0CC27D4AAB2AAFB13CD719ABC7F466B702A983318B8D0C3040DC56B0960291F30EA75986917A17BFB76BC4B4AA8EA396F127A41F73219DD06F27126A5F06519E328B5CEA9D67AC122B092FC3613D99F403DB86CCDD760BF8398C1AD6BE4B5474C34864F2B10D97E265FEDB464E01CB3A426A3BD8B3457267317B7649D62D4847C3AADB7036391CE068758BF1847B96C5F3A69BD36A9468F9CAB27476456B4EB510BB063C46D529758C6680B4588CE4F5CD9D225BF700B27F53C13D49808D561F0413315C01989B1582755CC4CC765391AA68A0617F39B843526081658532C2C0025CF378B31411E867C978109454818916F8052336CFC9788F87E53236DAB54112B876042FE9054D9C07B80F7B43AA900BD2580F386058CFF5624A9A062474990C2C126E22BFB300C452306B859201B3C67B7356CC7DCC36CAD7A7BFC44DA0591D8CC45BC7765E405A2F2373996A72433F332CF77A5C9F3146DE121447264A84339A652960109044B1FC707680AF70E457B41259A1B84C37892C1079B8F08204F218CE85796701B05BEE5961F55584FDD3BBAFF25F53F78D0B48266EFC4991E090EEB28799C002CFE902D1222EDB8A026813295E604921269D0E0A1F4CC70F4E528A092B4E15300EB67808ED20995EF3A80CB999553456DEFA4769791FE24C934DDB490AF61FE1FC0027FB10AD260A7C33BCFBF212D3B50568844FB022B7DAC9509B308A0040C9B5482D89EB41A655B7C193C01BF96E5CF8A08B4C1C344336FF53CE9F79009FAA3A3921807D9B4C25739C38568584367F5D882E4AFD33697EB22AD03D369E37C0FE3B981047BED55E0BC0999976E4A36C\",\n          \"dk\": \"F0AA12AF6024851A922B789BA87B869072382CF829D8AC455EA25BBD3C3B7BE58C4DA362494369D9482AD1C42952C64DF0A220B6668DD4364AD3E698BBA190DA9C5E67263A7ED27C3E90C8FA38441327CE092C0B91E08ECDE5C4CFAB7A24271DD782ADF8FC6984701625579F26A196E9B7A7A80B8804C564A735B7A9BC7161129F5FDB0FD0E6226A89071FB288884C3635CBA37294253EA57AF9BBAC807135876A338A9A2CAE4CB35938BE4735BBF06CABFC797FD2137282C8CC15F70BEDA8BBE41962119C4D326113A534B4E29AB464924858CAB7B8ECB0B388C6D184B8765C29B5218806944ED636B8793A0D70B3B1B2664F9AAA0F18D5991AC4C6AA3CCF0A633707BBC97D291BC8C96FE6B32490632383886F3723BC6EA606D09C50B28314736A317C15804E1B17C07045A1971869178E9864C21C24902EE70CE3682A31336DB482BAA9AB22686A725716CDC84623CF39365B44AE6F88AF7C1B42C3168C4CEAADD875609AD247068202056C59DAE502C7B421394328D6197EC4FABDDAC3A47D9C614F7A0587A54476A7C370BA84FDEAC91B460C8E266C28AA5209D80885B7279CB685D5D184FEF93525B41CBDB6AEE769BD7C3BA3806338A27399398B617B45C6A94A9A58089E91367366DA0646624BBFCC35CE5B33971243EA0A8D61137CE7AB99636008326003B02B13BF200A93CABC55A2B2A2382EEF3732ADA819E3806AF2E8BC0602CA86A8ADFC41B0DD4A0E95FB9A25296EEA0027550A8FD04921EC30B7D50558E4B7CE82193F39A998012299CCDA061FFA23391519023C1CFD66C341765E62E38871D9A57F80A657D7347E927E55751A9F768A842A187E76C70440517523B7D1C88B1635208D5C7C4C357002E55768B21462D483318B9AECA5BD81A13507D36CCC7063084CB2657183A6C0C78D5B62A7E75C0275C29B304375C77BC8E85B569B968FCCA5E0E2146C951B7590CD0F3CB38E85586D02763610C67885AC56E84048C40FAA129EB05B8F7A67468B6145F949963CF165CA0B56F7E45DE523C1663B311F0B5DA037862559B2739C974B5CB18D16CE2695668D7B0DECA64EC9928523893C91850D91AA4B76B030C2E74C65A0AC5576399ECBB4E6723DE63825D3B9A1CA374AB763A52A541E8382245542896177AD07D9CFB5F2C3EA87AE2164625DEA89DCD06AF22753DFB1A3CEE1A45146409FE655F1C80ACFF107772562784C2C5CE67E9F395EFEA4609BB7308E13031B68C7B3B0C1CE9626E311264127CF8634C9DC35CA53036E9E30BB7B35C286236A2DB2BB9808006F68BAE0125512531320B3661305D0D4B1672D64C424D38536BA98C6417E3EB175D6C1857788C231C02ECABC4B28BA325FD47F3B56C62D0856703C5296E2576D258B98AA273E3418B43C9A52804EF842C543313ADF481F1E974E93210BFFAA36CFF758CC74914BC43110FB1C819678CB03952DB71C0A560D5425287D9590E6F1373902884A38A908082363E0CAB4B646AB016754BBA4E2FCB14E572025C806BD031C7F415377287001811E4F69A110D48449D24085FC43BFAA5465C1AF82A833A7575C578A464442A33C93B0CEC7952ACAA53EA70B01F49200827956B7B21AA697A6859F248C1DC9CB9FD04057EB96006455C062E3C0722346ADB366DA0AB980C782C417B360EB1C1F6762EBF967D713A0D93A28CF9206E95451E91373805047D8A14FFD2041B4468B26C79B697A14EA75A33876BB865096C0289C1AC4B91A4399821349DC66496DF02B15CA433FD97D96F46F72E0B23EB561E809601C053A35F4171A963EC3F542B423BAFC56134B7C0C5927C746E6C8055B3B70B31CAD6A168DD78F63C64FE3044280297C6630562C48A822B570E3FA8A76995BEFE67734274337F14FE00C723AF55D596BBD0B2C04E5AC6D52A0AEDB04B2BF09B1F9736E9456C40D5976B1EC2CA21C28AC761E39C583CDF256AA3262C3264250B1C19B00849FA83BCB6614DA0BB19752AE9BCB7BD16A71017066F73804AEB3C7F9213BE4634401A7AAE7BC56E603516FA2839A791EA89C58052030DB4AA2887737D9BC9AD21ABB94246546892785CBBBFBD45102C24D2F2370BE91923E732892E599D3154502E878CCB990CE8383D2F5ABF021BA424008D07685B8B54C6A6550D9CCBD93066A5A651A9B2710D25B382E72CC57096900E16B7B9868A06909F441104860BF03134316382F64D4CDDB596C57866E0776C04F8C0F67F2286B7242477059EBE022BDA200B6271AF875A829FC368E1574F4F3C56BB39D7FF61131F014FC59870FE45466D968C109339F0CCC67F6A7092372CB977C75998EBECB68A46059D7146A2EEAADDB258FCDD404D6E95E5400C8432535EFD635AEE5235D3B8ABA4153B46C7D234AA94DDB66B229456C41B2FBF93F39AA048C158E50C312FD623DC1E818A494980A34C41568942C30373E430DD5250D0CC27D4AAB2AAFB13CD719ABC7F466B702A983318B8D0C3040DC56B0960291F30EA75986917A17BFB76BC4B4AA8EA396F127A41F73219DD06F27126A5F06519E328B5CEA9D67AC122B092FC3613D99F403DB86CCDD760BF8398C1AD6BE4B5474C34864F2B10D97E265FEDB464E01CB3A426A3BD8B3457267317B7649D62D4847C3AADB7036391CE068758BF1847B96C5F3A69BD36A9468F9CAB27476456B4EB510BB063C46D529758C6680B4588CE4F5CD9D225BF700B27F53C13D49808D561F0413315C01989B1582755CC4CC765391AA68A0617F39B843526081658532C2C0025CF378B31411E867C978109454818916F8052336CFC9788F87E53236DAB54112B876042FE9054D9C07B80F7B43AA900BD2580F386058CFF5624A9A062474990C2C126E22BFB300C452306B859201B3C67B7356CC7DCC36CAD7A7BFC44DA0591D8CC45BC7765E405A2F2373996A72433F332CF77A5C9F3146DE121447264A84339A652960109044B1FC707680AF70E457B41259A1B84C37892C1079B8F08204F218CE85796701B05BEE5961F55584FDD3BBAFF25F53F78D0B48266EFC4991E090EEB28799C002CFE902D1222EDB8A026813295E604921269D0E0A1F4CC70F4E528A092B4E15300EB67808ED20995EF3A80CB999553456DEFA4769791FE24C934DDB490AF61FE1FC0027FB10AD260A7C33BCFBF212D3B50568844FB022B7DAC9509B308A0040C9B5482D89EB41A655B7C193C01BF96E5CF8A08B4C1C344336FF53CE9F79009FAA3A3921807D9B4C25739C38568584367F5D882E4AFD33697EB22AD03D369E37C0FE3B981047BED55E0BC0999976E4A36C9D71C52D37B30F0CADA8753234F7BE062673BAC70613CE6AA0C704C60E481CC1DA9B17E5EB62AD1009253B91A5C9D143F7BAAA3E76BD89AA399B671CCEB7619D\",\n          \"c\": \"2248562375F15D15580AAD60BF6C78957F86C7BD1F78D47B6FA78E68DACBEF2BE4ABB382C409B81A7F746CFA6F90246E0A33540A1C22ECF83298C0E0104E37C29755D5C0025DC5D9655A0A861A534DC58B23522B4F0961F8D40DCE1FAB1A8FED98B7ED1A027C784AA3AFC5B06680C8F64CD281788326CC4CC2F746E0AEE756F71DD7C3F594458E87382B0135DEE1F4897B80086A4667F260FA19C9A9C4BFEFA1FB054504EE11AA7286FADBAA1192C176294EC7E5A9E383A8AF658077348CF74D1707DA1A8F3E400187401D26BF9225B4B36A00466F75276BF2D10A0146C4611951D75B3AFCA5CB4F8ABA70D3999D56273C86413CCD6944AEAC00FE4D5FBD49F00186950126847AAA2F1D87732E4A42B5944BCBA773A83A8F168875B89ECFC6AB3642A7CF2303EB9929825F1B9A4BAD731EB6C2A6848A959EDE0FBF95ADEA3C4E159A30A376DE5DD9BCE1DD4B85500EAF83871A13F3EC1DFE74D86A383C957D6FE3BA1BB81CCCFB3DEFAC3567FCD167F9B202E72677D2F2012BE72CCA62DCA41E5F92519266FBDE6F60A691D78F0366FB0D79BF924C98D565511CE23EC62F3C7FAE1A3C1BC7817CA67CDCE53D1493EA94ECF0176372F9891D81D0964E409C38079D7548D9DBD463D5CF2302E07FD565EA41E8958C563293EBD58620D08CA822D70F87F4F2CF37E963A730DD5A591F2C5F372C9697118AFF2995170308BE96C21A0EF094CAE5372E1FC43172EEA54509C74E5C83A0BD352663BC49F8100C64A65D45D216CFDC69D8333049487261EF03F492D1DE706B00867BEBCF86ED3CF0CAAF4D94D2869E7BC62DBD4C5127FBE626DC26891D67EEE545CE2A3C6CFA5EE273FC017493B08535B907A852E9F4A166C7B8D7261773B73B6FC96822150484A04954A92FC05D0F8AB9716F6653251794F6B2FA63616322DA4CDF97D352566EBD6E23ED822A118A41C0A80FD420408645D077F7F1561FF5B342445C0C8DDDA5E46CF2F253513BEDED0548DE267ABEDF4A9B7809436AAD6F5258FF89581B4D66D9A1B5DCD1A35BE090DD4B67C29944ACFFAC110B65332C469D1266B256BB4B462CB3B6C2B71D8A119D3218D40B00CF449F9807ABF0B54313845FCAB484AFA418ED6E532136EBFD242E0BC499C7A6788FA9BF64CCFA6E5E0B78FA2708D3B9DB1ACD3EF4E3EB1B105EF73ABD0C0AB0D0055279478FDFF8F154CBAB11A9A5FA8F8170D9DD4B1DAD43F9B0DEBA377E674B2CB9424E754B3B203BCF6AF2C71C8A012320DD57CCAA59F2017545CC64A76523C79DC9932AF8999F2C01687CC80FE4CCC45C6C66F6453CAC8BE9545686920FDC8FE4C5D7B1A9C4C556585D70D520ABDC01B650A409FC907A4472229EB981F74E70EDE2DFD97122AA2C1468210932B8A48A9A38A5836F387B5086D1D3BBE516E33D4D989F73105D3CA0B6E126CFA3C11DD270E2D0168C4E08D9E61C951D4E759E6B97313F4A2BA4E5C7CED65D8800083CF016750646A851F533F631FEA14E8CBDE9EEB02FBB5E2621E31DCA51A60EAAB8D9C57BC3\",\n          \"k\": \"9C6EF50DAE26887F7FE5B0173C055E88DC2FE09384890E11777F742B99AD7C6C\",\n          \"m\": \"E0AAD46FDDE0B8E64361C3233263D8A751F5583DBE91AAA6E69E6318FC7A8EE0\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 32,\n          \"deferred\": false,\n          \"ek\": \"B7F522438A05310B12921A8ABE79B4887CB548317B23A21A4213A0F940BC3268464343C89540B83155C980698C657251CE30C75073C3D3C73AD9902ED10C32CB101B0537827F338A94D9615154370165B9D22707693C5E0657BF890A1C5E820899087683D176986162156ABA109CADB1729A0B0A057C8007E4A6AE7E272C860190ED4CBC126082C1992B9F541FF68A419226A666F6A9439CC82B3424C3515ED1821DA501449B84B1D6C169B9F336AFC9109F4B0AA6A069D5C46095B40EC7C886C1B2BA060CC8C0A1B2FBB53FC102701FF33BE42C92FC8287EB311CBE4BB5D523420850B9D962158D14188C4A794E4C0A8A065757EA0C9A65429F114A983B4D55743FBEE267ACA76C14B1492E2CA839AA5B4762868FF2123A30B76D858B0EE4CEB0B9ABD3A2711BA5167B73226C3B96C184A1681A562F07365D686E17F10EB75545F4E0C52CA6BF41627B8D6A19787A3676159EF334A3F4C538E50484F97CBAA334C5B800A34C642D097549D8AAA6E6E16261E038CDDB816CE46BA2B8B821445209E5AB1CF71B008965FBF3A2673B5074943D2102092DF603F2989F32085B96CCB2BA245A7AAAC06C763640D11730E828A0BA3129A43F6F0825A682886D8CCD9E84B93999474B63CC8D272911D72729E79E2A494AF5EA20C1CA8434323164709090382644B11F0F6C806A5044098BB936F5CECB28A81AB516A1857B651B93CBABB6E3D76B7F79AAF6B503762A10ED8499347771F221C2A96A90049C73E6E1132AF53A88D38EA817CEFA29848FCC0AAD125CA1C829F03896CBA635DAA33ACE7C7EBD18CD7AE03A0F9A3659090F5F9976D600B451BB6C1F3281D6C2884522618AE20CD583AB9B32344B6A2C294B0684E6BAB686A39431292F70748AD02AACB57CC3153CA6862D2B15108EE71FD51C2B7F81ABCA9367D4619964EC7C63EC653FD1374EE3A9BF22C4819264C2A750DEC193D5CC836430B18C535D3E573F4574104D471D972361A6955E2E10AF34D95B4A123218667685B1AC6D696053966F3C56297B844657C85402A357649A82E4579B667A36734B665583AB026699D2F6B8F4F1BB25186858543B61260BD3CCBBA264A30135088C0422ADB4508A60485AAB3C63ECC9AF981A1C5913950CB8949B5179E48C7240CD6C858E1144C7EE92A885008209A055C1C3795FA3C49A98082CA077CE4A75B0021108454D3B0C9F01C00CB29166281A24BCA77ABB471A036C9DB1B8946B930BBE8615CBE4AEF6608DEC389BC75C443753C56F5A62FB6C532BD13D89506C4C5985307A53B2403B42265B35B1CFFD6B2CB443A359D75587B2947C7076EB2A2B5BFCA8B1CBA865674C1A2C1A5EB4917BFB9707590434ACADADC92CC7739933323924B17FB516974714A42E38BFB7F20E752AC6464503170C860FD91ABFAA4C6B70CCBB8A2FE3EA91C94B26D5C64CDEE57F936C1AC10704D473AE10479D798BCD6026BACF4C599DC104D70439B0E2A32C91A5D58010ABA9984A34689E360C7B296486A4C681405EC3FB92B44210D5D3CF52534095859E42BC9BCA7B3864D43CB9F46E685C0BEE8853FBD31285DC2EDF129635C70437B612955B7E56A128DEA321298662F4F96A8E1ABE81B54DA4292AFD5FE45E31E16B17919D9EBF8E87B38D48053AA9F6F41C5B55AB86C4E0BEE558\",\n          \"dk\": \"B4581DD3A668DEBB44ADEB1EB7274625494C6E4A8B303528A3C0CE78F58CA988834A4766B1941DA48898D7A8022589AE577B79DFA056A0305A034385EE20543941ACBAACB71A0184F3D308E790C52F1C210E12C726006DF729448B8B4C2B58B14D150461CB69BE1C840463AC5D4A5BCD07AA81246A3DAB0E82D555B624A3D878021E4604B4907BFD906308024D77B15836C64984C422F5EC3784448DE4D5AA98BC9DFAE665F82B046C446D33321734A2AFE72638683CA596822614994F54B856D9E80ABBC312303C7341F99720C080DB72C635D647BA733CBE521230F528D0119377436CE8CB1F7A48B7ED15904C414FB8404A5DD403D3B07FEBDBC129D274472C634DD2A5BF14C304F702C0861DBDA793834ACE9A851BE24B5DA56A4D433AB08E48C1BE6C5BA22067FD59C38F39A25E1230CEA26BB89BA94A0A3A5FD78788E4A150819E951C1C09B6B9D7B7B56DAB4C38A51A80B56DE30790B72B1743FA7782E40320104B2715A22A7794E4C3BB90CB493B8A4304C8084D18C0E822217A9985BD282B26F60F0A986865C39759CCB69109177A826275E663CF81179FA6181D746230DA0F73D991274BB1C3F802FD324CCA01B284BC6E33D9B853D76B95778DAD094E1DBB2DFCB099055C848B540CBA3962A843211DF6B15B5591C5630ECBAA834A9460BA5822A6200DA8E51B5B5292FE221862A36282B306B08AC03454C8A4A3B0C4AC15127C1CCA392E78B9C15BB8B4460A357DC90C2CCABDB7284D50D440D651410AA849648435CB320CDAC8782E31CB868C7F0375C5EAF51B6628C52AC6BEAEB1777C9B53277BB014EC831F3783E6086455CBA77E2391F89C2182DA4D95058120D2451944C0C9D91272BA1C70732EB332B83518B8925781CF90887F43446885C3A1FA6AB1636052731E7D3B126E556AECD9572DA449F97509D9C795E03961A1B82BBECB74A3859A802BC47EA0B014F5606C678FA15323D571AEDFF78575038374D99F13724FB5120890099A3DD531B7ACC7CD286C78C7182AA00140F48595F47BF0B8B6B74535DEE35C1FE904AEF4CCC591AF85E9BDDC978C95DB382BD82278028C79F0741AA85DE2DB4353C1C38299B010F7378CA21F0DE3A337B15135318E5DF69144593947B1A0F9110E5D3B7710D3892EFAB3FEB3BA67D0B977671F81CC44F38355D6D7C3B8677846A08159CA574CD150E5999C9DCABCBB342B5DD1C26B95762B5619EA21350616BFB06C04058BCBD6F9887879574020C4234CA77021CD52074BEDD81AFCFCC5ACA6691B2B4364183EC3C220E20A0060C4A40D4A5592C4BEE8D87E57E6A2CE99CA299631DDE545AA745D5FB9594A67011C5C2AA1090608E4078CCC59CAEC5F40DAAB6991AF1782B9F4F58F8CD25A597393AE1771DAF2246838C1A4E40BFD3A2C1366A563D698E3321B06672E523AA863A3B68E1053FC8A8428F7393F037290BC177337B534F80E93A19021DC3D5C94AA6859022CEC5AC5E8C16A83470AAA4C72952F823408D52981D8BAAB4D529751D02596CA129031C03576BF3B32304913BEAA20759F245D3F658C8A3C1A41D69EC10C6DEB336CC00B25E332253385B1E6F52DE1EBC45A487CAF133EF1104F286AA12FB1A2552AB1B7F522438A05310B12921A8ABE79B4887CB548317B23A21A4213A0F940BC3268464343C89540B83155C980698C657251CE30C75073C3D3C73AD9902ED10C32CB101B0537827F338A94D9615154370165B9D22707693C5E0657BF890A1C5E820899087683D176986162156ABA109CADB1729A0B0A057C8007E4A6AE7E272C860190ED4CBC126082C1992B9F541FF68A419226A666F6A9439CC82B3424C3515ED1821DA501449B84B1D6C169B9F336AFC9109F4B0AA6A069D5C46095B40EC7C886C1B2BA060CC8C0A1B2FBB53FC102701FF33BE42C92FC8287EB311CBE4BB5D523420850B9D962158D14188C4A794E4C0A8A065757EA0C9A65429F114A983B4D55743FBEE267ACA76C14B1492E2CA839AA5B4762868FF2123A30B76D858B0EE4CEB0B9ABD3A2711BA5167B73226C3B96C184A1681A562F07365D686E17F10EB75545F4E0C52CA6BF41627B8D6A19787A3676159EF334A3F4C538E50484F97CBAA334C5B800A34C642D097549D8AAA6E6E16261E038CDDB816CE46BA2B8B821445209E5AB1CF71B008965FBF3A2673B5074943D2102092DF603F2989F32085B96CCB2BA245A7AAAC06C763640D11730E828A0BA3129A43F6F0825A682886D8CCD9E84B93999474B63CC8D272911D72729E79E2A494AF5EA20C1CA8434323164709090382644B11F0F6C806A5044098BB936F5CECB28A81AB516A1857B651B93CBABB6E3D76B7F79AAF6B503762A10ED8499347771F221C2A96A90049C73E6E1132AF53A88D38EA817CEFA29848FCC0AAD125CA1C829F03896CBA635DAA33ACE7C7EBD18CD7AE03A0F9A3659090F5F9976D600B451BB6C1F3281D6C2884522618AE20CD583AB9B32344B6A2C294B0684E6BAB686A39431292F70748AD02AACB57CC3153CA6862D2B15108EE71FD51C2B7F81ABCA9367D4619964EC7C63EC653FD1374EE3A9BF22C4819264C2A750DEC193D5CC836430B18C535D3E573F4574104D471D972361A6955E2E10AF34D95B4A123218667685B1AC6D696053966F3C56297B844657C85402A357649A82E4579B667A36734B665583AB026699D2F6B8F4F1BB25186858543B61260BD3CCBBA264A30135088C0422ADB4508A60485AAB3C63ECC9AF981A1C5913950CB8949B5179E48C7240CD6C858E1144C7EE92A885008209A055C1C3795FA3C49A98082CA077CE4A75B0021108454D3B0C9F01C00CB29166281A24BCA77ABB471A036C9DB1B8946B930BBE8615CBE4AEF6608DEC389BC75C443753C56F5A62FB6C532BD13D89506C4C5985307A53B2403B42265B35B1CFFD6B2CB443A359D75587B2947C7076EB2A2B5BFCA8B1CBA865674C1A2C1A5EB4917BFB9707590434ACADADC92CC7739933323924B17FB516974714A42E38BFB7F20E752AC6464503170C860FD91ABFAA4C6B70CCBB8A2FE3EA91C94B26D5C64CDEE57F936C1AC10704D473AE10479D798BCD6026BACF4C599DC104D70439B0E2A32C91A5D58010ABA9984A34689E360C7B296486A4C681405EC3FB92B44210D5D3CF52534095859E42BC9BCA7B3864D43CB9F46E685C0BEE8853FBD31285DC2EDF129635C70437B612955B7E56A128DEA321298662F4F96A8E1ABE81B54DA4292AFD5FE45E31E16B17919D9EBF8E87B38D48053AA9F6F41C5B55AB86C4E0BEE558A7F40DCE21FC27EB6E7596A711E8B29FA33B3AEAFCA1F90450EFB0FA358688A591271D83D0E2BF964B9C7D2CA6227184BBE74EC134043A44DBBF8EF3B18EC43C\",\n          \"c\": \"72CFAA01CB4D24B32D0A12BD199C20BD3CFDB6F063CE9608A0DEDF0FABFFE8EDAFF2536244B7942B6FBB62297D85456519E9CBE3E587ACCF54FC28062765E0C6250A204007F82ECF4AB4CE33D78CD0B4B6E502B44B0259A4634FDCE0116835E5449313C089E603EAD7C49C08DEA8DECC81D8E2528B5AC9C98A5EE6BD58E3B60E98922614ADFA9390F9A2B6B66272024B20263AF2126C3477447C04F0C8A42FB8399ACBD6DD669AF1C805A204C2173503ACFD770ACE4470B7D683F751906B7B3E5E8B1EB6241EFAC9ACCC3AB204F4AF77AE4C1033F3B177C322B72AD1C52A10B35782631B74EB883A5CEABDEA1961F327AA53EB14A1DFB2B58A4E7B37D14B5B565CA21340F181BDE4EB3C6445AE772B07ADEE4237262DE99245ADEBDCCFE7E68F96AE76ED8ADB62A6FCA116397011F3A77074D568F38BA6A131EAA7727FC9BC8A2B00016A37ABA76BA1CC11989771E3FC7AF635B46AB69487347B6C8684885436AC1E8CFFB1B65054AC01268005C71C70F36899F543F876C0B9742E29FC4086564A074EE95AC5CB395D6CE1B1E384920AFE580C5526C713D963DAC69D20C4A96932303B632ADCB361D2D3AD37CA4F7875A5BE6AE62C333751283A430E78842CEF8092F85B54B064A558DC1D25A18BBF3C0B496FFF38B214F5D9A611019BC4EE49C3C1ED06DD705D720D58A97AB6FEF5518969F2A8605BB10B64E6FA31B8E096BAC3573043854921E4210DFFF279578D2DAFD40738F0714EDDF16C2868809223FC8BD6EBCBB3B331B1E8ADAAA7597E53E31D9E7B478A9F6E7DDA731AE9571F698A1C977C4F3401C9A05665E0B8C080B34964C15E13ADEF0348AB9CA3B64F18BEE6117D7DACAD1F08FD9B8AA8C5F47881338BAEE1B94FC40ABA11B0FB914154583BDBCCDA62D3AE898BD60B9C643D67514534FCE277087CCB66A25D345290AEE7C1B07D57D53896574CA762AE8D17A61D796F4A8270022DB314E27CE7906E4119C003385D88BE165BB80493FEE768001BB42676D2B71D58FA19199E714A0864546F2166F46F4787845525CB59B2F6F8C3E0943421A70EAB2705420BA3A62ED9AB8288DF8CA09A5ABFA64FDCD0049C61FC7B226249E0E116FA5CC0D9C2EB3B7391A40BDC0921F4D2936D368D8263791156741EE85F2C0267E858FC01E89B6149EAA18B0F8C8F827CAD5F8AC68F24FDE5E185B3223333E3A0B8245EF30B8E5E5B3E04874ED3F75A5CD25E1AB1130F0DD6D5DECF88E332F96B4F9A4C58F14ED57250B47B1CF3AD093E2B9C54922B1214000A98049003D1266ECD0F68237285A709E24704ED1CD37F3C64E15CA637D431AF5CA060AEBF5E0CFBFE464510669317944FE07F7EA48618478300725961E04EECDB73B411206EF5F3DF2809573D7FC42458D262EFB242D19F9D9AD9A8F2C05AFD31AE350E83CEDA11AABDAE85E2E32B1A226BBDBFD2D5C2B7B4DDA94012D53AA7289AE675C33E9E8F8F6F06537E240A97998DADCC39C836FCB8AC24D794AD291D42127E8B513CE0346E145B488FA220BA149A\",\n          \"k\": \"05BD5B91C2F634E5B8BC59697D180CF1B36A244C6EDFEFE7458308B5854C77FB\",\n          \"m\": \"90347D478D5D964D66A54BE930FD9F7FD3C2AE1492DAC35A6CBDD02616BCE14A\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 33,\n          \"deferred\": false,\n          \"ek\": \"AF98338A682D431CA0E17775EB170E3742ABEA300D6A46C567C364DE8939831695C59BB7686729C9001E25A85FE926CC6E584E2BC86D3B25BC9D6ABB97EB7F15AC23656B3185CBAFE0C39FA0789DF0678FBF5A43E6E0C53EC38076572D9D84B1ABE742E2F6C0C8CB08CDCACD23B71D57D06708F32D50870D9D636DF1DC01A8378A211A134BB255DDDC0B62C75812AB1677C50DF56B1FF62024FD722D3E732F56A2C6EA10CC31F280ABC8347788CB291AC5A1820525B9A33A7089DD689962A046B652AD182639278279EB884163E2B115A29A3899CB0EDB4514A0836BFB8A51D834C1939B8DC108B6138FB88B9199DCA5F7F64AFF36B9296613F891265778C7963C3E702B81C54834469AC8B59920BEE7878F01052B77B0F54B6DC61AA3DE695D20786F7D309C3B16A8D2C90A921CC317E91C015AC80DB106D4810EBBF2C5A1530506AB1E28B7A32BF67A6981185F98B44CABCF6706B134B5537DC697D16AC003808119B0BF94C84BF569422C8BCFF834237B902D83BC4C16F5CDFBBA5CDAF0A69FC87DDC885DD3AAA852342C1EB8179756087C678EAE2878A777655FC19719593FF600789ACC6322018791874D33D9284EA512AF1231D4CC87F1BA6F6293A653036E590A892F101CC518110C984AC55C2931C976828283F0266609A45A8C7CBF8C23CA0279B133A38F9DC797D5E58011096D45441F11DC59AB5B66846A87059314FDC64E8CF50040B57432A18F46078AF5A6132B006856604CE009128FC5445BCB6891E91A8677060F39A2CAAA183FB8A0F6751051AB85474A539A4183A4486471418FDBEBCF40055AC46075C3B2190248A8202431DB828B82E1320DBA47A23A94DE5CC378B48FF4B633E2D666B561827F8013429956D3F5947FBB848B3A511452870D00BE30610D0ACC418F8536686ABF66851707D89FCBF65959BA3062F6A16E268488D04BE4E370C4247947FA823B27133ADA76DB58CF3ED1109CE433CA0042CDB809AA1B5D5E9C4258C6067B0004A905786473A0CA4B3E9D90443891A148407A8D89121379A1F37C70770586365A74FFE5C6FF170791681E5CE10BB88BC0BCCA82CFC81424AA5BDA072D1BB5BC62F6687A05A949C2B04005284FBA8ED0646A394658A5868304C42C05605E9292801630129C76A1FC083DC3696DC4E904BB1916BA2287DC37232AE2C962108F245C3A0B702F53271F6BB00363D88690A7BF37B345ACEB5426F34BCD7670ACF15955401A20F938A4407E00D8008C147DA00C080A226F71F15DD8DA20D0C6AFEE431B6B2733513BC877F5545753305E8C03FAEA935C183BFC1C40561B59FE80956CE2C5BD3C28F2248249E2A0527891B3B64BF0EB89938A42008C32D668054E508871012FE8653473FB70CAFA0A8F1A509948A2A967CECFC7B5DAE34E166086433C5977208C45D97B24238E875A790C2779974B4721F573C03C4D20777F1C4589227765E8E7AFD0D59EDDDC722CC6230356B477C490234CAF858893F0E446727CC88E1411242344DAC6AE6C2CC1D2B1AF2B8C32BCFB253013411F18693ACAB9A7A86CA5590964D39A8A50768718BD948566A2822206226020165965F7B68871AAFD3474FA306A2DC31A98C60FD2E5AAA8A0B72BDD2F70D6D5DEDE7D679758D8A325B6CF11E7922902ACD92A3A8CB43863CE98\",\n          \"dk\": \"D261A4F71B7248868BE7C32F8AC688174747FA519CC3A4A5D757C3D1E7641893849AE969D8F8A6B553A1CDBA41D46B74ED9B8C8C19409D3AC3664226BC74AD96A74221C086BD3A0BACD66438239E0A8B7EADDC72ED7A453C6B7570B4C763D84883D788D8F8A080972A0635AA563842526327AACC510688B648231B4AD019B3CB9ED6F161673A6C13A6CC850A6C5692B5A3150D2A4805C0875028217DE1058132E68A97501C97AB81DC229F5804079DA5A7F353A1B999BBFE63BD9A4A4B6BC212BCC5B4AFA795C6D25541A522E3BC4FF86841AC12034D9553504217E7DB127504682046604401381FBBC08DEA12095AB71C882FE01BB95B7723F3CB196E1B8917639FC74007845A1FEDFB890F096A13E281D66B9CFAA22A93C8CEC9C9393C2BC81395B37C001D56C659D3C541AA97133D09CDB4758F148966EDD675999248CB786BBCF2A15138165016114F5ABB9BE105D8BB8A3DE8835C85A28B3C49E0C928B89512F4E380F371882C745056899B2FE465DA9130D478C46B2B4075B487AFB96DE727B88A83962B8B99D42C89CEDB5C4369BE78F7737F721AF0645263709E2DE0CE96C0CEEBA76DAAEA7757077905DA5F75D932C965C844C91F09381147C021549783A47609111965C619AA7540881F281633607F7B9983E0E2B7AD011E41CB5BBC186B4BE084CFA136B5E01B36328416C997D87376EFCC277C5AABE703C0E70C741D1809A0A36DADE7BC5111B1F046AD63449BD1C149B1C47FC18C091CF430C377A8AB0A92C9249FBBD106E7A060A4B252E1ACA050941A1AE333F2C3651F27B2CBA7C5CDDC86F0519B9F485ED563CA9E5773F72742877109006D8E9A3B49712612B5B94115024632AA0FE39A8F527891E03C97FF2C57EA532DA594C73C3B03151376702C21B6191826E7BC42DCC5D9CA2295165A85B4981D311AD1E84DB1E0249933AC134796D84166E335C478A46D1AD893325256620B2E60FC9D71832685D406C2AA13D3A614A10410A27876B1F6CEA13A88CF401242665B9051C137802A63991D82733801FA5611131D2CC745481253FBA4788E520CA165AF49E6AB6BD59B10C95CE5911B73745383D91A9E99620FB9862DF06FC248AE44C43D042A7ECF21B6E70A32660377651A24A486AB48598D1D55AB858ACDC7007D98B66D74B368BB03AA4992CA32D2483BC67050F2A302D0133F31071F3CC286A09CEB2139320B496A607B2B36B07A245E90F3850CAA8F9823AF2CA6B537439C861737348765A988256C597370954B02F44244AB15A9F78BBE9C7908701E4571B384A3147D8A4ABB0256D3E45DB5057AB156A4B6405085D31D8A602ACF6280C0E9CFDFF756A0EC3D43C65FB70867B09CBB596206E0E3C14B62188DE815544247A10122788884C3103BE557877698708236BC2363BE9C0424A0930CA63922174C3AA2585B53784D2362B25EAB9AC55AA866C8CA59A7859006937D206D6FD1AC4F80BC1C433F4A67BCAAC05221385A70042AA011104C56037FE0875E2C86BDF384772600EF391E8DF30AE370CC792569D7A5213A6C7866DB65A196652F0C699E4C57ADB90AF0498D33CBB1C6969659C22C3A2596FBAB175C992530776CB8A6463D37CD70E89E075506AF98338A682D431CA0E17775EB170E3742ABEA300D6A46C567C364DE8939831695C59BB7686729C9001E25A85FE926CC6E584E2BC86D3B25BC9D6ABB97EB7F15AC23656B3185CBAFE0C39FA0789DF0678FBF5A43E6E0C53EC38076572D9D84B1ABE742E2F6C0C8CB08CDCACD23B71D57D06708F32D50870D9D636DF1DC01A8378A211A134BB255DDDC0B62C75812AB1677C50DF56B1FF62024FD722D3E732F56A2C6EA10CC31F280ABC8347788CB291AC5A1820525B9A33A7089DD689962A046B652AD182639278279EB884163E2B115A29A3899CB0EDB4514A0836BFB8A51D834C1939B8DC108B6138FB88B9199DCA5F7F64AFF36B9296613F891265778C7963C3E702B81C54834469AC8B59920BEE7878F01052B77B0F54B6DC61AA3DE695D20786F7D309C3B16A8D2C90A921CC317E91C015AC80DB106D4810EBBF2C5A1530506AB1E28B7A32BF67A6981185F98B44CABCF6706B134B5537DC697D16AC003808119B0BF94C84BF569422C8BCFF834237B902D83BC4C16F5CDFBBA5CDAF0A69FC87DDC885DD3AAA852342C1EB8179756087C678EAE2878A777655FC19719593FF600789ACC6322018791874D33D9284EA512AF1231D4CC87F1BA6F6293A653036E590A892F101CC518110C984AC55C2931C976828283F0266609A45A8C7CBF8C23CA0279B133A38F9DC797D5E58011096D45441F11DC59AB5B66846A87059314FDC64E8CF50040B57432A18F46078AF5A6132B006856604CE009128FC5445BCB6891E91A8677060F39A2CAAA183FB8A0F6751051AB85474A539A4183A4486471418FDBEBCF40055AC46075C3B2190248A8202431DB828B82E1320DBA47A23A94DE5CC378B48FF4B633E2D666B561827F8013429956D3F5947FBB848B3A511452870D00BE30610D0ACC418F8536686ABF66851707D89FCBF65959BA3062F6A16E268488D04BE4E370C4247947FA823B27133ADA76DB58CF3ED1109CE433CA0042CDB809AA1B5D5E9C4258C6067B0004A905786473A0CA4B3E9D90443891A148407A8D89121379A1F37C70770586365A74FFE5C6FF170791681E5CE10BB88BC0BCCA82CFC81424AA5BDA072D1BB5BC62F6687A05A949C2B04005284FBA8ED0646A394658A5868304C42C05605E9292801630129C76A1FC083DC3696DC4E904BB1916BA2287DC37232AE2C962108F245C3A0B702F53271F6BB00363D88690A7BF37B345ACEB5426F34BCD7670ACF15955401A20F938A4407E00D8008C147DA00C080A226F71F15DD8DA20D0C6AFEE431B6B2733513BC877F5545753305E8C03FAEA935C183BFC1C40561B59FE80956CE2C5BD3C28F2248249E2A0527891B3B64BF0EB89938A42008C32D668054E508871012FE8653473FB70CAFA0A8F1A509948A2A967CECFC7B5DAE34E166086433C5977208C45D97B24238E875A790C2779974B4721F573C03C4D20777F1C4589227765E8E7AFD0D59EDDDC722CC6230356B477C490234CAF858893F0E446727CC88E1411242344DAC6AE6C2CC1D2B1AF2B8C32BCFB253013411F18693ACAB9A7A86CA5590964D39A8A50768718BD948566A2822206226020165965F7B68871AAFD3474FA306A2DC31A98C60FD2E5AAA8A0B72BDD2F70D6D5DEDE7D679758D8A325B6CF11E7922902ACD92A3A8CB43863CE98D74B9CCDA4F1119680B65475539C5D6AD9CC013C32F7DC34DD644E17FD8FCE117743372B043D1C0784B22FE9852E14D43E7A05A19D7FBDEF102AB9743822A129\",\n          \"c\": \"36A6244DF4F7569DCCA35691306D2E1CE906993034093AA928CA368540FD0D1787051D491033CFFCC6805520137FD271585D651FD67D6C4B9D9BDE958892705DF8A8D2C55C426B6ADEA1F187732579DA8F922D881994BE04A1CFF591F669019FC4AF9411FA61BD3DD88073F8E119C67BAEFCD221DE8230E8B6D7D4D739AD28A1F6B2C9FCE301DB55CAC39778FCDDD389A44DADCF65513AA05853A88D472A7E46319CAB47E3AF09913623360E1ACC954BA17AACB84486F81B0FB34AA3966F6796293EB7F4233052BAD3BD2EC9B1CE1079D8A5C0F9B795C9113EA865F62F1103260ABF902A5D6497BA7F74D1F0E5B2CF564D5B3ED6E6D6AD186F07B62FAB78577CCBCFE6A2DA803E9199785EADA1B6675B6846535FA157166713D37A55C8A99AF87A4038B2225C3E55DD21557238C96226D618979A57204F9EA5447A57106EE6E6E8211C30CAFCCB709B3CFB42B3F4A538F29671578B66F406FC5A6AB274219A58629DE5F84C55AB1D8C39077A42342A1A220C65D5AA8BBB0097E3AF13A03166823474F0798D4D0B36C8EE7C2F71C665D49AAF9CFAB1E72E69783347AA4416055DB68DD30B31C604799D970430209A4F73E70ED3F8FF056A8C9E9F8064766A3CC31304DF31AF27BC6A5AA9BB6143483A89DA5F376EFFD539CF90BF120E8D8E2F5753F99D9415D27E79FC888907325E45316CF5975D717690DE4D587B4539CB36008C127205650865376B78FC07CF7F5B2EC247553A116E386307570913C423F6713875D532EBD576B440E47732FFEEFA7EFA76F29ED763C088183C08471AEB47CDE561D01FC41D063C41BE58BBBAE2EF0C1DBEA24C20FB789697836E0BD63FE39F914278C75BF6CB1C8B1A73F67913D56DFF3451834CCF02B57411DDAB251AEE7D939DEDA1F3CA918B76F6757241E97F068F8D4024731258CFCFBB5CAB90DB10337D2FE85BA4139A329B1BC44DFE1D26D36CDD7E43A62DDDB749098AA2F403659EE15356FDF8C23FECCD02482851310BB702031E126D1D9AF109B6F2B452F65B685C85A67E97D4658FC7D551286AEA960FF348D47F81583B94032D1F0AB71997077F8D119AFD039B3D4DAF678A9EE0F23422B90F6898E39E8F42C6A3B2D7BCA38364EA6BDD2277A8FE32AECDDD4C2822F78B3E0B7DBB0250126D4A8F813EF6B6471EECB3B48753439D099E2B43FD315A259A5757D0DFFF298ACCB6EAB59C964AE7FDD3FA35630FC7E72CF9D538F226916BC11250B36C24DDE8404A5F4FA684321713E57AC84CFB0F1DFB813102E77ABA2D5D77F789CCDC760F75DBF26827FD5833BC42C9EB86AB9B1C2E4DE4E4D5D257170D20D8673FB0E4795FDB5DAC063EA801AC14E67C8E98F3DBEF3F3A0059CBCEF321AB5A288A4BC9B093F0FB958CFD8694E6DD9BD3F37344A9454AC4F86D1CC5959CB6714DAF0116338479B99F251D5C2C8965F3CF1B4966348E1102972AF4FA858B09F9173A72A1ECC911ECA8B578BAEE37F7413D821655F3A207B6DBFBB7FD7974EAC6C9923DE8F65F3A4F8321D16B34\",\n          \"k\": \"4EF33F2E08DB26B11979F95FF6C624B4168CE9055FD31390EDFAAD5E2DABA6A8\",\n          \"m\": \"119BC36B5F856C0A2F136B3EE42041B817125A600E829FF6B4B402131A26ABF1\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 34,\n          \"deferred\": false,\n          \"ek\": \"463B553102898CA297E0C205F3C9582273ACFBDB13BEC53341BBB6724C774C741ABB16B2F992C878114FAF67C248603D8AD5842E7230E59B7537119B204AA8B2E0830CA3511886AC5280242A9A817D9A8C7EC8B95DE23254174D1F507129BAA49CAC9C6800CD1039BA2F6625584B8F3AB27657E6B7BA2A58080591D572C7E0B5ABCBA3BABF0525736ACA427A1F13245426C08DCAC5752891169B1A8373B4BEA8A49DAC3B8163E78E74195E8AB54007534133C02AD1930DD3CB066E7114DBF4CC9545C3616B1895469A1E08A57273B235874AEEB32EA3433DBE8732B871B4D0E3192ADC30AC002E4B82798E0C9AF004973F5749456605B897C5AA9938C38343B3690160615B3406AFC6FA20D04AA0214B9C224AC7E64A6E85F38AAC01632A501A6CA1A4D3E0ABAE384AA2A50911E8BFE945792C9283F9A898A1CB2E2934550E3CC52E9BB4C2EC125F64C28B0B12E0805A031824F423B7CFD9A11086523F1305E4C130C17161AFF366472875645BC8A1B14DBB104494C746F84B4599C07FF4E74A3A1408CC983CC2C6CC34BA04333659C1F960E06B73257C5EAD5B3406D70C3DCA05B9EB1E0EAA89EFF046B58471FB7268BDB6402174BF03F92D4D22C7F204A35AC6551455603D8382CA3AC726C1BA3ECB5D4244BC62F10989B4B9851C4346DBCDB959516CC260CE2599EB1232CFD65FDF39AE7EB0763AE47A359367A7720D02B07A5A634DBDF74F4AD589EE2691BB4862D06B6836F70BA8C582F3A7996BD7BECB998CB1AB1564C812AD04920B9121139817E8F06172B217481C379E2C368C0A12194195A03177A822C76B0C7C79AB700FD5C24EFB24BF29139F4930E6515E7A1880D971644ACA12E4278AD22CB96370AE09614D96CC0328265F27E2B2D60465C8A370736632257B191B72185DA1A2E6FAA6759580C6E51E6290B76944C2A8743610424DCAB89CC9B79AFEC512DF9CC5B4807B0C93A889F872D1D8950CB4B371335718035709F36710647157141E6F596FF677B132352D3C35389903375999B2A0D460CF16A581C3CE5A057DF42051A7BA167D6647BD683290ABBEB3E67277C055C5E70674555B98D365442BB8D2859FE47C1309178537E78DA79B7016890C94864328CACF327746D0E32D65D9C7C0574FA5BA9E1FBBA72D990C62E1AD74759BAD3B7F9759B334B10EC8A1AC3D8453783A621C898C99945195928445D21B6F40215724919C3959D77731C2ECB171489AF6F545C7055D55778CADD250DF49913A7769D14774D87940D9C0C53969BB2AA151E063CF78CC541EF643AD7C916B961983983F886C7D7B6A016391C6B9C64E95A03FD1F65B6C924B2353678D3079676C25E236745955CE5A9500BDE96A1287B6A795B3382B438E64C588878CE447530D174B53641416E328E428BBC347AAD32342C0F17D1EA15FBE147E3D0009C1CA0C0E5A437F1708A602026F0252BBEA8B7C70445A456678E0777994C021F61A8C689DCDA40BB196512C8C53FC3B0EC26B3E2474419AD79EBFA58087AC8987BC15C802028ADBCFB37722B0999F6EF71629AC2966FAB95A5A2D3F74300FE668F9D912F8815561C53E1BD24455E58FF3F7BFBC2207D7966B1414CD0D695C5BABA93618A89F32CF29B33FE97EE961F5DF14FDCCD0E81878F6C76D5651730F6456DB0938BF\",\n          \"dk\": \"ACE60FD2C1783948CE064702818B3CD0DC8151C34BFE02B16DBA87042A58A26286E6E20D990C478CCA9A56F729F5F34AD28A85FAA71F8B89C6D188539685BD873C76902229D0C982039A5B95DA6A91C7883E1AA82C81834169C74B45A021C04A4FE665EFB41CD07B660D9478D6F831A9BC70ABD757D7BA3790E559625186C197CE99E6979640A60DA017FCC83351D955F03A32B9234CA6CB226331C5F5A342506422E3C099FF702444659BCE0BAEBA2194C8089284F8A218C903B368C563B6A05557402D30BC73EB91B6B2A5626664E34C44B4A04BCA0187B7C733FDF4BF02A84A513753261646B01BC4F333BA66EB4D710B6884A26D2AFB3032C5474E741940F556D9F963B97BAD3715B6002C941833C8216B220600B57A2544AD497D5E5029E0A0148146755F0A72E1205AB7FCA58E31256C44178F8309F6A81FE9FB083F019D1C4407BAC85EF6264B72531C3BD5A5F270A68884841E190667759CA884478F3A641F798057DA702B998ED17C18D099B71986045A7B5163D72C40225B0BEB48943B444477B56EBABF34877501F94477CC6582B9025395B387E23D41321D225065DDB933E2CB0C1C35A927619BE10217DEBA2A812A15AE83AE46C9B22F03C65C73B108E571302CCC7922AB500C3525F34B572946309C7FAEC5930122772377BCB1B9990C3B1DBB19051FC4805BE809B63A67B5D9096F7693E78762E4640A2C55C8A4957C8B1130A3C464F2B67AAED0191145944887CAAE29C4595C7FE9B4630706968A6B8AB79AAB481A31C1C02E2C46A43CD69D7F5CA1CDA7AFA185AECE69AFAC3156D7F1470C26B812C7453DC724B94A809B94A6880C02DE987247EB777D451645689969F1450906B82949273A216D60C757E470A9C6C6415A8051B33049B6E82885A78A12FA4375A323A686CADF08C5AE52699F6383C1F6B27C558F915B0D2261927B3C24BBD19097A19A6C905544FB664BD766074B31C870140B8840B1E73F63A68D6A876C40EB80EA96563A469423206929DB169BC80B749A1AB26C9EA4AC44B2AC611C51910B9300636570D79C362FF2571404622E6510AB1B9E6B2530E9E420D423579B5802BE18913F8CC06F3789E74C69DF8C80FF451EE84603868523581438CFB9959A83BA0C9A6C5A629E97C867955B84BAEC4FA36597BC2999AF7638D10BA3F6B68C3DBA1F5828B234CA83B2F14C9B7281FDE8B7A318A00D1A3E87DB04A94091F1D0A326858AB8BB54B1F27BDA8BA3E7C75C48B03386A06903487CA830517369120B8103E86809286C92E6033D37F5C6918139B8CBBFEFA9871B5934C16B6441635E7C478C010A554BD4CBBE119C4B77062BE49F0FC301D4D6394AF0C5518325C9C327E8E001FFB78D157940468AAC6B8BAA38A20A11F439DE402DEE5856A5866EA6A9B2FF549AB040B3F4B43772F89F66138B543A644E6B337850A09D7A747827473406A82973479C1700CF72B39C813F8EB51B4DD04B13FB9D945C42BCE147E56074F30485F800C63510B634C08BEA128AFCA6B4EF543724CB6CABC6B160DB957490816BC8C33122C2923B78FD87BBE1453C7DA562F185289C56017CA572DAC0AA2865499FC0322DA35EDD7919FD9A20D2D9C1D1586C8E5205463B553102898CA297E0C205F3C9582273ACFBDB13BEC53341BBB6724C774C741ABB16B2F992C878114FAF67C248603D8AD5842E7230E59B7537119B204AA8B2E0830CA3511886AC5280242A9A817D9A8C7EC8B95DE23254174D1F507129BAA49CAC9C6800CD1039BA2F6625584B8F3AB27657E6B7BA2A58080591D572C7E0B5ABCBA3BABF0525736ACA427A1F13245426C08DCAC5752891169B1A8373B4BEA8A49DAC3B8163E78E74195E8AB54007534133C02AD1930DD3CB066E7114DBF4CC9545C3616B1895469A1E08A57273B235874AEEB32EA3433DBE8732B871B4D0E3192ADC30AC002E4B82798E0C9AF004973F5749456605B897C5AA9938C38343B3690160615B3406AFC6FA20D04AA0214B9C224AC7E64A6E85F38AAC01632A501A6CA1A4D3E0ABAE384AA2A50911E8BFE945792C9283F9A898A1CB2E2934550E3CC52E9BB4C2EC125F64C28B0B12E0805A031824F423B7CFD9A11086523F1305E4C130C17161AFF366472875645BC8A1B14DBB104494C746F84B4599C07FF4E74A3A1408CC983CC2C6CC34BA04333659C1F960E06B73257C5EAD5B3406D70C3DCA05B9EB1E0EAA89EFF046B58471FB7268BDB6402174BF03F92D4D22C7F204A35AC6551455603D8382CA3AC726C1BA3ECB5D4244BC62F10989B4B9851C4346DBCDB959516CC260CE2599EB1232CFD65FDF39AE7EB0763AE47A359367A7720D02B07A5A634DBDF74F4AD589EE2691BB4862D06B6836F70BA8C582F3A7996BD7BECB998CB1AB1564C812AD04920B9121139817E8F06172B217481C379E2C368C0A12194195A03177A822C76B0C7C79AB700FD5C24EFB24BF29139F4930E6515E7A1880D971644ACA12E4278AD22CB96370AE09614D96CC0328265F27E2B2D60465C8A370736632257B191B72185DA1A2E6FAA6759580C6E51E6290B76944C2A8743610424DCAB89CC9B79AFEC512DF9CC5B4807B0C93A889F872D1D8950CB4B371335718035709F36710647157141E6F596FF677B132352D3C35389903375999B2A0D460CF16A581C3CE5A057DF42051A7BA167D6647BD683290ABBEB3E67277C055C5E70674555B98D365442BB8D2859FE47C1309178537E78DA79B7016890C94864328CACF327746D0E32D65D9C7C0574FA5BA9E1FBBA72D990C62E1AD74759BAD3B7F9759B334B10EC8A1AC3D8453783A621C898C99945195928445D21B6F40215724919C3959D77731C2ECB171489AF6F545C7055D55778CADD250DF49913A7769D14774D87940D9C0C53969BB2AA151E063CF78CC541EF643AD7C916B961983983F886C7D7B6A016391C6B9C64E95A03FD1F65B6C924B2353678D3079676C25E236745955CE5A9500BDE96A1287B6A795B3382B438E64C588878CE447530D174B53641416E328E428BBC347AAD32342C0F17D1EA15FBE147E3D0009C1CA0C0E5A437F1708A602026F0252BBEA8B7C70445A456678E0777994C021F61A8C689DCDA40BB196512C8C53FC3B0EC26B3E2474419AD79EBFA58087AC8987BC15C802028ADBCFB37722B0999F6EF71629AC2966FAB95A5A2D3F74300FE668F9D912F8815561C53E1BD24455E58FF3F7BFBC2207D7966B1414CD0D695C5BABA93618A89F32CF29B33FE97EE961F5DF14FDCCD0E81878F6C76D5651730F6456DB0938BF885E38C95A03788929E70D0A17C2D4E23764EF31D826BD4E78F114E7D8F056B2C07BD30B423B29EC3F26A36A916A247C45D1C67392F267A9C3CF0AE0B2F75A56\",\n          \"c\": \"434A4E54F4450AF0673AE77E391B9C25BE63AF58D3F65507AFCC28E6C17B8238585085E39D89280C7F8BF73948AD6A543F9A5A73D3EFDC039F654EDFAD6B4216D60E121EE433C9A347DF67792F8C99169066AD7E5B19A3F4236C240C506887E2F98EC51C565940249D992006D04CA1391A433A43EE2C8EDA8C54D4B731A1570FCDC0AEFF0837D42827D2570AF1A9FA900445F51FCC482F0AC088DE5D4DA40015535EFB350A8797F62DB7DB8DAA0B96ECF6DA3024EF80520533C6E394A197BFB22E91A38E7F6A7CD7FCB4A7A78C9510144D42E94C3AC8B2F0F6914C11078720D3B9B848E6BC211D56447D2FC7F20F59F4C5A72716176CF2274CD82DF2FB2BB0E634AA0EFE9D4EED10C790B14754A54AC295BBCF4DF1A129987EAEDB0DA0FE3931888C9F0EAFD5399BD91A6036B7169C788FAF63FF8ED16DD3A92E8040FFFCB6487DC15B734297FCA279155F365C9AFFEE13EB303B05C52F6365D8F64D61EBCFCE6076458D80E97D325C12B1B9FAF46D8B078DB977963DE5DD75A353C1E7FBD9A1EFA97A6F10AA77B65FF0FD699D2118D6A9ED13499BA2FD10AE9513F1417F6BA75448D7490CA487241CDD2D3300677695095EA755495F6327E92257FA9E29A39793F4E9B8CF6E43E0E1D6EB4620B523E79917C0C328C3A0C55ED76B16191AC58325C32FF4E6E4FA570F2F8174C2F21A9A6B8B1E82E2F7E0388CF7BA95AF70C7F0884C4D2876C3C6E479510520D62DA9D49F435026A401CA1E5E6D5F0DF062CD34C8478336BEB3073D38EA7F37ECC969545DD503F4C10FBDEB112C947EA8CA38A180BE6E3CE1095BCE75EAC5BAB6C436784E83DC03A9CAF1D90DED7784D97F141E642334FD2B51D957224278948CADE3885003777031CCAF8BF81D40C9EBD7127CF4AA9FD983A2F8209EB7C8F27360123EFAFA1A5ECC2B5C07CC4F1F6F5268BF0655C9AF176E0D6913BB5A2886D74DC727C8B72AF1721A4F75B0C54A76365B87367EBB38B7B294B8F75019A2C96E1C5D62E782E903B7A772589410A84857F565939018612EE18E81D535C8435273E5ED6ED28FF4CE7734D643501EC0330F3C46A11F8E17A2E8041D4E0F0F01B7B865ADCF440B7313ECC1C1D5E956AB82783D89A635BDBF7CCE80EF21D3151A80A83FA01DA706394592140E882B8F86A36D58BBFFCC897F5B6A26B1AEBDA91B56EE668EACE53E6DE716939B8FEDDF0133849DAFD340B3B658778CD9F6E7BFA550463903E46C9BE92EA6B6AA6327BD197EEAB7F8DA8FD7CB0E1C4A4FF5D4EBA425D647353549C853621D4B617CF2301CD6D2F2BBEBEF733D7C403F3EBA5F3FA504EBA0069C4CBD4E017F2A0F15750D89F1A0D129806C6466350F87F3939593DF02F211A2096EAB5C917320FE98173D66DCC57EC74500FDEA7A56D011C32D60007D2F416FD1971AB61F2BCF075831F094BA7F75F4DF07525D747E4A313F9388465AA0A9448F75DE2524DD4012A4161982B927F743B5813F35120EA5BCA90551DD3ABEADCC9AA7238B115DA518464E51526F213D4555\",\n          \"k\": \"A2F646AC5A87355FBFE9A37E58F405420221E523844C9D00AB089EFA0FABF280\",\n          \"m\": \"697CC7445AE2C9ECCA2569B7871F0BBB364E63E4B782F734FAFED4FE33E4AF14\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 35,\n          \"deferred\": false,\n          \"ek\": \"0387B2D669850AD9379CA70B3EF1BBFD487214F08616824787C4506C83740BE76CBE452CF99369CA80674D3B22D3E04FF95093BD00369BDC5C8126133C5A15B8A99A912132B19C275489B7914858E806CF4CB10D3FF33E018CA8776A88FA30C8782179D289132E0CA4A1BB19463571BE1977109C2BBE4C9433D6986AF12E7079C4B5783390C95C8468545F48A5D669CC0567CDBAC54420DAB1DA63456C692678105E08A822D0F99584D7A8BC76CD9038C2768663FA11A8D8E7B86BA52DD21B323EC22AC2E7B921216282233039546803D509FFB12BA2FB7D3FD38F4B06B848233BC90C2ED68AC41FF24C6425C6C9321A7B697BA5588B434B1B5A49CBB9FC479BE68257498A05865A7A0217B8319E7BB8B55C15967319CC510147BE9BA5511A967CAA2102C6A41298C0F7620E626693E976318E6A7635536B2CAC9DDDFA58E2382401AC778D145774494D2F476F43E0C3AEE004D360C4A65571BA02C7DA13002661B4838B22B9378F34579F46653A30A88BB501B325956B0B5A261174360F128850908B5A3CCDB5B611CAA26773368B13330C48C6667BB351A2AAACEC4B2A2C70601513CC3A18B5B1DC421C5A056D1645FE9312507AA1D52929AC289F6D75269BA42061D556243B34C638A72DEAB939B496435951C53B22095906C68B1299A288D5F5CCB8762E76477C5A0611EB468ADA9073C79B09E80621CA9C4D69A7146A812133B9637F074586A5BFF4AAB6CD0140B43A1B6E2ABEE7AC50489B9E5270A93541CE03B72C00A26E274A7CD3793334CC04E9285FDA673E0241BB9C9A34AB7935508739E52A29F22CBB9664251C6068FD3842C80B17A0C1772F285525B22C65AC1DCD520217DB5715381C9A663DB0E512B9F9711537BEF235313DC46B758839321341E95348A75972A7FB1DBFB183910A442E985628843B93505534C63C8D8A5358B61D7353C2D174AA1416677172ABC66725E7317ECC83BFF257819790C9F9C506DBFC647DB27BD9747673F666D1239845744BE742BB27067BEC53437D48A528F3221D60C3E1EB00D87045D050BEBCC2B433F2BB2962B0E2E70FF0CC52A5BA7412E39194AA226872184108C4FBC459090C59E4A27FA4823ED8B3CD3C59C2CC6A81D3017A08234206C3854EB3B75BA441AB8C321C55CC8574A04DF42E201180846982333A92D057C61140874A9A80A46037DC2A3CECD534E9EC39943BB300B1772510B7E8186C2D5B6555584F54E39A2AB6473806B9C9E71ACDC8C15E43A885A4C9B42763766C49DDDB890C8C7B22159DC91865D0E79AB10372A9F45963C59D8C1857CB654B2BDAC0AF0A24A663C71B744DAD1AB68A90462BD79F6BA55376125562A5B1326AABA7C19E593B7D0CC1C8711B8CFE3547A00ACCE1646E766BC03767CF5BFBAD8F08590E2165C7706544DB5A942925070A878D13418D31CED35092F19C197DC2A517967E41898796A51AF8911A1857244C085045677865B125573CA5E24231251243682C6DCEE524088572D2015D9A25691F2532E75907C107323731591BB061D525757540338BE19913323D08A4082239363CE4741C92AA29F8CAF146AF1D277A76BB22CD54663914647532AEFDA02406099A75A63F7F2CA5BEFC74A6724896CAB84D12376744CCB1C6ECB1DCABFD20AAEB88BDBD04AA5A7E2C867B\",\n          \"dk\": \"030C762257832CD05D14B5940C483177EB3ABD152BF255C7E3131FD131867DF38679D2B554B2B397282071344DFFB7729FAB95F893B912D15B3BF43D39A97CBFD50C84D83C0D39AAAC921827981AD5D0001954369C3344FE869EAF7A45332334E4F30A1B94350D7951A0E4549612AD016528E2C94128052A304A87E2E666CB101E2F765ABFF89C17F1823D211C4B4355E1B734BCD42326B1BA24491903063B2CC18ABE453B003389C67590F00046D174C1C8B237A60AC9FC78AC381C11C358B37FF7B791FC1560D493630BBDB3C40CD4556B9E671B36814AB439687CF248B0CC794C491C4DB2066325257B45370F2CC8519071615BC73095CA69834955131ED7826FB9E89E56926615730EF1B6C4538B3253030578F63BEF067C0EB70CC4D08FEF146488988EB2EAB0648CBBCC4C163F129D32318536CC91AE103CE058318A9530AFB73EABC518B59AA546AC64B736167AB41DC27C8BB21912CE011C50EA1C4315A5A480183D9616ADC79EEC86B813D38E449B1FE0BA07038811BCF63C1EF57AE6DC2F58BB522B51C356901501A608D547B2ED4439C420853D1BB62E438CDF4CC179E760CEEB0B35DA53C0606E7AFB4BE14B5ACD9B7DCA6174D990670A8BA5F5D391B22781A051946F6B33B7D098155048515413819A0544058265403F6272B6FE4625A78A0F4196871578A6DD041476D0339049B1D70A2583E6514E089144E32960DBC233CB16CCF5691B8A5A1434B42F9B9C73A7BDFF384CCD952A79C507E5A4552FE2ACC3CBC118974DC0260024951FAB3A8347CA016CC220E1B8CFB22B769258223AF300639C5F3F2387D4B204C802BDAE8179222B4D8626C88E63BB74E33C7D210F34749465991B38844AF6A43BC81BBE232698B8CB8416BB55B385CB23C086ABD92F59B209ECCC9C4F660061309815434C5B27B6B2CA33918B0F97A813A59B007E57AD04B24D31379964D1923B218CC79798EB8AC65489B9F4ACA5097B248D1A25FB5A3C44411CA0195B02E0538A7B9A89EC660F2B796FF39D42A18E7C585A0B737798C75AFE74B1EB8941D10831012841CAB389A1DCBFAEC211F3535C34EC05F07619B883B3EC04A4CFEC19E4670486E57DAF0C086CB0395B537D06BC2955F90684FCB38D17B0AB43A5061659C69A522B2970F5579A6450AD2DC76163B5480F89437F5B46CF74AD6A85961E265F595375B36A2C96C52F80D4246DA29BAAD2A18DDB3E38F3C9C18B8732123299A00F689B1E32C492ED24280992CD5C8B3AC3109405A631C5F49C77D97158F93F7473B263F8B3395276346156FA7044732CB5273B27715228EDA6C53CAC9165AB3325622BFA9941C0F93913CB29D55A1A4C1404C8C8025DB6A80EB95B16E80A0E348D8EF54947E6CCC4C8374EF14284373C83B677D83ABC9B5BC0B7C67AFF0664B0A2786575B0A2A962209335D13B003D013C11C68176B6C3337C920AB063D12331D2D0282BF0B460634A3F823F2EE8776E4088D33651392B7E16BCC44E46345F2414CF175734286A73251C6163C1B3F60AAE7A1A3E478EF5E56AFA7C3391F7B0C4D380A1A60DBA7ACFDA1155975C1FCC7603929247DF2B8040A12D63369B54849A52F41B2AA77A32DC867B7304E092110387B2D669850AD9379CA70B3EF1BBFD487214F08616824787C4506C83740BE76CBE452CF99369CA80674D3B22D3E04FF95093BD00369BDC5C8126133C5A15B8A99A912132B19C275489B7914858E806CF4CB10D3FF33E018CA8776A88FA30C8782179D289132E0CA4A1BB19463571BE1977109C2BBE4C9433D6986AF12E7079C4B5783390C95C8468545F48A5D669CC0567CDBAC54420DAB1DA63456C692678105E08A822D0F99584D7A8BC76CD9038C2768663FA11A8D8E7B86BA52DD21B323EC22AC2E7B921216282233039546803D509FFB12BA2FB7D3FD38F4B06B848233BC90C2ED68AC41FF24C6425C6C9321A7B697BA5588B434B1B5A49CBB9FC479BE68257498A05865A7A0217B8319E7BB8B55C15967319CC510147BE9BA5511A967CAA2102C6A41298C0F7620E626693E976318E6A7635536B2CAC9DDDFA58E2382401AC778D145774494D2F476F43E0C3AEE004D360C4A65571BA02C7DA13002661B4838B22B9378F34579F46653A30A88BB501B325956B0B5A261174360F128850908B5A3CCDB5B611CAA26773368B13330C48C6667BB351A2AAACEC4B2A2C70601513CC3A18B5B1DC421C5A056D1645FE9312507AA1D52929AC289F6D75269BA42061D556243B34C638A72DEAB939B496435951C53B22095906C68B1299A288D5F5CCB8762E76477C5A0611EB468ADA9073C79B09E80621CA9C4D69A7146A812133B9637F074586A5BFF4AAB6CD0140B43A1B6E2ABEE7AC50489B9E5270A93541CE03B72C00A26E274A7CD3793334CC04E9285FDA673E0241BB9C9A34AB7935508739E52A29F22CBB9664251C6068FD3842C80B17A0C1772F285525B22C65AC1DCD520217DB5715381C9A663DB0E512B9F9711537BEF235313DC46B758839321341E95348A75972A7FB1DBFB183910A442E985628843B93505534C63C8D8A5358B61D7353C2D174AA1416677172ABC66725E7317ECC83BFF257819790C9F9C506DBFC647DB27BD9747673F666D1239845744BE742BB27067BEC53437D48A528F3221D60C3E1EB00D87045D050BEBCC2B433F2BB2962B0E2E70FF0CC52A5BA7412E39194AA226872184108C4FBC459090C59E4A27FA4823ED8B3CD3C59C2CC6A81D3017A08234206C3854EB3B75BA441AB8C321C55CC8574A04DF42E201180846982333A92D057C61140874A9A80A46037DC2A3CECD534E9EC39943BB300B1772510B7E8186C2D5B6555584F54E39A2AB6473806B9C9E71ACDC8C15E43A885A4C9B42763766C49DDDB890C8C7B22159DC91865D0E79AB10372A9F45963C59D8C1857CB654B2BDAC0AF0A24A663C71B744DAD1AB68A90462BD79F6BA55376125562A5B1326AABA7C19E593B7D0CC1C8711B8CFE3547A00ACCE1646E766BC03767CF5BFBAD8F08590E2165C7706544DB5A942925070A878D13418D31CED35092F19C197DC2A517967E41898796A51AF8911A1857244C085045677865B125573CA5E24231251243682C6DCEE524088572D2015D9A25691F2532E75907C107323731591BB061D525757540338BE19913323D08A4082239363CE4741C92AA29F8CAF146AF1D277A76BB22CD54663914647532AEFDA02406099A75A63F7F2CA5BEFC74A6724896CAB84D12376744CCB1C6ECB1DCABFD20AAEB88BDBD04AA5A7E2C867B429A81D1EF4BA900CF2342C35E355A429B5480869376869E37EF269561E028999F094D80AFE79A90E314F0064F00819FCA23920F563589055EAFF682CE66C3D3\",\n          \"c\": \"DB1E2920A7C52A79F588F79A711636149E2D0FEE6FBE132DA3F0AD98EA4AEDACA476BEADF3CF1D6DB5337430B833AB4FB4ABB07A0F05A0874E80C3CBE9BF0C044F711444EDCFE6D0F168BB56687CE965D35FDD06E1E4C5E004D2BFB1DE6767BC41D834DABD875A15AAB03F3A0F8155495684F45AAF2A28803F50C994681E1677FF8A960B3864B7E95599FACD1ECB063B54837C0C6F3D3C39C7183601251F6CF3156C8F544D789D28348F2F26513766362D6430AF36216791592C2F496D77BBC5B1793047E7EC5561CD7393AC4A540104745EFC932D744ABFCC302943905BCC00073E19509CBF876699095CC53425B772F8567120C662219A40F3A818E32756806C0A283D949AAB6477BED8C0C4E1E3510FF96C929D4173B38B10E6C93E5B9B98914FBD4809D9B8576FB4B403B199117087668B19799B19E3B830E132F98DDA88ACE8FF3154D92233CFC5FD02186E13DA50AC91C6CA8E1CA486BAE9255A57FDEF5EA7DE7C0703E5093493F8D0E0EE50400EE69AF8EBA22FF27C834033B39E0A48F81E58F5E5A7D801B0BDFD8B57981AF14E7267A3C24F897F97A6CF2A4D2E143707C49B51A234C54EEF76DA73C5681F302E272D195DB2C52A79505387B0B6B0DDDD5022ACE0AB94AE87A3F0B6F1663571EC9F3494163EF107957365778EBA8E0ECAC280DB5197C6E6349DF677D653084013578D2840DC3490D50CB863360ABB5CDA3A2DDE41992FC9805B8D418EB4BEF140F199FABC45039AA26697346C1C4A6D32E8F52D4077E4081BC7355D3E4D8864B98F8B9EF7911EECD44C5F3587827B7E8B8E4870B67491B61104A9442CF662FD0A9BD795FBDD5C364ACD4B40300B60C6225D6B6C4F5994D86B9EA4ADBA280B3781C11ED11F9CDEDCB03BD372DB567279D332C794D3C5CC49A335C4D8E174D25C4EAFF55FC23342EADD9F55E1D180B88EFA835286DFCBE8CA28D14F6E4C900BBF80A693A32D8654FAE9819612A1DD9C3C08C5B5E97F3F8E74ECBDA90E5DD8D661E2C81ABC2BB571F977CF86637F06E0CB5E9EBEB9CBBE8CEA5B223FA6343A7CB7C8F138364E9E4DC4D1D4055DCE415A682EF76DA70679F72527B3E913EF12A28E22F8609BE456638DFF0756BE30CE010262DEBE5B8A1656FD5823705D9AF35666FA6B7B1E43977B0AA229C8343C45B8BAEEC0F0833B1A2462A19E78F4A6B4A909BA41FFA10C2FD8F98FCB0B10E75B1E2EFC54552747105AEF1589E8BB8BA7DC9A2127121FC63F9834F0FFEAF85D87D53FAE1BE362B394A8574701599A8918D855D2F39A798D6A2268C984507D46FF978F1DF46F2752D6D593630459E9ECF827ADD4693CBA0D6A22DA6ECFD6A7A2B9B2448292C850EBB5FE03C0DB452D7F06278D779EBD87FDDFA445A387A2F40D040D9F97DCE88E164A766105D95CF2A91CD85795A09F5299D5C51AD28FED62F4B67186185FD52837691956EF5A116A1C37B6CEE25B59025A319847A4EC9062FA343BFAC0FDF62A9C4FAE854823FA0D8A906632DF5466DAF9D8B631DCA3310C090D9B7E\",\n          \"k\": \"DEC4780793A61DC6222167547E251BEC419B282883B18F9BD06E053DB258C174\",\n          \"m\": \"52CEBDECF06579F4A9351F77CA95B5CEDD034D812F3FB7FB50320CA80E4118D5\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 36,\n          \"deferred\": false,\n          \"ek\": \"82DCBC98650A04861DDF15380C8644C6B93F197A5B10702CB944439BF7AAFF090C49E8CA58DC507E5643C17A4D2912BACFB76454D45FD6EACD191910B472463C49B76684777BFA71BC18973677256F649FA6041A6158046F75268E7CB72E8A974EB2CC7DF1B8AB45B0C651BF3D99211C071D55B9443E6A4A65B976962300E5F7325A228A61727D8733C0B3012CC51C2332FC5BEDB05962B771A232B55B4BCB41AC4D85FC6BE5380ECD3259CDE1B809D8B6E67978213798A828540ADB86124B6F137CBEDC4B64E7CCACF78031211A300A79C59508A60A73ABF93580AA64A7E661C282F5C00AD8784B002F5AC79EC4AC85E0290FA861B9D8B30D9E56CB934C6F4CD5C033C7AF7FB55F20C5A11574B3A460C7A3D73376E6CA77AB911CA118C963B97AB675ACAAC6BB73B0080BB5E0261F79E40C0FCA9129A9C118EA705B0BC7A2B45FF06CC15214A807D3521026BFF008024E951AA0752BFEC022CCF92A519B8D1CA910809B63CFA57C341490BC673243B7CC3E2A6AEA2BAD2B885E39738E3973011A67B266C438F2C2C1F63B251558947C00741939278055ACB8569F91110D895B97DC07BACF1389EED1271051CBF6A7B633A47848FB57BEF485993C5462E3B5548B96CD161D58510794CA60951A2A046C8743807619861D37A420DBE94633877134A92180C05B14E10615783C7B32B135428E59311BAAA32FE37A7C35C4390D1185F2931BAD539F08A02198D6450DC55BFEF625E3E209EEF2A7F38074DC681126CBBEE6AA3C974BA8D7ACCBA719292CFA33E4D5094F527BE94AA2231A762CC0970C7C81793471295B9B2A58810952A63B6589B29953C942CE49C87FB7B49B400210FF645C88179FD76BA408DA81C7464E76FCC3329589AA99855564329D8914E34A7B0D02685A92817C28B2B3F8A7A7615BBD569F2DAC20E161077EF46C6FB926ACF3C94DCC9A4B0A27641370B4027AB082896E188BC5CA32BDE911C5D865C87A22207AC35B72B0B8FB342E3A53BF735796F3C8930417CA84CEBDA48FC1E34AF9610F88C18523A4037E3540757162875CB5F82B1718B32EE70A39E5B216220AB3148CB1CCD118CE213F2D5680EBF865AB018631C88FDB6C763C082F59C6CD95156595E22FB0DAACB56341B7645027845813A59DF203104D9C8B1F647BD3537937F59030979463BB63F7C9B69E57CA17491F3618CDD162A9FE1C3F9B326F567C26D1AC6050685FC68A93CF59178BB87EC8D8B703519E60A22702BB5FBCF3BCD6D496B79B65415C175C554298572AAE35437BF899ADAA5C4F513E9F220553093BEBF1769743C6AB0B9FB64703C6305CC64B84CD76B7C8E61429781A822990B698852A3B583AF48C3B22092C513A223110589941D530C8B4CA25CB4361F2001B166C85CF910F006C5B0B71A109AC5986F8264BA26563E69D122644B6348818145CC026B783D917E74C4F46E0C9BF86250F521C21B64D50E910D1F1CAEF4B05F5A50491E9CAC2F3B5CE36407D9937D0E28E47B52AC460103F272642093CC9EB18BAD44AD36B8EEC367F155159ADBA0251F195E52032F1B0514BE83F8DEBCBBFDB5755267E76B028BD07CB8327CD431A7B73D289247210EC905A0529234B2C62C7A66338C1D381C88466B4832204B1B05CD1BF8E0A4693D941A178F62E9E09B74CAD5B\",\n          \"dk\": \"1BE898512189156358E1203AD43B548D039403004E5A5287505054A04B6C9E13B13E76332AF0B500435505D8C9C46B1FD6DABACBF58258479F0459BB13F5977A842AB7102092CB9BF67C3C4524AC2D25608348756529C8CD0652D5E1BCE22A1C8BEA2A72655EBB6A4A25C95A4EB242C760091DA949805A8F16BA7CA0874E01173D873590EBECC87CF79B103096574524B8149BBB9BC4A813AFC20C2D11ABAD8802834A362908480E76E79A3BE55ABEE3336326184E9A7B044603C4A5636EE88FD4974F82511926B95339AC23001B342041735719AF81C23A003C540DD79F34584A025429D477B7B4858FBE4A9D01F5572E8287385C5C69B0223296924EE27BFBA2757D5B11A194803B1094ECF66267411052BC0C021A77C773A940AC08487B90FB33631AAB5D9190AD613C6F360216725505178952CFC63DD5737B1917A63208586E5ABFF7EB03C9B7066B190C03B6CF77048E2D222719D8517E707DA6AB4FE42B4058161F7DEC4145231A6BAA9FB04C638898081543503A5CB7488C77CB0C6D39E45FD793B38B9019D7216C0027A06729854F378C7FFA6BF599A54BD290BAA60A92FB60CFA46BE667C052C176DE9A1C526406566093B4E899E6738C05595FDEB833BC06B3C5014AE6820082566E65AB9F55D915FB3B0B2E560EF2675E34107AE23BC746E23554996DB1AA94A6DBB20A0B43865A9907D644BF46C2F8E5BDCD298BD78A214049A69F650055725174FABE4CD2309A509BA3CC96EC611BD0C6BC5677B95D0B3BF3A51B5BCB06780591B60A0D22291922C27DC9F24C1EEABE27A704463B5F52829DB80CA0EAF917437841C8230526930705EA5CE2BBCD6FEA79F2F196E7CC74A560B13D48C7A2650D5929C08D53168145935C7638103291A5B2518314AFE8819088A25205D17F4ACB5F8080A824874B5DC559597B24E7D706AF510746EA651A3B66137973E163CC96F13E0D631BC696C451470B4145B073360EC034A39421626FF7510C1B88518262D764CE8D342720674B368C8A17F96F992979EFE25272E50402796A2FE59B3AA37EA1E63640B470BD0763D40029CCE3C54024BF33E63A8FE313AB9549E4D32A75C67D21E77D846AC79BEB704ED576C914A866359EA64A548252395E5A873805472C233FFAA6B5766179E0B1CD83098A16971313871762E531C7AB2483A1AF2C19559633B10FE80053504E5584104082C6B8967E86D63025BA11949B6CCC10BB52C93B032936512166099680C83A90E0EBAB162723208AA16E21418010C6C8B79354422695429F382380B08BAE2138C6679958A7711B78362DB5E087BD0459F27AC095031CA024A37ED1624638A7EE4C54A89009888659B2C9779EC4548D0257EA03CE5ED553A4DBC6C8BC7FDB21017EF4353EA450D4B1548BC65E42097B5C86411693260EF44CAB02A008158BCF81CE94388E87BC38E0BC1261544C8670A9CFC954458C02A310BD00DC174C3C1F06326F9A34AC0D2B4D37DA1389D31F83F49E169CBE4A5B742E475CB6609C4CA9B90B584B664741FB97BCDB84A08FC6028317BC03F2C0EA665355E589BB24448F3222976A59426A93E4711072664DDF616D5D5C3971EB03F2D4076399649F11CBF23194208C0D82DCBC98650A04861DDF15380C8644C6B93F197A5B10702CB944439BF7AAFF090C49E8CA58DC507E5643C17A4D2912BACFB76454D45FD6EACD191910B472463C49B76684777BFA71BC18973677256F649FA6041A6158046F75268E7CB72E8A974EB2CC7DF1B8AB45B0C651BF3D99211C071D55B9443E6A4A65B976962300E5F7325A228A61727D8733C0B3012CC51C2332FC5BEDB05962B771A232B55B4BCB41AC4D85FC6BE5380ECD3259CDE1B809D8B6E67978213798A828540ADB86124B6F137CBEDC4B64E7CCACF78031211A300A79C59508A60A73ABF93580AA64A7E661C282F5C00AD8784B002F5AC79EC4AC85E0290FA861B9D8B30D9E56CB934C6F4CD5C033C7AF7FB55F20C5A11574B3A460C7A3D73376E6CA77AB911CA118C963B97AB675ACAAC6BB73B0080BB5E0261F79E40C0FCA9129A9C118EA705B0BC7A2B45FF06CC15214A807D3521026BFF008024E951AA0752BFEC022CCF92A519B8D1CA910809B63CFA57C341490BC673243B7CC3E2A6AEA2BAD2B885E39738E3973011A67B266C438F2C2C1F63B251558947C00741939278055ACB8569F91110D895B97DC07BACF1389EED1271051CBF6A7B633A47848FB57BEF485993C5462E3B5548B96CD161D58510794CA60951A2A046C8743807619861D37A420DBE94633877134A92180C05B14E10615783C7B32B135428E59311BAAA32FE37A7C35C4390D1185F2931BAD539F08A02198D6450DC55BFEF625E3E209EEF2A7F38074DC681126CBBEE6AA3C974BA8D7ACCBA719292CFA33E4D5094F527BE94AA2231A762CC0970C7C81793471295B9B2A58810952A63B6589B29953C942CE49C87FB7B49B400210FF645C88179FD76BA408DA81C7464E76FCC3329589AA99855564329D8914E34A7B0D02685A92817C28B2B3F8A7A7615BBD569F2DAC20E161077EF46C6FB926ACF3C94DCC9A4B0A27641370B4027AB082896E188BC5CA32BDE911C5D865C87A22207AC35B72B0B8FB342E3A53BF735796F3C8930417CA84CEBDA48FC1E34AF9610F88C18523A4037E3540757162875CB5F82B1718B32EE70A39E5B216220AB3148CB1CCD118CE213F2D5680EBF865AB018631C88FDB6C763C082F59C6CD95156595E22FB0DAACB56341B7645027845813A59DF203104D9C8B1F647BD3537937F59030979463BB63F7C9B69E57CA17491F3618CDD162A9FE1C3F9B326F567C26D1AC6050685FC68A93CF59178BB87EC8D8B703519E60A22702BB5FBCF3BCD6D496B79B65415C175C554298572AAE35437BF899ADAA5C4F513E9F220553093BEBF1769743C6AB0B9FB64703C6305CC64B84CD76B7C8E61429781A822990B698852A3B583AF48C3B22092C513A223110589941D530C8B4CA25CB4361F2001B166C85CF910F006C5B0B71A109AC5986F8264BA26563E69D122644B6348818145CC026B783D917E74C4F46E0C9BF86250F521C21B64D50E910D1F1CAEF4B05F5A50491E9CAC2F3B5CE36407D9937D0E28E47B52AC460103F272642093CC9EB18BAD44AD36B8EEC367F155159ADBA0251F195E52032F1B0514BE83F8DEBCBBFDB5755267E76B028BD07CB8327CD431A7B73D289247210EC905A0529234B2C62C7A66338C1D381C88466B4832204B1B05CD1BF8E0A4693D941A178F62E9E09B74CAD5B988AD4B51E1589D3379C6D3E70209E6EA8655AF17EB0907869F974DAB202F540A54A288137BE236A5FCF6A8FBB160B2C2EDCF2F1F63A92F0E985CC634563DB61\",\n          \"c\": \"443DCF704633C6E74255A6E83F0F4BB686BCD9CEFACFC536EC6292A2F4D70C11E4F9FE4A94F4B8E92548EC234FECAFA84BF1372D7858C554BAF1E2556865185C02B0214E950F2C30624E5E42FFD1702CFFC5045F3FAC68E8D2E48063F9402DA21E69E49A1E9AA77DAC5EA1E60ED77E8C498D70FC67782EC17255E0CB7E116DC75168A5FC7C08A2D4A974A71E08A12CE9948A15631A90CE034C04ADF9E99F3B93D5FC4B2C9E880018496F8C92D40181C6A47E313DB9565B377BC3EC2D5ECF0D43A9ACE5EB0439A230DF989A01A662D83C8D2BD7C0A5402ADBFADF91507A8313F1C6B7D705E930182FA10B91FF7359C0D2BF26317015DFF6845E2F4275C76F5CA9F115BF90067E95EA7AA2847F05ED50897F62CC8AE42DAD71C3CDA04AF5FBB756CB7F11C78794F8FC4544D8B3AC1E9B44B545AFDAA2E7C5945E11832DD2D693952F51B87837DA9049D080843D1A4CBF551227822A7B522F449C974925886961E7FE0849790F9B2FF8DF8D0D741E9A1B8B08647810A64D5420BF795E5EA336CA4FAEDD55876E12FA6EEE591AF0F1989D0C1E1CD1252A305467CED0B12766D12316CEDD93FA773B11AA9BDC9D74827C30FE4AAEB456B8AB700D4AF165F13D8A0D8AAF741C445F341628AC4F4254C96063A6986A5851EB19438FE9F633B131029545A2665093F2972678F62EAACEBDF7A49F3D4D67790E4142E2B2A0AA57327ED89A0B9E641CFAD529F61A82ADEABFE7DE1C0D7A99078A8C1E7A00C8CD251B9EB8ED4997508E16AE39B50AF7A132A6AC83031C7E0207E894AED90066CA97F2F616D0C935738D18401B9BC7904424DF5756525EC5D8358EC0B9AD428D0EB195541A93E75D35FADC6B0F8D5CBC8BF15C9F3A7C22BF95A4D864D011D6944BAD207A27E2B05C8C51C6686E9164EACB06D3C675642575739BEC34660A31CC45970C28954297FB37B4C51C4B3337C9725E5A6F1DD0831A8A06B4DF2F445DBF08612AF2D65EF19594C4B3E3F034C5547491EFC3AE9B34A47CB2F35159AA21C097A2FF9CBD66C7A50143DE4F7FF9E4218DFDAF52DAF4040EA17B8F1616E590699FF6C71C37D1386806B2F36E534195460E2774D475052EFE3338B43F8C906D3FC0A736E103FD192B4084F6311F34A2BD162C0B2B9A60EF6F3468E881D7CF56A1455B815F2999934311894F4EAD90DF175E60F56B56C1A2BEF12892563FA216D1C477F061C46DCF2CC056C5E972C5F3018DF6346983163FA7C787EF85BA9979E30AF9CB4C4E2A0F4DFD6EE5B95AC7F8ECFD46E7491B4424C22E82C521F436E4656E298931E3791164360A0502BEAE7654D43C3BCF66B2D69B5E2EDF0CE17961ADCE8A600789E6545AA31FDC12030CCD478C82D1E319F4DFF1681B1209BD867627A443329B48FCD884BD986D105772CA3786CD336366C0DEC0E9469ACDE5FEC612DC24862A7FA00D45EBA7019D237FBA37EA67A4A074C652E6EF910B2C206FBA0DE6D23CF8EFCA947A18737410DCF14E496F64582B06D78FCCD7538232C4D8F122CD9FAB29568B\",\n          \"k\": \"9DC0B2ED91CF4609FFB8F7240D6CD3F65D45105A35770A105B910BD9CC911CD1\",\n          \"m\": \"161889F2E92B1BB28A257B45D179FB76847B664E6D7B5FD9698204A426EE96EC\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 37,\n          \"deferred\": false,\n          \"ek\": \"FC964FC0820E5DE7A73BC507469B013F2C81A225C8C067C4A9351467847DC4D38FF3237A9A38A8F7C273B08B260C5A9B20F8493A668459E0BCAC480E0BB50BC62941720283F758B7AB13B444E8A1F8366FB247C3CC339D413671ACDA6354220B9CA0515881A688063FA87487356C842324B35B064B5847A75509AFF086086CAB80DE6C7218D95877313B0663031E867610107F1491A9F069A2DAA4A88F26CD6CB97B43B51FB7156A47550434421024CC360500AA7595BBBA99116057A64A98C76D61082ABC47BBC2614D2B23EEF8A68D1065ABBCC057B2717DBA5D6B82BEC6683551F082C85C3C430580D7125D1EBB1EA410748D6835C5BA080D1140F9574A4460752C5278E5F24DA829235DEACBFC3663FF71421F3B4C3F073764D11C39C9AB6B6B2F1F24A7CC84AA73A976721AA7BBEC515D0A0613D06F80FA461A032A274911B82CCB9B9A4AC86C3555D37A267C54137B141A043E9C861C58D6A88F5A47B895ADDCA736D11CB227D190481B4FDF680943539437B04BA202B21B19C06BC8037B93681E2121E7A49DB8271422834CA4253F5B96AB7E723FDC599B8BF0567C176BC7C754B6402642921ADB8C30CB8329A1E3CCFFC8A501A077E34495D9333B897C41F9E2A11151902A9B19AEE16BD4038DCFA6118192CC3B09523BD89C0DD158B87276A6316DA118785551B9E1078C22949D6D5302EE5945F87C5A8EBB6220B673B8B7AE6499C993633E844BA8A14CCBDFA931D1145BBC260EC933CA2B8B861F505A9691CEB2B5716CDB1E07DC6ECA0693CE0B8BBEACB59F705814F2C46B336C65240BA7791B10226D3892AC60D11F67F29283ECBCB459CADF6867F41847A99A2E0469A417078AD80B1BB35549FEA2164B8B557B5610C64C810E211DB697ADC43034DE45BF407729B9F8CCBE3379D471367DE881E6EC741A4130C78A90FF24C7C3899129038851069BC68B9EB533354AAB155F829F6AEC9EC9D82B20B4927EECC243F014EC846D59D5C8F3517F96873511967408128A1705B834F81C4F1169EA500A66EB21682A09CE44CE4C9541FD506A6AF12CA3634733E4661681871717A560685794685DA121565671B50FE866B0A565B0A9728A97C6AEC4442C498C54D77184C44D919083921204334CB058FC5EA4765C0478104770B743A5093E842C013769F16B02A46083872C58D3D678DCE87A3799381DF117D0F357998C853606692ECC1F087C0883CA575D9159BE882045E60DDBA38ED3BC8660121FB8EB2D6049A5843A2D2A53BD529881799487B30438B4A585B2E77768E87D060C0D676283431A55BA9C129F036751835299D78CD2D85748C246F8915927B1C9A1194BAC1A103B5A1BB69370C685B15AA264C436683ABA9F4A03C49DACC5A1D3CD5C133599EB16006850BAC4108239144CE682B7301EB220A363032FB608137786898375366B002719580EC7F7733BB2C3DE27974FEB56C72AA61252A2D63CB63D380F1FE3142439AD1151B543591AB0915B514B3E97EBA17CCAA73FF0A3459393BAE20B1D8A8961C0615B180DECE602335A11F7617EBCE0A3A0B3AD1A78A3C8C682976388851285D83BA9E358AF22A14039CA015FE1B42F8A3EB454578504153817455045166F24D6DF0071E884AF76ECBBFA430FC31D1F77405F4B404B538725F561884EDA\",\n          \"dk\": \"7F7A7CC42A00986601D7E76792ABC9454422BF39AA4FA93144980345066236F60D629ABA3BB74FA79718F0A95F6434508A9B0F0BC672D4F5152A8561B8674892BBCD79AC8082394DF7E95F7910C8A2945C55DA8D847B5480B34671E20532B03229D51B2D9A3288E867D59A9C189A8686295D2604B048425331700FC329A948236AF5A83E9DE525417BB235604C43B3B2628642CE11B911A2A88038A5FA233555B3104BCBB733E446E9455B9D1A7409357AA19656F23590CA11CE05336B0BE8406DD39CD3B6A725D49A499B1809791540A948D72C56623A768C27A32826744D75871579B85D16695905BC62744885929723F308D4870ABD61B7EC3CAF86D2422D6C53CBF92DACB85AE49622F6E37B96F3BA812B26F0C1220B4C4D37A9BA4032197FD4025FC2C75147534F28CC8258A0314151DED61CB391B17E842FDE96AA3AB89C2C13C6BC015EA2DA00D12877320B4610C34526D31E2C7A981028C2C55BA22479A62F375BA7400AA4076617258C1E5A35650312F75253CCB11B28FA2FCF170FCF9A96C34385EB181C6D777621BB3CF6081A8457A7B92C89F0167B80976CF7512C10E1662DA3018AA9129E3661A59ACD85D75BA9707CECDC23A9FAB2DBD6AD85938DACF56C8FA27C2A7BBB520CBACEEBCDB088B65E47B9687865A231A393D645F080B61BB288C9166976208FAE67B8919766A7910BE02BBFD8586186361740926182A3ACA832B2151B5FB01A0E726650D4E92EDF59BA973446E4C886904919D07A8EB67267AE366D8FF6311C78B6F17193C655B46F51CC47E05B4D3416984CBD2C14AE228B44C1141427876AB6818DE42807FC1723B3BB2266418219004E68B59CA71C5C2B8B8C6D7405A0508D6A63ACC2B4C19E228F943718F18C8C743A020BE75DCC1B1CE1F1793FA52C6AD5CA3D63C871366237DC82F01A837A758D9005CE677C9E1477CF5B49A2427932F971821461593426498DF90AAB257A1683355DAB67FE9234AD2832216AA0A2F4690E0785A08459A2809717DB7A231C3A5D84A98EC91A446B1BEB8851D2A65D7D61569EA98DA56C7A2CA24C71BC511747111FB31CCDA0CB391638BC3126D4427ACD9171D7D2A79AB9815ED62C332ACB36335CED45411D1545E3832B52685D444344C77325F1372E89F254F5E33A1BB450B1A54FFE32CB4811B813E27C12EB95161A5A18105D2D937012070868AB3975D552C056267F0946E1A5C9EB585F170CC5A6A578405382E030A2A3265F468A9911341F7A4746B71C65C090398E4443EC6AB4486BA36788AF0996AF5C6389EB31237CFA0DC8C17132051A28060376A0C133FC51F6EB81BC035501952164504115F8C567B52E96BCA0FE13A2BBCCAF08637AE31BB4C0032EE32514E041C2DC4587684027D988133658414567328F42AEC2FB89B4C830ACE152C326443D82687A19C87D122EEF72C1E742BB4A8C52B81646CB0C487EDB917A122B44751F209489ABD629A0F232D0502DF7A5BF5710215DFCAD13F2B6374535A839CBC9A505EB3A9E8EF17B16B70FE04752C8E1936926BBEE856A188C02183768217293656C4B1E9372B363BC673110255014FAC01ECF6B48371B8E7E401C01E1640B8A4F1C3A4009489FF2D46BFC964FC0820E5DE7A73BC507469B013F2C81A225C8C067C4A9351467847DC4D38FF3237A9A38A8F7C273B08B260C5A9B20F8493A668459E0BCAC480E0BB50BC62941720283F758B7AB13B444E8A1F8366FB247C3CC339D413671ACDA6354220B9CA0515881A688063FA87487356C842324B35B064B5847A75509AFF086086CAB80DE6C7218D95877313B0663031E867610107F1491A9F069A2DAA4A88F26CD6CB97B43B51FB7156A47550434421024CC360500AA7595BBBA99116057A64A98C76D61082ABC47BBC2614D2B23EEF8A68D1065ABBCC057B2717DBA5D6B82BEC6683551F082C85C3C430580D7125D1EBB1EA410748D6835C5BA080D1140F9574A4460752C5278E5F24DA829235DEACBFC3663FF71421F3B4C3F073764D11C39C9AB6B6B2F1F24A7CC84AA73A976721AA7BBEC515D0A0613D06F80FA461A032A274911B82CCB9B9A4AC86C3555D37A267C54137B141A043E9C861C58D6A88F5A47B895ADDCA736D11CB227D190481B4FDF680943539437B04BA202B21B19C06BC8037B93681E2121E7A49DB8271422834CA4253F5B96AB7E723FDC599B8BF0567C176BC7C754B6402642921ADB8C30CB8329A1E3CCFFC8A501A077E34495D9333B897C41F9E2A11151902A9B19AEE16BD4038DCFA6118192CC3B09523BD89C0DD158B87276A6316DA118785551B9E1078C22949D6D5302EE5945F87C5A8EBB6220B673B8B7AE6499C993633E844BA8A14CCBDFA931D1145BBC260EC933CA2B8B861F505A9691CEB2B5716CDB1E07DC6ECA0693CE0B8BBEACB59F705814F2C46B336C65240BA7791B10226D3892AC60D11F67F29283ECBCB459CADF6867F41847A99A2E0469A417078AD80B1BB35549FEA2164B8B557B5610C64C810E211DB697ADC43034DE45BF407729B9F8CCBE3379D471367DE881E6EC741A4130C78A90FF24C7C3899129038851069BC68B9EB533354AAB155F829F6AEC9EC9D82B20B4927EECC243F014EC846D59D5C8F3517F96873511967408128A1705B834F81C4F1169EA500A66EB21682A09CE44CE4C9541FD506A6AF12CA3634733E4661681871717A560685794685DA121565671B50FE866B0A565B0A9728A97C6AEC4442C498C54D77184C44D919083921204334CB058FC5EA4765C0478104770B743A5093E842C013769F16B02A46083872C58D3D678DCE87A3799381DF117D0F357998C853606692ECC1F087C0883CA575D9159BE882045E60DDBA38ED3BC8660121FB8EB2D6049A5843A2D2A53BD529881799487B30438B4A585B2E77768E87D060C0D676283431A55BA9C129F036751835299D78CD2D85748C246F8915927B1C9A1194BAC1A103B5A1BB69370C685B15AA264C436683ABA9F4A03C49DACC5A1D3CD5C133599EB16006850BAC4108239144CE682B7301EB220A363032FB608137786898375366B002719580EC7F7733BB2C3DE27974FEB56C72AA61252A2D63CB63D380F1FE3142439AD1151B543591AB0915B514B3E97EBA17CCAA73FF0A3459393BAE20B1D8A8961C0615B180DECE602335A11F7617EBCE0A3A0B3AD1A78A3C8C682976388851285D83BA9E358AF22A14039CA015FE1B42F8A3EB454578504153817455045166F24D6DF0071E884AF76ECBBFA430FC31D1F77405F4B404B538725F561884EDA1279BE1122713D340A3C86B3CE48C6C5CB5E48522DE5B24AB57F59FC341BE6ECC2F75B1351CDC350BD1726A124C06B996F566FF820A4D3569F634D564EE84224\",\n          \"c\": \"58919FC3F7105957A7599EC0F84E2A1031F42E26DFD7DE44CAC5B99E1272313BB2A6F52B8D33C9054B368439A123AB49E75C7B3FD397F1E6B962126FEB574D0005C69572CCF752B10D9E84569C9BCF3F3550784AA1239E0B4B4FFB38EB5937B287B5C7D8DD0FDBB38E3BDC081B43FB7967E7E35D8FDA89153AEBB7FCD7DD810738A555C653B84F6BD246A17FE2EDD0FFA8AFF151EAB2973F2F8600FDDE69B61D06EC99CC547D4A81896A36BA9A3DC66A5251EF145C9A690B2C696C165FEE89B8C2A14ACF8B5A12CBF2216118FE3B29E2FB0F194D418BCE181B096FFBAA4AA515CD45275459BC5FA6E1D1ED33D00609F11DCDC5247FDFE841E01ABB7B935BCFAF9960E7E06C82463BD27BCDBCC3D12AA0F033BDC4605109AF3D7B270FC5D18C3CFDDEC2CDBB3587DBE25E4B0E9AB2049ACFCDE49874960482CD25DCF45433179A9BB69145DFDECA9AD2D3BA9AFA4B6B8681DED8DEDAD95241557BE198A7CBBBAE937F70D7FC513782258D89AE870FD2411BEE47E4993B9BC1F66852E4D314BD54C848143398278F11776CD8FA01FF345E606A514ECF780AA5EEDD94B7F88C495F8E56FEE7FADE2F6CBD3406FE540428BF89CDF66357A0F5F45DE330D9FAF72F02D41D9BD7B6EC9165C70386824DBA7E2322A0409764FA9DCA366B4A9C1D3E01495806F959D13213471978833CA2BA501C634322E67CBEC59FFA4193E848025BADE1971035636CE8CC833047EACF669FF064772647AF66CBA4C5C87D101C1EB2EAB87A8C3CF31D6C970E846C8B50289DD1572B19705CCA364D2B3090A75AB299749D0A320C2260A686DF569F802FDE0589799BD54BB5FB8873ED15CFCE76CD20954E52BF7B3DBC68B573EC0502F3D6715C249486B5691F91CD11B4A7B3CD1ABB8C942E2426B0AC782B306BAF6C0D9F2E2EF53FA9E142BDDFC4FFC6930084036BE9E51D997C8E24F89556E0CB21BCCCE16330AD104A8C6851B2E6CE8C58613E19459E2C2398C9B33A5FFD19CE5D9624344C52F379AF49F04F1405E9510598622B9C6F88E95DEE43A91C0540661AF86E3016BBC2339C2E922166AA2D6ABF987F1E78EB1E3CA9486380E62577FF487E1E7A4536C7ABE7F8FC9569E254B96BBAE04DE314F7D5C36A388336D53B9BA8667350C7F3A04DA68BCEA28BD95C52D14E1DFBBF78C8CAC150CC15D015E4332FE109E9FDAEB64BC3A400599CA3D2CBD2A720FB35675A5BA78BE61C4A82B4315D3AF4FBB0150592B7EC135E063CD5D7C23CDD256D4501513BA576BD6726878F2B43B64ED0DB79CEAA344365617570A1E7957ACA8F5B5F2FF708190B2A4985A6101CE0475DBD528489F4FDD8A9E4276CBB7E6FCF74866A49B430A666C4DB800DEEEABC4E7AE9DCF6D91C421246F91394B949575CFDCAA8527CB9A4CE58CC1A53C7FB43193FA39E4C12EF8DEC1F1C6CB8134E5938AB23A9CDADB4099498F5F39274F10228FC297EEB7BE6E392F00B092E70B0BC262F5E8720E87C8BE1730E22193290DC1AFA0AD2D98BA6206AB3E7B63FFE6C69D576\",\n          \"k\": \"D8D24017609D9ABA1414D18AD4AC9E14A0954AC1A80AE9F29527351898F61483\",\n          \"m\": \"3349557DA70FF69886ED032A91D8FC23BE9E5245406670679A6E92AED870D369\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 38,\n          \"deferred\": false,\n          \"ek\": \"ECEC377523150E39B8E5A4B85F8237A685630EA7A3443A9249D4B90E1BA3438CB095AB5555706EACB3A61D5143029856C7E80B0D9156BDE63AB4632605F6009450B1CFB6B81A58A43687C4DB56BF087A75DF047237E9A9B79408A8369CA3AA1731A7190FF3C11A7699E3F83A1F655B3E2C252D732BEED0703A642BA3500859AC70C51C3DE886C26BAA47EB221074F97FB74957F9C61A02405D2E2B95FE198C2AA24AB3D649A3B6803C3636C03B3E199AC07D103960577738CC833CD42CFF97C55FB4BC577B70D1A8642ABB77A1530ED0F1405B2245347256C0D47005A634C9237420574130F607BE82CD00D81E15562AB8461F681159737ABDA7441F7871AE7B28401A6BAB14412A5BE5A4BDA22A46425B8B51964A673278B873C4F61DD8CC5A7DD3B89E11652C677E052643A32139AB971B5B0061D600440495B96AA3113725640F6A91DA0B4B651630EED446BE53C6C6365BDD2BA0F757CEFE05C1231B666404199937736E6A5FB3466BC4590767C2BFB2041DCF254886466288BB45F8E91BBA876B9C093BE2616BB3D2B516526495E74BD19B1E9C36543697ACDA981687111FD05A1B9F2255E8809152138B983790EFE836DD7A68A8A150513699969015BD65B05D364FBC851E11B506C607BE0CD116910C94A3E3258AD263683462F5826B85B4BB1DD2917B4034AD25210AC26F2355B05681015C9A2BF622BD28431CF2868ED213124F124C7C6797C34952870CCEA4970AF909BE10A57BC7AC37A536B7DE2A8B0C4459A6223B4EB781C2462ED3AA135015754BE56F2B501AFE68022C81C16D40726B8C77B0845209FC4B77B64FF08609C0567390F4C9A8187A15C719FC5C9C01433CA77809D3F9195D8BAA94B24CFD909D6374B8DCD2355D1723400AB552F264C938B4E1E8033BE2A0B6FBA03729B38FA39F285AC01A136C07D10DEF390BEA5688C41B155D81174662AE48766D1791C60F1606BDF9177D995D9167B755908A68E7A7CEE4AF53A42EDB486C8D119FED00BFA643ABEB12666E2CC2295B8336122E7D9601F4DA405B7666EA276A172026264C23863665E2C21FF72A6CE4186016D88F167A358889AC1F8B028CB4622CF07474181BBA6BC5D331088B88A1C185B28B90CBDDD54D88D4A911DB638EF236D6D06EE968B8E5FC11F2D9AEF14A3248EC921FF0ACA4BB1369F7B5B3991ED6B16C3A918FD9397BA4E454FD209567D97C8BFB47558B6AC7726D6C83768E0A64B034C0EB36C8BFB0BB86C52B51FB7DE503C390B97B00A4534206B267C42333B4C05BC90C7FD25609548B2E7087E792BA21E360038A6218872F94035E5F13149CB440B302A591817EB0E229AFF03CE9005535978F930622DD422722C059E0BB855143053F348B23D5ABBD49B7DE9478EBB63380E75F9054922AB65F4E897D02F9AF641CC6A34957B91A7969A1270E4264451053267137ECAB8C8C8B9B76354A63E1ADFDCA087E54A67F8A2DE4AC6A636348017C6203B4A27322167C2ACC85F63335E8AE4D1909DD1C3DD228C0E3B84470446EDDE572F7924DD409764FD98F43A20E5AC15AE9202BC1AB58BADB3D06B6B77E81ADF763836398BBC45433015B6523652751B8292AC51230440C112C6317A66F24C4BF927C7EB8C186C9DA10E1BC25A4DF1CCA9B6C3407955972448DEBDB284B\",\n          \"dk\": \"F8B574AEFA5362C79AA69AC10326288D0571F154CB001C7FBE1C274C1586792068209CB65D70CFFB21947ED752EBE9B7D72502726C1DAA9450FCCCC3A4C4BA0D1890C530C601C18A7001B079684EEA47AC24251E9A599967A24EF47B52B4FC580E995E411237C02687CB38030DE74F3EA0C1740316269322482C64414A8C73230A3B58554C57142F2613D190BAA3142220FC4D99A5806C89785B8730B39C12CBEA94F69573A61999C82B7E4BE3A9B605243D25971E24B330A7A51E5681774AB7120C13DA25ACE8494019192F8CFC3043628F7E6B531F8886AB2BAFA1D39E09CA373AFC1A46919F8244066ED1B7C2124C01D760D216978191BF3573C093A15FEC579D83EB5C6074B0A4BC13094232857181DADACD2C5A4AAD97C016894EB413C2CE671687A875B45CC1BA0C63D9696C4FA102BBE797FDC86634DC28FD121EA9D904642ACFC56C2DACD2A7BEF96A43A23F11591EB81C954182403416ABDA87BB5AA75D510007B8D53491E0BEFD149658AC135D1341FEA37983D1A0EA0B40CE08A2547122C3022AE2DA8E2E7BBE8DF46548A91A50A1C8FE651E8E487788A2AB894B7912B15C10D09571E23589D166C989329B29349CF7A037916B467AB29ED59CCD37AC63C0AF6F335F5FD6BC09845BDEC1C86748BC389889425026E3105DCBDA9E4C5664BC2B75208287EC78B8AB031B6B494B37D51B0F540B4BE0360BA4744DAA369FF384C1D5BEF1640A7C76C7FD4743C7EB631ED126F0408809E00700120CD6DA114C5211521C067F1C7EE001AF1AFB46F7DCB90D70A3A35098946A4EE53629CCB678642A11E18B687C9680FAA3AE926C60A5672A0C786F17273C313548D6894B27D52AB5F51E9623B4B03398FE9014E376040E522178185230456D505BC1026AB91BCCAD5B2C38ED2BB445753F30845A65A816816476BF184CB55714A64375E480CEF74917E616CD50E7AB635A88D96414DA6710E49A468C3AC91B85726536B78C9B93DABB5866998619B377BE0B54C5B6AAD4940156C549782C64653A68FB0B15E02089FBAB37955104BED20696C48E388257375C080C33BDEEA081EA3285F6A94A128736DC221C17A88CB50A6283F4C6684AC0FBF8C110344DCDE703E4A7C3DC49296AA684A31CCC0DBAB7EDDB2EF9C40D35A42D077C62ACD734BAD82E7475833A1527222112739C8591F61D2156BD6BC886FE501873EC67061C3EBEF03427C969647918433088C0C8C7CBACC9CFEABDCA3A91AD17A2F5AC8C9F195D53C294878529B0B9C8D310897C09483B3C46A841CBB6183107154E99E7A91DF44972C7BC7162926C9672ECA74FE155BBBD4160D039821049A8FDDB91C99752B99B34E122173F654CFD5C69DFE7608CC96F7AF1A0B8476FA4B715E8B81F01E24A79565D01005667DC264D31AF87F6777E118EAC48346ABCB281523E97A787194B6A6DE33362F31117958C1D35AB4C190818C0049FCB6B42151C8F038B8018B997CA0E7DD9962B0A62FE38A424C06A4E650E68191C4538AE67B1C3E997627BE3B26BE14166F016A8E80070CA86B034ABCC358B2CE917FCA34CC2850920656E80597A35936B28794C63FA944151C0CAF278CB589F86171D0F263767F294A71B6059E496ECEC377523150E39B8E5A4B85F8237A685630EA7A3443A9249D4B90E1BA3438CB095AB5555706EACB3A61D5143029856C7E80B0D9156BDE63AB4632605F6009450B1CFB6B81A58A43687C4DB56BF087A75DF047237E9A9B79408A8369CA3AA1731A7190FF3C11A7699E3F83A1F655B3E2C252D732BEED0703A642BA3500859AC70C51C3DE886C26BAA47EB221074F97FB74957F9C61A02405D2E2B95FE198C2AA24AB3D649A3B6803C3636C03B3E199AC07D103960577738CC833CD42CFF97C55FB4BC577B70D1A8642ABB77A1530ED0F1405B2245347256C0D47005A634C9237420574130F607BE82CD00D81E15562AB8461F681159737ABDA7441F7871AE7B28401A6BAB14412A5BE5A4BDA22A46425B8B51964A673278B873C4F61DD8CC5A7DD3B89E11652C677E052643A32139AB971B5B0061D600440495B96AA3113725640F6A91DA0B4B651630EED446BE53C6C6365BDD2BA0F757CEFE05C1231B666404199937736E6A5FB3466BC4590767C2BFB2041DCF254886466288BB45F8E91BBA876B9C093BE2616BB3D2B516526495E74BD19B1E9C36543697ACDA981687111FD05A1B9F2255E8809152138B983790EFE836DD7A68A8A150513699969015BD65B05D364FBC851E11B506C607BE0CD116910C94A3E3258AD263683462F5826B85B4BB1DD2917B4034AD25210AC26F2355B05681015C9A2BF622BD28431CF2868ED213124F124C7C6797C34952870CCEA4970AF909BE10A57BC7AC37A536B7DE2A8B0C4459A6223B4EB781C2462ED3AA135015754BE56F2B501AFE68022C81C16D40726B8C77B0845209FC4B77B64FF08609C0567390F4C9A8187A15C719FC5C9C01433CA77809D3F9195D8BAA94B24CFD909D6374B8DCD2355D1723400AB552F264C938B4E1E8033BE2A0B6FBA03729B38FA39F285AC01A136C07D10DEF390BEA5688C41B155D81174662AE48766D1791C60F1606BDF9177D995D9167B755908A68E7A7CEE4AF53A42EDB486C8D119FED00BFA643ABEB12666E2CC2295B8336122E7D9601F4DA405B7666EA276A172026264C23863665E2C21FF72A6CE4186016D88F167A358889AC1F8B028CB4622CF07474181BBA6BC5D331088B88A1C185B28B90CBDDD54D88D4A911DB638EF236D6D06EE968B8E5FC11F2D9AEF14A3248EC921FF0ACA4BB1369F7B5B3991ED6B16C3A918FD9397BA4E454FD209567D97C8BFB47558B6AC7726D6C83768E0A64B034C0EB36C8BFB0BB86C52B51FB7DE503C390B97B00A4534206B267C42333B4C05BC90C7FD25609548B2E7087E792BA21E360038A6218872F94035E5F13149CB440B302A591817EB0E229AFF03CE9005535978F930622DD422722C059E0BB855143053F348B23D5ABBD49B7DE9478EBB63380E75F9054922AB65F4E897D02F9AF641CC6A34957B91A7969A1270E4264451053267137ECAB8C8C8B9B76354A63E1ADFDCA087E54A67F8A2DE4AC6A636348017C6203B4A27322167C2ACC85F63335E8AE4D1909DD1C3DD228C0E3B84470446EDDE572F7924DD409764FD98F43A20E5AC15AE9202BC1AB58BADB3D06B6B77E81ADF763836398BBC45433015B6523652751B8292AC51230440C112C6317A66F24C4BF927C7EB8C186C9DA10E1BC25A4DF1CCA9B6C3407955972448DEBDB284B3185127E1DB71871889DFF67F5D4E97656F7115F28F00AED6D032FB3816189C9EC00525B2D58F1FF5026A6F9CEE39EF8DCA115C0BADF3ABF3244BB9FCD113DD9\",\n          \"c\": \"0E505EABFC938559CA3FE4C830365B6EDCCA3FF29E815FB9EC8CF1857458A49E461FA69EF81E5C2EC80875885CB2FA27B810375368E7ACB588915944CCA39F196EFCA8D7D8DC4C7A41B12997052C80141058C24FB77E37AF8B590836B81913ABC4F8705F8A1005CE016E33230C04437C8187C4D94613CF60AABA4D664A836D5DC8F5CBDA739A1E464CB8838CC6E9B8D5A80A90799AF838724256BE25AC34E6A3D4E622B254E3FB8AE2D7C55BF64A88562853C876CF18E8FCA1AF3548570492AA3B400E3FE33C58B303BFEA014166ADA4E2BA779B463C4E606F46DBB00EE7F937788872F52B25E8DAE7BBCBB47956A1117B84E8B3A01354C5C2C276F89EFFD60A0CBB8B7B8BB5F29E26C47B34F9026E05B41C06B10CAD7F15446DCCF2740B9EF4E26E7AA3DDF63DF06A530E0E6765B24E213DCE686C292B4210C0492ED87B08BEA2C18F0B7AAF6EBC0C67FFE888F9BD0D55B4F232BDDE3F6C00DC972C16601EF2DBEFA5496547C913C2A51367E8E5B1B16AE02182E668BF8B7B5EEA226EEB93BC4564B74AF9B2EC5A9210EA9608E23B7104DF0C400EB97CD23694EC81B529B0B4E5F5881E3FC53414534482E6A22386B7A6AAA7D79B714395FC88A1C626BFE0174A14FBD8025E6841904D7D3DDD3BE941A102C1D435EC9ED62C0F07A60BE6BEFCD8E3BF34B2EA2FCA21DE91469479D5466CD6B599CC6DC9358319BDDE21484BA923A8293FA5A3A6664B81709FFFFC61180602F2E82E09DE6F80098DCDE7E50543A32A2AE4DF13F30A2ACC1F143AEA50388380F68ED9A70D8F90709839557E87B71A348A6BBE8F2BCD513B8AAD667113868F8DBA8B44F844CDDFBD6079B0964BC9164D7153EA9042B0166C2D35048CEF2FEEAC4979C5DC6E7C3470F99820C1B6FB72BBBB421D7E7602BE9424179515D6A6EF4E9599F44C878D25F2A3EAF38C1B06C6C45DBFCA9E53199AB78C9BB7CDFC9DBCEC46BFF12CF77D6F1F00B8A52A943BCA9D85A5E485E07C053FEAC6A2FA26CCE9FDBE9441CE1F379F15FBA36417EFB888EC69C2966EF3E52088E395843A32607575CE79C686FAD4E20223E198C069AA592CBF7EAA2B5FA375A01D2DD78326F5FDA930630D93C7C095B8BB7D0B6FC6020F2729412EC1C0D89F03989B1FDD708F21D22E5EE9C7C1F0D6E116BF633F7F989D693508EB1CCFD87F0455CF8B796FBE8BAA5B9043DCCB18FE013C91FC6CD47CDD38FCEDE636266FF5495A0493EF8932FE569D1E8E3AB4C477A106FE4C5426493DDC24C02CC59CD086D16DC4C7F494C12F75FB7971C0309ADE96445CEFAED2067A18D0AF1B6992FE1087F0A863D52DD2AA128CFA888B0FED293F674E29F2C9215A0558331F52F8E8F2C58EB9AD654A3978C8294A083795CFFC311F94E743A83FC684921D5A56AF179FD9EEA692D3AC2B937B07D54CD132DD802238AD59E8504603368EF12114F6E21E2536AEF4956715F24A58F8F59971FF0075DC6FFA14965A4813503674A3FF2946C4B36BA9520B5C8E905DCB770296F7A1C991C28C6AA5D3\",\n          \"k\": \"330D1A2D0E0B5DD7F759C29A22D91A09BEF17F8C5566A0D3F30E3817CFB7CBFF\",\n          \"m\": \"6F1694589DFFCE022DC4DF1852FA49A41C6E8AB9F7887E70DDAEF4232B045DFE\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 39,\n          \"deferred\": false,\n          \"ek\": \"48E11D1B0572F2892B8F81A8BE330B16C7353B5C319A34874F959409E6A41A71585BF36AEB651869AA87D400C6AF32B7F8270BB9D5C53BC3B600F493904AB000149FAFA1B090B544EA0101EE940EFE878DD64739CBD78D317A09A45775B2AB9E0383C75D049083BA64019658B4F44573C1B7C0B3A59495606D9162438B1A0A000C3AA5834AD5BBD11893598052E5B543E209215E41B8D49C4107F5658F7685CE53459F21452AC66F0D2398B6971A5A23A056444947220E95454A404C7213686E58A42E346CC0C44759504B2FA07198D742B1BE01A66C2BC64498CBCF6482DAC99DE7DA703EC1441D08C4A701BF84FC956B11411AD610146C59DA6454A8B3B742D91DE3C09A5317A9DADC23ED0A922AF339C090060B869B55FB3AA1C603C40868337687EBB27E37B804EA834D4D0084B21307DB0A64F69B7B49B5B0D237AC428ABDFACC01D1012A04008CFDD336A06B9728A036B6C60430A8CE887C4B006AA7E7C54834E2934F8400D030AA63E61FEE96C22FD49685A7C3DC2287EC2964CAC78B40340200C3AA6C8A67BDD30FB7605F0205CC0CA043A4574235B342FCE51195873F7FD99077CB31229B91D5923B67D391B457198125B7F198BC7CE58FD6B96BC35CC3D0281957B4110A011262B1C4CC0445722CB5E8C102408593CE6445C4584E373AC890187986A49414643CCB2861742466939C347CF021D1C19D36398A82C64D1FBA4C44B56CF384C04A1944449258700A66B1E2C9185A5E8B17772538B33B038B877353DC251952DBB899F04A6C677247D4CF4E4A5D2B22313A33A6D49237E7C04C61B2B0026531583C6CF6332C4C7853B7A8C6A4A5783D1B79FC5725F9C7157600747C0203E313CE91976CE44CC4E0D67DFA830448999357F076F2D41EE6FBA68C53C9AE5C025DF867E3060F3CF164218142D6C2B5B526BCE6789DBCBC5765846565575AB154C545FC3A81AB8FA1353B703B72B011A0405588FC0B8C20D13ACE9A93F6B28EA1C05CC686CB0AE722662A9ED902AE87D48B8A9667CBC0BD75742CEF313F1649C42C760796C979369736CFF05516A72BA36849D2E213AA497A6E7774EB9A896AF0A13387ADE8F594DEB482D9FB9E4C740360B5089CD101E5450C837C8214247243417C05ACB9D319809D33168E707BD1168709F7829B584053B554D95557DC032683D885AB406E205B6F495C23EB673E05651ED9DCB87C4967987B7B78EB349F4AABD35C7242079FB30607ED82A0503173A1B785CC36A2181892114003E2D33D17AA3B0A5CA47974B8F1C93B35107DA3900A87792433F154C643B34519941CC60710F07A71E4AE09A7539E72B9235A07D5DB53C048493B4AC8B9F0A3696892A0164DCE38B7F8E711506A9220A5C6C61AC2EF3942D3984907E59DC703ADBD93B6B509A156CC34B226188778BD191402DC94124CC7981151934F68733D131540056466437CE2B31C1320362AE5247C35CDA6E13E5D9BB89D0C8DCFB16BE8BC355A0C0C54E7ABAC12621ADB1EAF82B3D1E915315A3B7F3C98F6CC117BC3C850258059737171478E3AAA7FE5D7B5CD218CB428153C07AF80667C096C96656C85B8B0BD1F606A7CEA963717A18AE642E9E8A853D938C66651EE31034CAEC7DA6097A3C35BA420022324EC00CF53B53E9EBADC6FEB57C9B5BF5F53DA\",\n          \"dk\": \"C6DAC8A176AC00983EE672BD5B613267D594ADD9096A5B3FC477B5E5E3AA05D1981313CB7848B1A8CCBC7DFB8134C56FD4EBCFB1CB1069663FF5425829F23ED1501E5C506C4FC8CD2CC36C74A63EF2956F3D71C3A22CCB622282069584D019090148CAD445333881AFACB2CD7FA2C5FED9A081678718D9A16982B5F35869B682620C968E9341760DFB6BA865CEB1873B31021848C348B30982619CA19C268F77312F20FB939778A09020A4D1220EFCD70862975930D5B36D37C361687FBB0186F972881D3CAEBA0686719C8FE2713B28B7C470BB23A0F47095286F9B5BA8F019C8B04623734800C205BFC4702E7DFC70FA32390159565E39A18C19CB1E758FB47BB8905BBAC6E53C92C52822C51D90CC869E0C0354B94C54F181252714C45426F71839146510564AC3CDF3ACFB0213C1F1CDEE523472D043EC8564209B123FAA0D5BF344C31C74ECE10E66B2C169079AD7D7366D404349F01BA6D247A46B57A2264C7093BB4D27514DC5620AA91E3EC407041AB3B8E4585CC870F4B5153BE304C31A9A3EA54EBC071D03D5758F00C6CFE8BE6C6635B45498031072EA0932045763744A1C8BAB8819EA6764805D73D994A20A5C8C4ACD505022205A4405020E1C74B06B89A498D871DBF37E7E46B3439BCEB0809FF1141678D428A2866BB14B00E7296961C7BFA9D932F664BC310B341C9158172876421736CF18CC14B7B3384A35319B8552351A30BC5225AC7DF5DBA28AE696E4B9AC30F426EF7C1272EC4567D21E16D6B5C8E44DEE901AD7C6146271A7C2215F2D3438AF669D572B12E3639888416F9979448A45A6528883C9F41EF0D6263FF04CA17385771C007053A5DA700DE42B05CB0468ECA8840B822D73F5268D192F5FE6B15B87A8CB790902A295BE32A62413A9867239FE08A422322E688BA4306BA98359A8FA726A7A1400756CAFE4DC67F97904496B5FFEF21D5CD1396558B4F7982D77001A87830057F569355C0E7D7870A6696C0EF47B31A0CD97757B94F195AF717CBC205FACFC10A8368F06F747AFCC6922297C3500248616781825A07A03626A371EB78A065DB3A7BD25CE0C722EF4F6747CBB5997EBC98BDCAAC92B12CA4A4647327FBB980F35B8102A105DB7361B82161B89880F1AB008B739775B4726D856C1A8D13024F36CF0B9C1E5828677127F0D82A870CB19572B93215B02281C0ECB96204C40B13653416F3C8CC761931FC452258A65EDB2C0A10560E184052D87CE52D2CB90311578494562307606195E2C484C91D93A3ED11957E3B162F7CBF66832E2C0143AAA80C9D309F14B8FED067C745A74BEA5AAE36CA0F264199B2892A00B430F616C43939F0F0C5E64DBBFE858AD17E5122CD96E5E312F27CB2AD4DAC9F628C28DE55AA957CEE08CA6B8C9C35136702C12233FF42919331C01D53DD4F817F2DB2B587585CD92867B7786212475325C50EB9254D0A2BBCB1B2AE563634224A8DCC167C8910F7405A8BFC0C978877528C71EECD3632E13997B66C242A9A3D69A768355C333D73BCB66AA3664074DE3A644D24B5AC48CD0F13A39AA3996209E9522C499F87777407E24B746C9CBC7C63256D2C5ABA644B2C1267DA61520FBB756724C9D09095803A13F48E11D1B0572F2892B8F81A8BE330B16C7353B5C319A34874F959409E6A41A71585BF36AEB651869AA87D400C6AF32B7F8270BB9D5C53BC3B600F493904AB000149FAFA1B090B544EA0101EE940EFE878DD64739CBD78D317A09A45775B2AB9E0383C75D049083BA64019658B4F44573C1B7C0B3A59495606D9162438B1A0A000C3AA5834AD5BBD11893598052E5B543E209215E41B8D49C4107F5658F7685CE53459F21452AC66F0D2398B6971A5A23A056444947220E95454A404C7213686E58A42E346CC0C44759504B2FA07198D742B1BE01A66C2BC64498CBCF6482DAC99DE7DA703EC1441D08C4A701BF84FC956B11411AD610146C59DA6454A8B3B742D91DE3C09A5317A9DADC23ED0A922AF339C090060B869B55FB3AA1C603C40868337687EBB27E37B804EA834D4D0084B21307DB0A64F69B7B49B5B0D237AC428ABDFACC01D1012A04008CFDD336A06B9728A036B6C60430A8CE887C4B006AA7E7C54834E2934F8400D030AA63E61FEE96C22FD49685A7C3DC2287EC2964CAC78B40340200C3AA6C8A67BDD30FB7605F0205CC0CA043A4574235B342FCE51195873F7FD99077CB31229B91D5923B67D391B457198125B7F198BC7CE58FD6B96BC35CC3D0281957B4110A011262B1C4CC0445722CB5E8C102408593CE6445C4584E373AC890187986A49414643CCB2861742466939C347CF021D1C19D36398A82C64D1FBA4C44B56CF384C04A1944449258700A66B1E2C9185A5E8B17772538B33B038B877353DC251952DBB899F04A6C677247D4CF4E4A5D2B22313A33A6D49237E7C04C61B2B0026531583C6CF6332C4C7853B7A8C6A4A5783D1B79FC5725F9C7157600747C0203E313CE91976CE44CC4E0D67DFA830448999357F076F2D41EE6FBA68C53C9AE5C025DF867E3060F3CF164218142D6C2B5B526BCE6789DBCBC5765846565575AB154C545FC3A81AB8FA1353B703B72B011A0405588FC0B8C20D13ACE9A93F6B28EA1C05CC686CB0AE722662A9ED902AE87D48B8A9667CBC0BD75742CEF313F1649C42C760796C979369736CFF05516A72BA36849D2E213AA497A6E7774EB9A896AF0A13387ADE8F594DEB482D9FB9E4C740360B5089CD101E5450C837C8214247243417C05ACB9D319809D33168E707BD1168709F7829B584053B554D95557DC032683D885AB406E205B6F495C23EB673E05651ED9DCB87C4967987B7B78EB349F4AABD35C7242079FB30607ED82A0503173A1B785CC36A2181892114003E2D33D17AA3B0A5CA47974B8F1C93B35107DA3900A87792433F154C643B34519941CC60710F07A71E4AE09A7539E72B9235A07D5DB53C048493B4AC8B9F0A3696892A0164DCE38B7F8E711506A9220A5C6C61AC2EF3942D3984907E59DC703ADBD93B6B509A156CC34B226188778BD191402DC94124CC7981151934F68733D131540056466437CE2B31C1320362AE5247C35CDA6E13E5D9BB89D0C8DCFB16BE8BC355A0C0C54E7ABAC12621ADB1EAF82B3D1E915315A3B7F3C98F6CC117BC3C850258059737171478E3AAA7FE5D7B5CD218CB428153C07AF80667C096C96656C85B8B0BD1F606A7CEA963717A18AE642E9E8A853D938C66651EE31034CAEC7DA6097A3C35BA420022324EC00CF53B53E9EBADC6FEB57C9B5BF5F53DA963F10A9456E56F427F58784728D6C58B3417E8D6F83A50E16130B751D979C1AA9580773A2674830CD525167DF109974FFD07155CF55615E23916E428C12925C\",\n          \"c\": \"36D0BCFFC4727964FF08A0DD933907E5DEEA4776C814FACD5EB4A45CB3C5FF4D0FA77C7A3E3922863E6403DD9D13A7762A72EABE484109399F14D8855CCF28E66A0E33B8055F815B61CEFCDDDA9B434556C3F696997FD5889EFD254D8340F84D845AF95ADEE7907126ADF6CEF4B4F37A9F6F9CCB2877821A0D7DEE947D13DF4BA7F1FD358D430F49B92962EB32A7B39B20666298A82B5EE52CBADD02D4D4E202330A640663D98CC30759DF86D4F5D8542B940A80C87B3E4AD6EAE0419AB6BC6696AEEBDCE1A8550F95947AD04F47A3C9A4F84E8490FBAB2C7137E8FBD20AFBE7D94FC35558C14149AA277F937AB28BFB33B7084C74A4F860D5738DDF8283248F22DB16F752B55900C8FED3B9C56B94A1BB525CE054504DF3FD651894F0F1EAC0B20D052DD0504C6E2DB7BD742CC3527D55BBBBB184E7E311F72A125B230E7D1364D77CE5B5B104BF1DDA76E054BDE897DE4437DEB871E99C5CC16D537BD40462F0756B796477E7B0FE435202CDCE1A3B5F30C0F757BC0263F3E8B6B4A7275CFB1EA24637752034BB32108BB1627DB0461EE9C521630022E00F7B340B69F95917A8AC0C6D7EF6E37D8E4635E77A8C3FF9E42F452EABA5CADCEA4D686C220A252DF9FE3B25F897C163EF40820DFE0A5CB27A1926CF26D78E307FCB67A9019017BC667116A46BFF210FAE126688765ACC48EA0F8D8D13819A3CBD9DBCCFCFDCBB604EB97E51F7D23C20B8221D34B5053DFAFA549C2D374BB2668CAAC16DBF6A9ED52A86DCCB5B59CA41DAC1661C597896A399CB6ED3FBA6A4C1F5DF6A29E3A65CB6C2F0ECECECE778CD1DBAEE4260D3FDFBCA9AAC3E161FE45B88152B8AF6AD0EAAE9D964E9EA362B5643621B997E35781B1232AEDF4B02543B339F074FE630C5D97AA49BACF3A0D3EA57246005B4497A14165C8BF39AD54B430A30D2EDABF024E0245F4C7B732DD90BF4BA1145C4B88CB81E6408E9D8CFB9CCDC4A9BFC4D9240A21091A11543FBD9DC8BE2F5A02E3650FD273EF529BEC9AAF5C2E30284075326B7CDFAB4FF448CFAFDF21AAA5DB1E0B2D3B181BB7BC9F4665557CEB08DFE29E0CE924F7ABB3D226E9A3EE74BF69EB0F59E58A232E778724770591219F2744E8149028CD61F6AD182E8F42462DA51546882D77F44243C698AC7B61B8FEEEF91EDC73C0B96A40DB2616BECA5A5686D8506090BB3F02075400A151B26682C8D81F05E9E87BF59024EE5AF6B6D015F9A81E19817CE5493571CBF8946C5BE67548ED11971877617B6550F372D5219BC07AD00D659196149250A2A32CD1387B46F793FAE968BCF3F1D4F0BEEDDE6E685C975E176DE2B5CCEC08930D0C7D176A43CB43AD728D131C5B5646C41113904B919AF4E8783EC14CBF11B341CB81CEAD8FF4599E2B58FE97E297C79EDFF394891C5D7318DB5684FDC0AA867F677B6147886AF4F28BCD8E69F3DE22A597DE59909FAEB7D7BD991DFEBE3A40EF9A61D0C725619A83F82088A14E2A1703CF700BD0189D39F031FC2E7E01A3BD8E8A4D6790A2262669A\",\n          \"k\": \"44AB396F38942BB69C09B2602629B53B820FCA6D3D1043B24AFB3184AE9B5565\",\n          \"m\": \"D8EF97421196B1A91448B2BA7E2B4D4B035B91DD85AE4E57E8FE3F0B0D524AE7\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 40,\n          \"deferred\": false,\n          \"ek\": \"AE742E0C4761E7731F98A96F57374EB4E321E1D33E3133C031355AFA974E82F50E7EB36084471D94490150188B34A113E300B2CB65BB44E6940124BFE3E21FBB4B01A7640817567949286922A22A8EF654D8465CA39722AE9A4D0054906F52C79428716971ABA0703C2A3C497DF73D0D0C8AF3BC36D12C80AC2701D8C4689E05645B2017B7EC830F8CC3C3419B1B14AA890B2A8B274C5316185B26473456A196D8C836352B437A4EB0E924E9D5776B528DE1BB433B7C703F302EDE284C9B22A6A502A7FD6B17AB5546B52C86DE883C80F74A9A7302E7270B73C521FCC51EB7E02BCC68CB257A1FD6EA63C195610AB8A8AEF2486E18AA081056B5F474FEBCBAA48560325C06DDBCB79EFA10589A62D9D54268062CC8E3693373066E57C876558FF3180C632269E77A96F305175F3A2F9833BDA9C43BAF62500C202D0DBA91B28CC6473703960785C8711604C9CC04A98BD19904189A6D62A159E6A83F25907EDCF457A080C70627C6F5B0612AE84D4317261BE728B786A111B74478044B7CF192DC6490475C9AE1E5C0FF97B812E0094A63AD818C5BE5304AE8981B52867912787114A14088261CF0747732548EE105927DB086088B89DA4773F3C4CCA0E44431D98A8EAB89C1D6C1D26109EA32CE99473E509B9D478614D26B06387B89D64C6785A50DB4C78FEB65786550647EA30C9029884DD34FC9227596B091E91766F8389A82B01623D7721F2069911128C309602F047F44825EF6A12BEDC72880AA3BB91504904276E3E9583228A7896570676B8DA28723EE17553A156AD5884AF0E1066E518D19951F98079F2244009369157E1915B4FC99254C4691D4A64A0AB0129BB235B00552C5B606220E45683341086F2DB3ACC16C8DB563259C6C8E6A360B9CBA54D048335E455FB406415D147122903325B341727C76ECD2C16D33188055534196C071F6B6806B9E057B5597158E036C3CA90174A83B965F8C3CA7562A3A24AE99298A10791D38FA3E5F30B5CE3A8438E6708A156FD51C055EFB5C65759D76B1BEBC231896095F213148AF839B4DEAA03C9628D0C943B0F14770A9A223FA3CF05C7EA983BB64501885A919678001E9219AEBFA71E5D3CB5BE585E7B939194C87C1EBA5BD56A953645D28C21F0E45686C9AC96F820A3AC26F69F705EC920392601E55FC1ED2B9BDC3C6BA00179F9DC687C2C01C29EC05D8BC5A3145C1F936CB8B20CB2015233E01184AE68BB0F96728C27521118466A5CE602CC4419881DEA62381D7870C14209B6A874FBB498472B3420A3A2A4203938863C7610DF73325B5C12EA5D2CEC78610F7237C4453536D7180EE7425C9DA6C43604DC6A074E2B203A0FC6B44679879AC2D94427746041809D84820379D14168BB0E816FD241598195CA1C447C2EA5D5B455A28E66527088E24163C6DE46D36DC1753CAB7451A6ACF09ABBAA1223E7A3B07E801F84071A7A3ADE760C7A2A12F4D12734530538D2179EA204ADD3A40222467649B4245942E80B50EFAF60B83E4591951101D025B05CC1EEFEA996BF1C015A6CDC9325EACBC349592B881CB3C3DA8C096127953F0B9697561DCDA6BA3D566FF3C14BF718ADFA47877A12E390A6CD0544D9524AFA37069C8ABA24F918FDD15986F9B1C6471A5C7A495588F79B71FCDBD7376406E5DC064\",\n          \"dk\": \"B7649A90D87901527052079DCBF86946E8C05A3558D5B35B185BB6224938B6B5A18F490AD2B439FCEA41F1A7A2DAE9B97245469237422B2422CA8B0E36D9A045E296DCD817CBB29CDA6A94445848C9EBAE5314CA9B063D7C666C9D533D6CC1CC4CB3CCEEF55E5F4A9681125142B77C6EC6CF4C94452E9CCFF601276DAC56AE1020CE7A0E9817501856ACE069566C62766827335E4897F9D47D7C777DE56561654386CD579A1535C569EA4172312E7106AFF19836E671399A172AE853656D206C63A2824D46C6DACB6AC3E21681E324728818CC977134531C329A9982686680E542208C3DCE02579051B37667814C439B45A3ADC9F12D597B42D7372B5E863E96FA8EB8A61EE999A2CC22607A057E6E69516BF8628A041E38C378E0A360CAE6BBB3E7846698C5917A2416BC5B81117CBA8905410C959B4A7241BB96974B29AB976BD2069F41960EDA9778D1EAA1BD7A74F6E40611EB68EA33583C707D6F005CA202ADB84C6009354679604B3D78A21DDC1910DA0FE554A872889CFF801AA496B0B5D498DF245965B783517A2B9F495FFA714FF17B28873A3BD6B5313C8AC70967AC62B90762620818EC1EC1E21080F5427A59C08BF7631F91AF9BF17814A238983526221774C8176F932AC90C240BF48996B9E912A7E6A187831EAF2A9B69616447C26BB598CDF1D52542E6C39BB5BCDC25B457241CE9F2B2E9713639916EEAD5B5E85637C8D527629018925A7F59C539F35371AB49CD49098DAD905296F28B2CA29295641633ABBD29513C886A82F0094715380A6B013B16E578FADBC7089C637BE0C6FC85A68275CDA850981DF357CD40C3DBE21B10376ED5C9AEB6FB3049889431D23C3BB6BB175A24A7233C5983C5D1864F1035C805631FDCCCCF301739BD1A82B4A9CF895439A05629024312BED71D4E93863B22B05B5494BA666D4686971513559E5815A0C7371D50B209C185E3326AEB222AE51C74B72B07F3A7A913E338DD3B94F750406D533703663B6DF204A7A1BCF2D608E3F9A72BACAF83B8393E8456A1D25CEAB511BCB7452DF51CC2EB75D5042E6FB26BD8B518034348A795878CC52906A478F57107A0D8548D9442AA964954B60F91EA1EFFD80C8511A26A7907234ABE3440BD60A595E02813F8BC76C9B83D85D7C0D707378D2B4081C7B0F7FB5F37C4BA0C7AB2670800D698134F4538E6D79224D9ADDF82986B91C856FCBE1B70940A103081D09CEDF1B527D94DAEF6427F968742961C6D910F6CBB3CB2734CB710BA1BF2C02838CDCB34482A452290843B12251FE776C5319A7319F47F22C4AF431996A7602E35841B0120151F427A307590E5321735CC8B2885B9B190A0C6466DDE597E41FC4AAC417882FB23E3866D3B325DCF8B0994434CE75A988B4B30EE9162E56809F638300CB647623A708351555AC4705081C628D48DC7FC7EC0F27A822B1650888CAD4CBD9D9B07DE6B82BBE47A966C331C743F46917104736D3ED15277B75250401B7A030CBB7A82935BB7E885C606A851701776E1B5487640444F33994C325D0DC99B2E06021C753F3C6A4B556A5B4A7705F2C0A32F4315E9AB35267C17002131AB608CDB13C02C50A1B152C4FF0C4D80AB8AAB6C4C8BC508AE742E0C4761E7731F98A96F57374EB4E321E1D33E3133C031355AFA974E82F50E7EB36084471D94490150188B34A113E300B2CB65BB44E6940124BFE3E21FBB4B01A7640817567949286922A22A8EF654D8465CA39722AE9A4D0054906F52C79428716971ABA0703C2A3C497DF73D0D0C8AF3BC36D12C80AC2701D8C4689E05645B2017B7EC830F8CC3C3419B1B14AA890B2A8B274C5316185B26473456A196D8C836352B437A4EB0E924E9D5776B528DE1BB433B7C703F302EDE284C9B22A6A502A7FD6B17AB5546B52C86DE883C80F74A9A7302E7270B73C521FCC51EB7E02BCC68CB257A1FD6EA63C195610AB8A8AEF2486E18AA081056B5F474FEBCBAA48560325C06DDBCB79EFA10589A62D9D54268062CC8E3693373066E57C876558FF3180C632269E77A96F305175F3A2F9833BDA9C43BAF62500C202D0DBA91B28CC6473703960785C8711604C9CC04A98BD19904189A6D62A159E6A83F25907EDCF457A080C70627C6F5B0612AE84D4317261BE728B786A111B74478044B7CF192DC6490475C9AE1E5C0FF97B812E0094A63AD818C5BE5304AE8981B52867912787114A14088261CF0747732548EE105927DB086088B89DA4773F3C4CCA0E44431D98A8EAB89C1D6C1D26109EA32CE99473E509B9D478614D26B06387B89D64C6785A50DB4C78FEB65786550647EA30C9029884DD34FC9227596B091E91766F8389A82B01623D7721F2069911128C309602F047F44825EF6A12BEDC72880AA3BB91504904276E3E9583228A7896570676B8DA28723EE17553A156AD5884AF0E1066E518D19951F98079F2244009369157E1915B4FC99254C4691D4A64A0AB0129BB235B00552C5B606220E45683341086F2DB3ACC16C8DB563259C6C8E6A360B9CBA54D048335E455FB406415D147122903325B341727C76ECD2C16D33188055534196C071F6B6806B9E057B5597158E036C3CA90174A83B965F8C3CA7562A3A24AE99298A10791D38FA3E5F30B5CE3A8438E6708A156FD51C055EFB5C65759D76B1BEBC231896095F213148AF839B4DEAA03C9628D0C943B0F14770A9A223FA3CF05C7EA983BB64501885A919678001E9219AEBFA71E5D3CB5BE585E7B939194C87C1EBA5BD56A953645D28C21F0E45686C9AC96F820A3AC26F69F705EC920392601E55FC1ED2B9BDC3C6BA00179F9DC687C2C01C29EC05D8BC5A3145C1F936CB8B20CB2015233E01184AE68BB0F96728C27521118466A5CE602CC4419881DEA62381D7870C14209B6A874FBB498472B3420A3A2A4203938863C7610DF73325B5C12EA5D2CEC78610F7237C4453536D7180EE7425C9DA6C43604DC6A074E2B203A0FC6B44679879AC2D94427746041809D84820379D14168BB0E816FD241598195CA1C447C2EA5D5B455A28E66527088E24163C6DE46D36DC1753CAB7451A6ACF09ABBAA1223E7A3B07E801F84071A7A3ADE760C7A2A12F4D12734530538D2179EA204ADD3A40222467649B4245942E80B50EFAF60B83E4591951101D025B05CC1EEFEA996BF1C015A6CDC9325EACBC349592B881CB3C3DA8C096127953F0B9697561DCDA6BA3D566FF3C14BF718ADFA47877A12E390A6CD0544D9524AFA37069C8ABA24F918FDD15986F9B1C6471A5C7A495588F79B71FCDBD7376406E5DC0648599741BE85FB959084A514EEF8306CAB0D933C51707EEE4782831FEADF8AFAFD6228B0EA7F3512B3757EC1D5057642AA3ED2265E73179113245683986C8FF04\",\n          \"c\": \"5DB0F63E2A2E640FCD26AD49D437B9287E2C3EEDC2E82A25A49AD859C867929BB10185B6457D64B467A1DE9176B5B4C163FAC1C6C0CF0132BB685F4B99366B8CE702BBFC369CBC1D294B9490E47FBD2335EE6641404D6444E7F7EC3C435F5FBA9134DA5D807B2D41ADF37FF44497826FC31E447C6BF66888893F38CFF04C5B565B036E1F9BF255EECC0F0CDBC625010C8CD211E74164394A87569B08469C2C892ECFB90D5D614C1F503182D9F7C168CEE2A1E9553BDC7E0919B9FAA1C2FC652A13F87394B1B43595CC6218BFE6CB8602F864295D06F0A60119723972BEFE7E73A70F8F9B99ED62FF27317DA4BE60F7CE98BD70A0C138E4E4874CC3D254CE3F93DCAFFB2C8F0E200C64DFF97437878E4FD6BE6F8AC8FF6C5982EAAB879955A699AB6071F0276E6999CFD2FB58329718C64928A18C404539CC1DBDA372098F9686C3CB9D945824424B76469D1DAC5D52D0A172447C19781B43BE9067DB2DC18B6C2CB1A5F9F0C0B4234FB605D4C96396DB5C2E38EDAA114B4C6181BE8AFE6DE1FDCF38EAA4C7EC97FD629F17F2E530CA5CF17DA32F14549F7361E38D64A08180E2FB7AEEFB2CACD993BF39020555596BF421C95C6DB96A0FF59CE65B2F624249EB73D19F34D6F9874CFA8AFD71C9BA1455055077BA7E5E2A9D074B8CD2DFBF1767BAFF41AECD9DDC1239CDB6BBA904A017665B57F2CA765A3C08CE233782A2AB13C66F6B6E55B14D71E1AFDF18F093FCDC1155B7471E12FFAEF7F13C01458544DA9157A898B6756620A8219DB5FDBCCBDC3C4FE21AF3902B2210631E63939D1BBA4B0B674F184000C985D4BFA7C7E3F0FE87850DB0504977693AA2B8D12A35B7619117D5DC8FA5AED0E556909D9533A25A5130F779D3DB7C5250F12DB5E1E51D035BED789BC6EC2D135F9BE244E28563E011535A09AE6EDBFAD5311EA083F0431F9C555F07AB21F0741B7DD4508A99CA4E6A6334C21FCDCD25482E987510FE623822F9033441844F99B4A489491F70C4459BDCA67DEF22C76528A8F0D0FD36B7099A7A34460B5EA1CFAF7ED83F0C7773B062A6EF062383703ECA07612D4960C410C86AAB9AEA0764F060C08B4B20941AA2541E8B8521A8E4B03913A48FBDA62BA060A762EF6199649D7FB6869BEF764B6688A7B77DEABB337BE54452947282E9B001D6B54974180B756219087AA81A8DA6C2E708BB894F099A0C5FD5E858867F083E504983E3FA1F0D9580CB926C7C4DEED4BA51D1E5EC0F8A6102880928BDC7A996F2257AAC86F25313B1D51E7EB784B2CB78102AFA94389E30E5FFA336ED49B1F9FC0D557DD7F9A9C9DB959806C9509160FB9EC4758825C5CC1D49D95CF134CFD1565016899A7C102C4E19220F011DF4D363F3B8EAAA419DBFBDC5CA66FAC7DDAE1F144AACD4209BC01569E0BCFA4FD383EC9AD4CF36474E0C2F8A9335A5E6CC595C781F3AA2EEEEDCF593AC27AAD55FDD46D0224CB3B88C3BBBF6CA6F2E7BB4043FF2B0C88D304B962F108C052E8B7CDB4D2573E47074BF146BC9E94783F811\",\n          \"k\": \"E5DCCE174C4B39536E548CC326893C4C4CF649699CEE746476A827CA567D12CF\",\n          \"m\": \"132E7CDAB9CD5199FF0937C266D50BC50BE764AD027DE45C858E3C2F79B7F07A\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 41,\n          \"deferred\": false,\n          \"ek\": \"11A62A8896CFDF943396BB8F58D9CE2C8A4DD07ABB736CA309614224E13CBC2A4EECE6A9E134B65625BF7332A0B978921F4B9B689736C7A90CD77120FB33124BC272EE9B656952980077BA0A2108AA34355C391118E2ADB2A1A6A61498DA2B3E7A6B3477890A6EB74CA66161979B1C44D10ECA94CC26C93B43E42FC68A93B85AAF0F9381DF309291E2160CA19615031576D1448AE4A36DB5A7EFA99542DA5F633C4A75470F9369AFF4D20FD2D96B62348745A46C81053A17FCA3FB927871E88E996C3555B696CDCB7639E13E1148397F4845364A7CCD245DB28577C678B061555A8F97C7DC4C861201C69106CA2F375E94B597BBB09067F4C730606D2AF3B6FDB98C14CA1A339B3D23174AB19C0849836C33720C9A04821F59B929D72421D7583E9919531327B296BD2325B5CB172FB7E620647091A99A053E20907588950E303237FA68F57B1A305233B6E931F22AB46769C4690420A94C3C234B564DEB3052C4ADB3B08A1F455E2B1B8132E6232E42CE33904146AB6FFB693158AC411DB0C49CA3990498BBBEC8BB06812A31E67761FC26AA861E328906D5B873CD2A8C92778260BC84B50703EAC6BB10432BC0115AF9FB60C8F02D90D6A2CFF80A69FC0E23A3068868A7A522CF96876FD1170BC8552E45EB65656740CF2AB106A553CC61112B659E6862346F64359FC6B58FAA70386B955DE918C5B641F9EAA2BE559A5BEBC40E113BB6B63058A80C1FE1664A81873BE506F8A795D9A8B4318342FEAA2A28C7ABAF1C624D5A8DCA352906D037B883AF54295328180EEB06C910A0B7945A14371C885EAC006E24B6A8F95936763B54C89BEB2379DE061AD5D0A65D8398310A5B8844A81F2C8C77E71F974BB0297ABB1C02442385621D607BD3138E063AC36D728ADE7AA6A90BBA32DC2FEC1498F3D68DA3290C17618E35DBC43262B009357CF6F79B04004316E82076961DE4B47F2E5229A9008D4CE902D9F26E5011A4B044AB0FDC668DBB6813479ECA3B0B18E028B1622A9F6B899420899FC7246CA86C00E7C4E1434365067544D02C44C840EF34CDEA6C3063BB64F18B05EE379A18F125514A6A94A95AAD315A55F0ACC567BBD0705B0AF70643F318A7C70EEE059F52C7A67353C89AB55BB091705F3059C93B7EFB305C7716A86F1363E977827C57713622236A365014B87E030BCCB3696F42E6365201CDDB5778951736BD92A4D78BBF3AC4939D3772C0D58F3F895DFD124AEF4B6C454140C002568AC4298062C1C755B99B5113F0AA2DC742077B3B6BAE714742C04789FA3BC12B83A9248092A0452451A7ACA25504EAB21073744EA7826260AB03C84D22066D2199A72FDBC6F818613FC16E45F00B1EDB2F07ACB5E653731F8B701BF0833A258F12B648CCE74A0DA79DD4CC08B2209042F478E897677871538B90BB0C954312574F90A0B189897AC05C5A2E345D91574C6051766DAC73B7B38A3414714C24B175C302CB095F79DCC430438069541D6E9835EAD22789900C2E56ADC602559D595906C8B5AE01A03572183283533C64064290BEEB0A73B9E2137A50C05C1BC635710C3A4525E9C87054CBBE986B49ECC2706C2AC3AC1A474CCA723208CCB6AA1BB7C5851B390DFA3B0E09B6AE60159D231D59DAD26BF5AD617218FD68D6157E4A276122133E14BA4208\",\n          \"dk\": \"8620482F5A28310A3DA5870035D90D8A47ABCC1A9653E4458EB04D20E6CD97A8CEF6652748156C89DC35AF000653FC4BC94165ACB185154400D53C9BBD746C8A3BB010C9A3CC5C305C32A8EA817B5B743747B63B726937644090ABD7B0AFD73AF359007ED3418FA18E4455928599971E95C0F321B2BA193E9BE79FDC00B70096A05455BD201C36F669C1BB830468D135D4972E0B363E6366555630AF095447009590EC93993E9A862B791B8E0555A8E9634D39145598C5F733C9052934ADFC7F491A787DF06DD4C5B7765A2C26D73A4C9A928E60C81A8595883894317B07ACE63342450FAD15659D4491C1387B66D743B9557F21540A3F8B501DD6BC69D7B9BB056ED5B04D21B8065EA95E83272B42485E22FB1EC394C0E0839F255A75403355B8C60F8FE3859BFB8854E418DA0266BB26C92EBA7F3D37821942B367167893F35B8C4C71BE82540E441DB9B252C7CC2753923A5AA15CA8393803B453BDC5435120965B0C96D7FB54276789A157B84A398142BAA351124EEBA2102969C8CF733B0A4008EA2B1FE7CA9D6A1827A6C51C8128CE97CC96C3188019D596A0CC7CE3705DAF489AC823A4813490B84963504026F4537218F1AE1757638318386AFA447191AEC0F14446D093ABCAB1E897405C67A547D0743E145C1015B9EB064FE743761DD717D42C8801C0B79623A622233066573F7A264D172C44884CAEA0C1A7A7CC5E8874B8D68212CE884943822B2E190C98A6033B9004626203705676801414AAAA2C44D6AD81828F821A85793B903B2A10EACA5AAB455297B8C6D6C4816664B97B99B74E194C3A5465FB8A2F1932B5E42500F67C2799328552EB8DBFB71200C9AE2075032A99B2C25863A045AD44324D31378CC1BB8C7B2673F9E0003431235139A6296A03E3C8A7E102A8DC7224E7D1296B4B2D3B284AFB3346E5332C43F01AF48703FDA319A6691364E36B41B8BA2022120F300ABD2A45D0BA630970BB07104E9B8A35274A6674E534A0880D89F13CCA889E705B188ABAC8F3B62A25228DED82291FB4A06D9483CB49442C02B4CE038498A691FFC98A890C4BC34C7FEE52373ED9A72A3725093A123C2B1DC7105C8A1C435693503DA87374644C1AA4A4A2A6C1A42C411BFCB20D038105137529A91114CCC5ECD5622FF5C8B36522EF5106C68885C19C579A68ACDBFA76A6C4AEF031B5C00A0F2A5B243D2A1C97ABA7B0792F3AA57583C9981ED1392E70CEEF210527AB5C7C1261A6D3501B16974E402FE29AAE78588E85339C6DA0152DA9025C95B5F09A2627A76C23651323096D3459C286DB946EBCBE3C8096A18C290874644B654DFF62CFBBBA909C37917D572E50D90048086FD76B346235858A796627FB0A9B1AC79E90615C47309F87A14275A1D17A11BB2B523FBCAB0CD5C0FD8744FF70453D1AB2AA98BAF93697BA411E07E1BB7174669945B6388844D692B324E4382533B0BF454682B155EA408011BBA2373A4FBBB6BF147CB75CFB213D428BBBF6074D38CEEEE78D3F9C2F230A11930308FDE482261C14C5B1B499321798105231C7375BDA983DD03EAFC8682369198802D0510626760A7CE5CC45F38377E93A685D7369310631711230C618CB83A20A92963D11A62A8896CFDF943396BB8F58D9CE2C8A4DD07ABB736CA309614224E13CBC2A4EECE6A9E134B65625BF7332A0B978921F4B9B689736C7A90CD77120FB33124BC272EE9B656952980077BA0A2108AA34355C391118E2ADB2A1A6A61498DA2B3E7A6B3477890A6EB74CA66161979B1C44D10ECA94CC26C93B43E42FC68A93B85AAF0F9381DF309291E2160CA19615031576D1448AE4A36DB5A7EFA99542DA5F633C4A75470F9369AFF4D20FD2D96B62348745A46C81053A17FCA3FB927871E88E996C3555B696CDCB7639E13E1148397F4845364A7CCD245DB28577C678B061555A8F97C7DC4C861201C69106CA2F375E94B597BBB09067F4C730606D2AF3B6FDB98C14CA1A339B3D23174AB19C0849836C33720C9A04821F59B929D72421D7583E9919531327B296BD2325B5CB172FB7E620647091A99A053E20907588950E303237FA68F57B1A305233B6E931F22AB46769C4690420A94C3C234B564DEB3052C4ADB3B08A1F455E2B1B8132E6232E42CE33904146AB6FFB693158AC411DB0C49CA3990498BBBEC8BB06812A31E67761FC26AA861E328906D5B873CD2A8C92778260BC84B50703EAC6BB10432BC0115AF9FB60C8F02D90D6A2CFF80A69FC0E23A3068868A7A522CF96876FD1170BC8552E45EB65656740CF2AB106A553CC61112B659E6862346F64359FC6B58FAA70386B955DE918C5B641F9EAA2BE559A5BEBC40E113BB6B63058A80C1FE1664A81873BE506F8A795D9A8B4318342FEAA2A28C7ABAF1C624D5A8DCA352906D037B883AF54295328180EEB06C910A0B7945A14371C885EAC006E24B6A8F95936763B54C89BEB2379DE061AD5D0A65D8398310A5B8844A81F2C8C77E71F974BB0297ABB1C02442385621D607BD3138E063AC36D728ADE7AA6A90BBA32DC2FEC1498F3D68DA3290C17618E35DBC43262B009357CF6F79B04004316E82076961DE4B47F2E5229A9008D4CE902D9F26E5011A4B044AB0FDC668DBB6813479ECA3B0B18E028B1622A9F6B899420899FC7246CA86C00E7C4E1434365067544D02C44C840EF34CDEA6C3063BB64F18B05EE379A18F125514A6A94A95AAD315A55F0ACC567BBD0705B0AF70643F318A7C70EEE059F52C7A67353C89AB55BB091705F3059C93B7EFB305C7716A86F1363E977827C57713622236A365014B87E030BCCB3696F42E6365201CDDB5778951736BD92A4D78BBF3AC4939D3772C0D58F3F895DFD124AEF4B6C454140C002568AC4298062C1C755B99B5113F0AA2DC742077B3B6BAE714742C04789FA3BC12B83A9248092A0452451A7ACA25504EAB21073744EA7826260AB03C84D22066D2199A72FDBC6F818613FC16E45F00B1EDB2F07ACB5E653731F8B701BF0833A258F12B648CCE74A0DA79DD4CC08B2209042F478E897677871538B90BB0C954312574F90A0B189897AC05C5A2E345D91574C6051766DAC73B7B38A3414714C24B175C302CB095F79DCC430438069541D6E9835EAD22789900C2E56ADC602559D595906C8B5AE01A03572183283533C64064290BEEB0A73B9E2137A50C05C1BC635710C3A4525E9C87054CBBE986B49ECC2706C2AC3AC1A474CCA723208CCB6AA1BB7C5851B390DFA3B0E09B6AE60159D231D59DAD26BF5AD617218FD68D6157E4A276122133E14BA42086B6697FD26E70625FE8F9F9519FD2E06C00167545ABD566773BE01A874722DBDF9212A246D98C21B61EEFDBFD8BABEB04F75EFF4D8BD5EC606AFF11F6C20F33A\",\n          \"c\": \"199604618D182236DCCB6F33D02C92C0B22A122FE24D08AF0E4360D48F2626A6D3FC9CBB712608997F1C3806DBDD2CA5CFA9223B3D9BEC3892B7680E6B6ACA245D024D349B1162610D87B3E80B45496B2C8A930666F52A94665169B3D1242540DB797E4E2CCBA389240BCFBA2FD6E0FD3C7C01D908EA99AEB014362E2B20C2793D58E7D6B7B6DFE4FC06D2962AA2BBC7DCCAAA9FCA60493E90BBA138C7D2F300758A8A04446F277E9681B29FD6F572923B4A1D0D3A8E0E26C952D348607E39F69A4FA6F4AB7D9A4B9715A8CA8926C33442DD72BDAD6E1E72817973C6E1C8D5518558A3727678F89D141E3959F572EE533D9AB12633A65E032CF1F434A73E7FA552FD45D2B43BA5152A3172AD58FDB6E2D005F9D8A836AEC708B28972C231D4BD69E77A18FED0BACA032A5D11FEC1266F532FDAEAF3C2F231D07332B274E07226B4D35B30611FB4B3E2F49343B6DFA74CF312FF82AAEC402A1995F7CC1866DC78AC4B7B05EF8500C5C8AD5285E035CF44A6E7CCBD7B11068425D60A66C6D8F5D24415D5C8146C47835B5FBE4882F5AA33164C8BC348BCC3F57DE43AF5EC5400B37CD32913DECB132379278C1B672B006CC0F09510A4A4D1FC139263E2A163AC39FE625139DD75553A8922005E6BE716CE6E5376DFBED8CB7CB4D5F52C2948C033BE9960AD3B910AC5DA388B14E19355C6A0829E5A804E65AE885062BB8B3527DB4FF50BA41A5CE45263720B4A7877D838D1C29EEBCC7C5CF9F5212088D99DE9AE7BE18C74EC7D677BBAEA6BD25F57D0233FB167C5BA0D20F4D32EF6DBA12C6F7AF794247EF35C32D9E4C989B43935BAC97071D3D966C37EF0028E7487077F71A0380E3F32FDEE8250972CCAE6857483C01BB4CD2B177F4F9504A636C307670AEC32510312BEB2EFFE0639A68218FEA75E0E4B6E654DECAA6F57656017E77217E6FFD095A7522F0DBA295986A3998ACF71BDA96016552DF74E6991060098A20585E99E33F7D6200B6EF22958B2207D029566EC96B02CF2AEED17F66C426C5DF559A98E5966918FD0ACFA54B6E6EE70BF3042100047FD481E5E3B73288BF03877C7B2F3A1D206C76B3426F37362388ADE66C05461441D944D99FC1EC3E67511C1B9B9919117A09680A7CF99D367EAB6E493790942C4394875B86B464D1DC98F5025B9F71D27D146C7AD9EFBFFF979552AF5DD127C2231E4452513C106E8D7EC6DD9CBD4BD706AC1F022DCDCD235CC45EF6804FA2D98D371D713178FDA88C5DD6DF98E1B3573D6092DE3C022E49D8E51EF237947AEEC5EEAB775107D714E3C4DB7DC2E408EA0F3CA80AFCA83288CD1E147930FCC727C8E944A1E013919C92109E9A764ABA96A090D31C83C93F8735ACD66A087193CE7664757D9B8868F03F7DBC3FC44C1049826B4876FA2FE06B16091E2E78335FA55A839E719FA5BBDC46B659C3BCFD91122F775DF9A9E8D0C43FF9CC2BDAB401B64E5CC35FB17681512C03C14C3B1EDB2A84D8998829A12E1C7FA590B8CE15CC8AFB37253FF5BB78E0B0EDA76AA\",\n          \"k\": \"66D5307AE26DCE8CFFFBBA9BC0B2C66E38B6E77537AE525B3E9A18BADBD72FE1\",\n          \"m\": \"E15BD4603F0EB64E32B3F1D1FA8EF6CC25D673A1D0BB659CEFBA2C153724C1E1\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 42,\n          \"deferred\": false,\n          \"ek\": \"E65223F4AB1C53267449A616AA32393E441F346887329AC4B9D006F61A25BCC3779C740EED0BBDEF0A371B95171C70C60F2387265469A8E7C612353FC51968AA43C04D1685EB27364E26065EDA53D2879D36552E51EA01BD818DD57319DFB127F2A578CD48BB6080C5AFE13ABA7808B5894893BB414A532C5223B91EB1CD2392333190054DFB4722F5B548E570FA19C7C485790AC4913892249E63BB324C620D835E5B8C922924791B08937F139CCBEB2D5C9A72F344888BA37B8CD7B06349A310BB71B06A3EACC1236AB03A5FF9B923FC5FADE54C5A1A2E803424CF9C42E5043305127F3EFC956D97946E6BB02EB913D6D21695A0A88708B6CC5611FEC61932774E462C18F4726FE992A301B74DE2F56E9F791E90992C3C109860474FBD6C5EB81CBFEC4A0C7F154E5C2375A2A379A83B4DA4C6796102B20C553F015AC5AC49785138B2460925BA3B156EA337D31214EB942EAA27062EC07328080927DCCA3CFAC51A243645D5055EE0C644428C4715032DB2757AF2AE25D727C78B08D7B59D8841B9D26139C3717FFED92E09FC5286BC1E1D290366E183FD2B5562859FC9C321B73132635A532DC3541096657225082CB37DDED92C8C480B5CA68F93AB53168B1FF2F08D8265AA8035753C96897DD06793A145EFDBBBC4518E0C87AA90A895E6955E5B62929999143CB16B09E26433A1C0D761051AC3776EB126A8C890940632B4C50DA7FC181439A11B483554E208D6FAA627B11DC6BB5073DA156D2A4A08832D2590873672066B9A35C65515B4CB5602753B26C9C865716A6BC3C059272F0DF54DB6395F59012ACF2A783BBB03CBBC3A95F65E04A0BA76149A8DC4A305901418324885D44780F65E17E4C6AAD08355A59C21558B0D206C73F59B5FE49F8C5B86A6747DD9E8CB9D560B22B51B5A6AA82277B676F04399570402818A97881EAE8A245E1B75FA1AC50BB0A952E319E523A292316DE3E5C4AA271265B79D37795AA3596CAAB1072F3775D06502880359D73C5EE0C782ECD04ABCB1255B8A5051CA51EC6C17EC955BB6E715659B4CCC4C74484313C4F5149A288178C49F99586AF54710BA549C09CAC0CDC76D15183C8748146A07C72049108E86C0447B371B844C3715A914E3853F4184337381320A02F94BA1D9EBBDB0033D377254E4EA51AF455A775CB8E85336A761864239426A449FFCB967B82461F6F5A21D7A36DB8B54A5AA3AEB2297C30463064C8AFA12908D4B2D1E5B0F1A23B1D55A5E96886B96C39E10C6A68008B0527703EA2799F42B5098C4178F47490983793DF4CE2B0A2D2B56719256206493C395C54292E516D4F8A5C906736C89810BE0846B8BA654C86A355B56736B9144853286D65E61413F7DD26D1D2ACF6728BF6B028F912602CD132E3D6603DCBAB4CF65B0E961A8B597A0810B9F78A14F0DFAA108811CF22C394F21BFC56155A67B85722C755351576B0AB614B7C9C2278DBAE211E33839B55383C5744E6270C92D65BA82590AC613BF1493768E7ACE60A14644C9A02FB30BB1A6B42A6B69A142BF670C48EE03077BA7A2964020D528C51CB17B0245AF11C4A8EB0785ACC738E4F0CF0FFB838748314CA51C81331DE2596345B44F44BC516EF123CD8997E1FBC93AD95C0A9CB71D46C5535A99B75122E5E710DA961BD873E66DDD\",\n          \"dk\": \"F755A8CDD185E9791ABB61B4028A435B336475DC9D3B20B5FB7C703BE6096C23C2AB95BB71127264E29EF9349001C70D26548797D2C3B6C23D841C1063513738F398C281786C908AD623B22CE690838579A7A08ECB5419366A65CAB11B7B598A38D1A878535819C21E08E1B25EAA50D5740C80D90877B921B522831ABC6EA9F7C5753292CDB21D47402F0E95179D0C3247E16C66801F9B057EEDE48070F456FE58906ECA7EA91ACD9B537AABE8A105B5B0AAE634A1E59E1A456B49215E9C792A0453ABACE0539D14C661ABA1033B7097945BF7D0AA35E9B2E98C9C48232C041BBB32A928AF58613898010DC0892591781343369EA7CC68D93ED41808B53AA1E7C69370281653109BF900A783A231E0E78FB9ECC75C5262D99A5401D974B6069E6ACB7DCDFA2B9E61A7D87117576C1076A497E096382775C47B2C20AEAC91FAFB5709670277B20E88D8B8174A44B0B565D669B108A4B61A22CC53262A79948A3779C4DEF87E8AE7222EA1061C76B052B3B3A07BAB2BC52104D7B093481B6E7A9AEF4530502126E718153F259C6D9142EA7ABD97D9037799368349CFCB4C6AFB541201856B6F8CBC4A6219FC963BA4951AA266402749C4F066B5A1703CAE32831901B6E51215F7F0A5370A6D1691284EC80126710224D42B7671A207C06822EC5420C186D253339D9A058DC552B6F8C38230015605883E777158413F915C3284E6C8203B21F502830B51193B39230A963BA38A3C2F0B122559847972923B67AAABE0A4450BBCE87A1F180A9EFDD8A63CD3ABA8274BED24C4E464A9AB1B893F9C08B6D4CF2F93107CB1148C5719DC81358C07C14D41883439C7D0E5C757815140CC79E1AB252281A4D190590AB36C0945C03EA3369CA244E3ACAAAE9C4BE8A77C9458A28774B3C1E794B459CA0EFA190FD681DB8BAC0A771D40651516073D91DCB763CC2761CBA360EA5B8BA402A2110E3E92522DC9A9C3737FC7D7012C33B928184AE22BA23B5854953648D4E9AF49DC3456EB3A38569F27915ED388AC17D1C71193CBCB4AAB74726D4A324294A47ED72C86D84064EF707A4AD7BE50948D82B671C49A19AA338948947993F6A819A669D9C70F910B6D86325BF3046E6FE3913745ACAE5A0EB1A62B203CC46774CF210B2B19C9C63E1A0E8EA9428FC20E2ADA4BF87A871F7C12125AAA8CF38550AA09EF4B82CDDA83121363345327C4FA9FB8C61877216441610F3368752B16121AD7746B405303E83E495CB8C19C3E32F039240392CA8C4E9EA1BAE0136463D032A3139231E61C9947C987980124B56D19C478A060C3A40AC9AFC311BD247818B499AA3811A2584303A58DD5F82355D963F3C27EA57556A5CC5A369619AA7267CCC49C6138ACAF63BE55E81EAB119C923B1163795ED4901F51F1C163FAC7A3F8C080C40E8F0895CD774B0654B137CB72E244B0B7FB58C8F133A9974E8AB74226F7BE847B5205E80E479633EA574B16005B47512E28F3AEAFB0B0775A1A30664888C45913301832F6C80737964EE58D79103F3BD12B1B704F6E8A770B012A0AAA003990BBA4E092A666C6FB1BABF941B94A6089C775479790A996C37B5E44923384874FC103690763B892702E6832CC3A3F69157CE65223F4AB1C53267449A616AA32393E441F346887329AC4B9D006F61A25BCC3779C740EED0BBDEF0A371B95171C70C60F2387265469A8E7C612353FC51968AA43C04D1685EB27364E26065EDA53D2879D36552E51EA01BD818DD57319DFB127F2A578CD48BB6080C5AFE13ABA7808B5894893BB414A532C5223B91EB1CD2392333190054DFB4722F5B548E570FA19C7C485790AC4913892249E63BB324C620D835E5B8C922924791B08937F139CCBEB2D5C9A72F344888BA37B8CD7B06349A310BB71B06A3EACC1236AB03A5FF9B923FC5FADE54C5A1A2E803424CF9C42E5043305127F3EFC956D97946E6BB02EB913D6D21695A0A88708B6CC5611FEC61932774E462C18F4726FE992A301B74DE2F56E9F791E90992C3C109860474FBD6C5EB81CBFEC4A0C7F154E5C2375A2A379A83B4DA4C6796102B20C553F015AC5AC49785138B2460925BA3B156EA337D31214EB942EAA27062EC07328080927DCCA3CFAC51A243645D5055EE0C644428C4715032DB2757AF2AE25D727C78B08D7B59D8841B9D26139C3717FFED92E09FC5286BC1E1D290366E183FD2B5562859FC9C321B73132635A532DC3541096657225082CB37DDED92C8C480B5CA68F93AB53168B1FF2F08D8265AA8035753C96897DD06793A145EFDBBBC4518E0C87AA90A895E6955E5B62929999143CB16B09E26433A1C0D761051AC3776EB126A8C890940632B4C50DA7FC181439A11B483554E208D6FAA627B11DC6BB5073DA156D2A4A08832D2590873672066B9A35C65515B4CB5602753B26C9C865716A6BC3C059272F0DF54DB6395F59012ACF2A783BBB03CBBC3A95F65E04A0BA76149A8DC4A305901418324885D44780F65E17E4C6AAD08355A59C21558B0D206C73F59B5FE49F8C5B86A6747DD9E8CB9D560B22B51B5A6AA82277B676F04399570402818A97881EAE8A245E1B75FA1AC50BB0A952E319E523A292316DE3E5C4AA271265B79D37795AA3596CAAB1072F3775D06502880359D73C5EE0C782ECD04ABCB1255B8A5051CA51EC6C17EC955BB6E715659B4CCC4C74484313C4F5149A288178C49F99586AF54710BA549C09CAC0CDC76D15183C8748146A07C72049108E86C0447B371B844C3715A914E3853F4184337381320A02F94BA1D9EBBDB0033D377254E4EA51AF455A775CB8E85336A761864239426A449FFCB967B82461F6F5A21D7A36DB8B54A5AA3AEB2297C30463064C8AFA12908D4B2D1E5B0F1A23B1D55A5E96886B96C39E10C6A68008B0527703EA2799F42B5098C4178F47490983793DF4CE2B0A2D2B56719256206493C395C54292E516D4F8A5C906736C89810BE0846B8BA654C86A355B56736B9144853286D65E61413F7DD26D1D2ACF6728BF6B028F912602CD132E3D6603DCBAB4CF65B0E961A8B597A0810B9F78A14F0DFAA108811CF22C394F21BFC56155A67B85722C755351576B0AB614B7C9C2278DBAE211E33839B55383C5744E6270C92D65BA82590AC613BF1493768E7ACE60A14644C9A02FB30BB1A6B42A6B69A142BF670C48EE03077BA7A2964020D528C51CB17B0245AF11C4A8EB0785ACC738E4F0CF0FFB838748314CA51C81331DE2596345B44F44BC516EF123CD8997E1FBC93AD95C0A9CB71D46C5535A99B75122E5E710DA961BD873E66DDDBB5310F78E18E35EBA80A0D5382866560D6E502D58908A97DB5FED7E935FD7178E82546B2BD2675908B124B41D52CB487BD98DF8D7BFF3AC859F4C685F91001E\",\n          \"c\": \"50F0E77866A24C305F778B6D08E9C33CB04C31B98D93A1637D5B2E9BB2443337333E91626DEC0C1C59EF1DBD000BDC394FDB2B6B471885CE4A2777A3E0094122A6DA08400897A635CBE0AF1D56654F4D56CCA59BE3B36B20666036DB3C02BD6E6E605ABC070525756A9CD08767335072D99A067D816DD3438E0F4EA4BA1150A7806E41BBC7521C70B66FD46496354CB47500C28695034480CE91E1D63937EFD2FA4F92FE0B95CB65EA70F610B1B4C9519DFC8C175410BAB2E337F07B3FEF8A8CFBFD45B2827A771EE5D6C2E6A3DED56D21E15563490F763B5FA668F5F4E982823A762AD38B78B573B72C9624798FD092A11EE9A010A31A60F8EBD647A5B11E437FF4502863F78EB2104A270692A541E15AB5731F13D11C20391716D60A912DE796D83387C152E9C171F86CA1EA439B369DFED63C68FBCC103F72E3A9D4C01558FEAC56C2DA7BD797A48257FD342870523C56102CB413069C833BF51AAA31403BE6C2CDB64B2C7A7F6CC9F848139320B974C165717D6A0A891CBDB27EACF96F23FB67C1B218360CB68B158981D32FAB2C12AB14890B0230673A29678EDCBCFC6D2857D2F78EA375962F459213F377E7CAE4DD79393D3C2ECC44FB64091D46F30926FF7E8C5D07E70B1407636300382633E2093FF708F541544FDC5F642D92B78CA167D2A73A871AB4C4D611AF2804B3BAD14CD64AD5BFE9C1DC4E142192A90ABCE2C5A8F6BA6D7D30A19E5AD8F9FAF34AB516A02511EA8D88B1A96DE9F6DA9616550753AFC151E7064E4024C661667807728CF85350B32DDAF7606E4BD491FA61FE05070311DEECE16F30F796E0B6BA179A4D7C8B3756BFF942AD2E412299C911EAECC18A3AC3C68EC76D82195615DBAB4C492AD1E2C7F210923B2E54056E557F78422A90FD647520B32626D017BC661E28CD7FF81492EDC2AB88D10F26B576BC6C8F7A79C5DB1279204859A3C82C06A391214F6331973A0332B27A339540D00AE047FE2C4296352190C2DA7C3B2849AB1A36E7EC74FD641A46D2EC61B3C0D394FB3218C6A0C7FC8B4ECB47E026929E15B0D1CA2513C17564E48543793466D72EC2F5355D43DAC60EC6E55FDCCB27A403148386DBF7BAEEE056AF976B1B4D06111701A3AFDF289A0E3969942CEC96E33C095DAE1334DDA76C54FB67A36735C96AE5DD53D379F5B05A51684D997593B7B25B2EEA0001834A9737AD050F6A7CE679D27D852C0DBF0A6644185FCBE7BE97200C586002206341CE063B042CFF9289CAF378EF0A4F93CD6B0736E8A0EA91A1D4FBB6FF30B255E9B604BB8A5F06D2C7C0607876568465352E76C5A31A44B54B8BBB7F0B016D7ADD77B23C443D3C8B1A7E67C05C80A77E2D9BD0662A37A4EC501482FCE4CEA342BE00321E2968107D0B265F168E6560C427B3ACEF9C14478A80DE557AC0A41025E32D8741D638CC8F76E35AF1AA32728E5C5592122973D1947538B8793420FE6A1FB6877ADFE5677F677761F5071AF5D6A70D706AFC275B7E551623E97EF793B814239DC1E3C53D23C3CE\",\n          \"k\": \"BE4A7B739BBBFEA62A02A555571465EDCFDECEEC83846760A0D39944F99266E0\",\n          \"m\": \"D176C0836015362D1DEFFC1901127B5C41C14AA518BFEE6C62F2EAEA1F226AB5\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 43,\n          \"deferred\": false,\n          \"ek\": \"6453B4019A8E77C7607ACC6DF337097A9A10EA4836DBFC7C796A4E73A236069C2005C2BA2D5843AE7A008A337876182C44B92E66EB5146826B246CC7C3F44D40B0C88A98C11B873A29D7A4C7336512E00413EB519AA39025AC463C7CC289270A680B88A7FB9CFF592FD65626E831A019EA9E4C9BB57BD34B8D28B8855616FCF15FD6138947CA36D863510E6B3D4B22B8F812249913143F7CADBF15AF991C7E36C1A610879D9240398A1A1224837F904C65F0C1527BCCCE8459B594739F4BC243D2360CAC320AFEB5B2F5A310C90B42D3EC369F64C0FD2118D0359DBC3141F602CA8D7507EF830F1E47682068ABF22593563A071653ACAFF19506B5630F2B6401B78953B152C7A78D33216695B7778F9467FA239E4ED089A1E6BCDB682C4EC1B7CF407B37B0882777B12436063AC0C47CB7A58BB7CF34C5CDEC291BF1EB851DDCBE42535E0639580B191CDBA9AB22F0243D1334048964968941E7140C44A1658FE20C8AA023D2CB133D5B06FFD99FFD4751EBEA170F9C30BAD8CA39C64C7FF357F0FA2585BB13645682DAE24518517545175E38D5276FC442FB2143FFD2A7B61903CD72583FC64DDFA654081A4533457D36A366BA538B6772C5FF11674F0A702340C542357B80B2C206BBAF2CF6406F339436A7A1050792AA674C2E9C9B7986404C94CEEFC343E0781155D8B737978BFAE3C8CA64723D0656E9B89D23568AC6658F0732C332096A217CC6FA3A09FEB16552F5C982CA6B540C3AC96C8D0AD576820CC46DD60EC82BA0CFC14073424902578AFEBB44C321A4A6B7AF9709B1D9F05EC9BB6B928C936B670543E1B05E649FA7C2B3B9FB441C976FDCEB889FF63019F26EB2326F36F82231A68F230C9BF384710E11CCA858970A33445FF05CC4399DC28B285857168DD388F0B95B7E3B1699830950022FBA81984AA1B1540959EC1469DC696D7BB9A3C42153540B24A350CFCCBC33E5D9136824500319ADEE626BEE22A53FF8CCE2FB195D38257DDB8C305949D720342EB58AF72236F2275F98D335DE35A0C0C66BA752BEAAC32D113AA43DA092C4888D1750A59BEC4EB14A94A84C9947660891457B52F1BC3EF33C951A1DE64ACAEEC99F7795B8C94A0320F40D0ED92EB7FA346E042453995F2394B714A372E6B678C0BB4E04670B347C156794672FCA87966B26F0EBBD2B8304C9B6560459019ED95539C7C0C20999971521F8F7537B13937AEAB1E23736186C672AF0B084D4C5C3E1887BDC0BB281B7DB924BFDD9034C6456C427B9643C89FEBA3440947DD6B0B4BDD54C38215371E1A74E543A6073CB46118DE3B2BD1154AC536652BD2666F5F710C03CB6ACE948FB61692FA3A3E2EC4E3AD971884412640342BBB4C09D1C11856825D79A8E5249243062033E379DD1EA6526676A2492520BC55A4533CEC47C54CDAC7483500C0751BA84F0CBB020CFAD07B6F4E893CAE78DA0C7082F7A9C78D2BAA064C33D20644431BEDCB9B493D05C13104B88870361433490927B015261B63B1ABF2B59777359F74BC2597238DE6729669383E8188C0DFA7235D935657B936101BA4DA9CB90F1AED0F06C03700574B99159FC3E59EB3258A9BD5F47439B45A30163309A6A29CDBBC017840C5655E3E8FAFDBCA14293B91C07EEAB7E6C066A6B8BA7EC5FAEC0350B9C887B18\",\n          \"dk\": \"A996A675909C3F1B1B676B54D0F29769F24B86FA482FD3127086C82E336C9D606B7CDA5F5002C6E27AC69DC26DA9D90BF31561C798CFB9B78B37F788CAE520116A4FAA4826765A41A16C650A1C33CFA91311682ACF0144825370D76657E7B18B87233A0B1393BA92022AA22D28D04B4A302380D780854519C014862545467626B8F7C0228E19960C528D4A511259C7B8C92A5C4E7060A76B8182A0A0ED5B6AF6F920FED0B1DB4C76A4149910B755D69A43217B942D796B9AC549ED9A464799C25AC0B1EE229F14FB485A795A2E072684CC33C8F20B49862157351A0BCB37932B7C4708374B7481C2A5C19DC752164235823935B96A9AABDBC4AE88C4D87131D3131467A8C969671503CA85B65B229491A33B4789C41722D9F10F97F335C2BB86B7945E0A489AA7171C13817D04C4534D324ABCF0CC78957A82214DAC93667E92CAC094003C4440C560CCA698A2287BB0A19CB0352C951D238D13BB89F9AC3AB184A2664423281681E8813D6B433A8CA7BE7B3C99368A1DADBB984FF298FBBB2645C11D6576131DDA12586C6DE2539641FA21D4870D471C3702A4C3376355C1EB42A90B5676756905C2173BCA5775192069382B3AA4CBEF7C95CAB35E44C49981D6ADC8C68E9D41109F5C18BBD80D13E80B661371B0CA6B48D19886E610FEB6AB4CB500EE0C12A7C770EA6B16F80C4D2EC016AE26B02A2520B5692437C80A857A09A6659218D147923375203AB1F2799462F28427C01900B08A8307B5F3BCBF833104A04C7B4444BD069BAEFAC4A271537702514703FC2388777CB7735AAB3684E03741F1B45261CB53ABC800E9F8AA77B62C7A805B9FD8895B568FA1136CE80A6E47D470B26A0ADD4A69BB33ABC8A6A49D61525866256D2656E46827BE895162A01CADBACBEE418695089BD9F1940408483FE87C31A68FEB4C35AA561827BB49E97776C64036379B16E719AD10F02BEC795760BC167FEC3F7B571D88075B161862B6C556B8C87B3636398D32426B1A11350203B93112531BC8AA0ABD23454C9D863E1E150990918A96B2B3A3A1416B0A8C2968AE5A110DCA445F515A15373026138865F5C64F625151FC546F9DC1B8A2F17C2E719BC97659CD255D34A3A6E28B8BAAB1C47EFBA49CB4B1220142EE843E24402462E785B01B664854BC54D6205312B6B70117C5C1C32D91C7B8032CD46A83B327609AD91519C9AA203A0A0B9C36B3671AF62890F301CB6BD4A3D39175E79A572253331144144BB2133F924A3E88142FE04BFBFC15EC84BF968A2E9A2459B109144E0445097349EBF3AD8E0C87641C74FA71699B687AB505B43CF0A668C7B5C4BB1A4D676081D8C1EE7B8A881791B9D16F92116C5D786B1678AFD49A761D9AAA33754E919972DAF27CEBE3CEFD3C90072119A78387FB056D98D41CEA45648CB5B87D217DE730703CCA26C7726F14CB9FCA3760646A627B255EF9AC156B6C1721445E20417CEFB3085394A3D94AAFD1273E4B75A5C340274160A52F26C376A018E0D880A578A24A8961D9D443A5908A32859F2A9794F80C62DDB001C411965802C6A0F10D572171FA32A44BD56ECAE78B7FD34492E28E61C47C8CC66686B401EC082013CA8CBA84545596A9D781786453B4019A8E77C7607ACC6DF337097A9A10EA4836DBFC7C796A4E73A236069C2005C2BA2D5843AE7A008A337876182C44B92E66EB5146826B246CC7C3F44D40B0C88A98C11B873A29D7A4C7336512E00413EB519AA39025AC463C7CC289270A680B88A7FB9CFF592FD65626E831A019EA9E4C9BB57BD34B8D28B8855616FCF15FD6138947CA36D863510E6B3D4B22B8F812249913143F7CADBF15AF991C7E36C1A610879D9240398A1A1224837F904C65F0C1527BCCCE8459B594739F4BC243D2360CAC320AFEB5B2F5A310C90B42D3EC369F64C0FD2118D0359DBC3141F602CA8D7507EF830F1E47682068ABF22593563A071653ACAFF19506B5630F2B6401B78953B152C7A78D33216695B7778F9467FA239E4ED089A1E6BCDB682C4EC1B7CF407B37B0882777B12436063AC0C47CB7A58BB7CF34C5CDEC291BF1EB851DDCBE42535E0639580B191CDBA9AB22F0243D1334048964968941E7140C44A1658FE20C8AA023D2CB133D5B06FFD99FFD4751EBEA170F9C30BAD8CA39C64C7FF357F0FA2585BB13645682DAE24518517545175E38D5276FC442FB2143FFD2A7B61903CD72583FC64DDFA654081A4533457D36A366BA538B6772C5FF11674F0A702340C542357B80B2C206BBAF2CF6406F339436A7A1050792AA674C2E9C9B7986404C94CEEFC343E0781155D8B737978BFAE3C8CA64723D0656E9B89D23568AC6658F0732C332096A217CC6FA3A09FEB16552F5C982CA6B540C3AC96C8D0AD576820CC46DD60EC82BA0CFC14073424902578AFEBB44C321A4A6B7AF9709B1D9F05EC9BB6B928C936B670543E1B05E649FA7C2B3B9FB441C976FDCEB889FF63019F26EB2326F36F82231A68F230C9BF384710E11CCA858970A33445FF05CC4399DC28B285857168DD388F0B95B7E3B1699830950022FBA81984AA1B1540959EC1469DC696D7BB9A3C42153540B24A350CFCCBC33E5D9136824500319ADEE626BEE22A53FF8CCE2FB195D38257DDB8C305949D720342EB58AF72236F2275F98D335DE35A0C0C66BA752BEAAC32D113AA43DA092C4888D1750A59BEC4EB14A94A84C9947660891457B52F1BC3EF33C951A1DE64ACAEEC99F7795B8C94A0320F40D0ED92EB7FA346E042453995F2394B714A372E6B678C0BB4E04670B347C156794672FCA87966B26F0EBBD2B8304C9B6560459019ED95539C7C0C20999971521F8F7537B13937AEAB1E23736186C672AF0B084D4C5C3E1887BDC0BB281B7DB924BFDD9034C6456C427B9643C89FEBA3440947DD6B0B4BDD54C38215371E1A74E543A6073CB46118DE3B2BD1154AC536652BD2666F5F710C03CB6ACE948FB61692FA3A3E2EC4E3AD971884412640342BBB4C09D1C11856825D79A8E5249243062033E379DD1EA6526676A2492520BC55A4533CEC47C54CDAC7483500C0751BA84F0CBB020CFAD07B6F4E893CAE78DA0C7082F7A9C78D2BAA064C33D20644431BEDCB9B493D05C13104B88870361433490927B015261B63B1ABF2B59777359F74BC2597238DE6729669383E8188C0DFA7235D935657B936101BA4DA9CB90F1AED0F06C03700574B99159FC3E59EB3258A9BD5F47439B45A30163309A6A29CDBBC017840C5655E3E8FAFDBCA14293B91C07EEAB7E6C066A6B8BA7EC5FAEC0350B9C887B1835EF87CAD46B39215CEC187D9B96A895FC9A8EC843B7CD3531BBCDF1BD64A22D5001876DCE843B5761DA9110759CDD04CE17B8936541525FE830CA69E53E1655\",\n          \"c\": \"ABF144FD5F6B1CC8E11E1405A8AEC1039BE0D84CB1E4EE622E50F721F9C76C74AF363507C354BFAE546030A8BF746315F1B223F2092AB783CFA62CF86C9E5F504BD489D8C6B009D1EE77B082C5112CE2675990F401573FA6643551C6189B8545363835404066736BE712553FC8408BC370DB3603B086198B338C55368F0342B617A6314C4147C27D75925591C30BB2BD39BC34F0006C056BEDF6D072A4F3719C5FCE7AFA82CAD9A21EB92A4E6C833318671EC4D262E4DC321164DDDFAAFF6EA91514F7942888C0625DC03D16878DD5F2A4ECBD0B4C508849D20E98D6C84D0D2BD7A7F1E70687EE04F8EA275D8BE04926F3213055107135F726DB314F6BA9E4FFAA23CC074BD50A5D192C25F4354082A7B25C128F3FCD87FC6BD842BCF0E8A05AD2DE51942978F5B2B322D5205095570FB1855B7AF3AD9B5A6FD6CE7D418921EF31E15928E733D10F96553401ACF750001D7814B4233F7A512F2A7F768C189ECCB5ABD0206F7463C812E0186B60D9C06DD93DBC9DFF13B3B39AB3610C03184A4FB2AFDFA964BCD95C1DC8692A01EDF4C22F0D8840D997D13FE228E393F47DC5591E68F64DF855127B5951E1F8259CF40D9EB5281781B5E171B908CFBEF4FCE13BCC41975890182AB74E9739D9CB4CEB30D600EE3FA70D858D60CF89D795E583BADFD5EE032653C731C52A67F401B2927C7492926651044608727570FAA99F618678000A61EBFCEFF973E40A0B56B961CF24959EF4E324811A4F1EC878E41B386F27A13BD322C012756B0E29ACAC140641B77B1986BB7C41D3DEA0E38A768475C3D6C6EA3A6851CE4578893592A5EA633C7C6E6144BE6DCE7B47EA653B8176C84E2754F3AC6AEF843A5E759626D3862A13B85C222A6E5E0836FA4ABE18DC57267BC77F2187258CB81D24E62CF972F13D74C56F970E4BB29D0D0F39E35E61D6DDBCDA0D90549AB5D87C78F8B7F7543CC95B2AECB0CEBA735A8EBDB8F78BCBAA41A27A9AF77C2F9EEACE5176C12B340ACCF63639538A6308139C1D9D4A68DCFDA14F2AE7239EA6D0454EA8A1684455DF3B57C4581F9E5B243F4A3A7B74537F8F8815B30758DA124044D9EECED7BD9AEB699CEB3D0C708D76F4176AB94DC47B6BE84AB40E677C937146D5EF26F854BC96C0050C2C7B1A99DEA2F8873DDEB668F2E4129C1159D8C4312E6D4FA1AF9E5739B04416B7E54AF21EA1864037B7D74094F970E9B7914066F7ED2220BB8CE88594050A8D20EFAAE7993E3CD4C4206542047A0B8D00DA347D8F97066BF9EBC2D6F77A68E91524809CA14E5B9559098A25F2BE13CF64F97EE4B9277C798EF544C07114E1B4ACEA057DB041D8006561E97AA4C60BB5E3E84974FD51A8B12254950508272883A06394EBD680732F9A0AC66DEFF31BA045174D2FA5B15D8D9E326CDA635799F38A5AE0D13EC12145B3EAD39FA1E8F6F97AAF5F437F28936362A235D00A7C85A4AA6BBD73A998347F06F83F1EA9F71F318B2EA96BBDA89B9149A5765C41D018BBF2190B3C6C526933BC828EBE4FF35B\",\n          \"k\": \"CC54EFA4BA6B3C0B651258EFA6C6850B1B31FB159C282D6F354DC18C8749ACD7\",\n          \"m\": \"4E302EB2BB5392782E7820868DEDB61F5A6AE558CA307A01ECDE4970E43EB448\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 44,\n          \"deferred\": false,\n          \"ek\": \"409471E1024944D5203F82C62D1CA67E96567F3A525F6AA22683C4D537BA87DCA66864C88F292630484CE6C2592BE194A59B9642B0C0C1C4771DF87710F11FC8898573BB874965B774C3BCF30920312BC3C1945876AC6CC1221C9019677087951EF43031373BE34492CC5638D530A4D01C86F9B70296162950D92B5D97724BC1B7E9056C78D16290F3BEA459AC21395FD8A60EA8551B5D935510557CC36CC5C42605A4459CD7CC603408C3AB2380F7F00F44EB0DE7457E6F40B8DA2042F34C18554606E8A274FC8B6842F299C1E80068F11870907D8D44269BB7789AF0C8465B72CE64B5431A885A00B0C200102C811E8ADB7893502277FB87213AA89051852E896B3A170A33CA8F44A7367756944760B1D3C37318AB76EACBB47BA15DBD83CF8151BD2E3672934702C3A21886B27BC3506DFA8CCDB77C818734A18230868092186431221E3020157374ADD138C8F3C69D7709BF60C5A06BA3E410AF156B2175020EBD7603AE10C76DB7C2CE066A98A0AD4131781CC30311B38A78CB809BA48136009C36144B47AA2052E59F16E0216561BB42062817C67D81C96223C37DA9332B261152ED90350BB2BBEDBC438641CC082C8E4A4181628253734CB262061E40143BA11BB5AB75A89270636B330E922C5298068C85C1352FA5A133C3900A762F57B831A907AC37A6C9CBA9C915290E10A447A8B454A0F735DDB8852D79B0FCEB207C25568F44CE9568305A9362ED584D77123A3716A667CA2952CACFD7382303A69A817B17D895441D0A9AE1F71ECE1277C0432A7FA2A63C81A545910B4B212F1D983DC634313BC4B3B1B39190438D47B16747FAB48DE4777911B8D3FB65FAC68CA096919267783B0497868A9F95E89A1A3B2861A3BC89CB77F6976463CC7B1CA6CB8D447AA65463174A384217CFD41B4B80B7AA4072B10BCCBFA73C53E45684C542B031288C1192AFB1077B4E2B560E248169C6B741904304B06D23C86C9434AD64C67D92FB872D28BF9F745F2CF210D6D0C9C72176A3242447E06925718FE6530E2B5018833831819C23D3402BF9DC22FA7B1B754C6CD5A5C610275352D8B0A04159713C26422C688ED70B14209DA8EC23E8A2B285D61B8C5993739707386B4A084B22F70BA9EE3073644521E9B60D77755C30B36180B6B6A64A94ACEA2AFAA784C53C0C78E79203F36E98016CD1D109E17952A49021572B5F43857FA401BA617146E09450C8F392E423AF9FA294B7E2C2D772784B157FF591A4A13919C573713312291BC02F0F0C93721796477608E8E69FC64262D404C95DB98E548A767A184047A92C8FD8AED972945E676D946AA0DF5A88C7A13D03E501D228482FF97D62E27529372CCA9681F0758C2B500BB5D691D1734859B93E8E177605729337B39860E256D61C75BD462A6607692BF74A4CCC346240058B766502E740AF864531301226EBC3B8B5B1378846190AAD61C9BEF1362537606DEAA531EF97582DD1551E7A5F960B8DEA7A1815272145B25C7D175CDB568A68F05058F255B5B586866278F3AA56568179DF4B848FE535132B7C52D3AA3829686EA7CB6FA1BF07099F055416B8041AA9E2522CD23FE29981B06B6556432ABEF02E0780BEAE777D26FC062D94F1BC4C683AD2B92303D532101461FC0B8115556C3D2F2B855D1009704611\",\n          \"dk\": \"895C17908C9C3325068A545C951A7E5A2970A205783F8426BF3359F556014D79ADFDBB03CDD8B2E56BB3F8A095CD143B2D73AC87D997DD64633C0056203971E38A1C1E268F1C3A0C19D0A573F9B6A0401FD6FA93B65BCACB7C5E7FD660C4F7802D99A01B41BE77C776240737BD2234BE38B655857AF88AB3E40294355A5B485316A2109447606B3F86C512D726F3B915524589D4A3929B223D9B9157CAAC627F037AAD467C256A39484B3C1F3CCAC9F20F4A8AA840009998298C7FFC453607B1E1F43F0F740631B051720A3CA1392A34D84C06AC135B3A22AB277E51C56664917254842ABB134EC4576D33C69C7C66211C0B40C6912547EB5B9DCA0AB2035552956860DC230FC616940AA8B3249F8D8570F763419DE0C8FB1678AFEA395A553D1E3592DB6B4766684FF4421E97CB3C6D12C7798C63EEC9CA09868D8290C0A8ECA681AC1280958BD67A615F947625C35EE0C36F15D6B6EEF79FBB811386A9713A78B9FB581A6D47178A0A3C8D11B23BF6A2D997BED0CC34662C109B843C455C87ECE728C790AD24E690376806776A0F1AB9CEF70C6E96B46916321F77251E42DC7775BBC8BA5C295473186A7A277B2561F0901E96602E5ED31D50038CA839B75A3164AD17CA92308F3E75264A6C8EF0D940FACB9DFAF855283551609B4B2CE0329BCBB76A0710E4807F95169439C6AB571477C4C485B73503F87157B75418780C4CADB54B4E2CADD6791688E867FAC57812671D31A92F18141AE033570D21A12C1420733CB7A9835B60AB4677445C49756DAE5477F1D400FAB10C5134092AAC2C0D42693283147E033CA08A6672C4001734233F496966C214E7A36ACE327AC883C944446DE2085233A84D18C6A24BA95B3684CEE5BA0FA30A2C1A483EC4668B74C356CC97C5995247AF39549C92BA9D544580579B1E3B49FC2864D945A7DD555D2F5CA42A519615AB75EA593B15E9BCAE108A3B711CABA18E58ACA44CF2BBEF8C00E99540888297AD37CFEE4BA60F843750AAB223889AD7148164F28330926368F009CC440801989BB4003FEAF3A25B76C6E6686C06E3889DA28DC62383035042F1E64196560775C3B2E6CB667D30578BFB889CE194CA582039710C5D673B2983A8004BBADA80627A7A33C3B524F5CABFC88995D99A58709A634398505D77121D76ABA1FA8B12F93E171321C8EC7CD2C996FD714D07A9B6FFB389A05C2761C45AC9F3B35C24A246E348AC7B0C7BA347589041EE24BCA378888CB6471546CD9E04A68469956BD59865B6682011BF1FE71226077BE7E9C81828AC7312417E46A00570A6D65C4FC4274CC43B73305B43EC8C96CC46BFC898535CB578A7F483D63C43DBF36F3D698613ECA11019581301AA319BBEEB29701ED472955B4C453110BD381F328573EF83677C65107BEA546CD930ADA0CBD153ADF7C7986557438CE099583C6F517C99AE62BB35769385459FF2FB1C38E130F7060AF97333D38BC6725374EAE980FF69046E905DD3E33F4D546C3AFB6EC146CF85A66366B700CB544FEFF9439D9521486240DCF31B821121DB914F107B32FFCA171B764B299C4A5DE793DB42AE20505381B820E7970AFC2064C80203D9B63188DC6689D67B009BB1B4A912409471E1024944D5203F82C62D1CA67E96567F3A525F6AA22683C4D537BA87DCA66864C88F292630484CE6C2592BE194A59B9642B0C0C1C4771DF87710F11FC8898573BB874965B774C3BCF30920312BC3C1945876AC6CC1221C9019677087951EF43031373BE34492CC5638D530A4D01C86F9B70296162950D92B5D97724BC1B7E9056C78D16290F3BEA459AC21395FD8A60EA8551B5D935510557CC36CC5C42605A4459CD7CC603408C3AB2380F7F00F44EB0DE7457E6F40B8DA2042F34C18554606E8A274FC8B6842F299C1E80068F11870907D8D44269BB7789AF0C8465B72CE64B5431A885A00B0C200102C811E8ADB7893502277FB87213AA89051852E896B3A170A33CA8F44A7367756944760B1D3C37318AB76EACBB47BA15DBD83CF8151BD2E3672934702C3A21886B27BC3506DFA8CCDB77C818734A18230868092186431221E3020157374ADD138C8F3C69D7709BF60C5A06BA3E410AF156B2175020EBD7603AE10C76DB7C2CE066A98A0AD4131781CC30311B38A78CB809BA48136009C36144B47AA2052E59F16E0216561BB42062817C67D81C96223C37DA9332B261152ED90350BB2BBEDBC438641CC082C8E4A4181628253734CB262061E40143BA11BB5AB75A89270636B330E922C5298068C85C1352FA5A133C3900A762F57B831A907AC37A6C9CBA9C915290E10A447A8B454A0F735DDB8852D79B0FCEB207C25568F44CE9568305A9362ED584D77123A3716A667CA2952CACFD7382303A69A817B17D895441D0A9AE1F71ECE1277C0432A7FA2A63C81A545910B4B212F1D983DC634313BC4B3B1B39190438D47B16747FAB48DE4777911B8D3FB65FAC68CA096919267783B0497868A9F95E89A1A3B2861A3BC89CB77F6976463CC7B1CA6CB8D447AA65463174A384217CFD41B4B80B7AA4072B10BCCBFA73C53E45684C542B031288C1192AFB1077B4E2B560E248169C6B741904304B06D23C86C9434AD64C67D92FB872D28BF9F745F2CF210D6D0C9C72176A3242447E06925718FE6530E2B5018833831819C23D3402BF9DC22FA7B1B754C6CD5A5C610275352D8B0A04159713C26422C688ED70B14209DA8EC23E8A2B285D61B8C5993739707386B4A084B22F70BA9EE3073644521E9B60D77755C30B36180B6B6A64A94ACEA2AFAA784C53C0C78E79203F36E98016CD1D109E17952A49021572B5F43857FA401BA617146E09450C8F392E423AF9FA294B7E2C2D772784B157FF591A4A13919C573713312291BC02F0F0C93721796477608E8E69FC64262D404C95DB98E548A767A184047A92C8FD8AED972945E676D946AA0DF5A88C7A13D03E501D228482FF97D62E27529372CCA9681F0758C2B500BB5D691D1734859B93E8E177605729337B39860E256D61C75BD462A6607692BF74A4CCC346240058B766502E740AF864531301226EBC3B8B5B1378846190AAD61C9BEF1362537606DEAA531EF97582DD1551E7A5F960B8DEA7A1815272145B25C7D175CDB568A68F05058F255B5B586866278F3AA56568179DF4B848FE535132B7C52D3AA3829686EA7CB6FA1BF07099F055416B8041AA9E2522CD23FE29981B06B6556432ABEF02E0780BEAE777D26FC062D94F1BC4C683AD2B92303D532101461FC0B8115556C3D2F2B855D1009704611FA9DC07F088B86A0879CEC27AE467955EFA0EE0A57A996B3B2846ADB293805DF1C399367603CB39ADC06F676FC6C04ACC64F24D88C1E3F36191D5294C82C45A4\",\n          \"c\": \"8C8DAB2B1D37BFAB6EBC4E502788E061EA1097C8708ABFBCB2375B77B25A985C608D83AF0A1049594D56F920830B98292E64DE1D6F6D748F543D9EF3847492DAB472D7C54949CEFFED40434519816E9C9C1E5477C209F4518EB441DB2036B7C1A4A26833A25BFAA6FD5DAFB2F39C04266A0E53F1E2886137943AD5EA9996206F10D5043C8E3BDA12122BB4419E1D9192285F153999A270CCB7BF0EF68E638A95F3BB416F670A8D13A4625B4FAA29B0E6DFE65C74F1007A3716113A3F194C6319D5FC3B354E2EE56C9C2CCF256A2CCB3B3B0E5BF573CAB638F826AAF85291DCBC01B763E83A834B1423400FC170B3112D2CB44653E6C17684CB2ED01CB247FF1FF1BB242F2519C73B41969CF912713F8BE34D5F9EDF33D431307A0F42481CB904E2A6A7D062A6E0A4CC2E63699029615F5F3B35361369A0D827D4052D50A8EBDE1469B1CA6546DAADFBBEB1424330BFB488BCA9B1B594E5E2962BF10F000098C2CDBFD9249A65DCEE7A887FBF19A4CE484AA3ACC53736FB24DC1DF4D54E6E4B37BBCACAD87E9E324BC5B48B7F8612575EC416219DFDDD225A13BACE2FC7498A92F6B19DF2ACBB8408259E4058A96A2831E71DC6DCB396B350DCEC403DDBF3262BC70721F1B7EFCF8653560189B70A78F4C74A0F1DE28490F9ED08E0E93C7AABA92BC52384DCB8D24B2B998B1D4345C47778832355A7B87C66987C53B0E0F1BE3A163CE306E2A9A579291C582E7FDB7A8845D85C6AFA78361F8B1EBE36D3B65D6EEE875363F1087DAA2A04183AEEEB2F65643E4D75217C8EEF3438052049C581187E405A792D1CB25D2BAC78B2F92BF8E90B60841B2CF13CB2439F2CFFEC0927B1C47D6415ECECBAD2BAC55122BE81C4C745B18111806670A4BC77C89B25C0FC79249481A6619877067D2C7B06350BEAD29AA0DF24D966F7BF5D6B46A2AE89408895121568F5CB93575A33E5CF7304A162C89AAE63C9F044D5BFF63542E60140E25EC9AC5A653CA7B2C8CFE2904CC4E829B32F3382E5CEC4D443DE510D970D17D42333B8951CDCCDB40FED38118AEEB3AB13A1AF93AD106620CF5538007A92E3354B6EB0E8DDC3A733DAB98BDC532171B4A44907E444BB13F44B78E5DA4BF142C4A8BA8BEE4CFE55045D42517016F86CD088C992AA15486A4236C55FBBDB052F26E15AF989A2045B647F5CAF5C970EA516696B237080AAEF314A05E76064EF8DE9AF726147B24BA2936F92429FD7BA2DEB5D1D955C73F81BEB2EA6471356E3E503A6C04BFE39E3CD98F9A1C8F10BA19C454D2053EB67B6CEE908A05A5D08B5E89D2B539D2123EBCFA9A8F9DDEDE7E6DA52B992F5AA88339BD16DF594CF53E81B6550B49544A70A1268D36A43AFB52FB0CB164E183FC7578A1CC5A1F162A1F0C86017E5B24D07BF9B554DF40916A35E708A736F6AE1732BE39F11265BB083DF21B41F92824A44E9A6CBFA9CD7803261085965F2B7E6561087041ADBC58E00BBC8FC111D89C36DA9FC71721451A106137B2D2A221D3F9A8E09171D64B98CB1EF4F1C\",\n          \"k\": \"C26DA6A23332B20914F703E7CB237D84F807CC7248DDC47599DDB0D40FDC1FAF\",\n          \"m\": \"7B334E045896C00F90D811489D491E8D72C4E3A22ED831C019FD4BD967B7A802\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 45,\n          \"deferred\": false,\n          \"ek\": \"D0F1B584A87CBA7409D8B98B49B1332ACA1545B29FA2D42BC537CF959C6182305C95E06F5040B447D00737B0BBBFD9AB0A6062FAE209B24959EB83C2F0C5A2D8AB36161B90E2A92C5939059F8B9586F071FB1B26121604D5252D136705D33911703B91DDD85ECF80A7A1741C29FCB06F321E8BDB8166EA8C899B8064B69D77927C0B503989101F1A6941F4DAB06BAC9973378C2E6B3B19D9506B2C9A6BC29A51582C8B47B46306C1B35A438A097B07405645FC66C2660F4C14457A41B91907CB2864338C09C0CBAA7A877A9543F775950487DCB8CD8A105822B93882271A17B6936912A55FB4109B45B09BB14EAC2134E0D7471DE891CA984FD7B194B1529EBDA3CE0C24C1A9A170D76145215C8CA7217A138C9FB7DA8385E8A0C56B5342DA66F9FAB03AACA4E3A1BC5495C7AB0AB0E451A22D480B00FB29CE641C46E7A5EF16867101A26911B0A49140F70B8684845850DB8E15CB7BF842CE712480B19343077674D8A7B7C47A5E1170A629E53ADDD10F4BB35A601A843B49391D7337D7B3B592182F3A1636E4A7CEA14676A0DC508423B94B115041EA5F925BA0F2FA8536AB11E6D451321450CDFA076EAA3577D605A7E0B354754B92F1B411487FD8122E8DAC14D8464EC4331DC746B5693451B52878923649144182C1F03DC4281A7991C60F4A4076117B50C21670809832F3A67DC82E448A49AA3A2002B1A8667CC947253015A6A88FFC133554746256A103D11C7BC98013264D5EFC15A0B2825B37C356A96F0A5B701B4CBABF57640F2A92796C5663570B30FAC65BB79ACB59CE5C4CC7757920496B85CF938992EB32048360559745368854EC7A85A8E862A973AD50D67938265D3541161BE9C4097323F5E77EB8F66887A89FB7DA1A2585A4DD52782AD221B515A1D37778B5911AC7C90FA7722FD5A9484486A6210157188ACB617413516955F5EB91D2691E1E38CC2D10638330029D033005A88606E6A941962F3FAC506FDA5211609138E503E3382BD71640364066A2C27780898AFC1CCA9509C23F022AA77546017B1943360FAE24A54A0B965BAA7966F45AA7F3BAA7C6271CD28D4E0263BCCBBFE4FBCDFFE05269B048E948BF2EEA1E3C1825FBB3AA8F182B2B7BA52877B2304434DCD561BF365BCDC6AE0960211BA14EE1F9C8CBEA3750472A02271BA7E79C4FE6C9019B25470962315C4F50332414F46233A354928980A7752D7A07215EC24133446521DA5E4C50B615AC4E78F07B6EEA141AE146FF5BC2C63167483CCAD311B847996339AA50B2C237B4486E3B538C75B14969676465F106B9069A6F627938690139F011F5FA7D1832B96A0309FC85920E142C9C57CF64852E4564334281BA63068C5666A71B194319E99FB024AD33F879D08C16743B34038AAD20D2C84CB7440EE04C1AF4B944227FE94B5D666C3E51523248D913B9A3693DD1147AD7064696CE5B613DFAE1B83A207742264E878C7804D603BDD71E56C5CA7F887158E805161C959F072CBFF82DAE020C80135A8D1890F1A9A839032B0D69B9A7A97785DA5C201685C683844DDC99F4A97E00A063E15122AF22C27430328EA1435ACB07ACDA0A3B6B784EB19E30E08353CA26118AC4D5C4A218A262C4BA135AAC24F1EE7C5EA0E13C86749E5E72541CA6CAA1E1C05174B08745437FEED0B9\",\n          \"dk\": \"51901DD6E7BF9FB37046292E2CEA4037DB8874DA2A9B24403D879FC8104A512C52903CA118E159D235C9344B815908C62C99AAEECA688F8756BB844AB02A15F7380BF92BAF4DE4CA5BD05F5E66914C136838231C563CC1D1729B026190D5085DF49C01794541408A4C50042636804E16D3AB3FBACEB6798119436D42945A25860825025F21279057A775ED835510E2B99439CF13B7261A83CDCBE39D62E42DE49CC96C45AA6E8018779B372FA0A1661146A2A33924298920DAAEE3E81FBFBBC5EF514B0E773C35D76D8FD413E9D4C07D60212E91B06EE1B795E3B8734660C4E3C44F745CF2058AF3AA5A7045317395078193C53B034A621A9798208FFEAA3B53802554AB077AC3B59AD33CF7782065F9466A782484F6049F54BCE6E85B2C957460478B7070860F108655C0901C26ADFDF553755B1CC1634A2FC820CE1B3F190C8A81A78C6E0789478B1D6F594300CC0C91979E222AA9BDA73F1C88286C94472944AFDC97326904C982855EBA982068B4A88BE346D939A2C4316FD1268E6E0200D40C7A7F701003028F1A05A50A0355F8D2CA5FC074B7CA4D7CD1087EEC90FD77300F73214C68093DFC4AB81C2B718B278A411185B9C86C8237C3A402809C632779136C641BF938CA3650900B62BF2101B827133C014398C503A7FB1B98C23BC5F492AC50B706E387993EDA7707B9662F180AAB380793D9B77D0006CB4BB83A1356E6033BECF84111A89FCC8A9A1E313A1EEC78227206670854CE4C7DE9CC87728B8F94F647A5147CC7B5B569D8B1572468D3E012FC3558A59C6BB808A1E7F1400B7CC92F1C55FA9C6DB91B884FE4C60F77A2C77624F5185EEF787D8DEAA066C50A4E71A1CBE964F06674EF8730B4E8BD599496A6322F25093321EA618B624D7CF15D2E18426842BE2F289AA4515D86CA89A077B254D95E037853780A238DECCF91A1123D3176A68C40B72085DD5179A49207B2A7AF7FDCB49F536B8A4884DF25684736BBCD546551589576499F7AB43625EC30F3786214961C89E32FB2C13E306B8C85A64D68A063B43425110420FEA50222D9A78A0409A550A4CC633DF75843C2CC3D71CA55CE012733997028E8778709B30A4C03FFBCA251990A8DF57D22B761978149DF42C8560246C72CAF55448B7FF114D2245CC58453CF836E40A851D23C754DC66574604FCDB49DD5F74C1EEC429293C3394CCE920A9522B598ADD73C8BC8C71E5122369B4429B88B0FD08DB19690C65518C8E243746734F6649C2DE845E19A19E594894D8B5382639AA87C9806525BCE13CABCFAC521884B4E2B26BDE7632A9A8C581BC42F87241F1408325B16C46532D0522F51E7B3E7E2A7F4532A2B4B8710589CD6B32EA0C7BEB60194CE9372A0FA79005214FA45153CE556E724A383AA5DFAC973EC73CD7D6CA1EFCA49DFAB32C32B0EC27879ED18932140AC340006BC5CB547A532DA668684218B997325F206AC22D169FCE0BABF6A1B0336B1CC039776A7A33952ADA8E54706FAC9E4B295ED219D2C334A30149647720F025920DF980DA164B0FAD128D415652F3BB8DFB0AD6B23A8EBC776E1035E863ACA12C9497F189185C7C2591072210C97BCF833ED28302E388247D325302394D284CBD0F1B584A87CBA7409D8B98B49B1332ACA1545B29FA2D42BC537CF959C6182305C95E06F5040B447D00737B0BBBFD9AB0A6062FAE209B24959EB83C2F0C5A2D8AB36161B90E2A92C5939059F8B9586F071FB1B26121604D5252D136705D33911703B91DDD85ECF80A7A1741C29FCB06F321E8BDB8166EA8C899B8064B69D77927C0B503989101F1A6941F4DAB06BAC9973378C2E6B3B19D9506B2C9A6BC29A51582C8B47B46306C1B35A438A097B07405645FC66C2660F4C14457A41B91907CB2864338C09C0CBAA7A877A9543F775950487DCB8CD8A105822B93882271A17B6936912A55FB4109B45B09BB14EAC2134E0D7471DE891CA984FD7B194B1529EBDA3CE0C24C1A9A170D76145215C8CA7217A138C9FB7DA8385E8A0C56B5342DA66F9FAB03AACA4E3A1BC5495C7AB0AB0E451A22D480B00FB29CE641C46E7A5EF16867101A26911B0A49140F70B8684845850DB8E15CB7BF842CE712480B19343077674D8A7B7C47A5E1170A629E53ADDD10F4BB35A601A843B49391D7337D7B3B592182F3A1636E4A7CEA14676A0DC508423B94B115041EA5F925BA0F2FA8536AB11E6D451321450CDFA076EAA3577D605A7E0B354754B92F1B411487FD8122E8DAC14D8464EC4331DC746B5693451B52878923649144182C1F03DC4281A7991C60F4A4076117B50C21670809832F3A67DC82E448A49AA3A2002B1A8667CC947253015A6A88FFC133554746256A103D11C7BC98013264D5EFC15A0B2825B37C356A96F0A5B701B4CBABF57640F2A92796C5663570B30FAC65BB79ACB59CE5C4CC7757920496B85CF938992EB32048360559745368854EC7A85A8E862A973AD50D67938265D3541161BE9C4097323F5E77EB8F66887A89FB7DA1A2585A4DD52782AD221B515A1D37778B5911AC7C90FA7722FD5A9484486A6210157188ACB617413516955F5EB91D2691E1E38CC2D10638330029D033005A88606E6A941962F3FAC506FDA5211609138E503E3382BD71640364066A2C27780898AFC1CCA9509C23F022AA77546017B1943360FAE24A54A0B965BAA7966F45AA7F3BAA7C6271CD28D4E0263BCCBBFE4FBCDFFE05269B048E948BF2EEA1E3C1825FBB3AA8F182B2B7BA52877B2304434DCD561BF365BCDC6AE0960211BA14EE1F9C8CBEA3750472A02271BA7E79C4FE6C9019B25470962315C4F50332414F46233A354928980A7752D7A07215EC24133446521DA5E4C50B615AC4E78F07B6EEA141AE146FF5BC2C63167483CCAD311B847996339AA50B2C237B4486E3B538C75B14969676465F106B9069A6F627938690139F011F5FA7D1832B96A0309FC85920E142C9C57CF64852E4564334281BA63068C5666A71B194319E99FB024AD33F879D08C16743B34038AAD20D2C84CB7440EE04C1AF4B944227FE94B5D666C3E51523248D913B9A3693DD1147AD7064696CE5B613DFAE1B83A207742264E878C7804D603BDD71E56C5CA7F887158E805161C959F072CBFF82DAE020C80135A8D1890F1A9A839032B0D69B9A7A97785DA5C201685C683844DDC99F4A97E00A063E15122AF22C27430328EA1435ACB07ACDA0A3B6B784EB19E30E08353CA26118AC4D5C4A218A262C4BA135AAC24F1EE7C5EA0E13C86749E5E72541CA6CAA1E1C05174B08745437FEED0B94BEB59C105550656320DE3955835AB95443E5E29C3324284CAA26E76BB6AB3D0B37534D57066DAE72629C29DC0B9090EDDA3D3AE710D53C3EDF2C0A8DEA0FF08\",\n          \"c\": \"3812F9581A4D32DB0FA1D98110858E6539FE3150FBD28F25574851F4A073CEA119A2389B50CE230D6AB30EBC0042DF57FD9C0EF7A0A2EFFCC08765EEC2454306948221F8C2E6AF8415C2E9AF939A148CE20C052C9E56429DD4DDA625485A6918CE70A9319B5AF49392EDA205449C083B3A14096C1AF57BB39A1C9453EEDB85F69FA268B3404E686F9FFFAE236D97DAD29AE3E2B84EE8522ED3D51EDC12203620BDE2783C061D248CFF786AB3C61C5BA6FE1804CB514D872A391E968C1A980050324DABAA48BBD7878117333B8BB793B3198E4D94AC7AF564C5D4947163C84FBAD5DBA3D8C8FFF49518550299FA5323FE20C50979E44EF0D943EA1CA8D03ABEB261E09D0D0CC2F60E108D462823771C9E789500088462DF65EEE971DF976018CA33D38028855C3CC6B3019ADCC82F31F2EFC7C82AD3A46BF9EFE934AB2C9BC7EB7B2745416A3722B03DD7A6030C697E1C5318D032C8F506BFAF1C6F3D0049B4F9741A9CD3165DA27E955A116545BC5FA274980FA302A3083DD3025310BCE0E88D13D58CEEC02D6AEC1D1DF9D90FA8C206C0BC9E2DF2C9145503BD7363F1BEEBCC5EB6797F6732D7D0BD9299E2B395F75C5ED574513E17E2378E0F53DA0ADFD69921B88DE01FF6A2B94059451FA9F4D19864178E3FE343624EC063617AEA697960EC61C01A6943FF92D32EDA1BC6BE8AB12690ADC7E7983E269CB552DD01D5A61C549D932B9D936BC2B8E15375216C2BB391E96F021A7B1D46C625FD76E8A9111E1AEC67885C9DD04A72BDBEA5377E543A393B8989A02D65FD269EAF8BF9C8EED223277EF118D888B3CC8D3E0D9CB75CAA04BC22B7440260B74594C9DC2B2398DC6FDCF7B7BBA625E475E6CCCA2089241D6F3492E9FC35E29A3DD934E6CD0E3BE920B165207117C9CA858B88E57F7175CF1B1773EB8C292DDE4FA978504160C1F19C1FA4991FBEA15DA8700FDCD70B938283EB38FBCB740250CFE2510C85B5CBC1967A94C065A446133BB4C1FACB5305DD3B497CDEDE3440150868746E5A8E1775C07ED534793DECC83458A3BBD1110739E3668F3E1195134458D23210E2C973696B542D99392447921BC6249C84959DA4657E2D3C675520B3A52349CE7AE92D0900A085F3A59E73438C3281505744ECFB53D8FE24494D129BA778A28113651535D6BC5D47C9E8FA0F4AA159C3A9EDFC40DDD399A808EACB88EE6524B481217A1F1575A3F77A25A98C46CC674CD53C369B69755C66D637CE35ECA9DE6B660A718DA7F5A88E77ED68D99616CEA7165AA840F60A289157FA01DA864478A519686657F0D88EEB1B9A601F80FCFCDF430A07A2A86CD076497C894297ACFE9B9DE1F27B6107E19CD278E3AD83E135D153B0D8B44E9A4A97F869D1F6DF45B4C96722316D22023990BF12B9D177119B4985B0FEE3317557EA0B832E9745A9E09AA814E718DDCC077C2809BA164AC5AB1B47020804A5C5E135BB67582BF6B10830030E7656E09CB55DB575EB54ACBACBE80D901011A5F73594772368F47B7528D8BE68940C\",\n          \"k\": \"104177A27A18B5F35D2CBE9BFABAA2EA987B4296946DEE575B45A3A9B44CA99D\",\n          \"m\": \"947AFE33934E8150B06BDD1EAE40CF82EA99C0C0106B101283EA382EDAD94A8E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 46,\n          \"deferred\": false,\n          \"ek\": \"28C938C98060954AA557102D5BF39C17444A811156ADB2554562C5ADE5666704171AA3B19964449E1C12E721A30299307EC7ABC2CBBC82010E3ADC6DFEB2C497DABBDAC973021C6535773C127B3793792EBC717D392BBFD4906D4F21C62FD6C565935DA98550A2A310488956799AB9743B94446658FC97A9E1C3B263C23022132D33D2789295C994B6A441000F7FBA45C83A920739A17312AA97B1625FE5118ACB1552B647FA91C93C11182D9B9AFBFCB1A0D1412A7BCF92FC2E1A197EE4E728BB1812B88A87729C6E117A45E4E0ACAB2BC4E4A214DBF3BB4528316718C477329DFB1229AF9BCF0A00317FF526B8E4828CA797B354A0BD304FFFFC3E16C86A49355A54A0005CB1CE532CBA6A19267451ABC9C645A3F49FB075922BA931B1F139F8C627113B5358E02E81E71C231A41C1D647840B5AA96937B22926D6477E5B3A5D1C00485BD91FE5679CEEF18670223E670879EB3B02D03958A7751C007564EDC37DC0524790C8A0035AB30AC310F8419897597721D6B4EA87300453CFE981440FF0C18A323A0159B47A9C262545359F5031EF8B24A0080BDF0B55E1606355B88FC57803A3E25AB5ABA59582054E184FD97CC3E4635BC77AB797A748677B52BA76A16A9564BA218655A0070F20BD52B0A8C512A947534BBDC99D38621C655970B209C6DC8A0033105060133B600367E62366030810062387BDD120CEA00424C4C307E64EED5A3685F7B78E51138E02C4FAF51AE359499E2A97BF3799E216377EC3581AC81C12CC8B41AB95EA98C6D0500BBC3960AC05A2292789844602CA7528B2B91EAAB7A5F21399E13927C58BB030F492102B4361DBB4B3023961117099A7648EB11B2BF25703972CF778B5D8AC087BA59BC5122DBA63AD03EABC02031EF60CC765A0478F2C15DF31165903CE56680C2A62CF7FA8459D26C2D3C346617C51042351B321BEDBDC751D9080E0F64D2673C17055BF5DA53D74A0088357C1BDB8589AF5505C40CEEFD7140B95780BDC9B31482C75F5C6F395264A54C2A54B3EB6454C879510BCFB42DCD9548C3A53996030CE76A8A2435316CC227B591FEA7A0C12405D27936A0326A66A6CAF0B271BB64337D9DCB2E2471DDC632F3E0300F92C04B288191F0856C59376FD755729F195C264824657725E221BD017350982BB982078CA7A6783C64C5EF462F329BF6BDAC685D1BE483AA78B7BA7F2A0BD0C0C3A24341C9AF09C72D82674B0849BB70BD4861E5FEC25375223F5E4A4E20095F330C43A3AB23B633A0DA04553F69043A0B6AF862A94835AD284C31F82C23E80C2A488872E4B314636094C70A5F7614B1174AE46C11F45661256D566C6801985A2A91990889999631FB5BD4072C884B85BA79521BB2C7A729CA3CA8C18F5EB340EB84B00CB991D17933B3212293828BE1734F21B775BE289F77CA45846B3B6F029D6C610A0738F61B5C80BC808A8A506BD1022940AC755A7A4765A1C4A36B42DA453AA4B516F78B2C48CB94500401FDC654C1BA5C0D58FCE894E455667B25269A429671B668114181F06060842D16980F7A772480F3EE46D58C01682AA538DF1813CF95B67D9156D945E2FD84A7003CBCF265CDC12AA7148B7AA55C66F44A82E4BCF7D8C7B1ECF22E1BD7033C0588FEB6A1D553CF8BC477D94FF875323943762AB1B\",\n          \"dk\": \"367C6ABCB47754ECB23287CB530C5F29BA90464CA083AC4C7698C44487B209639E37E43DDC8055CC936A0C3A4EEB215EBF7651B6D8849CF2077C5969D66194D6BCC1DC96B21695856E072D83194D4F2966C649A55BECA2E9D9BA3E9049F985C9F06311A5619975653D0617288EC85BB13626C2A713E1245EBB914E4EC7651366615470483E478DAD1276A3C46C08E0BB1AB4AD833A584DD84222E2B880450806426937626FCC4597133B9FF2A4355EE20258334AA20C5BF7639A215AB2FB4B3A4D74AD8D66C06EEB290D370447053F9EAB57FB42821C2667011A95BE07CD33847FBBA78F44C7B10537C3AE97C2BA653232827C9400538D4BCCB371B2E7BBBCBA0C34A83851C0236750A472AAA163080C562A40908D1585549A4F8C44798AC877D4C2027A2018399C3B5A03C278B20057436B1A00BD83B8ABC3B6B78D18B7A94252BED77128617ACAF3BEB1A937F516AD7D0330DC9694D7835E38A35E63156D0C9B48473639B3C1A34315C67C389D686669CA4759C41748E6A43BB6648820061C54E41B193B79F147659DFA83D5FA3EE7D195D0D826203899EC813070D325D943CE82A7194D3A5114FCAA71266A5A2CB8CC2B837D261E28FA2B12C5AF2C20C19DC6CB49559BEDE9A793A5B272F8BC357A291D55CC0347093AF3547D6913E61C4316E49422874D84342BDEB508C4B1259757C4D9D139729404B4C083B242A1A9920328FA912A5B9B1D67300528741C2A0D56F5870C1316710C6950B93E8535A8D1512891A257FF17B71FA46BE18167951BB87A5CBCCFFA8DA5C3C471144523400F7C06B6F6E9CF2595101D230CF4B256BEE416638389569C774D1B5A25BB2690B4BE446C4FF68B1C64D87CAB248C9E7682E56CCA64FA8CEF2C9C43CC8B8C265EAED9A45945B308A4A946399CBFE59541D144DBA75F72E87272996AD1C1752E1524DE6C8965735E7A12544283A7438264644B32E9D78DCB99B82602AF6AA75D10C645AAC8A30CA90B02C914FD512C1367AACD1C38FD38588F527A24E01C9118A71BA0B4C71AB6298845A26A24B6A4762A537BC6A0797A8725DB5BA56FD67038B1CF6ADA8E7B4887B9E369478061228B8985922B6A909FA318AB2477867DE299BC957087B26E11E134C3EA4198659971F69302618E1146CFEDB6210A99657C075C7A14A569780E6E94395EA4AE38CC735F7375B71A4C6066CF08251859036A9111BB21D03EB9294A756C7D3002052E9492500C77B88C4F9E41A0D347424D095B816C96EC790527D74928028A7F542F4541BD3AE06E11E24481B36F4E22930D29742E4A3F3D1B2126258CFE080503F9CDE8133A99D31AF41047269077DAD309AB705DDA9A7CC44374FDFBC26E771ECBA3A420D162DEBB440BCB87C4A993B1C656F4A0250B029F20449C63105F865A1E26E21915C0099A2CB0279A0824DC1DEAD981AF301E34759EB985C313C7488673AEE516655421997C811E6F3676D07C9D52C27022C665D7518864840F7536AC69432451A1C3E86093852B12C465BF460317C5311E12D3790EDB06B22B54F9B7018AB16E1C06049B774C68C35177F369BF2B6423F5175A76777E4A30E67489A1D0319CB1040454B9CD1C9976658DFFA43009D62128C938C98060954AA557102D5BF39C17444A811156ADB2554562C5ADE5666704171AA3B19964449E1C12E721A30299307EC7ABC2CBBC82010E3ADC6DFEB2C497DABBDAC973021C6535773C127B3793792EBC717D392BBFD4906D4F21C62FD6C565935DA98550A2A310488956799AB9743B94446658FC97A9E1C3B263C23022132D33D2789295C994B6A441000F7FBA45C83A920739A17312AA97B1625FE5118ACB1552B647FA91C93C11182D9B9AFBFCB1A0D1412A7BCF92FC2E1A197EE4E728BB1812B88A87729C6E117A45E4E0ACAB2BC4E4A214DBF3BB4528316718C477329DFB1229AF9BCF0A00317FF526B8E4828CA797B354A0BD304FFFFC3E16C86A49355A54A0005CB1CE532CBA6A19267451ABC9C645A3F49FB075922BA931B1F139F8C627113B5358E02E81E71C231A41C1D647840B5AA96937B22926D6477E5B3A5D1C00485BD91FE5679CEEF18670223E670879EB3B02D03958A7751C007564EDC37DC0524790C8A0035AB30AC310F8419897597721D6B4EA87300453CFE981440FF0C18A323A0159B47A9C262545359F5031EF8B24A0080BDF0B55E1606355B88FC57803A3E25AB5ABA59582054E184FD97CC3E4635BC77AB797A748677B52BA76A16A9564BA218655A0070F20BD52B0A8C512A947534BBDC99D38621C655970B209C6DC8A0033105060133B600367E62366030810062387BDD120CEA00424C4C307E64EED5A3685F7B78E51138E02C4FAF51AE359499E2A97BF3799E216377EC3581AC81C12CC8B41AB95EA98C6D0500BBC3960AC05A2292789844602CA7528B2B91EAAB7A5F21399E13927C58BB030F492102B4361DBB4B3023961117099A7648EB11B2BF25703972CF778B5D8AC087BA59BC5122DBA63AD03EABC02031EF60CC765A0478F2C15DF31165903CE56680C2A62CF7FA8459D26C2D3C346617C51042351B321BEDBDC751D9080E0F64D2673C17055BF5DA53D74A0088357C1BDB8589AF5505C40CEEFD7140B95780BDC9B31482C75F5C6F395264A54C2A54B3EB6454C879510BCFB42DCD9548C3A53996030CE76A8A2435316CC227B591FEA7A0C12405D27936A0326A66A6CAF0B271BB64337D9DCB2E2471DDC632F3E0300F92C04B288191F0856C59376FD755729F195C264824657725E221BD017350982BB982078CA7A6783C64C5EF462F329BF6BDAC685D1BE483AA78B7BA7F2A0BD0C0C3A24341C9AF09C72D82674B0849BB70BD4861E5FEC25375223F5E4A4E20095F330C43A3AB23B633A0DA04553F69043A0B6AF862A94835AD284C31F82C23E80C2A488872E4B314636094C70A5F7614B1174AE46C11F45661256D566C6801985A2A91990889999631FB5BD4072C884B85BA79521BB2C7A729CA3CA8C18F5EB340EB84B00CB991D17933B3212293828BE1734F21B775BE289F77CA45846B3B6F029D6C610A0738F61B5C80BC808A8A506BD1022940AC755A7A4765A1C4A36B42DA453AA4B516F78B2C48CB94500401FDC654C1BA5C0D58FCE894E455667B25269A429671B668114181F06060842D16980F7A772480F3EE46D58C01682AA538DF1813CF95B67D9156D945E2FD84A7003CBCF265CDC12AA7148B7AA55C66F44A82E4BCF7D8C7B1ECF22E1BD7033C0588FEB6A1D553CF8BC477D94FF875323943762AB1BF710097FD6086E9C14C3703A3FE5A5573EEA9872B6F28B4A383B70F37099CCB55B07AB371A4D050DDEB134D78D044F9937A01F9E17DFBFC4E495051A4948CD4C\",\n          \"c\": \"20BA3A872DA897C006AA6DA3D6793AE2B384C1F7B97CFF48DF291419D3CA55C4745A4C5336D6D62BCBD9941140506E39B4E080A3E669DBC5A123A20D2ADBA8A47D89F81209CFCCCE9253A639860333201E0505EC1E58AFC7DF41EBDE37569A22D3DF0C6E1D41F0D32F8118F83674D681E907E1B1E7F5A14A9E3E1B3435B30C375F5A8776CE5CDA68BA32BDDC44B900A6AD0C0AE3CFB468ADB9F8B21FA3C1D7EF97C4DFE1CB1B4F785479FFE9AB47FBAEEC6A688D288C2904124C2DF76C5674813C92E8DC3F246A515D268E44FE1BD1F562B0705C5DEA91B9782E96E740852CAE7949A84C14ACC6996F1D5CE5B5A2261DEAAD4787031CEFCA49AE532FFDDB7E097DD67D2C22238D8279EE0F8D1657F80E630DA1629F0C12910CCAF1296F2BA404CE59F8C22A792611A8A4BA5C88D88971C86A9E45E0DBEF51D4D14C5E01DA5F2C46E9D031A07BD9BED7A4375C895EE84B57C6D14211D90DC327BC6ABEF72779C9AE856E93BC74F1D8FFE00ECFB2A212AFE587B0022727082FE09B79A130CCADEDECCA7CFBD852AACA426819A688E9AF837A6EB5DAE47F73B46EB48CE5E059403F7C5D522E3B7E26D0115677428946C516023319D78AADC641C7B3F7A1F6E5F4429C00C0C5798DA60CDCD79111C0EC5B4BD86CA2A47F676B45D4380D2C94D6AA360E6497BC9B25C092D0B58D8DFD0D315DE457D95C09355DD9B36950245FD8909EE0AD6A1637F4F00476861FB8930E81040CCD6224FF19D2B84D37D0C7A383C6E2CBB122B0167260EB0B47BB6C424123213AAB6C89F2D527E20275966638B194F01F16863E2371D45572FC0D63541B896DA6943CF59CEB74EF53834ED8632FFF4975AACAEE7818F7B3F0514333CEE2CDC4284E2CF68BE084AF748880988CF77011EFCC00A66B0B4C6EE5628A950984127CFB4322FB98A9FBF1DFFA28A9E5C2893F3CBA1DF900D9ADB533DD9A991FBCB94E5E5167A4C8FF2DF80E504679090B7F729AD702F3EAA6A35FC4336FD0FCEFA1C8532E60321A7D72FEC4F10EAD95D210479B060EAB5BAC9A3AB6F33D22FCDC514401B50DED0187D3483C8F49C2B00D39502689E666009DBDD1A0B9AAD5EE2F3A7EE70D7E381D478F70D38C1455A7A39FA854BDB75C2E3F32A01F42936D684283A9E12A10CFCE0C8ED77CA394930A68E1CE4383C25C18A93D26A2B997DF21F12BCA7A478CED58C08CD353FB91E01BC163354791C9F7B43A5A90FEA0ED92D4E9041D0D9C36F1C204498568105DD409FD71A3A0BA7B0E740EA3690F5B8C487BC026DF1BD7C8F9C9B9BADBAB28D815D3EA200A9F75F0BB9D55AFC4AFC5F38A0609616DB5B41C7AF9479003BD2F1868AE2BDA2682922CB1C90270DB852FEA6F7489E686C85C20534151D94DB33367F02454C415251682A53CC2A0EE1A845A77F6DD04ED14B3E0B6099375B7DD10D4412215F2413EE326DDA5A4285481A2A70CC0FEC98DE1D77A8FF93BAC3196D7610E842B6A3D2908B61AB7A864C0CE2DB55D3E7DC4BDE89766BED1709986009DCB00FCA4BDC3\",\n          \"k\": \"A2718B4EB96D591690F62FDFCC264BC457C3A1B755F4CF64B359BC945C254CE9\",\n          \"m\": \"DC8510F45528D6981E59C1AA6B743BB844377D7339E359036929F0EEC54FE63C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 47,\n          \"deferred\": false,\n          \"ek\": \"4A6C1F1A816FB66471E6C18CE6126CD02BA76C730E721572DC730566496B3AC28F92A15F4C534C48578C18B443F24681BA2B3C3B390669A288EF7676C9A6B0970340F567C90407617B28A9A03274D8288006288B0A5B686FFCBACB273536941CD06927BCE109BF2330CF65A3CD0A2706EB33150A88504484FE182BC3C78CC5A7711845C3A277C763DCBDF16B19F1001727D5677687ABCF4B32CD161350EBA4206B2E1A65848528B7E1A90E04165BEE054F43793C23B0660FE7AF3E5964DC7C275458040AFC05EEC71A57E67C2BB794CE2680E7C4170FDB67F0F36DBA809F724B805BE66EEC38181FF1B0E9F94F7903661ABB17FD9BC0B0B19BCE2560E2C2C764F4863BCC40B5FC10B6513483A827C1668CCA8AA0B68039C8ACBE81B639E55AAACF2C761D78A77C2651573471434A14949943D945BC0ED2187DB442E41B901DBA0B14580710870B81B78D21996AFDDC6ACD1473673414E5C7C35EE60EDE3579223A7A2A436667CC51DD366DEAF615CAA4CAA073B60718B1E5B62ABF50BEBF4844EADC2ADA98CEAA8CAEBF187C2F26882673307A13CF83904607A8865A65592866499505051F10A4DE52893E07BABEEBAA4272B14EA974B5114101385AF1A011A488BA25179A118B312D8AC86FB58592D328941A3A0DEA6102C86F1E237DDB321615297A2BB660D5E0705A6672FD08A5CF212AC9B5B7A1306C87952B1C11B7D4E7C5B5FCB7030725C2A14AED641D78453E9FE5C6E6280D2CA57832FBB091C4418303CB4AA32DCC3C211DFC8A9BB429E750B2251149727ACBB112607EAB9B7744173D795B06D10EF8B1AC1AE54DA30A05188B0164B6B6E9223E93D377259163D62530CE9A0584F82E59534AC3E061B1B48BAAAA9DE1F8BEC150A63EB579DC528EDF977E7E421743F6A4C5DB61DD31334D3C63ED7755529064D67A4BA3780BC7C87731B86509738C1F7215275ACFC6E3B651FA91539B2B10111452570639E966766B64477C3F1E255390840FB9635500F2B513281E9D8B5BCCEB360F05913591899C1CBF95D23CB8D714A007990BB344C3A62446A80945D0CA84D3727CC8C39E0553A13B7050C12A1FB8BE6567930E0B6ABF6C484FD340CE916596B36BDC4085595CB2CF088467B0BA7F915A4E9581E7269C9A234F1A004A8BD0A2D7654018E55D6861B4067A29F6425FF1F5A00A804E3B3461DAAB9265D15BD530AC10686FE11038E772BC1CC46DB5C401FA76C1EE70165FB7C67C5025C2515E4182B46B92194FF361E252CCC43B060F50A64C79750BD162A586C5A63071D6E93DABA8BD525548F444C621A77E3EC583218788E3A7113DE38EB2ECCEBB0CAD737BAD9C21B39DB255458B8D6EE70C250CACC74A3A19C342B052A0FA5B92399013EEEC1BEEBC8E02F80C137030BA0A5DC8C3028B550925367BC732CE0E09570AB1AF01B7CACBB78A89BCC8C674449B932B98487AF0257928E64AD06967B38B30A0620FF9D49D2D63C2E7847E5B26046B457A7BB37CF012A7F18AAE0CAA41AEF72937B35AFDAB0174688B0724B25D67563536950081931A3ABE89B59297F701F246A6462B6016EA4EFD42A15C984E7157B5B4A335064809BD7739BAA23519098233C7454E241B23C4644A2AA1EB16E456E23567C4C3C6662ABFE76F52FE97F07F1298BDB70F62A5650A\",\n          \"dk\": \"74D0A332727A99FB07FA104DBAA09E120A80F2C3BD27971D5C8283E14C587A4C1064FBBD25919712C34D561270980352939739D378881F21813C077FD4A30A14873E01FA659B567069F9BF7BE0A94C9B2F62A9243C986F4B8671074BCCE5421FB12922F8B9BC50A9B4A4015F3B58BB493B34CDB7909817B0E1CC76F4B71A6BA45E32C03152D0BD972B6C557A3E84B62CA0F830DCF55FAE743AF419C75B253210347B00383043B3BC5B9425DA8151294C03FC422F1C22235AC1B3D487C84D63882A504CBBCA84CC192173225FCC3BAE53601AD905554BD1CAE026867141802F41111411824580837E35B5B4E47FA3173530A40285000E695610488407ED986041DA7B0059A054F1877BE8887CFC028417C1D78ACFF8A09FED7B289BF96F3F993E805CAB566CC0FAEB3A29EAA40A2820CF20912A1A4F3EE1C5A48C212C421D78326D367C6C41E7136E810F8A8B00E2514AC351358D574CD0F7344A121B05131B05040EBEBC08D0E203599503F3029EB3A586DBA5862F371B78852338281982B1CA30C70580ABC429D80EBF9A972D9B026FF24C387352F63943131AAE1A804B10D47059E00885EB670D462DD36BAA0FDB7E798915A0688D6FB3313B100445465E66E35CF664B060E399ADFA9712A32AC631B542B66145505627C95664A6117BE68E7BF3B12FD53CE02B43AEA449652A6BBAE782FD4178F832C766C09DFCA17686E57CFC563B32A43566C30BE1A85A923A6620FA5623ABCCF5133CED5499D424A67983620BC519BD3730C51B6E894BAAEB516A5AF16881619903B3051243013E6B8AFD20B2AA56ADAF436EA9A9A59AF2922DC07B644936C94B0F7E4057621519AFB8C53485BBA390777C591BDA7716C8B1383141197DA00CB9975B37C6A8DEFBA0E45563ED27336F3A19D64AB0A803A7CE5A5D5C2C513A1CB53434681AB0AF7AA96A946B5A598A15C6AC54192B447ADA0B1DFB08DBE72734A87AAC6B4F65CB9F2D319CC984AF4428A41825A504C1963B719548418A79918A5723514155389EE70E2DE0BFC46706918B7C0EE0022CE04E86B21A98646850A6329E8BB08EA78FE1633B4C64BA32C4B1D920C543CA6DAC053338716F2F194DDC8AC1CEF24EDD52CB6EF39FCD6B5B04C306223BBBE511CA8D1315C56942D4EC321C63331F099655349F174A0A4729841C8B5DEA646D34218E9DDBC48C968B6CD949587922D8CC9E94EB780055663085B02DB26063F14651218B31210A74C72CD273383FBB1236659F4EE69315651785C822B425BFDEB96D53D674118B249EE6AA963C31CCB76E9CB068C4681680FB6167EB6AB3F518CAA5175693843BCC678E0C5477D93CE95ABC023590F3EA9708FC98C7045C75EC3905662DBC485ABB02A23CAB7E36D6B48FCB8E43F74D507BBEC5CC827FF186111ACE6BB1B2802C5EB9552698C320B523A692F68112267BA5BCA8C49589352C17F117078376A1FDB6B91F9C6ABB3059BBC53796F90E0FA575DC203DBB893AF1B7AC43F2050BD56A8C4212F055AB9BF7579EB809BF9C71F64BB3D6314AD5E8643A4240F5AB35880905E9D4176D25567C502C4C103910F78356C1C47867A9E2AAAD057C909865614612AC0DB4679561922487116064BF4A6C1F1A816FB66471E6C18CE6126CD02BA76C730E721572DC730566496B3AC28F92A15F4C534C48578C18B443F24681BA2B3C3B390669A288EF7676C9A6B0970340F567C90407617B28A9A03274D8288006288B0A5B686FFCBACB273536941CD06927BCE109BF2330CF65A3CD0A2706EB33150A88504484FE182BC3C78CC5A7711845C3A277C763DCBDF16B19F1001727D5677687ABCF4B32CD161350EBA4206B2E1A65848528B7E1A90E04165BEE054F43793C23B0660FE7AF3E5964DC7C275458040AFC05EEC71A57E67C2BB794CE2680E7C4170FDB67F0F36DBA809F724B805BE66EEC38181FF1B0E9F94F7903661ABB17FD9BC0B0B19BCE2560E2C2C764F4863BCC40B5FC10B6513483A827C1668CCA8AA0B68039C8ACBE81B639E55AAACF2C761D78A77C2651573471434A14949943D945BC0ED2187DB442E41B901DBA0B14580710870B81B78D21996AFDDC6ACD1473673414E5C7C35EE60EDE3579223A7A2A436667CC51DD366DEAF615CAA4CAA073B60718B1E5B62ABF50BEBF4844EADC2ADA98CEAA8CAEBF187C2F26882673307A13CF83904607A8865A65592866499505051F10A4DE52893E07BABEEBAA4272B14EA974B5114101385AF1A011A488BA25179A118B312D8AC86FB58592D328941A3A0DEA6102C86F1E237DDB321615297A2BB660D5E0705A6672FD08A5CF212AC9B5B7A1306C87952B1C11B7D4E7C5B5FCB7030725C2A14AED641D78453E9FE5C6E6280D2CA57832FBB091C4418303CB4AA32DCC3C211DFC8A9BB429E750B2251149727ACBB112607EAB9B7744173D795B06D10EF8B1AC1AE54DA30A05188B0164B6B6E9223E93D377259163D62530CE9A0584F82E59534AC3E061B1B48BAAAA9DE1F8BEC150A63EB579DC528EDF977E7E421743F6A4C5DB61DD31334D3C63ED7755529064D67A4BA3780BC7C87731B86509738C1F7215275ACFC6E3B651FA91539B2B10111452570639E966766B64477C3F1E255390840FB9635500F2B513281E9D8B5BCCEB360F05913591899C1CBF95D23CB8D714A007990BB344C3A62446A80945D0CA84D3727CC8C39E0553A13B7050C12A1FB8BE6567930E0B6ABF6C484FD340CE916596B36BDC4085595CB2CF088467B0BA7F915A4E9581E7269C9A234F1A004A8BD0A2D7654018E55D6861B4067A29F6425FF1F5A00A804E3B3461DAAB9265D15BD530AC10686FE11038E772BC1CC46DB5C401FA76C1EE70165FB7C67C5025C2515E4182B46B92194FF361E252CCC43B060F50A64C79750BD162A586C5A63071D6E93DABA8BD525548F444C621A77E3EC583218788E3A7113DE38EB2ECCEBB0CAD737BAD9C21B39DB255458B8D6EE70C250CACC74A3A19C342B052A0FA5B92399013EEEC1BEEBC8E02F80C137030BA0A5DC8C3028B550925367BC732CE0E09570AB1AF01B7CACBB78A89BCC8C674449B932B98487AF0257928E64AD06967B38B30A0620FF9D49D2D63C2E7847E5B26046B457A7BB37CF012A7F18AAE0CAA41AEF72937B35AFDAB0174688B0724B25D67563536950081931A3ABE89B59297F701F246A6462B6016EA4EFD42A15C984E7157B5B4A335064809BD7739BAA23519098233C7454E241B23C4644A2AA1EB16E456E23567C4C3C6662ABFE76F52FE97F07F1298BDB70F62A5650A159FA6BCB8D2EF121A97A25B0607D94B3DC6D5D48B620839F143E8BA01BDFC55E8D41C96F1D340408D550400AF1CABD517EAC8447644605BD2B50A850216815D\",\n          \"c\": \"473A1EB71AE24A5F5F3A2FC86E9F48EFF07570BAF66E36C2C86453424F218BFDCD2338EA9514242A877ECC28BCD1BE87A71CC4D413ED8A2E3EE4D3209AC01DF03EC3B28FDF3D572B0F8713D41EF8800C3C1DD4B60AB084711F9B402AA34593D1549624DB9F895FA48314E0AE94DBF0EBE54EC81733DF6B3F5F38DF0DB91F051ACCEFEE0B32A26EBE37A5B12302F3F809121E879A7D0A3F29F5F9973CAFDD09220F848F03E2CAA1E64B6C6AC1D46A9F9874796D63738B11C91D9971AA2C1595A4148B145379CD0DD606596CCE45C334255BBD6761C4720870771CE40D8A9D51BA655854915F2C23FCBB0D40930B0EB27C96356A6C5503FF5453E10DC198F5D2AB476988030BCD2A56FC235EDD3538E997D50B3007CE0D28E46D2FFEDA62545F2AC5D6ACEB37B089C900CE167D68D358A445D1BCCF6429B810E74BDF07E04CFAFD5FD30E59CE541B2BE55AAFA1B928306715741BD806256E4D71F9CEE10A119D6C8D860866324901C8E582A7FD658EC83185103F7F0C9E9230052F0DAE235A93646F3754CCB6FF9D4E4E1CF47B262E5ADB4412376A3D1969CD7B2413F37382F665E642699696FA9868B25BEC0DF374789AE0B476E206194691C0EA5A16878113D39903FF112207B36F7617D6A06B864AFDB5C83095C194A71694623C31EAF91CEDB387F5A1DEE666B6EF95065222D6F98384FD59005B2807A68F3BC75D99D298C7A432B2160A2079B5A8AFB97EA67F37588FA5E246FEC3BF82E18A75370BA268A4FFD85B957D2A573F23BFB004E79ACCB100087065B33DEFCA73AB7523F17AD2C17F76773B84646D92CB3187C1F6E5B74EBC7C7DC71048C7358F21F029B7DBCAC8543CE74BA6C59625E7897832CEA6CBCDB64DBDAA3E603A9374F16942E00C2BAFA6A44D9451365AA1B9ECADE2963C8BCB79FD2F836229B8BB5A0385D39A017CFA6F875DFF4CADF9E9D9290D9B7BF6D0F3BC009972B9F15D330A9EFCD42246EA642209A712F922630EF2AAFE31414F0C4990513C2A208BF7D230ED7F9827C5E449E32FC4242851EB2D78E1EFC11C26A15BBD6DE677549B2939D075A0F53C3BECE462E56ED9324878928B58A85A5269025B9FED7080D77482A4AC5747310BD52367126B6322DABDFB5B75CFF41E4AC012B1FC141285D6533A78A8D13DEC193C1826B20EE5B7FA13FCB85E514FDC69C7D6E5C48633F2E7524F9420074719FF206D5E1757FF9512D4F8B44FC7242E9DE11DA99B2388D1B0F2327407FB01E5CDF0A24C94CD9D706E9DC8D0673DDC5AC818D96A221B732996DBA40BF68AE1FFBFB6FC1E0294908601D3AC88D76891FE62A93402D69EF797D2024966EEC8E6B96848F0B9E39CA6CC80E0C4556EDD85D6E70473DEEBEDDC4CC49DB1D41EF18B0B3B3F17E28B436C29460B6C9BCEB3492BE8ABAC057C03ECA4D266D60B173D206ED77C1D3480D7666DEC028F31367BAFE002C9F63EC6CBA2528D72220D5E987CEA74E9AF7B3072F681C72D2E6DE1DBAE3D8ED43DA7F2F5AC75516E38F5C244D42B061C3538\",\n          \"k\": \"E87B61A6496FDA38F948EDC5F9CA5735579D47F6355B214727EF5BEB3C13CA32\",\n          \"m\": \"62A2DA94F109C0DEF56DFB275B1A0EEABF82AF8C6CDFFA94085AA93015BC1821\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 48,\n          \"deferred\": false,\n          \"ek\": \"36C053F748B3A0655C3BA86303E099538971AB05BB4B2167D84B4F8BE79CCE515F28A90A5AD5CBC97B84CE10A39BB59AD2FB4AED7CAD205200A3C688AA32AE73F552A33B7DF1BA197F16CFA853C7FF24C1DD586ADE74CB0982449AB9815A63672C32420C1812326554C7219CA7698324115615C41D2E74A0138A42356232D7218549684CD9800F1A8413F3D952E78C915AD56FB0D1CC76D48F2ABCC0E6D79C67198D53B3B6FA57A4E9140D3C4286C97C7CBE48C3FCC81C6CD1540B12A077BC54F6F46711C844B46559194B5597A815827307E0E58AD7C4CFDDE826FC3594002A88C83760B56331A61C542C7B4487A851D7FC4D6A4072B7FA7C77647F8F252337252B1C9709517A7E217B26F26984D8D0AF75C9C1012977A3996D855A86C47266224C2167E585C9687C0FC5259E5BA10FA23DC4A2C163E74EFDA01956965A5DCA57BCCA2EBD4606B810882A337CB33347820C937E821E75A5BFBCC75BD1E37D81342F8248366945C0AA2B98F6E228A3AC6EF9CC28208470A3FA56C2F51AF83418E3D13212BC2C64304B4EC55001BC3620635C989CC4E4ECA06A453C8E5689BE8CA010C4026FE201EFAA271C2A772563CE7147932DFB7256F28975E2829D454BB2CC64AF8684B9ECAE285A93EC2212AF4CB65E484461D2B2121B15B37AC9446A8308437B887271564BC8143A8AF1F912BAC58E06AA8E03764FDD823180E30D47056A6D217ABA6533AF4399BCC798AB59AB0880913E14895526AD2F06A6D6536A0DE701D7DB9B2EAC7466FB85AC0020D961B39B88A8DFFC3FAC57524170B70490876F3A6FB16020EFBBA26E17C012AC4280C46E7F643CC334BCC302BF3BAC10A3891FF0F2BB81D29D8E421A71272C0F2439AAF29916461DC24B4899325FF005C0E30085AAAC7BF21B58B114BB0025305C8A1DADE9A1811329792713C5236B44B52AB8791856C535DC8B0E4DD0753689BFE0636DB99697B074BD2D3A8C4AE69929F00B8B256617FCAA96C980E8A629BDA58CF6A76DA49A004055CD4BE21E2C02B5533A7FBBC4525A95167175480A7B09BAC2BAA47A03D4872620C191287932029C9B53737A69148B7F483C607148DB23594DDB2F149ACA20F13AE693C2265A71E911A4D22ACB1ECCCC4C96B1A2C0CD45A5AC25533AF6829919D93770E42BEAD230BF8739B48C9990ECAD9E679C501137C49019C2F857C9D08FD533B01D50AC6BF2398655A1CBABC670299ABF46B95913A845151DE2796666BA6824237AF61789857CCAF0AB0F4B430FE966C85818B11E469CA2D7B705833813B8A775C77138F91700C64BCC6360231B1540F77113A33DA08189E454465474947EAA7F6731613BF9AD6BCC6D7DD1B62E1A3AA67C647A62712BC9CF67FBB690205E6E6997FDA7719EB9994D199505342D42FB3C6AB1936825534980768102619D79B9F197CB05C16A9922B4AD752C4926539AE184756114601A6ED90443D2869D0FDBAC6F447D91F37FD4E6649A90B70C73182B6162375610E9166AC76656A9B9242DA02CEAEB970A1803BD570F48B7B7557928CC4AC5D494BCC9386078183675D760322A91A1FAB770198A437C052EF366F6C905062010AC09AAD9594F581036A7E11B0D6C3B40BBBBC340CBF6D130BFB4E7CE4696BDB01ABE0436AC41B279FD576FE86BE94D213F70\",\n          \"dk\": \"705A6AD224AB8C18A1DAB7039A7282EEF71B9AB515ECA53B257B43E39C5890B131027737F089115599CA3513CBFBDB536FA39A872CBD8DC10CCB3CCDA36CB910902CDECB4522AC2146A39EF42465921A767F56670EE297CE2A3C67A24D7D55540DBC15F2500831F384E9D9646946A4D133CD0686A9F8AC3919C36466AB2B72E668B1659A81699B5A9B62C79905D6F2674CA471BDC657FE973FF95437304708E88B0805858077D28BA3632821239B2C3174B21849A0492E4B1B6A42D39C656B41277B0D8D868DCE06906D82210F9424FBD1CFD81134E503AFE93AAE02574AE1D32608D92CCFF667BD87B45F58ADD75B79A2912A73672F0A9794086084B9F58F21B7137FF42333B47661FC6695B56AC18996064606B37426C4E04A9173425B985B6214ADE879B8A0116309466BB0408DB243A39E123F3EC68E7D47C7743A22A062A12F9224861069BF8C50B80A1799AC28BDEB43FD0A3F6C72BE1AE97699A1607B68644D09A546945B57E69DBEACC41165C8E221031DA43A41B8312E3A1C1D1C7329189EEACB0D9946600D63B533ECA1EFB11C3E6C12D888C0C8C331553361E9623CE2D29854FCB3064A9054C93F7142A8862CB83529183BFA66309858444293CDE44CADA626AD100DD0D76DF7C07B455C4A7C3B04354529445C9D12B65B0C59A7CE3C7F350619C1D3A65AB314F624A0F456715A1CBFB970BF0A571F0B9C78766014FEB46CD750C52A06B5CC8B5FFDBC8754792E44B7AC0B6332BEF32C57976688800EEDB0482062011E99C801CBC5A42C8768624B84DC4FEEF6580CD86275891B460218A8E652CCF228251B9ABCD9A9083A5AFE088BA85A0F1F10485784903623C347F9554EF0722E605C2D8012CCA89822500EEA037C3F319F5EB8A070373F34196842D96BFEE44E1EC9904754A3A143218E23968D28A83F91CE70908AA9178336E50688A446BDD85E1DB69DB9018A88D427F94474BAC3CD473A687201801D590277736C10047ED9790A33A39FC3267EFD154351956374339F50D75EF0F61661B68E7E59111D153505DB69E65190F80C9B64CBB77B958E83FA82F9CC6A945B3F46039D3B368E18A34CE7038D597C2ED16833C3F7B286EA17D58CCE86DC4FB7F1007E9A4C64F573AA053F778B9ED71825D97C8E1C52C953B08942A1AE30935F8765787F44505FA38B9CD93258F0CA6D34269852786CA82D980BBD72254D4D0A9C1A8559420C232E98CF222A469AC9893B6643DE6A499A53B4E73A9D989A615D3A22507694FC051EEAA27274BB3F19938758E90BB83B0999BC43A90B60D424901B7522220414D5D475AFABA945DBCD83807A58D3B5C4313F9F7027EFDB2299E873AA6A18658963691070D9A6A8C8BC061FD9935D521C0B319323F1B7209866BB052207B6AE8FF81EC8C12A3E035E29404FD28086EEC313517BBCF4A173F3573D69BBB77B177615874C0C04C0138C83AC8CB576E4C29363ADC8F88FF29A67C44952937B7ED61A1A0F11147C345555DA1089B768D7A0751875B44E36A63488A23C281FFC518E4A548D3EA6491E634AB97A5EE71805F5A7C037E2B2720CD037A2501D9C239DA14A618C2A53DA7C7D096EC2D0481B70A1D45075F2A000F5C60EBF9A8F36C053F748B3A0655C3BA86303E099538971AB05BB4B2167D84B4F8BE79CCE515F28A90A5AD5CBC97B84CE10A39BB59AD2FB4AED7CAD205200A3C688AA32AE73F552A33B7DF1BA197F16CFA853C7FF24C1DD586ADE74CB0982449AB9815A63672C32420C1812326554C7219CA7698324115615C41D2E74A0138A42356232D7218549684CD9800F1A8413F3D952E78C915AD56FB0D1CC76D48F2ABCC0E6D79C67198D53B3B6FA57A4E9140D3C4286C97C7CBE48C3FCC81C6CD1540B12A077BC54F6F46711C844B46559194B5597A815827307E0E58AD7C4CFDDE826FC3594002A88C83760B56331A61C542C7B4487A851D7FC4D6A4072B7FA7C77647F8F252337252B1C9709517A7E217B26F26984D8D0AF75C9C1012977A3996D855A86C47266224C2167E585C9687C0FC5259E5BA10FA23DC4A2C163E74EFDA01956965A5DCA57BCCA2EBD4606B810882A337CB33347820C937E821E75A5BFBCC75BD1E37D81342F8248366945C0AA2B98F6E228A3AC6EF9CC28208470A3FA56C2F51AF83418E3D13212BC2C64304B4EC55001BC3620635C989CC4E4ECA06A453C8E5689BE8CA010C4026FE201EFAA271C2A772563CE7147932DFB7256F28975E2829D454BB2CC64AF8684B9ECAE285A93EC2212AF4CB65E484461D2B2121B15B37AC9446A8308437B887271564BC8143A8AF1F912BAC58E06AA8E03764FDD823180E30D47056A6D217ABA6533AF4399BCC798AB59AB0880913E14895526AD2F06A6D6536A0DE701D7DB9B2EAC7466FB85AC0020D961B39B88A8DFFC3FAC57524170B70490876F3A6FB16020EFBBA26E17C012AC4280C46E7F643CC334BCC302BF3BAC10A3891FF0F2BB81D29D8E421A71272C0F2439AAF29916461DC24B4899325FF005C0E30085AAAC7BF21B58B114BB0025305C8A1DADE9A1811329792713C5236B44B52AB8791856C535DC8B0E4DD0753689BFE0636DB99697B074BD2D3A8C4AE69929F00B8B256617FCAA96C980E8A629BDA58CF6A76DA49A004055CD4BE21E2C02B5533A7FBBC4525A95167175480A7B09BAC2BAA47A03D4872620C191287932029C9B53737A69148B7F483C607148DB23594DDB2F149ACA20F13AE693C2265A71E911A4D22ACB1ECCCC4C96B1A2C0CD45A5AC25533AF6829919D93770E42BEAD230BF8739B48C9990ECAD9E679C501137C49019C2F857C9D08FD533B01D50AC6BF2398655A1CBABC670299ABF46B95913A845151DE2796666BA6824237AF61789857CCAF0AB0F4B430FE966C85818B11E469CA2D7B705833813B8A775C77138F91700C64BCC6360231B1540F77113A33DA08189E454465474947EAA7F6731613BF9AD6BCC6D7DD1B62E1A3AA67C647A62712BC9CF67FBB690205E6E6997FDA7719EB9994D199505342D42FB3C6AB1936825534980768102619D79B9F197CB05C16A9922B4AD752C4926539AE184756114601A6ED90443D2869D0FDBAC6F447D91F37FD4E6649A90B70C73182B6162375610E9166AC76656A9B9242DA02CEAEB970A1803BD570F48B7B7557928CC4AC5D494BCC9386078183675D760322A91A1FAB770198A437C052EF366F6C905062010AC09AAD9594F581036A7E11B0D6C3B40BBBBC340CBF6D130BFB4E7CE4696BDB01ABE0436AC41B279FD576FE86BE94D213F70E1D563B9DD64A334930BDF5141DF65BF77A06052C9EA81679080E231A8A61E0B97442F30F0F28F7A851B0D3E76BC74DF890916D2ECBA20DEBCBE3453655F78C9\",\n          \"c\": \"07C9FAC7CCCF6B5497C9BCE51371F26B574BE236DB8009103A7617953AD68ABF08A3F134B5C2807229AC884903D3B6B2596020D6C789FE3CAC6468EE89F4004C037125BD1F848B1731424A94574AF2A67ACB415E6EED82167C590C61DB7B34BCC571178794DBCDDA2B404B4F4A25D17EA1503A820504BD0819F248F472B48FF54B3CF01F8DD743ABA8495AADD848F1F8B3114614463FFD7CE3E9726B9F13F2A4DA5DD7B761C484E2C98D457FF788BB5DECB6C9223F112BA6A5064854056D3884DFAA65A677C13E785CB30925BD5878A1515087472F285969D38B937458F7A8C968BB86D8AF7EB851EF950F83D554115E84D743A886F7B2922D581499B36CE7A049E35C9CB629889B626620872BDAF1B31B1BA08545CC57D2680B17E21F0FBA6EA16EDE8B956E497DF4C2960221FA3D697BB33CD592BB3D370834D9A5DD325ECAF88B87E8249CC70643FC807D085B357105B235800A0A7260267A9C1888D9CF620AE27315EF42A808BCBCE4D705C63EB5319530B228FD233BAB8C53F84277037A441ABC26EE386A06028BF75470D3B2CD441E93547F519DE930EF1871F96FEB3210FBADA58A39CE69417137A9EC019A12CDC5DD340B613F6DB2C08AD937EA3C31B553D40D176CA69643ED16CB525A1FBDA92FF6FA87528DC20022B75B99ABB49A5838022F271698EA91F25D3613A34C686712A9327ADD20F2324E3A32C5C33F234F879CD28024E312926C9B2D5C327AB26A29CE4E4200D23B4BA7CD541370AECFAA6A20AA025B969EC6017033B32798CC20E3A2C69725B5262ED9B8384110FFFE13687EDEE0AEACA60DEC2576CBE150508C25E69796B792F28A08DF7A1949FEA5FEA4AB9376CC4C3A604847CD69B1ADFD171983D6E894FD886CBB3F1CF3704ABA6EA413D0846CE803AF766B67A37A058F95818AF1FACC2AAA90DE7CD503A188295701F1CF204344E31EBDFBAEEF2521201DBFA905774C31791F7A766E4221611FA3D0ED8F0CC491EB9A8B8A9994073D746619FB2BD6A11F9770C0DD00B17D56234240CF014BE52ABB743DA0F9CCA508BEDE7BC5A011DA9F24C55F1AC2BFD18A813CA6A10985FCE51722211B8A6FFAA3C793D9CA4675F56B8743D454F78FA5EAA75CF80030905B844ADF4FB15EABB755FE5BC18523C8BDD6CB75BBEB3EEA082D9A7FBF97F409CE8B92F366195C80EB216C052C45A1915530BA7B9ED90C6ABB5B13B723759FCA390BACA96C582DE3B3C5D4AB46A9DB5E241935F12284ED11BA6AACA5988F39D2F0196DBE0C15640DD2FC44F206C60C936C53C4443F4BAC175BF5C60C3FB28ECC4862C03AB3AC197577B3CC4FBA1289733124E8E247392D7C87AD9E365A46003FC510DD9C71194600BFF8AD87792F251E9141E577A274253ABD987E239B69A59CE5277576236423865B59891BA9BB053CC215CF0F1885BF5E54E33CE85A9C2D915839616D44CAB2E34E1E4509F49FBA104E0CF58A8327A6A7C53A2F3508E34568923B4CD21E1B31D7B985A290D08422EFD30715EC0F6A0EC0DC7789\",\n          \"k\": \"2418BB42B89BA875664583EDF241327F3798379BD14B64351044F6C96B3D2C27\",\n          \"m\": \"F374D3C7172C308D7AC5AB1F1CE5BB9785B98AFCBF4E9120B42EA83BD3BB1867\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 49,\n          \"deferred\": false,\n          \"ek\": \"75F91DC864B819E71CE8CA50A7BB41AE94818BAB31B7F888ACE44071D2795361CA2B2666704721B02558212AB41A300B6D80B332A50448E36786D101A68A94D42325F718CCC4EB3521424C35E02A0A1A7A3514696C547E1918982B1066A3CB633E2B1D23280CDB333AA7B3C4BE7C18AEC44678D649C8774A4C552E45948801681F883165EB2C5E9D43CF38628E8C934C5F30819D4287AE4A51A797BDB0769534169E293C3A76A76361D79DEDA8068FAC187EF6B647C148E919141F02564B0AC4996B27794915A2D1C669DA50DD9B6E9BB57612CBC83765AA962068B8477D703848DEA456592A89B3CC321C3B86C55AB980BACD2F2605395BCF9E965B95D59DD608A8B8B85F66C8822F5B2DEDD36D3F97BC1503686F74991093A5799A146B434C7C2A6C573B3AD8C44FC517C92D042D623A7414D56DC45454CD08919FDB8D79A7316A4C14AA9961DA76C9DDC4CC6CDB37B474835E9A86563212F2B3908875588E2484BE8C4A49E3BC5EC609FB4BCD52D62AFB1B713B556CDDCC8BBD3B322E1766C627A0410A3D5E0B6F3E499272FCAFEAC90CDD100AB5D686F22023F8413F64521D19E38EBAB1CA6E31CC10278184F50843DA5CB1BB5164DBAB23374065B1043B1A7B1A241DDBD4BC6EA9057CC1A995644AE73231348414414C7EBC58BCB9B538A091434DC9010904BAB0613FA640C0A903BBE57A100B08A44A70507436C366268E7531C84A977B9EA2263E770E98306537B505A01284F653C066BA2F9FBC6CB3740258892076C01FA5CBBC8BE97FF0830BC746BA27555305D17A931371D04873CE2B89F5694BEF68515E7552EE03BB8E0517622768E6022DFD1044CA86A715930530E504D8031574087572974915885CD0351AF3610A5154559F688492579BB723A22F791B089C2CE3A383C09CBEFDC151DC266DCD54C196F25F0EE34092109C6AA6CAA76373E9125E143B2D0EA0874E4A35F06961DD812E60700F598926FAA7C1F7CA579CE9AEF3557F74E2039059258A001FC7F3B4A4ECAC6303672F12B9CC366E448124A27CA6FC43C6157A378535817B8B0C981809ECD54D92F135D0126480804EFFE40134E6C2190B95BC7C5E6A72B6487A6A454239DFE10AA99614AFA6A8F0E12FA86007CE464AED4063025B213D8597D412C9617AB62D9592D4320D7F97A6AC4092DCB311C21636358172C43B5626642336A96C1466BC30426DFAD5C0DEA50F715B19827B1935C79CFA1C9CA1F799F3954EB1C0720D134CF9A3122263B995E9268C3A1BE18A7112ECA0C4914F2659734A31A5D5708F3A145E6801C18761A280465C2D4600B9D968B3CA61EF9C7687FB1597499597ABADA930AEA4666DF40029F645A601984646144DB4CB68B3466CD7DA154C027536886A390234CA792FF6AA5C7CA258831AC1B0FAAFC21890BFE522A7685837A550CB343B79B2955C4ABF4939792133AB0D4A77C909BE0F0233AA80AC6B6AAB0F8102F374982C6BA88563ACF7254760263D7EC8C302F656BC227B3B558BBB444365A2CD95B421DA5914417C50E84311C6A2218A49169866518C201622F10970D75D38815787355A0C6126EBF189AB30321C15898AB117F6183C47174DAAB7BB8F96C3F4F239B642516A48F015E838A3DBAA500DE409C13F28FCCE5F266A98ABCB2D92E1BE99E438BF\",\n          \"dk\": \"95A85BE67984E9D6BEA070223A469C08D73C6012AD76261380F45A7DA43CFC831E92D55C566BBDA6E6B1BFE83CE7E5523829C5D3C2B5CF874FED240032B43A011823D38856E519868CB0A9B0433A51167835568EBD67A2A6991B8C529E9BDAA0A4A95BBA93480909AF3931B969869C3A425669B412C60B5AF2D6B874C98E01DA95FD0672599303E25CCD42D7B19B73508CF286FAB706074A98437B7C9B7357DE242200AC71FA8636381623233135E530BE92E83133092B17A26F688919C5A45634971D129542FF2362E94243F2249B65C9B2F9729FE6425E76E36FB88019C2A863CB617C58DA9209A92C2A405E35574198E83739772F63C97CD90753795C981C81BD3CFA384A6CC51DA15F21D7BDB87B6CD2BA10F928C9AEB1595A4539C8A3600331B07256680E4C0648A0A786747E00672B08DA47F29A3FA7D9CC2E119BC6EB579BB30037F66ABD645951F499FDF12ABB3C979FF90E15A5397D2358BDD6CEF02B5658B44242A74573D41C216554CFCC2CC4CCB48BC488ED5A450210CDD0FA1D04831E14514BB93148E01BA975B71E5463001CA8330AB2064F31346C716ED431BD91A477C660535D88B32E3AAB39C4A9DDC2BABDA392FDBB3DDB3611E06A42E1AA18F985B8D202C23A5443F5B6C161579B6FE0C2A1A20347B2A48D77CE8F954F7EEB193470C90A66B3EC7266AAE78BCD2469A5573ED4D2CFB600A7764364ED47C4C3DB97E5E5170361C09BA07E7B3B16D07432A7100F0615506D32771A7258F1D33B71101941A148C6368C9DCBC403037037F6BE9620BDFFB418BE497942C410C6E6A957E604B0F4467678B83BF9BBD628BF5929A63C6170C065B6F5D84D7E2BCD8EA019ED72A600E6AED7187A1CAC1506F95127CA235AE9ACC89B2CD709C18FD895002C3B2400C38DA16DFD7B2221D9BB212A471475845A58B7342830DC49B6676516EA9BBE4C458BE77B6A99E518AB1C1BB8CC54B50392FAB00367940B790C6AEE35B587E6060ADB3C92FC579AFB459B3992B6E110776115ECD03C625B9DE1378C85FC5EA0C8AAB6818D123A5F20866984D4C6D261156E78A15D322E9BB4B5B288B6E6C03ADD4A9285ABBB460C9D2C0629AD7CC48351BEA588848D95283995CC7C081A8BF453EA62B3AA48608AC2BC02067C93D01CBF1531C0B35F52C32BF1748E56185FC4C24127E9CD94A9C4E8AB12C38086F5A17B0234A41A6C772466255B1C8F95C4BB4D7048E9A542D8B7B36DD59CB3F48F08891B2BC6590A179787CC83E41453438CBC34318BB0B072AC6A013571270194508986BD5D74724BB22724B2B18AC92A3939C9589A03448B8C8E812C4E2486501A97ECFCC84D3A32824160EA183C16F3839FB20CA8FAAA4ED54FE9625993F1A064809537525F9872AB31EC9B6AB922CF6806714959AC63A89F51570A98033585A99E72713DA18A34C57A2E5A558F56A04E185864398C56888FFE66ABB96BBD12B39DFEB0909D0A932D6943031693F0296373246D35767E74B4C29EF52ECB1B371B77AD5588AFEAD9CDD978331B7A51E4C4CFBE9B17550C20DB02742EA8737D8C26214791F4EB3D7CF1329E45600542560E9A350CF74412F1681178A0ABEB6706595084A19D8FD5C99811A275F91DC864B819E71CE8CA50A7BB41AE94818BAB31B7F888ACE44071D2795361CA2B2666704721B02558212AB41A300B6D80B332A50448E36786D101A68A94D42325F718CCC4EB3521424C35E02A0A1A7A3514696C547E1918982B1066A3CB633E2B1D23280CDB333AA7B3C4BE7C18AEC44678D649C8774A4C552E45948801681F883165EB2C5E9D43CF38628E8C934C5F30819D4287AE4A51A797BDB0769534169E293C3A76A76361D79DEDA8068FAC187EF6B647C148E919141F02564B0AC4996B27794915A2D1C669DA50DD9B6E9BB57612CBC83765AA962068B8477D703848DEA456592A89B3CC321C3B86C55AB980BACD2F2605395BCF9E965B95D59DD608A8B8B85F66C8822F5B2DEDD36D3F97BC1503686F74991093A5799A146B434C7C2A6C573B3AD8C44FC517C92D042D623A7414D56DC45454CD08919FDB8D79A7316A4C14AA9961DA76C9DDC4CC6CDB37B474835E9A86563212F2B3908875588E2484BE8C4A49E3BC5EC609FB4BCD52D62AFB1B713B556CDDCC8BBD3B322E1766C627A0410A3D5E0B6F3E499272FCAFEAC90CDD100AB5D686F22023F8413F64521D19E38EBAB1CA6E31CC10278184F50843DA5CB1BB5164DBAB23374065B1043B1A7B1A241DDBD4BC6EA9057CC1A995644AE73231348414414C7EBC58BCB9B538A091434DC9010904BAB0613FA640C0A903BBE57A100B08A44A70507436C366268E7531C84A977B9EA2263E770E98306537B505A01284F653C066BA2F9FBC6CB3740258892076C01FA5CBBC8BE97FF0830BC746BA27555305D17A931371D04873CE2B89F5694BEF68515E7552EE03BB8E0517622768E6022DFD1044CA86A715930530E504D8031574087572974915885CD0351AF3610A5154559F688492579BB723A22F791B089C2CE3A383C09CBEFDC151DC266DCD54C196F25F0EE34092109C6AA6CAA76373E9125E143B2D0EA0874E4A35F06961DD812E60700F598926FAA7C1F7CA579CE9AEF3557F74E2039059258A001FC7F3B4A4ECAC6303672F12B9CC366E448124A27CA6FC43C6157A378535817B8B0C981809ECD54D92F135D0126480804EFFE40134E6C2190B95BC7C5E6A72B6487A6A454239DFE10AA99614AFA6A8F0E12FA86007CE464AED4063025B213D8597D412C9617AB62D9592D4320D7F97A6AC4092DCB311C21636358172C43B5626642336A96C1466BC30426DFAD5C0DEA50F715B19827B1935C79CFA1C9CA1F799F3954EB1C0720D134CF9A3122263B995E9268C3A1BE18A7112ECA0C4914F2659734A31A5D5708F3A145E6801C18761A280465C2D4600B9D968B3CA61EF9C7687FB1597499597ABADA930AEA4666DF40029F645A601984646144DB4CB68B3466CD7DA154C027536886A390234CA792FF6AA5C7CA258831AC1B0FAAFC21890BFE522A7685837A550CB343B79B2955C4ABF4939792133AB0D4A77C909BE0F0233AA80AC6B6AAB0F8102F374982C6BA88563ACF7254760263D7EC8C302F656BC227B3B558BBB444365A2CD95B421DA5914417C50E84311C6A2218A49169866518C201622F10970D75D38815787355A0C6126EBF189AB30321C15898AB117F6183C47174DAAB7BB8F96C3F4F239B642516A48F015E838A3DBAA500DE409C13F28FCCE5F266A98ABCB2D92E1BE99E438BF3220B4816EF8681B4DB93059811DA8B0D65AB12AB874E57F3B09C33BC6C20A028449B1C5A6D50E3AE0E604C9CA666594335BB1B083669CB54EE7E960D8905C8B\",\n          \"c\": \"36DC4E1498A52C255C5AC4A9A1AE3F17F8472B71548C919FB7C2F3ACF0A35D6FA8584307282CB7FC9B5B04E5E1047BF03686981708A62ED593B6DAB954F39670036102A0BB348839857A68212C783319678819F6CA6CE1191B34D86157838523BA0A69B7B695139C17C21FECFEEC191930E81C66D1177A277E77D714B2EDCC33BDC89A7F14A6B83862300AFA860F229FABA58D2B2E8C89C734591E7A1CB1D65D87C6A0841B78ACD1BF05BCCCDED0F27B38DD27E57EFFF3BC4C3E80ABD4A78F85D233EB8046FF2BB7EDC87BABBD280D1F3650A3BF621A23E0C3A7BD42F8AC90B0F741EA3B3E8D65F0B1774FCCAF35FC809514F5D4EF303D821A2DE926BD700B085B92BDB91E8D04B6EC23C8000D8E1426EB7743D999B845E79CE993A83F202E2E82B0B491DC6BB006D9BE69311BC5E980A95EBDFCD65DECDF42B887C29281E8F8C19607175080D04485E5DFD0F84219A5FBF8E94C7C16C44F10274166CE965AEA357AC9C5A2BE63E47603F6D4D5469308793D8E3F6ED473AEA36C68E3F9FDFDB97996B34020978BD69410961FB216DF9F3349CC6B6028ACD3255EEC2C14F2341858782B8C34D3141A19F8A64E989F70A6B67B200E8E482A2E271077489B7C97D567449E2543CD5735D7A70DC1FB26539B996A550DB92C9857C9687492238B108B40548C22A5442150604D0C097FB02061F5E45552B4B443E7F31D85CB5BA9BAEDCD839445613E9C9826F1427755A36988A7E175E729D801468F986B6A0553C00562606B032F7580D51AA5DB6CD214D69E8636A8FC957E7D3907CB5A33B2C80780B7D782E040B9914A7B8EC51AA7711B9D27581FD2D5055E618467D2EB94B4FEECC66C96268C62222E3F438293C2733EB921706F1D669B55C3EC9AB54F6507A4E0D742494B8788988E9A628A5F5C1DCC2C5AA659723006BDF0ED4E70A8CC194416E11F5BC812599ADB937A25B302F6A371808443377EEF2E2A3F8C7A3C7294EC7D7FFF22DBD3BE07F89207B30228A20DBF432CA96549794514BA4BD27C0FBAAD1290505471E7BBAFE53E039B7476D4DBADF13F1DBE83A9101F58351682AAEBB435C0AD8FD6148981AF7885A9F49E7A802BF226182597EC5BE5DD5E0381C1D72CAA2678D0FC4B354469669C271D331C328560055057B25F92FCDE0BC378AE6886434BB85EC96FEDFA369DDCE4F36F8AD8FF79894A1628DFC68CD53542EDDEBC0D92BBFE2F7601621693B10AA2A3CE2ED2CAFCE6BCBA4E7B0BEC2E5656F1ED4B751F081BD4A0356B89815F724A823030979AC1D6C8B1CEB63980717EA24803B2D6E3FDD9F5B4A3397F4DA86834D99FDD91661CB1A808A197F9FF122B2843B0E4230230604640D82FC26AF612AC980E6516E1B6C5724B579154EF2F770983C0BA33B832C14B691282B9381AA708E49D812F7EE180B0F9858D4D21386C5D60490083ACA768181C2840E5EA30833C3FFE79E2747892AB225703396D792BE152DE2E8802264B2939296A9E1733883F430F372455D520B839C0DFF54BF76F2BA87B4A5960\",\n          \"k\": \"2F323DFA37A737802227ED21012FA0BA624F532F8A3DD979AEFCC554C1C2BE92\",\n          \"m\": \"DD252F728FC9553CFEE90924565E984C8E1462CDE58AD8C4ED8DFCE98A7F39B9\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 50,\n          \"deferred\": false,\n          \"ek\": \"19E628A96B033E4358CFA8C3C4A642D7127C16A50BEEE5C57A25476D8BB86D019DAD84C613D96A389802EED2031B1A07D478612DB405BBE9BB42A4BF9E7AA91CF2950D00013C4CB7A9B0CF88A87B19E41995EA9EB09660746B1A81A2C74F0BBC28287851CCAE8CB8118ABAA627F44289A9C355DB863095B397D1C449C20AA60B5F1EF94F9002B212C733D81C5A92631CD3B661B11056C9026E84A34E19836411A76AA53089071194E2C9696B31BE8351CB735521A42CB83C52C421724F2FB76D245BB5571358314A2967604084F64AD30911600B33AC9B236DB66487058F35611D0894051F31C032972A03A1CA5822317F953F91A74885E21766824F0FB601C593BC64253E7C36639462721C232D32A076EE245B9A7BC96C0A70ACEA36F8F41E1242AB2D00593BDA4C550018B1345D0806A3D1E8C7A8CC29790127370754A24A73E3A4B08DEC3A88C04C2022235E55CDCAF80938B579B63123E306603C781FF3ACA1105076AA2BBA1D966BC28BB3F3A7340D210D7E7A1735B01E749C0AE990775E2306AEB80229B918542608B7E281B6BB696E1C82342BA196D15EBEF20D541CC57CA928A9DB41E972CC1CD333BC7A64952247FCBC578860191FA67971A29E40C06B81CB5A765C7B26522C19A63C40EC31EBECCF7DF44CDB8B4E7BC805D42A28ACAC3EB2684ABDF46DB7E4C5B01A770B4C0D79D7190B97461BB75CD4A78574677CE26BC30CC25215B1C20C290A1575790C86AEBDEC7993CC94CAE23D5FA2871A184E721886BC920CAC11607FE96878971F161B788E5653698A9F8981339E581969C1B61A6073C02CC3822A2A2D0A6F5BABCA0D57175B86942CFAC2B0FC9F1684324A4139CB1342D7A6B26643A923FA14988173E58A82B67532C6AB89C0D5AA1CB47E44703FD7AA6816501023229B1E76833D7948F0743BE373BCD361130899010727CDACD4347B3A6645541BC7173361860953E61F5824BA2ECB94101383DA6A95CB4591C1BAAAB1B221F43B91D7985A173A0FDA756FE9B38ED1EA4F2A428E34F352A37A45EA83998B66C4E60B40650A87FF5185DCB0055B9C011378442462C285D18A0DBA658DC5B17D1620CE566C86879F38754CC8C1BA014827B444CCF8E9029A7763318CBFE0220DBED60F6720C6F96B4E70C712B58091BE3A6C1C4639D1ABAB09674D2E653DEC768D728BA593AA3C61B17DE17A07B3E85558F38DBF848D65AB3A1AC183A62AAD4CB18344E71BE4C97FEB59001F4ACA46342AAC389B22C47C2A216D9AB50B1A02405DA01494C44184363C31F5C39AC90507D94093F882E6891E24571BF39932DC5482554C12AF6A5F0B3AB4CBD276D6D850894CB4F414196D511731D23C8E039282F6C11D089BEBFB91F0A766935B3ED66458842B4C757585FA6657947529294930C886A50689CA5DF1AA39B8346A14150F051E5462A99E56CA383BA5ABD455F00AA9BBBA660FCCA2C5A8A74299581C274B89E4756552570A33514CB46CD7A3AED34A2DA42C49DE1A8645D64AF3D644BE586068A1B5804965BB7A6134CC8DACD82FF2E954625C92B0D9763E22CCF7450A8FA70F5793400D2832A03A5A75D404D1E2822DA607F6F9C4FB510858978372C621AA72399A34B9E2618F97EAB82B56D93E51FD73A90A78E2AC85826B8E6335330DEB8C644A29A1\",\n          \"dk\": \"D9758470207110D1C1DE602163F955BA262CCF3564AFD86DA03047AE9C63F5A67587E008CC3423D221949505C534B6998900114A3B6775F1715AB139CF4C8C5849A034929F957407C4512091930A5B9A38846402AAB49DBC39160B0ACF4B6A3E9EEAA095917828F28F667C9904B8A7BD4C231652A32343BEA81A7EEDB68AB1229FFD16CAA46B67B9F264150AC2DB97CE09B55E4312654E9012F2699243F41B380073C6A5985F87125DC199BB05620455A2BB77222764C9D7966CEFF2467D632964567CBD1B98FF062CEDF1C23A31B891C48C0B3833660C3C39762C4A470C85F802C5A03FCE328B21A658391A7B417B2EB5B37EA1262817EA78BB72C833C08CF74C9C34075AC0B975AD6ABF2DBB1AB72A9EE5758A1E47B1E5B41506D39995D04CC942A0C2306BF270CD43E1373F82B2DFD8A0294BB2AA37C762684969B2046FB04DA621AD990336E2FB3E53780B311872F9E581B1890A67EC4951C071FD7091DF250F06F367220B0EAD226286E6BF1DCC63D0E85A2C64B39BF924C3716F045411A54A31EEC41BE1846E08649EF19C78E1A938CDB1034BC5879CE91444BB126DACC12EA2A32D4C5749E3C6F9E270F1B4B0E2244E3308014499CAE3640E96E4ACD5F1C641D5B2E1CA4B9D65654CBC2A53499E643A9E9E1A2453EBC707A20E070793FDE87E12F71FD060186E5690E5B031F858CB991714DF959D6269209454360471C4E1E365DED681636B02592A2747F78EFDBBA0841C54DAF980CBF728EDAC5EA7B9556AAA7746CC209408495DA8547E392A4A02887618A7F7C25537596254D48E9AD94A52E1A334A06D2A22A358D1121C9A81AA6C1EFC08B3C8356212F259699163EBF1A31EE845E13AA10652B41580BAE9D683E8B958BA95257E947C12B04DC73299EF510FF5C94F598524CE790D21372728B38AFE966AF114C2DBB094BA216F65D2476E6B5898BB2C72A0C8C1311A81DA95C0B03D942C81ACB73BF13A7B1C22B289C0A5DC7C8E9A71480600BE6F5A14BC7340C9290C03F984EDA2A8499534FD0BB96BA1B4D1335974350A32FA9911396C3BEB940AA1A87C86C3F08C393A18CEFE7565EA6402F8C0914735AE938C3977576CF9941070583015A8499E84C588F13F9BB29FD278659759A08978585CB33D213CAC9270BD7AE3C6A1612523C8ABF8F7C4D3EAC1F6F7860F02C9316005C647B30B7C3E75CB73CBA301F2DB327C0B38F7223881B70456834565B3B0461B030EE68891F22F09129AED4613EEE57DBF0ACD9745ADB89633EDE163ED1C3CBF935C5EF90E4591C7E0B79E2CD629E9A963415B1071256BC2C3905BA30516D01B3CB1C10101830095BBA6D08E7F2006AAAB4B11852A0E2C8D780001C99899A65B96E6BC3B68D84F28F34FE64A9578C078611AA507F15BDBB99DB9F943768B1D4DD6A335D6BAAAE33288B16EAA6496C312B0A08B052F9B9552E23A0EA59E9FE329D461A8CAD9176BFC7006E20985157023BB503599A1798AA89D406FB3E4586F230F797CA17D121CA4AA568BA579AFD8575C801A734083F9214CACA3C6CF3ACADAAC27BE1807432C11CAF806C66953EC6A3F24390FA4AA78F5B90BBA061238F9817FAAAB5D2AB0CE8A3CBB6C686645C6A08CC919E628A96B033E4358CFA8C3C4A642D7127C16A50BEEE5C57A25476D8BB86D019DAD84C613D96A389802EED2031B1A07D478612DB405BBE9BB42A4BF9E7AA91CF2950D00013C4CB7A9B0CF88A87B19E41995EA9EB09660746B1A81A2C74F0BBC28287851CCAE8CB8118ABAA627F44289A9C355DB863095B397D1C449C20AA60B5F1EF94F9002B212C733D81C5A92631CD3B661B11056C9026E84A34E19836411A76AA53089071194E2C9696B31BE8351CB735521A42CB83C52C421724F2FB76D245BB5571358314A2967604084F64AD30911600B33AC9B236DB66487058F35611D0894051F31C032972A03A1CA5822317F953F91A74885E21766824F0FB601C593BC64253E7C36639462721C232D32A076EE245B9A7BC96C0A70ACEA36F8F41E1242AB2D00593BDA4C550018B1345D0806A3D1E8C7A8CC29790127370754A24A73E3A4B08DEC3A88C04C2022235E55CDCAF80938B579B63123E306603C781FF3ACA1105076AA2BBA1D966BC28BB3F3A7340D210D7E7A1735B01E749C0AE990775E2306AEB80229B918542608B7E281B6BB696E1C82342BA196D15EBEF20D541CC57CA928A9DB41E972CC1CD333BC7A64952247FCBC578860191FA67971A29E40C06B81CB5A765C7B26522C19A63C40EC31EBECCF7DF44CDB8B4E7BC805D42A28ACAC3EB2684ABDF46DB7E4C5B01A770B4C0D79D7190B97461BB75CD4A78574677CE26BC30CC25215B1C20C290A1575790C86AEBDEC7993CC94CAE23D5FA2871A184E721886BC920CAC11607FE96878971F161B788E5653698A9F8981339E581969C1B61A6073C02CC3822A2A2D0A6F5BABCA0D57175B86942CFAC2B0FC9F1684324A4139CB1342D7A6B26643A923FA14988173E58A82B67532C6AB89C0D5AA1CB47E44703FD7AA6816501023229B1E76833D7948F0743BE373BCD361130899010727CDACD4347B3A6645541BC7173361860953E61F5824BA2ECB94101383DA6A95CB4591C1BAAAB1B221F43B91D7985A173A0FDA756FE9B38ED1EA4F2A428E34F352A37A45EA83998B66C4E60B40650A87FF5185DCB0055B9C011378442462C285D18A0DBA658DC5B17D1620CE566C86879F38754CC8C1BA014827B444CCF8E9029A7763318CBFE0220DBED60F6720C6F96B4E70C712B58091BE3A6C1C4639D1ABAB09674D2E653DEC768D728BA593AA3C61B17DE17A07B3E85558F38DBF848D65AB3A1AC183A62AAD4CB18344E71BE4C97FEB59001F4ACA46342AAC389B22C47C2A216D9AB50B1A02405DA01494C44184363C31F5C39AC90507D94093F882E6891E24571BF39932DC5482554C12AF6A5F0B3AB4CBD276D6D850894CB4F414196D511731D23C8E039282F6C11D089BEBFB91F0A766935B3ED66458842B4C757585FA6657947529294930C886A50689CA5DF1AA39B8346A14150F051E5462A99E56CA383BA5ABD455F00AA9BBBA660FCCA2C5A8A74299581C274B89E4756552570A33514CB46CD7A3AED34A2DA42C49DE1A8645D64AF3D644BE586068A1B5804965BB7A6134CC8DACD82FF2E954625C92B0D9763E22CCF7450A8FA70F5793400D2832A03A5A75D404D1E2822DA607F6F9C4FB510858978372C621AA72399A34B9E2618F97EAB82B56D93E51FD73A90A78E2AC85826B8E6335330DEB8C644A29A16A3A54F67614A889B92ACBD1D3EC4CBD6C46E8B33FBC2F3C92DF3887DC1DA71A003B9B894A4AE13E6F46DED925CA80189437C0910FA73E146A646178544922DC\",\n          \"c\": \"958FB0A1F80268072D82D593A66B039E548E205C63A4689B48E0752F9041E5D1C2246EF6A1BEA2773CCCFF2A80059C651DA70EAE2AFCF2E83CA15AE29684A11213C01E0DC9F3A25B492A8B44AC188D187A24A6A30B82AA7BD80CBFB992455F280D0D36D2E0A0B3EF65606D55922D29C5A0920D57F8F6EA2AD518B41CCBA23BCC35BAD2E844B5D9000E36AE1F1DCB3D3CF23922D82446506038D5C33925AE96E174876F0C96220BB775EBEF7AD0E48A1E1C785633F6B3585F5FEDAF521F8343440981FCB72DF5860A42343824A0F43A0A371E7D41472199F749B310AA32A5769DD29328E60A7424FAF2D90E6FAC34653B5F59602785FBD09BE26D184B61447327F3772A072AF1A7D4317722AF139EE56D4A7D765D6D73D7B13DA6BF4ABACD1B82320ADE56624CC75BA78278B38F4F4E17FC704B5C88E4800A6E4F626DF21974E2D76AD9BCF70BB9826D790D8A7BF7F23FF9D03021D9B6F48D89D855F4D070CEDA92B28915EF2F77BB79BF80D9E03D9730F1B018AB3AD932588805E995010134774C9C061D11BF852AA5C7403515956D29924EAA1B6BBBE4E4BAD554F4C91F67A268744A12FBE28C3D7A403776DD742431EABE6EE093B988B7F5ACCFF53AC714E03306773F9E54234BA12B1F35421CEE91D81E8A2F91D8AF62733A97A93B1FC2053216083044156ECBFB9919B25C9F3187E07FAE83E9C662AAD0571505413664FAC5ED94C5407CA06036B8C377D8D7D306E1548539B5219CB3DD77D78475EC2FA6DD4C06121230D99151ACE36BF3A3B8DEA00636EE1F6E80D3604E76B3A8E13395B4D590A389F492931A6B37EE27DFB145C1F5C6EEF8615E673E6F6A44BB69F2C19E83A1BCFB3ADAE4815C2DFF7C8A6FA39D41D2A95BB4452D3AF38221754A41E132BFDC70B6B8097D2EB19DA00D55D8DFA80C59070D325766831451B932C2686A71399DEB0F8CF3EBD32C1F6360C1106522BDDD3F3D061196372BE337BF35040A57D1BA098AEDEC56ED240B8AEED2E8A8C0ACEA5BF4109CFD6C55DEF7883A848132BA492FBD364A8B0BDE322EF62D907106F07BD44D97D07AFBF9AF057D7397A2FDE829123FABECC1890B08C18EAAAB2F32B76A7099B3DC09EBEE6B9774AAEF06EDB414E5DB1559CCFA65023D203EE7D867AF48931F06BB86B3AD7E9C0D52A8BD97027A9AA865CDDAE57853BF8F3AB4279533C5D83F240F181A748BF0761CA97130D59C751F8C87544E93E98BAE954EC84AACE8F70E2E6BBDA10389C825E29A147415FAE1B75DF6F1F17191458081EC5CC791250805E5BC1E51273AD723BCA56B3C99FC51EF8B304B960928BF40737BE291C9FB3BC1B051F8D40DB99EFD3260E0121C66336F78901451D3A2D1C2F080FF3EB5480295E949BE9CC5EAB852483B6E7668C6414CF10F6B416603D82F517224FA679BF3057EC4F6DB856A010B74769F78171156A7A604B1F63980A4AA1C8C57442A1596FF8605F3BE684C1D0841F8F4016EC54F4BE2527349B2F0B21E926DC01D324D5842ED2A69290E908F5ABC8855\",\n          \"k\": \"C44EE4E3EB80C46D6BF5CC3E08CB93019C8C80DB0CBE89708E8A6902DE87B699\",\n          \"m\": \"297ECD18E2880A596F572B66458410A0D827851EFA55F1C9CC513F7991F0DA0A\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 3,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-1024\",\n      \"function\": \"encapsulation\",\n      \"tests\": [\n        {\n          \"tcId\": 51,\n          \"deferred\": false,\n          \"ek\": \"307A4CEA4148219B958EA0B7886659235A4D1980B192610847D86EF32739F94C3B446C4D81D89B8B422A9D079C88B11ACAF321B014294E18B296E52F3F744CF9634A4FB01DB0D99EF20A633A552E76A0585C6109F018768B763AF3678B4780089C1342B96907A29A1C11521C744C2797D0BF2B9CCDCA614672B45076773F458A31EF869BE1EB2EFEB50D0E37495DC5CA55E07528934F6293C4168027D0E53D07FACC6630CB08197E53FB193A171135DC8AD9979402A71B6926BCDCDC47B93401910A5FCC1A813B682B09BA7A72D2486D6C799516465C14729B26949B0B7CBC7C640F267FED80B162C51FD8E09227C101D505A8FAE8A2D7054E28A78BA8750DECF9057C83979F7ABB084945648006C5B28804F34E73B238111A65A1F500B1CC606A848F2859070BEBA7573179F36149CF5801BF89A1C38CC278415528D03BDB943F96280C8CC52042D9B91FAA9D6EA7BCBB7AB1897A3266966F78393426C76D8A49578B98B159EBB46EE0A883A270D8057CD0231C86906A91DBBADE6B2469581E2BCA2FEA8389F7C74BCD70961EA5B934FBCF9A6590BF86B8DB548854D9A3FB30110433BD7A1B659CA8568085639237B3BDC37B7FA716D482A25B54106B3A8F54D3AA99B5123DA96066904592F3A54EE23A7981AB608A2F4413CC658946C6D7780EA765644B3CC06C70034AB4EB351912E7715B56755D09021571BF340AB92598A24E811893195B96A1629F8041F58658431561FC0AB15292B913EC473F04479BC145CD4C563A286235646CD305A9BE1014E2C7B130C33EB77CC4A0D9786BD6BC2A954BF3005778F8917CE13789BBB962807858B67731572B6D3C9B4B5206FAC9A7C8961698D88324A915186899B29923F08442A3D386BD416BCC9A100164C930EC35EAFB6AB35851B6C8CE6377366A175F3D75298C518D44898933F53DEE617145093379C4659F68583B2B28122666BEC57838991FF16C368DD22C36E780C91A3582E25E19794C6BF2AB42458A8DD7705DE2C2AA20C054E84B3EF35032798626C248263253A71A11943571340A978CD0A602E47DEE540A8814BA06F31414797CDF6049582361BBABA387A83D89913FE4C0C112B95621A4BDA8123A14D1A842FB57B83A4FBAF33A8E552238A596AAE7A150D75DA648BC44644977BA1F87A4C68A8C4BD245B7D00721F7D64E822B085B901312EC37A8169802160CCE1160F010BE8CBCACE8E7B005D7839234A707868309D03784B4273B1C8A160133ED298184704625F29CFA086D13263EE5899123C596BA788E5C54A8E9BA829B8A9D904BC4BC0BBEA76BC53FF811214598472C9C202B73EFF035DC09703AF7BF1BABAAC73193CB46117A7C9492A43FC95789A924C5912787B2E2090EBBCFD3796221F06DEBF9CF70E056B8B9161D6347F47335F3E1776DA4BB87C15CC826146FF0249A413B45AA93A805196EA453114B524E310AEDAA46E3B99642368782566D049A726D6CCA910993AED621D0149EA588A9ABD909DBB69AA22829D9B83ADA2209A6C2659F2169D668B9314842C6E22A74958B4C25BBDCD293D99CB609D866749A485DFB56024883CF5465DBA0363206587F45597F89002FB8607232138E03B2A894525F265370054B48863614472B95D0A2303442E378B0DD1C75ACBAB971A9A8D1281C79613ACEC6933C377B3C578C2A61A1EC181B101297A37CC5197B2942F6A0E4704C0EC63540481B9F159DC255B59BB55DF496AE54217B7689BD51DBA0383A3D72D852FFCA76DF05B66EECCBD47BC53040817628C71E361D6AF889084916B408A466C96E7086C4A60A10FCF7537BB94AFBCC7D437590919C28650C4F2368259226A9BFDA3A3A0BA1B5087D9D76442FD786C6F81C68C0360D7194D7072C4533AEA86C2D1F8C0A27696066F6CFD11003F797270B32389713CFFA093D991B63844C385E72277F166F5A3934D6BB89A4788DE28321DEFC7457AB484BD30986DC1DAB3008CD7B22F69702FABB9A1045407DA4791C3590FF599D81D688CFA7CC12A68C50F51A1009411B44850F9015DC84A93B17C7A207552C661EA9838E31B95EAD546248E56BE7A5130505268771199880A141771A9E47ACFED590CB3AA7CB7C5F74911D8912C29D6233F4D53BC64139E2F55BE75507DD77868E384AEC581F3F411DB1A742972D3EBFD3315C84A5AD63A0E75C8BCA3E3041E05D9067AFF3B1244F763E7983\",\n          \"dk\": \"673751CBB596541131C66398662CB4B0EB80796A88B28144A5BBC854F80D4B35BE0AB241E4795F8FBBA814F50FA80498CBE8BF68A0A583A4C5981B41DF0667DB614A628C3060697438E62C8D36026EE29C96B673BF1A194EE49481351F4D1748DD01CD023142F01057142B741CBA8302E432F88C63D0B4B5767AC3A5A59AFA3A321E65B1D1511807A06E16A04B2F1070E465586D4A9B68E2B42D57A356FA7BB3D04E51B193FF4C757CFA0F15924EA6E49AFB83B2919C985869ADA544338F44AE96A874C425AF87BC73F3CB0FD2627B1539B1F19A77E36B7FC817851D39BD8A069A6C2202C17469D421A588E65DAF450030B6674EC1C734AA25414B119E61B26EFC90DF81059D2B9599414F93692BF45A4B1C5CC09EDB37B1B1433026AEA6B0200722B819C7BC061C53A4304992FCA2AEE2324A324AB91C3E5D562096B8A141756940F15A2800C274EA4F65817E639C5D2A278C6A294F9DB331F84CCB0A10309F530A06EB962573C86005C15BFC7531A143026396721297E25CB655A294964B2FE531905F2802376B8ACE35AE3E2814BAB7062BC1A840657DBFCB5F41BB55475697849A31E2222E995518CA7640AD4B9CEE9820984138BE0510FFD6AC225393A5F0CB030528CD2A0610E78A5CF1B073039A6D143068C53DBD15A1D4446DA7B310EE795D1FB31B2F97008F83BDF348A593A3BDCBB571907B36D0978162C253E6F50106C463149834ABFB0707D8AB4A4BABC323598A085B309764B7C32C9DB0C9F2D52EF2F00BACE7846868C33B82AFA430A4C2F67B698A60526A161CD62115DCA767C203E3E2CC787031A73B5B7DBA1EEE5AB04B77BB569B952D9A15D198779804197D23C18E5B055F5C8087D742F64418D6505E70418ABFC6B1BF7BB3DE286599F4676CF87946D65144998AFAE1C689449E3F349FD0809AFB856DDE4A94A2C0258D56432F40C3DA812D3FD3B72259A61D2882E0F50B355121E564C6BD33366F32BF4A5996B9998961354925A2BACDF48056118453AC3792A7879B71579ADB65F5D83B1ED6C8C49836DE379DAA027E62B96F683C1688935CB3FCCD64329267273E60C6CD59BA1B7FC911E2662527ECCB7A474E5EF00CA9F789A3838E889242E7FB2B08F3790613C4EED3C912EC4EB029B971096B384727697B4DDC3B698C9A6DA6971FA4C574ECD18EB1C84C0C5790153AA6B9DB61D8BAC0A680A37ED623582A7E8C0885EBB35AF341477764368E0647B14553672316D0B90317C5B53AA747E61B4750DB9E63CC3712900005CA24226B523E0A179582C85968C107857BB41521B7342B13DCAC462A53BE38446F2142519667B48B1C68FCAFA4D3C7E3E5AFF163C41F2C1B4DBAC5456C30776078E7C3A713819F6B9ACA55D77D60637183A723035730F94285C42AC3587637F66AC30F2C4039E60420967576E27B96C8C004D9585F33939AC44F0D195B35D472FC219076F12D0984AC844728D5D2266BB5CD8B325DDA497B4F397BFE722C9D7684201A921F502271985CB3F31C04884C090B063631253DC454537031F2C82C10A1722DE6C556464DC9D64389DA37E469480C921065C79A30C83C867C952B30548A6B5BDFEB6EA6247480F163B427B17CF94889220FE934564DAB90F5B6A11648870B654495A6691AE21FEA86BDC8C49093FA07E926AF3ABA0E7CEC21F613B49986C6C8A139EDA70B7ED8211A3215E8C43EF8C151AE61740EF83B48276033614B58E9CEB992233CD21DFF70C7A6F7171707A2ADD37ACBF136A4EB4A79517FD0C8AFF0B5126435C3100331F208A546C9A4044A8F0503C8ADE9506A018B4CA7C6E8D70120017D38B13B52786A85A540D81B8E71C376B796A7215ABF065086D3C80EE94B8F09E2A3BA13B82583B825388E87BA010AF507173563789A1DCD088907C52BD7FC1C6930605F060F37978211C10FB5717E3FA291D20B5D43FB74CD4711394B0027E41C52B523797470532CBE123C92950720E5E255256577D4E156EBD4C698D813405C61430B978694ACDE78031E74BA1D8517DAE2346F008411231FCCE7BFF75BC361E691E776049004097B36490D876288701B2D3A1743AB8753D47AC6200E2DA7458D3A059681233872794E6720186B20108B1D1033971CE19ED67A2A28E499A360A4AD86AE4194034F202F8FA3626FE75F307A4CEA4148219B958EA0B7886659235A4D1980B192610847D86EF32739F94C3B446C4D81D89B8B422A9D079C88B11ACAF321B014294E18B296E52F3F744CF9634A4FB01DB0D99EF20A633A552E76A0585C6109F018768B763AF3678B4780089C1342B96907A29A1C11521C744C2797D0BF2B9CCDCA614672B45076773F458A31EF869BE1EB2EFEB50D0E37495DC5CA55E07528934F6293C4168027D0E53D07FACC6630CB08197E53FB193A171135DC8AD9979402A71B6926BCDCDC47B93401910A5FCC1A813B682B09BA7A72D2486D6C799516465C14729B26949B0B7CBC7C640F267FED80B162C51FD8E09227C101D505A8FAE8A2D7054E28A78BA8750DECF9057C83979F7ABB084945648006C5B28804F34E73B238111A65A1F500B1CC606A848F2859070BEBA7573179F36149CF5801BF89A1C38CC278415528D03BDB943F96280C8CC52042D9B91FAA9D6EA7BCBB7AB1897A3266966F78393426C76D8A49578B98B159EBB46EE0A883A270D8057CD0231C86906A91DBBADE6B2469581E2BCA2FEA8389F7C74BCD70961EA5B934FBCF9A6590BF86B8DB548854D9A3FB30110433BD7A1B659CA8568085639237B3BDC37B7FA716D482A25B54106B3A8F54D3AA99B5123DA96066904592F3A54EE23A7981AB608A2F4413CC658946C6D7780EA765644B3CC06C70034AB4EB351912E7715B56755D09021571BF340AB92598A24E811893195B96A1629F8041F58658431561FC0AB15292B913EC473F04479BC145CD4C563A286235646CD305A9BE1014E2C7B130C33EB77CC4A0D9786BD6BC2A954BF3005778F8917CE13789BBB962807858B67731572B6D3C9B4B5206FAC9A7C8961698D88324A915186899B29923F08442A3D386BD416BCC9A100164C930EC35EAFB6AB35851B6C8CE6377366A175F3D75298C518D44898933F53DEE617145093379C4659F68583B2B28122666BEC57838991FF16C368DD22C36E780C91A3582E25E19794C6BF2AB42458A8DD7705DE2C2AA20C054E84B3EF35032798626C248263253A71A11943571340A978CD0A602E47DEE540A8814BA06F31414797CDF6049582361BBABA387A83D89913FE4C0C112B95621A4BDA8123A14D1A842FB57B83A4FBAF33A8E552238A596AAE7A150D75DA648BC44644977BA1F87A4C68A8C4BD245B7D00721F7D64E822B085B901312EC37A8169802160CCE1160F010BE8CBCACE8E7B005D7839234A707868309D03784B4273B1C8A160133ED298184704625F29CFA086D13263EE5899123C596BA788E5C54A8E9BA829B8A9D904BC4BC0BBEA76BC53FF811214598472C9C202B73EFF035DC09703AF7BF1BABAAC73193CB46117A7C9492A43FC95789A924C5912787B2E2090EBBCFD3796221F06DEBF9CF70E056B8B9161D6347F47335F3E1776DA4BB87C15CC826146FF0249A413B45AA93A805196EA453114B524E310AEDAA46E3B99642368782566D049A726D6CCA910993AED621D0149EA588A9ABD909DBB69AA22829D9B83ADA2209A6C2659F2169D668B9314842C6E22A74958B4C25BBDCD293D99CB609D866749A485DFB56024883CF5465DBA0363206587F45597F89002FB8607232138E03B2A894525F265370054B48863614472B95D0A2303442E378B0DD1C75ACBAB971A9A8D1281C79613ACEC6933C377B3C578C2A61A1EC181B101297A37CC5197B2942F6A0E4704C0EC63540481B9F159DC255B59BB55DF496AE54217B7689BD51DBA0383A3D72D852FFCA76DF05B66EECCBD47BC53040817628C71E361D6AF889084916B408A466C96E7086C4A60A10FCF7537BB94AFBCC7D437590919C28650C4F2368259226A9BFDA3A3A0BA1B5087D9D76442FD786C6F81C68C0360D7194D7072C4533AEA86C2D1F8C0A27696066F6CFD11003F797270B32389713CFFA093D991B63844C385E72277F166F5A3934D6BB89A4788DE28321DEFC7457AB484BD30986DC1DAB3008CD7B22F69702FABB9A1045407DA4791C3590FF599D81D688CFA7CC12A68C50F51A1009411B44850F9015DC84A93B17C7A207552C661EA9838E31B95EAD546248E56BE7A5130505268771199880A141771A9E47ACFED590CB3AA7CB7C5F74911D8912C29D6233F4D53BC64139E2F55BE75507DD77868E384AEC581F3F411DB1A742972D3EBFD3315C84A5AD63A0E75C8BCA3E3041E05D9067AFF3B1244F763E7983D48BA34134BAB88D635D8CF8FF5D686058FA68B6C2FEEAA5FA4DE65757086C0125E937BCC0D02FAA8988AE7169DF07F6A771E6E7FE3AB65E965C63C3E40ED909\",\n          \"c\": \"E2D5FD4C13CEA0B52D874FEA9012F3A51743A1093710BBF23950F9147A472EE5533928A2F46D592F35DA8B4F758C893B0D7B98948BE447B17CB2AE58AF8A489DDD9232B99B1C0D2DE77CAA472BC3BBD4A7C60DBFDCA92EBF3A1CE1C22DAD13E887004E2924FD22656F5E508791DE06D85E1A1426808ED9A89F6E2FD3C245D4758B22B02CADE33B60FC889A33FC4447EDEBBFD4530DE86596A33789D5DBA6E6EC9F89879AF4BE4909A69017C9BB7A5E31815EA5F132EEC4984FAA7CCF594DD00D4D8487E45621AF8F6E330551439C93EC078A7A3CC1594AF91F8417375FD6088CEB5E85C67099091BAC11498A0D711455F5E0D95CD7BBE5CDD8FECB319E6853C23C9BE2C763DF578666C40A40A87486E46BA8716146192904510A6DC59DA8025825283D684DB91410B4F12C6D8FBD0ADD75D3098918CB04AC7BC4DB0D6BCDF1194DD86292E05B7B8630625B589CC509D215BBD06A2E7C66F424CDF8C40AC6C1E5AE6C964B7D9E92F95FC5C8852281628B81B9AFABC7F03BE3F62E8047BB88D01C68687B8DD4FE63820062B6788A53729053826ED3B7C7EF8241E19C85117B3C5341881D4F299E50374C8EEFD5560BD18319A7963A3D02F0FBE84BC484B5A4018B97D274191C95F702BAB9B0D105FAF9FDCFF97E437236567599FAF73B075D406104D403CDF81224DA590BEC2897E30109E1F2E5AE4610C809A73F638C84210B3447A7C8B6DDDB5AE200BF20E2FE4D4BA6C6B12767FB8760F66C5118E7A9935B41C9A471A1D3237688C1E618CC3BE936AA3F5E44E086820B810E063211FC21C4044B3AC4D00DF1BCC7B24DC07BA48B23B0FC12A3ED3D0A5CF7671415AB9CF21286FE63FB41418570555D4739B88104A8593F293025A4E3EE7C67E4B48E40F6BA8C09860C3FBBE55D45B45FC9AB629B17C276C9C9E2AF3A043BEAFC18FD4F25EE7F83BDDCD2D93914B7ED4F7C9AF127F3F15C277BE16551FEF3AE03D7B9143F0C9C019AB97EEA076366131F518363711B34E96D3F8A513F3E20B1D452C4B7AE3B975EA94D880DAC6693399750D02220403F0D3E3FC1172A4DE9DC280EAF0FEE2883A6660BF5A3D246FF41D21B36EA521CF7AA689F800D0F86F4FA1057D8A13F9DA8FFFD0DC1FAD3C04BB1CCCB7C834DB051A7AC2E4C60301996C93071EA416B421759935659CF62CA5F13AE07C3B195C148159D8BEB03D440B00F5305765F20C0C46EEE59C6D16206402DB1C715E888BDE59C781F35A7CC7C1C5ECB2155AE3E959C0964CC1EF8D7C69D1458A9A42F95F4C6B5B996345712AA290FBBF7DFD4A6E86463022A3F4725F6511BF7EA5E95C707CD3573609AADEAF540152C495F37FE6EC8BB9FA2AA61D15735934F4737928FDE90BA995722465D4A64505A5201F07AA58CFD8AE226E02070B2DBF512B975319A7E8753B4FDAE0EB4922869CC8E25C4A5560C2A0685DE3AC392A8925BA882004894742E43CCFC277439EC8050A9AEB42932E01C840DFCEDCC34D3991289A62C17D1284C839514B93351DBB2DDA81F924565D70E7079D5B8126CAAB7A4A1C731655A53BCC09F5D63EC9086DEA650055985EDFA8297D9C95410C5D1894D17D5930549ADBC2B8733C99FE62E17C4DE34A5D89B12D18E42A422D2CE779C2C28EB2D98003D5CD323FCBECF02B5066E0E734810F09ED89013C00F011BD220F2E5D6A362DF90599198A093B03C8D8EFBFE0B617592FAF1E64220C4440B53FFB47164F369C95290BA9F3108D686C57DB645C53C012E57AF25BD6693E2CC6B57651AF1591FE5D8916640EC017C253DF0606BB6B3035FAE748F3D4034223B1B5EFBF5283E778C1094291CF7B19BE0F317350E6F8518FDE0EFB1381FB6E16C241F7F17A5210693A274159E7FAC868CD0DC4359C3D9EEFEA0D9E31E43FA651392C65A543A59B3EEE3A639DC9417D056A5FF0F160BEEE2EAC29A7D88C0982CF70B5A46379F21E506AAC61A9BB1B8C2B9DAB0E44A823B61D0AA11D94F76A4A8E21F9D4280683208F4EA911116F6FD6A97426934EC3426B8C8F703DA85E9DCF99336136003728B8ECDD04A389F6A817A78BFA61BA46020BF3C34829508F9D06D1553CD987AAC380D86F168843BA3904DE5F7058A41B4CD388BC9CE3ABA7EE7139B7FC9E5B8CFAAA38990BD4A5DB32E2613E7EC4F5F8B1292A38C6F4FF5A40490D76B126652FCF86E245235D636C65CD102B01E22781A72918C\",\n          \"k\": \"7264BDE5C6CEC14849693E2C3C86E48F80958A4F6186FC69333A4148E6E497F3\",\n          \"m\": \"59C5154C04AE43AAFF32700F081700389D54BEC4C37C088B1C53F66212B12C72\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 52,\n          \"deferred\": false,\n          \"ek\": \"16E08D929596ABD2BA47558090531AA277B00DC8337AF578F3A18B3DA8738CA434ED41B537ACCC58182310352331A43A0CA85C606823C824602085B2338142BE48A00E068289310559E9155C6A991CF457F098C61C6B79C584B24C883296B03F9D100489C546ACB28B2DB181BF7B4EC80140F1ABA4130512BA2A0F96C9453DFC479BA1CA9689629779AD731B159A61582CF67989266EFF84455D191032486242E6A9CCA6314B788A3783A0D003A4BE1AC50700611DA61476962E48E38AA5250CB4E60E44B52F00C5233D0A72E3D010D65ACF50CA1704CAB0EBA28D084387DA4BC8BAF7BF3212954652577CE52CD0E9768B3CC606000FEAEC499CB13AC1CBCA0F5B6A0BC7B8B9C140DB83174448050D72C51F18BF1A570FD6314ED91A4DACA6C231404250704A86561F5861785F4B47A15420975225300C621EC11FB6F04C8613982CD16AC85A8EAF62B07FB16A2BAB515D84941AB7AC45DC58D43ACA35697DC711BF8D7BBB41B95BF48716A1BC462F332DB93B67CF858D694B66D9899069EB795B4C1E407ACC74493CC5908B21441838702A3ED0683AE0599CB487A2AC154727A1CFB30104A9B0715698D5E51417832AC67139EF752BA77B7C27217472C62AB8099B4EE2A1D6D98A37EA56058A94D8B86FBFD17972E46A496B2530232F821B68D306AC78BA8D719C6DF278AC79E6036CE55D4E3995CC772E4538BC99E5A5AFF866AA733E6A15A4C7D61ABE8A315E908B588566DBF922C17B6ECB773B59D15416935EB8197FE751A4A5C49AD6FA5D087489F299B20E6721DCC297990751A57489C3A9CB59745FA51191A37873A166C84AF394D280982FA2171183345FF5BC17077B5432236108C6537CB68465C08EA6C98D4B1B606B73BD2A6036B16922B712B68553CAE23630B926276762E3D55DBC1A2FA1CB1372C9460B7727E2CA7382F0B696D005E07AA6C2C763225C30D846710D2286244BC2C751A5BB5CB71F24C75B40C3D1DC0369506D78D39BE3564358764A074567C51BB81B1090ACB301AB95864406B500CD04A2517C582601057328C8467847B4A3248A4BB63251317A9AF93475063CA34D382C4AEC93164011882A6AEE1771EFC99E84E1B68217281B123672999431BB1D4DAA180E9202372C8CD7150FBE3166718AC3746CB0E020AB0A349F88E21D319394676919CB08B29203A6EAC112B63178C7B8C29CC28C4C085A7D6660B12BC64B10A00C038F80076AF0769FB6D42240CA010843AA33B5C534A1C3391928ACF90132D0598E35BFAB062F771696C93696A351C5322C6648CB539660902526202AB34BED4ABC9DA427A1602ED5278897785A9375110A87529D74B951750649DC2B03C0642755132734B808897B1494C98F87376F223207C267A9D5961BC6472B3B8EBBE9ACB9A79A3E2A3FFF428282BB1B79525B7DD265A9986D362566E93886B106C7DBA07FD1C78CC24008852B152822120E73807D8B17486067FAA964330BA67027A84E2BA8A91801D46A059DDA37EDD31875600794E3588AD44331741CEB3990908A57A7C1CA8D7AA3D9864F8E501E9B5603C1FA8ED23327BEB22B08BA26E79C90928B756F96771FD7244B346CB18415CD3CC5BD845E394BCB5C6399F96338534182F015947EF7230A0AB825382957F8950B31CF94F31C0867255A597D9501A76DC2BB7AE455D8296953C51C7BA03A3A0A769207082F45A5100CB49C86317B1650B5898BEBAC512960830A37022CDCBBABCA0AA6DAB3E452A12C1040D54C1BBC372F1997C0DF75BE5D1C88C1618F1833B223D02E2B0980FC187D93A75B57E0487D2CC36AFC1838519378E5634502106AA7B3923830C9B9BA6717694E340B7B51CD63917FF9770635F42F212085458A45BFA09265F074036545FB39CCD08522135AA522670A640B3AA37782D9C7794DACAC86D651B030B33F14464B9CAAE3E883E9582F16558B03D77EFC01AF01E2327CAC368268A4A7141F375C833AD3B4369533FA727FE051C33A1ACAEE8832E32986067468EAD91D79A90058F608F97A1226CBC26339540778B3C1B0421E88458CF69C8DC73287A36D80B57F7FB5B787B66C22658863DB1F60985156BC28BDA25C56C5BD35812020880DCCE46546965817DCC3F1667496F12589065EC68853863C1C581B7F378C82ECEB88D1AB88CFD7DE4C88E0E556D945755EE2558034EC6FFEFAFC68E26128BD7625563BF279\",\n          \"dk\": \"4DD7722880771C554AA6D99D5A873FA7723F18EC976BE29EE5438D7671BA97438000F91A396CAA464BA3CFA2598586AB4D1BB0C9803A82AA1C5B13AD1647972FDC154B61800B0C87215657D13B6BF8CCC69E5C8572EB9AADEB6CDC8871F8F7416FD032ADE5A3E863591B6B3756293057F80024F67A7ED35706185FB6B30F8C836F9624B291C009BA6742C23C65F718291DA3210D25B594E7C00F575B6B87316682115D84116974389248921E0A94302C5DED39CF6C6455B9B277E9F48C2880AD64CB5E96115EBDF1BF42A540228A092FEB8A09449BC626571F4007AE84824AC8CF92EBAD8E25A62E776183BCA7EFF1417CB30EB4043B98649C9276B4AF9CAE89232C7F7C920608A527A92AADF809EC69C651447664E98F369B2704B3054C656AC4570D991C6C5859063C249B70DC5D49CC8202541B13B6005AB09F4A471C28F4785D1A5B52389C3E0B1544DC2098F5284C43802AA16E29F32C93CA611CB170B82C4F6F514A18A985755C3622C2A8C15514340660E9F7025462909DBB6FBBDB3BADDB700714CC47297316F45589922440D6A0AACA8216902AD00647E920C4584AB73CB7C546DA0021C22EA2E59D229536E9596EA6598ADEE85D563C2CB5D82908D60462F430F6245D882A3395F754D561B949CC5CD27240C2CA41406C3D09313ED2A4A600046871F030F41B4528E7316F9104ED550C38C43844789BFD330EC75A8EB3909C156A9E54805C568B5581E603C6C9CD9713AC995077AA52747D281C28BBB535B9BADBD867C65111680C2FDD1032E1E49A67C6C141F53F44461AE427A416B8538747C144025EE3278B6EB26EFE09C9CC8AC7075324D8392A44CC260A1297FCBA4096DC537AC82E5DB560E0C541D114A5E3891068B4B123CBA214D72C36A519B409874D4011E7DB2F7978343B3656EE600031229BDA3171E4480413716B97E5486F07C32E94805FDB48ABA2144288B96C1281AB180A1BECAC17B4C206677920C52B77B53F7D7C782C562C4DA6CB69C5A828056ACB445CCFA62A8EC3AB75C5940D302BD4E38A381015947B973D547A5CFB1460668476529F6411C7AD3A2B13A0C1AC5B71D0132568591374CB27ECB02DE9CCB41D150025C26CD48CAC600C6E9A6A1398884F49EA97096662C06C56F5B76E3F444336A2C7E2FA201A66AD65F2C8BB2340C89C7572698D84190031545CC14420564722820CB1CC6C34708530207B10F3660AF2D72E1C4A4A5729896F79B6BD85B93E8C1B74C521EFC79B10085C92D5857CD200E7A4C7A284524D25636C04BA9120B76A86957F848AA2540A6CB36EBD26354C38213F6713F255457821A47CB00F01185CFBE45E50C624985130086C392226501E6B84D1C666DCC959FA0209114804EF8B99456C16D0B59E81568AD092B3DAF8B70737C3BCD10DC9D48D7DDAAD2159A96B28AA77505364DC92E170699FA95BEF6C26DBB6CD4D972026A00A74896CA2E883E0FBCB19D21E5EFA2776837BE30C373CF405A1628C1FB795095B7FDCBC063357B862FA2823D43A2F1819B0D892A59104B7E64889703F8BE2C39C9892ED393AED6A62330C3EF2B32F5B8A59975488FDF60D8CA366888B9EAC3A094EBA3513F6225638A6D2916CE6F76ABE1242EFEBAD49F29D4A289D6ED2549383B66868A33018107C1618ED3613933A9360F27E6885B7A18167E3F124115184671506F8B6C2F4A18A4EA30FD7FBBE5752A1FB72BE2E909CFA441AFF70C15BE67640F12672DA671224B6BD1B39A7F744E158B9B37B859633068F493902870BD2043A41B605E1395CC305288D9C3ADE5551D92B199260156F3710EF09C8B20A44ADA53D17E31C829BA8775579CEB110F4177BC314653EB6BF03514ECBF009BC6A95A8B79B90E18DB763C46699A9C0C23C32A31B1BD350D4301ADC63A298A486DA95B006175E99E2959881B2D84CA0588C44410173D1148AEA165E93C34CF4711FA3E215F0A193AC679E6298874221B727F141480356D408017D5215D2463C7F62A554E11D0C204A6B93314116BA19CB3F7E1571DF533EB64372FB68A7544C3A7B793D9D79CDB0CC4DD3855E9337C7E8932B1DD3BAE23245FF854FEA2BA93FE06B18E4C694322873DC8C7B70816325A9AE1A1E08686A982715A3C10CACD17F8C2920CB13B251510C55775251850A21D3BB16E08D929596ABD2BA47558090531AA277B00DC8337AF578F3A18B3DA8738CA434ED41B537ACCC58182310352331A43A0CA85C606823C824602085B2338142BE48A00E068289310559E9155C6A991CF457F098C61C6B79C584B24C883296B03F9D100489C546ACB28B2DB181BF7B4EC80140F1ABA4130512BA2A0F96C9453DFC479BA1CA9689629779AD731B159A61582CF67989266EFF84455D191032486242E6A9CCA6314B788A3783A0D003A4BE1AC50700611DA61476962E48E38AA5250CB4E60E44B52F00C5233D0A72E3D010D65ACF50CA1704CAB0EBA28D084387DA4BC8BAF7BF3212954652577CE52CD0E9768B3CC606000FEAEC499CB13AC1CBCA0F5B6A0BC7B8B9C140DB83174448050D72C51F18BF1A570FD6314ED91A4DACA6C231404250704A86561F5861785F4B47A15420975225300C621EC11FB6F04C8613982CD16AC85A8EAF62B07FB16A2BAB515D84941AB7AC45DC58D43ACA35697DC711BF8D7BBB41B95BF48716A1BC462F332DB93B67CF858D694B66D9899069EB795B4C1E407ACC74493CC5908B21441838702A3ED0683AE0599CB487A2AC154727A1CFB30104A9B0715698D5E51417832AC67139EF752BA77B7C27217472C62AB8099B4EE2A1D6D98A37EA56058A94D8B86FBFD17972E46A496B2530232F821B68D306AC78BA8D719C6DF278AC79E6036CE55D4E3995CC772E4538BC99E5A5AFF866AA733E6A15A4C7D61ABE8A315E908B588566DBF922C17B6ECB773B59D15416935EB8197FE751A4A5C49AD6FA5D087489F299B20E6721DCC297990751A57489C3A9CB59745FA51191A37873A166C84AF394D280982FA2171183345FF5BC17077B5432236108C6537CB68465C08EA6C98D4B1B606B73BD2A6036B16922B712B68553CAE23630B926276762E3D55DBC1A2FA1CB1372C9460B7727E2CA7382F0B696D005E07AA6C2C763225C30D846710D2286244BC2C751A5BB5CB71F24C75B40C3D1DC0369506D78D39BE3564358764A074567C51BB81B1090ACB301AB95864406B500CD04A2517C582601057328C8467847B4A3248A4BB63251317A9AF93475063CA34D382C4AEC93164011882A6AEE1771EFC99E84E1B68217281B123672999431BB1D4DAA180E9202372C8CD7150FBE3166718AC3746CB0E020AB0A349F88E21D319394676919CB08B29203A6EAC112B63178C7B8C29CC28C4C085A7D6660B12BC64B10A00C038F80076AF0769FB6D42240CA010843AA33B5C534A1C3391928ACF90132D0598E35BFAB062F771696C93696A351C5322C6648CB539660902526202AB34BED4ABC9DA427A1602ED5278897785A9375110A87529D74B951750649DC2B03C0642755132734B808897B1494C98F87376F223207C267A9D5961BC6472B3B8EBBE9ACB9A79A3E2A3FFF428282BB1B79525B7DD265A9986D362566E93886B106C7DBA07FD1C78CC24008852B152822120E73807D8B17486067FAA964330BA67027A84E2BA8A91801D46A059DDA37EDD31875600794E3588AD44331741CEB3990908A57A7C1CA8D7AA3D9864F8E501E9B5603C1FA8ED23327BEB22B08BA26E79C90928B756F96771FD7244B346CB18415CD3CC5BD845E394BCB5C6399F96338534182F015947EF7230A0AB825382957F8950B31CF94F31C0867255A597D9501A76DC2BB7AE455D8296953C51C7BA03A3A0A769207082F45A5100CB49C86317B1650B5898BEBAC512960830A37022CDCBBABCA0AA6DAB3E452A12C1040D54C1BBC372F1997C0DF75BE5D1C88C1618F1833B223D02E2B0980FC187D93A75B57E0487D2CC36AFC1838519378E5634502106AA7B3923830C9B9BA6717694E340B7B51CD63917FF9770635F42F212085458A45BFA09265F074036545FB39CCD08522135AA522670A640B3AA37782D9C7794DACAC86D651B030B33F14464B9CAAE3E883E9582F16558B03D77EFC01AF01E2327CAC368268A4A7141F375C833AD3B4369533FA727FE051C33A1ACAEE8832E32986067468EAD91D79A90058F608F97A1226CBC26339540778B3C1B0421E88458CF69C8DC73287A36D80B57F7FB5B787B66C22658863DB1F60985156BC28BDA25C56C5BD35812020880DCCE46546965817DCC3F1667496F12589065EC68853863C1C581B7F378C82ECEB88D1AB88CFD7DE4C88E0E556D945755EE2558034EC6FFEFAFC68E26128BD7625563BF279560143610E550E6C27E7AE725C958594A71FCB0350F3CE623FFD626D381C38A24D9D475487B57327D5EFD4EB3307FC1A19EF63E2E11D82AFDC95B51A4FF19D77\",\n          \"c\": \"6930583C55501AF07198C21B52C1A66D60D3E6A403EE412E9751AF2DB2AE360BBE29EA953050D455E25CFFB6E9DB5CB6D881375E7B28BABAF2C7946BC5A4757F61A4970BBF1CADC21C72E782A4A31E92FAB1980E7B2D51AC68CCC6222636D05645B4C85DC7DBDDD6EDE4D52478BD336C81D85708857359DB863F73B839660C3383EED5F621D1CBD3C1C1E5B3F5A5E2BD340824FF5F48690D185F725C821A2681E27EF8C3BB76CDC4CDAF720A8C657601107FFAFE761D4709C35CF62023B1690F2068038D444B9867F2FD7D619F3162D286A42E4B4A5C23E9768AC694B466DAEC80C6A09BED0CAEAE9B1F063708BB800068CE610C0346114981A48921A9BA7091F4E615B5E4FB91CDDBA00272B98FC8DB9282C43B3BF34A393BAC9EB25B6C92235204AAAAB683142BF66E9B37DC1EE10122A3492CC31EAE416D4C364780F696C0691E6449F3570C0AF421192CF44684B1F2BBFD97E2C2B15D6DC4D589069C351BCEAFCE7D2AF4C57DAA75601EECA9CCF72A47D473688B9E21D3EEF68E79BEC63BA7CFCA6D1B47AF8F45DBDE1D3CF6DD108F756F935379303DC3FEBF11BAECA5A2B299586D8DD45B0A17DAD6F2E3F2A63FC0F6435C2108DE90E3C42387A068D7E26C52C966C50A253F9CE19F1B13CDBB75C445D0C01C2EC3133BF9EAB4B6FF0DDA9C87C37FB677827B62107685793406698F08AF44632260D8C298042BDE014A8E3510705719CE0F2A75169363FAF9A0575558809940D3C7FD1E8CC027055789A1A69D9252330410C66CF41F00E67935A7A0D927D6E8EEF2F183377D6CA76F5C0A06F606462B6110600B8345421CCF5F77FF096A800030A0729BFA24521DEB7ECD3AC12B2A7F3A65921F60CB10B3C23C572F5248CDF83C34AB1EFA70AB3F1E78F3CBC0361A407F649ED4F4372A59DE9C11183DBB2661A1707029EB5334BA67231A53C118412723C9E146E0AADC891AA7A37F05F1E63DCB22CCD774FC0AAFBE2A0148DA31EEA8D855F05427E0D416C8A24259EE7D7F0584F01348316BB637F9F18080466610FF013D050F41941CEED3854A90D92E6DA33181D7DA541F148153728C64BEFB5A9CE23F2506FF5A97F3E6372AEBB119646D8E7DE1892F357FF6B4BCA001AC9543BE983E4A919F841A6AC30945F3D516222A1BA8418DCC05D3C2A26D36F43BB2A64F66737EB94CD5D973392CF47EF81CA2BCE1A5C89023EA226E4FB0136D922AD2E67364858213A2CD951369712E3E61DA5C1E8B2F6C21A4A80908CEAA1DF311CED7EBE78E245DDAB3C298C7D2ECC6C78DC5C8ADA322281F6C1B8A33ECE1720E32614085986220A8F8A128097E65904B9285327A8940F02CBBDBB36E8C650FE065F7FE69B30197FDA4F61A7EB3AF7B517668921A6E3C10D79E00853A4DFA985DBEA19AD55BF0BA53CB5EE16DDBD417FE498D2E98921E743B1D2B0192590C738E770F7BCB60B129F0BFB3F2BCC3752DBA1B433C6AF5CFFC18E963BB906BDBFA0564205C482BF032F21DEA5D9A61278ABA2122560FB2030A7893868D10B03E1105AC27527C206DE6538BB235F14FC6DE386A9418A3227264297B09A9A9F1401C24F81B8A2A5A7A6373457AD9DD02642300D564E3030629DED71D014C834F6A5005F2DB283687A2744841A86D3F9DD6A5D332AB097FE04715DA746915FD07A1E6D65C9C60DAA1ECCF71D1F4A4BA8AA9516263790DAEFC1D606DD009E079D1AB84E808DFF4BD56D76336345B23291EC5EF217FAC6CBE590CBE5D31EFDE35D4F7041EB20F7B2232DF031699927D9FC2B08E44A36DB2FE5BFABB6CA53FF050F7CC1D31660EB375D788D83AE56CF359C557E5FD4B881327181C2CA6D86B39BCC22A4C45F7B915183CA0CA00B65A06BE77A56163674F49CA79822BA11596BB5EA52ECBDC139364D84153F97D193E5E05A4F0A618F6018B45F9A646163C999F8D40CEBF85A3D51C05024E39AEF608625C93A1B1144F34EA25A4F3C588BF6841E736921BA111215740F8A1903C065CF08FB2BCD24EAF3E7733CD59066DC85AC0206822402A6AEE784F194BDD411806731CC42430678D4A0D027900D5427639AF42262D57E7BC8242A3FAB2BE536C931DE54D406535AB881C71D9C9A4CFFFB37AD298FE879EB7279DF03B9A42C6B69618478C0886C23688AF1799227163E90955B016BA01F3B9AEE10DC5C889D3883F1163CE483584D7FC09D570BE76968081485086\",\n          \"k\": \"4BE636AD0F1522EE10798CE9EF454ED219A13B6791FD2E042A417B2A220DAE79\",\n          \"m\": \"2E2C821791D3EA49D0AF380B97AA24532F6109D85360A751BB8B4C048C48D26F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 53,\n          \"deferred\": false,\n          \"ek\": \"54570A4B2AB131F9139EC171AC5ABD140863A6A0C5C13C8AF54094E95620E4866BCB8483EBF0B21FF9987B44650951750EF0BB76334235651CFA85153451B1975380513552FDC693124617F395121DA27F86AC80AE363707CC3F264CBA1B703DA67348BE45BDB4293C69E31EA73B0DFC35083F2493185B108E09467B2BCD44AB17D7BBC41F73057FBA8C2732730EF110E740AE75178890746FE149C8898B467CE116F68743A7B35453720BC5D9261946CEF3D9931A4C5F2F271041F0C4BA277778D56EF9DCA3E8CC2A4937BD9A276D173258B70C0BD9A0C695732CED11079D816E9D00A1C44A095F746D4EF14C53C64399964884977D503961A48C14E02A4B7055A74A31C5C55AA503C9927F017A194917F3AA13B33BB8FC86126A8B33EAE53CC2631C15C39FFE2615877084FE0C94BD5013A9CB467FA66E780A46EC5BA392D3C7B6579F63F2656796622AE9428D128BC546B33B26C697392B57A240338117107ABB0C146B2F789FB1A4552A4747C520B161C6A356F1368F2ABF7340BFC8031535876A185C11BFF5176B474B3812BE1E7603085453BD5B289CA97F20B82D88E30064B39872ECBC35E447E8431D5A61B25220CABA58594E96CAE054A791736B51E42F3FCC1F2F7CB8C86C9B67FB5FD53C93ABF3123D11CF5AE491B25703AD386839F4B0370C3AFF50903724C592141B88EBC6F61732F1DB038B15528F1389FC574634071CFF89B39D5A3F50D5B8E6E932C434C9BEF450E42765EE1486C3FB1221A3B3FC42C5ACA709602BBAB9A9BDE4A9716CDA0370B767D1E29128A5560164ACF0D03EC60833AEE125040B5A5D45827C1A2E2B3CACA923BD51134E216921656579F2773C4F0002A285A9EA975670F02399626D65135119754F01F09E204C305C880E93816516B0CCF996076AC93540144231F54ECD27C7C3DBA3DE988E8BC1838F68550ED8BA14D35E14586516BCCF459A3E63C9C69366A5A2992302092790341E7FE08FCA4B77906C839FE929635B89971512EB73456462BA9DB473770B9689446E4D69848E21670CD76F04896456E7BCB4352B086979DBF4905F656C1D67169D71933FD8B827EA5D0B5B00E0C3476BE53F56D11FE47C1BCB5727C51198381916572257D9E5A811E6601191477F45B2244BAB96943B16B28074B7129337333BE218DC114DF16A83C4C1B76A28223489384CAA096097A26B0C17E1E6686D52214F756A12F3C5E08985843909DDC1C4FB99CBB552C9D2594B85C796856C48A3D99A2B93510425A90B4AAA035150DD1394174429B6A394EE495E060812C6044678CA6F5DE9AAD4C11CB4060225F07F004969FFE991B7442176E00D7AD4C6C266435764CAECC34DEE027ED1CC4B80BCA242495378F0097B797C9A0C3F49D7BC2961A41D751728915166730DAE7A588C550F64F06CEA7A868640157F05C69C781682967CD4720040060782500C97593FD5715CD84A05AA10958FB482C38C7DA01A910C5B0C055BC2252B6FDC659A94F36422285F2CD477E350575C4AB3048C0F2AF83116BCA3EF65B0FB73C77747C3D585AAA478945288CDB0853C14D04DAD16052B71C02AF9A3CE254095C26736E6235CD6B0C340812DEB282249AAC87C7E35639E31A5943F61CE43728332B9280495CC9268825AA923DD5155BCE616B3675EAA5891FFA7A6BBA07F14196001253427E8BC4C609318940EF31030C5C9965A26B7B5A2440EF56BCCB424DC75350AA0129E05C83C19856CD93BC9928A8D674381E6B79EBB0319DA1BC7F427DBC1830FF1CCC468B5E68B23AAD73526D16DFDFA30D8CB3559296E35449F36B280B191587A692DBCD610D75342DB7A919F8ABB1E12BA44B185B7560AF79200427150D34601EA5A929025855B34B3067A01BB0B9564C186B2820917D5BC444B30755BAB812C89D201AB6D376135F2220F37AC35876B6012975731077F464ACBEB7A1E7764AD8502A68C9616B09C33FC6688166E1E3170CF6534C0212731049A31D4353BF80A99390064953419E9CD1DECA6F3007D31298DD335B69B971E4F5261F4888318A6A0C0E2AD79C19A827436E87886E29768AB261863243EB5D101A11922B2558D8F53BBCFC1BDD652CA6B10C123C41575F609630C660DD9205E571B276A36C64C770CF3B2D362064F0C94E2BBA158F235F27464280CA9026DE7D64D513C6120035933D3067D03AAFB1021A78860771BC04B4652\",\n          \"dk\": \"B3E23EDA19008B5C42573A1A3BBA8CC34B23AA7A36761143480C8FCFF318FC4048B8C697004D82D12907C031885B92C7EF5BBC4D22A75B493FB19512C5FA208D800C134B1AE9A1CB19E8978B2003EBA888609CB147D7A451CB5199B0259FF8511CC5561338C6C54C2607CBCFE85C9809278F5D0B79925B0522A017B4B45D380C77020A722FA93CCA36904D0B4F04770F4DE13EAD906DBEE00C0FD98212E21B4BF1AD4BD0536FA232C8733719B20B2D8250ED698B01104019957F532B2B4D9489529A265CA9C773178EBAB24659C16423C4C00517AF879AC016A53E70929292140DC9849B65F5406A730CC20C84BA41B29888607A49B4F9C7947D155E60C707A6DC6AE42C6091886ED8F3B5D2655E45DCC945603365E4548DC568C8558C310C84D8E729793226FC30649FBB7FF989565393BD4DA91ECAB1325A49B76BF20A2303B081180B08DC324E01A3526B5B17E13B5C46694948997424179C29434DDC09DAB11CB4A47A6FBB669E02172CA30B2655B2658813F1666281E33EF520CBFC40ADB07A45E8D56B9EC28617DAB8D0BB1C6098CD474444AE523A5C0C47A17C5AFD845BDB14CD3661676C5194C9C48C10230D3A08A48A23B50861CD1EC19EFD5B25137435CB676DCF322F40043998E801467422471752B8FC1CB9E8CEBC2B218F60228C2452396598D186CC9008AFA626326784656228A8A0B00829F56B1C9184E32C757205C80AE5113242B4B41C16C6730A54059F37E0338A215431B9260307BE517776EDFAC0537630C62688B4586CF5A93EB4959C7BA0CEDCB904B2B6B41CE0B966B73C4DCB22F0B420734137C5276E1EF428976C2EBBC055CF583721805D64C57E42C7B2FFB5C3ED33B83E21190B1432E8D6B7D7845D467499F4F1C28BBA4F69568B2680283E779645605901FC918194165C0B62386CB2570809AA1283B31652CC4283568B16D5B2397AE3BD7A19254AC4C711067E98C6203316AFE29337B570ABFC5B272AF386A4A27893B03D3D5C67D2649250F20B10D09DF1E93216EA2A6E2BA8B5B768E9084F6675915AFB917E39051895A5485877CB7C9F7192144704920413666C069B0B731AF8D29953669FD51BB16B2960B6A2470562A0CFE49CDA85982CDC4BD9DC3D28109F32E5365D056469027EBA86904A4AB5CF592819B5848CE425543309DD76B8FA6B9D854A1434A48A636759C8128818E3197ABB33584A4F9AF94FF56839D625799D01B4AEC07EAE36C0FCA3C5A76B6CE97C7F83D3617550A0D309063EAC9E37616065774B6E78C9E3C4140D618CF707295AF10A90702E84168A65B40E172879D6B249DC2094FBAC234E563C1DA060C4A65AE4C2357179CF87480B14919A5DA7BF64BBB40B148C91A960E4A2C3BCB903CF57BF09251AB19830028088FA49C4EF61511FD2A23299CA8FE16873B81E015A724BA5C8BD110BD2D657D212751FE88875975B0322351989831854397DF90DDC82A3F59CB8952086C8F10973242CB778597EC0370838651F02313C575535D97298FC2C9CF312FE066797FC2B61B15B3874858BAC6460D600856703B6CA5A8A90BDCF3490938368670C2E29FC8EDDEB8793481CB3BA9C2D14AF5C8A135329AAA18C31BFB72C1E8A9BBA1756E687300206B3796903A3C2C2704904CC00C947998F97F46388D83CD4E4526CE3A68348A1D7701C120C0F46D0509F904B5092594E4970E8D35F60F87132E68A369BBD4BAAAC11EC976B840BFCC19B1EF48CD2937E54F3AE5B0B482443A80DD0843DE7B66C3014227A383768537D009184F86D17655A5CBCA6D9A01AD988AFA6A52BBFD6C7048285F63C0A160B965F4C39DB142029C9BCF78235ACF2132A14B946D23DFCD7C44564BB8A71879DE2C038F2359D2793B30695A4E23942D08711630CE8E06F1DFCCC987C347BBC0AFE093A5BC0177C873052A26395806C936AB453B79B24F74B7120085AD7377590AC2BEAC5FB41536C152DAF934EE2D568EC8C60EEA14F958A18E6F0ADF0D87321CCCA163A4F8BC31C0867A32C74BE4CD3245FB7C9DB3535CE3C220EE9ACEC0663DA907364EB9673038A90E18C1DF523E5F98A115490F11368DF939A9405CF8E3B0E90E32931935170E28312AB7C8B875CD4AB4FB991BDBE1C692CC356B9B96159B69142E723C6599CF5B3502DA142DE136E54570A4B2AB131F9139EC171AC5ABD140863A6A0C5C13C8AF54094E95620E4866BCB8483EBF0B21FF9987B44650951750EF0BB76334235651CFA85153451B1975380513552FDC693124617F395121DA27F86AC80AE363707CC3F264CBA1B703DA67348BE45BDB4293C69E31EA73B0DFC35083F2493185B108E09467B2BCD44AB17D7BBC41F73057FBA8C2732730EF110E740AE75178890746FE149C8898B467CE116F68743A7B35453720BC5D9261946CEF3D9931A4C5F2F271041F0C4BA277778D56EF9DCA3E8CC2A4937BD9A276D173258B70C0BD9A0C695732CED11079D816E9D00A1C44A095F746D4EF14C53C64399964884977D503961A48C14E02A4B7055A74A31C5C55AA503C9927F017A194917F3AA13B33BB8FC86126A8B33EAE53CC2631C15C39FFE2615877084FE0C94BD5013A9CB467FA66E780A46EC5BA392D3C7B6579F63F2656796622AE9428D128BC546B33B26C697392B57A240338117107ABB0C146B2F789FB1A4552A4747C520B161C6A356F1368F2ABF7340BFC8031535876A185C11BFF5176B474B3812BE1E7603085453BD5B289CA97F20B82D88E30064B39872ECBC35E447E8431D5A61B25220CABA58594E96CAE054A791736B51E42F3FCC1F2F7CB8C86C9B67FB5FD53C93ABF3123D11CF5AE491B25703AD386839F4B0370C3AFF50903724C592141B88EBC6F61732F1DB038B15528F1389FC574634071CFF89B39D5A3F50D5B8E6E932C434C9BEF450E42765EE1486C3FB1221A3B3FC42C5ACA709602BBAB9A9BDE4A9716CDA0370B767D1E29128A5560164ACF0D03EC60833AEE125040B5A5D45827C1A2E2B3CACA923BD51134E216921656579F2773C4F0002A285A9EA975670F02399626D65135119754F01F09E204C305C880E93816516B0CCF996076AC93540144231F54ECD27C7C3DBA3DE988E8BC1838F68550ED8BA14D35E14586516BCCF459A3E63C9C69366A5A2992302092790341E7FE08FCA4B77906C839FE929635B89971512EB73456462BA9DB473770B9689446E4D69848E21670CD76F04896456E7BCB4352B086979DBF4905F656C1D67169D71933FD8B827EA5D0B5B00E0C3476BE53F56D11FE47C1BCB5727C51198381916572257D9E5A811E6601191477F45B2244BAB96943B16B28074B7129337333BE218DC114DF16A83C4C1B76A28223489384CAA096097A26B0C17E1E6686D52214F756A12F3C5E08985843909DDC1C4FB99CBB552C9D2594B85C796856C48A3D99A2B93510425A90B4AAA035150DD1394174429B6A394EE495E060812C6044678CA6F5DE9AAD4C11CB4060225F07F004969FFE991B7442176E00D7AD4C6C266435764CAECC34DEE027ED1CC4B80BCA242495378F0097B797C9A0C3F49D7BC2961A41D751728915166730DAE7A588C550F64F06CEA7A868640157F05C69C781682967CD4720040060782500C97593FD5715CD84A05AA10958FB482C38C7DA01A910C5B0C055BC2252B6FDC659A94F36422285F2CD477E350575C4AB3048C0F2AF83116BCA3EF65B0FB73C77747C3D585AAA478945288CDB0853C14D04DAD16052B71C02AF9A3CE254095C26736E6235CD6B0C340812DEB282249AAC87C7E35639E31A5943F61CE43728332B9280495CC9268825AA923DD5155BCE616B3675EAA5891FFA7A6BBA07F14196001253427E8BC4C609318940EF31030C5C9965A26B7B5A2440EF56BCCB424DC75350AA0129E05C83C19856CD93BC9928A8D674381E6B79EBB0319DA1BC7F427DBC1830FF1CCC468B5E68B23AAD73526D16DFDFA30D8CB3559296E35449F36B280B191587A692DBCD610D75342DB7A919F8ABB1E12BA44B185B7560AF79200427150D34601EA5A929025855B34B3067A01BB0B9564C186B2820917D5BC444B30755BAB812C89D201AB6D376135F2220F37AC35876B6012975731077F464ACBEB7A1E7764AD8502A68C9616B09C33FC6688166E1E3170CF6534C0212731049A31D4353BF80A99390064953419E9CD1DECA6F3007D31298DD335B69B971E4F5261F4888318A6A0C0E2AD79C19A827436E87886E29768AB261863243EB5D101A11922B2558D8F53BBCFC1BDD652CA6B10C123C41575F609630C660DD9205E571B276A36C64C770CF3B2D362064F0C94E2BBA158F235F27464280CA9026DE7D64D513C6120035933D3067D03AAFB1021A78860771BC04B4652CCC54F1107CDD25CC96547EDFEE21D1854D037E1DA63CCC916569AAD31B3AF3BA28EBA34CB5F909FD026770036785C668C4181E8C5E6C458C1B786999C42E152\",\n          \"c\": \"E56A8BBA70BA91912F94B7B44F860C332F1CE8D6990EFEE73AA8BC42E890CC1932C65FF3C22B543EFC1E3ADD83757542160EB4C34C129B1260D4E0CA57CB3E403DEC9DC4DD08875BBE186D82401552D82B7E838C50ACE4096D2E2A07F0D4E5C0AA36EFA6674AD28367536AA0B608A552DA186C1F816731675A635CF39D1629D064736495DDEC6E8D494E27E64A05646D6F9C9FB7C02D62F8978969B1184C55B231560561934CD1EC48476E16F980A879EAF400EACE154F294D359ADE5E189D926FC567402BA0F031E1738A286B18AE6D4565CEF9CF884FF5108019704776D62FA44F0ED1D5E8083A449C5E6A1E7BCD0B5380406B05CB43493B7D91B731C0559CFD51ABC4CB452DD304B63061236F6E8845D469D593755CA9ED6DCD181F672DC4DCD6D950A44D632A7A820F3F3147AE93492A4C6F9F565ABA3DAEB648F6AF723C032CCBE300A83AC138C1D203368E2E407162686ED09955251777AC26DF72DED523E39EEF1A5C0885595A0F46F0BE5BA370A1CCFF54E7E34C6EF92EEDD2432C1860019B58E4B3091AC6DE14677522C037707D61C0B028B498E4CAD3A162B4579CEC0A72F2E4AF38E771F0D4A3BDE3F4B7AD110846B5C1B34A5828C3003EAE34707E0B51D8EE4C32E678F7F581386159182C142B1CDA23991573CCB84DC621737212D8146C8490517BF4AA8C16CC32E7B8157993147872802A8D6894F0B73F11B3225D5D210AD1F8DA8FB1332BDD4A88A559D2C8C8890360F9E91234AC29CACD7434DBF44B8F96FEE02103D6B6499B889A166E30F1B2A3EF53532866C65FC8990F6F00E5165DC2A46E77178EB12809B8D15DF1284DB61DC21A87198E59BBFF3583F8D1AD2774B398F36B8F7AC326D76C23A30F670CE2F5853A79C9C9BE46E98D3BF069CFDB292089142091D3D3CCE6EDE3FCA9C5E0168B655630072755B2BC4DCEF5AE34CA27FAE997CEA1A3FB94B9C94E1975DA4122BC65F563D6D515D2ED8A145B3DB0BC8AA945834FCB84E8C8F41029C8EA3DDF2182733B1CE6F806BFB6A8A702ECB73FA66DE4477D5467FBAE85E5E86BE9403B1ABD824B9E00C7DD4E9B7FA0F663A286B6C5461912EF8AB26A8EF900F060D491D2647A3E0111E5DC83B9E481D0D8DE18482F8D705C046EABA60574116D974FE8721AF9F2FC6C73D2D85CA9063FEB299BAE4AB386D23696EAE5D65BADF7148232D39CFB719642787B539DB6EA3FA9082DE19021AB24C29985CD3EF6EAAA6E6E28ADECEA9CB8CAD6D1D81B84C93ECC8F30FDD04B9D5234ADDC4872976687C667BA02EF41DE88E09DF18916CC26FBAC32D10F9AFF3ACF636FF8B4CEDEADB4F9DC1466EC65884AAA34D6DCCDCA02C86417FB9A98D4CCBCF8119C217DF2D3F5A43768D96250704C44ECC8E6FEE91ED46B4AA59FFDB154217DA4ACABDF56B7B05EE41E7EB4F1A5962B24617CD36F7C4C3A2700AD6315C83EBEEBCB6D799810841286C8EB75F7B7FC249A1195C331E617AA8FFF34171C5BE52F753E1C998F1801D3EFE61F135E963140C1EF17CCE1CB9006ACA4A2045F4D508249165F74DC718D5625017E6856590F439842EA064114CB3F34FEE61877829BBB688B4F9DC04DE3C7AC455AF176CEF8719DA7A44A1E3E379BF7936AC3693DDA97E5AA0E384CC7A7C20C4FE13B98AD95F2CCE880A9E3438DCDB50296CD3463DD0DBC3DADAD3D72D00836C7BD3232D4F43B89F73665E9B94BE84A31C9256DCCF57BB1DCCFEB11989576965C0007B054636A85DC70E6C54E5FABD7875E609AA4B9413DBA1DDDE880ECC37DBF1C3CF6D50F797282A623FBD04AC7EB21596123811C6635614B8F75952B83B11E635FB87F0229DDFC7F527197EE4FF99AAFDB9110057C075F2436B08A4A4D6B565CF0EFEBF397E3C565EABC7A26E662203109823E82978AC0B61496D773925D56982373B009127E2A75E33B1490C07FBEB30E04205C305689A233C5C2A1E5700D64C8D1A58304854CD0D06BC51F7C4B592790A2B0B786051E60EAB9D6515E54F2D1AFE4828FA4C904E5E7922B326E8EE663B5A4950444B396EF66471217BD0547FCD65C10B81F9223680D8F84B1BC33894B1D8B6FA2FEF37E79BCEEEB70AFCAA007B75E52BAD27F66889BA4EEF7428BB3F800345A2C2C95B9BED9C86C715A572D6B0EFC7439CB1711A632551F770EC5BEDCC67A68FE83EE6C30EC08E0BF8317819F7B1A5C9C27B6252D48B80E\",\n          \"k\": \"FBF22CE8BD5102D34529C46FBA28B1BBC9787A570C0DDED9CBAA48D29D76725E\",\n          \"m\": \"5729B2AF60A4A5EE3BA6D7F255D7D2437812579942FF2C6F48611669135DD695\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 54,\n          \"deferred\": false,\n          \"ek\": \"6914CE520C02BF6687BEB9330C704296E40E350303EB356AFFE66FE8C11A83102472EBC96B6634E76C19B89A5E3E4078AC751480B66288120A0D9B69D78387CFA44F32C08BB7768B292BCC03141D77871F5405ACD1403564A27EEA55298AA0544460CB1C8AB9E4C006EC2A1FB5B48F39745D66FA899C456DE0CA4A5CC4B2E4979820F635B5A286ADB3B8AEA97855504935E21C37E226DAEB41D2B1778EB84F764651B66ACF2E7B44FDD43E037400B50C97485047B06BB5B3D1A16FE95AAB462305933A3C730EB43CB6D5C10D9DC25FE0F8085A689C0C611700F894CAF593BA4833466BAC987211BD548C81A2BBDFABB5CCA192FEAB0E1AF3774F851A646281D1841681A40A1FC34EBC8BBD731BBAE927C403BA9E2993588B885008317965F9967D249EAEB85C650C18251AC22930A6A68366F72037B2250CD9B5A568766842A812C3C7BF7106CCFB122007808A84236C2E5A88BB111EAE388F3756B2410776C6D745F770208F5B8BDC087F16F86D52098B7EFC2416A786772124D41187546BB8681672951960C6148591713C19783186A189806CA9D6EC3BC0BCB49404362B82B311E8C970107C9B7A3D6D526BC59C48BE93C699C79167C63A45289AF1773BA21A9A2F99C6A982034914B55A301C27C73D27B50AF02B35B6307DE2F24B1453064F847308836604C56C1ADB3306114AEBA38331A56E6C126445EAA258E25FB692A761EB62FB1BCC88AC79E5EC8CB583CD8ED799D00516B1021958333394B30166718EA0A600CC37403115516768BA1F110F4F27042D589E719350931735023604BC6B0412B67423826EB91711ADB11AE8067C688B080CCC3939142CFE33C9D58710FDEC082A88903E9238D3276316018D38A841930C919A2493A06051CD9CCEF8E46D62F61AAE1A8600A291B55C35EFA8974F1CBE0DA10005A68659904E13631BBD92AE617ACB7618223931C989EB45C5C88FCD74715DD17AA86704C57A2A2561533B562FA7E5B45BF7212682900E70CB2C8227BEC67CB1D7536B405EB6424CF2F5A5593AB77D579156750B78148C33E79BD9E798BA162FA3332A6A7AC354CA3818C174CA0B354B4C13564111DAB79A0BB95FFBA8A71355558A303FF4E9333A708DCA860915B19EC026B7C4022635E404228776931683467337372B7FF7665E9DF38CD11921594C899AA3B55852B76FBCAD0159254B91B06C412DBB5C998A283314A673FDE81009A6C27F733554914B3582C31D3B2B7FA80B9BE6157122ADCC891B21E43AD0343CB014455BD00DFC2C5A671A86A884A5757336D267ABB2F61E18A68CEBD24E09311AA6621B09E4437CB8C6FFF7A0930771D48AC952E317DCE9A5C1261DD83287D7B950ABEBA6C5300105188DD4B4A992840FE996AE2C3B20D7BA00B11AB9396C73A5DC5477FC0141171F6BB05D4806CC9B244A7AF264D84956FFB8A67384CC522BB1335737D5A680ACD3AD478191638A92A0E7292672A75982AAA8A93E321CAB0DCC616A4B9C344735BF6521FC86B0AA3CB1568A9EF7B19AFFE9C352B5B84BD7B66B07C7378B23B601B88C43B794E25DA22083AAFC52682B2CA90302453AA595C6B43E24A84FF07BF298CE2FF1CB22C6BAC6511784AB4FD5AB1A3B376684867951AA4C78986E04A639E8770A77BBA187F08F7E910AAC433FB5461D374C7022693594FB0E25E52882D310495B1340820AA5FA13E187B6335460C37B0DC1848D7CD6C85711CD292994184481560837820A2B9E3A09B34ACCD1E296BE3816B26764DE9B0B16ECA200B21B9E338014FC5F99A8323C657061C59683DA62B22A370313BF5150476DE36B09912283544E2DB6398B1168BC6A33A257605DB863D3C4A6F190488C402D07C754245C804AF6CE9E2589904666AA335B09C35B78731E14739EE2829AFDC991EFEA87BCB3529EFB1677D5378E59B31167792C89449F2A7BDBE01C8B2883C9B880E626AB1837836BD9216118302690148308A944B1B20D92089E113DF5845AD2F489DEB51DD4D4906E93AB9D027D89261C0317561710A033F55B6D19756967159D70870446A48E6B1B26E05258210918C38DC5985D832556FF6872ECB2BDC1A009F8071DA7992F3249AF49C675DB4502F3D235A63C6321B56DE227AC0AE436E44723FF293FC2979DFEF64B9686A3E28597D5954DCF3594F10EB4D23B649636B79B831AD8AEE1E86B66A18D9A3E3F249BC38D79AE\",\n          \"dk\": \"F13845558CB25F9604BF64CA636A3086E28549689AD94450429C7E1D3A556EF52DFD543ED6209423C58827149C8F121DB21950D06C2D2ABC883E260178CBC5B3FC4036CCA60B147B7B31BBA888913A2B16F0956ED922645F8AC26A9607A5B1C984FB5B983BA78838642B3789513790B738CD2A7805EEE46FE88A645CE282DDD823A1B6BE3B19503932A0B8E0BC0CD89AE9E657F8F519283648638C67545CA06BF7A3DBEC594358C2FC21528940300774A60B6316A7160A73CC806513278ABCB10214922C266E2339CAD7FC921A6C643432766A617EFFAC457F70087C923DD9E959E62839C1287F2611C4DAC7548E536E8A4764CA570AF315C2A46CAC87725D04003EC9E261B6AB2CFBBA931F76323BB52EBDBACD62E0418A1C5DFAC0AAFB556D33D5979F9A29EA264DECF7B7E59BAFE4EC3A03588483D27C9F275EF5E409B034CBEE79B9DDB9C2A04964B11437930C9F54161283E1B1BCAB696A82C6C983010E3A769609AA51B75E53C012DA673746676EA9A7CFF84016DD28B3554060E6B4B6DA9334476A230CC12DC4458B447B27A4D427E45256A6A4BAA1705AF7BC5BB19406D828B729D27147A93B52A4416B460EB72896000705956917F74578424179F4A42F7BF61999B9800294757CD992A1B3B17C08116B0B942B4585E5728AACA79FD8E05B5F90478B56276AB0B7EA18B2DEE307A8C0C017200F2DC687C6D8453F8813807610A043B0C0124092910E82D84BAF169B39E5AB55FCC5B7DA8749998D26836CF30A3F1393750EA670E10A763C64590E3CBF20380F6F329468603E8C5092477AC1A15081FB977A5076771ED2B127D82A555905BE784D5D0477A1D01FF450B863026374287F925AA3518359EF1236119C1304A28E6503480E72273493553255B416032841D907BE8B709C3C145EE890ED16C7F5AA9B8139715307C846083A23550688989CF214A05B4B8C4BF81C4190338A4707DFEAC69D54399C30A046259C7F75435DF3A1105C1F095261283612223582F8FB7258003597E96E5B062450A3665369B962B3B447BBB6262242043B4462721943250BAA684CA55629C8591AB7A65D0AF80B772190418083D06BA7943251E2AC5891821730701DBD2C3AF4678563B8590572223CE14DDBC59AE8E0BA67D8370C2092389B060CF601B433216AE04191E2C608DBC5D6093336884773562BBE7036858158DEEA032B505E8E275B1C7259C4DA6EE450AE1D108FC592C7BF327D2EC484E4586E74C7930233851FC7AA4BE99267D56E4DF11624762E78241D051BBCFE710F78797069364133D8B80DA2CB15E96DEC9A82BCD5B80142AD054532CBB87033B55C22987E7F9B45D007866EE057C1371098783D53662E33A9249B94ADF038554A467090448FCA0543F7B00D29A7B4FFE95D5BDA0DF929B7FFE619BCFA46C0F632FBA13014F09BE589C1D6372757877E62579F806336FDC6B93A210A04E64B43A693FB20C333259C1572930A778BFED83F2C1CA974C0701DA3576720A50185A8AD36C2DA2402679899BEB34B1732796F3B7A939B48D9C56164820531A755F7128C3B7517F29235E174CB72309239674FFAE391A35A6DB97785190C21B256273280422829806C344B3885C2C057558538631F82871678649FFC3F152B0D31B6CAF6F0CF7EC821BF80579B75B099A167C0680D946AB236E3525BC160A0A184F1C13194D909C26B3E9DD18236BB6FC4D71BFAE589A2E119A6EC668BA39965A64A3A920BAFAA9E1DBC18CF143A3DA9461F3050CF4A4CE1C53041972C81E2818B9BB332504BB2F40DCD18A8D852C0B9DC4A72C03EE74922F0596F119A58282A28BA6813D76AAFE9369D4C4B333CE5BC1085676AD58180CB20CBF829827207DC3565EDD1B435A5501803BEB4034F48C07D737AB924871B3B0C879B86976E2C8ED769A6CE8771C947B4EAF75229B8508BA85D41E9529402BA5B626975FAA48C08CFE5F6CA345A4EE105CC5E01B9CD826E9EFB7EEA3157CF5B34A66839ECD34CA355311EEB91A61A5EF6301CCEE5B60D05A4D3D02AF4D8CD1FCB7C914683F53B2CB6E620A2367420043927CA88129C2561709CD5C6AFE7D0A00D3BCD8DA9AAFE605E9F0291B5097806E112C773CFA81C1192811D24ABCAA691600CFBBFF230BB86E781C37B7EFD547DFC79208516956914CE520C02BF6687BEB9330C704296E40E350303EB356AFFE66FE8C11A83102472EBC96B6634E76C19B89A5E3E4078AC751480B66288120A0D9B69D78387CFA44F32C08BB7768B292BCC03141D77871F5405ACD1403564A27EEA55298AA0544460CB1C8AB9E4C006EC2A1FB5B48F39745D66FA899C456DE0CA4A5CC4B2E4979820F635B5A286ADB3B8AEA97855504935E21C37E226DAEB41D2B1778EB84F764651B66ACF2E7B44FDD43E037400B50C97485047B06BB5B3D1A16FE95AAB462305933A3C730EB43CB6D5C10D9DC25FE0F8085A689C0C611700F894CAF593BA4833466BAC987211BD548C81A2BBDFABB5CCA192FEAB0E1AF3774F851A646281D1841681A40A1FC34EBC8BBD731BBAE927C403BA9E2993588B885008317965F9967D249EAEB85C650C18251AC22930A6A68366F72037B2250CD9B5A568766842A812C3C7BF7106CCFB122007808A84236C2E5A88BB111EAE388F3756B2410776C6D745F770208F5B8BDC087F16F86D52098B7EFC2416A786772124D41187546BB8681672951960C6148591713C19783186A189806CA9D6EC3BC0BCB49404362B82B311E8C970107C9B7A3D6D526BC59C48BE93C699C79167C63A45289AF1773BA21A9A2F99C6A982034914B55A301C27C73D27B50AF02B35B6307DE2F24B1453064F847308836604C56C1ADB3306114AEBA38331A56E6C126445EAA258E25FB692A761EB62FB1BCC88AC79E5EC8CB583CD8ED799D00516B1021958333394B30166718EA0A600CC37403115516768BA1F110F4F27042D589E719350931735023604BC6B0412B67423826EB91711ADB11AE8067C688B080CCC3939142CFE33C9D58710FDEC082A88903E9238D3276316018D38A841930C919A2493A06051CD9CCEF8E46D62F61AAE1A8600A291B55C35EFA8974F1CBE0DA10005A68659904E13631BBD92AE617ACB7618223931C989EB45C5C88FCD74715DD17AA86704C57A2A2561533B562FA7E5B45BF7212682900E70CB2C8227BEC67CB1D7536B405EB6424CF2F5A5593AB77D579156750B78148C33E79BD9E798BA162FA3332A6A7AC354CA3818C174CA0B354B4C13564111DAB79A0BB95FFBA8A71355558A303FF4E9333A708DCA860915B19EC026B7C4022635E404228776931683467337372B7FF7665E9DF38CD11921594C899AA3B55852B76FBCAD0159254B91B06C412DBB5C998A283314A673FDE81009A6C27F733554914B3582C31D3B2B7FA80B9BE6157122ADCC891B21E43AD0343CB014455BD00DFC2C5A671A86A884A5757336D267ABB2F61E18A68CEBD24E09311AA6621B09E4437CB8C6FFF7A0930771D48AC952E317DCE9A5C1261DD83287D7B950ABEBA6C5300105188DD4B4A992840FE996AE2C3B20D7BA00B11AB9396C73A5DC5477FC0141171F6BB05D4806CC9B244A7AF264D84956FFB8A67384CC522BB1335737D5A680ACD3AD478191638A92A0E7292672A75982AAA8A93E321CAB0DCC616A4B9C344735BF6521FC86B0AA3CB1568A9EF7B19AFFE9C352B5B84BD7B66B07C7378B23B601B88C43B794E25DA22083AAFC52682B2CA90302453AA595C6B43E24A84FF07BF298CE2FF1CB22C6BAC6511784AB4FD5AB1A3B376684867951AA4C78986E04A639E8770A77BBA187F08F7E910AAC433FB5461D374C7022693594FB0E25E52882D310495B1340820AA5FA13E187B6335460C37B0DC1848D7CD6C85711CD292994184481560837820A2B9E3A09B34ACCD1E296BE3816B26764DE9B0B16ECA200B21B9E338014FC5F99A8323C657061C59683DA62B22A370313BF5150476DE36B09912283544E2DB6398B1168BC6A33A257605DB863D3C4A6F190488C402D07C754245C804AF6CE9E2589904666AA335B09C35B78731E14739EE2829AFDC991EFEA87BCB3529EFB1677D5378E59B31167792C89449F2A7BDBE01C8B2883C9B880E626AB1837836BD9216118302690148308A944B1B20D92089E113DF5845AD2F489DEB51DD4D4906E93AB9D027D89261C0317561710A033F55B6D19756967159D70870446A48E6B1B26E05258210918C38DC5985D832556FF6872ECB2BDC1A009F8071DA7992F3249AF49C675DB4502F3D235A63C6321B56DE227AC0AE436E44723FF293FC2979DFEF64B9686A3E28597D5954DCF3594F10EB4D23B649636B79B831AD8AEE1E86B66A18D9A3E3F249BC38D79AE684426A70833DA1EEB4B57F24F46357D5F7E2BC00853A19775E51394883FD13808716659B02B188799AF5A6BB44CA2C61E4453C93B8AFE22EEAD4A006E31AE22\",\n          \"c\": \"4EDB49DE2FB344B6E0CBFA6023FB26F38B58A6378247CEAEDC9C375B426C2AB0AAD40FAEF291E7022CE4A71CB4550A8128D627218864FA4FCB726778A560C6D2EE40829024CF2077DF34575B37B5FA95D9F1645C7C8121679E4E2D96B591203266CEF61A137039637BB347C828C550B725C551396E72976D23A354947200BA37633ECA962A164A7780B7E737BCF92CEDA690E87A28491C95E751DCC89E65BF511514E0D68A0CAF8EF6AF7207066FD10CB841EA7A2638EC1B652FD43C256CD207AB20B32BEAE063D3EEB063825FAF3C82D978CC01FCDDDE2F093E6B76ADBC6FAEC94FF4B3DD399AB7A24D4DC79BC9A70178A10D31CFA874A34A8953B2BBD60318F90907A3FCE6B85A45954FAC3143139EAFAD8450BB225E21AD4D40BEF3A812D26B1EDF5495523048FF8E7B646BA657F8123ECD950EC5A037FE6476B23466059F372FCC934B47FCEBA612C6D4BEFD6A1AA9553D376EE2F0DB2D2CF763C4C2D3B9DF0BE73A39C785A8B1ACD2CDAB9748443065F6A8FBE891A7B20CD2EB3D0718522C60B6A3ADB949779B17EDE8FF21798D6515758983570EE14F7A7C092CF91E53DEB45555EB0F888BAF4C6BF403DC0982C461E5D4EB51256A5D91FD0E41ADB0D15FD2AA7E2EEAFC12CDB508E03EC2285664D317130650191F578F4DB195D3FB2AEE0804B81939AB040A9DCC4B0523F18599E69611A83B2CE7DE1B77E3DB031498F7DF3F1B95F6ED23FC9E716B365DB32E4AB1A599D41E4C13EAD0BBE635329688122C13C1563D6D882160D75C62B1D72448A2B3F7415C1EC1D87E1C6D1CF3746764001698CD079BD9DC25C8D31F981AD1592A1339708300892187955163F74C937D61635842D07F919D8AAB565927D81FCDA514467C7C22F81E9F2585B423092D6AB13A97697337EA2568E268F84739AD1C04F2A2214919F967958500DBDB448559CEAC825B9CCA2B16B94D85B3BA2E0F2D8C22AE3E9267E73C30CA52E2EB9EBD295F6906A88277DA0FD7BD7E2C986561177B7E1CFD204368C6A82F2DF63DAAC040DFBD3F92C300A49A8884B8BE2C2C5EA32135640BAF8CC22547B3EAA81E259AF2BB1C67B45749ECAEF090CBDC7A3DA9DA00E2B879453EA20EBE8A73AD6E37B81D041F2BA9DA99F8140E0793BF1257F809C04702B2FF942BC9E6DFF57F7D477F361513A21D04ECBA1F17694E6E82347AF22C04CF8FBA55E6271A128BB17CBBAC1B07327E56B4904151EA709AD5E96D2AA731507C0C32F1F79EA64B64D172F7591F14C3D3C1B27F179FF8683B5C89BB63F790C2AF58DE6C529FCB0156ADA6A14E752D8D7F7F1E5ACC2FEF83D6EB0206B30044008E18A6C51C01E949B64243A889FA568AFAA6D3B39A04B9953090D39B7C127BAD662B8C146279E554FAF92892819F015C2A87604793E14B64C13F21A728753DA36E67CF7A9CB31E4CD48EFBDBC875EE29E3ED797D99127AD84361F285A117897E66908220FF417BB560268D3AE754E9C8C7981F210B6F3A0B0EBAB428EBCBFFBA22A7E9FF52ECBE3D9B12E12CAD2E80EAC592E42D17290E3B1E68982FCC7198C8007301E026FC4EEBE482F5132577FF39EF4ED9EA44E6BF410D6203187890139650907CBFBFF75284E5B07A0546552F41903A85E5F074B80F9D484AFBD86EF53667C259855015AB795ED6864FC8DEF018471524900995A7925D5B28900C28AC06D79CB795DF93B46D480417C9BCBD1E1BF3876CD5A2874E60025B0F9AD7A0CC35FBAC64E17528A668F3C005EF0D3CE305BA400413DD0A28F5046180564D689D55D9E8380D766E0C397A2E3F26F70E4833353E4C30A1B007C299D0CB5DAB0A1273A1873E2F5ED10651AA844C61FFBFBB753512A5A3A6A105B9AFF1F8E9A92E0C4EA7998E7261460A83CF36D553992611EC607097421F1A7034297EECAE76DE14AF5011BFD8E3407AECCC47FBFC86A702C04BEC48B47D8CF4C7AA760C5F5323D29832857B86E9347DDDD05C7110A7B8AD3752DBB3A335786AA1C1AA0409FB33FD69F85AFC5B4FD9D7864361675B7796AC9DFB77B17EDD34B84A217966055A818A09E17B0490AA39A19F048A9294DA105F285FF698394ABFCDCDDD21AE40BC31E273A3A814BA54C211962D80784DE017C75A247ACF22CC657E11BF64DEB7696CA6E23BB12410B57708174DD9B47E903BD44193CFBE47719417DA52154E1B1A3EF89DC46EBC855061262DA5C6E933AA\",\n          \"k\": \"FBC5B7112172B55A75DD415C4CB3D5C46A54D90A3DE27BEA4CDD116B19FB99A9\",\n          \"m\": \"FE8AD6E3F3EF1FD1890FB7FF75A8CD9B2A04CAFA7ACEAA99D06D116B81039DEE\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 55,\n          \"deferred\": false,\n          \"ek\": \"DF970CC655479D5361B2C99F4E6A60F9D15D9941A36F126062564EEFC3C75871437C021BCE4314A243236FA06A1657B68526652AC65AB05712A1381084254D048670AA24A995096D139236D6A7AEC6A830A4CC259EBA039B6A6E451A0B29D0B4D5FB5FD365B2D81C0D54FBBCFEE19993FA70FB5C92B29C95F0065629D84760272076BC11C3B72B96BC15B3651093DC440F6876E8A27834560B61B8A041499172045ABB4223F0D1AFC71616E8B6958653A08CA776B8916849C81A45A8BCE158B055B69E2F7487D4962FE31061B4B60034567E85496503108CB70167AC4BCFE402A50763941A15A3AB8CBA7DCC220D707ADBE4983A16C3484536EFC860833A527A7388CCB89693FC21906B932BD21584B046677AB9C4E0A54325CD3DD5A78E17CBC3D8CECEDC096CE167E9048C35973F359B7E9B729F95535408919958790427B1CA951080C9A45AA451CADCFAC9FB5804F8CABA9F88C09412ADA0FC2553B5921BE60FA01505BBBA2C33A22B73A69EAFB530AA090ED1734D7561C25299AE954ABA49A0464480B731198A68F9583CF0663322A1A493B6E2A2A2B06CB18B84C701E4AC73E91765FA72641B86249C5817511565023B6719252268585F2B077A3588C0291B36D9CEC74A1DF4FB33A4B95DE0FC7EB1746FD4E17401133F583954B9C947AE6A9AB443CC99924913646F70B29387293459F6317919C177F87EC01CA9E3210455EA25429ACCD4020CF4E43F98999D3739979CE39DECC5361F152B330255444A05F13406F6742BB51156A07854BB0A91C0B45C0F1678FE034BB7B2558232C14F83C822A22C7AAB6934661682E23CADB26D9D9284B519A4595954BC56CBDD0062BF657898B24C80F3010B849E20B269BBC68A538749A6F06DA2D62BC7FA8A25074629D12D22C6517BD4A20D8531B26BB13D2C9B2AE1639243B803D30455A1C0E4CCB5E3C206F4A6BB5C65B3205C56BFA678AC4189CD108DA5C654F4DA7EA30B0D7B23985EE80EECC2297CDBCB6A2A6B3A3C474181BB42F9150CE57FBC6C8ECCE43990F8C31F5579C71C7DC9615D47506666D05BC59339052BB090078E58858780671A65A14CEA22018661B9945A0D0EA2679388C308460AA9367CCC97CE6EA3960AA04BDD28B1B98158E02BAD3EBC3A97F2BB3A1319FB6178B99B42016826F4814960355722B8CE3338BAE0E97084613DBE41064D0A6F3082CFE25A18978975AFC04D0E1C9CCB0C1DF5C9054B22912844077DBAC39E40CC18C87179D420AC782D40E3AB5294C5B5E786814918D048835BC5333F421B366B26568C20E832AEB0410605C3B78394CB1CF5108F5C7538630C8EE4A611412F75F1691133A93EF744D36ACDCE302516735B09B05776049DEEE674F894580A55B792CB4ECC5916D0E3042D3231C8E70DAE111AF3A386ABE5A251FB9F18B6AAB4D79CF16BC884BC0993266C35CB267A9AAD057CBAE4ACC071123CD8D35C86A7C23A6771FC5799CF29ACD8F08E6C0B719E3173162418CD6B008C52A29EC5A701C97ADC906BA4852F7590353B190E96BB8A6C67AB9245CF45BC7DA31B5888B5B6FD77912E81A9CB522946D77305303B6F28B6A89469E734B5FFC4009C341114768EF8C0489E66C7EA3106F224A9AB4B0E147911757521BF39587BF211D9DB8F9BDC9EC21B881AC727810B4D1303A38938A70AB3248BC547B3783F68A84C5C77053726B808825FE17BB8D5672D77E4A178AB3C8EFA8AAEE419A09BB31CD40692A4BA273A137530CEB7198C5F38CD960049BE317AA924ACBAE241FAC48AB540C6CF7A4E58CA5AD3076DD7FAC09DDB1B49DA3D5FC751AE812A416B098CD712ECB99CD0567AAC9BB48DE700120584FD09A201D49A77D16132527F5C0138868C18FEC89B3D593EFA912FF66C79A2832A1B0234F5F0B67091ADFBC507913A41A5C22FFED19109AA9EC0F99395685682F6934010056543AA88738F1D88ABB1BAA21D35585F5534EF6241D81476FF33B434B4C335323C77318156F75370E1403C390242E6C7A75A9D800C0CFD84AE2AD52B9A0A492D441131B68C27DCAAEA93C2E3E2C661E054D8228B03243539649B861927947C76500910A5EA0164D43865B62BB1B30D032334EAF296ABBA4780DA5F12EBABFD021C3FF3A164B0745C09328D5BA0F6127807824E2A7C8012201510C3368067847C71C50A9D8DE22AC182D1644E25A39ABFCE37DB3224F725F065\",\n          \"dk\": \"D6E3B9FCA0A85F25A724020E8FB38AB88BCBD82A7149897E5D0945DF34B0AE07B17055198A7A50A1328E844A186C375EE9B15008B8978235445CA39A73EC8941A14ABB6750B8B2A8CEE62CE44BAAE93585AA6945476B073D176EEAD288060801F9B71C27E366180782E0AC500E2105D6995E47141D25901632DC20D2D89AEA997B5031033F295F92C5C599A0237E1C24B9DB75C03A01C08A78E42A8A0BE3B469FCC341601823160581B529226B7FC161042A3680EA476F12AB121D6A3CB90C24DE501CFAE8C275239F4DB27D22D58CD47440052210DB6A291E080A51A34440A768B70085EF3251AA045FA40A6722C44619512D08E873F1645D0D542EEF6CB12F5B2DB3E3186403B9670A39E0F2CAD3F54998E459AD7647FA8B4F6D900EAE144547F9C506728D080AAE5B2726F5E5212036C5DF172648E6762E09792056C8269289CAD365CD18405A0C182FBC0132443701462B32F31F62E92DEDD4BACF3135F2453A28A92E0D308EED64854D009A700C0BC49B2BFF97C3D12BC481D131174B06AB1B9C10C92EDE463655506E13069B277A0A37201A8B3C6E3EC020B5882ACAE56A23B84232E66211821E2DEA3C9C8B069269229D59AB7FF7758A7C51FA388D74A85B0459364C3669E4B1A230E063AF9868F4CA31694B5E69752872B15FD4A6C577BC4B1821867C456DC6547954C8845425BA9CA5868135159417BD314BAC8BD8533E477B0C759F7FD48F1BF0281C79774A82927FE1264BC2BBB7D4AA479A76EFB9314FA856AD608793AA1D571775A7E363F9E220122047C84648F8596727817F80ECA00E0B4A046AA1255223632098E696BC0E40ACC6A5CACD84C19148B5B4716FDFC2C4CD477D923269E662370C105190983F45550FAD198AE6D512D72362E16C350E783DB9F4BAC4B525C163A20FF261E14A3757B8503C14ABB7F0C7618737A2E0822DA837D606996C073F389A8B68CC2B25170A282C051BE02FF9901FE3D53EF8367587824D4E8051CFE0731E676D754A532B3329BDE45A7F2A92A5055D96560E5B902DFC67B4AEF100E90115E6393BF70397981CA1DF6C8FD6249941FB4D7849434CE52CC0257E98A1724E9BA47425AA21A0C465372D0A299643564EDBD44E3E4119CC063B7A12B969A40A38D33DA23A059968965B405077A8084439AD9D8495876953896827C8E02855F8CD34B288789CA03DB8C8D5914D538794B97B2F6444CCE53686EB07CB21C0BEAC757B3424CE969BBA4685081F926B30791D63C14573C01E999455B1395B6EB19102B7A70CB524C5DA378259461D1284E1EACA66419C166306BB6A8980A54ECA11C2797C988716ADB5E5BE7F4B0E2C2C0BA6118D93A934D8980742285738EBA7F8997F2C9B4D6A3C18968772C821A8E7A9A6969B948CB39977D88AF487A7C7795F4156BB076588A2DC5C2E361EA31BBEBD689194D55D8C43A7420B9D1B0842C0FC45BAB664B310657F57B8BA72A5CC34CE9A1BA939164ADFA40A643B4EE786415891B5F67444567A5B85156F0CB4931F7A7E50157345CA00EEBCA7921A8B0AB00519C0AC2E174452C69D3D963D4FAAA68F6066DAF78D25F798F333949CD80E055116C3F687BA4B16E1A50820155BF6D0740533C4A9A7AFC863CB2C44175DC0BC3D2026EC66408CF893C65B8E31C79D17F33D1C101963D46C0E77C265A977E2D45969B602550873EC3AAE1536339BF51CC1F033B1130B3B269026331B17662B9DB79CE42078FA870946782BC169306DB8034AA72D38572342F621E08895303165A5303BC8B24C81062A14A6B84ED929127920E3EA2763A6A4E65060DB9565DC13305F112D61685ED4A1A8B52969143A7BB42B6B76A3C2FCF96242AB3B58B752188670D4F14CC6BB278C31B59E19A1EBB9905D019B21B8B054933B006823100B5015054A9B26587D9B594B083F01D80B43648A428A514D11814C42059A0C5EA8377BDD25351FD000178485241BB72C28CE0EF210052259DA97C589D66657F4A6B6BACB54F417ABF485E01A1B58FCB0D455925A21605274C16684C1C69034672082D6B923F08BC339753CB0A4329E37C6A7DCCAF3801D1EB72A49C03E7CD0B1A8DB9B44106E33ECC8CB432367C7C061E122DB7C31B8E2AFF9A750F579AA1561950924A44ADA9A60ACCEE2159028C356A431741D0037DF970CC655479D5361B2C99F4E6A60F9D15D9941A36F126062564EEFC3C75871437C021BCE4314A243236FA06A1657B68526652AC65AB05712A1381084254D048670AA24A995096D139236D6A7AEC6A830A4CC259EBA039B6A6E451A0B29D0B4D5FB5FD365B2D81C0D54FBBCFEE19993FA70FB5C92B29C95F0065629D84760272076BC11C3B72B96BC15B3651093DC440F6876E8A27834560B61B8A041499172045ABB4223F0D1AFC71616E8B6958653A08CA776B8916849C81A45A8BCE158B055B69E2F7487D4962FE31061B4B60034567E85496503108CB70167AC4BCFE402A50763941A15A3AB8CBA7DCC220D707ADBE4983A16C3484536EFC860833A527A7388CCB89693FC21906B932BD21584B046677AB9C4E0A54325CD3DD5A78E17CBC3D8CECEDC096CE167E9048C35973F359B7E9B729F95535408919958790427B1CA951080C9A45AA451CADCFAC9FB5804F8CABA9F88C09412ADA0FC2553B5921BE60FA01505BBBA2C33A22B73A69EAFB530AA090ED1734D7561C25299AE954ABA49A0464480B731198A68F9583CF0663322A1A493B6E2A2A2B06CB18B84C701E4AC73E91765FA72641B86249C5817511565023B6719252268585F2B077A3588C0291B36D9CEC74A1DF4FB33A4B95DE0FC7EB1746FD4E17401133F583954B9C947AE6A9AB443CC99924913646F70B29387293459F6317919C177F87EC01CA9E3210455EA25429ACCD4020CF4E43F98999D3739979CE39DECC5361F152B330255444A05F13406F6742BB51156A07854BB0A91C0B45C0F1678FE034BB7B2558232C14F83C822A22C7AAB6934661682E23CADB26D9D9284B519A4595954BC56CBDD0062BF657898B24C80F3010B849E20B269BBC68A538749A6F06DA2D62BC7FA8A25074629D12D22C6517BD4A20D8531B26BB13D2C9B2AE1639243B803D30455A1C0E4CCB5E3C206F4A6BB5C65B3205C56BFA678AC4189CD108DA5C654F4DA7EA30B0D7B23985EE80EECC2297CDBCB6A2A6B3A3C474181BB42F9150CE57FBC6C8ECCE43990F8C31F5579C71C7DC9615D47506666D05BC59339052BB090078E58858780671A65A14CEA22018661B9945A0D0EA2679388C308460AA9367CCC97CE6EA3960AA04BDD28B1B98158E02BAD3EBC3A97F2BB3A1319FB6178B99B42016826F4814960355722B8CE3338BAE0E97084613DBE41064D0A6F3082CFE25A18978975AFC04D0E1C9CCB0C1DF5C9054B22912844077DBAC39E40CC18C87179D420AC782D40E3AB5294C5B5E786814918D048835BC5333F421B366B26568C20E832AEB0410605C3B78394CB1CF5108F5C7538630C8EE4A611412F75F1691133A93EF744D36ACDCE302516735B09B05776049DEEE674F894580A55B792CB4ECC5916D0E3042D3231C8E70DAE111AF3A386ABE5A251FB9F18B6AAB4D79CF16BC884BC0993266C35CB267A9AAD057CBAE4ACC071123CD8D35C86A7C23A6771FC5799CF29ACD8F08E6C0B719E3173162418CD6B008C52A29EC5A701C97ADC906BA4852F7590353B190E96BB8A6C67AB9245CF45BC7DA31B5888B5B6FD77912E81A9CB522946D77305303B6F28B6A89469E734B5FFC4009C341114768EF8C0489E66C7EA3106F224A9AB4B0E147911757521BF39587BF211D9DB8F9BDC9EC21B881AC727810B4D1303A38938A70AB3248BC547B3783F68A84C5C77053726B808825FE17BB8D5672D77E4A178AB3C8EFA8AAEE419A09BB31CD40692A4BA273A137530CEB7198C5F38CD960049BE317AA924ACBAE241FAC48AB540C6CF7A4E58CA5AD3076DD7FAC09DDB1B49DA3D5FC751AE812A416B098CD712ECB99CD0567AAC9BB48DE700120584FD09A201D49A77D16132527F5C0138868C18FEC89B3D593EFA912FF66C79A2832A1B0234F5F0B67091ADFBC507913A41A5C22FFED19109AA9EC0F99395685682F6934010056543AA88738F1D88ABB1BAA21D35585F5534EF6241D81476FF33B434B4C335323C77318156F75370E1403C390242E6C7A75A9D800C0CFD84AE2AD52B9A0A492D441131B68C27DCAAEA93C2E3E2C661E054D8228B03243539649B861927947C76500910A5EA0164D43865B62BB1B30D032334EAF296ABBA4780DA5F12EBABFD021C3FF3A164B0745C09328D5BA0F6127807824E2A7C8012201510C3368067847C71C50A9D8DE22AC182D1644E25A39ABFCE37DB3224F725F0655D6E5BFC5F96134D2C183ABB3911441EC66B794509ACECEDFB7359BA96E9097A6AE0162D48029F424D913B464EC63CFADB3A377109A8759849A8D8542508F050\",\n          \"c\": \"5F7721D08E0CABF5F01821C90767B448F4F53DAAA7ED10FC21702CEE8FC28C32FE20AE36052291C7887B9E84D3C22EA9B401F870A12DFDDC32F1A848E0EEF27C9B7A7A3AD57340946A180596A38757BD6B2570FD92102528B2D0DD804D7F4A76620DAF0767428B3B63B842512EE6816B86B5C5AE08EA8B3A2D61431F185382E8957D399C3E32BF832322915705700EE2A19CBBFD12069CB903F30053B0FFEB61149266593F0733ACE7356455A6D70F7EE6BF7D07199FDEAC313CEE1E06EE5AD50E89BE5A39A73C82277E67DDAB9C88FE4E734FEF19065ADE9C548CB6F91F90C6B899E5FB243284AA1F16CF19F607E18AE8E8A01BDF06AA7F0EEC34E7C1ACA8C407807D986B6B64AAC6A6F5D395AE57A48A4C135F17DD2B5B62EA5C3B83675545787F5EF43832A908D5D8FDCA3D7E60E884F70EAABB062CFC0078539FBE68087CA01CB0401B634A275EBCE328633428EBFDCBA373505BEE1DAB5B531C90C4136036D5981B93CAFD71F6F9FEFF5BF2BF5B01F44FE57F87ACC60DAB8C88DE57087E08506097F31191C376B40048D0349B7F6F5A77E83A8A0B0F3C317AACDEA059F7B7949C2F14087924A28B752206ECEAE424BD7F3B379F29F74D3ABE320BDE982911C15FF6990A3546A8391AC98ED942C63290307F10C398F85D5B0CC11758DCA79B320EA26BDE1F898CFB061882B984FCE7487A69AEB5E4DFECD42EEF146B93F38318F5766F263C67771EC67B0644FB0854E77B02DBA2C05797D24EB1D219F473A732FCDF41317259C0AD919C0AA77B329CC8B3A448C4BE35CDA609F0C4EB4A01B801F380A4F5441BAA6D415507A4BAADFD80E50B563F570ABA9701B264AF7830482F536B49EC2EAC2ED19E853CD51A0C2D3F234135EE3B0A627F1070DCA5016CA3F381333CC546EBED09BD5A7B53B31874FD7A7391451C0195CD64C4256089D709182F4D5DC83CBABC94F53D94FAFCD60A4DF94E06C51F95C7854B5A9A34BB3A8BB06E6B25F2C4098D3A5010DC0793EC23AFE1F55B667E2D2685CC8D335509E41217F05DC99B9998369845F4793C55869300283E119A0C6445A214B5DB10CC62D7214D3DA936A264F566E28B06D9EE12DE50CBF2C68B6DA8E80AC1B1780E5A2EB95CF40F1B947AE4CE0AE1D169408D63FE4209522A5B4686E32E5F8EC4DF81B4EF33837D9C120843234B7431D997D1EA678430CAA9F0228518C8D7E1F0FC3AA31900CAC9EC0BBE002E478527F8E2B24A41253851B4954FB935094C2E3766BEDB53C75CFE7A76EDEA1365B925BFA047D8A3463E5F5709EB39C7E69B2E1B58375D177522C25BFCF29D8E58EFD215350C76CC4D5996AD0FBC7E3ADD94A87E154C49C68F5F18AD4728C9EF1D5040F795EB62DD8CFDDCC83DFF8C5FAA7861D0AE62AED8B232FAB8DB8F64A62A613480D1087F1430D6783490BF29E418BD1B524008676FAF04A29ED9F07FB8E3F4800E2217DD5176392CDEB059762AB5515E243618D7D6CA7B7988CBE52D7B76D0EBB87420644CDB018C92BFA53B57C116643D3DA1F194687E8004BEE958FE968F90E50153550992EFB49AF037D28835E292CF8867A7FDD055833700026E8A97CE9C65A446E9B424EDA15B036DD37C4122609CF70C04E238078A1B759B9A3901AB3336AB47D208FAC6D8B45946D9533F9F48D5B44FE228CEF14F9B9E67F140708E2CC4C9F8D4F55B57658566D9A5E2BBB6554AF989C89A0ED2C88934477F59C14480AF77CF7B3773BF5FA29D0BFD89DFFE4BD597634DF3FCB8C404CC935651D4EDCAD17E5150595D0E7A2599965CAC799C30D37BC2882E0088FEC1DD687471F194130925F931B0A6BC56DF3FA0962CDCB75C008F30F5664B065BA94091C7961BF7549089EFEA6D79372A7C21F2728363EBE32654A5E2AFDEE5E2E9584A658868F48F481032BCC65930031CFE3B777B2FDE5551013481D7584D133772E5230ED502D4208C6128998F140ED9589530B9CD312AE221A8DF4CF979F0D65D8556C5EA30C310AA84E493767C583954931F151DE4DB5491869875B89B7A49294044FFC59BC732CAB155BD549043F30968B821463E4F61B6E10C151A1F1C873F29C49D88B1C075EBF120951018A6C289A6DB7E352557F2DA5D2F2FD8A7AF33FB9664650B63EFA6778C4BE12B104BB3778193C6683B1EACF0264B448A195DB8566951B0BFB3D64EAAFDE75ACE82C067CD1656D88440F029FBF010\",\n          \"k\": \"7F8443BFA35178C5DD7008B9D8DDEFF28BADAD893E16313FCED911730A9F0B3B\",\n          \"m\": \"0AA3B1F8FFA63F89F949DA18B6D8570BC5811F85A4BFB293E9D411ACD43C3227\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 56,\n          \"deferred\": false,\n          \"ek\": \"8FB403D0105FD0EBAFD6505968774504C7823474A1C26280A178BFD33368E14491C100CF58379A55575D753B75456196B5257D2B4BCBEB586EB0397BDC20B798DB6F12A8245FE80DAD6026DCC76EEC5B6DD6F9B5EEAC5211D408CE0553B11394F1440220F03FBBF4158A1C31F4735BB2296AF336C4869713BDD14D686940EC57CDAEFAB8FEBA2FEDD2C51AE61B4D67C5FA6B444FFA2E150788F1D4A12CC7875EF70711A38207C840C2A0337BC3CF42EC1F81464F731C5F460820CD473532983EA710846279B000586140A9B8F2B873C200B8F8D55D1A422F2668103FA8169DB77EA5957C4FA9A72259B60DC27EA16BCD31A051BB528246F05DE7B22398EA32C2230FB520A0D600CD8C049DF9BA6A5C665961344974385FA55525AE340342757BE2831807C552881C13BA133CB9BA2394453818D579787A6CFA356FA560BBDED6182A676126494D98A80161A4987B126DF67633E7433BCEF68690A12DE679512507AC1B9186367139213A3023418C69D0C67DF320C2CC88BB7045543C3380E4A953A03E63854FB986BDC898111F6A7A8FCC74A4F3971F21850E9770CC07CB0C5122CE8B2F873813AC558D9D4474C80084F0A924B3E2B7F4F4137EF5007F0C5605658DED4677A314A44D783C2E3222683B4DBEEC349FABA4430AAFD49160B6C91F2AF4092EF63EBEBA1937705F18A387A6307A733773206B8F2D4052D4C36D2F557FC0E226266116CE41B59233363ECB728C0343C0648AB1045CB03A1C225B89FA488114A060AAF48A68CB7B20A3C55A71ACDD7742DD05AA8495815646353DB21D4A7235450076CB94041D94A5D0245BBFA3598E870FF0E30FB81213B6F9916B78AEF3D43574CC07C9B5CF257A92876C49ECD73B5AC24DEC0BB25FBA54B29CC4A410AD40B5C6FAA398CFB82D2401878C79AF021CA0653A0FDA4B6474F942F93700ABA364AE3611B7666EDE2A8B2EBBA701AA61387500D674A9A9FB0351F06A5CB699EF73457A8ACE9E8B63184768363C50AC000220F2CB708AA27522149975BC951553EFD5B577D507279346E5D00F2EB31EF1DC1EECB59007027E65D28F0D10B7484654241984010B224F174EA69B1C36A302BC20B3244B030B231FA5532A4B206D3B3398E9EBB773B9CD9821BA688C06BABAB29DFB6AA2D427A258693BD1AB13276990B1775AC94B6BDA89F8866AA3A94003B3985086232A4A61CEEA8B1B711609B3285AD158BAE4C21581AA1541CEC7101AB7541098EC7093E442B21296ED172540972F52C04EAF6581EE942C6ADB287A2AC956D84B7F726158D8637962A40986B1F2AA1AD6762C08412F67D1A7F69B6F0F87AB022B1A48F4727CEC2277D1B157932B860A156DE79FEADB99DA7CA0D0B7A4A9D0B3686A100505369C47AD5DF8661CDBA888FC4C7EB1CBF9555548D87755C3232BC3467727AC4625B49145BBD95537585018A7FBA2A8834ED587CCA5F44884C87BF5B5AF8857A188B77A36A01B16F28934E38C9513B5A44926FF41212B13629E2986B8458AE3E9975BBC40C69214068BB78C127FFD4A664DB90BFC068B67106A43F1BC7B094E6048A529B974EF615517ABA2ACA75C97093487C2A265330EE5454985C6435FB989E100B5C9B80DB0A49421D6A75E1632F4B03F5AD96515039D8291C67A15902FE1C027133939183EA0434AD25B45CCBC0C69162CFC1CB527E392B1756E64561F4442BC5814098F33AC673614A8DA326F8818718603110836AB31778536A9C47BA820960CF181CA829A9911792D67DB209E9A66CAC38BC57A4CBD4AA3D07C3447F72D4231A69326AF512314D9AC918DA0BE19B624E5446180166D05F607B1EABC04B2A442DB33C71B66CAD3AA8E559662284807A69EB0469E8B321C73900A74C4A66D40B3CF382D53A12FFA26B9C8888516633E9F292052FA231404483FA41E42A9AC72312838BC7E17027C5EA055967497F3C9988D5A6C4076913DAA9D33C11D4335C850121A93C25A89E3AC0612087943A35E23204EEB12921675AEAB244BB53EA24C969DFC2A0F99BF925666C9A69F123A756104CFBCC88C5A2C8AD7A21956272E22F58035C140BDA371DDE7329D6728E2A05A150719C2D578FC511DD3361DF765C83BF2B04BA7546E0CB5C28C211489C96265335FB5BF6B3C3779E3CFF1C77379E336F3481CEF55F2F61A3CD1F98B88F29760A2BBCCFA7BC9270DCD07F290CEB1D75B0F5941\",\n          \"dk\": \"A707213F208CD438561CF949B2A2066EA1234F619588A6A9B9771E26055FCBC313E918C225B8A6E4234C3664965F20B7F722CE407A68A578888343621AB24E429B1C88C5CA352B98BFFC66B6C7B01D861634B64A986CB5BC16CA4A89C87C455FBF8B74BBEC707F67C552F9B03DA8C521E02D147192AF05BE3C394091B60934EA3032646DF68680A91396BC091CD8806A76D676693C2366EA7196DAC82D060336FC81FB4BCA553C2E21449A69E3C4F84B9906D0AEC2B9C18262C8BA231BA5608972A67B1396BF98D26839901F0BEB0EFCD907E0015BB21CC7F0C22E09B76969E8C09FEBCF105AB9D6A754F6EC6253F615445180BC8668A8D03CE68C2609470092A29FF2127990B14467F960C536B37BB4A980843C8CC76D9D10B2CA26823D513380655883216B5E49C9DFE62D14C1A37132BB072511557118C7B43AC4488D962989394366B3402E739BBA1F69A4F2512F3A50568B8046EDC4A319725E5067CE423B58F496399C44AB2A51C866A26323C68891F5AFCC5B0FE7D37AC31403629CC5B65B9A71E192EED60B7989006B824E0B510E3577C92A3751BD2AA5B1538742303700212CD3C54183724B351C40BAA4CAE46C79CD9383FE1719D333BD4FC33F019B3096065087039CCD21A48C582DF54BC1BDF4BB5391A1DFCC5EC3C57FB95BC9FBE5B611453812E936EFC35AA4C51B42329C31A39A2860C921D9B326F51FE47945877CBFC95178450972BB4A57AD2B2AF1D28AC1D7B01505AB18E964FA567FD9F53B397C2C0891408BEC73494A4563598BD2D1B4DD58603CA80C35A708448B3CFBD4B1490C9ADDD35FA0A4A6F01ABCEFA421DA989DBB506550194FBBA8623AFB8AB97B6E90B0570676191B23AD8D25C5EC054948A8416D13AA890B7FACC6CBA03B1E12F819564A44A9054594500F8F74AFAC8B188EB350FE35A8841A144950C5C75538E8F78621C07FDBEB9A187B0A12773700E0B3BFC7CA6B317B89783250C8263FAA6CA8B53CAA662E3AEC1F07B1AA9461ACE168CD873787B28A6ED501451D5CA306E0000621C35E8BBE57A8795E978637FC781CBC76C60663CAE160D4968988634C9B5937234C5F88BC4CE140763D0520F635BA2BC107049C78B826A1B8A23DB4EA58EB1BCD5D1931329C04DF199030B07F258045DFCBC426B36A8BD0A3A7AA55CFBA400654A879AB97342CA506D6579738913E702C7630AEF8778C4CF61CFEAC53F9EA8D17CC766A09C646662A14077DC95600783B9E372660FB2BB806D84DA12C64CD110F05EA1A8E373D3606083E3382C9EC7FAA649E61F0343FD721F99745F8E2ACFD0707DB053B6F67A0214740BB999D9B434E9218B388F73097F83EAE9792D4515B8267B1F650896AB9BA11C778E3897871B28893281FF33C0441C767556B4FB5852608B01909398115216191A98E38A5B16C8578F1748D69B84BCC474817714CA29534ED17CA995973D0E935D3221A394BAB5F70BAC95590D2791E5C724F77C09B5E0542BB1279EFB4C2F842BB2A8288DF512451D503727187CD3B7812B67257E107248CBF2B63C099228064020AC45B46DF110FEC3960ACD0B4F992745450116BE351123896A2489A76FA4739DAC06DA63CE4FCB6C2C46BA3577D37BA1AFB296ABD2819F324100106745E99B534606F67898ACD724D2F390F67B8AFEA0B7310491F86A31680755AD1FB78E9AA2AC61769DFC01013C38D7D9430F697A51FD627B7699656751407DA16AD8173585BA595ACB14E019ABEF921CD49BA131CA7DA2B2C7D5107356961973039FAF30E551478CD3C7137BA6637673CF715439A09A6D7FC955A35330FC923F09CB33D31B5E3F906EC3BB229C4813F2B1D4D449388507A9377BC100264CDD2C50D5B9E5410A92B7A6B8C5263896CB2BF3C4643F7142A8CA2CD571779766890ECC84453584EE666D4A3C651762E1E267BAF71108E04C847B8693DC1CB2D6929DB811D505588D03629A25753A6787A02B19F3111C6F025B33BC80905DB11A15282A97A031AF8A52AE40B7B83939F7C210D995E24757B589182D92903647256A921C0520445D9B177F4F33F77174CA76577FDA72289D49592FA0E58EC50F9EBC0DB40482F277A1F6381E4372289168F3675A4F3731378590671301A9164390DD3656EEACEC005954F3B3399E6045966434021168FB403D0105FD0EBAFD6505968774504C7823474A1C26280A178BFD33368E14491C100CF58379A55575D753B75456196B5257D2B4BCBEB586EB0397BDC20B798DB6F12A8245FE80DAD6026DCC76EEC5B6DD6F9B5EEAC5211D408CE0553B11394F1440220F03FBBF4158A1C31F4735BB2296AF336C4869713BDD14D686940EC57CDAEFAB8FEBA2FEDD2C51AE61B4D67C5FA6B444FFA2E150788F1D4A12CC7875EF70711A38207C840C2A0337BC3CF42EC1F81464F731C5F460820CD473532983EA710846279B000586140A9B8F2B873C200B8F8D55D1A422F2668103FA8169DB77EA5957C4FA9A72259B60DC27EA16BCD31A051BB528246F05DE7B22398EA32C2230FB520A0D600CD8C049DF9BA6A5C665961344974385FA55525AE340342757BE2831807C552881C13BA133CB9BA2394453818D579787A6CFA356FA560BBDED6182A676126494D98A80161A4987B126DF67633E7433BCEF68690A12DE679512507AC1B9186367139213A3023418C69D0C67DF320C2CC88BB7045543C3380E4A953A03E63854FB986BDC898111F6A7A8FCC74A4F3971F21850E9770CC07CB0C5122CE8B2F873813AC558D9D4474C80084F0A924B3E2B7F4F4137EF5007F0C5605658DED4677A314A44D783C2E3222683B4DBEEC349FABA4430AAFD49160B6C91F2AF4092EF63EBEBA1937705F18A387A6307A733773206B8F2D4052D4C36D2F557FC0E226266116CE41B59233363ECB728C0343C0648AB1045CB03A1C225B89FA488114A060AAF48A68CB7B20A3C55A71ACDD7742DD05AA8495815646353DB21D4A7235450076CB94041D94A5D0245BBFA3598E870FF0E30FB81213B6F9916B78AEF3D43574CC07C9B5CF257A92876C49ECD73B5AC24DEC0BB25FBA54B29CC4A410AD40B5C6FAA398CFB82D2401878C79AF021CA0653A0FDA4B6474F942F93700ABA364AE3611B7666EDE2A8B2EBBA701AA61387500D674A9A9FB0351F06A5CB699EF73457A8ACE9E8B63184768363C50AC000220F2CB708AA27522149975BC951553EFD5B577D507279346E5D00F2EB31EF1DC1EECB59007027E65D28F0D10B7484654241984010B224F174EA69B1C36A302BC20B3244B030B231FA5532A4B206D3B3398E9EBB773B9CD9821BA688C06BABAB29DFB6AA2D427A258693BD1AB13276990B1775AC94B6BDA89F8866AA3A94003B3985086232A4A61CEEA8B1B711609B3285AD158BAE4C21581AA1541CEC7101AB7541098EC7093E442B21296ED172540972F52C04EAF6581EE942C6ADB287A2AC956D84B7F726158D8637962A40986B1F2AA1AD6762C08412F67D1A7F69B6F0F87AB022B1A48F4727CEC2277D1B157932B860A156DE79FEADB99DA7CA0D0B7A4A9D0B3686A100505369C47AD5DF8661CDBA888FC4C7EB1CBF9555548D87755C3232BC3467727AC4625B49145BBD95537585018A7FBA2A8834ED587CCA5F44884C87BF5B5AF8857A188B77A36A01B16F28934E38C9513B5A44926FF41212B13629E2986B8458AE3E9975BBC40C69214068BB78C127FFD4A664DB90BFC068B67106A43F1BC7B094E6048A529B974EF615517ABA2ACA75C97093487C2A265330EE5454985C6435FB989E100B5C9B80DB0A49421D6A75E1632F4B03F5AD96515039D8291C67A15902FE1C027133939183EA0434AD25B45CCBC0C69162CFC1CB527E392B1756E64561F4442BC5814098F33AC673614A8DA326F8818718603110836AB31778536A9C47BA820960CF181CA829A9911792D67DB209E9A66CAC38BC57A4CBD4AA3D07C3447F72D4231A69326AF512314D9AC918DA0BE19B624E5446180166D05F607B1EABC04B2A442DB33C71B66CAD3AA8E559662284807A69EB0469E8B321C73900A74C4A66D40B3CF382D53A12FFA26B9C8888516633E9F292052FA231404483FA41E42A9AC72312838BC7E17027C5EA055967497F3C9988D5A6C4076913DAA9D33C11D4335C850121A93C25A89E3AC0612087943A35E23204EEB12921675AEAB244BB53EA24C969DFC2A0F99BF925666C9A69F123A756104CFBCC88C5A2C8AD7A21956272E22F58035C140BDA371DDE7329D6728E2A05A150719C2D578FC511DD3361DF765C83BF2B04BA7546E0CB5C28C211489C96265335FB5BF6B3C3779E3CFF1C77379E336F3481CEF55F2F61A3CD1F98B88F29760A2BBCCFA7BC9270DCD07F290CEB1D75B0F5941582D82EF332E43017214599D3E49B9F9CDF7E5EF8417B8A95EF46A21618AE908AEA17274D9BC31873ECE5211AAA326A34048F067A162DE56CD27FB17CEE38628\",\n          \"c\": \"78BDBF1509D64B097F89F9158A5474E57CC04818DC01713DADF6C574AAF9115C23C641077EBE2CB2B713066501DDEB196A72067639F30890A41EAE9A565E6EF4735F1DA233FC7647F1B9397BD00E7F387B58BD4C90CC310172AD52BCC4EFE4FF533A096061EE5DF3E3F86D7F196EBFCB02FCF5EF6BDC5CEA034D5F33E61F6F805E4F76CB425B466D620EA166E828D692E12C568767482BF32C98A5A8142015A66BE48618347D49997AB6B0F53426BBDECF1A653458C2D8A6D80D49B736C2445216BF580E85BB794987986955523C6D78573608F1E2A2EDEC9F2862A289DB9D9BC8780B96FAF5D2DCDB0C6AA4A381C97FDA9A67C2F6052B145C8DA98C5BEE640ACD64586DEFDEF5FE6430F883B68C57366083B24C783561A41A3AFE3435ACFDC8BC5F14711079A8B0419000D41BAA5D45B70F9BA844DAD01E3CE992BACCD90B6B22B6CD81BC67BBFF830FA5EACFD6F508EDE4A11998DCE7F9C715F404F20C0AA98FABA50E0028994DCC9BEA50EC89A8CE26F49791DDAB8E16150A4C0A9883E59DD6737EB9EC79B63CF6612A71DF6F4CA023591DF6B5800EDF02942EA286C95F650415EF28C6F71E6D29DFAD488D2BB735E5658352A38F9DB04541716122AB51AD5B8DB07098232536F93210B5350A473ABEC4AC6ED14C0F91E069D0208A3E7B805474D48069AF8143530A8002C682F6D2987841EBBF4DD0FE49ADC161EF507C05B705720395F7D2187AB92EA8F02B2B29863DBAD1E4A293E7FF2F9DAB5B522087C756A4A5560A60B1F79C017F85AB5B854A431CB36C8A675BDC8568EE681B62C71F0FE67AE59A56DC61D4731AC415B1D33ED23D52491446AB14AF99314B7B160A39EA56CB2C1BDA78A8396DF2F5382FFAAE8EF66A9152BA6FE38D0A0B8D12FA2B74C1FEF982D69E9F16A3A75EC3FE31AEB7531CB37E0E067232C2C3C3A441C8A8345DD2E538B8BDE88AE750CE6BC8CF8219F401F34671C1933C63FE46E24E422A1FEA8D46B0A59792956ADE770D306D358DDE52B6EE0C437581096370FB85BF38105D939DE745E7B08221E9F8AA9892E4B7EFEE803452748825E1BD338B3DB87E09395970206D97FEF5219387F5C56FF9FB18C1A711FE28FED5C6CBF6A65D4B86480F7A05FC1922272C0AF27CC072ACD598ED7C7B068717CACDF8222CDD114B977B0D1E49FE3EA2EF8624EC73865A6D89ACB77AA0F05BFB3386795686938A98CDAD4930E141B05AB6DC401E89B719DFA8A40C9641561BB38EC2C288FCDD6032C8575298B020DAEEAD80FBCF43645E893F34CC29B7ADD1D815A2F735B15E30A1A3192D00C525E859D3ADEB62CBFEF3D0FF0D5026E0249DEC8DBFCF57300AFD3A40C9838284D2622FE07588BD81B542E6E12CC3FBC2CB7991F55FEE665F9F19BE36E49A4AD541718592F1829FF22DECDA26A8D595F71438F899512E409C24D2B2D679094E9A65C994566166D27BC299C89D5E78713DE0AE04372EE150A93985177E3CA7423CA96D4CA89E6A862EF80223D88887B6075F63130D4E3292EEFE9F850F57D67CD0C57FFE88DEFB61D9A6AFA917550160A1806CC525792899C9BB3B8D4506138233724A159C824DF5164C34405DE662897F601FF533CF70307CF7CDA04C072F0777CF5C31A7F90AEE8C169ED37D15CCC23743F5C77A9A5D12B1C90260DBA9C94A1B98EAE019B09EA2A94D34DAFB8A06856AE14B42483746C52A41A403E5F116DEE001AE45F2DA987CE56CF5CD9BE99EBD0D2AC1231E9BDBE5715A594F277B4BDD6D96C23199B72A1495DFDC1A28EA7178C02F623D4B6E77520AC134870A8BD62C1083110755222C69C685DCAFD8D58B881A72626B46D8CF3F01BB37134B5CF47EAF95C5FB6E0DB5EBE822DD98F2061A7695C2D5BCBAA26926DA9BC55F2FD8B5A009E0F8EE9838D86B6434CCD28E05BE7E1FAE1AE2DEA7DAA1270890C170378B65995D53A7D3DD5076B86A73B3FC7EB73C0C59D2A9B54210458E0ECA8EB3FD1278FC1B8A87395B59A3BF37E4C52BE2A59640D944169B53D109E348B3ADBB62026FDE56E422372DDD77713B546ED46279C13382441F9553A28D55313FA07709A227230120FAC743028D49D58C1989E32A03E58D0F2D87D28663B05ED75F91AE849657ACD9685AA1F528A6F9331E891F7028537F08D86B4F8118D52105F297D1FFD3503AD700C5FC5A1187ACAD5FC6AD2B4EAE36CFB99146D6FFC6B8F8F3B7\",\n          \"k\": \"71E743C143334C36D7078D290BFF42D53E7D775C6DE3FEA876E054CB8042D3F9\",\n          \"m\": \"2429F93D29E48EB6A25ABBA3EE2F3423CDDDD0ECF4B2090C6CA5BF4883F4F3BA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 57,\n          \"deferred\": false,\n          \"ek\": \"FDE545438A542588554452B13FB2092F3113D0DB9541437B6ABB3A02A713871AA479A1B6F5C20F208226A0028B5A680AE845335DBC9B9C3B91E833C8FEECC89F0CCCBE4216949684E094A96A469EA2B09F5EC49A352084CADA99EAC591661A1A2C468CCDCC4C374310E78BB0FA38803B1B1B078C9BF0D9280323067BE76B1BD3912901695FE1C4AF4A1BC38661AF797F9D7B9D8EF091EA3A0A53210C57A706ACE6A33F09083B728F767B51520BBA7BEB364817B820DAA55877CCBA3CBFEDF60E48FC8A0B5BC6D223CF56C3392021A6DE432F4B4A3E58ECBF462376BB62933D6815FD085653D5C3A267C150697BCDACC54E336D7024662BCA1782E531DC339851B33A0778A779544F4B081C05998787916A802436DA982B79164C5A5823D11AA271187E8F90790CE95C1E474E2861960F756F3191267E0158C1150608B5CA7AF09C23579608386822ACB02D559974FC6FED8558CFA6383ED0B5FCA318648699DBB7195F24A000E6A99FF8B4DCD0C3C7B84E59F7CA21979F646B4D8E7A61289605FB027DDD3A10CAB301B9918C365714E1391C42185874980328C04900AA9967627660C6ADA737260D74A1CEA5578A254FF0F203DFD6C832E86EFDB25A08AC5609DBBB469A7125A7C1C8644223AA51EBF9C98DFB64FE530FBF47210BB3C142360EA864AEB447459C6B361757AECFCA733CA2C5EA485B0AC065E2951F145CCA4FB6951579B4A19CB042A3B34419CF29B99A6E579945EC357C7C7BCD7578175BBD51444C1DC9A7AA54574A32623D2C90146B44F2547EE9930282F25447207B03C04EF6AC1294B9BAC6C36AE1666401E318B730020A832ED789199CA8445239B765D48610C6B25DC4C592DA7906C130A6E356C066534388651A0B4F495C39962C9F20DB1224B40AE0642091115BFCA34B5455568E97B0D45B74592905E02BBEB8FC1E2D1C49051242AD000D69009B12A206B988B8CB26A91DBC6305777234FC22142AC85B828D1E8B9E589800214173E858497B964E6DD98997576F5F4B9C68090563F1B004D38643E2872B196B1F607AAB3C22D73324DDD49CE4214F13065405767348A20BC82329EAE2283FB65ED1EB939CF25A7B1762E9686DD3576602073112141ED315300BE6B75F860884E0718C7596FC39172AD9BA4EB837393C49DCA98AD8CC15B849965C017CECDAC67161A7DE6863698C8A64E23A19402E977539E834887954867A29CD2F46B0FBD6240D89141FD8C9ED390099D905C5101B63B30D4D61AF14965FAC227470B38642064412747BDDD2063A942EB0F02843156C1D637D2D8C6C5161CC3A21A69C6220B39CA03BE975B81921C56C140422A8791A9CA2A9AFEC49353B65AC2440C31E2084FACC6F7F23A027A682CAAC402A69675305363BA85947BA5275B6868660B654D91264870FCBE39BD2E68B9048C29732A3A7306AEAEA8A08BB65E5E078105424A3B8803EF9C62CD65D8A65CE6D528A1A818E1BAAA4139CB451F5A20C135F8A231E843531CD700A2E69BFFF186DFF49108562361F353A8E5A23C900043034BA6B9C6CDF2A7C67847F78E0955F7C49EA94785AD3495CF4562EC86F72E22B562382B82B392DBACC1F536C18C20086A879B82C291F45CCB4C24221836383D03997C43EDFE165A1A046405A193E0ACDD08B122AF6925DB303491BCB87DAA36CAAB91197BA64A9A9E3C4A760174129B080FD18339E07ACAF00CAA118A84360AF0333755B869694D07BD108AF4A094C51A9165CCA9ABA36C2E3E43FE4DBBDDF257E08D03B333773D5E051F8101B8EC7B80FD1AF5E9C053FC71C0A1C785954C3AA4C33FFC95AF6CA3B248B5F050AC11B3998C51C8A294544CA4A60DC47529EE1A3849314AAA62A05D3557827AF57FCA1DF9435C4014669B9142DB620AF044C195B6BF046582C52BE435473C74A34CBE751599412B96480678B668B238DF373302076856BA63AB627CB21F3707BA18B999A3A27DBC727EAA8E0497FC3854A4A926905D0968E3886FFFC4F6A2B4273993282635B9BA013B4B8C91E7A47AA624F8262A14C42B4B8A36027573D4790BCF5D386B9181FA0F357230A2185747875970C668142E3673087AA02A5B7AA058C7527B1C0A1C9B05C912D49D1BC8FF629FA77CFEDD1A7F6EA52F9BB49A82B38C022C52C87C346482D42A797BD3A972E19DC352C66A0A315C04CEDAE314DC0D335EEADEA8ED3B75EA17EB388\",\n          \"dk\": \"698C6D273359EFC42931A425DA3B8B9772747190B26204AD5B352807098287B672019B9604337BD50988A1C7AEEA975795E12AF9348A6D180B6F086AA9E92B43F31813B0A357C36CD31504F65285EEE03CBC288C0EC06AEF318622B21839E74A38D33C1FACC773A29BBB0652DFD728B90A613B323D735790DA292ED25403EDF5B2260342D75889732852FF4813F701AD3CE1C9F960C53057C293A09779A88745E901EF744A07E511290277D1467A2A082501DA4C10755E5F2AA8E3560AE33A5D37C77EC0809A1B766484422D00E3422CF19F64DCCC1DA80816E321ADBAB7E7E79B3BF382EB732F86FB771FC1A6B63A6655D161D8B21141034722480C31328AEE1167FCB477626521E833208EA53F95D46B8F2A4D36AB00E34B5E9D324D9C2A6E8AB7752E54CE1AFA9CA5DC461C208F66AB0370C90E96DA0E90340E014BCCC9676094BAB2FD7B831298C4589429E012739989BCEB5B09C8A0AA7F56CE2B680FE0D19D9C8171DA580034184C52E30FF3B712C5D92BA93B9FE8D61C7583227503B3F33A83CB613FCD23C3CA0B4689823380B4037ABA9CF1698F48C50ACEAA2D4BD524BC34BBFF4204C4A359CD3896E3057F9C367F0ED074896B6DE44CC5B492AF3E52A29643B87EC895FB3375E21A48FE5CC0CF3A8810B9ADC950C158F13E29994FDB67C093C09A09A4C4565A21E808916040431250BA91B95D8E42C3DE2A1DB032AF1A3529D1820ECE9A686DC554B86950FA9ABEF1D8B416EA11AAA5984232662F2734609C9DD7112E19B5550F115FA571CAC73078F08B601C649B1A6949875461FBBC416CD951CEA13B8E996DA68382A14664A4920B4E9240562C6893AC8D089BCC4B3B55719C4F9C331F2576985A07C8A0192FA599197C49260CF57B6793ABEF0619258B886975422DE0CFBC20383C3124F74004E3360D988855DA225E810A9D9C910079D80A21B2A806D851E8831581F68D3914C0A0FA9B518B71E387113EC8616592BB71E481928C2E48AB75C9C7A4BFC48ABBEA6DA77A45B4FA5C71BC31F85C902552B517EA36C2E3436A506371C8A827AA721CF57045D21D52597640F1536D518268B65B19F2BE40ACCE482B26671AC51ACA27DEF703497770083611AEB83795EC58C84262EC4A3D78258953D248580CC6D069273D12984BA5BFA359AB25484F79A3B8CE4739D570CA1C6844C8FB525CB764970A72B1381B68394AA6308AD00C96F19A17F7441B544C3C95903A2E9125090C12A8BAC7461CBD8E37025CBAAFA797B70D8896E6E5A60AD332516B3594D06F75A18DF335BFFF876F4B103F8ECBB85547246F226A95285F081337F0719A9EC453D4517B0A2838DF0117EB480039583C27B53A4684138875C4CFD06C7E357F6C836D5797A88323A8D7600979E95025372160A9BFDBD32101012034E64613E198FBD92226A4AD9954795A9227A9777C78F3B8669CC524446D77C8219839AD23B21DEA6486995A029A875D4767A866E51D7E00205FFC5FF43222CE923882D88D72B856FD7C00EA07266FCA70A849A3C9D227021B3BC90B8FB2BBA0173930984847B42B90CBE930DFF08FE4C14D424CBC82FC81DA1B6AE5477D49C30D9E4CC9C651AB6976AF47A18C188388ED0C3163E0B5C9152C82E859DF0C16D3B2478AEC5ECBFA4AFA7560B858142AB48EDB94617C92522CB07C39767CABC55F8BEB00CB48C9ED19497594A161E744E764104347777F5B8168A95F579315394C6F184CAB00CB3B27C19FEE96354E4B62E490572D597455888BCA0259E6F7027AE44F7F95CBF4592B722696102448E7B437CEE5565569B27ED9CCB23A75DD094A98678129272A6C2C27DB5B272B7B9BEADB34CEC5091C405649897665BB633012694F4094C595370E2B91BD8C585817063F556A39FC625EC38D86A5CD6D83CCEA20BA6DC108FBC3C723ABC62092C612F503F8896102E9124DA46BDD00748141891782AD9086876AC920B11C5A6849C67A4B70434236937243D940817C2016A644C53EB7C0F8540C52301D63122CD7E43E98116511D455D3675DBB42944F7CA936F51364D07249200A17A7C83FDA1A2017835C68965EE617DFD5B5C1F026CBD2B79B68A6D76443278972B6914A5937AC8C213292050F0578A0D9219EA00345D17BA529D6B5190381C255B307ABBEEF7025FA36C1FDE545438A542588554452B13FB2092F3113D0DB9541437B6ABB3A02A713871AA479A1B6F5C20F208226A0028B5A680AE845335DBC9B9C3B91E833C8FEECC89F0CCCBE4216949684E094A96A469EA2B09F5EC49A352084CADA99EAC591661A1A2C468CCDCC4C374310E78BB0FA38803B1B1B078C9BF0D9280323067BE76B1BD3912901695FE1C4AF4A1BC38661AF797F9D7B9D8EF091EA3A0A53210C57A706ACE6A33F09083B728F767B51520BBA7BEB364817B820DAA55877CCBA3CBFEDF60E48FC8A0B5BC6D223CF56C3392021A6DE432F4B4A3E58ECBF462376BB62933D6815FD085653D5C3A267C150697BCDACC54E336D7024662BCA1782E531DC339851B33A0778A779544F4B081C05998787916A802436DA982B79164C5A5823D11AA271187E8F90790CE95C1E474E2861960F756F3191267E0158C1150608B5CA7AF09C23579608386822ACB02D559974FC6FED8558CFA6383ED0B5FCA318648699DBB7195F24A000E6A99FF8B4DCD0C3C7B84E59F7CA21979F646B4D8E7A61289605FB027DDD3A10CAB301B9918C365714E1391C42185874980328C04900AA9967627660C6ADA737260D74A1CEA5578A254FF0F203DFD6C832E86EFDB25A08AC5609DBBB469A7125A7C1C8644223AA51EBF9C98DFB64FE530FBF47210BB3C142360EA864AEB447459C6B361757AECFCA733CA2C5EA485B0AC065E2951F145CCA4FB6951579B4A19CB042A3B34419CF29B99A6E579945EC357C7C7BCD7578175BBD51444C1DC9A7AA54574A32623D2C90146B44F2547EE9930282F25447207B03C04EF6AC1294B9BAC6C36AE1666401E318B730020A832ED789199CA8445239B765D48610C6B25DC4C592DA7906C130A6E356C066534388651A0B4F495C39962C9F20DB1224B40AE0642091115BFCA34B5455568E97B0D45B74592905E02BBEB8FC1E2D1C49051242AD000D69009B12A206B988B8CB26A91DBC6305777234FC22142AC85B828D1E8B9E589800214173E858497B964E6DD98997576F5F4B9C68090563F1B004D38643E2872B196B1F607AAB3C22D73324DDD49CE4214F13065405767348A20BC82329EAE2283FB65ED1EB939CF25A7B1762E9686DD3576602073112141ED315300BE6B75F860884E0718C7596FC39172AD9BA4EB837393C49DCA98AD8CC15B849965C017CECDAC67161A7DE6863698C8A64E23A19402E977539E834887954867A29CD2F46B0FBD6240D89141FD8C9ED390099D905C5101B63B30D4D61AF14965FAC227470B38642064412747BDDD2063A942EB0F02843156C1D637D2D8C6C5161CC3A21A69C6220B39CA03BE975B81921C56C140422A8791A9CA2A9AFEC49353B65AC2440C31E2084FACC6F7F23A027A682CAAC402A69675305363BA85947BA5275B6868660B654D91264870FCBE39BD2E68B9048C29732A3A7306AEAEA8A08BB65E5E078105424A3B8803EF9C62CD65D8A65CE6D528A1A818E1BAAA4139CB451F5A20C135F8A231E843531CD700A2E69BFFF186DFF49108562361F353A8E5A23C900043034BA6B9C6CDF2A7C67847F78E0955F7C49EA94785AD3495CF4562EC86F72E22B562382B82B392DBACC1F536C18C20086A879B82C291F45CCB4C24221836383D03997C43EDFE165A1A046405A193E0ACDD08B122AF6925DB303491BCB87DAA36CAAB91197BA64A9A9E3C4A760174129B080FD18339E07ACAF00CAA118A84360AF0333755B869694D07BD108AF4A094C51A9165CCA9ABA36C2E3E43FE4DBBDDF257E08D03B333773D5E051F8101B8EC7B80FD1AF5E9C053FC71C0A1C785954C3AA4C33FFC95AF6CA3B248B5F050AC11B3998C51C8A294544CA4A60DC47529EE1A3849314AAA62A05D3557827AF57FCA1DF9435C4014669B9142DB620AF044C195B6BF046582C52BE435473C74A34CBE751599412B96480678B668B238DF373302076856BA63AB627CB21F3707BA18B999A3A27DBC727EAA8E0497FC3854A4A926905D0968E3886FFFC4F6A2B4273993282635B9BA013B4B8C91E7A47AA624F8262A14C42B4B8A36027573D4790BCF5D386B9181FA0F357230A2185747875970C668142E3673087AA02A5B7AA058C7527B1C0A1C9B05C912D49D1BC8FF629FA77CFEDD1A7F6EA52F9BB49A82B38C022C52C87C346482D42A797BD3A972E19DC352C66A0A315C04CEDAE314DC0D335EEADEA8ED3B75EA17EB3882A9072AC6B041BA7624CF3158048EE475506482D536D15DBEC594818AB0E91025AB34FAEA7275E5D6C8EE1104CB19F4B1C14B6C51ADB118163C6A48540E1C5D2\",\n          \"c\": \"4CBA673C63D21AF9EF1A30F8AE10628CA81926A8C0799D276BE363A182F64C312CA1AE996865034FB1F2F4FFAB6D132B141C9CC01FE4BA6B16B8FA1F6F59D140B89CE159239BCEDA3E93EBF6D858D647385392563BCC1B421505F29D0C74FC6028AC536DF5D8693C6E4BD36A9AE85737B1C14632DCB11896B50775A75C8A334D334B7983D6778D1BB2452090736CD9B488025AAA2AADC091ADEF4705BEE3BA81A404772916AD78B4CE2E2EFA0C54F77FCB4AF0AB62720D52C9B181F80BD6A71B65456BB8A6EBC4203C8D224B6B8CD544559734A9E51270D06680240E48E72F6A294F29CE05C7B9226DB34E2883A065E6FB742FD38A00DCA0938D0DB500A012E84903425EC1680A71BD7688F26DDCAD3C8368C2B384BDF407E83C5441B427CACB25FEF42F6A8C350E67DEDC9BFA7007B28F6742028268358981D72C07C455630D8B845D61F77F0129CFD4DB594BDC4BE54D958E8A3D8B6A9A4DE77BB66B393DA7B823F5D3C25C5EACCFA258A9F7D9E05038E7F8CB06A32886296FAD4B7822DC2249B1809E5244716A552462C8F1E21D87A30703A3000C6D3AA082BDA96C441F65014AC10C74F708AB8BD83E5CF6BF42124746FBB44F5F65A9D564DB514AC49D43335A6D8677A30A54F673BEE43784B2596346F49073EA27BF4C2AEA4CD790A5F24E772F69F591129CF33CBCF5D012384230AD91D8603D3A54EEDF75EBB579CF4027F1E40792F0FB2769EA83494CBAA6D8732C0ACF474AA422DF63B8BB1D19AA37078C690E5C44B495C87AADFFEEBDF0FCCD969A22E151978AE67E9B7D950BDDB18D9014536E91B1FDA68F41394653E4A066EF8AC92D61CA56CA3B504A6FFD5993785A958A3E2890D40D58CE12790CA35AFCAD8569B75DE5E033B6813B6E85F1837C5F62BD61C3E94132ADE97F2F8F3FDA5A54A958865BA898A8A67C38E61C8625C563A5EE7F6D059E22FF626976332D0E0A4F7A240434999F16BB194FC77B21DD09A47EB819B1D5365553E8A86A09A83BFC612BB38618C84132C0E7A8212C5D2327F5BB7BBB5C55DE72E013A198CD806239112D0ECFCD05092DDFC34A9453EAAE2EA59061734C4A480CA346652025BDD35F49FEDE97B301E3AE6246EF54C298F26968D44F144958CF7094520473198C6A654B4105D2F8CE350C05359BFF40C15D61B633C6F963F3EF5D37FCE3383EBB14F3FC04150B4D6FF34A16A29AE0D38A802EE52B0585D9CE8BB8D3F27BA43A577673AC96FF063AD37FF4081A67BEBF1FAB1AFF168A0DE3B0220249765EF67481C7C30ED4A1FDE1BB62F10557034A6226B6B64892D4443BD8965D1A632F3EC8D58A82FA39FF13B43A02A82D6A1ED0FA651342192B4B4E799A398914C49FC5C71A7B8798ACD44871859E3D8CE777833D0295A9782EC48334D098D10EEBD198CF06B0795AD505FE0342D5AF7E4E99EE34BF0C70AAA2E4978419DEDBFD6EFD30E985E6539DC139DC2D96E28BE541642645F54781EB682BAEF49C109E0665F4928E9BDA891AB87D872BCD28B683F13336419010BF53DB4349873544331823BAF9789DDDD5BCC7831E2C10F50C6E33632C9D4C47ECACFC664D7D683B34934BA03AAA4A9FD1236C4F97A1EFFBE3B183268308BE20A781DD22D0A3BDFB450A102988525832F03DAC2A9441DEAF2F3C5DBE47060930EF9687008D7B58AA81FB79F08DB833D7F2C3E880B4F621AC7FC6601849C7CED42166D31A530619BB9D8CCFF269C20EEF4AD814C42B3BD2B35C4100DA58C8EFE5B46C2957673ECBE586C95163ABD5D9F2ACE64059218685F369B3815F01D693098B7EF8E7F89F202F8AE76594EA7A66D5F4B74F80CE0FC274E647B26CD1E63E88FF27AE783648DFA85D5DD56863000F03D8D215E21EF11A1C2E8E57D5CF770D0B7AE22557F458F3ABAAF5B9BE1408E9CF4823D7FE129472B9E4B89394DE4907F85F94A3A42299E432BCA661BE17D23B6B61A7F20630B0ACA4529AB8B03FBDABA1625D2D49E724BF7748AF96FD3FF11915A84408BF80C70FC0381E39016CFC8739B80F7C9E2DEF0C53383557E15FDE9BB31FF367C2625542433388F19CF70B568D64E958FD33FAB2DCAB72D4778E2F094D94A62DA941D13B56BC6CFEE6D1C3D81A82DA67344994FA405C31A90B9EFCAA8C7242F07BE73B3AD9ADE3CC4EB73017FD6D7AB37F3B34BCF1D2B291EFE9D4F8C4C33952C479AA0AA7B824E3B895F8B954\",\n          \"k\": \"91C5D5F6016A421C4C5017CD8E805008533F67FF7037E8C62FB52A2D6657EE5E\",\n          \"m\": \"B65043CD3672CF9AE2CACC94F923CEF63B5127ABC63C2A5AE6C064B8C6FE7C57\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 58,\n          \"deferred\": false,\n          \"ek\": \"E7104DFD284FC4B28110B06282BA8EB859CD60F8934CBA78B1F1653CD4A9613B4BCD5C32A3B30EF863A448D3A17FFC5908438800C5CE12475F3D5445761CCB25541FAE3653A8B8776256C7F5384481772ED0E3C4E512988E605E1C75A85606AA574442888B87F0A77C08824FDFF2B5FDEA4834112BF77C011F7855AA065ADEC66F505A72EE45300E2C9B18588E2F6B57B1C2B26F491166AA9D89D9BC2ED26DD311A81E4232FCC720CA8A788D5A00DB696763256F58B2B68BC32653C71F84088383473F19A76ADE5CB5B6BB466561AE7294770F9148E9E44B2FA1A23B1B99CEC1327F60A002728F21CB9D25AA79F526CCB4EC1531864AA550C72336509FB3061717BADFB2682EB59053333CEC75BD6E244F002D0BC2B1C8008737AB62CD42D2BDF9CA2FECE31DCF836264F84F76569A16046761885C163CCCE986122A11C8911C9257B88FA2CB792BE55FC14048BB995F11606E4C3C82B3113D99B451686ABB1F129E22D63EB14C6C3EC2479BD0243F57876175B138BB0D42313A28C67F1134733F76977BA87B5A11523953947B684282905F2AE90F5BE1CFB0088539313A9FE2A3AC9729C2FB5FBCF492C5ACC90A74C934B1A29AC19D2E20CE74436742A1415A9A0A4CE3756C199AF7593A7685C02F19768C6A1F4859B04B0186C63580916915053404054929F8B417F6A3919882C8D31B220F1817DCE08267DB0EAF61301C2844CA4556F813279D122C43A8820560A09A18AE2807163547B9A3E22F83E14AC4DC3FE69A25FD5C2CACB6A67905C9C754565EB5B17AF089C4653302AA830375B9CF0BB6D43904DD8277D50BBF4D43ADE05918ECFA74DD432CFBD829C4F9838DD76EB75813DCF397E3507FC9C6BD69C2B220644D3EB8839769CFCED608F98874EBAC4367A11274317297DBB424B95B9062ACB4C83CC5E879E6688447F26C0D2BA3763406318BCDA3228CEB0641C7250DAC193A8713744A6ABE5F6C106DC4049CDB49FB946DDB7A08D2D73556648F4CB82E2AE989ED61677B805F3A61344A14A2F3DC3964A90E4B83CC22AA8E7CD532BAB55880F212751C4CFB93AC14F564AFAA723FF6B31990A88DD32A557AA86EC2ABC114745C8038F4C1C924258B5BF02134605923E865539425F10424319131B0CB17D72C2981A944FCE3CD85A2509E93BE3AB5A65E53380772A56D74945D60C57A112FD87A2EB7989CC14A973CD05590495122811A6D8992C0064C77A78C7E390C43FB695726B7582C96C50A8EC5B4081CAAB6E9B64870166326A8ADA4731030290CD444B9E28149E06BACC7A79C5E7B24BAC292B7F92C7AD720F957013E1B118B3A40095C705DB33DE334ADD011CF3DCCBACCABA3B12A6FB5962E01C7C8D83912D9F98ACF8B1BA073B25AEA0C177074E7EB08CFF3CC7D7709E723340BCC00B014A0B313008DE52533B72393D70B3EC767A8B6CBAB61B8B7A9C86B263A1528A9FE366D60104C0D320CEFB76A688B407C0742F8BA4AB8007A3651C0F26B81E92746DB701837B575AB00A6B6D56FC5C12C956685ED0AC3F0C6A5870935298640E5070A8B74AD540CBCF273705C68115A828F72B09C83551714D67455EB46DC0BC14145CC38F5C1DF675A3677ADD28C69F0D462B1F1A0ACE8AA30B877382280F903B7DF9503E4C5048F4368B2A58565552EFB106E9AACC75562C31E3A3FE7790EC9269987C627EFF798A9A0484F215A3968525A0915C4B4AA7A8170EFB1466936A8B8440DAD499CB2F94373CC91F62673B59A90C728A818247513445D5AE25D85AAAB0036135DB5BD74D106AC557E0E8754D1E071B5B62FFF7575847B47905BBDCD5958A7A86852C34DD01286CBD50F0CA4628E31C5EBA531DBC02C9DE1BCD82C218C2CCA87A42A28D98ACFD1103F80AA9073C127381B4584C923492960537A28B868F214251E328FF44CC984972949003916B76D89D472B97013A52141FBAB0939B03D8D57364A387FFFB4A59D7B5D586680D7F54DC27AA7BAC39580103C74621E6D8970176AB2965ACBF453A7217A3C21769449E9AAE851090B52390642C467C9A233501110F059EF4A1A02BB06CBE1084A08C85AEA6237DCA55E2698D177716E662E2DC711D475C2EC9745BB061A90432C6463BA827BC32E0B0FA6B015324C9B574078AD823ECAB9010D0BC640C0A20E17317B2A30AE8AAC4C94695EAFDEB2BDD14D78D1CA07CEB411455C0EF10D23FB50CA\",\n          \"dk\": \"1A47BE10A1273C9312ECD31E36786BF2C9A13382CE6919AB8F4C33A92A5E4C5A862CF22E39F34793A4BEA35B4CAC858A32B79EF3C56A6C1C7E9205A22F0CAF77936722424CB6B207B15484BD602239D66AB2D5A0C342AEAEC83C95E42BD6D1817A1AC7C50A189F4248A9EAAB81B53EE952C9897178C2E92BBBA7B84A7348D8C504CECCA2261A4F72EC9EECD82E1329A55D5A64B8743F08999B78B88D4C60C0FB1387398A2323946D7A071B6898C8BA3250734933764C31A91782F2050CB3597B0FC63C280C576298C062D0968539C740D6B89A6A28D20871F8E2C35BDB86EE8B747CA81B83D856407B7C0A604AB1F753D38844DD890209B337F2770883B82FD44808356B24A36195D23C88D5F5857F4624ACD31211B695F0638C6AB870856C1277EC24C525A37C39259A99707256BCE0DC9F2B9606AC93A0146A5B5BE41B22E63382B56F7203733682CA87EC80F8C58F4294167F4772F0E83176BCBA554545EAF711B02A7DB7B896D44BB0F47BC46659A8305329D82172A1DB4C72955839D9752277C3401A7D1800088A768F473B80B7C746AACC8205BB798E905C1E1008F454AA5363B1F8E9CF3CCA72AFE1703C03A585D66C528BC42A311E0479CAE4763D789C583A56A3F6836E73A73BBF01073703AC90254CE50B27560842B3FBBF5DAA4C6DA37B9858BC7F2054A596348187BB99646A7933234721B1A3FA65564A5E3525276C5A7D68960FBC7C095AD403EA0C0380E68B4A5083F0527D3C563313B21A2816446A4AAFB32B52D8A60CDFF26281412353D02205868F9452BF50E0440049630C022CCF05AE6A7289747C573062560A3316C895586994CC7D2C4FCBEA0974C5845400C0C9218BA8BA2FBFB8C93C025AFB6C341FB2B44DDC6046B99F2CA2A269F931B76C0BB89BB424C9CDFE54136F716C8613C461B92297429EB471AE1C47AB4C3199281039D41A3B67908F3586997C8A9980D9145BBA2A098524461184AF05576BE349A614862310AC68788A0F06C418E255FE6B63F03C99E30699DBD77378E02AACF6C2021ACE42F69F42A5716CC37C64617FA7C68721BB8A3CEC3609BB8799422074D143E676599154B186554D2D5C9EA57B3EA3917BEC6C61C5A351A9908471B94856758964B77EB7D7C2CB1A4DB3000D4A87B9448686DF075031263EE5C0253668AF61C8259CD33E1E9C1FDAB4B4B17012EF831BE6C3BABC8463B3084FE2524B3227829D24BA38F88D850B21CC06638F258378D15F73D4C6B3D1BA498959E3A9C9257C8DFB2624F99A15CB09B1CB7469F61421B929A5C07ACFD781A8F4069C446760E7ACABA6E2B4CFC89553969EC1B21A41432965A8BE9E5A73CD402900472CB184812E1B5B2136149E4083D17AAF728242B470209C9C466AA3823A9A7B1C35A36F062BB670573EA059BEE9A2F1E4612AAC19B7A3337C3AB647318A88A22226F606FD645E324971D5D58E89A0838B00553A14449679A01149298294B9FB641B08674BE4615DCE174CEB019982D75996220F33E2393D8397FFF6217AF3452CDA8D3820AE291839B2280BEFC2179B344340777009378E56FBB90D934F04809C3E0A8512AC69D087BF669214A7125337C15CECC2165FB7C620D06CE422A9E5EB17D5983AC0509BF598AF45428BAAB16546480670DA4042F93FFD147E4418254109B8E0D97260429E5AD2553A2C86E6E7294764943CF14014B57B8A5113B2F02E54220981177BA1D4720A0556AC211B44509F0F913407560961E0AA084A0AE2C15CFA7CA77D9CB8CB683400CC72FB39AC7A758C0C21013CE6046CE2950762704BB0050D43083D85751AA5A141771DD5E9B4E1785E83C2B5997758B6FC3628DC43EFF472C73C4C15100BAE570513AA532296405C5936847976AEDA9F5B0AA704E35EFFE533840599EFC74F3A1C587C2640F2509F39FA14C0630F8393C96AB864EBC0B4996468D08A5515A3B4D71856AC31BA1B141D36E0794A6633167A471E199C7694B105230E0D11268E809029F72CB0C798F889CBF4F79418505DD5E9112F775C77D6C75E089DEE49AB0B685B837ABB7391B6965A432DD6CBA0670871F601AC186FF56C8C40169238E71BCD866546603F59B0C3D510371DD53923C1C92EE71037C47A8EF830132519F1666BC574AD1A74661A1C46DBA709E317974829CEE7104DFD284FC4B28110B06282BA8EB859CD60F8934CBA78B1F1653CD4A9613B4BCD5C32A3B30EF863A448D3A17FFC5908438800C5CE12475F3D5445761CCB25541FAE3653A8B8776256C7F5384481772ED0E3C4E512988E605E1C75A85606AA574442888B87F0A77C08824FDFF2B5FDEA4834112BF77C011F7855AA065ADEC66F505A72EE45300E2C9B18588E2F6B57B1C2B26F491166AA9D89D9BC2ED26DD311A81E4232FCC720CA8A788D5A00DB696763256F58B2B68BC32653C71F84088383473F19A76ADE5CB5B6BB466561AE7294770F9148E9E44B2FA1A23B1B99CEC1327F60A002728F21CB9D25AA79F526CCB4EC1531864AA550C72336509FB3061717BADFB2682EB59053333CEC75BD6E244F002D0BC2B1C8008737AB62CD42D2BDF9CA2FECE31DCF836264F84F76569A16046761885C163CCCE986122A11C8911C9257B88FA2CB792BE55FC14048BB995F11606E4C3C82B3113D99B451686ABB1F129E22D63EB14C6C3EC2479BD0243F57876175B138BB0D42313A28C67F1134733F76977BA87B5A11523953947B684282905F2AE90F5BE1CFB0088539313A9FE2A3AC9729C2FB5FBCF492C5ACC90A74C934B1A29AC19D2E20CE74436742A1415A9A0A4CE3756C199AF7593A7685C02F19768C6A1F4859B04B0186C63580916915053404054929F8B417F6A3919882C8D31B220F1817DCE08267DB0EAF61301C2844CA4556F813279D122C43A8820560A09A18AE2807163547B9A3E22F83E14AC4DC3FE69A25FD5C2CACB6A67905C9C754565EB5B17AF089C4653302AA830375B9CF0BB6D43904DD8277D50BBF4D43ADE05918ECFA74DD432CFBD829C4F9838DD76EB75813DCF397E3507FC9C6BD69C2B220644D3EB8839769CFCED608F98874EBAC4367A11274317297DBB424B95B9062ACB4C83CC5E879E6688447F26C0D2BA3763406318BCDA3228CEB0641C7250DAC193A8713744A6ABE5F6C106DC4049CDB49FB946DDB7A08D2D73556648F4CB82E2AE989ED61677B805F3A61344A14A2F3DC3964A90E4B83CC22AA8E7CD532BAB55880F212751C4CFB93AC14F564AFAA723FF6B31990A88DD32A557AA86EC2ABC114745C8038F4C1C924258B5BF02134605923E865539425F10424319131B0CB17D72C2981A944FCE3CD85A2509E93BE3AB5A65E53380772A56D74945D60C57A112FD87A2EB7989CC14A973CD05590495122811A6D8992C0064C77A78C7E390C43FB695726B7582C96C50A8EC5B4081CAAB6E9B64870166326A8ADA4731030290CD444B9E28149E06BACC7A79C5E7B24BAC292B7F92C7AD720F957013E1B118B3A40095C705DB33DE334ADD011CF3DCCBACCABA3B12A6FB5962E01C7C8D83912D9F98ACF8B1BA073B25AEA0C177074E7EB08CFF3CC7D7709E723340BCC00B014A0B313008DE52533B72393D70B3EC767A8B6CBAB61B8B7A9C86B263A1528A9FE366D60104C0D320CEFB76A688B407C0742F8BA4AB8007A3651C0F26B81E92746DB701837B575AB00A6B6D56FC5C12C956685ED0AC3F0C6A5870935298640E5070A8B74AD540CBCF273705C68115A828F72B09C83551714D67455EB46DC0BC14145CC38F5C1DF675A3677ADD28C69F0D462B1F1A0ACE8AA30B877382280F903B7DF9503E4C5048F4368B2A58565552EFB106E9AACC75562C31E3A3FE7790EC9269987C627EFF798A9A0484F215A3968525A0915C4B4AA7A8170EFB1466936A8B8440DAD499CB2F94373CC91F62673B59A90C728A818247513445D5AE25D85AAAB0036135DB5BD74D106AC557E0E8754D1E071B5B62FFF7575847B47905BBDCD5958A7A86852C34DD01286CBD50F0CA4628E31C5EBA531DBC02C9DE1BCD82C218C2CCA87A42A28D98ACFD1103F80AA9073C127381B4584C923492960537A28B868F214251E328FF44CC984972949003916B76D89D472B97013A52141FBAB0939B03D8D57364A387FFFB4A59D7B5D586680D7F54DC27AA7BAC39580103C74621E6D8970176AB2965ACBF453A7217A3C21769449E9AAE851090B52390642C467C9A233501110F059EF4A1A02BB06CBE1084A08C85AEA6237DCA55E2698D177716E662E2DC711D475C2EC9745BB061A90432C6463BA827BC32E0B0FA6B015324C9B574078AD823ECAB9010D0BC640C0A20E17317B2A30AE8AAC4C94695EAFDEB2BDD14D78D1CA07CEB411455C0EF10D23FB50CA524A8A240ABCAF9BEA0AEAA9C7EFC5BDD617D02E395BA073E0E6F8E621D501F698174C242417B71B8FE5465BADDC9DED85C393381CD5F07E2B05B9437BA39448\",\n          \"c\": \"E34FC9C40C386D57D2086BB28B5907228054405BBA248DE609729521BCE8DFB4A24E4742B780449CD66C32D969D8325550FD1545E5008956F040E1CA31358B79193044F77A0AE406A36D28C53A50678570A880A5FD4547BD7C805F9C4129FBA6B0671BD997F696426AADC8BA7682844D43089E33C622AF15BFFFEA5F622B65E74EA912CFE0C7B6EA3693E30DE4DE61AEB0D19C42B94CD84D72016355CAAC4C450BB4B8789A0BC67BC1785DBE9EF751E516110957E5CE8B03B59557343424472888666D8BDCA40A2700711F45C398CD92D1969CAF226EA229D1FD0666D831F92081A7A4C0B3033CB7824339E15C2E92AF53420F482E8597D6A47F1DB85C2680424E4E6B465E844259DEDC4C8D7C36AEBABA1BA872975C0B4D09D3881161734BD52100CFC3EA63119DF2CB1F43464584FC689098798CBF89057E69E480FE292DBF4663CD797094E746C6A99A27B88A7ADA2960117A7649B98F5CEA23B98952C77F5101061BACBD974256EF9F9CBB47DAC1978D31F257A1FF7E3C7291BCF3C466F815C019E2752DCD250A73C90F361C5A183A7F98FD9ED2E26D8BC3D3448267857E5C3744425F89941F89FB1D0433B107198E1583BC1AC83D903C40D4A357BE69CD3E840075CD315794CBE003D3C4E7FD04650C69F86DDC7C375EFA202AE129B1283C65F9E251F94000BCBA61AA0231A230EF0224BD6D955B06D2F3E2C978C0A9D48E68EC967C886B39CEB2B0C538551B89E6C71E750480E6DD1249658AB6C8835EFFE9593A178F619DE6BA506C2805E96B2C6D52584FBB78036D35514945AC008DAE2CBA6A1B6845320B3810C66432D628B01D99241FB70FFAA6842330D1A4361619025F24B3EA7F95C9892B95FC60F84B6230FEE12FA4638339792B225F04798E574A95209ECE2C4BB1C0C1A021820C5C6169095FA425693948D3A6204362B855D58B8B8942ED05DEAC54FF776F7F6296EE3730EE0A48B143833D7C1D30AA9266202B66928D07DB9D22E893BF96AE5C84823AF84ED4C32790A959F5C282EA6D8F4488A587EB3EAA1A3A79DDC74AF8FD8F093E2C43016E9954733FF50A37DF4CCC3DECDC2C433BA22C6A400399DE7567E028758D0AE28ADAC787109A94C75B428D38757D3E14D4D4F332CA1B12B5ED2D8526F89BFAEE860ADAF3A7A68B2E18F1099B95351D13B36FBF624CF3493FDC5AF9E6E2A42F66760A79D5BC3778DA0673AC2D374FB101F4A54133E4606F5BDA0888E722FC8D721FEA7FC02F05C649BDB7C1B5B0598BCDEB6B4CEDB58EEFE2C85CB732A2F17B3C74C6893D051BA1806963FB75B3F4BB54D03B208534BD49709E7D3FA752F71936926B03C3B2409717D6F6670B4C448FA0BAE47144A90E175524CEE0595C9D1C2EFBB34197BF4095E4255AD1DA5D74C854C03934AAC68C88629219563AAF017732BBA45049F4011056AE8F624D4D2A7B53A1DEE0DAABAC0E0D9E6D1B7B910CD8C70FA5BD58A078BD3821ABDDA5520D8C3389D268171A433ACE1E247994BB54D78B06BFDC9C71587D9B5EF9C202E15A2977AE436D3E2401D78E6E63E74C1056CA5EFC62D978AB473E8AE49BBF81ED62670BC860909F294984453930E53733DD31383D804723848CF0DEA6407737EC575D6C9F11F80068855ADB886D5FF956E70CB09F86D5D5548FA33CA645E3ACE04DF2F8CB4F6FB283CD6274188C90C6A8DF1DE37EF648B73CC33ACD56C914D2F083CF7E3063EBA33C58F0E261E8F0CDFA45DBACD99CF7290FA5F8EACC9869CB88E4ABF16EF483EDF5792193BE06347ABF719439E0AD27CCF613B688201585730110FBD2584348200A110C4CA81FB2CFAB41505B345577E97A994FEA7363B3B0CBEE900CDCF41A4EA067BE2451214F878163789AE67A49739EB09F6E8B40F6DD1B0D8EE039CB0157D0DB71158463CE866145A62DA83E7D3CD10C99153307ECA66B1B076686FF5B023F532F58B2781E44F8CBEA51C07FEAE217CE1E202A4CF8662C6D848FDF432AF34C6662F43562D5D81374C72A9673B379D0DD2CCE47C6224CE16899172DE2F26E039D82C9DB631E8AF85E326424C299BEB570C943F8ABC251CF1AC2481BE4E7B2B8CA0247195730002E69748E6BB8A7E1CB22AEB7D74A651BACE2856D114CCC994D3B833AEFE17D7015D450568E776789D173485D389C136DAE32459FFB88EC8A46C102672F75DB509AC6A486D68BD9780B76B2FDF34807\",\n          \"k\": \"D82B00EC3ECC8818741F6CBFFBC99350E84EE4D4A104214774525D8B78C93CC5\",\n          \"m\": \"6C8C075658F4257D42010EDFB1D7EA290D3344EE6E4C43DA799366985AD52243\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 59,\n          \"deferred\": false,\n          \"ek\": \"F1E27E473692D70B178B466FB72CABA716C2468B308C0689615A0077215741D70A633734F0140F4479BAC081370D3565D24A4D0D7AA189F1C28792BAFFE9928436B9C0215FC655253D645B3B84C237212A7868131AA623DCD8BC2ED65A2E082C4EE29A978B0C458C9E5BD656ADFC68BF64291C387886493D12825B8363932AD17B8752BB49B63D074A508AB3CDB7341ACDC5AB76B8C5F9C93FD4EB74CD9891BAA467DDB28E1733A4260A2A4D496A3F705E318A042AAC818E408DC7A9BA20520E3A4CA237C1C581598D839B67822CAEE8C04E7BA6219ABB23D9455917B84CC5C94BF8594A2D1127601BC3E76B54155B1AD8C5754D13B564906E3688C3DCB9B963B7295310341EB0A36917919D289C2A1B001CC4360285A40DE87978FC532A242422829BF8079D71F347EDC072B9884A5D26C84E07B72E529DA1FCC8E57C1C2BC30685D12635B3A136755A70224D79FA7AB1124A69701DFF4AB135508014FB4BCF044218D4918790455923039AF9591087A5ACAC37363097C41B4153D375C933C488C216C5CC1AC56134F9B3567519CA21496A4CA06AFB84C0DC2743673AA94B15867EEC0945398376DC6981C6A9BFBA78D9950E27E16D3852A63CB08BDCE1C99172B4181654BF5A90496C764B6B96A25331E84162F902009BB4B76D8BC41C08AB7FA8B1C4657AB51CAD6FCC324E4BC70979AEE6125F2BF916FE8C3F4F5A72A695A6F8E34084728B0FF59CE1FB5161D8AF07CAB498D23475869F746563DAFB49B64103EAE0CE8A17BFDF1853E743262593C327608B27614F32C1ADCCC9B638029F7F9377B302248017747AB5553833ADE94A3846D309D5BB9872C89AC4E7831D9A3A3FDA26A42747E96AA6EE375FD407585E964CC4D6821323C7874986A1192FC0C62099A764A4554B8D89B9D9585A339072E9D677ED3475181197EA28BF6D84A3DE367D7529AE481133D1618007D5832E0A13B7028BCF5CAA36F4861DD22DAA497167E171CAF912B9FC8056016520ACCE1F68397246097A5755F8462FAE3413408264BDD915BB253A6229C26B14426F439D9AB79C39B11A7F51B9376A300C7530744B754713A50D71BDC299B9C0C7B266CC41F31AB512D0BCF5AC9BEE85BEA109734E7861646A7160BC3A27AA5CA3F87128468F3266B2B156210AF79D03890386A9AC0CB71BA3B41937960E575494F60B0704960518399199CA227D48B99ABC8E85C1795DA67C0D331B40CA8E448A934A38CB24D0730E181A7B30366335958D5993385871DE69C97B22A1DDC6BE8C5242BFD93073893D2B498627B76B6D39ACD7405B64C16E60E190CF45B5EB12735572AEF8F99E37CC3C1492979D3718BD363EABD56E1E416ADF9CB106F3540ECB4ABB052D57C728A558AB52FC4FB9C078374919F29558481C3ACB782959E2C41AC05B8701217E7BBF945B970298A694FCB0E12490A6C5665E267A8AA1A5F060CC0A08093E816EF9644720024FD0ABC5C30C39EA095FC5426E732C2490B288FB38AA6A3517D16BAE8F38BFB091BD70260BE1D80B2694B3651316484C1E97B714A67A0416179F2C582E25F770EDE422453607ACA79C1EF217C2418840609FEDF914D083595C10CC4CA70D40A8BAC5470DFE4638DD86A03DD428CC305569963CF620931B40245FF3098DB6C6A1870D5D7B838C976D0D0B462F441B2D79BED3A488697A191C20C0A8281CA8A39D61E69A0767CE753A02EE8987414AA3BB99BAB032CF451BAC1075162CD1097B00AF6A4A82F8112F28861BF0A6AA22733DB462BD15F95A211161ABC0C5B5330FE4D63854F3C0F074C077D06AC2839F06913FBAF33F7F91A948A19D01D6110AE54025CA068D1B0D7CC66A6EE0BA83E377FD2313EF44C87A58354D841B0158C8010B6EEC2898A9A0778072149452C2B6A628256C257FC224C0B767DCB3710071A71B43015D994ADCACCBF7B3A234C11FB2B56E75E4154DBB473978BC25758EEDB5319AECCEEF44AD82C3584F8378437763C3055C4A84AB71815EE5E360AE0A363286936486C69976AF602885769C1CBE21229B9C350562AAD4A91BE145AB3B090B9031175EF96CD275CDC68444BF890CAC56241159C55A474647A4257A978470BC97D28549F21590EAEC25D9603ECB5909D375CAA56B076DE917B8C7251C92B34563BB56AA6C3749113A837B5F6B5496259F5EB94195E2AADEDA453309F91EC2AACB59E01FBAB4B8F8\",\n          \"dk\": \"01105ACEB10675548E2BCB6C4FA56A7B70C0824039D35A5B9BE38DA1187274C8135C24692024A44430BE5D0834CE64964F7CC0D2A8C86A6A64AFA8CA4F2BAEE347C05374B6A58A764D493F1F765122575A31AB87C5278B6E94C8704880795462E3E16177B1C3ED4B4432194148089B5218777BB3927B4650CE44160C8ACDC7153802A266612C9102289A73606498F3A6F79001E37330C5A848908C34C0C8559E056537C25309B10FC4377DDF305167D41714A70CAB02133C500627664D84359173DCA053922D874C927C1C0BF86468730254A9CB279DBB19EE0929E9776A20E3574C4403DAB3344DE865312B2CF49301DFF86BE06481C3EA9F7969AC5AD777CB9C8FC887B2782885E8F97B13728EDE4661AF68CA9CE80208763831F9385D5547DDC81ECE830716557242072039C469E9876247C53D1847955795414A6946FDFC6786F9389CF09EFC021B7B2796B4A2A48C0A3DCBBB8256507CC0CB7FDE45B87B7C2DCC8C9210279E84DA022AE65434F139A70629D15882A1B05ACB847049E9B72E1CB6AA7647F09447EB47BB052B9D8C92850E72ABEFD734BFB55DA941905B99228EC17CA66BA82C541D3D669BFC3AA05AE6966E4B67EEDC3468A632DEA41C30EA5D0F943B53D828CE6879699933AC6C15D85A6D78DA3F84D744455784642B520DC4C40951AC26725E3723BB48A75BA247B5A4EC5C5E832DCB68861F4A6A6F8CBF13719A834AA2F6C9B9A99A7A64785F65D4768EB01A2C5B719FEC6418BC98428C321C40B7AB923C0AB107F59B5AD0EC37FD82850F64782293C1CD0700C062AAC0752CA91CCF5409147F9526099997AE622945A91BD4CB32B61569C1E1347766935B3C76D67B4D704B5D9E362CCA28833DFA1A089C2C6A2380E63C812F7ABC9BCC6B53380E6B9943009A1CB4325A2D529EE5E6A428BC1F0F190BD9E20C4EB761F8B6C6B5916371FBCEAC171E2E976927D8401218623E3C403B072273EB2D68F78CEEC20DBB89669E56151B83775489CC6EB9C11903A81A001FB6408F0AA2AED1B576C16B2AD71C4293832DADE8769833A7FE517F332B9AA9394D915838F8A62A7ED850DFB7AD82D94517087B4F4C8B4314A6E0E0A49CA96FAD929184030FBA82A86E9A214F3C5E5CB7AFA31A3FC5217AC400C6FCF18C61686564F01D0C5B1832794103E8A198552202E8CD265203009D0882D4AA4508727D2749FC0A06944B7071DC0FA2B5A849B0922F78C3A8D300E5D536792348F56630D3A75A74EA143BD5C8B93429B8F74B8239BD9FF54F2F293A94AA3F4B4B9D9D6434D85750BE959F15479D2CD0C7C2CC0DFB8C95990653B7AB2F32100C97688672E09E672670CDF4CF5B4714968C533D81997FE8A92996949603691BD2216A320B46035DF84B256E23298B2B3FCD12748D550120988785C6B72B20C3BAA22CAF8789C0880A86A12C8C6C11B022A908BCCD8B144C191A299D2A95426C5127447AD3D3A70E31B95F38BFEFDC0D781097685CCE6FD8468EEA7046C54715968B9394B3934B6229067636ABC41AB644F555B5DB95CC5EF25FEFF8186BA226EA95AEB6D76DB3B9BDE2A1C3BCB72CC0B7AF0DC4BAB0F0AB7B1A12763A2485B22649CA392E8C0CCBA819BE77745BB1C4CB68AA8A439A970A939006B3AF9C3A585B094FF7A1A7F7AED61085EE4816D632A327CAAFBB6899C4458ED43B327975A0E2325D5A225D7F2CADFDFBC016D439147B892B05A61FD34AB2D62BB462468E1A9F310307F33928BD16AC3626B484CA82DBEB405400801AE2A06CA624673C3BDD2B72DDD3641FBBB7952703A5349339813404810E554B7DF7BC18793A485E97535038919046BB8558545B87248E3B80852BB7D5A23EF5E0A396C0239D57582A03350110426CB0317A5005D558A5516727B257B54CFABCDA8B49D433AD96979E575A93AB2376515C0E31AA5B0F08B2B7B69E232CB3D8C7C809C03752F355CDB5118A11BC3AF00BF2513095D4A6594457098199544C0224A80BA18C02D5FB951B4118235B1C7E9C966FF891059B11DC566F872441158C61CA2326B7D171A50015AC663E0ED71612247ED3712ABCC121052CA5AC04032DEB786B91735D612DD4BC5CEB514072145ED6B2AF926037B7655BC7379DE4FB3AAC64A3428558B8871B5979593BD33E22DA826E44CC313A2EF1E27E473692D70B178B466FB72CABA716C2468B308C0689615A0077215741D70A633734F0140F4479BAC081370D3565D24A4D0D7AA189F1C28792BAFFE9928436B9C0215FC655253D645B3B84C237212A7868131AA623DCD8BC2ED65A2E082C4EE29A978B0C458C9E5BD656ADFC68BF64291C387886493D12825B8363932AD17B8752BB49B63D074A508AB3CDB7341ACDC5AB76B8C5F9C93FD4EB74CD9891BAA467DDB28E1733A4260A2A4D496A3F705E318A042AAC818E408DC7A9BA20520E3A4CA237C1C581598D839B67822CAEE8C04E7BA6219ABB23D9455917B84CC5C94BF8594A2D1127601BC3E76B54155B1AD8C5754D13B564906E3688C3DCB9B963B7295310341EB0A36917919D289C2A1B001CC4360285A40DE87978FC532A242422829BF8079D71F347EDC072B9884A5D26C84E07B72E529DA1FCC8E57C1C2BC30685D12635B3A136755A70224D79FA7AB1124A69701DFF4AB135508014FB4BCF044218D4918790455923039AF9591087A5ACAC37363097C41B4153D375C933C488C216C5CC1AC56134F9B3567519CA21496A4CA06AFB84C0DC2743673AA94B15867EEC0945398376DC6981C6A9BFBA78D9950E27E16D3852A63CB08BDCE1C99172B4181654BF5A90496C764B6B96A25331E84162F902009BB4B76D8BC41C08AB7FA8B1C4657AB51CAD6FCC324E4BC70979AEE6125F2BF916FE8C3F4F5A72A695A6F8E34084728B0FF59CE1FB5161D8AF07CAB498D23475869F746563DAFB49B64103EAE0CE8A17BFDF1853E743262593C327608B27614F32C1ADCCC9B638029F7F9377B302248017747AB5553833ADE94A3846D309D5BB9872C89AC4E7831D9A3A3FDA26A42747E96AA6EE375FD407585E964CC4D6821323C7874986A1192FC0C62099A764A4554B8D89B9D9585A339072E9D677ED3475181197EA28BF6D84A3DE367D7529AE481133D1618007D5832E0A13B7028BCF5CAA36F4861DD22DAA497167E171CAF912B9FC8056016520ACCE1F68397246097A5755F8462FAE3413408264BDD915BB253A6229C26B14426F439D9AB79C39B11A7F51B9376A300C7530744B754713A50D71BDC299B9C0C7B266CC41F31AB512D0BCF5AC9BEE85BEA109734E7861646A7160BC3A27AA5CA3F87128468F3266B2B156210AF79D03890386A9AC0CB71BA3B41937960E575494F60B0704960518399199CA227D48B99ABC8E85C1795DA67C0D331B40CA8E448A934A38CB24D0730E181A7B30366335958D5993385871DE69C97B22A1DDC6BE8C5242BFD93073893D2B498627B76B6D39ACD7405B64C16E60E190CF45B5EB12735572AEF8F99E37CC3C1492979D3718BD363EABD56E1E416ADF9CB106F3540ECB4ABB052D57C728A558AB52FC4FB9C078374919F29558481C3ACB782959E2C41AC05B8701217E7BBF945B970298A694FCB0E12490A6C5665E267A8AA1A5F060CC0A08093E816EF9644720024FD0ABC5C30C39EA095FC5426E732C2490B288FB38AA6A3517D16BAE8F38BFB091BD70260BE1D80B2694B3651316484C1E97B714A67A0416179F2C582E25F770EDE422453607ACA79C1EF217C2418840609FEDF914D083595C10CC4CA70D40A8BAC5470DFE4638DD86A03DD428CC305569963CF620931B40245FF3098DB6C6A1870D5D7B838C976D0D0B462F441B2D79BED3A488697A191C20C0A8281CA8A39D61E69A0767CE753A02EE8987414AA3BB99BAB032CF451BAC1075162CD1097B00AF6A4A82F8112F28861BF0A6AA22733DB462BD15F95A211161ABC0C5B5330FE4D63854F3C0F074C077D06AC2839F06913FBAF33F7F91A948A19D01D6110AE54025CA068D1B0D7CC66A6EE0BA83E377FD2313EF44C87A58354D841B0158C8010B6EEC2898A9A0778072149452C2B6A628256C257FC224C0B767DCB3710071A71B43015D994ADCACCBF7B3A234C11FB2B56E75E4154DBB473978BC25758EEDB5319AECCEEF44AD82C3584F8378437763C3055C4A84AB71815EE5E360AE0A363286936486C69976AF602885769C1CBE21229B9C350562AAD4A91BE145AB3B090B9031175EF96CD275CDC68444BF890CAC56241159C55A474647A4257A978470BC97D28549F21590EAEC25D9603ECB5909D375CAA56B076DE917B8C7251C92B34563BB56AA6C3749113A837B5F6B5496259F5EB94195E2AADEDA453309F91EC2AACB59E01FBAB4B8F8839735FC7BB6B7B2B3ECAFF53F7CCEA5AED1A76414F3B57EB29825F79A4E7AD90330DE5C761E9371E9BBC4EBD1B98C390180BB2BF749D427500D5F562A6CAC38\",\n          \"c\": \"17EC1004F9E3F5AC1BB90F19D09F7CA08983179820FC9B945CC220973112318E0C212814C5F852B8E675B392140C4B2E20D5B1E4F972CBA5CE389792DBAF7C068C17211C376CFB907FA4FD468835703F559CEA25E0A12F2267326894AB7A3F4D7D83D9D5C98F922F16DEBD6D77663D2421A60F54248F5784A4D5AE151532E6573B8FFD81421B3A7E3FDAE32104F347049785EDC6AF47A417EA8BAAEC8B89E88D3B6870835EF552F7CB57E480C06B3CE95D238B460BA40EFECB0F6C9510211F02C92CBE6B4D7AE23471D187D1AC95AB0C33D2E886E32232427C1BE7DBD3342A4396378E263D7D64CF996B76ABE1BC57F12E55C9A4789B20CC087ABB217A09951BF4CF2778304F95231C05BCB803AEFD0596BF1164270ADAD28944771BE9B5050075F3F47E5C3FB5859D19E989F4E03429E1A877CE9D65FE605FA0B10F7062A003BA13614E35C940204D321D1676DB769817FAFF8D1C02321748189BCD6CBB961858FA080326BB24536A29CD19D7A25D7818FD212E28FCCC25E1949F8F6A0EFDBDB402710B4E0E7EE67C8CF475E2E0CDCD29B0B8F52712550499E24F0EC0DFB8DB333ED1C5B8F1B3D93DE676AF65ED80CBC1406E6EE78B35EC607986130F85EC3766BE06B01FBD1C93F98F8AF8FF8224CF7F23DCFD9B3CD4576A933672AC1817114BD218647BB5AD70B249F65981C2F12FDAF575A009240B11F92702527692310719D0EDBC87BD7B80D0067381BDAAEAE5FFBF82E9487CED9C51B5A2689C338E410EC6200EE40289166DD37EFD87CF4433FE78E470089DA0B2AED03EC4601B1BA3EB4C85A261462F32B2886F6BBFB6C509E058C2CB3643FE5DAB864676ADF3AAC9C4172E5BAFBDCA0BE501BFAD5A35EAEA5608E1D2200361E581385D640C2F71DCD585B6C9946F455A071DF253EAECBF61E1ADF160BF32F4AA1ED1B4F35B0D6FE5F83B2980EB81C0E4DF06CE50530919920AB319D3233DB5A5FBDE2E33DE18B66F78045F79EF9536C4AE168689B51F54619324BA1FAD9A60C406041BDD8B01A2D83C0406B72F5A6854625F41BA1AD27E15309C9763ED8FF0FE2CBEAEC0033EBE14B211DF23D16411476B637688C6269A0B7A9CE57A344373B948B3210F8666120B6A5F4F5EA238A8EEF5A757C7D20E37835CFE472843F94C043E12AC6F36EB65075480DC580E4B7510D7D6B49A794FEFD6F0FEE8AA3477AABFB26B3D1D1F0D7D694F5B1BB2618A57FE655AC2EC19869DF7EF57C422ADE6A181B57F33E6FC9B4810AE23F41EE82EC760F571F5511FFA71EFFC867B8417D719E5986D366AD7D01CB021EA809E80C508D68DEACB4C5C116982BEE6BF99BC2D052430A29BFF82EAD2A28FC816065EBBB05E4A20722BC677D0CB1E1AAD732DA8F0D854E044B5176279F1C401CF553A565668794AAB06956019E291916DB406641A0B4A94CD2F94AD954AF203B440997C9AA315D00E9791DF171B724859DF1FE44C6AFE66C1FB35543C8AC69F5953A98B015357AA829605D7246555F20296DC8D8ACC312AFBA78920C6922AD1A3C895BCE9D54C902DD87334391D5D68692E67EE3D5E1471E4EDD20A28AD22B5EEAF2A27B7DC70D5C53CFF4884DDDF837B74E8581BB2473C969DCE8B55F31EE0098932A0BFBACC0428CCA1E130466898637D876DFB972B0E0AF10C1133A8AAA8703172DDCF28A08BF8698228952E3BD3B29D6DD22C72EFC18583E80AD5523BF3828ECA00DE5D3149F09BB31B588D2F3FA205CD00C78DE8D01DA73EA5956EB0FBCFF01A3C6FA7A4B6C08B23724C47989F7E999FD49961820A8F9C7E84ACE6D7FFED8EC119BA3EACB18E1E16DDBAA0503F227BAF09E5620ED738BCCADF3FACF7F57364865B61D21107EABE4E961B04DF62CEA1EF0E7604711994CF92CF1A8B7940CBA6212AB98ED5E37934CFE9F4122C7AB33F8666BD5F0C4B240C3FCEE1A3AF8A4574CAD95D68230F9A8E66D96079CF2D58FBF7090885E83D99F6810DDAD3B0A546F3FF71CEDB22558F3C823886E2DC089916F9164CE4007F93D6BFCE1AAD3AB69A4CEFEF87F430D73F95FE96A290A3A61E4F6C5EAC36377A0F0B50A460404590E5B1AE3B4499A85123055B73296396E29083EBC31CFB4C9946C7EC4A4D6D46ADDF64C4D9EB9FC81D4BE566C464C9F93EEA01E739BC8077E9B13856AB53AE9A12C3C85DB5B06FB3A47CF855240F87D4715EE99EAC7B9C8D1F331C811547DD\",\n          \"k\": \"8AF0912F3635D93D537E9065529A3D69590AB2E66607540B4ED97BF6D985AD09\",\n          \"m\": \"BD990171C3252230BE21FA7F186A121686187B77C234C37CA5122A7AC77E318B\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 60,\n          \"deferred\": false,\n          \"ek\": \"1909B5ED6C0D8D3C301F144373956424139AFF46757A070167BAA70BE7BC375A6D43E18FB0EB4A69B49A2D951E9265BC159A282B724F3477A2DFC89378CB9FE6CA8CBF55BE9976CEC668402D7A3D2E4CA2AF6A1E479621DB16A3187461036513C84ABE5BB228484023760266DE5CA11F1A8C63EA77F6E45F35D4CAB24443B51B851DD2C58EAAB9D426C90064AF14F8BD8984CDBB84C7E2208C5AA2C7E3A222249943985CC1F199BBAC9B8719849E5D539986D8B063A363B76953D22A508CEB03C46A04F19769E327A774A063B1D412B7B43D5651051E547487F2293EE2B78849BD36D2A54E966F8B386BC735152DC34E1A12772F67506B6474A2C57AC0A8CF53122949D52828D69E8A7A326F18599BDCA23C8C8E3B28624F26609DA18044411A9866C3A7D7083F09707A70192CEA40B936BCE696112AC92582428F30A800876BA7BB40560561AC5996A4B11639BDF00DD67A52F85C6BAB54055C2352121591260B656C43B2C2E89A891C97F39B7921794CB5A552C05A2D8E8BAEC89250666734C2DB73EAE68484DA254969918B10914CF4274C2C27319412360433ED27C810712F4E3145B5265D1EDA7B6917CFFCA61CC087926CB116020C7C49D1A8A723C197CB44118B3861A0773D52C6CE25A9E2C66BCFB6C5BEB43185EAC855D1A99C0A2FD5A348BB0C579DC23C7E777AA57249008497C7332EAD85C491A89B76427835983E00F5856B1443A60C83C714B122369A97C356C0755FE1F249C8D56F0D9897680CAC247A7EFA8386744B607BD7C195B52C07001384C17EE70AA04393C081EB43C7F912F3D786C7E63DF043A277329AE2C90A0E721C5713A47417A9ED1370C2761C1C029630C26DD215028EBA6CEF5239C25383F6370863F622BC055517946EC0D2C4CD909182CB51C817A04C53B4309B14507069303245B4B55B47226970D269D87340DD5A864A85A62CE3115AB7A972C813FD18C9AA0B582E79C228468292A8A133CA5C53A4C842233FFFDA7DD0002444C51B2009654067C7AA34BA17B7368745C1719572A2C66D6D894B92049EBDF629C7C5A2D1EA82C7791B7840729DFC7975C2C18F3B319B45AFD1E0BB0C527D3FE8BB7E95A7DF958EF9268ACDCA208053ABEA894928DA891788177185B362EC3364F93D4B602167942603412CCB595DA8910B748A2EC022C3EDA8437781BEC4A913D2C56043A1BD6A5B2A858A8F7742A82986509891A9EDA7859D3A99FD78A11335910D830FBA1B13823154ADF8984DB9066B6735704196D49C8771B870C05504EA110E252B4DEC2743F1F39142B6BC5B635110CB37925A3CB0AA83858A54294B29054805B969A6352925CC53017235CD75DB70C86198EAE30AE8F933412C0504DAAFE7516BC9D7A780E04290891E5594819F08C2ECF083711A85E1A679235441976B03E37B0B0DAC6BC8456497220F2D4B927688021FF90F2E00A64DA33D3C878462529B98CB709B01BDA6D27FCAF7899BE90853BC6C8652137851C3C7FB40CB3647AD8B7B081B46C8A2587086426D48819AC1AE43C429954BB5F7F2227FD8058C18AD81203462C055ADD5045BF48CE22271C5A67E20ACC0EC93B182684E2B230C0E44C0688A2BA0A6BFB7D10C476B24719700C70A7EDEC0C6879345AA781B6B415FCFE4373F78BB33CC62A659B8286154073193894B6D8AE85284033082186478FC763231154518B1A8A59B1CBBB56A50A8F9C38223C5BFDA5C310A1C30F2569C4E179D4039331293CC18870E1D1B90E0008B29008FECF00152748FA9248B7D64491455044D86C6C3565AB4618233A2154BD277DD5067B2E6784DA94876D1BEC361869493542C470D7E58A7B6F9236E200D66CB16D70A6273E40C8C69C033CA8EBD7A2E5ACA69A5E0CAD2C352D0F36BC8A0B90A29B604EA0DD6A77FE9505E717A721849A0B435C08C66AA44E202015CAC90D364D1D83F21E5177A100A9E7855A5C5C0A4B46B388C58D256088E455D59016A4DF337C8A69827A399E2E9BCAAA7B11CE6440A7536D0BC544CDA12392340504088B8EC6CB737C3B1C0A11F9B83C049759D74BA44507C6881B1F98167720C0B35A095DB1A7659B52E38803935241766237E073B1D2C52BB5F4152AD80625E7C52352AA4EBD89820F4AD1DDC3944784A239A9F71B243AB50C1700BA6FE82A501B275DC3391EE30C022997F00F09D0F5FC8A9F5B02358F99511C32A582B24C0\",\n          \"dk\": \"5D802C21CB29AE541E06365D0E295C066C199C1668BC5255FD02C99323C31646625E2363BEF0900E13BFA27307D1712BF4D3AFFB80383A5ACCBAC575EE02980EB855245121E6359AF388B57AB94C47989497C38A09E517E6726316F2474ACBBE2E999A16E9989B8951C8DB8955F790E61B5D3F7304CD9A41BC64C8B692707A4418EF05B57CD34DE597172F7926A46868B2264FE7521289654944A066B63B67CE375CA069357D9B0B905AC871D0851DCC3DD49847A131AB6F92934CDB794A28A1AB07309811BF06633A5DDA037FF3BA5021C251589738C2A794721E42735D1EF733F8F0812BF12B628158D83577E0107E6DAB92361CA40A904F5BC3788FF609F04854B38210D441222128B0CEBC0F6CD25D6AC3CC14E616EB4A0D6E500EDF5BA06ED7ADF647A11E365533D9C3ED2C93CFA08FA715BE8A71534D007647FBBE164C4C077094BC372B64546354235861434192B85C68B7AB3610C15D5B59A30C1832F4659ACA2FA99118614C804AD75DD8FC51B2772E7225858CEC196B60008366C1F1BA8C837A4539EC10C7A14F938283175121489019BB419350121EA18284DD14B8BEDC9304CA42AC3C9E5674773CE84D621072F90873212669988978A9C3CA012610979A4BBA114A0F39BE7E6820FE881948D444D6254887E362FDA99407446584D75D78B848DB869FE5230FA2093B5C170FEBCACC3D55B1BD6CC9EDD2850FDA3C9937648DD614CBC86640453C05B403CE145C0A1A7AFF29A05663432DEAC8FEB4343B054692E923C0B99600F56697B77394CB6DD8618605EB5CCD5B6546545056081CA8A12FD38787C3FB759BE18F300B4ABC9BBE37E2873521355BF7091C6576A792B2BD6A42F367A3FA94310B14049AD8A0F710C6877A481753A0B42A8979824F1F850EA961959E87113CA2608C59AABCCC1332B5394B1B8EFF2A486AA7B0CB36361DF5C2C91AA039109F99249379B4339013352AFA68D41A0800E02572539D557496D944726677399BB136DA27B248A901ABB76890D10ABDA81172172BCCE82807A1900D2AA5D919B0211BA64C9806231C543EB6C35D2B27EF96AA2FA26C80742029D0BF7F72676CD0369FAAC07CB99251F69A0D45C952785D48655AC3B74A1E019FAFE1430B78B7B5DCCC42D6BF5956B6333925C13BAD2E853F0E75BE0D78935695214AC252EE1AA0653BB9C5330E2508616F4057A49C7BBAD11F10422B84E5252ED320611853F39C42C9E811B0B458B0B44142BC16D017B2896C8C223B96AEC573BD43AFEEBB34E4260B88D91238F3861E587648A6BC74128CF1024B034755CEA4927202CC7879A0AF778F04C47888B13ECBE48B7F08A4CEE7CB97A4C272B8A014622389281EC7E996AB1024379A5833362C420297AB97A98248CAE6E0A65B2644F0DA07C7193C0AC096ABD59D24603ABF827DFC6B7A5B7CACD23090AD900BE4E7C8E34C736AB4CE71750D537784206489810BAF00E2B3076628D0B13C058264D13B5B67064C9DCA02DF9C93B5002091347EA1522B15228B031276EAE603B88B384B075063569CF8E7634E598F2D872B238287B8BB0A681A4F3D711497949E93FB91753A0DF27258F79B3D8B44A62C2929E265789843717AC518024BBCB23C9EC658891128CBAAC9010E700B7371C37F48B75059443D89CF35C62294E68A1A65B19E7900A80B59D78808EC462AF659398D934077A3785F999B66C60059D24601D83957597C6EA99FC5E95E4B436AD988653BA05A9FE3AD8118237B1A84D448498B426247FB5DE4979209C06800D6A9D39543AE787A461B1E433066EF2B2FB0A481388C5A68557915C4B8CF7531120C1D340133F5D5705E59136531265E3B0B4A1A501172AD8918C1F02A7B06453D8C05B48D561345050B49F893DB946AE26B6245F1926CC205E6F81A7455935B775324F1298A3BBDFFE53ABFB950176C7F65820A43C519FF596E898166B9A1563CBA7CC971B73077AEE1DA384E88887289B63E662AE7984FB8F974B7A42CCCF17750B37FB8A9424106275F122DFD9245A0A28A319CCD49D985FC58A1AAB178F12B8B3034AE7AF89532619EF24A0A77637C0033981103CB5CF9AB09D879EA067193427FBA219D49A5A35F95137EBC4A0D046186BC9E3C091B14195E2B43BC5A79C0B793C97E02823EEA301678723815631909B5ED6C0D8D3C301F144373956424139AFF46757A070167BAA70BE7BC375A6D43E18FB0EB4A69B49A2D951E9265BC159A282B724F3477A2DFC89378CB9FE6CA8CBF55BE9976CEC668402D7A3D2E4CA2AF6A1E479621DB16A3187461036513C84ABE5BB228484023760266DE5CA11F1A8C63EA77F6E45F35D4CAB24443B51B851DD2C58EAAB9D426C90064AF14F8BD8984CDBB84C7E2208C5AA2C7E3A222249943985CC1F199BBAC9B8719849E5D539986D8B063A363B76953D22A508CEB03C46A04F19769E327A774A063B1D412B7B43D5651051E547487F2293EE2B78849BD36D2A54E966F8B386BC735152DC34E1A12772F67506B6474A2C57AC0A8CF53122949D52828D69E8A7A326F18599BDCA23C8C8E3B28624F26609DA18044411A9866C3A7D7083F09707A70192CEA40B936BCE696112AC92582428F30A800876BA7BB40560561AC5996A4B11639BDF00DD67A52F85C6BAB54055C2352121591260B656C43B2C2E89A891C97F39B7921794CB5A552C05A2D8E8BAEC89250666734C2DB73EAE68484DA254969918B10914CF4274C2C27319412360433ED27C810712F4E3145B5265D1EDA7B6917CFFCA61CC087926CB116020C7C49D1A8A723C197CB44118B3861A0773D52C6CE25A9E2C66BCFB6C5BEB43185EAC855D1A99C0A2FD5A348BB0C579DC23C7E777AA57249008497C7332EAD85C491A89B76427835983E00F5856B1443A60C83C714B122369A97C356C0755FE1F249C8D56F0D9897680CAC247A7EFA8386744B607BD7C195B52C07001384C17EE70AA04393C081EB43C7F912F3D786C7E63DF043A277329AE2C90A0E721C5713A47417A9ED1370C2761C1C029630C26DD215028EBA6CEF5239C25383F6370863F622BC055517946EC0D2C4CD909182CB51C817A04C53B4309B14507069303245B4B55B47226970D269D87340DD5A864A85A62CE3115AB7A972C813FD18C9AA0B582E79C228468292A8A133CA5C53A4C842233FFFDA7DD0002444C51B2009654067C7AA34BA17B7368745C1719572A2C66D6D894B92049EBDF629C7C5A2D1EA82C7791B7840729DFC7975C2C18F3B319B45AFD1E0BB0C527D3FE8BB7E95A7DF958EF9268ACDCA208053ABEA894928DA891788177185B362EC3364F93D4B602167942603412CCB595DA8910B748A2EC022C3EDA8437781BEC4A913D2C56043A1BD6A5B2A858A8F7742A82986509891A9EDA7859D3A99FD78A11335910D830FBA1B13823154ADF8984DB9066B6735704196D49C8771B870C05504EA110E252B4DEC2743F1F39142B6BC5B635110CB37925A3CB0AA83858A54294B29054805B969A6352925CC53017235CD75DB70C86198EAE30AE8F933412C0504DAAFE7516BC9D7A780E04290891E5594819F08C2ECF083711A85E1A679235441976B03E37B0B0DAC6BC8456497220F2D4B927688021FF90F2E00A64DA33D3C878462529B98CB709B01BDA6D27FCAF7899BE90853BC6C8652137851C3C7FB40CB3647AD8B7B081B46C8A2587086426D48819AC1AE43C429954BB5F7F2227FD8058C18AD81203462C055ADD5045BF48CE22271C5A67E20ACC0EC93B182684E2B230C0E44C0688A2BA0A6BFB7D10C476B24719700C70A7EDEC0C6879345AA781B6B415FCFE4373F78BB33CC62A659B8286154073193894B6D8AE85284033082186478FC763231154518B1A8A59B1CBBB56A50A8F9C38223C5BFDA5C310A1C30F2569C4E179D4039331293CC18870E1D1B90E0008B29008FECF00152748FA9248B7D64491455044D86C6C3565AB4618233A2154BD277DD5067B2E6784DA94876D1BEC361869493542C470D7E58A7B6F9236E200D66CB16D70A6273E40C8C69C033CA8EBD7A2E5ACA69A5E0CAD2C352D0F36BC8A0B90A29B604EA0DD6A77FE9505E717A721849A0B435C08C66AA44E202015CAC90D364D1D83F21E5177A100A9E7855A5C5C0A4B46B388C58D256088E455D59016A4DF337C8A69827A399E2E9BCAAA7B11CE6440A7536D0BC544CDA12392340504088B8EC6CB737C3B1C0A11F9B83C049759D74BA44507C6881B1F98167720C0B35A095DB1A7659B52E38803935241766237E073B1D2C52BB5F4152AD80625E7C52352AA4EBD89820F4AD1DDC3944784A239A9F71B243AB50C1700BA6FE82A501B275DC3391EE30C022997F00F09D0F5FC8A9F5B02358F99511C32A582B24C0011C0579E0446E2C171BEAF2BD014E13D2B88B6515E2B8A11CCB8FA4B91BF2B8A932A47B71E782BA97D69908DB41682AF409C94C050DD621CF8D958627D0FD2F\",\n          \"c\": \"588B326FAF4C640216A4E3DD75FFAE0D4E6BA0B6AE4214491C3BDEF276E98585CCC730B0188706E3CB275EECBF0F023EAE4E4A5D07A68D961EBA5DB25061AE3C76C2FBF6B898D90C44E479E2859F0245D579032146BB34AF36DC16A9CA55E6FAF15A6D53C5A0554F9D5D39582AD6225A1729C4F3672C5FAC82AFC900740F7B738D99FFF2E4A660BAF194E2C129CE4C6DB57859C8334D859D49F1FB46B55D6A0AA71CFA726E6289E808AB016129CCCA273A56E78812B1F1A390311286E9C4F0E8ACF6806E9DB5EB2BC782AD0D68FF394331BE7DE253AACDE455E4185C81E7DE685B7358CED67FCB92DA724A93AA86A09D33B504DFD0DC2A5F113168E6DEE9098BDEA0054B3035142503A5AD671B5041113AD0395A40A476DCF52F2C41CAD9A862762639ED23205A90EAC964B68785DE9883BCC7EB43CAB6126A116F1303B53C0DEBF9A574F3835F3CD791CEC539CF15C4C20894013F21C3E903E39DC36B230A505D33F6F81A713533494F62241E1ABE839FDFA972648EDB64D3329CAF8786C4B19DE97A4188A5D2AE995FB45333ADE7122BAF902062B56E0C5A34732C493A2F4BE714B431B6E29AF52AC27061CEE02F05ED5A96D71DBB42B06C3BAEE5E23136B015C9A7DECA77AD6A7850B58119CFF9F445D1F36FA628564F02F1BCFABA5C2783469CE4CEBF996F6BE9C2FDF5210AE428C221039BB4E343A09460C81DA72C43B52DDB44616CD03BB9F1319AB399FF44837D14966A5BCBEBCF7CC482B1E691E20D2FDE85CC327011D1A6E5514641E3F0B76E5B6E1B403A76F735C785BA81CD53B72B237B18220F9EC51BE811CF614B454BA43FB58591A0C3385421810E7EDE6895DDF6566C1B265DF21965F9BEAF6FD3599CE636E66987F2DF9559D27E04E37F7428C205DC52061B92238777199ADC0F5A19FCA01617129284A6FE91AB3F880B5741932BB690ABE5AD7D68107E330534EAA8F13A35218CD16109C1E7D4F9203EC7A21404745EB0F1CD614B8AAC8E030F6FBD84FA4C554C3699170CB2EC060FCF2E21B7FDDBCA825418BF3266EABC203F77AD94668A6CDDCE524A805115562ACDAFB88381CC0EC7A00BC7CC168BB40AC36BA89A6637DE33A31B6E90209752F8364B0D659530BDACF2D695F1D1BADE99FCC6A726CF110491CC3C19A18786E2EEAF7E7978DF2D90E92B9C0C3344D506978F09F5F33AFBA3CCAACB76F9B6C11261E9AA0965F22DDE4FB8ACE9BD7EF16AA9BB1633DF10D96CFF15930D760898A2CC48DEF58546DA07D0A74FACB66F2A37D9DE09F1D95EAB1D695A247E55C648DFA2D2E23A89E755051C9CAAA410B6A0947140AE1A8B0E1411933AD5A53878D1FD6CB980217F96C6DCBD6F4D3D8490B34D110500C95435B4AF6946B019DFA20476B31AFDEE8CA8346DF824D2DCE53996F1960570E1A8360B2C583A44239CAF65591D931F85AFA503BB3A4AB3FD763E824721BAE2537B2FEB4AF06C9459D18CD6B07A68328132C5D4C06E0088812EE20689BCF8183C953854A48A3B8848A8990D3ADBC2F3D2D789029A1E58869B4347D1955E776F0DD0BF9E86AF8381DBEF172AFC9917595CD0E85921315E81F69AC5DFD4D334A13EA8ED5EFDF8D1334E4C873A10CC5E1BEA470977D17A5E4C0DA2EDA1DB017BE8152447DE1D3FBDBA79168C33BF393BAB32630658F10EAF6DBABE5184EA6437C69386F154D1505492271CFF931381E29B8442DEF27A3D123DCE1422F099D505C237509D6AC344A3B7C84DC0C3E5070E5DADCE76404456E6B46EBB1C38BAF1DC5F9677A969DD2BBBB351E3D0BBC54C50FC2A6F15BEE73EC0CBE906895573B02615518BEA90A75B1623F5E11D86D6019461691891C1CB518EAEDC8AF0AE64F92AF0A653685C4F219B974D3DC52496E8EB7DBCE61568BE3987E28A5B6F5B5161CA4B46E42DAE63ADB497C75552142F6C93EF95189601AD27F3213C150F34BE5C38DCB3A703024F00F9D4BFEE3058DEBAF13EC461C61CD51463D50AB338CC9475D0C3F8FEF25D4B65B5657E7AC200B633148C587549A6E0EEAE7EF63BDBE1AFA1625873991FCD7C11DB82E5358931024A10911F43C289A7816F293527279F90A3C0A62D5FDA98995AA784E557B0C4DE77FD18872F07351791623668541FFC4373731B5751689C313AD5BA560BC58A8BBB3514AB20F27CB721A65C1D88006FFF5F9EDFB69A89304A6CDBEDCE1261BE42BB7BF7\",\n          \"k\": \"3D14BBCD60FFC1EBB9E96EA5FB23A5A18BA6E370D092E2BA5E3232ACB5A5FF70\",\n          \"m\": \"135056EAAD8A28DEB1BE77EEA30CDEBC7B3DD89D1444DBAE145F39898256ADB3\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 61,\n          \"deferred\": false,\n          \"ek\": \"11068DF3FB0ED1514D8B2B8F424C03B75995406482FC8AA24BA003886711E921832222367CB2C2EE87617A30A270B447D4A1BAA3377B83FAC80A7C8D779669C49C461B311DDA442FF10C9F2B9057225423BD119C68E34FA7A92DA2D36C58014A9076C391A451EBA75B5C92A2F14853845838D0DB502629B796703523C29575B4A5094C1BE329066BFB7AB6E17FD0163BE4E60E9D676FB970C68AE416897174D4577E0AA8BE0D234FBC80279FD724AD5925FAE2B478816CB07C165A335F1B27B030E7BA5CA40F610B0E07C82FA134812D9A4E083836B158231DEB7568B73A9B3C60780266BB550A36C7288A9387D1E4C904D79296088047F34F4E72A23A1720B7543FADC3821C6442F4B501E6894A6E25231437AAA7A04329A8CC6AC729B185781B8BC4D9142063E55A0F053FFF57CF93C5851C86714E872840A264E316C37CF90CE60960EC2B0F4D18418AC96E202C07EA61B2B1E6A430DC5DD1A18926E0BD88351B56434E26CB10268468D67B2D04FB2AE7C43C032675A2C88A3E704A7D110B4A544975616D694C9E6E243E3D546217471B14711505B945E87C0D39170E3AA71A0663062CF07FFD0C3952D526B662CAFCE429A35839D667C7F0090965CB1F87642D2A8C28A0A57B4FF539CBC3195A0B063F3966CDF41FB4D9451A84970B6B93E006342247BF5DF06B041A6A2B019C3FF93DFD39502E198F5587B358B39B640C7A71C43A31D711E9E5434B75AABC436531542F90730EC7FA6B72A3613BAA4C0579A245B06B979B22623C93D5CB6437D17A7E3302BA1CA5EA0533F02B1D481573C1C3AB05424900F1613B379707433D7089A316F52012B646B9A24A90F87584C36C1F58B891F5482B757F758A03E53B38F282C2700502984ABE9F1C2BA00376091021DB8A1ED27B01AB11B7F404B2C5FC9E0F8943BE434FA4135A4714507FA628F4027D0DBA912E0B3B096C057BE77506FB8BCF5A8EE6DA96CF335BE8C0555EE446DCB96DC2567348433E98D46C83319E3FE1624AF07AD838241C5CBC3C78C8493214447C8AD0D40DBF319057531FC456CFDEF999F9F280234A03C7437786B1469D705655D01EDE1A72BD0981978815086A0DE74B059BE879199A67C67BBCAAAA87201A1910F39B5D35670A9790DB3B5EFC798BD9EC46F2FA1E993110F180203BD1A7D3506B87325DDDD695211440F60CC1C5B7A947F42DB94ABF0C8C3B203A7038682DA9840BC8B50C4343A4D49AB89DE916CE72835AD4C7298B68BDB417EE999EFE7B20C6C99A0B36BACE2C35D7388DCECA369768463051B377CB57BC0B155B969EFAD615BF7B3AA1189702A7494CCBADB0266224FB5BA75189766771BD1B54FE1139E6C607C95A2DCC2462515C4C1981C931C6628202A1BAD941AB7667F89BBE94118568F0C5CC59914AB113AA4C9DC2787F92BA2E9DD7363D99CB7EF6AD2EB5C1B992670B5541B1BA9368E7986D4628AE96836018934FD100670260ED931BF9A47652B85C44F988B559A48B2007361A3E4FA88AB8CCCF94939D5F419861FCC617250C38DCBA082CA50035A196880540E50A0E1BB363E81B1FD158DCA79DFFFACEB2494712F28A52249A103C5FCF888D275A8BA3768BA0599F5FE1CF632B07F4120570EC5A3EF96CD3031A3425A0CF3A7E408337412288575C1A414A271149A2B115825D409A1C478980CCBB4B4B1C2AF675DB141A3CD12B0860318EC340B7D3C2C0521C57D5675864243AA55C517BAFDA7187A174C109CC1C65BA42AB5288EA23CBA8C286EF98479BBA0396106999C220CFD043C60C4B9610ACF0086AE9D883A0A89BDA5AB9993C1DDF82610B0A1C6D75093B207A72037E77E76CCB282F2A0C7A9149941F3A149EA18CA519C1800B392F48C3DB358EF1179498CC2DA8EB48180A99F1BA280012A324AC8DF3FC8B3D6859A0B4334980157994502C80C0AAD301F6E9311AE9C132889A3C26501678CFED55427F94B05B57404B4BACC7607E19E873D1902163B25C06BB3CEBBA5D403456C2CA26CBE27240ABC8B6A85E50762FFBCA34FF96C1525B2A35435A7CCA663B37C202931CC16039EBAB2D6E45BE1002023BE959F2568D4FDCADD8A1B6EDE529AB088D79DAB407D4791AB43FE5087E4A9CCEBFE02C0C44C3FA925B7294A92E928C29663D2854210D405ECA2C2E5032A3058C6FB2B381F5597FE98855CD09B73DDFB831B56551306BA4591ECF403545BA\",\n          \"dk\": \"01D50182530F72E8711F976B0254B13EC997A6C63338E2A0009B3470A9AE5A014765380C3DD1964F4C92AAB0182DC07C11E278DC6C3619E2125AE86985F09107A48208B62AA764A5ACFA56AB5ABBEB93402092A86CC347FD674EDDDC9307D42B593C62D94181F85729657288EC88915066306ABC84D8C888AC4607C739590B8B5F08543816B13E5539007EE732825CAF044210371A4BB85382B7D76F3A1520296207654CC2F94B9735800161B952C8928BFC611DDBBC16DC13A12CCC4A6CF776B8EBC0A800BD72B71A138BB045CB0455A4B8BE59187ED12AE2D6C9A242BAF78B21CF544AF2EB7988100B3B85C68A638C3A123E58959559063C53C53568A326BEF9696E9CAEAD689F67B59087A0AD6721755B8C0DFC83654D3ABBA0B0484CB43FAC99BA196B86E0D26A99FA6697A80B7607020EA2CB4BEC39FF556ED6934678821BDCF69D91116EB82469C183173672195C74A0EE836E55AB57B2BB340A4569B0B91DB07270A5902ACE3647F75C429A78C942B40E64C1712C274FA4711F3494937661048F85BF81BA39F1B1CB2ACA86B4D7B4480A433B0407C7D4306CDA7A13F536AB3386C95301102657556C240A576D8791048609782E3A2593886EF8C41556B4209D45250C5B063412BCAB9718AC64C7F9A5BA775AA566EBB957C947447353F9B8C958679FCB2B93C4D4842E65405AF3A9F8D49C22225081C27F5470A6E3A44C8EE9979866A4F1E5AF959C2082494AC9A34F4B3813B0760CA45C8D489227F23CACD7D21BC4999243AB7286679E1E9B12DFF50AC8C68B7F8794FADA1B15A2235CF84752A714C8DCC5297AABF8C529B8C470F9E24A972056FE5776AC9C53359828152B984E016BAB14774BA956F3C6197452BA9956302EB63D9591307F7891B9F60BA85057FD48913E210845513FE744418B7348C66CB309A93DD4CCC15FE836456AB94BAB6D7078482CEC5D92FA178B0558225358F571C851E720A40446BD23CF2136B18D0530A460CB6BF67FC2B1C5C255999D868DDC5A04B0C0205A44278E7C100B2857DE304D37B93959E76D59D801A1D111CA17281BA2C82407243012626DA8066C6891CE9765E3691F1ABC16A56534F7EA0AD3517C8911C42B8085E3D1423F650957293AC6FC64E7F955926689CF72B62B6C8EAF178FCA4B10A147BA8A1201BBF20B3F4414C09594AAB84CFF465BC5AAAF17240AF7B47600E83336FA8FA5887CC761697DD148ADC21B12294281E25B08B22636E48E0AD94868B984CAD77BF1F1AC6FB9A3281B05CBC940929520A7A3094B1B611E096441313139E7C47A0B15859959BC8A76B2CAAB5A015EE09C19181693020B4130D3A6079947B0003CB0A94C644B190C24CA786889AA22882B37119274B4B8941967C49FB1811BAD43B7BB539A0EC143CBF61D9245AB96287164A4B1CCE519CAB092442C370E885794A81B185065AD41BE2760A83BC426D68C8E82C194A7C7020180BAA051705D21B291A95791D42081F68066AB243075A5E4A82F940C802A06BB910296DB82C4923A6DF25333576069E1900E0979570FD67832C37FFAAC3DACC48184EACE1FE0379F6C0C2EC6BC08F6A80EE6ACE301B8C259539CE7B7A87B5AB098978CCB0EA2A602DB1B287AB03568080CAB141066478A5E7728A642A5B9A97E435021FA8B5FC6B333C22185EC10839A94BD630126565972DB56408405BC35DA1786D068DC80992C3A0178F5C8A2CBBADB13B8F35A5B307A8086D216432B616DD5B47A02551CABA49BB82D306715097059D92019150549C70C0B7C53C4E763CB73384365C1004B369136D51662C41F0525CB6374193A498E23862ED0578619B20557E3A095355AA39A2F78E4257AC24DD3C519AB178C0700205C34639184B128B069BC14979885BFB7C81C89409165743AAABC78495A0BBA11950243A56C645BE4329624E71692B09E20EA4F81079FA50CC1A49946EAC26127833546FCC923ECCD3CE894BA5A4F5913192B07BECE87B3A81095C82B55B5D72A3DF1CA438668DEC2BE1CB0674C25CC51938ED5E9B2399502A2157A66B5053CC2117957490A113699975383F69ADB51A87CC9582D041628424D76E547BE32C273CB013F75388E7C9CF91190CA293CE2AA7F3FD749B2FC2DBDC9468B80904E061F06E8593CF33A74B305EFE157BD6B7E11068DF3FB0ED1514D8B2B8F424C03B75995406482FC8AA24BA003886711E921832222367CB2C2EE87617A30A270B447D4A1BAA3377B83FAC80A7C8D779669C49C461B311DDA442FF10C9F2B9057225423BD119C68E34FA7A92DA2D36C58014A9076C391A451EBA75B5C92A2F14853845838D0DB502629B796703523C29575B4A5094C1BE329066BFB7AB6E17FD0163BE4E60E9D676FB970C68AE416897174D4577E0AA8BE0D234FBC80279FD724AD5925FAE2B478816CB07C165A335F1B27B030E7BA5CA40F610B0E07C82FA134812D9A4E083836B158231DEB7568B73A9B3C60780266BB550A36C7288A9387D1E4C904D79296088047F34F4E72A23A1720B7543FADC3821C6442F4B501E6894A6E25231437AAA7A04329A8CC6AC729B185781B8BC4D9142063E55A0F053FFF57CF93C5851C86714E872840A264E316C37CF90CE60960EC2B0F4D18418AC96E202C07EA61B2B1E6A430DC5DD1A18926E0BD88351B56434E26CB10268468D67B2D04FB2AE7C43C032675A2C88A3E704A7D110B4A544975616D694C9E6E243E3D546217471B14711505B945E87C0D39170E3AA71A0663062CF07FFD0C3952D526B662CAFCE429A35839D667C7F0090965CB1F87642D2A8C28A0A57B4FF539CBC3195A0B063F3966CDF41FB4D9451A84970B6B93E006342247BF5DF06B041A6A2B019C3FF93DFD39502E198F5587B358B39B640C7A71C43A31D711E9E5434B75AABC436531542F90730EC7FA6B72A3613BAA4C0579A245B06B979B22623C93D5CB6437D17A7E3302BA1CA5EA0533F02B1D481573C1C3AB05424900F1613B379707433D7089A316F52012B646B9A24A90F87584C36C1F58B891F5482B757F758A03E53B38F282C2700502984ABE9F1C2BA00376091021DB8A1ED27B01AB11B7F404B2C5FC9E0F8943BE434FA4135A4714507FA628F4027D0DBA912E0B3B096C057BE77506FB8BCF5A8EE6DA96CF335BE8C0555EE446DCB96DC2567348433E98D46C83319E3FE1624AF07AD838241C5CBC3C78C8493214447C8AD0D40DBF319057531FC456CFDEF999F9F280234A03C7437786B1469D705655D01EDE1A72BD0981978815086A0DE74B059BE879199A67C67BBCAAAA87201A1910F39B5D35670A9790DB3B5EFC798BD9EC46F2FA1E993110F180203BD1A7D3506B87325DDDD695211440F60CC1C5B7A947F42DB94ABF0C8C3B203A7038682DA9840BC8B50C4343A4D49AB89DE916CE72835AD4C7298B68BDB417EE999EFE7B20C6C99A0B36BACE2C35D7388DCECA369768463051B377CB57BC0B155B969EFAD615BF7B3AA1189702A7494CCBADB0266224FB5BA75189766771BD1B54FE1139E6C607C95A2DCC2462515C4C1981C931C6628202A1BAD941AB7667F89BBE94118568F0C5CC59914AB113AA4C9DC2787F92BA2E9DD7363D99CB7EF6AD2EB5C1B992670B5541B1BA9368E7986D4628AE96836018934FD100670260ED931BF9A47652B85C44F988B559A48B2007361A3E4FA88AB8CCCF94939D5F419861FCC617250C38DCBA082CA50035A196880540E50A0E1BB363E81B1FD158DCA79DFFFACEB2494712F28A52249A103C5FCF888D275A8BA3768BA0599F5FE1CF632B07F4120570EC5A3EF96CD3031A3425A0CF3A7E408337412288575C1A414A271149A2B115825D409A1C478980CCBB4B4B1C2AF675DB141A3CD12B0860318EC340B7D3C2C0521C57D5675864243AA55C517BAFDA7187A174C109CC1C65BA42AB5288EA23CBA8C286EF98479BBA0396106999C220CFD043C60C4B9610ACF0086AE9D883A0A89BDA5AB9993C1DDF82610B0A1C6D75093B207A72037E77E76CCB282F2A0C7A9149941F3A149EA18CA519C1800B392F48C3DB358EF1179498CC2DA8EB48180A99F1BA280012A324AC8DF3FC8B3D6859A0B4334980157994502C80C0AAD301F6E9311AE9C132889A3C26501678CFED55427F94B05B57404B4BACC7607E19E873D1902163B25C06BB3CEBBA5D403456C2CA26CBE27240ABC8B6A85E50762FFBCA34FF96C1525B2A35435A7CCA663B37C202931CC16039EBAB2D6E45BE1002023BE959F2568D4FDCADD8A1B6EDE529AB088D79DAB407D4791AB43FE5087E4A9CCEBFE02C0C44C3FA925B7294A92E928C29663D2854210D405ECA2C2E5032A3058C6FB2B381F5597FE98855CD09B73DDFB831B56551306BA4591ECF403545BAC17C983272288473C7676430281761C00CD2557C8470374B257D99D63E68C2631D1B02042D01389FB44726DCFE6501D72FE645D5F098EC86393687E2E245FDEF\",\n          \"c\": \"6802F268BD6991AF8993B2CE0365253B67FBFF0422D0536119A91A8AA3591EBA7BEEF1B3E08702DD63D9FB48D8FAEF6BD7BA282095687A9C70FFA21960EC27EC9D899A50FD2C6103E1018DB559D7CE368FE9CB0D4206445CDCE0B74F1BBB4DDE53008E50C632A36D9B02E97192DD633AF5936DFEB0F5FEBF306428E7993F9E3A8E47617F224AB50403725624023DC43AD6CCD3A37931E6514458D7F16294AB8ABDB042842F259937B31BF2BF327B2E8A86B20B6A0BA3AB87D897EFE6ED969E10D80BA1C7F53ACD704542DA064B8BFE8EEA9D73CF6453F3E1F0137E0A52C41A709689D3311A0695FF25B8E54512A4BEE5ABD52A887C52B1A509C2E7547EE621117E1A024800B935C1D50DC7B3A7D9D385A1172713336EB49C630EDA7E1490AD13316E5E0F7302006FB6ACEBDC6ED9EDBCFE2F9846ED0F7CB1CC2BBB593A7DBC6879B916C81BE5FEA5F4361B4B2FF17AF7B7D21DEB36D9DF9E504EBDF11ACD7273A1D7BB13D690BAB23A52777E208A740E75F797251E9F87915B975E7E764D3B2ADF90937D79D5F8FB1EA8CBA525C4786457E497ECA4A10757E533A7644FA04034308FB197FB133D136D0B0C9B40E0977E6C2572156F164F3917A4D0E6100B7EB9F22517450309B479634E9770F23C83EF87FA9AE94E90FEA32FBADB9DF1D50DB1F1F8ADE62F1501FDD1D90ACED42446A859AE7802F7F66BF785AA454E89EA8CBD5F600F9AC4E8237C3C2812D5439BAF89CB2D636441C566BD43C5D45489B61B2BD637119A5E5E0CE452EB309B4D7F6C7C387930A9FF6B90CB3C225C99554543EED7A71533090EE0C8A6CF857F4BAFF48244FAF5DC85DAC61D737B7E6CCDA8D9C439B2120A47A2CBEDC95D1C3E56EA4CBB9DDC89DEDEE5103ED468C8115F544DDEE5DBF41456F6465E1CD5C01DC470D9A9A64763BB679BA6592E64C82F9488E008235D6FCB84AFA7D2E454058AE57875F2782BAEF71F1512069FA24802E62AFB5298E307AA1EC074B2B91356045159CD6EF8EB946B6EED50D25E729FB176D4A866BAF36BFFAFF3907928D25764BE3EE7D226E7C82A7D9F2A3AA69022083082FAC7250E6DBDB43BBB8C77E03924D8D297CAE45097CC7A3833CBB75E69D03D01198A9B9331A8E9E10AB82EE349099918B07878120EF812B1B278283042234B36DCCA031B13687E9F1853E5503B32C1E1CBC51FB88FA5B1B044CC715F24883FBB2B5D45C7F461E3023AEE3F18A34030EACCB8A21FE0178F845A5380C3BD8B8DAB193BE5E02F45D30F8B8A0173D89ED0D6C4A6959CC1AEB34CA8B96FC46393FFA35123BC87D580CBA66A21F0F30E30F9899216343D6E65B8FBB1BB5130AEDFC4BE7234AE6665E0D087CD92812437E18C81AF042A55840A58C8C15FFFE182E19D9F156A246CC1D359C36CF355B71076BB6B9F9E9C8C6F9A909CC58F41CB79560D7D849626D6CD1739D90B3067D6B33B9A989A4107F6B0103F0F7F391DE8FABF9DB10F580EF53885DEA39CD96AA8343164EDE94E6CB7CEB8347C24A23C40A3D0C851808B46D5A84EE6E1676626DB741C7674B0D33DAB62FC8AAED40F4E6A9590354B0D24226FEF439D5D89D1B48DA564D30744F5ACE5C61ED8C3AD522D87381E0311850F03B76497AC92AF2D9B7FF8DB0BA1D5A2D63586AE3DD08C4A0462A39441A20C59332D2F07053B1436117B1A9D43C477D3D956E129ABE92D7E6D7A2A383E4F0A19F59F8C565DA746D847368ACC95E7A00581B6A330129AA1A718D0B1860CE775A1BBBE244B04C2D1A94F37E2E360757B9830D2E7814D402E808689C9B02E871061D3B8A6DC408ADE9D9C3B77C8B0CE8B3B23A246FC1BFCAAC2B2635C1AA12AB3EA937237EB224B5E87331B5411A2B2F17214B1A86B7644B2BC9CFC2515A6BF413E58380FD49FE624C0E3DE4FFCE4B880907436F425596A1005D55B366FD18188EE6DE1CBF9614A35CF163268DCB56C76355D4ABB3913D9C31C1AB3E9755A305FE2E186715DC3274EFBC1A7C97E2A70F5D6DC31FAE5C56F4F335D8C77AAAD4CA2DB284ED2E56E79FABE984758C6348B7F195BB082BF5D8482965CA1C6F6BE8D8FB9F0E176BB4308F8B064E48A848332A81E34D28811B76AD62EF9F51EE606F74DB8E98E9CD5D7B2356B28D6A5C7398625549DC0C35E4D44E9CFE30BF8AB8A75AFEA84CECC051D9298D7B91EA8DE1FE7736F1D2038B8D3BD2A1A1FEFF4730254D2BA\",\n          \"k\": \"B45BA5490571D2DFAB9D9204398EE8F141A3FF5B415A2E2A8AA3391263992C82\",\n          \"m\": \"54E7B2E3305950EA570F823FE36A7999E419BB36181B5514860BED41F418EE77\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 62,\n          \"deferred\": false,\n          \"ek\": \"4F4504922C38F9871EEABAB438D03555F272E9F9493F005F5F8CAF8A620F362A1D1B90A66289111D03929118CAF31B0AD9ECBB203017A02711BAD26B02310379C117F6F58257F6C6ED5A14BB32760D5A9EA062A9C106064D703BC9E378DF498B347075A7F6515787C500472BFACA0729A4590D39C291DCAB327C10BDCC6B1837883BC90B379B3C32F56C30F78C2A347EE0452FFE4878E2280608265931CC7E123768B0F93EB0081D642B4DC756210952541BB3BE08352CB983911F3B944671B759A16FB28C3883020892865E903045C84BA1E5E54159604AC058056C6153F738745140CB8B5ABE3318809651790EDC20CA5380CC76568DA5915C5AB8272748B42446EFAB58852C619332A08F537C28E93EC676345E0A2666708CB5D5281F981D3B8A19180565704808963B4015DC9B8C06905DA43680402E6B5888F2EC7448AA6578787EBF42329D060998E74B09C13D1C432A17F56D18BC624F02AC7ECB6235C42A24B42639FC490D07A635360932C71E25720EF59340F21836B74AB8F31C2CAC035AEFEA846210AD665A5D64616861F1CA3B963A2C10C662F7AC66298751AB3CE6A64C59770568B4B234129550702375D722AE4BBA5FD0899C84A834E7B7938627C9285912D346099423BA37B25364B67258A121650E895660C8E014A5B08D38B7AEC29175B424C234D2B87C791B60159AE753CB61D240DDD2814F1A7EB69A79AC37634850CBFBE2CCE6E918D1CCC70E13707732A6AB11329A3336D8E221531335BED560A468092CF690D1B8791F58993BEA001186AF5E48A2BE0B3D17656A3ED98BC3628C9F863A2C88C90AF25A11AB41FE7CADBB3C1013B59A115AB83F9003BE28759A3148706B891D9770961AC53D4C8B6CE5788ECA3F4A4A47D7F5165DC7B94B358591348BAD7B8CDA482B26F650DA1765A0911527260703E2CCC703A69F10331095BD3AB167DB52A3EC16B69F233862EA11AA9118FF539071A05066388A6DFA10BA4819D31A03B3980677CC6B888263D74007057C6C3C272464497A58681816C3C59E05064D147F62D5C848B8919C97C3BAC20A43CB58F8CA98EAB48B221851F634459B7AC514161A1C490B8B576A0FB2497A65006EA78F9ED50763A19F8D2C99D546642A68B6C2A49FFF919687E67EF0ECBCE23266114982FDF15F572A9519EC12CB90BDB3516E85E01106B9BA456738F4206F5F0C314156307F4CADC8983A91F730219C322A1602714849CD1B7DF5D02C24758DA6DA8FC8600CA2C1CF7294B1F6598D9E844625C29F589006C0875F3FB33EB357B7AAF0607BB31743153B98D64A8FC56DE56714D71303E9F6A3ED228961E607D6D6C0D4776828B7574A8CA28D837D24470E56F3A6CB6A4A90F5A9BA8A3F60576857E1C7AA52A16E1A84F7421EDF46C312BC83EE0776A9819DFA8B66FF2BB907A96F4D6228FF34ABC68248C1990C2484CBBC70745B045C602C1552B952FE8B4D24D027E1C65B3704945E507D09A9A6AA7000C1607A27591F606AC1D2C5CB55BBA149A15C4B261894EC9A9AB9C3DE446C1832744CF6BBBF949D54498319504BABACB8DFB15F90F66DA9E0B7F8B7A42A7B09C5E19DF58CAD8641C2DFAC77412B49D17A0C02341C6FB78DF00809903835518392D0288907184F126A69BA77863C84C1813835926311EA705433298D77AACA4DFC30CA218DAE405732841605840625E57D7CC3AEFCBA637CF8077ACBB9CBA0744E462D02F406ACB24E9A3C5CC5048337390E4EAA295F6831161CC7FED8938EC6287C8C08DCB0AAE66340C4A380B5B816B8110F3D190EB97C8457DCCB410739C48B8132A32C0CF59028962A8BD1B03F0229C908BE9D22C9AE65880E110977508595F6754776B4203AB1633870C4EACF7B3689A8D43F4C213FA839B7C0281D242452455A31F396BBBDC41D9C4C5F99960ECA0537DCDA3FC6405B139241EFFB20CFA05A739A43EBD39E2A516A6D136F78F43CC5E24C2335CBBBD7335DB820A235129F10C62D13C5013732214012B00C1383E670D5D37484850D2EA49E9E835A750C63BFCB2357E472A4406C6DD2990349987707CAAA1030D4206460848F3C988179E31A4334AEE89511AE4297A668A154270D963C7EB930CD3C35290C9585F428690F88721443757D793D9DF912680C21923138D119B777530A3FBB406E110F408199CFAE822AF8B67807B0181714C1EB366D2750DBA3CEE603\",\n          \"dk\": \"7A8721EBD06F9A1845A2089AA7437CBCDB4B86C4529BDB0043B7BDC5CB95639CA32989550F64296482C8CFC15590444D8CF5C699348C0541AC0224B94435B972BA98832A7B6C86600E8C4AD9DCB0409CC278F01A6748005E7BC06AD00BE8D731E4182F16563234960634683956B58217D1B69848C934840F02C49398F0BBB5EBCDD422C54249C7195066F9F900C0C982A9048F4CF3800A10CDD1858269E59C307B06DD050F5A58C6230A44D8A9979D491A289A4B3C88CD7AA9307FD93DA99910B9F3A97FA759BD25C6D9F3A41677B8DE41CA23D3089C43B584C11C8031205BFCC8A813BE1D5169714C84BFD499A019030514072E59A50E5B7BFBD8A03DAB360E16AB02FB270B434F274A77ED1A67F870C6461A5C641618254C8F2061BFDA921B1E906626CC956758685190017487128D491C07A33E3C023972488D9F504BBF27BBF02CC85D5B8F98E61C59F27B7C879F4CB341FEC1A7B7A13E7FC496B3A511AF593312B17C9691369C10B7E440A93292BB25B501A3522E4ECC1DCE5A8255092F87248C6DF39D9027C5ABA21E10F003F88BC2ABE25B5533C2F390C4C500927DAC631307C6BF17CF511C8927FB75BA1B44499900FE0545F20565FC139BE9A5221D99C656643FCD38CA16756D1D81ABE54971961A60D47C4365600EF43BC97862A41AAC136A396D3E549ED7469B1CF0AFB4D107E9C685AA8BB26FAACBD070B4BC9C1FF502BBF64350902972A75810EF04670EC9A9CB99830FB88C150AADA8C457BD338894A18ED751AE584758E16660C221B7FF538347C05EA7987FA92A96BAC91FCBF0CAE004107C8472E0118D78F0166971CA14E1BDCDD229FB631FBB81CA8D4A3282064E9FD3374D674D3D212DE71B0FE8765E05F66C251BAD690C300858A220A620205239907274B754AF091701E3EC2ACE6B7213190F69187164C8606355CE0C1B3D7716CB7BE1C335B506F2646EF18A092F6B6328A656A66A5F6D534962F5ADA6E335D7F37EB61776A8D26326B639B48C95099C3F801A7F3E511CA1E45400DD1735444055624F1235A21757349502372C4B6C01C96D96D9220D0638C2163CA7765DDEB58F80BB5F42442D20972DC94131EA83B6CB687E6664B66A3736AA29828F40949B4636A4A505B2F7B2EB9626B491BC96B16664B545127957B846626DE12506CA449D13A657EA9D8D495D0F8B1C0CE367591C1186172CD6E380828B1E5C4B0224D0298D872E0BAC79524707F7014369D520AF821BC1F706BFAC6C692C0CBDD9B96B8292EDB27EB7C43530F8A3ED848513946EA1D0BA5FA00784D0A85A528800B1C463440080750C7AC6043B0529F73B29B40358F457A68E164855E7626E964CF5DC2011DAC25DE82EF72A3503280F3F23A058783625E68D9480ADDBFA4FFDE76F5AE87ACC209C43C65F11F652954A658C86C9FEE684BB1B7BF73211EA002A25E1BED40750D41CB3665291843B0DB542BBAD481D6B3B021DD8635AE93D60D07807A95753C79DE35829DE5C88DF095301C433130A511210792624A8556379B0F35D9E3A2E583B24B1BC587D038C5C59BE09CABEE6597BB9CA49AD022E35C7319CE928140A026D05B9761AC129524332151DE9E65E4E9A0289032E5EBB7A47A82AAE9BB2ACE6AD16162336BC8507C0C24AC0C4A2D076D929A67AF8959EE472E14CA87BD0B89076AE9B4C3F95E3BCBE8815AA7A849F6A385A8B0756917A3EBB44122989A49A33517657D67021D2F6BE9D64A1ABE9B8C590A9EF81043D19C556B97DFDFC03D7A4173405C8BD38235EC07BA7C80BD97B4566BB89C227855673B63A09925028C24834ADAD1C26B7D804ABA3110326B5273B8430045B2D317B1770BB3FE592BBD32F55244601F58583580431C35BCC955C9A5656A3431B943A7E03D5B76967749C886458348FCF9CA7487A66F9B0AB3D92462ED192D19B7D8B58532B09642F490940803EE97576093C5C32D1A4770B86E7F58530776B5BA1051F1B1F35309ACDF97319846789292DB784259185B80C3B0288D103CC1C2E4E23B97E9B140E18B34AE8A4484647B66430996844E73B0A0F79A6F5179234E22C8DA676A379AB8EDA542DD1921B481E866422BD0822C2FC33391293EB31B31D7A9574E31D7E070FAF298E46BA8CEA3001FA0210A6890A6C68B73DFA519B66A23ED78D4F4504922C38F9871EEABAB438D03555F272E9F9493F005F5F8CAF8A620F362A1D1B90A66289111D03929118CAF31B0AD9ECBB203017A02711BAD26B02310379C117F6F58257F6C6ED5A14BB32760D5A9EA062A9C106064D703BC9E378DF498B347075A7F6515787C500472BFACA0729A4590D39C291DCAB327C10BDCC6B1837883BC90B379B3C32F56C30F78C2A347EE0452FFE4878E2280608265931CC7E123768B0F93EB0081D642B4DC756210952541BB3BE08352CB983911F3B944671B759A16FB28C3883020892865E903045C84BA1E5E54159604AC058056C6153F738745140CB8B5ABE3318809651790EDC20CA5380CC76568DA5915C5AB8272748B42446EFAB58852C619332A08F537C28E93EC676345E0A2666708CB5D5281F981D3B8A19180565704808963B4015DC9B8C06905DA43680402E6B5888F2EC7448AA6578787EBF42329D060998E74B09C13D1C432A17F56D18BC624F02AC7ECB6235C42A24B42639FC490D07A635360932C71E25720EF59340F21836B74AB8F31C2CAC035AEFEA846210AD665A5D64616861F1CA3B963A2C10C662F7AC66298751AB3CE6A64C59770568B4B234129550702375D722AE4BBA5FD0899C84A834E7B7938627C9285912D346099423BA37B25364B67258A121650E895660C8E014A5B08D38B7AEC29175B424C234D2B87C791B60159AE753CB61D240DDD2814F1A7EB69A79AC37634850CBFBE2CCE6E918D1CCC70E13707732A6AB11329A3336D8E221531335BED560A468092CF690D1B8791F58993BEA001186AF5E48A2BE0B3D17656A3ED98BC3628C9F863A2C88C90AF25A11AB41FE7CADBB3C1013B59A115AB83F9003BE28759A3148706B891D9770961AC53D4C8B6CE5788ECA3F4A4A47D7F5165DC7B94B358591348BAD7B8CDA482B26F650DA1765A0911527260703E2CCC703A69F10331095BD3AB167DB52A3EC16B69F233862EA11AA9118FF539071A05066388A6DFA10BA4819D31A03B3980677CC6B888263D74007057C6C3C272464497A58681816C3C59E05064D147F62D5C848B8919C97C3BAC20A43CB58F8CA98EAB48B221851F634459B7AC514161A1C490B8B576A0FB2497A65006EA78F9ED50763A19F8D2C99D546642A68B6C2A49FFF919687E67EF0ECBCE23266114982FDF15F572A9519EC12CB90BDB3516E85E01106B9BA456738F4206F5F0C314156307F4CADC8983A91F730219C322A1602714849CD1B7DF5D02C24758DA6DA8FC8600CA2C1CF7294B1F6598D9E844625C29F589006C0875F3FB33EB357B7AAF0607BB31743153B98D64A8FC56DE56714D71303E9F6A3ED228961E607D6D6C0D4776828B7574A8CA28D837D24470E56F3A6CB6A4A90F5A9BA8A3F60576857E1C7AA52A16E1A84F7421EDF46C312BC83EE0776A9819DFA8B66FF2BB907A96F4D6228FF34ABC68248C1990C2484CBBC70745B045C602C1552B952FE8B4D24D027E1C65B3704945E507D09A9A6AA7000C1607A27591F606AC1D2C5CB55BBA149A15C4B261894EC9A9AB9C3DE446C1832744CF6BBBF949D54498319504BABACB8DFB15F90F66DA9E0B7F8B7A42A7B09C5E19DF58CAD8641C2DFAC77412B49D17A0C02341C6FB78DF00809903835518392D0288907184F126A69BA77863C84C1813835926311EA705433298D77AACA4DFC30CA218DAE405732841605840625E57D7CC3AEFCBA637CF8077ACBB9CBA0744E462D02F406ACB24E9A3C5CC5048337390E4EAA295F6831161CC7FED8938EC6287C8C08DCB0AAE66340C4A380B5B816B8110F3D190EB97C8457DCCB410739C48B8132A32C0CF59028962A8BD1B03F0229C908BE9D22C9AE65880E110977508595F6754776B4203AB1633870C4EACF7B3689A8D43F4C213FA839B7C0281D242452455A31F396BBBDC41D9C4C5F99960ECA0537DCDA3FC6405B139241EFFB20CFA05A739A43EBD39E2A516A6D136F78F43CC5E24C2335CBBBD7335DB820A235129F10C62D13C5013732214012B00C1383E670D5D37484850D2EA49E9E835A750C63BFCB2357E472A4406C6DD2990349987707CAAA1030D4206460848F3C988179E31A4334AEE89511AE4297A668A154270D963C7EB930CD3C35290C9585F428690F88721443757D793D9DF912680C21923138D119B777530A3FBB406E110F408199CFAE822AF8B67807B0181714C1EB366D2750DBA3CEE603BF822762AA356CDBD08EADB7D166690F4A00D797419ADCCB9133C3E5EB671B5654CBE9E686EFF218AD6583359070544146921F5107809454E73FC105FA7A9A0F\",\n          \"c\": \"566EE0837DD0AD41C30D9C318F736E6722D037E07BAE16234A2051509180D399518883004079AED8B2B6E18A2CD1E2056DB76EECF47C3E1268A5E662FA6D029F7EEFBEBE1587919346CF7D38C6DA819D7A3A89B2BE65D6E2F87A6F348E8F9C67F99B5ED655B5C0A6AFA15DA8CBB310B364552195C8F70B37F153270322E5E45B86F074EB3BFB3B03DAA7E81B474011F2F3DCEFC3CACB7E701B1AC7DEF0650362CDF5F6531E5E5FEEC973208124DD22BE3167F49BBB9F160AE159E692C007801E1FEA10034A20EC460F72FCC57C9C2E6CC749F5110AC6CD7A20B6CBAEECC6E6C5FA131F09B19EFDD175420B2762E4CCCC03906524AC63C6AC92B1995935A83B674299095DC4D2E26E3D31B8A4D71E9094A4D50F76DEED368F2DDABA358C306646AD0148408F8B8E6F5899F598CFDAF90C9CFB50A285150692EA3955EB4FD80BE445777C601EA5B59EE07E5B828BA576F6F300D674973D658A8D4B6967C3A3ECE68BAE27A46AFAED43D3392B985FC5BC7D79B0D56321316649FCFA84EA05F02EB8E3142D72C06D93FA73451CC0134B53AE1B038B4EEF71946665AFDD50CF3A33D188389DA9A32E7B46DEBA552C337DEC2B28CB3570DF15A229AB8D3FD86277FD5AB595A0ABA2DAFB7AA62F2CDAC997F13BCBC93A42D37CB83A52FAA8B01F8C97D196F1FD7A618566CE8593BE11DE0437D2F82476E65D522ECC3D8B1C247ED0EB7590648E1A51057F953F0932A567B799BD431344E1E0A6211BF07CCFB0B53DA9D39C59C4290ABA2930BC691F83830779C89F14FF6643D277035E5711333979D563DD1BD4F52295D45C98A60C5D59AEFFF4ED119B2D88C7D5C7E7EAE0F58207389547BA5FA995678A94D7127C407EA0BDA29CC8701B83B27447654B156461226BBE337FD69BF0FCC99013646751BC53D9070568E4D2C0DB6A06CA491BF80EB6C2C5807542CF6569BA84B88EAE67FCBE2DEABB56A5D06E945F8389EB68ADCDBDF0CD9018ED713BE071A0A87415647A97DBB6665C09C5F27899BED8837CBCE0010C70D7D04419240EDAA185EE7AB14A1C55AC6546ECA6781804997AB2B15CCFC9035ED7F170CEDBA0E280195E7B2C2C33CBD5BFD10CA8A2E82D977762BA6AE5476767F7E9FE787FBC624A81D1467C26CF2C1F1B1BAE476522DD198FB9FD5130EC41DA3683B788D07DBA2FFA0D460B66E5967D161AF00E61388BF317897E30B35BF8EF1A580DF5071471808E764E01043982082DD2BAC13DCD049B1DC66D44A670EF6E063B31AE29F391BBBA0ED0ADBDE06B0E5403B68BD1A4D997EF3F965E591D8CE8A0843D64EA4554FAB3DC9D5A96540DC2ADA49504F25213CC4EAD1E097E0CC516A4E3DE6F272D8DFF93604C39551E8F848E349315F46011AADDAD6F0BA68F1D6CCC68AB6AFCDAA1D47FAFBC056A063641DE73414C26D997F243DDA0817AEB734BAFD35C59F86AC30D45D1E5A0FBE63AED51B02E6C7851A858D5E0C23E53DE1DD0413D408F665D62F24FF2F314A283B7E3848F1E17AF5000BB9D279C0647C1434F783A1A21BC7A8349D62AAEE19FF06677F81C954F9D6A69ACE7BE06518ED800923C6ACB36842CE65EC81749C388AA92164D28FD34D75637F23C49DBD74353181655EC029593BCA3214C73A540CD9C3F6B1056A2137D0C280BDC90C13E973555D8C4D64531CB7360F1E67C1CACBE9F2D59AFD3BB5962C4DEA4FD9341D87E0DF55D344F4CF0A30CACB8CFB10D4B6B07AAA6B28CAEBFBEF4F0B4CF6EAD9387392A27DDE2C4BED1F9908DFCB9E1383864AF6B9271F0C265D2651919C66BC5D3850CA741A7705D0B4F7D2FB82DBD376079AFDA8523756200E6E400BED88992520429215C3702C97721D76F0C9EEAD7DFD2ED0D604D6BFED6D942DF0AC48ECE1BD16FB35A301165816B0B4DDD881255FCA8C2EBD6A4C1ED56CA83CD868AA7EFB0199709DAEC10171B870D3A9808809FF0670BC96B66CB45EE5D0198146C0AF1CD920B1E315879E8D24ECD8DC03F3E8C43A973ECFD91344F2E9520DE4922544592C26F085D53A7AE03A866C369366A4CD78EC059E871C90996CB4463F21C7F0A9914E38ED324743570818FEF155915F2E1087744DCCC6B6DE8991371DEAA4DACC534974B1838BF8A98066F20B07B809936A36B6D4C4CBEA220B16241FC875DA5D5573FF74E3ABBD2D110FB2B288136857CF5F0AF36E226EB792BEF4CB79A6A8741FFE20E\",\n          \"k\": \"27CEEFAB9BC1F2050BE2874B3A81CF1148567A1E73A5DFE89C640C0C88F35580\",\n          \"m\": \"F2C864FFBDC366EB96BC5F5FDE0D4B3348A07E861D9EBA90E70896F7FFCBD55E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 63,\n          \"deferred\": false,\n          \"ek\": \"44DB8BD3C4B5FE45CD408B054B878343261AB8F812BC75A953C8617582B10D789F7114A00E0166EBEBA0BA149370639BCF020197F82080BCBB9BDB49D15493114698079B7021C55CC275612D843FBFEB0F2B86CB659C6EED1CC5661B711621320A85B307C96F66C4C35C3081410045AF4B898C2C14B645AF9E0C9CFC03BE42DA4DB3A759E77B7D6894BF380991C3D4CB0D5B2CFFAAA08DF0085B8555228CA8E18672880C4F96E555F879C2CD6740960B94B6A60BE7557D2E230083D757DF76506311BEBF28C5496C00F338594AF41AA15566B24B004E60956AD09CCDB034B7C35BC5D550F52828007C1F85503AD86A9C811C440E6600B16CCD51EC098AE298042C128494A203D20BDB4789C4314C8D14543C9553954C9FC34C40912862500B2F3A5929471382D3531FC9767CAE6937D6E37D6AE18FE5B858BA968927201912782C6D436851C80E7AB0782FC5AC1D9C2F8CAC0D6C29148E220EB30909B2A43A4322B90E741C90C19AD52CBC1527CCC883209952A5A7C68E3E580BD987338BDC75FA84CDCAC3A0990246A7050E1022AF89C7165E45B32ED995A8C4B7FB28999E4CC2C79250DAB67119AC2C6B5B195526439E17A913F3BE3BE896E8023AE6A78AC00244B8EB2891CB5634955F908424C4B77BFDDA017D3B71909320CC53C4D4196E0C39701F8839711BCAE59BAB4B4908F5121B931777E17B331AFA5E955B5CD5840A734A41E35CC016C01407B02039B5064E97C24F4B9197091DE6AB22D0FC0459033EF7816D12596CCEDA120F534CE74571A4E668F5C1488C1503BC0354161BB651F673DA9019C9A83D9D97381E72CCBB1628F8F9A42039CFA07B1D67A1AC37407319360883539E7E060D32469CB9D87DE784439D2B453B77C158045F4BE5A96F090603963A18574FC027C0A0597D6A30B9235ACF5FD49619F47F7E8833D5D719B9F95825571B32C17B0B4AAE2DDB7EC0B14413E73EFA105EA308AAAAC76C2A257DD4A36407468802703787C4AF27D21EDD08108534AE994A1805AB5E6235B88AE7C6997374E50C1077F7702F809B092368F43A9CB2AB539FA104F5B23A4E69B9D46C4452208E3DCA58CC1045955784C82099EFB01ADB86CFEEFBA1EF3A43F16B7CDAE28A3E74C24B6648B8F20AB88088974B75BCC79E692C57501783F57434B7C2BCDE20B2E7129857EC9FC95452BACC9D96D89CB892C78CA07E790906A6FAB62CE170B63443CBC9BAD6F48B383050FB788270392F396C8CB8A73D4912115C41869D54AE1AE6173A34B6C3C91A5F6C39DA659268456E0B51389D7B05D4367C9F5C6CBD371E84EA9BEEB4C09DF9205296B9E947BD4E086A97760CCCDBA94A2108D02CC6544028E05A2FED297F32491042241EFC36A24F634DF3626A217ABA75882A050B7569318E8E00AE8A267EC0207CE9E8A524E1CE8F8CA117B0C77A680231F773B756152878363A9C38D93423C0378A60C8AAF8063BBD1484A3F37D055215A4403AB3C80DC9223D91165BC2D077C71A49093548D4A32922633258844193759D4DF26E1CE367C3229FECA492A9E7669ED4158CC08D9FF0C24027AE9759A517DACA6CD58090D5673C753101987B8914BD8B71C552D96DD093257A9313CBB757AE42860790AC70A593ED65A492078B6043B7B2AC4CC7D85A7145C6EE210C26D60906A5571AAC3EE71333155055381281A584A66C090252A3CA70D9AF09063F3DCC9BBF020715126122602AC9F9930D59669647CE64F356DA97A5C1740BB283781C2A10CB161BAB6B709CF9AC7C7911A23A9F4D6507537C497F3BA5CFE52AE9E34B1D9922669520DA2662BAA0282B33C5029145CC34B0FA9CCF201C6491E12646DB1D1548C0FBF6C7925C9670F4ABC84AB289958A26A9310ABA90CF8C41B39B7DC96673724CBF4BCC7754A71E3158B35214A8706652E11621635C5B6AEBAF64365497518D5D1B6635178E318569D23C50C3683CEEF593498037457C4494F03C7E92C055BABEB96C70EBBA20658787C1B27D6CBAB485741712F9169E3265E376A0241A36C0B2303F8865E6AA9D89213CA1F874E758A569E4055D70BB1B512C0845B933E470B1D9730B5024F520C8F2221B9D49836405525EC3864065BCA04A6AFDA11CC59A9BC28479D9D0B42B1B733E186D50B603CEDA34E8D283AB0939005B06815FD3531BAD6AA926F931C478F71A699A17741447FAAF6CF360D4C64098E9F1\",\n          \"dk\": \"1149A69B309A5FEA7002DA1B0D4B4A45A37D863841D68452692A96223C203CC24B05285915D48F00091A033328ED12167A4BA398A9BBC5063A0A28802CDB3D8B108C359601AEDC31AB8C8729E81DF9828ED5900715264EFDD3B0C77176D6D0ADE200685A239CFF946EAD338351A54A82921E9CFC6BCC60813F2873BB50C1796276501740FE2554E130A01DE941D8CA3A0B37318C467D893491718BA84A26A4229A06476986C668843198C12F034178064D4A4B7A889C921C49028D6B9272497A4C0C6E03F78C6433538484C3B5D662058BA9C9E9B474E636A4E20CE194BA87F98B7EE4A26D4A414A4C4D97A3A1F420885098871D2036D6E9A2FFAB978E217996E64CAFC34E30F453D2E1CF20AB6747D1C0931A23147052F2A30E1EA07927A28733799F88FC8199896EEBD324845119D03838F17286F32A7F1EE89408606E66C72A546333E3E714DE56467DF46AA6C10CED9626C999506AB60DD5226259762D6A028E2B4798DCD77217073907962DAF17BC6E54B11A8618F97104602250DFE40409E615226077BE778D8791838CB1B11A925AFF9622BD4328539CC015BC321E9CAEE3699B8CF9C075376E6F2C50C495821DB1903C2BC04DAB2BE90913CF845C368BABEAAAA79E998D1F066ACB67CBBA673FD3C1014CB743060B1DB2A93593C316BD982FE9FC24049221B9BBAD0EAAA47561AD4B81C81B46C69FD3847C8559E77A368C57520329A06CE7542C4771039072500AA324B723FC96605BA225654008EE7BB68AEA14843B18D3977B050914DCF2BC77A91EA7C4B46ED04CBAF47B2BF04129B05644EA360F3B69C2867FF7F5B710A4947AB29BC5882BE2169E5D7442D659CFB46BABA459869FD291B9D505181A306394AB9643810FAA68E5534351D3744E4CA3053AAAFBDA7523A33996BC344D91BBF8B7C41B1998C2623F261A94DC123244AC9DAAF49902D407882B067D12C1A835A5AC546BF99C2264B525F9628EEF5BBE78458A103236AB8B37D67A5EAE21369BE740D4D3B567242517404A53A049C3076C310560AFE9AE495557699B28B2DA72B91720A1524CC265067BD3227381741E0410F19C960E0B8AF48CACCCD70002403884A95E578B04864A81BC1737373135E9A2573B51CA9CD4C11A8248C3122C5192A3ACF167FA1A21879C09A7C21754980485A3272D8985A0F52A4CF77813AAC828231B06F4BAEC5B15D55638F889679653458620C54B45BE8C67C6D2FCAE05368741B26E9F6646D0142298079740A61A0743A10412BF2C4469D60C6B08966F1180C4B3C83E62D3B0E4D13AA29CB765D67A97524D1AC75A0212928766CA29114B906A93F42CC0CF1B2B6A6C2948F80E9A5954120660BBB2B1B42297B8C53D9BF349A86327249779C4C41442655D321A55F12A589CC62F18147599C6059D08B4365A3DE541C845FA37061C13338A8A1475BA67C12F0C67A3430B7CC53A46875241D3359A64F104910B73763197C5E1A88D5B1D05EA98B97524D4B9A59A7655276905D6B1662EC9C638AB0D8B746D93C9CFFAA59EAE762A54096B124344B0F4BC7CA6696D53385554B115C2707E203C6D161D14B31025F336C08B17F907442E369A54F404CC6B0DF3C62ECFC805C8D967A29A1B17030EA26C45AB71684F7A6065A8715AE7A37D6C5E3AE25A862B3185FA53C0424A3892197A339A6EA435264798948462F8846DE6A04C77C951F3C5C9262B16D3DC8E7D7B3ADFE75EE3CC84706BBE9B830DC0972C15E404B147C4FAC09BF089B057E5850153978C8C625513ADF872A30FD7B1D9F69B97AC123E11A611C6A416B4410483894BC8AC738B3E0A9C0D7F611D88611A5C62CAF879307A20732F17B26CC52531B447894193AD45497D64B8562193F670C8B8AB1F74DA79A945B16E01AD18006AC222345C20CA5B2099E9818E0DD155E14B64215C88E0A72A57C6574C692AFD1A382EB832F6033E8B78AD68BB60CB218F58908B92533C635A4D41723DB231BC2CBB5B7AC03D718A96D3D80120C79AE43493A9404E3BC46391721F68F80FEB7C47CA4A79BE9618480344D6790402799AA5E155F2E229C45C970E772D517995493661A04A2CB8FB90DFB056044616669096ED51C8E2F131B5FC729DFBC876A412024C692899540334071DA2911EDAA7D1C49FF6C96D4C4C2A51199B44DB8BD3C4B5FE45CD408B054B878343261AB8F812BC75A953C8617582B10D789F7114A00E0166EBEBA0BA149370639BCF020197F82080BCBB9BDB49D15493114698079B7021C55CC275612D843FBFEB0F2B86CB659C6EED1CC5661B711621320A85B307C96F66C4C35C3081410045AF4B898C2C14B645AF9E0C9CFC03BE42DA4DB3A759E77B7D6894BF380991C3D4CB0D5B2CFFAAA08DF0085B8555228CA8E18672880C4F96E555F879C2CD6740960B94B6A60BE7557D2E230083D757DF76506311BEBF28C5496C00F338594AF41AA15566B24B004E60956AD09CCDB034B7C35BC5D550F52828007C1F85503AD86A9C811C440E6600B16CCD51EC098AE298042C128494A203D20BDB4789C4314C8D14543C9553954C9FC34C40912862500B2F3A5929471382D3531FC9767CAE6937D6E37D6AE18FE5B858BA968927201912782C6D436851C80E7AB0782FC5AC1D9C2F8CAC0D6C29148E220EB30909B2A43A4322B90E741C90C19AD52CBC1527CCC883209952A5A7C68E3E580BD987338BDC75FA84CDCAC3A0990246A7050E1022AF89C7165E45B32ED995A8C4B7FB28999E4CC2C79250DAB67119AC2C6B5B195526439E17A913F3BE3BE896E8023AE6A78AC00244B8EB2891CB5634955F908424C4B77BFDDA017D3B71909320CC53C4D4196E0C39701F8839711BCAE59BAB4B4908F5121B931777E17B331AFA5E955B5CD5840A734A41E35CC016C01407B02039B5064E97C24F4B9197091DE6AB22D0FC0459033EF7816D12596CCEDA120F534CE74571A4E668F5C1488C1503BC0354161BB651F673DA9019C9A83D9D97381E72CCBB1628F8F9A42039CFA07B1D67A1AC37407319360883539E7E060D32469CB9D87DE784439D2B453B77C158045F4BE5A96F090603963A18574FC027C0A0597D6A30B9235ACF5FD49619F47F7E8833D5D719B9F95825571B32C17B0B4AAE2DDB7EC0B14413E73EFA105EA308AAAAC76C2A257DD4A36407468802703787C4AF27D21EDD08108534AE994A1805AB5E6235B88AE7C6997374E50C1077F7702F809B092368F43A9CB2AB539FA104F5B23A4E69B9D46C4452208E3DCA58CC1045955784C82099EFB01ADB86CFEEFBA1EF3A43F16B7CDAE28A3E74C24B6648B8F20AB88088974B75BCC79E692C57501783F57434B7C2BCDE20B2E7129857EC9FC95452BACC9D96D89CB892C78CA07E790906A6FAB62CE170B63443CBC9BAD6F48B383050FB788270392F396C8CB8A73D4912115C41869D54AE1AE6173A34B6C3C91A5F6C39DA659268456E0B51389D7B05D4367C9F5C6CBD371E84EA9BEEB4C09DF9205296B9E947BD4E086A97760CCCDBA94A2108D02CC6544028E05A2FED297F32491042241EFC36A24F634DF3626A217ABA75882A050B7569318E8E00AE8A267EC0207CE9E8A524E1CE8F8CA117B0C77A680231F773B756152878363A9C38D93423C0378A60C8AAF8063BBD1484A3F37D055215A4403AB3C80DC9223D91165BC2D077C71A49093548D4A32922633258844193759D4DF26E1CE367C3229FECA492A9E7669ED4158CC08D9FF0C24027AE9759A517DACA6CD58090D5673C753101987B8914BD8B71C552D96DD093257A9313CBB757AE42860790AC70A593ED65A492078B6043B7B2AC4CC7D85A7145C6EE210C26D60906A5571AAC3EE71333155055381281A584A66C090252A3CA70D9AF09063F3DCC9BBF020715126122602AC9F9930D59669647CE64F356DA97A5C1740BB283781C2A10CB161BAB6B709CF9AC7C7911A23A9F4D6507537C497F3BA5CFE52AE9E34B1D9922669520DA2662BAA0282B33C5029145CC34B0FA9CCF201C6491E12646DB1D1548C0FBF6C7925C9670F4ABC84AB289958A26A9310ABA90CF8C41B39B7DC96673724CBF4BCC7754A71E3158B35214A8706652E11621635C5B6AEBAF64365497518D5D1B6635178E318569D23C50C3683CEEF593498037457C4494F03C7E92C055BABEB96C70EBBA20658787C1B27D6CBAB485741712F9169E3265E376A0241A36C0B2303F8865E6AA9D89213CA1F874E758A569E4055D70BB1B512C0845B933E470B1D9730B5024F520C8F2221B9D49836405525EC3864065BCA04A6AFDA11CC59A9BC28479D9D0B42B1B733E186D50B603CEDA34E8D283AB0939005B06815FD3531BAD6AA926F931C478F71A699A17741447FAAF6CF360D4C64098E9F1E5AEC6C6BE5AE6CFBB5E8B66EAFEFC8DDDDDA717B32220C994BE42776B296D9F7814DC0CDB963F257E983581EFD3A55A7B58E09734B10FB5A6F1CE03B12D16D4\",\n          \"c\": \"E4BE80FBAA47E08AB72D52DDAB90B35FF1F4C1DA793739388E49A548C6A1EE07770C6FD8153A3984AC2800150B20E2347DCE0A06D2C83D2A203DFF7788C969C969616FC1BE122067614989F34D0D84F9CEA1767D0D9D83DF8C573CF4A3EEAA6A0147731A373768DD38505BAA12C18B524FB2682BDEF71FEFFFCCAF0A8CB4F42A3A1E048DF6A66CF898A171FAFC840B46A8994F7D9A00CA42CFC2539FB3404472A39EF65B0AA7A376A421FB55B619E65C295A51047EC80334A7B40F3925FEAE500350D71139F6DF4C7EA9655ED2C869A7DE115FF7E926DB881E3372D47079FF3F48A944DBA7F70B0AE01CE961F16CBADD7C57A94EBCEEBD709B414F7DA764FCDA39FF044FB0EDC16BDE8C68AA7DBE92C1AE3C226EFDB7C5F1746BB56FAA7ED34B2A32C7A95A05EEB7E75DF4F7BEC3EB78B74A058FB95C20B33EB9E30ACD8340B685CCA66F2D1F6737646F67B28CD62FBEDF708A4277A8B6E82F012895438D14A3807C087BACAA432ED6A099470E28E4B06B64CF6B249E4AE72DB468948E874565ABA879FC3322A1A89881C55628C37781D28B39102E97E74F0921932434ECB061E6C388611217D29E16A0DDAEFDA0B420DCB83D5FEC1552025A98C4D6F19D6C1E23E934842D07856CDF0E5A9E8CE20F68C8425C8D54F7216B6B66BC3E1ABCC6DD5841EE0E1CC5C7BDC5A7F729A9930CD4BC946A33D6534ADB2BE0DB65490E58075FE3A8CFC273AFDE116A6A4C317177613DC93AED69D85ECEA4C55E43DA1D08780B30D7FAE75BD1000043A3B56E58ADE657679A54E3828A97CBF7436601408E5C00936D4BD3A4137E75AEFE338C6843EF3626FAF2684C6AB8C3F3A04773C913DDC72DEDEE9D45E3F0E37A3D8D5AD2D3DC9CA90B0CEB666C646F265E5A75D3D6F7E2455E11B5866C9D620FB2EF4D9CE86D0106DC84B35D603985B2562B3FA0BF6868313907B852F4B23E2AF6840C808EF8AD8A7B51456CE3CAAB34CF68CCED6A81EC1F9AC7E090AA1F854E275169C888499407C52CD6A7E1A7D572E31BBE6365056686E53A430015E330E89CEC44BA98A1DD6872FA3D4D71B19C65903DCEA30932C2FB94CF8233F26D50EDF3213A9574874B0AEBB1B6AF0807F352C40E2945A134EE7DF88439ED578AD4E2D7EA11D0C9C6CCDB83A03B6B9D026DEF328D2B3DEE0335C8092C46381E65B159E7478C615D14C2E800126ECF70455C3B4DA925F36C186DE2AE1B22D814F974D5BFB988F7D09140AB99A662382EF56370373DEA434AC42D1664EF116E92A90442518FD84952C7F35D0F42859BE3427C4F28741335485258756B00E2A90E7CCAF6B564326D25D76CE8A6FEEBAFCAE375135C1CCCCF12D232A0B062FA46F166AE2D99A36DCA64BF2F55B0151F64028700F831D26E62C41DF8DF1A46A8697C5A2F8EDB75C910CA1D382BEBFD3A4521E174F4FB3A258AF666501D00ECA819D310257E7AF85F4087AD1501EC4E18D1661EAF75F50BCF8DC80968FEF78770065699E8A857C12E507D88626AF509A2331CC228E1A2BB3526B687E63EC763EEFB375FE751EECFEA143BACD4455E8F6E1ABF0F82E4D41C5CB770BDFC3F78150D584DBAE744BE4C20199984445F435BDB46454D662F41C61A848A7C887C1A04D41D4B92EFE657ECDB9387E84495EC37CD183F9C2EB5E859D722A614F3EEBFD13FFE2491FECDB4DD2C06914EED59F8211908516C799AD7B9B46C5EE5AFA808B67B1E36F81D9DD3C9B27188BFD40495BAAC44DAB36AC61ECDE47CDFDA7AFD9C40952AA477818A38E3060613D879E78874254C599697ACADD42F1049A3E5BCAE75F88F7771E294EFC9D3899DCA955263671F5205953D62378A310FAB336EAAF4837CE6DAAEF1C4A141F6192E934A20AE23BCF803215B5A96D6CA99EE65A205EFAF39082A42193C5090783B426B35A1C8BA6A6FD00D3341ED24008E1D70946E22126F7CBD71A49AB15D2561FFB4DDE90A497A89049B22BE50905B63107BBBD13E5AEF39FB761091C7384519455057CC407FFAD746145BFFEE33E120922E06FAAD8C5349B46B3133B4EBF1AD9C84E9ADF35B5DFCA3141353A1766C04907A6CE0C3E9C6D84EF9E732637AD033829B13E0B9526A1C8BDF4296AB460B32CF29845888277C479565EBAA7C30811D2DA71BED5560A5688DC18643818FAE07D531DE7196B947CD1D94F4447B77CB48F82DAC0C7404E302F0445B656475DC65608B\",\n          \"k\": \"0CA16C93880B3BF4802D0EF7F03E5C192440CC4B399E9A55637F1E6AE6DB225E\",\n          \"m\": \"EF29D988D373C381541AC8723EB67C68CEDFB9DEC0FF2B40CDC763378B380C12\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 64,\n          \"deferred\": false,\n          \"ek\": \"E2C18353FB0F024A0E05FC7F0F14B49A9A36CB49CE7EB963A2C20A5228B0B6ECA4AF63A1F7B1B8260B64D2F2C0718397F4F402A0946C9DE5B0F083B57903418C30BD7CC321574531B2163DDDF23A69B04B1D393C5916280DC11485FAC369D0AD1C85656D4A6A7BA480FA5ACC1788AC37C9706C26A357D7821D636078FC2204530C3D3358DA002F3136CF033443C519B6C505D00FE92B04A305A890267B238882EA9010554A1671862BE8097B67B8E8A37E8F0A5C96F94FBE296162AB1FA456785550AEE6B05A80554606B76714AA91C5E7B747A2005900077874CBD91970111C800C49CF05A4332570863929497AD6617D1B5614F2670390574F554C7D337417C792FF8953143638F60A7BF9517890D56D02039AD30392DEB045D8CC4994274E5CF83B702784637C6D53284F4F14C15335A86A642427E589AA454C6FB9707155BE23745FEAD04EC1CB07DA341F7FF1B6D17C315BC3282634A39C9104392B1D12222E8D091AAC69CD5A72276B01BE22FC3C634B41AB613DDC602EAAA00A72478D713C7A0F895880224CA8489E6860868A7B72CF876FDDB230C55C0523342C5F42B0A533A8FEB798D3104145E27D29CC96D54301D5B07274F700B9201555430D8EB961AA7C3307B0B56B479BCB6147D9035D786A58E7199F6BC9335918204A345826446A50801603EAC992E93FB104B834D7220B1494FAA2CB1A59650B00AAB00AA15F6909DFA969DED0B540648830360ED052661557280683084B93B746E27D916A05C96CB73964A4AE15CEBF4C84A1528506A69D2FC27A2A16CF270A09654A8B566651D097C61079A859171BDFD35262116479742DF74C0845D596FB83984F27811FF81CEEE0C37D618B93321A7DF0C0D22124CF521D92715EA7DBA1DD6112E9152CE2E15FED4769805A5857896EDAE13854648AC9DC26B770109372929FA511B573685D7951ACF21BDEBBC97D384D454B5625059A7EB8034FB4533F55B897B77930F3911F2808F54944CFB96BC0CC5AEDB8960A9C024984CDE756441C6381B3E38909DB3E85A36408C5414B471A68093B188134B3F54C751061093C4DEEF54891039AD71A74134806C7F102CDE67432D37DDDB4ABF33987A2AA47634C80260B5A246758102C864B2B40613C42425313855897FCC835C7C587B007C94760AF26A1531CEC874B9C492F4AA4C97567D6A61C21EB00FC52B1E0F24E9D680F8FE38B82243FD0B562C0DC86E2024593562B4AF4A682ACBE720813859B92A9C20DEEE4B4FFA1A1B17A24C394455077450B4C2CCE67A0E5C4016C746FF508A1DF89A5BE2A2A7B866ACA0A72A603843949909CD16DA542068604A380741C32E986366749CDD461A4362401CB8243613A2C442983B52C681A3CFC362FEDB337910668C5732906C41637778BFA619D502640545715F709C31B506534C0216790779EFC053B4A7789662B989A75C6580754A92588A30617E960C714171C8310A2802092261A85A5C882D407AE263A119B3E68E3BE158BB0325C30026BAB21D30B09637A54FA0A4B980DF807048F139223D33CCF206FC68672113A55E2084B6700B396F68873636DD8E5479BC0CA3E47B52329BD7F20681D5C2208AC0DE2123D50F6023A4472B0300CB3F8865BF7B49374AF01B002C4087BDAF4CAAE4336AA3A78E3856DC408B04E9AAAE77C464351C97938CAE829171BF724A8779C7FE20E5FC254D0BC283B773C2DFC83D28C3E854140A81267C2D2C67F361DFAF623D817B1655CA8A626A8A91BB088F539576C8795CC8A53044FF547B328377F4D17273AC323E6481CBFA058CCC456741057F1F668CAC51F34D666AEDCC176382037C674B4793104A4BF6B336E3D59A1C43958FD827493CB956C046025737B7FC0A3C2098BAB916E397191BEF5CC13F3BA0FD98F7A25BE0CCB113EE081B9AC808E2C728801C70CFBB2BD9A34469C064EA07952BA0D579380F7E782D0D5C6E1C26F3E17619CD47D4BA88C61600721BC2AB3C50F591988CCD77AFD3C43655272565401AD0778671939636C8BC24C794FB73EFFB99232503A0B50030A672CC8C2C0FDD5CD43B131DF0C9B9F867039E2A2B41B3CFB1A82C983AC54E002388692AD885C676AC2D7D0C5645B06A2684E9463AAAFEBC6C07709DC68078F7BCA36F5584579C3E3261D319A58392518ED507ED54ABFCA95CCDDB6C74949DCA48D01DBE3525A0BD91AC78428D5A930A5\",\n          \"dk\": \"A5AB9B1E9973958172E182A9133A3DAB22806CDA0B88E72BAF581183973220061D836405C702A8494422B43568291CC432443543270DE0B7C85C56AAB20C0A17617C13B9903458C20C423A65092533AC4BDE6609E90711BC031A953C3A3139960F14CB17F8B3C3C68BBE6B84271AC5F003898A79912DB2B68A1C05AC204626C0275551BA6BE211D6962E7147863B44CF9CCB6D6836007FE44AC028B43EC9AB65B792B61159AE35477FFA5D783281BE62CB707496F1AAB956F364ED1B8971F35B6F8179FD77C30980A163A51836B06F44311C3E7062172B80391AC5B20409CF16BDAAB4047DBB56255C325580976CD60113C72A194772149C6C2F342507016FF6B919613260902B3DAC2673D4584C9F87BD093AAD02950B8D750C1304A749F703D0365433065476B0CED26B3DF644B5F34B89A055C707EC6D059620F12962C74A0B78FC7CE12311B1F94504558DCB10BB27B198E2D80E1F8378C1462133209AA1A99BE03A978726AA4F88152C05859F667C708A9430C6C1FED06784E72D0F30198749AE8BA86172263175FA6FF380CE7E325AC4A6BF3201881DC16705A530EDABB7E0A28383D16E6F5BB0A87164917A6E942A9768B049FE37931D0192C7B5928FE14846B2686307A77757AB142996B049ACFF85AD5F058526510AF0D72E67DBBB17179A91B3847929145AF84FC355A1F454A82963A52CE6CAAB7BBAF7B22044391AD76187F37ABFF9210905495292F289A45077EC567D0F5C077FD44DF2A568F682368D2046803880B85994C61637B4B19C32D217B3DC961D9858D88A6D8FF2620E9674D9F5C9D066B3346A765E8684CDE20C4ED300395B6C1AC200F28930668B426FBC509F810EFC1396B167953ECA9670D34594415B61293D849C4245A57F6E2481ED0A40AFC8B2C1F7476151AE95C68B26E6679C155239C922D0F89E10B9C58ABA7A56EC7890F97B31E05CE97C4691F27167929313531086D46041E3452F74390BF06A787416F58537F6395AD5397E59CBC118D6825F26C181BA514D0BB42AF862BA5670C592132D4473C66A13EBF58E63021887E96DAFB14FFC408149F55D8A68AFAB24AFD1482C27555818495BF1B88E0F072ECCB44A0145A880C51172C6A411940FAD3BC31CAB64D5A2160FD8903445A6361B9FBCE5B7CE74761E339BD6CC3517551C0D2357095CAB8B34656C182165979A7F01930B331D6AF6A5DA8B57BAF76ABE198C2492AF2365843D33C84D013E13D13ADA4B1B387B8CF4DB7445C002D689CD93017A7D3A03F1B224EF7075AB88616C5B2DC343C0D46504D292A1A51960189A5FB968C985D707902B54DC390C8EA9A8EEE19DA0B0436B1259268C7B87A26D48A978901B1C44C6C0646CBB0A079F999345C31147AA9B3A51451F11C766C5B3C0851A00BB78350D60AA6437228B84A9DDF6869310946B46C483A05D7C04060AC89402691CE07B1769B23062927B875B4506E208F939A02BFA0AFC954FD0B034FB5B268EFCA527B01712E12F83E8B7E52996A68B63EE865782593C0FDC222E4449BE0657567903D48827CE526BB4215A31B8147D475C06D629B2A39DA7D258123AA844CCBBEDC83872A96E426B4DCF546BF2455DD739B8D973BD5C43C1A48A2FC4EACC676C5062069F9AEBA459D3A8ED880C63FA6ED80C06CA906FDCA895F2D9CECAB617740891F2A20B14DAA6343840F0CB33AFD38850CA87B0813388339B582A8DB0721339DBA66C2B9C118CC1400047FB7634CE8A8063B20175D49BD4B22E87B3AF335535528CAEEFE31827F20FC75831113B3013492F3F1709C873C09666928559110A5947626A14093128292022EA021750828D8121477BB0150D07D00E715F08598520F2744BD849E7082DEF2384A742B1DE61B44073691D6A34519A541F7B265E9B7F500155931628FF194D29225BEFC366ACE60F3394ACA71C1E9A7C2021DC241B339244F0CA3685AF2CB16AA668692F485D31590DD3448352E3C7C892191DD81605EC9211330EB720922F5A096FF342BEDB2D02160B95686E8302B82408A5076324EEA620ED81AB3BD097E173AE25BA777BC03AD2459191B225CAB540F8C1197885AD7ADB324206652BC678FCA24A57263DE089ACE6E1794E4B49EC85735B91830513AA9808572818C7716A61A51B7C59408703F066B7466CE2C18353FB0F024A0E05FC7F0F14B49A9A36CB49CE7EB963A2C20A5228B0B6ECA4AF63A1F7B1B8260B64D2F2C0718397F4F402A0946C9DE5B0F083B57903418C30BD7CC321574531B2163DDDF23A69B04B1D393C5916280DC11485FAC369D0AD1C85656D4A6A7BA480FA5ACC1788AC37C9706C26A357D7821D636078FC2204530C3D3358DA002F3136CF033443C519B6C505D00FE92B04A305A890267B238882EA9010554A1671862BE8097B67B8E8A37E8F0A5C96F94FBE296162AB1FA456785550AEE6B05A80554606B76714AA91C5E7B747A2005900077874CBD91970111C800C49CF05A4332570863929497AD6617D1B5614F2670390574F554C7D337417C792FF8953143638F60A7BF9517890D56D02039AD30392DEB045D8CC4994274E5CF83B702784637C6D53284F4F14C15335A86A642427E589AA454C6FB9707155BE23745FEAD04EC1CB07DA341F7FF1B6D17C315BC3282634A39C9104392B1D12222E8D091AAC69CD5A72276B01BE22FC3C634B41AB613DDC602EAAA00A72478D713C7A0F895880224CA8489E6860868A7B72CF876FDDB230C55C0523342C5F42B0A533A8FEB798D3104145E27D29CC96D54301D5B07274F700B9201555430D8EB961AA7C3307B0B56B479BCB6147D9035D786A58E7199F6BC9335918204A345826446A50801603EAC992E93FB104B834D7220B1494FAA2CB1A59650B00AAB00AA15F6909DFA969DED0B540648830360ED052661557280683084B93B746E27D916A05C96CB73964A4AE15CEBF4C84A1528506A69D2FC27A2A16CF270A09654A8B566651D097C61079A859171BDFD35262116479742DF74C0845D596FB83984F27811FF81CEEE0C37D618B93321A7DF0C0D22124CF521D92715EA7DBA1DD6112E9152CE2E15FED4769805A5857896EDAE13854648AC9DC26B770109372929FA511B573685D7951ACF21BDEBBC97D384D454B5625059A7EB8034FB4533F55B897B77930F3911F2808F54944CFB96BC0CC5AEDB8960A9C024984CDE756441C6381B3E38909DB3E85A36408C5414B471A68093B188134B3F54C751061093C4DEEF54891039AD71A74134806C7F102CDE67432D37DDDB4ABF33987A2AA47634C80260B5A246758102C864B2B40613C42425313855897FCC835C7C587B007C94760AF26A1531CEC874B9C492F4AA4C97567D6A61C21EB00FC52B1E0F24E9D680F8FE38B82243FD0B562C0DC86E2024593562B4AF4A682ACBE720813859B92A9C20DEEE4B4FFA1A1B17A24C394455077450B4C2CCE67A0E5C4016C746FF508A1DF89A5BE2A2A7B866ACA0A72A603843949909CD16DA542068604A380741C32E986366749CDD461A4362401CB8243613A2C442983B52C681A3CFC362FEDB337910668C5732906C41637778BFA619D502640545715F709C31B506534C0216790779EFC053B4A7789662B989A75C6580754A92588A30617E960C714171C8310A2802092261A85A5C882D407AE263A119B3E68E3BE158BB0325C30026BAB21D30B09637A54FA0A4B980DF807048F139223D33CCF206FC68672113A55E2084B6700B396F68873636DD8E5479BC0CA3E47B52329BD7F20681D5C2208AC0DE2123D50F6023A4472B0300CB3F8865BF7B49374AF01B002C4087BDAF4CAAE4336AA3A78E3856DC408B04E9AAAE77C464351C97938CAE829171BF724A8779C7FE20E5FC254D0BC283B773C2DFC83D28C3E854140A81267C2D2C67F361DFAF623D817B1655CA8A626A8A91BB088F539576C8795CC8A53044FF547B328377F4D17273AC323E6481CBFA058CCC456741057F1F668CAC51F34D666AEDCC176382037C674B4793104A4BF6B336E3D59A1C43958FD827493CB956C046025737B7FC0A3C2098BAB916E397191BEF5CC13F3BA0FD98F7A25BE0CCB113EE081B9AC808E2C728801C70CFBB2BD9A34469C064EA07952BA0D579380F7E782D0D5C6E1C26F3E17619CD47D4BA88C61600721BC2AB3C50F591988CCD77AFD3C43655272565401AD0778671939636C8BC24C794FB73EFFB99232503A0B50030A672CC8C2C0FDD5CD43B131DF0C9B9F867039E2A2B41B3CFB1A82C983AC54E002388692AD885C676AC2D7D0C5645B06A2684E9463AAAFEBC6C07709DC68078F7BCA36F5584579C3E3261D319A58392518ED507ED54ABFCA95CCDDB6C74949DCA48D01DBE3525A0BD91AC78428D5A930A5BD1F755E964833390BB7E9BEE7B1C5B34D07CF3593530572F57DFFDE8C0A167CBF377142317C203D37A8BCA5289614884A22271B47D91E03EE6D366B24902271\",\n          \"c\": \"4995176407FA65D288DDBA1FE91F7D2ED8B686096D49FCB85655AE5BBB09001FA8B8167F20C31A62169C319F798E38BDFD580DD070FA31499931C580950E2023C739D0D9C13F96ABAD0DF1F1E8B718C78D228BE5855CDCB5EB3A0B63C6EF156640615A763CD211FB94F540379F1876DD0B8619FC2EF14CEDC6BB9265F5C01B2833EA4F726F68B9F8B3AFFD39E71925AEA4EBF66894DDD1C4761155E9663AB89ADE8A50E9EA6B8253DB7085B8002FFB6C409921D7295F37E9E0ECD45C7B204FFC45792238CCB54997D2CA0B7CC4F056FA2B783A384528721CE77A7AE5C6938FD1CAE8CE5E7BB57405C6D5DC0B9517D45C580B0BD91AF807EB1989BB713C2DE9D3C7A891E31FECEBB5244E89F7F9DD574159415A81C459BA845F36E39B7B5FEBAF565D8A79D1E9119C618258297FECEF53430933956BF4BDB3BD9F12DEEE9BD0691FBE24512C178F1086DD2063B46166A3B00679C8D26EA493F28490523DDE0AA711DA3BBE9E6F05F569808C97EAC0AB5C8C0CA1F1BD7172DB19D94625904C08EBB3AA704108D033BE20B6EC6520065E1FD328C01437B4373E8DDE6F86170563D1E17ADF963F9D82AD654702AF5FBD30B76727A75982DCDCA3B5398C82FA42E6225AFB16C6FB466E9D5CC040BEA834DCE7704F94E0296504DF1C63908AD8729FA8DF7C9DEEE23D1D5922AF0548BBBA27873CF73E2970976DD56CBD562AEBA199906F86847AB60FD9D25BDEAD0EC2DC3C7D251D2396EDBBEFB438A098C7665094C088D9BF57AD4327C61CB484E0E730D5E7E98926B11CD4519C729FDCEBF625D7C743996F3B2E8FC38C29433ECE14D427C08EA079AD0B47466A7533C480E06C48CBA27E180011BD10A1B1F7089A150D48FF59EAA9154046F954303F3FD9BBAB91C667F7367868DCE21471B2ED4F2200674E221F71BBE457A323EFF93E89C5DFA4E8EDF87C8DE741ADAB529587663EE1180D60BB42CD8BDC050AC41457C8FB931EDD71B68B0448D036679D5BCEF2C91EDDEE940BC8D3E412A363712413F6EE9697D932941281381F1FD1EB6ACBC6FCEA805CC6FB301C4FDF4062B6BF12AF691FF0E65EF4448DEB4E403C2D62610A8451807B14D722C0940AE6C7C30EE6EA4C4D7CE51076A27AA93F3DECBFF2E0D753DCFCC8444A223074FB0A291764DA8B4A8E7B17CA12F7F3AEAF69B11C0D00835140769C59EACD9E9363DEDD402F126480F1763C50676FF0DD8D21C2DD74B2F0C988A88D49EB40F99A728A3A896AFA9A27F865DE5F4F97A2899CA92F1804C5F3A6969EA60DA4EF3B27D822B5FA6D71C8690721A531EB637CFE3D1369A08F4B0B978FAFADB14A26242810F5B5906576A7F3F9C7B57837CAE2F10E164067E08E35D5A6E2A27DC81EB1C9E6058047EC8127C388AAD709C0632CA237FB23BCA6204763AEE078E876C78AF48B6D22867DC199065880E52EE13449568300EC14396B35CCE9878D6A6517B425F7E0C7A146F62198D1165AABCA7158C7F6E91F32D427006B9F1B160FF7E5570FA36DDBBD16971A165E6A550B1648DCD01BA4854FF7627FC35C95F2CAE53A742645C859A96AF248F6686201DD3AD31D783C093E8AF94F15B099EAF49A0DD4664A8767E0F618ADCE6394E09562AE39FB4A249852B00DE3A14B263DD7DC12617839FDFC57A7E219C638514BF6635B5D6D32379D15452302C9EB9EDFEF626FDF46110D10195B605EE6A78B5C772F57E91FFC73FF4DC0DB7461512D66D2B7BBBFA9B85D0B28F9FAE8FB979FEDCC59AFF493E1C3E03DD91FAAD36CA4B2430AFB342566379E49CD06591D70AA18C9880B121CDD802A5C37622175191C4497C11447ACB6390E604F9D026C9624D8A6E23292A4EE82F7D99AC1AE2C541D9380280F9FE484F4E996D9D39D471450DAD5BA51C0268AA0BBBAA02B06DEE0FB2E956E303AB1DBE5C221B69C5239EC35F95C0A723AB4CA76C4E0C57E6D559695DAEA5E7BD1070BCFC3D0968B7A2A2EC80B3E663421D47D9D5F0970D6651A62A1270D4B47B03AD4E1C3615F9593266849E6C0496CF40333AF199734051EBE1FC3B02B3B72F61872D0BC86E729DD73C81571421FC9C58DB88DCB0318A25B0112206B1BD2141435294CEC995B18A4FFB723C2F73327602656EB7C84D82266D1B288AB7D363497E009AACD7454F00052B96B0A6205A91CE542F6CA0C7C187001FB0EC8C917ABAADAED2C5DE369224B8F00EBADB8\",\n          \"k\": \"0CB1CD5F942305DFEEC6F10D2138621F61283C5A87EE1C5205D3BEC21D9E5489\",\n          \"m\": \"3D6441A62F1998E2B5B9B1E73A9A5022FD005778204977F66F7A5FCEAF17E30E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 65,\n          \"deferred\": false,\n          \"ek\": \"53EA0623859BC606082104996B5CC3FF292846658A17D60CEFF78B4EE37FD693876084A46A9A8337B61079705D2269BFE7486161E3370FC7492B92C666A889962B7450B15993C027051B8F0ABBAC66EB2024F6CBB63868D6389F39E580E05A357E5179367A48D5E00F03503C0E4C11EF5758B05C8B61AA51B3019C5CB017DEE224685035651703D0C5877414846C30A20DFC25B6E8AA76D98AC95534D1CA8A7E9177C57B2B46A0237B8B678BF889221C64C1882B7196AEFCB8056E0CAB1AFB0E50D467F09A9733960C4BDB5AEA3C9F0447B8A7198AF5D6A9DFA6785ADA30456B843F84CEDFC6761CE507B85A3715C3CC53409972D28E8F2A1E9EDC9DC3A368E2257A57373C967CCCF8002B5AC3C762E8C3B9B607C7451A9DD28F06472F3B4503C1A31A276727958210810716F53A8578FC76CFDAA35CB757BF069CC8429A9DB9C4F5B1602315882F11C83C260720A11FD6D01FB94171588539C5328E951B1E7F5605B4557F5A476430456A33572B60161A43A18A3780516D232F8B87B5E821081B109A39DAA7D74396B8213B37D314DE258B3FF0934FB27C276A913A337051E038EEE78542D78C8A04051A76A3BCA319BBDB99D3FA87FC9A03AADB8D0561B77C394D2F60A84B096D873BAAE7342378D8513E8B8B13DC6AC9E285966CB2E53521E4928465841ED7CA8F76B56BAC958E011C6CB751A4F7D788DC73CDE1F67A24EA9CAC41A0676C11B3D618386465C7557FB0BBB7A3441497764C9D898313A2ADF59983E0B2031BF7364EF0C25E347D2EF1111F29B8509085264C9D366970FE8CC19293AE8407602A21001391A51C609C7184C8FC0267E683C6FB83B0EE66B6EC1C930B1A5D60C108EDB646841415EDC5CCAE32CD8581BC48E58E8598285A4B31F805A56E395556B120B2DA591D3985B4D7C50ACB1D5DD12617343F9FBC9854250500C03725B010D45361C3DC745648CF337774AFE704EECAB4DEB9746104566C8CB0DC512A06E198CF04A8DF13AB6966AF12D552F3B6BA72A266625749D6AA30D1DC9DE8DC26870C317C9C26324A265787635E2CAC1922057916A425872C48E29C39AA4C06E24A462161C20ACFA6B458F56012B3A828300570F9EB92ABB3400691C8E76BBC1D3B36E6254E771130B555A68CA05A3A8112051CB10A2A3B0F547A11E53D803970E671B0E85C146BE1AB47F63AC46439F1C7AF41326DAAC72AB1274CC6D0AA3291C0C2843229B820E3C7B0B3B600EE72AF71671FA875CBA59782221781F3338F45B1466D60C31F6189F2D29C1F427D440B3560C11CEBBA7B0E298D2B270FB6D21DDEB30990271F15A3947B464802062851984AF1DCCC5A7083FE7C81A454A03AA9674D98CCDE10A76B3C696802347E519156F977CDA5923D73656A0081A26320C6CA7E1AD7A12362176D9A18AA75356E147FE3278C911070D2D61111B87FBBCCA67B991E5915196994034CC83246C024F27C1C62353E79290C26552DD52A57F19ACE5C006BF7774CD6C07BAED7696D61C51C8108C42237D8F4846BD4891AC44605D93F8EC03DE607456555365212C35D59B7FB267BE713CA43502BF3045CC2400C11A571C8468093F499C7E46D34C9A75602782BA606AE67A5D3C50D31CA5FD6E3B963FB7650DC5BB312BC35F67650E4B12B780D4B6680F640372A99C90433A7AA388AE8043DC5E9AB6105CC6FD91E61F35CBA99CA39E979256A1700AA9373AC14F282B71ECC419417984AC34677F0A83386B91028B4B7ACAC22C57908AA9F04F878E7B6420132A47AB5A1BF5A30116A6E59B6157E4228BFF499CEB59A09276B5D398B6A8677E725725D5C54809097833CA7E6136EC994AEFFE00C87F6845F20BF76C2A348E08C3895277AA082B84202229B7698924B4D5684E8C8AB81B34B87CA28D3E95FFF82B5DDA9BA9172076D937B6D02745DB7C1F77B3B3C795872CA728EC36029DA80353310D4A50CDDAB29E1381FC20AC97F48920A445F566C390206C6183666B2523B6A43BFC3E077CC0C422C1C91176C50B34C419AB4C97D640F1FB913B760685B64126725C7527A313A454D5F683B23828965067943E12BF633411B7B8C71F648E4FCCBD2A1262FD350A66CCAF2FB1D9CD66515C52E1BDB1478726426C84022F60A6DC61F7CA2B33A60957BC50B4E248DA86071FDF8080E7801F81199A9FB0C5888643D8192ABA9DA4D73869D884AA2A7E0727231D4FD\",\n          \"dk\": \"76956EE8A1A973DBC0C7D40334A81B43D495168A6FBA8AB9A4392069096B598B99F1D0B9F0285DDB138DCBD1B0301ACAB6AA370C101EEDC17C748AA6B1F69A796AA6D20232AD98C64A4627EA98B3A595C0DCF558EFA42077B8BA2F29CEDF464E21858112E3A29EB327CABBA5FA33BDD1297E4224ABCADC68BD0B3AE7895FAEF1692BE2A79F95720D541245657C754330DFAC485CF3275D5C613FA74E99185AA8B38E21856C0995567DE346D2D9B8C691689A6A3442C0C2EE871D0D6B7D6D46C8C4C504CAE38EF7957886F70538803079F986AA945D0F0C25F1EA9C41FBB4D2C145460C39E4695A7A9560714A954647CEAEE703EC8B40AC38423C64A46E64196109C6776424007866AB06B85D73A267CB789A2A7EE4B827FF0ACEC846C4A31A01BBEB3EDDD7336C5BBB4575BF40365673DB24B4034F6AF1C0906C71AA3701ACAC8EC96114399B1CF3712FB1840C31A255477C2709D05C80981AC892B2275223007C8D84E9C4E9854F3A426819084EC2823714F47452B921B200834BD659758705BFF3CEE671756513A6562424EF1A1F21589CF321A9D790C846E768D3A022527937CB82309E95962643C2AF5A6422EBB2DEA1ACF32839A30993C3A604EA97A5AE000DB8F8144F414D7E86513E5CB0B0E2596C53C7851222CDC48C48224D4863A7B35C478A42285B8935B594970AA67B89323DA2B67B6A7A6A9FEA7B3BF648FA64067EBA6BAAD6BF640A44CDD896B68B125504C3DA5BB9DD552BA400618C453FE57683568CB2A8B86BC33B519885A06889981A87CE21D913D102AEB9A26E5474784F499914670AF5DC8613D9BBBB9A51CE708FFBC8A63F2A6E5DF6CB461A7F215A8CE2E472F1B813028C49E9E11777B3B1317ABF96513C1E349352FC2BFF7AB0995147CF46AAEBB71D18399F678CA8F72960C6C5052585027D180435551A3F8B44FCF06E6BC8B9D57A171601AE1EF3C21E7133C6C83C365C72B3C0A56EDCBB6B40695738C713D34C4BE6282896539EB261425C1572685C7026B4F56967F359A121FA031FF007772B5BB92B1167E25D706B99097A2F38ACAAC81C02748BCF02748DE17A8D2B857DC2474366CA0660328BA08CC6D52B3CD844B0B821AFF3B22F10294813702F7E24A78FE083BA1BB88EF55C11534524E059588A02EC83B652CCB4D55968E8B52F293B4E75BA1790E95701D850C8313EA5E77D62681156991B67146D1C1A6DC6E7004D2037C7C7AC48C80BD566A78EE811F3E0C7598948BF897A6F0AAB81001FB52B4974A3C1150BA5C9685D3A632E68BB9D4925882A48AA5B01AEE9A44DF574ABDECB7B1B3B755188804F74A9524231EC444300B4644BB56E6606523DE99ACC8A4CC6EB2D7A93C1847B5FE288CF19F96E86C49DAE2B39FD678C63844EB4A7C39A25895F338ADF1330ED92AFB2013EB1B5729BACBB36EA25B56B6C5C309021B74E96D037B14077F025830E8044EEA0BC36D1610B9741E1B5CDC9584469864CCBF413A2DAAA260C8FADD82702EA66B6C811CECA5BE4DCCE53DACEBBDA081A184C7A55371AAB3A53ECC2E9B82C27D743635C4240F5394ED05F5C265AE6A120249103D7E2443680C655F10D45050AEF210B9CD306465282A5A2C182089129E53C0B4C028F34761D19C4096A622EEAB6C5262338735FB1D28E1C50032DD31434E149E90CCD8123ADDBB0AD5B903FAD334F4EFAAA04376E225C17353B278CCAAB104C83A855941A1583D6938C5466A9D6C63AC98253338BB19AD8645B70C05F90744C96779FC9479384CA19D46AD43567799AA248CB3768F324D8320160712BFA9B781681AEC61675CE2230C1F02E59A1CE52836F3CC252A0D4B27B14C46B2A08A2D0C656B5449A390C90DB69B3571BED8A76A2E26E41E08C728B68A1B659E8E8466D96B653D132EAC36F2578338D2334DB7B237AF744FCC08013288C1FA9AA26D88E1E399549A323F41718C138A857EB5103A131578ABF0221520E2ABD914B8F993455F980B726597345942F4A319963E65A704B884FF02D01C76A0A3503020CAA99F6C5D8894FFA3917645BAF9AF793CD46577291216A4A415033C31E78AD8F8793E4E97048FAAC12887267C9C043BA1A5DE6C009E5C8A68026F6798479F45496A142CA031CF0A3C0F8FB1110C3C37148322897A6899895C125B385CCB153EA0623859BC606082104996B5CC3FF292846658A17D60CEFF78B4EE37FD693876084A46A9A8337B61079705D2269BFE7486161E3370FC7492B92C666A889962B7450B15993C027051B8F0ABBAC66EB2024F6CBB63868D6389F39E580E05A357E5179367A48D5E00F03503C0E4C11EF5758B05C8B61AA51B3019C5CB017DEE224685035651703D0C5877414846C30A20DFC25B6E8AA76D98AC95534D1CA8A7E9177C57B2B46A0237B8B678BF889221C64C1882B7196AEFCB8056E0CAB1AFB0E50D467F09A9733960C4BDB5AEA3C9F0447B8A7198AF5D6A9DFA6785ADA30456B843F84CEDFC6761CE507B85A3715C3CC53409972D28E8F2A1E9EDC9DC3A368E2257A57373C967CCCF8002B5AC3C762E8C3B9B607C7451A9DD28F06472F3B4503C1A31A276727958210810716F53A8578FC76CFDAA35CB757BF069CC8429A9DB9C4F5B1602315882F11C83C260720A11FD6D01FB94171588539C5328E951B1E7F5605B4557F5A476430456A33572B60161A43A18A3780516D232F8B87B5E821081B109A39DAA7D74396B8213B37D314DE258B3FF0934FB27C276A913A337051E038EEE78542D78C8A04051A76A3BCA319BBDB99D3FA87FC9A03AADB8D0561B77C394D2F60A84B096D873BAAE7342378D8513E8B8B13DC6AC9E285966CB2E53521E4928465841ED7CA8F76B56BAC958E011C6CB751A4F7D788DC73CDE1F67A24EA9CAC41A0676C11B3D618386465C7557FB0BBB7A3441497764C9D898313A2ADF59983E0B2031BF7364EF0C25E347D2EF1111F29B8509085264C9D366970FE8CC19293AE8407602A21001391A51C609C7184C8FC0267E683C6FB83B0EE66B6EC1C930B1A5D60C108EDB646841415EDC5CCAE32CD8581BC48E58E8598285A4B31F805A56E395556B120B2DA591D3985B4D7C50ACB1D5DD12617343F9FBC9854250500C03725B010D45361C3DC745648CF337774AFE704EECAB4DEB9746104566C8CB0DC512A06E198CF04A8DF13AB6966AF12D552F3B6BA72A266625749D6AA30D1DC9DE8DC26870C317C9C26324A265787635E2CAC1922057916A425872C48E29C39AA4C06E24A462161C20ACFA6B458F56012B3A828300570F9EB92ABB3400691C8E76BBC1D3B36E6254E771130B555A68CA05A3A8112051CB10A2A3B0F547A11E53D803970E671B0E85C146BE1AB47F63AC46439F1C7AF41326DAAC72AB1274CC6D0AA3291C0C2843229B820E3C7B0B3B600EE72AF71671FA875CBA59782221781F3338F45B1466D60C31F6189F2D29C1F427D440B3560C11CEBBA7B0E298D2B270FB6D21DDEB30990271F15A3947B464802062851984AF1DCCC5A7083FE7C81A454A03AA9674D98CCDE10A76B3C696802347E519156F977CDA5923D73656A0081A26320C6CA7E1AD7A12362176D9A18AA75356E147FE3278C911070D2D61111B87FBBCCA67B991E5915196994034CC83246C024F27C1C62353E79290C26552DD52A57F19ACE5C006BF7774CD6C07BAED7696D61C51C8108C42237D8F4846BD4891AC44605D93F8EC03DE607456555365212C35D59B7FB267BE713CA43502BF3045CC2400C11A571C8468093F499C7E46D34C9A75602782BA606AE67A5D3C50D31CA5FD6E3B963FB7650DC5BB312BC35F67650E4B12B780D4B6680F640372A99C90433A7AA388AE8043DC5E9AB6105CC6FD91E61F35CBA99CA39E979256A1700AA9373AC14F282B71ECC419417984AC34677F0A83386B91028B4B7ACAC22C57908AA9F04F878E7B6420132A47AB5A1BF5A30116A6E59B6157E4228BFF499CEB59A09276B5D398B6A8677E725725D5C54809097833CA7E6136EC994AEFFE00C87F6845F20BF76C2A348E08C3895277AA082B84202229B7698924B4D5684E8C8AB81B34B87CA28D3E95FFF82B5DDA9BA9172076D937B6D02745DB7C1F77B3B3C795872CA728EC36029DA80353310D4A50CDDAB29E1381FC20AC97F48920A445F566C390206C6183666B2523B6A43BFC3E077CC0C422C1C91176C50B34C419AB4C97D640F1FB913B760685B64126725C7527A313A454D5F683B23828965067943E12BF633411B7B8C71F648E4FCCBD2A1262FD350A66CCAF2FB1D9CD66515C52E1BDB1478726426C84022F60A6DC61F7CA2B33A60957BC50B4E248DA86071FDF8080E7801F81199A9FB0C5888643D8192ABA9DA4D73869D884AA2A7E0727231D4FD1AE23027CC9C62AB641D0F46EB5F17471706A42AFC921B48AC0A39E50F560DC1502C2C1BF811FABD14733D6ABE45283CBB7292278166018EC48D33904703828D\",\n          \"c\": \"6070F979C45C39BC4191E7B6C735F278337E043F0DB0D24B7321A51391896F4A7D122CD14F98A2C0272C4D3FD039F029227B6D83BB800C7C6E09A7894B6CD86FD68A32F31CE3880EB9D4694C4551058BC58193AB13EAA62DF3DD14BB9606C29511E4AFC1E0566DA175D4AA45E428B241D6F3419710A8FDCCB0BB74792811570E0C24A29D034B83FFCF98755CF227AA3F8C80E9AF7370DC7A7705AD3808C3B33FF40EF131C879779F72722EA582E130D825B7912B3F6DBC5659D59088333C1883DD43B7FFF8F942D584F42352FE4A15AA8D3D0D5829884293A693585FD4763085541FF83BFDBB506DE54796D3641038410E4617E009042881A942311849CC789658BEC01554A4D6F9E5636E59A76C1733C40D90ED8B0763F16B8217684F484B9FE19E07A8947A3EF04C27645F300A664505ADD17015C2B1A319D414AAD10C638B1F37FCF61F81A80624CE7D75E2759E0B9942CE6251349DEB9EF56A5A4245D6B598046186D91240626F1F37AEE04704E7F6A14214A520603B7ED44CCADB8A2E39093718EAC9AEFDB96CA2822D8213F66655017BCBD465716DF13542B053FBF0D99018D4549E2D1B19735D6DB95041C0AF04A169183CE0A634BA114E90C30429984633097141E1BE19DF941F2FFD228E2C627F02798BBE3A886147CE23168E335590C1C3DD337E980CE4769AB187A2A6E855166646F91E14E3B97700753ABB6F811D474A412FCB951C2568EBA98EC9D2C51F08F3DB5D2EA797531D65A250579F98BEF2EC5FA1179C2DC6D9E27E66F983AD70AADB1F5067C104FE7B7A22F808F4C5AB71B881F2D6510EE85A0118C74DDCDDDC8DE8DC551D41BC14DF90294567AB06FD76FC87B92AF9AC0B456386C714D5773B500CF15596A4A71D8A6E23578AA9D89C596D67FD08A379868305148076106FDE47DAC20882E913BD2D397179D4E611E8CF25608AC3B50D12A7F7EEE1572B403387DE2D0EFDC35A3C8644BC3BD4DA9F1E2E1F2ED341CC1DFEE0E39416DCD6261AF74E84E0F6D33D91EB0DAECF19597BEABEA1B690514766F3C8EEE663923B3FD25D36401D33A39E5972B8B17A41D230374D7C1783B208545167243B31C32C9DAA330B3636A1305F96DD612990A5F1D7A4C407DCE61CF73F5BAF5C55B737D9152A5FDB19DC4969E9CB4B049318B88EF4AEF97DA536DFAF3BE6D3EE6D1F2E4C3F7E17963418CDA89C2A481237A6F2EC303F380595872F050D1569ACABEDA5A3219F8E7BB75CA77541395AC44EE9CEEBD1D4BF59507D77014F310CEBE3A322C8636DC450A279C54332F46C8205653556360B51B31E06BC1B5AC9FC9091859DFF301EA407E5AC051231F774C94E2E932A08CE22ADA65E4EF74D7A4BB1CB1947F5490E88D569BAFDFF756EC451494E4315FA25283BA63FF61BAE0E45C4582342CFF3F3B42E297B97C9E73E946151F8D29A47E57A6ABF45EFC5411B607DC160FAF7674665D0F0E5039ED8C7E59BE98FEBA7C81ED54DD51A4542E5CEE2712F25825B18177FB8E8BF4FE1657062F31B8D8090A9512CAF8876B09E97725CD36C9BA2738F1D4C863A7B99B27476812700D24F3576EF6007D2A26B79ACE6CDA9B291527C800F0D666B784A11BED29B8D8267D5A643E33F255B6693B129F38BEB976E4FE7AEEB0C78BDBD8D31326A155921236255B7FD029307B80CB002BE230DC3036F8AB7CA446BE34BF5A5F29E2D5A0DBAEFABABFF6DFDE4E400FE491F40C2861B0BFEEF44DCB14803C79E8FE79C7C3AE113E8FA0B2F507A3711B8C56ACD0B090935EF932E6004A36B9390260F3080C2DC75A61B69E758EA4D31F8142845218E1DDD85AB7579428258CB5C57286FE308D1BC164BFBD574246FE6F7ECD9BE6C91B24EDFEAE6F2BEF86B13780649AE57B1F7D764786FC4C7EDE4775C2461CBE37026BBE6BE5D8B0637239BE3A7F8B6E56E2B86478E9B5461F5040596B2207061BE9E7851DDBCD619DBFD7410B05A361FD25613BBCF5379D347E43DA571EE03EA19C8553B966E9FEAC579635241E82749F648A7935077163523D19B9936648EE5FE5CC902E66F4379156721A63824E97294D8FF0C4CA93D995F6C635E010937B8C94AF28BE078FC94E94F9DFF2E6747E2F0287BB6C756D8D3E1D1B0A736B92279F37861FBBFF8322C676472B44667815755EFD9BB4920AAFB689B7692892960E059D010AC883ACD8B5068C1EE9B87E82A4B637A006B\",\n          \"k\": \"12266ADDBCC27B282DC0566CCE7473F4D705D1DB4B3D82130AE29C3999C6A999\",\n          \"m\": \"637B7A1B57EB76C50417601EB71269E050008F415DF974C07BEF46CEFD08368E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 66,\n          \"deferred\": false,\n          \"ek\": \"51F74568AA5F4A54B2F4C9CFBC7666B7075A9D65A016416A49F16EC0CA716B75A3D80609FAAB06DFE69661706013A0C5FB7AB0340CC1BA72C2D7E1635B33B0EC3B29FEB369CEF671BFB907B3A2219B1554151A62B43638C635219F86B792B18527D8CFF00BB24853A5A6B81C9BB77790B25BA8A71A7CF7231A9007EB8B50F586139279CACCACCA4C7C64EDEAA194768D6A33BDFC42218EB279FD111E1C9633307AA30830B161D93F13973BDBB3772FA57309E36880816F97DB8E7E6411CC569A578A5C6F62AA33C2354CDC15D2065451E1ACC212ADBDE104F094378ED8882BFA416B1C55BBBC20C1E5AC89892B0CBA2B54516937E781F7A4A9B1802876598FF95832F6478ABEA33362A40EE28AA957E74540129D6FF3BCC0114A4AC8A40AD30BBDDA2BFD2BC5A6CB4DE5978F9AAB64CC8659A5765AB11AC3A654A247165253C81065359E070921188A312805417FA149D335ABE8404293329C73DA1E2E43CBF9B22B0D84339F792C9FC97006345FE6DB931AD71F897878B4318073B9A64400B5B4D0C4D6FB57E8B511743859D8E873B9C5052578282F9A6362172014089F2993BA7FCC2444FB185EF055D9D6A0EB564D5E4667E537AC9B49AD1241950140B188F4C88CD6A241BC42122A3DD7720E6E6B214D2721BDFB9358E7026F1A024132CBD0BB0EDFD83552F271B151099CE55D783C88B29A578B2943E3809A68D268DF746557435B5EE1597CCB58446403B98C88DF38399CE58BCDA1CD00462B9D933A83546526BB9E5C887050F5109090600CE84072DBAFAED20D137A7D129ACCDF1712C94098267148C0B1A301DC618D684152C817F27B3E8AF83F91951347C633C7921804D853A2E66C948143DF1B3C15B201EEA56E764273CF50AA58B45E71950FFD32C69B079564C48B08A43857E09FC33B8222A3852A055E3E4CA7B72B190E6C8B557AAB84D7BD138CA148330B75AB5FEAB8419A55938771053A5B09261CB0D4A42575E72F8F505D77F0896780CCF589376D5A651E4008CF600972008540A0896F80C17BFC78E16B64956677CBF864683735A279B966A4A906B1972AE4875BFC81EEFB75248B72E3A3768EB56D2DC3C04921011F4A92FDCC8EC9347353FA0055CAB83B7465D221B877A70B59715A6075799C19C8818595E6D478D659C7F9155A841B28EC751866CA3C2A829D43467E12563F5A4154947581D4B4ACD827B3315B44B3E31BEE0C04E9C03D1A186AAA7438EA9AA15FCA83D26B79A2093CC4505051B31F3D563832F397C8671516648AE92108A8E149392209A04816EFA1207CF854FA655538C161F6561274E5154D89A223665FC285B6D3A45A2F38278DA7B5055BAF8B1A052196C2547C3BACE09CFBA1299071635510ADDE3898C4C20E4F8034B454BFCD0CA262812D43C4A8660A5B2CA42E2FFA7AAB513DFCBC437077609596A45FE8BF330741A88A5FFD20AE19B58C3B19774A047F563AC6CF0A205A2A4C1D6641F93AB4C0F39E6BB51032F0A12B8A45E900954BB3A4379129E44918998A50454050607101BDE2975E66C0AE2436664B15850294FBC985B638C8303767DB7A10A8A74D5AEA0EE2B6105112525890C6D5A8CD1712A2913B73C386C450F7286F4A8ED7284AD9F36A22494E51D43950526B19766E388AAD84A39391399628F9C251D4C784AC2AD0424FC57BBEC391A549D59E5638A8056929203BB8F9A44A27117B28BA2670508680D55B82B4CA8E523BEF1AC4CFE83511B920309A735C62CDD5151338C50D104950C9273263E8250488135240A949958090F06BD46B5AAA3035CF9B7308C09952B66AB5999EC6E6BE7A027964249D63D1817F430737C96F54B391EB44640959277B56B495C49F482B829F97B94FF8B4F0E347A9B6BB83E9AC6617000694AF0C7AA13A273BE2981286ABC62444AA16FC043CE4B1B4F50437C9503F815772A8BFBF6B229D48B75D94BC02219E0F71BA020C37169B42CDC1A88081B7846C3636F3598630B314D809AAD1B7F3D335B41468C019C2C061240BE39325D34DDE9C24FD29CB3ED62F815C248F4B6E8D3908FA3B9C8AB1534AE49176652DB909059402128F18C0C9E52DED217C7CA864E378CB59711B0C764B96F13EC5E3766297C9B303806C685F4A861878CA8415D60B057814E554B04F49C3DF9AAC26A5CDCEC91B59F32BED063E299ACD431F8B781FBC1AB90AE6AD004FFA864E0E16AB57\",\n          \"dk\": \"2E78C01275512282C156828B65705F7163A682878F9E7C0762153C8E63012DB043810B705D9C3DC092C1427808D5C8BBD361A860C5260D36A3DEAA97A3D97771F7804B3B97CE43A4ACD7A54B3068E2727AAAC7C3F960AAE2880F8CF049898A6D90664024136967D18EA0FC6736EB0D20665DF0075BD9789E621158A4A20F036832E71C05D1B3218EF75EFAE05262A21C1A4A8A427350B0C128DF37C775D602732B21E5AB36674C0AF61655E28C56BBB9B1D6174749082F29D8177427A24E5597769758C2C4BCCADC32A618C57837BA8FDAA8B89B35AD8A2C72EC03F9707C0FA4C01A231976B0B15CB39438217C061CB54AFB40475911806952A42441E0F77918E7CBEA9639D1F1A2DB9C785EB5025307C1924862BB68C5FC652D2AA9AE043B31953393FFF3A2616B1DE4649088450617F548005A477C1897260046B60BC120764A25DC4D9E1285C48878D3738A3A47687CF36621267041982E50CB6311827E6D27278E6862C724007E75B35499838D9553D9C79196D84A7BB2B386790ECD7647D3A60BACE84C679AC3A0B212181A0478D72E4E50B7C10CC5A1B154970C0625111A4ED8CFB47209672B7597B16FE79786E8B571A32A7EC59BB71F745439C181FF23C26B96A5B10A41324B2F8021648CCB016BDB92D894CEEFEA1876A30DFDE22973DB8B70D7CBACF5B6B4B54CFB9B2522919007B6526C084B8F392326461F0BD6CA9966351BB45F2B77564D1723A0EAAF0A2AAB372B9C18FAAA21A00DD2F4570A367EB2EB4E3E719D1E72A164C4A8DE6609540B6E93F71759362C2A6C930385217821BA00D0BD41C059176086E61620187B6B7D448A34A8631B9913B2C3307E529D360749DE11571BF91866A098CCD29DBE9314A770C217356EC4A0CC3D98CA22EC85F9C3C4F3A065EC5C48AA9269A8E5482B9781E7A39B6FFB287FF730919535C5987034A32AD4101986BC9F17B2AEC906169E3C56DBB213D6FC4BFE48038B0C1E235C4E106C1616D95F75142CD02B18AF13B56CD45EB1C46E6F008234992385EB21A6A7C219021EF47B55F5301E60F9CE8FDB1E3BC56F4DA253F9799A160A077872AB7551013375A2C8B3B9AF70BB048C4389EB90CF31512E080AEC7673C7F23EADDA8BC17372C8BCAF100438B0167FE2737BDD54C9571CBDB843510D615198821A2AB16126D107507B5D31D4349C0A2E47D6044B6802700C9FBB5B3300A2B65D4C457578639696BE7336B618284D99514701CA0B3C6701B008CDE5A88908D813E2BCB86D032CB13B70D34A35D07567B6DCBFF3343DA4A9B7390CA1FBE195E384464D154FCB169AC35087C3013127835F3F98B996E9815DD4C14A6995ED766DCA79ACCAB324AC20184A93C616F53BFA722BC9FC55924048702820A423193A858E6E399470C3373F8B0FAE335A1694B96BC29B15B939F81312311B548BD819A8E9168B303E2F82AD8110531B8B21F00517A0483516C160E8229971CA293016B8A5828B83F87951B266523388A82458ACFCC98647B2043A2525EA8F5F0A66436AA8F0A2C736E8284C8257FA49475B2A54DB2C3A9B0BA5DB92C48088CB79572CB21B7CFCD8285B54600F75035A00AAA5B31AE4B201755BCF0E4B32A84588136C9D48B129837CBA057AB02BD34A3871BA824A286685B3598007E39B931886BC22944535F95A5E6C1E8A95B48E22A6541C1EC078903CD05DE9E43EF054661412629D860717A90F91685BC54B62A315348F4B8A74B672C19883BA58C854D349394B20FDBACC8AB0C0A8972DAF080759EC20C6677DD956A9A48B07A727ABB239382A2767FF2522E7217AD3A2AC6C13331799BB0BD39803598FC04921A1F90B7DDA2FDE2A2FBA4173C9D390FC31B6388684D6970BB4562A69688283EB6327D960DB19B308A9BEF87CBE30606C56F8306AB134E32ACECE27011F740004D473A6C1016C5C9F4ACB992933801DA80B379043845A3432B375EF670DE54C6AC7206BDF711B38F272DC916BEAEA6A370008845C792CC9C2F64BA10DB51ACBB18EE0EA99D028CDDE671F14FC00492BC43E282813A8B0BB13C3BFC0404F362A8A45012B77377B8CAEF7734E0C22C4BA4969F610482E47882B209C4201378611C88E9CAB82E5CA8BDA976F11803A11AD5B288C043B2B8891168E4C75F62678BCB05EA5B11FC66A2551F74568AA5F4A54B2F4C9CFBC7666B7075A9D65A016416A49F16EC0CA716B75A3D80609FAAB06DFE69661706013A0C5FB7AB0340CC1BA72C2D7E1635B33B0EC3B29FEB369CEF671BFB907B3A2219B1554151A62B43638C635219F86B792B18527D8CFF00BB24853A5A6B81C9BB77790B25BA8A71A7CF7231A9007EB8B50F586139279CACCACCA4C7C64EDEAA194768D6A33BDFC42218EB279FD111E1C9633307AA30830B161D93F13973BDBB3772FA57309E36880816F97DB8E7E6411CC569A578A5C6F62AA33C2354CDC15D2065451E1ACC212ADBDE104F094378ED8882BFA416B1C55BBBC20C1E5AC89892B0CBA2B54516937E781F7A4A9B1802876598FF95832F6478ABEA33362A40EE28AA957E74540129D6FF3BCC0114A4AC8A40AD30BBDDA2BFD2BC5A6CB4DE5978F9AAB64CC8659A5765AB11AC3A654A247165253C81065359E070921188A312805417FA149D335ABE8404293329C73DA1E2E43CBF9B22B0D84339F792C9FC97006345FE6DB931AD71F897878B4318073B9A64400B5B4D0C4D6FB57E8B511743859D8E873B9C5052578282F9A6362172014089F2993BA7FCC2444FB185EF055D9D6A0EB564D5E4667E537AC9B49AD1241950140B188F4C88CD6A241BC42122A3DD7720E6E6B214D2721BDFB9358E7026F1A024132CBD0BB0EDFD83552F271B151099CE55D783C88B29A578B2943E3809A68D268DF746557435B5EE1597CCB58446403B98C88DF38399CE58BCDA1CD00462B9D933A83546526BB9E5C887050F5109090600CE84072DBAFAED20D137A7D129ACCDF1712C94098267148C0B1A301DC618D684152C817F27B3E8AF83F91951347C633C7921804D853A2E66C948143DF1B3C15B201EEA56E764273CF50AA58B45E71950FFD32C69B079564C48B08A43857E09FC33B8222A3852A055E3E4CA7B72B190E6C8B557AAB84D7BD138CA148330B75AB5FEAB8419A55938771053A5B09261CB0D4A42575E72F8F505D77F0896780CCF589376D5A651E4008CF600972008540A0896F80C17BFC78E16B64956677CBF864683735A279B966A4A906B1972AE4875BFC81EEFB75248B72E3A3768EB56D2DC3C04921011F4A92FDCC8EC9347353FA0055CAB83B7465D221B877A70B59715A6075799C19C8818595E6D478D659C7F9155A841B28EC751866CA3C2A829D43467E12563F5A4154947581D4B4ACD827B3315B44B3E31BEE0C04E9C03D1A186AAA7438EA9AA15FCA83D26B79A2093CC4505051B31F3D563832F397C8671516648AE92108A8E149392209A04816EFA1207CF854FA655538C161F6561274E5154D89A223665FC285B6D3A45A2F38278DA7B5055BAF8B1A052196C2547C3BACE09CFBA1299071635510ADDE3898C4C20E4F8034B454BFCD0CA262812D43C4A8660A5B2CA42E2FFA7AAB513DFCBC437077609596A45FE8BF330741A88A5FFD20AE19B58C3B19774A047F563AC6CF0A205A2A4C1D6641F93AB4C0F39E6BB51032F0A12B8A45E900954BB3A4379129E44918998A50454050607101BDE2975E66C0AE2436664B15850294FBC985B638C8303767DB7A10A8A74D5AEA0EE2B6105112525890C6D5A8CD1712A2913B73C386C450F7286F4A8ED7284AD9F36A22494E51D43950526B19766E388AAD84A39391399628F9C251D4C784AC2AD0424FC57BBEC391A549D59E5638A8056929203BB8F9A44A27117B28BA2670508680D55B82B4CA8E523BEF1AC4CFE83511B920309A735C62CDD5151338C50D104950C9273263E8250488135240A949958090F06BD46B5AAA3035CF9B7308C09952B66AB5999EC6E6BE7A027964249D63D1817F430737C96F54B391EB44640959277B56B495C49F482B829F97B94FF8B4F0E347A9B6BB83E9AC6617000694AF0C7AA13A273BE2981286ABC62444AA16FC043CE4B1B4F50437C9503F815772A8BFBF6B229D48B75D94BC02219E0F71BA020C37169B42CDC1A88081B7846C3636F3598630B314D809AAD1B7F3D335B41468C019C2C061240BE39325D34DDE9C24FD29CB3ED62F815C248F4B6E8D3908FA3B9C8AB1534AE49176652DB909059402128F18C0C9E52DED217C7CA864E378CB59711B0C764B96F13EC5E3766297C9B303806C685F4A861878CA8415D60B057814E554B04F49C3DF9AAC26A5CDCEC91B59F32BED063E299ACD431F8B781FBC1AB90AE6AD004FFA864E0E16AB5777DEF69A903066F0C5A3F4CFAC6B7408862923728492E7E5FF26A83A48EBB8BAE5D9C71D76D5958744741B9FA4B7EB67799F54AA0717478C4BF0EA6E0B012AEE\",\n          \"c\": \"229B069FFA4848A699156C894955CD9BF623BD28ED0E2F34B8E1F62A1B3DD00A6AAD501DFA776604A874C5FF1E60C3FE89EC281DD320BA2C1EA16E99D147B0548710EE11CA2540DEAC882A7B63057400030EDE2E75BDA52622287D3680A2FA7DAA94FE289D1E3879E1039CE2B65C9407E7A49CB93E76B4B4BC1D247227F437696D816C08E401B2D82670E189AC9F6A33EBDB2C0B16C2E18C9009BADD550B1F533C265A3E0153C982D4D64B215B7CCABBEE4B644B1592A766C30B28963F0991EFCCB5A94D38B38813CF9998A362318AAD81CCD6A0251FE63DF879DA7B7CE4C9CC28E86211E97773D3AEC98E1931734CE8629BCC668C490426BA60CE2E28E2871DB69B9683CC6AACC4F588733A7ACED2F17EBEF11061251AA8745F147BF1DA650386F7CC637E9D02870C16E2F06164E694828BAA66610EE984B7C60D0BF82F8B6D79EAF56D75FE605B3CED809DFAFAE3F858632E3147C3BBAB8D931A3B00E90693E5B840A77277C9FBE86FEFB2BAF134AC21B8B47E3D0A009894028DD5645ECE152838290EA835944ACCD78CE3038E8A2DD991ECD66DA742FEEF94125E554BBE04F0E923EF1BB02381DDE16F1D41BEFC8C418A6818FEF891DC75ABF717DD84979FA47FA3280364444AA77D72C26E808EF6202BE04C4FBE4B26A5B8E60317AC1D6860F5E728CF316DA287B3DFA391AEA8AB4CABF0D88419CA2C4B461E8C4BC633F0F2A6AAB7A86179170DCA1DE6A2885983CB4C0BA34B1D2A53AB9751ED91C49DE832ED22BE9443B8C2732B32A9BC14889121E5B261946EDD759394474627A9D82F6962F76D67B94649788EE82FC81081F008F6DD183F27B69CA19A5CD1FCC962502E827183AE32DF8A369F1A6DC44075BED6CD3C909B161FA62CD67F2425DD1D86D8401F7ACD5A4D5CCB94B9E22E4D518A9EBCBC705ED3CE7068977E1F4D7DC12BC51717D00225DA24402BAFB56DA5B7C8580F53CF66CE8F960B1A58A40534D29014A73781323E520D60097B5BBC5492D87D9103F040BDACC7640DAB97E6127458153953E0624A26ADC8875225ED6FEC2876DBCEB536D01DF0001477E64DDC542898B6D502BAE3FD7B49049A1104A290107904CEA445A045EE1432E79D8B358C4F28E42DCB05A94D04B78069AFF8A0A2CF2AF94D9837C2EBEF85A914163EE1F10E411DCBD7805A1EA204468EEE5E6DD682E2C1C7E30E260DE3AAD4AF6F2C6FDFF083847962EDD0052F13F0B8F2E4659F8D00260F38EE0BDDD4D6953CA0BB7B055CF7FD160D19AE5C3A1967FA0AFB09D71AE4AB63FCA185DF32DEF74E786E050FAD63D789546AB7723B64EAB6BBAC9B0B0AC9D3C994A71A68BF5B378B4BA8336CD1D4725F3EFA23E4B9585D3006C42485312AD7EAFFF80CE82F7A82DE7864D9EDB3A569892ABDDAFDBE384225BE377A051B75F0F5FC99C291CEEA13A6D1C1E6F8C1C5E72A86016029761B3E2D024F33B7C5B91CB2481F7BE4986FB381A3B8E20F8FDD5A390572727541B4D4988337CE19014433BB47F31D1847AA0C4E28288B1707744F542383271B32D85B73E3857D89703BC80FA6695F617D6DCCC17147779B9A4522C955D443CAB3F61E3667B44C0FFCACC136C3A487CCC760F7952A28C2826A7F640BEB3B3712EC3DC52D75047B7D3ACBDB74F32B0139FFF1C2A66818EDABF1C3FAE5DDA87512C8ECF60C133D4B98A7D1E1C72DD8A487B0A2FD9F8AA59EA4941B9958A2025385636B9258F4188B8F1CA96C4FA50A34E326616ED2354364C892295D2AD5726E4FA077DCE9FD110A95823B5DC355664ECCB7543E987D9CF986453C5ADBD438F849C6578CE859842F90975365E3FD5D083A0442E7830D580F51819A04941EC9284B31FC457060A7A6EB91BA7A87C0A54F1F422C5F0B6D8D4323881DF5525B35CCF992C2E701534A213BD935D30684086BB916B44FD9D678EE8BA7578B9E427FA36C139F1FD92DACA7FC8DB85607DBBD3532DC24BA45B53A36180316DD4ED5FD4449034308F07A6571F9943E194A4200F6834557CE568712D41078024762C7D020274B6404B326996EBF2464E40B8D9842D0417890DA0BBA9013A97A9259FDEF545EF780FC0D501A0137E9AB22A1BDC96252BD5137C96697628E559B00C2AE813323F2B9A1E2616809AB59945647167AE2B2E757C21273FA0CB3FBEA7E4096DC1CC8F4F114B94F5D760D496C1BA8ECD6B1EC68AECE4C9A25D1C561\",\n          \"k\": \"EBEFAF52036034E249B29A1825226DBF469C492494E9C4F13BD1010B963E0D4C\",\n          \"m\": \"B5C84B4535CC622A5D6B93229BCE68789D3014D500D3263B6E0F54359D20ECE8\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 67,\n          \"deferred\": false,\n          \"ek\": \"10971ED51709D256CAD9B57F0E48850C4B803642732C89A84439CD31C6B0F045915090168F8952B3382BF1186A56D2CBC102898F112FD91A2BE5AB0391A67C3C5255CEC88FACF3B6ABD427E3A410ED22C2C6C173F4894E9BF20CA3713CF0E8506846507BC762047715A7A37DA3A5730D8A1556560609E4CCB61298C4B2BEC374B534D0B266C427088816E843B118FA9CB27B9D1F549480F98F7F4B5AE71BA376B0B9180A157D8128D2286C4B5BC54F8590852B5CDDC5C11E6B4B36EC7CD6081D45A9C50272643CF0569A9A237581AD36D0705C004091F7C46D9073739226EFBCBAD5B551680C86E4290C3CA1AA756851A380ACFC033FBDAAC5B82C364DB075633A9A10B8137A05B29863AD1DA1CAEF5ACD2D75A9643B495EBBC184038B58B9A95998AA95B672A2D15CB78B0A00E386F433425C469A7677B8569CAC8F5A3A035C777B4570BAE1898FD379ABA62C960024E55645CA6625327457A37A92FBD68C76E79528D3B9E3E8917F93B5774B9A7DC44AE416006D8955895B6C611814C99432F683C67C09C354B97DA3CB0DB93841991B764C9A08EB26B9BFA96EBB39027D6399AD2688AC047360A7989BAC587FBA7DA76153360298A3D67FE73C983E4563F67207E1B835B7DC7CC9CC3372E12DB6EC759B86A684702D2B6CC805726DCB04AEFBDA224C02405635040EF60FD799A906116410A6C5EFD587BF1778CEDA4CB0152A272B89E43017B40C9B974600F03908EC397556E8AA7271828C049F895753E7BA96569A280CA15F5A82CC1F6121B6F7AEB214C726B1C64F7A958DB7459D4A30E4599A71FAB28D5196288A443D6C35C4AA27DF674EF6B4B72FC6485A31410457C182304538424A7501A7D62086910ABE0C54CC752B5E8E87039E64233EF5B837C3B50286172DE5C9CEB92C9A0BBDE4E7B14A0419DC6AA03F655592D25DF4A6191EDB8545351B2D89A7FA89826988657763A93EB71F8832B159E690B95C04DF37A2C9024451A18088A1C801B40A601AAAA6EB692E229817D8A390BB2474A95A7131C84D1745272A289C84BC2742CA1097A610D8A90F2C609651AAF5D0414C3C3FB3D37AD8CA382C9980A52539BFF73FCDE029FEAB0F184A7EDE4C7142478253C84EA5D7B6DB963ABC39A6B5BC29D43BAA24B4B26C7C53DE096E85E945310A484F591AAEAB48BEF98353C17EEB457E7D0719247A62EA85400B23491CB65932200F74EAAD3DF7184A54654C7B6DC931B574A2A55C7679F15C5944191A58D2A5B24674FD80452B22ABB7305501D9A156D0C48C656A35540FCBD43F14F531557BB9E8A3A710C000AF227FC6878AD28A708A34320191A236B38E64F3C4E705555DFC1FA3A144D8E6A122F7C5F557462579765A017AAF4A814E6B1201CC4E8D0757C378198A5798D3A59186A54FF158A451C42564B4659E0A35BE734600210A68F8A64E3951E849CB1062BCD6335970005B1B69C3A4845226A2C0834C1A467C6A799609CB870C7370C39265C0015BCE968419EC7C58464CB518550338B6807A97528FB34D1011A67F15B06C1BCD7B5909047136702C9AE604B927F28B8918CDC6F07A5D60B38BF7CFF2EAAE3C075D1579A30A76093B7A6DB44AB21A722F818861480129ED03B1FEFCC2607B5A1D5BA6ECB91B75B9810F991DC55823333A927AC13C3C297EBE02A27B949F47CC70A2266FDC76B5B55468A6B98D33955BAB3418F2D3C1D206C29EBB05E97811EC140307F9347255381AE152113C2ED2A7C1812A93FBC67ABFC1C1233B864AFC4B34B9CD30358990F51477911D6C68338A9857335B9152C5302F32BD5602A9ED97892F14951F111CA2B607DC46CFE6297D0A41BEE369CB3EB941FC2701104713A1784D6DE88948032AC78251328A8C58311848EA02008669B4BC811D6232A859756B306F24607DFD812DE89A0A6A00304FE795DD1C4A55366E615957855ABE11D97C7BE95A43A757948C5F4C046E9DF6A70674541B20010837307443444C27AD36C431F1F372EA659EB0693447FA5C40E8A38A7472D4A1BB0180433585B0CEF74650733CD90094199ABDFC1392E24C6D1C74408D00AE27F05D1729433A90AD9BD22F1B6C8155F68BAD1307811770A749A85F060E74625893462CCD946E2C1B79B6CB3CEF971899AB29F4328CE29313E4794C3463AEA46106D9CB82B5EC39D83D27C4CB3B69DAFA2E955D002E61C3E7BB247A76042FFEEE7E\",\n          \"dk\": \"57C613B806BEA20AC23E2CCCF3B36DD7AC5F84843F273C8D9923CB8770A34A63885973AD2E006B45437722151C3D0539D7723AD1682473C378C88670D646646473A18BDC30367C136874BDCCA49E5C42774ED96279CAAB1594777100706BABCBFD46652F42702CC00C810C2F753263551C44C928B6BD319F1C1444EB014DF1A874282B99CD11AE59413ADF4A3459F9A52592B1EC026977E9B26022A6DC059EAF38854D49C4F691A0E96A031D822907C664D3E6B263B35F3E967167F953D91A05A5AA0C4F5B5B288745695AAA2378420B6C0803D855C3D42A623A67B3376712B2B299E3B155134FDB582D0D221CEE318FAE1309A906728ECBADD9CC298BA245A8D4C081B7BDF87AB4E56C07DD98488F342BD405B3CB327FBDD85C80D2766E2485E3A351E0A3039AA60738858FBAF5BBF6D9CB25592E1D82468BC6B23E678BC5E4559F125F69B414E1AC5AD6B4498C9738328CCFC6BC4E7469CCBFE7AAAECA8A22874727A9194EF2BBD7564DB3D927D85A716A4551903944A642B5131511B619323CB620435B501580354BE09C3E46AB78BC25D08B29440B9984E75D75F263A081A562A2C96D538F6A037A6CE9C1B9A0021E0591F874AD08032BBD18A1C6621D597A47D4311AD0CB91E4DA1A03952BA7718187B86BC6E396CD716E539C2AEEC8285629A5A7B4596C6B4B5B5B272FBC00F12B49355C1AC73B5EC245C3B6E59F3C81C76E9675F51616D4608795A7BBFA3B0B96AA90FCF55BA7768CFF752FBBF3C0E2847EA939AA88D0C077A5C721DC1F09D2100DAC98B9F49848A7BF3028931C72451715789C11CEBAD69456131B70CBC27882C598311A0AC50CE188BD4A671197934EAB72B8842157F364ACCE67A4CE358A521895DA285331C412A20A88567B93623875949734D2665FB2A907E03A74D24531294C6FB1B45588AA61655977330C0F46F5CAF50466EF66AA89F8C15145A6903621BA269DA593CA28C5C439BBC2B0F74D3FC213F10029FF66CFA50AAB701048383A188EF7A8F40A7C73C19DB0C92248C95A3F061D53E3A3E9D76966C62C711B7DADD17ECA87B418B891527CCDD872B328C47B0F2798EEB90A9B46C75009523CECB196F5B941769F1167C4AC86249A09521DB9C005BBAE80B1925D01476E5C8ED2519F579CB46A01B1A618AB228342F06993E7252FDED96FEC54ABC11142FFCC6181F2071BF1B5C4A23EB782A0DD03078B1A9C5433C1E1306BBF4C388386814BF859C1070A033016DD599CD221A35DAC93739C9B97435013B6567EA18E8D60CD3F8186BCE420252027FC1C91CAF785135300CE525351A26E1281C05AB6BF9B0CC3AC01706A7B7B855719D7290BF16064D8136A2209A70DB24BC5F693AD385779A8CB144C80FA0435EC6C4EC9492015988634DC88BE46164255B9E78910299CC52174654F48B351B418636A2D41A474384562F9141F37985C343265A3788F7A53414A94A9F8982DE444BDFDBACEE37296FE2B669C307EC0584E975654C3209B082B4E9454B058D439C7CB7FB0D80F2CD77B330271F287A0F481995E26AC2ED0B572F04FE5C75E3B256FD5B2CF6A953EF0A85FC953A5DB971CDE68CCB2F5A804699DDE247D4C09B9AD34BBE2E7188D2C90D862AE1DF56FEA9A8639029CEF0B73C7352D1C94A29CFC28325AB1D14100A7F79AB087A8EAA749119B7F96E29B93293662C57533B47F66163D31BB3E4DF28BE89C48227C3FB2E3673EF858C7B11D37C2658ADA9DF048299EB883472637C774012A935B17C72987253F211C27BA97AB867A5D35C375158A637273993F68B4341A644405C013273C6C881E49D2AAC13C15213C9352D3C84EBA1951DA0394E19B660BC622696AEBF8841912B2B3347D9C8CA1D98565C4AC93A5255FD91B41A9A1BDF255C6C0860F05176D41F58CC8305D40F2622087133EE968165B1CDF41B4A14A7061710D9C00156296C4C428CE2E57163C0C77F2C9416E89905586C29D012111B07C475A79A9D227D9A531857A77B4017F0E800739980BD401503B472749ECA34816B76A22260DC120C986AD57861DFD2B2BE7D184F9B95AFA859392ACAD78016F18479BE83361568001F9187BC22C35A2A0BCE4AC7840354F3E9C43D711CC12E57E3FA2BACB6AC8B7993FC698B48574AEE7F6ADECA360233371F95240FF492210971ED51709D256CAD9B57F0E48850C4B803642732C89A84439CD31C6B0F045915090168F8952B3382BF1186A56D2CBC102898F112FD91A2BE5AB0391A67C3C5255CEC88FACF3B6ABD427E3A410ED22C2C6C173F4894E9BF20CA3713CF0E8506846507BC762047715A7A37DA3A5730D8A1556560609E4CCB61298C4B2BEC374B534D0B266C427088816E843B118FA9CB27B9D1F549480F98F7F4B5AE71BA376B0B9180A157D8128D2286C4B5BC54F8590852B5CDDC5C11E6B4B36EC7CD6081D45A9C50272643CF0569A9A237581AD36D0705C004091F7C46D9073739226EFBCBAD5B551680C86E4290C3CA1AA756851A380ACFC033FBDAAC5B82C364DB075633A9A10B8137A05B29863AD1DA1CAEF5ACD2D75A9643B495EBBC184038B58B9A95998AA95B672A2D15CB78B0A00E386F433425C469A7677B8569CAC8F5A3A035C777B4570BAE1898FD379ABA62C960024E55645CA6625327457A37A92FBD68C76E79528D3B9E3E8917F93B5774B9A7DC44AE416006D8955895B6C611814C99432F683C67C09C354B97DA3CB0DB93841991B764C9A08EB26B9BFA96EBB39027D6399AD2688AC047360A7989BAC587FBA7DA76153360298A3D67FE73C983E4563F67207E1B835B7DC7CC9CC3372E12DB6EC759B86A684702D2B6CC805726DCB04AEFBDA224C02405635040EF60FD799A906116410A6C5EFD587BF1778CEDA4CB0152A272B89E43017B40C9B974600F03908EC397556E8AA7271828C049F895753E7BA96569A280CA15F5A82CC1F6121B6F7AEB214C726B1C64F7A958DB7459D4A30E4599A71FAB28D5196288A443D6C35C4AA27DF674EF6B4B72FC6485A31410457C182304538424A7501A7D62086910ABE0C54CC752B5E8E87039E64233EF5B837C3B50286172DE5C9CEB92C9A0BBDE4E7B14A0419DC6AA03F655592D25DF4A6191EDB8545351B2D89A7FA89826988657763A93EB71F8832B159E690B95C04DF37A2C9024451A18088A1C801B40A601AAAA6EB692E229817D8A390BB2474A95A7131C84D1745272A289C84BC2742CA1097A610D8A90F2C609651AAF5D0414C3C3FB3D37AD8CA382C9980A52539BFF73FCDE029FEAB0F184A7EDE4C7142478253C84EA5D7B6DB963ABC39A6B5BC29D43BAA24B4B26C7C53DE096E85E945310A484F591AAEAB48BEF98353C17EEB457E7D0719247A62EA85400B23491CB65932200F74EAAD3DF7184A54654C7B6DC931B574A2A55C7679F15C5944191A58D2A5B24674FD80452B22ABB7305501D9A156D0C48C656A35540FCBD43F14F531557BB9E8A3A710C000AF227FC6878AD28A708A34320191A236B38E64F3C4E705555DFC1FA3A144D8E6A122F7C5F557462579765A017AAF4A814E6B1201CC4E8D0757C378198A5798D3A59186A54FF158A451C42564B4659E0A35BE734600210A68F8A64E3951E849CB1062BCD6335970005B1B69C3A4845226A2C0834C1A467C6A799609CB870C7370C39265C0015BCE968419EC7C58464CB518550338B6807A97528FB34D1011A67F15B06C1BCD7B5909047136702C9AE604B927F28B8918CDC6F07A5D60B38BF7CFF2EAAE3C075D1579A30A76093B7A6DB44AB21A722F818861480129ED03B1FEFCC2607B5A1D5BA6ECB91B75B9810F991DC55823333A927AC13C3C297EBE02A27B949F47CC70A2266FDC76B5B55468A6B98D33955BAB3418F2D3C1D206C29EBB05E97811EC140307F9347255381AE152113C2ED2A7C1812A93FBC67ABFC1C1233B864AFC4B34B9CD30358990F51477911D6C68338A9857335B9152C5302F32BD5602A9ED97892F14951F111CA2B607DC46CFE6297D0A41BEE369CB3EB941FC2701104713A1784D6DE88948032AC78251328A8C58311848EA02008669B4BC811D6232A859756B306F24607DFD812DE89A0A6A00304FE795DD1C4A55366E615957855ABE11D97C7BE95A43A757948C5F4C046E9DF6A70674541B20010837307443444C27AD36C431F1F372EA659EB0693447FA5C40E8A38A7472D4A1BB0180433585B0CEF74650733CD90094199ABDFC1392E24C6D1C74408D00AE27F05D1729433A90AD9BD22F1B6C8155F68BAD1307811770A749A85F060E74625893462CCD946E2C1B79B6CB3CEF971899AB29F4328CE29313E4794C3463AEA46106D9CB82B5EC39D83D27C4CB3B69DAFA2E955D002E61C3E7BB247A76042FFEEE7E94666B893AB96697ADA5692E4E959DE6DB5C00F2B2353E615C5704ECCDE45D38404AEA8B2BDAF3FCB7F4FAD5FAA16EBA8A4BC94618FE14508C39F39A66BC59DD\",\n          \"c\": \"2467AFABEC5F378284AB6501C7322603DA732D11497FAF4C59B2E858222844D4780B1F7B0777EF4B7F61DF0253584BE5C46638535FB39072286DB984DD3DE335282458ACD297A585B64DC354858A8167AC4F4E1D00CDFDDE658A6D217C9C1255442C66B1B6F74EB0529A54A8B07290A9E07D2F74B18345757E21894639A8267830E6B065FCF746F8D3DFBBD23878B76F8B606B1227BDA4F221D2CA559BE133DDF9343811A5E5B3B0DFA27B4F9E24D86B7E959A9FD83392EC4B616C39AD9DB1D96D465A509F92647E4149A9D38381457A3A45A393BE987886FC7E8CDC561341383E35FD80680FE7F2D39DF791681D3C6A7C74031788C1D92F1D731F4261E5E385D9BD8D23D37B1AEBF2611707A6CF7C55418FEAF01577A2E26A248E02AB9F7FEA4A79CE55A2E4A8733AD8B3DE19639588DB04A5D8EAB7A1BF139C2BC0028E30988E3F2C1331B65AFC026FF68C08D3111B8E919A380A7CF4EE02DBB48CF552221B6C55C1C7EC9435A44316A6A8C35C8CE1F36EF657CFF16BC06A4F42CCDA96082CCCF0F903E5F1870B5BBA2EE4A1CD2EA06BF782421C8FCC73B43AFBA339D4EBB0FCA2958473FAE663B62DAE9805D1E7B469EF0F121A3528B4BD07556635EB0D3A83C7D3F776264F3667FE41C5EFF8B1861377E1671E2BD552202CA3F26A98BBF7E2453C1910F686A2EE82221ED50AC18A8538E02B7C70BEDB0E60B42D894B232073B8A222C055A4DA8ED707DF8F63471C7E8773DF9D0B3A4F0CA2861B2B8DD74AA3F216003672E5B132890628C7F279AE70A509E7A744C285F08ACB6BBE7F75D6B5200A5530188F93BE1D4416FF46A9BEE9E77FCB9079E11B264471C9F6FE2AC6927D3FD860A18931ED80D6AF7424FE3C93289D7787EA6334854DF131046E40C8ABD10FBFF2D4D4507352A619F5BFFE9EFF570C59D4DDEF3A1443EC91725F4488521E8941646B7E040DB141AB414E90E38E97F04F7BA3523DAB892494E5F2AF8B46E84761079AB191AFCE3571086A3EDBF02654791FBFDCF604762B84AE98B4E23A4F8CE9471A720C9C4E3AAB26CCD831361887DE84637B275EBC41BB4D98D53670603242297254B8E2C240B62A29EAB940640B2625FDBE2ED8511D0C4F2E04507177AAA81DA30C7FBFF30F7A4F03A2876E5C91722AFEF4DCF7929B3A1055645A7DE5F96CB13A81F7FABE6717E86773CD031390D10A36CB9E2A1FAECE1F318619FB2AFEA3C4C985630DE1E2651DA675A4930AF07A5B64162B28D16E700E3B389E5F142ABE21FF972B40F8EF6DF51411685F350BB6EDE53054E93ECCECA9384770B45E206ED7829D59E90A7345128ED2D4C8FD74075B240C47787F8D8AD3CB3BBBCC796328E701510BF5999EFFCB75FCB30B23D4B8F25D607901A6A942D263E5D1F4103914BEE27C4342F34591F923CB81D893AE4F4FF0B8B23076240C8B034F31D69622A6068C9428CFBE4996C1259EAFBA0DBF2AC43FB876D11A5A6F7944DE18DB2BE640F812ED6BF68A6CD09925D2EA64D46CF6BB5D3DBCF5BBD42D932FA017411DADE4857C279C70DFC12BB3BBC9AC09E21A7D7337E666EC9E823C0A5B03423F3398B53B877299DAD9097F986FB11C8E6E4C9673A9D10AC9FA80A23A88A5DF3BB7E722BA0439845EA58082534D0D7BD495C62EC40A86BB39EDDBF508DB899E3CD037CE1B08286127B3E62034635A98C1DA1E03CC60E0C5962B83CCDB942A33442F6BCBFDA4A5746CE94BA762ED1768C974281EF1736346708E4CA4D3EA7B2AD6462E807D12C33A8786D1BB3D384A6D5A6A739661A553ACE176044F5BE194A3C34F5BD5F23D4A2741D79C2799F25C486B51BDB1216CAF00FCFD1B2C1864807D347784E1E6C268C1A44D2D1603F544C7FAC0D278FA3386BE2E67896B39C965FED7A965B48E3B41F53EAC185352305DCE69DBE5F3C2EA28A6A87F1D5F1F3D5414CB78CF3195DF57B4C002601C0080C86F0E2D48DDA703E5F6234FD2B630A3FD65F2788ACECB31DE9E218686875A610C25EEC894E4CCB1D8FB645C97827DFEA89EDE57713BAD1F3CAA3D6EBC177F7F3B8FBDC61689952202B5AF485E2931EA07C89A8FEB0D344F216E82C4036BCA0FE766162C804EF25DB55D0D69617166D4DC23933956E13EEEB85695683F74D7CE5E162A1C5A0ABDA10D4C2E9531B4A898E6575179EB571CAE7D5848B065893E8FFA721DB4BC3C8F7E767DCC80C346CC014223\",\n          \"k\": \"77A12A7C2BCCD4700A35BF559EB1B8062628C5F7D540F2FB50A99516A11F639D\",\n          \"m\": \"FCB46FB66E388182DF6149F60DBD0FCA88D1BB1A9866A2C97B84848531230B48\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 68,\n          \"deferred\": false,\n          \"ek\": \"54B452960A93BBC12032E0507F7B83F112BA14CAC4A733700AF288D6DC0918C2B3D1625F475083E7967ED8A708B2B147D50658E1881DA6501D6AC6B7EF5AA4EAA5A7AB85844E3994E44A5431145A873834EB88A177400E95ABA99FCBA079914074209E06E17CED04636AB4ABD7824B98DB2A35215D5FEC091D5428B2928B809AA94C57694B2B65DE050817D6AD7326687E9A68695151F60A9B1502789AB8017760A6F3E7029B5074CBA929765B5C1DA8217C1105535699FC59195ED3A0E8799C66E3264F692C8DA198082860793446FD0536EA0301F5104AA689CC0263633F7A436621864099AF9C108D882043467B4888922D2254CFE8F937DCE040AC5B8641102959A90539F5177E21AF7AEBAEA2D9731AC87150A52068A71C183B8F4BE312A033719EA12E2CB8A197C1BA28B093E50437FF9779F0377BF5698557B9A45BBB7C8ED1AED0D982D71993F80CC5E7F16330D142BD0AC602F42DF89391156CCF1621AEAEB95701933A3CA82487E057FD186199BB5F4E2341D91A1D5B753B53E3719F65CBF0B3AE33153461459708B6B6E1131C9573C7B0CBCDF3A1C8ADA919D09C020FE6648221CE7AD35AB37C16010A3CAB1251B5930EC69C1728CA45E6639E97626A09469A6E204386E6A458D35E58ABCBE5C279BF79BA85021DEA6B6DE6E9A802198B969560F2C8C99328C5427207623C03E68181851401BDD615E2263761DAA3D0D16324FCAC9FF98E85ECA88FE668526501AF4C135F259277202E96D56F91C07D86692C91FC14187B99F1847EF11AB06CC019EA89AA88E114C034608C03A77280BC1B96307F81531009278193BBD1F5ADD812B278796309102CC2CAA9CF8C127472C4C63B47015CB852798379C73D0C4517E985ADC6F60E49053CB2B5BEDE0230A9E9A138C44178489EC5D5C2F069B737A143D6802454647C1B8C0837B43A89C879A0656CF087C733B7A1A7D153E092AAF193B6334A729B2B9F64D311090B29BDBBC5F1CA5E054461EEF72CAB14580B505BED3543ED15B02E33AD2A63B6EF980C407998AA5C357705B047D1848B4CB551845D3FE9A2BD491AD8E60308D880D6C56607F4AD24616845F092FCAC559CDCAA6FB16F2BB19475B700BC16C8FA8526055333EF389F6983C105E879FB4738166A1705BB625412B847553C462145A13175C60820783998150C40A2F673A3117FE1778A0A4A75F37BAC7D8A6C9690870D6874D2650182FC51C003698FD164BF5314FE3A4FA291BDD9EC3920F5A656000BA05A8F6CB020EE4701E8D8AB8BFC8DBA31887E65C7E403BFB036BCCF3C281ECABDAEC3984FB87D1C97C1F1112F8252C31A986941A50107D40EC6CC67E6E20F446C598832061BA21212E37943359B6B005621BB275235AAD581985168C468E15BF6C56C9AB65C7574B44FD83A32ABA5CA9831A6769135FA9916E3729361CF0D1142610C9A98363ED6642B853461C062C7D0B9B5F70694C8D4C2F073AE788A9CCA13446CD6BE40C82478E338C5DC34CCFB538451B6F94877A4B56B5AE6B3136B9BAFA1CCCC4902585907331C1F4811602716A5EAC209953B75A3B30EFD1B4E42F716B37A2F79E2C8ED062B1F79976C010EDFE4838929589A90B0A0ABB3E41CC8AA974C4C1308C4E12FE2D24162738A3811070D796701929C12E9A94FA96A823B6903498C4862732D48122D0265B0E571F0B4C43C62015F422753B2A562A80D68F19E2637581AD77592B67AC785B923AACD631A0B53D375E911797C0B76C79C53161015DFA7A67D1485C21A5B039A609F7386F8AC339B962472907D48F753314155C78A3DCF2010B1880C48196C0CD3B38B79BA9243CCB4AB5DF9D4569EC11F2F88525082B1C2224573C94FE3D24927B7AFF7E44B27755A0DA4C8053A7B8876AF4F809C22205B710B43C4D8B046CBB562B2BEED1C27CE736BDEA5317B658139509E4737C52A77186666947F7796D4261FAA8B9767814B7D096856A272BF6572B4998D2F6128D3FC6AA0FB2B66486A578B984C8835D1223D9E446AE6752D355A0FA09379F108ADD50683AD57AE731942C4F9C938464EB95C4D08264456F565E5981625FC75260496D05610CEBA5A751560059C38A8E2809609BAC2C853B819A781F61A7AF0C6FD728D9DB2C5623521D762440568272DD8831A4AA1B2F4CC8016BC53964CD7D3262D93710C56033B0F515A7B9E0AD3D6CBF0049DF4E55FD931257F\",\n          \"dk\": \"BD706C3D795690598DD0B25656791B6B6B40262B01474B55119796CFB2642E827DEB6C21F8E8A9E8B10C78156963C1A81D66580F061DF187A7C19984CE9ABA8C598EA109C7EC0173CBEB9DC3594F87772DF044AFD5312345F499ED502E5D42C5ED280BC35B1F9B79C86E00A9DF6422B5C8CFC455544EFC91542ABC594C7240747ADAF429A9B7C1F0F8BD7530B1ECD1CE98541EFA4032BA23CAA16B723524A5D9223CC1FC954F56B2BCA820E0E50F30696ABD11AC5359615A17815A155F7C27C84B0A85B649C8E3C75C8003A9541A77EDCB2FE00376EE25A0FEC314D6B3057D21335D52607504B278D5A05AE4C653458525393149401E1B2751FCF09831831A20899A115BB0D5585E64F42AEDE11A31271FFAD587C8B6117B346BDE545B88311349DCC89B39664939AF3925A289353FC5F48B37F547A6BB2CFD1A1B1A1C245D74078E0C8CE9172D4F853DCB35B5CAE79B26C607F25530B9500D1E645904E4047424309380C3332B0001B11A9CBA8BEF28161995411503610773A801492AB47C4AD32A08D9D621C08B553EC2AF4010CF0B8B6D894B86B4E8032C535FA39B0053218515C6B27E807C73EC2B966601FA976367005FA0183340534063D6CBDDD46287AA78205B5738D49426F0A5E87440FB01B70EBA2EF522743A65AB88736C1144093B76A1317849A5C9CAD9119FE6E5A8CBA27E0AD9B1AD6C3EE7238B386B7DB24872DBF09C6FB3485C58669BDC4264C5CBEF94200DDA8635B0AE23455F156135C9BA68DC675FECF3568D100BF022CA70F09AF609262B1636006644799038E5903D39688BEF9364E6482EBE347EFA71231CA1B08D0B2F83E5302F205AFF055DCBDC4B309933D5C737B35806DB19A3CC62A63AF56652833A67174196261A091918C933C21CAB913BE2965E760EAB34AEBB1B756F028A4DA7B0B6C20C43CC47BBE837A74AA5B42180B31333A2E6887C1616292C9C2855200006C34EC21C4BF3139DB74C1F06686BF88233B25B6C59A332372FB86A45568A120306428580BF72E082886C240A1980EB027F61560BCC1906DAEC1D48583B4AE85366840CBAD733E5AB2DC5044C9500A1ECD7B1060CB870A705F73618F0DB015A4BA33CB57ADC33A825FB3761F0758B7B7DBE5C4377020C8FF30750E53CA05C6F2E2704F4149267EAA355F2878A89C8E7394268437A807BB29A839FB2173D85102C8F06BC91B3679905200D581CFBE69C9BE25A53641D4619C12AB887E618B2798570C61108884C7AA22A3727A08EB5653F9E869678CA049C55389B9A235D27195DA868BC82C3A572B8CB878C96EC0FC0A07511923A23069C26CBAB2B879F40C081E55772E9A9A8BA826719185D627342A0411901C6396E0A086946A271B64750B32673266975A124ECEA8F3FE66115F543C9FA7D64064EFDFB368038918F5773344A2D01B288E80321EA67BC9FF7614A8BCF3C92A77040B316E66619A68D000CB60B543DC3953C3F92BD14516560833273542B3C1ACD85A49FAA4AB1C9474D8F203B8D49C66005364AEA0214444C779AC700F8878C15914D0A8CD2DB8C19F3943DFA17707C268381CC8BA02195B689B9362C5DB137BDE853A97976CAD043E7455A1CE2A86DA7701B884BFB937C6444150C68147DC0494B0B369D82181FDB90F8949AE3FC5283DB35504006F46C6EFCF08FF60330B720826722B945E3233A14CDD11AC888F3A561E767F6F48C2EC92DBC76489EB516C6226E063C08E6208228CC14940455E9C3700883BD6158074821CAA410A9AA6C3DB41A1E02367163E761260A8C1205B28429B53A6408DB144EEEE9BAC587521D725529A75C8EF83F2A4BC9F91258443CA265ABB54DA06114541F4B1B385D677C33B473BC0045A64078F84C977A3BC73DB8B30643B3CEBB073F0B2C0CCC8E7CB98F94242F50598447A84CDA593396490CF74071256B0F5EFC0A2EE9732E611F07487A839261A0677219838BEDFC5CC932601B500340777F0C7A92AC18596438C04EA82783EC3F119B8BCA09A4BE77748E9C83FEFC3B582110415C6AB5C76EC830929185172B325D9439CD96AB463D3021435306AC72BF476A70BF96A486537BBB87467B0329E855B5B1E7CDCD405EAA3C63E179277A959E5858AAC275964CF9569FB22F18C5C175930406B9CB89085D9230309030A554B452960A93BBC12032E0507F7B83F112BA14CAC4A733700AF288D6DC0918C2B3D1625F475083E7967ED8A708B2B147D50658E1881DA6501D6AC6B7EF5AA4EAA5A7AB85844E3994E44A5431145A873834EB88A177400E95ABA99FCBA079914074209E06E17CED04636AB4ABD7824B98DB2A35215D5FEC091D5428B2928B809AA94C57694B2B65DE050817D6AD7326687E9A68695151F60A9B1502789AB8017760A6F3E7029B5074CBA929765B5C1DA8217C1105535699FC59195ED3A0E8799C66E3264F692C8DA198082860793446FD0536EA0301F5104AA689CC0263633F7A436621864099AF9C108D882043467B4888922D2254CFE8F937DCE040AC5B8641102959A90539F5177E21AF7AEBAEA2D9731AC87150A52068A71C183B8F4BE312A033719EA12E2CB8A197C1BA28B093E50437FF9779F0377BF5698557B9A45BBB7C8ED1AED0D982D71993F80CC5E7F16330D142BD0AC602F42DF89391156CCF1621AEAEB95701933A3CA82487E057FD186199BB5F4E2341D91A1D5B753B53E3719F65CBF0B3AE33153461459708B6B6E1131C9573C7B0CBCDF3A1C8ADA919D09C020FE6648221CE7AD35AB37C16010A3CAB1251B5930EC69C1728CA45E6639E97626A09469A6E204386E6A458D35E58ABCBE5C279BF79BA85021DEA6B6DE6E9A802198B969560F2C8C99328C5427207623C03E68181851401BDD615E2263761DAA3D0D16324FCAC9FF98E85ECA88FE668526501AF4C135F259277202E96D56F91C07D86692C91FC14187B99F1847EF11AB06CC019EA89AA88E114C034608C03A77280BC1B96307F81531009278193BBD1F5ADD812B278796309102CC2CAA9CF8C127472C4C63B47015CB852798379C73D0C4517E985ADC6F60E49053CB2B5BEDE0230A9E9A138C44178489EC5D5C2F069B737A143D6802454647C1B8C0837B43A89C879A0656CF087C733B7A1A7D153E092AAF193B6334A729B2B9F64D311090B29BDBBC5F1CA5E054461EEF72CAB14580B505BED3543ED15B02E33AD2A63B6EF980C407998AA5C357705B047D1848B4CB551845D3FE9A2BD491AD8E60308D880D6C56607F4AD24616845F092FCAC559CDCAA6FB16F2BB19475B700BC16C8FA8526055333EF389F6983C105E879FB4738166A1705BB625412B847553C462145A13175C60820783998150C40A2F673A3117FE1778A0A4A75F37BAC7D8A6C9690870D6874D2650182FC51C003698FD164BF5314FE3A4FA291BDD9EC3920F5A656000BA05A8F6CB020EE4701E8D8AB8BFC8DBA31887E65C7E403BFB036BCCF3C281ECABDAEC3984FB87D1C97C1F1112F8252C31A986941A50107D40EC6CC67E6E20F446C598832061BA21212E37943359B6B005621BB275235AAD581985168C468E15BF6C56C9AB65C7574B44FD83A32ABA5CA9831A6769135FA9916E3729361CF0D1142610C9A98363ED6642B853461C062C7D0B9B5F70694C8D4C2F073AE788A9CCA13446CD6BE40C82478E338C5DC34CCFB538451B6F94877A4B56B5AE6B3136B9BAFA1CCCC4902585907331C1F4811602716A5EAC209953B75A3B30EFD1B4E42F716B37A2F79E2C8ED062B1F79976C010EDFE4838929589A90B0A0ABB3E41CC8AA974C4C1308C4E12FE2D24162738A3811070D796701929C12E9A94FA96A823B6903498C4862732D48122D0265B0E571F0B4C43C62015F422753B2A562A80D68F19E2637581AD77592B67AC785B923AACD631A0B53D375E911797C0B76C79C53161015DFA7A67D1485C21A5B039A609F7386F8AC339B962472907D48F753314155C78A3DCF2010B1880C48196C0CD3B38B79BA9243CCB4AB5DF9D4569EC11F2F88525082B1C2224573C94FE3D24927B7AFF7E44B27755A0DA4C8053A7B8876AF4F809C22205B710B43C4D8B046CBB562B2BEED1C27CE736BDEA5317B658139509E4737C52A77186666947F7796D4261FAA8B9767814B7D096856A272BF6572B4998D2F6128D3FC6AA0FB2B66486A578B984C8835D1223D9E446AE6752D355A0FA09379F108ADD50683AD57AE731942C4F9C938464EB95C4D08264456F565E5981625FC75260496D05610CEBA5A751560059C38A8E2809609BAC2C853B819A781F61A7AF0C6FD728D9DB2C5623521D762440568272DD8831A4AA1B2F4CC8016BC53964CD7D3262D93710C56033B0F515A7B9E0AD3D6CBF0049DF4E55FD931257F068034A9F16D0024CC9BB412EA9C778DE819A4CB27EBE5614C8994C9F2DDE25A96672A036E27BA0C2A7ECD385E3F4381DEAF2BB1EC0F30A8AC7B01F0A15A0716\",\n          \"c\": \"F0E6EFFC0E4FFDDEF79E12EAD79C5CCA3416B4F5E251DBE96E14A1E5865FD41C3A45900AE13534BAE5D95CDF84A02CB66143A44B86ED2E535FA1F0787F0C3E3B9736CC02D88A169D698C3ECCACCC4E2569F06A470CE0D012CF2920F9F04BF96C80CFCB0686B0CF93F01F28DD66ACD772A68B978DA2BD77E9C65178FE8FFE23509E440030ABA284F4C94DFFDDB2393FF8EA554AB99D568ADE6DD18B3240EB792F004216F3B528A4BF3B6DBDDE1F19F51498AB876CF9492D93CFCD060BA476E91B12845FE7BB290E42841B7FDF4056765A6460FDF4047B8DA73269511D748A27EFB971A9AE4233555EFC77F826F0C0BD3E3AC9BD2ACA40C7CEE537BFB288CFE14788366C04D9B2460775774268EFA0C3C73B5B60675750D65362D97C43341B6B559EFD27AADE3CD256BF89487DFB9BD467E3E3CDB7C06539CD20F314EB2E0582C1C46FAA5E8E6918EE28B2DF21AE24397DDB1FF70AAC4B7C861876B2A9034DCABC05CC4768494C77F3436D7F2D6CA8EA7CDDCA32D11BC22DC59049873EFB2A113AC61610C8833665ECCF7AD30936673371B0F9B561833AB5B76B2BA0FE402394C57692F900F01842BE61C45AFEDFB88194626E533BED9C9EE00CAAC2400FE98B66A9EEAC83CB7A337CC7731E90FFF9AF002B92DDAAB612A657F62B422C233CB019939EA79C434F71809DCFF197149C08D76EEC52EE00CE5AEAACC68ACF75373F20EF63F6195B1D550836E847F14FBBD48ABE5BC70698C46FC8C05856398686E249FA189B11423F4663092C74AA8CCD080FB100FB89060AF9813CE8E1EEFDEA21EA1A849DFA066200498F99BD3FBF4D57BBC992C5C8BB0918DB64B59DED4F5DFAF5DE70B9491E33AAEEFAEAC4E56F9D038CF2CE7A706A290E4CF9996387C789E0C133D5E1ECDDE5452548FAB8243BD5344EB3EDEF93465A53693EFCC664E0A845135C35E8702FC692CF64A7F8DBE02397C514B9918AB946F83C557CC85E70B258767E0FFAB91D4A1377AA7FFCAA5E1A43D07D1AD9649368F0A40E0E0EFDB16F68042DEFE20A75CDDE570B54895A03BAC59789BFA49633DA8E1B50552904650727C0799AE441E1499C6FDBA71FC0D362535EF708C8B9268D2C9962E777AB94F6A6390EEA9E296D46CE376EB295FE0B1E8E8FBC1B9ED154E32BA81F83EEB4BED921102E284D671961FE849E7EE7DB54B4972A6BD65022B8F5C3324AED5B177D6699D64C0D12462E35B0A00D66655DCEF462C7EC070E8D402B33AC617D8276CE022FB9550A6E4BB320DF06E903E7F5081CA65D644997454CD2EE845DB85746E91B9CEEF823D60665B930285B9D13B8120474F1E0C25D4ADAA3C9DD1AA33B34DA066C8527CCCB844284D4DE9A8F7E01CA702802C49FDE099442BD010224F83BA5FE71CA01EA4CBCAF4B01AA4E128B86E8D1478A4E06D8A107FB36C5FD6A73CD92E3CBE4FF1C18E972A552E9E733536E97F6B609336E3BFEACB0CF89D7D110D4A422500BA6A80EB169FECB4AF0C3AFC9B8BEF2EB150BB23B362FDB5E097CC75A17E25673DB42E434D7C0D331F8469456BF01F5A40DF00A7B07D955A8F47241B9359C8444DA41764C6F6B90D1BCCA4BA61FE386E63F4B51FF2A6E4242620463FD4011EEE199F748A572284F817521CB59B7EC568F9EE259257E77ED1B3A209B3B9E0C92FC96BEA77B0494D1004F84D299DF67A8E37668C98672A3F896A82D7C4D05A8E78EC412912085B3A63511FB1275EE70A28ACA0B879D43100DF07D43E9AE5D68224BD7A29658C1342F0D5C920B4914CEE39B0CEB3FC10E5E2D76789795D1C136E82E94E7951B370C3E3C41A8811C789D74DE1F888AE9C36AF369596F9FFB654A707B5416B2BAB90D52A4D4958CA9F389D1C46E4574848D2BD9132DCEB5A1B71BCDB5AECD627333358B0F311F6A5356161D6E78F6AEA499FF4378FDFEBF2105210D3E48FE5DC992362A9347A02CF032A568B6337BCC71C40A4AB4D64860BE0CE0E336E97E18063AEA8133C77B2B53BF9E9DF7CBDD146821E0CD1A1BEA0C73BD25D30DBFC994FFE911EC1CEC3524DBF480E9908439E00DEE4E3765DDF0C5459858E4EB68A0570B2EB6F8C1D90DA25215B10F2F5AC9053C243019524B82CA9AB0E184BCBACB131622582BEE592837EDB94FEE88B4AF8668714D8A7AA30C47A90CC0EFB1C421000FA6D2BA54741B238193E86CBFE4F7956B00F2A857417F95C50D69C0EB\",\n          \"k\": \"80376EC749550B531BC5CB538F78FAB38342C7DCD74B83FD83CD058227B9B3BD\",\n          \"m\": \"4CED177C0A454052BCBD682B39BEA31D0D219A73184BC00C100964C25BD106D3\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 69,\n          \"deferred\": false,\n          \"ek\": \"C701BA88063589288E73E4BE1AB28C6E3ABB70AAA03F656AE87B38A9EC947786A41D722EC63C0E9E8C1DDA078E28608D11E84219E35239D8030C212F34CC284EEC748D34C6CAB04A7F3C15A5DBA8C74C8666167CA4508EB47774A7C3BCD6589306FC6365D74018D2082DE1753A854EC6FA7813431D31C6A3FA711E03F042E6251E2E27787BEC65AA8CA47FEB5662599083E6944F442716342DF25AA551C36643EC3B9598184431405C8C8EB4AC8360E1789A06B2D13232E831C39A005881273B0086591760AE8A20913D942314F45BA5094AAAA05444469D12C20566B0C008CB2C5FB90257929DCB648E0D616001095309E00FE19235DD29B1EFC2A80E1402E851CD19D2BE9B4001ED214E418C156BB6BB85C0744C3B81AFEA59BBF055C8046386B576BDD009CB412D13B3918B19A658C690726BC93D4928F1B2A7C386320D41BE98C1AECFB75288B94511113B902489FFF157DEF97D762A5302B89AD18546F5C9262C06C98F62632681859D132F756A7008B76CD92C760BB12EB276BC5611B5A1F20A22982AB7616F8802136F54815D66BDF87859C7A79F29769AB4CA735181557AEB9D76468D8855302D4A5E9CC171DC6000C7363789C3A76E59905EF5686C31C765541131E62120399B16CAB27A4677C8E36150E728CE747D2C2B305CB780250CBB1282044B5443DB128695F4A6637102ED3469E3548592D3BE83FB30FDCAB034C1499F69ABDD174E82041FA4CB9B25773BAFCB071BAC2394B6B9E3D5446289A8E08339D91BC12E84ABEE2BA51A563E1DD154E075CC33409609835A8345C86EF96B31F28E3B42ACEAB464380413F475B00CA904E958CB260B186E3C5CFC6068914638AE831D8AA78BEE7B65B9233F8950783973C4971AB0C3BC9F7A7A5176083B4B9B1C045157738C460F77A6D9311A3591789DB0BDE85C3A863594DE182D789A7244E05FA05965A4294FDEB2AF843CAB9F273687E3487C4756B0D441303C9C8B6284733652F074B087288F6BB08AEFA990517C08E9E984A4029FA1C420FCB19FAA78B61C35B4DBA021889826108A1A95A6816DCAA08D535C77959C7EB20209307996592E94DA842A52C329E3625CECC6A8624EDDF7B77C2264933B7B263499AD0930F539C0F24A14A157A56AA469891403BEA0A3531CCFF5A217282652B027513C05D03FE142DD14BD85755507473AC91767C69CBDDDA71CAF8A12A611C0F7F50CEBE87BE9D88514611A9516A6433B8FCBA54527FA10532A808E57979E73C8EB444DB3404D4888370A419E1AB04760617AA0E66BB2E715FB17036D300D9F3480D691450BF0571F875EDFA668D68B16D426668A68BE91F1337AD15577375477045633984AEE80824A08C486F25443E61B47658D03A21A51B085C656664B8345C1149CF690A9B99543B9E98CD090CC4B57C5A1D7BDE6E256E2059B20752E44331486515083DA5A99697A47A342BCD9679C77C6A243896B05CDED231919D0448994ACEC094E082A7434C9A3F6B97F21056CD827AB2648613B3B95CA065E1B3CC3740BC7F4F156650A722FF26D70E5276F53267CCCA11513A719624A2F1BB99677B845A436C9AA3A2D1202F1094F9040813B26BA6DA05E904206DC33A665959228B62BD9391133AB21B79967AF31CA0C727B464A8737535973042831FB06A0B89CA65B6DF8F3983037957DC838CFE0C65D7791D3148A3FB59656472BFBBC8AFC7C6EBBCB6BD83B6760643425FA72FE343DEE61187A73C98B9000E2577990F9109A8B1DAF30BF384227D45BB8BD4CABFA4A9CB20B0B81152BB46281A68445F7F78AA56545B59C75E9852912870373B1CCBB14396EDC70C2C00896762A129B775B4B9168C97F9A259FC22219DDB0CAD614C4F06030140A1F335B033AA2A1753CA5E2A2ACBBA83A3CD65978686FDB8959B4F39CF41B98EFD2048241B69B27AFBD142FD5046AF69AA3EBF67ED1632C4683B88C6BB7B388B4A4D7178A8ABEDF1523A6816F10C40065056DC9393D21F76A48EB7C122331900406D960A4D46C9EF12429B8E4C45C4032F089458818C759584947F6415D1468A6436E764CC86E725A936000B242155CA0A9B884B69490CBD223C76FBA954319161748706CE5A31FB067C20663F443B885E4C5C1A421D589777AA4C13555432AF00B27571D8217B09AD4BC9732C8ED10BB8841315539DA5DD99F9A7FACC71557853FA10547CF7B89E98345\",\n          \"dk\": \"5C9509C7986A2EB53A2BA32392CB6D1C103793B5A8D2938E635B3BC1124B730B63B4A061DAABCD2257C733AA010357376D631178D231E30037625BC6CC9192E6678D815353E6E10519586385F6224526A3662687284AC3734C7A4B5C16FA2B81D9568CDAC1CCE8A01F9BA991D2C2693A2569A9C47E378C591CBA78870449A977C11E8B8117731F815ACA82365D709C2C5E532CB6FAA33D9C0D36551B07C8172AD48D424C818F47C1CEE1B0AC0C80565A2D9D397129557DD2C0372A898A79F5ABAF4B07400C359CDC7A67844AA7530F3ED4A8E9AA9FECE383B37A365FD6A6A980196C4B3AC7A6B511671769A9888267514E01A45A8125264781C9C1B7A7F4143359217A455A6A2A00E1C38FCB174BBBD1729270661752BF56B53AB9D1766D3CCB1F6A537FE37E6C30329FC1C5B2E62BBAE28AF9310E5F773FCE940FBE9C6A0FA302126A98BC87BA830B77B81154BC525FDE8209585B95E810384BEBB6156A6A77771F74392F2592557F4C8556D8689C2B56EDE29E0821304D9255E063C7B972C6DACC860930915411CBC1EA62E11B45072091E7F24333613698F842D5374513A69966A61842509ACCA0445C51366F7AC7CEB0B6681116C8F4BA4AAA256E8C6909CC596F167F9FB1AEB7533DC1F2149EB14A8F6531D3F36CC6A339FD3A711B94067BC4BBA63100385396192216741C8AE5FB955B7B4E80A23049603B7EA2CA0BB2A79800CF533B50F133B345C2344061AF6EDCA4614299701040C31128D99C1B851497A9BC843F537F7D31A16B65490EB1158EC29E4F88B722D76AC1F58C05F3B4E0F6CD9FD69FA86898727AA9E799957BF4B4893A67B624C285E2A99378823AD60AF5D2287BC703A0E377F84539A19C1B13BB5B6199A355728F6B313931B566FC54C592410E81A7506E60BDF4525BCE9C73CDF7C58DAA7880D8AD628A51450606FD0325F0CA3D75CCBCAB41AD7D80391147A57201D0A9FA06DBD13870E4051B297753A8075C3B2C5404C9DA6A20FB12CF45AA745C05A219D21DEE1793AFE144221BB2FFDCABA47CB6BB4B0F3C25AFA5938F5FB566A28601D6783488E2671DF06115C125C0D93AC13A8CA4ACA2404727F6D13194C563B602668D886FC45B3AA8FA357A19BBC06CBAA109B9A47B01D814CA8CCB2815C8AD8F466A936C0659234888B8C55F71C75792896CD9C8E8641F548225B7C7AD388A5CDFF705DF37A1F2901749C48791E62CDC5076E394BA15B26AEF1A38434564088B0D741C11970931CB8C75EEE092A71571F4409F76192C64971F8F395F69DA9F3582A8E9B64B8629743E3B94117B0DA2EBB7BE522697663E9BE12515659AB8811539D22933B7BDA0400883982B430430B1E261A25734D0D990BA819F2C59584FD29EF2FC10F95B05E7C7A73B66C4F6984153C45030F99A1E9A378028A17961465C1C3146A0AE7096300CF966D4320356C3CC1079A53DF829DEA5A711A683AF708968C4811A8725A0397C332B0344537F4E2391AB9BA656075251C216435894DFD97F8F0B63E3E5B48DFA8852C43A30D67475E64F32B9952DC6732E165D1D854A00B72B7D8A44AF57621C7B7FB6B17D8ED597CF825242778A4F5A03AB305EAB2BB9ED27C504816A05C2B2B13A9E41C233AF420FE7BC6E7202B850A24C861C1B0196B8B4C87F2986063C04CB90F56CD3207B93229E197150FD8BAB314C8D71892FA633001F5469F3AAAF694B051F7A1362C3C6D3485077A12DB0052907C493BC6109CE627CF2F953AAD71F654A9167B05D708789EC0100DDD4421237C6814073A5B10E3D1001D18B69A57815D203ADDA9610A7232EEC6C33763B86C4F777F2D7392CCCCF8D6C4297C1111020B11C8258D9B61B26D50E3142A94A6B50C0AB51607B43F6147DC6D45BD1E64A1AA25F2309BA2C525489A75BDF5848D80B6F3087735D55246D9723D3B7CCA2848536F7CECFE6B0FD031B0AD538EBF091B4F674AC53BE78F1A00E2AAE18B537CFDA0CDE143ED8D7217763C6CA2C052775298FD1662913859A1276DD1B2305D3469EE39164D568C1904D89B66CA4366FA3907CCE699D9AA12F36719BC4CA47800B765D392D0AEC702F452EE411271BF945FF58B5E68C7197AA4882F8A5C7C4C11C4A1BB3AC39205775298A9A1A4B677322413187CAFE99C178AC181F0446F9875FC701BA88063589288E73E4BE1AB28C6E3ABB70AAA03F656AE87B38A9EC947786A41D722EC63C0E9E8C1DDA078E28608D11E84219E35239D8030C212F34CC284EEC748D34C6CAB04A7F3C15A5DBA8C74C8666167CA4508EB47774A7C3BCD6589306FC6365D74018D2082DE1753A854EC6FA7813431D31C6A3FA711E03F042E6251E2E27787BEC65AA8CA47FEB5662599083E6944F442716342DF25AA551C36643EC3B9598184431405C8C8EB4AC8360E1789A06B2D13232E831C39A005881273B0086591760AE8A20913D942314F45BA5094AAAA05444469D12C20566B0C008CB2C5FB90257929DCB648E0D616001095309E00FE19235DD29B1EFC2A80E1402E851CD19D2BE9B4001ED214E418C156BB6BB85C0744C3B81AFEA59BBF055C8046386B576BDD009CB412D13B3918B19A658C690726BC93D4928F1B2A7C386320D41BE98C1AECFB75288B94511113B902489FFF157DEF97D762A5302B89AD18546F5C9262C06C98F62632681859D132F756A7008B76CD92C760BB12EB276BC5611B5A1F20A22982AB7616F8802136F54815D66BDF87859C7A79F29769AB4CA735181557AEB9D76468D8855302D4A5E9CC171DC6000C7363789C3A76E59905EF5686C31C765541131E62120399B16CAB27A4677C8E36150E728CE747D2C2B305CB780250CBB1282044B5443DB128695F4A6637102ED3469E3548592D3BE83FB30FDCAB034C1499F69ABDD174E82041FA4CB9B25773BAFCB071BAC2394B6B9E3D5446289A8E08339D91BC12E84ABEE2BA51A563E1DD154E075CC33409609835A8345C86EF96B31F28E3B42ACEAB464380413F475B00CA904E958CB260B186E3C5CFC6068914638AE831D8AA78BEE7B65B9233F8950783973C4971AB0C3BC9F7A7A5176083B4B9B1C045157738C460F77A6D9311A3591789DB0BDE85C3A863594DE182D789A7244E05FA05965A4294FDEB2AF843CAB9F273687E3487C4756B0D441303C9C8B6284733652F074B087288F6BB08AEFA990517C08E9E984A4029FA1C420FCB19FAA78B61C35B4DBA021889826108A1A95A6816DCAA08D535C77959C7EB20209307996592E94DA842A52C329E3625CECC6A8624EDDF7B77C2264933B7B263499AD0930F539C0F24A14A157A56AA469891403BEA0A3531CCFF5A217282652B027513C05D03FE142DD14BD85755507473AC91767C69CBDDDA71CAF8A12A611C0F7F50CEBE87BE9D88514611A9516A6433B8FCBA54527FA10532A808E57979E73C8EB444DB3404D4888370A419E1AB04760617AA0E66BB2E715FB17036D300D9F3480D691450BF0571F875EDFA668D68B16D426668A68BE91F1337AD15577375477045633984AEE80824A08C486F25443E61B47658D03A21A51B085C656664B8345C1149CF690A9B99543B9E98CD090CC4B57C5A1D7BDE6E256E2059B20752E44331486515083DA5A99697A47A342BCD9679C77C6A243896B05CDED231919D0448994ACEC094E082A7434C9A3F6B97F21056CD827AB2648613B3B95CA065E1B3CC3740BC7F4F156650A722FF26D70E5276F53267CCCA11513A719624A2F1BB99677B845A436C9AA3A2D1202F1094F9040813B26BA6DA05E904206DC33A665959228B62BD9391133AB21B79967AF31CA0C727B464A8737535973042831FB06A0B89CA65B6DF8F3983037957DC838CFE0C65D7791D3148A3FB59656472BFBBC8AFC7C6EBBCB6BD83B6760643425FA72FE343DEE61187A73C98B9000E2577990F9109A8B1DAF30BF384227D45BB8BD4CABFA4A9CB20B0B81152BB46281A68445F7F78AA56545B59C75E9852912870373B1CCBB14396EDC70C2C00896762A129B775B4B9168C97F9A259FC22219DDB0CAD614C4F06030140A1F335B033AA2A1753CA5E2A2ACBBA83A3CD65978686FDB8959B4F39CF41B98EFD2048241B69B27AFBD142FD5046AF69AA3EBF67ED1632C4683B88C6BB7B388B4A4D7178A8ABEDF1523A6816F10C40065056DC9393D21F76A48EB7C122331900406D960A4D46C9EF12429B8E4C45C4032F089458818C759584947F6415D1468A6436E764CC86E725A936000B242155CA0A9B884B69490CBD223C76FBA954319161748706CE5A31FB067C20663F443B885E4C5C1A421D589777AA4C13555432AF00B27571D8217B09AD4BC9732C8ED10BB8841315539DA5DD99F9A7FACC71557853FA10547CF7B89E983458E7830EA58B9A79ECA86EC2D5F5589D9A7F30FC06A0E33AFC44CE2717FA011A531E55E9C652B7C9456926E3A720B75ED2D4028057F31ED51E22D1C75FC29DB2E\",\n          \"c\": \"34BCAF3DBA6667162E71A484F74C056A37DB223C1F9FE03CC4246BD9B1542C6AAA6B8C21FBA518633B8824D3ECDFEE9F5981C4E75F0CCEF4E957EDC63BD1A49E5D599A01C5B60D2391D280CEA34637692B80083AF030424DAA91D95A2E10D372B827A0A7214CC74CCD91B9EA4E85D4919CEE6BD08BDC8303317157E3D95D0A94F486F595E64D246EF015A3E2780854B09C9E1C077FAF641DE76218986FE7CF6C8C94C37252C5315C1B1CC9434F286789FD159A6993FB75F3936D4C04602C1EEF033F4E95E412EE772DEBBF4872600981D4B45749AFE498763D11541177031232D4F14143B6053ACEC654F2C9906896E79DC5A5AB57402923BDFDCED57FA49E8CA155EF37012F78B5353484C006980D90DA581410857C152F2E1DEA213B8C28A6DDDED12A782E23858F204CC1BDC84BED3C05F93EF03911342CA7AA280EE850749EDC1A3F5F998505014A824B63BC67D68BCAACFB6511D4BF2EB1ABA077899912540D78426AF13CC0888BCC7807E932445E30F72DABD6E35B8C04D454B4FCEF1E6BD17CD53EF04B363942C1361959AA7305D5F68844C3271146DADF8C588B470217CC10778FEA4AE93A5C5D5B5925AFA212E25A4CC34A8CE84D56DC476297B512EC89BD7DC67FC109B829BF101648D9B7F6494817AFDED31D85F68E5E98F717A2F0D1B6554066DF76713ACB17A1520127223E5E4B59B030EE714C1A9D3A7C4D08B928DCCFF1B53BA776B250DF9CFE6CE299F2AF835D78E62BD3BEC8FCE068858AB1D31C4F371291D54EE4FD87E6936299DB9051884155AA8F7C9A9EAB177E33787FBBA8C5026CE8FE935A5A7944BC6E48352E61314888F7A7EC5F08894829A88A4C955FB4E8D8DDFE4193CCA985A4FE6F6E16815BC524B0CDA186CECC4B6CA23A11330E9E2B9982EB6D3E24B36FDA826633E4D36318B8BDFE6628ADD238BEB63325EFA3F27F93BE9287E1AC9E3FDB6A5B39729961173E65D21F5A51373CC63F24F710064A177290613A172FAD51D607BA075805976F65331F5197307A30F12DBA0824DAE229394757EB5593E1AD31D98E45E7E6B864F7FC1ED00F85891C65C91C9E76A1E5B336D8E3AEA8DB52AF3A36ACD0E64EB877D19887E0B803DCCA831E58F6194F0FC651342A49860803240BC32FB8751FB3F513F43352DBCEBE6DAA56A3DC457F705BEE993BB325A9CDAF929C513BBD9194F4379BF8350F8AD81BC11D3CEFD110AD315E6402F6D01AC5B2B8DA5D0E78832AAE47D1836400B681229A363E184BCD5E45BC8A3F1F3C38FBF7D84C493BC712E1BFC80CDE8C15E53DE6EF27F10D68500EE9D3709E059B8A6BA91589B5F8DBD6E62E127C36BF3C97007E2E7E1A97E45C1ADC60D1B97F8348CB5D88C3592C375B4AEE18648DC8648CE3305E2D055ED01C3830D9E329C76E13E4616FFDCD6773B31BC5502BF35C5BC81193FF0873A77644CAE2AEDAD246925F4CFDF567F6853D45BF1485E8DA3DDB049F39A0CAFC67EBE2D155F41B9B938B0B22B082FB6D2336CAE45E17BBDCDA5E87AE0CF5C8E80F2757A40005DB5071475A183A200EE0563AA483B29DBC58F49C3C322F999E00D185204E448A838E9C63C85777D76B1057F08D552112B636A4E0F0EDB123997B062F1DD1CC79F8ED186A2C6F4DC8B2F1F1A2BD490623CDC7DC7AFBB5CD23708BB45F52C716E5490EDD2BADFE24B77F138C466A6DBA515485FAADBCE9413B6CDE0E7D997B9BEA49023867323B57FB8BF795F272883FB3F3A0D21BF74229158ABE822F1018B5AFDB650CF012D2251F0A4E5BC1974ADE394D77E0E4116B2A11ACA9618B000EFBCEFEB7D4FDE9738C75A05E14461B6064FD5399D260EBCB7B89D2C0D1589C1324259C37ED7C2224A6CF7350849B401D2A6AC909AE433DE8C7ABF0199ED44ED2B569221815455088611AA6C88FCAE75DE832E390CBB9CA075F07C31FE72B9C7CA88EBB6D446B60644CE9BC62DDA18F5C2C95EE2BD4955EF8CB50FA4157396CB156314110B531BC209EF90AC16608A2E080618BBE9BCE24A3900131EE334CC106AF9A1C41E9DC33151EEE2E980A45E7EAA649369585C9FC52CC614C8226F8AB2768285C320BE1FB6E18CC66CD160C66AA2098A3BA33F036FE1C743274F5D9029A8C8F0BEF6834A9F4BA6C0E26DB903DED5519F566B02FD32CDA1624BDE365191738C9C98CB0687CB138DC3F8833948A2090F0F55FA68F186D711D26F1\",\n          \"k\": \"E941F064338BCC6AC1F7679881709DDEBD2A94AAF087EA9FB5021838DACC8E72\",\n          \"m\": \"F594FE1E810814496BC73A1523FA1E0FF207AD5F5F0FD4B232C25EB9F6EB5B1C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 70,\n          \"deferred\": false,\n          \"ek\": \"24155F435909068A815A6853C9F17BD928B0B7891B83F4447C2C5213760678D74FDA8C793C071002040EF4D81BCF676483B85D2AC796F964C9B4F8B7EF77656FA2976BFA65B9E6AFB7811903E114D3060A1A967A1465C512D2C1513863A548A787279E81674FFD45BCBD7C398D567F22D7291F50324D45C9A2FB26DE3A7A7CB875058979FCF4BF19A03A8A721F17E514773A83FA0A939393ADE55CC7176AC984CB43C7092DC2D0A55DEA11F3E41B879B5B52984BE72158B08BC0EDF25866563A56D4B98DF9528FFB28BF345C4D136EA84791DAE11ED324AB279C6C8983AF725818829CCE05770AC3837A2C92AE2B1812D143032C72A32C94C2A06882ECE2CB73C0C41EDA64B607BDBFD9258E03A013B9CB3BBC5C40FB10CE413B11897E81D32585240851E487877C4D5274C6CCD45CDE88978994CF463111A20BBA9905A6114899BDBAAB1053541BB3502D9779DCB48FC8E523FA260F8582B6B8F30232342848492DAB73197EC492407ACDB52844033C9ACBC79C366A4583D98585942B05281328659506D714FF856A9C2B5F370B5136F896BCB87A23331AD0A7610FB69515E84A0A302407FB9949ECCFD91550A8618049A48BA2F4096567A0D383BDEDC705645C4025E0B139C22E6A16023062901B8A2B6B515DC8E06A4D5514489229D926521942BE150B96F2486CB34B09D7E7237A440588C357CE061DFDE1362DF1C2E58C1E1D72235E922B61EB2163D906B5E051BC58497C7CAAF5363724985D0FB3BAE964A86AB36EC46AA3D7AA290B428744623A3DF900DE975E3AFCCCA539837BD897F12CC555D684E34BBB3CD7041FCB2004A687B5E6A3E801CFE9C8C2815C2AC4783042A23736137447337D9221116A62C0E1D8C191B53E22C92A71A15D9D22505FA5BE17221A7236392C0348562216BCB037D4880A6D1236D3626E1B354142B5C897C646BD62CE000C27C53991A1C69176D03A16A61FCA51067C596CF01B89933A258B43C22F489AC70C1D8658B2C0FB2D3A6924A704302F33C25BDB556E97780747CF652C80E87B23479AB2C8119DFCEC47265A4F7D524E50294C826AC4C7B39C3C0782763A1A6ABABB9E14903720A9380939FDC9A99C6766C9D8B2828881BAFA555A35022A23A6F57CCA446319E71780AFB3767E9311D5C990F1E4ABDFF51A0E7238AA98AC412C05356583B05C170BF793CE770F79F81E99F78FD46310EEE27F614226F42367319C81C98A1FE2B2C09F886ED827610C8621B079A5A51863BB69ABC01C4A75051B50D83D1AF85B89F6918313AE5AB0999035088813102BEA06CBCB14656928DCD53A30255B4931017BBC4682CBA54378CB7E118A275728B83A4BD2586D271A8F64C06562248CCAD9A1FFB8265512712D0B707424B015157788318C7086BD964490A11A35FA43BED6ECC533E07D2A6ACC806990072A7DC0322573D6B88E29A7B47CC42508C7CCE64AD436AAE0160309E6922B655FD3B96FEC22CFC7349A746A0012D60A88C17D7934246EA0290369B3F7783FB177534F300BBED06EDCA87786D5175B93239B115254D676C921C1E5C3BA42737AD51B4BA30B1E65A8B59EB6A45C34950E9AABB7E5CD2F556485036BEF93CEAFBA97E83408D61C6FEEB908873B7FCBE93D7E386F2563CB4A80A671A1155762A02968622CE9163284777A3B4AF4B8858392BDEB9446BF59CEDEB689C609859C9B47561A8B39A19F7B36896DE82AF89BC41CE6C54791A78D6258534AB7090C502CE0A6F70CB3DB442A9DCA58E766A2E72286B42BB431B4740B125C73D43F49C1994761A57E3291CE95A2F4B88B3DA31CA994685A75B71CC622B3C2BEF60C222696C031789EFA1A3DBDA576EC1B802C261D1F44A5E4B973AB73946E41159D622BD8A0833396517DD2C51904AB67F05BF5FCBF9B47BF05A18DA600C1AA411B60901096838A5CBB8EA236A0D4C7109BF65EF8E6B2507651F2FAA61ACA225744A1B56354CB945065D6ACFD3697B8D331C0929A59AAA06570B0FD3B02135006DD198A92D4037301BEC0B328725533FC385B2E03033ABC4E1CD4975E438B19D902C3E3435019A42F664C063A5A5E31932ABAC10420A10A89B54582138CE812A473945952460F541B9CE51C9A23C5CC6BA0896249F097218210A329956057E6BE66DB1BD139A791FCB95B3603F12BC19E2876617520522AE31D827CAE8422FAE85C30AF33DBAA77967001910F\",\n          \"dk\": \"A3254A747B57C2065E22233D7D2C1F9CD32B8586CD2FD5A2C1FBA0BF976ED7FB3A36B6710662B9A5AC6DC7AA11D581A0B6E36307E76C9C14128129C30322375381688A6C4A01137018557DCA3184B26091B91AB05241A4A080C813B20686596A5CA8C29188B2F005378E41055DD24D0544C624813A527CC25EB546B30B004B054438E333E5B5C467E08094EC07EBD25758B57FFCF9A958AB343B08BC248B0721A8C17DFC019DA719C4037DAE956F22C41711910513C79F7668B075BBBAA5307AE8131E2184C4164C17BE098E9C9755FCA53C0C781D7889544731A3F24C986C517C43156FFBD35328BACFF1DC436EAC6CB1F92F05231BAEE052D060AFC2C298B5C2877CA8B6BE22C3BA9C3348368DEC30AC7B05A58DA06A6CD1B47B32B709704679839DD873239A8A76F3CB8DABB621AF84BEE644A3C9C348A2B071ADAB3BE40973CD01958778600325CB70B486A7C84A0BD577B313328F792A284B5E1CA4B17F476F24C75C3FC325C07508E4EB9EEDAB0D321B2603582F765C481BF908E0CB574925046C50196A59B35AA282EF1936FE574016E4017F8A9E90E49D20F9279E262CF332CFC0E45E34E22FE460091DEC6DE5EC3A6DE702051284BC234E250B7A63D282215A27DD036149E412DC2C58D8E42E2F8A3C680C9A88B73C8603A53D08509849B379C5A3A6EA6B910539BF420510A833EC6527036139029AADD250A095B56C96A25DAEA70EE8B737366A1A6786769B78C874CA377B652C2BBC93276785D0893A196316F6DBA1248BAB288086D783811A2C1FBB7685B979822306C6B2CCC736562AEF3A19B5A364DD472442B37BC7A363CEC9073872BB4E65667B794A2CCAA319F6C437D2A4EC4A73F0FC5CC3D485837778A3C880005A5FE8890BB42BC88D070E5D7788CED8BFAFB28A07294C81F911A9700A38A880B0A85A97D2A08EA3B0FCBB69BE19A6D330B9AE492D8F3174F83BC22A3A98147621651CADCEEBB3A2D3B18954121D82183A7B465B034454E5390C191BBC063BE47B3DB5381FA2C6862BFC8D34226471E90C45E38EF602511BD4BA1807B95CB72930C537C99B0C1864267FBB206A66A836293F4775416521C175424191CA749A251549F6155EC8883890161BD09EDEA361C3F3CA3C086AAD70A3012C2C196366209264FAC4076E977408A91FDCEB60B8FC01F6B1C1F44C65AE14C3AC366D46F24333B567A9503C8AB0C0B2CC9BDA25400DFA0632058DBDB40309F91CDB42ABD76B82CD5248AE982C994119E08A524D5506395BAE3CBA8EFEA758973C8B87C8B132D12B32A29C75F94A44350413D38F426A57A6609C9D50B1CCC41183BB35CDE7026295777B5A0A5E29765BF80F319C8C7BC9BABF8102E798BF92F923BBD71DE01A28FDD3A0EFC27C52C30B5E7157E90C3E3F3019FA128FDC6934B5E773BEA3935CBB79BEB360B2841D6BD317757805F6E4CBE61012011BCE8E60CF0505A607E757AA96ABD0C75F15A948B45A56A137AB28BB3795FB7192C3501D060E4EE642F9A26AD661490982857E49280D99196899A70DC63926F41A9BC8A5BCA13B3CCA3B2A3998083A9BEFE93CD41A800AD6252D9993716B7AEBE46FB003C850EB86CD045FF963A528558353903FB0A62552C0818A43370EF32CA5E4045EB13035A1793AE75852229825ABBABB591DB2F1CDBED76DB7E5CB74086FA9A6A803D8B0D23B9BAFB112B029721AB70974E6A17757413FE363CDC040CB789562326302F509F317CEC82B489560B2C83C5E3E214579CC76A9140FD9975176B97A8E086E4CF46989C024A16067155971704130D4743BEF8A9E4E63B196142C2F46CCC5FC41FA2009AC1BC25B8A67DDB03982A161F5544AAB7CA22DA70E7CBA515AE7AB46185AF4473E04B593333799C9E31C91CA9CF85187D2D76FBA1B6086B6499C5CC614636CFEDB3A2C226AA0851AA7182D4C4296A2825A83ACA2B0C38E5538B5C7F42A36FC2102F6BDDB5870CA9B1EFD1942861C038B88122FA9929BAAC480B5C9AC6964A8DA9D59ABB52D69A8A5897EF745CB31E5891A1867674BA526A3B79C9405924B00896A97A83394DE4C4539E1B877822BF35968C09188B81ACA2852335AF8A711493E6FD843B7863EC61013B9401AD1B860B6B555AE394A11601518D45C7F089EA746A7852A0D72F2699DD98D24155F435909068A815A6853C9F17BD928B0B7891B83F4447C2C5213760678D74FDA8C793C071002040EF4D81BCF676483B85D2AC796F964C9B4F8B7EF77656FA2976BFA65B9E6AFB7811903E114D3060A1A967A1465C512D2C1513863A548A787279E81674FFD45BCBD7C398D567F22D7291F50324D45C9A2FB26DE3A7A7CB875058979FCF4BF19A03A8A721F17E514773A83FA0A939393ADE55CC7176AC984CB43C7092DC2D0A55DEA11F3E41B879B5B52984BE72158B08BC0EDF25866563A56D4B98DF9528FFB28BF345C4D136EA84791DAE11ED324AB279C6C8983AF725818829CCE05770AC3837A2C92AE2B1812D143032C72A32C94C2A06882ECE2CB73C0C41EDA64B607BDBFD9258E03A013B9CB3BBC5C40FB10CE413B11897E81D32585240851E487877C4D5274C6CCD45CDE88978994CF463111A20BBA9905A6114899BDBAAB1053541BB3502D9779DCB48FC8E523FA260F8582B6B8F30232342848492DAB73197EC492407ACDB52844033C9ACBC79C366A4583D98585942B05281328659506D714FF856A9C2B5F370B5136F896BCB87A23331AD0A7610FB69515E84A0A302407FB9949ECCFD91550A8618049A48BA2F4096567A0D383BDEDC705645C4025E0B139C22E6A16023062901B8A2B6B515DC8E06A4D5514489229D926521942BE150B96F2486CB34B09D7E7237A440588C357CE061DFDE1362DF1C2E58C1E1D72235E922B61EB2163D906B5E051BC58497C7CAAF5363724985D0FB3BAE964A86AB36EC46AA3D7AA290B428744623A3DF900DE975E3AFCCCA539837BD897F12CC555D684E34BBB3CD7041FCB2004A687B5E6A3E801CFE9C8C2815C2AC4783042A23736137447337D9221116A62C0E1D8C191B53E22C92A71A15D9D22505FA5BE17221A7236392C0348562216BCB037D4880A6D1236D3626E1B354142B5C897C646BD62CE000C27C53991A1C69176D03A16A61FCA51067C596CF01B89933A258B43C22F489AC70C1D8658B2C0FB2D3A6924A704302F33C25BDB556E97780747CF652C80E87B23479AB2C8119DFCEC47265A4F7D524E50294C826AC4C7B39C3C0782763A1A6ABABB9E14903720A9380939FDC9A99C6766C9D8B2828881BAFA555A35022A23A6F57CCA446319E71780AFB3767E9311D5C990F1E4ABDFF51A0E7238AA98AC412C05356583B05C170BF793CE770F79F81E99F78FD46310EEE27F614226F42367319C81C98A1FE2B2C09F886ED827610C8621B079A5A51863BB69ABC01C4A75051B50D83D1AF85B89F6918313AE5AB0999035088813102BEA06CBCB14656928DCD53A30255B4931017BBC4682CBA54378CB7E118A275728B83A4BD2586D271A8F64C06562248CCAD9A1FFB8265512712D0B707424B015157788318C7086BD964490A11A35FA43BED6ECC533E07D2A6ACC806990072A7DC0322573D6B88E29A7B47CC42508C7CCE64AD436AAE0160309E6922B655FD3B96FEC22CFC7349A746A0012D60A88C17D7934246EA0290369B3F7783FB177534F300BBED06EDCA87786D5175B93239B115254D676C921C1E5C3BA42737AD51B4BA30B1E65A8B59EB6A45C34950E9AABB7E5CD2F556485036BEF93CEAFBA97E83408D61C6FEEB908873B7FCBE93D7E386F2563CB4A80A671A1155762A02968622CE9163284777A3B4AF4B8858392BDEB9446BF59CEDEB689C609859C9B47561A8B39A19F7B36896DE82AF89BC41CE6C54791A78D6258534AB7090C502CE0A6F70CB3DB442A9DCA58E766A2E72286B42BB431B4740B125C73D43F49C1994761A57E3291CE95A2F4B88B3DA31CA994685A75B71CC622B3C2BEF60C222696C031789EFA1A3DBDA576EC1B802C261D1F44A5E4B973AB73946E41159D622BD8A0833396517DD2C51904AB67F05BF5FCBF9B47BF05A18DA600C1AA411B60901096838A5CBB8EA236A0D4C7109BF65EF8E6B2507651F2FAA61ACA225744A1B56354CB945065D6ACFD3697B8D331C0929A59AAA06570B0FD3B02135006DD198A92D4037301BEC0B328725533FC385B2E03033ABC4E1CD4975E438B19D902C3E3435019A42F664C063A5A5E31932ABAC10420A10A89B54582138CE812A473945952460F541B9CE51C9A23C5CC6BA0896249F097218210A329956057E6BE66DB1BD139A791FCB95B3603F12BC19E2876617520522AE31D827CAE8422FAE85C30AF33DBAA77967001910F38849E1BC23427C122859A98B41D65FC5EACBF4FD851681FEA0ED777FAFFB534B49335D1DEC43FE7888BDDD29CD891FC632616E3B4107E091ADAE014BF7F473A\",\n          \"c\": \"6FC99751A98DB6AE5A504A1E4D3D37A91E7FC63F2D3457F3EACA0BF1045668E13D39B5F93F5EDE0CA55A0F5FBB90CFFEFE9292D8B723235CD6E2C7E92AB086852CCFC31D674BC95FEFD2505DD650FC3EFD43ED787A5BBE009B33E7BC2C47C1377C874796F0A01B9E3F4028C797932E39819EC4B3EDCDDF25EDE63EFBA53935BB421F0C63C1AC1BD0E879DD0B3A47B5A35A5F215158A35A22151EE86AB2C3E9F5CDC387FABE327FC38D0943F1F0D9891BE86F1D8F78D1C29C7700CEFB35FA629CF8120798D9FB27E3882E948C7B3BC08D09527BBE1F4D9D88F3336316FE93FD91D00A9A53F17AEC68E2AD8B335F18330ED608857C3996D111776F9B29855E75F59FAA9ACAB79CDEFA617AE017C0EB7F797206BDA76E1F01AE4C83D24A1166B68A7DDCD3B7B2A1C6D0A470D7120A273758BD4A078F1D851C9EF50C846B246213948F273B045185965028EF8B02AC189135BA3B83904EC978998F1F8A55303057F985BA26A85FA467C4E620C4742A36BE7250ADB5A937CC9355FCDEBB045DA27369DCABDE4ED33CA66906AB0BAD8C6310507BA241F54F37BD6F778DFAAF9B7530574B526D742D74DC1CE917843E27533E014C3A8348D9EF56768829BA4A03FA2A215DBF2C45E2FE06ABC268E6D335EAC3C584B6F221414955EE02BA70996CB27AFAD316D18D90BAAB3DED3C5CE0EF31F3B4155977381E5C76A106CC7BC934B4EA92142E18837930842DFD225FE0E57D32DB7C079C26DCC936C7A526E3A117202E0A29304899D08EF478BAF0A2413AA5D41F5758B22597B11E5A24659C514DB176E7448CED43D5087665E48FA2CBEBD8F300168BEC904E1DC1A39ABBB4558DE1F31725DB46DBD6C847D4BA1E260651473553002688FBE3DCF05ACBDAB5B0EA28DA9D83D6EA5EECA8862799DE09B42AAB2641E84A158240C495EBD6A527DC3840D099EE019F919E9667904D22B6A73EBBE5D5B1A781B9D480FE9915905722358DF0268E1BF3362FEC849C5883C4A34A707CF2C02E4CDA03CA8904659B933ADD46A81171F7155C7275C6035B2B572CDEF3599E5B4779E606F5FA757411BC97D9B46806AE144920B29213D2C887CBD79121D091FCC02B1D922B11D05583F1C8DB8C1CF4092667784BF4B766DD263A5832FBA3EFE2297224E01581AF372511D1C6FBA61D933D36DDC2FC845694B67D34F11F95B8CCD626463F58918D6A99C5D06F502518939C9AB9187F1BA2C1FBF9541B4AC2D9181495B0FE2A3741B556600CCE8B78CA02AD412B62856D7F588F6C49B08EE4E304D145FED15E2212FF167FA48E1D75883504C5CCDB4F4F2BBC3798DC2BFD9A74D747E9C8755127E73EA344FFC110F88164605A1813A8BF1EA4C0EDE38592CBC857A976FC07E64C149705EABA7EA02D8BBC3340AB38C4196FE28DBFAC25FC1A7A8C9E096D8FA5BAFC110862B47282472B8C731A45A7A7EC3BC19ABBC35077824B05FF19B9831D4BBEA9B34F8B28D50E3451D9DCF9B05E0C2330BB78C30A71F774760116894444B37B0FF1E36ABA0D0375DCEE290CF46219DB7D5110D4E51A217C0456227FD342CE67A922935005B3C7D1785FA79C608C149050373097258AA5B36D187B23DC602A970ED7CE5D36CB1AD10970C19AF67AF01BBFCC620D9BFC9C7DD355F274B6D2B585B730CE9449F697A51208FC742261780FBB178AB667FAEDE8422F734FE341DCF8D635DC7F2804735286F83EBB7CFB482685064BDA42E825120CF078F487B76D69402A74D6D4149922EFE4A8FC0837D8FADB1225A54434EBA471D0FE90DB55A3252B902BAAE0D3569F35055F1EDD9F0AA2AEC8DB9D1AE8A5F2D9D6B51D6FA65DEC58308274628EA66CC199CB209372EFD70D1B1DFBFDDED0B3F60F83C069A2E26B0A38F7DE67209E8BCD09FFC17B48FA95CB02D686CCC0E419D9ADCBCF5154CD18BF8F5D13A6FBF083A018F66D9BF28B84CBE9A1C3902BD55C8EAC8D72C8E7B11828D3F9B1794FCB3E6A50F498901DEAE1381E7F10A30244C94EA9F2471A1500772B3ACD0869F00FFFDAC3CB2E8F69385C60D7E8013B3C7D2C358DE712F3F34598A7D1623E6C68373C30F7EE7666FC4C96AB93CC33E4A3839C33BFDCFAA4EA20A25DD3E2325FE93D142A18D3AC06CAAA2EAC6A9CBF67E5E087BBB896E803DC358BB69992A7CE72A69C7C511A85535EF05B4FA790A574E21585C3FBB0B7DB585CFD98076816BE849379\",\n          \"k\": \"D3772614A38598397B21269656EFBA39B15E482299F3ADAF1F82225595A1B7DF\",\n          \"m\": \"ACDF91D5B4F2047AB9C7A8C2F4809FF69B9D480334C501E6BC66D535D309B100\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 71,\n          \"deferred\": false,\n          \"ek\": \"FE456F478002570478BED88055B4567135B19DB8CF52A56E41824984A8BEDB64467D7A0A45654153C8645C5054C0E35C2C75877633BA83AB25857CBE5EA998FC6C79D983B5B1A984FCBB14CBEB483587BC5D952A4BF422344A70C4BB8E9162CB31D2411637526514920CBBACA6A911FB80662096B85194B308B881189813D171CDF6463893CC58EC512DDBFA31920CB1F33AC72C15BE64A3BBD114B27185455DBCCE9BC67AFDF6C8A393CD8A427A73E052FAC64AC22B5DAA7481E7D017B8E14BD84690D9054BCF2557E942BC7248AFAE57911FC64732C450EC3C1CCA599B96DB685EB704764CA7EED07F9BAC7103F38356F86FB9639665C35A576B8627CA6DF0F138FD198405B6371AB1BBB7EC4175F739B408B281D28BCA369AC0F1971A374A97E9097FEC3A5ADA6C2C7714B5CA0958E4848C9CCCB5172F1A6B7F2BDBB37EA346D1C9AC0C31A5822218F9D3544DB6464E605029073DF5F030939C8B7CF84A714904DDF56E63F04B2240376820417FF320BED27D8C385841C54222097742E23B972A6B7491524B88538A07988FFB9861D0B693CC80BDF6BE9B645193C06EF8132813E715A2D72C55140A1A9C54163B2780414A5996489326CF52112640B08166FB9F9461BA19661F1F47B8270396A0A6C8218B636E8496CDAB3639338D50D0919BB7610AD1BF9FF76D7D0B1F0E241E61A6377CD83D430118DC222452426A9259709D91BB46444793741AEC8998F876553B7770E9CC1E3FC749DF81AA4461BF5E6CC1622980968878ABF113CA67B7646A0463604D90353C635C2F2E330F71949E2E21806091319B328731B298E99127777808153652C9C763AB386ED7F6CFEFF10034090D134A642A218D277B6B00511F35A25BB3DA1F299C22B4A3889D10B3754C52D3F1B1373840A973793EC4A5DBE76D2C29626F1BB469722BBB99B003ECAA85545CBC313AEFFA6006F0AB7A857AEA991F9BDBB92E3B75DC300D1C656B2324CCE8DA2FD95C9F510676A8D6AAE77535BE3A9ADF8B1C974293FCCA05DDB31AE1CC81768CA90D00A4A7F8191C127F6B8CA010B46F3C05A808D9911A626B80365E6AE6BCB428BF3EC8710557485DDAC34583B43BF55D257089EC997F857B94752B8BFE7942689A5397F71944343088D6A9A2BA4936B2AAB663857B87278C3293491A75C861987C187655F13FC2A28749E3891E8C236B1B675E250639E5461BFC94F6B7CAB6D8077E4CB8719420DFD4951F76CC6288229EF49C8B8821C3D497CD860C58A619C9294689B5029F5B2D0BC95C370A8C7FA3C521A695C829A9A1F8012D479E38DB4CFE650550664CC9E0B631E2682AABC81A601A62EA16548584A571CE417508AABC4F1970407ACC47B449890598621EA7623530409EC49AEEDA8AED0336DE4BC4D4F9163A3C82C12A2C5D86B24FE56235CCCD26F5AE517367523AC739C6B69E977E3FF2A854F8040DC4B97FB3A29967005D373E0D91166A679A517822F7AA59BD4B00185AA25E0929091ACEA1FA4ED7F7BF7E463005D59403010539465D5D918C1CE22C55A50C749C6B02A360345A0A0790970717986AE9A020564D82DC851AE815D5FC76FDEC1411D01C56166773D7067FB29CF842BA9E8136F755631ECA53D49B7B2B296D65E63D29F96931C02E2AC013C016BB05EA6817F19E1286C3C2B9A803276FB58A916C838E923932895C9961BB3A4BA6ABD52880B86B391035764C4B953684AD13C5918ADA13AFBC42D4B6B1D523B39C380AB8390E6984179A5863AD488897258CBEF615D53281AF99239D4A59AEE6ABD6EA9F72B88E34D544F25A18FB9A45B0BA7906367ADBFBBE3BC467EE7BA3520360DF56040E730C2ADB162F78BA49EA4E2D1969839BA16B2AC991CA439D337CED7153A6E80877B73F6A16A22FE196F5F833ED7C0133505EF9E323C00355D2405B8CA01FAB6B780385121DAB8544C2C609C708193B969701728BE36F5F972C0C402D2E230370175971A601C0560A22789262E539AFB55B4EDA9F8DAC1C63FB77B91B2E77F17D98C0297FB17919CB23E4D45BA538523047A17690C245E009B031469F6CCB30980649A14211997A823C092C98452517A2BFBCBAA0B43478AC399FA9B58FB96687CBBEF715AC3855C493665735192A718965D3825325F151D3A325318A667492B7F6324FB28DE4A16F77BBA3884809A3445D53289ABBBA26997E95B89029457749E9B70D\",\n          \"dk\": \"4FEC81A2301E8E900CDCAB71152484E745CDDEF6C1394A31663046548620E41BB4B96B275AD23D004AC2AE606EEFACACA4815E06DC3F1CE76659E8517CD107F6BB3FBEDAA9F448999544771B773EC5A0CD23E8A090585DDF0892EA7720F35B2AABDA2E113AA85E9CA26EC49BCB6299368A66F2385E7D22884E708ADE3185FACBABFA4904D1378F86934A81217B7E273F47DC67BB91B64EE20DAD457531B4C7545B72AB397CDCF1158150B7A7E3193A322F9B4B0B72838BA8A642DBA31176095E8A338C889A0A8E419D2F2709D109863DA90D3A88C4D95A5C61E805A7815696391EAAC76FAF252B9763A1A390441C70B585A908E8829A60885CE870A50CA0697F56CD63E856FF2C24791A80E7284EDFC8C36580142F757A27DAB8279160CDF4581FC05AE611021C689666FA300281B3E46589A07925923A3F32260FD9B8454EB862A3E29F8F3C5B500AB7F911422DD737A1E117E7329FF88950E9A3549B41BFCAD103A0C7853F7241F48CC9A6C4A45901230C3358021489DF2AB5DCE2B626653B00F62A9533B3394136E934B1B977310638099B79AB29161A13F839294683BF99C031F3B3CA4778F76268785CA85E0580317B8FD225566BA2069409815BF05D21218E517A64DD9CC0140C90B3C05C8E1B9912028BFDB04722F407116598BFC4A80A71576F06918F6BA64663970980B6473845B9AC281915893F5376FCB00B90C323B447518F7A8C7214CEECF85918A97829718BB0D594F429C0BC990D9CC7AE39B73EA737832CC66BA0BB4EB07780CA40B4BA43B8CE54AA90C74F40E57567B1822BF83FD7C7ACE97509B3B6BF101C23CC6B92F5CB36472C1DF4B3B97218C1D3880EDCC6AFBE63A096A279F16785CCB7233D5B423BA35D6BFC383CE12B0A7B4F44C5620A197FD6C1AABE457DB5A274781340FA9CB603D830884BA7B632B0FB56753A5117F0C806FA208F89E1B7F4156DF6B8A0937C1EB0E5148AFB6BA88090E50366161BC222C17464719BC6026EF806B77578994877345D0111D834CAC01362CDA26868957032249AFCB522B92555A3C414B544A4F2CBA499FB918D7079EED7221A2738BF915AC7033A897BC1F8812C64A5271EDCAA29125C9F273578E73CECE2CBE628BA49C4C06450AC06C7BD960B31694A86C6322D7F35BAABF415E43B4A2A074E8CBB945BF9A79453B44B05C4A252AE8240087C97204D1A1954B8AC438A2293690C470384948C094D59A9346A1314F22D589844760C626488381F4CC62E647D4175BCDFB16DDA3C4DE819BCB3A00AF208B7AA2C652996993277932C79697AA3A0A777C56B506ACB1C7EAD291459C995942B3396FACFD5B8951CCA1D64A55CD09B6A3E5772E5B754C3DC065D312A5C41394CF75094DB95A741319199ABBC996255F06F8E52025FB81785813B95D89E32A19EC1803616C2CEAE484883F8A438F40E38735C3FB4452534BA16025553C5C459E9A9E65C6E7E84474B9B6FE17606D476099E2A3861CC1E01611A368ACA50EB2A01760BFBA72CFD75AC60A0C95B161CE759001E742CD8327373E6B507206C798167362C3ADD26216A105094575CADB712E2B3A10DAB8A8828496F3C805F61C0D0087783BB47173C6BCD850BE86B780CFA8463F555ABA49981C63DAA4A1E9550550901A012CA53ACB3B71CE400542C2B5CD6366F84A2D4DCB64F9C6D6B790376A0042A1285E4E52522A090E42266CE8AA8919C036E7C1B8A776CA945ABD96690463A766180942A126876C9BA0DC9961C634E049448B217AFF7D16D5B2C1B20768CC5E73CC88637C217138B917AE522AE3F742165B6B754649ECF7ACDB974B9DD310D69C58DF3A23D01B8A7456C6FE76CB6649C7049EB0634FC633D4C7E99B754C8A679D50861974CC8F5F01059A5B97F40C301C3471768A668D2CECBCA91DAABAAC5E59A80453F846CC3751B0DF8B0701F20BD35C9AD564211E2C3991A5606B823903B981CC9D66CC1E5044F484085800BCBD835C553B781086750675C04C1A1916781D363643932BA8E8C2BDDB65E00E0AC990684EFF2217BB02AE0F33FE520C73FBAAD021346882AC6794B9045213D265103E4C25B1DE68254253EEE9C4B81253987B193794B4E23B67FEA3B5036508F798229BC7C1D8CB476022B616C65674683384787C1ED222D5E5903BA31BBFE456F478002570478BED88055B4567135B19DB8CF52A56E41824984A8BEDB64467D7A0A45654153C8645C5054C0E35C2C75877633BA83AB25857CBE5EA998FC6C79D983B5B1A984FCBB14CBEB483587BC5D952A4BF422344A70C4BB8E9162CB31D2411637526514920CBBACA6A911FB80662096B85194B308B881189813D171CDF6463893CC58EC512DDBFA31920CB1F33AC72C15BE64A3BBD114B27185455DBCCE9BC67AFDF6C8A393CD8A427A73E052FAC64AC22B5DAA7481E7D017B8E14BD84690D9054BCF2557E942BC7248AFAE57911FC64732C450EC3C1CCA599B96DB685EB704764CA7EED07F9BAC7103F38356F86FB9639665C35A576B8627CA6DF0F138FD198405B6371AB1BBB7EC4175F739B408B281D28BCA369AC0F1971A374A97E9097FEC3A5ADA6C2C7714B5CA0958E4848C9CCCB5172F1A6B7F2BDBB37EA346D1C9AC0C31A5822218F9D3544DB6464E605029073DF5F030939C8B7CF84A714904DDF56E63F04B2240376820417FF320BED27D8C385841C54222097742E23B972A6B7491524B88538A07988FFB9861D0B693CC80BDF6BE9B645193C06EF8132813E715A2D72C55140A1A9C54163B2780414A5996489326CF52112640B08166FB9F9461BA19661F1F47B8270396A0A6C8218B636E8496CDAB3639338D50D0919BB7610AD1BF9FF76D7D0B1F0E241E61A6377CD83D430118DC222452426A9259709D91BB46444793741AEC8998F876553B7770E9CC1E3FC749DF81AA4461BF5E6CC1622980968878ABF113CA67B7646A0463604D90353C635C2F2E330F71949E2E21806091319B328731B298E99127777808153652C9C763AB386ED7F6CFEFF10034090D134A642A218D277B6B00511F35A25BB3DA1F299C22B4A3889D10B3754C52D3F1B1373840A973793EC4A5DBE76D2C29626F1BB469722BBB99B003ECAA85545CBC313AEFFA6006F0AB7A857AEA991F9BDBB92E3B75DC300D1C656B2324CCE8DA2FD95C9F510676A8D6AAE77535BE3A9ADF8B1C974293FCCA05DDB31AE1CC81768CA90D00A4A7F8191C127F6B8CA010B46F3C05A808D9911A626B80365E6AE6BCB428BF3EC8710557485DDAC34583B43BF55D257089EC997F857B94752B8BFE7942689A5397F71944343088D6A9A2BA4936B2AAB663857B87278C3293491A75C861987C187655F13FC2A28749E3891E8C236B1B675E250639E5461BFC94F6B7CAB6D8077E4CB8719420DFD4951F76CC6288229EF49C8B8821C3D497CD860C58A619C9294689B5029F5B2D0BC95C370A8C7FA3C521A695C829A9A1F8012D479E38DB4CFE650550664CC9E0B631E2682AABC81A601A62EA16548584A571CE417508AABC4F1970407ACC47B449890598621EA7623530409EC49AEEDA8AED0336DE4BC4D4F9163A3C82C12A2C5D86B24FE56235CCCD26F5AE517367523AC739C6B69E977E3FF2A854F8040DC4B97FB3A29967005D373E0D91166A679A517822F7AA59BD4B00185AA25E0929091ACEA1FA4ED7F7BF7E463005D59403010539465D5D918C1CE22C55A50C749C6B02A360345A0A0790970717986AE9A020564D82DC851AE815D5FC76FDEC1411D01C56166773D7067FB29CF842BA9E8136F755631ECA53D49B7B2B296D65E63D29F96931C02E2AC013C016BB05EA6817F19E1286C3C2B9A803276FB58A916C838E923932895C9961BB3A4BA6ABD52880B86B391035764C4B953684AD13C5918ADA13AFBC42D4B6B1D523B39C380AB8390E6984179A5863AD488897258CBEF615D53281AF99239D4A59AEE6ABD6EA9F72B88E34D544F25A18FB9A45B0BA7906367ADBFBBE3BC467EE7BA3520360DF56040E730C2ADB162F78BA49EA4E2D1969839BA16B2AC991CA439D337CED7153A6E80877B73F6A16A22FE196F5F833ED7C0133505EF9E323C00355D2405B8CA01FAB6B780385121DAB8544C2C609C708193B969701728BE36F5F972C0C402D2E230370175971A601C0560A22789262E539AFB55B4EDA9F8DAC1C63FB77B91B2E77F17D98C0297FB17919CB23E4D45BA538523047A17690C245E009B031469F6CCB30980649A14211997A823C092C98452517A2BFBCBAA0B43478AC399FA9B58FB96687CBBEF715AC3855C493665735192A718965D3825325F151D3A325318A667492B7F6324FB28DE4A16F77BBA3884809A3445D53289ABBBA26997E95B89029457749E9B70DBD81FE53DAA89CC40752B7C0394154498DA83D6C03DF3B7834AB3CB12CE24953093744B72373F405DAB7EAC89D0B66540FBF92B623BCC950B73DC2251610E6A8\",\n          \"c\": \"C1C14C85C884F4FE4CEE2D0C470FB97D54B2A992F7900B8E57025CD88C755895415E67F6CCE90E241F534E950F91CDF2E0A72F59D6A721B6203F7E08BDE5197407F0E79220238A6AE9D6A5F95EE0246E86C35E9F4473D5CF3F59421187DAF3346A513EE8E628ED64F18F9ECAA1968D8CE28BE468C01C0792F5B70FFFC3F2C7AEFE601D8B12B2252E01856579B56AD5E686EF2A3D27FFD75DA7337D7866078F2C830B405450F0233D60BCC06F90612A1C516770A0BF25EE2F59607ABA0704DFBB01C18DE2EEDCE64472A93F417797AA5A0BB4C83D03E283D2A0EA37BDA94B060C5D1EAF854A05DCD60D4A6BF7925E1182F15BA3E9D992534D4C6953B9AE08DAAFBA92802D63E0A67161CD00F0AE3D26645688B00398BDA91F7507B8112D24DE8C7146F223DF6ABA561FD67B1B58CC909DED55FF34EF8478C76195B269BB650C30B522724A46209998DD77C6D55653C39CA608770B8863F2BA3A12EEE891C9BF94E77E95F2578907966B121DF02527E68A100A7E2D528BDC50BEAAB63F70803A61A5F93FC3FF8D1EBB46A96F88FB3CEAC5BFD3AD2434875430EE00EC06CC1F79D4811E7BD4DFA4F07A25052579440AB733BC189ECEFF0F37929E93ACA05F52AB4FAF030E8A39E37974F421E97FEB08F0238AF55F9FD33BA30797651FA5AA4D8D44EFD3F00E1F805AE89585D1A36C13AE8C3059CEA591B69C54835E8A112E45371CF46DEBCB625698CDB3AB1E1712740B3C7C2AAABF543477CF835193944513200AE2FF7D634D6ADC7B947238717C2E313D606A9759CF9CE2CDAD5E271FF0C55FFF1CBFF2391C21D22C0969FA9525F920F0BBE5948B99F1A3E91CEAE892148B443F312B66534A7C4276D88E981BB01956E06E1AF8F2399EA1DFC6FA67E0185575E82E1A4ACF62E240A63BE3AE57D9F93BD1208CF2E36BF7DF24812A431E2966849F8C46610E7889A89DC4F2F3B3221B2A6A99E2811072ECCC7FABA0F4188D33E4A40EA8CA5B48A6644FE367B54C4D2C16B4F3392A8D6399AC09D820E2DD4D9756C63D9AA22D8E591C04E0F2D79D22C1B75EA4F393080C168EEF1F1CF4AE4FB87086D5E6ED067D1764BACD6002E4AE63E250364F09C2B1E0EE6D68E2F3454F541FD58704DAD3DD1E195AB98E9EB91D3F167CE1FF578D7F1E465996F14D281EDD48BFF29DA518678A7D17A7AAF37A4B0A4194E5F4948337EC7B6CBFF9BBFBC90F2CD1B9C778FFD69164AC71F3034A1031C08A669499DCDCC097E893BBA7520888F69CC9F29599576E44DFABB0FC4C3EF5C1F2BB9E5816D51F6602FF8B88F2F3B63BC66D685610BDB30076E6C8F3E99C99F483534B46813E0144101A82972F5C843884E613B9A75B0FAC9A5A64D1CD6FD44BB12233FB9E50183E66815F9CFE7AD7D12C24556E826323F7A2ABBA8036E84DF71292F3C209A4B5D1DBD69FAF65B5860A500AF82E17E6421EA59F11DE2ADB9737C4EADDE3F7334A53DAB3AFCA556762D2B7D0D1565C9952B93BA394C4A0CD75DF1FFE76ECB4DB2D755A29563A7A85258194CF8C7899A0EC125453747CB775496BB172D9CD580396A15487D2A1801D19B899DDAFEDBBC9E8832C0E602D25A4CE237EE79460A069445952BC7476F31B3C67CD2CDDA4167DCB65F09B32B800CB04D716D6D2995BBBC5497085F543AAC81A0944E7FAD8FDDF92B67058AA0C4BC32EDAA9760844C7351D64726C506A61226124C816035A3A32F8F42B9CB808429E77EFDAC4E6731FFE83EBB97E0E805065B25A318A2545EB6B5A7306F687F1144C3812209CDA0CA9F9AC0F66A9C29BCB5279BA4061105BE2BCC187C37A186EF9399B93E5EAFD81C70C91696DBFB0721024145E0018A135EBC7004BA5DD824F5CD95515B8E2AC94C6DF5D07C40ABF03B59CDBD138270B049A640A1C1075E2703DB1B547D8C013DA7E3970E1F8946F3C9112AE214109D4AEF6EE0136576457150E8C54C2056D3688147CBB3C533E1DCC781551477FE4FCE6A1BE0116C5C0748CCFC8179B28F27797F58600A99AD1A3C5595F0A09AA6442A516D122EEE099C60EFD54389ED65389DAD246386C6CA2BD9BB3EBFB0034A3B65D08AE6F10AFBA3B4558946543462D627188EA244AC96020FA9BC43313A46B22259AAEFC421E6A2BFDF450990A0732C8518DA7286AC923804928161D3C8CA77516AB03D2631AFA65CA44E3820295673A1ED5E012DA4294541FAA8964A6AA\",\n          \"k\": \"7E151A29276FA13C06F530A46EA14DC37B820963562AE9069C17A7FAD27C5F1F\",\n          \"m\": \"696EF6079C573B67BA3531CC69730216A3A8136EB6F647481382A5CD93C6B7AF\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 72,\n          \"deferred\": false,\n          \"ek\": \"FD1B79ACFC0D6D79464A3A59A5E18DA6D54C7E272B77703A22610C75FAC3675B89E350047C80C6F5B6023A455C88A44B0327BBB6F40E1D502861F77E7F2732742C88DCFBB539464E3189A5B21ACA30474E3758AA8BD2259987B8390698FD52378DD505D964350AA30B26AA925D83B6C3CB069113A2A3133FC2C19714A042C3056C774805262595CD2504FDE7A82D19878AE208E8173D9A855D39173D027B05CBAC4DE7408A4C0A4427109D60571D98D1738FA09B1D80BE859B34F3268F31A6CC472846C6E5C50F2A3E22CC8E81C78DA627CB2F474CE1EA0B6C02C7F9135129056D05F62D9B36BA466B9806F4ACF09302B135906AF7BFB43020A68444D22168388C8944BA60E7E803458B10D3B061F0652E08607CC2214CFCD2BC0B1B2148BA14691635C40492A4464230F7B6C9AC0366D813249B37FF57AD8533C752C0CE2B765D0319B84F204B9960B8C553721E5C1B93178621D4A16B8C5D2BEC6A91018F80861BC92CB44B25AC1B8C937793892A989D8FF465623666B1E467340984F45A1A591453D894C0E27876F851C664E19C3DBB3F62D7C7B9AC5C4D201FD3C0B3F7597D56A34CABC65F5E62898B0358A803B8A8651E8526C010184EDC0A1008F047E082BC63F0236FB93A9E70750255A28065A32B582CBADB3178F4B146A47893C62AF7CC63DF404715182013F13A11249CE5513D242A2E8B33C4B1B730B59A1B121181BB39449CC021C97C7B6F187966522C4DABA78BF231B945806B590C5638B1767280C29A070C0619A74A6D3D65C704DA99018418266A2AB1D2861B80700F67C6C845744A51220C4743E2A6639F188C95871B44D586F05A1EDEEAB382D123FF731042C29D50096155EB59906A7BDD2881D94081BC330CC3A71D3C1391485109CBD8026744AB57632D0EB35087B11EBDDC24222B4ECB435586C7226B11644AB9A243291CAC62CF3EF90B7991014CB7906D9B455A023FC94C7DC935221C4B2A58398F803801BD2A1BD5B40C17B529FABBC5F3219419DCC911E70FA0397AB407BD73258002594EEBECB45218154196A07A53AEA0ECC56F25AC5184CC39DAB2399B555851B933B45971C85CD3299819A27F756848C33C4E67B6449796B99A914AC8083719D752F06580D328087510AF7A5122F95A02A30190CE72482BF04FA37306A28836AB7A6AB11A2A311C1D9591630FD827431A54C2C738C5AC9CFC67BAC2480763E2CAA1C81E52C9455E305438BAC378D8A4A05168D72C39FEA242DBC794BD11B504E4630487142AE867903C4872F0B9A8F1572403A51940CCEF99618958A71569A8E32992F881B6084354548046F1E4186E11707CA72103DC7292A7858EC77D24BA225819B9574470C467033E35C3FC8A8CF955CD82565576EA576F5C43E0682C50D0260A30043CD40010B2B70C60A86F242EF193C4D3B837F33771DCA999253278D2CA2B9B88473DFC7565F282EED3739967345F90AB34A902F393A2389A557C815591665D63652F43C321ABF0712A012DFA6C04EF86A67F95A16B784E3BA18CB13215EA09CC2B5238F9391BB6A5C6DDA2C6F670A1E58329A8544C1E2C3E5D2A478057A5E46A26982911CC599D83A5A18D918FAECC86217A165C052344A38ECD885AA0E4CBC7D4742C99B1700578D454ABD5C83F587CA48A3A219D72B55F1934BAF1249ED25110177B6B126E0E1379111BCAFFF719D65B5570681164A80AE5E71D677586625020F605103513A0CC94A6632220A5687D89EC887956CC3F6575DF735C738222FF7AC70D468860420D0E53C192FBB31C8B834FD10EBB4B38C1517BC6E60BCC7A5831741DE067274B637060D4CFB3A82624B4207D94355EA3BB562AA9C5B9C04E2182C45955A06C5C7DEC5AD78BC9FAC6A87637BCB14AAC3CA76D5695196937CA34A36F183C599645CD67F33E060C6BB784227C735773534B4D501D25584C0F412A1A697C041C59EB85472ECC1B780C509E3417C9890722E57003A1B5AEEB11A764A6248C724F8770400747F04675C2F54BB1A983C59573EB8C64B39A3F552B79F5D365FC461A0414CA2BCA1DAC961C2E3A0A72109D79E26A59C1B659C4A45E9C8C2882CF6E26B67764244E3BC739349EFA0B9DB47B2152634240CB7A44A0A2B9CC70D557B6849A9976118DD8E74CB7896915368BFD120E31C4A261683DE548D0D6BF829F1E94FCF9E53757DC7EC8255975E848CD84360CBEF3FD\",\n          \"dk\": \"DD5097EC5CB6CEA0268886223C0370FA495D5BD55C93D6B52F74B426A2BF1EBC3A791A6BB6A1B7EF585291E3C4176AA91CE998D73A2359361B20D715D8250385DABF4B4438CD26326A19A651D16817F514A317795496349BE3024BD7AE24C75BE3F313A64151E31BCB0BD533B7388A0A865C41AC4FD2D087AEEB1F2758771433A780D88A6D47255B523A72358BEDB3BD64895039EB2C9434524A926B0291C0D4084C743CBBE35C35F2866DA31C27A5EB1CB87C014B021056507702702D196B34F5B15E11B953B3972123A757084CB50E46B6A03B5F66F41CA7C7720A63C380935A90F9AFD820C1A4D305C7D3929E0C93A5801D62C31F3CD9C92A8C9C61350587F0972AD0C22B32209ABCB16F69C2ECFA89197B27D64507001C71AA22475E851560F046DFBC935397C28A9237D19081983652C80C4C5CC6C5757C8AB5AAA39FC591F88322B851CA8CE5609FE51C1D5B3B6086531046A4688121F86326D03A5E9FBB761B22B67A0CB84EFCB0EDA957B1587F8FB8330AF5BD01581DAAC976C46A9B0684131B75519251CCFB33A6E32322CCC9BDE7A2504A31A7C1642A631B116089BBE4D12B90A761C5D2BE88B58B373351C385467D196A169BB328970B196A3E3F5C0B06FB45A5C84020F37A9A94CA47F11D04B5A40B14C1093C45EB29AA834A3BF275B1483A4762E7434F761456F45F0C3116E78AC4B619B9174027B830882C53A845E9BAF1B750C8F52D1B5A3DE474A57F57BA9B0BA122A67ED4BB13A0C608325295F2E41629312E26674CE55C9C1B1A252988908315A662956AFD73609F546A70D48F63CB0B6650B9FEA1787AF017609B2CBB30B8F605A08BF12FAE63A0BAA37C25054FAB5004C9635CB2508E46E979777A6AB2F412072999A717030CC4C55EC13314E509D22A27BB290BC9778414C42552F88E739570FBEC7A4F87A4E9BCAB909377F5288A9F64AF721562B05CB5D92268614142BB8A9FBA30ADE6B825F8D9B0E2CB686AC9AF7EBAACB0695F24C1BD29C9CB1C5CAC6C9778FD8C45AFE6B6B3D32CE07570B2A804A3F111F07678E141AB27F12CB375B1E329974FCA796E2247B7B31B6AE23372978D7F7BB7A278897C25C7138510430CBB6F23BF5AC1B9B124B5E546472C80877D503906841547B45A72A319537C1E6A335B3E71AC04DC01D4A678F7C75538590120D2BCB0B767AC601148B12D66F1AD59A6CF4AC125C229C4C7A75DEE611862A47FD82A049600ACA1266BF4B06BEE3C6540E1796E23AE3C11317C8B158BC814F1931F2E68A4281B7DC0F48344E50FEE201F2022BF7E48C79B89AD917B3F6795A8E6C0A3F4C795467983EE9457CE57C22C18B65CAAB119BCBF10328EFBE91A9AB8765E989C190A1108799200552545706964D045FA54478CAC50DC1044266B70298AB27231592B2675E09705EBEC47C64868C26A416D6C97A1D87563DC56FD5A3715B45E063A632BA278A4C11B3ED76557782D1F3640EE305238842D5D8A0251021D63F976696516970CC6D8A8214CCA2F5A265BF2B5AF145A8109413147E88E9C7926F7C668B38289BC6556C10334DC3438904212A02B868F0B5773C088BEEC7F1A914DE0520260E5823F89935103727CF8325206963491335ECB4BC20ABB8D76481F0703BB077881958F5E55274BF9A149C11B7B92B4BB63B8BAF06CEC031A8F899A66E28E9610837100207DF7155B096A33BA1EC3A779634C3F571AAEF3B6B7B97960E268685466CF06E683DD6586315413FDAC5303E664330AB575832C394A084C65BE3D41BD533C631BCB411C718145C264F977435B51BFA8A7C2999147792493B8DB0892540B93E27E452C47F7E57B1DE09C04519F9CB1023855A06B2C76F5252FEBFC1527D55DB1087ACFB7AEA532471AE14449F843B47B96B8C06775F27642630DEEE6877DB17D2BE4C101950332614B4A5040FC74C52C07597C1B6C9D46443D68A2652C2FF9EB57E4F4B247CC31D6E5C732D9083A985BE9752506C07F02CB868004CE0B3154FB5A92F6F5A15472A9FDE333AB1B0F08386397F9621566790BD8AE0CB89763F2CE74495D1A54A800E657A8D42613B12A8F3A416795359AF55E89B28D2D18A3E8B110DA297D33CB0BEF9C14D4058AC8DCC633CC682A7C3BCF10BE800BCBCDB7CC773412EC643DC77B5F5CF0A87DFB25FD1B79ACFC0D6D79464A3A59A5E18DA6D54C7E272B77703A22610C75FAC3675B89E350047C80C6F5B6023A455C88A44B0327BBB6F40E1D502861F77E7F2732742C88DCFBB539464E3189A5B21ACA30474E3758AA8BD2259987B8390698FD52378DD505D964350AA30B26AA925D83B6C3CB069113A2A3133FC2C19714A042C3056C774805262595CD2504FDE7A82D19878AE208E8173D9A855D39173D027B05CBAC4DE7408A4C0A4427109D60571D98D1738FA09B1D80BE859B34F3268F31A6CC472846C6E5C50F2A3E22CC8E81C78DA627CB2F474CE1EA0B6C02C7F9135129056D05F62D9B36BA466B9806F4ACF09302B135906AF7BFB43020A68444D22168388C8944BA60E7E803458B10D3B061F0652E08607CC2214CFCD2BC0B1B2148BA14691635C40492A4464230F7B6C9AC0366D813249B37FF57AD8533C752C0CE2B765D0319B84F204B9960B8C553721E5C1B93178621D4A16B8C5D2BEC6A91018F80861BC92CB44B25AC1B8C937793892A989D8FF465623666B1E467340984F45A1A591453D894C0E27876F851C664E19C3DBB3F62D7C7B9AC5C4D201FD3C0B3F7597D56A34CABC65F5E62898B0358A803B8A8651E8526C010184EDC0A1008F047E082BC63F0236FB93A9E70750255A28065A32B582CBADB3178F4B146A47893C62AF7CC63DF404715182013F13A11249CE5513D242A2E8B33C4B1B730B59A1B121181BB39449CC021C97C7B6F187966522C4DABA78BF231B945806B590C5638B1767280C29A070C0619A74A6D3D65C704DA99018418266A2AB1D2861B80700F67C6C845744A51220C4743E2A6639F188C95871B44D586F05A1EDEEAB382D123FF731042C29D50096155EB59906A7BDD2881D94081BC330CC3A71D3C1391485109CBD8026744AB57632D0EB35087B11EBDDC24222B4ECB435586C7226B11644AB9A243291CAC62CF3EF90B7991014CB7906D9B455A023FC94C7DC935221C4B2A58398F803801BD2A1BD5B40C17B529FABBC5F3219419DCC911E70FA0397AB407BD73258002594EEBECB45218154196A07A53AEA0ECC56F25AC5184CC39DAB2399B555851B933B45971C85CD3299819A27F756848C33C4E67B6449796B99A914AC8083719D752F06580D328087510AF7A5122F95A02A30190CE72482BF04FA37306A28836AB7A6AB11A2A311C1D9591630FD827431A54C2C738C5AC9CFC67BAC2480763E2CAA1C81E52C9455E305438BAC378D8A4A05168D72C39FEA242DBC794BD11B504E4630487142AE867903C4872F0B9A8F1572403A51940CCEF99618958A71569A8E32992F881B6084354548046F1E4186E11707CA72103DC7292A7858EC77D24BA225819B9574470C467033E35C3FC8A8CF955CD82565576EA576F5C43E0682C50D0260A30043CD40010B2B70C60A86F242EF193C4D3B837F33771DCA999253278D2CA2B9B88473DFC7565F282EED3739967345F90AB34A902F393A2389A557C815591665D63652F43C321ABF0712A012DFA6C04EF86A67F95A16B784E3BA18CB13215EA09CC2B5238F9391BB6A5C6DDA2C6F670A1E58329A8544C1E2C3E5D2A478057A5E46A26982911CC599D83A5A18D918FAECC86217A165C052344A38ECD885AA0E4CBC7D4742C99B1700578D454ABD5C83F587CA48A3A219D72B55F1934BAF1249ED25110177B6B126E0E1379111BCAFFF719D65B5570681164A80AE5E71D677586625020F605103513A0CC94A6632220A5687D89EC887956CC3F6575DF735C738222FF7AC70D468860420D0E53C192FBB31C8B834FD10EBB4B38C1517BC6E60BCC7A5831741DE067274B637060D4CFB3A82624B4207D94355EA3BB562AA9C5B9C04E2182C45955A06C5C7DEC5AD78BC9FAC6A87637BCB14AAC3CA76D5695196937CA34A36F183C599645CD67F33E060C6BB784227C735773534B4D501D25584C0F412A1A697C041C59EB85472ECC1B780C509E3417C9890722E57003A1B5AEEB11A764A6248C724F8770400747F04675C2F54BB1A983C59573EB8C64B39A3F552B79F5D365FC461A0414CA2BCA1DAC961C2E3A0A72109D79E26A59C1B659C4A45E9C8C2882CF6E26B67764244E3BC739349EFA0B9DB47B2152634240CB7A44A0A2B9CC70D557B6849A9976118DD8E74CB7896915368BFD120E31C4A261683DE548D0D6BF829F1E94FCF9E53757DC7EC8255975E848CD84360CBEF3FDD737B4854C1D79C36194DA7346217580DB481C0DB2116D4DC3A296BF64A89F466B256F764B817B1F7901D0B12CDC08EF3C2419B0A23EB25ACBA70917A39F3171\",\n          \"c\": \"163F9FDECE0DFEC9D2BA04A7CF3A72A3CEF584D0F4CA5B041B72AA48068C21D474A61C0AC96725C657E6BA96210929BA18C5192AF13724E72F4CFCB551F6D0C2A59A4C4410D284D45077AF6C3A7911D0D0B4534409EC8C521C2C8BCB8B14D4901C0F8C85FD3CCABE31B6C5784F4818B2A195B9B837DEB60D739C608CBDF9E13C06B7F1132F6EC0A4823CE42B00079A19F1A81269BF26820474F0C0CAA81E20FB059AAF8E11B51741B5849A87FD0349CE05FC37759B61D191479B391CBB4EEE04908B7680E047232DA268EE4388D9D92FD944689D6BB8BEB9C4FFDE45FD77435E6CDE430D34C5D50C107B963D8E1AA79B0DC4633F31B79B5DAF009B76ACE6BD277F5F71EBF3B08A4BD511E26B8B1291D23689818E0BFE4DCA6EE0023297926D777F44B1A3DB409A5013E366118B98571059AAAF40FEB83E660894E11DEFCEB4A08CBAED1C17CA20836F81F78A128D42D94B3D71B010AFF7818FD2FFE7D34FAA458CDEEE897E79DD9D8632CA772303EFBAAAE1591810823BBD57BCD3858B37673436FA41D89D8219DDF163243FD773A40D1402CF4AADB34AB4BD75FA675DBA29B69A7C464372111EEEA8263D05ACAB73BA1655556CCEE0057417DE564B7C3CAA72DAC1AE5936E75EBD9B30671653FCF5C4C007ACF7C076688815492189D4BD7B76E6BDD2E4C5372B963DEE5DA9719C57B228299C4DE44D136FD5E125C9781CDDD9DA18F58AD586DCA62DFDBF3683AFB7BD5AFCFDBD0E5238DD89EA6ACEC0EBE39103DE643D017C67576017F550D7C6047AE2AEC3C1E4EE85D33C2B1C33AB551887C035BC437B7C5DDC5D06E08C35E1F46502CDBE630A4C59C14309EB2921D8468713246408FDC894C993FBBFAEA06A4CD8D8AF2E86A672E8763AD4E4003CD4379DBB1DB93B08CA8BA9B16BE4D039623DC264C6E0C730921B9B42CD665F7CAD4462B233DEB6DBC0A9CC5969D98D8783203C3E3004D29ABCDEF33E83D8FF5DAEF548B2AB91F558D23B2973A882A41B85FFABD54F908A79A8A0D2889BA54195405D8C24DB03EA21A92FA9B733EFB75480E8E7757F60F0E58433E599D9B9325368F1135FC3EDB9791E4A25D9A714F37970ECAFD78EA94582BB370F405216D4F22998AEE28D0FEB31FE42D2F533D5F97BEFA759F25684C1714677CC96E23668F697D2C39087A50A131BF8AE98E5618476464283F6F75DEDB24D7174D090C3E3685F10B6D90C423778EE272FBEA69D53C0BEAB7024A4D64AC85B147F9BE83EF3B26DEB5C3843131BD4BFB24CAE9564503F41197DD33355F0E7C7CF069A87B7D68B5BA0343AA5E6B9BAFE6C93D1894241C86F9163353B2E79C28ADB24A7F052A21BE8F3B3807AC16E66AC3B04EAC1D92ED78DD2468A58599DF566DA1F052DF6E7786F38EE15510D7491B1AFD87578C42075103C9EFCC49362AEAB9FB8F6BBA88506B32A536A5D535BA449AF8AA79FC4418F6A43A0289899CEFD33D93685FF4AA2BF413526D06DBFBE5BB95C6481A61755E961B2CFCECA472BE408EC95175A942EB27F21212E7C8E26F40E241650460471477B600A5895D934940C8B86AF54889C7D42454D8E40BA689FE02C7653AA7F6A6CBE16DA3C81BB6653B20B2AD24E172073C38CB3E73C192D7C6D0AB2E7DA418A3DFA2C6A45C53A8DC81EF0FDD349E91C2D62B1B38197B2AB41C8478B6F551437FEFE9BA127F19BDA0185E63E9DBB09BEB1519DE53A4811070B8E89F447D6639E76D4DAC5719D64A22B161D9A13D3FBCF707C2FEA9E92DC675B92F0AE208DE93E49D2015A33249CEA4B8AA236E4B27C3B4111B7E62A5FB4E841746AFCC5E37A81A7C501AD8E1C725BAF7C358848B63FF0ED9461ACED678DD501BA506C5376A69BA03258F681CC343BEFAB1AAAE513AED9910AF6A5970CEBC113532A18BF94366C280A992DAD57F2E57D6EA6DDC5AC1A465B1D5CA8EC6016A592B5CFB240D3C11532F0DAD89190E2B2B597AD12A4648EB605A69F2B34BFDD19F092738CAE74E5182A9E2BB91FBC5C87116F10D8DFDD751FD68806ACC64BED13D832873EA674D70755C89F60F1C0046B182D1F5BFE5C851AF7707A1B1A2E2C3A8BA52088454C8798D344F29283B006DB102910766C9CA33F405CA44207057479C4BA23D11C564D21D3DC98D6AB45C57B969DBF42513E8D589C0DFD602922CD875B6D0988527B9EC39917BE456FA0D8339999E68E51E07CDAB598D1B8\",\n          \"k\": \"42D06DEDA4AB863ECE98FCA1C9C3E5CCCF7CB03B8411B381B028034F12B282F7\",\n          \"m\": \"B2180DB6D5A468155A4C45C90495F8875538F05B8B8587644B4A668CC8936447\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 73,\n          \"deferred\": false,\n          \"ek\": \"5AE08BEC33AF8C2967C72B389BF868211B80B3F3621279CB84739860479BE408929695B043BBC623B13B7313494111C07EACCBAEDA248DF4BFD4E480526958FB9811FF98CCC755231045C744D29A6A3103012A3C61613C3305C0A424C8F483362B5150A045888AB09E39F08FCC81B79005C1B3B0BA5FAC23BF438048C675122ACE4CA672D4D6540F9064AA9766937017B147CB05111C0706A5B549498899423B1000A27B648F0BAB03307F8E492D582723BB20B044E91CE259C611F71B7BB57E90D4A249D02A7892B4589A63EB591CC559A3477766776505689820601B9CBD75AFC344C43FD9C9FF55AA4C791DC0188B17177032796E07368CEA357874882F32948EE0B63D589C808F833AD2566E22FB02C77AA78767B194731EBF5728F1A858C6C85102B70C56B96D32428364EA7A904A7EC4F2517AFB2AAC817AED1A53D701CF16168AC2E45730D57F93C12B9CDB38D602025B1C53E4A1CEB4EBBDC9403DAAA368BBDC9BD9858BB0B53945948CC3DBBD7B2B41A97B847447402C5624D30CA0738912BA19780BC5C81A7B9E26C3C2E155CF5C1A58D26413988267738B848F20681B823E9828BE7A6A8799A1360C2298F448BFCB5C422ADB3D6CE32D8FFC1A2E52244DB9C994416F15C3B5433319C04559E3C3B895B1B322232A69029AFEF53500AC5C87A4606F5268BFAA26FD99B45C456D96E1B248BC8D2D79CBC9435D3A8B962E91A81FC76B1670ADFD12B3FF044078A72BC36C8A435B966DBA5C0CC97B71635206CCCCE2972B80C818DC43325830A02C6482CB5B2079B5AB6AC50C24E95D16C8259945AAE9D45CC66B1F125B70915C5F222670E54A5B8CCBA4F973BFCBD763BF2BCACFF5AD51A9255481925C893C630B38678A355EF40A39F56570D961846A2E6CD827CC1BA4B3813B6B956CF4AA26D7E9CCE1721E04B9A5B8D4910CDC484134730B639B57B39CB5F69663EC7490B339C61B4EE4AA83393ABEBC60868B945CFDE98933296793F74466320615F03DC4222F69461DBEA4458E2B6E1573CFF4F610F78C3AB609036641007EE369BBE382ECDC888EF97C1E1812B87964EC19B04DF5194B4049DFD2A87B63034F7A02C3BB731CB67F7282B8DCB131146570AFB993661B059797799CEB7D081CA13421BE41B32ADE2404B0519A00B88A0E088E29F846F5B738113B31C680BB96AA736CEC1138CC5F5D9C67CBAC9B29E1C1226812E1BAA39F151BDFF15037E30E1D3B9CF86423131A4FBAD95FA0585015485B5DB755E0F45D129841EB7614D6C7AFF6194061DB727B299A30238E8B716C72400828B377D46B3F2AE9119EF72AB410C97CD4B31EF77A610242D3ACB17CC2B335C5B2DCB3759A4255921096087CA0ED428AE6703CB15A9D9D8A5E0D64B85534AEB4F2076130848CB450EE55954AE61F40CC25932851A2D80FA3601C74906F1A5360A69120A5FCB4793326F0C8222969A40FC16D46F1CDB318BA511C72AB516A4F31CE67204938E68EB4F16202F42D9E095F1714BF2719C7E571498CE304DA997FEF45549B5BB9F3834209096ABB39C686E4B2C9C796BA69B368417A45970178E9801221944EAB90656B623B53903554AF1F0C7644D79B2BC186C0EA5C791795F99A0B9C29346CF9639EA892935878E6F2AAB73CCEC7201D38A26305A286BB1970BCE86033A14D3677AFEFC81757A69C07299164790473055F10A8279357BBEB31C173B87B62293BF963C453988B390B3697A3CDE4EBBA5CF1B08F78CAC7498DFBB6407D6220C376A4799AA3A8871D0270870B9495499C88C98A5762F7CCB6FB0CA10046F06C06864345CF4C5B40B11181A973E473ABB9786FF307C65CB7048FEA4885041E65E0013422BCCFB65BE8CC9CFBB4448A645FD3F81779AAB29ACA1648708F249C8B3F9CBF55C736F5746854D89A923C1CFFA781D26C241616CD01D7B372F568B50BCA58D88D01FCBB23931DE6DA2D688A6751158971A5B19C6B46C3B761D8385238F8B9831266B09B16B5559B5EB82F64B7133AD36C5497C9BBA4AC9DCC094C3673691207D3587C97197F5CF7C1DDAB6A9F1383211639F3C57BFFF3B01709974C79C9C51B2E29E91770911A52281F030531239CB049BBCD841B1D764C49E0E8492AD383A093925AA818FC0CCEA049271F307C51F3C97A7AB5A81B43B52A6BCE2CEDB2D0E706D280DDE4A0991FCAF55CD36AC05F3593F3C797A9DCFD0FD0E0\",\n          \"dk\": \"3B052C99E38F21B24FB9461F66218E682A2343807AC6B0BFA79B8E077257EA63B5AFEC54AA9565D85536EBDB036E82389E228ABF62167E1539F31A5CD19C87896C2A31117C180C954E8BC5FCC63ECDA30762383D7C7990F8C1689BF90F28F460C66AB468337353838D6B675CFE475C4FDC6BCF3112F5B4A142A439D64C557F0075B8DB9D059AC883A1548B031CA8EB58D2930A47A18A0B2518DD6B56A8AC961CB41B1C7A9DF2479E3E43C4A45703C2C66103A0199BC09347CB5E0D700E27B61024C45B28C3B9112A899855CF7C77B1B2D3237B6A3E36157E888597D87C07D7885A89A257AEC29369800C9DEBB1EC5A188DD367A6437FF3B97EF3D7201589987C3238570A8CD1F9089591068F572BF72954A07A160380C2B4C23DD9381878BC54140B101C27873356381EA7425D3C83EBE93C4CABAD33B292B15B68F794816175A5B69B36DD973966CB9EDE35C4EF46326CAA08B95C961F693D0B499103BB0CBB42CA25E08607600438E004E5E373A994A0883508BBFA002566BBB3A79269CCC9687434B22350A4B0720C549529298CDB3126D8162FAFF29ADD11154A73A1B3A4BE3799CD53C533A6A6331C9843E83678B2226F36931424367976211E834A50C0101A6B9144BBE12522B42B660915812C6D07204EF0934B258711E0C50C99A0979CA7A765817AB57A75264B336CD8AB86DB894BBB3414726466A7857E92509B8BB39C67315C205BBE19321756B6A3C925910A1CDA417AAE4B268B8C771D311DF152696BA0659384A0D5AB2EF5B7AED2880C75C83CB5232A3BD126668307BD725E901A77AB46855DAAAD109BB6BE8A3AD49755FB4450624296BAA08BFC48CE9DF0BE6A00016C966D1B6A8C6E959A5C8ABF6799B63F2280CA1610971C046C960C691183C233A1CFA889489BCE2DC927B6C388BCB74EE1FB4000EAA98D081BF2F95907A10CE2D4548B91159EE877C12A775538508AAB9A6BC59201B3ABE4F7539E34C56BFC13CC559F80D9A1796B81F90308AFF381B99B686680990E9CB7E830AAA8612862C1C0DC0AC74B3016E3649FA5D7B8BBF870C7459BB34213DA480D33C56C170C71C912B78DD4264C979ED64098401A96F353BE82EA241B200B7094B60566B1042A39B94120B053CD0F56644EF8BD88079BF8A935083A94D169C765014917A391A0A780CA6C9C0CB88CABE20928D2CF7BAC04589B108E0C7E55A64070C52EDD65CE50EACE1DD63B10471266B1547D79C919593C36A10894320DEFD85CEBD12647E984B48C259F59B398286142F0059726408E56538F5C876067821685C1CE3C8F05006BAFA48718D54295B64BBF82BD6DA9403A2AA5E9F21380756A4AF71F9EA1CF99F31F22BA7513609AB3ACBCFDEBA09D5C5DE45B0D83F13D0557AB627805D02588C2E772351B2679F86ADE56873C509D6F15120BE86BEF908BFEE629DE3B221D4B03A9909B6B722181085B82C087BD4C985601C6EF1665890054E90A897CFB06C38314F2766C7BF31E4664C0EE025EAB2A80233AA1533032AC809718A8577F1C2D968475E4A0BBD25514A3B27C68EB49775ACBDC22377A920948F2A7B4C78F17520273E4206AFC8E6B580BC597CC1773CB34963EB7726C55B7555041852075A7C06602B9D717EB5574823352FE49134D972EC7B2532A273CA0EB5540B9CE9C6A9CBD1B3079C0BE96B178CA71593DA4A49A52B287144C962B28063292EDDCC84C13A6F7B0B26F0B252E2835A7694AB7B8C6F8B132448583F1A27F8AA9A86224648B05551E332187B296F542CDF746754B13B395B16B451625A032AB7C044AFC6C8EF548321183B695A8CE05E29296804BA0D96902B98ACDA81E98B996748385796CC3DFFB46026A434A71428531C0D7F5C05B877815265B989A16BBBC928DB6CE11CA3249FC3410EBC8EE088B7053AC44090D050A1DA73275118B9D3272AD38234A294C6A52C98AA1CAB0A2089450BB7AAC635D55D1A9099A1FC951783078AD83B771135836FD32425C16C8C5EB259DBB0F27840BA574AFEFB52ABC6091BBC8AEF6D6684B02BB5414BB39BC65378B715A492E0BFA13ABE7713D401EE930AB98E5CEE7DC3C12F1085C528DB8121552D95E7A081C6DB34DD616BAF3D0427E0CCE01E5A804DC5A100958F94C89A2FB1D88ECC13B702ADD75B5847234DBCB015AE08BEC33AF8C2967C72B389BF868211B80B3F3621279CB84739860479BE408929695B043BBC623B13B7313494111C07EACCBAEDA248DF4BFD4E480526958FB9811FF98CCC755231045C744D29A6A3103012A3C61613C3305C0A424C8F483362B5150A045888AB09E39F08FCC81B79005C1B3B0BA5FAC23BF438048C675122ACE4CA672D4D6540F9064AA9766937017B147CB05111C0706A5B549498899423B1000A27B648F0BAB03307F8E492D582723BB20B044E91CE259C611F71B7BB57E90D4A249D02A7892B4589A63EB591CC559A3477766776505689820601B9CBD75AFC344C43FD9C9FF55AA4C791DC0188B17177032796E07368CEA357874882F32948EE0B63D589C808F833AD2566E22FB02C77AA78767B194731EBF5728F1A858C6C85102B70C56B96D32428364EA7A904A7EC4F2517AFB2AAC817AED1A53D701CF16168AC2E45730D57F93C12B9CDB38D602025B1C53E4A1CEB4EBBDC9403DAAA368BBDC9BD9858BB0B53945948CC3DBBD7B2B41A97B847447402C5624D30CA0738912BA19780BC5C81A7B9E26C3C2E155CF5C1A58D26413988267738B848F20681B823E9828BE7A6A8799A1360C2298F448BFCB5C422ADB3D6CE32D8FFC1A2E52244DB9C994416F15C3B5433319C04559E3C3B895B1B322232A69029AFEF53500AC5C87A4606F5268BFAA26FD99B45C456D96E1B248BC8D2D79CBC9435D3A8B962E91A81FC76B1670ADFD12B3FF044078A72BC36C8A435B966DBA5C0CC97B71635206CCCCE2972B80C818DC43325830A02C6482CB5B2079B5AB6AC50C24E95D16C8259945AAE9D45CC66B1F125B70915C5F222670E54A5B8CCBA4F973BFCBD763BF2BCACFF5AD51A9255481925C893C630B38678A355EF40A39F56570D961846A2E6CD827CC1BA4B3813B6B956CF4AA26D7E9CCE1721E04B9A5B8D4910CDC484134730B639B57B39CB5F69663EC7490B339C61B4EE4AA83393ABEBC60868B945CFDE98933296793F74466320615F03DC4222F69461DBEA4458E2B6E1573CFF4F610F78C3AB609036641007EE369BBE382ECDC888EF97C1E1812B87964EC19B04DF5194B4049DFD2A87B63034F7A02C3BB731CB67F7282B8DCB131146570AFB993661B059797799CEB7D081CA13421BE41B32ADE2404B0519A00B88A0E088E29F846F5B738113B31C680BB96AA736CEC1138CC5F5D9C67CBAC9B29E1C1226812E1BAA39F151BDFF15037E30E1D3B9CF86423131A4FBAD95FA0585015485B5DB755E0F45D129841EB7614D6C7AFF6194061DB727B299A30238E8B716C72400828B377D46B3F2AE9119EF72AB410C97CD4B31EF77A610242D3ACB17CC2B335C5B2DCB3759A4255921096087CA0ED428AE6703CB15A9D9D8A5E0D64B85534AEB4F2076130848CB450EE55954AE61F40CC25932851A2D80FA3601C74906F1A5360A69120A5FCB4793326F0C8222969A40FC16D46F1CDB318BA511C72AB516A4F31CE67204938E68EB4F16202F42D9E095F1714BF2719C7E571498CE304DA997FEF45549B5BB9F3834209096ABB39C686E4B2C9C796BA69B368417A45970178E9801221944EAB90656B623B53903554AF1F0C7644D79B2BC186C0EA5C791795F99A0B9C29346CF9639EA892935878E6F2AAB73CCEC7201D38A26305A286BB1970BCE86033A14D3677AFEFC81757A69C07299164790473055F10A8279357BBEB31C173B87B62293BF963C453988B390B3697A3CDE4EBBA5CF1B08F78CAC7498DFBB6407D6220C376A4799AA3A8871D0270870B9495499C88C98A5762F7CCB6FB0CA10046F06C06864345CF4C5B40B11181A973E473ABB9786FF307C65CB7048FEA4885041E65E0013422BCCFB65BE8CC9CFBB4448A645FD3F81779AAB29ACA1648708F249C8B3F9CBF55C736F5746854D89A923C1CFFA781D26C241616CD01D7B372F568B50BCA58D88D01FCBB23931DE6DA2D688A6751158971A5B19C6B46C3B761D8385238F8B9831266B09B16B5559B5EB82F64B7133AD36C5497C9BBA4AC9DCC094C3673691207D3587C97197F5CF7C1DDAB6A9F1383211639F3C57BFFF3B01709974C79C9C51B2E29E91770911A52281F030531239CB049BBCD841B1D764C49E0E8492AD383A093925AA818FC0CCEA049271F307C51F3C97A7AB5A81B43B52A6BCE2CEDB2D0E706D280DDE4A0991FCAF55CD36AC05F3593F3C797A9DCFD0FD0E004FA8E0B4695D94C8D670F7E010B0562E8AAA1C46B3EDD2CF2457F39E36967B6F7A6D1FC80331296B8B5568C0B506ED3EDD6DE5E81E0F76F63F7297FB41C2CC8\",\n          \"c\": \"8C4E7C59965B9CBF50961819684ABE5DAFC3AA381F779869ABFD60C263407D4C03CE96CD714C0A62532CEB8E460D2455B84374F4EA30FA5BCECC77EF9FE108AC3EBD24AF4E60194E8FE7E50152AC312B50F9DFF28AB23FD2C9ABE4C970E7752016C84093F9D8483E93F526B1E505097BA5687C7A7FC8CC2CD0AE94DACBAAFC2D4F766CD212F6C04E23762045A1C856EA1D6F505FC96BFF1D1E485654690E7A7698A05B48847784E2629E6D81A692C93793FC2BEE69194A106E3BEA85A82FC692A0A3360D96128ABD52E72E7B3A12838C5BAA00B5A5B5DF9CFF4E027EBB7D8270C2D6183BBB10CFBDAD15FC56C7E3166751E7DC11C7DB5E5ED6CA5414ABEDD0619890A03C713C910F55526451171A826997B3D86CD9EFCA8E5276D7CA2CE1EDC0266CF876380901AB366E0EF60849774A2DE260D2D155F27440A6FC88EF99DF76C70F4D4EECB4A901CB714C6EA72D6D3BF6FE1D71988E6DD8EA001E0A55C00BF49B62900A801AF1A0CDEB6096503F70B36CD85AB57DB22536FE202E1DD796461E6E78AF79F56C332C97833BD91260540C4A5245E81C08CCB70A62EC17B0E2BBF46D5601B76E2B8AF386F7A3F210D44A7E61A4885D0981907B40F01736E9C65CC4FEFF0EF978AD93B84C6C5071A8E793EF0572F584BF27C328EF33B5ECE0844A83590394D834E225C2CC87DABC829068FC0E7549EA93EB52C3311C4723EF6F84A0C0B94B7F8C610876DE7DA886354B7984A6715B4F0C9A22F422144ECDFC23AF2858D147C26F3BBE5663F5006521CBF222748F67B01CD80CE5B6BB9DC7807C93EDA3B882C7CD5E668E40E6026F70D6E58AB760162816ADF71E7B785E67EC786441FBCA9BE950377BB7256C3C559DCD937413131AB80DDD24BDD441551CC4F1A956CA96039ADDBB919EC52594A1D8BBAC241FF615685B05850A206BD4CAE5FDFE50DC848FFA52334DAC3C9A1BA55D481C579E5CB3F9D9206DA201908606E15B494373C458785362DA9FB2DFFE91C415FC6FA81B1DD682BF0971F6ABD89B954E4C97F5986033AD13A26EFA12C5D52CF90B513C9B0604EF1676F7CB440276602BB0EA117BD20B4F4FD1BD2F05D5ED3473DB2F047433E228E92BEA1A7C602A691196CE71B1745E4453E0E7F510BD78963CC0C31D30CDC5EFDD55CAD3DA519CDC565D73DF8A611CDEBA8D18F0B7B0DF3C5678198B6F39B478903ECAA791C3ACAD02CE3641C174C35BB210A48530426E39914980DFE742E4982BD3BC043585A4D6F65FCD35ED6C5CE403EA2DFB84B1E7E380EC0947D84E85EA70D116FB5D8F4E883261AB7BBD78AC094F45FE830E69451642C4C294E20509A6F13E769706ADEED355931F1193B5E76B1106F5F0D718DEC2EF7DEBFEDE714CD2D65370629FB920E54781535ADD8CCEF0981684103065785B34045190028E4E03AD5E5D509C68A117B4A00B6FEA07D26CEE453BA12321115BE2BE279E12D6EC5F80B263347D87DD6612A14F7BE08BD1A8F3A6A3D1E78C33E71C0F64152322277EE4E03DBFB009D5A1DDD9DCBA1BEE12BFA9B39DD542710B5AAD1052C767782FFC04180478E0AAF3F775DA8F68C094F84FB2E3A56B6E0FB98C79DF1A2A83449F63A351CB9BDEB0C83E871CD9146113EB5E4B28B8723F949D6AD19A5148F73541A7CDEE4ECFCF51A8EE474B27E65E983BACAE976C0E18873795C278489EF8C8503769037CF6173F2B62369AE7C39939AA5A806F423A76BA18B258748305FCBA016A12160C2428E7C60B839E98DA95742E15C8B32BCFA990B6B41F043910D3AAB1D1FCABD8AABA55D13A713D7FD62D25F396C67D9516576E2DE37A319556698104D1560FCB5A690C4D48D0E23D24717E8FAD776DE0DEE2339A0A6CF3338F930874EAEB38C75B62DD99F751D5616973E538C641F1C47034C8C9A1A565A889A1B565FFC3D012E16457CAC00856BFCCA7305CD90E295E98F26F8FBC438D2743D0849FCF0A05DAA139A64837C8FF56601475A80BABEF0A62671D4BA8F7A2D3B836DBCDC49CCE8685FED7C9B27ACF203687176B86D316C4EA9C549669363CC6D76E99A8E3426F34623224E7707DA7D1AECA02E19CA0985F05794E239BB8E21843D49C4C2F8EF59894A437BE6BC4C67838EC5DEA6D09DCB93B914D9E73628155D87C90E6D4B3D3DB2F31513BF692313E28E452BF205ABED9D7AE841AFD11B7483389F89E14CEC729A3CC69F0390A9F2F\",\n          \"k\": \"6FE2B3BA0E972312DB3DBD84F9B8E1D7E3AF411BAB1750D75E7374BAA6B25F45\",\n          \"m\": \"ACA147DD83685FDC5BE522178384DDB0C8714D0F818A5A20CD1AAA71730D8E36\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 74,\n          \"deferred\": false,\n          \"ek\": \"9EBAA37C34B6F0C92BC7E98A0C56689C989E33E9126DAA3D94C95C6D60639DDCC008E4220F8C6348E44738C1AC50521A926B9BCD42511A33C1A704871C11A962E062B9625169A842F8D15CF3F3A0A2411017860C88A22D01262666F6895390AF652204ED9CB0F7589A63C23ECA851C25B527D242AC8D310B3199AF5EF7C54472739E17A6986ACAEE76B0B262C5BFB051343580E60150250865397B4C498B465B6C62E1C3A4B89B756871313AC1C4034509803C59103AA4B9A50CFC7C7EE0A34EAA417009538570C197D04C60CFB41578E274AF9ABED10B4BB7B71A4B925DDCF5432940934B222A36BC80E96C1094E5C264364660012514444ADE22A6121C30C4F037FAD29C2819350700A10C9861FEA471BD05C3D31A2EEA2A9CEFABCCF0B4AB9C33C19FF126B017370073561857084A83ABFA5B3550C2A93B785A4223AC57F4B6FCA0B4D12422B509CA45B63775FBCA4E785972E775E2989D844A4308B786F230CE990010541868C2C21B3D311E6AB14C4D417B37D10DEEBBA6309686896017F08C224DC806AE87658A0B00ECABC2A9F66029873C6C3446914C5A967441BB4AC6900575FA52238636707C69097E19C30ED66A9F28762056585C5738A6F51ADE3603B963476213B78EF65AB6CA7F612134E82A6BECF48D4DB4AD7958787A377483F0A8BAA76B06243DA4F61CF803807C17837A6293D144637BD3A92039282DA86485B005767519C9C0A0B513CA5325866DB3AA67638A2C0117FC80C4845ABADCA92C529C539431363A9A58E6A25CE6A42AB8E5CAD3865AC1B69E77B2B48DD5CFEDABAC79257D15269D72B1CE90265556DA687D894D15F11B1268692E22AD8313877FA8629F3BA224F8C5A98AA31FA58FDD05101C3705496516970B200F984B67EB497DB9AACBD6922EC84C73DB42EC0BA4D1D11859DC356765C82947B4953ACFAECB5969D34A4A8A44BA53346023517C2474A301AE7177A0253904186A5527772CD7845A4129A90B292051765772C09EF585A5BBC976F1F9B24C7AA4478B04B47398491785D9B9B9376B89D6C90320831117949A72406BE6076B4268A356E615A2168C9EEA37B2165B143304B9C51B3CC97AE3A6B0DDB185825C8B7AB073B5086B7D215D4C953F7BC9863339A045DBA784F5489FA51096234DD08B7C457B1C326B8F73672881562310E90B43FA7A37B7550696422A1B32A2615D4E8C5B3C3C61C4479A15D94F210A165311BB4C95060FB8287BC526C212B1EE0470C5497480DA396B2C39B9BB413E267583C249911029DF9B7822327764C16FA67307EFA499F0A7CADCE8B1732C91D284B505D59A444B41ED738C09A40F335C7F7050B80DAC9E5609C000473E440840D1799A86705276761E8CAC3474C1836AA9C6B4D732118AC515EC2A4DFCACB7929776501EF02415A0638819582F6AA2853DD189CF7900F834671718221933C2962B3FE598BC2EB0245C229B2ECA5B1A1A2F94BC6E278875570BCB92C043493267F7DB06C887BD66E38CDEECBCD0A998FFC094376119E4A961020C8BA05B15FE06062FD4892DC0C023AC6B2806BF863ACD85A120A0701435305A65E91D5D908B0BDBBCCBD518BC9481A5920BB4900468A893589128845BC95294A0DC5918274C4FDE2A0B18467699D637AF4CB62DBA3735E33DFE25AE1493501A590DFD0342506B748EFA5590BB689D18A50B2151C9E9B559B346E8E530A14A1CBE5AA2843431CA32AEDEEC4E9081B50A3BA764B1B0EDCC99B17B3FCB3046E6C2C1B1966F2F481E4E1CBFA4E7546E0A9E6ADB649D59151209942B92B019E8394416C19004CEF1E9235BE0228E6B2CE3603A37966BBF51CA366A3F2D18B931E4B3A827887FB3147331A746E713F60040F3277CEBC2BDB7251E84EBA5012A10CCE965E4A9963151698AF1492CF51372C37AD0D78F817B466A26372F3213873802EE26558C9ACD0F511B3F9138EDCA56FCCB36392ACE11411EB2936CCE03A0F2F347E2332E14E85B8652984D09CCEFEB8963E2A5EF845888576EE1B36EF362274D292F10355163BCBD51A7AD7B284BC4A10C3370A1391003289016F22310FD3C4CFB31CF88A42685103F3813393C1A12915981C11A616FBB8DD6905712F387D279C24B17230D32B018E14902CA1BB0561CCB73A0E2F4C8DAD9969745A1D238250B76FAA9FE97E3E08E3BCBB6E860DC8EC5EF30B92C5648EDF353871C148883FC\",\n          \"dk\": \"BAB28389C4A099F98AC074612F487570115D0A031BEDC16B46D77C7493318891B30B259E7284749676A76B1637CB6CBD842260B4BB9FA1230DE3E151F1E5170A82110BB2B1EB992AF9E2BA98BA4AFCFA3E71765B9346412E10998BC8461232AB5AB24A16F23BC358AFE01A1C7F8A3B05A8AC5EA9591D2689B56466438761A6220D151682BBD0CD23C42B998A1871352C1C2C0AFB1889A47C0B0FA8B2768A5ACF1C4D0B34BD7C16CEF3611851432F361A9176D77ACBA7C88C219CB98819552279819B6BD6C57BB93AC239DAA6A8D53F9D1665F8B866ABDB52FDB1BD818923FFB803C5A8B61CF9B9750A2F03522B4DD5AAC7FC345CF28278F93B1E976A381818AC2275AA16CA44204C6513394439A75D7B1E5970735F016CA6045FA4E68F4A9C31F9C4766C6A56DCA1C83101346B3A1BD796170904398A859734B145933660AE905433B40A8763AA37A868C2E3A33ED709BC9CC1F4411472B31981F6AE08560C68759966B047CA295FAFB60485A28A8181CEF7954C78BC85F395952583CC026C46D9A540E8640B9FD175F4702E5A3A131503C3251097C93332D5113C57082B7CE09A9DD271EFB7ACBB92257C0B5B50EC7BB418B83AC53623AC6B7109C713D0C9C79A750A603B2B2714A109545FB4506A474BBB7AB8556462CA596741E9B0CE3A7FC63A49039450B41675F6E0AF517623C060218C755CD3702FB4A44DB91A80F50CA32F3AC56555B8EA0B26C9E8BAB210889873470873CAD9D7346D8253E2268BCA725E900C22D72318FF686EB9CC80616315AF936F6DC7579748094BDC1301B4A17E826F60A59519B66C2599B52E963054906AA85025F1E26E85CBCC2985ABA727C77A12794D7A67C82B8B41D1110C63C3DB43CD4684001E486E18789B4E207C34D3A207674EF842C2DE7322FB400747B3246387C84F18608DE182BA3C79C2414EB60A7F75040301F32AC543334EC028C0F92914089602D15099C696019A625D357D91E6BC79581F463127A2641060460F526025A0C020352303BEAC1F1BF53E7B258744813308DC69022A37B4D72969586DEFB883A299CF814A98601766CB861F97CC7321764794D1AA928B081593AE3FAC6D3BB71ADD53C52C21B350FB3C6BE59AF019C0384941B8932C673211134C34BE8C8697FA038C90C09730B3ED556118C455B6168C84E3CF5E1AB818C02E4F00002099C78D4120252B9D86B4051A7C446E5509C5839B1023A6F9A9947AFC10AB1764774B9475D941104278AC59C87A18C1F2B17FA0306068F291D0F2CE5F5069B4A3055118C60DDAA96AB0826FD374B4F8B6DDAC28BCF6398FDB9393C718F0BA2775E54F2556CE2C2678241BCD68A00B81B87991ACBA1D9003DB76AA36075A0131023A373C51B065E2EC083F9A3A5CEB22D246711346A357E94E10C199D7A09C87D1188D1C3C0D1134DA650D5E479051B140D4A0B49492C303A40FCE4457C00A023202B81BABB699845EC77244DA184A495196446C96C0D3A0644710225A744E458782D38C63C459BF2A08833755C7066396042479874976C1512BCC046C83786B4424C2D022BDA1925DCA930067AEE3E6454E1C78B1C5A7C603B85D506892852C9D39AC7808CDE5CB71E371378042CA0B5C35414BA7A64294DC42A0463841626A5BC6F37B98D4393BD9B9E5E020B7849EE72188CD1231DF530E36E4CD72580E05359A8D97108CB82E9D1B7CFA94BC92FCC174F3A4767CB1D6FA719E202D8AD68657A81ECBC0861B645D3A781A725A6552ECC12B64CB12A6CB0AE33C64624A8D28B026C482AEBC6A69D95557FC3CFCF4B2FEE502CC7210B42C089AFB636AD573E7430A8A184685465C6B882987255430D2842BA75E892A5AE23A3A636840E6B3CD9F76975EF41C034CBD799412584CC85625208855083BAC67D85B0FE011568ECC7365798AC3EA91C61442EE260BD380C2494680FB411BD16714CBB6468BE41013F724CCA4B4FF364D095422124652D4A53110C05B89B095BDD1C7B8D19689A706E1DC656E6C1A3FC767A1D44EBD67C5B370A0F5925DC551551AB6B604A27F56A19003731DC071B2E98C86F7BA9872DA783DC68E1B19ADED603CE7F3BCF806A7774A044D9C51B26C9DDB8B809C0CC5F8D0682C7C8A56A9C89F412DB5BC4ED438AA89574B8A77206396807FC431F9A2B29EBAA37C34B6F0C92BC7E98A0C56689C989E33E9126DAA3D94C95C6D60639DDCC008E4220F8C6348E44738C1AC50521A926B9BCD42511A33C1A704871C11A962E062B9625169A842F8D15CF3F3A0A2411017860C88A22D01262666F6895390AF652204ED9CB0F7589A63C23ECA851C25B527D242AC8D310B3199AF5EF7C54472739E17A6986ACAEE76B0B262C5BFB051343580E60150250865397B4C498B465B6C62E1C3A4B89B756871313AC1C4034509803C59103AA4B9A50CFC7C7EE0A34EAA417009538570C197D04C60CFB41578E274AF9ABED10B4BB7B71A4B925DDCF5432940934B222A36BC80E96C1094E5C264364660012514444ADE22A6121C30C4F037FAD29C2819350700A10C9861FEA471BD05C3D31A2EEA2A9CEFABCCF0B4AB9C33C19FF126B017370073561857084A83ABFA5B3550C2A93B785A4223AC57F4B6FCA0B4D12422B509CA45B63775FBCA4E785972E775E2989D844A4308B786F230CE990010541868C2C21B3D311E6AB14C4D417B37D10DEEBBA6309686896017F08C224DC806AE87658A0B00ECABC2A9F66029873C6C3446914C5A967441BB4AC6900575FA52238636707C69097E19C30ED66A9F28762056585C5738A6F51ADE3603B963476213B78EF65AB6CA7F612134E82A6BECF48D4DB4AD7958787A377483F0A8BAA76B06243DA4F61CF803807C17837A6293D144637BD3A92039282DA86485B005767519C9C0A0B513CA5325866DB3AA67638A2C0117FC80C4845ABADCA92C529C539431363A9A58E6A25CE6A42AB8E5CAD3865AC1B69E77B2B48DD5CFEDABAC79257D15269D72B1CE90265556DA687D894D15F11B1268692E22AD8313877FA8629F3BA224F8C5A98AA31FA58FDD05101C3705496516970B200F984B67EB497DB9AACBD6922EC84C73DB42EC0BA4D1D11859DC356765C82947B4953ACFAECB5969D34A4A8A44BA53346023517C2474A301AE7177A0253904186A5527772CD7845A4129A90B292051765772C09EF585A5BBC976F1F9B24C7AA4478B04B47398491785D9B9B9376B89D6C90320831117949A72406BE6076B4268A356E615A2168C9EEA37B2165B143304B9C51B3CC97AE3A6B0DDB185825C8B7AB073B5086B7D215D4C953F7BC9863339A045DBA784F5489FA51096234DD08B7C457B1C326B8F73672881562310E90B43FA7A37B7550696422A1B32A2615D4E8C5B3C3C61C4479A15D94F210A165311BB4C95060FB8287BC526C212B1EE0470C5497480DA396B2C39B9BB413E267583C249911029DF9B7822327764C16FA67307EFA499F0A7CADCE8B1732C91D284B505D59A444B41ED738C09A40F335C7F7050B80DAC9E5609C000473E440840D1799A86705276761E8CAC3474C1836AA9C6B4D732118AC515EC2A4DFCACB7929776501EF02415A0638819582F6AA2853DD189CF7900F834671718221933C2962B3FE598BC2EB0245C229B2ECA5B1A1A2F94BC6E278875570BCB92C043493267F7DB06C887BD66E38CDEECBCD0A998FFC094376119E4A961020C8BA05B15FE06062FD4892DC0C023AC6B2806BF863ACD85A120A0701435305A65E91D5D908B0BDBBCCBD518BC9481A5920BB4900468A893589128845BC95294A0DC5918274C4FDE2A0B18467699D637AF4CB62DBA3735E33DFE25AE1493501A590DFD0342506B748EFA5590BB689D18A50B2151C9E9B559B346E8E530A14A1CBE5AA2843431CA32AEDEEC4E9081B50A3BA764B1B0EDCC99B17B3FCB3046E6C2C1B1966F2F481E4E1CBFA4E7546E0A9E6ADB649D59151209942B92B019E8394416C19004CEF1E9235BE0228E6B2CE3603A37966BBF51CA366A3F2D18B931E4B3A827887FB3147331A746E713F60040F3277CEBC2BDB7251E84EBA5012A10CCE965E4A9963151698AF1492CF51372C37AD0D78F817B466A26372F3213873802EE26558C9ACD0F511B3F9138EDCA56FCCB36392ACE11411EB2936CCE03A0F2F347E2332E14E85B8652984D09CCEFEB8963E2A5EF845888576EE1B36EF362274D292F10355163BCBD51A7AD7B284BC4A10C3370A1391003289016F22310FD3C4CFB31CF88A42685103F3813393C1A12915981C11A616FBB8DD6905712F387D279C24B17230D32B018E14902CA1BB0561CCB73A0E2F4C8DAD9969745A1D238250B76FAA9FE97E3E08E3BCBB6E860DC8EC5EF30B92C5648EDF353871C148883FCED356581C66D62FCFDA63A9ED071F7E5EE0A9AC9DBF48E4653DB78A6BBFFF1919399B9CE712E51B00A12EBA403E181CDA45AC150688E2D09614014661B339E6F\",\n          \"c\": \"E9FFEAEAA83250A944F247022FA3F6572C8496C0AFF81462BFDDE2FF310DBA5E6820FE52E33C2CE7BF0C8DEE97175E20000B3577BF84DFD69BF45DAD481E94AAF25BEB959CEAD9DC539492202E85AFB680165C8A1C2BA42490CEE563E4EF4B821EF34B0EE08C0ED8452235F99E123B650985D9B1477D21F936BD937CAF67343FAEE1EB684EB4E0EF202F5A58445DA5F6D5D8AE7071BD531C0D736C2570F5FD593FAE9BCA5EF988B6BE44A323DCE5883806DFB7678704004B228FCADE75EDC4CDB7F4F87B0C315FC9A40B4AE1E6944C96CAE75481907804BF308780F411FE068CF09BF99AE3FEC2F656C9CA02158694E5B22F044FCAB131B7D942BE6ACA98128CEC79DAD0AAB4BD566A2F041AA043520B9D8642C9B744D4155F926DFE87AA8A031BA5E54090A93EA5091D9938C6C3CD31F83D2BD46661FF339E66513E284DF7FFD64A047F741DC81F46A6A7F03A9025D554F93D889A5F8DDD75B3C0F31DD64CD4218050AB496C5D01E632D35981237F248D7B31C6F39678D4FED7DABB29C242699F2F588D2FC56972B6EE7A94C1FF01584F56B86BC3C1B58DA0B9616B9C5E316D7FD7AA9C22D51F0BF69A080E595D794B5F0924A4448CEDD0E03B414B91D03FB511AEAFDD5CBD4FDEA6CBC0C62849B2CA7A6023251CEB720E52407D03C9412F0C87ECD974EFE304C7791CDE5911F7731EBCBE969F039C6C3FAD7138CAECCE0D3FBB5F47FAF77B80531D53CFE23E1E04921864F5346EC3EAA4B663512DB4344A5579BC4CF32C8FF33C5F32CF44238608C19EF4074CFF9ECE4265084C4515E919E2118FE535480CA816E4AD6632BBA726D9B43DA4DFDD2DD1E0E85D001BD9FBD52BD9A6A8548A41251F939A63E683CC5A076FF3F0A2C33B223AF28997CCB36D3577FD09BF46F5988C0042C666929B877132B0D5375210D20C7F3816FA1AB81F3699091874E0CADBC1550BD6DFE24335EDDA44FFC421C9D89D9E250562408BEFF06D01719CA54FE91727CCBD08FB4B45BEF75B08F560B0FB9618103FBC216A3D5AE72A869BD40E51170E88DDBB713F8A0531DCC2645CB185F2F31BEEA7086B5AA84F5248B2014F9D85E56B330F119F88E26EEDF055A5DFD87582E91D3FF59FAF9AF3032D60424DF0DFEEC98F2BCD2702A10551E7DA73628D4F6E9065049B172AB861EE64176E21FC7DFEA1208A632C78D72DD2409798DE854C3EB67A985A4C04CFF154A69D8443F6AD9EDBAA666CD29260D203739F3B77891DAE26F03DD469CE4C55377B5F3AB9DA1A42F7F8F8AA944E7A921A53F7ADB99533F48D4E9922982FA0B149ABFD95095CCFBF3CD719F59535349E66653FFAC1D0DB3857688DA952426D6AFEF5EDFAA7732133FD9CD86831BE5A7809E209B5E1214FCEFD775D3627511340F53976B571BA909BF5234DF037C8D1E4A11BBBC8DBBABCD39AC47CA6CBBF38C7326CC253FC59163FA313CB86DDADC05AA6CEF529AC23FA315734922885BAD9E5CCFEE38CD20AD2A245590CAA674AE6195F4938A95F46E59CE060E7D7960362A7C19F19DCE8424E8E7FF5B084BC093FCCA9BBE9228B089497D51BE411AB06CC60301A510799539287BAD4E2FE023B6B29386349794AF3F8165AB8F6F4BDFFF114B817B10F4598355E7AEAA31CA73F8F06FFB478483EFFA2C11C94C10E8053E95E041ABF346498F1A2765B460DAA89EAF3EAFE952E35B10630D78FF742977F2F5FCB63712901FD9181A1C97E0918DF81FEBD517E6820D70A509C15C144CDF6D1EC37FDC7CF2353A3CA7136071DF8777E45D5D8B5146C058666926096F51025881DACFE2392F82019580BF82E998CFA5AE06A9EDE3FEA2B4C6C1FA0D02822CFAF4F2452AE695AF70264D5D7935FC19F2917090FAAE36790032CB85A5E875C05D0483B2D4021892E825A2D0977724DD1E73BBC8C430A0F6F4574AE112575F87AE30BFE94EE8260B5141A9E414739B518C11378A7C574962F0DFEF8E2DA1C1FB8B4E502D182E14F64A91FC55A80D65183B7B592FF077D3504B9AB88F6CC5F4356488DE0DED68A1C469AD9556E2B467187EB2B0B466DB99DE24BFEF3D9944B244C022F6977C0CA987BA8D0D10944CAF8AE6B89C226EE50E338F653A641EC39DB3AA834E25576B0A5853A7BA26E59D65031DC780E8588EB936D182D988BB77B4C9276A737E44CA1A27804687E137A84A872EE1970448BFBC7C42007E4518FAFDB544F9A\",\n          \"k\": \"7BCDBA65823F7A36497091555C7E558D933E707016AA485708FADD30EFB8D8D5\",\n          \"m\": \"B974689F6F36C7AB262C8B97D5469ACD3BCAA3A3454F611FF0B304FE1DF6C66E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 75,\n          \"deferred\": false,\n          \"ek\": \"A6902B0559182B341E3A6ABE0B646DCC84B39881948CA396CA782A65D6221589333E98A88A9C320D83A10422948B64A0DA1C4ED165B0144B22190656F1DAAE36A5641A4863A3985AE9D08456629C153A596289C01BD2912FC3603EF7ADD1EBAE1A745675F316ED0A6D4CB5C5A431C2871B939B5648D1018911D752BF08B40CC571D863B33C9043E3B2C73C67CB9A4C8087F28D7D37B9B66B638CC66796C146135C2A743AB94FC693473089E6D32492842D8D114C9A92C6721818CD0530C91A09FD7C3A124B7E98DB056C77049008A112AB61D03A8C38738B96F64D4E530E5345958EA38F19517B7293B6D2DA11BAD668988820CE8479C3B5A832C708FE09481CAA7ABDAC2E8658544067810AA31D88A52F66459B3E0A5AA8EBCBA1B5C9B8776FAAF59CFCAC69A1CC8EF166615998AE88968FABC67D67C7675B94CEF447667FF53D2B3890943303BC88C5B3DB08BA75187B6CBC97503E9A563745824A533C59A71B66D5CC21972B843BE0C0768970571796AB992B39F43F2CD8017B5517A91235474A821C541B526B0404212C194238B18AA5B8A175E4A59417D119EF8A385B7458980335842BB318214E55794C635692736B6708863A066462EEF41ED66771EAC578E029642954111C0B20F9BA3B8646456DA39873B89F6449324CBAB5ED017CC1C72E1ED4322D55B2030A8E0B390E54384449096A2DE676BEA01F03A458160341B7C743719B89A158120B790AE8E1AB4468467C4507F7CB659EB8C16A77315E553AB287836C4472EA1075C7A1C84DB4CABDD5CEB2CB5638D75FE3567659443B38B2B21C422DF97A14A8A633C2279DDB056302AB0FA83905C846AB57D2120EA1CC92195320B5B873B406CB44B73FB02160200460431581979967FC16233333A8EB7D48A120F451B3FAE9BAF6318E384B6D42B36D5B38188D849652935494C4BBBDFA22A562A27BFAB60B3455E775912D27698AA30F76CC291107629E3627EB443D0F00AA3F220877010876B485F57495C9E80DC4F3AFD6118E1849ABA06919968163CC9CB358DA03B3550198A2C43F476954F1557058C05EF828D8EB982340C9BC6A5AC1A27977402B2242A40D89072C1270DD8772008A505D4604831C1924B97C980B01788107472800EFD807FAFA1ED3466548D698CAB76B820667D630C7D03447820BB8815BBCB8DCC11752788782CA3BFC51DB9BA0D16198132A0ACDA7BD7A6531A8AAB9C8AC2770030C512740CE62CB60F79F8087CC9177B56E6A4B8155B854993E2BCB78AA849E72398590B24E87A31097AA54ACA80A9CEB29BBC5B24CBB7DA084B76B5C522F73185A05A8024629F8A6AE304CC7FE88258C94A396912A16B491DD0843D8A82BBA86843F458E91261588D7AFE89A1D7EA9786DD13C4111CF89EAB9118A7EA39A72408C8F1EA2C77765C12881192A33C552D61A76424783E5C48C881E9A541B8F6157EC138AD5E93AA925194FA37644F397A98C60DBE786983C8B2CB6C76C73CCD0371CF0CA85998C65D1F76DD5E545991A00567A48EE69A537921379923D7676401A06A81D68BD9FC4C2A28B4B72C966576BC6DA870BE8C415F94174F8137B3EE4793786652EAC97D6C91DBA4749CE4B69DB82209B24C5FAA0C4A0CAA71A39BA51E7A6F19381219900E6F8AF06F0B224326EFC2C8560A95AF8296D2B87353360A5AD3B6DCDD95377B0B201EA7A6E475D5D563A373554E83982A35503E1C179324276F6147B698C32AD24AAEFE0B2B1FA911444808C6A25CBA21B4BB008F49C0CA8D209E2BA02E3B164DA41793C212F9C5B2E0EB00CE7146284C0B26FF940268318EB330C6F0100ADD43FBF0615D2EBC27B0819CDEB6A327C590DC83D678AA4BA910E509B4EBCDCBF3B74CA581A2B55F46154D17F9AEB3A09D5C30356A75AA2859BC11DFE866E3C463C96826613880470A1CE8276B033C1810BD645B5278C25F81B678228F4D20FA3726C5715311DA4BE1F1C0752DB6D16D889D27313C4046E05FC161E889DFDE2255F434C8008AB12167F3B6A15D74B4791B293C902CCC6F4C98A75344864C35E6B189C8C116735C7636A6F0AF653DB17CBE0E3CA7C677CF9604CFADA8031611321DBC90308BCF7F93AFE1B02B0C9826A4139C4B2097CD061BCCCC5B3C562BB13A30CDBB318CB9457C1B9A69236B820AC0717EB65554D8CBB46EC883E0392B79C92DB8D07FCD393633C020FE2C469A32D\",\n          \"dk\": \"8CCAC9928C0FBEDB3AD6A04E1853BF4689B035E0C8EE7C26BA2140CE1CC8476960CE51732FA0310D636D06D802B164B826F376395AA0D371C9D56275D6AC5878D13655083769E14974B22FF9B02C5D1370332A47626702E5E2044E05B597F94FBC429E50E65B437223F32662A1C37081C3B7A884B37DECCA999C1EC3997F38696A48880589805E965ABDE6558722F5481666120D4C6B12447593399CFBE881BFA754B57350CB060A80CC7F95C65CC6905F6E701E1DB03A38CAB8ABE9412A990DE094C99A514C68969940B5A82776B6EDEC32F7DB9A5D7417F4012DB6F36AD8D8967F2B144488483779AAF36188A7F27B9FB8A9222A02411B110137BDFB835040E220E19A8B2F8A25453286561240C9E02A36591B6829BE48177E086599DFA003D951CBDB4B24313CB8D5FABF9ED54140FBA91E3921BA9184FAFB5136D64F22A2B59B27116DE13893AB6FAED8A1346742F838641DB76912D30D2481657194CE3E709E9AC272DE638C21580CAE015CB04A89E587CCEB0B05A972407F9C96F8474A859344EE0C31A2D30AFA74948FE2B6A4C26C3CD2C9C0D7400B802AA36487F6C216989A5F806536853B9FAEA2C2A91C039CD07A4E435A58D80DAD50A991150B1A1B0042818A38F6929E325B6D29C70BEA1877D73403DB08B856C10C2851D0E30EC6BAAA9DA6974851CFB31211EA282B8ADBC7425274D1BCAA806652344BB663D7297304192E06AA53A438A2D147DB58A1901786FA9924216923B01C1FAD85C0B79332E9DB420A94964CE9BC2CC19276615B7FA3A489DBCBFCD07412F186ED412CC866270AA53A5E645ECDE278ACA36BF5381DE2DC57C9B812BA13863E55BCA68C76E1F496F216663E877AAF2C1959397AB9B1BC21B1A1D9AB5091484780F58A399C058D17AC7653A288D27E18D43B4506C69C477640069446A5B00F9ACD93200E21B40905A09035886B11647F1182BC6F824CF7A5B1D1951FD9049690450EB559677E888BAB0924DEF1741E22B610894109CCB549E4A5EE0A5F4C761E6A293F61EB2C5739B659E4B5086A0C1B4A0382B27980B07C1D2035F1DB53BB82787AC249B2011F3590CD9FE5515F813C65953AE46CCCB7C3210E24AF82B16DB5916A6B23393844636C278641C48CEE2276760518C919AF3E16C3C9714D1544CED4D2A64DF482CB4794B8A77BC5E6CA2718513E6213F620BFC8063A667CCCCBC99C7CC1328A5CAA31DB820A4088CD825313D677C49419B2692E00813E33A0B62604CEB47A547FB2A92B63CAFEAC4B63A41FB1805CCACACDC62C373EE63C49F09899929ED85C89D884287ACABDBF717F3C49A511DA53DB43AF1AF30CAA7B09E0D760B66C805382C427C3BB77F24381D71F691CCA9DA8AB1FA56695E2CAC9B543B1F5C3BE2749A804928BC005F0908576FC87CDB88AFFC4869105A1B5C6364A64083793A4900434AD7B9A0196BAFBB3B54B7A0D23FB8440A65170296008BA3A94A94AC5AB36F11507F6B7443A928F592589ACA7C6AA5BA6932840124A7DD65A20F72A5D8CB356846A1D670CAA2BC06A4D64C4001154CDAACCC7F21CE0C1C877B2556833165890056D929510D4252A065EDFE2152FEA9876210DB26B7619F220BF5C4DE7D05AF2530E47FB43C6BA7D385482998994024C04D576AAA4AC2B1A951A01596A0EE36F6D8B0BFE0A5800EB2ADD9758DCCB368B701EA6D4B9605A11E7369E44F67DD499896CE297CEF763CFE99C14D41E11913337714C6C79084DE7C37AD76C7A838D6C40B485034C6F55345EF729C0930397166AE317AE5FE85F4DB999B3842387C245EAB7500A51260097CF87D77C56B86009F15CA8B60B624011086C229CB73C6A90A81296B4216145FDF76528DB8AEC671E7062AE1105214F01770BF98477F9098482AEA044B92B6AAB0C70480759108B4AABDCA580A1C640F32826B7CB4734D2743DB5090E8B97C1AC46E3F755AD8BCA6D9A93B414B7CDB15E81D9017CB1291FD263585A02E2EA555EE176E8D29AFAB58AF189C5480199409CBE7D871E42EAA4702C7647D023FCA094232ACEC3B994B1B93930CB9631E34C3BC6BD71116FBC751C872A5F7435376E05941D9BA80BB520EB9A6ADA4174B671B682472F3EE9AA57579265190D618575E2B996C3A1A5AC868B96339BB0024BC4385C96D1B5146871A6902B0559182B341E3A6ABE0B646DCC84B39881948CA396CA782A65D6221589333E98A88A9C320D83A10422948B64A0DA1C4ED165B0144B22190656F1DAAE36A5641A4863A3985AE9D08456629C153A596289C01BD2912FC3603EF7ADD1EBAE1A745675F316ED0A6D4CB5C5A431C2871B939B5648D1018911D752BF08B40CC571D863B33C9043E3B2C73C67CB9A4C8087F28D7D37B9B66B638CC66796C146135C2A743AB94FC693473089E6D32492842D8D114C9A92C6721818CD0530C91A09FD7C3A124B7E98DB056C77049008A112AB61D03A8C38738B96F64D4E530E5345958EA38F19517B7293B6D2DA11BAD668988820CE8479C3B5A832C708FE09481CAA7ABDAC2E8658544067810AA31D88A52F66459B3E0A5AA8EBCBA1B5C9B8776FAAF59CFCAC69A1CC8EF166615998AE88968FABC67D67C7675B94CEF447667FF53D2B3890943303BC88C5B3DB08BA75187B6CBC97503E9A563745824A533C59A71B66D5CC21972B843BE0C0768970571796AB992B39F43F2CD8017B5517A91235474A821C541B526B0404212C194238B18AA5B8A175E4A59417D119EF8A385B7458980335842BB318214E55794C635692736B6708863A066462EEF41ED66771EAC578E029642954111C0B20F9BA3B8646456DA39873B89F6449324CBAB5ED017CC1C72E1ED4322D55B2030A8E0B390E54384449096A2DE676BEA01F03A458160341B7C743719B89A158120B790AE8E1AB4468467C4507F7CB659EB8C16A77315E553AB287836C4472EA1075C7A1C84DB4CABDD5CEB2CB5638D75FE3567659443B38B2B21C422DF97A14A8A633C2279DDB056302AB0FA83905C846AB57D2120EA1CC92195320B5B873B406CB44B73FB02160200460431581979967FC16233333A8EB7D48A120F451B3FAE9BAF6318E384B6D42B36D5B38188D849652935494C4BBBDFA22A562A27BFAB60B3455E775912D27698AA30F76CC291107629E3627EB443D0F00AA3F220877010876B485F57495C9E80DC4F3AFD6118E1849ABA06919968163CC9CB358DA03B3550198A2C43F476954F1557058C05EF828D8EB982340C9BC6A5AC1A27977402B2242A40D89072C1270DD8772008A505D4604831C1924B97C980B01788107472800EFD807FAFA1ED3466548D698CAB76B820667D630C7D03447820BB8815BBCB8DCC11752788782CA3BFC51DB9BA0D16198132A0ACDA7BD7A6531A8AAB9C8AC2770030C512740CE62CB60F79F8087CC9177B56E6A4B8155B854993E2BCB78AA849E72398590B24E87A31097AA54ACA80A9CEB29BBC5B24CBB7DA084B76B5C522F73185A05A8024629F8A6AE304CC7FE88258C94A396912A16B491DD0843D8A82BBA86843F458E91261588D7AFE89A1D7EA9786DD13C4111CF89EAB9118A7EA39A72408C8F1EA2C77765C12881192A33C552D61A76424783E5C48C881E9A541B8F6157EC138AD5E93AA925194FA37644F397A98C60DBE786983C8B2CB6C76C73CCD0371CF0CA85998C65D1F76DD5E545991A00567A48EE69A537921379923D7676401A06A81D68BD9FC4C2A28B4B72C966576BC6DA870BE8C415F94174F8137B3EE4793786652EAC97D6C91DBA4749CE4B69DB82209B24C5FAA0C4A0CAA71A39BA51E7A6F19381219900E6F8AF06F0B224326EFC2C8560A95AF8296D2B87353360A5AD3B6DCDD95377B0B201EA7A6E475D5D563A373554E83982A35503E1C179324276F6147B698C32AD24AAEFE0B2B1FA911444808C6A25CBA21B4BB008F49C0CA8D209E2BA02E3B164DA41793C212F9C5B2E0EB00CE7146284C0B26FF940268318EB330C6F0100ADD43FBF0615D2EBC27B0819CDEB6A327C590DC83D678AA4BA910E509B4EBCDCBF3B74CA581A2B55F46154D17F9AEB3A09D5C30356A75AA2859BC11DFE866E3C463C96826613880470A1CE8276B033C1810BD645B5278C25F81B678228F4D20FA3726C5715311DA4BE1F1C0752DB6D16D889D27313C4046E05FC161E889DFDE2255F434C8008AB12167F3B6A15D74B4791B293C902CCC6F4C98A75344864C35E6B189C8C116735C7636A6F0AF653DB17CBE0E3CA7C677CF9604CFADA8031611321DBC90308BCF7F93AFE1B02B0C9826A4139C4B2097CD061BCCCC5B3C562BB13A30CDBB318CB9457C1B9A69236B820AC0717EB65554D8CBB46EC883E0392B79C92DB8D07FCD393633C020FE2C469A32DF3F0316106B102E875DF4653219F35BEEBE7CFD5F0C59D1CF68055C61539BAD1231FA46366271E63D3B696A0FD4569870859EFF47CB57C3C6A65B22253A739FB\",\n          \"c\": \"3E07145AEE491606A4DFBBF9C7301FB8F21A6F46F8F87253346A5981C7D83EE23CB6BDC508AB0756A8E2D8713A03275A551C0B291DECBF6C0A3F976758ACA963B590FEE44E8D1056AA95AB5D1B77A0016E3AA605EB564337BE2FB33E54054A08C7A3174E8E7FC0F079B1BE8C30BC0FA7C03972DE8294F9F24251F834711C0BD340C9EE20BFC74CF99E8C0CC8AEFBB057B0F7E3CD0AE6E0C47EF67F22C13C2B16179942D8AC24FF81D99CD9C5ECC5065C0BB0C4A9B36FEAB42B2F06A6A0F9AC2FF4AC50864C6D03CD97F785B7B3C521392E246DD0D5FA5218EE1AC30A223194E5A21267D1DBBF4DDF1018858D69EBB382907597BED3D90936B5C039DA96E5BDDDB8A5645EB1BE21C1504221067B293B4C6C81EB983CD49B5A1DAAF7DB602E990DEBF76613C6111B3FDD2ACA243C3B92D4E6988BD43082F6339A89898FA0CC05C1859DF99EE74F3748DA53BA99561A5F5C1EB1544A314343FB9167EE9E822814A6CE530836239DB515A8582CD9ED338B2A4765A7C265F0825B1DFC6E6CDD41E137C5FDEBCDA6878433EE1BAFFD7F64020D9606E397A12AF66253E19EE2CF4115C173EE73535DE0DD5A7EB7E2EBD769362982F9B09AA5D6548AE9163D0ECBF4929A950853069AECB829AF4F91A517C8E8D2DE761CC9F5729931E4396D261DDC3C66350B20FA1B37ED3BAF2092F7BE7C85DC1D73ED66A5C7ECA6D6ABA46E09B03102D0325E712699DEB28426AA5309D8892BC767BF099EFEE2481A589CE304427D9FB13A65913FBB37C039C9390C9E9BA3988A81C98CC60014117CEDBB09234FEE8529B9C3CDA11292EBF1678BB9B2A76C5CBB43AD1F947A984348DD8983509D7A3D3B3A560D6337CBE40D32F554C24E10BB720150D4440B630492CDF711193498E4CFCF3F8983BEC12DDC14EA3084C63A418050FE55085E279F94109B4AC6CE02E91D5CDFA62E9EDF05947A40F4BDF8C4A5FB712F86772DC1D9393482D45692463E3697A925BB7CB49F7B9E030199F4955EFF2C829C128DBDCFCB68A3CC57FA5DB71D90ABE690B97FD9387BB517352045F509A9C7A2F01EEDACB35E5E660ACF9ECDEA3F4201DA07BFB8AFBEE7AE32A77779D68A77EB23DF57FAB5E1C7B21E7515709F0BD475361311B831D336461ECBDA68646B8D036779AD9DE23EFDC399C4C90ACFCEC65B877C75A6C5782DFF158B618C0E4B43FC0EBAA641550A44721F35C09864508A3FD0718C2D6C0F235E454D30D969882DBADE20BAE506244B0D99EC1F9664A624A9F46B99B573210A4959CA9B3B897B40FDD92346953BB526893EE06C96C39EBAFDCAC9BC45759299754812CA556E5E5525477F88D207187B1916251B703CAE95B1CCC3585F7431B23969D20646BD1E61066BEA322F16CF8E58DEC2A5CFED648DD98826DEFD121C30302979B215FA0FFD233E61CACF09CD929605EAFA9D2083ACD78CA7C97227B379B0359832B5E1EFDD2CD72562DEC3B8D23B39003539E4E9B8F3C6A74398F18A3DF3F05067F95410F274B3DC0A3A8680CA8C53C746BB4208BA23B5752FB24121B8088AE702C8CE10CBAC6E733108072B29FBA6261491EF0F06161592C19846F18B9341B85D6A5DF4863F7F9F00EC4F8A669085F03F6461B3DC0271D38198FCD546AA1A8DAA4925E9172633816686FC07A855C92AB7D9B4E692D5BA6F51B0B9928EA778DE8A167123B0A80C8AB0B8CAABC5FDD12736A9089D8F60CFDC9D5A8231EB64EE8CEAD2B1FF610BE325DB34520495792E8B9D5403B0C2451671ECF9871BD5FCECDAE8CAD4E9E19815A60CBDA867CB0F5CD1A8A2366B5129B4A5799609909D43968BC296DF77592E8FF5F3ED02248279B761A4397F6930D30D47C31B657F8D1A13C99210CB3E17E84D414FAE4DC6E9E182106D353256A7271D0A5D23050FF33CDC1C48A64BCDD6069F71522BA1D33D9F13470EC1D0D348EAAFADD2DA2EF5D1CA6B05699EC818B6FBD719F8D42FC0F1172574F71C468204034E24A68DD7F92E341852984CF349CA5059E69B19E88CD4929EA8220D04CB4F06BC9A59F0F0528C83D59D4DC36B17EFE9F0B83FC581CECFCD981F419A987D2380AAEAAD7684EE7EF2B920DEF9C0801781B5B34C7E6FABE8EBB4B531A476D970248F2E0D0F17B38C5E2C46B45779383180620C5440C2FBD59033877B84CB411970862FD2C47CA91757B33243CD74EC15E5A622F44940F65E3F42372F8B\",\n          \"k\": \"06B511E4AFDB9427A2296FD9BD7C6467DE6A25D78866F770C2F41462D299038E\",\n          \"m\": \"7B93EBA796CAD98FDBCEAF0B8F3BFF196C1F89125B2AA88F623A91DC6AEE3771\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 4,\n      \"testType\": \"VAL\",\n      \"parameterSet\": \"ML-KEM-512\",\n      \"function\": \"decapsulation\",\n      \"ek\": \"5AC81984D4A5A83619735A842BD172C0D1B39F43588AF170458BA9EE7492EAAA94EA53A4D38498ECBB98A5F407E7C97B4E166E397192C216033014B878E938075C6C1F10A0065ABC3163722F1A2EFFEC8D6E3A0C4F7174FC16B79FB5186A75168F81A56AA48A20A04BDDF182C6E179C3F69061555EF7396DD0B7499601A6EB3A96A9A22D04F1168DB56355B07600A20370637B645976BBD97B6D6288A0D3036360472E3AC71D566DB8FBB1B1D76CB755CD0D68BDBFC048EBA2525EEA9DD5B144FB3B60FBC34239320CBC069B35AB16B8756536FB33E8A6AF1DD42C79F48AD120AE4B159D3D8C319060CCE569C3F6035365585D34413795A6A18EC5136AB13C90E3AF14C0B8A464C86B9073222B56B3F7328AEA798155325911250EF016D72802E3878AA50540CC983956971D6EFA352C02554DC760A5A91358EA56370884FD5B3F85B70E83E4697DEB1705169E9C60A74528CF15281CB1B1C457D467B5F93A60373D10E0CF6A837AA3C9596A72BEC29B2D7E58653D533061D381D51759752217EB46CAC7807C4AD38B611644ACF0A3F26B6B084AB47A83BF0D696F8A4768FC35BCA6BC7903B2A237C27749F5510C863869E6AE56BB2AFE4771C9221874F50F5B14BAAD5993B49238FD0A0C9F79B7B4584E41301F7A885C9F91819BEA00D512581730539FB37E59E86A6D19CA25F0A811C9B428BA8614AA4F94807BC031CBCC183F3BF07FE2C1A6EBA80D5A706EE0DAB27E231458025D84A7A9B0230501116C290A6BB50626D97B939850942828390B0A2001B7853AD1AE9B011B2DB36CAEEA73A2328E3C56485B491C299115A017C907AB54317260A593A0D7BA6D06615D6E2CA84B860EFF3CCB597211BFE36BDEF8069AFA36C5A73392722650E4957DCA597ACBA5605B63C163CFA94B64DDD62301A4332083361972589DB0599A694DD4547A5EE9196577C22ED427AC89BB8BA3753EB76C41F2C1129C8A77D6805FA719B1B6CA11B740A78A3D41B5330526AB87D58D5925315A1485EDC647C1604EB38138DE637AD2C6CA5BE44E1008B2C0867B229CCC36619E2758C4C2029EAEB26E7A803FCA305A59CD585E117D698ECE011CC3FCE54D2E114545A21AC5BE6771AB8F1312\",\n      \"dk\": \"69F9CBFD1237BA161CF6E6C18F488FC6E39AB4A5C9E6C22EA4E3AD8F267A9C442010D32E61F83E6BFA5C58706145376DBB849528F68007C822B33A95B84904DCD2708D0340C8B808BCD3AAD0E48B85849583A1B4E5945DD9514A7F6461E057B7ECF61957E97CF62815F9C32294B326E1A1C4E360B9498BA80F8CA91532B171D0AEFC4849FA53BC617932E208A677C6044A6600B8D8B83F26A747B18CFB78BEAFC551AD52B7CA6CB88F3B5D9CE2AF6C67956C478CEF491F59E0191B3BBE929B94B666C176138B00F49724341EE2E164B94C053C185A51F93E00F36861613A7FD72FEBD23A8B96A260234239C9628F995DC13807B43A69468167CB1A8F9DD07EE3B33238F63096EBC49D5051C4B65963D74A4766C226F0B94F1862C2124C8C749748C0BC4DC14CB34906B81C5524FB8100798542DC6CC2AA0A708575EABCC11F96A9E61C017A96A7CE93C42091737113AE783C0AE8755E594111EDFABFD86C3212C612A7B62AFD3C7A5C78B2F07344B789C2B2DBB5F4448BE97BBA4233C0039C0FE84300F9B03AC99497E6D46B6E95308FF84790F612CF186EC16811E80C179316A63B25703F60B842B61907E62894E736647B3C09DA6FEC5932782B36E0635085A3949E694D7E17CBA3D9064330438C071B5836A770C55F6213CC1425845DE5A334D75D3E5058C7809FDA4BCD78191DA9797325E6236C2650FC604EE43A83CEB34980084403A33259857907799A9D2A713A633B5C904727F61E42520991D655705CB6BC1B74AF60713EF8712F14086869BE8EB297D228B325A0609FD615EAB7081540A61A82ABF43B7DF98A595BE11F416B41E1EB75BB57977C25C64E97437D88CA5FDA6159D668F6BAB8157555B5D54C0F47CBCD16843B1A0A0F0210EE310313967F3D516499018FDF3114772470A1889CC06CB6B6690AC31ABCFAF4BC707684545B000B580CCBFCBCE9FA70AAEA0BBD9110992A7C6C06CB368527FD229090757E6FE75705FA592A7608F050C6F88703CC28CB000C1D7E77B897B72C62BCC7AEA21A57729483D2211832BED612430C983103C69E8C072C0EA7898F2283BEC48C5AC81984D4A5A83619735A842BD172C0D1B39F43588AF170458BA9EE7492EAAA94EA53A4D38498ECBB98A5F407E7C97B4E166E397192C216033014B878E938075C6C1F10A0065ABC3163722F1A2EFFEC8D6E3A0C4F7174FC16B79FB5186A75168F81A56AA48A20A04BDDF182C6E179C3F69061555EF7396DD0B7499601A6EB3A96A9A22D04F1168DB56355B07600A20370637B645976BBD97B6D6288A0D3036360472E3AC71D566DB8FBB1B1D76CB755CD0D68BDBFC048EBA2525EEA9DD5B144FB3B60FBC34239320CBC069B35AB16B8756536FB33E8A6AF1DD42C79F48AD120AE4B159D3D8C319060CCE569C3F6035365585D34413795A6A18EC5136AB13C90E3AF14C0B8A464C86B9073222B56B3F7328AEA798155325911250EF016D72802E3878AA50540CC983956971D6EFA352C02554DC760A5A91358EA56370884FD5B3F85B70E83E4697DEB1705169E9C60A74528CF15281CB1B1C457D467B5F93A60373D10E0CF6A837AA3C9596A72BEC29B2D7E58653D533061D381D51759752217EB46CAC7807C4AD38B611644ACF0A3F26B6B084AB47A83BF0D696F8A4768FC35BCA6BC7903B2A237C27749F5510C863869E6AE56BB2AFE4771C9221874F50F5B14BAAD5993B49238FD0A0C9F79B7B4584E41301F7A885C9F91819BEA00D512581730539FB37E59E86A6D19CA25F0A811C9B428BA8614AA4F94807BC031CBCC183F3BF07FE2C1A6EBA80D5A706EE0DAB27E231458025D84A7A9B0230501116C290A6BB50626D97B939850942828390B0A2001B7853AD1AE9B011B2DB36CAEEA73A2328E3C56485B491C299115A017C907AB54317260A593A0D7BA6D06615D6E2CA84B860EFF3CCB597211BFE36BDEF8069AFA36C5A73392722650E4957DCA597ACBA5605B63C163CFA94B64DDD62301A4332083361972589DB0599A694DD4547A5EE9196577C22ED427AC89BB8BA3753EB76C41F2C1129C8A77D6805FA719B1B6CA11B740A78A3D41B5330526AB87D58D5925315A1485EDC647C1604EB38138DE637AD2C6CA5BE44E1008B2C0867B229CCC36619E2758C4C2029EAEB26E7A803FCA305A59CD585E117D698ECE011CC3FCE54D2E114545A21AC5BE6771AB8F13122FAD295E745A503B142F91AEF7BDE99998845FDA043555C9C1EE535BE125E5DCE5D266667E723E67B6BA891C16CBA174098A3F351778B0888C9590A9090CD404\",\n      \"tests\": [\n        {\n          \"tcId\": 76,\n          \"deferred\": false,\n          \"c\": \"161CD259FEAA7EC6B286498A9A6F69F8B262A2E2093D0FBD76D5DC1C9FDE0DEDB36581004CB48112F852E7F87F649E8A42CD9E0349E7DABDF0A9AC1B521C37EA5241370A8AB2911CC79902C95D28224FA8896AD715209ECDD5D784E91DD9D0BE916B4565F4D5669AEE0DEF931E9768294EEC5258DE8391ECE271E7E4CFD9D23A79FAC3A8E0DB5DDD6E0107235688BBDF7BC5D5632F206C63A0C9564F30965CA58C69FF92D25A4F93A09EAB9B9085947E078A23E4D9C13B8A56E73E18DF42D6949FAF5921F2E373D450C8C09D07B152A97C245447429481D498BEB7256BC47F68F9922B0B1C62D9C23F9F733DD73792CFC7B43CBCEA277D51B2B8AD4A4F522F642CAD5C5DEB21F3627F8AF4D3E5BC9E91D4CB2F124B5BD7C2F4A050CA755BDB8056609663FB9511C9AD83B5039088CC01F0DD54353B0DD7433F0C6CEE0D075959810DEC5416522BB1F1F65547A0C2E9CC9BC17F8D39D29309EBE79F21331B75E12AF2E93F03F74F7F87D360F1DAF86CED736092A211A8158859C42E223CFE2E6E553437D80576CFD1944E97EEFF9B49E5ECCFC678EE165268DFE3D3596B4B86204A81C6063B0CDCE619FDBB96DF7DE6E0BD5270B4D59C4DC508476E7F0708F98C7A4F6645C49D06100C760C599528D1B8BBFE628191CC083C8D225A093F9F17E35574986F86BAA46898B589F3CB7DB46A45F3EDD4FAC20808F4CD0249DA693F8FABFBD4E10C02C65BA8C8610FA8C6DF3DBAEB6763DD482AF41558B1E15CC9C7A72E071685AC19A051F19245B9F77C3038A54E2958623EB8105955609E27D67CF72EC5C4A8E9B9C2924A9E2298508BABA13CF111FDFB062C9607AC1AAA6C637310A8894BF0B96F0C19136186B618DFFB275528BED1CC2715DEF412F77A3CF96645733B048A78474320D1A380F5EEDBDA21FA0125C91D3C37C54BF3752A1F8471C81FCAE2D3EDA966E14E66F223B054D79848FF9411D634024A098970ADE6A88B5F9069F760584DC4CFFFCEA8ECE11BB5566BD2360AB707DF2D21B67488D931F020069176423E6944490CB385E70B358A25346BAFCDD06D402FF24D6C1E5F61A85D\",\n          \"k\": \"DF462AD68F1EC8972ED9B02D6DE0604BDEC75720E050497351E6EC933E71F882\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 77,\n          \"deferred\": false,\n          \"c\": \"5C26D456C6C7B0E8DF0B125E5D5428FE393655127A5E05BDD1BCAC14C47493783097B6185058FA700555DD8AF10F0F979A39A603826FFEB0B44E9487539F3F1A07C673E96640DDF754C8B98CD83473568B49D095F682C1ACF0E160AB93EB41A16A57D53B419620D351C837315080D530845CF8D63CFCCDB6E9DFBE220A2C14221AA392E6337FA364DF0D2E0398F15AC3DC822B5DD7217081107A45C8CB8EACA51E034117962AEE7EC0EE212FA67A5D4B07D355A0981E4285116ECF5CA9FAB6E3105E4DE4AEC5E32938A1EB91E65CE7B39C3B9829AA1E72B8092C3622E519EE092FAC8106D6597CEB941C763288723CB55044A36D4181052A78B424B0DE1B0260F624A8D3B317095371EE9BEEA9272250D598AC63C2138D23F99087777A902EBA2163171A07546B72FCE7F86EE3B1DC1B8EAC85440B8D241742C3771F91BF981909E4F3E2505C594761259ED3AADA6AA09181B99037A395D66E6EE4BBEF97DE6BA36C53A1808CBA50938038C151603105BD6A4199EA44BF4B08961672598CB708F896E03CD9B8F8AD89DECFBE6BE0EF0006B7BD2F4AA6EB21C0218EDE601D46924CF391AE3A44E43D96EBE84A630937C3409EF0710970C27E3ADD4E64DC64E83942ABEA9CCF498EF1FE72B254043D2775A37E0B5DDD3F596EA131E0734AFA9D0223F4CD9D1AB7304CA979AD37F717BEDC3A9526F8FC94433FE4614F82E709456F39BEE7BACC84E5A70114AF1C2AC8B9B3FAA81C8F35F5A5D24189E1A457F58166473F5F1DF0170AAB5E4AC8FC719F945CCBE6F2FED24B23321D95C4C850B278B8C4EA02E3098D5A599AA3D842CF889B7F284AC5E6E66386D63F2C860B997966B4DF2C32288A50045012B7362727B856AF4F8258509B563758752FFBB1040F3C2AD8B0DED64FC15C95C1A16DE0DAE6625A9EFFCE190FC7F3261D844C114913C6B1152A258A37761B81879B59C37A1DFAC07C3E934510B45DA44C2581A79DAFBF00FABB207306269D9B74B93F4367B3BA22CCC51B362DE16E49D9FDBF8CFF84F6CE6892CA2245D34CEB9C8759E702832B66A572DE9F3016A38F7328700F96B2E947\",\n          \"k\": \"A4A24E182FEA12FF128AB2D4AFE6569817513FFC547DB70636752C9C66C002B8\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 78,\n          \"deferred\": false,\n          \"c\": \"79E255908B83DAD198AA6EA7219D5C170DA8548B172A2C28D53EB890914E16A6CE4405E8867112D35228DAC037743E25D26D720742C95935218ABBD93B4EB1C145794697EE761EA567BD561C6F5C076A48A34485539C49D23784606432B4913640644CDEE799961E5332E9502E683FEE98C9E1D071D8976E7F652EEE92E736D598F3B4D7217C0ED30FFA7DE590BBADCC0574A7280E502694A13A4E1D5D8837633A2EABDC97F36722D772A380595859134B9ACE346360860F8E60EACAB4AA3F9CF1DA73B5813F773008E0153B1BA0A5940DBC5C9E71E9A46BB4EA04AA9757E8E1AD0209C86334D05FCB611F3A00C7D983C7B9C160B7807CED18E5BC64A52462F4F9438199C2E4C6E9E70EDE2614913BE6D0C28894319B7B646444B5C86FCE61297EC11B21D216AC79159801ED3181667B15A7F30873BFD5727802E7B6588BDD04A5F7CFCD47043E600B4B3A0227E924E2CB92E514547BE4C1236C7AB2139F986AB956C704485DE570841F5857108D2AA57C535B3D44D0535208D501A9B56FFCBE8FDE32B375B90A5578EE44940E1E1888C21A4045D0338149D4C80CEF47BA25558E1842116E1E25499714163C0EE9A95A87A27CA2A61C4BD8D28BB04DE34EFB6E44FA7026B158883019B89AC4A5B5CA8F347A3FE892EE3949BD40D0614B9923052ED174FFBA720F516B6FD1317754A95520C66E3907B32A1648B344C34B3FA2ACEA2C8410DEEB40483529AC7D83351D888E968E457644CD76B8CAA55FC25BA1359F4A50119B1E69242DCE30E93983E50285DC0592537C6202F2E3C9878067A1777EA6A4E5ACC31614AE52787454FEEF503B82492828A736BF22E3278CF2ECAC1D0E11EC67815046CB4A66A8F48D04D4FB3C91CE7C251B37A8F3FB62A37489FFE63BDE22BAA18D4AA5BCCC0D8C709786B6C94D268382B649598A7A6785582CB2C02A2E9BECE29AC919785CCD026ADB6C9D8E85C3332DA956DC20B8470F8DD78B47E19B49BA5B27326D4937E93CC3453BB67EAFE42CAB03A70960DF236C04C344CA7177FD1E72E7E0A2C10D14F0C054337BD14152D4AFE9BB6243260E696EEE1327\",\n          \"k\": \"3B506D5A3BFB30D82FDD45B918F032A4023B9692D7EA6426FB2ADAB7DD5E274C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 79,\n          \"deferred\": false,\n          \"c\": \"D980E3002A28401678E1641E9E7A34D12CB2B4C9D986C7DB0FAB941AA83E43F42C44368138ED65B7A917B22ABA2DB0FBE44E5E9E7A3DAC3301379F7152585C8C7195031774CBEC61D8E1E093D694970B5377DC94CEDC53B6D3802BAF1C6F9152E05DCF66C1643528155C78118DFB00646B90726C75131DC934DCBB706ADBEC64E07F0D113BF71D4205D8E47DE67B9E01B224B82CA24405AE5591BBB1307D44E405E3866B1BB51ECE5985EE95D54568A81E7F285596DCAEBDC807EE6C8322EF2100EB38D10327DF92BC10A74A2D44842AA02AE9101A24736949D116CEC81F30C3092AAD941FC7F4BA10670CC0894A2F81E3155B9081004B4ADFB6532A1458F727F418D3F8F228E7425ACB7A4E4A3653529D1B9F72E57B8AB5852E35D0093B548FCC354A590C256B50BCADC30B55B5E05A3231611C93D5E34775741374F3E703B6E6362B35A68E33D859918D93EC03633220C61E1B81ED7AB1A5E46D4E640A9DE4E5A19FD11F0C24C556FE8D91F2358E7E78033A3C9FBF68C99DBA351F8F866CFF14E990C29E4A47579376606EB85A9D07D0BFD835C7670A5F4F2D4EA62B2BB13528A27BA3420095B852E3B73AB38E3CD068E276D8BD7A0B85BEFA48FEB72EBFC5240408B069FBC28F48B3A8E6556D7C601ADF98F0E9D64108F0BFE3C3B56C800D76E6DB14736809CEC22FA811EE92EA7950421B22F613E1349D259CF877075D476798C66FF58DBED013675BF6C5D3704528FBBE91486D7E956F785F73EDB6EB42861694F5C27E318C7B481377770125D99F236875D5B26CC8ED9859393BA2D8531E25FD6E2B3560BBC13176EBA638C72626C32D0250AB7F7EFFE661B18A641F75AE279C39771C7F23EAD50CD3AE461BB0EAACDDAD4F9C6436EDA5F2348F0ED4CD9514310AAE609B539368F53EA787ACD630080B832221B1CEF67FEB63CD2DDBA25282B020A3CB418130AD96D66EBAC09F09CB35BE0539B44924CEE15C6DBEAEE04C5BE5B9C43535FCA64B32BB204497AF175513375971B15F107F88980F0C0EB15D34762C98198A94EDCA385F32FD82CE7F6FB36F12B742C1755417A8D3F7D8A9\",\n          \"k\": \"68EE2117F8A66503091AEF490D1B9DC9EF3B3E62B97567F46A5EF2328263E5A6\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 80,\n          \"deferred\": false,\n          \"c\": \"E1E906D018CCDE68680DF762DE4612106E918C29FF5D576D8FEF01D5241B666C26CB7B2F80793146AEEB9308511D9BA264A9A05D6621425730A50479B889F8C6E5AF66CFC9A3123D3B7335C06C8CD2F867484240E8B6C19EFBD3C15C33CFD10EA482D1897A516D07E39C3C1AA866C10655736F18689ECE7359D91E6EF5CEE957930258CD890C09EE1714150347A18DC97AD955B60750624755135AC81AFF8352B701EC5FF50AD925ADA003A617AE64DBFD305038E1E40108C6F12CCDD7738A83C9F7A76164F670ED4756097426700E51BA02EE36BF12FF22A316790F2C2FE7216C12F03023D87E2ADB99683229E77D6B1938EACF10D8686CEE46127CD7652A33FC05414FE370A159C516D250F7D345BAE5E1C9628A65FA9F5ED9E39FA10A316620A2D760C5DC128A5C6137F193226D18B5E013E300A41B1F2D1B47D90E3DF8B4FD71A794FAE0404570261477B32DC80CEA32F2DE743ACF7EBDD41EDBB0119EAF7F872A50A5F4C92A8B85DF792DBAC764C3A9A5A5C12D9E3913356C7F5463BEE9BD2A739FD485493B1C0DEEF716E129ED1E085F146E0D70A7E58D924F576F948BEBEF7842ED831FFD58B4656F91686B4D0F832DD3A4E6FA9F11A4870E9602E0DF0EF4FE9312EC4C7EC216D3EEDCA2076D0F0B5C9FE139145222347E816EBDC1AA70CBEB5D65954427A3DF6A78BEA86C410462596950AB8798F9BACE51A46A544C1BD17A86C2995E3BC82A7F965401A599103B0896E1B9EBA540614CE8F218DC7290103A6044E87069286E5BB18CBD89EF562B6AE1C7353F64D8CED183FA8D05D6B6A6633751EF342D839562733CDC1977684317EBC71378EC02B298671F76EEDCC3041E943A76ED9E0C496B798E10B59BAE17A195544C05CD1FA6A161358EA1D4DC7DF454220D79B235C24720021174C1BF47859CA30BCB57FC19CAD92ABF24C051D48B3D9D46779F910D26AAB4543DB2A0044BEDD491E1D72D8FDF361B50F1FD10AF4668D78F56FDC7E96CA16DCCD9BE8C1819DECAEBBECE41C09093DD508191562D6510307FA4685144D9679E84F58929D79E693DA041B12B76F629912B1E49\",\n          \"k\": \"B5191E505481428549AC5B5548EB747FE5290D51DAB6D49BD15CBD702129EA45\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 81,\n          \"deferred\": false,\n          \"c\": \"3EAE23CC5F424EDC10108FCAE8EA3AC2BE8E90EB6AFC438B5A7DCD8E149AEBA25F0D5B25052C030F8157CC5BFB876A62F6A85B6C1C954F7C0F99EF4E3AE4B48C1CA9AF035543ECA1069B067057FEFB1E50FE0374F4162F0628F1D383A8B111EA9DE854EF33FB79488AA81E75712E5B9B6485290F0956B0574A6A9E1B4D677A832A85717CF7FF5A9E23B205C4FBD4ED7C2F7C5D91F46CD6A1EDB692750A4C1B11DB15C5643C7572FF9B765713C5C97C05BC2B861997CC6CC2C4D82CC62A32EA361630454756138C015D5501E362BC4E2B03A7AD679293658E45CF155B1C4F165954D594871CBF556CFAD2C3E6EB238DA3FF3A8140C5FCB74A278ED495DD14849D4C874C3E1F6E56EE657238F4E927FAE4588F1628DECE45C625AE0A6137868B9E86CFE29CCB4483CCA6FEE905F084B2B03A84DA421417CA5087B19654C803CF072B3C9E37A70B24E30E2F52B1DFCB6817ED05D38BB6FC7558B9B96AFA0CDBE708025D8D0454B90767753524CEEF8372150480BD104F1B7E659AD28EB155842CA81A55E81B707DAAF2F42A0B1CCE0B3BFF23F5ACF984ED20B0970ECF973DD0D5E33D34FBFD1672BFFF6725B5F1F869945FC67C5D01F3ED1CD8CD43A2008181AF7F65B0922D4BC634670AAD8A23A698AC3675EB3452DCE23D7E1A130964CCF4E26A9CD3D424A54ED7861E2D807F9C98E434A78695EFAC8BC86C69CD5911A2F52B5DAF50866151C5D00FAFCAB6219A9BA675413B4BB28619CFFACA38B9ABD1C3647BCE412336C02044EAA752B79248EBA1A7AED403801DAE5377CD55F517432B677A75DE4D4B504EBBF6453E319BB6EDACE30EE44810332CC84CBFFEE2B20548EEEEE1CA131AD87CCC284B3677E7F632D69F776060005439DE5648E466AF68C6616C63144451126D10311798A9B311064301BFAC1E4641830B1FAF4963F14A740529C360A73A351B6D330364BCCD2B012CD2B571ED243BF63F2FC1F1963604923B397A57680290D413FE7413B2C6C01D5BD6A0E314A644ECB10C69418251F48D3C3941211CEDB083F6FCDE24C5F5832034780B539D3FB1493C631F0C10F2F50262FA\",\n          \"k\": \"6262EB082F7C05044FAD90335BF60D117E52B382BACDDAB97D776CCB427AB672\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 82,\n          \"deferred\": false,\n          \"c\": \"B3B6B84D9A33C59758CBEEF8EA26540D4E3D4A45BDC623CA1D0AC05D8E780D2DA1FAB26A0E250527FB0B9BD56B2A0686BD0FD310164A17244374B82FC9A93FC0AC6067929C4718B2054C7AA4AF1FDBC9EBCC55A787F7C0B98A8E6181E5604E8F7108B181AD1385422EB747286FF72BD1EF650AB88865BFC37EA5536A220C29ACE17F8AA82A77F92E0A031E526171C44BD5FA1E7946CD063B1A7E113FAEAF92015CB3CCFBDD9C5E0329CD3DBD1B8CA90EC226ACC27716615B5998E0F5A5BFBA347FDD3DD851682B8968858F4A73FE5FD952CE7FF597185855E4B7F76F44BC1B24DB7C8C3A37217DBDF0BA7168D91B59DE9EA219195D29A5C67327D51D4E05131119C81722794D825B9F01DDA93C74B176545E32E638243891EE09E2AC1A9693C83D4BEFACCF25C81554802FC422C75812E18BCCF4D3CF208BE6EB16FB4E82C4ECE33C838A0B3D3EA4C027F41B4027643D9E4B6A7EFBD8D42A65B29786F4A00C16ED4492F4E945469C6E03A9A297AD9763333A2B9725DB5C6F8DF1CA7B0E77F5E6364FB6E8E528578350A04E4E4617E72E5FD67FE029AE2D738D8DFE24730D9D737D8E30ADCB602102FE2D99B915C9B04CDA463D444ED9C6E6A71BCAAFE503BF1D15270DEF8B9D7AA5557177EDEAD75E2FB01A4635A46D2F95DEA6314DE4965EBA8358210F79E64933AB4B6600856124363A47C6063433BB670266AA8FB968D947AD96D97C4003A50B0D1119E3A73E00363AE5EE85B5815A5BD944280031F0DC9B98F1F5C589F259A486BFE26EE1446D937EEDAE41275AB72E0CD15EAA6368F59686DE08E147CE2F5978B366D0A4F98ACD7D4004D1D0A4897A0DF5B1AF9F811BBAC64952D10E36A3EF78D379EF0E95DCD2D804C07AD8D1A8882FE1F2FF188F31B886BB597FF16F4D597EB337319AB4E81565EE4AC0A9BB3B6C3184C9C66511D7313555EF703194A747D0857DD27F92A6DE12DB311828B684FF3F1D848D5E92E0EFD7BC6B3EA7039296D587A075781880039A7C0DD6DB66EDEE3A22F7F2EF02B267429F6BEE16F214A59EB96CA79EC5065784445ED2FB631BADF6645991736BE7ED\",\n          \"k\": \"94EAE21B192F9D8FEB94E72B8F24BB0E1442F1F569323B202A497DCB64F9791D\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 83,\n          \"deferred\": false,\n          \"c\": \"FA42A8B407B527A8CD9351560BA4DA60756B27FEF326BC549B3A4429B2E58EAF22B3A36AE554B416DCE209E8CC708846312992DCDD43AB177347363F81578B94F451F1D046233BEEC6B42C9E0D55F3D741A55F7C564C4A9D5ECF6B067723E4403A17CBCFAA00E2F8D2EDFE1E236AE861011A5DB659042AA23BEB01A0471D178DB91039EEE5FC7EE85AC6FF3845959E5001C61CD1756EC681C97F4A70887884157D664A505ED7E4E1F4598EBF8BCDC0BEDE7FC0A89B3E14237187CED97BB0C0E54D21F4DF47BC8FC3F863978DBB673835D17931B7819535C1ACCD8706F8726BB0A0DE20BB824560AE5BAAE2F0BF0E3E676FF74C681474534C857837E7040C33B7F031AD9900A29DCB71BC305DB0ABF92CEB5DD2EB8E644F23AC0BFD8DCD2B44101FD7CB8A287318979BAE754661FFB13097B2A52B50236094693A754DC97CFAB550877A4D8C6CBA8B4A2E3D719ABF0EB13D40976B9E3F6C433DB1E16D794466D2C023988528AD0336CE43636DD50FA6A5E899578EACEFACA5FFC5B6FCC8C53E21503B83ABEDF2174FE08B4B960476934C5D6021829AB7AA7767492FAFBEE492A08524FBED46E8D0451C6BE1BE02B55653326735B0D8CCE951A5CF534E3547731EE36EB9BA38E0AE253B8CEC35001EECE0058E634A11F59FD6F21C1A3882E291F59B1FE3EC7F55315E0A65F9D011210462A8CAFC9779208452FD4F3B64FF456EA8588D2CB394A9169F1392646880A1C63721A2277FDA432FC6EBB61FF87AD473FF41D831DD95111CF0A1D69F001A008C3FD00B46F5342EDB8DADC818E6470D21C915F3E91992806E5B18DB314F9592E0EC8B8F0DEAF92DC89C194449A2539BE7C6A1B01B6F3DC496CF33CA25825B66971880652BC6E4FBD901A286C50D625F0F682B0B4CC769EB00940C45ADE947844175A3BBE8DB92BA6DAE5BE456CEA41384BB29A8C9D4E08F1375D4865A69A59619724900DDFAE48A2D12975C789E76104AE114F30ED4F836E46BCD8CA7520F4838651225894595C4F7BEFD7ED41EAF6F395EDE40F988CBDA7E08122A61E552801C7F3E84039FFF17E3534610D3434B996312\",\n          \"k\": \"23B74A4AD3F8E3EE73481A768E1F5CFAAE068ED38C0AE1E7A03159D2E9B0BA93\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 84,\n          \"deferred\": false,\n          \"c\": \"9F972164967C0CD03A3DD68714FE0B4EE0EDF9ACF63AA068C10FA947F8A03264B4309EE61C8C9B0C03C5FEDFC7B77AA862DDEFEFE394FB09A2396097452585ACD0CE510324A03F36904AF07B765575DCB3B1A84131C352EF14C2572E39DDBC8118875ECFD7EF7D2E41D9C9BE858FF08DBFABF8A80BCF18FDA8735F440D9B8FCAE0E67C5BF0171B99800BBF0F3EADF76F9FD69BB0734F1356C53EA9CD64E86C14C084BC3B1FEF040E5FA939F8F0D5171AD02628AFD8B02DB7D7B5C3B32F1A8EF3AD4116ADD4502414163C14D49EC73E5F4B25C5BAAB82C73401975F2119C569E1F2873DA202F32BDFB76F9AF49F22604D1B1BB173DDF6ED70D82B360C13822F5F9BC4C4D5F2391E4FB6BCB723A56666087A55E033E50202EFBBE7DAAF96AC541C855AA4154E37CDA55B1BEAB005554947F781512E2873B5CD8B118EE0932DB2FF427A15BD114D7DA79C7D899FD820A0222DF90D8E85CEAD8A1BD96A88D6D58C0A4FBDE3AA55DFA1E4B12AE6964DEDE20FE337E4BA5EE8B67CE1ADAE9851D021A56B999DED62D0E4471CD928E9AA4AEEC5C878199149D82C3CF4FCB68F63DF27842C37E52182A7E3B332F24948F3646874326B4FDF215524A1095A224F6EB02355974A6DE9746824A3954B700903292DA43D5DD51DD9D8E98E63DD01C357E4913855190049E0F1A8D9725B095ADAC4885FE832E0BEE82BF3DC355668093B475FFCF7D92228FDEDF0451C441B345372D6EE58408462E2C3BF22A095E5E23A159397FC959C126CAF936A3E64552003FEB2B963AF7F915885445EB25B934D659900DD0506A5FCB7168392824945AABFCCD01D9EA8A2256FC8E7AAF0C4243025A9F47F295F9D2713D5257D626057E904E34B8C0530A11DF2D15AE6BF1ADA6971B233B5DFB59EF8B9EB813E7E52794883BD6D676119B5B86333CBE6427F97ED719C432127805A9790837A1EB04B82907A59CED1286164A9F02716CDAAEE48799599CB09F5CA8BDE83CE8278382776CC3246EB2C0EA91C1A9D0CD7406B419A22CD6115018B9641405F9F44E13D2CD6AB457825326FC5CDE85C94DF86097BFB5204530FE8\",\n          \"k\": \"E9A6006C6C4D5A51829AADEADE89CC104358D0823BA8CB5AF4599D59E1679638\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 85,\n          \"deferred\": false,\n          \"c\": \"082411FFADCEE22B6C33277C32130E4C77CCB1849A2E7BDCE47EB519CAAACAAAA8DF129C5D876EAA7495ADED159D27F525EDD5F1F86B7A4FD50AC0B1F7B07C23F726D96F82D17818EEE6F032D0AEAD04D0F56EC244218905FD779268B259E29BAEF8BC66B42F47DC5BBFEA06620F38E0F373BA3F598CA7244A9F5B6823CA293BDACDD6D7B2E49BB2D00D1811C0F7FB2736876699D3F115C1D5AC58EBCFF10F514D863A56901F3DEE1328ACEF5D37DFEF841392BB29A88324CB51820A0CB30A4C222F7450F321B6617EEE7E722004AEBB5A52ABC3A984B8A142F0193EB90654FF86B8799EF7BDC01BBCD7C151587557334E01B833E950260C5E126C2BBF35EC030BBACFEF2812819A20960A9CA4E8D4836A7282F8F99AAC18BC02F6275582C7D1E6197938F67A80FB2363BF77A96355FA9E0AB19883CEA65A3010795E4A48A8B22FD04EC4578DA4452DC1B851C03A93AB147F3A34981515B75AB80D10A96570C2BF9ACB2E1662CF86E077EA455ED1B130D59CCA1F603A3471C408A342C42BE1AF6BA3E096E78CCF36CFCF6705078800E4E968FF372CE836AF5090E2442CF73E565146C69CBC0F55DB89BE1179CDF24DB6DD2C73371B00BD8CEC89FBBFAB3537DD0F50156FFA2D604BD135B91728DC93AAF31EBB51BCA15C02270D93051FBC0CF006C57F6BDDF5B8E60866E7A051358C4D0363ECB9A5EC3B6C745C41A3EFA2887B6B5AD8DC68E3C3FA17291D3D044D7085C6E2D3EB12FC3536CA8A6BEAC7B55BC2DD77B6F102C577B988E03AD963FF34CE4DFAF5194A05F12606D8E62FA7E20329E6630177BD60BCE780E014A856207A2745E5A22801A680CDBF0653EFC71F263E795AD7C495A90B7A5BECE0CC3F879B411A39A4346C677F53094298C0B2596DA1B136A32415E68A249161217414CC0F5F4D40614E162A3A757BDA41A80FCD17202AE062832D971FFD0A2F66D5EE94A26B1B78582E9F79F65A20D94EAC98DCC54D62B191DA89108126143E810AF6F8345723C69C009C481837FCED2408A8E37C96A248D7DDEFC7BBF73A5A91BFC10163813D22B0B26D5C6E380CCFCD6598844913\",\n          \"k\": \"3136E97F0A1CB0208B1CD89E510F2A37A5412AA5A2012E24327572886DD69408\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 5,\n      \"testType\": \"VAL\",\n      \"parameterSet\": \"ML-KEM-768\",\n      \"function\": \"decapsulation\",\n      \"ek\": \"F80751877A80FB724A0210C3E1692F397C2F1DDC2E6BA17AF81B92ACFABEF5F7573CB493D184027B718238C89A3549B8905B28A83362867C082D3019D3CA70700731CEB73E8472C1A3A093361C5FEA6A7D40955D07A41B64E50081A361B604CC518447C8E25765AB7D68B243275207AF8CA6564A4CB1E94199DBA1878C59BEC809AB48B2F211BADC6A1998D9C7227C1303F469D46A9C7E5303F98ABA67569AE8227C16BA1FB3244466A25E7F823671810CC26206FEB29C7E2A1A91959EEB03A98252A4F7412674EB9A4B277E1F2595FCA64033B41B40330812E9735B7C607501CD8183A22AFC3392553744F33C4D202526945C6D78A60E201A16987A6FA59D94464B56506556784824A07058F57320E76C825B9347F2936F4A0E5CDAA18CF8833945AE312A36B5F5A3810AAC82381FDAE4CB9C6831D8EB8ABAB850416443D739086B1C326FC2A3975704E396A59680C3B5F360F5480D2B62169CD94CA71B37BC5878BA2985E068BA050B2CE50726D4B4451B77AAA8676EAE094982210192197B1E92A27F59868B78867887B9A70C32AF84630AA908814379E6519150BA16439B5E2B0603D06AA6674557F5B0983E5CB6A97596069B01BB3128C416680657204FD07640392E16B19F337A99A304844E1AA474E9C799062971F672268960F5A82F950070BBE9C2A71950A3785BDF0B8440255ED63928D257845168B1ECCC4191325AA76645719B28EBD89302DC6723C786DF5217B243099CA78238E57E64692F206B177ABC259660395CD7860FB35A16F6B2FE6548C85AB66330C517FA74CDF3CB49D26B1181901AF775A1E180813B6A24C456829B5C38104ECE43C76A437A6A33B6FC6C5E65C8A89466C1425485B29B9E1854368AFCA353E143D0A90A6C6C9E7FDB62A606856B5614F12B64B796020C3534C3605CFDC73B86714F411850228A28B8F4B49E663416C84F7E381F6AF1071343BF9D39B45439240CC03897295FEA080B14BB2D8119A880E164495C61BEBC7139C11857C85E1750338D6343913706A507C9566464CD2837CF914D1A3C35E89B235C6AB7ED078BED234757C02EF6993D4A273CB8150528DA4D76708177E9425546C83E147039766603B30DA6268F4598A53194240A2832A3D67533B5056F9AAAC61B4B17B9A2693AA0D58891E6CC56CDD772410900C405AF20B903797C64876915C37B8487A1449CE924CD345C29A36E08238F7A157CC7E516AB5BA73C8063F726BB5A0A0319E57127438C7FC601C99CCAAE4C1A83726FDCB5045ED1A82A985EA995396D77272C66CE493289F6110910F37C2741CE47026A6F8261999C6482572B1693912EF12EEBEA7ACF9234FB409F2A6090E6B0BFD895469D0B2A921BB723F87A33EA5465AB90F514B67698C0768B6CA498B022C512FA0875F054AA2265867E31C0E522651E024A07D60DD9F633166921F4126BC2B6AA01CC15A09B85BFF8218C5AAE95BC1FFB26AE5A137670F04910CA9D7241B6660C394C5455917746A26682FB71A432EA9530E839BDEB07433004F45A0DDAA0B24E3A566A540815F281E3FC259AC6CBC0ACB8D62268B603BC676AB415C474BB94873E4487AE31A4E3845C79901550890EE8784EEF904FEE62BA8C5F952C68413052E0A7E3388BB8FF0AD602AE3EA14D9DF6DD5E4CC6A381A41D\",\n      \"dk\": \"1E4AC87B1A692A529FDBBAB93374C57D110B10F2B1DDEBAC0D196B7BA631B8E9293028A8F379888C422DC8D32BBF226010C2C1EC73189080456B0564B258B0F23131BC79C8E8C11CEF3938B243C5CE9C0EDD37C8F9D29877DBBB615B9B5AC3C948487E467196A9143EFBC7CEDB64B45D4ACDA2666CBC2804F2C8662E128F6A9969EC15BC0B9351F6F96346AA7ABC743A14FA030E37A2E7597BDDFC5A22F9CEDAF8614832527210B26F024C7F6C0DCF551E97A4858764C321D1834AD51D75BB246D277237B7BD41DC4362D063F4298292272D01011780B79856B296C4E946658B79603197C9B2A99EC66ACB06CE2F69B5A5A61E9BD06AD443CEB0C74ED65345A903B614E81368AAC2B3D2A79CA8CCAA1C3B88FB82A36632860B3F7950833FD0212EC96EDE4AB6F5A0BDA3EC6060A658F9457F6CC87C6B620C1A1451987486E496612A101D0E9C20577C571EDB5282608BF4E1AC926C0DB1C82A504A799D89885CA6252BD5B1C183AF701392A407C05B848C2A3016C40613F02A449B3C7926DA067A533116506840097510460BBFD36073DCB0BFA009B36A9123EAA68F835F74A01B00D2097835964DF521CE9210789C30B7F06E5844B444C53322396E4799BAF6A88AF7315860D0192D48C2C0DA6B5BA64325543ACDF5900E8BC477AB05820072D463AFFED097E062BD78C99D12B385131A241B708865B4190AF69EA0A64DB71448A60829369C7555198E438C9ABC310BC70101913BB12FAA5BEEF975841617C847CD6B336F877987753822020B92C4CC97055C9B1E0B128BF11F505005B6AB0E627795A20609EFA991E598B80F37B1C6A1C3A1E9AEE7028F77570AB2139128A00108C50EB305CDB8F9A603A6B078413F6F9B14C6D82B5199CE59D887902A281A027B717495FE12672A127BBF9B256C43720D7C160B281C12757DA135B1933352BE4AB67E40248AFC318E2370C3B8208E695BDF337459B9ACBFE5B487F76E9B4B4001D6CF90CA8C699A174D42972DC733F33389FDF59A1DABA81D834955027334185AD02C76CF294846CA9294BA0ED66741DDEC791CAB34196AC5657C5A78321B56C33306B5102397A5C09C3508F76B48282459F81D0C72A43F737BC2F12F45422628B67DB51AC1424276A6C08C3F7615665BBB8E928148A270F991BCF365A90F87C30687B68809C91F231813B866BEA82E30374D80AA0C02973437498A53B14BF6B6CA1ED76AB8A20D54A083F4A26B7C038D81967640C20BF4431E71DACCE8577B21240E494C31F2D877DAF4924FD39D82D6167FBCC1F9C5A259F843E30987CCC4BCE7493A2404B5E44387F707425781B743FB555685584E2557CC038B1A9B3F4043121F5472EB2B96E5941FEC011CEEA50791636C6ABC26C1377EE3B5146FC7C85CB335B1E795EEC2033EE44B9AA90685245EF7B4436C000E66BC8BCBF1CDB803AC1421B1FDB266D5291C8310373A8A3CE9562AB197953871AB99F382CC5AA9C0F273D1DCA55D2712853871E1A83CB3B85450F76D3F3C42BAB5505F7212FDB6B8B7F6029972A8F3751E4C94C1108B02D6AC79F8D938F05A1B2C229B14B42B31B01A364017E59578C6B033833774CB9B570F9086B722903B375446B495D8A29BF80751877A80FB724A0210C3E1692F397C2F1DDC2E6BA17AF81B92ACFABEF5F7573CB493D184027B718238C89A3549B8905B28A83362867C082D3019D3CA70700731CEB73E8472C1A3A093361C5FEA6A7D40955D07A41B64E50081A361B604CC518447C8E25765AB7D68B243275207AF8CA6564A4CB1E94199DBA1878C59BEC809AB48B2F211BADC6A1998D9C7227C1303F469D46A9C7E5303F98ABA67569AE8227C16BA1FB3244466A25E7F823671810CC26206FEB29C7E2A1A91959EEB03A98252A4F7412674EB9A4B277E1F2595FCA64033B41B40330812E9735B7C607501CD8183A22AFC3392553744F33C4D202526945C6D78A60E201A16987A6FA59D94464B56506556784824A07058F57320E76C825B9347F2936F4A0E5CDAA18CF8833945AE312A36B5F5A3810AAC82381FDAE4CB9C6831D8EB8ABAB850416443D739086B1C326FC2A3975704E396A59680C3B5F360F5480D2B62169CD94CA71B37BC5878BA2985E068BA050B2CE50726D4B4451B77AAA8676EAE094982210192197B1E92A27F59868B78867887B9A70C32AF84630AA908814379E6519150BA16439B5E2B0603D06AA6674557F5B0983E5CB6A97596069B01BB3128C416680657204FD07640392E16B19F337A99A304844E1AA474E9C799062971F672268960F5A82F950070BBE9C2A71950A3785BDF0B8440255ED63928D257845168B1ECCC4191325AA76645719B28EBD89302DC6723C786DF5217B243099CA78238E57E64692F206B177ABC259660395CD7860FB35A16F6B2FE6548C85AB66330C517FA74CDF3CB49D26B1181901AF775A1E180813B6A24C456829B5C38104ECE43C76A437A6A33B6FC6C5E65C8A89466C1425485B29B9E1854368AFCA353E143D0A90A6C6C9E7FDB62A606856B5614F12B64B796020C3534C3605CFDC73B86714F411850228A28B8F4B49E663416C84F7E381F6AF1071343BF9D39B45439240CC03897295FEA080B14BB2D8119A880E164495C61BEBC7139C11857C85E1750338D6343913706A507C9566464CD2837CF914D1A3C35E89B235C6AB7ED078BED234757C02EF6993D4A273CB8150528DA4D76708177E9425546C83E147039766603B30DA6268F4598A53194240A2832A3D67533B5056F9AAAC61B4B17B9A2693AA0D58891E6CC56CDD772410900C405AF20B903797C64876915C37B8487A1449CE924CD345C29A36E08238F7A157CC7E516AB5BA73C8063F726BB5A0A0319E57127438C7FC601C99CCAAE4C1A83726FDCB5045ED1A82A985EA995396D77272C66CE493289F6110910F37C2741CE47026A6F8261999C6482572B1693912EF12EEBEA7ACF9234FB409F2A6090E6B0BFD895469D0B2A921BB723F87A33EA5465AB90F514B67698C0768B6CA498B022C512FA0875F054AA2265867E31C0E522651E024A07D60DD9F633166921F4126BC2B6AA01CC15A09B85BFF8218C5AAE95BC1FFB26AE5A137670F04910CA9D7241B6660C394C5455917746A26682FB71A432EA9530E839BDEB07433004F45A0DDAA0B24E3A566A540815F281E3FC259AC6CBC0ACB8D62268B603BC676AB415C474BB94873E4487AE31A4E3845C79901550890EE8784EEF904FEE62BA8C5F952C68413052E0A7E3388BB8FF0AD602AE3EA14D9DF6DD5E4CC6A381A41DA5C137ECC49DF587E178EAF47702EC623780691A3233F69F12BD9C9B9637C51378AD71A831055277254CC63C5AD4CB76B4AB82E5FCA135E8D26A6B3A89FA5B6F\",\n      \"tests\": [\n        {\n          \"tcId\": 86,\n          \"deferred\": false,\n          \"c\": \"74A26C7D27146A22C7EAB420134E973799CEC1DA2DF61AE0FA7905A3A47485A063076BFA22D6E4FE5059DE0A32E38F11ABD63F990E91BD0E3A5BC6E710DFE5DC0F6D4A18147EBC2E2D9B179374D83692C53EFBD45F28A2A928C2494F903576C410EB1773895EBEADB119960EEBDA9C3C710795A6D9B781FC58B30D08107F4E20944A382AFB079F31D21724F2C26E6A53412F0A908BE7586F2B3D6D7C1DEA0270E98AA209244BD88ED68AAE01432342BA5F49E015CB476B5B78D15EA77A354CC9E9FD07137D8760BE42FD4746C62C02028E7B405DDC95DF3D021921CFEDDB3D961B957ECA302A263DAB2DC117BEB3E79EFACFCF936DFC09FC0D19C358D724FA381EA06CA067C384E944302C3907AB15A1DA4B41352692ADD59B061541F07EFF25EC42F46E1A0E370CAD06FF3FD997D4D2C5648AF762231B382D0593401936CBA21551A2AE30D8E8EFFCF43916B83138BB5E610364429879FA9CDD5B7D3CF2FEABAA1DC8D50CE69402E21103E795DF7074D1FCF65F8A4E18986D5417780602C63BE5A044863384BD3D8FFB685EAC567ED8349DCF2CEB702B7375B145729998049D13E2CD466CF2231B9D3A20018EE908F8514A6C6A89DF7232F91FCD84B81EBC8BC539E9A37A4324755564BE1BF4FA1FB4571E0ABBC9B52F9D090C33BE599DE6C8532C7CB7EC8B4E2D3C07505280E99923865903FFD18BC13B9C8164AA1EAE84E38D3F57FDB8801785F105A6A8574BD2FE9BF305848E525330BC2D24F0257E47A4950F433A9233E8CDEBA81DBAE7D8C1A06D01F70DE6EF663207D84952827BAB3D451CBEA0990007FBDB4240FE899A706F7C1563E05C70BE9D575189EF83E0CF76195F6652491CCE04F1CE2092170A92E0DD7301246A4C44FC0B4EE6AAA63FC7027840ABD2EC25F654589738CD38B9E10B975CFB6C1D2EB4DA97736998F84FDDDD810D72DA3C5AB13507420DDBFAA4F7750C1FAE9C7DFB30F40A12AEA689FC78DA900020E3ABB32A364D5C6B3C7544A1B5734A41E95C8314B448CD0B738D829AF772A8F81C51ADBA2D85F326C8F5D6961CF12D44A9BEDEA00D1DF5B48F429B1CE0C15EA5F5BC10B017247BA2C6BE922B0563B8E9698677CB6C45CCF2081BF84219D2904C11FF92199F8AEFAD62D8608E200802C5A07202CC820E9E520E31BF36A83002ECA4018B0B3A398801562AA86C77AB0D50A8FBC3768B0A643B97E7F9072168DE29B8175999C9AA48D301A3F0303172E9C7D4F16329D5CA9D42397C3982E10C9DA42DE88BD6C2AB91C1E71E778E58BB8F801F207A88A9B47F9C687AFBBA34EDA6D2899E4FA0008AA2B539711753DC7C07F614E814F683D6C037562AE1FBBE6D7D5FA54B7A6D9451E11B01AACCC3BF2ED64742DD100E0EAB2DF6CCCF937B6D5981ECA0E01F3245CF26A72AD1ADF066C8F5430D72F509963A657D85E554C14E26E8BEC5D5F3AB998C9B29F16B04747D80749B30E51FD2A7F690C22F9986AAF6358D6FAB8DED54971B32641DE2B258590EEAA6BF1F32324A7C4C983F49466D86\",\n          \"k\": \"3D23B10DF232A180786F61261E85278251746580BEBCA6ACBAD60AEF6952BE69\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 87,\n          \"deferred\": false,\n          \"c\": \"39EFB90089F1DC32A54370B3EEDF2B12880DC7D657F0404E41F7DAAA73E7F06CB90BBEEC7544160768EC3B56681D057AE1DB58F0123286D3A8CDD0B414CF9894FDA1CFF3A37CF67B82C5C7AD3427F2F2B393978B94E524F33334E4A98AFFEA8D7514D6E12E85086E58A0C078EBA64435441F3E3702EA27EEA984E46893BB886572491F22AE09F8D50774B4DDD5CF478CB0B2D070437E86645EF62AA83599093732F81A75D1D5DE15C31EC81AC4D67852FDE089D580B71E3DB07C71394424E0936BF74D0C9405BD3DFB60B920E7EFA38C72D5912BBD301BD3F3709CBEEEB7BFD0767B77A8639913E8C228FBB7E3E13C423BF05AC65B7E75F29C9048F161AF1B4B41C495ADB53FECC57FED0DCF792050A2A586C33AA4A7F6BCDA9068EA295FB692BDCA756FCC47CA0A8C84DB5DCB6A616605F3D3A34C4D23EC14942492C07EF123C8D084DF21F3B2141D277FA16E3CF4D5A3AB8D78CE8370F411DF737647A2D6123120AEE1CCF7DEFC35A5408FA6013E94703E8E04C50BADCBBF2E1FF0FB82DB4AAC595B9EAA9E370C9C6175CEF20B1D0B8A4309AB91918451E6C8A6DF04AE468D446FD9E83F9252F145A2B44A19E7B27DA56044717DB5A6ED5F6E5CDD90208ABC324290292B1F2E84FB69F5989D9921DCB4F058DCAF7B99DF71B26BD1090E457767954B8ACC84FDDFD663D64027528077B3C9E370600942E4C1175B487FBF25E267474B5238576010CCCE3315CEDD5634658B2028F3FB9959D77FA23756DB4878697C9BC491DBD68986B9073D187F2A9E72C943D94C97DA865CFD9C23508105637FED62E56E745555909A49D23B86E620D48FD55A92CC2266C38B857F5DF9BB683D60B084819CF04F5BB8CBED05AC6F48C518EDB5B222F5E6DCBB438182A7BA3B2279E5856828CBE9BDA6009A70D20DA082D2FFBD092EDAD4B272E46D215B8ECC26222499F024327A391CEB007789757FF8FA8267429F0534F305F75709DCC4229803EA8E612F55890C5FDF8252794D5C9C4058C2258A5599BA858A02F89A6FDB35C4F2364A4C6B326A31F7D04F62C2FAFE51D280CD7A4CAB66404FDFD033EADD07974BCAA7F0CB7401B9484DAF9F325B6BA53FBF41219384B264F24AA8D65281693295E6F71FCA885F808026829A3FC32DC9603F0CED36F0B58A296B44ADDA3AAF10638C31F354D1A5AC34E77D4D0154C9546709E920258F73E039FBC223EE74A270840165F64E3051B10B5E63F9ACCF5D1EF40E43F5823B15F8C25CAFCE698A64F9AE316D3905B8E510C56CF7544CA94719735A640F2B8C3A2B828A04E0568863937595E5B9DADA33533D9D676AA657FE69152E93159A00C5962F4DFF9C901A9AB32DB28B93F4BA780E44A2F73878AA76E112E3490205AF83000EFD889FCEEA5E87AE9AE01EE1CCF6BA0461A8D8654B7702C09BB41C4F61A00D05F031B244EDED8D1CAC7916BEB9AA67A3880F4C3516A8D8204932EA00EFB3AA20369FB6BE404843C7411E88428568AB9A39124EAD115298D49C998651E5EF613A6819336683\",\n          \"k\": \"1D2DCACEC14CBB78FE9E418937835EED088CC0683300C965EF3972081F01C4E9\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 88,\n          \"deferred\": false,\n          \"c\": \"A5C81C76C24305E1CE5D8135D41523682E9EE6D7B40AD41DF1F37C9B17DCE78076019A6B0B7C95C9BE7AF29507B2D5A6987C8EE3259190855243E6E56F5620608C52D96FAB103A8700FBA1A87DCA6078118A0871762C9534C0C0C3978C91C3A01F0F608DCF757815438FE8957C8A859183B1B6721A0865BEBC799D4E5C0E7BD3EAE4858E6AB6A2E7658ED80D4ED158B036B93FA03AFA6AE3136CF3D693C911BCC75905E5B0CB2865B9E9884522A77777613E53111D5A1C7D3DAB734CEB03657AE0C89763E99471054776BAE7D51B0E73A5BB35AEC30FF6BC93684916FEF1162586452F426653E2CA844D5744307FF9AEB287A6447783B21A0E939C81421D631F5DCB452E51ED34E3DAD1CF504E0A3B0F4711A8DC6499D1691D109569336CE1558A4C0A464E2087EA8F9E3B18F747EF61F4576AEB42B17CADB7F0FD84DA8E3A6F471D95EDFA65BE9E6C9F6AE756A22A4F1A5C543C26BA7BAD88E16D5F5B7E12E2D4CA34B3A64D17F87CCFC4FF8C5E4F53752A077C68721E8CC817F9FF24876170FF2AF89FA95855A5B1DE347C07FDDBCFE7264AA5ED6401491561D831538F852B0ED7B9E8EBAFFC060284F22D2BAEE56FA9F6D01432A115A2D6A64C38AE0A50BA362FB57B53E3E855B83CE8C42274045599F65FA6A8921D85F94ED230B516712DB6FD2FF28B3A3371D9BE058AE75C2FA591B7EC3C3DAA1F7642BC26C324C08090607E6662154DB37CF747967A1F9FC29089F570EBE60EEEF89FD24481028C85AEF1DC3B09F22CD3691BBBB821C7A8A0F35AD12BE1DD199B977048F3D48C16BB2CA94CECB8928770D5BB329A0327E0B286FAA1C65281031A31C84F2EDC9C04D475ED4E128E51EFA97D0148CBA6C95F674C589F301C265BED708E9AD8DA3C5CECBDEEED35EF1E253132BA89920D786B88230B013BCF2DC92D6B157AFA8DA8592CD0743D4982BE60D7C2D5C472AB9FA7F4CC3D12B0EBAF0ABE555C75805426844DD9428643F84406A1B8D6FAEDFD8AE6E73A72772A2159ACABD972AEB6F7DE091AC5FDD7F49A3DC6641CDF62446B4B04A31F73B80A62F80A404A8CB18CE3E65480EF7B52BF0091117E5D08EAE1B0AABB72E6DFFFF76F6E44BBD7EA570D6604BC2E74318BAFA315A38861AA1B21AFB2A53F2614F1D640075984AE62E2FCA1D1B4DB369F15705CE7D4DF8AE98264501051C0DEF21D645D49625AF02CA428D9F0C2CD9FBAEEAB97E8E9151662B6992B4C99AB1B925D08920363373F76D3FDF0828CAA69C8B1BDC6F521DF641CF1C8A4E7EF0C23289A4E2CF18ACEBBE4C1E68369BD5235120142ECDD1A73811E2E533A647D7AEE16DAA03B683639DCF1E1F1E71CFAED48F69AEC3E831733DA19CEBEC1DDBF71CBAE0800F2F6D64A096EC495D62F4344F7AA5621B322353A795AA099EA3A070272D053D4653A20CF210EAAF12CAE6023D8E5118DF04B384A44D1EDB91C44989EF7EE57F2BF81A24BDC76807DA967EE6525410C5C485067EFC3D39A9AD42CC753BAA59A1FD28AF35C00D18A406A28FC79BA\",\n          \"k\": \"DC5B8888BC1EBA5C1969C21164EA43E22E7AC0CD012A2F26CB8C487E69EF7CE4\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 89,\n          \"deferred\": false,\n          \"c\": \"0BAF0F6E91ECAE3199F4921631891A14C13B418B53384992DA3A8DADA7DEFFB9E1E5F559D27344B60BE81ECD01CAB1E316573D571ED46F59248F4023DB0282207E730549CDB60E793E4CD17AC6F2800E2D1FFB83477A6FE1D73992682123EA730C63269DB13088D6DA46D086CCEA2176398EAC663270B8B2F337A55E19F4C500DE066B5441794C2D0CCADFE5ABDE7D93FD7D6468BC4F925633366D9316788B90B110A4D99485E7E578537A267744FB266A4F243FA02E3A81DA67ED477923B36B37BE21DDA21EB51DCA1F0CE41652145F4C542B2E5C922617033608246BBE2B5250A368804ABDB2EF6C31C491CE3DD852AEABF6EEF1530F4C99286B4B595D57CF3A99580B59AAA2C55E080B5230EA19CF2701D21A37FEFD6F9709657A21ADD063ECBC197B5AD068BE502A2E090D83F4156B671E46617BE6D6A17D0425FAC565C4A0E48966E9D900CB2C2B0D296E0BAA9D6C5E0514CD78834053058A97D3DDF81529079858737440812670E818C9891681D350ECEC93DAE389D534A5C78F01811917061CAC0003D2BEA390EB63FA0FE9BABCD7FF302D4B66567B2BFA67B20F962847D010AA4193CBE9F8CC1B14F8B237C22675B298A8376DFB6037BF7CEA36BDEAD5B505111F67730824B4964815D00F63EE98B9BEA0F2F47CC007D5606ED7F967CB15CCD4AFBC99881CFD297BDC2A509ED3CB320DF58DC4A5BCD1CB100B9D6418CB8E0F40DEF293DA2370CA729B0FAB071FA6AEB0F3F5D1925AB2DF732F98DDBFF23D5411E4921A1C506F2F93251E822C4CF83998B000FE65ED386F5745B1D4D91AD9F98B45E713C8D944409E9D354F42FDB9749A5107C8831562E683498C55E1475E552AC10858AB9867BF8003FB88B3B09F6E8AD8E94CE82E342B1780D68EC8565FC0684AB6C798BF09FA65BE62C37A0862ABFE99D7DBE1431B4CFE007B7EC7930B14F6D161BDCAAE2217D69D9FDBB4F882B9F464F8642ACD9BA018B93A8E3A965194ACCD96E661CF0CF4A2662076E20E8BC319693F1953DAB93FEB9BCAD666832DF42F250FADBCFAF742D68642021BD6FFD97720C3E5AB86D82CE8B14C0289DBF51B50C13CFCEC12A3922DCD2DE8473329AEB23580B22F9C36B4F06D6579751BE0593120F808F0E145D94D1DDBBE1D489B744CF6C35964C3DD96D95FB693543C69766877DA80BDE8ACDF62C366D0A4A553187461F671376F7E70F554965D57760CDF5C6F6366E33B3BFB550CC1F93D98D250F90D7D36BC01581C49417546BF6BBA9D10D41C0A008855F321547BDD5A6CFA2A2516F71415B5BC2D5FA1B9B79FDC7F2B78AA113375EC1717F0F273BD8CBEF59139518A4E8A67DB4D071257000336BB07497F72FAAC2C1FC0F553B2EBA53475F466A2B36AFE0B72B4342E995C544E6E14FF7D327F80E7AC6F65190045F380B5978F50E33272484626266125A39DA08B46256624CE34223BB17299B8B8162753812F2644C9A13C51430B02ABD188DD1A4547C920BA27CDAF145BDEBC6F45EEE3F2F55553010F7B35AC63A3C7C61C\",\n          \"k\": \"DCBEB5E4E8B14BD3031D5916BA03258119A5DACDAC850CB483BD7AA80B7038D8\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 90,\n          \"deferred\": false,\n          \"c\": \"2513DE1E55ED0E862614587FE47F308C90A1F426470CA1293BDDF7B9DDD6C368DC152F45C71354904ED48E15A1CB449B4C45D0F201ED5C7D3A047A72F080265D66C47D39469097EEEABBAA3B07ED1F1AEAB80C7D24552FA8889C674A5D4840289DE6B0FA9A222E693708D1F252DFE8B993956883C07067C1C0844EF0BEB49F63534D21D471D6B727FFC59477F9E89E5BEB2AF0CBEB052F003414DA4070008753CFC0C6D0FA9D1C15388FE5886EADD3474F28E4682C0E01784A037DC3799330EA380767B0D0B6EDFC9730E04D1039548A6F83889098522EBAB684DA6FE26A4A6891D86D40FCD9A24F743D74B23B1596810727C81BB3F9F3BADFAE9997949EE0E24987FA182A00D73DCEADF667E90E5AE76A1F83A91FCEA78C96269F0C9501F1D4CE682506A7EA89302A1480E18CDC1F6D57B5312EAF808895B20897E9A782F916CD75B4981DA1381F14EB1EC248B27F0E6966A0CD75414A735928B2120615D88FA57AF5C40E61750F0A0F8E605747E7C32D5A23F14124969C072E949C8475E3108D689D2D20797FE14618811E9A497FD26B9E71355852D4B36340B61695E3745F8D07644AC6E2C18B3FC276D4D19DB69A7CF26086F172E2BCE1618A740A0C739FD504F72C2A72ADB5564BC85DAB4C9CE790D78D14D3BD242DF04106D96CE7C3B392CCED9B99DF359FD51F306CBCBD5B46B8487CD7B7EDD3C5C02965C84630DA1B6B8B317FE55F7C79E05CDAC9E863023DAF470E9C3FB8C01FDF3AEDF2193BFA69A806E2E70151ABCF96D31CF6A317C059CA8C7D456A8E5EBAA6C1283A319F188AAA80D8301E321754E5FB4E0B25594B01BC5F82FF25B064C766424D658459EFD7A20B65DB181811E6D5A4BD153F7066BD7757D2D417D21F83D7C4CB6A0703A42032F0FD198D9D8B0F91B359FBE908432C3286E1EF9D601702157EFBAB68E0E7136BFC90D26BD8A9A7018DE4C4BF05CE465F917D20A4F221A4EE78813A1E8A117C8470929701CCC201A85E7F18B6BC96FE80B1E074661525D3FD0CE2565AB11155DAFE4D3410328D6DBB4DD99A84FE96283D32322522B88B3AA2A11C0324B1D5556EF408D37B0DF802D163FE38D7C38916A26810BD175D22762353C3175DC6040C899E07A339CD4DDBD4D5549E02C0D691263936A9F63111412B60AA9F57486334E40B2BC1B8EAA487A094E45C3F77F72EA741CE225ECBE2B5E4A1FC080070A658FDF9E2B388722855267B30D94B63C3ED35D475B7EB22E3D2462ABA9CF2A86B738EBB270AB29708A2614A557E33A620B507286E5D4CA57E2CEEDB9965FF1C3E1777F980CDFB1445BBE0B6ACBA0216980F962FBFABE265B3ADFE8641088287468827AE601B6A165DEED39C0E8773BF2046BBF63634BDBCAF98358D25FDE475781733DDE8C6D6383D13B6D48FF1B65E2FF13AAA9CCCFC3C626935C5270F9E23A71A87CF2BD793CB175D23EA5FBD82C18A1822428C32DB9E31B94BE3144ABB00F5ACAAA431C17386719C3FF47C38720B1AB01889DAD877BADC9FC716F648FC8B551F\",\n          \"k\": \"2C37C49E94DF715B3C09E63A39E04DB8D26BD2B9072C9B21076BDFC0B608534C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 91,\n          \"deferred\": false,\n          \"c\": \"8A4336FDDB3F55D16ADBBE54C6EF0DB27F20679393D86EA4590CB6F5F09BC4EB76181A13C9826FBD2A7174BE8A11F13759EE23DA15337A4C5612480E0A843CC6D04F3A902E144EFDC0AC118BF8553B984E758E6D7ED1373B20A5726271C5F4B542FCCD6379671CE37A5D0128F55539B9A855172CA2DA3BB6823484A87DC2333F56CBADF4A694A5DAE341A0E3FBB3D852929FBAFBF4A5C12CD3494CDF910010A0FAFBC09B375BABFFDEACCD12E6E7BD347CBFBD0C84CDABB5004CA11DDC6D14C1BD700FE3EB2371E3293F7185E2A065532C3B6529E60240E7AB6456139D66745F17B94FDF2C54B13EE4DEBF1B77099718804BAEAAACD2BC60A190487CDC76AF2EEB906E4C9F2664A30FAFB65013B8CA393793B650CAC4A93377A6511D739C2136CEC59E1BD14584989A591E1F3B7F6D7237AEDB556880810FABDB1D7F8250B61A2D16A3337DA65AEA644D7E2226BE5F24CBE01C8A33A4CCA06F6F646A3F5453FE2D9FDEA8D8613F491BCF2AEA950DB1D9B43C7C3F86FA2F4A51CB44EB9761363C38723852925247D92E37FC694D2CB00248023D5448CDE2867125250B17388440C188F7E500CEF7747A101E0BF2521E2C8A2D04F42D834C0274ECBC73E94612CCDB1C4B908BAF63C09C945AD4645912A0666E9844A1614B7F34415C1842F9B1C7DAF7EE4459A8724B7050F6B5833341691019149F351A7F11AE2416DCD5B36F18B1A4B82CC3E924114CFC126CA309E319D497A594B0AB2AFB58C19DEF3BC3AD885B29AEAC81F346A19683B8577F4A1E0F30BDC85A3814CD1196E6B29E55E5C0E4E028872477CB675B2408E136D15E54C85E8A468423CB795D9348BFCC975B4EC20A23991E6E9EF91D676983AC26B66C71548FB46C4BF06E280D7C55E7B8DB90743A8F893F95AEB4DED1DC65C5E0B61FBAD9DA0DDAC274591AA6CF23C79C09414356584F0BE02CE9B500A3EE6BD4FA0119783F50E800ED36D3A4445934DCFD87A31AF3ABC02CAC39C4B28068EECC6D16B6FA187A073BA143209C0F38AFE100BC700D461B1B364ED298AAFDFC716FA6E3870E6258B66645091FCF9413EDF6BC79B75132A46D1DFBBCE3CE9B0558EF003929CC6E3D57BC4FD3092EEAC4ED71B7B7FC70D0E65901DC9196928C5B8CF4A63C62797727C192CF1CE4315120A57D4C8CFD03143AF8754432EEBADCADBCD26C2E3A14BB43A951AFDC19EE67AAEC5DE0722E9D11E3627AD1B624ADF0FB6FD2A6733B2B1B1411DD14EE87AD3BCBBCAD2EB4A38EA00575BFA99332400083FC519C3733F6EDCCAAF71D09A7164E18A9E9587A8D9B9A46563FD3F14BFA2F2B8EBD9FDEAAEF466E591F502151E43A7E1123273E5E0574814B20253A17917D7BDF8370BC50461AC8D86127DC527B8290FE386F1AC1E6E9D7B493BB7FEDEC9E5A82DC1402DEAE71B18AB4B658E43F707259039EB9978D4FB0D62839A0DD8E3A1183CE330D57BC7927F7CCF06BA10A0478B7E2EC818195171AFF75C29B283E759F4D2F5D55F0FFC35E0581D98E582107BF64A6D80603\",\n          \"k\": \"47033B02A6DC056FFEB5FC1E96205C166374AB84A5F3F7B06427BB006E71A5A4\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 92,\n          \"deferred\": false,\n          \"c\": \"6095A951753A644DD898D69138B4E521A704DCFAAD44EB53E284F836A469349C5B9279248AFC57AC93FA34A643DE02B724615CF5865927FED60A6B41E4AB15B4DA3599F13D2C1996C6D6989443BE6FB81F5BA03BDD53462BE5812A3E177876A102B0EBDFCB16DE7B29B5123A79DD82E5CD47ABA02759FAF5401E3BF03144A90AE957EC04DB9864ADE1C5A700CEC7872CCB64FF931984DDC3FB8D4971D761E5544130278C75A1B04E641E070A747789A71E09409C155C7D341D5F828A575EE74439155930DF22FD7716185BDF917472432A30A6762C9FE1A254442F755804D295B1698B47A67BBFDE178200F9CC3D4C705F4AC1B00C372D468E16ED3CBAAA862A2574A9574A7280878BB82DA7BD1B2A58943456838F2E6AA9F6EF1827C5B24FA09DE07E9B3153B0F44A4F2AEA7610F9CCA92565740E7295BA3AC5764A20A44D4E1862E55B1DF7913B279F438B3B34E0C22FD90E06497F7DCF8D62352447C2B8C51C214796194CDF66D5001278D0D55F82FA31DAA72BA6CDA34E60D696ED79C7056BFE97265F3D1BC07719B745ADD4A83404D91A184E629FC24AE236CF6AFAE46295D24B431D819E366F51E1BB2B44B1FB7A3060091DEA1D416268CA550EE4E41FCA1F387E941DBE4EBAE222D3CF625632D1A61414038FD437BFA20005EBC404ADCDE2DC10DB741A3B7534C40822520C4703FDFB6B380F7DB72B725B330D0C20DF256BBDDC31E0EA20E636A9FAE310185A5081923BAFE041AC6FCD4E73F5F7237142B74681F637996D28C3FDE6052243269D19316C56993722EADF19A985E579ED559F971E69EB5125937EBC80ECD15A4F80D7067905A4D39C6220EFE43883CF22E9A366F8911E21D0491B8FF61FD07B733E707A08DB400E438DAA00D481C5AC62064CF47AFE3AB08027B3890E8C8835CEAF8128F9D887A6CB7FDE879D9611C01281A0F02DE0E969C9131F8512138036EC1967DCA45AA30BE8C5B1008113E17A91D9F8E9995C07C0B13A45668C96356F09C3E08FE4C7DF5F7230E0C93EEF08E8958B55E213718C516E624B57765257D21696A3458FFBA11DE708C4EE9AF2EDC5F37458DEC8B985076882D3F4DEB00BFD8E7EA4D57BAEAEC6BABC0E28C15419CCD785CF6ACEC96D1111CDD1DA9A151F59A7366B64A53F0497D3B5A8ECB60D7C220E99126CDE82938C7E131BD841300AE461A1817703ED5B0510B47F2C2980F1E11CFBECB524B295C42187F15B0C9F6B0EB1E70B3EC43ED955528B1E42E2BCB31F3A1CFB5E9C807E8D366E9227A87784748B277D6C885B1385C6C691B3DBD7841DD89721B3A8BF96EBA99C53D4BB3B41DB9409B992BCC2D8FC53E70723CA1FDC1341A3E608D7F62F2322C6A9BA1316639690A22AECEE364B4F13949A0310FBA1A0E35DDA5FF840DABAC55041B0931D9EBEC89B78DD930512340B4B5D0877AF546FF0F342FB76B647D604EE2E20207924F39907D6E72DD4A9A1ED0B6D7364CCE69981F56CBDEDD51CBAF6FDDB36E327AD65D4FE283D253E6BF3C7969FFF1F34DCC742\",\n          \"k\": \"F0CF9CF06A81EE545A33B310616117D6096FB56F0D4F7E49FE0A37550320D3C4\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 93,\n          \"deferred\": false,\n          \"c\": \"2AACD2E6B884BE6A3DDD80155BDCA80EBAF0E2BF714312BBA30D5B367F2D95AC7BEC3965AB05AFA370A42A512B5EFE4B0DEFF3E163AF186B725BCAFD2AFB2BD2A0DBAB74C2BF9362E27D69B6B4B5AA6500EBC9316EA4112745F1C6E98F2DEF9132C7C0BFFEAAFAF994C89B96D3F436B875178963FBC18D2E06ECAF3871787C1AE93B3210896837EC1DA87F0FD8F14AB7C5CB2531E90F415FEBDA378E5492E1DEC8243FE2E8A7BAA6FB6A034D9C524E99D848A804F150915BFD66067C8603B5DB0FE29E27D3F6CA629E96BF3E9C77A5919701EC19646C69A73DFAAB0ABA28FE3E9EAAEB475A441B9B0D62B259DC6B77DEC964AB57D5D776988D54E6246C526F1E8EFDF454E7F0DDAED5363CE02B279CD3B554C251793C3A616C07A7BABA8062919A2B46C64C152BC887A27E382254EA6D50CCC0702B7BC0994BAC09B7891FA64A773AE0B4FBF8204C13A4950FC2C4DF60CEFED7582FD9FBB8C83442517BA0E3B60D9A04FBB24ABCECB303E3FDD37F1037741FD2489F632192A6B9C122A7344CB781A0F61D5011EB0251A842AD4838F9B8D52E21A783F0D839E8BA221CCDD6B968A2B5FD21B8458BF53C9C8076AC0C52C0F53097ED1C25C9F6F12407772D6743EB8E0CE8B1A926F0FDD0DB00482D9590675E56D4509CB5E5F32FC3B4A2DAB2BA080F9A7CDD0B611742A8F83CEE1B091E629D2A0371FDB5A64412B5FA63716961527640D02885C4A09B04A3A6F5EC01A9E0DBB8FC4DDD9E05BDB240AC4878F0D41461C4661777417D6150422FEAB6A39F156CADB5F5D3BEBE417BABCEFF5AAD1B7A624FC23ABE28B2AB2E8273E8F44636A60CDAD9236DCB02FCF87722C899AA321C564B25BC33B4976BC9603BB8B8AB18B5B04625981FB38B2A42722CE2358FC0BA99EF4B122C7B70BB347D0D482DA30638EF8B9C1D9121D83BCDBBEB2A608617054F4B3FDD33E9A08F8DF999A98E715DBF04F8EFACF123BBEB37B9038E9AD906E3C570BB398C10E6D36647A2B0B2731FD39F726171EFC7321BC67D936F7989EA58336E549A34B73F097E3EA2C25887EC6A2E9FED5D2CFF475E99F392162D959DE1C4A4DAD3C96542756AEC3367F7B2515F2225BF7B704B780A6D0B279B8B4EE4879A9BBB2F3303216CBADEA00D229C03E3E2843892FA8E5B0A600D0E3EBDD14FA229819CE9C10B8D5F393DE0119A5B509B80D56B06783447F931177123824910C9BFDE9A29FBA0252E69A90B3E717832866115C06EA73B033EC3B0D45DFDB69A76B484DB0BE7A81215B3817E1C02F9A5DEE8967B147DF9F63C93A6E396DE4251A5A706DFDE9670B8B2F6C4C3E2509142256FDDA905C125FBBB294EB29A3B4D9BE3B67762AFC049B96B3F41B8C31BC5D7B522DCD1AD12B252370A8A57E42F6A9AC26FF784B374DA4B86FFDB65CC753CD049F1A21CF832447E1DF7BA7D0D11E403FC18BC545501E16568595AEB6BD7811C214CF2FB1CDFB07BB32321F536E3896B6EF4D16130ADD71B271CD1027E35538D9E475A3A53DFEA430C151DF7D516CD0D9B\",\n          \"k\": \"0EA983FF9D76F056AA42BB772AA27C8A163172F43E6BC9BC55B83038E095792B\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 94,\n          \"deferred\": false,\n          \"c\": \"8FFBC80E4662864D6F373DC8837AA91B3CC26B68124ABD73DAD025A1D1C18829DCF077D303579E5F39F4BE101BB9E355DFB5323882EACB3D184E6812C03A7BEBE25166D55F821A00F80B8D2BAB1A7EEC83D384AFDF30F6BBC9960C4662067EF7E200E37268B9F5348FF484642799258B45E541101A21FDD6FBFAA2374A28FAA97204953B95BBD1BB519785210DA7C8A09D071D8AFC9B29F2C3C2909A4C53671408B8083BCF5AE03D45C0CFBA399F44D24A06321BB74F6863B7D4BF0BFE73C8AF8EE1DDA45212E3F9C853D4D0E16F8EBDB8581C4ADEEE833D81A9E0A9E8587E9C19E689E6DF715564BCE27CFA73BA16226A77CE44DC496992F41AB918643C6D86A8B26ABA6F94F3502D22DD94FE55483F67C635B307745D33F17133293639118E70CE42C6DB7332D4862C73D5B84415454AD51F89B5559B5C85D6B6ED47B6958F21FBC2ADF8C8A9D43FD2E1B0C02418D227B83F85CBC3A81C719E8602781AE71E15E6D714919E52FCCCFD9A68B4751825BFBB53B7940B15B546158DBBC612E602F660B9E0FF439E0156C4C8792346014BA1B4838C7425AB34744DE51D854CBBA58B7E67E014122518036CE1541A1675AFEAE4F29A5318602ABBD0A1540F33176C984E306098DBD08E822ABB55F9FF38D9E31EA4695150F2CB60BC2EB5F4780CBEBB210CF48662C454C7A42360F306FB03617C998AD8A9297D6B71A71285F7AE8DFB336FA922540C92DC71F777D3B4D11D87B8D082FA8A00DF647CF7FEB27403D3CF50D829EEE3575A01E2CCA57849B11B14F001BE180DD5FA13C03B98EDEA6358C5AB30A526027CB45E33E646B37988CC84B979CC5CFC3BFDA05BD2C7B8CB1B11AFEE007E20FCCF8D0F764F4A6D2F6A8B74281800CBDCBBCF0DF1EC9D27E6A94968604D9EFD37928B6856C48F0108155595D03231DFC22DC0C8EE614090F37E0828B48A4DD371C677B5DBA95E417F12C9A396875FB05623F7A544AEAE41A0AA536FB8D767BA2E14752C84E147149F655AE7B903CAA591AE00267ADD3EA816612AB0B9A5FB263C70C4367062F7794274C75AC66F706AE93699859D55B2E4960E9D538F38A2FAEE366B80DC78BB673A9E1B057D711F9DDB3770947E6DD7BCFB425B96670506758AEA39A5ECB33A1B76B822AF903787DA3B61A7B9263C0FAE1B729B1A2E16FEB50C32A8728181D4E8A9F8376C39F6AABC2C022306B05E494CF9B6ADEEEC95887440508981D6A74707FCEFA24B9F0DC3AABC984E9C44174E6DFB51FCF4588C57F9659A8E7A6FAEAFBAE7ABE4600444936B3763463D4AE411DDC1C98585E0DE58867251079BE72075973275141801B98F7B9397C096A56B8CD83CFBD374E182F7DCC9A7C764DBBF4D7576A1CC9239848E7295D29CF034A1A7AE33A386C3DDC24A535168ED23D7ADE9433B50DC5694C969F4C546EF2293CD842F4B62B6B7435F597CF5C1733884E0A6AA47FA31887DEDC6C402D8ED013E49E5CAD7718CCEFEE0E6A041715CC9ADD79965413049ABCE88636AA7543EE2601F162838EF6B\",\n          \"k\": \"342765B77A09BA6863F2ADA782E3719803F7AB714EE807DE89A1617B5C74F60F\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 95,\n          \"deferred\": false,\n          \"c\": \"17976BAC62F66CEF2B6F947C121079B6F2E9350C137E738BFD884FF2BA6E211640A30FBF2695EDF7046E1F5234AB1C8A9B0E8A3FF88EF18C1E5512D5F69E4A36CC9362F00920481E5460B1FB0C2B9FF0CD0D95718966AF7EC1F76B8DA93F6AB179A5DE70DEE34C579E284AE8504ED96E1A85898076F69AAFEC1357533EBB636FBA2372204DAB87C47AF27D4D9EB1B4FF4286D6A9FA7FD506C9FEBA596D2047DB765C1EBF1F7921867D394487F6BE926E6B0323058CB591195436ECC805C8B88615C7A03833AABF490337063DFEED698F7DA8DD589A794C956C2BF8D8CA4AE18B0A7767693802BD6DD53F543E105EC526C1D1D00AC9C0B606BD9B3A1D52CB8C56F8535ECADD8239308F2FE7E1D7BFAC5848B547B4579AFC13A0B2BEDEFA46322F92E2B73980695369C5F48D37F9345F20C7820DB6DE09D5E8313B73ED705B33646FB14CC4D40D65290A4C27360FBBD080E61A16BB15E9560A097E4AEC16F8B8030FAE1D47E024F10C33E6A1C56AEB8EC2F6AD6EF4B8FF04C67307B23E470FB3E5BCB6F533F955C36FDB46516A07DFF2956130AD0924158CC2A083378FB9AE32DE89CF774D82C2FC70DA48536372299C61927A5AE67E55E792B64FE61F06EFFC1F216CC9D739ADBF3B2190E1D080E00F169F145FE32AF7EC7CBA1D76FA6839D5FD2068E1DFFF557755FF2F4271204A5468C79C7BB8D00FAD63938F12D53B243B3FF866556913EB57AD2AE034F8B62B1A1B9DA2B1D45800B4CEF1E1943A0C92F0EF2EE924F80CF67EBD3D0199D45ED4DCC00140829A0992DB43616CC468508B852EB822066A05CC91D6BC2B47E5622B774F8128ECBBB94CADD15588B36A71E9FD97B05D69E8BAF00D30A3D3C00E663E00AFC9F5E1BAC8534ED5F6E5AB47D7EFDF6537753408299A9E8D5F5AE0FE36A9EC41C6DC9F78A891BFA9C8E90AA1A457A0C01AF70CBC9E55B68A5D8CC5CD3BD6886AE11FF510C6ED0EB2F5C081B25989518BA217BC1C153864E5BB312EF0D43D6DA4A0FDE44F1157CD238E8D70BEB420BD310F8E5DB9D74EF4EC9980CBA74358FC77C5D4FAE3036E176647D78C73900C79BFBF0BC545ABF7CBB4DC7F6041D4FA3B66E4D4655E24B11DC30B0061C452A605CE73362F2A3F052370D873FC68DFFCD3999FDEDB45DD9F2A02B4699BCF1FC5F888B019B5028465F30AEFAD946D481285D1122EA78F3BD8B1982558C38FA3DF0F058B12EEBB11F4C7809F6334EA1D7FE0B529C0BC9C67044648178D2AE9232E4E88DD6D0016D8A590B7703F1A017A4A2671BBB24FA97ADE1B61C489AFE9B3E63CF4CCC42168C98880921C2C0EA7D24DB6DD676B77F7B6C0525C8D0578C7F5A20DBF2F82873904D7CF2522CE6360397B254B18C3059A4BEA169A44D9BA17CFDA1827EABECD269FD391CBC0D49D71FA81AC16F9A0DED9E72A58D1BC2262979D8D7E531D1C46A8F107BDA18A1D2CCD17334183DD3E79D905ACA7DAD348BC6D5CE124A1397EB3B89BE7580720B5DD00BD3A63DAD813E0E967EFEDF17F3D960E70A4F83F\",\n          \"k\": \"F175CA29D36784E3B7A6F6D8682DE3548115C25EC1751DAF6B5FC3318F690802\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 6,\n      \"testType\": \"VAL\",\n      \"parameterSet\": \"ML-KEM-1024\",\n      \"function\": \"decapsulation\",\n      \"ek\": \"3BC1858826C6B39279C2DA7438A370ED8A0AA5169E3BEC29ED88478732758D454143E227F8595883297842E6AF133B17E4811B0F5713AC73B7E347423EB92822D2306FA14500A7207A0672672046544ACC4EA9C16ED7421A069E0D737A98628519C6A29A424A868B46D9A0CC7C6C9DDD8B8BCBF422C8F48A73143D5ABB66BC55499418430802BAC544463CC7319D17998F29411365766D04C847F3129D9077B7D8339BFB96A6739C3F6B74A8F05F9138AB2FE37ACB57634D1820B50176F5A0B6BC2940F1D5938F1936B5F95828B92EB72973C1590AEB7A552CECA10B00C303B7C75D402071A79E2C810AF7C745E3336712492A42043F2903A37C6434CEE20B1D159B057699FF9C1D3BD68029839A08F43E6C1C819913532F911DD370C7021488E11CB504CB9C70570FFF35B4B4601191DC1AD9E6ADC5FA9618798D7CC860C87A939E4CCF8533632268CF1A51AFF0CB811C5545CB1656E65269477430699CCDEA3800630B78CD5810334CCF02E013F3B80244E70ACDB060BBE7A553B063456B2EA807473413165CE57DD563473CFBC90618ADE1F0B888AA48E722BB2751858FE19687442A48E7CA0D2A29CD51BFD8F78C17B9660BFB54A470B2AE9A955C6AB8D6E5CC92AC8ED3C185DAA8BC29F0578EBB812B97C9E5A848A6384DE4E75A31470B53066A8D027BA44B21749C0492465F9072B28376C4E290B30C1863F9E5B79996083422BD8C272C10ECC6EB9A0A8225B31AA0A66E35B9C0B9A79582BA20A3C04CD29914F083A0158288BA4D6EB62D87264B912BCA39732FBDE536A377AD02B8C835D4A2F4E7B1CE115D0C860BEAA7955A49AD689586A89A2B9F9B10D1595D2FC065AD018A7D56C614471F8E946FE8AB49E8226591119FCADB4F9A861631378736B6688B782D58E97E4572753A9664B6B8536812B25911AA76A242375433192738EEE762F6B84315BB3436231E0A9B277ED28AE0050728346457E13405062DB2804B8DA60BB5C793D4CC0E101CBA2D9182FD7124FF52BF4CA28292AC26D678088953971DBA0B6FEC2C9659353291C70C5B9245A0CA253304AFD3C95102BEA66875C6201680B4BDA38687B648C28EB37478E3BC00CA8A3CC27204642B42B68FCBE7B21A366D0668A5029A7DEEF94CDD6A95D7EA8931673BF7112D4042107B1B8B9700C974F9C4E83A8FACD89BFE0CA3CC4C2FCE80A03D3576C222A792B72B1F070AB7F6B6F2B5CA2AF5054AFA70A896990159B45D1003E2A05648675E596016F1B71DD0F7BDA7E2097FC73B3A143D12C726020AC34958AD7062B92B9ABF3CA6BE5AE29F57135E625A367971837E6363D1532094E022A23467CF932E1F89B5B0803C1EC99B585A78B5865096746F32258214ECB38065C97F455E155ACC2DD005A9C76BED59CDA73837D303504E6C976A606A2BE7BBEC5948B91A349E8936688CC0279754B743ABC58666B19B6C3260051F19206BB962BB6633EB0048E32BAACC5B020D02C86CA9770AD469DB54A106AC73A35B8057422B3DB202C5A5B4E3D535F0FC99326C4B8B7B16F1CB5AF96803FA8C195FC0BCEDDAAF012A51728B76489082373C91E92C87ACCA795160782E3B0DD643544BB96ABC2708D49B759CF057AA223BAFD96A330BAF39810FE8671B4343C297DA1E1969C996216AB5106DA668941B160D4477017136CBCA5B5A8D44C4A8B1CF3EF79785E5AA25C3A1AD6C24FD140F79207DE5A499F8A1534FFA804AA7B3889CBE25C0414704AA57897F17862364ECA56258007248813912B836497F0359C2F7238A05D305A0EA152E72B44417A868134E91B3CA7931232FD4C25F8C2A492A339CDC0A138967211451F2562678FA14080A34436C42B07865AC036A81E97A7787A938025CAF813450368BED0C94B1857604526405D27A1C1ABC81B5B6EC13C71930A97D9232CF7021EF87A4D155328E62B583A83B4AF21F9F5750F8575150424F63B899D71CAD267C09E4467146E16E9B6C653F008C311375E2E006D4076A546B82F5314222F7C654317E79EC6035B73FAF491757E61C828326D53044541C4D4537ABD3EA1E67998C3382974CA78AE1B1960E4A9226B0219AB070F0D7AA66D76F9316ADB80C54D6499771B471E8168D47BCAA08324AB6BA92C3A70275F24FA4DC10E251633FB98D162BB5537202C6A553CE7841C4D40B873B85CA03A0A1E1CFADE6BA5180AB1323CCBA9A3E9C53D37575\",\n      \"dk\": \"8445C336F3518B298163DCBB6357597983CA2E873DCB49610CF52F14DBCB947C1F3EE9266967276B0C576CF7C30EE6B93DEA5118676CBEE1B1D4794206FB369ABA41167B4393855C84EBA8F32373C05BAE7631C802744AADB6C2DE41250C494315230B52826C34587CB21B183B49B2A5AC04921AC6BFAC1B24A4B37A93A4B168CCE7591BE6111F476260F2762959F5C1640118C2423772E2AD03DC7168A38C6DD39F5F7254264280C8BC10B914168070472FA880ACB8601A8A0837F25FE194687CD68B7DE2340F036DAD891D38D1B0CE9C2633355CF57B50B896036FCA260D2669F85BAC79714FDAFB41EF80B8C30264C31386AE60B05FAA542A26B41EB85F67068F088034FF67AA2E815AAB8BCA6BF71F70ECC3CBCBC45EF701FCD542BD21C7B09568F369C669F396473844FBA14957F51974D852B978014603A210C019036287008994F21255B25099AD82AA132438963B2C0A47CDF5F32BA46B76C7A6559F18BFD555B762E487B6AC992FE20E283CA0B3F6164496955995C3B28A57BBC29826F06FB38B253470AF631BC46C3A8F9CE824321985DD01C05F69B824F916633B40654C75AAEB9385576FFDE2990A6B0A3BE829D6D84E34F1780589C79204C63C798F55D23187E461D48C21E5C047E535B19F458BBA1345B9E41E0CB4A9C2D8C40B490A3BABC553B3026B1672D28CBC8B498A3A99579A832FEAE74610F0B6250CC333E9493EB1621ED34AA4AB175F2CA231152509ACB6AC86B20F6B39108439E5EC12D465A0FEF35003E14277A21812146B2544716D6AB82D1B0726C27A98D589EBDACC4C54BA77B2498F217E14E34E66025A2A143A992520A61C0672CC9CCED7C9450C683E90A3E4651DB623A6DB39AC26125B7FC1986D7B0493B8B72DE7707DC20BBDD43713156AF7D9430EF45399663C2202739168692DD657545B056D9C92385A7F414B34B90C7960D57B35BA7DDE7B81FCA0119D741B12780926018FE4C8030BF038E18B4FA33743D0D3C846417E9D5915C246315938B1E233614501D026959551258B233230D428B181B132F1D0B026067BA816999BC0CD6B547E548B63C9EAA091BAC493DC598DBC2B0E146A2591C2A8C009DD5170AAE027C541A1B5E66E45C65612984C46770493EC896EF25AA9305E9F06692CD0B2F06962E205BEBE113A34EBB1A4830A9B3749641BB935007B23B24BFE576956254D7A35AA496AC446C67A7FEC85A60057E8580617BCB3FAD15C76440FED54CC789394FEA24452CC6B0585B7EB0A88BBA9500D9800E6241AFEB523B55A96A535151D1049573206E59C7FEB070966823634F77D5F1291755A243119621AF8084AB7AC1E22A0568C6201417CBE3655D8A08DD5B513884C98D5A493FD49382EA41860F133CCD601E885966426A2B1F23D42D82E24582D99725192C21777467B1457B1DD429A0C41A5C3D704CEA06278C59941B438C62727097809B4530DBE837EA396B6D31077FAD3733053989A8442AAC4255CB163B8CA2F27501EA967305695ABD659AA02C83EE60BB574203E9937AE1C621C8ECB5CC1D21D556960B5B9161EA96FFFEBAC72E1B8A6154FC4D88B56C04741F090CBB156A737C9E6A22BA8AC704BC304F8E17E5EA845FDE59FBF788CCE0B97C8761F89A242F3052583C6844A632031C964A6C4A85A128A28619BA1BB3D1BEA4B49841FC847614A066841F52ED0EB8AE0B8B096E92B8195405815B231266F36B18C1A53333DAB95D2A9A374B5478A4A41FB8759957C9AB22CAE545AB544BA8DD05B83F3A613A2437ADB073A9635CB4BBC965FB454CF27B298A40CD0DA3B8F9CA99D8CB4286C5EB476416796070BA535AAA58CDB451CD6DB5CBB0CA20F0C71DE97C30DA97EC7906D06B4B939396028C46BA0E7A865BC8308A3810F1212006339F7BC169B1666FDF475911BBC8AAAB41755C9A8AABFA23C0E37F84FE46999E030494B9298EF9934E8A649C0A5CCE2B22F31809AFED23955D87881D99FC1D352896CAC9055BEA0D016CCBA7805A3A50E221630379BD01135221CAD5D9517C8CC42637B9FC0718E9A9BB4945C72D8D11D3D659D83A3C419509AF5B470DD89B7F3ACCF5F35CFC322115FD66A5CD2875651326F9B3168913BE5B9C87AE0B025EC7A2F4A072750946AC61170A7826D9704C5A23A1C0A2325146C3BC1858826C6B39279C2DA7438A370ED8A0AA5169E3BEC29ED88478732758D454143E227F8595883297842E6AF133B17E4811B0F5713AC73B7E347423EB92822D2306FA14500A7207A0672672046544ACC4EA9C16ED7421A069E0D737A98628519C6A29A424A868B46D9A0CC7C6C9DDD8B8BCBF422C8F48A73143D5ABB66BC55499418430802BAC544463CC7319D17998F29411365766D04C847F3129D9077B7D8339BFB96A6739C3F6B74A8F05F9138AB2FE37ACB57634D1820B50176F5A0B6BC2940F1D5938F1936B5F95828B92EB72973C1590AEB7A552CECA10B00C303B7C75D402071A79E2C810AF7C745E3336712492A42043F2903A37C6434CEE20B1D159B057699FF9C1D3BD68029839A08F43E6C1C819913532F911DD370C7021488E11CB504CB9C70570FFF35B4B4601191DC1AD9E6ADC5FA9618798D7CC860C87A939E4CCF8533632268CF1A51AFF0CB811C5545CB1656E65269477430699CCDEA3800630B78CD5810334CCF02E013F3B80244E70ACDB060BBE7A553B063456B2EA807473413165CE57DD563473CFBC90618ADE1F0B888AA48E722BB2751858FE19687442A48E7CA0D2A29CD51BFD8F78C17B9660BFB54A470B2AE9A955C6AB8D6E5CC92AC8ED3C185DAA8BC29F0578EBB812B97C9E5A848A6384DE4E75A31470B53066A8D027BA44B21749C0492465F9072B28376C4E290B30C1863F9E5B79996083422BD8C272C10ECC6EB9A0A8225B31AA0A66E35B9C0B9A79582BA20A3C04CD29914F083A0158288BA4D6EB62D87264B912BCA39732FBDE536A377AD02B8C835D4A2F4E7B1CE115D0C860BEAA7955A49AD689586A89A2B9F9B10D1595D2FC065AD018A7D56C614471F8E946FE8AB49E8226591119FCADB4F9A861631378736B6688B782D58E97E4572753A9664B6B8536812B25911AA76A242375433192738EEE762F6B84315BB3436231E0A9B277ED28AE0050728346457E13405062DB2804B8DA60BB5C793D4CC0E101CBA2D9182FD7124FF52BF4CA28292AC26D678088953971DBA0B6FEC2C9659353291C70C5B9245A0CA253304AFD3C95102BEA66875C6201680B4BDA38687B648C28EB37478E3BC00CA8A3CC27204642B42B68FCBE7B21A366D0668A5029A7DEEF94CDD6A95D7EA8931673BF7112D4042107B1B8B9700C974F9C4E83A8FACD89BFE0CA3CC4C2FCE80A03D3576C222A792B72B1F070AB7F6B6F2B5CA2AF5054AFA70A896990159B45D1003E2A05648675E596016F1B71DD0F7BDA7E2097FC73B3A143D12C726020AC34958AD7062B92B9ABF3CA6BE5AE29F57135E625A367971837E6363D1532094E022A23467CF932E1F89B5B0803C1EC99B585A78B5865096746F32258214ECB38065C97F455E155ACC2DD005A9C76BED59CDA73837D303504E6C976A606A2BE7BBEC5948B91A349E8936688CC0279754B743ABC58666B19B6C3260051F19206BB962BB6633EB0048E32BAACC5B020D02C86CA9770AD469DB54A106AC73A35B8057422B3DB202C5A5B4E3D535F0FC99326C4B8B7B16F1CB5AF96803FA8C195FC0BCEDDAAF012A51728B76489082373C91E92C87ACCA795160782E3B0DD643544BB96ABC2708D49B759CF057AA223BAFD96A330BAF39810FE8671B4343C297DA1E1969C996216AB5106DA668941B160D4477017136CBCA5B5A8D44C4A8B1CF3EF79785E5AA25C3A1AD6C24FD140F79207DE5A499F8A1534FFA804AA7B3889CBE25C0414704AA57897F17862364ECA56258007248813912B836497F0359C2F7238A05D305A0EA152E72B44417A868134E91B3CA7931232FD4C25F8C2A492A339CDC0A138967211451F2562678FA14080A34436C42B07865AC036A81E97A7787A938025CAF813450368BED0C94B1857604526405D27A1C1ABC81B5B6EC13C71930A97D9232CF7021EF87A4D155328E62B583A83B4AF21F9F5750F8575150424F63B899D71CAD267C09E4467146E16E9B6C653F008C311375E2E006D4076A546B82F5314222F7C654317E79EC6035B73FAF491757E61C828326D53044541C4D4537ABD3EA1E67998C3382974CA78AE1B1960E4A9226B0219AB070F0D7AA66D76F9316ADB80C54D6499771B471E8168D47BCAA08324AB6BA92C3A70275F24FA4DC10E251633FB98D162BB5537202C6A553CE7841C4D40B873B85CA03A0A1E1CFADE6BA5180AB1323CCBA9A3E9C53D37575AB1FD9E7316C6FEECB0A14DF6F2DA56C2F56F55A89635CFCFDA47927AF1F0A47B2D4E4E61634B1B51D37A3A307A972420DE1B7A481B83E583B6AF16F63CB00C6\",\n      \"tests\": [\n        {\n          \"tcId\": 96,\n          \"deferred\": false,\n          \"c\": \"0C681B4AA81F26ADFB645EC24B3752F6B32C68645AA5E7A999B62036A53DC5CB060A473C08E5DA5C0F5AF0E5170C6597E50EC08060F99B0C00EE9BDDAD7E7D25A22B226F90149B4CE887C72FB60AFF2144EA2A72383B3118F922D032A16F554289902A14CF7755512BB1186BAFAFFE794D2B6CDE90109E6582D39CE0C96197484B3FA07FC91D394FC8D88E7FC4BE002E2DB56F0C4D9D3FBDA274536A0B86ABC6E39BDA52931AEBB8F1084C5C1F7CB3177788B7F331B7074361163491D428E78BCBB57B630841AA987333377CF09569CFD14CC2A11C501BDF82C93DE05BEA20060DE89C686B824571CEF94AB3FDAFA8512619813669D4F53637FEFA4D028CB233E56930E2235F7E6034CA94B143B77AD4A68756E8A9184DBA61A89F91EDFB51A39211402473A5F89145736B2BF8569C705B0CDB8980A447E4E1EAAD3E7E0578F5F86B8D03C9DAFE875E339B4423845616799EDCE05F31B92664C5A59253A60E9D89548A300C1ADB6D190A775C5EE6E8A89B6E779B034C3400A625F4BBEDBF919C45B2BCD14C669248FC43C3EF47E100758942E75E8ED6075A96D70D4EBD2B61358224DDA1EC4C19C2A92898176FEB3C02EDCB9908BAE49BD94AF028EDF8CFC2E5F2E0BD375006986AD49E717548E746FEF49C868BCEA2790AA97E04061B75605CB39EFD463D7B3D68BA574434FF7BE8E2B84BFC47E67E9CD15F3ED450C61AFBA79A20B0B6F287777C72F4AD248174F1959477AA7A7C97F122C50447C7484F382BC47D81FCC9C7E892C8839D37B35394B53E6B2B1895ABB0DE8C98F2633DC4413A8D5735DFC9A64026B6F34779D6AC8AD99CC31AA898C2E7057F3DB8A1A8A98527A79E43552F28D1023E1F6A6B84855CF5E6DF889BA269F048946E84021C65C5A93B007B07741C1EE176C73949110F548EF4332DCDD491D2CEFD0248883F5E9525BC91F30AF17CF5A98DD44EF9A71F99BB732985BA10A723EF476FCF966DA9456B24978E33050D0EC90D3CE46378851C9ECFCFD36C895D44E9E506993082523D26185766B23568CB95E64108F89D1014747C67B6F3C8767BE5FC341227DE9488861C5FE811409F80957D07522A72CF6AB0378D0F2F28AF548185C3936777994466A019D33B18A54F380A33892AB4D4BD507B5A61D0D358341AC92F07B43B8F6AFC6991BB6A1EAC23CA6F73E91F2464BD119098D7E768E77ECE53FB899BEB42265ECF7B271F66546282D472C36239006BB0ABABCCA24550BAA0A601348C810FF5F9EE504BF7155DEE4141A11605A4F3509AC9CAEF6624D21DE332D5D50828B52E92885D3B90553B14463AFB1EDCCD3B569B5A7F00BB66769DADAC23AD8BB5D73A6F390E6FC2F6F8EE3CF4009A5C3E1EF60E8F040672D262E6490379BBC70495DFF237BECD9952CD7EDEB6D1DFC360B3FC8B0AF480FFE024AEEFCD4E9CE95D9B469C9A70E5110DA0BAC124FC3741DCF49116261796504D5F490B433C33C40EDCE2B75151DA256A868A5E35F86226B8151C91934CCC3DACA391DECCA745375660B6EC41AE5D810838CBEEFFA12557884412357B1008363D32B237AA1DD8E2D9C6367ADA09B2C95060206CEC3EED391FDC5DBEF6F08BDF0408E585AE5EBC8E9745D44FECA975ABBC140BB37B8ADD16FCC2956910DC72BB3F02E9A130C9A84F9CCB74D134CDF40AFCBA2009C8F0040239BC99220EF64C4DCCDE2E2E5C9B68602FBE8EF4C98B3468C79DF4E078511BFB8AA3DA09597A02511E7C21A7CF66A93843A94868F19E8552552E3ACDF6CB810634DB97CBC4BB569709DAD4845645446FA8D289FC59307B801E60CE2A91E06E9C22C16E2E59BDE38A416BB1B4AC5457438FDC5D64450A89ECB832C1BB279DBF59334681776AC00409846D09D6F687772E340850AB8673384215E12C8D0F531C451E58493E0EE415AD594DF38C34408C7ED9F0C392F1534604EAC3D9C15465A9A46632214B536990D78078E5BD7EAE2013FFF8FDD8B275C89D97C9353DF3C42A28E814D8468E2B48DB0976D88F5EECEFEAFB8F7F4AF291A728F6249ECF5622339269AA945329E919F8B441C83D5507F30DF0FD2B13FF806F522DAA11AF676A513C149C70F0D6E99A880450A54E0417FE3C1E513E9D920E30A8B42891267A2DC50AD81F98044920C099DF22C73998A25C581A5178C72B17AC875BC68548A0FB0CBEE38F05017B12433343A658F1980C8124EA6DD81F\",\n          \"k\": \"8F336E9C28DF349E03220AF01C42832FEFAB1F2A74C16FAF6F64AD071C1A3394\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 97,\n          \"deferred\": false,\n          \"c\": \"4F90106FF7C3DC4E47417F31AB56B1C5E426C1ECD5878AAD2B705E75062DA5FA6F4D18B704C941C6C6D941FD21191A69210BC39E24950D9F851B6DE8CE30023DC7536439104D42245F3E04E6AA6763F8AC97ADBD04CC69547BCE0BF290FFB5D12946301174AF1B0868C14D4293FA9DCC5B23F809B02CC78DEFE7F27935B9B681E531FC21CCB2AF8EF6144D8498E63E0EE48AF8D4CEF7AC1F669AC740B06F79DDB58E794F2FC2CA832E05A0374C18A4F2CC78343EEA064ABC5F468F4DD11E0B6E8FA1D18A221D8241450C05EB9EDF90D9D7F666AC82E7FD44AF9328E0BC6004D5B114E80E9B980D18E081D771DFCB2ACFD40142A2EB33234F75733EAB7D8EE8A5A6F796681A4A8AF85CCE86971B821D4AD8371049E94E280B77B15D111A42AEADFC08D4F804BD78885443E81A393DF7C8754C460915846E09A0596587460038F55D06EC21434A1C2DF44D0C16706E8D2B83F0E7833976EF05BF1D9F0DDC9A37597E401B817C2BEC8E02EB9DF7591E239F25F8648E7F2F4F673093BD9CB703DA32B353F58514C6AB55748B194E52F153D52F5F33FE95C5F9F65EA97BA721E8DDF333B64D233A867A12701E00C5D8A9B5AE344F3D847C27C079DCC9C3B40EC4604A9F041E7987E8B930C658B9A132DE4E422C0E27553A2A0EAB8C859EB0E5677E83272725C5C1652E61B9BBF5C9C59BC2357A4D1DB9C607F34DC1BA074B84DFC69E4097A7AD2BA9A58000027296AD39FC1CE218A5EEC7ADFA8AA3B9100B0B603CFC83C152589E12E6BD9EE10C49131A701D315DFEC38E018328916F9FFAA7305CFB66781707D2D1020EB782F9F003DB4E46B87D693F62E8BDE170141FF71F26DDF5310C00C9163655F5217DD2C8B0466AC89DB55BD7FB3B0964BC9009E9686185117DCB50D6D0297753CF7F1217E819EE60E3F0FAEC4A5AF0C2EA83CCDE15CF045C6961DE8FF6235C9D93BA4C89B7A82A7471FCFB0B8EAD54D56E8A1DE21B3933AC5B4A0689EEF3598926E17BBB16AEC61EC30A2CCC0E0323EC282887C108C3A4E83E3666493D8653D0E92443808C79D770BFF48A49E65AE089FEC790BBA4C66354EF67A334C1EA5C6C5707B6928EBD1BDB6A940FA242C6EBD7F3E71272421C9082841A6CAD2894BB8AC85F105D8BBC9E6F0A3DF0D7C46F6E2F4CAB904ED157AFA85D4A852220A9636E1E8821643A9E4028D87A430432F09354B3973182385CF5ABFC8F84982BEE0BCBF5D18637399163A09EB45711E07C4458498C76979107CF91B3FC590EA4AD715D656D5E56DC32146580101C952E02ED7017960D54CAACCC70607196980ADBDAEA420A52C0559ED23C9514F8CA7AB7F3BAAFD2FAB58960A64128D5A50E9AD8DB7D23A90CE64C1BC349D118D3603358377F84FF5A64457FA1CF41B27094BCA72360BD429415B9EF9ACCB7A5D7B9E5F5FDCA8FCFA4592E91D7E5120DF7E3C6675AF2211BB94D856A5D2285FBBB36984A1345590930B13232565D54812A9345324C232653190323CC67C840E478D09E6DDBCF999F7AA3B556F80332E67ACA41EC0661088D7696BB64E9A98A0749FAA9854D9B48754023BACAF3C8081A46157C6453BDC89341D3092F3B5337874CE5DE559A56A2FFB7F401F6E28EECAF4FDE5B60DEA73D6B2182EF68E07A8297F3C959E17139B5DEDC72C7A0E103AFF866E89D1F62A1F6B97B61BC059BDE5A2A06087EF783A441F23DD191C692D03C097FF9EE831F7715C6E508BF475E79A8353E84B06A9356045C8FD09FBA35879069B9A3F478FBD051143C13D753BC45F3040E85985EFD6B149EFA9455A18E2894E6EA0BE58F451FF1156F93CC7117B5D091E9DD50D41BFCCD44F2C4EB7812AEFD13C8B68D7F0103BB6CA38D233B6AADD01845B7E44D13C1CB1577D6C4354B063991344787F8C0BE667A7440B98917AD64CC2EF2BC82EFC3398B3B1B238540756CE9FC5EDD26CC20E761D592A1A0530AA8BEFCFE8DADBAC99A417CA0827F4983FF5BE656669F2B5F985FF6B16C44BBEA131D1FCC70FC53BF31EF225D1F5D41863B51B57EA65C6164F7531AE492EFA64161B7DABA3EF4586F3459BE8A962367DC276597B98E91FF594EFE8849BAD4CF91B9E5F244CF03CA9615BE128E96958533544A56E735994B92E4EF0D5FAB54B78EC66641C7463F225D261C144F00A0270741D7A511994833635A8A9B670CBFBEF239BF83327E247943B205DA68DB94E3F3\",\n          \"k\": \"7545CC458E0A274A83B13554224F0BD01D57CC4775AD12468D3FEE5B08C93A6A\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 98,\n          \"deferred\": false,\n          \"c\": \"26CC4F22E035BC00687D557655C46B6E1C447ACB824204FEF7582EB8DBC704D7CE72B0A5FFE54FB89BD7B779B5B1DD1573010B227473FDEFFFB74DF7DCC1E6B48B554563C6C23004AE2CB1996943821F480E91081F1A6765E08A8AAB7F203E95DEEA49A1129A676DCB21540D2AAE1B21223DDDF1453150483176F3EA3580CE631FC85508690D8DDCBC9513A4A5951A440232223FB2ED9E0E5A8ACFEE113D22548B8E98131EE1F45A33656F079870A146F12819BFDDF8792C3C9AC3BBEA3A92B8606FF2B7296DB9D9782C8E788AF4C961840041735DE456A35E5536D861CA118D67408E84D8BB9128B65F2C11C7147EAC928599979EF195A7979CFC48277CF1FDF4B0CAAEB3F8A172A3CA25A3A8C39AAB4495A70E0AFD3861C41A8C01FAD1E9D81281CAE1C33572BA4BCA9A5294000FFD040545B021AF583F56434ACCD4CB7B788517243B09737D355ECE53273FC0C492F251FA02E47EA846121DFF00CBF2767D4DEB25F705591D26FB1B6F839A58EBA4572745A618CB2EBE02CC0CB1C62AA9F0EFB794C385BC47E440BEB38BA742C7357A97CF33098E2EA4D823BD0B9699FB1EBFA806D64FAB18E106D4A97B23A889355C7A2635A9D3BB330A1B8EE5E707DC32C20CACFED68C8DE783562488A64400A4528EF568D833D73E456A9AC22431B2C22441EF5BCE3E77CCEC99D2D1C092ED8A28D686214313F683D4A020FA714459C36A257DDFF7B19B7ED05A16FCACA2570279A11E1439D07F2F23B88411404749C37836585182F31AD65CFEADCFEC3FA905CD4BFE2B6ECAE99D469F3EFC55615D45D19360EBB7C68C73ABD4562EEDA283776C887E70A971176DDC10FC399EAD6B9E247353C25289C0836C626E5376326FE5630C3098436556D61F5C75DA6057008A6E1D50B4F270FCB86F868D5F235428B4D7E13010D20175D4CF0759F56422CF955A721792DEB8EC887E5225F6E52CDFF40B8BD3FEE4DEBC7B363574FD1F3CC113A3B4281F4E8DC3AEBE4B67500ACB50B5DB1BB64F0634B19D4612F597DE2B4CAEEE8A3258DDF8436ACADF3677B46E7E5CF41071DEAD3FBCE2A73388E19AC0C7748E10E3F586E2EB844ADFC079EC0A2CD8C9BAC8E859460DCDAB688AAAA179882B91111A604F75198F55B17C79AD4BE3FDB493B59775ED449BF938B594D87A1C9F721D1C39868591496E62BDBF5CC2947DD81B65ED8CA0BAF0A64E924B5F4FFA88BE86C3594EA7472B822D2D84CDBFC7A2C5039FEC6EBB14FAE2D5D7E9CAF1C2B8788E7354BB6A12C4EA1ABDF0811417586F01553AFD9D8B1EA233066023BC45FA4BC064E7D289AE9DDAF1F985E4BAA86C55BA1F1866E010C55E166C3AA29A682A81195819B7165DF6CC72045D143135EDABA08ACF9DD9FCB8CE732F9CDF1A99C772A2EDAB78647132C33B80E7F03C84A044491B311BC6F3571E7935C6EDFB283BC59F29DD5CCFF9DD6A9640139B173E64F2755F6BBD977F15AF1524827DCE4C2FDF1EBB7C35F0F34800E5A07FC83821FA6CD41695B322F0909D55251372DB8B3CB147FBBF6264BF764B1A20BFA41EFB84D109D4E374564C760AAB66EE823970EE7BFC1D9DB860840BC4767E4A46F1855526A7D902D4FA954C7F337C7C1205FD4AAA70D7F5D904F1D0CF1DBFB63675991B26B590260714920A7249E75D21199D8C002BD702C5398C45A359965D367FA15A73B83197DB3BF3AE9E987479CD81283419E557F993884EA4F17996CCA39FBA8941EDD70FC86E3A46C84C656F77E9DFA5DB31D8761A8FC1D5A2FE9C1CF67DDA1408A212951A5A1D5E9260BF367FD824ECBE8534AA5C63F3E9E2EE4EC53CB42663A79706088A846614B10EDB58B45BF063ACEF64DBB5ED8808588B51A80EC327B95DB34A2107FA96776F1DD0340C7918D0B846883EED35F5730D67165D4A51DC50533458F045E1266CE5C1CA6A30D931DA81732A876987482F2DB58694C574731E92CE6F9083A5EAD8143F244A8DF04C6DE1B2B07ED86D5593CAFC2A7B3E819C03C70B7B32AC0D576AC2E2E5843A39E4D36EFACBCE679307A1998F9C9DED50BF39CD29A529A82F26B5B4538F9CBBD547B9E4D5F7F31B555A8FCA1F9ABDEF3483640DE77D558735C15A588D944F9D76B06E417B1DA873F38A21321CDACE8D4BDDC49EBA4165D40820BA19A437D65B337B8C037041631D09F8ADD1400524F4A3BC33F9213AC7926548B9C43A4BC0148807D9\",\n          \"k\": \"1A9EC19662B68932E5DE4EED9C3F16A4AA8E6E4129F8EFC2E9C7F0B6E82E3327\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 99,\n          \"deferred\": false,\n          \"c\": \"B36564F2BBECFE4DD315E84612BD765E3F2E84F5D8D86FC0708F72FCAF284A0850708CE6E11D0BE154C00F930D18C0A8D8071B612556238A64B679A083B2FC1A204079EE19A4095E71E0EED695B3CA764F4F4E5D7366430A8933F0356DB074C2D68048E046481E5481E4F5A2F365EA9C4C7A6BEA51CDBF1BF31366F863327126DDD101F8220034FB4A3C68232C5CC84229EB1E35F19AC2016A8E4805A87797F940B72A472F129FF5B751964AEEC96847B0BCA5D7F391CA9053380DE83CBC31F341599FEFE36A1CD83B30A1B7CB588874CCC5F443F73ADFA2CE7E7271A5726272A7E5FC721E85D9755D672F5B2A0EAC8065D2C3835B7F0B2F7C77A27AAC438E345BAA378A572AA676632434737FA59A7E197135BD6AF2619A828AAC865D7F34AFB771BB55B5B7E93B9489AE98C694EAA26C6A86F41D0C53522DA4D90F2AB267675BABFBE963C4C68534A24D1EAEA2BE97702E28CABE5FD080DA6B3C432EB0E55F9FE8C1C0422A44F57002A1F96E6D53E8AB9539E909346D150082DF69F54D27017B9A7633B7BD9F7E6274B1F97D7CB4BF5FC2E34E77ECA1317E7854304C75C388CCD1386C694E93CADC856E136C2C0EE7E113A125C79443C5D1A80A9698BF58248B0903A45961603D1EA0E89E3C0650EA3E82368A6C477CCD1B0180542401BB1DE70E25F64A5DE41D62D0467353EE488E1F692EB60778452B53088473B084D0819B725268AAE752FC8CB56384C7AF9D319CAAEC958FC3EAEF57E0F35F1BFE1BABAA2C64A2D9813EE16F22A94C1C00B29EE82F11C47224A9C5424E647B9883918C9CF2CAF51B7FA825121C5D13ECEB5F66E4EA11526E0C37DBCD464C5BA78A36A31A62B2DECC7DF51C24843EC2325C74A771A7D73D35BF2AC4578932A6C2A7323375A2B7679188CFE804E5EFF4A04B7E14F8851770048F076B32BA4F19F4530364C0529EC3FB2D0DDABDC85DE2257F4DF05686AB498FDBEAE3A1439627DD8885E4C8744156C2B155BD2F965AF0F2017F163A6016C274E8532CA43C784B7AD4747A58253EDFB739D68E376D7ED246E5474454F463F4212090DF4F4D7F88C097B18180B05F2E89EEBB834B9BB6DD9E5F6036ECDD5908CA4962609C208A557A36B7FBC72158A6D86322F4303434F6AFFB34527E47E0599DDC88EAD31814646A81188E79E1B6D562E01FE1EF148FE8825758CFA5BD7B738E3BECDDDCA4C59093CA24581E531667DBA2C295B565951445E410FBC99D795887BD48AB87D6D413B64957993CD7525A0A0A5D393CA1EDF7788E4DFACDFA7B394B6163BB948C9C6779BDDCC8F26BC073BEAD0FC87236704A0DC0D89DEB4F8174E91D249C4DCD9260BC7C86CFB35B985813E1689D83083949927303741550CB782E256E79800F41B5C7D981D68E60978E5190A2C51C812DCC3952AA34212625834B2F8CF8CE8019AD6CE8F00FF910CCCF0CAF5A3596AF8DF947EFDE954F361665458F77787E528937BC52C59950746C783D8C5216570E6F0A944E6BD661F23C7A9AF3C602DF851EA2E5627186A6CCBCC470E07B290E4F754D5A8D6BAD8C34F39B4BA838CB467681B0173C33FA51ABE122BAE3DC06660950CFA5C228CDBA2F5EEF2613D2850DF9B5FEBE7333BE93F90E4DEE219AD18425DEE4006FA3009666C83DF7EDFB2EA4F99902C694248F9D51C7B6FBE53780EB218732C11368C33449D051489FDB01B1A1064FB06DED747ADE38F7A12DCDAA92D64DB4C2C43DFE53068A77339E1479C8C93192793B1C752FA7FB23B57DB5B428622D27CBF608CD7406FDB543FF3BD26FD7ED7269427C6B93491BE6724D071F58AF434FDAD2F0FAD5730A60F3EEF94C59CBC5884F36274C4CD984303EEAAD17E1785914DC804BBAF35406995E3D56094F0FDD71C7650A6C37393C0EF4C167CD2FBC28EB4EDD34B5383CA3D1B89D7BADB0270065B5AE2D461E6DEE53291230ED3CC3B616A7E8A86A4265A98C10A44066301470BBCDB257F35489BA5DCA320A390AF23CEF6ABA8B291538D9C4E965969087E394EDA44C060E28220BF72AB98F1C055159892DFF079D283C52997DCFDC2FD8291FFDF322809BE3CDC113DE9D495EA5F9FA5DDE5052192CA6F26BD510433B197131A7E954AEC5E58F0A341D7E4602BAE46BB1987B5C1D845E6AE5569DC2AFE0C7984DDD9B0B184CD6ABC0AADF5E13E0F110E8876D572200DD837FEF193278119B861C196C7522\",\n          \"k\": \"F098B5187D66F9687666207379D9A52532C38C0396F917827BE99222D0BE8762\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 100,\n          \"deferred\": false,\n          \"c\": \"4B30E5256A941008BAD9BD14060445AD208769EDEA1C5B6E4ED506FB334A2378520B5EDC9217D626E1377839A18F2D21C0CC8902622E4AB79E83DEC449FFFD45A4CBF3AC253142D935DD310B5E4C5D591A9BD61795F8ABF00AA04EBAF96195B6CC7D7C3910FD7D75E25A9D0D79FA453178B06FC6B1E99F189CDA90276D6B69FBEA28D68CC82707A46CBEAB819239BE69BA76D749E27CAC9E5FFE88064B9972DB77C49679D6DDC6E6B03DAA0DDF0106B1A61141DF827E96AC542DC90A69CB316EB4F78C611C0155F9138F527006121DA16DB46531ADEC2FF599378A819CFBE3B079C9FE7E368B91A9E40F97A3E79A4F1F05574CE2AC3A525C206D9E55CE16D42D2F0F4863F896E808FE168B34A102BB81BD607BD02CCFFBA5C189497502A55F3E601F8F61B40A5202BAF9AC87D058E67B9E1CDEA0E4B02FF2DEED7477609A9AE2116512C42079D87AD74B05622E02979EF0A0F1D6375D93576EB6553FB1AC70ABDACBFBDB18735E949EC6D1667E978547A5CEAF2F4DCA6FF5D8346A960CE6925BF2B3F316238D6BC8ACBE67BC1AACD5A9A5D130A3D3B39C3BD7C1B06227A59BF4723AE9656D9922D9228A3404D4856E39702DFDC01C6E8CB6000E0779364BAD4F021BCFD7288CE7049D544E8423B2890C3083FDDB9BC720AC4C6A1A4EEA6BA1927B307E6CB72131B6B831AAD036A50A54608D106EDACD83EBDF104AA80C917314D295E903FDF36CD04EB786CF93AFF1279C2172002F7EE92DFAB3A99BF42C2BE7B7D0EDDD38029AB5AE18F5CFF8A2F1D2EA2EC7F34770FBA8A8BEEB0E1FF6F1C1A036F1BD84030004696BF4FB4161F252436C0401AEC911CBF1D7530D9D801B1B9B3A682329AE2F6930191E48189CD40706256B864D6F016597B4AA86FEE4F0E2362D8BCC743E98531EB2B335DE2DD299F231FAA808F6BC7D8F13DE8EAA30C5698D64E508D3534935B9941C2E40A458BEA82DAE4151ECF6DCD40320E1009BD9FBEE248F4EB6DB4437482BDFD83FDAF8367CC1845E64A23A310F904D5FAAD67241AA7748764C26EC881788D1EE0A39944071E5ACB656AB8CEA285C282545030EBBE6FB595E296E1EA37D7AE529B96CAECED11331D80C92D3DACDD7DC93237D815A9C6CEB9209C0BF3548ED1AD691929B2C1035E80A21477747E313049DEAD43A40B0960A96BF3C3E9BADEBC3B4D424FE7DC4DE5CE7788E31AEA3EC8965740D424CEB66D4A5678260051BFEFF09A3CB24C1AB7782AFBFEDE5EE1ED4EB14AD2A13142E8201CD1B52CE064F05ACFB019E21A73D84A80E30FAA48ABEFECA970BBF17FFA6F3A90AEF80EFA31C494E721231289143416AB9621737FC016380E6079EC6CD962BF7CC0750582EB218F869CE117D399DEF9AA66F7D2F07FD22BEB9E50B94CA5FC758C9DD4D2984A156748C52307731FC78F8539F8264BAD6DD56C0C23937A9A850E66BA298C3D39105ECACA9A573D887C9A4FE33D487F2126097B165594E1F8106C937758AB6EE75EDF39D2BCDE78AB611A034A72FDBEE67A80F3315571AB4DB94C56A19EFB63B8E7708566412F73D4974B160183FB5B6C44C8CED990B29C57BBEEAC5EABDCB11CCED9A17322B6EF197121B4094D7EA4A1B4EC44A68B447FE4C8119A6A33BFB66EA6844DB5B6094119AD1DE89449DE922B9A0D1253EA18C62418EB87330C6B33EEE02D4486F62A4D31CA24F098BE2F187CA6019025AD6E1C2FE69800D8BFA2C646F9FC6BCB3D369A78310084FF163D2065631C41748E7E3B25E8F2C9EDA2E107AA2046FE3F5DCC0A9A39FCE41813C8F1946C3AC07A22A6A56C4AFC626E68FF8CBC4982C1E60C3A9F288D1C4F2B8D7187EF2FAE30B77C4DD73499C2B3793B24014CFFEF6D80063DD1C1F3AC7F14FB61E5E81F850AB865BA873404BEB898FF7A2DCFA3B955DDB161B5781AFE8EF127BA2C8BFDBC2FB1C7D80FC650420214314023F6F65C17FC48927BBAE88D48D2E1976119C2F8310232942DD4C3AD4518D1E4DA9DD588691837122F5E5DE0FF1FA685DE134DFD1348CE3B5BE60B18BBF474074829E7D81AE087F149259122D47B728F369D1D8455EE571F715788C254F2EF438034BFF0A11F2F008E19B370BBEEE135A00DBE7F3C2970208F5F5D0E2765C395CA81B2FD80FC384AD046564229C759315B6CFFAD03A56996556E7714DABDE28F7A9BB5DE2C05B1F3596AF66C747D9A9313673F19AD4BAC6EAA7\",\n          \"k\": \"FBC9EB4E8D611C153AA9ADCAEE5781DA5C0112B3AB75956180A5CA40BFA0F53E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 101,\n          \"deferred\": false,\n          \"c\": \"CA9564B54F15561C8238E6CFD88137EBD4D277FD5D64BAFC33D6E575947F0FF9F93E3B0A4023DDB6FE480D7D6A2B9ABCD6E6E011EF37C0699A6D60D9AB4B05BC685B0A9AF7D3BD999C7AC1CDF017E6AF1DCF0313759CBB21539D7774C31D7ED8C039AC34D0C6A5F7590A3DFE193D73FA96B3458A364DE1555284D85A2BAE7BA9E57ABA00134E6B09C09777F2F1D7125AF858D81D14C71E34E8F668468997334B72E002920FD3FAD8D588355343FA949F1CC0BC263C7F7A7FEA6AD708DB756AF983B16A593EC224F7D69208938A4526400E326CBED532A777301DDEB5E539CFCE60DB8A022AFC52204C71710C204968FD1457919EB71CA15522AD56ED6B60404D62D1DAD0D06E4A2AD6BC746B28859A77226B774BF56BF7F019F2837F51509E9EBE9EB069DA27401CD1D7BF2A74CBE8341A7F213D061619F4E5F52984FE47066D910F1146CCD8DB48210FA2518D6B9FADFF16ED9D389292C07C8A7021F32BCD538AB06A6D5ADB13D7A96F65A4062A17E26B301CC8AD420732126D7CB801DD489AFF2D717D07A2748B4B01D162D228D5F1533CD5FEE8DFF8F032DFB270B61095785E44CEBBD4EA27158362D2A27582CE78594D4D7428B6AD958A9F1604EBA76A8CC0530E1001AC97E5ACC5EE670D5DC6A78AA45300A2BD5F0802CDEE564FA640A19FB554383A4E4CCF2E5BB3A41879C9428CBDB8DE1F4D3FDEFC18C2A8BAE42C096244279E57B307614C843B341BCCF530F6B187121DD83A9A160A3579C3188A98FE2F49A85A2705B9F76DEF04D5D04676D8319F243DFC99A5F90771B34D2A45EFF92C0CA8E4B542B8ED4C2AFBC92C26F8DD20B26B15F9E719AF22F571EE5B9573D5BD1931138D6315C5104BF80AECF830548E98AB23DFA44E5A23C6CF57740926D1E146937AF8D220684919FD89082E260286AB66F66F8A1B81BEE07A85907D07FCCFB9A1002CDD47A33535C9FC0938E3CDDED04D3FABE6326CBF5643373BAE1151704220E49E177C4D0C6168647E5976670DB7F6D0C12F169955E31F553A53A76093DA2A9A0C589F9AFDCCAAD9EC5449ACC01E12A70BCEB389AC104407415782AF2EA3C73D9EB2797CE6D3C005061C5059AB625DD7D273D4D92D1F4EB411A4033492F19921F60D0317AF286866B865E33B6235F0E3528228CF9DB242124F0A6375D50CAB3851DD2A3A022C1E636E332C90D97FBBFC2CF0B971AB1A89014BC2D942FDF015555431ACB3E7A6F258B816BA84892A1DFE3780A0E0C2E6C06149218E70D60D62573BB51856716C0DDA63A983C4008982E842E655E5767DD203DB3490E1E6BDBEC16350296D879F017BA695FC1CB3BBE516B741A67CA6CE09314AE27F718DF68DE698198289B457884FFF1E439F30D9117D19ED7E466084BD5A73E26B5E1567B148D4C7ACD1368B1CE2709B3AF233679E61914202D0DCFC81EF3ACC250DCEA602103C7E529FD6F31A186927E790E3DEE09DCE87DF694ADA7A3B7BB3BEC64456EE983E25DC6CA1CBCD752D72ABE6FDA2FC81A46F81E83AA9738D528C6FA3E69C453346D0C9A0734DF36BB7650D1D2AFA8A5C4C5A936D41258BD4193DA74FFB180CFF582A32D6F6ACB93836E009E8C880592BE61532215F1F6FA50E8FFBB82208A94D8510F70DC6633DC04F9D94C6AC46EDDD4EB36873E064CBBE65D343957CA7B75024EEBB56F589C3DD2253D68D12DC892ABB1FA4BF033B9B732E89A8B89541F04C6462F62F13B09C6705B31036294F1AC38EDC0C2298D7C6F4374C3B5C368D10DA8D371383CBFB4491126A83D1F75DF44F29BCD39349A9BF6526D14B339FCB440647A5FAB63A370089DF162DDBCEAC8966648DDAD6669E1EADC1C8A33E9B7378693E229C6B715F2F0AE54A67455E79FF8970F23E655E7A540D28958E2E102DC99B5CE5772D00831671CC6F7024BBAE8B04173E439054C96AB3BE918C40C5A8D42A9122CA29C56044D340420D2EAAEB738EEF70331D488169FE91B521835297D7326CA272B2614144EAA0B7A75CC7F3849138255B8A1D7DB875BC7C25D28EE5941DE89BF7B063046CA0CFF31A99D7B1846E01B519137B67647F024B3F6DC70045B6950EC6ADBD68F43F67464858D515D6E3EC5F99D9F1C849831BEB4224FEDB01236712E1D715F6A752D0682169A0E83F064FD6F081F338837FE654BCE7C8CAAA8CA90C8505945D9BB3EA58661102FF0ED3F0DE30C4013122D8CF08E0\",\n          \"k\": \"D970209BBE4676405E1CF15D053A04F93D800AF1B32EAEB1E4B644ED09ADE8E8\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 102,\n          \"deferred\": false,\n          \"c\": \"C18D7D95DB69D4FE1E6DB385024D83C01F4790E2BFD25DB3F5DD5A208ABE06551BE0936A84091F081308471E82AC9ED6BAA90824D5525701AE0B638003C21D5EBBCBC17FCA8F522BE4F9FE5ECF38BB66131578163D50994E532D0776B187498C5A85AFF617D4550345F3855F968A8964B4F3CACCFFEC82C92BC8617C78C98E10C91AB505A92CEFE0AD6C8B66406AEA4C3FBC5275047B3E983AD42BCD31838A39B92E1C61E9E62443DF3A45044819D9E289B5514D4F74E08FC3914C0B66D2352CF8B1FABB4AC9B0748A43547BECD29D447D01083803D34E8B7EC89B4F0D78B88AEB33E308989BED2D7A78E1C06A11F3BE808F0B9D9712C80D63DA10475849DDF6CB0DBB1007FCDEDCA3C4220386B78D9EC6E380B4F57731D964470B42CF7D4B4E6D98A12A9021121C8BACC4B132A274941DA57D824FCB60B83565F5CE05D140653DD4C70F385B55D485D24935F3631AC63FA12FF50BCF4E431EE4074B2A05A2A97354C7B4169B13EB225F1727F8424F7CA6317A04C355FE785248E67E053C4D4BDBBC47DC7760AC71DFA2502FBAD1180F2B095425198A26BD5A0F7DFAB70524B8F076E7C7F215B0536B0023F8A9F7784809FF4DA245C2EAC5F9E0AB85D987C5B6FEBDB3DF197347BDFB8D5F1547FD2A59D4B434FA7ECF8D8535903D3892868BA0632F194AD6E4D5A3B30E5A6B92F829642DD4A3031358F9F0D9D46530602A35CE455F0E360C14A754828972D85561AA835D87275AC510856D26192EA319FEB45709346929DA5C5919510CB2A482CBB0F1CB4BF6FCC0343F6DBDFDE734919EF335356ABE82F80786EF0CA22E5B03A05963E7051E1FA7EA4BC3141B5746D264BE1A32CCCD39DFF8F9E5E2AB4C5F51CEFEDE3C1CD118351F9A8ED30649D407FD31F6C4BDA3AF44888ADFC3D118BBD04412FF810A7E106EC32F7524E4750DC5F35A9C55541421B5E412E57BAF24622627F02633F524FF854F71011580598C5CE01190258310BB12D7DB5F95E9EBE5F72C97E89287C2F9007A9332EC51DEF1AA2F2CADA9A8A547C3508B4D294364EAFC858B98C60C5469CC7C3CE3ED659B5A54E889FEFAF825FC777AA74A8896C9447704CA7300FC5DF5810681E3ABE083C1285B3B97EFB0E21F78F45409D00B2E1680DA79439734190AFF0D68E062970F8F6B0F1E84A559B09ACD9938913FC26484DF2125FB6D7FC2E3F0DCBD72D9E5DEDBE7E44CE7D895CC9CF6945BDE0C52F92340F9FAD3009BC90D4C2D3DEF7C1F10A862F9D71681537FE4E2716912DAAB8C9DCBD81A083220F68B05F7502F3911B1B6E3B26DA14EF646DCE67852FAB6145BBE7E21725C21CBB2849C63D01AEAD932F8EE9345D8666786AF06AD0C89B08495A6EA95992301E2D8B6A14426971C7B31626BC93BBE76CF3DB9487B5BFC5BAF298F1A92FC3BE276983E53701F9A550E2961E6E2F07317381364719BF3FC741E2A5A0664D8873120D0C11287E92DB12126332D43F35407C01F7F85DF7916B651EE4A30D602E71227733EC9252EC8346361DEFC23397CEAD0C23AF44C77A4C97242C7FA9065BF0C81983AF3E516C1B8FFF3DD5A6C43B6ED5AD8BB3327A09B6B459168F3E497DCB65FE7593E8AB429B8EB2B31F76DF08A6A8F35EC4CA994037493A8C04A73D8191D682542FCBE16E657D3E477A7D25A1D650450E94FAF485CB76FE7110BAA902D74C335FEE1546D076163B5540D8495E16E909E1D28C15BFB421756B921778A784E16207BAD407B64B9CD83AFB0A602374DE06F5C836F4A1ADFD495012DA8D3FA4B829F735B31BAA6364A2AC11BD18E40628DCF82238D86B0B5EE9DF6D179103E1D12F5191475FE3008A5382CC24648CBB24F2298758823B7F93DF10B380C3179F07DC3277021E9EEA2BE5CED646260165B57A18C26E259F83576938828D4C7617623006682CAA613AAB770791874B55E2D0BB32DDB628919B42C09BB7DAE1FBEE8661CC13F8B6A47CF5D6085A2AED796E305738B508599673DCD03AFC267023814FB1DF7EB928D5762BBAB4515921D81C6CAF551DC6EA16C1D31125B99299ADE63FBFB9BC1CE46331394CE472DE6DCEFB2BF9B3828B0110246419C47D2A1FEF16097B943A310C0664A92A155C8273402E83CB94D7E733E4527E7525E9BAB219B69676804C1F67088184038668D55AC4F6E04CEC0EED4B05DE649A9C2064F241AAF9732B09B0EE4E5BB2C0386E45FBD44\",\n          \"k\": \"B93CAB6CB4C636B56EDF0DDA556D2AF2622AE197B5AB78F95249204A6E2E824A\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 103,\n          \"deferred\": false,\n          \"c\": \"E7B361D043C4A0B3A780121E9648DCF38DFC10ED5E47EE4DB5523C1EE1F53552640C89D9D7EFB9DFAA8EBAC7AF137A850AE41A0FF8F8CE31FDFD3E671555180EE46AB58322FE4E5F525146F9D4CDD1D1CA01413C5AC7259E1A2604A5951755F47D1761ED16B26D3DCFF79A263BC38852007BD3F381FC3B79B46A4B372918AF3117180726117BDB33C063BF5EE5D69CB48D267929CEDC890B743DEA43205EAD46FFA69D9B30C1AA5F146CBA3B0B7C4D4A50FA8D120777F1661430DA1B9D1C1DB4E10C5C3C2436D381EB13DDCC61EEB3F9AB46B60D2FB714929139E9C27F8730684EE17B077BDC003500027FAC94DA80870B382EFE41BAAD902A491C29DC95A8F80BB075A4E61C8F100FA738BED869B48F897ABC9CCE08917B5073229A94F3790947FC1AB6C2DEB5B012D60EC156A8A041DE151EED8884EC616089CB08EAEE37C006F5BBD10ACE9596A34FB09345A14FC4EA674E4A74699BA5240FF1282ED1E64360BDC9F7336051A33DD48809CF0011BEB3CE8C681588AB29F5F26175A8A6FD7884CB96CE3964FF10A67FC4A4E14CA516162C16D8127CBC45BCF7C88C89C8602032298B53C19FD099809814BE0504BCFD2407F48F2F24C5ACB89DB4F54A018DD586D871C58CC0D998FB4B0E5F5DAC631BE8367DDE88ED711F069FC8A80EBB573A7DA12AD8F13A4CA1E8A22D9EB53C55B80F700C58E6EADE6CDEB35C262EB42C903AD854F843547B79464524833A05E3FDC46092E41D8E339E7662D1209D338B8A02994D15A10439C1DC02A5B0EDEA58AF197866F43269A57C747DF389EC597F523211770E9C7E4CEFC5E43EEF791897EE43CA6146F3F757B66B9E7592E728565325D1740A1736CD0E678BBAA043F4C355FDE27094F74FAE27AD8C270930DF637C652BC1957F958DB013C146F2A4C5F451AB58A55C2B638A82755C11991B049E82F8D3CD3E7D3571FD5A83B60280E92031B610FADAC9E5F61DA469DC4C51381C970E03F09CA560E5D69D9B32AD6C1DDB6FBE9F8FC0551A909187AD65AFDC067EC6AA01AD684AE4C4F2E1F64046083D3EB347A6B6B23BBBF14668B9650D9364A6A7666593DB86FEB59628A91169F8AE24F67680789D316338B2F27766A83957831D98C88C837215AE3BD49767ADCADE758320ACE76D7F39E2970EDD19657F0EC12583164C325F0A000D065036BA2522F960C87F9852F30BC6BB5419CD8C0A1F9757BD358E748CB244A5E677AB9F9319A43A9BAC847A566052CA42C1C1DB36A0D97F144BED3BC5F11A5C8BEF7A74A4CC67748F1DF53F8B4714E0A04256B36A814B08B78A9737757E3F1347F9E5535DF1AC98B08ACC1409278B925F3B6C7863BC0969520FC6183E216B4E8E449B0FB999F1C65567AE2064774454EFDA67E1749FB24A91B55DFFE7DB75C4E24E8EF2389214EC3E95972CD53234CDAF8958D651A7A95802E65499A8A7811A65ABBA90129D5EC4247D8183316EF818E79BE839BB3379E4B8F4EE9438BFE310105C91F8703AE94D8F9D53096E2341E74E0237DB2665F16954B9713DD9638B05970A9A96261586B04F9FF369028DCC43D35B51F95E69B0323A1CEACC4A5E2EF640CDAF3407BF5F5E14C9042FF299786BC55965EB7C8BB161487FCD7911BDC2FBB65100F2200E16C690F801EA6615F9130EB99DF816B188B06E9A3105B78212B76609DF190FF102CCC451746CBAD16464E8E2F647B75777F664DE86F5089C37E3A54A6AA8B456CB98B42DEB5529C06DA45C2D2A13060BE56A061CE210ECD307FF5AD5BE39CDD8D27B4A3403323D4F53BE35FB4E31670F73CCE74CF73CFBB29A5FC2EECB5F852CA911942066D826404B77251BE5BC5980D0A6E0DB4D753D86490D4250536DACF05D82064A28324B49AD4AD202CD0FC939BB7A3CD9FB1E3E196348EF336DCDBE4BC831DF5847070D0B2BE1C4910FE1C69F58C6A7A2E7FEBD51BE1D0E050D5D721D7537A0325E7ED30AECA75A2A81BBD86EBF91CAFE4483D2729271ABDBB65C2CD9973627D2820DC7ADE3E26CA2F466EB117B2BA98EC868DC728ABC6907D49E2495504133FAA7F8758FD23076D1A65A91C75512F89EE4E2F3E480D6ECB0EE90793F4F93FFB75DF58A7072C91D5A1D9EF0C3B1DDAE79EF576E6A276F78CDC24664897F07B3A20691601EAD2F499C50589BDBCEC74FCADED1A8AFDFC061C2712ED599D48A3ACB3D86515D664D0CF3FA349A1910CB\",\n          \"k\": \"2E85AE4441DB0930391278E9D6920D9AC77D6C752DB2628CBFE9D76228DDC954\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 104,\n          \"deferred\": false,\n          \"c\": \"EDD3FAB8AEB1240FB31B836CD1603E1F904BD1F87318DCB02A7DE18B4044385CDB51E343787E583CB043EE23899658420F9DEDB23CAB2BCD1013F573C0C7978521596631F6590105CB7B281AB1591B7056BE068DF838E0B1679F3B88D95208EF4B3019625EEA7704CE79F33AF339AB883B0C48B3C4413921F43AF2515A85023B5D98D06E619238C8D033FB7DD19611CC60CF395A03B0681913B299531B13728B278D353FA093C633710B65000772DD6D7CA59C85DB62196DCCE1B75559ECD3FB42DDD8E57EB4CF3B4E35B57EA3D6063221B81F1B802DD7D76DE308CB0A738C3B5833E9F4427AF3C3D79B521E7E665B052B9A365DCDFE5A688B06EDCDFA2143C938F852E32D6B49808CFFD01A8655B767034F8C638E8AF94BF3EB9EA39AED1D2D22E181888DD608BF9392FD73822303A41996F41D51A924FA6EF76A9C82709A21BEF1DD004693EC9468B335F9BD1FD94D5E6D89D570FC6D23B7F5CAD2975F418B8C4A0EB82EA2A3C979B1C15B0FB0A23F844764DDD49A8B89C0D4BC8C311BB43725EE9BAACC4796F58C0F1180A4F6AFEF45178659B35A74FF34A8E93A64FA4CD003269EC67C5BD528E015B2311B5E2D33472638CD65FC7D5127335EDA862BDEA05F4290EF9B370BE69DC89E7D71DD2522E669700D5D02D8DCD75FE2AD9EDC307225D61C7805CE1EBBC806A08BD360F86FD27B599582B22C57DDE77B08F7537482FB5D75BDB9F3F4EA07DFB0711C25AD1950058EAAA2E17D8F676BE6B72B1687383D8E0DD60A8277F6FDA202D6F8957EE21308AE81ABF72A89924DB44238B262D2FCE733E12E5413C31FDEF94860D5BB0FB0AEDCB1EFA8F87CCC76189FA5D8157FB4FB14652DC188157B2B746B596FE6F2FEB197DE139B80922C2EC14B58E743E3335893ABF85B99BC4566FA1BEED449658C5993CD08BF78F7DBFF808F611D6EB8F0BD7977E854BC195D711C03EA532403547B6ABEAE827481BB5D53C867710215835260097B6CA730FC722A74E230434B08F38EB1ABAF555CD4CC6CB9310E32F93E0ACAC1E915A4B57E0774B013BE7DD435B5C6AF6018944349841F84E8C28260149F266C99FC05E0DB8D5A63DE362CDE45F5BDFB6F30D55A84ACA22E8640A1287DA51714F2C8D4B184A5F671E0E907134E34D875C9A4709ECE3B7FE15713A0DD972505298C18A9D35956149EC9AF45C475016D7C8B5CBFFE2108882B86FFD380A79892BA1909489E016CF9933705E2FA72ACC8569501553401C397648DC47948935C0197D6F162464DE42BA537611CCB67A988030ABF6081946FDD1ED8C6B23691EA160E8543735894839F13C270B3E1F68607A7EECE09AF06A34F4FC9096D4EFFE4905BE56F3EB397C13472F6621F3EE45A59C8001ABF9302E036BEDEB2E0EC92A03FFE0DF52262621728C6791C4E7D8C226D17A04B6E7EB2FF8586585D639B449C85224EEC67E30537ABE85C8F7E2306DB80E968D8585CED3A9E21622BBC38D43A7D79E2457C67307CC208064D3568C476ED79359CD0A4B0BECA02FA702661056E187A51C2D154638F9000DF856ABB82CFE12C47543E46FFDBADD2DC69EBB3FF444E7E1E95235541015E6CC7A0429C82EBD6942C6420AD598C08080DCEC800509C142ED5A642951F491E748B5436148B90ACCBEC35D0D85FAF4E472ED3F1A089113808D3ECDF77EEF3E089FA5A1635B90EF99034AAA49D4D13058EAA5A8797E37C59CEB86C7CCFE1F574E1086DB9BE744BDE5067AD5D6C450FADFF2338DB110736FBBD86B41B29C29D3899CE60E9BEDF775416541350AAA9BD9B5D57573C542375BB0297912863C86AAE39D153CCB29F1811CDE58978951CE8EEFA6F9D10121AD1FB89F02AF8E96AC08DD10E3274E8CA79D910667166797468D3D3BD6D7EF5F2C6FC4F110268A2716CB273F29BBC347050BD98BDD88F30A96E7A9E840A55087F42A09B03D04E612640BA4D86BE87DA6D20ED0ECCCA2523EE7C4E9D2A96E7378BF71308850832769417EB6250FC768B0EDF92FD45216A235435A3E32AE5B22BF913027D81B0D5508D2AF88120A50206DD7837B79C45B21DB4FE59D23F4AB051BF012B13F6EE5B34C83C8D8CC9BB35266D0EF3834F52CE6CC5BCB7C5989198465A9E9DEE1A1F262FAB26FE0D0964E624869DF2607858815418F85A1F503BE5217794CED29D02E19D40C4BC8E65C46FE3815C1E548976649D4332B1841EA03022\",\n          \"k\": \"5CDD11E1565AF6FBC0DC373651C6F2DC833EBBC54FC0FE2855C0C19EFDD6D877\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 105,\n          \"deferred\": false,\n          \"c\": \"C72FA15560FEE6B014E73F5F93C307F74EF9C49AA8F7DF578C002AF20419040D6AB6AC46F78FB03F56A9C5C95902D8CBCE34D79853EDF0C319AF5469E32D0B9FC3C41628970E0B3A6C408B509C74DFA218BD23FA7A11DEA2D2277B3522BEF6606E3415D0DD51556440CC1AF59CAE6F23368BCAC3E1509503368354D1E3EC9E91F8B2D377DCC323D578DEB222585E43F97A6D1855B576297F3EC39F5F9EA1B2F72A0E701DB35D633DBCC5FFF76A2D39AE9DF2A3F6326B7671A4C0BB7177897DFF4FAF9FE5CFBCC94966BD298EA2627CF19C1CA866E5927C6E41970F544479D9A6D814AB72E2963F959CBEF37BF905BE98D8C8F3C25FAD3983F71D0C0D27D9FF17E4B34C2F8664406151E92ECA980F6CBE8F8926638398C9BCE9C69A92A30CE82F28CB4FE4110EAC40437BD64D38412030FB8DB3A4242672807737E707E59A0ACFA782127EFCB7BCEC39DFEC55C3109F958E86E0D381C4E9E9FE43110517778C08A140CF440F209011768EE34E5742ECC1E4CED045922D698A29E5557A29C237885D8559F110E4B540FE1298B97920EDF59BC8EBCA11EB91F471B6647864B384AE5A6BB494942BB1F537301B39EDD6F664E4A7877C173614B09D981401D5AA98A8BA4BF1992DD7B7A65BCE7E87FCDFC7B29AB69ADDC9036D71BB9BC08F4E7D9A57B784911CEE7D0EE5A559332981B6475290FB4410D8BA1F00FFC4850031708EB6A83AF524447F491CC25F23FBED71476FBA5C64BCD50D88A3ACD2BE1DF461B11F6D537B2929D073FCFB9E2545E1B097A12F52C411B2AF6C20A27ECD1C084568F4A76A87A4A79F7711012CBEDA777D913CC6B15E6C4E9BCE2C773991946CB9CEFB7F105B15FD2CD3E721E6C1DF69B66BEDF2157ACAD45458FD8C9C1AF910394A13C300696BBBB5B1E1145076BC6B9E3D30A680EA29B6370618B47AF77108EDE6BFCEBFBCFEDDD27FD9F0DA6D289060095C4E309DC3D26DCBFB9E8AF34E12BDD335FAC434663D4D802C8B04AC884352D27739C4DF22F3D7DB38084BAE2C0A15485DF4E356DF2FFBB5BBACA78D0B4886909C4482A6366991776B788C0941437BF858DD83AAA50104D725171C09B7DB521AA65CCCA3CDAFB2E61CDEF66B55D80E201DF44654E7B1FFCA29EFC1E44A8CBA406C8DAC6207C0BD5DA964FBE137ACCD84405A94F5F51D82CE701DD16774BA5F0A7A2BED7F9BB9A4F25C3095D1F8980721A7ECBCE957825A9BE9F4F818E56D35909A3F9DE5487DA0011EBCF9F4D768B72D236042175ED599D731AAFCD45D3D837FB8B64304ED7F22A8C3949BFA25B83A8C05FE9748F63A38201B460E16FFE4329C8464C9BF07D45DF2BA9AE7A84DCFC4CAB7BE42CBD360F61051CD56F68A71FE9E78231986832C9564D02B973EA2D3FCDBAEC374612C1B74DD483F08BAC30F6C9306E7092CC8FE1D20B937AFA4BC605ED4398A8B81A470870E97EA7D51562111D04BF9D09D9BC07533FCDA1E8DA2F2823AD621DB169C99FB112E44FDEFD597B61160815A1776139B685DA9DF6B4C22F6FF6CA3CC46B3264E456E98FF1F301122C88D42928403ED0E0E5F49BB0B450429980ACEFA1A80DA26638B5D2310FCADB0836223CB0894E6FA014D351AE052A70AB5F515641F153509FFB90B8DE495B946AB8C7D7CFEF56D3C66DC871F1D3A38494EF6AB82066E96B9F2782D6B5931B78B7117C389D155759CBC1690897DA66E50D0865209887552C8A6035B8F6911760F8D0A450FB926096721D962877FBFD87D92C37C71836B8BB9FCE92B4637785DC8E8C1D379081C14C73872E676A1C854F1BB68649BD552B48D12F62B17E9A48CCAF63885899C7B781DC3A6D7DE7DA28E286C9FD644D3521F0320B7ECA8FD0AAFFFFF90DAEC85BA80868A2EC69CC73AE00AE29FF5BA37D94510CA19E1EDAA64F30CD79A58B42FC9A6402CE31AF54BAE84DFED8D0C76142A347542265B794A0AEF4A08B4B5DFCADBD56757ECD98F175D80B44121257964293F300FF750107C1B72463D4634EBEDF4705F76C908844763D0D6813FFBE5411FBBFE16C08F32BD1BB3FB8EA5C5339A1B0194DA543E64C1F8065CE526D2754EF95A287DDC97B790FF34EA37863BB166BF0BD99E3A961BC91C1A4F84B63700C9EF5D8D31CEC9E1AE33C554BE638D5C1217CD2DBA13CC143F969DCBF285407A9B608F859812E7F668D4538BE179D11ED767A6971A2AA9CBB545EA01998E\",\n          \"k\": \"C751783FCA654B1FB5F210C6CAAAB9D5E46A969E546A0834D618A952DCCCF3E3\",\n          \"reason\": \"modify ciphertext\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/peergos/server/tests/fips203/encaps/mlkem/MLKEMEncapsulatorTests.java",
    "content": "package peergos.server.tests.fips203.encaps.mlkem;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.Before;\nimport org.junit.Test;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.Encapsulation;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.encaps.mlkem.MLKEMEncapsulator;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.mlkem.MLKEMEncapsulationKey;\nimport peergos.server.tests.fips203.harness.TestCase;\nimport peergos.server.tests.fips203.harness.TestGroup;\nimport peergos.server.tests.fips203.harness.TestPrompt;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.HexFormat;\nimport java.util.Objects;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\n\npublic class MLKEMEncapsulatorTests {\n\n    private TestPrompt prompt;\n\n    private TestPrompt loadTestPrompt(String fromResource) throws IOException {\n\n        // Create an ObjectMapper instance\n        var objectMapper = new ObjectMapper();\n\n        // Load the JSON test prompts\n        try (InputStream inputStream = MLKEMEncapsulatorTests.class.getResourceAsStream(fromResource)) {\n            if (inputStream == null) {\n                throw new IOException(\"Could not find \" + fromResource);\n            }\n\n            // Deserialize JSON into POJO\n            return objectMapper.readValue(inputStream, TestPrompt.class);\n        }\n    }\n\n    @Before\n    public void setUpTest() throws IOException {\n\n        prompt = loadTestPrompt(\"internalProjection.json\");\n\n    }\n\n    private void execTestCase(ParameterSet params, TestCase testCase) {\n\n        // Create keygen under test\n        MLKEMEncapsulator mlKemEncapsulator = MLKEMEncapsulator.create(params);\n\n        // Print header\n        System.out.printf(\"%n[Test Case %d] using %s Parameter Set:%n\", testCase.getTcId(), params.getName());\n\n        // Inputs\n        byte[] inputEK = HexFormat.of().parseHex((String) testCase.getValues().get(\"ek\"));\n        byte[] inputM = HexFormat.of().parseHex((String) testCase.getValues().get(\"m\"));\n\n        // Outputs\n        byte[] expectedC = HexFormat.of().parseHex((String) testCase.getValues().get(\"c\"));\n        byte[] expectedK = HexFormat.of().parseHex((String) testCase.getValues().get(\"k\"));\n\n        // Generate the encapsulation\n        Encapsulation encapsulation = mlKemEncapsulator.encapsulate(MLKEMEncapsulationKey.create(inputEK), inputM);\n\n        assertNotNull(encapsulation);\n        assertNotNull(encapsulation.getCipherText());\n        assertNotNull(encapsulation.getSharedSecretKey());\n\n        // Extract the shared secret\n        byte[] sharedSecret = encapsulation.getSharedSecretKey().getBytes();\n\n        // Verify it is the expected length\n        assertEquals(params.getSharedSecretKeyLength(), sharedSecret.length);\n\n        // Extract the cipherText\n        byte[] cipherText = encapsulation.getCipherText().getBytes();\n\n        // Verify it is the expected length\n        assertEquals(params.getCiphertextLength(), cipherText.length);\n\n        // Iterate through each byte and validate they are the same\n        System.out.printf(\" -- Shared Secret Key%n\");\n        System.out.printf(\"   --> Expect: %s%n\", HexFormat.of().formatHex(expectedK));\n        System.out.printf(\"   --> Actual: %s%n\", HexFormat.of().formatHex(sharedSecret));\n        for (int i = 0; i < params.getSharedSecretKeyLength(); i++) {\n            assertEquals(expectedK[i], sharedSecret[i]);\n        }\n\n        // Iterate through each byte and validate they are the same\n        System.out.printf(\" -- CipherText%n\");\n        System.out.printf(\"   --> Expect: %s%n\", HexFormat.of().formatHex(expectedC));\n        System.out.printf(\"   --> Actual: %s%n\", HexFormat.of().formatHex(cipherText));\n        for (int i = 0; i < params.getCiphertextLength(); i++) {\n            assertEquals(expectedC[i], cipherText[i]);\n        }\n    }\n\n    @Test\n    public void mlKem512EncapsTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_512;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())\n                && Objects.equals(testGroup.getFunction(), \"encapsulation\")\n            ) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase : testGroup.getTests()) {\n                    execTestCase(params, testCase);\n                }\n            }\n        }\n\n    }\n\n    @Test\n    public void mlKem768EncapsTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_768;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())\n                    && Objects.equals(testGroup.getFunction(), \"encapsulation\")\n            ) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase : testGroup.getTests()) {\n                    execTestCase(params, testCase);\n                }\n            }\n        }\n\n    }\n\n    @Test\n    public void mlKem1024EncapsTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_1024;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())\n                    && Objects.equals(testGroup.getFunction(), \"encapsulation\")\n            ) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase : testGroup.getTests()) {\n                    execTestCase(params, testCase);\n                }\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/encaps/mlkem/internalProjection.json",
    "content": "{\n  \"vsId\": 42,\n  \"algorithm\": \"ML-KEM\",\n  \"mode\": \"encapDecap\",\n  \"revision\": \"FIPS203\",\n  \"isSample\": true,\n  \"testGroups\": [\n    {\n      \"tgId\": 1,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-512\",\n      \"function\": \"encapsulation\",\n      \"tests\": [\n        {\n          \"tcId\": 1,\n          \"deferred\": false,\n          \"ek\": \"DD1924935AA8E617AF18B5A065AC45727767EE897CF4F9442B2ACE30C0237B307D3E76BF8EEB78ADDC4AACD16463D8602FD5487B63C88BB66027F37D0D614D6F9C24603C42947664AC4398C6C52383469B4F9777E5EC7206210F3E5A796BF45C53268E25F39AC261AF3BFA2EE755BEB8B67AB3AC8DF6C629C1176E9E3B965E9369F9B3B92AD7C20955641D99526FE7B9FE8C850820275CD964849250090733CE124ECF316624374BD18B7C358C06E9C136EE1259A9245ABC55B964D689F5A08292D28265658EBB40CBFE488A2228275590AB9F32A34109709C1C291D4A23337274C7A5A5991C7A87B81C974AB18CE77859E4995E7C14F0371748B7712FB52C5966CD63063C4F3B81B47C45DDE83FB3A2724029B10B3230214C04FA0577FC29AC9086AE18C53B3ED44E507412FCA04B4F538A51588EC1F1029D152D9AE7735F76A077AA9484380AED9189E5912487FCC5B7C7012D9223DD967EECDAC3008A8931B648243537F548C171698C5B381D846A72E5C92D4226C5A8909884F1C4A3404C1720A5279414D7F27B2B982652B6740219C56D217780D7A5E5BA59836349F726881DEA18EF75C0772A8B922766953718CACC14CCBACB5FC412A2D0BE521817645AB2BF6A4785E92BC94CAF477A967876796C0A5190315AC0885671A4C749564C3B2C7AED9064EBA299EF214BA2F40493667C8BD032AEC5621711B41A3852C5C2BAB4A349CE4B7F085A812BBBC820B81BEFE63A05B8BCDFE9C2A70A8B1ACA9BF9816481907FF4432461111287303F0BD817C05726BFA18A2E24C7724921028032F622BD960A317D83B356B57F4A8004499CBC73C97D1EB7745972631C0561C1A3AB6EF91BD363280A10545DA693E6D58AED6845E7CC5F0D08CA7905052C77366D1972CCFCC1A27610CB543665AA798E20940128B9567A7EDB7A900407C70D359438435E13961608D552A94C5CDA7859220509B483C5C52A210E9C812BC0C2328CA00E789A56B2606B90292E3543DACAA2431841D61A22CA90C1CCF0B5B4E0A6F640536D1A26AB5B8D2151327928CE02904CF1D15E32788A95F62D3C270B6FA1508F97B9155A2726D80A1AFA3C5387A276A4D031A08ABF4F2E74F1A0BB8A0FD3CB\",\n          \"dk\": \"A5E26E1B2360203944ACFC2D7C376780E55B5A5CA38674919437C794F54B8217BB0629C84C692EF7827EED864D0C508990CA4553F16F4720CB75368C1B8CA9DBC175F51BBEBAA456F36611A2364775D248C0F4C40B342608F7370A983CF75C915570248E367375B665D9357CE4A8553E659BE4A60CA68B58724689C23B74D34C9E78E168E7CB0DF84641E41B6E6807BE6CF4CF8F338525D57090B08AAB5721216395C49147F6E817B117B129987317A7A5FF15A279F86AF93C6A4995954000C3D4D8B0A07499A95A5C98D0B8303702DFD801B67C37268904C96ABC462750384BAEA767A5AD30C5D452682B3AC864D1671DB38F1CF2CE6E6C901D39C144DA3D93B863F95717C3C585AB876D3EF2B10AFA0B8142164C3C27FB179A923A3F924B15CEBB22EC762907324F1CD4C47573CA1F103CA88844F3B86687280B3B5BB569B1C118B63565055834F39F320CB88C05C199E29684D7802CF45D8DA342CC444D91A84D6D9461C873B66F9785488723A167412019077C9A7FCF4C7BD028BE3007B3483026A442A095124C9607C950443FD69993615697E9AC1CB9D380437B85EB300CE4D9B5A5BC2132660DA3527031A1057A565F2C76775565B0088637707410F2E955355425EFE496113149CF52C901BCCC48864C8AA4262367213602B63AA1A8BED77826C0C476152AB3464A20C9CD73F17A1D019466F2AE37859E6E5A8BB8862A480C1B12D6797B79663ED2333F188F34E6CF6EC87E43979F88787CE35877DDF0B689547BF5BA9EEBB2659D76354EBC39EE83975310ACA4F8867FF290793CC08BF29E60A97C28A71EA3084FE27845AB3664E80592412043B03056FDD5744BD74C9584094C2B75C689ACA8E4B3D3F91994E4722B9B331399310975275A0065935B6CDF5A6A8216188452394238BC82736488A84A0C96C580A81C69032AD5E96F4C3061DF5AB246C258CBA0B68A32916BFC6686730B3FF0944A070F535A113FC349CDDB0B67B40DEBFB5215167090F9891365BB3D87639FDA05843A079A430FD5892F57AC4510450DEC00B7905A3A14442231919F9ED4A76B2B159A6CCC3685B3DD1924935AA8E617AF18B5A065AC45727767EE897CF4F9442B2ACE30C0237B307D3E76BF8EEB78ADDC4AACD16463D8602FD5487B63C88BB66027F37D0D614D6F9C24603C42947664AC4398C6C52383469B4F9777E5EC7206210F3E5A796BF45C53268E25F39AC261AF3BFA2EE755BEB8B67AB3AC8DF6C629C1176E9E3B965E9369F9B3B92AD7C20955641D99526FE7B9FE8C850820275CD964849250090733CE124ECF316624374BD18B7C358C06E9C136EE1259A9245ABC55B964D689F5A08292D28265658EBB40CBFE488A2228275590AB9F32A34109709C1C291D4A23337274C7A5A5991C7A87B81C974AB18CE77859E4995E7C14F0371748B7712FB52C5966CD63063C4F3B81B47C45DDE83FB3A2724029B10B3230214C04FA0577FC29AC9086AE18C53B3ED44E507412FCA04B4F538A51588EC1F1029D152D9AE7735F76A077AA9484380AED9189E5912487FCC5B7C7012D9223DD967EECDAC3008A8931B648243537F548C171698C5B381D846A72E5C92D4226C5A8909884F1C4A3404C1720A5279414D7F27B2B982652B6740219C56D217780D7A5E5BA59836349F726881DEA18EF75C0772A8B922766953718CACC14CCBACB5FC412A2D0BE521817645AB2BF6A4785E92BC94CAF477A967876796C0A5190315AC0885671A4C749564C3B2C7AED9064EBA299EF214BA2F40493667C8BD032AEC5621711B41A3852C5C2BAB4A349CE4B7F085A812BBBC820B81BEFE63A05B8BCDFE9C2A70A8B1ACA9BF9816481907FF4432461111287303F0BD817C05726BFA18A2E24C7724921028032F622BD960A317D83B356B57F4A8004499CBC73C97D1EB7745972631C0561C1A3AB6EF91BD363280A10545DA693E6D58AED6845E7CC5F0D08CA7905052C77366D1972CCFCC1A27610CB543665AA798E20940128B9567A7EDB7A900407C70D359438435E13961608D552A94C5CDA7859220509B483C5C52A210E9C812BC0C2328CA00E789A56B2606B90292E3543DACAA2431841D61A22CA90C1CCF0B5B4E0A6F640536D1A26AB5B8D2151327928CE02904CF1D15E32788A95F62D3C270B6FA1508F97B9155A2726D80A1AFA3C5387A276A4D031A08ABF4F2E74F1A0BB8A0FD3CB0AC923A76D541CA65FDEC9C788A407326C7DB508119F617F43B6E8A6F48A398702E051C20C31DE77A1BA6777829F5539C886E3E14DED294D56AE5E88AC06AB09\",\n          \"c\": \"19C592505907C24C5FA2EBFA932D2CBB48F3E4340A28F7EBA5D068FCACABEDF77784E2B24D7961775F0BF1A997AE8BA9FC4311BE63716779C2B788F812CBB78C74E7517E22E910EFF5F38D44469C50DE1675AE198FD6A289AE7E6C30A9D4351B3D1F4C36EFF9C68DA91C40B82DC9B2799A33A26B60A4E70D7101862779469F3A9DAEC8E3E8F8C6A16BF092FBA5866186B8D208FDEB274AC1F829659DC2BE4AC4F306CB5584BAD1936A92C9B76819234281BB395841C25756086EA564CA3E227E3D9F1052C0766D2EB79A47C150721E0DEA7C0069D551B264801B7727ECAF82EECB99A876FDA090BF6C3FC6B109F1701485F03CE66274B8435B0A014CFB3E79CCED67057B5AE2AD7F5279EB714942E4C1CCFF7E85C0DB43E5D41289207363B444BB51BB8AB0371E70CBD55F0F3DAD403E105176E3E8A225D84AC8BEE38C821EE0F547431145DCB3139286ABB11794A43A3C1B5229E4BCFE959C78ADAEE2D5F2497B5D24BC21FA03A9A58C2455373EC89583E7E588D7FE67991EE93783ED4A6F9EEAE04E64E2E1E0E699F6DC9C5D39EF9278C985E7FDF2A764FFD1A0B95792AD681E930D76DF4EFE5D65DBBD0F1438481ED833AD4946AD1C69AD21DD7C86185774426F3FCF53B52AD4B40D228CE124072F592C7DAA057F17D790A5BD5B93834D58C08C88DC8F0EF488156425B744654EACA9D64858A4D6CEB478795194BFADB18DC0EA054F9771215AD3CB1FD031D7BE4598621926478D375A1845AA91D7C733F8F0E188C83896EDF83B8646C99E29C0DA2290E71C3D2E970720C97B5B7F950486033C6A2571DDF2BCCDABB2DFA5FCE4C3A1884606041D181C728794AE0E806ECB49AF16756A4CE73C87BD4234E60F05535FA5929FD5A34473266401F63BBD6B90E003472AC0CE88F1B666597279D056A632C8D6B790FD411767848A69E37A8A839BC766A02CA2F695EC63F056A4E2A114CACF9FD90D730C970DB387F6DE73395F701A1D953B2A89DD7EDAD439FC205A54A481E889B098D5255670F026B4A2BF02D2BDDE87C766B25FC5E0FD453757E756D18C8CD912F9A77F8E6BF0205374B462\",\n          \"k\": \"0BF323338D6F0A21D5514B673CD10B714CE6E36F35BCD1BF544196368EE51A13\",\n          \"m\": \"6FF02E1DC7FD911BEEE0C692C8BD100C3E5C48964D31DF92994218E80664A6CA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 2,\n          \"deferred\": false,\n          \"ek\": \"49469911485CB1C06A48A449F1A43B0456406243AF447A7CECD5467DF322A159AF32B6C59CB05D200CAC34DA66D8DBCFF8326FCCC08A77C9286F590F33C06AC36049B91442F18AC6C00C240E713D387C8BB2BA3780E6BBFE90A4B1D7B155360ED9ACBD63205BC3482B8953B3490427F28392B083A47BE5B18EF6AB51B9859FDB659B8424BA93A8F470014FCB6AB9E61FACC61311BC1BCB098469D9702FE54F8F931DE7B2B57543750A346367371F3724384261B569AB5D8C870A01822BB4E6C617F17DB6EB0D0989C5644459281828EF4AC11A119EF794530436CAA0E28B8CB5365E4854E1AB4BF87CA018AC6A8E62C36C97117014A569AB472DEAC7E7B1108BB8BDBD710AE8D033A1961917E171AAC6841B5A9C5D869E68974D79B8C70775955520CEC21B5EDB05B60FE230EC143BFCC15B550370E1D58E76D1B5BDB0747412B952131DB306A4DB2395CA69CB9912C0A1660517553C92A7210056C4B6F347CDF33C326A27155CA1A7BB809F8A4A46703537B6530E5987E02A9EDD83297A1218C68699E5A898F9487279D35BD01B57B57A30E85483194C68069BAB0FC4BA0F48880DD1089DD21ABCD07228D9B213C32EA184291A2B12E3E19B4E4B7CA51879C46AB93057AE748236265935F0B16CB456256419AFD82147C9247CE6A7CEE4F349AAA59DA7A4AEBB2B0ED225775CE085A9AABBF49B756CF833CAF22B39AB4D9AC4A26758430866C9D76C5EA68300AF90697FD304AD715C591798C7948FC1A954FFB40301A432B29726884266A3D8B8EED13F3A33A106349FF58075C9D67923A59E06322FC7258017179E7859ABB2A83234D81330B1297AE842AA1C0C8543972E5B3AEA4490D4B0CCC1F9681266A6A0F96DD955924EF25E39642556F18443497D2EAB77AFC01B1D2003F1981C92060743141458FC606BA85DE9DBB1BAC78D6ABBBC73BC7F4E0C6BCE13C8B49BA367E60929C61C34FC7BFF013D4743BF92A70B4683C724B06717237A76D58F81EA39A9F96F6D627162299BC82A355D06C766318BBFEA3386860D683AB75FA03A87B1B2C1D99A30EAAD78A79CEEB43C1621C54F79AEDF82C7F8CD5FB11F119FA35CCA71B4CCF3E1D83F07C3A66C3E82F2D84F640DE5\",\n          \"dk\": \"5639898D7A061A47880E01A1DD869A4393A58EC5811124A22AF2CAB3C61BE3E70492F22EACBA0B52E2C2F31914A466C647379957EAA4F16C9A10440BD25B6A6260626C76C2B5B7391A16897681535B1A279C38CD4661CC04044425297E47F6827BB8258C246894DB965EDCBD6C864D7ED91DC3A12FE0A86C6D2C40EB5812FAF8421F447DAD43BF2140AB9F473C1ED23EFEE952DC0ACB86C068CADCCCC32BB22117B6F8F9B6184BBEE13184B21BB78B87B33879147ED01BE9441D6D240019C2AF998C771A51B616983627354BF0EA1A4C73B8E2A092AB1772CA7CA194666DBD5C06A3464F3A55946C8B816147103AC24934A3488B95185414AB05FAA360C63FBB00A915271C50F25987E817CF849463B8C65CA76F71A83676D75436D27F2362294EB8C55017BAF3A77C57064586D0BC08AA4906E86DDD13830E2C21716258DB81592814681C823DC499752D62B414925B76B5A4BCA94EA30B2B20D489D887B63BFA72D9A327C121C635904800D68F3BF50A01E765E86894D802C1679964A0CC528CA558F960A5A9D8243CF01D94A99E68E4C2BDAB0576A1780ABC3BB8783E35F7441F395F7A1134C4016200E32602640D42252AFE3CBB3A624724066D5644BF93B8BE699B42B052591FC685D00A289AE76766C5A8A384034F5274E4B81ACC6A52EC0A33EDA0201FB600C702606D5806E95C2610C367426A02A7F1808B09B0119B2D71B7A1D9E93F9339309B941C67C855236761B7DCBDCD501AD736C15C0609C4141D1920651A631C39EBB1ABF4AFF51A56F3BC11BCAB098E960DFF115DB306121F19723A3CA5B9838349DC25781CC9804551C8B80FE9897216D72E1036CDB1C11D6BD710BB60C110507392B32DCB3A1ABCA4B67AD5A88DF76399E5134B8063CA068736E626C0CAC3655711D7004E19D56FF0C11F18417061E70C7F2B9A2DD920E297284EF21CF30BA29E1124C3945357C0AF8EF060D949613417A6B35305239BAC986B8E51468655744CD31BC9BAEA4E0BEA33CF665EDE79BAFEE8102A68AD5891AF29FC0409C00AEB904458FC73ED36460D191003A707EA81CBD3ABC5C2C24049469911485CB1C06A48A449F1A43B0456406243AF447A7CECD5467DF322A159AF32B6C59CB05D200CAC34DA66D8DBCFF8326FCCC08A77C9286F590F33C06AC36049B91442F18AC6C00C240E713D387C8BB2BA3780E6BBFE90A4B1D7B155360ED9ACBD63205BC3482B8953B3490427F28392B083A47BE5B18EF6AB51B9859FDB659B8424BA93A8F470014FCB6AB9E61FACC61311BC1BCB098469D9702FE54F8F931DE7B2B57543750A346367371F3724384261B569AB5D8C870A01822BB4E6C617F17DB6EB0D0989C5644459281828EF4AC11A119EF794530436CAA0E28B8CB5365E4854E1AB4BF87CA018AC6A8E62C36C97117014A569AB472DEAC7E7B1108BB8BDBD710AE8D033A1961917E171AAC6841B5A9C5D869E68974D79B8C70775955520CEC21B5EDB05B60FE230EC143BFCC15B550370E1D58E76D1B5BDB0747412B952131DB306A4DB2395CA69CB9912C0A1660517553C92A7210056C4B6F347CDF33C326A27155CA1A7BB809F8A4A46703537B6530E5987E02A9EDD83297A1218C68699E5A898F9487279D35BD01B57B57A30E85483194C68069BAB0FC4BA0F48880DD1089DD21ABCD07228D9B213C32EA184291A2B12E3E19B4E4B7CA51879C46AB93057AE748236265935F0B16CB456256419AFD82147C9247CE6A7CEE4F349AAA59DA7A4AEBB2B0ED225775CE085A9AABBF49B756CF833CAF22B39AB4D9AC4A26758430866C9D76C5EA68300AF90697FD304AD715C591798C7948FC1A954FFB40301A432B29726884266A3D8B8EED13F3A33A106349FF58075C9D67923A59E06322FC7258017179E7859ABB2A83234D81330B1297AE842AA1C0C8543972E5B3AEA4490D4B0CCC1F9681266A6A0F96DD955924EF25E39642556F18443497D2EAB77AFC01B1D2003F1981C92060743141458FC606BA85DE9DBB1BAC78D6ABBBC73BC7F4E0C6BCE13C8B49BA367E60929C61C34FC7BFF013D4743BF92A70B4683C724B06717237A76D58F81EA39A9F96F6D627162299BC82A355D06C766318BBFEA3386860D683AB75FA03A87B1B2C1D99A30EAAD78A79CEEB43C1621C54F79AEDF82C7F8CD5FB11F119FA35CCA71B4CCF3E1D83F07C3A66C3E82F2D84F640DE5B2F75D3486FE6CEBB15F8E0CB70ED8950970C944912A03717D9D168C7B589DB71AC2F3294D2ED2611E9CB1E07CD9684148F13E9ACEB931C8CDA7427873B44B37\",\n          \"c\": \"FEB020751BCADF864161AFEA7B63E63088517A5EADBC52F0833E6DE2E03C66EF3F71F92FB61B277A26D6C2D9E01B88F0738E1A7409CEBFC9D7230C69E02D3BC7F0403B01512F0E082CB9023C7174623478CDBD6CC5E6D65A09AFA63C8B2686234DAC6FAA19A82087F0847B40AB47ABDE90108A13F3AB3601B7EA70B766F1645E7B4428C4AF8CCC19B62C057C8EE9F41E77DC00E5FF5F4ADD0E8EDAD2CC9D6DB40015E5207E7CDAFC915B8FDB1FACD6415FE3E8EB4DACADD7742560C3E2D3EE0EFCD306FAC97A8FE45CFEBEF1AC1B2F5D5316A4EF9C7D3DB6582354680E8932079567D148473CCDFCF32FD7E6D7F3226EC30EF2792F819229B55C5CB8514A77CFA44F6E8531102D073E8CEC089B4268C86B759F043A0E9D8EC8D57EB5618C6D0ABD44D64E9CA25913588F6CB4CF7D0BC914625737140C0C7E559BD00B2C448886B983893FF7F18ADEA02E41A07117644D5A208928892B20685F7A8476C641B25C48EF63551712AB97B0FB759431B287DFD1488EA11CEF813240E2F4E1F5619084B5D7EFFDE09ECE072614BDF6A970168F8D6628FBD521F1431C9ADC46CE31281AB6D6E762E8A7779FDE0F5CFCC36AA677E1F032010F110FA6B460F7294545DE7BB28D6D5CEDA5D8832EFD3E32F9605A7BD43673A00EFD0CCAF63BDEB49C9AC1872B9D3C8CF7A9C81936F18667D15261DECB9A354144D7C3F2BE726FD0A83B1C27E8C6AD66B1AB77425541EBE321EED279526C79154FF27C1B5306414E60684ED42DB69BC2785A1CBE14CD0AEE879173CD3C94020210CAB4BABC531C058857522D9ADF25FC5C3F2FB0A0DB8BD15F23807AC84C7319EC29FABCA43C99F72354415ECA9844D06D299C9F4AE49E0BD8CA7623B8FC9D2DAFFF3F368E4BBA32489F3EC202A9799094469C674CC216750AE88F387A6C030D94BD8E870706D2D74A27A27CEC92CFF8CFD9E09BE6FEF40F807CACAB3628262C501ECAABCE9CBDDE8B3766813BBB8189CAE1AB38A3605F6354F95432E8EBB8B3A5768D2B816E2F7D22139C0135D0070CC41D40ACEECC4CA16636F08C404E3E5E6F6C7D8D652F6084F6477729\",\n          \"k\": \"9183CD7EF4AAF2F21E2E852771F524B10CB2BDB8C0BA1DD36EF48AB391DC7307\",\n          \"m\": \"4660985A5838041F2E50381CB4E7AC908BAC83CC1E074220C6705E3F5FBFC2EF\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 3,\n          \"deferred\": false,\n          \"ek\": \"C70876917C9AC2A2CF2110000910898E21804D940D6F761A525578E2B0399F1C0800B62C8720060F88746122C948446472E73AFE78A205857C6D32B6C01CA0EC742311840A36A8BD157B1C50A944B4C213FCF9558E3068D9714ACDBBC88A1538B59764C1F59C079C5BDA31BF55EB731BF69717A9CA63DC316636461CE2CC42201C359B88D044156DA30AAB932F902030BEEA6D58998FB2363C8223BE3DDC437AAB4D1BA27FCE058E701B10FD211E5862542C058F3DE33D5561B8BB3A83D6C55377862181150173D2135CA3C8084615668C38ABE8B72F193FDF96ACFA3470FAA53450017EFC36882C5AC722659D229225E01C5F976171E19172BCE5290073AA08276FFC85218F208BA3A1108B62545941C8991C4CE279C697D8CD06436134511D300C3A5FEB963D36CF29D6065E085C9663443342A8388763B16566D99C5C5DA4A451C2688E10936293948A57C4CE7B73BCBB698D9B71E2974638D5433E56A411E4089F966F603120D56A48DB653A5CD373D42A6AF3117D71AA4D26A1639D63A305B8BE1DE15C6F678519FA671A91483E46AEA26C0C59872CDAF57B3A6453CD660A0D9A0A562BA23678CDCDEAAD1CDA5820C65EC480347FF2872EA9574C4148C7D59980C584646409793190BC721CA6EA9BF029C695563B0598BCB357A1A221CEB3AB96F2269F45C24108E137D1BC34708B1B66352627340CCAE6CB22F41A75A0A55F58701DC4157A1316FF968CEE300FD55A37B5957F41220BF547933C67201D67B1AE278D9649BCAFC68F082B7D1C6016EF360D200CA0EBC76046DC5AAB171A0D544719E3411B587DCE380D467BC13368A25FA51A3E9B4EDC77561408B776F83A76B24B489310E3B608F1B40EB7395470F86C1D0622C1545065707462D89B364CAB2CEAC9501157F91BC52A2816E9598846B56C1F7B1A6D06195F92BD53931078E923C9D1552782325878832224B21A1C26562245B636943803ACBADC559D3BB6DEC54ED360239F74695ED3364DE562751892F2B2936F6895C166697A4A44D10A49C59C53DDE4B2631705440B957100AF4E1BBD47F2AADF22780AE0255D0AC18A87A28D282DD1C59C0C51D373C8F4A912400D5C87B499E0BE8CA991939F160B\",\n          \"dk\": \"AC789443C51AB5E60BFE7B56D8418AFB874C4C563C88C60B77A1A1516B01CCBC36445B90EAFA778CAA4EFD759982D2313EC061F1E259827214E415A583A911A68581E8B228DA8C30182B3157B50742F835121BB501F7B96947330867BABC615D1044BEFC0467D8B88E78FCC2089C9C8E563DD56BB08FDBB8A5451EDBDBB7ABCA20573B7AA8BA48BD5BCA558CC7B2E512A314CBD5D6A4CECBAEED533216007A7AD988B0D06A37C1C443F16B5B246C2F7C38BF68CDA1DA8098C6307C60658D6C596EF67759923FFAD16EBCC4912010785B7798AF299296653E2D865A12631C2BB49C417C19A655897CA6491CB6ABBCA64B6BFA5D811096D2F99773888153590068F8220534705C7C0E0B1C2538D31A7F0C70ABA02B50B9449D111792372B17C11F85AA67202017A2D07CDB6B25C1D97ECE11991E0C0374167E82419802BBB45F87899F262CBA0C5C74C993849B4CF412C858700B4BC9BBAC82907EA400118C59289CC07BE9CCB265B85B7B2294944F8E50465FAA496FF3865EE52CF6A07492231FBEC32064B30B5E6672616650983B730CC19F0C3BA4B354CB7A1B77A9AAC32D30712EE302CF131B6E38C40A9CC40C3C661848690710BC4DAB94D484B4D7EA7596C380F6C6AC023C01CD773260C95DB6A317F56C62CE0B62A0A59D5BFB99A77B6533C026F4FC25AB3221C643C44C75AB5F70A73A40487AAB651CB62D6E0B7AE24A2D736BBB26777417328078B6A16E151966A72DCEDC8E3C309F99A3989FC61722CB5EB6EB8D3C8993FA57B487D8B6D9C82D37D865FFC172D5478262C3AD8498593E2CC6E9D559F3356DA36CADD4560356351DC40466FB2B840048C4FC857F323126C5944C5C762E5835A90629C0155428B9F99472555FAAD44131E0193021C40DB0A9006543965B8CC1FC0B80092C69938DE4874EC92BB3207AA76CF32ABDF57848BCAFB6F242BF7905C782B38AE1B77DD2787FAA90112768544AB9A8354B9AD41304B15167747A1DE82B52B7CC55CC0D6A59140AC29FDA1654C628492AF47DBE02C8D809189B098D9F8C90C57A91CF853BDE416F6D09638AD2AFF2A0A7987C25C70876917C9AC2A2CF2110000910898E21804D940D6F761A525578E2B0399F1C0800B62C8720060F88746122C948446472E73AFE78A205857C6D32B6C01CA0EC742311840A36A8BD157B1C50A944B4C213FCF9558E3068D9714ACDBBC88A1538B59764C1F59C079C5BDA31BF55EB731BF69717A9CA63DC316636461CE2CC42201C359B88D044156DA30AAB932F902030BEEA6D58998FB2363C8223BE3DDC437AAB4D1BA27FCE058E701B10FD211E5862542C058F3DE33D5561B8BB3A83D6C55377862181150173D2135CA3C8084615668C38ABE8B72F193FDF96ACFA3470FAA53450017EFC36882C5AC722659D229225E01C5F976171E19172BCE5290073AA08276FFC85218F208BA3A1108B62545941C8991C4CE279C697D8CD06436134511D300C3A5FEB963D36CF29D6065E085C9663443342A8388763B16566D99C5C5DA4A451C2688E10936293948A57C4CE7B73BCBB698D9B71E2974638D5433E56A411E4089F966F603120D56A48DB653A5CD373D42A6AF3117D71AA4D26A1639D63A305B8BE1DE15C6F678519FA671A91483E46AEA26C0C59872CDAF57B3A6453CD660A0D9A0A562BA23678CDCDEAAD1CDA5820C65EC480347FF2872EA9574C4148C7D59980C584646409793190BC721CA6EA9BF029C695563B0598BCB357A1A221CEB3AB96F2269F45C24108E137D1BC34708B1B66352627340CCAE6CB22F41A75A0A55F58701DC4157A1316FF968CEE300FD55A37B5957F41220BF547933C67201D67B1AE278D9649BCAFC68F082B7D1C6016EF360D200CA0EBC76046DC5AAB171A0D544719E3411B587DCE380D467BC13368A25FA51A3E9B4EDC77561408B776F83A76B24B489310E3B608F1B40EB7395470F86C1D0622C1545065707462D89B364CAB2CEAC9501157F91BC52A2816E9598846B56C1F7B1A6D06195F92BD53931078E923C9D1552782325878832224B21A1C26562245B636943803ACBADC559D3BB6DEC54ED360239F74695ED3364DE562751892F2B2936F6895C166697A4A44D10A49C59C53DDE4B2631705440B957100AF4E1BBD47F2AADF22780AE0255D0AC18A87A28D282DD1C59C0C51D373C8F4A912400D5C87B499E0BE8CA991939F160BBD4FAFE5DBA7B6E6DEE2892A0D23D7FF97262CF3BE7D86976521FD0E33969DD804179FA8AF901D178A41C1E9F51DBADF03A4393E002689723B0C5963C5EE326E\",\n          \"c\": \"7E132EADB0E35C2A8E0916939F5BD7EA42DF683EE4E64D0512D75B2882FB6372A5233B6BA26A9A1C418171EC4C3EF24E98FD578C87396DB28E35C980A6B3DEC1F772086EDD53126C46A82C6D4F51C1B57F49CF1487D188336CBF99F740EDF5A01D30729FA486B551E0B236D5E08C56C80FFDB2D1CA10040C6435A1E0711E2ED6FBC1A48AEEB6A5D8A59D036D9B702EB3A884476C781BF2996F9BF27C79648552F2A150BDAFBF8E134D44B4B4558EFD9D92F2289CA975E65B601BE687B31D6F028A51B16A5C83B0C20DF3F279C9EFCB66060330854355C02405CD9690BA6F8942918CA5F7C37CE3BA8BBF1F285A4ECBEEEF53BE3366D4AE61377BA5A1730CC82444753A11931790D1228E8CB87F7BC9CA71E6E871351EB81A332D33EE06E83048F84169BE950991863814917D56F97F8A0896B8D8A4725CA965C726BD3C1EF3892175B19D8B9CF83AB82CF55B02558BD255A35085E88D3E3B6185537D8559A6675F8773EC7775FF6518E281701D50A450009B845327D2FA5FEF85FD5B5F6F3BC3895742FF16E483CAF3356B4AFA6ADE61C96D09AE63AC9715B4C0D4AD64072EFEE7B70D7BE0E3AD7D84CAF9A8439AABBFCEF44B624DAB8ED6A4AA57E2AADB22AE7D42DEF201862DADEECBF0BA88EE5D4BB723EC35F99A8556E67DD592A04920B8B228D0460ECDD389EACD55BE9F77EFE906176C5C9A1C3D3B4788410CB4E7035260FA2A2E6E3906E5BA6F3C4CF5AD16098392A0F3EAB85FBC59673BB49B3231229963647402533FE2A8E6EE7B85110300B7E20955974347CE5547521032BD57D8D7A1B202220142FE22676239F8C84BF4ECC2A9A18482EFEF216A232E7ED7583E090DB56F61AAE17B6755506D366ACC6516CCB54537D45B10324A46282E4CD881AC40B45BECDC5238A9605BC722FDA5332E2596049AC12DF07FD8011AD170E12CD8B05E261BC4DAC52D7B0BF42B88267D0AA310F480D67022C740666F9101701ED5319C1DBDEF15F6AAC5E0173CC5436B28848443C7099324F78FB2363CE5BA841DB75C3987C3840659FCE7C675BC5047F9F5C6BBC0057F28B32DFAF9A09E969A\",\n          \"k\": \"941DA82106D0DB42FFCB4EEDBC4123DF57BF0DF2A4E9119969872904FDC0B9B5\",\n          \"m\": \"0D643FF311D83CEDCB3A95BA0F76216A49BCA389A225396F708EC9A51BF18517\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 4,\n          \"deferred\": false,\n          \"ek\": \"E868A60B3888165A293FDBAB8B75C008E2695AE85A1C839CB08290AE87200868623049839674CF8E87C8D20C5C369B17ADDB344E263045017370D6885DD25785A0C430250795FC4AA1AB215109692B44C530005054020B32D73DED4B06AC32CC1AC47571B7244F2A05AD063DB57BB285C03C465A5FECB4489F82908EC72908B942153C318509C1508556B15B3BC7A671F6A69EDE283C5B87C2383324C3183B645086261B7CC4B53CEC6C5694106142A12A108A29D749AD47B93583020B3CFC328CCCA1611338A8576BA98659D9181607C42D7EE9B0C3174309FA7B3EDBC6BE422515C203B208C9AA1B3C387909EF511FF2A47AB03A747E0A83326B894168136BD6361D170B8D4087C99552D992929D832D58F26BF3D707B6D302F741456DE30FA6509A1314773FFC079AC94AAA10376ADBB70D857E556272F2A32E68485F84E28AD9326F656A57CE234AD2826298BA4C1CD4960BE4523126A405BC3E772121B1F7AB256239E9761B0EDB5C8D746B4D3323134742D69182091170E08C6E0D3B1862E202F332C7A70A25A3E2482113A5A2C257BDC6AF93C981E5ACC95118CCBBDB5B6244C1D060C9288BB2BB1A6791F748C4182B4AE6A68E060E2028690DEACB1E887A9213C693D57971FB6EDF6A95BA3821D445479B142737887BAD938B944376EAF87B15B210CF273B0BE07707FC52475C01B4485893710D84F5132D025E82630F8415330D257E1642ABFD8CC878E8B199C44107672FBDB8330F3C8CE8154E4E8036E86CCDBCE4B693B0C513132B8B740FFDFC51341BCFFA0BC204D63D6696B2271BABA633A572143E74382346C701F92C099B4371D18269A32769889C61AD428FBC110D450ACC95892791656A7DB39868E03FE6F97420910AE8476D43851AEC9A03734AB4E8E4087DDB9626F36ABF7480705C0E81C3887A64794CCB29587593BBB435C2377477B7A0E6A3C9050A84A99936B94C09DE28B018E73CA7C48B083C5BC1E73A24E20D16771711A863D1423E884808E0C8B6E250986AD4BF1B8592F8EB1549DACB4117747F2994D1808B65DB2AA5B05EBD7A0E614888EF86494B93822876C351BA487DB0D0C6F33C353B368BDEA3BC149CB74DAFCBEE209C50B88354\",\n          \"dk\": \"F9D66260ABC38576A4B54864B8CB3C1380365EEB412D896570BB243C29359D36366CE050D839462A5CCFD56BC4B28224715C39E0848C72094B5DE99C63252F20BB07D1AC305F34845A914684E0B03CEB6F0DDCCBC118CE06E7708CDC1E7F660824F7AA21487B7CFC1D6B788428FAB2F5062223CC6EC6285B70D7AAFD50832970525B7406F59C2A974AC1A6C0C813B4A71EA45371B1A7E2168F3E972F9EBBA639236E9947482ED75891FC834FC8CB44C51AC272A8CA2A0058F57A05C22D9166028D063BF5EB3576467368024C739514DB517D4E1B1CE2AAB8004D4987A96444B4B14CE633A4793D2B40071A77373D630A8CFAA884B074AFA03152C882E9C14806E65797340F0ED100F9D161ED46581B7C750926C009861C3CC89894258E9CF2032283708D1B83AFFBB7A6B890D9F9C725A1CF2790ACB1424C5BAA05C0B3CDBDFC78808695D6531877254B85819370F971CD196080B70AA1AAA8C691A8BA40C44FB60A898543979B5C29C3A40C0117C0162BC06A816626152B472F57178050E1BE9D66CB6C30584912C39FC45D60F26689696836FC60521298F4529888286636C5435A36BF6F2841D058AC19AAB8789924778B473A567189B4711F655BF5D25871D548AF92CC5392C30D2B50EB3C3381033D00C48B89C58E8EC81FA9942A2FB11A79549487F602FD8A85D6439C54C34493966AC4B3C04BB670A2AC537DE04E52218DE78ABF019085E470949565C111106F0C321620A65133439180783BC7261D0F47024423C43A2038544A2C72D480AFD1C219EAC504AB4003D11FAFDA4173B4A4E7E038AA659A3309AD9D1B2489C270F88CA659547F49F45E56FA149603B54A1611061762DB1B4D8AF629D1DC1396069BF1B9324195B5DA92ADE118588A664389BABDC35B92E5919DA2BA30254521DA834DD728749F9814D6777EB4A24AB4BC7BE5910EB39331CF3461D7B56392F99AFCD2B25E4A2948925C7BB744FB05C3BACAA53500B25109B4881C5BCC28661BA6626D648F3396450692503624737D5496C68BB7A83801534A9F86285EAB94B80536292F0A2FFD59647428CB15DA481DCB2DE868A60B3888165A293FDBAB8B75C008E2695AE85A1C839CB08290AE87200868623049839674CF8E87C8D20C5C369B17ADDB344E263045017370D6885DD25785A0C430250795FC4AA1AB215109692B44C530005054020B32D73DED4B06AC32CC1AC47571B7244F2A05AD063DB57BB285C03C465A5FECB4489F82908EC72908B942153C318509C1508556B15B3BC7A671F6A69EDE283C5B87C2383324C3183B645086261B7CC4B53CEC6C5694106142A12A108A29D749AD47B93583020B3CFC328CCCA1611338A8576BA98659D9181607C42D7EE9B0C3174309FA7B3EDBC6BE422515C203B208C9AA1B3C387909EF511FF2A47AB03A747E0A83326B894168136BD6361D170B8D4087C99552D992929D832D58F26BF3D707B6D302F741456DE30FA6509A1314773FFC079AC94AAA10376ADBB70D857E556272F2A32E68485F84E28AD9326F656A57CE234AD2826298BA4C1CD4960BE4523126A405BC3E772121B1F7AB256239E9761B0EDB5C8D746B4D3323134742D69182091170E08C6E0D3B1862E202F332C7A70A25A3E2482113A5A2C257BDC6AF93C981E5ACC95118CCBBDB5B6244C1D060C9288BB2BB1A6791F748C4182B4AE6A68E060E2028690DEACB1E887A9213C693D57971FB6EDF6A95BA3821D445479B142737887BAD938B944376EAF87B15B210CF273B0BE07707FC52475C01B4485893710D84F5132D025E82630F8415330D257E1642ABFD8CC878E8B199C44107672FBDB8330F3C8CE8154E4E8036E86CCDBCE4B693B0C513132B8B740FFDFC51341BCFFA0BC204D63D6696B2271BABA633A572143E74382346C701F92C099B4371D18269A32769889C61AD428FBC110D450ACC95892791656A7DB39868E03FE6F97420910AE8476D43851AEC9A03734AB4E8E4087DDB9626F36ABF7480705C0E81C3887A64794CCB29587593BBB435C2377477B7A0E6A3C9050A84A99936B94C09DE28B018E73CA7C48B083C5BC1E73A24E20D16771711A863D1423E884808E0C8B6E250986AD4BF1B8592F8EB1549DACB4117747F2994D1808B65DB2AA5B05EBD7A0E614888EF86494B93822876C351BA487DB0D0C6F33C353B368BDEA3BC149CB74DAFCBEE209C50B88354804C79976E41410C336BD08C60D65A16BAABB81987C8E6C6716060488905CF550084F403AAD82B09B96BB6C85D25165EB9E5BDFE784F096522D8BEA8007E19F1\",\n          \"c\": \"0756A8BA612C014FECBE00AC1E49271C6FDD87EF587C9879D6F8159E10982B920D3D1F477D9DCA08619185AD10D802A9C4C68D4E68F54E7A530126EEAB93386AE1F184843D90C34A1FCF7E9F54EE61B40A6DD52BD091A4E44DED49D8E8C9B63A395FA22ED602D03E399755F49AD766C49E24994969CCDA54C986DD47DE9646BF5D1DD5AC0EF7B10F86837C7EF18A05E6AEA2254A956D06062EE2B9FB3640C60E8F5907E99524B4B0EC608B7A0FA698B78B7485B6C07DB74897290659A99FBBA2D8CE4FBA364F4A5978984D1ACF0C175B90B1A04C14DEC67561FD9FA789D418EB8033C99863CF52E6653CC4C951F95D718D77088CBBC36A9B520156803B85F3BAA123008C4B2CBEB52C47D790BDEDFD7E7E8B7693DA18AE01A034781DF62CE288DDD29949DCCF28D0B2FD712C4A28282FCBADAF8CC9F811C1D81893161EE2ACEF36D0C3BF128FDFC77C514DE6CE81A9762E7CFDD53D90FA643F64C68969B2076A259BE106D4C5E5DE63461F48C50ECA1AACCF7EB56C288672FD5E7F2EE3167FD01C3A0FE73BDC97BA69474ADD93203BC01BC1D9F8DEFD59433CE26D117C66692149CC8610C89B5DF5B6741AF6C4BDA814657321AE6CEBCCBD5169B32C70083E2A52D6135FB4262862E080888716245CCEEFD6243F0C2C757A3C2915ED58F2D0C82CA5F9C24547601DDAD0EE1832DF0D779D001C28AE57845444F9A527F32ABA50318135931D3991E629B575762B7DDD0239A9E9EBA94603EB5CAA3B2245A608D84325D5D8093DC4136AA736F5D70E581ABA35EFE446E0FFF5F727D413A9C5C5ABAE83AB0F27A710D32BAEF300BA753C4DC7A997AC24A3E8908F1A705DF7D16876DC3854FD25D747CB8FCFD9202EA8DE379891B98A1859CE74F035BCC28ACE3EFD85B7AE2AD566CF64B91B9808686A4D4ECF498F53DA2F514A34F45EE14E28CECFC12BBB34A08F5FD68D6A856C039102C298F40787D64786085255A855965AE4A4B61C7CC395B4E04CE55BC9875F6C179F707008544901D64C3A3C987C88525323D075586A3FDC6FEEA867F0161F619F8004E4093D40FAB63AF2737C51C9637B5F\",\n          \"k\": \"8A77C7C5D298C5A9724AC05B9FE0D8843A7D859CC40E07C786F1F96F921F76C3\",\n          \"m\": \"AA28DCC71FA83D9997DD733D8B0D0394D84D33A3D3E1B74CB74DC6049628F861\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 5,\n          \"deferred\": false,\n          \"ek\": \"76608E0B9539C1B6470F9ACCCF5465C6B355CFA9424DA750CC085477B4A0E9F671DE185995FA38088506A8524D6384A8F505058800B3F14019DF2977E79436D81B8653B844920BB927D2CC7657666DE99C4D865FCAEA3F18302A36961ECA216740075244215625419758B8B8A82C46FBB79333E56D8076BC2F98610FB3474E75A471AA6D746708037020D3578381CC7C959459FDF21839E8413B44C218643CAC49A0179B2DCCB11235345641A10F3D369FD03C8FF733589607616A50926C176CEB45217A0A62A1F4AD3D06669EEC8C947372E8D082E34789A8D9867C0198E5B06934D904AC5318B58A2A5F3AB2522C611F7BA5A1351231F836881116847551D834A310EB82E0B2C6E1728196B7206D00094AA928AD8138F97879B56B55FCDC0D35C76C336526CF52C6192B0793745658E53155241F46663A8DC153ED12862942674B029492D701BAB56D2468C4B165B588D2A6A94C515E9BCB91A55BD372A613590CD76416C6A0AFC40A60669C90BC3A228F05A6DCEA473688CCB80A4CFA3C9BC5157CE4D37BCED562D5B52B70A48294912BD127BE37C39EC4F23F7C3C01A521C56D121D6829A76B7235C2BAAA2BC841634521D8F360D5B7B31B0648FA442C546878A7CB2FCBA5559F94A90308CBDB72C60551409E364CEC93CE24D16593609159735111F918FEBA4B35465BA14AB8AA1A705AA9ABDFC596864347C6E445F23B64B81AB330B454D14413ED47CCAFA49946B828AC3056CB4195C5351FD65A4BAA57262F9A85A4DB552D939BF539B8C4C547B801C1D99C7044393A08143C332AA440477258E04A83F6089C864B1A69418D0B4B739A4BA729B3FCC9C10F9834BB9409950449A8EC09ACAC79BA0681B31A2C7E687793C352F496010AF4A30CFBBB89CB3012216D8E2B489676B05E4B8F142825C4C4552FF7A48802C3E8040C4B2C744AC477EA84901A27BACA79635BA6C0ED28947BBA8B788ACBD17CBB01CA931E4BCF2BC15EC01476AF0A5F8672897AB5A2F9F850BB0ABE6B61A2A4C800F5671F13D712E8686C780677952A0AF65B463CD0289FE3B56F89A6F7D1643F0823093589FC76619ECC2590076DEAA2D895CF1A81924A0490D99446E364BBA45C3BAC1D40\",\n          \"dk\": \"7880A126F08E51F0A6E89329E38741B15C1D4F2410131BA19CD49B9BD32A71487081E4B0E4692CBFC01CCEFA210BA23303DC9C05E317A9F5A51C535BDB6BBE7F1043A5F841E04B7F70E566AD3C28A84352B5064B25DA223B16A8478BB038E221E2F853A1BC07E8940F97175292C6BB8236661D587F6F539BF0616642B82456F7839BF0A9A393919E1356E070058E0442A61CA6F9019240194E61C453420181F8D0A108F87BAAB970AB0B6DC5A7909AF7193C975BE88849641341E42C6F92851EB8885A951779D606AE9CF7271732B728C35B5FD7611FC7C44362A14B16B16F779CEBDC1740F6A8CD9540CFF05BCB95B1C10044EA14256C34B4DF46C2B5363B33390728C152A6CC9778E21A0C909D83001608C773F4DB30E6774DC7F6B80F966BB1494764E8983EE62F35B14C977C1B05C43734528B4281412738C3C10CB4A1016C5EA5B220A4899D91311B786706285804038EB83357DB1B4FF3B12C71850F33733A52582707351EDEA8336C30A326C66805638318E32E4027C97485A77FE4817AA65A48085824D24ABED83443296EE2A306D4642F71FA98C2647124022812C5652FE4AD7B6892E2239FAAA9261F9B2374F242900461A469AE7F2259CDF527F062C1D55C3948840E4FA7305A088B126C27F40C6A9FE0938879A5C7F56157C32E77E417E5356BA200CAC228C647244CFC03479373C40EEC90CB115D4C723D1057233D42AEF5E7557B0713E29700A246BD168272EB0348D282408FF66B6DC16968E7AB493A64EE626F96436D996A181FD5583BDC2F8609B9A6736FAFF56C80375055A8992EEC85FF6575CCAC18C2B763F1E745691259BF4695324C0A42C3C87B7639CD45352A7603730A44FC7473C79A3C72F18D77CCCCE489C897FB3C7204145DA416C214818EDC7AAFA0262D5A330A4C4FDD72C03DD5568AD1620F6564F19A98630C90482C7C064A9788F0A227F7332B901BC359A6FB2ABB30C5131B98429DF96C35A2A81359C0D9426B516847375B8AF236687A071179EC53B54C1736918599C193FBF5C6B421CF7F029B691482DD2A539C0268E590A3CA563FA88674A6238576608E0B9539C1B6470F9ACCCF5465C6B355CFA9424DA750CC085477B4A0E9F671DE185995FA38088506A8524D6384A8F505058800B3F14019DF2977E79436D81B8653B844920BB927D2CC7657666DE99C4D865FCAEA3F18302A36961ECA216740075244215625419758B8B8A82C46FBB79333E56D8076BC2F98610FB3474E75A471AA6D746708037020D3578381CC7C959459FDF21839E8413B44C218643CAC49A0179B2DCCB11235345641A10F3D369FD03C8FF733589607616A50926C176CEB45217A0A62A1F4AD3D06669EEC8C947372E8D082E34789A8D9867C0198E5B06934D904AC5318B58A2A5F3AB2522C611F7BA5A1351231F836881116847551D834A310EB82E0B2C6E1728196B7206D00094AA928AD8138F97879B56B55FCDC0D35C76C336526CF52C6192B0793745658E53155241F46663A8DC153ED12862942674B029492D701BAB56D2468C4B165B588D2A6A94C515E9BCB91A55BD372A613590CD76416C6A0AFC40A60669C90BC3A228F05A6DCEA473688CCB80A4CFA3C9BC5157CE4D37BCED562D5B52B70A48294912BD127BE37C39EC4F23F7C3C01A521C56D121D6829A76B7235C2BAAA2BC841634521D8F360D5B7B31B0648FA442C546878A7CB2FCBA5559F94A90308CBDB72C60551409E364CEC93CE24D16593609159735111F918FEBA4B35465BA14AB8AA1A705AA9ABDFC596864347C6E445F23B64B81AB330B454D14413ED47CCAFA49946B828AC3056CB4195C5351FD65A4BAA57262F9A85A4DB552D939BF539B8C4C547B801C1D99C7044393A08143C332AA440477258E04A83F6089C864B1A69418D0B4B739A4BA729B3FCC9C10F9834BB9409950449A8EC09ACAC79BA0681B31A2C7E687793C352F496010AF4A30CFBBB89CB3012216D8E2B489676B05E4B8F142825C4C4552FF7A48802C3E8040C4B2C744AC477EA84901A27BACA79635BA6C0ED28947BBA8B788ACBD17CBB01CA931E4BCF2BC15EC01476AF0A5F8672897AB5A2F9F850BB0ABE6B61A2A4C800F5671F13D712E8686C780677952A0AF65B463CD0289FE3B56F89A6F7D1643F0823093589FC76619ECC2590076DEAA2D895CF1A81924A0490D99446E364BBA45C3BAC1D405D5240D40DACB83C6E97603086982B2BF96DC0108BE0A5C76AB85AD6985BD6891BBEDEA993E606D87B101D21308B55560DA8BC3F7C7AED02BFD2D42E4D722BF4\",\n          \"c\": \"371904CA3678917FEE951EF2AF3C21CFFAE77DBD02E836EABA8AFF8B687D8FC28E2443E2F6FA020FDD2962976C6D8062FB57F22A3ADB52EB9AC8472EE2A08C4F4D98632D1D752AB7BE7D57F5FDFCF6355E1AECF7C68DA4FAA809177F9C8A749CB779F49F97B65E3467DDE74CE2C68590465B53E91F2C5C988AF7A0BEEC8090E3E3C60441FEB212D2602CFA3AFC27EAA686EB92CC5BF7E914489A33646FC6A63AF1284C108CD287001CC9867A70E3D62B7A437B1B87C095A26543F1B7EF16EA944F7D4FB382AD3329121281FD6CE8EF3EC215C7C13C2FDBD971CD5599ECF5ACD46616B1C911F03AD284826291F56BB412467143D392C07AA0A50BCF9E7B66CB9FDB25041CA81413B9B392C2F937F033D34A8A293E737D7487E2BCFC2C48E101878E03B1EB0CDEB3BC55BC318286521347828C9A5B4B12AF365EE268A743AE1F12B4A145264E628FC9D35DEE5ACB6122996E01E78221E46AD966B53C122EFD33774C7908BCAC16D79BCF41CB6F648DA913293434B237DD06511D2A5A05E8E5C01C44408E53CB4DB6FB22874D492362AE1D2BF16170690392D979A76627C3D5F5347EBAD4315A76649BF09C0DA85A741CC72C5D63A71E3F7BAFA97337FEE2C8C5EB5583257AC1A94C601AC42071FC3ACCA48A8C42ED3667847B3D938BCEC9AB45C8B150E35F2323127583DFBEEF5C1E8CD0E1333B7F4204FED4B5C2FBFD978B3D42352BAC156AD32916B2C22C76C10BEC77BD31046AD7AEACC2A55DAB6E5DFBF9212FD45A763E125762D6E6D35B253F05E18656DF844594987ACC39A34CA480497CCB2981207E16A2FB2F4FA75C8F603CEF272CC63E771EA6F45AF51B18AFFBBB7B2D3C8E31260D11CE04224FFFEE8E665D20977C3C30FE1C9E34495C3AA47019020B2B4582F8CCA36D34D5D27904A769DFD6C6A224B7B7AD693A7C24D04E04063D5B0CC653047895DF676CADCA882DB762FC3E432171E358B2E2C24BF514FDB6303635C6D3F3CEBDFB711E97A730D63D8C71A6DBD0349041BCE85864CF021EC2D335E48647DDB8EDC5C2DA942D87B922FCE5BCBA8E4E59DDF31EAB6A97D6F01049\",\n          \"k\": \"7251DB9D63102DAC680DD894609F12B795371A4012BBD22C05AF846E5D43E884\",\n          \"m\": \"A4BAA4C603DA1368C1F2AC552A331F77BF1D598C6BCB540D43CA1E6D4B8BDE77\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 6,\n          \"deferred\": false,\n          \"ek\": \"E331342D560ED6120D12B8044C013C5A8A1204164B00130354015213361BFB439417F5AB35A18A4D1600ADE0163F6A1760B08B3BD8A16C3A82D253428508216E3952923070BCBC922FDA6D0E1A734232194EF979790828A110B9F096CD37B5C07D758D3D68CDB9642FAF100D732031DEA07BA884ADA7D4436C490924920D3B3B7B67297E5D8A984DB9136F44557C6BAA5023219048600B8550DEE669A9F38858D79BAB8682FFAC37B0CB9362723B7A1087A21C477513BD386A30D92A5F52F008A32784019B61E0C8C22B096B356C65CEA631CAB14632A2B978A40A91BA7544559A116CC74C28332DC745EB19AC5EF50425E59FE2C777EB0AB7F4EB49A3393CEFCBC846B63D399A6B86CB6168958E71045208C81F07F62CE826729BA2162C2B835185759CF4AEA00025F4AB46EB56AEDD408B0E016C6ED538A1280F6A0CAB2CE7B573258937F0A265D663F0F33495BC4852D73C774061DC388C3E75ACC69A84DE303EDB605CDBB238DB265AA26A8267FCB4A0922A5413CB65783CF37CA2051071FF00CF57BB6000C6057BD93D4DF31EF35CA5A4327CA8AA2EF7D33A04E5261E732EE8FB819AA446E02CB4370B2934140EDF17BAF6040F71B34F251C6C65B766D317C4DD181AC04103409B65C6A7ADA248549BE72A2B43B2E4B85B58CB5FFC0B33C675C0E0D50B2020BED86005BDE97B9862436CF7A7ED687591F9719E4803254BCE195185CB3C9A48DB6F148A0876B00D1132AFDFBA1C91F57E980B96F1906439489093194F2CEA56C46942796749926B36D498B026353CCBAC0BFDC84C208502E0518512D427DB058219B35DC793B789F5C579D9A5E7285B3AE90F71FBB40FA39C5AACAE582211D9130B7D309AB3C34FC279AD6016C611F902AF959C984564BFA565891A5D1FE13C9D27181A512EE1CA31BCB55114C316BE427B01B6A519E441F66B926CE959A90A712DA1B81DCB0AE6D67749987B3E541A59D2680B11AF845234937B63065B32BFBBC1C9086C7DCA12E258C21E7AC52F812CE4A16E347C5CC115C2A16355F4745B810A4B5140CAE72248B554725D9C186523CF625013D5CA11B7951425CCC18E014EE5E94C2695BB469BD83646256DE038ADDF203E0B60B1F6\",\n          \"dk\": \"C97A2268733ED7822CD3B809F395637709A15E02CA5CD7381F27B78D9060BF67B334E193372029FA6601FA5BCAD83429A1F22ED0291AEED22790F62F27A57AE87B81D2A7B2BE812B6988400A62156FDC6DA8261C81283DCEA41C0ED48291128E38007784F65A2B39681BEA58945971551BB873869DFD5C093EF64172583AC2CA8A4646B81786A0A5B039C1F00D3503B31D352A92C2AA7880945924AF63D872E8C85BA6D499B22B17682B508CF981F9C9A8B0DB33B0E7917F9197D0D05FF4A5C38A5102E20317DED4837F46A88A41A1A9D588EBCA92440651A1F5774DC7803D3C88547C57A297A4086855AF7C197254CBEC831A0A4666E265C68CA35678068600303AA17B49A4893B0ED913BBEC197E323DD48B94893AAA0999096E7791F9E628C2E26A9E70C8B6B6AC3D1C6B61943E0220BF30D3A70BEA3272069DAAE676CEC68405A6821E0918B0730CD8D282AC733E4749C05CC382D5E727567A5B8F7258E3358831B160D9F90DC76856A0F462D2D6C66B9266C487131F37634B276E2817A2A946B71B7B84F93C64495CA2BA2947F731442B38BA9A954676580B1CB504E3903AE2B8B02CBB1FCF19A6F37342CB5A13FC923529C0AA1B18B16CC1C7AF1946A41950FB81617C369555F5CBB720971DC8076764A20AE54557AB2208684426F25B0CB0ACC6B75D0ACA76713495528461449C2DDA394FEF871B81D38D651529D7BACADFA3CC05ECB0C27444E1A53C3822641BD0B43220A682772D67D7AFEEF60EAB2B3AD3A87DB56391E471968A65261CB3B219CABFE9975A71645A7A2C9E2A234AB49B77EB689063ACA410134AA8570B687767D8F50687F4844A2B3C1C5B05B188005A0B17D4710086D283F6834EBA728F12996841350507CB5BBE247355A464AC7A008D4B9A07240475C2BD3FE1C469788F51003762F7AB89A9C980F4ABBE76782C084BB8F106AB453A7B7A16047222BB4484F992472ABB62D4EBC053BAC367EBB80579A31F2C949D53AACBCB8ECD9AAB2FF07D86388B26E47AEDE4971A4290DCD7358D306FDD951681513FFD5054BB568534D188D608870ED2028DA36B65A37AE331342D560ED6120D12B8044C013C5A8A1204164B00130354015213361BFB439417F5AB35A18A4D1600ADE0163F6A1760B08B3BD8A16C3A82D253428508216E3952923070BCBC922FDA6D0E1A734232194EF979790828A110B9F096CD37B5C07D758D3D68CDB9642FAF100D732031DEA07BA884ADA7D4436C490924920D3B3B7B67297E5D8A984DB9136F44557C6BAA5023219048600B8550DEE669A9F38858D79BAB8682FFAC37B0CB9362723B7A1087A21C477513BD386A30D92A5F52F008A32784019B61E0C8C22B096B356C65CEA631CAB14632A2B978A40A91BA7544559A116CC74C28332DC745EB19AC5EF50425E59FE2C777EB0AB7F4EB49A3393CEFCBC846B63D399A6B86CB6168958E71045208C81F07F62CE826729BA2162C2B835185759CF4AEA00025F4AB46EB56AEDD408B0E016C6ED538A1280F6A0CAB2CE7B573258937F0A265D663F0F33495BC4852D73C774061DC388C3E75ACC69A84DE303EDB605CDBB238DB265AA26A8267FCB4A0922A5413CB65783CF37CA2051071FF00CF57BB6000C6057BD93D4DF31EF35CA5A4327CA8AA2EF7D33A04E5261E732EE8FB819AA446E02CB4370B2934140EDF17BAF6040F71B34F251C6C65B766D317C4DD181AC04103409B65C6A7ADA248549BE72A2B43B2E4B85B58CB5FFC0B33C675C0E0D50B2020BED86005BDE97B9862436CF7A7ED687591F9719E4803254BCE195185CB3C9A48DB6F148A0876B00D1132AFDFBA1C91F57E980B96F1906439489093194F2CEA56C46942796749926B36D498B026353CCBAC0BFDC84C208502E0518512D427DB058219B35DC793B789F5C579D9A5E7285B3AE90F71FBB40FA39C5AACAE582211D9130B7D309AB3C34FC279AD6016C611F902AF959C984564BFA565891A5D1FE13C9D27181A512EE1CA31BCB55114C316BE427B01B6A519E441F66B926CE959A90A712DA1B81DCB0AE6D67749987B3E541A59D2680B11AF845234937B63065B32BFBBC1C9086C7DCA12E258C21E7AC52F812CE4A16E347C5CC115C2A16355F4745B810A4B5140CAE72248B554725D9C186523CF625013D5CA11B7951425CCC18E014EE5E94C2695BB469BD83646256DE038ADDF203E0B60B1F6A1E6FF9222F4F2C0B6E2F4CE6BCCB009EFFC6A423DF374F485EDBC06000C8FBAF7868913CFD39EE71033FD55572599095F2E641FFD2175F6472AD7E38809A25E\",\n          \"c\": \"A5A62163CA438B8A067E66246A18B815146656D4015E6CF9A1FF0EA73BAF7FEC4B3E177D850822CCAA0EC3191B18CBC05EFF51C78947E4565E105DC3570946E1CD76EC2AAF0AA18FC41D8C8F74A1FA602891DBF82FA7CBDA9E0235A35E9256DBEE2A4708C7472AA5E55F8AB1362883C267D1629163E5BF048056BC8D1C67D934B274C4CC0A486BCAEE2B8BE3FB21126643417607393E57A93483BF37A3091CE196D4FB3F1B645A17B8CD6259301BBE4FDAE4174512690D68CA888DBE194E3E2F2B7AFC4C43B6AF0EF99BD4A9CFB5114A178F501BF2ABAFBD74230C9BD549D91165E96D0B19BBF96C3A938B8E6F0C30EE148933399F0FB13B70F606094EF9B02C526BD66B6E1C2FCAFAB16E0A24911B7F3BC7904FBA00C27A752072CD94E9DC7A894BAAB5E4118AA74A32B3F8668A4C5098B466746B99008A979670572944122DFD32807564C4B56D387B7C48F727D121CC34365BA85FAFE27793EFDDD70E5B0183CF9E8BE4E9B92276E49DC675001E0CC8D061CCC36845C05833308CB99C9FDCA57CB8AF659E30BE417B776D31DD99835373396E7F58A9D07D301525DCA367C1FC39C228BDDC630E0FD76D651558B220891B209DC7AF154E2C51A254B088F083A1099F80CDE8274C6FCA19CAF00338D02208327967537F8FCE0CAD2F37CB90F10DB8FEAF457A25E049D85165433115787CD7487D8ABBF1BAC4A1D694715FDA4E145BE3D9F68E18551C2A8EA31163A6407AB6968FBAA88A0CBD30870CA3DE1D61BF4F72E582B9045C83BDD3E2E26276C9A3E0D81FD9E9BBFEDE81C047E2B3F3445AA5BDE4FF909160181B1F8089F759AE9CB206C5027E04991ACBA93A098585857CB1A983DF67F8E543B626449D7F2A52B64296B2DFEB1673FFDE4CDA17F62AA035A909FF44853AE23DBABF048248C1333BA6E6BE74D2EAFAB8FF52AB31CC47CBE84A2221D4CCC498D670C8BCF382ECCEDAE8599C4FDBE7F1B328A4AC91EAC2CA326D216BC904EA0AC019DAD008ACEAFCC6CC71C97A8AB70DDCB16761EFC8ED656CA72E0385E97F14F971132370DE24A682764A88B2BAD33C56E095C7DDC6F355\",\n          \"k\": \"F8F9921AC3524E9AF70CAAFCE21A20ED5FC76DC988625CA9465A257D43A6FBBB\",\n          \"m\": \"C08584D2F5C950E371668A4FC8F527E20AF1532CC28EE6B5620729155B06389F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 7,\n          \"deferred\": false,\n          \"ek\": \"83B08010002CFAE6362BC02EA2C08809E5031C213F78795391E4390F4A4F7D8403B79458B792B3B2A0BA0EA82811912CEA1C2553B264C03992D28A29AFC0C50FCC0343DCA764457D7F1A73A975044752CFAE0101AA47A3B706594B403B5561A451013EDAD45B60E42905A97D2A0288510B66E9E75CCA8A6B2E5504A9380785DB36B21B5B1839111A543B18E3B41F30232E1BC816A1992A01C51C32CEEA8B4CDBD4C30CF792AAA5BFF2D54DA6B988FD8C14534B2D24334625A68CACF5897B495D55A873AAD557C29957DA2644714303CCC8744640C932B8307B8A08C2403A94E17387F4BDF786B38276756D95C0B9FC4E56C7AB925643A21A9BB7BC6AC58262DAAB33AD09348F7C569147359DCC794EA4673ED1ABDFB71EE199B31FE1CADD2579F94682FB013039B935A3167FF8460EB3006DC7EA297B643B150C5CE360BB197732B7F9198F4A7479EBB8CB97A22010618E22200E896C807C5056D78CE088A8478A751B95750A849D12677C6DCA08CDA502EF603FB5C67566338EB13114FD10622BF8B0212C70E92C68827999ED7177CEA44A7A055F12A00231F3C5FEC1BC07812EA902CAEE99B899B39E88FC7FF6973311483F2A9BAB7917A56D2CBB549AA887E545A7554910947CB9F09423C75EE63B626E8BAAA7A00890340DA33B7C2CB6052B5CC47CE65A0873CE9126816D8BCB6A1C8D39265BD1797F610244C565670DF05E58760690D52B6882A9A303582AA03ACB3B2D857CA0D7B8A2BF859860DB729D2740FD87AF798497D558042319638A33326E3C5CB0D3CD6E71A346365B308221A1DC2E49F51E818B22ACF66FA0C586958CA78204A1FAF135F17977ED2801C4591F2893A756A0C4988BCCB011B389D0656A717A069BBBF1F893A3BB64FF28B66BD2106FC23C2BAA2E4495580D08C896A92F2AF36CBB13CEF4AB94E4CC9D866459BF6CCCDCE7912B3119EEF0AD4CF5093CD85674679A298272EAD6B14F2950F18C602C52AB821234F655419B843C31491C47C596596967F1EB036550A941A03790874FD8498DAFDA127C8B0561116F9D414171778C7C36012DA9BC543158A00352A2E39D802CA5254FB4A43FF40242ACA967C85D45EE0F8E13DDD9951336DADD5E\",\n          \"dk\": \"2DE77A797897F74413868189055980F0040EE0C0917405662CE681BC387D80339E8F5190B63CC50F66867CE78841379DE141141EB15A06E3948439A0F438BC56656A5846C98B21C037A888D1D23DE23A1C945B8B2691CAB7B1570606149A930200BC23B2EC335542619619BF8CB6C7C1A0B8F60C4EFF721ADE91C4AAC36E17894E54D3A006B60FDC62616666927C90A5BFD633F124BBCCB1355A901AE432CB344A72B525B58BD3C78ACBB919C3118E1346631B7E8A514F19006CC3D93AA693296EDABB57D37B61A863D3E6BC95F9C5BF114124FA5BE9425679CCA54BF316C97392BB5A7A16BCBD00953BA7650BB73BCDA3278E1EFC1C7D07B73F942326D871FFD470006657AD2B26E930759C474FEFC2CE64F631C7A39017C9B07D41434925740F03CB10335543D17BEFD57C5312A4C8609503924A4880A7664339FBCAAC331863E88C21F1109740CC8516201A20F349CF62CD56E3004DD1C56B255918699B3DBB3F262724C3F75AE212C9B0093781222B10EB9953806644F958702B80FF6274169979ED5C02ED270634B68F435433B6A36F49731C216C7C3E58BBD58ABC299C14CD1AADD4E9098F7C14354702194412A7F5341A451B6672AD433BA3A7331A006D781CC66BDD8680F5A201FA71B3B9D69FE81A624491CE09D461A8B50558B66C72F198B650A329E729C0753FF7E33E6DD41161AAA43B881DA83CB862BA187FD9AFF7885DCBA80C18C730F51360AA08CABA71C88DA42587600E59B97EDFDA010308A2E8483C71F92C3DE34625DB9E09540A8EDB1696E09C97EBB86507BABB3A901186815128A3A331CE51042E03875278048D4B085DB51094D967CD6BF6A872641E3DFA0A5DD7ADC582A303F3CC5C032C89C6B841397FE901702682939452C88D615BF488C7AF3CB433B72163436DC4E7112EF62F82D91C783ACEEB047D6F46036B70578F79634C98B5962C0EBE98AA97401ACA4B19E6A86D451206E9B06E4CB774F392AF4E1C587AA415B191818E438D0585459E43A50F61AAD3E934FCB09D6C78B43DC7BA6DFA50236984C6992149900843959AE8A7A9AA35146F78AABE2C2983B08010002CFAE6362BC02EA2C08809E5031C213F78795391E4390F4A4F7D8403B79458B792B3B2A0BA0EA82811912CEA1C2553B264C03992D28A29AFC0C50FCC0343DCA764457D7F1A73A975044752CFAE0101AA47A3B706594B403B5561A451013EDAD45B60E42905A97D2A0288510B66E9E75CCA8A6B2E5504A9380785DB36B21B5B1839111A543B18E3B41F30232E1BC816A1992A01C51C32CEEA8B4CDBD4C30CF792AAA5BFF2D54DA6B988FD8C14534B2D24334625A68CACF5897B495D55A873AAD557C29957DA2644714303CCC8744640C932B8307B8A08C2403A94E17387F4BDF786B38276756D95C0B9FC4E56C7AB925643A21A9BB7BC6AC58262DAAB33AD09348F7C569147359DCC794EA4673ED1ABDFB71EE199B31FE1CADD2579F94682FB013039B935A3167FF8460EB3006DC7EA297B643B150C5CE360BB197732B7F9198F4A7479EBB8CB97A22010618E22200E896C807C5056D78CE088A8478A751B95750A849D12677C6DCA08CDA502EF603FB5C67566338EB13114FD10622BF8B0212C70E92C68827999ED7177CEA44A7A055F12A00231F3C5FEC1BC07812EA902CAEE99B899B39E88FC7FF6973311483F2A9BAB7917A56D2CBB549AA887E545A7554910947CB9F09423C75EE63B626E8BAAA7A00890340DA33B7C2CB6052B5CC47CE65A0873CE9126816D8BCB6A1C8D39265BD1797F610244C565670DF05E58760690D52B6882A9A303582AA03ACB3B2D857CA0D7B8A2BF859860DB729D2740FD87AF798497D558042319638A33326E3C5CB0D3CD6E71A346365B308221A1DC2E49F51E818B22ACF66FA0C586958CA78204A1FAF135F17977ED2801C4591F2893A756A0C4988BCCB011B389D0656A717A069BBBF1F893A3BB64FF28B66BD2106FC23C2BAA2E4495580D08C896A92F2AF36CBB13CEF4AB94E4CC9D866459BF6CCCDCE7912B3119EEF0AD4CF5093CD85674679A298272EAD6B14F2950F18C602C52AB821234F655419B843C31491C47C596596967F1EB036550A941A03790874FD8498DAFDA127C8B0561116F9D414171778C7C36012DA9BC543158A00352A2E39D802CA5254FB4A43FF40242ACA967C85D45EE0F8E13DDD9951336DADD5ECD8D9B32FE7AB08059F4D70A3AB29FDAA5385C32E8F39DA46953FF323FAF2E6A0E461934F91330CFBCBD4CF4142F5CDF2065476376506BA36FA778DBFB29077A\",\n          \"c\": \"A07E5CA46B6B8A0370B19BEAC4FD58C994AF463C5F773D1638C3A296CF17CA8C18F3A0AB8E1DEBAB9E42995471B0EC8B473AD1F54EDDC84F48DA0EB534C567A73775CFE32F81C94246D991FA1E05EC6C31AA0B802949D5D7D8E5C4D7EF65E3080C01946F02CBE93F65BBAC03898FD25CDC32010EC4BB0119E30BF07F71A38E30FB5091F17D9F856653263F1982F526855324B6898C2671751DF332E58EC54C903A6BB6BEA0C96263913025DFB386651E6187BBDDB1FFE726C0DE8266FAF77384D2992E5EE8DCB31F41044754839C4525B9DB85B57E13F8C02120816D98B1C220687287CD7192C4DF31327676DE1D94C4EFEBEF3628E5E444386ADC087773ECF0FC79306828E58CD5A64CEB419EF383CE920A6FBB59ED2D2C86A78A069C90F9D52BAEBF4007AFA02C1D541BCAF0C8379D1788AD0AAFD6AAD91F4AFDF9C1C165CAB4EEC304DF6FF9F4E40E18F20FB78B3669DE6C0EDD35A38DA399BCD513C49A07F517AB446B19F4A0D13905C3D496CCCE68E8E778DAAB503CADD99B10951D417B5B6A3753CF9189C2DB624C39D1913F97C8ACC47A399DC2DD3539B083A7EDC3F1B7968B2D342BB78B0D8D9B2D026273D8CA46930A98C113E515F9FF779D10AFC857E44A0E190F90DF1E9B2AF4F5EAFCB451535AA8046CC7338722C29E729E93976D097C0BA766C1C977E20796472770BA41F4964107FE11F9412EA5846E512A7FFD42E71BF50DE6D8D86BBF01EC2A867006A0F881AE97104F2E476244A869C1FF895DF12FC04DB5BF2830191E1CF58CDED8EF7494C9E532282B36C6E72D1F961ECABE75CE5A572E30250E73CE74FA5A2D3C9E5DDB5DBA93865BAD0A219A3A8670D3EFFC7CA1119F383F36768CAD4B514E0644DF95D2E7EF768D487FB98C73EB489D79EB8849A96AB0E84B8B4C97464E7E1BE4F0CCA859BDDC3881DB30E333B68CFC90D8B472E577983544EBB38B729CA073FFA80DE085C861668B7843E3576BF89579A1B9FE0CC7884675A2530D5BCC38E88136A50BB28C491BB6579D789106315C91AF1F0465FF5853D1D1F9D762514523A80559A90DFBD682C4B0E1F522D855\",\n          \"k\": \"70D18ECFFEA01D8C2D4BA32516A042A925618FE4A3A69FF7B932361EAE5C6B47\",\n          \"m\": \"1D51A0CC52E85972001B77047D97DF5F47AE11FFC6C31B4AF42FB0791A3DB40F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 8,\n          \"deferred\": false,\n          \"ek\": \"28D9320FC6A5EC10059D3A531C364A99CA5A13CA98FCA773FE3B4EC6851E29EB8C0CC19440922CBF26859DAC0801FB0D0868B7C6C2917C51B7F644B4CE2C9C8AD9697D1102496B9B11B012105915C1A897065B05E6E7713740A391FC2B486249779864C74A1820F34C44063E47C87278C696DE5260270901190A5A618CAED0DB8B21D15C2D0552678833C1793070128DE0E43257413E99A1A58BC50323B0C722D69C36E82659410683CA4107C51D2D2126156C7357586EA553CD88CAC0E4530EA66B1FFBA700EF3A85259B5FABA131517CAAA0383028E125DC1670FF7C62D74775EC58A5B2B6074794225BAB8BEE7C14FE950AFDBC33B9A6CBE8492B3EC2BC16734EEADA20A45C07ADF81FE3401547F06603018BF5C4AD908AADCD9B5FA1D08244D922EE3AB1B3C693335AB64060369E328D31EBB3699BC3F390363610B2333A61BD67CB7DE4C6050C47D6D1402E7B997EC2C2E9B76AE5C77DF610A893183A1117747B3927BA7BB9B9ABB00E0B7E376C3310C3A63CBB03A2CC4BE1265CA35BC4D5C0CEB4187E9F686290472E9B0264B110C80F7A5D5C34678A746A5C4623CF8B54021A725F2C96E4923C2F531A7AC1405E275C44697A9C794625878F4CCB5D9DB9CF903C4F9DD180D0CB25D09AA389675A972223DBDA0435169C8652967C74C583223398F34E86D46DA0D026F5D661A4C4A85A85AD3DA358ACBA0B7E6C9C25D2CCB1A89B5D35A4F6516349CB1E983B793A04BC495311FF7433CFB8BE2722965308014D914014DA39322C0844F3C657804F9561274936B6556A639903808F8495F13C1138DBA65B573327490D8A772961B70B5C6CB4F8906B1B92639AE1AD25045CE5401AAE924491108B81044ED9E6B996A991AF83011B39021FB177FC8834A244B38C850C6FFCA4BD2323F896431FE64658B45299C280CF02894DFC03593135E172CF5B523734153E3CA24D160CCD64340AE6D021C21B8D5B98AEA0D9187F85AE1E5B0D9B06B1D1B12EC223B35FB2B07D81A42B11120A869C0B873831A41632CB0C1D819418647BBD92712920CFC2F24DC7871A194860105A6F0E10302C391E1DC2FA4BAA0C8576BC6E55F40A12DE2944202C00C192B497300E587946F1FACB\",\n          \"dk\": \"DBB9466138876E21BE1B9311F2D37C4BC447ACE23FED0137DF657CF6A396BAAB372552BB03B6AB73F6B534DCCC9CC9A40740BB0D833D64220340661BE56346C9A6A6B2E9787D5A588D1B8721218075048146F75434936CB948A8A66B5DB020C54B542D03BB4C0F352E0377B171AACD222259AEF33D2DD0A341C2BFF5BAC187391800363AF7EA21A6E535C6E226FC50661B85CF44BC1782E74963C4329D785C8661CBFA4B402CC9493A2042CA0743B156C11DDBCC2B1667EDD786C3C73C83C793A069ACC9F9CD353B94F88222A732575937B5BE0B23FAF5BD1BE1A958D77DEAA54C8AD5845E712241D92F8D1218E4070E527771495277FF0828E3C8BFFEC984832141A49264BF146FC9CC9676C59A8EA66C223888A3835932C39C7DDC15D2862C5EC54F125C799C804E6839309FD446C65A1285547662E79E54389030874A55EB6AC6D488E45863E6259D0473366E7A228EEC9A2BFB747378146010B78A712098E81F3FE0666A4669DC2B1BF9FB2BE6444AAC940427D764153575B82A143DCBB78B2097953731618A947F7057949A2B9890A43B702C052C61A5F35BA53825C02C1685A0ABCFD378F232A5251C38444B8F8349A860D67499B832271457940C8B437529D92A92B7676380628394244A8D55B4B845BBA7A674675289BEF6B1DCD532F42C6C9CE199D2AB0CF0AC4FDE7BA0B85266E540A651D6264B0A071C3C3DBBB0BBA579B587A0CEAA6C4E30305888C3BB9D073427173672063D8D5751BE6133996332846264DFC5092DA38246A74BE3A415895CC978C7A890961487650177EC32F6E30F5BFC928C2B36D7181D8F925266355A48247C4DB35E099363D303C157122F5606180C8AB08200CBBBD19B8240CDF7110C5F558F61565DD920213C60A906370D9D5820987AA2D0F00264221B5FCA7EB791A9075552FB015CFE3582B0A37F8D392D8FA3371531B9AEB54B07C0521CD9BD625669D26AB998E354CB1A5FE0D058FC866D1C67080FF99F1163917FFBCBA3D16D1071A895FC87967230A40BA46610AAB97169085700BE733346758E9B567C11C93C334145A37BB65F55CF09849128D9320FC6A5EC10059D3A531C364A99CA5A13CA98FCA773FE3B4EC6851E29EB8C0CC19440922CBF26859DAC0801FB0D0868B7C6C2917C51B7F644B4CE2C9C8AD9697D1102496B9B11B012105915C1A897065B05E6E7713740A391FC2B486249779864C74A1820F34C44063E47C87278C696DE5260270901190A5A618CAED0DB8B21D15C2D0552678833C1793070128DE0E43257413E99A1A58BC50323B0C722D69C36E82659410683CA4107C51D2D2126156C7357586EA553CD88CAC0E4530EA66B1FFBA700EF3A85259B5FABA131517CAAA0383028E125DC1670FF7C62D74775EC58A5B2B6074794225BAB8BEE7C14FE950AFDBC33B9A6CBE8492B3EC2BC16734EEADA20A45C07ADF81FE3401547F06603018BF5C4AD908AADCD9B5FA1D08244D922EE3AB1B3C693335AB64060369E328D31EBB3699BC3F390363610B2333A61BD67CB7DE4C6050C47D6D1402E7B997EC2C2E9B76AE5C77DF610A893183A1117747B3927BA7BB9B9ABB00E0B7E376C3310C3A63CBB03A2CC4BE1265CA35BC4D5C0CEB4187E9F686290472E9B0264B110C80F7A5D5C34678A746A5C4623CF8B54021A725F2C96E4923C2F531A7AC1405E275C44697A9C794625878F4CCB5D9DB9CF903C4F9DD180D0CB25D09AA389675A972223DBDA0435169C8652967C74C583223398F34E86D46DA0D026F5D661A4C4A85A85AD3DA358ACBA0B7E6C9C25D2CCB1A89B5D35A4F6516349CB1E983B793A04BC495311FF7433CFB8BE2722965308014D914014DA39322C0844F3C657804F9561274936B6556A639903808F8495F13C1138DBA65B573327490D8A772961B70B5C6CB4F8906B1B92639AE1AD25045CE5401AAE924491108B81044ED9E6B996A991AF83011B39021FB177FC8834A244B38C850C6FFCA4BD2323F896431FE64658B45299C280CF02894DFC03593135E172CF5B523734153E3CA24D160CCD64340AE6D021C21B8D5B98AEA0D9187F85AE1E5B0D9B06B1D1B12EC223B35FB2B07D81A42B11120A869C0B873831A41632CB0C1D819418647BBD92712920CFC2F24DC7871A194860105A6F0E10302C391E1DC2FA4BAA0C8576BC6E55F40A12DE2944202C00C192B497300E587946F1FACBFA704DBD0B4F1351219286AA8A868F5F17C01DAFF0AA77B36857D416B7CCD47EA078DF2CFDAAC3393EB22F912F4DD6B49366F5C33F3FFACBD766EA7DC8E2EA48\",\n          \"c\": \"B687F42683C3C4EC4D178FC0B437B20E0612D06B76E78D3F74CDD1A3FFC75D5CAD7271CBF01C65BFC917B214CFFE0041AD9E0ABAAAD326746159E02A81567075CD4EA0B3ADC31DFD8F7D85099E2C5E43CECC717C9D9AC27530EBF7FF76D529A499CE1DA92A15AF94261076A42696A24708C8314E9707D14969BC20F0FE15CD26BD53793A24220DC346526884027C2EB342C680DE9F6CCC816035B9263F8CA25F47E5FFBF564C08CCD4C2CBFD7A53C68BA6C8429093C0474D9840734838664C7250D1A19DCC381434BDDE0DCF8403E7C5FB4F79DD595DC601BAA787173F5946F9594379C2D81DCA8E460D46A19E5C6881607BB08DD66DAB954DC5650EB18ADCD3AD5C4E50DD88EB8CD224159748EE0921EBEBF569C91C0CA37151BFE3688049F791D7389E7E8356611E6FB2221C407F3AB2C8DAFEC6B7336BDF115BE3F2A6D22A852FFDFFC258DA596A1C760672708D16A0DEF4902538EA39FD8D34D79B43F45236D265DDE44B64AC3A6106652FA301F2A5E8AE8E5D181812EE0EF7039EB6C34E954E85568BC882F0AB4EAE260621FE45B79C2A71421A3CC73576439F9B15410A62DEDB1E1C1DAD45AEBD86C6B91E0C6600D28590BCF8DFCB5222890DC48AB7931136AA5793998C1C7C97267B460B5E7726EC03287BCE6A815A5CCB408E2C945BA6E0BA9539C7DA8182478F2F466661B5780FA99C875D9B0FBA379E43526B479B202313728EECE94B3EFDCA70696AC99EE56237C3E5665A4495AC4BAC8B9E2DC1386CB2FCAD904EA3BA78B9053E631D8F84B34BDAAA590D74705911E27D14B012BD85364E2CC2B92E11852B0AEFB3CE7082998C7AEF3B376AC05984091BBFD25697F4F1161C7379B84C8F0E84435D3023782BF65BB2DE49B32A7D432310C87AF0D79B1CB59D86EEB8EA100C17CF92EB85881E2A29D17363EE263F787D8BB054079161ABF904717024B40293B1E9064CA9937805BAC81B4ED9809557CFFBDB1E68F39E4176046769C85124A66A78671B9BB2B105560883521E2B000B423D5AA9D94945BC0480500BE1BD0F2F13214AC13189CCF95A6EC0E825389D4462AE9B7E7B\",\n          \"k\": \"82D886E17A88F82C66E8B1E7E329CC61EB0EA64EE63FA02676B362F8DFF29D51\",\n          \"m\": \"BC2D661E6283B835BAEE160D1448957AC2366DCD087176E252F81F1D11E28781\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 9,\n          \"deferred\": false,\n          \"ek\": \"70A72E79BA178F8951F762242A240036093906A0AC1F2501402779AC6315AE8A0BBDD47A8F347A7E8840269157C93A00CCE5A27CBB16AD9179798376BE8638FD97C976D3513F96B2F0542B78893644793185425864C7950E23102BBA9A4BC9B14F2B4981B673F8C55480FCB4B834490C9BCBCAC7566747A38777CAA5FCADFE9710D8627236E52D9B5463456586E4F963A7308040EA243E62A8EF87B418D92F95D41A57AC468ED5CD51A2B643D11E8F03AAB4F43F2C8160A89B5ACA63C7C9B59E2602458AB103AC58085EF7A98465884BF16A662342BAC721C1E59EBEBCB2CCB9A4FB8A3A650C52B612243D133F870613BF5A9F30095456247C34C779C899B6CD17A181903E0BD1A13AC73BD2848724F25EC2EC037852CD6A960E2A3509E982513FBA840D28BB3B732D76C5909B6612A6650548C709961368F8116236B6709AEC97F7C625094B93BE5B11E9217C8E845ACEF8ABE3742724969C74622C5CFA7776135CC2616B5079AE4535C39C1A3154866AD548C950052B88888761D846C6240C9B3BC0EECAA284ECC4499660DD2BC1B3959AB943612F35610EF1688C41105693061BF16D6E290BFD4941C0460FC173694538604CEC6774C9614EF16B08666ADD3916CFBB39B4BB75EFC166C7E8BF70AB8B872630AC554028E8C147BBB56D3BA312029A68300CC743B81BB35878B26892696F538334BB0A22DC91391D71CFDE723D9E813B2EB844B6A534280880C37B9421572A6B85AFB8AAAF5AF0BACEF356B7F32636B50B3336B7636ACD6C685ACB178AF9986BCB93BBF1C4C69AF1AB569B60B133A480D39B4BA319A6535CA92293D5082ABE76204C7420CEE02C1D372D0413B9F8368098104120D432CD184550667A976C9DDFE0916D423EA2221E64FA95B3F254F59359BE711087732B7D8336B99CB95C7BAC714742D0595BE953273E551D1122AF9B49495DD1B935255726A643D2F8A4EC9A6F573A97C8507FBE6754179A9439C33B0CA5BE3DBC9F19A18C3253A6830C6B69821C86E38A71CAB632E0A578F57A5377015FF43DEE85BF9C67591A1CCAF2C6917387C4B5193404C87AEED269310ABA894FCF51793DDA786F80CA209AE909B8147FAF316B06E4AD8C516BC83B\",\n          \"dk\": \"E2FA96EA25A100737368D8A9D7C3B3CE4C0085DC98854BB29306C45025C0CEFB64EFC64688D46485F8CEF9A81C8D4B53A3BC04DE05BA93EA006A760F55FC46FFE1B659A14FBB16AA00CC1EC3ECBD6C55CB64055F95B8308B690488B146669308C3C3B82D59736CA2A204F07D7984BAAC0109868C666D8644D4A29F0CB02F9C50853428A2FF773E7534989DA2567D044AD9A353628C107D9635637AC330C5A5ABFA1D548C6D552199746C0770B433848258FD185834C309DB32588E57250E206998C8783C177D8DEA3DDBE3473FA8688D7065E1BBB3F5EA79DE04AA4EC5AB16609FB3C4CFEF1659AC60B00765B38106A860506DF15C20E3D474546559822B91D5208CC03931B2C81F54122E9AC68115FA5F608CC0BFBBA7CA4211FF330154D591A48859F39B68BA36BE285B1147DC45FBCC5147A84C42315E4A412BA3D28D537C54AE295CF3400AD400807A38270578692DB112BE01359BC1CFBC001A6AC9AA070B024DD2C6E16B10C3987118F680389A34DC05606CAC4192599D2360125093905FF719A3792A1D83AAC0370A2286959C0B00A7371FF96B9035415DFCD6A0433B19D36800489C4636469BB195C4EA2563B2131B22AAB5059A30A4648E2A956535073E08376650024E52A72F1303BFD1393C7B7C2D320C74DDDB36EF035DDB3CB81F74ABCB2616EDB97FA96AAF8AC65311F790518636D89A80CA289FBFFB2B9E899A8169B0F855BA584263031C8CB5D7598AA43FBA3214FF490D56223168F6BE27557277734CEB673EFED604EF647990A27C0D8B5AB126649E3590A6F5588C7468AAFA7E4AE1C01BD96F66A645B88565310C3CF917B512C22A19589929D867D977B5D33A55DBE92C13F28FF92C6866CC4E75528CC7F5AC58FA4CE523435E08CAACA26CB71B9564C1B167BA48D9E724032368FFC29D8B15A695903210F2300B5415AA25A7034BC5E35B7C91670164DC5559185D489CCF9F168F4F175AB1F2B42249B95589474D8C9FC7EC1D15449C233A41F7A0CBE9284866A7B47170BFCCD2052F5838391A15DB0018F96CBAE3356356BB00B963ACA97A1E15757FCA358E63453F70A72E79BA178F8951F762242A240036093906A0AC1F2501402779AC6315AE8A0BBDD47A8F347A7E8840269157C93A00CCE5A27CBB16AD9179798376BE8638FD97C976D3513F96B2F0542B78893644793185425864C7950E23102BBA9A4BC9B14F2B4981B673F8C55480FCB4B834490C9BCBCAC7566747A38777CAA5FCADFE9710D8627236E52D9B5463456586E4F963A7308040EA243E62A8EF87B418D92F95D41A57AC468ED5CD51A2B643D11E8F03AAB4F43F2C8160A89B5ACA63C7C9B59E2602458AB103AC58085EF7A98465884BF16A662342BAC721C1E59EBEBCB2CCB9A4FB8A3A650C52B612243D133F870613BF5A9F30095456247C34C779C899B6CD17A181903E0BD1A13AC73BD2848724F25EC2EC037852CD6A960E2A3509E982513FBA840D28BB3B732D76C5909B6612A6650548C709961368F8116236B6709AEC97F7C625094B93BE5B11E9217C8E845ACEF8ABE3742724969C74622C5CFA7776135CC2616B5079AE4535C39C1A3154866AD548C950052B88888761D846C6240C9B3BC0EECAA284ECC4499660DD2BC1B3959AB943612F35610EF1688C41105693061BF16D6E290BFD4941C0460FC173694538604CEC6774C9614EF16B08666ADD3916CFBB39B4BB75EFC166C7E8BF70AB8B872630AC554028E8C147BBB56D3BA312029A68300CC743B81BB35878B26892696F538334BB0A22DC91391D71CFDE723D9E813B2EB844B6A534280880C37B9421572A6B85AFB8AAAF5AF0BACEF356B7F32636B50B3336B7636ACD6C685ACB178AF9986BCB93BBF1C4C69AF1AB569B60B133A480D39B4BA319A6535CA92293D5082ABE76204C7420CEE02C1D372D0413B9F8368098104120D432CD184550667A976C9DDFE0916D423EA2221E64FA95B3F254F59359BE711087732B7D8336B99CB95C7BAC714742D0595BE953273E551D1122AF9B49495DD1B935255726A643D2F8A4EC9A6F573A97C8507FBE6754179A9439C33B0CA5BE3DBC9F19A18C3253A6830C6B69821C86E38A71CAB632E0A578F57A5377015FF43DEE85BF9C67591A1CCAF2C6917387C4B5193404C87AEED269310ABA894FCF51793DDA786F80CA209AE909B8147FAF316B06E4AD8C516BC83BB3E7410628F44018D9DFC0EEDB18DCFAC7847E688013C039343B7FA08E0F9E191F64385D36D685D9D38D2A68F5825A84B881DECD0CE337355956C68C7F2B32EC\",\n          \"c\": \"FEBB296071C87A2541D8C0BBEF2F132BC433D608E04E65C035055494F9D3AEE01231784514801870A66357792C0F73238C18B99DEB53522AB3DE54A40EA37D24D62EB782187CCAE51E9DEBE131910ECABE37F312D6FAFCC8A5C1091C0C80769CDF6ADFC3A1C1F3F11DAEAAB65966885B193ABEB6D2B1A81082BB171713A983F073346E672D9F51ED6F1F1D71DDAC85B3A8188B37956709240C78D1EF276E6F534BFA98C52DBDD43E0506F665319506D11642110BA872A9DF8C197ADA9575980048639C930F29C9C45BCD7BE9774B49C2FBD7954ECBE0158D1B6911ED7FDB4EA3FA92F63BBBC34DAB800B2843B5BDC15B2EDECF6DC700A304B31C8E19049EE0371BC9A22E3F6B1C710BCC3AC662148FD9FD729DC3C339E17C4123EEBE60A36269AF28F8A81136379E76C35903C3E017B40E38F273D1B95238F71FB2D2FE6C880307762CB855C0C1951DD2C2779DCEC5285052D60CFCDE76C73B3E95F1D4868C491C71928A3DC04455F0B7F10564D4D65F358DE0AEF7C27D25E89B89E85F6A0B3C34C8AEAC06276F93E4E631EA6120F4E0130F1617891F67731075F6438DF717A4208E45DE930CDA28B737F902C3CC1592CDDF805FA269BC0DC98C40CB9DDD24AF71EAFC6B0B10C9EF2CE262F6D4A22F3C9FAF2553638DE522E5207570248FB87AE1C3DF5144F8EC2DBB4DC57F1C5F74D401D92D0F9E1D7AB98A6BE2090169FAC2C9FC9C6CFF726BD87C3FF2565052C85478FF53CE69EAE1700254AAA94125FA1B7236F4D9258987257B57988D8091AE2B0C06732C8C9FA35C2BC0896EE39825CB2C1B889EC496CE290DB565F403107E58F3DB1B2D40261EBB52492F11E3AEE9B755332B1A000595AF766AAA3D15116865BD3C6C1FDE48A149766BBE0381498B5B2BED28E4E8AA2C87FBA08D28AC8AE64ED47E8796D006169A90CEBDAA2DF63C8E809F169ABEAA9662349D740312B7ED2F26B7762352DDF8BCFF3E545DE5CAB29B5057086438944128DDA68C36C937ADA250A1826532231082E7CEEBB082C62E0BC2E1424D14FC40E057A1591886A6141C49EE309E97AF0C64D1F70FCF8BE9EBEF\",\n          \"k\": \"21CFA40FDE8834A21A9E419B7AD8B9E1F59B7CB184A0CC18932523CF45A1CA75\",\n          \"m\": \"6745F4F0730AE3F14A428A95C9CDFE82717EAA94F65B00A01566A4DCC9ED1E5E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 10,\n          \"deferred\": false,\n          \"ek\": \"5E101D0614A50B307E1AEABE717721690422A1A97C1D05713ED3B206D57919AAB32F6096A503C5C1C1BA332063489038FD1BC7F7B816F6F343274C19E01B1F9C088B311A0C8C09864BEA142BA2C829C5B74C988266BC9F12D37EEAD97FFD48AC5DF0C7C595A5626A23E20663E8F11A6BB66BEFF4A91D6C7CF551B34DD541D3DBA1F16BAB95312A60F9625DDC4FF1C6690E9B05FCC5CE05742D65D5915740AD9A7B60F9D098AA62A6DD3968DFAB5ED93A54E305596AC8B1E4967A5AAC15BB4B540C421571EA324FB422990C2B9B82A12915745C24BFF841731C6C07FE705CCC9B4C15EC42193472A4542ED5FC0B0CA98A91B7CDEF68A6E7ABBCD07B984795151D72365F31BFC76793FE485FDC663C6B7CC43F5A9D410BAAFFBA43CA968988337C9BF802CA221B3C366A22E66F703C695B178830473D7C372D338A4548E7C88A9C4837860419A2B60A3CA9AA19A21B570BD22CA228C019EC6420E77B340F32265BF86A666C9FFC75B64FAA22860B205DE33492B1720BE514BCD5A47496BDEC246D97ABC377430528940BA447579A90C9E18152F69BAD12CB7C51C48CAA8BAD1EEB2867F31691B32B75528056E56955BC4400A249550569C722410FA2573007BCD2B7331648AC9B424667582131473C2E8B11D1299DB6927384B257A81B6E7868AD00216C8548739EC83485CB330B03BE99DB38EDA338C6240828D7A5FF463B1C0C88092CBFEF3A2598993C72F922F2EBA31FEC317570B73DBC53C7F04810878FC9718552CA2C5B7220B6B7298886C94D9C8762A4BD05D8BDE7E9483850C0F75617686174D42CABA69B2968004992467D1CE078B03884F9E00B901473E7059195487C5E3BB0B938049A257C8046B92D129A906669BB1B92B1A83FAF951A9E8332A1833D665A720D0451801874036A7A5133318CA94015A63FEB52C40158644C3869F2E37BC18160C8ACA2381BB9B20B5A58961AF2E8593884ABB47A6D968A8C6BBB83CF089C6B75B6522C77B2B4064EFC50C2948A18C85B1436787C3A076547271F33B864D5CCC585B317193572428FEDD54B923973A11287B6527F68535DA629C904AFED0646115879DB4F48777D2CCDC3784E28834C7E503964FBD58C3652152D\",\n          \"dk\": \"09035BBB278F4BBB6709E427B18149B8D60E9697BA4DEB42DFF673B87682244B907B476132C8914F26045F1794ABF41234F829E4C38A3E1007FF6C74CDD639057185AF8A419B6C56129101D4A428CA92BB42481F23F5888C596ADF8A0F51BC42BBCB7D93305D072832251466D65C864B48794D126F6EFA34FC22592FA2A843D1AEAFC5B7B84CC982CA54B0C62A4C083EF545BB8B38B7F3C97BF9067277A5CB65B0618192091159200A321948415C387B7E4D65C7061331C65A29FC323E76E11E2811A76788B1B811BF7B17952820C077954A6F731A1209A4F5F68A2059C3E7969B01FB3A2FEB7EBE0C304F2C604DC0B1628A839478059674574DE82A35B0B55A22818399247DFA2363161EF981A2E2E117B3B1A61EA42ACA4A6030E5C139D663171867AA364E7DD904AC9BAFD69B480CD35D1A9964CA5C96E3D0660E10B51E805605968AF4747D1414B258190CEBC6C87B1C476AC9B48A0A8161F66E10BBA0D2FC619C415A0846346C9AABFD2B25773A40B3A242DE25004001CD409B8E5B99C0408ACCD8127355790E0D9B03A7AC3BF9F54E1B4466DA83CC0CC7920BF583DCA743568907AF8B381B4553EFA608204A86FD59A03B333F8E4049D3FC36E04A89EF2091169BCE577368043C20725861E7D76C68497D9EAC7F2838B0DF8CB44A829C1A89A7030A3D5BE81F480275B2DBCE88BB08435446EBEB5236C1A38ABA7B1F64304B289AE2073A414434F7521E44B8CBCCCC4784B16649F5B63812AA5C723B6C4024FA8BA361E44DBF21684FCC9FE99BAF2B71BE70AB358A867D85628FA4402A97B836713036AED2A8C14141424581917B6A88A48B9037CE5AD49A4B3CB86972BE77A83A773BB64E63B8AD552E97D9604F596E1B64A891B4526BA6B9C31226C7B491F0C529BC93A2E5651FC1F5C02F27A800683E52ECC087121D4A515864A8602E3432761A385D7355B20475804B98A193BF503326EE3163821677FBC371DBD76502398199D1577C576DD7128BE9E7621AA7BE650A1A4841262BE23A6946A4809875279A93107589C8821DDD23B0C0EBB16FB047364221E3C1671F32325575555E101D0614A50B307E1AEABE717721690422A1A97C1D05713ED3B206D57919AAB32F6096A503C5C1C1BA332063489038FD1BC7F7B816F6F343274C19E01B1F9C088B311A0C8C09864BEA142BA2C829C5B74C988266BC9F12D37EEAD97FFD48AC5DF0C7C595A5626A23E20663E8F11A6BB66BEFF4A91D6C7CF551B34DD541D3DBA1F16BAB95312A60F9625DDC4FF1C6690E9B05FCC5CE05742D65D5915740AD9A7B60F9D098AA62A6DD3968DFAB5ED93A54E305596AC8B1E4967A5AAC15BB4B540C421571EA324FB422990C2B9B82A12915745C24BFF841731C6C07FE705CCC9B4C15EC42193472A4542ED5FC0B0CA98A91B7CDEF68A6E7ABBCD07B984795151D72365F31BFC76793FE485FDC663C6B7CC43F5A9D410BAAFFBA43CA968988337C9BF802CA221B3C366A22E66F703C695B178830473D7C372D338A4548E7C88A9C4837860419A2B60A3CA9AA19A21B570BD22CA228C019EC6420E77B340F32265BF86A666C9FFC75B64FAA22860B205DE33492B1720BE514BCD5A47496BDEC246D97ABC377430528940BA447579A90C9E18152F69BAD12CB7C51C48CAA8BAD1EEB2867F31691B32B75528056E56955BC4400A249550569C722410FA2573007BCD2B7331648AC9B424667582131473C2E8B11D1299DB6927384B257A81B6E7868AD00216C8548739EC83485CB330B03BE99DB38EDA338C6240828D7A5FF463B1C0C88092CBFEF3A2598993C72F922F2EBA31FEC317570B73DBC53C7F04810878FC9718552CA2C5B7220B6B7298886C94D9C8762A4BD05D8BDE7E9483850C0F75617686174D42CABA69B2968004992467D1CE078B03884F9E00B901473E7059195487C5E3BB0B938049A257C8046B92D129A906669BB1B92B1A83FAF951A9E8332A1833D665A720D0451801874036A7A5133318CA94015A63FEB52C40158644C3869F2E37BC18160C8ACA2381BB9B20B5A58961AF2E8593884ABB47A6D968A8C6BBB83CF089C6B75B6522C77B2B4064EFC50C2948A18C85B1436787C3A076547271F33B864D5CCC585B317193572428FEDD54B923973A11287B6527F68535DA629C904AFED0646115879DB4F48777D2CCDC3784E28834C7E503964FBD58C3652152D01E6E2FFEC99716B96F8708E8702954EEF4142F1526CE74057D0049AF5D0376D8846E9C3DB8A50D814B91408C2FF732842F8D8DCAB2AA5CBD2848D44A65C056A\",\n          \"c\": \"8830427E2A9F37CCFBE39067C9D14B9404B83F9DE1BD9AF3E167D2053FD526F8534FB8960B8425CBA720065307602B8E89EB9810D7436CD44C4ADF87EB25F8B8F87865325383238931ABB418580D4774D645C71ECDDAC6B9F4EBAF3410DC142ECFDC357CB3521E62EBE0EF28BD41DF94A593374D8B9EF362D71A7D5AD6300E9C31514B5DE5AAB25E421646E152D5EE9A530F8BE6D0FF5D77DDB93827E525862437A9B6593ED284AFCC8453B409745DE7AB21FABC824307CEACF7D68D9E0EB54E69C98E3B94C61D9B0B84EAF064A966A7F99746EDC93F36DDF7826FB08C635891861FEE8D72A4FDE67F5BE139044BC775E73E7CB2695E24D81D84B2274461EC66E6A7E62571D306A667BB7C6F53AA1C3D403E2C6D48E03B29A164DB2AB7ACBB7F955F1E8CA6F836125B386453E047CB800F65656684FDD5BE79A8F12A2C90839B6EE89D73EFE016C09D878F16B92D62819B85E4275637305BBCD4FB25C578FB5CEDEFB3F9DB6165B5211623B2E53B53A71C5C2EF62A4255BB2E5AB6B9743353D0013760F89CF8A07140EC75D6BB8335DC3E1D2DC1393E43535119F3661F476168522CC25C7A702B58967113771FC6EC6B0F133DD349209C35012AA380819450487670359A906529490000F7B7179C8B6B44ED64C5700B190FD2B80E089F81E724B560E2F9479F8CA9C325B2D0E3873458E9B387BD1B2D84BF4CADF8924F55DC9C410871157B9999E0F580DEF7449F4CAF080028DED23F5437ADD8C3BF004268C9E6BAA21EF9F9C117A543E946D469A9FED47AE20524C3110D1F968A02A8C1DB24DE10316D5C2C0C28A10A043A1FC6393D7C0D6F2AA5DC379D64C1B870A1FA8D543FB17F0E5CF8F174208B370C6A4C44FA851CBA345EF09C70DCF5CE5412BC11A56E4FCC38A48D9BFF662DAFEBE105DDD686575DB01A1AA327A35E64B1DE9F55D1B3C6439E5A0396DC60A2BEF31D52ACEAB818B7068456FAA775F3F3D0BFDB4E78A3E3D38FF6162AC0AACEEFDB04474C93F071833BFC0DF73E0EB4B3A6AE04B87EB3490151A6A1DDE59BB286D449347ED0370929059D775E7909E8F35470DBDC6C\",\n          \"k\": \"CEC6DF7A0A9B79894EE00697CA123B88C4CF94EDAE8514C8A024498E909C72D9\",\n          \"m\": \"C3ED79224CB07A8D37DC9C789BC7AC8E278968E429087E5B2C0E878934DAA53F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 11,\n          \"deferred\": false,\n          \"ek\": \"F3007CA415887CB8294B07BEDD802012CAB18EAC1E58268C821BA9A63739E64AC4ECA1A1CFCC7E61E73E72214981ACC725B3040205134A998E3B0168F46901788054FB20C88EA08F8A96B1AA86CCF2D778E3363F4202CD8C655A14C13D8F954709F46FB78175DA1C839C612CC94984D33A40A2B3CE56C28302E9C849CB51F8C60B614924E21B374A381F473344847CA1E4C610A538645EBB7CF3E1894CB2AA2EF2884F1CBF7734426AA315BD52569E533D374A91EE6A8BF8E05393EBC1506BA0718C40465A7F24483D933926EBF49E9A65C56A1B25F1A122D03A14F150C62EF186EE39335DB1A99D2498F8C552F32568CF714C678943D74651AE345513A4B4D1460040831603874CB281CFF1F49A3FD43EC80724560136C07C3392D4B4493726AB25CC00585B1966B8E73246ABF05D78C43293670F18B02EC5D107A8518F014ABC1108B60B3781AAD6BB5152B228A5BAD5D81FBF0ABADFC1B89047C3EFFC77FA5ACF0FE88CFBD40A5270408F20601A981FA666B1FF9B6324A47D67DA9621F60042C593725A604CA25483714AEE23A38B01AE7EEC4BD2422D7231157533B819F554BB76BF26D9A2EFC10C758B584B1B663CB91023C1869E5283FE13144CC3149F6A2037D53008F720337A58534360765038AC222F97B9C7551004740A5F526597712504FB2B102906A35375943C011C8BC4B44323CBC690A5814A385CAAC581A8530CB201FDC902546C924E1A51B1154EA2D56C3D898394A722FB3B962102262B9A410B12AFDCE8250947BC13A88B40399E05B025A9770D6381206D895E2D319D6C2A237A26B06C7302B61C1288EBABCC439A2B8284B81407BE18674102AA5569A10461C8B1E550ABF41F5773C7EBC8B2E2B968EDD56800B9AD3D95057CCCBEF4CB143489C219A6570B9A4CE23B6044580F7AD2CD2597B8EB5300AFC63C4DA2A98E1C7ACB57C2D7A06E836781F5CB86BDB82AE4CB1577A0AE5540B4D23B8EB100B02966C440B68CAFF36801B99F38649D3D8A49DA61C719D196888481C798B76CB0BEC1AA037AE4271E04BE54075AA51569D700A95254A8CFF1874522935259AF76CC7555A056505D0ED973B075E185A37AE9EC366F52023FE381ED83FB42486B\",\n          \"dk\": \"8B177D624AC1BC09657FF155207B487BE10A043371240026BB1578A6B96017F0BD124038F96422C1898C85C2B5ACA61292D39C1E4A472E99B5EA32631364C6B509A54CF0810FAA061F668199F32673EC6BCDE700894236B64B1F01A4A9599529E6950748830E3E64B9C1E12EAFD26147A86D585C35F56BB1B2689B30614185DC9E2EB96A72FA560EBBA991C36BD5D5B75CF2A7E217CB07AC500FCA1314470B99977248475BA90504EFD64D9BE624C3C13FF9DB9BF3E85EC9B70536451C2D2C993455C8F1061C53A404328788A80877D6BB2CD2432375B37D9DA1146A0904F7263D7E01429DC1767274A654699E862C4064F6C165C6440B3A9393315E8B74493A0C430DEA414C5AA1166B47CD611DD072A0E78A64E191B8C369809F644603C2C92527B3328B9C4C491CB92364E0B632A1F86052302DF0CC196D259828113977347AB59528C91409C2B15E9262966B6980A4FC3A472318FDA787A94B52D9499076D118D02259DC5A6A16D3B90EDB5AA88C40315C703EA52A4B227A2D7103C2C0BF0EB455442A692B3BB9DE56B6528B8DCF383569610872C9469B21BE123C7B30B251310BAD364272C4D7691514755C149EDB752A67854A7830A2B5E6C7913549A4B78709007F30178C832BB9BEB8072FE42B96AC73C17A55ED10CFAB0690AEE71739A8828EF27D3B356156461A9E406E864A931906AF731A241B8B55D344B7ADF93C0477B4A8EC41E82457798666DFB96BAE33876E384CFE3AAB021622599078F15CAD6FD5A7829BC87D20595A5430BFC6AB2B138D54BC36A6B58BE1148906C2566C5347BE340C71949DC849A0F38081DC0B7B7CAC8DBBA5341EC61A1DD889D262BDA734A0EE691063D0CB08045414E37548195C6CA41B1F69BE334638582441E3A3CAF965962277466CC1A5056608B72C5B6232B2901A93D615CBADB57F2265917A330EC8208BAF046BA2960B80E502B4F48ACCA6AA972528D3504C1696C6D4D116325361CEB15F9CEAC560019C3B3A8F374397DD17AE23588D40779473684DE24768EB628D38F336C075A2CD56402783863FA2827AE03B74A2B3B0C4AE36300FF3007CA415887CB8294B07BEDD802012CAB18EAC1E58268C821BA9A63739E64AC4ECA1A1CFCC7E61E73E72214981ACC725B3040205134A998E3B0168F46901788054FB20C88EA08F8A96B1AA86CCF2D778E3363F4202CD8C655A14C13D8F954709F46FB78175DA1C839C612CC94984D33A40A2B3CE56C28302E9C849CB51F8C60B614924E21B374A381F473344847CA1E4C610A538645EBB7CF3E1894CB2AA2EF2884F1CBF7734426AA315BD52569E533D374A91EE6A8BF8E05393EBC1506BA0718C40465A7F24483D933926EBF49E9A65C56A1B25F1A122D03A14F150C62EF186EE39335DB1A99D2498F8C552F32568CF714C678943D74651AE345513A4B4D1460040831603874CB281CFF1F49A3FD43EC80724560136C07C3392D4B4493726AB25CC00585B1966B8E73246ABF05D78C43293670F18B02EC5D107A8518F014ABC1108B60B3781AAD6BB5152B228A5BAD5D81FBF0ABADFC1B89047C3EFFC77FA5ACF0FE88CFBD40A5270408F20601A981FA666B1FF9B6324A47D67DA9621F60042C593725A604CA25483714AEE23A38B01AE7EEC4BD2422D7231157533B819F554BB76BF26D9A2EFC10C758B584B1B663CB91023C1869E5283FE13144CC3149F6A2037D53008F720337A58534360765038AC222F97B9C7551004740A5F526597712504FB2B102906A35375943C011C8BC4B44323CBC690A5814A385CAAC581A8530CB201FDC902546C924E1A51B1154EA2D56C3D898394A722FB3B962102262B9A410B12AFDCE8250947BC13A88B40399E05B025A9770D6381206D895E2D319D6C2A237A26B06C7302B61C1288EBABCC439A2B8284B81407BE18674102AA5569A10461C8B1E550ABF41F5773C7EBC8B2E2B968EDD56800B9AD3D95057CCCBEF4CB143489C219A6570B9A4CE23B6044580F7AD2CD2597B8EB5300AFC63C4DA2A98E1C7ACB57C2D7A06E836781F5CB86BDB82AE4CB1577A0AE5540B4D23B8EB100B02966C440B68CAFF36801B99F38649D3D8A49DA61C719D196888481C798B76CB0BEC1AA037AE4271E04BE54075AA51569D700A95254A8CFF1874522935259AF76CC7555A056505D0ED973B075E185A37AE9EC366F52023FE381ED83FB42486B5FA88098305912B30A55D51412219D3B2A6271BD46046F454AA4AE238431115880FCD17FAB3E190E96CE2AB5E42ADCEE8E516644801B0C0D42BA08B82F5E6E9A\",\n          \"c\": \"128FEFB85AF81CD2D9BE101E5B2C6D4D10C43C870A5E180D9E811541F16875B9D1D4842CD6B2A9555D16C7C47A1A30647BECC8628788194ACA88048B291A3A83CB5D4346C5D741CAA1AE631B59020795049046BA09C262D50896BC4D390F4963970FECB91DF2DE283EAD7CBA46F8DEF0AF5C9819B3F76B7EA1C653584911809310ECF9CB171F1B0C83F147D70996F57B18D0D7BF596983017E02AA7B465210B5BF402444167831D409D2D9A7CE9C24D3DC6CE7A3F71DD0E7F13F66214B29753D625A7874D4606B3688D8FDFDF0459034C4B61794EB476D02C375DE54E543F4C5CE160D0764AD5F001B4CAC7FEFE69B06B5DD4188D6A75DA0EFE81C8BF2B378F888BBD41F9976B56CEE9B6A30AC1F7DAED843FF1A6C209CBA6AB8CFA42E0270817C7CD1A8EE1D8E5552A5771A95B0C621666AAB4738897A5C35F54618D41E4BE592EB6E530228B21A09D56A86039DADA8A8D530E7D95658CF9C3AD3E2476FA037B38F8730EBA96423F5CDF884E9F707B18326A9BC9EE51072AEF8096B9D2CA9D2347E4981AE99ABAB9A2DEE0DBFE2AEE8BBFF5F2EBACE1899089B2AF44318F1530E2DB95F6A6004BEA7BA1643801C2384E254A4E42372E74B30CDAABA3A5A7868A43A91F58503C7DF9FEA920D8C29EECFCCD6D42332D2DF6E2A689865BA65A03B65F0E9338BCCE725BB3E50B28FBCF0F194E24D7EBF89FE4B7B546014667962D92FB7F33681110958A6F5AA0129717FE505C5EB2A009E641FFA73AAC6F214F9B75EA658D012FC638D7C607D6C8292140D856BC2FF86E5DCA2B357C6C92934E342C84AB22374E2C65C5071F1A29E21A3D346A5F4F2B6EDFC1985CEDAFD9F62BC44B07C42E4C34B24450FA07394FE067804775F98846E5F72977CA6B58484D2EC6A5634B2C11485DA0D4AC1F96226EC3920A3FCE229E801F2C9F175C56DC03059B00154A7540CBC4A538B700DD948C19EAF88EF6C206B5F58EB6538DEE0C87E132C62086F38C8F1E9A799E845E2A7472DD393A3FD455617A9C687C1503ED17D8E01C992F503A699249B81BCBA9A9F7606217BAFABAC90995A85DF6663A7BB6370DD\",\n          \"k\": \"9015AB2A00F4E86BC82E6B3F5208D45BA0A725876A9E19D52C9A43332554D3CB\",\n          \"m\": \"41C74E66327238C6F7B2ED2683FC5E88CC35083512BC285CCB7165499F34A0B8\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 12,\n          \"deferred\": false,\n          \"ek\": \"A4F9A7CDCC7E700196FFF92085458D7D6855DCEBC5D98260E4C32C94DCAA47606619C6C02752333D30BCB0E82909D7657FF199668B8824C7B323A50D51203E97A944467930FEDC000C69496751C3C69A4FA4492CA6454665E56C9B677845D6CEF70A21DC7028C4D487629B9738C300D17995F839B59793A0A8813BF9012E0139BDD0EA31582BA31E070BC7F2B44FE57CF5F36CB9D92C6926B271C290CA44BC3068C2981C6CC7932A82E69C1AA9616D99C36CF37D8DE7642F2A0A615B6B85660224ECA4AA6B4779A20A2DC93BF31BB25582705B56B04FC1898CAB5FF97B6934158B4BD9994B4A1643F8A185483F93AB349536A29A3445250575CFE07122021AEB450DF71943E0818B664951137880AA5586C66260B835BAC540378374869084B8E3CC91B9C12248E2A75C060EFB6925C35A3DF34BB5A8E76B4FC4706DA00C2C0331502A3B4030A8CA25AC07EA139FB8B4733957784551208662250B542FFB6B6985A130C109FEF00E30D6C238FC0A10E567737B8776B94D3FEAC0FD9B055FEC78BBC4961015033CF78805872F1F9899F69492AF6A35180C8F8393071BF020F746506FA50E5AE7A675542610F883B24A103A32CC407933CC2160FBEB0375A514A2D405992BCC8FCB82F3799365D238E02C1886B9499CA2C90D8B33FF32AF51F5933F721FAD5CBF78EC78802C7DD06777DB8C0FA3C0CF09790F65630E772177D4D9A35718ADEA8758057601C249C92876799167604664BFAA93537572A6A1425BC0AC53C85AC64DD9A7BE436F110C3AA68BA0552994DC019F9AD80B2767B879C8B7D4664AFA6C937276AB40417AFDC2895FA756909864C4E1867A537B42E271C86953B16279843C88DB9385EFF67FCB8A6BA13025B259398F149B49E1A4A9271CD7C1B5F0C6BD7AEA36CC6739C2660E262A3189E9AD342215FB5A95DB2B179CA763A8AC500EF59A4A243F6E6C96C278426D472161C1069A31992ED9AF9B88275D21815F1584CE7381D487457CC15E04E13FB4B93F309588D238BA0B59867680585C043857516395BACC287491B0E65B1C175B41045F2B94202F888B103A6683BA958FFE411135C3552BD546F9E3AAECB8C783AB074E809056545A8F7B89E7BD8DF0\",\n          \"dk\": \"5260A5E1F6990DCC65328126BAE9BABC0B28E7010A2F9244362218C5E9074A331179B16A07676727966476FBBFACA90119722A168C6CD5A1619A0548C15144FB408C1099BEE94C25DCB119EC6A677F3B0D0D705BF8B7047C564BA5C1843378903CA64D1EE7357D0A2102865417865CC97C93EA5362B4BACB5E5604ADE8CF16E6A70F455A91D104E98C9D63273476C067AAF45B8739099C527B7ABBA56E9B1D939C8A776C4931A14B9B34907EB309FED94406145274738D7C061F5F7038CC2AB9AF6243C913A3CE212ADD4B983FB646A7732BF1B30BF2759CB52936C325CF475C47A6A54979BA2A17D4066D52A728BA6198CB52762829D85C56F0DC1B4352A191AAACE6CB1C26A348E45A25BE0C86A66B16DEF9AC236C2C03BA883CC80908F8586501C5C8D02F0EFAB2D9924CBC2B2935CC73D946A8A370362FAB0C8D94891D980CC8665290B7471D5477902B8A83A0C5B5B2A8BBA4622E272AA9FCA8AF59C40E109C682AAE7BA0C61B49C127629AB0628E402828E7246219AC368063660B1910C7D10CC5992501841612C44F4056B319044721EC9550E235BBFC6D384A9013C52C61257BB9D315F8C383DA7C66A4690C29930F6912665BB494CE63A96185B4DA193C1D58CD79A312ED57BD4E4A86FDBA31D87995C9409A6D03924D6C35145BB0233BBDE8A717E2291F673903839047C23B401FA680EFFA41869664F7F3612C552B01C9CB05D518E6010A744CB5756A31A50BB965AB3ACD62781955295D7C4A07FA13EF911DE93AA477952D99CBCCB3A83FB0E50046917506352B34158E8E451AFFCB9E31F7B800D00D23CA1A927C4AD9B57E0DC7B69EE270A063B3B286ABDAA22C946AC6C436233C6979C3570BB5889F39264017A63E09FB54547C5711B0B6C8576A8F1C8B013A0EF8190795F42E376C803EA4A8908605C8F51E07C3983A357423D594D79080C71AB3EF930901D31D6539328BA5B18725BD47E6AE466C2891F4C889426D5BD267A9B696EC4739C5F978619C786333472EF241353688A02C1154D4650C0C5E22C61AB77B0FE1D193573760720CB2E633868B0B114DD8CD03316DA4F9A7CDCC7E700196FFF92085458D7D6855DCEBC5D98260E4C32C94DCAA47606619C6C02752333D30BCB0E82909D7657FF199668B8824C7B323A50D51203E97A944467930FEDC000C69496751C3C69A4FA4492CA6454665E56C9B677845D6CEF70A21DC7028C4D487629B9738C300D17995F839B59793A0A8813BF9012E0139BDD0EA31582BA31E070BC7F2B44FE57CF5F36CB9D92C6926B271C290CA44BC3068C2981C6CC7932A82E69C1AA9616D99C36CF37D8DE7642F2A0A615B6B85660224ECA4AA6B4779A20A2DC93BF31BB25582705B56B04FC1898CAB5FF97B6934158B4BD9994B4A1643F8A185483F93AB349536A29A3445250575CFE07122021AEB450DF71943E0818B664951137880AA5586C66260B835BAC540378374869084B8E3CC91B9C12248E2A75C060EFB6925C35A3DF34BB5A8E76B4FC4706DA00C2C0331502A3B4030A8CA25AC07EA139FB8B4733957784551208662250B542FFB6B6985A130C109FEF00E30D6C238FC0A10E567737B8776B94D3FEAC0FD9B055FEC78BBC4961015033CF78805872F1F9899F69492AF6A35180C8F8393071BF020F746506FA50E5AE7A675542610F883B24A103A32CC407933CC2160FBEB0375A514A2D405992BCC8FCB82F3799365D238E02C1886B9499CA2C90D8B33FF32AF51F5933F721FAD5CBF78EC78802C7DD06777DB8C0FA3C0CF09790F65630E772177D4D9A35718ADEA8758057601C249C92876799167604664BFAA93537572A6A1425BC0AC53C85AC64DD9A7BE436F110C3AA68BA0552994DC019F9AD80B2767B879C8B7D4664AFA6C937276AB40417AFDC2895FA756909864C4E1867A537B42E271C86953B16279843C88DB9385EFF67FCB8A6BA13025B259398F149B49E1A4A9271CD7C1B5F0C6BD7AEA36CC6739C2660E262A3189E9AD342215FB5A95DB2B179CA763A8AC500EF59A4A243F6E6C96C278426D472161C1069A31992ED9AF9B88275D21815F1584CE7381D487457CC15E04E13FB4B93F309588D238BA0B59867680585C043857516395BACC287491B0E65B1C175B41045F2B94202F888B103A6683BA958FFE411135C3552BD546F9E3AAECB8C783AB074E809056545A8F7B89E7BD8DF0AE9CB398180B4EFE7B808B5881B8F0E5F9A8C23F7FF068DF3BD63457D3B48469DFD461BAA311495C347EFC0C40ACCA288BED6D4DBCF3BEA45D5AFCB6E7FFBC2D\",\n          \"c\": \"D9B7CFCCD8D7790A264374AD1ACF09AAAEEEF36B2AE84D657C05C697901FCC6C6B6F31BE49D729E31FBE760A93D9BF54D0FC37B81F6240D3BCBD911142EE7C330A570CED051BA7DE20810F59D6A2BB0B00F7525F071EDFB8B9DCAD854C70FD454784EB8F68638A1880D468FEA90EF517EA77594B53E901A2BD3FE2BA66F69B6F644FA0556D43FD799145B389CDDCEBAFB1A84B9C6F34231D0028584A8FFD70E69E1C84F33884ED6D95793803281561ADABF1EEEE72C22790558F3A6B7F0A54FCB96BFAB67314951158CE54880D201E9E8B0E76A47DB6FE8E7C767A4F604ACA6A598A25233440687ACEE588E5085B7A28C09E01E4906F3D834938833F165CD6FDAB1524F7FAA64C0B44C1691DA39FE88C19548F9D3ED4EAB68E853CAE954C7749AD6C55383E254E7FFC9662D500AFB1FF1A6A0312D7FDA9606C9E3665C46D0F7DA6C3B3F61EFB25DF574126D3843FFFE720651A06ABF241A68702B7B9A07648AD17E5238FD29D1CCF781605FF482857F1B10E36CE1BCFDEC8D8A0AEFFE0643E65E1BF0B060FCDC5C591CA15B645B701D33D1FB4ADEA2D13562D73CF68361BED92FF108BCC5C31B8E9AA913C112EF54C529BE6A4D2CC64808DFB5CD5EA8499007FA9F156CFA686248FD7232E797E4944E433FCE98B3864B46751D2C55FBC1C4C71FE96A875CBEA1F47F1D6A3C98E80876780B95936EB0368CD56284B211E670BE4ABB5536E6F9C7BB1D2A23C04705BA1D851408A2C566E9893B5C9EC60245EA2174016096B9FB8E8476A94E174E7CD68C66AA805512A5D851ADB17A99B49C33754BAA091E834A09C90880C95032D385458BF514A3B88C67FBA9E93317998AB39F712A5C38F1BB51BD9FDC0B538189580DFDAB817341145752F840FB4207EDB939855745977A65B27642F7C28C91FA7D78075ADB813D896DBBD57DF60EEB46A9C00E07F1F63867978B61AE357F695A1E2D415496773CB52258E94012527DD1FBD35B0A239A48894C4F54FD2606C9B5919BDD52C671D9FD169D8F4C6FE9D01E19B358A84876D303AF979222BAB23CEECE34209D8C1F890091B547374FEFEA7E5A6B8\",\n          \"k\": \"6D339C7DE13DA2BC3F672AEC4DDE931C811FAA91A8E91182DE4F94F2009EF16B\",\n          \"m\": \"6DB6A3F134471A89ABEC3384BB48A3C405DD3B2A5EF53821A3C1EA74DD562799\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 13,\n          \"deferred\": false,\n          \"ek\": \"34166E98297165A69FE3818A3857B7D6E97180D938B8B56ADE8C361F010DA4E933F85A911214465BA7A0432AB99A519BDC81647CF72C4498A220C5B5CFD28566F4AE2CDB025E6270668153AE8B54C22BA5270A1DDB5660984A9812C32E6AF44385C494D55013814900FDC0CCECD7963D9945C4A7329FD5038F382181507D43A96402A973EF683499854D29BA964A530763A09DFCF4B97EF549E5A60C1EC921F8A2C9745B364316784AB27D96F8A9980727E5AB1FEC832DCD023D1774807BAB6D02E898EB0CCDC0EA26DCD16518089BAFE96AE395348481427788794D357B59E4421355656318B3984623C472A01C215CC13B74A3A154FBFC4DE4877ACE367B20397F6FD29E757354E6542A31E48E01B43EFED71BC6AA3540D3093E5794D889B74CF73C6096AD1CE46F4D5A912BB77C10C8CD2F92A6A952B7EE647D7E73C73185942CC331AABC3CAEAB4E0F86776DE4B5EC53B571B818B6F9BB31937EEBFA22C6E9B486E0CBC8658CF2537B6128C939A477E5376C59024440C56E853994FC588EE3D883B3A369D6CA24F228412140BEE37C11E8718831B24C91C46A54D09F75B99015A92AC7049697260652A4288E49A25F8C223709A035F36DAED2749DF387F6C53DB022535E729A10C8542D84B79AC5B44029BAAA360EAFC90CDCC754835A207378309045B7DAF6B8C5B6462A3B777150BA65C73B68F75089E79F1E820889A7705F4B6C7E404436F68DE4B50F0A082C0A8258E80B22646C29E7C732D9489AEFC11AC788593EA8C865970F83C3957829A818D1734DA277871493B45B93DBFBCDF0366C3635C0247358B1C7B8EF81B4B2EC8B4AFAC90E1983FB1C70121691E5913AC3B2689EB28773179E7225788FC319490393149A352EEC56732BB61B1C086CAC906D422D05917F3F1C6F3A57A7B61BAF9A852A8859B5B4F183B36715E578C63DFC347F86005FA11B407494AC4B19A7FABF59D10A34C58C6308768CF88BA463AE8BC243405277AC935D1C5238335B47066910917183444C79E959AAB64915C9609A5A12461F9125062C2536B73C5A5C1002269B0C023E7228AD1084918A247167D0CA5D87F83ED7B3EF523CA41BB22FA002ADD4DCDB3E7B68C892797481BC0B\",\n          \"dk\": \"BE01752AEA3528793D84B87D34010B58DA56FA4219F62A2BF66530B4115EA494A0AEC432AB2A2F6624C984E522BBEB6B34B1329D1501BF41B48CE820D99B3476413A87FC1055EA373BF32DCEE2573F13B14AA43A441BB2FBD97CB863B1D2CB0B60078552E8941F7A9A4703C6ED6621CB055C5C2CA4BE869887140746131CEDD20FCE9C38AE2221E49C384D76A9CD6141EAB46CDEE4B8F9EB734C0A698A038E1F14C4EB92A526711EE058502F54005DF57A443C11806496F14756687136F44B80FAB94E5A145F6A1A8224886DECB1B5C872202190106A95C588C0BC8ABA28EA5BB4C3D39330D17FA78098E1BA7550EA3CE47516F91801D01BC09AB8C9897495DCE68E45366A8BE5962612A4B0E6943DA72EBC87270A64B083225C2D408EDB1596B0FAB8BE0540098B0BF32C18B7401AE1F65C14F4C81337AB8F64A4A7818127AC10195C929C51B85001498D9CA7BB5A13D2FA515CDA75D6E795611B1ABE18981B972D3086136E5841EADCC52C2162B1461250C9111A3016B3B78BADDC6F59E4BEDF989DE0DC03D1555B98CAB2CDD150ACE44B241B6C4938002EE610910A22126B94937A9A7DB1BCB2079DB81B0BAF0A2A1AA80D609B76381888E0FA05329C49C379BA71300A9171B0227B442C610FDC2995C9778CE990B3C1DA83F3C8C1F2C499A1B9A1D936538C9C61B71C83524747C4775FADC055F08AC79622B4CFAC6996C5602C992713607F2D59621A9B1BCE67517A26BFFD636922362E96DA43B5C72BB0697F24167DF97B724971AA5C8B5011C02DE7857F761154E24B6D1741C49616BD17E360A9586D612589FA81625A5C55083092550C8F69B883E6C1510A122C94860EA277274BE1028353219F7B0CFE837B59D895F0A0597BB1AB93D39C6EEA6FE41A70BA395B97FA20B7161437543CBD107BA552CC13BA389F985622C5B4BE7C7F600A6C45196DF7B66BC1053C5A4C7EE2027B49E6ADBC100996717936C89B1F878ED03C4254E84FCD3251129A078749AFB1713635A83FAAE1C201F1012D68940BFB0B44BB87D43344FBC1C2F58B29B2943B148BCD9D4C368837B0BB6A0AEF083134166E98297165A69FE3818A3857B7D6E97180D938B8B56ADE8C361F010DA4E933F85A911214465BA7A0432AB99A519BDC81647CF72C4498A220C5B5CFD28566F4AE2CDB025E6270668153AE8B54C22BA5270A1DDB5660984A9812C32E6AF44385C494D55013814900FDC0CCECD7963D9945C4A7329FD5038F382181507D43A96402A973EF683499854D29BA964A530763A09DFCF4B97EF549E5A60C1EC921F8A2C9745B364316784AB27D96F8A9980727E5AB1FEC832DCD023D1774807BAB6D02E898EB0CCDC0EA26DCD16518089BAFE96AE395348481427788794D357B59E4421355656318B3984623C472A01C215CC13B74A3A154FBFC4DE4877ACE367B20397F6FD29E757354E6542A31E48E01B43EFED71BC6AA3540D3093E5794D889B74CF73C6096AD1CE46F4D5A912BB77C10C8CD2F92A6A952B7EE647D7E73C73185942CC331AABC3CAEAB4E0F86776DE4B5EC53B571B818B6F9BB31937EEBFA22C6E9B486E0CBC8658CF2537B6128C939A477E5376C59024440C56E853994FC588EE3D883B3A369D6CA24F228412140BEE37C11E8718831B24C91C46A54D09F75B99015A92AC7049697260652A4288E49A25F8C223709A035F36DAED2749DF387F6C53DB022535E729A10C8542D84B79AC5B44029BAAA360EAFC90CDCC754835A207378309045B7DAF6B8C5B6462A3B777150BA65C73B68F75089E79F1E820889A7705F4B6C7E404436F68DE4B50F0A082C0A8258E80B22646C29E7C732D9489AEFC11AC788593EA8C865970F83C3957829A818D1734DA277871493B45B93DBFBCDF0366C3635C0247358B1C7B8EF81B4B2EC8B4AFAC90E1983FB1C70121691E5913AC3B2689EB28773179E7225788FC319490393149A352EEC56732BB61B1C086CAC906D422D05917F3F1C6F3A57A7B61BAF9A852A8859B5B4F183B36715E578C63DFC347F86005FA11B407494AC4B19A7FABF59D10A34C58C6308768CF88BA463AE8BC243405277AC935D1C5238335B47066910917183444C79E959AAB64915C9609A5A12461F9125062C2536B73C5A5C1002269B0C023E7228AD1084918A247167D0CA5D87F83ED7B3EF523CA41BB22FA002ADD4DCDB3E7B68C892797481BC0B25939AD5E8DF2448392861CD66369376BE1E6828D87503F46841BB7682A42BA34940BEAD249B04A55DC051633480E518638E7792F57535B3FAC26F0A535A9494\",\n          \"c\": \"3BCD7972030D4F3414C2D151C52BDB8F96500ADB92F89A721A305EA938987F4B0314F093FAE503D8375C134046365443E3E000E19984777AE9189169E20AEC928F3DF3E1CCD2963BAEF94436E3D8116721413C7254F90208C788644A3AD90AECA2814526EEE017E07E222ED0987E5693C2C4EBD524F2B79772B974FD738C59D18FDC9E091F32351F86C57F57A21BE5706C6394D06253FB4526FEB48ACD18668324B7E662E5909CD76F160FE8C562975789F6C7290D1BD167E647FA2FA61FC753D5AA6FF7C62BCF3D7144D3EC02AFCB3E162C3D47F268D78F08FD3B621F66970C9A2A95C003092C3246DFDB1104AB31FAF7FC140D7AA39AF34F429C51041AE7BEDC26608A8BF52D43901BE92E65DCB87B832442ABC64A9F61745F70596A148D3EB7E40E7C8A49B24155BDE63635FD26FDB6458145D06FBA000E577073A407B36D4CD898A312285871487B50B25589D39BB453521F8436DB251710CA8F6F3E5D6EEF56F52291F7AC3DB7520E03DD95058C5CC4AE39E35FCAEC9C7E0284A9483C09D473EA173BAED7BAE5E6397F128C872469CB092A65FD1D2CEDA8E659CA97E7781EDDE6EDA94E68746182FB5A44BF7951C4768F66532445F577950642756BB1FD08448128CAE0D819BDB41DA547914CE892963F64C609C44170AED7918B3192EFCAB9AFB493CEEB327A4A6D21F7FDA7ABAEFED12FCE2F180C8A01FB905482B8BD65859CBED2FB8D13C65CFF497D8C9E0621DFCF8ABA62FB0FF0DE460C04313127031FF4883E9077A4A3FFF4D21740E02563F9595E2DB7B5867A7D5AAC7D7CC2E6206B9DD07CA8D2743F69D3FD0D5C00EB16E55827EE917205816DB1C6ED1BEECD4D529C9A1FA1C9115312B3C9392790BDBDE5EAE4078C90D7CA55CDC4021EEE7D488949AFCC05F2D7F4AD5505B3613983A87ED316B0D16443CBACD8206B593D86ED37B4C884B7C1124B74F30C6F2FEC61AA6EB96CA206EED58164F87D5849814F793FC54BFE5D5AF81E497F60E3C1CBBE2FA1FA8A602A4A36F60A567E9605CD439A2096B2EB051F3F9DD901BF119E172F52D617B6E5EC6422B0B05C030C20649C\",\n          \"k\": \"63AC7D8750E143131B3FE26C0FD5484F5D60DC8D22B542EBFF0D5D8B54F34EEF\",\n          \"m\": \"121DC782B740EAE666E709EA6E3CC6CEB8EAD204CD7D85D2256839E98CA57003\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 14,\n          \"deferred\": false,\n          \"ek\": \"F3BACC6A8C9D390141981325C2DBA6EEF3805950199468661ED4BA300174FCE35411963B2B07599E14524446892603BC300AA3A25B45B41A857D50C06595B40B80159A477D8AD628F455038960BB23B0BC32D90D48E014BBF0AC12F93F17E36976A8BFD1E7B682212815A18B10B5301E0B88A7C17C850AAEC549B98195532C9323559591F9DA52703CA2EF921B1DA82D46A5B522F69109AC83351C46BBC375B61A8E7FEB0161423D5246362DBC8C866A899FE42F3C3C52EF82AF2CD05C11913B87E67B060A2200BB60D8B94E58C1B609DC9FE36A147B5C83CBFAB3C5F59F2032307E5745B7C68E324CA91EC987AF771D9D774B49E18E1F828E3D17641C51CFAAC9450D3A4404141187546AEA64939D94B884868876C86348E27DB0F66BD1A06ABD331B8999A446370982EB25979C35733791A49608D412500E9A144036970C086F15AC90F1699CA79928416858389908F1300356B9C382C94A2AD5491E86C51905BC398CA41FCBB05951A54A3681C806CC4868266E100EC2A334DDEAB98BCC5699E192AEE81DAB58C5C3D24C564C9821C7C0F981A500D91E66108C52013134B9440591AAC5A9BDC6208B22BA88F269B823DC08BD0C4067B7BDE01B5B12985CFA30C9172C9A2D95A33D4BC4EC181F17CA7A294989F0306FE2A7C7F16BCF301752BDE3CD3F2CB81460C0E3B0373EB8B79FD84B05A783E14454A2F2ABC693AF0AB78A0095A3695158A2B6783D32BFF25A4F6EBC4AB5010C0E394437D208ED001CB3B2C92A794607A6BC5EB580E7D52342B004B3A17E20FB70A4C5C2B8CB39C9E265E012C7D03540E7B47681409E30D169AE1277F1230D97B432E4276406D0C4FE52661E69ABD03472F2A56F52B51A5A317B57D229EE0898DE0CB2FEF71DFB71A85ADA97E93B9F2CE3113E838AB458A9FE826090369EA0F496DBB2A21D98981AC4410CF698CCBB6EC6724E498ACD1AF8C480A6AB1A67907D0492098015DB80A98FA8CFD79A74AFD439B4327C07A03C71AA7589D757CAD8BF86DCAC20B133A5511A47DC74B12317FB305098069DFE360250AB29A4239B1792352C3571978B708323BD2CCE72D18392A9C3CB7F504A14014231E9B4F65FEC62AD3125780D51E256CFE1\",\n          \"dk\": \"DB859103F7ADE02C4713C26A95447BCB6BC359F21EE93B845C5C0A145A9841BB576D206B60E46417AB9163A86BE30AC95FE3C9F9EB21DE282FF8D61A68CC43EFFA3FE3BBC9A5D56C304194DADCB94BDAAEF8CA74857C6C5C58A35D415D4E23BEBB602ECCE89B376CB61365C5014C128D090BD3F1CD51380A7D63965C7B143A19663F007CDCC5736BE624ED2721B5F915B8AB4405E5B44C3CAEA181B60A6347A1F0AD7278A879C75829935B056600D078133361A495539E612B9F677263A4A808A0851C54F65D54E8195BD4313D76A9590213EEC21B8FF2561E191DF5B8227D03632782B8A539524E6349D1693B61C0BD4F47B7CAC2AD539A69C609BD6146400F9590A0796DA89ABD99B62C30756CDA7854A792AD8DFC8FC695CE68FAB4AFE88134090AAECB29C0D75C1916A3B6225A76CB6C0E530BE46A60A3A2C8C07427ECB7C090002378B98F03450AA7AA0D8AE5226967C7E9F43AB50C098EB80FC72347E631120B8BBBA3E5C731A7ADE238B55A1763772A86C1159F96EB0B96D5CE0D3C9BA68B9D099A7184A461F91010B8B98C08E0B67A67CC6D4414390A0009493955172B85076F17BAA9A9645C21101ED962574E7C5AADA2B1EA401D05C92494A07664E6521219CDB248CF2D7B76E4D8BC77E30A6CC80017B282F6F55DD81355A8D296D4F0C71D95C14D9031510B012C989BCF959B51B44913B0808C786467AACB0E870C4297034F47AF6226BF8C3527D83B15C547CB94070739367CD48949DC7126E3672068C09C55C48BFB610AC87B81CDE4619221656C624DC1C24C6A038EEF5B58A09A801B225A132C85EAD459DB0A49DEC270B2F046C9482D41B20699E97C11905D4333ADD8139C659289537A411E3BA8FEB1072DB3C723551C7057BA55ABBF0F6755A465B73A93312D4C0BE8C9BAEFC872B48131B6920CBB48114846436E091554F2C352C919B6B16BABAABAB0D1424A5AA5CDD32FFF661DFF15A5BE1A83A0E517FEA9CE2462554F3C64A21C2769A18206F34071C1B2AFA03403B814CFB90DAB476F29B6ABE9DA68F4AB42A10C11C54A54CE51BD03B23DF85072B333114589BCF3BACC6A8C9D390141981325C2DBA6EEF3805950199468661ED4BA300174FCE35411963B2B07599E14524446892603BC300AA3A25B45B41A857D50C06595B40B80159A477D8AD628F455038960BB23B0BC32D90D48E014BBF0AC12F93F17E36976A8BFD1E7B682212815A18B10B5301E0B88A7C17C850AAEC549B98195532C9323559591F9DA52703CA2EF921B1DA82D46A5B522F69109AC83351C46BBC375B61A8E7FEB0161423D5246362DBC8C866A899FE42F3C3C52EF82AF2CD05C11913B87E67B060A2200BB60D8B94E58C1B609DC9FE36A147B5C83CBFAB3C5F59F2032307E5745B7C68E324CA91EC987AF771D9D774B49E18E1F828E3D17641C51CFAAC9450D3A4404141187546AEA64939D94B884868876C86348E27DB0F66BD1A06ABD331B8999A446370982EB25979C35733791A49608D412500E9A144036970C086F15AC90F1699CA79928416858389908F1300356B9C382C94A2AD5491E86C51905BC398CA41FCBB05951A54A3681C806CC4868266E100EC2A334DDEAB98BCC5699E192AEE81DAB58C5C3D24C564C9821C7C0F981A500D91E66108C52013134B9440591AAC5A9BDC6208B22BA88F269B823DC08BD0C4067B7BDE01B5B12985CFA30C9172C9A2D95A33D4BC4EC181F17CA7A294989F0306FE2A7C7F16BCF301752BDE3CD3F2CB81460C0E3B0373EB8B79FD84B05A783E14454A2F2ABC693AF0AB78A0095A3695158A2B6783D32BFF25A4F6EBC4AB5010C0E394437D208ED001CB3B2C92A794607A6BC5EB580E7D52342B004B3A17E20FB70A4C5C2B8CB39C9E265E012C7D03540E7B47681409E30D169AE1277F1230D97B432E4276406D0C4FE52661E69ABD03472F2A56F52B51A5A317B57D229EE0898DE0CB2FEF71DFB71A85ADA97E93B9F2CE3113E838AB458A9FE826090369EA0F496DBB2A21D98981AC4410CF698CCBB6EC6724E498ACD1AF8C480A6AB1A67907D0492098015DB80A98FA8CFD79A74AFD439B4327C07A03C71AA7589D757CAD8BF86DCAC20B133A5511A47DC74B12317FB305098069DFE360250AB29A4239B1792352C3571978B708323BD2CCE72D18392A9C3CB7F504A14014231E9B4F65FEC62AD3125780D51E256CFE1C9EFC09C35370E689B7071A0232850E93F30C5E774FCD37BEE6F8DB91C039E822D53C0A2C522F3E692881F0A4C65BBA41050E7D310898A6747509513A03A418C\",\n          \"c\": \"DC29B9910BC0978FDB8F6D7C215C91E003C550A7244B5D98509145A3544C5CB2AD40A332B2817D15182C8A31108109073D4416992C99149241C5FD147A48F23981C2B69C34E7A72D11B7DA6ED9973AA55A4812239BF8E0DD193E1EDC85635D31FCB26F094E23D47C84F805755D58D3FF7B30E81B8066986D2DC94210778F2F52F94C569AEF36A35F4AAA445B54180F703C28684D842763C9C1C0AFBFED51895B06670D97F68548E40202BF56A1BE5F874A6A440E4673E4A3E4095DB97FD9D36B30F4BE492FC957FA898E4E9BDE175C91927B058D0A1E10A500DA733B640B08DEE07AB4ABA009DC5B5E300F477E6E34431CC8A5DD699EC6D7C509637B6475C5D28DDB73E790F7EB60F7398303B4501D56E2161755D3E43B24AB4C1B391E4FA041C8FE0153BAD4CB6072213EFCE733FD9490583B93DB3D319B51E8DD497A1CFCCBFCC3B227747A9B86B2C5DA52F36894450B2750CEF3B425671EF059C0C4BAE8CB25E6CD626409F79E63CE4262B2275A45D18618DB57E9CF3D8CEA6B22B69340B9807B1DD696B9CDBEA445BD0E1FBDE9D86C92265C808B677A10EEAEDBB71E04949A2FDCC3094ECCD5C37C08A9B3636AAD670356633BAE9B7C16B5A8C4AC79C2873B4056C65E1CBDF13F7A55FA5EC9C4E530B3F13479D8435AC937C165C1269AA8AA7D939433AB0EF01BB87D2FC4D9B9405545D59FAFE004DF0A8086F486B04B1105DF829840CE198BCCE0F55C7486572891E31EFAE42785A2557CA8A685AC8A2655D71E4266D3418BAF29728193C3C5C22E7BF12A933CBEA2D3665C8A155B7B8B8E9EFFC7F99B580393AF3F76ECF9D731D5A299E8D8B2ACD2EDF58F6AE336D3DEE5D7258DE80C86B0E311536F8FAB511574504DB04B4E8326176C15DE143E5DA575C027585E1C8DA38CB50A2A72DAE9D5E266F106261395CC2F9575C6E59B7C73476A65BDCEDDB9E03CF299EEB5089683043FC97A8EB247CD46656A7CC5812DEB8E111CA1040E9BD541CD8AD4786FFB04ED643456E72F3C6DF4D595B1ECB097D6564D42D5915BE55D339D7583AC55B4C1D8221258C0A5BA1443F9BFFFDD2AEE31\",\n          \"k\": \"CEC93D98469424039335CB12FD0ABA4CAAFF3E3B99E55A53507F2CD3458536F4\",\n          \"m\": \"307C7DF0692D264A8186B8D844C7287B236D0FC7EC148BCFBF261A16B0FB7B61\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 15,\n          \"deferred\": false,\n          \"ek\": \"E3F49934F999E0884E019B9299EA5DED8B5374851D9BC24079FA481C534793D328BC3B3563D0922098B98AE26406BB81A48AC0D24102E1D3624F974FCE49786890AAF9EA2E9CC058133C66847B67EFD61C7B469DC8E983ACB6ACB98C25D2A84A6EA82DB780531D0A4FA3A84400C70DDB48CC52406CB3155EC059894A4C01902129D1614C3BC25372C730CDD6CA8B204B9D18793ECA8B080881119549779A67FD456520B1011C6609CD4329C1832F7ABA7997D79B9510409C920E8EBA32A476B72061CE58A8A0A8B369315A814982A519E88E8FB25B3C74B527776C671C7B48C3C9EEB76F2A60BB39D5722A71459B180BCA9A9FBA14B3AB764E5F931DA0A55FB247A93ABB8509334050A029CB7145D4141990929A7D224FDC905419D62828625E316778E1517B151CAFB1B8B8E5E96958447944E266F808017C7CCAED571C64D9BA153709C303C92DC69B808941A8C3C760E52EC7388E800159CBF8626F953E356A827AB8692FE1982F07884FE3B094ACC0075128B3D54D85C39C08D065B6E40B5475CE1F269A2227AD4B368C683A7E797A7857C26EBC1C5207E6113C8B17F1A77186549FB63617611C33CCF38102062094F12EFD292A7B0B98A1423B92DC318E919B3DF441E58A36C04C7E60B270A453707DE23958974EC3A455294168D1265EC6E606AC40C79B690E5829A0F72910A0F5031ED1A5AEF841C58A036A10025B419E0E2BB4C6782EC317786BC1CD538C18B7223345699D44860D0BA04413E70B7D5126D2237545E550893427E34C6FFA40200AFB5D527B80C4A3A32B42B49FF5043B14AAF6A69144D58388A254F7F05F1DDA51F980A3D5DA436540A67569042E6B42337A712FB65A208392016710B8FAA763321DAC533BD06219AC6C48B6929D54D7B6A4A46280D2B242A8C1BA5B2BB42487AB031558BAC899EC3188A1459DB434C5463DEB43BEA974C1167B83C657C93A9604114128256156EDFBC604000C05C84FDEFB2218203131F40483F9C3B06C0FD524129FD65D0AB55714052A24588184164AD4B5929668A5A6F26F9CF44BA946874A21827E5B68A71B21F20785308A3F8DD6EFE22012FA9F25E348661EA987E6455F85D1A368EF1789708DC7AA8E849A\",\n          \"dk\": \"372270C8D59DBC34CF01B107E7E711D265669C5C01EE729926C033D53B0790D05F97DBB75059B3BBB122B73309ACA75D91582C409B39886416F5D97219872FD564935DF0B30F0A5BE2B3B6D08AC5193283E70C93C25C558BE17A93020F375C44B640AE41D974EC2020640515D74CC083143677B32C73D688088B03E8455DF248212B7AA82FC01F1BA0AD96B9430711BF05DBCCB075732F700B124C6B298B171D84C4F338994E0611FEA9AD4244C968DB44AB2967AA7666B8652C12588A3E728235C7A9B88A4D3D7A6C05B46A0D53C31744ACED6BC02B372B364085DE5CC13B2B4B7BBC36C0B06127A67AB170B9BC7C7F938A0A26254808E35A6024920B463EC8D223ECACA90327488EE8246EE5B076CCB5A734B197D61676576A2A4714FE0B3D69B87B581660981CCEA31923A1FB3498802BEA542FF3F1078529161291B014B94785F6BDE14595ED2B0B0560614C547F6952AEAF44974B6365C488235CA23A93759C9F707CDAFB8D81D701B31B08E473852605A493B8CE06B15BB95641069C85B0BB7D826A6F77D743B4051210C742314B787C77AB9193A9765256402C4575F3910C63C92EB816DFB9821C324D6852A5792041FEB441102839DDCB4D49E6ADFB291395B12DD409756E7CCB67192A3137CDAB072DD5004C7742C60A68CB45D63EC2A85F4452A9DD1A06781200D9D65A049BA37A7349402175E6D82AE4F16F802598A1463B46C2A45F583C8F051A84C399F227797F0A0A5051A3B13171C47A8A2CBA99EDBA992E58A5B4E544D19944DF60A1E7A2CFAD69216F4377B48B4B04C206BE5C6F83EA47F702620015029AD3AEF31970EA739ED5E887195C8DC94CAA7B199AB88634326A8303F535B71BCD514C44FFA33F5613388D32778B3C91BACC9F5E659CD170CEFD174D76B096AA2923E99C1AB288CE5897C9066631E1D82267212FAE1221905A7454D753FA916F85094B9DD87972F49D832C19351125BD754047F7200FE049868185658318BEFB8D913A41C6C1887F79C02567AB9236B485148D736B063B6B1EEA0C5F9D8491D279264261B375A799144C9BA787B5DEA211BB85A2E3F49934F999E0884E019B9299EA5DED8B5374851D9BC24079FA481C534793D328BC3B3563D0922098B98AE26406BB81A48AC0D24102E1D3624F974FCE49786890AAF9EA2E9CC058133C66847B67EFD61C7B469DC8E983ACB6ACB98C25D2A84A6EA82DB780531D0A4FA3A84400C70DDB48CC52406CB3155EC059894A4C01902129D1614C3BC25372C730CDD6CA8B204B9D18793ECA8B080881119549779A67FD456520B1011C6609CD4329C1832F7ABA7997D79B9510409C920E8EBA32A476B72061CE58A8A0A8B369315A814982A519E88E8FB25B3C74B527776C671C7B48C3C9EEB76F2A60BB39D5722A71459B180BCA9A9FBA14B3AB764E5F931DA0A55FB247A93ABB8509334050A029CB7145D4141990929A7D224FDC905419D62828625E316778E1517B151CAFB1B8B8E5E96958447944E266F808017C7CCAED571C64D9BA153709C303C92DC69B808941A8C3C760E52EC7388E800159CBF8626F953E356A827AB8692FE1982F07884FE3B094ACC0075128B3D54D85C39C08D065B6E40B5475CE1F269A2227AD4B368C683A7E797A7857C26EBC1C5207E6113C8B17F1A77186549FB63617611C33CCF38102062094F12EFD292A7B0B98A1423B92DC318E919B3DF441E58A36C04C7E60B270A453707DE23958974EC3A455294168D1265EC6E606AC40C79B690E5829A0F72910A0F5031ED1A5AEF841C58A036A10025B419E0E2BB4C6782EC317786BC1CD538C18B7223345699D44860D0BA04413E70B7D5126D2237545E550893427E34C6FFA40200AFB5D527B80C4A3A32B42B49FF5043B14AAF6A69144D58388A254F7F05F1DDA51F980A3D5DA436540A67569042E6B42337A712FB65A208392016710B8FAA763321DAC533BD06219AC6C48B6929D54D7B6A4A46280D2B242A8C1BA5B2BB42487AB031558BAC899EC3188A1459DB434C5463DEB43BEA974C1167B83C657C93A9604114128256156EDFBC604000C05C84FDEFB2218203131F40483F9C3B06C0FD524129FD65D0AB55714052A24588184164AD4B5929668A5A6F26F9CF44BA946874A21827E5B68A71B21F20785308A3F8DD6EFE22012FA9F25E348661EA987E6455F85D1A368EF1789708DC7AA8E849A8034B3E7058DC6E140C0F4220A80B43CACD9758598E55F2931A11A2F5C9026556B16944CAA344BD9BB904392078ADBE511660D4F9228446D69DF2A20A6CEE850\",\n          \"c\": \"7122A73DFE33E937B2D3350EADC73B3EE70C3BC5E9C4B2E7DC590A491CB7DA736B3B37294B0F13013BA8FFD8B25C2164E8EE528044A230220D8203AC4D2ED48FF05C479762CE72DC62957E839580C7FAFA23556119AFA66A53655C48E6193E1B386E4689821F5FA81643B22A7455A8BF30523098721042830259D90B69E21F038607140030A9EEAF30EBC813835AF12CAC2E018F7EF30473D235E6631ECA0306D6AB9E45608DEB559416CC92A7B4D465CD56184B0C4353D8A8D96C257FFAD6A90E090C8D735FD32A14849DCA6B383ACA3FE0A9F482A5A5069AE3B9542E83BD873C3D3C0B052C5DF69D267DB237D65EAB2E84F38B4272F079F84FB6D64F17D864464522E6F79D2BA9C4F1C2E6A0EB8FAEA6CF8AFC71E79B084E77D7BDBAAEB233F107697D245EF9BA19E142C73C9513E711621530B040DCC9B70088436DA2564F97FDC79E8D062ED490778BE78BBAB0B9E71559C6F5A7A314D73C4E16CE627E88F27F1502BD90C001607214772DDEA44C59040DCE7051F0BF2BBD712EC82CEE54A6F41E19DFEB32AF373BFAC06469346AF5CD7B32A15B66A5147E0D880FC180C228ADDC6755C3957740CF7A41F83B3DB58D23B19A33A4275EC795FD1EA20CF6BE52F5070B261A68AA0504CDCF3391A84EAB931FB14EAB16BDA72F69E15364962DA5988E4F0C37C715E5805DDC9674CC1E44449A0E534FB5E4499B32B6B959DFC0E937F40C4F0922EC6B29D8D4C8676F2555FC43B6D860696294A0FE7776D4944CDDCE8133646A0DCFD9D9117595E542E8F82BDFFDD969DA7736724A7FF71E322333EFE3CFD97BC04E80968248BAD48BDFF5F8AA6B0BC1C5141A7B70755199F83B0A896B2FE547F68CBEDAFFCB101A1520E1AC1E6FC364078D626FF53140271CF9E6FE8299D58DF313C50D082D7EEF995DC34BB98BE6026EB87EBB6AE6B776F72C71030DA2DE3F9A84B45953F80EB0A5F06B7408E7F9BC9EBE7064AD8BD9DA234CF4F29DED508106416A6B39131862D4DE2774663658F02F53D85206C8A9A3B78AF18623574E109DBF54EE08659ED285543ACEA5DD2533045FB16EDFF387612CB0\",\n          \"k\": \"BD132E98714A75116BB032DFA0C7B0C34EAD0780C576DF9EC11200256B4BDA87\",\n          \"m\": \"60363F5CDB16BC516A1367DFCE1B72926FB2189B88AA1DEBFD22F440B9CAF0C2\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 16,\n          \"deferred\": false,\n          \"ek\": \"964B324032B0EA05C8042A49F702AE0FCB19257B1EB97079994A9C67043D99CB08708A2F64E694CCA51B71431DD20A77BAC80AE5A259EB99517020CC8CD53D525454A1B6052D4C598D9C504795835D9A45CB5CC7BC6313B857B1884067B0D97961108E57AB00DDD8AA5A69356093348948208947ABEFE117AD92C8562B5DB7DC037AE89C76D00C4315548372A272C32B30168620380CA0A697724913C173AE31AAB4B3B54F3760246A310F199027A72BACAB7046AA702710C145EB9A6F312B3B8FC291762BC301C900E7AB41D298A01D890AC3C85A40D0BE406000C6537BF9503E70986861D90B2EE3C8B0047E66B8B88CAB3AE9FAC382C23D06B835789BBB47A94823180859969535D058E7946C69438F89236AEEA110E404B51C1C22ED04CBB2EA32C957795797806F179A38642023E8AF6516A11393A0256A7EA3C27A8600A669FABB89E144E30A839D391E6C1A1F7F96A1E5FCCA889C8B09C3779B407075D97EE893AA4CAA1F5E5CCF3C5CC4DC361C8C80525BA81C05488012CB61D3EC87720A2CF0977BBA49B8F9D81D3EBB50FBFAA420E27AFABC387EDB87F114C1688830D4137BAAB10D1A6554179398B2D939C8F4387AF0007CEA2AFA9BC56E2766182B0C8D876123FB1A64C405CCD7A9869466B5825E23F41C4BD1747D5CC95C27912DA559535B195B96879F8C14F6030020278DBA6CA371C76F6FB7059F0B7E1EC67AD410B12F36878274C0B19A2F571A49A0358B945BBCAC5142F131C94E05C6A96C08DCA52819640431930D4089B2CA85A4FBA62F2B2614ADC7C4C80B06745320F837CFDBCB0A2885C321738C2B4233085A1B5F4AAAA658354078CCD008C496F0006AE08FE917BBE35433570B4C1838AB48593466D58F18A43A025355ADD4B333EB8146251E32FAAD8742BC847A7916212D5DC69329D0A3AE57147628928FD0B08752A21AC308515229EDC169FB6141FFAB3347F427ABC759C40C8D29448A273441A9C53654042967087F470024EC714B0708C033436995AC4FA678A9FCD724F703411454C04C21764C1B243146587C774897C6752BF79868846811106552C8AA574AD8DB7372836844DA6BABBFE62263107076C4CB48CB256D359D08F68375BC\",\n          \"dk\": \"879036ADEB2F85A51E3AB14979E71A494A8D3DC4CCB6B6A35F826CB03761A1A799F621957DB260133988DD12600DC33602226CEC50434BF059E3B11FE5F19A42BA1DF9164EA118090FB7A1AE857AB7E8ADBAF66DCE7CCC01B8BFA93153D3E5A644C56B1116058F47C663CCA1F54C3184BBB0FE8CBD05E53265ACBE72CB37286929D85B856CC8587DB46DBB3622930465775893FB7401E9E98430FCBDB9966DB60280C8319349C18C62E56DC9BB538DD296A47169753A1394680D202A7EC33ACAA12B315BD69F93A07F77B9C6396209FF4198FA88508A4548F16503B1C4079F7086CEB9BB4F751A0B8175B3F193805685F098A45E0BA16EF3828599842A240B05A201ACFB45B63C4C2D6BC35EC36D69E4142666B8600AAA123B6E6D7C15171BA2FDD94B0205BBD4963A45CC46BC089BDF817C52933E36DBC911174EB62CC4AE4AC8C4E45782736125948A97A2C66E7C9A756CA146253D8C07349A6C4D4D97BE122BA433DC388F972CBE79695C878A23537F33C3B339879BA0481A41032E961618198529DD154FB38B918A019A0FB75827084881871176E02C1E8ACB746832F44191C1B36D2B96CB630499E83007636AC656707998F436C1472EC3406515276E7A4840DC509123D2907AC7424416A656A74E49EB478A01750470ABA7F5AA416BB99A50CC6FD823AC1B9178376A8DDACCDDD2975CD68724FBA3970216983219B70A4ECE563A00C44CF121880FC8C5936124DA62008BAC6319806E1368B4B8467CA8980962C826FC20761D131D6692C2F1907BDDC83F2CEBCDE692C8CFE6880BA357D1D056A022A1D3A4C0D7F638161011AA83B18500096591A1D6B55110976CBE0A03BB78AC7CC34FC5774D794B3871B94BC7C081D5895CB44C0EC7E9B2782004C5C61564F582C537C6B72B5B0B924799181D6D012EA130B2D5E0C998F0C3CD829BBE48645A6905F6D240AA518CE42851989C987CB9CCE9F71C43328703F5882DD13EA0841251FA4177B593BFD58AFD146631D876C1D96310CB2C6850783C5939A431483DA78E09C3041880B1C3686B0C45B442427B61443D957C01A580B287B1AE964B324032B0EA05C8042A49F702AE0FCB19257B1EB97079994A9C67043D99CB08708A2F64E694CCA51B71431DD20A77BAC80AE5A259EB99517020CC8CD53D525454A1B6052D4C598D9C504795835D9A45CB5CC7BC6313B857B1884067B0D97961108E57AB00DDD8AA5A69356093348948208947ABEFE117AD92C8562B5DB7DC037AE89C76D00C4315548372A272C32B30168620380CA0A697724913C173AE31AAB4B3B54F3760246A310F199027A72BACAB7046AA702710C145EB9A6F312B3B8FC291762BC301C900E7AB41D298A01D890AC3C85A40D0BE406000C6537BF9503E70986861D90B2EE3C8B0047E66B8B88CAB3AE9FAC382C23D06B835789BBB47A94823180859969535D058E7946C69438F89236AEEA110E404B51C1C22ED04CBB2EA32C957795797806F179A38642023E8AF6516A11393A0256A7EA3C27A8600A669FABB89E144E30A839D391E6C1A1F7F96A1E5FCCA889C8B09C3779B407075D97EE893AA4CAA1F5E5CCF3C5CC4DC361C8C80525BA81C05488012CB61D3EC87720A2CF0977BBA49B8F9D81D3EBB50FBFAA420E27AFABC387EDB87F114C1688830D4137BAAB10D1A6554179398B2D939C8F4387AF0007CEA2AFA9BC56E2766182B0C8D876123FB1A64C405CCD7A9869466B5825E23F41C4BD1747D5CC95C27912DA559535B195B96879F8C14F6030020278DBA6CA371C76F6FB7059F0B7E1EC67AD410B12F36878274C0B19A2F571A49A0358B945BBCAC5142F131C94E05C6A96C08DCA52819640431930D4089B2CA85A4FBA62F2B2614ADC7C4C80B06745320F837CFDBCB0A2885C321738C2B4233085A1B5F4AAAA658354078CCD008C496F0006AE08FE917BBE35433570B4C1838AB48593466D58F18A43A025355ADD4B333EB8146251E32FAAD8742BC847A7916212D5DC69329D0A3AE57147628928FD0B08752A21AC308515229EDC169FB6141FFAB3347F427ABC759C40C8D29448A273441A9C53654042967087F470024EC714B0708C033436995AC4FA678A9FCD724F703411454C04C21764C1B243146587C774897C6752BF79868846811106552C8AA574AD8DB7372836844DA6BABBFE62263107076C4CB48CB256D359D08F68375BC6D63B8A32687E3ADAB407548CE8B83437F355FCE2D96C1BCEC6C006F7E493B743ABD1651588386750AD3B35DADE74C328DF82778D99596561ABD71F194AAD28C\",\n          \"c\": \"4DBE5E9CC3989D6CCE8D9F491B94985E770AA2ED9D214D45B03832D90EC0817A9F06856EB4D6AD8BEB6B64B7F1892EF9E13C594C7BC1B9222C655F259603868047D35ABAA67FE36816BDC493502E0B3E4129DD58A8971590E182B71386F6BA4F4580618EF186668C0579109E6DED3DC1A8F22EF6FEAD9D20D9435C144CA8D46367F178CD0957A638AFCDA69C96C59B3FE03557107915629E21F148603EF68FFDE3327FDCA00A6B1A3E498F90FE7634E56AEF588DE8E9C567D3C9F8E5E66A7DA505DEB1AF29DB37CBC5089C27BDCAF1E06614D796B89B3C1D9B40294FF479D6E64BEA14520E734600CCAEF6AFB0DFE7B66291F79859E988A4FC78431EBAEF90F511273B16DA1FB42C404D793EB36414A40DAFEFCA07F1787FF9454FCE27EB142B985D21D8E086ECDF1B2DB9B7DB12F1BA442BA3C8C16613A8BB7D4F155155F6AED9196E54BA77027B8C040E6A047AD6F41FF174FF3CA4F38A167221B8715FF60661E744EA43F49335C3E2197E57D7A1DD5CF53DA8D3ABF359251675461EFEF9CC2F033B6ABD73B35A8CF740D65089CCF5FBB94DFE1CCAF26AE43F7E5F630209C7A6EC93753E808345AAD1068D18DE64BFC4A8BAA6EB6E72D97824216F922729D9FE73401EABD11113FD7B69AB97FB10525F273EE35B2390446EFFF9BDEF7FEBC416E6668B06FF5AA7A5C50DDE17554606228B8586BC93E6A7B9B239D8DCFF2E4E55ECBC75C9E449F25E48CC2100C3F130429EB3F841EC2C1A8297769AE8CA81DE932833DB2CF10C9EA57A682A48E26E3D42D9BA9A6D61C82F4014B804B8AD8619CF84AFD3645976ACD87D1B3B7D40CDD87C35C2E3973296D22851C80F84B792FDBD81F549968E2BC6DF18C37F50805D29CB657DCD9C392070FF3EB4EDEBA8DB298447A713E517B94B1CA4C11CD462B2ACED35C4AEA82D65E5D5F050CBD7DD8B40BE07628025F8A352CE8A15D8FCA7FEB78F9D6A1D179FF4AA336CDF3925FDE9631BB53649DC65BAA226CDA770E50ADB9E2E3651A4E6776748D76F9444960FD14B9116AD419F6E5A4D74116890F05AC8ED0BE52A1C70DDECDEEE8C6914B40B1BCF\",\n          \"k\": \"CAA24999EFE659AEFCF18FC9C722FAC1D5DACE583B716AD3828B15C7DF5D94DF\",\n          \"m\": \"579474C123B3381801867203E0021E2B7F15E5F9426D75A3EDA6CBCAECECCF43\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 17,\n          \"deferred\": false,\n          \"ek\": \"87EB24113349A40704B7243F2AE3653F24B87EF50E2220924BE06BC1E86E8AD9011768B255969F4F29C21BD38D6687756D795F07286B4EB04C30143C4F76810CD3233E630417F86EC751CAA87806CDD93EC0B5332758CDEC3A37B4F84878F89A4E687F0E457E45602517B676FC944E0BF9A9E9903EF895800D019304AC223E37623EE0C7DB798AA1A7452D298036A4197E89B0F279A385DC19D20BCED386352FE512A9EA90F973B6890964490538FD82A1748C98384C38C1434EECDB6EBF561366C228AD0086D142966BE1B8E1025097509E460850087935CF543A7309342424394267032A84358CD76E4F98C34B0005B39811233CC425A237AB49CDA9A884DEF6832B1A2031F5AF07DA4C23681B561C7D91A22C65129206187EFD2926016717F1F318EA95CC037208D2B313E0BBC59257710EE99F9B81187837861404CF4E930D0A4185CD3779CD4830CE2A02049AA0BBA0BF06003DF8A358CFA4CB2D3B7C7A508FEA1B930D1BA379E246B9BB793D1C2548F8BD908315BF521200261E42C843D963074CE4CBB3A5898976815CF3CB0DCC137F0CC414ECAA8883CDE897649E81BF707216FFF23B124B3FE6A39A45EA317BA5ACF8116E9825BE6AC518B4658D5D23CAFA600F94A9ACDDD38F303978BD64B4CDE34E73334C1B212060E29085925E6A3319EAF72097826F4233CEF2F4AB1BC9CD99B9338FF43BB9EAB9723140CCB86741C44E5D083CF6FC97D071CAB3135F6B358172468DAD6834FF3A1AD0750F06837EC25C6AA4F179D259A0FFBCB4E6756B627BC1F9F76DFCE2712F7340F6AB2109F023D6BAC5F0A6A63063BDA11625FD93B3E8C7315D387AABBCCF763237D785A89130A3DF63AB2F587D8AC4BC90B795C3639EF2352D0314AD638A532E625A78F296B5C5A38BE65995F8859F279795D0795433BDDACA847140650A72637DEB3529E8179CC056FF58C3FA094678526E12E791272BB623367D0103A68F6811B0636AE101090B2480EF8C6C59B51426E6A3E8C5303C3200EB783A9A133AD2E5261B5371FA338B6D016367650BE0D4A93BF3CD9C48629F8A448AD9AE86258F454BB2F60B1BCA578FE20B26DF0732C3222BD4B8A71A2F5038031BEF9EB0DFEFB2666C\",\n          \"dk\": \"77E8C08006BBA3A7423AE68A67F48336EB0F5FA3B14F931F8B88AAAD384FBCC3CC291480228079D4179A3B6A0E555A4D3E6A5C8BD72A5C0779E770C56B53BAE1974EE047AC3DC953525C03C5ACCC0164C02C65B10407CBEA6AB0F9A413F0E169CCE31446BA219F765B90353F8FB327F9F8C767F6BEA749666FEA81D1186AFEB5170977BADAC98E0BE57854431756C042DB86430C27A358A3BBCF823261C8017FC8B81F9970BF9B73433531C13AAE0A960D82C06CC491865CF4450BA6641D1729B0F7769D073FFE99BF2A8C8D40538CA08318FD163FC5F12264266710D7259F765DA6786EA386C5E9543BEAA54FC785C1C83940FEBB2C866B737DB93D2E798E03A52645330901D118D0C973A1E997E5BC39967577E51C752DE187EAAC8E5C167BEF272F79483A658B6382B93F67E07E3182A183AB9842951FBEFC8CA6525D571B9B7C995D89366AF8E62B371B6A5A0990F3B7AFA054B997E921AD708623950EE7A2C6AED3316580735002C48A298AAD0174DB794A5C862092061E0C25824AE84B6BE824A33911A112AD51509E29424B4D933946528224E64B4BF924552706E7307B4FB7B8C2D083B2245A5787A19100CE78465C3B663330006C8BD0B626CBB9ABFAA3F551ABF18AC146081ED1AC99A37944915CB6E1713DDA4049C3F0CCE4611E6A21458124A850119CD5B846867C6DF0E9592B0A1AF7F1C5210C6CC2E5192A44778AF59BF90935AB8812E811B909528A402174B3747ECF6CB6DBC58DFAF15CE849BE309760A0F9274409C8EB25A679764090E59F27553AE4B31A00DA84B25A843F23C6D9024B23CA942EFC05753123C703391C21B539C9973E14611562036C793CCEA69B0A51785664C0367B94BBDC45833BBD6F132F1A34A31134A934E72A6F988A36A9BA6516981F242F67C33C9F71C113D40380147274427F429A1F26FC5C5EAB9379F0A73DF442B4B2A299E827B0A522DA24690DE17F72A547E9E228D98541C5C02058B1879D5946AC83B338277B6362C72371A6F866C2C72481024A4ACCD0B91AA560249CB5B01B0BABEC227D1AA1D6FB5E878C66214A9DFF0BA989C78987EB24113349A40704B7243F2AE3653F24B87EF50E2220924BE06BC1E86E8AD9011768B255969F4F29C21BD38D6687756D795F07286B4EB04C30143C4F76810CD3233E630417F86EC751CAA87806CDD93EC0B5332758CDEC3A37B4F84878F89A4E687F0E457E45602517B676FC944E0BF9A9E9903EF895800D019304AC223E37623EE0C7DB798AA1A7452D298036A4197E89B0F279A385DC19D20BCED386352FE512A9EA90F973B6890964490538FD82A1748C98384C38C1434EECDB6EBF561366C228AD0086D142966BE1B8E1025097509E460850087935CF543A7309342424394267032A84358CD76E4F98C34B0005B39811233CC425A237AB49CDA9A884DEF6832B1A2031F5AF07DA4C23681B561C7D91A22C65129206187EFD2926016717F1F318EA95CC037208D2B313E0BBC59257710EE99F9B81187837861404CF4E930D0A4185CD3779CD4830CE2A02049AA0BBA0BF06003DF8A358CFA4CB2D3B7C7A508FEA1B930D1BA379E246B9BB793D1C2548F8BD908315BF521200261E42C843D963074CE4CBB3A5898976815CF3CB0DCC137F0CC414ECAA8883CDE897649E81BF707216FFF23B124B3FE6A39A45EA317BA5ACF8116E9825BE6AC518B4658D5D23CAFA600F94A9ACDDD38F303978BD64B4CDE34E73334C1B212060E29085925E6A3319EAF72097826F4233CEF2F4AB1BC9CD99B9338FF43BB9EAB9723140CCB86741C44E5D083CF6FC97D071CAB3135F6B358172468DAD6834FF3A1AD0750F06837EC25C6AA4F179D259A0FFBCB4E6756B627BC1F9F76DFCE2712F7340F6AB2109F023D6BAC5F0A6A63063BDA11625FD93B3E8C7315D387AABBCCF763237D785A89130A3DF63AB2F587D8AC4BC90B795C3639EF2352D0314AD638A532E625A78F296B5C5A38BE65995F8859F279795D0795433BDDACA847140650A72637DEB3529E8179CC056FF58C3FA094678526E12E791272BB623367D0103A68F6811B0636AE101090B2480EF8C6C59B51426E6A3E8C5303C3200EB783A9A133AD2E5261B5371FA338B6D016367650BE0D4A93BF3CD9C48629F8A448AD9AE86258F454BB2F60B1BCA578FE20B26DF0732C3222BD4B8A71A2F5038031BEF9EB0DFEFB2666C29148150CC61C6C8B7FD408B3C9B21B6BF530E9D9AB72573FC6DED2E4A10C4C0D01000728E8DA5326C713E45EDF82C441D51791E0AE7663DF7E931EA208B7313\",\n          \"c\": \"A707FA4EE57BCFA296EF6D10B848DEE8A48BBF84A465529F837977FFACB3E429D0D2C58BA2A10406995B6328AE91728087648B7F018FF9E570E533F982EB58FFECF6BB104CF2E6D819E3E17CFEC29F31F275C64450B5D8C14E231C563F03FB978B214A51DAE1118D8BD235920B401A706F8C3917D3CA066CF0D1D6591893B244ADF6F0E514575E67BCE3CA8217330153EFF5F91FBA1C23CAB3906F2DB8BEFE01742EA2DBBDC0D3B7127BCA805430792E30CC2CC7C1108EE02CF429820C232A65E4A3CED4949FA184A8B624EB4C4B72DE88750FF7565E35A54E71DC289AE7E59F9F05A915BB0B35C7FD36967EBEFAAD806779A116DEEB462306E3757C94F2B6EEC836DDF1CAE12E3AA58F44AF495F410321661E5451ACCE0365C74EE70A630E59A16CB48532A8A7DC7B2276120450820CDE94FB32DE747E643176FB02ED2BB06111E56627E790C304AB163AA0B424C280940459900F51B95FFFAEB244B31873A5C452508E354FD8C7B1C9DFCD73B79F9D1D5A76413CF25C1E378461F075990599F452E0221C0C8ACE25BF0227632C667D8930D12E5A136F8EE42E19677FC3A1ED91D88238527F4F5B8DDCA69A9E25B2AA84719F9D6550D57D8B2BF8B42C3B46D760694A15FB894155F75FAF61E67672A5BD8AC7C1A2F82812944558701181A8F7AD48E1C5E3048F1DEDF19BF5DDB322B0A5559616DABFFDABA2AF717EEA488379E75446524023563FB1CB34102715A63F1E2966C72EBD6A7C590174699BCF325C627970DFA7DB82D8FA9C39D82E412FA7827CD6CC04D23607985B97E5E5E368A23F25FC516BF771DFA1AF4CD65794F72FF61DEB1541001C08C8038E633800967F6AAE7F9CFED288921DB4A8CBF0BA1562B93016DCD051444F4E23817EB081E23309B044390E33F8D73AADC7ED244239B090740C30C73663170E0703DF06B886BBE4735BCA02E44E25382A18D6F18C0CF9CD452DA692C5958CFBA89F84E4BF5DBFDF2B3002318E08E8215BE0E5D770439BECB122A8F8A93CD93D3E8CEB6E89F83B224CF6D7F7526B1181E4D7781FED7A0177BAC4FD82FC229E6A8A61FD4A958A2D\",\n          \"k\": \"F9F4E46B44C781A74DC60E149C81047C89C75469123ABC787DEAB36EE769102C\",\n          \"m\": \"E2F0D46B6C4A43E94CF967EF2BAC7B68C6E0424A37DB52F2BC0C1695D1A66B67\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 18,\n          \"deferred\": false,\n          \"ek\": \"FC93AF4B238968BC258C1CB5B94592501A51C3368DC9041A56DC5E5D230343380E287396E9B821270633DAD37C920182C82C9236FB28BA025DE94A8E552A605AF1393C988E08419BAB6C8709EBB41076192435699F1C48DC149D3F040A5A36ACC7C869FAA5AB3507B8B32A6847EC901DC8B9B19A04B70865955C4B3B01676DBA6B463C36397B2A42218BE0DA0B55E1A5ED8975C002A9520C114CB672DF5B3A7F838911F7869F148D51DA2584AA0C15F4C2A8C95653C95970D2BB4805928DC02D3DB050D6338A50B6C76332911E639DE912656B72619DC66790F491E0DB15C258C000650DE47268303BA52A49A9179A4DBECB45E7B47A74A35DAB781F52D27CCD2B6C75474474DA0374E25D21AC152E5AAB5225BC7EC5A16F5A1FEE930C300A8A15C090CEEA999D045D80CA73B5484C9E8823604AB464404F9A9609AE920ECDC688E17090CA6B7DF2108151E04BEB6A27F7EC8ABF95BF84D23CAA254A93F89DBBE133A6B1862A20B2B7241ED0695C31B17A7AF1A6A557BDB431A3D2DC06580A8DFB52C91592A80E3C1995E5B69D03AE3B8C13F37AC4852C888E077C290928420A4170A377F0A519DEE8789FB7BFE6A214C394038E281B56AC36B5474286A23142E14561D494AC2CCF95322919A34369B94D79BA787194A471F3A3271BAEFB4019FFC040C831447FDC9BC19B948A0710471A233F0B7104826A83B881CBF4764B1BB4441A843255A8445A034AD6972F4C66C906550C1C5E0D30B718C339618B3564456EFDD0AF36A14D7B705E62153F5C880F017740288545A6D51B13A671EE00970217BB3B33B69B56C823148A60499CFA960B06EACB3F9224275BC907D89E4722C1E35C425DB0ACE65B7929F66F82204AA2F0148BC0B7126B4EB13C4692EB3732E3B2559AB0B833AECF072C2060B1DAB6A9F8BACE386A098CB9B7B7447E6957B6219C6607B3CD83BB64A4B12C973C28DD7933A4B95AC3C7C59824A32E271007ABB369FB06FCC82B20D38BD436B54B155A5FC25381361D13B41D0C4B309E3A4C295C18B4F845573051C81B0E6E37A0D7869D23412F9CF55B3AF5121E491E45292535577EBB8EB2CFF39D783AE72D468F687BBFD838E6A61F5C5B95FF1F20379091\",\n          \"dk\": \"F38BB156C26C61164521D1A109AA9B8108251877AE7E9878ACD67C3B497E40323A94D891BF700CA9E1A2219C7BFFEC710F575AA6A3BBCF9090FAAA82D2F0544F325A8B72A5B2F616CF87A9B698C7D5790974547EC5A8963D9649B598370861AB3C1628DB704368A256542B944BE03E28BAB87B6C28AB08ABFAA65827C5045871CB9F7911B81A0D577C1EFE3AB8FA5671A1565AEB286CE9C2175E60CFCDF809AC7384CE260D904C56CF6036AEB18A7B14B3B09C902337707BF739C365748B08BE40885DA99AB3F909C6C9D401ACE3C1816B2D2C8731BF46910A636D5882460C061DDC175290290B40B2AF1CC80CBD1C3BC669339CB95FD3D320A663C5937CC6D0C98D7D211D9CAB2D9D26764DD446893646569A14DE84A7BD7037DD4C644B633D4BC23BACE8308811BD0232A8737A011E98C2F82627BBE173640BC6A22AC957C5BBED4433F802908ECC8837152152C167C296C9254695523AA427A919D7942AC5952EC691697E0216F7A4C0A8C3A6D9B9CFBAC6A47EBB6C80C05BA64B2C94CA8A99F717B1E503C8185A3103A6A6C229D34753ABA4A2D1E9C977EAB176EA1B3882051ABC9C37ABC0D9F29F01753730C90E811391FB75B8EBDC3A9AC525BA3A02B54378BEF835EC51A824C63C5D8509A394183176004A2A0D0A565B439A909A5271D1A3C7CE3A9C09F809C1E2220D28AC38B52ED6F93D73D7A97779B61433CC709323EB8349D5902242861F99E261744A4EDC58073E676D1D84BA5C070A87240F6E82BD700B7AE7EA08E720815FE820A5629CBB498509AB53F359526977B09EC7A33C78CF3985BDDDCC86770106F834900E6A41622AB6A4C2B02F8A7AB54A53A0AC112180B5CA02452056A421F140C5B1BAF5EA0BA5B34B95B7CC71678F31E97C95056C1E4569B3359261A1AB1DF4749358468526CFC922C499C97888588EEF14BC31DB595B71742BB038CAC479B79B3083E77460C9CD860C1AB4E2595A99942E97A40CB0743E5403A9749B5DE2ADDB5A1A1A850353B213F4B6667A561A1E25266773577BD2367193A156DA92C9E72E036BA63D103BE6E0AAD5325F9F5A55E1D749FC93AF4B238968BC258C1CB5B94592501A51C3368DC9041A56DC5E5D230343380E287396E9B821270633DAD37C920182C82C9236FB28BA025DE94A8E552A605AF1393C988E08419BAB6C8709EBB41076192435699F1C48DC149D3F040A5A36ACC7C869FAA5AB3507B8B32A6847EC901DC8B9B19A04B70865955C4B3B01676DBA6B463C36397B2A42218BE0DA0B55E1A5ED8975C002A9520C114CB672DF5B3A7F838911F7869F148D51DA2584AA0C15F4C2A8C95653C95970D2BB4805928DC02D3DB050D6338A50B6C76332911E639DE912656B72619DC66790F491E0DB15C258C000650DE47268303BA52A49A9179A4DBECB45E7B47A74A35DAB781F52D27CCD2B6C75474474DA0374E25D21AC152E5AAB5225BC7EC5A16F5A1FEE930C300A8A15C090CEEA999D045D80CA73B5484C9E8823604AB464404F9A9609AE920ECDC688E17090CA6B7DF2108151E04BEB6A27F7EC8ABF95BF84D23CAA254A93F89DBBE133A6B1862A20B2B7241ED0695C31B17A7AF1A6A557BDB431A3D2DC06580A8DFB52C91592A80E3C1995E5B69D03AE3B8C13F37AC4852C888E077C290928420A4170A377F0A519DEE8789FB7BFE6A214C394038E281B56AC36B5474286A23142E14561D494AC2CCF95322919A34369B94D79BA787194A471F3A3271BAEFB4019FFC040C831447FDC9BC19B948A0710471A233F0B7104826A83B881CBF4764B1BB4441A843255A8445A034AD6972F4C66C906550C1C5E0D30B718C339618B3564456EFDD0AF36A14D7B705E62153F5C880F017740288545A6D51B13A671EE00970217BB3B33B69B56C823148A60499CFA960B06EACB3F9224275BC907D89E4722C1E35C425DB0ACE65B7929F66F82204AA2F0148BC0B7126B4EB13C4692EB3732E3B2559AB0B833AECF072C2060B1DAB6A9F8BACE386A098CB9B7B7447E6957B6219C6607B3CD83BB64A4B12C973C28DD7933A4B95AC3C7C59824A32E271007ABB369FB06FCC82B20D38BD436B54B155A5FC25381361D13B41D0C4B309E3A4C295C18B4F845573051C81B0E6E37A0D7869D23412F9CF55B3AF5121E491E45292535577EBB8EB2CFF39D783AE72D468F687BBFD838E6A61F5C5B95FF1F20379091CF21077C6E3D08D75668EB9DE6088C89F26636404240ED78CF9683E58F178427D527C588E4CBF3A4A4F983B4DFEFB28FAAD96A659A16B403180DDC7E49391AE6\",\n          \"c\": \"34F172C9C056D82BD5DA9A1EBEF6241212452C78A2FB05DBC7C234F46847B3C3B8A1DD0B3316D4C96F84FF3F45B9A8E2BE97417A58946A83892A39C553C59B20164F64C37A3BEA9A14913A6F384AE5FE4B3E00861B903FDA24D740C29F086D1A517B24FB1A101F5855A9D2FA1237472595889F9826C6C5DC0F0FD14A359B2FC4A39A49BE7095E9CDD57D112BF4792433078CB93FF7A36BF5500B61E94545E1578C3817D81AC2E86414BE0339E26E9395E65957370762A5AD089FDB6C74960E7D6AAD7FBCA78833E69F0FCD60A581E836EC41CCDAB3659E422CD2EA42F95D86D79A5974DDF913E6E85061C29467BA1610B5C81E5A5E527F7B7BD1E2B1A21F64E00E11D7ADD5EDCD8898CA3CF5E497DB64DC68502D6183F583FB4BBF7826F8ED843F99634FD6E00DC4E9A87E0271777C7980FD2E72ED83B253B6F0BFE363413E9FEBCDD261ADED6822EBD9501A0C10EF825D4D20D6DAF36068AE03C9B8426939B81761689A6EC6019389B99BFD1DA02D3B0725FAD3DB4B9DF9FE5F291E91414B81B3E64680CF7DA55CFEF76C14D883C7A85299971F328402CFA1EF2064737AFECC27E4A49074C47F08DFFCD4E3AE86062BE0802F7F0FC1BA9C4791BEDEDB83BF432D9B81925C968467A42CDB2C7CF581C2B645933CBC5B03C9B285B6C559BB7985C0CDF7C242A908F0B78DE6DEEBB9BA848F8B3BBAD7A4663BBD26540660E1160C918EA19DF06C64395BE4A439F9963F4982A6EC981F0FD844F1C6FB5507B54618ED1491710ABE264339AB866D393C0FD953AC8B38AEE24AFFE1988F988982506E5D7CAACD8B5A78E13F68321C77F8AEF760B8D45CE5307CF6A3DF13B2D77C6901847E7E9715D1B84DD43CD7A806F8D0DF99B257F8F34C1F2E7891B226F54562FF3C48A05728020E768B863FBF5A2331CA967D55DC8F3468CE8BF5ED401D0E98159C5882720CC34F61FF9076256371377A179A25228AA5450C28AE27826491ECBF5174D70D94C5B6E4AF04853DD89003F7FBBBC241CE87B96AD6F6BB0C3407E448F2E75D2A040F7978B8FC717F69B3C1124FF46667234B2D7EE8946FE63BB18E19\",\n          \"k\": \"52C8EBA213E652AC3F3CDDACCC5586E3C26332A4BF5E57B69421E6DD45C5B873\",\n          \"m\": \"7B34969C65DB28996B6F9C440DE09074CC98DB4F08BD43E4CD948EE4ECFDE8CA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 19,\n          \"deferred\": false,\n          \"ek\": \"894C58C872A67C630143FB29F5C2533614AC1ED6CA436463EFB38C7BF8A8DA348DAF4BBEF00A8E61E650CA9C6BCAD30CD330CC2D032ED7E42F41A9967C3AB568A0301A5A12EF70A2A6EA97B187691AEAC151029CBE350065B0C2239B67E303050D92A692237E21900BF2901A69231B14F04901ABA8D22A881365B943BA429AE31A72B8701CB07029E129B5D3BECF9090A70A5D3A86BE1BBABCBEF35592A7A74C2CC0560C0EC2ABA3095589C7DB2A5849CBCD483730F479ACD55B212C942F900407E46C0C584200C1CAD7D05C7DF467FD31C31E266A50C375689CAB1BE672774C77062018E74CA1BF6BC5F223802EF93CF7A1383F548196A9B4DE958C49014EDA8501BC8A56B2E1A2B0D46E18101D68816EFCE2297CAA5CC509399A0152232026B754A90FF104B9189426742C2FD8055191BC162B04A262C3D23B996C2AA867F3154521BB19E0A69647718FB76EA5B487C930A454943F0FECA6FE163F40557AAFB9785DA0B6450B0BB4095BA23AAFE83B731356B290DB64B4C39DEB5B30FEF38086E6841B660D41EA593E1C24028C60BC44A6D83696F88772425235A4F05A1847AFD5C84DEDC0B4C1D003F29AAC29710AEDA5053A4C0AF556C4F5508C45FB20141B1FB641AAA6EC6E88D0859553896C31A0206631750C954EE73B415CA163D30FBB75CFEB15714E3330AA346F7DE02014214C2EC5AE550906EC5632CF429BF3467FCB0327017703D08A7F3E9A7785D0C9D6377FAFE527259A0ED5894E2A8C766F688FE2574DF57108C81C60A8705DC7D102891437CB2B9988C879D7403967A83A70CA48D6F1B18A3408BAA13BAD9C26297CC10D920F0C53CAE4D85C6EE0C95DF22321F0A05894CB97C4A45F79CC002DA373AA857B65C726F54B03184934B11376852E9FF58099C72BBF531943EB5626C2C3EF6BCA145A2896B4C1BD29A353F49EC2FBB746134C1F50AE6D771160E64848396A5DA65EC32014F9669BB83AC52298B24DC2AB0BA2C82C0B4017B34F9D979D7D07659C4B76160999C47221DEE91521418948CB2EA72532E6A1C273B764D3160EA072A48782336B648C12B6AE93B5AFF289F81236AF0F9A338E8EC35154DB40386949D6E32A71D635053D5F55990C92\",\n          \"dk\": \"62BA496B01A577F941C19004EACA6997F59F417945028925FFBBC684CA3BA6D1A1B9427387E7B7E84A1AD2E00418D471BFE304428AB3D0F1587411BA3971679FD42DDF418546B6681AC8AE63774E62F16460402A3A4730505A650D23B2E14534A1D00CF974A0BA035DA2E29A4A616F1E7832B3C37181967DDB5C37A18023EEB07B0E645FED3A34EF4544F336960CCA7B83A2139DD671C8369016A1BDFDB6980184B98E376A92701D36D07E6D0964ADEC327EFA08D0F84283A58895466A47EB084E042938450D5A2209962358A719B305844210971766C2A756D372B1228302601853C37B3F1AB45571C6F714528F223BB6F8A74F91BCD4840E61924872F7B7FF7469BE8935EB743BED787E4FD753D56494C44591B15990A4F9445856C7938CBD38D546CAD98179C03DB1973CB4BBC3DBD0AF79B28386240FC008357F346552F21ADB551219433E3950675F100E10345B942672205924A80607D6160BDE629EA14C012EC62817D28527A80BA5081F7867AD3516193E80B6EE726428F6409BC6B0A391A6DDA32839A9B6B9F23625DC1FE2CA6FA021BC1A44CE6DABB640E480CB0B5306E5A466BC4C16BB51CFA79AA5467753A5A47873A11B0C670F1C584C4AA39C91A76BC361F1DB598BA63FD3490517DA397F36C669686BC6122131E713AB8083C3F10869C80689B5141BD4BF1C92C22D9ACE8651AF5A236132428E2C2A064C977143528CF9233DBE4B8B913A69E8C34D97D5309A9B1EDC5915EBC42138C93005C2C4BF7C83E857379529A6FC124C4297568EDB7EE47C031308A26B8A17F3F0BA1D217142E300340945BE8B0A85035C5F926ACD21066B8248EFD730E842772996259BE076BDFB1398424360EB38DF029E9893A274E20A44A87CB4FA5E90A55099F96961AA5BB6E69913C7B9117697570A261A4A24C3862198D359114C767092824BB7B7684BCAB4B7017CA3172DE25C187B4728792A305C966BE64B7F2A8A22432136B864E75A14AFA433B7A1C617D80C3F20707124BB3B53CE17E605DAEA53A6677530305C56927D5F5C8493E33B1C37B4D796955CD4A7DC60C56A44703AAAA0894C58C872A67C630143FB29F5C2533614AC1ED6CA436463EFB38C7BF8A8DA348DAF4BBEF00A8E61E650CA9C6BCAD30CD330CC2D032ED7E42F41A9967C3AB568A0301A5A12EF70A2A6EA97B187691AEAC151029CBE350065B0C2239B67E303050D92A692237E21900BF2901A69231B14F04901ABA8D22A881365B943BA429AE31A72B8701CB07029E129B5D3BECF9090A70A5D3A86BE1BBABCBEF35592A7A74C2CC0560C0EC2ABA3095589C7DB2A5849CBCD483730F479ACD55B212C942F900407E46C0C584200C1CAD7D05C7DF467FD31C31E266A50C375689CAB1BE672774C77062018E74CA1BF6BC5F223802EF93CF7A1383F548196A9B4DE958C49014EDA8501BC8A56B2E1A2B0D46E18101D68816EFCE2297CAA5CC509399A0152232026B754A90FF104B9189426742C2FD8055191BC162B04A262C3D23B996C2AA867F3154521BB19E0A69647718FB76EA5B487C930A454943F0FECA6FE163F40557AAFB9785DA0B6450B0BB4095BA23AAFE83B731356B290DB64B4C39DEB5B30FEF38086E6841B660D41EA593E1C24028C60BC44A6D83696F88772425235A4F05A1847AFD5C84DEDC0B4C1D003F29AAC29710AEDA5053A4C0AF556C4F5508C45FB20141B1FB641AAA6EC6E88D0859553896C31A0206631750C954EE73B415CA163D30FBB75CFEB15714E3330AA346F7DE02014214C2EC5AE550906EC5632CF429BF3467FCB0327017703D08A7F3E9A7785D0C9D6377FAFE527259A0ED5894E2A8C766F688FE2574DF57108C81C60A8705DC7D102891437CB2B9988C879D7403967A83A70CA48D6F1B18A3408BAA13BAD9C26297CC10D920F0C53CAE4D85C6EE0C95DF22321F0A05894CB97C4A45F79CC002DA373AA857B65C726F54B03184934B11376852E9FF58099C72BBF531943EB5626C2C3EF6BCA145A2896B4C1BD29A353F49EC2FBB746134C1F50AE6D771160E64848396A5DA65EC32014F9669BB83AC52298B24DC2AB0BA2C82C0B4017B34F9D979D7D07659C4B76160999C47221DEE91521418948CB2EA72532E6A1C273B764D3160EA072A48782336B648C12B6AE93B5AFF289F81236AF0F9A338E8EC35154DB40386949D6E32A71D635053D5F55990C929492E80637971E303800ADF446B35E44F02C1B2936E5381CAAA9738F9F9F79B077A1D61917B642825666A5D08C5DCD13E5ADC0A5E248F28DA3A32BF1188864A4\",\n          \"c\": \"1077E1871719ACE56B2178A208B3F891C187DA970A51633C9996D278EB738375627AF9052866F15ADB21D21B8D0070A19A3024893FA32773D2E832DDC2480070FCDC03A61504857CC40E0E024AF04532E288F5F37F3877303263F4C66848ABD68E5D7FFBBA91B8BE624B63019D69088ACC1C37E79AFEDD5D1D2CD7721A0E5328AF2081C19417873E2B29794A2D2BAEAF67783B64BFD6E473B27E6B05EACC6079F4B8EE61C07FF13060DA3DC04B556307A1D6A7B896AB496CCC52C94897885E59061E70D12B4A9EB0B4C81F331CE3AE2B47B753CFD5CADF96E9C81EC90021F28F3FD33E2EA2EDB61B87D9B7894EFDDD968DE92A232A148AF1F0C31CA9419DE93ECDCB06055FCDAFBF655F2FEED26D3A6316BA259F7AC18A15893A95A635E364A1338C4EAF1F480B6E6DF424584F8DF7EF411902CA5A9A12FC440EC4E2E9CF0E1C3631EECE02A5134B793EC9C8EFFEA700CB8F6729C413AD1432EE4C8A92AE41D9FCA9D19D7871ED136EE3E0B8ADAAE428F0D4BCFCED8C107040D53C858DD2167E0415C98F46FB6327FE7D1B0359D8B3F3A491F4C708CB46064BF872D8830580D41AA8BA93E40570307A21554E5204284974F23287BD6A92D8A2C64A6F1687FBE7AEF7E224455F639BBE235F027DCF160D7249FF010F2BF6E1E358DA17314399C4B5129741E1171B0BAFF8E5BDD4A7BB8DA81F8D387BB32B8A3192136231C49D9A5B88BD1B6A30A9DA7B508893152FC5FCA58592F808E98914781D48D0CA7314A9A166F5154F9354D060BEFFDCCA00227F3BDAA59672820803FA83720D5BA5035B78E02E3ACA332DD0427CA7B2075D6770D8DE005947EC6E82389117D51C7186ECBA9F0BA3C81BF927AC6D75ACAA9826C612328A908E47684D7A97D49673261A7794EE63B9E99F378FDDE9580492FFFE99535CFC76C4D57CDAD1B5C51E751FFEFABE8772F6CCC1634808E2A0C9E09548AA41A267CE0086BB78B14163AFF59E45D603C495E3B1EBB4F2527A5B1DD4638A5DA9CF1429370E1A2F886EE35AC4287FDDC19297F7F96C223191698B35C4C78E1E4A46FFBD3B3CCE38FE63199BB8D46B1E\",\n          \"k\": \"F7AE95AAB26A52F3E8976BEEC50476D3B5FBD7ECF1A610054DC199A99497A1B9\",\n          \"m\": \"4F7798D88974637071717FCAD2C0ED5333945D51341FBA4BF1962A3915D986DA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 20,\n          \"deferred\": false,\n          \"ek\": \"42803BF789094EEA7D875BC10D50C7D3947CB77CA48A301323B54FDBC94E480372E54AAB12C9680765A3E22988FF42184741A440E348C8D19EE8E7CF87B23CE6944663A4B28937160B0C25943A8742F811D0E417CABA034628A11FA2A06E3A5F51E47EC26A9AA5209582F73F1C705F9CAA74F1F300FE9049030061B9C61DB9999E1498BDC9250EBD324F61827463A6A5104796EFF25405AC687092B78248A2F9171E891729CFC11F86316AD6754E8AD249E7849958B2AB380783CC4123BBC087EDF1075BA8304E339F01242C15385A4007616D873587FCAA5AC786731447DB8C537F0CC64731A790E1BF23412AD9342F9654B87DB07E8EA506C534611F48A9453855CA669EC8AB548F648F8AA9A715D0BC09757A285320DC666637950A691AB8E2630A08B59258205BFCAB9DD37C94B9F4706A1114FCCB806E54751A12576063666252CE7F5041432C095CB469B5A1A634508CF1E2398716272E9570C1790FB53A3100D57B5EBCAECC6CCA50B4656F5219DF477C8AD4B7B727BC2BE25246C526D06B8DCC30423421667E554DB0ECC21837539F43A719CA8952A95B143745F79899F7D1C79BD546B6559B1109BF6D60275CBA5DBDCA608FE34CD96369D67B880CCB00F4B3CE6D3C6CD3A35DB44A6F8E440215B027E03109A6546662789B6A096784A0C64A1A4F07F981CFD201F3687691955184684E22C872400749A180CB5B6317599316790A7ECAB711861469EC794FC894AEDEDB90E45164413B82F18CB162769EB92A47630A98FDFCB09C94456E74C0FBE345ED88A43466860CF0472B6C9D4780AC4CC7189F77A19684C54789A9CB99CDD46534914B1990659E71F2662933148E13272E723C46B4809CB32A42D378DA7688F21CB53DA08AD3897ACBA940640089C73049B1E92C94C943D5EA32EAA19AE7EC072527912643ADCB591CC86B97696B98386615AABC1D32D23D4D22919E35786E77A4E7E30C2F6730DF129089943FFCC1678AA600AF4C3F40741867DAA06701A8DFE6421CD0A5DF34C32C933C9AC2B141C5134A7BBD469266D952AFF44995F8904A148B363E3C2A13978A049B968E24051FF6E312DF77FC5663502B0187A5588C1D84149B2DB835045F9BDC1F70\",\n          \"dk\": \"DDBA891C987B8439ADA0114885B54CB210ADE141C92E3226F4B25E1303B93A68092FC95B5FD9BB80805D8EB2869AE6320A90458E4432E6510ABC3C478C291B63F02E476235390C0162468145234744C6208D642DE2E56031A95B83843B7FBC2E20A83AE5229A4ADA7DEF77864B975D475132CFE0C94BCA0762E369ACD52BC8C1135BACC67083819EC229D8962E989C1FF548A127F86E17C60C8695B592D1C9C3689EC3A9096970931BEC56E99499FB3AA982F663F67CA547115D320ACFECE132B3CA9CD5D223EF20B39F7B78E1526E82C816436BCB6FA822C105319AEB9D2B5450129ABD8D1971A043AC7BFB129B5504F35BCB13DA4E1700CB215A3B7ED6512F558D62EC83DAB765D0816B4D085C09E0035E830996EBAE1475B86F39B23613BE9E403A12A7ACE2DCA602722C593043301A35B4DA84A3BC1D53107F3C1C6260323739C27CE09566882BBE3B31C86CF8381EEB10D86167F300A6E510A70E0A881F282FD8C4A1DCE069F417753F069EDFE46A91837C2FA57D737B425B7A4D0544B6DA399368926296C98F7C5C2532374B95C03F3EE15E526B98E2C48EEB3718DCE2C9FB760147434D6FD1BF040AA5A329867DE87094BABEFA563759E49E1A94A737D6488E2280DE5BC827A90A39930BB78CA70D33A476C65CC9F6CB2F8BBB7024652E43212821568288533A51627F8765E1B64AACB86F08333A428AAD4902518DCC3F77563BAD858CA32515BFA8992A5304D8B69E5A6C79E40B7D361B70C2762BC359CB08401AF7967D8C3186EA826A3C4C0748F67E471B92D3216D39091F26EC477051CCACB27696D9AB918904D75743A709CDD09460AC04863B5412E6E935CCE23B3F6217FBF2CEAA6CC37B8C2B6719C4661510A88BBB74220622B589EBA490309A0FC8E7998739CF6205883BC64B86A317A3E1A86F0C65348C2D62A741E80A8AC416467133581BAB2EAA7B5B88025F4711914ED6996DA29E429B5A03C358192C049AD17E6280CCC231AC0C075E3396AD9258AEFAF757FA23259CEC41F5E7CD7AF7B12DD8219AAA5272E7CC30C240B2E2A7B92CBEC265149EAC2A42E3B92C2C0242803BF789094EEA7D875BC10D50C7D3947CB77CA48A301323B54FDBC94E480372E54AAB12C9680765A3E22988FF42184741A440E348C8D19EE8E7CF87B23CE6944663A4B28937160B0C25943A8742F811D0E417CABA034628A11FA2A06E3A5F51E47EC26A9AA5209582F73F1C705F9CAA74F1F300FE9049030061B9C61DB9999E1498BDC9250EBD324F61827463A6A5104796EFF25405AC687092B78248A2F9171E891729CFC11F86316AD6754E8AD249E7849958B2AB380783CC4123BBC087EDF1075BA8304E339F01242C15385A4007616D873587FCAA5AC786731447DB8C537F0CC64731A790E1BF23412AD9342F9654B87DB07E8EA506C534611F48A9453855CA669EC8AB548F648F8AA9A715D0BC09757A285320DC666637950A691AB8E2630A08B59258205BFCAB9DD37C94B9F4706A1114FCCB806E54751A12576063666252CE7F5041432C095CB469B5A1A634508CF1E2398716272E9570C1790FB53A3100D57B5EBCAECC6CCA50B4656F5219DF477C8AD4B7B727BC2BE25246C526D06B8DCC30423421667E554DB0ECC21837539F43A719CA8952A95B143745F79899F7D1C79BD546B6559B1109BF6D60275CBA5DBDCA608FE34CD96369D67B880CCB00F4B3CE6D3C6CD3A35DB44A6F8E440215B027E03109A6546662789B6A096784A0C64A1A4F07F981CFD201F3687691955184684E22C872400749A180CB5B6317599316790A7ECAB711861469EC794FC894AEDEDB90E45164413B82F18CB162769EB92A47630A98FDFCB09C94456E74C0FBE345ED88A43466860CF0472B6C9D4780AC4CC7189F77A19684C54789A9CB99CDD46534914B1990659E71F2662933148E13272E723C46B4809CB32A42D378DA7688F21CB53DA08AD3897ACBA940640089C73049B1E92C94C943D5EA32EAA19AE7EC072527912643ADCB591CC86B97696B98386615AABC1D32D23D4D22919E35786E77A4E7E30C2F6730DF129089943FFCC1678AA600AF4C3F40741867DAA06701A8DFE6421CD0A5DF34C32C933C9AC2B141C5134A7BBD469266D952AFF44995F8904A148B363E3C2A13978A049B968E24051FF6E312DF77FC5663502B0187A5588C1D84149B2DB835045F9BDC1F700F0ED35733D6D2807A9D1358FDEA6AAB613409738917AEA1C9F4D0CBAC25D0298A10A703C91D253B506276C2E15E683FF297EE8713F9AA8F400F73AFB9DBB392\",\n          \"c\": \"4391421C7C0C25DA903B2A944EC32FAEC0E88682FB3146AA621952E3219016F2FFCF97EBB7C7D6EB95891350EE783147BD5B0B1B089743DFEB15C4D81D6BA42B119A7765A73F19EBB39C565D2564EDFF9D57B2C48E8F42DC891315198D9EB17A9C5B5A9FCC169EA8695D1FCB82A96F79BB5432D47BB06106A9AC0D0AC91C3A23D28FFC19971041716D6688759DA314D6DFD40D087489E85780D7BA66D9D526E70038A5DEDFE6576DD240E7C3E3A629606632B71CA08CDC9206F593B51B80190364FDE88448EF5F110E650DE902C27E48BB82E9F2A007610B671AE048F29119FA07A98C86A46174598E0DFD6BD21C8D59C95408600D5181D600EF0BC302ACDF00F99E6D391257432D314696E4E12F2FEE1334574773F28EFFD813F70E9327D83FC239D04315B1F9C95C4C214B71946A733503064F3171C17DCD219DAD8BAF21A31EC0F9817B6A8B3C4B73C43B70357DBC771955F797F8BA28B56F31032376044F3BB33EBCAAE4AF9A93E2584A142008AE3A9CF75ADC2B3AED29ABCB8D03B28DA272AA5E4A695F9E6CFED430EFF445881F9208913A2E0CD61FDB5BA029D3228AB334EE9CBFC730AF95161ECDD1852E52C41291E0CF8ADE3790DA710C5307B5EFFF0E528A9FE2F6C2027E52501244A3E29CBA29E6AD9447AB43F5B4FCFFF9F3A8E7AC090CF1C6D2BC85B39DE79153E7BC36EF2D37FFC98D9BB21D37AD41E457D5D4E36E7C128DEC48422DB0E26D3E76823687F39D43ACDA2F77531612D449295D1740EFC6AD532C233F2CE6A14121C62171DF4B7166355E1F1E939FD597B3038F54AA056BDEEDB25026998E1D0C047C78D648C2D3373782E1862C8BC0D9BCDA9FBBEAAACD80104122091B3AEA9EB113533C75F1C2FDBA188A08DC549D229F408B592CFDF438B61E8321A367F6956CCA81F0E13DFC3CBD1DC9FC1504307A3F14843B4B3E09571E26FF61F69F2775BEEBBEDB5059A3333D5BFD5A7DECCE8FBD89E50B8CB5C52779A9B9CB19866560DB4EE457E3D18991A561268869E47DAA00C59445ECC7B683171CE81E58CD4FEFFD93E31D5EFAE77CCCECFC995A16DC190F59F91A\",\n          \"k\": \"5418AE44ED01EC65F14D5CDB12AB6004B35744E935AC8A9C3D8D607F946BB706\",\n          \"m\": \"E20AC1D70FA6A2C8A286EF0E3665C79668A5E6AE80197BBF13A0D0EF553ACF1F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 21,\n          \"deferred\": false,\n          \"ek\": \"39B893D833BC95E37373D2C71FCA4336AC84DFB6460F156174B7B95DA92351468EC97439F2B88656F9B0B944850F499415981A66B76333402D43EC30F511AFDE7623BC00A1C58136484350A15824AB0A434561CDA2305C815318D204CA0315C5C9703954224522E76DAB0716719A06F723006D66A2916941139B1F29E53783E6BCC568628D6B0A0E72A44D3C19BAF35FD6FAAA9B2396CABA9B3BEA5C901B836758671391304E5330448A62CBAB155811C5018064FA002A73A71EE6146458DAB896B716560649BB823BA9F73ABFE44E80E1C4D0D58E8C9B397A5841FF9C93CB8488851C482C534BB710B27C6C9CF7A15356427EE8F1B40815A9416B233F4A05065495D4B88BD2A42A36DAA392A9CE49D67F4017484368017FB91ECDC34488F463B99A6CE60359A7F3C32D3A3DFAB70817726B73BC1C36FA4CAC3A0EEC9C6BD3A63DF9957502076150C4759F5362BA493F71D669A724C79788532DA797445942FDD214C3A9103738C00AB54CDE3639497A951C03A70A0B01182B2B9C8C889052857A2A541EBBBB77C78F90377EADA6CF21796E44EC4D5BF03BBDE6773E571785F1847252B1D7182BB4B08B41C54C889072CA0922A56C19CAA890D7F899EA21C9186A3A7106458BDA086FE017640B77FF554FEA32A2C2297D82144EFFE20D7836180CE46FDBDC18D357B6FA7ABDDB2858770598C03169BFCAB9B8794BCC48B779E87672D83F62E04D2B42B7E31451273BA0362B682C955C382442C010005E66A2644550BF9390FB427E4839616AD8C8342ABB0944B3ACE932FC134BC6EC7C16D35427E22DBC4A454D9B09FB49AA496BAC88909F54CA88E1747E1081AEDB23BF370394D3492F5B6AB02C721FE3E0A199AAAEA03A91EA1C44FCB70BD4140912AAAEC9F779DCB4A8C0CA9D903C0FA15BB7E5335ECB3724CE621D602772255775EEF07ED060734E7365ACBAA557E25AEC10A27EF405E1A431AD8052E0B752A08346D1961134C77CE4FA356D66094856141EC05FD7385D009000DC6C4F97A91B074CA154773498F61C646951BA5B6B5D50C4E35C6206A6C33043308B474A7CF587C0F07578DB4D6FF52DC654A5BCD997296C79A97F8C16E5667F527DAA3ABEC018CF1671\",\n          \"dk\": \"F5A19CCA8A702ECA16162166BE451187A9300AB182DEF2987683383B3861ADC83BFFB41FCED714706AB26B1751B53B09282500F3320776A4C75F22C7710636C21B008988203599BCE9D53EA8E044E9F37720D2C1E34101186B896FC590A6593AC6B2131A093185C82D61D58405780D16927BF7514B9814BA5CB42B4AE66A37C55E06044CC0CAC774B01CEC02554CC40FE55326A612AE1DC76D7BD6418FD01C7ED25A15AC16F5B8129265C54B0065758390EAEA0F69B212D33B94A4854E912172BEF1AED684AEAF1560DB303AB1FA0146C715EEA9950D317A87953730B32886C164FD7845DDF6B3894A7460509DD9122B12A3938EC40649537EF21592BAB202500331ECC1A39C5AC2D63A1139AC93E18886F68189078C7090593584D59C50D12A2B5CC08EC6A9FD33BC3C2391A7C3BB4FC51F6A536D962C6AB3EA7978D57290719A80A8926A812CD034422CB0CB49416ABE61B03B041E88882059624843679AB699ABEC6988D918710F5B3FDB188B6031C7FEA47C292A04E05CA2961BA4263C298A06956631705254AAB817C46903B537328B6233B06E9530F0AAB1F3F9C16BC6276B746DC0F44FD9A476CBF23DFD27C45B4B56F8B5BF9AE0092028CE72819DA1C6A5FA48942CFC0A24B04ADE63B33642B5B112CECD370C3F3930FF6305B1B0A1D2C490DFF73706A8AC5D34B21CD8AB8D3111559B915634430D9C78460961F805214DD2434411C1C28C769DFAA9FF19C3E240638181B5C83422E5431028420F74A707914354881938BA842228D2C19440CA99DA68AF9372B8AC6D68A34CC75832C41B1406C66EFD1161E8D0B42CD27EFB18A162006375D40E13E24FAA32489DD91E0DB8A32F11A4BB9139E0123C5DB00D5CB1BD4617809CF3214C581276312BA0819699841C6072128A8C3046656AE6815B78F61AB3890FD7E2BAE1A2A37CB27D3CFC3E4EAA84F65350E5DB36DF499FBA17839192901FA7570EC72381A86581B69760A277A8E63EE3C16D0FC45EEDF4270EEC32733B6D0DA8BFC0539D64136E4E260725191B78831F37F942018946C5113807F9161BB89F6A168C6CE2991DEA0F39B893D833BC95E37373D2C71FCA4336AC84DFB6460F156174B7B95DA92351468EC97439F2B88656F9B0B944850F499415981A66B76333402D43EC30F511AFDE7623BC00A1C58136484350A15824AB0A434561CDA2305C815318D204CA0315C5C9703954224522E76DAB0716719A06F723006D66A2916941139B1F29E53783E6BCC568628D6B0A0E72A44D3C19BAF35FD6FAAA9B2396CABA9B3BEA5C901B836758671391304E5330448A62CBAB155811C5018064FA002A73A71EE6146458DAB896B716560649BB823BA9F73ABFE44E80E1C4D0D58E8C9B397A5841FF9C93CB8488851C482C534BB710B27C6C9CF7A15356427EE8F1B40815A9416B233F4A05065495D4B88BD2A42A36DAA392A9CE49D67F4017484368017FB91ECDC34488F463B99A6CE60359A7F3C32D3A3DFAB70817726B73BC1C36FA4CAC3A0EEC9C6BD3A63DF9957502076150C4759F5362BA493F71D669A724C79788532DA797445942FDD214C3A9103738C00AB54CDE3639497A951C03A70A0B01182B2B9C8C889052857A2A541EBBBB77C78F90377EADA6CF21796E44EC4D5BF03BBDE6773E571785F1847252B1D7182BB4B08B41C54C889072CA0922A56C19CAA890D7F899EA21C9186A3A7106458BDA086FE017640B77FF554FEA32A2C2297D82144EFFE20D7836180CE46FDBDC18D357B6FA7ABDDB2858770598C03169BFCAB9B8794BCC48B779E87672D83F62E04D2B42B7E31451273BA0362B682C955C382442C010005E66A2644550BF9390FB427E4839616AD8C8342ABB0944B3ACE932FC134BC6EC7C16D35427E22DBC4A454D9B09FB49AA496BAC88909F54CA88E1747E1081AEDB23BF370394D3492F5B6AB02C721FE3E0A199AAAEA03A91EA1C44FCB70BD4140912AAAEC9F779DCB4A8C0CA9D903C0FA15BB7E5335ECB3724CE621D602772255775EEF07ED060734E7365ACBAA557E25AEC10A27EF405E1A431AD8052E0B752A08346D1961134C77CE4FA356D66094856141EC05FD7385D009000DC6C4F97A91B074CA154773498F61C646951BA5B6B5D50C4E35C6206A6C33043308B474A7CF587C0F07578DB4D6FF52DC654A5BCD997296C79A97F8C16E5667F527DAA3ABEC018CF16717815E832D512DB0C38A08C78F9C7D3CD3010367902146A12A335AD3148A3C8BBDBEC6DE5ABF972F91D59054FDF0B3F927DDD6EC3477C162C2294048A4E6C3FE2\",\n          \"c\": \"9CA67EEE0B5C186A08356C38E33B9E7317637F3CDE3EE9D6E04C1208F9B9CF63386553425BD51F35E523180B29E3DDB8161F1FC632528A5D5AF0418F5C32B767106A774E5D97047B0A49F6C9FE2ACD3C12A6D45B49BAB8A4C95958507BDEEE88B4659373F8A1D605744F5B65AD2E5A5EA081AD2C55670793CB78691B2BF2CFBA1FD1BD6AD4D9E87FBB64A52CADAB26B4D66684AB2FCAE330173F864FBC3B6461ED1B4EF1BB054D59F2CE2B8C62CA06808B99AA29AB2BC941026494B3233FB5AC8B5E200DE2F2F40DB93C0F567348033C1CBF08D491F3CDF59835791BF4751B4A22AB312A7A9C6FAA6B3FD5021F10F8F3D5C0CCC40483CA28322CB75A80E0DA05BE5D848F43CB3473864B26591C27DAC580D354A9D2DB35C9BFF76B42DA9675A2CD63075F33C2A1D1626992D5ACFAB3E7DAFB8F017F54757C26074DBFD523F18C7757ADDB23476528D540A96EC669E6BF0C1DDA200BABEDB965511546F2C96024D344EF0E17A4481ABFB5C2C07C8D23757321BFC9A58529D5B71428D08A056083E5A027BB059E2813AA9A015BDF7C941DDA306B3C54A08D1613109716F11FCA932EC55A4D31806BE21C47CB1B10A88587276C57CF389CA28AD40EACEEC94A57A5361007B5A85F0A44E5B5E8E354B2EC791F42BDD1830CACF5722788A48837AAD2A2DB34E33B56F9C986DA6E9C485FA96487C1AB608CE903B6D335C47B1ECB129D39194B99DD369A122C6A16948F689C94C8A54D6CB4C5E39D073570460BBE04AEDDB0D0432B69E1724DD61A8941B9C2D26B49ABE6CB87FA1D093CD6DE08033C77C11808B0315D8B347E7EBA33537F99F64250C65690F8AC19951679C580CBE6E36A7FF0A624FADFC84B220FF5EB7B9BA306B4A03DCC305669C1DF2210FE76024E21904E1950446EC85FD5A04580CBD9843D5BE7F90F82A901BDFEED370AB83D416F92C58B5CF143D4306C9FDF43FEE62B6D1A0248C2B6F305331F4159382D92AC6388614EC84729450B85B7DDCCBFF9A97403B186DA21480DFD1DDA6499600C326B3A813AE123F8175D2AFCBCD5A519BF706CFCDDD6F36A2DEE5FC3F34263D8CC\",\n          \"k\": \"89D60F46DC4A11DD81C284E97631F08DE239C06B157529A15BF9B53C9EFBF9DE\",\n          \"m\": \"AC25F29AF8D8A2DBD359600C8A500144D6C0236D729DA016C3F116CBBF621002\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 22,\n          \"deferred\": false,\n          \"ek\": \"0AF62D21757B6AF2C9853421D2D74E0DD86A416A228A9035299C27B32528CC526B371C56ECE17C93B7CBBDBAC84D35441E6B3A17811F3C0A6556825174AC3C2BA51D53F176F027C20FEC424F7539ED3C6C7B964260489C3D8BBA94323630B87E856064E3BA81D3F018516971DFA54AC903C9AD9747AD8C5DB35780A033C385C23CBCB21D26153F93626C41237A02185C41C3935B306FDF568F453B255072348D9716AE9CB9B9ECA6FB0B78DF7927D093A3BCCC7F7BA711B27CCDC0051BE4A31BF3A0B224E0770BFAAF26F599AD190173A4A012A52C29DB006CD37076A12CBA65B679C10455E5442D92609DF209F6556CA36BC852D3A282F14AF3EB2A852AA268B1AA92346631953204F4ABD9726338B8B275A26ECFB80428881613C952545B082AC2BC75699F3285AC10905A00449AE6A42B43D1A4890155A9080A587090F7C91FD97833E13B601EEA06104CA41C408EE73C055EB1CD9FB0934E25660381518758CF3A8C95BB0397931427FCBA515D320D423A7440459C614B9ACDB25A9C143A6F704541A339D9811409D6BE200429B33674F61CA6626C7FB879001AC65F08C18310A4BCE685C37944079DA907316060A64069F87CC5D9FB3B56604AD8A52E72A1405AF5C549498F147CCA4A7749A37B5D96084B01AA3241288BD22592AEEC778FAC1C4A2BBF6085C2B5DB7D9C00B9864C480D8826C2E02B246AA945EB61F9425DE8E604B7D76486418FF0F5AB59BC6EEBB4932D6075F667A6AFA74BB66255F03C6F15A63818E759A4B05F480A4C0CD1072E26627513BCB85474E89676501AB2ED21A8F487780CA8CB8560B27A766213092DCA0B02ED3A24C8351EAE88947A764F142C0E24B192A2421367265C0B37093E0A69AE31756C5906845A8C7D01550E07C72FBB18DEF56E1E8B320F688110C4C4DC7680C9FA05D5B1AB63EB65450283ED6A52F9F535FDEC31C207A483413725022A2256663E2657977660AE63CA8671A8C343AFEFA8C43D9B8D0B93B458416CA4C5B87D121469EA5B520ABC0C079546BC1353757AE2BB1D301974BA03C86F624F34A4B481B29036F59F37DC2D5C1721EFBE0A0C22A966C895E5198A91F916DE62C0FB3A769806AE5827AE6F358D8CD6\",\n          \"dk\": \"ECC90EA4A07BB0F318F976967D17B40206A6D06C00FC1CA6AF95AA38712DB92093E8E94CFF6450BE071127ACB7AF76A5FE8A71FB511488E57DBEE396D65807A5D0A86BAB9DFAF69DF38A362E62ACE8F7AE7CC638931716F2411ECCB621CDF181C4677D66793AA969BBEAE71211991DAF14968C95754B2BAFC6779BA5E4697094B28C1340276322527C7F259B4F39CA48AB2AAD90B2959BF1B12F983C0B3B9014A04392902C5C47A92987ADFBC654BB87C4DE63AAC4B2C45A3BC37972A607F491C0D891FD7BB9FC96032850BD110A2B09EACC134B9E99B5A09BB11D9D5163B880CBD73817085C68E571AAB45439A79117F7DA5E81735808F97A42249728A0B63834001D1C008AABAFD72A068309BE74D49C7BEB0EA9251BE6C1C7323B84402C725C212D361BBA03385FB7641AB1D74B39690DD1448B6B01A85F82A7F43C22B0E3962214246133750D23CFD1776FF64B4D1FE17B8257670379320FAA81447395D7069A74254156E900C9278B9789629EB33CE7C0C867992596966ED9875579CC2A525BAD71E920AB5B0A69E69995152BABEB1D4272A04F3283D988826AD0AB24E24C0B505EA5AA02EAD063D7B587603383148521005034E6162106E29A7EC3926EEC024DAB8AA0A5768C7142EF56A5AF17829E505224A2C3F8526DAB6A76E714766D2450D60A68D23AA4FA392A2903C276A965FD1A15693644515A336F6115CB050709855E95035564157308F3062FA03B0DE2C00269BD8D414C6A574D485061A6A28245375513009F205CC1402694BE76338D0795849AB011205E880537E0696E6F4636CBD9ACE8E279A8015D57108BE0A638B3D8516487BF763257C00A6175407433A204C099BBA4972EDE1763EA5889FBFC25742CB66B9360A6A5661DE7BD4DEC856E55A5BD354E01DC6843B4C306B957E72CA528188403E324AC69AF09820C74E6A389881CE882B86517BCFA881435BC8817A1489250A35EA19B80C62CCE52A0A6935A89E47F658041BC93B2B979A7DE815D97D15E5B55310568BBDCE78C6437C848D479E65C60B75391FEE94683BBC379EB83A545AD98A070F281837E24090AF62D21757B6AF2C9853421D2D74E0DD86A416A228A9035299C27B32528CC526B371C56ECE17C93B7CBBDBAC84D35441E6B3A17811F3C0A6556825174AC3C2BA51D53F176F027C20FEC424F7539ED3C6C7B964260489C3D8BBA94323630B87E856064E3BA81D3F018516971DFA54AC903C9AD9747AD8C5DB35780A033C385C23CBCB21D26153F93626C41237A02185C41C3935B306FDF568F453B255072348D9716AE9CB9B9ECA6FB0B78DF7927D093A3BCCC7F7BA711B27CCDC0051BE4A31BF3A0B224E0770BFAAF26F599AD190173A4A012A52C29DB006CD37076A12CBA65B679C10455E5442D92609DF209F6556CA36BC852D3A282F14AF3EB2A852AA268B1AA92346631953204F4ABD9726338B8B275A26ECFB80428881613C952545B082AC2BC75699F3285AC10905A00449AE6A42B43D1A4890155A9080A587090F7C91FD97833E13B601EEA06104CA41C408EE73C055EB1CD9FB0934E25660381518758CF3A8C95BB0397931427FCBA515D320D423A7440459C614B9ACDB25A9C143A6F704541A339D9811409D6BE200429B33674F61CA6626C7FB879001AC65F08C18310A4BCE685C37944079DA907316060A64069F87CC5D9FB3B56604AD8A52E72A1405AF5C549498F147CCA4A7749A37B5D96084B01AA3241288BD22592AEEC778FAC1C4A2BBF6085C2B5DB7D9C00B9864C480D8826C2E02B246AA945EB61F9425DE8E604B7D76486418FF0F5AB59BC6EEBB4932D6075F667A6AFA74BB66255F03C6F15A63818E759A4B05F480A4C0CD1072E26627513BCB85474E89676501AB2ED21A8F487780CA8CB8560B27A766213092DCA0B02ED3A24C8351EAE88947A764F142C0E24B192A2421367265C0B37093E0A69AE31756C5906845A8C7D01550E07C72FBB18DEF56E1E8B320F688110C4C4DC7680C9FA05D5B1AB63EB65450283ED6A52F9F535FDEC31C207A483413725022A2256663E2657977660AE63CA8671A8C343AFEFA8C43D9B8D0B93B458416CA4C5B87D121469EA5B520ABC0C079546BC1353757AE2BB1D301974BA03C86F624F34A4B481B29036F59F37DC2D5C1721EFBE0A0C22A966C895E5198A91F916DE62C0FB3A769806AE5827AE6F358D8CD6CD2DB91B660C482E6C8B2AAB016B0354DC138DC2BF97D5F960E1D8CC51F09806BF737AC0198871CA09B8C1E4928C4F51B47816A69F4174A4BC9A274F2E10D051\",\n          \"c\": \"398189254F2C82F3B9F6826C377BE31222C4E199954CE883CD44E135BE51E8B1A767969BAAC6FB3DFBF59BF38F2A005798D45B1032FF660C37E1AB24E629D84F79B0673E44D12359CD6632BF4AFDB2ECDB2A1BC960E7B7E12ED89116AC5423ADE1AF5CB43FFD173D2878F11BFF604E8D2B59FF847B570F52D5A5048D16038FFD3A6A86F00513C8394434DB5D87019D6CC46738678A45577698DA6E13B466504DCAE736EB36C83369ABC434B8296C3D9BAC5C46C700F5D0CB0EA37A64017E0DCD82A1301649ADBB8339E7F7C0D6CC42B1EF2690F769BBBFFD50AC546447858CD1B46A31E43CD1133691C4600D745BE6BAFD4E9A4B08E4147DEF63E52516FDC2AAD98A77011876DB533374A85805AAE72B25F0A1B30331750914E79570ADEC5D20B391EEED8C235D295C5C7B3A6FC9C6F8D46EF0F2288785BDB4A99BA461BF2EEE99E58BF46A34989DD128062B511A4724FA7A528CAD251A3D4144E0CC39B89DB093A07FF65204B3A44FD20079ADFE17AAE7AC3306C79495338B73D711C11ECD0BF5BACA4F51BCC6A8CD54EE1D339C146241344433B91436E54E17B7999C3101F4FAC0C6D765407A8F7357DB41C43E1E899C5A786ABA6FA1CF216D8C795A98A9A4E6F4FCB6BD38D82A4AE26E556D672504CB8C33ED921B6CE69FF9B7E1F29FEDB7926956278C1010375360E9F149CDEDC4F44C69D18940E85EFDB467C6D7979549882B94BA635694EC91AB5D3459F244DF94863C180BC623C6FCD5297A1797A272F6CCE06EFCDC1F24E6FEEF30C30D50605D7D7FEB2886854281F573B0ECA200739B307706ACB22B05A6755C50FDD9DDED42442990E9F34778B6615DB04A3F39EE3959C0407AEAB90B580ECEC910836C6E2C30561B056BBCF04EB576284314135CA48630155915195B36039B52CD4B546882F536E2B71E5E952AD560059AFA6DEB52305DC8923FCB52E5C8031596E9596BCFF1F0D05CE5106969532F040BCEF32A7FFAEED70A12050FB21835E3BBEF84C548830C9CDEAC86BC6D3AFACDA53CCA62ACC28ACF22089C70014469D22E967C81D3D7B8BD77F50CA03930B7099801CECB\",\n          \"k\": \"66D121707FFB368BC5D4C73FD24DC2DFB742419B203DED2B3E157EE56044C128\",\n          \"m\": \"7114A4B4195826CFF174FCB75336B25D4D1BF2224D585014CBADB0C4CFBF7729\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 23,\n          \"deferred\": false,\n          \"ek\": \"F568158F1981259934435BC2BFD38A4A100BB9826E429312EA3CBF04A5397A723E740112859C74CA0C2783D5CC9798AB3C273D3AD6911FF76BF7235AEA38C4459761FD4262F483AC5D081900FC1EA3337439A3B4A2787E0D5763DDD2100E6A60DEC12B07B84FA8226017456B7F65679FFC31518C172F93895CFCB75FB74054116CA5662886BBCAD5F6B63534703E8B0DC807952EA32577389F94A7355DB079639AAF39B13D9F95CEF9AC9BC4B3AAB0F7070358644ED38C401A73BC2B2270D01D1C207D7FAA5233D74296302A90E4551D380C715265DD0B22F94681EE6093450186738B32BAA1B1FCC3C1F0A309312B662D25120F6B6D31B2B29809537C4A220D303DB4E5CE51B606DB64CA4B86947056015E8857220B8AE4822F28D31A37F63A89644C4E3A797FD7236A52640CDA3DA6D1782E933361A74B2CF665D04A07DC3843A40A0D8C526BE790721FBA16492557F51B6B6904998422B6848C93EA5043A782312C1250D81C5BD0407F5FFC99DF3993D91A71AD8499BDA29427E51BBC93AF43AB49E9062397F58CFD4B8048392AED850D95BB2F26DC55D3D8AA2A7249DFC35DD145569BA276ABA90B78BA143A876C3D97A66E02BDA3A34BD5DC4918F776E2E2C09E46047771B8C6FAB8C5215BE7514900A77567896899E94282E326D4848606FA8F2D350509B19251B4CA4B5B016C4181E8091D501327CCB9AFACD14C6FE4808E6802A8875FB786A52B70359845634BF1C185C887F1BC6FB8221347DB1DEB6C183C412C6D3684A3CC244D37C1C503B69DE67D4B02773DB8096DE18BC717CB3FD9CBA06A370EE84B9ED13597E6BC4CF88474E6956370C88078A56383799DF76621183C70D522DCB8BF07A9546D9CB9873997AFB51F94B49EDE924643900E201097D9853593F2A7E430AF5C57C754142AB462A12D327D04115540B505B8146BB64347A22BC71C02BD2E9C8CB360AAE5C9B173BC2E844CABA9659C2CC695D0019C135963987A5F019989B3064F8A0A8E4CAC8C7909AC4BC028C4AC4750E8C4FBC1433826AEFDDB2315002A27A2B43C539935649890B31710C56BCDDB64E7866BD07009CF11E676BD64E680645DFC88E803063DFE292C2047525EE37B4F3BF7AC\",\n          \"dk\": \"9A6264C4F2200B6775B81585D2665C5FB3711C7CA596C23FA6883AF47C8FF44158278A7437A32488F5C2AAA9A763674ABD01056A767E1CC48D327B0639E85148C8A60F97AC7EB55BDDF389057C72EAB6419640C4EC7174E420BBEAC4C4B9B8A1A726CAA6A27D226529245A6BE1496303395ADD725CCFB43FAA37206011931852B2E117AA93CC99EBECA070B6B9F5C28B60A3469A6497423A638D6C03E711AE1B64CF613824957973F0E30253B355545206585113A4921DBC32336E750CB940C574F7740E145CDD41C6FB1AB4C7186A3BE12026F1CDA6D80A15916CEB8C78A95A094B330C83111F6EB50A6EC17129363F6684443C09716D129ECF0A097DAA50CB021014228E77F6826350C03F16388BFB7578DB3B368591397650ABF39C947736D2D49E1D4625A9DC3CFF2B14520282FE9AC9E92C9C72745DFD1550474AB13510BB0709A69BFBBC7CB2BB1C4BADB45709F093AA24D09CE75BC8DAC99FF5243E98872233E0B51BB87C7785B5BF558DB3CC5CBEDA8D710039D1E260C878836E4A493EF67534F4C269D090F5F0906BE7117F6B848AA72187DB8DDBB62728143A4F569D31439D72D7085C299CD2F77ACB2C8D8C7C33D4860BD9C5AAE7821D1D67BA3FB81FAC8AB513F035816B019B5C464DCA4A289C1E503654E0A8160D1C37174C565EB876644A6EE3FA6107369A08D27EEA331614406ABA1B7EBD91AF36ABB2A7F9870272CEB7EA69DC0431CA1960A8352BDEB4526E729F49AB6428C15C00EC30D6A9757A953AC4D51063973BC954BB30CA1A37C57B1E402C9E7AA653679F88B6CA0BEA8EBF992F7EB422E90697E6EBB1AD72C754130088759E95091AD4DA61B5350138D9387E5744D2B8049457A0E07265A42961ECFA043498ADD3FA517518C8FD8C0C16A059B4E02BBCA459E3FCABC68C0E656C2AADF50DEC8624E14AB964174393F0A152952069160F1A3724234316C290C398F4AE3F7C3D8720464A4125E3F831B3ACAC315B2DF6F58160F98AFEBCBBDC85702584442DDC9306E66BF234B5017B9AD9B51D95C7AB7431428E36C31F8288DB21B8771B12784866A4189FFDA8B7F568158F1981259934435BC2BFD38A4A100BB9826E429312EA3CBF04A5397A723E740112859C74CA0C2783D5CC9798AB3C273D3AD6911FF76BF7235AEA38C4459761FD4262F483AC5D081900FC1EA3337439A3B4A2787E0D5763DDD2100E6A60DEC12B07B84FA8226017456B7F65679FFC31518C172F93895CFCB75FB74054116CA5662886BBCAD5F6B63534703E8B0DC807952EA32577389F94A7355DB079639AAF39B13D9F95CEF9AC9BC4B3AAB0F7070358644ED38C401A73BC2B2270D01D1C207D7FAA5233D74296302A90E4551D380C715265DD0B22F94681EE6093450186738B32BAA1B1FCC3C1F0A309312B662D25120F6B6D31B2B29809537C4A220D303DB4E5CE51B606DB64CA4B86947056015E8857220B8AE4822F28D31A37F63A89644C4E3A797FD7236A52640CDA3DA6D1782E933361A74B2CF665D04A07DC3843A40A0D8C526BE790721FBA16492557F51B6B6904998422B6848C93EA5043A782312C1250D81C5BD0407F5FFC99DF3993D91A71AD8499BDA29427E51BBC93AF43AB49E9062397F58CFD4B8048392AED850D95BB2F26DC55D3D8AA2A7249DFC35DD145569BA276ABA90B78BA143A876C3D97A66E02BDA3A34BD5DC4918F776E2E2C09E46047771B8C6FAB8C5215BE7514900A77567896899E94282E326D4848606FA8F2D350509B19251B4CA4B5B016C4181E8091D501327CCB9AFACD14C6FE4808E6802A8875FB786A52B70359845634BF1C185C887F1BC6FB8221347DB1DEB6C183C412C6D3684A3CC244D37C1C503B69DE67D4B02773DB8096DE18BC717CB3FD9CBA06A370EE84B9ED13597E6BC4CF88474E6956370C88078A56383799DF76621183C70D522DCB8BF07A9546D9CB9873997AFB51F94B49EDE924643900E201097D9853593F2A7E430AF5C57C754142AB462A12D327D04115540B505B8146BB64347A22BC71C02BD2E9C8CB360AAE5C9B173BC2E844CABA9659C2CC695D0019C135963987A5F019989B3064F8A0A8E4CAC8C7909AC4BC028C4AC4750E8C4FBC1433826AEFDDB2315002A27A2B43C539935649890B31710C56BCDDB64E7866BD07009CF11E676BD64E680645DFC88E803063DFE292C2047525EE37B4F3BF7ACCBD82243C1021E9F731F11A6853EABBA8F4E69636C67C2FA6A4718AA4B2BEBB16CC4395DB6F56E75AEC04D1DDE60A119BD846E85AFA528388FF76A185EF98201\",\n          \"c\": \"6DB2BA6A74409C3771A865799F60210A98E0FC38795DE8978FFCF49CDCF97CD68942C89386E5EEBC6273E1C61223BD2BBBE096B43A45E9585076D2A522B2D14FB24A60164B5D49BD4C648CEA83059D12344E32AE6807AC1BF67C7CEEB08C23AC7A0E379FBE383C0986C3B93AD367CEBEE306082B1B26CE6C47EF6F1ECE2CF6EBF836AB453D1A574E7931E1E1DCDF709DED62B534D84BEA05BDB0C6EE0FED3A8465EC43AE00766E4BE8FFA01AFFE5B40165140D1723F3456FE95F62FB4E299295E417F1EA19DF70E45B17FF5951F2D68C87D16FF823FFD6DB683C0E0D89280BFCB6E0E273705230CB70BE7E1890C461A534DCC73C94A2430190E6380A0F48919D7349327E0514F53D4E10677C8FAF771590C9A6E4F8F5443527275962686BACDA101701B399D6BB2911460F84B636C6B1FF92A5D3141CD28A00B1E2484D9A708A1B2BE85C4FBDEF8939634D3DD1B9C9AFF193D9D97850B92880AC8E859C0328551BE21CEB3D553339A5FE9D450F08087465F333FFECE8472AD6C0DD4E41F1C2189178952DEE12A444E1346F744A3A315FF524F41339A0395F65FD97DB4211106118CBCC438BE76087E7E04F47F8C999A8AF661D652FB4EAABC82DC3718739C5D5106C3A85CAB0EC34FB53913000DACE82573FC1682BCE19BF8816B075DDBC871D8DECF5D2350FBB1392A54E94222C9A038AFFEC64ECA6AE2B963D5D45E82DA816B893B4327E0A8A7F11C5D4A2E153F3B4FB1226F707DFA65409439B152B65E38256D8288DE3339BCE574747E5AB26F5B0B114B29A3503DA863D32E3193434ABC77F35807386EAFB37959E9F18C8A0FE654062ED0A589B71C6539A1251B00E816DAAC71F63D35CA189893E0A95D9205A2FE5DA7CD9408EFA51EC442B6EDD8DC1666BE3B222A7429D76A1B70F39A291948D47ACBD8CE0D581F6A8984407377F0CF3A2D7C23A62351B8151AC0FDCB7CF5C3458CE9F69F5DB1E57EE177B46AD28306E1701F91C8BA0864BF447C0E5CB39FFF907EA79B92E86672CE8CB4CC3639FD95EFEC48EB59F855AEE1417ED920BFDA282EC36A2035FD0D7FC64F1B74BB300AA05\",\n          \"k\": \"5E95F007FFA0F4C822238DE22203E3ECCF50020594E1A8D993E8026FE9039159\",\n          \"m\": \"C78E7B1E5EE8F20EF0B67089306E1ABAFD15760B2DD2D7A59D2C00D496FA0FE0\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 24,\n          \"deferred\": false,\n          \"ek\": \"F9238B1B1744FCE415242CB135300B13831D95854E3CEB49E72074B4C09B64859AF81A0C078418C79150D0083B8F9834B14116AF308BC7194C92575119371AA9C74C83350519C0108FC18620453E4820810B5A89D42B5B84FC134F0741E4C0466B2C4B58758A3D561EEA095CB3F469C3A34F19A29F3D20C1381669A6AB1857D3859561096BD8B7A917B40D4B754AE5CDDA47002CD870867203D9901AC5B852EB778705360DF4D35DA7B217951CBD2C026A4AE15221368DA5635D17EA307EFABED74342A6C387D29B59868C4955F6A5B0786AF5CB03F130AC7420663E343BF00998D83B9BFD6458DD63A3B5045C467957A729417CC133FB2048B8DB71BE36905B213A1D8C1BEB76BD27A7CBE281C305F44769746F90307B01613080D50F96CB451D717EF5F372D488B6FDCB762927B910936F161764131C80F09316526CBE62EBB07DB7300D7A81890557000046FD7C098999994FB87691A59F437A7F6D3690075921E973805693AC1CCBC6637471B2C561437A8E30220BDA078435455FBC368DFAD026F85417A5DA96D8FB6272D06F785929B38529D5990DAE3C7596AA72B9381121C6C94A7A593A5AA62BE830BFC0BF5B34C57221AE288302559045F3686D8941576EB41F32516763A90553A5727697AAD39B46C5D84A2382980FE3AC0F904DD10C97078B197B0BC4CB8764F22943F584BFA9F3609D417D65B3C367DA544FAB84AD16025CBA7CE7794CB3ACA32F3042CBB4C25605998A74BC16F80B91C5635DABA3B8AA0D90B1B59CC042BEE19709A747666348577088DC4C5C3168CEFCC5C12C3B4A06C08CFE4CBE21359F03FB99E1C6CF8C84ABDADB1DA905C4EB605FB9C8CE7979458080AC594759CDD82A6E164A9EB781F4BB24147039CFC98889C077698B2471417504E99BE7F283B0929A35E14E3670291DD728599181A6911EA2912B48515F88689A73E9688957473B0CAEDE73937C7BC385D6B2FCF6B76017A8F23A2A7CB54E8A98004E0C97EBC826D1A1142D8162EBC9A311C325AAD94900D9B115276E91DCABA8D015D8A549F7F56A4B182CB3304FCFFB3232F64C2FCB839420663E9391828968D6FEA820C57B8816E1F5D3B414481523D24B81E1E2C429FFF401\",\n          \"dk\": \"DA28574C93C4660286CCC27A47B8437391BE1A3774E46106D689A28AAB03C3BC86FD066384FCB69B51840B1647730A63B7A44D338C7DCAACC24DDA232615962E027E4F5A6638A222EF484826EBAEB557801EC3C0E597BC9653863C534A5232024DBB234F6014E940346B27CB391B87DB64CDF6C642FE87C534D554212C4A8B531AF5F13F1BF1B49E837A2080908C802567C23C7FC1C4274152C8E3A11AC92D9C71639590007C2175E3396339A55F9181AB21525E1EF64EF64B08C3739EF003A6312A439D18AADB1673665B3E154241708C88551941EE9C6DD1126E3C6294026476586907AEBA78A9D04F3E885EBE6B1413CB01BE3383A3DB7626500D8A95C076A7CA5F0CCB7CF785C6939C7E67A16E015583433739D588C0420D57F4719539722B3B4A1FC8A89F93C5766280C2DB4BE16296DB8236A4F7AD2F963A24E9CBF10659E4F7332EBC549471CF84A0A7A17A3942947E4C807C11B37F2144431955C9C4BCC6A9D87DE013408D6C735F9C150F51290254C70D1573838271039661D9860805148B8721A0D73C6589A49DB56C5BC524418DB313972A0277773EFE82139F800501ACA4295A2E95933D0C97A38B27814775C951C0585413CCD8F2197BA169A599A0F8429279584D38D569FF4B3F8B8B40D2C11DFFF524E490490CA21E7312A5B4423D6AA46907B9C1FF46173F3A0C257A80A8E9601C5C4551EC28F52433B62A1391A87EFC466C2AD564EA4B84D8A50128533EA32663A0969CA22A4E34622F1EC1659A2884D76267F904A7D1BBACE0FCCF8340B1680A3FFCB1A53B3213E59C5A7245467CFBC5C1C27910E97F455B8B4B73C04DB36DDE46C063F9AF178853DE82A83C2AA453286B71E14BF6372D4FF46EDD207962079C1E46CDDBF5C40135765465AEA5B5A2BD9059C32A530A8816A63126651AA0A446AA6EA30FC6B09E0F6350EE4C4BD2A750020083B1F60AF0B13702DB7B69693D2F00A06710B7C59710FB813C24E27767E28A1422B266D18A29664B6CF1A179735640EACD3C0A8FFBBB9498D57C4F399DF0F638537A6815024C04D70DF7378DA0910590E58F59977B858465F9238B1B1744FCE415242CB135300B13831D95854E3CEB49E72074B4C09B64859AF81A0C078418C79150D0083B8F9834B14116AF308BC7194C92575119371AA9C74C83350519C0108FC18620453E4820810B5A89D42B5B84FC134F0741E4C0466B2C4B58758A3D561EEA095CB3F469C3A34F19A29F3D20C1381669A6AB1857D3859561096BD8B7A917B40D4B754AE5CDDA47002CD870867203D9901AC5B852EB778705360DF4D35DA7B217951CBD2C026A4AE15221368DA5635D17EA307EFABED74342A6C387D29B59868C4955F6A5B0786AF5CB03F130AC7420663E343BF00998D83B9BFD6458DD63A3B5045C467957A729417CC133FB2048B8DB71BE36905B213A1D8C1BEB76BD27A7CBE281C305F44769746F90307B01613080D50F96CB451D717EF5F372D488B6FDCB762927B910936F161764131C80F09316526CBE62EBB07DB7300D7A81890557000046FD7C098999994FB87691A59F437A7F6D3690075921E973805693AC1CCBC6637471B2C561437A8E30220BDA078435455FBC368DFAD026F85417A5DA96D8FB6272D06F785929B38529D5990DAE3C7596AA72B9381121C6C94A7A593A5AA62BE830BFC0BF5B34C57221AE288302559045F3686D8941576EB41F32516763A90553A5727697AAD39B46C5D84A2382980FE3AC0F904DD10C97078B197B0BC4CB8764F22943F584BFA9F3609D417D65B3C367DA544FAB84AD16025CBA7CE7794CB3ACA32F3042CBB4C25605998A74BC16F80B91C5635DABA3B8AA0D90B1B59CC042BEE19709A747666348577088DC4C5C3168CEFCC5C12C3B4A06C08CFE4CBE21359F03FB99E1C6CF8C84ABDADB1DA905C4EB605FB9C8CE7979458080AC594759CDD82A6E164A9EB781F4BB24147039CFC98889C077698B2471417504E99BE7F283B0929A35E14E3670291DD728599181A6911EA2912B48515F88689A73E9688957473B0CAEDE73937C7BC385D6B2FCF6B76017A8F23A2A7CB54E8A98004E0C97EBC826D1A1142D8162EBC9A311C325AAD94900D9B115276E91DCABA8D015D8A549F7F56A4B182CB3304FCFFB3232F64C2FCB839420663E9391828968D6FEA820C57B8816E1F5D3B414481523D24B81E1E2C429FFF40142C07F795BB51073526A3ACAC38565B3001A89053744886DEBA29F978A97E55A5E31EBD9243C452668809BE6A57BD4E87955928132F1C0AE88233769F141957F\",\n          \"c\": \"B098B60E7D24AFD22B6D949017D7EB64F5B22E09486CDABBF5C968A552E570814AA7EE78C4812AE1F9A62DFEE18AE28C460FAF64B34DF838C868D9F68605E6B174DB175DB8703BB461228725743526B4746CF3196BF15980B6D765D0C70E0435D06EB99DE367CCDBA94ED3062E793DA70678CF40581F1510A715971231429E4CBB97BB68442147ABCD0604D77D1B086F224039B81289C4BB649427BF1509A72FC94F5D239D45DEF93CB926E031049BCAC7E75EEC5689D731EA0A619BB91EDE099252EC631FECA51583C80EA01271310DEB2B075080D7E57141536CE566CC42BDA1EE5D57783C47460597D6919E6993FD57E0C35C612182C6EF8F6924273E1749C7BF6963F37C5A0CE92473A69487A5E40E29339920376F369BCDE9C3CD87A1FBC5E204CAF004372C5839BB725DFD16ED3311898CA15F05BFCD53429074679D0A40FFC162409B339ADF37877343F18C6658ACC96A451940FC09CB7441E0C8A6D309C2223D69095CD8409AF38557836AB5F1DED6B0CB8B3EC30E4C18C15FE1B764A7B932DC3831E91BB5DC62E50A880E1E1F6FA94EA688994E682E6EB28958367456BEFBF61D4CE5B84F64CE980AA2D6AEB4685188E1EA1844292912E5E00D89CA39B11C326BFB076688FB2F03E6BF6EBE8CDD381B5A3776771BA80D88C2625B357815925235B111AA823980512103ED6C861ACC918FC9EA208F08D0923E2CE6A168B13597D91C2F05A9FE7649BB37018922C700C90C5E467DC58E4E51EF87FBEEFBCC8D64E9C4DCA60C4F32B250FD19A0DC8D9159FC936082175C52E0A73953A0E9B8A1000C9F87F0A6E49D271F053D8549FD1A014BEBE89405A54A3F77DE7CB136BEA832E94E18B8BABE44F11CA6E798B1827AF292235A896D865CB3CECD98F8F6AED3952CB33C85F6D1156E1B16481DBB8D74158A1F84A403764BB120F4853D17167E176CFEF7787826DD9A1281E269A7418CB87D80485FDB0D1B73C0DBAC76F5E07FEED9511090B303B4785A72BF77A9445C512703E1942F2E72BDED8508DD4C1B5D4C21F76D0B535ED7915B8AB709521F85814F1AE3F0FF3F4357DCC94216\",\n          \"k\": \"759E8EB2831DCCEE0EADA89C237570E11A9419694AD1CF4474892DFC6877AB16\",\n          \"m\": \"D23A22F6DE6C0F3C28F5A7A8E54581BDB312A56BC90CF3B22A5BB39C9ABF420E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 25,\n          \"deferred\": false,\n          \"ek\": \"305988FF211E278150CE00B65C2669A14830AAA1A4EF2973101443E2A73A1BDC2CFD5AB88C54539573A7A5FA705C573693599C850370CA4A66E853CC283CCA0D2B52084C054C420ABC71ACC2C10D34C61C9CB0459331511107832B0A3BDEC7A0CD941D15A13F193162477211F29345414987ADD22C0FE95AA734745FB32F1114081957017B479108F626EF004D08AA327F0274B1DC2AC9963D8F557BBB58A9E16B7613DB8E379679006119B2DCC441DA80F9AA7F0BC2A8456CB78713A13985625AAA3AA9C6375BA06395F66EC3B63D4EAB2524322979A9B9E1178C4A966FB808C75416338237B3165AB20135ACB0437ABCD96251218F0C382731C45C9FA8141943743C1B1F5D77A064FC7968CA1F26B2B756F0B623323A01D8B0E8DCCB714441D7D5647F6C4338477926B248FAFC997B77C4A2BA625A3A5AAAC60C7A57682DE39D91D46778790CC5B45738D7866F7A909FEB27F70C9C4445A534C0AAF7A487CA8499BA372D3380A13C87978D82677A37C5C7A4490B716CCA2BC262C8853CB60CFA571D2DFAC83D428A010C760FC291E390102E3B6384C35067886940336AF5751E8BA399D11CC9788228E1E21A61F69E2AF7409CABB70DA8C775AA217EACABA3B2BFC7D608B374A47A2C96F56C57748B5137F8093121C0150C1A61803D5C6B42CA439E81A8C9926084F5B98931B7079DD44E2C9376DE77626EB543DF071C7A2C630A995502535FE8570EF7C987675C70DF8525D495C5F93B0B5EC7CF59154CE08A84ECCA36FADA9962160A1C2CCD3A728B148CCC9DE733719387D6C166BD9691E3EA6B22550A85A09989A49C90D68A51F195561A4F424CBE3152652272200FD642DE292A2061065EE1B962964BA33281B9B88222B122A3247AB9C247A9A6342F5082A685C8C355A1F96A0277BB8A97979937C17A47A2C36B3040C6B437BFC28B8DECB59DD9AF1B6818C146B74B0AAFFDCCB710E6BB3AD5A5F6AB7F8B2C2AD8980D6B569A5DD523E20379A6005695033260ECB823C7579E610F00A30EF745811DE05762A874A1986764A399B5CB1212403F6E7184028C5B2C47BCBA1A6537F180F096BAD9FA53AA495443314B91B46600EC339B950E9C4F1B1AD5E92385E3F7CA\",\n          \"dk\": \"CB402B0AB79835868924D80496343B0AC32FD1A13D7B39BF75893DE4529754B68219C58ACFDAA4CD964316AB05C170249A763416DA3DB9449BF32B0D41F7B53C406C62C24C33E2820A6C818B93992A1CAF55F155370BB18FA415DBC45977A335A0D03C0C9757FD26A4CC00980A2C35D699B96838525867A8A17719A109BA6FDAC6D187671F065BA38255D2824FDF5C66D0065858942982EB5C9839181170C633D76D123918D30A23A3A8884B7073C1E2C914FC0117F190C6C85DD327886A8CAAF01A6F9DD537E7797DE0D906DF15CB305765AE6BC724B7B911D293CF8AB7A9C9119D4B09A1794D388347A492CEB7C0664427320537B6CBA990F76581856A2AB6534AAC19B3015132EDB1023F68BD4A935C293168DFDB96C49A6C40B0C86592CFCFF74F59781C6264528A63653649A021568E71290EF4BB3FA445A925682CC04A161549696F3A984BE61B3CA46D3615983F50987E049A5DD1B0BC885F44F0AAE5E836E468789BEB6F7DB10F2AE07C479607AD156C96E49A790078D13C8205B366974AA32A696962434CB972BB78724123E33396463E83A30BD4265CAB598BCC61895D6BC4443C8D3B1B5D27B607599707E3C0454BC2A84BB53FDE1678FE905A0EC7A9458C2540E7741A10AA079802BAA2BBE90C03C3C80EA6178989CC6A3B9488551252B6828B9138B8C259A38DF9412ABCC01A4183BFD19545216E0FC1532119AF0019603F3C247D408ADBC9215E3614832BCEDA531F63A0729552B2EC99B120B8722275440B0C764E7A6BF0A10E73ABC0098761C45068E7E1549611822ED923131A91DA17B523309A1B59525CE427359B6BF54250B282C56985519C151FBD788F9D4737D8EC1968E79B0BB8CFC9E012C26AA66DA8284EAA3E9770B1D0D760A68254D8A111CD722362BA6E4969422B21BBC41838B3171046E87277CC400120C293417E411673373B98303BC533540192E256BAC46E57BB96F6D906FE291FBD843899A3212A894709349C3A22084A3CC4DE7B86CFF66B841583991747C36CAD49667AE4EC295D76765B401B78E59B68B0B0208A6D12FB3720BC9B77FCC3E56287305988FF211E278150CE00B65C2669A14830AAA1A4EF2973101443E2A73A1BDC2CFD5AB88C54539573A7A5FA705C573693599C850370CA4A66E853CC283CCA0D2B52084C054C420ABC71ACC2C10D34C61C9CB0459331511107832B0A3BDEC7A0CD941D15A13F193162477211F29345414987ADD22C0FE95AA734745FB32F1114081957017B479108F626EF004D08AA327F0274B1DC2AC9963D8F557BBB58A9E16B7613DB8E379679006119B2DCC441DA80F9AA7F0BC2A8456CB78713A13985625AAA3AA9C6375BA06395F66EC3B63D4EAB2524322979A9B9E1178C4A966FB808C75416338237B3165AB20135ACB0437ABCD96251218F0C382731C45C9FA8141943743C1B1F5D77A064FC7968CA1F26B2B756F0B623323A01D8B0E8DCCB714441D7D5647F6C4338477926B248FAFC997B77C4A2BA625A3A5AAAC60C7A57682DE39D91D46778790CC5B45738D7866F7A909FEB27F70C9C4445A534C0AAF7A487CA8499BA372D3380A13C87978D82677A37C5C7A4490B716CCA2BC262C8853CB60CFA571D2DFAC83D428A010C760FC291E390102E3B6384C35067886940336AF5751E8BA399D11CC9788228E1E21A61F69E2AF7409CABB70DA8C775AA217EACABA3B2BFC7D608B374A47A2C96F56C57748B5137F8093121C0150C1A61803D5C6B42CA439E81A8C9926084F5B98931B7079DD44E2C9376DE77626EB543DF071C7A2C630A995502535FE8570EF7C987675C70DF8525D495C5F93B0B5EC7CF59154CE08A84ECCA36FADA9962160A1C2CCD3A728B148CCC9DE733719387D6C166BD9691E3EA6B22550A85A09989A49C90D68A51F195561A4F424CBE3152652272200FD642DE292A2061065EE1B962964BA33281B9B88222B122A3247AB9C247A9A6342F5082A685C8C355A1F96A0277BB8A97979937C17A47A2C36B3040C6B437BFC28B8DECB59DD9AF1B6818C146B74B0AAFFDCCB710E6BB3AD5A5F6AB7F8B2C2AD8980D6B569A5DD523E20379A6005695033260ECB823C7579E610F00A30EF745811DE05762A874A1986764A399B5CB1212403F6E7184028C5B2C47BCBA1A6537F180F096BAD9FA53AA495443314B91B46600EC339B950E9C4F1B1AD5E92385E3F7CA320C1B0462C9C95B0367A4A13BEE7F2574BFBDF01921E7C2BA5AD3D6954E8334C39524D35D19623E3F4B21EA8BFFAFE599515D49A90278F7529215781B9A9F82\",\n          \"c\": \"BC00B2A45132B099533C3441157FE9E260F7B47CFA31730421FC913920B72A7AF375DAA469C22A17E8A4EBACB8ACB89D1DC841028190538BCACF028B7709D14E38DE97A99004F54B8D84A1372C250185895486C5426E6AD1D4C42F69D4902DF59A2ECEF40979E6C240EBA46FC0ED0788CD75B1B6BA6F382950BBA1C2A0F779B3100C0A26639A9733F3B912FB1CAD4DDD118D4AE13198204FAE7EE59277315662B9CBC9EFBC1D756127525B4996CFDCDF9B7DF7E9A2E71B9BA72650370DBF75A2F39D0004CA6F7FA59C8951FAA76091362C8938D5EC82E6EDAA06BFDA4852DB9F11EAB5C659D21777AD6365AFC524FD0090551535A6DD2EBB8E5F8A2D1C1DDA87655BC1038C6501610291382969EF3CA1730947DEBFCD5B95B68D63750E77A59CCBB328D57347824D6FF2F09B0152F0B404DD023A6F2DF7E61030BAEDC765500ED03A81237FEEBCC3022403D17EF9398296B0AF4747209E0CFE925DCD71B70DD71DFD96181CA30129EC97A21C0D18E3B6315CDA1E88DCBCDDD1912A4947E6E1BAE6250CBDD931CA1B7D146041E973AC0139FF6A23107D44E61293D1AC9E249B5F4E3CF69E55361440DDF9B2558EC793F8968CC09716CD9DEC2BAE26A0A5587BE97CEE4B9CBD3506794559C2D7D3550011CA37424CCBDA8BF479098A5E76031D729EDE3B67C6E5A0A2AF11627C1AAFD3C16F548C4841AD9307096AE806210CC0173429C9699F5D95162B9B56D7199D4809A294579905E2C5D3BE1F890F65727F92D97CEF4915724FE3CEBF00E3A01336FAC1C86ADF6A8ED654256DEAC45464E537EAB98A918C69CC6AD91A53E69158DBD71A18A83DA3EACD67A65F7DAB277E82B5F9535E61448A1AEE1F52FAD989E14332EFFE97D3309CC2BD58E45AFB5A7056C20AEAF1E4D5A0EF5B0C1507923CAF7937657109A83A437EF10CC035BDF983F88FA04EE6C338346FABEAD3413DF0071F960AEB121FECDE71BEF8800142CC159F9A6EB729205D3A980F11B5960699EB3A9394237B96E58F141058F8057A4D15895D4F77C49BBC021B452FFBACFF2C74279EA83D0C57EA4CE5D7952314206A3FABC3\",\n          \"k\": \"2239EC88DA575EBB9329448904221C63CDF517DBE3029713E3840CF4C54819E3\",\n          \"m\": \"C0A5ECA859643D0134F2231C8F3764044B7E6073C92C9CDF71BD64FBC59ADDB9\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 2,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-768\",\n      \"function\": \"encapsulation\",\n      \"tests\": [\n        {\n          \"tcId\": 26,\n          \"deferred\": false,\n          \"ek\": \"89D2CB65F94DCBFC890EFC7D0E5A7A38344D1641A3D0B024D50797A5F23C3A18B3101A1269069F43A842BACC098A8821271C673DB1BEB33034E4D7774D16635C7C2C3C2763453538BC1632E1851591A51642974E5928ABB8E55FE55612F9B141AFF015545394B2092E590970EC29A7B7E7AA1FB4493BF7CB731906C2A5CB49E6614859064E19B8FA26AF51C44B5E7535BFDAC072B646D3EA490D277F0D97CED47395FED91E8F2BCE0E3CA122C2025F74067AB928A822B35653A74F06757629AFB1A1CAF237100EA935E793C8F58A71B3D6AE2C8658B10150D4A38F572A0D49D28AE89451D338326FDB3B4350036C1081117740EDB86B12081C5C1223DBB5660D5B3CB3787D481849304C68BE875466F14EE5495C2BD795AE412D09002D65B8719B90CBA3603AC4958EA03CC138C86F7851593125334701B677F82F4952A4C93B5B4C134BB42A857FD15C650864A6AA94EB691C0B691BE4684C1F5B7490467FC01B1D1FDA4DDA35C4ECC231BC73A6FEF42C99D34EB82A4D014987B3E386910C62679A118F3C5BD9F467E4162042424357DB92EF484A4A1798C1257E870A30CB20AAA0335D83314FE0AA7E63A862648041A72A6321523220B1ACE9BB701B21AC1253CB812C15575A9085EABEADE73A4AE76E6A7B158A20586D78A5AC620A5C9ABCC9C043350A73656B0ABE822DA5E0BA76045FAD75401D7A3B703791B7E99261710F86B72421D240A347638377205A152C794130A4E047742B888303BDDC309116764DE7424CEBEA6DB65348AC537E01A9CC56EA667D5AA87AC9AAA4317D262C10143050B8D07A728CA633C13E468ABCEAD372C77B8ECF3B986B98C1E55860B2B4216766AD874C35ED7205068739230220B5A2317D102C598356F168ACBE80608DE4C9A710B8DD07078CD7C671058AF1B0B8304A314F7B29BE78A933C7B9294424954A1BF8BC745DE86198659E0E1225A910726074969C39A97C19240601A46E013DCDCB677A8CBD2C95A40629C256F24A328951DF57502AB30772CC7E5B850027C8551781CE4985BDACF6B865C104E8A4BC65C41694D456B7169E45AB3D7ACABEAFE23AD6A7B94D1979A2F4C1CAE7CD77D681D290B5D8E451BFDCCCF5310B9D12A88EC29B10255D5E17A192670AA9731C5CA67EC784C502781BE8527D6FC003C6701B3632284B40307A527C7620377FEB0B73F722C9E3CD4DEC64876B93AB5B7CFC4A657F852B659282864384F442B22E8A21109387B8B47585FC680D0BA45C7A8B1D7274BDA57845D100D0F42A3B74628773351FD7AC305B2497639BE90B3F4F71A6AA3561EECC6A691BB5CB3914D8634CA1E1AF543C049A8C6E868C51F0423BD2D5AE09B79E57C27F3FE3AE2B26A441BABFC6718CE8C05B4FE793B910B8FBCBBE7F1013242B40E0514D0BDC5C88BAC594C794CE5122FBF34896819147B928381587963B0B90034AA07A10BE176E01C80AD6A4B71B10AF4241400A2A4CBBC05961A15EC1474ED51A3CC6D35800679A462809CAA3AB4F7094CD6610B4A700CBA939E7EAC93E38C99755908727619ED76A34E53C4FA25BFC97008206697DD145E5B9188E5B014E941681E15FE3E132B8A3903474148BA28B987111C9BCB3989BBBC671C581B44A492845F288E62196E471FED3C39C1BBDDB0837D0D4706B0922C4\",\n          \"dk\": \"B09125AFB3CFB5295581373AB6885284D9706318280D223EDC987FD14410DBE82E6AC89ADFAB70E67CA4B1C641AD037FD8C47870F159EC79CDCD52605B9890499BB6DBD8347F342C61436B642C0DDF4617DB06198B8285DCE4C09D9775A2F41C8CD18AF8E75F57D4127DF94D901AC83BACBD584CC50C43750F49B357F59350875C9B475480A8AAA168592DDB158614A639813566D205368C6C39F0413CA3230DF60D44008282B682AC66B76C3C95F00B2A555035529C86EF3905B4A3968FEA7802B6C5EECB08E8F0C42D7AB7CD21A62FB136412A1840B52C99970CCF51892F73497C3775BE2189F7FC25E7C74D81FC217683292AA4866DDB04469855323A0810F0893DE5C7F94A9C0B5337DB83C44891B2E694695B76575032BF51761682958BD4F97BE9A355B4A85BB6858B7E5A5EF653AB781056AF9187D811C3A8936E5706503DB57062410BCC9421F1AB867A657856C411C4E025ECB3C387729AE8E112F330B988E22F47C35C280750D21B107687AF7B329EF3CB5289F06FB7D44548391E97BA6DD499B5907C54958413D92AA99D5646CF47A8F48CB70A07AD056B4EEFE6C8C46645F7028A32410558638C48E83AC1570160C3833BF64052F5B7DF4364D3E0B24E790AA7C98CEE0441E6731D9DE22D156C61E1C740397672EF54724F01B9D49923AA321F86B98823F21360138392B90C69434635275F9BFBB9B8A99E8E1B7F4EC25F75DBCE33C13F750170BD6722EFE496E7463E16AAA5867B869A96AD41B22BD2556C924596FD778D79A102F6E46D8EB18FEFAC8DB19993E5414AC816705286892492C8C9E852D6145DFF0C10E4A6703A459E7E732A6DFA2766A622B0622BFEDB8F41C125F61B2EC264853B9CCC165979F6A263BEB148905AAC7618A70E829E23F28696F92EF6FA07C102CDBDB1288BA5CFF3A81ABBA15974535FE3106A80068F14E98964572350A7112B1601C196710C096CCF164FBCE1AABAC9C5B9535070E61AB8068D611CA765FABB6412607DAB30C4FC6AD073731FDC4C48B88E267C47B439AD2560C30561815CEB1F52C896489944BBBAB52B1B1D1680A1057964DAFA600C93A39A447DDBB0ADF911AFE3E823D8ACC7CC04659F625F2C1837BB175282542CD22601F621581AB5A6C0384E087CCD32A5380B522FDD3A4202B5B41C85CAFF2903B2DC2645703D9BC711FBB404C0C0376187AC588AAF5718522D2273A9408DABCBC9701698D2DA172AA6267A4C9693A24011C2265A2B6DC8E96304A98DDC5319A3140C399A08412C20F48537870BB84C32A094457895511FF7EC421DE01A64B78534653F78327441B90CD115939DFAAFA95B40D0A63D62D12EB5C9096018CC83871E44E6CD0BE26D16B7B5A209B8E6471D2954ADF9FABD0153707C9CAA2BCC38DED841C791A0EB597EEEE2C518D926EDB28AB53CAA5B7746466931B0AC9150688BF37049C1F82BCF648332434CD0A92FD2C958353A26CB65CB499057109B2D688CC43C4B385DA7C50868AF1B8075E57088F5DB12DFA493EACB6DC4EC6E205BAA2A89858EC2823C00553714CDE47A96E36C7C198B3EC57CCF74D92CDDB86AA0A8B8B5CA9D52BB60ABA79F4F72B0125532CEB7A9077480D2BB60DF51A989D2CB65F94DCBFC890EFC7D0E5A7A38344D1641A3D0B024D50797A5F23C3A18B3101A1269069F43A842BACC098A8821271C673DB1BEB33034E4D7774D16635C7C2C3C2763453538BC1632E1851591A51642974E5928ABB8E55FE55612F9B141AFF015545394B2092E590970EC29A7B7E7AA1FB4493BF7CB731906C2A5CB49E6614859064E19B8FA26AF51C44B5E7535BFDAC072B646D3EA490D277F0D97CED47395FED91E8F2BCE0E3CA122C2025F74067AB928A822B35653A74F06757629AFB1A1CAF237100EA935E793C8F58A71B3D6AE2C8658B10150D4A38F572A0D49D28AE89451D338326FDB3B4350036C1081117740EDB86B12081C5C1223DBB5660D5B3CB3787D481849304C68BE875466F14EE5495C2BD795AE412D09002D65B8719B90CBA3603AC4958EA03CC138C86F7851593125334701B677F82F4952A4C93B5B4C134BB42A857FD15C650864A6AA94EB691C0B691BE4684C1F5B7490467FC01B1D1FDA4DDA35C4ECC231BC73A6FEF42C99D34EB82A4D014987B3E386910C62679A118F3C5BD9F467E4162042424357DB92EF484A4A1798C1257E870A30CB20AAA0335D83314FE0AA7E63A862648041A72A6321523220B1ACE9BB701B21AC1253CB812C15575A9085EABEADE73A4AE76E6A7B158A20586D78A5AC620A5C9ABCC9C043350A73656B0ABE822DA5E0BA76045FAD75401D7A3B703791B7E99261710F86B72421D240A347638377205A152C794130A4E047742B888303BDDC309116764DE7424CEBEA6DB65348AC537E01A9CC56EA667D5AA87AC9AAA4317D262C10143050B8D07A728CA633C13E468ABCEAD372C77B8ECF3B986B98C1E55860B2B4216766AD874C35ED7205068739230220B5A2317D102C598356F168ACBE80608DE4C9A710B8DD07078CD7C671058AF1B0B8304A314F7B29BE78A933C7B9294424954A1BF8BC745DE86198659E0E1225A910726074969C39A97C19240601A46E013DCDCB677A8CBD2C95A40629C256F24A328951DF57502AB30772CC7E5B850027C8551781CE4985BDACF6B865C104E8A4BC65C41694D456B7169E45AB3D7ACABEAFE23AD6A7B94D1979A2F4C1CAE7CD77D681D290B5D8E451BFDCCCF5310B9D12A88EC29B10255D5E17A192670AA9731C5CA67EC784C502781BE8527D6FC003C6701B3632284B40307A527C7620377FEB0B73F722C9E3CD4DEC64876B93AB5B7CFC4A657F852B659282864384F442B22E8A21109387B8B47585FC680D0BA45C7A8B1D7274BDA57845D100D0F42A3B74628773351FD7AC305B2497639BE90B3F4F71A6AA3561EECC6A691BB5CB3914D8634CA1E1AF543C049A8C6E868C51F0423BD2D5AE09B79E57C27F3FE3AE2B26A441BABFC6718CE8C05B4FE793B910B8FBCBBE7F1013242B40E0514D0BDC5C88BAC594C794CE5122FBF34896819147B928381587963B0B90034AA07A10BE176E01C80AD6A4B71B10AF4241400A2A4CBBC05961A15EC1474ED51A3CC6D35800679A462809CAA3AB4F7094CD6610B4A700CBA939E7EAC93E38C99755908727619ED76A34E53C4FA25BFC97008206697DD145E5B9188E5B014E941681E15FE3E132B8A3903474148BA28B987111C9BCB3989BBBC671C581B44A492845F288E62196E471FED3C39C1BBDDB0837D0D4706B0922C472E31DF613DA9A1DD33B5D2D8939684B89F7649E1C59B959FFBE972786C477F66177DBF3B059173FD06AFCD90E80E862174FC57F97607BBFF5B73D6360FB5C37\",\n          \"c\": \"56B42D593AAB8E8773BD92D76EABDDF3B1546F8326F57A7B773764B6C0DD30470F68DFF82E0DCA92509274ECFE83A954735FDE6E14676DAAA3680C30D524F4EFA79ED6A1F9ED7E1C00560E8683538C3105AB931BE0D2B249B38CB9B13AF5CEAF7887A59DBA16688A7F28DE0B14D19F391EB41832A56479416CCF94E997390ED7878EEAFF49328A70E0AB5FCE6C63C09B35F4E45994DE615B88BB722F70E87D2BBD72AE71E1EE9008E459D8E743039A8DDEB874FCE5301A2F8C0EE8C2FEE7A4EE68B5ED6A6D9AB74F98BB3BA0FE89E82BD5A525C5E8790F818CCC605877D46C8BDB5C337B025BB840FF471896E43BFA99D73DBE31805C27A43E57F0618B3AE522A4644E0D4E4C1C548489431BE558F3BFC50E16617E110DD7AF9A6FD83E3FBB68C304D15F6CB700D61D7AA915A6751EA3BA80223E654132A20999A43BF408592730B9A9499636C09FA729F9CB1F9D3442F47357A2B9CF15D3103B9BF396C23088F118EDE346B5C03891CFA5D517CEF8471322E7E31087C4B036ABAD784BFF72A9B11FA198FACBCB91F067FEAF76FCFE5327C1070B3DA6988400756760D2D1F060298F1683D51E3616E98C51C9C03AA42F2E633651A47AD3CC2AB4A852AE0C4B04B4E1C3DD944445A2B12B4F42A6435105C04122FC3587AFE409A00B308D63C5DD8163654504EEDBB7B5329577C35FBEB3F463872CAC28142B3C12A740EC6EA7CE9AD78C6FC8FE1B4DF5FC55C1667F31F2312DA07799DC870A478608549FEDAFE021F1CF2984180364E90AD98D845652AA3CDD7A8EB09F5E51423FAB42A7B7BB4D514864BE8D71297E9C3B17A993F0AE62E8EF52637BD1B885BD9B6AB727854D703D8DC478F96CB81FCE4C60383AC01FCF0F971D4C8F352B7A82E218652F2C106CA92AE686BACFCEF5D327347A97A9B375D67341552BC2C538778E0F9801823CCDFCD1EAADED55B18C9757E3F212B2889D3857DB51F981D16185FD0F900853A75005E3020A8B95B7D8F2F2631C70D78A957C7A62E1B3719070ACD1FD480C25B83847DA027B6EBBC2EEC2DF22C87F9B46D5D7BAF156B53CEE929572B92C4784C4E829F3446A1FFE47F99DECD0436029DDEBD3ED8E87E5E73D123DBE8A4DDACF2ABDE87F33AE2B621C0EC5D5CAD1259DEEC2AEFF6088F04F27A20338B5762543E5100899A4CBFB7B3CA456B3A19B83A4C432230C23E1C7F107C4CB112152F1C0F30DA0BB33F4F11F47EEA43872BAFA84AE22256D708E0604DADE4B2A4DDE8CCCF11930E13553934AE3ECE52F3D7CCC00287377879FE6B8ECE7EF79423507C9DA339559C20DE1C51955999BAE47401DC3CDFAA1B256D09C7DB9FC8698BFCEFA7302D56FBCDE1FBAAA1C653454E6FD3D84E4F79A931C681CBB6CB462B10DAE112BDFB7F65C7FDF6E5FC594EC3A474A94BD97E6EC81F71C230BF70CA0F13CE3DFFBD9FF9804EFD8F37A4D3629B43A8F55544EBC5AC0ABD9A33D79699068346A0F1A3A96E115A5D80BE165B562D082984D5AACC3A2301981A6418F8BA7D7B0D7CA5875C6\",\n          \"k\": \"2696D28E9C61C2A01CE9B1608DCB9D292785A0CD58EFB7FE13B1DE95F0DB55B3\",\n          \"m\": \"2CE74AD291133518FE60C7DF5D251B9D82ADD48462FF505C6E547E949E6B6BF7\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 27,\n          \"deferred\": false,\n          \"ek\": \"F5841D6AEA683FDBA16308BDAB828DDDD7735B8B7A0DAC6A57EB5134B91D8D6CBD989580411144E1FB5A6A559A7056376210A8284742D22A5881C5214C90023FC910D5D02A869087557900273BB875420B5717CD0B23064AA820CDF372F3E4778D70AEB5D02B6182C4D37110D782B6E80303332697B4C610A384A0C632C0D9484A1D3B5EA921525BEC5755C839DF942F24A027DB50B2D760066D10A117BC9A1B65C448CB9ACF3B4F644316E8941C449803F6851A74D832A739B2C0EA9258C7258E98BD3E833D879A6845EC4ECC44B6FA699388135F5E4830F2625E9FA5CC982C578B2593D350B06288A854D3349C24586D3AA2E68726A873B1E5AAA3B22671D8C69AEB180718CB456B942E4B6678E620A00BCA310C722DDD499EAD9C6B66666A3DE39A45D7AF0BBB7AB6A0BEAF8BBCBBA17B1D097ABB09A70E410352D2084423AC53ECBB4C196021F01E662A60C68B3BF48A5F0864A25577912F52620CE6347BD27FF68A17D4B92CD7D01B89E3487A5BC2859781F3EBB8B5B4C2D682636C486A000A576A4B63AFFC05082B5ABE3CC0B37B1E586C2107D97157E325A067BB86453414A15594A510DCFB2FE1A0074483120FB83440DB1B8C3B41E36364F92056083CB9CF91B39F28CF00F6AD098AA10FDB4B4D9B64ED1338E0D5B7A5169C3D8C0184B19966E54272F765C0337BBD307F8C97369A7A87DA44A5BF468DB8A9AA5EA598F885AB50174B0F9025A4EB53D2323D202A05265331FD836DF8E02B4595458551ABED8A3875B83BF976942372CB37296C813ACD2C27B41A5514B66AB25759009DB38A9D0473D5B7A9A7D6795F1188A079B1792A01141347AF2194CA681055D36E954C02D6935BBA7C2EF7F4B5E47C8B0A0069F29575E863967CE4C53105230472172FB79E69089D5A7BCAA95784BFA279EFE67DA145308BAAA1A5A303757946C2866B4841660A99C1968B8F7DE799ABD71806EB9F091397C1CC4171152A6AFC36BD733FC6C53545361AB6258CB45C9F1331BAEA85BE4558935984C081F73E4B377E0251CA7C396BBBB81D271BB9F0589E1BE3218B0B5840372253AA80A5DB79E11199C0832B2433880B68BD84FC02AA3CBBEC205EBBC7B050967B4DFB11E2FA63BCF6B7656A8028AB607CB084C21747ED573A055166F82215D7201D5D439A19F584F470B4272962C137B38545309547CEC25B09C96459AB7B4DA69C8D7B9277BBC4B5568813DA904141A011D9B45AC1F181273149F3C46F45CA9735221B97CB528E8AB59C5711A57C603F7A91803254E8CC4A37D84D1F6535E5A791A50145E1E073430810B3AB79DF4053538C7DB4826A1B428A84553BB881A23507385271B32F854706BB2D3E884E7B391985B39B7BA373071455187B3DD7DA75F6988BBD6BC39EF2808C245AEC9C024CA16546A16F63831A7B6797951A40894A5E38422F30B87E70355CCBE960B216592D0073F1240C21BB109AE76C9DE5B7835BC08AC6601C314A82232FA6F6896BD7834F0254BF112602022844F0CBA9FC3D2E3A58EDD56DDC498ADC9A03FCB43CA138640F85397FD5731F537D6BDC3AC76563D6516F1CF24F84B7C957635DEFBBB70071621C8B2585380A63660EF2CB6CA5910BAD42A1B621CAB8C26780D4251DFD1C6370EF12193C3CEF0223187A4557BC08F4ADD382\",\n          \"dk\": \"62DC65F32C94A1365605B30807CA5A34996AC9311532B3A23906683B44A9D9B6136BD9AA72F369E77C701E72086E5137EC7350DF480B6437B31B2863DAEBBFC08B9C27D00CE7F6349A971ED1E505039B73EDA8C7614334216A5F2719587DA3CE79B92D14581BF58590BBB3C01108C21F9B6933E4128B28A7834B2DA4922431F921ABD52EBB5295AC4957EFD662ADF97265E912A3D115F6695536AA2D6AD15E74D58C29FC0ACB536C2B43157FF21C57AC1B600979FFB223DDB70E76EB8D56502921654BBCE29B30E5C7E359B86E25A5EB018B0428BA5D53B23F6BA37E3265854C1A58DA09B9FC3DE4B98906E3A8E9C575DC82AD2389CF2E9C32D3BB8337A8221C59077722C7D39C9F536B3CB523C8756250176155907B20F5BB2C7F3A9B85E5A527FB0FCBC01D7EA971162789BDBC840ED68D24AB7F626B0E401BCA03A225F5E4AD2C17B8C87893683B7D1CA83B1AE0B3D0CBC34FE793A07469049C60E9169A47701828B395ACB3BE3364CC5C2A5892713137B04AD0720FF7307D917C61A269CD5B583F4CB65BA2FB562AFC535117264CD46AB03637D832A37585640379A2C3C80D4CD621D4638D3F6972B1EBBEF61A3CE5461B6F1A253AC2C150255B188656CCF492BDD41A921BA7CEF679C8D33265FC0A8AEB6988E2B813561EE6C4A9A8297E935008173C58112756E5288A4ADB6FD192A478546A41D88041038289A4708704BEDA425988D493DF57044E6304E89896CE25C4978A6278AC70EECC27EFC271F1E4A9C0F5AF17E1441357B8EDC88799421FA56A8731063776322837E01FED1AA6B2E4C86A5C55FC5B735E315C22284B37C238FAE93916188247815554DA511AFB634C75A1B8E783F5530BD38A57B4F77E48C812DE683507F5A24CA867AA3A96440678F21404D9D6B182259190C87ED86C6F632C90D03070F7761ED85C7AC22261C6566C76B66C2E800DC427C07633C90A61077B078E2DB251527835239957DFC2055F176FF758506983135D437650E71D77957A8A48B56CE59C566395078C2E7008A4D7604BAAB7AB78C461B7718D6BB27C55206630C331823619A08773DC562392F817D73B350421929D83BC49C6C8DD5C38F6D46F08755C316093F5C5454280A3DBDA4E5B508E40E0ADCC80AD27112E2D2C9DA3392A034B6277F8157FA1BC0ACA1C1F857CC4D6A6EB880C667164F6AB886B5A1D84C6321D735A5A1047AD985FA0DC317B22BB54E0C04FD3C27FA976F2EC633DC1B14A502E5A02275AE67E6B68583B8C7FF97827059BAE1CC3765A5C7202227AB640C1BB68924B928B489A3F6BAB68E2F0C9D338589443C3909353FB98487BFA6A91B71A62A9CDD39AC6065668680388E1A33467A3C32EC61A1D605E6AC92751D05BDE0930867A96E2713D933582D1BA4A85434590324B33A522A5B2BE13F667A96914D2F6289CE735BD3CAD15F6ADDB4B4BC7AC47C88A2C52FAA7CF434E7B8995A4F54A7043273E357FD6B37F3EC924457913C351BADB0B415CE60754BA7621A8C85FD0C678E1C9BF4C3283749A33784098B9065B4724A6A4A5A155751E0845F908B67C673C0B6C278C35CFC86A450A8526608683DFE69D59E1C905D4A3EF95B9E134AF8E6A54FDCBAF3E028EF5841D6AEA683FDBA16308BDAB828DDDD7735B8B7A0DAC6A57EB5134B91D8D6CBD989580411144E1FB5A6A559A7056376210A8284742D22A5881C5214C90023FC910D5D02A869087557900273BB875420B5717CD0B23064AA820CDF372F3E4778D70AEB5D02B6182C4D37110D782B6E80303332697B4C610A384A0C632C0D9484A1D3B5EA921525BEC5755C839DF942F24A027DB50B2D760066D10A117BC9A1B65C448CB9ACF3B4F644316E8941C449803F6851A74D832A739B2C0EA9258C7258E98BD3E833D879A6845EC4ECC44B6FA699388135F5E4830F2625E9FA5CC982C578B2593D350B06288A854D3349C24586D3AA2E68726A873B1E5AAA3B22671D8C69AEB180718CB456B942E4B6678E620A00BCA310C722DDD499EAD9C6B66666A3DE39A45D7AF0BBB7AB6A0BEAF8BBCBBA17B1D097ABB09A70E410352D2084423AC53ECBB4C196021F01E662A60C68B3BF48A5F0864A25577912F52620CE6347BD27FF68A17D4B92CD7D01B89E3487A5BC2859781F3EBB8B5B4C2D682636C486A000A576A4B63AFFC05082B5ABE3CC0B37B1E586C2107D97157E325A067BB86453414A15594A510DCFB2FE1A0074483120FB83440DB1B8C3B41E36364F92056083CB9CF91B39F28CF00F6AD098AA10FDB4B4D9B64ED1338E0D5B7A5169C3D8C0184B19966E54272F765C0337BBD307F8C97369A7A87DA44A5BF468DB8A9AA5EA598F885AB50174B0F9025A4EB53D2323D202A05265331FD836DF8E02B4595458551ABED8A3875B83BF976942372CB37296C813ACD2C27B41A5514B66AB25759009DB38A9D0473D5B7A9A7D6795F1188A079B1792A01141347AF2194CA681055D36E954C02D6935BBA7C2EF7F4B5E47C8B0A0069F29575E863967CE4C53105230472172FB79E69089D5A7BCAA95784BFA279EFE67DA145308BAAA1A5A303757946C2866B4841660A99C1968B8F7DE799ABD71806EB9F091397C1CC4171152A6AFC36BD733FC6C53545361AB6258CB45C9F1331BAEA85BE4558935984C081F73E4B377E0251CA7C396BBBB81D271BB9F0589E1BE3218B0B5840372253AA80A5DB79E11199C0832B2433880B68BD84FC02AA3CBBEC205EBBC7B050967B4DFB11E2FA63BCF6B7656A8028AB607CB084C21747ED573A055166F82215D7201D5D439A19F584F470B4272962C137B38545309547CEC25B09C96459AB7B4DA69C8D7B9277BBC4B5568813DA904141A011D9B45AC1F181273149F3C46F45CA9735221B97CB528E8AB59C5711A57C603F7A91803254E8CC4A37D84D1F6535E5A791A50145E1E073430810B3AB79DF4053538C7DB4826A1B428A84553BB881A23507385271B32F854706BB2D3E884E7B391985B39B7BA373071455187B3DD7DA75F6988BBD6BC39EF2808C245AEC9C024CA16546A16F63831A7B6797951A40894A5E38422F30B87E70355CCBE960B216592D0073F1240C21BB109AE76C9DE5B7835BC08AC6601C314A82232FA6F6896BD7834F0254BF112602022844F0CBA9FC3D2E3A58EDD56DDC498ADC9A03FCB43CA138640F85397FD5731F537D6BDC3AC76563D6516F1CF24F84B7C957635DEFBBB70071621C8B2585380A63660EF2CB6CA5910BAD42A1B621CAB8C26780D4251DFD1C6370EF12193C3CEF0223187A4557BC08F4ADD38239082384D084D2B67B5956A1463685AAA7BDE716AC1791935C47504893E18F24866531E34AD01E68FD6CE8DEE12B40398FFC74FDC4A8DA6785A966640FCC4F85\",\n          \"c\": \"BE483938DAC565B129658D168D494E522B52D031DE7FCC2FC6D52BDCE3F649AB140ECE5B25486B5F85D43ED6D85F6BBDC4141DCFA6C03F680C7B6D51484B461F700E207E2E281070DD48AED510A64E6849C462705AE29C566E6F2461F90387DAA3108FE9372A2B8D11CC2CD6CA20D9D1CEBC31C12B3DAF01F9CB67A4DB488DAF1760A48A29BB4E25A26752FF161B94DFC82A9773A8E5B9F761DA751FBBA982FEAB1A7FA3460CF669D5B8B3BEF8EDA6310009EE7130478222FBCC59CCCC248FBA6384DB7BF5D3B553C8ED134135F09DECA3877C9C4B22A478F892317841DE917E642B966906886358B09E8761E98EED4EC8309C578502C070E7C4E43CF2FFDDF1E4CED37762FC8D5D5C65348FDF01A0CC85314C022040982B94F4CC7FB565EB00C218CC61740062F896E992038F58D02B170DC903BB665B2A6CD724E201C17E646816E2AD528BAA20C43BC8ECC090F644256AA22FA3365820FE7C8AA5D168D67A21785D4BB2BEEE4FD3943FE351A0E94AACF9A5B4859EA97F3A5AECD213169356876B756137697F4C40A567CD960AA0436E61986407B2B88839FA226966271004C1445E057F932BBDE1274757A55F2AC8846FF770B1565C746814276487A9D3E454F5FAB0D77C82723A114BDE9882911A02192DA811D9B3DD2B2C7255C15E3346D6ED745C28A1F3C7BF4CE2DF9213E6FAB9CE90D7941C86E5EBA1CD90C9D12B94274D2D2C3AF727690A425BA8DF2527B26071D5A4C969EA61B646773810513A1AEF7F7E6AD5C5922569611CE5E94B674069C7914EB0CCB3DD03842A9C32302EFD8CAF9A1E4094339D7E857C994FB30C01D7F116EF66D8A502267848E38B080F0E5206DA26549FC7EC8F3D713F1241A09941CD7EA71DD86044F909A0D8C67361996D12E2D42C16E08CA7F789DF296C00393BFC83E47AA8130454F78DE07149D4FBCB304810BEDF462542B4B24A1A1D0A9F2B5B8706431287BA88B026E329E8865AB4F0AAD74D849F34945EDF6B3719E8103B110404A8FBC300592807851C442B506295B2FC76A600A0F9C3B3D796CDCD3C27B10FEB1BBBB462BBCE0BDD33292CD873D2396B0924BDDF8DA7408C4E680956DAD992E45925E9721985D4547BBE2684F4D4FD220FA87773447BF7A620F979FD529D86D2753F0E77C498E02B1EB55812D9E19EE6C99A61543EEF1C124716448FDDB46EB2D460179148DA2F01AA91C9B9B04A350A63D98B8CEB6005A39734C8F3CF9094D650812E1707CAAA98EC35D4ACFE425C48E4D8A1BF190DA3438684A27564255C8E5D1A97033F87077429711128BDF396DEB75E304376FAB9CC33EBA906D3804819534817EA309E3C260F9697F55BF4AA5C08A8A59EAB27BFCA0C2301434D7B490312CFB5095BF9948E3554E5409AA74EA7BFEFB9BC7CA61FAC565F2F7384F5832C2C29FC9F5D1EBAB56612C6696DC93FF21DB4DCD87F09705EE062DB948F68C6D5F7D1886059C87604089ADADA5DB49EA2BF3C3813A71018F1F559B2D72E35A013E3D9CBFDA480B43E616B9C7A\",\n          \"k\": \"44263624052C18E3AA23310697414499F1C0EAE45A1060D84EEB65FCDBCB5733\",\n          \"m\": \"76D04F481E68B2F901ECAB58B6369A2CC31A9DCCED82A1BBD426BE0AEE266AEE\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 28,\n          \"deferred\": false,\n          \"ek\": \"92D1A81751C40C606885C737EFD2B599413311EAAC707939B37500699131A44535F21C5AE596741F7668525108B4B7AFBA814FAC8AB0063B6A9060CED936CC6DA2CE4131695A89C35F2BA2F39A27D3925775FA9F43486E4C95C165A666FC3305AF30B419611D291775E0F08F34A65EFA146E46D207533B908F744BD246A94A4A35137731D02AC43E779E262A66F668784B30B231D83E4369400248AF3EE28432821F07B5020725C8D769B305B3AFA685A42E28C4F0E35BF407549361A67D7B6699CA0F293CCB776019585759502792F8D76A3698872F817A0C621084E53695701795ABBE16C466017BCC02B518EA387103C59D17127B844350AE428929810559A08BC91C2A29DAC3D6C14DA0979DCB4210142C6CD5B7CF18CB77E2E13029C3C23D2089C295411560024AC2AF25B94FBC14796652CFD8A524B6ACB8D9A262B7C26A279BBA7D4995A92A5500E081864200BFB51D46686AE14130E3C5A728FBB76944BA658718FC041DFD3A2480B9B6658A9D595BC4CBDC105BE019E128909978240EA29DA7C66664E17183E0B44969B284DB06D4311751DA4ECC6CC75C06395D5B9537078D24E2091AA45A92D18378415F1183C6B4E7546A1A1792CC07384106A5D5C8B1A369D3D6A8C83B927B72C1FDC7CE27449A5228C85BB6B0CFA85954C0CE5A5BB947F68C8107C1FF3B7D3D4900FEE59206B4CCAC5A1B4E65465609692F76227EEC0721A59B92262DB0F735E391343DC5836BBA779D6A558F8BC0001388E8363E3CB63CE49C4C7669C82B2B650B4611D094707571065B943F2108BCA33747367AB953D9423AFC5609591BF49B8A99650E4D8010617CC58645080DC0A141C34DE1D69E5932032E7B1BAB0CB2A8BAC3506B7D5E713DA79CA4E177A6CB27A545C9A80B3A489941A47AF84F59F292E314302ACB8EF0006F50A539E319951F6CCEE9F478773A8B0AE73C14B729EF4C0B89A99B87F4C9B8BAC735D31BB833342BD501EF458F955496138A6D07D1777A9489A24C74A5799A70C942FC839D20A8C228F7453BC29C02BBA3B0827801143F67691EC3481F9609BF79D9A8B7A1A7A610B05856B5FC8C3521968ED9695A00D71FE8C390C60A59D6734C608B7AC0B4643F7BA1DFD05B5BD853C9432269A9555E3912E9B263C7B939384A1794A50F8688296869AADB4B853091A291E42F485A6F93547E03BC1B57A603C81B7897198DC59252F9805A6266435EB2A26B6300D22667A878C3401800E6612C026C4F0FF99C889531D637036126227B674B95A38A2A93497FF83C8D3143A5398BE9C59909800B02C677B27A42621C190D865AFAC05513F72758B494585435F2357B97342D951A2AB23A1CF8A229BE909487BB2B8F521B09E0C4849632BFCC821CE30025B837A455B2D7D58EE4B0AAE1A25F8A5693F62B1AB77C229890899264BF63189ABBCC80AD1B8ADFDB21B0C2481342A137FCAE8A64B1E21C805B187AB7C1B637D57FCD8811E49C1D2A065848A769B7F02D99E40F4BE3783DE3AE4FE97E23CA716AFC0814C935293641D7C40EA1088EE89C2A43505237A593565A05065081F6181F35C55338C427CA628727DAAF8F5B5322E34488904949E45C61BB915525676ED2659EFC97C6A53376478B629FB32D49047412A49E98F186564A36EEF1CA4920C912B1211B\",\n          \"dk\": \"96B453C51B12941167F1E155BF08755F122716B9C82F92B94DA348B5BA4D8FD517A1756B82522444DB1F28172BB0C37658351DC545053D7995D6591897B39B3A7478FFCAA784D4210B0B291D81AD3921BF093B4329C505C63C1ED5B481F30CA2CB3A9951F00F5091BFBC21319132A0C4B46980248014967E687102D689B0CC04813503988F2C1FDC6CB62AD7616C1A85A12134A6F57F00052AABCBC990CCA1FFB91D14344E4A193F9F6346E8455221830D14B9216AD54E82432402CB51AC44A988A6A2D6035BDAA69FD46206C83B8FF43C33EBCCC47B22B3E57151E09C803869709A5A9597E1830B0C050F87580C23A64BE7C82506CFD238A36F34CF613CBA3AAC9358B84043D9A1938CA244454D7F6753E7A2012D5497F437662B57791189C7878B04617A719B659416BB22ABA04E01159CC7745802DB76F7334CE0715956065F01F8BB2CCA7F9787271CD75E4A918B0462B715ECCBEAC34ACE288A0A23C98924289DF32F13B04831798D9DFA64B229CFEB380F4F70A598A93A8C3A68AA7469C2027101B30A26298099A8CD456826EB5B21826A0C56F4C619938E5150523BA79595BA73B8460A681391B052CA2060AD68344A8A2CCD2F64A211C3A3802336FC583B7C21C53311CB89793D7DB5C68EA15FE49158F5E73E065574EB537D382C7D82B14231732CC1633686E53C723A1F43874AC6B543C4D99335B327DDA3BFC49119B00B3576321513D156E415A2D85689641A7A782800DC234BC8D589F5A384ADCA2A91053E41E18F2AB97F89E98B0C7984C0497EED8B2FD9D828C4BC7676C54C009D994E1786A4B177B7557508947BC8C706FCE8271D4A9630F3BA9D993ED9E82C5CE4A4F2BA83225C08E37A43116708A7B27C1B3620A539334F846FEF6084F8193071BA34CC9B919A0311FD7526C647A38674C81871B4F6D49DCBF83D46C545E5C543D26472EE9BBD04628B0E5434A2B342FF3A950DFCBD9EF078EE8BABBA09BC420967EA06832E78C2946746224B1FB539BF1E44CD67A9BD42F9C319EC7244EA5088949925778304E1BE08A2A9048B37B225325B22A3FCF2BAFF45C55110175E930BB7F6603F95577325B809179A09662E948453DF33313620BF195155C2D36E2B7B4BB4E6AE680B5C9A050A341B25D8BBA007B98C4AB554169952ACEC26864B9CD62B20A43BA2221903516BA174D25143D872B6D5C1C3961950D08EE1350E5700A111289987619E8FD5A18067A94D0B16D27318E4623BC80312F5017FFD241EA1A58E8F4A19EC0B4B7CE44E832A80D1C8C39FE7A535838179789115E83873CAB5BAA2BFEDA8CB07414C3BB0BC44C09D5765911F3233A7C32216914C520A5BBD63944C0153A8054B2E825A83ABBD3C564E7FAB19D081AB59AB138D1B250455B5FDDC16A8D080339182C1540BDC615D0B1C9EC1DC01455B4E59E5917D26BC0EC746A7D300D836B74D0BA16099BF4D87A9C6526BB8A5A9115C59B5F538CC0A17B3965513D92335487A4263C17AD4BA9C6144A0A4AA4DB83EFFEA9970B96C01260E91CAC3D652C88375C50CD0516929B364AB3BBEB15D68FA83C4F72449C47112557741058FDDD3CBD6898241E27FCB16090280002AD686F43A91F9186DC6898292D1A81751C40C606885C737EFD2B599413311EAAC707939B37500699131A44535F21C5AE596741F7668525108B4B7AFBA814FAC8AB0063B6A9060CED936CC6DA2CE4131695A89C35F2BA2F39A27D3925775FA9F43486E4C95C165A666FC3305AF30B419611D291775E0F08F34A65EFA146E46D207533B908F744BD246A94A4A35137731D02AC43E779E262A66F668784B30B231D83E4369400248AF3EE28432821F07B5020725C8D769B305B3AFA685A42E28C4F0E35BF407549361A67D7B6699CA0F293CCB776019585759502792F8D76A3698872F817A0C621084E53695701795ABBE16C466017BCC02B518EA387103C59D17127B844350AE428929810559A08BC91C2A29DAC3D6C14DA0979DCB4210142C6CD5B7CF18CB77E2E13029C3C23D2089C295411560024AC2AF25B94FBC14796652CFD8A524B6ACB8D9A262B7C26A279BBA7D4995A92A5500E081864200BFB51D46686AE14130E3C5A728FBB76944BA658718FC041DFD3A2480B9B6658A9D595BC4CBDC105BE019E128909978240EA29DA7C66664E17183E0B44969B284DB06D4311751DA4ECC6CC75C06395D5B9537078D24E2091AA45A92D18378415F1183C6B4E7546A1A1792CC07384106A5D5C8B1A369D3D6A8C83B927B72C1FDC7CE27449A5228C85BB6B0CFA85954C0CE5A5BB947F68C8107C1FF3B7D3D4900FEE59206B4CCAC5A1B4E65465609692F76227EEC0721A59B92262DB0F735E391343DC5836BBA779D6A558F8BC0001388E8363E3CB63CE49C4C7669C82B2B650B4611D094707571065B943F2108BCA33747367AB953D9423AFC5609591BF49B8A99650E4D8010617CC58645080DC0A141C34DE1D69E5932032E7B1BAB0CB2A8BAC3506B7D5E713DA79CA4E177A6CB27A545C9A80B3A489941A47AF84F59F292E314302ACB8EF0006F50A539E319951F6CCEE9F478773A8B0AE73C14B729EF4C0B89A99B87F4C9B8BAC735D31BB833342BD501EF458F955496138A6D07D1777A9489A24C74A5799A70C942FC839D20A8C228F7453BC29C02BBA3B0827801143F67691EC3481F9609BF79D9A8B7A1A7A610B05856B5FC8C3521968ED9695A00D71FE8C390C60A59D6734C608B7AC0B4643F7BA1DFD05B5BD853C9432269A9555E3912E9B263C7B939384A1794A50F8688296869AADB4B853091A291E42F485A6F93547E03BC1B57A603C81B7897198DC59252F9805A6266435EB2A26B6300D22667A878C3401800E6612C026C4F0FF99C889531D637036126227B674B95A38A2A93497FF83C8D3143A5398BE9C59909800B02C677B27A42621C190D865AFAC05513F72758B494585435F2357B97342D951A2AB23A1CF8A229BE909487BB2B8F521B09E0C4849632BFCC821CE30025B837A455B2D7D58EE4B0AAE1A25F8A5693F62B1AB77C229890899264BF63189ABBCC80AD1B8ADFDB21B0C2481342A137FCAE8A64B1E21C805B187AB7C1B637D57FCD8811E49C1D2A065848A769B7F02D99E40F4BE3783DE3AE4FE97E23CA716AFC0814C935293641D7C40EA1088EE89C2A43505237A593565A05065081F6181F35C55338C427CA628727DAAF8F5B5322E34488904949E45C61BB915525676ED2659EFC97C6A53376478B629FB32D49047412A49E98F186564A36EEF1CA4920C912B1211B1EAAA1990D4FB6A021D7CF8F417D45FE1F49BA84E111A448E8B7DEBBEF902A966BD5EEEA2E6D38487D083F30093ECF02E7EDC4FD4585C73EF6E71FAD24E15E2F\",\n          \"c\": \"2E7CDA2E97146A7BB3C33C5EF76D1A4F4D93A59F1B8441BF6A32D88EBA5609490CB3283DE2C43E4D1DFF2DB55E4DB9B4C3A377B3E9B33FF1CD3D6A2047C7FE0B6D8155DBD4C0296E8CE60C74DCC82080E31AF13169D638EE6396439F49AE426BBE5AC6BEF9B2BFF423AA24BD2C168E0F4F2078419A5865F1808B866FBD19CC221791952D9C2101C3EC3A6F597F97C2268F8F6FF273E4B443B8E95D93B6AEED85F71509ACA3F366938E6BFECC3B0A35F859D3EB486BF321A1F3A7350B39F7A89773DA2C5B235132C9580380DDCDA3A910E89734F03F871FE504BEA38918299DFE7C9F60A6E4CB607768F0A3338910D45612B31BFB6A0424489E0A4E514D2F41C3B4A0001E794A5275F8D047C892870E647BBED53BEE167BE27EC2A43D2D7DC10982F96E3B586119D27EEA5909A18800B79644FC9D15CD7D2200229C1380FE2E939DF89FEACF4834DFD1D3C8ADDB8F365BB94359C4698AF15AAFD4F3289233701C217CB4FF979EE781C8420ED9EEFF53D58F046B774B821EA3021F7DFE33A79F882C955C86FED0702AEABDCC6D32186B7D40DD325B9FB7BFDFB1D34C63B19433F0D80739765EB9D8BD210669675DB3F4349BBF23B49B7A967CA2304ED8F143D27981C26FECCB1658B5BE11DD858BEFC3DEDE25DBA9FD22341E63A5884C41A0ECB68C543E0B021135BE381D42DDB9F67CE1473D8840E00138B39998018E0E869FB0F94823A5191B928C7D13F157318901EA8F8E5A5A0DF0ED71FD2CBC6489A46E5171FD14A09F73420C77947941DCCB4F122866E93D94A9DB0030A20663705B11C93E89396F1B7E7728B6B450AB5DCA0932850190D712E3F27EB207473D18E29B20B433F4E6BBC99B28AEFB5ED0DC74BA529377F0F8A93BB7208CE98049A862FD513E81290187A5B2765E4EC5B4F211058310D0396CFCEB90B9E86E681AEC3D3D81C787A3BF16A412329AE643576A50F2A72E59165AA357ADE9C194A4DE0ED5254FD206D05BC375D1B5E8960B7293C768B7A66796DE0D5587752CDF7921C2053A5A970B9FEBD7A20F336C93839D567D1CE241F061565A893A409EAC2645C02D3FF00AA024F31E50946A8CEC435508486AD757114FC138E57B42F2CE12A248355CF35191341892DD910DF5528306C947B0ADFC0AFAD68DE715E8B2D9A43B8858BFC04F73B44A04C4E0D331DEFD57587276B188965C5924BF1118713C05E975090C52C4DC2BF7BCAF47E4E274DEEF4FEF3D91EBA65F616B8C476FB9EFCE61CB8A0524D97C27491A0C9BD7D99B0EDDB2A3E50248793FEF1C248C15301A3B765E9AE21FEA0AF86F09A5BF42D21638FF6D169D6127463962D3BA17F5CA63ADF63F317CE2B7CED21311A05CA842E0DD6664953DA479851E80F270B4A7FD11C3FD6A52862716AF8A67FEC893BBD104F5394F118D579B787730D6C37AC242A328F724DE9C0AC6E091A3E4CE01E29400836ABB6D1363E049C3CDFF2048F0FB1D36FA1B70070576B8A14E766CC098989EA9C624446DA2D4D45E7381AF63041EDAC0197149AA0E\",\n          \"k\": \"69B8F091A450890C0DCCE0120E9BAB05054C7785A797C93B6FA39FF5E0BC5A70\",\n          \"m\": \"FD3C91294D8C974930B4B6135AB647D4A7885C83FCDCB30CBD38332E14094491\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 29,\n          \"deferred\": false,\n          \"ek\": \"CB2468A0185567F8A60ADB33CA5239C11C4A3E0C031D385DCFA28C3AE2A9F71904BF379CB9E0BEDEAC82B2A537357A9C3AD33362602A1CF6458A745ABED9233A092B962BBA2B0A66DD4A85DAE11B0C28230AA44C40F2B68687B7D54833062CBB5B0233E25496D1C84204B2BAB06050C00C308EF53DD8345143BA810AAC477C8119B1C595D964B13F837849B58D8E1BCE56437F2ECA84B86C91173670E0995D9769642B0AB0BB0713D313AEB7C41C9B2CC411B3B62B110308D94F0DA1BB69D406DFE7286519692502BC83E15BD50494C1047980AC6043B18D7CB72EECB00EEDA515C97C0ED08B5D4BB001BBF08D9821B9A75C02666C357F00278279348462759A602359F5A953247C1928172A013E3C53B7E95B81A64A6DB3AA86CD7670CF7C38AF357A317B71671B950E2BB85EAA6138A5AD93A1640618BD98092D85D36015C0B0DFF6059F3B09AE5007F89AB551230D8B9A2BCA59238405372BAB7F4D8069FF7457EF3720A171C3A6312CC8D69191CA909A13946CF8577A96086893C0C59B026A21835543B8583A05F569977E072299BA580C179EA2976810AAAA93F57DE223B97CBB4CBE559E18139372EB1A2095B8B509AE2CEA5B4944335ED385B23B5EB7D0847A557AAAE89611937495C825B7236280ABB6D409CD513A03C91139A251B3278C929DB23BD815BF68007D50B8356C03BE88F86E410ABFF6CCB3B11BC5D41C7839D48F365AB4E9E29F0B0094A4C4ABBD6761527850286432530241968122D2BC877DF5781D17AE12095C177B69BE3B989E81CB539A2FF2E28D623303FED5750C58B9CE051A6B813899BB3C74D82B2F127E51030BCEB396EF547ED37ACAF1D46A77F7B2B24B48AD399B53FAA69C079FFEFCB9F8367F7C513D142C8392A8BE6089CC2301685460BCCB19BFD6AC821DD840A3DA30C3208668C7103EAB78C6520C1291239DF8217C25450B70302020A4BFF384F0619142A04C769C9B18D27E42FA30BCE60A1EC2AFB618752A3917EEDC37BD15C44D0ABA1FA3A40C23AA14C98E016A592B459BF4C13C07354A1DE0539445882E21B97B1767015810325086AE71CDC0B5C0D7287BBD99B381680BD6559B04FB84835C4419316FCD223B84D03816D86C738334FDC894BC81B1E26813A2159D426AB1FB70A88FFC1782E649BD860D148B33F7607083A5928F1C835F880DC9E3235EA51E78A403F7BA726D17951CB7AE7630AA7394A80D95A1EDB1059F140F3EBC8D1C9BA9F43B75ED90538CB219F8E4A2B202084E049473D57D987CAAD2B51DB009CF8A538F8205B0B4049F41ECB344F84C538CA49F5A8FF309D036C6C520932F08072F5678CA68568A41330D9BEB34BD308F963224F80B5DB9B10E54D146CFAB5AEF84A7F9C62B7BC24A26A578FCC51E36738BDCC8AD24410155AAB74D8691C1E699E3722481BA074EEC75DAFB723863C308F60E10D195617BCDB877153D9CAA775647A83B1DE113019CD6C3A7CA38554023A6BC7EEC594FA6C14DA8353737F0A549715E39C49FD9625F187C3BE1602B6BA178D1784B52690B1E1380847203C13624418C1C1DAA0B231AC0FB39293843CC3D6B48C32B15098748DB0B3672377407EB7441B82371B56EB3E90C983A895AF85D57E76C53088D944840CC309853814266D66DCE88915049579CC45CD602\",\n          \"dk\": \"58C845051321CDE3A3C19C0D3BDA49688751B7C113CAA67BDAF87204D20ED9D79E39D135E64708827BAF19D25807B35F76500445B971DEB6A7D2680D46032490D042FD59B963291B5BA589776499DF1635B1249A12C3CF42322EFEECADEC1A243738260F2503364087A6AC839D1B5B85DA2A0B337BEE74761683A0ED501F67869FA587CEAD708A2309329A7483A8A3C895241A15732C46E2A864C94A33C36C6A71400E70678863B89A5B6BB8B0129679591A87127518BC2D903F06826605D7CC0DEB7E5A95B5BCB691ED852C94CA95683025771235156B406DB024CCD430822C785C3B265633AAE6BC18E4BBC4C6C983F1F0B0F514001E4106F36CAECE1380D661242E43B585A8B1203431BF46BE69567B9A883E2F67539B4C62EA6294B7365796578FAACA0F212B8B87B2AEE6BA128A07A03F20085D8122182465D46A8C227363EA7B4400920CA22C454CEBA538E5CAA110B7762087B86B37CC1932A2B5595D265F3326335D407712D80EA34C4ABAF0BF9D4429300B14BCD2B9E186301BF25C43EB544FB32D30BC6C643BB028577142761C33243D9C038BA0BCBD7502B63196AEE79A4D8DB630BC748D7220C43F76C2A77A7D60F728EE1133DB0C7AA8F0CD1DC0100B85460EB31D0BC9658021443D6564C0D3AFFC6CA1E59C5DCCAA454156923CE7B2E8B123B1EB39EFBAA7EE0CB17F627C79B690816B5EB317210C16A1BB13A803C2453197A19E2728C9F61017D20D6C19C48D21566A583A848A3BDE79628CD4150A30AC1C44AA2673C1810B89545971D9E4199A12B82FC05774475797C6CCD0A9C699F50F0CF663DFA14FD809437E0889B285959DFA29A08040802BA77E3C3CE275CDC3A92388518C9B70BD66BC3424F67FC9FB924F59C500D2C138E7C06179A3EBD800D6F96490DB82C041AE9C958D5015A66D91320D86A243AABB8BE17260D86AE4F5A6229859811552E0BC1000F3C47543698DDC7A61CA5F68E68D47A96733F32C8437B893B10FCA1B0A6866ADDA2CC610E22D155984855A9CB99339FBCCC972F62867D7BA04DA7C10377419E36D8E3876B2C5ACE1380A505CBC418A181196B150B53F5FA3B9AC08CF34E78A39A9B66559A859EC22461C9D836CCC433B8342C94712EC46BF716D0628AA3D421AF6A24B02F38DD4C54E3CDAC15AE47863B0AF21214723FB9592893825031DA00664C1CBAC1585B60E7A71022A35B3E23063CB49B278CA82B51EB9E010E6A3B7C3227356733A6ED0BD3D84A6B7C1C0BFF42842168F9703B1636698B171902AC4775E1A15F0A44B39E9247FB61C0BC90BBE2AB156B11EB2CA98BAB75DB92B844154258FC6BB29C164DA177441968F094941C3FB417C078654CC6358C6143FC32BC1518059105EA8DCA282F138C3EB61A48B2D63266012966BA12654C7B4142129AA0D5841BE0B5784A94CF1007FFD5112EE21AE82D7BF3FE0B1509710569CA6749A0B706C5B79A883BA985967731276FC2B8431BCAB9B1E2E9330FFAC9DF591AB5B17317AA93D8903708CA8929EFB8DBB2861E067CF57A2606F174C5DC80FB26A70EDEA0D849A6977D39866A78F3AA417B3B87D1EF00A7AE2734F249D64C68A84F016DFA57C8FE72443848EB058A5D5B784CB2468A0185567F8A60ADB33CA5239C11C4A3E0C031D385DCFA28C3AE2A9F71904BF379CB9E0BEDEAC82B2A537357A9C3AD33362602A1CF6458A745ABED9233A092B962BBA2B0A66DD4A85DAE11B0C28230AA44C40F2B68687B7D54833062CBB5B0233E25496D1C84204B2BAB06050C00C308EF53DD8345143BA810AAC477C8119B1C595D964B13F837849B58D8E1BCE56437F2ECA84B86C91173670E0995D9769642B0AB0BB0713D313AEB7C41C9B2CC411B3B62B110308D94F0DA1BB69D406DFE7286519692502BC83E15BD50494C1047980AC6043B18D7CB72EECB00EEDA515C97C0ED08B5D4BB001BBF08D9821B9A75C02666C357F00278279348462759A602359F5A953247C1928172A013E3C53B7E95B81A64A6DB3AA86CD7670CF7C38AF357A317B71671B950E2BB85EAA6138A5AD93A1640618BD98092D85D36015C0B0DFF6059F3B09AE5007F89AB551230D8B9A2BCA59238405372BAB7F4D8069FF7457EF3720A171C3A6312CC8D69191CA909A13946CF8577A96086893C0C59B026A21835543B8583A05F569977E072299BA580C179EA2976810AAAA93F57DE223B97CBB4CBE559E18139372EB1A2095B8B509AE2CEA5B4944335ED385B23B5EB7D0847A557AAAE89611937495C825B7236280ABB6D409CD513A03C91139A251B3278C929DB23BD815BF68007D50B8356C03BE88F86E410ABFF6CCB3B11BC5D41C7839D48F365AB4E9E29F0B0094A4C4ABBD6761527850286432530241968122D2BC877DF5781D17AE12095C177B69BE3B989E81CB539A2FF2E28D623303FED5750C58B9CE051A6B813899BB3C74D82B2F127E51030BCEB396EF547ED37ACAF1D46A77F7B2B24B48AD399B53FAA69C079FFEFCB9F8367F7C513D142C8392A8BE6089CC2301685460BCCB19BFD6AC821DD840A3DA30C3208668C7103EAB78C6520C1291239DF8217C25450B70302020A4BFF384F0619142A04C769C9B18D27E42FA30BCE60A1EC2AFB618752A3917EEDC37BD15C44D0ABA1FA3A40C23AA14C98E016A592B459BF4C13C07354A1DE0539445882E21B97B1767015810325086AE71CDC0B5C0D7287BBD99B381680BD6559B04FB84835C4419316FCD223B84D03816D86C738334FDC894BC81B1E26813A2159D426AB1FB70A88FFC1782E649BD860D148B33F7607083A5928F1C835F880DC9E3235EA51E78A403F7BA726D17951CB7AE7630AA7394A80D95A1EDB1059F140F3EBC8D1C9BA9F43B75ED90538CB219F8E4A2B202084E049473D57D987CAAD2B51DB009CF8A538F8205B0B4049F41ECB344F84C538CA49F5A8FF309D036C6C520932F08072F5678CA68568A41330D9BEB34BD308F963224F80B5DB9B10E54D146CFAB5AEF84A7F9C62B7BC24A26A578FCC51E36738BDCC8AD24410155AAB74D8691C1E699E3722481BA074EEC75DAFB723863C308F60E10D195617BCDB877153D9CAA775647A83B1DE113019CD6C3A7CA38554023A6BC7EEC594FA6C14DA8353737F0A549715E39C49FD9625F187C3BE1602B6BA178D1784B52690B1E1380847203C13624418C1C1DAA0B231AC0FB39293843CC3D6B48C32B15098748DB0B3672377407EB7441B82371B56EB3E90C983A895AF85D57E76C53088D944840CC309853814266D66DCE88915049579CC45CD60235DC954C8CA6DC15AB79B2C974D77BA09F049C2007BFD5F81A2BE06F178A0EA5FA1646B083D3C34FDC56A8B5797E26890EC84F86E18EAA17ED3DDC78300313E9\",\n          \"c\": \"1DA1EF5325F46C686D3AB385F8AA79758CA0E6C0092265C636DEDF9C5F34A0F7A36783AED59E21EFF5A8CEA55439E5B13C42AA68E1C19BCD0CA8C629FFF79198673D416A9CE82DCB80D7905968B02E84EB04005D0AD971700B87A023708F169369DED4833B8C13C8C277CC1CD7EF32488DB63E5C1058CCDB73F88A679C41A36144EC2866130D68914503889E783A5E28A1E701B0C198AAB245E6F61337CC9B1CE2CE8B8CD6EB106B969E120CD09EE174E458AAB80ABB5795B091E07166A39F15349C0EE271D063100D07E46E9AA07DE76DF152753EE298930E0172900F7A4E47128E5BE9CE81A317B07282E3735AFA02FC0F89A6561F5B4275E3DBD31FFE2A04947F8CC6067C3A8E8FB625E6BD23BC20F63DB535FAB0E2C44CCD50339959D3A83AF0FD57AFB2C6BBEE6B9920D56A805447CBF7ADB6F957B9DDE850044E7DC47ADA07BAAC747069241FE4B46F1F1DD8DC2E4BF52ED6792ACB987A1528B89213E10EAC95D86519A95EF6EF5D9701971AEC0608EFA2A51A5D0127B3BDFED8E8107FDA600D17D913ECDD8D9860C16E8788CC9CBBC99EEA2A7AD8CF35B85670A0B15607F3ED98D88AB1A6585E1E0561C37DCE34AA00757BC1F6CFC81C7BC2EDC7A011FF12C1C35CEF9D1F8BE5B80860B5ED0707A04472E94D2C3D7C1B1BA4611EFE6D023CEED3B486A066E3B0121687DD9AFE0C4771678EB7B0D85D249C77BE8721B89DC086C4C5F14D9851C51D51CA2646A32929E36A33A35EFA58B0978B2DEE5CBFCD23F3B830CF1AF3EE6743538F82E246F7A9F76B6B8E43C84C9539EAA2A0DAB6EDECB4061B0B211C5547574088B8EC42BF6F21FCF299BEEC8CFF41CFD1B49639032F4ACAC92251B9F37CBF51098F4DEE7D88363A1910C9A6BF689E8DB93EEDBFBE8FACC4D1707686E1BF9E5E790DDBC6874218FBB43128783F611D1EBAE677D526057A87FD33AF449648EDF506E93342CDD38AFB6EC3FAE952101B384E841D889C025FAD91099F2AE41EC3E3DEC70252663C01B4B04EC1501422A97B5AD5AA27CC9EBBE2C22BC22B8C706F04FEE274764F1DBC4CA60DC56631BB2CADDD5399A2F061FCC21541D2595D15CFB6DB464775D4ED48559CCC97DD25F64CF2FCD30013EC35AFA96C1E3368CEAB29AD03FDD5B9BDF1FA1356132210702466719DF52FC34A0A1479FB913B6DFD9CCAA0F9D672AC618591B808B4315A5E17889D99E271FCBBC3C4B496DE8179A74C1293468392E2B592F3E6925B9F81604790DDC3EC0D1056F31F3184FC0330497961EC8E2737FE866AE4C262E5218E06EA7C24B464AC7D5FBB44069B9BDAFC96E014DDCD168C457140078B0A7DEAABFE04773BB1335497CBCCF4083E6D41288B3901029F1B266AA938A9F14763C679DF1E1C58EF406BC2ACE2A236B37557219DA24812036E557ED6B6C1A3A1776C5C0E64E1AE1A2A0747CF2CC55E32D48A7B1387FC9158222AB2582AF43580043F858E527B25379081B97CF0BE6AA5653E186CA066BB7D57B6C4AB8131C68423B12622CFE234696D761E\",\n          \"k\": \"C21C8C4B59906D0C4ADB1F3CAF47F9EB326B8A62B3392407211D502F40C7E07A\",\n          \"m\": \"7DB18CA35A53AB3A65E4C17FA096DDECB19FC7747E657B49D1C1710DBD1D197B\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 30,\n          \"deferred\": false,\n          \"ek\": \"0F613B04128F82A73867D9185891C29D6C3E1381843BD502D86099A740BAD5BAC68C590510CA3F6F2B5463B264BED34952F10C784A92AB7696268410F1F28C06DA7A18416A7B1B5FD23393C33592248C9A8B3956A999483E000F2A2C6F796052FBB22F7F182A191602FD93AC066355B71B7E6BE36E531B1D34F0382E34A4CF623B7B1127519A7BC4EA3FB0D1C91626A417B6129DECAFF2865273F759B4DA1A95F79FAD0CAB09FB61D34B9AD78B8046F5601FB53D28951AB73842B8921B2FF10417DB4F9964A41EE820FBA83D61D30D0EF2C2CBCB1F6CCB9E77523DCCBC37ADB8B9E31A0F0E1C4192911060E8677140690EC671A5445F42FB1A68DB4D1678BF9B60129D98A859837599ABA0DF465DAE76972946C73E8343A19A3C03657806574F59D2611163334FDB0EE8B0C13C679C6D175C22807C86F0C199C89CC43C0DBE6587F0A36199143EAE30116B3B0D49839AA18CA49E2992740B5DBF1C91ADA352D39AB7D0C23FCCFC41E783CA0A333FFE00074E72BC834669931630898B718CD5304E253071E730F1EB067F94861DD98FD9A262FAC919FE870E3D21BCFBF67180C57A5D2797F6C7B96F544CDF92C3FA8B49EB366C02885140D128B88225750733E6105868AC48A468002F35C34C38AD70A7BEC9C37713B98D9EA8A716CBC85D0C3EF5B4BF8F20BC6BBC61CE8150B4F842CEE0A40E0A7FBADB76BE9B5CF2B39749F51BEED8400822044E2CB01C021B0D9B7FE67A8AFF9B227C120F643B85152B40EC4A1734E2A141159A5A96C8B74115A0C92E6B913464994E980A8A304194A595BAB892801C15CBC5033FFAE70F368A62C65A598773C199E20EC07B73B1ACAAF48CC963A6986D9A3D2D236352F66065025751B9884D7CBE3A5538A9B90372D4C22797659B5CA8D357CF00B27C5714344374A5131B261D948DE22390D0521F50553E78223388903CCF241FD608CB4FC1AC7C44A2D3141F0B998B99E704ABEA1E634C1F44231D347254DC2CCDDC114AC4488A549BC4696400F0BABA18198F46E8CE43C93581FB661C745AB5A5520D4450982569BA1B192FEACF37EA8EC8B8B267271A042022DFA177A7C1CC1CF6765492AEC59729D6F61690F27B5D010F22C30E44D15864395BF01771158BCC5F8B48F656CE5D9049D6F1C11CDA96F8C724E0460BDAAA1DD9CCAF0C46162B943461B729AF3543BC9A8CE3B34971B39DD25A6051D41A248BBCD5555D7E2028A5B453FC4062DAB1337B134009C71BDDD705C2E44C64E15C18F26361D8A7A2F7A13D3C31CB3147ED777024B8A90B558812B47AC861BE5B894B989200BEDB242E559801C164003C6EFA61AC34649993F782CCA150DEE1443892BA3DB87B094C491C798B5FA1AA30491B7C7CC3769476D28AA17E754773390316647CF33076CC6CA84C804B26E75295D2A4754B505A3831DC793ABD874C6C1451911B97AD39825EA42D65DA63C42CCE1EFB1397167F1E8933399C2E288AA57E000406A4C66B5A357AC59036F37A52516701B52934C6A1109593809C0F72170A0EC9AD22EA6DA64B0028680B405924287280A8C49B2D516DC9D6B93D42A9B544C6833C0340865905EB00F4661B200686A0A47FE280937BB00F8022B8F0E64AC251BB62D09FBAB3E7C79CCD450EECA94120B05A0B071588E2150EDA6B14150F\",\n          \"dk\": \"E434A051F4253EDCA7BAF48841E07438B892F47ACBE249CA763061CD3B6C9112629CCB760107BA96C260793CBDA5BB4EFA184DE27432683C96A20B39D1B72C4FEA3D05D299EC16CB7BCC7C853B83E92AC8064C38DBE256C86C711ACCA9B0C480A0444C6F8C8EB187C32BE0C5A1F20EED5365C32656AF7335275920091A38EFB8BAF8629E87928E51B2370FEA2B66D188A09264D21CB0AE66B5590A051CA16CE77CAE914003AB994691DC0A09FBCA27644A7E48405AFC5F4CCA9525C113DFF133C1BC057E742FCEC38531EBA8E2D252C543172459817C18774CACCFEBC227D267B5684C325E17C0EAC74B34D22E136B98C9A8532139ADB4ACCA2F3A63A21751D0324CDA8A15230918E3856A07C4A51813569B76093A14771E3606A33C389ACC42453C05027501E9B5AE311C9D2D640E165816FCA60BF6C5423C9C585D642151F7314C9C656E8C3572C407C6DC0EEF1169AF4B5B130ABC0507ABBB52681135984BAB688AF080F567B93F1C2C37E231828A3106600FE11429DDA8C673380105E26423A06BC97BBB7B619A5E54429EF549BD9120BE23892073777F8C21E3700C90751BE53658BA9035380ABAD8A6522DA19ED801646A772353DA9355A75C65164940189A79439985545863DB9896F95F7553BE184ABD982C4EE692CC08046A8951C95EDCCC3B5B942359B82FF4C3ACD86AB0AAA5DEF49ADBD344291A8A15F613A1A8591710BC1B854682659D7FABA1D05924F063942E384953EB59CCD55F3EA4081AB38E13AA12DCDA528CD06907018E9C287042583191B8BC75767266A0C9AE074F0CC4B946906941D17591014ED2FB4A0A3A01179C26BFC85A5EB9A859284E13F04F65F589FCB9557EE5095AE79036033E2D5336A0AB191776A4CF01A6F45B62F3B8767A8413DF6539A5B1781DD30C2BB41A940A4E2D209526E600C713AC522C104BC8C428895F35547E4DF1214804869EC5B92BF9C2FC28A361E050182805E4919BB4EAB368EA0417E03D8611CB09F884C05B3E66294A93EC62D694BD5393AB3114046E29B3B177B997F7CE9E2305BA257A923B10C7A3BE7FA881DC1376FA879BFBDBBF0B8000DEF00DB26A522A96598B450D5B39B943553291D42AC6EA0D1FD04247A41420DB7314F676557765FBB15BBE314EFC968C76074BC3B609268C264B0441EAF92E09E59793273DD8B50ECFFC103DF067496C8015893D05DC28ED3445D726BF0936145720CF7794290FF189B8C38AAE3BC55D2167BAA3AE39BB90D19948DD439C7D827CB2F37C17B0A0D8A74D6CA82D4448114DC72D91991972F122018197F192B4ADC4AC41EB0F2C220DF2A94E61463BCC105A174C985933B09D3326AA2989E2CB4BBD7C5B557C46AD9990BDE8183CA63CDFE98F51070D97C7AA635C8149276C58E9CFC314BE31455939C854B03055436650D3651C3D566F8129648749499AC1946F120D02A6CD230100D5A2788FC35D4C47B7CF086363193843523A81221FD09461F06919DBB5287D727957EA4A2E448D0D8CB3A8860E71E8C60EEA37885878B7E07DE6323908830491DACCA88B50CB901B384ABC4B33236F727D1A5466E5506F6100AB16AB4567937E39A2605A52783D574DA5C961254704795C450F613B04128F82A73867D9185891C29D6C3E1381843BD502D86099A740BAD5BAC68C590510CA3F6F2B5463B264BED34952F10C784A92AB7696268410F1F28C06DA7A18416A7B1B5FD23393C33592248C9A8B3956A999483E000F2A2C6F796052FBB22F7F182A191602FD93AC066355B71B7E6BE36E531B1D34F0382E34A4CF623B7B1127519A7BC4EA3FB0D1C91626A417B6129DECAFF2865273F759B4DA1A95F79FAD0CAB09FB61D34B9AD78B8046F5601FB53D28951AB73842B8921B2FF10417DB4F9964A41EE820FBA83D61D30D0EF2C2CBCB1F6CCB9E77523DCCBC37ADB8B9E31A0F0E1C4192911060E8677140690EC671A5445F42FB1A68DB4D1678BF9B60129D98A859837599ABA0DF465DAE76972946C73E8343A19A3C03657806574F59D2611163334FDB0EE8B0C13C679C6D175C22807C86F0C199C89CC43C0DBE6587F0A36199143EAE30116B3B0D49839AA18CA49E2992740B5DBF1C91ADA352D39AB7D0C23FCCFC41E783CA0A333FFE00074E72BC834669931630898B718CD5304E253071E730F1EB067F94861DD98FD9A262FAC919FE870E3D21BCFBF67180C57A5D2797F6C7B96F544CDF92C3FA8B49EB366C02885140D128B88225750733E6105868AC48A468002F35C34C38AD70A7BEC9C37713B98D9EA8A716CBC85D0C3EF5B4BF8F20BC6BBC61CE8150B4F842CEE0A40E0A7FBADB76BE9B5CF2B39749F51BEED8400822044E2CB01C021B0D9B7FE67A8AFF9B227C120F643B85152B40EC4A1734E2A141159A5A96C8B74115A0C92E6B913464994E980A8A304194A595BAB892801C15CBC5033FFAE70F368A62C65A598773C199E20EC07B73B1ACAAF48CC963A6986D9A3D2D236352F66065025751B9884D7CBE3A5538A9B90372D4C22797659B5CA8D357CF00B27C5714344374A5131B261D948DE22390D0521F50553E78223388903CCF241FD608CB4FC1AC7C44A2D3141F0B998B99E704ABEA1E634C1F44231D347254DC2CCDDC114AC4488A549BC4696400F0BABA18198F46E8CE43C93581FB661C745AB5A5520D4450982569BA1B192FEACF37EA8EC8B8B267271A042022DFA177A7C1CC1CF6765492AEC59729D6F61690F27B5D010F22C30E44D15864395BF01771158BCC5F8B48F656CE5D9049D6F1C11CDA96F8C724E0460BDAAA1DD9CCAF0C46162B943461B729AF3543BC9A8CE3B34971B39DD25A6051D41A248BBCD5555D7E2028A5B453FC4062DAB1337B134009C71BDDD705C2E44C64E15C18F26361D8A7A2F7A13D3C31CB3147ED777024B8A90B558812B47AC861BE5B894B989200BEDB242E559801C164003C6EFA61AC34649993F782CCA150DEE1443892BA3DB87B094C491C798B5FA1AA30491B7C7CC3769476D28AA17E754773390316647CF33076CC6CA84C804B26E75295D2A4754B505A3831DC793ABD874C6C1451911B97AD39825EA42D65DA63C42CCE1EFB1397167F1E8933399C2E288AA57E000406A4C66B5A357AC59036F37A52516701B52934C6A1109593809C0F72170A0EC9AD22EA6DA64B0028680B405924287280A8C49B2D516DC9D6B93D42A9B544C6833C0340865905EB00F4661B200686A0A47FE280937BB00F8022B8F0E64AC251BB62D09FBAB3E7C79CCD450EECA94120B05A0B071588E2150EDA6B14150F81E7F3A4D5E46DC6FA36B4C63BAC9B8DB69CD90E250ACFD99280A13C10C4F6F9B02201A4ED8B58B3F3F38D4B28A3C6E87D6AAB11566531DEA6FC00781E6216E1\",\n          \"c\": \"A0C773196F91C0A7A3CD3BA0764E4FFA331F6962116C3B9FFF775F47A02AE2B0BE69FB89CAD33F5E059E051B92FA124FA25810EDA08AA89F4E5838A250315952E85BF73246C4019DC0F8DC7E6FAC2C0BD1E0191EA0032221F4C5549D914145B3BE2AF25886DB7526439BB9ABB6EF57C959D9CC76404FA02206B5CD4A2EDFA23B9F137729E7FDFD46CE8CB326CC04E73EAD7DBDA6C76EC19972E10049394E03BE7933315AD8B4DF72D0582EA9E36205F07A5B3A0B007A683D677D4571B907F0F967227E5562873D45F96FF2A117040EF2AC2026BF1B6470FF40F50D0A2E53979F3F61AE0E041EFA26E058F753D2436AE9DF06E70252268CA9502859C291BFED18AB563C2A5A74EB4E572E1A916C75E8E7C6B31FF39EC44D29B581598439F8A5E7FDD72C720E703FA24FD10FDEA3A43B06CE30B5C41D2C891724C7872642381860252B20345665038444E8E1167D5CCA83FF29A40C306E18CEE1B22CC583E26B8FAD23D2FA3862CAA0A21F6EF090959982E0E6B1E58413969892A41614A76325DA18C7FE4A73F27FCF275A3495134DF24D94AA1D4DD96174922360F39441F69F8F07EC5B060178AAE1CA5D00F3DD37ED4B55DABFD28203E65205AAAE8B2A885E211A5E35B9FE23CCF991A7A5C156FF8E1B0253B42AE6A4605A55C1A0F47E8054D9195D4F2496458EE4FC64DFA8FC4D2BAEE710900120ABDFD16E6AEA23550BB1D33175B9441E04BA281998D89CF3DE29184CC2EBD2626A1FEA4BA05D1867AD3FEE28C7154EA5FB0E463268B2AD17DF5388C2A7259F044CAE83D51894D61FBB4690E6DCAE9822D63A39B2D887BB2D81E8D57085C8D52773ABE2AA9B8AB04735669311655A3B6AEA829AB486B54E54F6E2AC63FDED2FF90C8EF88C9E2812218433B59677DD9FBAB3E15558DF418DA6F3D61ED45900FB3415B5F1A2A7C600DB715EFD6A17F729FA317086180BB126BF26FDB8637B8D7C2523651A70E233C5B180403B507C04D93DD01A60591BCC77D60FA874CD3DDBCCF7246ECC63FF447DB9CF31C114D3A9E4518D672E803E0D5A9A7821855340DD65FB91157FBA1005EA5EB64E4AB762210EA81E7F93F8F3DCFA6164C5F68060FCA6E3D52F83E6FBABC47B3684C96F5F718EC731CD5CE61A3C368AF5E68EF74020A2B143D37AB268AA4DAF203EE27702DA2446915D000F55B90E2C67B01D1B2ADC07D613B6C88760E6AF7B50C08B88446B43FF419ED8994B9E380A8E35C60AB495DBFD8424F50BB85B1A4DA62597B630811EC6DEACCAB4B049AD3C99DFB034A91DD144D757D88B6DFD0E1F62F4ECFCDD76A40F6B01C6CF27FD2B3B8656CE335A194A9A7E04FCC08F234BE1ED5DFF2A73650EFE7A65A8F9298F522926275FCBA55BF167DF37CD48208F37894949973E3A3E8FA7DE0F45317C01B5A5139BB30BA10767DDAD39864C7DE034601577C600C929245C97016E82534B74575135F2326303B1532DAB96500D314B41903C22D79F7067FE2BBB33424238AC1BAA581BBC6E0DDF3A0162A8066D3D88CED57736\",\n          \"k\": \"7265696182169279EF65779A021AC0A0E0E7E4CFD37C8546D4DCB1BF08572AA3\",\n          \"m\": \"876B17263B409171B746C6936EC65FC94137F958DC974BF98110A1D07F6D95F9\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 31,\n          \"deferred\": false,\n          \"ek\": \"C9CB9FD04057EB96006455C062E3C0722346ADB366DA0AB980C782C417B360EB1C1F6762EBF967D713A0D93A28CF9206E95451E91373805047D8A14FFD2041B4468B26C79B697A14EA75A33876BB865096C0289C1AC4B91A4399821349DC66496DF02B15CA433FD97D96F46F72E0B23EB561E809601C053A35F4171A963EC3F542B423BAFC56134B7C0C5927C746E6C8055B3B70B31CAD6A168DD78F63C64FE3044280297C6630562C48A822B570E3FA8A76995BEFE67734274337F14FE00C723AF55D596BBD0B2C04E5AC6D52A0AEDB04B2BF09B1F9736E9456C40D5976B1EC2CA21C28AC761E39C583CDF256AA3262C3264250B1C19B00849FA83BCB6614DA0BB19752AE9BCB7BD16A71017066F73804AEB3C7F9213BE4634401A7AAE7BC56E603516FA2839A791EA89C58052030DB4AA2887737D9BC9AD21ABB94246546892785CBBBFBD45102C24D2F2370BE91923E732892E599D3154502E878CCB990CE8383D2F5ABF021BA424008D07685B8B54C6A6550D9CCBD93066A5A651A9B2710D25B382E72CC57096900E16B7B9868A06909F441104860BF03134316382F64D4CDDB596C57866E0776C04F8C0F67F2286B7242477059EBE022BDA200B6271AF875A829FC368E1574F4F3C56BB39D7FF61131F014FC59870FE45466D968C109339F0CCC67F6A7092372CB977C75998EBECB68A46059D7146A2EEAADDB258FCDD404D6E95E5400C8432535EFD635AEE5235D3B8ABA4153B46C7D234AA94DDB66B229456C41B2FBF93F39AA048C158E50C312FD623DC1E818A494980A34C41568942C30373E430DD5250D0CC27D4AAB2AAFB13CD719ABC7F466B702A983318B8D0C3040DC56B0960291F30EA75986917A17BFB76BC4B4AA8EA396F127A41F73219DD06F27126A5F06519E328B5CEA9D67AC122B092FC3613D99F403DB86CCDD760BF8398C1AD6BE4B5474C34864F2B10D97E265FEDB464E01CB3A426A3BD8B3457267317B7649D62D4847C3AADB7036391CE068758BF1847B96C5F3A69BD36A9468F9CAB27476456B4EB510BB063C46D529758C6680B4588CE4F5CD9D225BF700B27F53C13D49808D561F0413315C01989B1582755CC4CC765391AA68A0617F39B843526081658532C2C0025CF378B31411E867C978109454818916F8052336CFC9788F87E53236DAB54112B876042FE9054D9C07B80F7B43AA900BD2580F386058CFF5624A9A062474990C2C126E22BFB300C452306B859201B3C67B7356CC7DCC36CAD7A7BFC44DA0591D8CC45BC7765E405A2F2373996A72433F332CF77A5C9F3146DE121447264A84339A652960109044B1FC707680AF70E457B41259A1B84C37892C1079B8F08204F218CE85796701B05BEE5961F55584FDD3BBAFF25F53F78D0B48266EFC4991E090EEB28799C002CFE902D1222EDB8A026813295E604921269D0E0A1F4CC70F4E528A092B4E15300EB67808ED20995EF3A80CB999553456DEFA4769791FE24C934DDB490AF61FE1FC0027FB10AD260A7C33BCFBF212D3B50568844FB022B7DAC9509B308A0040C9B5482D89EB41A655B7C193C01BF96E5CF8A08B4C1C344336FF53CE9F79009FAA3A3921807D9B4C25739C38568584367F5D882E4AFD33697EB22AD03D369E37C0FE3B981047BED55E0BC0999976E4A36C\",\n          \"dk\": \"F0AA12AF6024851A922B789BA87B869072382CF829D8AC455EA25BBD3C3B7BE58C4DA362494369D9482AD1C42952C64DF0A220B6668DD4364AD3E698BBA190DA9C5E67263A7ED27C3E90C8FA38441327CE092C0B91E08ECDE5C4CFAB7A24271DD782ADF8FC6984701625579F26A196E9B7A7A80B8804C564A735B7A9BC7161129F5FDB0FD0E6226A89071FB288884C3635CBA37294253EA57AF9BBAC807135876A338A9A2CAE4CB35938BE4735BBF06CABFC797FD2137282C8CC15F70BEDA8BBE41962119C4D326113A534B4E29AB464924858CAB7B8ECB0B388C6D184B8765C29B5218806944ED636B8793A0D70B3B1B2664F9AAA0F18D5991AC4C6AA3CCF0A633707BBC97D291BC8C96FE6B32490632383886F3723BC6EA606D09C50B28314736A317C15804E1B17C07045A1971869178E9864C21C24902EE70CE3682A31336DB482BAA9AB22686A725716CDC84623CF39365B44AE6F88AF7C1B42C3168C4CEAADD875609AD247068202056C59DAE502C7B421394328D6197EC4FABDDAC3A47D9C614F7A0587A54476A7C370BA84FDEAC91B460C8E266C28AA5209D80885B7279CB685D5D184FEF93525B41CBDB6AEE769BD7C3BA3806338A27399398B617B45C6A94A9A58089E91367366DA0646624BBFCC35CE5B33971243EA0A8D61137CE7AB99636008326003B02B13BF200A93CABC55A2B2A2382EEF3732ADA819E3806AF2E8BC0602CA86A8ADFC41B0DD4A0E95FB9A25296EEA0027550A8FD04921EC30B7D50558E4B7CE82193F39A998012299CCDA061FFA23391519023C1CFD66C341765E62E38871D9A57F80A657D7347E927E55751A9F768A842A187E76C70440517523B7D1C88B1635208D5C7C4C357002E55768B21462D483318B9AECA5BD81A13507D36CCC7063084CB2657183A6C0C78D5B62A7E75C0275C29B304375C77BC8E85B569B968FCCA5E0E2146C951B7590CD0F3CB38E85586D02763610C67885AC56E84048C40FAA129EB05B8F7A67468B6145F949963CF165CA0B56F7E45DE523C1663B311F0B5DA037862559B2739C974B5CB18D16CE2695668D7B0DECA64EC9928523893C91850D91AA4B76B030C2E74C65A0AC5576399ECBB4E6723DE63825D3B9A1CA374AB763A52A541E8382245542896177AD07D9CFB5F2C3EA87AE2164625DEA89DCD06AF22753DFB1A3CEE1A45146409FE655F1C80ACFF107772562784C2C5CE67E9F395EFEA4609BB7308E13031B68C7B3B0C1CE9626E311264127CF8634C9DC35CA53036E9E30BB7B35C286236A2DB2BB9808006F68BAE0125512531320B3661305D0D4B1672D64C424D38536BA98C6417E3EB175D6C1857788C231C02ECABC4B28BA325FD47F3B56C62D0856703C5296E2576D258B98AA273E3418B43C9A52804EF842C543313ADF481F1E974E93210BFFAA36CFF758CC74914BC43110FB1C819678CB03952DB71C0A560D5425287D9590E6F1373902884A38A908082363E0CAB4B646AB016754BBA4E2FCB14E572025C806BD031C7F415377287001811E4F69A110D48449D24085FC43BFAA5465C1AF82A833A7575C578A464442A33C93B0CEC7952ACAA53EA70B01F49200827956B7B21AA697A6859F248C1DC9CB9FD04057EB96006455C062E3C0722346ADB366DA0AB980C782C417B360EB1C1F6762EBF967D713A0D93A28CF9206E95451E91373805047D8A14FFD2041B4468B26C79B697A14EA75A33876BB865096C0289C1AC4B91A4399821349DC66496DF02B15CA433FD97D96F46F72E0B23EB561E809601C053A35F4171A963EC3F542B423BAFC56134B7C0C5927C746E6C8055B3B70B31CAD6A168DD78F63C64FE3044280297C6630562C48A822B570E3FA8A76995BEFE67734274337F14FE00C723AF55D596BBD0B2C04E5AC6D52A0AEDB04B2BF09B1F9736E9456C40D5976B1EC2CA21C28AC761E39C583CDF256AA3262C3264250B1C19B00849FA83BCB6614DA0BB19752AE9BCB7BD16A71017066F73804AEB3C7F9213BE4634401A7AAE7BC56E603516FA2839A791EA89C58052030DB4AA2887737D9BC9AD21ABB94246546892785CBBBFBD45102C24D2F2370BE91923E732892E599D3154502E878CCB990CE8383D2F5ABF021BA424008D07685B8B54C6A6550D9CCBD93066A5A651A9B2710D25B382E72CC57096900E16B7B9868A06909F441104860BF03134316382F64D4CDDB596C57866E0776C04F8C0F67F2286B7242477059EBE022BDA200B6271AF875A829FC368E1574F4F3C56BB39D7FF61131F014FC59870FE45466D968C109339F0CCC67F6A7092372CB977C75998EBECB68A46059D7146A2EEAADDB258FCDD404D6E95E5400C8432535EFD635AEE5235D3B8ABA4153B46C7D234AA94DDB66B229456C41B2FBF93F39AA048C158E50C312FD623DC1E818A494980A34C41568942C30373E430DD5250D0CC27D4AAB2AAFB13CD719ABC7F466B702A983318B8D0C3040DC56B0960291F30EA75986917A17BFB76BC4B4AA8EA396F127A41F73219DD06F27126A5F06519E328B5CEA9D67AC122B092FC3613D99F403DB86CCDD760BF8398C1AD6BE4B5474C34864F2B10D97E265FEDB464E01CB3A426A3BD8B3457267317B7649D62D4847C3AADB7036391CE068758BF1847B96C5F3A69BD36A9468F9CAB27476456B4EB510BB063C46D529758C6680B4588CE4F5CD9D225BF700B27F53C13D49808D561F0413315C01989B1582755CC4CC765391AA68A0617F39B843526081658532C2C0025CF378B31411E867C978109454818916F8052336CFC9788F87E53236DAB54112B876042FE9054D9C07B80F7B43AA900BD2580F386058CFF5624A9A062474990C2C126E22BFB300C452306B859201B3C67B7356CC7DCC36CAD7A7BFC44DA0591D8CC45BC7765E405A2F2373996A72433F332CF77A5C9F3146DE121447264A84339A652960109044B1FC707680AF70E457B41259A1B84C37892C1079B8F08204F218CE85796701B05BEE5961F55584FDD3BBAFF25F53F78D0B48266EFC4991E090EEB28799C002CFE902D1222EDB8A026813295E604921269D0E0A1F4CC70F4E528A092B4E15300EB67808ED20995EF3A80CB999553456DEFA4769791FE24C934DDB490AF61FE1FC0027FB10AD260A7C33BCFBF212D3B50568844FB022B7DAC9509B308A0040C9B5482D89EB41A655B7C193C01BF96E5CF8A08B4C1C344336FF53CE9F79009FAA3A3921807D9B4C25739C38568584367F5D882E4AFD33697EB22AD03D369E37C0FE3B981047BED55E0BC0999976E4A36C9D71C52D37B30F0CADA8753234F7BE062673BAC70613CE6AA0C704C60E481CC1DA9B17E5EB62AD1009253B91A5C9D143F7BAAA3E76BD89AA399B671CCEB7619D\",\n          \"c\": \"2248562375F15D15580AAD60BF6C78957F86C7BD1F78D47B6FA78E68DACBEF2BE4ABB382C409B81A7F746CFA6F90246E0A33540A1C22ECF83298C0E0104E37C29755D5C0025DC5D9655A0A861A534DC58B23522B4F0961F8D40DCE1FAB1A8FED98B7ED1A027C784AA3AFC5B06680C8F64CD281788326CC4CC2F746E0AEE756F71DD7C3F594458E87382B0135DEE1F4897B80086A4667F260FA19C9A9C4BFEFA1FB054504EE11AA7286FADBAA1192C176294EC7E5A9E383A8AF658077348CF74D1707DA1A8F3E400187401D26BF9225B4B36A00466F75276BF2D10A0146C4611951D75B3AFCA5CB4F8ABA70D3999D56273C86413CCD6944AEAC00FE4D5FBD49F00186950126847AAA2F1D87732E4A42B5944BCBA773A83A8F168875B89ECFC6AB3642A7CF2303EB9929825F1B9A4BAD731EB6C2A6848A959EDE0FBF95ADEA3C4E159A30A376DE5DD9BCE1DD4B85500EAF83871A13F3EC1DFE74D86A383C957D6FE3BA1BB81CCCFB3DEFAC3567FCD167F9B202E72677D2F2012BE72CCA62DCA41E5F92519266FBDE6F60A691D78F0366FB0D79BF924C98D565511CE23EC62F3C7FAE1A3C1BC7817CA67CDCE53D1493EA94ECF0176372F9891D81D0964E409C38079D7548D9DBD463D5CF2302E07FD565EA41E8958C563293EBD58620D08CA822D70F87F4F2CF37E963A730DD5A591F2C5F372C9697118AFF2995170308BE96C21A0EF094CAE5372E1FC43172EEA54509C74E5C83A0BD352663BC49F8100C64A65D45D216CFDC69D8333049487261EF03F492D1DE706B00867BEBCF86ED3CF0CAAF4D94D2869E7BC62DBD4C5127FBE626DC26891D67EEE545CE2A3C6CFA5EE273FC017493B08535B907A852E9F4A166C7B8D7261773B73B6FC96822150484A04954A92FC05D0F8AB9716F6653251794F6B2FA63616322DA4CDF97D352566EBD6E23ED822A118A41C0A80FD420408645D077F7F1561FF5B342445C0C8DDDA5E46CF2F253513BEDED0548DE267ABEDF4A9B7809436AAD6F5258FF89581B4D66D9A1B5DCD1A35BE090DD4B67C29944ACFFAC110B65332C469D1266B256BB4B462CB3B6C2B71D8A119D3218D40B00CF449F9807ABF0B54313845FCAB484AFA418ED6E532136EBFD242E0BC499C7A6788FA9BF64CCFA6E5E0B78FA2708D3B9DB1ACD3EF4E3EB1B105EF73ABD0C0AB0D0055279478FDFF8F154CBAB11A9A5FA8F8170D9DD4B1DAD43F9B0DEBA377E674B2CB9424E754B3B203BCF6AF2C71C8A012320DD57CCAA59F2017545CC64A76523C79DC9932AF8999F2C01687CC80FE4CCC45C6C66F6453CAC8BE9545686920FDC8FE4C5D7B1A9C4C556585D70D520ABDC01B650A409FC907A4472229EB981F74E70EDE2DFD97122AA2C1468210932B8A48A9A38A5836F387B5086D1D3BBE516E33D4D989F73105D3CA0B6E126CFA3C11DD270E2D0168C4E08D9E61C951D4E759E6B97313F4A2BA4E5C7CED65D8800083CF016750646A851F533F631FEA14E8CBDE9EEB02FBB5E2621E31DCA51A60EAAB8D9C57BC3\",\n          \"k\": \"9C6EF50DAE26887F7FE5B0173C055E88DC2FE09384890E11777F742B99AD7C6C\",\n          \"m\": \"E0AAD46FDDE0B8E64361C3233263D8A751F5583DBE91AAA6E69E6318FC7A8EE0\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 32,\n          \"deferred\": false,\n          \"ek\": \"B7F522438A05310B12921A8ABE79B4887CB548317B23A21A4213A0F940BC3268464343C89540B83155C980698C657251CE30C75073C3D3C73AD9902ED10C32CB101B0537827F338A94D9615154370165B9D22707693C5E0657BF890A1C5E820899087683D176986162156ABA109CADB1729A0B0A057C8007E4A6AE7E272C860190ED4CBC126082C1992B9F541FF68A419226A666F6A9439CC82B3424C3515ED1821DA501449B84B1D6C169B9F336AFC9109F4B0AA6A069D5C46095B40EC7C886C1B2BA060CC8C0A1B2FBB53FC102701FF33BE42C92FC8287EB311CBE4BB5D523420850B9D962158D14188C4A794E4C0A8A065757EA0C9A65429F114A983B4D55743FBEE267ACA76C14B1492E2CA839AA5B4762868FF2123A30B76D858B0EE4CEB0B9ABD3A2711BA5167B73226C3B96C184A1681A562F07365D686E17F10EB75545F4E0C52CA6BF41627B8D6A19787A3676159EF334A3F4C538E50484F97CBAA334C5B800A34C642D097549D8AAA6E6E16261E038CDDB816CE46BA2B8B821445209E5AB1CF71B008965FBF3A2673B5074943D2102092DF603F2989F32085B96CCB2BA245A7AAAC06C763640D11730E828A0BA3129A43F6F0825A682886D8CCD9E84B93999474B63CC8D272911D72729E79E2A494AF5EA20C1CA8434323164709090382644B11F0F6C806A5044098BB936F5CECB28A81AB516A1857B651B93CBABB6E3D76B7F79AAF6B503762A10ED8499347771F221C2A96A90049C73E6E1132AF53A88D38EA817CEFA29848FCC0AAD125CA1C829F03896CBA635DAA33ACE7C7EBD18CD7AE03A0F9A3659090F5F9976D600B451BB6C1F3281D6C2884522618AE20CD583AB9B32344B6A2C294B0684E6BAB686A39431292F70748AD02AACB57CC3153CA6862D2B15108EE71FD51C2B7F81ABCA9367D4619964EC7C63EC653FD1374EE3A9BF22C4819264C2A750DEC193D5CC836430B18C535D3E573F4574104D471D972361A6955E2E10AF34D95B4A123218667685B1AC6D696053966F3C56297B844657C85402A357649A82E4579B667A36734B665583AB026699D2F6B8F4F1BB25186858543B61260BD3CCBBA264A30135088C0422ADB4508A60485AAB3C63ECC9AF981A1C5913950CB8949B5179E48C7240CD6C858E1144C7EE92A885008209A055C1C3795FA3C49A98082CA077CE4A75B0021108454D3B0C9F01C00CB29166281A24BCA77ABB471A036C9DB1B8946B930BBE8615CBE4AEF6608DEC389BC75C443753C56F5A62FB6C532BD13D89506C4C5985307A53B2403B42265B35B1CFFD6B2CB443A359D75587B2947C7076EB2A2B5BFCA8B1CBA865674C1A2C1A5EB4917BFB9707590434ACADADC92CC7739933323924B17FB516974714A42E38BFB7F20E752AC6464503170C860FD91ABFAA4C6B70CCBB8A2FE3EA91C94B26D5C64CDEE57F936C1AC10704D473AE10479D798BCD6026BACF4C599DC104D70439B0E2A32C91A5D58010ABA9984A34689E360C7B296486A4C681405EC3FB92B44210D5D3CF52534095859E42BC9BCA7B3864D43CB9F46E685C0BEE8853FBD31285DC2EDF129635C70437B612955B7E56A128DEA321298662F4F96A8E1ABE81B54DA4292AFD5FE45E31E16B17919D9EBF8E87B38D48053AA9F6F41C5B55AB86C4E0BEE558\",\n          \"dk\": \"B4581DD3A668DEBB44ADEB1EB7274625494C6E4A8B303528A3C0CE78F58CA988834A4766B1941DA48898D7A8022589AE577B79DFA056A0305A034385EE20543941ACBAACB71A0184F3D308E790C52F1C210E12C726006DF729448B8B4C2B58B14D150461CB69BE1C840463AC5D4A5BCD07AA81246A3DAB0E82D555B624A3D878021E4604B4907BFD906308024D77B15836C64984C422F5EC3784448DE4D5AA98BC9DFAE665F82B046C446D33321734A2AFE72638683CA596822614994F54B856D9E80ABBC312303C7341F99720C080DB72C635D647BA733CBE521230F528D0119377436CE8CB1F7A48B7ED15904C414FB8404A5DD403D3B07FEBDBC129D274472C634DD2A5BF14C304F702C0861DBDA793834ACE9A851BE24B5DA56A4D433AB08E48C1BE6C5BA22067FD59C38F39A25E1230CEA26BB89BA94A0A3A5FD78788E4A150819E951C1C09B6B9D7B7B56DAB4C38A51A80B56DE30790B72B1743FA7782E40320104B2715A22A7794E4C3BB90CB493B8A4304C8084D18C0E822217A9985BD282B26F60F0A986865C39759CCB69109177A826275E663CF81179FA6181D746230DA0F73D991274BB1C3F802FD324CCA01B284BC6E33D9B853D76B95778DAD094E1DBB2DFCB099055C848B540CBA3962A843211DF6B15B5591C5630ECBAA834A9460BA5822A6200DA8E51B5B5292FE221862A36282B306B08AC03454C8A4A3B0C4AC15127C1CCA392E78B9C15BB8B4460A357DC90C2CCABDB7284D50D440D651410AA849648435CB320CDAC8782E31CB868C7F0375C5EAF51B6628C52AC6BEAEB1777C9B53277BB014EC831F3783E6086455CBA77E2391F89C2182DA4D95058120D2451944C0C9D91272BA1C70732EB332B83518B8925781CF90887F43446885C3A1FA6AB1636052731E7D3B126E556AECD9572DA449F97509D9C795E03961A1B82BBECB74A3859A802BC47EA0B014F5606C678FA15323D571AEDFF78575038374D99F13724FB5120890099A3DD531B7ACC7CD286C78C7182AA00140F48595F47BF0B8B6B74535DEE35C1FE904AEF4CCC591AF85E9BDDC978C95DB382BD82278028C79F0741AA85DE2DB4353C1C38299B010F7378CA21F0DE3A337B15135318E5DF69144593947B1A0F9110E5D3B7710D3892EFAB3FEB3BA67D0B977671F81CC44F38355D6D7C3B8677846A08159CA574CD150E5999C9DCABCBB342B5DD1C26B95762B5619EA21350616BFB06C04058BCBD6F9887879574020C4234CA77021CD52074BEDD81AFCFCC5ACA6691B2B4364183EC3C220E20A0060C4A40D4A5592C4BEE8D87E57E6A2CE99CA299631DDE545AA745D5FB9594A67011C5C2AA1090608E4078CCC59CAEC5F40DAAB6991AF1782B9F4F58F8CD25A597393AE1771DAF2246838C1A4E40BFD3A2C1366A563D698E3321B06672E523AA863A3B68E1053FC8A8428F7393F037290BC177337B534F80E93A19021DC3D5C94AA6859022CEC5AC5E8C16A83470AAA4C72952F823408D52981D8BAAB4D529751D02596CA129031C03576BF3B32304913BEAA20759F245D3F658C8A3C1A41D69EC10C6DEB336CC00B25E332253385B1E6F52DE1EBC45A487CAF133EF1104F286AA12FB1A2552AB1B7F522438A05310B12921A8ABE79B4887CB548317B23A21A4213A0F940BC3268464343C89540B83155C980698C657251CE30C75073C3D3C73AD9902ED10C32CB101B0537827F338A94D9615154370165B9D22707693C5E0657BF890A1C5E820899087683D176986162156ABA109CADB1729A0B0A057C8007E4A6AE7E272C860190ED4CBC126082C1992B9F541FF68A419226A666F6A9439CC82B3424C3515ED1821DA501449B84B1D6C169B9F336AFC9109F4B0AA6A069D5C46095B40EC7C886C1B2BA060CC8C0A1B2FBB53FC102701FF33BE42C92FC8287EB311CBE4BB5D523420850B9D962158D14188C4A794E4C0A8A065757EA0C9A65429F114A983B4D55743FBEE267ACA76C14B1492E2CA839AA5B4762868FF2123A30B76D858B0EE4CEB0B9ABD3A2711BA5167B73226C3B96C184A1681A562F07365D686E17F10EB75545F4E0C52CA6BF41627B8D6A19787A3676159EF334A3F4C538E50484F97CBAA334C5B800A34C642D097549D8AAA6E6E16261E038CDDB816CE46BA2B8B821445209E5AB1CF71B008965FBF3A2673B5074943D2102092DF603F2989F32085B96CCB2BA245A7AAAC06C763640D11730E828A0BA3129A43F6F0825A682886D8CCD9E84B93999474B63CC8D272911D72729E79E2A494AF5EA20C1CA8434323164709090382644B11F0F6C806A5044098BB936F5CECB28A81AB516A1857B651B93CBABB6E3D76B7F79AAF6B503762A10ED8499347771F221C2A96A90049C73E6E1132AF53A88D38EA817CEFA29848FCC0AAD125CA1C829F03896CBA635DAA33ACE7C7EBD18CD7AE03A0F9A3659090F5F9976D600B451BB6C1F3281D6C2884522618AE20CD583AB9B32344B6A2C294B0684E6BAB686A39431292F70748AD02AACB57CC3153CA6862D2B15108EE71FD51C2B7F81ABCA9367D4619964EC7C63EC653FD1374EE3A9BF22C4819264C2A750DEC193D5CC836430B18C535D3E573F4574104D471D972361A6955E2E10AF34D95B4A123218667685B1AC6D696053966F3C56297B844657C85402A357649A82E4579B667A36734B665583AB026699D2F6B8F4F1BB25186858543B61260BD3CCBBA264A30135088C0422ADB4508A60485AAB3C63ECC9AF981A1C5913950CB8949B5179E48C7240CD6C858E1144C7EE92A885008209A055C1C3795FA3C49A98082CA077CE4A75B0021108454D3B0C9F01C00CB29166281A24BCA77ABB471A036C9DB1B8946B930BBE8615CBE4AEF6608DEC389BC75C443753C56F5A62FB6C532BD13D89506C4C5985307A53B2403B42265B35B1CFFD6B2CB443A359D75587B2947C7076EB2A2B5BFCA8B1CBA865674C1A2C1A5EB4917BFB9707590434ACADADC92CC7739933323924B17FB516974714A42E38BFB7F20E752AC6464503170C860FD91ABFAA4C6B70CCBB8A2FE3EA91C94B26D5C64CDEE57F936C1AC10704D473AE10479D798BCD6026BACF4C599DC104D70439B0E2A32C91A5D58010ABA9984A34689E360C7B296486A4C681405EC3FB92B44210D5D3CF52534095859E42BC9BCA7B3864D43CB9F46E685C0BEE8853FBD31285DC2EDF129635C70437B612955B7E56A128DEA321298662F4F96A8E1ABE81B54DA4292AFD5FE45E31E16B17919D9EBF8E87B38D48053AA9F6F41C5B55AB86C4E0BEE558A7F40DCE21FC27EB6E7596A711E8B29FA33B3AEAFCA1F90450EFB0FA358688A591271D83D0E2BF964B9C7D2CA6227184BBE74EC134043A44DBBF8EF3B18EC43C\",\n          \"c\": \"72CFAA01CB4D24B32D0A12BD199C20BD3CFDB6F063CE9608A0DEDF0FABFFE8EDAFF2536244B7942B6FBB62297D85456519E9CBE3E587ACCF54FC28062765E0C6250A204007F82ECF4AB4CE33D78CD0B4B6E502B44B0259A4634FDCE0116835E5449313C089E603EAD7C49C08DEA8DECC81D8E2528B5AC9C98A5EE6BD58E3B60E98922614ADFA9390F9A2B6B66272024B20263AF2126C3477447C04F0C8A42FB8399ACBD6DD669AF1C805A204C2173503ACFD770ACE4470B7D683F751906B7B3E5E8B1EB6241EFAC9ACCC3AB204F4AF77AE4C1033F3B177C322B72AD1C52A10B35782631B74EB883A5CEABDEA1961F327AA53EB14A1DFB2B58A4E7B37D14B5B565CA21340F181BDE4EB3C6445AE772B07ADEE4237262DE99245ADEBDCCFE7E68F96AE76ED8ADB62A6FCA116397011F3A77074D568F38BA6A131EAA7727FC9BC8A2B00016A37ABA76BA1CC11989771E3FC7AF635B46AB69487347B6C8684885436AC1E8CFFB1B65054AC01268005C71C70F36899F543F876C0B9742E29FC4086564A074EE95AC5CB395D6CE1B1E384920AFE580C5526C713D963DAC69D20C4A96932303B632ADCB361D2D3AD37CA4F7875A5BE6AE62C333751283A430E78842CEF8092F85B54B064A558DC1D25A18BBF3C0B496FFF38B214F5D9A611019BC4EE49C3C1ED06DD705D720D58A97AB6FEF5518969F2A8605BB10B64E6FA31B8E096BAC3573043854921E4210DFFF279578D2DAFD40738F0714EDDF16C2868809223FC8BD6EBCBB3B331B1E8ADAAA7597E53E31D9E7B478A9F6E7DDA731AE9571F698A1C977C4F3401C9A05665E0B8C080B34964C15E13ADEF0348AB9CA3B64F18BEE6117D7DACAD1F08FD9B8AA8C5F47881338BAEE1B94FC40ABA11B0FB914154583BDBCCDA62D3AE898BD60B9C643D67514534FCE277087CCB66A25D345290AEE7C1B07D57D53896574CA762AE8D17A61D796F4A8270022DB314E27CE7906E4119C003385D88BE165BB80493FEE768001BB42676D2B71D58FA19199E714A0864546F2166F46F4787845525CB59B2F6F8C3E0943421A70EAB2705420BA3A62ED9AB8288DF8CA09A5ABFA64FDCD0049C61FC7B226249E0E116FA5CC0D9C2EB3B7391A40BDC0921F4D2936D368D8263791156741EE85F2C0267E858FC01E89B6149EAA18B0F8C8F827CAD5F8AC68F24FDE5E185B3223333E3A0B8245EF30B8E5E5B3E04874ED3F75A5CD25E1AB1130F0DD6D5DECF88E332F96B4F9A4C58F14ED57250B47B1CF3AD093E2B9C54922B1214000A98049003D1266ECD0F68237285A709E24704ED1CD37F3C64E15CA637D431AF5CA060AEBF5E0CFBFE464510669317944FE07F7EA48618478300725961E04EECDB73B411206EF5F3DF2809573D7FC42458D262EFB242D19F9D9AD9A8F2C05AFD31AE350E83CEDA11AABDAE85E2E32B1A226BBDBFD2D5C2B7B4DDA94012D53AA7289AE675C33E9E8F8F6F06537E240A97998DADCC39C836FCB8AC24D794AD291D42127E8B513CE0346E145B488FA220BA149A\",\n          \"k\": \"05BD5B91C2F634E5B8BC59697D180CF1B36A244C6EDFEFE7458308B5854C77FB\",\n          \"m\": \"90347D478D5D964D66A54BE930FD9F7FD3C2AE1492DAC35A6CBDD02616BCE14A\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 33,\n          \"deferred\": false,\n          \"ek\": \"AF98338A682D431CA0E17775EB170E3742ABEA300D6A46C567C364DE8939831695C59BB7686729C9001E25A85FE926CC6E584E2BC86D3B25BC9D6ABB97EB7F15AC23656B3185CBAFE0C39FA0789DF0678FBF5A43E6E0C53EC38076572D9D84B1ABE742E2F6C0C8CB08CDCACD23B71D57D06708F32D50870D9D636DF1DC01A8378A211A134BB255DDDC0B62C75812AB1677C50DF56B1FF62024FD722D3E732F56A2C6EA10CC31F280ABC8347788CB291AC5A1820525B9A33A7089DD689962A046B652AD182639278279EB884163E2B115A29A3899CB0EDB4514A0836BFB8A51D834C1939B8DC108B6138FB88B9199DCA5F7F64AFF36B9296613F891265778C7963C3E702B81C54834469AC8B59920BEE7878F01052B77B0F54B6DC61AA3DE695D20786F7D309C3B16A8D2C90A921CC317E91C015AC80DB106D4810EBBF2C5A1530506AB1E28B7A32BF67A6981185F98B44CABCF6706B134B5537DC697D16AC003808119B0BF94C84BF569422C8BCFF834237B902D83BC4C16F5CDFBBA5CDAF0A69FC87DDC885DD3AAA852342C1EB8179756087C678EAE2878A777655FC19719593FF600789ACC6322018791874D33D9284EA512AF1231D4CC87F1BA6F6293A653036E590A892F101CC518110C984AC55C2931C976828283F0266609A45A8C7CBF8C23CA0279B133A38F9DC797D5E58011096D45441F11DC59AB5B66846A87059314FDC64E8CF50040B57432A18F46078AF5A6132B006856604CE009128FC5445BCB6891E91A8677060F39A2CAAA183FB8A0F6751051AB85474A539A4183A4486471418FDBEBCF40055AC46075C3B2190248A8202431DB828B82E1320DBA47A23A94DE5CC378B48FF4B633E2D666B561827F8013429956D3F5947FBB848B3A511452870D00BE30610D0ACC418F8536686ABF66851707D89FCBF65959BA3062F6A16E268488D04BE4E370C4247947FA823B27133ADA76DB58CF3ED1109CE433CA0042CDB809AA1B5D5E9C4258C6067B0004A905786473A0CA4B3E9D90443891A148407A8D89121379A1F37C70770586365A74FFE5C6FF170791681E5CE10BB88BC0BCCA82CFC81424AA5BDA072D1BB5BC62F6687A05A949C2B04005284FBA8ED0646A394658A5868304C42C05605E9292801630129C76A1FC083DC3696DC4E904BB1916BA2287DC37232AE2C962108F245C3A0B702F53271F6BB00363D88690A7BF37B345ACEB5426F34BCD7670ACF15955401A20F938A4407E00D8008C147DA00C080A226F71F15DD8DA20D0C6AFEE431B6B2733513BC877F5545753305E8C03FAEA935C183BFC1C40561B59FE80956CE2C5BD3C28F2248249E2A0527891B3B64BF0EB89938A42008C32D668054E508871012FE8653473FB70CAFA0A8F1A509948A2A967CECFC7B5DAE34E166086433C5977208C45D97B24238E875A790C2779974B4721F573C03C4D20777F1C4589227765E8E7AFD0D59EDDDC722CC6230356B477C490234CAF858893F0E446727CC88E1411242344DAC6AE6C2CC1D2B1AF2B8C32BCFB253013411F18693ACAB9A7A86CA5590964D39A8A50768718BD948566A2822206226020165965F7B68871AAFD3474FA306A2DC31A98C60FD2E5AAA8A0B72BDD2F70D6D5DEDE7D679758D8A325B6CF11E7922902ACD92A3A8CB43863CE98\",\n          \"dk\": \"D261A4F71B7248868BE7C32F8AC688174747FA519CC3A4A5D757C3D1E7641893849AE969D8F8A6B553A1CDBA41D46B74ED9B8C8C19409D3AC3664226BC74AD96A74221C086BD3A0BACD66438239E0A8B7EADDC72ED7A453C6B7570B4C763D84883D788D8F8A080972A0635AA563842526327AACC510688B648231B4AD019B3CB9ED6F161673A6C13A6CC850A6C5692B5A3150D2A4805C0875028217DE1058132E68A97501C97AB81DC229F5804079DA5A7F353A1B999BBFE63BD9A4A4B6BC212BCC5B4AFA795C6D25541A522E3BC4FF86841AC12034D9553504217E7DB127504682046604401381FBBC08DEA12095AB71C882FE01BB95B7723F3CB196E1B8917639FC74007845A1FEDFB890F096A13E281D66B9CFAA22A93C8CEC9C9393C2BC81395B37C001D56C659D3C541AA97133D09CDB4758F148966EDD675999248CB786BBCF2A15138165016114F5ABB9BE105D8BB8A3DE8835C85A28B3C49E0C928B89512F4E380F371882C745056899B2FE465DA9130D478C46B2B4075B487AFB96DE727B88A83962B8B99D42C89CEDB5C4369BE78F7737F721AF0645263709E2DE0CE96C0CEEBA76DAAEA7757077905DA5F75D932C965C844C91F09381147C021549783A47609111965C619AA7540881F281633607F7B9983E0E2B7AD011E41CB5BBC186B4BE084CFA136B5E01B36328416C997D87376EFCC277C5AABE703C0E70C741D1809A0A36DADE7BC5111B1F046AD63449BD1C149B1C47FC18C091CF430C377A8AB0A92C9249FBBD106E7A060A4B252E1ACA050941A1AE333F2C3651F27B2CBA7C5CDDC86F0519B9F485ED563CA9E5773F72742877109006D8E9A3B49712612B5B94115024632AA0FE39A8F527891E03C97FF2C57EA532DA594C73C3B03151376702C21B6191826E7BC42DCC5D9CA2295165A85B4981D311AD1E84DB1E0249933AC134796D84166E335C478A46D1AD893325256620B2E60FC9D71832685D406C2AA13D3A614A10410A27876B1F6CEA13A88CF401242665B9051C137802A63991D82733801FA5611131D2CC745481253FBA4788E520CA165AF49E6AB6BD59B10C95CE5911B73745383D91A9E99620FB9862DF06FC248AE44C43D042A7ECF21B6E70A32660377651A24A486AB48598D1D55AB858ACDC7007D98B66D74B368BB03AA4992CA32D2483BC67050F2A302D0133F31071F3CC286A09CEB2139320B496A607B2B36B07A245E90F3850CAA8F9823AF2CA6B537439C861737348765A988256C597370954B02F44244AB15A9F78BBE9C7908701E4571B384A3147D8A4ABB0256D3E45DB5057AB156A4B6405085D31D8A602ACF6280C0E9CFDFF756A0EC3D43C65FB70867B09CBB596206E0E3C14B62188DE815544247A10122788884C3103BE557877698708236BC2363BE9C0424A0930CA63922174C3AA2585B53784D2362B25EAB9AC55AA866C8CA59A7859006937D206D6FD1AC4F80BC1C433F4A67BCAAC05221385A70042AA011104C56037FE0875E2C86BDF384772600EF391E8DF30AE370CC792569D7A5213A6C7866DB65A196652F0C699E4C57ADB90AF0498D33CBB1C6969659C22C3A2596FBAB175C992530776CB8A6463D37CD70E89E075506AF98338A682D431CA0E17775EB170E3742ABEA300D6A46C567C364DE8939831695C59BB7686729C9001E25A85FE926CC6E584E2BC86D3B25BC9D6ABB97EB7F15AC23656B3185CBAFE0C39FA0789DF0678FBF5A43E6E0C53EC38076572D9D84B1ABE742E2F6C0C8CB08CDCACD23B71D57D06708F32D50870D9D636DF1DC01A8378A211A134BB255DDDC0B62C75812AB1677C50DF56B1FF62024FD722D3E732F56A2C6EA10CC31F280ABC8347788CB291AC5A1820525B9A33A7089DD689962A046B652AD182639278279EB884163E2B115A29A3899CB0EDB4514A0836BFB8A51D834C1939B8DC108B6138FB88B9199DCA5F7F64AFF36B9296613F891265778C7963C3E702B81C54834469AC8B59920BEE7878F01052B77B0F54B6DC61AA3DE695D20786F7D309C3B16A8D2C90A921CC317E91C015AC80DB106D4810EBBF2C5A1530506AB1E28B7A32BF67A6981185F98B44CABCF6706B134B5537DC697D16AC003808119B0BF94C84BF569422C8BCFF834237B902D83BC4C16F5CDFBBA5CDAF0A69FC87DDC885DD3AAA852342C1EB8179756087C678EAE2878A777655FC19719593FF600789ACC6322018791874D33D9284EA512AF1231D4CC87F1BA6F6293A653036E590A892F101CC518110C984AC55C2931C976828283F0266609A45A8C7CBF8C23CA0279B133A38F9DC797D5E58011096D45441F11DC59AB5B66846A87059314FDC64E8CF50040B57432A18F46078AF5A6132B006856604CE009128FC5445BCB6891E91A8677060F39A2CAAA183FB8A0F6751051AB85474A539A4183A4486471418FDBEBCF40055AC46075C3B2190248A8202431DB828B82E1320DBA47A23A94DE5CC378B48FF4B633E2D666B561827F8013429956D3F5947FBB848B3A511452870D00BE30610D0ACC418F8536686ABF66851707D89FCBF65959BA3062F6A16E268488D04BE4E370C4247947FA823B27133ADA76DB58CF3ED1109CE433CA0042CDB809AA1B5D5E9C4258C6067B0004A905786473A0CA4B3E9D90443891A148407A8D89121379A1F37C70770586365A74FFE5C6FF170791681E5CE10BB88BC0BCCA82CFC81424AA5BDA072D1BB5BC62F6687A05A949C2B04005284FBA8ED0646A394658A5868304C42C05605E9292801630129C76A1FC083DC3696DC4E904BB1916BA2287DC37232AE2C962108F245C3A0B702F53271F6BB00363D88690A7BF37B345ACEB5426F34BCD7670ACF15955401A20F938A4407E00D8008C147DA00C080A226F71F15DD8DA20D0C6AFEE431B6B2733513BC877F5545753305E8C03FAEA935C183BFC1C40561B59FE80956CE2C5BD3C28F2248249E2A0527891B3B64BF0EB89938A42008C32D668054E508871012FE8653473FB70CAFA0A8F1A509948A2A967CECFC7B5DAE34E166086433C5977208C45D97B24238E875A790C2779974B4721F573C03C4D20777F1C4589227765E8E7AFD0D59EDDDC722CC6230356B477C490234CAF858893F0E446727CC88E1411242344DAC6AE6C2CC1D2B1AF2B8C32BCFB253013411F18693ACAB9A7A86CA5590964D39A8A50768718BD948566A2822206226020165965F7B68871AAFD3474FA306A2DC31A98C60FD2E5AAA8A0B72BDD2F70D6D5DEDE7D679758D8A325B6CF11E7922902ACD92A3A8CB43863CE98D74B9CCDA4F1119680B65475539C5D6AD9CC013C32F7DC34DD644E17FD8FCE117743372B043D1C0784B22FE9852E14D43E7A05A19D7FBDEF102AB9743822A129\",\n          \"c\": \"36A6244DF4F7569DCCA35691306D2E1CE906993034093AA928CA368540FD0D1787051D491033CFFCC6805520137FD271585D651FD67D6C4B9D9BDE958892705DF8A8D2C55C426B6ADEA1F187732579DA8F922D881994BE04A1CFF591F669019FC4AF9411FA61BD3DD88073F8E119C67BAEFCD221DE8230E8B6D7D4D739AD28A1F6B2C9FCE301DB55CAC39778FCDDD389A44DADCF65513AA05853A88D472A7E46319CAB47E3AF09913623360E1ACC954BA17AACB84486F81B0FB34AA3966F6796293EB7F4233052BAD3BD2EC9B1CE1079D8A5C0F9B795C9113EA865F62F1103260ABF902A5D6497BA7F74D1F0E5B2CF564D5B3ED6E6D6AD186F07B62FAB78577CCBCFE6A2DA803E9199785EADA1B6675B6846535FA157166713D37A55C8A99AF87A4038B2225C3E55DD21557238C96226D618979A57204F9EA5447A57106EE6E6E8211C30CAFCCB709B3CFB42B3F4A538F29671578B66F406FC5A6AB274219A58629DE5F84C55AB1D8C39077A42342A1A220C65D5AA8BBB0097E3AF13A03166823474F0798D4D0B36C8EE7C2F71C665D49AAF9CFAB1E72E69783347AA4416055DB68DD30B31C604799D970430209A4F73E70ED3F8FF056A8C9E9F8064766A3CC31304DF31AF27BC6A5AA9BB6143483A89DA5F376EFFD539CF90BF120E8D8E2F5753F99D9415D27E79FC888907325E45316CF5975D717690DE4D587B4539CB36008C127205650865376B78FC07CF7F5B2EC247553A116E386307570913C423F6713875D532EBD576B440E47732FFEEFA7EFA76F29ED763C088183C08471AEB47CDE561D01FC41D063C41BE58BBBAE2EF0C1DBEA24C20FB789697836E0BD63FE39F914278C75BF6CB1C8B1A73F67913D56DFF3451834CCF02B57411DDAB251AEE7D939DEDA1F3CA918B76F6757241E97F068F8D4024731258CFCFBB5CAB90DB10337D2FE85BA4139A329B1BC44DFE1D26D36CDD7E43A62DDDB749098AA2F403659EE15356FDF8C23FECCD02482851310BB702031E126D1D9AF109B6F2B452F65B685C85A67E97D4658FC7D551286AEA960FF348D47F81583B94032D1F0AB71997077F8D119AFD039B3D4DAF678A9EE0F23422B90F6898E39E8F42C6A3B2D7BCA38364EA6BDD2277A8FE32AECDDD4C2822F78B3E0B7DBB0250126D4A8F813EF6B6471EECB3B48753439D099E2B43FD315A259A5757D0DFFF298ACCB6EAB59C964AE7FDD3FA35630FC7E72CF9D538F226916BC11250B36C24DDE8404A5F4FA684321713E57AC84CFB0F1DFB813102E77ABA2D5D77F789CCDC760F75DBF26827FD5833BC42C9EB86AB9B1C2E4DE4E4D5D257170D20D8673FB0E4795FDB5DAC063EA801AC14E67C8E98F3DBEF3F3A0059CBCEF321AB5A288A4BC9B093F0FB958CFD8694E6DD9BD3F37344A9454AC4F86D1CC5959CB6714DAF0116338479B99F251D5C2C8965F3CF1B4966348E1102972AF4FA858B09F9173A72A1ECC911ECA8B578BAEE37F7413D821655F3A207B6DBFBB7FD7974EAC6C9923DE8F65F3A4F8321D16B34\",\n          \"k\": \"4EF33F2E08DB26B11979F95FF6C624B4168CE9055FD31390EDFAAD5E2DABA6A8\",\n          \"m\": \"119BC36B5F856C0A2F136B3EE42041B817125A600E829FF6B4B402131A26ABF1\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 34,\n          \"deferred\": false,\n          \"ek\": \"463B553102898CA297E0C205F3C9582273ACFBDB13BEC53341BBB6724C774C741ABB16B2F992C878114FAF67C248603D8AD5842E7230E59B7537119B204AA8B2E0830CA3511886AC5280242A9A817D9A8C7EC8B95DE23254174D1F507129BAA49CAC9C6800CD1039BA2F6625584B8F3AB27657E6B7BA2A58080591D572C7E0B5ABCBA3BABF0525736ACA427A1F13245426C08DCAC5752891169B1A8373B4BEA8A49DAC3B8163E78E74195E8AB54007534133C02AD1930DD3CB066E7114DBF4CC9545C3616B1895469A1E08A57273B235874AEEB32EA3433DBE8732B871B4D0E3192ADC30AC002E4B82798E0C9AF004973F5749456605B897C5AA9938C38343B3690160615B3406AFC6FA20D04AA0214B9C224AC7E64A6E85F38AAC01632A501A6CA1A4D3E0ABAE384AA2A50911E8BFE945792C9283F9A898A1CB2E2934550E3CC52E9BB4C2EC125F64C28B0B12E0805A031824F423B7CFD9A11086523F1305E4C130C17161AFF366472875645BC8A1B14DBB104494C746F84B4599C07FF4E74A3A1408CC983CC2C6CC34BA04333659C1F960E06B73257C5EAD5B3406D70C3DCA05B9EB1E0EAA89EFF046B58471FB7268BDB6402174BF03F92D4D22C7F204A35AC6551455603D8382CA3AC726C1BA3ECB5D4244BC62F10989B4B9851C4346DBCDB959516CC260CE2599EB1232CFD65FDF39AE7EB0763AE47A359367A7720D02B07A5A634DBDF74F4AD589EE2691BB4862D06B6836F70BA8C582F3A7996BD7BECB998CB1AB1564C812AD04920B9121139817E8F06172B217481C379E2C368C0A12194195A03177A822C76B0C7C79AB700FD5C24EFB24BF29139F4930E6515E7A1880D971644ACA12E4278AD22CB96370AE09614D96CC0328265F27E2B2D60465C8A370736632257B191B72185DA1A2E6FAA6759580C6E51E6290B76944C2A8743610424DCAB89CC9B79AFEC512DF9CC5B4807B0C93A889F872D1D8950CB4B371335718035709F36710647157141E6F596FF677B132352D3C35389903375999B2A0D460CF16A581C3CE5A057DF42051A7BA167D6647BD683290ABBEB3E67277C055C5E70674555B98D365442BB8D2859FE47C1309178537E78DA79B7016890C94864328CACF327746D0E32D65D9C7C0574FA5BA9E1FBBA72D990C62E1AD74759BAD3B7F9759B334B10EC8A1AC3D8453783A621C898C99945195928445D21B6F40215724919C3959D77731C2ECB171489AF6F545C7055D55778CADD250DF49913A7769D14774D87940D9C0C53969BB2AA151E063CF78CC541EF643AD7C916B961983983F886C7D7B6A016391C6B9C64E95A03FD1F65B6C924B2353678D3079676C25E236745955CE5A9500BDE96A1287B6A795B3382B438E64C588878CE447530D174B53641416E328E428BBC347AAD32342C0F17D1EA15FBE147E3D0009C1CA0C0E5A437F1708A602026F0252BBEA8B7C70445A456678E0777994C021F61A8C689DCDA40BB196512C8C53FC3B0EC26B3E2474419AD79EBFA58087AC8987BC15C802028ADBCFB37722B0999F6EF71629AC2966FAB95A5A2D3F74300FE668F9D912F8815561C53E1BD24455E58FF3F7BFBC2207D7966B1414CD0D695C5BABA93618A89F32CF29B33FE97EE961F5DF14FDCCD0E81878F6C76D5651730F6456DB0938BF\",\n          \"dk\": \"ACE60FD2C1783948CE064702818B3CD0DC8151C34BFE02B16DBA87042A58A26286E6E20D990C478CCA9A56F729F5F34AD28A85FAA71F8B89C6D188539685BD873C76902229D0C982039A5B95DA6A91C7883E1AA82C81834169C74B45A021C04A4FE665EFB41CD07B660D9478D6F831A9BC70ABD757D7BA3790E559625186C197CE99E6979640A60DA017FCC83351D955F03A32B9234CA6CB226331C5F5A342506422E3C099FF702444659BCE0BAEBA2194C8089284F8A218C903B368C563B6A05557402D30BC73EB91B6B2A5626664E34C44B4A04BCA0187B7C733FDF4BF02A84A513753261646B01BC4F333BA66EB4D710B6884A26D2AFB3032C5474E741940F556D9F963B97BAD3715B6002C941833C8216B220600B57A2544AD497D5E5029E0A0148146755F0A72E1205AB7FCA58E31256C44178F8309F6A81FE9FB083F019D1C4407BAC85EF6264B72531C3BD5A5F270A68884841E190667759CA884478F3A641F798057DA702B998ED17C18D099B71986045A7B5163D72C40225B0BEB48943B444477B56EBABF34877501F94477CC6582B9025395B387E23D41321D225065DDB933E2CB0C1C35A927619BE10217DEBA2A812A15AE83AE46C9B22F03C65C73B108E571302CCC7922AB500C3525F34B572946309C7FAEC5930122772377BCB1B9990C3B1DBB19051FC4805BE809B63A67B5D9096F7693E78762E4640A2C55C8A4957C8B1130A3C464F2B67AAED0191145944887CAAE29C4595C7FE9B4630706968A6B8AB79AAB481A31C1C02E2C46A43CD69D7F5CA1CDA7AFA185AECE69AFAC3156D7F1470C26B812C7453DC724B94A809B94A6880C02DE987247EB777D451645689969F1450906B82949273A216D60C757E470A9C6C6415A8051B33049B6E82885A78A12FA4375A323A686CADF08C5AE52699F6383C1F6B27C558F915B0D2261927B3C24BBD19097A19A6C905544FB664BD766074B31C870140B8840B1E73F63A68D6A876C40EB80EA96563A469423206929DB169BC80B749A1AB26C9EA4AC44B2AC611C51910B9300636570D79C362FF2571404622E6510AB1B9E6B2530E9E420D423579B5802BE18913F8CC06F3789E74C69DF8C80FF451EE84603868523581438CFB9959A83BA0C9A6C5A629E97C867955B84BAEC4FA36597BC2999AF7638D10BA3F6B68C3DBA1F5828B234CA83B2F14C9B7281FDE8B7A318A00D1A3E87DB04A94091F1D0A326858AB8BB54B1F27BDA8BA3E7C75C48B03386A06903487CA830517369120B8103E86809286C92E6033D37F5C6918139B8CBBFEFA9871B5934C16B6441635E7C478C010A554BD4CBBE119C4B77062BE49F0FC301D4D6394AF0C5518325C9C327E8E001FFB78D157940468AAC6B8BAA38A20A11F439DE402DEE5856A5866EA6A9B2FF549AB040B3F4B43772F89F66138B543A644E6B337850A09D7A747827473406A82973479C1700CF72B39C813F8EB51B4DD04B13FB9D945C42BCE147E56074F30485F800C63510B634C08BEA128AFCA6B4EF543724CB6CABC6B160DB957490816BC8C33122C2923B78FD87BBE1453C7DA562F185289C56017CA572DAC0AA2865499FC0322DA35EDD7919FD9A20D2D9C1D1586C8E5205463B553102898CA297E0C205F3C9582273ACFBDB13BEC53341BBB6724C774C741ABB16B2F992C878114FAF67C248603D8AD5842E7230E59B7537119B204AA8B2E0830CA3511886AC5280242A9A817D9A8C7EC8B95DE23254174D1F507129BAA49CAC9C6800CD1039BA2F6625584B8F3AB27657E6B7BA2A58080591D572C7E0B5ABCBA3BABF0525736ACA427A1F13245426C08DCAC5752891169B1A8373B4BEA8A49DAC3B8163E78E74195E8AB54007534133C02AD1930DD3CB066E7114DBF4CC9545C3616B1895469A1E08A57273B235874AEEB32EA3433DBE8732B871B4D0E3192ADC30AC002E4B82798E0C9AF004973F5749456605B897C5AA9938C38343B3690160615B3406AFC6FA20D04AA0214B9C224AC7E64A6E85F38AAC01632A501A6CA1A4D3E0ABAE384AA2A50911E8BFE945792C9283F9A898A1CB2E2934550E3CC52E9BB4C2EC125F64C28B0B12E0805A031824F423B7CFD9A11086523F1305E4C130C17161AFF366472875645BC8A1B14DBB104494C746F84B4599C07FF4E74A3A1408CC983CC2C6CC34BA04333659C1F960E06B73257C5EAD5B3406D70C3DCA05B9EB1E0EAA89EFF046B58471FB7268BDB6402174BF03F92D4D22C7F204A35AC6551455603D8382CA3AC726C1BA3ECB5D4244BC62F10989B4B9851C4346DBCDB959516CC260CE2599EB1232CFD65FDF39AE7EB0763AE47A359367A7720D02B07A5A634DBDF74F4AD589EE2691BB4862D06B6836F70BA8C582F3A7996BD7BECB998CB1AB1564C812AD04920B9121139817E8F06172B217481C379E2C368C0A12194195A03177A822C76B0C7C79AB700FD5C24EFB24BF29139F4930E6515E7A1880D971644ACA12E4278AD22CB96370AE09614D96CC0328265F27E2B2D60465C8A370736632257B191B72185DA1A2E6FAA6759580C6E51E6290B76944C2A8743610424DCAB89CC9B79AFEC512DF9CC5B4807B0C93A889F872D1D8950CB4B371335718035709F36710647157141E6F596FF677B132352D3C35389903375999B2A0D460CF16A581C3CE5A057DF42051A7BA167D6647BD683290ABBEB3E67277C055C5E70674555B98D365442BB8D2859FE47C1309178537E78DA79B7016890C94864328CACF327746D0E32D65D9C7C0574FA5BA9E1FBBA72D990C62E1AD74759BAD3B7F9759B334B10EC8A1AC3D8453783A621C898C99945195928445D21B6F40215724919C3959D77731C2ECB171489AF6F545C7055D55778CADD250DF49913A7769D14774D87940D9C0C53969BB2AA151E063CF78CC541EF643AD7C916B961983983F886C7D7B6A016391C6B9C64E95A03FD1F65B6C924B2353678D3079676C25E236745955CE5A9500BDE96A1287B6A795B3382B438E64C588878CE447530D174B53641416E328E428BBC347AAD32342C0F17D1EA15FBE147E3D0009C1CA0C0E5A437F1708A602026F0252BBEA8B7C70445A456678E0777994C021F61A8C689DCDA40BB196512C8C53FC3B0EC26B3E2474419AD79EBFA58087AC8987BC15C802028ADBCFB37722B0999F6EF71629AC2966FAB95A5A2D3F74300FE668F9D912F8815561C53E1BD24455E58FF3F7BFBC2207D7966B1414CD0D695C5BABA93618A89F32CF29B33FE97EE961F5DF14FDCCD0E81878F6C76D5651730F6456DB0938BF885E38C95A03788929E70D0A17C2D4E23764EF31D826BD4E78F114E7D8F056B2C07BD30B423B29EC3F26A36A916A247C45D1C67392F267A9C3CF0AE0B2F75A56\",\n          \"c\": \"434A4E54F4450AF0673AE77E391B9C25BE63AF58D3F65507AFCC28E6C17B8238585085E39D89280C7F8BF73948AD6A543F9A5A73D3EFDC039F654EDFAD6B4216D60E121EE433C9A347DF67792F8C99169066AD7E5B19A3F4236C240C506887E2F98EC51C565940249D992006D04CA1391A433A43EE2C8EDA8C54D4B731A1570FCDC0AEFF0837D42827D2570AF1A9FA900445F51FCC482F0AC088DE5D4DA40015535EFB350A8797F62DB7DB8DAA0B96ECF6DA3024EF80520533C6E394A197BFB22E91A38E7F6A7CD7FCB4A7A78C9510144D42E94C3AC8B2F0F6914C11078720D3B9B848E6BC211D56447D2FC7F20F59F4C5A72716176CF2274CD82DF2FB2BB0E634AA0EFE9D4EED10C790B14754A54AC295BBCF4DF1A129987EAEDB0DA0FE3931888C9F0EAFD5399BD91A6036B7169C788FAF63FF8ED16DD3A92E8040FFFCB6487DC15B734297FCA279155F365C9AFFEE13EB303B05C52F6365D8F64D61EBCFCE6076458D80E97D325C12B1B9FAF46D8B078DB977963DE5DD75A353C1E7FBD9A1EFA97A6F10AA77B65FF0FD699D2118D6A9ED13499BA2FD10AE9513F1417F6BA75448D7490CA487241CDD2D3300677695095EA755495F6327E92257FA9E29A39793F4E9B8CF6E43E0E1D6EB4620B523E79917C0C328C3A0C55ED76B16191AC58325C32FF4E6E4FA570F2F8174C2F21A9A6B8B1E82E2F7E0388CF7BA95AF70C7F0884C4D2876C3C6E479510520D62DA9D49F435026A401CA1E5E6D5F0DF062CD34C8478336BEB3073D38EA7F37ECC969545DD503F4C10FBDEB112C947EA8CA38A180BE6E3CE1095BCE75EAC5BAB6C436784E83DC03A9CAF1D90DED7784D97F141E642334FD2B51D957224278948CADE3885003777031CCAF8BF81D40C9EBD7127CF4AA9FD983A2F8209EB7C8F27360123EFAFA1A5ECC2B5C07CC4F1F6F5268BF0655C9AF176E0D6913BB5A2886D74DC727C8B72AF1721A4F75B0C54A76365B87367EBB38B7B294B8F75019A2C96E1C5D62E782E903B7A772589410A84857F565939018612EE18E81D535C8435273E5ED6ED28FF4CE7734D643501EC0330F3C46A11F8E17A2E8041D4E0F0F01B7B865ADCF440B7313ECC1C1D5E956AB82783D89A635BDBF7CCE80EF21D3151A80A83FA01DA706394592140E882B8F86A36D58BBFFCC897F5B6A26B1AEBDA91B56EE668EACE53E6DE716939B8FEDDF0133849DAFD340B3B658778CD9F6E7BFA550463903E46C9BE92EA6B6AA6327BD197EEAB7F8DA8FD7CB0E1C4A4FF5D4EBA425D647353549C853621D4B617CF2301CD6D2F2BBEBEF733D7C403F3EBA5F3FA504EBA0069C4CBD4E017F2A0F15750D89F1A0D129806C6466350F87F3939593DF02F211A2096EAB5C917320FE98173D66DCC57EC74500FDEA7A56D011C32D60007D2F416FD1971AB61F2BCF075831F094BA7F75F4DF07525D747E4A313F9388465AA0A9448F75DE2524DD4012A4161982B927F743B5813F35120EA5BCA90551DD3ABEADCC9AA7238B115DA518464E51526F213D4555\",\n          \"k\": \"A2F646AC5A87355FBFE9A37E58F405420221E523844C9D00AB089EFA0FABF280\",\n          \"m\": \"697CC7445AE2C9ECCA2569B7871F0BBB364E63E4B782F734FAFED4FE33E4AF14\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 35,\n          \"deferred\": false,\n          \"ek\": \"0387B2D669850AD9379CA70B3EF1BBFD487214F08616824787C4506C83740BE76CBE452CF99369CA80674D3B22D3E04FF95093BD00369BDC5C8126133C5A15B8A99A912132B19C275489B7914858E806CF4CB10D3FF33E018CA8776A88FA30C8782179D289132E0CA4A1BB19463571BE1977109C2BBE4C9433D6986AF12E7079C4B5783390C95C8468545F48A5D669CC0567CDBAC54420DAB1DA63456C692678105E08A822D0F99584D7A8BC76CD9038C2768663FA11A8D8E7B86BA52DD21B323EC22AC2E7B921216282233039546803D509FFB12BA2FB7D3FD38F4B06B848233BC90C2ED68AC41FF24C6425C6C9321A7B697BA5588B434B1B5A49CBB9FC479BE68257498A05865A7A0217B8319E7BB8B55C15967319CC510147BE9BA5511A967CAA2102C6A41298C0F7620E626693E976318E6A7635536B2CAC9DDDFA58E2382401AC778D145774494D2F476F43E0C3AEE004D360C4A65571BA02C7DA13002661B4838B22B9378F34579F46653A30A88BB501B325956B0B5A261174360F128850908B5A3CCDB5B611CAA26773368B13330C48C6667BB351A2AAACEC4B2A2C70601513CC3A18B5B1DC421C5A056D1645FE9312507AA1D52929AC289F6D75269BA42061D556243B34C638A72DEAB939B496435951C53B22095906C68B1299A288D5F5CCB8762E76477C5A0611EB468ADA9073C79B09E80621CA9C4D69A7146A812133B9637F074586A5BFF4AAB6CD0140B43A1B6E2ABEE7AC50489B9E5270A93541CE03B72C00A26E274A7CD3793334CC04E9285FDA673E0241BB9C9A34AB7935508739E52A29F22CBB9664251C6068FD3842C80B17A0C1772F285525B22C65AC1DCD520217DB5715381C9A663DB0E512B9F9711537BEF235313DC46B758839321341E95348A75972A7FB1DBFB183910A442E985628843B93505534C63C8D8A5358B61D7353C2D174AA1416677172ABC66725E7317ECC83BFF257819790C9F9C506DBFC647DB27BD9747673F666D1239845744BE742BB27067BEC53437D48A528F3221D60C3E1EB00D87045D050BEBCC2B433F2BB2962B0E2E70FF0CC52A5BA7412E39194AA226872184108C4FBC459090C59E4A27FA4823ED8B3CD3C59C2CC6A81D3017A08234206C3854EB3B75BA441AB8C321C55CC8574A04DF42E201180846982333A92D057C61140874A9A80A46037DC2A3CECD534E9EC39943BB300B1772510B7E8186C2D5B6555584F54E39A2AB6473806B9C9E71ACDC8C15E43A885A4C9B42763766C49DDDB890C8C7B22159DC91865D0E79AB10372A9F45963C59D8C1857CB654B2BDAC0AF0A24A663C71B744DAD1AB68A90462BD79F6BA55376125562A5B1326AABA7C19E593B7D0CC1C8711B8CFE3547A00ACCE1646E766BC03767CF5BFBAD8F08590E2165C7706544DB5A942925070A878D13418D31CED35092F19C197DC2A517967E41898796A51AF8911A1857244C085045677865B125573CA5E24231251243682C6DCEE524088572D2015D9A25691F2532E75907C107323731591BB061D525757540338BE19913323D08A4082239363CE4741C92AA29F8CAF146AF1D277A76BB22CD54663914647532AEFDA02406099A75A63F7F2CA5BEFC74A6724896CAB84D12376744CCB1C6ECB1DCABFD20AAEB88BDBD04AA5A7E2C867B\",\n          \"dk\": \"030C762257832CD05D14B5940C483177EB3ABD152BF255C7E3131FD131867DF38679D2B554B2B397282071344DFFB7729FAB95F893B912D15B3BF43D39A97CBFD50C84D83C0D39AAAC921827981AD5D0001954369C3344FE869EAF7A45332334E4F30A1B94350D7951A0E4549612AD016528E2C94128052A304A87E2E666CB101E2F765ABFF89C17F1823D211C4B4355E1B734BCD42326B1BA24491903063B2CC18ABE453B003389C67590F00046D174C1C8B237A60AC9FC78AC381C11C358B37FF7B791FC1560D493630BBDB3C40CD4556B9E671B36814AB439687CF248B0CC794C491C4DB2066325257B45370F2CC8519071615BC73095CA69834955131ED7826FB9E89E56926615730EF1B6C4538B3253030578F63BEF067C0EB70CC4D08FEF146488988EB2EAB0648CBBCC4C163F129D32318536CC91AE103CE058318A9530AFB73EABC518B59AA546AC64B736167AB41DC27C8BB21912CE011C50EA1C4315A5A480183D9616ADC79EEC86B813D38E449B1FE0BA07038811BCF63C1EF57AE6DC2F58BB522B51C356901501A608D547B2ED4439C420853D1BB62E438CDF4CC179E760CEEB0B35DA53C0606E7AFB4BE14B5ACD9B7DCA6174D990670A8BA5F5D391B22781A051946F6B33B7D098155048515413819A0544058265403F6272B6FE4625A78A0F4196871578A6DD041476D0339049B1D70A2583E6514E089144E32960DBC233CB16CCF5691B8A5A1434B42F9B9C73A7BDFF384CCD952A79C507E5A4552FE2ACC3CBC118974DC0260024951FAB3A8347CA016CC220E1B8CFB22B769258223AF300639C5F3F2387D4B204C802BDAE8179222B4D8626C88E63BB74E33C7D210F34749465991B38844AF6A43BC81BBE232698B8CB8416BB55B385CB23C086ABD92F59B209ECCC9C4F660061309815434C5B27B6B2CA33918B0F97A813A59B007E57AD04B24D31379964D1923B218CC79798EB8AC65489B9F4ACA5097B248D1A25FB5A3C44411CA0195B02E0538A7B9A89EC660F2B796FF39D42A18E7C585A0B737798C75AFE74B1EB8941D10831012841CAB389A1DCBFAEC211F3535C34EC05F07619B883B3EC04A4CFEC19E4670486E57DAF0C086CB0395B537D06BC2955F90684FCB38D17B0AB43A5061659C69A522B2970F5579A6450AD2DC76163B5480F89437F5B46CF74AD6A85961E265F595375B36A2C96C52F80D4246DA29BAAD2A18DDB3E38F3C9C18B8732123299A00F689B1E32C492ED24280992CD5C8B3AC3109405A631C5F49C77D97158F93F7473B263F8B3395276346156FA7044732CB5273B27715228EDA6C53CAC9165AB3325622BFA9941C0F93913CB29D55A1A4C1404C8C8025DB6A80EB95B16E80A0E348D8EF54947E6CCC4C8374EF14284373C83B677D83ABC9B5BC0B7C67AFF0664B0A2786575B0A2A962209335D13B003D013C11C68176B6C3337C920AB063D12331D2D0282BF0B460634A3F823F2EE8776E4088D33651392B7E16BCC44E46345F2414CF175734286A73251C6163C1B3F60AAE7A1A3E478EF5E56AFA7C3391F7B0C4D380A1A60DBA7ACFDA1155975C1FCC7603929247DF2B8040A12D63369B54849A52F41B2AA77A32DC867B7304E092110387B2D669850AD9379CA70B3EF1BBFD487214F08616824787C4506C83740BE76CBE452CF99369CA80674D3B22D3E04FF95093BD00369BDC5C8126133C5A15B8A99A912132B19C275489B7914858E806CF4CB10D3FF33E018CA8776A88FA30C8782179D289132E0CA4A1BB19463571BE1977109C2BBE4C9433D6986AF12E7079C4B5783390C95C8468545F48A5D669CC0567CDBAC54420DAB1DA63456C692678105E08A822D0F99584D7A8BC76CD9038C2768663FA11A8D8E7B86BA52DD21B323EC22AC2E7B921216282233039546803D509FFB12BA2FB7D3FD38F4B06B848233BC90C2ED68AC41FF24C6425C6C9321A7B697BA5588B434B1B5A49CBB9FC479BE68257498A05865A7A0217B8319E7BB8B55C15967319CC510147BE9BA5511A967CAA2102C6A41298C0F7620E626693E976318E6A7635536B2CAC9DDDFA58E2382401AC778D145774494D2F476F43E0C3AEE004D360C4A65571BA02C7DA13002661B4838B22B9378F34579F46653A30A88BB501B325956B0B5A261174360F128850908B5A3CCDB5B611CAA26773368B13330C48C6667BB351A2AAACEC4B2A2C70601513CC3A18B5B1DC421C5A056D1645FE9312507AA1D52929AC289F6D75269BA42061D556243B34C638A72DEAB939B496435951C53B22095906C68B1299A288D5F5CCB8762E76477C5A0611EB468ADA9073C79B09E80621CA9C4D69A7146A812133B9637F074586A5BFF4AAB6CD0140B43A1B6E2ABEE7AC50489B9E5270A93541CE03B72C00A26E274A7CD3793334CC04E9285FDA673E0241BB9C9A34AB7935508739E52A29F22CBB9664251C6068FD3842C80B17A0C1772F285525B22C65AC1DCD520217DB5715381C9A663DB0E512B9F9711537BEF235313DC46B758839321341E95348A75972A7FB1DBFB183910A442E985628843B93505534C63C8D8A5358B61D7353C2D174AA1416677172ABC66725E7317ECC83BFF257819790C9F9C506DBFC647DB27BD9747673F666D1239845744BE742BB27067BEC53437D48A528F3221D60C3E1EB00D87045D050BEBCC2B433F2BB2962B0E2E70FF0CC52A5BA7412E39194AA226872184108C4FBC459090C59E4A27FA4823ED8B3CD3C59C2CC6A81D3017A08234206C3854EB3B75BA441AB8C321C55CC8574A04DF42E201180846982333A92D057C61140874A9A80A46037DC2A3CECD534E9EC39943BB300B1772510B7E8186C2D5B6555584F54E39A2AB6473806B9C9E71ACDC8C15E43A885A4C9B42763766C49DDDB890C8C7B22159DC91865D0E79AB10372A9F45963C59D8C1857CB654B2BDAC0AF0A24A663C71B744DAD1AB68A90462BD79F6BA55376125562A5B1326AABA7C19E593B7D0CC1C8711B8CFE3547A00ACCE1646E766BC03767CF5BFBAD8F08590E2165C7706544DB5A942925070A878D13418D31CED35092F19C197DC2A517967E41898796A51AF8911A1857244C085045677865B125573CA5E24231251243682C6DCEE524088572D2015D9A25691F2532E75907C107323731591BB061D525757540338BE19913323D08A4082239363CE4741C92AA29F8CAF146AF1D277A76BB22CD54663914647532AEFDA02406099A75A63F7F2CA5BEFC74A6724896CAB84D12376744CCB1C6ECB1DCABFD20AAEB88BDBD04AA5A7E2C867B429A81D1EF4BA900CF2342C35E355A429B5480869376869E37EF269561E028999F094D80AFE79A90E314F0064F00819FCA23920F563589055EAFF682CE66C3D3\",\n          \"c\": \"DB1E2920A7C52A79F588F79A711636149E2D0FEE6FBE132DA3F0AD98EA4AEDACA476BEADF3CF1D6DB5337430B833AB4FB4ABB07A0F05A0874E80C3CBE9BF0C044F711444EDCFE6D0F168BB56687CE965D35FDD06E1E4C5E004D2BFB1DE6767BC41D834DABD875A15AAB03F3A0F8155495684F45AAF2A28803F50C994681E1677FF8A960B3864B7E95599FACD1ECB063B54837C0C6F3D3C39C7183601251F6CF3156C8F544D789D28348F2F26513766362D6430AF36216791592C2F496D77BBC5B1793047E7EC5561CD7393AC4A540104745EFC932D744ABFCC302943905BCC00073E19509CBF876699095CC53425B772F8567120C662219A40F3A818E32756806C0A283D949AAB6477BED8C0C4E1E3510FF96C929D4173B38B10E6C93E5B9B98914FBD4809D9B8576FB4B403B199117087668B19799B19E3B830E132F98DDA88ACE8FF3154D92233CFC5FD02186E13DA50AC91C6CA8E1CA486BAE9255A57FDEF5EA7DE7C0703E5093493F8D0E0EE50400EE69AF8EBA22FF27C834033B39E0A48F81E58F5E5A7D801B0BDFD8B57981AF14E7267A3C24F897F97A6CF2A4D2E143707C49B51A234C54EEF76DA73C5681F302E272D195DB2C52A79505387B0B6B0DDDD5022ACE0AB94AE87A3F0B6F1663571EC9F3494163EF107957365778EBA8E0ECAC280DB5197C6E6349DF677D653084013578D2840DC3490D50CB863360ABB5CDA3A2DDE41992FC9805B8D418EB4BEF140F199FABC45039AA26697346C1C4A6D32E8F52D4077E4081BC7355D3E4D8864B98F8B9EF7911EECD44C5F3587827B7E8B8E4870B67491B61104A9442CF662FD0A9BD795FBDD5C364ACD4B40300B60C6225D6B6C4F5994D86B9EA4ADBA280B3781C11ED11F9CDEDCB03BD372DB567279D332C794D3C5CC49A335C4D8E174D25C4EAFF55FC23342EADD9F55E1D180B88EFA835286DFCBE8CA28D14F6E4C900BBF80A693A32D8654FAE9819612A1DD9C3C08C5B5E97F3F8E74ECBDA90E5DD8D661E2C81ABC2BB571F977CF86637F06E0CB5E9EBEB9CBBE8CEA5B223FA6343A7CB7C8F138364E9E4DC4D1D4055DCE415A682EF76DA70679F72527B3E913EF12A28E22F8609BE456638DFF0756BE30CE010262DEBE5B8A1656FD5823705D9AF35666FA6B7B1E43977B0AA229C8343C45B8BAEEC0F0833B1A2462A19E78F4A6B4A909BA41FFA10C2FD8F98FCB0B10E75B1E2EFC54552747105AEF1589E8BB8BA7DC9A2127121FC63F9834F0FFEAF85D87D53FAE1BE362B394A8574701599A8918D855D2F39A798D6A2268C984507D46FF978F1DF46F2752D6D593630459E9ECF827ADD4693CBA0D6A22DA6ECFD6A7A2B9B2448292C850EBB5FE03C0DB452D7F06278D779EBD87FDDFA445A387A2F40D040D9F97DCE88E164A766105D95CF2A91CD85795A09F5299D5C51AD28FED62F4B67186185FD52837691956EF5A116A1C37B6CEE25B59025A319847A4EC9062FA343BFAC0FDF62A9C4FAE854823FA0D8A906632DF5466DAF9D8B631DCA3310C090D9B7E\",\n          \"k\": \"DEC4780793A61DC6222167547E251BEC419B282883B18F9BD06E053DB258C174\",\n          \"m\": \"52CEBDECF06579F4A9351F77CA95B5CEDD034D812F3FB7FB50320CA80E4118D5\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 36,\n          \"deferred\": false,\n          \"ek\": \"82DCBC98650A04861DDF15380C8644C6B93F197A5B10702CB944439BF7AAFF090C49E8CA58DC507E5643C17A4D2912BACFB76454D45FD6EACD191910B472463C49B76684777BFA71BC18973677256F649FA6041A6158046F75268E7CB72E8A974EB2CC7DF1B8AB45B0C651BF3D99211C071D55B9443E6A4A65B976962300E5F7325A228A61727D8733C0B3012CC51C2332FC5BEDB05962B771A232B55B4BCB41AC4D85FC6BE5380ECD3259CDE1B809D8B6E67978213798A828540ADB86124B6F137CBEDC4B64E7CCACF78031211A300A79C59508A60A73ABF93580AA64A7E661C282F5C00AD8784B002F5AC79EC4AC85E0290FA861B9D8B30D9E56CB934C6F4CD5C033C7AF7FB55F20C5A11574B3A460C7A3D73376E6CA77AB911CA118C963B97AB675ACAAC6BB73B0080BB5E0261F79E40C0FCA9129A9C118EA705B0BC7A2B45FF06CC15214A807D3521026BFF008024E951AA0752BFEC022CCF92A519B8D1CA910809B63CFA57C341490BC673243B7CC3E2A6AEA2BAD2B885E39738E3973011A67B266C438F2C2C1F63B251558947C00741939278055ACB8569F91110D895B97DC07BACF1389EED1271051CBF6A7B633A47848FB57BEF485993C5462E3B5548B96CD161D58510794CA60951A2A046C8743807619861D37A420DBE94633877134A92180C05B14E10615783C7B32B135428E59311BAAA32FE37A7C35C4390D1185F2931BAD539F08A02198D6450DC55BFEF625E3E209EEF2A7F38074DC681126CBBEE6AA3C974BA8D7ACCBA719292CFA33E4D5094F527BE94AA2231A762CC0970C7C81793471295B9B2A58810952A63B6589B29953C942CE49C87FB7B49B400210FF645C88179FD76BA408DA81C7464E76FCC3329589AA99855564329D8914E34A7B0D02685A92817C28B2B3F8A7A7615BBD569F2DAC20E161077EF46C6FB926ACF3C94DCC9A4B0A27641370B4027AB082896E188BC5CA32BDE911C5D865C87A22207AC35B72B0B8FB342E3A53BF735796F3C8930417CA84CEBDA48FC1E34AF9610F88C18523A4037E3540757162875CB5F82B1718B32EE70A39E5B216220AB3148CB1CCD118CE213F2D5680EBF865AB018631C88FDB6C763C082F59C6CD95156595E22FB0DAACB56341B7645027845813A59DF203104D9C8B1F647BD3537937F59030979463BB63F7C9B69E57CA17491F3618CDD162A9FE1C3F9B326F567C26D1AC6050685FC68A93CF59178BB87EC8D8B703519E60A22702BB5FBCF3BCD6D496B79B65415C175C554298572AAE35437BF899ADAA5C4F513E9F220553093BEBF1769743C6AB0B9FB64703C6305CC64B84CD76B7C8E61429781A822990B698852A3B583AF48C3B22092C513A223110589941D530C8B4CA25CB4361F2001B166C85CF910F006C5B0B71A109AC5986F8264BA26563E69D122644B6348818145CC026B783D917E74C4F46E0C9BF86250F521C21B64D50E910D1F1CAEF4B05F5A50491E9CAC2F3B5CE36407D9937D0E28E47B52AC460103F272642093CC9EB18BAD44AD36B8EEC367F155159ADBA0251F195E52032F1B0514BE83F8DEBCBBFDB5755267E76B028BD07CB8327CD431A7B73D289247210EC905A0529234B2C62C7A66338C1D381C88466B4832204B1B05CD1BF8E0A4693D941A178F62E9E09B74CAD5B\",\n          \"dk\": \"1BE898512189156358E1203AD43B548D039403004E5A5287505054A04B6C9E13B13E76332AF0B500435505D8C9C46B1FD6DABACBF58258479F0459BB13F5977A842AB7102092CB9BF67C3C4524AC2D25608348756529C8CD0652D5E1BCE22A1C8BEA2A72655EBB6A4A25C95A4EB242C760091DA949805A8F16BA7CA0874E01173D873590EBECC87CF79B103096574524B8149BBB9BC4A813AFC20C2D11ABAD8802834A362908480E76E79A3BE55ABEE3336326184E9A7B044603C4A5636EE88FD4974F82511926B95339AC23001B342041735719AF81C23A003C540DD79F34584A025429D477B7B4858FBE4A9D01F5572E8287385C5C69B0223296924EE27BFBA2757D5B11A194803B1094ECF66267411052BC0C021A77C773A940AC08487B90FB33631AAB5D9190AD613C6F360216725505178952CFC63DD5737B1917A63208586E5ABFF7EB03C9B7066B190C03B6CF77048E2D222719D8517E707DA6AB4FE42B4058161F7DEC4145231A6BAA9FB04C638898081543503A5CB7488C77CB0C6D39E45FD793B38B9019D7216C0027A06729854F378C7FFA6BF599A54BD290BAA60A92FB60CFA46BE667C052C176DE9A1C526406566093B4E899E6738C05595FDEB833BC06B3C5014AE6820082566E65AB9F55D915FB3B0B2E560EF2675E34107AE23BC746E23554996DB1AA94A6DBB20A0B43865A9907D644BF46C2F8E5BDCD298BD78A214049A69F650055725174FABE4CD2309A509BA3CC96EC611BD0C6BC5677B95D0B3BF3A51B5BCB06780591B60A0D22291922C27DC9F24C1EEABE27A704463B5F52829DB80CA0EAF917437841C8230526930705EA5CE2BBCD6FEA79F2F196E7CC74A560B13D48C7A2650D5929C08D53168145935C7638103291A5B2518314AFE8819088A25205D17F4ACB5F8080A824874B5DC559597B24E7D706AF510746EA651A3B66137973E163CC96F13E0D631BC696C451470B4145B073360EC034A39421626FF7510C1B88518262D764CE8D342720674B368C8A17F96F992979EFE25272E50402796A2FE59B3AA37EA1E63640B470BD0763D40029CCE3C54024BF33E63A8FE313AB9549E4D32A75C67D21E77D846AC79BEB704ED576C914A866359EA64A548252395E5A873805472C233FFAA6B5766179E0B1CD83098A16971313871762E531C7AB2483A1AF2C19559633B10FE80053504E5584104082C6B8967E86D63025BA11949B6CCC10BB52C93B032936512166099680C83A90E0EBAB162723208AA16E21418010C6C8B79354422695429F382380B08BAE2138C6679958A7711B78362DB5E087BD0459F27AC095031CA024A37ED1624638A7EE4C54A89009888659B2C9779EC4548D0257EA03CE5ED553A4DBC6C8BC7FDB21017EF4353EA450D4B1548BC65E42097B5C86411693260EF44CAB02A008158BCF81CE94388E87BC38E0BC1261544C8670A9CFC954458C02A310BD00DC174C3C1F06326F9A34AC0D2B4D37DA1389D31F83F49E169CBE4A5B742E475CB6609C4CA9B90B584B664741FB97BCDB84A08FC6028317BC03F2C0EA665355E589BB24448F3222976A59426A93E4711072664DDF616D5D5C3971EB03F2D4076399649F11CBF23194208C0D82DCBC98650A04861DDF15380C8644C6B93F197A5B10702CB944439BF7AAFF090C49E8CA58DC507E5643C17A4D2912BACFB76454D45FD6EACD191910B472463C49B76684777BFA71BC18973677256F649FA6041A6158046F75268E7CB72E8A974EB2CC7DF1B8AB45B0C651BF3D99211C071D55B9443E6A4A65B976962300E5F7325A228A61727D8733C0B3012CC51C2332FC5BEDB05962B771A232B55B4BCB41AC4D85FC6BE5380ECD3259CDE1B809D8B6E67978213798A828540ADB86124B6F137CBEDC4B64E7CCACF78031211A300A79C59508A60A73ABF93580AA64A7E661C282F5C00AD8784B002F5AC79EC4AC85E0290FA861B9D8B30D9E56CB934C6F4CD5C033C7AF7FB55F20C5A11574B3A460C7A3D73376E6CA77AB911CA118C963B97AB675ACAAC6BB73B0080BB5E0261F79E40C0FCA9129A9C118EA705B0BC7A2B45FF06CC15214A807D3521026BFF008024E951AA0752BFEC022CCF92A519B8D1CA910809B63CFA57C341490BC673243B7CC3E2A6AEA2BAD2B885E39738E3973011A67B266C438F2C2C1F63B251558947C00741939278055ACB8569F91110D895B97DC07BACF1389EED1271051CBF6A7B633A47848FB57BEF485993C5462E3B5548B96CD161D58510794CA60951A2A046C8743807619861D37A420DBE94633877134A92180C05B14E10615783C7B32B135428E59311BAAA32FE37A7C35C4390D1185F2931BAD539F08A02198D6450DC55BFEF625E3E209EEF2A7F38074DC681126CBBEE6AA3C974BA8D7ACCBA719292CFA33E4D5094F527BE94AA2231A762CC0970C7C81793471295B9B2A58810952A63B6589B29953C942CE49C87FB7B49B400210FF645C88179FD76BA408DA81C7464E76FCC3329589AA99855564329D8914E34A7B0D02685A92817C28B2B3F8A7A7615BBD569F2DAC20E161077EF46C6FB926ACF3C94DCC9A4B0A27641370B4027AB082896E188BC5CA32BDE911C5D865C87A22207AC35B72B0B8FB342E3A53BF735796F3C8930417CA84CEBDA48FC1E34AF9610F88C18523A4037E3540757162875CB5F82B1718B32EE70A39E5B216220AB3148CB1CCD118CE213F2D5680EBF865AB018631C88FDB6C763C082F59C6CD95156595E22FB0DAACB56341B7645027845813A59DF203104D9C8B1F647BD3537937F59030979463BB63F7C9B69E57CA17491F3618CDD162A9FE1C3F9B326F567C26D1AC6050685FC68A93CF59178BB87EC8D8B703519E60A22702BB5FBCF3BCD6D496B79B65415C175C554298572AAE35437BF899ADAA5C4F513E9F220553093BEBF1769743C6AB0B9FB64703C6305CC64B84CD76B7C8E61429781A822990B698852A3B583AF48C3B22092C513A223110589941D530C8B4CA25CB4361F2001B166C85CF910F006C5B0B71A109AC5986F8264BA26563E69D122644B6348818145CC026B783D917E74C4F46E0C9BF86250F521C21B64D50E910D1F1CAEF4B05F5A50491E9CAC2F3B5CE36407D9937D0E28E47B52AC460103F272642093CC9EB18BAD44AD36B8EEC367F155159ADBA0251F195E52032F1B0514BE83F8DEBCBBFDB5755267E76B028BD07CB8327CD431A7B73D289247210EC905A0529234B2C62C7A66338C1D381C88466B4832204B1B05CD1BF8E0A4693D941A178F62E9E09B74CAD5B988AD4B51E1589D3379C6D3E70209E6EA8655AF17EB0907869F974DAB202F540A54A288137BE236A5FCF6A8FBB160B2C2EDCF2F1F63A92F0E985CC634563DB61\",\n          \"c\": \"443DCF704633C6E74255A6E83F0F4BB686BCD9CEFACFC536EC6292A2F4D70C11E4F9FE4A94F4B8E92548EC234FECAFA84BF1372D7858C554BAF1E2556865185C02B0214E950F2C30624E5E42FFD1702CFFC5045F3FAC68E8D2E48063F9402DA21E69E49A1E9AA77DAC5EA1E60ED77E8C498D70FC67782EC17255E0CB7E116DC75168A5FC7C08A2D4A974A71E08A12CE9948A15631A90CE034C04ADF9E99F3B93D5FC4B2C9E880018496F8C92D40181C6A47E313DB9565B377BC3EC2D5ECF0D43A9ACE5EB0439A230DF989A01A662D83C8D2BD7C0A5402ADBFADF91507A8313F1C6B7D705E930182FA10B91FF7359C0D2BF26317015DFF6845E2F4275C76F5CA9F115BF90067E95EA7AA2847F05ED50897F62CC8AE42DAD71C3CDA04AF5FBB756CB7F11C78794F8FC4544D8B3AC1E9B44B545AFDAA2E7C5945E11832DD2D693952F51B87837DA9049D080843D1A4CBF551227822A7B522F449C974925886961E7FE0849790F9B2FF8DF8D0D741E9A1B8B08647810A64D5420BF795E5EA336CA4FAEDD55876E12FA6EEE591AF0F1989D0C1E1CD1252A305467CED0B12766D12316CEDD93FA773B11AA9BDC9D74827C30FE4AAEB456B8AB700D4AF165F13D8A0D8AAF741C445F341628AC4F4254C96063A6986A5851EB19438FE9F633B131029545A2665093F2972678F62EAACEBDF7A49F3D4D67790E4142E2B2A0AA57327ED89A0B9E641CFAD529F61A82ADEABFE7DE1C0D7A99078A8C1E7A00C8CD251B9EB8ED4997508E16AE39B50AF7A132A6AC83031C7E0207E894AED90066CA97F2F616D0C935738D18401B9BC7904424DF5756525EC5D8358EC0B9AD428D0EB195541A93E75D35FADC6B0F8D5CBC8BF15C9F3A7C22BF95A4D864D011D6944BAD207A27E2B05C8C51C6686E9164EACB06D3C675642575739BEC34660A31CC45970C28954297FB37B4C51C4B3337C9725E5A6F1DD0831A8A06B4DF2F445DBF08612AF2D65EF19594C4B3E3F034C5547491EFC3AE9B34A47CB2F35159AA21C097A2FF9CBD66C7A50143DE4F7FF9E4218DFDAF52DAF4040EA17B8F1616E590699FF6C71C37D1386806B2F36E534195460E2774D475052EFE3338B43F8C906D3FC0A736E103FD192B4084F6311F34A2BD162C0B2B9A60EF6F3468E881D7CF56A1455B815F2999934311894F4EAD90DF175E60F56B56C1A2BEF12892563FA216D1C477F061C46DCF2CC056C5E972C5F3018DF6346983163FA7C787EF85BA9979E30AF9CB4C4E2A0F4DFD6EE5B95AC7F8ECFD46E7491B4424C22E82C521F436E4656E298931E3791164360A0502BEAE7654D43C3BCF66B2D69B5E2EDF0CE17961ADCE8A600789E6545AA31FDC12030CCD478C82D1E319F4DFF1681B1209BD867627A443329B48FCD884BD986D105772CA3786CD336366C0DEC0E9469ACDE5FEC612DC24862A7FA00D45EBA7019D237FBA37EA67A4A074C652E6EF910B2C206FBA0DE6D23CF8EFCA947A18737410DCF14E496F64582B06D78FCCD7538232C4D8F122CD9FAB29568B\",\n          \"k\": \"9DC0B2ED91CF4609FFB8F7240D6CD3F65D45105A35770A105B910BD9CC911CD1\",\n          \"m\": \"161889F2E92B1BB28A257B45D179FB76847B664E6D7B5FD9698204A426EE96EC\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 37,\n          \"deferred\": false,\n          \"ek\": \"FC964FC0820E5DE7A73BC507469B013F2C81A225C8C067C4A9351467847DC4D38FF3237A9A38A8F7C273B08B260C5A9B20F8493A668459E0BCAC480E0BB50BC62941720283F758B7AB13B444E8A1F8366FB247C3CC339D413671ACDA6354220B9CA0515881A688063FA87487356C842324B35B064B5847A75509AFF086086CAB80DE6C7218D95877313B0663031E867610107F1491A9F069A2DAA4A88F26CD6CB97B43B51FB7156A47550434421024CC360500AA7595BBBA99116057A64A98C76D61082ABC47BBC2614D2B23EEF8A68D1065ABBCC057B2717DBA5D6B82BEC6683551F082C85C3C430580D7125D1EBB1EA410748D6835C5BA080D1140F9574A4460752C5278E5F24DA829235DEACBFC3663FF71421F3B4C3F073764D11C39C9AB6B6B2F1F24A7CC84AA73A976721AA7BBEC515D0A0613D06F80FA461A032A274911B82CCB9B9A4AC86C3555D37A267C54137B141A043E9C861C58D6A88F5A47B895ADDCA736D11CB227D190481B4FDF680943539437B04BA202B21B19C06BC8037B93681E2121E7A49DB8271422834CA4253F5B96AB7E723FDC599B8BF0567C176BC7C754B6402642921ADB8C30CB8329A1E3CCFFC8A501A077E34495D9333B897C41F9E2A11151902A9B19AEE16BD4038DCFA6118192CC3B09523BD89C0DD158B87276A6316DA118785551B9E1078C22949D6D5302EE5945F87C5A8EBB6220B673B8B7AE6499C993633E844BA8A14CCBDFA931D1145BBC260EC933CA2B8B861F505A9691CEB2B5716CDB1E07DC6ECA0693CE0B8BBEACB59F705814F2C46B336C65240BA7791B10226D3892AC60D11F67F29283ECBCB459CADF6867F41847A99A2E0469A417078AD80B1BB35549FEA2164B8B557B5610C64C810E211DB697ADC43034DE45BF407729B9F8CCBE3379D471367DE881E6EC741A4130C78A90FF24C7C3899129038851069BC68B9EB533354AAB155F829F6AEC9EC9D82B20B4927EECC243F014EC846D59D5C8F3517F96873511967408128A1705B834F81C4F1169EA500A66EB21682A09CE44CE4C9541FD506A6AF12CA3634733E4661681871717A560685794685DA121565671B50FE866B0A565B0A9728A97C6AEC4442C498C54D77184C44D919083921204334CB058FC5EA4765C0478104770B743A5093E842C013769F16B02A46083872C58D3D678DCE87A3799381DF117D0F357998C853606692ECC1F087C0883CA575D9159BE882045E60DDBA38ED3BC8660121FB8EB2D6049A5843A2D2A53BD529881799487B30438B4A585B2E77768E87D060C0D676283431A55BA9C129F036751835299D78CD2D85748C246F8915927B1C9A1194BAC1A103B5A1BB69370C685B15AA264C436683ABA9F4A03C49DACC5A1D3CD5C133599EB16006850BAC4108239144CE682B7301EB220A363032FB608137786898375366B002719580EC7F7733BB2C3DE27974FEB56C72AA61252A2D63CB63D380F1FE3142439AD1151B543591AB0915B514B3E97EBA17CCAA73FF0A3459393BAE20B1D8A8961C0615B180DECE602335A11F7617EBCE0A3A0B3AD1A78A3C8C682976388851285D83BA9E358AF22A14039CA015FE1B42F8A3EB454578504153817455045166F24D6DF0071E884AF76ECBBFA430FC31D1F77405F4B404B538725F561884EDA\",\n          \"dk\": \"7F7A7CC42A00986601D7E76792ABC9454422BF39AA4FA93144980345066236F60D629ABA3BB74FA79718F0A95F6434508A9B0F0BC672D4F5152A8561B8674892BBCD79AC8082394DF7E95F7910C8A2945C55DA8D847B5480B34671E20532B03229D51B2D9A3288E867D59A9C189A8686295D2604B048425331700FC329A948236AF5A83E9DE525417BB235604C43B3B2628642CE11B911A2A88038A5FA233555B3104BCBB733E446E9455B9D1A7409357AA19656F23590CA11CE05336B0BE8406DD39CD3B6A725D49A499B1809791540A948D72C56623A768C27A32826744D75871579B85D16695905BC62744885929723F308D4870ABD61B7EC3CAF86D2422D6C53CBF92DACB85AE49622F6E37B96F3BA812B26F0C1220B4C4D37A9BA4032197FD4025FC2C75147534F28CC8258A0314151DED61CB391B17E842FDE96AA3AB89C2C13C6BC015EA2DA00D12877320B4610C34526D31E2C7A981028C2C55BA22479A62F375BA7400AA4076617258C1E5A35650312F75253CCB11B28FA2FCF170FCF9A96C34385EB181C6D777621BB3CF6081A8457A7B92C89F0167B80976CF7512C10E1662DA3018AA9129E3661A59ACD85D75BA9707CECDC23A9FAB2DBD6AD85938DACF56C8FA27C2A7BBB520CBACEEBCDB088B65E47B9687865A231A393D645F080B61BB288C9166976208FAE67B8919766A7910BE02BBFD8586186361740926182A3ACA832B2151B5FB01A0E726650D4E92EDF59BA973446E4C886904919D07A8EB67267AE366D8FF6311C78B6F17193C655B46F51CC47E05B4D3416984CBD2C14AE228B44C1141427876AB6818DE42807FC1723B3BB2266418219004E68B59CA71C5C2B8B8C6D7405A0508D6A63ACC2B4C19E228F943718F18C8C743A020BE75DCC1B1CE1F1793FA52C6AD5CA3D63C871366237DC82F01A837A758D9005CE677C9E1477CF5B49A2427932F971821461593426498DF90AAB257A1683355DAB67FE9234AD2832216AA0A2F4690E0785A08459A2809717DB7A231C3A5D84A98EC91A446B1BEB8851D2A65D7D61569EA98DA56C7A2CA24C71BC511747111FB31CCDA0CB391638BC3126D4427ACD9171D7D2A79AB9815ED62C332ACB36335CED45411D1545E3832B52685D444344C77325F1372E89F254F5E33A1BB450B1A54FFE32CB4811B813E27C12EB95161A5A18105D2D937012070868AB3975D552C056267F0946E1A5C9EB585F170CC5A6A578405382E030A2A3265F468A9911341F7A4746B71C65C090398E4443EC6AB4486BA36788AF0996AF5C6389EB31237CFA0DC8C17132051A28060376A0C133FC51F6EB81BC035501952164504115F8C567B52E96BCA0FE13A2BBCCAF08637AE31BB4C0032EE32514E041C2DC4587684027D988133658414567328F42AEC2FB89B4C830ACE152C326443D82687A19C87D122EEF72C1E742BB4A8C52B81646CB0C487EDB917A122B44751F209489ABD629A0F232D0502DF7A5BF5710215DFCAD13F2B6374535A839CBC9A505EB3A9E8EF17B16B70FE04752C8E1936926BBEE856A188C02183768217293656C4B1E9372B363BC673110255014FAC01ECF6B48371B8E7E401C01E1640B8A4F1C3A4009489FF2D46BFC964FC0820E5DE7A73BC507469B013F2C81A225C8C067C4A9351467847DC4D38FF3237A9A38A8F7C273B08B260C5A9B20F8493A668459E0BCAC480E0BB50BC62941720283F758B7AB13B444E8A1F8366FB247C3CC339D413671ACDA6354220B9CA0515881A688063FA87487356C842324B35B064B5847A75509AFF086086CAB80DE6C7218D95877313B0663031E867610107F1491A9F069A2DAA4A88F26CD6CB97B43B51FB7156A47550434421024CC360500AA7595BBBA99116057A64A98C76D61082ABC47BBC2614D2B23EEF8A68D1065ABBCC057B2717DBA5D6B82BEC6683551F082C85C3C430580D7125D1EBB1EA410748D6835C5BA080D1140F9574A4460752C5278E5F24DA829235DEACBFC3663FF71421F3B4C3F073764D11C39C9AB6B6B2F1F24A7CC84AA73A976721AA7BBEC515D0A0613D06F80FA461A032A274911B82CCB9B9A4AC86C3555D37A267C54137B141A043E9C861C58D6A88F5A47B895ADDCA736D11CB227D190481B4FDF680943539437B04BA202B21B19C06BC8037B93681E2121E7A49DB8271422834CA4253F5B96AB7E723FDC599B8BF0567C176BC7C754B6402642921ADB8C30CB8329A1E3CCFFC8A501A077E34495D9333B897C41F9E2A11151902A9B19AEE16BD4038DCFA6118192CC3B09523BD89C0DD158B87276A6316DA118785551B9E1078C22949D6D5302EE5945F87C5A8EBB6220B673B8B7AE6499C993633E844BA8A14CCBDFA931D1145BBC260EC933CA2B8B861F505A9691CEB2B5716CDB1E07DC6ECA0693CE0B8BBEACB59F705814F2C46B336C65240BA7791B10226D3892AC60D11F67F29283ECBCB459CADF6867F41847A99A2E0469A417078AD80B1BB35549FEA2164B8B557B5610C64C810E211DB697ADC43034DE45BF407729B9F8CCBE3379D471367DE881E6EC741A4130C78A90FF24C7C3899129038851069BC68B9EB533354AAB155F829F6AEC9EC9D82B20B4927EECC243F014EC846D59D5C8F3517F96873511967408128A1705B834F81C4F1169EA500A66EB21682A09CE44CE4C9541FD506A6AF12CA3634733E4661681871717A560685794685DA121565671B50FE866B0A565B0A9728A97C6AEC4442C498C54D77184C44D919083921204334CB058FC5EA4765C0478104770B743A5093E842C013769F16B02A46083872C58D3D678DCE87A3799381DF117D0F357998C853606692ECC1F087C0883CA575D9159BE882045E60DDBA38ED3BC8660121FB8EB2D6049A5843A2D2A53BD529881799487B30438B4A585B2E77768E87D060C0D676283431A55BA9C129F036751835299D78CD2D85748C246F8915927B1C9A1194BAC1A103B5A1BB69370C685B15AA264C436683ABA9F4A03C49DACC5A1D3CD5C133599EB16006850BAC4108239144CE682B7301EB220A363032FB608137786898375366B002719580EC7F7733BB2C3DE27974FEB56C72AA61252A2D63CB63D380F1FE3142439AD1151B543591AB0915B514B3E97EBA17CCAA73FF0A3459393BAE20B1D8A8961C0615B180DECE602335A11F7617EBCE0A3A0B3AD1A78A3C8C682976388851285D83BA9E358AF22A14039CA015FE1B42F8A3EB454578504153817455045166F24D6DF0071E884AF76ECBBFA430FC31D1F77405F4B404B538725F561884EDA1279BE1122713D340A3C86B3CE48C6C5CB5E48522DE5B24AB57F59FC341BE6ECC2F75B1351CDC350BD1726A124C06B996F566FF820A4D3569F634D564EE84224\",\n          \"c\": \"58919FC3F7105957A7599EC0F84E2A1031F42E26DFD7DE44CAC5B99E1272313BB2A6F52B8D33C9054B368439A123AB49E75C7B3FD397F1E6B962126FEB574D0005C69572CCF752B10D9E84569C9BCF3F3550784AA1239E0B4B4FFB38EB5937B287B5C7D8DD0FDBB38E3BDC081B43FB7967E7E35D8FDA89153AEBB7FCD7DD810738A555C653B84F6BD246A17FE2EDD0FFA8AFF151EAB2973F2F8600FDDE69B61D06EC99CC547D4A81896A36BA9A3DC66A5251EF145C9A690B2C696C165FEE89B8C2A14ACF8B5A12CBF2216118FE3B29E2FB0F194D418BCE181B096FFBAA4AA515CD45275459BC5FA6E1D1ED33D00609F11DCDC5247FDFE841E01ABB7B935BCFAF9960E7E06C82463BD27BCDBCC3D12AA0F033BDC4605109AF3D7B270FC5D18C3CFDDEC2CDBB3587DBE25E4B0E9AB2049ACFCDE49874960482CD25DCF45433179A9BB69145DFDECA9AD2D3BA9AFA4B6B8681DED8DEDAD95241557BE198A7CBBBAE937F70D7FC513782258D89AE870FD2411BEE47E4993B9BC1F66852E4D314BD54C848143398278F11776CD8FA01FF345E606A514ECF780AA5EEDD94B7F88C495F8E56FEE7FADE2F6CBD3406FE540428BF89CDF66357A0F5F45DE330D9FAF72F02D41D9BD7B6EC9165C70386824DBA7E2322A0409764FA9DCA366B4A9C1D3E01495806F959D13213471978833CA2BA501C634322E67CBEC59FFA4193E848025BADE1971035636CE8CC833047EACF669FF064772647AF66CBA4C5C87D101C1EB2EAB87A8C3CF31D6C970E846C8B50289DD1572B19705CCA364D2B3090A75AB299749D0A320C2260A686DF569F802FDE0589799BD54BB5FB8873ED15CFCE76CD20954E52BF7B3DBC68B573EC0502F3D6715C249486B5691F91CD11B4A7B3CD1ABB8C942E2426B0AC782B306BAF6C0D9F2E2EF53FA9E142BDDFC4FFC6930084036BE9E51D997C8E24F89556E0CB21BCCCE16330AD104A8C6851B2E6CE8C58613E19459E2C2398C9B33A5FFD19CE5D9624344C52F379AF49F04F1405E9510598622B9C6F88E95DEE43A91C0540661AF86E3016BBC2339C2E922166AA2D6ABF987F1E78EB1E3CA9486380E62577FF487E1E7A4536C7ABE7F8FC9569E254B96BBAE04DE314F7D5C36A388336D53B9BA8667350C7F3A04DA68BCEA28BD95C52D14E1DFBBF78C8CAC150CC15D015E4332FE109E9FDAEB64BC3A400599CA3D2CBD2A720FB35675A5BA78BE61C4A82B4315D3AF4FBB0150592B7EC135E063CD5D7C23CDD256D4501513BA576BD6726878F2B43B64ED0DB79CEAA344365617570A1E7957ACA8F5B5F2FF708190B2A4985A6101CE0475DBD528489F4FDD8A9E4276CBB7E6FCF74866A49B430A666C4DB800DEEEABC4E7AE9DCF6D91C421246F91394B949575CFDCAA8527CB9A4CE58CC1A53C7FB43193FA39E4C12EF8DEC1F1C6CB8134E5938AB23A9CDADB4099498F5F39274F10228FC297EEB7BE6E392F00B092E70B0BC262F5E8720E87C8BE1730E22193290DC1AFA0AD2D98BA6206AB3E7B63FFE6C69D576\",\n          \"k\": \"D8D24017609D9ABA1414D18AD4AC9E14A0954AC1A80AE9F29527351898F61483\",\n          \"m\": \"3349557DA70FF69886ED032A91D8FC23BE9E5245406670679A6E92AED870D369\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 38,\n          \"deferred\": false,\n          \"ek\": \"ECEC377523150E39B8E5A4B85F8237A685630EA7A3443A9249D4B90E1BA3438CB095AB5555706EACB3A61D5143029856C7E80B0D9156BDE63AB4632605F6009450B1CFB6B81A58A43687C4DB56BF087A75DF047237E9A9B79408A8369CA3AA1731A7190FF3C11A7699E3F83A1F655B3E2C252D732BEED0703A642BA3500859AC70C51C3DE886C26BAA47EB221074F97FB74957F9C61A02405D2E2B95FE198C2AA24AB3D649A3B6803C3636C03B3E199AC07D103960577738CC833CD42CFF97C55FB4BC577B70D1A8642ABB77A1530ED0F1405B2245347256C0D47005A634C9237420574130F607BE82CD00D81E15562AB8461F681159737ABDA7441F7871AE7B28401A6BAB14412A5BE5A4BDA22A46425B8B51964A673278B873C4F61DD8CC5A7DD3B89E11652C677E052643A32139AB971B5B0061D600440495B96AA3113725640F6A91DA0B4B651630EED446BE53C6C6365BDD2BA0F757CEFE05C1231B666404199937736E6A5FB3466BC4590767C2BFB2041DCF254886466288BB45F8E91BBA876B9C093BE2616BB3D2B516526495E74BD19B1E9C36543697ACDA981687111FD05A1B9F2255E8809152138B983790EFE836DD7A68A8A150513699969015BD65B05D364FBC851E11B506C607BE0CD116910C94A3E3258AD263683462F5826B85B4BB1DD2917B4034AD25210AC26F2355B05681015C9A2BF622BD28431CF2868ED213124F124C7C6797C34952870CCEA4970AF909BE10A57BC7AC37A536B7DE2A8B0C4459A6223B4EB781C2462ED3AA135015754BE56F2B501AFE68022C81C16D40726B8C77B0845209FC4B77B64FF08609C0567390F4C9A8187A15C719FC5C9C01433CA77809D3F9195D8BAA94B24CFD909D6374B8DCD2355D1723400AB552F264C938B4E1E8033BE2A0B6FBA03729B38FA39F285AC01A136C07D10DEF390BEA5688C41B155D81174662AE48766D1791C60F1606BDF9177D995D9167B755908A68E7A7CEE4AF53A42EDB486C8D119FED00BFA643ABEB12666E2CC2295B8336122E7D9601F4DA405B7666EA276A172026264C23863665E2C21FF72A6CE4186016D88F167A358889AC1F8B028CB4622CF07474181BBA6BC5D331088B88A1C185B28B90CBDDD54D88D4A911DB638EF236D6D06EE968B8E5FC11F2D9AEF14A3248EC921FF0ACA4BB1369F7B5B3991ED6B16C3A918FD9397BA4E454FD209567D97C8BFB47558B6AC7726D6C83768E0A64B034C0EB36C8BFB0BB86C52B51FB7DE503C390B97B00A4534206B267C42333B4C05BC90C7FD25609548B2E7087E792BA21E360038A6218872F94035E5F13149CB440B302A591817EB0E229AFF03CE9005535978F930622DD422722C059E0BB855143053F348B23D5ABBD49B7DE9478EBB63380E75F9054922AB65F4E897D02F9AF641CC6A34957B91A7969A1270E4264451053267137ECAB8C8C8B9B76354A63E1ADFDCA087E54A67F8A2DE4AC6A636348017C6203B4A27322167C2ACC85F63335E8AE4D1909DD1C3DD228C0E3B84470446EDDE572F7924DD409764FD98F43A20E5AC15AE9202BC1AB58BADB3D06B6B77E81ADF763836398BBC45433015B6523652751B8292AC51230440C112C6317A66F24C4BF927C7EB8C186C9DA10E1BC25A4DF1CCA9B6C3407955972448DEBDB284B\",\n          \"dk\": \"F8B574AEFA5362C79AA69AC10326288D0571F154CB001C7FBE1C274C1586792068209CB65D70CFFB21947ED752EBE9B7D72502726C1DAA9450FCCCC3A4C4BA0D1890C530C601C18A7001B079684EEA47AC24251E9A599967A24EF47B52B4FC580E995E411237C02687CB38030DE74F3EA0C1740316269322482C64414A8C73230A3B58554C57142F2613D190BAA3142220FC4D99A5806C89785B8730B39C12CBEA94F69573A61999C82B7E4BE3A9B605243D25971E24B330A7A51E5681774AB7120C13DA25ACE8494019192F8CFC3043628F7E6B531F8886AB2BAFA1D39E09CA373AFC1A46919F8244066ED1B7C2124C01D760D216978191BF3573C093A15FEC579D83EB5C6074B0A4BC13094232857181DADACD2C5A4AAD97C016894EB413C2CE671687A875B45CC1BA0C63D9696C4FA102BBE797FDC86634DC28FD121EA9D904642ACFC56C2DACD2A7BEF96A43A23F11591EB81C954182403416ABDA87BB5AA75D510007B8D53491E0BEFD149658AC135D1341FEA37983D1A0EA0B40CE08A2547122C3022AE2DA8E2E7BBE8DF46548A91A50A1C8FE651E8E487788A2AB894B7912B15C10D09571E23589D166C989329B29349CF7A037916B467AB29ED59CCD37AC63C0AF6F335F5FD6BC09845BDEC1C86748BC389889425026E3105DCBDA9E4C5664BC2B75208287EC78B8AB031B6B494B37D51B0F540B4BE0360BA4744DAA369FF384C1D5BEF1640A7C76C7FD4743C7EB631ED126F0408809E00700120CD6DA114C5211521C067F1C7EE001AF1AFB46F7DCB90D70A3A35098946A4EE53629CCB678642A11E18B687C9680FAA3AE926C60A5672A0C786F17273C313548D6894B27D52AB5F51E9623B4B03398FE9014E376040E522178185230456D505BC1026AB91BCCAD5B2C38ED2BB445753F30845A65A816816476BF184CB55714A64375E480CEF74917E616CD50E7AB635A88D96414DA6710E49A468C3AC91B85726536B78C9B93DABB5866998619B377BE0B54C5B6AAD4940156C549782C64653A68FB0B15E02089FBAB37955104BED20696C48E388257375C080C33BDEEA081EA3285F6A94A128736DC221C17A88CB50A6283F4C6684AC0FBF8C110344DCDE703E4A7C3DC49296AA684A31CCC0DBAB7EDDB2EF9C40D35A42D077C62ACD734BAD82E7475833A1527222112739C8591F61D2156BD6BC886FE501873EC67061C3EBEF03427C969647918433088C0C8C7CBACC9CFEABDCA3A91AD17A2F5AC8C9F195D53C294878529B0B9C8D310897C09483B3C46A841CBB6183107154E99E7A91DF44972C7BC7162926C9672ECA74FE155BBBD4160D039821049A8FDDB91C99752B99B34E122173F654CFD5C69DFE7608CC96F7AF1A0B8476FA4B715E8B81F01E24A79565D01005667DC264D31AF87F6777E118EAC48346ABCB281523E97A787194B6A6DE33362F31117958C1D35AB4C190818C0049FCB6B42151C8F038B8018B997CA0E7DD9962B0A62FE38A424C06A4E650E68191C4538AE67B1C3E997627BE3B26BE14166F016A8E80070CA86B034ABCC358B2CE917FCA34CC2850920656E80597A35936B28794C63FA944151C0CAF278CB589F86171D0F263767F294A71B6059E496ECEC377523150E39B8E5A4B85F8237A685630EA7A3443A9249D4B90E1BA3438CB095AB5555706EACB3A61D5143029856C7E80B0D9156BDE63AB4632605F6009450B1CFB6B81A58A43687C4DB56BF087A75DF047237E9A9B79408A8369CA3AA1731A7190FF3C11A7699E3F83A1F655B3E2C252D732BEED0703A642BA3500859AC70C51C3DE886C26BAA47EB221074F97FB74957F9C61A02405D2E2B95FE198C2AA24AB3D649A3B6803C3636C03B3E199AC07D103960577738CC833CD42CFF97C55FB4BC577B70D1A8642ABB77A1530ED0F1405B2245347256C0D47005A634C9237420574130F607BE82CD00D81E15562AB8461F681159737ABDA7441F7871AE7B28401A6BAB14412A5BE5A4BDA22A46425B8B51964A673278B873C4F61DD8CC5A7DD3B89E11652C677E052643A32139AB971B5B0061D600440495B96AA3113725640F6A91DA0B4B651630EED446BE53C6C6365BDD2BA0F757CEFE05C1231B666404199937736E6A5FB3466BC4590767C2BFB2041DCF254886466288BB45F8E91BBA876B9C093BE2616BB3D2B516526495E74BD19B1E9C36543697ACDA981687111FD05A1B9F2255E8809152138B983790EFE836DD7A68A8A150513699969015BD65B05D364FBC851E11B506C607BE0CD116910C94A3E3258AD263683462F5826B85B4BB1DD2917B4034AD25210AC26F2355B05681015C9A2BF622BD28431CF2868ED213124F124C7C6797C34952870CCEA4970AF909BE10A57BC7AC37A536B7DE2A8B0C4459A6223B4EB781C2462ED3AA135015754BE56F2B501AFE68022C81C16D40726B8C77B0845209FC4B77B64FF08609C0567390F4C9A8187A15C719FC5C9C01433CA77809D3F9195D8BAA94B24CFD909D6374B8DCD2355D1723400AB552F264C938B4E1E8033BE2A0B6FBA03729B38FA39F285AC01A136C07D10DEF390BEA5688C41B155D81174662AE48766D1791C60F1606BDF9177D995D9167B755908A68E7A7CEE4AF53A42EDB486C8D119FED00BFA643ABEB12666E2CC2295B8336122E7D9601F4DA405B7666EA276A172026264C23863665E2C21FF72A6CE4186016D88F167A358889AC1F8B028CB4622CF07474181BBA6BC5D331088B88A1C185B28B90CBDDD54D88D4A911DB638EF236D6D06EE968B8E5FC11F2D9AEF14A3248EC921FF0ACA4BB1369F7B5B3991ED6B16C3A918FD9397BA4E454FD209567D97C8BFB47558B6AC7726D6C83768E0A64B034C0EB36C8BFB0BB86C52B51FB7DE503C390B97B00A4534206B267C42333B4C05BC90C7FD25609548B2E7087E792BA21E360038A6218872F94035E5F13149CB440B302A591817EB0E229AFF03CE9005535978F930622DD422722C059E0BB855143053F348B23D5ABBD49B7DE9478EBB63380E75F9054922AB65F4E897D02F9AF641CC6A34957B91A7969A1270E4264451053267137ECAB8C8C8B9B76354A63E1ADFDCA087E54A67F8A2DE4AC6A636348017C6203B4A27322167C2ACC85F63335E8AE4D1909DD1C3DD228C0E3B84470446EDDE572F7924DD409764FD98F43A20E5AC15AE9202BC1AB58BADB3D06B6B77E81ADF763836398BBC45433015B6523652751B8292AC51230440C112C6317A66F24C4BF927C7EB8C186C9DA10E1BC25A4DF1CCA9B6C3407955972448DEBDB284B3185127E1DB71871889DFF67F5D4E97656F7115F28F00AED6D032FB3816189C9EC00525B2D58F1FF5026A6F9CEE39EF8DCA115C0BADF3ABF3244BB9FCD113DD9\",\n          \"c\": \"0E505EABFC938559CA3FE4C830365B6EDCCA3FF29E815FB9EC8CF1857458A49E461FA69EF81E5C2EC80875885CB2FA27B810375368E7ACB588915944CCA39F196EFCA8D7D8DC4C7A41B12997052C80141058C24FB77E37AF8B590836B81913ABC4F8705F8A1005CE016E33230C04437C8187C4D94613CF60AABA4D664A836D5DC8F5CBDA739A1E464CB8838CC6E9B8D5A80A90799AF838724256BE25AC34E6A3D4E622B254E3FB8AE2D7C55BF64A88562853C876CF18E8FCA1AF3548570492AA3B400E3FE33C58B303BFEA014166ADA4E2BA779B463C4E606F46DBB00EE7F937788872F52B25E8DAE7BBCBB47956A1117B84E8B3A01354C5C2C276F89EFFD60A0CBB8B7B8BB5F29E26C47B34F9026E05B41C06B10CAD7F15446DCCF2740B9EF4E26E7AA3DDF63DF06A530E0E6765B24E213DCE686C292B4210C0492ED87B08BEA2C18F0B7AAF6EBC0C67FFE888F9BD0D55B4F232BDDE3F6C00DC972C16601EF2DBEFA5496547C913C2A51367E8E5B1B16AE02182E668BF8B7B5EEA226EEB93BC4564B74AF9B2EC5A9210EA9608E23B7104DF0C400EB97CD23694EC81B529B0B4E5F5881E3FC53414534482E6A22386B7A6AAA7D79B714395FC88A1C626BFE0174A14FBD8025E6841904D7D3DDD3BE941A102C1D435EC9ED62C0F07A60BE6BEFCD8E3BF34B2EA2FCA21DE91469479D5466CD6B599CC6DC9358319BDDE21484BA923A8293FA5A3A6664B81709FFFFC61180602F2E82E09DE6F80098DCDE7E50543A32A2AE4DF13F30A2ACC1F143AEA50388380F68ED9A70D8F90709839557E87B71A348A6BBE8F2BCD513B8AAD667113868F8DBA8B44F844CDDFBD6079B0964BC9164D7153EA9042B0166C2D35048CEF2FEEAC4979C5DC6E7C3470F99820C1B6FB72BBBB421D7E7602BE9424179515D6A6EF4E9599F44C878D25F2A3EAF38C1B06C6C45DBFCA9E53199AB78C9BB7CDFC9DBCEC46BFF12CF77D6F1F00B8A52A943BCA9D85A5E485E07C053FEAC6A2FA26CCE9FDBE9441CE1F379F15FBA36417EFB888EC69C2966EF3E52088E395843A32607575CE79C686FAD4E20223E198C069AA592CBF7EAA2B5FA375A01D2DD78326F5FDA930630D93C7C095B8BB7D0B6FC6020F2729412EC1C0D89F03989B1FDD708F21D22E5EE9C7C1F0D6E116BF633F7F989D693508EB1CCFD87F0455CF8B796FBE8BAA5B9043DCCB18FE013C91FC6CD47CDD38FCEDE636266FF5495A0493EF8932FE569D1E8E3AB4C477A106FE4C5426493DDC24C02CC59CD086D16DC4C7F494C12F75FB7971C0309ADE96445CEFAED2067A18D0AF1B6992FE1087F0A863D52DD2AA128CFA888B0FED293F674E29F2C9215A0558331F52F8E8F2C58EB9AD654A3978C8294A083795CFFC311F94E743A83FC684921D5A56AF179FD9EEA692D3AC2B937B07D54CD132DD802238AD59E8504603368EF12114F6E21E2536AEF4956715F24A58F8F59971FF0075DC6FFA14965A4813503674A3FF2946C4B36BA9520B5C8E905DCB770296F7A1C991C28C6AA5D3\",\n          \"k\": \"330D1A2D0E0B5DD7F759C29A22D91A09BEF17F8C5566A0D3F30E3817CFB7CBFF\",\n          \"m\": \"6F1694589DFFCE022DC4DF1852FA49A41C6E8AB9F7887E70DDAEF4232B045DFE\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 39,\n          \"deferred\": false,\n          \"ek\": \"48E11D1B0572F2892B8F81A8BE330B16C7353B5C319A34874F959409E6A41A71585BF36AEB651869AA87D400C6AF32B7F8270BB9D5C53BC3B600F493904AB000149FAFA1B090B544EA0101EE940EFE878DD64739CBD78D317A09A45775B2AB9E0383C75D049083BA64019658B4F44573C1B7C0B3A59495606D9162438B1A0A000C3AA5834AD5BBD11893598052E5B543E209215E41B8D49C4107F5658F7685CE53459F21452AC66F0D2398B6971A5A23A056444947220E95454A404C7213686E58A42E346CC0C44759504B2FA07198D742B1BE01A66C2BC64498CBCF6482DAC99DE7DA703EC1441D08C4A701BF84FC956B11411AD610146C59DA6454A8B3B742D91DE3C09A5317A9DADC23ED0A922AF339C090060B869B55FB3AA1C603C40868337687EBB27E37B804EA834D4D0084B21307DB0A64F69B7B49B5B0D237AC428ABDFACC01D1012A04008CFDD336A06B9728A036B6C60430A8CE887C4B006AA7E7C54834E2934F8400D030AA63E61FEE96C22FD49685A7C3DC2287EC2964CAC78B40340200C3AA6C8A67BDD30FB7605F0205CC0CA043A4574235B342FCE51195873F7FD99077CB31229B91D5923B67D391B457198125B7F198BC7CE58FD6B96BC35CC3D0281957B4110A011262B1C4CC0445722CB5E8C102408593CE6445C4584E373AC890187986A49414643CCB2861742466939C347CF021D1C19D36398A82C64D1FBA4C44B56CF384C04A1944449258700A66B1E2C9185A5E8B17772538B33B038B877353DC251952DBB899F04A6C677247D4CF4E4A5D2B22313A33A6D49237E7C04C61B2B0026531583C6CF6332C4C7853B7A8C6A4A5783D1B79FC5725F9C7157600747C0203E313CE91976CE44CC4E0D67DFA830448999357F076F2D41EE6FBA68C53C9AE5C025DF867E3060F3CF164218142D6C2B5B526BCE6789DBCBC5765846565575AB154C545FC3A81AB8FA1353B703B72B011A0405588FC0B8C20D13ACE9A93F6B28EA1C05CC686CB0AE722662A9ED902AE87D48B8A9667CBC0BD75742CEF313F1649C42C760796C979369736CFF05516A72BA36849D2E213AA497A6E7774EB9A896AF0A13387ADE8F594DEB482D9FB9E4C740360B5089CD101E5450C837C8214247243417C05ACB9D319809D33168E707BD1168709F7829B584053B554D95557DC032683D885AB406E205B6F495C23EB673E05651ED9DCB87C4967987B7B78EB349F4AABD35C7242079FB30607ED82A0503173A1B785CC36A2181892114003E2D33D17AA3B0A5CA47974B8F1C93B35107DA3900A87792433F154C643B34519941CC60710F07A71E4AE09A7539E72B9235A07D5DB53C048493B4AC8B9F0A3696892A0164DCE38B7F8E711506A9220A5C6C61AC2EF3942D3984907E59DC703ADBD93B6B509A156CC34B226188778BD191402DC94124CC7981151934F68733D131540056466437CE2B31C1320362AE5247C35CDA6E13E5D9BB89D0C8DCFB16BE8BC355A0C0C54E7ABAC12621ADB1EAF82B3D1E915315A3B7F3C98F6CC117BC3C850258059737171478E3AAA7FE5D7B5CD218CB428153C07AF80667C096C96656C85B8B0BD1F606A7CEA963717A18AE642E9E8A853D938C66651EE31034CAEC7DA6097A3C35BA420022324EC00CF53B53E9EBADC6FEB57C9B5BF5F53DA\",\n          \"dk\": \"C6DAC8A176AC00983EE672BD5B613267D594ADD9096A5B3FC477B5E5E3AA05D1981313CB7848B1A8CCBC7DFB8134C56FD4EBCFB1CB1069663FF5425829F23ED1501E5C506C4FC8CD2CC36C74A63EF2956F3D71C3A22CCB622282069584D019090148CAD445333881AFACB2CD7FA2C5FED9A081678718D9A16982B5F35869B682620C968E9341760DFB6BA865CEB1873B31021848C348B30982619CA19C268F77312F20FB939778A09020A4D1220EFCD70862975930D5B36D37C361687FBB0186F972881D3CAEBA0686719C8FE2713B28B7C470BB23A0F47095286F9B5BA8F019C8B04623734800C205BFC4702E7DFC70FA32390159565E39A18C19CB1E758FB47BB8905BBAC6E53C92C52822C51D90CC869E0C0354B94C54F181252714C45426F71839146510564AC3CDF3ACFB0213C1F1CDEE523472D043EC8564209B123FAA0D5BF344C31C74ECE10E66B2C169079AD7D7366D404349F01BA6D247A46B57A2264C7093BB4D27514DC5620AA91E3EC407041AB3B8E4585CC870F4B5153BE304C31A9A3EA54EBC071D03D5758F00C6CFE8BE6C6635B45498031072EA0932045763744A1C8BAB8819EA6764805D73D994A20A5C8C4ACD505022205A4405020E1C74B06B89A498D871DBF37E7E46B3439BCEB0809FF1141678D428A2866BB14B00E7296961C7BFA9D932F664BC310B341C9158172876421736CF18CC14B7B3384A35319B8552351A30BC5225AC7DF5DBA28AE696E4B9AC30F426EF7C1272EC4567D21E16D6B5C8E44DEE901AD7C6146271A7C2215F2D3438AF669D572B12E3639888416F9979448A45A6528883C9F41EF0D6263FF04CA17385771C007053A5DA700DE42B05CB0468ECA8840B822D73F5268D192F5FE6B15B87A8CB790902A295BE32A62413A9867239FE08A422322E688BA4306BA98359A8FA726A7A1400756CAFE4DC67F97904496B5FFEF21D5CD1396558B4F7982D77001A87830057F569355C0E7D7870A6696C0EF47B31A0CD97757B94F195AF717CBC205FACFC10A8368F06F747AFCC6922297C3500248616781825A07A03626A371EB78A065DB3A7BD25CE0C722EF4F6747CBB5997EBC98BDCAAC92B12CA4A4647327FBB980F35B8102A105DB7361B82161B89880F1AB008B739775B4726D856C1A8D13024F36CF0B9C1E5828677127F0D82A870CB19572B93215B02281C0ECB96204C40B13653416F3C8CC761931FC452258A65EDB2C0A10560E184052D87CE52D2CB90311578494562307606195E2C484C91D93A3ED11957E3B162F7CBF66832E2C0143AAA80C9D309F14B8FED067C745A74BEA5AAE36CA0F264199B2892A00B430F616C43939F0F0C5E64DBBFE858AD17E5122CD96E5E312F27CB2AD4DAC9F628C28DE55AA957CEE08CA6B8C9C35136702C12233FF42919331C01D53DD4F817F2DB2B587585CD92867B7786212475325C50EB9254D0A2BBCB1B2AE563634224A8DCC167C8910F7405A8BFC0C978877528C71EECD3632E13997B66C242A9A3D69A768355C333D73BCB66AA3664074DE3A644D24B5AC48CD0F13A39AA3996209E9522C499F87777407E24B746C9CBC7C63256D2C5ABA644B2C1267DA61520FBB756724C9D09095803A13F48E11D1B0572F2892B8F81A8BE330B16C7353B5C319A34874F959409E6A41A71585BF36AEB651869AA87D400C6AF32B7F8270BB9D5C53BC3B600F493904AB000149FAFA1B090B544EA0101EE940EFE878DD64739CBD78D317A09A45775B2AB9E0383C75D049083BA64019658B4F44573C1B7C0B3A59495606D9162438B1A0A000C3AA5834AD5BBD11893598052E5B543E209215E41B8D49C4107F5658F7685CE53459F21452AC66F0D2398B6971A5A23A056444947220E95454A404C7213686E58A42E346CC0C44759504B2FA07198D742B1BE01A66C2BC64498CBCF6482DAC99DE7DA703EC1441D08C4A701BF84FC956B11411AD610146C59DA6454A8B3B742D91DE3C09A5317A9DADC23ED0A922AF339C090060B869B55FB3AA1C603C40868337687EBB27E37B804EA834D4D0084B21307DB0A64F69B7B49B5B0D237AC428ABDFACC01D1012A04008CFDD336A06B9728A036B6C60430A8CE887C4B006AA7E7C54834E2934F8400D030AA63E61FEE96C22FD49685A7C3DC2287EC2964CAC78B40340200C3AA6C8A67BDD30FB7605F0205CC0CA043A4574235B342FCE51195873F7FD99077CB31229B91D5923B67D391B457198125B7F198BC7CE58FD6B96BC35CC3D0281957B4110A011262B1C4CC0445722CB5E8C102408593CE6445C4584E373AC890187986A49414643CCB2861742466939C347CF021D1C19D36398A82C64D1FBA4C44B56CF384C04A1944449258700A66B1E2C9185A5E8B17772538B33B038B877353DC251952DBB899F04A6C677247D4CF4E4A5D2B22313A33A6D49237E7C04C61B2B0026531583C6CF6332C4C7853B7A8C6A4A5783D1B79FC5725F9C7157600747C0203E313CE91976CE44CC4E0D67DFA830448999357F076F2D41EE6FBA68C53C9AE5C025DF867E3060F3CF164218142D6C2B5B526BCE6789DBCBC5765846565575AB154C545FC3A81AB8FA1353B703B72B011A0405588FC0B8C20D13ACE9A93F6B28EA1C05CC686CB0AE722662A9ED902AE87D48B8A9667CBC0BD75742CEF313F1649C42C760796C979369736CFF05516A72BA36849D2E213AA497A6E7774EB9A896AF0A13387ADE8F594DEB482D9FB9E4C740360B5089CD101E5450C837C8214247243417C05ACB9D319809D33168E707BD1168709F7829B584053B554D95557DC032683D885AB406E205B6F495C23EB673E05651ED9DCB87C4967987B7B78EB349F4AABD35C7242079FB30607ED82A0503173A1B785CC36A2181892114003E2D33D17AA3B0A5CA47974B8F1C93B35107DA3900A87792433F154C643B34519941CC60710F07A71E4AE09A7539E72B9235A07D5DB53C048493B4AC8B9F0A3696892A0164DCE38B7F8E711506A9220A5C6C61AC2EF3942D3984907E59DC703ADBD93B6B509A156CC34B226188778BD191402DC94124CC7981151934F68733D131540056466437CE2B31C1320362AE5247C35CDA6E13E5D9BB89D0C8DCFB16BE8BC355A0C0C54E7ABAC12621ADB1EAF82B3D1E915315A3B7F3C98F6CC117BC3C850258059737171478E3AAA7FE5D7B5CD218CB428153C07AF80667C096C96656C85B8B0BD1F606A7CEA963717A18AE642E9E8A853D938C66651EE31034CAEC7DA6097A3C35BA420022324EC00CF53B53E9EBADC6FEB57C9B5BF5F53DA963F10A9456E56F427F58784728D6C58B3417E8D6F83A50E16130B751D979C1AA9580773A2674830CD525167DF109974FFD07155CF55615E23916E428C12925C\",\n          \"c\": \"36D0BCFFC4727964FF08A0DD933907E5DEEA4776C814FACD5EB4A45CB3C5FF4D0FA77C7A3E3922863E6403DD9D13A7762A72EABE484109399F14D8855CCF28E66A0E33B8055F815B61CEFCDDDA9B434556C3F696997FD5889EFD254D8340F84D845AF95ADEE7907126ADF6CEF4B4F37A9F6F9CCB2877821A0D7DEE947D13DF4BA7F1FD358D430F49B92962EB32A7B39B20666298A82B5EE52CBADD02D4D4E202330A640663D98CC30759DF86D4F5D8542B940A80C87B3E4AD6EAE0419AB6BC6696AEEBDCE1A8550F95947AD04F47A3C9A4F84E8490FBAB2C7137E8FBD20AFBE7D94FC35558C14149AA277F937AB28BFB33B7084C74A4F860D5738DDF8283248F22DB16F752B55900C8FED3B9C56B94A1BB525CE054504DF3FD651894F0F1EAC0B20D052DD0504C6E2DB7BD742CC3527D55BBBBB184E7E311F72A125B230E7D1364D77CE5B5B104BF1DDA76E054BDE897DE4437DEB871E99C5CC16D537BD40462F0756B796477E7B0FE435202CDCE1A3B5F30C0F757BC0263F3E8B6B4A7275CFB1EA24637752034BB32108BB1627DB0461EE9C521630022E00F7B340B69F95917A8AC0C6D7EF6E37D8E4635E77A8C3FF9E42F452EABA5CADCEA4D686C220A252DF9FE3B25F897C163EF40820DFE0A5CB27A1926CF26D78E307FCB67A9019017BC667116A46BFF210FAE126688765ACC48EA0F8D8D13819A3CBD9DBCCFCFDCBB604EB97E51F7D23C20B8221D34B5053DFAFA549C2D374BB2668CAAC16DBF6A9ED52A86DCCB5B59CA41DAC1661C597896A399CB6ED3FBA6A4C1F5DF6A29E3A65CB6C2F0ECECECE778CD1DBAEE4260D3FDFBCA9AAC3E161FE45B88152B8AF6AD0EAAE9D964E9EA362B5643621B997E35781B1232AEDF4B02543B339F074FE630C5D97AA49BACF3A0D3EA57246005B4497A14165C8BF39AD54B430A30D2EDABF024E0245F4C7B732DD90BF4BA1145C4B88CB81E6408E9D8CFB9CCDC4A9BFC4D9240A21091A11543FBD9DC8BE2F5A02E3650FD273EF529BEC9AAF5C2E30284075326B7CDFAB4FF448CFAFDF21AAA5DB1E0B2D3B181BB7BC9F4665557CEB08DFE29E0CE924F7ABB3D226E9A3EE74BF69EB0F59E58A232E778724770591219F2744E8149028CD61F6AD182E8F42462DA51546882D77F44243C698AC7B61B8FEEEF91EDC73C0B96A40DB2616BECA5A5686D8506090BB3F02075400A151B26682C8D81F05E9E87BF59024EE5AF6B6D015F9A81E19817CE5493571CBF8946C5BE67548ED11971877617B6550F372D5219BC07AD00D659196149250A2A32CD1387B46F793FAE968BCF3F1D4F0BEEDDE6E685C975E176DE2B5CCEC08930D0C7D176A43CB43AD728D131C5B5646C41113904B919AF4E8783EC14CBF11B341CB81CEAD8FF4599E2B58FE97E297C79EDFF394891C5D7318DB5684FDC0AA867F677B6147886AF4F28BCD8E69F3DE22A597DE59909FAEB7D7BD991DFEBE3A40EF9A61D0C725619A83F82088A14E2A1703CF700BD0189D39F031FC2E7E01A3BD8E8A4D6790A2262669A\",\n          \"k\": \"44AB396F38942BB69C09B2602629B53B820FCA6D3D1043B24AFB3184AE9B5565\",\n          \"m\": \"D8EF97421196B1A91448B2BA7E2B4D4B035B91DD85AE4E57E8FE3F0B0D524AE7\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 40,\n          \"deferred\": false,\n          \"ek\": \"AE742E0C4761E7731F98A96F57374EB4E321E1D33E3133C031355AFA974E82F50E7EB36084471D94490150188B34A113E300B2CB65BB44E6940124BFE3E21FBB4B01A7640817567949286922A22A8EF654D8465CA39722AE9A4D0054906F52C79428716971ABA0703C2A3C497DF73D0D0C8AF3BC36D12C80AC2701D8C4689E05645B2017B7EC830F8CC3C3419B1B14AA890B2A8B274C5316185B26473456A196D8C836352B437A4EB0E924E9D5776B528DE1BB433B7C703F302EDE284C9B22A6A502A7FD6B17AB5546B52C86DE883C80F74A9A7302E7270B73C521FCC51EB7E02BCC68CB257A1FD6EA63C195610AB8A8AEF2486E18AA081056B5F474FEBCBAA48560325C06DDBCB79EFA10589A62D9D54268062CC8E3693373066E57C876558FF3180C632269E77A96F305175F3A2F9833BDA9C43BAF62500C202D0DBA91B28CC6473703960785C8711604C9CC04A98BD19904189A6D62A159E6A83F25907EDCF457A080C70627C6F5B0612AE84D4317261BE728B786A111B74478044B7CF192DC6490475C9AE1E5C0FF97B812E0094A63AD818C5BE5304AE8981B52867912787114A14088261CF0747732548EE105927DB086088B89DA4773F3C4CCA0E44431D98A8EAB89C1D6C1D26109EA32CE99473E509B9D478614D26B06387B89D64C6785A50DB4C78FEB65786550647EA30C9029884DD34FC9227596B091E91766F8389A82B01623D7721F2069911128C309602F047F44825EF6A12BEDC72880AA3BB91504904276E3E9583228A7896570676B8DA28723EE17553A156AD5884AF0E1066E518D19951F98079F2244009369157E1915B4FC99254C4691D4A64A0AB0129BB235B00552C5B606220E45683341086F2DB3ACC16C8DB563259C6C8E6A360B9CBA54D048335E455FB406415D147122903325B341727C76ECD2C16D33188055534196C071F6B6806B9E057B5597158E036C3CA90174A83B965F8C3CA7562A3A24AE99298A10791D38FA3E5F30B5CE3A8438E6708A156FD51C055EFB5C65759D76B1BEBC231896095F213148AF839B4DEAA03C9628D0C943B0F14770A9A223FA3CF05C7EA983BB64501885A919678001E9219AEBFA71E5D3CB5BE585E7B939194C87C1EBA5BD56A953645D28C21F0E45686C9AC96F820A3AC26F69F705EC920392601E55FC1ED2B9BDC3C6BA00179F9DC687C2C01C29EC05D8BC5A3145C1F936CB8B20CB2015233E01184AE68BB0F96728C27521118466A5CE602CC4419881DEA62381D7870C14209B6A874FBB498472B3420A3A2A4203938863C7610DF73325B5C12EA5D2CEC78610F7237C4453536D7180EE7425C9DA6C43604DC6A074E2B203A0FC6B44679879AC2D94427746041809D84820379D14168BB0E816FD241598195CA1C447C2EA5D5B455A28E66527088E24163C6DE46D36DC1753CAB7451A6ACF09ABBAA1223E7A3B07E801F84071A7A3ADE760C7A2A12F4D12734530538D2179EA204ADD3A40222467649B4245942E80B50EFAF60B83E4591951101D025B05CC1EEFEA996BF1C015A6CDC9325EACBC349592B881CB3C3DA8C096127953F0B9697561DCDA6BA3D566FF3C14BF718ADFA47877A12E390A6CD0544D9524AFA37069C8ABA24F918FDD15986F9B1C6471A5C7A495588F79B71FCDBD7376406E5DC064\",\n          \"dk\": \"B7649A90D87901527052079DCBF86946E8C05A3558D5B35B185BB6224938B6B5A18F490AD2B439FCEA41F1A7A2DAE9B97245469237422B2422CA8B0E36D9A045E296DCD817CBB29CDA6A94445848C9EBAE5314CA9B063D7C666C9D533D6CC1CC4CB3CCEEF55E5F4A9681125142B77C6EC6CF4C94452E9CCFF601276DAC56AE1020CE7A0E9817501856ACE069566C62766827335E4897F9D47D7C777DE56561654386CD579A1535C569EA4172312E7106AFF19836E671399A172AE853656D206C63A2824D46C6DACB6AC3E21681E324728818CC977134531C329A9982686680E542208C3DCE02579051B37667814C439B45A3ADC9F12D597B42D7372B5E863E96FA8EB8A61EE999A2CC22607A057E6E69516BF8628A041E38C378E0A360CAE6BBB3E7846698C5917A2416BC5B81117CBA8905410C959B4A7241BB96974B29AB976BD2069F41960EDA9778D1EAA1BD7A74F6E40611EB68EA33583C707D6F005CA202ADB84C6009354679604B3D78A21DDC1910DA0FE554A872889CFF801AA496B0B5D498DF245965B783517A2B9F495FFA714FF17B28873A3BD6B5313C8AC70967AC62B90762620818EC1EC1E21080F5427A59C08BF7631F91AF9BF17814A238983526221774C8176F932AC90C240BF48996B9E912A7E6A187831EAF2A9B69616447C26BB598CDF1D52542E6C39BB5BCDC25B457241CE9F2B2E9713639916EEAD5B5E85637C8D527629018925A7F59C539F35371AB49CD49098DAD905296F28B2CA29295641633ABBD29513C886A82F0094715380A6B013B16E578FADBC7089C637BE0C6FC85A68275CDA850981DF357CD40C3DBE21B10376ED5C9AEB6FB3049889431D23C3BB6BB175A24A7233C5983C5D1864F1035C805631FDCCCCF301739BD1A82B4A9CF895439A05629024312BED71D4E93863B22B05B5494BA666D4686971513559E5815A0C7371D50B209C185E3326AEB222AE51C74B72B07F3A7A913E338DD3B94F750406D533703663B6DF204A7A1BCF2D608E3F9A72BACAF83B8393E8456A1D25CEAB511BCB7452DF51CC2EB75D5042E6FB26BD8B518034348A795878CC52906A478F57107A0D8548D9442AA964954B60F91EA1EFFD80C8511A26A7907234ABE3440BD60A595E02813F8BC76C9B83D85D7C0D707378D2B4081C7B0F7FB5F37C4BA0C7AB2670800D698134F4538E6D79224D9ADDF82986B91C856FCBE1B70940A103081D09CEDF1B527D94DAEF6427F968742961C6D910F6CBB3CB2734CB710BA1BF2C02838CDCB34482A452290843B12251FE776C5319A7319F47F22C4AF431996A7602E35841B0120151F427A307590E5321735CC8B2885B9B190A0C6466DDE597E41FC4AAC417882FB23E3866D3B325DCF8B0994434CE75A988B4B30EE9162E56809F638300CB647623A708351555AC4705081C628D48DC7FC7EC0F27A822B1650888CAD4CBD9D9B07DE6B82BBE47A966C331C743F46917104736D3ED15277B75250401B7A030CBB7A82935BB7E885C606A851701776E1B5487640444F33994C325D0DC99B2E06021C753F3C6A4B556A5B4A7705F2C0A32F4315E9AB35267C17002131AB608CDB13C02C50A1B152C4FF0C4D80AB8AAB6C4C8BC508AE742E0C4761E7731F98A96F57374EB4E321E1D33E3133C031355AFA974E82F50E7EB36084471D94490150188B34A113E300B2CB65BB44E6940124BFE3E21FBB4B01A7640817567949286922A22A8EF654D8465CA39722AE9A4D0054906F52C79428716971ABA0703C2A3C497DF73D0D0C8AF3BC36D12C80AC2701D8C4689E05645B2017B7EC830F8CC3C3419B1B14AA890B2A8B274C5316185B26473456A196D8C836352B437A4EB0E924E9D5776B528DE1BB433B7C703F302EDE284C9B22A6A502A7FD6B17AB5546B52C86DE883C80F74A9A7302E7270B73C521FCC51EB7E02BCC68CB257A1FD6EA63C195610AB8A8AEF2486E18AA081056B5F474FEBCBAA48560325C06DDBCB79EFA10589A62D9D54268062CC8E3693373066E57C876558FF3180C632269E77A96F305175F3A2F9833BDA9C43BAF62500C202D0DBA91B28CC6473703960785C8711604C9CC04A98BD19904189A6D62A159E6A83F25907EDCF457A080C70627C6F5B0612AE84D4317261BE728B786A111B74478044B7CF192DC6490475C9AE1E5C0FF97B812E0094A63AD818C5BE5304AE8981B52867912787114A14088261CF0747732548EE105927DB086088B89DA4773F3C4CCA0E44431D98A8EAB89C1D6C1D26109EA32CE99473E509B9D478614D26B06387B89D64C6785A50DB4C78FEB65786550647EA30C9029884DD34FC9227596B091E91766F8389A82B01623D7721F2069911128C309602F047F44825EF6A12BEDC72880AA3BB91504904276E3E9583228A7896570676B8DA28723EE17553A156AD5884AF0E1066E518D19951F98079F2244009369157E1915B4FC99254C4691D4A64A0AB0129BB235B00552C5B606220E45683341086F2DB3ACC16C8DB563259C6C8E6A360B9CBA54D048335E455FB406415D147122903325B341727C76ECD2C16D33188055534196C071F6B6806B9E057B5597158E036C3CA90174A83B965F8C3CA7562A3A24AE99298A10791D38FA3E5F30B5CE3A8438E6708A156FD51C055EFB5C65759D76B1BEBC231896095F213148AF839B4DEAA03C9628D0C943B0F14770A9A223FA3CF05C7EA983BB64501885A919678001E9219AEBFA71E5D3CB5BE585E7B939194C87C1EBA5BD56A953645D28C21F0E45686C9AC96F820A3AC26F69F705EC920392601E55FC1ED2B9BDC3C6BA00179F9DC687C2C01C29EC05D8BC5A3145C1F936CB8B20CB2015233E01184AE68BB0F96728C27521118466A5CE602CC4419881DEA62381D7870C14209B6A874FBB498472B3420A3A2A4203938863C7610DF73325B5C12EA5D2CEC78610F7237C4453536D7180EE7425C9DA6C43604DC6A074E2B203A0FC6B44679879AC2D94427746041809D84820379D14168BB0E816FD241598195CA1C447C2EA5D5B455A28E66527088E24163C6DE46D36DC1753CAB7451A6ACF09ABBAA1223E7A3B07E801F84071A7A3ADE760C7A2A12F4D12734530538D2179EA204ADD3A40222467649B4245942E80B50EFAF60B83E4591951101D025B05CC1EEFEA996BF1C015A6CDC9325EACBC349592B881CB3C3DA8C096127953F0B9697561DCDA6BA3D566FF3C14BF718ADFA47877A12E390A6CD0544D9524AFA37069C8ABA24F918FDD15986F9B1C6471A5C7A495588F79B71FCDBD7376406E5DC0648599741BE85FB959084A514EEF8306CAB0D933C51707EEE4782831FEADF8AFAFD6228B0EA7F3512B3757EC1D5057642AA3ED2265E73179113245683986C8FF04\",\n          \"c\": \"5DB0F63E2A2E640FCD26AD49D437B9287E2C3EEDC2E82A25A49AD859C867929BB10185B6457D64B467A1DE9176B5B4C163FAC1C6C0CF0132BB685F4B99366B8CE702BBFC369CBC1D294B9490E47FBD2335EE6641404D6444E7F7EC3C435F5FBA9134DA5D807B2D41ADF37FF44497826FC31E447C6BF66888893F38CFF04C5B565B036E1F9BF255EECC0F0CDBC625010C8CD211E74164394A87569B08469C2C892ECFB90D5D614C1F503182D9F7C168CEE2A1E9553BDC7E0919B9FAA1C2FC652A13F87394B1B43595CC6218BFE6CB8602F864295D06F0A60119723972BEFE7E73A70F8F9B99ED62FF27317DA4BE60F7CE98BD70A0C138E4E4874CC3D254CE3F93DCAFFB2C8F0E200C64DFF97437878E4FD6BE6F8AC8FF6C5982EAAB879955A699AB6071F0276E6999CFD2FB58329718C64928A18C404539CC1DBDA372098F9686C3CB9D945824424B76469D1DAC5D52D0A172447C19781B43BE9067DB2DC18B6C2CB1A5F9F0C0B4234FB605D4C96396DB5C2E38EDAA114B4C6181BE8AFE6DE1FDCF38EAA4C7EC97FD629F17F2E530CA5CF17DA32F14549F7361E38D64A08180E2FB7AEEFB2CACD993BF39020555596BF421C95C6DB96A0FF59CE65B2F624249EB73D19F34D6F9874CFA8AFD71C9BA1455055077BA7E5E2A9D074B8CD2DFBF1767BAFF41AECD9DDC1239CDB6BBA904A017665B57F2CA765A3C08CE233782A2AB13C66F6B6E55B14D71E1AFDF18F093FCDC1155B7471E12FFAEF7F13C01458544DA9157A898B6756620A8219DB5FDBCCBDC3C4FE21AF3902B2210631E63939D1BBA4B0B674F184000C985D4BFA7C7E3F0FE87850DB0504977693AA2B8D12A35B7619117D5DC8FA5AED0E556909D9533A25A5130F779D3DB7C5250F12DB5E1E51D035BED789BC6EC2D135F9BE244E28563E011535A09AE6EDBFAD5311EA083F0431F9C555F07AB21F0741B7DD4508A99CA4E6A6334C21FCDCD25482E987510FE623822F9033441844F99B4A489491F70C4459BDCA67DEF22C76528A8F0D0FD36B7099A7A34460B5EA1CFAF7ED83F0C7773B062A6EF062383703ECA07612D4960C410C86AAB9AEA0764F060C08B4B20941AA2541E8B8521A8E4B03913A48FBDA62BA060A762EF6199649D7FB6869BEF764B6688A7B77DEABB337BE54452947282E9B001D6B54974180B756219087AA81A8DA6C2E708BB894F099A0C5FD5E858867F083E504983E3FA1F0D9580CB926C7C4DEED4BA51D1E5EC0F8A6102880928BDC7A996F2257AAC86F25313B1D51E7EB784B2CB78102AFA94389E30E5FFA336ED49B1F9FC0D557DD7F9A9C9DB959806C9509160FB9EC4758825C5CC1D49D95CF134CFD1565016899A7C102C4E19220F011DF4D363F3B8EAAA419DBFBDC5CA66FAC7DDAE1F144AACD4209BC01569E0BCFA4FD383EC9AD4CF36474E0C2F8A9335A5E6CC595C781F3AA2EEEEDCF593AC27AAD55FDD46D0224CB3B88C3BBBF6CA6F2E7BB4043FF2B0C88D304B962F108C052E8B7CDB4D2573E47074BF146BC9E94783F811\",\n          \"k\": \"E5DCCE174C4B39536E548CC326893C4C4CF649699CEE746476A827CA567D12CF\",\n          \"m\": \"132E7CDAB9CD5199FF0937C266D50BC50BE764AD027DE45C858E3C2F79B7F07A\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 41,\n          \"deferred\": false,\n          \"ek\": \"11A62A8896CFDF943396BB8F58D9CE2C8A4DD07ABB736CA309614224E13CBC2A4EECE6A9E134B65625BF7332A0B978921F4B9B689736C7A90CD77120FB33124BC272EE9B656952980077BA0A2108AA34355C391118E2ADB2A1A6A61498DA2B3E7A6B3477890A6EB74CA66161979B1C44D10ECA94CC26C93B43E42FC68A93B85AAF0F9381DF309291E2160CA19615031576D1448AE4A36DB5A7EFA99542DA5F633C4A75470F9369AFF4D20FD2D96B62348745A46C81053A17FCA3FB927871E88E996C3555B696CDCB7639E13E1148397F4845364A7CCD245DB28577C678B061555A8F97C7DC4C861201C69106CA2F375E94B597BBB09067F4C730606D2AF3B6FDB98C14CA1A339B3D23174AB19C0849836C33720C9A04821F59B929D72421D7583E9919531327B296BD2325B5CB172FB7E620647091A99A053E20907588950E303237FA68F57B1A305233B6E931F22AB46769C4690420A94C3C234B564DEB3052C4ADB3B08A1F455E2B1B8132E6232E42CE33904146AB6FFB693158AC411DB0C49CA3990498BBBEC8BB06812A31E67761FC26AA861E328906D5B873CD2A8C92778260BC84B50703EAC6BB10432BC0115AF9FB60C8F02D90D6A2CFF80A69FC0E23A3068868A7A522CF96876FD1170BC8552E45EB65656740CF2AB106A553CC61112B659E6862346F64359FC6B58FAA70386B955DE918C5B641F9EAA2BE559A5BEBC40E113BB6B63058A80C1FE1664A81873BE506F8A795D9A8B4318342FEAA2A28C7ABAF1C624D5A8DCA352906D037B883AF54295328180EEB06C910A0B7945A14371C885EAC006E24B6A8F95936763B54C89BEB2379DE061AD5D0A65D8398310A5B8844A81F2C8C77E71F974BB0297ABB1C02442385621D607BD3138E063AC36D728ADE7AA6A90BBA32DC2FEC1498F3D68DA3290C17618E35DBC43262B009357CF6F79B04004316E82076961DE4B47F2E5229A9008D4CE902D9F26E5011A4B044AB0FDC668DBB6813479ECA3B0B18E028B1622A9F6B899420899FC7246CA86C00E7C4E1434365067544D02C44C840EF34CDEA6C3063BB64F18B05EE379A18F125514A6A94A95AAD315A55F0ACC567BBD0705B0AF70643F318A7C70EEE059F52C7A67353C89AB55BB091705F3059C93B7EFB305C7716A86F1363E977827C57713622236A365014B87E030BCCB3696F42E6365201CDDB5778951736BD92A4D78BBF3AC4939D3772C0D58F3F895DFD124AEF4B6C454140C002568AC4298062C1C755B99B5113F0AA2DC742077B3B6BAE714742C04789FA3BC12B83A9248092A0452451A7ACA25504EAB21073744EA7826260AB03C84D22066D2199A72FDBC6F818613FC16E45F00B1EDB2F07ACB5E653731F8B701BF0833A258F12B648CCE74A0DA79DD4CC08B2209042F478E897677871538B90BB0C954312574F90A0B189897AC05C5A2E345D91574C6051766DAC73B7B38A3414714C24B175C302CB095F79DCC430438069541D6E9835EAD22789900C2E56ADC602559D595906C8B5AE01A03572183283533C64064290BEEB0A73B9E2137A50C05C1BC635710C3A4525E9C87054CBBE986B49ECC2706C2AC3AC1A474CCA723208CCB6AA1BB7C5851B390DFA3B0E09B6AE60159D231D59DAD26BF5AD617218FD68D6157E4A276122133E14BA4208\",\n          \"dk\": \"8620482F5A28310A3DA5870035D90D8A47ABCC1A9653E4458EB04D20E6CD97A8CEF6652748156C89DC35AF000653FC4BC94165ACB185154400D53C9BBD746C8A3BB010C9A3CC5C305C32A8EA817B5B743747B63B726937644090ABD7B0AFD73AF359007ED3418FA18E4455928599971E95C0F321B2BA193E9BE79FDC00B70096A05455BD201C36F669C1BB830468D135D4972E0B363E6366555630AF095447009590EC93993E9A862B791B8E0555A8E9634D39145598C5F733C9052934ADFC7F491A787DF06DD4C5B7765A2C26D73A4C9A928E60C81A8595883894317B07ACE63342450FAD15659D4491C1387B66D743B9557F21540A3F8B501DD6BC69D7B9BB056ED5B04D21B8065EA95E83272B42485E22FB1EC394C0E0839F255A75403355B8C60F8FE3859BFB8854E418DA0266BB26C92EBA7F3D37821942B367167893F35B8C4C71BE82540E441DB9B252C7CC2753923A5AA15CA8393803B453BDC5435120965B0C96D7FB54276789A157B84A398142BAA351124EEBA2102969C8CF733B0A4008EA2B1FE7CA9D6A1827A6C51C8128CE97CC96C3188019D596A0CC7CE3705DAF489AC823A4813490B84963504026F4537218F1AE1757638318386AFA447191AEC0F14446D093ABCAB1E897405C67A547D0743E145C1015B9EB064FE743761DD717D42C8801C0B79623A622233066573F7A264D172C44884CAEA0C1A7A7CC5E8874B8D68212CE884943822B2E190C98A6033B9004626203705676801414AAAA2C44D6AD81828F821A85793B903B2A10EACA5AAB455297B8C6D6C4816664B97B99B74E194C3A5465FB8A2F1932B5E42500F67C2799328552EB8DBFB71200C9AE2075032A99B2C25863A045AD44324D31378CC1BB8C7B2673F9E0003431235139A6296A03E3C8A7E102A8DC7224E7D1296B4B2D3B284AFB3346E5332C43F01AF48703FDA319A6691364E36B41B8BA2022120F300ABD2A45D0BA630970BB07104E9B8A35274A6674E534A0880D89F13CCA889E705B188ABAC8F3B62A25228DED82291FB4A06D9483CB49442C02B4CE038498A691FFC98A890C4BC34C7FEE52373ED9A72A3725093A123C2B1DC7105C8A1C435693503DA87374644C1AA4A4A2A6C1A42C411BFCB20D038105137529A91114CCC5ECD5622FF5C8B36522EF5106C68885C19C579A68ACDBFA76A6C4AEF031B5C00A0F2A5B243D2A1C97ABA7B0792F3AA57583C9981ED1392E70CEEF210527AB5C7C1261A6D3501B16974E402FE29AAE78588E85339C6DA0152DA9025C95B5F09A2627A76C23651323096D3459C286DB946EBCBE3C8096A18C290874644B654DFF62CFBBBA909C37917D572E50D90048086FD76B346235858A796627FB0A9B1AC79E90615C47309F87A14275A1D17A11BB2B523FBCAB0CD5C0FD8744FF70453D1AB2AA98BAF93697BA411E07E1BB7174669945B6388844D692B324E4382533B0BF454682B155EA408011BBA2373A4FBBB6BF147CB75CFB213D428BBBF6074D38CEEEE78D3F9C2F230A11930308FDE482261C14C5B1B499321798105231C7375BDA983DD03EAFC8682369198802D0510626760A7CE5CC45F38377E93A685D7369310631711230C618CB83A20A92963D11A62A8896CFDF943396BB8F58D9CE2C8A4DD07ABB736CA309614224E13CBC2A4EECE6A9E134B65625BF7332A0B978921F4B9B689736C7A90CD77120FB33124BC272EE9B656952980077BA0A2108AA34355C391118E2ADB2A1A6A61498DA2B3E7A6B3477890A6EB74CA66161979B1C44D10ECA94CC26C93B43E42FC68A93B85AAF0F9381DF309291E2160CA19615031576D1448AE4A36DB5A7EFA99542DA5F633C4A75470F9369AFF4D20FD2D96B62348745A46C81053A17FCA3FB927871E88E996C3555B696CDCB7639E13E1148397F4845364A7CCD245DB28577C678B061555A8F97C7DC4C861201C69106CA2F375E94B597BBB09067F4C730606D2AF3B6FDB98C14CA1A339B3D23174AB19C0849836C33720C9A04821F59B929D72421D7583E9919531327B296BD2325B5CB172FB7E620647091A99A053E20907588950E303237FA68F57B1A305233B6E931F22AB46769C4690420A94C3C234B564DEB3052C4ADB3B08A1F455E2B1B8132E6232E42CE33904146AB6FFB693158AC411DB0C49CA3990498BBBEC8BB06812A31E67761FC26AA861E328906D5B873CD2A8C92778260BC84B50703EAC6BB10432BC0115AF9FB60C8F02D90D6A2CFF80A69FC0E23A3068868A7A522CF96876FD1170BC8552E45EB65656740CF2AB106A553CC61112B659E6862346F64359FC6B58FAA70386B955DE918C5B641F9EAA2BE559A5BEBC40E113BB6B63058A80C1FE1664A81873BE506F8A795D9A8B4318342FEAA2A28C7ABAF1C624D5A8DCA352906D037B883AF54295328180EEB06C910A0B7945A14371C885EAC006E24B6A8F95936763B54C89BEB2379DE061AD5D0A65D8398310A5B8844A81F2C8C77E71F974BB0297ABB1C02442385621D607BD3138E063AC36D728ADE7AA6A90BBA32DC2FEC1498F3D68DA3290C17618E35DBC43262B009357CF6F79B04004316E82076961DE4B47F2E5229A9008D4CE902D9F26E5011A4B044AB0FDC668DBB6813479ECA3B0B18E028B1622A9F6B899420899FC7246CA86C00E7C4E1434365067544D02C44C840EF34CDEA6C3063BB64F18B05EE379A18F125514A6A94A95AAD315A55F0ACC567BBD0705B0AF70643F318A7C70EEE059F52C7A67353C89AB55BB091705F3059C93B7EFB305C7716A86F1363E977827C57713622236A365014B87E030BCCB3696F42E6365201CDDB5778951736BD92A4D78BBF3AC4939D3772C0D58F3F895DFD124AEF4B6C454140C002568AC4298062C1C755B99B5113F0AA2DC742077B3B6BAE714742C04789FA3BC12B83A9248092A0452451A7ACA25504EAB21073744EA7826260AB03C84D22066D2199A72FDBC6F818613FC16E45F00B1EDB2F07ACB5E653731F8B701BF0833A258F12B648CCE74A0DA79DD4CC08B2209042F478E897677871538B90BB0C954312574F90A0B189897AC05C5A2E345D91574C6051766DAC73B7B38A3414714C24B175C302CB095F79DCC430438069541D6E9835EAD22789900C2E56ADC602559D595906C8B5AE01A03572183283533C64064290BEEB0A73B9E2137A50C05C1BC635710C3A4525E9C87054CBBE986B49ECC2706C2AC3AC1A474CCA723208CCB6AA1BB7C5851B390DFA3B0E09B6AE60159D231D59DAD26BF5AD617218FD68D6157E4A276122133E14BA42086B6697FD26E70625FE8F9F9519FD2E06C00167545ABD566773BE01A874722DBDF9212A246D98C21B61EEFDBFD8BABEB04F75EFF4D8BD5EC606AFF11F6C20F33A\",\n          \"c\": \"199604618D182236DCCB6F33D02C92C0B22A122FE24D08AF0E4360D48F2626A6D3FC9CBB712608997F1C3806DBDD2CA5CFA9223B3D9BEC3892B7680E6B6ACA245D024D349B1162610D87B3E80B45496B2C8A930666F52A94665169B3D1242540DB797E4E2CCBA389240BCFBA2FD6E0FD3C7C01D908EA99AEB014362E2B20C2793D58E7D6B7B6DFE4FC06D2962AA2BBC7DCCAAA9FCA60493E90BBA138C7D2F300758A8A04446F277E9681B29FD6F572923B4A1D0D3A8E0E26C952D348607E39F69A4FA6F4AB7D9A4B9715A8CA8926C33442DD72BDAD6E1E72817973C6E1C8D5518558A3727678F89D141E3959F572EE533D9AB12633A65E032CF1F434A73E7FA552FD45D2B43BA5152A3172AD58FDB6E2D005F9D8A836AEC708B28972C231D4BD69E77A18FED0BACA032A5D11FEC1266F532FDAEAF3C2F231D07332B274E07226B4D35B30611FB4B3E2F49343B6DFA74CF312FF82AAEC402A1995F7CC1866DC78AC4B7B05EF8500C5C8AD5285E035CF44A6E7CCBD7B11068425D60A66C6D8F5D24415D5C8146C47835B5FBE4882F5AA33164C8BC348BCC3F57DE43AF5EC5400B37CD32913DECB132379278C1B672B006CC0F09510A4A4D1FC139263E2A163AC39FE625139DD75553A8922005E6BE716CE6E5376DFBED8CB7CB4D5F52C2948C033BE9960AD3B910AC5DA388B14E19355C6A0829E5A804E65AE885062BB8B3527DB4FF50BA41A5CE45263720B4A7877D838D1C29EEBCC7C5CF9F5212088D99DE9AE7BE18C74EC7D677BBAEA6BD25F57D0233FB167C5BA0D20F4D32EF6DBA12C6F7AF794247EF35C32D9E4C989B43935BAC97071D3D966C37EF0028E7487077F71A0380E3F32FDEE8250972CCAE6857483C01BB4CD2B177F4F9504A636C307670AEC32510312BEB2EFFE0639A68218FEA75E0E4B6E654DECAA6F57656017E77217E6FFD095A7522F0DBA295986A3998ACF71BDA96016552DF74E6991060098A20585E99E33F7D6200B6EF22958B2207D029566EC96B02CF2AEED17F66C426C5DF559A98E5966918FD0ACFA54B6E6EE70BF3042100047FD481E5E3B73288BF03877C7B2F3A1D206C76B3426F37362388ADE66C05461441D944D99FC1EC3E67511C1B9B9919117A09680A7CF99D367EAB6E493790942C4394875B86B464D1DC98F5025B9F71D27D146C7AD9EFBFFF979552AF5DD127C2231E4452513C106E8D7EC6DD9CBD4BD706AC1F022DCDCD235CC45EF6804FA2D98D371D713178FDA88C5DD6DF98E1B3573D6092DE3C022E49D8E51EF237947AEEC5EEAB775107D714E3C4DB7DC2E408EA0F3CA80AFCA83288CD1E147930FCC727C8E944A1E013919C92109E9A764ABA96A090D31C83C93F8735ACD66A087193CE7664757D9B8868F03F7DBC3FC44C1049826B4876FA2FE06B16091E2E78335FA55A839E719FA5BBDC46B659C3BCFD91122F775DF9A9E8D0C43FF9CC2BDAB401B64E5CC35FB17681512C03C14C3B1EDB2A84D8998829A12E1C7FA590B8CE15CC8AFB37253FF5BB78E0B0EDA76AA\",\n          \"k\": \"66D5307AE26DCE8CFFFBBA9BC0B2C66E38B6E77537AE525B3E9A18BADBD72FE1\",\n          \"m\": \"E15BD4603F0EB64E32B3F1D1FA8EF6CC25D673A1D0BB659CEFBA2C153724C1E1\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 42,\n          \"deferred\": false,\n          \"ek\": \"E65223F4AB1C53267449A616AA32393E441F346887329AC4B9D006F61A25BCC3779C740EED0BBDEF0A371B95171C70C60F2387265469A8E7C612353FC51968AA43C04D1685EB27364E26065EDA53D2879D36552E51EA01BD818DD57319DFB127F2A578CD48BB6080C5AFE13ABA7808B5894893BB414A532C5223B91EB1CD2392333190054DFB4722F5B548E570FA19C7C485790AC4913892249E63BB324C620D835E5B8C922924791B08937F139CCBEB2D5C9A72F344888BA37B8CD7B06349A310BB71B06A3EACC1236AB03A5FF9B923FC5FADE54C5A1A2E803424CF9C42E5043305127F3EFC956D97946E6BB02EB913D6D21695A0A88708B6CC5611FEC61932774E462C18F4726FE992A301B74DE2F56E9F791E90992C3C109860474FBD6C5EB81CBFEC4A0C7F154E5C2375A2A379A83B4DA4C6796102B20C553F015AC5AC49785138B2460925BA3B156EA337D31214EB942EAA27062EC07328080927DCCA3CFAC51A243645D5055EE0C644428C4715032DB2757AF2AE25D727C78B08D7B59D8841B9D26139C3717FFED92E09FC5286BC1E1D290366E183FD2B5562859FC9C321B73132635A532DC3541096657225082CB37DDED92C8C480B5CA68F93AB53168B1FF2F08D8265AA8035753C96897DD06793A145EFDBBBC4518E0C87AA90A895E6955E5B62929999143CB16B09E26433A1C0D761051AC3776EB126A8C890940632B4C50DA7FC181439A11B483554E208D6FAA627B11DC6BB5073DA156D2A4A08832D2590873672066B9A35C65515B4CB5602753B26C9C865716A6BC3C059272F0DF54DB6395F59012ACF2A783BBB03CBBC3A95F65E04A0BA76149A8DC4A305901418324885D44780F65E17E4C6AAD08355A59C21558B0D206C73F59B5FE49F8C5B86A6747DD9E8CB9D560B22B51B5A6AA82277B676F04399570402818A97881EAE8A245E1B75FA1AC50BB0A952E319E523A292316DE3E5C4AA271265B79D37795AA3596CAAB1072F3775D06502880359D73C5EE0C782ECD04ABCB1255B8A5051CA51EC6C17EC955BB6E715659B4CCC4C74484313C4F5149A288178C49F99586AF54710BA549C09CAC0CDC76D15183C8748146A07C72049108E86C0447B371B844C3715A914E3853F4184337381320A02F94BA1D9EBBDB0033D377254E4EA51AF455A775CB8E85336A761864239426A449FFCB967B82461F6F5A21D7A36DB8B54A5AA3AEB2297C30463064C8AFA12908D4B2D1E5B0F1A23B1D55A5E96886B96C39E10C6A68008B0527703EA2799F42B5098C4178F47490983793DF4CE2B0A2D2B56719256206493C395C54292E516D4F8A5C906736C89810BE0846B8BA654C86A355B56736B9144853286D65E61413F7DD26D1D2ACF6728BF6B028F912602CD132E3D6603DCBAB4CF65B0E961A8B597A0810B9F78A14F0DFAA108811CF22C394F21BFC56155A67B85722C755351576B0AB614B7C9C2278DBAE211E33839B55383C5744E6270C92D65BA82590AC613BF1493768E7ACE60A14644C9A02FB30BB1A6B42A6B69A142BF670C48EE03077BA7A2964020D528C51CB17B0245AF11C4A8EB0785ACC738E4F0CF0FFB838748314CA51C81331DE2596345B44F44BC516EF123CD8997E1FBC93AD95C0A9CB71D46C5535A99B75122E5E710DA961BD873E66DDD\",\n          \"dk\": \"F755A8CDD185E9791ABB61B4028A435B336475DC9D3B20B5FB7C703BE6096C23C2AB95BB71127264E29EF9349001C70D26548797D2C3B6C23D841C1063513738F398C281786C908AD623B22CE690838579A7A08ECB5419366A65CAB11B7B598A38D1A878535819C21E08E1B25EAA50D5740C80D90877B921B522831ABC6EA9F7C5753292CDB21D47402F0E95179D0C3247E16C66801F9B057EEDE48070F456FE58906ECA7EA91ACD9B537AABE8A105B5B0AAE634A1E59E1A456B49215E9C792A0453ABACE0539D14C661ABA1033B7097945BF7D0AA35E9B2E98C9C48232C041BBB32A928AF58613898010DC0892591781343369EA7CC68D93ED41808B53AA1E7C69370281653109BF900A783A231E0E78FB9ECC75C5262D99A5401D974B6069E6ACB7DCDFA2B9E61A7D87117576C1076A497E096382775C47B2C20AEAC91FAFB5709670277B20E88D8B8174A44B0B565D669B108A4B61A22CC53262A79948A3779C4DEF87E8AE7222EA1061C76B052B3B3A07BAB2BC52104D7B093481B6E7A9AEF4530502126E718153F259C6D9142EA7ABD97D9037799368349CFCB4C6AFB541201856B6F8CBC4A6219FC963BA4951AA266402749C4F066B5A1703CAE32831901B6E51215F7F0A5370A6D1691284EC80126710224D42B7671A207C06822EC5420C186D253339D9A058DC552B6F8C38230015605883E777158413F915C3284E6C8203B21F502830B51193B39230A963BA38A3C2F0B122559847972923B67AAABE0A4450BBCE87A1F180A9EFDD8A63CD3ABA8274BED24C4E464A9AB1B893F9C08B6D4CF2F93107CB1148C5719DC81358C07C14D41883439C7D0E5C757815140CC79E1AB252281A4D190590AB36C0945C03EA3369CA244E3ACAAAE9C4BE8A77C9458A28774B3C1E794B459CA0EFA190FD681DB8BAC0A771D40651516073D91DCB763CC2761CBA360EA5B8BA402A2110E3E92522DC9A9C3737FC7D7012C33B928184AE22BA23B5854953648D4E9AF49DC3456EB3A38569F27915ED388AC17D1C71193CBCB4AAB74726D4A324294A47ED72C86D84064EF707A4AD7BE50948D82B671C49A19AA338948947993F6A819A669D9C70F910B6D86325BF3046E6FE3913745ACAE5A0EB1A62B203CC46774CF210B2B19C9C63E1A0E8EA9428FC20E2ADA4BF87A871F7C12125AAA8CF38550AA09EF4B82CDDA83121363345327C4FA9FB8C61877216441610F3368752B16121AD7746B405303E83E495CB8C19C3E32F039240392CA8C4E9EA1BAE0136463D032A3139231E61C9947C987980124B56D19C478A060C3A40AC9AFC311BD247818B499AA3811A2584303A58DD5F82355D963F3C27EA57556A5CC5A369619AA7267CCC49C6138ACAF63BE55E81EAB119C923B1163795ED4901F51F1C163FAC7A3F8C080C40E8F0895CD774B0654B137CB72E244B0B7FB58C8F133A9974E8AB74226F7BE847B5205E80E479633EA574B16005B47512E28F3AEAFB0B0775A1A30664888C45913301832F6C80737964EE58D79103F3BD12B1B704F6E8A770B012A0AAA003990BBA4E092A666C6FB1BABF941B94A6089C775479790A996C37B5E44923384874FC103690763B892702E6832CC3A3F69157CE65223F4AB1C53267449A616AA32393E441F346887329AC4B9D006F61A25BCC3779C740EED0BBDEF0A371B95171C70C60F2387265469A8E7C612353FC51968AA43C04D1685EB27364E26065EDA53D2879D36552E51EA01BD818DD57319DFB127F2A578CD48BB6080C5AFE13ABA7808B5894893BB414A532C5223B91EB1CD2392333190054DFB4722F5B548E570FA19C7C485790AC4913892249E63BB324C620D835E5B8C922924791B08937F139CCBEB2D5C9A72F344888BA37B8CD7B06349A310BB71B06A3EACC1236AB03A5FF9B923FC5FADE54C5A1A2E803424CF9C42E5043305127F3EFC956D97946E6BB02EB913D6D21695A0A88708B6CC5611FEC61932774E462C18F4726FE992A301B74DE2F56E9F791E90992C3C109860474FBD6C5EB81CBFEC4A0C7F154E5C2375A2A379A83B4DA4C6796102B20C553F015AC5AC49785138B2460925BA3B156EA337D31214EB942EAA27062EC07328080927DCCA3CFAC51A243645D5055EE0C644428C4715032DB2757AF2AE25D727C78B08D7B59D8841B9D26139C3717FFED92E09FC5286BC1E1D290366E183FD2B5562859FC9C321B73132635A532DC3541096657225082CB37DDED92C8C480B5CA68F93AB53168B1FF2F08D8265AA8035753C96897DD06793A145EFDBBBC4518E0C87AA90A895E6955E5B62929999143CB16B09E26433A1C0D761051AC3776EB126A8C890940632B4C50DA7FC181439A11B483554E208D6FAA627B11DC6BB5073DA156D2A4A08832D2590873672066B9A35C65515B4CB5602753B26C9C865716A6BC3C059272F0DF54DB6395F59012ACF2A783BBB03CBBC3A95F65E04A0BA76149A8DC4A305901418324885D44780F65E17E4C6AAD08355A59C21558B0D206C73F59B5FE49F8C5B86A6747DD9E8CB9D560B22B51B5A6AA82277B676F04399570402818A97881EAE8A245E1B75FA1AC50BB0A952E319E523A292316DE3E5C4AA271265B79D37795AA3596CAAB1072F3775D06502880359D73C5EE0C782ECD04ABCB1255B8A5051CA51EC6C17EC955BB6E715659B4CCC4C74484313C4F5149A288178C49F99586AF54710BA549C09CAC0CDC76D15183C8748146A07C72049108E86C0447B371B844C3715A914E3853F4184337381320A02F94BA1D9EBBDB0033D377254E4EA51AF455A775CB8E85336A761864239426A449FFCB967B82461F6F5A21D7A36DB8B54A5AA3AEB2297C30463064C8AFA12908D4B2D1E5B0F1A23B1D55A5E96886B96C39E10C6A68008B0527703EA2799F42B5098C4178F47490983793DF4CE2B0A2D2B56719256206493C395C54292E516D4F8A5C906736C89810BE0846B8BA654C86A355B56736B9144853286D65E61413F7DD26D1D2ACF6728BF6B028F912602CD132E3D6603DCBAB4CF65B0E961A8B597A0810B9F78A14F0DFAA108811CF22C394F21BFC56155A67B85722C755351576B0AB614B7C9C2278DBAE211E33839B55383C5744E6270C92D65BA82590AC613BF1493768E7ACE60A14644C9A02FB30BB1A6B42A6B69A142BF670C48EE03077BA7A2964020D528C51CB17B0245AF11C4A8EB0785ACC738E4F0CF0FFB838748314CA51C81331DE2596345B44F44BC516EF123CD8997E1FBC93AD95C0A9CB71D46C5535A99B75122E5E710DA961BD873E66DDDBB5310F78E18E35EBA80A0D5382866560D6E502D58908A97DB5FED7E935FD7178E82546B2BD2675908B124B41D52CB487BD98DF8D7BFF3AC859F4C685F91001E\",\n          \"c\": \"50F0E77866A24C305F778B6D08E9C33CB04C31B98D93A1637D5B2E9BB2443337333E91626DEC0C1C59EF1DBD000BDC394FDB2B6B471885CE4A2777A3E0094122A6DA08400897A635CBE0AF1D56654F4D56CCA59BE3B36B20666036DB3C02BD6E6E605ABC070525756A9CD08767335072D99A067D816DD3438E0F4EA4BA1150A7806E41BBC7521C70B66FD46496354CB47500C28695034480CE91E1D63937EFD2FA4F92FE0B95CB65EA70F610B1B4C9519DFC8C175410BAB2E337F07B3FEF8A8CFBFD45B2827A771EE5D6C2E6A3DED56D21E15563490F763B5FA668F5F4E982823A762AD38B78B573B72C9624798FD092A11EE9A010A31A60F8EBD647A5B11E437FF4502863F78EB2104A270692A541E15AB5731F13D11C20391716D60A912DE796D83387C152E9C171F86CA1EA439B369DFED63C68FBCC103F72E3A9D4C01558FEAC56C2DA7BD797A48257FD342870523C56102CB413069C833BF51AAA31403BE6C2CDB64B2C7A7F6CC9F848139320B974C165717D6A0A891CBDB27EACF96F23FB67C1B218360CB68B158981D32FAB2C12AB14890B0230673A29678EDCBCFC6D2857D2F78EA375962F459213F377E7CAE4DD79393D3C2ECC44FB64091D46F30926FF7E8C5D07E70B1407636300382633E2093FF708F541544FDC5F642D92B78CA167D2A73A871AB4C4D611AF2804B3BAD14CD64AD5BFE9C1DC4E142192A90ABCE2C5A8F6BA6D7D30A19E5AD8F9FAF34AB516A02511EA8D88B1A96DE9F6DA9616550753AFC151E7064E4024C661667807728CF85350B32DDAF7606E4BD491FA61FE05070311DEECE16F30F796E0B6BA179A4D7C8B3756BFF942AD2E412299C911EAECC18A3AC3C68EC76D82195615DBAB4C492AD1E2C7F210923B2E54056E557F78422A90FD647520B32626D017BC661E28CD7FF81492EDC2AB88D10F26B576BC6C8F7A79C5DB1279204859A3C82C06A391214F6331973A0332B27A339540D00AE047FE2C4296352190C2DA7C3B2849AB1A36E7EC74FD641A46D2EC61B3C0D394FB3218C6A0C7FC8B4ECB47E026929E15B0D1CA2513C17564E48543793466D72EC2F5355D43DAC60EC6E55FDCCB27A403148386DBF7BAEEE056AF976B1B4D06111701A3AFDF289A0E3969942CEC96E33C095DAE1334DDA76C54FB67A36735C96AE5DD53D379F5B05A51684D997593B7B25B2EEA0001834A9737AD050F6A7CE679D27D852C0DBF0A6644185FCBE7BE97200C586002206341CE063B042CFF9289CAF378EF0A4F93CD6B0736E8A0EA91A1D4FBB6FF30B255E9B604BB8A5F06D2C7C0607876568465352E76C5A31A44B54B8BBB7F0B016D7ADD77B23C443D3C8B1A7E67C05C80A77E2D9BD0662A37A4EC501482FCE4CEA342BE00321E2968107D0B265F168E6560C427B3ACEF9C14478A80DE557AC0A41025E32D8741D638CC8F76E35AF1AA32728E5C5592122973D1947538B8793420FE6A1FB6877ADFE5677F677761F5071AF5D6A70D706AFC275B7E551623E97EF793B814239DC1E3C53D23C3CE\",\n          \"k\": \"BE4A7B739BBBFEA62A02A555571465EDCFDECEEC83846760A0D39944F99266E0\",\n          \"m\": \"D176C0836015362D1DEFFC1901127B5C41C14AA518BFEE6C62F2EAEA1F226AB5\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 43,\n          \"deferred\": false,\n          \"ek\": \"6453B4019A8E77C7607ACC6DF337097A9A10EA4836DBFC7C796A4E73A236069C2005C2BA2D5843AE7A008A337876182C44B92E66EB5146826B246CC7C3F44D40B0C88A98C11B873A29D7A4C7336512E00413EB519AA39025AC463C7CC289270A680B88A7FB9CFF592FD65626E831A019EA9E4C9BB57BD34B8D28B8855616FCF15FD6138947CA36D863510E6B3D4B22B8F812249913143F7CADBF15AF991C7E36C1A610879D9240398A1A1224837F904C65F0C1527BCCCE8459B594739F4BC243D2360CAC320AFEB5B2F5A310C90B42D3EC369F64C0FD2118D0359DBC3141F602CA8D7507EF830F1E47682068ABF22593563A071653ACAFF19506B5630F2B6401B78953B152C7A78D33216695B7778F9467FA239E4ED089A1E6BCDB682C4EC1B7CF407B37B0882777B12436063AC0C47CB7A58BB7CF34C5CDEC291BF1EB851DDCBE42535E0639580B191CDBA9AB22F0243D1334048964968941E7140C44A1658FE20C8AA023D2CB133D5B06FFD99FFD4751EBEA170F9C30BAD8CA39C64C7FF357F0FA2585BB13645682DAE24518517545175E38D5276FC442FB2143FFD2A7B61903CD72583FC64DDFA654081A4533457D36A366BA538B6772C5FF11674F0A702340C542357B80B2C206BBAF2CF6406F339436A7A1050792AA674C2E9C9B7986404C94CEEFC343E0781155D8B737978BFAE3C8CA64723D0656E9B89D23568AC6658F0732C332096A217CC6FA3A09FEB16552F5C982CA6B540C3AC96C8D0AD576820CC46DD60EC82BA0CFC14073424902578AFEBB44C321A4A6B7AF9709B1D9F05EC9BB6B928C936B670543E1B05E649FA7C2B3B9FB441C976FDCEB889FF63019F26EB2326F36F82231A68F230C9BF384710E11CCA858970A33445FF05CC4399DC28B285857168DD388F0B95B7E3B1699830950022FBA81984AA1B1540959EC1469DC696D7BB9A3C42153540B24A350CFCCBC33E5D9136824500319ADEE626BEE22A53FF8CCE2FB195D38257DDB8C305949D720342EB58AF72236F2275F98D335DE35A0C0C66BA752BEAAC32D113AA43DA092C4888D1750A59BEC4EB14A94A84C9947660891457B52F1BC3EF33C951A1DE64ACAEEC99F7795B8C94A0320F40D0ED92EB7FA346E042453995F2394B714A372E6B678C0BB4E04670B347C156794672FCA87966B26F0EBBD2B8304C9B6560459019ED95539C7C0C20999971521F8F7537B13937AEAB1E23736186C672AF0B084D4C5C3E1887BDC0BB281B7DB924BFDD9034C6456C427B9643C89FEBA3440947DD6B0B4BDD54C38215371E1A74E543A6073CB46118DE3B2BD1154AC536652BD2666F5F710C03CB6ACE948FB61692FA3A3E2EC4E3AD971884412640342BBB4C09D1C11856825D79A8E5249243062033E379DD1EA6526676A2492520BC55A4533CEC47C54CDAC7483500C0751BA84F0CBB020CFAD07B6F4E893CAE78DA0C7082F7A9C78D2BAA064C33D20644431BEDCB9B493D05C13104B88870361433490927B015261B63B1ABF2B59777359F74BC2597238DE6729669383E8188C0DFA7235D935657B936101BA4DA9CB90F1AED0F06C03700574B99159FC3E59EB3258A9BD5F47439B45A30163309A6A29CDBBC017840C5655E3E8FAFDBCA14293B91C07EEAB7E6C066A6B8BA7EC5FAEC0350B9C887B18\",\n          \"dk\": \"A996A675909C3F1B1B676B54D0F29769F24B86FA482FD3127086C82E336C9D606B7CDA5F5002C6E27AC69DC26DA9D90BF31561C798CFB9B78B37F788CAE520116A4FAA4826765A41A16C650A1C33CFA91311682ACF0144825370D76657E7B18B87233A0B1393BA92022AA22D28D04B4A302380D780854519C014862545467626B8F7C0228E19960C528D4A511259C7B8C92A5C4E7060A76B8182A0A0ED5B6AF6F920FED0B1DB4C76A4149910B755D69A43217B942D796B9AC549ED9A464799C25AC0B1EE229F14FB485A795A2E072684CC33C8F20B49862157351A0BCB37932B7C4708374B7481C2A5C19DC752164235823935B96A9AABDBC4AE88C4D87131D3131467A8C969671503CA85B65B229491A33B4789C41722D9F10F97F335C2BB86B7945E0A489AA7171C13817D04C4534D324ABCF0CC78957A82214DAC93667E92CAC094003C4440C560CCA698A2287BB0A19CB0352C951D238D13BB89F9AC3AB184A2664423281681E8813D6B433A8CA7BE7B3C99368A1DADBB984FF298FBBB2645C11D6576131DDA12586C6DE2539641FA21D4870D471C3702A4C3376355C1EB42A90B5676756905C2173BCA5775192069382B3AA4CBEF7C95CAB35E44C49981D6ADC8C68E9D41109F5C18BBD80D13E80B661371B0CA6B48D19886E610FEB6AB4CB500EE0C12A7C770EA6B16F80C4D2EC016AE26B02A2520B5692437C80A857A09A6659218D147923375203AB1F2799462F28427C01900B08A8307B5F3BCBF833104A04C7B4444BD069BAEFAC4A271537702514703FC2388777CB7735AAB3684E03741F1B45261CB53ABC800E9F8AA77B62C7A805B9FD8895B568FA1136CE80A6E47D470B26A0ADD4A69BB33ABC8A6A49D61525866256D2656E46827BE895162A01CADBACBEE418695089BD9F1940408483FE87C31A68FEB4C35AA561827BB49E97776C64036379B16E719AD10F02BEC795760BC167FEC3F7B571D88075B161862B6C556B8C87B3636398D32426B1A11350203B93112531BC8AA0ABD23454C9D863E1E150990918A96B2B3A3A1416B0A8C2968AE5A110DCA445F515A15373026138865F5C64F625151FC546F9DC1B8A2F17C2E719BC97659CD255D34A3A6E28B8BAAB1C47EFBA49CB4B1220142EE843E24402462E785B01B664854BC54D6205312B6B70117C5C1C32D91C7B8032CD46A83B327609AD91519C9AA203A0A0B9C36B3671AF62890F301CB6BD4A3D39175E79A572253331144144BB2133F924A3E88142FE04BFBFC15EC84BF968A2E9A2459B109144E0445097349EBF3AD8E0C87641C74FA71699B687AB505B43CF0A668C7B5C4BB1A4D676081D8C1EE7B8A881791B9D16F92116C5D786B1678AFD49A761D9AAA33754E919972DAF27CEBE3CEFD3C90072119A78387FB056D98D41CEA45648CB5B87D217DE730703CCA26C7726F14CB9FCA3760646A627B255EF9AC156B6C1721445E20417CEFB3085394A3D94AAFD1273E4B75A5C340274160A52F26C376A018E0D880A578A24A8961D9D443A5908A32859F2A9794F80C62DDB001C411965802C6A0F10D572171FA32A44BD56ECAE78B7FD34492E28E61C47C8CC66686B401EC082013CA8CBA84545596A9D781786453B4019A8E77C7607ACC6DF337097A9A10EA4836DBFC7C796A4E73A236069C2005C2BA2D5843AE7A008A337876182C44B92E66EB5146826B246CC7C3F44D40B0C88A98C11B873A29D7A4C7336512E00413EB519AA39025AC463C7CC289270A680B88A7FB9CFF592FD65626E831A019EA9E4C9BB57BD34B8D28B8855616FCF15FD6138947CA36D863510E6B3D4B22B8F812249913143F7CADBF15AF991C7E36C1A610879D9240398A1A1224837F904C65F0C1527BCCCE8459B594739F4BC243D2360CAC320AFEB5B2F5A310C90B42D3EC369F64C0FD2118D0359DBC3141F602CA8D7507EF830F1E47682068ABF22593563A071653ACAFF19506B5630F2B6401B78953B152C7A78D33216695B7778F9467FA239E4ED089A1E6BCDB682C4EC1B7CF407B37B0882777B12436063AC0C47CB7A58BB7CF34C5CDEC291BF1EB851DDCBE42535E0639580B191CDBA9AB22F0243D1334048964968941E7140C44A1658FE20C8AA023D2CB133D5B06FFD99FFD4751EBEA170F9C30BAD8CA39C64C7FF357F0FA2585BB13645682DAE24518517545175E38D5276FC442FB2143FFD2A7B61903CD72583FC64DDFA654081A4533457D36A366BA538B6772C5FF11674F0A702340C542357B80B2C206BBAF2CF6406F339436A7A1050792AA674C2E9C9B7986404C94CEEFC343E0781155D8B737978BFAE3C8CA64723D0656E9B89D23568AC6658F0732C332096A217CC6FA3A09FEB16552F5C982CA6B540C3AC96C8D0AD576820CC46DD60EC82BA0CFC14073424902578AFEBB44C321A4A6B7AF9709B1D9F05EC9BB6B928C936B670543E1B05E649FA7C2B3B9FB441C976FDCEB889FF63019F26EB2326F36F82231A68F230C9BF384710E11CCA858970A33445FF05CC4399DC28B285857168DD388F0B95B7E3B1699830950022FBA81984AA1B1540959EC1469DC696D7BB9A3C42153540B24A350CFCCBC33E5D9136824500319ADEE626BEE22A53FF8CCE2FB195D38257DDB8C305949D720342EB58AF72236F2275F98D335DE35A0C0C66BA752BEAAC32D113AA43DA092C4888D1750A59BEC4EB14A94A84C9947660891457B52F1BC3EF33C951A1DE64ACAEEC99F7795B8C94A0320F40D0ED92EB7FA346E042453995F2394B714A372E6B678C0BB4E04670B347C156794672FCA87966B26F0EBBD2B8304C9B6560459019ED95539C7C0C20999971521F8F7537B13937AEAB1E23736186C672AF0B084D4C5C3E1887BDC0BB281B7DB924BFDD9034C6456C427B9643C89FEBA3440947DD6B0B4BDD54C38215371E1A74E543A6073CB46118DE3B2BD1154AC536652BD2666F5F710C03CB6ACE948FB61692FA3A3E2EC4E3AD971884412640342BBB4C09D1C11856825D79A8E5249243062033E379DD1EA6526676A2492520BC55A4533CEC47C54CDAC7483500C0751BA84F0CBB020CFAD07B6F4E893CAE78DA0C7082F7A9C78D2BAA064C33D20644431BEDCB9B493D05C13104B88870361433490927B015261B63B1ABF2B59777359F74BC2597238DE6729669383E8188C0DFA7235D935657B936101BA4DA9CB90F1AED0F06C03700574B99159FC3E59EB3258A9BD5F47439B45A30163309A6A29CDBBC017840C5655E3E8FAFDBCA14293B91C07EEAB7E6C066A6B8BA7EC5FAEC0350B9C887B1835EF87CAD46B39215CEC187D9B96A895FC9A8EC843B7CD3531BBCDF1BD64A22D5001876DCE843B5761DA9110759CDD04CE17B8936541525FE830CA69E53E1655\",\n          \"c\": \"ABF144FD5F6B1CC8E11E1405A8AEC1039BE0D84CB1E4EE622E50F721F9C76C74AF363507C354BFAE546030A8BF746315F1B223F2092AB783CFA62CF86C9E5F504BD489D8C6B009D1EE77B082C5112CE2675990F401573FA6643551C6189B8545363835404066736BE712553FC8408BC370DB3603B086198B338C55368F0342B617A6314C4147C27D75925591C30BB2BD39BC34F0006C056BEDF6D072A4F3719C5FCE7AFA82CAD9A21EB92A4E6C833318671EC4D262E4DC321164DDDFAAFF6EA91514F7942888C0625DC03D16878DD5F2A4ECBD0B4C508849D20E98D6C84D0D2BD7A7F1E70687EE04F8EA275D8BE04926F3213055107135F726DB314F6BA9E4FFAA23CC074BD50A5D192C25F4354082A7B25C128F3FCD87FC6BD842BCF0E8A05AD2DE51942978F5B2B322D5205095570FB1855B7AF3AD9B5A6FD6CE7D418921EF31E15928E733D10F96553401ACF750001D7814B4233F7A512F2A7F768C189ECCB5ABD0206F7463C812E0186B60D9C06DD93DBC9DFF13B3B39AB3610C03184A4FB2AFDFA964BCD95C1DC8692A01EDF4C22F0D8840D997D13FE228E393F47DC5591E68F64DF855127B5951E1F8259CF40D9EB5281781B5E171B908CFBEF4FCE13BCC41975890182AB74E9739D9CB4CEB30D600EE3FA70D858D60CF89D795E583BADFD5EE032653C731C52A67F401B2927C7492926651044608727570FAA99F618678000A61EBFCEFF973E40A0B56B961CF24959EF4E324811A4F1EC878E41B386F27A13BD322C012756B0E29ACAC140641B77B1986BB7C41D3DEA0E38A768475C3D6C6EA3A6851CE4578893592A5EA633C7C6E6144BE6DCE7B47EA653B8176C84E2754F3AC6AEF843A5E759626D3862A13B85C222A6E5E0836FA4ABE18DC57267BC77F2187258CB81D24E62CF972F13D74C56F970E4BB29D0D0F39E35E61D6DDBCDA0D90549AB5D87C78F8B7F7543CC95B2AECB0CEBA735A8EBDB8F78BCBAA41A27A9AF77C2F9EEACE5176C12B340ACCF63639538A6308139C1D9D4A68DCFDA14F2AE7239EA6D0454EA8A1684455DF3B57C4581F9E5B243F4A3A7B74537F8F8815B30758DA124044D9EECED7BD9AEB699CEB3D0C708D76F4176AB94DC47B6BE84AB40E677C937146D5EF26F854BC96C0050C2C7B1A99DEA2F8873DDEB668F2E4129C1159D8C4312E6D4FA1AF9E5739B04416B7E54AF21EA1864037B7D74094F970E9B7914066F7ED2220BB8CE88594050A8D20EFAAE7993E3CD4C4206542047A0B8D00DA347D8F97066BF9EBC2D6F77A68E91524809CA14E5B9559098A25F2BE13CF64F97EE4B9277C798EF544C07114E1B4ACEA057DB041D8006561E97AA4C60BB5E3E84974FD51A8B12254950508272883A06394EBD680732F9A0AC66DEFF31BA045174D2FA5B15D8D9E326CDA635799F38A5AE0D13EC12145B3EAD39FA1E8F6F97AAF5F437F28936362A235D00A7C85A4AA6BBD73A998347F06F83F1EA9F71F318B2EA96BBDA89B9149A5765C41D018BBF2190B3C6C526933BC828EBE4FF35B\",\n          \"k\": \"CC54EFA4BA6B3C0B651258EFA6C6850B1B31FB159C282D6F354DC18C8749ACD7\",\n          \"m\": \"4E302EB2BB5392782E7820868DEDB61F5A6AE558CA307A01ECDE4970E43EB448\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 44,\n          \"deferred\": false,\n          \"ek\": \"409471E1024944D5203F82C62D1CA67E96567F3A525F6AA22683C4D537BA87DCA66864C88F292630484CE6C2592BE194A59B9642B0C0C1C4771DF87710F11FC8898573BB874965B774C3BCF30920312BC3C1945876AC6CC1221C9019677087951EF43031373BE34492CC5638D530A4D01C86F9B70296162950D92B5D97724BC1B7E9056C78D16290F3BEA459AC21395FD8A60EA8551B5D935510557CC36CC5C42605A4459CD7CC603408C3AB2380F7F00F44EB0DE7457E6F40B8DA2042F34C18554606E8A274FC8B6842F299C1E80068F11870907D8D44269BB7789AF0C8465B72CE64B5431A885A00B0C200102C811E8ADB7893502277FB87213AA89051852E896B3A170A33CA8F44A7367756944760B1D3C37318AB76EACBB47BA15DBD83CF8151BD2E3672934702C3A21886B27BC3506DFA8CCDB77C818734A18230868092186431221E3020157374ADD138C8F3C69D7709BF60C5A06BA3E410AF156B2175020EBD7603AE10C76DB7C2CE066A98A0AD4131781CC30311B38A78CB809BA48136009C36144B47AA2052E59F16E0216561BB42062817C67D81C96223C37DA9332B261152ED90350BB2BBEDBC438641CC082C8E4A4181628253734CB262061E40143BA11BB5AB75A89270636B330E922C5298068C85C1352FA5A133C3900A762F57B831A907AC37A6C9CBA9C915290E10A447A8B454A0F735DDB8852D79B0FCEB207C25568F44CE9568305A9362ED584D77123A3716A667CA2952CACFD7382303A69A817B17D895441D0A9AE1F71ECE1277C0432A7FA2A63C81A545910B4B212F1D983DC634313BC4B3B1B39190438D47B16747FAB48DE4777911B8D3FB65FAC68CA096919267783B0497868A9F95E89A1A3B2861A3BC89CB77F6976463CC7B1CA6CB8D447AA65463174A384217CFD41B4B80B7AA4072B10BCCBFA73C53E45684C542B031288C1192AFB1077B4E2B560E248169C6B741904304B06D23C86C9434AD64C67D92FB872D28BF9F745F2CF210D6D0C9C72176A3242447E06925718FE6530E2B5018833831819C23D3402BF9DC22FA7B1B754C6CD5A5C610275352D8B0A04159713C26422C688ED70B14209DA8EC23E8A2B285D61B8C5993739707386B4A084B22F70BA9EE3073644521E9B60D77755C30B36180B6B6A64A94ACEA2AFAA784C53C0C78E79203F36E98016CD1D109E17952A49021572B5F43857FA401BA617146E09450C8F392E423AF9FA294B7E2C2D772784B157FF591A4A13919C573713312291BC02F0F0C93721796477608E8E69FC64262D404C95DB98E548A767A184047A92C8FD8AED972945E676D946AA0DF5A88C7A13D03E501D228482FF97D62E27529372CCA9681F0758C2B500BB5D691D1734859B93E8E177605729337B39860E256D61C75BD462A6607692BF74A4CCC346240058B766502E740AF864531301226EBC3B8B5B1378846190AAD61C9BEF1362537606DEAA531EF97582DD1551E7A5F960B8DEA7A1815272145B25C7D175CDB568A68F05058F255B5B586866278F3AA56568179DF4B848FE535132B7C52D3AA3829686EA7CB6FA1BF07099F055416B8041AA9E2522CD23FE29981B06B6556432ABEF02E0780BEAE777D26FC062D94F1BC4C683AD2B92303D532101461FC0B8115556C3D2F2B855D1009704611\",\n          \"dk\": \"895C17908C9C3325068A545C951A7E5A2970A205783F8426BF3359F556014D79ADFDBB03CDD8B2E56BB3F8A095CD143B2D73AC87D997DD64633C0056203971E38A1C1E268F1C3A0C19D0A573F9B6A0401FD6FA93B65BCACB7C5E7FD660C4F7802D99A01B41BE77C776240737BD2234BE38B655857AF88AB3E40294355A5B485316A2109447606B3F86C512D726F3B915524589D4A3929B223D9B9157CAAC627F037AAD467C256A39484B3C1F3CCAC9F20F4A8AA840009998298C7FFC453607B1E1F43F0F740631B051720A3CA1392A34D84C06AC135B3A22AB277E51C56664917254842ABB134EC4576D33C69C7C66211C0B40C6912547EB5B9DCA0AB2035552956860DC230FC616940AA8B3249F8D8570F763419DE0C8FB1678AFEA395A553D1E3592DB6B4766684FF4421E97CB3C6D12C7798C63EEC9CA09868D8290C0A8ECA681AC1280958BD67A615F947625C35EE0C36F15D6B6EEF79FBB811386A9713A78B9FB581A6D47178A0A3C8D11B23BF6A2D997BED0CC34662C109B843C455C87ECE728C790AD24E690376806776A0F1AB9CEF70C6E96B46916321F77251E42DC7775BBC8BA5C295473186A7A277B2561F0901E96602E5ED31D50038CA839B75A3164AD17CA92308F3E75264A6C8EF0D940FACB9DFAF855283551609B4B2CE0329BCBB76A0710E4807F95169439C6AB571477C4C485B73503F87157B75418780C4CADB54B4E2CADD6791688E867FAC57812671D31A92F18141AE033570D21A12C1420733CB7A9835B60AB4677445C49756DAE5477F1D400FAB10C5134092AAC2C0D42693283147E033CA08A6672C4001734233F496966C214E7A36ACE327AC883C944446DE2085233A84D18C6A24BA95B3684CEE5BA0FA30A2C1A483EC4668B74C356CC97C5995247AF39549C92BA9D544580579B1E3B49FC2864D945A7DD555D2F5CA42A519615AB75EA593B15E9BCAE108A3B711CABA18E58ACA44CF2BBEF8C00E99540888297AD37CFEE4BA60F843750AAB223889AD7148164F28330926368F009CC440801989BB4003FEAF3A25B76C6E6686C06E3889DA28DC62383035042F1E64196560775C3B2E6CB667D30578BFB889CE194CA582039710C5D673B2983A8004BBADA80627A7A33C3B524F5CABFC88995D99A58709A634398505D77121D76ABA1FA8B12F93E171321C8EC7CD2C996FD714D07A9B6FFB389A05C2761C45AC9F3B35C24A246E348AC7B0C7BA347589041EE24BCA378888CB6471546CD9E04A68469956BD59865B6682011BF1FE71226077BE7E9C81828AC7312417E46A00570A6D65C4FC4274CC43B73305B43EC8C96CC46BFC898535CB578A7F483D63C43DBF36F3D698613ECA11019581301AA319BBEEB29701ED472955B4C453110BD381F328573EF83677C65107BEA546CD930ADA0CBD153ADF7C7986557438CE099583C6F517C99AE62BB35769385459FF2FB1C38E130F7060AF97333D38BC6725374EAE980FF69046E905DD3E33F4D546C3AFB6EC146CF85A66366B700CB544FEFF9439D9521486240DCF31B821121DB914F107B32FFCA171B764B299C4A5DE793DB42AE20505381B820E7970AFC2064C80203D9B63188DC6689D67B009BB1B4A912409471E1024944D5203F82C62D1CA67E96567F3A525F6AA22683C4D537BA87DCA66864C88F292630484CE6C2592BE194A59B9642B0C0C1C4771DF87710F11FC8898573BB874965B774C3BCF30920312BC3C1945876AC6CC1221C9019677087951EF43031373BE34492CC5638D530A4D01C86F9B70296162950D92B5D97724BC1B7E9056C78D16290F3BEA459AC21395FD8A60EA8551B5D935510557CC36CC5C42605A4459CD7CC603408C3AB2380F7F00F44EB0DE7457E6F40B8DA2042F34C18554606E8A274FC8B6842F299C1E80068F11870907D8D44269BB7789AF0C8465B72CE64B5431A885A00B0C200102C811E8ADB7893502277FB87213AA89051852E896B3A170A33CA8F44A7367756944760B1D3C37318AB76EACBB47BA15DBD83CF8151BD2E3672934702C3A21886B27BC3506DFA8CCDB77C818734A18230868092186431221E3020157374ADD138C8F3C69D7709BF60C5A06BA3E410AF156B2175020EBD7603AE10C76DB7C2CE066A98A0AD4131781CC30311B38A78CB809BA48136009C36144B47AA2052E59F16E0216561BB42062817C67D81C96223C37DA9332B261152ED90350BB2BBEDBC438641CC082C8E4A4181628253734CB262061E40143BA11BB5AB75A89270636B330E922C5298068C85C1352FA5A133C3900A762F57B831A907AC37A6C9CBA9C915290E10A447A8B454A0F735DDB8852D79B0FCEB207C25568F44CE9568305A9362ED584D77123A3716A667CA2952CACFD7382303A69A817B17D895441D0A9AE1F71ECE1277C0432A7FA2A63C81A545910B4B212F1D983DC634313BC4B3B1B39190438D47B16747FAB48DE4777911B8D3FB65FAC68CA096919267783B0497868A9F95E89A1A3B2861A3BC89CB77F6976463CC7B1CA6CB8D447AA65463174A384217CFD41B4B80B7AA4072B10BCCBFA73C53E45684C542B031288C1192AFB1077B4E2B560E248169C6B741904304B06D23C86C9434AD64C67D92FB872D28BF9F745F2CF210D6D0C9C72176A3242447E06925718FE6530E2B5018833831819C23D3402BF9DC22FA7B1B754C6CD5A5C610275352D8B0A04159713C26422C688ED70B14209DA8EC23E8A2B285D61B8C5993739707386B4A084B22F70BA9EE3073644521E9B60D77755C30B36180B6B6A64A94ACEA2AFAA784C53C0C78E79203F36E98016CD1D109E17952A49021572B5F43857FA401BA617146E09450C8F392E423AF9FA294B7E2C2D772784B157FF591A4A13919C573713312291BC02F0F0C93721796477608E8E69FC64262D404C95DB98E548A767A184047A92C8FD8AED972945E676D946AA0DF5A88C7A13D03E501D228482FF97D62E27529372CCA9681F0758C2B500BB5D691D1734859B93E8E177605729337B39860E256D61C75BD462A6607692BF74A4CCC346240058B766502E740AF864531301226EBC3B8B5B1378846190AAD61C9BEF1362537606DEAA531EF97582DD1551E7A5F960B8DEA7A1815272145B25C7D175CDB568A68F05058F255B5B586866278F3AA56568179DF4B848FE535132B7C52D3AA3829686EA7CB6FA1BF07099F055416B8041AA9E2522CD23FE29981B06B6556432ABEF02E0780BEAE777D26FC062D94F1BC4C683AD2B92303D532101461FC0B8115556C3D2F2B855D1009704611FA9DC07F088B86A0879CEC27AE467955EFA0EE0A57A996B3B2846ADB293805DF1C399367603CB39ADC06F676FC6C04ACC64F24D88C1E3F36191D5294C82C45A4\",\n          \"c\": \"8C8DAB2B1D37BFAB6EBC4E502788E061EA1097C8708ABFBCB2375B77B25A985C608D83AF0A1049594D56F920830B98292E64DE1D6F6D748F543D9EF3847492DAB472D7C54949CEFFED40434519816E9C9C1E5477C209F4518EB441DB2036B7C1A4A26833A25BFAA6FD5DAFB2F39C04266A0E53F1E2886137943AD5EA9996206F10D5043C8E3BDA12122BB4419E1D9192285F153999A270CCB7BF0EF68E638A95F3BB416F670A8D13A4625B4FAA29B0E6DFE65C74F1007A3716113A3F194C6319D5FC3B354E2EE56C9C2CCF256A2CCB3B3B0E5BF573CAB638F826AAF85291DCBC01B763E83A834B1423400FC170B3112D2CB44653E6C17684CB2ED01CB247FF1FF1BB242F2519C73B41969CF912713F8BE34D5F9EDF33D431307A0F42481CB904E2A6A7D062A6E0A4CC2E63699029615F5F3B35361369A0D827D4052D50A8EBDE1469B1CA6546DAADFBBEB1424330BFB488BCA9B1B594E5E2962BF10F000098C2CDBFD9249A65DCEE7A887FBF19A4CE484AA3ACC53736FB24DC1DF4D54E6E4B37BBCACAD87E9E324BC5B48B7F8612575EC416219DFDDD225A13BACE2FC7498A92F6B19DF2ACBB8408259E4058A96A2831E71DC6DCB396B350DCEC403DDBF3262BC70721F1B7EFCF8653560189B70A78F4C74A0F1DE28490F9ED08E0E93C7AABA92BC52384DCB8D24B2B998B1D4345C47778832355A7B87C66987C53B0E0F1BE3A163CE306E2A9A579291C582E7FDB7A8845D85C6AFA78361F8B1EBE36D3B65D6EEE875363F1087DAA2A04183AEEEB2F65643E4D75217C8EEF3438052049C581187E405A792D1CB25D2BAC78B2F92BF8E90B60841B2CF13CB2439F2CFFEC0927B1C47D6415ECECBAD2BAC55122BE81C4C745B18111806670A4BC77C89B25C0FC79249481A6619877067D2C7B06350BEAD29AA0DF24D966F7BF5D6B46A2AE89408895121568F5CB93575A33E5CF7304A162C89AAE63C9F044D5BFF63542E60140E25EC9AC5A653CA7B2C8CFE2904CC4E829B32F3382E5CEC4D443DE510D970D17D42333B8951CDCCDB40FED38118AEEB3AB13A1AF93AD106620CF5538007A92E3354B6EB0E8DDC3A733DAB98BDC532171B4A44907E444BB13F44B78E5DA4BF142C4A8BA8BEE4CFE55045D42517016F86CD088C992AA15486A4236C55FBBDB052F26E15AF989A2045B647F5CAF5C970EA516696B237080AAEF314A05E76064EF8DE9AF726147B24BA2936F92429FD7BA2DEB5D1D955C73F81BEB2EA6471356E3E503A6C04BFE39E3CD98F9A1C8F10BA19C454D2053EB67B6CEE908A05A5D08B5E89D2B539D2123EBCFA9A8F9DDEDE7E6DA52B992F5AA88339BD16DF594CF53E81B6550B49544A70A1268D36A43AFB52FB0CB164E183FC7578A1CC5A1F162A1F0C86017E5B24D07BF9B554DF40916A35E708A736F6AE1732BE39F11265BB083DF21B41F92824A44E9A6CBFA9CD7803261085965F2B7E6561087041ADBC58E00BBC8FC111D89C36DA9FC71721451A106137B2D2A221D3F9A8E09171D64B98CB1EF4F1C\",\n          \"k\": \"C26DA6A23332B20914F703E7CB237D84F807CC7248DDC47599DDB0D40FDC1FAF\",\n          \"m\": \"7B334E045896C00F90D811489D491E8D72C4E3A22ED831C019FD4BD967B7A802\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 45,\n          \"deferred\": false,\n          \"ek\": \"D0F1B584A87CBA7409D8B98B49B1332ACA1545B29FA2D42BC537CF959C6182305C95E06F5040B447D00737B0BBBFD9AB0A6062FAE209B24959EB83C2F0C5A2D8AB36161B90E2A92C5939059F8B9586F071FB1B26121604D5252D136705D33911703B91DDD85ECF80A7A1741C29FCB06F321E8BDB8166EA8C899B8064B69D77927C0B503989101F1A6941F4DAB06BAC9973378C2E6B3B19D9506B2C9A6BC29A51582C8B47B46306C1B35A438A097B07405645FC66C2660F4C14457A41B91907CB2864338C09C0CBAA7A877A9543F775950487DCB8CD8A105822B93882271A17B6936912A55FB4109B45B09BB14EAC2134E0D7471DE891CA984FD7B194B1529EBDA3CE0C24C1A9A170D76145215C8CA7217A138C9FB7DA8385E8A0C56B5342DA66F9FAB03AACA4E3A1BC5495C7AB0AB0E451A22D480B00FB29CE641C46E7A5EF16867101A26911B0A49140F70B8684845850DB8E15CB7BF842CE712480B19343077674D8A7B7C47A5E1170A629E53ADDD10F4BB35A601A843B49391D7337D7B3B592182F3A1636E4A7CEA14676A0DC508423B94B115041EA5F925BA0F2FA8536AB11E6D451321450CDFA076EAA3577D605A7E0B354754B92F1B411487FD8122E8DAC14D8464EC4331DC746B5693451B52878923649144182C1F03DC4281A7991C60F4A4076117B50C21670809832F3A67DC82E448A49AA3A2002B1A8667CC947253015A6A88FFC133554746256A103D11C7BC98013264D5EFC15A0B2825B37C356A96F0A5B701B4CBABF57640F2A92796C5663570B30FAC65BB79ACB59CE5C4CC7757920496B85CF938992EB32048360559745368854EC7A85A8E862A973AD50D67938265D3541161BE9C4097323F5E77EB8F66887A89FB7DA1A2585A4DD52782AD221B515A1D37778B5911AC7C90FA7722FD5A9484486A6210157188ACB617413516955F5EB91D2691E1E38CC2D10638330029D033005A88606E6A941962F3FAC506FDA5211609138E503E3382BD71640364066A2C27780898AFC1CCA9509C23F022AA77546017B1943360FAE24A54A0B965BAA7966F45AA7F3BAA7C6271CD28D4E0263BCCBBFE4FBCDFFE05269B048E948BF2EEA1E3C1825FBB3AA8F182B2B7BA52877B2304434DCD561BF365BCDC6AE0960211BA14EE1F9C8CBEA3750472A02271BA7E79C4FE6C9019B25470962315C4F50332414F46233A354928980A7752D7A07215EC24133446521DA5E4C50B615AC4E78F07B6EEA141AE146FF5BC2C63167483CCAD311B847996339AA50B2C237B4486E3B538C75B14969676465F106B9069A6F627938690139F011F5FA7D1832B96A0309FC85920E142C9C57CF64852E4564334281BA63068C5666A71B194319E99FB024AD33F879D08C16743B34038AAD20D2C84CB7440EE04C1AF4B944227FE94B5D666C3E51523248D913B9A3693DD1147AD7064696CE5B613DFAE1B83A207742264E878C7804D603BDD71E56C5CA7F887158E805161C959F072CBFF82DAE020C80135A8D1890F1A9A839032B0D69B9A7A97785DA5C201685C683844DDC99F4A97E00A063E15122AF22C27430328EA1435ACB07ACDA0A3B6B784EB19E30E08353CA26118AC4D5C4A218A262C4BA135AAC24F1EE7C5EA0E13C86749E5E72541CA6CAA1E1C05174B08745437FEED0B9\",\n          \"dk\": \"51901DD6E7BF9FB37046292E2CEA4037DB8874DA2A9B24403D879FC8104A512C52903CA118E159D235C9344B815908C62C99AAEECA688F8756BB844AB02A15F7380BF92BAF4DE4CA5BD05F5E66914C136838231C563CC1D1729B026190D5085DF49C01794541408A4C50042636804E16D3AB3FBACEB6798119436D42945A25860825025F21279057A775ED835510E2B99439CF13B7261A83CDCBE39D62E42DE49CC96C45AA6E8018779B372FA0A1661146A2A33924298920DAAEE3E81FBFBBC5EF514B0E773C35D76D8FD413E9D4C07D60212E91B06EE1B795E3B8734660C4E3C44F745CF2058AF3AA5A7045317395078193C53B034A621A9798208FFEAA3B53802554AB077AC3B59AD33CF7782065F9466A782484F6049F54BCE6E85B2C957460478B7070860F108655C0901C26ADFDF553755B1CC1634A2FC820CE1B3F190C8A81A78C6E0789478B1D6F594300CC0C91979E222AA9BDA73F1C88286C94472944AFDC97326904C982855EBA982068B4A88BE346D939A2C4316FD1268E6E0200D40C7A7F701003028F1A05A50A0355F8D2CA5FC074B7CA4D7CD1087EEC90FD77300F73214C68093DFC4AB81C2B718B278A411185B9C86C8237C3A402809C632779136C641BF938CA3650900B62BF2101B827133C014398C503A7FB1B98C23BC5F492AC50B706E387993EDA7707B9662F180AAB380793D9B77D0006CB4BB83A1356E6033BECF84111A89FCC8A9A1E313A1EEC78227206670854CE4C7DE9CC87728B8F94F647A5147CC7B5B569D8B1572468D3E012FC3558A59C6BB808A1E7F1400B7CC92F1C55FA9C6DB91B884FE4C60F77A2C77624F5185EEF787D8DEAA066C50A4E71A1CBE964F06674EF8730B4E8BD599496A6322F25093321EA618B624D7CF15D2E18426842BE2F289AA4515D86CA89A077B254D95E037853780A238DECCF91A1123D3176A68C40B72085DD5179A49207B2A7AF7FDCB49F536B8A4884DF25684736BBCD546551589576499F7AB43625EC30F3786214961C89E32FB2C13E306B8C85A64D68A063B43425110420FEA50222D9A78A0409A550A4CC633DF75843C2CC3D71CA55CE012733997028E8778709B30A4C03FFBCA251990A8DF57D22B761978149DF42C8560246C72CAF55448B7FF114D2245CC58453CF836E40A851D23C754DC66574604FCDB49DD5F74C1EEC429293C3394CCE920A9522B598ADD73C8BC8C71E5122369B4429B88B0FD08DB19690C65518C8E243746734F6649C2DE845E19A19E594894D8B5382639AA87C9806525BCE13CABCFAC521884B4E2B26BDE7632A9A8C581BC42F87241F1408325B16C46532D0522F51E7B3E7E2A7F4532A2B4B8710589CD6B32EA0C7BEB60194CE9372A0FA79005214FA45153CE556E724A383AA5DFAC973EC73CD7D6CA1EFCA49DFAB32C32B0EC27879ED18932140AC340006BC5CB547A532DA668684218B997325F206AC22D169FCE0BABF6A1B0336B1CC039776A7A33952ADA8E54706FAC9E4B295ED219D2C334A30149647720F025920DF980DA164B0FAD128D415652F3BB8DFB0AD6B23A8EBC776E1035E863ACA12C9497F189185C7C2591072210C97BCF833ED28302E388247D325302394D284CBD0F1B584A87CBA7409D8B98B49B1332ACA1545B29FA2D42BC537CF959C6182305C95E06F5040B447D00737B0BBBFD9AB0A6062FAE209B24959EB83C2F0C5A2D8AB36161B90E2A92C5939059F8B9586F071FB1B26121604D5252D136705D33911703B91DDD85ECF80A7A1741C29FCB06F321E8BDB8166EA8C899B8064B69D77927C0B503989101F1A6941F4DAB06BAC9973378C2E6B3B19D9506B2C9A6BC29A51582C8B47B46306C1B35A438A097B07405645FC66C2660F4C14457A41B91907CB2864338C09C0CBAA7A877A9543F775950487DCB8CD8A105822B93882271A17B6936912A55FB4109B45B09BB14EAC2134E0D7471DE891CA984FD7B194B1529EBDA3CE0C24C1A9A170D76145215C8CA7217A138C9FB7DA8385E8A0C56B5342DA66F9FAB03AACA4E3A1BC5495C7AB0AB0E451A22D480B00FB29CE641C46E7A5EF16867101A26911B0A49140F70B8684845850DB8E15CB7BF842CE712480B19343077674D8A7B7C47A5E1170A629E53ADDD10F4BB35A601A843B49391D7337D7B3B592182F3A1636E4A7CEA14676A0DC508423B94B115041EA5F925BA0F2FA8536AB11E6D451321450CDFA076EAA3577D605A7E0B354754B92F1B411487FD8122E8DAC14D8464EC4331DC746B5693451B52878923649144182C1F03DC4281A7991C60F4A4076117B50C21670809832F3A67DC82E448A49AA3A2002B1A8667CC947253015A6A88FFC133554746256A103D11C7BC98013264D5EFC15A0B2825B37C356A96F0A5B701B4CBABF57640F2A92796C5663570B30FAC65BB79ACB59CE5C4CC7757920496B85CF938992EB32048360559745368854EC7A85A8E862A973AD50D67938265D3541161BE9C4097323F5E77EB8F66887A89FB7DA1A2585A4DD52782AD221B515A1D37778B5911AC7C90FA7722FD5A9484486A6210157188ACB617413516955F5EB91D2691E1E38CC2D10638330029D033005A88606E6A941962F3FAC506FDA5211609138E503E3382BD71640364066A2C27780898AFC1CCA9509C23F022AA77546017B1943360FAE24A54A0B965BAA7966F45AA7F3BAA7C6271CD28D4E0263BCCBBFE4FBCDFFE05269B048E948BF2EEA1E3C1825FBB3AA8F182B2B7BA52877B2304434DCD561BF365BCDC6AE0960211BA14EE1F9C8CBEA3750472A02271BA7E79C4FE6C9019B25470962315C4F50332414F46233A354928980A7752D7A07215EC24133446521DA5E4C50B615AC4E78F07B6EEA141AE146FF5BC2C63167483CCAD311B847996339AA50B2C237B4486E3B538C75B14969676465F106B9069A6F627938690139F011F5FA7D1832B96A0309FC85920E142C9C57CF64852E4564334281BA63068C5666A71B194319E99FB024AD33F879D08C16743B34038AAD20D2C84CB7440EE04C1AF4B944227FE94B5D666C3E51523248D913B9A3693DD1147AD7064696CE5B613DFAE1B83A207742264E878C7804D603BDD71E56C5CA7F887158E805161C959F072CBFF82DAE020C80135A8D1890F1A9A839032B0D69B9A7A97785DA5C201685C683844DDC99F4A97E00A063E15122AF22C27430328EA1435ACB07ACDA0A3B6B784EB19E30E08353CA26118AC4D5C4A218A262C4BA135AAC24F1EE7C5EA0E13C86749E5E72541CA6CAA1E1C05174B08745437FEED0B94BEB59C105550656320DE3955835AB95443E5E29C3324284CAA26E76BB6AB3D0B37534D57066DAE72629C29DC0B9090EDDA3D3AE710D53C3EDF2C0A8DEA0FF08\",\n          \"c\": \"3812F9581A4D32DB0FA1D98110858E6539FE3150FBD28F25574851F4A073CEA119A2389B50CE230D6AB30EBC0042DF57FD9C0EF7A0A2EFFCC08765EEC2454306948221F8C2E6AF8415C2E9AF939A148CE20C052C9E56429DD4DDA625485A6918CE70A9319B5AF49392EDA205449C083B3A14096C1AF57BB39A1C9453EEDB85F69FA268B3404E686F9FFFAE236D97DAD29AE3E2B84EE8522ED3D51EDC12203620BDE2783C061D248CFF786AB3C61C5BA6FE1804CB514D872A391E968C1A980050324DABAA48BBD7878117333B8BB793B3198E4D94AC7AF564C5D4947163C84FBAD5DBA3D8C8FFF49518550299FA5323FE20C50979E44EF0D943EA1CA8D03ABEB261E09D0D0CC2F60E108D462823771C9E789500088462DF65EEE971DF976018CA33D38028855C3CC6B3019ADCC82F31F2EFC7C82AD3A46BF9EFE934AB2C9BC7EB7B2745416A3722B03DD7A6030C697E1C5318D032C8F506BFAF1C6F3D0049B4F9741A9CD3165DA27E955A116545BC5FA274980FA302A3083DD3025310BCE0E88D13D58CEEC02D6AEC1D1DF9D90FA8C206C0BC9E2DF2C9145503BD7363F1BEEBCC5EB6797F6732D7D0BD9299E2B395F75C5ED574513E17E2378E0F53DA0ADFD69921B88DE01FF6A2B94059451FA9F4D19864178E3FE343624EC063617AEA697960EC61C01A6943FF92D32EDA1BC6BE8AB12690ADC7E7983E269CB552DD01D5A61C549D932B9D936BC2B8E15375216C2BB391E96F021A7B1D46C625FD76E8A9111E1AEC67885C9DD04A72BDBEA5377E543A393B8989A02D65FD269EAF8BF9C8EED223277EF118D888B3CC8D3E0D9CB75CAA04BC22B7440260B74594C9DC2B2398DC6FDCF7B7BBA625E475E6CCCA2089241D6F3492E9FC35E29A3DD934E6CD0E3BE920B165207117C9CA858B88E57F7175CF1B1773EB8C292DDE4FA978504160C1F19C1FA4991FBEA15DA8700FDCD70B938283EB38FBCB740250CFE2510C85B5CBC1967A94C065A446133BB4C1FACB5305DD3B497CDEDE3440150868746E5A8E1775C07ED534793DECC83458A3BBD1110739E3668F3E1195134458D23210E2C973696B542D99392447921BC6249C84959DA4657E2D3C675520B3A52349CE7AE92D0900A085F3A59E73438C3281505744ECFB53D8FE24494D129BA778A28113651535D6BC5D47C9E8FA0F4AA159C3A9EDFC40DDD399A808EACB88EE6524B481217A1F1575A3F77A25A98C46CC674CD53C369B69755C66D637CE35ECA9DE6B660A718DA7F5A88E77ED68D99616CEA7165AA840F60A289157FA01DA864478A519686657F0D88EEB1B9A601F80FCFCDF430A07A2A86CD076497C894297ACFE9B9DE1F27B6107E19CD278E3AD83E135D153B0D8B44E9A4A97F869D1F6DF45B4C96722316D22023990BF12B9D177119B4985B0FEE3317557EA0B832E9745A9E09AA814E718DDCC077C2809BA164AC5AB1B47020804A5C5E135BB67582BF6B10830030E7656E09CB55DB575EB54ACBACBE80D901011A5F73594772368F47B7528D8BE68940C\",\n          \"k\": \"104177A27A18B5F35D2CBE9BFABAA2EA987B4296946DEE575B45A3A9B44CA99D\",\n          \"m\": \"947AFE33934E8150B06BDD1EAE40CF82EA99C0C0106B101283EA382EDAD94A8E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 46,\n          \"deferred\": false,\n          \"ek\": \"28C938C98060954AA557102D5BF39C17444A811156ADB2554562C5ADE5666704171AA3B19964449E1C12E721A30299307EC7ABC2CBBC82010E3ADC6DFEB2C497DABBDAC973021C6535773C127B3793792EBC717D392BBFD4906D4F21C62FD6C565935DA98550A2A310488956799AB9743B94446658FC97A9E1C3B263C23022132D33D2789295C994B6A441000F7FBA45C83A920739A17312AA97B1625FE5118ACB1552B647FA91C93C11182D9B9AFBFCB1A0D1412A7BCF92FC2E1A197EE4E728BB1812B88A87729C6E117A45E4E0ACAB2BC4E4A214DBF3BB4528316718C477329DFB1229AF9BCF0A00317FF526B8E4828CA797B354A0BD304FFFFC3E16C86A49355A54A0005CB1CE532CBA6A19267451ABC9C645A3F49FB075922BA931B1F139F8C627113B5358E02E81E71C231A41C1D647840B5AA96937B22926D6477E5B3A5D1C00485BD91FE5679CEEF18670223E670879EB3B02D03958A7751C007564EDC37DC0524790C8A0035AB30AC310F8419897597721D6B4EA87300453CFE981440FF0C18A323A0159B47A9C262545359F5031EF8B24A0080BDF0B55E1606355B88FC57803A3E25AB5ABA59582054E184FD97CC3E4635BC77AB797A748677B52BA76A16A9564BA218655A0070F20BD52B0A8C512A947534BBDC99D38621C655970B209C6DC8A0033105060133B600367E62366030810062387BDD120CEA00424C4C307E64EED5A3685F7B78E51138E02C4FAF51AE359499E2A97BF3799E216377EC3581AC81C12CC8B41AB95EA98C6D0500BBC3960AC05A2292789844602CA7528B2B91EAAB7A5F21399E13927C58BB030F492102B4361DBB4B3023961117099A7648EB11B2BF25703972CF778B5D8AC087BA59BC5122DBA63AD03EABC02031EF60CC765A0478F2C15DF31165903CE56680C2A62CF7FA8459D26C2D3C346617C51042351B321BEDBDC751D9080E0F64D2673C17055BF5DA53D74A0088357C1BDB8589AF5505C40CEEFD7140B95780BDC9B31482C75F5C6F395264A54C2A54B3EB6454C879510BCFB42DCD9548C3A53996030CE76A8A2435316CC227B591FEA7A0C12405D27936A0326A66A6CAF0B271BB64337D9DCB2E2471DDC632F3E0300F92C04B288191F0856C59376FD755729F195C264824657725E221BD017350982BB982078CA7A6783C64C5EF462F329BF6BDAC685D1BE483AA78B7BA7F2A0BD0C0C3A24341C9AF09C72D82674B0849BB70BD4861E5FEC25375223F5E4A4E20095F330C43A3AB23B633A0DA04553F69043A0B6AF862A94835AD284C31F82C23E80C2A488872E4B314636094C70A5F7614B1174AE46C11F45661256D566C6801985A2A91990889999631FB5BD4072C884B85BA79521BB2C7A729CA3CA8C18F5EB340EB84B00CB991D17933B3212293828BE1734F21B775BE289F77CA45846B3B6F029D6C610A0738F61B5C80BC808A8A506BD1022940AC755A7A4765A1C4A36B42DA453AA4B516F78B2C48CB94500401FDC654C1BA5C0D58FCE894E455667B25269A429671B668114181F06060842D16980F7A772480F3EE46D58C01682AA538DF1813CF95B67D9156D945E2FD84A7003CBCF265CDC12AA7148B7AA55C66F44A82E4BCF7D8C7B1ECF22E1BD7033C0588FEB6A1D553CF8BC477D94FF875323943762AB1B\",\n          \"dk\": \"367C6ABCB47754ECB23287CB530C5F29BA90464CA083AC4C7698C44487B209639E37E43DDC8055CC936A0C3A4EEB215EBF7651B6D8849CF2077C5969D66194D6BCC1DC96B21695856E072D83194D4F2966C649A55BECA2E9D9BA3E9049F985C9F06311A5619975653D0617288EC85BB13626C2A713E1245EBB914E4EC7651366615470483E478DAD1276A3C46C08E0BB1AB4AD833A584DD84222E2B880450806426937626FCC4597133B9FF2A4355EE20258334AA20C5BF7639A215AB2FB4B3A4D74AD8D66C06EEB290D370447053F9EAB57FB42821C2667011A95BE07CD33847FBBA78F44C7B10537C3AE97C2BA653232827C9400538D4BCCB371B2E7BBBCBA0C34A83851C0236750A472AAA163080C562A40908D1585549A4F8C44798AC877D4C2027A2018399C3B5A03C278B20057436B1A00BD83B8ABC3B6B78D18B7A94252BED77128617ACAF3BEB1A937F516AD7D0330DC9694D7835E38A35E63156D0C9B48473639B3C1A34315C67C389D686669CA4759C41748E6A43BB6648820061C54E41B193B79F147659DFA83D5FA3EE7D195D0D826203899EC813070D325D943CE82A7194D3A5114FCAA71266A5A2CB8CC2B837D261E28FA2B12C5AF2C20C19DC6CB49559BEDE9A793A5B272F8BC357A291D55CC0347093AF3547D6913E61C4316E49422874D84342BDEB508C4B1259757C4D9D139729404B4C083B242A1A9920328FA912A5B9B1D67300528741C2A0D56F5870C1316710C6950B93E8535A8D1512891A257FF17B71FA46BE18167951BB87A5CBCCFFA8DA5C3C471144523400F7C06B6F6E9CF2595101D230CF4B256BEE416638389569C774D1B5A25BB2690B4BE446C4FF68B1C64D87CAB248C9E7682E56CCA64FA8CEF2C9C43CC8B8C265EAED9A45945B308A4A946399CBFE59541D144DBA75F72E87272996AD1C1752E1524DE6C8965735E7A12544283A7438264644B32E9D78DCB99B82602AF6AA75D10C645AAC8A30CA90B02C914FD512C1367AACD1C38FD38588F527A24E01C9118A71BA0B4C71AB6298845A26A24B6A4762A537BC6A0797A8725DB5BA56FD67038B1CF6ADA8E7B4887B9E369478061228B8985922B6A909FA318AB2477867DE299BC957087B26E11E134C3EA4198659971F69302618E1146CFEDB6210A99657C075C7A14A569780E6E94395EA4AE38CC735F7375B71A4C6066CF08251859036A9111BB21D03EB9294A756C7D3002052E9492500C77B88C4F9E41A0D347424D095B816C96EC790527D74928028A7F542F4541BD3AE06E11E24481B36F4E22930D29742E4A3F3D1B2126258CFE080503F9CDE8133A99D31AF41047269077DAD309AB705DDA9A7CC44374FDFBC26E771ECBA3A420D162DEBB440BCB87C4A993B1C656F4A0250B029F20449C63105F865A1E26E21915C0099A2CB0279A0824DC1DEAD981AF301E34759EB985C313C7488673AEE516655421997C811E6F3676D07C9D52C27022C665D7518864840F7536AC69432451A1C3E86093852B12C465BF460317C5311E12D3790EDB06B22B54F9B7018AB16E1C06049B774C68C35177F369BF2B6423F5175A76777E4A30E67489A1D0319CB1040454B9CD1C9976658DFFA43009D62128C938C98060954AA557102D5BF39C17444A811156ADB2554562C5ADE5666704171AA3B19964449E1C12E721A30299307EC7ABC2CBBC82010E3ADC6DFEB2C497DABBDAC973021C6535773C127B3793792EBC717D392BBFD4906D4F21C62FD6C565935DA98550A2A310488956799AB9743B94446658FC97A9E1C3B263C23022132D33D2789295C994B6A441000F7FBA45C83A920739A17312AA97B1625FE5118ACB1552B647FA91C93C11182D9B9AFBFCB1A0D1412A7BCF92FC2E1A197EE4E728BB1812B88A87729C6E117A45E4E0ACAB2BC4E4A214DBF3BB4528316718C477329DFB1229AF9BCF0A00317FF526B8E4828CA797B354A0BD304FFFFC3E16C86A49355A54A0005CB1CE532CBA6A19267451ABC9C645A3F49FB075922BA931B1F139F8C627113B5358E02E81E71C231A41C1D647840B5AA96937B22926D6477E5B3A5D1C00485BD91FE5679CEEF18670223E670879EB3B02D03958A7751C007564EDC37DC0524790C8A0035AB30AC310F8419897597721D6B4EA87300453CFE981440FF0C18A323A0159B47A9C262545359F5031EF8B24A0080BDF0B55E1606355B88FC57803A3E25AB5ABA59582054E184FD97CC3E4635BC77AB797A748677B52BA76A16A9564BA218655A0070F20BD52B0A8C512A947534BBDC99D38621C655970B209C6DC8A0033105060133B600367E62366030810062387BDD120CEA00424C4C307E64EED5A3685F7B78E51138E02C4FAF51AE359499E2A97BF3799E216377EC3581AC81C12CC8B41AB95EA98C6D0500BBC3960AC05A2292789844602CA7528B2B91EAAB7A5F21399E13927C58BB030F492102B4361DBB4B3023961117099A7648EB11B2BF25703972CF778B5D8AC087BA59BC5122DBA63AD03EABC02031EF60CC765A0478F2C15DF31165903CE56680C2A62CF7FA8459D26C2D3C346617C51042351B321BEDBDC751D9080E0F64D2673C17055BF5DA53D74A0088357C1BDB8589AF5505C40CEEFD7140B95780BDC9B31482C75F5C6F395264A54C2A54B3EB6454C879510BCFB42DCD9548C3A53996030CE76A8A2435316CC227B591FEA7A0C12405D27936A0326A66A6CAF0B271BB64337D9DCB2E2471DDC632F3E0300F92C04B288191F0856C59376FD755729F195C264824657725E221BD017350982BB982078CA7A6783C64C5EF462F329BF6BDAC685D1BE483AA78B7BA7F2A0BD0C0C3A24341C9AF09C72D82674B0849BB70BD4861E5FEC25375223F5E4A4E20095F330C43A3AB23B633A0DA04553F69043A0B6AF862A94835AD284C31F82C23E80C2A488872E4B314636094C70A5F7614B1174AE46C11F45661256D566C6801985A2A91990889999631FB5BD4072C884B85BA79521BB2C7A729CA3CA8C18F5EB340EB84B00CB991D17933B3212293828BE1734F21B775BE289F77CA45846B3B6F029D6C610A0738F61B5C80BC808A8A506BD1022940AC755A7A4765A1C4A36B42DA453AA4B516F78B2C48CB94500401FDC654C1BA5C0D58FCE894E455667B25269A429671B668114181F06060842D16980F7A772480F3EE46D58C01682AA538DF1813CF95B67D9156D945E2FD84A7003CBCF265CDC12AA7148B7AA55C66F44A82E4BCF7D8C7B1ECF22E1BD7033C0588FEB6A1D553CF8BC477D94FF875323943762AB1BF710097FD6086E9C14C3703A3FE5A5573EEA9872B6F28B4A383B70F37099CCB55B07AB371A4D050DDEB134D78D044F9937A01F9E17DFBFC4E495051A4948CD4C\",\n          \"c\": \"20BA3A872DA897C006AA6DA3D6793AE2B384C1F7B97CFF48DF291419D3CA55C4745A4C5336D6D62BCBD9941140506E39B4E080A3E669DBC5A123A20D2ADBA8A47D89F81209CFCCCE9253A639860333201E0505EC1E58AFC7DF41EBDE37569A22D3DF0C6E1D41F0D32F8118F83674D681E907E1B1E7F5A14A9E3E1B3435B30C375F5A8776CE5CDA68BA32BDDC44B900A6AD0C0AE3CFB468ADB9F8B21FA3C1D7EF97C4DFE1CB1B4F785479FFE9AB47FBAEEC6A688D288C2904124C2DF76C5674813C92E8DC3F246A515D268E44FE1BD1F562B0705C5DEA91B9782E96E740852CAE7949A84C14ACC6996F1D5CE5B5A2261DEAAD4787031CEFCA49AE532FFDDB7E097DD67D2C22238D8279EE0F8D1657F80E630DA1629F0C12910CCAF1296F2BA404CE59F8C22A792611A8A4BA5C88D88971C86A9E45E0DBEF51D4D14C5E01DA5F2C46E9D031A07BD9BED7A4375C895EE84B57C6D14211D90DC327BC6ABEF72779C9AE856E93BC74F1D8FFE00ECFB2A212AFE587B0022727082FE09B79A130CCADEDECCA7CFBD852AACA426819A688E9AF837A6EB5DAE47F73B46EB48CE5E059403F7C5D522E3B7E26D0115677428946C516023319D78AADC641C7B3F7A1F6E5F4429C00C0C5798DA60CDCD79111C0EC5B4BD86CA2A47F676B45D4380D2C94D6AA360E6497BC9B25C092D0B58D8DFD0D315DE457D95C09355DD9B36950245FD8909EE0AD6A1637F4F00476861FB8930E81040CCD6224FF19D2B84D37D0C7A383C6E2CBB122B0167260EB0B47BB6C424123213AAB6C89F2D527E20275966638B194F01F16863E2371D45572FC0D63541B896DA6943CF59CEB74EF53834ED8632FFF4975AACAEE7818F7B3F0514333CEE2CDC4284E2CF68BE084AF748880988CF77011EFCC00A66B0B4C6EE5628A950984127CFB4322FB98A9FBF1DFFA28A9E5C2893F3CBA1DF900D9ADB533DD9A991FBCB94E5E5167A4C8FF2DF80E504679090B7F729AD702F3EAA6A35FC4336FD0FCEFA1C8532E60321A7D72FEC4F10EAD95D210479B060EAB5BAC9A3AB6F33D22FCDC514401B50DED0187D3483C8F49C2B00D39502689E666009DBDD1A0B9AAD5EE2F3A7EE70D7E381D478F70D38C1455A7A39FA854BDB75C2E3F32A01F42936D684283A9E12A10CFCE0C8ED77CA394930A68E1CE4383C25C18A93D26A2B997DF21F12BCA7A478CED58C08CD353FB91E01BC163354791C9F7B43A5A90FEA0ED92D4E9041D0D9C36F1C204498568105DD409FD71A3A0BA7B0E740EA3690F5B8C487BC026DF1BD7C8F9C9B9BADBAB28D815D3EA200A9F75F0BB9D55AFC4AFC5F38A0609616DB5B41C7AF9479003BD2F1868AE2BDA2682922CB1C90270DB852FEA6F7489E686C85C20534151D94DB33367F02454C415251682A53CC2A0EE1A845A77F6DD04ED14B3E0B6099375B7DD10D4412215F2413EE326DDA5A4285481A2A70CC0FEC98DE1D77A8FF93BAC3196D7610E842B6A3D2908B61AB7A864C0CE2DB55D3E7DC4BDE89766BED1709986009DCB00FCA4BDC3\",\n          \"k\": \"A2718B4EB96D591690F62FDFCC264BC457C3A1B755F4CF64B359BC945C254CE9\",\n          \"m\": \"DC8510F45528D6981E59C1AA6B743BB844377D7339E359036929F0EEC54FE63C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 47,\n          \"deferred\": false,\n          \"ek\": \"4A6C1F1A816FB66471E6C18CE6126CD02BA76C730E721572DC730566496B3AC28F92A15F4C534C48578C18B443F24681BA2B3C3B390669A288EF7676C9A6B0970340F567C90407617B28A9A03274D8288006288B0A5B686FFCBACB273536941CD06927BCE109BF2330CF65A3CD0A2706EB33150A88504484FE182BC3C78CC5A7711845C3A277C763DCBDF16B19F1001727D5677687ABCF4B32CD161350EBA4206B2E1A65848528B7E1A90E04165BEE054F43793C23B0660FE7AF3E5964DC7C275458040AFC05EEC71A57E67C2BB794CE2680E7C4170FDB67F0F36DBA809F724B805BE66EEC38181FF1B0E9F94F7903661ABB17FD9BC0B0B19BCE2560E2C2C764F4863BCC40B5FC10B6513483A827C1668CCA8AA0B68039C8ACBE81B639E55AAACF2C761D78A77C2651573471434A14949943D945BC0ED2187DB442E41B901DBA0B14580710870B81B78D21996AFDDC6ACD1473673414E5C7C35EE60EDE3579223A7A2A436667CC51DD366DEAF615CAA4CAA073B60718B1E5B62ABF50BEBF4844EADC2ADA98CEAA8CAEBF187C2F26882673307A13CF83904607A8865A65592866499505051F10A4DE52893E07BABEEBAA4272B14EA974B5114101385AF1A011A488BA25179A118B312D8AC86FB58592D328941A3A0DEA6102C86F1E237DDB321615297A2BB660D5E0705A6672FD08A5CF212AC9B5B7A1306C87952B1C11B7D4E7C5B5FCB7030725C2A14AED641D78453E9FE5C6E6280D2CA57832FBB091C4418303CB4AA32DCC3C211DFC8A9BB429E750B2251149727ACBB112607EAB9B7744173D795B06D10EF8B1AC1AE54DA30A05188B0164B6B6E9223E93D377259163D62530CE9A0584F82E59534AC3E061B1B48BAAAA9DE1F8BEC150A63EB579DC528EDF977E7E421743F6A4C5DB61DD31334D3C63ED7755529064D67A4BA3780BC7C87731B86509738C1F7215275ACFC6E3B651FA91539B2B10111452570639E966766B64477C3F1E255390840FB9635500F2B513281E9D8B5BCCEB360F05913591899C1CBF95D23CB8D714A007990BB344C3A62446A80945D0CA84D3727CC8C39E0553A13B7050C12A1FB8BE6567930E0B6ABF6C484FD340CE916596B36BDC4085595CB2CF088467B0BA7F915A4E9581E7269C9A234F1A004A8BD0A2D7654018E55D6861B4067A29F6425FF1F5A00A804E3B3461DAAB9265D15BD530AC10686FE11038E772BC1CC46DB5C401FA76C1EE70165FB7C67C5025C2515E4182B46B92194FF361E252CCC43B060F50A64C79750BD162A586C5A63071D6E93DABA8BD525548F444C621A77E3EC583218788E3A7113DE38EB2ECCEBB0CAD737BAD9C21B39DB255458B8D6EE70C250CACC74A3A19C342B052A0FA5B92399013EEEC1BEEBC8E02F80C137030BA0A5DC8C3028B550925367BC732CE0E09570AB1AF01B7CACBB78A89BCC8C674449B932B98487AF0257928E64AD06967B38B30A0620FF9D49D2D63C2E7847E5B26046B457A7BB37CF012A7F18AAE0CAA41AEF72937B35AFDAB0174688B0724B25D67563536950081931A3ABE89B59297F701F246A6462B6016EA4EFD42A15C984E7157B5B4A335064809BD7739BAA23519098233C7454E241B23C4644A2AA1EB16E456E23567C4C3C6662ABFE76F52FE97F07F1298BDB70F62A5650A\",\n          \"dk\": \"74D0A332727A99FB07FA104DBAA09E120A80F2C3BD27971D5C8283E14C587A4C1064FBBD25919712C34D561270980352939739D378881F21813C077FD4A30A14873E01FA659B567069F9BF7BE0A94C9B2F62A9243C986F4B8671074BCCE5421FB12922F8B9BC50A9B4A4015F3B58BB493B34CDB7909817B0E1CC76F4B71A6BA45E32C03152D0BD972B6C557A3E84B62CA0F830DCF55FAE743AF419C75B253210347B00383043B3BC5B9425DA8151294C03FC422F1C22235AC1B3D487C84D63882A504CBBCA84CC192173225FCC3BAE53601AD905554BD1CAE026867141802F41111411824580837E35B5B4E47FA3173530A40285000E695610488407ED986041DA7B0059A054F1877BE8887CFC028417C1D78ACFF8A09FED7B289BF96F3F993E805CAB566CC0FAEB3A29EAA40A2820CF20912A1A4F3EE1C5A48C212C421D78326D367C6C41E7136E810F8A8B00E2514AC351358D574CD0F7344A121B05131B05040EBEBC08D0E203599503F3029EB3A586DBA5862F371B78852338281982B1CA30C70580ABC429D80EBF9A972D9B026FF24C387352F63943131AAE1A804B10D47059E00885EB670D462DD36BAA0FDB7E798915A0688D6FB3313B100445465E66E35CF664B060E399ADFA9712A32AC631B542B66145505627C95664A6117BE68E7BF3B12FD53CE02B43AEA449652A6BBAE782FD4178F832C766C09DFCA17686E57CFC563B32A43566C30BE1A85A923A6620FA5623ABCCF5133CED5499D424A67983620BC519BD3730C51B6E894BAAEB516A5AF16881619903B3051243013E6B8AFD20B2AA56ADAF436EA9A9A59AF2922DC07B644936C94B0F7E4057621519AFB8C53485BBA390777C591BDA7716C8B1383141197DA00CB9975B37C6A8DEFBA0E45563ED27336F3A19D64AB0A803A7CE5A5D5C2C513A1CB53434681AB0AF7AA96A946B5A598A15C6AC54192B447ADA0B1DFB08DBE72734A87AAC6B4F65CB9F2D319CC984AF4428A41825A504C1963B719548418A79918A5723514155389EE70E2DE0BFC46706918B7C0EE0022CE04E86B21A98646850A6329E8BB08EA78FE1633B4C64BA32C4B1D920C543CA6DAC053338716F2F194DDC8AC1CEF24EDD52CB6EF39FCD6B5B04C306223BBBE511CA8D1315C56942D4EC321C63331F099655349F174A0A4729841C8B5DEA646D34218E9DDBC48C968B6CD949587922D8CC9E94EB780055663085B02DB26063F14651218B31210A74C72CD273383FBB1236659F4EE69315651785C822B425BFDEB96D53D674118B249EE6AA963C31CCB76E9CB068C4681680FB6167EB6AB3F518CAA5175693843BCC678E0C5477D93CE95ABC023590F3EA9708FC98C7045C75EC3905662DBC485ABB02A23CAB7E36D6B48FCB8E43F74D507BBEC5CC827FF186111ACE6BB1B2802C5EB9552698C320B523A692F68112267BA5BCA8C49589352C17F117078376A1FDB6B91F9C6ABB3059BBC53796F90E0FA575DC203DBB893AF1B7AC43F2050BD56A8C4212F055AB9BF7579EB809BF9C71F64BB3D6314AD5E8643A4240F5AB35880905E9D4176D25567C502C4C103910F78356C1C47867A9E2AAAD057C909865614612AC0DB4679561922487116064BF4A6C1F1A816FB66471E6C18CE6126CD02BA76C730E721572DC730566496B3AC28F92A15F4C534C48578C18B443F24681BA2B3C3B390669A288EF7676C9A6B0970340F567C90407617B28A9A03274D8288006288B0A5B686FFCBACB273536941CD06927BCE109BF2330CF65A3CD0A2706EB33150A88504484FE182BC3C78CC5A7711845C3A277C763DCBDF16B19F1001727D5677687ABCF4B32CD161350EBA4206B2E1A65848528B7E1A90E04165BEE054F43793C23B0660FE7AF3E5964DC7C275458040AFC05EEC71A57E67C2BB794CE2680E7C4170FDB67F0F36DBA809F724B805BE66EEC38181FF1B0E9F94F7903661ABB17FD9BC0B0B19BCE2560E2C2C764F4863BCC40B5FC10B6513483A827C1668CCA8AA0B68039C8ACBE81B639E55AAACF2C761D78A77C2651573471434A14949943D945BC0ED2187DB442E41B901DBA0B14580710870B81B78D21996AFDDC6ACD1473673414E5C7C35EE60EDE3579223A7A2A436667CC51DD366DEAF615CAA4CAA073B60718B1E5B62ABF50BEBF4844EADC2ADA98CEAA8CAEBF187C2F26882673307A13CF83904607A8865A65592866499505051F10A4DE52893E07BABEEBAA4272B14EA974B5114101385AF1A011A488BA25179A118B312D8AC86FB58592D328941A3A0DEA6102C86F1E237DDB321615297A2BB660D5E0705A6672FD08A5CF212AC9B5B7A1306C87952B1C11B7D4E7C5B5FCB7030725C2A14AED641D78453E9FE5C6E6280D2CA57832FBB091C4418303CB4AA32DCC3C211DFC8A9BB429E750B2251149727ACBB112607EAB9B7744173D795B06D10EF8B1AC1AE54DA30A05188B0164B6B6E9223E93D377259163D62530CE9A0584F82E59534AC3E061B1B48BAAAA9DE1F8BEC150A63EB579DC528EDF977E7E421743F6A4C5DB61DD31334D3C63ED7755529064D67A4BA3780BC7C87731B86509738C1F7215275ACFC6E3B651FA91539B2B10111452570639E966766B64477C3F1E255390840FB9635500F2B513281E9D8B5BCCEB360F05913591899C1CBF95D23CB8D714A007990BB344C3A62446A80945D0CA84D3727CC8C39E0553A13B7050C12A1FB8BE6567930E0B6ABF6C484FD340CE916596B36BDC4085595CB2CF088467B0BA7F915A4E9581E7269C9A234F1A004A8BD0A2D7654018E55D6861B4067A29F6425FF1F5A00A804E3B3461DAAB9265D15BD530AC10686FE11038E772BC1CC46DB5C401FA76C1EE70165FB7C67C5025C2515E4182B46B92194FF361E252CCC43B060F50A64C79750BD162A586C5A63071D6E93DABA8BD525548F444C621A77E3EC583218788E3A7113DE38EB2ECCEBB0CAD737BAD9C21B39DB255458B8D6EE70C250CACC74A3A19C342B052A0FA5B92399013EEEC1BEEBC8E02F80C137030BA0A5DC8C3028B550925367BC732CE0E09570AB1AF01B7CACBB78A89BCC8C674449B932B98487AF0257928E64AD06967B38B30A0620FF9D49D2D63C2E7847E5B26046B457A7BB37CF012A7F18AAE0CAA41AEF72937B35AFDAB0174688B0724B25D67563536950081931A3ABE89B59297F701F246A6462B6016EA4EFD42A15C984E7157B5B4A335064809BD7739BAA23519098233C7454E241B23C4644A2AA1EB16E456E23567C4C3C6662ABFE76F52FE97F07F1298BDB70F62A5650A159FA6BCB8D2EF121A97A25B0607D94B3DC6D5D48B620839F143E8BA01BDFC55E8D41C96F1D340408D550400AF1CABD517EAC8447644605BD2B50A850216815D\",\n          \"c\": \"473A1EB71AE24A5F5F3A2FC86E9F48EFF07570BAF66E36C2C86453424F218BFDCD2338EA9514242A877ECC28BCD1BE87A71CC4D413ED8A2E3EE4D3209AC01DF03EC3B28FDF3D572B0F8713D41EF8800C3C1DD4B60AB084711F9B402AA34593D1549624DB9F895FA48314E0AE94DBF0EBE54EC81733DF6B3F5F38DF0DB91F051ACCEFEE0B32A26EBE37A5B12302F3F809121E879A7D0A3F29F5F9973CAFDD09220F848F03E2CAA1E64B6C6AC1D46A9F9874796D63738B11C91D9971AA2C1595A4148B145379CD0DD606596CCE45C334255BBD6761C4720870771CE40D8A9D51BA655854915F2C23FCBB0D40930B0EB27C96356A6C5503FF5453E10DC198F5D2AB476988030BCD2A56FC235EDD3538E997D50B3007CE0D28E46D2FFEDA62545F2AC5D6ACEB37B089C900CE167D68D358A445D1BCCF6429B810E74BDF07E04CFAFD5FD30E59CE541B2BE55AAFA1B928306715741BD806256E4D71F9CEE10A119D6C8D860866324901C8E582A7FD658EC83185103F7F0C9E9230052F0DAE235A93646F3754CCB6FF9D4E4E1CF47B262E5ADB4412376A3D1969CD7B2413F37382F665E642699696FA9868B25BEC0DF374789AE0B476E206194691C0EA5A16878113D39903FF112207B36F7617D6A06B864AFDB5C83095C194A71694623C31EAF91CEDB387F5A1DEE666B6EF95065222D6F98384FD59005B2807A68F3BC75D99D298C7A432B2160A2079B5A8AFB97EA67F37588FA5E246FEC3BF82E18A75370BA268A4FFD85B957D2A573F23BFB004E79ACCB100087065B33DEFCA73AB7523F17AD2C17F76773B84646D92CB3187C1F6E5B74EBC7C7DC71048C7358F21F029B7DBCAC8543CE74BA6C59625E7897832CEA6CBCDB64DBDAA3E603A9374F16942E00C2BAFA6A44D9451365AA1B9ECADE2963C8BCB79FD2F836229B8BB5A0385D39A017CFA6F875DFF4CADF9E9D9290D9B7BF6D0F3BC009972B9F15D330A9EFCD42246EA642209A712F922630EF2AAFE31414F0C4990513C2A208BF7D230ED7F9827C5E449E32FC4242851EB2D78E1EFC11C26A15BBD6DE677549B2939D075A0F53C3BECE462E56ED9324878928B58A85A5269025B9FED7080D77482A4AC5747310BD52367126B6322DABDFB5B75CFF41E4AC012B1FC141285D6533A78A8D13DEC193C1826B20EE5B7FA13FCB85E514FDC69C7D6E5C48633F2E7524F9420074719FF206D5E1757FF9512D4F8B44FC7242E9DE11DA99B2388D1B0F2327407FB01E5CDF0A24C94CD9D706E9DC8D0673DDC5AC818D96A221B732996DBA40BF68AE1FFBFB6FC1E0294908601D3AC88D76891FE62A93402D69EF797D2024966EEC8E6B96848F0B9E39CA6CC80E0C4556EDD85D6E70473DEEBEDDC4CC49DB1D41EF18B0B3B3F17E28B436C29460B6C9BCEB3492BE8ABAC057C03ECA4D266D60B173D206ED77C1D3480D7666DEC028F31367BAFE002C9F63EC6CBA2528D72220D5E987CEA74E9AF7B3072F681C72D2E6DE1DBAE3D8ED43DA7F2F5AC75516E38F5C244D42B061C3538\",\n          \"k\": \"E87B61A6496FDA38F948EDC5F9CA5735579D47F6355B214727EF5BEB3C13CA32\",\n          \"m\": \"62A2DA94F109C0DEF56DFB275B1A0EEABF82AF8C6CDFFA94085AA93015BC1821\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 48,\n          \"deferred\": false,\n          \"ek\": \"36C053F748B3A0655C3BA86303E099538971AB05BB4B2167D84B4F8BE79CCE515F28A90A5AD5CBC97B84CE10A39BB59AD2FB4AED7CAD205200A3C688AA32AE73F552A33B7DF1BA197F16CFA853C7FF24C1DD586ADE74CB0982449AB9815A63672C32420C1812326554C7219CA7698324115615C41D2E74A0138A42356232D7218549684CD9800F1A8413F3D952E78C915AD56FB0D1CC76D48F2ABCC0E6D79C67198D53B3B6FA57A4E9140D3C4286C97C7CBE48C3FCC81C6CD1540B12A077BC54F6F46711C844B46559194B5597A815827307E0E58AD7C4CFDDE826FC3594002A88C83760B56331A61C542C7B4487A851D7FC4D6A4072B7FA7C77647F8F252337252B1C9709517A7E217B26F26984D8D0AF75C9C1012977A3996D855A86C47266224C2167E585C9687C0FC5259E5BA10FA23DC4A2C163E74EFDA01956965A5DCA57BCCA2EBD4606B810882A337CB33347820C937E821E75A5BFBCC75BD1E37D81342F8248366945C0AA2B98F6E228A3AC6EF9CC28208470A3FA56C2F51AF83418E3D13212BC2C64304B4EC55001BC3620635C989CC4E4ECA06A453C8E5689BE8CA010C4026FE201EFAA271C2A772563CE7147932DFB7256F28975E2829D454BB2CC64AF8684B9ECAE285A93EC2212AF4CB65E484461D2B2121B15B37AC9446A8308437B887271564BC8143A8AF1F912BAC58E06AA8E03764FDD823180E30D47056A6D217ABA6533AF4399BCC798AB59AB0880913E14895526AD2F06A6D6536A0DE701D7DB9B2EAC7466FB85AC0020D961B39B88A8DFFC3FAC57524170B70490876F3A6FB16020EFBBA26E17C012AC4280C46E7F643CC334BCC302BF3BAC10A3891FF0F2BB81D29D8E421A71272C0F2439AAF29916461DC24B4899325FF005C0E30085AAAC7BF21B58B114BB0025305C8A1DADE9A1811329792713C5236B44B52AB8791856C535DC8B0E4DD0753689BFE0636DB99697B074BD2D3A8C4AE69929F00B8B256617FCAA96C980E8A629BDA58CF6A76DA49A004055CD4BE21E2C02B5533A7FBBC4525A95167175480A7B09BAC2BAA47A03D4872620C191287932029C9B53737A69148B7F483C607148DB23594DDB2F149ACA20F13AE693C2265A71E911A4D22ACB1ECCCC4C96B1A2C0CD45A5AC25533AF6829919D93770E42BEAD230BF8739B48C9990ECAD9E679C501137C49019C2F857C9D08FD533B01D50AC6BF2398655A1CBABC670299ABF46B95913A845151DE2796666BA6824237AF61789857CCAF0AB0F4B430FE966C85818B11E469CA2D7B705833813B8A775C77138F91700C64BCC6360231B1540F77113A33DA08189E454465474947EAA7F6731613BF9AD6BCC6D7DD1B62E1A3AA67C647A62712BC9CF67FBB690205E6E6997FDA7719EB9994D199505342D42FB3C6AB1936825534980768102619D79B9F197CB05C16A9922B4AD752C4926539AE184756114601A6ED90443D2869D0FDBAC6F447D91F37FD4E6649A90B70C73182B6162375610E9166AC76656A9B9242DA02CEAEB970A1803BD570F48B7B7557928CC4AC5D494BCC9386078183675D760322A91A1FAB770198A437C052EF366F6C905062010AC09AAD9594F581036A7E11B0D6C3B40BBBBC340CBF6D130BFB4E7CE4696BDB01ABE0436AC41B279FD576FE86BE94D213F70\",\n          \"dk\": \"705A6AD224AB8C18A1DAB7039A7282EEF71B9AB515ECA53B257B43E39C5890B131027737F089115599CA3513CBFBDB536FA39A872CBD8DC10CCB3CCDA36CB910902CDECB4522AC2146A39EF42465921A767F56670EE297CE2A3C67A24D7D55540DBC15F2500831F384E9D9646946A4D133CD0686A9F8AC3919C36466AB2B72E668B1659A81699B5A9B62C79905D6F2674CA471BDC657FE973FF95437304708E88B0805858077D28BA3632821239B2C3174B21849A0492E4B1B6A42D39C656B41277B0D8D868DCE06906D82210F9424FBD1CFD81134E503AFE93AAE02574AE1D32608D92CCFF667BD87B45F58ADD75B79A2912A73672F0A9794086084B9F58F21B7137FF42333B47661FC6695B56AC18996064606B37426C4E04A9173425B985B6214ADE879B8A0116309466BB0408DB243A39E123F3EC68E7D47C7743A22A062A12F9224861069BF8C50B80A1799AC28BDEB43FD0A3F6C72BE1AE97699A1607B68644D09A546945B57E69DBEACC41165C8E221031DA43A41B8312E3A1C1D1C7329189EEACB0D9946600D63B533ECA1EFB11C3E6C12D888C0C8C331553361E9623CE2D29854FCB3064A9054C93F7142A8862CB83529183BFA66309858444293CDE44CADA626AD100DD0D76DF7C07B455C4A7C3B04354529445C9D12B65B0C59A7CE3C7F350619C1D3A65AB314F624A0F456715A1CBFB970BF0A571F0B9C78766014FEB46CD750C52A06B5CC8B5FFDBC8754792E44B7AC0B6332BEF32C57976688800EEDB0482062011E99C801CBC5A42C8768624B84DC4FEEF6580CD86275891B460218A8E652CCF228251B9ABCD9A9083A5AFE088BA85A0F1F10485784903623C347F9554EF0722E605C2D8012CCA89822500EEA037C3F319F5EB8A070373F34196842D96BFEE44E1EC9904754A3A143218E23968D28A83F91CE70908AA9178336E50688A446BDD85E1DB69DB9018A88D427F94474BAC3CD473A687201801D590277736C10047ED9790A33A39FC3267EFD154351956374339F50D75EF0F61661B68E7E59111D153505DB69E65190F80C9B64CBB77B958E83FA82F9CC6A945B3F46039D3B368E18A34CE7038D597C2ED16833C3F7B286EA17D58CCE86DC4FB7F1007E9A4C64F573AA053F778B9ED71825D97C8E1C52C953B08942A1AE30935F8765787F44505FA38B9CD93258F0CA6D34269852786CA82D980BBD72254D4D0A9C1A8559420C232E98CF222A469AC9893B6643DE6A499A53B4E73A9D989A615D3A22507694FC051EEAA27274BB3F19938758E90BB83B0999BC43A90B60D424901B7522220414D5D475AFABA945DBCD83807A58D3B5C4313F9F7027EFDB2299E873AA6A18658963691070D9A6A8C8BC061FD9935D521C0B319323F1B7209866BB052207B6AE8FF81EC8C12A3E035E29404FD28086EEC313517BBCF4A173F3573D69BBB77B177615874C0C04C0138C83AC8CB576E4C29363ADC8F88FF29A67C44952937B7ED61A1A0F11147C345555DA1089B768D7A0751875B44E36A63488A23C281FFC518E4A548D3EA6491E634AB97A5EE71805F5A7C037E2B2720CD037A2501D9C239DA14A618C2A53DA7C7D096EC2D0481B70A1D45075F2A000F5C60EBF9A8F36C053F748B3A0655C3BA86303E099538971AB05BB4B2167D84B4F8BE79CCE515F28A90A5AD5CBC97B84CE10A39BB59AD2FB4AED7CAD205200A3C688AA32AE73F552A33B7DF1BA197F16CFA853C7FF24C1DD586ADE74CB0982449AB9815A63672C32420C1812326554C7219CA7698324115615C41D2E74A0138A42356232D7218549684CD9800F1A8413F3D952E78C915AD56FB0D1CC76D48F2ABCC0E6D79C67198D53B3B6FA57A4E9140D3C4286C97C7CBE48C3FCC81C6CD1540B12A077BC54F6F46711C844B46559194B5597A815827307E0E58AD7C4CFDDE826FC3594002A88C83760B56331A61C542C7B4487A851D7FC4D6A4072B7FA7C77647F8F252337252B1C9709517A7E217B26F26984D8D0AF75C9C1012977A3996D855A86C47266224C2167E585C9687C0FC5259E5BA10FA23DC4A2C163E74EFDA01956965A5DCA57BCCA2EBD4606B810882A337CB33347820C937E821E75A5BFBCC75BD1E37D81342F8248366945C0AA2B98F6E228A3AC6EF9CC28208470A3FA56C2F51AF83418E3D13212BC2C64304B4EC55001BC3620635C989CC4E4ECA06A453C8E5689BE8CA010C4026FE201EFAA271C2A772563CE7147932DFB7256F28975E2829D454BB2CC64AF8684B9ECAE285A93EC2212AF4CB65E484461D2B2121B15B37AC9446A8308437B887271564BC8143A8AF1F912BAC58E06AA8E03764FDD823180E30D47056A6D217ABA6533AF4399BCC798AB59AB0880913E14895526AD2F06A6D6536A0DE701D7DB9B2EAC7466FB85AC0020D961B39B88A8DFFC3FAC57524170B70490876F3A6FB16020EFBBA26E17C012AC4280C46E7F643CC334BCC302BF3BAC10A3891FF0F2BB81D29D8E421A71272C0F2439AAF29916461DC24B4899325FF005C0E30085AAAC7BF21B58B114BB0025305C8A1DADE9A1811329792713C5236B44B52AB8791856C535DC8B0E4DD0753689BFE0636DB99697B074BD2D3A8C4AE69929F00B8B256617FCAA96C980E8A629BDA58CF6A76DA49A004055CD4BE21E2C02B5533A7FBBC4525A95167175480A7B09BAC2BAA47A03D4872620C191287932029C9B53737A69148B7F483C607148DB23594DDB2F149ACA20F13AE693C2265A71E911A4D22ACB1ECCCC4C96B1A2C0CD45A5AC25533AF6829919D93770E42BEAD230BF8739B48C9990ECAD9E679C501137C49019C2F857C9D08FD533B01D50AC6BF2398655A1CBABC670299ABF46B95913A845151DE2796666BA6824237AF61789857CCAF0AB0F4B430FE966C85818B11E469CA2D7B705833813B8A775C77138F91700C64BCC6360231B1540F77113A33DA08189E454465474947EAA7F6731613BF9AD6BCC6D7DD1B62E1A3AA67C647A62712BC9CF67FBB690205E6E6997FDA7719EB9994D199505342D42FB3C6AB1936825534980768102619D79B9F197CB05C16A9922B4AD752C4926539AE184756114601A6ED90443D2869D0FDBAC6F447D91F37FD4E6649A90B70C73182B6162375610E9166AC76656A9B9242DA02CEAEB970A1803BD570F48B7B7557928CC4AC5D494BCC9386078183675D760322A91A1FAB770198A437C052EF366F6C905062010AC09AAD9594F581036A7E11B0D6C3B40BBBBC340CBF6D130BFB4E7CE4696BDB01ABE0436AC41B279FD576FE86BE94D213F70E1D563B9DD64A334930BDF5141DF65BF77A06052C9EA81679080E231A8A61E0B97442F30F0F28F7A851B0D3E76BC74DF890916D2ECBA20DEBCBE3453655F78C9\",\n          \"c\": \"07C9FAC7CCCF6B5497C9BCE51371F26B574BE236DB8009103A7617953AD68ABF08A3F134B5C2807229AC884903D3B6B2596020D6C789FE3CAC6468EE89F4004C037125BD1F848B1731424A94574AF2A67ACB415E6EED82167C590C61DB7B34BCC571178794DBCDDA2B404B4F4A25D17EA1503A820504BD0819F248F472B48FF54B3CF01F8DD743ABA8495AADD848F1F8B3114614463FFD7CE3E9726B9F13F2A4DA5DD7B761C484E2C98D457FF788BB5DECB6C9223F112BA6A5064854056D3884DFAA65A677C13E785CB30925BD5878A1515087472F285969D38B937458F7A8C968BB86D8AF7EB851EF950F83D554115E84D743A886F7B2922D581499B36CE7A049E35C9CB629889B626620872BDAF1B31B1BA08545CC57D2680B17E21F0FBA6EA16EDE8B956E497DF4C2960221FA3D697BB33CD592BB3D370834D9A5DD325ECAF88B87E8249CC70643FC807D085B357105B235800A0A7260267A9C1888D9CF620AE27315EF42A808BCBCE4D705C63EB5319530B228FD233BAB8C53F84277037A441ABC26EE386A06028BF75470D3B2CD441E93547F519DE930EF1871F96FEB3210FBADA58A39CE69417137A9EC019A12CDC5DD340B613F6DB2C08AD937EA3C31B553D40D176CA69643ED16CB525A1FBDA92FF6FA87528DC20022B75B99ABB49A5838022F271698EA91F25D3613A34C686712A9327ADD20F2324E3A32C5C33F234F879CD28024E312926C9B2D5C327AB26A29CE4E4200D23B4BA7CD541370AECFAA6A20AA025B969EC6017033B32798CC20E3A2C69725B5262ED9B8384110FFFE13687EDEE0AEACA60DEC2576CBE150508C25E69796B792F28A08DF7A1949FEA5FEA4AB9376CC4C3A604847CD69B1ADFD171983D6E894FD886CBB3F1CF3704ABA6EA413D0846CE803AF766B67A37A058F95818AF1FACC2AAA90DE7CD503A188295701F1CF204344E31EBDFBAEEF2521201DBFA905774C31791F7A766E4221611FA3D0ED8F0CC491EB9A8B8A9994073D746619FB2BD6A11F9770C0DD00B17D56234240CF014BE52ABB743DA0F9CCA508BEDE7BC5A011DA9F24C55F1AC2BFD18A813CA6A10985FCE51722211B8A6FFAA3C793D9CA4675F56B8743D454F78FA5EAA75CF80030905B844ADF4FB15EABB755FE5BC18523C8BDD6CB75BBEB3EEA082D9A7FBF97F409CE8B92F366195C80EB216C052C45A1915530BA7B9ED90C6ABB5B13B723759FCA390BACA96C582DE3B3C5D4AB46A9DB5E241935F12284ED11BA6AACA5988F39D2F0196DBE0C15640DD2FC44F206C60C936C53C4443F4BAC175BF5C60C3FB28ECC4862C03AB3AC197577B3CC4FBA1289733124E8E247392D7C87AD9E365A46003FC510DD9C71194600BFF8AD87792F251E9141E577A274253ABD987E239B69A59CE5277576236423865B59891BA9BB053CC215CF0F1885BF5E54E33CE85A9C2D915839616D44CAB2E34E1E4509F49FBA104E0CF58A8327A6A7C53A2F3508E34568923B4CD21E1B31D7B985A290D08422EFD30715EC0F6A0EC0DC7789\",\n          \"k\": \"2418BB42B89BA875664583EDF241327F3798379BD14B64351044F6C96B3D2C27\",\n          \"m\": \"F374D3C7172C308D7AC5AB1F1CE5BB9785B98AFCBF4E9120B42EA83BD3BB1867\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 49,\n          \"deferred\": false,\n          \"ek\": \"75F91DC864B819E71CE8CA50A7BB41AE94818BAB31B7F888ACE44071D2795361CA2B2666704721B02558212AB41A300B6D80B332A50448E36786D101A68A94D42325F718CCC4EB3521424C35E02A0A1A7A3514696C547E1918982B1066A3CB633E2B1D23280CDB333AA7B3C4BE7C18AEC44678D649C8774A4C552E45948801681F883165EB2C5E9D43CF38628E8C934C5F30819D4287AE4A51A797BDB0769534169E293C3A76A76361D79DEDA8068FAC187EF6B647C148E919141F02564B0AC4996B27794915A2D1C669DA50DD9B6E9BB57612CBC83765AA962068B8477D703848DEA456592A89B3CC321C3B86C55AB980BACD2F2605395BCF9E965B95D59DD608A8B8B85F66C8822F5B2DEDD36D3F97BC1503686F74991093A5799A146B434C7C2A6C573B3AD8C44FC517C92D042D623A7414D56DC45454CD08919FDB8D79A7316A4C14AA9961DA76C9DDC4CC6CDB37B474835E9A86563212F2B3908875588E2484BE8C4A49E3BC5EC609FB4BCD52D62AFB1B713B556CDDCC8BBD3B322E1766C627A0410A3D5E0B6F3E499272FCAFEAC90CDD100AB5D686F22023F8413F64521D19E38EBAB1CA6E31CC10278184F50843DA5CB1BB5164DBAB23374065B1043B1A7B1A241DDBD4BC6EA9057CC1A995644AE73231348414414C7EBC58BCB9B538A091434DC9010904BAB0613FA640C0A903BBE57A100B08A44A70507436C366268E7531C84A977B9EA2263E770E98306537B505A01284F653C066BA2F9FBC6CB3740258892076C01FA5CBBC8BE97FF0830BC746BA27555305D17A931371D04873CE2B89F5694BEF68515E7552EE03BB8E0517622768E6022DFD1044CA86A715930530E504D8031574087572974915885CD0351AF3610A5154559F688492579BB723A22F791B089C2CE3A383C09CBEFDC151DC266DCD54C196F25F0EE34092109C6AA6CAA76373E9125E143B2D0EA0874E4A35F06961DD812E60700F598926FAA7C1F7CA579CE9AEF3557F74E2039059258A001FC7F3B4A4ECAC6303672F12B9CC366E448124A27CA6FC43C6157A378535817B8B0C981809ECD54D92F135D0126480804EFFE40134E6C2190B95BC7C5E6A72B6487A6A454239DFE10AA99614AFA6A8F0E12FA86007CE464AED4063025B213D8597D412C9617AB62D9592D4320D7F97A6AC4092DCB311C21636358172C43B5626642336A96C1466BC30426DFAD5C0DEA50F715B19827B1935C79CFA1C9CA1F799F3954EB1C0720D134CF9A3122263B995E9268C3A1BE18A7112ECA0C4914F2659734A31A5D5708F3A145E6801C18761A280465C2D4600B9D968B3CA61EF9C7687FB1597499597ABADA930AEA4666DF40029F645A601984646144DB4CB68B3466CD7DA154C027536886A390234CA792FF6AA5C7CA258831AC1B0FAAFC21890BFE522A7685837A550CB343B79B2955C4ABF4939792133AB0D4A77C909BE0F0233AA80AC6B6AAB0F8102F374982C6BA88563ACF7254760263D7EC8C302F656BC227B3B558BBB444365A2CD95B421DA5914417C50E84311C6A2218A49169866518C201622F10970D75D38815787355A0C6126EBF189AB30321C15898AB117F6183C47174DAAB7BB8F96C3F4F239B642516A48F015E838A3DBAA500DE409C13F28FCCE5F266A98ABCB2D92E1BE99E438BF\",\n          \"dk\": \"95A85BE67984E9D6BEA070223A469C08D73C6012AD76261380F45A7DA43CFC831E92D55C566BBDA6E6B1BFE83CE7E5523829C5D3C2B5CF874FED240032B43A011823D38856E519868CB0A9B0433A51167835568EBD67A2A6991B8C529E9BDAA0A4A95BBA93480909AF3931B969869C3A425669B412C60B5AF2D6B874C98E01DA95FD0672599303E25CCD42D7B19B73508CF286FAB706074A98437B7C9B7357DE242200AC71FA8636381623233135E530BE92E83133092B17A26F688919C5A45634971D129542FF2362E94243F2249B65C9B2F9729FE6425E76E36FB88019C2A863CB617C58DA9209A92C2A405E35574198E83739772F63C97CD90753795C981C81BD3CFA384A6CC51DA15F21D7BDB87B6CD2BA10F928C9AEB1595A4539C8A3600331B07256680E4C0648A0A786747E00672B08DA47F29A3FA7D9CC2E119BC6EB579BB30037F66ABD645951F499FDF12ABB3C979FF90E15A5397D2358BDD6CEF02B5658B44242A74573D41C216554CFCC2CC4CCB48BC488ED5A450210CDD0FA1D04831E14514BB93148E01BA975B71E5463001CA8330AB2064F31346C716ED431BD91A477C660535D88B32E3AAB39C4A9DDC2BABDA392FDBB3DDB3611E06A42E1AA18F985B8D202C23A5443F5B6C161579B6FE0C2A1A20347B2A48D77CE8F954F7EEB193470C90A66B3EC7266AAE78BCD2469A5573ED4D2CFB600A7764364ED47C4C3DB97E5E5170361C09BA07E7B3B16D07432A7100F0615506D32771A7258F1D33B71101941A148C6368C9DCBC403037037F6BE9620BDFFB418BE497942C410C6E6A957E604B0F4467678B83BF9BBD628BF5929A63C6170C065B6F5D84D7E2BCD8EA019ED72A600E6AED7187A1CAC1506F95127CA235AE9ACC89B2CD709C18FD895002C3B2400C38DA16DFD7B2221D9BB212A471475845A58B7342830DC49B6676516EA9BBE4C458BE77B6A99E518AB1C1BB8CC54B50392FAB00367940B790C6AEE35B587E6060ADB3C92FC579AFB459B3992B6E110776115ECD03C625B9DE1378C85FC5EA0C8AAB6818D123A5F20866984D4C6D261156E78A15D322E9BB4B5B288B6E6C03ADD4A9285ABBB460C9D2C0629AD7CC48351BEA588848D95283995CC7C081A8BF453EA62B3AA48608AC2BC02067C93D01CBF1531C0B35F52C32BF1748E56185FC4C24127E9CD94A9C4E8AB12C38086F5A17B0234A41A6C772466255B1C8F95C4BB4D7048E9A542D8B7B36DD59CB3F48F08891B2BC6590A179787CC83E41453438CBC34318BB0B072AC6A013571270194508986BD5D74724BB22724B2B18AC92A3939C9589A03448B8C8E812C4E2486501A97ECFCC84D3A32824160EA183C16F3839FB20CA8FAAA4ED54FE9625993F1A064809537525F9872AB31EC9B6AB922CF6806714959AC63A89F51570A98033585A99E72713DA18A34C57A2E5A558F56A04E185864398C56888FFE66ABB96BBD12B39DFEB0909D0A932D6943031693F0296373246D35767E74B4C29EF52ECB1B371B77AD5588AFEAD9CDD978331B7A51E4C4CFBE9B17550C20DB02742EA8737D8C26214791F4EB3D7CF1329E45600542560E9A350CF74412F1681178A0ABEB6706595084A19D8FD5C99811A275F91DC864B819E71CE8CA50A7BB41AE94818BAB31B7F888ACE44071D2795361CA2B2666704721B02558212AB41A300B6D80B332A50448E36786D101A68A94D42325F718CCC4EB3521424C35E02A0A1A7A3514696C547E1918982B1066A3CB633E2B1D23280CDB333AA7B3C4BE7C18AEC44678D649C8774A4C552E45948801681F883165EB2C5E9D43CF38628E8C934C5F30819D4287AE4A51A797BDB0769534169E293C3A76A76361D79DEDA8068FAC187EF6B647C148E919141F02564B0AC4996B27794915A2D1C669DA50DD9B6E9BB57612CBC83765AA962068B8477D703848DEA456592A89B3CC321C3B86C55AB980BACD2F2605395BCF9E965B95D59DD608A8B8B85F66C8822F5B2DEDD36D3F97BC1503686F74991093A5799A146B434C7C2A6C573B3AD8C44FC517C92D042D623A7414D56DC45454CD08919FDB8D79A7316A4C14AA9961DA76C9DDC4CC6CDB37B474835E9A86563212F2B3908875588E2484BE8C4A49E3BC5EC609FB4BCD52D62AFB1B713B556CDDCC8BBD3B322E1766C627A0410A3D5E0B6F3E499272FCAFEAC90CDD100AB5D686F22023F8413F64521D19E38EBAB1CA6E31CC10278184F50843DA5CB1BB5164DBAB23374065B1043B1A7B1A241DDBD4BC6EA9057CC1A995644AE73231348414414C7EBC58BCB9B538A091434DC9010904BAB0613FA640C0A903BBE57A100B08A44A70507436C366268E7531C84A977B9EA2263E770E98306537B505A01284F653C066BA2F9FBC6CB3740258892076C01FA5CBBC8BE97FF0830BC746BA27555305D17A931371D04873CE2B89F5694BEF68515E7552EE03BB8E0517622768E6022DFD1044CA86A715930530E504D8031574087572974915885CD0351AF3610A5154559F688492579BB723A22F791B089C2CE3A383C09CBEFDC151DC266DCD54C196F25F0EE34092109C6AA6CAA76373E9125E143B2D0EA0874E4A35F06961DD812E60700F598926FAA7C1F7CA579CE9AEF3557F74E2039059258A001FC7F3B4A4ECAC6303672F12B9CC366E448124A27CA6FC43C6157A378535817B8B0C981809ECD54D92F135D0126480804EFFE40134E6C2190B95BC7C5E6A72B6487A6A454239DFE10AA99614AFA6A8F0E12FA86007CE464AED4063025B213D8597D412C9617AB62D9592D4320D7F97A6AC4092DCB311C21636358172C43B5626642336A96C1466BC30426DFAD5C0DEA50F715B19827B1935C79CFA1C9CA1F799F3954EB1C0720D134CF9A3122263B995E9268C3A1BE18A7112ECA0C4914F2659734A31A5D5708F3A145E6801C18761A280465C2D4600B9D968B3CA61EF9C7687FB1597499597ABADA930AEA4666DF40029F645A601984646144DB4CB68B3466CD7DA154C027536886A390234CA792FF6AA5C7CA258831AC1B0FAAFC21890BFE522A7685837A550CB343B79B2955C4ABF4939792133AB0D4A77C909BE0F0233AA80AC6B6AAB0F8102F374982C6BA88563ACF7254760263D7EC8C302F656BC227B3B558BBB444365A2CD95B421DA5914417C50E84311C6A2218A49169866518C201622F10970D75D38815787355A0C6126EBF189AB30321C15898AB117F6183C47174DAAB7BB8F96C3F4F239B642516A48F015E838A3DBAA500DE409C13F28FCCE5F266A98ABCB2D92E1BE99E438BF3220B4816EF8681B4DB93059811DA8B0D65AB12AB874E57F3B09C33BC6C20A028449B1C5A6D50E3AE0E604C9CA666594335BB1B083669CB54EE7E960D8905C8B\",\n          \"c\": \"36DC4E1498A52C255C5AC4A9A1AE3F17F8472B71548C919FB7C2F3ACF0A35D6FA8584307282CB7FC9B5B04E5E1047BF03686981708A62ED593B6DAB954F39670036102A0BB348839857A68212C783319678819F6CA6CE1191B34D86157838523BA0A69B7B695139C17C21FECFEEC191930E81C66D1177A277E77D714B2EDCC33BDC89A7F14A6B83862300AFA860F229FABA58D2B2E8C89C734591E7A1CB1D65D87C6A0841B78ACD1BF05BCCCDED0F27B38DD27E57EFFF3BC4C3E80ABD4A78F85D233EB8046FF2BB7EDC87BABBD280D1F3650A3BF621A23E0C3A7BD42F8AC90B0F741EA3B3E8D65F0B1774FCCAF35FC809514F5D4EF303D821A2DE926BD700B085B92BDB91E8D04B6EC23C8000D8E1426EB7743D999B845E79CE993A83F202E2E82B0B491DC6BB006D9BE69311BC5E980A95EBDFCD65DECDF42B887C29281E8F8C19607175080D04485E5DFD0F84219A5FBF8E94C7C16C44F10274166CE965AEA357AC9C5A2BE63E47603F6D4D5469308793D8E3F6ED473AEA36C68E3F9FDFDB97996B34020978BD69410961FB216DF9F3349CC6B6028ACD3255EEC2C14F2341858782B8C34D3141A19F8A64E989F70A6B67B200E8E482A2E271077489B7C97D567449E2543CD5735D7A70DC1FB26539B996A550DB92C9857C9687492238B108B40548C22A5442150604D0C097FB02061F5E45552B4B443E7F31D85CB5BA9BAEDCD839445613E9C9826F1427755A36988A7E175E729D801468F986B6A0553C00562606B032F7580D51AA5DB6CD214D69E8636A8FC957E7D3907CB5A33B2C80780B7D782E040B9914A7B8EC51AA7711B9D27581FD2D5055E618467D2EB94B4FEECC66C96268C62222E3F438293C2733EB921706F1D669B55C3EC9AB54F6507A4E0D742494B8788988E9A628A5F5C1DCC2C5AA659723006BDF0ED4E70A8CC194416E11F5BC812599ADB937A25B302F6A371808443377EEF2E2A3F8C7A3C7294EC7D7FFF22DBD3BE07F89207B30228A20DBF432CA96549794514BA4BD27C0FBAAD1290505471E7BBAFE53E039B7476D4DBADF13F1DBE83A9101F58351682AAEBB435C0AD8FD6148981AF7885A9F49E7A802BF226182597EC5BE5DD5E0381C1D72CAA2678D0FC4B354469669C271D331C328560055057B25F92FCDE0BC378AE6886434BB85EC96FEDFA369DDCE4F36F8AD8FF79894A1628DFC68CD53542EDDEBC0D92BBFE2F7601621693B10AA2A3CE2ED2CAFCE6BCBA4E7B0BEC2E5656F1ED4B751F081BD4A0356B89815F724A823030979AC1D6C8B1CEB63980717EA24803B2D6E3FDD9F5B4A3397F4DA86834D99FDD91661CB1A808A197F9FF122B2843B0E4230230604640D82FC26AF612AC980E6516E1B6C5724B579154EF2F770983C0BA33B832C14B691282B9381AA708E49D812F7EE180B0F9858D4D21386C5D60490083ACA768181C2840E5EA30833C3FFE79E2747892AB225703396D792BE152DE2E8802264B2939296A9E1733883F430F372455D520B839C0DFF54BF76F2BA87B4A5960\",\n          \"k\": \"2F323DFA37A737802227ED21012FA0BA624F532F8A3DD979AEFCC554C1C2BE92\",\n          \"m\": \"DD252F728FC9553CFEE90924565E984C8E1462CDE58AD8C4ED8DFCE98A7F39B9\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 50,\n          \"deferred\": false,\n          \"ek\": \"19E628A96B033E4358CFA8C3C4A642D7127C16A50BEEE5C57A25476D8BB86D019DAD84C613D96A389802EED2031B1A07D478612DB405BBE9BB42A4BF9E7AA91CF2950D00013C4CB7A9B0CF88A87B19E41995EA9EB09660746B1A81A2C74F0BBC28287851CCAE8CB8118ABAA627F44289A9C355DB863095B397D1C449C20AA60B5F1EF94F9002B212C733D81C5A92631CD3B661B11056C9026E84A34E19836411A76AA53089071194E2C9696B31BE8351CB735521A42CB83C52C421724F2FB76D245BB5571358314A2967604084F64AD30911600B33AC9B236DB66487058F35611D0894051F31C032972A03A1CA5822317F953F91A74885E21766824F0FB601C593BC64253E7C36639462721C232D32A076EE245B9A7BC96C0A70ACEA36F8F41E1242AB2D00593BDA4C550018B1345D0806A3D1E8C7A8CC29790127370754A24A73E3A4B08DEC3A88C04C2022235E55CDCAF80938B579B63123E306603C781FF3ACA1105076AA2BBA1D966BC28BB3F3A7340D210D7E7A1735B01E749C0AE990775E2306AEB80229B918542608B7E281B6BB696E1C82342BA196D15EBEF20D541CC57CA928A9DB41E972CC1CD333BC7A64952247FCBC578860191FA67971A29E40C06B81CB5A765C7B26522C19A63C40EC31EBECCF7DF44CDB8B4E7BC805D42A28ACAC3EB2684ABDF46DB7E4C5B01A770B4C0D79D7190B97461BB75CD4A78574677CE26BC30CC25215B1C20C290A1575790C86AEBDEC7993CC94CAE23D5FA2871A184E721886BC920CAC11607FE96878971F161B788E5653698A9F8981339E581969C1B61A6073C02CC3822A2A2D0A6F5BABCA0D57175B86942CFAC2B0FC9F1684324A4139CB1342D7A6B26643A923FA14988173E58A82B67532C6AB89C0D5AA1CB47E44703FD7AA6816501023229B1E76833D7948F0743BE373BCD361130899010727CDACD4347B3A6645541BC7173361860953E61F5824BA2ECB94101383DA6A95CB4591C1BAAAB1B221F43B91D7985A173A0FDA756FE9B38ED1EA4F2A428E34F352A37A45EA83998B66C4E60B40650A87FF5185DCB0055B9C011378442462C285D18A0DBA658DC5B17D1620CE566C86879F38754CC8C1BA014827B444CCF8E9029A7763318CBFE0220DBED60F6720C6F96B4E70C712B58091BE3A6C1C4639D1ABAB09674D2E653DEC768D728BA593AA3C61B17DE17A07B3E85558F38DBF848D65AB3A1AC183A62AAD4CB18344E71BE4C97FEB59001F4ACA46342AAC389B22C47C2A216D9AB50B1A02405DA01494C44184363C31F5C39AC90507D94093F882E6891E24571BF39932DC5482554C12AF6A5F0B3AB4CBD276D6D850894CB4F414196D511731D23C8E039282F6C11D089BEBFB91F0A766935B3ED66458842B4C757585FA6657947529294930C886A50689CA5DF1AA39B8346A14150F051E5462A99E56CA383BA5ABD455F00AA9BBBA660FCCA2C5A8A74299581C274B89E4756552570A33514CB46CD7A3AED34A2DA42C49DE1A8645D64AF3D644BE586068A1B5804965BB7A6134CC8DACD82FF2E954625C92B0D9763E22CCF7450A8FA70F5793400D2832A03A5A75D404D1E2822DA607F6F9C4FB510858978372C621AA72399A34B9E2618F97EAB82B56D93E51FD73A90A78E2AC85826B8E6335330DEB8C644A29A1\",\n          \"dk\": \"D9758470207110D1C1DE602163F955BA262CCF3564AFD86DA03047AE9C63F5A67587E008CC3423D221949505C534B6998900114A3B6775F1715AB139CF4C8C5849A034929F957407C4512091930A5B9A38846402AAB49DBC39160B0ACF4B6A3E9EEAA095917828F28F667C9904B8A7BD4C231652A32343BEA81A7EEDB68AB1229FFD16CAA46B67B9F264150AC2DB97CE09B55E4312654E9012F2699243F41B380073C6A5985F87125DC199BB05620455A2BB77222764C9D7966CEFF2467D632964567CBD1B98FF062CEDF1C23A31B891C48C0B3833660C3C39762C4A470C85F802C5A03FCE328B21A658391A7B417B2EB5B37EA1262817EA78BB72C833C08CF74C9C34075AC0B975AD6ABF2DBB1AB72A9EE5758A1E47B1E5B41506D39995D04CC942A0C2306BF270CD43E1373F82B2DFD8A0294BB2AA37C762684969B2046FB04DA621AD990336E2FB3E53780B311872F9E581B1890A67EC4951C071FD7091DF250F06F367220B0EAD226286E6BF1DCC63D0E85A2C64B39BF924C3716F045411A54A31EEC41BE1846E08649EF19C78E1A938CDB1034BC5879CE91444BB126DACC12EA2A32D4C5749E3C6F9E270F1B4B0E2244E3308014499CAE3640E96E4ACD5F1C641D5B2E1CA4B9D65654CBC2A53499E643A9E9E1A2453EBC707A20E070793FDE87E12F71FD060186E5690E5B031F858CB991714DF959D6269209454360471C4E1E365DED681636B02592A2747F78EFDBBA0841C54DAF980CBF728EDAC5EA7B9556AAA7746CC209408495DA8547E392A4A02887618A7F7C25537596254D48E9AD94A52E1A334A06D2A22A358D1121C9A81AA6C1EFC08B3C8356212F259699163EBF1A31EE845E13AA10652B41580BAE9D683E8B958BA95257E947C12B04DC73299EF510FF5C94F598524CE790D21372728B38AFE966AF114C2DBB094BA216F65D2476E6B5898BB2C72A0C8C1311A81DA95C0B03D942C81ACB73BF13A7B1C22B289C0A5DC7C8E9A71480600BE6F5A14BC7340C9290C03F984EDA2A8499534FD0BB96BA1B4D1335974350A32FA9911396C3BEB940AA1A87C86C3F08C393A18CEFE7565EA6402F8C0914735AE938C3977576CF9941070583015A8499E84C588F13F9BB29FD278659759A08978585CB33D213CAC9270BD7AE3C6A1612523C8ABF8F7C4D3EAC1F6F7860F02C9316005C647B30B7C3E75CB73CBA301F2DB327C0B38F7223881B70456834565B3B0461B030EE68891F22F09129AED4613EEE57DBF0ACD9745ADB89633EDE163ED1C3CBF935C5EF90E4591C7E0B79E2CD629E9A963415B1071256BC2C3905BA30516D01B3CB1C10101830095BBA6D08E7F2006AAAB4B11852A0E2C8D780001C99899A65B96E6BC3B68D84F28F34FE64A9578C078611AA507F15BDBB99DB9F943768B1D4DD6A335D6BAAAE33288B16EAA6496C312B0A08B052F9B9552E23A0EA59E9FE329D461A8CAD9176BFC7006E20985157023BB503599A1798AA89D406FB3E4586F230F797CA17D121CA4AA568BA579AFD8575C801A734083F9214CACA3C6CF3ACADAAC27BE1807432C11CAF806C66953EC6A3F24390FA4AA78F5B90BBA061238F9817FAAAB5D2AB0CE8A3CBB6C686645C6A08CC919E628A96B033E4358CFA8C3C4A642D7127C16A50BEEE5C57A25476D8BB86D019DAD84C613D96A389802EED2031B1A07D478612DB405BBE9BB42A4BF9E7AA91CF2950D00013C4CB7A9B0CF88A87B19E41995EA9EB09660746B1A81A2C74F0BBC28287851CCAE8CB8118ABAA627F44289A9C355DB863095B397D1C449C20AA60B5F1EF94F9002B212C733D81C5A92631CD3B661B11056C9026E84A34E19836411A76AA53089071194E2C9696B31BE8351CB735521A42CB83C52C421724F2FB76D245BB5571358314A2967604084F64AD30911600B33AC9B236DB66487058F35611D0894051F31C032972A03A1CA5822317F953F91A74885E21766824F0FB601C593BC64253E7C36639462721C232D32A076EE245B9A7BC96C0A70ACEA36F8F41E1242AB2D00593BDA4C550018B1345D0806A3D1E8C7A8CC29790127370754A24A73E3A4B08DEC3A88C04C2022235E55CDCAF80938B579B63123E306603C781FF3ACA1105076AA2BBA1D966BC28BB3F3A7340D210D7E7A1735B01E749C0AE990775E2306AEB80229B918542608B7E281B6BB696E1C82342BA196D15EBEF20D541CC57CA928A9DB41E972CC1CD333BC7A64952247FCBC578860191FA67971A29E40C06B81CB5A765C7B26522C19A63C40EC31EBECCF7DF44CDB8B4E7BC805D42A28ACAC3EB2684ABDF46DB7E4C5B01A770B4C0D79D7190B97461BB75CD4A78574677CE26BC30CC25215B1C20C290A1575790C86AEBDEC7993CC94CAE23D5FA2871A184E721886BC920CAC11607FE96878971F161B788E5653698A9F8981339E581969C1B61A6073C02CC3822A2A2D0A6F5BABCA0D57175B86942CFAC2B0FC9F1684324A4139CB1342D7A6B26643A923FA14988173E58A82B67532C6AB89C0D5AA1CB47E44703FD7AA6816501023229B1E76833D7948F0743BE373BCD361130899010727CDACD4347B3A6645541BC7173361860953E61F5824BA2ECB94101383DA6A95CB4591C1BAAAB1B221F43B91D7985A173A0FDA756FE9B38ED1EA4F2A428E34F352A37A45EA83998B66C4E60B40650A87FF5185DCB0055B9C011378442462C285D18A0DBA658DC5B17D1620CE566C86879F38754CC8C1BA014827B444CCF8E9029A7763318CBFE0220DBED60F6720C6F96B4E70C712B58091BE3A6C1C4639D1ABAB09674D2E653DEC768D728BA593AA3C61B17DE17A07B3E85558F38DBF848D65AB3A1AC183A62AAD4CB18344E71BE4C97FEB59001F4ACA46342AAC389B22C47C2A216D9AB50B1A02405DA01494C44184363C31F5C39AC90507D94093F882E6891E24571BF39932DC5482554C12AF6A5F0B3AB4CBD276D6D850894CB4F414196D511731D23C8E039282F6C11D089BEBFB91F0A766935B3ED66458842B4C757585FA6657947529294930C886A50689CA5DF1AA39B8346A14150F051E5462A99E56CA383BA5ABD455F00AA9BBBA660FCCA2C5A8A74299581C274B89E4756552570A33514CB46CD7A3AED34A2DA42C49DE1A8645D64AF3D644BE586068A1B5804965BB7A6134CC8DACD82FF2E954625C92B0D9763E22CCF7450A8FA70F5793400D2832A03A5A75D404D1E2822DA607F6F9C4FB510858978372C621AA72399A34B9E2618F97EAB82B56D93E51FD73A90A78E2AC85826B8E6335330DEB8C644A29A16A3A54F67614A889B92ACBD1D3EC4CBD6C46E8B33FBC2F3C92DF3887DC1DA71A003B9B894A4AE13E6F46DED925CA80189437C0910FA73E146A646178544922DC\",\n          \"c\": \"958FB0A1F80268072D82D593A66B039E548E205C63A4689B48E0752F9041E5D1C2246EF6A1BEA2773CCCFF2A80059C651DA70EAE2AFCF2E83CA15AE29684A11213C01E0DC9F3A25B492A8B44AC188D187A24A6A30B82AA7BD80CBFB992455F280D0D36D2E0A0B3EF65606D55922D29C5A0920D57F8F6EA2AD518B41CCBA23BCC35BAD2E844B5D9000E36AE1F1DCB3D3CF23922D82446506038D5C33925AE96E174876F0C96220BB775EBEF7AD0E48A1E1C785633F6B3585F5FEDAF521F8343440981FCB72DF5860A42343824A0F43A0A371E7D41472199F749B310AA32A5769DD29328E60A7424FAF2D90E6FAC34653B5F59602785FBD09BE26D184B61447327F3772A072AF1A7D4317722AF139EE56D4A7D765D6D73D7B13DA6BF4ABACD1B82320ADE56624CC75BA78278B38F4F4E17FC704B5C88E4800A6E4F626DF21974E2D76AD9BCF70BB9826D790D8A7BF7F23FF9D03021D9B6F48D89D855F4D070CEDA92B28915EF2F77BB79BF80D9E03D9730F1B018AB3AD932588805E995010134774C9C061D11BF852AA5C7403515956D29924EAA1B6BBBE4E4BAD554F4C91F67A268744A12FBE28C3D7A403776DD742431EABE6EE093B988B7F5ACCFF53AC714E03306773F9E54234BA12B1F35421CEE91D81E8A2F91D8AF62733A97A93B1FC2053216083044156ECBFB9919B25C9F3187E07FAE83E9C662AAD0571505413664FAC5ED94C5407CA06036B8C377D8D7D306E1548539B5219CB3DD77D78475EC2FA6DD4C06121230D99151ACE36BF3A3B8DEA00636EE1F6E80D3604E76B3A8E13395B4D590A389F492931A6B37EE27DFB145C1F5C6EEF8615E673E6F6A44BB69F2C19E83A1BCFB3ADAE4815C2DFF7C8A6FA39D41D2A95BB4452D3AF38221754A41E132BFDC70B6B8097D2EB19DA00D55D8DFA80C59070D325766831451B932C2686A71399DEB0F8CF3EBD32C1F6360C1106522BDDD3F3D061196372BE337BF35040A57D1BA098AEDEC56ED240B8AEED2E8A8C0ACEA5BF4109CFD6C55DEF7883A848132BA492FBD364A8B0BDE322EF62D907106F07BD44D97D07AFBF9AF057D7397A2FDE829123FABECC1890B08C18EAAAB2F32B76A7099B3DC09EBEE6B9774AAEF06EDB414E5DB1559CCFA65023D203EE7D867AF48931F06BB86B3AD7E9C0D52A8BD97027A9AA865CDDAE57853BF8F3AB4279533C5D83F240F181A748BF0761CA97130D59C751F8C87544E93E98BAE954EC84AACE8F70E2E6BBDA10389C825E29A147415FAE1B75DF6F1F17191458081EC5CC791250805E5BC1E51273AD723BCA56B3C99FC51EF8B304B960928BF40737BE291C9FB3BC1B051F8D40DB99EFD3260E0121C66336F78901451D3A2D1C2F080FF3EB5480295E949BE9CC5EAB852483B6E7668C6414CF10F6B416603D82F517224FA679BF3057EC4F6DB856A010B74769F78171156A7A604B1F63980A4AA1C8C57442A1596FF8605F3BE684C1D0841F8F4016EC54F4BE2527349B2F0B21E926DC01D324D5842ED2A69290E908F5ABC8855\",\n          \"k\": \"C44EE4E3EB80C46D6BF5CC3E08CB93019C8C80DB0CBE89708E8A6902DE87B699\",\n          \"m\": \"297ECD18E2880A596F572B66458410A0D827851EFA55F1C9CC513F7991F0DA0A\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 3,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-1024\",\n      \"function\": \"encapsulation\",\n      \"tests\": [\n        {\n          \"tcId\": 51,\n          \"deferred\": false,\n          \"ek\": \"307A4CEA4148219B958EA0B7886659235A4D1980B192610847D86EF32739F94C3B446C4D81D89B8B422A9D079C88B11ACAF321B014294E18B296E52F3F744CF9634A4FB01DB0D99EF20A633A552E76A0585C6109F018768B763AF3678B4780089C1342B96907A29A1C11521C744C2797D0BF2B9CCDCA614672B45076773F458A31EF869BE1EB2EFEB50D0E37495DC5CA55E07528934F6293C4168027D0E53D07FACC6630CB08197E53FB193A171135DC8AD9979402A71B6926BCDCDC47B93401910A5FCC1A813B682B09BA7A72D2486D6C799516465C14729B26949B0B7CBC7C640F267FED80B162C51FD8E09227C101D505A8FAE8A2D7054E28A78BA8750DECF9057C83979F7ABB084945648006C5B28804F34E73B238111A65A1F500B1CC606A848F2859070BEBA7573179F36149CF5801BF89A1C38CC278415528D03BDB943F96280C8CC52042D9B91FAA9D6EA7BCBB7AB1897A3266966F78393426C76D8A49578B98B159EBB46EE0A883A270D8057CD0231C86906A91DBBADE6B2469581E2BCA2FEA8389F7C74BCD70961EA5B934FBCF9A6590BF86B8DB548854D9A3FB30110433BD7A1B659CA8568085639237B3BDC37B7FA716D482A25B54106B3A8F54D3AA99B5123DA96066904592F3A54EE23A7981AB608A2F4413CC658946C6D7780EA765644B3CC06C70034AB4EB351912E7715B56755D09021571BF340AB92598A24E811893195B96A1629F8041F58658431561FC0AB15292B913EC473F04479BC145CD4C563A286235646CD305A9BE1014E2C7B130C33EB77CC4A0D9786BD6BC2A954BF3005778F8917CE13789BBB962807858B67731572B6D3C9B4B5206FAC9A7C8961698D88324A915186899B29923F08442A3D386BD416BCC9A100164C930EC35EAFB6AB35851B6C8CE6377366A175F3D75298C518D44898933F53DEE617145093379C4659F68583B2B28122666BEC57838991FF16C368DD22C36E780C91A3582E25E19794C6BF2AB42458A8DD7705DE2C2AA20C054E84B3EF35032798626C248263253A71A11943571340A978CD0A602E47DEE540A8814BA06F31414797CDF6049582361BBABA387A83D89913FE4C0C112B95621A4BDA8123A14D1A842FB57B83A4FBAF33A8E552238A596AAE7A150D75DA648BC44644977BA1F87A4C68A8C4BD245B7D00721F7D64E822B085B901312EC37A8169802160CCE1160F010BE8CBCACE8E7B005D7839234A707868309D03784B4273B1C8A160133ED298184704625F29CFA086D13263EE5899123C596BA788E5C54A8E9BA829B8A9D904BC4BC0BBEA76BC53FF811214598472C9C202B73EFF035DC09703AF7BF1BABAAC73193CB46117A7C9492A43FC95789A924C5912787B2E2090EBBCFD3796221F06DEBF9CF70E056B8B9161D6347F47335F3E1776DA4BB87C15CC826146FF0249A413B45AA93A805196EA453114B524E310AEDAA46E3B99642368782566D049A726D6CCA910993AED621D0149EA588A9ABD909DBB69AA22829D9B83ADA2209A6C2659F2169D668B9314842C6E22A74958B4C25BBDCD293D99CB609D866749A485DFB56024883CF5465DBA0363206587F45597F89002FB8607232138E03B2A894525F265370054B48863614472B95D0A2303442E378B0DD1C75ACBAB971A9A8D1281C79613ACEC6933C377B3C578C2A61A1EC181B101297A37CC5197B2942F6A0E4704C0EC63540481B9F159DC255B59BB55DF496AE54217B7689BD51DBA0383A3D72D852FFCA76DF05B66EECCBD47BC53040817628C71E361D6AF889084916B408A466C96E7086C4A60A10FCF7537BB94AFBCC7D437590919C28650C4F2368259226A9BFDA3A3A0BA1B5087D9D76442FD786C6F81C68C0360D7194D7072C4533AEA86C2D1F8C0A27696066F6CFD11003F797270B32389713CFFA093D991B63844C385E72277F166F5A3934D6BB89A4788DE28321DEFC7457AB484BD30986DC1DAB3008CD7B22F69702FABB9A1045407DA4791C3590FF599D81D688CFA7CC12A68C50F51A1009411B44850F9015DC84A93B17C7A207552C661EA9838E31B95EAD546248E56BE7A5130505268771199880A141771A9E47ACFED590CB3AA7CB7C5F74911D8912C29D6233F4D53BC64139E2F55BE75507DD77868E384AEC581F3F411DB1A742972D3EBFD3315C84A5AD63A0E75C8BCA3E3041E05D9067AFF3B1244F763E7983\",\n          \"dk\": \"673751CBB596541131C66398662CB4B0EB80796A88B28144A5BBC854F80D4B35BE0AB241E4795F8FBBA814F50FA80498CBE8BF68A0A583A4C5981B41DF0667DB614A628C3060697438E62C8D36026EE29C96B673BF1A194EE49481351F4D1748DD01CD023142F01057142B741CBA8302E432F88C63D0B4B5767AC3A5A59AFA3A321E65B1D1511807A06E16A04B2F1070E465586D4A9B68E2B42D57A356FA7BB3D04E51B193FF4C757CFA0F15924EA6E49AFB83B2919C985869ADA544338F44AE96A874C425AF87BC73F3CB0FD2627B1539B1F19A77E36B7FC817851D39BD8A069A6C2202C17469D421A588E65DAF450030B6674EC1C734AA25414B119E61B26EFC90DF81059D2B9599414F93692BF45A4B1C5CC09EDB37B1B1433026AEA6B0200722B819C7BC061C53A4304992FCA2AEE2324A324AB91C3E5D562096B8A141756940F15A2800C274EA4F65817E639C5D2A278C6A294F9DB331F84CCB0A10309F530A06EB962573C86005C15BFC7531A143026396721297E25CB655A294964B2FE531905F2802376B8ACE35AE3E2814BAB7062BC1A840657DBFCB5F41BB55475697849A31E2222E995518CA7640AD4B9CEE9820984138BE0510FFD6AC225393A5F0CB030528CD2A0610E78A5CF1B073039A6D143068C53DBD15A1D4446DA7B310EE795D1FB31B2F97008F83BDF348A593A3BDCBB571907B36D0978162C253E6F50106C463149834ABFB0707D8AB4A4BABC323598A085B309764B7C32C9DB0C9F2D52EF2F00BACE7846868C33B82AFA430A4C2F67B698A60526A161CD62115DCA767C203E3E2CC787031A73B5B7DBA1EEE5AB04B77BB569B952D9A15D198779804197D23C18E5B055F5C8087D742F64418D6505E70418ABFC6B1BF7BB3DE286599F4676CF87946D65144998AFAE1C689449E3F349FD0809AFB856DDE4A94A2C0258D56432F40C3DA812D3FD3B72259A61D2882E0F50B355121E564C6BD33366F32BF4A5996B9998961354925A2BACDF48056118453AC3792A7879B71579ADB65F5D83B1ED6C8C49836DE379DAA027E62B96F683C1688935CB3FCCD64329267273E60C6CD59BA1B7FC911E2662527ECCB7A474E5EF00CA9F789A3838E889242E7FB2B08F3790613C4EED3C912EC4EB029B971096B384727697B4DDC3B698C9A6DA6971FA4C574ECD18EB1C84C0C5790153AA6B9DB61D8BAC0A680A37ED623582A7E8C0885EBB35AF341477764368E0647B14553672316D0B90317C5B53AA747E61B4750DB9E63CC3712900005CA24226B523E0A179582C85968C107857BB41521B7342B13DCAC462A53BE38446F2142519667B48B1C68FCAFA4D3C7E3E5AFF163C41F2C1B4DBAC5456C30776078E7C3A713819F6B9ACA55D77D60637183A723035730F94285C42AC3587637F66AC30F2C4039E60420967576E27B96C8C004D9585F33939AC44F0D195B35D472FC219076F12D0984AC844728D5D2266BB5CD8B325DDA497B4F397BFE722C9D7684201A921F502271985CB3F31C04884C090B063631253DC454537031F2C82C10A1722DE6C556464DC9D64389DA37E469480C921065C79A30C83C867C952B30548A6B5BDFEB6EA6247480F163B427B17CF94889220FE934564DAB90F5B6A11648870B654495A6691AE21FEA86BDC8C49093FA07E926AF3ABA0E7CEC21F613B49986C6C8A139EDA70B7ED8211A3215E8C43EF8C151AE61740EF83B48276033614B58E9CEB992233CD21DFF70C7A6F7171707A2ADD37ACBF136A4EB4A79517FD0C8AFF0B5126435C3100331F208A546C9A4044A8F0503C8ADE9506A018B4CA7C6E8D70120017D38B13B52786A85A540D81B8E71C376B796A7215ABF065086D3C80EE94B8F09E2A3BA13B82583B825388E87BA010AF507173563789A1DCD088907C52BD7FC1C6930605F060F37978211C10FB5717E3FA291D20B5D43FB74CD4711394B0027E41C52B523797470532CBE123C92950720E5E255256577D4E156EBD4C698D813405C61430B978694ACDE78031E74BA1D8517DAE2346F008411231FCCE7BFF75BC361E691E776049004097B36490D876288701B2D3A1743AB8753D47AC6200E2DA7458D3A059681233872794E6720186B20108B1D1033971CE19ED67A2A28E499A360A4AD86AE4194034F202F8FA3626FE75F307A4CEA4148219B958EA0B7886659235A4D1980B192610847D86EF32739F94C3B446C4D81D89B8B422A9D079C88B11ACAF321B014294E18B296E52F3F744CF9634A4FB01DB0D99EF20A633A552E76A0585C6109F018768B763AF3678B4780089C1342B96907A29A1C11521C744C2797D0BF2B9CCDCA614672B45076773F458A31EF869BE1EB2EFEB50D0E37495DC5CA55E07528934F6293C4168027D0E53D07FACC6630CB08197E53FB193A171135DC8AD9979402A71B6926BCDCDC47B93401910A5FCC1A813B682B09BA7A72D2486D6C799516465C14729B26949B0B7CBC7C640F267FED80B162C51FD8E09227C101D505A8FAE8A2D7054E28A78BA8750DECF9057C83979F7ABB084945648006C5B28804F34E73B238111A65A1F500B1CC606A848F2859070BEBA7573179F36149CF5801BF89A1C38CC278415528D03BDB943F96280C8CC52042D9B91FAA9D6EA7BCBB7AB1897A3266966F78393426C76D8A49578B98B159EBB46EE0A883A270D8057CD0231C86906A91DBBADE6B2469581E2BCA2FEA8389F7C74BCD70961EA5B934FBCF9A6590BF86B8DB548854D9A3FB30110433BD7A1B659CA8568085639237B3BDC37B7FA716D482A25B54106B3A8F54D3AA99B5123DA96066904592F3A54EE23A7981AB608A2F4413CC658946C6D7780EA765644B3CC06C70034AB4EB351912E7715B56755D09021571BF340AB92598A24E811893195B96A1629F8041F58658431561FC0AB15292B913EC473F04479BC145CD4C563A286235646CD305A9BE1014E2C7B130C33EB77CC4A0D9786BD6BC2A954BF3005778F8917CE13789BBB962807858B67731572B6D3C9B4B5206FAC9A7C8961698D88324A915186899B29923F08442A3D386BD416BCC9A100164C930EC35EAFB6AB35851B6C8CE6377366A175F3D75298C518D44898933F53DEE617145093379C4659F68583B2B28122666BEC57838991FF16C368DD22C36E780C91A3582E25E19794C6BF2AB42458A8DD7705DE2C2AA20C054E84B3EF35032798626C248263253A71A11943571340A978CD0A602E47DEE540A8814BA06F31414797CDF6049582361BBABA387A83D89913FE4C0C112B95621A4BDA8123A14D1A842FB57B83A4FBAF33A8E552238A596AAE7A150D75DA648BC44644977BA1F87A4C68A8C4BD245B7D00721F7D64E822B085B901312EC37A8169802160CCE1160F010BE8CBCACE8E7B005D7839234A707868309D03784B4273B1C8A160133ED298184704625F29CFA086D13263EE5899123C596BA788E5C54A8E9BA829B8A9D904BC4BC0BBEA76BC53FF811214598472C9C202B73EFF035DC09703AF7BF1BABAAC73193CB46117A7C9492A43FC95789A924C5912787B2E2090EBBCFD3796221F06DEBF9CF70E056B8B9161D6347F47335F3E1776DA4BB87C15CC826146FF0249A413B45AA93A805196EA453114B524E310AEDAA46E3B99642368782566D049A726D6CCA910993AED621D0149EA588A9ABD909DBB69AA22829D9B83ADA2209A6C2659F2169D668B9314842C6E22A74958B4C25BBDCD293D99CB609D866749A485DFB56024883CF5465DBA0363206587F45597F89002FB8607232138E03B2A894525F265370054B48863614472B95D0A2303442E378B0DD1C75ACBAB971A9A8D1281C79613ACEC6933C377B3C578C2A61A1EC181B101297A37CC5197B2942F6A0E4704C0EC63540481B9F159DC255B59BB55DF496AE54217B7689BD51DBA0383A3D72D852FFCA76DF05B66EECCBD47BC53040817628C71E361D6AF889084916B408A466C96E7086C4A60A10FCF7537BB94AFBCC7D437590919C28650C4F2368259226A9BFDA3A3A0BA1B5087D9D76442FD786C6F81C68C0360D7194D7072C4533AEA86C2D1F8C0A27696066F6CFD11003F797270B32389713CFFA093D991B63844C385E72277F166F5A3934D6BB89A4788DE28321DEFC7457AB484BD30986DC1DAB3008CD7B22F69702FABB9A1045407DA4791C3590FF599D81D688CFA7CC12A68C50F51A1009411B44850F9015DC84A93B17C7A207552C661EA9838E31B95EAD546248E56BE7A5130505268771199880A141771A9E47ACFED590CB3AA7CB7C5F74911D8912C29D6233F4D53BC64139E2F55BE75507DD77868E384AEC581F3F411DB1A742972D3EBFD3315C84A5AD63A0E75C8BCA3E3041E05D9067AFF3B1244F763E7983D48BA34134BAB88D635D8CF8FF5D686058FA68B6C2FEEAA5FA4DE65757086C0125E937BCC0D02FAA8988AE7169DF07F6A771E6E7FE3AB65E965C63C3E40ED909\",\n          \"c\": \"E2D5FD4C13CEA0B52D874FEA9012F3A51743A1093710BBF23950F9147A472EE5533928A2F46D592F35DA8B4F758C893B0D7B98948BE447B17CB2AE58AF8A489DDD9232B99B1C0D2DE77CAA472BC3BBD4A7C60DBFDCA92EBF3A1CE1C22DAD13E887004E2924FD22656F5E508791DE06D85E1A1426808ED9A89F6E2FD3C245D4758B22B02CADE33B60FC889A33FC4447EDEBBFD4530DE86596A33789D5DBA6E6EC9F89879AF4BE4909A69017C9BB7A5E31815EA5F132EEC4984FAA7CCF594DD00D4D8487E45621AF8F6E330551439C93EC078A7A3CC1594AF91F8417375FD6088CEB5E85C67099091BAC11498A0D711455F5E0D95CD7BBE5CDD8FECB319E6853C23C9BE2C763DF578666C40A40A87486E46BA8716146192904510A6DC59DA8025825283D684DB91410B4F12C6D8FBD0ADD75D3098918CB04AC7BC4DB0D6BCDF1194DD86292E05B7B8630625B589CC509D215BBD06A2E7C66F424CDF8C40AC6C1E5AE6C964B7D9E92F95FC5C8852281628B81B9AFABC7F03BE3F62E8047BB88D01C68687B8DD4FE63820062B6788A53729053826ED3B7C7EF8241E19C85117B3C5341881D4F299E50374C8EEFD5560BD18319A7963A3D02F0FBE84BC484B5A4018B97D274191C95F702BAB9B0D105FAF9FDCFF97E437236567599FAF73B075D406104D403CDF81224DA590BEC2897E30109E1F2E5AE4610C809A73F638C84210B3447A7C8B6DDDB5AE200BF20E2FE4D4BA6C6B12767FB8760F66C5118E7A9935B41C9A471A1D3237688C1E618CC3BE936AA3F5E44E086820B810E063211FC21C4044B3AC4D00DF1BCC7B24DC07BA48B23B0FC12A3ED3D0A5CF7671415AB9CF21286FE63FB41418570555D4739B88104A8593F293025A4E3EE7C67E4B48E40F6BA8C09860C3FBBE55D45B45FC9AB629B17C276C9C9E2AF3A043BEAFC18FD4F25EE7F83BDDCD2D93914B7ED4F7C9AF127F3F15C277BE16551FEF3AE03D7B9143F0C9C019AB97EEA076366131F518363711B34E96D3F8A513F3E20B1D452C4B7AE3B975EA94D880DAC6693399750D02220403F0D3E3FC1172A4DE9DC280EAF0FEE2883A6660BF5A3D246FF41D21B36EA521CF7AA689F800D0F86F4FA1057D8A13F9DA8FFFD0DC1FAD3C04BB1CCCB7C834DB051A7AC2E4C60301996C93071EA416B421759935659CF62CA5F13AE07C3B195C148159D8BEB03D440B00F5305765F20C0C46EEE59C6D16206402DB1C715E888BDE59C781F35A7CC7C1C5ECB2155AE3E959C0964CC1EF8D7C69D1458A9A42F95F4C6B5B996345712AA290FBBF7DFD4A6E86463022A3F4725F6511BF7EA5E95C707CD3573609AADEAF540152C495F37FE6EC8BB9FA2AA61D15735934F4737928FDE90BA995722465D4A64505A5201F07AA58CFD8AE226E02070B2DBF512B975319A7E8753B4FDAE0EB4922869CC8E25C4A5560C2A0685DE3AC392A8925BA882004894742E43CCFC277439EC8050A9AEB42932E01C840DFCEDCC34D3991289A62C17D1284C839514B93351DBB2DDA81F924565D70E7079D5B8126CAAB7A4A1C731655A53BCC09F5D63EC9086DEA650055985EDFA8297D9C95410C5D1894D17D5930549ADBC2B8733C99FE62E17C4DE34A5D89B12D18E42A422D2CE779C2C28EB2D98003D5CD323FCBECF02B5066E0E734810F09ED89013C00F011BD220F2E5D6A362DF90599198A093B03C8D8EFBFE0B617592FAF1E64220C4440B53FFB47164F369C95290BA9F3108D686C57DB645C53C012E57AF25BD6693E2CC6B57651AF1591FE5D8916640EC017C253DF0606BB6B3035FAE748F3D4034223B1B5EFBF5283E778C1094291CF7B19BE0F317350E6F8518FDE0EFB1381FB6E16C241F7F17A5210693A274159E7FAC868CD0DC4359C3D9EEFEA0D9E31E43FA651392C65A543A59B3EEE3A639DC9417D056A5FF0F160BEEE2EAC29A7D88C0982CF70B5A46379F21E506AAC61A9BB1B8C2B9DAB0E44A823B61D0AA11D94F76A4A8E21F9D4280683208F4EA911116F6FD6A97426934EC3426B8C8F703DA85E9DCF99336136003728B8ECDD04A389F6A817A78BFA61BA46020BF3C34829508F9D06D1553CD987AAC380D86F168843BA3904DE5F7058A41B4CD388BC9CE3ABA7EE7139B7FC9E5B8CFAAA38990BD4A5DB32E2613E7EC4F5F8B1292A38C6F4FF5A40490D76B126652FCF86E245235D636C65CD102B01E22781A72918C\",\n          \"k\": \"7264BDE5C6CEC14849693E2C3C86E48F80958A4F6186FC69333A4148E6E497F3\",\n          \"m\": \"59C5154C04AE43AAFF32700F081700389D54BEC4C37C088B1C53F66212B12C72\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 52,\n          \"deferred\": false,\n          \"ek\": \"16E08D929596ABD2BA47558090531AA277B00DC8337AF578F3A18B3DA8738CA434ED41B537ACCC58182310352331A43A0CA85C606823C824602085B2338142BE48A00E068289310559E9155C6A991CF457F098C61C6B79C584B24C883296B03F9D100489C546ACB28B2DB181BF7B4EC80140F1ABA4130512BA2A0F96C9453DFC479BA1CA9689629779AD731B159A61582CF67989266EFF84455D191032486242E6A9CCA6314B788A3783A0D003A4BE1AC50700611DA61476962E48E38AA5250CB4E60E44B52F00C5233D0A72E3D010D65ACF50CA1704CAB0EBA28D084387DA4BC8BAF7BF3212954652577CE52CD0E9768B3CC606000FEAEC499CB13AC1CBCA0F5B6A0BC7B8B9C140DB83174448050D72C51F18BF1A570FD6314ED91A4DACA6C231404250704A86561F5861785F4B47A15420975225300C621EC11FB6F04C8613982CD16AC85A8EAF62B07FB16A2BAB515D84941AB7AC45DC58D43ACA35697DC711BF8D7BBB41B95BF48716A1BC462F332DB93B67CF858D694B66D9899069EB795B4C1E407ACC74493CC5908B21441838702A3ED0683AE0599CB487A2AC154727A1CFB30104A9B0715698D5E51417832AC67139EF752BA77B7C27217472C62AB8099B4EE2A1D6D98A37EA56058A94D8B86FBFD17972E46A496B2530232F821B68D306AC78BA8D719C6DF278AC79E6036CE55D4E3995CC772E4538BC99E5A5AFF866AA733E6A15A4C7D61ABE8A315E908B588566DBF922C17B6ECB773B59D15416935EB8197FE751A4A5C49AD6FA5D087489F299B20E6721DCC297990751A57489C3A9CB59745FA51191A37873A166C84AF394D280982FA2171183345FF5BC17077B5432236108C6537CB68465C08EA6C98D4B1B606B73BD2A6036B16922B712B68553CAE23630B926276762E3D55DBC1A2FA1CB1372C9460B7727E2CA7382F0B696D005E07AA6C2C763225C30D846710D2286244BC2C751A5BB5CB71F24C75B40C3D1DC0369506D78D39BE3564358764A074567C51BB81B1090ACB301AB95864406B500CD04A2517C582601057328C8467847B4A3248A4BB63251317A9AF93475063CA34D382C4AEC93164011882A6AEE1771EFC99E84E1B68217281B123672999431BB1D4DAA180E9202372C8CD7150FBE3166718AC3746CB0E020AB0A349F88E21D319394676919CB08B29203A6EAC112B63178C7B8C29CC28C4C085A7D6660B12BC64B10A00C038F80076AF0769FB6D42240CA010843AA33B5C534A1C3391928ACF90132D0598E35BFAB062F771696C93696A351C5322C6648CB539660902526202AB34BED4ABC9DA427A1602ED5278897785A9375110A87529D74B951750649DC2B03C0642755132734B808897B1494C98F87376F223207C267A9D5961BC6472B3B8EBBE9ACB9A79A3E2A3FFF428282BB1B79525B7DD265A9986D362566E93886B106C7DBA07FD1C78CC24008852B152822120E73807D8B17486067FAA964330BA67027A84E2BA8A91801D46A059DDA37EDD31875600794E3588AD44331741CEB3990908A57A7C1CA8D7AA3D9864F8E501E9B5603C1FA8ED23327BEB22B08BA26E79C90928B756F96771FD7244B346CB18415CD3CC5BD845E394BCB5C6399F96338534182F015947EF7230A0AB825382957F8950B31CF94F31C0867255A597D9501A76DC2BB7AE455D8296953C51C7BA03A3A0A769207082F45A5100CB49C86317B1650B5898BEBAC512960830A37022CDCBBABCA0AA6DAB3E452A12C1040D54C1BBC372F1997C0DF75BE5D1C88C1618F1833B223D02E2B0980FC187D93A75B57E0487D2CC36AFC1838519378E5634502106AA7B3923830C9B9BA6717694E340B7B51CD63917FF9770635F42F212085458A45BFA09265F074036545FB39CCD08522135AA522670A640B3AA37782D9C7794DACAC86D651B030B33F14464B9CAAE3E883E9582F16558B03D77EFC01AF01E2327CAC368268A4A7141F375C833AD3B4369533FA727FE051C33A1ACAEE8832E32986067468EAD91D79A90058F608F97A1226CBC26339540778B3C1B0421E88458CF69C8DC73287A36D80B57F7FB5B787B66C22658863DB1F60985156BC28BDA25C56C5BD35812020880DCCE46546965817DCC3F1667496F12589065EC68853863C1C581B7F378C82ECEB88D1AB88CFD7DE4C88E0E556D945755EE2558034EC6FFEFAFC68E26128BD7625563BF279\",\n          \"dk\": \"4DD7722880771C554AA6D99D5A873FA7723F18EC976BE29EE5438D7671BA97438000F91A396CAA464BA3CFA2598586AB4D1BB0C9803A82AA1C5B13AD1647972FDC154B61800B0C87215657D13B6BF8CCC69E5C8572EB9AADEB6CDC8871F8F7416FD032ADE5A3E863591B6B3756293057F80024F67A7ED35706185FB6B30F8C836F9624B291C009BA6742C23C65F718291DA3210D25B594E7C00F575B6B87316682115D84116974389248921E0A94302C5DED39CF6C6455B9B277E9F48C2880AD64CB5E96115EBDF1BF42A540228A092FEB8A09449BC626571F4007AE84824AC8CF92EBAD8E25A62E776183BCA7EFF1417CB30EB4043B98649C9276B4AF9CAE89232C7F7C920608A527A92AADF809EC69C651447664E98F369B2704B3054C656AC4570D991C6C5859063C249B70DC5D49CC8202541B13B6005AB09F4A471C28F4785D1A5B52389C3E0B1544DC2098F5284C43802AA16E29F32C93CA611CB170B82C4F6F514A18A985755C3622C2A8C15514340660E9F7025462909DBB6FBBDB3BADDB700714CC47297316F45589922440D6A0AACA8216902AD00647E920C4584AB73CB7C546DA0021C22EA2E59D229536E9596EA6598ADEE85D563C2CB5D82908D60462F430F6245D882A3395F754D561B949CC5CD27240C2CA41406C3D09313ED2A4A600046871F030F41B4528E7316F9104ED550C38C43844789BFD330EC75A8EB3909C156A9E54805C568B5581E603C6C9CD9713AC995077AA52747D281C28BBB535B9BADBD867C65111680C2FDD1032E1E49A67C6C141F53F44461AE427A416B8538747C144025EE3278B6EB26EFE09C9CC8AC7075324D8392A44CC260A1297FCBA4096DC537AC82E5DB560E0C541D114A5E3891068B4B123CBA214D72C36A519B409874D4011E7DB2F7978343B3656EE600031229BDA3171E4480413716B97E5486F07C32E94805FDB48ABA2144288B96C1281AB180A1BECAC17B4C206677920C52B77B53F7D7C782C562C4DA6CB69C5A828056ACB445CCFA62A8EC3AB75C5940D302BD4E38A381015947B973D547A5CFB1460668476529F6411C7AD3A2B13A0C1AC5B71D0132568591374CB27ECB02DE9CCB41D150025C26CD48CAC600C6E9A6A1398884F49EA97096662C06C56F5B76E3F444336A2C7E2FA201A66AD65F2C8BB2340C89C7572698D84190031545CC14420564722820CB1CC6C34708530207B10F3660AF2D72E1C4A4A5729896F79B6BD85B93E8C1B74C521EFC79B10085C92D5857CD200E7A4C7A284524D25636C04BA9120B76A86957F848AA2540A6CB36EBD26354C38213F6713F255457821A47CB00F01185CFBE45E50C624985130086C392226501E6B84D1C666DCC959FA0209114804EF8B99456C16D0B59E81568AD092B3DAF8B70737C3BCD10DC9D48D7DDAAD2159A96B28AA77505364DC92E170699FA95BEF6C26DBB6CD4D972026A00A74896CA2E883E0FBCB19D21E5EFA2776837BE30C373CF405A1628C1FB795095B7FDCBC063357B862FA2823D43A2F1819B0D892A59104B7E64889703F8BE2C39C9892ED393AED6A62330C3EF2B32F5B8A59975488FDF60D8CA366888B9EAC3A094EBA3513F6225638A6D2916CE6F76ABE1242EFEBAD49F29D4A289D6ED2549383B66868A33018107C1618ED3613933A9360F27E6885B7A18167E3F124115184671506F8B6C2F4A18A4EA30FD7FBBE5752A1FB72BE2E909CFA441AFF70C15BE67640F12672DA671224B6BD1B39A7F744E158B9B37B859633068F493902870BD2043A41B605E1395CC305288D9C3ADE5551D92B199260156F3710EF09C8B20A44ADA53D17E31C829BA8775579CEB110F4177BC314653EB6BF03514ECBF009BC6A95A8B79B90E18DB763C46699A9C0C23C32A31B1BD350D4301ADC63A298A486DA95B006175E99E2959881B2D84CA0588C44410173D1148AEA165E93C34CF4711FA3E215F0A193AC679E6298874221B727F141480356D408017D5215D2463C7F62A554E11D0C204A6B93314116BA19CB3F7E1571DF533EB64372FB68A7544C3A7B793D9D79CDB0CC4DD3855E9337C7E8932B1DD3BAE23245FF854FEA2BA93FE06B18E4C694322873DC8C7B70816325A9AE1A1E08686A982715A3C10CACD17F8C2920CB13B251510C55775251850A21D3BB16E08D929596ABD2BA47558090531AA277B00DC8337AF578F3A18B3DA8738CA434ED41B537ACCC58182310352331A43A0CA85C606823C824602085B2338142BE48A00E068289310559E9155C6A991CF457F098C61C6B79C584B24C883296B03F9D100489C546ACB28B2DB181BF7B4EC80140F1ABA4130512BA2A0F96C9453DFC479BA1CA9689629779AD731B159A61582CF67989266EFF84455D191032486242E6A9CCA6314B788A3783A0D003A4BE1AC50700611DA61476962E48E38AA5250CB4E60E44B52F00C5233D0A72E3D010D65ACF50CA1704CAB0EBA28D084387DA4BC8BAF7BF3212954652577CE52CD0E9768B3CC606000FEAEC499CB13AC1CBCA0F5B6A0BC7B8B9C140DB83174448050D72C51F18BF1A570FD6314ED91A4DACA6C231404250704A86561F5861785F4B47A15420975225300C621EC11FB6F04C8613982CD16AC85A8EAF62B07FB16A2BAB515D84941AB7AC45DC58D43ACA35697DC711BF8D7BBB41B95BF48716A1BC462F332DB93B67CF858D694B66D9899069EB795B4C1E407ACC74493CC5908B21441838702A3ED0683AE0599CB487A2AC154727A1CFB30104A9B0715698D5E51417832AC67139EF752BA77B7C27217472C62AB8099B4EE2A1D6D98A37EA56058A94D8B86FBFD17972E46A496B2530232F821B68D306AC78BA8D719C6DF278AC79E6036CE55D4E3995CC772E4538BC99E5A5AFF866AA733E6A15A4C7D61ABE8A315E908B588566DBF922C17B6ECB773B59D15416935EB8197FE751A4A5C49AD6FA5D087489F299B20E6721DCC297990751A57489C3A9CB59745FA51191A37873A166C84AF394D280982FA2171183345FF5BC17077B5432236108C6537CB68465C08EA6C98D4B1B606B73BD2A6036B16922B712B68553CAE23630B926276762E3D55DBC1A2FA1CB1372C9460B7727E2CA7382F0B696D005E07AA6C2C763225C30D846710D2286244BC2C751A5BB5CB71F24C75B40C3D1DC0369506D78D39BE3564358764A074567C51BB81B1090ACB301AB95864406B500CD04A2517C582601057328C8467847B4A3248A4BB63251317A9AF93475063CA34D382C4AEC93164011882A6AEE1771EFC99E84E1B68217281B123672999431BB1D4DAA180E9202372C8CD7150FBE3166718AC3746CB0E020AB0A349F88E21D319394676919CB08B29203A6EAC112B63178C7B8C29CC28C4C085A7D6660B12BC64B10A00C038F80076AF0769FB6D42240CA010843AA33B5C534A1C3391928ACF90132D0598E35BFAB062F771696C93696A351C5322C6648CB539660902526202AB34BED4ABC9DA427A1602ED5278897785A9375110A87529D74B951750649DC2B03C0642755132734B808897B1494C98F87376F223207C267A9D5961BC6472B3B8EBBE9ACB9A79A3E2A3FFF428282BB1B79525B7DD265A9986D362566E93886B106C7DBA07FD1C78CC24008852B152822120E73807D8B17486067FAA964330BA67027A84E2BA8A91801D46A059DDA37EDD31875600794E3588AD44331741CEB3990908A57A7C1CA8D7AA3D9864F8E501E9B5603C1FA8ED23327BEB22B08BA26E79C90928B756F96771FD7244B346CB18415CD3CC5BD845E394BCB5C6399F96338534182F015947EF7230A0AB825382957F8950B31CF94F31C0867255A597D9501A76DC2BB7AE455D8296953C51C7BA03A3A0A769207082F45A5100CB49C86317B1650B5898BEBAC512960830A37022CDCBBABCA0AA6DAB3E452A12C1040D54C1BBC372F1997C0DF75BE5D1C88C1618F1833B223D02E2B0980FC187D93A75B57E0487D2CC36AFC1838519378E5634502106AA7B3923830C9B9BA6717694E340B7B51CD63917FF9770635F42F212085458A45BFA09265F074036545FB39CCD08522135AA522670A640B3AA37782D9C7794DACAC86D651B030B33F14464B9CAAE3E883E9582F16558B03D77EFC01AF01E2327CAC368268A4A7141F375C833AD3B4369533FA727FE051C33A1ACAEE8832E32986067468EAD91D79A90058F608F97A1226CBC26339540778B3C1B0421E88458CF69C8DC73287A36D80B57F7FB5B787B66C22658863DB1F60985156BC28BDA25C56C5BD35812020880DCCE46546965817DCC3F1667496F12589065EC68853863C1C581B7F378C82ECEB88D1AB88CFD7DE4C88E0E556D945755EE2558034EC6FFEFAFC68E26128BD7625563BF279560143610E550E6C27E7AE725C958594A71FCB0350F3CE623FFD626D381C38A24D9D475487B57327D5EFD4EB3307FC1A19EF63E2E11D82AFDC95B51A4FF19D77\",\n          \"c\": \"6930583C55501AF07198C21B52C1A66D60D3E6A403EE412E9751AF2DB2AE360BBE29EA953050D455E25CFFB6E9DB5CB6D881375E7B28BABAF2C7946BC5A4757F61A4970BBF1CADC21C72E782A4A31E92FAB1980E7B2D51AC68CCC6222636D05645B4C85DC7DBDDD6EDE4D52478BD336C81D85708857359DB863F73B839660C3383EED5F621D1CBD3C1C1E5B3F5A5E2BD340824FF5F48690D185F725C821A2681E27EF8C3BB76CDC4CDAF720A8C657601107FFAFE761D4709C35CF62023B1690F2068038D444B9867F2FD7D619F3162D286A42E4B4A5C23E9768AC694B466DAEC80C6A09BED0CAEAE9B1F063708BB800068CE610C0346114981A48921A9BA7091F4E615B5E4FB91CDDBA00272B98FC8DB9282C43B3BF34A393BAC9EB25B6C92235204AAAAB683142BF66E9B37DC1EE10122A3492CC31EAE416D4C364780F696C0691E6449F3570C0AF421192CF44684B1F2BBFD97E2C2B15D6DC4D589069C351BCEAFCE7D2AF4C57DAA75601EECA9CCF72A47D473688B9E21D3EEF68E79BEC63BA7CFCA6D1B47AF8F45DBDE1D3CF6DD108F756F935379303DC3FEBF11BAECA5A2B299586D8DD45B0A17DAD6F2E3F2A63FC0F6435C2108DE90E3C42387A068D7E26C52C966C50A253F9CE19F1B13CDBB75C445D0C01C2EC3133BF9EAB4B6FF0DDA9C87C37FB677827B62107685793406698F08AF44632260D8C298042BDE014A8E3510705719CE0F2A75169363FAF9A0575558809940D3C7FD1E8CC027055789A1A69D9252330410C66CF41F00E67935A7A0D927D6E8EEF2F183377D6CA76F5C0A06F606462B6110600B8345421CCF5F77FF096A800030A0729BFA24521DEB7ECD3AC12B2A7F3A65921F60CB10B3C23C572F5248CDF83C34AB1EFA70AB3F1E78F3CBC0361A407F649ED4F4372A59DE9C11183DBB2661A1707029EB5334BA67231A53C118412723C9E146E0AADC891AA7A37F05F1E63DCB22CCD774FC0AAFBE2A0148DA31EEA8D855F05427E0D416C8A24259EE7D7F0584F01348316BB637F9F18080466610FF013D050F41941CEED3854A90D92E6DA33181D7DA541F148153728C64BEFB5A9CE23F2506FF5A97F3E6372AEBB119646D8E7DE1892F357FF6B4BCA001AC9543BE983E4A919F841A6AC30945F3D516222A1BA8418DCC05D3C2A26D36F43BB2A64F66737EB94CD5D973392CF47EF81CA2BCE1A5C89023EA226E4FB0136D922AD2E67364858213A2CD951369712E3E61DA5C1E8B2F6C21A4A80908CEAA1DF311CED7EBE78E245DDAB3C298C7D2ECC6C78DC5C8ADA322281F6C1B8A33ECE1720E32614085986220A8F8A128097E65904B9285327A8940F02CBBDBB36E8C650FE065F7FE69B30197FDA4F61A7EB3AF7B517668921A6E3C10D79E00853A4DFA985DBEA19AD55BF0BA53CB5EE16DDBD417FE498D2E98921E743B1D2B0192590C738E770F7BCB60B129F0BFB3F2BCC3752DBA1B433C6AF5CFFC18E963BB906BDBFA0564205C482BF032F21DEA5D9A61278ABA2122560FB2030A7893868D10B03E1105AC27527C206DE6538BB235F14FC6DE386A9418A3227264297B09A9A9F1401C24F81B8A2A5A7A6373457AD9DD02642300D564E3030629DED71D014C834F6A5005F2DB283687A2744841A86D3F9DD6A5D332AB097FE04715DA746915FD07A1E6D65C9C60DAA1ECCF71D1F4A4BA8AA9516263790DAEFC1D606DD009E079D1AB84E808DFF4BD56D76336345B23291EC5EF217FAC6CBE590CBE5D31EFDE35D4F7041EB20F7B2232DF031699927D9FC2B08E44A36DB2FE5BFABB6CA53FF050F7CC1D31660EB375D788D83AE56CF359C557E5FD4B881327181C2CA6D86B39BCC22A4C45F7B915183CA0CA00B65A06BE77A56163674F49CA79822BA11596BB5EA52ECBDC139364D84153F97D193E5E05A4F0A618F6018B45F9A646163C999F8D40CEBF85A3D51C05024E39AEF608625C93A1B1144F34EA25A4F3C588BF6841E736921BA111215740F8A1903C065CF08FB2BCD24EAF3E7733CD59066DC85AC0206822402A6AEE784F194BDD411806731CC42430678D4A0D027900D5427639AF42262D57E7BC8242A3FAB2BE536C931DE54D406535AB881C71D9C9A4CFFFB37AD298FE879EB7279DF03B9A42C6B69618478C0886C23688AF1799227163E90955B016BA01F3B9AEE10DC5C889D3883F1163CE483584D7FC09D570BE76968081485086\",\n          \"k\": \"4BE636AD0F1522EE10798CE9EF454ED219A13B6791FD2E042A417B2A220DAE79\",\n          \"m\": \"2E2C821791D3EA49D0AF380B97AA24532F6109D85360A751BB8B4C048C48D26F\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 53,\n          \"deferred\": false,\n          \"ek\": \"54570A4B2AB131F9139EC171AC5ABD140863A6A0C5C13C8AF54094E95620E4866BCB8483EBF0B21FF9987B44650951750EF0BB76334235651CFA85153451B1975380513552FDC693124617F395121DA27F86AC80AE363707CC3F264CBA1B703DA67348BE45BDB4293C69E31EA73B0DFC35083F2493185B108E09467B2BCD44AB17D7BBC41F73057FBA8C2732730EF110E740AE75178890746FE149C8898B467CE116F68743A7B35453720BC5D9261946CEF3D9931A4C5F2F271041F0C4BA277778D56EF9DCA3E8CC2A4937BD9A276D173258B70C0BD9A0C695732CED11079D816E9D00A1C44A095F746D4EF14C53C64399964884977D503961A48C14E02A4B7055A74A31C5C55AA503C9927F017A194917F3AA13B33BB8FC86126A8B33EAE53CC2631C15C39FFE2615877084FE0C94BD5013A9CB467FA66E780A46EC5BA392D3C7B6579F63F2656796622AE9428D128BC546B33B26C697392B57A240338117107ABB0C146B2F789FB1A4552A4747C520B161C6A356F1368F2ABF7340BFC8031535876A185C11BFF5176B474B3812BE1E7603085453BD5B289CA97F20B82D88E30064B39872ECBC35E447E8431D5A61B25220CABA58594E96CAE054A791736B51E42F3FCC1F2F7CB8C86C9B67FB5FD53C93ABF3123D11CF5AE491B25703AD386839F4B0370C3AFF50903724C592141B88EBC6F61732F1DB038B15528F1389FC574634071CFF89B39D5A3F50D5B8E6E932C434C9BEF450E42765EE1486C3FB1221A3B3FC42C5ACA709602BBAB9A9BDE4A9716CDA0370B767D1E29128A5560164ACF0D03EC60833AEE125040B5A5D45827C1A2E2B3CACA923BD51134E216921656579F2773C4F0002A285A9EA975670F02399626D65135119754F01F09E204C305C880E93816516B0CCF996076AC93540144231F54ECD27C7C3DBA3DE988E8BC1838F68550ED8BA14D35E14586516BCCF459A3E63C9C69366A5A2992302092790341E7FE08FCA4B77906C839FE929635B89971512EB73456462BA9DB473770B9689446E4D69848E21670CD76F04896456E7BCB4352B086979DBF4905F656C1D67169D71933FD8B827EA5D0B5B00E0C3476BE53F56D11FE47C1BCB5727C51198381916572257D9E5A811E6601191477F45B2244BAB96943B16B28074B7129337333BE218DC114DF16A83C4C1B76A28223489384CAA096097A26B0C17E1E6686D52214F756A12F3C5E08985843909DDC1C4FB99CBB552C9D2594B85C796856C48A3D99A2B93510425A90B4AAA035150DD1394174429B6A394EE495E060812C6044678CA6F5DE9AAD4C11CB4060225F07F004969FFE991B7442176E00D7AD4C6C266435764CAECC34DEE027ED1CC4B80BCA242495378F0097B797C9A0C3F49D7BC2961A41D751728915166730DAE7A588C550F64F06CEA7A868640157F05C69C781682967CD4720040060782500C97593FD5715CD84A05AA10958FB482C38C7DA01A910C5B0C055BC2252B6FDC659A94F36422285F2CD477E350575C4AB3048C0F2AF83116BCA3EF65B0FB73C77747C3D585AAA478945288CDB0853C14D04DAD16052B71C02AF9A3CE254095C26736E6235CD6B0C340812DEB282249AAC87C7E35639E31A5943F61CE43728332B9280495CC9268825AA923DD5155BCE616B3675EAA5891FFA7A6BBA07F14196001253427E8BC4C609318940EF31030C5C9965A26B7B5A2440EF56BCCB424DC75350AA0129E05C83C19856CD93BC9928A8D674381E6B79EBB0319DA1BC7F427DBC1830FF1CCC468B5E68B23AAD73526D16DFDFA30D8CB3559296E35449F36B280B191587A692DBCD610D75342DB7A919F8ABB1E12BA44B185B7560AF79200427150D34601EA5A929025855B34B3067A01BB0B9564C186B2820917D5BC444B30755BAB812C89D201AB6D376135F2220F37AC35876B6012975731077F464ACBEB7A1E7764AD8502A68C9616B09C33FC6688166E1E3170CF6534C0212731049A31D4353BF80A99390064953419E9CD1DECA6F3007D31298DD335B69B971E4F5261F4888318A6A0C0E2AD79C19A827436E87886E29768AB261863243EB5D101A11922B2558D8F53BBCFC1BDD652CA6B10C123C41575F609630C660DD9205E571B276A36C64C770CF3B2D362064F0C94E2BBA158F235F27464280CA9026DE7D64D513C6120035933D3067D03AAFB1021A78860771BC04B4652\",\n          \"dk\": \"B3E23EDA19008B5C42573A1A3BBA8CC34B23AA7A36761143480C8FCFF318FC4048B8C697004D82D12907C031885B92C7EF5BBC4D22A75B493FB19512C5FA208D800C134B1AE9A1CB19E8978B2003EBA888609CB147D7A451CB5199B0259FF8511CC5561338C6C54C2607CBCFE85C9809278F5D0B79925B0522A017B4B45D380C77020A722FA93CCA36904D0B4F04770F4DE13EAD906DBEE00C0FD98212E21B4BF1AD4BD0536FA232C8733719B20B2D8250ED698B01104019957F532B2B4D9489529A265CA9C773178EBAB24659C16423C4C00517AF879AC016A53E70929292140DC9849B65F5406A730CC20C84BA41B29888607A49B4F9C7947D155E60C707A6DC6AE42C6091886ED8F3B5D2655E45DCC945603365E4548DC568C8558C310C84D8E729793226FC30649FBB7FF989565393BD4DA91ECAB1325A49B76BF20A2303B081180B08DC324E01A3526B5B17E13B5C46694948997424179C29434DDC09DAB11CB4A47A6FBB669E02172CA30B2655B2658813F1666281E33EF520CBFC40ADB07A45E8D56B9EC28617DAB8D0BB1C6098CD474444AE523A5C0C47A17C5AFD845BDB14CD3661676C5194C9C48C10230D3A08A48A23B50861CD1EC19EFD5B25137435CB676DCF322F40043998E801467422471752B8FC1CB9E8CEBC2B218F60228C2452396598D186CC9008AFA626326784656228A8A0B00829F56B1C9184E32C757205C80AE5113242B4B41C16C6730A54059F37E0338A215431B9260307BE517776EDFAC0537630C62688B4586CF5A93EB4959C7BA0CEDCB904B2B6B41CE0B966B73C4DCB22F0B420734137C5276E1EF428976C2EBBC055CF583721805D64C57E42C7B2FFB5C3ED33B83E21190B1432E8D6B7D7845D467499F4F1C28BBA4F69568B2680283E779645605901FC918194165C0B62386CB2570809AA1283B31652CC4283568B16D5B2397AE3BD7A19254AC4C711067E98C6203316AFE29337B570ABFC5B272AF386A4A27893B03D3D5C67D2649250F20B10D09DF1E93216EA2A6E2BA8B5B768E9084F6675915AFB917E39051895A5485877CB7C9F7192144704920413666C069B0B731AF8D29953669FD51BB16B2960B6A2470562A0CFE49CDA85982CDC4BD9DC3D28109F32E5365D056469027EBA86904A4AB5CF592819B5848CE425543309DD76B8FA6B9D854A1434A48A636759C8128818E3197ABB33584A4F9AF94FF56839D625799D01B4AEC07EAE36C0FCA3C5A76B6CE97C7F83D3617550A0D309063EAC9E37616065774B6E78C9E3C4140D618CF707295AF10A90702E84168A65B40E172879D6B249DC2094FBAC234E563C1DA060C4A65AE4C2357179CF87480B14919A5DA7BF64BBB40B148C91A960E4A2C3BCB903CF57BF09251AB19830028088FA49C4EF61511FD2A23299CA8FE16873B81E015A724BA5C8BD110BD2D657D212751FE88875975B0322351989831854397DF90DDC82A3F59CB8952086C8F10973242CB778597EC0370838651F02313C575535D97298FC2C9CF312FE066797FC2B61B15B3874858BAC6460D600856703B6CA5A8A90BDCF3490938368670C2E29FC8EDDEB8793481CB3BA9C2D14AF5C8A135329AAA18C31BFB72C1E8A9BBA1756E687300206B3796903A3C2C2704904CC00C947998F97F46388D83CD4E4526CE3A68348A1D7701C120C0F46D0509F904B5092594E4970E8D35F60F87132E68A369BBD4BAAAC11EC976B840BFCC19B1EF48CD2937E54F3AE5B0B482443A80DD0843DE7B66C3014227A383768537D009184F86D17655A5CBCA6D9A01AD988AFA6A52BBFD6C7048285F63C0A160B965F4C39DB142029C9BCF78235ACF2132A14B946D23DFCD7C44564BB8A71879DE2C038F2359D2793B30695A4E23942D08711630CE8E06F1DFCCC987C347BBC0AFE093A5BC0177C873052A26395806C936AB453B79B24F74B7120085AD7377590AC2BEAC5FB41536C152DAF934EE2D568EC8C60EEA14F958A18E6F0ADF0D87321CCCA163A4F8BC31C0867A32C74BE4CD3245FB7C9DB3535CE3C220EE9ACEC0663DA907364EB9673038A90E18C1DF523E5F98A115490F11368DF939A9405CF8E3B0E90E32931935170E28312AB7C8B875CD4AB4FB991BDBE1C692CC356B9B96159B69142E723C6599CF5B3502DA142DE136E54570A4B2AB131F9139EC171AC5ABD140863A6A0C5C13C8AF54094E95620E4866BCB8483EBF0B21FF9987B44650951750EF0BB76334235651CFA85153451B1975380513552FDC693124617F395121DA27F86AC80AE363707CC3F264CBA1B703DA67348BE45BDB4293C69E31EA73B0DFC35083F2493185B108E09467B2BCD44AB17D7BBC41F73057FBA8C2732730EF110E740AE75178890746FE149C8898B467CE116F68743A7B35453720BC5D9261946CEF3D9931A4C5F2F271041F0C4BA277778D56EF9DCA3E8CC2A4937BD9A276D173258B70C0BD9A0C695732CED11079D816E9D00A1C44A095F746D4EF14C53C64399964884977D503961A48C14E02A4B7055A74A31C5C55AA503C9927F017A194917F3AA13B33BB8FC86126A8B33EAE53CC2631C15C39FFE2615877084FE0C94BD5013A9CB467FA66E780A46EC5BA392D3C7B6579F63F2656796622AE9428D128BC546B33B26C697392B57A240338117107ABB0C146B2F789FB1A4552A4747C520B161C6A356F1368F2ABF7340BFC8031535876A185C11BFF5176B474B3812BE1E7603085453BD5B289CA97F20B82D88E30064B39872ECBC35E447E8431D5A61B25220CABA58594E96CAE054A791736B51E42F3FCC1F2F7CB8C86C9B67FB5FD53C93ABF3123D11CF5AE491B25703AD386839F4B0370C3AFF50903724C592141B88EBC6F61732F1DB038B15528F1389FC574634071CFF89B39D5A3F50D5B8E6E932C434C9BEF450E42765EE1486C3FB1221A3B3FC42C5ACA709602BBAB9A9BDE4A9716CDA0370B767D1E29128A5560164ACF0D03EC60833AEE125040B5A5D45827C1A2E2B3CACA923BD51134E216921656579F2773C4F0002A285A9EA975670F02399626D65135119754F01F09E204C305C880E93816516B0CCF996076AC93540144231F54ECD27C7C3DBA3DE988E8BC1838F68550ED8BA14D35E14586516BCCF459A3E63C9C69366A5A2992302092790341E7FE08FCA4B77906C839FE929635B89971512EB73456462BA9DB473770B9689446E4D69848E21670CD76F04896456E7BCB4352B086979DBF4905F656C1D67169D71933FD8B827EA5D0B5B00E0C3476BE53F56D11FE47C1BCB5727C51198381916572257D9E5A811E6601191477F45B2244BAB96943B16B28074B7129337333BE218DC114DF16A83C4C1B76A28223489384CAA096097A26B0C17E1E6686D52214F756A12F3C5E08985843909DDC1C4FB99CBB552C9D2594B85C796856C48A3D99A2B93510425A90B4AAA035150DD1394174429B6A394EE495E060812C6044678CA6F5DE9AAD4C11CB4060225F07F004969FFE991B7442176E00D7AD4C6C266435764CAECC34DEE027ED1CC4B80BCA242495378F0097B797C9A0C3F49D7BC2961A41D751728915166730DAE7A588C550F64F06CEA7A868640157F05C69C781682967CD4720040060782500C97593FD5715CD84A05AA10958FB482C38C7DA01A910C5B0C055BC2252B6FDC659A94F36422285F2CD477E350575C4AB3048C0F2AF83116BCA3EF65B0FB73C77747C3D585AAA478945288CDB0853C14D04DAD16052B71C02AF9A3CE254095C26736E6235CD6B0C340812DEB282249AAC87C7E35639E31A5943F61CE43728332B9280495CC9268825AA923DD5155BCE616B3675EAA5891FFA7A6BBA07F14196001253427E8BC4C609318940EF31030C5C9965A26B7B5A2440EF56BCCB424DC75350AA0129E05C83C19856CD93BC9928A8D674381E6B79EBB0319DA1BC7F427DBC1830FF1CCC468B5E68B23AAD73526D16DFDFA30D8CB3559296E35449F36B280B191587A692DBCD610D75342DB7A919F8ABB1E12BA44B185B7560AF79200427150D34601EA5A929025855B34B3067A01BB0B9564C186B2820917D5BC444B30755BAB812C89D201AB6D376135F2220F37AC35876B6012975731077F464ACBEB7A1E7764AD8502A68C9616B09C33FC6688166E1E3170CF6534C0212731049A31D4353BF80A99390064953419E9CD1DECA6F3007D31298DD335B69B971E4F5261F4888318A6A0C0E2AD79C19A827436E87886E29768AB261863243EB5D101A11922B2558D8F53BBCFC1BDD652CA6B10C123C41575F609630C660DD9205E571B276A36C64C770CF3B2D362064F0C94E2BBA158F235F27464280CA9026DE7D64D513C6120035933D3067D03AAFB1021A78860771BC04B4652CCC54F1107CDD25CC96547EDFEE21D1854D037E1DA63CCC916569AAD31B3AF3BA28EBA34CB5F909FD026770036785C668C4181E8C5E6C458C1B786999C42E152\",\n          \"c\": \"E56A8BBA70BA91912F94B7B44F860C332F1CE8D6990EFEE73AA8BC42E890CC1932C65FF3C22B543EFC1E3ADD83757542160EB4C34C129B1260D4E0CA57CB3E403DEC9DC4DD08875BBE186D82401552D82B7E838C50ACE4096D2E2A07F0D4E5C0AA36EFA6674AD28367536AA0B608A552DA186C1F816731675A635CF39D1629D064736495DDEC6E8D494E27E64A05646D6F9C9FB7C02D62F8978969B1184C55B231560561934CD1EC48476E16F980A879EAF400EACE154F294D359ADE5E189D926FC567402BA0F031E1738A286B18AE6D4565CEF9CF884FF5108019704776D62FA44F0ED1D5E8083A449C5E6A1E7BCD0B5380406B05CB43493B7D91B731C0559CFD51ABC4CB452DD304B63061236F6E8845D469D593755CA9ED6DCD181F672DC4DCD6D950A44D632A7A820F3F3147AE93492A4C6F9F565ABA3DAEB648F6AF723C032CCBE300A83AC138C1D203368E2E407162686ED09955251777AC26DF72DED523E39EEF1A5C0885595A0F46F0BE5BA370A1CCFF54E7E34C6EF92EEDD2432C1860019B58E4B3091AC6DE14677522C037707D61C0B028B498E4CAD3A162B4579CEC0A72F2E4AF38E771F0D4A3BDE3F4B7AD110846B5C1B34A5828C3003EAE34707E0B51D8EE4C32E678F7F581386159182C142B1CDA23991573CCB84DC621737212D8146C8490517BF4AA8C16CC32E7B8157993147872802A8D6894F0B73F11B3225D5D210AD1F8DA8FB1332BDD4A88A559D2C8C8890360F9E91234AC29CACD7434DBF44B8F96FEE02103D6B6499B889A166E30F1B2A3EF53532866C65FC8990F6F00E5165DC2A46E77178EB12809B8D15DF1284DB61DC21A87198E59BBFF3583F8D1AD2774B398F36B8F7AC326D76C23A30F670CE2F5853A79C9C9BE46E98D3BF069CFDB292089142091D3D3CCE6EDE3FCA9C5E0168B655630072755B2BC4DCEF5AE34CA27FAE997CEA1A3FB94B9C94E1975DA4122BC65F563D6D515D2ED8A145B3DB0BC8AA945834FCB84E8C8F41029C8EA3DDF2182733B1CE6F806BFB6A8A702ECB73FA66DE4477D5467FBAE85E5E86BE9403B1ABD824B9E00C7DD4E9B7FA0F663A286B6C5461912EF8AB26A8EF900F060D491D2647A3E0111E5DC83B9E481D0D8DE18482F8D705C046EABA60574116D974FE8721AF9F2FC6C73D2D85CA9063FEB299BAE4AB386D23696EAE5D65BADF7148232D39CFB719642787B539DB6EA3FA9082DE19021AB24C29985CD3EF6EAAA6E6E28ADECEA9CB8CAD6D1D81B84C93ECC8F30FDD04B9D5234ADDC4872976687C667BA02EF41DE88E09DF18916CC26FBAC32D10F9AFF3ACF636FF8B4CEDEADB4F9DC1466EC65884AAA34D6DCCDCA02C86417FB9A98D4CCBCF8119C217DF2D3F5A43768D96250704C44ECC8E6FEE91ED46B4AA59FFDB154217DA4ACABDF56B7B05EE41E7EB4F1A5962B24617CD36F7C4C3A2700AD6315C83EBEEBCB6D799810841286C8EB75F7B7FC249A1195C331E617AA8FFF34171C5BE52F753E1C998F1801D3EFE61F135E963140C1EF17CCE1CB9006ACA4A2045F4D508249165F74DC718D5625017E6856590F439842EA064114CB3F34FEE61877829BBB688B4F9DC04DE3C7AC455AF176CEF8719DA7A44A1E3E379BF7936AC3693DDA97E5AA0E384CC7A7C20C4FE13B98AD95F2CCE880A9E3438DCDB50296CD3463DD0DBC3DADAD3D72D00836C7BD3232D4F43B89F73665E9B94BE84A31C9256DCCF57BB1DCCFEB11989576965C0007B054636A85DC70E6C54E5FABD7875E609AA4B9413DBA1DDDE880ECC37DBF1C3CF6D50F797282A623FBD04AC7EB21596123811C6635614B8F75952B83B11E635FB87F0229DDFC7F527197EE4FF99AAFDB9110057C075F2436B08A4A4D6B565CF0EFEBF397E3C565EABC7A26E662203109823E82978AC0B61496D773925D56982373B009127E2A75E33B1490C07FBEB30E04205C305689A233C5C2A1E5700D64C8D1A58304854CD0D06BC51F7C4B592790A2B0B786051E60EAB9D6515E54F2D1AFE4828FA4C904E5E7922B326E8EE663B5A4950444B396EF66471217BD0547FCD65C10B81F9223680D8F84B1BC33894B1D8B6FA2FEF37E79BCEEEB70AFCAA007B75E52BAD27F66889BA4EEF7428BB3F800345A2C2C95B9BED9C86C715A572D6B0EFC7439CB1711A632551F770EC5BEDCC67A68FE83EE6C30EC08E0BF8317819F7B1A5C9C27B6252D48B80E\",\n          \"k\": \"FBF22CE8BD5102D34529C46FBA28B1BBC9787A570C0DDED9CBAA48D29D76725E\",\n          \"m\": \"5729B2AF60A4A5EE3BA6D7F255D7D2437812579942FF2C6F48611669135DD695\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 54,\n          \"deferred\": false,\n          \"ek\": \"6914CE520C02BF6687BEB9330C704296E40E350303EB356AFFE66FE8C11A83102472EBC96B6634E76C19B89A5E3E4078AC751480B66288120A0D9B69D78387CFA44F32C08BB7768B292BCC03141D77871F5405ACD1403564A27EEA55298AA0544460CB1C8AB9E4C006EC2A1FB5B48F39745D66FA899C456DE0CA4A5CC4B2E4979820F635B5A286ADB3B8AEA97855504935E21C37E226DAEB41D2B1778EB84F764651B66ACF2E7B44FDD43E037400B50C97485047B06BB5B3D1A16FE95AAB462305933A3C730EB43CB6D5C10D9DC25FE0F8085A689C0C611700F894CAF593BA4833466BAC987211BD548C81A2BBDFABB5CCA192FEAB0E1AF3774F851A646281D1841681A40A1FC34EBC8BBD731BBAE927C403BA9E2993588B885008317965F9967D249EAEB85C650C18251AC22930A6A68366F72037B2250CD9B5A568766842A812C3C7BF7106CCFB122007808A84236C2E5A88BB111EAE388F3756B2410776C6D745F770208F5B8BDC087F16F86D52098B7EFC2416A786772124D41187546BB8681672951960C6148591713C19783186A189806CA9D6EC3BC0BCB49404362B82B311E8C970107C9B7A3D6D526BC59C48BE93C699C79167C63A45289AF1773BA21A9A2F99C6A982034914B55A301C27C73D27B50AF02B35B6307DE2F24B1453064F847308836604C56C1ADB3306114AEBA38331A56E6C126445EAA258E25FB692A761EB62FB1BCC88AC79E5EC8CB583CD8ED799D00516B1021958333394B30166718EA0A600CC37403115516768BA1F110F4F27042D589E719350931735023604BC6B0412B67423826EB91711ADB11AE8067C688B080CCC3939142CFE33C9D58710FDEC082A88903E9238D3276316018D38A841930C919A2493A06051CD9CCEF8E46D62F61AAE1A8600A291B55C35EFA8974F1CBE0DA10005A68659904E13631BBD92AE617ACB7618223931C989EB45C5C88FCD74715DD17AA86704C57A2A2561533B562FA7E5B45BF7212682900E70CB2C8227BEC67CB1D7536B405EB6424CF2F5A5593AB77D579156750B78148C33E79BD9E798BA162FA3332A6A7AC354CA3818C174CA0B354B4C13564111DAB79A0BB95FFBA8A71355558A303FF4E9333A708DCA860915B19EC026B7C4022635E404228776931683467337372B7FF7665E9DF38CD11921594C899AA3B55852B76FBCAD0159254B91B06C412DBB5C998A283314A673FDE81009A6C27F733554914B3582C31D3B2B7FA80B9BE6157122ADCC891B21E43AD0343CB014455BD00DFC2C5A671A86A884A5757336D267ABB2F61E18A68CEBD24E09311AA6621B09E4437CB8C6FFF7A0930771D48AC952E317DCE9A5C1261DD83287D7B950ABEBA6C5300105188DD4B4A992840FE996AE2C3B20D7BA00B11AB9396C73A5DC5477FC0141171F6BB05D4806CC9B244A7AF264D84956FFB8A67384CC522BB1335737D5A680ACD3AD478191638A92A0E7292672A75982AAA8A93E321CAB0DCC616A4B9C344735BF6521FC86B0AA3CB1568A9EF7B19AFFE9C352B5B84BD7B66B07C7378B23B601B88C43B794E25DA22083AAFC52682B2CA90302453AA595C6B43E24A84FF07BF298CE2FF1CB22C6BAC6511784AB4FD5AB1A3B376684867951AA4C78986E04A639E8770A77BBA187F08F7E910AAC433FB5461D374C7022693594FB0E25E52882D310495B1340820AA5FA13E187B6335460C37B0DC1848D7CD6C85711CD292994184481560837820A2B9E3A09B34ACCD1E296BE3816B26764DE9B0B16ECA200B21B9E338014FC5F99A8323C657061C59683DA62B22A370313BF5150476DE36B09912283544E2DB6398B1168BC6A33A257605DB863D3C4A6F190488C402D07C754245C804AF6CE9E2589904666AA335B09C35B78731E14739EE2829AFDC991EFEA87BCB3529EFB1677D5378E59B31167792C89449F2A7BDBE01C8B2883C9B880E626AB1837836BD9216118302690148308A944B1B20D92089E113DF5845AD2F489DEB51DD4D4906E93AB9D027D89261C0317561710A033F55B6D19756967159D70870446A48E6B1B26E05258210918C38DC5985D832556FF6872ECB2BDC1A009F8071DA7992F3249AF49C675DB4502F3D235A63C6321B56DE227AC0AE436E44723FF293FC2979DFEF64B9686A3E28597D5954DCF3594F10EB4D23B649636B79B831AD8AEE1E86B66A18D9A3E3F249BC38D79AE\",\n          \"dk\": \"F13845558CB25F9604BF64CA636A3086E28549689AD94450429C7E1D3A556EF52DFD543ED6209423C58827149C8F121DB21950D06C2D2ABC883E260178CBC5B3FC4036CCA60B147B7B31BBA888913A2B16F0956ED922645F8AC26A9607A5B1C984FB5B983BA78838642B3789513790B738CD2A7805EEE46FE88A645CE282DDD823A1B6BE3B19503932A0B8E0BC0CD89AE9E657F8F519283648638C67545CA06BF7A3DBEC594358C2FC21528940300774A60B6316A7160A73CC806513278ABCB10214922C266E2339CAD7FC921A6C643432766A617EFFAC457F70087C923DD9E959E62839C1287F2611C4DAC7548E536E8A4764CA570AF315C2A46CAC87725D04003EC9E261B6AB2CFBBA931F76323BB52EBDBACD62E0418A1C5DFAC0AAFB556D33D5979F9A29EA264DECF7B7E59BAFE4EC3A03588483D27C9F275EF5E409B034CBEE79B9DDB9C2A04964B11437930C9F54161283E1B1BCAB696A82C6C983010E3A769609AA51B75E53C012DA673746676EA9A7CFF84016DD28B3554060E6B4B6DA9334476A230CC12DC4458B447B27A4D427E45256A6A4BAA1705AF7BC5BB19406D828B729D27147A93B52A4416B460EB72896000705956917F74578424179F4A42F7BF61999B9800294757CD992A1B3B17C08116B0B942B4585E5728AACA79FD8E05B5F90478B56276AB0B7EA18B2DEE307A8C0C017200F2DC687C6D8453F8813807610A043B0C0124092910E82D84BAF169B39E5AB55FCC5B7DA8749998D26836CF30A3F1393750EA670E10A763C64590E3CBF20380F6F329468603E8C5092477AC1A15081FB977A5076771ED2B127D82A555905BE784D5D0477A1D01FF450B863026374287F925AA3518359EF1236119C1304A28E6503480E72273493553255B416032841D907BE8B709C3C145EE890ED16C7F5AA9B8139715307C846083A23550688989CF214A05B4B8C4BF81C4190338A4707DFEAC69D54399C30A046259C7F75435DF3A1105C1F095261283612223582F8FB7258003597E96E5B062450A3665369B962B3B447BBB6262242043B4462721943250BAA684CA55629C8591AB7A65D0AF80B772190418083D06BA7943251E2AC5891821730701DBD2C3AF4678563B8590572223CE14DDBC59AE8E0BA67D8370C2092389B060CF601B433216AE04191E2C608DBC5D6093336884773562BBE7036858158DEEA032B505E8E275B1C7259C4DA6EE450AE1D108FC592C7BF327D2EC484E4586E74C7930233851FC7AA4BE99267D56E4DF11624762E78241D051BBCFE710F78797069364133D8B80DA2CB15E96DEC9A82BCD5B80142AD054532CBB87033B55C22987E7F9B45D007866EE057C1371098783D53662E33A9249B94ADF038554A467090448FCA0543F7B00D29A7B4FFE95D5BDA0DF929B7FFE619BCFA46C0F632FBA13014F09BE589C1D6372757877E62579F806336FDC6B93A210A04E64B43A693FB20C333259C1572930A778BFED83F2C1CA974C0701DA3576720A50185A8AD36C2DA2402679899BEB34B1732796F3B7A939B48D9C56164820531A755F7128C3B7517F29235E174CB72309239674FFAE391A35A6DB97785190C21B256273280422829806C344B3885C2C057558538631F82871678649FFC3F152B0D31B6CAF6F0CF7EC821BF80579B75B099A167C0680D946AB236E3525BC160A0A184F1C13194D909C26B3E9DD18236BB6FC4D71BFAE589A2E119A6EC668BA39965A64A3A920BAFAA9E1DBC18CF143A3DA9461F3050CF4A4CE1C53041972C81E2818B9BB332504BB2F40DCD18A8D852C0B9DC4A72C03EE74922F0596F119A58282A28BA6813D76AAFE9369D4C4B333CE5BC1085676AD58180CB20CBF829827207DC3565EDD1B435A5501803BEB4034F48C07D737AB924871B3B0C879B86976E2C8ED769A6CE8771C947B4EAF75229B8508BA85D41E9529402BA5B626975FAA48C08CFE5F6CA345A4EE105CC5E01B9CD826E9EFB7EEA3157CF5B34A66839ECD34CA355311EEB91A61A5EF6301CCEE5B60D05A4D3D02AF4D8CD1FCB7C914683F53B2CB6E620A2367420043927CA88129C2561709CD5C6AFE7D0A00D3BCD8DA9AAFE605E9F0291B5097806E112C773CFA81C1192811D24ABCAA691600CFBBFF230BB86E781C37B7EFD547DFC79208516956914CE520C02BF6687BEB9330C704296E40E350303EB356AFFE66FE8C11A83102472EBC96B6634E76C19B89A5E3E4078AC751480B66288120A0D9B69D78387CFA44F32C08BB7768B292BCC03141D77871F5405ACD1403564A27EEA55298AA0544460CB1C8AB9E4C006EC2A1FB5B48F39745D66FA899C456DE0CA4A5CC4B2E4979820F635B5A286ADB3B8AEA97855504935E21C37E226DAEB41D2B1778EB84F764651B66ACF2E7B44FDD43E037400B50C97485047B06BB5B3D1A16FE95AAB462305933A3C730EB43CB6D5C10D9DC25FE0F8085A689C0C611700F894CAF593BA4833466BAC987211BD548C81A2BBDFABB5CCA192FEAB0E1AF3774F851A646281D1841681A40A1FC34EBC8BBD731BBAE927C403BA9E2993588B885008317965F9967D249EAEB85C650C18251AC22930A6A68366F72037B2250CD9B5A568766842A812C3C7BF7106CCFB122007808A84236C2E5A88BB111EAE388F3756B2410776C6D745F770208F5B8BDC087F16F86D52098B7EFC2416A786772124D41187546BB8681672951960C6148591713C19783186A189806CA9D6EC3BC0BCB49404362B82B311E8C970107C9B7A3D6D526BC59C48BE93C699C79167C63A45289AF1773BA21A9A2F99C6A982034914B55A301C27C73D27B50AF02B35B6307DE2F24B1453064F847308836604C56C1ADB3306114AEBA38331A56E6C126445EAA258E25FB692A761EB62FB1BCC88AC79E5EC8CB583CD8ED799D00516B1021958333394B30166718EA0A600CC37403115516768BA1F110F4F27042D589E719350931735023604BC6B0412B67423826EB91711ADB11AE8067C688B080CCC3939142CFE33C9D58710FDEC082A88903E9238D3276316018D38A841930C919A2493A06051CD9CCEF8E46D62F61AAE1A8600A291B55C35EFA8974F1CBE0DA10005A68659904E13631BBD92AE617ACB7618223931C989EB45C5C88FCD74715DD17AA86704C57A2A2561533B562FA7E5B45BF7212682900E70CB2C8227BEC67CB1D7536B405EB6424CF2F5A5593AB77D579156750B78148C33E79BD9E798BA162FA3332A6A7AC354CA3818C174CA0B354B4C13564111DAB79A0BB95FFBA8A71355558A303FF4E9333A708DCA860915B19EC026B7C4022635E404228776931683467337372B7FF7665E9DF38CD11921594C899AA3B55852B76FBCAD0159254B91B06C412DBB5C998A283314A673FDE81009A6C27F733554914B3582C31D3B2B7FA80B9BE6157122ADCC891B21E43AD0343CB014455BD00DFC2C5A671A86A884A5757336D267ABB2F61E18A68CEBD24E09311AA6621B09E4437CB8C6FFF7A0930771D48AC952E317DCE9A5C1261DD83287D7B950ABEBA6C5300105188DD4B4A992840FE996AE2C3B20D7BA00B11AB9396C73A5DC5477FC0141171F6BB05D4806CC9B244A7AF264D84956FFB8A67384CC522BB1335737D5A680ACD3AD478191638A92A0E7292672A75982AAA8A93E321CAB0DCC616A4B9C344735BF6521FC86B0AA3CB1568A9EF7B19AFFE9C352B5B84BD7B66B07C7378B23B601B88C43B794E25DA22083AAFC52682B2CA90302453AA595C6B43E24A84FF07BF298CE2FF1CB22C6BAC6511784AB4FD5AB1A3B376684867951AA4C78986E04A639E8770A77BBA187F08F7E910AAC433FB5461D374C7022693594FB0E25E52882D310495B1340820AA5FA13E187B6335460C37B0DC1848D7CD6C85711CD292994184481560837820A2B9E3A09B34ACCD1E296BE3816B26764DE9B0B16ECA200B21B9E338014FC5F99A8323C657061C59683DA62B22A370313BF5150476DE36B09912283544E2DB6398B1168BC6A33A257605DB863D3C4A6F190488C402D07C754245C804AF6CE9E2589904666AA335B09C35B78731E14739EE2829AFDC991EFEA87BCB3529EFB1677D5378E59B31167792C89449F2A7BDBE01C8B2883C9B880E626AB1837836BD9216118302690148308A944B1B20D92089E113DF5845AD2F489DEB51DD4D4906E93AB9D027D89261C0317561710A033F55B6D19756967159D70870446A48E6B1B26E05258210918C38DC5985D832556FF6872ECB2BDC1A009F8071DA7992F3249AF49C675DB4502F3D235A63C6321B56DE227AC0AE436E44723FF293FC2979DFEF64B9686A3E28597D5954DCF3594F10EB4D23B649636B79B831AD8AEE1E86B66A18D9A3E3F249BC38D79AE684426A70833DA1EEB4B57F24F46357D5F7E2BC00853A19775E51394883FD13808716659B02B188799AF5A6BB44CA2C61E4453C93B8AFE22EEAD4A006E31AE22\",\n          \"c\": \"4EDB49DE2FB344B6E0CBFA6023FB26F38B58A6378247CEAEDC9C375B426C2AB0AAD40FAEF291E7022CE4A71CB4550A8128D627218864FA4FCB726778A560C6D2EE40829024CF2077DF34575B37B5FA95D9F1645C7C8121679E4E2D96B591203266CEF61A137039637BB347C828C550B725C551396E72976D23A354947200BA37633ECA962A164A7780B7E737BCF92CEDA690E87A28491C95E751DCC89E65BF511514E0D68A0CAF8EF6AF7207066FD10CB841EA7A2638EC1B652FD43C256CD207AB20B32BEAE063D3EEB063825FAF3C82D978CC01FCDDDE2F093E6B76ADBC6FAEC94FF4B3DD399AB7A24D4DC79BC9A70178A10D31CFA874A34A8953B2BBD60318F90907A3FCE6B85A45954FAC3143139EAFAD8450BB225E21AD4D40BEF3A812D26B1EDF5495523048FF8E7B646BA657F8123ECD950EC5A037FE6476B23466059F372FCC934B47FCEBA612C6D4BEFD6A1AA9553D376EE2F0DB2D2CF763C4C2D3B9DF0BE73A39C785A8B1ACD2CDAB9748443065F6A8FBE891A7B20CD2EB3D0718522C60B6A3ADB949779B17EDE8FF21798D6515758983570EE14F7A7C092CF91E53DEB45555EB0F888BAF4C6BF403DC0982C461E5D4EB51256A5D91FD0E41ADB0D15FD2AA7E2EEAFC12CDB508E03EC2285664D317130650191F578F4DB195D3FB2AEE0804B81939AB040A9DCC4B0523F18599E69611A83B2CE7DE1B77E3DB031498F7DF3F1B95F6ED23FC9E716B365DB32E4AB1A599D41E4C13EAD0BBE635329688122C13C1563D6D882160D75C62B1D72448A2B3F7415C1EC1D87E1C6D1CF3746764001698CD079BD9DC25C8D31F981AD1592A1339708300892187955163F74C937D61635842D07F919D8AAB565927D81FCDA514467C7C22F81E9F2585B423092D6AB13A97697337EA2568E268F84739AD1C04F2A2214919F967958500DBDB448559CEAC825B9CCA2B16B94D85B3BA2E0F2D8C22AE3E9267E73C30CA52E2EB9EBD295F6906A88277DA0FD7BD7E2C986561177B7E1CFD204368C6A82F2DF63DAAC040DFBD3F92C300A49A8884B8BE2C2C5EA32135640BAF8CC22547B3EAA81E259AF2BB1C67B45749ECAEF090CBDC7A3DA9DA00E2B879453EA20EBE8A73AD6E37B81D041F2BA9DA99F8140E0793BF1257F809C04702B2FF942BC9E6DFF57F7D477F361513A21D04ECBA1F17694E6E82347AF22C04CF8FBA55E6271A128BB17CBBAC1B07327E56B4904151EA709AD5E96D2AA731507C0C32F1F79EA64B64D172F7591F14C3D3C1B27F179FF8683B5C89BB63F790C2AF58DE6C529FCB0156ADA6A14E752D8D7F7F1E5ACC2FEF83D6EB0206B30044008E18A6C51C01E949B64243A889FA568AFAA6D3B39A04B9953090D39B7C127BAD662B8C146279E554FAF92892819F015C2A87604793E14B64C13F21A728753DA36E67CF7A9CB31E4CD48EFBDBC875EE29E3ED797D99127AD84361F285A117897E66908220FF417BB560268D3AE754E9C8C7981F210B6F3A0B0EBAB428EBCBFFBA22A7E9FF52ECBE3D9B12E12CAD2E80EAC592E42D17290E3B1E68982FCC7198C8007301E026FC4EEBE482F5132577FF39EF4ED9EA44E6BF410D6203187890139650907CBFBFF75284E5B07A0546552F41903A85E5F074B80F9D484AFBD86EF53667C259855015AB795ED6864FC8DEF018471524900995A7925D5B28900C28AC06D79CB795DF93B46D480417C9BCBD1E1BF3876CD5A2874E60025B0F9AD7A0CC35FBAC64E17528A668F3C005EF0D3CE305BA400413DD0A28F5046180564D689D55D9E8380D766E0C397A2E3F26F70E4833353E4C30A1B007C299D0CB5DAB0A1273A1873E2F5ED10651AA844C61FFBFBB753512A5A3A6A105B9AFF1F8E9A92E0C4EA7998E7261460A83CF36D553992611EC607097421F1A7034297EECAE76DE14AF5011BFD8E3407AECCC47FBFC86A702C04BEC48B47D8CF4C7AA760C5F5323D29832857B86E9347DDDD05C7110A7B8AD3752DBB3A335786AA1C1AA0409FB33FD69F85AFC5B4FD9D7864361675B7796AC9DFB77B17EDD34B84A217966055A818A09E17B0490AA39A19F048A9294DA105F285FF698394ABFCDCDDD21AE40BC31E273A3A814BA54C211962D80784DE017C75A247ACF22CC657E11BF64DEB7696CA6E23BB12410B57708174DD9B47E903BD44193CFBE47719417DA52154E1B1A3EF89DC46EBC855061262DA5C6E933AA\",\n          \"k\": \"FBC5B7112172B55A75DD415C4CB3D5C46A54D90A3DE27BEA4CDD116B19FB99A9\",\n          \"m\": \"FE8AD6E3F3EF1FD1890FB7FF75A8CD9B2A04CAFA7ACEAA99D06D116B81039DEE\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 55,\n          \"deferred\": false,\n          \"ek\": \"DF970CC655479D5361B2C99F4E6A60F9D15D9941A36F126062564EEFC3C75871437C021BCE4314A243236FA06A1657B68526652AC65AB05712A1381084254D048670AA24A995096D139236D6A7AEC6A830A4CC259EBA039B6A6E451A0B29D0B4D5FB5FD365B2D81C0D54FBBCFEE19993FA70FB5C92B29C95F0065629D84760272076BC11C3B72B96BC15B3651093DC440F6876E8A27834560B61B8A041499172045ABB4223F0D1AFC71616E8B6958653A08CA776B8916849C81A45A8BCE158B055B69E2F7487D4962FE31061B4B60034567E85496503108CB70167AC4BCFE402A50763941A15A3AB8CBA7DCC220D707ADBE4983A16C3484536EFC860833A527A7388CCB89693FC21906B932BD21584B046677AB9C4E0A54325CD3DD5A78E17CBC3D8CECEDC096CE167E9048C35973F359B7E9B729F95535408919958790427B1CA951080C9A45AA451CADCFAC9FB5804F8CABA9F88C09412ADA0FC2553B5921BE60FA01505BBBA2C33A22B73A69EAFB530AA090ED1734D7561C25299AE954ABA49A0464480B731198A68F9583CF0663322A1A493B6E2A2A2B06CB18B84C701E4AC73E91765FA72641B86249C5817511565023B6719252268585F2B077A3588C0291B36D9CEC74A1DF4FB33A4B95DE0FC7EB1746FD4E17401133F583954B9C947AE6A9AB443CC99924913646F70B29387293459F6317919C177F87EC01CA9E3210455EA25429ACCD4020CF4E43F98999D3739979CE39DECC5361F152B330255444A05F13406F6742BB51156A07854BB0A91C0B45C0F1678FE034BB7B2558232C14F83C822A22C7AAB6934661682E23CADB26D9D9284B519A4595954BC56CBDD0062BF657898B24C80F3010B849E20B269BBC68A538749A6F06DA2D62BC7FA8A25074629D12D22C6517BD4A20D8531B26BB13D2C9B2AE1639243B803D30455A1C0E4CCB5E3C206F4A6BB5C65B3205C56BFA678AC4189CD108DA5C654F4DA7EA30B0D7B23985EE80EECC2297CDBCB6A2A6B3A3C474181BB42F9150CE57FBC6C8ECCE43990F8C31F5579C71C7DC9615D47506666D05BC59339052BB090078E58858780671A65A14CEA22018661B9945A0D0EA2679388C308460AA9367CCC97CE6EA3960AA04BDD28B1B98158E02BAD3EBC3A97F2BB3A1319FB6178B99B42016826F4814960355722B8CE3338BAE0E97084613DBE41064D0A6F3082CFE25A18978975AFC04D0E1C9CCB0C1DF5C9054B22912844077DBAC39E40CC18C87179D420AC782D40E3AB5294C5B5E786814918D048835BC5333F421B366B26568C20E832AEB0410605C3B78394CB1CF5108F5C7538630C8EE4A611412F75F1691133A93EF744D36ACDCE302516735B09B05776049DEEE674F894580A55B792CB4ECC5916D0E3042D3231C8E70DAE111AF3A386ABE5A251FB9F18B6AAB4D79CF16BC884BC0993266C35CB267A9AAD057CBAE4ACC071123CD8D35C86A7C23A6771FC5799CF29ACD8F08E6C0B719E3173162418CD6B008C52A29EC5A701C97ADC906BA4852F7590353B190E96BB8A6C67AB9245CF45BC7DA31B5888B5B6FD77912E81A9CB522946D77305303B6F28B6A89469E734B5FFC4009C341114768EF8C0489E66C7EA3106F224A9AB4B0E147911757521BF39587BF211D9DB8F9BDC9EC21B881AC727810B4D1303A38938A70AB3248BC547B3783F68A84C5C77053726B808825FE17BB8D5672D77E4A178AB3C8EFA8AAEE419A09BB31CD40692A4BA273A137530CEB7198C5F38CD960049BE317AA924ACBAE241FAC48AB540C6CF7A4E58CA5AD3076DD7FAC09DDB1B49DA3D5FC751AE812A416B098CD712ECB99CD0567AAC9BB48DE700120584FD09A201D49A77D16132527F5C0138868C18FEC89B3D593EFA912FF66C79A2832A1B0234F5F0B67091ADFBC507913A41A5C22FFED19109AA9EC0F99395685682F6934010056543AA88738F1D88ABB1BAA21D35585F5534EF6241D81476FF33B434B4C335323C77318156F75370E1403C390242E6C7A75A9D800C0CFD84AE2AD52B9A0A492D441131B68C27DCAAEA93C2E3E2C661E054D8228B03243539649B861927947C76500910A5EA0164D43865B62BB1B30D032334EAF296ABBA4780DA5F12EBABFD021C3FF3A164B0745C09328D5BA0F6127807824E2A7C8012201510C3368067847C71C50A9D8DE22AC182D1644E25A39ABFCE37DB3224F725F065\",\n          \"dk\": \"D6E3B9FCA0A85F25A724020E8FB38AB88BCBD82A7149897E5D0945DF34B0AE07B17055198A7A50A1328E844A186C375EE9B15008B8978235445CA39A73EC8941A14ABB6750B8B2A8CEE62CE44BAAE93585AA6945476B073D176EEAD288060801F9B71C27E366180782E0AC500E2105D6995E47141D25901632DC20D2D89AEA997B5031033F295F92C5C599A0237E1C24B9DB75C03A01C08A78E42A8A0BE3B469FCC341601823160581B529226B7FC161042A3680EA476F12AB121D6A3CB90C24DE501CFAE8C275239F4DB27D22D58CD47440052210DB6A291E080A51A34440A768B70085EF3251AA045FA40A6722C44619512D08E873F1645D0D542EEF6CB12F5B2DB3E3186403B9670A39E0F2CAD3F54998E459AD7647FA8B4F6D900EAE144547F9C506728D080AAE5B2726F5E5212036C5DF172648E6762E09792056C8269289CAD365CD18405A0C182FBC0132443701462B32F31F62E92DEDD4BACF3135F2453A28A92E0D308EED64854D009A700C0BC49B2BFF97C3D12BC481D131174B06AB1B9C10C92EDE463655506E13069B277A0A37201A8B3C6E3EC020B5882ACAE56A23B84232E66211821E2DEA3C9C8B069269229D59AB7FF7758A7C51FA388D74A85B0459364C3669E4B1A230E063AF9868F4CA31694B5E69752872B15FD4A6C577BC4B1821867C456DC6547954C8845425BA9CA5868135159417BD314BAC8BD8533E477B0C759F7FD48F1BF0281C79774A82927FE1264BC2BBB7D4AA479A76EFB9314FA856AD608793AA1D571775A7E363F9E220122047C84648F8596727817F80ECA00E0B4A046AA1255223632098E696BC0E40ACC6A5CACD84C19148B5B4716FDFC2C4CD477D923269E662370C105190983F45550FAD198AE6D512D72362E16C350E783DB9F4BAC4B525C163A20FF261E14A3757B8503C14ABB7F0C7618737A2E0822DA837D606996C073F389A8B68CC2B25170A282C051BE02FF9901FE3D53EF8367587824D4E8051CFE0731E676D754A532B3329BDE45A7F2A92A5055D96560E5B902DFC67B4AEF100E90115E6393BF70397981CA1DF6C8FD6249941FB4D7849434CE52CC0257E98A1724E9BA47425AA21A0C465372D0A299643564EDBD44E3E4119CC063B7A12B969A40A38D33DA23A059968965B405077A8084439AD9D8495876953896827C8E02855F8CD34B288789CA03DB8C8D5914D538794B97B2F6444CCE53686EB07CB21C0BEAC757B3424CE969BBA4685081F926B30791D63C14573C01E999455B1395B6EB19102B7A70CB524C5DA378259461D1284E1EACA66419C166306BB6A8980A54ECA11C2797C988716ADB5E5BE7F4B0E2C2C0BA6118D93A934D8980742285738EBA7F8997F2C9B4D6A3C18968772C821A8E7A9A6969B948CB39977D88AF487A7C7795F4156BB076588A2DC5C2E361EA31BBEBD689194D55D8C43A7420B9D1B0842C0FC45BAB664B310657F57B8BA72A5CC34CE9A1BA939164ADFA40A643B4EE786415891B5F67444567A5B85156F0CB4931F7A7E50157345CA00EEBCA7921A8B0AB00519C0AC2E174452C69D3D963D4FAAA68F6066DAF78D25F798F333949CD80E055116C3F687BA4B16E1A50820155BF6D0740533C4A9A7AFC863CB2C44175DC0BC3D2026EC66408CF893C65B8E31C79D17F33D1C101963D46C0E77C265A977E2D45969B602550873EC3AAE1536339BF51CC1F033B1130B3B269026331B17662B9DB79CE42078FA870946782BC169306DB8034AA72D38572342F621E08895303165A5303BC8B24C81062A14A6B84ED929127920E3EA2763A6A4E65060DB9565DC13305F112D61685ED4A1A8B52969143A7BB42B6B76A3C2FCF96242AB3B58B752188670D4F14CC6BB278C31B59E19A1EBB9905D019B21B8B054933B006823100B5015054A9B26587D9B594B083F01D80B43648A428A514D11814C42059A0C5EA8377BDD25351FD000178485241BB72C28CE0EF210052259DA97C589D66657F4A6B6BACB54F417ABF485E01A1B58FCB0D455925A21605274C16684C1C69034672082D6B923F08BC339753CB0A4329E37C6A7DCCAF3801D1EB72A49C03E7CD0B1A8DB9B44106E33ECC8CB432367C7C061E122DB7C31B8E2AFF9A750F579AA1561950924A44ADA9A60ACCEE2159028C356A431741D0037DF970CC655479D5361B2C99F4E6A60F9D15D9941A36F126062564EEFC3C75871437C021BCE4314A243236FA06A1657B68526652AC65AB05712A1381084254D048670AA24A995096D139236D6A7AEC6A830A4CC259EBA039B6A6E451A0B29D0B4D5FB5FD365B2D81C0D54FBBCFEE19993FA70FB5C92B29C95F0065629D84760272076BC11C3B72B96BC15B3651093DC440F6876E8A27834560B61B8A041499172045ABB4223F0D1AFC71616E8B6958653A08CA776B8916849C81A45A8BCE158B055B69E2F7487D4962FE31061B4B60034567E85496503108CB70167AC4BCFE402A50763941A15A3AB8CBA7DCC220D707ADBE4983A16C3484536EFC860833A527A7388CCB89693FC21906B932BD21584B046677AB9C4E0A54325CD3DD5A78E17CBC3D8CECEDC096CE167E9048C35973F359B7E9B729F95535408919958790427B1CA951080C9A45AA451CADCFAC9FB5804F8CABA9F88C09412ADA0FC2553B5921BE60FA01505BBBA2C33A22B73A69EAFB530AA090ED1734D7561C25299AE954ABA49A0464480B731198A68F9583CF0663322A1A493B6E2A2A2B06CB18B84C701E4AC73E91765FA72641B86249C5817511565023B6719252268585F2B077A3588C0291B36D9CEC74A1DF4FB33A4B95DE0FC7EB1746FD4E17401133F583954B9C947AE6A9AB443CC99924913646F70B29387293459F6317919C177F87EC01CA9E3210455EA25429ACCD4020CF4E43F98999D3739979CE39DECC5361F152B330255444A05F13406F6742BB51156A07854BB0A91C0B45C0F1678FE034BB7B2558232C14F83C822A22C7AAB6934661682E23CADB26D9D9284B519A4595954BC56CBDD0062BF657898B24C80F3010B849E20B269BBC68A538749A6F06DA2D62BC7FA8A25074629D12D22C6517BD4A20D8531B26BB13D2C9B2AE1639243B803D30455A1C0E4CCB5E3C206F4A6BB5C65B3205C56BFA678AC4189CD108DA5C654F4DA7EA30B0D7B23985EE80EECC2297CDBCB6A2A6B3A3C474181BB42F9150CE57FBC6C8ECCE43990F8C31F5579C71C7DC9615D47506666D05BC59339052BB090078E58858780671A65A14CEA22018661B9945A0D0EA2679388C308460AA9367CCC97CE6EA3960AA04BDD28B1B98158E02BAD3EBC3A97F2BB3A1319FB6178B99B42016826F4814960355722B8CE3338BAE0E97084613DBE41064D0A6F3082CFE25A18978975AFC04D0E1C9CCB0C1DF5C9054B22912844077DBAC39E40CC18C87179D420AC782D40E3AB5294C5B5E786814918D048835BC5333F421B366B26568C20E832AEB0410605C3B78394CB1CF5108F5C7538630C8EE4A611412F75F1691133A93EF744D36ACDCE302516735B09B05776049DEEE674F894580A55B792CB4ECC5916D0E3042D3231C8E70DAE111AF3A386ABE5A251FB9F18B6AAB4D79CF16BC884BC0993266C35CB267A9AAD057CBAE4ACC071123CD8D35C86A7C23A6771FC5799CF29ACD8F08E6C0B719E3173162418CD6B008C52A29EC5A701C97ADC906BA4852F7590353B190E96BB8A6C67AB9245CF45BC7DA31B5888B5B6FD77912E81A9CB522946D77305303B6F28B6A89469E734B5FFC4009C341114768EF8C0489E66C7EA3106F224A9AB4B0E147911757521BF39587BF211D9DB8F9BDC9EC21B881AC727810B4D1303A38938A70AB3248BC547B3783F68A84C5C77053726B808825FE17BB8D5672D77E4A178AB3C8EFA8AAEE419A09BB31CD40692A4BA273A137530CEB7198C5F38CD960049BE317AA924ACBAE241FAC48AB540C6CF7A4E58CA5AD3076DD7FAC09DDB1B49DA3D5FC751AE812A416B098CD712ECB99CD0567AAC9BB48DE700120584FD09A201D49A77D16132527F5C0138868C18FEC89B3D593EFA912FF66C79A2832A1B0234F5F0B67091ADFBC507913A41A5C22FFED19109AA9EC0F99395685682F6934010056543AA88738F1D88ABB1BAA21D35585F5534EF6241D81476FF33B434B4C335323C77318156F75370E1403C390242E6C7A75A9D800C0CFD84AE2AD52B9A0A492D441131B68C27DCAAEA93C2E3E2C661E054D8228B03243539649B861927947C76500910A5EA0164D43865B62BB1B30D032334EAF296ABBA4780DA5F12EBABFD021C3FF3A164B0745C09328D5BA0F6127807824E2A7C8012201510C3368067847C71C50A9D8DE22AC182D1644E25A39ABFCE37DB3224F725F0655D6E5BFC5F96134D2C183ABB3911441EC66B794509ACECEDFB7359BA96E9097A6AE0162D48029F424D913B464EC63CFADB3A377109A8759849A8D8542508F050\",\n          \"c\": \"5F7721D08E0CABF5F01821C90767B448F4F53DAAA7ED10FC21702CEE8FC28C32FE20AE36052291C7887B9E84D3C22EA9B401F870A12DFDDC32F1A848E0EEF27C9B7A7A3AD57340946A180596A38757BD6B2570FD92102528B2D0DD804D7F4A76620DAF0767428B3B63B842512EE6816B86B5C5AE08EA8B3A2D61431F185382E8957D399C3E32BF832322915705700EE2A19CBBFD12069CB903F30053B0FFEB61149266593F0733ACE7356455A6D70F7EE6BF7D07199FDEAC313CEE1E06EE5AD50E89BE5A39A73C82277E67DDAB9C88FE4E734FEF19065ADE9C548CB6F91F90C6B899E5FB243284AA1F16CF19F607E18AE8E8A01BDF06AA7F0EEC34E7C1ACA8C407807D986B6B64AAC6A6F5D395AE57A48A4C135F17DD2B5B62EA5C3B83675545787F5EF43832A908D5D8FDCA3D7E60E884F70EAABB062CFC0078539FBE68087CA01CB0401B634A275EBCE328633428EBFDCBA373505BEE1DAB5B531C90C4136036D5981B93CAFD71F6F9FEFF5BF2BF5B01F44FE57F87ACC60DAB8C88DE57087E08506097F31191C376B40048D0349B7F6F5A77E83A8A0B0F3C317AACDEA059F7B7949C2F14087924A28B752206ECEAE424BD7F3B379F29F74D3ABE320BDE982911C15FF6990A3546A8391AC98ED942C63290307F10C398F85D5B0CC11758DCA79B320EA26BDE1F898CFB061882B984FCE7487A69AEB5E4DFECD42EEF146B93F38318F5766F263C67771EC67B0644FB0854E77B02DBA2C05797D24EB1D219F473A732FCDF41317259C0AD919C0AA77B329CC8B3A448C4BE35CDA609F0C4EB4A01B801F380A4F5441BAA6D415507A4BAADFD80E50B563F570ABA9701B264AF7830482F536B49EC2EAC2ED19E853CD51A0C2D3F234135EE3B0A627F1070DCA5016CA3F381333CC546EBED09BD5A7B53B31874FD7A7391451C0195CD64C4256089D709182F4D5DC83CBABC94F53D94FAFCD60A4DF94E06C51F95C7854B5A9A34BB3A8BB06E6B25F2C4098D3A5010DC0793EC23AFE1F55B667E2D2685CC8D335509E41217F05DC99B9998369845F4793C55869300283E119A0C6445A214B5DB10CC62D7214D3DA936A264F566E28B06D9EE12DE50CBF2C68B6DA8E80AC1B1780E5A2EB95CF40F1B947AE4CE0AE1D169408D63FE4209522A5B4686E32E5F8EC4DF81B4EF33837D9C120843234B7431D997D1EA678430CAA9F0228518C8D7E1F0FC3AA31900CAC9EC0BBE002E478527F8E2B24A41253851B4954FB935094C2E3766BEDB53C75CFE7A76EDEA1365B925BFA047D8A3463E5F5709EB39C7E69B2E1B58375D177522C25BFCF29D8E58EFD215350C76CC4D5996AD0FBC7E3ADD94A87E154C49C68F5F18AD4728C9EF1D5040F795EB62DD8CFDDCC83DFF8C5FAA7861D0AE62AED8B232FAB8DB8F64A62A613480D1087F1430D6783490BF29E418BD1B524008676FAF04A29ED9F07FB8E3F4800E2217DD5176392CDEB059762AB5515E243618D7D6CA7B7988CBE52D7B76D0EBB87420644CDB018C92BFA53B57C116643D3DA1F194687E8004BEE958FE968F90E50153550992EFB49AF037D28835E292CF8867A7FDD055833700026E8A97CE9C65A446E9B424EDA15B036DD37C4122609CF70C04E238078A1B759B9A3901AB3336AB47D208FAC6D8B45946D9533F9F48D5B44FE228CEF14F9B9E67F140708E2CC4C9F8D4F55B57658566D9A5E2BBB6554AF989C89A0ED2C88934477F59C14480AF77CF7B3773BF5FA29D0BFD89DFFE4BD597634DF3FCB8C404CC935651D4EDCAD17E5150595D0E7A2599965CAC799C30D37BC2882E0088FEC1DD687471F194130925F931B0A6BC56DF3FA0962CDCB75C008F30F5664B065BA94091C7961BF7549089EFEA6D79372A7C21F2728363EBE32654A5E2AFDEE5E2E9584A658868F48F481032BCC65930031CFE3B777B2FDE5551013481D7584D133772E5230ED502D4208C6128998F140ED9589530B9CD312AE221A8DF4CF979F0D65D8556C5EA30C310AA84E493767C583954931F151DE4DB5491869875B89B7A49294044FFC59BC732CAB155BD549043F30968B821463E4F61B6E10C151A1F1C873F29C49D88B1C075EBF120951018A6C289A6DB7E352557F2DA5D2F2FD8A7AF33FB9664650B63EFA6778C4BE12B104BB3778193C6683B1EACF0264B448A195DB8566951B0BFB3D64EAAFDE75ACE82C067CD1656D88440F029FBF010\",\n          \"k\": \"7F8443BFA35178C5DD7008B9D8DDEFF28BADAD893E16313FCED911730A9F0B3B\",\n          \"m\": \"0AA3B1F8FFA63F89F949DA18B6D8570BC5811F85A4BFB293E9D411ACD43C3227\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 56,\n          \"deferred\": false,\n          \"ek\": \"8FB403D0105FD0EBAFD6505968774504C7823474A1C26280A178BFD33368E14491C100CF58379A55575D753B75456196B5257D2B4BCBEB586EB0397BDC20B798DB6F12A8245FE80DAD6026DCC76EEC5B6DD6F9B5EEAC5211D408CE0553B11394F1440220F03FBBF4158A1C31F4735BB2296AF336C4869713BDD14D686940EC57CDAEFAB8FEBA2FEDD2C51AE61B4D67C5FA6B444FFA2E150788F1D4A12CC7875EF70711A38207C840C2A0337BC3CF42EC1F81464F731C5F460820CD473532983EA710846279B000586140A9B8F2B873C200B8F8D55D1A422F2668103FA8169DB77EA5957C4FA9A72259B60DC27EA16BCD31A051BB528246F05DE7B22398EA32C2230FB520A0D600CD8C049DF9BA6A5C665961344974385FA55525AE340342757BE2831807C552881C13BA133CB9BA2394453818D579787A6CFA356FA560BBDED6182A676126494D98A80161A4987B126DF67633E7433BCEF68690A12DE679512507AC1B9186367139213A3023418C69D0C67DF320C2CC88BB7045543C3380E4A953A03E63854FB986BDC898111F6A7A8FCC74A4F3971F21850E9770CC07CB0C5122CE8B2F873813AC558D9D4474C80084F0A924B3E2B7F4F4137EF5007F0C5605658DED4677A314A44D783C2E3222683B4DBEEC349FABA4430AAFD49160B6C91F2AF4092EF63EBEBA1937705F18A387A6307A733773206B8F2D4052D4C36D2F557FC0E226266116CE41B59233363ECB728C0343C0648AB1045CB03A1C225B89FA488114A060AAF48A68CB7B20A3C55A71ACDD7742DD05AA8495815646353DB21D4A7235450076CB94041D94A5D0245BBFA3598E870FF0E30FB81213B6F9916B78AEF3D43574CC07C9B5CF257A92876C49ECD73B5AC24DEC0BB25FBA54B29CC4A410AD40B5C6FAA398CFB82D2401878C79AF021CA0653A0FDA4B6474F942F93700ABA364AE3611B7666EDE2A8B2EBBA701AA61387500D674A9A9FB0351F06A5CB699EF73457A8ACE9E8B63184768363C50AC000220F2CB708AA27522149975BC951553EFD5B577D507279346E5D00F2EB31EF1DC1EECB59007027E65D28F0D10B7484654241984010B224F174EA69B1C36A302BC20B3244B030B231FA5532A4B206D3B3398E9EBB773B9CD9821BA688C06BABAB29DFB6AA2D427A258693BD1AB13276990B1775AC94B6BDA89F8866AA3A94003B3985086232A4A61CEEA8B1B711609B3285AD158BAE4C21581AA1541CEC7101AB7541098EC7093E442B21296ED172540972F52C04EAF6581EE942C6ADB287A2AC956D84B7F726158D8637962A40986B1F2AA1AD6762C08412F67D1A7F69B6F0F87AB022B1A48F4727CEC2277D1B157932B860A156DE79FEADB99DA7CA0D0B7A4A9D0B3686A100505369C47AD5DF8661CDBA888FC4C7EB1CBF9555548D87755C3232BC3467727AC4625B49145BBD95537585018A7FBA2A8834ED587CCA5F44884C87BF5B5AF8857A188B77A36A01B16F28934E38C9513B5A44926FF41212B13629E2986B8458AE3E9975BBC40C69214068BB78C127FFD4A664DB90BFC068B67106A43F1BC7B094E6048A529B974EF615517ABA2ACA75C97093487C2A265330EE5454985C6435FB989E100B5C9B80DB0A49421D6A75E1632F4B03F5AD96515039D8291C67A15902FE1C027133939183EA0434AD25B45CCBC0C69162CFC1CB527E392B1756E64561F4442BC5814098F33AC673614A8DA326F8818718603110836AB31778536A9C47BA820960CF181CA829A9911792D67DB209E9A66CAC38BC57A4CBD4AA3D07C3447F72D4231A69326AF512314D9AC918DA0BE19B624E5446180166D05F607B1EABC04B2A442DB33C71B66CAD3AA8E559662284807A69EB0469E8B321C73900A74C4A66D40B3CF382D53A12FFA26B9C8888516633E9F292052FA231404483FA41E42A9AC72312838BC7E17027C5EA055967497F3C9988D5A6C4076913DAA9D33C11D4335C850121A93C25A89E3AC0612087943A35E23204EEB12921675AEAB244BB53EA24C969DFC2A0F99BF925666C9A69F123A756104CFBCC88C5A2C8AD7A21956272E22F58035C140BDA371DDE7329D6728E2A05A150719C2D578FC511DD3361DF765C83BF2B04BA7546E0CB5C28C211489C96265335FB5BF6B3C3779E3CFF1C77379E336F3481CEF55F2F61A3CD1F98B88F29760A2BBCCFA7BC9270DCD07F290CEB1D75B0F5941\",\n          \"dk\": \"A707213F208CD438561CF949B2A2066EA1234F619588A6A9B9771E26055FCBC313E918C225B8A6E4234C3664965F20B7F722CE407A68A578888343621AB24E429B1C88C5CA352B98BFFC66B6C7B01D861634B64A986CB5BC16CA4A89C87C455FBF8B74BBEC707F67C552F9B03DA8C521E02D147192AF05BE3C394091B60934EA3032646DF68680A91396BC091CD8806A76D676693C2366EA7196DAC82D060336FC81FB4BCA553C2E21449A69E3C4F84B9906D0AEC2B9C18262C8BA231BA5608972A67B1396BF98D26839901F0BEB0EFCD907E0015BB21CC7F0C22E09B76969E8C09FEBCF105AB9D6A754F6EC6253F615445180BC8668A8D03CE68C2609470092A29FF2127990B14467F960C536B37BB4A980843C8CC76D9D10B2CA26823D513380655883216B5E49C9DFE62D14C1A37132BB072511557118C7B43AC4488D962989394366B3402E739BBA1F69A4F2512F3A50568B8046EDC4A319725E5067CE423B58F496399C44AB2A51C866A26323C68891F5AFCC5B0FE7D37AC31403629CC5B65B9A71E192EED60B7989006B824E0B510E3577C92A3751BD2AA5B1538742303700212CD3C54183724B351C40BAA4CAE46C79CD9383FE1719D333BD4FC33F019B3096065087039CCD21A48C582DF54BC1BDF4BB5391A1DFCC5EC3C57FB95BC9FBE5B611453812E936EFC35AA4C51B42329C31A39A2860C921D9B326F51FE47945877CBFC95178450972BB4A57AD2B2AF1D28AC1D7B01505AB18E964FA567FD9F53B397C2C0891408BEC73494A4563598BD2D1B4DD58603CA80C35A708448B3CFBD4B1490C9ADDD35FA0A4A6F01ABCEFA421DA989DBB506550194FBBA8623AFB8AB97B6E90B0570676191B23AD8D25C5EC054948A8416D13AA890B7FACC6CBA03B1E12F819564A44A9054594500F8F74AFAC8B188EB350FE35A8841A144950C5C75538E8F78621C07FDBEB9A187B0A12773700E0B3BFC7CA6B317B89783250C8263FAA6CA8B53CAA662E3AEC1F07B1AA9461ACE168CD873787B28A6ED501451D5CA306E0000621C35E8BBE57A8795E978637FC781CBC76C60663CAE160D4968988634C9B5937234C5F88BC4CE140763D0520F635BA2BC107049C78B826A1B8A23DB4EA58EB1BCD5D1931329C04DF199030B07F258045DFCBC426B36A8BD0A3A7AA55CFBA400654A879AB97342CA506D6579738913E702C7630AEF8778C4CF61CFEAC53F9EA8D17CC766A09C646662A14077DC95600783B9E372660FB2BB806D84DA12C64CD110F05EA1A8E373D3606083E3382C9EC7FAA649E61F0343FD721F99745F8E2ACFD0707DB053B6F67A0214740BB999D9B434E9218B388F73097F83EAE9792D4515B8267B1F650896AB9BA11C778E3897871B28893281FF33C0441C767556B4FB5852608B01909398115216191A98E38A5B16C8578F1748D69B84BCC474817714CA29534ED17CA995973D0E935D3221A394BAB5F70BAC95590D2791E5C724F77C09B5E0542BB1279EFB4C2F842BB2A8288DF512451D503727187CD3B7812B67257E107248CBF2B63C099228064020AC45B46DF110FEC3960ACD0B4F992745450116BE351123896A2489A76FA4739DAC06DA63CE4FCB6C2C46BA3577D37BA1AFB296ABD2819F324100106745E99B534606F67898ACD724D2F390F67B8AFEA0B7310491F86A31680755AD1FB78E9AA2AC61769DFC01013C38D7D9430F697A51FD627B7699656751407DA16AD8173585BA595ACB14E019ABEF921CD49BA131CA7DA2B2C7D5107356961973039FAF30E551478CD3C7137BA6637673CF715439A09A6D7FC955A35330FC923F09CB33D31B5E3F906EC3BB229C4813F2B1D4D449388507A9377BC100264CDD2C50D5B9E5410A92B7A6B8C5263896CB2BF3C4643F7142A8CA2CD571779766890ECC84453584EE666D4A3C651762E1E267BAF71108E04C847B8693DC1CB2D6929DB811D505588D03629A25753A6787A02B19F3111C6F025B33BC80905DB11A15282A97A031AF8A52AE40B7B83939F7C210D995E24757B589182D92903647256A921C0520445D9B177F4F33F77174CA76577FDA72289D49592FA0E58EC50F9EBC0DB40482F277A1F6381E4372289168F3675A4F3731378590671301A9164390DD3656EEACEC005954F3B3399E6045966434021168FB403D0105FD0EBAFD6505968774504C7823474A1C26280A178BFD33368E14491C100CF58379A55575D753B75456196B5257D2B4BCBEB586EB0397BDC20B798DB6F12A8245FE80DAD6026DCC76EEC5B6DD6F9B5EEAC5211D408CE0553B11394F1440220F03FBBF4158A1C31F4735BB2296AF336C4869713BDD14D686940EC57CDAEFAB8FEBA2FEDD2C51AE61B4D67C5FA6B444FFA2E150788F1D4A12CC7875EF70711A38207C840C2A0337BC3CF42EC1F81464F731C5F460820CD473532983EA710846279B000586140A9B8F2B873C200B8F8D55D1A422F2668103FA8169DB77EA5957C4FA9A72259B60DC27EA16BCD31A051BB528246F05DE7B22398EA32C2230FB520A0D600CD8C049DF9BA6A5C665961344974385FA55525AE340342757BE2831807C552881C13BA133CB9BA2394453818D579787A6CFA356FA560BBDED6182A676126494D98A80161A4987B126DF67633E7433BCEF68690A12DE679512507AC1B9186367139213A3023418C69D0C67DF320C2CC88BB7045543C3380E4A953A03E63854FB986BDC898111F6A7A8FCC74A4F3971F21850E9770CC07CB0C5122CE8B2F873813AC558D9D4474C80084F0A924B3E2B7F4F4137EF5007F0C5605658DED4677A314A44D783C2E3222683B4DBEEC349FABA4430AAFD49160B6C91F2AF4092EF63EBEBA1937705F18A387A6307A733773206B8F2D4052D4C36D2F557FC0E226266116CE41B59233363ECB728C0343C0648AB1045CB03A1C225B89FA488114A060AAF48A68CB7B20A3C55A71ACDD7742DD05AA8495815646353DB21D4A7235450076CB94041D94A5D0245BBFA3598E870FF0E30FB81213B6F9916B78AEF3D43574CC07C9B5CF257A92876C49ECD73B5AC24DEC0BB25FBA54B29CC4A410AD40B5C6FAA398CFB82D2401878C79AF021CA0653A0FDA4B6474F942F93700ABA364AE3611B7666EDE2A8B2EBBA701AA61387500D674A9A9FB0351F06A5CB699EF73457A8ACE9E8B63184768363C50AC000220F2CB708AA27522149975BC951553EFD5B577D507279346E5D00F2EB31EF1DC1EECB59007027E65D28F0D10B7484654241984010B224F174EA69B1C36A302BC20B3244B030B231FA5532A4B206D3B3398E9EBB773B9CD9821BA688C06BABAB29DFB6AA2D427A258693BD1AB13276990B1775AC94B6BDA89F8866AA3A94003B3985086232A4A61CEEA8B1B711609B3285AD158BAE4C21581AA1541CEC7101AB7541098EC7093E442B21296ED172540972F52C04EAF6581EE942C6ADB287A2AC956D84B7F726158D8637962A40986B1F2AA1AD6762C08412F67D1A7F69B6F0F87AB022B1A48F4727CEC2277D1B157932B860A156DE79FEADB99DA7CA0D0B7A4A9D0B3686A100505369C47AD5DF8661CDBA888FC4C7EB1CBF9555548D87755C3232BC3467727AC4625B49145BBD95537585018A7FBA2A8834ED587CCA5F44884C87BF5B5AF8857A188B77A36A01B16F28934E38C9513B5A44926FF41212B13629E2986B8458AE3E9975BBC40C69214068BB78C127FFD4A664DB90BFC068B67106A43F1BC7B094E6048A529B974EF615517ABA2ACA75C97093487C2A265330EE5454985C6435FB989E100B5C9B80DB0A49421D6A75E1632F4B03F5AD96515039D8291C67A15902FE1C027133939183EA0434AD25B45CCBC0C69162CFC1CB527E392B1756E64561F4442BC5814098F33AC673614A8DA326F8818718603110836AB31778536A9C47BA820960CF181CA829A9911792D67DB209E9A66CAC38BC57A4CBD4AA3D07C3447F72D4231A69326AF512314D9AC918DA0BE19B624E5446180166D05F607B1EABC04B2A442DB33C71B66CAD3AA8E559662284807A69EB0469E8B321C73900A74C4A66D40B3CF382D53A12FFA26B9C8888516633E9F292052FA231404483FA41E42A9AC72312838BC7E17027C5EA055967497F3C9988D5A6C4076913DAA9D33C11D4335C850121A93C25A89E3AC0612087943A35E23204EEB12921675AEAB244BB53EA24C969DFC2A0F99BF925666C9A69F123A756104CFBCC88C5A2C8AD7A21956272E22F58035C140BDA371DDE7329D6728E2A05A150719C2D578FC511DD3361DF765C83BF2B04BA7546E0CB5C28C211489C96265335FB5BF6B3C3779E3CFF1C77379E336F3481CEF55F2F61A3CD1F98B88F29760A2BBCCFA7BC9270DCD07F290CEB1D75B0F5941582D82EF332E43017214599D3E49B9F9CDF7E5EF8417B8A95EF46A21618AE908AEA17274D9BC31873ECE5211AAA326A34048F067A162DE56CD27FB17CEE38628\",\n          \"c\": \"78BDBF1509D64B097F89F9158A5474E57CC04818DC01713DADF6C574AAF9115C23C641077EBE2CB2B713066501DDEB196A72067639F30890A41EAE9A565E6EF4735F1DA233FC7647F1B9397BD00E7F387B58BD4C90CC310172AD52BCC4EFE4FF533A096061EE5DF3E3F86D7F196EBFCB02FCF5EF6BDC5CEA034D5F33E61F6F805E4F76CB425B466D620EA166E828D692E12C568767482BF32C98A5A8142015A66BE48618347D49997AB6B0F53426BBDECF1A653458C2D8A6D80D49B736C2445216BF580E85BB794987986955523C6D78573608F1E2A2EDEC9F2862A289DB9D9BC8780B96FAF5D2DCDB0C6AA4A381C97FDA9A67C2F6052B145C8DA98C5BEE640ACD64586DEFDEF5FE6430F883B68C57366083B24C783561A41A3AFE3435ACFDC8BC5F14711079A8B0419000D41BAA5D45B70F9BA844DAD01E3CE992BACCD90B6B22B6CD81BC67BBFF830FA5EACFD6F508EDE4A11998DCE7F9C715F404F20C0AA98FABA50E0028994DCC9BEA50EC89A8CE26F49791DDAB8E16150A4C0A9883E59DD6737EB9EC79B63CF6612A71DF6F4CA023591DF6B5800EDF02942EA286C95F650415EF28C6F71E6D29DFAD488D2BB735E5658352A38F9DB04541716122AB51AD5B8DB07098232536F93210B5350A473ABEC4AC6ED14C0F91E069D0208A3E7B805474D48069AF8143530A8002C682F6D2987841EBBF4DD0FE49ADC161EF507C05B705720395F7D2187AB92EA8F02B2B29863DBAD1E4A293E7FF2F9DAB5B522087C756A4A5560A60B1F79C017F85AB5B854A431CB36C8A675BDC8568EE681B62C71F0FE67AE59A56DC61D4731AC415B1D33ED23D52491446AB14AF99314B7B160A39EA56CB2C1BDA78A8396DF2F5382FFAAE8EF66A9152BA6FE38D0A0B8D12FA2B74C1FEF982D69E9F16A3A75EC3FE31AEB7531CB37E0E067232C2C3C3A441C8A8345DD2E538B8BDE88AE750CE6BC8CF8219F401F34671C1933C63FE46E24E422A1FEA8D46B0A59792956ADE770D306D358DDE52B6EE0C437581096370FB85BF38105D939DE745E7B08221E9F8AA9892E4B7EFEE803452748825E1BD338B3DB87E09395970206D97FEF5219387F5C56FF9FB18C1A711FE28FED5C6CBF6A65D4B86480F7A05FC1922272C0AF27CC072ACD598ED7C7B068717CACDF8222CDD114B977B0D1E49FE3EA2EF8624EC73865A6D89ACB77AA0F05BFB3386795686938A98CDAD4930E141B05AB6DC401E89B719DFA8A40C9641561BB38EC2C288FCDD6032C8575298B020DAEEAD80FBCF43645E893F34CC29B7ADD1D815A2F735B15E30A1A3192D00C525E859D3ADEB62CBFEF3D0FF0D5026E0249DEC8DBFCF57300AFD3A40C9838284D2622FE07588BD81B542E6E12CC3FBC2CB7991F55FEE665F9F19BE36E49A4AD541718592F1829FF22DECDA26A8D595F71438F899512E409C24D2B2D679094E9A65C994566166D27BC299C89D5E78713DE0AE04372EE150A93985177E3CA7423CA96D4CA89E6A862EF80223D88887B6075F63130D4E3292EEFE9F850F57D67CD0C57FFE88DEFB61D9A6AFA917550160A1806CC525792899C9BB3B8D4506138233724A159C824DF5164C34405DE662897F601FF533CF70307CF7CDA04C072F0777CF5C31A7F90AEE8C169ED37D15CCC23743F5C77A9A5D12B1C90260DBA9C94A1B98EAE019B09EA2A94D34DAFB8A06856AE14B42483746C52A41A403E5F116DEE001AE45F2DA987CE56CF5CD9BE99EBD0D2AC1231E9BDBE5715A594F277B4BDD6D96C23199B72A1495DFDC1A28EA7178C02F623D4B6E77520AC134870A8BD62C1083110755222C69C685DCAFD8D58B881A72626B46D8CF3F01BB37134B5CF47EAF95C5FB6E0DB5EBE822DD98F2061A7695C2D5BCBAA26926DA9BC55F2FD8B5A009E0F8EE9838D86B6434CCD28E05BE7E1FAE1AE2DEA7DAA1270890C170378B65995D53A7D3DD5076B86A73B3FC7EB73C0C59D2A9B54210458E0ECA8EB3FD1278FC1B8A87395B59A3BF37E4C52BE2A59640D944169B53D109E348B3ADBB62026FDE56E422372DDD77713B546ED46279C13382441F9553A28D55313FA07709A227230120FAC743028D49D58C1989E32A03E58D0F2D87D28663B05ED75F91AE849657ACD9685AA1F528A6F9331E891F7028537F08D86B4F8118D52105F297D1FFD3503AD700C5FC5A1187ACAD5FC6AD2B4EAE36CFB99146D6FFC6B8F8F3B7\",\n          \"k\": \"71E743C143334C36D7078D290BFF42D53E7D775C6DE3FEA876E054CB8042D3F9\",\n          \"m\": \"2429F93D29E48EB6A25ABBA3EE2F3423CDDDD0ECF4B2090C6CA5BF4883F4F3BA\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 57,\n          \"deferred\": false,\n          \"ek\": \"FDE545438A542588554452B13FB2092F3113D0DB9541437B6ABB3A02A713871AA479A1B6F5C20F208226A0028B5A680AE845335DBC9B9C3B91E833C8FEECC89F0CCCBE4216949684E094A96A469EA2B09F5EC49A352084CADA99EAC591661A1A2C468CCDCC4C374310E78BB0FA38803B1B1B078C9BF0D9280323067BE76B1BD3912901695FE1C4AF4A1BC38661AF797F9D7B9D8EF091EA3A0A53210C57A706ACE6A33F09083B728F767B51520BBA7BEB364817B820DAA55877CCBA3CBFEDF60E48FC8A0B5BC6D223CF56C3392021A6DE432F4B4A3E58ECBF462376BB62933D6815FD085653D5C3A267C150697BCDACC54E336D7024662BCA1782E531DC339851B33A0778A779544F4B081C05998787916A802436DA982B79164C5A5823D11AA271187E8F90790CE95C1E474E2861960F756F3191267E0158C1150608B5CA7AF09C23579608386822ACB02D559974FC6FED8558CFA6383ED0B5FCA318648699DBB7195F24A000E6A99FF8B4DCD0C3C7B84E59F7CA21979F646B4D8E7A61289605FB027DDD3A10CAB301B9918C365714E1391C42185874980328C04900AA9967627660C6ADA737260D74A1CEA5578A254FF0F203DFD6C832E86EFDB25A08AC5609DBBB469A7125A7C1C8644223AA51EBF9C98DFB64FE530FBF47210BB3C142360EA864AEB447459C6B361757AECFCA733CA2C5EA485B0AC065E2951F145CCA4FB6951579B4A19CB042A3B34419CF29B99A6E579945EC357C7C7BCD7578175BBD51444C1DC9A7AA54574A32623D2C90146B44F2547EE9930282F25447207B03C04EF6AC1294B9BAC6C36AE1666401E318B730020A832ED789199CA8445239B765D48610C6B25DC4C592DA7906C130A6E356C066534388651A0B4F495C39962C9F20DB1224B40AE0642091115BFCA34B5455568E97B0D45B74592905E02BBEB8FC1E2D1C49051242AD000D69009B12A206B988B8CB26A91DBC6305777234FC22142AC85B828D1E8B9E589800214173E858497B964E6DD98997576F5F4B9C68090563F1B004D38643E2872B196B1F607AAB3C22D73324DDD49CE4214F13065405767348A20BC82329EAE2283FB65ED1EB939CF25A7B1762E9686DD3576602073112141ED315300BE6B75F860884E0718C7596FC39172AD9BA4EB837393C49DCA98AD8CC15B849965C017CECDAC67161A7DE6863698C8A64E23A19402E977539E834887954867A29CD2F46B0FBD6240D89141FD8C9ED390099D905C5101B63B30D4D61AF14965FAC227470B38642064412747BDDD2063A942EB0F02843156C1D637D2D8C6C5161CC3A21A69C6220B39CA03BE975B81921C56C140422A8791A9CA2A9AFEC49353B65AC2440C31E2084FACC6F7F23A027A682CAAC402A69675305363BA85947BA5275B6868660B654D91264870FCBE39BD2E68B9048C29732A3A7306AEAEA8A08BB65E5E078105424A3B8803EF9C62CD65D8A65CE6D528A1A818E1BAAA4139CB451F5A20C135F8A231E843531CD700A2E69BFFF186DFF49108562361F353A8E5A23C900043034BA6B9C6CDF2A7C67847F78E0955F7C49EA94785AD3495CF4562EC86F72E22B562382B82B392DBACC1F536C18C20086A879B82C291F45CCB4C24221836383D03997C43EDFE165A1A046405A193E0ACDD08B122AF6925DB303491BCB87DAA36CAAB91197BA64A9A9E3C4A760174129B080FD18339E07ACAF00CAA118A84360AF0333755B869694D07BD108AF4A094C51A9165CCA9ABA36C2E3E43FE4DBBDDF257E08D03B333773D5E051F8101B8EC7B80FD1AF5E9C053FC71C0A1C785954C3AA4C33FFC95AF6CA3B248B5F050AC11B3998C51C8A294544CA4A60DC47529EE1A3849314AAA62A05D3557827AF57FCA1DF9435C4014669B9142DB620AF044C195B6BF046582C52BE435473C74A34CBE751599412B96480678B668B238DF373302076856BA63AB627CB21F3707BA18B999A3A27DBC727EAA8E0497FC3854A4A926905D0968E3886FFFC4F6A2B4273993282635B9BA013B4B8C91E7A47AA624F8262A14C42B4B8A36027573D4790BCF5D386B9181FA0F357230A2185747875970C668142E3673087AA02A5B7AA058C7527B1C0A1C9B05C912D49D1BC8FF629FA77CFEDD1A7F6EA52F9BB49A82B38C022C52C87C346482D42A797BD3A972E19DC352C66A0A315C04CEDAE314DC0D335EEADEA8ED3B75EA17EB388\",\n          \"dk\": \"698C6D273359EFC42931A425DA3B8B9772747190B26204AD5B352807098287B672019B9604337BD50988A1C7AEEA975795E12AF9348A6D180B6F086AA9E92B43F31813B0A357C36CD31504F65285EEE03CBC288C0EC06AEF318622B21839E74A38D33C1FACC773A29BBB0652DFD728B90A613B323D735790DA292ED25403EDF5B2260342D75889732852FF4813F701AD3CE1C9F960C53057C293A09779A88745E901EF744A07E511290277D1467A2A082501DA4C10755E5F2AA8E3560AE33A5D37C77EC0809A1B766484422D00E3422CF19F64DCCC1DA80816E321ADBAB7E7E79B3BF382EB732F86FB771FC1A6B63A6655D161D8B21141034722480C31328AEE1167FCB477626521E833208EA53F95D46B8F2A4D36AB00E34B5E9D324D9C2A6E8AB7752E54CE1AFA9CA5DC461C208F66AB0370C90E96DA0E90340E014BCCC9676094BAB2FD7B831298C4589429E012739989BCEB5B09C8A0AA7F56CE2B680FE0D19D9C8171DA580034184C52E30FF3B712C5D92BA93B9FE8D61C7583227503B3F33A83CB613FCD23C3CA0B4689823380B4037ABA9CF1698F48C50ACEAA2D4BD524BC34BBFF4204C4A359CD3896E3057F9C367F0ED074896B6DE44CC5B492AF3E52A29643B87EC895FB3375E21A48FE5CC0CF3A8810B9ADC950C158F13E29994FDB67C093C09A09A4C4565A21E808916040431250BA91B95D8E42C3DE2A1DB032AF1A3529D1820ECE9A686DC554B86950FA9ABEF1D8B416EA11AAA5984232662F2734609C9DD7112E19B5550F115FA571CAC73078F08B601C649B1A6949875461FBBC416CD951CEA13B8E996DA68382A14664A4920B4E9240562C6893AC8D089BCC4B3B55719C4F9C331F2576985A07C8A0192FA599197C49260CF57B6793ABEF0619258B886975422DE0CFBC20383C3124F74004E3360D988855DA225E810A9D9C910079D80A21B2A806D851E8831581F68D3914C0A0FA9B518B71E387113EC8616592BB71E481928C2E48AB75C9C7A4BFC48ABBEA6DA77A45B4FA5C71BC31F85C902552B517EA36C2E3436A506371C8A827AA721CF57045D21D52597640F1536D518268B65B19F2BE40ACCE482B26671AC51ACA27DEF703497770083611AEB83795EC58C84262EC4A3D78258953D248580CC6D069273D12984BA5BFA359AB25484F79A3B8CE4739D570CA1C6844C8FB525CB764970A72B1381B68394AA6308AD00C96F19A17F7441B544C3C95903A2E9125090C12A8BAC7461CBD8E37025CBAAFA797B70D8896E6E5A60AD332516B3594D06F75A18DF335BFFF876F4B103F8ECBB85547246F226A95285F081337F0719A9EC453D4517B0A2838DF0117EB480039583C27B53A4684138875C4CFD06C7E357F6C836D5797A88323A8D7600979E95025372160A9BFDBD32101012034E64613E198FBD92226A4AD9954795A9227A9777C78F3B8669CC524446D77C8219839AD23B21DEA6486995A029A875D4767A866E51D7E00205FFC5FF43222CE923882D88D72B856FD7C00EA07266FCA70A849A3C9D227021B3BC90B8FB2BBA0173930984847B42B90CBE930DFF08FE4C14D424CBC82FC81DA1B6AE5477D49C30D9E4CC9C651AB6976AF47A18C188388ED0C3163E0B5C9152C82E859DF0C16D3B2478AEC5ECBFA4AFA7560B858142AB48EDB94617C92522CB07C39767CABC55F8BEB00CB48C9ED19497594A161E744E764104347777F5B8168A95F579315394C6F184CAB00CB3B27C19FEE96354E4B62E490572D597455888BCA0259E6F7027AE44F7F95CBF4592B722696102448E7B437CEE5565569B27ED9CCB23A75DD094A98678129272A6C2C27DB5B272B7B9BEADB34CEC5091C405649897665BB633012694F4094C595370E2B91BD8C585817063F556A39FC625EC38D86A5CD6D83CCEA20BA6DC108FBC3C723ABC62092C612F503F8896102E9124DA46BDD00748141891782AD9086876AC920B11C5A6849C67A4B70434236937243D940817C2016A644C53EB7C0F8540C52301D63122CD7E43E98116511D455D3675DBB42944F7CA936F51364D07249200A17A7C83FDA1A2017835C68965EE617DFD5B5C1F026CBD2B79B68A6D76443278972B6914A5937AC8C213292050F0578A0D9219EA00345D17BA529D6B5190381C255B307ABBEEF7025FA36C1FDE545438A542588554452B13FB2092F3113D0DB9541437B6ABB3A02A713871AA479A1B6F5C20F208226A0028B5A680AE845335DBC9B9C3B91E833C8FEECC89F0CCCBE4216949684E094A96A469EA2B09F5EC49A352084CADA99EAC591661A1A2C468CCDCC4C374310E78BB0FA38803B1B1B078C9BF0D9280323067BE76B1BD3912901695FE1C4AF4A1BC38661AF797F9D7B9D8EF091EA3A0A53210C57A706ACE6A33F09083B728F767B51520BBA7BEB364817B820DAA55877CCBA3CBFEDF60E48FC8A0B5BC6D223CF56C3392021A6DE432F4B4A3E58ECBF462376BB62933D6815FD085653D5C3A267C150697BCDACC54E336D7024662BCA1782E531DC339851B33A0778A779544F4B081C05998787916A802436DA982B79164C5A5823D11AA271187E8F90790CE95C1E474E2861960F756F3191267E0158C1150608B5CA7AF09C23579608386822ACB02D559974FC6FED8558CFA6383ED0B5FCA318648699DBB7195F24A000E6A99FF8B4DCD0C3C7B84E59F7CA21979F646B4D8E7A61289605FB027DDD3A10CAB301B9918C365714E1391C42185874980328C04900AA9967627660C6ADA737260D74A1CEA5578A254FF0F203DFD6C832E86EFDB25A08AC5609DBBB469A7125A7C1C8644223AA51EBF9C98DFB64FE530FBF47210BB3C142360EA864AEB447459C6B361757AECFCA733CA2C5EA485B0AC065E2951F145CCA4FB6951579B4A19CB042A3B34419CF29B99A6E579945EC357C7C7BCD7578175BBD51444C1DC9A7AA54574A32623D2C90146B44F2547EE9930282F25447207B03C04EF6AC1294B9BAC6C36AE1666401E318B730020A832ED789199CA8445239B765D48610C6B25DC4C592DA7906C130A6E356C066534388651A0B4F495C39962C9F20DB1224B40AE0642091115BFCA34B5455568E97B0D45B74592905E02BBEB8FC1E2D1C49051242AD000D69009B12A206B988B8CB26A91DBC6305777234FC22142AC85B828D1E8B9E589800214173E858497B964E6DD98997576F5F4B9C68090563F1B004D38643E2872B196B1F607AAB3C22D73324DDD49CE4214F13065405767348A20BC82329EAE2283FB65ED1EB939CF25A7B1762E9686DD3576602073112141ED315300BE6B75F860884E0718C7596FC39172AD9BA4EB837393C49DCA98AD8CC15B849965C017CECDAC67161A7DE6863698C8A64E23A19402E977539E834887954867A29CD2F46B0FBD6240D89141FD8C9ED390099D905C5101B63B30D4D61AF14965FAC227470B38642064412747BDDD2063A942EB0F02843156C1D637D2D8C6C5161CC3A21A69C6220B39CA03BE975B81921C56C140422A8791A9CA2A9AFEC49353B65AC2440C31E2084FACC6F7F23A027A682CAAC402A69675305363BA85947BA5275B6868660B654D91264870FCBE39BD2E68B9048C29732A3A7306AEAEA8A08BB65E5E078105424A3B8803EF9C62CD65D8A65CE6D528A1A818E1BAAA4139CB451F5A20C135F8A231E843531CD700A2E69BFFF186DFF49108562361F353A8E5A23C900043034BA6B9C6CDF2A7C67847F78E0955F7C49EA94785AD3495CF4562EC86F72E22B562382B82B392DBACC1F536C18C20086A879B82C291F45CCB4C24221836383D03997C43EDFE165A1A046405A193E0ACDD08B122AF6925DB303491BCB87DAA36CAAB91197BA64A9A9E3C4A760174129B080FD18339E07ACAF00CAA118A84360AF0333755B869694D07BD108AF4A094C51A9165CCA9ABA36C2E3E43FE4DBBDDF257E08D03B333773D5E051F8101B8EC7B80FD1AF5E9C053FC71C0A1C785954C3AA4C33FFC95AF6CA3B248B5F050AC11B3998C51C8A294544CA4A60DC47529EE1A3849314AAA62A05D3557827AF57FCA1DF9435C4014669B9142DB620AF044C195B6BF046582C52BE435473C74A34CBE751599412B96480678B668B238DF373302076856BA63AB627CB21F3707BA18B999A3A27DBC727EAA8E0497FC3854A4A926905D0968E3886FFFC4F6A2B4273993282635B9BA013B4B8C91E7A47AA624F8262A14C42B4B8A36027573D4790BCF5D386B9181FA0F357230A2185747875970C668142E3673087AA02A5B7AA058C7527B1C0A1C9B05C912D49D1BC8FF629FA77CFEDD1A7F6EA52F9BB49A82B38C022C52C87C346482D42A797BD3A972E19DC352C66A0A315C04CEDAE314DC0D335EEADEA8ED3B75EA17EB3882A9072AC6B041BA7624CF3158048EE475506482D536D15DBEC594818AB0E91025AB34FAEA7275E5D6C8EE1104CB19F4B1C14B6C51ADB118163C6A48540E1C5D2\",\n          \"c\": \"4CBA673C63D21AF9EF1A30F8AE10628CA81926A8C0799D276BE363A182F64C312CA1AE996865034FB1F2F4FFAB6D132B141C9CC01FE4BA6B16B8FA1F6F59D140B89CE159239BCEDA3E93EBF6D858D647385392563BCC1B421505F29D0C74FC6028AC536DF5D8693C6E4BD36A9AE85737B1C14632DCB11896B50775A75C8A334D334B7983D6778D1BB2452090736CD9B488025AAA2AADC091ADEF4705BEE3BA81A404772916AD78B4CE2E2EFA0C54F77FCB4AF0AB62720D52C9B181F80BD6A71B65456BB8A6EBC4203C8D224B6B8CD544559734A9E51270D06680240E48E72F6A294F29CE05C7B9226DB34E2883A065E6FB742FD38A00DCA0938D0DB500A012E84903425EC1680A71BD7688F26DDCAD3C8368C2B384BDF407E83C5441B427CACB25FEF42F6A8C350E67DEDC9BFA7007B28F6742028268358981D72C07C455630D8B845D61F77F0129CFD4DB594BDC4BE54D958E8A3D8B6A9A4DE77BB66B393DA7B823F5D3C25C5EACCFA258A9F7D9E05038E7F8CB06A32886296FAD4B7822DC2249B1809E5244716A552462C8F1E21D87A30703A3000C6D3AA082BDA96C441F65014AC10C74F708AB8BD83E5CF6BF42124746FBB44F5F65A9D564DB514AC49D43335A6D8677A30A54F673BEE43784B2596346F49073EA27BF4C2AEA4CD790A5F24E772F69F591129CF33CBCF5D012384230AD91D8603D3A54EEDF75EBB579CF4027F1E40792F0FB2769EA83494CBAA6D8732C0ACF474AA422DF63B8BB1D19AA37078C690E5C44B495C87AADFFEEBDF0FCCD969A22E151978AE67E9B7D950BDDB18D9014536E91B1FDA68F41394653E4A066EF8AC92D61CA56CA3B504A6FFD5993785A958A3E2890D40D58CE12790CA35AFCAD8569B75DE5E033B6813B6E85F1837C5F62BD61C3E94132ADE97F2F8F3FDA5A54A958865BA898A8A67C38E61C8625C563A5EE7F6D059E22FF626976332D0E0A4F7A240434999F16BB194FC77B21DD09A47EB819B1D5365553E8A86A09A83BFC612BB38618C84132C0E7A8212C5D2327F5BB7BBB5C55DE72E013A198CD806239112D0ECFCD05092DDFC34A9453EAAE2EA59061734C4A480CA346652025BDD35F49FEDE97B301E3AE6246EF54C298F26968D44F144958CF7094520473198C6A654B4105D2F8CE350C05359BFF40C15D61B633C6F963F3EF5D37FCE3383EBB14F3FC04150B4D6FF34A16A29AE0D38A802EE52B0585D9CE8BB8D3F27BA43A577673AC96FF063AD37FF4081A67BEBF1FAB1AFF168A0DE3B0220249765EF67481C7C30ED4A1FDE1BB62F10557034A6226B6B64892D4443BD8965D1A632F3EC8D58A82FA39FF13B43A02A82D6A1ED0FA651342192B4B4E799A398914C49FC5C71A7B8798ACD44871859E3D8CE777833D0295A9782EC48334D098D10EEBD198CF06B0795AD505FE0342D5AF7E4E99EE34BF0C70AAA2E4978419DEDBFD6EFD30E985E6539DC139DC2D96E28BE541642645F54781EB682BAEF49C109E0665F4928E9BDA891AB87D872BCD28B683F13336419010BF53DB4349873544331823BAF9789DDDD5BCC7831E2C10F50C6E33632C9D4C47ECACFC664D7D683B34934BA03AAA4A9FD1236C4F97A1EFFBE3B183268308BE20A781DD22D0A3BDFB450A102988525832F03DAC2A9441DEAF2F3C5DBE47060930EF9687008D7B58AA81FB79F08DB833D7F2C3E880B4F621AC7FC6601849C7CED42166D31A530619BB9D8CCFF269C20EEF4AD814C42B3BD2B35C4100DA58C8EFE5B46C2957673ECBE586C95163ABD5D9F2ACE64059218685F369B3815F01D693098B7EF8E7F89F202F8AE76594EA7A66D5F4B74F80CE0FC274E647B26CD1E63E88FF27AE783648DFA85D5DD56863000F03D8D215E21EF11A1C2E8E57D5CF770D0B7AE22557F458F3ABAAF5B9BE1408E9CF4823D7FE129472B9E4B89394DE4907F85F94A3A42299E432BCA661BE17D23B6B61A7F20630B0ACA4529AB8B03FBDABA1625D2D49E724BF7748AF96FD3FF11915A84408BF80C70FC0381E39016CFC8739B80F7C9E2DEF0C53383557E15FDE9BB31FF367C2625542433388F19CF70B568D64E958FD33FAB2DCAB72D4778E2F094D94A62DA941D13B56BC6CFEE6D1C3D81A82DA67344994FA405C31A90B9EFCAA8C7242F07BE73B3AD9ADE3CC4EB73017FD6D7AB37F3B34BCF1D2B291EFE9D4F8C4C33952C479AA0AA7B824E3B895F8B954\",\n          \"k\": \"91C5D5F6016A421C4C5017CD8E805008533F67FF7037E8C62FB52A2D6657EE5E\",\n          \"m\": \"B65043CD3672CF9AE2CACC94F923CEF63B5127ABC63C2A5AE6C064B8C6FE7C57\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 58,\n          \"deferred\": false,\n          \"ek\": \"E7104DFD284FC4B28110B06282BA8EB859CD60F8934CBA78B1F1653CD4A9613B4BCD5C32A3B30EF863A448D3A17FFC5908438800C5CE12475F3D5445761CCB25541FAE3653A8B8776256C7F5384481772ED0E3C4E512988E605E1C75A85606AA574442888B87F0A77C08824FDFF2B5FDEA4834112BF77C011F7855AA065ADEC66F505A72EE45300E2C9B18588E2F6B57B1C2B26F491166AA9D89D9BC2ED26DD311A81E4232FCC720CA8A788D5A00DB696763256F58B2B68BC32653C71F84088383473F19A76ADE5CB5B6BB466561AE7294770F9148E9E44B2FA1A23B1B99CEC1327F60A002728F21CB9D25AA79F526CCB4EC1531864AA550C72336509FB3061717BADFB2682EB59053333CEC75BD6E244F002D0BC2B1C8008737AB62CD42D2BDF9CA2FECE31DCF836264F84F76569A16046761885C163CCCE986122A11C8911C9257B88FA2CB792BE55FC14048BB995F11606E4C3C82B3113D99B451686ABB1F129E22D63EB14C6C3EC2479BD0243F57876175B138BB0D42313A28C67F1134733F76977BA87B5A11523953947B684282905F2AE90F5BE1CFB0088539313A9FE2A3AC9729C2FB5FBCF492C5ACC90A74C934B1A29AC19D2E20CE74436742A1415A9A0A4CE3756C199AF7593A7685C02F19768C6A1F4859B04B0186C63580916915053404054929F8B417F6A3919882C8D31B220F1817DCE08267DB0EAF61301C2844CA4556F813279D122C43A8820560A09A18AE2807163547B9A3E22F83E14AC4DC3FE69A25FD5C2CACB6A67905C9C754565EB5B17AF089C4653302AA830375B9CF0BB6D43904DD8277D50BBF4D43ADE05918ECFA74DD432CFBD829C4F9838DD76EB75813DCF397E3507FC9C6BD69C2B220644D3EB8839769CFCED608F98874EBAC4367A11274317297DBB424B95B9062ACB4C83CC5E879E6688447F26C0D2BA3763406318BCDA3228CEB0641C7250DAC193A8713744A6ABE5F6C106DC4049CDB49FB946DDB7A08D2D73556648F4CB82E2AE989ED61677B805F3A61344A14A2F3DC3964A90E4B83CC22AA8E7CD532BAB55880F212751C4CFB93AC14F564AFAA723FF6B31990A88DD32A557AA86EC2ABC114745C8038F4C1C924258B5BF02134605923E865539425F10424319131B0CB17D72C2981A944FCE3CD85A2509E93BE3AB5A65E53380772A56D74945D60C57A112FD87A2EB7989CC14A973CD05590495122811A6D8992C0064C77A78C7E390C43FB695726B7582C96C50A8EC5B4081CAAB6E9B64870166326A8ADA4731030290CD444B9E28149E06BACC7A79C5E7B24BAC292B7F92C7AD720F957013E1B118B3A40095C705DB33DE334ADD011CF3DCCBACCABA3B12A6FB5962E01C7C8D83912D9F98ACF8B1BA073B25AEA0C177074E7EB08CFF3CC7D7709E723340BCC00B014A0B313008DE52533B72393D70B3EC767A8B6CBAB61B8B7A9C86B263A1528A9FE366D60104C0D320CEFB76A688B407C0742F8BA4AB8007A3651C0F26B81E92746DB701837B575AB00A6B6D56FC5C12C956685ED0AC3F0C6A5870935298640E5070A8B74AD540CBCF273705C68115A828F72B09C83551714D67455EB46DC0BC14145CC38F5C1DF675A3677ADD28C69F0D462B1F1A0ACE8AA30B877382280F903B7DF9503E4C5048F4368B2A58565552EFB106E9AACC75562C31E3A3FE7790EC9269987C627EFF798A9A0484F215A3968525A0915C4B4AA7A8170EFB1466936A8B8440DAD499CB2F94373CC91F62673B59A90C728A818247513445D5AE25D85AAAB0036135DB5BD74D106AC557E0E8754D1E071B5B62FFF7575847B47905BBDCD5958A7A86852C34DD01286CBD50F0CA4628E31C5EBA531DBC02C9DE1BCD82C218C2CCA87A42A28D98ACFD1103F80AA9073C127381B4584C923492960537A28B868F214251E328FF44CC984972949003916B76D89D472B97013A52141FBAB0939B03D8D57364A387FFFB4A59D7B5D586680D7F54DC27AA7BAC39580103C74621E6D8970176AB2965ACBF453A7217A3C21769449E9AAE851090B52390642C467C9A233501110F059EF4A1A02BB06CBE1084A08C85AEA6237DCA55E2698D177716E662E2DC711D475C2EC9745BB061A90432C6463BA827BC32E0B0FA6B015324C9B574078AD823ECAB9010D0BC640C0A20E17317B2A30AE8AAC4C94695EAFDEB2BDD14D78D1CA07CEB411455C0EF10D23FB50CA\",\n          \"dk\": \"1A47BE10A1273C9312ECD31E36786BF2C9A13382CE6919AB8F4C33A92A5E4C5A862CF22E39F34793A4BEA35B4CAC858A32B79EF3C56A6C1C7E9205A22F0CAF77936722424CB6B207B15484BD602239D66AB2D5A0C342AEAEC83C95E42BD6D1817A1AC7C50A189F4248A9EAAB81B53EE952C9897178C2E92BBBA7B84A7348D8C504CECCA2261A4F72EC9EECD82E1329A55D5A64B8743F08999B78B88D4C60C0FB1387398A2323946D7A071B6898C8BA3250734933764C31A91782F2050CB3597B0FC63C280C576298C062D0968539C740D6B89A6A28D20871F8E2C35BDB86EE8B747CA81B83D856407B7C0A604AB1F753D38844DD890209B337F2770883B82FD44808356B24A36195D23C88D5F5857F4624ACD31211B695F0638C6AB870856C1277EC24C525A37C39259A99707256BCE0DC9F2B9606AC93A0146A5B5BE41B22E63382B56F7203733682CA87EC80F8C58F4294167F4772F0E83176BCBA554545EAF711B02A7DB7B896D44BB0F47BC46659A8305329D82172A1DB4C72955839D9752277C3401A7D1800088A768F473B80B7C746AACC8205BB798E905C1E1008F454AA5363B1F8E9CF3CCA72AFE1703C03A585D66C528BC42A311E0479CAE4763D789C583A56A3F6836E73A73BBF01073703AC90254CE50B27560842B3FBBF5DAA4C6DA37B9858BC7F2054A596348187BB99646A7933234721B1A3FA65564A5E3525276C5A7D68960FBC7C095AD403EA0C0380E68B4A5083F0527D3C563313B21A2816446A4AAFB32B52D8A60CDFF26281412353D02205868F9452BF50E0440049630C022CCF05AE6A7289747C573062560A3316C895586994CC7D2C4FCBEA0974C5845400C0C9218BA8BA2FBFB8C93C025AFB6C341FB2B44DDC6046B99F2CA2A269F931B76C0BB89BB424C9CDFE54136F716C8613C461B92297429EB471AE1C47AB4C3199281039D41A3B67908F3586997C8A9980D9145BBA2A098524461184AF05576BE349A614862310AC68788A0F06C418E255FE6B63F03C99E30699DBD77378E02AACF6C2021ACE42F69F42A5716CC37C64617FA7C68721BB8A3CEC3609BB8799422074D143E676599154B186554D2D5C9EA57B3EA3917BEC6C61C5A351A9908471B94856758964B77EB7D7C2CB1A4DB3000D4A87B9448686DF075031263EE5C0253668AF61C8259CD33E1E9C1FDAB4B4B17012EF831BE6C3BABC8463B3084FE2524B3227829D24BA38F88D850B21CC06638F258378D15F73D4C6B3D1BA498959E3A9C9257C8DFB2624F99A15CB09B1CB7469F61421B929A5C07ACFD781A8F4069C446760E7ACABA6E2B4CFC89553969EC1B21A41432965A8BE9E5A73CD402900472CB184812E1B5B2136149E4083D17AAF728242B470209C9C466AA3823A9A7B1C35A36F062BB670573EA059BEE9A2F1E4612AAC19B7A3337C3AB647318A88A22226F606FD645E324971D5D58E89A0838B00553A14449679A01149298294B9FB641B08674BE4615DCE174CEB019982D75996220F33E2393D8397FFF6217AF3452CDA8D3820AE291839B2280BEFC2179B344340777009378E56FBB90D934F04809C3E0A8512AC69D087BF669214A7125337C15CECC2165FB7C620D06CE422A9E5EB17D5983AC0509BF598AF45428BAAB16546480670DA4042F93FFD147E4418254109B8E0D97260429E5AD2553A2C86E6E7294764943CF14014B57B8A5113B2F02E54220981177BA1D4720A0556AC211B44509F0F913407560961E0AA084A0AE2C15CFA7CA77D9CB8CB683400CC72FB39AC7A758C0C21013CE6046CE2950762704BB0050D43083D85751AA5A141771DD5E9B4E1785E83C2B5997758B6FC3628DC43EFF472C73C4C15100BAE570513AA532296405C5936847976AEDA9F5B0AA704E35EFFE533840599EFC74F3A1C587C2640F2509F39FA14C0630F8393C96AB864EBC0B4996468D08A5515A3B4D71856AC31BA1B141D36E0794A6633167A471E199C7694B105230E0D11268E809029F72CB0C798F889CBF4F79418505DD5E9112F775C77D6C75E089DEE49AB0B685B837ABB7391B6965A432DD6CBA0670871F601AC186FF56C8C40169238E71BCD866546603F59B0C3D510371DD53923C1C92EE71037C47A8EF830132519F1666BC574AD1A74661A1C46DBA709E317974829CEE7104DFD284FC4B28110B06282BA8EB859CD60F8934CBA78B1F1653CD4A9613B4BCD5C32A3B30EF863A448D3A17FFC5908438800C5CE12475F3D5445761CCB25541FAE3653A8B8776256C7F5384481772ED0E3C4E512988E605E1C75A85606AA574442888B87F0A77C08824FDFF2B5FDEA4834112BF77C011F7855AA065ADEC66F505A72EE45300E2C9B18588E2F6B57B1C2B26F491166AA9D89D9BC2ED26DD311A81E4232FCC720CA8A788D5A00DB696763256F58B2B68BC32653C71F84088383473F19A76ADE5CB5B6BB466561AE7294770F9148E9E44B2FA1A23B1B99CEC1327F60A002728F21CB9D25AA79F526CCB4EC1531864AA550C72336509FB3061717BADFB2682EB59053333CEC75BD6E244F002D0BC2B1C8008737AB62CD42D2BDF9CA2FECE31DCF836264F84F76569A16046761885C163CCCE986122A11C8911C9257B88FA2CB792BE55FC14048BB995F11606E4C3C82B3113D99B451686ABB1F129E22D63EB14C6C3EC2479BD0243F57876175B138BB0D42313A28C67F1134733F76977BA87B5A11523953947B684282905F2AE90F5BE1CFB0088539313A9FE2A3AC9729C2FB5FBCF492C5ACC90A74C934B1A29AC19D2E20CE74436742A1415A9A0A4CE3756C199AF7593A7685C02F19768C6A1F4859B04B0186C63580916915053404054929F8B417F6A3919882C8D31B220F1817DCE08267DB0EAF61301C2844CA4556F813279D122C43A8820560A09A18AE2807163547B9A3E22F83E14AC4DC3FE69A25FD5C2CACB6A67905C9C754565EB5B17AF089C4653302AA830375B9CF0BB6D43904DD8277D50BBF4D43ADE05918ECFA74DD432CFBD829C4F9838DD76EB75813DCF397E3507FC9C6BD69C2B220644D3EB8839769CFCED608F98874EBAC4367A11274317297DBB424B95B9062ACB4C83CC5E879E6688447F26C0D2BA3763406318BCDA3228CEB0641C7250DAC193A8713744A6ABE5F6C106DC4049CDB49FB946DDB7A08D2D73556648F4CB82E2AE989ED61677B805F3A61344A14A2F3DC3964A90E4B83CC22AA8E7CD532BAB55880F212751C4CFB93AC14F564AFAA723FF6B31990A88DD32A557AA86EC2ABC114745C8038F4C1C924258B5BF02134605923E865539425F10424319131B0CB17D72C2981A944FCE3CD85A2509E93BE3AB5A65E53380772A56D74945D60C57A112FD87A2EB7989CC14A973CD05590495122811A6D8992C0064C77A78C7E390C43FB695726B7582C96C50A8EC5B4081CAAB6E9B64870166326A8ADA4731030290CD444B9E28149E06BACC7A79C5E7B24BAC292B7F92C7AD720F957013E1B118B3A40095C705DB33DE334ADD011CF3DCCBACCABA3B12A6FB5962E01C7C8D83912D9F98ACF8B1BA073B25AEA0C177074E7EB08CFF3CC7D7709E723340BCC00B014A0B313008DE52533B72393D70B3EC767A8B6CBAB61B8B7A9C86B263A1528A9FE366D60104C0D320CEFB76A688B407C0742F8BA4AB8007A3651C0F26B81E92746DB701837B575AB00A6B6D56FC5C12C956685ED0AC3F0C6A5870935298640E5070A8B74AD540CBCF273705C68115A828F72B09C83551714D67455EB46DC0BC14145CC38F5C1DF675A3677ADD28C69F0D462B1F1A0ACE8AA30B877382280F903B7DF9503E4C5048F4368B2A58565552EFB106E9AACC75562C31E3A3FE7790EC9269987C627EFF798A9A0484F215A3968525A0915C4B4AA7A8170EFB1466936A8B8440DAD499CB2F94373CC91F62673B59A90C728A818247513445D5AE25D85AAAB0036135DB5BD74D106AC557E0E8754D1E071B5B62FFF7575847B47905BBDCD5958A7A86852C34DD01286CBD50F0CA4628E31C5EBA531DBC02C9DE1BCD82C218C2CCA87A42A28D98ACFD1103F80AA9073C127381B4584C923492960537A28B868F214251E328FF44CC984972949003916B76D89D472B97013A52141FBAB0939B03D8D57364A387FFFB4A59D7B5D586680D7F54DC27AA7BAC39580103C74621E6D8970176AB2965ACBF453A7217A3C21769449E9AAE851090B52390642C467C9A233501110F059EF4A1A02BB06CBE1084A08C85AEA6237DCA55E2698D177716E662E2DC711D475C2EC9745BB061A90432C6463BA827BC32E0B0FA6B015324C9B574078AD823ECAB9010D0BC640C0A20E17317B2A30AE8AAC4C94695EAFDEB2BDD14D78D1CA07CEB411455C0EF10D23FB50CA524A8A240ABCAF9BEA0AEAA9C7EFC5BDD617D02E395BA073E0E6F8E621D501F698174C242417B71B8FE5465BADDC9DED85C393381CD5F07E2B05B9437BA39448\",\n          \"c\": \"E34FC9C40C386D57D2086BB28B5907228054405BBA248DE609729521BCE8DFB4A24E4742B780449CD66C32D969D8325550FD1545E5008956F040E1CA31358B79193044F77A0AE406A36D28C53A50678570A880A5FD4547BD7C805F9C4129FBA6B0671BD997F696426AADC8BA7682844D43089E33C622AF15BFFFEA5F622B65E74EA912CFE0C7B6EA3693E30DE4DE61AEB0D19C42B94CD84D72016355CAAC4C450BB4B8789A0BC67BC1785DBE9EF751E516110957E5CE8B03B59557343424472888666D8BDCA40A2700711F45C398CD92D1969CAF226EA229D1FD0666D831F92081A7A4C0B3033CB7824339E15C2E92AF53420F482E8597D6A47F1DB85C2680424E4E6B465E844259DEDC4C8D7C36AEBABA1BA872975C0B4D09D3881161734BD52100CFC3EA63119DF2CB1F43464584FC689098798CBF89057E69E480FE292DBF4663CD797094E746C6A99A27B88A7ADA2960117A7649B98F5CEA23B98952C77F5101061BACBD974256EF9F9CBB47DAC1978D31F257A1FF7E3C7291BCF3C466F815C019E2752DCD250A73C90F361C5A183A7F98FD9ED2E26D8BC3D3448267857E5C3744425F89941F89FB1D0433B107198E1583BC1AC83D903C40D4A357BE69CD3E840075CD315794CBE003D3C4E7FD04650C69F86DDC7C375EFA202AE129B1283C65F9E251F94000BCBA61AA0231A230EF0224BD6D955B06D2F3E2C978C0A9D48E68EC967C886B39CEB2B0C538551B89E6C71E750480E6DD1249658AB6C8835EFFE9593A178F619DE6BA506C2805E96B2C6D52584FBB78036D35514945AC008DAE2CBA6A1B6845320B3810C66432D628B01D99241FB70FFAA6842330D1A4361619025F24B3EA7F95C9892B95FC60F84B6230FEE12FA4638339792B225F04798E574A95209ECE2C4BB1C0C1A021820C5C6169095FA425693948D3A6204362B855D58B8B8942ED05DEAC54FF776F7F6296EE3730EE0A48B143833D7C1D30AA9266202B66928D07DB9D22E893BF96AE5C84823AF84ED4C32790A959F5C282EA6D8F4488A587EB3EAA1A3A79DDC74AF8FD8F093E2C43016E9954733FF50A37DF4CCC3DECDC2C433BA22C6A400399DE7567E028758D0AE28ADAC787109A94C75B428D38757D3E14D4D4F332CA1B12B5ED2D8526F89BFAEE860ADAF3A7A68B2E18F1099B95351D13B36FBF624CF3493FDC5AF9E6E2A42F66760A79D5BC3778DA0673AC2D374FB101F4A54133E4606F5BDA0888E722FC8D721FEA7FC02F05C649BDB7C1B5B0598BCDEB6B4CEDB58EEFE2C85CB732A2F17B3C74C6893D051BA1806963FB75B3F4BB54D03B208534BD49709E7D3FA752F71936926B03C3B2409717D6F6670B4C448FA0BAE47144A90E175524CEE0595C9D1C2EFBB34197BF4095E4255AD1DA5D74C854C03934AAC68C88629219563AAF017732BBA45049F4011056AE8F624D4D2A7B53A1DEE0DAABAC0E0D9E6D1B7B910CD8C70FA5BD58A078BD3821ABDDA5520D8C3389D268171A433ACE1E247994BB54D78B06BFDC9C71587D9B5EF9C202E15A2977AE436D3E2401D78E6E63E74C1056CA5EFC62D978AB473E8AE49BBF81ED62670BC860909F294984453930E53733DD31383D804723848CF0DEA6407737EC575D6C9F11F80068855ADB886D5FF956E70CB09F86D5D5548FA33CA645E3ACE04DF2F8CB4F6FB283CD6274188C90C6A8DF1DE37EF648B73CC33ACD56C914D2F083CF7E3063EBA33C58F0E261E8F0CDFA45DBACD99CF7290FA5F8EACC9869CB88E4ABF16EF483EDF5792193BE06347ABF719439E0AD27CCF613B688201585730110FBD2584348200A110C4CA81FB2CFAB41505B345577E97A994FEA7363B3B0CBEE900CDCF41A4EA067BE2451214F878163789AE67A49739EB09F6E8B40F6DD1B0D8EE039CB0157D0DB71158463CE866145A62DA83E7D3CD10C99153307ECA66B1B076686FF5B023F532F58B2781E44F8CBEA51C07FEAE217CE1E202A4CF8662C6D848FDF432AF34C6662F43562D5D81374C72A9673B379D0DD2CCE47C6224CE16899172DE2F26E039D82C9DB631E8AF85E326424C299BEB570C943F8ABC251CF1AC2481BE4E7B2B8CA0247195730002E69748E6BB8A7E1CB22AEB7D74A651BACE2856D114CCC994D3B833AEFE17D7015D450568E776789D173485D389C136DAE32459FFB88EC8A46C102672F75DB509AC6A486D68BD9780B76B2FDF34807\",\n          \"k\": \"D82B00EC3ECC8818741F6CBFFBC99350E84EE4D4A104214774525D8B78C93CC5\",\n          \"m\": \"6C8C075658F4257D42010EDFB1D7EA290D3344EE6E4C43DA799366985AD52243\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 59,\n          \"deferred\": false,\n          \"ek\": \"F1E27E473692D70B178B466FB72CABA716C2468B308C0689615A0077215741D70A633734F0140F4479BAC081370D3565D24A4D0D7AA189F1C28792BAFFE9928436B9C0215FC655253D645B3B84C237212A7868131AA623DCD8BC2ED65A2E082C4EE29A978B0C458C9E5BD656ADFC68BF64291C387886493D12825B8363932AD17B8752BB49B63D074A508AB3CDB7341ACDC5AB76B8C5F9C93FD4EB74CD9891BAA467DDB28E1733A4260A2A4D496A3F705E318A042AAC818E408DC7A9BA20520E3A4CA237C1C581598D839B67822CAEE8C04E7BA6219ABB23D9455917B84CC5C94BF8594A2D1127601BC3E76B54155B1AD8C5754D13B564906E3688C3DCB9B963B7295310341EB0A36917919D289C2A1B001CC4360285A40DE87978FC532A242422829BF8079D71F347EDC072B9884A5D26C84E07B72E529DA1FCC8E57C1C2BC30685D12635B3A136755A70224D79FA7AB1124A69701DFF4AB135508014FB4BCF044218D4918790455923039AF9591087A5ACAC37363097C41B4153D375C933C488C216C5CC1AC56134F9B3567519CA21496A4CA06AFB84C0DC2743673AA94B15867EEC0945398376DC6981C6A9BFBA78D9950E27E16D3852A63CB08BDCE1C99172B4181654BF5A90496C764B6B96A25331E84162F902009BB4B76D8BC41C08AB7FA8B1C4657AB51CAD6FCC324E4BC70979AEE6125F2BF916FE8C3F4F5A72A695A6F8E34084728B0FF59CE1FB5161D8AF07CAB498D23475869F746563DAFB49B64103EAE0CE8A17BFDF1853E743262593C327608B27614F32C1ADCCC9B638029F7F9377B302248017747AB5553833ADE94A3846D309D5BB9872C89AC4E7831D9A3A3FDA26A42747E96AA6EE375FD407585E964CC4D6821323C7874986A1192FC0C62099A764A4554B8D89B9D9585A339072E9D677ED3475181197EA28BF6D84A3DE367D7529AE481133D1618007D5832E0A13B7028BCF5CAA36F4861DD22DAA497167E171CAF912B9FC8056016520ACCE1F68397246097A5755F8462FAE3413408264BDD915BB253A6229C26B14426F439D9AB79C39B11A7F51B9376A300C7530744B754713A50D71BDC299B9C0C7B266CC41F31AB512D0BCF5AC9BEE85BEA109734E7861646A7160BC3A27AA5CA3F87128468F3266B2B156210AF79D03890386A9AC0CB71BA3B41937960E575494F60B0704960518399199CA227D48B99ABC8E85C1795DA67C0D331B40CA8E448A934A38CB24D0730E181A7B30366335958D5993385871DE69C97B22A1DDC6BE8C5242BFD93073893D2B498627B76B6D39ACD7405B64C16E60E190CF45B5EB12735572AEF8F99E37CC3C1492979D3718BD363EABD56E1E416ADF9CB106F3540ECB4ABB052D57C728A558AB52FC4FB9C078374919F29558481C3ACB782959E2C41AC05B8701217E7BBF945B970298A694FCB0E12490A6C5665E267A8AA1A5F060CC0A08093E816EF9644720024FD0ABC5C30C39EA095FC5426E732C2490B288FB38AA6A3517D16BAE8F38BFB091BD70260BE1D80B2694B3651316484C1E97B714A67A0416179F2C582E25F770EDE422453607ACA79C1EF217C2418840609FEDF914D083595C10CC4CA70D40A8BAC5470DFE4638DD86A03DD428CC305569963CF620931B40245FF3098DB6C6A1870D5D7B838C976D0D0B462F441B2D79BED3A488697A191C20C0A8281CA8A39D61E69A0767CE753A02EE8987414AA3BB99BAB032CF451BAC1075162CD1097B00AF6A4A82F8112F28861BF0A6AA22733DB462BD15F95A211161ABC0C5B5330FE4D63854F3C0F074C077D06AC2839F06913FBAF33F7F91A948A19D01D6110AE54025CA068D1B0D7CC66A6EE0BA83E377FD2313EF44C87A58354D841B0158C8010B6EEC2898A9A0778072149452C2B6A628256C257FC224C0B767DCB3710071A71B43015D994ADCACCBF7B3A234C11FB2B56E75E4154DBB473978BC25758EEDB5319AECCEEF44AD82C3584F8378437763C3055C4A84AB71815EE5E360AE0A363286936486C69976AF602885769C1CBE21229B9C350562AAD4A91BE145AB3B090B9031175EF96CD275CDC68444BF890CAC56241159C55A474647A4257A978470BC97D28549F21590EAEC25D9603ECB5909D375CAA56B076DE917B8C7251C92B34563BB56AA6C3749113A837B5F6B5496259F5EB94195E2AADEDA453309F91EC2AACB59E01FBAB4B8F8\",\n          \"dk\": \"01105ACEB10675548E2BCB6C4FA56A7B70C0824039D35A5B9BE38DA1187274C8135C24692024A44430BE5D0834CE64964F7CC0D2A8C86A6A64AFA8CA4F2BAEE347C05374B6A58A764D493F1F765122575A31AB87C5278B6E94C8704880795462E3E16177B1C3ED4B4432194148089B5218777BB3927B4650CE44160C8ACDC7153802A266612C9102289A73606498F3A6F79001E37330C5A848908C34C0C8559E056537C25309B10FC4377DDF305167D41714A70CAB02133C500627664D84359173DCA053922D874C927C1C0BF86468730254A9CB279DBB19EE0929E9776A20E3574C4403DAB3344DE865312B2CF49301DFF86BE06481C3EA9F7969AC5AD777CB9C8FC887B2782885E8F97B13728EDE4661AF68CA9CE80208763831F9385D5547DDC81ECE830716557242072039C469E9876247C53D1847955795414A6946FDFC6786F9389CF09EFC021B7B2796B4A2A48C0A3DCBBB8256507CC0CB7FDE45B87B7C2DCC8C9210279E84DA022AE65434F139A70629D15882A1B05ACB847049E9B72E1CB6AA7647F09447EB47BB052B9D8C92850E72ABEFD734BFB55DA941905B99228EC17CA66BA82C541D3D669BFC3AA05AE6966E4B67EEDC3468A632DEA41C30EA5D0F943B53D828CE6879699933AC6C15D85A6D78DA3F84D744455784642B520DC4C40951AC26725E3723BB48A75BA247B5A4EC5C5E832DCB68861F4A6A6F8CBF13719A834AA2F6C9B9A99A7A64785F65D4768EB01A2C5B719FEC6418BC98428C321C40B7AB923C0AB107F59B5AD0EC37FD82850F64782293C1CD0700C062AAC0752CA91CCF5409147F9526099997AE622945A91BD4CB32B61569C1E1347766935B3C76D67B4D704B5D9E362CCA28833DFA1A089C2C6A2380E63C812F7ABC9BCC6B53380E6B9943009A1CB4325A2D529EE5E6A428BC1F0F190BD9E20C4EB761F8B6C6B5916371FBCEAC171E2E976927D8401218623E3C403B072273EB2D68F78CEEC20DBB89669E56151B83775489CC6EB9C11903A81A001FB6408F0AA2AED1B576C16B2AD71C4293832DADE8769833A7FE517F332B9AA9394D915838F8A62A7ED850DFB7AD82D94517087B4F4C8B4314A6E0E0A49CA96FAD929184030FBA82A86E9A214F3C5E5CB7AFA31A3FC5217AC400C6FCF18C61686564F01D0C5B1832794103E8A198552202E8CD265203009D0882D4AA4508727D2749FC0A06944B7071DC0FA2B5A849B0922F78C3A8D300E5D536792348F56630D3A75A74EA143BD5C8B93429B8F74B8239BD9FF54F2F293A94AA3F4B4B9D9D6434D85750BE959F15479D2CD0C7C2CC0DFB8C95990653B7AB2F32100C97688672E09E672670CDF4CF5B4714968C533D81997FE8A92996949603691BD2216A320B46035DF84B256E23298B2B3FCD12748D550120988785C6B72B20C3BAA22CAF8789C0880A86A12C8C6C11B022A908BCCD8B144C191A299D2A95426C5127447AD3D3A70E31B95F38BFEFDC0D781097685CCE6FD8468EEA7046C54715968B9394B3934B6229067636ABC41AB644F555B5DB95CC5EF25FEFF8186BA226EA95AEB6D76DB3B9BDE2A1C3BCB72CC0B7AF0DC4BAB0F0AB7B1A12763A2485B22649CA392E8C0CCBA819BE77745BB1C4CB68AA8A439A970A939006B3AF9C3A585B094FF7A1A7F7AED61085EE4816D632A327CAAFBB6899C4458ED43B327975A0E2325D5A225D7F2CADFDFBC016D439147B892B05A61FD34AB2D62BB462468E1A9F310307F33928BD16AC3626B484CA82DBEB405400801AE2A06CA624673C3BDD2B72DDD3641FBBB7952703A5349339813404810E554B7DF7BC18793A485E97535038919046BB8558545B87248E3B80852BB7D5A23EF5E0A396C0239D57582A03350110426CB0317A5005D558A5516727B257B54CFABCDA8B49D433AD96979E575A93AB2376515C0E31AA5B0F08B2B7B69E232CB3D8C7C809C03752F355CDB5118A11BC3AF00BF2513095D4A6594457098199544C0224A80BA18C02D5FB951B4118235B1C7E9C966FF891059B11DC566F872441158C61CA2326B7D171A50015AC663E0ED71612247ED3712ABCC121052CA5AC04032DEB786B91735D612DD4BC5CEB514072145ED6B2AF926037B7655BC7379DE4FB3AAC64A3428558B8871B5979593BD33E22DA826E44CC313A2EF1E27E473692D70B178B466FB72CABA716C2468B308C0689615A0077215741D70A633734F0140F4479BAC081370D3565D24A4D0D7AA189F1C28792BAFFE9928436B9C0215FC655253D645B3B84C237212A7868131AA623DCD8BC2ED65A2E082C4EE29A978B0C458C9E5BD656ADFC68BF64291C387886493D12825B8363932AD17B8752BB49B63D074A508AB3CDB7341ACDC5AB76B8C5F9C93FD4EB74CD9891BAA467DDB28E1733A4260A2A4D496A3F705E318A042AAC818E408DC7A9BA20520E3A4CA237C1C581598D839B67822CAEE8C04E7BA6219ABB23D9455917B84CC5C94BF8594A2D1127601BC3E76B54155B1AD8C5754D13B564906E3688C3DCB9B963B7295310341EB0A36917919D289C2A1B001CC4360285A40DE87978FC532A242422829BF8079D71F347EDC072B9884A5D26C84E07B72E529DA1FCC8E57C1C2BC30685D12635B3A136755A70224D79FA7AB1124A69701DFF4AB135508014FB4BCF044218D4918790455923039AF9591087A5ACAC37363097C41B4153D375C933C488C216C5CC1AC56134F9B3567519CA21496A4CA06AFB84C0DC2743673AA94B15867EEC0945398376DC6981C6A9BFBA78D9950E27E16D3852A63CB08BDCE1C99172B4181654BF5A90496C764B6B96A25331E84162F902009BB4B76D8BC41C08AB7FA8B1C4657AB51CAD6FCC324E4BC70979AEE6125F2BF916FE8C3F4F5A72A695A6F8E34084728B0FF59CE1FB5161D8AF07CAB498D23475869F746563DAFB49B64103EAE0CE8A17BFDF1853E743262593C327608B27614F32C1ADCCC9B638029F7F9377B302248017747AB5553833ADE94A3846D309D5BB9872C89AC4E7831D9A3A3FDA26A42747E96AA6EE375FD407585E964CC4D6821323C7874986A1192FC0C62099A764A4554B8D89B9D9585A339072E9D677ED3475181197EA28BF6D84A3DE367D7529AE481133D1618007D5832E0A13B7028BCF5CAA36F4861DD22DAA497167E171CAF912B9FC8056016520ACCE1F68397246097A5755F8462FAE3413408264BDD915BB253A6229C26B14426F439D9AB79C39B11A7F51B9376A300C7530744B754713A50D71BDC299B9C0C7B266CC41F31AB512D0BCF5AC9BEE85BEA109734E7861646A7160BC3A27AA5CA3F87128468F3266B2B156210AF79D03890386A9AC0CB71BA3B41937960E575494F60B0704960518399199CA227D48B99ABC8E85C1795DA67C0D331B40CA8E448A934A38CB24D0730E181A7B30366335958D5993385871DE69C97B22A1DDC6BE8C5242BFD93073893D2B498627B76B6D39ACD7405B64C16E60E190CF45B5EB12735572AEF8F99E37CC3C1492979D3718BD363EABD56E1E416ADF9CB106F3540ECB4ABB052D57C728A558AB52FC4FB9C078374919F29558481C3ACB782959E2C41AC05B8701217E7BBF945B970298A694FCB0E12490A6C5665E267A8AA1A5F060CC0A08093E816EF9644720024FD0ABC5C30C39EA095FC5426E732C2490B288FB38AA6A3517D16BAE8F38BFB091BD70260BE1D80B2694B3651316484C1E97B714A67A0416179F2C582E25F770EDE422453607ACA79C1EF217C2418840609FEDF914D083595C10CC4CA70D40A8BAC5470DFE4638DD86A03DD428CC305569963CF620931B40245FF3098DB6C6A1870D5D7B838C976D0D0B462F441B2D79BED3A488697A191C20C0A8281CA8A39D61E69A0767CE753A02EE8987414AA3BB99BAB032CF451BAC1075162CD1097B00AF6A4A82F8112F28861BF0A6AA22733DB462BD15F95A211161ABC0C5B5330FE4D63854F3C0F074C077D06AC2839F06913FBAF33F7F91A948A19D01D6110AE54025CA068D1B0D7CC66A6EE0BA83E377FD2313EF44C87A58354D841B0158C8010B6EEC2898A9A0778072149452C2B6A628256C257FC224C0B767DCB3710071A71B43015D994ADCACCBF7B3A234C11FB2B56E75E4154DBB473978BC25758EEDB5319AECCEEF44AD82C3584F8378437763C3055C4A84AB71815EE5E360AE0A363286936486C69976AF602885769C1CBE21229B9C350562AAD4A91BE145AB3B090B9031175EF96CD275CDC68444BF890CAC56241159C55A474647A4257A978470BC97D28549F21590EAEC25D9603ECB5909D375CAA56B076DE917B8C7251C92B34563BB56AA6C3749113A837B5F6B5496259F5EB94195E2AADEDA453309F91EC2AACB59E01FBAB4B8F8839735FC7BB6B7B2B3ECAFF53F7CCEA5AED1A76414F3B57EB29825F79A4E7AD90330DE5C761E9371E9BBC4EBD1B98C390180BB2BF749D427500D5F562A6CAC38\",\n          \"c\": \"17EC1004F9E3F5AC1BB90F19D09F7CA08983179820FC9B945CC220973112318E0C212814C5F852B8E675B392140C4B2E20D5B1E4F972CBA5CE389792DBAF7C068C17211C376CFB907FA4FD468835703F559CEA25E0A12F2267326894AB7A3F4D7D83D9D5C98F922F16DEBD6D77663D2421A60F54248F5784A4D5AE151532E6573B8FFD81421B3A7E3FDAE32104F347049785EDC6AF47A417EA8BAAEC8B89E88D3B6870835EF552F7CB57E480C06B3CE95D238B460BA40EFECB0F6C9510211F02C92CBE6B4D7AE23471D187D1AC95AB0C33D2E886E32232427C1BE7DBD3342A4396378E263D7D64CF996B76ABE1BC57F12E55C9A4789B20CC087ABB217A09951BF4CF2778304F95231C05BCB803AEFD0596BF1164270ADAD28944771BE9B5050075F3F47E5C3FB5859D19E989F4E03429E1A877CE9D65FE605FA0B10F7062A003BA13614E35C940204D321D1676DB769817FAFF8D1C02321748189BCD6CBB961858FA080326BB24536A29CD19D7A25D7818FD212E28FCCC25E1949F8F6A0EFDBDB402710B4E0E7EE67C8CF475E2E0CDCD29B0B8F52712550499E24F0EC0DFB8DB333ED1C5B8F1B3D93DE676AF65ED80CBC1406E6EE78B35EC607986130F85EC3766BE06B01FBD1C93F98F8AF8FF8224CF7F23DCFD9B3CD4576A933672AC1817114BD218647BB5AD70B249F65981C2F12FDAF575A009240B11F92702527692310719D0EDBC87BD7B80D0067381BDAAEAE5FFBF82E9487CED9C51B5A2689C338E410EC6200EE40289166DD37EFD87CF4433FE78E470089DA0B2AED03EC4601B1BA3EB4C85A261462F32B2886F6BBFB6C509E058C2CB3643FE5DAB864676ADF3AAC9C4172E5BAFBDCA0BE501BFAD5A35EAEA5608E1D2200361E581385D640C2F71DCD585B6C9946F455A071DF253EAECBF61E1ADF160BF32F4AA1ED1B4F35B0D6FE5F83B2980EB81C0E4DF06CE50530919920AB319D3233DB5A5FBDE2E33DE18B66F78045F79EF9536C4AE168689B51F54619324BA1FAD9A60C406041BDD8B01A2D83C0406B72F5A6854625F41BA1AD27E15309C9763ED8FF0FE2CBEAEC0033EBE14B211DF23D16411476B637688C6269A0B7A9CE57A344373B948B3210F8666120B6A5F4F5EA238A8EEF5A757C7D20E37835CFE472843F94C043E12AC6F36EB65075480DC580E4B7510D7D6B49A794FEFD6F0FEE8AA3477AABFB26B3D1D1F0D7D694F5B1BB2618A57FE655AC2EC19869DF7EF57C422ADE6A181B57F33E6FC9B4810AE23F41EE82EC760F571F5511FFA71EFFC867B8417D719E5986D366AD7D01CB021EA809E80C508D68DEACB4C5C116982BEE6BF99BC2D052430A29BFF82EAD2A28FC816065EBBB05E4A20722BC677D0CB1E1AAD732DA8F0D854E044B5176279F1C401CF553A565668794AAB06956019E291916DB406641A0B4A94CD2F94AD954AF203B440997C9AA315D00E9791DF171B724859DF1FE44C6AFE66C1FB35543C8AC69F5953A98B015357AA829605D7246555F20296DC8D8ACC312AFBA78920C6922AD1A3C895BCE9D54C902DD87334391D5D68692E67EE3D5E1471E4EDD20A28AD22B5EEAF2A27B7DC70D5C53CFF4884DDDF837B74E8581BB2473C969DCE8B55F31EE0098932A0BFBACC0428CCA1E130466898637D876DFB972B0E0AF10C1133A8AAA8703172DDCF28A08BF8698228952E3BD3B29D6DD22C72EFC18583E80AD5523BF3828ECA00DE5D3149F09BB31B588D2F3FA205CD00C78DE8D01DA73EA5956EB0FBCFF01A3C6FA7A4B6C08B23724C47989F7E999FD49961820A8F9C7E84ACE6D7FFED8EC119BA3EACB18E1E16DDBAA0503F227BAF09E5620ED738BCCADF3FACF7F57364865B61D21107EABE4E961B04DF62CEA1EF0E7604711994CF92CF1A8B7940CBA6212AB98ED5E37934CFE9F4122C7AB33F8666BD5F0C4B240C3FCEE1A3AF8A4574CAD95D68230F9A8E66D96079CF2D58FBF7090885E83D99F6810DDAD3B0A546F3FF71CEDB22558F3C823886E2DC089916F9164CE4007F93D6BFCE1AAD3AB69A4CEFEF87F430D73F95FE96A290A3A61E4F6C5EAC36377A0F0B50A460404590E5B1AE3B4499A85123055B73296396E29083EBC31CFB4C9946C7EC4A4D6D46ADDF64C4D9EB9FC81D4BE566C464C9F93EEA01E739BC8077E9B13856AB53AE9A12C3C85DB5B06FB3A47CF855240F87D4715EE99EAC7B9C8D1F331C811547DD\",\n          \"k\": \"8AF0912F3635D93D537E9065529A3D69590AB2E66607540B4ED97BF6D985AD09\",\n          \"m\": \"BD990171C3252230BE21FA7F186A121686187B77C234C37CA5122A7AC77E318B\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 60,\n          \"deferred\": false,\n          \"ek\": \"1909B5ED6C0D8D3C301F144373956424139AFF46757A070167BAA70BE7BC375A6D43E18FB0EB4A69B49A2D951E9265BC159A282B724F3477A2DFC89378CB9FE6CA8CBF55BE9976CEC668402D7A3D2E4CA2AF6A1E479621DB16A3187461036513C84ABE5BB228484023760266DE5CA11F1A8C63EA77F6E45F35D4CAB24443B51B851DD2C58EAAB9D426C90064AF14F8BD8984CDBB84C7E2208C5AA2C7E3A222249943985CC1F199BBAC9B8719849E5D539986D8B063A363B76953D22A508CEB03C46A04F19769E327A774A063B1D412B7B43D5651051E547487F2293EE2B78849BD36D2A54E966F8B386BC735152DC34E1A12772F67506B6474A2C57AC0A8CF53122949D52828D69E8A7A326F18599BDCA23C8C8E3B28624F26609DA18044411A9866C3A7D7083F09707A70192CEA40B936BCE696112AC92582428F30A800876BA7BB40560561AC5996A4B11639BDF00DD67A52F85C6BAB54055C2352121591260B656C43B2C2E89A891C97F39B7921794CB5A552C05A2D8E8BAEC89250666734C2DB73EAE68484DA254969918B10914CF4274C2C27319412360433ED27C810712F4E3145B5265D1EDA7B6917CFFCA61CC087926CB116020C7C49D1A8A723C197CB44118B3861A0773D52C6CE25A9E2C66BCFB6C5BEB43185EAC855D1A99C0A2FD5A348BB0C579DC23C7E777AA57249008497C7332EAD85C491A89B76427835983E00F5856B1443A60C83C714B122369A97C356C0755FE1F249C8D56F0D9897680CAC247A7EFA8386744B607BD7C195B52C07001384C17EE70AA04393C081EB43C7F912F3D786C7E63DF043A277329AE2C90A0E721C5713A47417A9ED1370C2761C1C029630C26DD215028EBA6CEF5239C25383F6370863F622BC055517946EC0D2C4CD909182CB51C817A04C53B4309B14507069303245B4B55B47226970D269D87340DD5A864A85A62CE3115AB7A972C813FD18C9AA0B582E79C228468292A8A133CA5C53A4C842233FFFDA7DD0002444C51B2009654067C7AA34BA17B7368745C1719572A2C66D6D894B92049EBDF629C7C5A2D1EA82C7791B7840729DFC7975C2C18F3B319B45AFD1E0BB0C527D3FE8BB7E95A7DF958EF9268ACDCA208053ABEA894928DA891788177185B362EC3364F93D4B602167942603412CCB595DA8910B748A2EC022C3EDA8437781BEC4A913D2C56043A1BD6A5B2A858A8F7742A82986509891A9EDA7859D3A99FD78A11335910D830FBA1B13823154ADF8984DB9066B6735704196D49C8771B870C05504EA110E252B4DEC2743F1F39142B6BC5B635110CB37925A3CB0AA83858A54294B29054805B969A6352925CC53017235CD75DB70C86198EAE30AE8F933412C0504DAAFE7516BC9D7A780E04290891E5594819F08C2ECF083711A85E1A679235441976B03E37B0B0DAC6BC8456497220F2D4B927688021FF90F2E00A64DA33D3C878462529B98CB709B01BDA6D27FCAF7899BE90853BC6C8652137851C3C7FB40CB3647AD8B7B081B46C8A2587086426D48819AC1AE43C429954BB5F7F2227FD8058C18AD81203462C055ADD5045BF48CE22271C5A67E20ACC0EC93B182684E2B230C0E44C0688A2BA0A6BFB7D10C476B24719700C70A7EDEC0C6879345AA781B6B415FCFE4373F78BB33CC62A659B8286154073193894B6D8AE85284033082186478FC763231154518B1A8A59B1CBBB56A50A8F9C38223C5BFDA5C310A1C30F2569C4E179D4039331293CC18870E1D1B90E0008B29008FECF00152748FA9248B7D64491455044D86C6C3565AB4618233A2154BD277DD5067B2E6784DA94876D1BEC361869493542C470D7E58A7B6F9236E200D66CB16D70A6273E40C8C69C033CA8EBD7A2E5ACA69A5E0CAD2C352D0F36BC8A0B90A29B604EA0DD6A77FE9505E717A721849A0B435C08C66AA44E202015CAC90D364D1D83F21E5177A100A9E7855A5C5C0A4B46B388C58D256088E455D59016A4DF337C8A69827A399E2E9BCAAA7B11CE6440A7536D0BC544CDA12392340504088B8EC6CB737C3B1C0A11F9B83C049759D74BA44507C6881B1F98167720C0B35A095DB1A7659B52E38803935241766237E073B1D2C52BB5F4152AD80625E7C52352AA4EBD89820F4AD1DDC3944784A239A9F71B243AB50C1700BA6FE82A501B275DC3391EE30C022997F00F09D0F5FC8A9F5B02358F99511C32A582B24C0\",\n          \"dk\": \"5D802C21CB29AE541E06365D0E295C066C199C1668BC5255FD02C99323C31646625E2363BEF0900E13BFA27307D1712BF4D3AFFB80383A5ACCBAC575EE02980EB855245121E6359AF388B57AB94C47989497C38A09E517E6726316F2474ACBBE2E999A16E9989B8951C8DB8955F790E61B5D3F7304CD9A41BC64C8B692707A4418EF05B57CD34DE597172F7926A46868B2264FE7521289654944A066B63B67CE375CA069357D9B0B905AC871D0851DCC3DD49847A131AB6F92934CDB794A28A1AB07309811BF06633A5DDA037FF3BA5021C251589738C2A794721E42735D1EF733F8F0812BF12B628158D83577E0107E6DAB92361CA40A904F5BC3788FF609F04854B38210D441222128B0CEBC0F6CD25D6AC3CC14E616EB4A0D6E500EDF5BA06ED7ADF647A11E365533D9C3ED2C93CFA08FA715BE8A71534D007647FBBE164C4C077094BC372B64546354235861434192B85C68B7AB3610C15D5B59A30C1832F4659ACA2FA99118614C804AD75DD8FC51B2772E7225858CEC196B60008366C1F1BA8C837A4539EC10C7A14F938283175121489019BB419350121EA18284DD14B8BEDC9304CA42AC3C9E5674773CE84D621072F90873212669988978A9C3CA012610979A4BBA114A0F39BE7E6820FE881948D444D6254887E362FDA99407446584D75D78B848DB869FE5230FA2093B5C170FEBCACC3D55B1BD6CC9EDD2850FDA3C9937648DD614CBC86640453C05B403CE145C0A1A7AFF29A05663432DEAC8FEB4343B054692E923C0B99600F56697B77394CB6DD8618605EB5CCD5B6546545056081CA8A12FD38787C3FB759BE18F300B4ABC9BBE37E2873521355BF7091C6576A792B2BD6A42F367A3FA94310B14049AD8A0F710C6877A481753A0B42A8979824F1F850EA961959E87113CA2608C59AABCCC1332B5394B1B8EFF2A486AA7B0CB36361DF5C2C91AA039109F99249379B4339013352AFA68D41A0800E02572539D557496D944726677399BB136DA27B248A901ABB76890D10ABDA81172172BCCE82807A1900D2AA5D919B0211BA64C9806231C543EB6C35D2B27EF96AA2FA26C80742029D0BF7F72676CD0369FAAC07CB99251F69A0D45C952785D48655AC3B74A1E019FAFE1430B78B7B5DCCC42D6BF5956B6333925C13BAD2E853F0E75BE0D78935695214AC252EE1AA0653BB9C5330E2508616F4057A49C7BBAD11F10422B84E5252ED320611853F39C42C9E811B0B458B0B44142BC16D017B2896C8C223B96AEC573BD43AFEEBB34E4260B88D91238F3861E587648A6BC74128CF1024B034755CEA4927202CC7879A0AF778F04C47888B13ECBE48B7F08A4CEE7CB97A4C272B8A014622389281EC7E996AB1024379A5833362C420297AB97A98248CAE6E0A65B2644F0DA07C7193C0AC096ABD59D24603ABF827DFC6B7A5B7CACD23090AD900BE4E7C8E34C736AB4CE71750D537784206489810BAF00E2B3076628D0B13C058264D13B5B67064C9DCA02DF9C93B5002091347EA1522B15228B031276EAE603B88B384B075063569CF8E7634E598F2D872B238287B8BB0A681A4F3D711497949E93FB91753A0DF27258F79B3D8B44A62C2929E265789843717AC518024BBCB23C9EC658891128CBAAC9010E700B7371C37F48B75059443D89CF35C62294E68A1A65B19E7900A80B59D78808EC462AF659398D934077A3785F999B66C60059D24601D83957597C6EA99FC5E95E4B436AD988653BA05A9FE3AD8118237B1A84D448498B426247FB5DE4979209C06800D6A9D39543AE787A461B1E433066EF2B2FB0A481388C5A68557915C4B8CF7531120C1D340133F5D5705E59136531265E3B0B4A1A501172AD8918C1F02A7B06453D8C05B48D561345050B49F893DB946AE26B6245F1926CC205E6F81A7455935B775324F1298A3BBDFFE53ABFB950176C7F65820A43C519FF596E898166B9A1563CBA7CC971B73077AEE1DA384E88887289B63E662AE7984FB8F974B7A42CCCF17750B37FB8A9424106275F122DFD9245A0A28A319CCD49D985FC58A1AAB178F12B8B3034AE7AF89532619EF24A0A77637C0033981103CB5CF9AB09D879EA067193427FBA219D49A5A35F95137EBC4A0D046186BC9E3C091B14195E2B43BC5A79C0B793C97E02823EEA301678723815631909B5ED6C0D8D3C301F144373956424139AFF46757A070167BAA70BE7BC375A6D43E18FB0EB4A69B49A2D951E9265BC159A282B724F3477A2DFC89378CB9FE6CA8CBF55BE9976CEC668402D7A3D2E4CA2AF6A1E479621DB16A3187461036513C84ABE5BB228484023760266DE5CA11F1A8C63EA77F6E45F35D4CAB24443B51B851DD2C58EAAB9D426C90064AF14F8BD8984CDBB84C7E2208C5AA2C7E3A222249943985CC1F199BBAC9B8719849E5D539986D8B063A363B76953D22A508CEB03C46A04F19769E327A774A063B1D412B7B43D5651051E547487F2293EE2B78849BD36D2A54E966F8B386BC735152DC34E1A12772F67506B6474A2C57AC0A8CF53122949D52828D69E8A7A326F18599BDCA23C8C8E3B28624F26609DA18044411A9866C3A7D7083F09707A70192CEA40B936BCE696112AC92582428F30A800876BA7BB40560561AC5996A4B11639BDF00DD67A52F85C6BAB54055C2352121591260B656C43B2C2E89A891C97F39B7921794CB5A552C05A2D8E8BAEC89250666734C2DB73EAE68484DA254969918B10914CF4274C2C27319412360433ED27C810712F4E3145B5265D1EDA7B6917CFFCA61CC087926CB116020C7C49D1A8A723C197CB44118B3861A0773D52C6CE25A9E2C66BCFB6C5BEB43185EAC855D1A99C0A2FD5A348BB0C579DC23C7E777AA57249008497C7332EAD85C491A89B76427835983E00F5856B1443A60C83C714B122369A97C356C0755FE1F249C8D56F0D9897680CAC247A7EFA8386744B607BD7C195B52C07001384C17EE70AA04393C081EB43C7F912F3D786C7E63DF043A277329AE2C90A0E721C5713A47417A9ED1370C2761C1C029630C26DD215028EBA6CEF5239C25383F6370863F622BC055517946EC0D2C4CD909182CB51C817A04C53B4309B14507069303245B4B55B47226970D269D87340DD5A864A85A62CE3115AB7A972C813FD18C9AA0B582E79C228468292A8A133CA5C53A4C842233FFFDA7DD0002444C51B2009654067C7AA34BA17B7368745C1719572A2C66D6D894B92049EBDF629C7C5A2D1EA82C7791B7840729DFC7975C2C18F3B319B45AFD1E0BB0C527D3FE8BB7E95A7DF958EF9268ACDCA208053ABEA894928DA891788177185B362EC3364F93D4B602167942603412CCB595DA8910B748A2EC022C3EDA8437781BEC4A913D2C56043A1BD6A5B2A858A8F7742A82986509891A9EDA7859D3A99FD78A11335910D830FBA1B13823154ADF8984DB9066B6735704196D49C8771B870C05504EA110E252B4DEC2743F1F39142B6BC5B635110CB37925A3CB0AA83858A54294B29054805B969A6352925CC53017235CD75DB70C86198EAE30AE8F933412C0504DAAFE7516BC9D7A780E04290891E5594819F08C2ECF083711A85E1A679235441976B03E37B0B0DAC6BC8456497220F2D4B927688021FF90F2E00A64DA33D3C878462529B98CB709B01BDA6D27FCAF7899BE90853BC6C8652137851C3C7FB40CB3647AD8B7B081B46C8A2587086426D48819AC1AE43C429954BB5F7F2227FD8058C18AD81203462C055ADD5045BF48CE22271C5A67E20ACC0EC93B182684E2B230C0E44C0688A2BA0A6BFB7D10C476B24719700C70A7EDEC0C6879345AA781B6B415FCFE4373F78BB33CC62A659B8286154073193894B6D8AE85284033082186478FC763231154518B1A8A59B1CBBB56A50A8F9C38223C5BFDA5C310A1C30F2569C4E179D4039331293CC18870E1D1B90E0008B29008FECF00152748FA9248B7D64491455044D86C6C3565AB4618233A2154BD277DD5067B2E6784DA94876D1BEC361869493542C470D7E58A7B6F9236E200D66CB16D70A6273E40C8C69C033CA8EBD7A2E5ACA69A5E0CAD2C352D0F36BC8A0B90A29B604EA0DD6A77FE9505E717A721849A0B435C08C66AA44E202015CAC90D364D1D83F21E5177A100A9E7855A5C5C0A4B46B388C58D256088E455D59016A4DF337C8A69827A399E2E9BCAAA7B11CE6440A7536D0BC544CDA12392340504088B8EC6CB737C3B1C0A11F9B83C049759D74BA44507C6881B1F98167720C0B35A095DB1A7659B52E38803935241766237E073B1D2C52BB5F4152AD80625E7C52352AA4EBD89820F4AD1DDC3944784A239A9F71B243AB50C1700BA6FE82A501B275DC3391EE30C022997F00F09D0F5FC8A9F5B02358F99511C32A582B24C0011C0579E0446E2C171BEAF2BD014E13D2B88B6515E2B8A11CCB8FA4B91BF2B8A932A47B71E782BA97D69908DB41682AF409C94C050DD621CF8D958627D0FD2F\",\n          \"c\": \"588B326FAF4C640216A4E3DD75FFAE0D4E6BA0B6AE4214491C3BDEF276E98585CCC730B0188706E3CB275EECBF0F023EAE4E4A5D07A68D961EBA5DB25061AE3C76C2FBF6B898D90C44E479E2859F0245D579032146BB34AF36DC16A9CA55E6FAF15A6D53C5A0554F9D5D39582AD6225A1729C4F3672C5FAC82AFC900740F7B738D99FFF2E4A660BAF194E2C129CE4C6DB57859C8334D859D49F1FB46B55D6A0AA71CFA726E6289E808AB016129CCCA273A56E78812B1F1A390311286E9C4F0E8ACF6806E9DB5EB2BC782AD0D68FF394331BE7DE253AACDE455E4185C81E7DE685B7358CED67FCB92DA724A93AA86A09D33B504DFD0DC2A5F113168E6DEE9098BDEA0054B3035142503A5AD671B5041113AD0395A40A476DCF52F2C41CAD9A862762639ED23205A90EAC964B68785DE9883BCC7EB43CAB6126A116F1303B53C0DEBF9A574F3835F3CD791CEC539CF15C4C20894013F21C3E903E39DC36B230A505D33F6F81A713533494F62241E1ABE839FDFA972648EDB64D3329CAF8786C4B19DE97A4188A5D2AE995FB45333ADE7122BAF902062B56E0C5A34732C493A2F4BE714B431B6E29AF52AC27061CEE02F05ED5A96D71DBB42B06C3BAEE5E23136B015C9A7DECA77AD6A7850B58119CFF9F445D1F36FA628564F02F1BCFABA5C2783469CE4CEBF996F6BE9C2FDF5210AE428C221039BB4E343A09460C81DA72C43B52DDB44616CD03BB9F1319AB399FF44837D14966A5BCBEBCF7CC482B1E691E20D2FDE85CC327011D1A6E5514641E3F0B76E5B6E1B403A76F735C785BA81CD53B72B237B18220F9EC51BE811CF614B454BA43FB58591A0C3385421810E7EDE6895DDF6566C1B265DF21965F9BEAF6FD3599CE636E66987F2DF9559D27E04E37F7428C205DC52061B92238777199ADC0F5A19FCA01617129284A6FE91AB3F880B5741932BB690ABE5AD7D68107E330534EAA8F13A35218CD16109C1E7D4F9203EC7A21404745EB0F1CD614B8AAC8E030F6FBD84FA4C554C3699170CB2EC060FCF2E21B7FDDBCA825418BF3266EABC203F77AD94668A6CDDCE524A805115562ACDAFB88381CC0EC7A00BC7CC168BB40AC36BA89A6637DE33A31B6E90209752F8364B0D659530BDACF2D695F1D1BADE99FCC6A726CF110491CC3C19A18786E2EEAF7E7978DF2D90E92B9C0C3344D506978F09F5F33AFBA3CCAACB76F9B6C11261E9AA0965F22DDE4FB8ACE9BD7EF16AA9BB1633DF10D96CFF15930D760898A2CC48DEF58546DA07D0A74FACB66F2A37D9DE09F1D95EAB1D695A247E55C648DFA2D2E23A89E755051C9CAAA410B6A0947140AE1A8B0E1411933AD5A53878D1FD6CB980217F96C6DCBD6F4D3D8490B34D110500C95435B4AF6946B019DFA20476B31AFDEE8CA8346DF824D2DCE53996F1960570E1A8360B2C583A44239CAF65591D931F85AFA503BB3A4AB3FD763E824721BAE2537B2FEB4AF06C9459D18CD6B07A68328132C5D4C06E0088812EE20689BCF8183C953854A48A3B8848A8990D3ADBC2F3D2D789029A1E58869B4347D1955E776F0DD0BF9E86AF8381DBEF172AFC9917595CD0E85921315E81F69AC5DFD4D334A13EA8ED5EFDF8D1334E4C873A10CC5E1BEA470977D17A5E4C0DA2EDA1DB017BE8152447DE1D3FBDBA79168C33BF393BAB32630658F10EAF6DBABE5184EA6437C69386F154D1505492271CFF931381E29B8442DEF27A3D123DCE1422F099D505C237509D6AC344A3B7C84DC0C3E5070E5DADCE76404456E6B46EBB1C38BAF1DC5F9677A969DD2BBBB351E3D0BBC54C50FC2A6F15BEE73EC0CBE906895573B02615518BEA90A75B1623F5E11D86D6019461691891C1CB518EAEDC8AF0AE64F92AF0A653685C4F219B974D3DC52496E8EB7DBCE61568BE3987E28A5B6F5B5161CA4B46E42DAE63ADB497C75552142F6C93EF95189601AD27F3213C150F34BE5C38DCB3A703024F00F9D4BFEE3058DEBAF13EC461C61CD51463D50AB338CC9475D0C3F8FEF25D4B65B5657E7AC200B633148C587549A6E0EEAE7EF63BDBE1AFA1625873991FCD7C11DB82E5358931024A10911F43C289A7816F293527279F90A3C0A62D5FDA98995AA784E557B0C4DE77FD18872F07351791623668541FFC4373731B5751689C313AD5BA560BC58A8BBB3514AB20F27CB721A65C1D88006FFF5F9EDFB69A89304A6CDBEDCE1261BE42BB7BF7\",\n          \"k\": \"3D14BBCD60FFC1EBB9E96EA5FB23A5A18BA6E370D092E2BA5E3232ACB5A5FF70\",\n          \"m\": \"135056EAAD8A28DEB1BE77EEA30CDEBC7B3DD89D1444DBAE145F39898256ADB3\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 61,\n          \"deferred\": false,\n          \"ek\": \"11068DF3FB0ED1514D8B2B8F424C03B75995406482FC8AA24BA003886711E921832222367CB2C2EE87617A30A270B447D4A1BAA3377B83FAC80A7C8D779669C49C461B311DDA442FF10C9F2B9057225423BD119C68E34FA7A92DA2D36C58014A9076C391A451EBA75B5C92A2F14853845838D0DB502629B796703523C29575B4A5094C1BE329066BFB7AB6E17FD0163BE4E60E9D676FB970C68AE416897174D4577E0AA8BE0D234FBC80279FD724AD5925FAE2B478816CB07C165A335F1B27B030E7BA5CA40F610B0E07C82FA134812D9A4E083836B158231DEB7568B73A9B3C60780266BB550A36C7288A9387D1E4C904D79296088047F34F4E72A23A1720B7543FADC3821C6442F4B501E6894A6E25231437AAA7A04329A8CC6AC729B185781B8BC4D9142063E55A0F053FFF57CF93C5851C86714E872840A264E316C37CF90CE60960EC2B0F4D18418AC96E202C07EA61B2B1E6A430DC5DD1A18926E0BD88351B56434E26CB10268468D67B2D04FB2AE7C43C032675A2C88A3E704A7D110B4A544975616D694C9E6E243E3D546217471B14711505B945E87C0D39170E3AA71A0663062CF07FFD0C3952D526B662CAFCE429A35839D667C7F0090965CB1F87642D2A8C28A0A57B4FF539CBC3195A0B063F3966CDF41FB4D9451A84970B6B93E006342247BF5DF06B041A6A2B019C3FF93DFD39502E198F5587B358B39B640C7A71C43A31D711E9E5434B75AABC436531542F90730EC7FA6B72A3613BAA4C0579A245B06B979B22623C93D5CB6437D17A7E3302BA1CA5EA0533F02B1D481573C1C3AB05424900F1613B379707433D7089A316F52012B646B9A24A90F87584C36C1F58B891F5482B757F758A03E53B38F282C2700502984ABE9F1C2BA00376091021DB8A1ED27B01AB11B7F404B2C5FC9E0F8943BE434FA4135A4714507FA628F4027D0DBA912E0B3B096C057BE77506FB8BCF5A8EE6DA96CF335BE8C0555EE446DCB96DC2567348433E98D46C83319E3FE1624AF07AD838241C5CBC3C78C8493214447C8AD0D40DBF319057531FC456CFDEF999F9F280234A03C7437786B1469D705655D01EDE1A72BD0981978815086A0DE74B059BE879199A67C67BBCAAAA87201A1910F39B5D35670A9790DB3B5EFC798BD9EC46F2FA1E993110F180203BD1A7D3506B87325DDDD695211440F60CC1C5B7A947F42DB94ABF0C8C3B203A7038682DA9840BC8B50C4343A4D49AB89DE916CE72835AD4C7298B68BDB417EE999EFE7B20C6C99A0B36BACE2C35D7388DCECA369768463051B377CB57BC0B155B969EFAD615BF7B3AA1189702A7494CCBADB0266224FB5BA75189766771BD1B54FE1139E6C607C95A2DCC2462515C4C1981C931C6628202A1BAD941AB7667F89BBE94118568F0C5CC59914AB113AA4C9DC2787F92BA2E9DD7363D99CB7EF6AD2EB5C1B992670B5541B1BA9368E7986D4628AE96836018934FD100670260ED931BF9A47652B85C44F988B559A48B2007361A3E4FA88AB8CCCF94939D5F419861FCC617250C38DCBA082CA50035A196880540E50A0E1BB363E81B1FD158DCA79DFFFACEB2494712F28A52249A103C5FCF888D275A8BA3768BA0599F5FE1CF632B07F4120570EC5A3EF96CD3031A3425A0CF3A7E408337412288575C1A414A271149A2B115825D409A1C478980CCBB4B4B1C2AF675DB141A3CD12B0860318EC340B7D3C2C0521C57D5675864243AA55C517BAFDA7187A174C109CC1C65BA42AB5288EA23CBA8C286EF98479BBA0396106999C220CFD043C60C4B9610ACF0086AE9D883A0A89BDA5AB9993C1DDF82610B0A1C6D75093B207A72037E77E76CCB282F2A0C7A9149941F3A149EA18CA519C1800B392F48C3DB358EF1179498CC2DA8EB48180A99F1BA280012A324AC8DF3FC8B3D6859A0B4334980157994502C80C0AAD301F6E9311AE9C132889A3C26501678CFED55427F94B05B57404B4BACC7607E19E873D1902163B25C06BB3CEBBA5D403456C2CA26CBE27240ABC8B6A85E50762FFBCA34FF96C1525B2A35435A7CCA663B37C202931CC16039EBAB2D6E45BE1002023BE959F2568D4FDCADD8A1B6EDE529AB088D79DAB407D4791AB43FE5087E4A9CCEBFE02C0C44C3FA925B7294A92E928C29663D2854210D405ECA2C2E5032A3058C6FB2B381F5597FE98855CD09B73DDFB831B56551306BA4591ECF403545BA\",\n          \"dk\": \"01D50182530F72E8711F976B0254B13EC997A6C63338E2A0009B3470A9AE5A014765380C3DD1964F4C92AAB0182DC07C11E278DC6C3619E2125AE86985F09107A48208B62AA764A5ACFA56AB5ABBEB93402092A86CC347FD674EDDDC9307D42B593C62D94181F85729657288EC88915066306ABC84D8C888AC4607C739590B8B5F08543816B13E5539007EE732825CAF044210371A4BB85382B7D76F3A1520296207654CC2F94B9735800161B952C8928BFC611DDBBC16DC13A12CCC4A6CF776B8EBC0A800BD72B71A138BB045CB0455A4B8BE59187ED12AE2D6C9A242BAF78B21CF544AF2EB7988100B3B85C68A638C3A123E58959559063C53C53568A326BEF9696E9CAEAD689F67B59087A0AD6721755B8C0DFC83654D3ABBA0B0484CB43FAC99BA196B86E0D26A99FA6697A80B7607020EA2CB4BEC39FF556ED6934678821BDCF69D91116EB82469C183173672195C74A0EE836E55AB57B2BB340A4569B0B91DB07270A5902ACE3647F75C429A78C942B40E64C1712C274FA4711F3494937661048F85BF81BA39F1B1CB2ACA86B4D7B4480A433B0407C7D4306CDA7A13F536AB3386C95301102657556C240A576D8791048609782E3A2593886EF8C41556B4209D45250C5B063412BCAB9718AC64C7F9A5BA775AA566EBB957C947447353F9B8C958679FCB2B93C4D4842E65405AF3A9F8D49C22225081C27F5470A6E3A44C8EE9979866A4F1E5AF959C2082494AC9A34F4B3813B0760CA45C8D489227F23CACD7D21BC4999243AB7286679E1E9B12DFF50AC8C68B7F8794FADA1B15A2235CF84752A714C8DCC5297AABF8C529B8C470F9E24A972056FE5776AC9C53359828152B984E016BAB14774BA956F3C6197452BA9956302EB63D9591307F7891B9F60BA85057FD48913E210845513FE744418B7348C66CB309A93DD4CCC15FE836456AB94BAB6D7078482CEC5D92FA178B0558225358F571C851E720A40446BD23CF2136B18D0530A460CB6BF67FC2B1C5C255999D868DDC5A04B0C0205A44278E7C100B2857DE304D37B93959E76D59D801A1D111CA17281BA2C82407243012626DA8066C6891CE9765E3691F1ABC16A56534F7EA0AD3517C8911C42B8085E3D1423F650957293AC6FC64E7F955926689CF72B62B6C8EAF178FCA4B10A147BA8A1201BBF20B3F4414C09594AAB84CFF465BC5AAAF17240AF7B47600E83336FA8FA5887CC761697DD148ADC21B12294281E25B08B22636E48E0AD94868B984CAD77BF1F1AC6FB9A3281B05CBC940929520A7A3094B1B611E096441313139E7C47A0B15859959BC8A76B2CAAB5A015EE09C19181693020B4130D3A6079947B0003CB0A94C644B190C24CA786889AA22882B37119274B4B8941967C49FB1811BAD43B7BB539A0EC143CBF61D9245AB96287164A4B1CCE519CAB092442C370E885794A81B185065AD41BE2760A83BC426D68C8E82C194A7C7020180BAA051705D21B291A95791D42081F68066AB243075A5E4A82F940C802A06BB910296DB82C4923A6DF25333576069E1900E0979570FD67832C37FFAAC3DACC48184EACE1FE0379F6C0C2EC6BC08F6A80EE6ACE301B8C259539CE7B7A87B5AB098978CCB0EA2A602DB1B287AB03568080CAB141066478A5E7728A642A5B9A97E435021FA8B5FC6B333C22185EC10839A94BD630126565972DB56408405BC35DA1786D068DC80992C3A0178F5C8A2CBBADB13B8F35A5B307A8086D216432B616DD5B47A02551CABA49BB82D306715097059D92019150549C70C0B7C53C4E763CB73384365C1004B369136D51662C41F0525CB6374193A498E23862ED0578619B20557E3A095355AA39A2F78E4257AC24DD3C519AB178C0700205C34639184B128B069BC14979885BFB7C81C89409165743AAABC78495A0BBA11950243A56C645BE4329624E71692B09E20EA4F81079FA50CC1A49946EAC26127833546FCC923ECCD3CE894BA5A4F5913192B07BECE87B3A81095C82B55B5D72A3DF1CA438668DEC2BE1CB0674C25CC51938ED5E9B2399502A2157A66B5053CC2117957490A113699975383F69ADB51A87CC9582D041628424D76E547BE32C273CB013F75388E7C9CF91190CA293CE2AA7F3FD749B2FC2DBDC9468B80904E061F06E8593CF33A74B305EFE157BD6B7E11068DF3FB0ED1514D8B2B8F424C03B75995406482FC8AA24BA003886711E921832222367CB2C2EE87617A30A270B447D4A1BAA3377B83FAC80A7C8D779669C49C461B311DDA442FF10C9F2B9057225423BD119C68E34FA7A92DA2D36C58014A9076C391A451EBA75B5C92A2F14853845838D0DB502629B796703523C29575B4A5094C1BE329066BFB7AB6E17FD0163BE4E60E9D676FB970C68AE416897174D4577E0AA8BE0D234FBC80279FD724AD5925FAE2B478816CB07C165A335F1B27B030E7BA5CA40F610B0E07C82FA134812D9A4E083836B158231DEB7568B73A9B3C60780266BB550A36C7288A9387D1E4C904D79296088047F34F4E72A23A1720B7543FADC3821C6442F4B501E6894A6E25231437AAA7A04329A8CC6AC729B185781B8BC4D9142063E55A0F053FFF57CF93C5851C86714E872840A264E316C37CF90CE60960EC2B0F4D18418AC96E202C07EA61B2B1E6A430DC5DD1A18926E0BD88351B56434E26CB10268468D67B2D04FB2AE7C43C032675A2C88A3E704A7D110B4A544975616D694C9E6E243E3D546217471B14711505B945E87C0D39170E3AA71A0663062CF07FFD0C3952D526B662CAFCE429A35839D667C7F0090965CB1F87642D2A8C28A0A57B4FF539CBC3195A0B063F3966CDF41FB4D9451A84970B6B93E006342247BF5DF06B041A6A2B019C3FF93DFD39502E198F5587B358B39B640C7A71C43A31D711E9E5434B75AABC436531542F90730EC7FA6B72A3613BAA4C0579A245B06B979B22623C93D5CB6437D17A7E3302BA1CA5EA0533F02B1D481573C1C3AB05424900F1613B379707433D7089A316F52012B646B9A24A90F87584C36C1F58B891F5482B757F758A03E53B38F282C2700502984ABE9F1C2BA00376091021DB8A1ED27B01AB11B7F404B2C5FC9E0F8943BE434FA4135A4714507FA628F4027D0DBA912E0B3B096C057BE77506FB8BCF5A8EE6DA96CF335BE8C0555EE446DCB96DC2567348433E98D46C83319E3FE1624AF07AD838241C5CBC3C78C8493214447C8AD0D40DBF319057531FC456CFDEF999F9F280234A03C7437786B1469D705655D01EDE1A72BD0981978815086A0DE74B059BE879199A67C67BBCAAAA87201A1910F39B5D35670A9790DB3B5EFC798BD9EC46F2FA1E993110F180203BD1A7D3506B87325DDDD695211440F60CC1C5B7A947F42DB94ABF0C8C3B203A7038682DA9840BC8B50C4343A4D49AB89DE916CE72835AD4C7298B68BDB417EE999EFE7B20C6C99A0B36BACE2C35D7388DCECA369768463051B377CB57BC0B155B969EFAD615BF7B3AA1189702A7494CCBADB0266224FB5BA75189766771BD1B54FE1139E6C607C95A2DCC2462515C4C1981C931C6628202A1BAD941AB7667F89BBE94118568F0C5CC59914AB113AA4C9DC2787F92BA2E9DD7363D99CB7EF6AD2EB5C1B992670B5541B1BA9368E7986D4628AE96836018934FD100670260ED931BF9A47652B85C44F988B559A48B2007361A3E4FA88AB8CCCF94939D5F419861FCC617250C38DCBA082CA50035A196880540E50A0E1BB363E81B1FD158DCA79DFFFACEB2494712F28A52249A103C5FCF888D275A8BA3768BA0599F5FE1CF632B07F4120570EC5A3EF96CD3031A3425A0CF3A7E408337412288575C1A414A271149A2B115825D409A1C478980CCBB4B4B1C2AF675DB141A3CD12B0860318EC340B7D3C2C0521C57D5675864243AA55C517BAFDA7187A174C109CC1C65BA42AB5288EA23CBA8C286EF98479BBA0396106999C220CFD043C60C4B9610ACF0086AE9D883A0A89BDA5AB9993C1DDF82610B0A1C6D75093B207A72037E77E76CCB282F2A0C7A9149941F3A149EA18CA519C1800B392F48C3DB358EF1179498CC2DA8EB48180A99F1BA280012A324AC8DF3FC8B3D6859A0B4334980157994502C80C0AAD301F6E9311AE9C132889A3C26501678CFED55427F94B05B57404B4BACC7607E19E873D1902163B25C06BB3CEBBA5D403456C2CA26CBE27240ABC8B6A85E50762FFBCA34FF96C1525B2A35435A7CCA663B37C202931CC16039EBAB2D6E45BE1002023BE959F2568D4FDCADD8A1B6EDE529AB088D79DAB407D4791AB43FE5087E4A9CCEBFE02C0C44C3FA925B7294A92E928C29663D2854210D405ECA2C2E5032A3058C6FB2B381F5597FE98855CD09B73DDFB831B56551306BA4591ECF403545BAC17C983272288473C7676430281761C00CD2557C8470374B257D99D63E68C2631D1B02042D01389FB44726DCFE6501D72FE645D5F098EC86393687E2E245FDEF\",\n          \"c\": \"6802F268BD6991AF8993B2CE0365253B67FBFF0422D0536119A91A8AA3591EBA7BEEF1B3E08702DD63D9FB48D8FAEF6BD7BA282095687A9C70FFA21960EC27EC9D899A50FD2C6103E1018DB559D7CE368FE9CB0D4206445CDCE0B74F1BBB4DDE53008E50C632A36D9B02E97192DD633AF5936DFEB0F5FEBF306428E7993F9E3A8E47617F224AB50403725624023DC43AD6CCD3A37931E6514458D7F16294AB8ABDB042842F259937B31BF2BF327B2E8A86B20B6A0BA3AB87D897EFE6ED969E10D80BA1C7F53ACD704542DA064B8BFE8EEA9D73CF6453F3E1F0137E0A52C41A709689D3311A0695FF25B8E54512A4BEE5ABD52A887C52B1A509C2E7547EE621117E1A024800B935C1D50DC7B3A7D9D385A1172713336EB49C630EDA7E1490AD13316E5E0F7302006FB6ACEBDC6ED9EDBCFE2F9846ED0F7CB1CC2BBB593A7DBC6879B916C81BE5FEA5F4361B4B2FF17AF7B7D21DEB36D9DF9E504EBDF11ACD7273A1D7BB13D690BAB23A52777E208A740E75F797251E9F87915B975E7E764D3B2ADF90937D79D5F8FB1EA8CBA525C4786457E497ECA4A10757E533A7644FA04034308FB197FB133D136D0B0C9B40E0977E6C2572156F164F3917A4D0E6100B7EB9F22517450309B479634E9770F23C83EF87FA9AE94E90FEA32FBADB9DF1D50DB1F1F8ADE62F1501FDD1D90ACED42446A859AE7802F7F66BF785AA454E89EA8CBD5F600F9AC4E8237C3C2812D5439BAF89CB2D636441C566BD43C5D45489B61B2BD637119A5E5E0CE452EB309B4D7F6C7C387930A9FF6B90CB3C225C99554543EED7A71533090EE0C8A6CF857F4BAFF48244FAF5DC85DAC61D737B7E6CCDA8D9C439B2120A47A2CBEDC95D1C3E56EA4CBB9DDC89DEDEE5103ED468C8115F544DDEE5DBF41456F6465E1CD5C01DC470D9A9A64763BB679BA6592E64C82F9488E008235D6FCB84AFA7D2E454058AE57875F2782BAEF71F1512069FA24802E62AFB5298E307AA1EC074B2B91356045159CD6EF8EB946B6EED50D25E729FB176D4A866BAF36BFFAFF3907928D25764BE3EE7D226E7C82A7D9F2A3AA69022083082FAC7250E6DBDB43BBB8C77E03924D8D297CAE45097CC7A3833CBB75E69D03D01198A9B9331A8E9E10AB82EE349099918B07878120EF812B1B278283042234B36DCCA031B13687E9F1853E5503B32C1E1CBC51FB88FA5B1B044CC715F24883FBB2B5D45C7F461E3023AEE3F18A34030EACCB8A21FE0178F845A5380C3BD8B8DAB193BE5E02F45D30F8B8A0173D89ED0D6C4A6959CC1AEB34CA8B96FC46393FFA35123BC87D580CBA66A21F0F30E30F9899216343D6E65B8FBB1BB5130AEDFC4BE7234AE6665E0D087CD92812437E18C81AF042A55840A58C8C15FFFE182E19D9F156A246CC1D359C36CF355B71076BB6B9F9E9C8C6F9A909CC58F41CB79560D7D849626D6CD1739D90B3067D6B33B9A989A4107F6B0103F0F7F391DE8FABF9DB10F580EF53885DEA39CD96AA8343164EDE94E6CB7CEB8347C24A23C40A3D0C851808B46D5A84EE6E1676626DB741C7674B0D33DAB62FC8AAED40F4E6A9590354B0D24226FEF439D5D89D1B48DA564D30744F5ACE5C61ED8C3AD522D87381E0311850F03B76497AC92AF2D9B7FF8DB0BA1D5A2D63586AE3DD08C4A0462A39441A20C59332D2F07053B1436117B1A9D43C477D3D956E129ABE92D7E6D7A2A383E4F0A19F59F8C565DA746D847368ACC95E7A00581B6A330129AA1A718D0B1860CE775A1BBBE244B04C2D1A94F37E2E360757B9830D2E7814D402E808689C9B02E871061D3B8A6DC408ADE9D9C3B77C8B0CE8B3B23A246FC1BFCAAC2B2635C1AA12AB3EA937237EB224B5E87331B5411A2B2F17214B1A86B7644B2BC9CFC2515A6BF413E58380FD49FE624C0E3DE4FFCE4B880907436F425596A1005D55B366FD18188EE6DE1CBF9614A35CF163268DCB56C76355D4ABB3913D9C31C1AB3E9755A305FE2E186715DC3274EFBC1A7C97E2A70F5D6DC31FAE5C56F4F335D8C77AAAD4CA2DB284ED2E56E79FABE984758C6348B7F195BB082BF5D8482965CA1C6F6BE8D8FB9F0E176BB4308F8B064E48A848332A81E34D28811B76AD62EF9F51EE606F74DB8E98E9CD5D7B2356B28D6A5C7398625549DC0C35E4D44E9CFE30BF8AB8A75AFEA84CECC051D9298D7B91EA8DE1FE7736F1D2038B8D3BD2A1A1FEFF4730254D2BA\",\n          \"k\": \"B45BA5490571D2DFAB9D9204398EE8F141A3FF5B415A2E2A8AA3391263992C82\",\n          \"m\": \"54E7B2E3305950EA570F823FE36A7999E419BB36181B5514860BED41F418EE77\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 62,\n          \"deferred\": false,\n          \"ek\": \"4F4504922C38F9871EEABAB438D03555F272E9F9493F005F5F8CAF8A620F362A1D1B90A66289111D03929118CAF31B0AD9ECBB203017A02711BAD26B02310379C117F6F58257F6C6ED5A14BB32760D5A9EA062A9C106064D703BC9E378DF498B347075A7F6515787C500472BFACA0729A4590D39C291DCAB327C10BDCC6B1837883BC90B379B3C32F56C30F78C2A347EE0452FFE4878E2280608265931CC7E123768B0F93EB0081D642B4DC756210952541BB3BE08352CB983911F3B944671B759A16FB28C3883020892865E903045C84BA1E5E54159604AC058056C6153F738745140CB8B5ABE3318809651790EDC20CA5380CC76568DA5915C5AB8272748B42446EFAB58852C619332A08F537C28E93EC676345E0A2666708CB5D5281F981D3B8A19180565704808963B4015DC9B8C06905DA43680402E6B5888F2EC7448AA6578787EBF42329D060998E74B09C13D1C432A17F56D18BC624F02AC7ECB6235C42A24B42639FC490D07A635360932C71E25720EF59340F21836B74AB8F31C2CAC035AEFEA846210AD665A5D64616861F1CA3B963A2C10C662F7AC66298751AB3CE6A64C59770568B4B234129550702375D722AE4BBA5FD0899C84A834E7B7938627C9285912D346099423BA37B25364B67258A121650E895660C8E014A5B08D38B7AEC29175B424C234D2B87C791B60159AE753CB61D240DDD2814F1A7EB69A79AC37634850CBFBE2CCE6E918D1CCC70E13707732A6AB11329A3336D8E221531335BED560A468092CF690D1B8791F58993BEA001186AF5E48A2BE0B3D17656A3ED98BC3628C9F863A2C88C90AF25A11AB41FE7CADBB3C1013B59A115AB83F9003BE28759A3148706B891D9770961AC53D4C8B6CE5788ECA3F4A4A47D7F5165DC7B94B358591348BAD7B8CDA482B26F650DA1765A0911527260703E2CCC703A69F10331095BD3AB167DB52A3EC16B69F233862EA11AA9118FF539071A05066388A6DFA10BA4819D31A03B3980677CC6B888263D74007057C6C3C272464497A58681816C3C59E05064D147F62D5C848B8919C97C3BAC20A43CB58F8CA98EAB48B221851F634459B7AC514161A1C490B8B576A0FB2497A65006EA78F9ED50763A19F8D2C99D546642A68B6C2A49FFF919687E67EF0ECBCE23266114982FDF15F572A9519EC12CB90BDB3516E85E01106B9BA456738F4206F5F0C314156307F4CADC8983A91F730219C322A1602714849CD1B7DF5D02C24758DA6DA8FC8600CA2C1CF7294B1F6598D9E844625C29F589006C0875F3FB33EB357B7AAF0607BB31743153B98D64A8FC56DE56714D71303E9F6A3ED228961E607D6D6C0D4776828B7574A8CA28D837D24470E56F3A6CB6A4A90F5A9BA8A3F60576857E1C7AA52A16E1A84F7421EDF46C312BC83EE0776A9819DFA8B66FF2BB907A96F4D6228FF34ABC68248C1990C2484CBBC70745B045C602C1552B952FE8B4D24D027E1C65B3704945E507D09A9A6AA7000C1607A27591F606AC1D2C5CB55BBA149A15C4B261894EC9A9AB9C3DE446C1832744CF6BBBF949D54498319504BABACB8DFB15F90F66DA9E0B7F8B7A42A7B09C5E19DF58CAD8641C2DFAC77412B49D17A0C02341C6FB78DF00809903835518392D0288907184F126A69BA77863C84C1813835926311EA705433298D77AACA4DFC30CA218DAE405732841605840625E57D7CC3AEFCBA637CF8077ACBB9CBA0744E462D02F406ACB24E9A3C5CC5048337390E4EAA295F6831161CC7FED8938EC6287C8C08DCB0AAE66340C4A380B5B816B8110F3D190EB97C8457DCCB410739C48B8132A32C0CF59028962A8BD1B03F0229C908BE9D22C9AE65880E110977508595F6754776B4203AB1633870C4EACF7B3689A8D43F4C213FA839B7C0281D242452455A31F396BBBDC41D9C4C5F99960ECA0537DCDA3FC6405B139241EFFB20CFA05A739A43EBD39E2A516A6D136F78F43CC5E24C2335CBBBD7335DB820A235129F10C62D13C5013732214012B00C1383E670D5D37484850D2EA49E9E835A750C63BFCB2357E472A4406C6DD2990349987707CAAA1030D4206460848F3C988179E31A4334AEE89511AE4297A668A154270D963C7EB930CD3C35290C9585F428690F88721443757D793D9DF912680C21923138D119B777530A3FBB406E110F408199CFAE822AF8B67807B0181714C1EB366D2750DBA3CEE603\",\n          \"dk\": \"7A8721EBD06F9A1845A2089AA7437CBCDB4B86C4529BDB0043B7BDC5CB95639CA32989550F64296482C8CFC15590444D8CF5C699348C0541AC0224B94435B972BA98832A7B6C86600E8C4AD9DCB0409CC278F01A6748005E7BC06AD00BE8D731E4182F16563234960634683956B58217D1B69848C934840F02C49398F0BBB5EBCDD422C54249C7195066F9F900C0C982A9048F4CF3800A10CDD1858269E59C307B06DD050F5A58C6230A44D8A9979D491A289A4B3C88CD7AA9307FD93DA99910B9F3A97FA759BD25C6D9F3A41677B8DE41CA23D3089C43B584C11C8031205BFCC8A813BE1D5169714C84BFD499A019030514072E59A50E5B7BFBD8A03DAB360E16AB02FB270B434F274A77ED1A67F870C6461A5C641618254C8F2061BFDA921B1E906626CC956758685190017487128D491C07A33E3C023972488D9F504BBF27BBF02CC85D5B8F98E61C59F27B7C879F4CB341FEC1A7B7A13E7FC496B3A511AF593312B17C9691369C10B7E440A93292BB25B501A3522E4ECC1DCE5A8255092F87248C6DF39D9027C5ABA21E10F003F88BC2ABE25B5533C2F390C4C500927DAC631307C6BF17CF511C8927FB75BA1B44499900FE0545F20565FC139BE9A5221D99C656643FCD38CA16756D1D81ABE54971961A60D47C4365600EF43BC97862A41AAC136A396D3E549ED7469B1CF0AFB4D107E9C685AA8BB26FAACBD070B4BC9C1FF502BBF64350902972A75810EF04670EC9A9CB99830FB88C150AADA8C457BD338894A18ED751AE584758E16660C221B7FF538347C05EA7987FA92A96BAC91FCBF0CAE004107C8472E0118D78F0166971CA14E1BDCDD229FB631FBB81CA8D4A3282064E9FD3374D674D3D212DE71B0FE8765E05F66C251BAD690C300858A220A620205239907274B754AF091701E3EC2ACE6B7213190F69187164C8606355CE0C1B3D7716CB7BE1C335B506F2646EF18A092F6B6328A656A66A5F6D534962F5ADA6E335D7F37EB61776A8D26326B639B48C95099C3F801A7F3E511CA1E45400DD1735444055624F1235A21757349502372C4B6C01C96D96D9220D0638C2163CA7765DDEB58F80BB5F42442D20972DC94131EA83B6CB687E6664B66A3736AA29828F40949B4636A4A505B2F7B2EB9626B491BC96B16664B545127957B846626DE12506CA449D13A657EA9D8D495D0F8B1C0CE367591C1186172CD6E380828B1E5C4B0224D0298D872E0BAC79524707F7014369D520AF821BC1F706BFAC6C692C0CBDD9B96B8292EDB27EB7C43530F8A3ED848513946EA1D0BA5FA00784D0A85A528800B1C463440080750C7AC6043B0529F73B29B40358F457A68E164855E7626E964CF5DC2011DAC25DE82EF72A3503280F3F23A058783625E68D9480ADDBFA4FFDE76F5AE87ACC209C43C65F11F652954A658C86C9FEE684BB1B7BF73211EA002A25E1BED40750D41CB3665291843B0DB542BBAD481D6B3B021DD8635AE93D60D07807A95753C79DE35829DE5C88DF095301C433130A511210792624A8556379B0F35D9E3A2E583B24B1BC587D038C5C59BE09CABEE6597BB9CA49AD022E35C7319CE928140A026D05B9761AC129524332151DE9E65E4E9A0289032E5EBB7A47A82AAE9BB2ACE6AD16162336BC8507C0C24AC0C4A2D076D929A67AF8959EE472E14CA87BD0B89076AE9B4C3F95E3BCBE8815AA7A849F6A385A8B0756917A3EBB44122989A49A33517657D67021D2F6BE9D64A1ABE9B8C590A9EF81043D19C556B97DFDFC03D7A4173405C8BD38235EC07BA7C80BD97B4566BB89C227855673B63A09925028C24834ADAD1C26B7D804ABA3110326B5273B8430045B2D317B1770BB3FE592BBD32F55244601F58583580431C35BCC955C9A5656A3431B943A7E03D5B76967749C886458348FCF9CA7487A66F9B0AB3D92462ED192D19B7D8B58532B09642F490940803EE97576093C5C32D1A4770B86E7F58530776B5BA1051F1B1F35309ACDF97319846789292DB784259185B80C3B0288D103CC1C2E4E23B97E9B140E18B34AE8A4484647B66430996844E73B0A0F79A6F5179234E22C8DA676A379AB8EDA542DD1921B481E866422BD0822C2FC33391293EB31B31D7A9574E31D7E070FAF298E46BA8CEA3001FA0210A6890A6C68B73DFA519B66A23ED78D4F4504922C38F9871EEABAB438D03555F272E9F9493F005F5F8CAF8A620F362A1D1B90A66289111D03929118CAF31B0AD9ECBB203017A02711BAD26B02310379C117F6F58257F6C6ED5A14BB32760D5A9EA062A9C106064D703BC9E378DF498B347075A7F6515787C500472BFACA0729A4590D39C291DCAB327C10BDCC6B1837883BC90B379B3C32F56C30F78C2A347EE0452FFE4878E2280608265931CC7E123768B0F93EB0081D642B4DC756210952541BB3BE08352CB983911F3B944671B759A16FB28C3883020892865E903045C84BA1E5E54159604AC058056C6153F738745140CB8B5ABE3318809651790EDC20CA5380CC76568DA5915C5AB8272748B42446EFAB58852C619332A08F537C28E93EC676345E0A2666708CB5D5281F981D3B8A19180565704808963B4015DC9B8C06905DA43680402E6B5888F2EC7448AA6578787EBF42329D060998E74B09C13D1C432A17F56D18BC624F02AC7ECB6235C42A24B42639FC490D07A635360932C71E25720EF59340F21836B74AB8F31C2CAC035AEFEA846210AD665A5D64616861F1CA3B963A2C10C662F7AC66298751AB3CE6A64C59770568B4B234129550702375D722AE4BBA5FD0899C84A834E7B7938627C9285912D346099423BA37B25364B67258A121650E895660C8E014A5B08D38B7AEC29175B424C234D2B87C791B60159AE753CB61D240DDD2814F1A7EB69A79AC37634850CBFBE2CCE6E918D1CCC70E13707732A6AB11329A3336D8E221531335BED560A468092CF690D1B8791F58993BEA001186AF5E48A2BE0B3D17656A3ED98BC3628C9F863A2C88C90AF25A11AB41FE7CADBB3C1013B59A115AB83F9003BE28759A3148706B891D9770961AC53D4C8B6CE5788ECA3F4A4A47D7F5165DC7B94B358591348BAD7B8CDA482B26F650DA1765A0911527260703E2CCC703A69F10331095BD3AB167DB52A3EC16B69F233862EA11AA9118FF539071A05066388A6DFA10BA4819D31A03B3980677CC6B888263D74007057C6C3C272464497A58681816C3C59E05064D147F62D5C848B8919C97C3BAC20A43CB58F8CA98EAB48B221851F634459B7AC514161A1C490B8B576A0FB2497A65006EA78F9ED50763A19F8D2C99D546642A68B6C2A49FFF919687E67EF0ECBCE23266114982FDF15F572A9519EC12CB90BDB3516E85E01106B9BA456738F4206F5F0C314156307F4CADC8983A91F730219C322A1602714849CD1B7DF5D02C24758DA6DA8FC8600CA2C1CF7294B1F6598D9E844625C29F589006C0875F3FB33EB357B7AAF0607BB31743153B98D64A8FC56DE56714D71303E9F6A3ED228961E607D6D6C0D4776828B7574A8CA28D837D24470E56F3A6CB6A4A90F5A9BA8A3F60576857E1C7AA52A16E1A84F7421EDF46C312BC83EE0776A9819DFA8B66FF2BB907A96F4D6228FF34ABC68248C1990C2484CBBC70745B045C602C1552B952FE8B4D24D027E1C65B3704945E507D09A9A6AA7000C1607A27591F606AC1D2C5CB55BBA149A15C4B261894EC9A9AB9C3DE446C1832744CF6BBBF949D54498319504BABACB8DFB15F90F66DA9E0B7F8B7A42A7B09C5E19DF58CAD8641C2DFAC77412B49D17A0C02341C6FB78DF00809903835518392D0288907184F126A69BA77863C84C1813835926311EA705433298D77AACA4DFC30CA218DAE405732841605840625E57D7CC3AEFCBA637CF8077ACBB9CBA0744E462D02F406ACB24E9A3C5CC5048337390E4EAA295F6831161CC7FED8938EC6287C8C08DCB0AAE66340C4A380B5B816B8110F3D190EB97C8457DCCB410739C48B8132A32C0CF59028962A8BD1B03F0229C908BE9D22C9AE65880E110977508595F6754776B4203AB1633870C4EACF7B3689A8D43F4C213FA839B7C0281D242452455A31F396BBBDC41D9C4C5F99960ECA0537DCDA3FC6405B139241EFFB20CFA05A739A43EBD39E2A516A6D136F78F43CC5E24C2335CBBBD7335DB820A235129F10C62D13C5013732214012B00C1383E670D5D37484850D2EA49E9E835A750C63BFCB2357E472A4406C6DD2990349987707CAAA1030D4206460848F3C988179E31A4334AEE89511AE4297A668A154270D963C7EB930CD3C35290C9585F428690F88721443757D793D9DF912680C21923138D119B777530A3FBB406E110F408199CFAE822AF8B67807B0181714C1EB366D2750DBA3CEE603BF822762AA356CDBD08EADB7D166690F4A00D797419ADCCB9133C3E5EB671B5654CBE9E686EFF218AD6583359070544146921F5107809454E73FC105FA7A9A0F\",\n          \"c\": \"566EE0837DD0AD41C30D9C318F736E6722D037E07BAE16234A2051509180D399518883004079AED8B2B6E18A2CD1E2056DB76EECF47C3E1268A5E662FA6D029F7EEFBEBE1587919346CF7D38C6DA819D7A3A89B2BE65D6E2F87A6F348E8F9C67F99B5ED655B5C0A6AFA15DA8CBB310B364552195C8F70B37F153270322E5E45B86F074EB3BFB3B03DAA7E81B474011F2F3DCEFC3CACB7E701B1AC7DEF0650362CDF5F6531E5E5FEEC973208124DD22BE3167F49BBB9F160AE159E692C007801E1FEA10034A20EC460F72FCC57C9C2E6CC749F5110AC6CD7A20B6CBAEECC6E6C5FA131F09B19EFDD175420B2762E4CCCC03906524AC63C6AC92B1995935A83B674299095DC4D2E26E3D31B8A4D71E9094A4D50F76DEED368F2DDABA358C306646AD0148408F8B8E6F5899F598CFDAF90C9CFB50A285150692EA3955EB4FD80BE445777C601EA5B59EE07E5B828BA576F6F300D674973D658A8D4B6967C3A3ECE68BAE27A46AFAED43D3392B985FC5BC7D79B0D56321316649FCFA84EA05F02EB8E3142D72C06D93FA73451CC0134B53AE1B038B4EEF71946665AFDD50CF3A33D188389DA9A32E7B46DEBA552C337DEC2B28CB3570DF15A229AB8D3FD86277FD5AB595A0ABA2DAFB7AA62F2CDAC997F13BCBC93A42D37CB83A52FAA8B01F8C97D196F1FD7A618566CE8593BE11DE0437D2F82476E65D522ECC3D8B1C247ED0EB7590648E1A51057F953F0932A567B799BD431344E1E0A6211BF07CCFB0B53DA9D39C59C4290ABA2930BC691F83830779C89F14FF6643D277035E5711333979D563DD1BD4F52295D45C98A60C5D59AEFFF4ED119B2D88C7D5C7E7EAE0F58207389547BA5FA995678A94D7127C407EA0BDA29CC8701B83B27447654B156461226BBE337FD69BF0FCC99013646751BC53D9070568E4D2C0DB6A06CA491BF80EB6C2C5807542CF6569BA84B88EAE67FCBE2DEABB56A5D06E945F8389EB68ADCDBDF0CD9018ED713BE071A0A87415647A97DBB6665C09C5F27899BED8837CBCE0010C70D7D04419240EDAA185EE7AB14A1C55AC6546ECA6781804997AB2B15CCFC9035ED7F170CEDBA0E280195E7B2C2C33CBD5BFD10CA8A2E82D977762BA6AE5476767F7E9FE787FBC624A81D1467C26CF2C1F1B1BAE476522DD198FB9FD5130EC41DA3683B788D07DBA2FFA0D460B66E5967D161AF00E61388BF317897E30B35BF8EF1A580DF5071471808E764E01043982082DD2BAC13DCD049B1DC66D44A670EF6E063B31AE29F391BBBA0ED0ADBDE06B0E5403B68BD1A4D997EF3F965E591D8CE8A0843D64EA4554FAB3DC9D5A96540DC2ADA49504F25213CC4EAD1E097E0CC516A4E3DE6F272D8DFF93604C39551E8F848E349315F46011AADDAD6F0BA68F1D6CCC68AB6AFCDAA1D47FAFBC056A063641DE73414C26D997F243DDA0817AEB734BAFD35C59F86AC30D45D1E5A0FBE63AED51B02E6C7851A858D5E0C23E53DE1DD0413D408F665D62F24FF2F314A283B7E3848F1E17AF5000BB9D279C0647C1434F783A1A21BC7A8349D62AAEE19FF06677F81C954F9D6A69ACE7BE06518ED800923C6ACB36842CE65EC81749C388AA92164D28FD34D75637F23C49DBD74353181655EC029593BCA3214C73A540CD9C3F6B1056A2137D0C280BDC90C13E973555D8C4D64531CB7360F1E67C1CACBE9F2D59AFD3BB5962C4DEA4FD9341D87E0DF55D344F4CF0A30CACB8CFB10D4B6B07AAA6B28CAEBFBEF4F0B4CF6EAD9387392A27DDE2C4BED1F9908DFCB9E1383864AF6B9271F0C265D2651919C66BC5D3850CA741A7705D0B4F7D2FB82DBD376079AFDA8523756200E6E400BED88992520429215C3702C97721D76F0C9EEAD7DFD2ED0D604D6BFED6D942DF0AC48ECE1BD16FB35A301165816B0B4DDD881255FCA8C2EBD6A4C1ED56CA83CD868AA7EFB0199709DAEC10171B870D3A9808809FF0670BC96B66CB45EE5D0198146C0AF1CD920B1E315879E8D24ECD8DC03F3E8C43A973ECFD91344F2E9520DE4922544592C26F085D53A7AE03A866C369366A4CD78EC059E871C90996CB4463F21C7F0A9914E38ED324743570818FEF155915F2E1087744DCCC6B6DE8991371DEAA4DACC534974B1838BF8A98066F20B07B809936A36B6D4C4CBEA220B16241FC875DA5D5573FF74E3ABBD2D110FB2B288136857CF5F0AF36E226EB792BEF4CB79A6A8741FFE20E\",\n          \"k\": \"27CEEFAB9BC1F2050BE2874B3A81CF1148567A1E73A5DFE89C640C0C88F35580\",\n          \"m\": \"F2C864FFBDC366EB96BC5F5FDE0D4B3348A07E861D9EBA90E70896F7FFCBD55E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 63,\n          \"deferred\": false,\n          \"ek\": \"44DB8BD3C4B5FE45CD408B054B878343261AB8F812BC75A953C8617582B10D789F7114A00E0166EBEBA0BA149370639BCF020197F82080BCBB9BDB49D15493114698079B7021C55CC275612D843FBFEB0F2B86CB659C6EED1CC5661B711621320A85B307C96F66C4C35C3081410045AF4B898C2C14B645AF9E0C9CFC03BE42DA4DB3A759E77B7D6894BF380991C3D4CB0D5B2CFFAAA08DF0085B8555228CA8E18672880C4F96E555F879C2CD6740960B94B6A60BE7557D2E230083D757DF76506311BEBF28C5496C00F338594AF41AA15566B24B004E60956AD09CCDB034B7C35BC5D550F52828007C1F85503AD86A9C811C440E6600B16CCD51EC098AE298042C128494A203D20BDB4789C4314C8D14543C9553954C9FC34C40912862500B2F3A5929471382D3531FC9767CAE6937D6E37D6AE18FE5B858BA968927201912782C6D436851C80E7AB0782FC5AC1D9C2F8CAC0D6C29148E220EB30909B2A43A4322B90E741C90C19AD52CBC1527CCC883209952A5A7C68E3E580BD987338BDC75FA84CDCAC3A0990246A7050E1022AF89C7165E45B32ED995A8C4B7FB28999E4CC2C79250DAB67119AC2C6B5B195526439E17A913F3BE3BE896E8023AE6A78AC00244B8EB2891CB5634955F908424C4B77BFDDA017D3B71909320CC53C4D4196E0C39701F8839711BCAE59BAB4B4908F5121B931777E17B331AFA5E955B5CD5840A734A41E35CC016C01407B02039B5064E97C24F4B9197091DE6AB22D0FC0459033EF7816D12596CCEDA120F534CE74571A4E668F5C1488C1503BC0354161BB651F673DA9019C9A83D9D97381E72CCBB1628F8F9A42039CFA07B1D67A1AC37407319360883539E7E060D32469CB9D87DE784439D2B453B77C158045F4BE5A96F090603963A18574FC027C0A0597D6A30B9235ACF5FD49619F47F7E8833D5D719B9F95825571B32C17B0B4AAE2DDB7EC0B14413E73EFA105EA308AAAAC76C2A257DD4A36407468802703787C4AF27D21EDD08108534AE994A1805AB5E6235B88AE7C6997374E50C1077F7702F809B092368F43A9CB2AB539FA104F5B23A4E69B9D46C4452208E3DCA58CC1045955784C82099EFB01ADB86CFEEFBA1EF3A43F16B7CDAE28A3E74C24B6648B8F20AB88088974B75BCC79E692C57501783F57434B7C2BCDE20B2E7129857EC9FC95452BACC9D96D89CB892C78CA07E790906A6FAB62CE170B63443CBC9BAD6F48B383050FB788270392F396C8CB8A73D4912115C41869D54AE1AE6173A34B6C3C91A5F6C39DA659268456E0B51389D7B05D4367C9F5C6CBD371E84EA9BEEB4C09DF9205296B9E947BD4E086A97760CCCDBA94A2108D02CC6544028E05A2FED297F32491042241EFC36A24F634DF3626A217ABA75882A050B7569318E8E00AE8A267EC0207CE9E8A524E1CE8F8CA117B0C77A680231F773B756152878363A9C38D93423C0378A60C8AAF8063BBD1484A3F37D055215A4403AB3C80DC9223D91165BC2D077C71A49093548D4A32922633258844193759D4DF26E1CE367C3229FECA492A9E7669ED4158CC08D9FF0C24027AE9759A517DACA6CD58090D5673C753101987B8914BD8B71C552D96DD093257A9313CBB757AE42860790AC70A593ED65A492078B6043B7B2AC4CC7D85A7145C6EE210C26D60906A5571AAC3EE71333155055381281A584A66C090252A3CA70D9AF09063F3DCC9BBF020715126122602AC9F9930D59669647CE64F356DA97A5C1740BB283781C2A10CB161BAB6B709CF9AC7C7911A23A9F4D6507537C497F3BA5CFE52AE9E34B1D9922669520DA2662BAA0282B33C5029145CC34B0FA9CCF201C6491E12646DB1D1548C0FBF6C7925C9670F4ABC84AB289958A26A9310ABA90CF8C41B39B7DC96673724CBF4BCC7754A71E3158B35214A8706652E11621635C5B6AEBAF64365497518D5D1B6635178E318569D23C50C3683CEEF593498037457C4494F03C7E92C055BABEB96C70EBBA20658787C1B27D6CBAB485741712F9169E3265E376A0241A36C0B2303F8865E6AA9D89213CA1F874E758A569E4055D70BB1B512C0845B933E470B1D9730B5024F520C8F2221B9D49836405525EC3864065BCA04A6AFDA11CC59A9BC28479D9D0B42B1B733E186D50B603CEDA34E8D283AB0939005B06815FD3531BAD6AA926F931C478F71A699A17741447FAAF6CF360D4C64098E9F1\",\n          \"dk\": \"1149A69B309A5FEA7002DA1B0D4B4A45A37D863841D68452692A96223C203CC24B05285915D48F00091A033328ED12167A4BA398A9BBC5063A0A28802CDB3D8B108C359601AEDC31AB8C8729E81DF9828ED5900715264EFDD3B0C77176D6D0ADE200685A239CFF946EAD338351A54A82921E9CFC6BCC60813F2873BB50C1796276501740FE2554E130A01DE941D8CA3A0B37318C467D893491718BA84A26A4229A06476986C668843198C12F034178064D4A4B7A889C921C49028D6B9272497A4C0C6E03F78C6433538484C3B5D662058BA9C9E9B474E636A4E20CE194BA87F98B7EE4A26D4A414A4C4D97A3A1F420885098871D2036D6E9A2FFAB978E217996E64CAFC34E30F453D2E1CF20AB6747D1C0931A23147052F2A30E1EA07927A28733799F88FC8199896EEBD324845119D03838F17286F32A7F1EE89408606E66C72A546333E3E714DE56467DF46AA6C10CED9626C999506AB60DD5226259762D6A028E2B4798DCD77217073907962DAF17BC6E54B11A8618F97104602250DFE40409E615226077BE778D8791838CB1B11A925AFF9622BD4328539CC015BC321E9CAEE3699B8CF9C075376E6F2C50C495821DB1903C2BC04DAB2BE90913CF845C368BABEAAAA79E998D1F066ACB67CBBA673FD3C1014CB743060B1DB2A93593C316BD982FE9FC24049221B9BBAD0EAAA47561AD4B81C81B46C69FD3847C8559E77A368C57520329A06CE7542C4771039072500AA324B723FC96605BA225654008EE7BB68AEA14843B18D3977B050914DCF2BC77A91EA7C4B46ED04CBAF47B2BF04129B05644EA360F3B69C2867FF7F5B710A4947AB29BC5882BE2169E5D7442D659CFB46BABA459869FD291B9D505181A306394AB9643810FAA68E5534351D3744E4CA3053AAAFBDA7523A33996BC344D91BBF8B7C41B1998C2623F261A94DC123244AC9DAAF49902D407882B067D12C1A835A5AC546BF99C2264B525F9628EEF5BBE78458A103236AB8B37D67A5EAE21369BE740D4D3B567242517404A53A049C3076C310560AFE9AE495557699B28B2DA72B91720A1524CC265067BD3227381741E0410F19C960E0B8AF48CACCCD70002403884A95E578B04864A81BC1737373135E9A2573B51CA9CD4C11A8248C3122C5192A3ACF167FA1A21879C09A7C21754980485A3272D8985A0F52A4CF77813AAC828231B06F4BAEC5B15D55638F889679653458620C54B45BE8C67C6D2FCAE05368741B26E9F6646D0142298079740A61A0743A10412BF2C4469D60C6B08966F1180C4B3C83E62D3B0E4D13AA29CB765D67A97524D1AC75A0212928766CA29114B906A93F42CC0CF1B2B6A6C2948F80E9A5954120660BBB2B1B42297B8C53D9BF349A86327249779C4C41442655D321A55F12A589CC62F18147599C6059D08B4365A3DE541C845FA37061C13338A8A1475BA67C12F0C67A3430B7CC53A46875241D3359A64F104910B73763197C5E1A88D5B1D05EA98B97524D4B9A59A7655276905D6B1662EC9C638AB0D8B746D93C9CFFAA59EAE762A54096B124344B0F4BC7CA6696D53385554B115C2707E203C6D161D14B31025F336C08B17F907442E369A54F404CC6B0DF3C62ECFC805C8D967A29A1B17030EA26C45AB71684F7A6065A8715AE7A37D6C5E3AE25A862B3185FA53C0424A3892197A339A6EA435264798948462F8846DE6A04C77C951F3C5C9262B16D3DC8E7D7B3ADFE75EE3CC84706BBE9B830DC0972C15E404B147C4FAC09BF089B057E5850153978C8C625513ADF872A30FD7B1D9F69B97AC123E11A611C6A416B4410483894BC8AC738B3E0A9C0D7F611D88611A5C62CAF879307A20732F17B26CC52531B447894193AD45497D64B8562193F670C8B8AB1F74DA79A945B16E01AD18006AC222345C20CA5B2099E9818E0DD155E14B64215C88E0A72A57C6574C692AFD1A382EB832F6033E8B78AD68BB60CB218F58908B92533C635A4D41723DB231BC2CBB5B7AC03D718A96D3D80120C79AE43493A9404E3BC46391721F68F80FEB7C47CA4A79BE9618480344D6790402799AA5E155F2E229C45C970E772D517995493661A04A2CB8FB90DFB056044616669096ED51C8E2F131B5FC729DFBC876A412024C692899540334071DA2911EDAA7D1C49FF6C96D4C4C2A51199B44DB8BD3C4B5FE45CD408B054B878343261AB8F812BC75A953C8617582B10D789F7114A00E0166EBEBA0BA149370639BCF020197F82080BCBB9BDB49D15493114698079B7021C55CC275612D843FBFEB0F2B86CB659C6EED1CC5661B711621320A85B307C96F66C4C35C3081410045AF4B898C2C14B645AF9E0C9CFC03BE42DA4DB3A759E77B7D6894BF380991C3D4CB0D5B2CFFAAA08DF0085B8555228CA8E18672880C4F96E555F879C2CD6740960B94B6A60BE7557D2E230083D757DF76506311BEBF28C5496C00F338594AF41AA15566B24B004E60956AD09CCDB034B7C35BC5D550F52828007C1F85503AD86A9C811C440E6600B16CCD51EC098AE298042C128494A203D20BDB4789C4314C8D14543C9553954C9FC34C40912862500B2F3A5929471382D3531FC9767CAE6937D6E37D6AE18FE5B858BA968927201912782C6D436851C80E7AB0782FC5AC1D9C2F8CAC0D6C29148E220EB30909B2A43A4322B90E741C90C19AD52CBC1527CCC883209952A5A7C68E3E580BD987338BDC75FA84CDCAC3A0990246A7050E1022AF89C7165E45B32ED995A8C4B7FB28999E4CC2C79250DAB67119AC2C6B5B195526439E17A913F3BE3BE896E8023AE6A78AC00244B8EB2891CB5634955F908424C4B77BFDDA017D3B71909320CC53C4D4196E0C39701F8839711BCAE59BAB4B4908F5121B931777E17B331AFA5E955B5CD5840A734A41E35CC016C01407B02039B5064E97C24F4B9197091DE6AB22D0FC0459033EF7816D12596CCEDA120F534CE74571A4E668F5C1488C1503BC0354161BB651F673DA9019C9A83D9D97381E72CCBB1628F8F9A42039CFA07B1D67A1AC37407319360883539E7E060D32469CB9D87DE784439D2B453B77C158045F4BE5A96F090603963A18574FC027C0A0597D6A30B9235ACF5FD49619F47F7E8833D5D719B9F95825571B32C17B0B4AAE2DDB7EC0B14413E73EFA105EA308AAAAC76C2A257DD4A36407468802703787C4AF27D21EDD08108534AE994A1805AB5E6235B88AE7C6997374E50C1077F7702F809B092368F43A9CB2AB539FA104F5B23A4E69B9D46C4452208E3DCA58CC1045955784C82099EFB01ADB86CFEEFBA1EF3A43F16B7CDAE28A3E74C24B6648B8F20AB88088974B75BCC79E692C57501783F57434B7C2BCDE20B2E7129857EC9FC95452BACC9D96D89CB892C78CA07E790906A6FAB62CE170B63443CBC9BAD6F48B383050FB788270392F396C8CB8A73D4912115C41869D54AE1AE6173A34B6C3C91A5F6C39DA659268456E0B51389D7B05D4367C9F5C6CBD371E84EA9BEEB4C09DF9205296B9E947BD4E086A97760CCCDBA94A2108D02CC6544028E05A2FED297F32491042241EFC36A24F634DF3626A217ABA75882A050B7569318E8E00AE8A267EC0207CE9E8A524E1CE8F8CA117B0C77A680231F773B756152878363A9C38D93423C0378A60C8AAF8063BBD1484A3F37D055215A4403AB3C80DC9223D91165BC2D077C71A49093548D4A32922633258844193759D4DF26E1CE367C3229FECA492A9E7669ED4158CC08D9FF0C24027AE9759A517DACA6CD58090D5673C753101987B8914BD8B71C552D96DD093257A9313CBB757AE42860790AC70A593ED65A492078B6043B7B2AC4CC7D85A7145C6EE210C26D60906A5571AAC3EE71333155055381281A584A66C090252A3CA70D9AF09063F3DCC9BBF020715126122602AC9F9930D59669647CE64F356DA97A5C1740BB283781C2A10CB161BAB6B709CF9AC7C7911A23A9F4D6507537C497F3BA5CFE52AE9E34B1D9922669520DA2662BAA0282B33C5029145CC34B0FA9CCF201C6491E12646DB1D1548C0FBF6C7925C9670F4ABC84AB289958A26A9310ABA90CF8C41B39B7DC96673724CBF4BCC7754A71E3158B35214A8706652E11621635C5B6AEBAF64365497518D5D1B6635178E318569D23C50C3683CEEF593498037457C4494F03C7E92C055BABEB96C70EBBA20658787C1B27D6CBAB485741712F9169E3265E376A0241A36C0B2303F8865E6AA9D89213CA1F874E758A569E4055D70BB1B512C0845B933E470B1D9730B5024F520C8F2221B9D49836405525EC3864065BCA04A6AFDA11CC59A9BC28479D9D0B42B1B733E186D50B603CEDA34E8D283AB0939005B06815FD3531BAD6AA926F931C478F71A699A17741447FAAF6CF360D4C64098E9F1E5AEC6C6BE5AE6CFBB5E8B66EAFEFC8DDDDDA717B32220C994BE42776B296D9F7814DC0CDB963F257E983581EFD3A55A7B58E09734B10FB5A6F1CE03B12D16D4\",\n          \"c\": \"E4BE80FBAA47E08AB72D52DDAB90B35FF1F4C1DA793739388E49A548C6A1EE07770C6FD8153A3984AC2800150B20E2347DCE0A06D2C83D2A203DFF7788C969C969616FC1BE122067614989F34D0D84F9CEA1767D0D9D83DF8C573CF4A3EEAA6A0147731A373768DD38505BAA12C18B524FB2682BDEF71FEFFFCCAF0A8CB4F42A3A1E048DF6A66CF898A171FAFC840B46A8994F7D9A00CA42CFC2539FB3404472A39EF65B0AA7A376A421FB55B619E65C295A51047EC80334A7B40F3925FEAE500350D71139F6DF4C7EA9655ED2C869A7DE115FF7E926DB881E3372D47079FF3F48A944DBA7F70B0AE01CE961F16CBADD7C57A94EBCEEBD709B414F7DA764FCDA39FF044FB0EDC16BDE8C68AA7DBE92C1AE3C226EFDB7C5F1746BB56FAA7ED34B2A32C7A95A05EEB7E75DF4F7BEC3EB78B74A058FB95C20B33EB9E30ACD8340B685CCA66F2D1F6737646F67B28CD62FBEDF708A4277A8B6E82F012895438D14A3807C087BACAA432ED6A099470E28E4B06B64CF6B249E4AE72DB468948E874565ABA879FC3322A1A89881C55628C37781D28B39102E97E74F0921932434ECB061E6C388611217D29E16A0DDAEFDA0B420DCB83D5FEC1552025A98C4D6F19D6C1E23E934842D07856CDF0E5A9E8CE20F68C8425C8D54F7216B6B66BC3E1ABCC6DD5841EE0E1CC5C7BDC5A7F729A9930CD4BC946A33D6534ADB2BE0DB65490E58075FE3A8CFC273AFDE116A6A4C317177613DC93AED69D85ECEA4C55E43DA1D08780B30D7FAE75BD1000043A3B56E58ADE657679A54E3828A97CBF7436601408E5C00936D4BD3A4137E75AEFE338C6843EF3626FAF2684C6AB8C3F3A04773C913DDC72DEDEE9D45E3F0E37A3D8D5AD2D3DC9CA90B0CEB666C646F265E5A75D3D6F7E2455E11B5866C9D620FB2EF4D9CE86D0106DC84B35D603985B2562B3FA0BF6868313907B852F4B23E2AF6840C808EF8AD8A7B51456CE3CAAB34CF68CCED6A81EC1F9AC7E090AA1F854E275169C888499407C52CD6A7E1A7D572E31BBE6365056686E53A430015E330E89CEC44BA98A1DD6872FA3D4D71B19C65903DCEA30932C2FB94CF8233F26D50EDF3213A9574874B0AEBB1B6AF0807F352C40E2945A134EE7DF88439ED578AD4E2D7EA11D0C9C6CCDB83A03B6B9D026DEF328D2B3DEE0335C8092C46381E65B159E7478C615D14C2E800126ECF70455C3B4DA925F36C186DE2AE1B22D814F974D5BFB988F7D09140AB99A662382EF56370373DEA434AC42D1664EF116E92A90442518FD84952C7F35D0F42859BE3427C4F28741335485258756B00E2A90E7CCAF6B564326D25D76CE8A6FEEBAFCAE375135C1CCCCF12D232A0B062FA46F166AE2D99A36DCA64BF2F55B0151F64028700F831D26E62C41DF8DF1A46A8697C5A2F8EDB75C910CA1D382BEBFD3A4521E174F4FB3A258AF666501D00ECA819D310257E7AF85F4087AD1501EC4E18D1661EAF75F50BCF8DC80968FEF78770065699E8A857C12E507D88626AF509A2331CC228E1A2BB3526B687E63EC763EEFB375FE751EECFEA143BACD4455E8F6E1ABF0F82E4D41C5CB770BDFC3F78150D584DBAE744BE4C20199984445F435BDB46454D662F41C61A848A7C887C1A04D41D4B92EFE657ECDB9387E84495EC37CD183F9C2EB5E859D722A614F3EEBFD13FFE2491FECDB4DD2C06914EED59F8211908516C799AD7B9B46C5EE5AFA808B67B1E36F81D9DD3C9B27188BFD40495BAAC44DAB36AC61ECDE47CDFDA7AFD9C40952AA477818A38E3060613D879E78874254C599697ACADD42F1049A3E5BCAE75F88F7771E294EFC9D3899DCA955263671F5205953D62378A310FAB336EAAF4837CE6DAAEF1C4A141F6192E934A20AE23BCF803215B5A96D6CA99EE65A205EFAF39082A42193C5090783B426B35A1C8BA6A6FD00D3341ED24008E1D70946E22126F7CBD71A49AB15D2561FFB4DDE90A497A89049B22BE50905B63107BBBD13E5AEF39FB761091C7384519455057CC407FFAD746145BFFEE33E120922E06FAAD8C5349B46B3133B4EBF1AD9C84E9ADF35B5DFCA3141353A1766C04907A6CE0C3E9C6D84EF9E732637AD033829B13E0B9526A1C8BDF4296AB460B32CF29845888277C479565EBAA7C30811D2DA71BED5560A5688DC18643818FAE07D531DE7196B947CD1D94F4447B77CB48F82DAC0C7404E302F0445B656475DC65608B\",\n          \"k\": \"0CA16C93880B3BF4802D0EF7F03E5C192440CC4B399E9A55637F1E6AE6DB225E\",\n          \"m\": \"EF29D988D373C381541AC8723EB67C68CEDFB9DEC0FF2B40CDC763378B380C12\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 64,\n          \"deferred\": false,\n          \"ek\": \"E2C18353FB0F024A0E05FC7F0F14B49A9A36CB49CE7EB963A2C20A5228B0B6ECA4AF63A1F7B1B8260B64D2F2C0718397F4F402A0946C9DE5B0F083B57903418C30BD7CC321574531B2163DDDF23A69B04B1D393C5916280DC11485FAC369D0AD1C85656D4A6A7BA480FA5ACC1788AC37C9706C26A357D7821D636078FC2204530C3D3358DA002F3136CF033443C519B6C505D00FE92B04A305A890267B238882EA9010554A1671862BE8097B67B8E8A37E8F0A5C96F94FBE296162AB1FA456785550AEE6B05A80554606B76714AA91C5E7B747A2005900077874CBD91970111C800C49CF05A4332570863929497AD6617D1B5614F2670390574F554C7D337417C792FF8953143638F60A7BF9517890D56D02039AD30392DEB045D8CC4994274E5CF83B702784637C6D53284F4F14C15335A86A642427E589AA454C6FB9707155BE23745FEAD04EC1CB07DA341F7FF1B6D17C315BC3282634A39C9104392B1D12222E8D091AAC69CD5A72276B01BE22FC3C634B41AB613DDC602EAAA00A72478D713C7A0F895880224CA8489E6860868A7B72CF876FDDB230C55C0523342C5F42B0A533A8FEB798D3104145E27D29CC96D54301D5B07274F700B9201555430D8EB961AA7C3307B0B56B479BCB6147D9035D786A58E7199F6BC9335918204A345826446A50801603EAC992E93FB104B834D7220B1494FAA2CB1A59650B00AAB00AA15F6909DFA969DED0B540648830360ED052661557280683084B93B746E27D916A05C96CB73964A4AE15CEBF4C84A1528506A69D2FC27A2A16CF270A09654A8B566651D097C61079A859171BDFD35262116479742DF74C0845D596FB83984F27811FF81CEEE0C37D618B93321A7DF0C0D22124CF521D92715EA7DBA1DD6112E9152CE2E15FED4769805A5857896EDAE13854648AC9DC26B770109372929FA511B573685D7951ACF21BDEBBC97D384D454B5625059A7EB8034FB4533F55B897B77930F3911F2808F54944CFB96BC0CC5AEDB8960A9C024984CDE756441C6381B3E38909DB3E85A36408C5414B471A68093B188134B3F54C751061093C4DEEF54891039AD71A74134806C7F102CDE67432D37DDDB4ABF33987A2AA47634C80260B5A246758102C864B2B40613C42425313855897FCC835C7C587B007C94760AF26A1531CEC874B9C492F4AA4C97567D6A61C21EB00FC52B1E0F24E9D680F8FE38B82243FD0B562C0DC86E2024593562B4AF4A682ACBE720813859B92A9C20DEEE4B4FFA1A1B17A24C394455077450B4C2CCE67A0E5C4016C746FF508A1DF89A5BE2A2A7B866ACA0A72A603843949909CD16DA542068604A380741C32E986366749CDD461A4362401CB8243613A2C442983B52C681A3CFC362FEDB337910668C5732906C41637778BFA619D502640545715F709C31B506534C0216790779EFC053B4A7789662B989A75C6580754A92588A30617E960C714171C8310A2802092261A85A5C882D407AE263A119B3E68E3BE158BB0325C30026BAB21D30B09637A54FA0A4B980DF807048F139223D33CCF206FC68672113A55E2084B6700B396F68873636DD8E5479BC0CA3E47B52329BD7F20681D5C2208AC0DE2123D50F6023A4472B0300CB3F8865BF7B49374AF01B002C4087BDAF4CAAE4336AA3A78E3856DC408B04E9AAAE77C464351C97938CAE829171BF724A8779C7FE20E5FC254D0BC283B773C2DFC83D28C3E854140A81267C2D2C67F361DFAF623D817B1655CA8A626A8A91BB088F539576C8795CC8A53044FF547B328377F4D17273AC323E6481CBFA058CCC456741057F1F668CAC51F34D666AEDCC176382037C674B4793104A4BF6B336E3D59A1C43958FD827493CB956C046025737B7FC0A3C2098BAB916E397191BEF5CC13F3BA0FD98F7A25BE0CCB113EE081B9AC808E2C728801C70CFBB2BD9A34469C064EA07952BA0D579380F7E782D0D5C6E1C26F3E17619CD47D4BA88C61600721BC2AB3C50F591988CCD77AFD3C43655272565401AD0778671939636C8BC24C794FB73EFFB99232503A0B50030A672CC8C2C0FDD5CD43B131DF0C9B9F867039E2A2B41B3CFB1A82C983AC54E002388692AD885C676AC2D7D0C5645B06A2684E9463AAAFEBC6C07709DC68078F7BCA36F5584579C3E3261D319A58392518ED507ED54ABFCA95CCDDB6C74949DCA48D01DBE3525A0BD91AC78428D5A930A5\",\n          \"dk\": \"A5AB9B1E9973958172E182A9133A3DAB22806CDA0B88E72BAF581183973220061D836405C702A8494422B43568291CC432443543270DE0B7C85C56AAB20C0A17617C13B9903458C20C423A65092533AC4BDE6609E90711BC031A953C3A3139960F14CB17F8B3C3C68BBE6B84271AC5F003898A79912DB2B68A1C05AC204626C0275551BA6BE211D6962E7147863B44CF9CCB6D6836007FE44AC028B43EC9AB65B792B61159AE35477FFA5D783281BE62CB707496F1AAB956F364ED1B8971F35B6F8179FD77C30980A163A51836B06F44311C3E7062172B80391AC5B20409CF16BDAAB4047DBB56255C325580976CD60113C72A194772149C6C2F342507016FF6B919613260902B3DAC2673D4584C9F87BD093AAD02950B8D750C1304A749F703D0365433065476B0CED26B3DF644B5F34B89A055C707EC6D059620F12962C74A0B78FC7CE12311B1F94504558DCB10BB27B198E2D80E1F8378C1462133209AA1A99BE03A978726AA4F88152C05859F667C708A9430C6C1FED06784E72D0F30198749AE8BA86172263175FA6FF380CE7E325AC4A6BF3201881DC16705A530EDABB7E0A28383D16E6F5BB0A87164917A6E942A9768B049FE37931D0192C7B5928FE14846B2686307A77757AB142996B049ACFF85AD5F058526510AF0D72E67DBBB17179A91B3847929145AF84FC355A1F454A82963A52CE6CAAB7BBAF7B22044391AD76187F37ABFF9210905495292F289A45077EC567D0F5C077FD44DF2A568F682368D2046803880B85994C61637B4B19C32D217B3DC961D9858D88A6D8FF2620E9674D9F5C9D066B3346A765E8684CDE20C4ED300395B6C1AC200F28930668B426FBC509F810EFC1396B167953ECA9670D34594415B61293D849C4245A57F6E2481ED0A40AFC8B2C1F7476151AE95C68B26E6679C155239C922D0F89E10B9C58ABA7A56EC7890F97B31E05CE97C4691F27167929313531086D46041E3452F74390BF06A787416F58537F6395AD5397E59CBC118D6825F26C181BA514D0BB42AF862BA5670C592132D4473C66A13EBF58E63021887E96DAFB14FFC408149F55D8A68AFAB24AFD1482C27555818495BF1B88E0F072ECCB44A0145A880C51172C6A411940FAD3BC31CAB64D5A2160FD8903445A6361B9FBCE5B7CE74761E339BD6CC3517551C0D2357095CAB8B34656C182165979A7F01930B331D6AF6A5DA8B57BAF76ABE198C2492AF2365843D33C84D013E13D13ADA4B1B387B8CF4DB7445C002D689CD93017A7D3A03F1B224EF7075AB88616C5B2DC343C0D46504D292A1A51960189A5FB968C985D707902B54DC390C8EA9A8EEE19DA0B0436B1259268C7B87A26D48A978901B1C44C6C0646CBB0A079F999345C31147AA9B3A51451F11C766C5B3C0851A00BB78350D60AA6437228B84A9DDF6869310946B46C483A05D7C04060AC89402691CE07B1769B23062927B875B4506E208F939A02BFA0AFC954FD0B034FB5B268EFCA527B01712E12F83E8B7E52996A68B63EE865782593C0FDC222E4449BE0657567903D48827CE526BB4215A31B8147D475C06D629B2A39DA7D258123AA844CCBBEDC83872A96E426B4DCF546BF2455DD739B8D973BD5C43C1A48A2FC4EACC676C5062069F9AEBA459D3A8ED880C63FA6ED80C06CA906FDCA895F2D9CECAB617740891F2A20B14DAA6343840F0CB33AFD38850CA87B0813388339B582A8DB0721339DBA66C2B9C118CC1400047FB7634CE8A8063B20175D49BD4B22E87B3AF335535528CAEEFE31827F20FC75831113B3013492F3F1709C873C09666928559110A5947626A14093128292022EA021750828D8121477BB0150D07D00E715F08598520F2744BD849E7082DEF2384A742B1DE61B44073691D6A34519A541F7B265E9B7F500155931628FF194D29225BEFC366ACE60F3394ACA71C1E9A7C2021DC241B339244F0CA3685AF2CB16AA668692F485D31590DD3448352E3C7C892191DD81605EC9211330EB720922F5A096FF342BEDB2D02160B95686E8302B82408A5076324EEA620ED81AB3BD097E173AE25BA777BC03AD2459191B225CAB540F8C1197885AD7ADB324206652BC678FCA24A57263DE089ACE6E1794E4B49EC85735B91830513AA9808572818C7716A61A51B7C59408703F066B7466CE2C18353FB0F024A0E05FC7F0F14B49A9A36CB49CE7EB963A2C20A5228B0B6ECA4AF63A1F7B1B8260B64D2F2C0718397F4F402A0946C9DE5B0F083B57903418C30BD7CC321574531B2163DDDF23A69B04B1D393C5916280DC11485FAC369D0AD1C85656D4A6A7BA480FA5ACC1788AC37C9706C26A357D7821D636078FC2204530C3D3358DA002F3136CF033443C519B6C505D00FE92B04A305A890267B238882EA9010554A1671862BE8097B67B8E8A37E8F0A5C96F94FBE296162AB1FA456785550AEE6B05A80554606B76714AA91C5E7B747A2005900077874CBD91970111C800C49CF05A4332570863929497AD6617D1B5614F2670390574F554C7D337417C792FF8953143638F60A7BF9517890D56D02039AD30392DEB045D8CC4994274E5CF83B702784637C6D53284F4F14C15335A86A642427E589AA454C6FB9707155BE23745FEAD04EC1CB07DA341F7FF1B6D17C315BC3282634A39C9104392B1D12222E8D091AAC69CD5A72276B01BE22FC3C634B41AB613DDC602EAAA00A72478D713C7A0F895880224CA8489E6860868A7B72CF876FDDB230C55C0523342C5F42B0A533A8FEB798D3104145E27D29CC96D54301D5B07274F700B9201555430D8EB961AA7C3307B0B56B479BCB6147D9035D786A58E7199F6BC9335918204A345826446A50801603EAC992E93FB104B834D7220B1494FAA2CB1A59650B00AAB00AA15F6909DFA969DED0B540648830360ED052661557280683084B93B746E27D916A05C96CB73964A4AE15CEBF4C84A1528506A69D2FC27A2A16CF270A09654A8B566651D097C61079A859171BDFD35262116479742DF74C0845D596FB83984F27811FF81CEEE0C37D618B93321A7DF0C0D22124CF521D92715EA7DBA1DD6112E9152CE2E15FED4769805A5857896EDAE13854648AC9DC26B770109372929FA511B573685D7951ACF21BDEBBC97D384D454B5625059A7EB8034FB4533F55B897B77930F3911F2808F54944CFB96BC0CC5AEDB8960A9C024984CDE756441C6381B3E38909DB3E85A36408C5414B471A68093B188134B3F54C751061093C4DEEF54891039AD71A74134806C7F102CDE67432D37DDDB4ABF33987A2AA47634C80260B5A246758102C864B2B40613C42425313855897FCC835C7C587B007C94760AF26A1531CEC874B9C492F4AA4C97567D6A61C21EB00FC52B1E0F24E9D680F8FE38B82243FD0B562C0DC86E2024593562B4AF4A682ACBE720813859B92A9C20DEEE4B4FFA1A1B17A24C394455077450B4C2CCE67A0E5C4016C746FF508A1DF89A5BE2A2A7B866ACA0A72A603843949909CD16DA542068604A380741C32E986366749CDD461A4362401CB8243613A2C442983B52C681A3CFC362FEDB337910668C5732906C41637778BFA619D502640545715F709C31B506534C0216790779EFC053B4A7789662B989A75C6580754A92588A30617E960C714171C8310A2802092261A85A5C882D407AE263A119B3E68E3BE158BB0325C30026BAB21D30B09637A54FA0A4B980DF807048F139223D33CCF206FC68672113A55E2084B6700B396F68873636DD8E5479BC0CA3E47B52329BD7F20681D5C2208AC0DE2123D50F6023A4472B0300CB3F8865BF7B49374AF01B002C4087BDAF4CAAE4336AA3A78E3856DC408B04E9AAAE77C464351C97938CAE829171BF724A8779C7FE20E5FC254D0BC283B773C2DFC83D28C3E854140A81267C2D2C67F361DFAF623D817B1655CA8A626A8A91BB088F539576C8795CC8A53044FF547B328377F4D17273AC323E6481CBFA058CCC456741057F1F668CAC51F34D666AEDCC176382037C674B4793104A4BF6B336E3D59A1C43958FD827493CB956C046025737B7FC0A3C2098BAB916E397191BEF5CC13F3BA0FD98F7A25BE0CCB113EE081B9AC808E2C728801C70CFBB2BD9A34469C064EA07952BA0D579380F7E782D0D5C6E1C26F3E17619CD47D4BA88C61600721BC2AB3C50F591988CCD77AFD3C43655272565401AD0778671939636C8BC24C794FB73EFFB99232503A0B50030A672CC8C2C0FDD5CD43B131DF0C9B9F867039E2A2B41B3CFB1A82C983AC54E002388692AD885C676AC2D7D0C5645B06A2684E9463AAAFEBC6C07709DC68078F7BCA36F5584579C3E3261D319A58392518ED507ED54ABFCA95CCDDB6C74949DCA48D01DBE3525A0BD91AC78428D5A930A5BD1F755E964833390BB7E9BEE7B1C5B34D07CF3593530572F57DFFDE8C0A167CBF377142317C203D37A8BCA5289614884A22271B47D91E03EE6D366B24902271\",\n          \"c\": \"4995176407FA65D288DDBA1FE91F7D2ED8B686096D49FCB85655AE5BBB09001FA8B8167F20C31A62169C319F798E38BDFD580DD070FA31499931C580950E2023C739D0D9C13F96ABAD0DF1F1E8B718C78D228BE5855CDCB5EB3A0B63C6EF156640615A763CD211FB94F540379F1876DD0B8619FC2EF14CEDC6BB9265F5C01B2833EA4F726F68B9F8B3AFFD39E71925AEA4EBF66894DDD1C4761155E9663AB89ADE8A50E9EA6B8253DB7085B8002FFB6C409921D7295F37E9E0ECD45C7B204FFC45792238CCB54997D2CA0B7CC4F056FA2B783A384528721CE77A7AE5C6938FD1CAE8CE5E7BB57405C6D5DC0B9517D45C580B0BD91AF807EB1989BB713C2DE9D3C7A891E31FECEBB5244E89F7F9DD574159415A81C459BA845F36E39B7B5FEBAF565D8A79D1E9119C618258297FECEF53430933956BF4BDB3BD9F12DEEE9BD0691FBE24512C178F1086DD2063B46166A3B00679C8D26EA493F28490523DDE0AA711DA3BBE9E6F05F569808C97EAC0AB5C8C0CA1F1BD7172DB19D94625904C08EBB3AA704108D033BE20B6EC6520065E1FD328C01437B4373E8DDE6F86170563D1E17ADF963F9D82AD654702AF5FBD30B76727A75982DCDCA3B5398C82FA42E6225AFB16C6FB466E9D5CC040BEA834DCE7704F94E0296504DF1C63908AD8729FA8DF7C9DEEE23D1D5922AF0548BBBA27873CF73E2970976DD56CBD562AEBA199906F86847AB60FD9D25BDEAD0EC2DC3C7D251D2396EDBBEFB438A098C7665094C088D9BF57AD4327C61CB484E0E730D5E7E98926B11CD4519C729FDCEBF625D7C743996F3B2E8FC38C29433ECE14D427C08EA079AD0B47466A7533C480E06C48CBA27E180011BD10A1B1F7089A150D48FF59EAA9154046F954303F3FD9BBAB91C667F7367868DCE21471B2ED4F2200674E221F71BBE457A323EFF93E89C5DFA4E8EDF87C8DE741ADAB529587663EE1180D60BB42CD8BDC050AC41457C8FB931EDD71B68B0448D036679D5BCEF2C91EDDEE940BC8D3E412A363712413F6EE9697D932941281381F1FD1EB6ACBC6FCEA805CC6FB301C4FDF4062B6BF12AF691FF0E65EF4448DEB4E403C2D62610A8451807B14D722C0940AE6C7C30EE6EA4C4D7CE51076A27AA93F3DECBFF2E0D753DCFCC8444A223074FB0A291764DA8B4A8E7B17CA12F7F3AEAF69B11C0D00835140769C59EACD9E9363DEDD402F126480F1763C50676FF0DD8D21C2DD74B2F0C988A88D49EB40F99A728A3A896AFA9A27F865DE5F4F97A2899CA92F1804C5F3A6969EA60DA4EF3B27D822B5FA6D71C8690721A531EB637CFE3D1369A08F4B0B978FAFADB14A26242810F5B5906576A7F3F9C7B57837CAE2F10E164067E08E35D5A6E2A27DC81EB1C9E6058047EC8127C388AAD709C0632CA237FB23BCA6204763AEE078E876C78AF48B6D22867DC199065880E52EE13449568300EC14396B35CCE9878D6A6517B425F7E0C7A146F62198D1165AABCA7158C7F6E91F32D427006B9F1B160FF7E5570FA36DDBBD16971A165E6A550B1648DCD01BA4854FF7627FC35C95F2CAE53A742645C859A96AF248F6686201DD3AD31D783C093E8AF94F15B099EAF49A0DD4664A8767E0F618ADCE6394E09562AE39FB4A249852B00DE3A14B263DD7DC12617839FDFC57A7E219C638514BF6635B5D6D32379D15452302C9EB9EDFEF626FDF46110D10195B605EE6A78B5C772F57E91FFC73FF4DC0DB7461512D66D2B7BBBFA9B85D0B28F9FAE8FB979FEDCC59AFF493E1C3E03DD91FAAD36CA4B2430AFB342566379E49CD06591D70AA18C9880B121CDD802A5C37622175191C4497C11447ACB6390E604F9D026C9624D8A6E23292A4EE82F7D99AC1AE2C541D9380280F9FE484F4E996D9D39D471450DAD5BA51C0268AA0BBBAA02B06DEE0FB2E956E303AB1DBE5C221B69C5239EC35F95C0A723AB4CA76C4E0C57E6D559695DAEA5E7BD1070BCFC3D0968B7A2A2EC80B3E663421D47D9D5F0970D6651A62A1270D4B47B03AD4E1C3615F9593266849E6C0496CF40333AF199734051EBE1FC3B02B3B72F61872D0BC86E729DD73C81571421FC9C58DB88DCB0318A25B0112206B1BD2141435294CEC995B18A4FFB723C2F73327602656EB7C84D82266D1B288AB7D363497E009AACD7454F00052B96B0A6205A91CE542F6CA0C7C187001FB0EC8C917ABAADAED2C5DE369224B8F00EBADB8\",\n          \"k\": \"0CB1CD5F942305DFEEC6F10D2138621F61283C5A87EE1C5205D3BEC21D9E5489\",\n          \"m\": \"3D6441A62F1998E2B5B9B1E73A9A5022FD005778204977F66F7A5FCEAF17E30E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 65,\n          \"deferred\": false,\n          \"ek\": \"53EA0623859BC606082104996B5CC3FF292846658A17D60CEFF78B4EE37FD693876084A46A9A8337B61079705D2269BFE7486161E3370FC7492B92C666A889962B7450B15993C027051B8F0ABBAC66EB2024F6CBB63868D6389F39E580E05A357E5179367A48D5E00F03503C0E4C11EF5758B05C8B61AA51B3019C5CB017DEE224685035651703D0C5877414846C30A20DFC25B6E8AA76D98AC95534D1CA8A7E9177C57B2B46A0237B8B678BF889221C64C1882B7196AEFCB8056E0CAB1AFB0E50D467F09A9733960C4BDB5AEA3C9F0447B8A7198AF5D6A9DFA6785ADA30456B843F84CEDFC6761CE507B85A3715C3CC53409972D28E8F2A1E9EDC9DC3A368E2257A57373C967CCCF8002B5AC3C762E8C3B9B607C7451A9DD28F06472F3B4503C1A31A276727958210810716F53A8578FC76CFDAA35CB757BF069CC8429A9DB9C4F5B1602315882F11C83C260720A11FD6D01FB94171588539C5328E951B1E7F5605B4557F5A476430456A33572B60161A43A18A3780516D232F8B87B5E821081B109A39DAA7D74396B8213B37D314DE258B3FF0934FB27C276A913A337051E038EEE78542D78C8A04051A76A3BCA319BBDB99D3FA87FC9A03AADB8D0561B77C394D2F60A84B096D873BAAE7342378D8513E8B8B13DC6AC9E285966CB2E53521E4928465841ED7CA8F76B56BAC958E011C6CB751A4F7D788DC73CDE1F67A24EA9CAC41A0676C11B3D618386465C7557FB0BBB7A3441497764C9D898313A2ADF59983E0B2031BF7364EF0C25E347D2EF1111F29B8509085264C9D366970FE8CC19293AE8407602A21001391A51C609C7184C8FC0267E683C6FB83B0EE66B6EC1C930B1A5D60C108EDB646841415EDC5CCAE32CD8581BC48E58E8598285A4B31F805A56E395556B120B2DA591D3985B4D7C50ACB1D5DD12617343F9FBC9854250500C03725B010D45361C3DC745648CF337774AFE704EECAB4DEB9746104566C8CB0DC512A06E198CF04A8DF13AB6966AF12D552F3B6BA72A266625749D6AA30D1DC9DE8DC26870C317C9C26324A265787635E2CAC1922057916A425872C48E29C39AA4C06E24A462161C20ACFA6B458F56012B3A828300570F9EB92ABB3400691C8E76BBC1D3B36E6254E771130B555A68CA05A3A8112051CB10A2A3B0F547A11E53D803970E671B0E85C146BE1AB47F63AC46439F1C7AF41326DAAC72AB1274CC6D0AA3291C0C2843229B820E3C7B0B3B600EE72AF71671FA875CBA59782221781F3338F45B1466D60C31F6189F2D29C1F427D440B3560C11CEBBA7B0E298D2B270FB6D21DDEB30990271F15A3947B464802062851984AF1DCCC5A7083FE7C81A454A03AA9674D98CCDE10A76B3C696802347E519156F977CDA5923D73656A0081A26320C6CA7E1AD7A12362176D9A18AA75356E147FE3278C911070D2D61111B87FBBCCA67B991E5915196994034CC83246C024F27C1C62353E79290C26552DD52A57F19ACE5C006BF7774CD6C07BAED7696D61C51C8108C42237D8F4846BD4891AC44605D93F8EC03DE607456555365212C35D59B7FB267BE713CA43502BF3045CC2400C11A571C8468093F499C7E46D34C9A75602782BA606AE67A5D3C50D31CA5FD6E3B963FB7650DC5BB312BC35F67650E4B12B780D4B6680F640372A99C90433A7AA388AE8043DC5E9AB6105CC6FD91E61F35CBA99CA39E979256A1700AA9373AC14F282B71ECC419417984AC34677F0A83386B91028B4B7ACAC22C57908AA9F04F878E7B6420132A47AB5A1BF5A30116A6E59B6157E4228BFF499CEB59A09276B5D398B6A8677E725725D5C54809097833CA7E6136EC994AEFFE00C87F6845F20BF76C2A348E08C3895277AA082B84202229B7698924B4D5684E8C8AB81B34B87CA28D3E95FFF82B5DDA9BA9172076D937B6D02745DB7C1F77B3B3C795872CA728EC36029DA80353310D4A50CDDAB29E1381FC20AC97F48920A445F566C390206C6183666B2523B6A43BFC3E077CC0C422C1C91176C50B34C419AB4C97D640F1FB913B760685B64126725C7527A313A454D5F683B23828965067943E12BF633411B7B8C71F648E4FCCBD2A1262FD350A66CCAF2FB1D9CD66515C52E1BDB1478726426C84022F60A6DC61F7CA2B33A60957BC50B4E248DA86071FDF8080E7801F81199A9FB0C5888643D8192ABA9DA4D73869D884AA2A7E0727231D4FD\",\n          \"dk\": \"76956EE8A1A973DBC0C7D40334A81B43D495168A6FBA8AB9A4392069096B598B99F1D0B9F0285DDB138DCBD1B0301ACAB6AA370C101EEDC17C748AA6B1F69A796AA6D20232AD98C64A4627EA98B3A595C0DCF558EFA42077B8BA2F29CEDF464E21858112E3A29EB327CABBA5FA33BDD1297E4224ABCADC68BD0B3AE7895FAEF1692BE2A79F95720D541245657C754330DFAC485CF3275D5C613FA74E99185AA8B38E21856C0995567DE346D2D9B8C691689A6A3442C0C2EE871D0D6B7D6D46C8C4C504CAE38EF7957886F70538803079F986AA945D0F0C25F1EA9C41FBB4D2C145460C39E4695A7A9560714A954647CEAEE703EC8B40AC38423C64A46E64196109C6776424007866AB06B85D73A267CB789A2A7EE4B827FF0ACEC846C4A31A01BBEB3EDDD7336C5BBB4575BF40365673DB24B4034F6AF1C0906C71AA3701ACAC8EC96114399B1CF3712FB1840C31A255477C2709D05C80981AC892B2275223007C8D84E9C4E9854F3A426819084EC2823714F47452B921B200834BD659758705BFF3CEE671756513A6562424EF1A1F21589CF321A9D790C846E768D3A022527937CB82309E95962643C2AF5A6422EBB2DEA1ACF32839A30993C3A604EA97A5AE000DB8F8144F414D7E86513E5CB0B0E2596C53C7851222CDC48C48224D4863A7B35C478A42285B8935B594970AA67B89323DA2B67B6A7A6A9FEA7B3BF648FA64067EBA6BAAD6BF640A44CDD896B68B125504C3DA5BB9DD552BA400618C453FE57683568CB2A8B86BC33B519885A06889981A87CE21D913D102AEB9A26E5474784F499914670AF5DC8613D9BBBB9A51CE708FFBC8A63F2A6E5DF6CB461A7F215A8CE2E472F1B813028C49E9E11777B3B1317ABF96513C1E349352FC2BFF7AB0995147CF46AAEBB71D18399F678CA8F72960C6C5052585027D180435551A3F8B44FCF06E6BC8B9D57A171601AE1EF3C21E7133C6C83C365C72B3C0A56EDCBB6B40695738C713D34C4BE6282896539EB261425C1572685C7026B4F56967F359A121FA031FF007772B5BB92B1167E25D706B99097A2F38ACAAC81C02748BCF02748DE17A8D2B857DC2474366CA0660328BA08CC6D52B3CD844B0B821AFF3B22F10294813702F7E24A78FE083BA1BB88EF55C11534524E059588A02EC83B652CCB4D55968E8B52F293B4E75BA1790E95701D850C8313EA5E77D62681156991B67146D1C1A6DC6E7004D2037C7C7AC48C80BD566A78EE811F3E0C7598948BF897A6F0AAB81001FB52B4974A3C1150BA5C9685D3A632E68BB9D4925882A48AA5B01AEE9A44DF574ABDECB7B1B3B755188804F74A9524231EC444300B4644BB56E6606523DE99ACC8A4CC6EB2D7A93C1847B5FE288CF19F96E86C49DAE2B39FD678C63844EB4A7C39A25895F338ADF1330ED92AFB2013EB1B5729BACBB36EA25B56B6C5C309021B74E96D037B14077F025830E8044EEA0BC36D1610B9741E1B5CDC9584469864CCBF413A2DAAA260C8FADD82702EA66B6C811CECA5BE4DCCE53DACEBBDA081A184C7A55371AAB3A53ECC2E9B82C27D743635C4240F5394ED05F5C265AE6A120249103D7E2443680C655F10D45050AEF210B9CD306465282A5A2C182089129E53C0B4C028F34761D19C4096A622EEAB6C5262338735FB1D28E1C50032DD31434E149E90CCD8123ADDBB0AD5B903FAD334F4EFAAA04376E225C17353B278CCAAB104C83A855941A1583D6938C5466A9D6C63AC98253338BB19AD8645B70C05F90744C96779FC9479384CA19D46AD43567799AA248CB3768F324D8320160712BFA9B781681AEC61675CE2230C1F02E59A1CE52836F3CC252A0D4B27B14C46B2A08A2D0C656B5449A390C90DB69B3571BED8A76A2E26E41E08C728B68A1B659E8E8466D96B653D132EAC36F2578338D2334DB7B237AF744FCC08013288C1FA9AA26D88E1E399549A323F41718C138A857EB5103A131578ABF0221520E2ABD914B8F993455F980B726597345942F4A319963E65A704B884FF02D01C76A0A3503020CAA99F6C5D8894FFA3917645BAF9AF793CD46577291216A4A415033C31E78AD8F8793E4E97048FAAC12887267C9C043BA1A5DE6C009E5C8A68026F6798479F45496A142CA031CF0A3C0F8FB1110C3C37148322897A6899895C125B385CCB153EA0623859BC606082104996B5CC3FF292846658A17D60CEFF78B4EE37FD693876084A46A9A8337B61079705D2269BFE7486161E3370FC7492B92C666A889962B7450B15993C027051B8F0ABBAC66EB2024F6CBB63868D6389F39E580E05A357E5179367A48D5E00F03503C0E4C11EF5758B05C8B61AA51B3019C5CB017DEE224685035651703D0C5877414846C30A20DFC25B6E8AA76D98AC95534D1CA8A7E9177C57B2B46A0237B8B678BF889221C64C1882B7196AEFCB8056E0CAB1AFB0E50D467F09A9733960C4BDB5AEA3C9F0447B8A7198AF5D6A9DFA6785ADA30456B843F84CEDFC6761CE507B85A3715C3CC53409972D28E8F2A1E9EDC9DC3A368E2257A57373C967CCCF8002B5AC3C762E8C3B9B607C7451A9DD28F06472F3B4503C1A31A276727958210810716F53A8578FC76CFDAA35CB757BF069CC8429A9DB9C4F5B1602315882F11C83C260720A11FD6D01FB94171588539C5328E951B1E7F5605B4557F5A476430456A33572B60161A43A18A3780516D232F8B87B5E821081B109A39DAA7D74396B8213B37D314DE258B3FF0934FB27C276A913A337051E038EEE78542D78C8A04051A76A3BCA319BBDB99D3FA87FC9A03AADB8D0561B77C394D2F60A84B096D873BAAE7342378D8513E8B8B13DC6AC9E285966CB2E53521E4928465841ED7CA8F76B56BAC958E011C6CB751A4F7D788DC73CDE1F67A24EA9CAC41A0676C11B3D618386465C7557FB0BBB7A3441497764C9D898313A2ADF59983E0B2031BF7364EF0C25E347D2EF1111F29B8509085264C9D366970FE8CC19293AE8407602A21001391A51C609C7184C8FC0267E683C6FB83B0EE66B6EC1C930B1A5D60C108EDB646841415EDC5CCAE32CD8581BC48E58E8598285A4B31F805A56E395556B120B2DA591D3985B4D7C50ACB1D5DD12617343F9FBC9854250500C03725B010D45361C3DC745648CF337774AFE704EECAB4DEB9746104566C8CB0DC512A06E198CF04A8DF13AB6966AF12D552F3B6BA72A266625749D6AA30D1DC9DE8DC26870C317C9C26324A265787635E2CAC1922057916A425872C48E29C39AA4C06E24A462161C20ACFA6B458F56012B3A828300570F9EB92ABB3400691C8E76BBC1D3B36E6254E771130B555A68CA05A3A8112051CB10A2A3B0F547A11E53D803970E671B0E85C146BE1AB47F63AC46439F1C7AF41326DAAC72AB1274CC6D0AA3291C0C2843229B820E3C7B0B3B600EE72AF71671FA875CBA59782221781F3338F45B1466D60C31F6189F2D29C1F427D440B3560C11CEBBA7B0E298D2B270FB6D21DDEB30990271F15A3947B464802062851984AF1DCCC5A7083FE7C81A454A03AA9674D98CCDE10A76B3C696802347E519156F977CDA5923D73656A0081A26320C6CA7E1AD7A12362176D9A18AA75356E147FE3278C911070D2D61111B87FBBCCA67B991E5915196994034CC83246C024F27C1C62353E79290C26552DD52A57F19ACE5C006BF7774CD6C07BAED7696D61C51C8108C42237D8F4846BD4891AC44605D93F8EC03DE607456555365212C35D59B7FB267BE713CA43502BF3045CC2400C11A571C8468093F499C7E46D34C9A75602782BA606AE67A5D3C50D31CA5FD6E3B963FB7650DC5BB312BC35F67650E4B12B780D4B6680F640372A99C90433A7AA388AE8043DC5E9AB6105CC6FD91E61F35CBA99CA39E979256A1700AA9373AC14F282B71ECC419417984AC34677F0A83386B91028B4B7ACAC22C57908AA9F04F878E7B6420132A47AB5A1BF5A30116A6E59B6157E4228BFF499CEB59A09276B5D398B6A8677E725725D5C54809097833CA7E6136EC994AEFFE00C87F6845F20BF76C2A348E08C3895277AA082B84202229B7698924B4D5684E8C8AB81B34B87CA28D3E95FFF82B5DDA9BA9172076D937B6D02745DB7C1F77B3B3C795872CA728EC36029DA80353310D4A50CDDAB29E1381FC20AC97F48920A445F566C390206C6183666B2523B6A43BFC3E077CC0C422C1C91176C50B34C419AB4C97D640F1FB913B760685B64126725C7527A313A454D5F683B23828965067943E12BF633411B7B8C71F648E4FCCBD2A1262FD350A66CCAF2FB1D9CD66515C52E1BDB1478726426C84022F60A6DC61F7CA2B33A60957BC50B4E248DA86071FDF8080E7801F81199A9FB0C5888643D8192ABA9DA4D73869D884AA2A7E0727231D4FD1AE23027CC9C62AB641D0F46EB5F17471706A42AFC921B48AC0A39E50F560DC1502C2C1BF811FABD14733D6ABE45283CBB7292278166018EC48D33904703828D\",\n          \"c\": \"6070F979C45C39BC4191E7B6C735F278337E043F0DB0D24B7321A51391896F4A7D122CD14F98A2C0272C4D3FD039F029227B6D83BB800C7C6E09A7894B6CD86FD68A32F31CE3880EB9D4694C4551058BC58193AB13EAA62DF3DD14BB9606C29511E4AFC1E0566DA175D4AA45E428B241D6F3419710A8FDCCB0BB74792811570E0C24A29D034B83FFCF98755CF227AA3F8C80E9AF7370DC7A7705AD3808C3B33FF40EF131C879779F72722EA582E130D825B7912B3F6DBC5659D59088333C1883DD43B7FFF8F942D584F42352FE4A15AA8D3D0D5829884293A693585FD4763085541FF83BFDBB506DE54796D3641038410E4617E009042881A942311849CC789658BEC01554A4D6F9E5636E59A76C1733C40D90ED8B0763F16B8217684F484B9FE19E07A8947A3EF04C27645F300A664505ADD17015C2B1A319D414AAD10C638B1F37FCF61F81A80624CE7D75E2759E0B9942CE6251349DEB9EF56A5A4245D6B598046186D91240626F1F37AEE04704E7F6A14214A520603B7ED44CCADB8A2E39093718EAC9AEFDB96CA2822D8213F66655017BCBD465716DF13542B053FBF0D99018D4549E2D1B19735D6DB95041C0AF04A169183CE0A634BA114E90C30429984633097141E1BE19DF941F2FFD228E2C627F02798BBE3A886147CE23168E335590C1C3DD337E980CE4769AB187A2A6E855166646F91E14E3B97700753ABB6F811D474A412FCB951C2568EBA98EC9D2C51F08F3DB5D2EA797531D65A250579F98BEF2EC5FA1179C2DC6D9E27E66F983AD70AADB1F5067C104FE7B7A22F808F4C5AB71B881F2D6510EE85A0118C74DDCDDDC8DE8DC551D41BC14DF90294567AB06FD76FC87B92AF9AC0B456386C714D5773B500CF15596A4A71D8A6E23578AA9D89C596D67FD08A379868305148076106FDE47DAC20882E913BD2D397179D4E611E8CF25608AC3B50D12A7F7EEE1572B403387DE2D0EFDC35A3C8644BC3BD4DA9F1E2E1F2ED341CC1DFEE0E39416DCD6261AF74E84E0F6D33D91EB0DAECF19597BEABEA1B690514766F3C8EEE663923B3FD25D36401D33A39E5972B8B17A41D230374D7C1783B208545167243B31C32C9DAA330B3636A1305F96DD612990A5F1D7A4C407DCE61CF73F5BAF5C55B737D9152A5FDB19DC4969E9CB4B049318B88EF4AEF97DA536DFAF3BE6D3EE6D1F2E4C3F7E17963418CDA89C2A481237A6F2EC303F380595872F050D1569ACABEDA5A3219F8E7BB75CA77541395AC44EE9CEEBD1D4BF59507D77014F310CEBE3A322C8636DC450A279C54332F46C8205653556360B51B31E06BC1B5AC9FC9091859DFF301EA407E5AC051231F774C94E2E932A08CE22ADA65E4EF74D7A4BB1CB1947F5490E88D569BAFDFF756EC451494E4315FA25283BA63FF61BAE0E45C4582342CFF3F3B42E297B97C9E73E946151F8D29A47E57A6ABF45EFC5411B607DC160FAF7674665D0F0E5039ED8C7E59BE98FEBA7C81ED54DD51A4542E5CEE2712F25825B18177FB8E8BF4FE1657062F31B8D8090A9512CAF8876B09E97725CD36C9BA2738F1D4C863A7B99B27476812700D24F3576EF6007D2A26B79ACE6CDA9B291527C800F0D666B784A11BED29B8D8267D5A643E33F255B6693B129F38BEB976E4FE7AEEB0C78BDBD8D31326A155921236255B7FD029307B80CB002BE230DC3036F8AB7CA446BE34BF5A5F29E2D5A0DBAEFABABFF6DFDE4E400FE491F40C2861B0BFEEF44DCB14803C79E8FE79C7C3AE113E8FA0B2F507A3711B8C56ACD0B090935EF932E6004A36B9390260F3080C2DC75A61B69E758EA4D31F8142845218E1DDD85AB7579428258CB5C57286FE308D1BC164BFBD574246FE6F7ECD9BE6C91B24EDFEAE6F2BEF86B13780649AE57B1F7D764786FC4C7EDE4775C2461CBE37026BBE6BE5D8B0637239BE3A7F8B6E56E2B86478E9B5461F5040596B2207061BE9E7851DDBCD619DBFD7410B05A361FD25613BBCF5379D347E43DA571EE03EA19C8553B966E9FEAC579635241E82749F648A7935077163523D19B9936648EE5FE5CC902E66F4379156721A63824E97294D8FF0C4CA93D995F6C635E010937B8C94AF28BE078FC94E94F9DFF2E6747E2F0287BB6C756D8D3E1D1B0A736B92279F37861FBBFF8322C676472B44667815755EFD9BB4920AAFB689B7692892960E059D010AC883ACD8B5068C1EE9B87E82A4B637A006B\",\n          \"k\": \"12266ADDBCC27B282DC0566CCE7473F4D705D1DB4B3D82130AE29C3999C6A999\",\n          \"m\": \"637B7A1B57EB76C50417601EB71269E050008F415DF974C07BEF46CEFD08368E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 66,\n          \"deferred\": false,\n          \"ek\": \"51F74568AA5F4A54B2F4C9CFBC7666B7075A9D65A016416A49F16EC0CA716B75A3D80609FAAB06DFE69661706013A0C5FB7AB0340CC1BA72C2D7E1635B33B0EC3B29FEB369CEF671BFB907B3A2219B1554151A62B43638C635219F86B792B18527D8CFF00BB24853A5A6B81C9BB77790B25BA8A71A7CF7231A9007EB8B50F586139279CACCACCA4C7C64EDEAA194768D6A33BDFC42218EB279FD111E1C9633307AA30830B161D93F13973BDBB3772FA57309E36880816F97DB8E7E6411CC569A578A5C6F62AA33C2354CDC15D2065451E1ACC212ADBDE104F094378ED8882BFA416B1C55BBBC20C1E5AC89892B0CBA2B54516937E781F7A4A9B1802876598FF95832F6478ABEA33362A40EE28AA957E74540129D6FF3BCC0114A4AC8A40AD30BBDDA2BFD2BC5A6CB4DE5978F9AAB64CC8659A5765AB11AC3A654A247165253C81065359E070921188A312805417FA149D335ABE8404293329C73DA1E2E43CBF9B22B0D84339F792C9FC97006345FE6DB931AD71F897878B4318073B9A64400B5B4D0C4D6FB57E8B511743859D8E873B9C5052578282F9A6362172014089F2993BA7FCC2444FB185EF055D9D6A0EB564D5E4667E537AC9B49AD1241950140B188F4C88CD6A241BC42122A3DD7720E6E6B214D2721BDFB9358E7026F1A024132CBD0BB0EDFD83552F271B151099CE55D783C88B29A578B2943E3809A68D268DF746557435B5EE1597CCB58446403B98C88DF38399CE58BCDA1CD00462B9D933A83546526BB9E5C887050F5109090600CE84072DBAFAED20D137A7D129ACCDF1712C94098267148C0B1A301DC618D684152C817F27B3E8AF83F91951347C633C7921804D853A2E66C948143DF1B3C15B201EEA56E764273CF50AA58B45E71950FFD32C69B079564C48B08A43857E09FC33B8222A3852A055E3E4CA7B72B190E6C8B557AAB84D7BD138CA148330B75AB5FEAB8419A55938771053A5B09261CB0D4A42575E72F8F505D77F0896780CCF589376D5A651E4008CF600972008540A0896F80C17BFC78E16B64956677CBF864683735A279B966A4A906B1972AE4875BFC81EEFB75248B72E3A3768EB56D2DC3C04921011F4A92FDCC8EC9347353FA0055CAB83B7465D221B877A70B59715A6075799C19C8818595E6D478D659C7F9155A841B28EC751866CA3C2A829D43467E12563F5A4154947581D4B4ACD827B3315B44B3E31BEE0C04E9C03D1A186AAA7438EA9AA15FCA83D26B79A2093CC4505051B31F3D563832F397C8671516648AE92108A8E149392209A04816EFA1207CF854FA655538C161F6561274E5154D89A223665FC285B6D3A45A2F38278DA7B5055BAF8B1A052196C2547C3BACE09CFBA1299071635510ADDE3898C4C20E4F8034B454BFCD0CA262812D43C4A8660A5B2CA42E2FFA7AAB513DFCBC437077609596A45FE8BF330741A88A5FFD20AE19B58C3B19774A047F563AC6CF0A205A2A4C1D6641F93AB4C0F39E6BB51032F0A12B8A45E900954BB3A4379129E44918998A50454050607101BDE2975E66C0AE2436664B15850294FBC985B638C8303767DB7A10A8A74D5AEA0EE2B6105112525890C6D5A8CD1712A2913B73C386C450F7286F4A8ED7284AD9F36A22494E51D43950526B19766E388AAD84A39391399628F9C251D4C784AC2AD0424FC57BBEC391A549D59E5638A8056929203BB8F9A44A27117B28BA2670508680D55B82B4CA8E523BEF1AC4CFE83511B920309A735C62CDD5151338C50D104950C9273263E8250488135240A949958090F06BD46B5AAA3035CF9B7308C09952B66AB5999EC6E6BE7A027964249D63D1817F430737C96F54B391EB44640959277B56B495C49F482B829F97B94FF8B4F0E347A9B6BB83E9AC6617000694AF0C7AA13A273BE2981286ABC62444AA16FC043CE4B1B4F50437C9503F815772A8BFBF6B229D48B75D94BC02219E0F71BA020C37169B42CDC1A88081B7846C3636F3598630B314D809AAD1B7F3D335B41468C019C2C061240BE39325D34DDE9C24FD29CB3ED62F815C248F4B6E8D3908FA3B9C8AB1534AE49176652DB909059402128F18C0C9E52DED217C7CA864E378CB59711B0C764B96F13EC5E3766297C9B303806C685F4A861878CA8415D60B057814E554B04F49C3DF9AAC26A5CDCEC91B59F32BED063E299ACD431F8B781FBC1AB90AE6AD004FFA864E0E16AB57\",\n          \"dk\": \"2E78C01275512282C156828B65705F7163A682878F9E7C0762153C8E63012DB043810B705D9C3DC092C1427808D5C8BBD361A860C5260D36A3DEAA97A3D97771F7804B3B97CE43A4ACD7A54B3068E2727AAAC7C3F960AAE2880F8CF049898A6D90664024136967D18EA0FC6736EB0D20665DF0075BD9789E621158A4A20F036832E71C05D1B3218EF75EFAE05262A21C1A4A8A427350B0C128DF37C775D602732B21E5AB36674C0AF61655E28C56BBB9B1D6174749082F29D8177427A24E5597769758C2C4BCCADC32A618C57837BA8FDAA8B89B35AD8A2C72EC03F9707C0FA4C01A231976B0B15CB39438217C061CB54AFB40475911806952A42441E0F77918E7CBEA9639D1F1A2DB9C785EB5025307C1924862BB68C5FC652D2AA9AE043B31953393FFF3A2616B1DE4649088450617F548005A477C1897260046B60BC120764A25DC4D9E1285C48878D3738A3A47687CF36621267041982E50CB6311827E6D27278E6862C724007E75B35499838D9553D9C79196D84A7BB2B386790ECD7647D3A60BACE84C679AC3A0B212181A0478D72E4E50B7C10CC5A1B154970C0625111A4ED8CFB47209672B7597B16FE79786E8B571A32A7EC59BB71F745439C181FF23C26B96A5B10A41324B2F8021648CCB016BDB92D894CEEFEA1876A30DFDE22973DB8B70D7CBACF5B6B4B54CFB9B2522919007B6526C084B8F392326461F0BD6CA9966351BB45F2B77564D1723A0EAAF0A2AAB372B9C18FAAA21A00DD2F4570A367EB2EB4E3E719D1E72A164C4A8DE6609540B6E93F71759362C2A6C930385217821BA00D0BD41C059176086E61620187B6B7D448A34A8631B9913B2C3307E529D360749DE11571BF91866A098CCD29DBE9314A770C217356EC4A0CC3D98CA22EC85F9C3C4F3A065EC5C48AA9269A8E5482B9781E7A39B6FFB287FF730919535C5987034A32AD4101986BC9F17B2AEC906169E3C56DBB213D6FC4BFE48038B0C1E235C4E106C1616D95F75142CD02B18AF13B56CD45EB1C46E6F008234992385EB21A6A7C219021EF47B55F5301E60F9CE8FDB1E3BC56F4DA253F9799A160A077872AB7551013375A2C8B3B9AF70BB048C4389EB90CF31512E080AEC7673C7F23EADDA8BC17372C8BCAF100438B0167FE2737BDD54C9571CBDB843510D615198821A2AB16126D107507B5D31D4349C0A2E47D6044B6802700C9FBB5B3300A2B65D4C457578639696BE7336B618284D99514701CA0B3C6701B008CDE5A88908D813E2BCB86D032CB13B70D34A35D07567B6DCBFF3343DA4A9B7390CA1FBE195E384464D154FCB169AC35087C3013127835F3F98B996E9815DD4C14A6995ED766DCA79ACCAB324AC20184A93C616F53BFA722BC9FC55924048702820A423193A858E6E399470C3373F8B0FAE335A1694B96BC29B15B939F81312311B548BD819A8E9168B303E2F82AD8110531B8B21F00517A0483516C160E8229971CA293016B8A5828B83F87951B266523388A82458ACFCC98647B2043A2525EA8F5F0A66436AA8F0A2C736E8284C8257FA49475B2A54DB2C3A9B0BA5DB92C48088CB79572CB21B7CFCD8285B54600F75035A00AAA5B31AE4B201755BCF0E4B32A84588136C9D48B129837CBA057AB02BD34A3871BA824A286685B3598007E39B931886BC22944535F95A5E6C1E8A95B48E22A6541C1EC078903CD05DE9E43EF054661412629D860717A90F91685BC54B62A315348F4B8A74B672C19883BA58C854D349394B20FDBACC8AB0C0A8972DAF080759EC20C6677DD956A9A48B07A727ABB239382A2767FF2522E7217AD3A2AC6C13331799BB0BD39803598FC04921A1F90B7DDA2FDE2A2FBA4173C9D390FC31B6388684D6970BB4562A69688283EB6327D960DB19B308A9BEF87CBE30606C56F8306AB134E32ACECE27011F740004D473A6C1016C5C9F4ACB992933801DA80B379043845A3432B375EF670DE54C6AC7206BDF711B38F272DC916BEAEA6A370008845C792CC9C2F64BA10DB51ACBB18EE0EA99D028CDDE671F14FC00492BC43E282813A8B0BB13C3BFC0404F362A8A45012B77377B8CAEF7734E0C22C4BA4969F610482E47882B209C4201378611C88E9CAB82E5CA8BDA976F11803A11AD5B288C043B2B8891168E4C75F62678BCB05EA5B11FC66A2551F74568AA5F4A54B2F4C9CFBC7666B7075A9D65A016416A49F16EC0CA716B75A3D80609FAAB06DFE69661706013A0C5FB7AB0340CC1BA72C2D7E1635B33B0EC3B29FEB369CEF671BFB907B3A2219B1554151A62B43638C635219F86B792B18527D8CFF00BB24853A5A6B81C9BB77790B25BA8A71A7CF7231A9007EB8B50F586139279CACCACCA4C7C64EDEAA194768D6A33BDFC42218EB279FD111E1C9633307AA30830B161D93F13973BDBB3772FA57309E36880816F97DB8E7E6411CC569A578A5C6F62AA33C2354CDC15D2065451E1ACC212ADBDE104F094378ED8882BFA416B1C55BBBC20C1E5AC89892B0CBA2B54516937E781F7A4A9B1802876598FF95832F6478ABEA33362A40EE28AA957E74540129D6FF3BCC0114A4AC8A40AD30BBDDA2BFD2BC5A6CB4DE5978F9AAB64CC8659A5765AB11AC3A654A247165253C81065359E070921188A312805417FA149D335ABE8404293329C73DA1E2E43CBF9B22B0D84339F792C9FC97006345FE6DB931AD71F897878B4318073B9A64400B5B4D0C4D6FB57E8B511743859D8E873B9C5052578282F9A6362172014089F2993BA7FCC2444FB185EF055D9D6A0EB564D5E4667E537AC9B49AD1241950140B188F4C88CD6A241BC42122A3DD7720E6E6B214D2721BDFB9358E7026F1A024132CBD0BB0EDFD83552F271B151099CE55D783C88B29A578B2943E3809A68D268DF746557435B5EE1597CCB58446403B98C88DF38399CE58BCDA1CD00462B9D933A83546526BB9E5C887050F5109090600CE84072DBAFAED20D137A7D129ACCDF1712C94098267148C0B1A301DC618D684152C817F27B3E8AF83F91951347C633C7921804D853A2E66C948143DF1B3C15B201EEA56E764273CF50AA58B45E71950FFD32C69B079564C48B08A43857E09FC33B8222A3852A055E3E4CA7B72B190E6C8B557AAB84D7BD138CA148330B75AB5FEAB8419A55938771053A5B09261CB0D4A42575E72F8F505D77F0896780CCF589376D5A651E4008CF600972008540A0896F80C17BFC78E16B64956677CBF864683735A279B966A4A906B1972AE4875BFC81EEFB75248B72E3A3768EB56D2DC3C04921011F4A92FDCC8EC9347353FA0055CAB83B7465D221B877A70B59715A6075799C19C8818595E6D478D659C7F9155A841B28EC751866CA3C2A829D43467E12563F5A4154947581D4B4ACD827B3315B44B3E31BEE0C04E9C03D1A186AAA7438EA9AA15FCA83D26B79A2093CC4505051B31F3D563832F397C8671516648AE92108A8E149392209A04816EFA1207CF854FA655538C161F6561274E5154D89A223665FC285B6D3A45A2F38278DA7B5055BAF8B1A052196C2547C3BACE09CFBA1299071635510ADDE3898C4C20E4F8034B454BFCD0CA262812D43C4A8660A5B2CA42E2FFA7AAB513DFCBC437077609596A45FE8BF330741A88A5FFD20AE19B58C3B19774A047F563AC6CF0A205A2A4C1D6641F93AB4C0F39E6BB51032F0A12B8A45E900954BB3A4379129E44918998A50454050607101BDE2975E66C0AE2436664B15850294FBC985B638C8303767DB7A10A8A74D5AEA0EE2B6105112525890C6D5A8CD1712A2913B73C386C450F7286F4A8ED7284AD9F36A22494E51D43950526B19766E388AAD84A39391399628F9C251D4C784AC2AD0424FC57BBEC391A549D59E5638A8056929203BB8F9A44A27117B28BA2670508680D55B82B4CA8E523BEF1AC4CFE83511B920309A735C62CDD5151338C50D104950C9273263E8250488135240A949958090F06BD46B5AAA3035CF9B7308C09952B66AB5999EC6E6BE7A027964249D63D1817F430737C96F54B391EB44640959277B56B495C49F482B829F97B94FF8B4F0E347A9B6BB83E9AC6617000694AF0C7AA13A273BE2981286ABC62444AA16FC043CE4B1B4F50437C9503F815772A8BFBF6B229D48B75D94BC02219E0F71BA020C37169B42CDC1A88081B7846C3636F3598630B314D809AAD1B7F3D335B41468C019C2C061240BE39325D34DDE9C24FD29CB3ED62F815C248F4B6E8D3908FA3B9C8AB1534AE49176652DB909059402128F18C0C9E52DED217C7CA864E378CB59711B0C764B96F13EC5E3766297C9B303806C685F4A861878CA8415D60B057814E554B04F49C3DF9AAC26A5CDCEC91B59F32BED063E299ACD431F8B781FBC1AB90AE6AD004FFA864E0E16AB5777DEF69A903066F0C5A3F4CFAC6B7408862923728492E7E5FF26A83A48EBB8BAE5D9C71D76D5958744741B9FA4B7EB67799F54AA0717478C4BF0EA6E0B012AEE\",\n          \"c\": \"229B069FFA4848A699156C894955CD9BF623BD28ED0E2F34B8E1F62A1B3DD00A6AAD501DFA776604A874C5FF1E60C3FE89EC281DD320BA2C1EA16E99D147B0548710EE11CA2540DEAC882A7B63057400030EDE2E75BDA52622287D3680A2FA7DAA94FE289D1E3879E1039CE2B65C9407E7A49CB93E76B4B4BC1D247227F437696D816C08E401B2D82670E189AC9F6A33EBDB2C0B16C2E18C9009BADD550B1F533C265A3E0153C982D4D64B215B7CCABBEE4B644B1592A766C30B28963F0991EFCCB5A94D38B38813CF9998A362318AAD81CCD6A0251FE63DF879DA7B7CE4C9CC28E86211E97773D3AEC98E1931734CE8629BCC668C490426BA60CE2E28E2871DB69B9683CC6AACC4F588733A7ACED2F17EBEF11061251AA8745F147BF1DA650386F7CC637E9D02870C16E2F06164E694828BAA66610EE984B7C60D0BF82F8B6D79EAF56D75FE605B3CED809DFAFAE3F858632E3147C3BBAB8D931A3B00E90693E5B840A77277C9FBE86FEFB2BAF134AC21B8B47E3D0A009894028DD5645ECE152838290EA835944ACCD78CE3038E8A2DD991ECD66DA742FEEF94125E554BBE04F0E923EF1BB02381DDE16F1D41BEFC8C418A6818FEF891DC75ABF717DD84979FA47FA3280364444AA77D72C26E808EF6202BE04C4FBE4B26A5B8E60317AC1D6860F5E728CF316DA287B3DFA391AEA8AB4CABF0D88419CA2C4B461E8C4BC633F0F2A6AAB7A86179170DCA1DE6A2885983CB4C0BA34B1D2A53AB9751ED91C49DE832ED22BE9443B8C2732B32A9BC14889121E5B261946EDD759394474627A9D82F6962F76D67B94649788EE82FC81081F008F6DD183F27B69CA19A5CD1FCC962502E827183AE32DF8A369F1A6DC44075BED6CD3C909B161FA62CD67F2425DD1D86D8401F7ACD5A4D5CCB94B9E22E4D518A9EBCBC705ED3CE7068977E1F4D7DC12BC51717D00225DA24402BAFB56DA5B7C8580F53CF66CE8F960B1A58A40534D29014A73781323E520D60097B5BBC5492D87D9103F040BDACC7640DAB97E6127458153953E0624A26ADC8875225ED6FEC2876DBCEB536D01DF0001477E64DDC542898B6D502BAE3FD7B49049A1104A290107904CEA445A045EE1432E79D8B358C4F28E42DCB05A94D04B78069AFF8A0A2CF2AF94D9837C2EBEF85A914163EE1F10E411DCBD7805A1EA204468EEE5E6DD682E2C1C7E30E260DE3AAD4AF6F2C6FDFF083847962EDD0052F13F0B8F2E4659F8D00260F38EE0BDDD4D6953CA0BB7B055CF7FD160D19AE5C3A1967FA0AFB09D71AE4AB63FCA185DF32DEF74E786E050FAD63D789546AB7723B64EAB6BBAC9B0B0AC9D3C994A71A68BF5B378B4BA8336CD1D4725F3EFA23E4B9585D3006C42485312AD7EAFFF80CE82F7A82DE7864D9EDB3A569892ABDDAFDBE384225BE377A051B75F0F5FC99C291CEEA13A6D1C1E6F8C1C5E72A86016029761B3E2D024F33B7C5B91CB2481F7BE4986FB381A3B8E20F8FDD5A390572727541B4D4988337CE19014433BB47F31D1847AA0C4E28288B1707744F542383271B32D85B73E3857D89703BC80FA6695F617D6DCCC17147779B9A4522C955D443CAB3F61E3667B44C0FFCACC136C3A487CCC760F7952A28C2826A7F640BEB3B3712EC3DC52D75047B7D3ACBDB74F32B0139FFF1C2A66818EDABF1C3FAE5DDA87512C8ECF60C133D4B98A7D1E1C72DD8A487B0A2FD9F8AA59EA4941B9958A2025385636B9258F4188B8F1CA96C4FA50A34E326616ED2354364C892295D2AD5726E4FA077DCE9FD110A95823B5DC355664ECCB7543E987D9CF986453C5ADBD438F849C6578CE859842F90975365E3FD5D083A0442E7830D580F51819A04941EC9284B31FC457060A7A6EB91BA7A87C0A54F1F422C5F0B6D8D4323881DF5525B35CCF992C2E701534A213BD935D30684086BB916B44FD9D678EE8BA7578B9E427FA36C139F1FD92DACA7FC8DB85607DBBD3532DC24BA45B53A36180316DD4ED5FD4449034308F07A6571F9943E194A4200F6834557CE568712D41078024762C7D020274B6404B326996EBF2464E40B8D9842D0417890DA0BBA9013A97A9259FDEF545EF780FC0D501A0137E9AB22A1BDC96252BD5137C96697628E559B00C2AE813323F2B9A1E2616809AB59945647167AE2B2E757C21273FA0CB3FBEA7E4096DC1CC8F4F114B94F5D760D496C1BA8ECD6B1EC68AECE4C9A25D1C561\",\n          \"k\": \"EBEFAF52036034E249B29A1825226DBF469C492494E9C4F13BD1010B963E0D4C\",\n          \"m\": \"B5C84B4535CC622A5D6B93229BCE68789D3014D500D3263B6E0F54359D20ECE8\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 67,\n          \"deferred\": false,\n          \"ek\": \"10971ED51709D256CAD9B57F0E48850C4B803642732C89A84439CD31C6B0F045915090168F8952B3382BF1186A56D2CBC102898F112FD91A2BE5AB0391A67C3C5255CEC88FACF3B6ABD427E3A410ED22C2C6C173F4894E9BF20CA3713CF0E8506846507BC762047715A7A37DA3A5730D8A1556560609E4CCB61298C4B2BEC374B534D0B266C427088816E843B118FA9CB27B9D1F549480F98F7F4B5AE71BA376B0B9180A157D8128D2286C4B5BC54F8590852B5CDDC5C11E6B4B36EC7CD6081D45A9C50272643CF0569A9A237581AD36D0705C004091F7C46D9073739226EFBCBAD5B551680C86E4290C3CA1AA756851A380ACFC033FBDAAC5B82C364DB075633A9A10B8137A05B29863AD1DA1CAEF5ACD2D75A9643B495EBBC184038B58B9A95998AA95B672A2D15CB78B0A00E386F433425C469A7677B8569CAC8F5A3A035C777B4570BAE1898FD379ABA62C960024E55645CA6625327457A37A92FBD68C76E79528D3B9E3E8917F93B5774B9A7DC44AE416006D8955895B6C611814C99432F683C67C09C354B97DA3CB0DB93841991B764C9A08EB26B9BFA96EBB39027D6399AD2688AC047360A7989BAC587FBA7DA76153360298A3D67FE73C983E4563F67207E1B835B7DC7CC9CC3372E12DB6EC759B86A684702D2B6CC805726DCB04AEFBDA224C02405635040EF60FD799A906116410A6C5EFD587BF1778CEDA4CB0152A272B89E43017B40C9B974600F03908EC397556E8AA7271828C049F895753E7BA96569A280CA15F5A82CC1F6121B6F7AEB214C726B1C64F7A958DB7459D4A30E4599A71FAB28D5196288A443D6C35C4AA27DF674EF6B4B72FC6485A31410457C182304538424A7501A7D62086910ABE0C54CC752B5E8E87039E64233EF5B837C3B50286172DE5C9CEB92C9A0BBDE4E7B14A0419DC6AA03F655592D25DF4A6191EDB8545351B2D89A7FA89826988657763A93EB71F8832B159E690B95C04DF37A2C9024451A18088A1C801B40A601AAAA6EB692E229817D8A390BB2474A95A7131C84D1745272A289C84BC2742CA1097A610D8A90F2C609651AAF5D0414C3C3FB3D37AD8CA382C9980A52539BFF73FCDE029FEAB0F184A7EDE4C7142478253C84EA5D7B6DB963ABC39A6B5BC29D43BAA24B4B26C7C53DE096E85E945310A484F591AAEAB48BEF98353C17EEB457E7D0719247A62EA85400B23491CB65932200F74EAAD3DF7184A54654C7B6DC931B574A2A55C7679F15C5944191A58D2A5B24674FD80452B22ABB7305501D9A156D0C48C656A35540FCBD43F14F531557BB9E8A3A710C000AF227FC6878AD28A708A34320191A236B38E64F3C4E705555DFC1FA3A144D8E6A122F7C5F557462579765A017AAF4A814E6B1201CC4E8D0757C378198A5798D3A59186A54FF158A451C42564B4659E0A35BE734600210A68F8A64E3951E849CB1062BCD6335970005B1B69C3A4845226A2C0834C1A467C6A799609CB870C7370C39265C0015BCE968419EC7C58464CB518550338B6807A97528FB34D1011A67F15B06C1BCD7B5909047136702C9AE604B927F28B8918CDC6F07A5D60B38BF7CFF2EAAE3C075D1579A30A76093B7A6DB44AB21A722F818861480129ED03B1FEFCC2607B5A1D5BA6ECB91B75B9810F991DC55823333A927AC13C3C297EBE02A27B949F47CC70A2266FDC76B5B55468A6B98D33955BAB3418F2D3C1D206C29EBB05E97811EC140307F9347255381AE152113C2ED2A7C1812A93FBC67ABFC1C1233B864AFC4B34B9CD30358990F51477911D6C68338A9857335B9152C5302F32BD5602A9ED97892F14951F111CA2B607DC46CFE6297D0A41BEE369CB3EB941FC2701104713A1784D6DE88948032AC78251328A8C58311848EA02008669B4BC811D6232A859756B306F24607DFD812DE89A0A6A00304FE795DD1C4A55366E615957855ABE11D97C7BE95A43A757948C5F4C046E9DF6A70674541B20010837307443444C27AD36C431F1F372EA659EB0693447FA5C40E8A38A7472D4A1BB0180433585B0CEF74650733CD90094199ABDFC1392E24C6D1C74408D00AE27F05D1729433A90AD9BD22F1B6C8155F68BAD1307811770A749A85F060E74625893462CCD946E2C1B79B6CB3CEF971899AB29F4328CE29313E4794C3463AEA46106D9CB82B5EC39D83D27C4CB3B69DAFA2E955D002E61C3E7BB247A76042FFEEE7E\",\n          \"dk\": \"57C613B806BEA20AC23E2CCCF3B36DD7AC5F84843F273C8D9923CB8770A34A63885973AD2E006B45437722151C3D0539D7723AD1682473C378C88670D646646473A18BDC30367C136874BDCCA49E5C42774ED96279CAAB1594777100706BABCBFD46652F42702CC00C810C2F753263551C44C928B6BD319F1C1444EB014DF1A874282B99CD11AE59413ADF4A3459F9A52592B1EC026977E9B26022A6DC059EAF38854D49C4F691A0E96A031D822907C664D3E6B263B35F3E967167F953D91A05A5AA0C4F5B5B288745695AAA2378420B6C0803D855C3D42A623A67B3376712B2B299E3B155134FDB582D0D221CEE318FAE1309A906728ECBADD9CC298BA245A8D4C081B7BDF87AB4E56C07DD98488F342BD405B3CB327FBDD85C80D2766E2485E3A351E0A3039AA60738858FBAF5BBF6D9CB25592E1D82468BC6B23E678BC5E4559F125F69B414E1AC5AD6B4498C9738328CCFC6BC4E7469CCBFE7AAAECA8A22874727A9194EF2BBD7564DB3D927D85A716A4551903944A642B5131511B619323CB620435B501580354BE09C3E46AB78BC25D08B29440B9984E75D75F263A081A562A2C96D538F6A037A6CE9C1B9A0021E0591F874AD08032BBD18A1C6621D597A47D4311AD0CB91E4DA1A03952BA7718187B86BC6E396CD716E539C2AEEC8285629A5A7B4596C6B4B5B5B272FBC00F12B49355C1AC73B5EC245C3B6E59F3C81C76E9675F51616D4608795A7BBFA3B0B96AA90FCF55BA7768CFF752FBBF3C0E2847EA939AA88D0C077A5C721DC1F09D2100DAC98B9F49848A7BF3028931C72451715789C11CEBAD69456131B70CBC27882C598311A0AC50CE188BD4A671197934EAB72B8842157F364ACCE67A4CE358A521895DA285331C412A20A88567B93623875949734D2665FB2A907E03A74D24531294C6FB1B45588AA61655977330C0F46F5CAF50466EF66AA89F8C15145A6903621BA269DA593CA28C5C439BBC2B0F74D3FC213F10029FF66CFA50AAB701048383A188EF7A8F40A7C73C19DB0C92248C95A3F061D53E3A3E9D76966C62C711B7DADD17ECA87B418B891527CCDD872B328C47B0F2798EEB90A9B46C75009523CECB196F5B941769F1167C4AC86249A09521DB9C005BBAE80B1925D01476E5C8ED2519F579CB46A01B1A618AB228342F06993E7252FDED96FEC54ABC11142FFCC6181F2071BF1B5C4A23EB782A0DD03078B1A9C5433C1E1306BBF4C388386814BF859C1070A033016DD599CD221A35DAC93739C9B97435013B6567EA18E8D60CD3F8186BCE420252027FC1C91CAF785135300CE525351A26E1281C05AB6BF9B0CC3AC01706A7B7B855719D7290BF16064D8136A2209A70DB24BC5F693AD385779A8CB144C80FA0435EC6C4EC9492015988634DC88BE46164255B9E78910299CC52174654F48B351B418636A2D41A474384562F9141F37985C343265A3788F7A53414A94A9F8982DE444BDFDBACEE37296FE2B669C307EC0584E975654C3209B082B4E9454B058D439C7CB7FB0D80F2CD77B330271F287A0F481995E26AC2ED0B572F04FE5C75E3B256FD5B2CF6A953EF0A85FC953A5DB971CDE68CCB2F5A804699DDE247D4C09B9AD34BBE2E7188D2C90D862AE1DF56FEA9A8639029CEF0B73C7352D1C94A29CFC28325AB1D14100A7F79AB087A8EAA749119B7F96E29B93293662C57533B47F66163D31BB3E4DF28BE89C48227C3FB2E3673EF858C7B11D37C2658ADA9DF048299EB883472637C774012A935B17C72987253F211C27BA97AB867A5D35C375158A637273993F68B4341A644405C013273C6C881E49D2AAC13C15213C9352D3C84EBA1951DA0394E19B660BC622696AEBF8841912B2B3347D9C8CA1D98565C4AC93A5255FD91B41A9A1BDF255C6C0860F05176D41F58CC8305D40F2622087133EE968165B1CDF41B4A14A7061710D9C00156296C4C428CE2E57163C0C77F2C9416E89905586C29D012111B07C475A79A9D227D9A531857A77B4017F0E800739980BD401503B472749ECA34816B76A22260DC120C986AD57861DFD2B2BE7D184F9B95AFA859392ACAD78016F18479BE83361568001F9187BC22C35A2A0BCE4AC7840354F3E9C43D711CC12E57E3FA2BACB6AC8B7993FC698B48574AEE7F6ADECA360233371F95240FF492210971ED51709D256CAD9B57F0E48850C4B803642732C89A84439CD31C6B0F045915090168F8952B3382BF1186A56D2CBC102898F112FD91A2BE5AB0391A67C3C5255CEC88FACF3B6ABD427E3A410ED22C2C6C173F4894E9BF20CA3713CF0E8506846507BC762047715A7A37DA3A5730D8A1556560609E4CCB61298C4B2BEC374B534D0B266C427088816E843B118FA9CB27B9D1F549480F98F7F4B5AE71BA376B0B9180A157D8128D2286C4B5BC54F8590852B5CDDC5C11E6B4B36EC7CD6081D45A9C50272643CF0569A9A237581AD36D0705C004091F7C46D9073739226EFBCBAD5B551680C86E4290C3CA1AA756851A380ACFC033FBDAAC5B82C364DB075633A9A10B8137A05B29863AD1DA1CAEF5ACD2D75A9643B495EBBC184038B58B9A95998AA95B672A2D15CB78B0A00E386F433425C469A7677B8569CAC8F5A3A035C777B4570BAE1898FD379ABA62C960024E55645CA6625327457A37A92FBD68C76E79528D3B9E3E8917F93B5774B9A7DC44AE416006D8955895B6C611814C99432F683C67C09C354B97DA3CB0DB93841991B764C9A08EB26B9BFA96EBB39027D6399AD2688AC047360A7989BAC587FBA7DA76153360298A3D67FE73C983E4563F67207E1B835B7DC7CC9CC3372E12DB6EC759B86A684702D2B6CC805726DCB04AEFBDA224C02405635040EF60FD799A906116410A6C5EFD587BF1778CEDA4CB0152A272B89E43017B40C9B974600F03908EC397556E8AA7271828C049F895753E7BA96569A280CA15F5A82CC1F6121B6F7AEB214C726B1C64F7A958DB7459D4A30E4599A71FAB28D5196288A443D6C35C4AA27DF674EF6B4B72FC6485A31410457C182304538424A7501A7D62086910ABE0C54CC752B5E8E87039E64233EF5B837C3B50286172DE5C9CEB92C9A0BBDE4E7B14A0419DC6AA03F655592D25DF4A6191EDB8545351B2D89A7FA89826988657763A93EB71F8832B159E690B95C04DF37A2C9024451A18088A1C801B40A601AAAA6EB692E229817D8A390BB2474A95A7131C84D1745272A289C84BC2742CA1097A610D8A90F2C609651AAF5D0414C3C3FB3D37AD8CA382C9980A52539BFF73FCDE029FEAB0F184A7EDE4C7142478253C84EA5D7B6DB963ABC39A6B5BC29D43BAA24B4B26C7C53DE096E85E945310A484F591AAEAB48BEF98353C17EEB457E7D0719247A62EA85400B23491CB65932200F74EAAD3DF7184A54654C7B6DC931B574A2A55C7679F15C5944191A58D2A5B24674FD80452B22ABB7305501D9A156D0C48C656A35540FCBD43F14F531557BB9E8A3A710C000AF227FC6878AD28A708A34320191A236B38E64F3C4E705555DFC1FA3A144D8E6A122F7C5F557462579765A017AAF4A814E6B1201CC4E8D0757C378198A5798D3A59186A54FF158A451C42564B4659E0A35BE734600210A68F8A64E3951E849CB1062BCD6335970005B1B69C3A4845226A2C0834C1A467C6A799609CB870C7370C39265C0015BCE968419EC7C58464CB518550338B6807A97528FB34D1011A67F15B06C1BCD7B5909047136702C9AE604B927F28B8918CDC6F07A5D60B38BF7CFF2EAAE3C075D1579A30A76093B7A6DB44AB21A722F818861480129ED03B1FEFCC2607B5A1D5BA6ECB91B75B9810F991DC55823333A927AC13C3C297EBE02A27B949F47CC70A2266FDC76B5B55468A6B98D33955BAB3418F2D3C1D206C29EBB05E97811EC140307F9347255381AE152113C2ED2A7C1812A93FBC67ABFC1C1233B864AFC4B34B9CD30358990F51477911D6C68338A9857335B9152C5302F32BD5602A9ED97892F14951F111CA2B607DC46CFE6297D0A41BEE369CB3EB941FC2701104713A1784D6DE88948032AC78251328A8C58311848EA02008669B4BC811D6232A859756B306F24607DFD812DE89A0A6A00304FE795DD1C4A55366E615957855ABE11D97C7BE95A43A757948C5F4C046E9DF6A70674541B20010837307443444C27AD36C431F1F372EA659EB0693447FA5C40E8A38A7472D4A1BB0180433585B0CEF74650733CD90094199ABDFC1392E24C6D1C74408D00AE27F05D1729433A90AD9BD22F1B6C8155F68BAD1307811770A749A85F060E74625893462CCD946E2C1B79B6CB3CEF971899AB29F4328CE29313E4794C3463AEA46106D9CB82B5EC39D83D27C4CB3B69DAFA2E955D002E61C3E7BB247A76042FFEEE7E94666B893AB96697ADA5692E4E959DE6DB5C00F2B2353E615C5704ECCDE45D38404AEA8B2BDAF3FCB7F4FAD5FAA16EBA8A4BC94618FE14508C39F39A66BC59DD\",\n          \"c\": \"2467AFABEC5F378284AB6501C7322603DA732D11497FAF4C59B2E858222844D4780B1F7B0777EF4B7F61DF0253584BE5C46638535FB39072286DB984DD3DE335282458ACD297A585B64DC354858A8167AC4F4E1D00CDFDDE658A6D217C9C1255442C66B1B6F74EB0529A54A8B07290A9E07D2F74B18345757E21894639A8267830E6B065FCF746F8D3DFBBD23878B76F8B606B1227BDA4F221D2CA559BE133DDF9343811A5E5B3B0DFA27B4F9E24D86B7E959A9FD83392EC4B616C39AD9DB1D96D465A509F92647E4149A9D38381457A3A45A393BE987886FC7E8CDC561341383E35FD80680FE7F2D39DF791681D3C6A7C74031788C1D92F1D731F4261E5E385D9BD8D23D37B1AEBF2611707A6CF7C55418FEAF01577A2E26A248E02AB9F7FEA4A79CE55A2E4A8733AD8B3DE19639588DB04A5D8EAB7A1BF139C2BC0028E30988E3F2C1331B65AFC026FF68C08D3111B8E919A380A7CF4EE02DBB48CF552221B6C55C1C7EC9435A44316A6A8C35C8CE1F36EF657CFF16BC06A4F42CCDA96082CCCF0F903E5F1870B5BBA2EE4A1CD2EA06BF782421C8FCC73B43AFBA339D4EBB0FCA2958473FAE663B62DAE9805D1E7B469EF0F121A3528B4BD07556635EB0D3A83C7D3F776264F3667FE41C5EFF8B1861377E1671E2BD552202CA3F26A98BBF7E2453C1910F686A2EE82221ED50AC18A8538E02B7C70BEDB0E60B42D894B232073B8A222C055A4DA8ED707DF8F63471C7E8773DF9D0B3A4F0CA2861B2B8DD74AA3F216003672E5B132890628C7F279AE70A509E7A744C285F08ACB6BBE7F75D6B5200A5530188F93BE1D4416FF46A9BEE9E77FCB9079E11B264471C9F6FE2AC6927D3FD860A18931ED80D6AF7424FE3C93289D7787EA6334854DF131046E40C8ABD10FBFF2D4D4507352A619F5BFFE9EFF570C59D4DDEF3A1443EC91725F4488521E8941646B7E040DB141AB414E90E38E97F04F7BA3523DAB892494E5F2AF8B46E84761079AB191AFCE3571086A3EDBF02654791FBFDCF604762B84AE98B4E23A4F8CE9471A720C9C4E3AAB26CCD831361887DE84637B275EBC41BB4D98D53670603242297254B8E2C240B62A29EAB940640B2625FDBE2ED8511D0C4F2E04507177AAA81DA30C7FBFF30F7A4F03A2876E5C91722AFEF4DCF7929B3A1055645A7DE5F96CB13A81F7FABE6717E86773CD031390D10A36CB9E2A1FAECE1F318619FB2AFEA3C4C985630DE1E2651DA675A4930AF07A5B64162B28D16E700E3B389E5F142ABE21FF972B40F8EF6DF51411685F350BB6EDE53054E93ECCECA9384770B45E206ED7829D59E90A7345128ED2D4C8FD74075B240C47787F8D8AD3CB3BBBCC796328E701510BF5999EFFCB75FCB30B23D4B8F25D607901A6A942D263E5D1F4103914BEE27C4342F34591F923CB81D893AE4F4FF0B8B23076240C8B034F31D69622A6068C9428CFBE4996C1259EAFBA0DBF2AC43FB876D11A5A6F7944DE18DB2BE640F812ED6BF68A6CD09925D2EA64D46CF6BB5D3DBCF5BBD42D932FA017411DADE4857C279C70DFC12BB3BBC9AC09E21A7D7337E666EC9E823C0A5B03423F3398B53B877299DAD9097F986FB11C8E6E4C9673A9D10AC9FA80A23A88A5DF3BB7E722BA0439845EA58082534D0D7BD495C62EC40A86BB39EDDBF508DB899E3CD037CE1B08286127B3E62034635A98C1DA1E03CC60E0C5962B83CCDB942A33442F6BCBFDA4A5746CE94BA762ED1768C974281EF1736346708E4CA4D3EA7B2AD6462E807D12C33A8786D1BB3D384A6D5A6A739661A553ACE176044F5BE194A3C34F5BD5F23D4A2741D79C2799F25C486B51BDB1216CAF00FCFD1B2C1864807D347784E1E6C268C1A44D2D1603F544C7FAC0D278FA3386BE2E67896B39C965FED7A965B48E3B41F53EAC185352305DCE69DBE5F3C2EA28A6A87F1D5F1F3D5414CB78CF3195DF57B4C002601C0080C86F0E2D48DDA703E5F6234FD2B630A3FD65F2788ACECB31DE9E218686875A610C25EEC894E4CCB1D8FB645C97827DFEA89EDE57713BAD1F3CAA3D6EBC177F7F3B8FBDC61689952202B5AF485E2931EA07C89A8FEB0D344F216E82C4036BCA0FE766162C804EF25DB55D0D69617166D4DC23933956E13EEEB85695683F74D7CE5E162A1C5A0ABDA10D4C2E9531B4A898E6575179EB571CAE7D5848B065893E8FFA721DB4BC3C8F7E767DCC80C346CC014223\",\n          \"k\": \"77A12A7C2BCCD4700A35BF559EB1B8062628C5F7D540F2FB50A99516A11F639D\",\n          \"m\": \"FCB46FB66E388182DF6149F60DBD0FCA88D1BB1A9866A2C97B84848531230B48\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 68,\n          \"deferred\": false,\n          \"ek\": \"54B452960A93BBC12032E0507F7B83F112BA14CAC4A733700AF288D6DC0918C2B3D1625F475083E7967ED8A708B2B147D50658E1881DA6501D6AC6B7EF5AA4EAA5A7AB85844E3994E44A5431145A873834EB88A177400E95ABA99FCBA079914074209E06E17CED04636AB4ABD7824B98DB2A35215D5FEC091D5428B2928B809AA94C57694B2B65DE050817D6AD7326687E9A68695151F60A9B1502789AB8017760A6F3E7029B5074CBA929765B5C1DA8217C1105535699FC59195ED3A0E8799C66E3264F692C8DA198082860793446FD0536EA0301F5104AA689CC0263633F7A436621864099AF9C108D882043467B4888922D2254CFE8F937DCE040AC5B8641102959A90539F5177E21AF7AEBAEA2D9731AC87150A52068A71C183B8F4BE312A033719EA12E2CB8A197C1BA28B093E50437FF9779F0377BF5698557B9A45BBB7C8ED1AED0D982D71993F80CC5E7F16330D142BD0AC602F42DF89391156CCF1621AEAEB95701933A3CA82487E057FD186199BB5F4E2341D91A1D5B753B53E3719F65CBF0B3AE33153461459708B6B6E1131C9573C7B0CBCDF3A1C8ADA919D09C020FE6648221CE7AD35AB37C16010A3CAB1251B5930EC69C1728CA45E6639E97626A09469A6E204386E6A458D35E58ABCBE5C279BF79BA85021DEA6B6DE6E9A802198B969560F2C8C99328C5427207623C03E68181851401BDD615E2263761DAA3D0D16324FCAC9FF98E85ECA88FE668526501AF4C135F259277202E96D56F91C07D86692C91FC14187B99F1847EF11AB06CC019EA89AA88E114C034608C03A77280BC1B96307F81531009278193BBD1F5ADD812B278796309102CC2CAA9CF8C127472C4C63B47015CB852798379C73D0C4517E985ADC6F60E49053CB2B5BEDE0230A9E9A138C44178489EC5D5C2F069B737A143D6802454647C1B8C0837B43A89C879A0656CF087C733B7A1A7D153E092AAF193B6334A729B2B9F64D311090B29BDBBC5F1CA5E054461EEF72CAB14580B505BED3543ED15B02E33AD2A63B6EF980C407998AA5C357705B047D1848B4CB551845D3FE9A2BD491AD8E60308D880D6C56607F4AD24616845F092FCAC559CDCAA6FB16F2BB19475B700BC16C8FA8526055333EF389F6983C105E879FB4738166A1705BB625412B847553C462145A13175C60820783998150C40A2F673A3117FE1778A0A4A75F37BAC7D8A6C9690870D6874D2650182FC51C003698FD164BF5314FE3A4FA291BDD9EC3920F5A656000BA05A8F6CB020EE4701E8D8AB8BFC8DBA31887E65C7E403BFB036BCCF3C281ECABDAEC3984FB87D1C97C1F1112F8252C31A986941A50107D40EC6CC67E6E20F446C598832061BA21212E37943359B6B005621BB275235AAD581985168C468E15BF6C56C9AB65C7574B44FD83A32ABA5CA9831A6769135FA9916E3729361CF0D1142610C9A98363ED6642B853461C062C7D0B9B5F70694C8D4C2F073AE788A9CCA13446CD6BE40C82478E338C5DC34CCFB538451B6F94877A4B56B5AE6B3136B9BAFA1CCCC4902585907331C1F4811602716A5EAC209953B75A3B30EFD1B4E42F716B37A2F79E2C8ED062B1F79976C010EDFE4838929589A90B0A0ABB3E41CC8AA974C4C1308C4E12FE2D24162738A3811070D796701929C12E9A94FA96A823B6903498C4862732D48122D0265B0E571F0B4C43C62015F422753B2A562A80D68F19E2637581AD77592B67AC785B923AACD631A0B53D375E911797C0B76C79C53161015DFA7A67D1485C21A5B039A609F7386F8AC339B962472907D48F753314155C78A3DCF2010B1880C48196C0CD3B38B79BA9243CCB4AB5DF9D4569EC11F2F88525082B1C2224573C94FE3D24927B7AFF7E44B27755A0DA4C8053A7B8876AF4F809C22205B710B43C4D8B046CBB562B2BEED1C27CE736BDEA5317B658139509E4737C52A77186666947F7796D4261FAA8B9767814B7D096856A272BF6572B4998D2F6128D3FC6AA0FB2B66486A578B984C8835D1223D9E446AE6752D355A0FA09379F108ADD50683AD57AE731942C4F9C938464EB95C4D08264456F565E5981625FC75260496D05610CEBA5A751560059C38A8E2809609BAC2C853B819A781F61A7AF0C6FD728D9DB2C5623521D762440568272DD8831A4AA1B2F4CC8016BC53964CD7D3262D93710C56033B0F515A7B9E0AD3D6CBF0049DF4E55FD931257F\",\n          \"dk\": \"BD706C3D795690598DD0B25656791B6B6B40262B01474B55119796CFB2642E827DEB6C21F8E8A9E8B10C78156963C1A81D66580F061DF187A7C19984CE9ABA8C598EA109C7EC0173CBEB9DC3594F87772DF044AFD5312345F499ED502E5D42C5ED280BC35B1F9B79C86E00A9DF6422B5C8CFC455544EFC91542ABC594C7240747ADAF429A9B7C1F0F8BD7530B1ECD1CE98541EFA4032BA23CAA16B723524A5D9223CC1FC954F56B2BCA820E0E50F30696ABD11AC5359615A17815A155F7C27C84B0A85B649C8E3C75C8003A9541A77EDCB2FE00376EE25A0FEC314D6B3057D21335D52607504B278D5A05AE4C653458525393149401E1B2751FCF09831831A20899A115BB0D5585E64F42AEDE11A31271FFAD587C8B6117B346BDE545B88311349DCC89B39664939AF3925A289353FC5F48B37F547A6BB2CFD1A1B1A1C245D74078E0C8CE9172D4F853DCB35B5CAE79B26C607F25530B9500D1E645904E4047424309380C3332B0001B11A9CBA8BEF28161995411503610773A801492AB47C4AD32A08D9D621C08B553EC2AF4010CF0B8B6D894B86B4E8032C535FA39B0053218515C6B27E807C73EC2B966601FA976367005FA0183340534063D6CBDDD46287AA78205B5738D49426F0A5E87440FB01B70EBA2EF522743A65AB88736C1144093B76A1317849A5C9CAD9119FE6E5A8CBA27E0AD9B1AD6C3EE7238B386B7DB24872DBF09C6FB3485C58669BDC4264C5CBEF94200DDA8635B0AE23455F156135C9BA68DC675FECF3568D100BF022CA70F09AF609262B1636006644799038E5903D39688BEF9364E6482EBE347EFA71231CA1B08D0B2F83E5302F205AFF055DCBDC4B309933D5C737B35806DB19A3CC62A63AF56652833A67174196261A091918C933C21CAB913BE2965E760EAB34AEBB1B756F028A4DA7B0B6C20C43CC47BBE837A74AA5B42180B31333A2E6887C1616292C9C2855200006C34EC21C4BF3139DB74C1F06686BF88233B25B6C59A332372FB86A45568A120306428580BF72E082886C240A1980EB027F61560BCC1906DAEC1D48583B4AE85366840CBAD733E5AB2DC5044C9500A1ECD7B1060CB870A705F73618F0DB015A4BA33CB57ADC33A825FB3761F0758B7B7DBE5C4377020C8FF30750E53CA05C6F2E2704F4149267EAA355F2878A89C8E7394268437A807BB29A839FB2173D85102C8F06BC91B3679905200D581CFBE69C9BE25A53641D4619C12AB887E618B2798570C61108884C7AA22A3727A08EB5653F9E869678CA049C55389B9A235D27195DA868BC82C3A572B8CB878C96EC0FC0A07511923A23069C26CBAB2B879F40C081E55772E9A9A8BA826719185D627342A0411901C6396E0A086946A271B64750B32673266975A124ECEA8F3FE66115F543C9FA7D64064EFDFB368038918F5773344A2D01B288E80321EA67BC9FF7614A8BCF3C92A77040B316E66619A68D000CB60B543DC3953C3F92BD14516560833273542B3C1ACD85A49FAA4AB1C9474D8F203B8D49C66005364AEA0214444C779AC700F8878C15914D0A8CD2DB8C19F3943DFA17707C268381CC8BA02195B689B9362C5DB137BDE853A97976CAD043E7455A1CE2A86DA7701B884BFB937C6444150C68147DC0494B0B369D82181FDB90F8949AE3FC5283DB35504006F46C6EFCF08FF60330B720826722B945E3233A14CDD11AC888F3A561E767F6F48C2EC92DBC76489EB516C6226E063C08E6208228CC14940455E9C3700883BD6158074821CAA410A9AA6C3DB41A1E02367163E761260A8C1205B28429B53A6408DB144EEEE9BAC587521D725529A75C8EF83F2A4BC9F91258443CA265ABB54DA06114541F4B1B385D677C33B473BC0045A64078F84C977A3BC73DB8B30643B3CEBB073F0B2C0CCC8E7CB98F94242F50598447A84CDA593396490CF74071256B0F5EFC0A2EE9732E611F07487A839261A0677219838BEDFC5CC932601B500340777F0C7A92AC18596438C04EA82783EC3F119B8BCA09A4BE77748E9C83FEFC3B582110415C6AB5C76EC830929185172B325D9439CD96AB463D3021435306AC72BF476A70BF96A486537BBB87467B0329E855B5B1E7CDCD405EAA3C63E179277A959E5858AAC275964CF9569FB22F18C5C175930406B9CB89085D9230309030A554B452960A93BBC12032E0507F7B83F112BA14CAC4A733700AF288D6DC0918C2B3D1625F475083E7967ED8A708B2B147D50658E1881DA6501D6AC6B7EF5AA4EAA5A7AB85844E3994E44A5431145A873834EB88A177400E95ABA99FCBA079914074209E06E17CED04636AB4ABD7824B98DB2A35215D5FEC091D5428B2928B809AA94C57694B2B65DE050817D6AD7326687E9A68695151F60A9B1502789AB8017760A6F3E7029B5074CBA929765B5C1DA8217C1105535699FC59195ED3A0E8799C66E3264F692C8DA198082860793446FD0536EA0301F5104AA689CC0263633F7A436621864099AF9C108D882043467B4888922D2254CFE8F937DCE040AC5B8641102959A90539F5177E21AF7AEBAEA2D9731AC87150A52068A71C183B8F4BE312A033719EA12E2CB8A197C1BA28B093E50437FF9779F0377BF5698557B9A45BBB7C8ED1AED0D982D71993F80CC5E7F16330D142BD0AC602F42DF89391156CCF1621AEAEB95701933A3CA82487E057FD186199BB5F4E2341D91A1D5B753B53E3719F65CBF0B3AE33153461459708B6B6E1131C9573C7B0CBCDF3A1C8ADA919D09C020FE6648221CE7AD35AB37C16010A3CAB1251B5930EC69C1728CA45E6639E97626A09469A6E204386E6A458D35E58ABCBE5C279BF79BA85021DEA6B6DE6E9A802198B969560F2C8C99328C5427207623C03E68181851401BDD615E2263761DAA3D0D16324FCAC9FF98E85ECA88FE668526501AF4C135F259277202E96D56F91C07D86692C91FC14187B99F1847EF11AB06CC019EA89AA88E114C034608C03A77280BC1B96307F81531009278193BBD1F5ADD812B278796309102CC2CAA9CF8C127472C4C63B47015CB852798379C73D0C4517E985ADC6F60E49053CB2B5BEDE0230A9E9A138C44178489EC5D5C2F069B737A143D6802454647C1B8C0837B43A89C879A0656CF087C733B7A1A7D153E092AAF193B6334A729B2B9F64D311090B29BDBBC5F1CA5E054461EEF72CAB14580B505BED3543ED15B02E33AD2A63B6EF980C407998AA5C357705B047D1848B4CB551845D3FE9A2BD491AD8E60308D880D6C56607F4AD24616845F092FCAC559CDCAA6FB16F2BB19475B700BC16C8FA8526055333EF389F6983C105E879FB4738166A1705BB625412B847553C462145A13175C60820783998150C40A2F673A3117FE1778A0A4A75F37BAC7D8A6C9690870D6874D2650182FC51C003698FD164BF5314FE3A4FA291BDD9EC3920F5A656000BA05A8F6CB020EE4701E8D8AB8BFC8DBA31887E65C7E403BFB036BCCF3C281ECABDAEC3984FB87D1C97C1F1112F8252C31A986941A50107D40EC6CC67E6E20F446C598832061BA21212E37943359B6B005621BB275235AAD581985168C468E15BF6C56C9AB65C7574B44FD83A32ABA5CA9831A6769135FA9916E3729361CF0D1142610C9A98363ED6642B853461C062C7D0B9B5F70694C8D4C2F073AE788A9CCA13446CD6BE40C82478E338C5DC34CCFB538451B6F94877A4B56B5AE6B3136B9BAFA1CCCC4902585907331C1F4811602716A5EAC209953B75A3B30EFD1B4E42F716B37A2F79E2C8ED062B1F79976C010EDFE4838929589A90B0A0ABB3E41CC8AA974C4C1308C4E12FE2D24162738A3811070D796701929C12E9A94FA96A823B6903498C4862732D48122D0265B0E571F0B4C43C62015F422753B2A562A80D68F19E2637581AD77592B67AC785B923AACD631A0B53D375E911797C0B76C79C53161015DFA7A67D1485C21A5B039A609F7386F8AC339B962472907D48F753314155C78A3DCF2010B1880C48196C0CD3B38B79BA9243CCB4AB5DF9D4569EC11F2F88525082B1C2224573C94FE3D24927B7AFF7E44B27755A0DA4C8053A7B8876AF4F809C22205B710B43C4D8B046CBB562B2BEED1C27CE736BDEA5317B658139509E4737C52A77186666947F7796D4261FAA8B9767814B7D096856A272BF6572B4998D2F6128D3FC6AA0FB2B66486A578B984C8835D1223D9E446AE6752D355A0FA09379F108ADD50683AD57AE731942C4F9C938464EB95C4D08264456F565E5981625FC75260496D05610CEBA5A751560059C38A8E2809609BAC2C853B819A781F61A7AF0C6FD728D9DB2C5623521D762440568272DD8831A4AA1B2F4CC8016BC53964CD7D3262D93710C56033B0F515A7B9E0AD3D6CBF0049DF4E55FD931257F068034A9F16D0024CC9BB412EA9C778DE819A4CB27EBE5614C8994C9F2DDE25A96672A036E27BA0C2A7ECD385E3F4381DEAF2BB1EC0F30A8AC7B01F0A15A0716\",\n          \"c\": \"F0E6EFFC0E4FFDDEF79E12EAD79C5CCA3416B4F5E251DBE96E14A1E5865FD41C3A45900AE13534BAE5D95CDF84A02CB66143A44B86ED2E535FA1F0787F0C3E3B9736CC02D88A169D698C3ECCACCC4E2569F06A470CE0D012CF2920F9F04BF96C80CFCB0686B0CF93F01F28DD66ACD772A68B978DA2BD77E9C65178FE8FFE23509E440030ABA284F4C94DFFDDB2393FF8EA554AB99D568ADE6DD18B3240EB792F004216F3B528A4BF3B6DBDDE1F19F51498AB876CF9492D93CFCD060BA476E91B12845FE7BB290E42841B7FDF4056765A6460FDF4047B8DA73269511D748A27EFB971A9AE4233555EFC77F826F0C0BD3E3AC9BD2ACA40C7CEE537BFB288CFE14788366C04D9B2460775774268EFA0C3C73B5B60675750D65362D97C43341B6B559EFD27AADE3CD256BF89487DFB9BD467E3E3CDB7C06539CD20F314EB2E0582C1C46FAA5E8E6918EE28B2DF21AE24397DDB1FF70AAC4B7C861876B2A9034DCABC05CC4768494C77F3436D7F2D6CA8EA7CDDCA32D11BC22DC59049873EFB2A113AC61610C8833665ECCF7AD30936673371B0F9B561833AB5B76B2BA0FE402394C57692F900F01842BE61C45AFEDFB88194626E533BED9C9EE00CAAC2400FE98B66A9EEAC83CB7A337CC7731E90FFF9AF002B92DDAAB612A657F62B422C233CB019939EA79C434F71809DCFF197149C08D76EEC52EE00CE5AEAACC68ACF75373F20EF63F6195B1D550836E847F14FBBD48ABE5BC70698C46FC8C05856398686E249FA189B11423F4663092C74AA8CCD080FB100FB89060AF9813CE8E1EEFDEA21EA1A849DFA066200498F99BD3FBF4D57BBC992C5C8BB0918DB64B59DED4F5DFAF5DE70B9491E33AAEEFAEAC4E56F9D038CF2CE7A706A290E4CF9996387C789E0C133D5E1ECDDE5452548FAB8243BD5344EB3EDEF93465A53693EFCC664E0A845135C35E8702FC692CF64A7F8DBE02397C514B9918AB946F83C557CC85E70B258767E0FFAB91D4A1377AA7FFCAA5E1A43D07D1AD9649368F0A40E0E0EFDB16F68042DEFE20A75CDDE570B54895A03BAC59789BFA49633DA8E1B50552904650727C0799AE441E1499C6FDBA71FC0D362535EF708C8B9268D2C9962E777AB94F6A6390EEA9E296D46CE376EB295FE0B1E8E8FBC1B9ED154E32BA81F83EEB4BED921102E284D671961FE849E7EE7DB54B4972A6BD65022B8F5C3324AED5B177D6699D64C0D12462E35B0A00D66655DCEF462C7EC070E8D402B33AC617D8276CE022FB9550A6E4BB320DF06E903E7F5081CA65D644997454CD2EE845DB85746E91B9CEEF823D60665B930285B9D13B8120474F1E0C25D4ADAA3C9DD1AA33B34DA066C8527CCCB844284D4DE9A8F7E01CA702802C49FDE099442BD010224F83BA5FE71CA01EA4CBCAF4B01AA4E128B86E8D1478A4E06D8A107FB36C5FD6A73CD92E3CBE4FF1C18E972A552E9E733536E97F6B609336E3BFEACB0CF89D7D110D4A422500BA6A80EB169FECB4AF0C3AFC9B8BEF2EB150BB23B362FDB5E097CC75A17E25673DB42E434D7C0D331F8469456BF01F5A40DF00A7B07D955A8F47241B9359C8444DA41764C6F6B90D1BCCA4BA61FE386E63F4B51FF2A6E4242620463FD4011EEE199F748A572284F817521CB59B7EC568F9EE259257E77ED1B3A209B3B9E0C92FC96BEA77B0494D1004F84D299DF67A8E37668C98672A3F896A82D7C4D05A8E78EC412912085B3A63511FB1275EE70A28ACA0B879D43100DF07D43E9AE5D68224BD7A29658C1342F0D5C920B4914CEE39B0CEB3FC10E5E2D76789795D1C136E82E94E7951B370C3E3C41A8811C789D74DE1F888AE9C36AF369596F9FFB654A707B5416B2BAB90D52A4D4958CA9F389D1C46E4574848D2BD9132DCEB5A1B71BCDB5AECD627333358B0F311F6A5356161D6E78F6AEA499FF4378FDFEBF2105210D3E48FE5DC992362A9347A02CF032A568B6337BCC71C40A4AB4D64860BE0CE0E336E97E18063AEA8133C77B2B53BF9E9DF7CBDD146821E0CD1A1BEA0C73BD25D30DBFC994FFE911EC1CEC3524DBF480E9908439E00DEE4E3765DDF0C5459858E4EB68A0570B2EB6F8C1D90DA25215B10F2F5AC9053C243019524B82CA9AB0E184BCBACB131622582BEE592837EDB94FEE88B4AF8668714D8A7AA30C47A90CC0EFB1C421000FA6D2BA54741B238193E86CBFE4F7956B00F2A857417F95C50D69C0EB\",\n          \"k\": \"80376EC749550B531BC5CB538F78FAB38342C7DCD74B83FD83CD058227B9B3BD\",\n          \"m\": \"4CED177C0A454052BCBD682B39BEA31D0D219A73184BC00C100964C25BD106D3\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 69,\n          \"deferred\": false,\n          \"ek\": \"C701BA88063589288E73E4BE1AB28C6E3ABB70AAA03F656AE87B38A9EC947786A41D722EC63C0E9E8C1DDA078E28608D11E84219E35239D8030C212F34CC284EEC748D34C6CAB04A7F3C15A5DBA8C74C8666167CA4508EB47774A7C3BCD6589306FC6365D74018D2082DE1753A854EC6FA7813431D31C6A3FA711E03F042E6251E2E27787BEC65AA8CA47FEB5662599083E6944F442716342DF25AA551C36643EC3B9598184431405C8C8EB4AC8360E1789A06B2D13232E831C39A005881273B0086591760AE8A20913D942314F45BA5094AAAA05444469D12C20566B0C008CB2C5FB90257929DCB648E0D616001095309E00FE19235DD29B1EFC2A80E1402E851CD19D2BE9B4001ED214E418C156BB6BB85C0744C3B81AFEA59BBF055C8046386B576BDD009CB412D13B3918B19A658C690726BC93D4928F1B2A7C386320D41BE98C1AECFB75288B94511113B902489FFF157DEF97D762A5302B89AD18546F5C9262C06C98F62632681859D132F756A7008B76CD92C760BB12EB276BC5611B5A1F20A22982AB7616F8802136F54815D66BDF87859C7A79F29769AB4CA735181557AEB9D76468D8855302D4A5E9CC171DC6000C7363789C3A76E59905EF5686C31C765541131E62120399B16CAB27A4677C8E36150E728CE747D2C2B305CB780250CBB1282044B5443DB128695F4A6637102ED3469E3548592D3BE83FB30FDCAB034C1499F69ABDD174E82041FA4CB9B25773BAFCB071BAC2394B6B9E3D5446289A8E08339D91BC12E84ABEE2BA51A563E1DD154E075CC33409609835A8345C86EF96B31F28E3B42ACEAB464380413F475B00CA904E958CB260B186E3C5CFC6068914638AE831D8AA78BEE7B65B9233F8950783973C4971AB0C3BC9F7A7A5176083B4B9B1C045157738C460F77A6D9311A3591789DB0BDE85C3A863594DE182D789A7244E05FA05965A4294FDEB2AF843CAB9F273687E3487C4756B0D441303C9C8B6284733652F074B087288F6BB08AEFA990517C08E9E984A4029FA1C420FCB19FAA78B61C35B4DBA021889826108A1A95A6816DCAA08D535C77959C7EB20209307996592E94DA842A52C329E3625CECC6A8624EDDF7B77C2264933B7B263499AD0930F539C0F24A14A157A56AA469891403BEA0A3531CCFF5A217282652B027513C05D03FE142DD14BD85755507473AC91767C69CBDDDA71CAF8A12A611C0F7F50CEBE87BE9D88514611A9516A6433B8FCBA54527FA10532A808E57979E73C8EB444DB3404D4888370A419E1AB04760617AA0E66BB2E715FB17036D300D9F3480D691450BF0571F875EDFA668D68B16D426668A68BE91F1337AD15577375477045633984AEE80824A08C486F25443E61B47658D03A21A51B085C656664B8345C1149CF690A9B99543B9E98CD090CC4B57C5A1D7BDE6E256E2059B20752E44331486515083DA5A99697A47A342BCD9679C77C6A243896B05CDED231919D0448994ACEC094E082A7434C9A3F6B97F21056CD827AB2648613B3B95CA065E1B3CC3740BC7F4F156650A722FF26D70E5276F53267CCCA11513A719624A2F1BB99677B845A436C9AA3A2D1202F1094F9040813B26BA6DA05E904206DC33A665959228B62BD9391133AB21B79967AF31CA0C727B464A8737535973042831FB06A0B89CA65B6DF8F3983037957DC838CFE0C65D7791D3148A3FB59656472BFBBC8AFC7C6EBBCB6BD83B6760643425FA72FE343DEE61187A73C98B9000E2577990F9109A8B1DAF30BF384227D45BB8BD4CABFA4A9CB20B0B81152BB46281A68445F7F78AA56545B59C75E9852912870373B1CCBB14396EDC70C2C00896762A129B775B4B9168C97F9A259FC22219DDB0CAD614C4F06030140A1F335B033AA2A1753CA5E2A2ACBBA83A3CD65978686FDB8959B4F39CF41B98EFD2048241B69B27AFBD142FD5046AF69AA3EBF67ED1632C4683B88C6BB7B388B4A4D7178A8ABEDF1523A6816F10C40065056DC9393D21F76A48EB7C122331900406D960A4D46C9EF12429B8E4C45C4032F089458818C759584947F6415D1468A6436E764CC86E725A936000B242155CA0A9B884B69490CBD223C76FBA954319161748706CE5A31FB067C20663F443B885E4C5C1A421D589777AA4C13555432AF00B27571D8217B09AD4BC9732C8ED10BB8841315539DA5DD99F9A7FACC71557853FA10547CF7B89E98345\",\n          \"dk\": \"5C9509C7986A2EB53A2BA32392CB6D1C103793B5A8D2938E635B3BC1124B730B63B4A061DAABCD2257C733AA010357376D631178D231E30037625BC6CC9192E6678D815353E6E10519586385F6224526A3662687284AC3734C7A4B5C16FA2B81D9568CDAC1CCE8A01F9BA991D2C2693A2569A9C47E378C591CBA78870449A977C11E8B8117731F815ACA82365D709C2C5E532CB6FAA33D9C0D36551B07C8172AD48D424C818F47C1CEE1B0AC0C80565A2D9D397129557DD2C0372A898A79F5ABAF4B07400C359CDC7A67844AA7530F3ED4A8E9AA9FECE383B37A365FD6A6A980196C4B3AC7A6B511671769A9888267514E01A45A8125264781C9C1B7A7F4143359217A455A6A2A00E1C38FCB174BBBD1729270661752BF56B53AB9D1766D3CCB1F6A537FE37E6C30329FC1C5B2E62BBAE28AF9310E5F773FCE940FBE9C6A0FA302126A98BC87BA830B77B81154BC525FDE8209585B95E810384BEBB6156A6A77771F74392F2592557F4C8556D8689C2B56EDE29E0821304D9255E063C7B972C6DACC860930915411CBC1EA62E11B45072091E7F24333613698F842D5374513A69966A61842509ACCA0445C51366F7AC7CEB0B6681116C8F4BA4AAA256E8C6909CC596F167F9FB1AEB7533DC1F2149EB14A8F6531D3F36CC6A339FD3A711B94067BC4BBA63100385396192216741C8AE5FB955B7B4E80A23049603B7EA2CA0BB2A79800CF533B50F133B345C2344061AF6EDCA4614299701040C31128D99C1B851497A9BC843F537F7D31A16B65490EB1158EC29E4F88B722D76AC1F58C05F3B4E0F6CD9FD69FA86898727AA9E799957BF4B4893A67B624C285E2A99378823AD60AF5D2287BC703A0E377F84539A19C1B13BB5B6199A355728F6B313931B566FC54C592410E81A7506E60BDF4525BCE9C73CDF7C58DAA7880D8AD628A51450606FD0325F0CA3D75CCBCAB41AD7D80391147A57201D0A9FA06DBD13870E4051B297753A8075C3B2C5404C9DA6A20FB12CF45AA745C05A219D21DEE1793AFE144221BB2FFDCABA47CB6BB4B0F3C25AFA5938F5FB566A28601D6783488E2671DF06115C125C0D93AC13A8CA4ACA2404727F6D13194C563B602668D886FC45B3AA8FA357A19BBC06CBAA109B9A47B01D814CA8CCB2815C8AD8F466A936C0659234888B8C55F71C75792896CD9C8E8641F548225B7C7AD388A5CDFF705DF37A1F2901749C48791E62CDC5076E394BA15B26AEF1A38434564088B0D741C11970931CB8C75EEE092A71571F4409F76192C64971F8F395F69DA9F3582A8E9B64B8629743E3B94117B0DA2EBB7BE522697663E9BE12515659AB8811539D22933B7BDA0400883982B430430B1E261A25734D0D990BA819F2C59584FD29EF2FC10F95B05E7C7A73B66C4F6984153C45030F99A1E9A378028A17961465C1C3146A0AE7096300CF966D4320356C3CC1079A53DF829DEA5A711A683AF708968C4811A8725A0397C332B0344537F4E2391AB9BA656075251C216435894DFD97F8F0B63E3E5B48DFA8852C43A30D67475E64F32B9952DC6732E165D1D854A00B72B7D8A44AF57621C7B7FB6B17D8ED597CF825242778A4F5A03AB305EAB2BB9ED27C504816A05C2B2B13A9E41C233AF420FE7BC6E7202B850A24C861C1B0196B8B4C87F2986063C04CB90F56CD3207B93229E197150FD8BAB314C8D71892FA633001F5469F3AAAF694B051F7A1362C3C6D3485077A12DB0052907C493BC6109CE627CF2F953AAD71F654A9167B05D708789EC0100DDD4421237C6814073A5B10E3D1001D18B69A57815D203ADDA9610A7232EEC6C33763B86C4F777F2D7392CCCCF8D6C4297C1111020B11C8258D9B61B26D50E3142A94A6B50C0AB51607B43F6147DC6D45BD1E64A1AA25F2309BA2C525489A75BDF5848D80B6F3087735D55246D9723D3B7CCA2848536F7CECFE6B0FD031B0AD538EBF091B4F674AC53BE78F1A00E2AAE18B537CFDA0CDE143ED8D7217763C6CA2C052775298FD1662913859A1276DD1B2305D3469EE39164D568C1904D89B66CA4366FA3907CCE699D9AA12F36719BC4CA47800B765D392D0AEC702F452EE411271BF945FF58B5E68C7197AA4882F8A5C7C4C11C4A1BB3AC39205775298A9A1A4B677322413187CAFE99C178AC181F0446F9875FC701BA88063589288E73E4BE1AB28C6E3ABB70AAA03F656AE87B38A9EC947786A41D722EC63C0E9E8C1DDA078E28608D11E84219E35239D8030C212F34CC284EEC748D34C6CAB04A7F3C15A5DBA8C74C8666167CA4508EB47774A7C3BCD6589306FC6365D74018D2082DE1753A854EC6FA7813431D31C6A3FA711E03F042E6251E2E27787BEC65AA8CA47FEB5662599083E6944F442716342DF25AA551C36643EC3B9598184431405C8C8EB4AC8360E1789A06B2D13232E831C39A005881273B0086591760AE8A20913D942314F45BA5094AAAA05444469D12C20566B0C008CB2C5FB90257929DCB648E0D616001095309E00FE19235DD29B1EFC2A80E1402E851CD19D2BE9B4001ED214E418C156BB6BB85C0744C3B81AFEA59BBF055C8046386B576BDD009CB412D13B3918B19A658C690726BC93D4928F1B2A7C386320D41BE98C1AECFB75288B94511113B902489FFF157DEF97D762A5302B89AD18546F5C9262C06C98F62632681859D132F756A7008B76CD92C760BB12EB276BC5611B5A1F20A22982AB7616F8802136F54815D66BDF87859C7A79F29769AB4CA735181557AEB9D76468D8855302D4A5E9CC171DC6000C7363789C3A76E59905EF5686C31C765541131E62120399B16CAB27A4677C8E36150E728CE747D2C2B305CB780250CBB1282044B5443DB128695F4A6637102ED3469E3548592D3BE83FB30FDCAB034C1499F69ABDD174E82041FA4CB9B25773BAFCB071BAC2394B6B9E3D5446289A8E08339D91BC12E84ABEE2BA51A563E1DD154E075CC33409609835A8345C86EF96B31F28E3B42ACEAB464380413F475B00CA904E958CB260B186E3C5CFC6068914638AE831D8AA78BEE7B65B9233F8950783973C4971AB0C3BC9F7A7A5176083B4B9B1C045157738C460F77A6D9311A3591789DB0BDE85C3A863594DE182D789A7244E05FA05965A4294FDEB2AF843CAB9F273687E3487C4756B0D441303C9C8B6284733652F074B087288F6BB08AEFA990517C08E9E984A4029FA1C420FCB19FAA78B61C35B4DBA021889826108A1A95A6816DCAA08D535C77959C7EB20209307996592E94DA842A52C329E3625CECC6A8624EDDF7B77C2264933B7B263499AD0930F539C0F24A14A157A56AA469891403BEA0A3531CCFF5A217282652B027513C05D03FE142DD14BD85755507473AC91767C69CBDDDA71CAF8A12A611C0F7F50CEBE87BE9D88514611A9516A6433B8FCBA54527FA10532A808E57979E73C8EB444DB3404D4888370A419E1AB04760617AA0E66BB2E715FB17036D300D9F3480D691450BF0571F875EDFA668D68B16D426668A68BE91F1337AD15577375477045633984AEE80824A08C486F25443E61B47658D03A21A51B085C656664B8345C1149CF690A9B99543B9E98CD090CC4B57C5A1D7BDE6E256E2059B20752E44331486515083DA5A99697A47A342BCD9679C77C6A243896B05CDED231919D0448994ACEC094E082A7434C9A3F6B97F21056CD827AB2648613B3B95CA065E1B3CC3740BC7F4F156650A722FF26D70E5276F53267CCCA11513A719624A2F1BB99677B845A436C9AA3A2D1202F1094F9040813B26BA6DA05E904206DC33A665959228B62BD9391133AB21B79967AF31CA0C727B464A8737535973042831FB06A0B89CA65B6DF8F3983037957DC838CFE0C65D7791D3148A3FB59656472BFBBC8AFC7C6EBBCB6BD83B6760643425FA72FE343DEE61187A73C98B9000E2577990F9109A8B1DAF30BF384227D45BB8BD4CABFA4A9CB20B0B81152BB46281A68445F7F78AA56545B59C75E9852912870373B1CCBB14396EDC70C2C00896762A129B775B4B9168C97F9A259FC22219DDB0CAD614C4F06030140A1F335B033AA2A1753CA5E2A2ACBBA83A3CD65978686FDB8959B4F39CF41B98EFD2048241B69B27AFBD142FD5046AF69AA3EBF67ED1632C4683B88C6BB7B388B4A4D7178A8ABEDF1523A6816F10C40065056DC9393D21F76A48EB7C122331900406D960A4D46C9EF12429B8E4C45C4032F089458818C759584947F6415D1468A6436E764CC86E725A936000B242155CA0A9B884B69490CBD223C76FBA954319161748706CE5A31FB067C20663F443B885E4C5C1A421D589777AA4C13555432AF00B27571D8217B09AD4BC9732C8ED10BB8841315539DA5DD99F9A7FACC71557853FA10547CF7B89E983458E7830EA58B9A79ECA86EC2D5F5589D9A7F30FC06A0E33AFC44CE2717FA011A531E55E9C652B7C9456926E3A720B75ED2D4028057F31ED51E22D1C75FC29DB2E\",\n          \"c\": \"34BCAF3DBA6667162E71A484F74C056A37DB223C1F9FE03CC4246BD9B1542C6AAA6B8C21FBA518633B8824D3ECDFEE9F5981C4E75F0CCEF4E957EDC63BD1A49E5D599A01C5B60D2391D280CEA34637692B80083AF030424DAA91D95A2E10D372B827A0A7214CC74CCD91B9EA4E85D4919CEE6BD08BDC8303317157E3D95D0A94F486F595E64D246EF015A3E2780854B09C9E1C077FAF641DE76218986FE7CF6C8C94C37252C5315C1B1CC9434F286789FD159A6993FB75F3936D4C04602C1EEF033F4E95E412EE772DEBBF4872600981D4B45749AFE498763D11541177031232D4F14143B6053ACEC654F2C9906896E79DC5A5AB57402923BDFDCED57FA49E8CA155EF37012F78B5353484C006980D90DA581410857C152F2E1DEA213B8C28A6DDDED12A782E23858F204CC1BDC84BED3C05F93EF03911342CA7AA280EE850749EDC1A3F5F998505014A824B63BC67D68BCAACFB6511D4BF2EB1ABA077899912540D78426AF13CC0888BCC7807E932445E30F72DABD6E35B8C04D454B4FCEF1E6BD17CD53EF04B363942C1361959AA7305D5F68844C3271146DADF8C588B470217CC10778FEA4AE93A5C5D5B5925AFA212E25A4CC34A8CE84D56DC476297B512EC89BD7DC67FC109B829BF101648D9B7F6494817AFDED31D85F68E5E98F717A2F0D1B6554066DF76713ACB17A1520127223E5E4B59B030EE714C1A9D3A7C4D08B928DCCFF1B53BA776B250DF9CFE6CE299F2AF835D78E62BD3BEC8FCE068858AB1D31C4F371291D54EE4FD87E6936299DB9051884155AA8F7C9A9EAB177E33787FBBA8C5026CE8FE935A5A7944BC6E48352E61314888F7A7EC5F08894829A88A4C955FB4E8D8DDFE4193CCA985A4FE6F6E16815BC524B0CDA186CECC4B6CA23A11330E9E2B9982EB6D3E24B36FDA826633E4D36318B8BDFE6628ADD238BEB63325EFA3F27F93BE9287E1AC9E3FDB6A5B39729961173E65D21F5A51373CC63F24F710064A177290613A172FAD51D607BA075805976F65331F5197307A30F12DBA0824DAE229394757EB5593E1AD31D98E45E7E6B864F7FC1ED00F85891C65C91C9E76A1E5B336D8E3AEA8DB52AF3A36ACD0E64EB877D19887E0B803DCCA831E58F6194F0FC651342A49860803240BC32FB8751FB3F513F43352DBCEBE6DAA56A3DC457F705BEE993BB325A9CDAF929C513BBD9194F4379BF8350F8AD81BC11D3CEFD110AD315E6402F6D01AC5B2B8DA5D0E78832AAE47D1836400B681229A363E184BCD5E45BC8A3F1F3C38FBF7D84C493BC712E1BFC80CDE8C15E53DE6EF27F10D68500EE9D3709E059B8A6BA91589B5F8DBD6E62E127C36BF3C97007E2E7E1A97E45C1ADC60D1B97F8348CB5D88C3592C375B4AEE18648DC8648CE3305E2D055ED01C3830D9E329C76E13E4616FFDCD6773B31BC5502BF35C5BC81193FF0873A77644CAE2AEDAD246925F4CFDF567F6853D45BF1485E8DA3DDB049F39A0CAFC67EBE2D155F41B9B938B0B22B082FB6D2336CAE45E17BBDCDA5E87AE0CF5C8E80F2757A40005DB5071475A183A200EE0563AA483B29DBC58F49C3C322F999E00D185204E448A838E9C63C85777D76B1057F08D552112B636A4E0F0EDB123997B062F1DD1CC79F8ED186A2C6F4DC8B2F1F1A2BD490623CDC7DC7AFBB5CD23708BB45F52C716E5490EDD2BADFE24B77F138C466A6DBA515485FAADBCE9413B6CDE0E7D997B9BEA49023867323B57FB8BF795F272883FB3F3A0D21BF74229158ABE822F1018B5AFDB650CF012D2251F0A4E5BC1974ADE394D77E0E4116B2A11ACA9618B000EFBCEFEB7D4FDE9738C75A05E14461B6064FD5399D260EBCB7B89D2C0D1589C1324259C37ED7C2224A6CF7350849B401D2A6AC909AE433DE8C7ABF0199ED44ED2B569221815455088611AA6C88FCAE75DE832E390CBB9CA075F07C31FE72B9C7CA88EBB6D446B60644CE9BC62DDA18F5C2C95EE2BD4955EF8CB50FA4157396CB156314110B531BC209EF90AC16608A2E080618BBE9BCE24A3900131EE334CC106AF9A1C41E9DC33151EEE2E980A45E7EAA649369585C9FC52CC614C8226F8AB2768285C320BE1FB6E18CC66CD160C66AA2098A3BA33F036FE1C743274F5D9029A8C8F0BEF6834A9F4BA6C0E26DB903DED5519F566B02FD32CDA1624BDE365191738C9C98CB0687CB138DC3F8833948A2090F0F55FA68F186D711D26F1\",\n          \"k\": \"E941F064338BCC6AC1F7679881709DDEBD2A94AAF087EA9FB5021838DACC8E72\",\n          \"m\": \"F594FE1E810814496BC73A1523FA1E0FF207AD5F5F0FD4B232C25EB9F6EB5B1C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 70,\n          \"deferred\": false,\n          \"ek\": \"24155F435909068A815A6853C9F17BD928B0B7891B83F4447C2C5213760678D74FDA8C793C071002040EF4D81BCF676483B85D2AC796F964C9B4F8B7EF77656FA2976BFA65B9E6AFB7811903E114D3060A1A967A1465C512D2C1513863A548A787279E81674FFD45BCBD7C398D567F22D7291F50324D45C9A2FB26DE3A7A7CB875058979FCF4BF19A03A8A721F17E514773A83FA0A939393ADE55CC7176AC984CB43C7092DC2D0A55DEA11F3E41B879B5B52984BE72158B08BC0EDF25866563A56D4B98DF9528FFB28BF345C4D136EA84791DAE11ED324AB279C6C8983AF725818829CCE05770AC3837A2C92AE2B1812D143032C72A32C94C2A06882ECE2CB73C0C41EDA64B607BDBFD9258E03A013B9CB3BBC5C40FB10CE413B11897E81D32585240851E487877C4D5274C6CCD45CDE88978994CF463111A20BBA9905A6114899BDBAAB1053541BB3502D9779DCB48FC8E523FA260F8582B6B8F30232342848492DAB73197EC492407ACDB52844033C9ACBC79C366A4583D98585942B05281328659506D714FF856A9C2B5F370B5136F896BCB87A23331AD0A7610FB69515E84A0A302407FB9949ECCFD91550A8618049A48BA2F4096567A0D383BDEDC705645C4025E0B139C22E6A16023062901B8A2B6B515DC8E06A4D5514489229D926521942BE150B96F2486CB34B09D7E7237A440588C357CE061DFDE1362DF1C2E58C1E1D72235E922B61EB2163D906B5E051BC58497C7CAAF5363724985D0FB3BAE964A86AB36EC46AA3D7AA290B428744623A3DF900DE975E3AFCCCA539837BD897F12CC555D684E34BBB3CD7041FCB2004A687B5E6A3E801CFE9C8C2815C2AC4783042A23736137447337D9221116A62C0E1D8C191B53E22C92A71A15D9D22505FA5BE17221A7236392C0348562216BCB037D4880A6D1236D3626E1B354142B5C897C646BD62CE000C27C53991A1C69176D03A16A61FCA51067C596CF01B89933A258B43C22F489AC70C1D8658B2C0FB2D3A6924A704302F33C25BDB556E97780747CF652C80E87B23479AB2C8119DFCEC47265A4F7D524E50294C826AC4C7B39C3C0782763A1A6ABABB9E14903720A9380939FDC9A99C6766C9D8B2828881BAFA555A35022A23A6F57CCA446319E71780AFB3767E9311D5C990F1E4ABDFF51A0E7238AA98AC412C05356583B05C170BF793CE770F79F81E99F78FD46310EEE27F614226F42367319C81C98A1FE2B2C09F886ED827610C8621B079A5A51863BB69ABC01C4A75051B50D83D1AF85B89F6918313AE5AB0999035088813102BEA06CBCB14656928DCD53A30255B4931017BBC4682CBA54378CB7E118A275728B83A4BD2586D271A8F64C06562248CCAD9A1FFB8265512712D0B707424B015157788318C7086BD964490A11A35FA43BED6ECC533E07D2A6ACC806990072A7DC0322573D6B88E29A7B47CC42508C7CCE64AD436AAE0160309E6922B655FD3B96FEC22CFC7349A746A0012D60A88C17D7934246EA0290369B3F7783FB177534F300BBED06EDCA87786D5175B93239B115254D676C921C1E5C3BA42737AD51B4BA30B1E65A8B59EB6A45C34950E9AABB7E5CD2F556485036BEF93CEAFBA97E83408D61C6FEEB908873B7FCBE93D7E386F2563CB4A80A671A1155762A02968622CE9163284777A3B4AF4B8858392BDEB9446BF59CEDEB689C609859C9B47561A8B39A19F7B36896DE82AF89BC41CE6C54791A78D6258534AB7090C502CE0A6F70CB3DB442A9DCA58E766A2E72286B42BB431B4740B125C73D43F49C1994761A57E3291CE95A2F4B88B3DA31CA994685A75B71CC622B3C2BEF60C222696C031789EFA1A3DBDA576EC1B802C261D1F44A5E4B973AB73946E41159D622BD8A0833396517DD2C51904AB67F05BF5FCBF9B47BF05A18DA600C1AA411B60901096838A5CBB8EA236A0D4C7109BF65EF8E6B2507651F2FAA61ACA225744A1B56354CB945065D6ACFD3697B8D331C0929A59AAA06570B0FD3B02135006DD198A92D4037301BEC0B328725533FC385B2E03033ABC4E1CD4975E438B19D902C3E3435019A42F664C063A5A5E31932ABAC10420A10A89B54582138CE812A473945952460F541B9CE51C9A23C5CC6BA0896249F097218210A329956057E6BE66DB1BD139A791FCB95B3603F12BC19E2876617520522AE31D827CAE8422FAE85C30AF33DBAA77967001910F\",\n          \"dk\": \"A3254A747B57C2065E22233D7D2C1F9CD32B8586CD2FD5A2C1FBA0BF976ED7FB3A36B6710662B9A5AC6DC7AA11D581A0B6E36307E76C9C14128129C30322375381688A6C4A01137018557DCA3184B26091B91AB05241A4A080C813B20686596A5CA8C29188B2F005378E41055DD24D0544C624813A527CC25EB546B30B004B054438E333E5B5C467E08094EC07EBD25758B57FFCF9A958AB343B08BC248B0721A8C17DFC019DA719C4037DAE956F22C41711910513C79F7668B075BBBAA5307AE8131E2184C4164C17BE098E9C9755FCA53C0C781D7889544731A3F24C986C517C43156FFBD35328BACFF1DC436EAC6CB1F92F05231BAEE052D060AFC2C298B5C2877CA8B6BE22C3BA9C3348368DEC30AC7B05A58DA06A6CD1B47B32B709704679839DD873239A8A76F3CB8DABB621AF84BEE644A3C9C348A2B071ADAB3BE40973CD01958778600325CB70B486A7C84A0BD577B313328F792A284B5E1CA4B17F476F24C75C3FC325C07508E4EB9EEDAB0D321B2603582F765C481BF908E0CB574925046C50196A59B35AA282EF1936FE574016E4017F8A9E90E49D20F9279E262CF332CFC0E45E34E22FE460091DEC6DE5EC3A6DE702051284BC234E250B7A63D282215A27DD036149E412DC2C58D8E42E2F8A3C680C9A88B73C8603A53D08509849B379C5A3A6EA6B910539BF420510A833EC6527036139029AADD250A095B56C96A25DAEA70EE8B737366A1A6786769B78C874CA377B652C2BBC93276785D0893A196316F6DBA1248BAB288086D783811A2C1FBB7685B979822306C6B2CCC736562AEF3A19B5A364DD472442B37BC7A363CEC9073872BB4E65667B794A2CCAA319F6C437D2A4EC4A73F0FC5CC3D485837778A3C880005A5FE8890BB42BC88D070E5D7788CED8BFAFB28A07294C81F911A9700A38A880B0A85A97D2A08EA3B0FCBB69BE19A6D330B9AE492D8F3174F83BC22A3A98147621651CADCEEBB3A2D3B18954121D82183A7B465B034454E5390C191BBC063BE47B3DB5381FA2C6862BFC8D34226471E90C45E38EF602511BD4BA1807B95CB72930C537C99B0C1864267FBB206A66A836293F4775416521C175424191CA749A251549F6155EC8883890161BD09EDEA361C3F3CA3C086AAD70A3012C2C196366209264FAC4076E977408A91FDCEB60B8FC01F6B1C1F44C65AE14C3AC366D46F24333B567A9503C8AB0C0B2CC9BDA25400DFA0632058DBDB40309F91CDB42ABD76B82CD5248AE982C994119E08A524D5506395BAE3CBA8EFEA758973C8B87C8B132D12B32A29C75F94A44350413D38F426A57A6609C9D50B1CCC41183BB35CDE7026295777B5A0A5E29765BF80F319C8C7BC9BABF8102E798BF92F923BBD71DE01A28FDD3A0EFC27C52C30B5E7157E90C3E3F3019FA128FDC6934B5E773BEA3935CBB79BEB360B2841D6BD317757805F6E4CBE61012011BCE8E60CF0505A607E757AA96ABD0C75F15A948B45A56A137AB28BB3795FB7192C3501D060E4EE642F9A26AD661490982857E49280D99196899A70DC63926F41A9BC8A5BCA13B3CCA3B2A3998083A9BEFE93CD41A800AD6252D9993716B7AEBE46FB003C850EB86CD045FF963A528558353903FB0A62552C0818A43370EF32CA5E4045EB13035A1793AE75852229825ABBABB591DB2F1CDBED76DB7E5CB74086FA9A6A803D8B0D23B9BAFB112B029721AB70974E6A17757413FE363CDC040CB789562326302F509F317CEC82B489560B2C83C5E3E214579CC76A9140FD9975176B97A8E086E4CF46989C024A16067155971704130D4743BEF8A9E4E63B196142C2F46CCC5FC41FA2009AC1BC25B8A67DDB03982A161F5544AAB7CA22DA70E7CBA515AE7AB46185AF4473E04B593333799C9E31C91CA9CF85187D2D76FBA1B6086B6499C5CC614636CFEDB3A2C226AA0851AA7182D4C4296A2825A83ACA2B0C38E5538B5C7F42A36FC2102F6BDDB5870CA9B1EFD1942861C038B88122FA9929BAAC480B5C9AC6964A8DA9D59ABB52D69A8A5897EF745CB31E5891A1867674BA526A3B79C9405924B00896A97A83394DE4C4539E1B877822BF35968C09188B81ACA2852335AF8A711493E6FD843B7863EC61013B9401AD1B860B6B555AE394A11601518D45C7F089EA746A7852A0D72F2699DD98D24155F435909068A815A6853C9F17BD928B0B7891B83F4447C2C5213760678D74FDA8C793C071002040EF4D81BCF676483B85D2AC796F964C9B4F8B7EF77656FA2976BFA65B9E6AFB7811903E114D3060A1A967A1465C512D2C1513863A548A787279E81674FFD45BCBD7C398D567F22D7291F50324D45C9A2FB26DE3A7A7CB875058979FCF4BF19A03A8A721F17E514773A83FA0A939393ADE55CC7176AC984CB43C7092DC2D0A55DEA11F3E41B879B5B52984BE72158B08BC0EDF25866563A56D4B98DF9528FFB28BF345C4D136EA84791DAE11ED324AB279C6C8983AF725818829CCE05770AC3837A2C92AE2B1812D143032C72A32C94C2A06882ECE2CB73C0C41EDA64B607BDBFD9258E03A013B9CB3BBC5C40FB10CE413B11897E81D32585240851E487877C4D5274C6CCD45CDE88978994CF463111A20BBA9905A6114899BDBAAB1053541BB3502D9779DCB48FC8E523FA260F8582B6B8F30232342848492DAB73197EC492407ACDB52844033C9ACBC79C366A4583D98585942B05281328659506D714FF856A9C2B5F370B5136F896BCB87A23331AD0A7610FB69515E84A0A302407FB9949ECCFD91550A8618049A48BA2F4096567A0D383BDEDC705645C4025E0B139C22E6A16023062901B8A2B6B515DC8E06A4D5514489229D926521942BE150B96F2486CB34B09D7E7237A440588C357CE061DFDE1362DF1C2E58C1E1D72235E922B61EB2163D906B5E051BC58497C7CAAF5363724985D0FB3BAE964A86AB36EC46AA3D7AA290B428744623A3DF900DE975E3AFCCCA539837BD897F12CC555D684E34BBB3CD7041FCB2004A687B5E6A3E801CFE9C8C2815C2AC4783042A23736137447337D9221116A62C0E1D8C191B53E22C92A71A15D9D22505FA5BE17221A7236392C0348562216BCB037D4880A6D1236D3626E1B354142B5C897C646BD62CE000C27C53991A1C69176D03A16A61FCA51067C596CF01B89933A258B43C22F489AC70C1D8658B2C0FB2D3A6924A704302F33C25BDB556E97780747CF652C80E87B23479AB2C8119DFCEC47265A4F7D524E50294C826AC4C7B39C3C0782763A1A6ABABB9E14903720A9380939FDC9A99C6766C9D8B2828881BAFA555A35022A23A6F57CCA446319E71780AFB3767E9311D5C990F1E4ABDFF51A0E7238AA98AC412C05356583B05C170BF793CE770F79F81E99F78FD46310EEE27F614226F42367319C81C98A1FE2B2C09F886ED827610C8621B079A5A51863BB69ABC01C4A75051B50D83D1AF85B89F6918313AE5AB0999035088813102BEA06CBCB14656928DCD53A30255B4931017BBC4682CBA54378CB7E118A275728B83A4BD2586D271A8F64C06562248CCAD9A1FFB8265512712D0B707424B015157788318C7086BD964490A11A35FA43BED6ECC533E07D2A6ACC806990072A7DC0322573D6B88E29A7B47CC42508C7CCE64AD436AAE0160309E6922B655FD3B96FEC22CFC7349A746A0012D60A88C17D7934246EA0290369B3F7783FB177534F300BBED06EDCA87786D5175B93239B115254D676C921C1E5C3BA42737AD51B4BA30B1E65A8B59EB6A45C34950E9AABB7E5CD2F556485036BEF93CEAFBA97E83408D61C6FEEB908873B7FCBE93D7E386F2563CB4A80A671A1155762A02968622CE9163284777A3B4AF4B8858392BDEB9446BF59CEDEB689C609859C9B47561A8B39A19F7B36896DE82AF89BC41CE6C54791A78D6258534AB7090C502CE0A6F70CB3DB442A9DCA58E766A2E72286B42BB431B4740B125C73D43F49C1994761A57E3291CE95A2F4B88B3DA31CA994685A75B71CC622B3C2BEF60C222696C031789EFA1A3DBDA576EC1B802C261D1F44A5E4B973AB73946E41159D622BD8A0833396517DD2C51904AB67F05BF5FCBF9B47BF05A18DA600C1AA411B60901096838A5CBB8EA236A0D4C7109BF65EF8E6B2507651F2FAA61ACA225744A1B56354CB945065D6ACFD3697B8D331C0929A59AAA06570B0FD3B02135006DD198A92D4037301BEC0B328725533FC385B2E03033ABC4E1CD4975E438B19D902C3E3435019A42F664C063A5A5E31932ABAC10420A10A89B54582138CE812A473945952460F541B9CE51C9A23C5CC6BA0896249F097218210A329956057E6BE66DB1BD139A791FCB95B3603F12BC19E2876617520522AE31D827CAE8422FAE85C30AF33DBAA77967001910F38849E1BC23427C122859A98B41D65FC5EACBF4FD851681FEA0ED777FAFFB534B49335D1DEC43FE7888BDDD29CD891FC632616E3B4107E091ADAE014BF7F473A\",\n          \"c\": \"6FC99751A98DB6AE5A504A1E4D3D37A91E7FC63F2D3457F3EACA0BF1045668E13D39B5F93F5EDE0CA55A0F5FBB90CFFEFE9292D8B723235CD6E2C7E92AB086852CCFC31D674BC95FEFD2505DD650FC3EFD43ED787A5BBE009B33E7BC2C47C1377C874796F0A01B9E3F4028C797932E39819EC4B3EDCDDF25EDE63EFBA53935BB421F0C63C1AC1BD0E879DD0B3A47B5A35A5F215158A35A22151EE86AB2C3E9F5CDC387FABE327FC38D0943F1F0D9891BE86F1D8F78D1C29C7700CEFB35FA629CF8120798D9FB27E3882E948C7B3BC08D09527BBE1F4D9D88F3336316FE93FD91D00A9A53F17AEC68E2AD8B335F18330ED608857C3996D111776F9B29855E75F59FAA9ACAB79CDEFA617AE017C0EB7F797206BDA76E1F01AE4C83D24A1166B68A7DDCD3B7B2A1C6D0A470D7120A273758BD4A078F1D851C9EF50C846B246213948F273B045185965028EF8B02AC189135BA3B83904EC978998F1F8A55303057F985BA26A85FA467C4E620C4742A36BE7250ADB5A937CC9355FCDEBB045DA27369DCABDE4ED33CA66906AB0BAD8C6310507BA241F54F37BD6F778DFAAF9B7530574B526D742D74DC1CE917843E27533E014C3A8348D9EF56768829BA4A03FA2A215DBF2C45E2FE06ABC268E6D335EAC3C584B6F221414955EE02BA70996CB27AFAD316D18D90BAAB3DED3C5CE0EF31F3B4155977381E5C76A106CC7BC934B4EA92142E18837930842DFD225FE0E57D32DB7C079C26DCC936C7A526E3A117202E0A29304899D08EF478BAF0A2413AA5D41F5758B22597B11E5A24659C514DB176E7448CED43D5087665E48FA2CBEBD8F300168BEC904E1DC1A39ABBB4558DE1F31725DB46DBD6C847D4BA1E260651473553002688FBE3DCF05ACBDAB5B0EA28DA9D83D6EA5EECA8862799DE09B42AAB2641E84A158240C495EBD6A527DC3840D099EE019F919E9667904D22B6A73EBBE5D5B1A781B9D480FE9915905722358DF0268E1BF3362FEC849C5883C4A34A707CF2C02E4CDA03CA8904659B933ADD46A81171F7155C7275C6035B2B572CDEF3599E5B4779E606F5FA757411BC97D9B46806AE144920B29213D2C887CBD79121D091FCC02B1D922B11D05583F1C8DB8C1CF4092667784BF4B766DD263A5832FBA3EFE2297224E01581AF372511D1C6FBA61D933D36DDC2FC845694B67D34F11F95B8CCD626463F58918D6A99C5D06F502518939C9AB9187F1BA2C1FBF9541B4AC2D9181495B0FE2A3741B556600CCE8B78CA02AD412B62856D7F588F6C49B08EE4E304D145FED15E2212FF167FA48E1D75883504C5CCDB4F4F2BBC3798DC2BFD9A74D747E9C8755127E73EA344FFC110F88164605A1813A8BF1EA4C0EDE38592CBC857A976FC07E64C149705EABA7EA02D8BBC3340AB38C4196FE28DBFAC25FC1A7A8C9E096D8FA5BAFC110862B47282472B8C731A45A7A7EC3BC19ABBC35077824B05FF19B9831D4BBEA9B34F8B28D50E3451D9DCF9B05E0C2330BB78C30A71F774760116894444B37B0FF1E36ABA0D0375DCEE290CF46219DB7D5110D4E51A217C0456227FD342CE67A922935005B3C7D1785FA79C608C149050373097258AA5B36D187B23DC602A970ED7CE5D36CB1AD10970C19AF67AF01BBFCC620D9BFC9C7DD355F274B6D2B585B730CE9449F697A51208FC742261780FBB178AB667FAEDE8422F734FE341DCF8D635DC7F2804735286F83EBB7CFB482685064BDA42E825120CF078F487B76D69402A74D6D4149922EFE4A8FC0837D8FADB1225A54434EBA471D0FE90DB55A3252B902BAAE0D3569F35055F1EDD9F0AA2AEC8DB9D1AE8A5F2D9D6B51D6FA65DEC58308274628EA66CC199CB209372EFD70D1B1DFBFDDED0B3F60F83C069A2E26B0A38F7DE67209E8BCD09FFC17B48FA95CB02D686CCC0E419D9ADCBCF5154CD18BF8F5D13A6FBF083A018F66D9BF28B84CBE9A1C3902BD55C8EAC8D72C8E7B11828D3F9B1794FCB3E6A50F498901DEAE1381E7F10A30244C94EA9F2471A1500772B3ACD0869F00FFFDAC3CB2E8F69385C60D7E8013B3C7D2C358DE712F3F34598A7D1623E6C68373C30F7EE7666FC4C96AB93CC33E4A3839C33BFDCFAA4EA20A25DD3E2325FE93D142A18D3AC06CAAA2EAC6A9CBF67E5E087BBB896E803DC358BB69992A7CE72A69C7C511A85535EF05B4FA790A574E21585C3FBB0B7DB585CFD98076816BE849379\",\n          \"k\": \"D3772614A38598397B21269656EFBA39B15E482299F3ADAF1F82225595A1B7DF\",\n          \"m\": \"ACDF91D5B4F2047AB9C7A8C2F4809FF69B9D480334C501E6BC66D535D309B100\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 71,\n          \"deferred\": false,\n          \"ek\": \"FE456F478002570478BED88055B4567135B19DB8CF52A56E41824984A8BEDB64467D7A0A45654153C8645C5054C0E35C2C75877633BA83AB25857CBE5EA998FC6C79D983B5B1A984FCBB14CBEB483587BC5D952A4BF422344A70C4BB8E9162CB31D2411637526514920CBBACA6A911FB80662096B85194B308B881189813D171CDF6463893CC58EC512DDBFA31920CB1F33AC72C15BE64A3BBD114B27185455DBCCE9BC67AFDF6C8A393CD8A427A73E052FAC64AC22B5DAA7481E7D017B8E14BD84690D9054BCF2557E942BC7248AFAE57911FC64732C450EC3C1CCA599B96DB685EB704764CA7EED07F9BAC7103F38356F86FB9639665C35A576B8627CA6DF0F138FD198405B6371AB1BBB7EC4175F739B408B281D28BCA369AC0F1971A374A97E9097FEC3A5ADA6C2C7714B5CA0958E4848C9CCCB5172F1A6B7F2BDBB37EA346D1C9AC0C31A5822218F9D3544DB6464E605029073DF5F030939C8B7CF84A714904DDF56E63F04B2240376820417FF320BED27D8C385841C54222097742E23B972A6B7491524B88538A07988FFB9861D0B693CC80BDF6BE9B645193C06EF8132813E715A2D72C55140A1A9C54163B2780414A5996489326CF52112640B08166FB9F9461BA19661F1F47B8270396A0A6C8218B636E8496CDAB3639338D50D0919BB7610AD1BF9FF76D7D0B1F0E241E61A6377CD83D430118DC222452426A9259709D91BB46444793741AEC8998F876553B7770E9CC1E3FC749DF81AA4461BF5E6CC1622980968878ABF113CA67B7646A0463604D90353C635C2F2E330F71949E2E21806091319B328731B298E99127777808153652C9C763AB386ED7F6CFEFF10034090D134A642A218D277B6B00511F35A25BB3DA1F299C22B4A3889D10B3754C52D3F1B1373840A973793EC4A5DBE76D2C29626F1BB469722BBB99B003ECAA85545CBC313AEFFA6006F0AB7A857AEA991F9BDBB92E3B75DC300D1C656B2324CCE8DA2FD95C9F510676A8D6AAE77535BE3A9ADF8B1C974293FCCA05DDB31AE1CC81768CA90D00A4A7F8191C127F6B8CA010B46F3C05A808D9911A626B80365E6AE6BCB428BF3EC8710557485DDAC34583B43BF55D257089EC997F857B94752B8BFE7942689A5397F71944343088D6A9A2BA4936B2AAB663857B87278C3293491A75C861987C187655F13FC2A28749E3891E8C236B1B675E250639E5461BFC94F6B7CAB6D8077E4CB8719420DFD4951F76CC6288229EF49C8B8821C3D497CD860C58A619C9294689B5029F5B2D0BC95C370A8C7FA3C521A695C829A9A1F8012D479E38DB4CFE650550664CC9E0B631E2682AABC81A601A62EA16548584A571CE417508AABC4F1970407ACC47B449890598621EA7623530409EC49AEEDA8AED0336DE4BC4D4F9163A3C82C12A2C5D86B24FE56235CCCD26F5AE517367523AC739C6B69E977E3FF2A854F8040DC4B97FB3A29967005D373E0D91166A679A517822F7AA59BD4B00185AA25E0929091ACEA1FA4ED7F7BF7E463005D59403010539465D5D918C1CE22C55A50C749C6B02A360345A0A0790970717986AE9A020564D82DC851AE815D5FC76FDEC1411D01C56166773D7067FB29CF842BA9E8136F755631ECA53D49B7B2B296D65E63D29F96931C02E2AC013C016BB05EA6817F19E1286C3C2B9A803276FB58A916C838E923932895C9961BB3A4BA6ABD52880B86B391035764C4B953684AD13C5918ADA13AFBC42D4B6B1D523B39C380AB8390E6984179A5863AD488897258CBEF615D53281AF99239D4A59AEE6ABD6EA9F72B88E34D544F25A18FB9A45B0BA7906367ADBFBBE3BC467EE7BA3520360DF56040E730C2ADB162F78BA49EA4E2D1969839BA16B2AC991CA439D337CED7153A6E80877B73F6A16A22FE196F5F833ED7C0133505EF9E323C00355D2405B8CA01FAB6B780385121DAB8544C2C609C708193B969701728BE36F5F972C0C402D2E230370175971A601C0560A22789262E539AFB55B4EDA9F8DAC1C63FB77B91B2E77F17D98C0297FB17919CB23E4D45BA538523047A17690C245E009B031469F6CCB30980649A14211997A823C092C98452517A2BFBCBAA0B43478AC399FA9B58FB96687CBBEF715AC3855C493665735192A718965D3825325F151D3A325318A667492B7F6324FB28DE4A16F77BBA3884809A3445D53289ABBBA26997E95B89029457749E9B70D\",\n          \"dk\": \"4FEC81A2301E8E900CDCAB71152484E745CDDEF6C1394A31663046548620E41BB4B96B275AD23D004AC2AE606EEFACACA4815E06DC3F1CE76659E8517CD107F6BB3FBEDAA9F448999544771B773EC5A0CD23E8A090585DDF0892EA7720F35B2AABDA2E113AA85E9CA26EC49BCB6299368A66F2385E7D22884E708ADE3185FACBABFA4904D1378F86934A81217B7E273F47DC67BB91B64EE20DAD457531B4C7545B72AB397CDCF1158150B7A7E3193A322F9B4B0B72838BA8A642DBA31176095E8A338C889A0A8E419D2F2709D109863DA90D3A88C4D95A5C61E805A7815696391EAAC76FAF252B9763A1A390441C70B585A908E8829A60885CE870A50CA0697F56CD63E856FF2C24791A80E7284EDFC8C36580142F757A27DAB8279160CDF4581FC05AE611021C689666FA300281B3E46589A07925923A3F32260FD9B8454EB862A3E29F8F3C5B500AB7F911422DD737A1E117E7329FF88950E9A3549B41BFCAD103A0C7853F7241F48CC9A6C4A45901230C3358021489DF2AB5DCE2B626653B00F62A9533B3394136E934B1B977310638099B79AB29161A13F839294683BF99C031F3B3CA4778F76268785CA85E0580317B8FD225566BA2069409815BF05D21218E517A64DD9CC0140C90B3C05C8E1B9912028BFDB04722F407116598BFC4A80A71576F06918F6BA64663970980B6473845B9AC281915893F5376FCB00B90C323B447518F7A8C7214CEECF85918A97829718BB0D594F429C0BC990D9CC7AE39B73EA737832CC66BA0BB4EB07780CA40B4BA43B8CE54AA90C74F40E57567B1822BF83FD7C7ACE97509B3B6BF101C23CC6B92F5CB36472C1DF4B3B97218C1D3880EDCC6AFBE63A096A279F16785CCB7233D5B423BA35D6BFC383CE12B0A7B4F44C5620A197FD6C1AABE457DB5A274781340FA9CB603D830884BA7B632B0FB56753A5117F0C806FA208F89E1B7F4156DF6B8A0937C1EB0E5148AFB6BA88090E50366161BC222C17464719BC6026EF806B77578994877345D0111D834CAC01362CDA26868957032249AFCB522B92555A3C414B544A4F2CBA499FB918D7079EED7221A2738BF915AC7033A897BC1F8812C64A5271EDCAA29125C9F273578E73CECE2CBE628BA49C4C06450AC06C7BD960B31694A86C6322D7F35BAABF415E43B4A2A074E8CBB945BF9A79453B44B05C4A252AE8240087C97204D1A1954B8AC438A2293690C470384948C094D59A9346A1314F22D589844760C626488381F4CC62E647D4175BCDFB16DDA3C4DE819BCB3A00AF208B7AA2C652996993277932C79697AA3A0A777C56B506ACB1C7EAD291459C995942B3396FACFD5B8951CCA1D64A55CD09B6A3E5772E5B754C3DC065D312A5C41394CF75094DB95A741319199ABBC996255F06F8E52025FB81785813B95D89E32A19EC1803616C2CEAE484883F8A438F40E38735C3FB4452534BA16025553C5C459E9A9E65C6E7E84474B9B6FE17606D476099E2A3861CC1E01611A368ACA50EB2A01760BFBA72CFD75AC60A0C95B161CE759001E742CD8327373E6B507206C798167362C3ADD26216A105094575CADB712E2B3A10DAB8A8828496F3C805F61C0D0087783BB47173C6BCD850BE86B780CFA8463F555ABA49981C63DAA4A1E9550550901A012CA53ACB3B71CE400542C2B5CD6366F84A2D4DCB64F9C6D6B790376A0042A1285E4E52522A090E42266CE8AA8919C036E7C1B8A776CA945ABD96690463A766180942A126876C9BA0DC9961C634E049448B217AFF7D16D5B2C1B20768CC5E73CC88637C217138B917AE522AE3F742165B6B754649ECF7ACDB974B9DD310D69C58DF3A23D01B8A7456C6FE76CB6649C7049EB0634FC633D4C7E99B754C8A679D50861974CC8F5F01059A5B97F40C301C3471768A668D2CECBCA91DAABAAC5E59A80453F846CC3751B0DF8B0701F20BD35C9AD564211E2C3991A5606B823903B981CC9D66CC1E5044F484085800BCBD835C553B781086750675C04C1A1916781D363643932BA8E8C2BDDB65E00E0AC990684EFF2217BB02AE0F33FE520C73FBAAD021346882AC6794B9045213D265103E4C25B1DE68254253EEE9C4B81253987B193794B4E23B67FEA3B5036508F798229BC7C1D8CB476022B616C65674683384787C1ED222D5E5903BA31BBFE456F478002570478BED88055B4567135B19DB8CF52A56E41824984A8BEDB64467D7A0A45654153C8645C5054C0E35C2C75877633BA83AB25857CBE5EA998FC6C79D983B5B1A984FCBB14CBEB483587BC5D952A4BF422344A70C4BB8E9162CB31D2411637526514920CBBACA6A911FB80662096B85194B308B881189813D171CDF6463893CC58EC512DDBFA31920CB1F33AC72C15BE64A3BBD114B27185455DBCCE9BC67AFDF6C8A393CD8A427A73E052FAC64AC22B5DAA7481E7D017B8E14BD84690D9054BCF2557E942BC7248AFAE57911FC64732C450EC3C1CCA599B96DB685EB704764CA7EED07F9BAC7103F38356F86FB9639665C35A576B8627CA6DF0F138FD198405B6371AB1BBB7EC4175F739B408B281D28BCA369AC0F1971A374A97E9097FEC3A5ADA6C2C7714B5CA0958E4848C9CCCB5172F1A6B7F2BDBB37EA346D1C9AC0C31A5822218F9D3544DB6464E605029073DF5F030939C8B7CF84A714904DDF56E63F04B2240376820417FF320BED27D8C385841C54222097742E23B972A6B7491524B88538A07988FFB9861D0B693CC80BDF6BE9B645193C06EF8132813E715A2D72C55140A1A9C54163B2780414A5996489326CF52112640B08166FB9F9461BA19661F1F47B8270396A0A6C8218B636E8496CDAB3639338D50D0919BB7610AD1BF9FF76D7D0B1F0E241E61A6377CD83D430118DC222452426A9259709D91BB46444793741AEC8998F876553B7770E9CC1E3FC749DF81AA4461BF5E6CC1622980968878ABF113CA67B7646A0463604D90353C635C2F2E330F71949E2E21806091319B328731B298E99127777808153652C9C763AB386ED7F6CFEFF10034090D134A642A218D277B6B00511F35A25BB3DA1F299C22B4A3889D10B3754C52D3F1B1373840A973793EC4A5DBE76D2C29626F1BB469722BBB99B003ECAA85545CBC313AEFFA6006F0AB7A857AEA991F9BDBB92E3B75DC300D1C656B2324CCE8DA2FD95C9F510676A8D6AAE77535BE3A9ADF8B1C974293FCCA05DDB31AE1CC81768CA90D00A4A7F8191C127F6B8CA010B46F3C05A808D9911A626B80365E6AE6BCB428BF3EC8710557485DDAC34583B43BF55D257089EC997F857B94752B8BFE7942689A5397F71944343088D6A9A2BA4936B2AAB663857B87278C3293491A75C861987C187655F13FC2A28749E3891E8C236B1B675E250639E5461BFC94F6B7CAB6D8077E4CB8719420DFD4951F76CC6288229EF49C8B8821C3D497CD860C58A619C9294689B5029F5B2D0BC95C370A8C7FA3C521A695C829A9A1F8012D479E38DB4CFE650550664CC9E0B631E2682AABC81A601A62EA16548584A571CE417508AABC4F1970407ACC47B449890598621EA7623530409EC49AEEDA8AED0336DE4BC4D4F9163A3C82C12A2C5D86B24FE56235CCCD26F5AE517367523AC739C6B69E977E3FF2A854F8040DC4B97FB3A29967005D373E0D91166A679A517822F7AA59BD4B00185AA25E0929091ACEA1FA4ED7F7BF7E463005D59403010539465D5D918C1CE22C55A50C749C6B02A360345A0A0790970717986AE9A020564D82DC851AE815D5FC76FDEC1411D01C56166773D7067FB29CF842BA9E8136F755631ECA53D49B7B2B296D65E63D29F96931C02E2AC013C016BB05EA6817F19E1286C3C2B9A803276FB58A916C838E923932895C9961BB3A4BA6ABD52880B86B391035764C4B953684AD13C5918ADA13AFBC42D4B6B1D523B39C380AB8390E6984179A5863AD488897258CBEF615D53281AF99239D4A59AEE6ABD6EA9F72B88E34D544F25A18FB9A45B0BA7906367ADBFBBE3BC467EE7BA3520360DF56040E730C2ADB162F78BA49EA4E2D1969839BA16B2AC991CA439D337CED7153A6E80877B73F6A16A22FE196F5F833ED7C0133505EF9E323C00355D2405B8CA01FAB6B780385121DAB8544C2C609C708193B969701728BE36F5F972C0C402D2E230370175971A601C0560A22789262E539AFB55B4EDA9F8DAC1C63FB77B91B2E77F17D98C0297FB17919CB23E4D45BA538523047A17690C245E009B031469F6CCB30980649A14211997A823C092C98452517A2BFBCBAA0B43478AC399FA9B58FB96687CBBEF715AC3855C493665735192A718965D3825325F151D3A325318A667492B7F6324FB28DE4A16F77BBA3884809A3445D53289ABBBA26997E95B89029457749E9B70DBD81FE53DAA89CC40752B7C0394154498DA83D6C03DF3B7834AB3CB12CE24953093744B72373F405DAB7EAC89D0B66540FBF92B623BCC950B73DC2251610E6A8\",\n          \"c\": \"C1C14C85C884F4FE4CEE2D0C470FB97D54B2A992F7900B8E57025CD88C755895415E67F6CCE90E241F534E950F91CDF2E0A72F59D6A721B6203F7E08BDE5197407F0E79220238A6AE9D6A5F95EE0246E86C35E9F4473D5CF3F59421187DAF3346A513EE8E628ED64F18F9ECAA1968D8CE28BE468C01C0792F5B70FFFC3F2C7AEFE601D8B12B2252E01856579B56AD5E686EF2A3D27FFD75DA7337D7866078F2C830B405450F0233D60BCC06F90612A1C516770A0BF25EE2F59607ABA0704DFBB01C18DE2EEDCE64472A93F417797AA5A0BB4C83D03E283D2A0EA37BDA94B060C5D1EAF854A05DCD60D4A6BF7925E1182F15BA3E9D992534D4C6953B9AE08DAAFBA92802D63E0A67161CD00F0AE3D26645688B00398BDA91F7507B8112D24DE8C7146F223DF6ABA561FD67B1B58CC909DED55FF34EF8478C76195B269BB650C30B522724A46209998DD77C6D55653C39CA608770B8863F2BA3A12EEE891C9BF94E77E95F2578907966B121DF02527E68A100A7E2D528BDC50BEAAB63F70803A61A5F93FC3FF8D1EBB46A96F88FB3CEAC5BFD3AD2434875430EE00EC06CC1F79D4811E7BD4DFA4F07A25052579440AB733BC189ECEFF0F37929E93ACA05F52AB4FAF030E8A39E37974F421E97FEB08F0238AF55F9FD33BA30797651FA5AA4D8D44EFD3F00E1F805AE89585D1A36C13AE8C3059CEA591B69C54835E8A112E45371CF46DEBCB625698CDB3AB1E1712740B3C7C2AAABF543477CF835193944513200AE2FF7D634D6ADC7B947238717C2E313D606A9759CF9CE2CDAD5E271FF0C55FFF1CBFF2391C21D22C0969FA9525F920F0BBE5948B99F1A3E91CEAE892148B443F312B66534A7C4276D88E981BB01956E06E1AF8F2399EA1DFC6FA67E0185575E82E1A4ACF62E240A63BE3AE57D9F93BD1208CF2E36BF7DF24812A431E2966849F8C46610E7889A89DC4F2F3B3221B2A6A99E2811072ECCC7FABA0F4188D33E4A40EA8CA5B48A6644FE367B54C4D2C16B4F3392A8D6399AC09D820E2DD4D9756C63D9AA22D8E591C04E0F2D79D22C1B75EA4F393080C168EEF1F1CF4AE4FB87086D5E6ED067D1764BACD6002E4AE63E250364F09C2B1E0EE6D68E2F3454F541FD58704DAD3DD1E195AB98E9EB91D3F167CE1FF578D7F1E465996F14D281EDD48BFF29DA518678A7D17A7AAF37A4B0A4194E5F4948337EC7B6CBFF9BBFBC90F2CD1B9C778FFD69164AC71F3034A1031C08A669499DCDCC097E893BBA7520888F69CC9F29599576E44DFABB0FC4C3EF5C1F2BB9E5816D51F6602FF8B88F2F3B63BC66D685610BDB30076E6C8F3E99C99F483534B46813E0144101A82972F5C843884E613B9A75B0FAC9A5A64D1CD6FD44BB12233FB9E50183E66815F9CFE7AD7D12C24556E826323F7A2ABBA8036E84DF71292F3C209A4B5D1DBD69FAF65B5860A500AF82E17E6421EA59F11DE2ADB9737C4EADDE3F7334A53DAB3AFCA556762D2B7D0D1565C9952B93BA394C4A0CD75DF1FFE76ECB4DB2D755A29563A7A85258194CF8C7899A0EC125453747CB775496BB172D9CD580396A15487D2A1801D19B899DDAFEDBBC9E8832C0E602D25A4CE237EE79460A069445952BC7476F31B3C67CD2CDDA4167DCB65F09B32B800CB04D716D6D2995BBBC5497085F543AAC81A0944E7FAD8FDDF92B67058AA0C4BC32EDAA9760844C7351D64726C506A61226124C816035A3A32F8F42B9CB808429E77EFDAC4E6731FFE83EBB97E0E805065B25A318A2545EB6B5A7306F687F1144C3812209CDA0CA9F9AC0F66A9C29BCB5279BA4061105BE2BCC187C37A186EF9399B93E5EAFD81C70C91696DBFB0721024145E0018A135EBC7004BA5DD824F5CD95515B8E2AC94C6DF5D07C40ABF03B59CDBD138270B049A640A1C1075E2703DB1B547D8C013DA7E3970E1F8946F3C9112AE214109D4AEF6EE0136576457150E8C54C2056D3688147CBB3C533E1DCC781551477FE4FCE6A1BE0116C5C0748CCFC8179B28F27797F58600A99AD1A3C5595F0A09AA6442A516D122EEE099C60EFD54389ED65389DAD246386C6CA2BD9BB3EBFB0034A3B65D08AE6F10AFBA3B4558946543462D627188EA244AC96020FA9BC43313A46B22259AAEFC421E6A2BFDF450990A0732C8518DA7286AC923804928161D3C8CA77516AB03D2631AFA65CA44E3820295673A1ED5E012DA4294541FAA8964A6AA\",\n          \"k\": \"7E151A29276FA13C06F530A46EA14DC37B820963562AE9069C17A7FAD27C5F1F\",\n          \"m\": \"696EF6079C573B67BA3531CC69730216A3A8136EB6F647481382A5CD93C6B7AF\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 72,\n          \"deferred\": false,\n          \"ek\": \"FD1B79ACFC0D6D79464A3A59A5E18DA6D54C7E272B77703A22610C75FAC3675B89E350047C80C6F5B6023A455C88A44B0327BBB6F40E1D502861F77E7F2732742C88DCFBB539464E3189A5B21ACA30474E3758AA8BD2259987B8390698FD52378DD505D964350AA30B26AA925D83B6C3CB069113A2A3133FC2C19714A042C3056C774805262595CD2504FDE7A82D19878AE208E8173D9A855D39173D027B05CBAC4DE7408A4C0A4427109D60571D98D1738FA09B1D80BE859B34F3268F31A6CC472846C6E5C50F2A3E22CC8E81C78DA627CB2F474CE1EA0B6C02C7F9135129056D05F62D9B36BA466B9806F4ACF09302B135906AF7BFB43020A68444D22168388C8944BA60E7E803458B10D3B061F0652E08607CC2214CFCD2BC0B1B2148BA14691635C40492A4464230F7B6C9AC0366D813249B37FF57AD8533C752C0CE2B765D0319B84F204B9960B8C553721E5C1B93178621D4A16B8C5D2BEC6A91018F80861BC92CB44B25AC1B8C937793892A989D8FF465623666B1E467340984F45A1A591453D894C0E27876F851C664E19C3DBB3F62D7C7B9AC5C4D201FD3C0B3F7597D56A34CABC65F5E62898B0358A803B8A8651E8526C010184EDC0A1008F047E082BC63F0236FB93A9E70750255A28065A32B582CBADB3178F4B146A47893C62AF7CC63DF404715182013F13A11249CE5513D242A2E8B33C4B1B730B59A1B121181BB39449CC021C97C7B6F187966522C4DABA78BF231B945806B590C5638B1767280C29A070C0619A74A6D3D65C704DA99018418266A2AB1D2861B80700F67C6C845744A51220C4743E2A6639F188C95871B44D586F05A1EDEEAB382D123FF731042C29D50096155EB59906A7BDD2881D94081BC330CC3A71D3C1391485109CBD8026744AB57632D0EB35087B11EBDDC24222B4ECB435586C7226B11644AB9A243291CAC62CF3EF90B7991014CB7906D9B455A023FC94C7DC935221C4B2A58398F803801BD2A1BD5B40C17B529FABBC5F3219419DCC911E70FA0397AB407BD73258002594EEBECB45218154196A07A53AEA0ECC56F25AC5184CC39DAB2399B555851B933B45971C85CD3299819A27F756848C33C4E67B6449796B99A914AC8083719D752F06580D328087510AF7A5122F95A02A30190CE72482BF04FA37306A28836AB7A6AB11A2A311C1D9591630FD827431A54C2C738C5AC9CFC67BAC2480763E2CAA1C81E52C9455E305438BAC378D8A4A05168D72C39FEA242DBC794BD11B504E4630487142AE867903C4872F0B9A8F1572403A51940CCEF99618958A71569A8E32992F881B6084354548046F1E4186E11707CA72103DC7292A7858EC77D24BA225819B9574470C467033E35C3FC8A8CF955CD82565576EA576F5C43E0682C50D0260A30043CD40010B2B70C60A86F242EF193C4D3B837F33771DCA999253278D2CA2B9B88473DFC7565F282EED3739967345F90AB34A902F393A2389A557C815591665D63652F43C321ABF0712A012DFA6C04EF86A67F95A16B784E3BA18CB13215EA09CC2B5238F9391BB6A5C6DDA2C6F670A1E58329A8544C1E2C3E5D2A478057A5E46A26982911CC599D83A5A18D918FAECC86217A165C052344A38ECD885AA0E4CBC7D4742C99B1700578D454ABD5C83F587CA48A3A219D72B55F1934BAF1249ED25110177B6B126E0E1379111BCAFFF719D65B5570681164A80AE5E71D677586625020F605103513A0CC94A6632220A5687D89EC887956CC3F6575DF735C738222FF7AC70D468860420D0E53C192FBB31C8B834FD10EBB4B38C1517BC6E60BCC7A5831741DE067274B637060D4CFB3A82624B4207D94355EA3BB562AA9C5B9C04E2182C45955A06C5C7DEC5AD78BC9FAC6A87637BCB14AAC3CA76D5695196937CA34A36F183C599645CD67F33E060C6BB784227C735773534B4D501D25584C0F412A1A697C041C59EB85472ECC1B780C509E3417C9890722E57003A1B5AEEB11A764A6248C724F8770400747F04675C2F54BB1A983C59573EB8C64B39A3F552B79F5D365FC461A0414CA2BCA1DAC961C2E3A0A72109D79E26A59C1B659C4A45E9C8C2882CF6E26B67764244E3BC739349EFA0B9DB47B2152634240CB7A44A0A2B9CC70D557B6849A9976118DD8E74CB7896915368BFD120E31C4A261683DE548D0D6BF829F1E94FCF9E53757DC7EC8255975E848CD84360CBEF3FD\",\n          \"dk\": \"DD5097EC5CB6CEA0268886223C0370FA495D5BD55C93D6B52F74B426A2BF1EBC3A791A6BB6A1B7EF585291E3C4176AA91CE998D73A2359361B20D715D8250385DABF4B4438CD26326A19A651D16817F514A317795496349BE3024BD7AE24C75BE3F313A64151E31BCB0BD533B7388A0A865C41AC4FD2D087AEEB1F2758771433A780D88A6D47255B523A72358BEDB3BD64895039EB2C9434524A926B0291C0D4084C743CBBE35C35F2866DA31C27A5EB1CB87C014B021056507702702D196B34F5B15E11B953B3972123A757084CB50E46B6A03B5F66F41CA7C7720A63C380935A90F9AFD820C1A4D305C7D3929E0C93A5801D62C31F3CD9C92A8C9C61350587F0972AD0C22B32209ABCB16F69C2ECFA89197B27D64507001C71AA22475E851560F046DFBC935397C28A9237D19081983652C80C4C5CC6C5757C8AB5AAA39FC591F88322B851CA8CE5609FE51C1D5B3B6086531046A4688121F86326D03A5E9FBB761B22B67A0CB84EFCB0EDA957B1587F8FB8330AF5BD01581DAAC976C46A9B0684131B75519251CCFB33A6E32322CCC9BDE7A2504A31A7C1642A631B116089BBE4D12B90A761C5D2BE88B58B373351C385467D196A169BB328970B196A3E3F5C0B06FB45A5C84020F37A9A94CA47F11D04B5A40B14C1093C45EB29AA834A3BF275B1483A4762E7434F761456F45F0C3116E78AC4B619B9174027B830882C53A845E9BAF1B750C8F52D1B5A3DE474A57F57BA9B0BA122A67ED4BB13A0C608325295F2E41629312E26674CE55C9C1B1A252988908315A662956AFD73609F546A70D48F63CB0B6650B9FEA1787AF017609B2CBB30B8F605A08BF12FAE63A0BAA37C25054FAB5004C9635CB2508E46E979777A6AB2F412072999A717030CC4C55EC13314E509D22A27BB290BC9778414C42552F88E739570FBEC7A4F87A4E9BCAB909377F5288A9F64AF721562B05CB5D92268614142BB8A9FBA30ADE6B825F8D9B0E2CB686AC9AF7EBAACB0695F24C1BD29C9CB1C5CAC6C9778FD8C45AFE6B6B3D32CE07570B2A804A3F111F07678E141AB27F12CB375B1E329974FCA796E2247B7B31B6AE23372978D7F7BB7A278897C25C7138510430CBB6F23BF5AC1B9B124B5E546472C80877D503906841547B45A72A319537C1E6A335B3E71AC04DC01D4A678F7C75538590120D2BCB0B767AC601148B12D66F1AD59A6CF4AC125C229C4C7A75DEE611862A47FD82A049600ACA1266BF4B06BEE3C6540E1796E23AE3C11317C8B158BC814F1931F2E68A4281B7DC0F48344E50FEE201F2022BF7E48C79B89AD917B3F6795A8E6C0A3F4C795467983EE9457CE57C22C18B65CAAB119BCBF10328EFBE91A9AB8765E989C190A1108799200552545706964D045FA54478CAC50DC1044266B70298AB27231592B2675E09705EBEC47C64868C26A416D6C97A1D87563DC56FD5A3715B45E063A632BA278A4C11B3ED76557782D1F3640EE305238842D5D8A0251021D63F976696516970CC6D8A8214CCA2F5A265BF2B5AF145A8109413147E88E9C7926F7C668B38289BC6556C10334DC3438904212A02B868F0B5773C088BEEC7F1A914DE0520260E5823F89935103727CF8325206963491335ECB4BC20ABB8D76481F0703BB077881958F5E55274BF9A149C11B7B92B4BB63B8BAF06CEC031A8F899A66E28E9610837100207DF7155B096A33BA1EC3A779634C3F571AAEF3B6B7B97960E268685466CF06E683DD6586315413FDAC5303E664330AB575832C394A084C65BE3D41BD533C631BCB411C718145C264F977435B51BFA8A7C2999147792493B8DB0892540B93E27E452C47F7E57B1DE09C04519F9CB1023855A06B2C76F5252FEBFC1527D55DB1087ACFB7AEA532471AE14449F843B47B96B8C06775F27642630DEEE6877DB17D2BE4C101950332614B4A5040FC74C52C07597C1B6C9D46443D68A2652C2FF9EB57E4F4B247CC31D6E5C732D9083A985BE9752506C07F02CB868004CE0B3154FB5A92F6F5A15472A9FDE333AB1B0F08386397F9621566790BD8AE0CB89763F2CE74495D1A54A800E657A8D42613B12A8F3A416795359AF55E89B28D2D18A3E8B110DA297D33CB0BEF9C14D4058AC8DCC633CC682A7C3BCF10BE800BCBCDB7CC773412EC643DC77B5F5CF0A87DFB25FD1B79ACFC0D6D79464A3A59A5E18DA6D54C7E272B77703A22610C75FAC3675B89E350047C80C6F5B6023A455C88A44B0327BBB6F40E1D502861F77E7F2732742C88DCFBB539464E3189A5B21ACA30474E3758AA8BD2259987B8390698FD52378DD505D964350AA30B26AA925D83B6C3CB069113A2A3133FC2C19714A042C3056C774805262595CD2504FDE7A82D19878AE208E8173D9A855D39173D027B05CBAC4DE7408A4C0A4427109D60571D98D1738FA09B1D80BE859B34F3268F31A6CC472846C6E5C50F2A3E22CC8E81C78DA627CB2F474CE1EA0B6C02C7F9135129056D05F62D9B36BA466B9806F4ACF09302B135906AF7BFB43020A68444D22168388C8944BA60E7E803458B10D3B061F0652E08607CC2214CFCD2BC0B1B2148BA14691635C40492A4464230F7B6C9AC0366D813249B37FF57AD8533C752C0CE2B765D0319B84F204B9960B8C553721E5C1B93178621D4A16B8C5D2BEC6A91018F80861BC92CB44B25AC1B8C937793892A989D8FF465623666B1E467340984F45A1A591453D894C0E27876F851C664E19C3DBB3F62D7C7B9AC5C4D201FD3C0B3F7597D56A34CABC65F5E62898B0358A803B8A8651E8526C010184EDC0A1008F047E082BC63F0236FB93A9E70750255A28065A32B582CBADB3178F4B146A47893C62AF7CC63DF404715182013F13A11249CE5513D242A2E8B33C4B1B730B59A1B121181BB39449CC021C97C7B6F187966522C4DABA78BF231B945806B590C5638B1767280C29A070C0619A74A6D3D65C704DA99018418266A2AB1D2861B80700F67C6C845744A51220C4743E2A6639F188C95871B44D586F05A1EDEEAB382D123FF731042C29D50096155EB59906A7BDD2881D94081BC330CC3A71D3C1391485109CBD8026744AB57632D0EB35087B11EBDDC24222B4ECB435586C7226B11644AB9A243291CAC62CF3EF90B7991014CB7906D9B455A023FC94C7DC935221C4B2A58398F803801BD2A1BD5B40C17B529FABBC5F3219419DCC911E70FA0397AB407BD73258002594EEBECB45218154196A07A53AEA0ECC56F25AC5184CC39DAB2399B555851B933B45971C85CD3299819A27F756848C33C4E67B6449796B99A914AC8083719D752F06580D328087510AF7A5122F95A02A30190CE72482BF04FA37306A28836AB7A6AB11A2A311C1D9591630FD827431A54C2C738C5AC9CFC67BAC2480763E2CAA1C81E52C9455E305438BAC378D8A4A05168D72C39FEA242DBC794BD11B504E4630487142AE867903C4872F0B9A8F1572403A51940CCEF99618958A71569A8E32992F881B6084354548046F1E4186E11707CA72103DC7292A7858EC77D24BA225819B9574470C467033E35C3FC8A8CF955CD82565576EA576F5C43E0682C50D0260A30043CD40010B2B70C60A86F242EF193C4D3B837F33771DCA999253278D2CA2B9B88473DFC7565F282EED3739967345F90AB34A902F393A2389A557C815591665D63652F43C321ABF0712A012DFA6C04EF86A67F95A16B784E3BA18CB13215EA09CC2B5238F9391BB6A5C6DDA2C6F670A1E58329A8544C1E2C3E5D2A478057A5E46A26982911CC599D83A5A18D918FAECC86217A165C052344A38ECD885AA0E4CBC7D4742C99B1700578D454ABD5C83F587CA48A3A219D72B55F1934BAF1249ED25110177B6B126E0E1379111BCAFFF719D65B5570681164A80AE5E71D677586625020F605103513A0CC94A6632220A5687D89EC887956CC3F6575DF735C738222FF7AC70D468860420D0E53C192FBB31C8B834FD10EBB4B38C1517BC6E60BCC7A5831741DE067274B637060D4CFB3A82624B4207D94355EA3BB562AA9C5B9C04E2182C45955A06C5C7DEC5AD78BC9FAC6A87637BCB14AAC3CA76D5695196937CA34A36F183C599645CD67F33E060C6BB784227C735773534B4D501D25584C0F412A1A697C041C59EB85472ECC1B780C509E3417C9890722E57003A1B5AEEB11A764A6248C724F8770400747F04675C2F54BB1A983C59573EB8C64B39A3F552B79F5D365FC461A0414CA2BCA1DAC961C2E3A0A72109D79E26A59C1B659C4A45E9C8C2882CF6E26B67764244E3BC739349EFA0B9DB47B2152634240CB7A44A0A2B9CC70D557B6849A9976118DD8E74CB7896915368BFD120E31C4A261683DE548D0D6BF829F1E94FCF9E53757DC7EC8255975E848CD84360CBEF3FDD737B4854C1D79C36194DA7346217580DB481C0DB2116D4DC3A296BF64A89F466B256F764B817B1F7901D0B12CDC08EF3C2419B0A23EB25ACBA70917A39F3171\",\n          \"c\": \"163F9FDECE0DFEC9D2BA04A7CF3A72A3CEF584D0F4CA5B041B72AA48068C21D474A61C0AC96725C657E6BA96210929BA18C5192AF13724E72F4CFCB551F6D0C2A59A4C4410D284D45077AF6C3A7911D0D0B4534409EC8C521C2C8BCB8B14D4901C0F8C85FD3CCABE31B6C5784F4818B2A195B9B837DEB60D739C608CBDF9E13C06B7F1132F6EC0A4823CE42B00079A19F1A81269BF26820474F0C0CAA81E20FB059AAF8E11B51741B5849A87FD0349CE05FC37759B61D191479B391CBB4EEE04908B7680E047232DA268EE4388D9D92FD944689D6BB8BEB9C4FFDE45FD77435E6CDE430D34C5D50C107B963D8E1AA79B0DC4633F31B79B5DAF009B76ACE6BD277F5F71EBF3B08A4BD511E26B8B1291D23689818E0BFE4DCA6EE0023297926D777F44B1A3DB409A5013E366118B98571059AAAF40FEB83E660894E11DEFCEB4A08CBAED1C17CA20836F81F78A128D42D94B3D71B010AFF7818FD2FFE7D34FAA458CDEEE897E79DD9D8632CA772303EFBAAAE1591810823BBD57BCD3858B37673436FA41D89D8219DDF163243FD773A40D1402CF4AADB34AB4BD75FA675DBA29B69A7C464372111EEEA8263D05ACAB73BA1655556CCEE0057417DE564B7C3CAA72DAC1AE5936E75EBD9B30671653FCF5C4C007ACF7C076688815492189D4BD7B76E6BDD2E4C5372B963DEE5DA9719C57B228299C4DE44D136FD5E125C9781CDDD9DA18F58AD586DCA62DFDBF3683AFB7BD5AFCFDBD0E5238DD89EA6ACEC0EBE39103DE643D017C67576017F550D7C6047AE2AEC3C1E4EE85D33C2B1C33AB551887C035BC437B7C5DDC5D06E08C35E1F46502CDBE630A4C59C14309EB2921D8468713246408FDC894C993FBBFAEA06A4CD8D8AF2E86A672E8763AD4E4003CD4379DBB1DB93B08CA8BA9B16BE4D039623DC264C6E0C730921B9B42CD665F7CAD4462B233DEB6DBC0A9CC5969D98D8783203C3E3004D29ABCDEF33E83D8FF5DAEF548B2AB91F558D23B2973A882A41B85FFABD54F908A79A8A0D2889BA54195405D8C24DB03EA21A92FA9B733EFB75480E8E7757F60F0E58433E599D9B9325368F1135FC3EDB9791E4A25D9A714F37970ECAFD78EA94582BB370F405216D4F22998AEE28D0FEB31FE42D2F533D5F97BEFA759F25684C1714677CC96E23668F697D2C39087A50A131BF8AE98E5618476464283F6F75DEDB24D7174D090C3E3685F10B6D90C423778EE272FBEA69D53C0BEAB7024A4D64AC85B147F9BE83EF3B26DEB5C3843131BD4BFB24CAE9564503F41197DD33355F0E7C7CF069A87B7D68B5BA0343AA5E6B9BAFE6C93D1894241C86F9163353B2E79C28ADB24A7F052A21BE8F3B3807AC16E66AC3B04EAC1D92ED78DD2468A58599DF566DA1F052DF6E7786F38EE15510D7491B1AFD87578C42075103C9EFCC49362AEAB9FB8F6BBA88506B32A536A5D535BA449AF8AA79FC4418F6A43A0289899CEFD33D93685FF4AA2BF413526D06DBFBE5BB95C6481A61755E961B2CFCECA472BE408EC95175A942EB27F21212E7C8E26F40E241650460471477B600A5895D934940C8B86AF54889C7D42454D8E40BA689FE02C7653AA7F6A6CBE16DA3C81BB6653B20B2AD24E172073C38CB3E73C192D7C6D0AB2E7DA418A3DFA2C6A45C53A8DC81EF0FDD349E91C2D62B1B38197B2AB41C8478B6F551437FEFE9BA127F19BDA0185E63E9DBB09BEB1519DE53A4811070B8E89F447D6639E76D4DAC5719D64A22B161D9A13D3FBCF707C2FEA9E92DC675B92F0AE208DE93E49D2015A33249CEA4B8AA236E4B27C3B4111B7E62A5FB4E841746AFCC5E37A81A7C501AD8E1C725BAF7C358848B63FF0ED9461ACED678DD501BA506C5376A69BA03258F681CC343BEFAB1AAAE513AED9910AF6A5970CEBC113532A18BF94366C280A992DAD57F2E57D6EA6DDC5AC1A465B1D5CA8EC6016A592B5CFB240D3C11532F0DAD89190E2B2B597AD12A4648EB605A69F2B34BFDD19F092738CAE74E5182A9E2BB91FBC5C87116F10D8DFDD751FD68806ACC64BED13D832873EA674D70755C89F60F1C0046B182D1F5BFE5C851AF7707A1B1A2E2C3A8BA52088454C8798D344F29283B006DB102910766C9CA33F405CA44207057479C4BA23D11C564D21D3DC98D6AB45C57B969DBF42513E8D589C0DFD602922CD875B6D0988527B9EC39917BE456FA0D8339999E68E51E07CDAB598D1B8\",\n          \"k\": \"42D06DEDA4AB863ECE98FCA1C9C3E5CCCF7CB03B8411B381B028034F12B282F7\",\n          \"m\": \"B2180DB6D5A468155A4C45C90495F8875538F05B8B8587644B4A668CC8936447\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 73,\n          \"deferred\": false,\n          \"ek\": \"5AE08BEC33AF8C2967C72B389BF868211B80B3F3621279CB84739860479BE408929695B043BBC623B13B7313494111C07EACCBAEDA248DF4BFD4E480526958FB9811FF98CCC755231045C744D29A6A3103012A3C61613C3305C0A424C8F483362B5150A045888AB09E39F08FCC81B79005C1B3B0BA5FAC23BF438048C675122ACE4CA672D4D6540F9064AA9766937017B147CB05111C0706A5B549498899423B1000A27B648F0BAB03307F8E492D582723BB20B044E91CE259C611F71B7BB57E90D4A249D02A7892B4589A63EB591CC559A3477766776505689820601B9CBD75AFC344C43FD9C9FF55AA4C791DC0188B17177032796E07368CEA357874882F32948EE0B63D589C808F833AD2566E22FB02C77AA78767B194731EBF5728F1A858C6C85102B70C56B96D32428364EA7A904A7EC4F2517AFB2AAC817AED1A53D701CF16168AC2E45730D57F93C12B9CDB38D602025B1C53E4A1CEB4EBBDC9403DAAA368BBDC9BD9858BB0B53945948CC3DBBD7B2B41A97B847447402C5624D30CA0738912BA19780BC5C81A7B9E26C3C2E155CF5C1A58D26413988267738B848F20681B823E9828BE7A6A8799A1360C2298F448BFCB5C422ADB3D6CE32D8FFC1A2E52244DB9C994416F15C3B5433319C04559E3C3B895B1B322232A69029AFEF53500AC5C87A4606F5268BFAA26FD99B45C456D96E1B248BC8D2D79CBC9435D3A8B962E91A81FC76B1670ADFD12B3FF044078A72BC36C8A435B966DBA5C0CC97B71635206CCCCE2972B80C818DC43325830A02C6482CB5B2079B5AB6AC50C24E95D16C8259945AAE9D45CC66B1F125B70915C5F222670E54A5B8CCBA4F973BFCBD763BF2BCACFF5AD51A9255481925C893C630B38678A355EF40A39F56570D961846A2E6CD827CC1BA4B3813B6B956CF4AA26D7E9CCE1721E04B9A5B8D4910CDC484134730B639B57B39CB5F69663EC7490B339C61B4EE4AA83393ABEBC60868B945CFDE98933296793F74466320615F03DC4222F69461DBEA4458E2B6E1573CFF4F610F78C3AB609036641007EE369BBE382ECDC888EF97C1E1812B87964EC19B04DF5194B4049DFD2A87B63034F7A02C3BB731CB67F7282B8DCB131146570AFB993661B059797799CEB7D081CA13421BE41B32ADE2404B0519A00B88A0E088E29F846F5B738113B31C680BB96AA736CEC1138CC5F5D9C67CBAC9B29E1C1226812E1BAA39F151BDFF15037E30E1D3B9CF86423131A4FBAD95FA0585015485B5DB755E0F45D129841EB7614D6C7AFF6194061DB727B299A30238E8B716C72400828B377D46B3F2AE9119EF72AB410C97CD4B31EF77A610242D3ACB17CC2B335C5B2DCB3759A4255921096087CA0ED428AE6703CB15A9D9D8A5E0D64B85534AEB4F2076130848CB450EE55954AE61F40CC25932851A2D80FA3601C74906F1A5360A69120A5FCB4793326F0C8222969A40FC16D46F1CDB318BA511C72AB516A4F31CE67204938E68EB4F16202F42D9E095F1714BF2719C7E571498CE304DA997FEF45549B5BB9F3834209096ABB39C686E4B2C9C796BA69B368417A45970178E9801221944EAB90656B623B53903554AF1F0C7644D79B2BC186C0EA5C791795F99A0B9C29346CF9639EA892935878E6F2AAB73CCEC7201D38A26305A286BB1970BCE86033A14D3677AFEFC81757A69C07299164790473055F10A8279357BBEB31C173B87B62293BF963C453988B390B3697A3CDE4EBBA5CF1B08F78CAC7498DFBB6407D6220C376A4799AA3A8871D0270870B9495499C88C98A5762F7CCB6FB0CA10046F06C06864345CF4C5B40B11181A973E473ABB9786FF307C65CB7048FEA4885041E65E0013422BCCFB65BE8CC9CFBB4448A645FD3F81779AAB29ACA1648708F249C8B3F9CBF55C736F5746854D89A923C1CFFA781D26C241616CD01D7B372F568B50BCA58D88D01FCBB23931DE6DA2D688A6751158971A5B19C6B46C3B761D8385238F8B9831266B09B16B5559B5EB82F64B7133AD36C5497C9BBA4AC9DCC094C3673691207D3587C97197F5CF7C1DDAB6A9F1383211639F3C57BFFF3B01709974C79C9C51B2E29E91770911A52281F030531239CB049BBCD841B1D764C49E0E8492AD383A093925AA818FC0CCEA049271F307C51F3C97A7AB5A81B43B52A6BCE2CEDB2D0E706D280DDE4A0991FCAF55CD36AC05F3593F3C797A9DCFD0FD0E0\",\n          \"dk\": \"3B052C99E38F21B24FB9461F66218E682A2343807AC6B0BFA79B8E077257EA63B5AFEC54AA9565D85536EBDB036E82389E228ABF62167E1539F31A5CD19C87896C2A31117C180C954E8BC5FCC63ECDA30762383D7C7990F8C1689BF90F28F460C66AB468337353838D6B675CFE475C4FDC6BCF3112F5B4A142A439D64C557F0075B8DB9D059AC883A1548B031CA8EB58D2930A47A18A0B2518DD6B56A8AC961CB41B1C7A9DF2479E3E43C4A45703C2C66103A0199BC09347CB5E0D700E27B61024C45B28C3B9112A899855CF7C77B1B2D3237B6A3E36157E888597D87C07D7885A89A257AEC29369800C9DEBB1EC5A188DD367A6437FF3B97EF3D7201589987C3238570A8CD1F9089591068F572BF72954A07A160380C2B4C23DD9381878BC54140B101C27873356381EA7425D3C83EBE93C4CABAD33B292B15B68F794816175A5B69B36DD973966CB9EDE35C4EF46326CAA08B95C961F693D0B499103BB0CBB42CA25E08607600438E004E5E373A994A0883508BBFA002566BBB3A79269CCC9687434B22350A4B0720C549529298CDB3126D8162FAFF29ADD11154A73A1B3A4BE3799CD53C533A6A6331C9843E83678B2226F36931424367976211E834A50C0101A6B9144BBE12522B42B660915812C6D07204EF0934B258711E0C50C99A0979CA7A765817AB57A75264B336CD8AB86DB894BBB3414726466A7857E92509B8BB39C67315C205BBE19321756B6A3C925910A1CDA417AAE4B268B8C771D311DF152696BA0659384A0D5AB2EF5B7AED2880C75C83CB5232A3BD126668307BD725E901A77AB46855DAAAD109BB6BE8A3AD49755FB4450624296BAA08BFC48CE9DF0BE6A00016C966D1B6A8C6E959A5C8ABF6799B63F2280CA1610971C046C960C691183C233A1CFA889489BCE2DC927B6C388BCB74EE1FB4000EAA98D081BF2F95907A10CE2D4548B91159EE877C12A775538508AAB9A6BC59201B3ABE4F7539E34C56BFC13CC559F80D9A1796B81F90308AFF381B99B686680990E9CB7E830AAA8612862C1C0DC0AC74B3016E3649FA5D7B8BBF870C7459BB34213DA480D33C56C170C71C912B78DD4264C979ED64098401A96F353BE82EA241B200B7094B60566B1042A39B94120B053CD0F56644EF8BD88079BF8A935083A94D169C765014917A391A0A780CA6C9C0CB88CABE20928D2CF7BAC04589B108E0C7E55A64070C52EDD65CE50EACE1DD63B10471266B1547D79C919593C36A10894320DEFD85CEBD12647E984B48C259F59B398286142F0059726408E56538F5C876067821685C1CE3C8F05006BAFA48718D54295B64BBF82BD6DA9403A2AA5E9F21380756A4AF71F9EA1CF99F31F22BA7513609AB3ACBCFDEBA09D5C5DE45B0D83F13D0557AB627805D02588C2E772351B2679F86ADE56873C509D6F15120BE86BEF908BFEE629DE3B221D4B03A9909B6B722181085B82C087BD4C985601C6EF1665890054E90A897CFB06C38314F2766C7BF31E4664C0EE025EAB2A80233AA1533032AC809718A8577F1C2D968475E4A0BBD25514A3B27C68EB49775ACBDC22377A920948F2A7B4C78F17520273E4206AFC8E6B580BC597CC1773CB34963EB7726C55B7555041852075A7C06602B9D717EB5574823352FE49134D972EC7B2532A273CA0EB5540B9CE9C6A9CBD1B3079C0BE96B178CA71593DA4A49A52B287144C962B28063292EDDCC84C13A6F7B0B26F0B252E2835A7694AB7B8C6F8B132448583F1A27F8AA9A86224648B05551E332187B296F542CDF746754B13B395B16B451625A032AB7C044AFC6C8EF548321183B695A8CE05E29296804BA0D96902B98ACDA81E98B996748385796CC3DFFB46026A434A71428531C0D7F5C05B877815265B989A16BBBC928DB6CE11CA3249FC3410EBC8EE088B7053AC44090D050A1DA73275118B9D3272AD38234A294C6A52C98AA1CAB0A2089450BB7AAC635D55D1A9099A1FC951783078AD83B771135836FD32425C16C8C5EB259DBB0F27840BA574AFEFB52ABC6091BBC8AEF6D6684B02BB5414BB39BC65378B715A492E0BFA13ABE7713D401EE930AB98E5CEE7DC3C12F1085C528DB8121552D95E7A081C6DB34DD616BAF3D0427E0CCE01E5A804DC5A100958F94C89A2FB1D88ECC13B702ADD75B5847234DBCB015AE08BEC33AF8C2967C72B389BF868211B80B3F3621279CB84739860479BE408929695B043BBC623B13B7313494111C07EACCBAEDA248DF4BFD4E480526958FB9811FF98CCC755231045C744D29A6A3103012A3C61613C3305C0A424C8F483362B5150A045888AB09E39F08FCC81B79005C1B3B0BA5FAC23BF438048C675122ACE4CA672D4D6540F9064AA9766937017B147CB05111C0706A5B549498899423B1000A27B648F0BAB03307F8E492D582723BB20B044E91CE259C611F71B7BB57E90D4A249D02A7892B4589A63EB591CC559A3477766776505689820601B9CBD75AFC344C43FD9C9FF55AA4C791DC0188B17177032796E07368CEA357874882F32948EE0B63D589C808F833AD2566E22FB02C77AA78767B194731EBF5728F1A858C6C85102B70C56B96D32428364EA7A904A7EC4F2517AFB2AAC817AED1A53D701CF16168AC2E45730D57F93C12B9CDB38D602025B1C53E4A1CEB4EBBDC9403DAAA368BBDC9BD9858BB0B53945948CC3DBBD7B2B41A97B847447402C5624D30CA0738912BA19780BC5C81A7B9E26C3C2E155CF5C1A58D26413988267738B848F20681B823E9828BE7A6A8799A1360C2298F448BFCB5C422ADB3D6CE32D8FFC1A2E52244DB9C994416F15C3B5433319C04559E3C3B895B1B322232A69029AFEF53500AC5C87A4606F5268BFAA26FD99B45C456D96E1B248BC8D2D79CBC9435D3A8B962E91A81FC76B1670ADFD12B3FF044078A72BC36C8A435B966DBA5C0CC97B71635206CCCCE2972B80C818DC43325830A02C6482CB5B2079B5AB6AC50C24E95D16C8259945AAE9D45CC66B1F125B70915C5F222670E54A5B8CCBA4F973BFCBD763BF2BCACFF5AD51A9255481925C893C630B38678A355EF40A39F56570D961846A2E6CD827CC1BA4B3813B6B956CF4AA26D7E9CCE1721E04B9A5B8D4910CDC484134730B639B57B39CB5F69663EC7490B339C61B4EE4AA83393ABEBC60868B945CFDE98933296793F74466320615F03DC4222F69461DBEA4458E2B6E1573CFF4F610F78C3AB609036641007EE369BBE382ECDC888EF97C1E1812B87964EC19B04DF5194B4049DFD2A87B63034F7A02C3BB731CB67F7282B8DCB131146570AFB993661B059797799CEB7D081CA13421BE41B32ADE2404B0519A00B88A0E088E29F846F5B738113B31C680BB96AA736CEC1138CC5F5D9C67CBAC9B29E1C1226812E1BAA39F151BDFF15037E30E1D3B9CF86423131A4FBAD95FA0585015485B5DB755E0F45D129841EB7614D6C7AFF6194061DB727B299A30238E8B716C72400828B377D46B3F2AE9119EF72AB410C97CD4B31EF77A610242D3ACB17CC2B335C5B2DCB3759A4255921096087CA0ED428AE6703CB15A9D9D8A5E0D64B85534AEB4F2076130848CB450EE55954AE61F40CC25932851A2D80FA3601C74906F1A5360A69120A5FCB4793326F0C8222969A40FC16D46F1CDB318BA511C72AB516A4F31CE67204938E68EB4F16202F42D9E095F1714BF2719C7E571498CE304DA997FEF45549B5BB9F3834209096ABB39C686E4B2C9C796BA69B368417A45970178E9801221944EAB90656B623B53903554AF1F0C7644D79B2BC186C0EA5C791795F99A0B9C29346CF9639EA892935878E6F2AAB73CCEC7201D38A26305A286BB1970BCE86033A14D3677AFEFC81757A69C07299164790473055F10A8279357BBEB31C173B87B62293BF963C453988B390B3697A3CDE4EBBA5CF1B08F78CAC7498DFBB6407D6220C376A4799AA3A8871D0270870B9495499C88C98A5762F7CCB6FB0CA10046F06C06864345CF4C5B40B11181A973E473ABB9786FF307C65CB7048FEA4885041E65E0013422BCCFB65BE8CC9CFBB4448A645FD3F81779AAB29ACA1648708F249C8B3F9CBF55C736F5746854D89A923C1CFFA781D26C241616CD01D7B372F568B50BCA58D88D01FCBB23931DE6DA2D688A6751158971A5B19C6B46C3B761D8385238F8B9831266B09B16B5559B5EB82F64B7133AD36C5497C9BBA4AC9DCC094C3673691207D3587C97197F5CF7C1DDAB6A9F1383211639F3C57BFFF3B01709974C79C9C51B2E29E91770911A52281F030531239CB049BBCD841B1D764C49E0E8492AD383A093925AA818FC0CCEA049271F307C51F3C97A7AB5A81B43B52A6BCE2CEDB2D0E706D280DDE4A0991FCAF55CD36AC05F3593F3C797A9DCFD0FD0E004FA8E0B4695D94C8D670F7E010B0562E8AAA1C46B3EDD2CF2457F39E36967B6F7A6D1FC80331296B8B5568C0B506ED3EDD6DE5E81E0F76F63F7297FB41C2CC8\",\n          \"c\": \"8C4E7C59965B9CBF50961819684ABE5DAFC3AA381F779869ABFD60C263407D4C03CE96CD714C0A62532CEB8E460D2455B84374F4EA30FA5BCECC77EF9FE108AC3EBD24AF4E60194E8FE7E50152AC312B50F9DFF28AB23FD2C9ABE4C970E7752016C84093F9D8483E93F526B1E505097BA5687C7A7FC8CC2CD0AE94DACBAAFC2D4F766CD212F6C04E23762045A1C856EA1D6F505FC96BFF1D1E485654690E7A7698A05B48847784E2629E6D81A692C93793FC2BEE69194A106E3BEA85A82FC692A0A3360D96128ABD52E72E7B3A12838C5BAA00B5A5B5DF9CFF4E027EBB7D8270C2D6183BBB10CFBDAD15FC56C7E3166751E7DC11C7DB5E5ED6CA5414ABEDD0619890A03C713C910F55526451171A826997B3D86CD9EFCA8E5276D7CA2CE1EDC0266CF876380901AB366E0EF60849774A2DE260D2D155F27440A6FC88EF99DF76C70F4D4EECB4A901CB714C6EA72D6D3BF6FE1D71988E6DD8EA001E0A55C00BF49B62900A801AF1A0CDEB6096503F70B36CD85AB57DB22536FE202E1DD796461E6E78AF79F56C332C97833BD91260540C4A5245E81C08CCB70A62EC17B0E2BBF46D5601B76E2B8AF386F7A3F210D44A7E61A4885D0981907B40F01736E9C65CC4FEFF0EF978AD93B84C6C5071A8E793EF0572F584BF27C328EF33B5ECE0844A83590394D834E225C2CC87DABC829068FC0E7549EA93EB52C3311C4723EF6F84A0C0B94B7F8C610876DE7DA886354B7984A6715B4F0C9A22F422144ECDFC23AF2858D147C26F3BBE5663F5006521CBF222748F67B01CD80CE5B6BB9DC7807C93EDA3B882C7CD5E668E40E6026F70D6E58AB760162816ADF71E7B785E67EC786441FBCA9BE950377BB7256C3C559DCD937413131AB80DDD24BDD441551CC4F1A956CA96039ADDBB919EC52594A1D8BBAC241FF615685B05850A206BD4CAE5FDFE50DC848FFA52334DAC3C9A1BA55D481C579E5CB3F9D9206DA201908606E15B494373C458785362DA9FB2DFFE91C415FC6FA81B1DD682BF0971F6ABD89B954E4C97F5986033AD13A26EFA12C5D52CF90B513C9B0604EF1676F7CB440276602BB0EA117BD20B4F4FD1BD2F05D5ED3473DB2F047433E228E92BEA1A7C602A691196CE71B1745E4453E0E7F510BD78963CC0C31D30CDC5EFDD55CAD3DA519CDC565D73DF8A611CDEBA8D18F0B7B0DF3C5678198B6F39B478903ECAA791C3ACAD02CE3641C174C35BB210A48530426E39914980DFE742E4982BD3BC043585A4D6F65FCD35ED6C5CE403EA2DFB84B1E7E380EC0947D84E85EA70D116FB5D8F4E883261AB7BBD78AC094F45FE830E69451642C4C294E20509A6F13E769706ADEED355931F1193B5E76B1106F5F0D718DEC2EF7DEBFEDE714CD2D65370629FB920E54781535ADD8CCEF0981684103065785B34045190028E4E03AD5E5D509C68A117B4A00B6FEA07D26CEE453BA12321115BE2BE279E12D6EC5F80B263347D87DD6612A14F7BE08BD1A8F3A6A3D1E78C33E71C0F64152322277EE4E03DBFB009D5A1DDD9DCBA1BEE12BFA9B39DD542710B5AAD1052C767782FFC04180478E0AAF3F775DA8F68C094F84FB2E3A56B6E0FB98C79DF1A2A83449F63A351CB9BDEB0C83E871CD9146113EB5E4B28B8723F949D6AD19A5148F73541A7CDEE4ECFCF51A8EE474B27E65E983BACAE976C0E18873795C278489EF8C8503769037CF6173F2B62369AE7C39939AA5A806F423A76BA18B258748305FCBA016A12160C2428E7C60B839E98DA95742E15C8B32BCFA990B6B41F043910D3AAB1D1FCABD8AABA55D13A713D7FD62D25F396C67D9516576E2DE37A319556698104D1560FCB5A690C4D48D0E23D24717E8FAD776DE0DEE2339A0A6CF3338F930874EAEB38C75B62DD99F751D5616973E538C641F1C47034C8C9A1A565A889A1B565FFC3D012E16457CAC00856BFCCA7305CD90E295E98F26F8FBC438D2743D0849FCF0A05DAA139A64837C8FF56601475A80BABEF0A62671D4BA8F7A2D3B836DBCDC49CCE8685FED7C9B27ACF203687176B86D316C4EA9C549669363CC6D76E99A8E3426F34623224E7707DA7D1AECA02E19CA0985F05794E239BB8E21843D49C4C2F8EF59894A437BE6BC4C67838EC5DEA6D09DCB93B914D9E73628155D87C90E6D4B3D3DB2F31513BF692313E28E452BF205ABED9D7AE841AFD11B7483389F89E14CEC729A3CC69F0390A9F2F\",\n          \"k\": \"6FE2B3BA0E972312DB3DBD84F9B8E1D7E3AF411BAB1750D75E7374BAA6B25F45\",\n          \"m\": \"ACA147DD83685FDC5BE522178384DDB0C8714D0F818A5A20CD1AAA71730D8E36\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 74,\n          \"deferred\": false,\n          \"ek\": \"9EBAA37C34B6F0C92BC7E98A0C56689C989E33E9126DAA3D94C95C6D60639DDCC008E4220F8C6348E44738C1AC50521A926B9BCD42511A33C1A704871C11A962E062B9625169A842F8D15CF3F3A0A2411017860C88A22D01262666F6895390AF652204ED9CB0F7589A63C23ECA851C25B527D242AC8D310B3199AF5EF7C54472739E17A6986ACAEE76B0B262C5BFB051343580E60150250865397B4C498B465B6C62E1C3A4B89B756871313AC1C4034509803C59103AA4B9A50CFC7C7EE0A34EAA417009538570C197D04C60CFB41578E274AF9ABED10B4BB7B71A4B925DDCF5432940934B222A36BC80E96C1094E5C264364660012514444ADE22A6121C30C4F037FAD29C2819350700A10C9861FEA471BD05C3D31A2EEA2A9CEFABCCF0B4AB9C33C19FF126B017370073561857084A83ABFA5B3550C2A93B785A4223AC57F4B6FCA0B4D12422B509CA45B63775FBCA4E785972E775E2989D844A4308B786F230CE990010541868C2C21B3D311E6AB14C4D417B37D10DEEBBA6309686896017F08C224DC806AE87658A0B00ECABC2A9F66029873C6C3446914C5A967441BB4AC6900575FA52238636707C69097E19C30ED66A9F28762056585C5738A6F51ADE3603B963476213B78EF65AB6CA7F612134E82A6BECF48D4DB4AD7958787A377483F0A8BAA76B06243DA4F61CF803807C17837A6293D144637BD3A92039282DA86485B005767519C9C0A0B513CA5325866DB3AA67638A2C0117FC80C4845ABADCA92C529C539431363A9A58E6A25CE6A42AB8E5CAD3865AC1B69E77B2B48DD5CFEDABAC79257D15269D72B1CE90265556DA687D894D15F11B1268692E22AD8313877FA8629F3BA224F8C5A98AA31FA58FDD05101C3705496516970B200F984B67EB497DB9AACBD6922EC84C73DB42EC0BA4D1D11859DC356765C82947B4953ACFAECB5969D34A4A8A44BA53346023517C2474A301AE7177A0253904186A5527772CD7845A4129A90B292051765772C09EF585A5BBC976F1F9B24C7AA4478B04B47398491785D9B9B9376B89D6C90320831117949A72406BE6076B4268A356E615A2168C9EEA37B2165B143304B9C51B3CC97AE3A6B0DDB185825C8B7AB073B5086B7D215D4C953F7BC9863339A045DBA784F5489FA51096234DD08B7C457B1C326B8F73672881562310E90B43FA7A37B7550696422A1B32A2615D4E8C5B3C3C61C4479A15D94F210A165311BB4C95060FB8287BC526C212B1EE0470C5497480DA396B2C39B9BB413E267583C249911029DF9B7822327764C16FA67307EFA499F0A7CADCE8B1732C91D284B505D59A444B41ED738C09A40F335C7F7050B80DAC9E5609C000473E440840D1799A86705276761E8CAC3474C1836AA9C6B4D732118AC515EC2A4DFCACB7929776501EF02415A0638819582F6AA2853DD189CF7900F834671718221933C2962B3FE598BC2EB0245C229B2ECA5B1A1A2F94BC6E278875570BCB92C043493267F7DB06C887BD66E38CDEECBCD0A998FFC094376119E4A961020C8BA05B15FE06062FD4892DC0C023AC6B2806BF863ACD85A120A0701435305A65E91D5D908B0BDBBCCBD518BC9481A5920BB4900468A893589128845BC95294A0DC5918274C4FDE2A0B18467699D637AF4CB62DBA3735E33DFE25AE1493501A590DFD0342506B748EFA5590BB689D18A50B2151C9E9B559B346E8E530A14A1CBE5AA2843431CA32AEDEEC4E9081B50A3BA764B1B0EDCC99B17B3FCB3046E6C2C1B1966F2F481E4E1CBFA4E7546E0A9E6ADB649D59151209942B92B019E8394416C19004CEF1E9235BE0228E6B2CE3603A37966BBF51CA366A3F2D18B931E4B3A827887FB3147331A746E713F60040F3277CEBC2BDB7251E84EBA5012A10CCE965E4A9963151698AF1492CF51372C37AD0D78F817B466A26372F3213873802EE26558C9ACD0F511B3F9138EDCA56FCCB36392ACE11411EB2936CCE03A0F2F347E2332E14E85B8652984D09CCEFEB8963E2A5EF845888576EE1B36EF362274D292F10355163BCBD51A7AD7B284BC4A10C3370A1391003289016F22310FD3C4CFB31CF88A42685103F3813393C1A12915981C11A616FBB8DD6905712F387D279C24B17230D32B018E14902CA1BB0561CCB73A0E2F4C8DAD9969745A1D238250B76FAA9FE97E3E08E3BCBB6E860DC8EC5EF30B92C5648EDF353871C148883FC\",\n          \"dk\": \"BAB28389C4A099F98AC074612F487570115D0A031BEDC16B46D77C7493318891B30B259E7284749676A76B1637CB6CBD842260B4BB9FA1230DE3E151F1E5170A82110BB2B1EB992AF9E2BA98BA4AFCFA3E71765B9346412E10998BC8461232AB5AB24A16F23BC358AFE01A1C7F8A3B05A8AC5EA9591D2689B56466438761A6220D151682BBD0CD23C42B998A1871352C1C2C0AFB1889A47C0B0FA8B2768A5ACF1C4D0B34BD7C16CEF3611851432F361A9176D77ACBA7C88C219CB98819552279819B6BD6C57BB93AC239DAA6A8D53F9D1665F8B866ABDB52FDB1BD818923FFB803C5A8B61CF9B9750A2F03522B4DD5AAC7FC345CF28278F93B1E976A381818AC2275AA16CA44204C6513394439A75D7B1E5970735F016CA6045FA4E68F4A9C31F9C4766C6A56DCA1C83101346B3A1BD796170904398A859734B145933660AE905433B40A8763AA37A868C2E3A33ED709BC9CC1F4411472B31981F6AE08560C68759966B047CA295FAFB60485A28A8181CEF7954C78BC85F395952583CC026C46D9A540E8640B9FD175F4702E5A3A131503C3251097C93332D5113C57082B7CE09A9DD271EFB7ACBB92257C0B5B50EC7BB418B83AC53623AC6B7109C713D0C9C79A750A603B2B2714A109545FB4506A474BBB7AB8556462CA596741E9B0CE3A7FC63A49039450B41675F6E0AF517623C060218C755CD3702FB4A44DB91A80F50CA32F3AC56555B8EA0B26C9E8BAB210889873470873CAD9D7346D8253E2268BCA725E900C22D72318FF686EB9CC80616315AF936F6DC7579748094BDC1301B4A17E826F60A59519B66C2599B52E963054906AA85025F1E26E85CBCC2985ABA727C77A12794D7A67C82B8B41D1110C63C3DB43CD4684001E486E18789B4E207C34D3A207674EF842C2DE7322FB400747B3246387C84F18608DE182BA3C79C2414EB60A7F75040301F32AC543334EC028C0F92914089602D15099C696019A625D357D91E6BC79581F463127A2641060460F526025A0C020352303BEAC1F1BF53E7B258744813308DC69022A37B4D72969586DEFB883A299CF814A98601766CB861F97CC7321764794D1AA928B081593AE3FAC6D3BB71ADD53C52C21B350FB3C6BE59AF019C0384941B8932C673211134C34BE8C8697FA038C90C09730B3ED556118C455B6168C84E3CF5E1AB818C02E4F00002099C78D4120252B9D86B4051A7C446E5509C5839B1023A6F9A9947AFC10AB1764774B9475D941104278AC59C87A18C1F2B17FA0306068F291D0F2CE5F5069B4A3055118C60DDAA96AB0826FD374B4F8B6DDAC28BCF6398FDB9393C718F0BA2775E54F2556CE2C2678241BCD68A00B81B87991ACBA1D9003DB76AA36075A0131023A373C51B065E2EC083F9A3A5CEB22D246711346A357E94E10C199D7A09C87D1188D1C3C0D1134DA650D5E479051B140D4A0B49492C303A40FCE4457C00A023202B81BABB699845EC77244DA184A495196446C96C0D3A0644710225A744E458782D38C63C459BF2A08833755C7066396042479874976C1512BCC046C83786B4424C2D022BDA1925DCA930067AEE3E6454E1C78B1C5A7C603B85D506892852C9D39AC7808CDE5CB71E371378042CA0B5C35414BA7A64294DC42A0463841626A5BC6F37B98D4393BD9B9E5E020B7849EE72188CD1231DF530E36E4CD72580E05359A8D97108CB82E9D1B7CFA94BC92FCC174F3A4767CB1D6FA719E202D8AD68657A81ECBC0861B645D3A781A725A6552ECC12B64CB12A6CB0AE33C64624A8D28B026C482AEBC6A69D95557FC3CFCF4B2FEE502CC7210B42C089AFB636AD573E7430A8A184685465C6B882987255430D2842BA75E892A5AE23A3A636840E6B3CD9F76975EF41C034CBD799412584CC85625208855083BAC67D85B0FE011568ECC7365798AC3EA91C61442EE260BD380C2494680FB411BD16714CBB6468BE41013F724CCA4B4FF364D095422124652D4A53110C05B89B095BDD1C7B8D19689A706E1DC656E6C1A3FC767A1D44EBD67C5B370A0F5925DC551551AB6B604A27F56A19003731DC071B2E98C86F7BA9872DA783DC68E1B19ADED603CE7F3BCF806A7774A044D9C51B26C9DDB8B809C0CC5F8D0682C7C8A56A9C89F412DB5BC4ED438AA89574B8A77206396807FC431F9A2B29EBAA37C34B6F0C92BC7E98A0C56689C989E33E9126DAA3D94C95C6D60639DDCC008E4220F8C6348E44738C1AC50521A926B9BCD42511A33C1A704871C11A962E062B9625169A842F8D15CF3F3A0A2411017860C88A22D01262666F6895390AF652204ED9CB0F7589A63C23ECA851C25B527D242AC8D310B3199AF5EF7C54472739E17A6986ACAEE76B0B262C5BFB051343580E60150250865397B4C498B465B6C62E1C3A4B89B756871313AC1C4034509803C59103AA4B9A50CFC7C7EE0A34EAA417009538570C197D04C60CFB41578E274AF9ABED10B4BB7B71A4B925DDCF5432940934B222A36BC80E96C1094E5C264364660012514444ADE22A6121C30C4F037FAD29C2819350700A10C9861FEA471BD05C3D31A2EEA2A9CEFABCCF0B4AB9C33C19FF126B017370073561857084A83ABFA5B3550C2A93B785A4223AC57F4B6FCA0B4D12422B509CA45B63775FBCA4E785972E775E2989D844A4308B786F230CE990010541868C2C21B3D311E6AB14C4D417B37D10DEEBBA6309686896017F08C224DC806AE87658A0B00ECABC2A9F66029873C6C3446914C5A967441BB4AC6900575FA52238636707C69097E19C30ED66A9F28762056585C5738A6F51ADE3603B963476213B78EF65AB6CA7F612134E82A6BECF48D4DB4AD7958787A377483F0A8BAA76B06243DA4F61CF803807C17837A6293D144637BD3A92039282DA86485B005767519C9C0A0B513CA5325866DB3AA67638A2C0117FC80C4845ABADCA92C529C539431363A9A58E6A25CE6A42AB8E5CAD3865AC1B69E77B2B48DD5CFEDABAC79257D15269D72B1CE90265556DA687D894D15F11B1268692E22AD8313877FA8629F3BA224F8C5A98AA31FA58FDD05101C3705496516970B200F984B67EB497DB9AACBD6922EC84C73DB42EC0BA4D1D11859DC356765C82947B4953ACFAECB5969D34A4A8A44BA53346023517C2474A301AE7177A0253904186A5527772CD7845A4129A90B292051765772C09EF585A5BBC976F1F9B24C7AA4478B04B47398491785D9B9B9376B89D6C90320831117949A72406BE6076B4268A356E615A2168C9EEA37B2165B143304B9C51B3CC97AE3A6B0DDB185825C8B7AB073B5086B7D215D4C953F7BC9863339A045DBA784F5489FA51096234DD08B7C457B1C326B8F73672881562310E90B43FA7A37B7550696422A1B32A2615D4E8C5B3C3C61C4479A15D94F210A165311BB4C95060FB8287BC526C212B1EE0470C5497480DA396B2C39B9BB413E267583C249911029DF9B7822327764C16FA67307EFA499F0A7CADCE8B1732C91D284B505D59A444B41ED738C09A40F335C7F7050B80DAC9E5609C000473E440840D1799A86705276761E8CAC3474C1836AA9C6B4D732118AC515EC2A4DFCACB7929776501EF02415A0638819582F6AA2853DD189CF7900F834671718221933C2962B3FE598BC2EB0245C229B2ECA5B1A1A2F94BC6E278875570BCB92C043493267F7DB06C887BD66E38CDEECBCD0A998FFC094376119E4A961020C8BA05B15FE06062FD4892DC0C023AC6B2806BF863ACD85A120A0701435305A65E91D5D908B0BDBBCCBD518BC9481A5920BB4900468A893589128845BC95294A0DC5918274C4FDE2A0B18467699D637AF4CB62DBA3735E33DFE25AE1493501A590DFD0342506B748EFA5590BB689D18A50B2151C9E9B559B346E8E530A14A1CBE5AA2843431CA32AEDEEC4E9081B50A3BA764B1B0EDCC99B17B3FCB3046E6C2C1B1966F2F481E4E1CBFA4E7546E0A9E6ADB649D59151209942B92B019E8394416C19004CEF1E9235BE0228E6B2CE3603A37966BBF51CA366A3F2D18B931E4B3A827887FB3147331A746E713F60040F3277CEBC2BDB7251E84EBA5012A10CCE965E4A9963151698AF1492CF51372C37AD0D78F817B466A26372F3213873802EE26558C9ACD0F511B3F9138EDCA56FCCB36392ACE11411EB2936CCE03A0F2F347E2332E14E85B8652984D09CCEFEB8963E2A5EF845888576EE1B36EF362274D292F10355163BCBD51A7AD7B284BC4A10C3370A1391003289016F22310FD3C4CFB31CF88A42685103F3813393C1A12915981C11A616FBB8DD6905712F387D279C24B17230D32B018E14902CA1BB0561CCB73A0E2F4C8DAD9969745A1D238250B76FAA9FE97E3E08E3BCBB6E860DC8EC5EF30B92C5648EDF353871C148883FCED356581C66D62FCFDA63A9ED071F7E5EE0A9AC9DBF48E4653DB78A6BBFFF1919399B9CE712E51B00A12EBA403E181CDA45AC150688E2D09614014661B339E6F\",\n          \"c\": \"E9FFEAEAA83250A944F247022FA3F6572C8496C0AFF81462BFDDE2FF310DBA5E6820FE52E33C2CE7BF0C8DEE97175E20000B3577BF84DFD69BF45DAD481E94AAF25BEB959CEAD9DC539492202E85AFB680165C8A1C2BA42490CEE563E4EF4B821EF34B0EE08C0ED8452235F99E123B650985D9B1477D21F936BD937CAF67343FAEE1EB684EB4E0EF202F5A58445DA5F6D5D8AE7071BD531C0D736C2570F5FD593FAE9BCA5EF988B6BE44A323DCE5883806DFB7678704004B228FCADE75EDC4CDB7F4F87B0C315FC9A40B4AE1E6944C96CAE75481907804BF308780F411FE068CF09BF99AE3FEC2F656C9CA02158694E5B22F044FCAB131B7D942BE6ACA98128CEC79DAD0AAB4BD566A2F041AA043520B9D8642C9B744D4155F926DFE87AA8A031BA5E54090A93EA5091D9938C6C3CD31F83D2BD46661FF339E66513E284DF7FFD64A047F741DC81F46A6A7F03A9025D554F93D889A5F8DDD75B3C0F31DD64CD4218050AB496C5D01E632D35981237F248D7B31C6F39678D4FED7DABB29C242699F2F588D2FC56972B6EE7A94C1FF01584F56B86BC3C1B58DA0B9616B9C5E316D7FD7AA9C22D51F0BF69A080E595D794B5F0924A4448CEDD0E03B414B91D03FB511AEAFDD5CBD4FDEA6CBC0C62849B2CA7A6023251CEB720E52407D03C9412F0C87ECD974EFE304C7791CDE5911F7731EBCBE969F039C6C3FAD7138CAECCE0D3FBB5F47FAF77B80531D53CFE23E1E04921864F5346EC3EAA4B663512DB4344A5579BC4CF32C8FF33C5F32CF44238608C19EF4074CFF9ECE4265084C4515E919E2118FE535480CA816E4AD6632BBA726D9B43DA4DFDD2DD1E0E85D001BD9FBD52BD9A6A8548A41251F939A63E683CC5A076FF3F0A2C33B223AF28997CCB36D3577FD09BF46F5988C0042C666929B877132B0D5375210D20C7F3816FA1AB81F3699091874E0CADBC1550BD6DFE24335EDDA44FFC421C9D89D9E250562408BEFF06D01719CA54FE91727CCBD08FB4B45BEF75B08F560B0FB9618103FBC216A3D5AE72A869BD40E51170E88DDBB713F8A0531DCC2645CB185F2F31BEEA7086B5AA84F5248B2014F9D85E56B330F119F88E26EEDF055A5DFD87582E91D3FF59FAF9AF3032D60424DF0DFEEC98F2BCD2702A10551E7DA73628D4F6E9065049B172AB861EE64176E21FC7DFEA1208A632C78D72DD2409798DE854C3EB67A985A4C04CFF154A69D8443F6AD9EDBAA666CD29260D203739F3B77891DAE26F03DD469CE4C55377B5F3AB9DA1A42F7F8F8AA944E7A921A53F7ADB99533F48D4E9922982FA0B149ABFD95095CCFBF3CD719F59535349E66653FFAC1D0DB3857688DA952426D6AFEF5EDFAA7732133FD9CD86831BE5A7809E209B5E1214FCEFD775D3627511340F53976B571BA909BF5234DF037C8D1E4A11BBBC8DBBABCD39AC47CA6CBBF38C7326CC253FC59163FA313CB86DDADC05AA6CEF529AC23FA315734922885BAD9E5CCFEE38CD20AD2A245590CAA674AE6195F4938A95F46E59CE060E7D7960362A7C19F19DCE8424E8E7FF5B084BC093FCCA9BBE9228B089497D51BE411AB06CC60301A510799539287BAD4E2FE023B6B29386349794AF3F8165AB8F6F4BDFFF114B817B10F4598355E7AEAA31CA73F8F06FFB478483EFFA2C11C94C10E8053E95E041ABF346498F1A2765B460DAA89EAF3EAFE952E35B10630D78FF742977F2F5FCB63712901FD9181A1C97E0918DF81FEBD517E6820D70A509C15C144CDF6D1EC37FDC7CF2353A3CA7136071DF8777E45D5D8B5146C058666926096F51025881DACFE2392F82019580BF82E998CFA5AE06A9EDE3FEA2B4C6C1FA0D02822CFAF4F2452AE695AF70264D5D7935FC19F2917090FAAE36790032CB85A5E875C05D0483B2D4021892E825A2D0977724DD1E73BBC8C430A0F6F4574AE112575F87AE30BFE94EE8260B5141A9E414739B518C11378A7C574962F0DFEF8E2DA1C1FB8B4E502D182E14F64A91FC55A80D65183B7B592FF077D3504B9AB88F6CC5F4356488DE0DED68A1C469AD9556E2B467187EB2B0B466DB99DE24BFEF3D9944B244C022F6977C0CA987BA8D0D10944CAF8AE6B89C226EE50E338F653A641EC39DB3AA834E25576B0A5853A7BA26E59D65031DC780E8588EB936D182D988BB77B4C9276A737E44CA1A27804687E137A84A872EE1970448BFBC7C42007E4518FAFDB544F9A\",\n          \"k\": \"7BCDBA65823F7A36497091555C7E558D933E707016AA485708FADD30EFB8D8D5\",\n          \"m\": \"B974689F6F36C7AB262C8B97D5469ACD3BCAA3A3454F611FF0B304FE1DF6C66E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 75,\n          \"deferred\": false,\n          \"ek\": \"A6902B0559182B341E3A6ABE0B646DCC84B39881948CA396CA782A65D6221589333E98A88A9C320D83A10422948B64A0DA1C4ED165B0144B22190656F1DAAE36A5641A4863A3985AE9D08456629C153A596289C01BD2912FC3603EF7ADD1EBAE1A745675F316ED0A6D4CB5C5A431C2871B939B5648D1018911D752BF08B40CC571D863B33C9043E3B2C73C67CB9A4C8087F28D7D37B9B66B638CC66796C146135C2A743AB94FC693473089E6D32492842D8D114C9A92C6721818CD0530C91A09FD7C3A124B7E98DB056C77049008A112AB61D03A8C38738B96F64D4E530E5345958EA38F19517B7293B6D2DA11BAD668988820CE8479C3B5A832C708FE09481CAA7ABDAC2E8658544067810AA31D88A52F66459B3E0A5AA8EBCBA1B5C9B8776FAAF59CFCAC69A1CC8EF166615998AE88968FABC67D67C7675B94CEF447667FF53D2B3890943303BC88C5B3DB08BA75187B6CBC97503E9A563745824A533C59A71B66D5CC21972B843BE0C0768970571796AB992B39F43F2CD8017B5517A91235474A821C541B526B0404212C194238B18AA5B8A175E4A59417D119EF8A385B7458980335842BB318214E55794C635692736B6708863A066462EEF41ED66771EAC578E029642954111C0B20F9BA3B8646456DA39873B89F6449324CBAB5ED017CC1C72E1ED4322D55B2030A8E0B390E54384449096A2DE676BEA01F03A458160341B7C743719B89A158120B790AE8E1AB4468467C4507F7CB659EB8C16A77315E553AB287836C4472EA1075C7A1C84DB4CABDD5CEB2CB5638D75FE3567659443B38B2B21C422DF97A14A8A633C2279DDB056302AB0FA83905C846AB57D2120EA1CC92195320B5B873B406CB44B73FB02160200460431581979967FC16233333A8EB7D48A120F451B3FAE9BAF6318E384B6D42B36D5B38188D849652935494C4BBBDFA22A562A27BFAB60B3455E775912D27698AA30F76CC291107629E3627EB443D0F00AA3F220877010876B485F57495C9E80DC4F3AFD6118E1849ABA06919968163CC9CB358DA03B3550198A2C43F476954F1557058C05EF828D8EB982340C9BC6A5AC1A27977402B2242A40D89072C1270DD8772008A505D4604831C1924B97C980B01788107472800EFD807FAFA1ED3466548D698CAB76B820667D630C7D03447820BB8815BBCB8DCC11752788782CA3BFC51DB9BA0D16198132A0ACDA7BD7A6531A8AAB9C8AC2770030C512740CE62CB60F79F8087CC9177B56E6A4B8155B854993E2BCB78AA849E72398590B24E87A31097AA54ACA80A9CEB29BBC5B24CBB7DA084B76B5C522F73185A05A8024629F8A6AE304CC7FE88258C94A396912A16B491DD0843D8A82BBA86843F458E91261588D7AFE89A1D7EA9786DD13C4111CF89EAB9118A7EA39A72408C8F1EA2C77765C12881192A33C552D61A76424783E5C48C881E9A541B8F6157EC138AD5E93AA925194FA37644F397A98C60DBE786983C8B2CB6C76C73CCD0371CF0CA85998C65D1F76DD5E545991A00567A48EE69A537921379923D7676401A06A81D68BD9FC4C2A28B4B72C966576BC6DA870BE8C415F94174F8137B3EE4793786652EAC97D6C91DBA4749CE4B69DB82209B24C5FAA0C4A0CAA71A39BA51E7A6F19381219900E6F8AF06F0B224326EFC2C8560A95AF8296D2B87353360A5AD3B6DCDD95377B0B201EA7A6E475D5D563A373554E83982A35503E1C179324276F6147B698C32AD24AAEFE0B2B1FA911444808C6A25CBA21B4BB008F49C0CA8D209E2BA02E3B164DA41793C212F9C5B2E0EB00CE7146284C0B26FF940268318EB330C6F0100ADD43FBF0615D2EBC27B0819CDEB6A327C590DC83D678AA4BA910E509B4EBCDCBF3B74CA581A2B55F46154D17F9AEB3A09D5C30356A75AA2859BC11DFE866E3C463C96826613880470A1CE8276B033C1810BD645B5278C25F81B678228F4D20FA3726C5715311DA4BE1F1C0752DB6D16D889D27313C4046E05FC161E889DFDE2255F434C8008AB12167F3B6A15D74B4791B293C902CCC6F4C98A75344864C35E6B189C8C116735C7636A6F0AF653DB17CBE0E3CA7C677CF9604CFADA8031611321DBC90308BCF7F93AFE1B02B0C9826A4139C4B2097CD061BCCCC5B3C562BB13A30CDBB318CB9457C1B9A69236B820AC0717EB65554D8CBB46EC883E0392B79C92DB8D07FCD393633C020FE2C469A32D\",\n          \"dk\": \"8CCAC9928C0FBEDB3AD6A04E1853BF4689B035E0C8EE7C26BA2140CE1CC8476960CE51732FA0310D636D06D802B164B826F376395AA0D371C9D56275D6AC5878D13655083769E14974B22FF9B02C5D1370332A47626702E5E2044E05B597F94FBC429E50E65B437223F32662A1C37081C3B7A884B37DECCA999C1EC3997F38696A48880589805E965ABDE6558722F5481666120D4C6B12447593399CFBE881BFA754B57350CB060A80CC7F95C65CC6905F6E701E1DB03A38CAB8ABE9412A990DE094C99A514C68969940B5A82776B6EDEC32F7DB9A5D7417F4012DB6F36AD8D8967F2B144488483779AAF36188A7F27B9FB8A9222A02411B110137BDFB835040E220E19A8B2F8A25453286561240C9E02A36591B6829BE48177E086599DFA003D951CBDB4B24313CB8D5FABF9ED54140FBA91E3921BA9184FAFB5136D64F22A2B59B27116DE13893AB6FAED8A1346742F838641DB76912D30D2481657194CE3E709E9AC272DE638C21580CAE015CB04A89E587CCEB0B05A972407F9C96F8474A859344EE0C31A2D30AFA74948FE2B6A4C26C3CD2C9C0D7400B802AA36487F6C216989A5F806536853B9FAEA2C2A91C039CD07A4E435A58D80DAD50A991150B1A1B0042818A38F6929E325B6D29C70BEA1877D73403DB08B856C10C2851D0E30EC6BAAA9DA6974851CFB31211EA282B8ADBC7425274D1BCAA806652344BB663D7297304192E06AA53A438A2D147DB58A1901786FA9924216923B01C1FAD85C0B79332E9DB420A94964CE9BC2CC19276615B7FA3A489DBCBFCD07412F186ED412CC866270AA53A5E645ECDE278ACA36BF5381DE2DC57C9B812BA13863E55BCA68C76E1F496F216663E877AAF2C1959397AB9B1BC21B1A1D9AB5091484780F58A399C058D17AC7653A288D27E18D43B4506C69C477640069446A5B00F9ACD93200E21B40905A09035886B11647F1182BC6F824CF7A5B1D1951FD9049690450EB559677E888BAB0924DEF1741E22B610894109CCB549E4A5EE0A5F4C761E6A293F61EB2C5739B659E4B5086A0C1B4A0382B27980B07C1D2035F1DB53BB82787AC249B2011F3590CD9FE5515F813C65953AE46CCCB7C3210E24AF82B16DB5916A6B23393844636C278641C48CEE2276760518C919AF3E16C3C9714D1544CED4D2A64DF482CB4794B8A77BC5E6CA2718513E6213F620BFC8063A667CCCCBC99C7CC1328A5CAA31DB820A4088CD825313D677C49419B2692E00813E33A0B62604CEB47A547FB2A92B63CAFEAC4B63A41FB1805CCACACDC62C373EE63C49F09899929ED85C89D884287ACABDBF717F3C49A511DA53DB43AF1AF30CAA7B09E0D760B66C805382C427C3BB77F24381D71F691CCA9DA8AB1FA56695E2CAC9B543B1F5C3BE2749A804928BC005F0908576FC87CDB88AFFC4869105A1B5C6364A64083793A4900434AD7B9A0196BAFBB3B54B7A0D23FB8440A65170296008BA3A94A94AC5AB36F11507F6B7443A928F592589ACA7C6AA5BA6932840124A7DD65A20F72A5D8CB356846A1D670CAA2BC06A4D64C4001154CDAACCC7F21CE0C1C877B2556833165890056D929510D4252A065EDFE2152FEA9876210DB26B7619F220BF5C4DE7D05AF2530E47FB43C6BA7D385482998994024C04D576AAA4AC2B1A951A01596A0EE36F6D8B0BFE0A5800EB2ADD9758DCCB368B701EA6D4B9605A11E7369E44F67DD499896CE297CEF763CFE99C14D41E11913337714C6C79084DE7C37AD76C7A838D6C40B485034C6F55345EF729C0930397166AE317AE5FE85F4DB999B3842387C245EAB7500A51260097CF87D77C56B86009F15CA8B60B624011086C229CB73C6A90A81296B4216145FDF76528DB8AEC671E7062AE1105214F01770BF98477F9098482AEA044B92B6AAB0C70480759108B4AABDCA580A1C640F32826B7CB4734D2743DB5090E8B97C1AC46E3F755AD8BCA6D9A93B414B7CDB15E81D9017CB1291FD263585A02E2EA555EE176E8D29AFAB58AF189C5480199409CBE7D871E42EAA4702C7647D023FCA094232ACEC3B994B1B93930CB9631E34C3BC6BD71116FBC751C872A5F7435376E05941D9BA80BB520EB9A6ADA4174B671B682472F3EE9AA57579265190D618575E2B996C3A1A5AC868B96339BB0024BC4385C96D1B5146871A6902B0559182B341E3A6ABE0B646DCC84B39881948CA396CA782A65D6221589333E98A88A9C320D83A10422948B64A0DA1C4ED165B0144B22190656F1DAAE36A5641A4863A3985AE9D08456629C153A596289C01BD2912FC3603EF7ADD1EBAE1A745675F316ED0A6D4CB5C5A431C2871B939B5648D1018911D752BF08B40CC571D863B33C9043E3B2C73C67CB9A4C8087F28D7D37B9B66B638CC66796C146135C2A743AB94FC693473089E6D32492842D8D114C9A92C6721818CD0530C91A09FD7C3A124B7E98DB056C77049008A112AB61D03A8C38738B96F64D4E530E5345958EA38F19517B7293B6D2DA11BAD668988820CE8479C3B5A832C708FE09481CAA7ABDAC2E8658544067810AA31D88A52F66459B3E0A5AA8EBCBA1B5C9B8776FAAF59CFCAC69A1CC8EF166615998AE88968FABC67D67C7675B94CEF447667FF53D2B3890943303BC88C5B3DB08BA75187B6CBC97503E9A563745824A533C59A71B66D5CC21972B843BE0C0768970571796AB992B39F43F2CD8017B5517A91235474A821C541B526B0404212C194238B18AA5B8A175E4A59417D119EF8A385B7458980335842BB318214E55794C635692736B6708863A066462EEF41ED66771EAC578E029642954111C0B20F9BA3B8646456DA39873B89F6449324CBAB5ED017CC1C72E1ED4322D55B2030A8E0B390E54384449096A2DE676BEA01F03A458160341B7C743719B89A158120B790AE8E1AB4468467C4507F7CB659EB8C16A77315E553AB287836C4472EA1075C7A1C84DB4CABDD5CEB2CB5638D75FE3567659443B38B2B21C422DF97A14A8A633C2279DDB056302AB0FA83905C846AB57D2120EA1CC92195320B5B873B406CB44B73FB02160200460431581979967FC16233333A8EB7D48A120F451B3FAE9BAF6318E384B6D42B36D5B38188D849652935494C4BBBDFA22A562A27BFAB60B3455E775912D27698AA30F76CC291107629E3627EB443D0F00AA3F220877010876B485F57495C9E80DC4F3AFD6118E1849ABA06919968163CC9CB358DA03B3550198A2C43F476954F1557058C05EF828D8EB982340C9BC6A5AC1A27977402B2242A40D89072C1270DD8772008A505D4604831C1924B97C980B01788107472800EFD807FAFA1ED3466548D698CAB76B820667D630C7D03447820BB8815BBCB8DCC11752788782CA3BFC51DB9BA0D16198132A0ACDA7BD7A6531A8AAB9C8AC2770030C512740CE62CB60F79F8087CC9177B56E6A4B8155B854993E2BCB78AA849E72398590B24E87A31097AA54ACA80A9CEB29BBC5B24CBB7DA084B76B5C522F73185A05A8024629F8A6AE304CC7FE88258C94A396912A16B491DD0843D8A82BBA86843F458E91261588D7AFE89A1D7EA9786DD13C4111CF89EAB9118A7EA39A72408C8F1EA2C77765C12881192A33C552D61A76424783E5C48C881E9A541B8F6157EC138AD5E93AA925194FA37644F397A98C60DBE786983C8B2CB6C76C73CCD0371CF0CA85998C65D1F76DD5E545991A00567A48EE69A537921379923D7676401A06A81D68BD9FC4C2A28B4B72C966576BC6DA870BE8C415F94174F8137B3EE4793786652EAC97D6C91DBA4749CE4B69DB82209B24C5FAA0C4A0CAA71A39BA51E7A6F19381219900E6F8AF06F0B224326EFC2C8560A95AF8296D2B87353360A5AD3B6DCDD95377B0B201EA7A6E475D5D563A373554E83982A35503E1C179324276F6147B698C32AD24AAEFE0B2B1FA911444808C6A25CBA21B4BB008F49C0CA8D209E2BA02E3B164DA41793C212F9C5B2E0EB00CE7146284C0B26FF940268318EB330C6F0100ADD43FBF0615D2EBC27B0819CDEB6A327C590DC83D678AA4BA910E509B4EBCDCBF3B74CA581A2B55F46154D17F9AEB3A09D5C30356A75AA2859BC11DFE866E3C463C96826613880470A1CE8276B033C1810BD645B5278C25F81B678228F4D20FA3726C5715311DA4BE1F1C0752DB6D16D889D27313C4046E05FC161E889DFDE2255F434C8008AB12167F3B6A15D74B4791B293C902CCC6F4C98A75344864C35E6B189C8C116735C7636A6F0AF653DB17CBE0E3CA7C677CF9604CFADA8031611321DBC90308BCF7F93AFE1B02B0C9826A4139C4B2097CD061BCCCC5B3C562BB13A30CDBB318CB9457C1B9A69236B820AC0717EB65554D8CBB46EC883E0392B79C92DB8D07FCD393633C020FE2C469A32DF3F0316106B102E875DF4653219F35BEEBE7CFD5F0C59D1CF68055C61539BAD1231FA46366271E63D3B696A0FD4569870859EFF47CB57C3C6A65B22253A739FB\",\n          \"c\": \"3E07145AEE491606A4DFBBF9C7301FB8F21A6F46F8F87253346A5981C7D83EE23CB6BDC508AB0756A8E2D8713A03275A551C0B291DECBF6C0A3F976758ACA963B590FEE44E8D1056AA95AB5D1B77A0016E3AA605EB564337BE2FB33E54054A08C7A3174E8E7FC0F079B1BE8C30BC0FA7C03972DE8294F9F24251F834711C0BD340C9EE20BFC74CF99E8C0CC8AEFBB057B0F7E3CD0AE6E0C47EF67F22C13C2B16179942D8AC24FF81D99CD9C5ECC5065C0BB0C4A9B36FEAB42B2F06A6A0F9AC2FF4AC50864C6D03CD97F785B7B3C521392E246DD0D5FA5218EE1AC30A223194E5A21267D1DBBF4DDF1018858D69EBB382907597BED3D90936B5C039DA96E5BDDDB8A5645EB1BE21C1504221067B293B4C6C81EB983CD49B5A1DAAF7DB602E990DEBF76613C6111B3FDD2ACA243C3B92D4E6988BD43082F6339A89898FA0CC05C1859DF99EE74F3748DA53BA99561A5F5C1EB1544A314343FB9167EE9E822814A6CE530836239DB515A8582CD9ED338B2A4765A7C265F0825B1DFC6E6CDD41E137C5FDEBCDA6878433EE1BAFFD7F64020D9606E397A12AF66253E19EE2CF4115C173EE73535DE0DD5A7EB7E2EBD769362982F9B09AA5D6548AE9163D0ECBF4929A950853069AECB829AF4F91A517C8E8D2DE761CC9F5729931E4396D261DDC3C66350B20FA1B37ED3BAF2092F7BE7C85DC1D73ED66A5C7ECA6D6ABA46E09B03102D0325E712699DEB28426AA5309D8892BC767BF099EFEE2481A589CE304427D9FB13A65913FBB37C039C9390C9E9BA3988A81C98CC60014117CEDBB09234FEE8529B9C3CDA11292EBF1678BB9B2A76C5CBB43AD1F947A984348DD8983509D7A3D3B3A560D6337CBE40D32F554C24E10BB720150D4440B630492CDF711193498E4CFCF3F8983BEC12DDC14EA3084C63A418050FE55085E279F94109B4AC6CE02E91D5CDFA62E9EDF05947A40F4BDF8C4A5FB712F86772DC1D9393482D45692463E3697A925BB7CB49F7B9E030199F4955EFF2C829C128DBDCFCB68A3CC57FA5DB71D90ABE690B97FD9387BB517352045F509A9C7A2F01EEDACB35E5E660ACF9ECDEA3F4201DA07BFB8AFBEE7AE32A77779D68A77EB23DF57FAB5E1C7B21E7515709F0BD475361311B831D336461ECBDA68646B8D036779AD9DE23EFDC399C4C90ACFCEC65B877C75A6C5782DFF158B618C0E4B43FC0EBAA641550A44721F35C09864508A3FD0718C2D6C0F235E454D30D969882DBADE20BAE506244B0D99EC1F9664A624A9F46B99B573210A4959CA9B3B897B40FDD92346953BB526893EE06C96C39EBAFDCAC9BC45759299754812CA556E5E5525477F88D207187B1916251B703CAE95B1CCC3585F7431B23969D20646BD1E61066BEA322F16CF8E58DEC2A5CFED648DD98826DEFD121C30302979B215FA0FFD233E61CACF09CD929605EAFA9D2083ACD78CA7C97227B379B0359832B5E1EFDD2CD72562DEC3B8D23B39003539E4E9B8F3C6A74398F18A3DF3F05067F95410F274B3DC0A3A8680CA8C53C746BB4208BA23B5752FB24121B8088AE702C8CE10CBAC6E733108072B29FBA6261491EF0F06161592C19846F18B9341B85D6A5DF4863F7F9F00EC4F8A669085F03F6461B3DC0271D38198FCD546AA1A8DAA4925E9172633816686FC07A855C92AB7D9B4E692D5BA6F51B0B9928EA778DE8A167123B0A80C8AB0B8CAABC5FDD12736A9089D8F60CFDC9D5A8231EB64EE8CEAD2B1FF610BE325DB34520495792E8B9D5403B0C2451671ECF9871BD5FCECDAE8CAD4E9E19815A60CBDA867CB0F5CD1A8A2366B5129B4A5799609909D43968BC296DF77592E8FF5F3ED02248279B761A4397F6930D30D47C31B657F8D1A13C99210CB3E17E84D414FAE4DC6E9E182106D353256A7271D0A5D23050FF33CDC1C48A64BCDD6069F71522BA1D33D9F13470EC1D0D348EAAFADD2DA2EF5D1CA6B05699EC818B6FBD719F8D42FC0F1172574F71C468204034E24A68DD7F92E341852984CF349CA5059E69B19E88CD4929EA8220D04CB4F06BC9A59F0F0528C83D59D4DC36B17EFE9F0B83FC581CECFCD981F419A987D2380AAEAAD7684EE7EF2B920DEF9C0801781B5B34C7E6FABE8EBB4B531A476D970248F2E0D0F17B38C5E2C46B45779383180620C5440C2FBD59033877B84CB411970862FD2C47CA91757B33243CD74EC15E5A622F44940F65E3F42372F8B\",\n          \"k\": \"06B511E4AFDB9427A2296FD9BD7C6467DE6A25D78866F770C2F41462D299038E\",\n          \"m\": \"7B93EBA796CAD98FDBCEAF0B8F3BFF196C1F89125B2AA88F623A91DC6AEE3771\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 4,\n      \"testType\": \"VAL\",\n      \"parameterSet\": \"ML-KEM-512\",\n      \"function\": \"decapsulation\",\n      \"ek\": \"5AC81984D4A5A83619735A842BD172C0D1B39F43588AF170458BA9EE7492EAAA94EA53A4D38498ECBB98A5F407E7C97B4E166E397192C216033014B878E938075C6C1F10A0065ABC3163722F1A2EFFEC8D6E3A0C4F7174FC16B79FB5186A75168F81A56AA48A20A04BDDF182C6E179C3F69061555EF7396DD0B7499601A6EB3A96A9A22D04F1168DB56355B07600A20370637B645976BBD97B6D6288A0D3036360472E3AC71D566DB8FBB1B1D76CB755CD0D68BDBFC048EBA2525EEA9DD5B144FB3B60FBC34239320CBC069B35AB16B8756536FB33E8A6AF1DD42C79F48AD120AE4B159D3D8C319060CCE569C3F6035365585D34413795A6A18EC5136AB13C90E3AF14C0B8A464C86B9073222B56B3F7328AEA798155325911250EF016D72802E3878AA50540CC983956971D6EFA352C02554DC760A5A91358EA56370884FD5B3F85B70E83E4697DEB1705169E9C60A74528CF15281CB1B1C457D467B5F93A60373D10E0CF6A837AA3C9596A72BEC29B2D7E58653D533061D381D51759752217EB46CAC7807C4AD38B611644ACF0A3F26B6B084AB47A83BF0D696F8A4768FC35BCA6BC7903B2A237C27749F5510C863869E6AE56BB2AFE4771C9221874F50F5B14BAAD5993B49238FD0A0C9F79B7B4584E41301F7A885C9F91819BEA00D512581730539FB37E59E86A6D19CA25F0A811C9B428BA8614AA4F94807BC031CBCC183F3BF07FE2C1A6EBA80D5A706EE0DAB27E231458025D84A7A9B0230501116C290A6BB50626D97B939850942828390B0A2001B7853AD1AE9B011B2DB36CAEEA73A2328E3C56485B491C299115A017C907AB54317260A593A0D7BA6D06615D6E2CA84B860EFF3CCB597211BFE36BDEF8069AFA36C5A73392722650E4957DCA597ACBA5605B63C163CFA94B64DDD62301A4332083361972589DB0599A694DD4547A5EE9196577C22ED427AC89BB8BA3753EB76C41F2C1129C8A77D6805FA719B1B6CA11B740A78A3D41B5330526AB87D58D5925315A1485EDC647C1604EB38138DE637AD2C6CA5BE44E1008B2C0867B229CCC36619E2758C4C2029EAEB26E7A803FCA305A59CD585E117D698ECE011CC3FCE54D2E114545A21AC5BE6771AB8F1312\",\n      \"dk\": \"69F9CBFD1237BA161CF6E6C18F488FC6E39AB4A5C9E6C22EA4E3AD8F267A9C442010D32E61F83E6BFA5C58706145376DBB849528F68007C822B33A95B84904DCD2708D0340C8B808BCD3AAD0E48B85849583A1B4E5945DD9514A7F6461E057B7ECF61957E97CF62815F9C32294B326E1A1C4E360B9498BA80F8CA91532B171D0AEFC4849FA53BC617932E208A677C6044A6600B8D8B83F26A747B18CFB78BEAFC551AD52B7CA6CB88F3B5D9CE2AF6C67956C478CEF491F59E0191B3BBE929B94B666C176138B00F49724341EE2E164B94C053C185A51F93E00F36861613A7FD72FEBD23A8B96A260234239C9628F995DC13807B43A69468167CB1A8F9DD07EE3B33238F63096EBC49D5051C4B65963D74A4766C226F0B94F1862C2124C8C749748C0BC4DC14CB34906B81C5524FB8100798542DC6CC2AA0A708575EABCC11F96A9E61C017A96A7CE93C42091737113AE783C0AE8755E594111EDFABFD86C3212C612A7B62AFD3C7A5C78B2F07344B789C2B2DBB5F4448BE97BBA4233C0039C0FE84300F9B03AC99497E6D46B6E95308FF84790F612CF186EC16811E80C179316A63B25703F60B842B61907E62894E736647B3C09DA6FEC5932782B36E0635085A3949E694D7E17CBA3D9064330438C071B5836A770C55F6213CC1425845DE5A334D75D3E5058C7809FDA4BCD78191DA9797325E6236C2650FC604EE43A83CEB34980084403A33259857907799A9D2A713A633B5C904727F61E42520991D655705CB6BC1B74AF60713EF8712F14086869BE8EB297D228B325A0609FD615EAB7081540A61A82ABF43B7DF98A595BE11F416B41E1EB75BB57977C25C64E97437D88CA5FDA6159D668F6BAB8157555B5D54C0F47CBCD16843B1A0A0F0210EE310313967F3D516499018FDF3114772470A1889CC06CB6B6690AC31ABCFAF4BC707684545B000B580CCBFCBCE9FA70AAEA0BBD9110992A7C6C06CB368527FD229090757E6FE75705FA592A7608F050C6F88703CC28CB000C1D7E77B897B72C62BCC7AEA21A57729483D2211832BED612430C983103C69E8C072C0EA7898F2283BEC48C5AC81984D4A5A83619735A842BD172C0D1B39F43588AF170458BA9EE7492EAAA94EA53A4D38498ECBB98A5F407E7C97B4E166E397192C216033014B878E938075C6C1F10A0065ABC3163722F1A2EFFEC8D6E3A0C4F7174FC16B79FB5186A75168F81A56AA48A20A04BDDF182C6E179C3F69061555EF7396DD0B7499601A6EB3A96A9A22D04F1168DB56355B07600A20370637B645976BBD97B6D6288A0D3036360472E3AC71D566DB8FBB1B1D76CB755CD0D68BDBFC048EBA2525EEA9DD5B144FB3B60FBC34239320CBC069B35AB16B8756536FB33E8A6AF1DD42C79F48AD120AE4B159D3D8C319060CCE569C3F6035365585D34413795A6A18EC5136AB13C90E3AF14C0B8A464C86B9073222B56B3F7328AEA798155325911250EF016D72802E3878AA50540CC983956971D6EFA352C02554DC760A5A91358EA56370884FD5B3F85B70E83E4697DEB1705169E9C60A74528CF15281CB1B1C457D467B5F93A60373D10E0CF6A837AA3C9596A72BEC29B2D7E58653D533061D381D51759752217EB46CAC7807C4AD38B611644ACF0A3F26B6B084AB47A83BF0D696F8A4768FC35BCA6BC7903B2A237C27749F5510C863869E6AE56BB2AFE4771C9221874F50F5B14BAAD5993B49238FD0A0C9F79B7B4584E41301F7A885C9F91819BEA00D512581730539FB37E59E86A6D19CA25F0A811C9B428BA8614AA4F94807BC031CBCC183F3BF07FE2C1A6EBA80D5A706EE0DAB27E231458025D84A7A9B0230501116C290A6BB50626D97B939850942828390B0A2001B7853AD1AE9B011B2DB36CAEEA73A2328E3C56485B491C299115A017C907AB54317260A593A0D7BA6D06615D6E2CA84B860EFF3CCB597211BFE36BDEF8069AFA36C5A73392722650E4957DCA597ACBA5605B63C163CFA94B64DDD62301A4332083361972589DB0599A694DD4547A5EE9196577C22ED427AC89BB8BA3753EB76C41F2C1129C8A77D6805FA719B1B6CA11B740A78A3D41B5330526AB87D58D5925315A1485EDC647C1604EB38138DE637AD2C6CA5BE44E1008B2C0867B229CCC36619E2758C4C2029EAEB26E7A803FCA305A59CD585E117D698ECE011CC3FCE54D2E114545A21AC5BE6771AB8F13122FAD295E745A503B142F91AEF7BDE99998845FDA043555C9C1EE535BE125E5DCE5D266667E723E67B6BA891C16CBA174098A3F351778B0888C9590A9090CD404\",\n      \"tests\": [\n        {\n          \"tcId\": 76,\n          \"deferred\": false,\n          \"c\": \"161CD259FEAA7EC6B286498A9A6F69F8B262A2E2093D0FBD76D5DC1C9FDE0DEDB36581004CB48112F852E7F87F649E8A42CD9E0349E7DABDF0A9AC1B521C37EA5241370A8AB2911CC79902C95D28224FA8896AD715209ECDD5D784E91DD9D0BE916B4565F4D5669AEE0DEF931E9768294EEC5258DE8391ECE271E7E4CFD9D23A79FAC3A8E0DB5DDD6E0107235688BBDF7BC5D5632F206C63A0C9564F30965CA58C69FF92D25A4F93A09EAB9B9085947E078A23E4D9C13B8A56E73E18DF42D6949FAF5921F2E373D450C8C09D07B152A97C245447429481D498BEB7256BC47F68F9922B0B1C62D9C23F9F733DD73792CFC7B43CBCEA277D51B2B8AD4A4F522F642CAD5C5DEB21F3627F8AF4D3E5BC9E91D4CB2F124B5BD7C2F4A050CA755BDB8056609663FB9511C9AD83B5039088CC01F0DD54353B0DD7433F0C6CEE0D075959810DEC5416522BB1F1F65547A0C2E9CC9BC17F8D39D29309EBE79F21331B75E12AF2E93F03F74F7F87D360F1DAF86CED736092A211A8158859C42E223CFE2E6E553437D80576CFD1944E97EEFF9B49E5ECCFC678EE165268DFE3D3596B4B86204A81C6063B0CDCE619FDBB96DF7DE6E0BD5270B4D59C4DC508476E7F0708F98C7A4F6645C49D06100C760C599528D1B8BBFE628191CC083C8D225A093F9F17E35574986F86BAA46898B589F3CB7DB46A45F3EDD4FAC20808F4CD0249DA693F8FABFBD4E10C02C65BA8C8610FA8C6DF3DBAEB6763DD482AF41558B1E15CC9C7A72E071685AC19A051F19245B9F77C3038A54E2958623EB8105955609E27D67CF72EC5C4A8E9B9C2924A9E2298508BABA13CF111FDFB062C9607AC1AAA6C637310A8894BF0B96F0C19136186B618DFFB275528BED1CC2715DEF412F77A3CF96645733B048A78474320D1A380F5EEDBDA21FA0125C91D3C37C54BF3752A1F8471C81FCAE2D3EDA966E14E66F223B054D79848FF9411D634024A098970ADE6A88B5F9069F760584DC4CFFFCEA8ECE11BB5566BD2360AB707DF2D21B67488D931F020069176423E6944490CB385E70B358A25346BAFCDD06D402FF24D6C1E5F61A85D\",\n          \"k\": \"DF462AD68F1EC8972ED9B02D6DE0604BDEC75720E050497351E6EC933E71F882\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 77,\n          \"deferred\": false,\n          \"c\": \"5C26D456C6C7B0E8DF0B125E5D5428FE393655127A5E05BDD1BCAC14C47493783097B6185058FA700555DD8AF10F0F979A39A603826FFEB0B44E9487539F3F1A07C673E96640DDF754C8B98CD83473568B49D095F682C1ACF0E160AB93EB41A16A57D53B419620D351C837315080D530845CF8D63CFCCDB6E9DFBE220A2C14221AA392E6337FA364DF0D2E0398F15AC3DC822B5DD7217081107A45C8CB8EACA51E034117962AEE7EC0EE212FA67A5D4B07D355A0981E4285116ECF5CA9FAB6E3105E4DE4AEC5E32938A1EB91E65CE7B39C3B9829AA1E72B8092C3622E519EE092FAC8106D6597CEB941C763288723CB55044A36D4181052A78B424B0DE1B0260F624A8D3B317095371EE9BEEA9272250D598AC63C2138D23F99087777A902EBA2163171A07546B72FCE7F86EE3B1DC1B8EAC85440B8D241742C3771F91BF981909E4F3E2505C594761259ED3AADA6AA09181B99037A395D66E6EE4BBEF97DE6BA36C53A1808CBA50938038C151603105BD6A4199EA44BF4B08961672598CB708F896E03CD9B8F8AD89DECFBE6BE0EF0006B7BD2F4AA6EB21C0218EDE601D46924CF391AE3A44E43D96EBE84A630937C3409EF0710970C27E3ADD4E64DC64E83942ABEA9CCF498EF1FE72B254043D2775A37E0B5DDD3F596EA131E0734AFA9D0223F4CD9D1AB7304CA979AD37F717BEDC3A9526F8FC94433FE4614F82E709456F39BEE7BACC84E5A70114AF1C2AC8B9B3FAA81C8F35F5A5D24189E1A457F58166473F5F1DF0170AAB5E4AC8FC719F945CCBE6F2FED24B23321D95C4C850B278B8C4EA02E3098D5A599AA3D842CF889B7F284AC5E6E66386D63F2C860B997966B4DF2C32288A50045012B7362727B856AF4F8258509B563758752FFBB1040F3C2AD8B0DED64FC15C95C1A16DE0DAE6625A9EFFCE190FC7F3261D844C114913C6B1152A258A37761B81879B59C37A1DFAC07C3E934510B45DA44C2581A79DAFBF00FABB207306269D9B74B93F4367B3BA22CCC51B362DE16E49D9FDBF8CFF84F6CE6892CA2245D34CEB9C8759E702832B66A572DE9F3016A38F7328700F96B2E947\",\n          \"k\": \"A4A24E182FEA12FF128AB2D4AFE6569817513FFC547DB70636752C9C66C002B8\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 78,\n          \"deferred\": false,\n          \"c\": \"79E255908B83DAD198AA6EA7219D5C170DA8548B172A2C28D53EB890914E16A6CE4405E8867112D35228DAC037743E25D26D720742C95935218ABBD93B4EB1C145794697EE761EA567BD561C6F5C076A48A34485539C49D23784606432B4913640644CDEE799961E5332E9502E683FEE98C9E1D071D8976E7F652EEE92E736D598F3B4D7217C0ED30FFA7DE590BBADCC0574A7280E502694A13A4E1D5D8837633A2EABDC97F36722D772A380595859134B9ACE346360860F8E60EACAB4AA3F9CF1DA73B5813F773008E0153B1BA0A5940DBC5C9E71E9A46BB4EA04AA9757E8E1AD0209C86334D05FCB611F3A00C7D983C7B9C160B7807CED18E5BC64A52462F4F9438199C2E4C6E9E70EDE2614913BE6D0C28894319B7B646444B5C86FCE61297EC11B21D216AC79159801ED3181667B15A7F30873BFD5727802E7B6588BDD04A5F7CFCD47043E600B4B3A0227E924E2CB92E514547BE4C1236C7AB2139F986AB956C704485DE570841F5857108D2AA57C535B3D44D0535208D501A9B56FFCBE8FDE32B375B90A5578EE44940E1E1888C21A4045D0338149D4C80CEF47BA25558E1842116E1E25499714163C0EE9A95A87A27CA2A61C4BD8D28BB04DE34EFB6E44FA7026B158883019B89AC4A5B5CA8F347A3FE892EE3949BD40D0614B9923052ED174FFBA720F516B6FD1317754A95520C66E3907B32A1648B344C34B3FA2ACEA2C8410DEEB40483529AC7D83351D888E968E457644CD76B8CAA55FC25BA1359F4A50119B1E69242DCE30E93983E50285DC0592537C6202F2E3C9878067A1777EA6A4E5ACC31614AE52787454FEEF503B82492828A736BF22E3278CF2ECAC1D0E11EC67815046CB4A66A8F48D04D4FB3C91CE7C251B37A8F3FB62A37489FFE63BDE22BAA18D4AA5BCCC0D8C709786B6C94D268382B649598A7A6785582CB2C02A2E9BECE29AC919785CCD026ADB6C9D8E85C3332DA956DC20B8470F8DD78B47E19B49BA5B27326D4937E93CC3453BB67EAFE42CAB03A70960DF236C04C344CA7177FD1E72E7E0A2C10D14F0C054337BD14152D4AFE9BB6243260E696EEE1327\",\n          \"k\": \"3B506D5A3BFB30D82FDD45B918F032A4023B9692D7EA6426FB2ADAB7DD5E274C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 79,\n          \"deferred\": false,\n          \"c\": \"D980E3002A28401678E1641E9E7A34D12CB2B4C9D986C7DB0FAB941AA83E43F42C44368138ED65B7A917B22ABA2DB0FBE44E5E9E7A3DAC3301379F7152585C8C7195031774CBEC61D8E1E093D694970B5377DC94CEDC53B6D3802BAF1C6F9152E05DCF66C1643528155C78118DFB00646B90726C75131DC934DCBB706ADBEC64E07F0D113BF71D4205D8E47DE67B9E01B224B82CA24405AE5591BBB1307D44E405E3866B1BB51ECE5985EE95D54568A81E7F285596DCAEBDC807EE6C8322EF2100EB38D10327DF92BC10A74A2D44842AA02AE9101A24736949D116CEC81F30C3092AAD941FC7F4BA10670CC0894A2F81E3155B9081004B4ADFB6532A1458F727F418D3F8F228E7425ACB7A4E4A3653529D1B9F72E57B8AB5852E35D0093B548FCC354A590C256B50BCADC30B55B5E05A3231611C93D5E34775741374F3E703B6E6362B35A68E33D859918D93EC03633220C61E1B81ED7AB1A5E46D4E640A9DE4E5A19FD11F0C24C556FE8D91F2358E7E78033A3C9FBF68C99DBA351F8F866CFF14E990C29E4A47579376606EB85A9D07D0BFD835C7670A5F4F2D4EA62B2BB13528A27BA3420095B852E3B73AB38E3CD068E276D8BD7A0B85BEFA48FEB72EBFC5240408B069FBC28F48B3A8E6556D7C601ADF98F0E9D64108F0BFE3C3B56C800D76E6DB14736809CEC22FA811EE92EA7950421B22F613E1349D259CF877075D476798C66FF58DBED013675BF6C5D3704528FBBE91486D7E956F785F73EDB6EB42861694F5C27E318C7B481377770125D99F236875D5B26CC8ED9859393BA2D8531E25FD6E2B3560BBC13176EBA638C72626C32D0250AB7F7EFFE661B18A641F75AE279C39771C7F23EAD50CD3AE461BB0EAACDDAD4F9C6436EDA5F2348F0ED4CD9514310AAE609B539368F53EA787ACD630080B832221B1CEF67FEB63CD2DDBA25282B020A3CB418130AD96D66EBAC09F09CB35BE0539B44924CEE15C6DBEAEE04C5BE5B9C43535FCA64B32BB204497AF175513375971B15F107F88980F0C0EB15D34762C98198A94EDCA385F32FD82CE7F6FB36F12B742C1755417A8D3F7D8A9\",\n          \"k\": \"68EE2117F8A66503091AEF490D1B9DC9EF3B3E62B97567F46A5EF2328263E5A6\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 80,\n          \"deferred\": false,\n          \"c\": \"E1E906D018CCDE68680DF762DE4612106E918C29FF5D576D8FEF01D5241B666C26CB7B2F80793146AEEB9308511D9BA264A9A05D6621425730A50479B889F8C6E5AF66CFC9A3123D3B7335C06C8CD2F867484240E8B6C19EFBD3C15C33CFD10EA482D1897A516D07E39C3C1AA866C10655736F18689ECE7359D91E6EF5CEE957930258CD890C09EE1714150347A18DC97AD955B60750624755135AC81AFF8352B701EC5FF50AD925ADA003A617AE64DBFD305038E1E40108C6F12CCDD7738A83C9F7A76164F670ED4756097426700E51BA02EE36BF12FF22A316790F2C2FE7216C12F03023D87E2ADB99683229E77D6B1938EACF10D8686CEE46127CD7652A33FC05414FE370A159C516D250F7D345BAE5E1C9628A65FA9F5ED9E39FA10A316620A2D760C5DC128A5C6137F193226D18B5E013E300A41B1F2D1B47D90E3DF8B4FD71A794FAE0404570261477B32DC80CEA32F2DE743ACF7EBDD41EDBB0119EAF7F872A50A5F4C92A8B85DF792DBAC764C3A9A5A5C12D9E3913356C7F5463BEE9BD2A739FD485493B1C0DEEF716E129ED1E085F146E0D70A7E58D924F576F948BEBEF7842ED831FFD58B4656F91686B4D0F832DD3A4E6FA9F11A4870E9602E0DF0EF4FE9312EC4C7EC216D3EEDCA2076D0F0B5C9FE139145222347E816EBDC1AA70CBEB5D65954427A3DF6A78BEA86C410462596950AB8798F9BACE51A46A544C1BD17A86C2995E3BC82A7F965401A599103B0896E1B9EBA540614CE8F218DC7290103A6044E87069286E5BB18CBD89EF562B6AE1C7353F64D8CED183FA8D05D6B6A6633751EF342D839562733CDC1977684317EBC71378EC02B298671F76EEDCC3041E943A76ED9E0C496B798E10B59BAE17A195544C05CD1FA6A161358EA1D4DC7DF454220D79B235C24720021174C1BF47859CA30BCB57FC19CAD92ABF24C051D48B3D9D46779F910D26AAB4543DB2A0044BEDD491E1D72D8FDF361B50F1FD10AF4668D78F56FDC7E96CA16DCCD9BE8C1819DECAEBBECE41C09093DD508191562D6510307FA4685144D9679E84F58929D79E693DA041B12B76F629912B1E49\",\n          \"k\": \"B5191E505481428549AC5B5548EB747FE5290D51DAB6D49BD15CBD702129EA45\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 81,\n          \"deferred\": false,\n          \"c\": \"3EAE23CC5F424EDC10108FCAE8EA3AC2BE8E90EB6AFC438B5A7DCD8E149AEBA25F0D5B25052C030F8157CC5BFB876A62F6A85B6C1C954F7C0F99EF4E3AE4B48C1CA9AF035543ECA1069B067057FEFB1E50FE0374F4162F0628F1D383A8B111EA9DE854EF33FB79488AA81E75712E5B9B6485290F0956B0574A6A9E1B4D677A832A85717CF7FF5A9E23B205C4FBD4ED7C2F7C5D91F46CD6A1EDB692750A4C1B11DB15C5643C7572FF9B765713C5C97C05BC2B861997CC6CC2C4D82CC62A32EA361630454756138C015D5501E362BC4E2B03A7AD679293658E45CF155B1C4F165954D594871CBF556CFAD2C3E6EB238DA3FF3A8140C5FCB74A278ED495DD14849D4C874C3E1F6E56EE657238F4E927FAE4588F1628DECE45C625AE0A6137868B9E86CFE29CCB4483CCA6FEE905F084B2B03A84DA421417CA5087B19654C803CF072B3C9E37A70B24E30E2F52B1DFCB6817ED05D38BB6FC7558B9B96AFA0CDBE708025D8D0454B90767753524CEEF8372150480BD104F1B7E659AD28EB155842CA81A55E81B707DAAF2F42A0B1CCE0B3BFF23F5ACF984ED20B0970ECF973DD0D5E33D34FBFD1672BFFF6725B5F1F869945FC67C5D01F3ED1CD8CD43A2008181AF7F65B0922D4BC634670AAD8A23A698AC3675EB3452DCE23D7E1A130964CCF4E26A9CD3D424A54ED7861E2D807F9C98E434A78695EFAC8BC86C69CD5911A2F52B5DAF50866151C5D00FAFCAB6219A9BA675413B4BB28619CFFACA38B9ABD1C3647BCE412336C02044EAA752B79248EBA1A7AED403801DAE5377CD55F517432B677A75DE4D4B504EBBF6453E319BB6EDACE30EE44810332CC84CBFFEE2B20548EEEEE1CA131AD87CCC284B3677E7F632D69F776060005439DE5648E466AF68C6616C63144451126D10311798A9B311064301BFAC1E4641830B1FAF4963F14A740529C360A73A351B6D330364BCCD2B012CD2B571ED243BF63F2FC1F1963604923B397A57680290D413FE7413B2C6C01D5BD6A0E314A644ECB10C69418251F48D3C3941211CEDB083F6FCDE24C5F5832034780B539D3FB1493C631F0C10F2F50262FA\",\n          \"k\": \"6262EB082F7C05044FAD90335BF60D117E52B382BACDDAB97D776CCB427AB672\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 82,\n          \"deferred\": false,\n          \"c\": \"B3B6B84D9A33C59758CBEEF8EA26540D4E3D4A45BDC623CA1D0AC05D8E780D2DA1FAB26A0E250527FB0B9BD56B2A0686BD0FD310164A17244374B82FC9A93FC0AC6067929C4718B2054C7AA4AF1FDBC9EBCC55A787F7C0B98A8E6181E5604E8F7108B181AD1385422EB747286FF72BD1EF650AB88865BFC37EA5536A220C29ACE17F8AA82A77F92E0A031E526171C44BD5FA1E7946CD063B1A7E113FAEAF92015CB3CCFBDD9C5E0329CD3DBD1B8CA90EC226ACC27716615B5998E0F5A5BFBA347FDD3DD851682B8968858F4A73FE5FD952CE7FF597185855E4B7F76F44BC1B24DB7C8C3A37217DBDF0BA7168D91B59DE9EA219195D29A5C67327D51D4E05131119C81722794D825B9F01DDA93C74B176545E32E638243891EE09E2AC1A9693C83D4BEFACCF25C81554802FC422C75812E18BCCF4D3CF208BE6EB16FB4E82C4ECE33C838A0B3D3EA4C027F41B4027643D9E4B6A7EFBD8D42A65B29786F4A00C16ED4492F4E945469C6E03A9A297AD9763333A2B9725DB5C6F8DF1CA7B0E77F5E6364FB6E8E528578350A04E4E4617E72E5FD67FE029AE2D738D8DFE24730D9D737D8E30ADCB602102FE2D99B915C9B04CDA463D444ED9C6E6A71BCAAFE503BF1D15270DEF8B9D7AA5557177EDEAD75E2FB01A4635A46D2F95DEA6314DE4965EBA8358210F79E64933AB4B6600856124363A47C6063433BB670266AA8FB968D947AD96D97C4003A50B0D1119E3A73E00363AE5EE85B5815A5BD944280031F0DC9B98F1F5C589F259A486BFE26EE1446D937EEDAE41275AB72E0CD15EAA6368F59686DE08E147CE2F5978B366D0A4F98ACD7D4004D1D0A4897A0DF5B1AF9F811BBAC64952D10E36A3EF78D379EF0E95DCD2D804C07AD8D1A8882FE1F2FF188F31B886BB597FF16F4D597EB337319AB4E81565EE4AC0A9BB3B6C3184C9C66511D7313555EF703194A747D0857DD27F92A6DE12DB311828B684FF3F1D848D5E92E0EFD7BC6B3EA7039296D587A075781880039A7C0DD6DB66EDEE3A22F7F2EF02B267429F6BEE16F214A59EB96CA79EC5065784445ED2FB631BADF6645991736BE7ED\",\n          \"k\": \"94EAE21B192F9D8FEB94E72B8F24BB0E1442F1F569323B202A497DCB64F9791D\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 83,\n          \"deferred\": false,\n          \"c\": \"FA42A8B407B527A8CD9351560BA4DA60756B27FEF326BC549B3A4429B2E58EAF22B3A36AE554B416DCE209E8CC708846312992DCDD43AB177347363F81578B94F451F1D046233BEEC6B42C9E0D55F3D741A55F7C564C4A9D5ECF6B067723E4403A17CBCFAA00E2F8D2EDFE1E236AE861011A5DB659042AA23BEB01A0471D178DB91039EEE5FC7EE85AC6FF3845959E5001C61CD1756EC681C97F4A70887884157D664A505ED7E4E1F4598EBF8BCDC0BEDE7FC0A89B3E14237187CED97BB0C0E54D21F4DF47BC8FC3F863978DBB673835D17931B7819535C1ACCD8706F8726BB0A0DE20BB824560AE5BAAE2F0BF0E3E676FF74C681474534C857837E7040C33B7F031AD9900A29DCB71BC305DB0ABF92CEB5DD2EB8E644F23AC0BFD8DCD2B44101FD7CB8A287318979BAE754661FFB13097B2A52B50236094693A754DC97CFAB550877A4D8C6CBA8B4A2E3D719ABF0EB13D40976B9E3F6C433DB1E16D794466D2C023988528AD0336CE43636DD50FA6A5E899578EACEFACA5FFC5B6FCC8C53E21503B83ABEDF2174FE08B4B960476934C5D6021829AB7AA7767492FAFBEE492A08524FBED46E8D0451C6BE1BE02B55653326735B0D8CCE951A5CF534E3547731EE36EB9BA38E0AE253B8CEC35001EECE0058E634A11F59FD6F21C1A3882E291F59B1FE3EC7F55315E0A65F9D011210462A8CAFC9779208452FD4F3B64FF456EA8588D2CB394A9169F1392646880A1C63721A2277FDA432FC6EBB61FF87AD473FF41D831DD95111CF0A1D69F001A008C3FD00B46F5342EDB8DADC818E6470D21C915F3E91992806E5B18DB314F9592E0EC8B8F0DEAF92DC89C194449A2539BE7C6A1B01B6F3DC496CF33CA25825B66971880652BC6E4FBD901A286C50D625F0F682B0B4CC769EB00940C45ADE947844175A3BBE8DB92BA6DAE5BE456CEA41384BB29A8C9D4E08F1375D4865A69A59619724900DDFAE48A2D12975C789E76104AE114F30ED4F836E46BCD8CA7520F4838651225894595C4F7BEFD7ED41EAF6F395EDE40F988CBDA7E08122A61E552801C7F3E84039FFF17E3534610D3434B996312\",\n          \"k\": \"23B74A4AD3F8E3EE73481A768E1F5CFAAE068ED38C0AE1E7A03159D2E9B0BA93\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 84,\n          \"deferred\": false,\n          \"c\": \"9F972164967C0CD03A3DD68714FE0B4EE0EDF9ACF63AA068C10FA947F8A03264B4309EE61C8C9B0C03C5FEDFC7B77AA862DDEFEFE394FB09A2396097452585ACD0CE510324A03F36904AF07B765575DCB3B1A84131C352EF14C2572E39DDBC8118875ECFD7EF7D2E41D9C9BE858FF08DBFABF8A80BCF18FDA8735F440D9B8FCAE0E67C5BF0171B99800BBF0F3EADF76F9FD69BB0734F1356C53EA9CD64E86C14C084BC3B1FEF040E5FA939F8F0D5171AD02628AFD8B02DB7D7B5C3B32F1A8EF3AD4116ADD4502414163C14D49EC73E5F4B25C5BAAB82C73401975F2119C569E1F2873DA202F32BDFB76F9AF49F22604D1B1BB173DDF6ED70D82B360C13822F5F9BC4C4D5F2391E4FB6BCB723A56666087A55E033E50202EFBBE7DAAF96AC541C855AA4154E37CDA55B1BEAB005554947F781512E2873B5CD8B118EE0932DB2FF427A15BD114D7DA79C7D899FD820A0222DF90D8E85CEAD8A1BD96A88D6D58C0A4FBDE3AA55DFA1E4B12AE6964DEDE20FE337E4BA5EE8B67CE1ADAE9851D021A56B999DED62D0E4471CD928E9AA4AEEC5C878199149D82C3CF4FCB68F63DF27842C37E52182A7E3B332F24948F3646874326B4FDF215524A1095A224F6EB02355974A6DE9746824A3954B700903292DA43D5DD51DD9D8E98E63DD01C357E4913855190049E0F1A8D9725B095ADAC4885FE832E0BEE82BF3DC355668093B475FFCF7D92228FDEDF0451C441B345372D6EE58408462E2C3BF22A095E5E23A159397FC959C126CAF936A3E64552003FEB2B963AF7F915885445EB25B934D659900DD0506A5FCB7168392824945AABFCCD01D9EA8A2256FC8E7AAF0C4243025A9F47F295F9D2713D5257D626057E904E34B8C0530A11DF2D15AE6BF1ADA6971B233B5DFB59EF8B9EB813E7E52794883BD6D676119B5B86333CBE6427F97ED719C432127805A9790837A1EB04B82907A59CED1286164A9F02716CDAAEE48799599CB09F5CA8BDE83CE8278382776CC3246EB2C0EA91C1A9D0CD7406B419A22CD6115018B9641405F9F44E13D2CD6AB457825326FC5CDE85C94DF86097BFB5204530FE8\",\n          \"k\": \"E9A6006C6C4D5A51829AADEADE89CC104358D0823BA8CB5AF4599D59E1679638\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 85,\n          \"deferred\": false,\n          \"c\": \"082411FFADCEE22B6C33277C32130E4C77CCB1849A2E7BDCE47EB519CAAACAAAA8DF129C5D876EAA7495ADED159D27F525EDD5F1F86B7A4FD50AC0B1F7B07C23F726D96F82D17818EEE6F032D0AEAD04D0F56EC244218905FD779268B259E29BAEF8BC66B42F47DC5BBFEA06620F38E0F373BA3F598CA7244A9F5B6823CA293BDACDD6D7B2E49BB2D00D1811C0F7FB2736876699D3F115C1D5AC58EBCFF10F514D863A56901F3DEE1328ACEF5D37DFEF841392BB29A88324CB51820A0CB30A4C222F7450F321B6617EEE7E722004AEBB5A52ABC3A984B8A142F0193EB90654FF86B8799EF7BDC01BBCD7C151587557334E01B833E950260C5E126C2BBF35EC030BBACFEF2812819A20960A9CA4E8D4836A7282F8F99AAC18BC02F6275582C7D1E6197938F67A80FB2363BF77A96355FA9E0AB19883CEA65A3010795E4A48A8B22FD04EC4578DA4452DC1B851C03A93AB147F3A34981515B75AB80D10A96570C2BF9ACB2E1662CF86E077EA455ED1B130D59CCA1F603A3471C408A342C42BE1AF6BA3E096E78CCF36CFCF6705078800E4E968FF372CE836AF5090E2442CF73E565146C69CBC0F55DB89BE1179CDF24DB6DD2C73371B00BD8CEC89FBBFAB3537DD0F50156FFA2D604BD135B91728DC93AAF31EBB51BCA15C02270D93051FBC0CF006C57F6BDDF5B8E60866E7A051358C4D0363ECB9A5EC3B6C745C41A3EFA2887B6B5AD8DC68E3C3FA17291D3D044D7085C6E2D3EB12FC3536CA8A6BEAC7B55BC2DD77B6F102C577B988E03AD963FF34CE4DFAF5194A05F12606D8E62FA7E20329E6630177BD60BCE780E014A856207A2745E5A22801A680CDBF0653EFC71F263E795AD7C495A90B7A5BECE0CC3F879B411A39A4346C677F53094298C0B2596DA1B136A32415E68A249161217414CC0F5F4D40614E162A3A757BDA41A80FCD17202AE062832D971FFD0A2F66D5EE94A26B1B78582E9F79F65A20D94EAC98DCC54D62B191DA89108126143E810AF6F8345723C69C009C481837FCED2408A8E37C96A248D7DDEFC7BBF73A5A91BFC10163813D22B0B26D5C6E380CCFCD6598844913\",\n          \"k\": \"3136E97F0A1CB0208B1CD89E510F2A37A5412AA5A2012E24327572886DD69408\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 5,\n      \"testType\": \"VAL\",\n      \"parameterSet\": \"ML-KEM-768\",\n      \"function\": \"decapsulation\",\n      \"ek\": \"F80751877A80FB724A0210C3E1692F397C2F1DDC2E6BA17AF81B92ACFABEF5F7573CB493D184027B718238C89A3549B8905B28A83362867C082D3019D3CA70700731CEB73E8472C1A3A093361C5FEA6A7D40955D07A41B64E50081A361B604CC518447C8E25765AB7D68B243275207AF8CA6564A4CB1E94199DBA1878C59BEC809AB48B2F211BADC6A1998D9C7227C1303F469D46A9C7E5303F98ABA67569AE8227C16BA1FB3244466A25E7F823671810CC26206FEB29C7E2A1A91959EEB03A98252A4F7412674EB9A4B277E1F2595FCA64033B41B40330812E9735B7C607501CD8183A22AFC3392553744F33C4D202526945C6D78A60E201A16987A6FA59D94464B56506556784824A07058F57320E76C825B9347F2936F4A0E5CDAA18CF8833945AE312A36B5F5A3810AAC82381FDAE4CB9C6831D8EB8ABAB850416443D739086B1C326FC2A3975704E396A59680C3B5F360F5480D2B62169CD94CA71B37BC5878BA2985E068BA050B2CE50726D4B4451B77AAA8676EAE094982210192197B1E92A27F59868B78867887B9A70C32AF84630AA908814379E6519150BA16439B5E2B0603D06AA6674557F5B0983E5CB6A97596069B01BB3128C416680657204FD07640392E16B19F337A99A304844E1AA474E9C799062971F672268960F5A82F950070BBE9C2A71950A3785BDF0B8440255ED63928D257845168B1ECCC4191325AA76645719B28EBD89302DC6723C786DF5217B243099CA78238E57E64692F206B177ABC259660395CD7860FB35A16F6B2FE6548C85AB66330C517FA74CDF3CB49D26B1181901AF775A1E180813B6A24C456829B5C38104ECE43C76A437A6A33B6FC6C5E65C8A89466C1425485B29B9E1854368AFCA353E143D0A90A6C6C9E7FDB62A606856B5614F12B64B796020C3534C3605CFDC73B86714F411850228A28B8F4B49E663416C84F7E381F6AF1071343BF9D39B45439240CC03897295FEA080B14BB2D8119A880E164495C61BEBC7139C11857C85E1750338D6343913706A507C9566464CD2837CF914D1A3C35E89B235C6AB7ED078BED234757C02EF6993D4A273CB8150528DA4D76708177E9425546C83E147039766603B30DA6268F4598A53194240A2832A3D67533B5056F9AAAC61B4B17B9A2693AA0D58891E6CC56CDD772410900C405AF20B903797C64876915C37B8487A1449CE924CD345C29A36E08238F7A157CC7E516AB5BA73C8063F726BB5A0A0319E57127438C7FC601C99CCAAE4C1A83726FDCB5045ED1A82A985EA995396D77272C66CE493289F6110910F37C2741CE47026A6F8261999C6482572B1693912EF12EEBEA7ACF9234FB409F2A6090E6B0BFD895469D0B2A921BB723F87A33EA5465AB90F514B67698C0768B6CA498B022C512FA0875F054AA2265867E31C0E522651E024A07D60DD9F633166921F4126BC2B6AA01CC15A09B85BFF8218C5AAE95BC1FFB26AE5A137670F04910CA9D7241B6660C394C5455917746A26682FB71A432EA9530E839BDEB07433004F45A0DDAA0B24E3A566A540815F281E3FC259AC6CBC0ACB8D62268B603BC676AB415C474BB94873E4487AE31A4E3845C79901550890EE8784EEF904FEE62BA8C5F952C68413052E0A7E3388BB8FF0AD602AE3EA14D9DF6DD5E4CC6A381A41D\",\n      \"dk\": \"1E4AC87B1A692A529FDBBAB93374C57D110B10F2B1DDEBAC0D196B7BA631B8E9293028A8F379888C422DC8D32BBF226010C2C1EC73189080456B0564B258B0F23131BC79C8E8C11CEF3938B243C5CE9C0EDD37C8F9D29877DBBB615B9B5AC3C948487E467196A9143EFBC7CEDB64B45D4ACDA2666CBC2804F2C8662E128F6A9969EC15BC0B9351F6F96346AA7ABC743A14FA030E37A2E7597BDDFC5A22F9CEDAF8614832527210B26F024C7F6C0DCF551E97A4858764C321D1834AD51D75BB246D277237B7BD41DC4362D063F4298292272D01011780B79856B296C4E946658B79603197C9B2A99EC66ACB06CE2F69B5A5A61E9BD06AD443CEB0C74ED65345A903B614E81368AAC2B3D2A79CA8CCAA1C3B88FB82A36632860B3F7950833FD0212EC96EDE4AB6F5A0BDA3EC6060A658F9457F6CC87C6B620C1A1451987486E496612A101D0E9C20577C571EDB5282608BF4E1AC926C0DB1C82A504A799D89885CA6252BD5B1C183AF701392A407C05B848C2A3016C40613F02A449B3C7926DA067A533116506840097510460BBFD36073DCB0BFA009B36A9123EAA68F835F74A01B00D2097835964DF521CE9210789C30B7F06E5844B444C53322396E4799BAF6A88AF7315860D0192D48C2C0DA6B5BA64325543ACDF5900E8BC477AB05820072D463AFFED097E062BD78C99D12B385131A241B708865B4190AF69EA0A64DB71448A60829369C7555198E438C9ABC310BC70101913BB12FAA5BEEF975841617C847CD6B336F877987753822020B92C4CC97055C9B1E0B128BF11F505005B6AB0E627795A20609EFA991E598B80F37B1C6A1C3A1E9AEE7028F77570AB2139128A00108C50EB305CDB8F9A603A6B078413F6F9B14C6D82B5199CE59D887902A281A027B717495FE12672A127BBF9B256C43720D7C160B281C12757DA135B1933352BE4AB67E40248AFC318E2370C3B8208E695BDF337459B9ACBFE5B487F76E9B4B4001D6CF90CA8C699A174D42972DC733F33389FDF59A1DABA81D834955027334185AD02C76CF294846CA9294BA0ED66741DDEC791CAB34196AC5657C5A78321B56C33306B5102397A5C09C3508F76B48282459F81D0C72A43F737BC2F12F45422628B67DB51AC1424276A6C08C3F7615665BBB8E928148A270F991BCF365A90F87C30687B68809C91F231813B866BEA82E30374D80AA0C02973437498A53B14BF6B6CA1ED76AB8A20D54A083F4A26B7C038D81967640C20BF4431E71DACCE8577B21240E494C31F2D877DAF4924FD39D82D6167FBCC1F9C5A259F843E30987CCC4BCE7493A2404B5E44387F707425781B743FB555685584E2557CC038B1A9B3F4043121F5472EB2B96E5941FEC011CEEA50791636C6ABC26C1377EE3B5146FC7C85CB335B1E795EEC2033EE44B9AA90685245EF7B4436C000E66BC8BCBF1CDB803AC1421B1FDB266D5291C8310373A8A3CE9562AB197953871AB99F382CC5AA9C0F273D1DCA55D2712853871E1A83CB3B85450F76D3F3C42BAB5505F7212FDB6B8B7F6029972A8F3751E4C94C1108B02D6AC79F8D938F05A1B2C229B14B42B31B01A364017E59578C6B033833774CB9B570F9086B722903B375446B495D8A29BF80751877A80FB724A0210C3E1692F397C2F1DDC2E6BA17AF81B92ACFABEF5F7573CB493D184027B718238C89A3549B8905B28A83362867C082D3019D3CA70700731CEB73E8472C1A3A093361C5FEA6A7D40955D07A41B64E50081A361B604CC518447C8E25765AB7D68B243275207AF8CA6564A4CB1E94199DBA1878C59BEC809AB48B2F211BADC6A1998D9C7227C1303F469D46A9C7E5303F98ABA67569AE8227C16BA1FB3244466A25E7F823671810CC26206FEB29C7E2A1A91959EEB03A98252A4F7412674EB9A4B277E1F2595FCA64033B41B40330812E9735B7C607501CD8183A22AFC3392553744F33C4D202526945C6D78A60E201A16987A6FA59D94464B56506556784824A07058F57320E76C825B9347F2936F4A0E5CDAA18CF8833945AE312A36B5F5A3810AAC82381FDAE4CB9C6831D8EB8ABAB850416443D739086B1C326FC2A3975704E396A59680C3B5F360F5480D2B62169CD94CA71B37BC5878BA2985E068BA050B2CE50726D4B4451B77AAA8676EAE094982210192197B1E92A27F59868B78867887B9A70C32AF84630AA908814379E6519150BA16439B5E2B0603D06AA6674557F5B0983E5CB6A97596069B01BB3128C416680657204FD07640392E16B19F337A99A304844E1AA474E9C799062971F672268960F5A82F950070BBE9C2A71950A3785BDF0B8440255ED63928D257845168B1ECCC4191325AA76645719B28EBD89302DC6723C786DF5217B243099CA78238E57E64692F206B177ABC259660395CD7860FB35A16F6B2FE6548C85AB66330C517FA74CDF3CB49D26B1181901AF775A1E180813B6A24C456829B5C38104ECE43C76A437A6A33B6FC6C5E65C8A89466C1425485B29B9E1854368AFCA353E143D0A90A6C6C9E7FDB62A606856B5614F12B64B796020C3534C3605CFDC73B86714F411850228A28B8F4B49E663416C84F7E381F6AF1071343BF9D39B45439240CC03897295FEA080B14BB2D8119A880E164495C61BEBC7139C11857C85E1750338D6343913706A507C9566464CD2837CF914D1A3C35E89B235C6AB7ED078BED234757C02EF6993D4A273CB8150528DA4D76708177E9425546C83E147039766603B30DA6268F4598A53194240A2832A3D67533B5056F9AAAC61B4B17B9A2693AA0D58891E6CC56CDD772410900C405AF20B903797C64876915C37B8487A1449CE924CD345C29A36E08238F7A157CC7E516AB5BA73C8063F726BB5A0A0319E57127438C7FC601C99CCAAE4C1A83726FDCB5045ED1A82A985EA995396D77272C66CE493289F6110910F37C2741CE47026A6F8261999C6482572B1693912EF12EEBEA7ACF9234FB409F2A6090E6B0BFD895469D0B2A921BB723F87A33EA5465AB90F514B67698C0768B6CA498B022C512FA0875F054AA2265867E31C0E522651E024A07D60DD9F633166921F4126BC2B6AA01CC15A09B85BFF8218C5AAE95BC1FFB26AE5A137670F04910CA9D7241B6660C394C5455917746A26682FB71A432EA9530E839BDEB07433004F45A0DDAA0B24E3A566A540815F281E3FC259AC6CBC0ACB8D62268B603BC676AB415C474BB94873E4487AE31A4E3845C79901550890EE8784EEF904FEE62BA8C5F952C68413052E0A7E3388BB8FF0AD602AE3EA14D9DF6DD5E4CC6A381A41DA5C137ECC49DF587E178EAF47702EC623780691A3233F69F12BD9C9B9637C51378AD71A831055277254CC63C5AD4CB76B4AB82E5FCA135E8D26A6B3A89FA5B6F\",\n      \"tests\": [\n        {\n          \"tcId\": 86,\n          \"deferred\": false,\n          \"c\": \"74A26C7D27146A22C7EAB420134E973799CEC1DA2DF61AE0FA7905A3A47485A063076BFA22D6E4FE5059DE0A32E38F11ABD63F990E91BD0E3A5BC6E710DFE5DC0F6D4A18147EBC2E2D9B179374D83692C53EFBD45F28A2A928C2494F903576C410EB1773895EBEADB119960EEBDA9C3C710795A6D9B781FC58B30D08107F4E20944A382AFB079F31D21724F2C26E6A53412F0A908BE7586F2B3D6D7C1DEA0270E98AA209244BD88ED68AAE01432342BA5F49E015CB476B5B78D15EA77A354CC9E9FD07137D8760BE42FD4746C62C02028E7B405DDC95DF3D021921CFEDDB3D961B957ECA302A263DAB2DC117BEB3E79EFACFCF936DFC09FC0D19C358D724FA381EA06CA067C384E944302C3907AB15A1DA4B41352692ADD59B061541F07EFF25EC42F46E1A0E370CAD06FF3FD997D4D2C5648AF762231B382D0593401936CBA21551A2AE30D8E8EFFCF43916B83138BB5E610364429879FA9CDD5B7D3CF2FEABAA1DC8D50CE69402E21103E795DF7074D1FCF65F8A4E18986D5417780602C63BE5A044863384BD3D8FFB685EAC567ED8349DCF2CEB702B7375B145729998049D13E2CD466CF2231B9D3A20018EE908F8514A6C6A89DF7232F91FCD84B81EBC8BC539E9A37A4324755564BE1BF4FA1FB4571E0ABBC9B52F9D090C33BE599DE6C8532C7CB7EC8B4E2D3C07505280E99923865903FFD18BC13B9C8164AA1EAE84E38D3F57FDB8801785F105A6A8574BD2FE9BF305848E525330BC2D24F0257E47A4950F433A9233E8CDEBA81DBAE7D8C1A06D01F70DE6EF663207D84952827BAB3D451CBEA0990007FBDB4240FE899A706F7C1563E05C70BE9D575189EF83E0CF76195F6652491CCE04F1CE2092170A92E0DD7301246A4C44FC0B4EE6AAA63FC7027840ABD2EC25F654589738CD38B9E10B975CFB6C1D2EB4DA97736998F84FDDDD810D72DA3C5AB13507420DDBFAA4F7750C1FAE9C7DFB30F40A12AEA689FC78DA900020E3ABB32A364D5C6B3C7544A1B5734A41E95C8314B448CD0B738D829AF772A8F81C51ADBA2D85F326C8F5D6961CF12D44A9BEDEA00D1DF5B48F429B1CE0C15EA5F5BC10B017247BA2C6BE922B0563B8E9698677CB6C45CCF2081BF84219D2904C11FF92199F8AEFAD62D8608E200802C5A07202CC820E9E520E31BF36A83002ECA4018B0B3A398801562AA86C77AB0D50A8FBC3768B0A643B97E7F9072168DE29B8175999C9AA48D301A3F0303172E9C7D4F16329D5CA9D42397C3982E10C9DA42DE88BD6C2AB91C1E71E778E58BB8F801F207A88A9B47F9C687AFBBA34EDA6D2899E4FA0008AA2B539711753DC7C07F614E814F683D6C037562AE1FBBE6D7D5FA54B7A6D9451E11B01AACCC3BF2ED64742DD100E0EAB2DF6CCCF937B6D5981ECA0E01F3245CF26A72AD1ADF066C8F5430D72F509963A657D85E554C14E26E8BEC5D5F3AB998C9B29F16B04747D80749B30E51FD2A7F690C22F9986AAF6358D6FAB8DED54971B32641DE2B258590EEAA6BF1F32324A7C4C983F49466D86\",\n          \"k\": \"3D23B10DF232A180786F61261E85278251746580BEBCA6ACBAD60AEF6952BE69\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 87,\n          \"deferred\": false,\n          \"c\": \"39EFB90089F1DC32A54370B3EEDF2B12880DC7D657F0404E41F7DAAA73E7F06CB90BBEEC7544160768EC3B56681D057AE1DB58F0123286D3A8CDD0B414CF9894FDA1CFF3A37CF67B82C5C7AD3427F2F2B393978B94E524F33334E4A98AFFEA8D7514D6E12E85086E58A0C078EBA64435441F3E3702EA27EEA984E46893BB886572491F22AE09F8D50774B4DDD5CF478CB0B2D070437E86645EF62AA83599093732F81A75D1D5DE15C31EC81AC4D67852FDE089D580B71E3DB07C71394424E0936BF74D0C9405BD3DFB60B920E7EFA38C72D5912BBD301BD3F3709CBEEEB7BFD0767B77A8639913E8C228FBB7E3E13C423BF05AC65B7E75F29C9048F161AF1B4B41C495ADB53FECC57FED0DCF792050A2A586C33AA4A7F6BCDA9068EA295FB692BDCA756FCC47CA0A8C84DB5DCB6A616605F3D3A34C4D23EC14942492C07EF123C8D084DF21F3B2141D277FA16E3CF4D5A3AB8D78CE8370F411DF737647A2D6123120AEE1CCF7DEFC35A5408FA6013E94703E8E04C50BADCBBF2E1FF0FB82DB4AAC595B9EAA9E370C9C6175CEF20B1D0B8A4309AB91918451E6C8A6DF04AE468D446FD9E83F9252F145A2B44A19E7B27DA56044717DB5A6ED5F6E5CDD90208ABC324290292B1F2E84FB69F5989D9921DCB4F058DCAF7B99DF71B26BD1090E457767954B8ACC84FDDFD663D64027528077B3C9E370600942E4C1175B487FBF25E267474B5238576010CCCE3315CEDD5634658B2028F3FB9959D77FA23756DB4878697C9BC491DBD68986B9073D187F2A9E72C943D94C97DA865CFD9C23508105637FED62E56E745555909A49D23B86E620D48FD55A92CC2266C38B857F5DF9BB683D60B084819CF04F5BB8CBED05AC6F48C518EDB5B222F5E6DCBB438182A7BA3B2279E5856828CBE9BDA6009A70D20DA082D2FFBD092EDAD4B272E46D215B8ECC26222499F024327A391CEB007789757FF8FA8267429F0534F305F75709DCC4229803EA8E612F55890C5FDF8252794D5C9C4058C2258A5599BA858A02F89A6FDB35C4F2364A4C6B326A31F7D04F62C2FAFE51D280CD7A4CAB66404FDFD033EADD07974BCAA7F0CB7401B9484DAF9F325B6BA53FBF41219384B264F24AA8D65281693295E6F71FCA885F808026829A3FC32DC9603F0CED36F0B58A296B44ADDA3AAF10638C31F354D1A5AC34E77D4D0154C9546709E920258F73E039FBC223EE74A270840165F64E3051B10B5E63F9ACCF5D1EF40E43F5823B15F8C25CAFCE698A64F9AE316D3905B8E510C56CF7544CA94719735A640F2B8C3A2B828A04E0568863937595E5B9DADA33533D9D676AA657FE69152E93159A00C5962F4DFF9C901A9AB32DB28B93F4BA780E44A2F73878AA76E112E3490205AF83000EFD889FCEEA5E87AE9AE01EE1CCF6BA0461A8D8654B7702C09BB41C4F61A00D05F031B244EDED8D1CAC7916BEB9AA67A3880F4C3516A8D8204932EA00EFB3AA20369FB6BE404843C7411E88428568AB9A39124EAD115298D49C998651E5EF613A6819336683\",\n          \"k\": \"1D2DCACEC14CBB78FE9E418937835EED088CC0683300C965EF3972081F01C4E9\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 88,\n          \"deferred\": false,\n          \"c\": \"A5C81C76C24305E1CE5D8135D41523682E9EE6D7B40AD41DF1F37C9B17DCE78076019A6B0B7C95C9BE7AF29507B2D5A6987C8EE3259190855243E6E56F5620608C52D96FAB103A8700FBA1A87DCA6078118A0871762C9534C0C0C3978C91C3A01F0F608DCF757815438FE8957C8A859183B1B6721A0865BEBC799D4E5C0E7BD3EAE4858E6AB6A2E7658ED80D4ED158B036B93FA03AFA6AE3136CF3D693C911BCC75905E5B0CB2865B9E9884522A77777613E53111D5A1C7D3DAB734CEB03657AE0C89763E99471054776BAE7D51B0E73A5BB35AEC30FF6BC93684916FEF1162586452F426653E2CA844D5744307FF9AEB287A6447783B21A0E939C81421D631F5DCB452E51ED34E3DAD1CF504E0A3B0F4711A8DC6499D1691D109569336CE1558A4C0A464E2087EA8F9E3B18F747EF61F4576AEB42B17CADB7F0FD84DA8E3A6F471D95EDFA65BE9E6C9F6AE756A22A4F1A5C543C26BA7BAD88E16D5F5B7E12E2D4CA34B3A64D17F87CCFC4FF8C5E4F53752A077C68721E8CC817F9FF24876170FF2AF89FA95855A5B1DE347C07FDDBCFE7264AA5ED6401491561D831538F852B0ED7B9E8EBAFFC060284F22D2BAEE56FA9F6D01432A115A2D6A64C38AE0A50BA362FB57B53E3E855B83CE8C42274045599F65FA6A8921D85F94ED230B516712DB6FD2FF28B3A3371D9BE058AE75C2FA591B7EC3C3DAA1F7642BC26C324C08090607E6662154DB37CF747967A1F9FC29089F570EBE60EEEF89FD24481028C85AEF1DC3B09F22CD3691BBBB821C7A8A0F35AD12BE1DD199B977048F3D48C16BB2CA94CECB8928770D5BB329A0327E0B286FAA1C65281031A31C84F2EDC9C04D475ED4E128E51EFA97D0148CBA6C95F674C589F301C265BED708E9AD8DA3C5CECBDEEED35EF1E253132BA89920D786B88230B013BCF2DC92D6B157AFA8DA8592CD0743D4982BE60D7C2D5C472AB9FA7F4CC3D12B0EBAF0ABE555C75805426844DD9428643F84406A1B8D6FAEDFD8AE6E73A72772A2159ACABD972AEB6F7DE091AC5FDD7F49A3DC6641CDF62446B4B04A31F73B80A62F80A404A8CB18CE3E65480EF7B52BF0091117E5D08EAE1B0AABB72E6DFFFF76F6E44BBD7EA570D6604BC2E74318BAFA315A38861AA1B21AFB2A53F2614F1D640075984AE62E2FCA1D1B4DB369F15705CE7D4DF8AE98264501051C0DEF21D645D49625AF02CA428D9F0C2CD9FBAEEAB97E8E9151662B6992B4C99AB1B925D08920363373F76D3FDF0828CAA69C8B1BDC6F521DF641CF1C8A4E7EF0C23289A4E2CF18ACEBBE4C1E68369BD5235120142ECDD1A73811E2E533A647D7AEE16DAA03B683639DCF1E1F1E71CFAED48F69AEC3E831733DA19CEBEC1DDBF71CBAE0800F2F6D64A096EC495D62F4344F7AA5621B322353A795AA099EA3A070272D053D4653A20CF210EAAF12CAE6023D8E5118DF04B384A44D1EDB91C44989EF7EE57F2BF81A24BDC76807DA967EE6525410C5C485067EFC3D39A9AD42CC753BAA59A1FD28AF35C00D18A406A28FC79BA\",\n          \"k\": \"DC5B8888BC1EBA5C1969C21164EA43E22E7AC0CD012A2F26CB8C487E69EF7CE4\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 89,\n          \"deferred\": false,\n          \"c\": \"0BAF0F6E91ECAE3199F4921631891A14C13B418B53384992DA3A8DADA7DEFFB9E1E5F559D27344B60BE81ECD01CAB1E316573D571ED46F59248F4023DB0282207E730549CDB60E793E4CD17AC6F2800E2D1FFB83477A6FE1D73992682123EA730C63269DB13088D6DA46D086CCEA2176398EAC663270B8B2F337A55E19F4C500DE066B5441794C2D0CCADFE5ABDE7D93FD7D6468BC4F925633366D9316788B90B110A4D99485E7E578537A267744FB266A4F243FA02E3A81DA67ED477923B36B37BE21DDA21EB51DCA1F0CE41652145F4C542B2E5C922617033608246BBE2B5250A368804ABDB2EF6C31C491CE3DD852AEABF6EEF1530F4C99286B4B595D57CF3A99580B59AAA2C55E080B5230EA19CF2701D21A37FEFD6F9709657A21ADD063ECBC197B5AD068BE502A2E090D83F4156B671E46617BE6D6A17D0425FAC565C4A0E48966E9D900CB2C2B0D296E0BAA9D6C5E0514CD78834053058A97D3DDF81529079858737440812670E818C9891681D350ECEC93DAE389D534A5C78F01811917061CAC0003D2BEA390EB63FA0FE9BABCD7FF302D4B66567B2BFA67B20F962847D010AA4193CBE9F8CC1B14F8B237C22675B298A8376DFB6037BF7CEA36BDEAD5B505111F67730824B4964815D00F63EE98B9BEA0F2F47CC007D5606ED7F967CB15CCD4AFBC99881CFD297BDC2A509ED3CB320DF58DC4A5BCD1CB100B9D6418CB8E0F40DEF293DA2370CA729B0FAB071FA6AEB0F3F5D1925AB2DF732F98DDBFF23D5411E4921A1C506F2F93251E822C4CF83998B000FE65ED386F5745B1D4D91AD9F98B45E713C8D944409E9D354F42FDB9749A5107C8831562E683498C55E1475E552AC10858AB9867BF8003FB88B3B09F6E8AD8E94CE82E342B1780D68EC8565FC0684AB6C798BF09FA65BE62C37A0862ABFE99D7DBE1431B4CFE007B7EC7930B14F6D161BDCAAE2217D69D9FDBB4F882B9F464F8642ACD9BA018B93A8E3A965194ACCD96E661CF0CF4A2662076E20E8BC319693F1953DAB93FEB9BCAD666832DF42F250FADBCFAF742D68642021BD6FFD97720C3E5AB86D82CE8B14C0289DBF51B50C13CFCEC12A3922DCD2DE8473329AEB23580B22F9C36B4F06D6579751BE0593120F808F0E145D94D1DDBBE1D489B744CF6C35964C3DD96D95FB693543C69766877DA80BDE8ACDF62C366D0A4A553187461F671376F7E70F554965D57760CDF5C6F6366E33B3BFB550CC1F93D98D250F90D7D36BC01581C49417546BF6BBA9D10D41C0A008855F321547BDD5A6CFA2A2516F71415B5BC2D5FA1B9B79FDC7F2B78AA113375EC1717F0F273BD8CBEF59139518A4E8A67DB4D071257000336BB07497F72FAAC2C1FC0F553B2EBA53475F466A2B36AFE0B72B4342E995C544E6E14FF7D327F80E7AC6F65190045F380B5978F50E33272484626266125A39DA08B46256624CE34223BB17299B8B8162753812F2644C9A13C51430B02ABD188DD1A4547C920BA27CDAF145BDEBC6F45EEE3F2F55553010F7B35AC63A3C7C61C\",\n          \"k\": \"DCBEB5E4E8B14BD3031D5916BA03258119A5DACDAC850CB483BD7AA80B7038D8\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 90,\n          \"deferred\": false,\n          \"c\": \"2513DE1E55ED0E862614587FE47F308C90A1F426470CA1293BDDF7B9DDD6C368DC152F45C71354904ED48E15A1CB449B4C45D0F201ED5C7D3A047A72F080265D66C47D39469097EEEABBAA3B07ED1F1AEAB80C7D24552FA8889C674A5D4840289DE6B0FA9A222E693708D1F252DFE8B993956883C07067C1C0844EF0BEB49F63534D21D471D6B727FFC59477F9E89E5BEB2AF0CBEB052F003414DA4070008753CFC0C6D0FA9D1C15388FE5886EADD3474F28E4682C0E01784A037DC3799330EA380767B0D0B6EDFC9730E04D1039548A6F83889098522EBAB684DA6FE26A4A6891D86D40FCD9A24F743D74B23B1596810727C81BB3F9F3BADFAE9997949EE0E24987FA182A00D73DCEADF667E90E5AE76A1F83A91FCEA78C96269F0C9501F1D4CE682506A7EA89302A1480E18CDC1F6D57B5312EAF808895B20897E9A782F916CD75B4981DA1381F14EB1EC248B27F0E6966A0CD75414A735928B2120615D88FA57AF5C40E61750F0A0F8E605747E7C32D5A23F14124969C072E949C8475E3108D689D2D20797FE14618811E9A497FD26B9E71355852D4B36340B61695E3745F8D07644AC6E2C18B3FC276D4D19DB69A7CF26086F172E2BCE1618A740A0C739FD504F72C2A72ADB5564BC85DAB4C9CE790D78D14D3BD242DF04106D96CE7C3B392CCED9B99DF359FD51F306CBCBD5B46B8487CD7B7EDD3C5C02965C84630DA1B6B8B317FE55F7C79E05CDAC9E863023DAF470E9C3FB8C01FDF3AEDF2193BFA69A806E2E70151ABCF96D31CF6A317C059CA8C7D456A8E5EBAA6C1283A319F188AAA80D8301E321754E5FB4E0B25594B01BC5F82FF25B064C766424D658459EFD7A20B65DB181811E6D5A4BD153F7066BD7757D2D417D21F83D7C4CB6A0703A42032F0FD198D9D8B0F91B359FBE908432C3286E1EF9D601702157EFBAB68E0E7136BFC90D26BD8A9A7018DE4C4BF05CE465F917D20A4F221A4EE78813A1E8A117C8470929701CCC201A85E7F18B6BC96FE80B1E074661525D3FD0CE2565AB11155DAFE4D3410328D6DBB4DD99A84FE96283D32322522B88B3AA2A11C0324B1D5556EF408D37B0DF802D163FE38D7C38916A26810BD175D22762353C3175DC6040C899E07A339CD4DDBD4D5549E02C0D691263936A9F63111412B60AA9F57486334E40B2BC1B8EAA487A094E45C3F77F72EA741CE225ECBE2B5E4A1FC080070A658FDF9E2B388722855267B30D94B63C3ED35D475B7EB22E3D2462ABA9CF2A86B738EBB270AB29708A2614A557E33A620B507286E5D4CA57E2CEEDB9965FF1C3E1777F980CDFB1445BBE0B6ACBA0216980F962FBFABE265B3ADFE8641088287468827AE601B6A165DEED39C0E8773BF2046BBF63634BDBCAF98358D25FDE475781733DDE8C6D6383D13B6D48FF1B65E2FF13AAA9CCCFC3C626935C5270F9E23A71A87CF2BD793CB175D23EA5FBD82C18A1822428C32DB9E31B94BE3144ABB00F5ACAAA431C17386719C3FF47C38720B1AB01889DAD877BADC9FC716F648FC8B551F\",\n          \"k\": \"2C37C49E94DF715B3C09E63A39E04DB8D26BD2B9072C9B21076BDFC0B608534C\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 91,\n          \"deferred\": false,\n          \"c\": \"8A4336FDDB3F55D16ADBBE54C6EF0DB27F20679393D86EA4590CB6F5F09BC4EB76181A13C9826FBD2A7174BE8A11F13759EE23DA15337A4C5612480E0A843CC6D04F3A902E144EFDC0AC118BF8553B984E758E6D7ED1373B20A5726271C5F4B542FCCD6379671CE37A5D0128F55539B9A855172CA2DA3BB6823484A87DC2333F56CBADF4A694A5DAE341A0E3FBB3D852929FBAFBF4A5C12CD3494CDF910010A0FAFBC09B375BABFFDEACCD12E6E7BD347CBFBD0C84CDABB5004CA11DDC6D14C1BD700FE3EB2371E3293F7185E2A065532C3B6529E60240E7AB6456139D66745F17B94FDF2C54B13EE4DEBF1B77099718804BAEAAACD2BC60A190487CDC76AF2EEB906E4C9F2664A30FAFB65013B8CA393793B650CAC4A93377A6511D739C2136CEC59E1BD14584989A591E1F3B7F6D7237AEDB556880810FABDB1D7F8250B61A2D16A3337DA65AEA644D7E2226BE5F24CBE01C8A33A4CCA06F6F646A3F5453FE2D9FDEA8D8613F491BCF2AEA950DB1D9B43C7C3F86FA2F4A51CB44EB9761363C38723852925247D92E37FC694D2CB00248023D5448CDE2867125250B17388440C188F7E500CEF7747A101E0BF2521E2C8A2D04F42D834C0274ECBC73E94612CCDB1C4B908BAF63C09C945AD4645912A0666E9844A1614B7F34415C1842F9B1C7DAF7EE4459A8724B7050F6B5833341691019149F351A7F11AE2416DCD5B36F18B1A4B82CC3E924114CFC126CA309E319D497A594B0AB2AFB58C19DEF3BC3AD885B29AEAC81F346A19683B8577F4A1E0F30BDC85A3814CD1196E6B29E55E5C0E4E028872477CB675B2408E136D15E54C85E8A468423CB795D9348BFCC975B4EC20A23991E6E9EF91D676983AC26B66C71548FB46C4BF06E280D7C55E7B8DB90743A8F893F95AEB4DED1DC65C5E0B61FBAD9DA0DDAC274591AA6CF23C79C09414356584F0BE02CE9B500A3EE6BD4FA0119783F50E800ED36D3A4445934DCFD87A31AF3ABC02CAC39C4B28068EECC6D16B6FA187A073BA143209C0F38AFE100BC700D461B1B364ED298AAFDFC716FA6E3870E6258B66645091FCF9413EDF6BC79B75132A46D1DFBBCE3CE9B0558EF003929CC6E3D57BC4FD3092EEAC4ED71B7B7FC70D0E65901DC9196928C5B8CF4A63C62797727C192CF1CE4315120A57D4C8CFD03143AF8754432EEBADCADBCD26C2E3A14BB43A951AFDC19EE67AAEC5DE0722E9D11E3627AD1B624ADF0FB6FD2A6733B2B1B1411DD14EE87AD3BCBBCAD2EB4A38EA00575BFA99332400083FC519C3733F6EDCCAAF71D09A7164E18A9E9587A8D9B9A46563FD3F14BFA2F2B8EBD9FDEAAEF466E591F502151E43A7E1123273E5E0574814B20253A17917D7BDF8370BC50461AC8D86127DC527B8290FE386F1AC1E6E9D7B493BB7FEDEC9E5A82DC1402DEAE71B18AB4B658E43F707259039EB9978D4FB0D62839A0DD8E3A1183CE330D57BC7927F7CCF06BA10A0478B7E2EC818195171AFF75C29B283E759F4D2F5D55F0FFC35E0581D98E582107BF64A6D80603\",\n          \"k\": \"47033B02A6DC056FFEB5FC1E96205C166374AB84A5F3F7B06427BB006E71A5A4\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 92,\n          \"deferred\": false,\n          \"c\": \"6095A951753A644DD898D69138B4E521A704DCFAAD44EB53E284F836A469349C5B9279248AFC57AC93FA34A643DE02B724615CF5865927FED60A6B41E4AB15B4DA3599F13D2C1996C6D6989443BE6FB81F5BA03BDD53462BE5812A3E177876A102B0EBDFCB16DE7B29B5123A79DD82E5CD47ABA02759FAF5401E3BF03144A90AE957EC04DB9864ADE1C5A700CEC7872CCB64FF931984DDC3FB8D4971D761E5544130278C75A1B04E641E070A747789A71E09409C155C7D341D5F828A575EE74439155930DF22FD7716185BDF917472432A30A6762C9FE1A254442F755804D295B1698B47A67BBFDE178200F9CC3D4C705F4AC1B00C372D468E16ED3CBAAA862A2574A9574A7280878BB82DA7BD1B2A58943456838F2E6AA9F6EF1827C5B24FA09DE07E9B3153B0F44A4F2AEA7610F9CCA92565740E7295BA3AC5764A20A44D4E1862E55B1DF7913B279F438B3B34E0C22FD90E06497F7DCF8D62352447C2B8C51C214796194CDF66D5001278D0D55F82FA31DAA72BA6CDA34E60D696ED79C7056BFE97265F3D1BC07719B745ADD4A83404D91A184E629FC24AE236CF6AFAE46295D24B431D819E366F51E1BB2B44B1FB7A3060091DEA1D416268CA550EE4E41FCA1F387E941DBE4EBAE222D3CF625632D1A61414038FD437BFA20005EBC404ADCDE2DC10DB741A3B7534C40822520C4703FDFB6B380F7DB72B725B330D0C20DF256BBDDC31E0EA20E636A9FAE310185A5081923BAFE041AC6FCD4E73F5F7237142B74681F637996D28C3FDE6052243269D19316C56993722EADF19A985E579ED559F971E69EB5125937EBC80ECD15A4F80D7067905A4D39C6220EFE43883CF22E9A366F8911E21D0491B8FF61FD07B733E707A08DB400E438DAA00D481C5AC62064CF47AFE3AB08027B3890E8C8835CEAF8128F9D887A6CB7FDE879D9611C01281A0F02DE0E969C9131F8512138036EC1967DCA45AA30BE8C5B1008113E17A91D9F8E9995C07C0B13A45668C96356F09C3E08FE4C7DF5F7230E0C93EEF08E8958B55E213718C516E624B57765257D21696A3458FFBA11DE708C4EE9AF2EDC5F37458DEC8B985076882D3F4DEB00BFD8E7EA4D57BAEAEC6BABC0E28C15419CCD785CF6ACEC96D1111CDD1DA9A151F59A7366B64A53F0497D3B5A8ECB60D7C220E99126CDE82938C7E131BD841300AE461A1817703ED5B0510B47F2C2980F1E11CFBECB524B295C42187F15B0C9F6B0EB1E70B3EC43ED955528B1E42E2BCB31F3A1CFB5E9C807E8D366E9227A87784748B277D6C885B1385C6C691B3DBD7841DD89721B3A8BF96EBA99C53D4BB3B41DB9409B992BCC2D8FC53E70723CA1FDC1341A3E608D7F62F2322C6A9BA1316639690A22AECEE364B4F13949A0310FBA1A0E35DDA5FF840DABAC55041B0931D9EBEC89B78DD930512340B4B5D0877AF546FF0F342FB76B647D604EE2E20207924F39907D6E72DD4A9A1ED0B6D7364CCE69981F56CBDEDD51CBAF6FDDB36E327AD65D4FE283D253E6BF3C7969FFF1F34DCC742\",\n          \"k\": \"F0CF9CF06A81EE545A33B310616117D6096FB56F0D4F7E49FE0A37550320D3C4\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 93,\n          \"deferred\": false,\n          \"c\": \"2AACD2E6B884BE6A3DDD80155BDCA80EBAF0E2BF714312BBA30D5B367F2D95AC7BEC3965AB05AFA370A42A512B5EFE4B0DEFF3E163AF186B725BCAFD2AFB2BD2A0DBAB74C2BF9362E27D69B6B4B5AA6500EBC9316EA4112745F1C6E98F2DEF9132C7C0BFFEAAFAF994C89B96D3F436B875178963FBC18D2E06ECAF3871787C1AE93B3210896837EC1DA87F0FD8F14AB7C5CB2531E90F415FEBDA378E5492E1DEC8243FE2E8A7BAA6FB6A034D9C524E99D848A804F150915BFD66067C8603B5DB0FE29E27D3F6CA629E96BF3E9C77A5919701EC19646C69A73DFAAB0ABA28FE3E9EAAEB475A441B9B0D62B259DC6B77DEC964AB57D5D776988D54E6246C526F1E8EFDF454E7F0DDAED5363CE02B279CD3B554C251793C3A616C07A7BABA8062919A2B46C64C152BC887A27E382254EA6D50CCC0702B7BC0994BAC09B7891FA64A773AE0B4FBF8204C13A4950FC2C4DF60CEFED7582FD9FBB8C83442517BA0E3B60D9A04FBB24ABCECB303E3FDD37F1037741FD2489F632192A6B9C122A7344CB781A0F61D5011EB0251A842AD4838F9B8D52E21A783F0D839E8BA221CCDD6B968A2B5FD21B8458BF53C9C8076AC0C52C0F53097ED1C25C9F6F12407772D6743EB8E0CE8B1A926F0FDD0DB00482D9590675E56D4509CB5E5F32FC3B4A2DAB2BA080F9A7CDD0B611742A8F83CEE1B091E629D2A0371FDB5A64412B5FA63716961527640D02885C4A09B04A3A6F5EC01A9E0DBB8FC4DDD9E05BDB240AC4878F0D41461C4661777417D6150422FEAB6A39F156CADB5F5D3BEBE417BABCEFF5AAD1B7A624FC23ABE28B2AB2E8273E8F44636A60CDAD9236DCB02FCF87722C899AA321C564B25BC33B4976BC9603BB8B8AB18B5B04625981FB38B2A42722CE2358FC0BA99EF4B122C7B70BB347D0D482DA30638EF8B9C1D9121D83BCDBBEB2A608617054F4B3FDD33E9A08F8DF999A98E715DBF04F8EFACF123BBEB37B9038E9AD906E3C570BB398C10E6D36647A2B0B2731FD39F726171EFC7321BC67D936F7989EA58336E549A34B73F097E3EA2C25887EC6A2E9FED5D2CFF475E99F392162D959DE1C4A4DAD3C96542756AEC3367F7B2515F2225BF7B704B780A6D0B279B8B4EE4879A9BBB2F3303216CBADEA00D229C03E3E2843892FA8E5B0A600D0E3EBDD14FA229819CE9C10B8D5F393DE0119A5B509B80D56B06783447F931177123824910C9BFDE9A29FBA0252E69A90B3E717832866115C06EA73B033EC3B0D45DFDB69A76B484DB0BE7A81215B3817E1C02F9A5DEE8967B147DF9F63C93A6E396DE4251A5A706DFDE9670B8B2F6C4C3E2509142256FDDA905C125FBBB294EB29A3B4D9BE3B67762AFC049B96B3F41B8C31BC5D7B522DCD1AD12B252370A8A57E42F6A9AC26FF784B374DA4B86FFDB65CC753CD049F1A21CF832447E1DF7BA7D0D11E403FC18BC545501E16568595AEB6BD7811C214CF2FB1CDFB07BB32321F536E3896B6EF4D16130ADD71B271CD1027E35538D9E475A3A53DFEA430C151DF7D516CD0D9B\",\n          \"k\": \"0EA983FF9D76F056AA42BB772AA27C8A163172F43E6BC9BC55B83038E095792B\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 94,\n          \"deferred\": false,\n          \"c\": \"8FFBC80E4662864D6F373DC8837AA91B3CC26B68124ABD73DAD025A1D1C18829DCF077D303579E5F39F4BE101BB9E355DFB5323882EACB3D184E6812C03A7BEBE25166D55F821A00F80B8D2BAB1A7EEC83D384AFDF30F6BBC9960C4662067EF7E200E37268B9F5348FF484642799258B45E541101A21FDD6FBFAA2374A28FAA97204953B95BBD1BB519785210DA7C8A09D071D8AFC9B29F2C3C2909A4C53671408B8083BCF5AE03D45C0CFBA399F44D24A06321BB74F6863B7D4BF0BFE73C8AF8EE1DDA45212E3F9C853D4D0E16F8EBDB8581C4ADEEE833D81A9E0A9E8587E9C19E689E6DF715564BCE27CFA73BA16226A77CE44DC496992F41AB918643C6D86A8B26ABA6F94F3502D22DD94FE55483F67C635B307745D33F17133293639118E70CE42C6DB7332D4862C73D5B84415454AD51F89B5559B5C85D6B6ED47B6958F21FBC2ADF8C8A9D43FD2E1B0C02418D227B83F85CBC3A81C719E8602781AE71E15E6D714919E52FCCCFD9A68B4751825BFBB53B7940B15B546158DBBC612E602F660B9E0FF439E0156C4C8792346014BA1B4838C7425AB34744DE51D854CBBA58B7E67E014122518036CE1541A1675AFEAE4F29A5318602ABBD0A1540F33176C984E306098DBD08E822ABB55F9FF38D9E31EA4695150F2CB60BC2EB5F4780CBEBB210CF48662C454C7A42360F306FB03617C998AD8A9297D6B71A71285F7AE8DFB336FA922540C92DC71F777D3B4D11D87B8D082FA8A00DF647CF7FEB27403D3CF50D829EEE3575A01E2CCA57849B11B14F001BE180DD5FA13C03B98EDEA6358C5AB30A526027CB45E33E646B37988CC84B979CC5CFC3BFDA05BD2C7B8CB1B11AFEE007E20FCCF8D0F764F4A6D2F6A8B74281800CBDCBBCF0DF1EC9D27E6A94968604D9EFD37928B6856C48F0108155595D03231DFC22DC0C8EE614090F37E0828B48A4DD371C677B5DBA95E417F12C9A396875FB05623F7A544AEAE41A0AA536FB8D767BA2E14752C84E147149F655AE7B903CAA591AE00267ADD3EA816612AB0B9A5FB263C70C4367062F7794274C75AC66F706AE93699859D55B2E4960E9D538F38A2FAEE366B80DC78BB673A9E1B057D711F9DDB3770947E6DD7BCFB425B96670506758AEA39A5ECB33A1B76B822AF903787DA3B61A7B9263C0FAE1B729B1A2E16FEB50C32A8728181D4E8A9F8376C39F6AABC2C022306B05E494CF9B6ADEEEC95887440508981D6A74707FCEFA24B9F0DC3AABC984E9C44174E6DFB51FCF4588C57F9659A8E7A6FAEAFBAE7ABE4600444936B3763463D4AE411DDC1C98585E0DE58867251079BE72075973275141801B98F7B9397C096A56B8CD83CFBD374E182F7DCC9A7C764DBBF4D7576A1CC9239848E7295D29CF034A1A7AE33A386C3DDC24A535168ED23D7ADE9433B50DC5694C969F4C546EF2293CD842F4B62B6B7435F597CF5C1733884E0A6AA47FA31887DEDC6C402D8ED013E49E5CAD7718CCEFEE0E6A041715CC9ADD79965413049ABCE88636AA7543EE2601F162838EF6B\",\n          \"k\": \"342765B77A09BA6863F2ADA782E3719803F7AB714EE807DE89A1617B5C74F60F\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 95,\n          \"deferred\": false,\n          \"c\": \"17976BAC62F66CEF2B6F947C121079B6F2E9350C137E738BFD884FF2BA6E211640A30FBF2695EDF7046E1F5234AB1C8A9B0E8A3FF88EF18C1E5512D5F69E4A36CC9362F00920481E5460B1FB0C2B9FF0CD0D95718966AF7EC1F76B8DA93F6AB179A5DE70DEE34C579E284AE8504ED96E1A85898076F69AAFEC1357533EBB636FBA2372204DAB87C47AF27D4D9EB1B4FF4286D6A9FA7FD506C9FEBA596D2047DB765C1EBF1F7921867D394487F6BE926E6B0323058CB591195436ECC805C8B88615C7A03833AABF490337063DFEED698F7DA8DD589A794C956C2BF8D8CA4AE18B0A7767693802BD6DD53F543E105EC526C1D1D00AC9C0B606BD9B3A1D52CB8C56F8535ECADD8239308F2FE7E1D7BFAC5848B547B4579AFC13A0B2BEDEFA46322F92E2B73980695369C5F48D37F9345F20C7820DB6DE09D5E8313B73ED705B33646FB14CC4D40D65290A4C27360FBBD080E61A16BB15E9560A097E4AEC16F8B8030FAE1D47E024F10C33E6A1C56AEB8EC2F6AD6EF4B8FF04C67307B23E470FB3E5BCB6F533F955C36FDB46516A07DFF2956130AD0924158CC2A083378FB9AE32DE89CF774D82C2FC70DA48536372299C61927A5AE67E55E792B64FE61F06EFFC1F216CC9D739ADBF3B2190E1D080E00F169F145FE32AF7EC7CBA1D76FA6839D5FD2068E1DFFF557755FF2F4271204A5468C79C7BB8D00FAD63938F12D53B243B3FF866556913EB57AD2AE034F8B62B1A1B9DA2B1D45800B4CEF1E1943A0C92F0EF2EE924F80CF67EBD3D0199D45ED4DCC00140829A0992DB43616CC468508B852EB822066A05CC91D6BC2B47E5622B774F8128ECBBB94CADD15588B36A71E9FD97B05D69E8BAF00D30A3D3C00E663E00AFC9F5E1BAC8534ED5F6E5AB47D7EFDF6537753408299A9E8D5F5AE0FE36A9EC41C6DC9F78A891BFA9C8E90AA1A457A0C01AF70CBC9E55B68A5D8CC5CD3BD6886AE11FF510C6ED0EB2F5C081B25989518BA217BC1C153864E5BB312EF0D43D6DA4A0FDE44F1157CD238E8D70BEB420BD310F8E5DB9D74EF4EC9980CBA74358FC77C5D4FAE3036E176647D78C73900C79BFBF0BC545ABF7CBB4DC7F6041D4FA3B66E4D4655E24B11DC30B0061C452A605CE73362F2A3F052370D873FC68DFFCD3999FDEDB45DD9F2A02B4699BCF1FC5F888B019B5028465F30AEFAD946D481285D1122EA78F3BD8B1982558C38FA3DF0F058B12EEBB11F4C7809F6334EA1D7FE0B529C0BC9C67044648178D2AE9232E4E88DD6D0016D8A590B7703F1A017A4A2671BBB24FA97ADE1B61C489AFE9B3E63CF4CCC42168C98880921C2C0EA7D24DB6DD676B77F7B6C0525C8D0578C7F5A20DBF2F82873904D7CF2522CE6360397B254B18C3059A4BEA169A44D9BA17CFDA1827EABECD269FD391CBC0D49D71FA81AC16F9A0DED9E72A58D1BC2262979D8D7E531D1C46A8F107BDA18A1D2CCD17334183DD3E79D905ACA7DAD348BC6D5CE124A1397EB3B89BE7580720B5DD00BD3A63DAD813E0E967EFEDF17F3D960E70A4F83F\",\n          \"k\": \"F175CA29D36784E3B7A6F6D8682DE3548115C25EC1751DAF6B5FC3318F690802\",\n          \"reason\": \"no modification\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 6,\n      \"testType\": \"VAL\",\n      \"parameterSet\": \"ML-KEM-1024\",\n      \"function\": \"decapsulation\",\n      \"ek\": \"3BC1858826C6B39279C2DA7438A370ED8A0AA5169E3BEC29ED88478732758D454143E227F8595883297842E6AF133B17E4811B0F5713AC73B7E347423EB92822D2306FA14500A7207A0672672046544ACC4EA9C16ED7421A069E0D737A98628519C6A29A424A868B46D9A0CC7C6C9DDD8B8BCBF422C8F48A73143D5ABB66BC55499418430802BAC544463CC7319D17998F29411365766D04C847F3129D9077B7D8339BFB96A6739C3F6B74A8F05F9138AB2FE37ACB57634D1820B50176F5A0B6BC2940F1D5938F1936B5F95828B92EB72973C1590AEB7A552CECA10B00C303B7C75D402071A79E2C810AF7C745E3336712492A42043F2903A37C6434CEE20B1D159B057699FF9C1D3BD68029839A08F43E6C1C819913532F911DD370C7021488E11CB504CB9C70570FFF35B4B4601191DC1AD9E6ADC5FA9618798D7CC860C87A939E4CCF8533632268CF1A51AFF0CB811C5545CB1656E65269477430699CCDEA3800630B78CD5810334CCF02E013F3B80244E70ACDB060BBE7A553B063456B2EA807473413165CE57DD563473CFBC90618ADE1F0B888AA48E722BB2751858FE19687442A48E7CA0D2A29CD51BFD8F78C17B9660BFB54A470B2AE9A955C6AB8D6E5CC92AC8ED3C185DAA8BC29F0578EBB812B97C9E5A848A6384DE4E75A31470B53066A8D027BA44B21749C0492465F9072B28376C4E290B30C1863F9E5B79996083422BD8C272C10ECC6EB9A0A8225B31AA0A66E35B9C0B9A79582BA20A3C04CD29914F083A0158288BA4D6EB62D87264B912BCA39732FBDE536A377AD02B8C835D4A2F4E7B1CE115D0C860BEAA7955A49AD689586A89A2B9F9B10D1595D2FC065AD018A7D56C614471F8E946FE8AB49E8226591119FCADB4F9A861631378736B6688B782D58E97E4572753A9664B6B8536812B25911AA76A242375433192738EEE762F6B84315BB3436231E0A9B277ED28AE0050728346457E13405062DB2804B8DA60BB5C793D4CC0E101CBA2D9182FD7124FF52BF4CA28292AC26D678088953971DBA0B6FEC2C9659353291C70C5B9245A0CA253304AFD3C95102BEA66875C6201680B4BDA38687B648C28EB37478E3BC00CA8A3CC27204642B42B68FCBE7B21A366D0668A5029A7DEEF94CDD6A95D7EA8931673BF7112D4042107B1B8B9700C974F9C4E83A8FACD89BFE0CA3CC4C2FCE80A03D3576C222A792B72B1F070AB7F6B6F2B5CA2AF5054AFA70A896990159B45D1003E2A05648675E596016F1B71DD0F7BDA7E2097FC73B3A143D12C726020AC34958AD7062B92B9ABF3CA6BE5AE29F57135E625A367971837E6363D1532094E022A23467CF932E1F89B5B0803C1EC99B585A78B5865096746F32258214ECB38065C97F455E155ACC2DD005A9C76BED59CDA73837D303504E6C976A606A2BE7BBEC5948B91A349E8936688CC0279754B743ABC58666B19B6C3260051F19206BB962BB6633EB0048E32BAACC5B020D02C86CA9770AD469DB54A106AC73A35B8057422B3DB202C5A5B4E3D535F0FC99326C4B8B7B16F1CB5AF96803FA8C195FC0BCEDDAAF012A51728B76489082373C91E92C87ACCA795160782E3B0DD643544BB96ABC2708D49B759CF057AA223BAFD96A330BAF39810FE8671B4343C297DA1E1969C996216AB5106DA668941B160D4477017136CBCA5B5A8D44C4A8B1CF3EF79785E5AA25C3A1AD6C24FD140F79207DE5A499F8A1534FFA804AA7B3889CBE25C0414704AA57897F17862364ECA56258007248813912B836497F0359C2F7238A05D305A0EA152E72B44417A868134E91B3CA7931232FD4C25F8C2A492A339CDC0A138967211451F2562678FA14080A34436C42B07865AC036A81E97A7787A938025CAF813450368BED0C94B1857604526405D27A1C1ABC81B5B6EC13C71930A97D9232CF7021EF87A4D155328E62B583A83B4AF21F9F5750F8575150424F63B899D71CAD267C09E4467146E16E9B6C653F008C311375E2E006D4076A546B82F5314222F7C654317E79EC6035B73FAF491757E61C828326D53044541C4D4537ABD3EA1E67998C3382974CA78AE1B1960E4A9226B0219AB070F0D7AA66D76F9316ADB80C54D6499771B471E8168D47BCAA08324AB6BA92C3A70275F24FA4DC10E251633FB98D162BB5537202C6A553CE7841C4D40B873B85CA03A0A1E1CFADE6BA5180AB1323CCBA9A3E9C53D37575\",\n      \"dk\": \"8445C336F3518B298163DCBB6357597983CA2E873DCB49610CF52F14DBCB947C1F3EE9266967276B0C576CF7C30EE6B93DEA5118676CBEE1B1D4794206FB369ABA41167B4393855C84EBA8F32373C05BAE7631C802744AADB6C2DE41250C494315230B52826C34587CB21B183B49B2A5AC04921AC6BFAC1B24A4B37A93A4B168CCE7591BE6111F476260F2762959F5C1640118C2423772E2AD03DC7168A38C6DD39F5F7254264280C8BC10B914168070472FA880ACB8601A8A0837F25FE194687CD68B7DE2340F036DAD891D38D1B0CE9C2633355CF57B50B896036FCA260D2669F85BAC79714FDAFB41EF80B8C30264C31386AE60B05FAA542A26B41EB85F67068F088034FF67AA2E815AAB8BCA6BF71F70ECC3CBCBC45EF701FCD542BD21C7B09568F369C669F396473844FBA14957F51974D852B978014603A210C019036287008994F21255B25099AD82AA132438963B2C0A47CDF5F32BA46B76C7A6559F18BFD555B762E487B6AC992FE20E283CA0B3F6164496955995C3B28A57BBC29826F06FB38B253470AF631BC46C3A8F9CE824321985DD01C05F69B824F916633B40654C75AAEB9385576FFDE2990A6B0A3BE829D6D84E34F1780589C79204C63C798F55D23187E461D48C21E5C047E535B19F458BBA1345B9E41E0CB4A9C2D8C40B490A3BABC553B3026B1672D28CBC8B498A3A99579A832FEAE74610F0B6250CC333E9493EB1621ED34AA4AB175F2CA231152509ACB6AC86B20F6B39108439E5EC12D465A0FEF35003E14277A21812146B2544716D6AB82D1B0726C27A98D589EBDACC4C54BA77B2498F217E14E34E66025A2A143A992520A61C0672CC9CCED7C9450C683E90A3E4651DB623A6DB39AC26125B7FC1986D7B0493B8B72DE7707DC20BBDD43713156AF7D9430EF45399663C2202739168692DD657545B056D9C92385A7F414B34B90C7960D57B35BA7DDE7B81FCA0119D741B12780926018FE4C8030BF038E18B4FA33743D0D3C846417E9D5915C246315938B1E233614501D026959551258B233230D428B181B132F1D0B026067BA816999BC0CD6B547E548B63C9EAA091BAC493DC598DBC2B0E146A2591C2A8C009DD5170AAE027C541A1B5E66E45C65612984C46770493EC896EF25AA9305E9F06692CD0B2F06962E205BEBE113A34EBB1A4830A9B3749641BB935007B23B24BFE576956254D7A35AA496AC446C67A7FEC85A60057E8580617BCB3FAD15C76440FED54CC789394FEA24452CC6B0585B7EB0A88BBA9500D9800E6241AFEB523B55A96A535151D1049573206E59C7FEB070966823634F77D5F1291755A243119621AF8084AB7AC1E22A0568C6201417CBE3655D8A08DD5B513884C98D5A493FD49382EA41860F133CCD601E885966426A2B1F23D42D82E24582D99725192C21777467B1457B1DD429A0C41A5C3D704CEA06278C59941B438C62727097809B4530DBE837EA396B6D31077FAD3733053989A8442AAC4255CB163B8CA2F27501EA967305695ABD659AA02C83EE60BB574203E9937AE1C621C8ECB5CC1D21D556960B5B9161EA96FFFEBAC72E1B8A6154FC4D88B56C04741F090CBB156A737C9E6A22BA8AC704BC304F8E17E5EA845FDE59FBF788CCE0B97C8761F89A242F3052583C6844A632031C964A6C4A85A128A28619BA1BB3D1BEA4B49841FC847614A066841F52ED0EB8AE0B8B096E92B8195405815B231266F36B18C1A53333DAB95D2A9A374B5478A4A41FB8759957C9AB22CAE545AB544BA8DD05B83F3A613A2437ADB073A9635CB4BBC965FB454CF27B298A40CD0DA3B8F9CA99D8CB4286C5EB476416796070BA535AAA58CDB451CD6DB5CBB0CA20F0C71DE97C30DA97EC7906D06B4B939396028C46BA0E7A865BC8308A3810F1212006339F7BC169B1666FDF475911BBC8AAAB41755C9A8AABFA23C0E37F84FE46999E030494B9298EF9934E8A649C0A5CCE2B22F31809AFED23955D87881D99FC1D352896CAC9055BEA0D016CCBA7805A3A50E221630379BD01135221CAD5D9517C8CC42637B9FC0718E9A9BB4945C72D8D11D3D659D83A3C419509AF5B470DD89B7F3ACCF5F35CFC322115FD66A5CD2875651326F9B3168913BE5B9C87AE0B025EC7A2F4A072750946AC61170A7826D9704C5A23A1C0A2325146C3BC1858826C6B39279C2DA7438A370ED8A0AA5169E3BEC29ED88478732758D454143E227F8595883297842E6AF133B17E4811B0F5713AC73B7E347423EB92822D2306FA14500A7207A0672672046544ACC4EA9C16ED7421A069E0D737A98628519C6A29A424A868B46D9A0CC7C6C9DDD8B8BCBF422C8F48A73143D5ABB66BC55499418430802BAC544463CC7319D17998F29411365766D04C847F3129D9077B7D8339BFB96A6739C3F6B74A8F05F9138AB2FE37ACB57634D1820B50176F5A0B6BC2940F1D5938F1936B5F95828B92EB72973C1590AEB7A552CECA10B00C303B7C75D402071A79E2C810AF7C745E3336712492A42043F2903A37C6434CEE20B1D159B057699FF9C1D3BD68029839A08F43E6C1C819913532F911DD370C7021488E11CB504CB9C70570FFF35B4B4601191DC1AD9E6ADC5FA9618798D7CC860C87A939E4CCF8533632268CF1A51AFF0CB811C5545CB1656E65269477430699CCDEA3800630B78CD5810334CCF02E013F3B80244E70ACDB060BBE7A553B063456B2EA807473413165CE57DD563473CFBC90618ADE1F0B888AA48E722BB2751858FE19687442A48E7CA0D2A29CD51BFD8F78C17B9660BFB54A470B2AE9A955C6AB8D6E5CC92AC8ED3C185DAA8BC29F0578EBB812B97C9E5A848A6384DE4E75A31470B53066A8D027BA44B21749C0492465F9072B28376C4E290B30C1863F9E5B79996083422BD8C272C10ECC6EB9A0A8225B31AA0A66E35B9C0B9A79582BA20A3C04CD29914F083A0158288BA4D6EB62D87264B912BCA39732FBDE536A377AD02B8C835D4A2F4E7B1CE115D0C860BEAA7955A49AD689586A89A2B9F9B10D1595D2FC065AD018A7D56C614471F8E946FE8AB49E8226591119FCADB4F9A861631378736B6688B782D58E97E4572753A9664B6B8536812B25911AA76A242375433192738EEE762F6B84315BB3436231E0A9B277ED28AE0050728346457E13405062DB2804B8DA60BB5C793D4CC0E101CBA2D9182FD7124FF52BF4CA28292AC26D678088953971DBA0B6FEC2C9659353291C70C5B9245A0CA253304AFD3C95102BEA66875C6201680B4BDA38687B648C28EB37478E3BC00CA8A3CC27204642B42B68FCBE7B21A366D0668A5029A7DEEF94CDD6A95D7EA8931673BF7112D4042107B1B8B9700C974F9C4E83A8FACD89BFE0CA3CC4C2FCE80A03D3576C222A792B72B1F070AB7F6B6F2B5CA2AF5054AFA70A896990159B45D1003E2A05648675E596016F1B71DD0F7BDA7E2097FC73B3A143D12C726020AC34958AD7062B92B9ABF3CA6BE5AE29F57135E625A367971837E6363D1532094E022A23467CF932E1F89B5B0803C1EC99B585A78B5865096746F32258214ECB38065C97F455E155ACC2DD005A9C76BED59CDA73837D303504E6C976A606A2BE7BBEC5948B91A349E8936688CC0279754B743ABC58666B19B6C3260051F19206BB962BB6633EB0048E32BAACC5B020D02C86CA9770AD469DB54A106AC73A35B8057422B3DB202C5A5B4E3D535F0FC99326C4B8B7B16F1CB5AF96803FA8C195FC0BCEDDAAF012A51728B76489082373C91E92C87ACCA795160782E3B0DD643544BB96ABC2708D49B759CF057AA223BAFD96A330BAF39810FE8671B4343C297DA1E1969C996216AB5106DA668941B160D4477017136CBCA5B5A8D44C4A8B1CF3EF79785E5AA25C3A1AD6C24FD140F79207DE5A499F8A1534FFA804AA7B3889CBE25C0414704AA57897F17862364ECA56258007248813912B836497F0359C2F7238A05D305A0EA152E72B44417A868134E91B3CA7931232FD4C25F8C2A492A339CDC0A138967211451F2562678FA14080A34436C42B07865AC036A81E97A7787A938025CAF813450368BED0C94B1857604526405D27A1C1ABC81B5B6EC13C71930A97D9232CF7021EF87A4D155328E62B583A83B4AF21F9F5750F8575150424F63B899D71CAD267C09E4467146E16E9B6C653F008C311375E2E006D4076A546B82F5314222F7C654317E79EC6035B73FAF491757E61C828326D53044541C4D4537ABD3EA1E67998C3382974CA78AE1B1960E4A9226B0219AB070F0D7AA66D76F9316ADB80C54D6499771B471E8168D47BCAA08324AB6BA92C3A70275F24FA4DC10E251633FB98D162BB5537202C6A553CE7841C4D40B873B85CA03A0A1E1CFADE6BA5180AB1323CCBA9A3E9C53D37575AB1FD9E7316C6FEECB0A14DF6F2DA56C2F56F55A89635CFCFDA47927AF1F0A47B2D4E4E61634B1B51D37A3A307A972420DE1B7A481B83E583B6AF16F63CB00C6\",\n      \"tests\": [\n        {\n          \"tcId\": 96,\n          \"deferred\": false,\n          \"c\": \"0C681B4AA81F26ADFB645EC24B3752F6B32C68645AA5E7A999B62036A53DC5CB060A473C08E5DA5C0F5AF0E5170C6597E50EC08060F99B0C00EE9BDDAD7E7D25A22B226F90149B4CE887C72FB60AFF2144EA2A72383B3118F922D032A16F554289902A14CF7755512BB1186BAFAFFE794D2B6CDE90109E6582D39CE0C96197484B3FA07FC91D394FC8D88E7FC4BE002E2DB56F0C4D9D3FBDA274536A0B86ABC6E39BDA52931AEBB8F1084C5C1F7CB3177788B7F331B7074361163491D428E78BCBB57B630841AA987333377CF09569CFD14CC2A11C501BDF82C93DE05BEA20060DE89C686B824571CEF94AB3FDAFA8512619813669D4F53637FEFA4D028CB233E56930E2235F7E6034CA94B143B77AD4A68756E8A9184DBA61A89F91EDFB51A39211402473A5F89145736B2BF8569C705B0CDB8980A447E4E1EAAD3E7E0578F5F86B8D03C9DAFE875E339B4423845616799EDCE05F31B92664C5A59253A60E9D89548A300C1ADB6D190A775C5EE6E8A89B6E779B034C3400A625F4BBEDBF919C45B2BCD14C669248FC43C3EF47E100758942E75E8ED6075A96D70D4EBD2B61358224DDA1EC4C19C2A92898176FEB3C02EDCB9908BAE49BD94AF028EDF8CFC2E5F2E0BD375006986AD49E717548E746FEF49C868BCEA2790AA97E04061B75605CB39EFD463D7B3D68BA574434FF7BE8E2B84BFC47E67E9CD15F3ED450C61AFBA79A20B0B6F287777C72F4AD248174F1959477AA7A7C97F122C50447C7484F382BC47D81FCC9C7E892C8839D37B35394B53E6B2B1895ABB0DE8C98F2633DC4413A8D5735DFC9A64026B6F34779D6AC8AD99CC31AA898C2E7057F3DB8A1A8A98527A79E43552F28D1023E1F6A6B84855CF5E6DF889BA269F048946E84021C65C5A93B007B07741C1EE176C73949110F548EF4332DCDD491D2CEFD0248883F5E9525BC91F30AF17CF5A98DD44EF9A71F99BB732985BA10A723EF476FCF966DA9456B24978E33050D0EC90D3CE46378851C9ECFCFD36C895D44E9E506993082523D26185766B23568CB95E64108F89D1014747C67B6F3C8767BE5FC341227DE9488861C5FE811409F80957D07522A72CF6AB0378D0F2F28AF548185C3936777994466A019D33B18A54F380A33892AB4D4BD507B5A61D0D358341AC92F07B43B8F6AFC6991BB6A1EAC23CA6F73E91F2464BD119098D7E768E77ECE53FB899BEB42265ECF7B271F66546282D472C36239006BB0ABABCCA24550BAA0A601348C810FF5F9EE504BF7155DEE4141A11605A4F3509AC9CAEF6624D21DE332D5D50828B52E92885D3B90553B14463AFB1EDCCD3B569B5A7F00BB66769DADAC23AD8BB5D73A6F390E6FC2F6F8EE3CF4009A5C3E1EF60E8F040672D262E6490379BBC70495DFF237BECD9952CD7EDEB6D1DFC360B3FC8B0AF480FFE024AEEFCD4E9CE95D9B469C9A70E5110DA0BAC124FC3741DCF49116261796504D5F490B433C33C40EDCE2B75151DA256A868A5E35F86226B8151C91934CCC3DACA391DECCA745375660B6EC41AE5D810838CBEEFFA12557884412357B1008363D32B237AA1DD8E2D9C6367ADA09B2C95060206CEC3EED391FDC5DBEF6F08BDF0408E585AE5EBC8E9745D44FECA975ABBC140BB37B8ADD16FCC2956910DC72BB3F02E9A130C9A84F9CCB74D134CDF40AFCBA2009C8F0040239BC99220EF64C4DCCDE2E2E5C9B68602FBE8EF4C98B3468C79DF4E078511BFB8AA3DA09597A02511E7C21A7CF66A93843A94868F19E8552552E3ACDF6CB810634DB97CBC4BB569709DAD4845645446FA8D289FC59307B801E60CE2A91E06E9C22C16E2E59BDE38A416BB1B4AC5457438FDC5D64450A89ECB832C1BB279DBF59334681776AC00409846D09D6F687772E340850AB8673384215E12C8D0F531C451E58493E0EE415AD594DF38C34408C7ED9F0C392F1534604EAC3D9C15465A9A46632214B536990D78078E5BD7EAE2013FFF8FDD8B275C89D97C9353DF3C42A28E814D8468E2B48DB0976D88F5EECEFEAFB8F7F4AF291A728F6249ECF5622339269AA945329E919F8B441C83D5507F30DF0FD2B13FF806F522DAA11AF676A513C149C70F0D6E99A880450A54E0417FE3C1E513E9D920E30A8B42891267A2DC50AD81F98044920C099DF22C73998A25C581A5178C72B17AC875BC68548A0FB0CBEE38F05017B12433343A658F1980C8124EA6DD81F\",\n          \"k\": \"8F336E9C28DF349E03220AF01C42832FEFAB1F2A74C16FAF6F64AD071C1A3394\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 97,\n          \"deferred\": false,\n          \"c\": \"4F90106FF7C3DC4E47417F31AB56B1C5E426C1ECD5878AAD2B705E75062DA5FA6F4D18B704C941C6C6D941FD21191A69210BC39E24950D9F851B6DE8CE30023DC7536439104D42245F3E04E6AA6763F8AC97ADBD04CC69547BCE0BF290FFB5D12946301174AF1B0868C14D4293FA9DCC5B23F809B02CC78DEFE7F27935B9B681E531FC21CCB2AF8EF6144D8498E63E0EE48AF8D4CEF7AC1F669AC740B06F79DDB58E794F2FC2CA832E05A0374C18A4F2CC78343EEA064ABC5F468F4DD11E0B6E8FA1D18A221D8241450C05EB9EDF90D9D7F666AC82E7FD44AF9328E0BC6004D5B114E80E9B980D18E081D771DFCB2ACFD40142A2EB33234F75733EAB7D8EE8A5A6F796681A4A8AF85CCE86971B821D4AD8371049E94E280B77B15D111A42AEADFC08D4F804BD78885443E81A393DF7C8754C460915846E09A0596587460038F55D06EC21434A1C2DF44D0C16706E8D2B83F0E7833976EF05BF1D9F0DDC9A37597E401B817C2BEC8E02EB9DF7591E239F25F8648E7F2F4F673093BD9CB703DA32B353F58514C6AB55748B194E52F153D52F5F33FE95C5F9F65EA97BA721E8DDF333B64D233A867A12701E00C5D8A9B5AE344F3D847C27C079DCC9C3B40EC4604A9F041E7987E8B930C658B9A132DE4E422C0E27553A2A0EAB8C859EB0E5677E83272725C5C1652E61B9BBF5C9C59BC2357A4D1DB9C607F34DC1BA074B84DFC69E4097A7AD2BA9A58000027296AD39FC1CE218A5EEC7ADFA8AA3B9100B0B603CFC83C152589E12E6BD9EE10C49131A701D315DFEC38E018328916F9FFAA7305CFB66781707D2D1020EB782F9F003DB4E46B87D693F62E8BDE170141FF71F26DDF5310C00C9163655F5217DD2C8B0466AC89DB55BD7FB3B0964BC9009E9686185117DCB50D6D0297753CF7F1217E819EE60E3F0FAEC4A5AF0C2EA83CCDE15CF045C6961DE8FF6235C9D93BA4C89B7A82A7471FCFB0B8EAD54D56E8A1DE21B3933AC5B4A0689EEF3598926E17BBB16AEC61EC30A2CCC0E0323EC282887C108C3A4E83E3666493D8653D0E92443808C79D770BFF48A49E65AE089FEC790BBA4C66354EF67A334C1EA5C6C5707B6928EBD1BDB6A940FA242C6EBD7F3E71272421C9082841A6CAD2894BB8AC85F105D8BBC9E6F0A3DF0D7C46F6E2F4CAB904ED157AFA85D4A852220A9636E1E8821643A9E4028D87A430432F09354B3973182385CF5ABFC8F84982BEE0BCBF5D18637399163A09EB45711E07C4458498C76979107CF91B3FC590EA4AD715D656D5E56DC32146580101C952E02ED7017960D54CAACCC70607196980ADBDAEA420A52C0559ED23C9514F8CA7AB7F3BAAFD2FAB58960A64128D5A50E9AD8DB7D23A90CE64C1BC349D118D3603358377F84FF5A64457FA1CF41B27094BCA72360BD429415B9EF9ACCB7A5D7B9E5F5FDCA8FCFA4592E91D7E5120DF7E3C6675AF2211BB94D856A5D2285FBBB36984A1345590930B13232565D54812A9345324C232653190323CC67C840E478D09E6DDBCF999F7AA3B556F80332E67ACA41EC0661088D7696BB64E9A98A0749FAA9854D9B48754023BACAF3C8081A46157C6453BDC89341D3092F3B5337874CE5DE559A56A2FFB7F401F6E28EECAF4FDE5B60DEA73D6B2182EF68E07A8297F3C959E17139B5DEDC72C7A0E103AFF866E89D1F62A1F6B97B61BC059BDE5A2A06087EF783A441F23DD191C692D03C097FF9EE831F7715C6E508BF475E79A8353E84B06A9356045C8FD09FBA35879069B9A3F478FBD051143C13D753BC45F3040E85985EFD6B149EFA9455A18E2894E6EA0BE58F451FF1156F93CC7117B5D091E9DD50D41BFCCD44F2C4EB7812AEFD13C8B68D7F0103BB6CA38D233B6AADD01845B7E44D13C1CB1577D6C4354B063991344787F8C0BE667A7440B98917AD64CC2EF2BC82EFC3398B3B1B238540756CE9FC5EDD26CC20E761D592A1A0530AA8BEFCFE8DADBAC99A417CA0827F4983FF5BE656669F2B5F985FF6B16C44BBEA131D1FCC70FC53BF31EF225D1F5D41863B51B57EA65C6164F7531AE492EFA64161B7DABA3EF4586F3459BE8A962367DC276597B98E91FF594EFE8849BAD4CF91B9E5F244CF03CA9615BE128E96958533544A56E735994B92E4EF0D5FAB54B78EC66641C7463F225D261C144F00A0270741D7A511994833635A8A9B670CBFBEF239BF83327E247943B205DA68DB94E3F3\",\n          \"k\": \"7545CC458E0A274A83B13554224F0BD01D57CC4775AD12468D3FEE5B08C93A6A\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 98,\n          \"deferred\": false,\n          \"c\": \"26CC4F22E035BC00687D557655C46B6E1C447ACB824204FEF7582EB8DBC704D7CE72B0A5FFE54FB89BD7B779B5B1DD1573010B227473FDEFFFB74DF7DCC1E6B48B554563C6C23004AE2CB1996943821F480E91081F1A6765E08A8AAB7F203E95DEEA49A1129A676DCB21540D2AAE1B21223DDDF1453150483176F3EA3580CE631FC85508690D8DDCBC9513A4A5951A440232223FB2ED9E0E5A8ACFEE113D22548B8E98131EE1F45A33656F079870A146F12819BFDDF8792C3C9AC3BBEA3A92B8606FF2B7296DB9D9782C8E788AF4C961840041735DE456A35E5536D861CA118D67408E84D8BB9128B65F2C11C7147EAC928599979EF195A7979CFC48277CF1FDF4B0CAAEB3F8A172A3CA25A3A8C39AAB4495A70E0AFD3861C41A8C01FAD1E9D81281CAE1C33572BA4BCA9A5294000FFD040545B021AF583F56434ACCD4CB7B788517243B09737D355ECE53273FC0C492F251FA02E47EA846121DFF00CBF2767D4DEB25F705591D26FB1B6F839A58EBA4572745A618CB2EBE02CC0CB1C62AA9F0EFB794C385BC47E440BEB38BA742C7357A97CF33098E2EA4D823BD0B9699FB1EBFA806D64FAB18E106D4A97B23A889355C7A2635A9D3BB330A1B8EE5E707DC32C20CACFED68C8DE783562488A64400A4528EF568D833D73E456A9AC22431B2C22441EF5BCE3E77CCEC99D2D1C092ED8A28D686214313F683D4A020FA714459C36A257DDFF7B19B7ED05A16FCACA2570279A11E1439D07F2F23B88411404749C37836585182F31AD65CFEADCFEC3FA905CD4BFE2B6ECAE99D469F3EFC55615D45D19360EBB7C68C73ABD4562EEDA283776C887E70A971176DDC10FC399EAD6B9E247353C25289C0836C626E5376326FE5630C3098436556D61F5C75DA6057008A6E1D50B4F270FCB86F868D5F235428B4D7E13010D20175D4CF0759F56422CF955A721792DEB8EC887E5225F6E52CDFF40B8BD3FEE4DEBC7B363574FD1F3CC113A3B4281F4E8DC3AEBE4B67500ACB50B5DB1BB64F0634B19D4612F597DE2B4CAEEE8A3258DDF8436ACADF3677B46E7E5CF41071DEAD3FBCE2A73388E19AC0C7748E10E3F586E2EB844ADFC079EC0A2CD8C9BAC8E859460DCDAB688AAAA179882B91111A604F75198F55B17C79AD4BE3FDB493B59775ED449BF938B594D87A1C9F721D1C39868591496E62BDBF5CC2947DD81B65ED8CA0BAF0A64E924B5F4FFA88BE86C3594EA7472B822D2D84CDBFC7A2C5039FEC6EBB14FAE2D5D7E9CAF1C2B8788E7354BB6A12C4EA1ABDF0811417586F01553AFD9D8B1EA233066023BC45FA4BC064E7D289AE9DDAF1F985E4BAA86C55BA1F1866E010C55E166C3AA29A682A81195819B7165DF6CC72045D143135EDABA08ACF9DD9FCB8CE732F9CDF1A99C772A2EDAB78647132C33B80E7F03C84A044491B311BC6F3571E7935C6EDFB283BC59F29DD5CCFF9DD6A9640139B173E64F2755F6BBD977F15AF1524827DCE4C2FDF1EBB7C35F0F34800E5A07FC83821FA6CD41695B322F0909D55251372DB8B3CB147FBBF6264BF764B1A20BFA41EFB84D109D4E374564C760AAB66EE823970EE7BFC1D9DB860840BC4767E4A46F1855526A7D902D4FA954C7F337C7C1205FD4AAA70D7F5D904F1D0CF1DBFB63675991B26B590260714920A7249E75D21199D8C002BD702C5398C45A359965D367FA15A73B83197DB3BF3AE9E987479CD81283419E557F993884EA4F17996CCA39FBA8941EDD70FC86E3A46C84C656F77E9DFA5DB31D8761A8FC1D5A2FE9C1CF67DDA1408A212951A5A1D5E9260BF367FD824ECBE8534AA5C63F3E9E2EE4EC53CB42663A79706088A846614B10EDB58B45BF063ACEF64DBB5ED8808588B51A80EC327B95DB34A2107FA96776F1DD0340C7918D0B846883EED35F5730D67165D4A51DC50533458F045E1266CE5C1CA6A30D931DA81732A876987482F2DB58694C574731E92CE6F9083A5EAD8143F244A8DF04C6DE1B2B07ED86D5593CAFC2A7B3E819C03C70B7B32AC0D576AC2E2E5843A39E4D36EFACBCE679307A1998F9C9DED50BF39CD29A529A82F26B5B4538F9CBBD547B9E4D5F7F31B555A8FCA1F9ABDEF3483640DE77D558735C15A588D944F9D76B06E417B1DA873F38A21321CDACE8D4BDDC49EBA4165D40820BA19A437D65B337B8C037041631D09F8ADD1400524F4A3BC33F9213AC7926548B9C43A4BC0148807D9\",\n          \"k\": \"1A9EC19662B68932E5DE4EED9C3F16A4AA8E6E4129F8EFC2E9C7F0B6E82E3327\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 99,\n          \"deferred\": false,\n          \"c\": \"B36564F2BBECFE4DD315E84612BD765E3F2E84F5D8D86FC0708F72FCAF284A0850708CE6E11D0BE154C00F930D18C0A8D8071B612556238A64B679A083B2FC1A204079EE19A4095E71E0EED695B3CA764F4F4E5D7366430A8933F0356DB074C2D68048E046481E5481E4F5A2F365EA9C4C7A6BEA51CDBF1BF31366F863327126DDD101F8220034FB4A3C68232C5CC84229EB1E35F19AC2016A8E4805A87797F940B72A472F129FF5B751964AEEC96847B0BCA5D7F391CA9053380DE83CBC31F341599FEFE36A1CD83B30A1B7CB588874CCC5F443F73ADFA2CE7E7271A5726272A7E5FC721E85D9755D672F5B2A0EAC8065D2C3835B7F0B2F7C77A27AAC438E345BAA378A572AA676632434737FA59A7E197135BD6AF2619A828AAC865D7F34AFB771BB55B5B7E93B9489AE98C694EAA26C6A86F41D0C53522DA4D90F2AB267675BABFBE963C4C68534A24D1EAEA2BE97702E28CABE5FD080DA6B3C432EB0E55F9FE8C1C0422A44F57002A1F96E6D53E8AB9539E909346D150082DF69F54D27017B9A7633B7BD9F7E6274B1F97D7CB4BF5FC2E34E77ECA1317E7854304C75C388CCD1386C694E93CADC856E136C2C0EE7E113A125C79443C5D1A80A9698BF58248B0903A45961603D1EA0E89E3C0650EA3E82368A6C477CCD1B0180542401BB1DE70E25F64A5DE41D62D0467353EE488E1F692EB60778452B53088473B084D0819B725268AAE752FC8CB56384C7AF9D319CAAEC958FC3EAEF57E0F35F1BFE1BABAA2C64A2D9813EE16F22A94C1C00B29EE82F11C47224A9C5424E647B9883918C9CF2CAF51B7FA825121C5D13ECEB5F66E4EA11526E0C37DBCD464C5BA78A36A31A62B2DECC7DF51C24843EC2325C74A771A7D73D35BF2AC4578932A6C2A7323375A2B7679188CFE804E5EFF4A04B7E14F8851770048F076B32BA4F19F4530364C0529EC3FB2D0DDABDC85DE2257F4DF05686AB498FDBEAE3A1439627DD8885E4C8744156C2B155BD2F965AF0F2017F163A6016C274E8532CA43C784B7AD4747A58253EDFB739D68E376D7ED246E5474454F463F4212090DF4F4D7F88C097B18180B05F2E89EEBB834B9BB6DD9E5F6036ECDD5908CA4962609C208A557A36B7FBC72158A6D86322F4303434F6AFFB34527E47E0599DDC88EAD31814646A81188E79E1B6D562E01FE1EF148FE8825758CFA5BD7B738E3BECDDDCA4C59093CA24581E531667DBA2C295B565951445E410FBC99D795887BD48AB87D6D413B64957993CD7525A0A0A5D393CA1EDF7788E4DFACDFA7B394B6163BB948C9C6779BDDCC8F26BC073BEAD0FC87236704A0DC0D89DEB4F8174E91D249C4DCD9260BC7C86CFB35B985813E1689D83083949927303741550CB782E256E79800F41B5C7D981D68E60978E5190A2C51C812DCC3952AA34212625834B2F8CF8CE8019AD6CE8F00FF910CCCF0CAF5A3596AF8DF947EFDE954F361665458F77787E528937BC52C59950746C783D8C5216570E6F0A944E6BD661F23C7A9AF3C602DF851EA2E5627186A6CCBCC470E07B290E4F754D5A8D6BAD8C34F39B4BA838CB467681B0173C33FA51ABE122BAE3DC06660950CFA5C228CDBA2F5EEF2613D2850DF9B5FEBE7333BE93F90E4DEE219AD18425DEE4006FA3009666C83DF7EDFB2EA4F99902C694248F9D51C7B6FBE53780EB218732C11368C33449D051489FDB01B1A1064FB06DED747ADE38F7A12DCDAA92D64DB4C2C43DFE53068A77339E1479C8C93192793B1C752FA7FB23B57DB5B428622D27CBF608CD7406FDB543FF3BD26FD7ED7269427C6B93491BE6724D071F58AF434FDAD2F0FAD5730A60F3EEF94C59CBC5884F36274C4CD984303EEAAD17E1785914DC804BBAF35406995E3D56094F0FDD71C7650A6C37393C0EF4C167CD2FBC28EB4EDD34B5383CA3D1B89D7BADB0270065B5AE2D461E6DEE53291230ED3CC3B616A7E8A86A4265A98C10A44066301470BBCDB257F35489BA5DCA320A390AF23CEF6ABA8B291538D9C4E965969087E394EDA44C060E28220BF72AB98F1C055159892DFF079D283C52997DCFDC2FD8291FFDF322809BE3CDC113DE9D495EA5F9FA5DDE5052192CA6F26BD510433B197131A7E954AEC5E58F0A341D7E4602BAE46BB1987B5C1D845E6AE5569DC2AFE0C7984DDD9B0B184CD6ABC0AADF5E13E0F110E8876D572200DD837FEF193278119B861C196C7522\",\n          \"k\": \"F098B5187D66F9687666207379D9A52532C38C0396F917827BE99222D0BE8762\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 100,\n          \"deferred\": false,\n          \"c\": \"4B30E5256A941008BAD9BD14060445AD208769EDEA1C5B6E4ED506FB334A2378520B5EDC9217D626E1377839A18F2D21C0CC8902622E4AB79E83DEC449FFFD45A4CBF3AC253142D935DD310B5E4C5D591A9BD61795F8ABF00AA04EBAF96195B6CC7D7C3910FD7D75E25A9D0D79FA453178B06FC6B1E99F189CDA90276D6B69FBEA28D68CC82707A46CBEAB819239BE69BA76D749E27CAC9E5FFE88064B9972DB77C49679D6DDC6E6B03DAA0DDF0106B1A61141DF827E96AC542DC90A69CB316EB4F78C611C0155F9138F527006121DA16DB46531ADEC2FF599378A819CFBE3B079C9FE7E368B91A9E40F97A3E79A4F1F05574CE2AC3A525C206D9E55CE16D42D2F0F4863F896E808FE168B34A102BB81BD607BD02CCFFBA5C189497502A55F3E601F8F61B40A5202BAF9AC87D058E67B9E1CDEA0E4B02FF2DEED7477609A9AE2116512C42079D87AD74B05622E02979EF0A0F1D6375D93576EB6553FB1AC70ABDACBFBDB18735E949EC6D1667E978547A5CEAF2F4DCA6FF5D8346A960CE6925BF2B3F316238D6BC8ACBE67BC1AACD5A9A5D130A3D3B39C3BD7C1B06227A59BF4723AE9656D9922D9228A3404D4856E39702DFDC01C6E8CB6000E0779364BAD4F021BCFD7288CE7049D544E8423B2890C3083FDDB9BC720AC4C6A1A4EEA6BA1927B307E6CB72131B6B831AAD036A50A54608D106EDACD83EBDF104AA80C917314D295E903FDF36CD04EB786CF93AFF1279C2172002F7EE92DFAB3A99BF42C2BE7B7D0EDDD38029AB5AE18F5CFF8A2F1D2EA2EC7F34770FBA8A8BEEB0E1FF6F1C1A036F1BD84030004696BF4FB4161F252436C0401AEC911CBF1D7530D9D801B1B9B3A682329AE2F6930191E48189CD40706256B864D6F016597B4AA86FEE4F0E2362D8BCC743E98531EB2B335DE2DD299F231FAA808F6BC7D8F13DE8EAA30C5698D64E508D3534935B9941C2E40A458BEA82DAE4151ECF6DCD40320E1009BD9FBEE248F4EB6DB4437482BDFD83FDAF8367CC1845E64A23A310F904D5FAAD67241AA7748764C26EC881788D1EE0A39944071E5ACB656AB8CEA285C282545030EBBE6FB595E296E1EA37D7AE529B96CAECED11331D80C92D3DACDD7DC93237D815A9C6CEB9209C0BF3548ED1AD691929B2C1035E80A21477747E313049DEAD43A40B0960A96BF3C3E9BADEBC3B4D424FE7DC4DE5CE7788E31AEA3EC8965740D424CEB66D4A5678260051BFEFF09A3CB24C1AB7782AFBFEDE5EE1ED4EB14AD2A13142E8201CD1B52CE064F05ACFB019E21A73D84A80E30FAA48ABEFECA970BBF17FFA6F3A90AEF80EFA31C494E721231289143416AB9621737FC016380E6079EC6CD962BF7CC0750582EB218F869CE117D399DEF9AA66F7D2F07FD22BEB9E50B94CA5FC758C9DD4D2984A156748C52307731FC78F8539F8264BAD6DD56C0C23937A9A850E66BA298C3D39105ECACA9A573D887C9A4FE33D487F2126097B165594E1F8106C937758AB6EE75EDF39D2BCDE78AB611A034A72FDBEE67A80F3315571AB4DB94C56A19EFB63B8E7708566412F73D4974B160183FB5B6C44C8CED990B29C57BBEEAC5EABDCB11CCED9A17322B6EF197121B4094D7EA4A1B4EC44A68B447FE4C8119A6A33BFB66EA6844DB5B6094119AD1DE89449DE922B9A0D1253EA18C62418EB87330C6B33EEE02D4486F62A4D31CA24F098BE2F187CA6019025AD6E1C2FE69800D8BFA2C646F9FC6BCB3D369A78310084FF163D2065631C41748E7E3B25E8F2C9EDA2E107AA2046FE3F5DCC0A9A39FCE41813C8F1946C3AC07A22A6A56C4AFC626E68FF8CBC4982C1E60C3A9F288D1C4F2B8D7187EF2FAE30B77C4DD73499C2B3793B24014CFFEF6D80063DD1C1F3AC7F14FB61E5E81F850AB865BA873404BEB898FF7A2DCFA3B955DDB161B5781AFE8EF127BA2C8BFDBC2FB1C7D80FC650420214314023F6F65C17FC48927BBAE88D48D2E1976119C2F8310232942DD4C3AD4518D1E4DA9DD588691837122F5E5DE0FF1FA685DE134DFD1348CE3B5BE60B18BBF474074829E7D81AE087F149259122D47B728F369D1D8455EE571F715788C254F2EF438034BFF0A11F2F008E19B370BBEEE135A00DBE7F3C2970208F5F5D0E2765C395CA81B2FD80FC384AD046564229C759315B6CFFAD03A56996556E7714DABDE28F7A9BB5DE2C05B1F3596AF66C747D9A9313673F19AD4BAC6EAA7\",\n          \"k\": \"FBC9EB4E8D611C153AA9ADCAEE5781DA5C0112B3AB75956180A5CA40BFA0F53E\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 101,\n          \"deferred\": false,\n          \"c\": \"CA9564B54F15561C8238E6CFD88137EBD4D277FD5D64BAFC33D6E575947F0FF9F93E3B0A4023DDB6FE480D7D6A2B9ABCD6E6E011EF37C0699A6D60D9AB4B05BC685B0A9AF7D3BD999C7AC1CDF017E6AF1DCF0313759CBB21539D7774C31D7ED8C039AC34D0C6A5F7590A3DFE193D73FA96B3458A364DE1555284D85A2BAE7BA9E57ABA00134E6B09C09777F2F1D7125AF858D81D14C71E34E8F668468997334B72E002920FD3FAD8D588355343FA949F1CC0BC263C7F7A7FEA6AD708DB756AF983B16A593EC224F7D69208938A4526400E326CBED532A777301DDEB5E539CFCE60DB8A022AFC52204C71710C204968FD1457919EB71CA15522AD56ED6B60404D62D1DAD0D06E4A2AD6BC746B28859A77226B774BF56BF7F019F2837F51509E9EBE9EB069DA27401CD1D7BF2A74CBE8341A7F213D061619F4E5F52984FE47066D910F1146CCD8DB48210FA2518D6B9FADFF16ED9D389292C07C8A7021F32BCD538AB06A6D5ADB13D7A96F65A4062A17E26B301CC8AD420732126D7CB801DD489AFF2D717D07A2748B4B01D162D228D5F1533CD5FEE8DFF8F032DFB270B61095785E44CEBBD4EA27158362D2A27582CE78594D4D7428B6AD958A9F1604EBA76A8CC0530E1001AC97E5ACC5EE670D5DC6A78AA45300A2BD5F0802CDEE564FA640A19FB554383A4E4CCF2E5BB3A41879C9428CBDB8DE1F4D3FDEFC18C2A8BAE42C096244279E57B307614C843B341BCCF530F6B187121DD83A9A160A3579C3188A98FE2F49A85A2705B9F76DEF04D5D04676D8319F243DFC99A5F90771B34D2A45EFF92C0CA8E4B542B8ED4C2AFBC92C26F8DD20B26B15F9E719AF22F571EE5B9573D5BD1931138D6315C5104BF80AECF830548E98AB23DFA44E5A23C6CF57740926D1E146937AF8D220684919FD89082E260286AB66F66F8A1B81BEE07A85907D07FCCFB9A1002CDD47A33535C9FC0938E3CDDED04D3FABE6326CBF5643373BAE1151704220E49E177C4D0C6168647E5976670DB7F6D0C12F169955E31F553A53A76093DA2A9A0C589F9AFDCCAAD9EC5449ACC01E12A70BCEB389AC104407415782AF2EA3C73D9EB2797CE6D3C005061C5059AB625DD7D273D4D92D1F4EB411A4033492F19921F60D0317AF286866B865E33B6235F0E3528228CF9DB242124F0A6375D50CAB3851DD2A3A022C1E636E332C90D97FBBFC2CF0B971AB1A89014BC2D942FDF015555431ACB3E7A6F258B816BA84892A1DFE3780A0E0C2E6C06149218E70D60D62573BB51856716C0DDA63A983C4008982E842E655E5767DD203DB3490E1E6BDBEC16350296D879F017BA695FC1CB3BBE516B741A67CA6CE09314AE27F718DF68DE698198289B457884FFF1E439F30D9117D19ED7E466084BD5A73E26B5E1567B148D4C7ACD1368B1CE2709B3AF233679E61914202D0DCFC81EF3ACC250DCEA602103C7E529FD6F31A186927E790E3DEE09DCE87DF694ADA7A3B7BB3BEC64456EE983E25DC6CA1CBCD752D72ABE6FDA2FC81A46F81E83AA9738D528C6FA3E69C453346D0C9A0734DF36BB7650D1D2AFA8A5C4C5A936D41258BD4193DA74FFB180CFF582A32D6F6ACB93836E009E8C880592BE61532215F1F6FA50E8FFBB82208A94D8510F70DC6633DC04F9D94C6AC46EDDD4EB36873E064CBBE65D343957CA7B75024EEBB56F589C3DD2253D68D12DC892ABB1FA4BF033B9B732E89A8B89541F04C6462F62F13B09C6705B31036294F1AC38EDC0C2298D7C6F4374C3B5C368D10DA8D371383CBFB4491126A83D1F75DF44F29BCD39349A9BF6526D14B339FCB440647A5FAB63A370089DF162DDBCEAC8966648DDAD6669E1EADC1C8A33E9B7378693E229C6B715F2F0AE54A67455E79FF8970F23E655E7A540D28958E2E102DC99B5CE5772D00831671CC6F7024BBAE8B04173E439054C96AB3BE918C40C5A8D42A9122CA29C56044D340420D2EAAEB738EEF70331D488169FE91B521835297D7326CA272B2614144EAA0B7A75CC7F3849138255B8A1D7DB875BC7C25D28EE5941DE89BF7B063046CA0CFF31A99D7B1846E01B519137B67647F024B3F6DC70045B6950EC6ADBD68F43F67464858D515D6E3EC5F99D9F1C849831BEB4224FEDB01236712E1D715F6A752D0682169A0E83F064FD6F081F338837FE654BCE7C8CAAA8CA90C8505945D9BB3EA58661102FF0ED3F0DE30C4013122D8CF08E0\",\n          \"k\": \"D970209BBE4676405E1CF15D053A04F93D800AF1B32EAEB1E4B644ED09ADE8E8\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 102,\n          \"deferred\": false,\n          \"c\": \"C18D7D95DB69D4FE1E6DB385024D83C01F4790E2BFD25DB3F5DD5A208ABE06551BE0936A84091F081308471E82AC9ED6BAA90824D5525701AE0B638003C21D5EBBCBC17FCA8F522BE4F9FE5ECF38BB66131578163D50994E532D0776B187498C5A85AFF617D4550345F3855F968A8964B4F3CACCFFEC82C92BC8617C78C98E10C91AB505A92CEFE0AD6C8B66406AEA4C3FBC5275047B3E983AD42BCD31838A39B92E1C61E9E62443DF3A45044819D9E289B5514D4F74E08FC3914C0B66D2352CF8B1FABB4AC9B0748A43547BECD29D447D01083803D34E8B7EC89B4F0D78B88AEB33E308989BED2D7A78E1C06A11F3BE808F0B9D9712C80D63DA10475849DDF6CB0DBB1007FCDEDCA3C4220386B78D9EC6E380B4F57731D964470B42CF7D4B4E6D98A12A9021121C8BACC4B132A274941DA57D824FCB60B83565F5CE05D140653DD4C70F385B55D485D24935F3631AC63FA12FF50BCF4E431EE4074B2A05A2A97354C7B4169B13EB225F1727F8424F7CA6317A04C355FE785248E67E053C4D4BDBBC47DC7760AC71DFA2502FBAD1180F2B095425198A26BD5A0F7DFAB70524B8F076E7C7F215B0536B0023F8A9F7784809FF4DA245C2EAC5F9E0AB85D987C5B6FEBDB3DF197347BDFB8D5F1547FD2A59D4B434FA7ECF8D8535903D3892868BA0632F194AD6E4D5A3B30E5A6B92F829642DD4A3031358F9F0D9D46530602A35CE455F0E360C14A754828972D85561AA835D87275AC510856D26192EA319FEB45709346929DA5C5919510CB2A482CBB0F1CB4BF6FCC0343F6DBDFDE734919EF335356ABE82F80786EF0CA22E5B03A05963E7051E1FA7EA4BC3141B5746D264BE1A32CCCD39DFF8F9E5E2AB4C5F51CEFEDE3C1CD118351F9A8ED30649D407FD31F6C4BDA3AF44888ADFC3D118BBD04412FF810A7E106EC32F7524E4750DC5F35A9C55541421B5E412E57BAF24622627F02633F524FF854F71011580598C5CE01190258310BB12D7DB5F95E9EBE5F72C97E89287C2F9007A9332EC51DEF1AA2F2CADA9A8A547C3508B4D294364EAFC858B98C60C5469CC7C3CE3ED659B5A54E889FEFAF825FC777AA74A8896C9447704CA7300FC5DF5810681E3ABE083C1285B3B97EFB0E21F78F45409D00B2E1680DA79439734190AFF0D68E062970F8F6B0F1E84A559B09ACD9938913FC26484DF2125FB6D7FC2E3F0DCBD72D9E5DEDBE7E44CE7D895CC9CF6945BDE0C52F92340F9FAD3009BC90D4C2D3DEF7C1F10A862F9D71681537FE4E2716912DAAB8C9DCBD81A083220F68B05F7502F3911B1B6E3B26DA14EF646DCE67852FAB6145BBE7E21725C21CBB2849C63D01AEAD932F8EE9345D8666786AF06AD0C89B08495A6EA95992301E2D8B6A14426971C7B31626BC93BBE76CF3DB9487B5BFC5BAF298F1A92FC3BE276983E53701F9A550E2961E6E2F07317381364719BF3FC741E2A5A0664D8873120D0C11287E92DB12126332D43F35407C01F7F85DF7916B651EE4A30D602E71227733EC9252EC8346361DEFC23397CEAD0C23AF44C77A4C97242C7FA9065BF0C81983AF3E516C1B8FFF3DD5A6C43B6ED5AD8BB3327A09B6B459168F3E497DCB65FE7593E8AB429B8EB2B31F76DF08A6A8F35EC4CA994037493A8C04A73D8191D682542FCBE16E657D3E477A7D25A1D650450E94FAF485CB76FE7110BAA902D74C335FEE1546D076163B5540D8495E16E909E1D28C15BFB421756B921778A784E16207BAD407B64B9CD83AFB0A602374DE06F5C836F4A1ADFD495012DA8D3FA4B829F735B31BAA6364A2AC11BD18E40628DCF82238D86B0B5EE9DF6D179103E1D12F5191475FE3008A5382CC24648CBB24F2298758823B7F93DF10B380C3179F07DC3277021E9EEA2BE5CED646260165B57A18C26E259F83576938828D4C7617623006682CAA613AAB770791874B55E2D0BB32DDB628919B42C09BB7DAE1FBEE8661CC13F8B6A47CF5D6085A2AED796E305738B508599673DCD03AFC267023814FB1DF7EB928D5762BBAB4515921D81C6CAF551DC6EA16C1D31125B99299ADE63FBFB9BC1CE46331394CE472DE6DCEFB2BF9B3828B0110246419C47D2A1FEF16097B943A310C0664A92A155C8273402E83CB94D7E733E4527E7525E9BAB219B69676804C1F67088184038668D55AC4F6E04CEC0EED4B05DE649A9C2064F241AAF9732B09B0EE4E5BB2C0386E45FBD44\",\n          \"k\": \"B93CAB6CB4C636B56EDF0DDA556D2AF2622AE197B5AB78F95249204A6E2E824A\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 103,\n          \"deferred\": false,\n          \"c\": \"E7B361D043C4A0B3A780121E9648DCF38DFC10ED5E47EE4DB5523C1EE1F53552640C89D9D7EFB9DFAA8EBAC7AF137A850AE41A0FF8F8CE31FDFD3E671555180EE46AB58322FE4E5F525146F9D4CDD1D1CA01413C5AC7259E1A2604A5951755F47D1761ED16B26D3DCFF79A263BC38852007BD3F381FC3B79B46A4B372918AF3117180726117BDB33C063BF5EE5D69CB48D267929CEDC890B743DEA43205EAD46FFA69D9B30C1AA5F146CBA3B0B7C4D4A50FA8D120777F1661430DA1B9D1C1DB4E10C5C3C2436D381EB13DDCC61EEB3F9AB46B60D2FB714929139E9C27F8730684EE17B077BDC003500027FAC94DA80870B382EFE41BAAD902A491C29DC95A8F80BB075A4E61C8F100FA738BED869B48F897ABC9CCE08917B5073229A94F3790947FC1AB6C2DEB5B012D60EC156A8A041DE151EED8884EC616089CB08EAEE37C006F5BBD10ACE9596A34FB09345A14FC4EA674E4A74699BA5240FF1282ED1E64360BDC9F7336051A33DD48809CF0011BEB3CE8C681588AB29F5F26175A8A6FD7884CB96CE3964FF10A67FC4A4E14CA516162C16D8127CBC45BCF7C88C89C8602032298B53C19FD099809814BE0504BCFD2407F48F2F24C5ACB89DB4F54A018DD586D871C58CC0D998FB4B0E5F5DAC631BE8367DDE88ED711F069FC8A80EBB573A7DA12AD8F13A4CA1E8A22D9EB53C55B80F700C58E6EADE6CDEB35C262EB42C903AD854F843547B79464524833A05E3FDC46092E41D8E339E7662D1209D338B8A02994D15A10439C1DC02A5B0EDEA58AF197866F43269A57C747DF389EC597F523211770E9C7E4CEFC5E43EEF791897EE43CA6146F3F757B66B9E7592E728565325D1740A1736CD0E678BBAA043F4C355FDE27094F74FAE27AD8C270930DF637C652BC1957F958DB013C146F2A4C5F451AB58A55C2B638A82755C11991B049E82F8D3CD3E7D3571FD5A83B60280E92031B610FADAC9E5F61DA469DC4C51381C970E03F09CA560E5D69D9B32AD6C1DDB6FBE9F8FC0551A909187AD65AFDC067EC6AA01AD684AE4C4F2E1F64046083D3EB347A6B6B23BBBF14668B9650D9364A6A7666593DB86FEB59628A91169F8AE24F67680789D316338B2F27766A83957831D98C88C837215AE3BD49767ADCADE758320ACE76D7F39E2970EDD19657F0EC12583164C325F0A000D065036BA2522F960C87F9852F30BC6BB5419CD8C0A1F9757BD358E748CB244A5E677AB9F9319A43A9BAC847A566052CA42C1C1DB36A0D97F144BED3BC5F11A5C8BEF7A74A4CC67748F1DF53F8B4714E0A04256B36A814B08B78A9737757E3F1347F9E5535DF1AC98B08ACC1409278B925F3B6C7863BC0969520FC6183E216B4E8E449B0FB999F1C65567AE2064774454EFDA67E1749FB24A91B55DFFE7DB75C4E24E8EF2389214EC3E95972CD53234CDAF8958D651A7A95802E65499A8A7811A65ABBA90129D5EC4247D8183316EF818E79BE839BB3379E4B8F4EE9438BFE310105C91F8703AE94D8F9D53096E2341E74E0237DB2665F16954B9713DD9638B05970A9A96261586B04F9FF369028DCC43D35B51F95E69B0323A1CEACC4A5E2EF640CDAF3407BF5F5E14C9042FF299786BC55965EB7C8BB161487FCD7911BDC2FBB65100F2200E16C690F801EA6615F9130EB99DF816B188B06E9A3105B78212B76609DF190FF102CCC451746CBAD16464E8E2F647B75777F664DE86F5089C37E3A54A6AA8B456CB98B42DEB5529C06DA45C2D2A13060BE56A061CE210ECD307FF5AD5BE39CDD8D27B4A3403323D4F53BE35FB4E31670F73CCE74CF73CFBB29A5FC2EECB5F852CA911942066D826404B77251BE5BC5980D0A6E0DB4D753D86490D4250536DACF05D82064A28324B49AD4AD202CD0FC939BB7A3CD9FB1E3E196348EF336DCDBE4BC831DF5847070D0B2BE1C4910FE1C69F58C6A7A2E7FEBD51BE1D0E050D5D721D7537A0325E7ED30AECA75A2A81BBD86EBF91CAFE4483D2729271ABDBB65C2CD9973627D2820DC7ADE3E26CA2F466EB117B2BA98EC868DC728ABC6907D49E2495504133FAA7F8758FD23076D1A65A91C75512F89EE4E2F3E480D6ECB0EE90793F4F93FFB75DF58A7072C91D5A1D9EF0C3B1DDAE79EF576E6A276F78CDC24664897F07B3A20691601EAD2F499C50589BDBCEC74FCADED1A8AFDFC061C2712ED599D48A3ACB3D86515D664D0CF3FA349A1910CB\",\n          \"k\": \"2E85AE4441DB0930391278E9D6920D9AC77D6C752DB2628CBFE9D76228DDC954\",\n          \"reason\": \"modify ciphertext\"\n        },\n        {\n          \"tcId\": 104,\n          \"deferred\": false,\n          \"c\": \"EDD3FAB8AEB1240FB31B836CD1603E1F904BD1F87318DCB02A7DE18B4044385CDB51E343787E583CB043EE23899658420F9DEDB23CAB2BCD1013F573C0C7978521596631F6590105CB7B281AB1591B7056BE068DF838E0B1679F3B88D95208EF4B3019625EEA7704CE79F33AF339AB883B0C48B3C4413921F43AF2515A85023B5D98D06E619238C8D033FB7DD19611CC60CF395A03B0681913B299531B13728B278D353FA093C633710B65000772DD6D7CA59C85DB62196DCCE1B75559ECD3FB42DDD8E57EB4CF3B4E35B57EA3D6063221B81F1B802DD7D76DE308CB0A738C3B5833E9F4427AF3C3D79B521E7E665B052B9A365DCDFE5A688B06EDCDFA2143C938F852E32D6B49808CFFD01A8655B767034F8C638E8AF94BF3EB9EA39AED1D2D22E181888DD608BF9392FD73822303A41996F41D51A924FA6EF76A9C82709A21BEF1DD004693EC9468B335F9BD1FD94D5E6D89D570FC6D23B7F5CAD2975F418B8C4A0EB82EA2A3C979B1C15B0FB0A23F844764DDD49A8B89C0D4BC8C311BB43725EE9BAACC4796F58C0F1180A4F6AFEF45178659B35A74FF34A8E93A64FA4CD003269EC67C5BD528E015B2311B5E2D33472638CD65FC7D5127335EDA862BDEA05F4290EF9B370BE69DC89E7D71DD2522E669700D5D02D8DCD75FE2AD9EDC307225D61C7805CE1EBBC806A08BD360F86FD27B599582B22C57DDE77B08F7537482FB5D75BDB9F3F4EA07DFB0711C25AD1950058EAAA2E17D8F676BE6B72B1687383D8E0DD60A8277F6FDA202D6F8957EE21308AE81ABF72A89924DB44238B262D2FCE733E12E5413C31FDEF94860D5BB0FB0AEDCB1EFA8F87CCC76189FA5D8157FB4FB14652DC188157B2B746B596FE6F2FEB197DE139B80922C2EC14B58E743E3335893ABF85B99BC4566FA1BEED449658C5993CD08BF78F7DBFF808F611D6EB8F0BD7977E854BC195D711C03EA532403547B6ABEAE827481BB5D53C867710215835260097B6CA730FC722A74E230434B08F38EB1ABAF555CD4CC6CB9310E32F93E0ACAC1E915A4B57E0774B013BE7DD435B5C6AF6018944349841F84E8C28260149F266C99FC05E0DB8D5A63DE362CDE45F5BDFB6F30D55A84ACA22E8640A1287DA51714F2C8D4B184A5F671E0E907134E34D875C9A4709ECE3B7FE15713A0DD972505298C18A9D35956149EC9AF45C475016D7C8B5CBFFE2108882B86FFD380A79892BA1909489E016CF9933705E2FA72ACC8569501553401C397648DC47948935C0197D6F162464DE42BA537611CCB67A988030ABF6081946FDD1ED8C6B23691EA160E8543735894839F13C270B3E1F68607A7EECE09AF06A34F4FC9096D4EFFE4905BE56F3EB397C13472F6621F3EE45A59C8001ABF9302E036BEDEB2E0EC92A03FFE0DF52262621728C6791C4E7D8C226D17A04B6E7EB2FF8586585D639B449C85224EEC67E30537ABE85C8F7E2306DB80E968D8585CED3A9E21622BBC38D43A7D79E2457C67307CC208064D3568C476ED79359CD0A4B0BECA02FA702661056E187A51C2D154638F9000DF856ABB82CFE12C47543E46FFDBADD2DC69EBB3FF444E7E1E95235541015E6CC7A0429C82EBD6942C6420AD598C08080DCEC800509C142ED5A642951F491E748B5436148B90ACCBEC35D0D85FAF4E472ED3F1A089113808D3ECDF77EEF3E089FA5A1635B90EF99034AAA49D4D13058EAA5A8797E37C59CEB86C7CCFE1F574E1086DB9BE744BDE5067AD5D6C450FADFF2338DB110736FBBD86B41B29C29D3899CE60E9BEDF775416541350AAA9BD9B5D57573C542375BB0297912863C86AAE39D153CCB29F1811CDE58978951CE8EEFA6F9D10121AD1FB89F02AF8E96AC08DD10E3274E8CA79D910667166797468D3D3BD6D7EF5F2C6FC4F110268A2716CB273F29BBC347050BD98BDD88F30A96E7A9E840A55087F42A09B03D04E612640BA4D86BE87DA6D20ED0ECCCA2523EE7C4E9D2A96E7378BF71308850832769417EB6250FC768B0EDF92FD45216A235435A3E32AE5B22BF913027D81B0D5508D2AF88120A50206DD7837B79C45B21DB4FE59D23F4AB051BF012B13F6EE5B34C83C8D8CC9BB35266D0EF3834F52CE6CC5BCB7C5989198465A9E9DEE1A1F262FAB26FE0D0964E624869DF2607858815418F85A1F503BE5217794CED29D02E19D40C4BC8E65C46FE3815C1E548976649D4332B1841EA03022\",\n          \"k\": \"5CDD11E1565AF6FBC0DC373651C6F2DC833EBBC54FC0FE2855C0C19EFDD6D877\",\n          \"reason\": \"no modification\"\n        },\n        {\n          \"tcId\": 105,\n          \"deferred\": false,\n          \"c\": \"C72FA15560FEE6B014E73F5F93C307F74EF9C49AA8F7DF578C002AF20419040D6AB6AC46F78FB03F56A9C5C95902D8CBCE34D79853EDF0C319AF5469E32D0B9FC3C41628970E0B3A6C408B509C74DFA218BD23FA7A11DEA2D2277B3522BEF6606E3415D0DD51556440CC1AF59CAE6F23368BCAC3E1509503368354D1E3EC9E91F8B2D377DCC323D578DEB222585E43F97A6D1855B576297F3EC39F5F9EA1B2F72A0E701DB35D633DBCC5FFF76A2D39AE9DF2A3F6326B7671A4C0BB7177897DFF4FAF9FE5CFBCC94966BD298EA2627CF19C1CA866E5927C6E41970F544479D9A6D814AB72E2963F959CBEF37BF905BE98D8C8F3C25FAD3983F71D0C0D27D9FF17E4B34C2F8664406151E92ECA980F6CBE8F8926638398C9BCE9C69A92A30CE82F28CB4FE4110EAC40437BD64D38412030FB8DB3A4242672807737E707E59A0ACFA782127EFCB7BCEC39DFEC55C3109F958E86E0D381C4E9E9FE43110517778C08A140CF440F209011768EE34E5742ECC1E4CED045922D698A29E5557A29C237885D8559F110E4B540FE1298B97920EDF59BC8EBCA11EB91F471B6647864B384AE5A6BB494942BB1F537301B39EDD6F664E4A7877C173614B09D981401D5AA98A8BA4BF1992DD7B7A65BCE7E87FCDFC7B29AB69ADDC9036D71BB9BC08F4E7D9A57B784911CEE7D0EE5A559332981B6475290FB4410D8BA1F00FFC4850031708EB6A83AF524447F491CC25F23FBED71476FBA5C64BCD50D88A3ACD2BE1DF461B11F6D537B2929D073FCFB9E2545E1B097A12F52C411B2AF6C20A27ECD1C084568F4A76A87A4A79F7711012CBEDA777D913CC6B15E6C4E9BCE2C773991946CB9CEFB7F105B15FD2CD3E721E6C1DF69B66BEDF2157ACAD45458FD8C9C1AF910394A13C300696BBBB5B1E1145076BC6B9E3D30A680EA29B6370618B47AF77108EDE6BFCEBFBCFEDDD27FD9F0DA6D289060095C4E309DC3D26DCBFB9E8AF34E12BDD335FAC434663D4D802C8B04AC884352D27739C4DF22F3D7DB38084BAE2C0A15485DF4E356DF2FFBB5BBACA78D0B4886909C4482A6366991776B788C0941437BF858DD83AAA50104D725171C09B7DB521AA65CCCA3CDAFB2E61CDEF66B55D80E201DF44654E7B1FFCA29EFC1E44A8CBA406C8DAC6207C0BD5DA964FBE137ACCD84405A94F5F51D82CE701DD16774BA5F0A7A2BED7F9BB9A4F25C3095D1F8980721A7ECBCE957825A9BE9F4F818E56D35909A3F9DE5487DA0011EBCF9F4D768B72D236042175ED599D731AAFCD45D3D837FB8B64304ED7F22A8C3949BFA25B83A8C05FE9748F63A38201B460E16FFE4329C8464C9BF07D45DF2BA9AE7A84DCFC4CAB7BE42CBD360F61051CD56F68A71FE9E78231986832C9564D02B973EA2D3FCDBAEC374612C1B74DD483F08BAC30F6C9306E7092CC8FE1D20B937AFA4BC605ED4398A8B81A470870E97EA7D51562111D04BF9D09D9BC07533FCDA1E8DA2F2823AD621DB169C99FB112E44FDEFD597B61160815A1776139B685DA9DF6B4C22F6FF6CA3CC46B3264E456E98FF1F301122C88D42928403ED0E0E5F49BB0B450429980ACEFA1A80DA26638B5D2310FCADB0836223CB0894E6FA014D351AE052A70AB5F515641F153509FFB90B8DE495B946AB8C7D7CFEF56D3C66DC871F1D3A38494EF6AB82066E96B9F2782D6B5931B78B7117C389D155759CBC1690897DA66E50D0865209887552C8A6035B8F6911760F8D0A450FB926096721D962877FBFD87D92C37C71836B8BB9FCE92B4637785DC8E8C1D379081C14C73872E676A1C854F1BB68649BD552B48D12F62B17E9A48CCAF63885899C7B781DC3A6D7DE7DA28E286C9FD644D3521F0320B7ECA8FD0AAFFFFF90DAEC85BA80868A2EC69CC73AE00AE29FF5BA37D94510CA19E1EDAA64F30CD79A58B42FC9A6402CE31AF54BAE84DFED8D0C76142A347542265B794A0AEF4A08B4B5DFCADBD56757ECD98F175D80B44121257964293F300FF750107C1B72463D4634EBEDF4705F76C908844763D0D6813FFBE5411FBBFE16C08F32BD1BB3FB8EA5C5339A1B0194DA543E64C1F8065CE526D2754EF95A287DDC97B790FF34EA37863BB166BF0BD99E3A961BC91C1A4F84B63700C9EF5D8D31CEC9E1AE33C554BE638D5C1217CD2DBA13CC143F969DCBF285407A9B608F859812E7F668D4538BE179D11ED767A6971A2AA9CBB545EA01998E\",\n          \"k\": \"C751783FCA654B1FB5F210C6CAAAB9D5E46A969E546A0834D618A952DCCCF3E3\",\n          \"reason\": \"modify ciphertext\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/peergos/server/tests/fips203/harness/TestCase.java",
    "content": "package peergos.server.tests.fips203.harness;\n\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class TestCase {\n\n    private int tcId;\n\n    private Map<String,Object> values = new HashMap<>();\n\n    @JsonAnySetter\n    public void setValues(String name, Object value) {\n        values.put(name, value);\n    }\n\n    public int getTcId() {\n        return tcId;\n    }\n\n    public Map<String, Object> getValues() {\n        return values;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/harness/TestGroup.java",
    "content": "package peergos.server.tests.fips203.harness;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TestGroup {\n\n    @JsonProperty\n    private int tgId;\n    @JsonProperty\n    private String testType;\n    @JsonProperty\n    private String parameterSet;\n    @JsonProperty\n    private String function;\n    @JsonProperty\n    private String ek;\n    @JsonProperty\n    private String dk;\n    private List<TestCase> tests = new ArrayList<>();\n\n    public int getTgId() {\n        return tgId;\n    }\n\n    public String getParameterSet() {\n        return parameterSet;\n    }\n\n    public String getFunction() {\n        return function;\n    }\n\n    public String getEk() {\n        return ek;\n    }\n\n    public String getDk() {\n        return dk;\n    }\n\n    public List<TestCase> getTests() {\n        return tests;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/harness/TestPrompt.java",
    "content": "package peergos.server.tests.fips203.harness;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TestPrompt {\n\n    @JsonProperty\n    private int vsId;\n    @JsonProperty\n    private String algorithm;\n    @JsonProperty\n    private String mode;\n    @JsonProperty\n    private String revision;\n\n    @JsonProperty(\"isSample\")\n    private boolean sample;\n\n    private List<TestGroup> testGroups = new ArrayList<>();\n\n    public List<TestGroup> getTestGroups() {\n        return testGroups;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/key/gen/mlkem/MLKEMKeyGeneratorTests.java",
    "content": "package peergos.server.tests.fips203.key.gen.mlkem;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.Before;\nimport org.junit.Test;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.KeyPair;\nimport peergos.server.tests.fips203.harness.TestCase;\nimport peergos.server.tests.fips203.harness.TestGroup;\nimport peergos.server.tests.fips203.harness.TestPrompt;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.key.gen.mlkem.MLKEMKeyPairGenerator;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.HexFormat;\nimport java.util.Objects;\n\nimport static org.junit.Assert.assertEquals;\n\npublic class MLKEMKeyGeneratorTests {\n\n    private TestPrompt prompt;\n\n    private TestPrompt loadTestPrompt(String fromResource) throws IOException {\n\n        // Create an ObjectMapper instance\n        var objectMapper = new ObjectMapper();\n\n        // Load the JSON test prompts\n        try (InputStream inputStream = MLKEMKeyGeneratorTests.class.getResourceAsStream(fromResource)) {\n            if (inputStream == null) {\n                throw new IOException(\"Could not find \" + fromResource);\n            }\n\n            // Deserialize JSON into POJO\n            return objectMapper.readValue(inputStream, TestPrompt.class);\n        }\n    }\n\n    @Before\n    public void setUpTest() throws IOException {\n\n        prompt = loadTestPrompt(\"internalProjection.json\");\n\n    }\n\n    private void execTestCase(ParameterSet params, TestCase testCase) {\n\n        // Create keygen under test\n        MLKEMKeyPairGenerator mlKemKeyGen = MLKEMKeyPairGenerator.create(params);\n\n        // Print header\n        System.out.printf(\"%n[Test Case %d] using %s Parameter Set:%n\", testCase.getTcId(), params.getName());\n\n        // Inputs\n        byte[] inputD = HexFormat.of().parseHex((String) testCase.getValues().get(\"d\"));\n        byte[] inputZ = HexFormat.of().parseHex((String) testCase.getValues().get(\"z\"));\n\n        // Outputs\n        byte[] expectedEK = HexFormat.of().parseHex((String) testCase.getValues().get(\"ek\"));\n        byte[] expectedDK = HexFormat.of().parseHex((String) testCase.getValues().get(\"dk\"));\n\n        // Generate the internal KeyPair\n        KeyPair keyPair = mlKemKeyGen.generateKeyPair(inputD, inputZ);\n\n        // Extract the encaps key\n        byte[] ek = keyPair.getEncapsulationKey().getBytes();\n\n        // Verify it is the expected length\n        assertEquals(params.getEncapsulationKeyLength(), ek.length);\n\n        // Extract the decaps key\n        byte[] dk = keyPair.getDecapsulationKey().getBytes();\n\n        // Verify it is the expected length\n        assertEquals(params.getDecapsulationKeyLength(), dk.length);\n\n        // Iterate through each byte and validate they are the same\n        System.out.printf(\" -- EK%n\");\n        System.out.printf(\"   --> Expect: %s%n\", HexFormat.of().formatHex(expectedEK));\n        System.out.printf(\"   --> Actual: %s%n\", HexFormat.of().formatHex(ek));\n        for (int i = 0; i < params.getEncapsulationKeyLength(); i++) {\n            assertEquals(expectedEK[i], ek[i]);\n        }\n\n        // Iterate through each byte and validate they are the same\n        System.out.printf(\" -- DK%n\");\n        System.out.printf(\"   --> Expect: %s%n\", HexFormat.of().formatHex(expectedDK));\n        System.out.printf(\"   --> Actual: %s%n\", HexFormat.of().formatHex(dk));\n        for (int i = 0; i < params.getEncapsulationKeyLength(); i++) {\n            assertEquals(expectedDK[i], dk[i]);\n        }\n    }\n\n    @Test\n    public void mlKem512KeyGenTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_512;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase: testGroup.getTests()) {\n                    execTestCase(params, testCase);\n                }\n            }\n        }\n\n    }\n\n    @Test\n    public void mlKem768KeyGenTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_768;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase: testGroup.getTests()) {\n                    execTestCase(params, testCase);\n                }\n            }\n        }\n\n    }\n\n    @Test\n    public void mlKem1024KeyGenTest() {\n\n        ParameterSet params = ParameterSet.ML_KEM_1024;\n\n        for (TestGroup testGroup: prompt.getTestGroups()) {\n            if (Objects.equals(testGroup.getParameterSet(), params.getName())) {\n                System.out.printf(\"Group %d using %s Parameter Set:%n\", testGroup.getTgId(), params.getName());\n                for (TestCase testCase: testGroup.getTests()) {\n                    execTestCase(params, testCase);\n                }\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/key/gen/mlkem/expectedResults.json",
    "content": "{\n  \"vsId\": 42,\n  \"algorithm\": \"ML-KEM\",\n  \"mode\": \"keyGen\",\n  \"revision\": \"FIPS203\",\n  \"isSample\": false,\n  \"testGroups\": [\n    {\n      \"tgId\": 1,\n      \"tests\": [\n        {\n          \"tcId\": 1,\n          \"ek\": \"A32439F85A3C21D21A71B9B92A9B64EA0AB84312C77023694FD64EAAB907A43539DDB27BA0A853CC9069EAC8508C653E600B2AC018381B4BB4A879ACDAD342F91179CA8249525CB1968BBE52F755B7F5B43D6663D7A3BF0F3357D8A21D15B52DB3818ECE5B402A60C993E7CF436487B8D2AE91E6C5B88275E75824B0007EF3123C0AB51B5CC61B9B22380DE66C5B20B060CBB986F8123D94060049CDF8036873A7BE109444A0A1CD87A48CAE54192484AF844429C1C58C29AC624CD504F1C44F1E1347822B6F221323859A7F6F754BFE710BDA60276240A4FF2A5350703786F5671F449F20C2A95AE7C2903A42CB3B303FF4C427C08B11B4CD31C418C6D18D0861873BFA0332F11271552ED7C035F0E4BC428C43720B39A65166BA9C2D3D770E130360CC2384E83095B1A159495533F116C7B558B650DB04D5A26EAAA08C3EE57DE45A7F88C6A3CEB24DC5397B88C3CEF003319BB0233FD692FDA1524475B351F3C782182DECF590B7723BE400BE14809C44329963FC46959211D6A623339537848C251669941D90B130258ADF55A720A724E8B6A6CAE3C2264B1624CCBE7B456B30C8C7393294CA5180BC837DD2E45DBD59B6E17B24FE93052EB7C43B27AC3DC249CA0CBCA4FB5897C0B744088A8A0779D32233826A01DD6489952A4825E5358A700BE0E179AC197710D83ECC853E52695E9BF87BB1F6CBD05B02D4E679E3B88DD483B0749B11BD37B383DCCA71F9091834A1695502C4B95FC9118C1CFC34C84C2265BBBC563C282666B60AE5C7F3851D25ECBB5021CC38CB73EB6A3411B1C29046CA66540667D136954460C6FCBC4BC7C049BB047FA67A63B3CC1111C1D8AC27E8058BCCA4A15455858A58358F7A61020BC9C4C17F8B95C268CCB404B9AAB4A272A21A70DAF6B6F15121EE01C156A354AA17087E07702EAB38B3241FDB553F657339D5E29DC5D91B7A5A828EE959FEBB90B07229F6E49D23C3A190297042FB43986955B69C28E1016F77A58B431514D21B888899C3608276081B75F568097CDC1748F32307885815F3AEC9651819AA6873D1A4EB83B1953843B93422519483FEF0059D36BB2DB1F3D468FB068C86E8973733C398EAF00E1702C6734AD8EB3B\",\n          \"dk\": \"7FE4206F26BEDB64C1ED0009615245DC98483F663ACC617E65898D596A8836C49FBD3B4A849759AA1546BDA835CAF175642C28280892A7878CC318BCC75B834CB29FDF5360D7F982A52C88AE914DBF02B58BEB8BA887AE8FAB5EB78731C6757805471EBCEC2E38DB1F4B8310D288920D8A492795A390A74BCD55CD8557B4DAABA82C28CB3F152C5231196193A66A8CCF34B80E1F6942C32BCFF96A6E3CF3939B7B942498CC5E4CB8E8468E702759852AA229C0257F02982097338607C0F0F45446FAB4267993B8A5908CAB9C46780134804AE18815B1020527A222EC4B39A3194E661737791714122662D8B9769F6C67DE625C0D483C3D420FF1BB889A727E756281513A70047648D29C0C30F9BE52EC0DEB977CF0F34FC2078483456964743410638C57B5539577BF85669078C356B3462E9FA5807D49591AFA41C1969F65E3405CB64DDF163F26734CE348B9CF4567A33A5969EB326CFB5ADC695DCA0C8B2A7B1F4F404CC7A0981E2CC24C1C23D16AA9B4392415E26C22F4A934D794C1FB4E5A67051123CCD153764DEC99D553529053C3DA550BCEA3AC54136A26A676D2BA8421067068C6381C2A62A727C933702EE5804A31CA865A45588FB74DE7E2223D88C0608A16BFEC4FAD6752DB56B48B8872BF26BA2FFA0CEDE5343BE8143689265E065F41A6925B86C892E62EB0772734F5A357C75CA1AC6DF78AB1B8885AD0819615376D33EBB98F8733A6755803D977BF51C12740424B2B49C28382A6917CBFA034C3F126A38C216C03C35770AD481B9084B5588DA65FF118A74F932C7E537ABE5863FB29A10C09701B441F8399C1F8A637825ACEA3E93180574FDEB88076661AB46951716A500184A040557266598CAF76105E1C1870B43969C3BCC1A04927638017498BB62CAFD3A6B082B7BF7A23450E191799619B925112D072025CA888548C791AA42251504D5D1C1CDDB213303B049E7346E8D83AD587836F35284E109727E66BBCC9521FE0B191630047D158F75640FFEB5456072740021AFD15A45469C583829DAAC8A7DEB05B24F0567E4317B3E3B33389B5C5F8B04B099FB4D103A32439F85A3C21D21A71B9B92A9B64EA0AB84312C77023694FD64EAAB907A43539DDB27BA0A853CC9069EAC8508C653E600B2AC018381B4BB4A879ACDAD342F91179CA8249525CB1968BBE52F755B7F5B43D6663D7A3BF0F3357D8A21D15B52DB3818ECE5B402A60C993E7CF436487B8D2AE91E6C5B88275E75824B0007EF3123C0AB51B5CC61B9B22380DE66C5B20B060CBB986F8123D94060049CDF8036873A7BE109444A0A1CD87A48CAE54192484AF844429C1C58C29AC624CD504F1C44F1E1347822B6F221323859A7F6F754BFE710BDA60276240A4FF2A5350703786F5671F449F20C2A95AE7C2903A42CB3B303FF4C427C08B11B4CD31C418C6D18D0861873BFA0332F11271552ED7C035F0E4BC428C43720B39A65166BA9C2D3D770E130360CC2384E83095B1A159495533F116C7B558B650DB04D5A26EAAA08C3EE57DE45A7F88C6A3CEB24DC5397B88C3CEF003319BB0233FD692FDA1524475B351F3C782182DECF590B7723BE400BE14809C44329963FC46959211D6A623339537848C251669941D90B130258ADF55A720A724E8B6A6CAE3C2264B1624CCBE7B456B30C8C7393294CA5180BC837DD2E45DBD59B6E17B24FE93052EB7C43B27AC3DC249CA0CBCA4FB5897C0B744088A8A0779D32233826A01DD6489952A4825E5358A700BE0E179AC197710D83ECC853E52695E9BF87BB1F6CBD05B02D4E679E3B88DD483B0749B11BD37B383DCCA71F9091834A1695502C4B95FC9118C1CFC34C84C2265BBBC563C282666B60AE5C7F3851D25ECBB5021CC38CB73EB6A3411B1C29046CA66540667D136954460C6FCBC4BC7C049BB047FA67A63B3CC1111C1D8AC27E8058BCCA4A15455858A58358F7A61020BC9C4C17F8B95C268CCB404B9AAB4A272A21A70DAF6B6F15121EE01C156A354AA17087E07702EAB38B3241FDB553F657339D5E29DC5D91B7A5A828EE959FEBB90B07229F6E49D23C3A190297042FB43986955B69C28E1016F77A58B431514D21B888899C3608276081B75F568097CDC1748F32307885815F3AEC9651819AA6873D1A4EB83B1953843B93422519483FEF0059D36BB2DB1F3D468FB068C86E8973733C398EAF00E1702C6734AD8EB3B620130D6C2B8C904A3BB9307BE5103F8D814505FB6A60AF7937EA6CAA117315E84CC9121AE56FBF39E67ADBD83AD2D3E3BB80843645206BDD9F2F629E3CC49B7\"\n        },\n        {\n          \"tcId\": 2,\n          \"ek\": \"3A51932399C6144CA7930C3B9C165BED5BA7B93635D2699EC5C85615254B9B8705D5922A0FCB48C9B561DE4114738BBD2F043E1E0B0DD601A095CA540944A20DE89CD4B637B4AABF983C61381A1CC0F03EC8E82F1D4C62269B1114B2673EB5BEC287703C42AA6B574AE2701EB35C5A017228A7E0B91DBB34537B9B19600DCCB26869813E5B1B1A92D22C5D89CF49BCCB75F3BC8DF9648B634B992141F05BCE85F19E64597D688234AFD830B7B5A41E317ACEF6707A0A60213622564440CD89A520287FDF8556453233282435CC813FA4356478448FE5D63AAF539FD526279F02A47CA692F385A7C2D8448EF6B830424FC7A88329C7B02554B72EB2A0994A649F33254C33001FF72C1034A87D79A34F2894AD0296CA529C029A5238154706C43B76B8568775BC1239C08DA177809BBFCF6A833CB62CF4C0A7103C2C7140963861A3B653A82F008D8D78742F1985B340552723AE6EF4095D22383393065F5B8AC63CCB8974614C1C7222458D5D638FEA48C577EB9D64760898B54411A4926CA95E23AA8DDEC12CB33B8F38131C3229A97757BCADA966CF1CCBFF47758DD14875957BE9607C6F66CEAA00A32DC99E575C6F624BC29AF1CE164A1CC2D5909FF93AC25821FBEA8BCFD27FC9211A0AFA2CEDAA0B1AFC89F7F3362ABC67A52AB88763803273C8B6CB2119831DD0D7774839386838946A9067155808F07A6A846B18024506675A618C8093B6D8C8E7AB47CDC847B60C660E7524737649EB11C481EB6273B28E68075E482C4A727C1FC1C0AF904539D335BFC837BFC7F62225FA8F3692566AF3247832CFE9B6C3A92743B2938CC543374A7637ED3C36724C651DD16499A023D40B07494C3018E39BFF4303BA9A3576D8C9BEAC6F45EAAE48A0ABF4770E9C697EC4E834AE04258F7C5912BA8B76FC613914AE31035276A5015CF75A9F3126AA37BED964CE13340942081DFC620ABA65963E33320912614D18CF69656F30BA15E079BE8A53A9E68C7C5D967469294D8833A81605BDD459A65C194B15F241B0D732A0BC87231B682E84B7AEF151C933C0C83410292017B85AA51537C1CAA114690AE2FFA37B4FBBC3591BD28EC78CB5254E52EA0C474EEE871848C83A5C90E6\",\n          \"dk\": \"FAB74ACD14154B721C4F5446B0020EAEFA1B8CE0897607CBF4DB9CF5472751D8AD3418B76614361B30CE0195596E832DDA286D69270EAFFBBC22E11B1EF4912D02B4F02B4C4E5073A7038BBD793ACFA39C3E92615063B460920BD44B815D02ABB872861EE720D73204FBC98978D228362CBA7FB9B999506137502813F92D7958895EB8C1F3E87CCDD25743F2279E8B2C60FA814860BFBEB52F0C3441FF9265882748A8B88E656C0366CB876BA5C8A32385977C1AF0A710A033277D3C53C2942AD7F250E9B3100B675E5539AC11842C09F717B62C60E85195FC0776F80C80612C273F836C5E9300FD0904D097A7609108EFC8CE4DD1077B951CE95257E2921EAB774A97C33724E295B6C9B72C357C7953BF01D6AE4145A708A1351CB800D60A7EE0FA8348F91C6C054F21403A88EBA965196813AA184C187EDF466AA6CC33F7E11E290508DB822019F50A09965A898BA270D6CE8FE6C6A5F059DC311D9BAC0D9EE289EA323838D317A2CBBF5B9C2296EB18708015B05427278B430E7A99BB5793E9E197666A02FC44514AB7041DBB1608F2B2A4A21ED885449D8566D9575977C3A7BCF45757074C3E3631EF1548453B9068685C35B9C4CBAA3A9F9B5978B03F7274076B99A3860172D038CC1CE2345E5A6FE372249DC3289895A9E567602DFC7A0A047A5625A055109747B09B35BB9CD9412A87F92A7F282258F1594B5A9AE9D38C9768A47D4A7ED108A502C222F2B51A30A46AC90A418F6C3CF953BFB50B9229EABE3FDCC51BE021D94A6F5DC4407954C073F76C509725CD0131130037E1F0A7216BC826E5037EC60F2D736DA4B72185895AEBD41FE6E6BFCC154C00F648A4CC62C686AE787445847C5128C924F7E15046124379258B8D651A14573584B50E95AA0674228DE8D6211D5083D849657E53868C08743B6479711A9B7832B99461C629635F91512C71E5662CA37BD31298FD116A9CC87F7A0036A4A55907CBB2AA640161F8A4FC4B7AA1FAC21D67360570B17A212E6E2B672E29A3C50B9063BCBEADACBA95DBADF509672172415F006E82A313916B6E4CA705EE12AB5447214E60C33A51932399C6144CA7930C3B9C165BED5BA7B93635D2699EC5C85615254B9B8705D5922A0FCB48C9B561DE4114738BBD2F043E1E0B0DD601A095CA540944A20DE89CD4B637B4AABF983C61381A1CC0F03EC8E82F1D4C62269B1114B2673EB5BEC287703C42AA6B574AE2701EB35C5A017228A7E0B91DBB34537B9B19600DCCB26869813E5B1B1A92D22C5D89CF49BCCB75F3BC8DF9648B634B992141F05BCE85F19E64597D688234AFD830B7B5A41E317ACEF6707A0A60213622564440CD89A520287FDF8556453233282435CC813FA4356478448FE5D63AAF539FD526279F02A47CA692F385A7C2D8448EF6B830424FC7A88329C7B02554B72EB2A0994A649F33254C33001FF72C1034A87D79A34F2894AD0296CA529C029A5238154706C43B76B8568775BC1239C08DA177809BBFCF6A833CB62CF4C0A7103C2C7140963861A3B653A82F008D8D78742F1985B340552723AE6EF4095D22383393065F5B8AC63CCB8974614C1C7222458D5D638FEA48C577EB9D64760898B54411A4926CA95E23AA8DDEC12CB33B8F38131C3229A97757BCADA966CF1CCBFF47758DD14875957BE9607C6F66CEAA00A32DC99E575C6F624BC29AF1CE164A1CC2D5909FF93AC25821FBEA8BCFD27FC9211A0AFA2CEDAA0B1AFC89F7F3362ABC67A52AB88763803273C8B6CB2119831DD0D7774839386838946A9067155808F07A6A846B18024506675A618C8093B6D8C8E7AB47CDC847B60C660E7524737649EB11C481EB6273B28E68075E482C4A727C1FC1C0AF904539D335BFC837BFC7F62225FA8F3692566AF3247832CFE9B6C3A92743B2938CC543374A7637ED3C36724C651DD16499A023D40B07494C3018E39BFF4303BA9A3576D8C9BEAC6F45EAAE48A0ABF4770E9C697EC4E834AE04258F7C5912BA8B76FC613914AE31035276A5015CF75A9F3126AA37BED964CE13340942081DFC620ABA65963E33320912614D18CF69656F30BA15E079BE8A53A9E68C7C5D967469294D8833A81605BDD459A65C194B15F241B0D732A0BC87231B682E84B7AEF151C933C0C83410292017B85AA51537C1CAA114690AE2FFA37B4FBBC3591BD28EC78CB5254E52EA0C474EEE871848C83A5C90E6A8ADE3E0536F87E2E908AA77EC32AD0A8555B3045331059C5AEBBADA69D0F0735D473027666FECF7024ABAF175B9BC42E84768C00AE2C5CF27A668121B02CD3A\"\n        },\n        {\n          \"tcId\": 3,\n          \"ek\": \"FA66C756BA9DAF40BBBEE9473B35BD71AC1DFA52A33D4A47B9BBC7EA9524DD344E086A6A0CC72941B645B3D49F6D12382CDBB843883F723078D096169F1373C6C8C5132260116963A0316E7D4268D6E36946E1AFC0457CABB3B848190E52A27BBFA42F7EBC0839E52E43611C6E49ABFD97299C4804BD08376020CC21B92BEB1B6D43EA52155B2E8D8B36B584807ACC96F237AD8365C822C66DDFD8A313849E8481A61116939CA8281EFC3D446B6CAA7583EB086FA5214DA67CA46DF75D25537A8C970CA718113DB96392B92435A88053D9141C2BC646D801E9697ED9C35A008AAB59245F56D74F64D1A81DABAB29C4571A6AC000501F1426A7AAAB5F44D4BD4BFB6E8D1CA75AE7949161B10DF906BF72BC460A08C346B5B3A5A6D3325E628546D6F4AF26F92FAEE81C97CBA29E06691AB64F185595B1F35A97A9BCB7E0011AAB485CB4ACA1E04DC534192A0B0E3C248BCF197E7D9B4A317C75C59812ED1694084CBBB8A40EDE334832B714AEA19DC2EA645CD0C3113C31C68379E9ECB5875C58C77B3084709062D77C72416CAC063D04FAA8A7825254CB2B539A78E3197D56153D4E775FF04930DF90546FC145493152A1D37FEA05A86C1A88FD86A8EF32C00B7A5EB04989AA9317C49AC7E9AC82032715F1B4CA448ACDF51198FD1BBA4FC00D1E22693572B0E1F71CF768ACE73C9733F2250B8A7BA3B74D8C12383B465C1D33881411C143FCB621C5CCD7469259A96330343352B699B7C5B850534145E10271897656F4328864A0FAC59C7886B854E8B6D7981307AB4F8B222AAA200D3435562B95554FCA31A0533B5581087AF492E444647F27789A1BC501016079B8C4F5208B0D2574206C7E6A63147950AD0190751E9C3EC0621DF05539A724732069C9D0CB2B7D77940A9A9C0AE40A4410B73B6747A1625D0548BB4AB337CED7442F55063D58CBBC6C6F70B839C8303CFB17ABEE195283A6010DA5BF86A20F7522B9CD0B9F59204C2DF50424AB659280BC27E72916542A0918AA5D618AA739729039169C7838838022E1E61805C83BDBD84BCA9A7C936060D9722DE33B2F3882075028BE255365C5523B6A9BDD23CD16255D8240B07F18D481D988D769FD9588C0BA064E\",\n          \"dk\": \"8D0A672A78C8D3C6A7F4608806D062DE426BF05360F33A000DA72551D56250BB8A8DA19A553A79FCD0ADE3989D1C999C5A57B58761A2D34B7F6B202910384BB38473931356E801B67BF5CDFEA96E7F541982534821D6818FA20829AB9F3F3AA05C4096A1CBB0E907634E7707E2A73DF8C213E118742CC12E0353C9D0B3BAEB296A8EE69FC5D312AA3554C2176411D009D67BAA4F07B45F5B55DFF00713CC3408C46F37389BCF149945B437EB234C81F724FAD57156D370FBF5B726193371A58C48176F8AE9A339DB91A7D7047B55C060DA7EDD910D20ABB6BB9905B41500F3246FABF1AC56BA0D2925600E333E241B72C44ABA71DBBDE6E55197B225D5C42BDD8C62E3313161DBA36094A98D555F99134CFC2382E5877D2530397FC48E7E5AA90676A7B41468C042433C09A09F7C9B5F633E636B316244191F00CF053ABA7365905B7BAA716A9645E54F75706610058761F7113D689320514D38A60A44F8191FAA5C3B13910E4218A703C98E48754B4431F630676928B5641807182C74A3A45B8E8B9568BA210EF8BACA4300E98592533A965FAB9EDD8B3004442E04340C3D3B8528283344E262B9F755E1223EE8108073A36DDF17C9DDF00276B5AC81339E1D8AC541BBB65E5A8D3857B28E60C516A75CA109C7A602B66EA6C9B0B4BBCB27C4F93C8EC44472DA9A7D7AF195DEC6547992AB8ACBBCEDCC87E88140D765518DDB3779F57C8C02C75FAB9515C3807F02740361280B539048DCB6ABB875FA6659E3083834DC450C67BE32D9709F8651840C5A4FB96B048677F9E735FCC1B0BC09CE2F1C39FEA8B0D29193EC886D02F23A413AB7E98001CDE7C8D4F69AB60C6C9270159931702BF5AA226936E7D0B53B29722AE3B29C497B5B74747061838FD8978BC2249FE83A453C3AFF02C67D6B563CE7B3FCDA452C52CA3E1282C4EB90683999FAF29D509BC4E4C41023E332E3C82433AC4371B84824B07C05A1B76D58BCE86AAAEC5AB0DD09A774291A65705BD0764A00C5CFA859938F942EE72C5520E26DD1C27B4C87C77712266DF6819A5C5A39BA8CB9667F862AC66E40069022BACA0B91FA66C756BA9DAF40BBBEE9473B35BD71AC1DFA52A33D4A47B9BBC7EA9524DD344E086A6A0CC72941B645B3D49F6D12382CDBB843883F723078D096169F1373C6C8C5132260116963A0316E7D4268D6E36946E1AFC0457CABB3B848190E52A27BBFA42F7EBC0839E52E43611C6E49ABFD97299C4804BD08376020CC21B92BEB1B6D43EA52155B2E8D8B36B584807ACC96F237AD8365C822C66DDFD8A313849E8481A61116939CA8281EFC3D446B6CAA7583EB086FA5214DA67CA46DF75D25537A8C970CA718113DB96392B92435A88053D9141C2BC646D801E9697ED9C35A008AAB59245F56D74F64D1A81DABAB29C4571A6AC000501F1426A7AAAB5F44D4BD4BFB6E8D1CA75AE7949161B10DF906BF72BC460A08C346B5B3A5A6D3325E628546D6F4AF26F92FAEE81C97CBA29E06691AB64F185595B1F35A97A9BCB7E0011AAB485CB4ACA1E04DC534192A0B0E3C248BCF197E7D9B4A317C75C59812ED1694084CBBB8A40EDE334832B714AEA19DC2EA645CD0C3113C31C68379E9ECB5875C58C77B3084709062D77C72416CAC063D04FAA8A7825254CB2B539A78E3197D56153D4E775FF04930DF90546FC145493152A1D37FEA05A86C1A88FD86A8EF32C00B7A5EB04989AA9317C49AC7E9AC82032715F1B4CA448ACDF51198FD1BBA4FC00D1E22693572B0E1F71CF768ACE73C9733F2250B8A7BA3B74D8C12383B465C1D33881411C143FCB621C5CCD7469259A96330343352B699B7C5B850534145E10271897656F4328864A0FAC59C7886B854E8B6D7981307AB4F8B222AAA200D3435562B95554FCA31A0533B5581087AF492E444647F27789A1BC501016079B8C4F5208B0D2574206C7E6A63147950AD0190751E9C3EC0621DF05539A724732069C9D0CB2B7D77940A9A9C0AE40A4410B73B6747A1625D0548BB4AB337CED7442F55063D58CBBC6C6F70B839C8303CFB17ABEE195283A6010DA5BF86A20F7522B9CD0B9F59204C2DF50424AB659280BC27E72916542A0918AA5D618AA739729039169C7838838022E1E61805C83BDBD84BCA9A7C936060D9722DE33B2F3882075028BE255365C5523B6A9BDD23CD16255D8240B07F18D481D988D769FD9588C0BA064E851F4FBEBBC2AB265691CDBF130A1A566398C6316707F7A9AE78ECC419698DD97A7FC526215D5AE3262985D17B00726462D1479CB038DE8C8A8FEA896A037B2C\"\n        },\n        {\n          \"tcId\": 4,\n          \"ek\": \"72828AB4196357F09AA50129BCA411CE9255422C4AC1C492A2A60544C26B8F37A7EC9B053F27656AB1AE2BE0B1C95346083C15C8F89AD92C477D5C03BB95AC4F8B6A1C9BAA8D199F1D83823072082737BEBBCC1BC1E7BFF9742C4AE86313001F2519C8E93080C2AA1D63376BD61C31BC189BCA8C8385E5A7B3A69380EB72F14B164166227B453C6F43C59A34A8B6724193D77858113AB5C90B2A605F5C3C8E3FF8BDDF82C82F0C3045A7CEBA99891A60016C660BE767809977CAE5B8ADA668BEAC5374CE54692DD02F51E9A90BC68C51955797975F63707E1DF3AC14BC877529A02EF7A317976EA722CA3B80A316B2688B074108F05C3CAAAEB0241D1C5B05D458A150B692E288CE47CCABAE255B9EA13F80D16066155B2E46524BA5A3BDF74C9F77A25402CFB4439C497C03314465C4A91152671E4F79239937BEC7A72DAB0B97B30C1A00F100216B1E2AB67C6520608F54BC92985504033CE63363E015B9E0C9BF75CC2F90AA9E09DBC221258B389585927905DE077927D3AF6C59309B12B3DDD0029BE56EC0CC1CF8AA3A5041BD954B0B7DB68AF2BC02284C9FCBF954875816DAA11E6668455C8978A247B4011B1010289EC564999D069AA2392E9CD9A0E45636C014315C15AFB5148F513CC87EEB8B56334ECA1C061058C06B3C285DA8203DCCBEA8B750E9D43E8CB837F1AA0635954A924C866C33441E7C0C23C97CF62CAE10D32C2A641C1538C2A7B1C4DBB7BF069B43D4210941A4AA0E41748D788AE831018BE1916D056AE4598834B6BD56AB86C3B7528456751B33A0406A2FCF8C0FE18C25A1C208A5A4CDBE593DA6581E27C6A5B4DB793154233CA5B3C2469A80F8C8D3C22C25A8537270CAD4480B20D47C6AC56EDAD91BFFEACBF605AD61C44BF86B7624B95CB1A5248E9210D9F18837FCB01EC68BF434725FB75FF67C016DD78E303C2C6E775D17E61642776368F17F467BC9E0E153659C5219678490BC110B8184B556A2AD275FE6913B6BC744DF313EECDB273CD637A84234601599F05C6B358312FDD959E20B81A434A6FE200AF26A50C670837E8266303A7C23219F18A670CE910DF8756E403C31C561245110CD2DE051B61979D5BCFCC3E03F68C1D3412C\",\n          \"dk\": \"871B000F61C393DA0F0C56B0CA4135223162F177373BB58BA97856F3A0AAAF9540AE6BB239C5221D6B3CC3BB623B5ABBA1D7CE45A75571E01B6AD429B4A1A73E0130DF3947BCE4123AE4A9AEB52987D4BEC94446CD70AECEC5CC98EA13DF3A323D8BB48B08B07161499CA76962737F36A17A4F83045B78AA33879BEED71287B70C35B92BB01AB9E7B0813DCA539F63CAC3B87B419A852104B73F05979BD8296D0967E58A6333F88E8CCC9A8A4A4E0CE25A57851FB0F568665163553BC200C190C9178526FB53328119E99C42EFC0C77729BD6873900394CAD9197E37C00FE9C703CB25AA54E73DA5A41132C1B9ACA95ABD4508872B1F6B8288477998A0842E58B1BC9CE061CEC2703AEBBD3C230AFCD2AE7E576126BC72A7925BFED55E62C06E16187E24086452344B8A5A5A462522AB693A88F57C69835DF0A603145C6DF2244A1F221977AB54C3E2023363A72F277217E15E616B54D6BB3959E3322EC9A276B7434A796BB5296E83407999F16408B11DDC79CBCEB46D01033C5FD091300855D7E64536480DD945CD33CC9D4221ABE5014D068BAE1851717633AE03E6627BB17976ECCE84807D5C2602030844AFB30994D26CDAA0C71AB591AE6069403C13E4B6BA89439F2C5393955AA1C4A6270339505B59009DA51527C33334D6903DF3A37613546F076AEE723046C13FFC729D64583A4A01CAB68C5C95C25706F0A918635E61A390FDA5745A9756EE32975CA4483982505981A3589232EAC3C0C31748A49429A711CBC16355DDD10660557C9270438D9635850918AA366A34B251F311C65C579FBBD2341A53CB79BC9A993475CB6AB9455B82A9DB5EBA53592EEC9A2C481792798A1483037F115336FAA8A4B07AC253B0A347227A7B12E3197531F875C91B0A636107CB38BB0BD613B057869403A77DDA0A80A26266B61D14A75433B99BA055A7FB625C4A17BE9A92B2E168899B6539972B4BC674040645496972617FF70DF5EB63B17B03F54BCAEB3118C4F31EAE075D2C1C9F1D5036FEA818A539ADEA50795AC933EC886FCC248E49F12DBBD23D8F812BE0678B19888116254A27B5A372828AB4196357F09AA50129BCA411CE9255422C4AC1C492A2A60544C26B8F37A7EC9B053F27656AB1AE2BE0B1C95346083C15C8F89AD92C477D5C03BB95AC4F8B6A1C9BAA8D199F1D83823072082737BEBBCC1BC1E7BFF9742C4AE86313001F2519C8E93080C2AA1D63376BD61C31BC189BCA8C8385E5A7B3A69380EB72F14B164166227B453C6F43C59A34A8B6724193D77858113AB5C90B2A605F5C3C8E3FF8BDDF82C82F0C3045A7CEBA99891A60016C660BE767809977CAE5B8ADA668BEAC5374CE54692DD02F51E9A90BC68C51955797975F63707E1DF3AC14BC877529A02EF7A317976EA722CA3B80A316B2688B074108F05C3CAAAEB0241D1C5B05D458A150B692E288CE47CCABAE255B9EA13F80D16066155B2E46524BA5A3BDF74C9F77A25402CFB4439C497C03314465C4A91152671E4F79239937BEC7A72DAB0B97B30C1A00F100216B1E2AB67C6520608F54BC92985504033CE63363E015B9E0C9BF75CC2F90AA9E09DBC221258B389585927905DE077927D3AF6C59309B12B3DDD0029BE56EC0CC1CF8AA3A5041BD954B0B7DB68AF2BC02284C9FCBF954875816DAA11E6668455C8978A247B4011B1010289EC564999D069AA2392E9CD9A0E45636C014315C15AFB5148F513CC87EEB8B56334ECA1C061058C06B3C285DA8203DCCBEA8B750E9D43E8CB837F1AA0635954A924C866C33441E7C0C23C97CF62CAE10D32C2A641C1538C2A7B1C4DBB7BF069B43D4210941A4AA0E41748D788AE831018BE1916D056AE4598834B6BD56AB86C3B7528456751B33A0406A2FCF8C0FE18C25A1C208A5A4CDBE593DA6581E27C6A5B4DB793154233CA5B3C2469A80F8C8D3C22C25A8537270CAD4480B20D47C6AC56EDAD91BFFEACBF605AD61C44BF86B7624B95CB1A5248E9210D9F18837FCB01EC68BF434725FB75FF67C016DD78E303C2C6E775D17E61642776368F17F467BC9E0E153659C5219678490BC110B8184B556A2AD275FE6913B6BC744DF313EECDB273CD637A84234601599F05C6B358312FDD959E20B81A434A6FE200AF26A50C670837E8266303A7C23219F18A670CE910DF8756E403C31C561245110CD2DE051B61979D5BCFCC3E03F68C1D3412CE81BFE29AC4AA0EDF35537F3ADEEF43D0411B85C7E1D1C54612167DCE488EBE36E584B168BB5399D52B458A8BD122DE14EEF214515B70F38F972F41783005755\"\n        },\n        {\n          \"tcId\": 5,\n          \"ek\": \"F2137B2BD0A33F81C4DF584BB46C60FED985D09589C125A6F7A9C4B3132F7BF4A4B4A268BB52702C3B5DED770B3AA30EC2708B93500C5E3C6998FB6EA1586EAE409B6D617C330827F2A417DEB0007C78C8F8C025B8C3415A31313378536EA672FF92625B3443EBE61836421841750E1372A81BA99825B67898F2009E4AC8D5F89396724B7BF29E16C72C1DF192AA5277F01A5428B4AF8B284D85D987E015AE1CB89AD9C9230E2313601B5B49416D0B18B9D75326123A02E363C444993213D387A1C7A021449FE4E35C4FC30DF0066CEA593BF7DA67EF6322537802CF068E2D56B178F929A630C3A5043528A489928CBE1A1468F2802F8F890C042114426BCE7FA6A335441BF1B159410C62FED916CEF3B0D6F0B54FDC3ABF9778743A87D3F417EE573897B45CD5BA4F893CB13AA077A696BB37DBA964D0189FD83DC725129D9425D9291652CC84EB3145FDC88482407748985D72C124EC835CD800BB11F81415A44E474BC553998C1CA945FBB0035EDA52B93B3D341A29E7F808418AAD2FE388D0483E15F68E3B903E7D9C6E051347B1988B6A0A441C8B1FA05C743CBA003BA946BF07996EE14F9E93AC88382CD7F2CE8028352CA7C36A73C9303337CF5377AEA685718C8802B7683019892A904C5ADA3661C1CCA5B4CCB81350D8484BA34CA49AC15B9DA09E1E6887A10802060AB20E04B0142367BEE71CFE023A15602BA83B7761FB08CF79424715CB6CF563E2ACB3E11CA3D6C4095084779213275E13A1E7F14389C301AD53CDE6369E6A9058CE282530593A9E8B1BF38BA08F40B85A94678B619550C437411861B957528CD0175170A1F2308B766209EBF454B4AC4E24C1B230CC793C935770543831131CE38A77A7248C43C79814A03E35D8CBFE02A4F5482E73B1B7F41569E42443A733CFA185B63A120809007A46D5C718C003DE2945B295C0EC42C30EF5BDA5107ADB2125A5E61575382F961C43A585C7400C5AAFBA2BCF595C71C94F0F59CE48904531E0C2EB862C4E66CD6CF280A84B926C42489CCA778A587F0935B7CA369B1140594A1114DDCA6A6F104671CA26E8804EE0D0513F39519217F5A027363BAA21AA561E954B5494D2482873722C7BF20ACCA9B880\",\n          \"dk\": \"DAF41A7FBA550B084674F174453A18D33994460409F80232044AB7411865F90281EA191165940794957CB0C26E1609B941117C38350F653A04E28438D54C3452379687EBBE463A4A7447CEE3836E7EACBFEE6AAFD5114DF0A595C9F98FD85C2DE879857236CDD0287B2F0041E58390C5510E1D288B0F690A49451B1F7002E13594CE71045AB1B455A280377393F54A0BB0C44EE06C024733675A1821E619434401608A824F86ABBB55B07B1DBB7A96B0B88C6885735966DCEC666CA3281352B8651A8C13E48C52C61220572676E500A6E0A5FFD238A76CA6FF29915CC975B3E5B3B6819C61495410F095023077EC3CB282675578C948B01C63146B8904C56A1D419B1FC0AC8E5B5CFEDA4D8A8A562206B4A8E93EDD180936960CCD206964EA5CD9D4BFD3540A4D5BB2AF302A7594820EF8115F2B0E38324EB024AD5E6AC92584C681D5A42079450EC464B900BEC5D9B8268A843A5095D2F41DCD648CF30B746F6C4EFDD77A5C432100073CA36B61C0EC4C4D64341A675E9E06369EC30D0E63CB702B6D32CC1DA0253D4CFB08E2255FA5A3B29305520AB737B5D820A0817EC5BB4A99C8936A5B396D927FA97177573CA01E568A67EC059E897A75E058429BA03321B2D190BFF5F8A4C1AA3CFF7A54EF3BBECFE4985BF253B2F126B092A8808175C0AC253C6C9BCDF3710C7B90FDC77B4F95BFEDF78347E72F806191FDB4481CD7794A993EABA23AF5E76A081243D54C118868BC1C51A2AC00808196573326AF95A1B4A4DA8AE0173588A978130772792284AF456E8B651A974A09D5795F0BE0B08F1570A6C21B33440FAE93966071ACEC727FD74728E4A9A1EC7687AA2884BB6B4A599129F857113637BF248C54CABC6BDA91739036B7B605491625062678638BDA4F111BA376DA0D04262BA8A082FCEA37E9C82FFFB330BB15506FE8038D6CAED266BC63316BECD6400DD7297E76068FE16A7EE6A13900857AB6C2D8234BAF41C4967C2535F9BF8F888DC655887B5C74F6D17A9AB2AB16F02D57D311CF6700CB238C297489C5016A09A15BAE7428128B158C23000E80A88978993E956489B93BF2137B2BD0A33F81C4DF584BB46C60FED985D09589C125A6F7A9C4B3132F7BF4A4B4A268BB52702C3B5DED770B3AA30EC2708B93500C5E3C6998FB6EA1586EAE409B6D617C330827F2A417DEB0007C78C8F8C025B8C3415A31313378536EA672FF92625B3443EBE61836421841750E1372A81BA99825B67898F2009E4AC8D5F89396724B7BF29E16C72C1DF192AA5277F01A5428B4AF8B284D85D987E015AE1CB89AD9C9230E2313601B5B49416D0B18B9D75326123A02E363C444993213D387A1C7A021449FE4E35C4FC30DF0066CEA593BF7DA67EF6322537802CF068E2D56B178F929A630C3A5043528A489928CBE1A1468F2802F8F890C042114426BCE7FA6A335441BF1B159410C62FED916CEF3B0D6F0B54FDC3ABF9778743A87D3F417EE573897B45CD5BA4F893CB13AA077A696BB37DBA964D0189FD83DC725129D9425D9291652CC84EB3145FDC88482407748985D72C124EC835CD800BB11F81415A44E474BC553998C1CA945FBB0035EDA52B93B3D341A29E7F808418AAD2FE388D0483E15F68E3B903E7D9C6E051347B1988B6A0A441C8B1FA05C743CBA003BA946BF07996EE14F9E93AC88382CD7F2CE8028352CA7C36A73C9303337CF5377AEA685718C8802B7683019892A904C5ADA3661C1CCA5B4CCB81350D8484BA34CA49AC15B9DA09E1E6887A10802060AB20E04B0142367BEE71CFE023A15602BA83B7761FB08CF79424715CB6CF563E2ACB3E11CA3D6C4095084779213275E13A1E7F14389C301AD53CDE6369E6A9058CE282530593A9E8B1BF38BA08F40B85A94678B619550C437411861B957528CD0175170A1F2308B766209EBF454B4AC4E24C1B230CC793C935770543831131CE38A77A7248C43C79814A03E35D8CBFE02A4F5482E73B1B7F41569E42443A733CFA185B63A120809007A46D5C718C003DE2945B295C0EC42C30EF5BDA5107ADB2125A5E61575382F961C43A585C7400C5AAFBA2BCF595C71C94F0F59CE48904531E0C2EB862C4E66CD6CF280A84B926C42489CCA778A587F0935B7CA369B1140594A1114DDCA6A6F104671CA26E8804EE0D0513F39519217F5A027363BAA21AA561E954B5494D2482873722C7BF20ACCA9B880BC69A3AF4B4C837C8018E52F6A1466D86D23BDBECBDD1F610245F0A670ED311637B87F960BF862D8B81AB5F56E9E24ED8EB011A05867A04DEC9BAA519AF45E22\"\n        },\n        {\n          \"tcId\": 6,\n          \"ek\": \"49B884A964CAE5E0223906A09366063C46250A551DE820A58FF3CDE2C69719316B91F6CF1178768B082F97822DF4D6172ED11876940230978B6B0156EC190C462BA75DE722E206535564A68F311C7EE015701CCD7E954B5B4074CF43CE700C70B221ABB2198FE63639D622669F309855C490F916BF39917E3E440C97FA6BFF270AF540476215A8845A88D3E3528F6715E86C26164B86F28263B01123DD05553B8964775B0C4E50A8DFE08CBF08B830A0973A963A09C1B077564EBC355A07546BCF2422C42500F9B26F586B2873ABC392633555F72126A2B51EC935333939C73580E44314407ABEB4B06DFC570287C5B758E8AC30A6B2E912833E2B72C55CB9BAC9BA7FB2AC03E560796480EF9A4342D49A6FC19E5C1A9386826C42EC6319759B1CA260D43247A0756E857A00D05B78AF65A9759159EEEB72E7118CB6D039930584111A8F2E0CB5FB449423871F7030AB5908B964C2ADE84195802521C16088D14403EBEAC1A7963BD9DA827DF908752568B230919791A6F21AADDDC05E77525FAD0323F215215DF107B4A3474DE3711A447192537145127D7CF2B33F9A4268106B2CE487612B000E6B9520CCC70D7225E2A6055ADA0DB8F024333AB1EF182C8309A697666FE2850C29A1B7B9394DCDDA8AADB9C3EE2910AD1CA85E34874ACCA693C8CDCB66A477E38CF1B5AF5B97260377B4A43B8C0E085C41F5B2EE44B4A7CA1D58D9720C62475986798C13C1A8887F3EC660C04C92F0AA7F0C581407F67CA03247DFC5AE411CBC6F3BBC6F16CA66B8389BBABC9193C22858AF6BFCCF5A4595747B9F9F9BA043E69DFE06110BF970B2F4C9FF1A149F76CC4A64C7C283A716791EA6CC6B3554367A83AE1545054B02A725185EC8042966F10F3F3B4EAA872DEBF7B10947986D1884F3C59116538A85537016B03BA4B7A156061EED350BD04868A60C93ABBB05F97314BAC935EF99BB4301C4EE6A59936443A7298F0584980E588E76CA44F12182DCBACED7F90F4C864F8747C0F2B6A796083656FB9953495787071E61B41B46B582771320B8969C9B5941055637D69A5D0DF3C798D00D16E7259CAB3C67520D2FDE0A3C05715DE22382E369D7644EA2C180C0FC5385E6394054\",\n          \"dk\": \"4795036ADB898D0B5E69AA2F9913270B8899A97750A219C2B88C2BC0795CBE255E04E959A081263087706619A1C9C19FE54C05B14B1BF3E5910E128643771DA298839CFA2D752836B1759C5B58CFCD3B4782976EA96729315958FCB11C66B25F0A1B57EB85B1B892A0937A877C4508796B3D6DF66CC19598D55B9842C848A7485CF87ABB2A340497CAB032376E16613B55147C78864B7D0B9AB1F52985F32561A3A80AA4BE63588983E90EC659B2C9FC45207972C8199B46095BB56B878E256732C892C9395021521780D40778C4BCA06C91404428FEB050F44225CE0693972796A3D2AC3C028377782F9C26528F45962175C42B526872E6B34D95355BD0B1EAA2707B6C2A7F1074217CAA1EE842C4064A5D556B9F70C37A7C52CB468694245E427CC71959B390BAA5FF744869165189C1A4A43BB3D39A9799A17305999A9F3171711398FB412D3BC4625FF04444B0557A765FE785414BF08B308A5F44A64CEA59988F294483F623063B602D55624BE494F2D057396883D48995D10080AB399663C00FE0E17162B2C47A19BD2A69CDB2DB76A3861336F04C2FE0A498FC9698C34BDCE751CCC3C9D4E9539CAA0C6523C1C3AA87C65908D9045203C32AB19442F0065CB1003E02E735999441DBB7ACB1E73C92C62E87D7A4A65A508829739A576B13A7C94331146EA66897248B4887862A4152B72B2FC1B9C7CC7447C8E449EE93BCB5F88AFC43226EE51AD47BB548C665A9A2833AB7938FE403A602051685A3CC89B4AA6225E47564DD09D05B101CAC312369A54B320194710AC53DF57C5479A7485250D1EACD5B8BB19069266A03C852F81947A1BAC5005E344447A9DB5FC4B87255C638524A9ED255BDB9700B31D514A5A72E08A51F42D963D71A88BF605E29C41EB8C88D48BCC0493CAE6129BA4B046B54AA08177634C8E8B1A5A948301256D3C3B6A9A8808B2B0F3716CBBF196427C661F342CC7B50CDB56990AD252242C2C996FC98DE1A26374C0575DB6773937F0E86A16F051D59DCBFA88058BC4801E096BAF9866B9C5A01A922A6EEA34D22CA346975BADBD28454821EFA0B1288281849B884A964CAE5E0223906A09366063C46250A551DE820A58FF3CDE2C69719316B91F6CF1178768B082F97822DF4D6172ED11876940230978B6B0156EC190C462BA75DE722E206535564A68F311C7EE015701CCD7E954B5B4074CF43CE700C70B221ABB2198FE63639D622669F309855C490F916BF39917E3E440C97FA6BFF270AF540476215A8845A88D3E3528F6715E86C26164B86F28263B01123DD05553B8964775B0C4E50A8DFE08CBF08B830A0973A963A09C1B077564EBC355A07546BCF2422C42500F9B26F586B2873ABC392633555F72126A2B51EC935333939C73580E44314407ABEB4B06DFC570287C5B758E8AC30A6B2E912833E2B72C55CB9BAC9BA7FB2AC03E560796480EF9A4342D49A6FC19E5C1A9386826C42EC6319759B1CA260D43247A0756E857A00D05B78AF65A9759159EEEB72E7118CB6D039930584111A8F2E0CB5FB449423871F7030AB5908B964C2ADE84195802521C16088D14403EBEAC1A7963BD9DA827DF908752568B230919791A6F21AADDDC05E77525FAD0323F215215DF107B4A3474DE3711A447192537145127D7CF2B33F9A4268106B2CE487612B000E6B9520CCC70D7225E2A6055ADA0DB8F024333AB1EF182C8309A697666FE2850C29A1B7B9394DCDDA8AADB9C3EE2910AD1CA85E34874ACCA693C8CDCB66A477E38CF1B5AF5B97260377B4A43B8C0E085C41F5B2EE44B4A7CA1D58D9720C62475986798C13C1A8887F3EC660C04C92F0AA7F0C581407F67CA03247DFC5AE411CBC6F3BBC6F16CA66B8389BBABC9193C22858AF6BFCCF5A4595747B9F9F9BA043E69DFE06110BF970B2F4C9FF1A149F76CC4A64C7C283A716791EA6CC6B3554367A83AE1545054B02A725185EC8042966F10F3F3B4EAA872DEBF7B10947986D1884F3C59116538A85537016B03BA4B7A156061EED350BD04868A60C93ABBB05F97314BAC935EF99BB4301C4EE6A59936443A7298F0584980E588E76CA44F12182DCBACED7F90F4C864F8747C0F2B6A796083656FB9953495787071E61B41B46B582771320B8969C9B5941055637D69A5D0DF3C798D00D16E7259CAB3C67520D2FDE0A3C05715DE22382E369D7644EA2C180C0FC5385E6394054BAEFAE1CB7C96BC32E97C146C2AD302DB01C6E7B8E43BC7A236C00C6FCA6F17C4B0A877F51434F70E2D8DB0A51BEB0A7572EF0DB7AC26ABC5D333C503B68BD5E\"\n        },\n        {\n          \"tcId\": 7,\n          \"ek\": \"36C0A54CE714E66B18D200AF7C822E555A9BCE32884D2313C0012FDB81436D8038C37258EDECB2EABC9174974758C55DCE98933322B49CE815DE3B83F3F4A7C0D7CEC0D2A08D68475853B64D21ACEE3C74BAF17C19306BFFEAAD2303ABFEB0AE941839A4F69B48B23447081BC42BC1CEC648C9D1B319E778DDBB262A7B7D0A0946E5B7B71AD54E881C42AA6B1DF268B998D45C2CA18B9F35900A6106F1D8CF88F07C37A7AE283AAAF1B94BCAC6A102F631053760C57B157A098FF4F47B023527BD526365259898484211749AD8493ACBA57C2E02C346EA5032553F2F590F785B7691C83DEB9C7124318969FBBEB81C361F6CADF4EA78C27185CD9B9BD99670ECB41F56C4CC261A39EC251DF34A144B189E108094244557F217312B1BA083F7804928C4950950CF75555BB0B7140406F38CC8B9D21449A756AEC213328AAFD6A8596BB20152BA888F4A591CE162968BC263984655F332ED420473FB6DD0BCCA2D0A3595E43046C61AF6D6317C123BC18AAD6E80736F41BF97134CA7DB2DA8C963F9E2518D865F7FB770C5BA20C2501B44A36E13782583C1579133260E41A77871CB0CD7827D7015FB66251EE9622EEC007FA88227C153B9E7509866AC836A85BF960182F14032606123608E302C0C07D2BDF79B9C398CC437A7A3A2688E0327418B483D1C2B6C3B92B76554A7E14555BF742180FC7F2036C260F50AF5D47AD24033FC62B08A6C10E2F0096B4C5EC5DA8260D2CF9B3816C92A353148C19AC410466C05699209C6B0628A5CA12472544AA822442B93D4F549F5C94A54C4A3BEE5A9375B55B69C77DC260AFE242F6333591279AD1E269D36F02722F62D235186074B3B1AD1C731E0A36996672E74797DC7BA677AA577A523FD38A4A3FA71B7782185911BD837432920A753996776465D33692C859252E294622EE86F2340A5BB624898774851B587B6203A7E94CC909623D027C9EF06493596B9E6A63C02A4CAD21B2EC4792C6FA70ED9AB90D8988F85D24BB37579D4A71C0823167DCC30382178E12C2082AC70F1D7A2DE652296A203D93C2E3CC1473C411C17390BB0ECA50981905AD7F76DCD197C5867AF1F94E3221BF042DCDC99B3B3679587BB2507C464618D\",\n          \"dk\": \"F9AA2D50611583687E826193429B23F39B07E2988D6A018D98E05E01A853C40BA5E321730868A55D245769F17CAE243F39D6389346A307934BEE6A011C4A00ED72610E61548A64C5F2D98BFAC484518C4304E454289CAD24A5BBFD3717AB191E5C83AEE1E22CC1B756C147786A13580715B3482A5301D2271588BE13374251B68EDF6024AE553F1BCB83CFECBB4DA1A0E759627A37BF3F83B73E267D40F867629380482CA32D6743A5C8565C7AC3DB3542813B010E98642B490B361638483728B47737A1B8367EDCC93D8B3960EB963038184AB09DF1945A154A8437556CE659054AA64D81048032375D795A21C89B5F5DB68DF203C496A06CD9B26B6331B2B00BC5ABC95DD6BB596DA819958A11A2F57A8C85C9471691F7570F41C0390C6571BFA22A91909BC9C09070C0B1A0F5AEBE59ABF0C3C36B2C17A5D1767454C15697C10CE453A1597F63440D6127A09941A5FB3A96DFFC86C82015BC623986E6BE7CB02AAA23B5D5E26E4CE01F8FD91D99B637A5BCAA23532D46B993D4EC5797C28EC6D248589CAC83A600A7D555E3EA7F07D09726B174AF282162A3B268A5280CFB6AAD7012E7E639598AA2003D9DD6707FA285AD334619978BA227B879B1F37DF83157DA95B5DE8CB66A01D036E766262719E21224A65932C6D1185676598BDC51C4A29330BB8C4B5176A069B93848B57BD24AD5F277A5681674138B5C1CA73075706C9797D6F8BF23E2AE5DB44D5D6B4144C09AC9C6BF6E556790968717C847A6C01AB99215162BC5B2DB6E02E19ED568A56C54BA4735A793DB473BE5708C705C784B9EE2E02532855F2F420608990101E34CA930C63A0833744B67965BCE0C1ACA3D67793DB7BCEFB32A7D378D38E87209F69C66097908EB8B07A94584335F37C44586A55C5024847C654B97C85BACE70B495559BCB60AB1625525E808DCA2496F549C8130CCAF60BDC3F782DA61AA98E03A80C05C6AF99308165B1291AE613C97C646A46E05AB87796DF6447E1D8326C03A7E9D7921BE218B69D469BA2449E5C714B7C5B1385C407BE14E7A978566E27409B72D04B9540FDC43D7BA0C95754C36C0A54CE714E66B18D200AF7C822E555A9BCE32884D2313C0012FDB81436D8038C37258EDECB2EABC9174974758C55DCE98933322B49CE815DE3B83F3F4A7C0D7CEC0D2A08D68475853B64D21ACEE3C74BAF17C19306BFFEAAD2303ABFEB0AE941839A4F69B48B23447081BC42BC1CEC648C9D1B319E778DDBB262A7B7D0A0946E5B7B71AD54E881C42AA6B1DF268B998D45C2CA18B9F35900A6106F1D8CF88F07C37A7AE283AAAF1B94BCAC6A102F631053760C57B157A098FF4F47B023527BD526365259898484211749AD8493ACBA57C2E02C346EA5032553F2F590F785B7691C83DEB9C7124318969FBBEB81C361F6CADF4EA78C27185CD9B9BD99670ECB41F56C4CC261A39EC251DF34A144B189E108094244557F217312B1BA083F7804928C4950950CF75555BB0B7140406F38CC8B9D21449A756AEC213328AAFD6A8596BB20152BA888F4A591CE162968BC263984655F332ED420473FB6DD0BCCA2D0A3595E43046C61AF6D6317C123BC18AAD6E80736F41BF97134CA7DB2DA8C963F9E2518D865F7FB770C5BA20C2501B44A36E13782583C1579133260E41A77871CB0CD7827D7015FB66251EE9622EEC007FA88227C153B9E7509866AC836A85BF960182F14032606123608E302C0C07D2BDF79B9C398CC437A7A3A2688E0327418B483D1C2B6C3B92B76554A7E14555BF742180FC7F2036C260F50AF5D47AD24033FC62B08A6C10E2F0096B4C5EC5DA8260D2CF9B3816C92A353148C19AC410466C05699209C6B0628A5CA12472544AA822442B93D4F549F5C94A54C4A3BEE5A9375B55B69C77DC260AFE242F6333591279AD1E269D36F02722F62D235186074B3B1AD1C731E0A36996672E74797DC7BA677AA577A523FD38A4A3FA71B7782185911BD837432920A753996776465D33692C859252E294622EE86F2340A5BB624898774851B587B6203A7E94CC909623D027C9EF06493596B9E6A63C02A4CAD21B2EC4792C6FA70ED9AB90D8988F85D24BB37579D4A71C0823167DCC30382178E12C2082AC70F1D7A2DE652296A203D93C2E3CC1473C411C17390BB0ECA50981905AD7F76DCD197C5867AF1F94E3221BF042DCDC99B3B3679587BB2507C464618DC0D607E1D82C3729DDE4E456836E956E187ACC8BB29F262BA6B5038F51F9F8D3B1EF909D94C56C134107B913B0ED29BC0851CCE424D0FB69EDC04C685A540871\"\n        },\n        {\n          \"tcId\": 8,\n          \"ek\": \"A2178E9F524466CAC53EB49DBC5367F4F096394526BBFCBBCF0178721902A0373BB4520D50F039517950C5C0115FCB53DE3ACAA4916C94BB19D746972DC882129B3A4F658E4671B538183A93B1775E56845C00569987103D1A3270D17D0C807EDEDC3F9390774F074741611A3A2B8723C671CCF6688051CFAF25B2310221BD0290F5E6C32DC03DD44C87E395B7E8B56FFE15BE62926411AC28F1A34DFE5B460091520BA244EF5A0D26401DE2B3A64DAA9CCAE3C6A09823D9C84DE1A17E1E94CB26992050F2381C3C947EF575C564CCB6580148A12200E97D9F2969295A90EA78548743C95D15AC8FDB0D91B83AF0A9A75A862AE2558FFE6A7CDA7008F0CC9B5831642047841314406DD0815A09ACA883667F080C6AB959C969140A767FF56A087DA5BD285265149963404293E2E1BCF7599A30A20CAAA056D5285C2731914D85C9F87BB3FC613A809293C81922343A0565B27B779A72BBB0B961D1AD8AC4AF5EC677F997A29C38BEF5F6CD90A897F379871C0B658A7849BA2C43725694B361331236ACCFB55C5E9650277C3D378259067354A8E2167E5578D8F8B610476B997403FAB4AEE365C118929956309B7F7114BAEABFA43408820593D334834B1826C9E705092953C5B73421785C3371687D8C366F9420706389A06198422BC13BAA7075F9B54BF6679A166942E231E1D60637B42FE9BC00E0A9ABC646B3D740201C3985EFE87FE5467F88D6456423A58B07B8FF8C1159E738DCB1A2B3313654DC9F60A072A34ABC3A8ACDBEF6429AA5C088666A9FB14119B69D37D9369D1662DFF3487E826686E812CFF23CEF60C52948424C4A11A933A2D28A8FA880A3E6753CAC7C79F4E111893016E0C067573CB588A6B1FDFB23DE855B9BA6441A837C9F47329A014732CB3DA8B9AC850816E7EAA56584734F3B60E8C732214A86D0309FEC791E7EB28A93836A0FD85C5FD497CED070D71101EACBB92A052140393B18D83FEF654CDF1CB587B2B5CD56CE8C4215C57C065F954CE2A2C0BB65744DC8C2D49930702663D2A0173E515450524B973B7AF4D6B7C816773B6C3B68EC1A06A3C74E038F9143015D3401CA6DBDBEF1D5EF5A349B1C9DAFB96E20DCFEE7BA2EECCE5A3AE6\",\n          \"dk\": \"2B409CB0052C3BB60811731A4CCA005D8C06028BB26D166770E680B1C3CF981608DCEC93069393EDC4559E56673D0B12B8884B8056BBB988672D373E26A3268153288EB91D1EC4642657B7081B7EA587294074555067ABC197A9FB83A15502CD103458CE2736FAF068B2344163A6913E8A232601C217ECB3DA3B57D8D4AA0AFB56A4AB20D250CF97206D78FC004CFA63DB3C27233B3A9A1B8369B102F03AC44F231683F10C4676C2E0BC19273606C8534C1151ACDDC299C9F0C432CB0F03FA1A8FBA4299B4C3AB6638FC924742B8871876989B7C4CC3685FB09B68F5F7126B5645F9D365E5342B23A12E09E4B7CA3ACC3EE06EACE941EAF54A8475492537295F732D8D88538A594CE96333A5E16EEF42155D241E0E2A1077477929A9C51B6737942A57AF4964E26CAD78F22859C453FCD1927592CEEC583724718C92B4BCABF121BC079992A1CA574894ABC1C364456D806A435291CE2C702247F286E9989FB108283B52CE5B6A209BF43F1C991BA200550D90CE2B2A23B715519AD3BC3ED277AA6A50975C2B54B6B47AE7C7E55A7B28BA996ED344A03C21824C9F0E0C55050A54F1F4CC3955353B955419B4BBE0962FB8D4C9EB94B2D2764BFEB41EBC6AB8383B4B6D58B97FC9394E6AA2D1B389E4BB5C4A72AECA0780F15B5F29D5910D3A40D1D21989379C640310F9598F05607F2E47129B27C347830388CA25DC0721B9927FD4B594819965843083B4D761DC76C67626393353999BB4149D4A0B7B5BB51987C96F10731E2BC51ED7621E68179120869226CBC3D4A5B4D07DE6E5396747674150B07720BAC4E94A982B76ABA552E880BE6B7227357028528A26850750685C5B73AB95FBAB8F0F604E3DCC5B4112A72133478942C68A878BD9D92C0E6CCAFB46B57E563C062922AEF461FD093463483C9A2188AE9113B24748077AC624726BD3D30D354497616359B292A9C4381ADEF34B215BB326147713274284F6B3CAC38A40BC9856E459C029A639299B528BA553C892F2219635D0489456B3577AAC3453B6075A72DBCA89B4F27B16F55B5E1C1A35214F24579033E5584A813F8F502DA2178E9F524466CAC53EB49DBC5367F4F096394526BBFCBBCF0178721902A0373BB4520D50F039517950C5C0115FCB53DE3ACAA4916C94BB19D746972DC882129B3A4F658E4671B538183A93B1775E56845C00569987103D1A3270D17D0C807EDEDC3F9390774F074741611A3A2B8723C671CCF6688051CFAF25B2310221BD0290F5E6C32DC03DD44C87E395B7E8B56FFE15BE62926411AC28F1A34DFE5B460091520BA244EF5A0D26401DE2B3A64DAA9CCAE3C6A09823D9C84DE1A17E1E94CB26992050F2381C3C947EF575C564CCB6580148A12200E97D9F2969295A90EA78548743C95D15AC8FDB0D91B83AF0A9A75A862AE2558FFE6A7CDA7008F0CC9B5831642047841314406DD0815A09ACA883667F080C6AB959C969140A767FF56A087DA5BD285265149963404293E2E1BCF7599A30A20CAAA056D5285C2731914D85C9F87BB3FC613A809293C81922343A0565B27B779A72BBB0B961D1AD8AC4AF5EC677F997A29C38BEF5F6CD90A897F379871C0B658A7849BA2C43725694B361331236ACCFB55C5E9650277C3D378259067354A8E2167E5578D8F8B610476B997403FAB4AEE365C118929956309B7F7114BAEABFA43408820593D334834B1826C9E705092953C5B73421785C3371687D8C366F9420706389A06198422BC13BAA7075F9B54BF6679A166942E231E1D60637B42FE9BC00E0A9ABC646B3D740201C3985EFE87FE5467F88D6456423A58B07B8FF8C1159E738DCB1A2B3313654DC9F60A072A34ABC3A8ACDBEF6429AA5C088666A9FB14119B69D37D9369D1662DFF3487E826686E812CFF23CEF60C52948424C4A11A933A2D28A8FA880A3E6753CAC7C79F4E111893016E0C067573CB588A6B1FDFB23DE855B9BA6441A837C9F47329A014732CB3DA8B9AC850816E7EAA56584734F3B60E8C732214A86D0309FEC791E7EB28A93836A0FD85C5FD497CED070D71101EACBB92A052140393B18D83FEF654CDF1CB587B2B5CD56CE8C4215C57C065F954CE2A2C0BB65744DC8C2D49930702663D2A0173E515450524B973B7AF4D6B7C816773B6C3B68EC1A06A3C74E038F9143015D3401CA6DBDBEF1D5EF5A349B1C9DAFB96E20DCFEE7BA2EECCE5A3AE6E2DD64A46E296448B930EA2D39F462B16DD4AC6AE402DA573C0968CCDAAC7F50671C8C054A52A67BEF8015DFDB5711C9197E84A5A553E794AE0811C8432FEF6A\"\n        },\n        {\n          \"tcId\": 9,\n          \"ek\": \"0285512CF6422AFB54DD1C54CD200FFB3AC28AE053E8B6443B96C83DE8595F710D883A513271CF0D93804BE93C84B9A754802C04455D89887DE048B2484088733A701847300FF3B137F509D46033CEC89A4B9602D5A83F77CC20FF4066BCF95904E2789755359F7B04B174983CC883BDB393BE2A02FD3433FEB126A4288EFC4670B0670B2715CDC5700DF6631DF29643E33382EC89B89BB1AD41C6ACAD704BEE982841CC588249BB91A818890BBB3C590A08446D88647FCB5CB15824CDB09C55FAA2457961BB3C79ADAA833F55F154072CAA445B8D61E23993CB69441C956492AF07E702568A350C01BD072CB2E379071845A67149B2F7B2358551770A34A9AE6AB45B2B6D5F700E597184A1425B28047817720C1981C583717F7F4BBF2F13A6D31A691DD3951310A9D135591717A451F00FDD71ABF325B041E3A1EB095F31D27653D1AD0F3CC945D431333A3349B7B8D786B16769309808C11095209CCA1442AA40B9A863B4973E85586899ABC17D1034E64505C1F9987172538E227111EB86A66C9077C268B7A24A477991DDA6753672BD972510EF3347E1B7314BE172A457A4A0B9909718418C478D396B9F15BB6D49D919E192BB714BA992686292056DEB4B1AC84C0A356911437BAF2E053B170162BD25A0E554A757037C1E40BA6E341EE230B592D33F0B910D32C01CC3F865D104C3B652A63229A90B155D41C68C3008C3DF42CB757A36634C6C7372CA47A82A652C5115BC0383842E5AC18CA6239799A37C91700845180AEB407B88D9390F0A6353EA1459D258E70567E2A5530588C63257B3A33050FEDC1C4FD58BF7F547648B96927A4B6C01901F2CC97995AD993C41A7290F7D555C173C1FB12AA514446E2495471305C9D94236656729DBF9832415C3F130864C4A2807509B14BB187F6450F9A92F020B62238A8F23BBC8ECE7BB33305E3C70B43589BF43C88D712B386C68945F7A7F56A23DC8D45ABA95A89BF18BB0E1AEF4DA810723775AD3125C4316056433EB04202BEC5B83D84040514407E7862685652AE2BAADE15056A2106E546A134A8527A7CDF78866E42CC7C322861CD5BC3D7424AA358204AE6D07694FAE81991F5C67C160B09429BE12543DE0F8\",\n          \"dk\": \"893175C6F86630760EFF4332E35A8642202B2F8503DD24AF50B1BCF10B63951B8E6D426D0357B27A93293252852911C14765099D330D25608885934216114FE2300D5119262C4B959A9928EB058DCBF1636E30A173F5A31EB580A90ACD8774233E652CF4E78AC1C51282D2BDA92A0F6B74ADBFEA4CB8A68F2D3CBFA3298AA8F203EB32063DAB216F811363E69A28A8C8417B524F09BC5D864CB5B502D88B2226193C3729CBB968105FDA459ED9688B12580957180D6A01FB16CCF3E78A9D07CB35A55135B2A1C3D559F5B01A4DD58311417F4E1B98875456849C6E1A632B539180A9108DBF872D96847E79E82C6D23A51AE23042763361A125AE4572BD104F03C548D3F1A377F481F261BD596321BC68B34B31A40829B8AE3CB49C2631A229244EBC08C722839DD8748346C979A9AEB36901E0D9980B6220CD2B32F6C8B1C01C5C61AA0055C32C4F99B1268CC806BBA9D0139D867389B04A54AD51816E965B5CE4A0EF245D6C2006457AA0265081D26214CEFB5671BA20166680CE0C8A817005F7574878B9AA168A8E6A6CB99924543A374834742C48665253E03DCCBC552A5938C0115C5F709B298B0C0E245696CACA53152728B60ACF3C5EE1F92DB30131DB853371021AECE73936D0606A8BAC25430CE0138CCE05048395301A03860D439F9CF45F82680DB9B98B0DBAAA41BB776B60CE41734AA92B914458A7C1CB9CCD55731FA5660D567F489B3BB56754319954FD6A78E6626C69C68A6EA882482113886B8BBB980F118193C1165FFA3A9A8D1A20626514F998CE61EA8BF92B09E253C773D5B2BFDC7C3DA38115B8AA30361C7BC4344CB772EC722B55D1AC4FB7C8510CAA1886C345DAA0CC440EA1DAB0BD131177B9013925418B4B1D54B90BF7F5914AA49B65882FF1297ACED09E36B066671666B875BA1BC37D2FB76F80240D2B996BF6515A0198B66B66A4DEE40DF748C807E1378EA76308076A05FB1F6A071E2D635576F952E4F62A15CC6442563EDE9C8E204789A8F06EC8EA6032D5032A77CAA5963CDDAB24EBEA5708C7B761A67CD0464A2A00233AA648080AB5B2191D73A91D0285512CF6422AFB54DD1C54CD200FFB3AC28AE053E8B6443B96C83DE8595F710D883A513271CF0D93804BE93C84B9A754802C04455D89887DE048B2484088733A701847300FF3B137F509D46033CEC89A4B9602D5A83F77CC20FF4066BCF95904E2789755359F7B04B174983CC883BDB393BE2A02FD3433FEB126A4288EFC4670B0670B2715CDC5700DF6631DF29643E33382EC89B89BB1AD41C6ACAD704BEE982841CC588249BB91A818890BBB3C590A08446D88647FCB5CB15824CDB09C55FAA2457961BB3C79ADAA833F55F154072CAA445B8D61E23993CB69441C956492AF07E702568A350C01BD072CB2E379071845A67149B2F7B2358551770A34A9AE6AB45B2B6D5F700E597184A1425B28047817720C1981C583717F7F4BBF2F13A6D31A691DD3951310A9D135591717A451F00FDD71ABF325B041E3A1EB095F31D27653D1AD0F3CC945D431333A3349B7B8D786B16769309808C11095209CCA1442AA40B9A863B4973E85586899ABC17D1034E64505C1F9987172538E227111EB86A66C9077C268B7A24A477991DDA6753672BD972510EF3347E1B7314BE172A457A4A0B9909718418C478D396B9F15BB6D49D919E192BB714BA992686292056DEB4B1AC84C0A356911437BAF2E053B170162BD25A0E554A757037C1E40BA6E341EE230B592D33F0B910D32C01CC3F865D104C3B652A63229A90B155D41C68C3008C3DF42CB757A36634C6C7372CA47A82A652C5115BC0383842E5AC18CA6239799A37C91700845180AEB407B88D9390F0A6353EA1459D258E70567E2A5530588C63257B3A33050FEDC1C4FD58BF7F547648B96927A4B6C01901F2CC97995AD993C41A7290F7D555C173C1FB12AA514446E2495471305C9D94236656729DBF9832415C3F130864C4A2807509B14BB187F6450F9A92F020B62238A8F23BBC8ECE7BB33305E3C70B43589BF43C88D712B386C68945F7A7F56A23DC8D45ABA95A89BF18BB0E1AEF4DA810723775AD3125C4316056433EB04202BEC5B83D84040514407E7862685652AE2BAADE15056A2106E546A134A8527A7CDF78866E42CC7C322861CD5BC3D7424AA358204AE6D07694FAE81991F5C67C160B09429BE12543DE0F855656EF499C81763075415747986E379B5BE816E964FFC959698CAA61FC6B31BC02D5CAD9E565727E19B2EFE4FA2E083F93EA0F5ADAF97522F33F416F786765F\"\n        },\n        {\n          \"tcId\": 10,\n          \"ek\": \"E295110B123EADD95814826F65D2568360578F2011EB917A24E45AAFA629D7E3C8DF3210D85831CB806B8EC64CF5157938CA08FF6848A11B19D8CBCACE2090474C30C6053E20138C01149114A2A73C129C8055ACA352B05AA4A464B07614C4ADCD002B0B477F624CAA0026820A2455FCE2CE9FF2CEAC299554DCC21FCBBE9C36A07AD67103C112B9F955A1B551D109C88EE54075F203016A30B4A437AFD635F105B7F5B1BD5ADC5925C9347176AC0374CB38250A6583928012A283D4B067E0CDCA554C04A3C1CB0578070CA5A545894841AD38E599643B1CDF94ADF4E917057886A8BC577ACB4668769707DC73D7186700F65DBCF3BA3964B952B7244070053532042924552CA4A8DB807FA12B572D7582B15BCC6DC152A1125955885ED176915448CB7CF5680FE7AB37D22FED7A5E76552481139112351E4FA01EC3B829822788E3A9A463A582F98B9DAFEC1E05378413D805D3D043234578C73032D9006C21C733FF45CB649AA10C09775350523476A5F760175FEC96A26B2E52BCBA008C1C28707ABE5189F2515164B0194BCC1803231B41DCAA20C335FFE0A8BEE5425CAA5832649450924BB1424734A0940E7A7A24D917741010B9B00C81DCBD4BF394BDB5BF03358614718FB1F162877824EE0156F4C462C597B937EB8768C2CFB6F7BE36D7C307C431AFA83824C812CDA245A6B923A5199DD59C1344895E4E67C07F448641D32E4F526693724E0A025369621B220341AAD6C43A4C7CF6C27A5CC65095BCCFD569019808B2AD850168A24371E292D55851A0BC58DFB0AE57E3512B1A26C57CB373F996C0341BDC4714C451979524A58B0502A9996CE09980AC9CB4EA763B8490B4A7B2530B87962B702BF1F04E3CA65AECA1086AA4317331C78848504A9784F40B4A36BCBE01948F08AB400036BAD091AA1AFA1020D60B6278686AB365F208A6D4021C90468FDBE7243BD89D5994A7FF092841389243AC308CECBC5C12105E1408D500A5931B86FD16B555E0AEC9509A63103EACA5A0090C0A0CBB90F8F970980B60954C5985F5644C0914D1C558B65C9F54A42343E824D713B0F1EC9F2BAAADFC824BAE43FE99FD58BF6E845A2AA9ABB4AD4171FD9ADD88AD012EA97F\",\n          \"dk\": \"17F2393AA323522A7D4A6590D57BB2C0EB6B54F578FEECC61C742BFFAA01C2E2A01F68586BC083907728C4431AD6726C58C3C35030CDF1B9808BD14773A91E01577B6E3B37312617D50672B9B9643638229BD151D6F904AB4023A5B1203556B690A41DC558A2BCB00B167C611F9977E5538F227023F088BDAC535556280D3BA63E65721797998FC2FABA44BBB697ACA8835ABF06247A184A8B91518B32A8295B086A9D6313E2825C27C7C126EA1365C68735FCA9128C6256202AF141CAF93525B833578CDC0E31EBB4B2A601643249D32B4C1021C79A1933ADB593E02555E0A467687A5E3044C1815163EDB75A27F15236F03437DA44BB0BBDC24751F04C1F5ED8CD87AC1D6A295CAE175C301781FD39555D688794E74C70D6674A80613A3AC4286A28095CBF68D51A98F50EA4415C6D35460049125A9197A18ACEB0819962BBC6C1055F64B09CD46B6CF9564EBE085767893F7A67AD021AB9157081250A150C904633F9314B6192E221543BB1B68B152B26A3BA74C0C50F54A00E3163505BB51658CDDFA59583847D99133520D08054F6CDF69618402A848A79571479AF36F84A33082E4E05B71401A31879CDDE8346412A481703A88C698416540D85380D8AF880933B5A252B731E9071FEB951500CA7422144A8080280E7054F840BB88763EC33C734754430CA01C5D8A0430B2843975BD1A283DB930CD4CB2B89425F63760426A93AAA27337F22B32577998C34BDD66B6CF6313EFCA95BE3A9CED1A8402533CB3DE7B52A94C551CBA6AF20010D0C7377F8165CB21445D9CD102A15A6A72C91BC4898A47A4C4262A06CCCF3B384E0A52116F05B3FCACF5E1137B96342B3E6BDFE9CCE7C0BADF855A8DA9A00439335ECD15062998609116A1B16BEBB208ECC37CD4ED3BBB3F6A022A9B708513D47164FB5CB51910448CE82186D3000CD487A20B84034929245108AAC455445743BEDAC9B62918644F7183F17C2BCC7770CB5355AB719024177B623478C2C4326E3474DA1924C963DDCF80048999E7D6BB3C9128083B83B9A13710F65ADDDB604FFF15B24E78221C427236799F9B942FA123BE295110B123EADD95814826F65D2568360578F2011EB917A24E45AAFA629D7E3C8DF3210D85831CB806B8EC64CF5157938CA08FF6848A11B19D8CBCACE2090474C30C6053E20138C01149114A2A73C129C8055ACA352B05AA4A464B07614C4ADCD002B0B477F624CAA0026820A2455FCE2CE9FF2CEAC299554DCC21FCBBE9C36A07AD67103C112B9F955A1B551D109C88EE54075F203016A30B4A437AFD635F105B7F5B1BD5ADC5925C9347176AC0374CB38250A6583928012A283D4B067E0CDCA554C04A3C1CB0578070CA5A545894841AD38E599643B1CDF94ADF4E917057886A8BC577ACB4668769707DC73D7186700F65DBCF3BA3964B952B7244070053532042924552CA4A8DB807FA12B572D7582B15BCC6DC152A1125955885ED176915448CB7CF5680FE7AB37D22FED7A5E76552481139112351E4FA01EC3B829822788E3A9A463A582F98B9DAFEC1E05378413D805D3D043234578C73032D9006C21C733FF45CB649AA10C09775350523476A5F760175FEC96A26B2E52BCBA008C1C28707ABE5189F2515164B0194BCC1803231B41DCAA20C335FFE0A8BEE5425CAA5832649450924BB1424734A0940E7A7A24D917741010B9B00C81DCBD4BF394BDB5BF03358614718FB1F162877824EE0156F4C462C597B937EB8768C2CFB6F7BE36D7C307C431AFA83824C812CDA245A6B923A5199DD59C1344895E4E67C07F448641D32E4F526693724E0A025369621B220341AAD6C43A4C7CF6C27A5CC65095BCCFD569019808B2AD850168A24371E292D55851A0BC58DFB0AE57E3512B1A26C57CB373F996C0341BDC4714C451979524A58B0502A9996CE09980AC9CB4EA763B8490B4A7B2530B87962B702BF1F04E3CA65AECA1086AA4317331C78848504A9784F40B4A36BCBE01948F08AB400036BAD091AA1AFA1020D60B6278686AB365F208A6D4021C90468FDBE7243BD89D5994A7FF092841389243AC308CECBC5C12105E1408D500A5931B86FD16B555E0AEC9509A63103EACA5A0090C0A0CBB90F8F970980B60954C5985F5644C0914D1C558B65C9F54A42343E824D713B0F1EC9F2BAAADFC824BAE43FE99FD58BF6E845A2AA9ABB4AD4171FD9ADD88AD012EA97FA29C976B07F9A7D707839A6D72116355207826F54F27A23B6BAF16524F3C4DA570567D6DFD6622814417BBF673812F2D02E5BFA897D464957AA4219841A93C19\"\n        },\n        {\n          \"tcId\": 11,\n          \"ek\": \"19288830B0ACACEC7E939B82BAFC8FBFBABAEF732309CACE58533E4F5C62B711C5D84820EA520F030A7E61534275206EB4BB00A53472AB206E50D68C7B3C3D82119BDF508272EA5D40116C9DAAB1F7BC5595B9623DEA2D683C69A079494E3A10BEB2B3A1667CDC031F76CC73595C20D3587970FC17671CAFACE83495037444EC6F485329F6A13328F67D5B839609A56D336C05FB712B1C78A944037CB3E9CEF1D46C420A118EF8712593172548C64FF616E1166649819FDE13235D5341C52A69BB154F48C672D3370B59C19A86B13196F25388089D8ED463FA93AEF6F3857832ADFB874765E318A59801ECA9799BB0A303B87867846A025129AED31C5D2336EA968120C0B88199A83221B1FE6BB2365A086165C0EFBC6185332FB10BB542A32547AB549B3C9E666246CEF04EBF3CCB3CD445C7FA2DA1DA6633E227B5209654AC28DCE78E45B2AA927244D8BC4D881C9462C87F228C07B9C488FA660EC29B60BCF494D6D97C163C53607556361298CEC90332732D7A921D5ABCC3BFE0A3439055093CA7EA1A5E75D70809A55ED886C5A9E90353860239EB907635BD3B46B78EB42821E773E62A1910331F127162B694CF64213305055AD00923FBD137A7F305F345884A6BC5B6C534CFFB6F986A9C444446E3D74170AA375D359E4FAA6C99E4315D230F9B69C37E2C444F29C7EC64C277864836410E8FC703437975E3E83E76F988432570BC635F868C96CBDAAE5C6B37D2E203D452C762747FE7494F146AB7591C9285B68E3973364BB1126C3585BB694DC2262E38D94CCA5C1427992E563C72C9A103FCDB4EEFEB46F5171E9DC2B611D9CE42197352E140B50902EFD676EDF89896B5A4808095FCAC5DDDB218E6AB2F5EB88D831648FF75B1382B6558BA9BF1D8316EF4040E82860716075E80CDC24799733B9EEE74702582320C5CB56C495670FC239CCB6FB8F91F7F217E5BC4A0376B9832F0AB8EB4184704B69C33CBECA37579FB085462705BC72AB7F37C3804B1BD14089FC07C85956038F80C67F409F433854B99198307B2ECB38170DBB55F422260D594F33B3DA9F35C4AC2C2DB9275CE21D56E9CDCF309777C616223E7C0ADD4FA7A6472CC0BC13CDE85BD4536ECB1\",\n          \"dk\": \"ADECAEBF22777691A57353B3D6DA4AC5959286C157A3245282D34812374C88067ED3109817C86447F5A807D24E82316C56944EBCC8A68C761170A218390193CBC14C2859466BD164753944C39269660A5583B2B3309216447886A702127BD77F4A5A03F9B19BE0A76A4ACC0F82267A8659BFC72054AA5A23C84940354323DA78569F1370553A71DFC54AFDFCB669377F3E324098E17709361CE1F824DD2C47ABB25FFFC25ADD519F6079C226D77E6B3231B5D74E11221869881A8DCC2188505628E86FAA2ABA16638400CD58B569002851288E631991C3C545B345A1D5646FE304059A4C742A4A27AAA9FBE9593F374B51B453C7E09427A6325474AC7FAB0A3168630202A016632B3E490D77CB61798320C4139780015F6B521A81E2C34494A6C4FA45DD3712B5349B142A1A57166C1FBC120FD5BEA702BA1878A9064028C575BD5535BF5DF70D46536A826BB687E82BB9A541DD795114359222A8733CFB5D6856269D977AE07887DF5395CA6553F74907E27B011320C103A4191E5AC4F6BCC1C12624D02B2295150C52CB6E5C1423E50922AFBCCB0BF70B2CE8379C2B9C635C44D7C72BBE11C23E4417BDDC9339A6CEE23514E790B1931820FDA6927DF74D79DA1B762562F207A2B14776B98615AAEA7276A585C6972BFEDB7327415AF609227A4BCB757762AB819236D5C615772CF00A492B846C731825E6643C53F55BE7D3555EA4B484A891FC35C47E406F12E6C6B34C090144C6754A12E969B13D0A98231AB7CEC0AABC414BB2066FE6F7297F2635BA79A18656914F4977721C6FA526081E2436975B85E9B7687ED16DA57413B5C16400823522434E38658983B7AC65A39EABD32043693D9B87C1F9F08D80E540A4A86033ACC7D6C961F1DC0C47981CD9588FE469BDDB86762992118B94CB9A213CB62A12E651B654518C843B3E3959612F0C6D97F419866CB89AFA055DF145FA28C302BB1E41E342FA852E3F47380F7536A3824A9B8697BCE6525271B3D0936C4BC172F5A5CB2AF5AD78B72A1DA30C761CA84F123E1BFA58B0454279B87517B906A030562F7691DA4A09C456975E601319288830B0ACACEC7E939B82BAFC8FBFBABAEF732309CACE58533E4F5C62B711C5D84820EA520F030A7E61534275206EB4BB00A53472AB206E50D68C7B3C3D82119BDF508272EA5D40116C9DAAB1F7BC5595B9623DEA2D683C69A079494E3A10BEB2B3A1667CDC031F76CC73595C20D3587970FC17671CAFACE83495037444EC6F485329F6A13328F67D5B839609A56D336C05FB712B1C78A944037CB3E9CEF1D46C420A118EF8712593172548C64FF616E1166649819FDE13235D5341C52A69BB154F48C672D3370B59C19A86B13196F25388089D8ED463FA93AEF6F3857832ADFB874765E318A59801ECA9799BB0A303B87867846A025129AED31C5D2336EA968120C0B88199A83221B1FE6BB2365A086165C0EFBC6185332FB10BB542A32547AB549B3C9E666246CEF04EBF3CCB3CD445C7FA2DA1DA6633E227B5209654AC28DCE78E45B2AA927244D8BC4D881C9462C87F228C07B9C488FA660EC29B60BCF494D6D97C163C53607556361298CEC90332732D7A921D5ABCC3BFE0A3439055093CA7EA1A5E75D70809A55ED886C5A9E90353860239EB907635BD3B46B78EB42821E773E62A1910331F127162B694CF64213305055AD00923FBD137A7F305F345884A6BC5B6C534CFFB6F986A9C444446E3D74170AA375D359E4FAA6C99E4315D230F9B69C37E2C444F29C7EC64C277864836410E8FC703437975E3E83E76F988432570BC635F868C96CBDAAE5C6B37D2E203D452C762747FE7494F146AB7591C9285B68E3973364BB1126C3585BB694DC2262E38D94CCA5C1427992E563C72C9A103FCDB4EEFEB46F5171E9DC2B611D9CE42197352E140B50902EFD676EDF89896B5A4808095FCAC5DDDB218E6AB2F5EB88D831648FF75B1382B6558BA9BF1D8316EF4040E82860716075E80CDC24799733B9EEE74702582320C5CB56C495670FC239CCB6FB8F91F7F217E5BC4A0376B9832F0AB8EB4184704B69C33CBECA37579FB085462705BC72AB7F37C3804B1BD14089FC07C85956038F80C67F409F433854B99198307B2ECB38170DBB55F422260D594F33B3DA9F35C4AC2C2DB9275CE21D56E9CDCF309777C616223E7C0ADD4FA7A6472CC0BC13CDE85BD4536ECB1EDE9E402A11646293C3851259F4E8E4412A3C5986F8BE4427BE39C4E869C061F71A6E59B13B36CAA406DBEC53F3FF2F0CC529098A4C8FBFD032C8BDB8B0E16FE\"\n        },\n        {\n          \"tcId\": 12,\n          \"ek\": \"99263CC303C92A0BC6810487CA1664E5405EEE5987A621C33E30483D4C7862F9760399B4C23822507810AA321D04009CFEB2505860BCA818BC0CFB9DF44A5F47D23AE80C25C2A67455B64AF2D276606A208C74867EDB5373BCB93B06AE81DC67FB73514FAB8E62516D0A0760FA0228E6BB0A252120FC7676A0340FC2939CE8118782631B91F962CB14200C928091876998B1B19EBCCD59F92815930DC6F9BE629099B3334772AB3A16D630D971B2093C136F29C2EEB09F03F8592FA4980599542E422FF636C8BDD6672454238D4C547FCB43DE1B10FB385CFAFB1F84D95E6C776973867E2E40615531A3126B076E9B30D3E9BDD2E12C34A9764E90000AD179CB159259202800299B2902606603227BB37A9B679320B60B3EB9A917D2C0F0CA6A3248B1F371801661A58FEC24B8807B06A5B67D087CC6E3008D823024575C5D7678C93973E8201101765DD5A10889091F8AF87512C206C2C60A96081FCFE32C3B7C18A643AD56515674A3B745D154F7076B65E191D5026301AB45076B00288B91DA147F02795A0D0AC9E8C61540405F44C8B7C766BB454525B8D48B9418B34E3135B8BA3AC9E2486B162C23B266856BC370B4833028C0F753C1E6490A9608B1C5169E6EF0B43BF6C5CE0A6122CCB28C9A96F794B4A5479394C49253217D4DE0A93884A1E47860B127B7DCEA8A0B17019AB410A0737BD81C6EE6EC6DA84569DA3A8ED34ABBC6588325007C4F5651814C45F9DB9AB460AF28B68671E6075AA8AC5F9B2166B79B81154274F058E38B24309017D2B098532964A3DC453F133194D1160A356098321C89033BD195848FF1C989FBCB92192A9E54C528F5259DA796DAF18831A040D8E8C999857FF5251A2A1510929940F1836216566F443AA903BC90528A9E7D842DE70BB75CC8939E48044BD67A4287CEABE22C3481B1F4775260E65A9DB00BF5218136143FC2E21CA720B11540A83DB93C16F10A12E7BAD3A27DCD807E15678687D325CB23256FCAB5073690E3B645329C87379C7B1DD7375755B2E9355D38A9870911B10BD5680FFA08C1442E412A6ACE489E4C89BC7923053748132526C541A0EC4C2A84B606573C36E027356F49F0978B029037455A22E3FA2390\",\n          \"dk\": \"30015BCADC63AF4C80E5F94529725041C8C0746963B3D1A5F655CBF9A8BBCB8B983DBA06CDD1256C1147B0ECCC4D03B600744139D84E6664B40AC22BFB1B03CD61908724078A8B405D868A462B7272594FC448C3EA5908A824091653CD71EC858291B26AB60ABB403884EAA6909A3AF7FB0519384D91856ECCF48C1843C50F1130B1A66DD38355DDE0533EF08484F4536968691BB1900CB52A80039F6CD201FD6C9AA45B0D1830576289C1D3B233BB11AC2D153F6E42A7B5A5AB4B204EEC2707AFB266BFE93C17B6AE2183513E38AAF7F6C0D29B347E6AAA31D24C2804606608A078A25CFE607846103744135E44872C0C410028908E2A608601730DE42B01D45400271033CF90691340ADF473C487DA2BA2E6B7BD2CC6B6EB0D9F01CD7031015F1015C6A24E0C19853E749E2F16C61C2809FF1AC00D113465868E56A919A8310FF6D43E3511322D293F9DBC5F965458A184744F3970B0A16359172B16B7C8CA43B311F4AE96380DA46292A3D1404604054069A181B1C202A02B9AB34DCE833BE08C2EB0DAC641C024C416BA0DB34E10546682F02B6B6B630B801CF8B2CC4F98C671307B92407BDFDCA4CD41B30B4AC8EA3811ED9665FAF479D1057B313CB113941155E539D4C61011360BEAB816CAC019051356533991343434E5C28AEEA797FAF85668B824F3F8BB0329CFC7ACA9992C3990C56CCE31A53F40CB2D07940093504E8A7CF091B137308E1D30B3B40A3624776F9955B62F3828DE769EDE44B89DC828473213AD08488C73A9C9FB2813C6750CE148E37AB3BEB0B60D2203BB444DE44ACCEF685047D9C325478F7CF3A72C9A6F1040A898B1A71EDA1B5F87B57FA08F2F75C4ED616C02512F205159F1881BC559C93925A639F05639589FFA2C38E3062A4FE091A507267916495B033FC0901A47C645A8D82425216EC251C8C0D37A4BFA44C0435E23D090D9D10528A03E611628C5654C5AA1C42280A3C54CB73CB26A5A68CC5D13496093B14B470E3A57038C30CA15712EECA379978521EC298E7BFA6AA698A04925CE47413A62283ECE3C79D955CC3E9C69F783C9B2D58438565A99263CC303C92A0BC6810487CA1664E5405EEE5987A621C33E30483D4C7862F9760399B4C23822507810AA321D04009CFEB2505860BCA818BC0CFB9DF44A5F47D23AE80C25C2A67455B64AF2D276606A208C74867EDB5373BCB93B06AE81DC67FB73514FAB8E62516D0A0760FA0228E6BB0A252120FC7676A0340FC2939CE8118782631B91F962CB14200C928091876998B1B19EBCCD59F92815930DC6F9BE629099B3334772AB3A16D630D971B2093C136F29C2EEB09F03F8592FA4980599542E422FF636C8BDD6672454238D4C547FCB43DE1B10FB385CFAFB1F84D95E6C776973867E2E40615531A3126B076E9B30D3E9BDD2E12C34A9764E90000AD179CB159259202800299B2902606603227BB37A9B679320B60B3EB9A917D2C0F0CA6A3248B1F371801661A58FEC24B8807B06A5B67D087CC6E3008D823024575C5D7678C93973E8201101765DD5A10889091F8AF87512C206C2C60A96081FCFE32C3B7C18A643AD56515674A3B745D154F7076B65E191D5026301AB45076B00288B91DA147F02795A0D0AC9E8C61540405F44C8B7C766BB454525B8D48B9418B34E3135B8BA3AC9E2486B162C23B266856BC370B4833028C0F753C1E6490A9608B1C5169E6EF0B43BF6C5CE0A6122CCB28C9A96F794B4A5479394C49253217D4DE0A93884A1E47860B127B7DCEA8A0B17019AB410A0737BD81C6EE6EC6DA84569DA3A8ED34ABBC6588325007C4F5651814C45F9DB9AB460AF28B68671E6075AA8AC5F9B2166B79B81154274F058E38B24309017D2B098532964A3DC453F133194D1160A356098321C89033BD195848FF1C989FBCB92192A9E54C528F5259DA796DAF18831A040D8E8C999857FF5251A2A1510929940F1836216566F443AA903BC90528A9E7D842DE70BB75CC8939E48044BD67A4287CEABE22C3481B1F4775260E65A9DB00BF5218136143FC2E21CA720B11540A83DB93C16F10A12E7BAD3A27DCD807E15678687D325CB23256FCAB5073690E3B645329C87379C7B1DD7375755B2E9355D38A9870911B10BD5680FFA08C1442E412A6ACE489E4C89BC7923053748132526C541A0EC4C2A84B606573C36E027356F49F0978B029037455A22E3FA23904DEBEEC7CC5EFAB5DAD8559722613802856CD726336A26B171DEC76493C71460B63478F2FC887334C707E9D836E3104892566B3568CD32B583F8C9A0DE1A1F0C\"\n        },\n        {\n          \"tcId\": 13,\n          \"ek\": \"37949792925BBDD82552AC59C024AE05B6C9FBAA558A9C4D1FD2A64EF27E2C077617C864F0FB704093B8ECA24CE0CBC9073125070C0CD24426CAF58F97B6549585C249E147E7440DA19B5D55C94EB99274B2D390D9BB6DEDC66545863AD1247013BA4021228AAB05060C9C6749599D8E74895E297C7F05ABB661B98D6003E6F064DF663BB5204D6E12A7ABF58ECF0BBF8C9B3379653E2AE073C3945E410BBF14FA6AF910B3562C7852923242B894E677777398413F511FE38B34E12C787AB75C7CB50C23E432A437B83B483236D7736BA515A3A76C956261B68274FF368CE9B04F1BA3C93E51A6A220744E370620C17B8A851B8A3B01A7017C6765150959AE4C567282D3C087FC6150279A8A3873A4A9231B5C4EFB998DF0A62B808A934F2701CFB2289DA578F1621151C136D79509EF3B5D9C16706CC3BAB798B1777A6A9C9A53F1B277D50CB7A40148485AC1550417AD735899532AB03AC8DACC790987A76AB988CADA1B1FE02400ABA70DA5C9C991725744740FCA0D5EE37486645E4B6047DA0C862A26A89268CB8B9781301AB43ED88FB5403BDA3A63882AA6780A6075474486643896B285F0E2272E261A54B09F0931099323781E797DD7173AFE84569F05BD12134C39B631A13812F2958B872950E25AA07CF2BC5CC90E930B71056B562F06AD2CA268CA82548BC6C7E079690B22B900E8C16CB9463C8B4FD731A74890C325B7CAD3871FD1F67DB78077A13A5E5F9720A4581DF75C4B9C15050D9C4923D7AC7669BE31C523D2C377F1917550862E8E5C39253A5317DCB16529528DD9B1993006DC796668B892DEBB8AE9E8A7E28877403872EF4BAC5367CB09795BD209ADDC54A329CB7549959F286A1237287756AC9943D2B5D21A936D1C7C2CC784E69661D76317ED0241B9C978C02A6E7DF86667506129697936E557251AABDF769CBAE5614BB2BD354110ADFCC8770CBC72738C13064DD14BA724B6755FBC176683B2B5283F6B99B60919A70A2C49CF29378847BD8AE1C502801334F8360369462DD97AD9E0836BEB57B98815D43734CA4C95247CAC55F38786A7747AFB8D8E678465921D7BF29D607FEDF94631E9377AFD92D51FF18D4F91280783577370AC011B\",\n          \"dk\": \"F6FA89BA9A8A3752C28830B89884388B95A2CB3624DF530E143346BBA491DC7082D5144BF36C9B2B828D1D516F9E8165826C1D9CD081F301224F8409FDE22C54F72AD6319033524F0331248043304CF53D44F31C24914689EA9E04A204C96561678A73ECF40E8938ADB137B9F3970464F72C4C115314304DC317ACEB5C2733661F49F1038141BF82174F3634AD82E08E69117A27B4292937482466CBB3041223B51E2BE32C8F7348E3B427F2C77A94D475FADB68C5613312B92A38A86DD694735EABB42ED21A90BC7FB0A5C90049BBC42926F5FC5A0F563E702266BFD44479D8B1C620C670E6AB0E7551619B8661F7BF33B97D12C20414C35866B007E34523184B967B9C6B4AA5AD7DD3CAB39497377C2F468538D52463A3E9945ED90F24FB964C85C47B202CA5EC533CDB286206A14B681A9F145B2BBB3F8CA65000B92CFFA80693EBBA5521C624D1B84906A8C6AC3C7881B10BC592B3B5549861B53BF1B6C1C19E70579EE366006D019E06492EF375BB1D8800B6C60EB7D3B5D78348EEE4B7AFA10E6786043B1C3A9BBCB3DE313189801BCEAC0F9B0C4E631366863297CE39C845835F5B25136BFC8C25C737054C184CE18074A1B079A00A6AF778EF8C3D45F5178A604CC19AC427118FEEF0014A047CC394589617040D96682BA919EDD61E0BC572A21CBE7D189F2AF69F02A6A313881D94A5259D8872A04854D11482635C9746CA083581680E5429BEC2C3A7164D39FB766D786F5FD54B9AFB3B90BB55459C2811860D7159BBAE38B67B4B18865B326CAB9AE62C9B1E636F0CE48260A4B90D6B1F4197371DC1661EA07E27993684CC9423FC5C15F8B1F34C0EDEB0B837A847B9ACBC53F4CF3E88766161C121595A0F940DF1E01C4CC3192F839BD7B660835C570C83577F3C9D88851EE3E9B74B00416F034DBDC2B7D081995854ACB5F4CFB55C9605E6891A8041CBB6101FA94B52F7902CFBB256B28648C9585E8B564E7322911B9A9030013157501D1991210B754983777FF99B42CCC87FE5A847E8A084662DC1D3058C6999EB6C7935B4470A25B4720925ACB046773B16C1E70E2AA5BD37949792925BBDD82552AC59C024AE05B6C9FBAA558A9C4D1FD2A64EF27E2C077617C864F0FB704093B8ECA24CE0CBC9073125070C0CD24426CAF58F97B6549585C249E147E7440DA19B5D55C94EB99274B2D390D9BB6DEDC66545863AD1247013BA4021228AAB05060C9C6749599D8E74895E297C7F05ABB661B98D6003E6F064DF663BB5204D6E12A7ABF58ECF0BBF8C9B3379653E2AE073C3945E410BBF14FA6AF910B3562C7852923242B894E677777398413F511FE38B34E12C787AB75C7CB50C23E432A437B83B483236D7736BA515A3A76C956261B68274FF368CE9B04F1BA3C93E51A6A220744E370620C17B8A851B8A3B01A7017C6765150959AE4C567282D3C087FC6150279A8A3873A4A9231B5C4EFB998DF0A62B808A934F2701CFB2289DA578F1621151C136D79509EF3B5D9C16706CC3BAB798B1777A6A9C9A53F1B277D50CB7A40148485AC1550417AD735899532AB03AC8DACC790987A76AB988CADA1B1FE02400ABA70DA5C9C991725744740FCA0D5EE37486645E4B6047DA0C862A26A89268CB8B9781301AB43ED88FB5403BDA3A63882AA6780A6075474486643896B285F0E2272E261A54B09F0931099323781E797DD7173AFE84569F05BD12134C39B631A13812F2958B872950E25AA07CF2BC5CC90E930B71056B562F06AD2CA268CA82548BC6C7E079690B22B900E8C16CB9463C8B4FD731A74890C325B7CAD3871FD1F67DB78077A13A5E5F9720A4581DF75C4B9C15050D9C4923D7AC7669BE31C523D2C377F1917550862E8E5C39253A5317DCB16529528DD9B1993006DC796668B892DEBB8AE9E8A7E28877403872EF4BAC5367CB09795BD209ADDC54A329CB7549959F286A1237287756AC9943D2B5D21A936D1C7C2CC784E69661D76317ED0241B9C978C02A6E7DF86667506129697936E557251AABDF769CBAE5614BB2BD354110ADFCC8770CBC72738C13064DD14BA724B6755FBC176683B2B5283F6B99B60919A70A2C49CF29378847BD8AE1C502801334F8360369462DD97AD9E0836BEB57B98815D43734CA4C95247CAC55F38786A7747AFB8D8E678465921D7BF29D607FEDF94631E9377AFD92D51FF18D4F91280783577370AC011BBEDEE8ABCAA18E82777BD6D7F6E49B9CED29D3E3687A5FE2B528C6AB45700A2F4EA6EC5384C51903758B807395181F6D6B4CCA3FA1CA24110B08A8AB1742C411\"\n        },\n        {\n          \"tcId\": 14,\n          \"ek\": \"32CBA337E8CA7A629B588B26218CAC94C03A73E67F6C2037B80607AD6907E2B7468774A836A270CBB03075173D97D837DF5C9E1A3889E26992900902F15A131DE49E6E36B212700C91715AB5AB568C50AA7851A83C353D60BCA348A76D26A62126B9152301A9164C3A01F05907E3283512BC72C2B39E4A7BAE0AB3963A129D2AA39A091F3534110243BCD036A0AD4A3BE03A1A7D3272D1F0ABE2E7455E82BDADBC8B25057667E79EEA898731D85B42381692419E3C101DFBF0132CE91448718B3E5C3F04274251C0854B8C3B3352B77F082DD8C10FEC06A7EE09A8ACC8A5370186E2DA4FA0DAC06CCB09610B23D8E32A1E756E2CB6A8A2BA9265C04075397EFF66A21D2B2C2990603B6664A1283297730C6E1C024D42783D982B875BA957DA4C4F695B49370A4F407ED3059614147D522936DADA78B34C1749C6C04A099BD1C19E09A32F2CB89DFD24696B2C3E97BA38C1BBB198D81C13B73B007D53B5CC70E4C3A856E0ABEB44B5ABEA1E449C72FA106DDD57C3BDBC592D01538A05B32FC85DE8E4952C2B4A2CBCCCA0100E3007885EB68473B95C9406515568AA0B1844EE2376308C841F70605BEBA3E8901707F7A874F087FBAC9A917977CFD63BC16269AC7800DE605663C16D3741077A099DBE6809C6EB162EF0AE62302114D01172567A11EB3BA5B1B425507966639E9916886A8882038B6FEDCC44DC2519DF6C79D8CCA6E7821CA96655136102FDC394A0F13FDF283B43A6BB06E2528E825398303F5940A852396B40C0560D14BC66243FE6773588591907005C5B39CA56267F2DAA4CF9455ADECA0E1CBA99E5E3991251072426C0BDB024A3F8771A8C19684A8CCF42C39948A0E7D951E9A63C5994340E43AB6937513608125FCC86346308BC87ADB154499802878A42273CAC8CA77B14D156A22921CD2AE9AEC363CCBDF2008A28621461700D288C199A51F2A6118BF2921FDA1FB963B9C048B989DC282AA9BBC9D0615F66CE21123CE5465273786886DA0289C55856B08569BC2A880742383A4B07A28E7F6B326AEA9BB5FB5070196BED6698015C232ED5A60EF9900347864D57229DCB6A9AEDC2A45045F720191A1AFCECAADC8C1E65E470DE2C6360C098955016\",\n          \"dk\": \"58B5077542461D311F6B00A10EAC4432C70881109FF588BFCB3073CB15382BB886119B2FF390C4BFD329D18C74A524CBFDA709CDEA35CE2AB73B13B399A9327731A29ECCC0589C762D594D80E12F1A005698B406E2D025F9AC025C46658AF413A17053CDE4CA13D128CFF52922F046535816ECD8601401C4E5CB15854567A9A7B165B42D84424F8CABA817B6631F9B20BDF41A859C56B1D6991F21257BF606C37A2503A83BEED9AD5085B4A0B709719CAA57484B8C639E613B766BF65C2CD449F8332142E0C650A618EA165DD0B594471C3C1E80BB95F504F8D2764A7984830765E182011336B9D2A23B07E2BAD8CA964E4A56007460F59A41A42AC62C17234E0443CBA6CF27724A6E8B651BDA3B788C8D99981A8281789C478143923883261C976475BD5602B013933CBC89A9959A9C2984CB3728D3E29702B26C3E728EDFB4165B387952FC5FDBF70AA32204147C458F471375AA961C1773A8AC3746EC49B7A120AF022A22878958B98FB6E37639D582C07233964106B3F015E95CCAF8A621945B00B61059804C8D47A82FD014269E2C5068B3ABE6B310D9C63C7D37B778D07DEB47B1178BA19C5B59C3E86796F2CF939341E1226C4A5537076085CD9B66CD731125CCB20E4249CD655E547748A8000392BA37F7840E515863E54575167A98B4BB81CAB934CCD53AF976CAB207868B18AB7530AF2D1908ADB85D000D32AFA706F81B90178001F2C57449402C64353F0FE33FE8139153FB3EC3577522373A0B092C55CA22EEA64FA886CF53E92B571C04465A7DF9909FCFC20463258957023097845C88A0C09CFB96AA960692F546B8A1893CE5846AD3CA83C98DB7650A42E56AEDC197ABB09579F6247F79257E71CBE27BA494F668EA60807DC61DCBCA1D1531261D776BD41901156B82C40BC5852517EA130D52D02132D151D28670F14494A207193E668613C58E0E8A2F5124B47EB5CFB2276FA75B27DF08359C5920FA9757A8AB32D40631F593C26E63ADD40B3DF6B849BC8AA8BCF7AC1530C73A802F2FF2CE94CC4EF26158A1F66C37EC4A947568CB68442A42C6E5BB358643BD90014232CBA337E8CA7A629B588B26218CAC94C03A73E67F6C2037B80607AD6907E2B7468774A836A270CBB03075173D97D837DF5C9E1A3889E26992900902F15A131DE49E6E36B212700C91715AB5AB568C50AA7851A83C353D60BCA348A76D26A62126B9152301A9164C3A01F05907E3283512BC72C2B39E4A7BAE0AB3963A129D2AA39A091F3534110243BCD036A0AD4A3BE03A1A7D3272D1F0ABE2E7455E82BDADBC8B25057667E79EEA898731D85B42381692419E3C101DFBF0132CE91448718B3E5C3F04274251C0854B8C3B3352B77F082DD8C10FEC06A7EE09A8ACC8A5370186E2DA4FA0DAC06CCB09610B23D8E32A1E756E2CB6A8A2BA9265C04075397EFF66A21D2B2C2990603B6664A1283297730C6E1C024D42783D982B875BA957DA4C4F695B49370A4F407ED3059614147D522936DADA78B34C1749C6C04A099BD1C19E09A32F2CB89DFD24696B2C3E97BA38C1BBB198D81C13B73B007D53B5CC70E4C3A856E0ABEB44B5ABEA1E449C72FA106DDD57C3BDBC592D01538A05B32FC85DE8E4952C2B4A2CBCCCA0100E3007885EB68473B95C9406515568AA0B1844EE2376308C841F70605BEBA3E8901707F7A874F087FBAC9A917977CFD63BC16269AC7800DE605663C16D3741077A099DBE6809C6EB162EF0AE62302114D01172567A11EB3BA5B1B425507966639E9916886A8882038B6FEDCC44DC2519DF6C79D8CCA6E7821CA96655136102FDC394A0F13FDF283B43A6BB06E2528E825398303F5940A852396B40C0560D14BC66243FE6773588591907005C5B39CA56267F2DAA4CF9455ADECA0E1CBA99E5E3991251072426C0BDB024A3F8771A8C19684A8CCF42C39948A0E7D951E9A63C5994340E43AB6937513608125FCC86346308BC87ADB154499802878A42273CAC8CA77B14D156A22921CD2AE9AEC363CCBDF2008A28621461700D288C199A51F2A6118BF2921FDA1FB963B9C048B989DC282AA9BBC9D0615F66CE21123CE5465273786886DA0289C55856B08569BC2A880742383A4B07A28E7F6B326AEA9BB5FB5070196BED6698015C232ED5A60EF9900347864D57229DCB6A9AEDC2A45045F720191A1AFCECAADC8C1E65E470DE2C6360C09895501683E3ACD9B35DCF6D9E93B006201B0FE23745F0E2E2CD1793BD7B3F6B84220BBC9FA6AA53F505506BE269CE201A1A6EF95692DD1350A7188F468D34C5DAE5EAD7\"\n        },\n        {\n          \"tcId\": 15,\n          \"ek\": \"230563C475571E5CA17BF116280196E0AB1ADF4CC7F4909859177190D17E6A7390DB291CDAAC33A0A08F6FD11608DC404A43AF7E467AD3E77F6004192A758409C459F4CBCCD5AA4DB3D47891416EDC659F8628B20A656CEEA904893B84EE9B6D44B4B1E3D2A5DAE49D7A38882D6C01A07720565A878E3A3B715B27DE84B14721885D5A8D06CBC93268A35A8A3360BB10530130A1C8664117A83BD550D8CA2846250D0FF722931ACF79824A537915D21C9892A786681B44997CBD17F7C02498AB3EE4C6204C79E848BBFDC06C0112C3BD622B9FC4964F64B539630F943583E962B765816E111C850AFA6073C1BB1EFA6E450B9B518C9EB98144F5912F75B30238297F0A934116D63BC9007DE2F21D192A4A515504BD7C73E58462DB50BC1A373922EA1AD5CC4C5FCC64030B4863B047D37A0CBC2AC48484CA642615F287610DDB718BC562559992206191D1507B3BB94B6714BB062468C13921692ABD9EB93A8D3CA09C8A52FCFA36D0F9711E545028BC737DB2B3ACF9BC850A48491488DE346DED5C47DFC089EF074F6C38B3BCA7C42746AED58660C5C984D8E59F3D28276D45263D667191A25BF35A3469B55E36A449DDE713F648A18469A8E417BFDE0784E2CA68589A50FFD38EA5744116F951BF79B90B1380717C6E4C8940627C5BD4F395EF0B38BEA7C556A14C03F2A3C5A2A039BA8CB0BBAB2C0C2B4C88A5D76849F657C361212E18CC96E39788B1D086ED61440F2B10266225C48A36E0380D783677E64564BF39BF8FC8AF7C581FCE252FFF73858533B24F2610A78A2D83D7625FC1687D0CB87088B8F53B475A7A2E8E1720E6E97C1471712AD948C8A2537DDC06BEA22BC6CA8575A26D4315180FB17ABC797DD135545AE953925795FD12167796A54A9793DE8621B5E6851180CF681583AB7A33C4584B709836D79A1913E6197FC4A66876C638B12C77BC9C8BB95FE2A251D61BC260016287DBCCE42C0FB6F30B1E41440957AF90318A48F61AF588C8A86C4E6DF854808C88F2343EB075BD4BF89F7CC081F76B2FC1D410475C27244C8377852B60204636AA7619FB3D7196CCD0B211AAE40E5BE5AAF9651F99A040D32D1335AC61B1685014B4839D3BFEE2660706F9\",\n          \"dk\": \"206C8727F6315309314A290D77492763F8391F365742B55AA2AAB82B98492E008869EBAC27545657E38FA68A37B97B1147111A21E10AB0D522495B92F2530032D440D0C966C5D56762C841D4E68556748D15509F25142E090473D48312B453750C155B4E763B23D515CB32487BF1922E7747DA07A6F3613A50630ADBF0A9CF7A9CF2F11D2C2A244FAC0E4878A6EE24609BE2098A047A0B45173AE5788BD831A5C6C237B601371CC0594CA08E8C1BC5A040ABEAB693ACAE59B058F59B633612081B170E334432C44C80F4EA4758C75701E22A66963425840752D49E92821356149D1328C275669A3B319EE92A2B32A87D313C3F20A8A2C56021503B0FB90B599F775FD008C182F54BE04A63236B459186586804CFF9F479CD531386EB57B6133BF3545076EBCA22BC3002F62B247147C1057BCAAC43B559B6F05842E2687ACA49CF2D0C855E5051C86A74567503D975C342C2AFAC165857C90BA5472C517226A4297FBB62ACD11458788736D592475ED00B2F5C41F2C5B90D6762B064BA0DFCC40E5CA433B2BE5F6B69AD364A63999E4067944F12AA7D2203167318EF6B5220A10504778987F59A6A8A9B2B702799E2A03DEA73A66B7AC776364D2A487E043030F8A476416BA05C62E918AA8670A39E769CEF5993CC34A2A28533AA79635988278A2B2F5E787A63502F41EC81FF660E96B9801E5566F80868C843B992B86976671C9BE56B1A146F83163250558F2B489E065B11AE976872D3B4B0897A9495527B3CCDA0A9A83784CA8E764943AA1F8497451AF259B61B306EBA0995B29C246812F090481F837FDF6663FB277E384250DED44B6297A46BB58C0628A48FE92EF5802BF14666E4282101D3CCBDE76675C6B99B7125A5A262374165F3BC7918F06CBD651DC302C793820281A0B83657A836365E9C982FF973C83FB6A57754967F7112061B9198A6B749064F7FB247FF262D7B234DE684319BA5A4AC0831078116AC70BB643081E9D9AB63D06061852CC3607931184CFD7A904238CC8B894F22C2269A682360E3C844894B830611A3E896CE69B63B96365E4B32CC215A5645AB721A74230563C475571E5CA17BF116280196E0AB1ADF4CC7F4909859177190D17E6A7390DB291CDAAC33A0A08F6FD11608DC404A43AF7E467AD3E77F6004192A758409C459F4CBCCD5AA4DB3D47891416EDC659F8628B20A656CEEA904893B84EE9B6D44B4B1E3D2A5DAE49D7A38882D6C01A07720565A878E3A3B715B27DE84B14721885D5A8D06CBC93268A35A8A3360BB10530130A1C8664117A83BD550D8CA2846250D0FF722931ACF79824A537915D21C9892A786681B44997CBD17F7C02498AB3EE4C6204C79E848BBFDC06C0112C3BD622B9FC4964F64B539630F943583E962B765816E111C850AFA6073C1BB1EFA6E450B9B518C9EB98144F5912F75B30238297F0A934116D63BC9007DE2F21D192A4A515504BD7C73E58462DB50BC1A373922EA1AD5CC4C5FCC64030B4863B047D37A0CBC2AC48484CA642615F287610DDB718BC562559992206191D1507B3BB94B6714BB062468C13921692ABD9EB93A8D3CA09C8A52FCFA36D0F9711E545028BC737DB2B3ACF9BC850A48491488DE346DED5C47DFC089EF074F6C38B3BCA7C42746AED58660C5C984D8E59F3D28276D45263D667191A25BF35A3469B55E36A449DDE713F648A18469A8E417BFDE0784E2CA68589A50FFD38EA5744116F951BF79B90B1380717C6E4C8940627C5BD4F395EF0B38BEA7C556A14C03F2A3C5A2A039BA8CB0BBAB2C0C2B4C88A5D76849F657C361212E18CC96E39788B1D086ED61440F2B10266225C48A36E0380D783677E64564BF39BF8FC8AF7C581FCE252FFF73858533B24F2610A78A2D83D7625FC1687D0CB87088B8F53B475A7A2E8E1720E6E97C1471712AD948C8A2537DDC06BEA22BC6CA8575A26D4315180FB17ABC797DD135545AE953925795FD12167796A54A9793DE8621B5E6851180CF681583AB7A33C4584B709836D79A1913E6197FC4A66876C638B12C77BC9C8BB95FE2A251D61BC260016287DBCCE42C0FB6F30B1E41440957AF90318A48F61AF588C8A86C4E6DF854808C88F2343EB075BD4BF89F7CC081F76B2FC1D410475C27244C8377852B60204636AA7619FB3D7196CCD0B211AAE40E5BE5AAF9651F99A040D32D1335AC61B1685014B4839D3BFEE2660706F970C2B01D9EDB8083BFB5CAC9AFC6E0A6D63D33F40E61F6A8055B63E799623A39A9EE7619E4F0250147ADC188649A45EB6D82DE5EACD5643CDC52E6DF8DF2F8EB\"\n        },\n        {\n          \"tcId\": 16,\n          \"ek\": \"8ED594BFE63B64E0C4C5A27BAF4C198E11AB91629C1C77788A05977042B4C452CDB7D66F0991BF05480763591226BCB1830B8527E5BEACC15A191ACCA3947DA6C015DA1B1800FB7B98E94BC3A61733669C6ACB8E6EDA749F879F565077FB637FC8A6892165AF17A77DB17856FA9266EE017105449C514CC6780B41A867757AD551A33341AA145085687E2C9C0CB51224A3C164BE9A36024AA8CFE97BD6C01F8F06A51F36964F1C6367D6A4C22C6B759756ADB3C558E49FE45395D7E8BF73C24305632A6573A8BB574EB6072441D66EDCE17A5B080D6F97C3A7030583837395E7378901A80B1B2CCC2B89E4C9AF74EAABD5604F9957C08D18635D68AD566A56D0D773CD71ACCEE3C697C851544B96FA4CB18A42183E2C0071A46F01F47E1455133DA73A0599AD2EEA39EBF9216DDA7012533CC4610C035C9D00E2962FA6ADFC065BDFA09AFD7A981CA0C3A010CE7040CB6A838B9C65A2E1580FFDD36911F23D69D25F211C0811215C96DA9E0B1C9C2DF24CF583633ADA8D786B6591D4A306D874A6E8A8DBD09443578D128648A9163E8734057D4449E1917B73ACCF7F87CD85DC0CA473756D42A174A8A6CF1A8A5A452F0FE17034E216A03224A423ACA52BB2EC7A6BBCF06E5616A4154683FF6CA625AA78ADD4CEEAF8BB48D37D36B79BBDC3650DA67982F6C4B2DCC00973525B4A3F06CB56B2A35F68974DCF508E6D093BBBA7780B59AAADDABFEF569A480903B648B41D4929C8A5110D6625DBB5B206CA384FBA479898B840D50E45D5CAA30C3EF143593C5BB75224629DD906AA754394F8549E4C7E5EAC45F2C216225C487CF247BAD2C5397B4A42D9B128054A80D24D4D9068373462547799EF612CF61906E224C03693B4CA28983F302B0E935EE11882BD8A8596533E004C8F59380412299A97BA5727A02CE3E410DA564BBD713BE085BDA1192EB27314136754513008A99CC3A2949FBD664C92920807860C1AB1814E1428642151CFAB3C8256832541A957F5020F89230C5C162A03437A917A7B96791F4B1401E79CB55375F585C9C4D166D9760CC36B802E6B5079B0836CEA1078B3BD7E5D33AB48E02C5BE40EFF344F369B920AC3F396FE7F940C6218CBE3B81D3904\",\n          \"dk\": \"207B287A81A1C2223C1BB9B373614F93B575DCA90BE6567EEF7B535C1684A6D0CA4B36420E989A830C9CDA69B8A7655D28F18325741A8CD874A88686A802C26DD6B450966C1EB02D271B89FB528988D4C6ECFA95E7BACF0BF11AF655AA48717CD69497D611CDD2F3437E4C96342305F300953EB9881B4678E4317C400228D945A6924042FD5A86A566A49A577E90955EB6B068ED0568CD535DB1B957597B4B90D864BDB05EDE024EF7D4AF63D282C3D876A7F2BE1DCC0848353818F91ACB935C2CF76E652CB720DBACA86522FB4B0E276A5BE79740F2706DF0E0C64BA18C42E9A7A71092B880328F6866EC599DCF56A4254578CF219D87DBC2AD78186FC3865366415B7932E4B28C70C30D7256BEEA676300700336A814D122ADE949BF6872AE7762CCAD2C7701557139CB5D403215A52327048789CC86A5E689B609260425E7291AA15B7C30460B1B04C1E70647741739F03575E348DDF4996DB587DEFB61940C61A6C1CE6A118C46A701D3F345C1747A666283871B2BB00661AD995B26112DC1B48EDC25A1899019213A3555AB4FE17AACDBE2B4E404A117B7B4A779134090342250613CE2B632A8083D1B56B0501D026C309D25A711AACCD82806DB867A6CDC85F03BB667B2354AC4820ED4A823A0BB93C3921706259F55336D7443FA26BCA90C94D94514C38B17D0DB2909CA4C1E3568CEA34266DB9D5C6CBEC1E104B60BCB3C77A6E3B45FAD814F3773699DD86BAAEA5A6CF6CBF856A5EF6B7F21A74B61A49956935895AA7368640266291088D7BC64E70F7462343F2B943C75B68069435E53684CB74BB3EC3ABB0C1CF17621BD811D95F488C4E035B0A2C2B00C7C6F4757F5711DBD285ABF018E28E87FC95212E7A8CF91BB43C95A69C86B42ADE439189242A4F43252B0950BC43FEFE39BA66334FA1A0114E822A97B6E81356B5C7A7393F9206A38B308CBCC0EC49F079979E0739196020FD6FB07D222B8F9D7C36E5241E973BC37B58C180473E122A4A4554391A3BFAA8658A74940BB856878D39598A1C7DC485971801226F64655E310DF8C689C492A3FFC509840143BB2681C92028ED594BFE63B64E0C4C5A27BAF4C198E11AB91629C1C77788A05977042B4C452CDB7D66F0991BF05480763591226BCB1830B8527E5BEACC15A191ACCA3947DA6C015DA1B1800FB7B98E94BC3A61733669C6ACB8E6EDA749F879F565077FB637FC8A6892165AF17A77DB17856FA9266EE017105449C514CC6780B41A867757AD551A33341AA145085687E2C9C0CB51224A3C164BE9A36024AA8CFE97BD6C01F8F06A51F36964F1C6367D6A4C22C6B759756ADB3C558E49FE45395D7E8BF73C24305632A6573A8BB574EB6072441D66EDCE17A5B080D6F97C3A7030583837395E7378901A80B1B2CCC2B89E4C9AF74EAABD5604F9957C08D18635D68AD566A56D0D773CD71ACCEE3C697C851544B96FA4CB18A42183E2C0071A46F01F47E1455133DA73A0599AD2EEA39EBF9216DDA7012533CC4610C035C9D00E2962FA6ADFC065BDFA09AFD7A981CA0C3A010CE7040CB6A838B9C65A2E1580FFDD36911F23D69D25F211C0811215C96DA9E0B1C9C2DF24CF583633ADA8D786B6591D4A306D874A6E8A8DBD09443578D128648A9163E8734057D4449E1917B73ACCF7F87CD85DC0CA473756D42A174A8A6CF1A8A5A452F0FE17034E216A03224A423ACA52BB2EC7A6BBCF06E5616A4154683FF6CA625AA78ADD4CEEAF8BB48D37D36B79BBDC3650DA67982F6C4B2DCC00973525B4A3F06CB56B2A35F68974DCF508E6D093BBBA7780B59AAADDABFEF569A480903B648B41D4929C8A5110D6625DBB5B206CA384FBA479898B840D50E45D5CAA30C3EF143593C5BB75224629DD906AA754394F8549E4C7E5EAC45F2C216225C487CF247BAD2C5397B4A42D9B128054A80D24D4D9068373462547799EF612CF61906E224C03693B4CA28983F302B0E935EE11882BD8A8596533E004C8F59380412299A97BA5727A02CE3E410DA564BBD713BE085BDA1192EB27314136754513008A99CC3A2949FBD664C92920807860C1AB1814E1428642151CFAB3C8256832541A957F5020F89230C5C162A03437A917A7B96791F4B1401E79CB55375F585C9C4D166D9760CC36B802E6B5079B0836CEA1078B3BD7E5D33AB48E02C5BE40EFF344F369B920AC3F396FE7F940C6218CBE3B81D3904BFFB491B529857FB52940ADB7920E6785BF951468B8578AF5D830FC94BA1C12F80CE5D65D1795C90B637C10360B04A4C21A70851F0A59D4D753F54CC00103FF4\"\n        },\n        {\n          \"tcId\": 17,\n          \"ek\": \"28669B7A46A351A14CDD8C5676451577D43AAF842AEF96BDD6AB11A948832082CE85A21CBA773EB01508EE3B230F8A5D9FC6617E959FAB9171AB232BBB560D84579992E00E9C338E77043B89B1804C4021DE8124E61C6D6D72BDE0DA854C19CC69D88FFB43B1F983BAC8A84B65480175B8C74B6BCCA470482FD813FA7773570B52B0221B73F48635821427D3B6CEC089CC0C1ECB398BF4956DB4666555F90385A500F24868545848C96C5553FC7387671957745582D662FBBCA46AC9C3392B929BF634DF35638C8255B8492BDBD98FB43B207604919FE855CB48911BB3186690600D80B417FB54E7641560BA5800D46784F97C4961856EA25AB3F2A0608019B0234F41571618851D2D6831B28B37AE777AF3C79A8CD9509BB9348A30A37CD04083660047A0AF514B328B4B3835EC0C2E66C03CFBB05D7418AE4B5578A58BC051A616180F899B371DFC1C441362B6F8359A7B83754AA2B46884072977AE1651137875D4440F04F173405A4E60BA58BD950EA49255ADF53D16B2B97E0C0943FC2ECAF4CF01357382708BE946448AF8721B10AE95A4BFF876B4D6C4571187CA8DF9940AEA45E7C8C63E4081C2096BCEF1679AFCC79EA272C3BC35E9DA7171712A0C3BC679F5379B3512DBA54583A6978A2050AC679F60C295099C82564489E3ACC816BB2E5D775D9D3522AEBC21856AB947F1C497CB20C1CB3F91446AF20646920A34A72394442504A7C817887C177E75459A902323B87A3186CD5001C16CEB73B9BA6A5EF9AFFEA729423C261F121F8653384740A50551CD90292B0671A15599C9164A5054AA9382F55C3FA5A376C868C5498D29E00F0CD9ADADC07E62431D11C21B22374474D5BB513C20C020400B696132EAC94CCCB684937B6CDC507B9B06C3B7C21AC37C5F70C8EB024565D896854C9507082C96A8B7D260C7496AC7546C3EDDA17620DA177F931BD91692890570716455497C8114D8B1FC78506E137813F1067869AB940AB5BF452EA0C02F450972570C4F96F144CA368D77614BF42494D84B3EE6A9AA8A74CE8D0988D038CD971A8C5924BB25923FDE557CD0F336DBA90CB12C38A604FB1DA89F328816D27707C8971D43A9C34B11B683DA50A2B46D270D\",\n          \"dk\": \"FF6013CDBC6DFCF608A3178E7F251FF65867B72B2B4A87A0C0B5BA5B908B0CD878B76BA6F5DBCA047C40FA96479EE2BB2B97CCF6C894AA68505DA331F932BF05CB5F41D394F7875733261E68A02DBFD360AD29589AA06E2440165B16A46913AD6675B68D8B9A27661B709B687969BE0E53CBD567A84CA5A8DD84A2D522C6643A9CA5321987FA7EEB1C0C3742BBF43060DA234AC9F21881644E4E18C7D49A9501930ACAAC17CF536485805E928169584B8C013C30C924A5C81508A9928D54E1325A298115C76F9F95C154DA38B1C857E8676F7A18A35EF2690F743C4F49AEAB6A5E0D67AA18B908F4E0477C7399139490BE077D3B3946BB7C086B3C261323844DBACE09F008F0D3AB5996498E164466421294AC3852C3066FB7191EC33415868CE554C73E0316B1FA0C414405D4FC576BBB6D45FBC2A6BA1882C8575CAC70BD33472AA62BAAD07586D7631D73CBC9AA8875A8927092A0498483A48575E8538760D253A73A8B4905253285A07239B4CF6A091699274BAA5D692CB2614661104AA94AA38228583AE3326548151F39821AFA55752DAC4B25DC0530115027B3CAEAE44AC824ABEEF301BBBBB79113651C686067131A6D41031A34B4AAEB9B868C6CA1122B63363EF7C373064B62E894A654651F348C7055EC758C90642B793622B050D48BB5C241793AA701388C70B3AAA248B270B1E996DB4024D8FB56810C985038C4CE6647B497A233915924E3A9E47A382FE277AE063C02D0371EE964E16B817A6919B7DBAE053117E2012ED3D54FA9A216B0CC3812168A56B34FDFE107E2157E4DD55460C8753293CD614CC9EFA12CA0CCC712CB97703C60D6B763BF23C6CD71539023B1D2958098524A115B2B201B4A1CFC7B0CE3A667C9814AD7C90D24A2295076605687D7CCC8926A1F3D209F02D2486A9764F8D26CA7BBCBA8E53714B52AB87148E7E69A99348514192C8C360718438E2A70ADB107C7D005B1F9537D50D930574A5ED684AE5DB38E6876255D188AF85CCB4ED946FEA92B2E29C5F93083AEF30C6D37B39FD45120BC0FB39711A52A3C108105AF039530505D48069580650528669B7A46A351A14CDD8C5676451577D43AAF842AEF96BDD6AB11A948832082CE85A21CBA773EB01508EE3B230F8A5D9FC6617E959FAB9171AB232BBB560D84579992E00E9C338E77043B89B1804C4021DE8124E61C6D6D72BDE0DA854C19CC69D88FFB43B1F983BAC8A84B65480175B8C74B6BCCA470482FD813FA7773570B52B0221B73F48635821427D3B6CEC089CC0C1ECB398BF4956DB4666555F90385A500F24868545848C96C5553FC7387671957745582D662FBBCA46AC9C3392B929BF634DF35638C8255B8492BDBD98FB43B207604919FE855CB48911BB3186690600D80B417FB54E7641560BA5800D46784F97C4961856EA25AB3F2A0608019B0234F41571618851D2D6831B28B37AE777AF3C79A8CD9509BB9348A30A37CD04083660047A0AF514B328B4B3835EC0C2E66C03CFBB05D7418AE4B5578A58BC051A616180F899B371DFC1C441362B6F8359A7B83754AA2B46884072977AE1651137875D4440F04F173405A4E60BA58BD950EA49255ADF53D16B2B97E0C0943FC2ECAF4CF01357382708BE946448AF8721B10AE95A4BFF876B4D6C4571187CA8DF9940AEA45E7C8C63E4081C2096BCEF1679AFCC79EA272C3BC35E9DA7171712A0C3BC679F5379B3512DBA54583A6978A2050AC679F60C295099C82564489E3ACC816BB2E5D775D9D3522AEBC21856AB947F1C497CB20C1CB3F91446AF20646920A34A72394442504A7C817887C177E75459A902323B87A3186CD5001C16CEB73B9BA6A5EF9AFFEA729423C261F121F8653384740A50551CD90292B0671A15599C9164A5054AA9382F55C3FA5A376C868C5498D29E00F0CD9ADADC07E62431D11C21B22374474D5BB513C20C020400B696132EAC94CCCB684937B6CDC507B9B06C3B7C21AC37C5F70C8EB024565D896854C9507082C96A8B7D260C7496AC7546C3EDDA17620DA177F931BD91692890570716455497C8114D8B1FC78506E137813F1067869AB940AB5BF452EA0C02F450972570C4F96F144CA368D77614BF42494D84B3EE6A9AA8A74CE8D0988D038CD971A8C5924BB25923FDE557CD0F336DBA90CB12C38A604FB1DA89F328816D27707C8971D43A9C34B11B683DA50A2B46D270D9F2CB0E23865248EA1B9E200008F61F1D55C1D4C1F30686982C673E44312AE6CB923CFEEC804B8C6A9E36B77B38A2886C45B1C731A33528ED2CB5A1F65E792F6\"\n        },\n        {\n          \"tcId\": 18,\n          \"ek\": \"77531B993B3BFDA74DAA9964E4D8781244493F3133DED822179B5E12C252AA5655B7748D3A187F3B017E6E9C7B82A1A494CCA1AB590DFCECC946087293BC42DC7A48584808FF62A0E1CB46B04122144004507610FBE7A359E399B44A08E4452FEFD0BC6C906078210B84D981E936CD54F1BAE79A57B7F64E7B7C69585C783E9C4A1CC30F3F6302DB928C1AF4CC0E0ACC4E627F78D41329303265FB0FA565CF83D7BEDD6833873A9EF6A32F33575884C1639A054998DA1B1D93C6A25379A367C9A14C1F7F1C75258A2ECD071809953E9293BF8ADB2E5BB109A50B6C290211BE035F3B6B481A341CF9EA372A1626FD0B193BD42A7B622D8253BE1123B37B28B6AFC7A1937AAB444CAC44740FA112BB7C960BCFC0818689C7E2607080D24D53DB80B2811EA5E74287090604AAB587F392DA186F693B06F59B45256128C33A942E05779CC570143196E85A45032984C5A834FFA23F0C114BA96B0B0392959BD4CAA859B12377A905330B77C2BAFB4A754924157B6367910C91C3AA41EEA76EA9ECCD4B9667FC4839A8EB2076D935E86694B8D9A6C7A1558F70391067A0B26BB368AB571621957F097B3CD1870D02A5DD5A646774391CC48BFBC012C315B1F06A6474C279599490C76049028B7EE886C55D979C24FB03A28C8756ABBB9D9B7E3F281AB8107EB22104992240A9E92089361E65643CF270C222971D3FB385CC5594C41B35BD2A0FADB51923143762944219459A56726ACEE42C91D03DA6C49D1B46C5EAB710BA68540981C65B2B7638273C4341058E54CCDC8130B155CBA88C2A16A9B2DDC4C92CAC9AC9267151AC39379C0F2078958C33563E360B8118118B185F906CB438AC052D625D04A17DCFA7A5CE4200509784D679081E173ECC2B4278618363637555464876D47E3494CA408BA45507A492039ECE577A17725D6472ABAE1562F3721195D193C962B01CACBD80A85DCFDC221698B54B52874DEB8450A435DCF1204A9CB329E21374268046AB00CB4416BFE207F018390E37CDF8797DB1238CFDC2C0B4F495B015CEDB138C36D415DF9672AB506095DC6862D49D5541CD3D93748955E792F5729A3191AE5DCAC826DE5B8ACB573648D95D36C5BF399CB4D9C4F8\",\n          \"dk\": \"9904CDEB001524F59BA84901022C3B4D81C1A0BA1724B373F4A6869D0574199787069A1FEDF8AEAFD82AFB823F5F718490E1359F495390393A12B15257C2C9E56BC52691228E00C0311C9577FACAAC34B65BD5428CE226779C8208DB6B25F5BFF40CCC1DA60A4C6AAE8EE553EB64B235918C879B5690E4B421375A1A1350A960C643DB4B8AE405E58974878B9F6F87CBAB57B4A3B8B1BFDA9823797C239740588A854D9AAB4283400E8B93168A1D680C05B07231B8A455C39893C260B0182B66E13ACAAA20ADFBFC11CCE0CBBE29C1C7B79B6D78A931878C4604174BBBC28C0A54F8C50459F7A93CE97D393424A4B8C9728871E9741AB54129E5919742BC4CD95B1775499DEF0753839622677739E87A8A8C651062F7408B389EC818C15D300A7B95AEFBEABFF078BA0E27A842DC89FDDC5378A5CADE512B3CFC65D8472ACD311E8BAB4979C15A17F33125327C9C73AEEA3642AC225958778618D522472AA89853142D35AC93264CC423A1EC8CB460A398746CC9C1615326B5819B0697D1452BFE3A86F21B7B3D11B1F6284806B5A09FF362CC5B50E0A45EB84B084EE024B3A654AA5BB7EC34604BD6627FDCA48EFCB410B9769FB6BB3A94835B2A4850128CAB534E2B3A5A5B4B367DAC9E3BE913AA12C887589150FA64EC0C1293F8410499220DF0ACA7A52C2C66CAFE9567C3B0B32F593E7ADBAE1BE64197110E9D8B77864158AAF77AD1F8232212758A5C7BA401B32C258389347C37B0B3D8B78C30436D54B800FD88772E309621955C49B77A5F9452B4B74876DAC662F76B63760D52D20692647BD1707045A09DAE8324D9688CA56B034F0BB4E378288C328ACD742169B3704C02BAECB68AF8309DC2D30BC836A856635CA4701BFB74C5662C69B11B6BE0CC984F12493540CE50DB83D3D764A5E63A13C2AD3CF5C67E8638CEEC9193470C6AA069240A53DF6C68C439B3EED8C5F4DA7076306E5223B9104662DFB35276B4823531C848747AF50081BD56A6C4B154C39B2E0CD3131B4742D241A385D938EB2C673028636FD893BCF59519C2A87D4BC42862CBFA0370D8D44CF3E032178AA077531B993B3BFDA74DAA9964E4D8781244493F3133DED822179B5E12C252AA5655B7748D3A187F3B017E6E9C7B82A1A494CCA1AB590DFCECC946087293BC42DC7A48584808FF62A0E1CB46B04122144004507610FBE7A359E399B44A08E4452FEFD0BC6C906078210B84D981E936CD54F1BAE79A57B7F64E7B7C69585C783E9C4A1CC30F3F6302DB928C1AF4CC0E0ACC4E627F78D41329303265FB0FA565CF83D7BEDD6833873A9EF6A32F33575884C1639A054998DA1B1D93C6A25379A367C9A14C1F7F1C75258A2ECD071809953E9293BF8ADB2E5BB109A50B6C290211BE035F3B6B481A341CF9EA372A1626FD0B193BD42A7B622D8253BE1123B37B28B6AFC7A1937AAB444CAC44740FA112BB7C960BCFC0818689C7E2607080D24D53DB80B2811EA5E74287090604AAB587F392DA186F693B06F59B45256128C33A942E05779CC570143196E85A45032984C5A834FFA23F0C114BA96B0B0392959BD4CAA859B12377A905330B77C2BAFB4A754924157B6367910C91C3AA41EEA76EA9ECCD4B9667FC4839A8EB2076D935E86694B8D9A6C7A1558F70391067A0B26BB368AB571621957F097B3CD1870D02A5DD5A646774391CC48BFBC012C315B1F06A6474C279599490C76049028B7EE886C55D979C24FB03A28C8756ABBB9D9B7E3F281AB8107EB22104992240A9E92089361E65643CF270C222971D3FB385CC5594C41B35BD2A0FADB51923143762944219459A56726ACEE42C91D03DA6C49D1B46C5EAB710BA68540981C65B2B7638273C4341058E54CCDC8130B155CBA88C2A16A9B2DDC4C92CAC9AC9267151AC39379C0F2078958C33563E360B8118118B185F906CB438AC052D625D04A17DCFA7A5CE4200509784D679081E173ECC2B4278618363637555464876D47E3494CA408BA45507A492039ECE577A17725D6472ABAE1562F3721195D193C962B01CACBD80A85DCFDC221698B54B52874DEB8450A435DCF1204A9CB329E21374268046AB00CB4416BFE207F018390E37CDF8797DB1238CFDC2C0B4F495B015CEDB138C36D415DF9672AB506095DC6862D49D5541CD3D93748955E792F5729A3191AE5DCAC826DE5B8ACB573648D95D36C5BF399CB4D9C4F8956A0C263895B5FB51D08CE116A156237494A6C79B54BD23134DB26D3D3F0B9B1F4863F16E38DFD2C42A9322FA1ACB941DF3BDFA000A202AC621936FCC5FE33A\"\n        },\n        {\n          \"tcId\": 19,\n          \"ek\": \"DA73368E74CEA1A5B1B1AB47FBF38B33A47BA9F98F3D719F938082806CCB59503E1EA999C35272875B112BE8097414BD304C5C8AE127EDDAB111F6B5A8C5B8F8C39E8C41754BBA57683C2397B612A11747C508103C689ECB9394FA9B54F898678A3459FF6104B3350B8B548F064C47EE0702D6910213CA5A5909134D5B4176BC60D7533CD1BA294EB4751054AECFB73478EC38322A7D7DA829DF8A2F19C31EA9442E5AD30E00707747787F005B9CA9B9544FC61290262247B8AF26B490704B21676A138E11A797391ACFE7A6C9D0692ABA2FEB982262327749D55E34A6A4AFE3A061E3256383667035236422A38A371FA6D48120CB1C78E53607ABBECF319A46C0834D2A05B8E303CAF734A9370BD6855C7E676E637576D50B0DE8676D209305A11C5202288B9E80BD235CC543458FBB470702B4A2C4758EAA992320F08FDDE8B25AA4A08480BC401C534A6C4A79867A26B19297FBBB90739973A79E05998140587E26CB8C464A96A3DB1FC24C5ADCA02D076B352576B506C451CEA40E3EA4412DA2048B25093D80C39AC6300BD0B318A7BE644C5838A301ECEAAC5AD6364807255476AC75CB0C9ED033E5350900C0795584034DEA59D8844FB669B09EBC8AFA43C45135640316A5D86947B9CA445D432D02A624D0F46D396C5A09E3C43F0148ECA5A926147A200750FAFCC13D53CAFF188718883AA4B4BE97C72C1A6136CE266FF6451669BC78C862C883C86422366657097AAFD77F88693A9ED328CEC05D2E984D188A044E026C65E212B2307D838A549E623793865E2D13926F85128841BE2FBC666C6921212AC85D804D4655448A07CF7F41CB8A9623D4E48D72958004C1C28093AB060371642307F8F7859F1B08D0E45259021E2FEA5D4DD99E3CCB980DD51307776AFECCB7EAE1776FF09676F3697EB98DB283414D9872337565D0450FF3765DB8F5CD3DA30DEC806ACEC879ACF9995D789F03F182B8E2C314C92E6C36512FD223D6A99CA9657A04125CA5CCCDD0F967B1A767D37AB08F26759886C555E9713ECC22C5FB2663398592549D731C84693449AF0675767A72E9817E131209F3CC907B5337C46F460DA1941825DA2DEBE7A27ADDE90CF053AD520522866B24\",\n          \"dk\": \"6656048914A30A786B57E77371D5AC2F5327D5D26EC077B6D2D817FB1ACA449A34990B5DC22638BF6473560BB853212343203F60298E91C64454B87121147BF55742218327E0D84ED1C07FD7755AED894A3BF3393068B7E99009678BCBC6028B74F0BA5862B3AA5B704AA3A4EA658E40751F69B51171177620E2794A64864A2617B603BB6CF7068435CF38371F84977E9D507F298CBC6D717E4955B9FB5B307870BA8B09952DBB9724252A622A22F6C89956077099919BF2D7CE7D456AB3F70FAE9B68362593D7ACCA8C4955D072908C981DF377B58881A8F4046F342178812C99E2E86F9557A81CF632430333C5E961F0B167A1FC2FAA8B7648F342E1022FB8478661D7505503BD6D18275029C8588C8E675B80B6D722E707C8E9B28636B894AB8164CFFB3BDE769116078F83B97953DBA7C00378DBCB620E3C650AE72955E5434A2CAEE4B15DFBD937BBCC3D1D256ABB361B7E007C8C53429698BBCAF54B3283AE02461D17A2C41AFA21BA466B2247CBE01483A0C0743A32B854A9AC78175E6BE102A9C62CFAC5811FE527D0F433B8E708FA106DA30A5E0C9C0E63B6B3356557982B3B03B437CB412EAB525668A7723CBBAE801025D5AA7EF2D5A5AF5051B0B958FFE677DDC30E70D8507BB24D2600344D226CF602916AA175D4BAAF27D96696B4BEA828895AE8BABB449649798905575D71C60F3B9C0B73EB3AF92B98B90284421988F257510BC822DA501C6C8C54FE0117BE44AF4A12820AC9203C52466C76651580B962B2543BD0BA7EE73CE28B76411316FEC5859576CE5F83C8C3B159BD16CE58A92730F627E4B9770EC9BAA5D343FD630ADB9326DE8340BF5AC319DC43E5E03B50809876B6804582069258B9078267666416022382A1816DC6E670FAC16D18D861DAF022389762D915912EC7A7A3F0534F93B7D38CAD99670734883BD8D31E8B70BE90839963ECC83091086E689091CA6EF8477AB21960432593DD7694D0E898038BBBD603ACA19B8BB150B80960A63894A231ABB7826B1976FC331839586E3C816A1450742C719E674A2D1BC6B1B54D5F407D1E96849FB841703005DA73368E74CEA1A5B1B1AB47FBF38B33A47BA9F98F3D719F938082806CCB59503E1EA999C35272875B112BE8097414BD304C5C8AE127EDDAB111F6B5A8C5B8F8C39E8C41754BBA57683C2397B612A11747C508103C689ECB9394FA9B54F898678A3459FF6104B3350B8B548F064C47EE0702D6910213CA5A5909134D5B4176BC60D7533CD1BA294EB4751054AECFB73478EC38322A7D7DA829DF8A2F19C31EA9442E5AD30E00707747787F005B9CA9B9544FC61290262247B8AF26B490704B21676A138E11A797391ACFE7A6C9D0692ABA2FEB982262327749D55E34A6A4AFE3A061E3256383667035236422A38A371FA6D48120CB1C78E53607ABBECF319A46C0834D2A05B8E303CAF734A9370BD6855C7E676E637576D50B0DE8676D209305A11C5202288B9E80BD235CC543458FBB470702B4A2C4758EAA992320F08FDDE8B25AA4A08480BC401C534A6C4A79867A26B19297FBBB90739973A79E05998140587E26CB8C464A96A3DB1FC24C5ADCA02D076B352576B506C451CEA40E3EA4412DA2048B25093D80C39AC6300BD0B318A7BE644C5838A301ECEAAC5AD6364807255476AC75CB0C9ED033E5350900C0795584034DEA59D8844FB669B09EBC8AFA43C45135640316A5D86947B9CA445D432D02A624D0F46D396C5A09E3C43F0148ECA5A926147A200750FAFCC13D53CAFF188718883AA4B4BE97C72C1A6136CE266FF6451669BC78C862C883C86422366657097AAFD77F88693A9ED328CEC05D2E984D188A044E026C65E212B2307D838A549E623793865E2D13926F85128841BE2FBC666C6921212AC85D804D4655448A07CF7F41CB8A9623D4E48D72958004C1C28093AB060371642307F8F7859F1B08D0E45259021E2FEA5D4DD99E3CCB980DD51307776AFECCB7EAE1776FF09676F3697EB98DB283414D9872337565D0450FF3765DB8F5CD3DA30DEC806ACEC879ACF9995D789F03F182B8E2C314C92E6C36512FD223D6A99CA9657A04125CA5CCCDD0F967B1A767D37AB08F26759886C555E9713ECC22C5FB2663398592549D731C84693449AF0675767A72E9817E131209F3CC907B5337C46F460DA1941825DA2DEBE7A27ADDE90CF053AD520522866B24F8568A428B981C5C2DCD0C7E8B487824825DC3F9C0356ADF0CE075394DD1BDC553F5EE39A553E831BE32EB490A6E1DE62FD4FE486EF58A4B99F6347759BB8905\"\n        },\n        {\n          \"tcId\": 20,\n          \"ek\": \"F7F7C6DCFA28B52749357994F5C18DF3138166373F1D9C613410287E1BADEC6C23120889829C251831507BE05E137202A9336EF5C69D33823B69F91EC25369FE3C8FA8F2A244A1ACBAF5C851418515548920333F0B584F60C22063D66AAF545328F1CFA23BA676140D0E7BA6D6D94D4409AEBF5B209524A68645277111BAC6A2B09A13944D9471E9F704441215D6B93D63FA01E3BAA4BEF7C701E485A1A89CC8E8602C5259CF3A176EBB85487C3BEAB80B4E5B8AE1B54335BC5BD1E94D35090CDE556E2559A27F770053541090CB94BFE3CDECB1B99C0B9914460631061060927507096A16B37DB0091616BC00CF68461C3A47BB585E8997C6A46C09B07C98ECCB10E30C45D014A1B9C2559EB190651296E97C7DE0BBC475BC49AB13398CD98C91A06273654977AC3B70EA83BD5657F47381D9090909735EB899BE2DA5BC73B56232736AA326845FF5AE6C452523406901781E095244C8F689B0271D53327659D0A625D75914E34394149C955935ED88C1B41475671338451B61234549C74091C9CB14E90B02356BA21CD4C960522190CC68C71AA088FB2EA5D1A777075DB4210A289B208BE66F6BFB6DA19540B740A34E822232A4BBFD028BCC304976A4C2ACD924153A974FA345959AC74D13C6C287265FFA6F8639BB0E407C1380627D74BA3ADCC81176A85503213109493A2C745D5A65EAD31333537C51CA45E8A34A3D555C19649726C6675B645519645E6331A81EE47C6EA35B49283AD8E48FAD465A426A3352C0001E8BAE99607A26A71126355A0127C288266951E845CBC0942023950CC73677228059321C2BC9414A190EDB61021BE21A4CC26CE4E1303C430BBE607B996AB304519A4148BCBD72B0B23BB947A36DA2C983898A71A8D18F8CD571ED274EE76713C540118B0CB45CA6BC72C5C96FF1B231823ABBF112FD2683F12973DCCA995B007DEC83B600999535590AA98A61C711A2DF076E65803746E93885621557821F49E3497EFB4A0F8C34ADB34B42CC0F32B09D898903F946A569C6CEAE0630E13004BC75241343061D2BA6B1D7BF44A00712941C10E64183E5A007708F798456C695B15F75C3C6A27310393A3049089ED8EF92DDB01CE5F59229FF7CC3\",\n          \"dk\": \"B0A66733CA122538CA1C747B2DB0CD8E3A67D1F3B1F695BC40540BC5BBAB6CB1664697B5DA7BB181DBC1546A2F5F4C4958D8C73833372F0355BED21262146574E24BD42B3FDCD45B333750B0AA8A29E34FB9AA76BDD2B1CB6799539463026029EC43655B716351288C95D1B81DC572A8980CC1EB42914B0B7E957FDC8373533201317A54D2A497A4D85E3F695F579942750AC04A9A7D068C4429FBAE653AB8AED01227C9892D79CFE300191BD310BC704BAAF78F982BC09F40325D869C962AB618F25A18477B46A9A42657C00DEC939A5187C26342C552880DEAC83A5338D3B10EF4C71D54DB77A8EB01EA87A534ACA53C70B25C98988AC4828A0B49B3627F7B6156F665133AB01F42B47E298B983939857849409BB76C4B0856A364C86FF6B32AD81F28DB51740048853836ED972B92295E16FC51A15465FE592090874FEB183C28090465204490F561A388430AAC26C8B133EABA1FADD91C9832C43EDB9A9B8195490B4153D1CD3B34523CD98F570B33502C818C765188B40E43A8B26D120E4A67ABD430650F7978FD3551620A782615214085C28B44B4986C952212C0A7F681B355964FC0C4EA914E66FC8E4EA40750404D1441C543FCC3B938B20464229FC99AEF56B1123C11EEB40DFA68C6F473C3B955685875256CA0BFCFA39944443C3F1160F808682933053E7759F8F2494DE65D4E4C159AFB4F34C6862D26C41DF2AFF896116007785D58068E5086B2426EB5124FCD0A9C8339BFA01116F862581F5307DD0C8B361113D346629BC29CE3A21CBCEA14483010A878A9048C11E1E6A07E011E4BAC09870037E1FC4987F94513403FF9D9964639885C14851A1B5FF5C119F4522976AC4E7839B1BD607F113A48739670BF0CC7A37498F8A0CB74B8A9DFD298C9F916DF547F68C2A369966D99C623CF782DBFF542C885BB5D9B027FE3CD50B54656179332EB3D162B8CF59B604BE519A5ABB105C0012967CD794217A8E4A3C43B785255AAF80988DFE0979413C31EE13D55DBC43B316E85EB0C7470C5F2577BFD185774CB03CB39CDB408C243F26DA2B424A55C3D8975CB1F900EFA1A3FF7F7C6DCFA28B52749357994F5C18DF3138166373F1D9C613410287E1BADEC6C23120889829C251831507BE05E137202A9336EF5C69D33823B69F91EC25369FE3C8FA8F2A244A1ACBAF5C851418515548920333F0B584F60C22063D66AAF545328F1CFA23BA676140D0E7BA6D6D94D4409AEBF5B209524A68645277111BAC6A2B09A13944D9471E9F704441215D6B93D63FA01E3BAA4BEF7C701E485A1A89CC8E8602C5259CF3A176EBB85487C3BEAB80B4E5B8AE1B54335BC5BD1E94D35090CDE556E2559A27F770053541090CB94BFE3CDECB1B99C0B9914460631061060927507096A16B37DB0091616BC00CF68461C3A47BB585E8997C6A46C09B07C98ECCB10E30C45D014A1B9C2559EB190651296E97C7DE0BBC475BC49AB13398CD98C91A06273654977AC3B70EA83BD5657F47381D9090909735EB899BE2DA5BC73B56232736AA326845FF5AE6C452523406901781E095244C8F689B0271D53327659D0A625D75914E34394149C955935ED88C1B41475671338451B61234549C74091C9CB14E90B02356BA21CD4C960522190CC68C71AA088FB2EA5D1A777075DB4210A289B208BE66F6BFB6DA19540B740A34E822232A4BBFD028BCC304976A4C2ACD924153A974FA345959AC74D13C6C287265FFA6F8639BB0E407C1380627D74BA3ADCC81176A85503213109493A2C745D5A65EAD31333537C51CA45E8A34A3D555C19649726C6675B645519645E6331A81EE47C6EA35B49283AD8E48FAD465A426A3352C0001E8BAE99607A26A71126355A0127C288266951E845CBC0942023950CC73677228059321C2BC9414A190EDB61021BE21A4CC26CE4E1303C430BBE607B996AB304519A4148BCBD72B0B23BB947A36DA2C983898A71A8D18F8CD571ED274EE76713C540118B0CB45CA6BC72C5C96FF1B231823ABBF112FD2683F12973DCCA995B007DEC83B600999535590AA98A61C711A2DF076E65803746E93885621557821F49E3497EFB4A0F8C34ADB34B42CC0F32B09D898903F946A569C6CEAE0630E13004BC75241343061D2BA6B1D7BF44A00712941C10E64183E5A007708F798456C695B15F75C3C6A27310393A3049089ED8EF92DDB01CE5F59229FF7CC3BDC6A66F789A31E64AFEBCBB1DD2F94747FC7559309D17920DCBBC3C38C4BB059C7C3E68F827936D8DC435942DC4925D180E6D5C911550089E1337D8BA77A06C\"\n        },\n        {\n          \"tcId\": 21,\n          \"ek\": \"E5169D5BCB1CFED42BBC044B89208FBE0399AAF9A3552B7B0A579A43BCACF0D1610F6057CF938A157A9DF8B797D402461559CEDDA6A15D754343DB49C4D095A4E763FD22833FAB8B909A2129E628B9FA656482AFE9659B52AB9C74EB885A667D42946B35D163136672F30404FB881FAE153B13589299C515185B25D878ADB1D903743B4ED2B96838C29FF3C5183CEB6CDA510E6EF85037163DB1D658831B97921273CA84A924947E47F88BD2E3ACFDB5B7EA634C70794D9CACABC67C08E4379DBEB84575542A108671FFD6289FC97B97F073C98A845E10BF63C1990347A6C165317A51CEA3231632A967D2A97619414701BCC6F839C51FB1885B7993A891B31F7A79B19176FF215EAD642BD9015DE7DA03584A867C83C0ED7B5EBB16CD6FB671B1F48BF426BCCC24529F91819931921016A627FC2960C8104913A6D4372964013B0951593E09B8256984E8999A28DA134AF4556F5A5A39A63FBE950828474FAEA86F33629DDA45C42CBB90A885AAD3A4C006E66741362A3FC9CFD92B94D6979EA5623554986F36F4C9898681B345C81823852FABC76234023587B10462008562262BB37981EAC047D458E8D14789C18CB740BE85B57BA613B884104DF64159B277C469F72EEF73095F34C4341374992BCE5D02169CCA1FEE5456F44BBB52C4C12610AB2E3A48DBC9A4FAE18B5CB6A1E2177191338AF490A18F107C4A9704DCE3A40F997233347A7ED8116BEC2469BBAEE4EC141AF80F45E67FDE896838761BCA4B7213D260BC362EB3924F0964A59A83827BDC69A1D816DB49A077150486245E07C3B7261B1DFA7492B778073BCA7BA8B690C4F47AC8398A3B10A18779935614282D64428C4B50FC879109B22F44E079AEFC50741677CB4566AE80114DE8033AEC9B04202EB45176E7BB77A9A1220845C9C2E5746037C422A96D23EB07E4B4996B2B12470C62EAE1C972A22A259B7B4098197C69571B9066DA5712BAC75AA1D69C4B289E2301927CC730E9E15F3E01928DE1AAE25B80D6A80DB4D834E61AA6BDCC8615C0638865C573B5B06EC9C16DC014BF53239F91BBE0D5C25D09A766405C35D11B3732B2D2AD39A22C1FCDBADDC3A81EDC8E5EF80FE66B2A69753C82FD63\",\n          \"dk\": \"4A7429DD79A88B892D16D35201558135962685EA2F76A67DAD213DBB846FA229764E354745381DF8B64217A9BAB2CBA9E143C22F901A3C0089081C6B7D4B91D7115C2A740D57745B782122A4D015BAB56E41FA2642DA060CE20078E57FC2706A676BB2E1381D639951F0866A401127A4312BA3F89CB8E558AFF6B86899617D57606BACA6351185F1159D9F4A469A0A8B36B50E13B164BCBC13F473B31A7118DC6A5C79702405E0ADEB466A359B8F47EB807ECCC0E0E28E55C28266219FFC3A530220B97B080C91302B77819A017B2689F7A5F9367EA8422412FA8AADB20B4E34006236454C9B6BA1B1C160BB46ABFA8CB0AC530EBC3D63909FD2CABCB009C67759AF90201C3808B35C917E82847232074C67D37D55454EACE4B4CEC23300BD58836786A5C02014E537848A012EA7C0E88071D305C539B7B4EBBB5DE188B566119956E264C8131DD318C80C56C000848C3EA22C0B358AC0B08B7B5A864601AE8C72C9A4113188251243E951FD9542061653502475D1396696C4C9E94105C3806E26128D24ECB03BDABEB2F49DFCF0795D4918E4394374DB596DD47B23BA543207C2C744C5D83C1C4E78C0C2B0072CF4A9870C4A6F1CB9B41867F6C1278D477928307752F0975FF30106982EDB719F1BB97B70C8828EF4C1C475B59BD61C7CFB95D95ACB251A0984E37C8ABC92A8B564DEA4A5C2301E60D10227AA7161B52B8BF4370CFB7342A756F343A020B84052626D3E9B2B11CB819D0358D2B99041320209248B9049B6D96302F871A0F69493721935F2753786CA1673A907618C83C9D3B9D2B6C702EB90C8A8BCCDA54EB782AE17513E38840BA30C6920F8B9C2E32653D7B5B7F6B50A08BC86CA5A1C756610A595F1D8034B26B29ED676AF2C26788A3CC3B271A9F30B2BB473D0E47C941711ED6355823A82FD957D1D709EA7152E5B5BA97F78CC2A997405BC46AE8CB24223B0B39129D8B7CE677C82BFA332B66B26C9E61DC13656EDF538C65C88AB15A1F019044BD0A5DD32090D0C7E59E269A4213EDDC469FFD18F1532A7AEAB051DF7727C14BC721B2F39326C43A266BC531850FBA0E5169D5BCB1CFED42BBC044B89208FBE0399AAF9A3552B7B0A579A43BCACF0D1610F6057CF938A157A9DF8B797D402461559CEDDA6A15D754343DB49C4D095A4E763FD22833FAB8B909A2129E628B9FA656482AFE9659B52AB9C74EB885A667D42946B35D163136672F30404FB881FAE153B13589299C515185B25D878ADB1D903743B4ED2B96838C29FF3C5183CEB6CDA510E6EF85037163DB1D658831B97921273CA84A924947E47F88BD2E3ACFDB5B7EA634C70794D9CACABC67C08E4379DBEB84575542A108671FFD6289FC97B97F073C98A845E10BF63C1990347A6C165317A51CEA3231632A967D2A97619414701BCC6F839C51FB1885B7993A891B31F7A79B19176FF215EAD642BD9015DE7DA03584A867C83C0ED7B5EBB16CD6FB671B1F48BF426BCCC24529F91819931921016A627FC2960C8104913A6D4372964013B0951593E09B8256984E8999A28DA134AF4556F5A5A39A63FBE950828474FAEA86F33629DDA45C42CBB90A885AAD3A4C006E66741362A3FC9CFD92B94D6979EA5623554986F36F4C9898681B345C81823852FABC76234023587B10462008562262BB37981EAC047D458E8D14789C18CB740BE85B57BA613B884104DF64159B277C469F72EEF73095F34C4341374992BCE5D02169CCA1FEE5456F44BBB52C4C12610AB2E3A48DBC9A4FAE18B5CB6A1E2177191338AF490A18F107C4A9704DCE3A40F997233347A7ED8116BEC2469BBAEE4EC141AF80F45E67FDE896838761BCA4B7213D260BC362EB3924F0964A59A83827BDC69A1D816DB49A077150486245E07C3B7261B1DFA7492B778073BCA7BA8B690C4F47AC8398A3B10A18779935614282D64428C4B50FC879109B22F44E079AEFC50741677CB4566AE80114DE8033AEC9B04202EB45176E7BB77A9A1220845C9C2E5746037C422A96D23EB07E4B4996B2B12470C62EAE1C972A22A259B7B4098197C69571B9066DA5712BAC75AA1D69C4B289E2301927CC730E9E15F3E01928DE1AAE25B80D6A80DB4D834E61AA6BDCC8615C0638865C573B5B06EC9C16DC014BF53239F91BBE0D5C25D09A766405C35D11B3732B2D2AD39A22C1FCDBADDC3A81EDC8E5EF80FE66B2A69753C82FD63D9CD4496493358B59E14BC382D58982A03F9561B4ED09C4D03D89EF77D9CF47E97A4C9A65A82BAEC15FF165E10490976EBB19FAFBA8F9E8E0DFFBDB4D5E1ACE5\"\n        },\n        {\n          \"tcId\": 22,\n          \"ek\": \"9DA1B8DCA84AF685C5EE685BAF30CA58BC22E9C75ABF67C8D5B8C283BC95004BB51CC6CB001466230710A9E5B43891581C9315A3B579B28B9FB60C5A44D47733105D136ABECE627889E19E4FD25A7C94C404207A4B982327A05451250C173B67A4F7CB2F19CBDA5637E6462842B7688559C599D62A6828AB62B75738E632C381C36E58424AC486BA4C1F4EAB5C4C9A96D27A1F4D3296F65A6094FA7FD27B02707C69A7B92787F737CD4623E3E51444CC698CBC7B38AB98F9883C63210442C8508A126870B7C45127C3C036B39A4C1E44909570382DC20B31E8E262ADA13847599EEAE60BFA024254726D36717C4C43B4BA148F82A93667B82407F2AA3E076E19E513564CA6907C293023773FBA330B8B9C90B08665122D31564BED3632936C5C70C4C452880BFAB97EE6018A67E0891EF64C86E8C4562C29CA609381B8AF55FC09069673FAE60E0412678958053F160F4E4AA135D89FBB0172D10CCCAD38070AE918D85C0866882829603B8E4B90E2074716A05FFE7B1959F8C16FA7CD42CC8A124B930AB87812C4C47B8C58352677EB718F508165CCD4CB3F6B6FA86460B744708F57046FD17215EC9AC6F92EA354AF8C83C0B4369AA475233A310FF7A915F9C64A10327F042C8D6450B0248621DD7748D116C090C6C6B58447433810EA47A07F9956482073FAA67CFE513AECDB87A1695A23E454D918CE0E69BBBBF0761F1B1C8F535469CC25204592DDF8008154144D148B20460F95B1B26D0CC5FF45081E379B85E642EBFA1728CA1435B081F4F608A92148FE27AD97D10BE5D3550ED08319218768D31D5DD9B2E8629B65A2345D259D45B4719AAB8616DB6422B9AEDA535099D503E8A865F3BB46C3A49C3784CCEE19CA3DE5189873C212068DE128C86884C2A3841D2DABA8088B4CECAB729C0C73421B8D0278343DE01584D37971E802A0699CAE5B2CF86800D6C348A0D8C4942538122C521B56A45244AB672402CF7B1A9E999820349F7E311940B5B47FD07C8CAAB8CD817DB897A4CC7591BEB264F043401B06932F81C5E8F525C81BC5D4C75D75CC98B8B6502625CACD671C408C21812277B7BA3388D40B4FE46510A1ADDFC040696BCFF31CD1DE026A9D5E8D3B6F\",\n          \"dk\": \"AB606FB6D65B4A42C61BF14D57C06B8EE33E740ABE1068833667B8CE769EA1FA5AFE7447863C3DBAB8B86C1C818CC3A2AEF08C50641F566BB154D57E293A468437C6A7805318B3578F83C927E6BB56AA31F096809A315C5D88473DD0BF2162BE18CBC013918504E791779A4A2CF82C5085BAAF78CE27690F164478C1F340A3582C845B2114978E2B7029D6A861FE302653213C1F9A437C9C870513ABA2B64A744A92BC37CCC8C0664CD833065613E7770C6C01A6F9091F02A99EB5F85C2361038A0662B3352C09FBC1948993724A87E2C71D63F7392E9950B2A84F241678FC15890247BE20C660AC3B2FDA9CBB9C919193F13EF3A5BA98901CDED47A6E1B300CF779BCD137748B571FC4CF890A69C1B5CDDBE30650877E2D82263448CDB1882D57716A76294CE8EC931AA131F06965678AB8FD778DAB50C58381B8090A7912462400E0B0E8F7C9F1FA78C57A056B423495C75F62D5138BB3A1034C70208917F30263E66B4D2949377BF29362A6531A81B82EA88517D3B943A14868D7691E4873F29AACFA8B71DBA337C9E0BCC1C8AA8BA82A17A28395B45EDCA609BF95B883731C62C000A3D431525391860223579C247AE825CDE91E1FE3B0A056BE5694627BF3BA189031353429D0C64C79448E5E4C80B8963C4D7229E9188B76F1738C30BDDF49B236544558C9A7F979B1C34826C0FC10B3362E84D62A665B16648CC8376BCB1A71116E260E0439A9EFE287AEC68942F3916CC77817C9C89F378E5976B3C8B9815F7013D573C9EA656A95F40F1243B5940B9F6E830341E1108A45CB68B346600406D87403B8352FD7014CA3762070FB8DF892871087B7E0BC5562B9686BAC3A67665AE4CB118A602C28394224D15C5A9615C86B40184A9B7E4A68DE936C8144A157F4B1AB036F30D92F0BC497ED0716380BAB4677111AD19BDDA98B333C960ACAB48E33AE12389492B98CF4FC90F7102012840010F63CF78B6193E133E45B81E6380B4E01CDD0814C8E97732B4BACF3171215728A378BAB58853E38FC51A788829E810292C5089EE972B8850182D37D49F8BFB09178D5CA779E91776BF8579DA1B8DCA84AF685C5EE685BAF30CA58BC22E9C75ABF67C8D5B8C283BC95004BB51CC6CB001466230710A9E5B43891581C9315A3B579B28B9FB60C5A44D47733105D136ABECE627889E19E4FD25A7C94C404207A4B982327A05451250C173B67A4F7CB2F19CBDA5637E6462842B7688559C599D62A6828AB62B75738E632C381C36E58424AC486BA4C1F4EAB5C4C9A96D27A1F4D3296F65A6094FA7FD27B02707C69A7B92787F737CD4623E3E51444CC698CBC7B38AB98F9883C63210442C8508A126870B7C45127C3C036B39A4C1E44909570382DC20B31E8E262ADA13847599EEAE60BFA024254726D36717C4C43B4BA148F82A93667B82407F2AA3E076E19E513564CA6907C293023773FBA330B8B9C90B08665122D31564BED3632936C5C70C4C452880BFAB97EE6018A67E0891EF64C86E8C4562C29CA609381B8AF55FC09069673FAE60E0412678958053F160F4E4AA135D89FBB0172D10CCCAD38070AE918D85C0866882829603B8E4B90E2074716A05FFE7B1959F8C16FA7CD42CC8A124B930AB87812C4C47B8C58352677EB718F508165CCD4CB3F6B6FA86460B744708F57046FD17215EC9AC6F92EA354AF8C83C0B4369AA475233A310FF7A915F9C64A10327F042C8D6450B0248621DD7748D116C090C6C6B58447433810EA47A07F9956482073FAA67CFE513AECDB87A1695A23E454D918CE0E69BBBBF0761F1B1C8F535469CC25204592DDF8008154144D148B20460F95B1B26D0CC5FF45081E379B85E642EBFA1728CA1435B081F4F608A92148FE27AD97D10BE5D3550ED08319218768D31D5DD9B2E8629B65A2345D259D45B4719AAB8616DB6422B9AEDA535099D503E8A865F3BB46C3A49C3784CCEE19CA3DE5189873C212068DE128C86884C2A3841D2DABA8088B4CECAB729C0C73421B8D0278343DE01584D37971E802A0699CAE5B2CF86800D6C348A0D8C4942538122C521B56A45244AB672402CF7B1A9E999820349F7E311940B5B47FD07C8CAAB8CD817DB897A4CC7591BEB264F043401B06932F81C5E8F525C81BC5D4C75D75CC98B8B6502625CACD671C408C21812277B7BA3388D40B4FE46510A1ADDFC040696BCFF31CD1DE026A9D5E8D3B6F960EBBE29B71F9BB16A8B5EDB72516DEB04771759A6A306CCEE5D40D8E2EFFDA973DBB6EAF76AF0C96F0F24EF9AE65ACD854301B5F7A7892A17FBB8601DE78D3\"\n        },\n        {\n          \"tcId\": 23,\n          \"ek\": \"0148396E994C3C5809BA5A53B28582B5B12827616D84BC9B6FCC94B84A1FF1EA88D308302F611683EB500953AF2D588EE43C7C4CCACC276128E6509DAE5A78DBF654CB9A0AFEF98339A46002B432C90BB863CA5221522BC7E903E2D1AC26E4968B64888DE455D6CC61A7FB2BD8E03BAF87310337361101BEAE92C64904BFD6D74F9D4327E49B15A7468889C6BE1F6A6DBB3529ED166BB0791381D42A59C85D54C1484B50AFFE893FB2A5BFD60974BC804E30A14E5DD22284C361CFC311C261C02D450B864095109B01DF56016A8BABEAE78F6145CD65343A8E7B098E904453E165D31273ECB4486B1B43BF453AE853B74AB920DB1068E8D9C8C8779BB63545EA0A15D4A1C19897C8BD40CA1C66381832A990FB74ADCB4FDFB2185AC33D9A24B58983B352AC26BE979D2FF8B8975B7D6B09B50E0C635625A99E91C552F37C37C72B763047C36192900B0A49BBCBB19C611CE77562D48D06CB735E174184952614B9236C55A256394FC5661F4F70BAF5B22BC1D67860A51E0F301AC5DB4F15D061028026CBB4A35E1782B406C8F92A40FC500F5FE706CB1A33DDB73597575BE787CEDC5C43383A404C88503B351732BB9FAA65C07C0B1EBCE001D5D18203672885E201C3D9855F81C36D39B060761DCD069E7CACC758026456E266DCA665AB32908069448BD251F3D3C09B15B4DC2234642295BA9419574A8E3987B69C4058FE692DB8124821B1A7F4097712B828AB69C9B0F5A6AF9B515588146EE9A1E79780A2C9634AC9CE38282B1E47856F365F23AC249D0BB7E42520B8B3A08792161B8B86C59CC0DECB2160F536394A750CB9373BC62BB2C6AF8257935465C550382822A915A501693A777CC9E46F415BB581C70B39DCA1F84673B6DCA1BF2897B8A57CE247267013A49D3543CC8533EB5BBCCEA0A9C82909ABEB98CADC45EBC5B4034145B7F5A777759CAC11CE4C359B0AD4BC30567E9F195C9082B3A71779D9A3B4EBA72E557AAE9B964285D142BA0031C3990192912F34B540104A7E13674B30C0569685A4ADA913BEA4439ED3C99172A3672886B857830DB939A2836A8E3186C8B8CFD87CEF4EBC154213985561AF0C82B28B8C856D8923219B1829FE8B51954BD0E8\",\n          \"dk\": \"4ED9B27229CB08667437F2B6F0B28ADA29A384F726E1F86DCD26433D55CDD5511158EC7D257892080A4DEE204FA1103890821E2F233281FB064D299DC9684D3D08A60669B014112338476F0455780714524DE0538628C72AD360A795038DF2C3A44058D71C285A4A951086889A8A4DB9AC9B4F76821E12732DF3713842012F3A8F4A611202434A1E6046E58085568037A4884F3D8539FA5005A8E10E93BC03C58BC9D575761E4A5379AAA8422A1CBAF58C8F9C1DFA00BD0550980F86B32036CC2F55C4C6602F5CC528BD1971ABD7AFB1528F39A7CFE1A42CBA9716B1A6A8A6722F04BC5760497F79556D7C065E4FFB9A4E8019F38744849B57D3F17AB8C37FF71CA594691AE9288BF7E1BFB09C9B113C375B4AAB07C91452E384E8F8B1629B7E38A1943513CB2AC40A5803B71DE455FB2B55FC50CE6170A15DC4A9719193A5C2CB17C62B87C53605E955225080011468913AAAA4946C9A084A2405A335BABFF2E19A3209C34273CC96A4509A0041E474C44F53617A36326A7892B6D079BC4651775B0E06EC01C81CBDAA7574CBF84940384A15777F241268CFE65E6408609E404A372BAE702790E185ADAC6AA93843B3CE88696E3252EF73B46C5A4DDE135372A32762B963670565531784144399C6195EA6C924B89AA5F9B64BA44416D92B3E75F7BDEC264549B045922612C0995BBFF71DC7505F04D4BDBEC3A40E7C402A225EF4153C1A322B6E19BCB0328B9ED23FE36A6F13780A6A78171E875CD37A79C3375671D066EB20072DA5CBFFB8772DA14DE98631899597F7F55AE84BBEBEF076E709CB68946041F0095759360E003665408105184B39A7261545AC0E7984FD32A773D05A04A48FBCF02B08E7B3E326583DB71F59E15C7D1A9D6AF1494CB34E63D5C6CC07C23BE95F3EDB765C33131FC92B07592C724832DEFCC1D5F937AA8323C1089CAB760D26AAA49EE1805FF62382998B0A4B8DFF6C91970126833B44518501A908A0A7C462E678495DF876CE3A71528CA877890B56F6B23C9C2C560B0ADA3A1915E265093267E21A2EC18351079011E6E0CF76967AE3B80E1038B3E203A40148396E994C3C5809BA5A53B28582B5B12827616D84BC9B6FCC94B84A1FF1EA88D308302F611683EB500953AF2D588EE43C7C4CCACC276128E6509DAE5A78DBF654CB9A0AFEF98339A46002B432C90BB863CA5221522BC7E903E2D1AC26E4968B64888DE455D6CC61A7FB2BD8E03BAF87310337361101BEAE92C64904BFD6D74F9D4327E49B15A7468889C6BE1F6A6DBB3529ED166BB0791381D42A59C85D54C1484B50AFFE893FB2A5BFD60974BC804E30A14E5DD22284C361CFC311C261C02D450B864095109B01DF56016A8BABEAE78F6145CD65343A8E7B098E904453E165D31273ECB4486B1B43BF453AE853B74AB920DB1068E8D9C8C8779BB63545EA0A15D4A1C19897C8BD40CA1C66381832A990FB74ADCB4FDFB2185AC33D9A24B58983B352AC26BE979D2FF8B8975B7D6B09B50E0C635625A99E91C552F37C37C72B763047C36192900B0A49BBCBB19C611CE77562D48D06CB735E174184952614B9236C55A256394FC5661F4F70BAF5B22BC1D67860A51E0F301AC5DB4F15D061028026CBB4A35E1782B406C8F92A40FC500F5FE706CB1A33DDB73597575BE787CEDC5C43383A404C88503B351732BB9FAA65C07C0B1EBCE001D5D18203672885E201C3D9855F81C36D39B060761DCD069E7CACC758026456E266DCA665AB32908069448BD251F3D3C09B15B4DC2234642295BA9419574A8E3987B69C4058FE692DB8124821B1A7F4097712B828AB69C9B0F5A6AF9B515588146EE9A1E79780A2C9634AC9CE38282B1E47856F365F23AC249D0BB7E42520B8B3A08792161B8B86C59CC0DECB2160F536394A750CB9373BC62BB2C6AF8257935465C550382822A915A501693A777CC9E46F415BB581C70B39DCA1F84673B6DCA1BF2897B8A57CE247267013A49D3543CC8533EB5BBCCEA0A9C82909ABEB98CADC45EBC5B4034145B7F5A777759CAC11CE4C359B0AD4BC30567E9F195C9082B3A71779D9A3B4EBA72E557AAE9B964285D142BA0031C3990192912F34B540104A7E13674B30C0569685A4ADA913BEA4439ED3C99172A3672886B857830DB939A2836A8E3186C8B8CFD87CEF4EBC154213985561AF0C82B28B8C856D8923219B1829FE8B51954BD0E8379CBECD878337A3709BC5A62C5528CB3504D6A87427DC404EFF9ACAE893CEEBD525CCE60C3E300ED36298A1C0D0165C147CB84197C4028257DAF39239E6EA5D\"\n        },\n        {\n          \"tcId\": 24,\n          \"ek\": \"E08607BB14655E1B5DAA36971A842091513B13720A20971FF32279FDE800CF82A95F96AC390749661958E510605DB13175B941D27AA8D1B07A76BB3EFCF8810F144F2566C938DA012E792F5EA41AB74431DD50584756963C752F8ED1109021521D4CA897019AF967275C92AEBA2655ABB291E9115C964C5E5EE748D8C0258DDC504DBB98218A7DC035251A958CC30821ADC997667616DE2A4497039189F582BC4AA9EA4A33BDC1858D8681D4088622A0CB8EA59413423BEDF0CFF7F36BB419B1B672820EC177B3920797244079738EBEAC8D34057804E9A6AA31B39F589F59198972213A12F80D4163A93511985B892ED8BA9A0FA260A4EA8A3BE1A556174C3A599F31D037293C5B8F6A229E23ACB6C772529B918C5324246C2E6B8C693C512BAD0C1677E013C7B10686E728E66723068256FE924F2F3A1578569D24D8327AC543C43AAF423776EEF10D43A69AD87058A37A95F4D8C444A08EAA9CB687E0851BC2A7D304B39B2953AC81A4E9E6B454DB06E75B495B11416D78C86009904377996BB0489BBC2C1B68C5FED757782082472616BCD7AB7747A36457B7BEE74F9BF39A10C9071BCC9269B61EE93B2062160EC69174229A9EDA906559F677018A28F1A278D6337CA54AC68F7265133A1CB59A64E0699348153613191DA8793D441C1422F498B9088EE3664153F67EC443CD8DD10D3068628F10C49662CEE84CC2C85B3C59553B7220AE499C1A9838A8D0E497C11717B5539575F62CB49597C9466A0331C118849F53CA4EAA516CD8343B28F5A0787313F6F1331976329203B0CF871A2CBA0418A574DB037DD12A139C3B36850C8E2B2C056920A1F656873DA9CEF9427D4FDB15D069CD54FC479DD63464F63598728D050444E2014D6E7040F7942991D34B228076B14207F8777D718180AC9C54379B7952F732E865A915F874D1BB839C1A4E2617455AD061509C83CD9A123A2B34C4393C8D09A2C9BB8D52643FB6205730C98976F99D649428CCDC4D109242E7660DA8187C1C1BA83FA11834B0824E31084F0693ADDC1F06DA292398906CF2C07DB5337EACB9492C57F12499B9BA7828A4D75A3EAF1E72A78C47ED24CDD9A7BE6E15D35A92BD110C97DF3F400DD95B\",\n          \"dk\": \"E3930A94D60F5D896AFC749DC3D5CE56A566D7D28795B7747EF95DF030C97C048657B6C1AC754E15F47120F75129E34B10BC3F11924D42655836D6B7E5D126309070E2D75E616A517481842B851148A586D9015045DA3602FABDE204AEDC633B95C30B8DD533D61222BB465BE3BCCD792868990C3E7C1B0E086ABD3EA9CF4F110B1E7612B0C32FDC693CE2FA3004E40A507674528749A470ACE9201D68F56CA717C19539A0418488CEF40CEA1458D6EC298FD04F60C9878F01851EF07B032C3A91B0C742A445E5E72131E00594D0595794745C26BF17E7A4297A9186B23F3DC90B0EA183CFC13AB5269CBC4310840029644876E6C6C73E066230F182CE3B0F039522DC7ACA25AB1DBEE371574786308C177E1ABE75C202D383C47409111FC29F72E18085542E231486F6F76A9CD0BCB9384DAF6318260761E96A0A36DBB50B902BF6355186B6BFB552AE94385523E02935F98C2A35AA3BBC03B7B9A4E4559C741347C304AA8C033524A8835294738908879374C8AE76BDB52C30203A50D5B9330CD2941E6A9EDD51659C50165C06232778B39AB6041E27BFA5DA56A5811C7CE35140724E7DB5072BEB217518120D5366DADC4653DB4D260C65DBA35342D34BADE3A27E3757345C3DC4D4B4AE1068D02989B63722BE94A2C8589DFF97B236B0CBEBC436F71B4DC2A619FB88C16A476A42E51410ACB5BDFBB01E9B695C2724536BADA18913E3F3555B120D61A158F8722688B6C703675CC9580C37C52E2A338E08192E3472BB600037DA863195F0A0E4C215FD57A19BA20CD57021F5025E0AD79FAB9123238890451547B0670491B03DA6C6567A767449B7BBA1FABEDF7BBD38E56A02F9AE20E0C5BE502350A1A27A9A5457B3C173796986F4703EE6BC2706630ED56859781DE66AA9B9AC87D9F512FE569C5F164B46AC0F3175516F633FE31C60B09893CEE29EAE725F65033E0B1BABBE453F84F6673A963FE74A8B5FA046108A2243690D9072406154C6A0024D2CA4B4EF1A681A911A781C776C126775B0CE65110BD8C76A1B2444F62B2CE4541C102C70C1F46B6B5AC0F8A8574836122A99B1E08607BB14655E1B5DAA36971A842091513B13720A20971FF32279FDE800CF82A95F96AC390749661958E510605DB13175B941D27AA8D1B07A76BB3EFCF8810F144F2566C938DA012E792F5EA41AB74431DD50584756963C752F8ED1109021521D4CA897019AF967275C92AEBA2655ABB291E9115C964C5E5EE748D8C0258DDC504DBB98218A7DC035251A958CC30821ADC997667616DE2A4497039189F582BC4AA9EA4A33BDC1858D8681D4088622A0CB8EA59413423BEDF0CFF7F36BB419B1B672820EC177B3920797244079738EBEAC8D34057804E9A6AA31B39F589F59198972213A12F80D4163A93511985B892ED8BA9A0FA260A4EA8A3BE1A556174C3A599F31D037293C5B8F6A229E23ACB6C772529B918C5324246C2E6B8C693C512BAD0C1677E013C7B10686E728E66723068256FE924F2F3A1578569D24D8327AC543C43AAF423776EEF10D43A69AD87058A37A95F4D8C444A08EAA9CB687E0851BC2A7D304B39B2953AC81A4E9E6B454DB06E75B495B11416D78C86009904377996BB0489BBC2C1B68C5FED757782082472616BCD7AB7747A36457B7BEE74F9BF39A10C9071BCC9269B61EE93B2062160EC69174229A9EDA906559F677018A28F1A278D6337CA54AC68F7265133A1CB59A64E0699348153613191DA8793D441C1422F498B9088EE3664153F67EC443CD8DD10D3068628F10C49662CEE84CC2C85B3C59553B7220AE499C1A9838A8D0E497C11717B5539575F62CB49597C9466A0331C118849F53CA4EAA516CD8343B28F5A0787313F6F1331976329203B0CF871A2CBA0418A574DB037DD12A139C3B36850C8E2B2C056920A1F656873DA9CEF9427D4FDB15D069CD54FC479DD63464F63598728D050444E2014D6E7040F7942991D34B228076B14207F8777D718180AC9C54379B7952F732E865A915F874D1BB839C1A4E2617455AD061509C83CD9A123A2B34C4393C8D09A2C9BB8D52643FB6205730C98976F99D649428CCDC4D109242E7660DA8187C1C1BA83FA11834B0824E31084F0693ADDC1F06DA292398906CF2C07DB5337EACB9492C57F12499B9BA7828A4D75A3EAF1E72A78C47ED24CDD9A7BE6E15D35A92BD110C97DF3F400DD95B402618B875F180B5A47574F635D61BB39EA75D44240CBD759B7B5C22C889851C9F2FC49CD848BA72FC17854B18D88ED65B630BA94A1BC5F6D3A458E1087D3A13\"\n        },\n        {\n          \"tcId\": 25,\n          \"ek\": \"655CA28309AD58DA0D71919EED2C5676E134C1AB1860923F1E34AF07581D64121B72B8ACE613BE88AA28BD7B0138B44FD9C39B65FAADA741934AF9207AFA367CC1AB29CB71806735973188CD146813331DEA81A811F61FD8F59FC668AB29C581C5878F2D4698E5CA52DD664AEC16ADC357CEA156876BB6663DB12479F488AA46B7873968D0AA43EDC0B847F19FD4410F5E45C2A599A09E634E65B206352383B79BC6EB9309574733F9F2226D4402A4946A00D7B469DCC58F910D5B149BB5731FD89CAF9734C2531381EC2A60F68A9DFCA55C288C68BEE4A919A9A82CF01E5EA86A170B76ED5B35AD9573C3156A24D8A9718683D0F44547F03EF0237B430C16C1A22AF41B7D46E839CA40A7F5F982ECDA7EF21A7E93283B6CBB566E2237636925B887616DB260019876386BB9CB63A42EAC0EADC4C6B99A3EC4131667A4BC3015B19A9A7896422DE73655379BB57675C26F31609BF5525AC13B1A711974F7CB80BACF59B43A8C0AC6B3249968E462F782CF85661207744B4E016C22531D4879B4AD9BC80320503D311CE818A29B95595AA6AA4590876C4C62E0F5A75ECAADF4154634475425B7BD690A09650C7B543589D0342C18A2B774CB7DC8C44F3502A2C2BC148465061842118321A8D4465B4383B45A52727B389FD5A9601E560C9FF68882C629D209527429C79A1ACC000B698181AE827A7E6F14235AF281460A91DBECC30E4AC3AFC4A22E45B3F1209BBDEABDD678197EFCB927B47B0C4204FD473AAB098984349FDF80B960203530E36153BAA675545F5DC6B49A276F1D210373553F9F90B2A5C661415C65A973991E2A99C756273D3261B8750431CA866FA234A7E98035804A0AAACF8D36A0BF4A5C9F9C5614138287858FC106CDB6918E2FB37BEAE2139F806838776E2B3534D78ABB199833E784A6FC144A6D7CB8984C4563E273CE11421927377A992D602BBD11457A2C88B999B13A6B3944445CC7C8328F47040C5CB8B2EB29192FAA2720B1788A408F59828D42017D27587CEC9CA49F44829AAB7FAACC484936B38E02820C03C880F7CEC7057B89A1604D7B1865090CDF056B83E84864E13B8B1B16E5BF2115103F5E1A77266147DEB23013A1A909C1FAE8BE\",\n          \"dk\": \"03D96BD4C488568ABFE20B8FB2C408AAD328481B228C03894BB5248292B2F87714622714A36936DBE1BC477AC57EF3B3F46570B776014F0C12D3B0BA3FE81AA3CA9908ABCFB47AC622AC4A4ADAB3C0052EF8F133E2F8B415C3BE8943BB55C902F792CD6F3A363D3C6530D0A4AB1886EDD770FF18832BB7AD89CA126450A0A2CA587EE1552F149C2DC570447A400C403A2883A8D79B361E4AB99B53B4F6C5CF88E6B2CCE28E69356FACC4217539583ED813ED0A554562C02F6696E9AC5714B5ADD1BA77E603528A3961591B5593337E7EA552E9770AB43009A531679F4C66C0E817BA0354BA24977572956DD7043C911133F28982D4B901307375CB2EEA0089D1C03E6594AA78884BD5E7C9A8A89E9661C2E2629240CCA2E4235B13AB21F75C6FC6A529A574CDE0C5381E40137A9CCBFD910243174BE6225C3ACC8F38393365530F2905294F9ABA5108A3487C9D056559C9E8A11CC94F7CDC46533C391FB2A491973F8BB9545773BB90FB43DD7435DF6C90F8B411A8274FF648A0EEE54FEF30315993C5BFEB6BA0233EE920CE01927F8752A63598528E463A856930598643D825279F99C113B1578EB0B1D72B730D8721B8B149DD0B558977B0C3BBB0E05383C4B8844F661B6C34220D2A5FD7113DA3217EFFA6AB0C382808C0A177987E7DC8B86F39C3872A66412755230B0CB119A3E49541AD1868B9753F069A167E604BE6E01F129A684B880930202221D0BED12B9763B8BF41E639CD233FE6AA0B1A8082D6D46E5316405AA06154C622A384154FD69434E161EC4470B82466D84CCFA01B0C795C9BD7175A2DDBBC5B1CA564D942A0226F096A54AB5719A58A60387B5AFED4878DA08E9763A164E150F50CA719214181165CF1069E29FA2AD81ACA55A802DC9A03F351369F525C5DDA776B93717A617A969811C89CC27ADB185B2A197A47C6C360B7D254C0F32A20C8A35990C67098896180AB9456866B09E62941482B626BC40F63136A677AA7E1236A7B8ADD810193EC4C3E221017A6A8DBF079122CB288D60795482039A4AA7CD785B85B44D1817E5F79232318C0F6F408648A3E5395C4655CA28309AD58DA0D71919EED2C5676E134C1AB1860923F1E34AF07581D64121B72B8ACE613BE88AA28BD7B0138B44FD9C39B65FAADA741934AF9207AFA367CC1AB29CB71806735973188CD146813331DEA81A811F61FD8F59FC668AB29C581C5878F2D4698E5CA52DD664AEC16ADC357CEA156876BB6663DB12479F488AA46B7873968D0AA43EDC0B847F19FD4410F5E45C2A599A09E634E65B206352383B79BC6EB9309574733F9F2226D4402A4946A00D7B469DCC58F910D5B149BB5731FD89CAF9734C2531381EC2A60F68A9DFCA55C288C68BEE4A919A9A82CF01E5EA86A170B76ED5B35AD9573C3156A24D8A9718683D0F44547F03EF0237B430C16C1A22AF41B7D46E839CA40A7F5F982ECDA7EF21A7E93283B6CBB566E2237636925B887616DB260019876386BB9CB63A42EAC0EADC4C6B99A3EC4131667A4BC3015B19A9A7896422DE73655379BB57675C26F31609BF5525AC13B1A711974F7CB80BACF59B43A8C0AC6B3249968E462F782CF85661207744B4E016C22531D4879B4AD9BC80320503D311CE818A29B95595AA6AA4590876C4C62E0F5A75ECAADF4154634475425B7BD690A09650C7B543589D0342C18A2B774CB7DC8C44F3502A2C2BC148465061842118321A8D4465B4383B45A52727B389FD5A9601E560C9FF68882C629D209527429C79A1ACC000B698181AE827A7E6F14235AF281460A91DBECC30E4AC3AFC4A22E45B3F1209BBDEABDD678197EFCB927B47B0C4204FD473AAB098984349FDF80B960203530E36153BAA675545F5DC6B49A276F1D210373553F9F90B2A5C661415C65A973991E2A99C756273D3261B8750431CA866FA234A7E98035804A0AAACF8D36A0BF4A5C9F9C5614138287858FC106CDB6918E2FB37BEAE2139F806838776E2B3534D78ABB199833E784A6FC144A6D7CB8984C4563E273CE11421927377A992D602BBD11457A2C88B999B13A6B3944445CC7C8328F47040C5CB8B2EB29192FAA2720B1788A408F59828D42017D27587CEC9CA49F44829AAB7FAACC484936B38E02820C03C880F7CEC7057B89A1604D7B1865090CDF056B83E84864E13B8B1B16E5BF2115103F5E1A77266147DEB23013A1A909C1FAE8BE213234B8355942F1CF9F299DC63B953C236F330E3D406312E9E0CE14A3987E8E0FB831AFA34B124F7456D0D09E4ED8607DE407101E6E75F305F9D67EF7C2FAE7\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 2,\n      \"tests\": [\n        {\n          \"tcId\": 26,\n          \"ek\": \"6D14A071F7CC452558D5E71A7B087062ECB1386844588246126402B1FA1637733CD5F60CC84BCB646A7892614D7C51B1C7F1A2799132F13427DC482158DA254470A59E00A4E49686FDC077559367270C2153F11007592C9C4310CF8A12C6A8713BD6BB51F3124F989BA0D54073CC242E0968780B875A869EFB851586B9A868A384B9E6821B201B932C455369A739EC22569C977C212B381871813656AF5B567EF893B584624C863A259000F17B254B98B185097C50EBB68B244342E05D4DE520125B8E1033B1436093ACE7CE8E71B458D525673363045A3B3EEA9455428A398705A42327ADB3774B7057F42B017EC0739A983F19E8214D09195FA24D2D571DB73C19A6F8460E50830D415F627B88E94A7B153791A0C0C7E9484C74D53C714889F0E321B6660A532A5BC0E557FBCA35E29BC611200ED3C633077A4D873C5CC67006B753BF6D6B7AF6CA402AB618236C0AFFBC801F8222FBC36CE0984E2B18C944BBCBEF03B1E1361C1F44B0D734AFB1566CFF8744DA8B9943D6B45A3C09030702CA201FFE20CB7EC5B0D4149EE2C28E8B23374F471B57150D0EC9336261A2D5CB84A3ACACC4289473A4C0ABC617C9ABC178734434C82E1685588A5C2EA2678F6B3C2228733130C466E5B86EF491153E48662247B875D201020B566B81B64D839AB4633BAA8ACE202BAAB4496297F9807ADBBB1E332C6F8022B2A18CFDD4A82530B6D3F007C3353898D966CC2C21CB4244BD00443F209870ACC42BC33068C724EC17223619C1093CCA6AEB29500664D1225036B4B81091906969481F1C723C140B9D6C168F5B64BEA69C5FD6385DF7364B8723BCC85E038C7E464A900D68A2127818994217AEC8BDB39A970A9963DE93688E2AC82ABCC22FB9277BA22009E878381A38163901C7D4C85019538D35CAAE9C41AF8C929EE20BB08CA619E72C2F2262C1C9938572551AC02DC9268FBCC35D79011C3C090AD40A4F111C9BE55C427EB796C1932D8673579AF1B4C638B0944489012A2559A3B02481B01AC30BA8960F80C0C2B3947D36A12C080498BEE448716C973416C8242804A3DA099EE137B0BA90FE4A5C6A89200276A0CFB643EC2C56A2D708D7B4373E44C1502A763A600586E6CDA6273897D44448287DC2E602DC39200BF6166236559FD12A60892AEB153DD651BB469910B4B34669F91DA8654D1EB72EB6E02800B3B0A7D0A48C836854D3A83E65569CB7230BB44F3F143A6DEC5F2C39AB90F274F2088BD3D6A6FCA0070273BEDC84777FB52E3C558B0AE06183D5A48D452F68E15207F861627ACA14279630F82EC3A0CA078633B600AFA79743A600215BE5637458CE2CE8AFF5A08EB5017B2C766577479F8DC6BF9F5CC75089932161B96CEA406620AEDB630407F7687EBBB4814C7981637A48A90DE68031E062A7AF7612B4F5C7A6DA86BD136529E64295A5613EA73BD3D4448CB81F243135C0A660BEB9C17E651DEF469A7D90A15D3481090BCBF227012328941FA46F39C5006AD93D458AA6ADD655862B418C3094F551460DF2153A5810A7DA74F0614C2588BE49DC6F5E88154642BD1D3762563326433507156A57C57694BDD26E7A246FEB723AED67B04887C8E476B48CAB59E5362F26A9EF50C2BC80BA146226216FE62968A60D04E8C170D741C7A2B0E1ABDAC968\",\n          \"dk\": \"98A1B2DA4A65CFB5845EA7311E6A06DB731F1590C41EE74BA10782715B35A3102DF637872BE65BAB37A1DE2511D703C70247B35EF27435485024D93FD9E77C43804F371749BA00B20A8C5C588BC9ABE068AEAAA938517EBFE53B6B663282903DCD189736D7296816C733A1C77C6375E5397C0F189BBFE47643A61F58F8A3C6911BE4611A8C7BC050021163D0A404DC14065748FF29BE60D2B9FDCC8FFD98C587F38C67115786464BDB342B17E897D64617CBFB117973A5458977A7D7617A1B4D83BA03C611138A4673B1EB34B078033F97CFFE80C146A26943F842B976327BF1CBC60119525BB9A3C03493349000DD8F51BA21A2E92361762324600E0C13AAA6CB69BFB24276483F6B02421259B7585263C1A028D682C508BBC2801A56E98B8F620B0483D79B5AD8585AC0A475BAC77865194196338791B7985A05D109395CCA8932722A91950D37E12B891420A52B62CBFA815DF6174CE00E68BCA75D4838CA280F713C7E6924AFD95BAA0D01ADA637B158347034C0AB1A7183331A820ACBCB83193A1A94C8F7E384AED0C35ED3CB3397BB638086E7A35A6408A3A4B90CE953707C19BC46C3B2DA3B2EE32319C56B928032B5ED1256D0753D341423E9DB139DE7714FF075CAF58FD9F57D1A54019B5926406830DAE29A875302A81256F4D6CF5E74034EA614BF70C2764B20C9589CDB5C25761A04E58292907C578A94A35836BEE3112DC2C3AE2192C9DEAA304B29C7FEA1BDF47B3B6BCBA2C0E55C9CDB6DE7149E9CB17917718F12C8032DE1ADE0648D405519C70719BECC701845CF9F4B912FE71983CA34F9018C7CA7BB2F6C5D7F8C5B297359EC75209C2543FF11C4244977C5969524EC454D44C323FCCA94ACAC273A0EC49B4A8A585BCE7A5B305C04C3506422580357016A850C3F7EE17205A77B291C7731C9836C02AEE5406F63C6A07A214382AA15336C05D1045588107645EA7DE6870FC0E55E1540974301C42EC14105518680F688ABE4CE453738FE471B87FC31F5C68A39E68AF51B0240B90E0364B04BAC43D6FB68AB65AE028B62BD683B7D28AD38806BEE725B5B2416A8D79C16EC2A99EA4A8D92A2F5052E67F97352289761C5C39FC5C742E9C0A740CA59FC0182F709D01B5187F00063DAAB397596EEA4A31BDBCBD4C1BB0C55BE7C6850FDA9326B353E288C5013226C3C3923A791609E8002E73A5F7B6BB4A877B1FDF53BB2BAB3DD424D31BBB448E609A66B0E343C286E8760312B6D37AA5201D21F53503D88389ADCA21C70FB6C0FC9C69D6616C9EA3780E35565C0C97C15179C95343ECC5E1C2A24DE4699F6875EA2FA2DD3E357BC43914795207E026B850A2237950C108A512FC88C22488112607088185FB0E09C2C4197A83687266BAB2E583E21C40F4CC008FE652804D8223F1520A90B0D5385C7553CC767C58D120CCD3EF5B5D1A6CD7BC00DFF1321B2F2C432B64EFB8A3F5D0064B3F34293026C851C2DED68B9DFF4A28F6A8D225535E0477084430CFFDA0AC0552F9A212785B749913A06FA2274C0D15BAD325458D323EF6BAE13C0010D525C1D5269973AC29BDA7C983746918BA0E002588E30375D78329E6B8BA8C4462A692FB6083842B8C8C92C60F252726D14A071F7CC452558D5E71A7B087062ECB1386844588246126402B1FA1637733CD5F60CC84BCB646A7892614D7C51B1C7F1A2799132F13427DC482158DA254470A59E00A4E49686FDC077559367270C2153F11007592C9C4310CF8A12C6A8713BD6BB51F3124F989BA0D54073CC242E0968780B875A869EFB851586B9A868A384B9E6821B201B932C455369A739EC22569C977C212B381871813656AF5B567EF893B584624C863A259000F17B254B98B185097C50EBB68B244342E05D4DE520125B8E1033B1436093ACE7CE8E71B458D525673363045A3B3EEA9455428A398705A42327ADB3774B7057F42B017EC0739A983F19E8214D09195FA24D2D571DB73C19A6F8460E50830D415F627B88E94A7B153791A0C0C7E9484C74D53C714889F0E321B6660A532A5BC0E557FBCA35E29BC611200ED3C633077A4D873C5CC67006B753BF6D6B7AF6CA402AB618236C0AFFBC801F8222FBC36CE0984E2B18C944BBCBEF03B1E1361C1F44B0D734AFB1566CFF8744DA8B9943D6B45A3C09030702CA201FFE20CB7EC5B0D4149EE2C28E8B23374F471B57150D0EC9336261A2D5CB84A3ACACC4289473A4C0ABC617C9ABC178734434C82E1685588A5C2EA2678F6B3C2228733130C466E5B86EF491153E48662247B875D201020B566B81B64D839AB4633BAA8ACE202BAAB4496297F9807ADBBB1E332C6F8022B2A18CFDD4A82530B6D3F007C3353898D966CC2C21CB4244BD00443F209870ACC42BC33068C724EC17223619C1093CCA6AEB29500664D1225036B4B81091906969481F1C723C140B9D6C168F5B64BEA69C5FD6385DF7364B8723BCC85E038C7E464A900D68A2127818994217AEC8BDB39A970A9963DE93688E2AC82ABCC22FB9277BA22009E878381A38163901C7D4C85019538D35CAAE9C41AF8C929EE20BB08CA619E72C2F2262C1C9938572551AC02DC9268FBCC35D79011C3C090AD40A4F111C9BE55C427EB796C1932D8673579AF1B4C638B0944489012A2559A3B02481B01AC30BA8960F80C0C2B3947D36A12C080498BEE448716C973416C8242804A3DA099EE137B0BA90FE4A5C6A89200276A0CFB643EC2C56A2D708D7B4373E44C1502A763A600586E6CDA6273897D44448287DC2E602DC39200BF6166236559FD12A60892AEB153DD651BB469910B4B34669F91DA8654D1EB72EB6E02800B3B0A7D0A48C836854D3A83E65569CB7230BB44F3F143A6DEC5F2C39AB90F274F2088BD3D6A6FCA0070273BEDC84777FB52E3C558B0AE06183D5A48D452F68E15207F861627ACA14279630F82EC3A0CA078633B600AFA79743A600215BE5637458CE2CE8AFF5A08EB5017B2C766577479F8DC6BF9F5CC75089932161B96CEA406620AEDB630407F7687EBBB4814C7981637A48A90DE68031E062A7AF7612B4F5C7A6DA86BD136529E64295A5613EA73BD3D4448CB81F243135C0A660BEB9C17E651DEF469A7D90A15D3481090BCBF227012328941FA46F39C5006AD93D458AA6ADD655862B418C3094F551460DF2153A5810A7DA74F0614C2588BE49DC6F5E88154642BD1D3762563326433507156A57C57694BDD26E7A246FEB723AED67B04887C8E476B48CAB59E5362F26A9EF50C2BC80BA146226216FE62968A60D04E8C170D741C7A2B0E1ABDAC968E29020839D052FA372585627F8B59EE312AE414C979D825F06A6929A79625718A85768F3486BD32A01BF9A8F21EA938E648EAE4E5448C34C3EB88820B159EEDD\"\n        },\n        {\n          \"tcId\": 27,\n          \"ek\": \"5CC523B2D908C45907A6694A665195171A5B2FB583A5C240CADCA8F0E83E46B14052C9620D3B7EF386CE8B9A5E873B65693B0D341C6EB2D10CE5E937CFB8C4C9134401BABFEEBBAECF47113A34B9C6E011BDC78A54F2B7BF36A5FFD27563D7443F2109F02A64C421411DDB2D1404A86F793A2DE62CDC560BFD6604D4B6330BA6AA621414E8C12DC71C25652ABAF36B875DE1978DD209AB53B885206C3A1B4F8B4A0670C087CDA9CDA7997437155659255C2D024822A448CE5157CF5B6E4C495A949960886A902C79591120117C4A73CE7B380C661851E1CA9EF1973D8A9D2A191B938C4110259C4227B600BA7EC9B033BB0300715032836573382445435A743CA61E923B18ADEC7CFAF10ADE908E582560EE91ACA012942319B4888109E55AA738A7BCF777C92B4B09A50A1C043C982C2C2357F73C1687B35BD123FC905E1A719353466A42B915DBF1A1750339BF0923419681E4531D97E2160AD896DB056570570510FB711169AF2DE0CBA51C5F5056242965AD429301E7020AE0141F845833A3FBA0B192426C001A7147C2926805CD86725442CADC2636BB769DCDE46D1BD12D30F4695593B5753870EF796FB2F3A53F283D5828B77CB75D5DE1BA25357C290A957FD501AEE0AE59D7AE97833B0BB640F781A08BD256C79117C220BDD83280A0069B29A645720096D297A2E5245439268C0ED01F75A939978372B9E05D93DA899C10BF6CDB18698C46EBE00BF90730E2EA393014461DEC6C87F17B2EE16C13B8507C6009BEE074F17367A5FC3067A28B7D804C32860EDE650E6FE85CF6E301D1B1647323199CA296ABC54D2811507572B5DFF92B54E3786D130938417624775D8534B0102B6B8006803DDB376EB830D1CA80E717BB7F260A5CA4A56BFC5DA790151725942AE7C42B2B9E385B4E0F995D4402161070B73A6BB0CDB77EF11B1286D75E315635E719088DC7909D026B198AC93BB4B6FE395843A4428F75C0C1448C605A8CABA0B8CD19CE465764B523628B3334E3885D68D5089E1A3045840C36A73AEFE7B93AB357FD8A46D7547A8EFB243E4953E67CA72CFA0B77835768AA0CD2D976820A97BC21C7033084AD45C0BF6B483ACA8A485641EB55A47BE36ABCEB96143BA90C515D5BE8513BB994CFA88FF4B3600E34C1E656877606B6280384A0F481458044C47732FA9B58195A5DFB48636E1558C56A43CB6941DEE5AEB1E27B89A7121BE166879B62BC01619A9ABE840CC678E028E9BC71CE233FD9DB8816294D71F1A080101912920534750DDE692F782BAC4D4481A0900E6BB952ADA798EE06232C200F57F76A914617914B7398A0433CD7A11B5AC09789034F39338CE567E3E7AEFE35B0C3B85D21506E8886587670761AF9BAD3261DAF22CBFC664604234B3B784EA001CC6702B9222545CFDB2965EB54678780EE3C9CC134CD2E655908D6BDF460BEE364C66D5ACCF4B492ADE9A0F3EB31995BADDE4628B67165FF6014D848541035CDA46949EC1C12FF492726A7214D1C7273FB85D5484E5A178751B56E3FB163D13A53C7B3038E09B847A8C06FF9B42E8C345CC95AAC1A09660AC1FC7A146E7845AB83390871655E604C4C009EE924AE107B61BC3664F488AC60783A1C346BD18C56CED3F03BC1B1E4075E9785F235EBC5CE6621414E77D52CEC3B2E\",\n          \"dk\": \"657004A34B4EA6B278BDC1BC94A997D86B206F88875A934042732CFAF8B3A0141FDD815F2203BD92AC478A9033126A8478FBB6453AAE005C03F60444163066EE922781D08DFB1508F547555B3027A2F75F28401A7D69A09669AC8309C3D4E4B49B214C4C76B3E4C26CED4940A325885C71883881B6C18C57BF22CB4484674A738988708FB7EC68855A96EF033B4A877038612B7B14BB3DCA791DC5CC7C85614A694D0672CB5656CA51C7B3CE11ABE1F4B790800FE7F47F97D640141702B147A3A6D99279B258CAE7899C353A66F6AF3C53C4A632BEB545B65A2724EF06CD05978E3EE20BF264A0335B21FC2137C71161A8A3AAA1A6AFABD023F58C0C393630E41561568C6669C2683B0B493A60A42889A178ACC3289BB135C891D89698C38AAE187C6E3DB16335FA61BF70C6D496B5251BCEFA9A1C95980E3810C0059C62E8838F1B0B46B4C5A2FEA19E790B2EB4C8C3A164C8BF5C89C2812E982B0F3DA0CDE958A26BD03A38C562CC67B2C07509E6742CB44C04320AA87C23C3E3A7506F26AFE94523D1B05280BA53B4ABB8C5717422D071396C6B7733A09B11CE1E6B2280F1C9215913FBA6522F90C009C0988CAAC61721993AE73DD71A551ED8431C1A8D286857455624842C4CFA80B9143CCEBF930AA1E738EFF1A46EFCC0D766B7E4AC39AD508D6CB9891DEB61B0AAC5FB9385E1D0682F786CA37C3DF1A38BDFC1162E975EB604163752CAC6C47E3BD909C53726C6D084188904CA98C743C9B5D700CBE4A809F1756DCF4C65C5A6B7A7F2725595A0C89C26381C218004B1A275701B50586A327652390FB68868CFE8084067ABC53A9A2CECC72BC625CA7751EC158F35E791008543EB202AE258C588E69E695425B9BA4FE0082ECC530EBFAB41DB23CFA8C2A63AAB11D179C91A712062536C4FF1C205287296B001121436C5F813747350C9AB63CEC0CCF7DAB3E642210517155228910C729BC9B24B138B85ED9A4678B2B4C67A73282842EA66CC458C706BF4A591BBCBBD370E09C937E396B76FE4A3B56B4CF638A5CE055CB63C1275D53B4197493A1A4309A4CCDADC3AD1F47A5E8C5C89235321028EF158094A6385C4E010D6F8CCF1C627BCB3600544B276D2AC9CC91D4BD5AD75DBCC8E7B7A981680212B5A3D395F8AA1CF2B0A23EBB63BDDC5185BE53A6C1410D0D96889A74265E3B34F4477FDF5B680D793F35C7A372B25A1F47C5875B34B80ACA2C25A0DE69D58E71856C55E37A79BC7376898C45BDAD66FD0A554D8F9BD69A525BAA4BF40B0AEFDEC66EA329ACF7B44D33C4FA248734F516BB0A69FF751A3E3D95975DC4E25194CD6F88E7264352628AF45B38A3434951FF99CBAEA812C04C354227431B01CCF2B5955B59BBB5A2BF382227D71631C541AF888232EF733A085AA1D14493C063B64E8BB28E3B7D0686CE8F942EEC58734525DBAC07159627863D97F7C198C50E9AB10E54979C394E90395E6A793C882CBA9D56179B75F11799709577F149CC93EA3A764C610EAE641F8FA2801A22B5686B335117C3C7B3D74986F70384A26A33B323787B7888CF873BE39411829D69D6E2CA2279971AE27660B5224D21015440844C457B6B9F2C50D19580489C63AE0612D423A5CC523B2D908C45907A6694A665195171A5B2FB583A5C240CADCA8F0E83E46B14052C9620D3B7EF386CE8B9A5E873B65693B0D341C6EB2D10CE5E937CFB8C4C9134401BABFEEBBAECF47113A34B9C6E011BDC78A54F2B7BF36A5FFD27563D7443F2109F02A64C421411DDB2D1404A86F793A2DE62CDC560BFD6604D4B6330BA6AA621414E8C12DC71C25652ABAF36B875DE1978DD209AB53B885206C3A1B4F8B4A0670C087CDA9CDA7997437155659255C2D024822A448CE5157CF5B6E4C495A949960886A902C79591120117C4A73CE7B380C661851E1CA9EF1973D8A9D2A191B938C4110259C4227B600BA7EC9B033BB0300715032836573382445435A743CA61E923B18ADEC7CFAF10ADE908E582560EE91ACA012942319B4888109E55AA738A7BCF777C92B4B09A50A1C043C982C2C2357F73C1687B35BD123FC905E1A719353466A42B915DBF1A1750339BF0923419681E4531D97E2160AD896DB056570570510FB711169AF2DE0CBA51C5F5056242965AD429301E7020AE0141F845833A3FBA0B192426C001A7147C2926805CD86725442CADC2636BB769DCDE46D1BD12D30F4695593B5753870EF796FB2F3A53F283D5828B77CB75D5DE1BA25357C290A957FD501AEE0AE59D7AE97833B0BB640F781A08BD256C79117C220BDD83280A0069B29A645720096D297A2E5245439268C0ED01F75A939978372B9E05D93DA899C10BF6CDB18698C46EBE00BF90730E2EA393014461DEC6C87F17B2EE16C13B8507C6009BEE074F17367A5FC3067A28B7D804C32860EDE650E6FE85CF6E301D1B1647323199CA296ABC54D2811507572B5DFF92B54E3786D130938417624775D8534B0102B6B8006803DDB376EB830D1CA80E717BB7F260A5CA4A56BFC5DA790151725942AE7C42B2B9E385B4E0F995D4402161070B73A6BB0CDB77EF11B1286D75E315635E719088DC7909D026B198AC93BB4B6FE395843A4428F75C0C1448C605A8CABA0B8CD19CE465764B523628B3334E3885D68D5089E1A3045840C36A73AEFE7B93AB357FD8A46D7547A8EFB243E4953E67CA72CFA0B77835768AA0CD2D976820A97BC21C7033084AD45C0BF6B483ACA8A485641EB55A47BE36ABCEB96143BA90C515D5BE8513BB994CFA88FF4B3600E34C1E656877606B6280384A0F481458044C47732FA9B58195A5DFB48636E1558C56A43CB6941DEE5AEB1E27B89A7121BE166879B62BC01619A9ABE840CC678E028E9BC71CE233FD9DB8816294D71F1A080101912920534750DDE692F782BAC4D4481A0900E6BB952ADA798EE06232C200F57F76A914617914B7398A0433CD7A11B5AC09789034F39338CE567E3E7AEFE35B0C3B85D21506E8886587670761AF9BAD3261DAF22CBFC664604234B3B784EA001CC6702B9222545CFDB2965EB54678780EE3C9CC134CD2E655908D6BDF460BEE364C66D5ACCF4B492ADE9A0F3EB31995BADDE4628B67165FF6014D848541035CDA46949EC1C12FF492726A7214D1C7273FB85D5484E5A178751B56E3FB163D13A53C7B3038E09B847A8C06FF9B42E8C345CC95AAC1A09660AC1FC7A146E7845AB83390871655E604C4C009EE924AE107B61BC3664F488AC60783A1C346BD18C56CED3F03BC1B1E4075E9785F235EBC5CE6621414E77D52CEC3B2EBBA283F4C993A010081E2CC571D97234472CC9858D199CF0D6E6B9BD720C2665DF0F282411F4A071489A8F618E2AE5AEF40131CAC5233D6D731522720C2FEB1C\"\n        },\n        {\n          \"tcId\": 28,\n          \"ek\": \"E1F90F4586A2A7444812451655F63852C48D2745BCC5D95C15552CA7355A216B1B5131656A95453A854DA8291046A05D96E74CC4507D31973D9606171D8405F211AC5040658411A3997CA061C3AD30EC2AE6CC79CD4C9AB1D1CB47996F02E42BD8819F62457CA5CB9923C570FC749531C61AEF02642576A04E88493AB084AFB353FC0B032AE8AEA812373A323268200FA820C88E1881F0A0CED7D9601DF56C891AC2CF6B299C553C6B1C8A470B68CFF347C2A071B26557F185B4E2138B421A9BB6DAB8FB41C5459644F08614E63C8C4BACC3DF5AB7F86C44E48239EF387217C9540DFB50002C08ED9CB631755446786D4B5BC14D16C5EF629CE2916687C40053A2CD50667CBB590F7D3A2AFD54AECBD6211C84739AB75B80A38E9F27B6D6F1BD4C838BB2706E5DA65B95498CFA61AB90169A2C06B0E79CBAE0051683221C98DA365A27C1DE417666ACCA178717934258207A51DFFA0C926B6E3DA5B084F07560D949AD615724C306EF1165A5B9616FBA84C7D71C1117BBF8296722012EFE25B29C63291D31758278430CD90E844764AC252F33135CD2137115933B38F4160FD482CBD9265C27AC3B6582FC201DEB7A52D23AA5B77BCE9B7C6D699655105B9883830D0171882612212272261A0CC9DDCBC7D3439FF3A01B0BD4B63972263D919BCC9B95018114A11BABECEA27A5BCA3DB896AA49543CC50BC07039D31135BE1354B6A2B6B4375513010CAE856B7AEF64BCE20912432C09FD18905200249D4CC250306C341CB837A96F2B67422B63C29FB8887A962A1F743F3D01795D34E277343E7577878F5A3EC02728E9238D56B2115F680AFC70BBB361B60C10FF7F4094FE240089577D59969907B9192097CC05516A7132C2477435C8BC01909B4AAE5537CA2C6AC79806B6B5F32FB688C609200F16279D9CA987B68EA83A6D6309F1230562196BA93767DF126C98E4C3A3A0BB969629BCCDCB428A333D2B96E50B814716A5479192DCC0C0E4B194AED6A169E5074EF977F689528C997C1B99B02E1B18794B56993743456214064F80CCDA66B71BC009772784AF04FB7F468E2E93E03C18778D13C72FA149C50C1C9F45167A53E09657B50BA2A19B31FA95C5C6550B14F9B931EB51C37890C95157DF4F974E3A167DC005481F945D23780B5498AC5AB80DD8ACCF2D1322D3253B9450EDA3C3B365C9EDC4A87D089AF7797B01BE716917842A4E99CE04C86A9F172062C473C203A328C10DF171FB10C97BA6B8E71271D705110C810843D658B15F2040B385B067B1CE160A4205CBD57B74926143609979F6A888EBBECB7703498A278AE963223A8AA41916A3D37D949A3E298F01CCD36A5B6E0BA9CFF38BB890AB18869B4FB7CA8C1711798CAAB2EAC01ABA26A060266A6A91BA877603E650F7D15C24F9B23C52A9C74F43150E3A1D5D25BD0326724A42572C32944DA713457CB36B14E30F72761480035423810D83721A97505668F11EB26285A1709321A1C8016DB8BB085996D1A4880BD3B1D8BF2754F3781D57BBDE68297AF710188486EB6D4AF7DE411D36787E4D945E33C45CDE051601243A1F7028AD52B3B5C7728F35DD5F8994D4B8D9FA767611A1ADEE8B38C5A7A0AA795D0A970C749A06DCE6CF1C8ED19D1F7E9F1F25538877CCEC133881C652489A84F948041\",\n          \"dk\": \"4967CD2CABA6E5B9C671732DA64B59450440532BBC0372C570341637B81346646971834CCB116C49C562D485982B3C602D723B721A8EF9A35CA6CB045F8A09AB9A176C55801901C2924874D65573F5C0B3F97C1DB4821AC3B23F7621BEBBFC4D1F924E9E0762F037904707128ED964B8B2C42B3B1BA7D101BB8C1A36E1040ADA4CBAFC2BFFAA9D12C69C01F3C65E3676C948C18C273F9EB34EB0C00682A285E6B8A514D1AEE73AB93423C187C57C286801A9AB79F2F7100FB08E03A24AB26625D972C1350B951064A0C2122179CB11914C284BB092DA4A044E2C457807CED5662D0DC23F8D8A951C9766AFFB11D3B3669826736A278FA44386CCD5519F3A04A87B0C9D693D0E505EB889CBC90785635CC08FEB4362E3B48134474B43771BAB84A9933BE0988834CB149A5C3724BB17FDA374D5B57F5260C8E60C37F440A8B3DCB5DC94B946495C025CA1258C7CA7AB56B3765C1EE0ADFD854E617AB40E26922EC667FCEB3192D01DF3D37A484239BA427823302440AA439580074D666DB14C1D1F0C9E5203822394988553C8A0925E04F5AA8B9942E6C9B0C6A942CE569F3987CFED7B7E7DE388AC6BBB7CE4C9FBB6C5D15531A558573431C6B398044F989EE581B95793279F0AB97F4355D9C566B231998C9C046C871A59C11A99B2271CA7364ED5C5A6FCC0EF27A7C147C829C69E09D01CEBDAB91F163C68EB18D382A1A081889281414DCB456CD6C2031C382771073B5621C7B60DC4B0A294C8AA62C5CDF68BB6B46692196198C1EB2FC9528B33A0B829CB9B809C010A3054230188DDFA60013375DC1C6A967146D1B77362A448E4FA97C3B72C2AF5C9A4193290630AB400CA5830024888AADB52A9D4894B5AA03322946062D523018131645B825D5BB8DCE285DF2977B96C02977BC889737C78C2A3DCC5666B652C6E8C24141516DD8520DFE84E5129AA6BF55BB1EC79C3771B029A3B91F9701677C854E4105D5A485F8CB6A5C29CB2F47A4A60281F8B1FC8BC150122B08296B45F97C58CEB743B42000720CBBE5022B7143D3E177023ACA482988135197237706C26A94B35E20DE3CC0C53CA9626F2615E4B8D581BC2656AA72A0AB9242670E6322A89489C97177E3EA1AB9C24338AA35FA272C76893053A76051F4A88DE1944FBB0AFC8E904CD1033E7DC0D0ED029A7531EB612C7B46775FDC09B54C483F6B06ED16427F50421B6F59C06FB0AE4F120C54644DD287CE3119E440AAA8E0A611AB9B52DB1B445036E2CF15BB8DC72CEF50DC3788BD85832D0C18B2685659F8A8BD55144A4EC9764109288B21113E4089E598BBA1453041C9717AB25BA5239FC54638B5A20247B9BB755A360E16F83246CA2D024CBD4BC8E966C2F102C6C02CEAABA0F92874179C8777F937D9A3CB74920BEFE6A759CC94DA0A3ADE2D739D43A99E1F06A0D6A41AAC076CA70171BD697F1CB16A3B481EABB2269B57D36599F3B734BCECAABF6D5835E365DF0261C5C11B8B5314E08EB209A8938B9AA6566E159E2472D97553972DAC5B83292EA350AE358C60FA7773B5C1AF64891C72643CBF8085176A05CB47577E50FA6D42E96C5A465E05C7DB75BE4262A7AA58090585A62363B6C989B8274C426802DE1F90F4586A2A7444812451655F63852C48D2745BCC5D95C15552CA7355A216B1B5131656A95453A854DA8291046A05D96E74CC4507D31973D9606171D8405F211AC5040658411A3997CA061C3AD30EC2AE6CC79CD4C9AB1D1CB47996F02E42BD8819F62457CA5CB9923C570FC749531C61AEF02642576A04E88493AB084AFB353FC0B032AE8AEA812373A323268200FA820C88E1881F0A0CED7D9601DF56C891AC2CF6B299C553C6B1C8A470B68CFF347C2A071B26557F185B4E2138B421A9BB6DAB8FB41C5459644F08614E63C8C4BACC3DF5AB7F86C44E48239EF387217C9540DFB50002C08ED9CB631755446786D4B5BC14D16C5EF629CE2916687C40053A2CD50667CBB590F7D3A2AFD54AECBD6211C84739AB75B80A38E9F27B6D6F1BD4C838BB2706E5DA65B95498CFA61AB90169A2C06B0E79CBAE0051683221C98DA365A27C1DE417666ACCA178717934258207A51DFFA0C926B6E3DA5B084F07560D949AD615724C306EF1165A5B9616FBA84C7D71C1117BBF8296722012EFE25B29C63291D31758278430CD90E844764AC252F33135CD2137115933B38F4160FD482CBD9265C27AC3B6582FC201DEB7A52D23AA5B77BCE9B7C6D699655105B9883830D0171882612212272261A0CC9DDCBC7D3439FF3A01B0BD4B63972263D919BCC9B95018114A11BABECEA27A5BCA3DB896AA49543CC50BC07039D31135BE1354B6A2B6B4375513010CAE856B7AEF64BCE20912432C09FD18905200249D4CC250306C341CB837A96F2B67422B63C29FB8887A962A1F743F3D01795D34E277343E7577878F5A3EC02728E9238D56B2115F680AFC70BBB361B60C10FF7F4094FE240089577D59969907B9192097CC05516A7132C2477435C8BC01909B4AAE5537CA2C6AC79806B6B5F32FB688C609200F16279D9CA987B68EA83A6D6309F1230562196BA93767DF126C98E4C3A3A0BB969629BCCDCB428A333D2B96E50B814716A5479192DCC0C0E4B194AED6A169E5074EF977F689528C997C1B99B02E1B18794B56993743456214064F80CCDA66B71BC009772784AF04FB7F468E2E93E03C18778D13C72FA149C50C1C9F45167A53E09657B50BA2A19B31FA95C5C6550B14F9B931EB51C37890C95157DF4F974E3A167DC005481F945D23780B5498AC5AB80DD8ACCF2D1322D3253B9450EDA3C3B365C9EDC4A87D089AF7797B01BE716917842A4E99CE04C86A9F172062C473C203A328C10DF171FB10C97BA6B8E71271D705110C810843D658B15F2040B385B067B1CE160A4205CBD57B74926143609979F6A888EBBECB7703498A278AE963223A8AA41916A3D37D949A3E298F01CCD36A5B6E0BA9CFF38BB890AB18869B4FB7CA8C1711798CAAB2EAC01ABA26A060266A6A91BA877603E650F7D15C24F9B23C52A9C74F43150E3A1D5D25BD0326724A42572C32944DA713457CB36B14E30F72761480035423810D83721A97505668F11EB26285A1709321A1C8016DB8BB085996D1A4880BD3B1D8BF2754F3781D57BBDE68297AF710188486EB6D4AF7DE411D36787E4D945E33C45CDE051601243A1F7028AD52B3B5C7728F35DD5F8994D4B8D9FA767611A1ADEE8B38C5A7A0AA795D0A970C749A06DCE6CF1C8ED19D1F7E9F1F25538877CCEC133881C652489A84F94804166E5248CD311286D6DD03E010391D90D76044BF498B53C9D8202A9EB643527395AA6DC620A6E9A60CF19A7B4F0FF805BDA8219522A548EE5857C3FF6060C7A2F\"\n        },\n        {\n          \"tcId\": 29,\n          \"ek\": \"602389F7CA3437B9197677CB9E9704A2BB73A7815EC1047D8D63A55CE1184EFBBBA3F701CB0C3D0D18B757BA23C6023B4D34964B66107C92C5E0AA577FB93F31FB9A73786E63E7CA4DA84215F6B05A883C19F8B0D0326025A41A98D056B70A18E6E6469EC63C80BA0B7EE330B89314838883BFA75F2C6155BAA1922FD446235CA76A634EF715776D3AA3728482C5F69931DA1FA0A406D75756D025C08DAA28EB2A226AC56988F68B54E3205C1B341528374B9B9BF07BA42BAAC34219597FDC66156155DED5A7C3F386103BB0EDA1CB1D4258CE1A971447075CAC2AF538A96F1C570014341624607A3C36BB4771DC99916EACCC04268D25DB95DC20B041B394FAB543118B74536187EA32BA1680B006652AEFFA9338FA00BC099846419630D38CE7D726C5CC84CEB9C154E9B309B6A99BC142CBD6B210408455704A3A644ABF7768E6E87B54734C1CD0C139B2292C612DDAD0AAFB239CDAB80629B91AD9585DA88A84857249E68595C0564F2A07735A76C1CAE64D28D14A191A9F0CFB709E216AF6CCC0654AC206B722A2E84BE8A0C13BA359BB1B741F191F076A7B27849D2DC146CD8456FB165157A2ADF2A45FC5647FD5213E8A105084138CF29A583A0B5BFEB0A523798085F2AC8A44745BD556D7C0959EC319563077F5D5137E8B7E9743115D390BFFB91DADDB0DA4A21433B963A15933FAE236A537837CD056F4145EEB7872E8153E0BA9702C7A20598481C630206EC359C5145BF123A18054AF7D6851B59707B010A0BC322D3E3CB0F0BBA619C0CCB5239A5D7B021007211024868DF03B4729578F029EA7DB1CE62C15F1843C16469AD144C805B5C217FB18CFC49CBEB80BDB9CCACCB181153C377997C509B6606DB808F04911FCC4CF1902035DC3016F64498C13AF1BA2C240C6567E5520FDF79A5A04634439641DD2672BA04B11931CCDD3629D860B9EC767C3AA4C9E09BE25A7752B64087B0A973E6B3278E66E89F4BCCBF3090A729467A88FA0AB1A6805C7ED45678E71BA79A85F267771E456B2D56627AD66C7CB07B8D73A69A2682C79B1C13986C07FC7986C7C3C84EA4B6A6C1691675912B2064E3598C32573B6B87E67B006E2F312F15A5AF4E237345468756390DB585514302A80746E985A104C43BA019C5DDA678047C6A48CA36A51CB3A333767BC35CA31B59CCD64A2C59AC884A8B1FB396FCD42C63057663E8A1F809709AE89AC805277E1151B92DB4F0E52923BBC093B7C6D15621D2BEC2C8DB77F25F86A62646C54C114BA03CC6154397127A033D402248383FAA8BD1980C93E123B57075633B6B643941B417890B1AA810C88461E60413264612B890FA83115D22041243C47263B57CB32BD84D839B9CB96E3777E2DC616D8ECCD1BB21D4AB00C83D4B4298A671CCC1FDDFC3E4DBAB48F1B8045D3991B3952D4799B7E1C3D31B74F9B619BF17016D143B16E593F64C08A71078C83B197BD530063D026367993E8C35305D211C2E321ACC889FC6917F70A33EC2AC4D7FA448A79A89B531BE89A59CDB8212E7715144721613619ABA2CC7E53A3D3F84908C09B26320FA3C1CC9D840F21851139236C7CECBB8B3C2681DC145AACC13B4BBB15560793364387A0CFC97C0312351F420A0892084CACAFEA4241305B12C78DF29F2FC5FFEE800216C1DDF275\",\n          \"dk\": \"7BAC37F9C7AC728C78DE12B13C0A2C1B522A837416529A93273A4A14007BADC7BBFAF03EB90A7CE4E8B87B70986806C9DDC9BFFAD06D51C081E8866E7624A373E87E8BE1A60A861650346296F8B89C7A0EC713CA9A0B9BBDBA370104A414C02DE8445B9F4314D4902EBB400226A827DFC6603812178786BB1D6A9CFD048A45B4A160333D73E344497BC526D262EB73AA9D3456375A9F237B004B2414171C6BE0F02A97E83B1F8C411FE350D9B51D42D11BB7A34F8C59AFAD18722B17B21C5253389A98B2A96E5BD798AD10AF6F27950F64638080C7F0284545BA6E2BD93BB141CA924966735C7383631EEBEBC1BFEA7160FC60C143C9359A43C3A03429D663196B723CD35FF4C465B356A77A7C5FA9408E91E08BB6F141F1A9B93B4452843873F5958AC7AB9C8A660BEE218598D09652F921E8980449A8087857B129B02A38FC664F0C21B4E95E3B400AF4B30EAF649B9C968C0AA5B2FF8781F8D01534FA5FCECA771B2A2ADD27710DB25402E94F66D37D52780E25E4317FF80F0C682641301015D7900A312C9643B336AC53C94889342B09138689E8BBA191700791197B0F804BD834064CB3446C9382E6839F25087F5DFC1C741B91A8FAA87E529E375A0CA9B424C429BBF0B6CC7027123EC211A9A5622611536290A0E30A50CBC567D08245B54224133923AE399EEE6454E858352333164510A8A510289F56B073ABAF82F5986BC52778A86253BB96E357AB4F78A7C3159DAD184B8B540BE1DB861FA1951C0C59526305FF005D741B2A312B586D75B72A6B31DE68A07DE4CFF0490A85009442390FB5F45F70F5AE3E286B20C35F55DC39DF6C7CFD992A3203718A58C2D849CFA78C4F9FB20D68E8A7B35065D3DA738F01C6986600677325AD146816137221E3C47B9376A3669CCB23A533A85B1028865BB4A99361486E789C0C42C3D36831D5E86278B48B2EF8042FAA2491B413FDD9AAC2493F44686A649BA866D30F887580857B7159161D1D009920033F9EEA92E37267CA936D0823A8CA195458731DBE45B25F46584268787D664FB7587AF4B67291255C66157CE7646B2F4B5015D75D5332C9E95470F52A9D8EB5438A390BD7254924D46C583A7892D53D42E9406E0A6063121A03F0A550F4AE2E8042D962A3AB69B223C37071F1BDD2BB1639D34041D1BFC2576B268ACDA311A5E0222DDFBC5D67A77EA0C37C90B474317195D567351280AB0E5840A3F60EA9C5162A706550877430F6882A68006A683A90B38F26C90D3FCC780C0CC7E6E88FEA5A02D912A1C485667F88AB329552A93837858235E98044A28A24E1F71435E09DE2AC2744BA2E116A136E95AED53BB906BB46FA214E218A815966214F581C1134545DD05EE4E9495AD172CB2BB199D79328E06A45A42A4EF37443EC6F57631D0959B84A1B169297A0F2DC16B13B1C43567C043237AB18BD53A8898F6ABCB0F8A012E2CE203649C4286C6D3C41346599E953B6899712D4B86BFC4B54A89B061E93B423793CDD798E6E5A1BFFD565245788B0BA6A0FA3BDD97960C9CB0A10DA892D829925FA4F85CC8FF895500933BB78B3993CC04006819FC93224D19A02E11651E5D3AB3CFA6DC1C206F6516A5CD5108D18B47960A6602389F7CA3437B9197677CB9E9704A2BB73A7815EC1047D8D63A55CE1184EFBBBA3F701CB0C3D0D18B757BA23C6023B4D34964B66107C92C5E0AA577FB93F31FB9A73786E63E7CA4DA84215F6B05A883C19F8B0D0326025A41A98D056B70A18E6E6469EC63C80BA0B7EE330B89314838883BFA75F2C6155BAA1922FD446235CA76A634EF715776D3AA3728482C5F69931DA1FA0A406D75756D025C08DAA28EB2A226AC56988F68B54E3205C1B341528374B9B9BF07BA42BAAC34219597FDC66156155DED5A7C3F386103BB0EDA1CB1D4258CE1A971447075CAC2AF538A96F1C570014341624607A3C36BB4771DC99916EACCC04268D25DB95DC20B041B394FAB543118B74536187EA32BA1680B006652AEFFA9338FA00BC099846419630D38CE7D726C5CC84CEB9C154E9B309B6A99BC142CBD6B210408455704A3A644ABF7768E6E87B54734C1CD0C139B2292C612DDAD0AAFB239CDAB80629B91AD9585DA88A84857249E68595C0564F2A07735A76C1CAE64D28D14A191A9F0CFB709E216AF6CCC0654AC206B722A2E84BE8A0C13BA359BB1B741F191F076A7B27849D2DC146CD8456FB165157A2ADF2A45FC5647FD5213E8A105084138CF29A583A0B5BFEB0A523798085F2AC8A44745BD556D7C0959EC319563077F5D5137E8B7E9743115D390BFFB91DADDB0DA4A21433B963A15933FAE236A537837CD056F4145EEB7872E8153E0BA9702C7A20598481C630206EC359C5145BF123A18054AF7D6851B59707B010A0BC322D3E3CB0F0BBA619C0CCB5239A5D7B021007211024868DF03B4729578F029EA7DB1CE62C15F1843C16469AD144C805B5C217FB18CFC49CBEB80BDB9CCACCB181153C377997C509B6606DB808F04911FCC4CF1902035DC3016F64498C13AF1BA2C240C6567E5520FDF79A5A04634439641DD2672BA04B11931CCDD3629D860B9EC767C3AA4C9E09BE25A7752B64087B0A973E6B3278E66E89F4BCCBF3090A729467A88FA0AB1A6805C7ED45678E71BA79A85F267771E456B2D56627AD66C7CB07B8D73A69A2682C79B1C13986C07FC7986C7C3C84EA4B6A6C1691675912B2064E3598C32573B6B87E67B006E2F312F15A5AF4E237345468756390DB585514302A80746E985A104C43BA019C5DDA678047C6A48CA36A51CB3A333767BC35CA31B59CCD64A2C59AC884A8B1FB396FCD42C63057663E8A1F809709AE89AC805277E1151B92DB4F0E52923BBC093B7C6D15621D2BEC2C8DB77F25F86A62646C54C114BA03CC6154397127A033D402248383FAA8BD1980C93E123B57075633B6B643941B417890B1AA810C88461E60413264612B890FA83115D22041243C47263B57CB32BD84D839B9CB96E3777E2DC616D8ECCD1BB21D4AB00C83D4B4298A671CCC1FDDFC3E4DBAB48F1B8045D3991B3952D4799B7E1C3D31B74F9B619BF17016D143B16E593F64C08A71078C83B197BD530063D026367993E8C35305D211C2E321ACC889FC6917F70A33EC2AC4D7FA448A79A89B531BE89A59CDB8212E7715144721613619ABA2CC7E53A3D3F84908C09B26320FA3C1CC9D840F21851139236C7CECBB8B3C2681DC145AACC13B4BBB15560793364387A0CFC97C0312351F420A0892084CACAFEA4241305B12C78DF29F2FC5FFEE800216C1DDF275A918B39F71BBB2C10DB35639E5FD2CE621868CC02149E029EB47899407D963007CF50F7237A97072F03F31CFD59FA8E863BCA3AF7375E0CA698FF665661C24CF\"\n        },\n        {\n          \"tcId\": 30,\n          \"ek\": \"C85428E8EA5D6D1C7E544703372498F68311C32BBC70B86F2A805FC94089A0421AD680053D5BB139EB95652ABA561B07B9C2639AC693972070F351A3FB6138FEE0A73BF63161B604D7DC0334D6C631BA25F584952045C6CB74A31581B866EB5FB69503A5E3C6F96652547968626CC9C6ACCFE9582778E928235305CC5447661A64363A9FB3CB3720868812B2A3F5E7820DDAA8BC799566773BB62B769A8C54E6B533803A48D877706303A76BA2188EA4900155728DF29E7AF050F8CA9F92B65ED59988496419070B0CD8E964B402491D134F59EB2E3995C1B3B654EA7A5628C677858208A7958C57C7CCC697677BC65091015AF9D6C688E234DEB528608A1AAF35C2CB2A14593376E1417098778439D9BD6EB31BC0B53F277BC6763794CFEB8766957F6B206D439BCDCE4466BE20B74F7A117D7102F4A2CC93B098795146CD2903D131315EB41E0DDBA6B562B8341753DBA98D1ED288D8F33148814847981C735600AF76BFD0B964521A8B3122B3B90BA8228B1C4C834C808550D7780BB33218F155752DD15713F5A32057C42313A6FF5BABD6AAA1F6F5460C774CB83C6EC16C84D4A5B8EF728E58E8B34D7C090692B73DB64BB02AC365800915D78A9452158FA210C99B608DA053F574AFFAF1A49AAA92B3058468346D9D5A6DA97A9269B4713AE17A214921AE76A90EFC5A4AE90147E154A562C1C96598DF670EEED86EB946C9B45460C63A1339BCBE23B4332A32C02A028A892478070624D35C950AC84ED7D1B5640A70A3A45BB47C39EC181F7C8039929003D4D674DE3C5C89B0678CF3B125C864E95439C20AAABD015AA52BB3C27316188A673785CF2C0C77CAAC7604439A69B46005EC646C37C510FA835CBB36C162AB7C944F8228A4FF32340C3C63FFA3CBC48C7E01D35A6DD010E211C21175171237067A614CBCF7B18CF98ED0E8B88B58555D871564B0B71B75B5D078428EE87E0379A23822BC34C00A5661602C4006BF5CA667780E81F1759CD63E6BE63CC6C07ECF44C2E11346E376733D185A281C8648A9B951E14308D7A55A0717BB5C9C3CD5C7D88636873453D6148E674093A406860028AB50FAC90349C5BBFC2A7B6A18E24655DE541356990BA1C7C465630475176A03D58059F26700CBC383A0750F5271B8D5BDD74468E98C189FE302A5A452D2E47340C2B04A0275B47229B2022198F14E674071C6568EB428B0C0748CAA0A2F8BC28FD27862687B452989B034072B69381CF73BA00F7B7C401C699BE466593918EA96C79A3980023B299595179065198F93C6D6456DD53B586E6079340952C1314A140605DD34126291396AD22F2C7820A4AC793768707E071772EA587FFB9AEB234ED7A250DB40A1DD7A3869104864392B6DFA23013C8CF8856EC8E30C0AAA748351284F67A0E3469D3CF7C28A85BAA2630907B70F7C31870E8A29DC7B908CA880BC03C3CD2CC7A57A9D48A238667776B6D9C77247ABEA49B0F6935AA9446F0F2CADCC7A809B651DB55131A7B662CE428EF89109BB33C4E423B05781C42BFB4514092D35BA6D8E77B71F6627C9D91EB3672E656A00EF64CA7551667882444DA47FEE440B0E59755FB33039FACF337CC572D7CBACD680CB882ABEB86D9A3937FF76EFAF15E6AD37597C50B3153DC8B18625508393935D2FBD49D32ECF\",\n          \"dk\": \"9FC4A82AB21F667A50692A482D59A06FF2620ACCAB62394DEDC7AA452761F38A5A499C8091498FD7A5910C1A14324685B9307236CC0773D98B36EB2136708DAEB21F8D4B713CA5456A02C1C8EC012514AE3FF6476D1C3D8D47638AB79AB83433B6592E2F1690BED646C0519EAD1CC58779786B384618604B1AD1340E178FD97A8B3EE4B009A5BA115CBC3832B92ADBA22BBA7823976683B8A4469A3BDEDB8FEF5C3582A5984CEB2CBF5A57C96B246D05BA21F08B8582CDB5632DFFB404C320A004E878E31B877C07B38B7A12142A5628D2BA4CA6680C9B9A6CF18B3B4673962533D7C503A24C0A91EC9E1F75219E573DF630724393A18A61178623224B9C4892F6A41657175D296634474F1C3BBD76E05B9C5788B9857655D142BBD8137F047A4927B923163493FB31EA5C151A4719ABD61634F8A8856A968C7538E2641D5B79CDC78971ADE70AF4F85B09770EA4E05642CBCF02F095A9149DD266809DE0911A5481CA22176BC9B0C5A7066A00434B15AFFF5C532E1042BDA52D425CA202A599D96A92D58B7B40F8A76EA34C6B45B76E6C07FA9CA20B3587DED93E94160AB0412697DAC2591530620242A9B41F37B11CE1347075271091B78EAC2221DD735DEED8189C0029099526E9DA24F234BBB423C590CC44C675AF125B021951576F5953CBD45B8F6326CA9399D3127EB430AA31F2C5154768A7E440F63738419834F41AB4A149278DE5882506085943BB6D19088FC11525973ED9132238A350F201479D9469CBB46117E7B74C0298B5E785F27384A4984452868BF5056D9A966D844ACD946073AE275D7F82C4583C08AE33499CC53D4437C3EF967F6C904B75F503330C4B19981B28CA8F26CB9F5604081B8706E657CB8E9431466CA0EBF1B71828A3238578BC2B38620C3416E84B3058174716A312C205ACFC481871588C37263B6839768C5CA575CF15058C52F39379B79D814788C19034EA17BE4969255EE767578B0B6B347FA9A52D199891B9976A7345B93C659CBD7814F5F300900581674BAB7D03585E802C8AEA7CAD748F5065039788B76813C7F991A642C44EB1F151C6A3BD96C4331F2C01EEC46A3A1BC30555BA7F2C9316F4CC7D18C9CB73962A71CD0EF32B121393041684CFE692D4834FD0B25CB0A04531E656706A7D593AA1D0FC56FA304CCA3B5BB3233E7B141B6AD1C448C35268C99815E451C2B821F034CEEBD748A72625F61B610E478099F6CCCD39BA2A050611894D5524257D9A06D3BA2BE2AC70EDE8717BF593FC159F8433CA0C54420453610DF451C4736512702A95B2B0FA5AB1B4CC3FA74B974A1354F8B2BEE35187BF049D0A8772BCC85443B04303051415324523DC1FD494268E172383F7570D366570DAC588A73FCE3104921B325E45A87BE798683A6C2D0C90A869BB2B4BBC332B7687C81A287853F7C06F596271F5596FDE93BEBA027977C788F472C114C889BA4C4D3F39C7A05A34E57A8D5C4C10A19C3A3BAB1711FBA07854824305AE9D46A6DD11331656C4267C20B511B6672C0CBBCB2D5B67973239B116035BF38C9BED8186762201CE295A5473A825C6210B70A70CC5C5E235384908104278AFE955628D80B43F13BE383A42BDEA6D05EA7CC85428E8EA5D6D1C7E544703372498F68311C32BBC70B86F2A805FC94089A0421AD680053D5BB139EB95652ABA561B07B9C2639AC693972070F351A3FB6138FEE0A73BF63161B604D7DC0334D6C631BA25F584952045C6CB74A31581B866EB5FB69503A5E3C6F96652547968626CC9C6ACCFE9582778E928235305CC5447661A64363A9FB3CB3720868812B2A3F5E7820DDAA8BC799566773BB62B769A8C54E6B533803A48D877706303A76BA2188EA4900155728DF29E7AF050F8CA9F92B65ED59988496419070B0CD8E964B402491D134F59EB2E3995C1B3B654EA7A5628C677858208A7958C57C7CCC697677BC65091015AF9D6C688E234DEB528608A1AAF35C2CB2A14593376E1417098778439D9BD6EB31BC0B53F277BC6763794CFEB8766957F6B206D439BCDCE4466BE20B74F7A117D7102F4A2CC93B098795146CD2903D131315EB41E0DDBA6B562B8341753DBA98D1ED288D8F33148814847981C735600AF76BFD0B964521A8B3122B3B90BA8228B1C4C834C808550D7780BB33218F155752DD15713F5A32057C42313A6FF5BABD6AAA1F6F5460C774CB83C6EC16C84D4A5B8EF728E58E8B34D7C090692B73DB64BB02AC365800915D78A9452158FA210C99B608DA053F574AFFAF1A49AAA92B3058468346D9D5A6DA97A9269B4713AE17A214921AE76A90EFC5A4AE90147E154A562C1C96598DF670EEED86EB946C9B45460C63A1339BCBE23B4332A32C02A028A892478070624D35C950AC84ED7D1B5640A70A3A45BB47C39EC181F7C8039929003D4D674DE3C5C89B0678CF3B125C864E95439C20AAABD015AA52BB3C27316188A673785CF2C0C77CAAC7604439A69B46005EC646C37C510FA835CBB36C162AB7C944F8228A4FF32340C3C63FFA3CBC48C7E01D35A6DD010E211C21175171237067A614CBCF7B18CF98ED0E8B88B58555D871564B0B71B75B5D078428EE87E0379A23822BC34C00A5661602C4006BF5CA667780E81F1759CD63E6BE63CC6C07ECF44C2E11346E376733D185A281C8648A9B951E14308D7A55A0717BB5C9C3CD5C7D88636873453D6148E674093A406860028AB50FAC90349C5BBFC2A7B6A18E24655DE541356990BA1C7C465630475176A03D58059F26700CBC383A0750F5271B8D5BDD74468E98C189FE302A5A452D2E47340C2B04A0275B47229B2022198F14E674071C6568EB428B0C0748CAA0A2F8BC28FD27862687B452989B034072B69381CF73BA00F7B7C401C699BE466593918EA96C79A3980023B299595179065198F93C6D6456DD53B586E6079340952C1314A140605DD34126291396AD22F2C7820A4AC793768707E071772EA587FFB9AEB234ED7A250DB40A1DD7A3869104864392B6DFA23013C8CF8856EC8E30C0AAA748351284F67A0E3469D3CF7C28A85BAA2630907B70F7C31870E8A29DC7B908CA880BC03C3CD2CC7A57A9D48A238667776B6D9C77247ABEA49B0F6935AA9446F0F2CADCC7A809B651DB55131A7B662CE428EF89109BB33C4E423B05781C42BFB4514092D35BA6D8E77B71F6627C9D91EB3672E656A00EF64CA7551667882444DA47FEE440B0E59755FB33039FACF337CC572D7CBACD680CB882ABEB86D9A3937FF76EFAF15E6AD37597C50B3153DC8B18625508393935D2FBD49D32ECFC86A41EFD315191F24D2E6BDD87433D5133D6734FBEAA9DA8043D91950000048C593627807074684B7D363441F80F6A3D185D67878702D33A4E0BDA2000F857D\"\n        },\n        {\n          \"tcId\": 31,\n          \"ek\": \"52764F398C4AED89848544114193553BB178FBFCA4883C76BE23363B3343F69C8AF0B59F7C0482A64985B50A6EEA5C700CE7BF31DAC43B82372405638010A320E2A28FE6321C0AB85BFA8723DB96E8D1508900996B27812507885C97A1E7C581E35915D0709C9CFB2BC713B305705406C522D0305F5BE51EB3272524A797ECC2AB5330538BCCA62CD51937330849138620032917ACC188E8B70DE56B43E812EF030C0B4C062B1437AAE13A355AA9B41B00EDF4151F2C964FD332F3014BEB990E19C2350050735C7B02C0936B6BD37CA18114854750E32A6790515E1B4A31B2B71F9DE4082C225679CB220522BA6235B89F2C8E231BCFC815B9EBB15534D5B352566942A742AB405194AA74B90B26B98B669DA43FEC232FCF351FA5C50F812057E44858096395CE67AEE48382063A7928CB4904D623C56974FAFBA18519BAB743168510700F715E07EB331050C4160A6AA024C126027D11C324A87BA13C3CB10E86B2212343BD0088382769B9494F769918DA7C8EED21896C75711AA34B0E3BA8527371271AAEA05C906C5CAF6B5A6A08C4998492323E04CD5E63B1857A4FA972BF8B430036A856A5F9183C4B652A9311F1901AE3A4A93D302C4E77282B04046F7AC2DF2333C21B343ADB75C369A5385B9CC70280858971716A0587B74492637DECC8130EC8418954763E39AE47BBB084103225C21F64D94F14D55B9BD2579C5BB03EE4915016C2CA416806CCCD56061F92C0B1EAEA50DBA36B76798F7B8AA8AFF2CC06B04BC3106F22845DC5D847B5D444F05803CBF062DE581E12163094A5C7131A24B9327DAFF9134F32B647D9CA52A65A91A4899D64100F3376E0D318C7B8188BFA88654AC370EB5D693C4D9D481CAA98A2C99C168A0306261165410B32A53BBBBA412B54702476101C6550C286D6615AA98D8FD95ECD88BF45CA383DB3C6239AC7AC16606877751311CE8B447D0F6B9499943706B5A46F291A57302FA4F3ACD4645DBB515868371FD79887636737E333C9B06285508832401107089176E06A2F17E19E4FF61623603E17367D33F023C2CC820961A6E3500092CA3B900BAD66C179249C75A5E08A5563BE1CE312ECDC0AD370518F66594314CF5C8BCA20948D54C70F800542BD5B64ACC9AB68FC90F48662CFFC8F801C06DE972571E8C25EBA7128E95C13011EF88240C2C9C0BA555AA1C70956C9621A600F2817435C09C75F8A08CEF2C5D31C5C5834A745249CEB7641E6B19E404865EB374507DB3394A77A0F47149896AF1C747F86C97201D8C1CC8209E571B8C4D6972ECB1CA930B15516922FF08BF0C942AB5C5D197669AF94457FC9BA5BB47DDBD3A34EE941943B0B262A63FD0922D803CA035C6C54DB71B5A5A76F46C787036BEF777296E80664F73A78175EF7DB567BDB6CD8D97D27649661C76216D99D6264A7EB274C507AA170E1C3F23CA00A04B861AAA29EC33D4584B12985AC3B36689F129F3D29B19DAA5DCE4150162A10FA835E1D89A21FE10E4B1489E457C415961648044774C49DE6908201068D13463F1A999D3103342F03020F043DF3CC2E897B8293D235E8B408F2BAB266CCB0FCE19F6779A9C7DC89E368B07BF56F29911593E78009358CEA0BA304C0C074FC86A0782F99BC38571CD1632770CBEA199AF4181984FD05FF371931\",\n          \"dk\": \"B9C74FBF945530C8B3F846A803ABA28337B79D4B282B127D727A145BD77C19D980ACF4A5B20A09D770BC96F9A32EC16ECC208126D0C755C334BFE36E0AA3B968D0BD9EB80FA4953C9B843AABC828BB044DE62AC903D655C5960F45A4133CEA1AE6C4A09D431B43B19511631483E457C9108D7F3AB090203F712BA759602FED0A68BB7C8B10DB592DC38606978C56198B7F3062A200B03FE281FF95A27ED48C630CC1699423F5C6A07F850F2280101178CA24C282A7455FAC19869B207C46540AA0E4628246777CEB02B24ABC1BFA02326B846D7A20A7E642116B97AE68219AC53712A0077E325B4677AD163A38A6B8754DA8957C52B6CA5905C43131467B86F2CB1B94891A88B79058D6C6D8DCBB2C167156DC81C59B60F66C16938935DCA2BBB80084484B1F11330B5010C2F6C5AADF352D2E5421BD5B8A77758B72A058A0A6074B3C08A9A2B0EFB7261CE38219542CA7759FABFAA924721D7EC8C11EB62976EAB8202AAF0BC082370A47735B0931CAB5D6A92F8A67184DC906856946004B991394C7F1F1B84A4C0BF3F249B73747732AC3F04C5FD94B33FFD8250AE4739FA04233B6C381F69A3B0AC7E0003613329FEF9A63F033CDD09137BE13BA7EF42F36342A1C215A5FF4AA76D044C0AAB7ACD7064BB064C9B66BD7E2C398404AF7164FD0169BC8DB926BF63DD0E2433C7C17854A7E538722E74118CACA4621A532B2FC8DD0E204DBD01B8B3C7AD1A5BC78E78E8C1265BDACB7C78A62A3E10EB7BC3E7FD25ED8827C23428E3A198CB4821B0EEA62C9154F6D305D00BD7BCA42A1BD488AC4412D39B1573ED978EB912CAB796A594C5E9AE6AE7C94C76BE61DD47354163A7E4063B8A592C91A8A6B34451AD400672603C60DDB26F1B0587BE2065362006372081C1A52110774EE4A0231842AC6E7B4345B47C6A5C42D3C2C951014A7B753A90345B0692C8A73A82D8B01589C3CEEC8ABA357931E18026604576141ABE253CA18C356757A9D0BD49C1FC1CD90364D7430B061DCAB362257F80405D35184282C8861613D6EAC28F1360B1888192B25719D7A3ADE28AB76FBAEC4DC35DE5B5CACD64A66A39824993F2FF7783CB339319998AA102369F43E841666CB41CF0D39B998267356DCAC4F0933E5E56CBD920FC56AC33BA82E80FB13C6C64932C65EC4C66E903859C4F20D1AB5825FA8322CDB41F4341811D7663A6397363212DED05DA0D76EB5308D32F4442A488665DA80F54ACD37C406FD44AAA56CB31781AD393171DD539727C6582594BFA278BD2729A9150B916A8CAE861153DEC3493E18277A766019CB556882266521536EF030BCA36115753E08762142B33C5E34A9E80716C57A03BB362F9334AC44744EBA711AD8F44C2EBC151976C49F179D24B53C30615BB42CBE8CE069714A9447D939DCBC128126C1F9AB35C222386D894B6ED50A580B3ED13ACAA18C8D49F5A5826A9D1B6530100702E2C9993C3AC5C9286AF6324EC9B8323C032884B344EAC49F21D76323DA9FCB324E8DD7AFCEE162AC14BB427C0197C4AB9AE490434586EA76AD9AA042A060973F301F5E9BC91072B61FBC27033C52DD160D424CCA566558242C9E4B145C7B5630AC825ED5B932A00381C5C94752764F398C4AED89848544114193553BB178FBFCA4883C76BE23363B3343F69C8AF0B59F7C0482A64985B50A6EEA5C700CE7BF31DAC43B82372405638010A320E2A28FE6321C0AB85BFA8723DB96E8D1508900996B27812507885C97A1E7C581E35915D0709C9CFB2BC713B305705406C522D0305F5BE51EB3272524A797ECC2AB5330538BCCA62CD51937330849138620032917ACC188E8B70DE56B43E812EF030C0B4C062B1437AAE13A355AA9B41B00EDF4151F2C964FD332F3014BEB990E19C2350050735C7B02C0936B6BD37CA18114854750E32A6790515E1B4A31B2B71F9DE4082C225679CB220522BA6235B89F2C8E231BCFC815B9EBB15534D5B352566942A742AB405194AA74B90B26B98B669DA43FEC232FCF351FA5C50F812057E44858096395CE67AEE48382063A7928CB4904D623C56974FAFBA18519BAB743168510700F715E07EB331050C4160A6AA024C126027D11C324A87BA13C3CB10E86B2212343BD0088382769B9494F769918DA7C8EED21896C75711AA34B0E3BA8527371271AAEA05C906C5CAF6B5A6A08C4998492323E04CD5E63B1857A4FA972BF8B430036A856A5F9183C4B652A9311F1901AE3A4A93D302C4E77282B04046F7AC2DF2333C21B343ADB75C369A5385B9CC70280858971716A0587B74492637DECC8130EC8418954763E39AE47BBB084103225C21F64D94F14D55B9BD2579C5BB03EE4915016C2CA416806CCCD56061F92C0B1EAEA50DBA36B76798F7B8AA8AFF2CC06B04BC3106F22845DC5D847B5D444F05803CBF062DE581E12163094A5C7131A24B9327DAFF9134F32B647D9CA52A65A91A4899D64100F3376E0D318C7B8188BFA88654AC370EB5D693C4D9D481CAA98A2C99C168A0306261165410B32A53BBBBA412B54702476101C6550C286D6615AA98D8FD95ECD88BF45CA383DB3C6239AC7AC16606877751311CE8B447D0F6B9499943706B5A46F291A57302FA4F3ACD4645DBB515868371FD79887636737E333C9B06285508832401107089176E06A2F17E19E4FF61623603E17367D33F023C2CC820961A6E3500092CA3B900BAD66C179249C75A5E08A5563BE1CE312ECDC0AD370518F66594314CF5C8BCA20948D54C70F800542BD5B64ACC9AB68FC90F48662CFFC8F801C06DE972571E8C25EBA7128E95C13011EF88240C2C9C0BA555AA1C70956C9621A600F2817435C09C75F8A08CEF2C5D31C5C5834A745249CEB7641E6B19E404865EB374507DB3394A77A0F47149896AF1C747F86C97201D8C1CC8209E571B8C4D6972ECB1CA930B15516922FF08BF0C942AB5C5D197669AF94457FC9BA5BB47DDBD3A34EE941943B0B262A63FD0922D803CA035C6C54DB71B5A5A76F46C787036BEF777296E80664F73A78175EF7DB567BDB6CD8D97D27649661C76216D99D6264A7EB274C507AA170E1C3F23CA00A04B861AAA29EC33D4584B12985AC3B36689F129F3D29B19DAA5DCE4150162A10FA835E1D89A21FE10E4B1489E457C415961648044774C49DE6908201068D13463F1A999D3103342F03020F043DF3CC2E897B8293D235E8B408F2BAB266CCB0FCE19F6779A9C7DC89E368B07BF56F29911593E78009358CEA0BA304C0C074FC86A0782F99BC38571CD1632770CBEA199AF4181984FD05FF37193132F434783F38ED277382AA17ACF5FEC87E72BEF729A63E69AF7387E9CC5BB339E01702E1228F530AC96DB053A415BE97749A109A1FD4057BA128649B17EC07AD\"\n        },\n        {\n          \"tcId\": 32,\n          \"ek\": \"662293FE760B0826256FA788B6A061039229502B6887527553496F09937F25C86D0762232C5A9C3CF9CE9AC7C43412295A180DAEF55C01053F71242257610F9F2AC7AD80A220991BA632B4DF1CCB43865C7469BB54A24A12D759943921B62563A1665F864C710B947C629B6AC169A05FEBA11DC87BA54420D35C9FBB364CBE5449E7B53A48B6361CF417CBB825AF93CB61D987442C500FF147B8703D6600CBC6789A83119C51D460EE759638D5302DB28B62A1950F6C63311917A51BAE092756D8DA55DDFA066E544FB7C71BD6D512D258CCB0B6B19B6693BEC15BDDD3706FEC949FD2B5AC81327A3349B62475F642927440C757266F8736B995C996DAE6685FDB89285377A38384EED54622052D197098E5D30C74D65F24B8C1F479A78037CD1936868801A7E688B9E8BB5BE333BA48540DE80AC9B12912F017961DF81CD02700CA6B30ED19325C7A507C5C1004652568F6240A060FF0316E1554168C5190DD32895B389580BCB7A1F5712EC3192B601A7FBC7F3EC9A3CA6106BD99B2800CB7E332CFB3824539801D87545711D6ABE408BE2963AA4AEBB76C9814B7808559831CFB84CB9DAA80104C51532B4867C731B53766932453E7058C23B27E7D63CF303ACD0D9A2F69B4A7B07360456A8EAA14C5142824DE66565335B589B01AE667C9CCF79EB5251A11305CF2349A8C9681A17B415F5190B602A5753B917F693B61A04B96D8C4B5AACD873B1B900B940F3C25E568C8293A679E2020C7B88F162B641891C9263116ED94C5A726078325955825CD697440729087C4D67F0CB126395C1E19824BAB9A1A1AABCC8D570CCA3BB4FF1B6F416A6E239926768657BE34B154129982006DD6B5048B2CC06679C27F070D7746870A976F1699802A61882B65B7D66736574C2A4065352F06C056703295291968237850FAAD14D56EAC4816AE5CA72BC59E04AB07FF4185B29B9B4A19970A81ABAA505FB6F4A3C1E632B2421EEA069EA40C450E3AC2435428A18C412F209509D83C69717963C62748C130E5D1CEC8A924F1E81BA852B3780B38B44AC17B3B70F4694E550B497F9A5DD740A2A7651085261A7840B52D3C95D2AAA899160A6AF7289127606A85233044B6F72C2261F3C2B89946053BA91C6120EDA024C70435B8917C1C2413324A179BE34EBA570C4C0762A8490CB76B021C2978B6237A792973EB994DF4714F230374D20C89C2E5A58E777075CC83EADB309EFA73ECF34771205B3B67C2BC848444A247D4F857D8C6679306AABAA3BD2D4A22E9B7B889574A68168B659B8F2C19560EA178BFDCC509996E8A992B2FBCBB6703478F6C3D6E3020AE0086DFE2408D56468AF66722123D6654C7BB1B246C35B9AC8C2CFDB35370D19C9FF173CD999BFC3BBCDAD053B00C941F8173D2621AEC1571DA391654FC50B9F99E51F26C5313B85E73506A0CA7D156012A63C51D228005B26FFB6427D8C279E9BC6A520ABDB38981D04A2B7ED5311F023572D57A01E3BCE0375A51FA5408C193AA80BBA033A20750C2A09521DD94B7F023271EF2A561089E170531DA641CFF4525233B93FCCA87617C685E00D0CB1317C540B770514A97279C04119436F872DE43A96747856339064794801FD00460DA5663ECDDC739C783608FA59F2E27E4AB3DEEC74061C16465780B59E4DC86\",\n          \"dk\": \"74996BF2C7B505FB4AB2E64D9AB83F5169CA5B062CE625184B0315D2B79945D04C818259E454099C55BE3A262B57C31EDA5B4F095169A09A8DBCA39DE41057E3CA126E4781D0503FFDDC89D24B28C3BA4BC5E9437C83A3EB8497E2956D30FCCB29AB0393EB1C1A0B7D2AE177EAF705E69A5B88726839516B096BAE87BB4FFC394FBB86967300AADB340E83CC9BDBC9830171088363BD1080A976EB9FD9A6B92C5273BD09073B8848936C8F1486AF320CA39EA592559A12B191432413C1C4977FD6DB5BB12B0DE877AB4CD63155690C31DB93B5EBBDF29B2A7E642C382478C9794AA5787B7A50CCB9850BF199798DB18861179E6020CC2FAC59925A0B6F775FB219726135237CCB90E321906314B8EAD933D7C53024733DCF076312F358EAD020A88B586DE53AF798C3A87178058207ED806AB4FBB349B953E7C13E6D2122A7C50B1EAC96803C7414DA65020A7ED62A91975BA6EE7174BE62695CA3A7AEC723CA701483B95DEED68A82DB95CE175B0F94A0160332DBE4575DB085215A9A520878FA76C6D3968A8DD9B38EF48738451417B3C835DA8B4F393ABFCA7DA6560278890C2BA8547BD71815043759B5A802130E8957398AE30BCE688CFB3B71E2A77068C88A3C42989735A180B64144817E8AE04A4DB010A3AA4FE24CC0AFC25C89266295532F4BFB89087008F7B265161410B6A72ABECA6B30921A8FF39AED231369D39D8781A7691933F12473B56683FB49CEC4A939BC221C11C11CA904647A7C2F77DBAD979080CBBB624C005488F6211CE7B9B2227A91F04749C2776E399377124FC7BAAC11215E6B75A7725417D049A94AD809711C1B78631DD7D24ED6720458914649F8B5FB6A30E3F97EDD116987655900278EC3A60C278A5132007C91416EA4AA3694A87D481B2EEB40674277783E391D5EE54110787622451D62F922A721C010921F570A095FFC00B8002DE9AC71EA7486A0DACC5CD0727DA19DAE03C9E0C6438CBA0B08F69590C83D6284C2092C2A98CA6405F9B19DEB5C52F7B5F91CB7B653688C633120FC82BB76A8A52AC0D5E68E74009F8E655A2057642A6004FAB653D6C79DFBDC3941E59921D8850399810A881BE80C974929CE9781074D50094D0A469D716B47FA72F2A3A4772A469A31284E91CFE5854F5016CF2747118280276582020D1472EAA3BED0AC192981098EC7524ABB1410A392439A7D9BF940EA19BF3E76BE94D856D5EB3F5D91A917C8886D561BA027AB7E166FDC12CFDA12C7B06AA552BC27E7B67BEE1C19E0612DA58C8CBF0A8EB2B03818D8158BFB1C09268AAB73C1D424C354C8557AC3556D3A944D203276187032771E4C668CDD1145DBF3B00393AAA19447C1FBBF5EE48DE7F07215F85D648A59B3D6602F69ADF57205DC57371CA84C1CFA4D69E32D065CA04A1BC5FFCC1FD215AE3B91936DFAB4EF9591AB18093F36CC6CF36ACC204CE517B9EE9B746E79C5D2E005F96768CB508E7FC96DD38338915C17C95C1BE072CD46A59A3D36887EC80C7B022D3A56C4E495AF71E8A40F2944AF9439241024C6496CD85942AB17359229AB2BB54A0D61301DB560F15263B0196525A2AF546C8EE2CA18F212AC8356AC4B2CB2E3FC7F9A414776A24C662293FE760B0826256FA788B6A061039229502B6887527553496F09937F25C86D0762232C5A9C3CF9CE9AC7C43412295A180DAEF55C01053F71242257610F9F2AC7AD80A220991BA632B4DF1CCB43865C7469BB54A24A12D759943921B62563A1665F864C710B947C629B6AC169A05FEBA11DC87BA54420D35C9FBB364CBE5449E7B53A48B6361CF417CBB825AF93CB61D987442C500FF147B8703D6600CBC6789A83119C51D460EE759638D5302DB28B62A1950F6C63311917A51BAE092756D8DA55DDFA066E544FB7C71BD6D512D258CCB0B6B19B6693BEC15BDDD3706FEC949FD2B5AC81327A3349B62475F642927440C757266F8736B995C996DAE6685FDB89285377A38384EED54622052D197098E5D30C74D65F24B8C1F479A78037CD1936868801A7E688B9E8BB5BE333BA48540DE80AC9B12912F017961DF81CD02700CA6B30ED19325C7A507C5C1004652568F6240A060FF0316E1554168C5190DD32895B389580BCB7A1F5712EC3192B601A7FBC7F3EC9A3CA6106BD99B2800CB7E332CFB3824539801D87545711D6ABE408BE2963AA4AEBB76C9814B7808559831CFB84CB9DAA80104C51532B4867C731B53766932453E7058C23B27E7D63CF303ACD0D9A2F69B4A7B07360456A8EAA14C5142824DE66565335B589B01AE667C9CCF79EB5251A11305CF2349A8C9681A17B415F5190B602A5753B917F693B61A04B96D8C4B5AACD873B1B900B940F3C25E568C8293A679E2020C7B88F162B641891C9263116ED94C5A726078325955825CD697440729087C4D67F0CB126395C1E19824BAB9A1A1AABCC8D570CCA3BB4FF1B6F416A6E239926768657BE34B154129982006DD6B5048B2CC06679C27F070D7746870A976F1699802A61882B65B7D66736574C2A4065352F06C056703295291968237850FAAD14D56EAC4816AE5CA72BC59E04AB07FF4185B29B9B4A19970A81ABAA505FB6F4A3C1E632B2421EEA069EA40C450E3AC2435428A18C412F209509D83C69717963C62748C130E5D1CEC8A924F1E81BA852B3780B38B44AC17B3B70F4694E550B497F9A5DD740A2A7651085261A7840B52D3C95D2AAA899160A6AF7289127606A85233044B6F72C2261F3C2B89946053BA91C6120EDA024C70435B8917C1C2413324A179BE34EBA570C4C0762A8490CB76B021C2978B6237A792973EB994DF4714F230374D20C89C2E5A58E777075CC83EADB309EFA73ECF34771205B3B67C2BC848444A247D4F857D8C6679306AABAA3BD2D4A22E9B7B889574A68168B659B8F2C19560EA178BFDCC509996E8A992B2FBCBB6703478F6C3D6E3020AE0086DFE2408D56468AF66722123D6654C7BB1B246C35B9AC8C2CFDB35370D19C9FF173CD999BFC3BBCDAD053B00C941F8173D2621AEC1571DA391654FC50B9F99E51F26C5313B85E73506A0CA7D156012A63C51D228005B26FFB6427D8C279E9BC6A520ABDB38981D04A2B7ED5311F023572D57A01E3BCE0375A51FA5408C193AA80BBA033A20750C2A09521DD94B7F023271EF2A561089E170531DA641CFF4525233B93FCCA87617C685E00D0CB1317C540B770514A97279C04119436F872DE43A96747856339064794801FD00460DA5663ECDDC739C783608FA59F2E27E4AB3DEEC74061C16465780B59E4DC8644132D7CEA4F7CB9B06AA59C4213FA6293563C4516CF033491742C389AF38643AE51639EF7F26FD2215AD11CBE1EDEB3B943D668EEEFEE13ED5B0DA3E0A5F3ED\"\n        },\n        {\n          \"tcId\": 33,\n          \"ek\": \"FF22387C34AC69033FB8CB685DF308A47B6A8C9978A152A7003B583DEB864C3C26432780D56785BB511EB2507E40BA80100A1E4FEA67A2290F26F75AAF655827E95C1624A127795D88F34927989594D228F1497CF998BBCF012ECF6156FDC63870EC49E38C29C6C35ACB89B031419A0BD835425A255400B05167758F56474D8B83466738383320E22514C69203AFF561DB2B1596C937EEB49044817949B726FDA5B46D6980F9099B17E6BBC4272F4185739A687E8171BA53497E26BBB215163FE18448F1D005E6827C2177CFA0A3CC516B48F3D1BA1484C33169963F071E13C20BEF371DD4013B1D5B0608C0C0C52241AEEB42F81B5D6BC09A6627C6846AC962328A54DA2573CB4A4D71C729A4AEC7411EACD1053B71B8D932C3AA6A759198B26516843B9C344E1A1874AA22EA88353E7C6C410121026CC75DEBA8C5E3083D83BCA0A4C3EBA5C653021AF25C6251FA486CD19AAB6A6458FA6D29C02A7FE6CB2192553193AB92857AB1141458916653432A71FA4667FAB3CD1A602AAA7294148B17C2B82F86900FF55EB4CC0A8A14940982496F417809956874077A4F256448F2B661FB0163C687CE8B08EFE14C1F4279CAF8BE239338535836FE5B12C1A8A4A9EA2BAAB732F3E36262AB028BB30EB88B7042297BF92B22757A0AB0A817DF03707892043DEB2FD8963FB369A8C76A8FFA17237EDB8CC0450B2E9726491A36FFC2B58E7C9CA803517FAC9C047C4CB056B0316C3CAB239FFAC13315A3675BE2B042938C079844F72601CB6A8353205211304DE0229A249148BA13C7E969CA28E95183863AB3F838EFA3BF9159CE989358FC1A69BE3C764C87A4FB8B1739510E7211617806ABF4C19FC48C6112F3C67AD92C342A40B7F5468A559563B2C2BBB7869B221959526B22032059F093BF3287FD652CF51C7A4233C17DE638E8C92EDB41C77ADC5ADA927BDB12A7EE2B00CB2577A6937C0EBA364F9093E8E94AA334545D1797071A85EE0BC9037B352CDA8D2D874E16B344E02894635A3B5380CDCB6674951C0399B10062429593573D39A06817E84F7D517332F92903C7CFF7651D7F1C37F648C22A872CC007BCF33A4E991CBD34069CCC6771B9582D83136F52790E74A24CC86C439AB7C5FF1088CCCB81FACAB6E1D0092D14A05DDC999AE4C006899D6FA6186940925BCB614DF83C5C95555358894A5A4ED2B1A8D8C7AF8B5C63CC917B849B4B544995635845B2F50BCADB9E25BA6AE2C10458D2C1358430A8EB27884B8EE27B6408265CD24C59F9B7B3E0CAA48D744BE36A3906F488C432634D88123A981B79A54C50237275262A5AB111FB51AFE1847C9E284A91733DE5F38C6D39A51987CDD368CB3E136D92A312F515A243F999C04495DD7B3EBB3CA1B82B3647935C77951672BC9520092D203AAD318852F93866E6F6494E695F4C7A97D035956C68B3A56C5A51298F1CB0997528376DD5CBCB5B15AA19A1A5B77B98F1355210543A0C0366F5328B2A49647021CC3246F1856847C637AF4B390B521B890C346CE5B4E3F1A51A874BA7DB1D2D8B8356BB0EDF48C9EA01177E801BFEDAB6444B4B911549BBEA1230FA6183949862A06954B04F4C9192B9EBCBA8151E20D48FA3CA8943F13734D9C4E642A7A7DD2BF360600ABD6E84E29BBA27F96C191480EB71CA68\",\n          \"dk\": \"33D925297A649E979A40147093E74DFFD1749917C75E088AB9800AAAD81ABEEB293FF88634706904220E90490D968C097E33A2925C251904871E78975E564606617D0228227DFC3613CA0BD6F1B6402476AFB25BC4D0AE47A2A401C95C721A04D0536E42DA76244908E8D0769D5524B1A96FC9548A7C7A2C6BAB0B081618F2C19ABC358A1DD18D96AACBAF943A4CC6A7FC316FC629A25346B82B42A746A9C4D27C951D363AC0669AB3D6368D619F17EA93DC2401DF36BDA6835DB6EB02DD211AC6927B918071DEFAB2B9F671DE554E727875746475EB886286A165749504B5919A39BBC4536C8154E5B7A9D3076E0A1F068B2C8305595ED2A1469321A7476D107273221C6C5C9A2224D4AE615991D67869674B3A5994255470BE7383C1FF636AD276B7B810794B3885C1788BDA26256DE13F8AC1179C3614ED27B0A7F220DC22425BB7B8B1A8AE2CA6057CA5BEB447042A43CC82543D43F6134E9C5DE6D52388AB9D4C746202EC759781B4AB0C684B4B270570B23078195D4233A71A451E196131EB8AF598BE86B1511DC2C85CB50D001150CA2A7A3692C7E2CA09C03A243B5B0176E9BEBD629B5F4CB599D15FCAE7A7D7F44566E0A4571740864633FACBAD195AADCDFBA9B322AFEF119436B96624AB410F764083395FCB4C81FE6616CA80CFFCD6A4CB9B35BD8557D0AC2A61AA1042B414AFB0020EE1006E593DF29A625E010C15F7A5DFBA4FE459060F90882EC91AF77181C3E32FB027AD397B8CDBF52D397654CF68B2335A3A75EC24C87852B846BC9FD07FD9D767396A5511402A1993C9AD9138FF800C6B6051A6B94DD2BB457E9883F2954980715706B07A15A186CA57A5F0F83A2CFA01D22C5AD79176C9AABCFC481B67989648000DAE714D49E6C5D9E0396339BA1C22216084BAA7B403DD998F0B9600BCF419698A2CC853CD145156FCB0C193928CC32631A2B3954AABCA05D43E1E4C2A3EE34815C8A0095102E8487A82397D2283562B19A5D88993AEF77B57886B1129B9F442986461A33293AD5B41AAFB25A8D8DC48F128B67B07C047B1CB52603082E422425059A585A7B84BC948D6B7FCA6A9E7BC17B44251343202EC892D2DB09F6EECB9483A03640C4C56B32A5B8AA44D847703C988B1C02FC02C7BCFA7642AA7C1B67B539708743517444D470F273A112EC87FB7145789744F5037095EB88015932EC4B3CE62A4314B97935CDC2F252CB839442D800816CD6AA0FEC81251F6CAF6C4A1E1A9C7CEDA798135639311ACDEC014E4BA0335A9919501C42A729E6E9556AB9BA1A1EC7D96E225159886F7C67F520305436864C9A70FE7272315A7CEEFFA6F39AA8FFEA3CEDBC8CEAB1318A64335C1C5530FFC89C0CC1D6F7905C5809E61363719F54FE7E8844D382BCA679554D368D3976206289B1A4BB987A6AD72B59CA2BC2D91A61902144065B762CDA31255119C9E19C579F4CD2A7620BCE0546565C0C82A35001900E614C62D2B7856142349B25E32CBAF0634BEB9D2B603B67019AC8FECEA9F5144782EE47E2EF823E9E646A253ADA9B8B809435C33F890B0D80A565C7FB5A19E5DCAA39E4983FC149BDC1B8477B808A5FC6379C7018134C73FA197F1E87E7443373D108AFF22387C34AC69033FB8CB685DF308A47B6A8C9978A152A7003B583DEB864C3C26432780D56785BB511EB2507E40BA80100A1E4FEA67A2290F26F75AAF655827E95C1624A127795D88F34927989594D228F1497CF998BBCF012ECF6156FDC63870EC49E38C29C6C35ACB89B031419A0BD835425A255400B05167758F56474D8B83466738383320E22514C69203AFF561DB2B1596C937EEB49044817949B726FDA5B46D6980F9099B17E6BBC4272F4185739A687E8171BA53497E26BBB215163FE18448F1D005E6827C2177CFA0A3CC516B48F3D1BA1484C33169963F071E13C20BEF371DD4013B1D5B0608C0C0C52241AEEB42F81B5D6BC09A6627C6846AC962328A54DA2573CB4A4D71C729A4AEC7411EACD1053B71B8D932C3AA6A759198B26516843B9C344E1A1874AA22EA88353E7C6C410121026CC75DEBA8C5E3083D83BCA0A4C3EBA5C653021AF25C6251FA486CD19AAB6A6458FA6D29C02A7FE6CB2192553193AB92857AB1141458916653432A71FA4667FAB3CD1A602AAA7294148B17C2B82F86900FF55EB4CC0A8A14940982496F417809956874077A4F256448F2B661FB0163C687CE8B08EFE14C1F4279CAF8BE239338535836FE5B12C1A8A4A9EA2BAAB732F3E36262AB028BB30EB88B7042297BF92B22757A0AB0A817DF03707892043DEB2FD8963FB369A8C76A8FFA17237EDB8CC0450B2E9726491A36FFC2B58E7C9CA803517FAC9C047C4CB056B0316C3CAB239FFAC13315A3675BE2B042938C079844F72601CB6A8353205211304DE0229A249148BA13C7E969CA28E95183863AB3F838EFA3BF9159CE989358FC1A69BE3C764C87A4FB8B1739510E7211617806ABF4C19FC48C6112F3C67AD92C342A40B7F5468A559563B2C2BBB7869B221959526B22032059F093BF3287FD652CF51C7A4233C17DE638E8C92EDB41C77ADC5ADA927BDB12A7EE2B00CB2577A6937C0EBA364F9093E8E94AA334545D1797071A85EE0BC9037B352CDA8D2D874E16B344E02894635A3B5380CDCB6674951C0399B10062429593573D39A06817E84F7D517332F92903C7CFF7651D7F1C37F648C22A872CC007BCF33A4E991CBD34069CCC6771B9582D83136F52790E74A24CC86C439AB7C5FF1088CCCB81FACAB6E1D0092D14A05DDC999AE4C006899D6FA6186940925BCB614DF83C5C95555358894A5A4ED2B1A8D8C7AF8B5C63CC917B849B4B544995635845B2F50BCADB9E25BA6AE2C10458D2C1358430A8EB27884B8EE27B6408265CD24C59F9B7B3E0CAA48D744BE36A3906F488C432634D88123A981B79A54C50237275262A5AB111FB51AFE1847C9E284A91733DE5F38C6D39A51987CDD368CB3E136D92A312F515A243F999C04495DD7B3EBB3CA1B82B3647935C77951672BC9520092D203AAD318852F93866E6F6494E695F4C7A97D035956C68B3A56C5A51298F1CB0997528376DD5CBCB5B15AA19A1A5B77B98F1355210543A0C0366F5328B2A49647021CC3246F1856847C637AF4B390B521B890C346CE5B4E3F1A51A874BA7DB1D2D8B8356BB0EDF48C9EA01177E801BFEDAB6444B4B911549BBEA1230FA6183949862A06954B04F4C9192B9EBCBA8151E20D48FA3CA8943F13734D9C4E642A7A7DD2BF360600ABD6E84E29BBA27F96C191480EB71CA68D4F2A9B485FFC544CD3DF67D23C80150AAF7A45CD946F4B7DB2B67F4F8B222536F9FF5654FDA78774498E2643E935D21412CEB49BC393532C80C47A982418F66\"\n        },\n        {\n          \"tcId\": 34,\n          \"ek\": \"11709FB3C60AA689CFC149954EE25C7071A4DFD724D0C4CAB2F4B3FF715FC16302ED2BAD6A926443A617AA2B44FE2283A660C0951A0A41C56EBBF5AC1945454B8A878E3C4FEB22C10FF5B0A3C7C177A84EFFD670622479A401A5909399F131909B349799CB116C51BD02778774764EE6B02EA64929311281FB821D074B1385A6A965E71F9A065DB2B78908048AA46118BCF95CFE0BC6F0318B1E3C4C2F895B98613DA1141170680191D896044C40EB4741D2D21E0C506410D9092F821DBDC04839C6C6C73487F9F239DAA0346563433C32B9A1F7254717A456B04F65965C614973DDB5C43B9072A120294021A21275007360940A5B8613C01E703128B9F27B037CBF88049FAAD7A10C2213F39A82510A872C7C2850D3CE5A48CBE5DA4B3547AAA3B8612F64840C00ABBB09BCB4F031EC6C2AC284A35D84796D4B23FA621C9E0CBA6E04632B8BC5D840765168C08F591871986FD44BBD78E56C30AC80E32229A0F57420872528308C52A028B1993A93003A9E40A1B75B6CE4ABB8EDC109C77B3D2A80B21F1827904C94666AB80AC9702CA78963F36510CC7CCC6AAC6E9489342371933AA4C6329926437A58CB8BD5CB64929929FE9707A13805B621A95803C7EC68A83676578C039C8F273B12C8A2C1853D3AC6169CE95364F791F7A6986EF765C5F0829FA888C8DB3FFFC3A701E3B4D8246B62B3A4FB637FF532BF152452A824AC25AC24E416CC21667B09055C77C12925E77FA67CCAF2C9602091149A730CD3F627A8613B149A2D3789C19CB6B09E5CB64E42A82628C4BB168A492529680B1BAD2A6F0F7350708030495768DD9C82D6E9812DE35FA90689087B8DF242414752AAD8E15BB3BAAFFA05A1057A8262FBB1749C9A6C4272750134F72682CA753B1E555A60D91B26728EE540132DE67958D3928C3A72CFF02BF92997B0B39F06FAA333F5987713BD113026D327AFA167B040436D9940C9AE7799A4D9CA86278EFFC846FC102625583B42234EA114B75057531A07A4E23C340E04A066F3ABD69AAEC83CAA5A88BF27A0592F5B443223864A341E2F3B21CD0262AC3B89A1E735D6B9001A1C98C12210A8E45E377A8424132A10D317BF02939B6877DEE6AE1D7818C3115076B741C307B805C4860862382D1770678B4006402B93979B2383868BA1901F50776A28978A016C4F95787E8C875F694F94E3116DD60F284158BC283B79C7CE377C3F1197AD3EEB3BDB2C8E0505250E58BA75816A21E171F9A8730FA2CB8CCA44D02902F07B6BCCDB6AF8705A78E6854935299A340E62A12418B6693A0909CE5A8F1349A993E75CC4806DFC045F4C28B4DE8C9AACA04713E62DBBE3AC857B5445D6A5F5C115D5C29318F2BA9FD126F46960DBEB3B3DE53350051980E7CD5DF450087BA97CF36B7A566F0DC1479545BCEA5C950AA4CD4EA324B04036BB6A93D8519C66B12AB2EBC05F849209A603BEC768C96587A33963BAF3A8F5D8309DB35ABBA81423C365371A9DFFA649A726C4A84604F4B893F3E30934D065CCB0383A2058C0E121C3133C7FAA4C3AB4B7321C3EFC7741400004D279C5D76AA512692774D9C319A59320BA18D5C116838C8CE96A43AB562CED703A05357CDC20AD5A2A32211A2949927F642B278E71BF94390BC90A3969017B88B8EB63FF6AC90AC92362\",\n          \"dk\": \"88A386E9E2401A9A264FABA1EA2B6A13B5BEE6207582309D39C43BD37CAF668AC6D1D36E8003B85FC37755441F94E60719E92294A88B4E72170D1992B5382B7FEB489DD93A60383A2D29316B7179B8462346F85662E1C7AC72C86A496CA1635B55B535CD4348D6C97D3F5A74FB60C6ECD77A62B83CCE9BAF0693952C86CCE257AF464033EDD5AFDB07333C2A7FF358B45097A8FD87BD5642B12E1B5EAFD73761BAC07BA748429426C98961AF89771FB709EAE812F9724697B4C970583012C2C4B12C369BDA8EDED8A1AAD2C042CC7F2FB86C74B82521C55FEEC5C1FB5087A3E1B93DC1750133386A491695428F09F0696158659817C66C86AD2BF388E2F74B848941AD5A8D790081D3A63E85683AB65499C0EC41C8FB146664CF3FB9311C3786B1D90DDB54B1390AC902F42B30C29F02C236DC313026772825FA70DFB8572C3BC5B968B143719A5B132620D811AFE27F3F096BE180817E36B3F1F78700F00AC574CD8287A25EE1CDFB844F9224C39ADB10B99044CB7009DC1A67EF397B23F2BB9AF5A0AE6833210728449026EBF20E354C14C856C930D61A07CABCC510042E079415D67F6CFA0E692344F3397D0A381CD3E56DF6398C6730C346502039FCCDCB0389DD97C07F9C22FFE5AF7D22CAF6150A7D7A4D60787A67C3582EC82AE8A62FD3A175F8E27C653CAB2A2C69F1D844B1972110B2AC3163BC119A69E4D628DF2C0C76266599D08BEB6B659176CDD07B1A50699DD92AB3EB2C787F90533C28156B8405CD46C1B6B67BE6399319A4BAC4A93FCB659C819A6EC9A1845F1C791C6915F7A40F3DC578C8E016A0E2703F776310B3BCD487B638671628BA508101800B94AF45AA0C3C49559A46979906A8BCA9A474D0AD63F035E95B918B0297440203E154A676572F21D0815BC45D3E7402E537669FFA65FD903A76219C94D275B71C3B9D69C024A17E4F0284179A5F4E668F981B7F968507FBD52797B19C89941BBCAA117786C202030C33F476656207B7C87CFDA16699842F72C5746D843501D9977F0804FEFC2D603C9BCE710547AC047F94980899BB1E167C26301FC146A6C89081BA492274C3C894936B57F94C12D1C3B47CBA740023A494C90942AE0290782850613DE10F52727CDD2204B106AE37B0BB3F3626DC41A6F0B3CD8646CB99F56F6ED137EF803E4439878C58B2EB280BB8FCB95F7021CCD41D2B1A40D6609830BB5E2401389733104A8077B3683EEAD98FE5EC5AC7679D3A2C6F227484D73B4F9E285E4B7C4F47E9071F637C5F8B713127CF2FE77378E2CFAFD148E433B2DBF74FE0475596051EF55007AEB8A59959C8B683A6B726BFD706C1F3E4A748430C01C893078178E3063F00F797AAE6394FD637AD155221C49C533977CE8014136873942114C54462B8750264248F04165853944B1F744EDA307B62998DFC0C63D5EB9D23DC2589210493A69C0B9820D12282E5689FDAFC1E1EAACBF88551D04B2983C2163C1A5F5CA647FC3288BCB5B543C89765B9203B5803A3006F5666BD73F5A44B60A5BDAB85B7C63556323EF82B45A4CBA06BEB388707873D28CC096651E817215485129154C15173B7A7AA826DF0649C96592C7AC8E1B27872C56CF0D3495CB54111709FB3C60AA689CFC149954EE25C7071A4DFD724D0C4CAB2F4B3FF715FC16302ED2BAD6A926443A617AA2B44FE2283A660C0951A0A41C56EBBF5AC1945454B8A878E3C4FEB22C10FF5B0A3C7C177A84EFFD670622479A401A5909399F131909B349799CB116C51BD02778774764EE6B02EA64929311281FB821D074B1385A6A965E71F9A065DB2B78908048AA46118BCF95CFE0BC6F0318B1E3C4C2F895B98613DA1141170680191D896044C40EB4741D2D21E0C506410D9092F821DBDC04839C6C6C73487F9F239DAA0346563433C32B9A1F7254717A456B04F65965C614973DDB5C43B9072A120294021A21275007360940A5B8613C01E703128B9F27B037CBF88049FAAD7A10C2213F39A82510A872C7C2850D3CE5A48CBE5DA4B3547AAA3B8612F64840C00ABBB09BCB4F031EC6C2AC284A35D84796D4B23FA621C9E0CBA6E04632B8BC5D840765168C08F591871986FD44BBD78E56C30AC80E32229A0F57420872528308C52A028B1993A93003A9E40A1B75B6CE4ABB8EDC109C77B3D2A80B21F1827904C94666AB80AC9702CA78963F36510CC7CCC6AAC6E9489342371933AA4C6329926437A58CB8BD5CB64929929FE9707A13805B621A95803C7EC68A83676578C039C8F273B12C8A2C1853D3AC6169CE95364F791F7A6986EF765C5F0829FA888C8DB3FFFC3A701E3B4D8246B62B3A4FB637FF532BF152452A824AC25AC24E416CC21667B09055C77C12925E77FA67CCAF2C9602091149A730CD3F627A8613B149A2D3789C19CB6B09E5CB64E42A82628C4BB168A492529680B1BAD2A6F0F7350708030495768DD9C82D6E9812DE35FA90689087B8DF242414752AAD8E15BB3BAAFFA05A1057A8262FBB1749C9A6C4272750134F72682CA753B1E555A60D91B26728EE540132DE67958D3928C3A72CFF02BF92997B0B39F06FAA333F5987713BD113026D327AFA167B040436D9940C9AE7799A4D9CA86278EFFC846FC102625583B42234EA114B75057531A07A4E23C340E04A066F3ABD69AAEC83CAA5A88BF27A0592F5B443223864A341E2F3B21CD0262AC3B89A1E735D6B9001A1C98C12210A8E45E377A8424132A10D317BF02939B6877DEE6AE1D7818C3115076B741C307B805C4860862382D1770678B4006402B93979B2383868BA1901F50776A28978A016C4F95787E8C875F694F94E3116DD60F284158BC283B79C7CE377C3F1197AD3EEB3BDB2C8E0505250E58BA75816A21E171F9A8730FA2CB8CCA44D02902F07B6BCCDB6AF8705A78E6854935299A340E62A12418B6693A0909CE5A8F1349A993E75CC4806DFC045F4C28B4DE8C9AACA04713E62DBBE3AC857B5445D6A5F5C115D5C29318F2BA9FD126F46960DBEB3B3DE53350051980E7CD5DF450087BA97CF36B7A566F0DC1479545BCEA5C950AA4CD4EA324B04036BB6A93D8519C66B12AB2EBC05F849209A603BEC768C96587A33963BAF3A8F5D8309DB35ABBA81423C365371A9DFFA649A726C4A84604F4B893F3E30934D065CCB0383A2058C0E121C3133C7FAA4C3AB4B7321C3EFC7741400004D279C5D76AA512692774D9C319A59320BA18D5C116838C8CE96A43AB562CED703A05357CDC20AD5A2A32211A2949927F642B278E71BF94390BC90A3969017B88B8EB63FF6AC90AC923625D0BB5F514CAC167BB2E2B5FE989CE88ED65315BC610D9A5BCC77BA80DFA2FF1D083E6922EF0A818308FD7FE7CF5AD3A96942442BE327B0A307685C2D4315901\"\n        },\n        {\n          \"tcId\": 35,\n          \"ek\": \"B749934F35347C7251B0359B6582502BABBEB5574F30C63568139B1CD854BE96B8A6F09069460205BA245182334F669E93F8C7D867945C3B75BF1CA810108E0F670249A62B9E6910D793C7F0A24BD3C40839D713F880B4EC6AA8AD8ACF6EB03E12FC5B1BF61ED2480A9CF68E30A441E3102611D25E9925503E704DA69393152A759DB92175E0343560B961759370BBAB159320EFA846E4A316CF8035D8EC991FC4529055A5C7F76F6FC5423AA041CC4691352A44E6E91E54DC129B000C0829C069C1538EAB1E5BEB58BF0733B64A17C5D159ED31AD4B15B36664465B6548CD4060B487C0C76890DF4B4EEC5705DE97BF47712C667023E627480A4533A9258162050F401CACD75232CF7621BB3B00F76A1CE2B42E65A91E65306F6C019C3F0C1E00FD2192E703EE5592A41C95E8263F53B16E54A952D72078A1589FCDD7BCEAB03A41D89E1D9071DB88C344F65C8D6714C367AF7C5287D6B68EA319A7DBE97F7604AFA67A27BA136C2B996B6A1C4647A8B56CB8C0D6A4BC9B33079D5B407522195ECB9FC23C778B27ABBFF026A8CB84ECB66AFDA43D40558990931736D32EC4937629FCCD01D7B14A1241D2ECB7929C443AAC703553C63D7C8410A891986C770200598F98C8AE008CA7D95CC347AC7BEC18ACD24028E6AA4DCC856BCB9D370195BCC85DD8496B62CC3A06B5A2A5961D781659C068143952C4329BBAE9A985DACAB8201B1854995FD2BC544BCA66FB927757B78207E9C44405C04002917B2BAFCAB59AA298B555717B79257AD5BC1AB1B10F92C229A9224CA16433D244770CA18A9CE12841EC38E8D131D75912C05372CF8C845E256DE2DC38283B0E2326A5DF7A3E4AA112EB91292709BA89F7672CA1BBD599859637782C7C25C61C03ED288CA6181F5E194A85C37615A1778BA51F62789614B715ADE62E85BB8490E7B7D1C81C9B578E411904D7C9C287442D406A1BD26CC759DA58FC867B0A39954F7B29702B74BEDB4C8DF87B7E57526C25C2123C1099A21CB3882C966191BAA5C89CB19B08DA6E20C588B68C954DC7CA9483B1B9FB508C694424D85C99E35AE8A084CED1C4D4B9CBB4246D28170BA94109853BB0224A2638E91FF3269AE2B013D515BD867A6BF7797B309342DA513DB1D18031BA29D72A0850669C27A1B27454B427785327225C73968FEA012D5389861B56631F89316F3B9D59C77E007B88607A53A5D7679BEB45320CD0EC605C2B0B6C7221BEFA0BBF1FD65888E5030AA18983AB1DED0979DE800DAF11B54C50971775C98BFB5B30772A6CE835021B4344D075022B4DA1A8A00E0BA140F26CC2A36F1685424642518F57217F60ACCA0B3B24BC2DFF61BF1A903210582B684178181B47116B4139E2478D37BBF006C9B8F849AF07497F493A8A0A0EF67268D8526F3D874A7EE3062E3590AA01CB09A4BB6AE73839602DFF6A0D335132864AA178C19D8D096E3094719D6ACEC6305CF3E59B190961C17B281077838E57805ABC615856C90BE8595B00C19D93B42247B925402ADB132E78C40E18C52459A542636208D0E068C12858BAA380FDB287D7E4B855716F2365354C63706EB5561C64A012F56BD64C06D2582E9CB34DCBBB837CC72C82A2CA557B328FBD9838D19A8BA5CAEF7F516F782E8BFDB53A793223A813F942BB5A6E0965E5\",\n          \"dk\": \"B325042B81C75CEBA41A96A16D9695F5842C3E6B9E7CE97B765A7B44E49B33501E72CB6F5F88A94C9A731A784A1DE1955A3337DD299DA82C5AC31AC581B46BADA8BB08C1736C785633610ECC5B68C2786D82C9A08D6277ED6914C43695F275286B5B5B0AB1C8D936AA569262B6358CA287A49E06392352706068484A14587C9B71084A35A5F8ACF78C47D34B43E84331835C4E75E17645E045ADA45D9F99C267B4345CB92241BCA8CA988353348A9DD73948A941D553BF444B0230A16F7F625DDEA463A2C49246273963BB1A9C2439B3DC18F3377A15414E775700BDC408353C7124E1BCA513AC5D415EAAD5A36D72A965357248E0AA5A8B84920C4BBE4104204449CD947484B42D49D93772C038D6554F0037BCDDCC50417025F4C0694EC7621ED82BE87BB3F3E9CDDE27388F55B730C40B722269D53C6BB388557AE11F960680F8F031443B6BA70C1CE45B329A6269F7B410D9C40ED4D18FF1B6CABFB20D89019F117032C5A2008484B078488D2AE065966C680F7098F860C896461BD1D0C2947B67431B3EBB3734664C4CF1213502E0C7826A4981FB8854A5A45B216F469C72CCA4637D9C61249232760B58D785B3B411683BCB33DBE175F9E240955A7B20D819C06C5AB567C8F289915CBC3C50204443F17EC096B399E11530505D94C93EEB9946E41362BAF7772C7B153AEA4620349BCEBB2243B85759093A98D13E5CDABF2BAA25A31464C19A358B1CCF6DCC10B5341E677C0B37C65EA52717FEB98C84470261E23FC7E02AF5D2CE086A326832174FB089B768B4701A9748225F01F98F1AC42DBD99594F34812DE15A09CAB805B695574145E0AB1D9600B18BE721E70C11978ABB91E159EAB67098A77D8F51ACE14CAAE9F3360645338F0A536450384452C89109AB3D9A5C8328B3CDF0334F748464A383883C033E673A6799678A860548FC4D47D970A8B24DA195CED6B52469AC36CE9BBD159A60FA6944014ABAF99868B963943685C7A15694D4B2BD6ED91734D601680C692314982C4445B6547CDE146B9B82C558F5ADDB84C2CFB93E91E61D4016728BF724377805F0060E3C4C943FB8AB1359979B495723C32AE096130E559604A5BB80817EC947A23785CBD7209D7B7672D1251CB234A196374364A0439623C51E997D2051C0C6630214DBB6FF1C88B1600BA11A0784D68807E830B885007C202376970B92625037677AD1B81270679349DC4CC56529B5BA205EA596AB489B8574CF668303EDA22578258739A525364A7A2C3439B6E74E9BB1A7462B2A51327719528358DA11D7BC856142A4118449F8B89C5BEBA43C117A27C65BB1E70DCF5B36A1E45416E2074BC1211F14AB957C4810C84818932653D17C4805BD6A00C80E0C0FF7C51295C670A8B6758C23A21CB21E6E097AF583AF2761AE3D62AF0EF56226E31492120B90322D76D6C1F657CFD0D2BD2F6CB4F8589012B99A73B82375C54E283C6F1DD12FD53B1DFEB540280375033B0DBA671A7D608FA3BBAF8422AACE1055A787C77506C199D19A01E1A0391798C8BB967A1C797C247C214B790B14606E3403EB91A57340557F872110C0185429AC9C454181375992D6C7C9F743DC832F2A13CCA8C8B42F63A609329DB749934F35347C7251B0359B6582502BABBEB5574F30C63568139B1CD854BE96B8A6F09069460205BA245182334F669E93F8C7D867945C3B75BF1CA810108E0F670249A62B9E6910D793C7F0A24BD3C40839D713F880B4EC6AA8AD8ACF6EB03E12FC5B1BF61ED2480A9CF68E30A441E3102611D25E9925503E704DA69393152A759DB92175E0343560B961759370BBAB159320EFA846E4A316CF8035D8EC991FC4529055A5C7F76F6FC5423AA041CC4691352A44E6E91E54DC129B000C0829C069C1538EAB1E5BEB58BF0733B64A17C5D159ED31AD4B15B36664465B6548CD4060B487C0C76890DF4B4EEC5705DE97BF47712C667023E627480A4533A9258162050F401CACD75232CF7621BB3B00F76A1CE2B42E65A91E65306F6C019C3F0C1E00FD2192E703EE5592A41C95E8263F53B16E54A952D72078A1589FCDD7BCEAB03A41D89E1D9071DB88C344F65C8D6714C367AF7C5287D6B68EA319A7DBE97F7604AFA67A27BA136C2B996B6A1C4647A8B56CB8C0D6A4BC9B33079D5B407522195ECB9FC23C778B27ABBFF026A8CB84ECB66AFDA43D40558990931736D32EC4937629FCCD01D7B14A1241D2ECB7929C443AAC703553C63D7C8410A891986C770200598F98C8AE008CA7D95CC347AC7BEC18ACD24028E6AA4DCC856BCB9D370195BCC85DD8496B62CC3A06B5A2A5961D781659C068143952C4329BBAE9A985DACAB8201B1854995FD2BC544BCA66FB927757B78207E9C44405C04002917B2BAFCAB59AA298B555717B79257AD5BC1AB1B10F92C229A9224CA16433D244770CA18A9CE12841EC38E8D131D75912C05372CF8C845E256DE2DC38283B0E2326A5DF7A3E4AA112EB91292709BA89F7672CA1BBD599859637782C7C25C61C03ED288CA6181F5E194A85C37615A1778BA51F62789614B715ADE62E85BB8490E7B7D1C81C9B578E411904D7C9C287442D406A1BD26CC759DA58FC867B0A39954F7B29702B74BEDB4C8DF87B7E57526C25C2123C1099A21CB3882C966191BAA5C89CB19B08DA6E20C588B68C954DC7CA9483B1B9FB508C694424D85C99E35AE8A084CED1C4D4B9CBB4246D28170BA94109853BB0224A2638E91FF3269AE2B013D515BD867A6BF7797B309342DA513DB1D18031BA29D72A0850669C27A1B27454B427785327225C73968FEA012D5389861B56631F89316F3B9D59C77E007B88607A53A5D7679BEB45320CD0EC605C2B0B6C7221BEFA0BBF1FD65888E5030AA18983AB1DED0979DE800DAF11B54C50971775C98BFB5B30772A6CE835021B4344D075022B4DA1A8A00E0BA140F26CC2A36F1685424642518F57217F60ACCA0B3B24BC2DFF61BF1A903210582B684178181B47116B4139E2478D37BBF006C9B8F849AF07497F493A8A0A0EF67268D8526F3D874A7EE3062E3590AA01CB09A4BB6AE73839602DFF6A0D335132864AA178C19D8D096E3094719D6ACEC6305CF3E59B190961C17B281077838E57805ABC615856C90BE8595B00C19D93B42247B925402ADB132E78C40E18C52459A542636208D0E068C12858BAA380FDB287D7E4B855716F2365354C63706EB5561C64A012F56BD64C06D2582E9CB34DCBBB837CC72C82A2CA557B328FBD9838D19A8BA5CAEF7F516F782E8BFDB53A793223A813F942BB5A6E0965E5B5E964695C24F57CD05B8BDC23949D382C7E9023CC1432BC131689528B1453B0A20ABA8A8DDC212DE825BE0D3BE57701A6B5B3A46A300D9B5945F579A59AFABE\"\n        },\n        {\n          \"tcId\": 36,\n          \"ek\": \"266B5ED5BB000FEB4C73CBCFF6E8A980326E798113223C1FD815922E1247992A2340B70B46A6CFF3638A9E741B478BC7E6C476D2F68CE244BBA73AA919C7A48770C5A9A798DB1901B2D93836F5C507D862811949CEB4BBC65CC2D6FC36E2215078149355B83B83635BEEFAADB6FAA387104213CB6609F8B36D291B3B567ADCD85EF569966E527102AA159F0912E5617E1B0C295D12838EB7790F0C4E4A93562EC21F94C517F5B6CC3F7B3F6E832F93893417A2797F9848D5333CE5741ADB867E64E898362249A2632F7ECA4BD38034BD5184BCAB71F38854A02A2C4693B32CE172C1FB41AF3A2B2AE370889C485F98587293C6758504189050CB437A991B8EA5E25569D1A9ED194D0E470A419B453120288D29763C3BAB00F5180CAB07DA070DB401CD28D69892B77A847A47C51A2D0B39A22074627CBC068D42988A60481BE5934E626201292AD073BFF81CCE044A33E3F3339C7B9446C607E666656A94A0034445635312B20A76CBC4C6763C338AA022D79A826D2841EECA89D433C5E7DC041ED488610A1F5A951FA966148D1B8597101C7CF8AC44F5183D42C689079164E90B5DC582A761571496B7A8C3543FF275D403B923B032CBB87D5EE90EF8853AE78590D0E8B200C3819705AD47E71568051C026B4B5C10760495994208AC61A08C23ABB921828A5D69740AF57507E339EF977649999330C6A9B584642F5539356590C6CB64872517996CAEF853219C5C2CC1BA04F0C8537039C9A3047DBF17CE18673B039B2A9EC083C7E53146863B61199169C9B1838CAB40508A6B709DD8502783654D55766E6B671B6C83201509395BC565B10904DC27BFE30842479528995A5888AA8E04374E2918A9EB642629A135B9306A21CB02C925BC99401EAD40285A7B3CF5A809FFF6502E727B332269E8C445C7D9303BF35C344615E19220E391AB4F2011634C292C22022AC0CD5FB51339B83833EC7404FC38108207B8A5B1416C2DD29785F3D87F0BC75F24C65864C31EDBE243EA0B8AD015A003BC9EAB583FDB5ACB32BA3BBAD6BE5A189472560B22614B5BD257249C4458C7601C8B0E48D0C72DBC5743E77350D64DD6A587B67280D04B20D1DB15275361DCA43CEC893EA5401FB3B459637817A18755876983818CC1C745CF70D2A8CBB36050601BFC34BA9685A02B6652535BA101A5193660A06CE54DB9700A977AB36E612549E51FDFAA5AB0490BA7A302DCB314ED026CA1D765D54145DA1623F9A7064D2926F25A0C9FDC4B8CEB8C765CC9B3A55A4BD79CFB9754ED5786F04395B7A80ABE92687D6900DD14C3AD743E2F9540534C63602858371C3D304263C2185E29261237A848DAE97507DCC5FDF60209112E08D269F56418F2FC875B693D5C9CABB9BC636FBCCE548227F42BB001EA6ED64057656566F51A927B178E17AAA24A0616CA362AD134AAC5D36C5447BF4DD0CB7DB95C9009B7109826B1D8ADEF95CE8B9B444848B41002426812700850CD7572A2530A2BDACB8CCCA5485032454E855116E91E6830A68EC04DF98370CDD18E12372320BB865AE818C6B32628489B59AB53B31ABBBC944861A0BD100B66FDC017AA0C79DD177CAE432024181820D23688BC991AE597981B56C3F45B5E4A92F34C7E72882C6B4F1D791C2719CF3DFBFB8F3FA04ADCC1D4FF07\",\n          \"dk\": \"24C6ABF468CF8893385B625491F2697ED724ED371DD9C76158418A6FFAC9E0A4158694AF115BB82549A0D02BCCB13282C4C37A2AFC617F347D7BC64DD6CB94C50922B8024ED4FA598B4912EE2ACE81FCCE893739678397C343C7DA4446A55687F236201E071B9C2C7715DCA6AC566702A809931B1D65919CDDB51A87B44B07629C659A104EF61BAA8702355128B9215A40E64F818C1CA297698EA14C00756B6D52974A7014E69B59E477362A63BCCD09BB474020FE51BF5337124C24C8E6F60DD2E495F3370E14A4CB1504234A4C8009B97365F166D385AC511A05EC06465D962D1BA88C191A9E28DAC4F517680D6586923BABA11BB30AC13D40466D52977929D62F48751458087830B02F5BD9B100A92DC6E602F3F24686365476C33CB9F5409856C80FA490DD6211AB9776487A3E15B33A92A347F3A92744C16DE3F8B3E942628C926BCDAB6CD80A068ED951F2689EF7066A9E2627F39C638A580441EB8FDFD9B2EA1B39D043AEEA1A0F48EA84490408818C602A828F869098FD2C429B35621DE9BC8039C6ED940BEBC32FD0C6C396B3248CFAC85C74325531A296A9C237FB2C4452457B4C43F0DAC214575A96A770170ACE6E0109E4A9C0E5F8985F4AC37CC84D206306AD7A2058EA58EA09AB3B7926941996B8125D7BD76472006C003784CF4479691BC422E00CA9976840D886DDC8A5BAFB87DB268EBD8248033703F6C6686CF928FFD736BDF3A26653319F22996C52387BF0A294944FDB1051315577B244C78578923F48B435362711B89585841FDFEC9FF047C76864055757CAC7EAC9D0F2999C341D314154DD58519B602D7855B6ED67049AE402F8913177914051E7C5A8400969F5CE9A485D95D544920AA7F169A198FA710FF73440D8949550A054562F16637D5A813C10BB29422AB193616687F062A1FA1693E3A9064982F9E96FDDB812C0763233687EE08928DC800FD2C8A3143148A0BA97633B5D97062A5B0572A89B2B265C4439357F96C01C6F464E72D0BF0CC1C421B3CD38610A9B0C89EE956AB896189C0B03AC173D31792ABD132F7174BE4D210B76E04B87E85B2E4A25F41332BC379B10D672862462F6AA3875DB6A22158EEE6B13135751DD49084862953CF275BA4580DA07921CF9482FC692643A6E9A5073A567C593AC01A57A75FC3735CD89029C99B3945A2BC7949B64377EECC96FCCFA2AC94CBE4C9AB4FC64673D042F416A80D867C9D5B3C9CB276FDB3C693181B071553B0EB66138656F33D0546322330980553BB2400D1256BECCC969937305553E8BC60C2D24AEACA90061CC7D38E3244253414222BC0D321C73EC6FD8804E71092995D48A6CD1C68CD1841011A45F4961BAB52EC176628B49CB0B840FC8B1120701227B0C92E10A1B34BB2475152BD72A82232345CFDB5B46E3403397B5D1276FE3E96703950FCFD49F1172CAF856A333497BB68C2ADAF026A7198BEE890718F474D7090151D21ECBA4B83E56069E1C6D34C5BCBFE94BF0A385702A9D2FC770D1905120A57FEF816AD941633558AEF7F96780C15112710DEDF8517CDB3314687BD4BA4C2B5A0BE253498894C7CA32401B517D812172A2B13146F30ADCBB6E79641A366BBB96B0688EB285266B5ED5BB000FEB4C73CBCFF6E8A980326E798113223C1FD815922E1247992A2340B70B46A6CFF3638A9E741B478BC7E6C476D2F68CE244BBA73AA919C7A48770C5A9A798DB1901B2D93836F5C507D862811949CEB4BBC65CC2D6FC36E2215078149355B83B83635BEEFAADB6FAA387104213CB6609F8B36D291B3B567ADCD85EF569966E527102AA159F0912E5617E1B0C295D12838EB7790F0C4E4A93562EC21F94C517F5B6CC3F7B3F6E832F93893417A2797F9848D5333CE5741ADB867E64E898362249A2632F7ECA4BD38034BD5184BCAB71F38854A02A2C4693B32CE172C1FB41AF3A2B2AE370889C485F98587293C6758504189050CB437A991B8EA5E25569D1A9ED194D0E470A419B453120288D29763C3BAB00F5180CAB07DA070DB401CD28D69892B77A847A47C51A2D0B39A22074627CBC068D42988A60481BE5934E626201292AD073BFF81CCE044A33E3F3339C7B9446C607E666656A94A0034445635312B20A76CBC4C6763C338AA022D79A826D2841EECA89D433C5E7DC041ED488610A1F5A951FA966148D1B8597101C7CF8AC44F5183D42C689079164E90B5DC582A761571496B7A8C3543FF275D403B923B032CBB87D5EE90EF8853AE78590D0E8B200C3819705AD47E71568051C026B4B5C10760495994208AC61A08C23ABB921828A5D69740AF57507E339EF977649999330C6A9B584642F5539356590C6CB64872517996CAEF853219C5C2CC1BA04F0C8537039C9A3047DBF17CE18673B039B2A9EC083C7E53146863B61199169C9B1838CAB40508A6B709DD8502783654D55766E6B671B6C83201509395BC565B10904DC27BFE30842479528995A5888AA8E04374E2918A9EB642629A135B9306A21CB02C925BC99401EAD40285A7B3CF5A809FFF6502E727B332269E8C445C7D9303BF35C344615E19220E391AB4F2011634C292C22022AC0CD5FB51339B83833EC7404FC38108207B8A5B1416C2DD29785F3D87F0BC75F24C65864C31EDBE243EA0B8AD015A003BC9EAB583FDB5ACB32BA3BBAD6BE5A189472560B22614B5BD257249C4458C7601C8B0E48D0C72DBC5743E77350D64DD6A587B67280D04B20D1DB15275361DCA43CEC893EA5401FB3B459637817A18755876983818CC1C745CF70D2A8CBB36050601BFC34BA9685A02B6652535BA101A5193660A06CE54DB9700A977AB36E612549E51FDFAA5AB0490BA7A302DCB314ED026CA1D765D54145DA1623F9A7064D2926F25A0C9FDC4B8CEB8C765CC9B3A55A4BD79CFB9754ED5786F04395B7A80ABE92687D6900DD14C3AD743E2F9540534C63602858371C3D304263C2185E29261237A848DAE97507DCC5FDF60209112E08D269F56418F2FC875B693D5C9CABB9BC636FBCCE548227F42BB001EA6ED64057656566F51A927B178E17AAA24A0616CA362AD134AAC5D36C5447BF4DD0CB7DB95C9009B7109826B1D8ADEF95CE8B9B444848B41002426812700850CD7572A2530A2BDACB8CCCA5485032454E855116E91E6830A68EC04DF98370CDD18E12372320BB865AE818C6B32628489B59AB53B31ABBBC944861A0BD100B66FDC017AA0C79DD177CAE432024181820D23688BC991AE597981B56C3F45B5E4A92F34C7E72882C6B4F1D791C2719CF3DFBFB8F3FA04ADCC1D4FF07BAF18B5A25081C8A9F526111B600954D39BADAB9044F59903D2A8F21F8E1D78B7FB950A8F51DCEC4BC7A573EDDA56ECC049E5688476BD5FD6CD076A8F99A019A\"\n        },\n        {\n          \"tcId\": 37,\n          \"ek\": \"FE97202108CD725098CDC892FA68301012B3156A2E5BD503F9D388B26B07EE099D5AB0BA6B4A5E7DF369F748BDAFC4C72B171747546B3957BCFE189E312058202885FE853966A7B3DD285E06D37000900228662910260CA1D783DCE2ACBC436BF3669C51A07FB990449AFC153C9C5630F8A2E4E4B7A8E8719A98920B104E72646F83D8C06AB631755239B4303B7AFBBA9DD72577FBB677C054B16813DCF373734A6E4D8B025B66C19936A13278B859165FCC6277E9F149FF979EFEBB9867A9124233A5107B5E29B2CAF81C84A3D05DE3DB316858C5FCB372FCF28705E2BF0B4855E6F97F17DB217AD6A7B7288310086AD129119FF88D20B35F2F287B1358A851185BC12046D5079A2CB3CCAF98BEB9E7C1E82A841F7B48B4F57B3B17467441865288BC7020CDB1B7564989AA3F47BAB9B0919C6819E671235CD7B39F39C71C870FF6A72612CB1BC58A86F565AFA6932C71F059B2F72BA83A8985DC26D91A0CCD49BD32080D30EAC67D96A061E906E2B15E5F43215A9160AD4682A27A2B1B9C3ED1C697CE7AC6A4B2B3963B9A99441FA9D6337739AEF5891D773B4C095C8E97A9BBEA918EFB6058AFBB2A63318972400B11B9523FD335E118CC1FA0BD0B03C8C3278787F9B7D1F752BF5B739FFB0EB0654E753C2F955351243C233E9CB149D49AA1F1459D61B03E452192B64AFD642D00800B4B815D0B9863CE2A4A8AF4C6E7BB3F845A32FD95184812896105946B7A8ED80A5ECAAB0348DC4D2C4C2B7DBA3D1EE6B6A9D35CF03A4585169FBEA45101B97BAD583F9CEB486612761A25AB393960DDF3C1A4E23E23134CE37B7E6833822AF3B278B1B69D27A4ED72221C055C7F006F3DC78BDD3C86759260E31C8D0503954EF31292EB1C8A877A00271109998D9A1A026C16482316994B2573D1673C5B727E75024786C1742414B746C1B2AC74B71920A2C79B0456C08BF1E519CF28AA70627A6A5C8496D9B061E4439B75080A763E50C1A4B32483E87AA5E67C25A8A7CE6FB864012C6A32535DB7123D24F7CA62750A18E96911EA0AD2ECABB7B02252D44DEBB2C55D552349D3798EF7812B991755A278CE158185171B696859968492CAD792DC82767E53333AF9812AA55C443959D14B1C385452367BCFB5A5955163B287254B9C4B0380EB818912BB175605EA55585F82AFEC62C2CB46BE6AC0CB99443E5676AD21EA08A467006E67C5FEF61193F3A54C4C5DE6A4314ABC3F62A83261A4B56AF18172B4A85C00C56CF28EA5C6BFEC6AA703530B7E641E792C35D1F1BBB1914D15708B88B87D161524B44B12167873C19237CAD32BB7C72B0CDB04C516142634101FD81B2B12ADBFE27512C0A091523732B82AA278CA6AD1AF28A03ACCFA28D591040FC950AE475976C5A7E192A9044099A4AC487D5A39E121B9A776CB190A2CA499403C3A5F9F027C49F627E6446D8FB53252E1B098AC8A5DD59EFAA22D2F57C83809003CE2B562231140542AF5D561ED7C74B4E145820CBE387254D78C79A8F6C241F10D94E26454764876631811492221B09B9C2A6C75520D03EA79CC129862A10AC2E0288446AF46165BD1F614C9D1C3606C9C8669C334534266A1946AFC0C580C19283CA1B59C88AD6B8ACAAFD7C3328CE3583325ECDD4B101DC28053788ECDDB4CB7A4B66997F9FD0995\",\n          \"dk\": \"D5DC5D1A0BA3C20670B1306E201A927764654F757026D15DAA35B77F5B81AFD975BE114CC9E9A31A4C5E83C28D86EBAFB1E65674EA7BFC5646EBB49C67D7B0EFD60FE283BD07D71EDD71B93C815C3CC066513A52C74081DFB0773EFB736D6C0E862CC5F11CB88FBA3AC62515C39B0692359AD457210371C65B6031745622BB68BE60337F5ECB065FA678CEA88165BC7AA4E002D3C892115A3504534F5DE12178FC17E2F513B3535813D30EFCCA68018128C5C3181225CACB1C76BF7A6E98195E0B793B78B0305121087A83A37D1989D0145BB4F11B5E579075B6C5AC845949DA2EA03BBDA446A6C7F5B88C8913454A605179004BD47BAF446BAA4705743A0B6E2833011CB4069664E80C58338C71BBF1C72CC89F77849F6FF354B280044DE60B6D16C31355B4AFA199FDBA078B018AAEF88D046B927ADB64935152273B16127701B0093EED2439CC89C3DA9A277D1AC5CAF65B48262A48521361192168425FB1C341D180CABCD7406154CF7F647F0A62700C630F093649E98936D12A1FFE00A368A2622D4579F521B57BB1510D6CC6D35C48AA96B07079C7C6E4265D768E4EF2C9FA463052E716B48053EE0521227544384C7AD1673389DC34029C02DF55199328376A4C07CE6C8AED221DFDB844F770282E537F15A83A5BE8873AA52FD17C324C6C4B763A7D6A3121AB384814D20B879A42E3149BDC58CE709B3DC6E87B6EC03375CB15584C0DFA6A05F48935DB376096293BFF5A311D4373A95AAC117A25C1FA95EB1C21658A2D313A3343F643969148B11727BA64882B8AB3EC772909EB9690FC72A8F1C6B249191317610F81999B161ACE6351AD330E89A18FA0986514848E80AC95BE41BAE18C1885D44CA30047FDC152EFB069D589296AF20337E12A1093CD54BAB36D252AA2A674A7C065A5676CCF507C1A07A2CC471C612500E124573C865F9C3C6D59AA02F54094141B5A3E8BB9CB7A7400A42AE4867440399306627AE9112DB909A1D6820C26ABA62428BCF27842F30BCB97D241D161CD17E8C3789BB94EE60238B2C987601F694A1BBA38581C261DBE2B94A89556F12620141889A7CAA997036D7693BC5EE961D06A1324EB7520953AB7E3BD00FB2B4E306312A6570277C71BF35DF403442325789846AC203B905F28127B3947780201D5B57B6BE7CAC796076D04B838A222C4F08004F585BF79645FE11EF52749B8C0A5656798F8F8BCDA89C29CF1B10B4429F8B65974164C1653B0B6EC8DCF39961A7B82DBE28604C579F3109F94577B42A4574AA36F2778CD424C6238996F4954605C7B25B50C455AC334D73343D90AC6A222324DF6BC4766B3D2C2A04C92A54F084DED24A9A4D5012AE741F12318D283B1CD668375129274149F33175C2E19453F3B4E60A1089E0647D37B0721254C86720DA0A97697B45C4CD5360AC513D4261BDF738A08F1BEA818C722367C30788EA2361384A02528E1423F4143C525B8C9A89A1A574A0D45AB9A8C4E02B01B894634C29A3FF9B1C1BADA025E8A99F38A092CE903CC03411FD25CE61110A4B423A01699BD922353FB65D8E796F4B77F226895EB822552F7AE0CB7C22788429F8132F158C824319030E770D6B9ABBBB912F3E592987826FE97202108CD725098CDC892FA68301012B3156A2E5BD503F9D388B26B07EE099D5AB0BA6B4A5E7DF369F748BDAFC4C72B171747546B3957BCFE189E312058202885FE853966A7B3DD285E06D37000900228662910260CA1D783DCE2ACBC436BF3669C51A07FB990449AFC153C9C5630F8A2E4E4B7A8E8719A98920B104E72646F83D8C06AB631755239B4303B7AFBBA9DD72577FBB677C054B16813DCF373734A6E4D8B025B66C19936A13278B859165FCC6277E9F149FF979EFEBB9867A9124233A5107B5E29B2CAF81C84A3D05DE3DB316858C5FCB372FCF28705E2BF0B4855E6F97F17DB217AD6A7B7288310086AD129119FF88D20B35F2F287B1358A851185BC12046D5079A2CB3CCAF98BEB9E7C1E82A841F7B48B4F57B3B17467441865288BC7020CDB1B7564989AA3F47BAB9B0919C6819E671235CD7B39F39C71C870FF6A72612CB1BC58A86F565AFA6932C71F059B2F72BA83A8985DC26D91A0CCD49BD32080D30EAC67D96A061E906E2B15E5F43215A9160AD4682A27A2B1B9C3ED1C697CE7AC6A4B2B3963B9A99441FA9D6337739AEF5891D773B4C095C8E97A9BBEA918EFB6058AFBB2A63318972400B11B9523FD335E118CC1FA0BD0B03C8C3278787F9B7D1F752BF5B739FFB0EB0654E753C2F955351243C233E9CB149D49AA1F1459D61B03E452192B64AFD642D00800B4B815D0B9863CE2A4A8AF4C6E7BB3F845A32FD95184812896105946B7A8ED80A5ECAAB0348DC4D2C4C2B7DBA3D1EE6B6A9D35CF03A4585169FBEA45101B97BAD583F9CEB486612761A25AB393960DDF3C1A4E23E23134CE37B7E6833822AF3B278B1B69D27A4ED72221C055C7F006F3DC78BDD3C86759260E31C8D0503954EF31292EB1C8A877A00271109998D9A1A026C16482316994B2573D1673C5B727E75024786C1742414B746C1B2AC74B71920A2C79B0456C08BF1E519CF28AA70627A6A5C8496D9B061E4439B75080A763E50C1A4B32483E87AA5E67C25A8A7CE6FB864012C6A32535DB7123D24F7CA62750A18E96911EA0AD2ECABB7B02252D44DEBB2C55D552349D3798EF7812B991755A278CE158185171B696859968492CAD792DC82767E53333AF9812AA55C443959D14B1C385452367BCFB5A5955163B287254B9C4B0380EB818912BB175605EA55585F82AFEC62C2CB46BE6AC0CB99443E5676AD21EA08A467006E67C5FEF61193F3A54C4C5DE6A4314ABC3F62A83261A4B56AF18172B4A85C00C56CF28EA5C6BFEC6AA703530B7E641E792C35D1F1BBB1914D15708B88B87D161524B44B12167873C19237CAD32BB7C72B0CDB04C516142634101FD81B2B12ADBFE27512C0A091523732B82AA278CA6AD1AF28A03ACCFA28D591040FC950AE475976C5A7E192A9044099A4AC487D5A39E121B9A776CB190A2CA499403C3A5F9F027C49F627E6446D8FB53252E1B098AC8A5DD59EFAA22D2F57C83809003CE2B562231140542AF5D561ED7C74B4E145820CBE387254D78C79A8F6C241F10D94E26454764876631811492221B09B9C2A6C75520D03EA79CC129862A10AC2E0288446AF46165BD1F614C9D1C3606C9C8669C334534266A1946AFC0C580C19283CA1B59C88AD6B8ACAAFD7C3328CE3583325ECDD4B101DC28053788ECDDB4CB7A4B66997F9FD0995CEFB593C11ED360F404732EA8B6542FA9796F2AEBB4C61EEA40B6D8A599C7F1351D509CF26799741631099039F713B22551E2B0F0297BB809DF0CC8FC3E47EEE\"\n        },\n        {\n          \"tcId\": 38,\n          \"ek\": \"85787649612C798187CD986E9578582F607DAED85C09B863A5476626016F88212EF2B70D81145C671162741C70C9DACF566A1B68025A7B66BAC9EB5ED8879D4CD40237D9C791B0A7E6C1C08A7354424B71D6739192B3BE48755CA513170BF3A6197400FBF94E9A707FE6D007E4062BA2D1BFF9192E294A981AB19839120401653208618C6DB25E40A28B8DD1A9E6D91A1408191C8C6638B04D2A50983C6B0640B934E5039729885DAECB0BCED2784B998CC296980626979DCC99A5726C4C07A6CAE1578089BAD68190AFA91491375C1C00624363C4A049A086794A046806117B36FC8BA8D38AA8097C727E3C59A6378703749C97D35CED818DB691783691C138C325541A1ADEE01CAC4800ACB2B008A0A5976C90B6BB8AC55B06B693390C30905866AF2014AB5D340A4EECCB4F91C0A2C700A68C67833C6FE10927D6CA935D8071F98BA31A1AB4B73128CB92BC1B8056372434A7B9524D95AD50508998A4AB179B0EB79C71392515E29BBD09682183394838A52734409EBEF5852DF7B9073098ACE127A7C95FFE48C30599540161C5D12CBF7BE203CD5AA4D861BF6768CBDB5C0401F70E4AC7B0ADC0305FB702C023AF65981A61E11A0B12C169E3AD569C90C417865F708C2A7B4B1C1866381C7EA6D17F4ED35730A84E5AFA975C728DE8B593D92251AEAA5841D8CE9A3037C1F47F8529CDE366BFC08C2A7337B8DEDA345008C43A5A901DF5C6B201B553F07FA799317A216205634130A2ADE04808032B0DC1E715C1B263C8FC07B251B14B9B1EF422C1BB171CA7D281C706A0251911ED345340DA4035D8947B058FE1D38F6EE02548B8056A24501E3B65B2568802417E98567322959EF118AB0C471B37496424FA327751C1DB40AD909424B66C811118A958782F3E57351C24934650B5A110B68C581C26CB7E634A283361B94D544E0EC713FCF52640CB180F25BD40D4114249828D5255B7EB9A1CF6589ED4BDE602317B4909950606F9C91E59E1B68599517A089466D939B43BA14B0A91987662FA63C27E1411DA78255111B961B45366515022421FBA4A04F48244449BCFEAF630CC4397A98226BC0B2A30778743EC2D2A9747A2127889794637F780A0ABC6122C79FE0830691A7EC277239190938F1648FD733C1761A42A84283E40BC7EFB707748CC0FAB76F5141122158B30655765257135EB2F08CB1E84067C64A503F58C8C54D765DADCC4C7181FFE1026E423B44AD717DCE8A6D765BE2A94C115434D16661F82410E7E19B8E3388972484638B6A52A058F2459414D3C57987215FD753434349BA001007940018EB49A52C78FFAF468D939C4AE585F2C1759E0A582BFDA316E53A363A1006BC26474F49C82B93EF4D7CA94A473D3788F5865B5D4323C3471888B06634AE325826144B5D9894CB59E8C6C6E3406459BCA1791FAA9743853175C48AD982F9775936FF68093AACBBCD87F6F30BDC1F19C93B2AECE6A3A4CD99ED3244323A7AF72C8598416543F50643B713AD2722BAFD092084BCF77385B6ECB2AC8F19BF9D97B353229496A0C6A5C73CCB06C2F2C5924E284EE3185D2160ED31B72D1F29B322C1DD11B5DABC5848933B4FB7B9AAFF1394AE76F624A231DB3C2DE108F5BDB1878551FF178901D67188E6B6F3A49C904A5539738F4305053044EEA5F9C\",\n          \"dk\": \"C4AC07C1B9ABFCA3BF31365671954516CB2E01E39337681D54262DB7B06E8CE8B4A98028F7176518098879083F64703207213945684CC4AB9CEC679834A35A8775509FE8CBBB279429D71A48FAA18485446A1A4FFD2996E24B76715A133C1486B2346055F22FB8666F487C44328688E5A6A6376682184B9057C9B65DF21101C3645C58393793B939C9B11C27360AE96E15A43BA3CA3A00358DFBD08A395C0A02C70FA45168BB6667A843A691A9C88FE11B79EB85F2034097166989024414770F0E2537A0F266F659591A06318947A715DB88A84BAD0434750E582A12C7C6981207239935C8529DBE0147E0E1CFB4D1BF8A6C1A699B736991CF2C88B22D466C5E481120A51F21A84877E710228766CEEC0168C2314DB6C85CAB02007875CFE3B5B613B1D50366EC450F1739712D8889D6653CDF41009CD42D3D6064DB930BEB551702813213C02110E435624920C242AEC784351228A847B1BF2C66AA1068364AA2BC674B2ABF2944018273DAEC324ABC94AB2AA5F21B318C84A77FC7A6D60570FB193A795993F2231497D8414B0082DB0A70AE81C8E5A61B461768B5653F51F31EC5E96F9041A49CA27A50598F23C575E0751E10C921A71676C2388C97FA83D481B9C466A265920DD9095005A1CDABA5B76E36AAEAB4BB58892F8A4C9590D2C96615A66F43A06FEC673C57B7376709CB759C61E37DE08C04D194A827173F5BC767FB788CE4A972EF148DADCB00B993A35C5369B3741D4C227E21112FAF923AB8C6478B856849A8B508BB4863D1C4239C0A469079CC677260FC8250C75887728B86A95F46B86E7C01214C1205C727707D9361E619CF792121F746815F9717DB937FAB2C094C90AA118C9D72801135AB87C99BBCB877915F8BA4E2E70B87303672D53EAA80A11A071C5AE307850C2864713287F976F7A465D8E4AC626B2390683ABE583E7AD4012F40778E4676BA30A163C70D5CFB5D755C0F806CCF0CCA7216BC16C2B7CA3F16239C147D81C4BBEAE635B2398E661B68EA863FEE4B94E6F2A44FB0A0BD8A75DEB9CCAF4AA14F14628EEBB7C88B63731587645B721ECA388EFB5495638863A13F4F6440C2E71636C9C1767B192932694C82740CB457D99B30C8396976A32AA5C027D3E36D0C3B09D24128B748CDB5F57BBF703E4173CA5C115B7000625FF06E280574EBB97E759302775093DB2878DB545C345B6525118D1C4458ADC9BD5F212B4D3BC77D66B9882A336231719736B448833B20F5448E759813FB856F95C60E29BA726B1D159B2BB342B737DC3FA7332B4492C583F83D9714029AE52A366C4734D29DB1E88397D22EAAD11D4A9B1FBEB7435F0B7016A400D4547A3C4B8F54B0A5E1F37082B4B751691E7CEB7C07D383705B8F42139326F0A985646B99DAA088C1BEBAB90AEC2B6B30E96229175C07AB4AB78A344A13C9A2F02B782860007A2D5611C7287410BDF0454F76B4F660B488FB94655B19B28B929B45664E6C8374F096B3B9A9A6262E2C2355A1DA73DBFA8160C5A16C08C68C647FDB408C223AB5D7D05820C3A1CF2994345305FBFC7D74CC20EE02C7C97C2FA5445AD4C9093CFAB7DE1C467768A01863347D1990A5882E355937694C0A01F7A9072A8B85787649612C798187CD986E9578582F607DAED85C09B863A5476626016F88212EF2B70D81145C671162741C70C9DACF566A1B68025A7B66BAC9EB5ED8879D4CD40237D9C791B0A7E6C1C08A7354424B71D6739192B3BE48755CA513170BF3A6197400FBF94E9A707FE6D007E4062BA2D1BFF9192E294A981AB19839120401653208618C6DB25E40A28B8DD1A9E6D91A1408191C8C6638B04D2A50983C6B0640B934E5039729885DAECB0BCED2784B998CC296980626979DCC99A5726C4C07A6CAE1578089BAD68190AFA91491375C1C00624363C4A049A086794A046806117B36FC8BA8D38AA8097C727E3C59A6378703749C97D35CED818DB691783691C138C325541A1ADEE01CAC4800ACB2B008A0A5976C90B6BB8AC55B06B693390C30905866AF2014AB5D340A4EECCB4F91C0A2C700A68C67833C6FE10927D6CA935D8071F98BA31A1AB4B73128CB92BC1B8056372434A7B9524D95AD50508998A4AB179B0EB79C71392515E29BBD09682183394838A52734409EBEF5852DF7B9073098ACE127A7C95FFE48C30599540161C5D12CBF7BE203CD5AA4D861BF6768CBDB5C0401F70E4AC7B0ADC0305FB702C023AF65981A61E11A0B12C169E3AD569C90C417865F708C2A7B4B1C1866381C7EA6D17F4ED35730A84E5AFA975C728DE8B593D92251AEAA5841D8CE9A3037C1F47F8529CDE366BFC08C2A7337B8DEDA345008C43A5A901DF5C6B201B553F07FA799317A216205634130A2ADE04808032B0DC1E715C1B263C8FC07B251B14B9B1EF422C1BB171CA7D281C706A0251911ED345340DA4035D8947B058FE1D38F6EE02548B8056A24501E3B65B2568802417E98567322959EF118AB0C471B37496424FA327751C1DB40AD909424B66C811118A958782F3E57351C24934650B5A110B68C581C26CB7E634A283361B94D544E0EC713FCF52640CB180F25BD40D4114249828D5255B7EB9A1CF6589ED4BDE602317B4909950606F9C91E59E1B68599517A089466D939B43BA14B0A91987662FA63C27E1411DA78255111B961B45366515022421FBA4A04F48244449BCFEAF630CC4397A98226BC0B2A30778743EC2D2A9747A2127889794637F780A0ABC6122C79FE0830691A7EC277239190938F1648FD733C1761A42A84283E40BC7EFB707748CC0FAB76F5141122158B30655765257135EB2F08CB1E84067C64A503F58C8C54D765DADCC4C7181FFE1026E423B44AD717DCE8A6D765BE2A94C115434D16661F82410E7E19B8E3388972484638B6A52A058F2459414D3C57987215FD753434349BA001007940018EB49A52C78FFAF468D939C4AE585F2C1759E0A582BFDA316E53A363A1006BC26474F49C82B93EF4D7CA94A473D3788F5865B5D4323C3471888B06634AE325826144B5D9894CB59E8C6C6E3406459BCA1791FAA9743853175C48AD982F9775936FF68093AACBBCD87F6F30BDC1F19C93B2AECE6A3A4CD99ED3244323A7AF72C8598416543F50643B713AD2722BAFD092084BCF77385B6ECB2AC8F19BF9D97B353229496A0C6A5C73CCB06C2F2C5924E284EE3185D2160ED31B72D1F29B322C1DD11B5DABC5848933B4FB7B9AAFF1394AE76F624A231DB3C2DE108F5BDB1878551FF178901D67188E6B6F3A49C904A5539738F4305053044EEA5F9CA8604CD90AAF5FB9BDA220814069AA00CB5B5FFB7B60E4BCC86F16ED0B49BA9B9C330AB4257D7B87C4742C6E95B66BDF805C6A145BF444836092C6B1D2C5FFFF\"\n        },\n        {\n          \"tcId\": 39,\n          \"ek\": \"AAF98BBEF422DAD045B9D3C2F134C71ACA3884604D3B87130EF84890D8870A4783DA18BD4C8B0667FB194393941C7508FE20AE5C077FD06A247DD7C19FD20940568B45938BEB39B1AE18CEA618BB8001C84DEC8CC170C12C77B6704AAE9B745083B3B00A020510A196EE2AA12195CCF650CB50600F35D61910397E645B886BFC58FB7C99C58AAD79552E13D31F05BA5904007E3B504D717156D652CB1D2353270CAFA6986846100B81C7816857AD7EB98D5CDA24E6DB3342F346AE5B162C11BB9E8C2AAF95C857277597398DA82A6DB7D7C34D7ABB1BB3155E72B0C6C811031A06D609C23306473317948E1B1AD9F7B59E22962BE33D750561146B135FF64058BC5524F26DBDE518C872A98D14862FF4599CB16C11D654F6762D531718C9B2BB51C34DDE019364F246891A3891F823AE85B6E54A4ED6A18673D4AFBC2C8C8F47C07C2B51DCFA56E5A57211914F437C2181A92723D5C09844951A0CC0470A594AB0C0E8A378A3015E39C5275E9B9D4E202ED6A4AC9AD1742C8455642932BA68C599E8883A3CB5FA11876023BD153166C7C33B3105C1BA9881F92C99CD8B41A475777F239AF2886570F94AA3626381A6B8B3C3AABDC39C2A6C0CAFF7CB95CB3162652BB0493726A81339377798D78AAE999BD9542565D5491556B716B09D0482C603B99501F8823AC6C5018543527928D2981C2957B9B7ECC33BF423F3B89DF352377DC554590A4BD6E7AEF624B873916CB638AB426A50F7789B25B420E4380413B10416C048A8069FCC62BC727AA9425700AFE835E12682215606F84C70DBAA4BD9B93D1E156913C62959C98A741A74810B996063624061B43D225B1AA71A02C50BF38016B4439FF156ACB652B278E0AAD91C455F03B29998317F596A1CB0051CFC21B1E76B59B52409164233D0C7B01A2B7C93120FC42BFB533347C2B4DA782BB55034C0A152CF51C44F39204A63C1F7867E57F27332791D8E3A1A13DB9A61BB0FE5660C6A8A2EB1365D30A83C3C1B9F03E66E58B0144CB6B292847F68CC3B0600C32FC45569158EBED0BAA50B26303707C1796BF9569193825603FB9237FC2AFBEA9D8D3B283D775583990E2672859155C70DDB7C0FEB84BC07B851F7B4BD44B84DACAC0FC913314113654567BE4370AE3179BB3BA56388A7EAFC3A1A10C25891B5A943C62A4297AD469D8040829029A8960CB9F65C21E10B64B07A713C40313E6A6EE0E939C741AA7BA93526022A8440858039A357EA2CEDB342C9299733381814E1C78FFBB1E289AA93C6ACD93C6DB2704F79922D277C9800EB3983865F65842D5BC8B346207C6C33612250110AB236DE202374132D60F5202C77367912C4FDFAC54DEC5FBC29076BA13FA013CE0FF70543B8989190AB63FA721F677FA5D0B248F2646E008830A06DDD613CB6063A8B9C37B4C263C4904E3B681EFA3568F0E429B947936EF41B2EF704751317EBE86A9597B8C0E6C81AA278FDC93F0379AFA3D1244D917401AB151A7805CE881216F208EB976FC442BB9DE86943143107B4AB479681F109AEDB28309067B9DCD6030DB4B558890FA1C5AD7FDBAA9BE39E6D6CAAED3C5286C73DEA3B6DBB940E6F647D43ABB364D92BCF099937706F8CD48F58BF77E3972CD846B20E9A1331AC0CC350080B65731270F9B2A951D93C68C98C\",\n          \"dk\": \"5C9B6453A08BA6B40F1F7C6C7CA464C76237EEDB9D74F0925D200A599684E85862AF8C29EF28871E7A55CC5824D381A9B7ACA2096B78081862D504010E1A9366000C022B3EE8AB91E6F7334287251F829C00221217823D641153B865A30F24B847154ADD5C9B4FF2350EA51D6C49A975AC2F65B541A52827DA25C93E9B36023C6129BA29E495B4914334E4D02CE21B66F1437874F1A56F03A6726C142F0B03D46543BED89D4597902C0C5B025895A04A9E22563560DB29793A353FD4528D0C6B0D0AC657358B04E7204C21A7378AAF3F0B93730C68D238695EA4CF3C9B35AE19B052B8CB2E041700902B4600BC30A846951BC0B9E42D9453B10ECC37165B08F22675657A6FC2023F0C1B42BBA175D329935B62468522BAC0944B83C5926A1C563D9323F175BB6EA9C2B1CC360FE8B60FAB81F5A1C9F31443D698B34336C0A1EB86E8E162FA21330E909CC0B69FD62738D823B6AC0462DE561DFF33094C1029ABC49381C406A316A3E2A887FFA9C3CDA9CA48E8AD802CB0231142C5F2CD83154DF65540CAFA16D39190FE100812816ACDD0720B4A44D337611651B1E505B7CAC0BF832841C573144BE42EF280B25C7B1700F344F82AB0C575B2D10C3A86449614B592CE42195F9C385E4CA257478E8D5320BAC24A68883DFC399AC236C497C27B5A80842AA018C0C0B459C2B14737197870C0E6D661510951D290B5549331E6C557DAB48F349B829AFBBEAB932E1E8638ECE2279C697220152FB84AA9DB5A8307F93438A51C22E8A1DDC54AB722A692EBCE6E719A82066E60B345C75729B4E7AD173BA7F363908411B6E724262BE0CB05F51E7739A5FE0C304690645C763CF7128DA2F71B2F7238A43003000995737A4DCFD8682BA8923C6752E53150DBD36468D6A2459A5A54450FA6D14A4295B573423E4BE95529C24D7D035C5F65C49D3684943AA505869CD525665545676A52958180538A521AF9274033BAB2C7AC83566041D9D40D92FB2DD2C147E456456DF0105264B0FB17853BF991789B6818F9BB75A381D4978D2EF7098626903C857CAB673353783C12C860258A363CAAA771ECC724B2332C1A254A3CB1547CCFFD149C187B345FA282DBB92D9793374CC67D0C0B5F946B78D81B08E521275AB7357050694C824033B9568BD335A8B837627B1494D9701181C7AA02A95DD3915E55A371F31C14F90C63075555BAAB65AA100AB99A6C1C85A1051D7E58A8E65BB312E02EB5485C05F302DDF814EED49D39E4A9BC688C989A17DF074C7FAC62FB73B0EEA85FD8177AFF41C4C3E40B407028C4F58E60DA14925385E4A0A6BA8B13C8618A8A08BF6A1630AF1B5200BA41596C9E440A5BA11C21F7C62A46379627CC4DB2E43A565A829FB605F186C405C519502C0115D9BDCB8857E03343707843A28047BDD6B445445560A686801C4D3F4C4C773537AFE7C5E2191853F56A8E75A9EA852552440B4E0642C3133FBB4CC678C2A35D093975F17BEC7972C938A7F0159A8BD8910E847DB09A1A2DE43576F9818A338787C10709F085987424C59546BA5493FE9696256C1B147105092A77A8A329563B061AF8CFEED35EA1E625F03716FA75B7839C325B94CB39C6CF1323CF4CA486F8703BAAF98BBEF422DAD045B9D3C2F134C71ACA3884604D3B87130EF84890D8870A4783DA18BD4C8B0667FB194393941C7508FE20AE5C077FD06A247DD7C19FD20940568B45938BEB39B1AE18CEA618BB8001C84DEC8CC170C12C77B6704AAE9B745083B3B00A020510A196EE2AA12195CCF650CB50600F35D61910397E645B886BFC58FB7C99C58AAD79552E13D31F05BA5904007E3B504D717156D652CB1D2353270CAFA6986846100B81C7816857AD7EB98D5CDA24E6DB3342F346AE5B162C11BB9E8C2AAF95C857277597398DA82A6DB7D7C34D7ABB1BB3155E72B0C6C811031A06D609C23306473317948E1B1AD9F7B59E22962BE33D750561146B135FF64058BC5524F26DBDE518C872A98D14862FF4599CB16C11D654F6762D531718C9B2BB51C34DDE019364F246891A3891F823AE85B6E54A4ED6A18673D4AFBC2C8C8F47C07C2B51DCFA56E5A57211914F437C2181A92723D5C09844951A0CC0470A594AB0C0E8A378A3015E39C5275E9B9D4E202ED6A4AC9AD1742C8455642932BA68C599E8883A3CB5FA11876023BD153166C7C33B3105C1BA9881F92C99CD8B41A475777F239AF2886570F94AA3626381A6B8B3C3AABDC39C2A6C0CAFF7CB95CB3162652BB0493726A81339377798D78AAE999BD9542565D5491556B716B09D0482C603B99501F8823AC6C5018543527928D2981C2957B9B7ECC33BF423F3B89DF352377DC554590A4BD6E7AEF624B873916CB638AB426A50F7789B25B420E4380413B10416C048A8069FCC62BC727AA9425700AFE835E12682215606F84C70DBAA4BD9B93D1E156913C62959C98A741A74810B996063624061B43D225B1AA71A02C50BF38016B4439FF156ACB652B278E0AAD91C455F03B29998317F596A1CB0051CFC21B1E76B59B52409164233D0C7B01A2B7C93120FC42BFB533347C2B4DA782BB55034C0A152CF51C44F39204A63C1F7867E57F27332791D8E3A1A13DB9A61BB0FE5660C6A8A2EB1365D30A83C3C1B9F03E66E58B0144CB6B292847F68CC3B0600C32FC45569158EBED0BAA50B26303707C1796BF9569193825603FB9237FC2AFBEA9D8D3B283D775583990E2672859155C70DDB7C0FEB84BC07B851F7B4BD44B84DACAC0FC913314113654567BE4370AE3179BB3BA56388A7EAFC3A1A10C25891B5A943C62A4297AD469D8040829029A8960CB9F65C21E10B64B07A713C40313E6A6EE0E939C741AA7BA93526022A8440858039A357EA2CEDB342C9299733381814E1C78FFBB1E289AA93C6ACD93C6DB2704F79922D277C9800EB3983865F65842D5BC8B346207C6C33612250110AB236DE202374132D60F5202C77367912C4FDFAC54DEC5FBC29076BA13FA013CE0FF70543B8989190AB63FA721F677FA5D0B248F2646E008830A06DDD613CB6063A8B9C37B4C263C4904E3B681EFA3568F0E429B947936EF41B2EF704751317EBE86A9597B8C0E6C81AA278FDC93F0379AFA3D1244D917401AB151A7805CE881216F208EB976FC442BB9DE86943143107B4AB479681F109AEDB28309067B9DCD6030DB4B558890FA1C5AD7FDBAA9BE39E6D6CAAED3C5286C73DEA3B6DBB940E6F647D43ABB364D92BCF099937706F8CD48F58BF77E3972CD846B20E9A1331AC0CC350080B65731270F9B2A951D93C68C98C1783913132F097618BB39BD4748B4EFE63DA07C26697F9B2F4E06CB2D27012AE18EA1C7532F706B06870D0A1047AAE33D9E1FF9E9BCBBD302D8817EB7B022A77\"\n        },\n        {\n          \"tcId\": 40,\n          \"ek\": \"D6525398362B71938C1C721695157F1A2BC24680BF6E265DE8726594C26CBAB9B040462B4DA30402D70EC050958FF5944D181374CC9A29E867C1B33CC36CBA66C66A75F44CD68112B543AEE0026D9C105EE556AD5FC2989346C7473C93E2641AA42904C7751D83CACACDA6B2175B1B9C1C41435C49027AA21119428C61482F655D4A0CCCFA5315100C314B680F702CAD99FAABEAF08335257F9380066A09BA0D4B04F91A52FBC09F3C348F0A49B840BCBFF51B63D380C53FD17DCA7B76E782580D926865A79AA4992FDCA771F4232AD68956FD1A32547588967B402B2C8E3F2264DC7BC8B5B770EAF4B413C1AC44A8A4A4061CC7FBB7ED809FD97C84B6190BCAD156E36018C44CC0C5A84E9A872EFCE63C370BB257C46AC285058FA09981D1CEF51A07C44994E68C9C3CD6394EEBC545F438858A6D18583E51DC6F15D7B0A3F02F26932928BA1F281012D93050A8D6685F5364FF952BE1C644965A3D255A57C95308E100C4AB8343E2311927212E6F7975A9FC7F72B4761B78069DC5553D740A5433108099BEBC850D5477CE3708474481026931082946A949E3B00DF640AA52BA6D04AB3C1115C91147BCC519DDE6BB953582DE996D38F35A51C7AF0AB999FD78A9B34BBB209869D3776C038B0211849A26674D5D5698904A9DCE362D6BD858B3848B9534B84DF672047930CF63B51D91C79321127FAC7778068E7AD26EF3E61E98B757F33127895A07BA6CCC0FF72A5D5288FEE49AD4D249B7E1B9E695B91EB147D0B1AB2CA4299F9A655BF25AD3B9ABC9B36EE98740FAE598F17113D3C8912BE694A30207E86A5633A32D2335ABE7D7267AEC476771B0A7FC8B0DE2BFE524423D0304AB19632166C790DB214D478726BC7A281685B2CC77E0BB9F8BA64245A80A5DB5570209B1FB0115EEC3CF67383F35E7C05ADC02E8A2BBD1994BBB8818646BB9B87349FDACAE5EC681D40A03A506754700665F4B372C2009711AB55D0C0D62506FD4701EC69C45EA8ABF7A20BBB2669AC460B6728B6845476F49DBCB927723DFFB7A224CA159DB7D1E63322F22BA31B6BD04DACA1E30AB53AA686B8704B483152516C94078916C32064FC638467329CA008784847275A61B44E758A6828D5377747B8848D2557DA23090865A58BB1C7E7448AB61796697DB1B5A70C881CB80A9438F13C8753D662ECD495E74B21CDFA177544A3D5F8912634143938935B4F26874231C2D58BBC0F38DAE4135955793596C4DD44798ED9A57FC178A3AE245DE19BE18C1604A06D041C5560E69093854999EB93D1F82447103B54D46383FD084907060FD756DE74B6FFB3675E0ACA92AC5A89DA53CEF6042FE0CABB55C68DDBA367CB999D7B0B935E752AF97196D465E57081E0378465CC6BA56234CCA1283E22334E65A7F56787BE509C1AAAB0942B39D58167DF5421E35B6C966965FEC61BBD8AAADA8B59DD0664DFCA12F88B92A1CA158E7D5B5B5401DA7D038CE9A70FB5C890E52A19CD32DE63219DD94981CA9B269F0C4BE40CAD533C6DB8687E4497526392B848C4BB706AAD7A68AFFB4AC3D21385A2CB4DDF369D41B6C978719C8D121D43551ADBC54F74CB3528655ADC64F62C59461C3531B6272A4AC9CA013A0203C6D8ECA72D67189DC07FCE68847F0053CA9F0F6AFC7D795AE4ECD3D8E6A02\",\n          \"dk\": \"E4028F81A3115D24B0661A525B546BBC901FF645B4A8D9127E500279A0B67C028DB6E660343012EEC35B8FC5B25B61374B19053D910BFC716E08517187B191582971C7A2C1564BA8B2E6C72F417681224DD3F663719032D0649C4F69229CCC9FB9E6B775D3376708704B5A715B67766CD045DCBBBF861B3170E44EDCAAB276C6190A5B188401353F61C4BBEB413811015BF273C760682F414CB74ACAE3624B9C75036BB32E6113BC59A2A9B0770E928C445B53030A8B262D34950C930BE7ABAF65376C0C90416333B2413C4F849A341BF0124CAA691CC2C2A8C328077302FBA57314E0747569BBE664339219867E42A0E1E92332A2103925CCB85C3ADD190CA3317E0DAB7CFAD92473CC5AD7C31A412417C858AA846721B268642F9641C05A338FA77ED5572A2AFAC80AB0746F07740BD8925E856225D61F73C1BE78381FFB751D6CC456FB488D143208C6E849C0DC10FE295148293251B4300FD72ADBA15BFE82406CAB7E0E4C4F7545869737B46B669EA45470BF9894BEB9CE8C57C4E18042D071C0D58C27AAB46C05446EBF69CA18E45E63039E12A12ABFB3C291692016182349DC805725334DE86E8971449EF7AF09D52089872314757110ABA0BF576C98C1A9D8355DD45A3D842455017A4156101931198920354B0BB66ABD7BC4A2741FB5B06424C14D07104460F3B1A7C54DE8BB6C7AC6091D5B7FDB91BE0A4616AA412C807799D23379756658B9F39345BC5DCF9168AC5B22B2E94BF43071D241129C06714FFAA4B74564B402521942769A391D9CB3C15F7572EE2407ABAB96ED161388C6B76346504650CBB58025155B1527850342E9354804A16626ADD0305532A6387F45781BC080011A1680716C7C1B3CF72717ACC91ECD189A5B03C7ED561DA771B68CF275684C2CCAFA8711F34104E4CFD8F6BD050B5E196564D6D710965740C9A533D1136F7009B9B69CB6BCC388CF7644681B4C2A475410934840A6AA0E316357FC8CFC612E0A57C7ECE8321264A8BE56C468E8C431A0CAE612056D520AC26847DAA4BC71BCA9B3E739D454A4C7B3808CB633A603C22644361611A9D363A407E99D55C459BA856F8295B742357B0BC7C53436A58B434A0280473775B3B3837503828B76D8BA5E182E3AC8154BC99A727A70BEF0B22E631BC56B631276898550443381C1DA223E2CB3A0C38236F35200E1F9C67EA42CA9E620D148209FFBBC07B333BB2B8013233F4C908706F87410C6829C44332C084DB2E0489894593D9627E9F370DECBA867788F9FC99B81127F6EF446412A36E3B2286FD013354C58A04B32E490B7AC53653BD2A15A096682D01689194A0232BFE522B7A85275A5834DBD105D9AC03C3B9416F344302B72C232890390D303DE975B0CE6AF800B0F587A17CA759BE4315EF9D4A3C27033FDD290646C4EBEF4964DB66BCDAB216A80737B2B674F5170B1620E868741792AC8290C1AA21B1460C61B367367A5A084800652E8C9528431AC16F73138A69678B218434463AB5587FDE45ED194B830F02996D81A2C92BC6F524448CAC8B4A5795C7A423625170DEC2120D437BFA96F0E8078771C9AF42943E8A51CC89C9759D3A92896C5494CC584559742FB564C068811ACB6D6525398362B71938C1C721695157F1A2BC24680BF6E265DE8726594C26CBAB9B040462B4DA30402D70EC050958FF5944D181374CC9A29E867C1B33CC36CBA66C66A75F44CD68112B543AEE0026D9C105EE556AD5FC2989346C7473C93E2641AA42904C7751D83CACACDA6B2175B1B9C1C41435C49027AA21119428C61482F655D4A0CCCFA5315100C314B680F702CAD99FAABEAF08335257F9380066A09BA0D4B04F91A52FBC09F3C348F0A49B840BCBFF51B63D380C53FD17DCA7B76E782580D926865A79AA4992FDCA771F4232AD68956FD1A32547588967B402B2C8E3F2264DC7BC8B5B770EAF4B413C1AC44A8A4A4061CC7FBB7ED809FD97C84B6190BCAD156E36018C44CC0C5A84E9A872EFCE63C370BB257C46AC285058FA09981D1CEF51A07C44994E68C9C3CD6394EEBC545F438858A6D18583E51DC6F15D7B0A3F02F26932928BA1F281012D93050A8D6685F5364FF952BE1C644965A3D255A57C95308E100C4AB8343E2311927212E6F7975A9FC7F72B4761B78069DC5553D740A5433108099BEBC850D5477CE3708474481026931082946A949E3B00DF640AA52BA6D04AB3C1115C91147BCC519DDE6BB953582DE996D38F35A51C7AF0AB999FD78A9B34BBB209869D3776C038B0211849A26674D5D5698904A9DCE362D6BD858B3848B9534B84DF672047930CF63B51D91C79321127FAC7778068E7AD26EF3E61E98B757F33127895A07BA6CCC0FF72A5D5288FEE49AD4D249B7E1B9E695B91EB147D0B1AB2CA4299F9A655BF25AD3B9ABC9B36EE98740FAE598F17113D3C8912BE694A30207E86A5633A32D2335ABE7D7267AEC476771B0A7FC8B0DE2BFE524423D0304AB19632166C790DB214D478726BC7A281685B2CC77E0BB9F8BA64245A80A5DB5570209B1FB0115EEC3CF67383F35E7C05ADC02E8A2BBD1994BBB8818646BB9B87349FDACAE5EC681D40A03A506754700665F4B372C2009711AB55D0C0D62506FD4701EC69C45EA8ABF7A20BBB2669AC460B6728B6845476F49DBCB927723DFFB7A224CA159DB7D1E63322F22BA31B6BD04DACA1E30AB53AA686B8704B483152516C94078916C32064FC638467329CA008784847275A61B44E758A6828D5377747B8848D2557DA23090865A58BB1C7E7448AB61796697DB1B5A70C881CB80A9438F13C8753D662ECD495E74B21CDFA177544A3D5F8912634143938935B4F26874231C2D58BBC0F38DAE4135955793596C4DD44798ED9A57FC178A3AE245DE19BE18C1604A06D041C5560E69093854999EB93D1F82447103B54D46383FD084907060FD756DE74B6FFB3675E0ACA92AC5A89DA53CEF6042FE0CABB55C68DDBA367CB999D7B0B935E752AF97196D465E57081E0378465CC6BA56234CCA1283E22334E65A7F56787BE509C1AAAB0942B39D58167DF5421E35B6C966965FEC61BBD8AAADA8B59DD0664DFCA12F88B92A1CA158E7D5B5B5401DA7D038CE9A70FB5C890E52A19CD32DE63219DD94981CA9B269F0C4BE40CAD533C6DB8687E4497526392B848C4BB706AAD7A68AFFB4AC3D21385A2CB4DDF369D41B6C978719C8D121D43551ADBC54F74CB3528655ADC64F62C59461C3531B6272A4AC9CA013A0203C6D8ECA72D67189DC07FCE68847F0053CA9F0F6AFC7D795AE4ECD3D8E6A023B1D861C34DA182BF4DD683ABE8D247898E71E95E27AF72494C02BA6FF3C8147C71F7E44295978FC63BF8F6A68F8609E98D155FD7A74E1FB7982733FBF8A6C25\"\n        },\n        {\n          \"tcId\": 41,\n          \"ek\": \"499B0EFC9A67CF754833785F22E7AA1AAA43EE135195F42410FAC0F90A6C7658BE53354D81939036C88A44801707CB2C743363D1623DF1C1C0EA474521979497178361A264C5C68DFD5484D6B19B1734CF6C73521266B4B5D81872F059A22C2A9219CAB4055FD8B95661CBCE716694DE5414B4CA0AB0AB3231A896AEA0883E6A6454F0025CB79894D83179F1036378A423D55005C973A297ABAD1B7321CBB89B6875F3B9522A27C83D7716B01AA8523686B3EA5F9F3722CD715A833CBB7AAC7AE3E24F1F08474E096D31EC3B608C3C67C1340E01B9780A09155C03C7971D54E0C9C7A6265C196C0FDB8E0D6C23D549708B9C2E2E7366F146C2BBC290673030BAD304B67A6133B3B22A41359262AF706785EDB44AA6B58EF1B71C69B4C1097230B90702BFD7657464991C46C4BA0775F807C0ACC14FCA9C31FDFAAE5FE525BD9538933C843764B7FC130322235823D55637B721B20B8825603A57862064517D2DC54545A0A7369270B7B15ED2B5265A401532F4ADCFFA3C94A5B6E89660530800CD317B45522AB8CA8E3EC71259FA60E97A808FC49EC9E998DC703FBCB3308069019DF8138E3B9B69D71F9FC708EB1364E7F7C2DB73765F13A53506AD03205EB5AC5DA86B24E322898B79917753839C5A63F400547D75B426F70D479A6340278D0607C765857E44A2A32AAC4A3D324BAB221FACA212E1A9315B452C5FE25EB0344138A1B191F8044C9A4CCF0901A66C8FA56AB468BA4513E8A045C6621F22719762C37C37C6E8C5C04CDA7A02FA287A2B04BD88BAE4A94DFAB47733D74B056C7541EC992372104A470B60F20E394C6994A13932236532A74B716A8D86F5ACCAE10CF6C1708D079766A7B5760474A9E351ACD6774A627CBCF10E441894940164AAC6554CF73EFA6A7C487216FFC93939561130F107E605C5A20A8B3A280F27942F7CA353BEB66EE67A2CC2AC68CD94310E8B67D597AB33368C83179F8F8124180494EC253B395114FDCA9E27FCAC8C7A0B1AE40D31581946C5506B5063E2050D0CCB6AD713024A9C4D08D09D211486ECD371F5033C8804C0531180B99447FEDB91181C327DC000E7159C1634013EC00C03C4C5ECCBA79DD138F20B4975F27946566A629A4819E7C77EE4625CD91C86615AD1A876E7045196E6C09EA22439751304D92B9C2B3C9BD32A6CE010C1BC5608245B76008A58A4A4C9072EADCA4AE103B63487366D130A5135C18FC4C6766CC8E06B988C51AFA6D1CD44133ECFD539BB0B8CE92A3FDD36127B9C03259161E8441A22F0436F9B1A0C57BC8BDC06371078A2C9548442310B4A5479F0054ABC375E75A1D170A949C91865326A76768A2F117CA524CA8047B26F7416FF677DE0BC07D518C5F71686DD544823DBB9622706FCA2A2112472C352B6BBFA0A3A7C2280CC85C35203A6DBCE3D3C4E8F9C1D454C730192968892BD257CBFC06179EADA01362BB1D1258E9F1BB563565535908803822969B3B2A21633390961891A2DC436B55EF392F0379EC484967A9068E99BAEE1F461241070342221660101651B2A2FA896934141DBA9B905E8C309F8981CC5B080C26CB585090EF97C4E01B8ECDB7F5DD88C4F8B85765B7908256D350898450C3592526E6A99B504A0D3D19FF79680DADC9890E865104DA28FE012506D19180565\",\n          \"dk\": \"073C0C1BB67152B5AAB7A58461B1AB82C3BFA8CBCAB360B48CE83CE15B8998579542183C8106743238809A43B848688025D2CAE11857BD75986BC0B2516552F640807A4625D6E28816883BFD48966FA2A39A3486892382BC799FB0C6623D687A2D7A9C6CC0867C2C96C162C272425C503CBB3DF71611565B43777DC805A799FCAE6895C709760689E45FB61779F1FA72EB7628CE299DECFB6D25E883D431069A3002BCE8AE00145AE57785D2F82E76F59B98FA5A5D31426DAC1F394B954377A6ADC243E67A8C05B389E3C013A7F98131DC599B6918B79C665D73C0DD676DB7A864534A536A75C108EA79CA267684812912D8711D32225550788E22A23B861C261083B9A98A3D325A08C828DA42753AB959E4C795FD370A1379C9C926038DAB8BD281AB46FB3D8C1795D6144C25CA1073F00C85A27B441034BE185B43AA0782A70EFA995FDDD35E753B226454A0C5998A8B6C0604991E3942AA699B91C6B17C67B373CDB05A48B05CBBD8B3F1B1139DCB18B15704E221179FD95478A6280DF1518FD3ACE9837E9701503476C230E582AB4BA5F88CA5ED306EBA481BA51B5FB5454BC96100CEACCE14B11A43915E44B0067C44408980CA872B568ADA7432A6B50FC6416DCA06D62B96F701BCFD7A48DB251D6949AD368779FC2858C6108D0F7C0AFCF9A7A8C0A6BC27BF76603B74438B5DB833B3AC8516EB3D99BB24BA310D12EA23FCC0111B02409FC670A1DBA9AED13849C78DFF1229B8CB4B3742046C4744BAA3067F456E8625974E3387662C6ED3B2A1B24262BC8A484EF185F222124C9B3ECAB4B7C4202C64309277B0A28B314C702B72D1C950647C6A27D477E50AC3B675593123961F941750192212B663EA79BE4314096B7A74E7517FDC6818A8C913C59C75BF6A72A2F34F6A77816B9354B48989A5EB62E250372AF04DBB56332B6545AB254CBF9867E354A8F2C4B614646F6F316A8294AE497B3B7E12B6D6953D2A764BC4A22DCE9185C7AB0A939883B2A48531B374F3C499DC746170650BE2F08D0AF4785094C96F3423DC5A8D90F536AAE1C3688353D9A67FF2E01E94CCA7E67A5F9842762B30BF8FB729D0694DAF93397A360E8C200BCF8B7A68B9BFD43159C89C6C88E389B5237CAD95BC85D366DA09BD2AB93A7E8B8CE58AB44E93680201605EF434AF729E3015A3F0F5A69C273724B557BC218360A31C88EB150A545C4BF7922852BE48972D2F479530216F9E75B4F33967214CBA93E601D5D536F8FA3083FB3E5BC6A752F9C3F87413DF31B0235ACB53EA6C96A74D5E02A22FFA4EA8805B0B721DD3A554108000A0BB6F25F4B7FFD11F4CD15F5F34201FB958A8D13F9A530E66B452FC8A160923928B2B9C9BFCA4DBF374923A301751175621A3EAAA2C046AAB0CD42D3B693E4AC19E7171A0ED87186C7C8EE4C576B9EC9A249B7B346B2119B32C78324BCA7C51497458A6E46A24B60E84CB583305BA0A3194E74A5EFA26A1E221CD85B2CD963263C134B2768C3ED37636CE59262A1969C926B29481066C76C17CF80813AA2C30100AE0F20B3C99395E9B2B3438CDDA5857F652A646777F58B38C86EA3F7BF2021ABA4B32070CC8B40CDB7B361AE3219110A00E55249C498F499B0EFC9A67CF754833785F22E7AA1AAA43EE135195F42410FAC0F90A6C7658BE53354D81939036C88A44801707CB2C743363D1623DF1C1C0EA474521979497178361A264C5C68DFD5484D6B19B1734CF6C73521266B4B5D81872F059A22C2A9219CAB4055FD8B95661CBCE716694DE5414B4CA0AB0AB3231A896AEA0883E6A6454F0025CB79894D83179F1036378A423D55005C973A297ABAD1B7321CBB89B6875F3B9522A27C83D7716B01AA8523686B3EA5F9F3722CD715A833CBB7AAC7AE3E24F1F08474E096D31EC3B608C3C67C1340E01B9780A09155C03C7971D54E0C9C7A6265C196C0FDB8E0D6C23D549708B9C2E2E7366F146C2BBC290673030BAD304B67A6133B3B22A41359262AF706785EDB44AA6B58EF1B71C69B4C1097230B90702BFD7657464991C46C4BA0775F807C0ACC14FCA9C31FDFAAE5FE525BD9538933C843764B7FC130322235823D55637B721B20B8825603A57862064517D2DC54545A0A7369270B7B15ED2B5265A401532F4ADCFFA3C94A5B6E89660530800CD317B45522AB8CA8E3EC71259FA60E97A808FC49EC9E998DC703FBCB3308069019DF8138E3B9B69D71F9FC708EB1364E7F7C2DB73765F13A53506AD03205EB5AC5DA86B24E322898B79917753839C5A63F400547D75B426F70D479A6340278D0607C765857E44A2A32AAC4A3D324BAB221FACA212E1A9315B452C5FE25EB0344138A1B191F8044C9A4CCF0901A66C8FA56AB468BA4513E8A045C6621F22719762C37C37C6E8C5C04CDA7A02FA287A2B04BD88BAE4A94DFAB47733D74B056C7541EC992372104A470B60F20E394C6994A13932236532A74B716A8D86F5ACCAE10CF6C1708D079766A7B5760474A9E351ACD6774A627CBCF10E441894940164AAC6554CF73EFA6A7C487216FFC93939561130F107E605C5A20A8B3A280F27942F7CA353BEB66EE67A2CC2AC68CD94310E8B67D597AB33368C83179F8F8124180494EC253B395114FDCA9E27FCAC8C7A0B1AE40D31581946C5506B5063E2050D0CCB6AD713024A9C4D08D09D211486ECD371F5033C8804C0531180B99447FEDB91181C327DC000E7159C1634013EC00C03C4C5ECCBA79DD138F20B4975F27946566A629A4819E7C77EE4625CD91C86615AD1A876E7045196E6C09EA22439751304D92B9C2B3C9BD32A6CE010C1BC5608245B76008A58A4A4C9072EADCA4AE103B63487366D130A5135C18FC4C6766CC8E06B988C51AFA6D1CD44133ECFD539BB0B8CE92A3FDD36127B9C03259161E8441A22F0436F9B1A0C57BC8BDC06371078A2C9548442310B4A5479F0054ABC375E75A1D170A949C91865326A76768A2F117CA524CA8047B26F7416FF677DE0BC07D518C5F71686DD544823DBB9622706FCA2A2112472C352B6BBFA0A3A7C2280CC85C35203A6DBCE3D3C4E8F9C1D454C730192968892BD257CBFC06179EADA01362BB1D1258E9F1BB563565535908803822969B3B2A21633390961891A2DC436B55EF392F0379EC484967A9068E99BAEE1F461241070342221660101651B2A2FA896934141DBA9B905E8C309F8981CC5B080C26CB585090EF97C4E01B8ECDB7F5DD88C4F8B85765B7908256D350898450C3592526E6A99B504A0D3D19FF79680DADC9890E865104DA28FE012506D19180565847F52D9587DA7DD37F7AE07BF1B9D4C94F03C702351FB4C5AF4200EFCA07F38EF668FB41F49E82EE0FE00919CC06507548321593A7ECD1D2112342608D95FFF\"\n        },\n        {\n          \"tcId\": 42,\n          \"ek\": \"7BC585BA01AEAF899394E3C00223562CA2163F977AF21CAF967A9C174B3719B097C440A60C5AB746683F15294E4CE40031F16A6FDA79C608095EEB6945241B25A77DAE55CD95161C94836940C93665D669745A013013875A722FF1B285047C9270687FBF2310D1510680406C70CB97A65CCAB59AC6C80C4937810A2488BA8C37C6CF022E4FC14135D2100B613E3BECC8D74261C0912683067429DB79CAA56469CB537E432257EC2EAF370590613716C64FDA2A0EFEF27727E5A437000C9145919158BE240B426E63AD426018FD3271D899846E6421277C511FDB14CDF1B16C48B99080B6FA68BBFB4C02DB837C65E6CB02235412AC3534FA91AAD35DCB3AA773FC2A050975ACB0C7928201636A7920F46DADAAAAF9072F6B23B138669BA4C9655A660F3911AEDF97633ED966CD1253CD7B947C09839CA53AF33B80A370AAA43C1ED6D26D64981D756060A17331A3D4B452FC34F186986936C2EDACCBE0629DB60493BFA49451D44BDEEA1D6605BE945625654A6A6C13943B2972D0A95CC07B70196579ED9013B8CB5BEC81AA61066B5AF6673B3681B21B65905C1E44B808B1995819B211D0BA9D4AF45E6C030D75D2C63E77264BE92DDBE1B54BD54060CC4E8A760DDC124EDBBA6DD8910922D18222AC42D802A1A377C0979B3D9CF8736ABA7EF223413E6712FA523716DC5A0C41CC3CC462C6FAAD0CF30674EA8469A06B1FFCA3D6E3AF0AE95689C70F3325B3BD6C3158BC36014B836F908D91269FB5B05CE6075A80D4A70562572A64AE090C9C39DCC5C6197D84C62752E30DF9A49657DB5ACA0084393A25C6047E629151C8BCC3C4897CA2BC8BD3EC4115B8230A155C15775C7CB7A3DAD6560E6798C57A607FEC8DF6493C24A11518280F081BAB5359AD5468B1717560F1E60273518ADA29819E11679D16CD20514EE44989C62582B0E79A1C09997CE67B9E46850F294BA940C7A6668B28EB6C953C81F50C2B94F14A0DA76DE5AAC603395ABC8866B92B88890B120A62CB06CA8F0F14806E19A794F73FDBB1A9638169FBC30BF4C764F66475395515683C9CA4E587312B4301547C25B58108408E64D6BC0A6B9BA8A4CEF76649290A185BF25DF5C14B81F633A399325380A91B5C2AB21399C4C88597202A50A966DDD8AAFD4A2A831897385AA5E19480437B5E1EA05F778BB8C616391C2766F1DAC500D1898AF957E022202C33BEE93001F7E56B0589C25DDBC5CACBBDBBA308B30228C5108019280812925E395329D3A72B6566694DE690746A1B244AA21B879E54590A4173197C2C930E683F645A83F6B288D3A48D00A9CA97180AEED5685691B8FCA95ACA9136EEE892B5F46CBF60BEE6D803DDC05555349D4FD1AF405C78D6B86E56C12A26657832D753B9D901557572E7960EADE5A9110CAD2C1353A415BD119BB8A42924B0BA004ECBCEBC430F13B33F82AA1D4727CBD8150F416A14AF1A6CBEE094D0EA963CC4C814E6A9CF097DF9B9BEC7D9B68D5BB23F767F1CE88FD44B5539F80F2EDA4B887576107C725A3A5D80240397907AA15842A400CB6200325B1542E268BA3C054E350364C836370DE786E5077EBB4B4C8961917870019C7A4794D27E332C14D26B6BC02202CABC83B8E9E01A155B3D735CAB97C80992BE75623F2B79E41453AEF4A09F6CCAFFAC73\",\n          \"dk\": \"EE528DC327151C18138C507974649A95121F768132E6C6647F2A193556C06040C80058394883C01D75763041146349BC8FA72E57BA7069656B2D138B85784C79B11E70025D5F3558EB8BB418CC8BEA139310F850F3557FD56A10F0D297BDC0C0DC3B4916CCCA4A419B5DD6A8F8F6B0F1AB6BB2B606C73AC36C0C93267A19B7C7CB82AB9C96CBA726770C5475A6B889CAAC178B8882BFC236CA32197523E95112697F7B75248B3C12F132B45535CD55E274706242A69644429A1C3BAB673B580C7DEC34FF7379AD073241156477D1798D1AB09AA5C53EF195E65A7C15E5569781A9B06376F77337A1E56506C785F5D4C95FE05BBCC736685370648A90320C059CE45E55841F08CC8024C24C473646B128451047B44F1C526FDC4B8D862DA60A97AD91CCE133B5BF7C70CC0A23B458380602882EA2741440CF05B1C960699C8A404444A19345F113DE341279C11B58A7BB6B903DE6054D55DAA4A501741E6953739564770205571CC93861A9D5D785F273338CC90432D00B64A488B023AA5EEAAB0543CB34916B55A5CD69380A330CC194640D41048745FC74CAEA0FC8C95A03FB40F701B763E73FFF6B1C8D2434D2646BA3C870C0A29CFA45A880380F6675CA36AC1A86C57C56A16BC6E5A9ABEC7404F86209B3AEC736A8E436407D2476CECB799A880AC8AC33A3F58C68CC047480BF85258B6C1C159B01BEFC73B7BE506BF75C67DE85893DD2425C467B2F627971925FBF666B80DA59DB285813D688916109EFD2951995953BA729DF127018B7043AA3A57BA33AFE2782F8F319C754C4E91780B446C4D3360A824223657405EA34AB1DE6181C702AD100CADE0380BE942EDB51A93D1A0A884308352CB2D93B920F1B467FC306F6A6AF9E41334F861360646CEFF6770BC1B7211A1D2B95377546A0AC4BA431F824A1953BBAA8BC9CD66789E55EB2C8CCBE4B51233C2578B8115061276B48651B63097BE21BE45868EA513F69B91F6455CA569C1C26317E0E1807C7B7B2D4D43F16958C2BA32CE0585E2DD1991B357C72239817B0484B3258ABF39988933710528E82F11BB8E798CCD5C323DAA16C9C0786C1C73AE396946A0EEDE43FC7568C98D056A1766AD702B4F6478CAC056410FB9B95A9427C32A87C84BE3C9A21C0CB46567ABCB3D1CCDC3B8ABA0A1BBF18A6D23474E3659BA696197E059B6F536523E60C0C69314A12312B37CE51686B5CB742715BBC4F241ED12B342045BA69010CBD24597DA695A331C4B8E7C693157A348C9E16A0A307900D86A032E08B5E802A29842C13DC7509D432C5AA7B679BA10BD090CF9FC086E0C5C2AAAB208F39179E693645672123D894EDA35C5E32031DDA9DF410C32F72BCA22B5645E7A5A9C804A45BC67A861A6FF5C821D9CF79E6BC4D8386BF77AB31C6BDA9DC6E99866B5E43193D9210A790CAA629BAD25304E6A946B3D87235CC39F5B66EE8B4C6EA73022B867C6CDAA133A0962579612C8CAE71BA151754B4862C7193994934F04FC3BB2C20A71CBB6962C5D16224D1719555143E33A0E70B79A2D6134CE0BA1797C8A2F98B71882D5BD214E73A950C849FEA2006B242B83F5C8998113BD27C01C3276FC20041ADE77AB144CBF6368D45C18C7BC585BA01AEAF899394E3C00223562CA2163F977AF21CAF967A9C174B3719B097C440A60C5AB746683F15294E4CE40031F16A6FDA79C608095EEB6945241B25A77DAE55CD95161C94836940C93665D669745A013013875A722FF1B285047C9270687FBF2310D1510680406C70CB97A65CCAB59AC6C80C4937810A2488BA8C37C6CF022E4FC14135D2100B613E3BECC8D74261C0912683067429DB79CAA56469CB537E432257EC2EAF370590613716C64FDA2A0EFEF27727E5A437000C9145919158BE240B426E63AD426018FD3271D899846E6421277C511FDB14CDF1B16C48B99080B6FA68BBFB4C02DB837C65E6CB02235412AC3534FA91AAD35DCB3AA773FC2A050975ACB0C7928201636A7920F46DADAAAAF9072F6B23B138669BA4C9655A660F3911AEDF97633ED966CD1253CD7B947C09839CA53AF33B80A370AAA43C1ED6D26D64981D756060A17331A3D4B452FC34F186986936C2EDACCBE0629DB60493BFA49451D44BDEEA1D6605BE945625654A6A6C13943B2972D0A95CC07B70196579ED9013B8CB5BEC81AA61066B5AF6673B3681B21B65905C1E44B808B1995819B211D0BA9D4AF45E6C030D75D2C63E77264BE92DDBE1B54BD54060CC4E8A760DDC124EDBBA6DD8910922D18222AC42D802A1A377C0979B3D9CF8736ABA7EF223413E6712FA523716DC5A0C41CC3CC462C6FAAD0CF30674EA8469A06B1FFCA3D6E3AF0AE95689C70F3325B3BD6C3158BC36014B836F908D91269FB5B05CE6075A80D4A70562572A64AE090C9C39DCC5C6197D84C62752E30DF9A49657DB5ACA0084393A25C6047E629151C8BCC3C4897CA2BC8BD3EC4115B8230A155C15775C7CB7A3DAD6560E6798C57A607FEC8DF6493C24A11518280F081BAB5359AD5468B1717560F1E60273518ADA29819E11679D16CD20514EE44989C62582B0E79A1C09997CE67B9E46850F294BA940C7A6668B28EB6C953C81F50C2B94F14A0DA76DE5AAC603395ABC8866B92B88890B120A62CB06CA8F0F14806E19A794F73FDBB1A9638169FBC30BF4C764F66475395515683C9CA4E587312B4301547C25B58108408E64D6BC0A6B9BA8A4CEF76649290A185BF25DF5C14B81F633A399325380A91B5C2AB21399C4C88597202A50A966DDD8AAFD4A2A831897385AA5E19480437B5E1EA05F778BB8C616391C2766F1DAC500D1898AF957E022202C33BEE93001F7E56B0589C25DDBC5CACBBDBBA308B30228C5108019280812925E395329D3A72B6566694DE690746A1B244AA21B879E54590A4173197C2C930E683F645A83F6B288D3A48D00A9CA97180AEED5685691B8FCA95ACA9136EEE892B5F46CBF60BEE6D803DDC05555349D4FD1AF405C78D6B86E56C12A26657832D753B9D901557572E7960EADE5A9110CAD2C1353A415BD119BB8A42924B0BA004ECBCEBC430F13B33F82AA1D4727CBD8150F416A14AF1A6CBEE094D0EA963CC4C814E6A9CF097DF9B9BEC7D9B68D5BB23F767F1CE88FD44B5539F80F2EDA4B887576107C725A3A5D80240397907AA15842A400CB6200325B1542E268BA3C054E350364C836370DE786E5077EBB4B4C8961917870019C7A4794D27E332C14D26B6BC02202CABC83B8E9E01A155B3D735CAB97C80992BE75623F2B79E41453AEF4A09F6CCAFFAC7316161113DF646837A28818D9C34EDAD57472944528FFBEC6B1BD204262DCA04F26345937ADC9104155275E7114E93D9F5847EEA73A9359358585B2D42301A294\"\n        },\n        {\n          \"tcId\": 43,\n          \"ek\": \"B07C141713A0AAD60845A07EFCC54C4DD073348261F7485FF93CCFA7000967A24E7F03BE00D4C0AAF13995F2C19D0C92B3F372D75802891B0A97E78917301D429B0AF9C7693882024AD301C5B1448474BD2B39CBEB18332CB6252FA7920D94AEE849C10A907B5FC827B94A58E37378161A4407647D025B590D1A1C50880B2DECB17774B22D3139445588B9BC93F7020FF6A9959E9A04FFD15669F9A8CFD412EAC051B940B2ED02561A4B9F4B57753423C10DBA0587287D9DD068C4660138E6C3EDB94664797B1C702D9D89A1C9D086C1F7A21238B4A22A8F6BE9075D0B712F4567E55C0CC1BB4788F2A693D6088D36331350B3969B66C35CAEE9A015FC80CCCC556F1C034431FBCD2796BCBD9347B39C472B27CC8F36468BB10D4D1767AFF24C33ACB86D7191AB359903F43E73DB80B5F39AD25933E0645013B2AD56456081495B7036B28B652F577C1D251CCF45D3BEA72539D359B94DB50B150AA5D4E56FDC91B2654A4367C650E3E1310B72403135C185B90C5005A0FA11C569FB82A3DCC71AE774D97B171987B9D128516801B9A7D91FA6DBA6E5341A0147963BD29415E90DDDBBAACB242056E487724285BE3756C37538D60BCD7DDC44FB25AED0C24CAA663BD650CA4E806BC8A259E9794AC4642447F225BE560B541BA90C227629B2ADA4245D83E41117A095874CB676144782439CCCE1893D26B7D427CE0060A106903891687C17D636CB865EBE4C28134885CDB7722D84B0BE41638D5A14E18023191B35AE228C1EF92914A505F93901ECF598E5093E31A41611F6726C42A96DC2200C5178CC320EEEB8B223C8746B72847F9B586A229929A3B5617A54F6E56C6CEB018AA0B02DA6CBFB1278015B68BF15BEE2F223DFE4025349ACBBA505BCF592623C6D3FA975AA661E74948757000CB476757B732F8647C4F8C431C85975A09BAA591B95CFFA3D89FB3AED59330164510A303609D0726BF63479116AB2EA92DB8B89859C82D3A0966CD5348BE479AA15B6BB395165777515688D172265C7018D08D59F72C74A682290CD771C557018A686CE1B3451D6AC0D4149BAF16C871779031BBB9C4E70C9602B38A82200D2D251C5B22C60CAAA98FACB66CC3DF2A3770EE282A9DC328658A2D582B894DB7B5EDBB63A7B90788786072698C2904343A1B9F6DB107E5A6CE1129A444B3DEF542E35728F080294330343B4843AFFE144827499539B0C79B2CC29961158F61A20502BF3BB38A784BC8A673F3E680C5F8406056A035FD1A3A5E445DBAC0282E1B7885A891BFB367D236AEAD15E4F5B5CE26A6ABB4840DD177000806FA066380CD381D5BA64CBD3B048D60CCE49975EF778C1F8240A1484D073CF3B5625E23A0E02D22875465687987FA6EA8D42C5CE28443BF726551F6CBE108732DB119581CC9CFED76D6D1CCC5FE132A9B51D6D4723FE2B64C70B65F7DABB8E71C3A876545CD1AE11B835C1FA4629E69DE1D489D085575FC3A677585CA06C594FFCBD17E76162366092C03F2AB951FFB012E89446F047A4925BA350AB51C1526FD79A853B2273C2B68A590816CA0536C4C467D0918DFED84230E9AA89063530204F096B6E18C29381CC9CE4BB391D6312AA804E84B9170E59272C4835BE92CE2AEE6D55F2AF72FC20B0DD71E5A81C39766E3BD54B78372F1C6A\",\n          \"dk\": \"B4FC40D80B09DECC317933826E7985C4E074C65CA16B6C7184F30CF1AA705ED7CDEF878624007ED6017BF9D4170C20928A71388ECB8FD2906D612525F3A8139261419B1799040BA0F5F29BDF683E1BCCCAA819A65FD3286EA55B98B3B0AEA03972A0988E352112F0984E64A36760317EA9BA7267949BB02423D5A53EFCADEA178FC568464859BD634259C6B8AF7C2BB9A462A3F0BB66A15B2BF0956DD7829DBB2BCFFF47A40AB487E5D76A57AB71A2AC6C5B651597F631F37A9EAAE10E42D4588711B1CCF1331ADA44EF532301900A6BA2764721C81B2ABB3280672386BD64D52356737055EA8F9409756A03A5E0B410EBD4463FEA79ABF65254957447B6BCB9989F733BCD58E8CC34D74500DA128247264E0253F0EC811DF199DF1298F87A34710998045BCCB5205441EA20BFD776EC02CC70075B272533C011A25EBC541CE65B54D03149A4C04E950834D8A33996ADBE386411D168C4F735354171B3BA908A208B8B0C8DD95113BBB301D361189DA703372A7458337027C27682C04A0CE6C0C0098CAFE1594BA01787568B944C58BE9A3EE4600444BCB15376648222C167FA317866745D2495D9B30522C830B6C447F935A38588BEF31CA014C5106402933B098F8432938A1267ED4BCCBE24B4CBB45AB3317BC60C8525FB21AF8B5C35546D49A41E35ACC7B24466E1A7B7138A045BCBCB2D98C0EF242C5762CF38EB29A61AB7CCDB94880710B9E7AA0366AE25034BF131012FD0AFCF211312066A667C3739941BE616B3EE1139D890BF4CC499E6720BD00103B29A05E5B993C6B89E281760B853418A2AC0E6A278A2D2C3A213514D486D2B01AC73A8C21A01AEE891A324E2AD45338431147988799FB8D6AF44F62BC386431F3C664E9100B3E4B86B195F31125D58DB59E7858F45506CC5EA11A9902668601AF0B7599508AFAD51799C389BE4C164691515F59C4D02A2CA092A3CD06181C804AC81A651EE142866B563089BA9A6B459DAA746DBC0263E04050AB458FA97682566C3E4949ABA5CB3DE8548AF3BCE2A253F8043C0EEFC96E3E83CE3A427FAC302AEBB2AD104454790BE531B77604A910640313E5CA5EDB6AD8729AFF6C37705E2BDBC9A9917D36B1925AF45A22C7EEBA2C26544FE5821218A19207990FBE4881FB8B4CA76424FF9022C4C61D680B0F1F668B537103CAB1E1640396050CA524465A9F80221B8A3CB8CCA8979B7C85A141E26649C26BB3DF4666280961CC72060814FB6720DA96979F7E25C4A68588A00801A121E60F217D565460D7C1AD47BA858585A7EA696AFC91544B533A7D86AB68C7F81856F03135912BA1D0F69AA73921C0EC87E8B770C1C332B378A9387C0BC64A64D4FE6AB5FEC50CDF41FEA9883BC44A9175A3DFC0CCAFBE434881CA7E2B53A825ACA950AC2050B20D1FA9B36536A34E83B860850DC90B9568547B7246A115A9394455818A2B081843CBFE129D0389CFE27B192673E13C56B9FA38E99784CA5E1CC0529960534C9AC2497524948C4401259B09628A605FA659661C96CADE04B82894116931C63207F6B46675E14549DC767291B579BDA32B6EA16121B97C19BB4A90287C9B6199C904DA6B6A1278BB7A7B77405A9696493A45C8C27B07C141713A0AAD60845A07EFCC54C4DD073348261F7485FF93CCFA7000967A24E7F03BE00D4C0AAF13995F2C19D0C92B3F372D75802891B0A97E78917301D429B0AF9C7693882024AD301C5B1448474BD2B39CBEB18332CB6252FA7920D94AEE849C10A907B5FC827B94A58E37378161A4407647D025B590D1A1C50880B2DECB17774B22D3139445588B9BC93F7020FF6A9959E9A04FFD15669F9A8CFD412EAC051B940B2ED02561A4B9F4B57753423C10DBA0587287D9DD068C4660138E6C3EDB94664797B1C702D9D89A1C9D086C1F7A21238B4A22A8F6BE9075D0B712F4567E55C0CC1BB4788F2A693D6088D36331350B3969B66C35CAEE9A015FC80CCCC556F1C034431FBCD2796BCBD9347B39C472B27CC8F36468BB10D4D1767AFF24C33ACB86D7191AB359903F43E73DB80B5F39AD25933E0645013B2AD56456081495B7036B28B652F577C1D251CCF45D3BEA72539D359B94DB50B150AA5D4E56FDC91B2654A4367C650E3E1310B72403135C185B90C5005A0FA11C569FB82A3DCC71AE774D97B171987B9D128516801B9A7D91FA6DBA6E5341A0147963BD29415E90DDDBBAACB242056E487724285BE3756C37538D60BCD7DDC44FB25AED0C24CAA663BD650CA4E806BC8A259E9794AC4642447F225BE560B541BA90C227629B2ADA4245D83E41117A095874CB676144782439CCCE1893D26B7D427CE0060A106903891687C17D636CB865EBE4C28134885CDB7722D84B0BE41638D5A14E18023191B35AE228C1EF92914A505F93901ECF598E5093E31A41611F6726C42A96DC2200C5178CC320EEEB8B223C8746B72847F9B586A229929A3B5617A54F6E56C6CEB018AA0B02DA6CBFB1278015B68BF15BEE2F223DFE4025349ACBBA505BCF592623C6D3FA975AA661E74948757000CB476757B732F8647C4F8C431C85975A09BAA591B95CFFA3D89FB3AED59330164510A303609D0726BF63479116AB2EA92DB8B89859C82D3A0966CD5348BE479AA15B6BB395165777515688D172265C7018D08D59F72C74A682290CD771C557018A686CE1B3451D6AC0D4149BAF16C871779031BBB9C4E70C9602B38A82200D2D251C5B22C60CAAA98FACB66CC3DF2A3770EE282A9DC328658A2D582B894DB7B5EDBB63A7B90788786072698C2904343A1B9F6DB107E5A6CE1129A444B3DEF542E35728F080294330343B4843AFFE144827499539B0C79B2CC29961158F61A20502BF3BB38A784BC8A673F3E680C5F8406056A035FD1A3A5E445DBAC0282E1B7885A891BFB367D236AEAD15E4F5B5CE26A6ABB4840DD177000806FA066380CD381D5BA64CBD3B048D60CCE49975EF778C1F8240A1484D073CF3B5625E23A0E02D22875465687987FA6EA8D42C5CE28443BF726551F6CBE108732DB119581CC9CFED76D6D1CCC5FE132A9B51D6D4723FE2B64C70B65F7DABB8E71C3A876545CD1AE11B835C1FA4629E69DE1D489D085575FC3A677585CA06C594FFCBD17E76162366092C03F2AB951FFB012E89446F047A4925BA350AB51C1526FD79A853B2273C2B68A590816CA0536C4C467D0918DFED84230E9AA89063530204F096B6E18C29381CC9CE4BB391D6312AA804E84B9170E59272C4835BE92CE2AEE6D55F2AF72FC20B0DD71E5A81C39766E3BD54B78372F1C6A0B2CEE55AB09D33BEBC1119E3D8268D321CE675CA8233E6AEE598C7652298B0163435E06C2AA3DFB3477120710D5E7FF0DC0DA68D4644A24F66A8012FB193697\"\n        },\n        {\n          \"tcId\": 44,\n          \"ek\": \"33C5396C96C17503A1F0D509FF56A842767B24B4C3D2C080D428090E61C88EFBC69D086B83E56F5F898D99EC04F588567B2077B0270BBA8263132C7690645D38AB584C9773AC35389B88C67E34424E003AE741521545039E1C3D2BEB4BD05674A700C150573E0781CE8EEC7D2AD17F2D214FFDA22BE21A55A9570488108B0A555C6D225A521045F6D002F1B330588049FA6BA2D79B552285542BFC3BF3BCCF76D922E7AB9039189A66B76D58D9C82443A32DD26A6B134DD3783EE3480018B04DE6D9561426287C9A8F35ABAFBFC9CFB69107AFD639E9C622A3DC19D0D19EF2C4394CB10E0F519E9C8A9A40F6C33754279ED5C1DF06B065B4760453A4CEE04C67E571565C82D5882211361B0D15C9C0F8A75892A2F1A79673CB31BFF12EA352A484E6CC82B44D1C7C20EFD72F112CA0C25450C672BC34C2B8ABB84C52FAB5B0C66FC2288C48957664ABC079296401C3A470D6BB0D6B231A591ADB1763EF1C522DC95DD4E47728E5BCCBC05A73D7C856309DFEA90E25B7A4ACB4705FD29715441565682620F992034A0DA861C51873212470BEAEA6096B737235CC2C94165723495D7EF43C6327AD9ED398EDF9BE0A29B83D595AC8C46A374B4F02C4B1820095E223092498503AB926B9302C948A439539ACF3C31E91B50190980BAE4411CC652D3108302BB1C4376975AB510D12642A3098BB7FA5B6C14354D6D490FB247FD9C1BF83D8C4DE9C7DC5D58780115D7E0005874BC813B40A6B6ABD1C802C9ABA01A9D893EAEC7EC3AB011EF44AC42154AE40066A72159A9015286C2115213921AC3182372236E7059D6A352626A6DC0C1F7A728FB35A34BCE2C7FBD0659CB6496CF57455E7034E979DF8F134AC398020F153D2624980625DB9AC24707ACE0D7490335561F8EB5CB0468F88DC934B4763846535701B3671C53D30038CBB2896426AA7460338266160173798E36C0C04FA70FF68A56F52A65379031F2565CCF30929A948A6C222D610BAEE964A32C2BB4AE389C7F85ECD0BB733BA1798BA347D142AEB482AE9200B9B4C589264B64A915B3158401B343AACB256D9E8CA942A9770AC4652066B4E16414C78152C3C1D123C2057EA4FD5A511CEA8B3CAB25B9F4258020144924878CED8ADBC9B4409F34DC1F87ACC4165A0F92A8EB364F469751CA87EF9B87B9674CAD2B8628966C7E7D6BEBDBB0B4E6CB51D017EDE9735CD3C3F12B83BF9651788ECCCB2D9B237A330B13B516BB40BD806974528AD133C8E68D839EFC852F3F04E960102E8A58B40615E3D53152C528291826216125907E352CFB53EAD5A33613AA78ED20F9CA3437FD3615980AC118963C8482AC806CE6237853EEC6FF62657A2B19EB6F8678722A06C2698FD15085ADA9222D211A86B4E1BF007EC5906704A69289A8E1F6C4F49A5CFDB90C13BE4771ED4641CB4987BDB80B09724F9A9982AA27AEBFB69D984A5CC04BD94D335500359095214616184993A37EB0422A263ACBE53A6DF5B316378C22E830E2A968661A840C09ACD6711507DA822FE9749DD4C522CD11CA2E57B225BC2C398BB05A975D7E72EAA2BBBCC6CBD88721CCC762049A44A70926778340F5FB7296BC33A8C010089011CB947C5B5E299D0DB66C9C89C5C3581222435C5EAF79EB6C152255ACF9BDFF03781400C9D599A905605\",\n          \"dk\": \"7C63447D4668CBCB42035803A4C9350E38AF8EA8A6296658D7A545B40215A2E7C70AF7726D32CC4B7062509AB15F776E1FB812C22062ECE079492900F0B18DB3D66B4CB2B871766145E237C51023286700238537DE2187A5F45F7671027B88A7CD372A54621D52063705E1B4C07786360805E8CBAA60D9AE953708C201B70DB39DCFCC88FAA2C0FF855C30F31D9C7193616333AF72C382B92DFDE56F48E20689C2C61E940264027923980ED787CD98AAB8476C4C7511A9EC9C0B08D9523D61BECDA2054E1B01B90947AED40961C7A7FF6BBEF64156B243159E4B3D343363DBD58B2060505CB066A31C629084B9E658425BB28D84642B1E216BED180D688415C81C2BCA4A49354709F4ACBB16B8AAF8263329314818FA98FCB7139E673AE702706D3274E020BF469C651FD5A08E6086F36C6451D00ACD94CA59248FD35C91CCD856056439AF18580C066C34DCCEF1C9700DB7C8206384E25531BBAA39CB3760FB346FF8F77DE85461F7198E8C396AA8CAACB8A42C37B1879B80953C1257E82770F098C4896C76A506B8605742C58385DE754797833FD1E08D3E5B45AB828666F15608B6C81DC7166CCC678D1CCF3CE5967B57ADD583C1CCB089C02CCE05B15F810AB3A43335BB0041F0699302972E23A5C229A67578AC9D5B1061CE5B59E72B81EAE01E0740490D84AF88EB572810CEB18C17008308FC860C65B4C1B9065DF9E2C37DF9AD096C96AA0722B19106984A24804ACFB36A9925B5A32D2019E933C54579959FD536992327BE519CD03190CF9595DC5303D4A24AA7D604FCC7BEFEC4C244E7836F63A876073CB757A61B919973696BE0EC1CC15041ACDB2CD9077E15C66FDDC9343CA2A2ECCBCC3FD2319C842F0099969FFA5DB4EA416DD83DAD971197785A5C3173981700ACAC5232C626A5C52B7AF217BD45A555C4898231B256866098518CA1D379822CBEBA148412A664E5094D24D1B6D77BCA7FD176B88856659B9E8513A078A1BD07781FAF1A697A7778C2B8A4A8A0CA48FB0AD8A03E39F7753C742CCA3911B948167DCA397F935BBE0CCEB9D1AD173A8A64E318F9DCA501B031C745939DE94CDAD55722EB2F022495C063259268CF5193B486315083F3B43997C732BC9DA99324252CB503F409340B5A64E74C11733DF256ADB10631C3C002B4E46C340BB99AE3246E794ED09A5524A742DA01076A78B967BC9204F764EC61B528E97176F92F440252FC1838D187A8ECD821E996B5239A1A0F320B12F11CB5C446001D6D765CB11C4A2B24B41F23894A9945B27902A6433CC4BB5B9AE19637B5E1BCD52A29745693F5742970606A89DAB78CCCB61EEA78B0CA49D678C2477B036C48C3D2A514005459728669F8743A23A26865B04A3AE930B6E9C67148A5A2A3643DE2B837610402E6039319969CD928D39794E1311739C0780A393E076C2DC89038A10C1079275F765CCAA064436BF5052051476C1C3BD4167739E2CBE63545148350CAB4134825B2C5D27A68524BBD937957C40F1C00BC06D677E2D5527550876BD7CA3F036DC0B5625F3976BF63CDA2E9B2FD4BCBBAEB911A01B0D3716CE5369F18A285E4BCC6269B407D42A7BDBB88D724C5AA6188B9716BA3D3A3B962A933C5396C96C17503A1F0D509FF56A842767B24B4C3D2C080D428090E61C88EFBC69D086B83E56F5F898D99EC04F588567B2077B0270BBA8263132C7690645D38AB584C9773AC35389B88C67E34424E003AE741521545039E1C3D2BEB4BD05674A700C150573E0781CE8EEC7D2AD17F2D214FFDA22BE21A55A9570488108B0A555C6D225A521045F6D002F1B330588049FA6BA2D79B552285542BFC3BF3BCCF76D922E7AB9039189A66B76D58D9C82443A32DD26A6B134DD3783EE3480018B04DE6D9561426287C9A8F35ABAFBFC9CFB69107AFD639E9C622A3DC19D0D19EF2C4394CB10E0F519E9C8A9A40F6C33754279ED5C1DF06B065B4760453A4CEE04C67E571565C82D5882211361B0D15C9C0F8A75892A2F1A79673CB31BFF12EA352A484E6CC82B44D1C7C20EFD72F112CA0C25450C672BC34C2B8ABB84C52FAB5B0C66FC2288C48957664ABC079296401C3A470D6BB0D6B231A591ADB1763EF1C522DC95DD4E47728E5BCCBC05A73D7C856309DFEA90E25B7A4ACB4705FD29715441565682620F992034A0DA861C51873212470BEAEA6096B737235CC2C94165723495D7EF43C6327AD9ED398EDF9BE0A29B83D595AC8C46A374B4F02C4B1820095E223092498503AB926B9302C948A439539ACF3C31E91B50190980BAE4411CC652D3108302BB1C4376975AB510D12642A3098BB7FA5B6C14354D6D490FB247FD9C1BF83D8C4DE9C7DC5D58780115D7E0005874BC813B40A6B6ABD1C802C9ABA01A9D893EAEC7EC3AB011EF44AC42154AE40066A72159A9015286C2115213921AC3182372236E7059D6A352626A6DC0C1F7A728FB35A34BCE2C7FBD0659CB6496CF57455E7034E979DF8F134AC398020F153D2624980625DB9AC24707ACE0D7490335561F8EB5CB0468F88DC934B4763846535701B3671C53D30038CBB2896426AA7460338266160173798E36C0C04FA70FF68A56F52A65379031F2565CCF30929A948A6C222D610BAEE964A32C2BB4AE389C7F85ECD0BB733BA1798BA347D142AEB482AE9200B9B4C589264B64A915B3158401B343AACB256D9E8CA942A9770AC4652066B4E16414C78152C3C1D123C2057EA4FD5A511CEA8B3CAB25B9F4258020144924878CED8ADBC9B4409F34DC1F87ACC4165A0F92A8EB364F469751CA87EF9B87B9674CAD2B8628966C7E7D6BEBDBB0B4E6CB51D017EDE9735CD3C3F12B83BF9651788ECCCB2D9B237A330B13B516BB40BD806974528AD133C8E68D839EFC852F3F04E960102E8A58B40615E3D53152C528291826216125907E352CFB53EAD5A33613AA78ED20F9CA3437FD3615980AC118963C8482AC806CE6237853EEC6FF62657A2B19EB6F8678722A06C2698FD15085ADA9222D211A86B4E1BF007EC5906704A69289A8E1F6C4F49A5CFDB90C13BE4771ED4641CB4987BDB80B09724F9A9982AA27AEBFB69D984A5CC04BD94D335500359095214616184993A37EB0422A263ACBE53A6DF5B316378C22E830E2A968661A840C09ACD6711507DA822FE9749DD4C522CD11CA2E57B225BC2C398BB05A975D7E72EAA2BBBCC6CBD88721CCC762049A44A70926778340F5FB7296BC33A8C010089011CB947C5B5E299D0DB66C9C89C5C3581222435C5EAF79EB6C152255ACF9BDFF03781400C9D599A905605EAFE2B26CB96B97C22564B28329B64A206331FF842BFED4ADFE3C7A0C4A471BA8C2942B7207C2C59BD56FF9EE0B120B1DAD81B05602623623CBC7E0C20C9B709\"\n        },\n        {\n          \"tcId\": 45,\n          \"ek\": \"E74A4568457E0C7727A53682E50CBF74082F01C9892F60B9DC57C79A82123703A8375C7EFBE299FC3352584CADA79726D8F039E7885508A897B9C54EE4478568168820DB7FFD82CBD0191FAB477199AC0FFF615475AA96E28980888B34A1E2C76115CDA09C2E1AF83FD7004DBD14AFA76773F755ACD40358A774045B09075703C2C08537C9E09727E23902CC453965CA74F9CDB865A02E228BE92850F078596F64841D093BC7BB9354E50FBFB31FD463762B800B88C6A691F22A815B46EDF848C174108DD7435F178B90690D5C8B0C430935C69C6A4451629958614CF00F5FCA7E88C08230E142C26C991F5782C1BB84A6C8750EE00348582F12365A761150B79489BEF53BE9BABB468A7F72F65B2D70068D5313D5011016CAA97C6961C554CE6419204AD28A7E5A7E807333DB1C759B6A546B087E9704B63A573752A0ABB7C702548CBF15100F3A6A807C016687C05058F76A42BAB1BE88C12FD574F4FBC051DA21234806FC279D122A4DD879175DC24E20570B435A14FAB908CFA5AA91A009164530417655799C0B379A10E7592EECA96F7AA84F9D718B95F595BF5887F2135D2BB106D599AAB77C5DFD5B4733506450923815F451EAEA6DA2FB0EF3C7AE6EB12543F8429B7B1BA00C5B05585964D334741AAAB7B2C787778709D2CBD9D1C42A061398939869FC0B21C37FA1A146965066C50889C5798B3EAACC0B5C6D7721CEDEC86BDF67A17BA37666A4226BE80EFEDC5262373117777B6995262F795735872B5CD4B52A59677B3ABFA302BA89E61DF76866CBF5C8F97C2620258570735C9B70265CB9AC98445416A6A1AA8252F89667EFE277C968101C58ABEC009902497D932C0D33F52C69221F96EB589E231877837491757BF2B05BE03C909AF3B780B8088F75610A02C45CE28097AAB34FE19CF364A865D77A9D402501C843684C1F7825C20BA3ABAF711172024FE6CB6F58591E4EEC4E40CC0CA891725927C56A55CAF5B1CC61F510ACCAA54371085FC9B03C169293E16FE74735648B4E752847CEF130A3FC031BE4C04EEC8614A7749FB7B41C2C69D2D23D73F205575655BD6A0F3FE19CF333A0F28A9BFE69B0DA112771EB2BBC30586AB0BC0176A400345986104F0832CE81A60E198B3180840A8BC528B0960CD2F26564D3797BE64DFD602BEAD0C94E1904773BCCD0162F0A17B3B4868B5CF57FCAD264E3F339C6E240C2861C6B6A0429616D88F5B41E991491652852441817033DD8F95A4B7C40EC5A74781A761D8ABEDA618D53D2CD86989AFCC54E5ED2CE26F8244E0247052364B0A11F42DC14B6B8A72ECC76754208560A479C2C104941404E22C800B0BB52E26449238235F113A23C4E46AAA4E54B6B7CD2A3802A2EBC3799B9556FB0164500C14CEC67BD36C31971291EE891134B9CB3E2DC01EF15143AC04EA1F41D9084B7B2EB91F96B7C9A97774A044E037285C820601E424F761BCCC712172B774FFB900EFF1158D6EBBE13775F27720FADB93CD98111E4E2788D86C0151B1458344025580C509738E490874B773849308B8D36C649373F9147781D58C31938B923218BC2E85F4C9B1233BCAE99D35C67DB3A85647801260409E6CF45924E4DA141B6C3A65B857A3D3DD5F476EF377E54214616593BC8CD4D05915F4F4FB9A98223663407787254\",\n          \"dk\": \"0D1B18A4D52F6BB6367CB50F0005BC6D9CC52E0872E86AB6B0B9132BA90A309B524E7B3EF93B696D9149CC17BF57A1A1F8371004127AA97A79E4B55CCD079470B04FF38833BBD732FE937B4877C3D643B5E489AC312271A2F9AE7B665FEB0AB13829696EF532F43CA2A848BE9787A873A4646CE5CABA271D82CBAEEFF095F153C7DD66C4484384D44663B1E75CCA0B479DEC62D7CCC97C5495D5CB3A1FA0C6DF6309A4E1B443051B00135166D77433C30B17A68B1723B83129B72BB28C61BC561FC25EEBB53E5E98C86B3B3D852B60D75402D636470E0171FC44716CCC4B9931178FD17B80A4895431492BBB1C98317E45C03D787A30A7C6530B814207C64CA1807D92530BF1EBB232460B68D52021A03A0C603AB6497F16267640316A8E5712DBAC68F3E1528A5B1E37D00856A32083E51958A265532B51DBEB02EB7820430C2974716582E92542E29255863DF251419777142691384C876263A544CE03039710B5A407B8BD7428307A739BA4B84E762C643A383B7A3CF266A1B6C7043D040B0A475B2A6402DDE1AAC192321D910553E2CC8A18CDB9D9B27C5CA0AD272F3145120A266C443A484E829DF079C98DA9CD25F4AA20B29F12A79BE03C7E86F4C3EE277C050831D8135DE1C3227627A732C1C337D047524BB0EB487EF75CA0365520C3E67324C5B2AF45A768D3986A395BFC7A65E673AA41057C8F698436C35D655A9D9FB54F441C1BF54A57E73B335D7B40EC992B4AEC0978267CA30387618C76BDB843A5461578C6938BEAAA0F8719E18469D7F76EA6FC5D73C59399689C1BAA8BA4812111EBBA47CAAD17D050B1E506247050FB62C530D0A1BCEB6D1C45C1DF048F0B14511A709D91F62BA10B4FB42591596456E2B4271FE817914B54DA4181ED7A9BCCC84DDF6614716AA561D99267DC1F8D596AF769482DE440F32B72A7B60DCB0035E94588C02CA9E8628D4A4B071007703E67AC0D87B2CD21B4502782A8BA6E201B6497C80A103003EC360374915E57154771C59ED6CA2FF272393DDC9449038B96B77E0E6C50EFCC7837996EBB3B4F1CD0079C474D9B2172E4947F2ADB7BB947090539AA43B48079133FCCBB92ED878ED76364F31CC094B68FDC371FA8DA78931C326F26961BB20F0DE742039A547C6C79B84B93CBC238C2252AFD504EDFDC14E9FCBF8051A43C75CC28897AD318CFFBE4B70A01373B324B4C732DAF4BA2F3019B5722ABB9D452AE092B89A5CCF7E8776C3222F03C50643B3FC3F8937D437DB19A10EB771FB755CD4939B73A24CFCD01BF6879A5905125C2EBC4903B8F3E91906ED479BA8B83DBC1A21BD6A8222A717FB6398AF89312170362D0486E0969F8E9A417B0C7C7069D8A41A8D9D3C3BEC2847ABC722D3851605C8671C23E0E82A96C1B1AC82C3A1DD758A0D63394F26F20BB6A6669883A87A1AB73AF02549F032A6C51E803DDB131EC47524DA9A283C39EE79AAFC78C569B07B403A89A5AFCAC7EB4400A6A8E6397B3BD49318AA5C37E13757B7145DCEC210C92AFC0186902FA74A604B7DE321488B357A8D9904913335E5A7CBE838E43309DE6C90264803F32338F92B6BAF7D89A112554F23C4A64FAA671D24201C61D8DA67D83AB37E25982E74A4568457E0C7727A53682E50CBF74082F01C9892F60B9DC57C79A82123703A8375C7EFBE299FC3352584CADA79726D8F039E7885508A897B9C54EE4478568168820DB7FFD82CBD0191FAB477199AC0FFF615475AA96E28980888B34A1E2C76115CDA09C2E1AF83FD7004DBD14AFA76773F755ACD40358A774045B09075703C2C08537C9E09727E23902CC453965CA74F9CDB865A02E228BE92850F078596F64841D093BC7BB9354E50FBFB31FD463762B800B88C6A691F22A815B46EDF848C174108DD7435F178B90690D5C8B0C430935C69C6A4451629958614CF00F5FCA7E88C08230E142C26C991F5782C1BB84A6C8750EE00348582F12365A761150B79489BEF53BE9BABB468A7F72F65B2D70068D5313D5011016CAA97C6961C554CE6419204AD28A7E5A7E807333DB1C759B6A546B087E9704B63A573752A0ABB7C702548CBF15100F3A6A807C016687C05058F76A42BAB1BE88C12FD574F4FBC051DA21234806FC279D122A4DD879175DC24E20570B435A14FAB908CFA5AA91A009164530417655799C0B379A10E7592EECA96F7AA84F9D718B95F595BF5887F2135D2BB106D599AAB77C5DFD5B4733506450923815F451EAEA6DA2FB0EF3C7AE6EB12543F8429B7B1BA00C5B05585964D334741AAAB7B2C787778709D2CBD9D1C42A061398939869FC0B21C37FA1A146965066C50889C5798B3EAACC0B5C6D7721CEDEC86BDF67A17BA37666A4226BE80EFEDC5262373117777B6995262F795735872B5CD4B52A59677B3ABFA302BA89E61DF76866CBF5C8F97C2620258570735C9B70265CB9AC98445416A6A1AA8252F89667EFE277C968101C58ABEC009902497D932C0D33F52C69221F96EB589E231877837491757BF2B05BE03C909AF3B780B8088F75610A02C45CE28097AAB34FE19CF364A865D77A9D402501C843684C1F7825C20BA3ABAF711172024FE6CB6F58591E4EEC4E40CC0CA891725927C56A55CAF5B1CC61F510ACCAA54371085FC9B03C169293E16FE74735648B4E752847CEF130A3FC031BE4C04EEC8614A7749FB7B41C2C69D2D23D73F205575655BD6A0F3FE19CF333A0F28A9BFE69B0DA112771EB2BBC30586AB0BC0176A400345986104F0832CE81A60E198B3180840A8BC528B0960CD2F26564D3797BE64DFD602BEAD0C94E1904773BCCD0162F0A17B3B4868B5CF57FCAD264E3F339C6E240C2861C6B6A0429616D88F5B41E991491652852441817033DD8F95A4B7C40EC5A74781A761D8ABEDA618D53D2CD86989AFCC54E5ED2CE26F8244E0247052364B0A11F42DC14B6B8A72ECC76754208560A479C2C104941404E22C800B0BB52E26449238235F113A23C4E46AAA4E54B6B7CD2A3802A2EBC3799B9556FB0164500C14CEC67BD36C31971291EE891134B9CB3E2DC01EF15143AC04EA1F41D9084B7B2EB91F96B7C9A97774A044E037285C820601E424F761BCCC712172B774FFB900EFF1158D6EBBE13775F27720FADB93CD98111E4E2788D86C0151B1458344025580C509738E490874B773849308B8D36C649373F9147781D58C31938B923218BC2E85F4C9B1233BCAE99D35C67DB3A85647801260409E6CF45924E4DA141B6C3A65B857A3D3DD5F476EF377E54214616593BC8CD4D05915F4F4FB9A982236634077872549E2FE7DD646C145484E163D6C36DC6EA5D802A0EEE6ADAC932C20FDAABB8BDD1EAE318341D06E0801C0CA4B873520C714740AD017FE5A158D3BD40960D907AB7\"\n        },\n        {\n          \"tcId\": 46,\n          \"ek\": \"3F476A07BA0E59113595E59410B41C61CC5D509895725A7010E20C83CA906EA13C1C58A604F3A2590727D3538ADB2754B879A3F2640C929388DF38A559108A5519B221E3A2DA515EC34B7CDFD704A04C18DB604CCDB2705CF273EFD4B464D4A3A9773FBBD82840C002D8529BE9377009F4558BC747AD33BE9DB2C290EBBE06972544A827BAC51C131684FA3C0111360F59C72E5D1660269672389748E2B8546E648DA50BB8243B832C150ABC1277B4F7A350028EBEA582202917B8247E4A054C84D31B7BC97837C5117D796655A098C1D26BCD6C506DA164F362CD3B231C089371E711890B795B2DB0C5E916A3A169060969001BD88AF165021FBC9B1D649F9A740CC48AA2EE0348D46394C48A776C040D18D867F3506962E4B619C21DB56C9B3042ABD1B78BA814064979C643E6CEB8025B4289BC1716C6E07873B52C545A9488102200E2877C24806E15F790FF15C9677882EAC857C620297B9B9155044D9C26626F6B97FF8491C0FA33742C785DFB3D401BC0B8C50D17D857A13C4918BAB9BDF5BC8695AE1C581334559E4F83B076CA492DE0A9D97409DD676B051C7C97E9655E2BCFC0E88DDB85480F1A5FE60C7E5A002BD33B0F13F7B4897A9B2D3B140468B9114BA041D952B08B340D2B3B970BA44738BDDD3CCFB3039F7C04C0264875C9B0B5B05115C4749B29DB6BD801328DF89307307F6689A43F72C83940B3C7D72BA35B812C660DF39A329867A9727119BD9A379C6C39522B669B461F7C6A5383C23289B4C065862DFD430E7D9B4385401CD870228EE1828C9660F1CC2F9DB63E5318A97FA8AD4B693505B859C5FA86816473080C4AE6C297767A210D355A4DE59AF60844227781BF7B3B09C624C8587B0C2A200BA1196F5C84B99630A03B07C86A1BA50C002E523C8DC498BB89A4DE17CC8BD2A1DE3250E282961C956C6B197904C1269080280E2CC636112CCFE88195536E52D84AC33332C7612A4FDAB8BBFBADA5149E05F1CCBF28C706E5B037F65C7EA47EF5E1792F90B7BA4C01EA027D54AC44C60373EC931BB5E5772AEC45581C613D1497D227C0F65470C05187FF214593B44EE8CB18BFF8195365BC56B3608AD6B228E755D8F52449E7A15E81A26E372E1DC19DED021088B8A2247A33E0C1857C22CAC798575A7ACDBB09B4A018084293CB2E766CB2A8C5D1845B0B878F74BCC41667AD93137B66F352453051EC360559402CED1A1A73EB9D45A97589E05A5C991082F3498D1A75708A3FE582B0EE3B95785C73F7679B374095E1EB47567C41E630A971D60C7A6B9A02C372A9A56B0206144DA6556EF37DACF9002CDCA7A3DAB5319259E06B7F5473643E975BB93B079A451C5AA87CC0B8ABCEE48BC1448482F9769BB27EBCE597214AA73CF8734B9C6C3E79A53FD83F120C642A690C8220008D834DDCB99DF152C8456C817E106717B5CAF96262F2CB3D33892B1506C18D25B065C0CA1017BA4D466D0AC849A112C42E218C7978A14F362BCAEBCA9A5602F1C96AAD325C9BDCB6420B76926157C02A4CAA659134908EB024CC88387FF64317354B87D7383A7317643D71713DA3B25F3B939B060B6BA0CE5838697BDA102310AC67A1080B0C11C4FBB44701CAF7A360730F6EDECDE8635ABC55FAD14FF51E2A271F7D44190ED0D0BA95131BE7DAB95F\",\n          \"dk\": \"9F487527A4789D287401760DE02688DE23B049C0BB20DC05AC651478A591D4178C7E25CC578832B05B1E136BCFB28157AC8A4D75BBAE195CA628028C2BF806EFC1A132D93F40850D64445BC892CE03159E1376A1701C171AB6B18F437184C671158CAF77A9BA9B1B63EB4422011C7E0E602273D94C42820666E18313C61844C403F7127C6E47B17A41C7970CAE2BD78E8BBB353451269C13628C605295AB52F263C655831AE338B1B71AA25C195FB1096E0573CC7AEBBDE9E7751B22A74726354E88322F1BC2FDF03E65342F06C6359B672E679C25FBA03840FC53F147B9A778AE421506C09AB17242CE41735E2168044EF95EC4C4410AA284B79B5B9354C419B5A9663A7C3DFC155BC57A04AB22E7578059994D33D8C76C578C7A98B5B7351D5850BC48820A199B3411350C7DF650B9D1A3E8EA2A0C32A9E88030D4AC3C22078D4950CBBBDA707800293F487D023C6476223FA5DB85E229BEFC13B6B98561EA80C30C8BCD3A48994D255AB6A134AE25A40D4B719BE2BD4FB050C477CA417406FE9A3197495CAC610D6E861CC5A6CFF7BBB8BF84B71701CE6BEB77F273505559752A6B96133A5776B784343797DF2C024C41A9E05ABC38688ED303C5A0553232D2721A13152E9A0154152BD561AA7C1991887912DD79235A8B6E04AA18C1A903280417E42B07DCC0310067BC40633FE441082BF78FE4859D8892A973B78C4F9623EF88C05901AB6C1A039C494CCC4B542474C4B369386842726F13C62E72339A37B472B538FC881711CBBC32A88D72FC93C0E2A8362C7AD8D236C2EA6724616040D799440228EAF306B655754DF78FE941315D419342B426CB273FC4604245459367F30A6A071C4C73718ED1019B1BAA65F273D665CFE79A8967A60367F470BC74C7881A516487BB1A4B4FC38AC6CC21CFEE071CB67119D54A8C077BCBA3E3103EF8724CBA5406519122EB9884F645E7248FA64489FD9763459275BD80787681433C1219C5B31DE587487FD10112274C196A4AAC7794F28A0DE98A8852F37E96A0C0FBCB5CA3B8A33678C71E469B0BE1B7D134100D8A4DA009503F800A56714D06D6BF5ED7A1D1AC8ABA3B458FDB6DAFC28EDE9CC7744A7B3353CD36569A7DF2CC0A4726E5B8AA01093A9B7994B9473E12F346B992C718E825E8F1B05404433B20AE2B464CAC95BD0833C313D96BA607C09998929776A5DEB17D259B4D14E6C1616914B07CBEE1000D11BAA276F64663F23D5CE670C01AB6BBBC9A5BEAA07994099250A54BE9A164EC67174B8C83E7115A72756164491D233A0B38239A90ABD7DB1DD0EBAC93327574DA1B242B4B98B794B8B77E5C9B684F618111EC8A259279A867066B43C4D4540C92FC5E764276A1DA8AFA378F3287C7EDA09A8FD41A8495566E5B1C8C4260EF96B123160221308E97E23F7D6B281EF94F5315CE86F8768FA2270E8953F1851E20A790F0F60ECB03AA414600B7575109FC1632FAA73E173987FBC2EB3BC94B7BB2BEA78ECA17AAA9416CD2905845DC699236517F3A45B06975EC19544C4792E9248C1FC59709C460E907031F831326867FBBE55CB32627822187D93740C2DA9AAA8606FE5979D340B97707144BD437C70C768D292CDA79C23F476A07BA0E59113595E59410B41C61CC5D509895725A7010E20C83CA906EA13C1C58A604F3A2590727D3538ADB2754B879A3F2640C929388DF38A559108A5519B221E3A2DA515EC34B7CDFD704A04C18DB604CCDB2705CF273EFD4B464D4A3A9773FBBD82840C002D8529BE9377009F4558BC747AD33BE9DB2C290EBBE06972544A827BAC51C131684FA3C0111360F59C72E5D1660269672389748E2B8546E648DA50BB8243B832C150ABC1277B4F7A350028EBEA582202917B8247E4A054C84D31B7BC97837C5117D796655A098C1D26BCD6C506DA164F362CD3B231C089371E711890B795B2DB0C5E916A3A169060969001BD88AF165021FBC9B1D649F9A740CC48AA2EE0348D46394C48A776C040D18D867F3506962E4B619C21DB56C9B3042ABD1B78BA814064979C643E6CEB8025B4289BC1716C6E07873B52C545A9488102200E2877C24806E15F790FF15C9677882EAC857C620297B9B9155044D9C26626F6B97FF8491C0FA33742C785DFB3D401BC0B8C50D17D857A13C4918BAB9BDF5BC8695AE1C581334559E4F83B076CA492DE0A9D97409DD676B051C7C97E9655E2BCFC0E88DDB85480F1A5FE60C7E5A002BD33B0F13F7B4897A9B2D3B140468B9114BA041D952B08B340D2B3B970BA44738BDDD3CCFB3039F7C04C0264875C9B0B5B05115C4749B29DB6BD801328DF89307307F6689A43F72C83940B3C7D72BA35B812C660DF39A329867A9727119BD9A379C6C39522B669B461F7C6A5383C23289B4C065862DFD430E7D9B4385401CD870228EE1828C9660F1CC2F9DB63E5318A97FA8AD4B693505B859C5FA86816473080C4AE6C297767A210D355A4DE59AF60844227781BF7B3B09C624C8587B0C2A200BA1196F5C84B99630A03B07C86A1BA50C002E523C8DC498BB89A4DE17CC8BD2A1DE3250E282961C956C6B197904C1269080280E2CC636112CCFE88195536E52D84AC33332C7612A4FDAB8BBFBADA5149E05F1CCBF28C706E5B037F65C7EA47EF5E1792F90B7BA4C01EA027D54AC44C60373EC931BB5E5772AEC45581C613D1497D227C0F65470C05187FF214593B44EE8CB18BFF8195365BC56B3608AD6B228E755D8F52449E7A15E81A26E372E1DC19DED021088B8A2247A33E0C1857C22CAC798575A7ACDBB09B4A018084293CB2E766CB2A8C5D1845B0B878F74BCC41667AD93137B66F352453051EC360559402CED1A1A73EB9D45A97589E05A5C991082F3498D1A75708A3FE582B0EE3B95785C73F7679B374095E1EB47567C41E630A971D60C7A6B9A02C372A9A56B0206144DA6556EF37DACF9002CDCA7A3DAB5319259E06B7F5473643E975BB93B079A451C5AA87CC0B8ABCEE48BC1448482F9769BB27EBCE597214AA73CF8734B9C6C3E79A53FD83F120C642A690C8220008D834DDCB99DF152C8456C817E106717B5CAF96262F2CB3D33892B1506C18D25B065C0CA1017BA4D466D0AC849A112C42E218C7978A14F362BCAEBCA9A5602F1C96AAD325C9BDCB6420B76926157C02A4CAA659134908EB024CC88387FF64317354B87D7383A7317643D71713DA3B25F3B939B060B6BA0CE5838697BDA102310AC67A1080B0C11C4FBB44701CAF7A360730F6EDECDE8635ABC55FAD14FF51E2A271F7D44190ED0D0BA95131BE7DAB95FA5A66716D011EEDF9E6A541F9438F8309660657EAFFCDB01A172998E56D9A60BEF38264520685080F52975BC957C5FB609FB0E1BD06D26F572CC5425CAE7DE5C\"\n        },\n        {\n          \"tcId\": 47,\n          \"ek\": \"BE62BD6BECB1FBA42DCEB479C8B8AF9D740F272661045437E6D0C13BE05D87821365509758B075C7D15A421876CF83C9BA1458F3C2B85002238DA64ADCD77FE7F1B33F3798B6C87609708DDD1C4F41AB9781A3BEAE2910D3D0A2496927C4907139CBA8D5B791FEDA3CD472538D9AC0FB9895DF079AF86B439096783D2BC6E967C211B3969B876CC0322E59C80A761495F50567276349EA736A43E5BAF638971C355FA4D3621FC60BEF5408067B5968E56DB7F2A2438233EED3AB2792443618A535389E3E7A3A1C061A8AC72423508C2747CBA968B7220911D0324522CAAFBBA85B5D1A830FBB0FA20CCF10F78656B512B34B28815C23DF718AD044937BD73198798858E41FE5AC16EEF5AC6E234DE5D7AA5AF49ED0BBCFA35843C7753613988707C2025F8654A9C8A37B4320BFD243BF8B1143B2644A715910C46DA9C88AB8F033EA233DAF3660F8630ECE308DE44B4699D6CEAF275065BA4DCA584026D261F753CCD0A860F705201B64B32F159C86558E633312DB15C2C3164889F49F181435A1877760C50206816F3502CDC3229107F92355467ECA127B51912EFBC911C5591DEBECA2AA6015274382BA5734F64B08E1160A4CDCA78CF722A1A31A590B5E90185AF809CD39A56A0D945BCDF0769E923C45E52DE4B45E2E22093E856FAD9B9C00641970B31BC0481D91884782D08979AA02484CAA2957CC6CCC4434EB5C814973B7E3A284DBAB2EC068A5DC047336CAD2990D37231604C584E05C689E30CDCD7668BC764E3D96184E7C45D6D988AA9B03CD3275747B2E1F0C68CB76326F263EBB910366D6C4A94A8252F6BA0F5269531188811A7286D6A2AFA3158F8243B7695805B8BDA1322B1A30049C684C6DCB574C20A9574BA78677B1135135ACF200A22749115C7E1BFB6A1614A7BCF612562A3CF6702742F586B6F55169C5AE1D14621074034C109518C86EBF191881D4B692E4027BC9C195E20F54A36B8387845F47BF23482359D90BAAC0B24E091D6937AF4049A043529EB884189D2B6FD4B05473E3696512910E9189C1508DF56450BFCBB6E97747AFA545C64B346CCB04B6F30733A1071BB84DF18A2A762460F8E6A1D9F0AC2840474FB61FE7087818F6A53E7CC799B0508CE97D06987E24BCB4E4B559161C5F1AA628D68B129CD178CE6069236C863AF8A2480C149EE5754AC04719E01D86855201554F922167B623236B417FAC5948D182B12E8210FBE23FFFEB81AF88532F0193FD61BD82D59FE646209D39AD75831EDC605AA6485CE90023DA218CBB19AD8FA858E7A661D85A3ECD136809E70B25F166339C307C350E4D5303AC844257872A2845A7F6CAB996918AFEC669C9A19D43F72A5862BEEA66AAB0D708C59C260DF09EC0BC2A265A311309CCD2ECCA4FEABD6176633863A71D75B0E7957266584D000A6824D467EC50421AA1369193B418072815562928D35C16227D22C821A8552ACD83A513AB683CC39D12537DC4456E1476C4ED9B4014C42A7BA6A78C9B72F3437228A918BCC6C13FCA16BD04C7B2B277AA4C2B903517FB8397598554C9C8885380B4199450A46635E7B4B338075E15DA4A3630862CDC2B811578626C5BBE0B73C0F375376708B4D8654560782F3D56064B96E318E3FAF0D63CBD17F9966EE839107DD8D6530A49F344E194B7\",\n          \"dk\": \"4CFB54B237989DB51A1AA97F36A3B2F4337F46AA0D2C05BB1D043A5513CE6FE84A98D62251360EC740221D1B7BE04098FC37A4AD633E95332075394E1C09AF426C146BF34290F50F318B48076653E9E66E972734CC9422750202995305827736D1F03464F37F8E468886CC8ECCD74F14706AD940B1CA73257451898ADB1476D0C813A0B06648A1DBC3150DC742D4B4CAA8EC12770974F87020B80BA9AA5274E4C63B747051AEEB5A1CF310EBEA23F17035DE2B4D51C226766ABD33F9443AFB1B21563C70E431E4D579E1AB26E5F6A2792A8ECF158C53C737F0582CCF50320FF65E634B26D3F9438E94981927ABBE2899DEF3055DC8AF679569A7AAA9FA4152697454D6DC04E2B5B2A9850AD75A1B9E060F11D817152C043500CE8391C0438BA1670B19D8333881647F5ACB813CAC66AE2065A603457EF3535B7338F32759E95923C4EC84D2248A66802D92A01EFA16CA74619C771048C1111ACF6BCD1508C667CA8C471409FA9C37F4A4AE23D505B9291511C1AD7996488CF7C611B7B3CA3855CB293BDDB19B7A1614017B1953BA7A218C50236A2F7D1B3B39BA1399D251D4D6394BDC244AA9C6A6A29FE0502E9563BEEEFA6066D5A9E52000BE59CB2D246E8A559A3B7B4B2E956BE12C2A6EB4C7156324BFA17EA8DB75A844234545147AB0AF65005D7E2470D71453224399F6C51A970973CCD3339C270A5615528FF1968811C652BA8A991C13FDFCCA4CC661F7895F54BBCC07E95E6AA6178BF56FF496C1F3576A42529A69E713A44A2C9F5B7445F794996C7F944325B34574FF364357910863D99060544606A619515B7103B76E2E4869300750D4766834750FC7867A0D98470AA502193131FC7C001B49A102AA135C7224E937973CB02BA1118520833892327428FA88BDE81E3A19BB030C6EA7E6BA69B7A82F6B20F19CAA2D20507ED3C2BB0A4BF814263D17324814017F7012EFF6886257615AE2B896B045231455235C09F706235A628FDD27685C6491EC91A1C8803BC6820C9788AE919C466F836C8CF50512D149F47322398226F7B791E568BC3B6C3E52B021AF3976E9562F582734796977890AA62DE021C0431527C7C15F4AAFCA3AC6B5B61DBED8652644523029495E268890F36E145C2060B84E2C110DDC0018F2678F99726E4A981D258C14BA453E73858492E786E5B96F6C31687E702D95110A68A77E7847C9B53C181E34434C2A51FEB36DDA926F60556202371A52A04391E662A26289A80774273BC8477968D76717BBAC9D5028965C9A9D537005C6A3CADB62206E5B2E7DFA877411C6DFDB1337FC6DFBD28BE42B774746C625D41D7ABA2D29E21ED3E13D1BF5AE3A12BB25650B480C309079CFC2E1C2114B6618E9BCC0B73D5E4202C992CB375B3A8DE0A35985C235B314DDAB4C4B888B216C761224780F885241F0C1F62837B247B06B719FA4AB85E9B63A52A947972C0678DA85B2298E81414AC5B42D41E04171438A25B56AB30C61DEEA1328562079131A52805C63DA0B8A6C324D56BA6F351B6D91041772A227B17384131A30BBCB9367319721C48743C7A9786252749082623780E4828601AB15BC08A9E103C5170181B0240EB4C00B7B085ACA634626B4069703BE62BD6BECB1FBA42DCEB479C8B8AF9D740F272661045437E6D0C13BE05D87821365509758B075C7D15A421876CF83C9BA1458F3C2B85002238DA64ADCD77FE7F1B33F3798B6C87609708DDD1C4F41AB9781A3BEAE2910D3D0A2496927C4907139CBA8D5B791FEDA3CD472538D9AC0FB9895DF079AF86B439096783D2BC6E967C211B3969B876CC0322E59C80A761495F50567276349EA736A43E5BAF638971C355FA4D3621FC60BEF5408067B5968E56DB7F2A2438233EED3AB2792443618A535389E3E7A3A1C061A8AC72423508C2747CBA968B7220911D0324522CAAFBBA85B5D1A830FBB0FA20CCF10F78656B512B34B28815C23DF718AD044937BD73198798858E41FE5AC16EEF5AC6E234DE5D7AA5AF49ED0BBCFA35843C7753613988707C2025F8654A9C8A37B4320BFD243BF8B1143B2644A715910C46DA9C88AB8F033EA233DAF3660F8630ECE308DE44B4699D6CEAF275065BA4DCA584026D261F753CCD0A860F705201B64B32F159C86558E633312DB15C2C3164889F49F181435A1877760C50206816F3502CDC3229107F92355467ECA127B51912EFBC911C5591DEBECA2AA6015274382BA5734F64B08E1160A4CDCA78CF722A1A31A590B5E90185AF809CD39A56A0D945BCDF0769E923C45E52DE4B45E2E22093E856FAD9B9C00641970B31BC0481D91884782D08979AA02484CAA2957CC6CCC4434EB5C814973B7E3A284DBAB2EC068A5DC047336CAD2990D37231604C584E05C689E30CDCD7668BC764E3D96184E7C45D6D988AA9B03CD3275747B2E1F0C68CB76326F263EBB910366D6C4A94A8252F6BA0F5269531188811A7286D6A2AFA3158F8243B7695805B8BDA1322B1A30049C684C6DCB574C20A9574BA78677B1135135ACF200A22749115C7E1BFB6A1614A7BCF612562A3CF6702742F586B6F55169C5AE1D14621074034C109518C86EBF191881D4B692E4027BC9C195E20F54A36B8387845F47BF23482359D90BAAC0B24E091D6937AF4049A043529EB884189D2B6FD4B05473E3696512910E9189C1508DF56450BFCBB6E97747AFA545C64B346CCB04B6F30733A1071BB84DF18A2A762460F8E6A1D9F0AC2840474FB61FE7087818F6A53E7CC799B0508CE97D06987E24BCB4E4B559161C5F1AA628D68B129CD178CE6069236C863AF8A2480C149EE5754AC04719E01D86855201554F922167B623236B417FAC5948D182B12E8210FBE23FFFEB81AF88532F0193FD61BD82D59FE646209D39AD75831EDC605AA6485CE90023DA218CBB19AD8FA858E7A661D85A3ECD136809E70B25F166339C307C350E4D5303AC844257872A2845A7F6CAB996918AFEC669C9A19D43F72A5862BEEA66AAB0D708C59C260DF09EC0BC2A265A311309CCD2ECCA4FEABD6176633863A71D75B0E7957266584D000A6824D467EC50421AA1369193B418072815562928D35C16227D22C821A8552ACD83A513AB683CC39D12537DC4456E1476C4ED9B4014C42A7BA6A78C9B72F3437228A918BCC6C13FCA16BD04C7B2B277AA4C2B903517FB8397598554C9C8885380B4199450A46635E7B4B338075E15DA4A3630862CDC2B811578626C5BBE0B73C0F375376708B4D8654560782F3D56064B96E318E3FAF0D63CBD17F9966EE839107DD8D6530A49F344E194B76A22A9BE6B0A57E59B2F2194C4AF45A76286DAB2B0E0FE8DD37AF72ED021ACA617E5AE70771674BE8903CC21B3A90248D993C261B6CEEF2C747873D113869B55\"\n        },\n        {\n          \"tcId\": 48,\n          \"ek\": \"1CD8880468590163A4B0692D9569089E24873F9A5738826AA8A326B3D013C0F0C65B41CD81A42290F628F174006E08C3CDF9B012050C2FB083F97A7770F8A3B120972676C6944B06320A56605409E33535ECBC1B1EE6201A457309102D845CC631450C2D802FD62A793AFA82C0C9BAFFB22F8FEAC2B9680DA32A51AC1C75F7E3899FA01471F9668DEC6CD765221E765D7C714D46085CF9B9B6FCC2891EF16E246489CE87B9028A5DEC713B84594847A4554AB4A39985213D009C617C0524217AA1A51DABC9CBF689B66325517726CA3E955F742126F13B72CE936EE1CA2D913B785B4704F975C221437C6A890E354810E27B57689A8564DB22783B3D46488F8646B50C366BE75566EDC0422C2784B7A33D10588BD83A89A07519B754C1EFE901FA9001C0B120A70C0BDC8CC5342C8D20281B8A7A24FF4240FF6990BFAA3197CAA594F63EE4C75B3014AF834BC4FB8249604855597B5ACEF45304D75C035981A8A7267FB04867E9C28EDB8BA6C64BC6B70B218CC738B097FD6527915018EBF712E7044BC7B50CB9151395814278089E9A003965B4AAAE46A19E5791331876B985BF23DA8BE19CC4EF857C98C91084C1CF0441A1B212580E271FEED51C98D88D4003B909B42627860E5B788F99521CB3953371F1686A437A2F3055F8D05B7FB01BA29886DD28A7269A99DF979DD80BCA5D399AC18A919985C643D68910F9C0C5923CC3A30C25E8B9F71935503513AC28A520D194CFA7129BD2A08CE76491B886B7531ED100700EA011D09319EB66B8B44A0C09913A0E40A2B1166DF676C205E648872386A221B45A428B9BDB0C8281032D52C22028BDCC96AC2CA6470F407F98D479ADE32B1D202F1F1141C3E6453084B551554989E00BFC2A44E7F5ABC8530FC174B6FA30B3F26B5A467A39322612B1C9C3CE01CFC540CEF912058FBACCEDD8BED38AAB401B88EE7058195CBCB7D8405CB71DC5B44EA19B34C9D1652E7192D50A03136BB87CB207C9DA8ADD47AD870A3AD5DA51B5E8874F71560C5490AD7137522823B167166E89BFCDD3492AB836ABC952A5E957E7A0CA963A2C697AC1F42664D7379E6AB2361D657D47CB34236B70F9658E71FA7CB2D51BABD2C67187022380335D9014176BC52878C1CB721CFA6B63FB92C55A9A53EA635C7522A93DF572DF5B01E4C4931DD47ECC73514EB5A6085206B79209A712C60BA4B338254411D08DC7C289E00C0C3F230406B47CCF4183D97005A0687F6BA6B7A02A4E9E781C99C28DBA8A32DE80AF53075CACE4629F03872F3606228335424A1311BB58BA3309D4087F839960DC3CC63D83832F0768BD13BA41EC2ECFB03D882799D10653A4F6BAEF00709169346FB83232F9685E309E36B23DF53AC8D0996FAF14440641AD59E3991054678596849FA44436454BD535A2692922A3C57BFFDBAFE0913240EAA29646A8B5E7461C269F58F92680401B70A82529634325D128AD23A452EC9A5E52A622A524F3205B6E9130BAC6830C3B109164A65CA9091226666DEC395AA63B0899BC6C798F86F74E3E5265CABC600C193AB7D1AAD5085D2C198DE66A4BD4649A26349AE5C88FA44BAE97B91D2A17B8578942B7D61E25E367CBDC29841B0116458C97F72499AE6B74006261E57B749B66ABDF0FF71F8CCEA4ED010C6A97739D7351\",\n          \"dk\": \"F14A379CCC4C2B873E4C6366D676712F1B15BC620AAE40C3B6F31739FC2889ECCED39507D9148082B2B37C70BF95E42A98CA9B13C52A22604644839DA14B67B3BB472E376F52E20641E06E3E6104B551353FF24646C97E0D1C8A0008478158CCE8E8B6D522896A395DD8F6AACED41A58D3A9C0C0BB23C55484A9CB9BE11E9BF370BBF045505C99A08C6D8B62825D1020B3794999F63BB083BDF260714D16ABC01C741C48776067C373CBBD24558A1A4938C3369955EB0D8673C1323A243CB72781C68BB64C1E754C702C832D68F5C0A1E1A1CA04A0077955CF131B74CAA9BAE30284BA467AAC056CDB8F47B5848CC03383B275C4E62B34881E8FF504E30893A5A7C6CD9038149C0F83BB0410578A3BF42A7B7BA7CC174ED0613FEBB88E5CFC7A8E9204D30A48D4C33FC0752169874963A88A1019BE594B502FF4A341D3C59E14100AB3773AC43A92F0C901A04A1FCC256DDB73D6F97AE58ABFA75528E6CAC933E2B3A5A84F8C24137670C531E6BE9CD9758BC5BB043266A22104825845196895D6966577325769220775C9C2B480C36E6C2C18B7255BB5277CD40DEA96806858299C201FE60C5A8E590F565863DC254FE8431A3E891149D86D8CC24EF5114103D8812FE282285C3A2806432AC7629716CC5DEA1EB9576E2B9562809A0AE66790F654278C16B355E1A4087A9600382F35BB3BF9F9541B4C5A98382D47D4B569685537662D4D891B7C949FCC4A27C81CA7A594BD66AA09AADB27AD43420863C2CBFC7A5D818984E343785C863085A97B2B77E8C5054418BF43BA61E62594E7011C1B3360F278130595959FA026C14A893579363BDACE7C291065C27C73340D400747BC29458FA324CD8880DD84A09FC973487358E68BB3221C4E5C4133914A13D2C53297BA10F02327FE37C43681A028A865576410F7DA2A7FA8867D563399F59DE8569485043623A1448C613B6424CCDAE8C59CCB53F1E8B0EF4A5751367F193637B6F78E0AFC2013FCBA7FE8AB5D7C70385A6869221A8FD88E246C7DEC6C83187C9680A8654E02269CFABF7438A234037BEAA7AF8D54448035BFE7A7430E264CF3F607DB07C3EB750BBB1926D5ACC839F02612C4BE88422AD9731FACC22F6DB1213DA36ECCAA97F7B5CC94146A0C50671F7725388712B30A1E8EFBBE72F693B7652F7FE5A9C3E10415168F6F9273AF55109BC9BAFFCA3E80989BA88905C1DAB706BB77ABE38D4235C44B43515DBB6CB15813EB4B0D8EF97647B331C689AB67E3921B840FB78804F83B0DE72239EBFA22CA799BD88107C36A505DA09E9EA4C4EE5802DB5047D2FB6B9B2C499EE55CCE4347A33C7A6EB2337D3589A3D099939B0C8619AE7A2414F0698B4B06D07648B2059C8BF8583095426A0776C81D4C5D24EB5AFF450E79FA07FB7026378B1E86D6AD2BA7227475261F37A6C58AABC6447E054AAB9C61A6E19B571AA57005E8906BDB6960A425B8B95671407145837FDD9B5CF51A4E42592CDE24332673CE7D023A3E2544970AB55857A1F0200521114B8096A366417052094175B549F474173E723A2377525EEC95C0D40C94988981792A8C324B61EB381F8ACD3BA987023B2E64D29C0FF63D5134527FA8AA5199667ED2331CD8880468590163A4B0692D9569089E24873F9A5738826AA8A326B3D013C0F0C65B41CD81A42290F628F174006E08C3CDF9B012050C2FB083F97A7770F8A3B120972676C6944B06320A56605409E33535ECBC1B1EE6201A457309102D845CC631450C2D802FD62A793AFA82C0C9BAFFB22F8FEAC2B9680DA32A51AC1C75F7E3899FA01471F9668DEC6CD765221E765D7C714D46085CF9B9B6FCC2891EF16E246489CE87B9028A5DEC713B84594847A4554AB4A39985213D009C617C0524217AA1A51DABC9CBF689B66325517726CA3E955F742126F13B72CE936EE1CA2D913B785B4704F975C221437C6A890E354810E27B57689A8564DB22783B3D46488F8646B50C366BE75566EDC0422C2784B7A33D10588BD83A89A07519B754C1EFE901FA9001C0B120A70C0BDC8CC5342C8D20281B8A7A24FF4240FF6990BFAA3197CAA594F63EE4C75B3014AF834BC4FB8249604855597B5ACEF45304D75C035981A8A7267FB04867E9C28EDB8BA6C64BC6B70B218CC738B097FD6527915018EBF712E7044BC7B50CB9151395814278089E9A003965B4AAAE46A19E5791331876B985BF23DA8BE19CC4EF857C98C91084C1CF0441A1B212580E271FEED51C98D88D4003B909B42627860E5B788F99521CB3953371F1686A437A2F3055F8D05B7FB01BA29886DD28A7269A99DF979DD80BCA5D399AC18A919985C643D68910F9C0C5923CC3A30C25E8B9F71935503513AC28A520D194CFA7129BD2A08CE76491B886B7531ED100700EA011D09319EB66B8B44A0C09913A0E40A2B1166DF676C205E648872386A221B45A428B9BDB0C8281032D52C22028BDCC96AC2CA6470F407F98D479ADE32B1D202F1F1141C3E6453084B551554989E00BFC2A44E7F5ABC8530FC174B6FA30B3F26B5A467A39322612B1C9C3CE01CFC540CEF912058FBACCEDD8BED38AAB401B88EE7058195CBCB7D8405CB71DC5B44EA19B34C9D1652E7192D50A03136BB87CB207C9DA8ADD47AD870A3AD5DA51B5E8874F71560C5490AD7137522823B167166E89BFCDD3492AB836ABC952A5E957E7A0CA963A2C697AC1F42664D7379E6AB2361D657D47CB34236B70F9658E71FA7CB2D51BABD2C67187022380335D9014176BC52878C1CB721CFA6B63FB92C55A9A53EA635C7522A93DF572DF5B01E4C4931DD47ECC73514EB5A6085206B79209A712C60BA4B338254411D08DC7C289E00C0C3F230406B47CCF4183D97005A0687F6BA6B7A02A4E9E781C99C28DBA8A32DE80AF53075CACE4629F03872F3606228335424A1311BB58BA3309D4087F839960DC3CC63D83832F0768BD13BA41EC2ECFB03D882799D10653A4F6BAEF00709169346FB83232F9685E309E36B23DF53AC8D0996FAF14440641AD59E3991054678596849FA44436454BD535A2692922A3C57BFFDBAFE0913240EAA29646A8B5E7461C269F58F92680401B70A82529634325D128AD23A452EC9A5E52A622A524F3205B6E9130BAC6830C3B109164A65CA9091226666DEC395AA63B0899BC6C798F86F74E3E5265CABC600C193AB7D1AAD5085D2C198DE66A4BD4649A26349AE5C88FA44BAE97B91D2A17B8578942B7D61E25E367CBDC29841B0116458C97F72499AE6B74006261E57B749B66ABDF0FF71F8CCEA4ED010C6A97739D7351C57B9807586DB3D99C6AFFAFB04CD2551A4B1DF17FCCB8D7D94C103EE6656B14BF83E3048B021F22DB57076A885729F95119CE63FAF51A69954BCCC51E014686\"\n        },\n        {\n          \"tcId\": 49,\n          \"ek\": \"A9D674C360C5B1E36D78F66C722392F167A126F09FCC4310A9320137EBA898E786F95B9CC5D0B9114206EC66159BB3A687427D3B2298F1317ADE160EBF44CAFAA62B38871C6239C38F67CA0922C9DD952D8AA55AA6A481386771FD003A271B659F4B730B589B717635E6EB62603C5BD6C96756DB4947C5B1A71B970CD16B6D2519205C08147685B7103BB7C130B40C11CFE9C73866567175CCCDF40522C10A80140D78F9A71C714BADB1CE2732B86BD0518BF4783FAA00694208C3C2BD4BA34DEC08ADD425A0687A0D8B392702DA57504A28E9099912A8AE4A964765D35E739C1C3158330F4C21DC3A58969AAEBC788EC8177443732EAA527DC1E5009E010D8F980369FCCB48527AA27C6553393709F0B252E9346671465D33C072E352B2619175878F946488AEE3B5EE201C753244E6C85A1DE48BC994B4B1F9AB4F81B9C9F6C243C4A57BAB99781CAB1FD152A205A893F07F16431401F746B5EC790EA89C8C672EB7267EC34036F5F541C334B499DAB565387C2341C27F4904546917295B8F640CBCDF9B254502B58A669D36AA3D834793CC2A4D6578397D3A6DA893710F6A1839D8CAD7067A020108BF98415569A5C5856C5678C9A41A3075F077E6150F54B57FE91691A1C05F5022939A322CB919ACDA1236FEC9885AEB2C3A975E5BA238F5E713AA751093846915B003E624577007C7048B23E29445EE123AD3D6604D5006332CCD238A0E23020244D99A2C8A28CB58A76A69A39CC05927767ACBF892E3B6CB27231472C9189FBC1BADD18CB9A0CCB9E9CB08D0BA65C9924BCB2D88EA9739835C3B388A7B416A5BC5A27B01260D732F237474292133F0E458F33CBC3A687F149A1F8D7B08FE76A0C7C9330F8582D378BE0E2B135097CC9527A24FA3913BC3C98F04298CA86A08F6969D98C6EEDC19D6026D18D93479698A237B319B8CBC7E266DD34162FDB07C04A677C7B27E758A9D42DB0E35B4369C325C1F5639E0AA30B87433BC9C977447A2F97413E351BB98242D68E02527053A75A0CA93AB8E1E8B521F4BA09EB1B32D6797C5D7BD2AC76E81946CA48BCC787A9D35022C31F0938F710482B1A5DDF91A9024900D019AE33B182D6424D86B558109158B188C3978B5345A7D85B70B30888083C50CFAEA947258891A192B05D8589A0B7415F6945D1275B9C12DC25055B372553D21ABC7B96BD7F821B0C78D6AC3CD5BEA024720625611126220A5A467A60C268B748632C6B19403391391D649BE701B85592FF7F256937843547339E01179E2D79F9D88168528CE5FD5A819108456A789F094093E3980413A14F11B71C57BC71617CA3234008B2907447B8EF8A58B87450AD9506BB84C9E3F66BCE6CBC5D503434D406387D8CDC22A64D53786C79A90C911723D8799B3A7B9AB20672DA92572FB609AC14D57435736055B89F9851DB4B621AA42610317F15802FFB457620B4A7712212DA5725C049E1FD3670DF15ED5A945549C38DE4826BEC844500A313B41921E21AA5EF186940497462117EBD8B383520680DC2646683E0E819B2DE992A82380F400BAA20560AB7A58D2176D4885204ED705F21084390C5AAB8879214286A1F199B3949A59E11B86582B5C13080766C7E2091EA99F1352D4488C7EA1547DD87024156A9FB2DF0C77B684540084890847AA6D85\",\n          \"dk\": \"49B061657A8EDBA51C06A38F4995CC0D68BD97C515A7576125F7961317046D3B0A21B44415B07592991570D05291AB34DCB808E57041FF249D97F471B57A8B7924BB8B699B9772360A8157CE513C58882B89F79E37287042A353A887761D73B2AA3A5185D999FD1A4609AB7FB3ACC8EA083E6DDB2A9592413CA11E7D51AD6378132AF88D977762A473A4849A303C648183D03773C610B8C07A6F741D2E887768E20FA9B749072CB64292B791731658E7297F8A584EFA2A9446911E155E0E27BD71DB8B1D52395982C40C4C65C7FCCEE20B684C2432A83579468B8A82A730305A157FF46F5C8781CD621143E31A20311803185335855116712E34498045093BAD6B80A8F332AB8C5A90025AD0B1CA6EC53401AA83DD01595540141C749687A53D775B4A691B69F6D32C275187A6CB1A4A336C341CC36CCB295C21609EAB1636EC0BB031345A3BAA345878EF43A46B82A9DBB7019658A43AA0368CAB4310103BD5589499B2C1E89A5C2A297ED707B7FAF8170FD2CAE245146929060CA5BD10D8316980C18824CDA429302B64C14385CBE36AA0DC6CB0941B204A55A710D2C68052C900C34C35A7AE7834CF5552264AA2583913348CDC79A08847ACE64024464E759AA99573BFC7B40F8CC4449379A40558B756B22410934C7969B42ACCCCC7D8231E5CA2DF5574384B9E0712C436FBC67AF74234E49B59480B0CF47C8DC31D47776BA9B000C0E80856B889FF09916CF8CE199680A45B16B1AC1438000D6472C79497014B6384AD069FA8AB1A004B518AA97066DCAC32D00893301D327008020431F2B01A4004523F0453DB14B3A84001EC3749D25473EB219AF1E0A7628BAE4A24BE7A1A5E24C3AB5D0A6661C4C1DE754755EBBC91865E8147C1D0233DEA643C4E648DBE3C50B96B898D81B4DB71CD9CD38BFFCB9E7C384EC58A7A27F29C7C5B20255149EDD02442E4CF8C8A34B73B6D36319F6FD3425BEAB4A8EB1698E03A1A344C88808FCF183AF5F6905B1B445CC9235BE74C1DB07D794BB867331EAEFB52AA165E31DB2048A62CA4D5197B3AB327281B132B0EAA18B1D8699EDCFB730B905A517036E3EC9BF40AC4F47440BFC231693025CA297B4F5198DC094066919934F32E5EA6712937CFE458A2BF916D382933A95487603B4AB303CC81BA94E467576787B7D8095B059020E6C43069745CA03C2B31229F2CE021F4D9BC38FC2886EC9570F2671872C766D40C970A0825083A01961FE97C9214533F3A8562E620A0C8B5354E8989420B4F639B323E415D8AD4732F299EFFF99A7AB47A100CB05F31C158F37FBF8B7B7F477DD3398A20F07F9BA0605AC67CE4379932D0B2E7E236882093846AC42B64806F4C3E035BCD1EBAB1D54084B63C870217B210D2CE816A35BAEA8CCF2C7FB4938FDDC320D9B81751116C4DBC2269C37413206704B631BE653BD20B7F920305F9EC8C46805E95FC504B1C33BDB5A1AD912F8D65221A784922A63578201FF3E869DDE76AFDC9068F657CD0B66E8D441DF7E642A01220F8001D6A1558F5F73F49C13F3E1BB8A9E7BFB56B677CD6AE6FE8C85C24CCD7A78790E1AD8809A8F964B43228155D2872A9B11849514035F154799202A0299C41F80D9D1A67A9D674C360C5B1E36D78F66C722392F167A126F09FCC4310A9320137EBA898E786F95B9CC5D0B9114206EC66159BB3A687427D3B2298F1317ADE160EBF44CAFAA62B38871C6239C38F67CA0922C9DD952D8AA55AA6A481386771FD003A271B659F4B730B589B717635E6EB62603C5BD6C96756DB4947C5B1A71B970CD16B6D2519205C08147685B7103BB7C130B40C11CFE9C73866567175CCCDF40522C10A80140D78F9A71C714BADB1CE2732B86BD0518BF4783FAA00694208C3C2BD4BA34DEC08ADD425A0687A0D8B392702DA57504A28E9099912A8AE4A964765D35E739C1C3158330F4C21DC3A58969AAEBC788EC8177443732EAA527DC1E5009E010D8F980369FCCB48527AA27C6553393709F0B252E9346671465D33C072E352B2619175878F946488AEE3B5EE201C753244E6C85A1DE48BC994B4B1F9AB4F81B9C9F6C243C4A57BAB99781CAB1FD152A205A893F07F16431401F746B5EC790EA89C8C672EB7267EC34036F5F541C334B499DAB565387C2341C27F4904546917295B8F640CBCDF9B254502B58A669D36AA3D834793CC2A4D6578397D3A6DA893710F6A1839D8CAD7067A020108BF98415569A5C5856C5678C9A41A3075F077E6150F54B57FE91691A1C05F5022939A322CB919ACDA1236FEC9885AEB2C3A975E5BA238F5E713AA751093846915B003E624577007C7048B23E29445EE123AD3D6604D5006332CCD238A0E23020244D99A2C8A28CB58A76A69A39CC05927767ACBF892E3B6CB27231472C9189FBC1BADD18CB9A0CCB9E9CB08D0BA65C9924BCB2D88EA9739835C3B388A7B416A5BC5A27B01260D732F237474292133F0E458F33CBC3A687F149A1F8D7B08FE76A0C7C9330F8582D378BE0E2B135097CC9527A24FA3913BC3C98F04298CA86A08F6969D98C6EEDC19D6026D18D93479698A237B319B8CBC7E266DD34162FDB07C04A677C7B27E758A9D42DB0E35B4369C325C1F5639E0AA30B87433BC9C977447A2F97413E351BB98242D68E02527053A75A0CA93AB8E1E8B521F4BA09EB1B32D6797C5D7BD2AC76E81946CA48BCC787A9D35022C31F0938F710482B1A5DDF91A9024900D019AE33B182D6424D86B558109158B188C3978B5345A7D85B70B30888083C50CFAEA947258891A192B05D8589A0B7415F6945D1275B9C12DC25055B372553D21ABC7B96BD7F821B0C78D6AC3CD5BEA024720625611126220A5A467A60C268B748632C6B19403391391D649BE701B85592FF7F256937843547339E01179E2D79F9D88168528CE5FD5A819108456A789F094093E3980413A14F11B71C57BC71617CA3234008B2907447B8EF8A58B87450AD9506BB84C9E3F66BCE6CBC5D503434D406387D8CDC22A64D53786C79A90C911723D8799B3A7B9AB20672DA92572FB609AC14D57435736055B89F9851DB4B621AA42610317F15802FFB457620B4A7712212DA5725C049E1FD3670DF15ED5A945549C38DE4826BEC844500A313B41921E21AA5EF186940497462117EBD8B383520680DC2646683E0E819B2DE992A82380F400BAA20560AB7A58D2176D4885204ED705F21084390C5AAB8879214286A1F199B3949A59E11B86582B5C13080766C7E2091EA99F1352D4488C7EA1547DD87024156A9FB2DF0C77B684540084890847AA6D85E8BEB5E40DA16CD0B6771A006BD6CC2A5BA77C278E3EDF52912210F80A5E1759F42861EFF7691614C3E8975AFB4E353F8C8C39E6F41BB637EC79BAA976D1ADC1\"\n        },\n        {\n          \"tcId\": 50,\n          \"ek\": \"744793A77A843520C9EB8B057FF28854193916F711902B1F8B4B9C235251FBDC8040C8A38C022557DB50CB28CC8DF99A859C2EC017C24015877D2A3572571FC783BF1E984401C11158E32A95351BE407A8AE6B5E054A33C2427F4A346761559F776671095B495074AF5CAC98D94297FB05624443CAA8151AF3A497F73203EF184CC7336650427CDFD47B10245EDDE30C3075933E431DC1B1C2A77B2073257C2AB73ABE706DE138CD54922CDB09CEFA60BA473092E43BC3E3CB951939B1C4CB2AFDCB4F8A5310FE9A790E560B25A40AC9E18D35A27238A60A126B398980BE8425C8A01B930FB336CB359F58131BBEE6AC450B8C811B2DDEA836F9D0C2C72A709E134321DCA727641A1D678A8E0CAE6BD6BBDAD8021AE3C1B6067FB1ECA42D215EC2C4297F6891567712F75047775B741713097B1271AFF026323A134DC20DEBFACE46D5B01820A9A862BA93E3AFCB5AC4C2204AF2E15787D0C857896D24DCBD6393C1FDDC12CDDA6A1F337B18D6C3CCD5283B37239A80424780399281C3FF1967B3BB6DAE499D42F511997077F38A8D63EB319342B1BC749924E467C6A2A11728826BEB52A39BA7A1A03C93C613D36A3F1F544B72A8BEC24009FDE07E677101456A2B76EC3353CB8944C339081A4B59672CD1B5446FFA6FC0B468D9FC83EBF896CE12158AFAA6812028F54B7920636784D3B3854030D05A9C19D936C6FACC5B81070CD524728A96A58A2E1207C73A4C533DDB00098141158B11DB674500781F47DB9B45F3CDE244502D377886418287C22075B2BD172606CCFA6F4914844525C113BCC7C049B3D14A2DF91B6C62E0A2EE5922DA3194DFC0C506D48ECCF5697BA31BD0839A4D3B3078CB7C2936CE1C507B0B54023585CADA6BA5657726075659DE16A6403110CE38A247CA85CE02C1DAC79FD175BCAAFA116F710516A4B73D78BFC6F9854228B6C5F035946B1F61B629F31328A65556C8F0661D58622C6021F58C1ACF154C6E78BCF563352AFB89B824513A35502F117861A34C52B1011F696A85185186BBA23B518C720763529C181D902CBBB58CFEF77CD2E72F4780979FD46126C0A4BC1965A6E070FC284B1F856852110E19C268390CA9494116EB293F1A111ABEC1AF04F706B114617F0362B3DB4B99E076287AAFB7127FE2C68904F61664D63D4FB267CE07AD5A659FB81886F8E0A9C26261A0055F48F432E9F8242C439F58B5A21EB777483B2E68B3B1118562430270340243F1E87C22CB5BE2F167DC39CF4C69B8B8E9B726256CBE605342F47E23A871BA7250BDA8997FD474B3375920522F2C511B3EA35CF459342893AA12B128CC2184E304335AC7BA04653898590A664672FE744F301651442BB341D32F36262C576A50F803B02F91105FEBCABEF7030D2A76CBEC6C2CF36283590BB44842853102D3D350801167D7A96968648C0CAB9AFD39CB7F658909283CB83CA78544929AD90069CA3E660196792A654A911CAD7C23024C90E25AA61B30A4D568C5A5C12A53565A93E910D5940EF3583856017BC25B05C4473F48584C6908C13D2B19E7A3A7992976D31A464807199AE3A6307CA55765590F6B4721632D70FA47E2C1098E36898BE3125E43C61CBB7A6C246CBA0002CF325A5D337989289BCCDA54835511DE9656287363DEE85033410AEAE1\",\n          \"dk\": \"045AA22AEAA4A8E0C66661A183700F04025D413CCDCBE1B7CD9B839BA61671B08A859B1AECA9014CB3695B29703CD4314954C48CC19A89E106079720EF23B597FA6A833638B7A7B1E74A40C8BA7A005AA799D22DC5AA97627A037BA2623C659621EA2FF40651DAA0A95B2218CF934DB132BDE8A29F889BA084DA3EA9C8B58FD1BEF4D45EB4A2501447640C7869F21C9B031718209288E442018F63097459AFCEB75815829B16EC48FF822B1F69B6B48C3AA3C65441E60F552A154BD85A71397AB8290564F527FA8B1E84AB4EE6450EB5CA658B2120164B7D84637CC7488FFDF717F9349734E6B2F585C040065A2E910CCCA070C0070C2C215BC8060930D30D7A73A0FADA94E46257BE169B73D7A35DD43EB9B0183589AD657A429D983564031166253B4EE513AFE986E007727D75BA22F41A0AEA4939081C7CE1BEA5AA8D8DFB692AD37726E05A9DBA9D70102A1DE64479A928D9789035D4B645F088ABE8A51B06826BA64079C4AD3EC2C8A7EBC9F5EA6A0C707E88B2224DEA55FC0C39EC258088E319CA48C3F0C86AA203669F164E5FD0BA467548050CA496579BD4099D705A82BF9634995ACE60DCAC96C9115226AF2B6A9D86497DA1C49FEEE35F5639BA1A8363DF5114EA06C190179161C3C4238115DD55200BD28A03EACCAA46C7A9126B6F0CC2BFD9047D9BBF7A2043BD965F244158C7F10548FBBC39E040F040145A45B12BC90A74065BD3E34925C94FFA968BEAD241A79B6B0F1C3911C444E3958906DB8834984897CA886DC1B9D63CA43803C74C5633F003BD148A691D396C98BA81918A560542CC6435082957A70A312DAD0AC54F81903B2280BEF3638A013654D26E0D2705939A31EF39547FB80BE113A336E277E04B8B905C9DD90509C434277246916A1A0A7E66BC1C74C4719751BD8879E07AB97721C0B27BC08B69B284F2BB47E43290F2BB434C489570C22AC867F06643AB100457D336CB944B6B5A185B639A403A332FC755584A9F2EAA315C319D3099B30710B7EFAA5BBE8A3981A9B158E0C7484595AA1B7CA3F335BB38A7E5AB51F2799E0EC2049AB781514A82CA7CC5A53BCFF3029320636285902EC1D24432B5292EF7600097929D374F3485AB2AA5C8A154CC3171693DBBC4382A5E44600AC8B1646A84A47B078D06EC554A309A1FC2BB473C69EA4732B6485BE812A1004D583D9A0B9953312CAB9B014A482D8C7BD1097231D0041FF599D0E043C7CA08FD0CA71AEB977EF271EC13AF122766566BCAEAA26554F87394847C50DC2B8D3885184069B1A173D1183E68C55867E55EA78610CC3183A7F60181D1751A12107225C7156B99F86846FDF5777502A201FB583202B28EB78EEF356918D2B5B7594AE364902D45C2E8691137C98CA7B2425274068D765AFF29095ED546309C2D4CD6A5DE0571E67566DB14C386A87F15379CD1190EF02806D6B8387C87C73B805E01DB128C0A86C1013FAA7CB7D46770A0835C5343C931AB8CFDF56AE4241108C55976F127334AC3E2B52360661B61532CCB641CA043AE9B3173D0133CB0E4581748B6F1AC00BA0BAE4960376782A25F36765A1A4318D73C18781C90755C657847ED6849CE1301B11A2FD75616B4F326DAD545744793A77A843520C9EB8B057FF28854193916F711902B1F8B4B9C235251FBDC8040C8A38C022557DB50CB28CC8DF99A859C2EC017C24015877D2A3572571FC783BF1E984401C11158E32A95351BE407A8AE6B5E054A33C2427F4A346761559F776671095B495074AF5CAC98D94297FB05624443CAA8151AF3A497F73203EF184CC7336650427CDFD47B10245EDDE30C3075933E431DC1B1C2A77B2073257C2AB73ABE706DE138CD54922CDB09CEFA60BA473092E43BC3E3CB951939B1C4CB2AFDCB4F8A5310FE9A790E560B25A40AC9E18D35A27238A60A126B398980BE8425C8A01B930FB336CB359F58131BBEE6AC450B8C811B2DDEA836F9D0C2C72A709E134321DCA727641A1D678A8E0CAE6BD6BBDAD8021AE3C1B6067FB1ECA42D215EC2C4297F6891567712F75047775B741713097B1271AFF026323A134DC20DEBFACE46D5B01820A9A862BA93E3AFCB5AC4C2204AF2E15787D0C857896D24DCBD6393C1FDDC12CDDA6A1F337B18D6C3CCD5283B37239A80424780399281C3FF1967B3BB6DAE499D42F511997077F38A8D63EB319342B1BC749924E467C6A2A11728826BEB52A39BA7A1A03C93C613D36A3F1F544B72A8BEC24009FDE07E677101456A2B76EC3353CB8944C339081A4B59672CD1B5446FFA6FC0B468D9FC83EBF896CE12158AFAA6812028F54B7920636784D3B3854030D05A9C19D936C6FACC5B81070CD524728A96A58A2E1207C73A4C533DDB00098141158B11DB674500781F47DB9B45F3CDE244502D377886418287C22075B2BD172606CCFA6F4914844525C113BCC7C049B3D14A2DF91B6C62E0A2EE5922DA3194DFC0C506D48ECCF5697BA31BD0839A4D3B3078CB7C2936CE1C507B0B54023585CADA6BA5657726075659DE16A6403110CE38A247CA85CE02C1DAC79FD175BCAAFA116F710516A4B73D78BFC6F9854228B6C5F035946B1F61B629F31328A65556C8F0661D58622C6021F58C1ACF154C6E78BCF563352AFB89B824513A35502F117861A34C52B1011F696A85185186BBA23B518C720763529C181D902CBBB58CFEF77CD2E72F4780979FD46126C0A4BC1965A6E070FC284B1F856852110E19C268390CA9494116EB293F1A111ABEC1AF04F706B114617F0362B3DB4B99E076287AAFB7127FE2C68904F61664D63D4FB267CE07AD5A659FB81886F8E0A9C26261A0055F48F432E9F8242C439F58B5A21EB777483B2E68B3B1118562430270340243F1E87C22CB5BE2F167DC39CF4C69B8B8E9B726256CBE605342F47E23A871BA7250BDA8997FD474B3375920522F2C511B3EA35CF459342893AA12B128CC2184E304335AC7BA04653898590A664672FE744F301651442BB341D32F36262C576A50F803B02F91105FEBCABEF7030D2A76CBEC6C2CF36283590BB44842853102D3D350801167D7A96968648C0CAB9AFD39CB7F658909283CB83CA78544929AD90069CA3E660196792A654A911CAD7C23024C90E25AA61B30A4D568C5A5C12A53565A93E910D5940EF3583856017BC25B05C4473F48584C6908C13D2B19E7A3A7992976D31A464807199AE3A6307CA55765590F6B4721632D70FA47E2C1098E36898BE3125E43C61CBB7A6C246CBA0002CF325A5D337989289BCCDA54835511DE9656287363DEE85033410AEAE16C770D1FA4C0F5DBB660530772FCC2297F59BC9DEE338CD124F0924CF7E3762D4DD0E86091649A0A08EA44DAB85DF56797F8BF46222C2DBA7DEC6374B9B2268E\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 3,\n      \"tests\": [\n        {\n          \"tcId\": 51,\n          \"ek\": \"A04184D4BC7B532A0F70A54D7757CDE6175A6843B861CB2BC4830C0012554CFC5D2C8A2027AA3CD967130E9B96241B11C4320C7649CC23A71BAFE691AFC08E680BCEF42907000718E4EACE8DA28214197BE1C269DA9CB541E1A3CE97CFADF9C6058780FE6793DBFA8218A2760B802B8DA2AA271A38772523A76736A7A31B9D3037AD21CEBB11A472B8792EB17558B940E70883F264592C689B240BB43D5408BF446432F412F4B9A5F6865CC252A43CF40A320391555591D67561FDD05353AB6B019B3A08A73353D51B6113AB2FA51D975648EE254AF89A230504A236A4658257740BDCBBE1708AB022C3C588A410DB3B9C308A06275BDF5B4859D3A2617A295E1A22F90198BAD0166F4A943417C5B831736CB2C8580ABFDE5714B586ABEEC0A175A08BC710C7A2895DE93AC438061BF7765D0D21CD418167CAF89D1EFC3448BCBB96D69B3E010C82D15CAB6CACC6799D3639669A5B21A633C865F8593B5B7BC800262BB837A924A6C5440E4FC73B41B23092C3912F4C6BEBB4C7B4C62908B03775666C22220DF9C88823E344C7308332345C8B795D34E8C051F21F5A21C214B69841358709B1C305B32CC2C3806AE9CCD3819FFF4507FE520FBFC27199BC23BE6B9B2D2AC1717579AC769279E2A7AAC68A371A47BA3A7DBE016F14E1A727333663C4A5CD1A0F8836CF7B5C49AC51485CA60345C990E06888720003731322C5B8CD5E6907FDA1157F468FD3FC20FA8175EEC95C291A262BA8C5BE990872418930852339D88A19B37FEFA3CFE82175C224407CA414BAEB37923B4D2D83134AE154E490A9B45A0563B06C953C3301450A2176A07C614A74E3478E48509F9A60AE945A8EBC7815121D90A3B0E07091A096CF02C57B25BCA58126AD0C629CE166A7EDB4B33221A0D3F72B85D562EC698B7D0A913D73806F1C5C87B38EC003CB303A3DC51B4B35356A67826D6EDAA8FEB93B98493B2D1C11B676A6AD9506A1AAAE13A824C7C08D1C6C2C4DBA9642C76EA7F6C8264B64A23CCCA9A74635FCBF03E00F1B5722B214376790793B2C4F0A13B5C40760B4218E1D2594DCB30A70D9C1782A5DD30576FA4144BFC8416EDA8118FC6472F56A979586F33BB070FB0F1B0B10BC4897EBE01BCA3893D4E16ADB25093A7417D0708C83A26322E22E6330091E30152BF823597C04CCF4CFC7331578F43A2726CCB428289A90C863259DD180C5FF142BEF41C7717094BE07856DA2B140FA67710967356AA47DFBC8D255B4722AB86D439B7E0A6090251D2D4C1ED5F20BBE6807BF65A90B7CB2EC0102AF02809DC9AC7D0A3ABC69C18365BCFF59185F33996887746185906C0191AED4407E139446459BE29C6822717644353D24AB6339156A9C424909F0A9025BB74720779BE43F16D81C8CC666E99710D8C68BB5CC4E12F314E925A551F09CC59003A1F88103C254BB978D75F394D3540E31E771CDA36E39EC54A62B5832664D821A72F1E6AFBBA27F84295B2694C498498E812BC8E9378FE541CEC5891B25062901CB7212E3CDC46179EC5BCEC10BC0B9311DE05074290687FD6A5392671654284CD9C8CC3EBA80EB3B662EB53EB75116704A1FEB5C2D056338532868DDF24EB8992AB8565D9E490CADF14804360DAA90718EAB616BAB0765D33987B47EFB6599C5563235E61E4BE670E97955AB292D9732CB8930948AC82DF230AC72297A23679D6B94C17F1359483254FEDC2F05819F0D069A443B78E3FC6C3EF4714B05A3FCA81CBBA60242A7060CD885D8F39981BB18092B23DAA59FD9578388688A09BBA079BC809A54843A60385E2310BBCBCC0213CE3DFAAB33B47F9D6305BC95C6107813C585C4B657BF30542833B14949F573C0612AD524BAAE69590C1277B86C286571BF66B3CFF46A3858C09906A794DF4A06E9D4B0A2E43F10F72A6C6C47E5646E2C799B71C33ED2F01EEB45938EB7A4E2E2908C53558A540D350369FA189C616943F7981D7618CF02A5B0A2BCC422E857D1A47871253D08293C1C179BCDC0437069107418205FDB9856623B8CA6B694C96C084B17F13BB6DF12B2CFBBC2B0E0C34B00D0FCD0AECFB27924F6984E747BE2A09D83A8664590A8077331491A4F7D720843F23E652C6FA840308DB4020337AAD37967034A9FB523B67CA70330F02D9EA20C1E84CB8E5757C9E1896B60581441ED618AA5B26DA56C0A5A73C4DCFD755E610B4FC81FF84E21\",\n          \"dk\": \"8C8B3722A82E550565521611EBBC63079944C9B1ABB3B0020FF12F631891A9C468D3A67BF6271280DA58D03CB042B3A461441637F929C273469AD15311E910DE18CB9537BA1BE42E98BB59E498A13FD440D0E69EE832B45CD95C382177D67096A18C07F1781663651BDCAC90DEDA3DDD143485864181C91FA2080F6DAB3F86204CEB64A7B4446895C03987A031CB4B6D9E0462FDA829172B6C012C638B29B5CD75A2C930A5596A3181C33A22D574D30261196BC350738D4FD9183A763336243ACED99B3221C71D8866895C4E52C119BF3280DAF80A95E15209A795C4435FBB3570FDB8AA9BF9AEFD43B094B781D5A81136DAB88B8799696556FEC6AE14B0BB8BE4695E9A124C2AB8FF4AB1229B8AAA8C6F41A60C34C7B56182C55C2C685E737C6CA00A23FB8A68C1CD61F30D3993A1653C1675AC5F0901A7160A73966408B8876B715396CFA4903FC69D60491F8146808C97CD5C533E71017909E97B835B86FF847B42A696375435E006061CF7A479463272114A89EB3EAF2246F0F8C104A14986828E0AD20420C9B37EA23F5C514949E77AD9E9AD12290DD1215E11DA274457AC86B1CE6864B122677F3718AA31B02580E64317178D38F25F609BC6C55BC374A1BF78EA8ECC219B30B74CBB3272A599238C93985170048F176775FB19962AC3B135AA59DB104F7114DBC2C2D42949ADECA6A85B323EE2B2B23A77D9DB235979A8E2D67CF7D2136BBBA71F269574B38888E1541340C19284074F9B7C8CF37EB01384E6E3822EC4882DFBBEC4E6098EF2B2FC177A1F0BCB65A57FDAA89315461BEB7885FB68B3CD096EDA596AC0E61DD7A9C507BC6345E0827DFCC8A3AC2DCE51AD731AA0EB932A6D0983992347CBEB3CD0D9C9719797CC21CF0062B0AD94CAD734C63E6B5D859CBE19F0368245351BF464D7505569790D2BB724D8659A9FEB1C7C473DC4D061E29863A2714BAC42ADCD1A8372776556F7928A7A44E94B6A25322D03C0A1622A7FD261522B7358F085BDFB60758762CB901031901B5EECF4920C81020A9B1781BCB9DD19A9DFB66458E7757C52CEC75B4BA740A24099CB56BB60A76B6901AA3E0169C9E83496D73C4C99435A28D613E97A1177F58B6CC595D3B2331E9CA7B57B74DC2C5277D26F2FE19240A55C35D6CFCA26C73E9A2D7C980D97960AE1A04698C16B398A5F20C35A0914145CE1674B71ABC6066A909A3E4B911E69D5A849430361F731B07246A6329B52361904225082D0AAC5B21D6B34862481A890C3C360766F04263603A6B73E802B1F70B2EB00046836B8F493BF10B90B8737C6C548449B294C47253BE26CA72336A632063AD3D0B48C8B0F4A34447EF13B764020DE739EB79ABA20E2BE1951825F293BEDD1089FCB0A91F560C8E17CDF52541DC2B81F972A7375B201F10C08D9B5BC8B95100054A3D0AAFF89BD08D6A0E7F2115A435231290460C9AD435A3B3CF35E52091EDD1890047BCC0AABB1ACEBC75F4A32BC1451ACC4969940788E89412188946C9143C5046BD1B458DF617C5DF533B052CD6038B7754034A23C2F7720134C7B4EACE01FAC0A2853A9285847ABBD06A3343A778AC6062E458BC5E61ECE1C0DE0206E6FE8A84034A7C5F1B005FB0A584051D3229B86C909AC5647B3D75569E05A88279D80E5C30F574DC327512C6BBE8101239EC62861F4BE67B05B9CDA9C545C13E7EB53CFF260AD9870199C21F8C63D64F0458A7141285023FEB829290872389644B0C3B73AC2C8E121A29BB1C43C19A233D56BED82740EB021C97B8EBBA40FF328B541760FCC372B52D3BC4FCBC06F424EAF253804D4CB46F41FF254C0C5BA483B44A87C219654555EC7C163C79B9CB760A2AD9BB722B93E0C28BD4B1685949C496EAB1AFF90919E3761B346838ABB2F01A91E554375AFDAAAF3826E6DB79FE7353A7A578A7C0598CE28B6D9915214236BBFFA6D45B6376A07924A39A7BE818286715C8A3C110CD76C02E0417AF138BDB95C3CCA798AC809ED69CFB672B6FDDC24D89C06A6558814AB0C21C62B2F84C0E3E0803DB337A4E0C7127A6B4C8C08B1D1A76BF07EB6E5B5BB47A16C74BC548375FB29CD789A5CFF91BDBD071859F4846E355BB0D29484E264DFF36C9177A7ACA78908879695CA87F25436BC12630724BB22F0CB64897FE5C41195280DA04184D4BC7B532A0F70A54D7757CDE6175A6843B861CB2BC4830C0012554CFC5D2C8A2027AA3CD967130E9B96241B11C4320C7649CC23A71BAFE691AFC08E680BCEF42907000718E4EACE8DA28214197BE1C269DA9CB541E1A3CE97CFADF9C6058780FE6793DBFA8218A2760B802B8DA2AA271A38772523A76736A7A31B9D3037AD21CEBB11A472B8792EB17558B940E70883F264592C689B240BB43D5408BF446432F412F4B9A5F6865CC252A43CF40A320391555591D67561FDD05353AB6B019B3A08A73353D51B6113AB2FA51D975648EE254AF89A230504A236A4658257740BDCBBE1708AB022C3C588A410DB3B9C308A06275BDF5B4859D3A2617A295E1A22F90198BAD0166F4A943417C5B831736CB2C8580ABFDE5714B586ABEEC0A175A08BC710C7A2895DE93AC438061BF7765D0D21CD418167CAF89D1EFC3448BCBB96D69B3E010C82D15CAB6CACC6799D3639669A5B21A633C865F8593B5B7BC800262BB837A924A6C5440E4FC73B41B23092C3912F4C6BEBB4C7B4C62908B03775666C22220DF9C88823E344C7308332345C8B795D34E8C051F21F5A21C214B69841358709B1C305B32CC2C3806AE9CCD3819FFF4507FE520FBFC27199BC23BE6B9B2D2AC1717579AC769279E2A7AAC68A371A47BA3A7DBE016F14E1A727333663C4A5CD1A0F8836CF7B5C49AC51485CA60345C990E06888720003731322C5B8CD5E6907FDA1157F468FD3FC20FA8175EEC95C291A262BA8C5BE990872418930852339D88A19B37FEFA3CFE82175C224407CA414BAEB37923B4D2D83134AE154E490A9B45A0563B06C953C3301450A2176A07C614A74E3478E48509F9A60AE945A8EBC7815121D90A3B0E07091A096CF02C57B25BCA58126AD0C629CE166A7EDB4B33221A0D3F72B85D562EC698B7D0A913D73806F1C5C87B38EC003CB303A3DC51B4B35356A67826D6EDAA8FEB93B98493B2D1C11B676A6AD9506A1AAAE13A824C7C08D1C6C2C4DBA9642C76EA7F6C8264B64A23CCCA9A74635FCBF03E00F1B5722B214376790793B2C4F0A13B5C40760B4218E1D2594DCB30A70D9C1782A5DD30576FA4144BFC8416EDA8118FC6472F56A979586F33BB070FB0F1B0B10BC4897EBE01BCA3893D4E16ADB25093A7417D0708C83A26322E22E6330091E30152BF823597C04CCF4CFC7331578F43A2726CCB428289A90C863259DD180C5FF142BEF41C7717094BE07856DA2B140FA67710967356AA47DFBC8D255B4722AB86D439B7E0A6090251D2D4C1ED5F20BBE6807BF65A90B7CB2EC0102AF02809DC9AC7D0A3ABC69C18365BCFF59185F33996887746185906C0191AED4407E139446459BE29C6822717644353D24AB6339156A9C424909F0A9025BB74720779BE43F16D81C8CC666E99710D8C68BB5CC4E12F314E925A551F09CC59003A1F88103C254BB978D75F394D3540E31E771CDA36E39EC54A62B5832664D821A72F1E6AFBBA27F84295B2694C498498E812BC8E9378FE541CEC5891B25062901CB7212E3CDC46179EC5BCEC10BC0B9311DE05074290687FD6A5392671654284CD9C8CC3EBA80EB3B662EB53EB75116704A1FEB5C2D056338532868DDF24EB8992AB8565D9E490CADF14804360DAA90718EAB616BAB0765D33987B47EFB6599C5563235E61E4BE670E97955AB292D9732CB8930948AC82DF230AC72297A23679D6B94C17F1359483254FEDC2F05819F0D069A443B78E3FC6C3EF4714B05A3FCA81CBBA60242A7060CD885D8F39981BB18092B23DAA59FD9578388688A09BBA079BC809A54843A60385E2310BBCBCC0213CE3DFAAB33B47F9D6305BC95C6107813C585C4B657BF30542833B14949F573C0612AD524BAAE69590C1277B86C286571BF66B3CFF46A3858C09906A794DF4A06E9D4B0A2E43F10F72A6C6C47E5646E2C799B71C33ED2F01EEB45938EB7A4E2E2908C53558A540D350369FA189C616943F7981D7618CF02A5B0A2BCC422E857D1A47871253D08293C1C179BCDC0437069107418205FDB9856623B8CA6B694C96C084B17F13BB6DF12B2CFBBC2B0E0C34B00D0FCD0AECFB27924F6984E747BE2A09D83A8664590A8077331491A4F7D720843F23E652C6FA840308DB4020337AAD37967034A9FB523B67CA70330F02D9EA20C1E84CB8E5757C9E1896B60581441ED618AA5B26DA56C0A5A73C4DCFD755E610B4FC81FF84E21D2E574DFD8CD0AE893AA7E125B44B924F45223EC09F2AD1141EA93A68050DBF699E3246884181F8E1DD44E0C7629093330221FD67D9B7D6E1510B2DBAD8762F7\"\n        },\n        {\n          \"tcId\": 52,\n          \"ek\": \"C5712512984D94A039FC87739DFCAE09934E7658A82FB0895A060D54F900C5AC1161DA09E2D833D5B60E60FB000AF1BF4F43B059B8272E79AF4572349940209BB21BA3BC3B1B6ACC281A35DAA15923496D0FDB32A8505DC8626847627BDE759175F11B457539465CCE3E591933D8B458F561EBA446711CBDF2B604E53B7EE0E0C2C0A15C35AC2A2C91BAC918170E5372C542636D7526BAFAABD10CC6F4382B01C74AE28B47289AB5E463A584465C9994B739367C9F82639801A3681768E134185C9A0DEB8965079A99451418EC051D0D723FECE5B53488207FF7994082C16043B13D278ED530640BE0B4F9AC75B52429EDCA9BC4FA7BDCB43FAB630DB25A5EF576461313CCAD5B2E85E36EBF9594689201458C9B2D96261221C8D3C21D91F53D83F0676ED7A78A6177791557DDFA33FE39699C19339AA9ACD70B34D9036D5391AB57ABB2A5EA368675A565D24A796193351A37C69A5866F4C99482CE4BB3B7795B83E584761EDAC6BFD8CF2433AFC53641E4689571B999E8236A151B6E42855F7E9BBFB8040FFA59CDE707612C9C717F5827DC2B51766889784A6942E8957E6AAAA5D8413F76A37FE69F6259CCFFDC7BECCCC1DAE419D969620C0AC674367558F532EF697058250113DCA01C051A88FABE2CC65795949166857F0F89104A1187C9D30517F25F49308BE4634AAE29B30C8360FF3CC38B5A7BE717584C10A79929B36C1516DE545566B76EACE143E011A4FD42702E95139EB2A746FC04AC99C5E9F07344C83020C34165F9572CD86F50BB9A55B13C6DF33305C8601FE1B103057519BA43B8EC1BF37603C0495F40087CC68A808848429F64BEC6EB336C37AC50F2B5CAC04D6B59870E4ABFBE773664C3926D2954E3D57F2C8147683A519B7264DF40CAB6F3BF262B760BF794416A5D601776E5165FD50C4BA4B07C49AC494C699C4705254A450B36CB38EAF96D6B0270492B84E5A5C208D6ABED761F033138D3BC9FCE42C17B160696C7CA9726BBD2B1C1E42C92556A06A5018EDF605B2D789688CB85066CAA0528BDA4E32542621727301B90333C1E4393FDB539ACF8AFC202BBC42546BB88A04AF9C089717F4073360B567D3967620BD8ACD0BA1762C56603647DEE371F552C92C82A69B1E461E4D1572FBC881AB526B49358F21A69DD3C7CE32BACFEDA9D5CCC34E09B9443EB189F69798FC80B61011B76239EEDC7C77F1B78D3077C5549C48BA8BC720CC2C8B88FC85A9A5CB6C1DA0829C504A9FA502899926BF0DC8FF9C02DC9FC005676A84CF16E2B23B7A5946289E400D0D2387E36841A227B7F10822572BD62F134EEDBCF1A66B6FCC907F9E0AF8D349FA8B5C4251C66B3690BB21A3253F3916020934381B46F1BB9C5F638BF8C8B256300B62B5D6F3A7FF680B514F6B3352A1994C8511957976836BF65979E13002AF1453C1FC037669B3465A0366B7B5F94F92C7707675FB08B2632AEF3D725CC4B3B6496B4BCEA2C865C982F7946079287D63931C8940B130776F5A7629A64915BC4B1FB09CD4C9114B1018937A83047EB3F22EB7EF5C866E9909CC89072E69C973EB22BEE6A3B1E383DA4006CCA560100C72BBA81237C1C7AB0A48A0CC58ACCE826B735C8BA19A87C9AC74E77295A8B26BDBB7685053C5A1572A09425CAE97D7F246D8D0B85AF20350999356ADA86628A787482393FD85A2166245B442F64B5516D595C471BA4CB577644738F87853F65236FF46ABAABEB9236616CF5999EADC9BA80F1C0FE8B6C45BCB543AB8E9097AF977612CF5A4E22C274A278472FA93E2B817706E11813F2B3865851C96683C83B52D2369DF3F74C111B4F4B01202277A918660B9641691412B637B7991973035F77B02D75A2143813BD49847F082C16E31EC89A2F8A588B2D40519892C939D782FFE18BE5D0BE1B5A41D594C32E246F886C37D43145DB8334B0E3364F65A76E0533FE052535DC7945669019E7310587C4C71A3883E1123A9A5BEA542F6D8CAB83CB905D26C82EF72A84285A07687ED90A2A32083F1D8519AC6289C9F6A5FE994C96ACBE0303BEB3B7A5A7457BC0118AE7008A0AD860310CCEA57BC313595A68CC8B682328D8C4440BA57E749BA40E968D09A0783CEA0CCA59B43FE9B42F157F38B67ED0379802ABC1CD50288D73581CCB59E3768C9801138B658FDAA87AC02DF5B5386C2DEFBB8605988CF7B1BC6CDF5C8F1F770EBE3E49\",\n          \"dk\": \"81D65577F87BECBC2A8975A7FB237049AC574D9C934FDC9764FB79597C0CFD236E8F516C3DB4AC0F627A02DC8426C051A6F0421B6A2689ECC469E92A0D816E85990E9298483902A6CAB76E74D476A9300E8121958306959AA362263C885B483E326285CF970BD84A694A553E9BB3AC3209AAE0F0521F3564CF352890289A530717F5E080A916613EB88304A7340A2413C20B02F62B58D68A3C97F57C8A11B1E58611A2A18B23FB3222E84D2ED287E002C1FDCA2D47C03DD5BC0B69210789241AC177907CC3916088B6D5E6B9B7C4C8C975725E38C5A3F54964057114D7CA565BB71F5C8C866A83F0E62AF7866D94C50E89B1BD8F1A8596B477AB743A427252D2128967B962F2E7590ED47670542BDF2162F8C2B1CBE434862670926330B90002E4C490B80A57CC4B02EC03B40CF6250E727C8E1C05E2C36E9E0AAC4FC0C4C4D89EEA2837408B53542513E5C898E62722415CB71B7A9EE7E8634A00028D549A2F912797C84778B7C5D4559A885430124A5D161789EA8972EC9A5298693F4857A4AC905E53A8866148117AA60D43938F84BA60F8C15B7BC88824611AB8EB74852155BFA82127D052B6138E8A7ACF28774DC2C798EA9097723BCD2EA4A0A1B38CB666830008A256F05057D126E9C440AAD52AC7C4B2E370914C2883ECB13AA5F53E43A25D59661809C960545186BB6931BB45561307A4D45C1A0EB80EB0B4166AB12EAAF8CF4D25CA4A8454B179246D3019B8EB5CD86B2BE40513C7828653BC08CC652FA59665DDDB0B94E4AEA22431F7557329434C688B5CC0789E675B0A9395AD2CAC7DDF166E0F5245DF835A00CC172B3A6192808741652C4025566C43177F5A9911B317009A38ADA45F8339693971AA1D773D6FA240F6D7880BB5244115AC2EAB5FC2408BAF4705C7E016E9B1B6A47567DF5298F47437F1C74949232EC496C3054B0A4805C8CAC2255950A8B7F683CF5B531C4C798554963875928CA4204AE8755AF433C2D52784A36404E1A368CC4FAD29BFD879565CA3CB52D56B3F273E7CEC08A1D7180F5037CA61B289828AA3838D01F9958388779ED2881FE894A9D3699542BC0A2C497F7251986B50EFF284474794C845A53E12205FCB823872640B1A8583856DBE11BD6DE49AFC0142AD940EE43B06D9D7141674212297B8478B784FCB8A886508451B376822726E00CD7FA1CA16DB9F591007B2689E0C827929612035F5150800692ECB83B244964FE6922402297F244E430A06AB897DDEFC70742BCA5AF7B634A1B3C8C719EE2909A99C19EA602B7E22C66001CFE5401440139DD6398B26141C9B23914E5940EB35105131451DD3CBEF8654F0483887004D22AABD57559DFFC11038D3BEDE5CBD44DA0119D87610E0CBE392415B33A35F57364C1177DC514AD94570140217982593CD20BEF5E43BD0638EFAD1478CF9943DA093AFD278037010C7086C04A53C0F607D8B867222DB98A56436E6FC3B28D7382116706DB679D9316C473C6D86F85DC40B0A0FE24D8905321336488E20739FB11652EB62C7A05CFDA115791CAE294A0534491CA5EA8F5BA6730E06AF33964469983770D13C858CB66D091B02DA5181ECAEB9C4A241A2222DAA77B6E5020530474C891D440BA6E3F2137B215526001BE17185F0048F3DEBA1ECF7BBF1A55EC9D57485969B43821930DA7672B33630209F8257B8DD749FA9F6C73CE3C903B2300D7304FBBBC6F5304F6DAB322117A621A851CDB66877A82C350B4F42525E16328C7B8A07948794CECAB06D7A7B13CB070C61A983647319E1B6B4E27FC900BD50F485B98121DB4180CB62AE4C3C68A8D59F18885EFEBC90B3F1C9F480B068DACDAD813A28EB200EAAAABD9A0DC5D72E9B4505778807AA6A52AD6142DC517443FC55CDFA56B2A6AACDB28B4B5045344CB418795241756943CC5BA1A4B73A2C90A2122121559FB15B015DB43B621B20F01A4731438B148395CAE4A7C36888FBF01603336991572753F35DEF2264D93BA831A46AD5FB3F663704617B712B81595A585123F03180F46285397551F27E98970DEBC2BF8298329BC87402869DF5B207C7415D1BA9615300B67FACB7A4AB1287E37938C2347C172F96A8826C944CA75C63488A9BDFD206B41C8E6E854B2D9C59D169361BA549254142387337A89C919C84512B2394C5712512984D94A039FC87739DFCAE09934E7658A82FB0895A060D54F900C5AC1161DA09E2D833D5B60E60FB000AF1BF4F43B059B8272E79AF4572349940209BB21BA3BC3B1B6ACC281A35DAA15923496D0FDB32A8505DC8626847627BDE759175F11B457539465CCE3E591933D8B458F561EBA446711CBDF2B604E53B7EE0E0C2C0A15C35AC2A2C91BAC918170E5372C542636D7526BAFAABD10CC6F4382B01C74AE28B47289AB5E463A584465C9994B739367C9F82639801A3681768E134185C9A0DEB8965079A99451418EC051D0D723FECE5B53488207FF7994082C16043B13D278ED530640BE0B4F9AC75B52429EDCA9BC4FA7BDCB43FAB630DB25A5EF576461313CCAD5B2E85E36EBF9594689201458C9B2D96261221C8D3C21D91F53D83F0676ED7A78A6177791557DDFA33FE39699C19339AA9ACD70B34D9036D5391AB57ABB2A5EA368675A565D24A796193351A37C69A5866F4C99482CE4BB3B7795B83E584761EDAC6BFD8CF2433AFC53641E4689571B999E8236A151B6E42855F7E9BBFB8040FFA59CDE707612C9C717F5827DC2B51766889784A6942E8957E6AAAA5D8413F76A37FE69F6259CCFFDC7BECCCC1DAE419D969620C0AC674367558F532EF697058250113DCA01C051A88FABE2CC65795949166857F0F89104A1187C9D30517F25F49308BE4634AAE29B30C8360FF3CC38B5A7BE717584C10A79929B36C1516DE545566B76EACE143E011A4FD42702E95139EB2A746FC04AC99C5E9F07344C83020C34165F9572CD86F50BB9A55B13C6DF33305C8601FE1B103057519BA43B8EC1BF37603C0495F40087CC68A808848429F64BEC6EB336C37AC50F2B5CAC04D6B59870E4ABFBE773664C3926D2954E3D57F2C8147683A519B7264DF40CAB6F3BF262B760BF794416A5D601776E5165FD50C4BA4B07C49AC494C699C4705254A450B36CB38EAF96D6B0270492B84E5A5C208D6ABED761F033138D3BC9FCE42C17B160696C7CA9726BBD2B1C1E42C92556A06A5018EDF605B2D789688CB85066CAA0528BDA4E32542621727301B90333C1E4393FDB539ACF8AFC202BBC42546BB88A04AF9C089717F4073360B567D3967620BD8ACD0BA1762C56603647DEE371F552C92C82A69B1E461E4D1572FBC881AB526B49358F21A69DD3C7CE32BACFEDA9D5CCC34E09B9443EB189F69798FC80B61011B76239EEDC7C77F1B78D3077C5549C48BA8BC720CC2C8B88FC85A9A5CB6C1DA0829C504A9FA502899926BF0DC8FF9C02DC9FC005676A84CF16E2B23B7A5946289E400D0D2387E36841A227B7F10822572BD62F134EEDBCF1A66B6FCC907F9E0AF8D349FA8B5C4251C66B3690BB21A3253F3916020934381B46F1BB9C5F638BF8C8B256300B62B5D6F3A7FF680B514F6B3352A1994C8511957976836BF65979E13002AF1453C1FC037669B3465A0366B7B5F94F92C7707675FB08B2632AEF3D725CC4B3B6496B4BCEA2C865C982F7946079287D63931C8940B130776F5A7629A64915BC4B1FB09CD4C9114B1018937A83047EB3F22EB7EF5C866E9909CC89072E69C973EB22BEE6A3B1E383DA4006CCA560100C72BBA81237C1C7AB0A48A0CC58ACCE826B735C8BA19A87C9AC74E77295A8B26BDBB7685053C5A1572A09425CAE97D7F246D8D0B85AF20350999356ADA86628A787482393FD85A2166245B442F64B5516D595C471BA4CB577644738F87853F65236FF46ABAABEB9236616CF5999EADC9BA80F1C0FE8B6C45BCB543AB8E9097AF977612CF5A4E22C274A278472FA93E2B817706E11813F2B3865851C96683C83B52D2369DF3F74C111B4F4B01202277A918660B9641691412B637B7991973035F77B02D75A2143813BD49847F082C16E31EC89A2F8A588B2D40519892C939D782FFE18BE5D0BE1B5A41D594C32E246F886C37D43145DB8334B0E3364F65A76E0533FE052535DC7945669019E7310587C4C71A3883E1123A9A5BEA542F6D8CAB83CB905D26C82EF72A84285A07687ED90A2A32083F1D8519AC6289C9F6A5FE994C96ACBE0303BEB3B7A5A7457BC0118AE7008A0AD860310CCEA57BC313595A68CC8B682328D8C4440BA57E749BA40E968D09A0783CEA0CCA59B43FE9B42F157F38B67ED0379802ABC1CD50288D73581CCB59E3768C9801138B658FDAA87AC02DF5B5386C2DEFBB8605988CF7B1BC6CDF5C8F1F770EBE3E4987A74BAADEC58CB97414E0D82652052055EEE3E3B64001A0DC6172A2A48DDD91007BF379B97DA0947F2E9BFDE3359E282C9CF1D2E68A80209B533104E90F432D\"\n        },\n        {\n          \"tcId\": 53,\n          \"ek\": \"F4A4800C492B0472295F4B65471C6170C96DBD90130867CC68369DF450214ECCA22420CE39341A321682C054719B33469BA78C5F0ACD0CF466F2A188713B5154B279E6A6BC1A91032D4948B2D521E3090E450ACBAFC250EC72B7E6DBA3FED99F22730B0588042F33C50F7A81F368ADCB348864AC6DE5479EB1A041F9E3B04304CA38694B5A5B0A674659D6F9BD49AC964ABC4BCC532DB8CAC7A437371DB665AE4CC5B68CAD6AE475472A05F21981998CBEECA435E14C72293127A12659AAA5572042A2789A3B8FCC1D81AA1B95929BDD1A8E13192CFC700AF0E99C6A839EA71297A2A496D9045FBB9425FACC6CC5111868D48AEF176B7462B321308AB199B02CB3A6783749CA845DAAFC1C1C3BB2BA6B0F6C29BA3B428F971C750223B84A5ABAB4B77285A04E03E78D686B38990208A0FA740D01BD05B828F0D36EF5B1056183B93F8238F4AB7AAF872CB7916BD63AA6288337DABA6A5593BBE431ABE3497314F9529A9B5A7B8212C4BC23989A87419903E7B9A2962B310F865969264F2C0A0E6BD99E21C15EB717626D642FC2C6375578780EE13D05A6B3581C70B32C96E4D4178A7920F3A012D3C4C9FCD16556E5965F12AD08B294582A3F441467A3185111E53DEAC9272869671C2CB435E0B60DE5AE0A1A2A53818667500E986395A997722A626ABFA5310E4A43379C15CD3174F4F209379626EF6C4A1F076C73539F26E7C4651317C4545709BC757A92BAF5F417F6B4235A3535D5CB9A6D903D64C89B857A841EA25747F7A0D207729CBBB3CA9B32CB1A38D2AAAD124658C216533F4380ED108040559C8C92CB343C30A5B6A156903E69BA14A3B3C4E9B8C73B2C1ACFA302DB96B9C724B28E6AB1EE489867AAB84BEBC3B417048DDB0304EA8910C55BEC887E59EC336BD96947DA832B213FCF8C68D9832EE0289DFCD8B91DB20BBC81C2833C144A044E917904E9968D0D096482549C55062C0DFA6FB1415FE491051BD9A1DBE65417CA39A0A5CFC79B1A8D909123D287B1E9B7F2A683CB864D693C65BD770C54FB6C243441328C95C6EC7EE5C0802FC0973CE88AD589353A430A543738FC9A5F9EB1730C7060A8279C61527B486669E1C2995F242DE5C42DD0021858C2A3E07250FEB268BBB38FAEF7BE2C4C6CC8B5CCEC0005627874666055091283B7E522B2D96F52A60B031BC6549410AB769BDDB15717A0557561ACE7082FDC463209E63F94919BF3C33BCD012AB9321A0134C017441B578410204A5BF70C57D572C7D7A17F12A2176668A8D9E0C9C216AE9F2466B4F961CBD1256F247EE3B0B23F2AC39ED8757E8A497E161B551CAE1A2C6B88629EE4788DEF79249606195E186398F01CFA070E8B57156922AE4823468DF5C7C5F9065F38639BC77B78A4ACCE5496459346B6D07C49128F0C9A08BBD3C448B97A31255F0B6A388BC8A221009EDDA1AF9C819840C67DAAC24F54D2220058C378E8A4A23825A49A5D4B631FB3080ACAB9BE2DE3A258EA76B01C47B4D8703CD22EB78B02AA2260302192021A0FC031A6FACB03A33055F25A2BB1D6698B4A8DCA9238DD5ACD6787C8912066EACA67FDE86FB6C14687B076D19450CCF75EE328BF9DD6ABB89030AD08BB917B738024022E69A243E779355850B18A7C97F99039271F6EC47418661779C0A528D4079C5ABED21475B864078AA92F69E64F7F12B0BA661B066114F1F43EFBA89B67C05518C15537115DA0909B4A097361586C064A82B6790766944BF99176D090AA8DF599AAD37922B16A2553503AF1B4D7B90FB967AD5A637EFB019E257A6CF2AABD470157352A3196B9339715A1B254B5FF37C8A799473CAC6D0AC6A4AB56BF03271831A33EBA29AF1AE689FF54A2431B5A3F3BA83DB5A51B0492CC2C0DE31962293539278A11B408BF50C0B6D82A9764280DE4F8155201424DDC63F58AB413813FFA011EDEEB4B8B005E707880AF88392A472AD487CB6B6CBAE8787F5F8B4A5822B269E74FD91635EDE9541B5C2A5C9A6FE15722AB2BB61F225D08A84D4A9577D52617EDEC650E429A45B0397147920DE7201925913F029985C229D1044DC1895BEA961F936165CB24BC9DF884F6F49267A19B9B623FE0095EED29B0C75805BD9AA29EE857A9970533344DA99963C057337D4C25D517A341CA4BDB86C74DF0B23A56762B5838F5C68FACD1433B948824CB86D88A1560E77F4EB4A2A95E140B648DB88D\",\n          \"dk\": \"25ACA925E757A4BBA64CE712864669DD8BC9A55007B2C3510AE778DC85A877AB08EC454C61F32205E76C509B81040466BC47C9EAE77B10067C14F241C2E992A16708D1C1604B0C3F5885BD2C86B7B1E8BBD90903B0CA242AE206A6D31846973FA38B9D09402A35E45E98E84D1B125155D86E667960CC058AB25C2B053352C64BC03A78CD86D53BC298A3D586A08C1506103B301AE502176804C328AE1F24CD270AA76CC223709AB7E4B8AF3F28043F5C21B8721F8C228B7F908ACD7C4F1DE5C8939B643B22BDCC1576B65901FCC0720C622C4FA5BADFB7442CF63CCC513A8C7709D3C399469907DF740DDB3104CEB9C92C33113D258AF7EB220EB9AED0B2C1BCBA70CB9C76B840276EF11FAD35BED30CAFE633CCC4E3606F758A0F993D27F80F84473F770565B371ADF5E61689B0402D681F0D93745D625DAD286356A152F689413FB89A569CCC39E71912B3BD345AC5643725969B896328B24568428E5C651401447B490180003CFA674A5093982FA465FFE017B62BB042CC9C67C955B3123B0755430666330547C333196322843744B9213C5528585B4080265CEA0A4931D77C7D81BFE73008B37ABD62082287608ECB2CA6E50CC4C9529E32DB7BB70917A5BC3F8BDAB897E52320395749629104F3B60E27A0D3C09508E422497B7D6F711DE43C315BD83836538F88C30B7AE4A5B5B122D9394E34479D698C8B535354C25B5BE572513DD24A64985D78517AC2F3861330663C1A2AF6C135E0E49E5B930AC6A6A81A203E0A3A4BD1642A304C654681A8769C6C6AB009690A7BFF3B5B9FB64B30C2AFB9280FCF10377601B9047BC6ED0A07310091A97A8016735695EC704FD8493273B2DFD8C1A372C6B7A1A91DA08D3F2767B372A688228135F69D77D1A20CB03C69482A7B67A1E59249FD098E19BC84CEC247922A52A3EAC641F9383EA19200E16874E876AFA5C2598355F5EC01CDA86BE976A7420061C8D9A2F1E341F1378FBBF31DDFE3699DC60637138DC3A19B5FE17D27F0352743C76477363B7C72CBAA2E0F83CB53A82EEBDACF0642C5C39868CDE28F5E83A08046C38B9A1367B2B68C50869E6A9B44379AF1111133ACBF3E51738D2ACCE510A1B5F40B5D17CF5B5710C70B20A1A4734934123C2BC8B1D3714123A4159021B0688484398CA1346D0B5A9CE6AB299F439CE0B83A29261E67B905B944C8E74041431738D4272FB11254CB483907998F364235668C960AB0BE12580908093C31C37BC769BFB285451FF96155102376B8803DB777C15683B963765F2BC40B29558275864B0668F4C269F36B1D6E935C16A2B160E6B9A922080039A2946198BE02A82DFC1B86B310539502B61BC4B8DB533D939B973830EDD6382DD02AEB876ED7332954D2B0F1831953614CE6622BC91C3729F7BC6C45780974B4A40C07B5E4614C858CFCCC4C42E32CB3492F4F36546FB6ABFF9C6AD8C96B54818B3EE3545D2A2687E14C13B29248C69403A63D79A886195C630BA97AA52539C8949DEBE323F68C53EF5AB121D4AA0AC10D2A28A90B39C03187761DE728FDEB17FEF21257DB7199D665D5A5C37823ACE4830F09B3440A345C3627A136FBB416033BB0E73AD9F1B2417075D1F0C928778DEABB59C03A8813FC8BFC8B27797CAE5467C6FC934AF36922F8983E4A8B4F9BA79E2C1B7D9D2297AB273289B8A1EE2CAF8740A201A9C6F2ACCFAC8A0DDF51BDF076521E22AB196811E0834E96AB84606A1E1F018868604BA32C832FDA967CAB1C3021AA4D59A3E731242D7792729CA646D8B976370C2F93332500A9AF32684EF6B6EE7B3690B57409C4A78AC92F9E58CBCF9854B18A737114045EF72C5B808DB039126A8C22C605287292AF93B932AE153EB932CCC06CA4B8DC18AE8C0237EB1122A1C0C1C95E4A5118CE674990707AFA146B5AE620FB44A3FBEAB22649A37495825485634EAA6B7565BFAEA2493D3605D9AB5140C27FEB1350C93998CA8C5E52B14BDB9679763C20D8CA516CB8B5F089BC30E5B9DAE631D6C571AA572F40922E13E8C9488375603605C1D3505FF27033F57040B904D4B71A30098A260169B49495A1B52C76E924FDA088ACD56B90565A49BA8E0A1C66F186367AA7710157CF4A28B2223C2D034079E5ACAA88DB0E9DFB585C7773142A43535CC6083B1EFAB0A8F4A4800C492B0472295F4B65471C6170C96DBD90130867CC68369DF450214ECCA22420CE39341A321682C054719B33469BA78C5F0ACD0CF466F2A188713B5154B279E6A6BC1A91032D4948B2D521E3090E450ACBAFC250EC72B7E6DBA3FED99F22730B0588042F33C50F7A81F368ADCB348864AC6DE5479EB1A041F9E3B04304CA38694B5A5B0A674659D6F9BD49AC964ABC4BCC532DB8CAC7A437371DB665AE4CC5B68CAD6AE475472A05F21981998CBEECA435E14C72293127A12659AAA5572042A2789A3B8FCC1D81AA1B95929BDD1A8E13192CFC700AF0E99C6A839EA71297A2A496D9045FBB9425FACC6CC5111868D48AEF176B7462B321308AB199B02CB3A6783749CA845DAAFC1C1C3BB2BA6B0F6C29BA3B428F971C750223B84A5ABAB4B77285A04E03E78D686B38990208A0FA740D01BD05B828F0D36EF5B1056183B93F8238F4AB7AAF872CB7916BD63AA6288337DABA6A5593BBE431ABE3497314F9529A9B5A7B8212C4BC23989A87419903E7B9A2962B310F865969264F2C0A0E6BD99E21C15EB717626D642FC2C6375578780EE13D05A6B3581C70B32C96E4D4178A7920F3A012D3C4C9FCD16556E5965F12AD08B294582A3F441467A3185111E53DEAC9272869671C2CB435E0B60DE5AE0A1A2A53818667500E986395A997722A626ABFA5310E4A43379C15CD3174F4F209379626EF6C4A1F076C73539F26E7C4651317C4545709BC757A92BAF5F417F6B4235A3535D5CB9A6D903D64C89B857A841EA25747F7A0D207729CBBB3CA9B32CB1A38D2AAAD124658C216533F4380ED108040559C8C92CB343C30A5B6A156903E69BA14A3B3C4E9B8C73B2C1ACFA302DB96B9C724B28E6AB1EE489867AAB84BEBC3B417048DDB0304EA8910C55BEC887E59EC336BD96947DA832B213FCF8C68D9832EE0289DFCD8B91DB20BBC81C2833C144A044E917904E9968D0D096482549C55062C0DFA6FB1415FE491051BD9A1DBE65417CA39A0A5CFC79B1A8D909123D287B1E9B7F2A683CB864D693C65BD770C54FB6C243441328C95C6EC7EE5C0802FC0973CE88AD589353A430A543738FC9A5F9EB1730C7060A8279C61527B486669E1C2995F242DE5C42DD0021858C2A3E07250FEB268BBB38FAEF7BE2C4C6CC8B5CCEC0005627874666055091283B7E522B2D96F52A60B031BC6549410AB769BDDB15717A0557561ACE7082FDC463209E63F94919BF3C33BCD012AB9321A0134C017441B578410204A5BF70C57D572C7D7A17F12A2176668A8D9E0C9C216AE9F2466B4F961CBD1256F247EE3B0B23F2AC39ED8757E8A497E161B551CAE1A2C6B88629EE4788DEF79249606195E186398F01CFA070E8B57156922AE4823468DF5C7C5F9065F38639BC77B78A4ACCE5496459346B6D07C49128F0C9A08BBD3C448B97A31255F0B6A388BC8A221009EDDA1AF9C819840C67DAAC24F54D2220058C378E8A4A23825A49A5D4B631FB3080ACAB9BE2DE3A258EA76B01C47B4D8703CD22EB78B02AA2260302192021A0FC031A6FACB03A33055F25A2BB1D6698B4A8DCA9238DD5ACD6787C8912066EACA67FDE86FB6C14687B076D19450CCF75EE328BF9DD6ABB89030AD08BB917B738024022E69A243E779355850B18A7C97F99039271F6EC47418661779C0A528D4079C5ABED21475B864078AA92F69E64F7F12B0BA661B066114F1F43EFBA89B67C05518C15537115DA0909B4A097361586C064A82B6790766944BF99176D090AA8DF599AAD37922B16A2553503AF1B4D7B90FB967AD5A637EFB019E257A6CF2AABD470157352A3196B9339715A1B254B5FF37C8A799473CAC6D0AC6A4AB56BF03271831A33EBA29AF1AE689FF54A2431B5A3F3BA83DB5A51B0492CC2C0DE31962293539278A11B408BF50C0B6D82A9764280DE4F8155201424DDC63F58AB413813FFA011EDEEB4B8B005E707880AF88392A472AD487CB6B6CBAE8787F5F8B4A5822B269E74FD91635EDE9541B5C2A5C9A6FE15722AB2BB61F225D08A84D4A9577D52617EDEC650E429A45B0397147920DE7201925913F029985C229D1044DC1895BEA961F936165CB24BC9DF884F6F49267A19B9B623FE0095EED29B0C75805BD9AA29EE857A9970533344DA99963C057337D4C25D517A341CA4BDB86C74DF0B23A56762B5838F5C68FACD1433B948824CB86D88A1560E77F4EB4A2A95E140B648DB88D7456EFF3A15CD68111A12974CB06566E9007C376E09CB10D47C73E43546AB16AE94F4E83E6CAABCA9E319D40F6CE0E3691B77C92D9E3766BE9B6F4B6DF2E640E\"\n        },\n        {\n          \"tcId\": 54,\n          \"ek\": \"0E4C2891EB5497C1BEA0A62A0AE207018A2C0FAA8991D76CD14C47916853C326123A3C2997BC27A0B7073F97881EDA9CA18996779064AB768749869E66C4CA6AA58A8B155371B4729BE2327165B103393B23941FC6941A2BBC3C9D1489D6692E146B73A428241172C1EE63161C82A6666B7F5F757BEA328CE6AB45D0D87FBD007333EA927C3810A0A1790DDCC14A607D45A046E23C043CD96196659337FA940E67C565E70CD7D45511BA4C01631D43DCAD1C09833D64BE170A0681E0978D1B4F27E253BBBC5A4C2377299041333A1FA1FA0329B9B6FF24647E3B03954A85BDD637697CA834C95E1A992011D52D2719AD5FC410A780291CC575E82A2AC229BD8C6C42D4673E958ACBB6DC6EA644AAE5B10D5D90907E82614881182B127A53356A26A62C8463B26E5415C131BD72B16D2BA1175FEC0B6E59723FF7CF917968AFC91FE0698F40F55A9C730A1A493314991CD0329083B84292DB23D706755CEB4CF645A706E43512432848639AA757C61E727E8112A06B590488C4917C74041B92C1F34194A849211785C10FF71D27917D45A3B2A67966BA454A3B4829DCCC3CA8493612AB1EB554469E9950B4DB78E8D96E2352CC1AB2895FB16362143A25B3171D4C05A2AA5E5DB845220AB77EBA1166A5842436B226966988BCA3CAFC7997B0A261F97AA24C70BEC48CDDD491A1D2CFCC4455D06AA956348478580D1C70008C0B4AEA3CCFE9EA6C37A3B467652179B446F4D060B532AAB668554ABB88974BA6AF174861D52124EB9D05E60E62E11D67F34F1747951C2C6CE385BCEDF5476B882571579937F57C5C605351D12AEF9C6CD9B8BF35E95F7AC70D50C62B3E68899C39173101AC8EA326DF755BCEDA4B746BACEBE9CB8B233AFAC73075738BE4EB905D26438C5A68D8A14645462DE90783CCBB841431A452B632AD672494377CCE80974247951711A94CB907611C7E96E501EAA13BF225A041FCCFC0318CAC430C58CA043719668E59B4CAB34E2FC2ADE662C7DAD744CB5963ED6A9EF2449A000A6080C555DE2B3EA80C5AB7091DC18C4811697FF6E0A6D2D98489FB211B6B786ECB0ED793448351B80668A46ECB41FB8A642F10A0CC41A1F836303023C33B3B54BA6C2599268C433C6C0832052174739267CFD80C74C3225AC4D75CA8235108165E69F233C213B5AC547794E7CE59DC8D21D76B2CE0CECE43578FD0A1CD38200DF0C020D39563B519BC138B39D819A7A8AAF7D9C931EA75FA9B69369A7A6038B4D3937F7EFB14CE779CC1227B7732172AD10DB932531E426938D067144B6BF4F67C3DB0135573C219552719CB67ECFC07A489BB81F4A0773988E5AB3ADF7C75445868673B6D255B72AA73B32EFC015EA83010F96148A289B10101990290843CBA81E27AC58495F12CB05F5A8454E66F5B10CF0425BB6A1A8580C4C4110304C4717C84431447E510B837C8D70C8B589241592A107E6920FDF8627357C3C0D931F1F69EEEF1C3C518BFCF4305C98A86CE950FA40195F04B79C1471D0389792502C344DA14C884890898262E8292C3E5912A595D0E561CB3594FB4391BB5F4B6571969C86A8457633A5D8212393773D5D736380707BE35895948AE4AE8A672A16BE1E536549A194427C24080BDE083A63AB65240936F3FCA52DCE473EC08B765246AABD6C1F9549C0236676AA2AD234065C65672ADA92281696CD10A1AF1580A88E602ADBA75BC84B10A57072D1695BAE791E26114FCB408576516B7741D0CA9038D2C100884C417F2739930C50E408CC0A83023E6B13C236965802F62B00BFDD0032F98BBE7084AE0B5C6A26613470A8BE5B6C6BAC59B9B725C55C71E4E8A381BF88A0341943B942228FA6E3B3BB614887846F05461D632685486BC73A8E1794483125E58031DCE0A5C8E175C3B73AE08C01D2EEC8677C32F7A2B0D620ACA1D128E6B42095D8B9179369EA88B5697380B4C13552EEA86D9F8188A1C9093B794A9538EEC3A6D5F621A9D991DA6974E6A804D00A2864450C5AE59C4FFE2AEDC23AC522939D5D2204168CD8F8C59E87723DDD4809D2913F6825043EA6C7C9340A91914FBD13C65384B06B955AA221D302112C1F0802F71443FA9ACA95CB236E64886B4B0332BAF527C4AC3A9B4D8956D5FC2904911B944EC3F9FA07267342FFC4CB9CF15CEFE7325CD4C34B9A742B4214F50A50E58B9BD03C053806F0677A35438CF5EFCD8\",\n          \"dk\": \"3E793CFC068B5ED1B1E6D8ACB44ACF89C78CFCA91603E5182B598ED7937F9CA4973DDC1FD3A5023AC5A7C5B34DDD98B3CAFC2B548C9670C18EA16C15982B8B46F74D5A56AB08A1C517FB3543F678C3A97AB45A7A0FB9BFD12AA9BDA96E695674E5A78F3BAC74E033C5A3687A27A13D889B4A36BA339AA25730DAC64C464970FCB268F20F79F055EFC06964F6679AC56EDFB065C39B475370C98C4670387B97B2FC488E567E3B6109FCF28B764890B1E8529DC3BF58851EDA97358AA7B42C872EBFD832FF17B31E2471B091C3254707FC526521B9256997369927286B332CF3657CF3EA584F46ACDC2901B98A117C86430E00474611BA4043953685449D415237BA4E63D99F92835F3BC04C575008FF02885CE26027D914C67CC7F61CBD1C492C07AB1B278B68AB37A13A9C3CCF0BB84CA18DB371C8C4415D29A47F20DB1BC18B52AE4A831DA34B4A541C0D0C9EB17CB2675845CF809709338242A29C9E6239F7797A790A00CA407CBC536A079201C1E2672E343CCE0714A5E8C446D413BAD40E569280B2C2A255481A169A43F5AA501E61914EE35C2964B295914FC4158F7D3678D9B4C3869782DF96189FAB4FD7C171CF3210B7D27E34A593A5A215A4966CF73BC1045402ED8C0E7E548363600A871A0330B81B6CE482CF33C368956E1A49B848755B910A609B77389AAB1E6CF60D75555D21B16336589B474B059813828A8925C681524D9A5FFEF972736856ED169A5E6096E430466D03598619B3709CC91CF24DD4F55AD484B081356A2241B438C37CB250CDDE46072C16454706922F35C5267614943A9155169483D82CA716327501CE7DB313C771AA197367E8E11E5092A519DB2D26FA390BD8070390294589871EBB26CFD636D3D438BB9B0055A8C99F31AFC91245650483945C2C265170D6724102B0B9D0EAAA914CA85C941A6D90668A39582C515C5EC10200C1458D53B0A5B012B4D8B27F917D151C0896C9328903C49DB6B4FBC4526722ABD17C2CDA19A3519BC781C00DF97C5642798359DB358DC153B2885AD8C4BC59E94242BCB3436460048A248CA40F8AF3BF04799B603037567C0A85D65DF17CB9C520B4B56C01DF4C26BFD39FF4D27A1984358465AF96558054A89320434910A54E80D860BC38168D7C1E0F978720CA64C57B7ADDC98B713813F7098D548631BB062B069682E30547AE8BBE394551CCA1A506ACCF37A8B052E86F03DBBFB3025482EA1B2FDC2A9A9B077024B2F0827A33E7661C199201365A840A11F3F649A4097F630B7EFB734438E83C67284E12014282804C46049AE7438352A9AFAAEA2F843A347D753972728B4AF1954248291B4AC278A8813FF5083C582FBB1B8D4D49A227A49A99FA04827265F8C9902B98B299CA4A22668096DA77F7651DF1DC723CB98DB9B921ECDA5634451BC5B94663440F35A358FD8174057A38EEC579E49209B11CCD2DE729E704749E70AD13678BFD365CA26192965076F1F3868503C450DB964EF48061159B0841A06E9A2D51FB78C6033E5A063D30D2C160526531B3BE8EC561D32857136C3E1FF757D150A7E48B876928A1D4BAB34162180E94545285865A6C6988645D1A746C6C744B196C317E6AC95163353EDC381FB7AAD5E07993BC3141A64489762F6B9041CA438F2C135FCD737503A64FE1E5592FA68587B40A7F9913DC560D1104C1A08CAD82275B4AE0838B3BAB51283078D619B77C82980569C524A50158B8F3D352F3060530E9AF6CBC9613C67110773F1A6892E3A83D30B918DE545FA48B19A797BC2A7246D29B251A46325F453EABC1B8EF795A24E488F3C269CE04C3EBC8C0CAC82DF837B4EC46AD747721695C5C18A45EBB52BE0CF608AA08A759F236BC59357387006968C913E97856810DBF266E16F46DB5585D8DC6510A954D3C090B4283318633609A7B3625C54EFC975A127C625AD768F1C03D57584B7D2012E680CCB4EB7A1C822E4BC3182D0483A6D894AA77AC35795717674EB0FACAA0B9C172DB95D257058E7A31B7F6951A3ACB8AA531EE9516C097AE8981671E2381BB523388082D2B07CC9B576401A5C195C05FD1C0849D02869B2834772A1EE6776EE66A970CE2B9021193D13C2642A2A3271C093DC858424115B9C1628AFC31BCF7AE18783602813012750A4CCABB1E609C2A649E0E4C2891EB5497C1BEA0A62A0AE207018A2C0FAA8991D76CD14C47916853C326123A3C2997BC27A0B7073F97881EDA9CA18996779064AB768749869E66C4CA6AA58A8B155371B4729BE2327165B103393B23941FC6941A2BBC3C9D1489D6692E146B73A428241172C1EE63161C82A6666B7F5F757BEA328CE6AB45D0D87FBD007333EA927C3810A0A1790DDCC14A607D45A046E23C043CD96196659337FA940E67C565E70CD7D45511BA4C01631D43DCAD1C09833D64BE170A0681E0978D1B4F27E253BBBC5A4C2377299041333A1FA1FA0329B9B6FF24647E3B03954A85BDD637697CA834C95E1A992011D52D2719AD5FC410A780291CC575E82A2AC229BD8C6C42D4673E958ACBB6DC6EA644AAE5B10D5D90907E82614881182B127A53356A26A62C8463B26E5415C131BD72B16D2BA1175FEC0B6E59723FF7CF917968AFC91FE0698F40F55A9C730A1A493314991CD0329083B84292DB23D706755CEB4CF645A706E43512432848639AA757C61E727E8112A06B590488C4917C74041B92C1F34194A849211785C10FF71D27917D45A3B2A67966BA454A3B4829DCCC3CA8493612AB1EB554469E9950B4DB78E8D96E2352CC1AB2895FB16362143A25B3171D4C05A2AA5E5DB845220AB77EBA1166A5842436B226966988BCA3CAFC7997B0A261F97AA24C70BEC48CDDD491A1D2CFCC4455D06AA956348478580D1C70008C0B4AEA3CCFE9EA6C37A3B467652179B446F4D060B532AAB668554ABB88974BA6AF174861D52124EB9D05E60E62E11D67F34F1747951C2C6CE385BCEDF5476B882571579937F57C5C605351D12AEF9C6CD9B8BF35E95F7AC70D50C62B3E68899C39173101AC8EA326DF755BCEDA4B746BACEBE9CB8B233AFAC73075738BE4EB905D26438C5A68D8A14645462DE90783CCBB841431A452B632AD672494377CCE80974247951711A94CB907611C7E96E501EAA13BF225A041FCCFC0318CAC430C58CA043719668E59B4CAB34E2FC2ADE662C7DAD744CB5963ED6A9EF2449A000A6080C555DE2B3EA80C5AB7091DC18C4811697FF6E0A6D2D98489FB211B6B786ECB0ED793448351B80668A46ECB41FB8A642F10A0CC41A1F836303023C33B3B54BA6C2599268C433C6C0832052174739267CFD80C74C3225AC4D75CA8235108165E69F233C213B5AC547794E7CE59DC8D21D76B2CE0CECE43578FD0A1CD38200DF0C020D39563B519BC138B39D819A7A8AAF7D9C931EA75FA9B69369A7A6038B4D3937F7EFB14CE779CC1227B7732172AD10DB932531E426938D067144B6BF4F67C3DB0135573C219552719CB67ECFC07A489BB81F4A0773988E5AB3ADF7C75445868673B6D255B72AA73B32EFC015EA83010F96148A289B10101990290843CBA81E27AC58495F12CB05F5A8454E66F5B10CF0425BB6A1A8580C4C4110304C4717C84431447E510B837C8D70C8B589241592A107E6920FDF8627357C3C0D931F1F69EEEF1C3C518BFCF4305C98A86CE950FA40195F04B79C1471D0389792502C344DA14C884890898262E8292C3E5912A595D0E561CB3594FB4391BB5F4B6571969C86A8457633A5D8212393773D5D736380707BE35895948AE4AE8A672A16BE1E536549A194427C24080BDE083A63AB65240936F3FCA52DCE473EC08B765246AABD6C1F9549C0236676AA2AD234065C65672ADA92281696CD10A1AF1580A88E602ADBA75BC84B10A57072D1695BAE791E26114FCB408576516B7741D0CA9038D2C100884C417F2739930C50E408CC0A83023E6B13C236965802F62B00BFDD0032F98BBE7084AE0B5C6A26613470A8BE5B6C6BAC59B9B725C55C71E4E8A381BF88A0341943B942228FA6E3B3BB614887846F05461D632685486BC73A8E1794483125E58031DCE0A5C8E175C3B73AE08C01D2EEC8677C32F7A2B0D620ACA1D128E6B42095D8B9179369EA88B5697380B4C13552EEA86D9F8188A1C9093B794A9538EEC3A6D5F621A9D991DA6974E6A804D00A2864450C5AE59C4FFE2AEDC23AC522939D5D2204168CD8F8C59E87723DDD4809D2913F6825043EA6C7C9340A91914FBD13C65384B06B955AA221D302112C1F0802F71443FA9ACA95CB236E64886B4B0332BAF527C4AC3A9B4D8956D5FC2904911B944EC3F9FA07267342FFC4CB9CF15CEFE7325CD4C34B9A742B4214F50A50E58B9BD03C053806F0677A35438CF5EFCD8CC8CB55EEE0FF5BA0F84F958550BE099B0E692A35E0908A5FD21A36B521C0F1EEC54F6E1E7FB12B796D0E56BE6FE3BA6EDAAB49B08712318B27D229606D2AC70\"\n        },\n        {\n          \"tcId\": 55,\n          \"ek\": \"B3C014D82BC4BFA186D2E2A4FCFB57E2205FE91195E1ACAA18A3699D1BA700770482C18160082946D85BE262136B7B1F5BBC00D4D20A1351C66966CE3C1A06EBF35A43036A5498424C73BA01C250C61399CE951AF176C96F317B0A23395164075A23A6D6A55916F5032310B5E28B1B970C3AB75B4058C86DD9A73ACE89B9F5A272A86B433AEB9AD755B166105AE0095645910E0AD6045C075B82596140213DCD231C03D0786C186C95342C01265B03A2A113374307B49062727C82F2CBA44862C7691703374AD76B772DA400C283A1AAE348EDA9582B9B42196216A8218305309C367648F58785B74A9E4DC068318BCE4CA87000DC86ECC52F2D465762A58BCCBA755E7A881AE5704C643278E784E646C48AFC3F240A728C4A6D2B6523EA3499C9344294293497D3A4A3E2373BD2CB48B0575DA052253693D1DB696DA0057B2243525AAB17770D61D09D0F3554D10AA7A56B6CB9A81F59EC73A1FB9A08D7BB08D90F70117A2988409B870AA2F82447E14DE7B3C16503CE645BA54EE5C6E71909642A3F76620AE0D479A6440AA5E457A946A884D0C741A2C7F27C9EA9E716EE1C01154120E777930E991C63E0A3134B030A8A0E220758C074484FC7019C5AAC212246F990C41A5B5E468888A5280A23057CA0B23947F332213A86910912C090421D4A008CC219C9C571AB09A9C59C248FE71A2C928D83E6C4DD5929D4D1B8F0CA98F4608F89EA1D59F063D827123026C7EC093A6360590BF54BA041C2C21473DD89314321B334680CE65B5737857D5FB557F1967075C9889266298F332A19C5888A3497F0580B8BE1AB4F7A300F6248B4D99431E4B9CF163E9B969FD7B664AC8248789C2866D05D01C579B491BC50C8099C30A0E6076800B00CB27929ECC8C9B7C44B32781070188B747C6EC1997017E8435035CAC06B2E74D54EC8B64E2A9B9BD6351604D3875C394C183B4B7D362866C96A78569A9E88C2D8767FE0CC0C55C506E5A50B1FB9CC18910D9F22648681CA8C66529EB3665E08BDDD725146C343BD7B9E51D37F57B602E012A1096793931097228A1536715923CA93D8C41816E04FA27A16F6EB50BDEC8A26D99FCC1901938B7475731DAF85C2058832315B93E9DA81B415B707F95EDC44BAD6AC3016E4A16E787E3FC59DF904AC385ACF28EBA6EE61188883CC72E9B298F02D2D49CEE59B6DED348038C7055202B47CBC0B40774BA78CAE78079B9E8A9392CBB57CE4400E9214C3784BCCF94151D510B182403D990C2F8C984B6AAA3AFC21F9F6711EF91EEB008482845FDF60BB5917720EC9C774943E04A7A5FAF161ED557701BC26717ACB2CC059E1308E2AA76D04919B30A70CC698402FA58D927C8A02773815A144128C5BD183A38C3182BCF19DB7A3158B9393C767926F5737B6B60EDDC01FE8930ADC2C4414146A9968311C0B367647911F4AAA4443AD0648C634828C62C0BA01C161162B5EEAA78CAA9A1425F55950EAAF71841250008FE1CA3B8F8421FB4438E65AAC914AAADCB45FE303CD9165CF867A7E9304CABF0B3E1DA63A5FCB56F03753D7074525425131D555B5E583B303B6616AB6EFB68CD5994E9296608700BD3239B03AA6A662FC354E2A49314B63D9E1801EA5127FE653E459B90E94C039D812DC031D536C400A2A70B860A9AED7BCC286182933C62F7C1E73B615F1525F6A165DFD4461AE0B856B298B96CB6756AA2255949255E6C95C12A969A698D37B6FC8B7B16BF1AAA1734DCFE1160D319B25236F591C7CFC8A08FBA48B42586F3371B0AB061C92F9C0E3E350DE5941C9F380D876480C2623F526111D676AB33ACA5D66185655A94E13418576B632DB8A0D3830738C44AF9C979514254ED8C82E34AE5DD6458E2039690B8492C353E2F0937887088B4786BAE016104648C0544482C562E0D2AADE390DC03765C7C1353634B83AB66AC7E1306378B5032B3148BB799E8B4907188EFA993FE72C73E3162F2DA43F0EC7C2F1C162EFFB5659A947A3AC6203B1C825125023926115605CA1909D912A0108C0115FD75D967BACF32B656EA64BB1808D1C1B43E8D7BE02EB6432D89B59F9A3FD109AEA36ACED2A137214BDD8686D977A68A6E4B6ED6C916A8594CFA3700AF608CB6A91AF150077D5A40EA4471AC19A7177B1E6A6BBD8A357F416BA8935AE73829A5B035B79AE8D108ED5D05A089242ECDBE94664DA47F187D3D493AC23E7\",\n          \"dk\": \"91200CC986B3575202DB535E04BC2E37A499076781ECFC2E5CB0259BE2A5618C1CED22128D194B0BF4397D6AB0E2F30F6AB23260BCCA10507CA7015C80D9409B4BC06DABC661D3A653208A505136926A554CE331E4B692105044DB060576C1C36A726C0E1379C463CFF2183166052813A6BC4B93365F2798825387209C8C53A1C2DE453887B20061E71245CA6C1C27809242C14215924FDB901EA035AAC45EB29B518D0477F5D8CCB7D39E16239EB0A5254F6380F328C1ACFBCDE08310804195B944AE8D98BC1D75325E062973D84C5AD958C14076720C4876A5B903E80DDFC7BD66E1B55356B896263FC3451E2A865212CA3B9C510FF0CA4865B6C1F2791C1A77A67336A429C9569AFABF535888D1075ED2B1A31CA78B9E3353020800FC18C1D47CA6C326529A375FDA667C06134986B891DCBA7F6AAC901768A156270A6E46490B33378EE75648BC4DCB1451A2212931F2096CD8500F23817975C25FDC164F41CB5D8A3613EC993A57711585BDE822B519683F8C9CB95ED701D6905F20A46E29C0C90C448B42742FD1BA05BFF590D2D97317C2473688A4B6AC63CC1CA28AD71FB7C5252C42534D27AF11D5947E1AB196CB2AFBCB41E0A8C50AE274D8CA1971B605520503E9C17AB6F41232FA9130BC951DC031A66A02A663756ADC7C15BC513ABC1592CCBCA9F5CD3AB29B2305B32D98C877217A1B92321930B34A096641070428B65FF4108A36C998E895375640B57788A72302931DB058ECB52F4D7A0623D74AC223C4ED6024515A364D280EE056760CB451280772AA0CADD1E0333AEAAB5357479D01696DE52AE2C8B71705706BF15C05064CA434280D4C8CAC411B0A390C6D6BBDF5E684BE3B850D3A83BC31C5C823604E533242E80DB57B2598CCAB30E53282F140C9897C85EA7BC9FC378761BDAB629E46336B714C0189E214EDB254512AAC558541D30CADEBB9665A1914A9D001F43504A0E0A566B311E746379F56558A760618C753A57B6B1A4251090795466A178A39904268A10F2451484C6B11C037953817AB156EF7383878B1BC38790CABEB4991EB2768A83F580945D46C49A8C0CA26BB99F72C40B73439253180DEE2A425C54C23FC6718A40611455BF4652F8CD91067B42131EC03CC5C0B59EBC5FF30B097D764306CB692ABB95D192CE44B1C0E981162F73997148DF270630AB6B37129813599239AFB46CDAA40CE472310EA67C2E0BA18765D34EC9FBFB82CBDB4BB6797B849BC778C9710BCD0BB2C611990B3232AAA4D1B898371738AAB9375813913DBD178A4CA2EEE4A8D09E72EDE6189272B1EF503AC8302A63396798FE5731C223E53E90EBA3805D1F8A65DF168C71408682C2790A167557A15E697A72CD2CB613B7371838EC85BBFB510B3344C3B61664BAA8B90079C30181ACBA83AB049808CE6421DB67393865C43D43C3825281492AA68413BA560B123E2140DC114060F93101CC08C63D848B172010C56C2F5377AB4B4A31CBCB265FA607F504147C0009EF812788A7EC53514460A8C8225A664348A5347291B73B423C8385CDCCD34A277913841F2C2434F2A34A2EB02B7270274E4AFB3F19933402E1A340D8A8B6F50EC4930086D462A85C9AA80950711C092BA7135A45CE30B37FBB21C574C6A1263D0554AADF36D9A6567716B914F41617047973D54C3F573805CD38170F004BD014989D33D939132A39B44D49345B857792B241B0A870396D95CCA133E7A7001579930DF70C201A71951E11257356129255A044678BDFCA5A7FC32001167155659F41B4022A9B15B37A197FC7C567CA63D74B4E9D286100861658139A98C868EBC9207237A5E4BC26EA867DA305A3C3A36083C9F79523B45F53B8FD40591304836B8520B227905413FBCAB24B22A7D1334452045AF354A4690455144390A381C1172C18D865C86B159AC944906F298A00571B61420BAA7E67787AC2256EC7217FB0C00C48960165CA6E4416059A4CAC2CA9535C8DD2265387B1E18950A7C917EC73AAD76A753E0D4A285B19089E264B6A53A35AC523FA955A4B48FCA59344503B12675B6C6C99593EC996DD9C456D842054A52DDE1106E89A30B5732FBF5C88BA98833A26FB47350D0509B17E8BA76878C2FB485B7CB5E7B4C73F6929FE09B54A09454A7145A4A812C26240EBF4126B3C014D82BC4BFA186D2E2A4FCFB57E2205FE91195E1ACAA18A3699D1BA700770482C18160082946D85BE262136B7B1F5BBC00D4D20A1351C66966CE3C1A06EBF35A43036A5498424C73BA01C250C61399CE951AF176C96F317B0A23395164075A23A6D6A55916F5032310B5E28B1B970C3AB75B4058C86DD9A73ACE89B9F5A272A86B433AEB9AD755B166105AE0095645910E0AD6045C075B82596140213DCD231C03D0786C186C95342C01265B03A2A113374307B49062727C82F2CBA44862C7691703374AD76B772DA400C283A1AAE348EDA9582B9B42196216A8218305309C367648F58785B74A9E4DC068318BCE4CA87000DC86ECC52F2D465762A58BCCBA755E7A881AE5704C643278E784E646C48AFC3F240A728C4A6D2B6523EA3499C9344294293497D3A4A3E2373BD2CB48B0575DA052253693D1DB696DA0057B2243525AAB17770D61D09D0F3554D10AA7A56B6CB9A81F59EC73A1FB9A08D7BB08D90F70117A2988409B870AA2F82447E14DE7B3C16503CE645BA54EE5C6E71909642A3F76620AE0D479A6440AA5E457A946A884D0C741A2C7F27C9EA9E716EE1C01154120E777930E991C63E0A3134B030A8A0E220758C074484FC7019C5AAC212246F990C41A5B5E468888A5280A23057CA0B23947F332213A86910912C090421D4A008CC219C9C571AB09A9C59C248FE71A2C928D83E6C4DD5929D4D1B8F0CA98F4608F89EA1D59F063D827123026C7EC093A6360590BF54BA041C2C21473DD89314321B334680CE65B5737857D5FB557F1967075C9889266298F332A19C5888A3497F0580B8BE1AB4F7A300F6248B4D99431E4B9CF163E9B969FD7B664AC8248789C2866D05D01C579B491BC50C8099C30A0E6076800B00CB27929ECC8C9B7C44B32781070188B747C6EC1997017E8435035CAC06B2E74D54EC8B64E2A9B9BD6351604D3875C394C183B4B7D362866C96A78569A9E88C2D8767FE0CC0C55C506E5A50B1FB9CC18910D9F22648681CA8C66529EB3665E08BDDD725146C343BD7B9E51D37F57B602E012A1096793931097228A1536715923CA93D8C41816E04FA27A16F6EB50BDEC8A26D99FCC1901938B7475731DAF85C2058832315B93E9DA81B415B707F95EDC44BAD6AC3016E4A16E787E3FC59DF904AC385ACF28EBA6EE61188883CC72E9B298F02D2D49CEE59B6DED348038C7055202B47CBC0B40774BA78CAE78079B9E8A9392CBB57CE4400E9214C3784BCCF94151D510B182403D990C2F8C984B6AAA3AFC21F9F6711EF91EEB008482845FDF60BB5917720EC9C774943E04A7A5FAF161ED557701BC26717ACB2CC059E1308E2AA76D04919B30A70CC698402FA58D927C8A02773815A144128C5BD183A38C3182BCF19DB7A3158B9393C767926F5737B6B60EDDC01FE8930ADC2C4414146A9968311C0B367647911F4AAA4443AD0648C634828C62C0BA01C161162B5EEAA78CAA9A1425F55950EAAF71841250008FE1CA3B8F8421FB4438E65AAC914AAADCB45FE303CD9165CF867A7E9304CABF0B3E1DA63A5FCB56F03753D7074525425131D555B5E583B303B6616AB6EFB68CD5994E9296608700BD3239B03AA6A662FC354E2A49314B63D9E1801EA5127FE653E459B90E94C039D812DC031D536C400A2A70B860A9AED7BCC286182933C62F7C1E73B615F1525F6A165DFD4461AE0B856B298B96CB6756AA2255949255E6C95C12A969A698D37B6FC8B7B16BF1AAA1734DCFE1160D319B25236F591C7CFC8A08FBA48B42586F3371B0AB061C92F9C0E3E350DE5941C9F380D876480C2623F526111D676AB33ACA5D66185655A94E13418576B632DB8A0D3830738C44AF9C979514254ED8C82E34AE5DD6458E2039690B8492C353E2F0937887088B4786BAE016104648C0544482C562E0D2AADE390DC03765C7C1353634B83AB66AC7E1306378B5032B3148BB799E8B4907188EFA993FE72C73E3162F2DA43F0EC7C2F1C162EFFB5659A947A3AC6203B1C825125023926115605CA1909D912A0108C0115FD75D967BACF32B656EA64BB1808D1C1B43E8D7BE02EB6432D89B59F9A3FD109AEA36ACED2A137214BDD8686D977A68A6E4B6ED6C916A8594CFA3700AF608CB6A91AF150077D5A40EA4471AC19A7177B1E6A6BBD8A357F416BA8935AE73829A5B035B79AE8D108ED5D05A089242ECDBE94664DA47F187D3D493AC23E7DE32CCA3941492845F6502143FBF02028F22B12F1ABADF29BD12458E5B698A875B78F8D30AADB59FA617EF807D5C23113A9908342F08E898E02991CA1D7B934D\"\n        },\n        {\n          \"tcId\": 56,\n          \"ek\": \"6A04C9598C985A554021C437246C8E1DB67F69B6092F7A18BA3309245480FEA533B478726E4C761568A00DECB368F104B1E908F1E9BE85022D6998581428881C6C1B06A89141831DCEC1A139303FBFD04174F06E81D09DA38251ABC33C32422917EA165FE821793BB13D553EADD9AE86C750A7C307AC6467F117B778ACA8E1CB8360152388EC20742011FA66C786675AA4813C8CD58EA2FC9ADBC4AECE94C1193B1799306D6DE532EB32928BD5957484B6E38272172B902AFCC4207958A433058280713FF055A9FB0FE6A3C79564A75C32AF13B4338CE1ADCC5825E9475C66201B8E6393D493001C29A44BB20326DB06983BCBA96805EF0573DAB1A506C33150A974EC96462473CA7AC7066F374A131854EF01A99A30770B0B508CDCB2B5D2B5F2208005378AD0935243EA61C8D876A1602CC047871425CF33BC22E07A94C71737BE251E75F85594F523B4CB241E97AC0306B8663075B8B18ECE09332991C774E225D7722DC1E87052E19293F8B802E7366A3C76D388351B63A11AF5703B4862699124E574B0117788DD89C8D7D2BC34806DCD183F28784E54B2725EDC9A797B1A9F110C0C91154BC927B06C77335A07E372B5C10C4DC8C5BD80EC79BB68AEAF8BAC1C583D4055C5EC4592462C60165C636F2A5FC3F9A0035B0094755AEEC7B076A7C84F8C3DA994C9C2C10C1E17CE56943933D733944753DF2546342890BAF305BA3B08628727BBF492F7123CA6DB9E067A02A7FC20B9138E5011A9E08116E77382C9AA594BE18470D0B9B3CA82FC652398A3B7466065732030F8622AFB4C29A88B1F8539976902AE18C9C0EF10780D8B405EBB37D873C6FE4534B8AC444D8A7817F6CE0C36B8C53AC03328B8D4B30DDA4111A7749B6476516B56147B3B9841E29D20520EAFC9247D9C6171A1165C971941A06CE6F58EC0A5B4FCF21CA78857BD6B2F0706BE3F568E380C24E537C31D74B0202014A24321AEE877B7E457FB669903A30D39AC9188239BA29157ACC82AE671C75887C0F1C9AC11DA5974E438EFA438467755EDB0C949536274DA3989972356A845811273E5308DDD4A3774047E7F347330133322521586635622937867DC549E058AC1D65BCAA8C6DED93C52FC329DD038DD244428EC602D0B11D5707AEA029328B61BFDE51B4EE3BA4CA8561031BD939ACBC70276A309D028A7674C736668D1735EF83B555689DD5790852916F450812F0A809C183A4BD2AA79B74FFEB232F3D67EBD0B3F97037ED8EA8A7C6CCAA4D300F3E01F2B8661AAC39FAFA78197A2482ED4745C56C757A692B1900D7DF921D6C35580F9882CFA294DFA0DBFC29BA21C9F648515EB2A81D2BB0F75941C1C1C6541D949A5454D96FB486C79B11BD9B56F45651B3567C75A261CB5C0B077352486C37715CA8D4CB727B167567BC58A530543047C1FA2459F87C837623C5B2939FC53AABCD5B1B8271A67C95B3E33C2D9922836A25BD1E61692BB36FF87229779AB75A883B26CB6B27553FB5782DC3C62D70C4AEFE86A3A36934770834142398187983D766BDE4916EB76852D74010E221460C4C5FEC132AA77B618AA0F81EC6A8E83BE7BE37239F1CEDD206EB8961F0D2AA3FFCB272A0785A36537240274C9993C4904A731995A3019CF6133C714185A449376565A65B5C2B1DEC8896C1BC46A1575CDF80958F2B96329791C208F51B4A8F41A8356000E0F1C51EC584ACE4434BF0035B5127478C11D659B842CAC49AE873F8192659A5735F87B7517B118748A74B8020D2823A25952CA3F43C7D3614741E054DC0B5E5AC11C8FE15E7F908A7B64B3016A39218134FB3092E7482E7399310709BE369CA383E95AD08718CFBA21F9DBCC8E0600DAD2C39B368A24E79DB0DB02395949CAE835AEE683C4A6AA6024B3109C18DC2BBAD65BBDAA52ABA6D9487EE3194579494B2441177019C0BCB78F8A2FCF7BA875548C1C3603AE3A874D2454C11074047310D136AB4EAA348F05B4662989CC6896DAAA24A94154CDBB30FB5023A37779B1E29F71A841830274B733ACAEA69D84FBAD4F138861FBC35F0545489AC8342A4F6F8C584A8442447062D35BAF44B3C92A38CE11A173FD5A3D8D6B24BB390ACE212E41042E75D50E318C6FB037AFCCB2B069E2BA66AC1233D53031279ABA1683AE42A745D0002805374165EAB94A17EF6881F0BA9392CEAC27250E82622E6F40D3810AE40CFF1F8496\",\n          \"dk\": \"BC00ADC7652EA7DCA970F2C234A87D9C3340EF2994A79A71B7200F9168CF76D9A301B85A01E6AE69D8959533A53F4890378067A6068C2C8201D373BA49C69FE6EB1716FB6BFAA4ABCC071B191515ECB758EE859D40A12429DA703052947F4097E7E40180477C01B62944CC2CB7E63952921E1EBBC800E1A7385B01E528548A31B0BBD0A5B0641E9160C36236CB58A36555D88AF118C35EDA2276B38D5A8C0C091795C1254D69AB5D4DD309E376BDD7E4A3E1333B3FFA378FDA8E62525201F9458F53443A255ED79B71102A2F3EC347166CAB38361B3E668BEE7588AEEACB5436B1F8E360F1D73999E290BC25C036ABAD878890C54BC8DBB863F8E23E5323ABDB004141C9C3C0963B7FFB7EDBCB545114A61A6B58F8487A07F388F4F5C00E9CABD425B7BEAA2B9947A0482A21572B48E65C8FCDBA214C0204E246CC3EAA069DD42DAF7285ABB55DAB8B2A016805A1C17D46C131ABD4483BAC6B5CB8C36A810DDD100E4A66A58209CA60877E40E2BF58E37BE97C8122D7097ED173FB272AADEA18EF91120405435365BF670C17B99068226597DDD7C4A6A16F844616C885666AB482B7858FA31405B86789CDA046D6561E997689C0D0A461E776C7313E811AC02D1BA8A95038B12430748644D0B933C9E35F685229D97BC810B3203C874673D18E8E0131FBD368C47466A9882F4A41473166C2E240A82D36B983273208EC4008231773D57FC9D4330703565CA8759E7CA9E64130BC5493C9F52C93AB4A7B3754B0F55EE9779CD08B2BFAD71899B75CD274ACA7C2BA9575AE56DCADFF13A6E6C5266BB7AD0D8BB820660023E9BE0FF759202CCEDFD7C3F2048D6B345CAA1C82D479A4D4E2C48A1818DA022435BC3D64B38C811B0BCFC147FAA505AF13740B78945F973BFD2A21A6A040B65214F934154E722D7F195877C5476EE615344462C2A95744956DCB5B2657F27DC2C6AC14E92E8404082D8A0EB6E488F2E56FD0292FDE931057D534051700AD0CA85ACBB5C6C02077DA856931899BE82CFA0A794AD1182CD3AB5E5188C357C3A2D9BD130A62223293DD249E477271C9E14AC48017D013A1435A5976C4165E8932CA032493B33814F90830A5AD75E32F747B94B4C52BF5EB30F73C02BCEC5903BA01F99ACF8B9ABB671B8D219C029BA1A2A6E847E86091C08C00B3914DF8D075C3913C805104A3B4BBC798B9F1D20AFB8118B7F26C32633B39FC792B3358606054BE3693A6E7B9C6B6B3A848A2D0B2ACD1C406706CB590B4B79B82BB88415CFF45376B8102B644193FA1890040127B58BB351862F3227AF18A50A9A983387A8646C99860BA2373337FDDF25CF8916E00252AD07516D1BC5629245EB341B1ED4B63F93044B131C01E301E8FBA5DE8AABA76F70F7D2A3ED4F78AD5210B47D8756E334D32CAAD88C22DBC537FB766447B09A05F1685C24A80F8C3A3202790B277A6CDC836767793D81955BC90A3DB3B71BD8C2FF01A12A59541A666202CA98689528DF9179A26156A6A414BB8DB350393B6B0F35DC163CBA4C94242A33C9778BB35563A8D213A9914225A0C6876B75035201DD14048DE28AF97D77260476DC399B37DBB48F3C943F3C990ABA7068D4299F7D289C6D66D4243570AF38D8FE95384B27E6B0C01258262CD0465F5FB5F424CBC9C39CF5948804F7034C632CB8A7B4ABC3095B174951C42364B05396E140524B989A889CDCFCA4F222830A98B041338A0273478E3661A5D44214A52A54EC65E933011EB7148EAB94E58EC1C9CB9A0E61C788880B15413B8365A929342B667005D6A35BEC272955F5CBB633798094A0DD84A09B6A04B16BA9D1C005D6B1708CD4300D26B506748CB7B4CCFB59818FFFCAC2A141ABCC1AB85BBA36C69608CCA475BDB61A56BA2FD6B2F75F502FCBA8AE05B11B73A381A810681EB0B1F446007267F7C12526BC3B430CB4BBED0694C290B0EB238056B5C64C1AA072C97F7173647BCBEB75965BD71C0747503929ACA0FD98EFE45C744A4B5BCC748616A8B24C5BDA4F0B9FCD42767420971A00F32415976B5A4DE31C03B10858CF1995B7B8526FC352A0C99A57095EEE0B8E7E40364587623B00B1799C807289DCFAB746B06C3E2912A6D3778FFFA8C1B43A39A433708817E7058C03698268E9275BCF21FB73052874307FE56CC8A384E6A04C9598C985A554021C437246C8E1DB67F69B6092F7A18BA3309245480FEA533B478726E4C761568A00DECB368F104B1E908F1E9BE85022D6998581428881C6C1B06A89141831DCEC1A139303FBFD04174F06E81D09DA38251ABC33C32422917EA165FE821793BB13D553EADD9AE86C750A7C307AC6467F117B778ACA8E1CB8360152388EC20742011FA66C786675AA4813C8CD58EA2FC9ADBC4AECE94C1193B1799306D6DE532EB32928BD5957484B6E38272172B902AFCC4207958A433058280713FF055A9FB0FE6A3C79564A75C32AF13B4338CE1ADCC5825E9475C66201B8E6393D493001C29A44BB20326DB06983BCBA96805EF0573DAB1A506C33150A974EC96462473CA7AC7066F374A131854EF01A99A30770B0B508CDCB2B5D2B5F2208005378AD0935243EA61C8D876A1602CC047871425CF33BC22E07A94C71737BE251E75F85594F523B4CB241E97AC0306B8663075B8B18ECE09332991C774E225D7722DC1E87052E19293F8B802E7366A3C76D388351B63A11AF5703B4862699124E574B0117788DD89C8D7D2BC34806DCD183F28784E54B2725EDC9A797B1A9F110C0C91154BC927B06C77335A07E372B5C10C4DC8C5BD80EC79BB68AEAF8BAC1C583D4055C5EC4592462C60165C636F2A5FC3F9A0035B0094755AEEC7B076A7C84F8C3DA994C9C2C10C1E17CE56943933D733944753DF2546342890BAF305BA3B08628727BBF492F7123CA6DB9E067A02A7FC20B9138E5011A9E08116E77382C9AA594BE18470D0B9B3CA82FC652398A3B7466065732030F8622AFB4C29A88B1F8539976902AE18C9C0EF10780D8B405EBB37D873C6FE4534B8AC444D8A7817F6CE0C36B8C53AC03328B8D4B30DDA4111A7749B6476516B56147B3B9841E29D20520EAFC9247D9C6171A1165C971941A06CE6F58EC0A5B4FCF21CA78857BD6B2F0706BE3F568E380C24E537C31D74B0202014A24321AEE877B7E457FB669903A30D39AC9188239BA29157ACC82AE671C75887C0F1C9AC11DA5974E438EFA438467755EDB0C949536274DA3989972356A845811273E5308DDD4A3774047E7F347330133322521586635622937867DC549E058AC1D65BCAA8C6DED93C52FC329DD038DD244428EC602D0B11D5707AEA029328B61BFDE51B4EE3BA4CA8561031BD939ACBC70276A309D028A7674C736668D1735EF83B555689DD5790852916F450812F0A809C183A4BD2AA79B74FFEB232F3D67EBD0B3F97037ED8EA8A7C6CCAA4D300F3E01F2B8661AAC39FAFA78197A2482ED4745C56C757A692B1900D7DF921D6C35580F9882CFA294DFA0DBFC29BA21C9F648515EB2A81D2BB0F75941C1C1C6541D949A5454D96FB486C79B11BD9B56F45651B3567C75A261CB5C0B077352486C37715CA8D4CB727B167567BC58A530543047C1FA2459F87C837623C5B2939FC53AABCD5B1B8271A67C95B3E33C2D9922836A25BD1E61692BB36FF87229779AB75A883B26CB6B27553FB5782DC3C62D70C4AEFE86A3A36934770834142398187983D766BDE4916EB76852D74010E221460C4C5FEC132AA77B618AA0F81EC6A8E83BE7BE37239F1CEDD206EB8961F0D2AA3FFCB272A0785A36537240274C9993C4904A731995A3019CF6133C714185A449376565A65B5C2B1DEC8896C1BC46A1575CDF80958F2B96329791C208F51B4A8F41A8356000E0F1C51EC584ACE4434BF0035B5127478C11D659B842CAC49AE873F8192659A5735F87B7517B118748A74B8020D2823A25952CA3F43C7D3614741E054DC0B5E5AC11C8FE15E7F908A7B64B3016A39218134FB3092E7482E7399310709BE369CA383E95AD08718CFBA21F9DBCC8E0600DAD2C39B368A24E79DB0DB02395949CAE835AEE683C4A6AA6024B3109C18DC2BBAD65BBDAA52ABA6D9487EE3194579494B2441177019C0BCB78F8A2FCF7BA875548C1C3603AE3A874D2454C11074047310D136AB4EAA348F05B4662989CC6896DAAA24A94154CDBB30FB5023A37779B1E29F71A841830274B733ACAEA69D84FBAD4F138861FBC35F0545489AC8342A4F6F8C584A8442447062D35BAF44B3C92A38CE11A173FD5A3D8D6B24BB390ACE212E41042E75D50E318C6FB037AFCCB2B069E2BA66AC1233D53031279ABA1683AE42A745D0002805374165EAB94A17EF6881F0BA9392CEAC27250E82622E6F40D3810AE40CFF1F84963DA07CBAFFA3C26C86115A24F33F1FAF547933AD64AFA40EF5F0DB03D53B340E384509DB0E97D4689A3CED953CFBFFA9D3B3B87CCB0C6A360FC0DF3CBCA399F9\"\n        },\n        {\n          \"tcId\": 57,\n          \"ek\": \"0C25AFFB80C5BC12C3CA5357097716E75B025C298F040C5AB8C92F93A08A91953277A7CAAA1776440C9E3A5C819D7B9BD58AC0A53C83D9635D3A882C36CC3E48959D01E67A4E363273F05504274BC144C5D4B4774FC98638895EF894A78113362EC428E0314FD5119C40811D22313D42362176E001616901952C1100C59E33428EDF13C68086571E8A65D042556A6A3B18582F79B443EE22B53631C02FC03FA6CA4BF4BB0B8B97410443C0A3CC958AD42BEDB3118254C19752AA70076FF8F312E50433884A72F287266BB83A417134CED7C66EEC3A9C90043044B61C795CA69B8EFEEC56D808610EF5B6FF764A59D308E60B64B577CA70487D340BA3C8027C45766D270969B284B54DB8A29831939174A2FF274F7EC131858355C23624CBE7B6967288C0B660ABAB4C11C2106F98909AA7AAF50BB1CA0221E1CC00B31696DD05C3E46C7CF5A22B28E404B171A8BAB14146B740B946148591C0505CBDA8E2456809D019E6183D4B10E5F9C6122A7E17D579C5AC4FFC62CDC9AA88102CC386E50770E36A257BCCAB1826BC07D0CC58AF9C49291DB9A63753CE3F9C2AE190834AA77662BB5072F89FA29A2388E532361537BC28310A6A4133B5A0A4795304117839832C8AB7A397141B8A3C47A52689BCC26E3851BE6524CF2FACCD8037B69279021539BD084B31FFF084ED7AC403EB8BD3EB5F409B8778783697B5512C0B91B426714C426882F10DD165C91EFC240C2B129500595D08A5AF198FD8C6A1A9D497654A80EAD40E8B3347B7C51F2E220C1B950788139DD07B6F00B1789EB0A7AEA60844339F04DA7811824F9544320064725B809862447397786381615D3EF4AF575A4CB8FCCE137A4C85241A72BBC424343A3C916C6E51BA58768855C29EB17AB4C8C8C3F3A0174F5979AC02795B60716FE96BFCE517821C254F2A8DD8D77FE778B82E3A53C8592048104FC959B23FE36450913BB1265BF91978FBDACFB3601DDB978CDD77314C8102DAA75D1EB8266137CBC2956714294659809E029067D7D6CB6C93148FB102C34450118835C85509E3F980446767FEA734AAF7C8ADB7CB11435E7600842FF96ED8C75FEBD643BC1A34D1666D8B6B9EDB2B8C72AC5614C603AC200D00461EBBA069EC751CB3C36A60616C73CBB8B2C17798358CC4C588BA3122FAD04D97D51687FA3B19D465F6E9AF95C68AE5225FE04A14BDD61B54A502F820C9DB0256B11AA524F463C11A21F1610DBD800A3D690E0139000B026B7C1628FCA3365A358FF8AA01429CAB2106055144CF0597C2DAA1A623DB1A85730AA0046127287BC407073E9C7C74042DDAAB008CEA58B8DC7765C85844B3395561A4772B5D4D551ADF12687B18C63260518576440EC7BD699843F5651F5E04B25085448CD138257C2E2612733ADBA6FE7357F1C366FBB1457C2458C4F848A24A1BAD74B9A2AB1A603408C540C091C6A100850B4A327211A7CE336426E7B250D50012D7262EDA4218C3B1A55D50337C63A9790406A846CEC038B15D157DDD030FDB293B187A63BB38208F075599803E29149B1F097B1867BF65EC69ADB00ACEB21345E1ADE7E5A36E20B69062C5CCA550C95331F069CFEC59429531B4380A079669C777764DCA8ACD8087AC091CC2B6075CB9436A3B4C22AFACBC0CFB95D361C2232390515A21C37AAAA839005AB97D6962BFDC8959A51ABDD427A15BE830319CCEADF3BB23B59B63452F6EE2956D11C47E721FFAD1724CFA7C1ACAC3455C6E0A75834A23AAB786079947ACA32113A2223B2446790738B464A94683D47357E8847063539080987BC742BD149756541F94D5539E886C13FC1FB291BD1FD8445DC47347072D063A71F9224A85366D3364A1B97ACA267C717BECBDCBA78FAE3BB589BC8794FA2D20A8CD8D7B58124843B7E46FF0F629EF92105DC43DF6645ED57BBDA265874B63A193D351BBD0A5A48CB819979C2A57A486A109957487B51AB7D8283076585E6F06AF66A7B7776BB219911391B67380215FA2216FC061C441B87AF6AA077E77777AB3C7101911B3DC5C1D59952879C1DE1B8741A24C4CD28239132E89024C357C76BCA7CD4696423EB6C578C53C32098E8CCC41321C93CDF79A0DF21B6F69C3DE80C9EBD838B7FB0759165DFD7A6200B88588E992CC173E0A6B0B87554CD2C3C4D42B6B575C9180F1A418D8E400CFF37C23D4C3E3EC2627627F6BCDD1E1F45D7E\",\n          \"dk\": \"BE49618EF5BCFD880DC312BFFDC519EFB4269D0B2391B89C01D6A6B05063346A4BBAFACFC8C091B64CC1AAA08D6F40BCB4043BAD7416858BB4121567BC380C042C2D90244943028DFACABBC6219060721832241591BC030410064E45149CF96C36A6A1FAEC889382340A5BBFC9193DBAF8A43CBA44BE25143AE10A33C6AF8541AC2503835E0743BD74CB899373BE77987492A071E57027C688A2123F56A5B3D268C1EB70A21F6A98BD239F9EE5927F21254FA5BF11F41578AA84770C78B9642E2F3C94EAB1CA5D10651F90A1E49033074B78B6F2C9D0D784C7A931E2EB8336F8802EAC29DE8737EF897FD0A31A04F76B30B5C6B68B7C3A2C3CF0C5B4B313A83F70A59A84901C8B819F8502F624368C493231C19340D67EDDE503ED9805A05A3E23102E6256117D9BA283619F0CB3373E743B2771A7D1B020949600098A40BBBBC364F6AE171BC6FBFBBFACF25B0BD37102D336EE1438022AAA2DEC0ABDA6689A4595A18161C7A9728A0C9B4F74A65D8A432A100B1F46263BF525024876B334CD8D9A1BAC2599F1F0AAADF04E7C83C7EFA4663CE59873533A0CB84AB8EA28BC9B808CE71832D45E4ACA223D7CAD92699B2DF37E080B6F3B6861C95A210362C5FB849EFAB8A845C25C375078247C397143C839F45569786E3EAB14A05679098B25785ACBA2D49C4FC47796271949795925A80EC53A8F7DF6A2A6488EB4E991CD7976584A2FE301CDAB93A57070148F8AAA81141BD932951EEA748D6066FE3B9D60143CAA0339CB8AC2530A0AD0EB444ACBBDA16B0E6A0A6E54885FAB6170A7723F4E3B23D18615B6AC580B2401E485C3543519BE6A5590C7469FF47A3DB91338A5AF6FD28A91720402F70DB8A491946192AB573A6E668CDC7B702231986C114EE18902C78A3D92D1CE616511A66999CF5C0071E615E8A7857D561B0B56C3C35560DDA90A18601B8650AE2520784D1257B3A7339C09B5CE95138E57B69FE90C1F97C03280C0806BC366E75E0D3CA2D355A2E2BA661EF0B96F556C50656724567745C1B650634A8DFA4699222010C59257C38C7D202328767663127DF2F294B00390F674C021B1934F65514AB125132B23A0A427919946D1A96A84491C353405F505937FFB8FC0813700E37C8151386E248C0BDABF1961B2859BCCD9B33960E424A87042D45A6AF180B2CF77B25B7912001C958EF55306927E8DE71488C0BB2C7C8B71767403B6185B41C22DE1BCA99AA86297A2A1E17015F553B967720B159160843DE88B3D033B4D5D563326547A404B352466C9A82415B08A54456B37037971B77A937A540A2DE05B91EA5E540996C8D15452467D0F81568DB2482019B783309F77867175A4CED0A0660FF2AFC1F09B09A03A7BDC1621A613AA4C2571B23FD7C1CECD5B75559C37CDB598AC108255479667E117F51B0E30866F20B087D9088765E7613F3584CCDA8EA7A13CF8E5583EA353661AB7A42A264F9AC6EC529C2CB7230CF45D2AC1B95D729F00B8792983AFECD44B3E88A197D34E2447490729BDAE7547B7119A970B0C3153B379148C4BB61472662669E2510C76549102187D891E56C20A3328C03AFB32760BBF09F604AB0B5C5DCABC83E80CB17A7BFE4B147F6670128A80E93B359191424C80C47CE001D926C6830962083B8BF1C03FF9F1A1034139D974229A581035E73F7CC10D56B281FE96755F214738B70AD6351F8F30CE5DC5555F6B69B806C5D2574553E7C94362A623F2C4B31502FF8094F6E08394C880EDA1299B2C03F1C696D80C3640584C13789E32192A27B38896884F6DF0BDE386A1A327C5C52314A4BACA2D7928AC761AD898CB4298905AA76ABE3C5B7BE75704317D44A9C98FE59C812C88A6E334C0163EDE8AB622F351D9F3926375291B5510C6E3AF2641A65FBA17C7778B9B2A6C2E5BBA6BA225EA40CF4ABA7639D488ADC0A970F6581213352A409065CA92AA8694916C57796B42C96C830FA84C6C70C2640C495F10A48D1A210BDB8BD0C20EF2074825651C5D9B3EC606351B34796FC578307CCCE54A2EBD648FF051CA6FC9AB0636CB6ACC49109343F1465052B57404D46EFDF856CF330E539402DF699E0576953C497A9D662E768ACDB8A9822E3A9271C401A364B1D060924B84939F7110B45012EA36121A4A91CD860258D559ADC0530C25AFFB80C5BC12C3CA5357097716E75B025C298F040C5AB8C92F93A08A91953277A7CAAA1776440C9E3A5C819D7B9BD58AC0A53C83D9635D3A882C36CC3E48959D01E67A4E363273F05504274BC144C5D4B4774FC98638895EF894A78113362EC428E0314FD5119C40811D22313D42362176E001616901952C1100C59E33428EDF13C68086571E8A65D042556A6A3B18582F79B443EE22B53631C02FC03FA6CA4BF4BB0B8B97410443C0A3CC958AD42BEDB3118254C19752AA70076FF8F312E50433884A72F287266BB83A417134CED7C66EEC3A9C90043044B61C795CA69B8EFEEC56D808610EF5B6FF764A59D308E60B64B577CA70487D340BA3C8027C45766D270969B284B54DB8A29831939174A2FF274F7EC131858355C23624CBE7B6967288C0B660ABAB4C11C2106F98909AA7AAF50BB1CA0221E1CC00B31696DD05C3E46C7CF5A22B28E404B171A8BAB14146B740B946148591C0505CBDA8E2456809D019E6183D4B10E5F9C6122A7E17D579C5AC4FFC62CDC9AA88102CC386E50770E36A257BCCAB1826BC07D0CC58AF9C49291DB9A63753CE3F9C2AE190834AA77662BB5072F89FA29A2388E532361537BC28310A6A4133B5A0A4795304117839832C8AB7A397141B8A3C47A52689BCC26E3851BE6524CF2FACCD8037B69279021539BD084B31FFF084ED7AC403EB8BD3EB5F409B8778783697B5512C0B91B426714C426882F10DD165C91EFC240C2B129500595D08A5AF198FD8C6A1A9D497654A80EAD40E8B3347B7C51F2E220C1B950788139DD07B6F00B1789EB0A7AEA60844339F04DA7811824F9544320064725B809862447397786381615D3EF4AF575A4CB8FCCE137A4C85241A72BBC424343A3C916C6E51BA58768855C29EB17AB4C8C8C3F3A0174F5979AC02795B60716FE96BFCE517821C254F2A8DD8D77FE778B82E3A53C8592048104FC959B23FE36450913BB1265BF91978FBDACFB3601DDB978CDD77314C8102DAA75D1EB8266137CBC2956714294659809E029067D7D6CB6C93148FB102C34450118835C85509E3F980446767FEA734AAF7C8ADB7CB11435E7600842FF96ED8C75FEBD643BC1A34D1666D8B6B9EDB2B8C72AC5614C603AC200D00461EBBA069EC751CB3C36A60616C73CBB8B2C17798358CC4C588BA3122FAD04D97D51687FA3B19D465F6E9AF95C68AE5225FE04A14BDD61B54A502F820C9DB0256B11AA524F463C11A21F1610DBD800A3D690E0139000B026B7C1628FCA3365A358FF8AA01429CAB2106055144CF0597C2DAA1A623DB1A85730AA0046127287BC407073E9C7C74042DDAAB008CEA58B8DC7765C85844B3395561A4772B5D4D551ADF12687B18C63260518576440EC7BD699843F5651F5E04B25085448CD138257C2E2612733ADBA6FE7357F1C366FBB1457C2458C4F848A24A1BAD74B9A2AB1A603408C540C091C6A100850B4A327211A7CE336426E7B250D50012D7262EDA4218C3B1A55D50337C63A9790406A846CEC038B15D157DDD030FDB293B187A63BB38208F075599803E29149B1F097B1867BF65EC69ADB00ACEB21345E1ADE7E5A36E20B69062C5CCA550C95331F069CFEC59429531B4380A079669C777764DCA8ACD8087AC091CC2B6075CB9436A3B4C22AFACBC0CFB95D361C2232390515A21C37AAAA839005AB97D6962BFDC8959A51ABDD427A15BE830319CCEADF3BB23B59B63452F6EE2956D11C47E721FFAD1724CFA7C1ACAC3455C6E0A75834A23AAB786079947ACA32113A2223B2446790738B464A94683D47357E8847063539080987BC742BD149756541F94D5539E886C13FC1FB291BD1FD8445DC47347072D063A71F9224A85366D3364A1B97ACA267C717BECBDCBA78FAE3BB589BC8794FA2D20A8CD8D7B58124843B7E46FF0F629EF92105DC43DF6645ED57BBDA265874B63A193D351BBD0A5A48CB819979C2A57A486A109957487B51AB7D8283076585E6F06AF66A7B7776BB219911391B67380215FA2216FC061C441B87AF6AA077E77777AB3C7101911B3DC5C1D59952879C1DE1B8741A24C4CD28239132E89024C357C76BCA7CD4696423EB6C578C53C32098E8CCC41321C93CDF79A0DF21B6F69C3DE80C9EBD838B7FB0759165DFD7A6200B88588E992CC173E0A6B0B87554CD2C3C4D42B6B575C9180F1A418D8E400CFF37C23D4C3E3EC2627627F6BCDD1E1F45D7EB647A2888D86D41D8661A91766BA969E80B9741B21D1EC6E349B52DE8191901B63DAD9B127F98E72A3C65ACF4B172FDBD9B9C39F24F728D1F40EB02C9949419D\"\n        },\n        {\n          \"tcId\": 58,\n          \"ek\": \"5BDC0216639954EB53EF77250D2853878BC0183C0EB5359414CAC928E3ADE2E78213904D7DA453E96969A89259822C205FA46CA6703B6D324890E59F0C377C778ABB5ED2466AC929E8AA2E9D47A8CCE606F52B18A7C35C20F4C2AFC99AC327802B856F13BC1D9E3646BF2A2038C454095C147BD1474663274DD50041C9939EECCE460B7FF1605743D4243553A0752465E93C628324BB2D3783EB1B63AE85AA53311C5574071E56991211426DAC6F6AAC3EEA1569B04654D4193DD689A32432447E1A3F0F5595FB37A35FECAE5C55CA839A3CED94AE6FA436A7D34C2A81587CA84018933FDF39A931B03D81491624651A5F6AC75E08077FAAAE8AB023D6344557D15FFCB91C10B06BDA114C5C133F3782C309C347F2A4434DAA89F202817A227C1E1240FF41A4F3CCACFA497CDDC53AC5B0C55614907865118C2C7AC0D5031CBC106CB17A26B97755271C86E871BCC9590E1C2684F2C24A172B10844CA44346FB969CEBA1A02B9922F3AB68457CBB1F2BC6B5F5CC4EACB9D0072EAC617B31D27301E002E978925966B642B00AC53C912EA85DB59C5506F4724B2C356CEB40BD58AE51843537C537EC97B24475318A5B30AE750691765FEE1014644B1CACC77C9C334805A58DE18121FEAC6BDB61184D2C273AB725C32091CA3441E5140433435AEB0B51B9C57066C43F308A12F782BCA5237D7B12119CDC9AE2E7C65EBC66B3F02A0BE7361E08838114BF0D5250304A36935135C61597A03903EF215C6A223DF8A9515E622932F7A29CFA74807B145A20ADAA7987B0B981B7A62D115927504A090F61B37592A5B4E51B977821E4D92228531D2BA1523BAB3D6C850FB0660646892BE2A006DF51743FA383AC47572F651AB02A272A458B81441C8E774547E1773EE29A6A27BA4E839C8E023A13635922009B1111801872C391641F0E239ACA70CE3FC01E5C3736FEE2AB3F85C4AFE3A0BB2529D6EC047337819084222F0ABE8F859925708AE3E62815083196248720EC28174C01D716AD472A0CB2747EDB932F4A58ACE9E2BADFE468BA2A138EAB864EE00C034C1207F92F73AB084E32BC244951477549B2189568BA28553937E5F1CE6ED1B7F0C1012560296442AA6DA0879705A65D77460A86C0C06422A2E6949BB78B955355EEA2749BA0948ED094E5D9CE7C79B9F4047BF4E2128E41CA5AB1BAB1A87A2565CDC018890D426B37F01713DC923CD372605763C3B51A5AD58109F031BD665192F38EED496EAB15AF89DB622DB73A88BB6104D44C49577483F468658A9C72C1A2046252DF7840325978589A85F07C7572E44445F9A8854825E784706A25A70ED0CEC3F5A372C41C54185294975A70AB759B4250E1F2CB35E860ECC25932F55E6991041AC24D56BCCBDA579E421C905469052A34225AD50B69E191B4A36EB7DB1DC1216BB41524497B279230053E910EAB49859D1A99DB3C56CD70995BF031FB71B0AA40CDF031183E5B3D8D8799CFC3ACDDA54010AA6169A80D62479583111FEB337354F6BDF36A33279B7472800EB604710AB94A0D584942E41B78F2C387993A5EA079F3C74207F7972646CFD4E055FB6B10BC69B369D166143B8A8E7400134999F05C6A26DBBEC3B23D92CB90F77156A0C8A96CE93F73962C17F8104B52AFA2B68B62810F998577E07CB16FAC499F38AD628B57FC1C9CC7A568410478176BBC14C8C7A594A07A3568B503BFE26BAD88D2CAE92B90F1E1472E9697F930B6E775A1A9B93EF62757B2A28F6962CF2C1478626A9B65B5AB89CC24EE4C9F765B8071737C25F383DD57598D2289DEE439D28AAE91A3B21E389267FB3D146276B7810D126AA5D096A16F679E28D5C91A580777D9CEA13C46234653634ABD964C720A11A6514A7FB7F5ACB0992BFC7C16FA84CD9686ACDFF2283A89625BC65FEADC9171ECA359BA50BEE412D31414EE110AD1D8748237941683BF292618F740445C0B728D51347928559E569EBFE896DC6295F87517AB31A8639B50C0F3A08E96926816B7B17C8ACB476C4C67B1DF7631DF7540276C99053B102738BFA2876E42A028869C688C18AAA9B00DF0F3A74CD81D86D161912C09362AB35BD8731A48CB45CB92EE8A3374B811EEF93A37F17DD7D8AFF88958E13012E6F7BD6DC63E27BC47F506048CE24BB7411538EB4B969656DCE8DF5ACF28D1BDF5ECD14A44A98350CA45699F033EFA44D25E93FC2094C49E\",\n          \"dk\": \"18A2405BD102699A2B22168A31D2C0E7F946160428EE787E8C6C57ECBCA0FD170DF93BC66203A8F06A3292310EE08495D1BC0DAF2A2D92C243DAC2B558291276217430A08D11B9C5DA411F2D55406BD4182913086489B43D74C602780701295A4C0054765338695231E6798020E7CBF1697573474AB35B27817BB38BC2313E70AFB914B5ABCBB28B7124302B338B471FC89336D1C35FB6D59C007402D1D19A1F96682F7551D8527EAA07389E55A8080791178407301ACF22EB32E0F7119DFA0E623035364CB0243B19918792011023A086016CE39BDAF0868FB08A4E1753B00B444A807787EB973CE52F63650B12A6B51EF435048A9F7136A5A77A99238763F433C00813C5CDF8CAB21B445630AE2D733F7683701242751F5C17A4F15FAFA5AC7E5A231097AD06299DD6AC194D1ABE895A0315E834BB364C286C8CD0216BB032B59D797245F40D624872036BA369355AF73C6FA5540BEC829112F28294191096A8AF53D37C5DA8B151330098582AA06C5455408765D43FE5B24C9C0471C9C85243C205747A43ECE8CEB31CA87BB51B39D15433F99D6FA638BF02373D42A09079399B4CB0A371984061370078A677E166D3D7B82E3174B58885D280A1D44911EB0482C58C7216D5A22694C767B9CB35BB0A1B0271471C558546037BE5585D43C40F6705D8C0C4579025898A2E21A0C5B0A4A42AB290E1F37D58486631210EFA0669819A90742926CEC68B1801B44D597E2CC84661917C9BBB6E2FD4B00D11AC616A245088B6AE323332FA40CFC46845842B02328D6E9A9246632DB8FC960B9340DE367A25D3996F48943ACA2656250D823BC999250C357C718A7CAC1197868D68C70F324391A75EB88C09AE835FD732B9047AC4DE843A6D00BA1EF79AEBC350B1D3541B874C3105B901441BE9F9663C306E085B0B0F98A4132586111417346B85AB5C26FB0522AFCCCA2168308803243BD1C1611574533466A1F709027485D680BE09624B03944EA5399E62A3A8A005B74227041E6C394CA22DBE3A6666541377573AC5357106400B35473B6672A7D4517F1C91A6A1266D25809B57720207E4CAFD88751CCA82AB54593CA126A0C60771CBC49535CC9A83882DA67D6F94BCCF7A5B605215B874C4786B24995A83C679BC12E870C61A3820DA5B89C0863B369A6C79B13ED67099EA02CCA1B9523529992A215C525BD5D37A39E69891FABAB70594AF8119F356A85587604AF35CC3A01692047576D97456BC1BA23639D4452A91C40A732509C70C83CFB080CE2BA6F29A80B0066D10A96A038C72CA7B6860594EE3C7C592016E147C7FE478B90F8A4CBEC166A3C474758642EB76CA3B0A806B16CDA19013986B10A3844A0CF86455F2808429B371A0592129414FD31BA24C995E303C776B014BD35CC644095B7140F2AB3336D55DE6D54F9FF244B6CC8580111C7DB2492A32A073E0196FA256A92CAF6B431B6F6053E52C308D6C0F8D86001258B78290AAAA85C9B2059A931833A6C82825B332943ABABF65451272AFF4BBC0FC279B603CC1E97C18F40641DF91776EDCB7DE245DF98099A9DCBE5B1C6C67FCAE6FB18A4F950833197364D94D224B8D29AB8860199CCF8CA3C8D1217D99484A631DC29923AA582FBB2566A44BAA1FC037C139CA3AF90D9D0516E7B0732F552BF264325A121F20D393C8E794D1E32A3C12BF68C849A6548BB308753524CE72130F037BA1C02A50162CC10437A5337C6056E850E2319474AA21AC223F8330C43E3A0BE3B0A42227266E6639252271CB2023A55866B004B8B43CCF7E212457157893247201B1AA4728BA476AC7D3B303A6F3BDB818641151BAB769C7BF2A1EBF027A79F81214687D0886C99FD5A46C8B4A1F5697A98BC9667CA574A1CA283BB4F424237B37C96A5836709C7FC0717537CA95213322E3396A55439E1F8C7756A0625FC23CD2745E63A054C47259025C92AAD22E2DC52045A8467AC069E6E22B65D25C2DCA2BDD31AF5057B0DBE93E3D921341046E89E5618A62C562349F2A646E65EBB64091ADECE8CFE68460FF95842EA6648C89BFB0E5AB1CE11103904AA04C4F55F7C64DD810917B756B02BC1054A8DB7116D0718E584ACB47C679E39232E1C87934D84111B0087C04C86ADC5C7F060D5B639FD9B1BC85775461100FA6BACCAD22495BDC0216639954EB53EF77250D2853878BC0183C0EB5359414CAC928E3ADE2E78213904D7DA453E96969A89259822C205FA46CA6703B6D324890E59F0C377C778ABB5ED2466AC929E8AA2E9D47A8CCE606F52B18A7C35C20F4C2AFC99AC327802B856F13BC1D9E3646BF2A2038C454095C147BD1474663274DD50041C9939EECCE460B7FF1605743D4243553A0752465E93C628324BB2D3783EB1B63AE85AA53311C5574071E56991211426DAC6F6AAC3EEA1569B04654D4193DD689A32432447E1A3F0F5595FB37A35FECAE5C55CA839A3CED94AE6FA436A7D34C2A81587CA84018933FDF39A931B03D81491624651A5F6AC75E08077FAAAE8AB023D6344557D15FFCB91C10B06BDA114C5C133F3782C309C347F2A4434DAA89F202817A227C1E1240FF41A4F3CCACFA497CDDC53AC5B0C55614907865118C2C7AC0D5031CBC106CB17A26B97755271C86E871BCC9590E1C2684F2C24A172B10844CA44346FB969CEBA1A02B9922F3AB68457CBB1F2BC6B5F5CC4EACB9D0072EAC617B31D27301E002E978925966B642B00AC53C912EA85DB59C5506F4724B2C356CEB40BD58AE51843537C537EC97B24475318A5B30AE750691765FEE1014644B1CACC77C9C334805A58DE18121FEAC6BDB61184D2C273AB725C32091CA3441E5140433435AEB0B51B9C57066C43F308A12F782BCA5237D7B12119CDC9AE2E7C65EBC66B3F02A0BE7361E08838114BF0D5250304A36935135C61597A03903EF215C6A223DF8A9515E622932F7A29CFA74807B145A20ADAA7987B0B981B7A62D115927504A090F61B37592A5B4E51B977821E4D92228531D2BA1523BAB3D6C850FB0660646892BE2A006DF51743FA383AC47572F651AB02A272A458B81441C8E774547E1773EE29A6A27BA4E839C8E023A13635922009B1111801872C391641F0E239ACA70CE3FC01E5C3736FEE2AB3F85C4AFE3A0BB2529D6EC047337819084222F0ABE8F859925708AE3E62815083196248720EC28174C01D716AD472A0CB2747EDB932F4A58ACE9E2BADFE468BA2A138EAB864EE00C034C1207F92F73AB084E32BC244951477549B2189568BA28553937E5F1CE6ED1B7F0C1012560296442AA6DA0879705A65D77460A86C0C06422A2E6949BB78B955355EEA2749BA0948ED094E5D9CE7C79B9F4047BF4E2128E41CA5AB1BAB1A87A2565CDC018890D426B37F01713DC923CD372605763C3B51A5AD58109F031BD665192F38EED496EAB15AF89DB622DB73A88BB6104D44C49577483F468658A9C72C1A2046252DF7840325978589A85F07C7572E44445F9A8854825E784706A25A70ED0CEC3F5A372C41C54185294975A70AB759B4250E1F2CB35E860ECC25932F55E6991041AC24D56BCCBDA579E421C905469052A34225AD50B69E191B4A36EB7DB1DC1216BB41524497B279230053E910EAB49859D1A99DB3C56CD70995BF031FB71B0AA40CDF031183E5B3D8D8799CFC3ACDDA54010AA6169A80D62479583111FEB337354F6BDF36A33279B7472800EB604710AB94A0D584942E41B78F2C387993A5EA079F3C74207F7972646CFD4E055FB6B10BC69B369D166143B8A8E7400134999F05C6A26DBBEC3B23D92CB90F77156A0C8A96CE93F73962C17F8104B52AFA2B68B62810F998577E07CB16FAC499F38AD628B57FC1C9CC7A568410478176BBC14C8C7A594A07A3568B503BFE26BAD88D2CAE92B90F1E1472E9697F930B6E775A1A9B93EF62757B2A28F6962CF2C1478626A9B65B5AB89CC24EE4C9F765B8071737C25F383DD57598D2289DEE439D28AAE91A3B21E389267FB3D146276B7810D126AA5D096A16F679E28D5C91A580777D9CEA13C46234653634ABD964C720A11A6514A7FB7F5ACB0992BFC7C16FA84CD9686ACDFF2283A89625BC65FEADC9171ECA359BA50BEE412D31414EE110AD1D8748237941683BF292618F740445C0B728D51347928559E569EBFE896DC6295F87517AB31A8639B50C0F3A08E96926816B7B17C8ACB476C4C67B1DF7631DF7540276C99053B102738BFA2876E42A028869C688C18AAA9B00DF0F3A74CD81D86D161912C09362AB35BD8731A48CB45CB92EE8A3374B811EEF93A37F17DD7D8AFF88958E13012E6F7BD6DC63E27BC47F506048CE24BB7411538EB4B969656DCE8DF5ACF28D1BDF5ECD14A44A98350CA45699F033EFA44D25E93FC2094C49E47269D7A3C68DB2C273EA465A5A30D6CE94BFF775EF4CB5F323C7EF064701B690A755A829F05597B2F2A90974F22FB1AEAB42892101222967E3A0AD612CEEBCA\"\n        },\n        {\n          \"tcId\": 59,\n          \"ek\": \"9ADB4E6D612D58585179C9965CF816553BCEAED61651C13CAE0672540BA9763333A25273B59864FB94AB650C6C41A6969EBBAAC0156A9A3621A1104EF578CF1185001316AF57F75C9CF401C4D2C9C8AA15BA8855C730577F128FA32122FB3B29E3813BA9C6CEA1B38BA069662C954794050C1F18C126542644D04EABB9656C446F8C08C6C6214AF2772264872E894086144B30712811A60C27E24C7C51A99A001D296B3CCB5CB1B9CBF4370208C23294442A9C229D1944B59C03CDD663E06CC73AC493D1D75CC1525993059EB4F61C6EE160AD229F3D015BCC919A90449FE8054890006E6BC98D19B602F142515371B8BAF9B456D04B3A429051006997A961422483B2D61857330F74D5117559213CD766E183210DF1100DA49DBD992A9FCAA1AC5834310AA9E9F44B4B2293BDDA76E22C585DB53CB904BD3292C84FDBC38D793136B12C2AF7A58D88C45B0212F0A083DB0929B1B070C74194EA0620A396BB43E6BA9AE43BAB85C589E93986C746EF4643DBDC1F0EF302CAB02512286F846702D4A3AB69FCA5D2B54C7B603CF5D4AE90705F0EA36A6479329B6461DF47026CA86765A79577F2AE1D8A2013E977FF49296247A3717401F7B961090165E34023E8E373CFC8CA5BE146A5690138532A01F9B18C138D9C7B110A66BC47161B708661FB5B5C507A9F3ED396A163A281B2809EDA52C78511C26CC8F478980DC429042A2CBE73600A8A06972260E8F2C387434FB0087435E711E14896B8841BC751A79FF9B3776217E53287270B26023ACD1DC95395F52546E94E287538D2E9539D8587DBE606748A7CDB76449EC2B0ABAC7170C439A4F2BD3F1A3F349B90DDCC13878024087AC33C38B64EE04427EA249D3C7845629CD8D0C82189814E6B2260CBC100B64039599B1BF91938C2B2C35CA2EBA70B8474522FA6C37A917D7B589F646266D2D07B348169F84968C466C9B90A7B00A6505F6A567D5A14BDD2477CFC0B515C7931B3BC1A7021DE7B2CC7C650097B0D2A94A7B024B207B6969E22C720683084CB998E0C422D1B2FD9969DA2BA9096358AE11A5E101C6DE23C8DC041AAA2F441F8693A01A872E6356D9907420EEC3F55B945E34562C0F3838695643DA00336C1CF1802199E093C07347F80F0959CFCA72CE65E2C675BF9CA8B01F97600AD177133CA15932C79C9781AD01B36F606DC74A648054AFE1B74352A4EEA2095A0120D7C5332D1B7A61DB284A3A0519AF63D891A2546D9748FE768604C27827B6E45E1AA1335AC8F2295A83A6AC9E42D0047AF55242EAB2534C7B38AB65749CD21B0441781E643BC16D03D0C437E03E33BCD4B92A9D32BAD6513FBF595DF30BE0475B69A632928A0B4E2A867E4229525953CF303C47CB83F609A1D20EA1D3146C80BF82061E9C5AD670E4288C01E889E95F937611B419C38563228BB9FA85D3E184109A004DEFA472848AC510CB81E18C5301B149B0264942CBB88F8995737C425E44F50F53B9B2A4C44FB0E5C7145775900D7708D51F21173A9170AB2902CB911BFB82D2D4B0333B1832F043A79A6A07530CA00E5CC58E6B0C2706CAE6A6A52D69C6C0B23C90812116AA971398D154404EC531F3712348D0AC62DFA9A1CC0B01C470E16693327AB9BBA8083F22B8314551712475E187400EB801F1BA34F78024A5DEC89B4C7811E68B30EBC91E08373BB92AC0FE8A861F4BF0A0B5877088371209D3294B866A86BB6B470344A069503AF99730773853236AA12CB3C2F679B45D0025876199C68E676298AB1773C92E33276A60C02FF7415EB10838A5035B7114E9834C5BCCA795330009EE57368B139F9D71135D1C86B0833D2CB81D660C56FCB3EDB848721C72A59A8CF45B6543CA7895E686C26A86A5AA7A8C1734C5F576D648482026391CD611389C44807EC771DEC4FE373B483B00D68F34AF776244EC07874A5C22C6C566CCC5C70369FB92A22B6BBC4F4144BDCE6C3A5051268DC8D6E8A2B8F8A8D48020804880FFA32BD01899CA9D3A70FD62A91B74ECEC51BB58594E7F162C7E94D44D64D66592880C55011A8550833A599375CF9207605784C11FABA854BBE6971065479B6D26589D2C756DB93C83E50936CC0C0089CCE8EE0A08149B732559586D07C4FAB56D7A263CAC656314268F55C64A1D6982E665DC42766D84206BAF103D14014221F2914A06162064F7E475811518022B301C262A125BB439D32\",\n          \"dk\": \"92D719984988C65BC4E3E60514E25F64C70318D049A14B110C2196176BB7F9183946C24894F075DE191261544ABE9BABD3280DB7A41B47A937C9597EE995CADC33C1AF87CA2D740F09249CCF376B7D5CBD1B69179323673BD6CD8CD9A84C47B7D232A912F4752BD94B93914427F2CD20C6391CF71F729472095C5D1C3C57E03689D57AB4ABE677909A9895638B790903E5E50D2421117F6A23E7A5CD6A3053FBAA42F386BBD3D82D8B788FF7E652D5830EE676BA978C60342694150A5B28D8A5AAA66C8172AD93D9B3B334C0BB4672DEC182E4395157A0997895BC44C48F3B222C5EABCBF7F2C2973C61B09B3889F0891E35934D4116663A60F751C48E22186B72969EC3BAC346B505CC6699C86BE8B1836152CD6BA37962A23E29B256C555845EF24F52C43DF0C20326841C77E3B87D16C59BEAC267056F40F7117E44C5C957465C09787A52594D1565825243FDE9C568115029F3C378B99F8E28AF395A2119590837B06B75E293682441CD7C5773F5BEFE7CCA172B8DD6EC0E792999D3B40A0439336F2629A1DB78227BB870F44896E112D8094524962F68370AD7092F60375C7A986340D32CC52633B1A714529425DF69BC918A30BBD577E938B6C1D06519078F3D934B7CF77FEB7A179BD8463829AA37D6C449877823231D0EE81B51FB12751946AA1C594ED175607B36D1B7B94CEB8D23B6AA680B9A72857BA61A445AE80C1CF45EF70B44AF6B3C428C8A25689AB8228218C6C0CC858E6A98823E3673913B9163A339327609FD459AFB05CADFCC2943E45A965B54F1A065F68B1F153CC8912329C0F0C0DC605FE6E52905C8B3F1EA3BD0E99418C645A8E6622DD577E9F2BCFB649C1DA09FB8E20DF470CC6317613D788FB544921C325B7B3B7A0A1C3ECAB62B866A513609590E871B17D7866F8A96B838C33A562DD1D47787D795BCE61AD3A9CFBEA5B03290320612A76BC198B0B84D8953909183A14FB318840469E2A909FCFA2C8A9B59E12C77B14A6503829AE8D3B493FB9A6BA6BABDFA7BE215B23ABB5B62C2C6042A909FA9999CFC2A23790548B6055D57B3FCE2C155A6C6115B05855249BEC0887F00369F38379111846946CB25F39305054BAF669E75830A505AC3148299E5AC2B4BF59C68986408270A8CA3A87A70A380EA7AD5871E7DD3165A733AA1D55F1184441DD84183B1002BCA518F370D02D5806498482B4236C8E1B1BF07870C2C7E1D44435077905A5C461238970B603F234B9351FA4213E0CB8DEC764475534AFA18AF07518C573DB0D9790C873A33293424A434B9A933AC644B37B14435D0AFE71B443F83BCEEEC9574016214DA8E66ECCA10668684EB5D776C5B4B2C05C2E03598A602717A72EC4B18E5F550992ABB76A1994B4842FADA39FDAB9DADAB09D6BB9B46AB4EEE69AEBC419F1587B8D7061BE188623F4471852C9294146261A67345F5A2347B206D2870DDA681C44CCFB420708AA398864469B8E08E76081C41F5977893813653A744766125F27C28E6C1DE51978448BB8770B8660B6E4767458DB70990E24FAA35B4DC7571B5EB69BE48C8646586F0ACB48A3B3252A02EF48A7DBD44A5E19177069C6F3F678794B8488C313F70EB4AD6FC09BF078467D362A436411AA689885B987104BC50E00340F8294A06AB8C02B0569B1A6426C13CB9B651677585BA213F7C297A949096B27A02E9510C803EBA002EA423AB3C810595D71F6201366591B60091B305EC7E80371E6DD171DB235D389B480F9C32CC7AA52DEA7714D3004DF5172E034DED271B9C679846A6AC4BE4580E4414EE42CAFED11387CBC98938BA6A59A07E2238B2B54FF6F18D3F48C2FB0636B5CAA3A581948067B1FDB083E92A46EED9B090032655A9BBA2B207EED10D419991AB813140CC898D11B24566970A601326072E15A31D73C0A625FC3E6B697575D293D1B3142F12190B28AD4DB6278CD9A92E50AEB3B60E687A14D2C48623955724582373F16DDE3919A8C7B98CBC514977C5E96895FDB17A952279B8222548EA4B08D4659517A7809C0E786986C2D69A1F7963CE2672726C7CA4A91509856476C13B9488A2A429804815126954C2CAA43F1EA7877BB10346C022A1407CDEEA6322D74C760283CA8699B6433AD16A0DC2238B9714887ACB0315D67139573EAB39A403C3459ADB4E6D612D58585179C9965CF816553BCEAED61651C13CAE0672540BA9763333A25273B59864FB94AB650C6C41A6969EBBAAC0156A9A3621A1104EF578CF1185001316AF57F75C9CF401C4D2C9C8AA15BA8855C730577F128FA32122FB3B29E3813BA9C6CEA1B38BA069662C954794050C1F18C126542644D04EABB9656C446F8C08C6C6214AF2772264872E894086144B30712811A60C27E24C7C51A99A001D296B3CCB5CB1B9CBF4370208C23294442A9C229D1944B59C03CDD663E06CC73AC493D1D75CC1525993059EB4F61C6EE160AD229F3D015BCC919A90449FE8054890006E6BC98D19B602F142515371B8BAF9B456D04B3A429051006997A961422483B2D61857330F74D5117559213CD766E183210DF1100DA49DBD992A9FCAA1AC5834310AA9E9F44B4B2293BDDA76E22C585DB53CB904BD3292C84FDBC38D793136B12C2AF7A58D88C45B0212F0A083DB0929B1B070C74194EA0620A396BB43E6BA9AE43BAB85C589E93986C746EF4643DBDC1F0EF302CAB02512286F846702D4A3AB69FCA5D2B54C7B603CF5D4AE90705F0EA36A6479329B6461DF47026CA86765A79577F2AE1D8A2013E977FF49296247A3717401F7B961090165E34023E8E373CFC8CA5BE146A5690138532A01F9B18C138D9C7B110A66BC47161B708661FB5B5C507A9F3ED396A163A281B2809EDA52C78511C26CC8F478980DC429042A2CBE73600A8A06972260E8F2C387434FB0087435E711E14896B8841BC751A79FF9B3776217E53287270B26023ACD1DC95395F52546E94E287538D2E9539D8587DBE606748A7CDB76449EC2B0ABAC7170C439A4F2BD3F1A3F349B90DDCC13878024087AC33C38B64EE04427EA249D3C7845629CD8D0C82189814E6B2260CBC100B64039599B1BF91938C2B2C35CA2EBA70B8474522FA6C37A917D7B589F646266D2D07B348169F84968C466C9B90A7B00A6505F6A567D5A14BDD2477CFC0B515C7931B3BC1A7021DE7B2CC7C650097B0D2A94A7B024B207B6969E22C720683084CB998E0C422D1B2FD9969DA2BA9096358AE11A5E101C6DE23C8DC041AAA2F441F8693A01A872E6356D9907420EEC3F55B945E34562C0F3838695643DA00336C1CF1802199E093C07347F80F0959CFCA72CE65E2C675BF9CA8B01F97600AD177133CA15932C79C9781AD01B36F606DC74A648054AFE1B74352A4EEA2095A0120D7C5332D1B7A61DB284A3A0519AF63D891A2546D9748FE768604C27827B6E45E1AA1335AC8F2295A83A6AC9E42D0047AF55242EAB2534C7B38AB65749CD21B0441781E643BC16D03D0C437E03E33BCD4B92A9D32BAD6513FBF595DF30BE0475B69A632928A0B4E2A867E4229525953CF303C47CB83F609A1D20EA1D3146C80BF82061E9C5AD670E4288C01E889E95F937611B419C38563228BB9FA85D3E184109A004DEFA472848AC510CB81E18C5301B149B0264942CBB88F8995737C425E44F50F53B9B2A4C44FB0E5C7145775900D7708D51F21173A9170AB2902CB911BFB82D2D4B0333B1832F043A79A6A07530CA00E5CC58E6B0C2706CAE6A6A52D69C6C0B23C90812116AA971398D154404EC531F3712348D0AC62DFA9A1CC0B01C470E16693327AB9BBA8083F22B8314551712475E187400EB801F1BA34F78024A5DEC89B4C7811E68B30EBC91E08373BB92AC0FE8A861F4BF0A0B5877088371209D3294B866A86BB6B470344A069503AF99730773853236AA12CB3C2F679B45D0025876199C68E676298AB1773C92E33276A60C02FF7415EB10838A5035B7114E9834C5BCCA795330009EE57368B139F9D71135D1C86B0833D2CB81D660C56FCB3EDB848721C72A59A8CF45B6543CA7895E686C26A86A5AA7A8C1734C5F576D648482026391CD611389C44807EC771DEC4FE373B483B00D68F34AF776244EC07874A5C22C6C566CCC5C70369FB92A22B6BBC4F4144BDCE6C3A5051268DC8D6E8A2B8F8A8D48020804880FFA32BD01899CA9D3A70FD62A91B74ECEC51BB58594E7F162C7E94D44D64D66592880C55011A8550833A599375CF9207605784C11FABA854BBE6971065479B6D26589D2C756DB93C83E50936CC0C0089CCE8EE0A08149B732559586D07C4FAB56D7A263CAC656314268F55C64A1D6982E665DC42766D84206BAF103D14014221F2914A06162064F7E475811518022B301C262A125BB439D3225F6DF8F68FACBDCE4839DCEEDC2B96D6191CA1DB11F347EA0D66F8C2458A848681F088AD6962FC397A1B9071852848CE9A7EDAE65A81485CEC87D0974707B7E\"\n        },\n        {\n          \"tcId\": 60,\n          \"ek\": \"F93584C1B0237F83AE90B664950B471E763CB7939C28D93604C83F73A30F80B3AA8F948086A142A132442712CBD71A9D495587CAC62F98F922E96C9D0095B1502B42DA76B59B1C183338C1C06A730DFC2E889273326593362006EF948AEE1A521E159FF6283DF3D11DEDB1655FC706DEB2C6BE879D3605C3AA463355FC726CE84962500B72776EBC9AA3D960A8084C3182EB8A43918F80EBCE5425257C2237CAC811022792485772BFA51CC7171F86F4C847782BFF123763FA650CB1B173D1AC78F7C19E764F5039736A3B2BF37B67C9783DA9EA6CD0A96FD7DA758E411575F6744DACB42AE2148B7BC9FBF49B4D25CCE02BB906B025EAB98171B80D55C8A328D15C2668A8FDC3A463CA6508E1818437498AA27B04D8514795BC4675C1B94ACEFD54603C498056038D3445745D44BD6F1A4316191B905C650668A3E2402D74E560CBB7A667A27EA4D38495010A71085957421923E21718E611CD5205FFF6AB91C65407C67E7EE19EF0A885EB88B02901478DAB0456D94E4073576AF0542E33C9286048E491ACD2EC2A02E5181436BE7D829AFCA68FE0A78588D886DF77B6F1F94A959B2AFDE328B8B5888569AC34D23771F0AA12C7954EC82A6E71C1760654C34A926DE4B0F07025B9517B2A036577728C7E35653CEC816C3677F703B0EB3C18AEC948368009CB013E365A1E643244289B0AE11440877266C27C955C7773D4377BA2D7C6B8DA7D23C48A19EAA7374A5A9B931F42E144A6CBB8CA06BBD42396B02C6AD16BA6B1E36B2161903EA4A654A31C507825347B9BB177C655B848BD943162F41818F113E864B456D504573B8C4E5B8A07F14713612F78537128253B1B9712C417CCAFB271DCD9683EB82C55D171723A32A5D40EDF9252D01C7ADB6664D6467421920C5B345B2D93C3DFCBCB1DC538D23454068AAA2B85CE41574079136476CB1CA401C1134977E97C72D2C85C37E872CB725A2AB55BB6C37EF2E517608A95C14A970480856A8A5737A06B46E93D03D6384AB8562AF2C37DBB1D52737CA8C5404632C4E9AA714A4A5CD3D073AF069CCDA490C64C42974B57D3BA6C500588C39BCFC4B26CE6119ECDB6A2A47141DA96BA2331996EA93C5D7618F4FC9A27FB6F5BA336F6801938EC26ADE456FDE01D41C22E1424611FB4487D8A2DA212A09749524F004CDC76B8F6339B11041A3174B8FD58B2C6E7B0166AB4246347D25AC489C43288A23A7E59C3CE320E3FCACE6B6277400ACF9E924FD12C0805B70354F3AD8DE92644F8B4936C81CF301FB87A7DC4578628868ABF501626B045D48537F2212A3FA5396C1C857B1084B21B1EB968AD60B43181C77712001290300C9CC2A3D6562A00FCB4E5F11C2AE412DD29181A034725A0CC75E2BDFB851DBFC28AB3362B8CF378BAC9939D340317C64469C27D8F3675C2A962DC7A00E9F67BA1EB8094F0B205388C59223C7BB963D8E21D933B779B360706AC5E06033C1907A8C8EB6E6017C285A8827E28B8CC7A5891CA381FD50D5EA231FEB5A7A4E4380FB5067A6505FA627D19715667FC5602B041CDF9074A3C58BDB9C77CF23F6AD1005C4A6F2581695F444578A734F8FC3E9FCCC07A873A5AD04CB2C724F6820AB1C081306B429F92834FE8BFE401448431BEBC4C7335EA0EC7050A0A49459DB286B774069B161DCBF3BABB5430FF727A419B1AF2371E1441BDD53C03E5445391A401668B693C6503F35CA6CC7A04CA298FFB81CFEAB4B8DF400E1F670801E7014DFCC8B3D8BD953BACC1D61F184A07B383A8325C1251A56E8BFCA75C04CF8653082D52A8D6C944191155AE886AD9AC010AC50BBD2BBA9144785F02B7B7BBBF7F1AB8DF210241F4AEA1700D840AAD5E2761C4066165A43C487B3AFF68BF3370891933BF46C13BE82331F189B678150748AC609B3378C3581AFF91C9A4F2B9C040661E11A917F7A604100E5B9863B8D3B55BE32CC1F40DC47C4F02622863A560F25B03864502F4304F1401B90E4969EC110331A418770B1CA626B3A86659AE9B7B630C6DE1576286976453377663950353BC8067691BD1E1C24C006204E3502C171BD43C33271548EDDAA641677D8D1161152216A4944D480440E3B418042184194C6E39FBCCD353C58A6C3BAD16A33D666D601237F341B7AEF3C98F237E585C8E152B444DF15A768B20104AFC8CE55CDA630D36C048C7D5206708B4699726C1E2FC3A722CB514\",\n          \"dk\": \"AAE64B6CDBAC89352E7D1311EA15C4C43635D40B98F471A9BB1C1A0B086868D2651546A3F6C1A1F6FB8E75010B8661BE3EF1251737126DBB8157B6A1AD3611B04B95DFE46510BB94E7B0B87B55315A98AB61946FE926984FB5A4BDA10333221B92EAC1CFC2A15BE83ADF9297FF61CF87991EEF977A584A3D4CC765D710CF3C630687990A85F4CD1973861868A8FA1A1D3C26A777371CF6108D1B83243B058F729448ECA2A5665B2B6B032BD83CAD34EB5CA394C6DE96AB3552BEF3B28DB6390978DB65A30C6B0528ADA091C80D42AE470078C1D3C45EA21AB418A865FCC2FA4C3D78B76E84B3C3AFC0BD9B2721A092AA274B7A4BC3B8A9DC57332C81B0695522316755A5B7F1F992F5E22C00B29CDC6281ECAA0E74A24C0C56573AB8453F469872722C8796BFD1B64D8AC93F123C74740511A0B983C20C153F005BCBD04965EC95975858045681DB4B8B70327CCDFC9FF8377D599384C15875EE346FC08066EF3BC09D46016B8754B8C0B6BD944D2B157CFE3392A25B98D48850536B5BFE119F58A0739A48654CC8BBA8EC1C1AF4A197EC758396CE4C0C4E66D0BD43A03CD65A6028C185C6C1C59FCBC4EE7017568061BE551BC0BB3509102E8F14829B603BA1CB910DEC374CEA99C301851111A3DACB2292CB95ED1C8B53B965C6D33307B40C538C63872B83DF8C3922385AA8CA075FC118DBB323035C768149221E2C135291070671270CA5342B89044DB59346314F95B95BCB06D0A1F66645B907862C60B4C8CEC097CA87C7B3F2720DDCB0C5D317B443437CEDD32F2357015F9350778A360219B7FC178531B2442F114F2024595EC4CAB1F6CA26F827542096F8B08C4492156424504CF46DFCAB2DB7074FBA212F72284194C59240EACDE348BFC22394985A09874B5BDCF788DFB9B38031B1AB762BC337195597A662673514603E49C2A485E61D37691B31C46EF67CA30C79A6037726735861BF5A31FE102B448CAF32B7103D3549AAE16BB8186670FCBEFEC61D47E923E4460B8CDB029AF81C99F80456033C1B1351F7FC5932902E791C8D65466A36523BA6D58FD370905F3B21465B88C125B23BEBA9F3DA624685833B4A189CE2480D4739AAD85E3EF41E0683B0376C561AA6685F2289891114CA4C380F43289C201CC6FB6E77213E1FB813F1A12266E9812BE91EED08ABDAD5477801201F313CE6A11B9C155F8E07310D6C7769E637EB123524342DFBC67FC0E20A3D09AB9B5C06B149C39FA12E21870273C8C97FD56A1CD346F627593A01606043A18F35C6A323A22A48B3DB49923D3A2BBC92051BD4A9301CAA0D9A386569A37261CC12172839499F928A6A70806B9D8A24AE304D98876AFE6C3E24D500BA08A8F285C9633414825AA01AFC5E55CB2AB91A53DCB47DC7904F6EA77196A8BB9E057A092208BD81C2B9675E1C263D0BA024D2D92B5E85C164E149572B0288531B6BF4BB1E629D7295842A5709D06810CCC896E64A5516130959881B58633F8C411F5032AADE7879CA3385715C8E590B6AAED47D38A708822AA7570848439867D6CAC7EF7A76DD3B890662CE65271EA065302D3A4B4FAA2143F5530E890A1B80B75935BC3C06569C7365A4166AB4D7C1B26030C2A777A121C0099921C77AAE3644BBDA315429C06CADB33BAA3CA419D5064A5BC9C7BC6C8D34C7FB0A254409372B679FCAB46D47C16B94BB2E8253475C0CC5BA6C7DA39141ECE51EDB844A4EA4AC6E3866625679FFF71CFEE1477A918CD82B0883B67BAF2A383B436FDEE7C58AD372E698778866377561BAFA8C9B596AB48676B08E2BA9EE1AAE07BB194D841586662DC64B1065CC8A52D899D09C6F96286E272895F8865B54DA8B33FB67AEA1133E611BF9ABB459AA4442A60CD09A4E39006616C3981604662F7006E0C9130739A66E79BFD123C679A368B64C331C285AC1CC721512A52148A382C09474773BADCA77CDDCB6E4C40022CA10E35A6BB205239074AD6B6524C2329103D281E8CC479AB4AA2E5C2960A903C1E05B48F07B5C88C384533D93A9BF07EB8B7376163532AFAF32B4C481694C11C97ECA8AF0E714F0427D16CBC331E9821E60C735392AE047303DD637AE5AAD915232EBF481BD03ABC845372C60AC586009BC717B6D1A1E78FBB7ADAC5DFA89C410E786AB4422A87BA21A088EF93584C1B0237F83AE90B664950B471E763CB7939C28D93604C83F73A30F80B3AA8F948086A142A132442712CBD71A9D495587CAC62F98F922E96C9D0095B1502B42DA76B59B1C183338C1C06A730DFC2E889273326593362006EF948AEE1A521E159FF6283DF3D11DEDB1655FC706DEB2C6BE879D3605C3AA463355FC726CE84962500B72776EBC9AA3D960A8084C3182EB8A43918F80EBCE5425257C2237CAC811022792485772BFA51CC7171F86F4C847782BFF123763FA650CB1B173D1AC78F7C19E764F5039736A3B2BF37B67C9783DA9EA6CD0A96FD7DA758E411575F6744DACB42AE2148B7BC9FBF49B4D25CCE02BB906B025EAB98171B80D55C8A328D15C2668A8FDC3A463CA6508E1818437498AA27B04D8514795BC4675C1B94ACEFD54603C498056038D3445745D44BD6F1A4316191B905C650668A3E2402D74E560CBB7A667A27EA4D38495010A71085957421923E21718E611CD5205FFF6AB91C65407C67E7EE19EF0A885EB88B02901478DAB0456D94E4073576AF0542E33C9286048E491ACD2EC2A02E5181436BE7D829AFCA68FE0A78588D886DF77B6F1F94A959B2AFDE328B8B5888569AC34D23771F0AA12C7954EC82A6E71C1760654C34A926DE4B0F07025B9517B2A036577728C7E35653CEC816C3677F703B0EB3C18AEC948368009CB013E365A1E643244289B0AE11440877266C27C955C7773D4377BA2D7C6B8DA7D23C48A19EAA7374A5A9B931F42E144A6CBB8CA06BBD42396B02C6AD16BA6B1E36B2161903EA4A654A31C507825347B9BB177C655B848BD943162F41818F113E864B456D504573B8C4E5B8A07F14713612F78537128253B1B9712C417CCAFB271DCD9683EB82C55D171723A32A5D40EDF9252D01C7ADB6664D6467421920C5B345B2D93C3DFCBCB1DC538D23454068AAA2B85CE41574079136476CB1CA401C1134977E97C72D2C85C37E872CB725A2AB55BB6C37EF2E517608A95C14A970480856A8A5737A06B46E93D03D6384AB8562AF2C37DBB1D52737CA8C5404632C4E9AA714A4A5CD3D073AF069CCDA490C64C42974B57D3BA6C500588C39BCFC4B26CE6119ECDB6A2A47141DA96BA2331996EA93C5D7618F4FC9A27FB6F5BA336F6801938EC26ADE456FDE01D41C22E1424611FB4487D8A2DA212A09749524F004CDC76B8F6339B11041A3174B8FD58B2C6E7B0166AB4246347D25AC489C43288A23A7E59C3CE320E3FCACE6B6277400ACF9E924FD12C0805B70354F3AD8DE92644F8B4936C81CF301FB87A7DC4578628868ABF501626B045D48537F2212A3FA5396C1C857B1084B21B1EB968AD60B43181C77712001290300C9CC2A3D6562A00FCB4E5F11C2AE412DD29181A034725A0CC75E2BDFB851DBFC28AB3362B8CF378BAC9939D340317C64469C27D8F3675C2A962DC7A00E9F67BA1EB8094F0B205388C59223C7BB963D8E21D933B779B360706AC5E06033C1907A8C8EB6E6017C285A8827E28B8CC7A5891CA381FD50D5EA231FEB5A7A4E4380FB5067A6505FA627D19715667FC5602B041CDF9074A3C58BDB9C77CF23F6AD1005C4A6F2581695F444578A734F8FC3E9FCCC07A873A5AD04CB2C724F6820AB1C081306B429F92834FE8BFE401448431BEBC4C7335EA0EC7050A0A49459DB286B774069B161DCBF3BABB5430FF727A419B1AF2371E1441BDD53C03E5445391A401668B693C6503F35CA6CC7A04CA298FFB81CFEAB4B8DF400E1F670801E7014DFCC8B3D8BD953BACC1D61F184A07B383A8325C1251A56E8BFCA75C04CF8653082D52A8D6C944191155AE886AD9AC010AC50BBD2BBA9144785F02B7B7BBBF7F1AB8DF210241F4AEA1700D840AAD5E2761C4066165A43C487B3AFF68BF3370891933BF46C13BE82331F189B678150748AC609B3378C3581AFF91C9A4F2B9C040661E11A917F7A604100E5B9863B8D3B55BE32CC1F40DC47C4F02622863A560F25B03864502F4304F1401B90E4969EC110331A418770B1CA626B3A86659AE9B7B630C6DE1576286976453377663950353BC8067691BD1E1C24C006204E3502C171BD43C33271548EDDAA641677D8D1161152216A4944D480440E3B418042184194C6E39FBCCD353C58A6C3BAD16A33D666D601237F341B7AEF3C98F237E585C8E152B444DF15A768B20104AFC8CE55CDA630D36C048C7D5206708B4699726C1E2FC3A722CB514936B2729D96EFF6FBF9B05E34251304A92EA873A21654F70C4632113C36F62CF40BBB2C581B2D694E369C0DA567371E8E53C328A59BCE775A625C9F5CC185E0F\"\n        },\n        {\n          \"tcId\": 61,\n          \"ek\": \"E42705D23A30A72638425280BCE72CD596BA2AA6B736A922740C52A0F470C0A1311ED82E15F4C5D865B6BE18B4810127282C1BE22A9BC580784EF371F18482F2E43777396A3E6A77FA284CBC6C663D63005DA53232B06A27D65547FA58E263C00AC701B3B6566F199F5EC172AD4BC12FEBA212C7159C90C589551110066D87E01D54924ADFE0CBEC921AC7B273B1FC400F6B81F23073D6AC0D7AC34F2DC60C08777C5559B013F23B91A743443B1B38375E91C254D8721F1AD2B02E9C566FAA294ED75BE16C0B0D82C1DCC8AB4A6C1D6E88B567C11A49AA055EEA87117A4C5B1C030F8ACD3AFC69D5FA8F082210E356A426C973A17492E8C3C64D87087A54589BF468071C0DAE016B6B51C52B628BEB986845B484FA213A27C6A1D505AE09D499FB92C17DF2AF010C71C1FA0FCD89A59EBCBDA4B7AFAB2CA68421AED1C464892C5ACFAA556DCC227929B6670904A01C5AB2AA0A5A246566C61A7BB9CA07630DD005CF8705B1B414CE8070A43D30CCB45572DEEB253E19ABCDE790A7F4BADD7863CB187FE8AAB9B0CC1C8309415F54AF4BA6BD6DC74C3AA13B0126254DA08F833C71810092E7332DD8CA4CC3199D4EB25405D310A3E9A6E65C8BFED0C6973A6F324CCC3EE16CC3E405D1C036086C5BE644A1A3709DB22A392EAC889CFCB6D9A03CCED44AB241AE735490EBE44A8B333C462891AFF72F2482C9B52B98106BB010A4CF03214330C5AEADB9B7314273D88880358A568DAA16DD8B496B6CA9C758B74F207B5001826391175EAA2DD9912E84B77EA257052065364949CE57365CC457854681729B389A7CC4782F7590EA6ABC49C40B4E23A2EF916F53240CA7BCA58370657490C52AC535E7CBA1CC58CAF52585DC148811264C2E6AA6728A254BBCC983D71716E82B7A53A2ABE376A5362E74F57BEB1979E2FA77998C869D6A799D0934D8475C8CB28AEBB3522387251D11352520C9A76697519B2DB6093CCC3A5A99D75E136C9A3D305F46F3176EC07FE4C30E25B21BDC2996DB413E0350AEE4A96125C5337AA70469F392942037674561D85969AC6664F6D92DD9B5108CDAC8A6E86E8BC1C1DF319AF330701E23AC743232680B3370B471CCD91761C110E309B5D77A48F4E06A681A2B38ACC3185946E7DB0D6EB1837D3199D83C676D667AD1CB51DC9982D3D36BCC121627D21AADF01928F32DF347CE495175F84641CC0704A24B7D099C416D3A56CC5CCD3ACA34F3F670B64C57AFD0591A628E4A19A83914C14212BB4D43AB1548CC772C25F38619692A5DD88730B093AFF5968B9AE60DC8AB8BE3D13F16928E4CD0CF604835F23B101BE7903876C8BB9970BC74A0E4752A67C4BE2F20C10BFB7765D863828565D190C5384273DED40E331168A4AC2A6CB8B0C5D600F5359BE70AB0B74C3C427857B34A9D2B7731DA6ABA7206253A67C22AAA10F7ACBFDB093BB7647B50D6AD449257AAA9CF29E5C5AA2982D6E0B67813BEF12B4E2A9669FB94394E34BD9583142456CA048763E0EABA7DDC48E3CB80736C6121F1888DB3CF85177942F322C3F029B427A6B8DB4976E1B80E9047BF91BDF2449F0B20C336E041B587182DD4A459C4800CA85308658E3BA78D66F47639AB1B70C45B43F70CF8197803BC1EC4A4424DD78C26039F4AC3C84F98A5AFB45A6970158E41296B925D33F68ECCD3B5A0B415F5B7CDE3473859DC48279255F9995561C205B244728DFC4F625316776267BD468BFBB287097911F8C2A32A0CBF14C3C708F9C4BF70C612C61FBA3720EC62918149165BBCA3CCD48CC8B272E538A1E818843B272DC694AD8E265E830034ACE5708CF22AE3D256745B50EC328CBF03760CA73E7E35241776864856BFD7C18577D411AAF99AD412CAACF44456228DE0176FA0E13CB925140F8A5BE9589F4B6B199F751B7A58751DB35680C78C69F83333155D80E08FA92A5B0FDAB909F9223D167333765D033213E3A65EB2B709BA200D861012D7145A9039C4A6B032D9D5802846441C379978EBA4B9970B275271292379B96717E48680FAE36DDC7018AFA8B3C1929AD62268F12AB87FEB92535A655158CE4957A1BE37C9922B4E08D57D85525D6F2B7698331FB3F1219D91A1272B7E92D3245E4649A8257BC362BA0E0462991A481A0A602809C46F4342ED40AD99FC74366406ACFCE7708AE11AA2C3436EE06121FE6CD52FACE80AA5FA2B65A0B1B28F8B28DE\",\n          \"dk\": \"8F841DB58339CB208908CA123BFA4FB00413A60A9EA76648FD69BEECE0B731042AB206368858B0AE798A7A26A1C96AA302AB998F352634CBAF93F81EA4C280830B8DE435CD4DFC3C3674CE2338AAF6543A57BAAE1232780B578BC3FB4230919CA56065A6E466FC19B9322404499199ACCCADB0236D6026BC927C58EA225D34A628721C746D2008C7B2A600CB94E5F93351088A1C6609CC350E5E0920D4D3139AC08A28A0A5DCFAACF057C3DCD933DB7A8159A657CE257481AA1AEA69BDCCBA961B82C56AC15A74DA139BF388610879EA716035372042984AE027C910E149FACC954762B75FE41214A7B808B168FA728F4D52500C88B2E96A121AC9249C3761A97C7333332CFFA182018A15D4B550A930BFE60AB73F20573345522162A5DF021C19F290567A95ACF4622A919AC20BB42B11BE06852094947897B4A7987981F989A29A13C31288B8C30B36244894CE502DA3967DC0636C94810A2A0B9EF72B215E936E330556F0309604B99093B246AB102553117CA48458E845CB87399A42499A0F7913A15358F334577D84004CF23522695EBC6630503B7ED77076EA7C0ABFA4218943413AD664F761C429011F8F0A66E5C22794F60E9FE2B995C004FE21AEF73AB339468735731A969A3E0E192998933F2864C6BC9A82B1A5A542CCCFB85CC629EAC5BF2CBF1F1438ED5109BE13B7F182280EFC07D937783FC5407215742329281AB7B8BA687DF328A3380CA3E4C09523B056EED254A4B7AE6C241EEE61A340528308D3C11F96A315D0B4B02BA47AC14388F2C005A0B15C75CC3D72717B2B13AE8A830AE7706064630A1C0B97858B75B228E7E6C12F1344F656A2D0405E3E932BDE3846F91AB60E183F2469C2A0B47E59E20B89303DFCF89EC1368243B03A4864C26D58B691026B2E627E9C36BAC877C048708B058B02B5327B3325423EEA5C52E311A6E208880A688D540127DBA7FF46CBBC35784BF538B5B04CE774BF5FF82BAFC844445719CE532D928339A19744E9F6391852B5B1E2C06F61CC2C28A16D3BB08081825F040CF0AA489751CE8C99CC0930379A426D3E9A15278B7D40021E5F890B06A767E65088311A8818B5088946CE9871A55D602B8FF389FD9B2E80060E053B82275436FC9690D1453B256CC510735CCE3331D109C1247B6875DBA71079A353293B624C6494762BE23B135DB14599F41CF3BA8EE07CA032E652E00B57A6214106C587B96097B45390A4431ED2B524C641112A95C65F93CCE87816E85B7955A91741650C4E8A944FA088CBF274E31244F77A4C50A98D44B96FB8625FFACCA584AB18740C8278A8ACA2E12989E60CE7A70145B394B7137EA86901D629C63ECB42BC59673DD73515DA3BE2C74D595B6E15AB4324786574D842A2518F2247ACCAE3012AE36CCBC48038BB5C69B3BF95D59016492E2E923CA562C8F8F444CDD3756A69336EE972786B88F2DB42DBB2309228279D6AA29474C2C94721556131DFD95E0520B0C23C2C1B49683D425EB313954154935CC06109182BE7C8A5D45A46B97196B6B794F4944EBE2052B3454EDC814938868FA1FC7ED4D76342591BA5EB96C73166AD311F7C80CFE7C149A2301DF1B8BB96C7742F03B5B3541A8E05418A7A3A89E1841B627D85CA7B6D56897DF5C5977A1E67C2B5D97894245620DCDA7B3183771081B4FD77476440013E417FBCFC4C38A51C1045219FEABD3C1258203A716FDC922F95CB71C15BAFCA2C777812C1AC390D540803B917D153B75D7A947AF328B620B93348CE7342212A286FB63812C2930920933473C95BB383B7AC054E4DF8CAF7D2A2C7C8CB3FDC5CA949177B40AF5DE102D9DB382D3C6D918155FAC94EE088A067C43BA21778D4E78FA0157C8457586E1AB6A07922FCE01407D63E7366BF68116EECEC8F01C262F766008C9A33C2A5A7C4575E95DB458E9C8289A30EA4E32E015856FCFC03C0EABD2021473B251C279ABC66D87EA1F66AEF5A8189544DE7A95C6F8544D8481786C72BF0EA329EEC4D2423846770707D922DDA38601B228E12451657725923B25CE29A10398B5D16492061D77E48EA023A929A4EC3634FA0073CB929CD1A42577B1E6F89CB9522A28B6A9248D243D423884E03799E6413F963462B7B3BF0276BAFE5AFE2F161EA76566FF4406649BA3C1B5811188FE42705D23A30A72638425280BCE72CD596BA2AA6B736A922740C52A0F470C0A1311ED82E15F4C5D865B6BE18B4810127282C1BE22A9BC580784EF371F18482F2E43777396A3E6A77FA284CBC6C663D63005DA53232B06A27D65547FA58E263C00AC701B3B6566F199F5EC172AD4BC12FEBA212C7159C90C589551110066D87E01D54924ADFE0CBEC921AC7B273B1FC400F6B81F23073D6AC0D7AC34F2DC60C08777C5559B013F23B91A743443B1B38375E91C254D8721F1AD2B02E9C566FAA294ED75BE16C0B0D82C1DCC8AB4A6C1D6E88B567C11A49AA055EEA87117A4C5B1C030F8ACD3AFC69D5FA8F082210E356A426C973A17492E8C3C64D87087A54589BF468071C0DAE016B6B51C52B628BEB986845B484FA213A27C6A1D505AE09D499FB92C17DF2AF010C71C1FA0FCD89A59EBCBDA4B7AFAB2CA68421AED1C464892C5ACFAA556DCC227929B6670904A01C5AB2AA0A5A246566C61A7BB9CA07630DD005CF8705B1B414CE8070A43D30CCB45572DEEB253E19ABCDE790A7F4BADD7863CB187FE8AAB9B0CC1C8309415F54AF4BA6BD6DC74C3AA13B0126254DA08F833C71810092E7332DD8CA4CC3199D4EB25405D310A3E9A6E65C8BFED0C6973A6F324CCC3EE16CC3E405D1C036086C5BE644A1A3709DB22A392EAC889CFCB6D9A03CCED44AB241AE735490EBE44A8B333C462891AFF72F2482C9B52B98106BB010A4CF03214330C5AEADB9B7314273D88880358A568DAA16DD8B496B6CA9C758B74F207B5001826391175EAA2DD9912E84B77EA257052065364949CE57365CC457854681729B389A7CC4782F7590EA6ABC49C40B4E23A2EF916F53240CA7BCA58370657490C52AC535E7CBA1CC58CAF52585DC148811264C2E6AA6728A254BBCC983D71716E82B7A53A2ABE376A5362E74F57BEB1979E2FA77998C869D6A799D0934D8475C8CB28AEBB3522387251D11352520C9A76697519B2DB6093CCC3A5A99D75E136C9A3D305F46F3176EC07FE4C30E25B21BDC2996DB413E0350AEE4A96125C5337AA70469F392942037674561D85969AC6664F6D92DD9B5108CDAC8A6E86E8BC1C1DF319AF330701E23AC743232680B3370B471CCD91761C110E309B5D77A48F4E06A681A2B38ACC3185946E7DB0D6EB1837D3199D83C676D667AD1CB51DC9982D3D36BCC121627D21AADF01928F32DF347CE495175F84641CC0704A24B7D099C416D3A56CC5CCD3ACA34F3F670B64C57AFD0591A628E4A19A83914C14212BB4D43AB1548CC772C25F38619692A5DD88730B093AFF5968B9AE60DC8AB8BE3D13F16928E4CD0CF604835F23B101BE7903876C8BB9970BC74A0E4752A67C4BE2F20C10BFB7765D863828565D190C5384273DED40E331168A4AC2A6CB8B0C5D600F5359BE70AB0B74C3C427857B34A9D2B7731DA6ABA7206253A67C22AAA10F7ACBFDB093BB7647B50D6AD449257AAA9CF29E5C5AA2982D6E0B67813BEF12B4E2A9669FB94394E34BD9583142456CA048763E0EABA7DDC48E3CB80736C6121F1888DB3CF85177942F322C3F029B427A6B8DB4976E1B80E9047BF91BDF2449F0B20C336E041B587182DD4A459C4800CA85308658E3BA78D66F47639AB1B70C45B43F70CF8197803BC1EC4A4424DD78C26039F4AC3C84F98A5AFB45A6970158E41296B925D33F68ECCD3B5A0B415F5B7CDE3473859DC48279255F9995561C205B244728DFC4F625316776267BD468BFBB287097911F8C2A32A0CBF14C3C708F9C4BF70C612C61FBA3720EC62918149165BBCA3CCD48CC8B272E538A1E818843B272DC694AD8E265E830034ACE5708CF22AE3D256745B50EC328CBF03760CA73E7E35241776864856BFD7C18577D411AAF99AD412CAACF44456228DE0176FA0E13CB925140F8A5BE9589F4B6B199F751B7A58751DB35680C78C69F83333155D80E08FA92A5B0FDAB909F9223D167333765D033213E3A65EB2B709BA200D861012D7145A9039C4A6B032D9D5802846441C379978EBA4B9970B275271292379B96717E48680FAE36DDC7018AFA8B3C1929AD62268F12AB87FEB92535A655158CE4957A1BE37C9922B4E08D57D85525D6F2B7698331FB3F1219D91A1272B7E92D3245E4649A8257BC362BA0E0462991A481A0A602809C46F4342ED40AD99FC74366406ACFCE7708AE11AA2C3436EE06121FE6CD52FACE80AA5FA2B65A0B1B28F8B28DE9213ED7BAA4999FD5812E87439CD569F1510F0536CB5A34D77C48FCD82BE86D8E15F322315265F9B847960B7185D962761ED79C62286A0DFDB13DBF550CE0107\"\n        },\n        {\n          \"tcId\": 62,\n          \"ek\": \"0902C611CC1D7395142836692CAC578AA1AC7B12970599690FF9958A7048B7D276DE2088D0B06E5C3971190909C6F216287B2344E3922AF9A4BFC1CEB7E5AD35302E555A504B6B65F710356CC30DF957BD60E974FD31729EB63620E30154636FEBD84DA66BBE270151787B7A8B2BA3431491860C8837B5A93C35C62AE2BDBA43BB58D4126EE8139FFB5BFA110C4075C34F9AC648673EECCB59AF60CB8A6372B69C6E39D418AF88BC23161812486A5F2BB9B2D970B811C360329593CC31FA47BEC0FCA95C226E46A6BD5F39674458BD1683CA4673C39D158D9EE13E9736A6863229156BAE9340532D1A1BFBF98145930EE0A5229323468F7B5A9AE9755F6B603A4360CAEC054276BD3E5A4BF29C85F1DACA2C61A514C1449F94A63C4A3CCCA06D195945EDB3C0DAB07FBF75BFDE57A454C564B99C7DA3E5BA095A462C741E634B3D2DD9B7DF837E5FC076A0C363389C8248036EE55477F55BA27F17740E0AB891D0305E77A284ECA7F84CB3C5483FB3992610384E42A731DB8C3B65B6BBAFF04039D09A9BE4494637A9599CA7A4C42FCCA473D4E88B830A4D2358A324620F9B765A0141CB57DC1B70918053611B7AAA7D6D973C8D4C2CC135940C3CA9B7385823C8A86755717A63BD8EDC5712335786B21CA4106C3E6AB14381A87C6C6FB70635159077BE8369699823F54BCCEFEA3D3AD61613409B41C007947015364BCF42F184BF3BC8B03C6976749347EA7564076CAE9360F1CAA3BDF183889766FD428D13936B25EAA8156776C813705B79CF11030ABDE33B87FA4200544740598EA451AF9ED65CE3D0088F948AE07134BC2830253C9A680477C26180621BA679CB78F4E1304743A2AF124054499A21A9B510C78B2F875445B8AD58A559CF624DB8F8ABD1188812C122EAE81AB0F50435C77D72918E03DBAC7FC61DE848B4BC1729140602E738734B43CC62CB48B8131A2E257D82CB58F69AA6C4A348EA48277A0C8504D07323C61C42B68E5A212B01D66DB645BCBB6B34AF9B8AD7F709EB3720C1B79EF09352A078892787AE73D0B5DF546CC5F35D84940999D9CC59FB39C41762AD4AB260E6795F37372BEAC07005BF80F4A5620B4F23A2255D212746F4CFF73995E53C075C6790CAF4745A2AB8CAE2A89D94A601673023819166A709010C9D4A42CE9AA8C96381B1A7A236781C0C0F10C653F71353224242D3A8F0786A31703CD2063929393DAC152186C84253825481FC86BDFC70E3C5975D3A1DE8F23002126A05DB29FDD143A1F60222D7409B468138A8AAE2A34C1FFB6CEAA863CBC9666DBA1FE7138DE19AAED65256B15CBB08110E25FBC7209750C2CB225F6C5C97836AA04BA1B636BDE9235709483998E85BC1A28F713CA3935505E840A82284B879100389EC2D2A5778C990CCDE45B44D26A9AB74C28705807312681BD662F3E5437772CB7C383D72A53537404FB551CC87274294A85C8BB8484C8113E29708664190F923A387E81EBDAC3CE5890A2C440B9D93323E0588CE003606A5204B93288EB6B62F657C6E511059AC4B3DD57D402064B9C086DB955A7958C399F17AB353108E821A1C1084E11B20F2123C1CA6600AD330587988586C8F698BA258165FA3199A423548EED0B46535935C964BB4A0988B2346BD64BA7FA23D42FAAEF424901878012A772FC4815C26D54037F66F8BBC0E82C7753E16C40E1C17C2BC35F470251393B3B7E84534226DB0237DA2960CFDC99EEB4CA4AFEAA15B9271D9F74DF6CA5631935579AA5E189A715827220CE26BB1C07D719751560765E5A2296CB4BD9C44C4D6376E39A89AA8B70AF987AA0C3C10E714931059AFBC6B07B3523B1175AB9E0902BAAA56754A76C09C4CE232B5EED28F58F097BE3A0F5C290615DC7A4C4C328E413D8E43487E124F8C28444E70A7AE02B64D618D8831609CC939859BB100FC31513B90CE30B59061C38BD17DD57A6A2B0A3640E591524A06A6063259467D1F0821813411E62713A742C0ADE48FE3575338356F4CE99C1995667C34CD2582C0F9B7C4A3DA412C737253A3C3957929D5E4CFEFECAC09BB557231A11ED94F3C7A1BE5060313F540EE5576221386DA4C6A7CB71E307B395DB9C6D7DCB256C42D6F52908C565F06DB823463A634AC57299687641A556F5599F961B6E2F0A30053BE44150E1BEA7DC6D1A4488855127512CE9B2B1F4C07FFABDDF9E7FC37FB0A738DED0707\",\n          \"dk\": \"D4B51192A45D0D05BA48D20D7F8210DE386535D2A9204105AF184B50C81E89889DDF30942A9352C6723D2C94C31B709D452266772A8D69D2784C8332687507A1F678142C390737AD55C42CB7D37A28E9B9A18C374D220FC5A9560F655588439D69616620237225B543E6FB67ACE6B149BAA6AB5C45E334B65C87BFA368C581389B1246639B8550F9D94A61BC806850222869AB9041858DD30C16D0B62B53C85A345F26E3113F51C76F137FECACBB26C6AB1C3898E4AA73889039DD61CDEE46379812CB68BA0A03252199F102C661427088A89EB64D9E6CCE4E9929C19CB91BFA2A816340BE89A636339E67A124F659A45E6A061027533BA635854A0CAA45998E6BA21CC10E53A1A9BEB42DF0107F6765C7B9C8A8E98448CF2A6ACC6514765935064417C5E094B038123AF395B78746E4B66AB3E7BCEA783B4F4B5169478C78B82194961F80FC64AD314CD9D089C559815DD91533DC04B9359FD7936D7B8BB5BED435C77057316440926766F3A665F361BC9314230BAC46AEA6AC7EF648C9901DF017BBDE558668A606809CC717C49EFCE5B9F469BE1A0A9853451B130A952C137F6812978270A194C5CFD899CE2E656FE9CBBE8C3C769C700218489DAC47A07CDAA36EF22EE7125EC5223FD959C335B70C56AB8D52333B4C6A1C12DB76A4C02F7E7410CB646E75A5301F19A080561FDE627C9E42B275FBA0800715B1D77643A8193EFB61DDE898305B4B53140A76CC851FB956FE766C8450100A1C9EC0044C32BC17948C3D4CF44C70077447147BCA735EC9F2251A423F96D694F0F01ED4813713545A791282F7086B025BC4B133B86EB4856A3C99F21129AEC57175818360D678FD549ED769365E4720B3A4A433217DBEBA261C48B75C26828199904820C8FC80098DA6A0A9463B24FA5F43A445F33B38985637AA30CE6FC83D7127BFF9DA633E25A362323564D25622064AF20C16D90620AA0875D65217A389438D236EC8F2C18AE3BFC95296CB951A604B9FC15C69A5927F86D98582C3169BD16C7EE1403BCA5BB1C2238B93B5B0F4C6E3969D98325444A09DE2636DA4A046367358CBA66B28531616733FD816B32BB714CAD1B10DF570062223F6366424BB0388DC1A6CC9AD72CCAD22EC51C671BB859C4A3BCAC9525342B6EC97DB62ADE9949339DA5338D07EE433A868060570E087C34B8BF62ACF15F05657869556CC00713CAA9E5C2AF40474F472B3E117B9F8DC4D148183952A0553F49EEF609F3CF12DB7B102855677A443BD8C94429C9168BDC06CA0FB3781E450DAE94A96E4239DF01A74FB3C016B4AA3463AF3E18D8EC653E838249937B50DB57ADA1CC1215972B410AE459BA481836215E12655534A6F183ACE939DEE553435E9BDD00B4273F0A99CD42607E903E85864F0D27CB4A26474265E2E7A56A576971D69C657D237C05CA7C8070F0FE3A8A1AB8549866D32A253F7B4AEBE6521E557131D361999C5CD6B9837299A2100A52450C9AF85890833A9AC8A3B1B1C0B90839295BA6997BDF262AA93196AEA9F9EB1485E8415FF7006CAEC9A0548AFCF6816938A5E77D5A383F6B3C140786A3626207218883597BC3670903364BD275AE420BAE20522D5E69F9D292E7C5A6A3F3134F767004D052E1476B2A95B790B295AE6F68689828F69EC7965F495E7667E7B420EA2E5C738964FC0257E9D423B98C409E9A27B41E0B87A396EC079338019AE8DEC20DFE0477E940B6233C7D6244A201A0DA4B45AE785158C545AE6B28B49F799E030C762F0BAC8987E12F079D28C4BF7074BED316AB4580D21A8CEF4C30D637275FA2C6884E61B6843C4BC696E8615C47D573FF20BA1053C4259076153BB9314E6CA62F1AF91F594ABD444BBD2B3B203A075083AE8036109710F2B169A688CBCF6D563FAE94D3DF65BFA92B1D20546B5A3A0FF6B3FAB634712566B0E021D617232C8029EB2F76DF624C8DC77AB3789739AB12AE3AB59A7B612A40970D5526023F690C8F1A9BEB4A49F066AA8073E1F865DF9313A63464C65510E078B61320A34E3624E2A6A3E16B7BC3FD995436C92C28B18B149C26754C0DFBA3121778D8E345E6FA4CE8F104F71B77FFE64C0AD945F1F227A031522D07990F88BA91FECB244B5936E87298FDC1836E82C0CB178175CAC5A36C27D3B71EFF5ACAA39200902C611CC1D7395142836692CAC578AA1AC7B12970599690FF9958A7048B7D276DE2088D0B06E5C3971190909C6F216287B2344E3922AF9A4BFC1CEB7E5AD35302E555A504B6B65F710356CC30DF957BD60E974FD31729EB63620E30154636FEBD84DA66BBE270151787B7A8B2BA3431491860C8837B5A93C35C62AE2BDBA43BB58D4126EE8139FFB5BFA110C4075C34F9AC648673EECCB59AF60CB8A6372B69C6E39D418AF88BC23161812486A5F2BB9B2D970B811C360329593CC31FA47BEC0FCA95C226E46A6BD5F39674458BD1683CA4673C39D158D9EE13E9736A6863229156BAE9340532D1A1BFBF98145930EE0A5229323468F7B5A9AE9755F6B603A4360CAEC054276BD3E5A4BF29C85F1DACA2C61A514C1449F94A63C4A3CCCA06D195945EDB3C0DAB07FBF75BFDE57A454C564B99C7DA3E5BA095A462C741E634B3D2DD9B7DF837E5FC076A0C363389C8248036EE55477F55BA27F17740E0AB891D0305E77A284ECA7F84CB3C5483FB3992610384E42A731DB8C3B65B6BBAFF04039D09A9BE4494637A9599CA7A4C42FCCA473D4E88B830A4D2358A324620F9B765A0141CB57DC1B70918053611B7AAA7D6D973C8D4C2CC135940C3CA9B7385823C8A86755717A63BD8EDC5712335786B21CA4106C3E6AB14381A87C6C6FB70635159077BE8369699823F54BCCEFEA3D3AD61613409B41C007947015364BCF42F184BF3BC8B03C6976749347EA7564076CAE9360F1CAA3BDF183889766FD428D13936B25EAA8156776C813705B79CF11030ABDE33B87FA4200544740598EA451AF9ED65CE3D0088F948AE07134BC2830253C9A680477C26180621BA679CB78F4E1304743A2AF124054499A21A9B510C78B2F875445B8AD58A559CF624DB8F8ABD1188812C122EAE81AB0F50435C77D72918E03DBAC7FC61DE848B4BC1729140602E738734B43CC62CB48B8131A2E257D82CB58F69AA6C4A348EA48277A0C8504D07323C61C42B68E5A212B01D66DB645BCBB6B34AF9B8AD7F709EB3720C1B79EF09352A078892787AE73D0B5DF546CC5F35D84940999D9CC59FB39C41762AD4AB260E6795F37372BEAC07005BF80F4A5620B4F23A2255D212746F4CFF73995E53C075C6790CAF4745A2AB8CAE2A89D94A601673023819166A709010C9D4A42CE9AA8C96381B1A7A236781C0C0F10C653F71353224242D3A8F0786A31703CD2063929393DAC152186C84253825481FC86BDFC70E3C5975D3A1DE8F23002126A05DB29FDD143A1F60222D7409B468138A8AAE2A34C1FFB6CEAA863CBC9666DBA1FE7138DE19AAED65256B15CBB08110E25FBC7209750C2CB225F6C5C97836AA04BA1B636BDE9235709483998E85BC1A28F713CA3935505E840A82284B879100389EC2D2A5778C990CCDE45B44D26A9AB74C28705807312681BD662F3E5437772CB7C383D72A53537404FB551CC87274294A85C8BB8484C8113E29708664190F923A387E81EBDAC3CE5890A2C440B9D93323E0588CE003606A5204B93288EB6B62F657C6E511059AC4B3DD57D402064B9C086DB955A7958C399F17AB353108E821A1C1084E11B20F2123C1CA6600AD330587988586C8F698BA258165FA3199A423548EED0B46535935C964BB4A0988B2346BD64BA7FA23D42FAAEF424901878012A772FC4815C26D54037F66F8BBC0E82C7753E16C40E1C17C2BC35F470251393B3B7E84534226DB0237DA2960CFDC99EEB4CA4AFEAA15B9271D9F74DF6CA5631935579AA5E189A715827220CE26BB1C07D719751560765E5A2296CB4BD9C44C4D6376E39A89AA8B70AF987AA0C3C10E714931059AFBC6B07B3523B1175AB9E0902BAAA56754A76C09C4CE232B5EED28F58F097BE3A0F5C290615DC7A4C4C328E413D8E43487E124F8C28444E70A7AE02B64D618D8831609CC939859BB100FC31513B90CE30B59061C38BD17DD57A6A2B0A3640E591524A06A6063259467D1F0821813411E62713A742C0ADE48FE3575338356F4CE99C1995667C34CD2582C0F9B7C4A3DA412C737253A3C3957929D5E4CFEFECAC09BB557231A11ED94F3C7A1BE5060313F540EE5576221386DA4C6A7CB71E307B395DB9C6D7DCB256C42D6F52908C565F06DB823463A634AC57299687641A556F5599F961B6E2F0A30053BE44150E1BEA7DC6D1A4488855127512CE9B2B1F4C07FFABDDF9E7FC37FB0A738DED0707C266F50028D4382821B206CE45306AC320BAE56F49DFDD86F37E1B36C23DC86DABD71039AE2E2700391011D9CC8265C2D5C9779002D54E1BDD9607402054CA95\"\n        },\n        {\n          \"tcId\": 63,\n          \"ek\": \"F4C227FF124D62F0269297261583A7D7EB570E6A52C8B9240D1B1A79946FF3990DDD20AD71284E7B004656270DFBA0681DD2C5C41939D6391F4AAABA0ED3C6AC55B4907534DE05B775CC6284D53D045BB611F5BC62F603327C143E100B6678176A40A4949A923CA651F1D2589896BA74E41722B3A0C1C652BCC7ACA769119332A080211CCA8AACD4799513BB0906A063F2AC750C99A805B792699400F609960D177BC006227BA40CF1C547BD81C9F5D67947B6A7BFABA98415C81A4137CD2CBF024322633A8B1B242594581ED8720A794293FDB282EBA064DF25301FEABBBD14BA4FB8970B52B4E3574BC57218AEBC5FB9E731F5A85CFD1154B42575A0B3BEE2FC97CB691AE9F4361F11692D9CB07D793A3C6588ADF24FE098B0CEA860E0173C35223C678A10CD3752E97766B28BA562D1CB93935E97E73B6DFC3248217E44FB08684B5419D749D9350F143CC66E9921FA6C4180D43A721424C06864EA98CD3E60C12020AE7123ADBC2CCF40577BA4F87E10B755A1567941F98E0D0A0D26A9303383026D021F2284CE313AAC3C0A765D11747EEA8BC2832EC87B22EF31AD0D1409FC66A782B2B09AC43EBFE215862B6462996A3703525281BA8328621E436DBD533AEA69B62B429F83A78843051AA0D003C6F8BA224148E15A5AB39A6EAD69262627C7B0C46730C66D2109512D424849A53CE58B9FF463B87945CF9E253130A64CB1F18A2ED875D43026BB8AA2B87ACB00186889C082391C95273BB864E548ADD29A876B3445A471B685C659E213853C62E1A802E2B3C4C392575620597C7131F2764AE68B95AE190927E110A10A553E008F39054DE62C16912064F0B7711EF59E85E50E30395A36A11EAED23CB48A01C8F67729F6B638E3B6820A0E530ABDE31A97E7DB117B035897952F0A0473AAE5897A86725C53855AA43A16BB31BE7C564FDBA0BDDB44D6E64E7959C871728C7C5111D989CFB9026902B82544B528D4FA8BE3196C6318CA1BB791B7069EF2D225277C60B03C48C183484250B4FB0A5B05E260E8A9C958CA51F9BC69DA7ACEB006569B427D2A60C070D81C410114E753BF85651E3504418E747B8D787F0237B184834FF8689519034FF6956313120A0495267B754BA5015D8A134688B11160A2CDE5E8B18D4400AB95AC6CFBCF5CE72020C60761128CBCD5BD36817360663F83A7A263B9420A654C7280381092B4D295B3DAC03E918926B5C29BEB0478A01A39A8B0CCCEC05125D84D2872CAFC97A8627866DC7380DE17709FAC24EA1A0E00DAB5990A919E0262DA522CCAF9B0D3F11268D0A2B2BC4D85E641F64048A3557887A266A8DB13EB555608960E49618B1E39708B65151C4BB8D2154CE7B6A13F86B75367B005C9A69DA98B82ABB17EF9678EB50F3F097A6292930191A15FE075BC304D49926B3D71B675C26869B6BB93264F74702E17102DB5BCC881751120CB8D3E05215E4B34A505529BAB1A2A259B7BC7999404BD15272E44B9AA4BDC891E01A21B8A71B46725DD187E2041C07092CFDDC902D3DBAFC29379691644D0E15A8F7378298A8834D1C1B078384C140F460B144826188F51852AE0ACBAA1ADC4D86147C02F61C1B154193B2627C609682A6E656725774A2FC60DE52A3837537C421A58312A7E6BD1CB7B7661FDB3C1D9C51C3A6A44F260982BDCC01FA149697B24B00B76C5DA2908181E8CE21A28D9142F12659023536F392AB3781C74569DBA10506CB79BA214C49CD18A1D9A693734791FDB76360A142B71252B9020099B389AEA8A25796CA43BB5FA881277D742B6C7CC4651CDF47B91C73B7CC816B16F040DCEEBB3E27C68F0388ABE1A4B2B8B88820B30C9D03C1ED75A9E46774B9612C6C039C1F0404F40CAB35369BCAA80FA2C53975C63F75211B1154F94C3AEE4DBB3EEE500091BA629900A54245CBB4C92296769D979530371A0EEC19DC6718AFACCA91D219E37788802B5C162237A7274C3BE821BC969493E884DF2E915ADC3AD6F1919218C844AF94C983ACA8038CC3874C0C080161D5B4F1266420881C906C145ED15692E8C24F2574F80EB3265157EF3E63BDA96390AC160CEE8B6A79B363C290B4B35AEF49CA4DFFAC0F0423249F27A6CF77558F0631581B36E58AD1DB09F4781B8F813B0ED6578FFBA5A73FABA29D94B6F9238A9089CF41BEE300083ABC96CF379FBD0190841D981FE9CFAD1C7FAF23D2426AE\",\n          \"dk\": \"95937E82B41B65D97063375BF2516320419CE2DA063E41B62E50AAB029407A98044163C56670C75B368D6771AB590A40177073DF486D083C62C5698BC02B97DA59CFEDE6305E3ACC6DCA183093622EE80177BC507DCB8C5780A398FAB838A7CBB7126883A006A848B743FB7D0FD4CE1E4CA026FACC5F981AFDF0AD72FACFA69904BAA9944335B14B9521B492876FE30D07F81BDD7162AFD5A4BAB76E13901CCCA7700C45B385C11F23951DA37740FF58818CEC668992C46B5A478AD221A3EC989F77A5D9B1095F2A7AA0849A731AAD51A6B51B0CBEED2A8531B9A3D3C39E55597E1076AE4B4146740A8A16051A9EFC5C4E016695BA97A26AAE6D850590773D00449CA68A4E65D0463EC15DD46813A7810051203ACC7A42814A7121AA6B45EC9E37DC2F40AB96A1C025835CA7C8198F292390E3D5C9967865F2108DD17402DA657175C65AC47842E1473F9BD29DDE44388695109453B823E204A05A9B3AD6A398B69185028460F8230E3241E4012659E05AAA9CAAB8D8079C65919C8304EB6AA78330BDDBCCA763B91729F81B05BAC53BD31332FABC69D4CDB85CC0DF0B47961731D26A2FC1649A5B53A05CD13109479A75B48F67898C63713EC7883BC3744C9A60C23AF5320C645DE8C242C0B17EB97974CCF76D57180E40330663F9BC737A40C2202F47B22580F5CA070C4488B1B139D0155B1006A7B31A79F0BAB9239B9E4B7DBACB534790ABD0F28141D43CA52792503B4C0CE96B0F505A4E263D1E9429F631447719B8580C902385B221FA2C174866E8872D1B299706B860343BC9AAE6660B77C7E2BA5C8C1CAE4A054A60038743655378080044EC7D45D36411768595BC1D30A43AE3805BD77211E1EAB4C97C9A4F6408FC48551D880B4728388F55519757B9B258781AD2915D40C9DA27961145AABC1666253171A62425C8BA9667322C7C9599104C034C1168A50A47A44A7BD078B24726561488661CF70FB2B7242BC354DEEA94324622B55607F06221A65255C2924982964A2FAC5BBA75CAD6B4677DA45F96DB2F91FA7517AB968A66AF8800AD431423F03936F3614D3ED7236C8A32F16B4DED28285CD46A5AC06A42397A2CDAB0FFE1A70208A226233A84B4AA6B0CC8BC1A4913B44322FACAA7917D119556412880286405EB6C9A7E75CE74C75EDF794A067747AA0B31E99C1F68C25696207C90B97CB57C5088982129811E08773249C5898FD4B86D79BCA5A02C1D389530EBB1AA2ACA1DF1A60FE19649F7026B9229B39A60F29982EAA639D9830074CAC4C375295719C4FCD03A36D99DC6306DFF96A4AA1B0110D37A1E716DDCFB0A322134A978C1D0998CD5A8943B267E2B6B0D6936111BF630B0CC89A549405CB295038ACBF22B6E15788CC8966060EC4BD71255511826CF163A6C45AE24FA8536C39F8C19C7856A3700972FDB959AAD019C04269C97D7CA55121A567BBA5711A7F7E28A13F955DD06988AE2BFF90C4C3C605E0B200DCBF139E576BC367C1D151A8F7297B5C76CB826208EDCA1CB23183397A37A92B233F65494E6E3644A364EB8F05FD93248DABAB286991297EA1A87B947A8C0C1E32A9B336B44E5D045E4418C8BD366DF77C2AD90CA2D690C516B032060C32314CB7DDA818E80456E588248609E4E4BAEC2B30FC1E8A984082C23555093D375FF129CDE4498BFD4A40AD1678AE78C5647B6998212CB0010241703A781727FD755B5907B12350A853440B9019F7BC3738A1A2BB291A549A9892D74BD2FC4B13BBA718F9877BDBA32B7D07F0DF33BC0C19AB3C2B567D7110BD3592274723F5A69AFA632FE8C41AE7CADAA115E61B897C4444FA7099AEF0290A6A13E8D551A3DF565490A825DEB6D808AA7AF377B27355324C355C32358955B883F6BBD42C66801CC443BF34F2FA040FCE4B7237C4ECEAACA821AA8B248841D242B6EA5561B070E06F711528109693AA289740F97A4169758151ED57A15381EE35297016B0699C34F9AE039ABA26ABB11148BB4344359CF14466841C7CEE7B3541B6072BF021E3B8348E078510DE0839EC55C38290F5C5095B429AF7BC970CE46110F54967C42A2AF4A3CD5BAAA5BBB500EA382E9427FE02595B0E6811FE32E932BA6B479114F5B7224F98EFDE80CDDE06BC580B30A5175B9B38958393B3BF0573A7086EDD0A1F4C227FF124D62F0269297261583A7D7EB570E6A52C8B9240D1B1A79946FF3990DDD20AD71284E7B004656270DFBA0681DD2C5C41939D6391F4AAABA0ED3C6AC55B4907534DE05B775CC6284D53D045BB611F5BC62F603327C143E100B6678176A40A4949A923CA651F1D2589896BA74E41722B3A0C1C652BCC7ACA769119332A080211CCA8AACD4799513BB0906A063F2AC750C99A805B792699400F609960D177BC006227BA40CF1C547BD81C9F5D67947B6A7BFABA98415C81A4137CD2CBF024322633A8B1B242594581ED8720A794293FDB282EBA064DF25301FEABBBD14BA4FB8970B52B4E3574BC57218AEBC5FB9E731F5A85CFD1154B42575A0B3BEE2FC97CB691AE9F4361F11692D9CB07D793A3C6588ADF24FE098B0CEA860E0173C35223C678A10CD3752E97766B28BA562D1CB93935E97E73B6DFC3248217E44FB08684B5419D749D9350F143CC66E9921FA6C4180D43A721424C06864EA98CD3E60C12020AE7123ADBC2CCF40577BA4F87E10B755A1567941F98E0D0A0D26A9303383026D021F2284CE313AAC3C0A765D11747EEA8BC2832EC87B22EF31AD0D1409FC66A782B2B09AC43EBFE215862B6462996A3703525281BA8328621E436DBD533AEA69B62B429F83A78843051AA0D003C6F8BA224148E15A5AB39A6EAD69262627C7B0C46730C66D2109512D424849A53CE58B9FF463B87945CF9E253130A64CB1F18A2ED875D43026BB8AA2B87ACB00186889C082391C95273BB864E548ADD29A876B3445A471B685C659E213853C62E1A802E2B3C4C392575620597C7131F2764AE68B95AE190927E110A10A553E008F39054DE62C16912064F0B7711EF59E85E50E30395A36A11EAED23CB48A01C8F67729F6B638E3B6820A0E530ABDE31A97E7DB117B035897952F0A0473AAE5897A86725C53855AA43A16BB31BE7C564FDBA0BDDB44D6E64E7959C871728C7C5111D989CFB9026902B82544B528D4FA8BE3196C6318CA1BB791B7069EF2D225277C60B03C48C183484250B4FB0A5B05E260E8A9C958CA51F9BC69DA7ACEB006569B427D2A60C070D81C410114E753BF85651E3504418E747B8D787F0237B184834FF8689519034FF6956313120A0495267B754BA5015D8A134688B11160A2CDE5E8B18D4400AB95AC6CFBCF5CE72020C60761128CBCD5BD36817360663F83A7A263B9420A654C7280381092B4D295B3DAC03E918926B5C29BEB0478A01A39A8B0CCCEC05125D84D2872CAFC97A8627866DC7380DE17709FAC24EA1A0E00DAB5990A919E0262DA522CCAF9B0D3F11268D0A2B2BC4D85E641F64048A3557887A266A8DB13EB555608960E49618B1E39708B65151C4BB8D2154CE7B6A13F86B75367B005C9A69DA98B82ABB17EF9678EB50F3F097A6292930191A15FE075BC304D49926B3D71B675C26869B6BB93264F74702E17102DB5BCC881751120CB8D3E05215E4B34A505529BAB1A2A259B7BC7999404BD15272E44B9AA4BDC891E01A21B8A71B46725DD187E2041C07092CFDDC902D3DBAFC29379691644D0E15A8F7378298A8834D1C1B078384C140F460B144826188F51852AE0ACBAA1ADC4D86147C02F61C1B154193B2627C609682A6E656725774A2FC60DE52A3837537C421A58312A7E6BD1CB7B7661FDB3C1D9C51C3A6A44F260982BDCC01FA149697B24B00B76C5DA2908181E8CE21A28D9142F12659023536F392AB3781C74569DBA10506CB79BA214C49CD18A1D9A693734791FDB76360A142B71252B9020099B389AEA8A25796CA43BB5FA881277D742B6C7CC4651CDF47B91C73B7CC816B16F040DCEEBB3E27C68F0388ABE1A4B2B8B88820B30C9D03C1ED75A9E46774B9612C6C039C1F0404F40CAB35369BCAA80FA2C53975C63F75211B1154F94C3AEE4DBB3EEE500091BA629900A54245CBB4C92296769D979530371A0EEC19DC6718AFACCA91D219E37788802B5C162237A7274C3BE821BC969493E884DF2E915ADC3AD6F1919218C844AF94C983ACA8038CC3874C0C080161D5B4F1266420881C906C145ED15692E8C24F2574F80EB3265157EF3E63BDA96390AC160CEE8B6A79B363C290B4B35AEF49CA4DFFAC0F0423249F27A6CF77558F0631581B36E58AD1DB09F4781B8F813B0ED6578FFBA5A73FABA29D94B6F9238A9089CF41BEE300083ABC96CF379FBD0190841D981FE9CFAD1C7FAF23D2426AE2A959860220DFD26FEE86E0F4EB1D8E31B240EFFB9EF6091AA0BCF551A09B2B9177A8DA7AF8DB3F712E1653D05A47D61B59F4F4950549382E56F761D7126F8F9\"\n        },\n        {\n          \"tcId\": 64,\n          \"ek\": \"BF2A86AD3020C241CE4A00913BDB13F82266DD873742A3519E44C39F20874575A780370BDBC32F50F892366C3D3E5A3CBD2A1A9EDAC8D5468A4FD28D18281C4ADB485B033E76B3B604A5394EB6A280F81C93B86839C2198430558889696CA715CA0B901D762A498875DE991479A45725E12340953D95461ED32AA018E33BBA0157F3A6BD1BF07FBE2207D81166F5410AC20096E95B6F48E09190ACB51A86AD4C51AD1733729272BE79BCB14834A230F00C64E0416AF77C2F155776D6528BD0C9762B60350430A3BCC51DEA9966D791A3904A1F6284A32B4C865CC67C1B66A7F00BA3C9971AD846DF61348D2C418DE0901ED19D32582ACA784AC96A55AAE69D27B58973B4816714B2139C2DF5A380739C43CDF0A161AB8535F678FE344C5E4881BAB8785805C615711ADEA09B702201360B2479CC7FBA69868903CED7074D7F93AA7772B3359644B0A3A169A59C177CC60E8B4360978FC7C513A95C355643563BA0371D973F7A991AD099BF94239815C7041A312CC3497E9A40A5D3F9504BE107FDEA73D8769025D75479575F5A45974FAC400F0189076457A2A96D9BC2C1B19C5382C423B3333F0C9B7E26FCA8F2DB01265460A4C253EAC707B39109AF07241DD6A8FC5A9D18494C3D715424108114280F3A5C23CEF53C5507BDF3BA5EDA6327AC15B484793011241305462DADE02D4459419909075C09BA1044B896A28E17866FEBCC0A4970B1734C4EFA42780C380EAAB17C82B96D72CBAA83127FFCD7998071BDF8EC59881AC74177C1E194A20BF5A4DD71C4E03A82D6E40B70986D2A368C53C54BD7B0CCE1B5A161140E01047443497EFB4C4D3FE126686BA354B504B9537C27B21B56355BC0A91F82E94853801801AA5FD8287EB5A1BAEA773EDF16B376311DB6089AF1950A385037DEABA09ECC491F4882CD484604F304730C11EB3C561F36B85D1B1ECAE15FA1326F9EA486CE32978E9C267C70AA33E0113F4C3B618B405A160B4C93BF943C806FB41C62F83200B81AB2CC57CEE96364D86D4630BCA5A70064151545EA9D9A419E41242B6135AF3A516C8DA4447AB71478266E5BF69F2B846075C30F3368B30912BE48A00CA3365BBAE0597A93942396515A72B675C460BF4403872995AF19CC25C737F86B642ED740C9E5477C0154D05860A30C2DD49B14E8DA87600C4E8CA63401DA998D7689D9CB364397C4DD056474C2BEE16060D4408A3E19BF6FF8A331466209F0B428F0316CA89188B9C7B2AC25A7940DB7A1766799361BA984A1BB7E789B2438D6777F92A15CE7A2D156369AB819B116CD99076095C70D265651550BCC1F4307497BB77E75023AF91D31F4414B38CB64CB570AFB56701C17CB692A46A444455C8063A3616590AF6064BC337237ED1824CCD7CFB96B55FFF91D7D4714CBBA74DEE5AB48A28389458E2D43476CEA2B1A0B6EC499A83CEB8EB4C7539BD93D3AC987AD07A1C9F079850C1444D0B5C9CC619614301CD77776661357FA80F29A3CE4A8AF8DF55FB259B754504687508AC12AA81DB8CD19C93875AC5C4CF13E115221EBA49F3BE008D4B80E9EBB8AE0CA997313872010497CF9556DF06D8F766D2025B03099BAC8202C5D90B52C5397CF55BFACEB5A1C079CC02960FB976A735357DF16B90210194627A5089B1D29CA4FE8A03B4FFBB544A645C5FA95EB386408DC65AAD89CDF0285F3745EDA596D070356D350775FA7A088D732A61C1AC57306359B4A642B8AA8BB91A347152A916D33427DAB37143AE14B2D804B0FC033EE2B9D996853ACD202B2689AD945CDA48938BCDB2D8DBA5449C80891B103172381200A68BB7B505EC5A373E15DF301BB7547770A5B21A571CF4CE87B1AF38D3D38329BE9AD366159FF1919DB40233E86BB497652379B9099F68B22EA91552273AB1801CE6BA9FE7B07ACE9A81DBB09B11CC4AECA25B5090C6FD885B7E19941D8561A15CC35046EC191157C393168C80311432C899B0A0AA578460963E7767750560064B93AB148AEF2FC2FBB029C89126535C6BF2DC48A04748D23A6225EE6C3EEA9B68C65BCA2E9C804EA5BDC441BD05293B8C19D96E369B5C3459DABCB20565405824EC3F57475AA4E19DA21A8EAC0DD312E9FC10B56D6C6EF40A130844D8FD68769E2CC08288D770CD00556578D853FE4896886536AF0633EFF38C1439E3E7950091189960045B3B3B18BD45BE846AA63E4D3845BA0\",\n          \"dk\": \"C62AA41327AC132035DF838111878AE1BC660EB53459381290580906A45376761BEDA32D498611D642173E7CA019DA1AF1E60EE43601F1262D3B396FB5611FDEC023C891407AF4A8ADF85D4C010F7DE70829138F0F520EFFB4C8D5230253CA72A6FA3D4C4158A65C12FB9A36BA440A40B476D8C65CED2968BB18A11751993B725920399933F3B18DFA9A4ED87A7AB277DC027CC33A83E43B9DB68C8F68D72FEE7210E9E1471869B347C79B848C623834C586B341B23203D2CB1AE5E9CFB87A1794D43598EB0CC8836BDD42371AC9B7D0513F4A7840C0371D73BA88A7DBC5A5753020298E3D616A8DB46C81C80045A553984939D7593535D6BF2A330BE54A3D1CC0B09774279F952A59F673298C6B1C078E0FC87038D03304E0C4585577A6842815D9C9F41534F216B5B0474F1574BE15051F3DD69CE3E9870AB13FAAD180C82A1ADC2927A1D20EEA168A97DA7ABCFB617B7570009AC7D212B6D05483B7F54F7C201588A879235823038834685BA5C7649CA7931D3D2B155A3410A76900E4738A24A614AAF119D8AB6111E383BA33A4614823063C31EDB9B031953AF6FACFD3D65ED1557EC879B89F3709D177A8818094B35500A10B30E10C7917956A0181BD6B0817F0D85347430EE6724B5380BEF508AB3B468AB6C8868FC48A62F3445104A41C8C288DDC5746C84301E5315C603F80E9B83F6C16920142611CC32B18BD7AFC28D3D343AAEB8AE7133F88E388AE6C7C4911318A3957904AA51E2905D5A02C30CB57DA0551B4134182C060DBE80BAAC4981B410D1D5104D92BB47D3BB6846937CF6B961FE66BDB5C069A0304D91AA22D8898F8067295DA9BD6DC3408F3636DE5CC6948C9DB2B17DD4651F1419668AABEF031CD06FC63BB9749C4DAB8A160B5A4C6A13F3A4F8941712EFA60E5B426E1BB29F65940368594B681CD30D41377F58254964FF0AA4D3A5B553327762036757429CD47D57DD73698BA0C765B3C539CE41A04A58775E70D4EF38D2B9775D203982479937E5135FF1B944F6CAA0E87269E52B52DDC3958C99C668170879B7FD03427C4F38E147B6ADEA1BF0123B01CBB199644AAEA95B76DA7C5E60839C3EC0D50C4A01E79810BBC1220F612FE34A89B173C7E6042BD876D0EDC2D9A03559027AE0844492B539F2843A2A49C6E969C121CC64E4CDBC624167C22911A27656109643F695334F3C73694758F06E94CBC15385F181EA1B012D93C5AF375034514074C92401A455BC21C8B97AB7D8CA64B58C415B53AB4D322CF28F69882A4C0F5BC060AB09AD643311F29398205753D623EA424C7F2D38CF0E1074F1CB5A9E91B19D91A637A83E15B3DF5B7C7987A4D5FC151E9012021F00CA8E31069F9451E4B6E5D7331E85980077367990AA632009EDBE0432722A825829074953E6BB752C7D86490D7C760E7177F54806CDAC557C70C3DA151B9F30877B22FF8742BE8077C7C82A212BA077C148D90E86D5AFA15AE30362CB287D3BC318CC28FF74AA74536A73009C00E9B8A7F8AC11BEB5CB6CC965D672141DB7784B8BAFA395996FA4DF8C5C014C8B73C3302AA6542950A163AAC35D7C222B1A2CF5C9A820D682B6E98242B28B8B9D32E973B823027211784415BF27E2A0C60125295BC35007E21A3CDB4BEB6E26FF26069DC1BB675032FFFF93924870F0616BCF6679353200152369881182937565B3BF4711882C9823B800EA93CF7889B14F521C5A34C81A6354B70CF02D0C195853846505DC2E82B3F3651815480CB739AA49C55E0B763C9982C69D0669DCC76A3D262C304843FD322E973159EA358E5811D4C324914BB89DC7BB1BA61C4C2E9C25B0886B6F5ABEB827992B6798250975BE205B6252CE3DC5DC2E00D4CB427FB9338D6A19166C2978EEC81A645714765095E89799082AB42D08FD2305159B42C80B13F44AA9FEB46B8F3A5552BA534B9F26A2E5B2C00822CFA573106555C9E8C5B1475A38E4782675B9A6FFA294A7B0A2FB26AA8387C46C20363B2BE83E9AF100211B02701CC0A10220134DF6230874A83C49CA846E1106591AC21B82BB587583C33BC97BC62C53C316AA58C525750225C795424B780EA39E9E79588AA9556B2AB9BE98057046EEEC406FB4A81ED364964C1559EFA623307A891A9B7F06433681B2D5F583257E2968786A77CB6C9BF2A86AD3020C241CE4A00913BDB13F82266DD873742A3519E44C39F20874575A780370BDBC32F50F892366C3D3E5A3CBD2A1A9EDAC8D5468A4FD28D18281C4ADB485B033E76B3B604A5394EB6A280F81C93B86839C2198430558889696CA715CA0B901D762A498875DE991479A45725E12340953D95461ED32AA018E33BBA0157F3A6BD1BF07FBE2207D81166F5410AC20096E95B6F48E09190ACB51A86AD4C51AD1733729272BE79BCB14834A230F00C64E0416AF77C2F155776D6528BD0C9762B60350430A3BCC51DEA9966D791A3904A1F6284A32B4C865CC67C1B66A7F00BA3C9971AD846DF61348D2C418DE0901ED19D32582ACA784AC96A55AAE69D27B58973B4816714B2139C2DF5A380739C43CDF0A161AB8535F678FE344C5E4881BAB8785805C615711ADEA09B702201360B2479CC7FBA69868903CED7074D7F93AA7772B3359644B0A3A169A59C177CC60E8B4360978FC7C513A95C355643563BA0371D973F7A991AD099BF94239815C7041A312CC3497E9A40A5D3F9504BE107FDEA73D8769025D75479575F5A45974FAC400F0189076457A2A96D9BC2C1B19C5382C423B3333F0C9B7E26FCA8F2DB01265460A4C253EAC707B39109AF07241DD6A8FC5A9D18494C3D715424108114280F3A5C23CEF53C5507BDF3BA5EDA6327AC15B484793011241305462DADE02D4459419909075C09BA1044B896A28E17866FEBCC0A4970B1734C4EFA42780C380EAAB17C82B96D72CBAA83127FFCD7998071BDF8EC59881AC74177C1E194A20BF5A4DD71C4E03A82D6E40B70986D2A368C53C54BD7B0CCE1B5A161140E01047443497EFB4C4D3FE126686BA354B504B9537C27B21B56355BC0A91F82E94853801801AA5FD8287EB5A1BAEA773EDF16B376311DB6089AF1950A385037DEABA09ECC491F4882CD484604F304730C11EB3C561F36B85D1B1ECAE15FA1326F9EA486CE32978E9C267C70AA33E0113F4C3B618B405A160B4C93BF943C806FB41C62F83200B81AB2CC57CEE96364D86D4630BCA5A70064151545EA9D9A419E41242B6135AF3A516C8DA4447AB71478266E5BF69F2B846075C30F3368B30912BE48A00CA3365BBAE0597A93942396515A72B675C460BF4403872995AF19CC25C737F86B642ED740C9E5477C0154D05860A30C2DD49B14E8DA87600C4E8CA63401DA998D7689D9CB364397C4DD056474C2BEE16060D4408A3E19BF6FF8A331466209F0B428F0316CA89188B9C7B2AC25A7940DB7A1766799361BA984A1BB7E789B2438D6777F92A15CE7A2D156369AB819B116CD99076095C70D265651550BCC1F4307497BB77E75023AF91D31F4414B38CB64CB570AFB56701C17CB692A46A444455C8063A3616590AF6064BC337237ED1824CCD7CFB96B55FFF91D7D4714CBBA74DEE5AB48A28389458E2D43476CEA2B1A0B6EC499A83CEB8EB4C7539BD93D3AC987AD07A1C9F079850C1444D0B5C9CC619614301CD77776661357FA80F29A3CE4A8AF8DF55FB259B754504687508AC12AA81DB8CD19C93875AC5C4CF13E115221EBA49F3BE008D4B80E9EBB8AE0CA997313872010497CF9556DF06D8F766D2025B03099BAC8202C5D90B52C5397CF55BFACEB5A1C079CC02960FB976A735357DF16B90210194627A5089B1D29CA4FE8A03B4FFBB544A645C5FA95EB386408DC65AAD89CDF0285F3745EDA596D070356D350775FA7A088D732A61C1AC57306359B4A642B8AA8BB91A347152A916D33427DAB37143AE14B2D804B0FC033EE2B9D996853ACD202B2689AD945CDA48938BCDB2D8DBA5449C80891B103172381200A68BB7B505EC5A373E15DF301BB7547770A5B21A571CF4CE87B1AF38D3D38329BE9AD366159FF1919DB40233E86BB497652379B9099F68B22EA91552273AB1801CE6BA9FE7B07ACE9A81DBB09B11CC4AECA25B5090C6FD885B7E19941D8561A15CC35046EC191157C393168C80311432C899B0A0AA578460963E7767750560064B93AB148AEF2FC2FBB029C89126535C6BF2DC48A04748D23A6225EE6C3EEA9B68C65BCA2E9C804EA5BDC441BD05293B8C19D96E369B5C3459DABCB20565405824EC3F57475AA4E19DA21A8EAC0DD312E9FC10B56D6C6EF40A130844D8FD68769E2CC08288D770CD00556578D853FE4896886536AF0633EFF38C1439E3E7950091189960045B3B3B18BD45BE846AA63E4D3845BA001699FB3EF1CB24186CF884DBF62F4BC68D598BEB013F7C438C66E180500AD0579E3B0D4F4AF344ED06FDE8BF4E104753E832294A3D2E4B66BE59149006A7B95\"\n        },\n        {\n          \"tcId\": 65,\n          \"ek\": \"4D09B276CC63BC453FF20B31FFDCA2FC3C34AEA706769A58AB6CC210551683C2BABC76B5CA69256816172BD926BA7C91A4DC12AC5A3FB087B6E1D3C0042BA5E0F98E5031A6E5077108153CCE342B1F832EFBBAC43A9C0B5A01283522271E649C3AE8C43C541C88603E58F99C1E8502DB34CF70AA48EAD6802844A7B25594308537BF657A07547CE1380AEB994F4F676FF6EA3B9A728AAE5B4E4FF34A5B68530C582194320E6BF76B997A8CF42819DE5B0D1C82491E741CEDA8AA6280094E332D95FA799FBBB769B2BF769798854899D67C1007FA29CD7245F727CB206456E4020F80A9600DE948DD2AB469C1883151A440C49570430D003C97642601CF010C200198ADB7ABC04379864C7FE6CC8BBC29CC1A6335869ACDE9DA9DFFA17C7B8747848CB7DF12B52E8C5EE7144F2EFC3ED00846B9F72598219793A6397E7726F8242B5EB92EBE2577E686B980F34859B07FD24084057B13A98880C93458D59BCD5AA68314EA18E80A94CD826FDBDAC1F05A472DE0872027C1CF924D5BDC9F236B1893DA61486757FC758806F580279919862C0333B15A66C138E4DC6ED50B17EAFABE9074AF14270C20806AFF0B58ED97631884435A55A8AB394EEEF5347B2640E6980C41A30A18E85A86074E03DBAC48855DE4E94538939A4264593B649E5BD981E0917C26D563D287A600EA89CEEA7021E754384157C8ABC5BE47C7DAF72E15787D1CE62723547B0AD86F852B150A25660A4A074E707884CA7934F9BEE2555A0F574B7F750DD293496FF681A09A4976671210FAC3C8C84A03F99F39A7B649A19F3E05A1D0E7A3728B253C5784F92CA7348A4BC086460A24B620845662246F1C0AB9BF91C8CB301D7DC362A3137643C92C8797CBA7E59A17DB9B99D08977D56A4F34AAEBC16FFBC8822822931FC73DC6BAB320972D96C16DAA41CCC89312E43275856856C4598313417C1B5261D9C1430A16C1D2F3CED08C862A21678A170A2E1A643E139873C878AA469F163C97AF7C7A1DA41034878D7E144DB826A7AE48B97A5A131185813D775BEA667CE0518B11429768F58BC30946766A6FE5B7BBCDDA7CE18C29F396307BB456AEC16FCB989F32660CA348A83FF36D1AF452C4584E117A3105C662135997142AB16CBB7C7D28560B087BA4B7B66A00563EFA3DA1C4764F8B7B5D128EC6202C505AC9446015620C359555268BF6AF74144EBB26351E98AF7DA51571F16A01B320595754C0DC23BBC75A8F036A5F6C3AD76907FBEC72530855891381B9F2B719429D4B6BC04F014F50FAB14131C60CBB279B61469E1B8664A38917CC308FE0C7AFF637DD4A76BAB66F08487739B20B9EF850F7F20DD83A1A47B53F00471A1617AD164AC1C04A8180BA68984B4F5DA9BBE1D0CA352509BC0211DA0B321789A61C98222508A03E0A6A7CF3849126435F36575D63B342FC2F02DA45FB8B5C9BBB6E064328550CBB6DA1AF16424A3AE9C1EBE566F4C4A883F966479C0BCB6585D3A2B1E14C3608E05AC5555454EBAF80E5C7BDE47B12F66118027E19F325024A5D51779C17D1B933A1CCE61003D018A3BBD67EA16B02EF997EF0678CB776479C52203875774C05B0EA9586FDE02964B8C3E1968B9940C88763822AE563D814BEEDE38A7D1653B2131558231AE3470608B51FB78CAA99C7252EC8AEAD74025CA8BD5C038283181E9381C73188C403A0B6F88B855FB5673B180F7581AD69513163379B34582128575DCA936A614A17C84C36222C3893D4B5F97829E132449490096605B61925844E31982BD601538739D8E7B378E180FA6A7EB9874CFBB8069447A96CB81169F3514CC20A93A42E0C9CC321EAC10B8AB21A893FC247BD557B6485A9C433390076573EEF854C2EF08AE116C58FC75CBC03BF8FE7312EA5305781CAC7273A78F01C73319B0B26670F26513E57660340563ACB16CEE7CE7CF0BA52671E23A44C85EB994A7A7BA15941F4C301D3B87E0D2A75D2EAAF1789237BB5B02930CE6AE5B965DC5E7277C8EEC0676C55436014AEFB8AA3B1BBA2D07B1F91487C99673A9135611BCC3B3546C42B23CE0A8099D6666FDEF62D402925B4561DEE3986B30C1C6278609496A474928D0A8351DF30C83CB4BF4BD826CB4319BD620C7AD5A7F1EBA1B8913A46D0365285A38C4C1574FA4287C02D71E91A43EF856ECCF60978C7B83099574EAE8C27A0571C4E51B320B34ED55E8B1E576E\",\n          \"dk\": \"CA16BB08B42E527B707E18B2ADDC64F2FB9542C89FF37B4B9187C72DC602FD83CA619836032C19D3497D49FA2AD23C0939721984DA633C14BE7300029CEB1AEE6631F236060D378852F6B1453214D1C87806E42E956286D2356B7B42731B054829B72B6A89BA4C16308A00C35D59A717E72BCFE69DB53C77D6295243FB599FB3BCA5E6C179205E15EAC020826EE8800A97EA3B51400103A362D0669550D1B56C466535B148862258EF25C3FE14A16362B5C940362B30C2E9A660BA66837071AC18D802392BCF17E62AE718288D175B0A61A3010943131070F84374A47C2EA3E690F7660E67E1B813DA13B9E52BB422A2145482C1A536D0E81BDA906A45DA576D09350FE9C223BC31D89571FFAC999EFB71FA59B29B0431FCB68B1FA37DC9585012414212883B640A10E0937CCF3BBA568A2A519B1EB02B515A26335FB52A1D6B9D122C212A913520661A795855A4D65695D4469C3766FEBA11AA4CCB77B4AAE7608A82EA2E22EABD4105075DAB6D88591A2B2B5B11918C15735DAE6546BEB95656F195F0BC70A3FA3D6FE9898BA1284CD368666C8D8A51A7440411525C3F6C694CC586CA15EAC1CD4B88A1A8CCC713233CC5946C009799DA4EC0294CAFB09D94580EE4E705FB842CDE2429694141C0062508C79D69A2515B843204B3857F77A352764A88A5677C885F00A88615E3524C7A609547184FE029EE7572DB3C821C8552FE499478DA9DD708BF602B73F180B5C801C9D7465E77C7B42A97CAA3F4CD7B62BF0AF7A7F2BA6187129012E5C7443738572C1BBDDB784459B9921B9B39D020F169BB7FC6CCC2B71C231A27462BAA34531C3838A1C13B12F353326D870D541895E259208AAC459D44C7CFC44324AC3CC2B7610C96BFAD5806692CA9F567500894A073629D66741A3EB33F56720FC80305F60791BE328DC8C28AA10097CCC03500D1601C25CE03550974F12F8E384C8B310F32A5CA60DCCD230B89788B41CCD6825251C601B54DBEE636CFD491E5B071F8A1BB599144B402C007B80C73B3AA4E6A591AFA2F108C1BDABA34FCE43866661954A046F0D5AEB0F681602490B6118C2ABB67EFF83C349933AF394185F72BCBC80F0C1751688A42E247451DA4BCE4DC5BB5204121236623095B25E91B95E0B5C196699E2CC284B88FFB11426F8944AB174DB91484A2FC7787A97172596DBBE0AF50C68CB9726C11ABB6FF6366CC280A23D457B638A307328FB5E70403EAA029D2B121118AAAA39089E655857391C0D4A85FB7AAA84448CC12A4CE428A37D5B6DE999C7CA82879112E17EAC572001215EB8F3F07C153E6B251E699B3103855C747A6B2A1FF44ADEE996EE5F545373C972D431854DA74DD744CB3C7CCE52A31E332112D70736EE859CD47C381691C9275B8FBB46299A48BEC07506D65337BF64CC5F50E2C3A2FFEE688AF4B52A4DAB2E2E4267851AF5C2CB55255629036674E0185E069266667B9472C8DCDA95B4B16B5845940CB7A378E3CCE2FA9CCDB787415F58C30C46E691B6B6DC4A8B0C892DFBC7699599CE25812A478874568260AB482C5F69D7E773CD928BEDCE7067217C0454310B1694264E3A9D3E41300B87543B4235CF432597A5638EA41637CA894080437766C472047BF5BBFBAEA557B658174A576697613A8A73254DA332F6B232A65B9017928288A44FCF8889CE79718A329CED152E35B4E4EDCCE1B197DACC7942D59C2B0E80A0FC74FC726BB951898AA15CC1B106A383AC10EFB4D48445F9CF59C4BA17D5817618BB27879067FE2615EAD1A2BDABA6C17D422DCA06C3C4578B873B17714A6B8F2AFAF8CB445F04238C954B38B28D1028D451A25C3797872601B98543029F57671F52F8F25AE3C88144D011B22B88F9E60841C3BB52205A00EB24D3254B8C37BB845E10A14B528A8D6C1D07B7AE4E66E4F88C26D116A853B4E41ECC6AFFCA83068188189063030B5BA854541433CC454133FA2B5A3407852A64D9277883DA9AED1D982695854E8D3B114DA709E1BC3B926170C4A2ADECBCF8402A3D9253851274BFED70BDBC817DDE56A166B61DA0793AE0818159C0626BAB102CAC3EAA05D8F996D6EC01FE5E9061147A98EF4A5BA361508B385A5060D5FD0C462F340B943914D117B7EB032348003E89C3D888485E3FA9A3489CACD855F4E55864D09B276CC63BC453FF20B31FFDCA2FC3C34AEA706769A58AB6CC210551683C2BABC76B5CA69256816172BD926BA7C91A4DC12AC5A3FB087B6E1D3C0042BA5E0F98E5031A6E5077108153CCE342B1F832EFBBAC43A9C0B5A01283522271E649C3AE8C43C541C88603E58F99C1E8502DB34CF70AA48EAD6802844A7B25594308537BF657A07547CE1380AEB994F4F676FF6EA3B9A728AAE5B4E4FF34A5B68530C582194320E6BF76B997A8CF42819DE5B0D1C82491E741CEDA8AA6280094E332D95FA799FBBB769B2BF769798854899D67C1007FA29CD7245F727CB206456E4020F80A9600DE948DD2AB469C1883151A440C49570430D003C97642601CF010C200198ADB7ABC04379864C7FE6CC8BBC29CC1A6335869ACDE9DA9DFFA17C7B8747848CB7DF12B52E8C5EE7144F2EFC3ED00846B9F72598219793A6397E7726F8242B5EB92EBE2577E686B980F34859B07FD24084057B13A98880C93458D59BCD5AA68314EA18E80A94CD826FDBDAC1F05A472DE0872027C1CF924D5BDC9F236B1893DA61486757FC758806F580279919862C0333B15A66C138E4DC6ED50B17EAFABE9074AF14270C20806AFF0B58ED97631884435A55A8AB394EEEF5347B2640E6980C41A30A18E85A86074E03DBAC48855DE4E94538939A4264593B649E5BD981E0917C26D563D287A600EA89CEEA7021E754384157C8ABC5BE47C7DAF72E15787D1CE62723547B0AD86F852B150A25660A4A074E707884CA7934F9BEE2555A0F574B7F750DD293496FF681A09A4976671210FAC3C8C84A03F99F39A7B649A19F3E05A1D0E7A3728B253C5784F92CA7348A4BC086460A24B620845662246F1C0AB9BF91C8CB301D7DC362A3137643C92C8797CBA7E59A17DB9B99D08977D56A4F34AAEBC16FFBC8822822931FC73DC6BAB320972D96C16DAA41CCC89312E43275856856C4598313417C1B5261D9C1430A16C1D2F3CED08C862A21678A170A2E1A643E139873C878AA469F163C97AF7C7A1DA41034878D7E144DB826A7AE48B97A5A131185813D775BEA667CE0518B11429768F58BC30946766A6FE5B7BBCDDA7CE18C29F396307BB456AEC16FCB989F32660CA348A83FF36D1AF452C4584E117A3105C662135997142AB16CBB7C7D28560B087BA4B7B66A00563EFA3DA1C4764F8B7B5D128EC6202C505AC9446015620C359555268BF6AF74144EBB26351E98AF7DA51571F16A01B320595754C0DC23BBC75A8F036A5F6C3AD76907FBEC72530855891381B9F2B719429D4B6BC04F014F50FAB14131C60CBB279B61469E1B8664A38917CC308FE0C7AFF637DD4A76BAB66F08487739B20B9EF850F7F20DD83A1A47B53F00471A1617AD164AC1C04A8180BA68984B4F5DA9BBE1D0CA352509BC0211DA0B321789A61C98222508A03E0A6A7CF3849126435F36575D63B342FC2F02DA45FB8B5C9BBB6E064328550CBB6DA1AF16424A3AE9C1EBE566F4C4A883F966479C0BCB6585D3A2B1E14C3608E05AC5555454EBAF80E5C7BDE47B12F66118027E19F325024A5D51779C17D1B933A1CCE61003D018A3BBD67EA16B02EF997EF0678CB776479C52203875774C05B0EA9586FDE02964B8C3E1968B9940C88763822AE563D814BEEDE38A7D1653B2131558231AE3470608B51FB78CAA99C7252EC8AEAD74025CA8BD5C038283181E9381C73188C403A0B6F88B855FB5673B180F7581AD69513163379B34582128575DCA936A614A17C84C36222C3893D4B5F97829E132449490096605B61925844E31982BD601538739D8E7B378E180FA6A7EB9874CFBB8069447A96CB81169F3514CC20A93A42E0C9CC321EAC10B8AB21A893FC247BD557B6485A9C433390076573EEF854C2EF08AE116C58FC75CBC03BF8FE7312EA5305781CAC7273A78F01C73319B0B26670F26513E57660340563ACB16CEE7CE7CF0BA52671E23A44C85EB994A7A7BA15941F4C301D3B87E0D2A75D2EAAF1789237BB5B02930CE6AE5B965DC5E7277C8EEC0676C55436014AEFB8AA3B1BBA2D07B1F91487C99673A9135611BCC3B3546C42B23CE0A8099D6666FDEF62D402925B4561DEE3986B30C1C6278609496A474928D0A8351DF30C83CB4BF4BD826CB4319BD620C7AD5A7F1EBA1B8913A46D0365285A38C4C1574FA4287C02D71E91A43EF856ECCF60978C7B83099574EAE8C27A0571C4E51B320B34ED55E8B1E576E82D819925EC1B1F45E255B12DE1637697CDDD47F41DDAC13484983D75BAEDFB2EF0F95F630F41B3AF911A30E543822DFA6B7684FEE36956D2BCF8FF080C9FA26\"\n        },\n        {\n          \"tcId\": 66,\n          \"ek\": \"FB28C9266161FFA370AF3C5C163A9B187A5D2499115A1120F2B84D71948E00A7969685C8FB6B8185566ECF337E1E20AF4E4A87D1F4BEEB1BBAEA51499960683C45B53057891A288E5A8393C4F19A06C222D4C9AFEE5AA984B6798D45A3F271793E77A5404677B08A1BC5D3CAB9D9A20A4880A11B71B7D4221C1302B8BC24FE65C4C63BC7D1B80D8BB40A7FB535C672BA32496904A9976B328A2002B14E9A071713CBD9B48E55359D451C3EEC47C4A7B9CE61AA5A9516C34009A8085A98725C13949644A72A338DBB7621530858FA1B40D6BE5DA89E9A422A08031CA3C003A0E31AFC4B36D97B4508BA3537DB1B16D1A5B5A98E6BC1CB70696F887940A60498E276C1C900036A30735B20B9762A3517C136A9DC20A8F5CD73BBB047A00DDAC6A6DB40BCAE42C1D6329FF54B9C37F82CBE6214C2A99E31DB80AFDA9BAF06916BD3CF83643D27828130495CA2FC67EA441E38A8032CF0003926901245973EA549EF48A9C65ACDD5E25D5F9834CCA805AEC7CD9F687D5CEB8C2CF04EB6A1356B68AA7AB9CCE7452A20C5344F11BA6197343886A80798098C63AC5AA6095FA2A1D2B0A07195ABB3E98057953AA0B20C00FC06FE553FEDE713DD4662DBDA4C7806B44904411D85C8B9C57912A72A2D25460467CA25027BC74C3066C7A941858EE4D04026914B45354D342B6643A280CAC8C9ED60B9AE021D38303799387D12A98419C26010FB9492262002B0002FDB167FB668B2D8C453987E83F570A43AABDFA5762015B9FC196FFF6542B189395BCB5C4AE598A858597622240E01856B17511C480627B4818C413ED065C68F359266008E888B9EC435A906C6721CF8964885A160C0512FC4B9B454A10E9954048B3212AA9C37A27D2697B1AD530FB33974AB25AAA9F33B85358454610E94410D47F85920F2A7F5D13C74CA4C6108B768A476C5BACB57F17AFADB60D437C21D397A09929B2E97915254A0FAB1848779726D766CE6C83639C4172DD5A1306CCCBB1491D56C1A5FB7CB5FC7836F0765246815F05A2A13385920D0B2C518395AA997E1D33A2AF588B7EAC1DEF4BA688555EC4961596873CD0A0A443551269C73F0B97B6F8CBBDD45BC5A15356BE370FAD74805032940AC0E9C2269C0FAAC67A647FED78E83094D08F58C23745E2BA70366F24A42635A944771C3E13C8EA25B8BD6B8B0783487F1A7A4A15776DAB44EF4192880BAE74C62DBA92A24782CB91237CEDA6E84A0C68244B9CD350DA0A67CA5477EBDFC4D40F59EAE1A88E9043653F5882CF614611641380CBAB15B6CB4189491ACA9D1BC22C56928A7B37E678A4819D48A7D27887F12B4E3878E8318BFFCD93B2B9C4ADA1676BB72B93DE19295DB59909315DB1209E3A02193C1126DC30BF77C6DF59AA4EF921020253FE2045BAE6760C206A7FDDB596EF802B1F3C620D353CDB008AA0A4B8393744E25A34ADAA31D2313B480C29DCC8DB110049DF796DF298CEA91015C9686FF3A300A7298F4B7237A893E02BC373641CA88F2345CCB4A6E7441D30B244814A665F08A18146AB65C3761BC5A87860B2E20482EC89D444180D10337C45C702D82A51DD036B5EAB0FA81B017BA82E16AACDB3A96A53775B9874EE8079CEA7845C3CABE0C847B7B4BAB1FE0680B12986D821968579D963703D90AB8DE020A7CF28DA8395887B2ABA5C09BD7102B8662554D1C472578A9F80CC15F431F18980BA1E26754243102D03D42E69C52AC25EB3A19FCF57FF9E1429E78CF6AE0A501AB3EC26C0D729B142636B873E9B68E277370366C0FE0543E44C08716C4F9B06546B1CB3DD243B505B1A15976B33788E8D1987F810D5797A8E9CBA8B126923A1A81855151780CA9CD4055D9495603A84CD7886BBFBCAFF499066496C967501DF3D861B97C5D250016BD3B3A18946C0CACAC1238CD235A6A609611EAE792E0560B831B636309517AF489A30269E172BBE7470C2D158F83C4B20F15C524025D6CF164200B3870663809A00E6A982835C87F80A68AFC1B5393B006D220A0AA26B9666690549523A0A9711E03B2419914FF76CA69A62671260B34F1C1A11A6E64067908839B87D2940D433162393A6F65B08E4CA643201023C63DD3C3808E9625CFF4C346491E34153937589310437069622208C646AB58584DF10F9930BA2F761DE4C2564BAE2B31A9A645135536FCB58B94489E4D993C9FBD4A89198AD91BE052B5E8FF\",\n          \"dk\": \"A6247A7F4938C4589C972C928D7C65B74634ABE136E1C732A094A539E4892780BF56A5A7CA52AFFAC643B7168B997809AB95C8DD4740954164819C4D94453053634316F3186DCA4A03E813BCF763E6436E05DA840EAC0FB65B11D2D97EA80B156966C5A0DC43799B74F3797D483486E4647C8EA61569880774D68C9EE2A5907910839A95E8E33CB10B2AF59C22128B0C8BA9AA7D0B7A48E26C6E157D58122A63E571B059073AF35AF40282D1C8AC2ED5B15B2C980D6437C27212B5CBCDE888BD8F4B1B3AE398182243468C98F453569C417CF3C043BE9A9BBC03D058212523F0A6A2A263381967A562B63E7C1A83A4BBD1D86226B5083F8B54D93413350B39270C9E784430AD30752ACB0CE0F2B7300B478A3801C1133A84009106DBA42AEC03AE413F137812A35B4CB29207A7FC332BD07064019AE8CA3F6A723191058D88A460D40522FFE0841AD5507B1942434939260228CD993266DA1C35B920012C0E349C224D492DD6D197CE7473FA25187AB87C192A5382FC3DB61C817DF62221666D6D4858F807C852853EB1AA8A7DFB4A79935711698572E12485294DF538257F44AB92A60D5D23AC91F56E138025BE0C93F995BB3A9620ED0A7588857B4F847F996C9D7DF40BEB05C87FF79BCBF222956CCA23F377FC36140FA379CB89365532490D470C3C5B4CB8F3560A7A0C7FF9A19AA0B0E03C100B5090AB42A9C652399FE79716B61921DA7211ECABE486A3E95B3EC0E79A4DB614F5282F2C8087F8A1CCEE9417D4F99C108202C92499DACA10E3AA9B46D254BD8593316B9D220A96496372DEE61B51E93D51E9AAFDE006AC267037044E92A3440A469BE8AB36C3A70B070C43BF889F1DAA6E975CA464F32BBA725D75016B86C8089D9338C9816FDA6220169CBD445C7C78136E9F56A431957068373DD3480F7AF88ECE4941A2FC3BA9C2A474FC1D583A59AA42795365B6C0954F3AD810F55B5B0B959F414137B7C3AC0351812E9B00E8B6BE2953C7AA839F24F02D8CDB356CC64B5A361EEF29A2FE12117D9CCB86D4A399F51CA8D796A29778474838D903830706A854E83BD0267C7CD43B927B83DBB29C63E32FF5ACAF1A2A0C50609F4381C32E78B583808841F6300AC40302CCAB18A945FD0CB510588796C61540FC559800A7A1657736B347C50657E559685A226B19F62B34050FA4D76CCEB3A9BDF5A3815A7F4558AAF108958806C16A264132968DC63CC40342239E7640BD394CF418CC3298B7ABE9CFEFFC1714E96486CC73BA380E5A79900AD19558C68EF1BCC3DADC3E7829BC7BC09F407C6384E51620492E5B643B10B45E3BE52CA851709582C9DE447A501A07026528239054901882F5B74E6DC791AA9397DE9403D5519F1368084EC65D8010390F23C7DF604CAD8092E950901F9860A267578AC5043773616BD1792307AC8626201CE2A4FD5613CD97A074F3476094BDA4EC32A161855A4888223327978A8F74625C74A572C37C027E619C7D5577335B3D61E3B53191517C5A50FE62C63A08B6ED65C054EA98CE22C7464CA2DB3C23724C9868007133A235A36A7C56BA237FB58237F7C61E4A66F7B51A2751CF3706367413AD57A01B687107A513A46ED70025B41B785354BF986D4687369F97759D12551C6BB203A609F28931DDC076E6E38FFD083EF4E99B8C3919DE18BC7601260CE74EB75779FD1C6EDD4C843F770F6504B4C468A80C8C3C37D80902C736A15539406A7BF01206173066D29902348708799C2A7C88B751D62BD2760488357836B4019DA55B4A3050242791EF3C94AAC0A821BB71A90840B7E06F842398CD50282A15718E29BB4B75796AE81C76503629056CAABB2A0A319761C10EAEBA23BCD3033D911ADF754C25EB3923331BA0C332CF357758469D765A2C492565923872FB1322F5E1672E010DFEB7815B940595CB4FB6B64FDE59A63C689BE4B7828458B31BB5103EC4C8F1F905FEEC2AD1D10C7689C5C0528A56E4444C958107868E477662C25071E28404AFA86411910D33B618276A5CE51850F3B67584949F75651180298E3C791ACA85590E6BC59C0C8365DC6D5AE02D34D91B1E6C7BEFCA8C7F042070E32BD4883ED1D15128D516A40235A086A6D385784C457FEEB1C704C426CA4C48155445E414200A7CB6A0B9C0DF343CBBF95C7655B4FB28C9266161FFA370AF3C5C163A9B187A5D2499115A1120F2B84D71948E00A7969685C8FB6B8185566ECF337E1E20AF4E4A87D1F4BEEB1BBAEA51499960683C45B53057891A288E5A8393C4F19A06C222D4C9AFEE5AA984B6798D45A3F271793E77A5404677B08A1BC5D3CAB9D9A20A4880A11B71B7D4221C1302B8BC24FE65C4C63BC7D1B80D8BB40A7FB535C672BA32496904A9976B328A2002B14E9A071713CBD9B48E55359D451C3EEC47C4A7B9CE61AA5A9516C34009A8085A98725C13949644A72A338DBB7621530858FA1B40D6BE5DA89E9A422A08031CA3C003A0E31AFC4B36D97B4508BA3537DB1B16D1A5B5A98E6BC1CB70696F887940A60498E276C1C900036A30735B20B9762A3517C136A9DC20A8F5CD73BBB047A00DDAC6A6DB40BCAE42C1D6329FF54B9C37F82CBE6214C2A99E31DB80AFDA9BAF06916BD3CF83643D27828130495CA2FC67EA441E38A8032CF0003926901245973EA549EF48A9C65ACDD5E25D5F9834CCA805AEC7CD9F687D5CEB8C2CF04EB6A1356B68AA7AB9CCE7452A20C5344F11BA6197343886A80798098C63AC5AA6095FA2A1D2B0A07195ABB3E98057953AA0B20C00FC06FE553FEDE713DD4662DBDA4C7806B44904411D85C8B9C57912A72A2D25460467CA25027BC74C3066C7A941858EE4D04026914B45354D342B6643A280CAC8C9ED60B9AE021D38303799387D12A98419C26010FB9492262002B0002FDB167FB668B2D8C453987E83F570A43AABDFA5762015B9FC196FFF6542B189395BCB5C4AE598A858597622240E01856B17511C480627B4818C413ED065C68F359266008E888B9EC435A906C6721CF8964885A160C0512FC4B9B454A10E9954048B3212AA9C37A27D2697B1AD530FB33974AB25AAA9F33B85358454610E94410D47F85920F2A7F5D13C74CA4C6108B768A476C5BACB57F17AFADB60D437C21D397A09929B2E97915254A0FAB1848779726D766CE6C83639C4172DD5A1306CCCBB1491D56C1A5FB7CB5FC7836F0765246815F05A2A13385920D0B2C518395AA997E1D33A2AF588B7EAC1DEF4BA688555EC4961596873CD0A0A443551269C73F0B97B6F8CBBDD45BC5A15356BE370FAD74805032940AC0E9C2269C0FAAC67A647FED78E83094D08F58C23745E2BA70366F24A42635A944771C3E13C8EA25B8BD6B8B0783487F1A7A4A15776DAB44EF4192880BAE74C62DBA92A24782CB91237CEDA6E84A0C68244B9CD350DA0A67CA5477EBDFC4D40F59EAE1A88E9043653F5882CF614611641380CBAB15B6CB4189491ACA9D1BC22C56928A7B37E678A4819D48A7D27887F12B4E3878E8318BFFCD93B2B9C4ADA1676BB72B93DE19295DB59909315DB1209E3A02193C1126DC30BF77C6DF59AA4EF921020253FE2045BAE6760C206A7FDDB596EF802B1F3C620D353CDB008AA0A4B8393744E25A34ADAA31D2313B480C29DCC8DB110049DF796DF298CEA91015C9686FF3A300A7298F4B7237A893E02BC373641CA88F2345CCB4A6E7441D30B244814A665F08A18146AB65C3761BC5A87860B2E20482EC89D444180D10337C45C702D82A51DD036B5EAB0FA81B017BA82E16AACDB3A96A53775B9874EE8079CEA7845C3CABE0C847B7B4BAB1FE0680B12986D821968579D963703D90AB8DE020A7CF28DA8395887B2ABA5C09BD7102B8662554D1C472578A9F80CC15F431F18980BA1E26754243102D03D42E69C52AC25EB3A19FCF57FF9E1429E78CF6AE0A501AB3EC26C0D729B142636B873E9B68E277370366C0FE0543E44C08716C4F9B06546B1CB3DD243B505B1A15976B33788E8D1987F810D5797A8E9CBA8B126923A1A81855151780CA9CD4055D9495603A84CD7886BBFBCAFF499066496C967501DF3D861B97C5D250016BD3B3A18946C0CACAC1238CD235A6A609611EAE792E0560B831B636309517AF489A30269E172BBE7470C2D158F83C4B20F15C524025D6CF164200B3870663809A00E6A982835C87F80A68AFC1B5393B006D220A0AA26B9666690549523A0A9711E03B2419914FF76CA69A62671260B34F1C1A11A6E64067908839B87D2940D433162393A6F65B08E4CA643201023C63DD3C3808E9625CFF4C346491E34153937589310437069622208C646AB58584DF10F9930BA2F761DE4C2564BAE2B31A9A645135536FCB58B94489E4D993C9FBD4A89198AD91BE052B5E8FFF2F75EA69691E4E53E952F98536718602B96B7E5A2FB218648F9353EA65FEABCDDD4871080BD4F761D972085851DE0A0408A2F5EEC3CD3786297A782402CA440\"\n        },\n        {\n          \"tcId\": 67,\n          \"ek\": \"DD0C664328C53E5B29F9A9CC55D97BA265A18C02AB73427307E78AC23C51A56478CAA21DB400A3AB081A0532729C40BE3FF9ACA2DA1799E29BB2A8325F1A9E7BB99D44F3C27427816B3BBAC0FB3DB6B23B7323A8CF4587D505B1D1FA9D71696064CB6A7D806872D68040714D94162658780295FC056B269D4F16AA4D98A5245C7B33462129130425712798C879079217CC3195E5CA67D137865181B6C2F92EAD04945E97720DE0CF7531BC46847D68E3C13840B94DB2242A048F0F22331050950B8A4B3FAB51A487134A2C9A95FB659342867D252FAF091D1D966836F1CE964428C63CA3950BBCC3D8A800191FF628680D23A8EBF6BE727225E4B91602E46949889D910CC46D6418411B394AF072FF49B894A1586AC7C1823BB89AD2997C726C3FBC65609CC37656BEC80A6B10336A346412FE34075DA69D829B8B937A87F76968BC93BF5D5639AD76448E084DD0D02AF4A7028D7C0892A9728CF1114622494D8B28D05065E6DA0813043504293A17D29273572E34A14816A01F5DF58BDF44AA1AE8CD0C6C6622CBBC07540405D5AAE8C77BB3418A91B982BAAB4F8B388224EC3E345764D9E66C2AFAA27B924A07039720D7B73084098457C488137B75339F1D7A6919A29FCAF71F4DB8A17AF6161488C02813191AA2B98FA51A6FA07BE8F288C51B538292683A4C52C7F7B2863C52C0843452B583CB583B98414B4CF97185082768855206871CC76A0A409802E0DC23036B3586D9991D241FDCE67D4986CECED74EA7E77BCC479EE4768952E0297568528AA1712468B267D474D069CAF9CB11E5B0CD23DA09D5095349E3614194B1C63596E7E65BEB6769516B7B0759C8FEA02C7AAA1466A7B41C759135965ACE8857074B95DD6A5ED9D36FDABB83341A841DB163C1332881322F5D2374EE11ACB0616A6B1CCD9AC7CA6673619BCCABAAE562D4C433147A2802C26B6431BDD73A455CF8CC242174AB5123BF9C26F8280E5755A4BDFC1A98CBBB92C152FD11655EE4459ADB2F163928EAF24ED09C72839145C96179D8C94E6B3227AC793F5FD990DD638ED150020934894684A38BB1AA859A3AAFC49EC5A53B8646CF60F5585F043CD71B13F9BA41DB563677B04EC6D8270FB55C78F8A58ED494D9348B4A774930C00752121081514596D5B86FC0CEFEFB4AFD2682EE056BEBCC0CC9446CC3C1A0F5F34DCCE8AFF0949FF16005D121275A5915C6D4B9407357AFFAA09CF6783ABB260FD53A333879433B45FF25B9D71A500BE315D2635DD4F42B47881803666441871083859D266B96C1327F96B40454D17ED5363A27CB612A69064B268DA9185B4506119D7A71FA1386AF6BA9789AC06E9A0F04DA5C6CF478F18044457418E0FC986681B81BE1A7AC1583AE95155DB64FDCF442E5EB39515B06DB280A23AA5DE2C19A1051A4F9404F712955ADD66A9D1A7E963BAE57BA75190B7447B70EC0170EC0C2CEF480132B375D3991B90B729F9A4356F062C00D030BF8AC4A82032D5F42B7D47B6009B274C4D2A5DD5553D29AC86F148ADE931A81534226EAAD33612AE89BAAB7729F3855339625742AEBAC2B042AF2F5B4F3A017A74342501C274832B314BCCAADF59DC52A0ADFC061F2256875D960CB654CD78BC46C709B2B9133F8801CD6766800E12516D80CA41011D68733901AC540533E0CE532CC82AEDB5CA0DAB476CC5A81188B54E0AC9FE66A1D977A6631519810128F98C3044BAA5991920C381BCF3D63B785701421733FA25C521DC10BA007040B917E55B863F95A1A3E924D3FD0AD3975662D38476BD3B47E46769820C3BF4705DF9A89DAA53A58AA77B65258A444B2002B681031BA765659D9C18D92D826733BB575705D6D7C7DE3D19240339AB4B12BBAD51ECF8638E6456E5CB350FD551E5A5C67DF050DC7E7866F4A10989C40BD3C22FE480343E07DB22734CE89870B6804273447A85169A170BC53B861B9545CC186501E8A541575A480562E5D7198B4D905B8EA51A48688A8E488A2A36F7EB15AAA398E6BF44B69622A14B3473B0AC09401223CE775761B8931D53D25E0AD51426970403B0A243E9307502574B1D6323C11F7CB5D855ADE147211438AA7FAB0DF89CEE8286618D8181DE196A311390BF171EDA88DAF1C0FBF540EF20504209482BF3B7D023100D8795634BC28CCE86A4131048041A23549B016EF00A31156458CC5808590AD8FD2EF59DFCC0C\",\n          \"dk\": \"73831E2C2993800A26266635B586921217B9AF812D5656B9CADA071A384F69D48A210660F51C4AE1F2A6F2E18AC7FC2AF9901318A56E36A483662579736943B7855BCD664BC7441FDEC83DAD5C54187A2FEE338DC9A4AA55780F07F3A94ACA4DAF9145FB099F05648DAC3C19EF450EFF4633500B9205424C2BC4081A017D52BC3302B28BC53668185B147B117C9A7C427249037CC165134B6A9A4663CB13AB0DA3B501C67866201485C5645D4006F929AA4A68831038961B61C950110106CC98C03287D291B080A7212BB4A7AF92AB3A2A06EE19200A531040949C5D57B2D8B09D637638B3690C4F1457A1E9A5BAF903DEEB894300984B76A03F0116D570CDE59981F10A2DD8590EC91765ADF252183C10FBA093004D5700715215427F2FC85D700747C8029B9CAC9A1A4AB8E0641D124540DCC3C1DDD62DBFC89D884C7DA2E515CA87A92DC99B1BA97B0596153EB53D4B8818D2644F638545D94258F5441D5C940A7CDBB3B3E66D3A886457316B15808F0AD71421C34266073B1500CE5279315A00A17FACAA04660A556266C3F44275C67B49793CC0679DE3B29BDAE8C9B15458AFB9B2395547C3DA2FA6010A8171001A50863376C172E64F51F87354C99465796789A854F8362C78588DCFCC98250C4524E51A401203E5D4680C5261CA3440C3077CCC6AAC3E809315781C9081629C4199D925679F02B70A200B3F8C98A3E0B1D3B0CF15442E55A76EF25C0B3734993510B7465C0081B82BA8136C7251C40EFAA6E0819046D1CBAE353A9C8547B9E160DFBCCE85627E2B16C0E5A51DBB50565B3A81DCD27FC9698BB742356CCA03CBA3678028534FD0B1B3C852F734ABE2452BC583A103C3C09445396F1A01F3FCCD951299509B2EE2A7468F15B927B0B2D435B56AC12C548C8107EA5D58682B481A568DB4A4B1209E9E36B879D31876F708A039A39DF58A0F0A2C04A19622C359FD395C99B3C5F497304CC11F5FAAB83842B7DF06344657243968156AC67E7938900D636BFB1C4224F677E6D119827ACBE0BA6E32C590AAFB140A041714B5BD3E06A2D7851BF91098C1647133118CC747AF18B8ADCF01C530C91E805966427A3EC4234272BB9D38A8908285C46EB759F176AE6B7395010878089A21C3FB5132AA789B92C7724C8217E9A5E66923552C4514A915C0B43499F8192324A50BE4679C586A646CB0B9808659792984726EE2420B5FB102B96A3D7898B7C98900913237D5D8885B9580556549C1B59CB4F4ACDB481D92A8CC5D447282F5C7BBA6BD0C7CA0D8EA2D3BDA78633C8FEA1561DE37AE94D51C24414F9178C224A92E79138FCD0337BF00121A51BCF5D27177B0CAD3A6665EB77033B10A00037B908062D381AE5C84C711D4B5CA9888180A0FA742B506D86C03A0C54CA488FA404BC0493C47D531D420360DEAB873C000A87340A2915073429C001A088B212EE46A80852C3B839561CFE5A336557EB689BDD3CB75C70C6FE279AB134A6ED3B20A03750C1CC30BB1D15FCFB37105B34F34026B660905D640BF06D097F0C06BB7E6C628BBBA9386CE6AC212A3370AC4FA65C64C5B235BAF13011E7928B896D78754335E2EF90AC5DB53DF83BFC6340D85D444D469C3B9A329567243E1A2B900A76C756C46E26C6936A0C772F88AA38C03D2137E2BA4179D228F04229F5AF755B3C7128AA16BFFB9354395B575D81EA4F266407A206C0CAF14D5151CC15C178B7D567B8C7DB637FB26A62C048CE9049710B07AD28B6168756234539952282D521313AFA65959432E7235C9991487AAE661B6B7108D48C9E4760D6315C14C1725CCF69CE69C2E407799DB8507A0C66CBE15BC65E4692CD5910B5975A3E3416204949C601F55A438B104026D36A71B5B1399BA6DED58166F210847D863D302AB4C915E969317D806643BB85912B9A0C07AC80FFB588ED86B359840D8B58EEAA0494266A11ACC201C6130DCD523A2C5CE2262A243665AAA45BA30D2CB06D4850CF65CBBFC772E0C20F9F652AB92950F0A3B33F448D6C014F5548A241B53B2C348E494C883409B99B257367BAC0CD42037921F8298647D91C06FB1589A17C1D0A99458938570C871B7C865C98734E1A88C4C112294913F3F0A35E8A26B17F85AFE567780B919C0D94C6ABC5D6EF1C4318A6EF21A1F8FA75653F612DD0C664328C53E5B29F9A9CC55D97BA265A18C02AB73427307E78AC23C51A56478CAA21DB400A3AB081A0532729C40BE3FF9ACA2DA1799E29BB2A8325F1A9E7BB99D44F3C27427816B3BBAC0FB3DB6B23B7323A8CF4587D505B1D1FA9D71696064CB6A7D806872D68040714D94162658780295FC056B269D4F16AA4D98A5245C7B33462129130425712798C879079217CC3195E5CA67D137865181B6C2F92EAD04945E97720DE0CF7531BC46847D68E3C13840B94DB2242A048F0F22331050950B8A4B3FAB51A487134A2C9A95FB659342867D252FAF091D1D966836F1CE964428C63CA3950BBCC3D8A800191FF628680D23A8EBF6BE727225E4B91602E46949889D910CC46D6418411B394AF072FF49B894A1586AC7C1823BB89AD2997C726C3FBC65609CC37656BEC80A6B10336A346412FE34075DA69D829B8B937A87F76968BC93BF5D5639AD76448E084DD0D02AF4A7028D7C0892A9728CF1114622494D8B28D05065E6DA0813043504293A17D29273572E34A14816A01F5DF58BDF44AA1AE8CD0C6C6622CBBC07540405D5AAE8C77BB3418A91B982BAAB4F8B388224EC3E345764D9E66C2AFAA27B924A07039720D7B73084098457C488137B75339F1D7A6919A29FCAF71F4DB8A17AF6161488C02813191AA2B98FA51A6FA07BE8F288C51B538292683A4C52C7F7B2863C52C0843452B583CB583B98414B4CF97185082768855206871CC76A0A409802E0DC23036B3586D9991D241FDCE67D4986CECED74EA7E77BCC479EE4768952E0297568528AA1712468B267D474D069CAF9CB11E5B0CD23DA09D5095349E3614194B1C63596E7E65BEB6769516B7B0759C8FEA02C7AAA1466A7B41C759135965ACE8857074B95DD6A5ED9D36FDABB83341A841DB163C1332881322F5D2374EE11ACB0616A6B1CCD9AC7CA6673619BCCABAAE562D4C433147A2802C26B6431BDD73A455CF8CC242174AB5123BF9C26F8280E5755A4BDFC1A98CBBB92C152FD11655EE4459ADB2F163928EAF24ED09C72839145C96179D8C94E6B3227AC793F5FD990DD638ED150020934894684A38BB1AA859A3AAFC49EC5A53B8646CF60F5585F043CD71B13F9BA41DB563677B04EC6D8270FB55C78F8A58ED494D9348B4A774930C00752121081514596D5B86FC0CEFEFB4AFD2682EE056BEBCC0CC9446CC3C1A0F5F34DCCE8AFF0949FF16005D121275A5915C6D4B9407357AFFAA09CF6783ABB260FD53A333879433B45FF25B9D71A500BE315D2635DD4F42B47881803666441871083859D266B96C1327F96B40454D17ED5363A27CB612A69064B268DA9185B4506119D7A71FA1386AF6BA9789AC06E9A0F04DA5C6CF478F18044457418E0FC986681B81BE1A7AC1583AE95155DB64FDCF442E5EB39515B06DB280A23AA5DE2C19A1051A4F9404F712955ADD66A9D1A7E963BAE57BA75190B7447B70EC0170EC0C2CEF480132B375D3991B90B729F9A4356F062C00D030BF8AC4A82032D5F42B7D47B6009B274C4D2A5DD5553D29AC86F148ADE931A81534226EAAD33612AE89BAAB7729F3855339625742AEBAC2B042AF2F5B4F3A017A74342501C274832B314BCCAADF59DC52A0ADFC061F2256875D960CB654CD78BC46C709B2B9133F8801CD6766800E12516D80CA41011D68733901AC540533E0CE532CC82AEDB5CA0DAB476CC5A81188B54E0AC9FE66A1D977A6631519810128F98C3044BAA5991920C381BCF3D63B785701421733FA25C521DC10BA007040B917E55B863F95A1A3E924D3FD0AD3975662D38476BD3B47E46769820C3BF4705DF9A89DAA53A58AA77B65258A444B2002B681031BA765659D9C18D92D826733BB575705D6D7C7DE3D19240339AB4B12BBAD51ECF8638E6456E5CB350FD551E5A5C67DF050DC7E7866F4A10989C40BD3C22FE480343E07DB22734CE89870B6804273447A85169A170BC53B861B9545CC186501E8A541575A480562E5D7198B4D905B8EA51A48688A8E488A2A36F7EB15AAA398E6BF44B69622A14B3473B0AC09401223CE775761B8931D53D25E0AD51426970403B0A243E9307502574B1D6323C11F7CB5D855ADE147211438AA7FAB0DF89CEE8286618D8181DE196A311390BF171EDA88DAF1C0FBF540EF20504209482BF3B7D023100D8795634BC28CCE86A4131048041A23549B016EF00A31156458CC5808590AD8FD2EF59DFCC0C3D74CF5CC0859F5089855A7EA2267CDBE04185599344C8E93EFCB5B3DC588FC6FA29BDC28D989B8C4BE84706A3CF21B36A1C6E355C88A361C7664818E4BC8E03\"\n        },\n        {\n          \"tcId\": 68,\n          \"ek\": \"E067AADA829618E4C683C404EA51ADB1A7A116C7A0AAEB995AF1BAB8D0751FBA714957AD6C7A0A9C73ACB8D347F9A9A469D85DD94B1B1E4AB74C94048F9AB578A2BDFCC75A213A65345129F78B26E3D901B3428A0F87952B91517460930386856AE25D00259D58EBBB403621335791EC6C2C6EC50711C07F56714B0ED3753F7459F07B83D1F838CA1875017B24FDC2CA6224B74423842ACC5C4B03D0658C7EED5A713A365CF523C9C2254E9C79059331BD730B123F53538955BC0FBA92FF9388192C950BC46C25339C82E98314D616B50993BB6241224AAE7EEB610EDAACC43ACD775941FD8088E03931F40C999D92360B74B86B857CFC6B5A74367DC72033941C78484031FFF53874308D52AA6FFA3491E0CB98366B8FF2866ACD1AC64B63770BC25BC33531175A4112D7684C01A8C4500F9FC083FE652FF78910B75093A34CADA85947D2602FF0716624FC9617EC71B9E87CACAB31CB3836A022523C3A3BF9F542EC306C22EB815E1038A48CC4F6596A1CB1482C7BADD7390D8DFB1FA01C5C30E458AD31BC5FA717503C0186F5A3B936C2D0AA7197733C2EB708F32B9FF611537773BC38B904CECB5D2BE29C988B22E415C7944726EDE09ACB76175448712F4BB8AE5AAF4498CF4189B0F0332FE774170F343A121071961CA130694B964033BC015F490B85DEA4A81756A95F3526A856327CA68F66E828A66B138C052115C0B6EB910BA703581617299855318BA9AFE6B152CC538836278D781771FA4C033A65AC5630B3EBA8B02A69717FFA1E73E77AFE06441D91CD45289927C40A1425AF113B179CC46C6606CA0EEB0D86765128A726AD50AE8DDC1AED2A07129ABBB3AA67B4E0192DA99D2C36686D3807E0B50CC9E30A82D97B0895B6C7670C01FC214DA38FFC54970D8399EDE259900A081954172F385E037C9B72B43A3ECC7FDB0510F6A930B837618ED756A62A0B3CF58B980C94210BCE63FB9CE88745FE8BC34D57248979398CB0C1C9B83AC979A107C955E97543756221AFF06C553A1F0E764AF19B93E0DCBBB9C327F76AB4BB890D179C90839430A749A2C43A66DC36394FF06DDC54070A2A2F152261E79597AC9B2AD32A39FA489382718E04DA6369C4AA0D4823BE87A60561CF49232C8AA20811EC7B29EC8A90A363F3B367CF9A81A08645C70A17EA41A1AC4C0E9407763D2A91B6732E46C6BA160972ED35CBE6000B7C1B83CA21623859CFB4317306593003B2A90BE01FB5A4030CC062C308C14924CA47A24FA5D13B82FA7153988DCD1077E2759FF250C198ECC4C860170E93BCC48CB009CA7535C62EE2A3AE61D07C828C68D19A55F6A7224F716E77A381E06933AC03A1B0D01B6D231E38D4A19EC9C6E8DCA42C218B290130BCB583F7E958FC3531037716B356948D726E602329A322A410945568BCACBAB75D65DCBB38B80CD20865563699838659E065CE749A6DF66542E8474BD140B50EBB5983F546FB393A0BF10C5891767B935E0F5171CC97A00607332088AED99B13EEF557CEF73429704244FA2B915CC7731BB9BD351FA131A726C837FE19047443A16BEBB02F4A5020878AFCDC72CB780485A793CE23BC6DBC11E04B0DBDA82F51B4327814471C01042A1119D5513408C119266A0476DB89B20B18EBB44A231CC5BC39C6BCFC4DD2A61EF9180BD76327AB05A37ECC4B2E8ABAAA307A6C58040484BF73B06AB4A65F7FD99660A759FA903A56DA3E8DEC389262AFF8BABE1B85168BD21762C86046290F35D51E4BCB8403F160D9DC4E00CA8987E46CE05A5FB04A112EC352B1B03219A4B50345C6F31915FEF4C2EF870D30A244EA4C20D82849628CB61ED677AC9890CB5463EB90BCCFEBAD9FAB23C26A33E6667D76C96220147685EAA84CCC3140104AAA4A559E51209DB124337A2795925C5E0230E7920D4E9338DA23ACCC8693D5952CF49AC26A74CAA6E05011E6385F787A7071CA08C61F59CA017ACBC15E9BAEB78837B99B93908A8CD6D7BD30CB6C23F9CE5E15204E0225B55C120B31C1276695C909C94AA1B5D0C0496278435352B50343A9974A55EAB5105FB4436570181F2B7AE82238CFB9C967F424C1D146BCF74A2349583D36C1F7629E70E0C4702C133F7475985B2DCD923465F39772E8AB76F3634C6A1F6E8088BBB0BD3A050CBF323F9EB76BBC4603617908BE85E6EA5E40A862180D9EF1380B7A859947562EF3A845669146\",\n          \"dk\": \"DDA5A4C182BC2917C5AEEB973FE853A397753BF576B753AD82583E6EF89CDE2B7B9B6B6FC65A349F66BCF4903902D22418E123BE13A3E6A29489362308452991448E53370B9F1315A83540D14A0CB60B9C80549556CB6F839B6062365693CA9665A01CD66574242092954B945D996ADDAA51F106A454002491FC0881D0489722979D90CEFD5235B9A1852D334B6ECB7CB3294C5B50A951E835DCFB24EF9BC124601567C0C05E1C194753CAB73A73E114C91C0341C7DCA0C904217F6BA200752FBD0B74E5503D141C17338AC6B6110E78F5592ED1B599B3A8FE65292779A473D95B3A1860DE955ABBC99349D0A4F85C97A32A154C89AF603799A438890E692370030AF4B69A3AFC91ED283EF4290524CC88E6E67D9CD00790F8B3E8626F528AC4A236570218B1710B82A86A308B4C4068E07422643BB8031E0D556700123F8F948463291257280CFCE2749812AC26A31C145667E9E94FEAE011C037C00E268F617ABC5DE656A8F8C858CA0A392201E144B9101BCD29401C98992E7F4ABDD02705E0F0AAE6D2BF2BF9081813CB1CF78454535146FAA994C666E628A4E1FB15CA5663551962380727A5933F91495B4EF290DE64A1D6D35CFD4A5279EC0460622C2E1B8409619D31144E674B4AE08951B25416F2839AFC4095D7257F34D54D3742997B77893C1C009C55A715D13BAC3092B9911F1BD962645C1A0A91639207BB98E15A37B570E39AA15011280E8B64B2E674CEC8931D26587D003CFC89498C997982AA60FCD8A3ECE93D784B0F9E7C10A6C380B76C1D6E91ACFD4672D4D9485B9C857B92C283B596D714A404444478117FECA0C14B641A0DD1B027DB7BF832316F0BBC256324BD4345C0A7CF3B98B2E6D01B1A9644F29010CEA70BBA08BE6877BFB1570CDC464A049C90C2B28C0A329904C1CFE6C92F076098C300288397CA65581A3BB8BA7D92A3D22C24E8691CFC610C9D3409B73CC2CE78A1C7E5AD07720CC30C984F73272CE4B43541AABF616714400D6D040609286AF1330F342A71BD3276454B3C33B5508817644DF14C42E93199E27FAD15309FD93BC8F88DB9078350797BE9CC027F03A7A18965682C83DDF182C411CB20EC3EF5C435973714255CC8CEE376654CB1AA6554BAB32F7E7587DEA22EB3C2565E760F503B6F103A54B2191FF3EB6E30126B951091B010BCB60C0A8A1C3FC958BC4DE5504A3C50BB6A2A5726B6B8E25024DBB7C02BCB2FE31D7CC581E42185FFA6503BD0A0D1C907FD13302EC37D191C141CCC0BF4D7A371A8C3389C5A799663709949B17195B4EC74B2DA225002CFB13668F83A1092A6C609522C07529BEA7A454EEAB74E15246AA17452451672A6314E5AA5AE0C57C2AB74653005A8F2B122859C0E8A0829CB0240BC0B5213AC9B035E9366BD3A4BC680D3CC45D96EDD33AFA02C747132001C0CA4FEAA29059AB0C1ACB1979B93C087B53911897B928422D0732E134DA0186794B4CD2E242C0EBBB5A7E10A82C44F874B749ADA0F91C6A26290B1CC49B7BFA150EE40886C9ACC8DCA75F813B13E732E94F85F6908C0523CA70C71A5BD394673D22E4648A443B25DB6DA4EB11C50A8AB30CB342A39B130AAD1A421B567601001B86ABA44A1311F05423A939B3F3404E5657476819D9E406FC265935542B48AF47FA78482C4443697358E784BB72B54B373E813E19186D5BA8997053189B314B5BB8DB699059CEC5FC2D58191B2A729D4A3BCA3AA7B377464939CC8E548A7789E56F67D08C1A0772C9836102321F659FF7B088E9968661609003DB687A295FB2BC6AA593DE4C9CFD6C55D2395ABAB916D3A8559451A6BEA299519654E8C8CBB56E455A85C37CA5A74FBF25252812F16B1B9CCA649718C00DDA596C506BB525159659C272FC1C7B5EC8272DA85D506959B231D1394A71D2C421E743964A58F38D52A60407FB8605961A63FA394431D35641991CB17DA5637732A5ABB2F34B81B7384974030ACD43886021A50B70924606BA1A295A489842FE3D10E92FB1507E240AE0B5616F8AA05299C94301E29A638A742028F9321F3955C61A59C021168FAEB17829025071B4015FA167FB6220DE0B29AA3BE2E88A171E32C9499B9B00BB867DB0C9915384F0A49BF3C5CFE539D22D3B998315CB8D09191634C248495C1977837A7B76C9B35E067AADA829618E4C683C404EA51ADB1A7A116C7A0AAEB995AF1BAB8D0751FBA714957AD6C7A0A9C73ACB8D347F9A9A469D85DD94B1B1E4AB74C94048F9AB578A2BDFCC75A213A65345129F78B26E3D901B3428A0F87952B91517460930386856AE25D00259D58EBBB403621335791EC6C2C6EC50711C07F56714B0ED3753F7459F07B83D1F838CA1875017B24FDC2CA6224B74423842ACC5C4B03D0658C7EED5A713A365CF523C9C2254E9C79059331BD730B123F53538955BC0FBA92FF9388192C950BC46C25339C82E98314D616B50993BB6241224AAE7EEB610EDAACC43ACD775941FD8088E03931F40C999D92360B74B86B857CFC6B5A74367DC72033941C78484031FFF53874308D52AA6FFA3491E0CB98366B8FF2866ACD1AC64B63770BC25BC33531175A4112D7684C01A8C4500F9FC083FE652FF78910B75093A34CADA85947D2602FF0716624FC9617EC71B9E87CACAB31CB3836A022523C3A3BF9F542EC306C22EB815E1038A48CC4F6596A1CB1482C7BADD7390D8DFB1FA01C5C30E458AD31BC5FA717503C0186F5A3B936C2D0AA7197733C2EB708F32B9FF611537773BC38B904CECB5D2BE29C988B22E415C7944726EDE09ACB76175448712F4BB8AE5AAF4498CF4189B0F0332FE774170F343A121071961CA130694B964033BC015F490B85DEA4A81756A95F3526A856327CA68F66E828A66B138C052115C0B6EB910BA703581617299855318BA9AFE6B152CC538836278D781771FA4C033A65AC5630B3EBA8B02A69717FFA1E73E77AFE06441D91CD45289927C40A1425AF113B179CC46C6606CA0EEB0D86765128A726AD50AE8DDC1AED2A07129ABBB3AA67B4E0192DA99D2C36686D3807E0B50CC9E30A82D97B0895B6C7670C01FC214DA38FFC54970D8399EDE259900A081954172F385E037C9B72B43A3ECC7FDB0510F6A930B837618ED756A62A0B3CF58B980C94210BCE63FB9CE88745FE8BC34D57248979398CB0C1C9B83AC979A107C955E97543756221AFF06C553A1F0E764AF19B93E0DCBBB9C327F76AB4BB890D179C90839430A749A2C43A66DC36394FF06DDC54070A2A2F152261E79597AC9B2AD32A39FA489382718E04DA6369C4AA0D4823BE87A60561CF49232C8AA20811EC7B29EC8A90A363F3B367CF9A81A08645C70A17EA41A1AC4C0E9407763D2A91B6732E46C6BA160972ED35CBE6000B7C1B83CA21623859CFB4317306593003B2A90BE01FB5A4030CC062C308C14924CA47A24FA5D13B82FA7153988DCD1077E2759FF250C198ECC4C860170E93BCC48CB009CA7535C62EE2A3AE61D07C828C68D19A55F6A7224F716E77A381E06933AC03A1B0D01B6D231E38D4A19EC9C6E8DCA42C218B290130BCB583F7E958FC3531037716B356948D726E602329A322A410945568BCACBAB75D65DCBB38B80CD20865563699838659E065CE749A6DF66542E8474BD140B50EBB5983F546FB393A0BF10C5891767B935E0F5171CC97A00607332088AED99B13EEF557CEF73429704244FA2B915CC7731BB9BD351FA131A726C837FE19047443A16BEBB02F4A5020878AFCDC72CB780485A793CE23BC6DBC11E04B0DBDA82F51B4327814471C01042A1119D5513408C119266A0476DB89B20B18EBB44A231CC5BC39C6BCFC4DD2A61EF9180BD76327AB05A37ECC4B2E8ABAAA307A6C58040484BF73B06AB4A65F7FD99660A759FA903A56DA3E8DEC389262AFF8BABE1B85168BD21762C86046290F35D51E4BCB8403F160D9DC4E00CA8987E46CE05A5FB04A112EC352B1B03219A4B50345C6F31915FEF4C2EF870D30A244EA4C20D82849628CB61ED677AC9890CB5463EB90BCCFEBAD9FAB23C26A33E6667D76C96220147685EAA84CCC3140104AAA4A559E51209DB124337A2795925C5E0230E7920D4E9338DA23ACCC8693D5952CF49AC26A74CAA6E05011E6385F787A7071CA08C61F59CA017ACBC15E9BAEB78837B99B93908A8CD6D7BD30CB6C23F9CE5E15204E0225B55C120B31C1276695C909C94AA1B5D0C0496278435352B50343A9974A55EAB5105FB4436570181F2B7AE82238CFB9C967F424C1D146BCF74A2349583D36C1F7629E70E0C4702C133F7475985B2DCD923465F39772E8AB76F3634C6A1F6E8088BBB0BD3A050CBF323F9EB76BBC4603617908BE85E6EA5E40A862180D9EF1380B7A859947562EF3A845669146A128CDD9B684F4A0907E80ABE2B7584BE10833A4DAF89DE5DCCAB7C001116B5208FED872D91297D8059743D3E7B6EE47548357E7F882B5BFE2F04314187ED424\"\n        },\n        {\n          \"tcId\": 69,\n          \"ek\": \"36FC681397CD63113EF7756444A3CD1FB693BEA7905A507CABA24AEEFC637104C90D311BCAEBCEDB1335AB26C82F255984118B17E28F2266C5D73818D5ECAC6DA8380189196C9568EC949DDF76694A4B39145758FE66A14301CD64A10CCFE46C3CA8419EE5766160465154680F8C435DC523F5136065555F503883165A525568878EEB4DC675445F300F50A1C42EFBB5F1C9A0A46684414CB4C8187255A2CBAD4321082698F2714F234678D4418E33A70CFD76313EDBADA57C82C65801C0DB89D4F7BE19857E31E16ADAE95F95CA0473082F7FAA0855723B5AE4186A6C26D482905C00689D1737CBA52583893995065B5C76C7FC7839CDFA9BACF1B8F7A68307B459CA96BD7091A06A83AE3F3146FA81296932291B2158E9B09EFADA0A2D5AA8F1DB2489E40E04B0C768335595960CCD586B78844F0E940F2007B3B9EB9747254D83C58443C2B7A347B86FAB1ED4354DA2D9669AA21DC2A938E0FBBA0EB57E4CCA4C2666B79559B70F9B61DA060E03C59850AB2D129064F8D6ADFDCB7C457235CEE9473B003DDC9252AED7C444644832A4AF310C431AB26292488C73C38BC03550F7569909417041E2924AC9338ED8251BF6CD18F2B9D1569FC688AD095A728C574AB8EC549AAC0ED13A6EEF5AB87EE622034CB87045997A6657AED6439AF8951946ABC47BB9BBB55A80C92F40E126D78B4353538B6995830044B6E7220030861E47A5CF0291BDCEF8ACECA5C9FB0B43901CB6CE99246D2A2CFBEA014F902046836CB0B58A6DCB79CB9275519A3295C134289880A0D6CEB91356428294AB0B11CA150B9E362872BC8C25E2A78748C9D245C2D36A05F9D87CB5D0C306A5C0DA74AEF98BB1DC45801B4B9EC5B73A0ABA407B0AB8065B23B8345A6147A258326450E399BFEB86E754561F09CEFD1C832E355939175A347303E0D7815C4056F862B08F7914051834423972C84010CAD31B34321066396D47D5C9A77B9F45111B93988C0692039B5A060805AE4365AF1F8B138CC83F2D681F740886C7F07CD153989DEA4A6B2A3597374F7D5BA8E7D01424BCB721071A59FAA49F74AEF15910E84CC138F09F31574DF9A7000175C80073A10CBA6ECFCB84FA680131B824241BB96FF17BF76C0243A03DE9D49895D640E841A07C2299A7C52E22D37C8EE1AF81847781BB2F25240CF91851A5634AE285825C8809C716B6DB795361CC374B87BF34687E18EB413D21ADCEA883D4837CE874693159BDD2920405FBBBE6674F19D0B3C3D6B6F4095A946A5F7E6B0C5B7252A8F72EA357BAEB475FEA0141621A607324B4B17AA13FB7AF2ED529E1728F4BFC083198628DE3C8968A9A92C8863E08AB34A00EFDC73EEE7A8F29F4906E848B8F56474DB2BDE054A660966C955418E8D387CEF43CCA3B5DFD949E91AA5D75C50C38C96BCFEA7740147FDCD8406C0536325A1B74576392EC1BB1B181D584AFD841B43B0034ACAC33416C8DA02777E1169AD4045CD51674342C468874B16B12CB13776E6E8C702D84C5D13933EC161EC10B82A9F339AB3B1D1A9519F342506A3C0E7DB20ED3D7029CE444C5FB4FDB88525AF4C565DAAED6A51B7776246CA880D0F72D5A40B88FF348E9677B37D19F0088997F77429EDBB63840070D71A60FB34FA5CB59F47B06BB03C3AEE80F393A9DE283604635411151222C696FA6401783431E9690BB8958135537332EFA805ABB5D69C9C8F9084EC38862A68082266CA6A0C3514A71727AB519C359CDBC663066FAC14600132D1216BE63AEC0CA96804096D22904F4194D87D35C6C2C0D5539C93BB909A5287C64DCC1246A48CD9CCD80311089CA694120ABFD5A7F16D06A0BE42C48878BE76253198A703080A463053D43867B73B67B26B99A551A3C0C41115D007E0EA0AEF1446FF88BA7FDD32261FC0776FC427C121B2D231E3AC843015B12583777CCD5AAC502A7FFF9B03FB614246848742342C2E0A871E1AD6D44847D0A5512566098C712AD89904EE051425358CA5A964643577B8C8ED09450E97B55F660C2F2F608FBD87CB59585FEC3218219B1C7C18200DD446402898E065879FB17C97914C1E83F262801A4E256E798598A176BBE952149F8AD5B992CE791559CE6AC90539C096A209B9A4F87AB73E158801EF2B939A511F416BD4DC195D406A55A66B3FEC2925C5474525026488ABE90196230EC15B09CC55CE383DA8ABED781DC5085BC8E58\",\n          \"dk\": \"67973C3154342CA60AE94B0B60FB2CA4DCB70BF13BBEF7CC9B54790AB3CBD882385BBC888321366F4AABE4696D0126B724A3AB8FE40E03137A28D1B9DE280C49342B162196B13C364FBC5E26651702651A15369F1B83A25B945BF7BA7259A329F107C91E13802B0119CBE59C463B3A53B51837683A6E75674174210A871A1F582A6AF0053EB862BDDB6FE53B21924A7A783C4712AC5A6A38062D83A31DDB6537A82EC135616947A7D6456DB50C95DF678B113275238A1A8312100270A730435E06E554268921A04A9B7FF5025884486D817D4D81610C2805B4207A013A4E4B785CCC0052F70318188380FA554E41A64BE315B3294C3EEAACBF46DB8C87105DFAD68575044F59CA15A379579D381ADC65101FBA2EF3CB2F4A02BFEF8A5B28A51A2283979CD45B753C06D3653B9124A04AA64577562C34DBBDC9A5AAE358A5C73259568A3B18E48EB536C6ECB05A00DD727582B2F51C5562D53311C09D26218DEF54455EF86066B76CEBC41584173472A6008C55C94DE4261116CB21D325B514C83FD07C49C30967D23FEDA60CB9373AB9CABD3D528A99E6898E7A6F4E4C9644022060C30757F5AD961369D537551312172ABCC1C228B9F4F642C952006D7C45C40CCC96FB7BD28731A2F277E7284375D03A35F68101278E70AA882AD82940EA1E1DE9616AE99EAEC6BBED52B1B110502062783B102297A144A54C10F43B2D4C529398EB0739DA57F183B195A39E3178501C9539355B8E456760FAF99D5C3C55B1AB3CCA017C04B94581B18AAD4594236456A02B4D99A00412F818D6EC1387680847D10539E0C063A334A2A86EE1794B49DC895EF95C3D801AB33748417C8F60245E69A8AB68C9BD04BB28783293C52051F9C5A532008CA9C0063FC77519709F05358BCD28955C4A9ED3D9319E1939CED04AEF489C0EF2AFF1EA05FF2C8189093B14C6917ACBA94A8C546063B2FCDAA6C77C1A424A81F3717D9DD88C7A3C64FC593AD66748523744BECB2434822F23632D6E362E5F38C6329958AA20096BBB4FE4F43167301D57332BAE31110E186FA5C41597B92922C79CCE8C59FF341A08E56CF7A1B610605D910510DC03308967CC7CEC7A4328600C601566D8766B601855CCCA887BC84ECB05AA1529DF77058EC0760377BF8E3AB30FE477C33594873B59F02CA226966DC9841569564F9A5557BEC87C2C81784664CB2C8A0770C7C15DD61F48B762CAF11E9C189C9E9947D788A13FE554E76CB14A631440AB2560AA747B447B08E20A5C8BADAD31BD79FB27091938C4EC7AFF409E69730E152048D540958458C219C007FB1A7711D9401B86A10685A66E3C439C005563746221F50382825E3D6A779E692022B2B98150B690723B8ECA370283611ACA06B5881F38A7757A38A25EA06FB5176788C68C01D33822DC9CF7248CB81B7511832845CB06FBA36931CC07BDE6BD299A69A18945DF4057ED724292B76FC1A4432C1C16F451CD1BD796451957C5734B90574D809B69A9B19F07837028C97FC56B10CE6779E81CC4B4714A34A254A1100CA2044399658DD878BE873036B8639609786FEB608B5134A79FD74EB3D485F1D44F39854B3C6552965B4F08F998DFA4721B6107C20433AF509FF0E571FB58B8AB15727AE24AA189CCAB216953F72899D823886364994CBA31EA12AEE12B85460D056C761024987F7683B40A692959513C346E3674CA736226A748345D5931D3250949DBAA2F5BBFD7361E748A6D69419A24D3CCF7F7A849A4301497A2BC1588B021314B0784ADF1C5D01B562B89B3863A92D9103638CA305146912A3588F3A6A816224686268FA692031F427036043090A6B93CA944B4DAB10B63421E69C948619229D6323720C773B900A8393BDAD30547E0BE24D26795DAB3F1806779FA13B1A09A4BFA93BCEA34F522430E1BC923E378EF4631BF76A0F1D3B6A6D8BDD6811293F02CC2096C0257A957261E87220169330BE5559B48698EECC58D151C50C85CA19C9471E7754A8524A8C742495D15365189A2156046CD7A916C2816D9F1626306C6575114703091CC7721CB848CA66108746B1C10341BE8608FAC87B869519ED8C6CDB0530EAA6C801110B8BFB8C4C0184660F79AC2485BEA7AC2A2C3C683D483113C8B5747309FF1AC459CB83560C8800502942B09D3870036FC681397CD63113EF7756444A3CD1FB693BEA7905A507CABA24AEEFC637104C90D311BCAEBCEDB1335AB26C82F255984118B17E28F2266C5D73818D5ECAC6DA8380189196C9568EC949DDF76694A4B39145758FE66A14301CD64A10CCFE46C3CA8419EE5766160465154680F8C435DC523F5136065555F503883165A525568878EEB4DC675445F300F50A1C42EFBB5F1C9A0A46684414CB4C8187255A2CBAD4321082698F2714F234678D4418E33A70CFD76313EDBADA57C82C65801C0DB89D4F7BE19857E31E16ADAE95F95CA0473082F7FAA0855723B5AE4186A6C26D482905C00689D1737CBA52583893995065B5C76C7FC7839CDFA9BACF1B8F7A68307B459CA96BD7091A06A83AE3F3146FA81296932291B2158E9B09EFADA0A2D5AA8F1DB2489E40E04B0C768335595960CCD586B78844F0E940F2007B3B9EB9747254D83C58443C2B7A347B86FAB1ED4354DA2D9669AA21DC2A938E0FBBA0EB57E4CCA4C2666B79559B70F9B61DA060E03C59850AB2D129064F8D6ADFDCB7C457235CEE9473B003DDC9252AED7C444644832A4AF310C431AB26292488C73C38BC03550F7569909417041E2924AC9338ED8251BF6CD18F2B9D1569FC688AD095A728C574AB8EC549AAC0ED13A6EEF5AB87EE622034CB87045997A6657AED6439AF8951946ABC47BB9BBB55A80C92F40E126D78B4353538B6995830044B6E7220030861E47A5CF0291BDCEF8ACECA5C9FB0B43901CB6CE99246D2A2CFBEA014F902046836CB0B58A6DCB79CB9275519A3295C134289880A0D6CEB91356428294AB0B11CA150B9E362872BC8C25E2A78748C9D245C2D36A05F9D87CB5D0C306A5C0DA74AEF98BB1DC45801B4B9EC5B73A0ABA407B0AB8065B23B8345A6147A258326450E399BFEB86E754561F09CEFD1C832E355939175A347303E0D7815C4056F862B08F7914051834423972C84010CAD31B34321066396D47D5C9A77B9F45111B93988C0692039B5A060805AE4365AF1F8B138CC83F2D681F740886C7F07CD153989DEA4A6B2A3597374F7D5BA8E7D01424BCB721071A59FAA49F74AEF15910E84CC138F09F31574DF9A7000175C80073A10CBA6ECFCB84FA680131B824241BB96FF17BF76C0243A03DE9D49895D640E841A07C2299A7C52E22D37C8EE1AF81847781BB2F25240CF91851A5634AE285825C8809C716B6DB795361CC374B87BF34687E18EB413D21ADCEA883D4837CE874693159BDD2920405FBBBE6674F19D0B3C3D6B6F4095A946A5F7E6B0C5B7252A8F72EA357BAEB475FEA0141621A607324B4B17AA13FB7AF2ED529E1728F4BFC083198628DE3C8968A9A92C8863E08AB34A00EFDC73EEE7A8F29F4906E848B8F56474DB2BDE054A660966C955418E8D387CEF43CCA3B5DFD949E91AA5D75C50C38C96BCFEA7740147FDCD8406C0536325A1B74576392EC1BB1B181D584AFD841B43B0034ACAC33416C8DA02777E1169AD4045CD51674342C468874B16B12CB13776E6E8C702D84C5D13933EC161EC10B82A9F339AB3B1D1A9519F342506A3C0E7DB20ED3D7029CE444C5FB4FDB88525AF4C565DAAED6A51B7776246CA880D0F72D5A40B88FF348E9677B37D19F0088997F77429EDBB63840070D71A60FB34FA5CB59F47B06BB03C3AEE80F393A9DE283604635411151222C696FA6401783431E9690BB8958135537332EFA805ABB5D69C9C8F9084EC38862A68082266CA6A0C3514A71727AB519C359CDBC663066FAC14600132D1216BE63AEC0CA96804096D22904F4194D87D35C6C2C0D5539C93BB909A5287C64DCC1246A48CD9CCD80311089CA694120ABFD5A7F16D06A0BE42C48878BE76253198A703080A463053D43867B73B67B26B99A551A3C0C41115D007E0EA0AEF1446FF88BA7FDD32261FC0776FC427C121B2D231E3AC843015B12583777CCD5AAC502A7FFF9B03FB614246848742342C2E0A871E1AD6D44847D0A5512566098C712AD89904EE051425358CA5A964643577B8C8ED09450E97B55F660C2F2F608FBD87CB59585FEC3218219B1C7C18200DD446402898E065879FB17C97914C1E83F262801A4E256E798598A176BBE952149F8AD5B992CE791559CE6AC90539C096A209B9A4F87AB73E158801EF2B939A511F416BD4DC195D406A55A66B3FEC2925C5474525026488ABE90196230EC15B09CC55CE383DA8ABED781DC5085BC8E587FE45A6DB8C05EA8FFC788FD2F73C26CEF305BFBC9BF7C5B32466B5417DB33ACEB8EA5E8C5EABACCFF162556DA53F0C02F72EE7A7DEA8E9EB70FC51C777645E6\"\n        },\n        {\n          \"tcId\": 70,\n          \"ek\": \"3201469EC68BC7EB2BD457769C69C09F1646DA20B3DB7A8756962DEB905FE387084A58B069901A9FE1A0939C38E6E414E23B516FD0651E174DC0F6388DC2CB8D605D3D5B1F6AEA3425A03B9A903C846ACC128C87CD5A17A46BB474A0BAB64112DDF1C35355C58164B7C6F9B8A5879CE8E1CEB4FCC26EA55E4833A401B54EA5C29D92E03A8FD5AED6C51E2437C7A89221ED73B2BA3BC5DC30276A0692FFFA3E28B8CB9025566D54A3DD84A9004730B2009A73B354644045B44224D9D96B6A137D8F61513BF8555FAA8F61F00215C8C8841091F7647E53055E6762A504C121474120BC70BED5C4102C82813AF656B9248ED6F630BB269CEE33AF26D474C8B3CDD8D959D10A5D2AE221D5C20D9628A26F1A11EDE680F8C8333A54AF8D79B67AB0BD9DA1680927B0B31B1D6653410C4CC0C6E95164C33506D8CD19A73E1031C733C55EFCA33F76BBAB03182BAFECBAEAD364CE38B22B74A0A244A15725B232616EF2CC0A3010758A698880A23FD58BB2199A3946F5CCACCC2D61AA5985488C0EB60D465566B94C762057ACB163CA636B548254C26F139F51A96E492035B9D725AC16B94C537E2FD78B1641882AD20DCD6C5BB21B79921A59631130E7885340F5311F9973CBD31783DA443BF7566AB3BACEB88232F83E7077845061C09D425C7AA01733CC63D5B7C17C6B3309408D1FA31AD909BC1B0AB6535B0AB6064722D847B07811098C7316071A751623654605FA453FDDE8176B42B2ED07CDA4144640EB5984B301390C64D520071F1C66AD69C550AC41D74915DC2156F78127A704CD678A655C87CEB43748F0989AAE1730D3E4B3374A72CE364F1E2ABA2E5CCEBBA440FDA8AD50A749FD908AA1578C02B35342BB96C3350392C5C25CD71093BB2E87A3A466B697489A3C9E9C84805160A7396D001633EE00AE5641A893BB374CF5CF62417AFE67682C08A10E0A59B8346ADD006F2B40669042616D57936593A62D5578808A1E9D55B1EF4C90EB049987DBAB69B3C8C4A596901BA2BFA74DD4B937547A82B7F5290BFB79E5651B2B4AAF3692CCFBA987D5EC778E61281B0B388A71222DA27A90776E66BBAD25118AFDAC425F15599176C5D5F1B491FA1FBBD9005A0AB93AF1C8280292AA468FF0B1218726C52E4AC8FA403C91D12EB1134D6B08ADEE2A065F191DDEC390A1232D63306AE1C60AE1908CA2294ECAC6AB5257644DA45D70E96C6FAA5E1DD02AEC276C5EE2A8D36A2A13EA21359934C8285498370460CCC2A4C886C72A9FF17AA214553F6826177630ACC2F70619D64CE3F04DF17A6599E9AE5D2BB97B731432421012FB229124CD52BB9E2EA7B33CCCACBD9029A69AB76F00CE84A0C0D2C80F54A77D6064AE85465FA74271BDC2C62CB940E9C32A67F9C94AFB7322B93016E31C4E34993C7A65D58117495C44A8EBCEA635AA6C29889F3AB1E39661925136A06BB269483C1D291F90478713A91A178193B25877D416C03242682E76071B78964EA626C6A85E4ED8A45CF573BE56369B9476A2FC76713AA11573A083D0A0B89BAC15228A42B9BE2F8617CF8BBBA21562FEE3313066A8E86063EDF365799B7EFCBCC11570196B535D40F65F59C0972E057E2C70048CC10DD292611028277B325791B865B9D863972AB1725B6BF1546DFE0030CD85621F92472CB0064CC2CD11411127231181E6745A3580F6E8383150C8C098A3EB455F5CD05725719D0F844BE76743F7D962261A5FE5C29E46D58EEBCAAF0C40C04BFA43089C6236AB1AB574CF28A9178A4634C5848259191C21401C05A00A48D0420BB2737816CC72BB3D5BCB5EABD5531ADBB2E875045E861114D2C5AD5997B104A4F3BBA26A6372EA6062C8D6229C196E5B7A572F898D94A307BA051F899C16A34981B818034CA84A14930AF2D38BE095BE4B304FA1BB69DDB72AD605294BD273FD314A4D1940161B97626B5C725935D358153B4A1318F1273C7C6A93FCB372196593D9655F3C41FFD4B7B72B7131D7C2607A8763778556EC2D3E09969A4C968E92240077051446375B2C4C9BA9CEE84563C4491673667C026843C1602A9BAA0705E27918A27E2B01B9E756A77F7A304345117ECC52E81823A0A8170086B89EC64A39C60EA6A78363FB79E7C0B886B1854C5A929DF98077F5AD847650926273A9495000E688124118F1D83CC93D0E4D2565D307F4CD0C166E9083E8CB47F6979CD0C6F05D5A\",\n          \"dk\": \"6F148AB8F31718811FA8326B85A430341A9CB4827004D5897659554F661B37551033D7B51D416CEC7921FFE4711A575BA6EB73643B78856B9C230768B6369A9D9C7C91B7620E4A709D38A73CD7A2CE48712EC03F0C94070B783A4EB3186051A56D469D01152D3799934A69C672600E0B223740F937607B8923768B20F7CBB9712BFC633654119B7299770C764C471399BFF2CE457918F3C3AB3A323CAB53BE1F491EEA526EE95C12A50C2A3D8C75589BB6919B97D37AC96753ACBE493109F60957E268F8CC27C497A89C12CBC7109CCDEA54E174B7AB6C4E9D68326261280D582DF8E976B2F67617B69FD2D58CFBC44F641C9598B293182939D4AB384C6B7920266E6C7BAA133A1A9E841AD0018ECF6010F0D292D7699177864D063641EE8BB1A2C7CCCD7C33C5574E9C499EE9E7417A5C072596C4FB76C2E56950B0907BD653B47966420E866FC3FA367C965A1D096FDA16C3CC1CB651DC693B960443617C58D13FC2595FC267720BB88D0633579BE84DC0232D973AAF92140B20CAB7D4444A96EB6A4235C0665460470994D94005B659BA5A6A345E51677402B69D378A0444B345340BE80230C7730F4342833543BA8079692333128E45B9DC48830C1892E69AB37C632771EA677A47B1518815E4976367888AE552A397576A6838B26426A587828B08EAC520E47463D00D1EF5CDD50838A961832647561BA75D6B1A5C93121949C46CBEE11938C5718013724131C18B0BAB806097C14445FCD303BC8BA3FBE2C0AA2C3FE45A74F5E22F77D6B405A962B1F95CC1E0620E6239AD57042E139955C97C359B09F6990D1C7747DEDB6F02313BDA02839AF16D7F181BDBD3245B272988432B35916144BABDB7F3CBB8A4575677484822B76739577CF945FC944C21D4956B0B2E3C86195B5724A03A896552BEEF5804D583BA6B2B9A67106A47621DBF911E62F677F933527A0095D6F4C0D5C3392548C23584CF91888A51269D15606542E601D8A7B915418F164A6AFEAA7AF60345D7A603A018838237861EF1C99194323773A8A18591F7539821D57856C44D3ABC334D266DFFA64AAD3B56DD7C7B1ED2B5026142CF508D9F25A2CA6199F7937E6AFB66B1C486D5512F1F12754ED5B70871160B8814986C078F006962C956404B6E5EE9C486E786C3C448B902B1841697F1208313F8543B40566E18746EF181DA1195711040E7268CC474A1AF4388A8FA11AB9A54B228A92E569415D12C06B246F8180F4B80395C708089DBA946B459ABB598F6F32708B8316AA73DBFE15D231C2D69627D434CC1674778B4FCAF23227527B2966F0718AC1C9D6D3B374829C46D063A4794C15A189DDA693FB6C08E46C0B7970B8AECB8B52EC35428E8826CF6B1FBC9A988E13ACD089E4DB2967DF64DBCA475BEDA31DA74C3BEE4BF74568EB257763C371BD252A525AC3325941C62EB68BE79701C11C18EFBB1DD010F84BCAF5EBACD35C67671C968A6B5098E0535D7932002EA7739C85F45085247032DE8EBB7109B9A522112DFB89C2904C1B611AF46762E4854AE2652B4A5580B6D902C12788E2F6C72A59A0A5C4385CAC77777C64D9920A5827568734370E891A2638AC12BD164C15C01A3DA0292F325DFA4069068329DDCC25A3964A845694DD6AA99F17AC9C12DADB4431C8707949CBE2E4B9764B65BAA0451146725B6A7B6CD6726E819AA8AE2971E334A66B0501F210C0A1975110BB8647CC8299C2F7D4C16DE4791B25A36B2592E3AFA0D059B9D4B3B50D9981160C23EB96BB1EA0A984D922C1D31A9C105CF5CE64D86222A12773E7ADC01C99B5F4754610A2B3F24C66D8E3212B8584DDF7B68128B215C600336409733049A54A719D0E5A657AC223136B1C9FC5D3F33545A051902AB0FF4B1CE5DA81BC8B3A78EA40828348210F953250211F85C1F3196418A32CAC6158850EA8B44A0CB72EA5C0B165DA1A0751DA41D58514439461942E478D3A3951E3273F35B938A842EAF331471B9B1417672DB5CA82F246B937448A08AA720060C5F2BC084D3157C25CA2F2BB02E2BC4289466C8721D7198B01F92C3EAB020BB92B318E78074218A48180EE799948501B84FDBB447FB009436CD20A69378E4BD87831630329AC0D76460E5134C60561180274E8C94B64C5F17BC4AB39625B49A13F1887B9AF06E3201469EC68BC7EB2BD457769C69C09F1646DA20B3DB7A8756962DEB905FE387084A58B069901A9FE1A0939C38E6E414E23B516FD0651E174DC0F6388DC2CB8D605D3D5B1F6AEA3425A03B9A903C846ACC128C87CD5A17A46BB474A0BAB64112DDF1C35355C58164B7C6F9B8A5879CE8E1CEB4FCC26EA55E4833A401B54EA5C29D92E03A8FD5AED6C51E2437C7A89221ED73B2BA3BC5DC30276A0692FFFA3E28B8CB9025566D54A3DD84A9004730B2009A73B354644045B44224D9D96B6A137D8F61513BF8555FAA8F61F00215C8C8841091F7647E53055E6762A504C121474120BC70BED5C4102C82813AF656B9248ED6F630BB269CEE33AF26D474C8B3CDD8D959D10A5D2AE221D5C20D9628A26F1A11EDE680F8C8333A54AF8D79B67AB0BD9DA1680927B0B31B1D6653410C4CC0C6E95164C33506D8CD19A73E1031C733C55EFCA33F76BBAB03182BAFECBAEAD364CE38B22B74A0A244A15725B232616EF2CC0A3010758A698880A23FD58BB2199A3946F5CCACCC2D61AA5985488C0EB60D465566B94C762057ACB163CA636B548254C26F139F51A96E492035B9D725AC16B94C537E2FD78B1641882AD20DCD6C5BB21B79921A59631130E7885340F5311F9973CBD31783DA443BF7566AB3BACEB88232F83E7077845061C09D425C7AA01733CC63D5B7C17C6B3309408D1FA31AD909BC1B0AB6535B0AB6064722D847B07811098C7316071A751623654605FA453FDDE8176B42B2ED07CDA4144640EB5984B301390C64D520071F1C66AD69C550AC41D74915DC2156F78127A704CD678A655C87CEB43748F0989AAE1730D3E4B3374A72CE364F1E2ABA2E5CCEBBA440FDA8AD50A749FD908AA1578C02B35342BB96C3350392C5C25CD71093BB2E87A3A466B697489A3C9E9C84805160A7396D001633EE00AE5641A893BB374CF5CF62417AFE67682C08A10E0A59B8346ADD006F2B40669042616D57936593A62D5578808A1E9D55B1EF4C90EB049987DBAB69B3C8C4A596901BA2BFA74DD4B937547A82B7F5290BFB79E5651B2B4AAF3692CCFBA987D5EC778E61281B0B388A71222DA27A90776E66BBAD25118AFDAC425F15599176C5D5F1B491FA1FBBD9005A0AB93AF1C8280292AA468FF0B1218726C52E4AC8FA403C91D12EB1134D6B08ADEE2A065F191DDEC390A1232D63306AE1C60AE1908CA2294ECAC6AB5257644DA45D70E96C6FAA5E1DD02AEC276C5EE2A8D36A2A13EA21359934C8285498370460CCC2A4C886C72A9FF17AA214553F6826177630ACC2F70619D64CE3F04DF17A6599E9AE5D2BB97B731432421012FB229124CD52BB9E2EA7B33CCCACBD9029A69AB76F00CE84A0C0D2C80F54A77D6064AE85465FA74271BDC2C62CB940E9C32A67F9C94AFB7322B93016E31C4E34993C7A65D58117495C44A8EBCEA635AA6C29889F3AB1E39661925136A06BB269483C1D291F90478713A91A178193B25877D416C03242682E76071B78964EA626C6A85E4ED8A45CF573BE56369B9476A2FC76713AA11573A083D0A0B89BAC15228A42B9BE2F8617CF8BBBA21562FEE3313066A8E86063EDF365799B7EFCBCC11570196B535D40F65F59C0972E057E2C70048CC10DD292611028277B325791B865B9D863972AB1725B6BF1546DFE0030CD85621F92472CB0064CC2CD11411127231181E6745A3580F6E8383150C8C098A3EB455F5CD05725719D0F844BE76743F7D962261A5FE5C29E46D58EEBCAAF0C40C04BFA43089C6236AB1AB574CF28A9178A4634C5848259191C21401C05A00A48D0420BB2737816CC72BB3D5BCB5EABD5531ADBB2E875045E861114D2C5AD5997B104A4F3BBA26A6372EA6062C8D6229C196E5B7A572F898D94A307BA051F899C16A34981B818034CA84A14930AF2D38BE095BE4B304FA1BB69DDB72AD605294BD273FD314A4D1940161B97626B5C725935D358153B4A1318F1273C7C6A93FCB372196593D9655F3C41FFD4B7B72B7131D7C2607A8763778556EC2D3E09969A4C968E92240077051446375B2C4C9BA9CEE84563C4491673667C026843C1602A9BAA0705E27918A27E2B01B9E756A77F7A304345117ECC52E81823A0A8170086B89EC64A39C60EA6A78363FB79E7C0B886B1854C5A929DF98077F5AD847650926273A9495000E688124118F1D83CC93D0E4D2565D307F4CD0C166E9083E8CB47F6979CD0C6F05D5AA184CD5ADDE3E9D68D66C7AD3ADAD382D8642BF03B85F068AEE861FA55B6340CDAC056B9A373687E44CCAB8751BD334F4942696B9076155F9D0E5BC0E89D85CF\"\n        },\n        {\n          \"tcId\": 71,\n          \"ek\": \"3563BADA724011CA9F7154964DB8092AEA60242B273BA94ACD33B61BE47B5AC2115E95BDBE56B214E04166356496A698C81744C06B114E2B37CC441A9AD8C90B936E1D86B9E1D9B17F352B393B9A1DDA0FEBC548989C12C120C7988A460B151895961733F84111058116C427E75A8FB6798B9691626F7A5274F46BCF5A160F0BC59B01C5D5E634EE9C1FD5D665AB59A4B495030855C538C86BE779CCD8CC076D83407A5C9AA3757841DC30F47592233A3AA8F3A721A5902FF8225F51764D59087D5B928E34572C23C511E236ABA0CD710A7EA0FA0CBA999F22445AF63CA783205E4D935CC9304BE9E92364C97993A1BC4DE491C5ACA2932575C99153A0BAA814644DF6A4922F426268EA1F7D97AB7629462F0C8465900AF523CF2FA978CDA56F4FC936BDE529553B56735701FAB3453C1A8ED0492222022E7C799EA11837C952CFF9A059992B26CE774460DAC2D23803A913CDE96662CDD78B4842A09E3A09A8D6A5B4052A25D39F63A43D9B9473FF184EB9B49B78B7165C62141866C2D4D866232A952E89CFC422CA5E510390DB98DE3A5DD0E3728BD5076656941894BD2C85C1B4FC400A9365F744B7EED6637C694D7C3104329AB526193E0CB2BF5E9B8E5D34630C16B097EB027C33621F23BA78E4470DA48354C29C114C242EEB6C62D15395D40B716A6137CB5B5AE2C27CD49D176B5D70416E254753F1150D5D8807F07916330C69B5F236F0672709834523092746897CE489B3FC878FAB716042509AA96BCFB839544449280C7C1CC3F79A22A25BBF47A33228CF1CB619E2492A95EA5F3D12C80BEB2412B81D6EC7CF6147287FF36449205CAAFB3625CC0A6FD4AEE3898D8FE56F62B06F6A7B7F1325B83A30B72FFCBFC9A672767BB2659B1B6500B4A3629F00286B82F9597E6CB21FDBA9AAAAC5F24273B2AC22EA4023219C7B32FCA7694693A2208791E1AC33C00C0DE64E2241001DC04249CCBEB797A1C38CAE3DA81C07364374033917A04AF6A19BE424C07DF33237B9223B7C04890CCB57780126B9A68AEB5783993F6C08783DC254B14482AD37AC90609BF3B0CEC7B39DB4241E0D9502DB60AFAE068749A295BEF8AABF861EA1292542927F6DB939AB7365142A37962A46230C37FB9519642119E03C45CEAC2F0B40198D6211758346E2FB284FF807A4C06552D7B8DF604D2EC771D2F74D0A258261280C60E71C9FE66168DC9493539EC1F018A520C02E6B8562C55831C18900A1C55FCC6A772480BEF2C75322C1CD116483562E6B7A0D16D351E2BC58032B5267325183148CA61079B2E8A8AA293863E0946D6772716053DF4A0260E3A36ADABD19713B46E89ACD650EA7932050724356D1261DA37916E85BF552C8AA628ADB08383781245EDA4A82E27D099496A26B5C893927DF02BEA47742B3C40B0E31B5D621012D1BC0F3B085E5BA6228E9B5E875901F1711CD12516EAAA4FFB16AF44AC384E0AB5469B222666FDFE32B1F5A4F3C0C022F6B4ED770572DB6B9B7FB715CEBA1C0EAAA19FB2361D83DD2465375A2BBE2793255619592A6044B71CFCA2916B0C6C895A38D7A28245398465432878D96446BF71A6C21C0A62500B030BE6B528646C860AEAB7B28FCCD87381C61D7106B518E22A2A03F5B7F48729A50672A4B039FB67B9DB0A803D304BFCF83782BD5A10250A8C8D96A9EA993E692CDE5871C6BF79FED564C5834634B88280D3A4CB18025E332532BC65FBC169DE0308012C08A581A1A5C9B1A89C75162B990FDAAA768A35DBD9ACE608981D589116BC45BD7D49425C7B11B573B8F554976152C9A17C273E4A3CAC15D5DF6724C1605D9A77CE487438248178C10486FD7C7E1EA52BB5467B066245560A2C4523B53304445F956E968C5AFF602B7F9378CB290BA0B4707841D175C01AC0BBEB18435E03850FCA479C1E8B7CF0730A5F23690F862C1970A5E9A8DD3F51E0655B2CE7BBD4B38264929C9499BADB505625B7A7BF260597F1239176C4892E37C3EA0770068C49E14A8DCE526D64791B6E3A1EB70A8FDBA17FF8476CA5B1BC9FB533CE7B1F0373D760AC691DC598899A3798A0D6F967699CCA69420116BE625CFC8C23879215C320B354412DC7311E21A3784A3445134977E995282033BD0080B84487BBF23A499CC1D17172BD029406523707B671E25F9DD605C8473C42FB131B08A1E33F1E85055C2DCBA8B2B04F9B8C07D906384\",\n          \"dk\": \"385CACF0957318BCCDE555CB6933660544AFDAF8BC43D4AED82135F8CA2131201E03C7A42C7875EF309F227684284275F590CAB8078AA51AB95A133BFD9910AD0218260309CE43360ED01926D13255D37FD837462FC685F64350CAD95F9B757B56C79564C57C10B31F25514C570C1EEEC1223AAC91353971D915635E3A0A8F4BC5E1F241B3F31F457B3984CC160E13AB6C46B41808CB067C8396D4425FFA0C2E208E95522D65D0583FE2C50F40C9888066CF3377D494BBCCA2CF5DB55ECF711AA48ACFD6F859F859992E5B57C9C56D9430C6360B076A8C0EDDD544F8BB5B7950A7DDC006E705597FC08AF695AF0765331CC8913D8944EC4262E195B6FAEBCE2A3B7472D44A320B1533AC3917FA8DF7283D33E999DE4756FD37005104C8E2D50C4356BB35A79C021C45A05C05B10688F5A53AB52255D9B6BF8FAA036F09923AF89CC6F66BDACAB817A6C9F88B2339170FDD478A7C674350209C2D0773A413134D9709DA988F743134FA89AFDA3C94C4F75DFEE2656AE2312DE6A42A0B43910A4CF061922456346039BB4AC466B8F9AD7867103C165041220C23F09F264383D183B66C7BA90B93BFF8CB9E736881EEBC21917AAFC9067F8814C17AD8A302184FEC862083D1B39C810B92E3CDDF937773CA9EAA2C342014CF4652A5E5D2816AF4B9586467CC702A4AF342DB0B5908AC2F9B0C4490A8962D011BCCEAB20EE46DE2B6B08D473D79E7A10AA52211191784551E7B289C2FC85318017844674AED2917B3ECCA9C814F9F988CA8A927029AC5A6E8582078220FE44AB8A63EBAD736C4496D726BA3A1B422F1265212503B686A5367279E9661702D21887AE81F283966A5C91A8638C63608CBB59CA80A679F2ECA74D828AB15592A15070CC6915D8B6A1A6CD13723D410276B6F6644A9D0610869347386012340A4C6B1207B57903527C010FFC1C8B59599DAF124999CC4FAE39164A5868107C198519B59FC38E1229021A901F569A009987D76606A1F66C88FD88AA4D552E7C277510316DAD731727928FBB4495560958042773E1B8252BBB14F3328E5079E2206743BFB18A799918E241AF3A808155262A8F9A1CB46C60696763AB1206666BE2659BD1C78B6ACA15FB1F823C6E15A70930D21BC07F306094715A62805C486513837C56F332B97CEA067318C0C8E222B5762968DCA48DAE6C8297C2FA322913EA947B06418096C22D1EC3F2CEC48A7F850BF01B5702A8085108FA53127DE0B949E6352365336BEECA65395106BE79AAA91623931828436A67C0C3917160837532E0949503F7891BED485B5056B211A1E354105E3661AB7A1BFF354BBEC695204F2A8EF55073FF27A313662C1E7C7F4E8AB46399D29E2BE910425CE92976B491A1696CA56827F4571C98B328046269379110A559B463F1B9770050F1750262542A6C6BB728B4332C555CF45468795F2884F10303E212D333A0445EBAC52D669DA8B1BC3033081D0516F01CF22C5BE8CA020C8F671FC27A71A518D08B7A2C1C3AC62D874293A1D33729626968638C85761E78ED704CB86F054BE86C35F6A5233253C0F097B34A557C3A0B7B5D16B0266718AB7BB5DC27AAC3A96BD0B807BE5392FB0C6804739800206C7287C130B2A77E5045D5491FA50C1C68629C538078A3091E7B967B4767C3758A9A4E871C6129D78F0B9BC7963CC110641F7C8350A860B611C3DEC98FBBB8FE4F40BD10BA1F9F6C7061B12E7541EFB527B8E4349469987E4FA42D8F4958D6243A1C032EEB5860DD0BEC0C9A1E4D88967B748BC536020524BF5D06C594199DBD6B6CBB795F64B0F6656655B9B36B7E84316D918E094078D1C4E03C75617197306192338747E32F46551ABC23E74AB757B12C85899C28238FC74C089E4101F0A42BD440E28BB832BD523F03C070FE065D7B96652D680DE68223971ACAF5B5C29B8AF836C41CF80AACE794800FD944B6224B8E207BE27AEF24BAA4B2436B28B6A85AA19612A4F11935DEC482D7EEBB006B35860D13A7FFB159F57812B518E2F3161A9E35C5B272DA3529284DC278664AEDF05B0E1D602BFB60436F026FDBC0A30F2579F1C4252021A132C31C73249065C5BBCF03A5D418DEBE578E5F4A9074685C68491ECEA6DC9EAA7BC819DD2C43F42049B92864A001D533741515EA5090F0870F455B03563BADA724011CA9F7154964DB8092AEA60242B273BA94ACD33B61BE47B5AC2115E95BDBE56B214E04166356496A698C81744C06B114E2B37CC441A9AD8C90B936E1D86B9E1D9B17F352B393B9A1DDA0FEBC548989C12C120C7988A460B151895961733F84111058116C427E75A8FB6798B9691626F7A5274F46BCF5A160F0BC59B01C5D5E634EE9C1FD5D665AB59A4B495030855C538C86BE779CCD8CC076D83407A5C9AA3757841DC30F47592233A3AA8F3A721A5902FF8225F51764D59087D5B928E34572C23C511E236ABA0CD710A7EA0FA0CBA999F22445AF63CA783205E4D935CC9304BE9E92364C97993A1BC4DE491C5ACA2932575C99153A0BAA814644DF6A4922F426268EA1F7D97AB7629462F0C8465900AF523CF2FA978CDA56F4FC936BDE529553B56735701FAB3453C1A8ED0492222022E7C799EA11837C952CFF9A059992B26CE774460DAC2D23803A913CDE96662CDD78B4842A09E3A09A8D6A5B4052A25D39F63A43D9B9473FF184EB9B49B78B7165C62141866C2D4D866232A952E89CFC422CA5E510390DB98DE3A5DD0E3728BD5076656941894BD2C85C1B4FC400A9365F744B7EED6637C694D7C3104329AB526193E0CB2BF5E9B8E5D34630C16B097EB027C33621F23BA78E4470DA48354C29C114C242EEB6C62D15395D40B716A6137CB5B5AE2C27CD49D176B5D70416E254753F1150D5D8807F07916330C69B5F236F0672709834523092746897CE489B3FC878FAB716042509AA96BCFB839544449280C7C1CC3F79A22A25BBF47A33228CF1CB619E2492A95EA5F3D12C80BEB2412B81D6EC7CF6147287FF36449205CAAFB3625CC0A6FD4AEE3898D8FE56F62B06F6A7B7F1325B83A30B72FFCBFC9A672767BB2659B1B6500B4A3629F00286B82F9597E6CB21FDBA9AAAAC5F24273B2AC22EA4023219C7B32FCA7694693A2208791E1AC33C00C0DE64E2241001DC04249CCBEB797A1C38CAE3DA81C07364374033917A04AF6A19BE424C07DF33237B9223B7C04890CCB57780126B9A68AEB5783993F6C08783DC254B14482AD37AC90609BF3B0CEC7B39DB4241E0D9502DB60AFAE068749A295BEF8AABF861EA1292542927F6DB939AB7365142A37962A46230C37FB9519642119E03C45CEAC2F0B40198D6211758346E2FB284FF807A4C06552D7B8DF604D2EC771D2F74D0A258261280C60E71C9FE66168DC9493539EC1F018A520C02E6B8562C55831C18900A1C55FCC6A772480BEF2C75322C1CD116483562E6B7A0D16D351E2BC58032B5267325183148CA61079B2E8A8AA293863E0946D6772716053DF4A0260E3A36ADABD19713B46E89ACD650EA7932050724356D1261DA37916E85BF552C8AA628ADB08383781245EDA4A82E27D099496A26B5C893927DF02BEA47742B3C40B0E31B5D621012D1BC0F3B085E5BA6228E9B5E875901F1711CD12516EAAA4FFB16AF44AC384E0AB5469B222666FDFE32B1F5A4F3C0C022F6B4ED770572DB6B9B7FB715CEBA1C0EAAA19FB2361D83DD2465375A2BBE2793255619592A6044B71CFCA2916B0C6C895A38D7A28245398465432878D96446BF71A6C21C0A62500B030BE6B528646C860AEAB7B28FCCD87381C61D7106B518E22A2A03F5B7F48729A50672A4B039FB67B9DB0A803D304BFCF83782BD5A10250A8C8D96A9EA993E692CDE5871C6BF79FED564C5834634B88280D3A4CB18025E332532BC65FBC169DE0308012C08A581A1A5C9B1A89C75162B990FDAAA768A35DBD9ACE608981D589116BC45BD7D49425C7B11B573B8F554976152C9A17C273E4A3CAC15D5DF6724C1605D9A77CE487438248178C10486FD7C7E1EA52BB5467B066245560A2C4523B53304445F956E968C5AFF602B7F9378CB290BA0B4707841D175C01AC0BBEB18435E03850FCA479C1E8B7CF0730A5F23690F862C1970A5E9A8DD3F51E0655B2CE7BBD4B38264929C9499BADB505625B7A7BF260597F1239176C4892E37C3EA0770068C49E14A8DCE526D64791B6E3A1EB70A8FDBA17FF8476CA5B1BC9FB533CE7B1F0373D760AC691DC598899A3798A0D6F967699CCA69420116BE625CFC8C23879215C320B354412DC7311E21A3784A3445134977E995282033BD0080B84487BBF23A499CC1D17172BD029406523707B671E25F9DD605C8473C42FB131B08A1E33F1E85055C2DCBA8B2B04F9B8C07D906384861D9A8CDDC54069D3E53E033E2530CF83C284A49AD15019F061C40B2D00AC7A4D727ACABD44DC48980691E0268B5B3FC1E476B3FDF9571F5CBC8DDFD400AB99\"\n        },\n        {\n          \"tcId\": 72,\n          \"ek\": \"E8A65EF0C7C7D7391DAB233EB277243732CAA45B53315B645FF9721D8AB0CFE54FF717157663B9098C2267633EFE3544A88014AD15C7B10A4551CB5F9BBA3D74946371B1AA54E6419CA7B4611149B5F02C73819A8C625AC52618F771202338AFF05969ED670BDD0B9DB562B8055884CBEA7A208A2DA850A05484C6B84109A5C2B498E8B8640ACC6F7AA6E1D7A95415469CC1C06D17176146CEFBA58E2B8929770851EBD6CA5DBA444548C622673DFBEBBC95767BEE2B86FF5BB37CF306B2945071EC1FA80C7540C56A36EB684D71C5E8B90AAA8C2421E21D64E0638D63C96E2B0BD9457BD5062199A01253D240C712315778AF487AAFBE28C650532A493C1B3E0251BC0841392839905AAD3D12289506742073862E8549E7E87EC28102DB7B89EBE91627CB95C0928C921978CB8C94875A94F1557498132200B4B76AA3C3AC9755C3B6BC5E5C59C9B3C6F7FB2CBB5AA457904FEFD2C370E29EEB2417BC45977BEA02A0DB9DE57937372298102755BA5B007EC88340B28AE7B819F9E07182F76070043CF3AA7E9256CF1CC3A6442B1726EB889CAB193222BCD3D8141843C1EF9921B8254712C04529D38846F595BC011B3288BCE0231ABC96985145043B579BB0864F40EBA6A66144E0972CDB10A71E44A088112B3C7907E5591D6EE85B34D4AF8DC58633081BA1A0A6EFE133B4C70ED215A22F451A7117B8C5F6A3D3FC4455214F5DCB8C31370546B4C4BA365A25017A9C2296AA641CBC4745BCAA72D17C9EAAB0155627BB45770D6D259BDF5B044053B4C7C8784FC433E89942B5D6C807A0707842CA4FBB98693303C118C9F4952123582FC4E22BB853944BFC1584810153F3AF5DBB74BB2928785C542AB647E9321D724AAC9EB9091707B5075C700A4817CAA2C355D4C99B8C5037B5288FF5279725BEBDAA5CE23609E5320981F11C8A116501D5ADD2D2B057C41E469257057B7A9C508017245C20C13A0C104FE1A517EE4621C367CE283183C07675BBE5895FBA4F30CC406772635982CEBC3153D046161C7B8BC4D87B648236263A44ED50B92DEBB624528C7A984C4C5047737A8021665279E9CF30069AADCC3F4A9245700436E646AF721A612828A07DC463D61B70223C7658F22190C83405999D92889CB0E5020052C31066665C4A021643C20764C370B9A5D1975D47646E1329072335432A19A03590B071482A3774BCA0601D8DBCC35A63742CD127F6384AFC6A971EE8CDD3E13687588D84C358A5537E753B2736840FD4E76A1CD0C68F9A990E5644404BBA84491E5F3B6F23EC45DF91538BD54070A702E69B66BF6C88CBDB8FDE693A2487681379A2AF94C592472FF167BD17966F9DF67E8FC51152AC613512930BA2BC2F453EEBCA91399B6D8F12AE891B3DEAECCFF120751866369BF8902D439D19F77B264002E5901B1C939FD86CA623277E6B09A72A93B52AB00754EAAA74833B8E366C8F0752639437F83CCC00F92EB7057C43AB94A75C31D5F854DCD3784920A36CA0C2DE244D1AE3A9E578B98E0634635549A8932A6BEC1F2EB321572A060EB110AF473F3E04323FD868A241891227222414C663285B05777BE82585DF3AB0ACFC121A642C541B3C3D7BB3157302EF62A1229156DB3609678B3B3E2576902145E5ECC78B65CF69BC8A7E6631494B804C4C89A9B595A8D95C039A9BA356670A536220C1CB54AC5BFCB7CE0125606A40C4EB4BAFC4C43BECFBAC84812F70C66796D6407F887CB0DB41F0FA7062E255896582B2B218FD96B6E5581A6FC26ED40272836950CB348AFE78CB135ABB6D015F299377632C72A7BBB2CDA7CBEC689E7551168483085B6B7426EB6967F729AD695D4E09C49B7A0010F80EAB54CBBAF4B641C956231663DB83512C97067CC7BFBEDCA93CC9A739A813B48C4793354C2DC642F869307AA191B77C2BC72BB048300B54906DFBD45255E92D81A98CD87149FEC3A428540D4E03C757260F3731CB7C4743A37194054AAE5FD6902D48A911F661EAD29321722E8243CE62E75ED82657423163A86362CAC2931954092909B0082BAD382A013A5044FAE9A12BF300670CA5771042933764E700B43259B130F033B023CBDA36815E2C6BC7C0601CEB7586A95DA4033E80E32123AB86C6135222EBAC0EDC17F5549D5B66760E07B2B16BA4FD8040E642990128A39A636B19FD3EDA4611BC1CDFD552AD1DB338FE3700F0920D56F3\",\n          \"dk\": \"0CC61046A844640446F07C1683D649F1168A00F1C859798944790CE1F884B2A99C177C579E314C91B33E483B86B64143EEC23F1587C8E038A42C85397A47CA7600C772D492E4CA5F7DC815100B9155449E72A0A526D24035FB20074941F0622FA9369E79D46378E45D5564245B0C10D9D873420895059428965CC22E064B349A4E40346F77A84BF7734C598C5775D39E1ECBCE2A6BCE6FA5CEA4F41A05949ED766C1A290299D9BB47DDC2E7A782F386C42D2AA04C991304BDB108801AB6802B04259757A63A88B7492A2FB2E93A1AE7646267F434C724134154C407808CEE0A7983C32727E44766E5C34EFD16C52086519FA0DC6C7098282555C069F76832F7707477F69566280BB6D406ED1714205B73A5711C449B91B4F8A2C2638B65221493BD940B5370A4C661F6F9C27AEA6A69DD0B61CEAA861E78328941091A95300602878222290CBC67E907D6C4CB2BD6623CCC5A23E46322498BAC5945E6B28283D7401B30401753859A2D4818ACC8D71437961DA02F2E8B5BA94B494E7A215F518C6BC8214C57237108C6B183F1551696A909C828A0E076341A5C141ED62A9987932D002A714366D8621CCDB0272E7923C6F3461AD341C145A864C6655C8E02AC0C91CC0613CE7D48027206C7D4988D82A1340A7BD337B82FA26400A4B97B5D819AF154CDFA639DA3A522DAA0F524B2B77FB422B8216FA83B8D94903452BCF5054A5081741B2E93F40548793B39B7197AAF027907B763179269CB06671B0CC2820F6015FA158CE93770A1A51CBF033ABA1A5C4363A1AABCA93F2800DC011B19B952774C4F90991001C19F3A688B2E01C934A7BFA705CD093AF85BA5D20FCB498E5C91B186378B63C04123D19A8C2B5209D7D8799EA7192BC3876D1943F17FB63B8185D89A37483AB74E62BB03C27BA845CCE57309DA7A8055642B9AC51B820AA8F61F88B819804DCD8C3CDCA2AA816BDABE9725BF83C61796ECB069D183600FEBC41114182338C97B67618DF818DCA151AED240E36C1A0DCA654886C6CD3731A92E3CE81D7CBB4BB1CCDD238C54C44E89126641AAEE1145BBC378C92C56E400B5DE838A18FB66D365501B406287ED8AF6D03CBCA4BB617B42DBEFB4A0435319A00133F4AB70631C1045515C6C23310F50406780F9E891187FC96736296B9E7AB1609C51A103AC34667925901EE3A9116CA97FB46B8325B13A64B008A88B03E0C383E152D7F8593B6638BA7F9AB500968A830042CBB50638A58AEC5B6BBC1938E5C7A605A80B161BC18841873ABB5F38ABCDDE99464736D3505C9EEB86E8A95747CF9CF3B9A52BB115505B156928A79DC9B9E1F110987D4808AA2C27B806D7B746014476E571AC2F416AABADB36CF994813887195CB63C15C678D480443CC0D2C60365E358F1FA9897A12279CE928E2CA8D67006D48592F54EA4CDEAC2748940AF558356760C420EB71DE2ACADB064586C5C01A698330266DC78C95DB8CB51907C6DD94B507C107E9FA7210C60FA5D8888A3CAD7AC1B6FEE59159D6212403B2B21944D0AC35B0137989F002D4F13E8B873F8B53B3ACB09478F4C00A7486AF0B5FCBD42E21654FF3DB786F2307E7E60236EBC89C36971E2920A27C5E7125BA1DC77B4F944BFDA8C4D3E09BC8A00B3099B6EFEC5F7F59AD925B79C3311A7F02433FB9133F845B58E0BEEF5611DE6890E45AA75645C34F832B08B7655CD6424F733FEDB9825EF3340A010E72781D16F8180F227110402816E6524D8B341EF5CBD098A8CBEB52C02B715D1789B0D38250EA6730E506E569A9E32A5A35C7341181B9E0C46296E91F3FE9C5A7159F9126BE378BAC03D467C7A39AD1605DDB2A3C7FF930A85C0710114ED2094956E005DD770699F4CD50767E53C15D5251B2621336CD6974CCD446C1C4028C62839EB699D869758E502CA158A39798688D95B1C4509EE9C57383209D69D105D56AB55A59955F78A65E49B5A4FC4A5791AE5D79118CE2B22AA9BA2DCB963BD672A6861D78C30D6873893E15C62A428BA3E77D09C94822693166E39C32E32666F4537E70A641181C13609F1C3535A91486FC3ACCCB29A0EC0133DF7917B71B8EE4509264788356E967335942014213E4862AA8B7691AC207A996711AD1AAFC30BCBADC0A35514F3FFAB45E53197E89B918AC75B504647A3684E8A65EF0C7C7D7391DAB233EB277243732CAA45B53315B645FF9721D8AB0CFE54FF717157663B9098C2267633EFE3544A88014AD15C7B10A4551CB5F9BBA3D74946371B1AA54E6419CA7B4611149B5F02C73819A8C625AC52618F771202338AFF05969ED670BDD0B9DB562B8055884CBEA7A208A2DA850A05484C6B84109A5C2B498E8B8640ACC6F7AA6E1D7A95415469CC1C06D17176146CEFBA58E2B8929770851EBD6CA5DBA444548C622673DFBEBBC95767BEE2B86FF5BB37CF306B2945071EC1FA80C7540C56A36EB684D71C5E8B90AAA8C2421E21D64E0638D63C96E2B0BD9457BD5062199A01253D240C712315778AF487AAFBE28C650532A493C1B3E0251BC0841392839905AAD3D12289506742073862E8549E7E87EC28102DB7B89EBE91627CB95C0928C921978CB8C94875A94F1557498132200B4B76AA3C3AC9755C3B6BC5E5C59C9B3C6F7FB2CBB5AA457904FEFD2C370E29EEB2417BC45977BEA02A0DB9DE57937372298102755BA5B007EC88340B28AE7B819F9E07182F76070043CF3AA7E9256CF1CC3A6442B1726EB889CAB193222BCD3D8141843C1EF9921B8254712C04529D38846F595BC011B3288BCE0231ABC96985145043B579BB0864F40EBA6A66144E0972CDB10A71E44A088112B3C7907E5591D6EE85B34D4AF8DC58633081BA1A0A6EFE133B4C70ED215A22F451A7117B8C5F6A3D3FC4455214F5DCB8C31370546B4C4BA365A25017A9C2296AA641CBC4745BCAA72D17C9EAAB0155627BB45770D6D259BDF5B044053B4C7C8784FC433E89942B5D6C807A0707842CA4FBB98693303C118C9F4952123582FC4E22BB853944BFC1584810153F3AF5DBB74BB2928785C542AB647E9321D724AAC9EB9091707B5075C700A4817CAA2C355D4C99B8C5037B5288FF5279725BEBDAA5CE23609E5320981F11C8A116501D5ADD2D2B057C41E469257057B7A9C508017245C20C13A0C104FE1A517EE4621C367CE283183C07675BBE5895FBA4F30CC406772635982CEBC3153D046161C7B8BC4D87B648236263A44ED50B92DEBB624528C7A984C4C5047737A8021665279E9CF30069AADCC3F4A9245700436E646AF721A612828A07DC463D61B70223C7658F22190C83405999D92889CB0E5020052C31066665C4A021643C20764C370B9A5D1975D47646E1329072335432A19A03590B071482A3774BCA0601D8DBCC35A63742CD127F6384AFC6A971EE8CDD3E13687588D84C358A5537E753B2736840FD4E76A1CD0C68F9A990E5644404BBA84491E5F3B6F23EC45DF91538BD54070A702E69B66BF6C88CBDB8FDE693A2487681379A2AF94C592472FF167BD17966F9DF67E8FC51152AC613512930BA2BC2F453EEBCA91399B6D8F12AE891B3DEAECCFF120751866369BF8902D439D19F77B264002E5901B1C939FD86CA623277E6B09A72A93B52AB00754EAAA74833B8E366C8F0752639437F83CCC00F92EB7057C43AB94A75C31D5F854DCD3784920A36CA0C2DE244D1AE3A9E578B98E0634635549A8932A6BEC1F2EB321572A060EB110AF473F3E04323FD868A241891227222414C663285B05777BE82585DF3AB0ACFC121A642C541B3C3D7BB3157302EF62A1229156DB3609678B3B3E2576902145E5ECC78B65CF69BC8A7E6631494B804C4C89A9B595A8D95C039A9BA356670A536220C1CB54AC5BFCB7CE0125606A40C4EB4BAFC4C43BECFBAC84812F70C66796D6407F887CB0DB41F0FA7062E255896582B2B218FD96B6E5581A6FC26ED40272836950CB348AFE78CB135ABB6D015F299377632C72A7BBB2CDA7CBEC689E7551168483085B6B7426EB6967F729AD695D4E09C49B7A0010F80EAB54CBBAF4B641C956231663DB83512C97067CC7BFBEDCA93CC9A739A813B48C4793354C2DC642F869307AA191B77C2BC72BB048300B54906DFBD45255E92D81A98CD87149FEC3A428540D4E03C757260F3731CB7C4743A37194054AAE5FD6902D48A911F661EAD29321722E8243CE62E75ED82657423163A86362CAC2931954092909B0082BAD382A013A5044FAE9A12BF300670CA5771042933764E700B43259B130F033B023CBDA36815E2C6BC7C0601CEB7586A95DA4033E80E32123AB86C6135222EBAC0EDC17F5549D5B66760E07B2B16BA4FD8040E642990128A39A636B19FD3EDA4611BC1CDFD552AD1DB338FE3700F0920D56F3771F1733A4C185573FFD9BC77988A1458D28A64F15512217C7B95C24D7CF48904E638D8AC3662450E09D8500DED751060B7990D54F137508B9897277F65EA952\"\n        },\n        {\n          \"tcId\": 73,\n          \"ek\": \"379C74CE8940E12A1790659FD6C2431D7C1043A8049A3B0AC1000273A0A68EC34AC6435018C19F3A5A18CD28AFF1254C27E92E25149E488A5CF5C724873A7CDEE0182CB874E1564F6036CCC6E5C02FD19937D459ACAA589BC3706BB60E82C93047030524A73C3831232659565FF49DBAB60BC8C18E203B68DE7784D96B4C1969AD8F164EFD531F8A297F46425CBBD0C4E3C858A01982DD4686860B0253D1067AC66353F7554DF00A2FFC46858C6693F04499054458864A4A7AA82230A4B04C1EFF8A2B9C5B1A43F0038A027CCDF73EDA1B16CE878D14B27DBC2AA0019BAED2C86F9741A9DFF624071545A4CABBC2895D9E9602C1027D05EB1C70DC2365423A65740F777565503C53B31C9D7DD06DF115BC2CC1BA80D992D185C485A78FB6A0B725E2A93897CF04F1B421093FA098BAF4B829FED6CC07073F0D13502E430F892A5E344B431DDA1EC9270A0DB64CBF40CFDC78293826BF4701181594B89F8CA52F3168DB931E7242121A71747564AF2DF0312BA4659A8BA2106983957877673269313B953534824E035AB75A5007B7BB5832B37936683B23B563FAB86EB749819616AC467B42D4B11922A9719067039480849B13E125353F54179B5524C7B5905A673E54EC888614AFA7781B918951C2C30E89043119351965256B26CC5B8E1973C1E838C6297D1305D02351A1C2E394DDA666D918545BB8CB69CB8141645AC7CB606A0500E0F94961CC8B33A5250E191169C3B4CBB927438764B356928431A7D5E73067346C9EA02D7F687A9DF1BE9018473CC7442F801B43F2307FD2380329C5D3DB8802823864A8895C0262A97547682A61A1652F4611717C09AC6DF8886D67B1213739DCE29271F65564CC642851005E224D634C996556AE35E3CCA9A6C746F7609CBB526C9B9CB144CEF1F39DBCD11207F43FB7296193B12F1E7B043DAC724FF9A32581192E13166FC91F10CC5330D6A7C4F04BD3248C61D227E70543E041354D55B1E52AC1BDA0657ED3C6F5F3444B3775DE1B0C337409C34B75A5B9A58DA8B7445C2E3A6BCCF1E02A0C1A20DC619893E6370D35BA4CB615D5B2BF12885749865675B9A925B21EE138830E20B2BAD258349A44747A08FC962476A0A7A1676415539086A543A087737DCC8E2565161F8697043819206202AB240C00C496D5254E8B0610D6C55CE52B20B0F63D65092F95879736D989728AA6EC91588F60682512CC50BCAA756A12F13C6C502BBB6CF210A7877DE46831A1E91D7549B50AC391A439BCDF2135FE81C81B25874189726D77AF828876A4F34E19E33009B8217E511DB2A7724B251109646E112823242C964B933F8B4625EDCB1471DC714684045AE79811A56B113803EB272AAB94709421239B009C3D4CC8463906DE646E7374C865103AACD4190206C43BD2334D496005427FDA19CF31872A941C75A6B1663F6C57754CA0D9D8A23A1B9592A1B3FF76A035F89B72B29E6207D05BAC0C349608E71B2E4A3CB3EA7651A26B374CE516C787976691BE124AB671049F3B573905D19906CA6E9C5912F6311410CA3A7B25973895C0E8733557A463E311BA20191EB77B3EDDDA437BC75D5F358AC883B2E8242449834AB6F5ACBF0422ECB438AA4009BB678E88D033C701322DC01D0F9B47EC135A25CB2B491B7F45B33686788F603B28E0A215AA1AA49BC2B2B10906F70004DAEB16F4E5805DB455BD794861BA1F8D55BA6CE29591728CBA492B89255762B25DDB71557E8366601535DD38609652CF47506379673B0124796C3CA7EB384EA0830DBB318E553525DEA69187A2151A227016DC53684C1922A25A90107C64E04CE1C7AF8481A437601D7C4AA014D18CB397B4079C660736453B35A5BAAB9371F0B5D4F867CE15AAA8739F6659B12FB93667DA30ABA01CE923853DD26187BB20A845691AB48785B22767ACC0D7389BA260BF115873CB784CB4C715E67776759511DEEC2B565361B84A8833C7A16C16AA3F29750397B0ABAA9B73A580527100511686A19A7AE757833EF826F4D21E4C020618809A48B67156711E70D5171BCCCF997CC048BBBB2AE064FFB9143F66A2E81A9341831999D822AD947CE88CB153BA595D5B0EF5D08EB856929412B9D2E551C959482AB545B1A0AD8583B7C0D42A95A3A080B772C77424A1AA1852DCA0662BCBA7B96CFAC5B1B689F01A5CEAEB2A0D52EB0FE9BF752B36B37830846812FFB88D\",\n          \"dk\": \"2ED75F448A5247A0BDE73C1A09DA83F27512A9E1998A48C80A0962ED307C0F8C4D221584D3A90D6C7B003EABBA2A2013F4D74CEF9A84AD123E430B316C08AFB04A64EFB31278F4CFAF1156285679203A275B267AAF7786346B40566B5BEC24B122C89108683431D0A8E8531EC70116F648004A9BA3DA807E54755BB10B9DDE973DD1E62C1A6C752D09B18DC00178C52A6F0504ABAA2B3E54B1D1A495A9A38BBF29812C472D28A8386F04BC41F555263A8DF34536E1925434407B3F885F7349A2C20BA5F275BBA62039F77A0BDD236EE4A7205B660AC04A48D6EC0927EC8986C73539754545E96B3F681F461A5424B954C5C69F1899BB8301B0138BBBD934A764168951B0BB42043FC6A441BC329574C726E9D31286063DA4556C02087FFC473C3B4307986A59AA595139A46351FBAAC3E1664B5A4A74D5916EB3BCB0F570B330ADF4574A92589C782A3CA520C43C4BC7987367BC64B9EC1629C41C80223154FFA3161A158647468B8DC670DA26496FB53F1BAB59261B8B9A3A725AC992567ABAECD027856650A9D78FA4656E3BA89BFEB80F7007562AC84EAB2917E18432CC5A4BCB969DEA5C73F1AC620612574F3106D1646FC266959723004C425BEA6B8B8E6326365C540C2844F666A482116D16C1272D90A439567706159C90623B1A79B62B14860926568C32360EDC9C868211E0036C9A3979B6F39126A150A9259CABC8631D9214CB43A8564A6DDBF785AFD44EDCCC7D2428A92766AEBA969D9E065D05E772C6DA572F9B0A7C5A7C0E54020B331CDF321CE7E48EAD96C4EE5062509113E274578F50A7AD131641BB3CEC76CA79B39529F92886B40F22086A6F527623E37797F1009E082277CA420C3310C0F35F2BD61776B8C5FCE14136DA388FE6CC37F3A7496C0062D0ADDC91B8ABA38817F45A7996AE0F7BA41D32C1FC255A43A6BBD9DC6A65A6BB72A306A3446418A77BC58C1D3B1B05437C6F85D01A300C9FADBA40F8F4B8DEA2CC358547165A467AD12BEC5C80C4F7AD6F308550B419BB06AE54CA29011A55502215CB0397C005544404B9A413CF74F5AFAA532C909A3D00FD37146721FB0B35363B9C52B71511D6C45B719E33CC43E016770936BDF6D2056AC21FF65CC813321144B98C965530A80654EFB350EF973AD505791766BDB4734C3E1A0394A89902416C3DE269B9A99E4B6CC39D64BCFAD64A552728905882278A238B214B94F62856C14C6294B8FFE80F3E62B3C91A687503094C2849BE8254314B56CDD35617D61F27C9960388811C830290808F9D0B2DA6AA4728606F55CABCB92AA9384CC3E03537BAA41BE636C7EFB036B5114019E40C74984884DBB512D61CC8A973D8E6C8EAD45E235C7E6235C181C062E82CBD26A2859BD9B510514D83114847ECA0DB8554663B1D74E5084460C396E43F3FF47D379A4574C77E8F1C11CDC5974471663244CE2EC399AAE83CF96889BE7628ED028E477C0658993941AB737861A9F8388AD1309BBBB23EE72181A1EC7BF445B1FCC5527E9C0F7D278362548131CB2132691E232515CB5C5CB6640E01F8B75D96A9E60ABB6929A7582219A8A8CCDAB2C4A03A035B8CB416114FF7604692427B35C135DB956F6BA796145CAFFB066663293FEEB462EF5C4D8C521B86436F011157245A891F233AA122C51AAA0CB0D5100B01575EB04B9A1B86D6A1808357265DA0570DDCCA2766C5802333C64B0D9E5A254A8645328C394F85B337A447F17753BB465F584462B40518705094D891C6143114522BA327547B61C928CF50758FA5C0C6FB2255A51E13E9AAC37000053C180EC7A6E597BF57DC656123793EC0CB24E9306FC0CA5CB723C8F46174179923E819A1DC760229018D7348AAD75263DA565129A39006299CD979BC73062003D0773611B07141FF2265F8E5583284201A4111F5719BD4397909D892AE8AA438121C5DCBABB9413194542E6D98AF6655871DF23C6A7A43B6B6C9D0790F53705F419B226497BBE92A3EC6315C6A443878B283A252423B13505DA298AE100A6E34BDEB734DAF0588BD54B2D5C1198C653B34A8C4D0447C5B6B137D203BB2612E46AC4B82BB97931550F3F835B57B8521828718498A3405C36A345A1323924221505906723847571C6985B0A59F1FE15C961B5F11630D04CA79FFE22F371A96379C74CE8940E12A1790659FD6C2431D7C1043A8049A3B0AC1000273A0A68EC34AC6435018C19F3A5A18CD28AFF1254C27E92E25149E488A5CF5C724873A7CDEE0182CB874E1564F6036CCC6E5C02FD19937D459ACAA589BC3706BB60E82C93047030524A73C3831232659565FF49DBAB60BC8C18E203B68DE7784D96B4C1969AD8F164EFD531F8A297F46425CBBD0C4E3C858A01982DD4686860B0253D1067AC66353F7554DF00A2FFC46858C6693F04499054458864A4A7AA82230A4B04C1EFF8A2B9C5B1A43F0038A027CCDF73EDA1B16CE878D14B27DBC2AA0019BAED2C86F9741A9DFF624071545A4CABBC2895D9E9602C1027D05EB1C70DC2365423A65740F777565503C53B31C9D7DD06DF115BC2CC1BA80D992D185C485A78FB6A0B725E2A93897CF04F1B421093FA098BAF4B829FED6CC07073F0D13502E430F892A5E344B431DDA1EC9270A0DB64CBF40CFDC78293826BF4701181594B89F8CA52F3168DB931E7242121A71747564AF2DF0312BA4659A8BA2106983957877673269313B953534824E035AB75A5007B7BB5832B37936683B23B563FAB86EB749819616AC467B42D4B11922A9719067039480849B13E125353F54179B5524C7B5905A673E54EC888614AFA7781B918951C2C30E89043119351965256B26CC5B8E1973C1E838C6297D1305D02351A1C2E394DDA666D918545BB8CB69CB8141645AC7CB606A0500E0F94961CC8B33A5250E191169C3B4CBB927438764B356928431A7D5E73067346C9EA02D7F687A9DF1BE9018473CC7442F801B43F2307FD2380329C5D3DB8802823864A8895C0262A97547682A61A1652F4611717C09AC6DF8886D67B1213739DCE29271F65564CC642851005E224D634C996556AE35E3CCA9A6C746F7609CBB526C9B9CB144CEF1F39DBCD11207F43FB7296193B12F1E7B043DAC724FF9A32581192E13166FC91F10CC5330D6A7C4F04BD3248C61D227E70543E041354D55B1E52AC1BDA0657ED3C6F5F3444B3775DE1B0C337409C34B75A5B9A58DA8B7445C2E3A6BCCF1E02A0C1A20DC619893E6370D35BA4CB615D5B2BF12885749865675B9A925B21EE138830E20B2BAD258349A44747A08FC962476A0A7A1676415539086A543A087737DCC8E2565161F8697043819206202AB240C00C496D5254E8B0610D6C55CE52B20B0F63D65092F95879736D989728AA6EC91588F60682512CC50BCAA756A12F13C6C502BBB6CF210A7877DE46831A1E91D7549B50AC391A439BCDF2135FE81C81B25874189726D77AF828876A4F34E19E33009B8217E511DB2A7724B251109646E112823242C964B933F8B4625EDCB1471DC714684045AE79811A56B113803EB272AAB94709421239B009C3D4CC8463906DE646E7374C865103AACD4190206C43BD2334D496005427FDA19CF31872A941C75A6B1663F6C57754CA0D9D8A23A1B9592A1B3FF76A035F89B72B29E6207D05BAC0C349608E71B2E4A3CB3EA7651A26B374CE516C787976691BE124AB671049F3B573905D19906CA6E9C5912F6311410CA3A7B25973895C0E8733557A463E311BA20191EB77B3EDDDA437BC75D5F358AC883B2E8242449834AB6F5ACBF0422ECB438AA4009BB678E88D033C701322DC01D0F9B47EC135A25CB2B491B7F45B33686788F603B28E0A215AA1AA49BC2B2B10906F70004DAEB16F4E5805DB455BD794861BA1F8D55BA6CE29591728CBA492B89255762B25DDB71557E8366601535DD38609652CF47506379673B0124796C3CA7EB384EA0830DBB318E553525DEA69187A2151A227016DC53684C1922A25A90107C64E04CE1C7AF8481A437601D7C4AA014D18CB397B4079C660736453B35A5BAAB9371F0B5D4F867CE15AAA8739F6659B12FB93667DA30ABA01CE923853DD26187BB20A845691AB48785B22767ACC0D7389BA260BF115873CB784CB4C715E67776759511DEEC2B565361B84A8833C7A16C16AA3F29750397B0ABAA9B73A580527100511686A19A7AE757833EF826F4D21E4C020618809A48B67156711E70D5171BCCCF997CC048BBBB2AE064FFB9143F66A2E81A9341831999D822AD947CE88CB153BA595D5B0EF5D08EB856929412B9D2E551C959482AB545B1A0AD8583B7C0D42A95A3A080B772C77424A1AA1852DCA0662BCBA7B96CFAC5B1B689F01A5CEAEB2A0D52EB0FE9BF752B36B37830846812FFB88DD27339E75E5E384EBA68A71FE2E52EC7AB0C15CFE33BBAFC892DB62D84ED070E7459AB99D24C1254EEECC035874BF19A64EFC8EDC9D369C11F5DF4DC83AB5FBC\"\n        },\n        {\n          \"tcId\": 74,\n          \"ek\": \"26160EB381AE1A868F10FA05934958B39790C5858085A82036224D2455823F11A3A5C537DB483A3AA7725ACA567CAC610B4618E3CC0381701FB60917F5DA63CF712618535210D102238A5768F876A17C4437AB8789662F426C5CE69258C3462D70636CD556AD9EF0A2CD9C1DC3C546A0B396CBE2C94BB24C40756815F796AEE7045B6082DCC49EA56849C639419871C480185E51E4AECFA1945DA10B43C366916531ABA8A01502881B76C6E6984F7EA6058DE460C1E3511A06631670B35ED6A8D5D7BC6589A62DE377BAD1582C085A3DB3BC47F176A27604035B9ED13853758A77AA1391E881214894013F1BAB687A699DF18B373A64073BCCBEE469E1C867D221C4675791F40102E1E2B12593152C87A47A9367833854EF5427DE49043308586673274DEA8C2AC8AC5EF5886D06205284831170BC6B9C0844248DF1CB40C5B627A29421AE900EF5F5666720304520ABD826AEC0476E0D32213D550D55442E863B0FB8ECB85A1CBDA002792F6C199838613780BA4CB22605B30372304F88897EEDFA09B6B0C4B4892C0A05746DB80F602C016230129C2B0C83351413D4C2197B92E608485301BFA2EBA2BC5C2E1EFB42109B0119F079560A87F01BC6CFF916E81A1AA4F520C0687C41C91A3BB5C4C1D712449C6E927C0490B2AB4B9422BF5655E332C4A1D1626E587F085A631C250C122537F5B213A0C231DC48CA7B88A4C21706FE701645F43A8A00CFA0F0A13900B3FB5C1B886C66F22520FD685AD0CC396991B0ECE81635BC27AD2813FC377E7A542D9A9602E22775CA34CE85B834ED755048286B82692E3F9AA15E89A8D8568FA2AC47AA32C748582B7889B447C079F404329FE03066E8A46412820EDC8B8878840DE54C80CC2851E08374A89FE8F7417CF90638726ECA61072C893CAF8C501D04CFD2F3BFFC094503234693C530A732324C0516E412137E2B39AFF1292B925205C972F1B6805E67A292B069E8CA73FFFB1DD2B1A5FB2057ABB67ED396494D123D05A579C23263B47ACEAC5862982B20E6BC5E32E03BCA175CDD8996044AA9D90B3A0488500DB340C06CC9B84749B4497AB6A3CDF05B58FD5850CFD911B5C58819D2444CB9386B18784762B16DF6BFBCE86BEDC9CA6EDC191171C88C5060648B419109C5BDDA9A7E98C48B1649B342B7ECA411959A9D62E5C0BEFC5B62F4BB6EE608CA18B43AF78784B8C7384ABC77426B5CD28EE70B93F808317230A686900C83512E2A83CD10749C3E5B7EE6826C1E01CC5FB27FC47B52DA462AE5EB65390A1A33CC9749941D530210AD070AB71487C2176B17621ADB11CFA1F3AA9C379C9FB0CDF00186608246C56584DB61AAA8C157A28B6161F23EDEB7365928C1EA8C962785C338E66FF1897B56154475E17F81C9845C4607E5294E21EA23EE0CC47E4BB9D3539D46D410A3149E098B539B196A2C90C06D5C103AAB08E1110D3F565AA8CC7E4F2167E6969BEDD5C3CC8A6F68F53E18931F8EF0723FE33A2789661C248B15A35ABD5919343AADC93BC6777539E952A3F49A3E34F51291709F6DBCBDCB65B4760B8795B7985DF8249EEA132A473CAB736E72D45A2A100FB560B70E600EB24AA596879F1B1A9DB705AAA489791A503E9463C19EDAA4ADAAADB77A6FB0BABE16973E63333F03B444C85C980D55727532765397B84870BB759469CEA72E28A75BBA8A5098B6A95750ADAB54361C802ADEA678FB6733D3766F7C50BAFC081A384A768E60AA2E6648ED81612778033C6038E7863887F19C1509518E61ABCFF77F1ED9459D10C53C0CC6AD60C95704138B9C579317714F393AE8287909ABAB7FA167B8B237CA52056A199D7558AD5F16821DD364B11B9307421D362BA42AA3002AD2025E347BCD7223EE4BA62C354C1CE9822C8C4B3B70A1708462453292C73ABB177949EA3106FE3075CBA15E2E143DD13A6600E55D801A440004621F6ACDC3CB8D53236FCCDC15B9F9CD321398AA230DA6722DDACBB98106367B3723FEE198E1AA7161C004C9BA1840F19721AA7D99B99A7057B1B33A28F2033904750ED1899551C1C8A9132D86B3781264CAC231C3A21440580A92F4C683CA90BC1CA78CE41B332EEA7D2F461425C9B91FB6867B4270AB785329218D74356A3B8423A044C3C6653B09E60821A1CA6032A0BC526228F18B9E38B3A7B69EA6C1C5C0C39DE56DA9763517FA3F65CEBED43C7B61282772DBC9\",\n          \"dk\": \"A0CBC9A9EB303971AE19D11CB1023CAE83B0578A8BADF6BBCFB444000041941120984930702ABE4D0442E79921D92CCD08364112D41433013110D74ADF2B53A8164582F56E2DF03FC1D283424A16603CCA86C8BB3211030574553A221A4236817DAB9935899201465FCED39A2E0845DDF09F2B346FD3FBB2B6EB72C7C6CB8EA1784A010AC17A79041408A1B7853BDA03161A478D48AC4D19C32F728C36D2CAC5C4784449A0BB5C02C4E506EDF7A7A1A8548F931FF36014A8AA651527A4AFF04AB5E52A29058DA78388192BA72D17A9F3428E784999C9F59ADC561CA971021C7C9DB552926D572F44F9B2B014ACA32A267EFBCB62D1962F48B6D867C12FA25723736088E80CD3024A2610C727CA96AA8C495A8688DD4C5F804C308D4A1693E6B2C676B3B44C127A4911F8522308E20EB5E816A8949D93303BBBA150744511A9C23259F663AB3444AE787FF766A3D76859637588C21634F7338FE98596911A92E1692E75D66CF37AAD1C02720DD47315B1AD9DA95AEDF383D6863D7F06214E626B3299A5EEA648CAC40067C5C271A81A8328C7612858BE4C736ABC35E3F982CD84C596685A3F974D61F450D130C1159993BADC5AEF7B71F9E1503BF2525FD2B191E95E35A852D65082928C47DD989646FC5FA9E92292EBB03FA4B52473B57FF59E52ACC2311844C59104F51147CB600D0DFB42205A225DB32CE6843557F61E3199BE1E8A237AEB1240B51113233C92E453FED3138501327948A68373457AD6271ABBB6CCF860DA15241F89C27D5074B6A7170407B3024708FB3689318AC024E0CCA2836D3D786737D298CA7559FDA75F38CC8CD3D4845A165E18EAA56547928B0875AF564F763C365839879AEB3962F520D37755A17983826843E55567CF2B45F2B59D716666528256651694FD4B5913D90A25637369779991E9246E6A59EE1437D7DBB62466A759301CD0F4CC551161C2662E0D0992DDF07CB4BC61EFAB0D4F81700C0694F971AF7CB0974367AA53A8A914770D03892B639746AF79179AB8405C08A5DF9525E5958D26C4543A82349A192A86D3529F660BFC49490B2147D9170FB5CB78FCB40863550E9AC42DD3215A2D86B3F3A4B0188A9F7F02949CA48A63DBA56224B436B2C22C085EF826AB521B0DBAA4A7ADE8355FA3C4BD708EA1AB68B1FA714D5841208BA9CF3611A9854F69A21E1B67C84903CC382B3663ECC2AA4B069A8639618536FB6B562CCA390C103645D9A06481666E92C1FDE91AD8291596286501B154C8E3AA6E0608E4CA6FB1512EBD25C9EB789E7765B342AABAC415B7B1E65FD38ACC880C61E1C01E96BB8FAC50972D536D49A20B46BAA303DAA481A39D68C71286495F248045B1B82020544B864C3E1951812D01169B4AB79510A284F058CFBC963431BA0FB7209BE995672A264DA3A6EF592EB121093D9B09F35A94BAF125C4F61508B1C339C6B58C5BA65A207571191253C3512503C400C70BEDD9859E363A7A1193CBB2C222782B77B62CDDD09F67BA9F4D632930E73E08F37F24279965C74BB402439E0A8289A43630983BAC924D0261BA07335B3F734428F538B2803DD47B26B2D5183533807CC43B67E79680E6C5E60709406B04548781CF497E8B890423F42186292FB8C988639C2F6DACC513014B91EA2D54393CFF768ACC42504DA84F21A21D372A2C7935334654856BD66C77423F10501CBCD39907EB7478FC0D7D0439C9739B43A363E324B319B219BD25BC1153AABA81B0F6A84B1D5A8C92885EBAC488099BCF47E91C8E4AA8EFACC0748B25B2969EDE004B4BDC3278B139C2445CDCA181FF8A3FD20777DA97CA9D182AC494336D424D87B53402596A0BA339611B5EF459B87C0163744007B008467387B44A680F3D916C8AD9352D7B6643509225874A05E03D2B0297739592AAA37772472162393CE7238482B51E8B6297E665CD9FEAC4B8664BA62CAB68FA25DA4459816922AD50AD06A347E40C4E5E55CF9A5CB738343DFE222FE46349E68A6526B1B1AA8CB5549BAEE622C2DB3C8806AB22F3C92449A67D3C7C5AF7A9C970839AA386A6D5518E00D8B11F42002F52C873DB6A4DB50047F815EE15C3D017676857023C57A017E431CE8555CEBA2BF48950F89095C2D278A1330D9F9416BE6442EC49A21C79BCFCF832677A4CEC8ACE26160EB381AE1A868F10FA05934958B39790C5858085A82036224D2455823F11A3A5C537DB483A3AA7725ACA567CAC610B4618E3CC0381701FB60917F5DA63CF712618535210D102238A5768F876A17C4437AB8789662F426C5CE69258C3462D70636CD556AD9EF0A2CD9C1DC3C546A0B396CBE2C94BB24C40756815F796AEE7045B6082DCC49EA56849C639419871C480185E51E4AECFA1945DA10B43C366916531ABA8A01502881B76C6E6984F7EA6058DE460C1E3511A06631670B35ED6A8D5D7BC6589A62DE377BAD1582C085A3DB3BC47F176A27604035B9ED13853758A77AA1391E881214894013F1BAB687A699DF18B373A64073BCCBEE469E1C867D221C4675791F40102E1E2B12593152C87A47A9367833854EF5427DE49043308586673274DEA8C2AC8AC5EF5886D06205284831170BC6B9C0844248DF1CB40C5B627A29421AE900EF5F5666720304520ABD826AEC0476E0D32213D550D55442E863B0FB8ECB85A1CBDA002792F6C199838613780BA4CB22605B30372304F88897EEDFA09B6B0C4B4892C0A05746DB80F602C016230129C2B0C83351413D4C2197B92E608485301BFA2EBA2BC5C2E1EFB42109B0119F079560A87F01BC6CFF916E81A1AA4F520C0687C41C91A3BB5C4C1D712449C6E927C0490B2AB4B9422BF5655E332C4A1D1626E587F085A631C250C122537F5B213A0C231DC48CA7B88A4C21706FE701645F43A8A00CFA0F0A13900B3FB5C1B886C66F22520FD685AD0CC396991B0ECE81635BC27AD2813FC377E7A542D9A9602E22775CA34CE85B834ED755048286B82692E3F9AA15E89A8D8568FA2AC47AA32C748582B7889B447C079F404329FE03066E8A46412820EDC8B8878840DE54C80CC2851E08374A89FE8F7417CF90638726ECA61072C893CAF8C501D04CFD2F3BFFC094503234693C530A732324C0516E412137E2B39AFF1292B925205C972F1B6805E67A292B069E8CA73FFFB1DD2B1A5FB2057ABB67ED396494D123D05A579C23263B47ACEAC5862982B20E6BC5E32E03BCA175CDD8996044AA9D90B3A0488500DB340C06CC9B84749B4497AB6A3CDF05B58FD5850CFD911B5C58819D2444CB9386B18784762B16DF6BFBCE86BEDC9CA6EDC191171C88C5060648B419109C5BDDA9A7E98C48B1649B342B7ECA411959A9D62E5C0BEFC5B62F4BB6EE608CA18B43AF78784B8C7384ABC77426B5CD28EE70B93F808317230A686900C83512E2A83CD10749C3E5B7EE6826C1E01CC5FB27FC47B52DA462AE5EB65390A1A33CC9749941D530210AD070AB71487C2176B17621ADB11CFA1F3AA9C379C9FB0CDF00186608246C56584DB61AAA8C157A28B6161F23EDEB7365928C1EA8C962785C338E66FF1897B56154475E17F81C9845C4607E5294E21EA23EE0CC47E4BB9D3539D46D410A3149E098B539B196A2C90C06D5C103AAB08E1110D3F565AA8CC7E4F2167E6969BEDD5C3CC8A6F68F53E18931F8EF0723FE33A2789661C248B15A35ABD5919343AADC93BC6777539E952A3F49A3E34F51291709F6DBCBDCB65B4760B8795B7985DF8249EEA132A473CAB736E72D45A2A100FB560B70E600EB24AA596879F1B1A9DB705AAA489791A503E9463C19EDAA4ADAAADB77A6FB0BABE16973E63333F03B444C85C980D55727532765397B84870BB759469CEA72E28A75BBA8A5098B6A95750ADAB54361C802ADEA678FB6733D3766F7C50BAFC081A384A768E60AA2E6648ED81612778033C6038E7863887F19C1509518E61ABCFF77F1ED9459D10C53C0CC6AD60C95704138B9C579317714F393AE8287909ABAB7FA167B8B237CA52056A199D7558AD5F16821DD364B11B9307421D362BA42AA3002AD2025E347BCD7223EE4BA62C354C1CE9822C8C4B3B70A1708462453292C73ABB177949EA3106FE3075CBA15E2E143DD13A6600E55D801A440004621F6ACDC3CB8D53236FCCDC15B9F9CD321398AA230DA6722DDACBB98106367B3723FEE198E1AA7161C004C9BA1840F19721AA7D99B99A7057B1B33A28F2033904750ED1899551C1C8A9132D86B3781264CAC231C3A21440580A92F4C683CA90BC1CA78CE41B332EEA7D2F461425C9B91FB6867B4270AB785329218D74356A3B8423A044C3C6653B09E60821A1CA6032A0BC526228F18B9E38B3A7B69EA6C1C5C0C39DE56DA9763517FA3F65CEBED43C7B61282772DBC9C49E09D937D24CFD29FF7B285F7B478AE4E219BBBD89A54C8B127CB0C65803144CC1CA6B662A4CE499EBE66D933CEAE58EE244CBDCAAE3C1F45A0D6947802B76\"\n        },\n        {\n          \"tcId\": 75,\n          \"ek\": \"44AC5FB94668AD165BC9B4B09C50148831B0BFC0563D97B66913AB32846C9206BD2BEA024FB44B6DF239A8A82AD42C5BB69A77A3195AB8A7BD5C3131C9079D05E248D84167B6F569C812CDB1212F0584AEF9C0251FFA0E23A407BD592C7829C5D2A673DF01696AF02DD2A07A9342157EE608A2A91241DBB3F8725CD5D8BDA020C9107747F82A0FC7A97AC30272B694257B135095B27481CC769BB0834586CF70CAA72C2696B105A4B6440FB4D045B61A3D0BDA48490B22CE215C985B313F7C74155015A7D079DE9A2EF4DAB3ADD1A1CB684CA377B428B90D2BBB0980249586056434E2088EE250D065951EF228362B1D8028BC793CC8DCB8805D86C5DA71B4C1396158C79711DCC2F56359D197ABDB890387FA795ED9288FD08851CC620D0357A3450B4C50CF61EABA54A44FD61203F557946027B36224148EB4AEEE709401296BE0B617604B6A48244938279D66CAC75F03809ED49421F518A3270F42273E37793D6162B626771D69C8A27AF27AD0A3B89B6C756068565FB65D245A88D180741A1AA507E116DC504C11358C7EB444180792D210438F96CCB3DC17D8E8B8E79918A106787509B8B570B093C325B8CB81A12A686CACADA32389B8207108E7AEF5447217843932559F1893B250DB8060D068D6D2B06999C36F412D47519C795A7284E3CDD42CA6304C348AAA96E72B8C64C37C92C34F78C56A58B32630812A95F53FFE4B3987B5146E0321AC254573933D03E7356628727F448CF3D9A328C1CA72B59B73AC3F0A3671FF58A604AA5FA7F63ED7C72965A833B5916266796757186784882CE8670CE4F717F057A1C08563B9832B89A759F2380C74821379B343E7E28BEAE6334CDB896587C2585508A78947A54B08055C3C6CAC9357102CDAE58748A337D511BD0BE93C88964720C3AB5367478A4BB82FC148539C1D54F54CD7B1605C113D4E526BEDC4CD8526892198336F344B1A59A83F3ABC890C9AC386954A6B521FC76FFC03B614C32E7F68868400142BD041347614A80A7A59F2511A28B1E6F74053842C8F0A49FA17C378FC0992E14282E394034081AA4164C2E9934E5C8246E334AC606B9BA723237144AB2AAA8E86B2ED33B6750CA239F82C9105CEC46888BD59B2AE6331F47501C01805BF867A63F1B211306009C7B60FE4AB7EC5982B1955059A5D69BB728FAB31681CB0418C033B676158501FD3011A3B6B44503378C2FA1C6EC254861532DCC16F78D743DD065C8F8781F798633B0CAE5172173AE78A02DA472C0BB4DDCB2EC88B70E69C24FF277837646802774C98D87ED20382E4A82C0EB905CCAAA977295AA085A26717578E04B27139B98D260155C0B6189A3A6AA77D0CA76B6409032D3868967C1A5B5AB747B2B1660715F15161E00C383B957062509C495C5B25C26B2EA26B3A74597B913D295728B33A9EC914A2E646ADF0968E07592E48BA1BF0817995B321F05A7DD8FA892A385ED4C052B0CAB7C84945FABA63F2099BBD960843F4ADB512A48991A38439B71D291512333483E8A511FB925A7A770E033EFE6A933F105DF2B5555BE5179F3B8DD8F216B0EC87AD297B5AE941BBD80BFB0A417824A420489CC9281DD1D545BF15C8459627C4E8684D018473EACB1C49C27564614417A9EF6984D708B1FDC03F08453A769C470156884DCC30C18A1970022B01CA2DD0928F50404A9B19CA41FC8C325323EAE97C484015289A5383B5A46DA19641A99DB8763B8E716DF656204616B07BA5C06EF11ABD428D9EC989D848A8CAF65B4CAA41A4F741A497A08EBC2FF8525149FB029FE867939255AB836DB23C87BE120559A99B64F4BE533C6F914149C45A221BE8A3F9C724FC9BB5782C9489346ACCF5CC69A94734C2AB332A272BF135E507163BB3019D9544151A56C1892706F86D40439BF311AC4A477BE17287F829015CCA7A2F416AA178493E9CC86E607C2266924256377E3171B1610F6CBC4654DB870E2A1A2F11774F6147810A44523036EEC6211410091AF54EC71ACC0212BB96CB3CADE37C55A67A3DDBB12FCB6E6DC758EB8B807F75CFB4E4AED460A67C9546C9D668E8F1A560A80D20707AA6A0CE9433BD8D283BCC743FCC9C9D23719C1A05A874339C80F079A5364842AC0F21F804B9BB5D6FF4387D19442C4BAD15A31FB9F5C9A5A0CF4C59A16FA309A69AAAC01C9F1F8817AC8B7DC0153478452C0A379D65A78BB3307E3CA4\",\n          \"dk\": \"275C73EE58A9FFD22DD3EB2B1590CD79407D35E950370892C6B2AAB97392D8200DA10AB7188B9909B191D32073926860F5F5A4D2C21C52F4AF2EF5066BF182A7156D0400319CB462CEA5A2C65126943158823B085B6118449401D5C17A67D9A0CE2C5FEEF515787A423C7446EE795C14B61D33AC8BD06A3D8CA5A78AE5A4BCAB1E5BAA336A4B8DF423A9072A511A98993DF205971C70D84CADF5ABAF5917741B34AE09D280375819059618D748AC48CB425EC54F7A7435C40442FD594EB1C081AE4A80F9D023FDC968968A482BE95A9CD03041CB8B8F3918A5B7957E5CA6C95053A55CB7DA014B15425C6637000C131A664318834C09DE92BC04172209D945C97AAB3240A93559569D03793560AD9C552FBC467516556E4A07A1DCF2A2D2C5332E5074128207E4E4491A8866D760791A18CBC7939835F02DDEC71964428D80AC730FCBB0F0C206434746F75C2A4D95CFD4C398507729845C257D17549AA2983290558453445C3CA35B6BB139B007B003A890996ABF463D5E12CF3DF0728B8259A7854F6A06999C92868AC16C9BB78F571972AA122975C25A68C31B73710DD2EC07C2103BDBFC1A67E7370444CD2394913218816AEA4E1043BF53B83377D4775BAC602871C79A28AD930316FA091E5678582A552B6A5CBEBE25CB6F9C9002469E29F1833FB049D2CA7040499170F4B6BE2915E7C10081DB012DC8B9A8AA2F1FA6A6D3FABFD49B7873C234A21B0C9B572401F80908D7370C4A5BB1D04CF9218CCC5C8BEE0660CB2ABC39501E3F2AA441168B6BC9435A9599EC571D39A35B49FBA16DDC4A3C88828D4B89390B77006026E85A46335354B2B422DE7AAEF60087E2F975A7453FDD6C56B75181F317969F95B7A024A1AE12B66FDBBF27186BB0C0896DE69C5DA5AE4CB638E43A8FA8C283B92687DA372AEF4569826803613824A5DC288E94345209A4D2B42DCF2A251917735636669C3AC06E72A835042504CB6A83026CA4B1A10DB29F9C229575599295881A7358C5949469ACC8B43AC80B4D0CAAB50846CC999B42E0299159049DD370A9B52878AC4B64547E3EC070B4D370B19924D90C6EF153232CF12E64D0BD04E124C55CB2B8D1376A46CE2B91A225985F7BD72D3B316767B3BD3206C5C8B311122300225A02F0E53664CB02B1350859D579A09710EAB927C5207256AC1128C6B0E09C7BBA408A2DC34C1160A8B13766BAC2A747A6BF20B1A6A22992451BA49CB5B97F7597F98B95E44B9B598AA993073ADBD6A9B5C81E10E21AAEA573A95A6716A3298412BE1D661746BB81C2F17744D587E3522143091FA215AC2D9B7C3C00397A1C3807F24AB4E968B0EA1A0E5A6601B7274469CD1A52AE5FAB7BAAB3C4A6D1A5D27A873E21A14DF52AE2C344BFE21B0F8A71A0511828E86F1FB08141C30C842538C61C7ADEB9251E33C73CEA9E7C02CF46E3C3E548355643000D3B715F339DE10745141B8D2A201A4D8C514185A6DC06375E35750E246D4CF1C8937955631CC7350CBC4C34C22B35C4336B95A12478419B8309E0137317B6CCDBAF70630BFD711783087967A93E1F95321EC319CA31004CF677D44359465564A889193FE04A7F8944EF30B5C0B43903279F07E2C04D116BE8322E9FAA4FA2562025910A264A814EA488D72CC03EB5853B40A406E1A7631933EC725D4E71516C751C3C0A3560BC0AD97078ACE63E9FC41A0F2B92B6E46A0836A39AE9B67C6B6F23A2A098987F3B6500ED179C6F14A0E0564166C5CF5C322A1E5654F8E083CD5B13ECD761B727020CDB5EAC5B9DF509AC0D9BCFD2A2976E586F90FC3949AAC845440FB929B9D5743723926FB2E6290EF3ABE663565AF1CC74587CDCB3034F7692D9488142231BE8651C5280A52EF048FD4C1C776210A39463CA0127A2F9065D6AB320E253A1A0C596E8797887844D8471A32795E6DA8D5774648AF25194C3AC8447C534B61C77F364D3136FB358861B05465AB09077912F536500F35002DF56015441A341E30D6458289F47B578B1BFE4C574228B0D16890108BA7D4DBC7EA8C9A5118AA6F6E48ED8E19B19F6629114551442CAB1E88541F09E85B79E123C912C967CBD4C273C47485EF552ACF2744AF83D72007137342D33A00F41EC9E0FE8AF0B641AC0F3BE42984D4D9C2E4D9393AB9992E78913A7DC3444AC5FB94668AD165BC9B4B09C50148831B0BFC0563D97B66913AB32846C9206BD2BEA024FB44B6DF239A8A82AD42C5BB69A77A3195AB8A7BD5C3131C9079D05E248D84167B6F569C812CDB1212F0584AEF9C0251FFA0E23A407BD592C7829C5D2A673DF01696AF02DD2A07A9342157EE608A2A91241DBB3F8725CD5D8BDA020C9107747F82A0FC7A97AC30272B694257B135095B27481CC769BB0834586CF70CAA72C2696B105A4B6440FB4D045B61A3D0BDA48490B22CE215C985B313F7C74155015A7D079DE9A2EF4DAB3ADD1A1CB684CA377B428B90D2BBB0980249586056434E2088EE250D065951EF228362B1D8028BC793CC8DCB8805D86C5DA71B4C1396158C79711DCC2F56359D197ABDB890387FA795ED9288FD08851CC620D0357A3450B4C50CF61EABA54A44FD61203F557946027B36224148EB4AEEE709401296BE0B617604B6A48244938279D66CAC75F03809ED49421F518A3270F42273E37793D6162B626771D69C8A27AF27AD0A3B89B6C756068565FB65D245A88D180741A1AA507E116DC504C11358C7EB444180792D210438F96CCB3DC17D8E8B8E79918A106787509B8B570B093C325B8CB81A12A686CACADA32389B8207108E7AEF5447217843932559F1893B250DB8060D068D6D2B06999C36F412D47519C795A7284E3CDD42CA6304C348AAA96E72B8C64C37C92C34F78C56A58B32630812A95F53FFE4B3987B5146E0321AC254573933D03E7356628727F448CF3D9A328C1CA72B59B73AC3F0A3671FF58A604AA5FA7F63ED7C72965A833B5916266796757186784882CE8670CE4F717F057A1C08563B9832B89A759F2380C74821379B343E7E28BEAE6334CDB896587C2585508A78947A54B08055C3C6CAC9357102CDAE58748A337D511BD0BE93C88964720C3AB5367478A4BB82FC148539C1D54F54CD7B1605C113D4E526BEDC4CD8526892198336F344B1A59A83F3ABC890C9AC386954A6B521FC76FFC03B614C32E7F68868400142BD041347614A80A7A59F2511A28B1E6F74053842C8F0A49FA17C378FC0992E14282E394034081AA4164C2E9934E5C8246E334AC606B9BA723237144AB2AAA8E86B2ED33B6750CA239F82C9105CEC46888BD59B2AE6331F47501C01805BF867A63F1B211306009C7B60FE4AB7EC5982B1955059A5D69BB728FAB31681CB0418C033B676158501FD3011A3B6B44503378C2FA1C6EC254861532DCC16F78D743DD065C8F8781F798633B0CAE5172173AE78A02DA472C0BB4DDCB2EC88B70E69C24FF277837646802774C98D87ED20382E4A82C0EB905CCAAA977295AA085A26717578E04B27139B98D260155C0B6189A3A6AA77D0CA76B6409032D3868967C1A5B5AB747B2B1660715F15161E00C383B957062509C495C5B25C26B2EA26B3A74597B913D295728B33A9EC914A2E646ADF0968E07592E48BA1BF0817995B321F05A7DD8FA892A385ED4C052B0CAB7C84945FABA63F2099BBD960843F4ADB512A48991A38439B71D291512333483E8A511FB925A7A770E033EFE6A933F105DF2B5555BE5179F3B8DD8F216B0EC87AD297B5AE941BBD80BFB0A417824A420489CC9281DD1D545BF15C8459627C4E8684D018473EACB1C49C27564614417A9EF6984D708B1FDC03F08453A769C470156884DCC30C18A1970022B01CA2DD0928F50404A9B19CA41FC8C325323EAE97C484015289A5383B5A46DA19641A99DB8763B8E716DF656204616B07BA5C06EF11ABD428D9EC989D848A8CAF65B4CAA41A4F741A497A08EBC2FF8525149FB029FE867939255AB836DB23C87BE120559A99B64F4BE533C6F914149C45A221BE8A3F9C724FC9BB5782C9489346ACCF5CC69A94734C2AB332A272BF135E507163BB3019D9544151A56C1892706F86D40439BF311AC4A477BE17287F829015CCA7A2F416AA178493E9CC86E607C2266924256377E3171B1610F6CBC4654DB870E2A1A2F11774F6147810A44523036EEC6211410091AF54EC71ACC0212BB96CB3CADE37C55A67A3DDBB12FCB6E6DC758EB8B807F75CFB4E4AED460A67C9546C9D668E8F1A560A80D20707AA6A0CE9433BD8D283BCC743FCC9C9D23719C1A05A874339C80F079A5364842AC0F21F804B9BB5D6FF4387D19442C4BAD15A31FB9F5C9A5A0CF4C59A16FA309A69AAAC01C9F1F8817AC8B7DC0153478452C0A379D65A78BB3307E3CA4D4F2CEEBE65173867CDDEC350D15A72CF1FEE868A9B819DD1DEB4E7478C00DECD16CC70224474A4D71E1F950C2D5CA72D8F08AF80E0C7F6E292C265A50CC30E8\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/peergos/server/tests/fips203/key/gen/mlkem/internalProjection.json",
    "content": "{\n  \"vsId\": 42,\n  \"algorithm\": \"ML-KEM\",\n  \"mode\": \"keyGen\",\n  \"revision\": \"FIPS203\",\n  \"isSample\": false,\n  \"testGroups\": [\n    {\n      \"tgId\": 1,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-512\",\n      \"tests\": [\n        {\n          \"tcId\": 1,\n          \"deferred\": false,\n          \"z\": \"84CC9121AE56FBF39E67ADBD83AD2D3E3BB80843645206BDD9F2F629E3CC49B7\",\n          \"d\": \"2CB843A02EF02EE109305F39119FABF49AB90A57FFECB3A0E75E179450F52761\",\n          \"ek\": \"A32439F85A3C21D21A71B9B92A9B64EA0AB84312C77023694FD64EAAB907A43539DDB27BA0A853CC9069EAC8508C653E600B2AC018381B4BB4A879ACDAD342F91179CA8249525CB1968BBE52F755B7F5B43D6663D7A3BF0F3357D8A21D15B52DB3818ECE5B402A60C993E7CF436487B8D2AE91E6C5B88275E75824B0007EF3123C0AB51B5CC61B9B22380DE66C5B20B060CBB986F8123D94060049CDF8036873A7BE109444A0A1CD87A48CAE54192484AF844429C1C58C29AC624CD504F1C44F1E1347822B6F221323859A7F6F754BFE710BDA60276240A4FF2A5350703786F5671F449F20C2A95AE7C2903A42CB3B303FF4C427C08B11B4CD31C418C6D18D0861873BFA0332F11271552ED7C035F0E4BC428C43720B39A65166BA9C2D3D770E130360CC2384E83095B1A159495533F116C7B558B650DB04D5A26EAAA08C3EE57DE45A7F88C6A3CEB24DC5397B88C3CEF003319BB0233FD692FDA1524475B351F3C782182DECF590B7723BE400BE14809C44329963FC46959211D6A623339537848C251669941D90B130258ADF55A720A724E8B6A6CAE3C2264B1624CCBE7B456B30C8C7393294CA5180BC837DD2E45DBD59B6E17B24FE93052EB7C43B27AC3DC249CA0CBCA4FB5897C0B744088A8A0779D32233826A01DD6489952A4825E5358A700BE0E179AC197710D83ECC853E52695E9BF87BB1F6CBD05B02D4E679E3B88DD483B0749B11BD37B383DCCA71F9091834A1695502C4B95FC9118C1CFC34C84C2265BBBC563C282666B60AE5C7F3851D25ECBB5021CC38CB73EB6A3411B1C29046CA66540667D136954460C6FCBC4BC7C049BB047FA67A63B3CC1111C1D8AC27E8058BCCA4A15455858A58358F7A61020BC9C4C17F8B95C268CCB404B9AAB4A272A21A70DAF6B6F15121EE01C156A354AA17087E07702EAB38B3241FDB553F657339D5E29DC5D91B7A5A828EE959FEBB90B07229F6E49D23C3A190297042FB43986955B69C28E1016F77A58B431514D21B888899C3608276081B75F568097CDC1748F32307885815F3AEC9651819AA6873D1A4EB83B1953843B93422519483FEF0059D36BB2DB1F3D468FB068C86E8973733C398EAF00E1702C6734AD8EB3B\",\n          \"dk\": \"7FE4206F26BEDB64C1ED0009615245DC98483F663ACC617E65898D596A8836C49FBD3B4A849759AA1546BDA835CAF175642C28280892A7878CC318BCC75B834CB29FDF5360D7F982A52C88AE914DBF02B58BEB8BA887AE8FAB5EB78731C6757805471EBCEC2E38DB1F4B8310D288920D8A492795A390A74BCD55CD8557B4DAABA82C28CB3F152C5231196193A66A8CCF34B80E1F6942C32BCFF96A6E3CF3939B7B942498CC5E4CB8E8468E702759852AA229C0257F02982097338607C0F0F45446FAB4267993B8A5908CAB9C46780134804AE18815B1020527A222EC4B39A3194E661737791714122662D8B9769F6C67DE625C0D483C3D420FF1BB889A727E756281513A70047648D29C0C30F9BE52EC0DEB977CF0F34FC2078483456964743410638C57B5539577BF85669078C356B3462E9FA5807D49591AFA41C1969F65E3405CB64DDF163F26734CE348B9CF4567A33A5969EB326CFB5ADC695DCA0C8B2A7B1F4F404CC7A0981E2CC24C1C23D16AA9B4392415E26C22F4A934D794C1FB4E5A67051123CCD153764DEC99D553529053C3DA550BCEA3AC54136A26A676D2BA8421067068C6381C2A62A727C933702EE5804A31CA865A45588FB74DE7E2223D88C0608A16BFEC4FAD6752DB56B48B8872BF26BA2FFA0CEDE5343BE8143689265E065F41A6925B86C892E62EB0772734F5A357C75CA1AC6DF78AB1B8885AD0819615376D33EBB98F8733A6755803D977BF51C12740424B2B49C28382A6917CBFA034C3F126A38C216C03C35770AD481B9084B5588DA65FF118A74F932C7E537ABE5863FB29A10C09701B441F8399C1F8A637825ACEA3E93180574FDEB88076661AB46951716A500184A040557266598CAF76105E1C1870B43969C3BCC1A04927638017498BB62CAFD3A6B082B7BF7A23450E191799619B925112D072025CA888548C791AA42251504D5D1C1CDDB213303B049E7346E8D83AD587836F35284E109727E66BBCC9521FE0B191630047D158F75640FFEB5456072740021AFD15A45469C583829DAAC8A7DEB05B24F0567E4317B3E3B33389B5C5F8B04B099FB4D103A32439F85A3C21D21A71B9B92A9B64EA0AB84312C77023694FD64EAAB907A43539DDB27BA0A853CC9069EAC8508C653E600B2AC018381B4BB4A879ACDAD342F91179CA8249525CB1968BBE52F755B7F5B43D6663D7A3BF0F3357D8A21D15B52DB3818ECE5B402A60C993E7CF436487B8D2AE91E6C5B88275E75824B0007EF3123C0AB51B5CC61B9B22380DE66C5B20B060CBB986F8123D94060049CDF8036873A7BE109444A0A1CD87A48CAE54192484AF844429C1C58C29AC624CD504F1C44F1E1347822B6F221323859A7F6F754BFE710BDA60276240A4FF2A5350703786F5671F449F20C2A95AE7C2903A42CB3B303FF4C427C08B11B4CD31C418C6D18D0861873BFA0332F11271552ED7C035F0E4BC428C43720B39A65166BA9C2D3D770E130360CC2384E83095B1A159495533F116C7B558B650DB04D5A26EAAA08C3EE57DE45A7F88C6A3CEB24DC5397B88C3CEF003319BB0233FD692FDA1524475B351F3C782182DECF590B7723BE400BE14809C44329963FC46959211D6A623339537848C251669941D90B130258ADF55A720A724E8B6A6CAE3C2264B1624CCBE7B456B30C8C7393294CA5180BC837DD2E45DBD59B6E17B24FE93052EB7C43B27AC3DC249CA0CBCA4FB5897C0B744088A8A0779D32233826A01DD6489952A4825E5358A700BE0E179AC197710D83ECC853E52695E9BF87BB1F6CBD05B02D4E679E3B88DD483B0749B11BD37B383DCCA71F9091834A1695502C4B95FC9118C1CFC34C84C2265BBBC563C282666B60AE5C7F3851D25ECBB5021CC38CB73EB6A3411B1C29046CA66540667D136954460C6FCBC4BC7C049BB047FA67A63B3CC1111C1D8AC27E8058BCCA4A15455858A58358F7A61020BC9C4C17F8B95C268CCB404B9AAB4A272A21A70DAF6B6F15121EE01C156A354AA17087E07702EAB38B3241FDB553F657339D5E29DC5D91B7A5A828EE959FEBB90B07229F6E49D23C3A190297042FB43986955B69C28E1016F77A58B431514D21B888899C3608276081B75F568097CDC1748F32307885815F3AEC9651819AA6873D1A4EB83B1953843B93422519483FEF0059D36BB2DB1F3D468FB068C86E8973733C398EAF00E1702C6734AD8EB3B620130D6C2B8C904A3BB9307BE5103F8D814505FB6A60AF7937EA6CAA117315E84CC9121AE56FBF39E67ADBD83AD2D3E3BB80843645206BDD9F2F629E3CC49B7\"\n        },\n        {\n          \"tcId\": 2,\n          \"deferred\": false,\n          \"z\": \"5D473027666FECF7024ABAF175B9BC42E84768C00AE2C5CF27A668121B02CD3A\",\n          \"d\": \"9EFF3FF8252400827F3B4389E4EC07E67948257C744278048C889D0789C5BFFA\",\n          \"ek\": \"3A51932399C6144CA7930C3B9C165BED5BA7B93635D2699EC5C85615254B9B8705D5922A0FCB48C9B561DE4114738BBD2F043E1E0B0DD601A095CA540944A20DE89CD4B637B4AABF983C61381A1CC0F03EC8E82F1D4C62269B1114B2673EB5BEC287703C42AA6B574AE2701EB35C5A017228A7E0B91DBB34537B9B19600DCCB26869813E5B1B1A92D22C5D89CF49BCCB75F3BC8DF9648B634B992141F05BCE85F19E64597D688234AFD830B7B5A41E317ACEF6707A0A60213622564440CD89A520287FDF8556453233282435CC813FA4356478448FE5D63AAF539FD526279F02A47CA692F385A7C2D8448EF6B830424FC7A88329C7B02554B72EB2A0994A649F33254C33001FF72C1034A87D79A34F2894AD0296CA529C029A5238154706C43B76B8568775BC1239C08DA177809BBFCF6A833CB62CF4C0A7103C2C7140963861A3B653A82F008D8D78742F1985B340552723AE6EF4095D22383393065F5B8AC63CCB8974614C1C7222458D5D638FEA48C577EB9D64760898B54411A4926CA95E23AA8DDEC12CB33B8F38131C3229A97757BCADA966CF1CCBFF47758DD14875957BE9607C6F66CEAA00A32DC99E575C6F624BC29AF1CE164A1CC2D5909FF93AC25821FBEA8BCFD27FC9211A0AFA2CEDAA0B1AFC89F7F3362ABC67A52AB88763803273C8B6CB2119831DD0D7774839386838946A9067155808F07A6A846B18024506675A618C8093B6D8C8E7AB47CDC847B60C660E7524737649EB11C481EB6273B28E68075E482C4A727C1FC1C0AF904539D335BFC837BFC7F62225FA8F3692566AF3247832CFE9B6C3A92743B2938CC543374A7637ED3C36724C651DD16499A023D40B07494C3018E39BFF4303BA9A3576D8C9BEAC6F45EAAE48A0ABF4770E9C697EC4E834AE04258F7C5912BA8B76FC613914AE31035276A5015CF75A9F3126AA37BED964CE13340942081DFC620ABA65963E33320912614D18CF69656F30BA15E079BE8A53A9E68C7C5D967469294D8833A81605BDD459A65C194B15F241B0D732A0BC87231B682E84B7AEF151C933C0C83410292017B85AA51537C1CAA114690AE2FFA37B4FBBC3591BD28EC78CB5254E52EA0C474EEE871848C83A5C90E6\",\n          \"dk\": \"FAB74ACD14154B721C4F5446B0020EAEFA1B8CE0897607CBF4DB9CF5472751D8AD3418B76614361B30CE0195596E832DDA286D69270EAFFBBC22E11B1EF4912D02B4F02B4C4E5073A7038BBD793ACFA39C3E92615063B460920BD44B815D02ABB872861EE720D73204FBC98978D228362CBA7FB9B999506137502813F92D7958895EB8C1F3E87CCDD25743F2279E8B2C60FA814860BFBEB52F0C3441FF9265882748A8B88E656C0366CB876BA5C8A32385977C1AF0A710A033277D3C53C2942AD7F250E9B3100B675E5539AC11842C09F717B62C60E85195FC0776F80C80612C273F836C5E9300FD0904D097A7609108EFC8CE4DD1077B951CE95257E2921EAB774A97C33724E295B6C9B72C357C7953BF01D6AE4145A708A1351CB800D60A7EE0FA8348F91C6C054F21403A88EBA965196813AA184C187EDF466AA6CC33F7E11E290508DB822019F50A09965A898BA270D6CE8FE6C6A5F059DC311D9BAC0D9EE289EA323838D317A2CBBF5B9C2296EB18708015B05427278B430E7A99BB5793E9E197666A02FC44514AB7041DBB1608F2B2A4A21ED885449D8566D9575977C3A7BCF45757074C3E3631EF1548453B9068685C35B9C4CBAA3A9F9B5978B03F7274076B99A3860172D038CC1CE2345E5A6FE372249DC3289895A9E567602DFC7A0A047A5625A055109747B09B35BB9CD9412A87F92A7F282258F1594B5A9AE9D38C9768A47D4A7ED108A502C222F2B51A30A46AC90A418F6C3CF953BFB50B9229EABE3FDCC51BE021D94A6F5DC4407954C073F76C509725CD0131130037E1F0A7216BC826E5037EC60F2D736DA4B72185895AEBD41FE6E6BFCC154C00F648A4CC62C686AE787445847C5128C924F7E15046124379258B8D651A14573584B50E95AA0674228DE8D6211D5083D849657E53868C08743B6479711A9B7832B99461C629635F91512C71E5662CA37BD31298FD116A9CC87F7A0036A4A55907CBB2AA640161F8A4FC4B7AA1FAC21D67360570B17A212E6E2B672E29A3C50B9063BCBEADACBA95DBADF509672172415F006E82A313916B6E4CA705EE12AB5447214E60C33A51932399C6144CA7930C3B9C165BED5BA7B93635D2699EC5C85615254B9B8705D5922A0FCB48C9B561DE4114738BBD2F043E1E0B0DD601A095CA540944A20DE89CD4B637B4AABF983C61381A1CC0F03EC8E82F1D4C62269B1114B2673EB5BEC287703C42AA6B574AE2701EB35C5A017228A7E0B91DBB34537B9B19600DCCB26869813E5B1B1A92D22C5D89CF49BCCB75F3BC8DF9648B634B992141F05BCE85F19E64597D688234AFD830B7B5A41E317ACEF6707A0A60213622564440CD89A520287FDF8556453233282435CC813FA4356478448FE5D63AAF539FD526279F02A47CA692F385A7C2D8448EF6B830424FC7A88329C7B02554B72EB2A0994A649F33254C33001FF72C1034A87D79A34F2894AD0296CA529C029A5238154706C43B76B8568775BC1239C08DA177809BBFCF6A833CB62CF4C0A7103C2C7140963861A3B653A82F008D8D78742F1985B340552723AE6EF4095D22383393065F5B8AC63CCB8974614C1C7222458D5D638FEA48C577EB9D64760898B54411A4926CA95E23AA8DDEC12CB33B8F38131C3229A97757BCADA966CF1CCBFF47758DD14875957BE9607C6F66CEAA00A32DC99E575C6F624BC29AF1CE164A1CC2D5909FF93AC25821FBEA8BCFD27FC9211A0AFA2CEDAA0B1AFC89F7F3362ABC67A52AB88763803273C8B6CB2119831DD0D7774839386838946A9067155808F07A6A846B18024506675A618C8093B6D8C8E7AB47CDC847B60C660E7524737649EB11C481EB6273B28E68075E482C4A727C1FC1C0AF904539D335BFC837BFC7F62225FA8F3692566AF3247832CFE9B6C3A92743B2938CC543374A7637ED3C36724C651DD16499A023D40B07494C3018E39BFF4303BA9A3576D8C9BEAC6F45EAAE48A0ABF4770E9C697EC4E834AE04258F7C5912BA8B76FC613914AE31035276A5015CF75A9F3126AA37BED964CE13340942081DFC620ABA65963E33320912614D18CF69656F30BA15E079BE8A53A9E68C7C5D967469294D8833A81605BDD459A65C194B15F241B0D732A0BC87231B682E84B7AEF151C933C0C83410292017B85AA51537C1CAA114690AE2FFA37B4FBBC3591BD28EC78CB5254E52EA0C474EEE871848C83A5C90E6A8ADE3E0536F87E2E908AA77EC32AD0A8555B3045331059C5AEBBADA69D0F0735D473027666FECF7024ABAF175B9BC42E84768C00AE2C5CF27A668121B02CD3A\"\n        },\n        {\n          \"tcId\": 3,\n          \"deferred\": false,\n          \"z\": \"7A7FC526215D5AE3262985D17B00726462D1479CB038DE8C8A8FEA896A037B2C\",\n          \"d\": \"C6636E8C2F87DD52A7F165A2A3BAD562ADB28CF738AA56B996B6062E95F66148\",\n          \"ek\": \"FA66C756BA9DAF40BBBEE9473B35BD71AC1DFA52A33D4A47B9BBC7EA9524DD344E086A6A0CC72941B645B3D49F6D12382CDBB843883F723078D096169F1373C6C8C5132260116963A0316E7D4268D6E36946E1AFC0457CABB3B848190E52A27BBFA42F7EBC0839E52E43611C6E49ABFD97299C4804BD08376020CC21B92BEB1B6D43EA52155B2E8D8B36B584807ACC96F237AD8365C822C66DDFD8A313849E8481A61116939CA8281EFC3D446B6CAA7583EB086FA5214DA67CA46DF75D25537A8C970CA718113DB96392B92435A88053D9141C2BC646D801E9697ED9C35A008AAB59245F56D74F64D1A81DABAB29C4571A6AC000501F1426A7AAAB5F44D4BD4BFB6E8D1CA75AE7949161B10DF906BF72BC460A08C346B5B3A5A6D3325E628546D6F4AF26F92FAEE81C97CBA29E06691AB64F185595B1F35A97A9BCB7E0011AAB485CB4ACA1E04DC534192A0B0E3C248BCF197E7D9B4A317C75C59812ED1694084CBBB8A40EDE334832B714AEA19DC2EA645CD0C3113C31C68379E9ECB5875C58C77B3084709062D77C72416CAC063D04FAA8A7825254CB2B539A78E3197D56153D4E775FF04930DF90546FC145493152A1D37FEA05A86C1A88FD86A8EF32C00B7A5EB04989AA9317C49AC7E9AC82032715F1B4CA448ACDF51198FD1BBA4FC00D1E22693572B0E1F71CF768ACE73C9733F2250B8A7BA3B74D8C12383B465C1D33881411C143FCB621C5CCD7469259A96330343352B699B7C5B850534145E10271897656F4328864A0FAC59C7886B854E8B6D7981307AB4F8B222AAA200D3435562B95554FCA31A0533B5581087AF492E444647F27789A1BC501016079B8C4F5208B0D2574206C7E6A63147950AD0190751E9C3EC0621DF05539A724732069C9D0CB2B7D77940A9A9C0AE40A4410B73B6747A1625D0548BB4AB337CED7442F55063D58CBBC6C6F70B839C8303CFB17ABEE195283A6010DA5BF86A20F7522B9CD0B9F59204C2DF50424AB659280BC27E72916542A0918AA5D618AA739729039169C7838838022E1E61805C83BDBD84BCA9A7C936060D9722DE33B2F3882075028BE255365C5523B6A9BDD23CD16255D8240B07F18D481D988D769FD9588C0BA064E\",\n          \"dk\": \"8D0A672A78C8D3C6A7F4608806D062DE426BF05360F33A000DA72551D56250BB8A8DA19A553A79FCD0ADE3989D1C999C5A57B58761A2D34B7F6B202910384BB38473931356E801B67BF5CDFEA96E7F541982534821D6818FA20829AB9F3F3AA05C4096A1CBB0E907634E7707E2A73DF8C213E118742CC12E0353C9D0B3BAEB296A8EE69FC5D312AA3554C2176411D009D67BAA4F07B45F5B55DFF00713CC3408C46F37389BCF149945B437EB234C81F724FAD57156D370FBF5B726193371A58C48176F8AE9A339DB91A7D7047B55C060DA7EDD910D20ABB6BB9905B41500F3246FABF1AC56BA0D2925600E333E241B72C44ABA71DBBDE6E55197B225D5C42BDD8C62E3313161DBA36094A98D555F99134CFC2382E5877D2530397FC48E7E5AA90676A7B41468C042433C09A09F7C9B5F633E636B316244191F00CF053ABA7365905B7BAA716A9645E54F75706610058761F7113D689320514D38A60A44F8191FAA5C3B13910E4218A703C98E48754B4431F630676928B5641807182C74A3A45B8E8B9568BA210EF8BACA4300E98592533A965FAB9EDD8B3004442E04340C3D3B8528283344E262B9F755E1223EE8108073A36DDF17C9DDF00276B5AC81339E1D8AC541BBB65E5A8D3857B28E60C516A75CA109C7A602B66EA6C9B0B4BBCB27C4F93C8EC44472DA9A7D7AF195DEC6547992AB8ACBBCEDCC87E88140D765518DDB3779F57C8C02C75FAB9515C3807F02740361280B539048DCB6ABB875FA6659E3083834DC450C67BE32D9709F8651840C5A4FB96B048677F9E735FCC1B0BC09CE2F1C39FEA8B0D29193EC886D02F23A413AB7E98001CDE7C8D4F69AB60C6C9270159931702BF5AA226936E7D0B53B29722AE3B29C497B5B74747061838FD8978BC2249FE83A453C3AFF02C67D6B563CE7B3FCDA452C52CA3E1282C4EB90683999FAF29D509BC4E4C41023E332E3C82433AC4371B84824B07C05A1B76D58BCE86AAAEC5AB0DD09A774291A65705BD0764A00C5CFA859938F942EE72C5520E26DD1C27B4C87C77712266DF6819A5C5A39BA8CB9667F862AC66E40069022BACA0B91FA66C756BA9DAF40BBBEE9473B35BD71AC1DFA52A33D4A47B9BBC7EA9524DD344E086A6A0CC72941B645B3D49F6D12382CDBB843883F723078D096169F1373C6C8C5132260116963A0316E7D4268D6E36946E1AFC0457CABB3B848190E52A27BBFA42F7EBC0839E52E43611C6E49ABFD97299C4804BD08376020CC21B92BEB1B6D43EA52155B2E8D8B36B584807ACC96F237AD8365C822C66DDFD8A313849E8481A61116939CA8281EFC3D446B6CAA7583EB086FA5214DA67CA46DF75D25537A8C970CA718113DB96392B92435A88053D9141C2BC646D801E9697ED9C35A008AAB59245F56D74F64D1A81DABAB29C4571A6AC000501F1426A7AAAB5F44D4BD4BFB6E8D1CA75AE7949161B10DF906BF72BC460A08C346B5B3A5A6D3325E628546D6F4AF26F92FAEE81C97CBA29E06691AB64F185595B1F35A97A9BCB7E0011AAB485CB4ACA1E04DC534192A0B0E3C248BCF197E7D9B4A317C75C59812ED1694084CBBB8A40EDE334832B714AEA19DC2EA645CD0C3113C31C68379E9ECB5875C58C77B3084709062D77C72416CAC063D04FAA8A7825254CB2B539A78E3197D56153D4E775FF04930DF90546FC145493152A1D37FEA05A86C1A88FD86A8EF32C00B7A5EB04989AA9317C49AC7E9AC82032715F1B4CA448ACDF51198FD1BBA4FC00D1E22693572B0E1F71CF768ACE73C9733F2250B8A7BA3B74D8C12383B465C1D33881411C143FCB621C5CCD7469259A96330343352B699B7C5B850534145E10271897656F4328864A0FAC59C7886B854E8B6D7981307AB4F8B222AAA200D3435562B95554FCA31A0533B5581087AF492E444647F27789A1BC501016079B8C4F5208B0D2574206C7E6A63147950AD0190751E9C3EC0621DF05539A724732069C9D0CB2B7D77940A9A9C0AE40A4410B73B6747A1625D0548BB4AB337CED7442F55063D58CBBC6C6F70B839C8303CFB17ABEE195283A6010DA5BF86A20F7522B9CD0B9F59204C2DF50424AB659280BC27E72916542A0918AA5D618AA739729039169C7838838022E1E61805C83BDBD84BCA9A7C936060D9722DE33B2F3882075028BE255365C5523B6A9BDD23CD16255D8240B07F18D481D988D769FD9588C0BA064E851F4FBEBBC2AB265691CDBF130A1A566398C6316707F7A9AE78ECC419698DD97A7FC526215D5AE3262985D17B00726462D1479CB038DE8C8A8FEA896A037B2C\"\n        },\n        {\n          \"tcId\": 4,\n          \"deferred\": false,\n          \"z\": \"6E584B168BB5399D52B458A8BD122DE14EEF214515B70F38F972F41783005755\",\n          \"d\": \"EDE2E63FDEE6ADA2FC6EA906AA8D92DE87FA6199AC15446B0B6F075BF9F76148\",\n          \"ek\": \"72828AB4196357F09AA50129BCA411CE9255422C4AC1C492A2A60544C26B8F37A7EC9B053F27656AB1AE2BE0B1C95346083C15C8F89AD92C477D5C03BB95AC4F8B6A1C9BAA8D199F1D83823072082737BEBBCC1BC1E7BFF9742C4AE86313001F2519C8E93080C2AA1D63376BD61C31BC189BCA8C8385E5A7B3A69380EB72F14B164166227B453C6F43C59A34A8B6724193D77858113AB5C90B2A605F5C3C8E3FF8BDDF82C82F0C3045A7CEBA99891A60016C660BE767809977CAE5B8ADA668BEAC5374CE54692DD02F51E9A90BC68C51955797975F63707E1DF3AC14BC877529A02EF7A317976EA722CA3B80A316B2688B074108F05C3CAAAEB0241D1C5B05D458A150B692E288CE47CCABAE255B9EA13F80D16066155B2E46524BA5A3BDF74C9F77A25402CFB4439C497C03314465C4A91152671E4F79239937BEC7A72DAB0B97B30C1A00F100216B1E2AB67C6520608F54BC92985504033CE63363E015B9E0C9BF75CC2F90AA9E09DBC221258B389585927905DE077927D3AF6C59309B12B3DDD0029BE56EC0CC1CF8AA3A5041BD954B0B7DB68AF2BC02284C9FCBF954875816DAA11E6668455C8978A247B4011B1010289EC564999D069AA2392E9CD9A0E45636C014315C15AFB5148F513CC87EEB8B56334ECA1C061058C06B3C285DA8203DCCBEA8B750E9D43E8CB837F1AA0635954A924C866C33441E7C0C23C97CF62CAE10D32C2A641C1538C2A7B1C4DBB7BF069B43D4210941A4AA0E41748D788AE831018BE1916D056AE4598834B6BD56AB86C3B7528456751B33A0406A2FCF8C0FE18C25A1C208A5A4CDBE593DA6581E27C6A5B4DB793154233CA5B3C2469A80F8C8D3C22C25A8537270CAD4480B20D47C6AC56EDAD91BFFEACBF605AD61C44BF86B7624B95CB1A5248E9210D9F18837FCB01EC68BF434725FB75FF67C016DD78E303C2C6E775D17E61642776368F17F467BC9E0E153659C5219678490BC110B8184B556A2AD275FE6913B6BC744DF313EECDB273CD637A84234601599F05C6B358312FDD959E20B81A434A6FE200AF26A50C670837E8266303A7C23219F18A670CE910DF8756E403C31C561245110CD2DE051B61979D5BCFCC3E03F68C1D3412C\",\n          \"dk\": \"871B000F61C393DA0F0C56B0CA4135223162F177373BB58BA97856F3A0AAAF9540AE6BB239C5221D6B3CC3BB623B5ABBA1D7CE45A75571E01B6AD429B4A1A73E0130DF3947BCE4123AE4A9AEB52987D4BEC94446CD70AECEC5CC98EA13DF3A323D8BB48B08B07161499CA76962737F36A17A4F83045B78AA33879BEED71287B70C35B92BB01AB9E7B0813DCA539F63CAC3B87B419A852104B73F05979BD8296D0967E58A6333F88E8CCC9A8A4A4E0CE25A57851FB0F568665163553BC200C190C9178526FB53328119E99C42EFC0C77729BD6873900394CAD9197E37C00FE9C703CB25AA54E73DA5A41132C1B9ACA95ABD4508872B1F6B8288477998A0842E58B1BC9CE061CEC2703AEBBD3C230AFCD2AE7E576126BC72A7925BFED55E62C06E16187E24086452344B8A5A5A462522AB693A88F57C69835DF0A603145C6DF2244A1F221977AB54C3E2023363A72F277217E15E616B54D6BB3959E3322EC9A276B7434A796BB5296E83407999F16408B11DDC79CBCEB46D01033C5FD091300855D7E64536480DD945CD33CC9D4221ABE5014D068BAE1851717633AE03E6627BB17976ECCE84807D5C2602030844AFB30994D26CDAA0C71AB591AE6069403C13E4B6BA89439F2C5393955AA1C4A6270339505B59009DA51527C33334D6903DF3A37613546F076AEE723046C13FFC729D64583A4A01CAB68C5C95C25706F0A918635E61A390FDA5745A9756EE32975CA4483982505981A3589232EAC3C0C31748A49429A711CBC16355DDD10660557C9270438D9635850918AA366A34B251F311C65C579FBBD2341A53CB79BC9A993475CB6AB9455B82A9DB5EBA53592EEC9A2C481792798A1483037F115336FAA8A4B07AC253B0A347227A7B12E3197531F875C91B0A636107CB38BB0BD613B057869403A77DDA0A80A26266B61D14A75433B99BA055A7FB625C4A17BE9A92B2E168899B6539972B4BC674040645496972617FF70DF5EB63B17B03F54BCAEB3118C4F31EAE075D2C1C9F1D5036FEA818A539ADEA50795AC933EC886FCC248E49F12DBBD23D8F812BE0678B19888116254A27B5A372828AB4196357F09AA50129BCA411CE9255422C4AC1C492A2A60544C26B8F37A7EC9B053F27656AB1AE2BE0B1C95346083C15C8F89AD92C477D5C03BB95AC4F8B6A1C9BAA8D199F1D83823072082737BEBBCC1BC1E7BFF9742C4AE86313001F2519C8E93080C2AA1D63376BD61C31BC189BCA8C8385E5A7B3A69380EB72F14B164166227B453C6F43C59A34A8B6724193D77858113AB5C90B2A605F5C3C8E3FF8BDDF82C82F0C3045A7CEBA99891A60016C660BE767809977CAE5B8ADA668BEAC5374CE54692DD02F51E9A90BC68C51955797975F63707E1DF3AC14BC877529A02EF7A317976EA722CA3B80A316B2688B074108F05C3CAAAEB0241D1C5B05D458A150B692E288CE47CCABAE255B9EA13F80D16066155B2E46524BA5A3BDF74C9F77A25402CFB4439C497C03314465C4A91152671E4F79239937BEC7A72DAB0B97B30C1A00F100216B1E2AB67C6520608F54BC92985504033CE63363E015B9E0C9BF75CC2F90AA9E09DBC221258B389585927905DE077927D3AF6C59309B12B3DDD0029BE56EC0CC1CF8AA3A5041BD954B0B7DB68AF2BC02284C9FCBF954875816DAA11E6668455C8978A247B4011B1010289EC564999D069AA2392E9CD9A0E45636C014315C15AFB5148F513CC87EEB8B56334ECA1C061058C06B3C285DA8203DCCBEA8B750E9D43E8CB837F1AA0635954A924C866C33441E7C0C23C97CF62CAE10D32C2A641C1538C2A7B1C4DBB7BF069B43D4210941A4AA0E41748D788AE831018BE1916D056AE4598834B6BD56AB86C3B7528456751B33A0406A2FCF8C0FE18C25A1C208A5A4CDBE593DA6581E27C6A5B4DB793154233CA5B3C2469A80F8C8D3C22C25A8537270CAD4480B20D47C6AC56EDAD91BFFEACBF605AD61C44BF86B7624B95CB1A5248E9210D9F18837FCB01EC68BF434725FB75FF67C016DD78E303C2C6E775D17E61642776368F17F467BC9E0E153659C5219678490BC110B8184B556A2AD275FE6913B6BC744DF313EECDB273CD637A84234601599F05C6B358312FDD959E20B81A434A6FE200AF26A50C670837E8266303A7C23219F18A670CE910DF8756E403C31C561245110CD2DE051B61979D5BCFCC3E03F68C1D3412CE81BFE29AC4AA0EDF35537F3ADEEF43D0411B85C7E1D1C54612167DCE488EBE36E584B168BB5399D52B458A8BD122DE14EEF214515B70F38F972F41783005755\"\n        },\n        {\n          \"tcId\": 5,\n          \"deferred\": false,\n          \"z\": \"37B87F960BF862D8B81AB5F56E9E24ED8EB011A05867A04DEC9BAA519AF45E22\",\n          \"d\": \"CD568FB1EEC23C436C011A55BE2FD4362EF000C890BDE7611EB5C4618AB74F8B\",\n          \"ek\": \"F2137B2BD0A33F81C4DF584BB46C60FED985D09589C125A6F7A9C4B3132F7BF4A4B4A268BB52702C3B5DED770B3AA30EC2708B93500C5E3C6998FB6EA1586EAE409B6D617C330827F2A417DEB0007C78C8F8C025B8C3415A31313378536EA672FF92625B3443EBE61836421841750E1372A81BA99825B67898F2009E4AC8D5F89396724B7BF29E16C72C1DF192AA5277F01A5428B4AF8B284D85D987E015AE1CB89AD9C9230E2313601B5B49416D0B18B9D75326123A02E363C444993213D387A1C7A021449FE4E35C4FC30DF0066CEA593BF7DA67EF6322537802CF068E2D56B178F929A630C3A5043528A489928CBE1A1468F2802F8F890C042114426BCE7FA6A335441BF1B159410C62FED916CEF3B0D6F0B54FDC3ABF9778743A87D3F417EE573897B45CD5BA4F893CB13AA077A696BB37DBA964D0189FD83DC725129D9425D9291652CC84EB3145FDC88482407748985D72C124EC835CD800BB11F81415A44E474BC553998C1CA945FBB0035EDA52B93B3D341A29E7F808418AAD2FE388D0483E15F68E3B903E7D9C6E051347B1988B6A0A441C8B1FA05C743CBA003BA946BF07996EE14F9E93AC88382CD7F2CE8028352CA7C36A73C9303337CF5377AEA685718C8802B7683019892A904C5ADA3661C1CCA5B4CCB81350D8484BA34CA49AC15B9DA09E1E6887A10802060AB20E04B0142367BEE71CFE023A15602BA83B7761FB08CF79424715CB6CF563E2ACB3E11CA3D6C4095084779213275E13A1E7F14389C301AD53CDE6369E6A9058CE282530593A9E8B1BF38BA08F40B85A94678B619550C437411861B957528CD0175170A1F2308B766209EBF454B4AC4E24C1B230CC793C935770543831131CE38A77A7248C43C79814A03E35D8CBFE02A4F5482E73B1B7F41569E42443A733CFA185B63A120809007A46D5C718C003DE2945B295C0EC42C30EF5BDA5107ADB2125A5E61575382F961C43A585C7400C5AAFBA2BCF595C71C94F0F59CE48904531E0C2EB862C4E66CD6CF280A84B926C42489CCA778A587F0935B7CA369B1140594A1114DDCA6A6F104671CA26E8804EE0D0513F39519217F5A027363BAA21AA561E954B5494D2482873722C7BF20ACCA9B880\",\n          \"dk\": \"DAF41A7FBA550B084674F174453A18D33994460409F80232044AB7411865F90281EA191165940794957CB0C26E1609B941117C38350F653A04E28438D54C3452379687EBBE463A4A7447CEE3836E7EACBFEE6AAFD5114DF0A595C9F98FD85C2DE879857236CDD0287B2F0041E58390C5510E1D288B0F690A49451B1F7002E13594CE71045AB1B455A280377393F54A0BB0C44EE06C024733675A1821E619434401608A824F86ABBB55B07B1DBB7A96B0B88C6885735966DCEC666CA3281352B8651A8C13E48C52C61220572676E500A6E0A5FFD238A76CA6FF29915CC975B3E5B3B6819C61495410F095023077EC3CB282675578C948B01C63146B8904C56A1D419B1FC0AC8E5B5CFEDA4D8A8A562206B4A8E93EDD180936960CCD206964EA5CD9D4BFD3540A4D5BB2AF302A7594820EF8115F2B0E38324EB024AD5E6AC92584C681D5A42079450EC464B900BEC5D9B8268A843A5095D2F41DCD648CF30B746F6C4EFDD77A5C432100073CA36B61C0EC4C4D64341A675E9E06369EC30D0E63CB702B6D32CC1DA0253D4CFB08E2255FA5A3B29305520AB737B5D820A0817EC5BB4A99C8936A5B396D927FA97177573CA01E568A67EC059E897A75E058429BA03321B2D190BFF5F8A4C1AA3CFF7A54EF3BBECFE4985BF253B2F126B092A8808175C0AC253C6C9BCDF3710C7B90FDC77B4F95BFEDF78347E72F806191FDB4481CD7794A993EABA23AF5E76A081243D54C118868BC1C51A2AC00808196573326AF95A1B4A4DA8AE0173588A978130772792284AF456E8B651A974A09D5795F0BE0B08F1570A6C21B33440FAE93966071ACEC727FD74728E4A9A1EC7687AA2884BB6B4A599129F857113637BF248C54CABC6BDA91739036B7B605491625062678638BDA4F111BA376DA0D04262BA8A082FCEA37E9C82FFFB330BB15506FE8038D6CAED266BC63316BECD6400DD7297E76068FE16A7EE6A13900857AB6C2D8234BAF41C4967C2535F9BF8F888DC655887B5C74F6D17A9AB2AB16F02D57D311CF6700CB238C297489C5016A09A15BAE7428128B158C23000E80A88978993E956489B93BF2137B2BD0A33F81C4DF584BB46C60FED985D09589C125A6F7A9C4B3132F7BF4A4B4A268BB52702C3B5DED770B3AA30EC2708B93500C5E3C6998FB6EA1586EAE409B6D617C330827F2A417DEB0007C78C8F8C025B8C3415A31313378536EA672FF92625B3443EBE61836421841750E1372A81BA99825B67898F2009E4AC8D5F89396724B7BF29E16C72C1DF192AA5277F01A5428B4AF8B284D85D987E015AE1CB89AD9C9230E2313601B5B49416D0B18B9D75326123A02E363C444993213D387A1C7A021449FE4E35C4FC30DF0066CEA593BF7DA67EF6322537802CF068E2D56B178F929A630C3A5043528A489928CBE1A1468F2802F8F890C042114426BCE7FA6A335441BF1B159410C62FED916CEF3B0D6F0B54FDC3ABF9778743A87D3F417EE573897B45CD5BA4F893CB13AA077A696BB37DBA964D0189FD83DC725129D9425D9291652CC84EB3145FDC88482407748985D72C124EC835CD800BB11F81415A44E474BC553998C1CA945FBB0035EDA52B93B3D341A29E7F808418AAD2FE388D0483E15F68E3B903E7D9C6E051347B1988B6A0A441C8B1FA05C743CBA003BA946BF07996EE14F9E93AC88382CD7F2CE8028352CA7C36A73C9303337CF5377AEA685718C8802B7683019892A904C5ADA3661C1CCA5B4CCB81350D8484BA34CA49AC15B9DA09E1E6887A10802060AB20E04B0142367BEE71CFE023A15602BA83B7761FB08CF79424715CB6CF563E2ACB3E11CA3D6C4095084779213275E13A1E7F14389C301AD53CDE6369E6A9058CE282530593A9E8B1BF38BA08F40B85A94678B619550C437411861B957528CD0175170A1F2308B766209EBF454B4AC4E24C1B230CC793C935770543831131CE38A77A7248C43C79814A03E35D8CBFE02A4F5482E73B1B7F41569E42443A733CFA185B63A120809007A46D5C718C003DE2945B295C0EC42C30EF5BDA5107ADB2125A5E61575382F961C43A585C7400C5AAFBA2BCF595C71C94F0F59CE48904531E0C2EB862C4E66CD6CF280A84B926C42489CCA778A587F0935B7CA369B1140594A1114DDCA6A6F104671CA26E8804EE0D0513F39519217F5A027363BAA21AA561E954B5494D2482873722C7BF20ACCA9B880BC69A3AF4B4C837C8018E52F6A1466D86D23BDBECBDD1F610245F0A670ED311637B87F960BF862D8B81AB5F56E9E24ED8EB011A05867A04DEC9BAA519AF45E22\"\n        },\n        {\n          \"tcId\": 6,\n          \"deferred\": false,\n          \"z\": \"4B0A877F51434F70E2D8DB0A51BEB0A7572EF0DB7AC26ABC5D333C503B68BD5E\",\n          \"d\": \"35DEE1F800CA85E482BB12AFDB882FAE62CC77A338E65CA2265D77243ADAE3F3\",\n          \"ek\": \"49B884A964CAE5E0223906A09366063C46250A551DE820A58FF3CDE2C69719316B91F6CF1178768B082F97822DF4D6172ED11876940230978B6B0156EC190C462BA75DE722E206535564A68F311C7EE015701CCD7E954B5B4074CF43CE700C70B221ABB2198FE63639D622669F309855C490F916BF39917E3E440C97FA6BFF270AF540476215A8845A88D3E3528F6715E86C26164B86F28263B01123DD05553B8964775B0C4E50A8DFE08CBF08B830A0973A963A09C1B077564EBC355A07546BCF2422C42500F9B26F586B2873ABC392633555F72126A2B51EC935333939C73580E44314407ABEB4B06DFC570287C5B758E8AC30A6B2E912833E2B72C55CB9BAC9BA7FB2AC03E560796480EF9A4342D49A6FC19E5C1A9386826C42EC6319759B1CA260D43247A0756E857A00D05B78AF65A9759159EEEB72E7118CB6D039930584111A8F2E0CB5FB449423871F7030AB5908B964C2ADE84195802521C16088D14403EBEAC1A7963BD9DA827DF908752568B230919791A6F21AADDDC05E77525FAD0323F215215DF107B4A3474DE3711A447192537145127D7CF2B33F9A4268106B2CE487612B000E6B9520CCC70D7225E2A6055ADA0DB8F024333AB1EF182C8309A697666FE2850C29A1B7B9394DCDDA8AADB9C3EE2910AD1CA85E34874ACCA693C8CDCB66A477E38CF1B5AF5B97260377B4A43B8C0E085C41F5B2EE44B4A7CA1D58D9720C62475986798C13C1A8887F3EC660C04C92F0AA7F0C581407F67CA03247DFC5AE411CBC6F3BBC6F16CA66B8389BBABC9193C22858AF6BFCCF5A4595747B9F9F9BA043E69DFE06110BF970B2F4C9FF1A149F76CC4A64C7C283A716791EA6CC6B3554367A83AE1545054B02A725185EC8042966F10F3F3B4EAA872DEBF7B10947986D1884F3C59116538A85537016B03BA4B7A156061EED350BD04868A60C93ABBB05F97314BAC935EF99BB4301C4EE6A59936443A7298F0584980E588E76CA44F12182DCBACED7F90F4C864F8747C0F2B6A796083656FB9953495787071E61B41B46B582771320B8969C9B5941055637D69A5D0DF3C798D00D16E7259CAB3C67520D2FDE0A3C05715DE22382E369D7644EA2C180C0FC5385E6394054\",\n          \"dk\": \"4795036ADB898D0B5E69AA2F9913270B8899A97750A219C2B88C2BC0795CBE255E04E959A081263087706619A1C9C19FE54C05B14B1BF3E5910E128643771DA298839CFA2D752836B1759C5B58CFCD3B4782976EA96729315958FCB11C66B25F0A1B57EB85B1B892A0937A877C4508796B3D6DF66CC19598D55B9842C848A7485CF87ABB2A340497CAB032376E16613B55147C78864B7D0B9AB1F52985F32561A3A80AA4BE63588983E90EC659B2C9FC45207972C8199B46095BB56B878E256732C892C9395021521780D40778C4BCA06C91404428FEB050F44225CE0693972796A3D2AC3C028377782F9C26528F45962175C42B526872E6B34D95355BD0B1EAA2707B6C2A7F1074217CAA1EE842C4064A5D556B9F70C37A7C52CB468694245E427CC71959B390BAA5FF744869165189C1A4A43BB3D39A9799A17305999A9F3171711398FB412D3BC4625FF04444B0557A765FE785414BF08B308A5F44A64CEA59988F294483F623063B602D55624BE494F2D057396883D48995D10080AB399663C00FE0E17162B2C47A19BD2A69CDB2DB76A3861336F04C2FE0A498FC9698C34BDCE751CCC3C9D4E9539CAA0C6523C1C3AA87C65908D9045203C32AB19442F0065CB1003E02E735999441DBB7ACB1E73C92C62E87D7A4A65A508829739A576B13A7C94331146EA66897248B4887862A4152B72B2FC1B9C7CC7447C8E449EE93BCB5F88AFC43226EE51AD47BB548C665A9A2833AB7938FE403A602051685A3CC89B4AA6225E47564DD09D05B101CAC312369A54B320194710AC53DF57C5479A7485250D1EACD5B8BB19069266A03C852F81947A1BAC5005E344447A9DB5FC4B87255C638524A9ED255BDB9700B31D514A5A72E08A51F42D963D71A88BF605E29C41EB8C88D48BCC0493CAE6129BA4B046B54AA08177634C8E8B1A5A948301256D3C3B6A9A8808B2B0F3716CBBF196427C661F342CC7B50CDB56990AD252242C2C996FC98DE1A26374C0575DB6773937F0E86A16F051D59DCBFA88058BC4801E096BAF9866B9C5A01A922A6EEA34D22CA346975BADBD28454821EFA0B1288281849B884A964CAE5E0223906A09366063C46250A551DE820A58FF3CDE2C69719316B91F6CF1178768B082F97822DF4D6172ED11876940230978B6B0156EC190C462BA75DE722E206535564A68F311C7EE015701CCD7E954B5B4074CF43CE700C70B221ABB2198FE63639D622669F309855C490F916BF39917E3E440C97FA6BFF270AF540476215A8845A88D3E3528F6715E86C26164B86F28263B01123DD05553B8964775B0C4E50A8DFE08CBF08B830A0973A963A09C1B077564EBC355A07546BCF2422C42500F9B26F586B2873ABC392633555F72126A2B51EC935333939C73580E44314407ABEB4B06DFC570287C5B758E8AC30A6B2E912833E2B72C55CB9BAC9BA7FB2AC03E560796480EF9A4342D49A6FC19E5C1A9386826C42EC6319759B1CA260D43247A0756E857A00D05B78AF65A9759159EEEB72E7118CB6D039930584111A8F2E0CB5FB449423871F7030AB5908B964C2ADE84195802521C16088D14403EBEAC1A7963BD9DA827DF908752568B230919791A6F21AADDDC05E77525FAD0323F215215DF107B4A3474DE3711A447192537145127D7CF2B33F9A4268106B2CE487612B000E6B9520CCC70D7225E2A6055ADA0DB8F024333AB1EF182C8309A697666FE2850C29A1B7B9394DCDDA8AADB9C3EE2910AD1CA85E34874ACCA693C8CDCB66A477E38CF1B5AF5B97260377B4A43B8C0E085C41F5B2EE44B4A7CA1D58D9720C62475986798C13C1A8887F3EC660C04C92F0AA7F0C581407F67CA03247DFC5AE411CBC6F3BBC6F16CA66B8389BBABC9193C22858AF6BFCCF5A4595747B9F9F9BA043E69DFE06110BF970B2F4C9FF1A149F76CC4A64C7C283A716791EA6CC6B3554367A83AE1545054B02A725185EC8042966F10F3F3B4EAA872DEBF7B10947986D1884F3C59116538A85537016B03BA4B7A156061EED350BD04868A60C93ABBB05F97314BAC935EF99BB4301C4EE6A59936443A7298F0584980E588E76CA44F12182DCBACED7F90F4C864F8747C0F2B6A796083656FB9953495787071E61B41B46B582771320B8969C9B5941055637D69A5D0DF3C798D00D16E7259CAB3C67520D2FDE0A3C05715DE22382E369D7644EA2C180C0FC5385E6394054BAEFAE1CB7C96BC32E97C146C2AD302DB01C6E7B8E43BC7A236C00C6FCA6F17C4B0A877F51434F70E2D8DB0A51BEB0A7572EF0DB7AC26ABC5D333C503B68BD5E\"\n        },\n        {\n          \"tcId\": 7,\n          \"deferred\": false,\n          \"z\": \"B1EF909D94C56C134107B913B0ED29BC0851CCE424D0FB69EDC04C685A540871\",\n          \"d\": \"D9502C86FB461300B8D142A906B766B0B42481EA9C83AAE2BB74390F882B0509\",\n          \"ek\": \"36C0A54CE714E66B18D200AF7C822E555A9BCE32884D2313C0012FDB81436D8038C37258EDECB2EABC9174974758C55DCE98933322B49CE815DE3B83F3F4A7C0D7CEC0D2A08D68475853B64D21ACEE3C74BAF17C19306BFFEAAD2303ABFEB0AE941839A4F69B48B23447081BC42BC1CEC648C9D1B319E778DDBB262A7B7D0A0946E5B7B71AD54E881C42AA6B1DF268B998D45C2CA18B9F35900A6106F1D8CF88F07C37A7AE283AAAF1B94BCAC6A102F631053760C57B157A098FF4F47B023527BD526365259898484211749AD8493ACBA57C2E02C346EA5032553F2F590F785B7691C83DEB9C7124318969FBBEB81C361F6CADF4EA78C27185CD9B9BD99670ECB41F56C4CC261A39EC251DF34A144B189E108094244557F217312B1BA083F7804928C4950950CF75555BB0B7140406F38CC8B9D21449A756AEC213328AAFD6A8596BB20152BA888F4A591CE162968BC263984655F332ED420473FB6DD0BCCA2D0A3595E43046C61AF6D6317C123BC18AAD6E80736F41BF97134CA7DB2DA8C963F9E2518D865F7FB770C5BA20C2501B44A36E13782583C1579133260E41A77871CB0CD7827D7015FB66251EE9622EEC007FA88227C153B9E7509866AC836A85BF960182F14032606123608E302C0C07D2BDF79B9C398CC437A7A3A2688E0327418B483D1C2B6C3B92B76554A7E14555BF742180FC7F2036C260F50AF5D47AD24033FC62B08A6C10E2F0096B4C5EC5DA8260D2CF9B3816C92A353148C19AC410466C05699209C6B0628A5CA12472544AA822442B93D4F549F5C94A54C4A3BEE5A9375B55B69C77DC260AFE242F6333591279AD1E269D36F02722F62D235186074B3B1AD1C731E0A36996672E74797DC7BA677AA577A523FD38A4A3FA71B7782185911BD837432920A753996776465D33692C859252E294622EE86F2340A5BB624898774851B587B6203A7E94CC909623D027C9EF06493596B9E6A63C02A4CAD21B2EC4792C6FA70ED9AB90D8988F85D24BB37579D4A71C0823167DCC30382178E12C2082AC70F1D7A2DE652296A203D93C2E3CC1473C411C17390BB0ECA50981905AD7F76DCD197C5867AF1F94E3221BF042DCDC99B3B3679587BB2507C464618D\",\n          \"dk\": \"F9AA2D50611583687E826193429B23F39B07E2988D6A018D98E05E01A853C40BA5E321730868A55D245769F17CAE243F39D6389346A307934BEE6A011C4A00ED72610E61548A64C5F2D98BFAC484518C4304E454289CAD24A5BBFD3717AB191E5C83AEE1E22CC1B756C147786A13580715B3482A5301D2271588BE13374251B68EDF6024AE553F1BCB83CFECBB4DA1A0E759627A37BF3F83B73E267D40F867629380482CA32D6743A5C8565C7AC3DB3542813B010E98642B490B361638483728B47737A1B8367EDCC93D8B3960EB963038184AB09DF1945A154A8437556CE659054AA64D81048032375D795A21C89B5F5DB68DF203C496A06CD9B26B6331B2B00BC5ABC95DD6BB596DA819958A11A2F57A8C85C9471691F7570F41C0390C6571BFA22A91909BC9C09070C0B1A0F5AEBE59ABF0C3C36B2C17A5D1767454C15697C10CE453A1597F63440D6127A09941A5FB3A96DFFC86C82015BC623986E6BE7CB02AAA23B5D5E26E4CE01F8FD91D99B637A5BCAA23532D46B993D4EC5797C28EC6D248589CAC83A600A7D555E3EA7F07D09726B174AF282162A3B268A5280CFB6AAD7012E7E639598AA2003D9DD6707FA285AD334619978BA227B879B1F37DF83157DA95B5DE8CB66A01D036E766262719E21224A65932C6D1185676598BDC51C4A29330BB8C4B5176A069B93848B57BD24AD5F277A5681674138B5C1CA73075706C9797D6F8BF23E2AE5DB44D5D6B4144C09AC9C6BF6E556790968717C847A6C01AB99215162BC5B2DB6E02E19ED568A56C54BA4735A793DB473BE5708C705C784B9EE2E02532855F2F420608990101E34CA930C63A0833744B67965BCE0C1ACA3D67793DB7BCEFB32A7D378D38E87209F69C66097908EB8B07A94584335F37C44586A55C5024847C654B97C85BACE70B495559BCB60AB1625525E808DCA2496F549C8130CCAF60BDC3F782DA61AA98E03A80C05C6AF99308165B1291AE613C97C646A46E05AB87796DF6447E1D8326C03A7E9D7921BE218B69D469BA2449E5C714B7C5B1385C407BE14E7A978566E27409B72D04B9540FDC43D7BA0C95754C36C0A54CE714E66B18D200AF7C822E555A9BCE32884D2313C0012FDB81436D8038C37258EDECB2EABC9174974758C55DCE98933322B49CE815DE3B83F3F4A7C0D7CEC0D2A08D68475853B64D21ACEE3C74BAF17C19306BFFEAAD2303ABFEB0AE941839A4F69B48B23447081BC42BC1CEC648C9D1B319E778DDBB262A7B7D0A0946E5B7B71AD54E881C42AA6B1DF268B998D45C2CA18B9F35900A6106F1D8CF88F07C37A7AE283AAAF1B94BCAC6A102F631053760C57B157A098FF4F47B023527BD526365259898484211749AD8493ACBA57C2E02C346EA5032553F2F590F785B7691C83DEB9C7124318969FBBEB81C361F6CADF4EA78C27185CD9B9BD99670ECB41F56C4CC261A39EC251DF34A144B189E108094244557F217312B1BA083F7804928C4950950CF75555BB0B7140406F38CC8B9D21449A756AEC213328AAFD6A8596BB20152BA888F4A591CE162968BC263984655F332ED420473FB6DD0BCCA2D0A3595E43046C61AF6D6317C123BC18AAD6E80736F41BF97134CA7DB2DA8C963F9E2518D865F7FB770C5BA20C2501B44A36E13782583C1579133260E41A77871CB0CD7827D7015FB66251EE9622EEC007FA88227C153B9E7509866AC836A85BF960182F14032606123608E302C0C07D2BDF79B9C398CC437A7A3A2688E0327418B483D1C2B6C3B92B76554A7E14555BF742180FC7F2036C260F50AF5D47AD24033FC62B08A6C10E2F0096B4C5EC5DA8260D2CF9B3816C92A353148C19AC410466C05699209C6B0628A5CA12472544AA822442B93D4F549F5C94A54C4A3BEE5A9375B55B69C77DC260AFE242F6333591279AD1E269D36F02722F62D235186074B3B1AD1C731E0A36996672E74797DC7BA677AA577A523FD38A4A3FA71B7782185911BD837432920A753996776465D33692C859252E294622EE86F2340A5BB624898774851B587B6203A7E94CC909623D027C9EF06493596B9E6A63C02A4CAD21B2EC4792C6FA70ED9AB90D8988F85D24BB37579D4A71C0823167DCC30382178E12C2082AC70F1D7A2DE652296A203D93C2E3CC1473C411C17390BB0ECA50981905AD7F76DCD197C5867AF1F94E3221BF042DCDC99B3B3679587BB2507C464618DC0D607E1D82C3729DDE4E456836E956E187ACC8BB29F262BA6B5038F51F9F8D3B1EF909D94C56C134107B913B0ED29BC0851CCE424D0FB69EDC04C685A540871\"\n        },\n        {\n          \"tcId\": 8,\n          \"deferred\": false,\n          \"z\": \"671C8C054A52A67BEF8015DFDB5711C9197E84A5A553E794AE0811C8432FEF6A\",\n          \"d\": \"07A9BEBF21C83F6E5417A73D8CF5B527568C903B5883CEC8347B4ADE73AD92D6\",\n          \"ek\": \"A2178E9F524466CAC53EB49DBC5367F4F096394526BBFCBBCF0178721902A0373BB4520D50F039517950C5C0115FCB53DE3ACAA4916C94BB19D746972DC882129B3A4F658E4671B538183A93B1775E56845C00569987103D1A3270D17D0C807EDEDC3F9390774F074741611A3A2B8723C671CCF6688051CFAF25B2310221BD0290F5E6C32DC03DD44C87E395B7E8B56FFE15BE62926411AC28F1A34DFE5B460091520BA244EF5A0D26401DE2B3A64DAA9CCAE3C6A09823D9C84DE1A17E1E94CB26992050F2381C3C947EF575C564CCB6580148A12200E97D9F2969295A90EA78548743C95D15AC8FDB0D91B83AF0A9A75A862AE2558FFE6A7CDA7008F0CC9B5831642047841314406DD0815A09ACA883667F080C6AB959C969140A767FF56A087DA5BD285265149963404293E2E1BCF7599A30A20CAAA056D5285C2731914D85C9F87BB3FC613A809293C81922343A0565B27B779A72BBB0B961D1AD8AC4AF5EC677F997A29C38BEF5F6CD90A897F379871C0B658A7849BA2C43725694B361331236ACCFB55C5E9650277C3D378259067354A8E2167E5578D8F8B610476B997403FAB4AEE365C118929956309B7F7114BAEABFA43408820593D334834B1826C9E705092953C5B73421785C3371687D8C366F9420706389A06198422BC13BAA7075F9B54BF6679A166942E231E1D60637B42FE9BC00E0A9ABC646B3D740201C3985EFE87FE5467F88D6456423A58B07B8FF8C1159E738DCB1A2B3313654DC9F60A072A34ABC3A8ACDBEF6429AA5C088666A9FB14119B69D37D9369D1662DFF3487E826686E812CFF23CEF60C52948424C4A11A933A2D28A8FA880A3E6753CAC7C79F4E111893016E0C067573CB588A6B1FDFB23DE855B9BA6441A837C9F47329A014732CB3DA8B9AC850816E7EAA56584734F3B60E8C732214A86D0309FEC791E7EB28A93836A0FD85C5FD497CED070D71101EACBB92A052140393B18D83FEF654CDF1CB587B2B5CD56CE8C4215C57C065F954CE2A2C0BB65744DC8C2D49930702663D2A0173E515450524B973B7AF4D6B7C816773B6C3B68EC1A06A3C74E038F9143015D3401CA6DBDBEF1D5EF5A349B1C9DAFB96E20DCFEE7BA2EECCE5A3AE6\",\n          \"dk\": \"2B409CB0052C3BB60811731A4CCA005D8C06028BB26D166770E680B1C3CF981608DCEC93069393EDC4559E56673D0B12B8884B8056BBB988672D373E26A3268153288EB91D1EC4642657B7081B7EA587294074555067ABC197A9FB83A15502CD103458CE2736FAF068B2344163A6913E8A232601C217ECB3DA3B57D8D4AA0AFB56A4AB20D250CF97206D78FC004CFA63DB3C27233B3A9A1B8369B102F03AC44F231683F10C4676C2E0BC19273606C8534C1151ACDDC299C9F0C432CB0F03FA1A8FBA4299B4C3AB6638FC924742B8871876989B7C4CC3685FB09B68F5F7126B5645F9D365E5342B23A12E09E4B7CA3ACC3EE06EACE941EAF54A8475492537295F732D8D88538A594CE96333A5E16EEF42155D241E0E2A1077477929A9C51B6737942A57AF4964E26CAD78F22859C453FCD1927592CEEC583724718C92B4BCABF121BC079992A1CA574894ABC1C364456D806A435291CE2C702247F286E9989FB108283B52CE5B6A209BF43F1C991BA200550D90CE2B2A23B715519AD3BC3ED277AA6A50975C2B54B6B47AE7C7E55A7B28BA996ED344A03C21824C9F0E0C55050A54F1F4CC3955353B955419B4BBE0962FB8D4C9EB94B2D2764BFEB41EBC6AB8383B4B6D58B97FC9394E6AA2D1B389E4BB5C4A72AECA0780F15B5F29D5910D3A40D1D21989379C640310F9598F05607F2E47129B27C347830388CA25DC0721B9927FD4B594819965843083B4D761DC76C67626393353999BB4149D4A0B7B5BB51987C96F10731E2BC51ED7621E68179120869226CBC3D4A5B4D07DE6E5396747674150B07720BAC4E94A982B76ABA552E880BE6B7227357028528A26850750685C5B73AB95FBAB8F0F604E3DCC5B4112A72133478942C68A878BD9D92C0E6CCAFB46B57E563C062922AEF461FD093463483C9A2188AE9113B24748077AC624726BD3D30D354497616359B292A9C4381ADEF34B215BB326147713274284F6B3CAC38A40BC9856E459C029A639299B528BA553C892F2219635D0489456B3577AAC3453B6075A72DBCA89B4F27B16F55B5E1C1A35214F24579033E5584A813F8F502DA2178E9F524466CAC53EB49DBC5367F4F096394526BBFCBBCF0178721902A0373BB4520D50F039517950C5C0115FCB53DE3ACAA4916C94BB19D746972DC882129B3A4F658E4671B538183A93B1775E56845C00569987103D1A3270D17D0C807EDEDC3F9390774F074741611A3A2B8723C671CCF6688051CFAF25B2310221BD0290F5E6C32DC03DD44C87E395B7E8B56FFE15BE62926411AC28F1A34DFE5B460091520BA244EF5A0D26401DE2B3A64DAA9CCAE3C6A09823D9C84DE1A17E1E94CB26992050F2381C3C947EF575C564CCB6580148A12200E97D9F2969295A90EA78548743C95D15AC8FDB0D91B83AF0A9A75A862AE2558FFE6A7CDA7008F0CC9B5831642047841314406DD0815A09ACA883667F080C6AB959C969140A767FF56A087DA5BD285265149963404293E2E1BCF7599A30A20CAAA056D5285C2731914D85C9F87BB3FC613A809293C81922343A0565B27B779A72BBB0B961D1AD8AC4AF5EC677F997A29C38BEF5F6CD90A897F379871C0B658A7849BA2C43725694B361331236ACCFB55C5E9650277C3D378259067354A8E2167E5578D8F8B610476B997403FAB4AEE365C118929956309B7F7114BAEABFA43408820593D334834B1826C9E705092953C5B73421785C3371687D8C366F9420706389A06198422BC13BAA7075F9B54BF6679A166942E231E1D60637B42FE9BC00E0A9ABC646B3D740201C3985EFE87FE5467F88D6456423A58B07B8FF8C1159E738DCB1A2B3313654DC9F60A072A34ABC3A8ACDBEF6429AA5C088666A9FB14119B69D37D9369D1662DFF3487E826686E812CFF23CEF60C52948424C4A11A933A2D28A8FA880A3E6753CAC7C79F4E111893016E0C067573CB588A6B1FDFB23DE855B9BA6441A837C9F47329A014732CB3DA8B9AC850816E7EAA56584734F3B60E8C732214A86D0309FEC791E7EB28A93836A0FD85C5FD497CED070D71101EACBB92A052140393B18D83FEF654CDF1CB587B2B5CD56CE8C4215C57C065F954CE2A2C0BB65744DC8C2D49930702663D2A0173E515450524B973B7AF4D6B7C816773B6C3B68EC1A06A3C74E038F9143015D3401CA6DBDBEF1D5EF5A349B1C9DAFB96E20DCFEE7BA2EECCE5A3AE6E2DD64A46E296448B930EA2D39F462B16DD4AC6AE402DA573C0968CCDAAC7F50671C8C054A52A67BEF8015DFDB5711C9197E84A5A553E794AE0811C8432FEF6A\"\n        },\n        {\n          \"tcId\": 9,\n          \"deferred\": false,\n          \"z\": \"C02D5CAD9E565727E19B2EFE4FA2E083F93EA0F5ADAF97522F33F416F786765F\",\n          \"d\": \"F682949EBFCFA5DA31368E3F177DD146448D0E62178959FCBA4CD4F02CD8B17E\",\n          \"ek\": \"0285512CF6422AFB54DD1C54CD200FFB3AC28AE053E8B6443B96C83DE8595F710D883A513271CF0D93804BE93C84B9A754802C04455D89887DE048B2484088733A701847300FF3B137F509D46033CEC89A4B9602D5A83F77CC20FF4066BCF95904E2789755359F7B04B174983CC883BDB393BE2A02FD3433FEB126A4288EFC4670B0670B2715CDC5700DF6631DF29643E33382EC89B89BB1AD41C6ACAD704BEE982841CC588249BB91A818890BBB3C590A08446D88647FCB5CB15824CDB09C55FAA2457961BB3C79ADAA833F55F154072CAA445B8D61E23993CB69441C956492AF07E702568A350C01BD072CB2E379071845A67149B2F7B2358551770A34A9AE6AB45B2B6D5F700E597184A1425B28047817720C1981C583717F7F4BBF2F13A6D31A691DD3951310A9D135591717A451F00FDD71ABF325B041E3A1EB095F31D27653D1AD0F3CC945D431333A3349B7B8D786B16769309808C11095209CCA1442AA40B9A863B4973E85586899ABC17D1034E64505C1F9987172538E227111EB86A66C9077C268B7A24A477991DDA6753672BD972510EF3347E1B7314BE172A457A4A0B9909718418C478D396B9F15BB6D49D919E192BB714BA992686292056DEB4B1AC84C0A356911437BAF2E053B170162BD25A0E554A757037C1E40BA6E341EE230B592D33F0B910D32C01CC3F865D104C3B652A63229A90B155D41C68C3008C3DF42CB757A36634C6C7372CA47A82A652C5115BC0383842E5AC18CA6239799A37C91700845180AEB407B88D9390F0A6353EA1459D258E70567E2A5530588C63257B3A33050FEDC1C4FD58BF7F547648B96927A4B6C01901F2CC97995AD993C41A7290F7D555C173C1FB12AA514446E2495471305C9D94236656729DBF9832415C3F130864C4A2807509B14BB187F6450F9A92F020B62238A8F23BBC8ECE7BB33305E3C70B43589BF43C88D712B386C68945F7A7F56A23DC8D45ABA95A89BF18BB0E1AEF4DA810723775AD3125C4316056433EB04202BEC5B83D84040514407E7862685652AE2BAADE15056A2106E546A134A8527A7CDF78866E42CC7C322861CD5BC3D7424AA358204AE6D07694FAE81991F5C67C160B09429BE12543DE0F8\",\n          \"dk\": \"893175C6F86630760EFF4332E35A8642202B2F8503DD24AF50B1BCF10B63951B8E6D426D0357B27A93293252852911C14765099D330D25608885934216114FE2300D5119262C4B959A9928EB058DCBF1636E30A173F5A31EB580A90ACD8774233E652CF4E78AC1C51282D2BDA92A0F6B74ADBFEA4CB8A68F2D3CBFA3298AA8F203EB32063DAB216F811363E69A28A8C8417B524F09BC5D864CB5B502D88B2226193C3729CBB968105FDA459ED9688B12580957180D6A01FB16CCF3E78A9D07CB35A55135B2A1C3D559F5B01A4DD58311417F4E1B98875456849C6E1A632B539180A9108DBF872D96847E79E82C6D23A51AE23042763361A125AE4572BD104F03C548D3F1A377F481F261BD596321BC68B34B31A40829B8AE3CB49C2631A229244EBC08C722839DD8748346C979A9AEB36901E0D9980B6220CD2B32F6C8B1C01C5C61AA0055C32C4F99B1268CC806BBA9D0139D867389B04A54AD51816E965B5CE4A0EF245D6C2006457AA0265081D26214CEFB5671BA20166680CE0C8A817005F7574878B9AA168A8E6A6CB99924543A374834742C48665253E03DCCBC552A5938C0115C5F709B298B0C0E245696CACA53152728B60ACF3C5EE1F92DB30131DB853371021AECE73936D0606A8BAC25430CE0138CCE05048395301A03860D439F9CF45F82680DB9B98B0DBAAA41BB776B60CE41734AA92B914458A7C1CB9CCD55731FA5660D567F489B3BB56754319954FD6A78E6626C69C68A6EA882482113886B8BBB980F118193C1165FFA3A9A8D1A20626514F998CE61EA8BF92B09E253C773D5B2BFDC7C3DA38115B8AA30361C7BC4344CB772EC722B55D1AC4FB7C8510CAA1886C345DAA0CC440EA1DAB0BD131177B9013925418B4B1D54B90BF7F5914AA49B65882FF1297ACED09E36B066671666B875BA1BC37D2FB76F80240D2B996BF6515A0198B66B66A4DEE40DF748C807E1378EA76308076A05FB1F6A071E2D635576F952E4F62A15CC6442563EDE9C8E204789A8F06EC8EA6032D5032A77CAA5963CDDAB24EBEA5708C7B761A67CD0464A2A00233AA648080AB5B2191D73A91D0285512CF6422AFB54DD1C54CD200FFB3AC28AE053E8B6443B96C83DE8595F710D883A513271CF0D93804BE93C84B9A754802C04455D89887DE048B2484088733A701847300FF3B137F509D46033CEC89A4B9602D5A83F77CC20FF4066BCF95904E2789755359F7B04B174983CC883BDB393BE2A02FD3433FEB126A4288EFC4670B0670B2715CDC5700DF6631DF29643E33382EC89B89BB1AD41C6ACAD704BEE982841CC588249BB91A818890BBB3C590A08446D88647FCB5CB15824CDB09C55FAA2457961BB3C79ADAA833F55F154072CAA445B8D61E23993CB69441C956492AF07E702568A350C01BD072CB2E379071845A67149B2F7B2358551770A34A9AE6AB45B2B6D5F700E597184A1425B28047817720C1981C583717F7F4BBF2F13A6D31A691DD3951310A9D135591717A451F00FDD71ABF325B041E3A1EB095F31D27653D1AD0F3CC945D431333A3349B7B8D786B16769309808C11095209CCA1442AA40B9A863B4973E85586899ABC17D1034E64505C1F9987172538E227111EB86A66C9077C268B7A24A477991DDA6753672BD972510EF3347E1B7314BE172A457A4A0B9909718418C478D396B9F15BB6D49D919E192BB714BA992686292056DEB4B1AC84C0A356911437BAF2E053B170162BD25A0E554A757037C1E40BA6E341EE230B592D33F0B910D32C01CC3F865D104C3B652A63229A90B155D41C68C3008C3DF42CB757A36634C6C7372CA47A82A652C5115BC0383842E5AC18CA6239799A37C91700845180AEB407B88D9390F0A6353EA1459D258E70567E2A5530588C63257B3A33050FEDC1C4FD58BF7F547648B96927A4B6C01901F2CC97995AD993C41A7290F7D555C173C1FB12AA514446E2495471305C9D94236656729DBF9832415C3F130864C4A2807509B14BB187F6450F9A92F020B62238A8F23BBC8ECE7BB33305E3C70B43589BF43C88D712B386C68945F7A7F56A23DC8D45ABA95A89BF18BB0E1AEF4DA810723775AD3125C4316056433EB04202BEC5B83D84040514407E7862685652AE2BAADE15056A2106E546A134A8527A7CDF78866E42CC7C322861CD5BC3D7424AA358204AE6D07694FAE81991F5C67C160B09429BE12543DE0F855656EF499C81763075415747986E379B5BE816E964FFC959698CAA61FC6B31BC02D5CAD9E565727E19B2EFE4FA2E083F93EA0F5ADAF97522F33F416F786765F\"\n        },\n        {\n          \"tcId\": 10,\n          \"deferred\": false,\n          \"z\": \"70567D6DFD6622814417BBF673812F2D02E5BFA897D464957AA4219841A93C19\",\n          \"d\": \"170CA6BB76C065255DFDCA3EB93C772E57EBEF8C9A291C8F0BC4444BF008C868\",\n          \"ek\": \"E295110B123EADD95814826F65D2568360578F2011EB917A24E45AAFA629D7E3C8DF3210D85831CB806B8EC64CF5157938CA08FF6848A11B19D8CBCACE2090474C30C6053E20138C01149114A2A73C129C8055ACA352B05AA4A464B07614C4ADCD002B0B477F624CAA0026820A2455FCE2CE9FF2CEAC299554DCC21FCBBE9C36A07AD67103C112B9F955A1B551D109C88EE54075F203016A30B4A437AFD635F105B7F5B1BD5ADC5925C9347176AC0374CB38250A6583928012A283D4B067E0CDCA554C04A3C1CB0578070CA5A545894841AD38E599643B1CDF94ADF4E917057886A8BC577ACB4668769707DC73D7186700F65DBCF3BA3964B952B7244070053532042924552CA4A8DB807FA12B572D7582B15BCC6DC152A1125955885ED176915448CB7CF5680FE7AB37D22FED7A5E76552481139112351E4FA01EC3B829822788E3A9A463A582F98B9DAFEC1E05378413D805D3D043234578C73032D9006C21C733FF45CB649AA10C09775350523476A5F760175FEC96A26B2E52BCBA008C1C28707ABE5189F2515164B0194BCC1803231B41DCAA20C335FFE0A8BEE5425CAA5832649450924BB1424734A0940E7A7A24D917741010B9B00C81DCBD4BF394BDB5BF03358614718FB1F162877824EE0156F4C462C597B937EB8768C2CFB6F7BE36D7C307C431AFA83824C812CDA245A6B923A5199DD59C1344895E4E67C07F448641D32E4F526693724E0A025369621B220341AAD6C43A4C7CF6C27A5CC65095BCCFD569019808B2AD850168A24371E292D55851A0BC58DFB0AE57E3512B1A26C57CB373F996C0341BDC4714C451979524A58B0502A9996CE09980AC9CB4EA763B8490B4A7B2530B87962B702BF1F04E3CA65AECA1086AA4317331C78848504A9784F40B4A36BCBE01948F08AB400036BAD091AA1AFA1020D60B6278686AB365F208A6D4021C90468FDBE7243BD89D5994A7FF092841389243AC308CECBC5C12105E1408D500A5931B86FD16B555E0AEC9509A63103EACA5A0090C0A0CBB90F8F970980B60954C5985F5644C0914D1C558B65C9F54A42343E824D713B0F1EC9F2BAAADFC824BAE43FE99FD58BF6E845A2AA9ABB4AD4171FD9ADD88AD012EA97F\",\n          \"dk\": \"17F2393AA323522A7D4A6590D57BB2C0EB6B54F578FEECC61C742BFFAA01C2E2A01F68586BC083907728C4431AD6726C58C3C35030CDF1B9808BD14773A91E01577B6E3B37312617D50672B9B9643638229BD151D6F904AB4023A5B1203556B690A41DC558A2BCB00B167C611F9977E5538F227023F088BDAC535556280D3BA63E65721797998FC2FABA44BBB697ACA8835ABF06247A184A8B91518B32A8295B086A9D6313E2825C27C7C126EA1365C68735FCA9128C6256202AF141CAF93525B833578CDC0E31EBB4B2A601643249D32B4C1021C79A1933ADB593E02555E0A467687A5E3044C1815163EDB75A27F15236F03437DA44BB0BBDC24751F04C1F5ED8CD87AC1D6A295CAE175C301781FD39555D688794E74C70D6674A80613A3AC4286A28095CBF68D51A98F50EA4415C6D35460049125A9197A18ACEB0819962BBC6C1055F64B09CD46B6CF9564EBE085767893F7A67AD021AB9157081250A150C904633F9314B6192E221543BB1B68B152B26A3BA74C0C50F54A00E3163505BB51658CDDFA59583847D99133520D08054F6CDF69618402A848A79571479AF36F84A33082E4E05B71401A31879CDDE8346412A481703A88C698416540D85380D8AF880933B5A252B731E9071FEB951500CA7422144A8080280E7054F840BB88763EC33C734754430CA01C5D8A0430B2843975BD1A283DB930CD4CB2B89425F63760426A93AAA27337F22B32577998C34BDD66B6CF6313EFCA95BE3A9CED1A8402533CB3DE7B52A94C551CBA6AF20010D0C7377F8165CB21445D9CD102A15A6A72C91BC4898A47A4C4262A06CCCF3B384E0A52116F05B3FCACF5E1137B96342B3E6BDFE9CCE7C0BADF855A8DA9A00439335ECD15062998609116A1B16BEBB208ECC37CD4ED3BBB3F6A022A9B708513D47164FB5CB51910448CE82186D3000CD487A20B84034929245108AAC455445743BEDAC9B62918644F7183F17C2BCC7770CB5355AB719024177B623478C2C4326E3474DA1924C963DDCF80048999E7D6BB3C9128083B83B9A13710F65ADDDB604FFF15B24E78221C427236799F9B942FA123BE295110B123EADD95814826F65D2568360578F2011EB917A24E45AAFA629D7E3C8DF3210D85831CB806B8EC64CF5157938CA08FF6848A11B19D8CBCACE2090474C30C6053E20138C01149114A2A73C129C8055ACA352B05AA4A464B07614C4ADCD002B0B477F624CAA0026820A2455FCE2CE9FF2CEAC299554DCC21FCBBE9C36A07AD67103C112B9F955A1B551D109C88EE54075F203016A30B4A437AFD635F105B7F5B1BD5ADC5925C9347176AC0374CB38250A6583928012A283D4B067E0CDCA554C04A3C1CB0578070CA5A545894841AD38E599643B1CDF94ADF4E917057886A8BC577ACB4668769707DC73D7186700F65DBCF3BA3964B952B7244070053532042924552CA4A8DB807FA12B572D7582B15BCC6DC152A1125955885ED176915448CB7CF5680FE7AB37D22FED7A5E76552481139112351E4FA01EC3B829822788E3A9A463A582F98B9DAFEC1E05378413D805D3D043234578C73032D9006C21C733FF45CB649AA10C09775350523476A5F760175FEC96A26B2E52BCBA008C1C28707ABE5189F2515164B0194BCC1803231B41DCAA20C335FFE0A8BEE5425CAA5832649450924BB1424734A0940E7A7A24D917741010B9B00C81DCBD4BF394BDB5BF03358614718FB1F162877824EE0156F4C462C597B937EB8768C2CFB6F7BE36D7C307C431AFA83824C812CDA245A6B923A5199DD59C1344895E4E67C07F448641D32E4F526693724E0A025369621B220341AAD6C43A4C7CF6C27A5CC65095BCCFD569019808B2AD850168A24371E292D55851A0BC58DFB0AE57E3512B1A26C57CB373F996C0341BDC4714C451979524A58B0502A9996CE09980AC9CB4EA763B8490B4A7B2530B87962B702BF1F04E3CA65AECA1086AA4317331C78848504A9784F40B4A36BCBE01948F08AB400036BAD091AA1AFA1020D60B6278686AB365F208A6D4021C90468FDBE7243BD89D5994A7FF092841389243AC308CECBC5C12105E1408D500A5931B86FD16B555E0AEC9509A63103EACA5A0090C0A0CBB90F8F970980B60954C5985F5644C0914D1C558B65C9F54A42343E824D713B0F1EC9F2BAAADFC824BAE43FE99FD58BF6E845A2AA9ABB4AD4171FD9ADD88AD012EA97FA29C976B07F9A7D707839A6D72116355207826F54F27A23B6BAF16524F3C4DA570567D6DFD6622814417BBF673812F2D02E5BFA897D464957AA4219841A93C19\"\n        },\n        {\n          \"tcId\": 11,\n          \"deferred\": false,\n          \"z\": \"71A6E59B13B36CAA406DBEC53F3FF2F0CC529098A4C8FBFD032C8BDB8B0E16FE\",\n          \"d\": \"176719D76EE1CEA83F7751BC4E3DDD00868B5C504C79AF8730B9F7595E7914A4\",\n          \"ek\": \"19288830B0ACACEC7E939B82BAFC8FBFBABAEF732309CACE58533E4F5C62B711C5D84820EA520F030A7E61534275206EB4BB00A53472AB206E50D68C7B3C3D82119BDF508272EA5D40116C9DAAB1F7BC5595B9623DEA2D683C69A079494E3A10BEB2B3A1667CDC031F76CC73595C20D3587970FC17671CAFACE83495037444EC6F485329F6A13328F67D5B839609A56D336C05FB712B1C78A944037CB3E9CEF1D46C420A118EF8712593172548C64FF616E1166649819FDE13235D5341C52A69BB154F48C672D3370B59C19A86B13196F25388089D8ED463FA93AEF6F3857832ADFB874765E318A59801ECA9799BB0A303B87867846A025129AED31C5D2336EA968120C0B88199A83221B1FE6BB2365A086165C0EFBC6185332FB10BB542A32547AB549B3C9E666246CEF04EBF3CCB3CD445C7FA2DA1DA6633E227B5209654AC28DCE78E45B2AA927244D8BC4D881C9462C87F228C07B9C488FA660EC29B60BCF494D6D97C163C53607556361298CEC90332732D7A921D5ABCC3BFE0A3439055093CA7EA1A5E75D70809A55ED886C5A9E90353860239EB907635BD3B46B78EB42821E773E62A1910331F127162B694CF64213305055AD00923FBD137A7F305F345884A6BC5B6C534CFFB6F986A9C444446E3D74170AA375D359E4FAA6C99E4315D230F9B69C37E2C444F29C7EC64C277864836410E8FC703437975E3E83E76F988432570BC635F868C96CBDAAE5C6B37D2E203D452C762747FE7494F146AB7591C9285B68E3973364BB1126C3585BB694DC2262E38D94CCA5C1427992E563C72C9A103FCDB4EEFEB46F5171E9DC2B611D9CE42197352E140B50902EFD676EDF89896B5A4808095FCAC5DDDB218E6AB2F5EB88D831648FF75B1382B6558BA9BF1D8316EF4040E82860716075E80CDC24799733B9EEE74702582320C5CB56C495670FC239CCB6FB8F91F7F217E5BC4A0376B9832F0AB8EB4184704B69C33CBECA37579FB085462705BC72AB7F37C3804B1BD14089FC07C85956038F80C67F409F433854B99198307B2ECB38170DBB55F422260D594F33B3DA9F35C4AC2C2DB9275CE21D56E9CDCF309777C616223E7C0ADD4FA7A6472CC0BC13CDE85BD4536ECB1\",\n          \"dk\": \"ADECAEBF22777691A57353B3D6DA4AC5959286C157A3245282D34812374C88067ED3109817C86447F5A807D24E82316C56944EBCC8A68C761170A218390193CBC14C2859466BD164753944C39269660A5583B2B3309216447886A702127BD77F4A5A03F9B19BE0A76A4ACC0F82267A8659BFC72054AA5A23C84940354323DA78569F1370553A71DFC54AFDFCB669377F3E324098E17709361CE1F824DD2C47ABB25FFFC25ADD519F6079C226D77E6B3231B5D74E11221869881A8DCC2188505628E86FAA2ABA16638400CD58B569002851288E631991C3C545B345A1D5646FE304059A4C742A4A27AAA9FBE9593F374B51B453C7E09427A6325474AC7FAB0A3168630202A016632B3E490D77CB61798320C4139780015F6B521A81E2C34494A6C4FA45DD3712B5349B142A1A57166C1FBC120FD5BEA702BA1878A9064028C575BD5535BF5DF70D46536A826BB687E82BB9A541DD795114359222A8733CFB5D6856269D977AE07887DF5395CA6553F74907E27B011320C103A4191E5AC4F6BCC1C12624D02B2295150C52CB6E5C1423E50922AFBCCB0BF70B2CE8379C2B9C635C44D7C72BBE11C23E4417BDDC9339A6CEE23514E790B1931820FDA6927DF74D79DA1B762562F207A2B14776B98615AAEA7276A585C6972BFEDB7327415AF609227A4BCB757762AB819236D5C615772CF00A492B846C731825E6643C53F55BE7D3555EA4B484A891FC35C47E406F12E6C6B34C090144C6754A12E969B13D0A98231AB7CEC0AABC414BB2066FE6F7297F2635BA79A18656914F4977721C6FA526081E2436975B85E9B7687ED16DA57413B5C16400823522434E38658983B7AC65A39EABD32043693D9B87C1F9F08D80E540A4A86033ACC7D6C961F1DC0C47981CD9588FE469BDDB86762992118B94CB9A213CB62A12E651B654518C843B3E3959612F0C6D97F419866CB89AFA055DF145FA28C302BB1E41E342FA852E3F47380F7536A3824A9B8697BCE6525271B3D0936C4BC172F5A5CB2AF5AD78B72A1DA30C761CA84F123E1BFA58B0454279B87517B906A030562F7691DA4A09C456975E601319288830B0ACACEC7E939B82BAFC8FBFBABAEF732309CACE58533E4F5C62B711C5D84820EA520F030A7E61534275206EB4BB00A53472AB206E50D68C7B3C3D82119BDF508272EA5D40116C9DAAB1F7BC5595B9623DEA2D683C69A079494E3A10BEB2B3A1667CDC031F76CC73595C20D3587970FC17671CAFACE83495037444EC6F485329F6A13328F67D5B839609A56D336C05FB712B1C78A944037CB3E9CEF1D46C420A118EF8712593172548C64FF616E1166649819FDE13235D5341C52A69BB154F48C672D3370B59C19A86B13196F25388089D8ED463FA93AEF6F3857832ADFB874765E318A59801ECA9799BB0A303B87867846A025129AED31C5D2336EA968120C0B88199A83221B1FE6BB2365A086165C0EFBC6185332FB10BB542A32547AB549B3C9E666246CEF04EBF3CCB3CD445C7FA2DA1DA6633E227B5209654AC28DCE78E45B2AA927244D8BC4D881C9462C87F228C07B9C488FA660EC29B60BCF494D6D97C163C53607556361298CEC90332732D7A921D5ABCC3BFE0A3439055093CA7EA1A5E75D70809A55ED886C5A9E90353860239EB907635BD3B46B78EB42821E773E62A1910331F127162B694CF64213305055AD00923FBD137A7F305F345884A6BC5B6C534CFFB6F986A9C444446E3D74170AA375D359E4FAA6C99E4315D230F9B69C37E2C444F29C7EC64C277864836410E8FC703437975E3E83E76F988432570BC635F868C96CBDAAE5C6B37D2E203D452C762747FE7494F146AB7591C9285B68E3973364BB1126C3585BB694DC2262E38D94CCA5C1427992E563C72C9A103FCDB4EEFEB46F5171E9DC2B611D9CE42197352E140B50902EFD676EDF89896B5A4808095FCAC5DDDB218E6AB2F5EB88D831648FF75B1382B6558BA9BF1D8316EF4040E82860716075E80CDC24799733B9EEE74702582320C5CB56C495670FC239CCB6FB8F91F7F217E5BC4A0376B9832F0AB8EB4184704B69C33CBECA37579FB085462705BC72AB7F37C3804B1BD14089FC07C85956038F80C67F409F433854B99198307B2ECB38170DBB55F422260D594F33B3DA9F35C4AC2C2DB9275CE21D56E9CDCF309777C616223E7C0ADD4FA7A6472CC0BC13CDE85BD4536ECB1EDE9E402A11646293C3851259F4E8E4412A3C5986F8BE4427BE39C4E869C061F71A6E59B13B36CAA406DBEC53F3FF2F0CC529098A4C8FBFD032C8BDB8B0E16FE\"\n        },\n        {\n          \"tcId\": 12,\n          \"deferred\": false,\n          \"z\": \"B63478F2FC887334C707E9D836E3104892566B3568CD32B583F8C9A0DE1A1F0C\",\n          \"d\": \"3C90FC402DA953172300194876B3B3BC958268747751346DE7134566CB8FAA5A\",\n          \"ek\": \"99263CC303C92A0BC6810487CA1664E5405EEE5987A621C33E30483D4C7862F9760399B4C23822507810AA321D04009CFEB2505860BCA818BC0CFB9DF44A5F47D23AE80C25C2A67455B64AF2D276606A208C74867EDB5373BCB93B06AE81DC67FB73514FAB8E62516D0A0760FA0228E6BB0A252120FC7676A0340FC2939CE8118782631B91F962CB14200C928091876998B1B19EBCCD59F92815930DC6F9BE629099B3334772AB3A16D630D971B2093C136F29C2EEB09F03F8592FA4980599542E422FF636C8BDD6672454238D4C547FCB43DE1B10FB385CFAFB1F84D95E6C776973867E2E40615531A3126B076E9B30D3E9BDD2E12C34A9764E90000AD179CB159259202800299B2902606603227BB37A9B679320B60B3EB9A917D2C0F0CA6A3248B1F371801661A58FEC24B8807B06A5B67D087CC6E3008D823024575C5D7678C93973E8201101765DD5A10889091F8AF87512C206C2C60A96081FCFE32C3B7C18A643AD56515674A3B745D154F7076B65E191D5026301AB45076B00288B91DA147F02795A0D0AC9E8C61540405F44C8B7C766BB454525B8D48B9418B34E3135B8BA3AC9E2486B162C23B266856BC370B4833028C0F753C1E6490A9608B1C5169E6EF0B43BF6C5CE0A6122CCB28C9A96F794B4A5479394C49253217D4DE0A93884A1E47860B127B7DCEA8A0B17019AB410A0737BD81C6EE6EC6DA84569DA3A8ED34ABBC6588325007C4F5651814C45F9DB9AB460AF28B68671E6075AA8AC5F9B2166B79B81154274F058E38B24309017D2B098532964A3DC453F133194D1160A356098321C89033BD195848FF1C989FBCB92192A9E54C528F5259DA796DAF18831A040D8E8C999857FF5251A2A1510929940F1836216566F443AA903BC90528A9E7D842DE70BB75CC8939E48044BD67A4287CEABE22C3481B1F4775260E65A9DB00BF5218136143FC2E21CA720B11540A83DB93C16F10A12E7BAD3A27DCD807E15678687D325CB23256FCAB5073690E3B645329C87379C7B1DD7375755B2E9355D38A9870911B10BD5680FFA08C1442E412A6ACE489E4C89BC7923053748132526C541A0EC4C2A84B606573C36E027356F49F0978B029037455A22E3FA2390\",\n          \"dk\": \"30015BCADC63AF4C80E5F94529725041C8C0746963B3D1A5F655CBF9A8BBCB8B983DBA06CDD1256C1147B0ECCC4D03B600744139D84E6664B40AC22BFB1B03CD61908724078A8B405D868A462B7272594FC448C3EA5908A824091653CD71EC858291B26AB60ABB403884EAA6909A3AF7FB0519384D91856ECCF48C1843C50F1130B1A66DD38355DDE0533EF08484F4536968691BB1900CB52A80039F6CD201FD6C9AA45B0D1830576289C1D3B233BB11AC2D153F6E42A7B5A5AB4B204EEC2707AFB266BFE93C17B6AE2183513E38AAF7F6C0D29B347E6AAA31D24C2804606608A078A25CFE607846103744135E44872C0C410028908E2A608601730DE42B01D45400271033CF90691340ADF473C487DA2BA2E6B7BD2CC6B6EB0D9F01CD7031015F1015C6A24E0C19853E749E2F16C61C2809FF1AC00D113465868E56A919A8310FF6D43E3511322D293F9DBC5F965458A184744F3970B0A16359172B16B7C8CA43B311F4AE96380DA46292A3D1404604054069A181B1C202A02B9AB34DCE833BE08C2EB0DAC641C024C416BA0DB34E10546682F02B6B6B630B801CF8B2CC4F98C671307B92407BDFDCA4CD41B30B4AC8EA3811ED9665FAF479D1057B313CB113941155E539D4C61011360BEAB816CAC019051356533991343434E5C28AEEA797FAF85668B824F3F8BB0329CFC7ACA9992C3990C56CCE31A53F40CB2D07940093504E8A7CF091B137308E1D30B3B40A3624776F9955B62F3828DE769EDE44B89DC828473213AD08488C73A9C9FB2813C6750CE148E37AB3BEB0B60D2203BB444DE44ACCEF685047D9C325478F7CF3A72C9A6F1040A898B1A71EDA1B5F87B57FA08F2F75C4ED616C02512F205159F1881BC559C93925A639F05639589FFA2C38E3062A4FE091A507267916495B033FC0901A47C645A8D82425216EC251C8C0D37A4BFA44C0435E23D090D9D10528A03E611628C5654C5AA1C42280A3C54CB73CB26A5A68CC5D13496093B14B470E3A57038C30CA15712EECA379978521EC298E7BFA6AA698A04925CE47413A62283ECE3C79D955CC3E9C69F783C9B2D58438565A99263CC303C92A0BC6810487CA1664E5405EEE5987A621C33E30483D4C7862F9760399B4C23822507810AA321D04009CFEB2505860BCA818BC0CFB9DF44A5F47D23AE80C25C2A67455B64AF2D276606A208C74867EDB5373BCB93B06AE81DC67FB73514FAB8E62516D0A0760FA0228E6BB0A252120FC7676A0340FC2939CE8118782631B91F962CB14200C928091876998B1B19EBCCD59F92815930DC6F9BE629099B3334772AB3A16D630D971B2093C136F29C2EEB09F03F8592FA4980599542E422FF636C8BDD6672454238D4C547FCB43DE1B10FB385CFAFB1F84D95E6C776973867E2E40615531A3126B076E9B30D3E9BDD2E12C34A9764E90000AD179CB159259202800299B2902606603227BB37A9B679320B60B3EB9A917D2C0F0CA6A3248B1F371801661A58FEC24B8807B06A5B67D087CC6E3008D823024575C5D7678C93973E8201101765DD5A10889091F8AF87512C206C2C60A96081FCFE32C3B7C18A643AD56515674A3B745D154F7076B65E191D5026301AB45076B00288B91DA147F02795A0D0AC9E8C61540405F44C8B7C766BB454525B8D48B9418B34E3135B8BA3AC9E2486B162C23B266856BC370B4833028C0F753C1E6490A9608B1C5169E6EF0B43BF6C5CE0A6122CCB28C9A96F794B4A5479394C49253217D4DE0A93884A1E47860B127B7DCEA8A0B17019AB410A0737BD81C6EE6EC6DA84569DA3A8ED34ABBC6588325007C4F5651814C45F9DB9AB460AF28B68671E6075AA8AC5F9B2166B79B81154274F058E38B24309017D2B098532964A3DC453F133194D1160A356098321C89033BD195848FF1C989FBCB92192A9E54C528F5259DA796DAF18831A040D8E8C999857FF5251A2A1510929940F1836216566F443AA903BC90528A9E7D842DE70BB75CC8939E48044BD67A4287CEABE22C3481B1F4775260E65A9DB00BF5218136143FC2E21CA720B11540A83DB93C16F10A12E7BAD3A27DCD807E15678687D325CB23256FCAB5073690E3B645329C87379C7B1DD7375755B2E9355D38A9870911B10BD5680FFA08C1442E412A6ACE489E4C89BC7923053748132526C541A0EC4C2A84B606573C36E027356F49F0978B029037455A22E3FA23904DEBEEC7CC5EFAB5DAD8559722613802856CD726336A26B171DEC76493C71460B63478F2FC887334C707E9D836E3104892566B3568CD32B583F8C9A0DE1A1F0C\"\n        },\n        {\n          \"tcId\": 13,\n          \"deferred\": false,\n          \"z\": \"4EA6EC5384C51903758B807395181F6D6B4CCA3FA1CA24110B08A8AB1742C411\",\n          \"d\": \"24B783E39214CC39910799ADECE53B32408C19CD9ED10DEC039A9FA2CFC1CA30\",\n          \"ek\": \"37949792925BBDD82552AC59C024AE05B6C9FBAA558A9C4D1FD2A64EF27E2C077617C864F0FB704093B8ECA24CE0CBC9073125070C0CD24426CAF58F97B6549585C249E147E7440DA19B5D55C94EB99274B2D390D9BB6DEDC66545863AD1247013BA4021228AAB05060C9C6749599D8E74895E297C7F05ABB661B98D6003E6F064DF663BB5204D6E12A7ABF58ECF0BBF8C9B3379653E2AE073C3945E410BBF14FA6AF910B3562C7852923242B894E677777398413F511FE38B34E12C787AB75C7CB50C23E432A437B83B483236D7736BA515A3A76C956261B68274FF368CE9B04F1BA3C93E51A6A220744E370620C17B8A851B8A3B01A7017C6765150959AE4C567282D3C087FC6150279A8A3873A4A9231B5C4EFB998DF0A62B808A934F2701CFB2289DA578F1621151C136D79509EF3B5D9C16706CC3BAB798B1777A6A9C9A53F1B277D50CB7A40148485AC1550417AD735899532AB03AC8DACC790987A76AB988CADA1B1FE02400ABA70DA5C9C991725744740FCA0D5EE37486645E4B6047DA0C862A26A89268CB8B9781301AB43ED88FB5403BDA3A63882AA6780A6075474486643896B285F0E2272E261A54B09F0931099323781E797DD7173AFE84569F05BD12134C39B631A13812F2958B872950E25AA07CF2BC5CC90E930B71056B562F06AD2CA268CA82548BC6C7E079690B22B900E8C16CB9463C8B4FD731A74890C325B7CAD3871FD1F67DB78077A13A5E5F9720A4581DF75C4B9C15050D9C4923D7AC7669BE31C523D2C377F1917550862E8E5C39253A5317DCB16529528DD9B1993006DC796668B892DEBB8AE9E8A7E28877403872EF4BAC5367CB09795BD209ADDC54A329CB7549959F286A1237287756AC9943D2B5D21A936D1C7C2CC784E69661D76317ED0241B9C978C02A6E7DF86667506129697936E557251AABDF769CBAE5614BB2BD354110ADFCC8770CBC72738C13064DD14BA724B6755FBC176683B2B5283F6B99B60919A70A2C49CF29378847BD8AE1C502801334F8360369462DD97AD9E0836BEB57B98815D43734CA4C95247CAC55F38786A7747AFB8D8E678465921D7BF29D607FEDF94631E9377AFD92D51FF18D4F91280783577370AC011B\",\n          \"dk\": \"F6FA89BA9A8A3752C28830B89884388B95A2CB3624DF530E143346BBA491DC7082D5144BF36C9B2B828D1D516F9E8165826C1D9CD081F301224F8409FDE22C54F72AD6319033524F0331248043304CF53D44F31C24914689EA9E04A204C96561678A73ECF40E8938ADB137B9F3970464F72C4C115314304DC317ACEB5C2733661F49F1038141BF82174F3634AD82E08E69117A27B4292937482466CBB3041223B51E2BE32C8F7348E3B427F2C77A94D475FADB68C5613312B92A38A86DD694735EABB42ED21A90BC7FB0A5C90049BBC42926F5FC5A0F563E702266BFD44479D8B1C620C670E6AB0E7551619B8661F7BF33B97D12C20414C35866B007E34523184B967B9C6B4AA5AD7DD3CAB39497377C2F468538D52463A3E9945ED90F24FB964C85C47B202CA5EC533CDB286206A14B681A9F145B2BBB3F8CA65000B92CFFA80693EBBA5521C624D1B84906A8C6AC3C7881B10BC592B3B5549861B53BF1B6C1C19E70579EE366006D019E06492EF375BB1D8800B6C60EB7D3B5D78348EEE4B7AFA10E6786043B1C3A9BBCB3DE313189801BCEAC0F9B0C4E631366863297CE39C845835F5B25136BFC8C25C737054C184CE18074A1B079A00A6AF778EF8C3D45F5178A604CC19AC427118FEEF0014A047CC394589617040D96682BA919EDD61E0BC572A21CBE7D189F2AF69F02A6A313881D94A5259D8872A04854D11482635C9746CA083581680E5429BEC2C3A7164D39FB766D786F5FD54B9AFB3B90BB55459C2811860D7159BBAE38B67B4B18865B326CAB9AE62C9B1E636F0CE48260A4B90D6B1F4197371DC1661EA07E27993684CC9423FC5C15F8B1F34C0EDEB0B837A847B9ACBC53F4CF3E88766161C121595A0F940DF1E01C4CC3192F839BD7B660835C570C83577F3C9D88851EE3E9B74B00416F034DBDC2B7D081995854ACB5F4CFB55C9605E6891A8041CBB6101FA94B52F7902CFBB256B28648C9585E8B564E7322911B9A9030013157501D1991210B754983777FF99B42CCC87FE5A847E8A084662DC1D3058C6999EB6C7935B4470A25B4720925ACB046773B16C1E70E2AA5BD37949792925BBDD82552AC59C024AE05B6C9FBAA558A9C4D1FD2A64EF27E2C077617C864F0FB704093B8ECA24CE0CBC9073125070C0CD24426CAF58F97B6549585C249E147E7440DA19B5D55C94EB99274B2D390D9BB6DEDC66545863AD1247013BA4021228AAB05060C9C6749599D8E74895E297C7F05ABB661B98D6003E6F064DF663BB5204D6E12A7ABF58ECF0BBF8C9B3379653E2AE073C3945E410BBF14FA6AF910B3562C7852923242B894E677777398413F511FE38B34E12C787AB75C7CB50C23E432A437B83B483236D7736BA515A3A76C956261B68274FF368CE9B04F1BA3C93E51A6A220744E370620C17B8A851B8A3B01A7017C6765150959AE4C567282D3C087FC6150279A8A3873A4A9231B5C4EFB998DF0A62B808A934F2701CFB2289DA578F1621151C136D79509EF3B5D9C16706CC3BAB798B1777A6A9C9A53F1B277D50CB7A40148485AC1550417AD735899532AB03AC8DACC790987A76AB988CADA1B1FE02400ABA70DA5C9C991725744740FCA0D5EE37486645E4B6047DA0C862A26A89268CB8B9781301AB43ED88FB5403BDA3A63882AA6780A6075474486643896B285F0E2272E261A54B09F0931099323781E797DD7173AFE84569F05BD12134C39B631A13812F2958B872950E25AA07CF2BC5CC90E930B71056B562F06AD2CA268CA82548BC6C7E079690B22B900E8C16CB9463C8B4FD731A74890C325B7CAD3871FD1F67DB78077A13A5E5F9720A4581DF75C4B9C15050D9C4923D7AC7669BE31C523D2C377F1917550862E8E5C39253A5317DCB16529528DD9B1993006DC796668B892DEBB8AE9E8A7E28877403872EF4BAC5367CB09795BD209ADDC54A329CB7549959F286A1237287756AC9943D2B5D21A936D1C7C2CC784E69661D76317ED0241B9C978C02A6E7DF86667506129697936E557251AABDF769CBAE5614BB2BD354110ADFCC8770CBC72738C13064DD14BA724B6755FBC176683B2B5283F6B99B60919A70A2C49CF29378847BD8AE1C502801334F8360369462DD97AD9E0836BEB57B98815D43734CA4C95247CAC55F38786A7747AFB8D8E678465921D7BF29D607FEDF94631E9377AFD92D51FF18D4F91280783577370AC011BBEDEE8ABCAA18E82777BD6D7F6E49B9CED29D3E3687A5FE2B528C6AB45700A2F4EA6EC5384C51903758B807395181F6D6B4CCA3FA1CA24110B08A8AB1742C411\"\n        },\n        {\n          \"tcId\": 14,\n          \"deferred\": false,\n          \"z\": \"9FA6AA53F505506BE269CE201A1A6EF95692DD1350A7188F468D34C5DAE5EAD7\",\n          \"d\": \"E4F2972F746E028108A5BB98EC97A307DC9363909DEAFC491F040B964675B9FC\",\n          \"ek\": \"32CBA337E8CA7A629B588B26218CAC94C03A73E67F6C2037B80607AD6907E2B7468774A836A270CBB03075173D97D837DF5C9E1A3889E26992900902F15A131DE49E6E36B212700C91715AB5AB568C50AA7851A83C353D60BCA348A76D26A62126B9152301A9164C3A01F05907E3283512BC72C2B39E4A7BAE0AB3963A129D2AA39A091F3534110243BCD036A0AD4A3BE03A1A7D3272D1F0ABE2E7455E82BDADBC8B25057667E79EEA898731D85B42381692419E3C101DFBF0132CE91448718B3E5C3F04274251C0854B8C3B3352B77F082DD8C10FEC06A7EE09A8ACC8A5370186E2DA4FA0DAC06CCB09610B23D8E32A1E756E2CB6A8A2BA9265C04075397EFF66A21D2B2C2990603B6664A1283297730C6E1C024D42783D982B875BA957DA4C4F695B49370A4F407ED3059614147D522936DADA78B34C1749C6C04A099BD1C19E09A32F2CB89DFD24696B2C3E97BA38C1BBB198D81C13B73B007D53B5CC70E4C3A856E0ABEB44B5ABEA1E449C72FA106DDD57C3BDBC592D01538A05B32FC85DE8E4952C2B4A2CBCCCA0100E3007885EB68473B95C9406515568AA0B1844EE2376308C841F70605BEBA3E8901707F7A874F087FBAC9A917977CFD63BC16269AC7800DE605663C16D3741077A099DBE6809C6EB162EF0AE62302114D01172567A11EB3BA5B1B425507966639E9916886A8882038B6FEDCC44DC2519DF6C79D8CCA6E7821CA96655136102FDC394A0F13FDF283B43A6BB06E2528E825398303F5940A852396B40C0560D14BC66243FE6773588591907005C5B39CA56267F2DAA4CF9455ADECA0E1CBA99E5E3991251072426C0BDB024A3F8771A8C19684A8CCF42C39948A0E7D951E9A63C5994340E43AB6937513608125FCC86346308BC87ADB154499802878A42273CAC8CA77B14D156A22921CD2AE9AEC363CCBDF2008A28621461700D288C199A51F2A6118BF2921FDA1FB963B9C048B989DC282AA9BBC9D0615F66CE21123CE5465273786886DA0289C55856B08569BC2A880742383A4B07A28E7F6B326AEA9BB5FB5070196BED6698015C232ED5A60EF9900347864D57229DCB6A9AEDC2A45045F720191A1AFCECAADC8C1E65E470DE2C6360C098955016\",\n          \"dk\": \"58B5077542461D311F6B00A10EAC4432C70881109FF588BFCB3073CB15382BB886119B2FF390C4BFD329D18C74A524CBFDA709CDEA35CE2AB73B13B399A9327731A29ECCC0589C762D594D80E12F1A005698B406E2D025F9AC025C46658AF413A17053CDE4CA13D128CFF52922F046535816ECD8601401C4E5CB15854567A9A7B165B42D84424F8CABA817B6631F9B20BDF41A859C56B1D6991F21257BF606C37A2503A83BEED9AD5085B4A0B709719CAA57484B8C639E613B766BF65C2CD449F8332142E0C650A618EA165DD0B594471C3C1E80BB95F504F8D2764A7984830765E182011336B9D2A23B07E2BAD8CA964E4A56007460F59A41A42AC62C17234E0443CBA6CF27724A6E8B651BDA3B788C8D99981A8281789C478143923883261C976475BD5602B013933CBC89A9959A9C2984CB3728D3E29702B26C3E728EDFB4165B387952FC5FDBF70AA32204147C458F471375AA961C1773A8AC3746EC49B7A120AF022A22878958B98FB6E37639D582C07233964106B3F015E95CCAF8A621945B00B61059804C8D47A82FD014269E2C5068B3ABE6B310D9C63C7D37B778D07DEB47B1178BA19C5B59C3E86796F2CF939341E1226C4A5537076085CD9B66CD731125CCB20E4249CD655E547748A8000392BA37F7840E515863E54575167A98B4BB81CAB934CCD53AF976CAB207868B18AB7530AF2D1908ADB85D000D32AFA706F81B90178001F2C57449402C64353F0FE33FE8139153FB3EC3577522373A0B092C55CA22EEA64FA886CF53E92B571C04465A7DF9909FCFC20463258957023097845C88A0C09CFB96AA960692F546B8A1893CE5846AD3CA83C98DB7650A42E56AEDC197ABB09579F6247F79257E71CBE27BA494F668EA60807DC61DCBCA1D1531261D776BD41901156B82C40BC5852517EA130D52D02132D151D28670F14494A207193E668613C58E0E8A2F5124B47EB5CFB2276FA75B27DF08359C5920FA9757A8AB32D40631F593C26E63ADD40B3DF6B849BC8AA8BCF7AC1530C73A802F2FF2CE94CC4EF26158A1F66C37EC4A947568CB68442A42C6E5BB358643BD90014232CBA337E8CA7A629B588B26218CAC94C03A73E67F6C2037B80607AD6907E2B7468774A836A270CBB03075173D97D837DF5C9E1A3889E26992900902F15A131DE49E6E36B212700C91715AB5AB568C50AA7851A83C353D60BCA348A76D26A62126B9152301A9164C3A01F05907E3283512BC72C2B39E4A7BAE0AB3963A129D2AA39A091F3534110243BCD036A0AD4A3BE03A1A7D3272D1F0ABE2E7455E82BDADBC8B25057667E79EEA898731D85B42381692419E3C101DFBF0132CE91448718B3E5C3F04274251C0854B8C3B3352B77F082DD8C10FEC06A7EE09A8ACC8A5370186E2DA4FA0DAC06CCB09610B23D8E32A1E756E2CB6A8A2BA9265C04075397EFF66A21D2B2C2990603B6664A1283297730C6E1C024D42783D982B875BA957DA4C4F695B49370A4F407ED3059614147D522936DADA78B34C1749C6C04A099BD1C19E09A32F2CB89DFD24696B2C3E97BA38C1BBB198D81C13B73B007D53B5CC70E4C3A856E0ABEB44B5ABEA1E449C72FA106DDD57C3BDBC592D01538A05B32FC85DE8E4952C2B4A2CBCCCA0100E3007885EB68473B95C9406515568AA0B1844EE2376308C841F70605BEBA3E8901707F7A874F087FBAC9A917977CFD63BC16269AC7800DE605663C16D3741077A099DBE6809C6EB162EF0AE62302114D01172567A11EB3BA5B1B425507966639E9916886A8882038B6FEDCC44DC2519DF6C79D8CCA6E7821CA96655136102FDC394A0F13FDF283B43A6BB06E2528E825398303F5940A852396B40C0560D14BC66243FE6773588591907005C5B39CA56267F2DAA4CF9455ADECA0E1CBA99E5E3991251072426C0BDB024A3F8771A8C19684A8CCF42C39948A0E7D951E9A63C5994340E43AB6937513608125FCC86346308BC87ADB154499802878A42273CAC8CA77B14D156A22921CD2AE9AEC363CCBDF2008A28621461700D288C199A51F2A6118BF2921FDA1FB963B9C048B989DC282AA9BBC9D0615F66CE21123CE5465273786886DA0289C55856B08569BC2A880742383A4B07A28E7F6B326AEA9BB5FB5070196BED6698015C232ED5A60EF9900347864D57229DCB6A9AEDC2A45045F720191A1AFCECAADC8C1E65E470DE2C6360C09895501683E3ACD9B35DCF6D9E93B006201B0FE23745F0E2E2CD1793BD7B3F6B84220BBC9FA6AA53F505506BE269CE201A1A6EF95692DD1350A7188F468D34C5DAE5EAD7\"\n        },\n        {\n          \"tcId\": 15,\n          \"deferred\": false,\n          \"z\": \"A9EE7619E4F0250147ADC188649A45EB6D82DE5EACD5643CDC52E6DF8DF2F8EB\",\n          \"d\": \"C5C26DF5BA8BAB4A293292BD070986A8063F736469F6ABBAB684F7127575172B\",\n          \"ek\": \"230563C475571E5CA17BF116280196E0AB1ADF4CC7F4909859177190D17E6A7390DB291CDAAC33A0A08F6FD11608DC404A43AF7E467AD3E77F6004192A758409C459F4CBCCD5AA4DB3D47891416EDC659F8628B20A656CEEA904893B84EE9B6D44B4B1E3D2A5DAE49D7A38882D6C01A07720565A878E3A3B715B27DE84B14721885D5A8D06CBC93268A35A8A3360BB10530130A1C8664117A83BD550D8CA2846250D0FF722931ACF79824A537915D21C9892A786681B44997CBD17F7C02498AB3EE4C6204C79E848BBFDC06C0112C3BD622B9FC4964F64B539630F943583E962B765816E111C850AFA6073C1BB1EFA6E450B9B518C9EB98144F5912F75B30238297F0A934116D63BC9007DE2F21D192A4A515504BD7C73E58462DB50BC1A373922EA1AD5CC4C5FCC64030B4863B047D37A0CBC2AC48484CA642615F287610DDB718BC562559992206191D1507B3BB94B6714BB062468C13921692ABD9EB93A8D3CA09C8A52FCFA36D0F9711E545028BC737DB2B3ACF9BC850A48491488DE346DED5C47DFC089EF074F6C38B3BCA7C42746AED58660C5C984D8E59F3D28276D45263D667191A25BF35A3469B55E36A449DDE713F648A18469A8E417BFDE0784E2CA68589A50FFD38EA5744116F951BF79B90B1380717C6E4C8940627C5BD4F395EF0B38BEA7C556A14C03F2A3C5A2A039BA8CB0BBAB2C0C2B4C88A5D76849F657C361212E18CC96E39788B1D086ED61440F2B10266225C48A36E0380D783677E64564BF39BF8FC8AF7C581FCE252FFF73858533B24F2610A78A2D83D7625FC1687D0CB87088B8F53B475A7A2E8E1720E6E97C1471712AD948C8A2537DDC06BEA22BC6CA8575A26D4315180FB17ABC797DD135545AE953925795FD12167796A54A9793DE8621B5E6851180CF681583AB7A33C4584B709836D79A1913E6197FC4A66876C638B12C77BC9C8BB95FE2A251D61BC260016287DBCCE42C0FB6F30B1E41440957AF90318A48F61AF588C8A86C4E6DF854808C88F2343EB075BD4BF89F7CC081F76B2FC1D410475C27244C8377852B60204636AA7619FB3D7196CCD0B211AAE40E5BE5AAF9651F99A040D32D1335AC61B1685014B4839D3BFEE2660706F9\",\n          \"dk\": \"206C8727F6315309314A290D77492763F8391F365742B55AA2AAB82B98492E008869EBAC27545657E38FA68A37B97B1147111A21E10AB0D522495B92F2530032D440D0C966C5D56762C841D4E68556748D15509F25142E090473D48312B453750C155B4E763B23D515CB32487BF1922E7747DA07A6F3613A50630ADBF0A9CF7A9CF2F11D2C2A244FAC0E4878A6EE24609BE2098A047A0B45173AE5788BD831A5C6C237B601371CC0594CA08E8C1BC5A040ABEAB693ACAE59B058F59B633612081B170E334432C44C80F4EA4758C75701E22A66963425840752D49E92821356149D1328C275669A3B319EE92A2B32A87D313C3F20A8A2C56021503B0FB90B599F775FD008C182F54BE04A63236B459186586804CFF9F479CD531386EB57B6133BF3545076EBCA22BC3002F62B247147C1057BCAAC43B559B6F05842E2687ACA49CF2D0C855E5051C86A74567503D975C342C2AFAC165857C90BA5472C517226A4297FBB62ACD11458788736D592475ED00B2F5C41F2C5B90D6762B064BA0DFCC40E5CA433B2BE5F6B69AD364A63999E4067944F12AA7D2203167318EF6B5220A10504778987F59A6A8A9B2B702799E2A03DEA73A66B7AC776364D2A487E043030F8A476416BA05C62E918AA8670A39E769CEF5993CC34A2A28533AA79635988278A2B2F5E787A63502F41EC81FF660E96B9801E5566F80868C843B992B86976671C9BE56B1A146F83163250558F2B489E065B11AE976872D3B4B0897A9495527B3CCDA0A9A83784CA8E764943AA1F8497451AF259B61B306EBA0995B29C246812F090481F837FDF6663FB277E384250DED44B6297A46BB58C0628A48FE92EF5802BF14666E4282101D3CCBDE76675C6B99B7125A5A262374165F3BC7918F06CBD651DC302C793820281A0B83657A836365E9C982FF973C83FB6A57754967F7112061B9198A6B749064F7FB247FF262D7B234DE684319BA5A4AC0831078116AC70BB643081E9D9AB63D06061852CC3607931184CFD7A904238CC8B894F22C2269A682360E3C844894B830611A3E896CE69B63B96365E4B32CC215A5645AB721A74230563C475571E5CA17BF116280196E0AB1ADF4CC7F4909859177190D17E6A7390DB291CDAAC33A0A08F6FD11608DC404A43AF7E467AD3E77F6004192A758409C459F4CBCCD5AA4DB3D47891416EDC659F8628B20A656CEEA904893B84EE9B6D44B4B1E3D2A5DAE49D7A38882D6C01A07720565A878E3A3B715B27DE84B14721885D5A8D06CBC93268A35A8A3360BB10530130A1C8664117A83BD550D8CA2846250D0FF722931ACF79824A537915D21C9892A786681B44997CBD17F7C02498AB3EE4C6204C79E848BBFDC06C0112C3BD622B9FC4964F64B539630F943583E962B765816E111C850AFA6073C1BB1EFA6E450B9B518C9EB98144F5912F75B30238297F0A934116D63BC9007DE2F21D192A4A515504BD7C73E58462DB50BC1A373922EA1AD5CC4C5FCC64030B4863B047D37A0CBC2AC48484CA642615F287610DDB718BC562559992206191D1507B3BB94B6714BB062468C13921692ABD9EB93A8D3CA09C8A52FCFA36D0F9711E545028BC737DB2B3ACF9BC850A48491488DE346DED5C47DFC089EF074F6C38B3BCA7C42746AED58660C5C984D8E59F3D28276D45263D667191A25BF35A3469B55E36A449DDE713F648A18469A8E417BFDE0784E2CA68589A50FFD38EA5744116F951BF79B90B1380717C6E4C8940627C5BD4F395EF0B38BEA7C556A14C03F2A3C5A2A039BA8CB0BBAB2C0C2B4C88A5D76849F657C361212E18CC96E39788B1D086ED61440F2B10266225C48A36E0380D783677E64564BF39BF8FC8AF7C581FCE252FFF73858533B24F2610A78A2D83D7625FC1687D0CB87088B8F53B475A7A2E8E1720E6E97C1471712AD948C8A2537DDC06BEA22BC6CA8575A26D4315180FB17ABC797DD135545AE953925795FD12167796A54A9793DE8621B5E6851180CF681583AB7A33C4584B709836D79A1913E6197FC4A66876C638B12C77BC9C8BB95FE2A251D61BC260016287DBCCE42C0FB6F30B1E41440957AF90318A48F61AF588C8A86C4E6DF854808C88F2343EB075BD4BF89F7CC081F76B2FC1D410475C27244C8377852B60204636AA7619FB3D7196CCD0B211AAE40E5BE5AAF9651F99A040D32D1335AC61B1685014B4839D3BFEE2660706F970C2B01D9EDB8083BFB5CAC9AFC6E0A6D63D33F40E61F6A8055B63E799623A39A9EE7619E4F0250147ADC188649A45EB6D82DE5EACD5643CDC52E6DF8DF2F8EB\"\n        },\n        {\n          \"tcId\": 16,\n          \"deferred\": false,\n          \"z\": \"80CE5D65D1795C90B637C10360B04A4C21A70851F0A59D4D753F54CC00103FF4\",\n          \"d\": \"EF0F6EDB707059073378E3419C8D9031D0732CFA931190EBD07FE291B1A3EBD3\",\n          \"ek\": \"8ED594BFE63B64E0C4C5A27BAF4C198E11AB91629C1C77788A05977042B4C452CDB7D66F0991BF05480763591226BCB1830B8527E5BEACC15A191ACCA3947DA6C015DA1B1800FB7B98E94BC3A61733669C6ACB8E6EDA749F879F565077FB637FC8A6892165AF17A77DB17856FA9266EE017105449C514CC6780B41A867757AD551A33341AA145085687E2C9C0CB51224A3C164BE9A36024AA8CFE97BD6C01F8F06A51F36964F1C6367D6A4C22C6B759756ADB3C558E49FE45395D7E8BF73C24305632A6573A8BB574EB6072441D66EDCE17A5B080D6F97C3A7030583837395E7378901A80B1B2CCC2B89E4C9AF74EAABD5604F9957C08D18635D68AD566A56D0D773CD71ACCEE3C697C851544B96FA4CB18A42183E2C0071A46F01F47E1455133DA73A0599AD2EEA39EBF9216DDA7012533CC4610C035C9D00E2962FA6ADFC065BDFA09AFD7A981CA0C3A010CE7040CB6A838B9C65A2E1580FFDD36911F23D69D25F211C0811215C96DA9E0B1C9C2DF24CF583633ADA8D786B6591D4A306D874A6E8A8DBD09443578D128648A9163E8734057D4449E1917B73ACCF7F87CD85DC0CA473756D42A174A8A6CF1A8A5A452F0FE17034E216A03224A423ACA52BB2EC7A6BBCF06E5616A4154683FF6CA625AA78ADD4CEEAF8BB48D37D36B79BBDC3650DA67982F6C4B2DCC00973525B4A3F06CB56B2A35F68974DCF508E6D093BBBA7780B59AAADDABFEF569A480903B648B41D4929C8A5110D6625DBB5B206CA384FBA479898B840D50E45D5CAA30C3EF143593C5BB75224629DD906AA754394F8549E4C7E5EAC45F2C216225C487CF247BAD2C5397B4A42D9B128054A80D24D4D9068373462547799EF612CF61906E224C03693B4CA28983F302B0E935EE11882BD8A8596533E004C8F59380412299A97BA5727A02CE3E410DA564BBD713BE085BDA1192EB27314136754513008A99CC3A2949FBD664C92920807860C1AB1814E1428642151CFAB3C8256832541A957F5020F89230C5C162A03437A917A7B96791F4B1401E79CB55375F585C9C4D166D9760CC36B802E6B5079B0836CEA1078B3BD7E5D33AB48E02C5BE40EFF344F369B920AC3F396FE7F940C6218CBE3B81D3904\",\n          \"dk\": \"207B287A81A1C2223C1BB9B373614F93B575DCA90BE6567EEF7B535C1684A6D0CA4B36420E989A830C9CDA69B8A7655D28F18325741A8CD874A88686A802C26DD6B450966C1EB02D271B89FB528988D4C6ECFA95E7BACF0BF11AF655AA48717CD69497D611CDD2F3437E4C96342305F300953EB9881B4678E4317C400228D945A6924042FD5A86A566A49A577E90955EB6B068ED0568CD535DB1B957597B4B90D864BDB05EDE024EF7D4AF63D282C3D876A7F2BE1DCC0848353818F91ACB935C2CF76E652CB720DBACA86522FB4B0E276A5BE79740F2706DF0E0C64BA18C42E9A7A71092B880328F6866EC599DCF56A4254578CF219D87DBC2AD78186FC3865366415B7932E4B28C70C30D7256BEEA676300700336A814D122ADE949BF6872AE7762CCAD2C7701557139CB5D403215A52327048789CC86A5E689B609260425E7291AA15B7C30460B1B04C1E70647741739F03575E348DDF4996DB587DEFB61940C61A6C1CE6A118C46A701D3F345C1747A666283871B2BB00661AD995B26112DC1B48EDC25A1899019213A3555AB4FE17AACDBE2B4E404A117B7B4A779134090342250613CE2B632A8083D1B56B0501D026C309D25A711AACCD82806DB867A6CDC85F03BB667B2354AC4820ED4A823A0BB93C3921706259F55336D7443FA26BCA90C94D94514C38B17D0DB2909CA4C1E3568CEA34266DB9D5C6CBEC1E104B60BCB3C77A6E3B45FAD814F3773699DD86BAAEA5A6CF6CBF856A5EF6B7F21A74B61A49956935895AA7368640266291088D7BC64E70F7462343F2B943C75B68069435E53684CB74BB3EC3ABB0C1CF17621BD811D95F488C4E035B0A2C2B00C7C6F4757F5711DBD285ABF018E28E87FC95212E7A8CF91BB43C95A69C86B42ADE439189242A4F43252B0950BC43FEFE39BA66334FA1A0114E822A97B6E81356B5C7A7393F9206A38B308CBCC0EC49F079979E0739196020FD6FB07D222B8F9D7C36E5241E973BC37B58C180473E122A4A4554391A3BFAA8658A74940BB856878D39598A1C7DC485971801226F64655E310DF8C689C492A3FFC509840143BB2681C92028ED594BFE63B64E0C4C5A27BAF4C198E11AB91629C1C77788A05977042B4C452CDB7D66F0991BF05480763591226BCB1830B8527E5BEACC15A191ACCA3947DA6C015DA1B1800FB7B98E94BC3A61733669C6ACB8E6EDA749F879F565077FB637FC8A6892165AF17A77DB17856FA9266EE017105449C514CC6780B41A867757AD551A33341AA145085687E2C9C0CB51224A3C164BE9A36024AA8CFE97BD6C01F8F06A51F36964F1C6367D6A4C22C6B759756ADB3C558E49FE45395D7E8BF73C24305632A6573A8BB574EB6072441D66EDCE17A5B080D6F97C3A7030583837395E7378901A80B1B2CCC2B89E4C9AF74EAABD5604F9957C08D18635D68AD566A56D0D773CD71ACCEE3C697C851544B96FA4CB18A42183E2C0071A46F01F47E1455133DA73A0599AD2EEA39EBF9216DDA7012533CC4610C035C9D00E2962FA6ADFC065BDFA09AFD7A981CA0C3A010CE7040CB6A838B9C65A2E1580FFDD36911F23D69D25F211C0811215C96DA9E0B1C9C2DF24CF583633ADA8D786B6591D4A306D874A6E8A8DBD09443578D128648A9163E8734057D4449E1917B73ACCF7F87CD85DC0CA473756D42A174A8A6CF1A8A5A452F0FE17034E216A03224A423ACA52BB2EC7A6BBCF06E5616A4154683FF6CA625AA78ADD4CEEAF8BB48D37D36B79BBDC3650DA67982F6C4B2DCC00973525B4A3F06CB56B2A35F68974DCF508E6D093BBBA7780B59AAADDABFEF569A480903B648B41D4929C8A5110D6625DBB5B206CA384FBA479898B840D50E45D5CAA30C3EF143593C5BB75224629DD906AA754394F8549E4C7E5EAC45F2C216225C487CF247BAD2C5397B4A42D9B128054A80D24D4D9068373462547799EF612CF61906E224C03693B4CA28983F302B0E935EE11882BD8A8596533E004C8F59380412299A97BA5727A02CE3E410DA564BBD713BE085BDA1192EB27314136754513008A99CC3A2949FBD664C92920807860C1AB1814E1428642151CFAB3C8256832541A957F5020F89230C5C162A03437A917A7B96791F4B1401E79CB55375F585C9C4D166D9760CC36B802E6B5079B0836CEA1078B3BD7E5D33AB48E02C5BE40EFF344F369B920AC3F396FE7F940C6218CBE3B81D3904BFFB491B529857FB52940ADB7920E6785BF951468B8578AF5D830FC94BA1C12F80CE5D65D1795C90B637C10360B04A4C21A70851F0A59D4D753F54CC00103FF4\"\n        },\n        {\n          \"tcId\": 17,\n          \"deferred\": false,\n          \"z\": \"B923CFEEC804B8C6A9E36B77B38A2886C45B1C731A33528ED2CB5A1F65E792F6\",\n          \"d\": \"BEE40356679E3EAE8B0C3FA07C1BFDC8835CEC26CA194D5EFC4301481C256C0E\",\n          \"ek\": \"28669B7A46A351A14CDD8C5676451577D43AAF842AEF96BDD6AB11A948832082CE85A21CBA773EB01508EE3B230F8A5D9FC6617E959FAB9171AB232BBB560D84579992E00E9C338E77043B89B1804C4021DE8124E61C6D6D72BDE0DA854C19CC69D88FFB43B1F983BAC8A84B65480175B8C74B6BCCA470482FD813FA7773570B52B0221B73F48635821427D3B6CEC089CC0C1ECB398BF4956DB4666555F90385A500F24868545848C96C5553FC7387671957745582D662FBBCA46AC9C3392B929BF634DF35638C8255B8492BDBD98FB43B207604919FE855CB48911BB3186690600D80B417FB54E7641560BA5800D46784F97C4961856EA25AB3F2A0608019B0234F41571618851D2D6831B28B37AE777AF3C79A8CD9509BB9348A30A37CD04083660047A0AF514B328B4B3835EC0C2E66C03CFBB05D7418AE4B5578A58BC051A616180F899B371DFC1C441362B6F8359A7B83754AA2B46884072977AE1651137875D4440F04F173405A4E60BA58BD950EA49255ADF53D16B2B97E0C0943FC2ECAF4CF01357382708BE946448AF8721B10AE95A4BFF876B4D6C4571187CA8DF9940AEA45E7C8C63E4081C2096BCEF1679AFCC79EA272C3BC35E9DA7171712A0C3BC679F5379B3512DBA54583A6978A2050AC679F60C295099C82564489E3ACC816BB2E5D775D9D3522AEBC21856AB947F1C497CB20C1CB3F91446AF20646920A34A72394442504A7C817887C177E75459A902323B87A3186CD5001C16CEB73B9BA6A5EF9AFFEA729423C261F121F8653384740A50551CD90292B0671A15599C9164A5054AA9382F55C3FA5A376C868C5498D29E00F0CD9ADADC07E62431D11C21B22374474D5BB513C20C020400B696132EAC94CCCB684937B6CDC507B9B06C3B7C21AC37C5F70C8EB024565D896854C9507082C96A8B7D260C7496AC7546C3EDDA17620DA177F931BD91692890570716455497C8114D8B1FC78506E137813F1067869AB940AB5BF452EA0C02F450972570C4F96F144CA368D77614BF42494D84B3EE6A9AA8A74CE8D0988D038CD971A8C5924BB25923FDE557CD0F336DBA90CB12C38A604FB1DA89F328816D27707C8971D43A9C34B11B683DA50A2B46D270D\",\n          \"dk\": \"FF6013CDBC6DFCF608A3178E7F251FF65867B72B2B4A87A0C0B5BA5B908B0CD878B76BA6F5DBCA047C40FA96479EE2BB2B97CCF6C894AA68505DA331F932BF05CB5F41D394F7875733261E68A02DBFD360AD29589AA06E2440165B16A46913AD6675B68D8B9A27661B709B687969BE0E53CBD567A84CA5A8DD84A2D522C6643A9CA5321987FA7EEB1C0C3742BBF43060DA234AC9F21881644E4E18C7D49A9501930ACAAC17CF536485805E928169584B8C013C30C924A5C81508A9928D54E1325A298115C76F9F95C154DA38B1C857E8676F7A18A35EF2690F743C4F49AEAB6A5E0D67AA18B908F4E0477C7399139490BE077D3B3946BB7C086B3C261323844DBACE09F008F0D3AB5996498E164466421294AC3852C3066FB7191EC33415868CE554C73E0316B1FA0C414405D4FC576BBB6D45FBC2A6BA1882C8575CAC70BD33472AA62BAAD07586D7631D73CBC9AA8875A8927092A0498483A48575E8538760D253A73A8B4905253285A07239B4CF6A091699274BAA5D692CB2614661104AA94AA38228583AE3326548151F39821AFA55752DAC4B25DC0530115027B3CAEAE44AC824ABEEF301BBBBB79113651C686067131A6D41031A34B4AAEB9B868C6CA1122B63363EF7C373064B62E894A654651F348C7055EC758C90642B793622B050D48BB5C241793AA701388C70B3AAA248B270B1E996DB4024D8FB56810C985038C4CE6647B497A233915924E3A9E47A382FE277AE063C02D0371EE964E16B817A6919B7DBAE053117E2012ED3D54FA9A216B0CC3812168A56B34FDFE107E2157E4DD55460C8753293CD614CC9EFA12CA0CCC712CB97703C60D6B763BF23C6CD71539023B1D2958098524A115B2B201B4A1CFC7B0CE3A667C9814AD7C90D24A2295076605687D7CCC8926A1F3D209F02D2486A9764F8D26CA7BBCBA8E53714B52AB87148E7E69A99348514192C8C360718438E2A70ADB107C7D005B1F9537D50D930574A5ED684AE5DB38E6876255D188AF85CCB4ED946FEA92B2E29C5F93083AEF30C6D37B39FD45120BC0FB39711A52A3C108105AF039530505D48069580650528669B7A46A351A14CDD8C5676451577D43AAF842AEF96BDD6AB11A948832082CE85A21CBA773EB01508EE3B230F8A5D9FC6617E959FAB9171AB232BBB560D84579992E00E9C338E77043B89B1804C4021DE8124E61C6D6D72BDE0DA854C19CC69D88FFB43B1F983BAC8A84B65480175B8C74B6BCCA470482FD813FA7773570B52B0221B73F48635821427D3B6CEC089CC0C1ECB398BF4956DB4666555F90385A500F24868545848C96C5553FC7387671957745582D662FBBCA46AC9C3392B929BF634DF35638C8255B8492BDBD98FB43B207604919FE855CB48911BB3186690600D80B417FB54E7641560BA5800D46784F97C4961856EA25AB3F2A0608019B0234F41571618851D2D6831B28B37AE777AF3C79A8CD9509BB9348A30A37CD04083660047A0AF514B328B4B3835EC0C2E66C03CFBB05D7418AE4B5578A58BC051A616180F899B371DFC1C441362B6F8359A7B83754AA2B46884072977AE1651137875D4440F04F173405A4E60BA58BD950EA49255ADF53D16B2B97E0C0943FC2ECAF4CF01357382708BE946448AF8721B10AE95A4BFF876B4D6C4571187CA8DF9940AEA45E7C8C63E4081C2096BCEF1679AFCC79EA272C3BC35E9DA7171712A0C3BC679F5379B3512DBA54583A6978A2050AC679F60C295099C82564489E3ACC816BB2E5D775D9D3522AEBC21856AB947F1C497CB20C1CB3F91446AF20646920A34A72394442504A7C817887C177E75459A902323B87A3186CD5001C16CEB73B9BA6A5EF9AFFEA729423C261F121F8653384740A50551CD90292B0671A15599C9164A5054AA9382F55C3FA5A376C868C5498D29E00F0CD9ADADC07E62431D11C21B22374474D5BB513C20C020400B696132EAC94CCCB684937B6CDC507B9B06C3B7C21AC37C5F70C8EB024565D896854C9507082C96A8B7D260C7496AC7546C3EDDA17620DA177F931BD91692890570716455497C8114D8B1FC78506E137813F1067869AB940AB5BF452EA0C02F450972570C4F96F144CA368D77614BF42494D84B3EE6A9AA8A74CE8D0988D038CD971A8C5924BB25923FDE557CD0F336DBA90CB12C38A604FB1DA89F328816D27707C8971D43A9C34B11B683DA50A2B46D270D9F2CB0E23865248EA1B9E200008F61F1D55C1D4C1F30686982C673E44312AE6CB923CFEEC804B8C6A9E36B77B38A2886C45B1C731A33528ED2CB5A1F65E792F6\"\n        },\n        {\n          \"tcId\": 18,\n          \"deferred\": false,\n          \"z\": \"1F4863F16E38DFD2C42A9322FA1ACB941DF3BDFA000A202AC621936FCC5FE33A\",\n          \"d\": \"C6D5B35B90FA9AB9A7B438B57942D653CAE67B314C7FD152013B4E90BEF8201B\",\n          \"ek\": \"77531B993B3BFDA74DAA9964E4D8781244493F3133DED822179B5E12C252AA5655B7748D3A187F3B017E6E9C7B82A1A494CCA1AB590DFCECC946087293BC42DC7A48584808FF62A0E1CB46B04122144004507610FBE7A359E399B44A08E4452FEFD0BC6C906078210B84D981E936CD54F1BAE79A57B7F64E7B7C69585C783E9C4A1CC30F3F6302DB928C1AF4CC0E0ACC4E627F78D41329303265FB0FA565CF83D7BEDD6833873A9EF6A32F33575884C1639A054998DA1B1D93C6A25379A367C9A14C1F7F1C75258A2ECD071809953E9293BF8ADB2E5BB109A50B6C290211BE035F3B6B481A341CF9EA372A1626FD0B193BD42A7B622D8253BE1123B37B28B6AFC7A1937AAB444CAC44740FA112BB7C960BCFC0818689C7E2607080D24D53DB80B2811EA5E74287090604AAB587F392DA186F693B06F59B45256128C33A942E05779CC570143196E85A45032984C5A834FFA23F0C114BA96B0B0392959BD4CAA859B12377A905330B77C2BAFB4A754924157B6367910C91C3AA41EEA76EA9ECCD4B9667FC4839A8EB2076D935E86694B8D9A6C7A1558F70391067A0B26BB368AB571621957F097B3CD1870D02A5DD5A646774391CC48BFBC012C315B1F06A6474C279599490C76049028B7EE886C55D979C24FB03A28C8756ABBB9D9B7E3F281AB8107EB22104992240A9E92089361E65643CF270C222971D3FB385CC5594C41B35BD2A0FADB51923143762944219459A56726ACEE42C91D03DA6C49D1B46C5EAB710BA68540981C65B2B7638273C4341058E54CCDC8130B155CBA88C2A16A9B2DDC4C92CAC9AC9267151AC39379C0F2078958C33563E360B8118118B185F906CB438AC052D625D04A17DCFA7A5CE4200509784D679081E173ECC2B4278618363637555464876D47E3494CA408BA45507A492039ECE577A17725D6472ABAE1562F3721195D193C962B01CACBD80A85DCFDC221698B54B52874DEB8450A435DCF1204A9CB329E21374268046AB00CB4416BFE207F018390E37CDF8797DB1238CFDC2C0B4F495B015CEDB138C36D415DF9672AB506095DC6862D49D5541CD3D93748955E792F5729A3191AE5DCAC826DE5B8ACB573648D95D36C5BF399CB4D9C4F8\",\n          \"dk\": \"9904CDEB001524F59BA84901022C3B4D81C1A0BA1724B373F4A6869D0574199787069A1FEDF8AEAFD82AFB823F5F718490E1359F495390393A12B15257C2C9E56BC52691228E00C0311C9577FACAAC34B65BD5428CE226779C8208DB6B25F5BFF40CCC1DA60A4C6AAE8EE553EB64B235918C879B5690E4B421375A1A1350A960C643DB4B8AE405E58974878B9F6F87CBAB57B4A3B8B1BFDA9823797C239740588A854D9AAB4283400E8B93168A1D680C05B07231B8A455C39893C260B0182B66E13ACAAA20ADFBFC11CCE0CBBE29C1C7B79B6D78A931878C4604174BBBC28C0A54F8C50459F7A93CE97D393424A4B8C9728871E9741AB54129E5919742BC4CD95B1775499DEF0753839622677739E87A8A8C651062F7408B389EC818C15D300A7B95AEFBEABFF078BA0E27A842DC89FDDC5378A5CADE512B3CFC65D8472ACD311E8BAB4979C15A17F33125327C9C73AEEA3642AC225958778618D522472AA89853142D35AC93264CC423A1EC8CB460A398746CC9C1615326B5819B0697D1452BFE3A86F21B7B3D11B1F6284806B5A09FF362CC5B50E0A45EB84B084EE024B3A654AA5BB7EC34604BD6627FDCA48EFCB410B9769FB6BB3A94835B2A4850128CAB534E2B3A5A5B4B367DAC9E3BE913AA12C887589150FA64EC0C1293F8410499220DF0ACA7A52C2C66CAFE9567C3B0B32F593E7ADBAE1BE64197110E9D8B77864158AAF77AD1F8232212758A5C7BA401B32C258389347C37B0B3D8B78C30436D54B800FD88772E309621955C49B77A5F9452B4B74876DAC662F76B63760D52D20692647BD1707045A09DAE8324D9688CA56B034F0BB4E378288C328ACD742169B3704C02BAECB68AF8309DC2D30BC836A856635CA4701BFB74C5662C69B11B6BE0CC984F12493540CE50DB83D3D764A5E63A13C2AD3CF5C67E8638CEEC9193470C6AA069240A53DF6C68C439B3EED8C5F4DA7076306E5223B9104662DFB35276B4823531C848747AF50081BD56A6C4B154C39B2E0CD3131B4742D241A385D938EB2C673028636FD893BCF59519C2A87D4BC42862CBFA0370D8D44CF3E032178AA077531B993B3BFDA74DAA9964E4D8781244493F3133DED822179B5E12C252AA5655B7748D3A187F3B017E6E9C7B82A1A494CCA1AB590DFCECC946087293BC42DC7A48584808FF62A0E1CB46B04122144004507610FBE7A359E399B44A08E4452FEFD0BC6C906078210B84D981E936CD54F1BAE79A57B7F64E7B7C69585C783E9C4A1CC30F3F6302DB928C1AF4CC0E0ACC4E627F78D41329303265FB0FA565CF83D7BEDD6833873A9EF6A32F33575884C1639A054998DA1B1D93C6A25379A367C9A14C1F7F1C75258A2ECD071809953E9293BF8ADB2E5BB109A50B6C290211BE035F3B6B481A341CF9EA372A1626FD0B193BD42A7B622D8253BE1123B37B28B6AFC7A1937AAB444CAC44740FA112BB7C960BCFC0818689C7E2607080D24D53DB80B2811EA5E74287090604AAB587F392DA186F693B06F59B45256128C33A942E05779CC570143196E85A45032984C5A834FFA23F0C114BA96B0B0392959BD4CAA859B12377A905330B77C2BAFB4A754924157B6367910C91C3AA41EEA76EA9ECCD4B9667FC4839A8EB2076D935E86694B8D9A6C7A1558F70391067A0B26BB368AB571621957F097B3CD1870D02A5DD5A646774391CC48BFBC012C315B1F06A6474C279599490C76049028B7EE886C55D979C24FB03A28C8756ABBB9D9B7E3F281AB8107EB22104992240A9E92089361E65643CF270C222971D3FB385CC5594C41B35BD2A0FADB51923143762944219459A56726ACEE42C91D03DA6C49D1B46C5EAB710BA68540981C65B2B7638273C4341058E54CCDC8130B155CBA88C2A16A9B2DDC4C92CAC9AC9267151AC39379C0F2078958C33563E360B8118118B185F906CB438AC052D625D04A17DCFA7A5CE4200509784D679081E173ECC2B4278618363637555464876D47E3494CA408BA45507A492039ECE577A17725D6472ABAE1562F3721195D193C962B01CACBD80A85DCFDC221698B54B52874DEB8450A435DCF1204A9CB329E21374268046AB00CB4416BFE207F018390E37CDF8797DB1238CFDC2C0B4F495B015CEDB138C36D415DF9672AB506095DC6862D49D5541CD3D93748955E792F5729A3191AE5DCAC826DE5B8ACB573648D95D36C5BF399CB4D9C4F8956A0C263895B5FB51D08CE116A156237494A6C79B54BD23134DB26D3D3F0B9B1F4863F16E38DFD2C42A9322FA1ACB941DF3BDFA000A202AC621936FCC5FE33A\"\n        },\n        {\n          \"tcId\": 19,\n          \"deferred\": false,\n          \"z\": \"53F5EE39A553E831BE32EB490A6E1DE62FD4FE486EF58A4B99F6347759BB8905\",\n          \"d\": \"5C6051E18E28FC5719E3172B967D25BB1649D87743440F7715E860AA212A256C\",\n          \"ek\": \"DA73368E74CEA1A5B1B1AB47FBF38B33A47BA9F98F3D719F938082806CCB59503E1EA999C35272875B112BE8097414BD304C5C8AE127EDDAB111F6B5A8C5B8F8C39E8C41754BBA57683C2397B612A11747C508103C689ECB9394FA9B54F898678A3459FF6104B3350B8B548F064C47EE0702D6910213CA5A5909134D5B4176BC60D7533CD1BA294EB4751054AECFB73478EC38322A7D7DA829DF8A2F19C31EA9442E5AD30E00707747787F005B9CA9B9544FC61290262247B8AF26B490704B21676A138E11A797391ACFE7A6C9D0692ABA2FEB982262327749D55E34A6A4AFE3A061E3256383667035236422A38A371FA6D48120CB1C78E53607ABBECF319A46C0834D2A05B8E303CAF734A9370BD6855C7E676E637576D50B0DE8676D209305A11C5202288B9E80BD235CC543458FBB470702B4A2C4758EAA992320F08FDDE8B25AA4A08480BC401C534A6C4A79867A26B19297FBBB90739973A79E05998140587E26CB8C464A96A3DB1FC24C5ADCA02D076B352576B506C451CEA40E3EA4412DA2048B25093D80C39AC6300BD0B318A7BE644C5838A301ECEAAC5AD6364807255476AC75CB0C9ED033E5350900C0795584034DEA59D8844FB669B09EBC8AFA43C45135640316A5D86947B9CA445D432D02A624D0F46D396C5A09E3C43F0148ECA5A926147A200750FAFCC13D53CAFF188718883AA4B4BE97C72C1A6136CE266FF6451669BC78C862C883C86422366657097AAFD77F88693A9ED328CEC05D2E984D188A044E026C65E212B2307D838A549E623793865E2D13926F85128841BE2FBC666C6921212AC85D804D4655448A07CF7F41CB8A9623D4E48D72958004C1C28093AB060371642307F8F7859F1B08D0E45259021E2FEA5D4DD99E3CCB980DD51307776AFECCB7EAE1776FF09676F3697EB98DB283414D9872337565D0450FF3765DB8F5CD3DA30DEC806ACEC879ACF9995D789F03F182B8E2C314C92E6C36512FD223D6A99CA9657A04125CA5CCCDD0F967B1A767D37AB08F26759886C555E9713ECC22C5FB2663398592549D731C84693449AF0675767A72E9817E131209F3CC907B5337C46F460DA1941825DA2DEBE7A27ADDE90CF053AD520522866B24\",\n          \"dk\": \"6656048914A30A786B57E77371D5AC2F5327D5D26EC077B6D2D817FB1ACA449A34990B5DC22638BF6473560BB853212343203F60298E91C64454B87121147BF55742218327E0D84ED1C07FD7755AED894A3BF3393068B7E99009678BCBC6028B74F0BA5862B3AA5B704AA3A4EA658E40751F69B51171177620E2794A64864A2617B603BB6CF7068435CF38371F84977E9D507F298CBC6D717E4955B9FB5B307870BA8B09952DBB9724252A622A22F6C89956077099919BF2D7CE7D456AB3F70FAE9B68362593D7ACCA8C4955D072908C981DF377B58881A8F4046F342178812C99E2E86F9557A81CF632430333C5E961F0B167A1FC2FAA8B7648F342E1022FB8478661D7505503BD6D18275029C8588C8E675B80B6D722E707C8E9B28636B894AB8164CFFB3BDE769116078F83B97953DBA7C00378DBCB620E3C650AE72955E5434A2CAEE4B15DFBD937BBCC3D1D256ABB361B7E007C8C53429698BBCAF54B3283AE02461D17A2C41AFA21BA466B2247CBE01483A0C0743A32B854A9AC78175E6BE102A9C62CFAC5811FE527D0F433B8E708FA106DA30A5E0C9C0E63B6B3356557982B3B03B437CB412EAB525668A7723CBBAE801025D5AA7EF2D5A5AF5051B0B958FFE677DDC30E70D8507BB24D2600344D226CF602916AA175D4BAAF27D96696B4BEA828895AE8BABB449649798905575D71C60F3B9C0B73EB3AF92B98B90284421988F257510BC822DA501C6C8C54FE0117BE44AF4A12820AC9203C52466C76651580B962B2543BD0BA7EE73CE28B76411316FEC5859576CE5F83C8C3B159BD16CE58A92730F627E4B9770EC9BAA5D343FD630ADB9326DE8340BF5AC319DC43E5E03B50809876B6804582069258B9078267666416022382A1816DC6E670FAC16D18D861DAF022389762D915912EC7A7A3F0534F93B7D38CAD99670734883BD8D31E8B70BE90839963ECC83091086E689091CA6EF8477AB21960432593DD7694D0E898038BBBD603ACA19B8BB150B80960A63894A231ABB7826B1976FC331839586E3C816A1450742C719E674A2D1BC6B1B54D5F407D1E96849FB841703005DA73368E74CEA1A5B1B1AB47FBF38B33A47BA9F98F3D719F938082806CCB59503E1EA999C35272875B112BE8097414BD304C5C8AE127EDDAB111F6B5A8C5B8F8C39E8C41754BBA57683C2397B612A11747C508103C689ECB9394FA9B54F898678A3459FF6104B3350B8B548F064C47EE0702D6910213CA5A5909134D5B4176BC60D7533CD1BA294EB4751054AECFB73478EC38322A7D7DA829DF8A2F19C31EA9442E5AD30E00707747787F005B9CA9B9544FC61290262247B8AF26B490704B21676A138E11A797391ACFE7A6C9D0692ABA2FEB982262327749D55E34A6A4AFE3A061E3256383667035236422A38A371FA6D48120CB1C78E53607ABBECF319A46C0834D2A05B8E303CAF734A9370BD6855C7E676E637576D50B0DE8676D209305A11C5202288B9E80BD235CC543458FBB470702B4A2C4758EAA992320F08FDDE8B25AA4A08480BC401C534A6C4A79867A26B19297FBBB90739973A79E05998140587E26CB8C464A96A3DB1FC24C5ADCA02D076B352576B506C451CEA40E3EA4412DA2048B25093D80C39AC6300BD0B318A7BE644C5838A301ECEAAC5AD6364807255476AC75CB0C9ED033E5350900C0795584034DEA59D8844FB669B09EBC8AFA43C45135640316A5D86947B9CA445D432D02A624D0F46D396C5A09E3C43F0148ECA5A926147A200750FAFCC13D53CAFF188718883AA4B4BE97C72C1A6136CE266FF6451669BC78C862C883C86422366657097AAFD77F88693A9ED328CEC05D2E984D188A044E026C65E212B2307D838A549E623793865E2D13926F85128841BE2FBC666C6921212AC85D804D4655448A07CF7F41CB8A9623D4E48D72958004C1C28093AB060371642307F8F7859F1B08D0E45259021E2FEA5D4DD99E3CCB980DD51307776AFECCB7EAE1776FF09676F3697EB98DB283414D9872337565D0450FF3765DB8F5CD3DA30DEC806ACEC879ACF9995D789F03F182B8E2C314C92E6C36512FD223D6A99CA9657A04125CA5CCCDD0F967B1A767D37AB08F26759886C555E9713ECC22C5FB2663398592549D731C84693449AF0675767A72E9817E131209F3CC907B5337C46F460DA1941825DA2DEBE7A27ADDE90CF053AD520522866B24F8568A428B981C5C2DCD0C7E8B487824825DC3F9C0356ADF0CE075394DD1BDC553F5EE39A553E831BE32EB490A6E1DE62FD4FE486EF58A4B99F6347759BB8905\"\n        },\n        {\n          \"tcId\": 20,\n          \"deferred\": false,\n          \"z\": \"9C7C3E68F827936D8DC435942DC4925D180E6D5C911550089E1337D8BA77A06C\",\n          \"d\": \"CA351B0F454DE9DB364E1DAB8AEF6E49C2E69439941935B24C00BB9952E65BB3\",\n          \"ek\": \"F7F7C6DCFA28B52749357994F5C18DF3138166373F1D9C613410287E1BADEC6C23120889829C251831507BE05E137202A9336EF5C69D33823B69F91EC25369FE3C8FA8F2A244A1ACBAF5C851418515548920333F0B584F60C22063D66AAF545328F1CFA23BA676140D0E7BA6D6D94D4409AEBF5B209524A68645277111BAC6A2B09A13944D9471E9F704441215D6B93D63FA01E3BAA4BEF7C701E485A1A89CC8E8602C5259CF3A176EBB85487C3BEAB80B4E5B8AE1B54335BC5BD1E94D35090CDE556E2559A27F770053541090CB94BFE3CDECB1B99C0B9914460631061060927507096A16B37DB0091616BC00CF68461C3A47BB585E8997C6A46C09B07C98ECCB10E30C45D014A1B9C2559EB190651296E97C7DE0BBC475BC49AB13398CD98C91A06273654977AC3B70EA83BD5657F47381D9090909735EB899BE2DA5BC73B56232736AA326845FF5AE6C452523406901781E095244C8F689B0271D53327659D0A625D75914E34394149C955935ED88C1B41475671338451B61234549C74091C9CB14E90B02356BA21CD4C960522190CC68C71AA088FB2EA5D1A777075DB4210A289B208BE66F6BFB6DA19540B740A34E822232A4BBFD028BCC304976A4C2ACD924153A974FA345959AC74D13C6C287265FFA6F8639BB0E407C1380627D74BA3ADCC81176A85503213109493A2C745D5A65EAD31333537C51CA45E8A34A3D555C19649726C6675B645519645E6331A81EE47C6EA35B49283AD8E48FAD465A426A3352C0001E8BAE99607A26A71126355A0127C288266951E845CBC0942023950CC73677228059321C2BC9414A190EDB61021BE21A4CC26CE4E1303C430BBE607B996AB304519A4148BCBD72B0B23BB947A36DA2C983898A71A8D18F8CD571ED274EE76713C540118B0CB45CA6BC72C5C96FF1B231823ABBF112FD2683F12973DCCA995B007DEC83B600999535590AA98A61C711A2DF076E65803746E93885621557821F49E3497EFB4A0F8C34ADB34B42CC0F32B09D898903F946A569C6CEAE0630E13004BC75241343061D2BA6B1D7BF44A00712941C10E64183E5A007708F798456C695B15F75C3C6A27310393A3049089ED8EF92DDB01CE5F59229FF7CC3\",\n          \"dk\": \"B0A66733CA122538CA1C747B2DB0CD8E3A67D1F3B1F695BC40540BC5BBAB6CB1664697B5DA7BB181DBC1546A2F5F4C4958D8C73833372F0355BED21262146574E24BD42B3FDCD45B333750B0AA8A29E34FB9AA76BDD2B1CB6799539463026029EC43655B716351288C95D1B81DC572A8980CC1EB42914B0B7E957FDC8373533201317A54D2A497A4D85E3F695F579942750AC04A9A7D068C4429FBAE653AB8AED01227C9892D79CFE300191BD310BC704BAAF78F982BC09F40325D869C962AB618F25A18477B46A9A42657C00DEC939A5187C26342C552880DEAC83A5338D3B10EF4C71D54DB77A8EB01EA87A534ACA53C70B25C98988AC4828A0B49B3627F7B6156F665133AB01F42B47E298B983939857849409BB76C4B0856A364C86FF6B32AD81F28DB51740048853836ED972B92295E16FC51A15465FE592090874FEB183C28090465204490F561A388430AAC26C8B133EABA1FADD91C9832C43EDB9A9B8195490B4153D1CD3B34523CD98F570B33502C818C765188B40E43A8B26D120E4A67ABD430650F7978FD3551620A782615214085C28B44B4986C952212C0A7F681B355964FC0C4EA914E66FC8E4EA40750404D1441C543FCC3B938B20464229FC99AEF56B1123C11EEB40DFA68C6F473C3B955685875256CA0BFCFA39944443C3F1160F808682933053E7759F8F2494DE65D4E4C159AFB4F34C6862D26C41DF2AFF896116007785D58068E5086B2426EB5124FCD0A9C8339BFA01116F862581F5307DD0C8B361113D346629BC29CE3A21CBCEA14483010A878A9048C11E1E6A07E011E4BAC09870037E1FC4987F94513403FF9D9964639885C14851A1B5FF5C119F4522976AC4E7839B1BD607F113A48739670BF0CC7A37498F8A0CB74B8A9DFD298C9F916DF547F68C2A369966D99C623CF782DBFF542C885BB5D9B027FE3CD50B54656179332EB3D162B8CF59B604BE519A5ABB105C0012967CD794217A8E4A3C43B785255AAF80988DFE0979413C31EE13D55DBC43B316E85EB0C7470C5F2577BFD185774CB03CB39CDB408C243F26DA2B424A55C3D8975CB1F900EFA1A3FF7F7C6DCFA28B52749357994F5C18DF3138166373F1D9C613410287E1BADEC6C23120889829C251831507BE05E137202A9336EF5C69D33823B69F91EC25369FE3C8FA8F2A244A1ACBAF5C851418515548920333F0B584F60C22063D66AAF545328F1CFA23BA676140D0E7BA6D6D94D4409AEBF5B209524A68645277111BAC6A2B09A13944D9471E9F704441215D6B93D63FA01E3BAA4BEF7C701E485A1A89CC8E8602C5259CF3A176EBB85487C3BEAB80B4E5B8AE1B54335BC5BD1E94D35090CDE556E2559A27F770053541090CB94BFE3CDECB1B99C0B9914460631061060927507096A16B37DB0091616BC00CF68461C3A47BB585E8997C6A46C09B07C98ECCB10E30C45D014A1B9C2559EB190651296E97C7DE0BBC475BC49AB13398CD98C91A06273654977AC3B70EA83BD5657F47381D9090909735EB899BE2DA5BC73B56232736AA326845FF5AE6C452523406901781E095244C8F689B0271D53327659D0A625D75914E34394149C955935ED88C1B41475671338451B61234549C74091C9CB14E90B02356BA21CD4C960522190CC68C71AA088FB2EA5D1A777075DB4210A289B208BE66F6BFB6DA19540B740A34E822232A4BBFD028BCC304976A4C2ACD924153A974FA345959AC74D13C6C287265FFA6F8639BB0E407C1380627D74BA3ADCC81176A85503213109493A2C745D5A65EAD31333537C51CA45E8A34A3D555C19649726C6675B645519645E6331A81EE47C6EA35B49283AD8E48FAD465A426A3352C0001E8BAE99607A26A71126355A0127C288266951E845CBC0942023950CC73677228059321C2BC9414A190EDB61021BE21A4CC26CE4E1303C430BBE607B996AB304519A4148BCBD72B0B23BB947A36DA2C983898A71A8D18F8CD571ED274EE76713C540118B0CB45CA6BC72C5C96FF1B231823ABBF112FD2683F12973DCCA995B007DEC83B600999535590AA98A61C711A2DF076E65803746E93885621557821F49E3497EFB4A0F8C34ADB34B42CC0F32B09D898903F946A569C6CEAE0630E13004BC75241343061D2BA6B1D7BF44A00712941C10E64183E5A007708F798456C695B15F75C3C6A27310393A3049089ED8EF92DDB01CE5F59229FF7CC3BDC6A66F789A31E64AFEBCBB1DD2F94747FC7559309D17920DCBBC3C38C4BB059C7C3E68F827936D8DC435942DC4925D180E6D5C911550089E1337D8BA77A06C\"\n        },\n        {\n          \"tcId\": 21,\n          \"deferred\": false,\n          \"z\": \"97A4C9A65A82BAEC15FF165E10490976EBB19FAFBA8F9E8E0DFFBDB4D5E1ACE5\",\n          \"d\": \"C467A43BF9E9CCADCE4581B53F8CA0B605583775AFCD0EBBB587907B3A813D94\",\n          \"ek\": \"E5169D5BCB1CFED42BBC044B89208FBE0399AAF9A3552B7B0A579A43BCACF0D1610F6057CF938A157A9DF8B797D402461559CEDDA6A15D754343DB49C4D095A4E763FD22833FAB8B909A2129E628B9FA656482AFE9659B52AB9C74EB885A667D42946B35D163136672F30404FB881FAE153B13589299C515185B25D878ADB1D903743B4ED2B96838C29FF3C5183CEB6CDA510E6EF85037163DB1D658831B97921273CA84A924947E47F88BD2E3ACFDB5B7EA634C70794D9CACABC67C08E4379DBEB84575542A108671FFD6289FC97B97F073C98A845E10BF63C1990347A6C165317A51CEA3231632A967D2A97619414701BCC6F839C51FB1885B7993A891B31F7A79B19176FF215EAD642BD9015DE7DA03584A867C83C0ED7B5EBB16CD6FB671B1F48BF426BCCC24529F91819931921016A627FC2960C8104913A6D4372964013B0951593E09B8256984E8999A28DA134AF4556F5A5A39A63FBE950828474FAEA86F33629DDA45C42CBB90A885AAD3A4C006E66741362A3FC9CFD92B94D6979EA5623554986F36F4C9898681B345C81823852FABC76234023587B10462008562262BB37981EAC047D458E8D14789C18CB740BE85B57BA613B884104DF64159B277C469F72EEF73095F34C4341374992BCE5D02169CCA1FEE5456F44BBB52C4C12610AB2E3A48DBC9A4FAE18B5CB6A1E2177191338AF490A18F107C4A9704DCE3A40F997233347A7ED8116BEC2469BBAEE4EC141AF80F45E67FDE896838761BCA4B7213D260BC362EB3924F0964A59A83827BDC69A1D816DB49A077150486245E07C3B7261B1DFA7492B778073BCA7BA8B690C4F47AC8398A3B10A18779935614282D64428C4B50FC879109B22F44E079AEFC50741677CB4566AE80114DE8033AEC9B04202EB45176E7BB77A9A1220845C9C2E5746037C422A96D23EB07E4B4996B2B12470C62EAE1C972A22A259B7B4098197C69571B9066DA5712BAC75AA1D69C4B289E2301927CC730E9E15F3E01928DE1AAE25B80D6A80DB4D834E61AA6BDCC8615C0638865C573B5B06EC9C16DC014BF53239F91BBE0D5C25D09A766405C35D11B3732B2D2AD39A22C1FCDBADDC3A81EDC8E5EF80FE66B2A69753C82FD63\",\n          \"dk\": \"4A7429DD79A88B892D16D35201558135962685EA2F76A67DAD213DBB846FA229764E354745381DF8B64217A9BAB2CBA9E143C22F901A3C0089081C6B7D4B91D7115C2A740D57745B782122A4D015BAB56E41FA2642DA060CE20078E57FC2706A676BB2E1381D639951F0866A401127A4312BA3F89CB8E558AFF6B86899617D57606BACA6351185F1159D9F4A469A0A8B36B50E13B164BCBC13F473B31A7118DC6A5C79702405E0ADEB466A359B8F47EB807ECCC0E0E28E55C28266219FFC3A530220B97B080C91302B77819A017B2689F7A5F9367EA8422412FA8AADB20B4E34006236454C9B6BA1B1C160BB46ABFA8CB0AC530EBC3D63909FD2CABCB009C67759AF90201C3808B35C917E82847232074C67D37D55454EACE4B4CEC23300BD58836786A5C02014E537848A012EA7C0E88071D305C539B7B4EBBB5DE188B566119956E264C8131DD318C80C56C000848C3EA22C0B358AC0B08B7B5A864601AE8C72C9A4113188251243E951FD9542061653502475D1396696C4C9E94105C3806E26128D24ECB03BDABEB2F49DFCF0795D4918E4394374DB596DD47B23BA543207C2C744C5D83C1C4E78C0C2B0072CF4A9870C4A6F1CB9B41867F6C1278D477928307752F0975FF30106982EDB719F1BB97B70C8828EF4C1C475B59BD61C7CFB95D95ACB251A0984E37C8ABC92A8B564DEA4A5C2301E60D10227AA7161B52B8BF4370CFB7342A756F343A020B84052626D3E9B2B11CB819D0358D2B99041320209248B9049B6D96302F871A0F69493721935F2753786CA1673A907618C83C9D3B9D2B6C702EB90C8A8BCCDA54EB782AE17513E38840BA30C6920F8B9C2E32653D7B5B7F6B50A08BC86CA5A1C756610A595F1D8034B26B29ED676AF2C26788A3CC3B271A9F30B2BB473D0E47C941711ED6355823A82FD957D1D709EA7152E5B5BA97F78CC2A997405BC46AE8CB24223B0B39129D8B7CE677C82BFA332B66B26C9E61DC13656EDF538C65C88AB15A1F019044BD0A5DD32090D0C7E59E269A4213EDDC469FFD18F1532A7AEAB051DF7727C14BC721B2F39326C43A266BC531850FBA0E5169D5BCB1CFED42BBC044B89208FBE0399AAF9A3552B7B0A579A43BCACF0D1610F6057CF938A157A9DF8B797D402461559CEDDA6A15D754343DB49C4D095A4E763FD22833FAB8B909A2129E628B9FA656482AFE9659B52AB9C74EB885A667D42946B35D163136672F30404FB881FAE153B13589299C515185B25D878ADB1D903743B4ED2B96838C29FF3C5183CEB6CDA510E6EF85037163DB1D658831B97921273CA84A924947E47F88BD2E3ACFDB5B7EA634C70794D9CACABC67C08E4379DBEB84575542A108671FFD6289FC97B97F073C98A845E10BF63C1990347A6C165317A51CEA3231632A967D2A97619414701BCC6F839C51FB1885B7993A891B31F7A79B19176FF215EAD642BD9015DE7DA03584A867C83C0ED7B5EBB16CD6FB671B1F48BF426BCCC24529F91819931921016A627FC2960C8104913A6D4372964013B0951593E09B8256984E8999A28DA134AF4556F5A5A39A63FBE950828474FAEA86F33629DDA45C42CBB90A885AAD3A4C006E66741362A3FC9CFD92B94D6979EA5623554986F36F4C9898681B345C81823852FABC76234023587B10462008562262BB37981EAC047D458E8D14789C18CB740BE85B57BA613B884104DF64159B277C469F72EEF73095F34C4341374992BCE5D02169CCA1FEE5456F44BBB52C4C12610AB2E3A48DBC9A4FAE18B5CB6A1E2177191338AF490A18F107C4A9704DCE3A40F997233347A7ED8116BEC2469BBAEE4EC141AF80F45E67FDE896838761BCA4B7213D260BC362EB3924F0964A59A83827BDC69A1D816DB49A077150486245E07C3B7261B1DFA7492B778073BCA7BA8B690C4F47AC8398A3B10A18779935614282D64428C4B50FC879109B22F44E079AEFC50741677CB4566AE80114DE8033AEC9B04202EB45176E7BB77A9A1220845C9C2E5746037C422A96D23EB07E4B4996B2B12470C62EAE1C972A22A259B7B4098197C69571B9066DA5712BAC75AA1D69C4B289E2301927CC730E9E15F3E01928DE1AAE25B80D6A80DB4D834E61AA6BDCC8615C0638865C573B5B06EC9C16DC014BF53239F91BBE0D5C25D09A766405C35D11B3732B2D2AD39A22C1FCDBADDC3A81EDC8E5EF80FE66B2A69753C82FD63D9CD4496493358B59E14BC382D58982A03F9561B4ED09C4D03D89EF77D9CF47E97A4C9A65A82BAEC15FF165E10490976EBB19FAFBA8F9E8E0DFFBDB4D5E1ACE5\"\n        },\n        {\n          \"tcId\": 22,\n          \"deferred\": false,\n          \"z\": \"973DBB6EAF76AF0C96F0F24EF9AE65ACD854301B5F7A7892A17FBB8601DE78D3\",\n          \"d\": \"D732CF45D7F44788E17C3B6DA9987495AB1AEFA233F74EEF8D3BE5B6C0C04E00\",\n          \"ek\": \"9DA1B8DCA84AF685C5EE685BAF30CA58BC22E9C75ABF67C8D5B8C283BC95004BB51CC6CB001466230710A9E5B43891581C9315A3B579B28B9FB60C5A44D47733105D136ABECE627889E19E4FD25A7C94C404207A4B982327A05451250C173B67A4F7CB2F19CBDA5637E6462842B7688559C599D62A6828AB62B75738E632C381C36E58424AC486BA4C1F4EAB5C4C9A96D27A1F4D3296F65A6094FA7FD27B02707C69A7B92787F737CD4623E3E51444CC698CBC7B38AB98F9883C63210442C8508A126870B7C45127C3C036B39A4C1E44909570382DC20B31E8E262ADA13847599EEAE60BFA024254726D36717C4C43B4BA148F82A93667B82407F2AA3E076E19E513564CA6907C293023773FBA330B8B9C90B08665122D31564BED3632936C5C70C4C452880BFAB97EE6018A67E0891EF64C86E8C4562C29CA609381B8AF55FC09069673FAE60E0412678958053F160F4E4AA135D89FBB0172D10CCCAD38070AE918D85C0866882829603B8E4B90E2074716A05FFE7B1959F8C16FA7CD42CC8A124B930AB87812C4C47B8C58352677EB718F508165CCD4CB3F6B6FA86460B744708F57046FD17215EC9AC6F92EA354AF8C83C0B4369AA475233A310FF7A915F9C64A10327F042C8D6450B0248621DD7748D116C090C6C6B58447433810EA47A07F9956482073FAA67CFE513AECDB87A1695A23E454D918CE0E69BBBBF0761F1B1C8F535469CC25204592DDF8008154144D148B20460F95B1B26D0CC5FF45081E379B85E642EBFA1728CA1435B081F4F608A92148FE27AD97D10BE5D3550ED08319218768D31D5DD9B2E8629B65A2345D259D45B4719AAB8616DB6422B9AEDA535099D503E8A865F3BB46C3A49C3784CCEE19CA3DE5189873C212068DE128C86884C2A3841D2DABA8088B4CECAB729C0C73421B8D0278343DE01584D37971E802A0699CAE5B2CF86800D6C348A0D8C4942538122C521B56A45244AB672402CF7B1A9E999820349F7E311940B5B47FD07C8CAAB8CD817DB897A4CC7591BEB264F043401B06932F81C5E8F525C81BC5D4C75D75CC98B8B6502625CACD671C408C21812277B7BA3388D40B4FE46510A1ADDFC040696BCFF31CD1DE026A9D5E8D3B6F\",\n          \"dk\": \"AB606FB6D65B4A42C61BF14D57C06B8EE33E740ABE1068833667B8CE769EA1FA5AFE7447863C3DBAB8B86C1C818CC3A2AEF08C50641F566BB154D57E293A468437C6A7805318B3578F83C927E6BB56AA31F096809A315C5D88473DD0BF2162BE18CBC013918504E791779A4A2CF82C5085BAAF78CE27690F164478C1F340A3582C845B2114978E2B7029D6A861FE302653213C1F9A437C9C870513ABA2B64A744A92BC37CCC8C0664CD833065613E7770C6C01A6F9091F02A99EB5F85C2361038A0662B3352C09FBC1948993724A87E2C71D63F7392E9950B2A84F241678FC15890247BE20C660AC3B2FDA9CBB9C919193F13EF3A5BA98901CDED47A6E1B300CF779BCD137748B571FC4CF890A69C1B5CDDBE30650877E2D82263448CDB1882D57716A76294CE8EC931AA131F06965678AB8FD778DAB50C58381B8090A7912462400E0B0E8F7C9F1FA78C57A056B423495C75F62D5138BB3A1034C70208917F30263E66B4D2949377BF29362A6531A81B82EA88517D3B943A14868D7691E4873F29AACFA8B71DBA337C9E0BCC1C8AA8BA82A17A28395B45EDCA609BF95B883731C62C000A3D431525391860223579C247AE825CDE91E1FE3B0A056BE5694627BF3BA189031353429D0C64C79448E5E4C80B8963C4D7229E9188B76F1738C30BDDF49B236544558C9A7F979B1C34826C0FC10B3362E84D62A665B16648CC8376BCB1A71116E260E0439A9EFE287AEC68942F3916CC77817C9C89F378E5976B3C8B9815F7013D573C9EA656A95F40F1243B5940B9F6E830341E1108A45CB68B346600406D87403B8352FD7014CA3762070FB8DF892871087B7E0BC5562B9686BAC3A67665AE4CB118A602C28394224D15C5A9615C86B40184A9B7E4A68DE936C8144A157F4B1AB036F30D92F0BC497ED0716380BAB4677111AD19BDDA98B333C960ACAB48E33AE12389492B98CF4FC90F7102012840010F63CF78B6193E133E45B81E6380B4E01CDD0814C8E97732B4BACF3171215728A378BAB58853E38FC51A788829E810292C5089EE972B8850182D37D49F8BFB09178D5CA779E91776BF8579DA1B8DCA84AF685C5EE685BAF30CA58BC22E9C75ABF67C8D5B8C283BC95004BB51CC6CB001466230710A9E5B43891581C9315A3B579B28B9FB60C5A44D47733105D136ABECE627889E19E4FD25A7C94C404207A4B982327A05451250C173B67A4F7CB2F19CBDA5637E6462842B7688559C599D62A6828AB62B75738E632C381C36E58424AC486BA4C1F4EAB5C4C9A96D27A1F4D3296F65A6094FA7FD27B02707C69A7B92787F737CD4623E3E51444CC698CBC7B38AB98F9883C63210442C8508A126870B7C45127C3C036B39A4C1E44909570382DC20B31E8E262ADA13847599EEAE60BFA024254726D36717C4C43B4BA148F82A93667B82407F2AA3E076E19E513564CA6907C293023773FBA330B8B9C90B08665122D31564BED3632936C5C70C4C452880BFAB97EE6018A67E0891EF64C86E8C4562C29CA609381B8AF55FC09069673FAE60E0412678958053F160F4E4AA135D89FBB0172D10CCCAD38070AE918D85C0866882829603B8E4B90E2074716A05FFE7B1959F8C16FA7CD42CC8A124B930AB87812C4C47B8C58352677EB718F508165CCD4CB3F6B6FA86460B744708F57046FD17215EC9AC6F92EA354AF8C83C0B4369AA475233A310FF7A915F9C64A10327F042C8D6450B0248621DD7748D116C090C6C6B58447433810EA47A07F9956482073FAA67CFE513AECDB87A1695A23E454D918CE0E69BBBBF0761F1B1C8F535469CC25204592DDF8008154144D148B20460F95B1B26D0CC5FF45081E379B85E642EBFA1728CA1435B081F4F608A92148FE27AD97D10BE5D3550ED08319218768D31D5DD9B2E8629B65A2345D259D45B4719AAB8616DB6422B9AEDA535099D503E8A865F3BB46C3A49C3784CCEE19CA3DE5189873C212068DE128C86884C2A3841D2DABA8088B4CECAB729C0C73421B8D0278343DE01584D37971E802A0699CAE5B2CF86800D6C348A0D8C4942538122C521B56A45244AB672402CF7B1A9E999820349F7E311940B5B47FD07C8CAAB8CD817DB897A4CC7591BEB264F043401B06932F81C5E8F525C81BC5D4C75D75CC98B8B6502625CACD671C408C21812277B7BA3388D40B4FE46510A1ADDFC040696BCFF31CD1DE026A9D5E8D3B6F960EBBE29B71F9BB16A8B5EDB72516DEB04771759A6A306CCEE5D40D8E2EFFDA973DBB6EAF76AF0C96F0F24EF9AE65ACD854301B5F7A7892A17FBB8601DE78D3\"\n        },\n        {\n          \"tcId\": 23,\n          \"deferred\": false,\n          \"z\": \"D525CCE60C3E300ED36298A1C0D0165C147CB84197C4028257DAF39239E6EA5D\",\n          \"d\": \"B670CEB5612A1287C4653B158A3CC522AAA1AA45B34A4C770DCA1E5BF3988F3D\",\n          \"ek\": \"0148396E994C3C5809BA5A53B28582B5B12827616D84BC9B6FCC94B84A1FF1EA88D308302F611683EB500953AF2D588EE43C7C4CCACC276128E6509DAE5A78DBF654CB9A0AFEF98339A46002B432C90BB863CA5221522BC7E903E2D1AC26E4968B64888DE455D6CC61A7FB2BD8E03BAF87310337361101BEAE92C64904BFD6D74F9D4327E49B15A7468889C6BE1F6A6DBB3529ED166BB0791381D42A59C85D54C1484B50AFFE893FB2A5BFD60974BC804E30A14E5DD22284C361CFC311C261C02D450B864095109B01DF56016A8BABEAE78F6145CD65343A8E7B098E904453E165D31273ECB4486B1B43BF453AE853B74AB920DB1068E8D9C8C8779BB63545EA0A15D4A1C19897C8BD40CA1C66381832A990FB74ADCB4FDFB2185AC33D9A24B58983B352AC26BE979D2FF8B8975B7D6B09B50E0C635625A99E91C552F37C37C72B763047C36192900B0A49BBCBB19C611CE77562D48D06CB735E174184952614B9236C55A256394FC5661F4F70BAF5B22BC1D67860A51E0F301AC5DB4F15D061028026CBB4A35E1782B406C8F92A40FC500F5FE706CB1A33DDB73597575BE787CEDC5C43383A404C88503B351732BB9FAA65C07C0B1EBCE001D5D18203672885E201C3D9855F81C36D39B060761DCD069E7CACC758026456E266DCA665AB32908069448BD251F3D3C09B15B4DC2234642295BA9419574A8E3987B69C4058FE692DB8124821B1A7F4097712B828AB69C9B0F5A6AF9B515588146EE9A1E79780A2C9634AC9CE38282B1E47856F365F23AC249D0BB7E42520B8B3A08792161B8B86C59CC0DECB2160F536394A750CB9373BC62BB2C6AF8257935465C550382822A915A501693A777CC9E46F415BB581C70B39DCA1F84673B6DCA1BF2897B8A57CE247267013A49D3543CC8533EB5BBCCEA0A9C82909ABEB98CADC45EBC5B4034145B7F5A777759CAC11CE4C359B0AD4BC30567E9F195C9082B3A71779D9A3B4EBA72E557AAE9B964285D142BA0031C3990192912F34B540104A7E13674B30C0569685A4ADA913BEA4439ED3C99172A3672886B857830DB939A2836A8E3186C8B8CFD87CEF4EBC154213985561AF0C82B28B8C856D8923219B1829FE8B51954BD0E8\",\n          \"dk\": \"4ED9B27229CB08667437F2B6F0B28ADA29A384F726E1F86DCD26433D55CDD5511158EC7D257892080A4DEE204FA1103890821E2F233281FB064D299DC9684D3D08A60669B014112338476F0455780714524DE0538628C72AD360A795038DF2C3A44058D71C285A4A951086889A8A4DB9AC9B4F76821E12732DF3713842012F3A8F4A611202434A1E6046E58085568037A4884F3D8539FA5005A8E10E93BC03C58BC9D575761E4A5379AAA8422A1CBAF58C8F9C1DFA00BD0550980F86B32036CC2F55C4C6602F5CC528BD1971ABD7AFB1528F39A7CFE1A42CBA9716B1A6A8A6722F04BC5760497F79556D7C065E4FFB9A4E8019F38744849B57D3F17AB8C37FF71CA594691AE9288BF7E1BFB09C9B113C375B4AAB07C91452E384E8F8B1629B7E38A1943513CB2AC40A5803B71DE455FB2B55FC50CE6170A15DC4A9719193A5C2CB17C62B87C53605E955225080011468913AAAA4946C9A084A2405A335BABFF2E19A3209C34273CC96A4509A0041E474C44F53617A36326A7892B6D079BC4651775B0E06EC01C81CBDAA7574CBF84940384A15777F241268CFE65E6408609E404A372BAE702790E185ADAC6AA93843B3CE88696E3252EF73B46C5A4DDE135372A32762B963670565531784144399C6195EA6C924B89AA5F9B64BA44416D92B3E75F7BDEC264549B045922612C0995BBFF71DC7505F04D4BDBEC3A40E7C402A225EF4153C1A322B6E19BCB0328B9ED23FE36A6F13780A6A78171E875CD37A79C3375671D066EB20072DA5CBFFB8772DA14DE98631899597F7F55AE84BBEBEF076E709CB68946041F0095759360E003665408105184B39A7261545AC0E7984FD32A773D05A04A48FBCF02B08E7B3E326583DB71F59E15C7D1A9D6AF1494CB34E63D5C6CC07C23BE95F3EDB765C33131FC92B07592C724832DEFCC1D5F937AA8323C1089CAB760D26AAA49EE1805FF62382998B0A4B8DFF6C91970126833B44518501A908A0A7C462E678495DF876CE3A71528CA877890B56F6B23C9C2C560B0ADA3A1915E265093267E21A2EC18351079011E6E0CF76967AE3B80E1038B3E203A40148396E994C3C5809BA5A53B28582B5B12827616D84BC9B6FCC94B84A1FF1EA88D308302F611683EB500953AF2D588EE43C7C4CCACC276128E6509DAE5A78DBF654CB9A0AFEF98339A46002B432C90BB863CA5221522BC7E903E2D1AC26E4968B64888DE455D6CC61A7FB2BD8E03BAF87310337361101BEAE92C64904BFD6D74F9D4327E49B15A7468889C6BE1F6A6DBB3529ED166BB0791381D42A59C85D54C1484B50AFFE893FB2A5BFD60974BC804E30A14E5DD22284C361CFC311C261C02D450B864095109B01DF56016A8BABEAE78F6145CD65343A8E7B098E904453E165D31273ECB4486B1B43BF453AE853B74AB920DB1068E8D9C8C8779BB63545EA0A15D4A1C19897C8BD40CA1C66381832A990FB74ADCB4FDFB2185AC33D9A24B58983B352AC26BE979D2FF8B8975B7D6B09B50E0C635625A99E91C552F37C37C72B763047C36192900B0A49BBCBB19C611CE77562D48D06CB735E174184952614B9236C55A256394FC5661F4F70BAF5B22BC1D67860A51E0F301AC5DB4F15D061028026CBB4A35E1782B406C8F92A40FC500F5FE706CB1A33DDB73597575BE787CEDC5C43383A404C88503B351732BB9FAA65C07C0B1EBCE001D5D18203672885E201C3D9855F81C36D39B060761DCD069E7CACC758026456E266DCA665AB32908069448BD251F3D3C09B15B4DC2234642295BA9419574A8E3987B69C4058FE692DB8124821B1A7F4097712B828AB69C9B0F5A6AF9B515588146EE9A1E79780A2C9634AC9CE38282B1E47856F365F23AC249D0BB7E42520B8B3A08792161B8B86C59CC0DECB2160F536394A750CB9373BC62BB2C6AF8257935465C550382822A915A501693A777CC9E46F415BB581C70B39DCA1F84673B6DCA1BF2897B8A57CE247267013A49D3543CC8533EB5BBCCEA0A9C82909ABEB98CADC45EBC5B4034145B7F5A777759CAC11CE4C359B0AD4BC30567E9F195C9082B3A71779D9A3B4EBA72E557AAE9B964285D142BA0031C3990192912F34B540104A7E13674B30C0569685A4ADA913BEA4439ED3C99172A3672886B857830DB939A2836A8E3186C8B8CFD87CEF4EBC154213985561AF0C82B28B8C856D8923219B1829FE8B51954BD0E8379CBECD878337A3709BC5A62C5528CB3504D6A87427DC404EFF9ACAE893CEEBD525CCE60C3E300ED36298A1C0D0165C147CB84197C4028257DAF39239E6EA5D\"\n        },\n        {\n          \"tcId\": 24,\n          \"deferred\": false,\n          \"z\": \"9F2FC49CD848BA72FC17854B18D88ED65B630BA94A1BC5F6D3A458E1087D3A13\",\n          \"d\": \"3236CB10279681238E5B0E2F5138A7F743443379F5F1A845F3D76B75D2C2A9DF\",\n          \"ek\": \"E08607BB14655E1B5DAA36971A842091513B13720A20971FF32279FDE800CF82A95F96AC390749661958E510605DB13175B941D27AA8D1B07A76BB3EFCF8810F144F2566C938DA012E792F5EA41AB74431DD50584756963C752F8ED1109021521D4CA897019AF967275C92AEBA2655ABB291E9115C964C5E5EE748D8C0258DDC504DBB98218A7DC035251A958CC30821ADC997667616DE2A4497039189F582BC4AA9EA4A33BDC1858D8681D4088622A0CB8EA59413423BEDF0CFF7F36BB419B1B672820EC177B3920797244079738EBEAC8D34057804E9A6AA31B39F589F59198972213A12F80D4163A93511985B892ED8BA9A0FA260A4EA8A3BE1A556174C3A599F31D037293C5B8F6A229E23ACB6C772529B918C5324246C2E6B8C693C512BAD0C1677E013C7B10686E728E66723068256FE924F2F3A1578569D24D8327AC543C43AAF423776EEF10D43A69AD87058A37A95F4D8C444A08EAA9CB687E0851BC2A7D304B39B2953AC81A4E9E6B454DB06E75B495B11416D78C86009904377996BB0489BBC2C1B68C5FED757782082472616BCD7AB7747A36457B7BEE74F9BF39A10C9071BCC9269B61EE93B2062160EC69174229A9EDA906559F677018A28F1A278D6337CA54AC68F7265133A1CB59A64E0699348153613191DA8793D441C1422F498B9088EE3664153F67EC443CD8DD10D3068628F10C49662CEE84CC2C85B3C59553B7220AE499C1A9838A8D0E497C11717B5539575F62CB49597C9466A0331C118849F53CA4EAA516CD8343B28F5A0787313F6F1331976329203B0CF871A2CBA0418A574DB037DD12A139C3B36850C8E2B2C056920A1F656873DA9CEF9427D4FDB15D069CD54FC479DD63464F63598728D050444E2014D6E7040F7942991D34B228076B14207F8777D718180AC9C54379B7952F732E865A915F874D1BB839C1A4E2617455AD061509C83CD9A123A2B34C4393C8D09A2C9BB8D52643FB6205730C98976F99D649428CCDC4D109242E7660DA8187C1C1BA83FA11834B0824E31084F0693ADDC1F06DA292398906CF2C07DB5337EACB9492C57F12499B9BA7828A4D75A3EAF1E72A78C47ED24CDD9A7BE6E15D35A92BD110C97DF3F400DD95B\",\n          \"dk\": \"E3930A94D60F5D896AFC749DC3D5CE56A566D7D28795B7747EF95DF030C97C048657B6C1AC754E15F47120F75129E34B10BC3F11924D42655836D6B7E5D126309070E2D75E616A517481842B851148A586D9015045DA3602FABDE204AEDC633B95C30B8DD533D61222BB465BE3BCCD792868990C3E7C1B0E086ABD3EA9CF4F110B1E7612B0C32FDC693CE2FA3004E40A507674528749A470ACE9201D68F56CA717C19539A0418488CEF40CEA1458D6EC298FD04F60C9878F01851EF07B032C3A91B0C742A445E5E72131E00594D0595794745C26BF17E7A4297A9186B23F3DC90B0EA183CFC13AB5269CBC4310840029644876E6C6C73E066230F182CE3B0F039522DC7ACA25AB1DBEE371574786308C177E1ABE75C202D383C47409111FC29F72E18085542E231486F6F76A9CD0BCB9384DAF6318260761E96A0A36DBB50B902BF6355186B6BFB552AE94385523E02935F98C2A35AA3BBC03B7B9A4E4559C741347C304AA8C033524A8835294738908879374C8AE76BDB52C30203A50D5B9330CD2941E6A9EDD51659C50165C06232778B39AB6041E27BFA5DA56A5811C7CE35140724E7DB5072BEB217518120D5366DADC4653DB4D260C65DBA35342D34BADE3A27E3757345C3DC4D4B4AE1068D02989B63722BE94A2C8589DFF97B236B0CBEBC436F71B4DC2A619FB88C16A476A42E51410ACB5BDFBB01E9B695C2724536BADA18913E3F3555B120D61A158F8722688B6C703675CC9580C37C52E2A338E08192E3472BB600037DA863195F0A0E4C215FD57A19BA20CD57021F5025E0AD79FAB9123238890451547B0670491B03DA6C6567A767449B7BBA1FABEDF7BBD38E56A02F9AE20E0C5BE502350A1A27A9A5457B3C173796986F4703EE6BC2706630ED56859781DE66AA9B9AC87D9F512FE569C5F164B46AC0F3175516F633FE31C60B09893CEE29EAE725F65033E0B1BABBE453F84F6673A963FE74A8B5FA046108A2243690D9072406154C6A0024D2CA4B4EF1A681A911A781C776C126775B0CE65110BD8C76A1B2444F62B2CE4541C102C70C1F46B6B5AC0F8A8574836122A99B1E08607BB14655E1B5DAA36971A842091513B13720A20971FF32279FDE800CF82A95F96AC390749661958E510605DB13175B941D27AA8D1B07A76BB3EFCF8810F144F2566C938DA012E792F5EA41AB74431DD50584756963C752F8ED1109021521D4CA897019AF967275C92AEBA2655ABB291E9115C964C5E5EE748D8C0258DDC504DBB98218A7DC035251A958CC30821ADC997667616DE2A4497039189F582BC4AA9EA4A33BDC1858D8681D4088622A0CB8EA59413423BEDF0CFF7F36BB419B1B672820EC177B3920797244079738EBEAC8D34057804E9A6AA31B39F589F59198972213A12F80D4163A93511985B892ED8BA9A0FA260A4EA8A3BE1A556174C3A599F31D037293C5B8F6A229E23ACB6C772529B918C5324246C2E6B8C693C512BAD0C1677E013C7B10686E728E66723068256FE924F2F3A1578569D24D8327AC543C43AAF423776EEF10D43A69AD87058A37A95F4D8C444A08EAA9CB687E0851BC2A7D304B39B2953AC81A4E9E6B454DB06E75B495B11416D78C86009904377996BB0489BBC2C1B68C5FED757782082472616BCD7AB7747A36457B7BEE74F9BF39A10C9071BCC9269B61EE93B2062160EC69174229A9EDA906559F677018A28F1A278D6337CA54AC68F7265133A1CB59A64E0699348153613191DA8793D441C1422F498B9088EE3664153F67EC443CD8DD10D3068628F10C49662CEE84CC2C85B3C59553B7220AE499C1A9838A8D0E497C11717B5539575F62CB49597C9466A0331C118849F53CA4EAA516CD8343B28F5A0787313F6F1331976329203B0CF871A2CBA0418A574DB037DD12A139C3B36850C8E2B2C056920A1F656873DA9CEF9427D4FDB15D069CD54FC479DD63464F63598728D050444E2014D6E7040F7942991D34B228076B14207F8777D718180AC9C54379B7952F732E865A915F874D1BB839C1A4E2617455AD061509C83CD9A123A2B34C4393C8D09A2C9BB8D52643FB6205730C98976F99D649428CCDC4D109242E7660DA8187C1C1BA83FA11834B0824E31084F0693ADDC1F06DA292398906CF2C07DB5337EACB9492C57F12499B9BA7828A4D75A3EAF1E72A78C47ED24CDD9A7BE6E15D35A92BD110C97DF3F400DD95B402618B875F180B5A47574F635D61BB39EA75D44240CBD759B7B5C22C889851C9F2FC49CD848BA72FC17854B18D88ED65B630BA94A1BC5F6D3A458E1087D3A13\"\n        },\n        {\n          \"tcId\": 25,\n          \"deferred\": false,\n          \"z\": \"0FB831AFA34B124F7456D0D09E4ED8607DE407101E6E75F305F9D67EF7C2FAE7\",\n          \"d\": \"C155568B6BA74DA317388423F8FB28585977EB858EE306CAE4174120F02A8D72\",\n          \"ek\": \"655CA28309AD58DA0D71919EED2C5676E134C1AB1860923F1E34AF07581D64121B72B8ACE613BE88AA28BD7B0138B44FD9C39B65FAADA741934AF9207AFA367CC1AB29CB71806735973188CD146813331DEA81A811F61FD8F59FC668AB29C581C5878F2D4698E5CA52DD664AEC16ADC357CEA156876BB6663DB12479F488AA46B7873968D0AA43EDC0B847F19FD4410F5E45C2A599A09E634E65B206352383B79BC6EB9309574733F9F2226D4402A4946A00D7B469DCC58F910D5B149BB5731FD89CAF9734C2531381EC2A60F68A9DFCA55C288C68BEE4A919A9A82CF01E5EA86A170B76ED5B35AD9573C3156A24D8A9718683D0F44547F03EF0237B430C16C1A22AF41B7D46E839CA40A7F5F982ECDA7EF21A7E93283B6CBB566E2237636925B887616DB260019876386BB9CB63A42EAC0EADC4C6B99A3EC4131667A4BC3015B19A9A7896422DE73655379BB57675C26F31609BF5525AC13B1A711974F7CB80BACF59B43A8C0AC6B3249968E462F782CF85661207744B4E016C22531D4879B4AD9BC80320503D311CE818A29B95595AA6AA4590876C4C62E0F5A75ECAADF4154634475425B7BD690A09650C7B543589D0342C18A2B774CB7DC8C44F3502A2C2BC148465061842118321A8D4465B4383B45A52727B389FD5A9601E560C9FF68882C629D209527429C79A1ACC000B698181AE827A7E6F14235AF281460A91DBECC30E4AC3AFC4A22E45B3F1209BBDEABDD678197EFCB927B47B0C4204FD473AAB098984349FDF80B960203530E36153BAA675545F5DC6B49A276F1D210373553F9F90B2A5C661415C65A973991E2A99C756273D3261B8750431CA866FA234A7E98035804A0AAACF8D36A0BF4A5C9F9C5614138287858FC106CDB6918E2FB37BEAE2139F806838776E2B3534D78ABB199833E784A6FC144A6D7CB8984C4563E273CE11421927377A992D602BBD11457A2C88B999B13A6B3944445CC7C8328F47040C5CB8B2EB29192FAA2720B1788A408F59828D42017D27587CEC9CA49F44829AAB7FAACC484936B38E02820C03C880F7CEC7057B89A1604D7B1865090CDF056B83E84864E13B8B1B16E5BF2115103F5E1A77266147DEB23013A1A909C1FAE8BE\",\n          \"dk\": \"03D96BD4C488568ABFE20B8FB2C408AAD328481B228C03894BB5248292B2F87714622714A36936DBE1BC477AC57EF3B3F46570B776014F0C12D3B0BA3FE81AA3CA9908ABCFB47AC622AC4A4ADAB3C0052EF8F133E2F8B415C3BE8943BB55C902F792CD6F3A363D3C6530D0A4AB1886EDD770FF18832BB7AD89CA126450A0A2CA587EE1552F149C2DC570447A400C403A2883A8D79B361E4AB99B53B4F6C5CF88E6B2CCE28E69356FACC4217539583ED813ED0A554562C02F6696E9AC5714B5ADD1BA77E603528A3961591B5593337E7EA552E9770AB43009A531679F4C66C0E817BA0354BA24977572956DD7043C911133F28982D4B901307375CB2EEA0089D1C03E6594AA78884BD5E7C9A8A89E9661C2E2629240CCA2E4235B13AB21F75C6FC6A529A574CDE0C5381E40137A9CCBFD910243174BE6225C3ACC8F38393365530F2905294F9ABA5108A3487C9D056559C9E8A11CC94F7CDC46533C391FB2A491973F8BB9545773BB90FB43DD7435DF6C90F8B411A8274FF648A0EEE54FEF30315993C5BFEB6BA0233EE920CE01927F8752A63598528E463A856930598643D825279F99C113B1578EB0B1D72B730D8721B8B149DD0B558977B0C3BBB0E05383C4B8844F661B6C34220D2A5FD7113DA3217EFFA6AB0C382808C0A177987E7DC8B86F39C3872A66412755230B0CB119A3E49541AD1868B9753F069A167E604BE6E01F129A684B880930202221D0BED12B9763B8BF41E639CD233FE6AA0B1A8082D6D46E5316405AA06154C622A384154FD69434E161EC4470B82466D84CCFA01B0C795C9BD7175A2DDBBC5B1CA564D942A0226F096A54AB5719A58A60387B5AFED4878DA08E9763A164E150F50CA719214181165CF1069E29FA2AD81ACA55A802DC9A03F351369F525C5DDA776B93717A617A969811C89CC27ADB185B2A197A47C6C360B7D254C0F32A20C8A35990C67098896180AB9456866B09E62941482B626BC40F63136A677AA7E1236A7B8ADD810193EC4C3E221017A6A8DBF079122CB288D60795482039A4AA7CD785B85B44D1817E5F79232318C0F6F408648A3E5395C4655CA28309AD58DA0D71919EED2C5676E134C1AB1860923F1E34AF07581D64121B72B8ACE613BE88AA28BD7B0138B44FD9C39B65FAADA741934AF9207AFA367CC1AB29CB71806735973188CD146813331DEA81A811F61FD8F59FC668AB29C581C5878F2D4698E5CA52DD664AEC16ADC357CEA156876BB6663DB12479F488AA46B7873968D0AA43EDC0B847F19FD4410F5E45C2A599A09E634E65B206352383B79BC6EB9309574733F9F2226D4402A4946A00D7B469DCC58F910D5B149BB5731FD89CAF9734C2531381EC2A60F68A9DFCA55C288C68BEE4A919A9A82CF01E5EA86A170B76ED5B35AD9573C3156A24D8A9718683D0F44547F03EF0237B430C16C1A22AF41B7D46E839CA40A7F5F982ECDA7EF21A7E93283B6CBB566E2237636925B887616DB260019876386BB9CB63A42EAC0EADC4C6B99A3EC4131667A4BC3015B19A9A7896422DE73655379BB57675C26F31609BF5525AC13B1A711974F7CB80BACF59B43A8C0AC6B3249968E462F782CF85661207744B4E016C22531D4879B4AD9BC80320503D311CE818A29B95595AA6AA4590876C4C62E0F5A75ECAADF4154634475425B7BD690A09650C7B543589D0342C18A2B774CB7DC8C44F3502A2C2BC148465061842118321A8D4465B4383B45A52727B389FD5A9601E560C9FF68882C629D209527429C79A1ACC000B698181AE827A7E6F14235AF281460A91DBECC30E4AC3AFC4A22E45B3F1209BBDEABDD678197EFCB927B47B0C4204FD473AAB098984349FDF80B960203530E36153BAA675545F5DC6B49A276F1D210373553F9F90B2A5C661415C65A973991E2A99C756273D3261B8750431CA866FA234A7E98035804A0AAACF8D36A0BF4A5C9F9C5614138287858FC106CDB6918E2FB37BEAE2139F806838776E2B3534D78ABB199833E784A6FC144A6D7CB8984C4563E273CE11421927377A992D602BBD11457A2C88B999B13A6B3944445CC7C8328F47040C5CB8B2EB29192FAA2720B1788A408F59828D42017D27587CEC9CA49F44829AAB7FAACC484936B38E02820C03C880F7CEC7057B89A1604D7B1865090CDF056B83E84864E13B8B1B16E5BF2115103F5E1A77266147DEB23013A1A909C1FAE8BE213234B8355942F1CF9F299DC63B953C236F330E3D406312E9E0CE14A3987E8E0FB831AFA34B124F7456D0D09E4ED8607DE407101E6E75F305F9D67EF7C2FAE7\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 2,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-768\",\n      \"tests\": [\n        {\n          \"tcId\": 26,\n          \"deferred\": false,\n          \"z\": \"A85768F3486BD32A01BF9A8F21EA938E648EAE4E5448C34C3EB88820B159EEDD\",\n          \"d\": \"E34A701C4C87582F42264EE422D3C684D97611F2523EFE0C998AF05056D693DC\",\n          \"ek\": \"6D14A071F7CC452558D5E71A7B087062ECB1386844588246126402B1FA1637733CD5F60CC84BCB646A7892614D7C51B1C7F1A2799132F13427DC482158DA254470A59E00A4E49686FDC077559367270C2153F11007592C9C4310CF8A12C6A8713BD6BB51F3124F989BA0D54073CC242E0968780B875A869EFB851586B9A868A384B9E6821B201B932C455369A739EC22569C977C212B381871813656AF5B567EF893B584624C863A259000F17B254B98B185097C50EBB68B244342E05D4DE520125B8E1033B1436093ACE7CE8E71B458D525673363045A3B3EEA9455428A398705A42327ADB3774B7057F42B017EC0739A983F19E8214D09195FA24D2D571DB73C19A6F8460E50830D415F627B88E94A7B153791A0C0C7E9484C74D53C714889F0E321B6660A532A5BC0E557FBCA35E29BC611200ED3C633077A4D873C5CC67006B753BF6D6B7AF6CA402AB618236C0AFFBC801F8222FBC36CE0984E2B18C944BBCBEF03B1E1361C1F44B0D734AFB1566CFF8744DA8B9943D6B45A3C09030702CA201FFE20CB7EC5B0D4149EE2C28E8B23374F471B57150D0EC9336261A2D5CB84A3ACACC4289473A4C0ABC617C9ABC178734434C82E1685588A5C2EA2678F6B3C2228733130C466E5B86EF491153E48662247B875D201020B566B81B64D839AB4633BAA8ACE202BAAB4496297F9807ADBBB1E332C6F8022B2A18CFDD4A82530B6D3F007C3353898D966CC2C21CB4244BD00443F209870ACC42BC33068C724EC17223619C1093CCA6AEB29500664D1225036B4B81091906969481F1C723C140B9D6C168F5B64BEA69C5FD6385DF7364B8723BCC85E038C7E464A900D68A2127818994217AEC8BDB39A970A9963DE93688E2AC82ABCC22FB9277BA22009E878381A38163901C7D4C85019538D35CAAE9C41AF8C929EE20BB08CA619E72C2F2262C1C9938572551AC02DC9268FBCC35D79011C3C090AD40A4F111C9BE55C427EB796C1932D8673579AF1B4C638B0944489012A2559A3B02481B01AC30BA8960F80C0C2B3947D36A12C080498BEE448716C973416C8242804A3DA099EE137B0BA90FE4A5C6A89200276A0CFB643EC2C56A2D708D7B4373E44C1502A763A600586E6CDA6273897D44448287DC2E602DC39200BF6166236559FD12A60892AEB153DD651BB469910B4B34669F91DA8654D1EB72EB6E02800B3B0A7D0A48C836854D3A83E65569CB7230BB44F3F143A6DEC5F2C39AB90F274F2088BD3D6A6FCA0070273BEDC84777FB52E3C558B0AE06183D5A48D452F68E15207F861627ACA14279630F82EC3A0CA078633B600AFA79743A600215BE5637458CE2CE8AFF5A08EB5017B2C766577479F8DC6BF9F5CC75089932161B96CEA406620AEDB630407F7687EBBB4814C7981637A48A90DE68031E062A7AF7612B4F5C7A6DA86BD136529E64295A5613EA73BD3D4448CB81F243135C0A660BEB9C17E651DEF469A7D90A15D3481090BCBF227012328941FA46F39C5006AD93D458AA6ADD655862B418C3094F551460DF2153A5810A7DA74F0614C2588BE49DC6F5E88154642BD1D3762563326433507156A57C57694BDD26E7A246FEB723AED67B04887C8E476B48CAB59E5362F26A9EF50C2BC80BA146226216FE62968A60D04E8C170D741C7A2B0E1ABDAC968\",\n          \"dk\": \"98A1B2DA4A65CFB5845EA7311E6A06DB731F1590C41EE74BA10782715B35A3102DF637872BE65BAB37A1DE2511D703C70247B35EF27435485024D93FD9E77C43804F371749BA00B20A8C5C588BC9ABE068AEAAA938517EBFE53B6B663282903DCD189736D7296816C733A1C77C6375E5397C0F189BBFE47643A61F58F8A3C6911BE4611A8C7BC050021163D0A404DC14065748FF29BE60D2B9FDCC8FFD98C587F38C67115786464BDB342B17E897D64617CBFB117973A5458977A7D7617A1B4D83BA03C611138A4673B1EB34B078033F97CFFE80C146A26943F842B976327BF1CBC60119525BB9A3C03493349000DD8F51BA21A2E92361762324600E0C13AAA6CB69BFB24276483F6B02421259B7585263C1A028D682C508BBC2801A56E98B8F620B0483D79B5AD8585AC0A475BAC77865194196338791B7985A05D109395CCA8932722A91950D37E12B891420A52B62CBFA815DF6174CE00E68BCA75D4838CA280F713C7E6924AFD95BAA0D01ADA637B158347034C0AB1A7183331A820ACBCB83193A1A94C8F7E384AED0C35ED3CB3397BB638086E7A35A6408A3A4B90CE953707C19BC46C3B2DA3B2EE32319C56B928032B5ED1256D0753D341423E9DB139DE7714FF075CAF58FD9F57D1A54019B5926406830DAE29A875302A81256F4D6CF5E74034EA614BF70C2764B20C9589CDB5C25761A04E58292907C578A94A35836BEE3112DC2C3AE2192C9DEAA304B29C7FEA1BDF47B3B6BCBA2C0E55C9CDB6DE7149E9CB17917718F12C8032DE1ADE0648D405519C70719BECC701845CF9F4B912FE71983CA34F9018C7CA7BB2F6C5D7F8C5B297359EC75209C2543FF11C4244977C5969524EC454D44C323FCCA94ACAC273A0EC49B4A8A585BCE7A5B305C04C3506422580357016A850C3F7EE17205A77B291C7731C9836C02AEE5406F63C6A07A214382AA15336C05D1045588107645EA7DE6870FC0E55E1540974301C42EC14105518680F688ABE4CE453738FE471B87FC31F5C68A39E68AF51B0240B90E0364B04BAC43D6FB68AB65AE028B62BD683B7D28AD38806BEE725B5B2416A8D79C16EC2A99EA4A8D92A2F5052E67F97352289761C5C39FC5C742E9C0A740CA59FC0182F709D01B5187F00063DAAB397596EEA4A31BDBCBD4C1BB0C55BE7C6850FDA9326B353E288C5013226C3C3923A791609E8002E73A5F7B6BB4A877B1FDF53BB2BAB3DD424D31BBB448E609A66B0E343C286E8760312B6D37AA5201D21F53503D88389ADCA21C70FB6C0FC9C69D6616C9EA3780E35565C0C97C15179C95343ECC5E1C2A24DE4699F6875EA2FA2DD3E357BC43914795207E026B850A2237950C108A512FC88C22488112607088185FB0E09C2C4197A83687266BAB2E583E21C40F4CC008FE652804D8223F1520A90B0D5385C7553CC767C58D120CCD3EF5B5D1A6CD7BC00DFF1321B2F2C432B64EFB8A3F5D0064B3F34293026C851C2DED68B9DFF4A28F6A8D225535E0477084430CFFDA0AC0552F9A212785B749913A06FA2274C0D15BAD325458D323EF6BAE13C0010D525C1D5269973AC29BDA7C983746918BA0E002588E30375D78329E6B8BA8C4462A692FB6083842B8C8C92C60F252726D14A071F7CC452558D5E71A7B087062ECB1386844588246126402B1FA1637733CD5F60CC84BCB646A7892614D7C51B1C7F1A2799132F13427DC482158DA254470A59E00A4E49686FDC077559367270C2153F11007592C9C4310CF8A12C6A8713BD6BB51F3124F989BA0D54073CC242E0968780B875A869EFB851586B9A868A384B9E6821B201B932C455369A739EC22569C977C212B381871813656AF5B567EF893B584624C863A259000F17B254B98B185097C50EBB68B244342E05D4DE520125B8E1033B1436093ACE7CE8E71B458D525673363045A3B3EEA9455428A398705A42327ADB3774B7057F42B017EC0739A983F19E8214D09195FA24D2D571DB73C19A6F8460E50830D415F627B88E94A7B153791A0C0C7E9484C74D53C714889F0E321B6660A532A5BC0E557FBCA35E29BC611200ED3C633077A4D873C5CC67006B753BF6D6B7AF6CA402AB618236C0AFFBC801F8222FBC36CE0984E2B18C944BBCBEF03B1E1361C1F44B0D734AFB1566CFF8744DA8B9943D6B45A3C09030702CA201FFE20CB7EC5B0D4149EE2C28E8B23374F471B57150D0EC9336261A2D5CB84A3ACACC4289473A4C0ABC617C9ABC178734434C82E1685588A5C2EA2678F6B3C2228733130C466E5B86EF491153E48662247B875D201020B566B81B64D839AB4633BAA8ACE202BAAB4496297F9807ADBBB1E332C6F8022B2A18CFDD4A82530B6D3F007C3353898D966CC2C21CB4244BD00443F209870ACC42BC33068C724EC17223619C1093CCA6AEB29500664D1225036B4B81091906969481F1C723C140B9D6C168F5B64BEA69C5FD6385DF7364B8723BCC85E038C7E464A900D68A2127818994217AEC8BDB39A970A9963DE93688E2AC82ABCC22FB9277BA22009E878381A38163901C7D4C85019538D35CAAE9C41AF8C929EE20BB08CA619E72C2F2262C1C9938572551AC02DC9268FBCC35D79011C3C090AD40A4F111C9BE55C427EB796C1932D8673579AF1B4C638B0944489012A2559A3B02481B01AC30BA8960F80C0C2B3947D36A12C080498BEE448716C973416C8242804A3DA099EE137B0BA90FE4A5C6A89200276A0CFB643EC2C56A2D708D7B4373E44C1502A763A600586E6CDA6273897D44448287DC2E602DC39200BF6166236559FD12A60892AEB153DD651BB469910B4B34669F91DA8654D1EB72EB6E02800B3B0A7D0A48C836854D3A83E65569CB7230BB44F3F143A6DEC5F2C39AB90F274F2088BD3D6A6FCA0070273BEDC84777FB52E3C558B0AE06183D5A48D452F68E15207F861627ACA14279630F82EC3A0CA078633B600AFA79743A600215BE5637458CE2CE8AFF5A08EB5017B2C766577479F8DC6BF9F5CC75089932161B96CEA406620AEDB630407F7687EBBB4814C7981637A48A90DE68031E062A7AF7612B4F5C7A6DA86BD136529E64295A5613EA73BD3D4448CB81F243135C0A660BEB9C17E651DEF469A7D90A15D3481090BCBF227012328941FA46F39C5006AD93D458AA6ADD655862B418C3094F551460DF2153A5810A7DA74F0614C2588BE49DC6F5E88154642BD1D3762563326433507156A57C57694BDD26E7A246FEB723AED67B04887C8E476B48CAB59E5362F26A9EF50C2BC80BA146226216FE62968A60D04E8C170D741C7A2B0E1ABDAC968E29020839D052FA372585627F8B59EE312AE414C979D825F06A6929A79625718A85768F3486BD32A01BF9A8F21EA938E648EAE4E5448C34C3EB88820B159EEDD\"\n        },\n        {\n          \"tcId\": 27,\n          \"deferred\": false,\n          \"z\": \"DF0F282411F4A071489A8F618E2AE5AEF40131CAC5233D6D731522720C2FEB1C\",\n          \"d\": \"444F032DD19AE7518C4B35B0732A41DC567845ABA8BD7B04A9C413A0CF2DE0B5\",\n          \"ek\": \"5CC523B2D908C45907A6694A665195171A5B2FB583A5C240CADCA8F0E83E46B14052C9620D3B7EF386CE8B9A5E873B65693B0D341C6EB2D10CE5E937CFB8C4C9134401BABFEEBBAECF47113A34B9C6E011BDC78A54F2B7BF36A5FFD27563D7443F2109F02A64C421411DDB2D1404A86F793A2DE62CDC560BFD6604D4B6330BA6AA621414E8C12DC71C25652ABAF36B875DE1978DD209AB53B885206C3A1B4F8B4A0670C087CDA9CDA7997437155659255C2D024822A448CE5157CF5B6E4C495A949960886A902C79591120117C4A73CE7B380C661851E1CA9EF1973D8A9D2A191B938C4110259C4227B600BA7EC9B033BB0300715032836573382445435A743CA61E923B18ADEC7CFAF10ADE908E582560EE91ACA012942319B4888109E55AA738A7BCF777C92B4B09A50A1C043C982C2C2357F73C1687B35BD123FC905E1A719353466A42B915DBF1A1750339BF0923419681E4531D97E2160AD896DB056570570510FB711169AF2DE0CBA51C5F5056242965AD429301E7020AE0141F845833A3FBA0B192426C001A7147C2926805CD86725442CADC2636BB769DCDE46D1BD12D30F4695593B5753870EF796FB2F3A53F283D5828B77CB75D5DE1BA25357C290A957FD501AEE0AE59D7AE97833B0BB640F781A08BD256C79117C220BDD83280A0069B29A645720096D297A2E5245439268C0ED01F75A939978372B9E05D93DA899C10BF6CDB18698C46EBE00BF90730E2EA393014461DEC6C87F17B2EE16C13B8507C6009BEE074F17367A5FC3067A28B7D804C32860EDE650E6FE85CF6E301D1B1647323199CA296ABC54D2811507572B5DFF92B54E3786D130938417624775D8534B0102B6B8006803DDB376EB830D1CA80E717BB7F260A5CA4A56BFC5DA790151725942AE7C42B2B9E385B4E0F995D4402161070B73A6BB0CDB77EF11B1286D75E315635E719088DC7909D026B198AC93BB4B6FE395843A4428F75C0C1448C605A8CABA0B8CD19CE465764B523628B3334E3885D68D5089E1A3045840C36A73AEFE7B93AB357FD8A46D7547A8EFB243E4953E67CA72CFA0B77835768AA0CD2D976820A97BC21C7033084AD45C0BF6B483ACA8A485641EB55A47BE36ABCEB96143BA90C515D5BE8513BB994CFA88FF4B3600E34C1E656877606B6280384A0F481458044C47732FA9B58195A5DFB48636E1558C56A43CB6941DEE5AEB1E27B89A7121BE166879B62BC01619A9ABE840CC678E028E9BC71CE233FD9DB8816294D71F1A080101912920534750DDE692F782BAC4D4481A0900E6BB952ADA798EE06232C200F57F76A914617914B7398A0433CD7A11B5AC09789034F39338CE567E3E7AEFE35B0C3B85D21506E8886587670761AF9BAD3261DAF22CBFC664604234B3B784EA001CC6702B9222545CFDB2965EB54678780EE3C9CC134CD2E655908D6BDF460BEE364C66D5ACCF4B492ADE9A0F3EB31995BADDE4628B67165FF6014D848541035CDA46949EC1C12FF492726A7214D1C7273FB85D5484E5A178751B56E3FB163D13A53C7B3038E09B847A8C06FF9B42E8C345CC95AAC1A09660AC1FC7A146E7845AB83390871655E604C4C009EE924AE107B61BC3664F488AC60783A1C346BD18C56CED3F03BC1B1E4075E9785F235EBC5CE6621414E77D52CEC3B2E\",\n          \"dk\": \"657004A34B4EA6B278BDC1BC94A997D86B206F88875A934042732CFAF8B3A0141FDD815F2203BD92AC478A9033126A8478FBB6453AAE005C03F60444163066EE922781D08DFB1508F547555B3027A2F75F28401A7D69A09669AC8309C3D4E4B49B214C4C76B3E4C26CED4940A325885C71883881B6C18C57BF22CB4484674A738988708FB7EC68855A96EF033B4A877038612B7B14BB3DCA791DC5CC7C85614A694D0672CB5656CA51C7B3CE11ABE1F4B790800FE7F47F97D640141702B147A3A6D99279B258CAE7899C353A66F6AF3C53C4A632BEB545B65A2724EF06CD05978E3EE20BF264A0335B21FC2137C71161A8A3AAA1A6AFABD023F58C0C393630E41561568C6669C2683B0B493A60A42889A178ACC3289BB135C891D89698C38AAE187C6E3DB16335FA61BF70C6D496B5251BCEFA9A1C95980E3810C0059C62E8838F1B0B46B4C5A2FEA19E790B2EB4C8C3A164C8BF5C89C2812E982B0F3DA0CDE958A26BD03A38C562CC67B2C07509E6742CB44C04320AA87C23C3E3A7506F26AFE94523D1B05280BA53B4ABB8C5717422D071396C6B7733A09B11CE1E6B2280F1C9215913FBA6522F90C009C0988CAAC61721993AE73DD71A551ED8431C1A8D286857455624842C4CFA80B9143CCEBF930AA1E738EFF1A46EFCC0D766B7E4AC39AD508D6CB9891DEB61B0AAC5FB9385E1D0682F786CA37C3DF1A38BDFC1162E975EB604163752CAC6C47E3BD909C53726C6D084188904CA98C743C9B5D700CBE4A809F1756DCF4C65C5A6B7A7F2725595A0C89C26381C218004B1A275701B50586A327652390FB68868CFE8084067ABC53A9A2CECC72BC625CA7751EC158F35E791008543EB202AE258C588E69E695425B9BA4FE0082ECC530EBFAB41DB23CFA8C2A63AAB11D179C91A712062536C4FF1C205287296B001121436C5F813747350C9AB63CEC0CCF7DAB3E642210517155228910C729BC9B24B138B85ED9A4678B2B4C67A73282842EA66CC458C706BF4A591BBCBBD370E09C937E396B76FE4A3B56B4CF638A5CE055CB63C1275D53B4197493A1A4309A4CCDADC3AD1F47A5E8C5C89235321028EF158094A6385C4E010D6F8CCF1C627BCB3600544B276D2AC9CC91D4BD5AD75DBCC8E7B7A981680212B5A3D395F8AA1CF2B0A23EBB63BDDC5185BE53A6C1410D0D96889A74265E3B34F4477FDF5B680D793F35C7A372B25A1F47C5875B34B80ACA2C25A0DE69D58E71856C55E37A79BC7376898C45BDAD66FD0A554D8F9BD69A525BAA4BF40B0AEFDEC66EA329ACF7B44D33C4FA248734F516BB0A69FF751A3E3D95975DC4E25194CD6F88E7264352628AF45B38A3434951FF99CBAEA812C04C354227431B01CCF2B5955B59BBB5A2BF382227D71631C541AF888232EF733A085AA1D14493C063B64E8BB28E3B7D0686CE8F942EEC58734525DBAC07159627863D97F7C198C50E9AB10E54979C394E90395E6A793C882CBA9D56179B75F11799709577F149CC93EA3A764C610EAE641F8FA2801A22B5686B335117C3C7B3D74986F70384A26A33B323787B7888CF873BE39411829D69D6E2CA2279971AE27660B5224D21015440844C457B6B9F2C50D19580489C63AE0612D423A5CC523B2D908C45907A6694A665195171A5B2FB583A5C240CADCA8F0E83E46B14052C9620D3B7EF386CE8B9A5E873B65693B0D341C6EB2D10CE5E937CFB8C4C9134401BABFEEBBAECF47113A34B9C6E011BDC78A54F2B7BF36A5FFD27563D7443F2109F02A64C421411DDB2D1404A86F793A2DE62CDC560BFD6604D4B6330BA6AA621414E8C12DC71C25652ABAF36B875DE1978DD209AB53B885206C3A1B4F8B4A0670C087CDA9CDA7997437155659255C2D024822A448CE5157CF5B6E4C495A949960886A902C79591120117C4A73CE7B380C661851E1CA9EF1973D8A9D2A191B938C4110259C4227B600BA7EC9B033BB0300715032836573382445435A743CA61E923B18ADEC7CFAF10ADE908E582560EE91ACA012942319B4888109E55AA738A7BCF777C92B4B09A50A1C043C982C2C2357F73C1687B35BD123FC905E1A719353466A42B915DBF1A1750339BF0923419681E4531D97E2160AD896DB056570570510FB711169AF2DE0CBA51C5F5056242965AD429301E7020AE0141F845833A3FBA0B192426C001A7147C2926805CD86725442CADC2636BB769DCDE46D1BD12D30F4695593B5753870EF796FB2F3A53F283D5828B77CB75D5DE1BA25357C290A957FD501AEE0AE59D7AE97833B0BB640F781A08BD256C79117C220BDD83280A0069B29A645720096D297A2E5245439268C0ED01F75A939978372B9E05D93DA899C10BF6CDB18698C46EBE00BF90730E2EA393014461DEC6C87F17B2EE16C13B8507C6009BEE074F17367A5FC3067A28B7D804C32860EDE650E6FE85CF6E301D1B1647323199CA296ABC54D2811507572B5DFF92B54E3786D130938417624775D8534B0102B6B8006803DDB376EB830D1CA80E717BB7F260A5CA4A56BFC5DA790151725942AE7C42B2B9E385B4E0F995D4402161070B73A6BB0CDB77EF11B1286D75E315635E719088DC7909D026B198AC93BB4B6FE395843A4428F75C0C1448C605A8CABA0B8CD19CE465764B523628B3334E3885D68D5089E1A3045840C36A73AEFE7B93AB357FD8A46D7547A8EFB243E4953E67CA72CFA0B77835768AA0CD2D976820A97BC21C7033084AD45C0BF6B483ACA8A485641EB55A47BE36ABCEB96143BA90C515D5BE8513BB994CFA88FF4B3600E34C1E656877606B6280384A0F481458044C47732FA9B58195A5DFB48636E1558C56A43CB6941DEE5AEB1E27B89A7121BE166879B62BC01619A9ABE840CC678E028E9BC71CE233FD9DB8816294D71F1A080101912920534750DDE692F782BAC4D4481A0900E6BB952ADA798EE06232C200F57F76A914617914B7398A0433CD7A11B5AC09789034F39338CE567E3E7AEFE35B0C3B85D21506E8886587670761AF9BAD3261DAF22CBFC664604234B3B784EA001CC6702B9222545CFDB2965EB54678780EE3C9CC134CD2E655908D6BDF460BEE364C66D5ACCF4B492ADE9A0F3EB31995BADDE4628B67165FF6014D848541035CDA46949EC1C12FF492726A7214D1C7273FB85D5484E5A178751B56E3FB163D13A53C7B3038E09B847A8C06FF9B42E8C345CC95AAC1A09660AC1FC7A146E7845AB83390871655E604C4C009EE924AE107B61BC3664F488AC60783A1C346BD18C56CED3F03BC1B1E4075E9785F235EBC5CE6621414E77D52CEC3B2EBBA283F4C993A010081E2CC571D97234472CC9858D199CF0D6E6B9BD720C2665DF0F282411F4A071489A8F618E2AE5AEF40131CAC5233D6D731522720C2FEB1C\"\n        },\n        {\n          \"tcId\": 28,\n          \"deferred\": false,\n          \"z\": \"5AA6DC620A6E9A60CF19A7B4F0FF805BDA8219522A548EE5857C3FF6060C7A2F\",\n          \"d\": \"092271D05CA63C60880AF404D60BC4BB9539E2EA12969581898D56E0AC9A5A68\",\n          \"ek\": \"E1F90F4586A2A7444812451655F63852C48D2745BCC5D95C15552CA7355A216B1B5131656A95453A854DA8291046A05D96E74CC4507D31973D9606171D8405F211AC5040658411A3997CA061C3AD30EC2AE6CC79CD4C9AB1D1CB47996F02E42BD8819F62457CA5CB9923C570FC749531C61AEF02642576A04E88493AB084AFB353FC0B032AE8AEA812373A323268200FA820C88E1881F0A0CED7D9601DF56C891AC2CF6B299C553C6B1C8A470B68CFF347C2A071B26557F185B4E2138B421A9BB6DAB8FB41C5459644F08614E63C8C4BACC3DF5AB7F86C44E48239EF387217C9540DFB50002C08ED9CB631755446786D4B5BC14D16C5EF629CE2916687C40053A2CD50667CBB590F7D3A2AFD54AECBD6211C84739AB75B80A38E9F27B6D6F1BD4C838BB2706E5DA65B95498CFA61AB90169A2C06B0E79CBAE0051683221C98DA365A27C1DE417666ACCA178717934258207A51DFFA0C926B6E3DA5B084F07560D949AD615724C306EF1165A5B9616FBA84C7D71C1117BBF8296722012EFE25B29C63291D31758278430CD90E844764AC252F33135CD2137115933B38F4160FD482CBD9265C27AC3B6582FC201DEB7A52D23AA5B77BCE9B7C6D699655105B9883830D0171882612212272261A0CC9DDCBC7D3439FF3A01B0BD4B63972263D919BCC9B95018114A11BABECEA27A5BCA3DB896AA49543CC50BC07039D31135BE1354B6A2B6B4375513010CAE856B7AEF64BCE20912432C09FD18905200249D4CC250306C341CB837A96F2B67422B63C29FB8887A962A1F743F3D01795D34E277343E7577878F5A3EC02728E9238D56B2115F680AFC70BBB361B60C10FF7F4094FE240089577D59969907B9192097CC05516A7132C2477435C8BC01909B4AAE5537CA2C6AC79806B6B5F32FB688C609200F16279D9CA987B68EA83A6D6309F1230562196BA93767DF126C98E4C3A3A0BB969629BCCDCB428A333D2B96E50B814716A5479192DCC0C0E4B194AED6A169E5074EF977F689528C997C1B99B02E1B18794B56993743456214064F80CCDA66B71BC009772784AF04FB7F468E2E93E03C18778D13C72FA149C50C1C9F45167A53E09657B50BA2A19B31FA95C5C6550B14F9B931EB51C37890C95157DF4F974E3A167DC005481F945D23780B5498AC5AB80DD8ACCF2D1322D3253B9450EDA3C3B365C9EDC4A87D089AF7797B01BE716917842A4E99CE04C86A9F172062C473C203A328C10DF171FB10C97BA6B8E71271D705110C810843D658B15F2040B385B067B1CE160A4205CBD57B74926143609979F6A888EBBECB7703498A278AE963223A8AA41916A3D37D949A3E298F01CCD36A5B6E0BA9CFF38BB890AB18869B4FB7CA8C1711798CAAB2EAC01ABA26A060266A6A91BA877603E650F7D15C24F9B23C52A9C74F43150E3A1D5D25BD0326724A42572C32944DA713457CB36B14E30F72761480035423810D83721A97505668F11EB26285A1709321A1C8016DB8BB085996D1A4880BD3B1D8BF2754F3781D57BBDE68297AF710188486EB6D4AF7DE411D36787E4D945E33C45CDE051601243A1F7028AD52B3B5C7728F35DD5F8994D4B8D9FA767611A1ADEE8B38C5A7A0AA795D0A970C749A06DCE6CF1C8ED19D1F7E9F1F25538877CCEC133881C652489A84F948041\",\n          \"dk\": \"4967CD2CABA6E5B9C671732DA64B59450440532BBC0372C570341637B81346646971834CCB116C49C562D485982B3C602D723B721A8EF9A35CA6CB045F8A09AB9A176C55801901C2924874D65573F5C0B3F97C1DB4821AC3B23F7621BEBBFC4D1F924E9E0762F037904707128ED964B8B2C42B3B1BA7D101BB8C1A36E1040ADA4CBAFC2BFFAA9D12C69C01F3C65E3676C948C18C273F9EB34EB0C00682A285E6B8A514D1AEE73AB93423C187C57C286801A9AB79F2F7100FB08E03A24AB26625D972C1350B951064A0C2122179CB11914C284BB092DA4A044E2C457807CED5662D0DC23F8D8A951C9766AFFB11D3B3669826736A278FA44386CCD5519F3A04A87B0C9D693D0E505EB889CBC90785635CC08FEB4362E3B48134474B43771BAB84A9933BE0988834CB149A5C3724BB17FDA374D5B57F5260C8E60C37F440A8B3DCB5DC94B946495C025CA1258C7CA7AB56B3765C1EE0ADFD854E617AB40E26922EC667FCEB3192D01DF3D37A484239BA427823302440AA439580074D666DB14C1D1F0C9E5203822394988553C8A0925E04F5AA8B9942E6C9B0C6A942CE569F3987CFED7B7E7DE388AC6BBB7CE4C9FBB6C5D15531A558573431C6B398044F989EE581B95793279F0AB97F4355D9C566B231998C9C046C871A59C11A99B2271CA7364ED5C5A6FCC0EF27A7C147C829C69E09D01CEBDAB91F163C68EB18D382A1A081889281414DCB456CD6C2031C382771073B5621C7B60DC4B0A294C8AA62C5CDF68BB6B46692196198C1EB2FC9528B33A0B829CB9B809C010A3054230188DDFA60013375DC1C6A967146D1B77362A448E4FA97C3B72C2AF5C9A4193290630AB400CA5830024888AADB52A9D4894B5AA03322946062D523018131645B825D5BB8DCE285DF2977B96C02977BC889737C78C2A3DCC5666B652C6E8C24141516DD8520DFE84E5129AA6BF55BB1EC79C3771B029A3B91F9701677C854E4105D5A485F8CB6A5C29CB2F47A4A60281F8B1FC8BC150122B08296B45F97C58CEB743B42000720CBBE5022B7143D3E177023ACA482988135197237706C26A94B35E20DE3CC0C53CA9626F2615E4B8D581BC2656AA72A0AB9242670E6322A89489C97177E3EA1AB9C24338AA35FA272C76893053A76051F4A88DE1944FBB0AFC8E904CD1033E7DC0D0ED029A7531EB612C7B46775FDC09B54C483F6B06ED16427F50421B6F59C06FB0AE4F120C54644DD287CE3119E440AAA8E0A611AB9B52DB1B445036E2CF15BB8DC72CEF50DC3788BD85832D0C18B2685659F8A8BD55144A4EC9764109288B21113E4089E598BBA1453041C9717AB25BA5239FC54638B5A20247B9BB755A360E16F83246CA2D024CBD4BC8E966C2F102C6C02CEAABA0F92874179C8777F937D9A3CB74920BEFE6A759CC94DA0A3ADE2D739D43A99E1F06A0D6A41AAC076CA70171BD697F1CB16A3B481EABB2269B57D36599F3B734BCECAABF6D5835E365DF0261C5C11B8B5314E08EB209A8938B9AA6566E159E2472D97553972DAC5B83292EA350AE358C60FA7773B5C1AF64891C72643CBF8085176A05CB47577E50FA6D42E96C5A465E05C7DB75BE4262A7AA58090585A62363B6C989B8274C426802DE1F90F4586A2A7444812451655F63852C48D2745BCC5D95C15552CA7355A216B1B5131656A95453A854DA8291046A05D96E74CC4507D31973D9606171D8405F211AC5040658411A3997CA061C3AD30EC2AE6CC79CD4C9AB1D1CB47996F02E42BD8819F62457CA5CB9923C570FC749531C61AEF02642576A04E88493AB084AFB353FC0B032AE8AEA812373A323268200FA820C88E1881F0A0CED7D9601DF56C891AC2CF6B299C553C6B1C8A470B68CFF347C2A071B26557F185B4E2138B421A9BB6DAB8FB41C5459644F08614E63C8C4BACC3DF5AB7F86C44E48239EF387217C9540DFB50002C08ED9CB631755446786D4B5BC14D16C5EF629CE2916687C40053A2CD50667CBB590F7D3A2AFD54AECBD6211C84739AB75B80A38E9F27B6D6F1BD4C838BB2706E5DA65B95498CFA61AB90169A2C06B0E79CBAE0051683221C98DA365A27C1DE417666ACCA178717934258207A51DFFA0C926B6E3DA5B084F07560D949AD615724C306EF1165A5B9616FBA84C7D71C1117BBF8296722012EFE25B29C63291D31758278430CD90E844764AC252F33135CD2137115933B38F4160FD482CBD9265C27AC3B6582FC201DEB7A52D23AA5B77BCE9B7C6D699655105B9883830D0171882612212272261A0CC9DDCBC7D3439FF3A01B0BD4B63972263D919BCC9B95018114A11BABECEA27A5BCA3DB896AA49543CC50BC07039D31135BE1354B6A2B6B4375513010CAE856B7AEF64BCE20912432C09FD18905200249D4CC250306C341CB837A96F2B67422B63C29FB8887A962A1F743F3D01795D34E277343E7577878F5A3EC02728E9238D56B2115F680AFC70BBB361B60C10FF7F4094FE240089577D59969907B9192097CC05516A7132C2477435C8BC01909B4AAE5537CA2C6AC79806B6B5F32FB688C609200F16279D9CA987B68EA83A6D6309F1230562196BA93767DF126C98E4C3A3A0BB969629BCCDCB428A333D2B96E50B814716A5479192DCC0C0E4B194AED6A169E5074EF977F689528C997C1B99B02E1B18794B56993743456214064F80CCDA66B71BC009772784AF04FB7F468E2E93E03C18778D13C72FA149C50C1C9F45167A53E09657B50BA2A19B31FA95C5C6550B14F9B931EB51C37890C95157DF4F974E3A167DC005481F945D23780B5498AC5AB80DD8ACCF2D1322D3253B9450EDA3C3B365C9EDC4A87D089AF7797B01BE716917842A4E99CE04C86A9F172062C473C203A328C10DF171FB10C97BA6B8E71271D705110C810843D658B15F2040B385B067B1CE160A4205CBD57B74926143609979F6A888EBBECB7703498A278AE963223A8AA41916A3D37D949A3E298F01CCD36A5B6E0BA9CFF38BB890AB18869B4FB7CA8C1711798CAAB2EAC01ABA26A060266A6A91BA877603E650F7D15C24F9B23C52A9C74F43150E3A1D5D25BD0326724A42572C32944DA713457CB36B14E30F72761480035423810D83721A97505668F11EB26285A1709321A1C8016DB8BB085996D1A4880BD3B1D8BF2754F3781D57BBDE68297AF710188486EB6D4AF7DE411D36787E4D945E33C45CDE051601243A1F7028AD52B3B5C7728F35DD5F8994D4B8D9FA767611A1ADEE8B38C5A7A0AA795D0A970C749A06DCE6CF1C8ED19D1F7E9F1F25538877CCEC133881C652489A84F94804166E5248CD311286D6DD03E010391D90D76044BF498B53C9D8202A9EB643527395AA6DC620A6E9A60CF19A7B4F0FF805BDA8219522A548EE5857C3FF6060C7A2F\"\n        },\n        {\n          \"tcId\": 29,\n          \"deferred\": false,\n          \"z\": \"7CF50F7237A97072F03F31CFD59FA8E863BCA3AF7375E0CA698FF665661C24CF\",\n          \"d\": \"BBF7574CF5F32BE49E1F39CE33870D9D6384056D60D223003B6B0C10D5C42180\",\n          \"ek\": \"602389F7CA3437B9197677CB9E9704A2BB73A7815EC1047D8D63A55CE1184EFBBBA3F701CB0C3D0D18B757BA23C6023B4D34964B66107C92C5E0AA577FB93F31FB9A73786E63E7CA4DA84215F6B05A883C19F8B0D0326025A41A98D056B70A18E6E6469EC63C80BA0B7EE330B89314838883BFA75F2C6155BAA1922FD446235CA76A634EF715776D3AA3728482C5F69931DA1FA0A406D75756D025C08DAA28EB2A226AC56988F68B54E3205C1B341528374B9B9BF07BA42BAAC34219597FDC66156155DED5A7C3F386103BB0EDA1CB1D4258CE1A971447075CAC2AF538A96F1C570014341624607A3C36BB4771DC99916EACCC04268D25DB95DC20B041B394FAB543118B74536187EA32BA1680B006652AEFFA9338FA00BC099846419630D38CE7D726C5CC84CEB9C154E9B309B6A99BC142CBD6B210408455704A3A644ABF7768E6E87B54734C1CD0C139B2292C612DDAD0AAFB239CDAB80629B91AD9585DA88A84857249E68595C0564F2A07735A76C1CAE64D28D14A191A9F0CFB709E216AF6CCC0654AC206B722A2E84BE8A0C13BA359BB1B741F191F076A7B27849D2DC146CD8456FB165157A2ADF2A45FC5647FD5213E8A105084138CF29A583A0B5BFEB0A523798085F2AC8A44745BD556D7C0959EC319563077F5D5137E8B7E9743115D390BFFB91DADDB0DA4A21433B963A15933FAE236A537837CD056F4145EEB7872E8153E0BA9702C7A20598481C630206EC359C5145BF123A18054AF7D6851B59707B010A0BC322D3E3CB0F0BBA619C0CCB5239A5D7B021007211024868DF03B4729578F029EA7DB1CE62C15F1843C16469AD144C805B5C217FB18CFC49CBEB80BDB9CCACCB181153C377997C509B6606DB808F04911FCC4CF1902035DC3016F64498C13AF1BA2C240C6567E5520FDF79A5A04634439641DD2672BA04B11931CCDD3629D860B9EC767C3AA4C9E09BE25A7752B64087B0A973E6B3278E66E89F4BCCBF3090A729467A88FA0AB1A6805C7ED45678E71BA79A85F267771E456B2D56627AD66C7CB07B8D73A69A2682C79B1C13986C07FC7986C7C3C84EA4B6A6C1691675912B2064E3598C32573B6B87E67B006E2F312F15A5AF4E237345468756390DB585514302A80746E985A104C43BA019C5DDA678047C6A48CA36A51CB3A333767BC35CA31B59CCD64A2C59AC884A8B1FB396FCD42C63057663E8A1F809709AE89AC805277E1151B92DB4F0E52923BBC093B7C6D15621D2BEC2C8DB77F25F86A62646C54C114BA03CC6154397127A033D402248383FAA8BD1980C93E123B57075633B6B643941B417890B1AA810C88461E60413264612B890FA83115D22041243C47263B57CB32BD84D839B9CB96E3777E2DC616D8ECCD1BB21D4AB00C83D4B4298A671CCC1FDDFC3E4DBAB48F1B8045D3991B3952D4799B7E1C3D31B74F9B619BF17016D143B16E593F64C08A71078C83B197BD530063D026367993E8C35305D211C2E321ACC889FC6917F70A33EC2AC4D7FA448A79A89B531BE89A59CDB8212E7715144721613619ABA2CC7E53A3D3F84908C09B26320FA3C1CC9D840F21851139236C7CECBB8B3C2681DC145AACC13B4BBB15560793364387A0CFC97C0312351F420A0892084CACAFEA4241305B12C78DF29F2FC5FFEE800216C1DDF275\",\n          \"dk\": \"7BAC37F9C7AC728C78DE12B13C0A2C1B522A837416529A93273A4A14007BADC7BBFAF03EB90A7CE4E8B87B70986806C9DDC9BFFAD06D51C081E8866E7624A373E87E8BE1A60A861650346296F8B89C7A0EC713CA9A0B9BBDBA370104A414C02DE8445B9F4314D4902EBB400226A827DFC6603812178786BB1D6A9CFD048A45B4A160333D73E344497BC526D262EB73AA9D3456375A9F237B004B2414171C6BE0F02A97E83B1F8C411FE350D9B51D42D11BB7A34F8C59AFAD18722B17B21C5253389A98B2A96E5BD798AD10AF6F27950F64638080C7F0284545BA6E2BD93BB141CA924966735C7383631EEBEBC1BFEA7160FC60C143C9359A43C3A03429D663196B723CD35FF4C465B356A77A7C5FA9408E91E08BB6F141F1A9B93B4452843873F5958AC7AB9C8A660BEE218598D09652F921E8980449A8087857B129B02A38FC664F0C21B4E95E3B400AF4B30EAF649B9C968C0AA5B2FF8781F8D01534FA5FCECA771B2A2ADD27710DB25402E94F66D37D52780E25E4317FF80F0C682641301015D7900A312C9643B336AC53C94889342B09138689E8BBA191700791197B0F804BD834064CB3446C9382E6839F25087F5DFC1C741B91A8FAA87E529E375A0CA9B424C429BBF0B6CC7027123EC211A9A5622611536290A0E30A50CBC567D08245B54224133923AE399EEE6454E858352333164510A8A510289F56B073ABAF82F5986BC52778A86253BB96E357AB4F78A7C3159DAD184B8B540BE1DB861FA1951C0C59526305FF005D741B2A312B586D75B72A6B31DE68A07DE4CFF0490A85009442390FB5F45F70F5AE3E286B20C35F55DC39DF6C7CFD992A3203718A58C2D849CFA78C4F9FB20D68E8A7B35065D3DA738F01C6986600677325AD146816137221E3C47B9376A3669CCB23A533A85B1028865BB4A99361486E789C0C42C3D36831D5E86278B48B2EF8042FAA2491B413FDD9AAC2493F44686A649BA866D30F887580857B7159161D1D009920033F9EEA92E37267CA936D0823A8CA195458731DBE45B25F46584268787D664FB7587AF4B67291255C66157CE7646B2F4B5015D75D5332C9E95470F52A9D8EB5438A390BD7254924D46C583A7892D53D42E9406E0A6063121A03F0A550F4AE2E8042D962A3AB69B223C37071F1BDD2BB1639D34041D1BFC2576B268ACDA311A5E0222DDFBC5D67A77EA0C37C90B474317195D567351280AB0E5840A3F60EA9C5162A706550877430F6882A68006A683A90B38F26C90D3FCC780C0CC7E6E88FEA5A02D912A1C485667F88AB329552A93837858235E98044A28A24E1F71435E09DE2AC2744BA2E116A136E95AED53BB906BB46FA214E218A815966214F581C1134545DD05EE4E9495AD172CB2BB199D79328E06A45A42A4EF37443EC6F57631D0959B84A1B169297A0F2DC16B13B1C43567C043237AB18BD53A8898F6ABCB0F8A012E2CE203649C4286C6D3C41346599E953B6899712D4B86BFC4B54A89B061E93B423793CDD798E6E5A1BFFD565245788B0BA6A0FA3BDD97960C9CB0A10DA892D829925FA4F85CC8FF895500933BB78B3993CC04006819FC93224D19A02E11651E5D3AB3CFA6DC1C206F6516A5CD5108D18B47960A6602389F7CA3437B9197677CB9E9704A2BB73A7815EC1047D8D63A55CE1184EFBBBA3F701CB0C3D0D18B757BA23C6023B4D34964B66107C92C5E0AA577FB93F31FB9A73786E63E7CA4DA84215F6B05A883C19F8B0D0326025A41A98D056B70A18E6E6469EC63C80BA0B7EE330B89314838883BFA75F2C6155BAA1922FD446235CA76A634EF715776D3AA3728482C5F69931DA1FA0A406D75756D025C08DAA28EB2A226AC56988F68B54E3205C1B341528374B9B9BF07BA42BAAC34219597FDC66156155DED5A7C3F386103BB0EDA1CB1D4258CE1A971447075CAC2AF538A96F1C570014341624607A3C36BB4771DC99916EACCC04268D25DB95DC20B041B394FAB543118B74536187EA32BA1680B006652AEFFA9338FA00BC099846419630D38CE7D726C5CC84CEB9C154E9B309B6A99BC142CBD6B210408455704A3A644ABF7768E6E87B54734C1CD0C139B2292C612DDAD0AAFB239CDAB80629B91AD9585DA88A84857249E68595C0564F2A07735A76C1CAE64D28D14A191A9F0CFB709E216AF6CCC0654AC206B722A2E84BE8A0C13BA359BB1B741F191F076A7B27849D2DC146CD8456FB165157A2ADF2A45FC5647FD5213E8A105084138CF29A583A0B5BFEB0A523798085F2AC8A44745BD556D7C0959EC319563077F5D5137E8B7E9743115D390BFFB91DADDB0DA4A21433B963A15933FAE236A537837CD056F4145EEB7872E8153E0BA9702C7A20598481C630206EC359C5145BF123A18054AF7D6851B59707B010A0BC322D3E3CB0F0BBA619C0CCB5239A5D7B021007211024868DF03B4729578F029EA7DB1CE62C15F1843C16469AD144C805B5C217FB18CFC49CBEB80BDB9CCACCB181153C377997C509B6606DB808F04911FCC4CF1902035DC3016F64498C13AF1BA2C240C6567E5520FDF79A5A04634439641DD2672BA04B11931CCDD3629D860B9EC767C3AA4C9E09BE25A7752B64087B0A973E6B3278E66E89F4BCCBF3090A729467A88FA0AB1A6805C7ED45678E71BA79A85F267771E456B2D56627AD66C7CB07B8D73A69A2682C79B1C13986C07FC7986C7C3C84EA4B6A6C1691675912B2064E3598C32573B6B87E67B006E2F312F15A5AF4E237345468756390DB585514302A80746E985A104C43BA019C5DDA678047C6A48CA36A51CB3A333767BC35CA31B59CCD64A2C59AC884A8B1FB396FCD42C63057663E8A1F809709AE89AC805277E1151B92DB4F0E52923BBC093B7C6D15621D2BEC2C8DB77F25F86A62646C54C114BA03CC6154397127A033D402248383FAA8BD1980C93E123B57075633B6B643941B417890B1AA810C88461E60413264612B890FA83115D22041243C47263B57CB32BD84D839B9CB96E3777E2DC616D8ECCD1BB21D4AB00C83D4B4298A671CCC1FDDFC3E4DBAB48F1B8045D3991B3952D4799B7E1C3D31B74F9B619BF17016D143B16E593F64C08A71078C83B197BD530063D026367993E8C35305D211C2E321ACC889FC6917F70A33EC2AC4D7FA448A79A89B531BE89A59CDB8212E7715144721613619ABA2CC7E53A3D3F84908C09B26320FA3C1CC9D840F21851139236C7CECBB8B3C2681DC145AACC13B4BBB15560793364387A0CFC97C0312351F420A0892084CACAFEA4241305B12C78DF29F2FC5FFEE800216C1DDF275A918B39F71BBB2C10DB35639E5FD2CE621868CC02149E029EB47899407D963007CF50F7237A97072F03F31CFD59FA8E863BCA3AF7375E0CA698FF665661C24CF\"\n        },\n        {\n          \"tcId\": 30,\n          \"deferred\": false,\n          \"z\": \"C593627807074684B7D363441F80F6A3D185D67878702D33A4E0BDA2000F857D\",\n          \"d\": \"D12CD9B65B7C58B2195AE0BE0282527BAC06C2D25CB0472628D64715F7F6A378\",\n          \"ek\": \"C85428E8EA5D6D1C7E544703372498F68311C32BBC70B86F2A805FC94089A0421AD680053D5BB139EB95652ABA561B07B9C2639AC693972070F351A3FB6138FEE0A73BF63161B604D7DC0334D6C631BA25F584952045C6CB74A31581B866EB5FB69503A5E3C6F96652547968626CC9C6ACCFE9582778E928235305CC5447661A64363A9FB3CB3720868812B2A3F5E7820DDAA8BC799566773BB62B769A8C54E6B533803A48D877706303A76BA2188EA4900155728DF29E7AF050F8CA9F92B65ED59988496419070B0CD8E964B402491D134F59EB2E3995C1B3B654EA7A5628C677858208A7958C57C7CCC697677BC65091015AF9D6C688E234DEB528608A1AAF35C2CB2A14593376E1417098778439D9BD6EB31BC0B53F277BC6763794CFEB8766957F6B206D439BCDCE4466BE20B74F7A117D7102F4A2CC93B098795146CD2903D131315EB41E0DDBA6B562B8341753DBA98D1ED288D8F33148814847981C735600AF76BFD0B964521A8B3122B3B90BA8228B1C4C834C808550D7780BB33218F155752DD15713F5A32057C42313A6FF5BABD6AAA1F6F5460C774CB83C6EC16C84D4A5B8EF728E58E8B34D7C090692B73DB64BB02AC365800915D78A9452158FA210C99B608DA053F574AFFAF1A49AAA92B3058468346D9D5A6DA97A9269B4713AE17A214921AE76A90EFC5A4AE90147E154A562C1C96598DF670EEED86EB946C9B45460C63A1339BCBE23B4332A32C02A028A892478070624D35C950AC84ED7D1B5640A70A3A45BB47C39EC181F7C8039929003D4D674DE3C5C89B0678CF3B125C864E95439C20AAABD015AA52BB3C27316188A673785CF2C0C77CAAC7604439A69B46005EC646C37C510FA835CBB36C162AB7C944F8228A4FF32340C3C63FFA3CBC48C7E01D35A6DD010E211C21175171237067A614CBCF7B18CF98ED0E8B88B58555D871564B0B71B75B5D078428EE87E0379A23822BC34C00A5661602C4006BF5CA667780E81F1759CD63E6BE63CC6C07ECF44C2E11346E376733D185A281C8648A9B951E14308D7A55A0717BB5C9C3CD5C7D88636873453D6148E674093A406860028AB50FAC90349C5BBFC2A7B6A18E24655DE541356990BA1C7C465630475176A03D58059F26700CBC383A0750F5271B8D5BDD74468E98C189FE302A5A452D2E47340C2B04A0275B47229B2022198F14E674071C6568EB428B0C0748CAA0A2F8BC28FD27862687B452989B034072B69381CF73BA00F7B7C401C699BE466593918EA96C79A3980023B299595179065198F93C6D6456DD53B586E6079340952C1314A140605DD34126291396AD22F2C7820A4AC793768707E071772EA587FFB9AEB234ED7A250DB40A1DD7A3869104864392B6DFA23013C8CF8856EC8E30C0AAA748351284F67A0E3469D3CF7C28A85BAA2630907B70F7C31870E8A29DC7B908CA880BC03C3CD2CC7A57A9D48A238667776B6D9C77247ABEA49B0F6935AA9446F0F2CADCC7A809B651DB55131A7B662CE428EF89109BB33C4E423B05781C42BFB4514092D35BA6D8E77B71F6627C9D91EB3672E656A00EF64CA7551667882444DA47FEE440B0E59755FB33039FACF337CC572D7CBACD680CB882ABEB86D9A3937FF76EFAF15E6AD37597C50B3153DC8B18625508393935D2FBD49D32ECF\",\n          \"dk\": \"9FC4A82AB21F667A50692A482D59A06FF2620ACCAB62394DEDC7AA452761F38A5A499C8091498FD7A5910C1A14324685B9307236CC0773D98B36EB2136708DAEB21F8D4B713CA5456A02C1C8EC012514AE3FF6476D1C3D8D47638AB79AB83433B6592E2F1690BED646C0519EAD1CC58779786B384618604B1AD1340E178FD97A8B3EE4B009A5BA115CBC3832B92ADBA22BBA7823976683B8A4469A3BDEDB8FEF5C3582A5984CEB2CBF5A57C96B246D05BA21F08B8582CDB5632DFFB404C320A004E878E31B877C07B38B7A12142A5628D2BA4CA6680C9B9A6CF18B3B4673962533D7C503A24C0A91EC9E1F75219E573DF630724393A18A61178623224B9C4892F6A41657175D296634474F1C3BBD76E05B9C5788B9857655D142BBD8137F047A4927B923163493FB31EA5C151A4719ABD61634F8A8856A968C7538E2641D5B79CDC78971ADE70AF4F85B09770EA4E05642CBCF02F095A9149DD266809DE0911A5481CA22176BC9B0C5A7066A00434B15AFFF5C532E1042BDA52D425CA202A599D96A92D58B7B40F8A76EA34C6B45B76E6C07FA9CA20B3587DED93E94160AB0412697DAC2591530620242A9B41F37B11CE1347075271091B78EAC2221DD735DEED8189C0029099526E9DA24F234BBB423C590CC44C675AF125B021951576F5953CBD45B8F6326CA9399D3127EB430AA31F2C5154768A7E440F63738419834F41AB4A149278DE5882506085943BB6D19088FC11525973ED9132238A350F201479D9469CBB46117E7B74C0298B5E785F27384A4984452868BF5056D9A966D844ACD946073AE275D7F82C4583C08AE33499CC53D4437C3EF967F6C904B75F503330C4B19981B28CA8F26CB9F5604081B8706E657CB8E9431466CA0EBF1B71828A3238578BC2B38620C3416E84B3058174716A312C205ACFC481871588C37263B6839768C5CA575CF15058C52F39379B79D814788C19034EA17BE4969255EE767578B0B6B347FA9A52D199891B9976A7345B93C659CBD7814F5F300900581674BAB7D03585E802C8AEA7CAD748F5065039788B76813C7F991A642C44EB1F151C6A3BD96C4331F2C01EEC46A3A1BC30555BA7F2C9316F4CC7D18C9CB73962A71CD0EF32B121393041684CFE692D4834FD0B25CB0A04531E656706A7D593AA1D0FC56FA304CCA3B5BB3233E7B141B6AD1C448C35268C99815E451C2B821F034CEEBD748A72625F61B610E478099F6CCCD39BA2A050611894D5524257D9A06D3BA2BE2AC70EDE8717BF593FC159F8433CA0C54420453610DF451C4736512702A95B2B0FA5AB1B4CC3FA74B974A1354F8B2BEE35187BF049D0A8772BCC85443B04303051415324523DC1FD494268E172383F7570D366570DAC588A73FCE3104921B325E45A87BE798683A6C2D0C90A869BB2B4BBC332B7687C81A287853F7C06F596271F5596FDE93BEBA027977C788F472C114C889BA4C4D3F39C7A05A34E57A8D5C4C10A19C3A3BAB1711FBA07854824305AE9D46A6DD11331656C4267C20B511B6672C0CBBCB2D5B67973239B116035BF38C9BED8186762201CE295A5473A825C6210B70A70CC5C5E235384908104278AFE955628D80B43F13BE383A42BDEA6D05EA7CC85428E8EA5D6D1C7E544703372498F68311C32BBC70B86F2A805FC94089A0421AD680053D5BB139EB95652ABA561B07B9C2639AC693972070F351A3FB6138FEE0A73BF63161B604D7DC0334D6C631BA25F584952045C6CB74A31581B866EB5FB69503A5E3C6F96652547968626CC9C6ACCFE9582778E928235305CC5447661A64363A9FB3CB3720868812B2A3F5E7820DDAA8BC799566773BB62B769A8C54E6B533803A48D877706303A76BA2188EA4900155728DF29E7AF050F8CA9F92B65ED59988496419070B0CD8E964B402491D134F59EB2E3995C1B3B654EA7A5628C677858208A7958C57C7CCC697677BC65091015AF9D6C688E234DEB528608A1AAF35C2CB2A14593376E1417098778439D9BD6EB31BC0B53F277BC6763794CFEB8766957F6B206D439BCDCE4466BE20B74F7A117D7102F4A2CC93B098795146CD2903D131315EB41E0DDBA6B562B8341753DBA98D1ED288D8F33148814847981C735600AF76BFD0B964521A8B3122B3B90BA8228B1C4C834C808550D7780BB33218F155752DD15713F5A32057C42313A6FF5BABD6AAA1F6F5460C774CB83C6EC16C84D4A5B8EF728E58E8B34D7C090692B73DB64BB02AC365800915D78A9452158FA210C99B608DA053F574AFFAF1A49AAA92B3058468346D9D5A6DA97A9269B4713AE17A214921AE76A90EFC5A4AE90147E154A562C1C96598DF670EEED86EB946C9B45460C63A1339BCBE23B4332A32C02A028A892478070624D35C950AC84ED7D1B5640A70A3A45BB47C39EC181F7C8039929003D4D674DE3C5C89B0678CF3B125C864E95439C20AAABD015AA52BB3C27316188A673785CF2C0C77CAAC7604439A69B46005EC646C37C510FA835CBB36C162AB7C944F8228A4FF32340C3C63FFA3CBC48C7E01D35A6DD010E211C21175171237067A614CBCF7B18CF98ED0E8B88B58555D871564B0B71B75B5D078428EE87E0379A23822BC34C00A5661602C4006BF5CA667780E81F1759CD63E6BE63CC6C07ECF44C2E11346E376733D185A281C8648A9B951E14308D7A55A0717BB5C9C3CD5C7D88636873453D6148E674093A406860028AB50FAC90349C5BBFC2A7B6A18E24655DE541356990BA1C7C465630475176A03D58059F26700CBC383A0750F5271B8D5BDD74468E98C189FE302A5A452D2E47340C2B04A0275B47229B2022198F14E674071C6568EB428B0C0748CAA0A2F8BC28FD27862687B452989B034072B69381CF73BA00F7B7C401C699BE466593918EA96C79A3980023B299595179065198F93C6D6456DD53B586E6079340952C1314A140605DD34126291396AD22F2C7820A4AC793768707E071772EA587FFB9AEB234ED7A250DB40A1DD7A3869104864392B6DFA23013C8CF8856EC8E30C0AAA748351284F67A0E3469D3CF7C28A85BAA2630907B70F7C31870E8A29DC7B908CA880BC03C3CD2CC7A57A9D48A238667776B6D9C77247ABEA49B0F6935AA9446F0F2CADCC7A809B651DB55131A7B662CE428EF89109BB33C4E423B05781C42BFB4514092D35BA6D8E77B71F6627C9D91EB3672E656A00EF64CA7551667882444DA47FEE440B0E59755FB33039FACF337CC572D7CBACD680CB882ABEB86D9A3937FF76EFAF15E6AD37597C50B3153DC8B18625508393935D2FBD49D32ECFC86A41EFD315191F24D2E6BDD87433D5133D6734FBEAA9DA8043D91950000048C593627807074684B7D363441F80F6A3D185D67878702D33A4E0BDA2000F857D\"\n        },\n        {\n          \"tcId\": 31,\n          \"deferred\": false,\n          \"z\": \"E01702E1228F530AC96DB053A415BE97749A109A1FD4057BA128649B17EC07AD\",\n          \"d\": \"79C006D5470C229AFCE7588546E52204B09F5086974865B426AAAA198C6CBA7A\",\n          \"ek\": \"52764F398C4AED89848544114193553BB178FBFCA4883C76BE23363B3343F69C8AF0B59F7C0482A64985B50A6EEA5C700CE7BF31DAC43B82372405638010A320E2A28FE6321C0AB85BFA8723DB96E8D1508900996B27812507885C97A1E7C581E35915D0709C9CFB2BC713B305705406C522D0305F5BE51EB3272524A797ECC2AB5330538BCCA62CD51937330849138620032917ACC188E8B70DE56B43E812EF030C0B4C062B1437AAE13A355AA9B41B00EDF4151F2C964FD332F3014BEB990E19C2350050735C7B02C0936B6BD37CA18114854750E32A6790515E1B4A31B2B71F9DE4082C225679CB220522BA6235B89F2C8E231BCFC815B9EBB15534D5B352566942A742AB405194AA74B90B26B98B669DA43FEC232FCF351FA5C50F812057E44858096395CE67AEE48382063A7928CB4904D623C56974FAFBA18519BAB743168510700F715E07EB331050C4160A6AA024C126027D11C324A87BA13C3CB10E86B2212343BD0088382769B9494F769918DA7C8EED21896C75711AA34B0E3BA8527371271AAEA05C906C5CAF6B5A6A08C4998492323E04CD5E63B1857A4FA972BF8B430036A856A5F9183C4B652A9311F1901AE3A4A93D302C4E77282B04046F7AC2DF2333C21B343ADB75C369A5385B9CC70280858971716A0587B74492637DECC8130EC8418954763E39AE47BBB084103225C21F64D94F14D55B9BD2579C5BB03EE4915016C2CA416806CCCD56061F92C0B1EAEA50DBA36B76798F7B8AA8AFF2CC06B04BC3106F22845DC5D847B5D444F05803CBF062DE581E12163094A5C7131A24B9327DAFF9134F32B647D9CA52A65A91A4899D64100F3376E0D318C7B8188BFA88654AC370EB5D693C4D9D481CAA98A2C99C168A0306261165410B32A53BBBBA412B54702476101C6550C286D6615AA98D8FD95ECD88BF45CA383DB3C6239AC7AC16606877751311CE8B447D0F6B9499943706B5A46F291A57302FA4F3ACD4645DBB515868371FD79887636737E333C9B06285508832401107089176E06A2F17E19E4FF61623603E17367D33F023C2CC820961A6E3500092CA3B900BAD66C179249C75A5E08A5563BE1CE312ECDC0AD370518F66594314CF5C8BCA20948D54C70F800542BD5B64ACC9AB68FC90F48662CFFC8F801C06DE972571E8C25EBA7128E95C13011EF88240C2C9C0BA555AA1C70956C9621A600F2817435C09C75F8A08CEF2C5D31C5C5834A745249CEB7641E6B19E404865EB374507DB3394A77A0F47149896AF1C747F86C97201D8C1CC8209E571B8C4D6972ECB1CA930B15516922FF08BF0C942AB5C5D197669AF94457FC9BA5BB47DDBD3A34EE941943B0B262A63FD0922D803CA035C6C54DB71B5A5A76F46C787036BEF777296E80664F73A78175EF7DB567BDB6CD8D97D27649661C76216D99D6264A7EB274C507AA170E1C3F23CA00A04B861AAA29EC33D4584B12985AC3B36689F129F3D29B19DAA5DCE4150162A10FA835E1D89A21FE10E4B1489E457C415961648044774C49DE6908201068D13463F1A999D3103342F03020F043DF3CC2E897B8293D235E8B408F2BAB266CCB0FCE19F6779A9C7DC89E368B07BF56F29911593E78009358CEA0BA304C0C074FC86A0782F99BC38571CD1632770CBEA199AF4181984FD05FF371931\",\n          \"dk\": \"B9C74FBF945530C8B3F846A803ABA28337B79D4B282B127D727A145BD77C19D980ACF4A5B20A09D770BC96F9A32EC16ECC208126D0C755C334BFE36E0AA3B968D0BD9EB80FA4953C9B843AABC828BB044DE62AC903D655C5960F45A4133CEA1AE6C4A09D431B43B19511631483E457C9108D7F3AB090203F712BA759602FED0A68BB7C8B10DB592DC38606978C56198B7F3062A200B03FE281FF95A27ED48C630CC1699423F5C6A07F850F2280101178CA24C282A7455FAC19869B207C46540AA0E4628246777CEB02B24ABC1BFA02326B846D7A20A7E642116B97AE68219AC53712A0077E325B4677AD163A38A6B8754DA8957C52B6CA5905C43131467B86F2CB1B94891A88B79058D6C6D8DCBB2C167156DC81C59B60F66C16938935DCA2BBB80084484B1F11330B5010C2F6C5AADF352D2E5421BD5B8A77758B72A058A0A6074B3C08A9A2B0EFB7261CE38219542CA7759FABFAA924721D7EC8C11EB62976EAB8202AAF0BC082370A47735B0931CAB5D6A92F8A67184DC906856946004B991394C7F1F1B84A4C0BF3F249B73747732AC3F04C5FD94B33FFD8250AE4739FA04233B6C381F69A3B0AC7E0003613329FEF9A63F033CDD09137BE13BA7EF42F36342A1C215A5FF4AA76D044C0AAB7ACD7064BB064C9B66BD7E2C398404AF7164FD0169BC8DB926BF63DD0E2433C7C17854A7E538722E74118CACA4621A532B2FC8DD0E204DBD01B8B3C7AD1A5BC78E78E8C1265BDACB7C78A62A3E10EB7BC3E7FD25ED8827C23428E3A198CB4821B0EEA62C9154F6D305D00BD7BCA42A1BD488AC4412D39B1573ED978EB912CAB796A594C5E9AE6AE7C94C76BE61DD47354163A7E4063B8A592C91A8A6B34451AD400672603C60DDB26F1B0587BE2065362006372081C1A52110774EE4A0231842AC6E7B4345B47C6A5C42D3C2C951014A7B753A90345B0692C8A73A82D8B01589C3CEEC8ABA357931E18026604576141ABE253CA18C356757A9D0BD49C1FC1CD90364D7430B061DCAB362257F80405D35184282C8861613D6EAC28F1360B1888192B25719D7A3ADE28AB76FBAEC4DC35DE5B5CACD64A66A39824993F2FF7783CB339319998AA102369F43E841666CB41CF0D39B998267356DCAC4F0933E5E56CBD920FC56AC33BA82E80FB13C6C64932C65EC4C66E903859C4F20D1AB5825FA8322CDB41F4341811D7663A6397363212DED05DA0D76EB5308D32F4442A488665DA80F54ACD37C406FD44AAA56CB31781AD393171DD539727C6582594BFA278BD2729A9150B916A8CAE861153DEC3493E18277A766019CB556882266521536EF030BCA36115753E08762142B33C5E34A9E80716C57A03BB362F9334AC44744EBA711AD8F44C2EBC151976C49F179D24B53C30615BB42CBE8CE069714A9447D939DCBC128126C1F9AB35C222386D894B6ED50A580B3ED13ACAA18C8D49F5A5826A9D1B6530100702E2C9993C3AC5C9286AF6324EC9B8323C032884B344EAC49F21D76323DA9FCB324E8DD7AFCEE162AC14BB427C0197C4AB9AE490434586EA76AD9AA042A060973F301F5E9BC91072B61FBC27033C52DD160D424CCA566558242C9E4B145C7B5630AC825ED5B932A00381C5C94752764F398C4AED89848544114193553BB178FBFCA4883C76BE23363B3343F69C8AF0B59F7C0482A64985B50A6EEA5C700CE7BF31DAC43B82372405638010A320E2A28FE6321C0AB85BFA8723DB96E8D1508900996B27812507885C97A1E7C581E35915D0709C9CFB2BC713B305705406C522D0305F5BE51EB3272524A797ECC2AB5330538BCCA62CD51937330849138620032917ACC188E8B70DE56B43E812EF030C0B4C062B1437AAE13A355AA9B41B00EDF4151F2C964FD332F3014BEB990E19C2350050735C7B02C0936B6BD37CA18114854750E32A6790515E1B4A31B2B71F9DE4082C225679CB220522BA6235B89F2C8E231BCFC815B9EBB15534D5B352566942A742AB405194AA74B90B26B98B669DA43FEC232FCF351FA5C50F812057E44858096395CE67AEE48382063A7928CB4904D623C56974FAFBA18519BAB743168510700F715E07EB331050C4160A6AA024C126027D11C324A87BA13C3CB10E86B2212343BD0088382769B9494F769918DA7C8EED21896C75711AA34B0E3BA8527371271AAEA05C906C5CAF6B5A6A08C4998492323E04CD5E63B1857A4FA972BF8B430036A856A5F9183C4B652A9311F1901AE3A4A93D302C4E77282B04046F7AC2DF2333C21B343ADB75C369A5385B9CC70280858971716A0587B74492637DECC8130EC8418954763E39AE47BBB084103225C21F64D94F14D55B9BD2579C5BB03EE4915016C2CA416806CCCD56061F92C0B1EAEA50DBA36B76798F7B8AA8AFF2CC06B04BC3106F22845DC5D847B5D444F05803CBF062DE581E12163094A5C7131A24B9327DAFF9134F32B647D9CA52A65A91A4899D64100F3376E0D318C7B8188BFA88654AC370EB5D693C4D9D481CAA98A2C99C168A0306261165410B32A53BBBBA412B54702476101C6550C286D6615AA98D8FD95ECD88BF45CA383DB3C6239AC7AC16606877751311CE8B447D0F6B9499943706B5A46F291A57302FA4F3ACD4645DBB515868371FD79887636737E333C9B06285508832401107089176E06A2F17E19E4FF61623603E17367D33F023C2CC820961A6E3500092CA3B900BAD66C179249C75A5E08A5563BE1CE312ECDC0AD370518F66594314CF5C8BCA20948D54C70F800542BD5B64ACC9AB68FC90F48662CFFC8F801C06DE972571E8C25EBA7128E95C13011EF88240C2C9C0BA555AA1C70956C9621A600F2817435C09C75F8A08CEF2C5D31C5C5834A745249CEB7641E6B19E404865EB374507DB3394A77A0F47149896AF1C747F86C97201D8C1CC8209E571B8C4D6972ECB1CA930B15516922FF08BF0C942AB5C5D197669AF94457FC9BA5BB47DDBD3A34EE941943B0B262A63FD0922D803CA035C6C54DB71B5A5A76F46C787036BEF777296E80664F73A78175EF7DB567BDB6CD8D97D27649661C76216D99D6264A7EB274C507AA170E1C3F23CA00A04B861AAA29EC33D4584B12985AC3B36689F129F3D29B19DAA5DCE4150162A10FA835E1D89A21FE10E4B1489E457C415961648044774C49DE6908201068D13463F1A999D3103342F03020F043DF3CC2E897B8293D235E8B408F2BAB266CCB0FCE19F6779A9C7DC89E368B07BF56F29911593E78009358CEA0BA304C0C074FC86A0782F99BC38571CD1632770CBEA199AF4181984FD05FF37193132F434783F38ED277382AA17ACF5FEC87E72BEF729A63E69AF7387E9CC5BB339E01702E1228F530AC96DB053A415BE97749A109A1FD4057BA128649B17EC07AD\"\n        },\n        {\n          \"tcId\": 32,\n          \"deferred\": false,\n          \"z\": \"AE51639EF7F26FD2215AD11CBE1EDEB3B943D668EEEFEE13ED5B0DA3E0A5F3ED\",\n          \"d\": \"B04F631B330D83991B5C01E7F69452DFC394F9689632F8C7F60DBFAB92A9CEA5\",\n          \"ek\": \"662293FE760B0826256FA788B6A061039229502B6887527553496F09937F25C86D0762232C5A9C3CF9CE9AC7C43412295A180DAEF55C01053F71242257610F9F2AC7AD80A220991BA632B4DF1CCB43865C7469BB54A24A12D759943921B62563A1665F864C710B947C629B6AC169A05FEBA11DC87BA54420D35C9FBB364CBE5449E7B53A48B6361CF417CBB825AF93CB61D987442C500FF147B8703D6600CBC6789A83119C51D460EE759638D5302DB28B62A1950F6C63311917A51BAE092756D8DA55DDFA066E544FB7C71BD6D512D258CCB0B6B19B6693BEC15BDDD3706FEC949FD2B5AC81327A3349B62475F642927440C757266F8736B995C996DAE6685FDB89285377A38384EED54622052D197098E5D30C74D65F24B8C1F479A78037CD1936868801A7E688B9E8BB5BE333BA48540DE80AC9B12912F017961DF81CD02700CA6B30ED19325C7A507C5C1004652568F6240A060FF0316E1554168C5190DD32895B389580BCB7A1F5712EC3192B601A7FBC7F3EC9A3CA6106BD99B2800CB7E332CFB3824539801D87545711D6ABE408BE2963AA4AEBB76C9814B7808559831CFB84CB9DAA80104C51532B4867C731B53766932453E7058C23B27E7D63CF303ACD0D9A2F69B4A7B07360456A8EAA14C5142824DE66565335B589B01AE667C9CCF79EB5251A11305CF2349A8C9681A17B415F5190B602A5753B917F693B61A04B96D8C4B5AACD873B1B900B940F3C25E568C8293A679E2020C7B88F162B641891C9263116ED94C5A726078325955825CD697440729087C4D67F0CB126395C1E19824BAB9A1A1AABCC8D570CCA3BB4FF1B6F416A6E239926768657BE34B154129982006DD6B5048B2CC06679C27F070D7746870A976F1699802A61882B65B7D66736574C2A4065352F06C056703295291968237850FAAD14D56EAC4816AE5CA72BC59E04AB07FF4185B29B9B4A19970A81ABAA505FB6F4A3C1E632B2421EEA069EA40C450E3AC2435428A18C412F209509D83C69717963C62748C130E5D1CEC8A924F1E81BA852B3780B38B44AC17B3B70F4694E550B497F9A5DD740A2A7651085261A7840B52D3C95D2AAA899160A6AF7289127606A85233044B6F72C2261F3C2B89946053BA91C6120EDA024C70435B8917C1C2413324A179BE34EBA570C4C0762A8490CB76B021C2978B6237A792973EB994DF4714F230374D20C89C2E5A58E777075CC83EADB309EFA73ECF34771205B3B67C2BC848444A247D4F857D8C6679306AABAA3BD2D4A22E9B7B889574A68168B659B8F2C19560EA178BFDCC509996E8A992B2FBCBB6703478F6C3D6E3020AE0086DFE2408D56468AF66722123D6654C7BB1B246C35B9AC8C2CFDB35370D19C9FF173CD999BFC3BBCDAD053B00C941F8173D2621AEC1571DA391654FC50B9F99E51F26C5313B85E73506A0CA7D156012A63C51D228005B26FFB6427D8C279E9BC6A520ABDB38981D04A2B7ED5311F023572D57A01E3BCE0375A51FA5408C193AA80BBA033A20750C2A09521DD94B7F023271EF2A561089E170531DA641CFF4525233B93FCCA87617C685E00D0CB1317C540B770514A97279C04119436F872DE43A96747856339064794801FD00460DA5663ECDDC739C783608FA59F2E27E4AB3DEEC74061C16465780B59E4DC86\",\n          \"dk\": \"74996BF2C7B505FB4AB2E64D9AB83F5169CA5B062CE625184B0315D2B79945D04C818259E454099C55BE3A262B57C31EDA5B4F095169A09A8DBCA39DE41057E3CA126E4781D0503FFDDC89D24B28C3BA4BC5E9437C83A3EB8497E2956D30FCCB29AB0393EB1C1A0B7D2AE177EAF705E69A5B88726839516B096BAE87BB4FFC394FBB86967300AADB340E83CC9BDBC9830171088363BD1080A976EB9FD9A6B92C5273BD09073B8848936C8F1486AF320CA39EA592559A12B191432413C1C4977FD6DB5BB12B0DE877AB4CD63155690C31DB93B5EBBDF29B2A7E642C382478C9794AA5787B7A50CCB9850BF199798DB18861179E6020CC2FAC59925A0B6F775FB219726135237CCB90E321906314B8EAD933D7C53024733DCF076312F358EAD020A88B586DE53AF798C3A87178058207ED806AB4FBB349B953E7C13E6D2122A7C50B1EAC96803C7414DA65020A7ED62A91975BA6EE7174BE62695CA3A7AEC723CA701483B95DEED68A82DB95CE175B0F94A0160332DBE4575DB085215A9A520878FA76C6D3968A8DD9B38EF48738451417B3C835DA8B4F393ABFCA7DA6560278890C2BA8547BD71815043759B5A802130E8957398AE30BCE688CFB3B71E2A77068C88A3C42989735A180B64144817E8AE04A4DB010A3AA4FE24CC0AFC25C89266295532F4BFB89087008F7B265161410B6A72ABECA6B30921A8FF39AED231369D39D8781A7691933F12473B56683FB49CEC4A939BC221C11C11CA904647A7C2F77DBAD979080CBBB624C005488F6211CE7B9B2227A91F04749C2776E399377124FC7BAAC11215E6B75A7725417D049A94AD809711C1B78631DD7D24ED6720458914649F8B5FB6A30E3F97EDD116987655900278EC3A60C278A5132007C91416EA4AA3694A87D481B2EEB40674277783E391D5EE54110787622451D62F922A721C010921F570A095FFC00B8002DE9AC71EA7486A0DACC5CD0727DA19DAE03C9E0C6438CBA0B08F69590C83D6284C2092C2A98CA6405F9B19DEB5C52F7B5F91CB7B653688C633120FC82BB76A8A52AC0D5E68E74009F8E655A2057642A6004FAB653D6C79DFBDC3941E59921D8850399810A881BE80C974929CE9781074D50094D0A469D716B47FA72F2A3A4772A469A31284E91CFE5854F5016CF2747118280276582020D1472EAA3BED0AC192981098EC7524ABB1410A392439A7D9BF940EA19BF3E76BE94D856D5EB3F5D91A917C8886D561BA027AB7E166FDC12CFDA12C7B06AA552BC27E7B67BEE1C19E0612DA58C8CBF0A8EB2B03818D8158BFB1C09268AAB73C1D424C354C8557AC3556D3A944D203276187032771E4C668CDD1145DBF3B00393AAA19447C1FBBF5EE48DE7F07215F85D648A59B3D6602F69ADF57205DC57371CA84C1CFA4D69E32D065CA04A1BC5FFCC1FD215AE3B91936DFAB4EF9591AB18093F36CC6CF36ACC204CE517B9EE9B746E79C5D2E005F96768CB508E7FC96DD38338915C17C95C1BE072CD46A59A3D36887EC80C7B022D3A56C4E495AF71E8A40F2944AF9439241024C6496CD85942AB17359229AB2BB54A0D61301DB560F15263B0196525A2AF546C8EE2CA18F212AC8356AC4B2CB2E3FC7F9A414776A24C662293FE760B0826256FA788B6A061039229502B6887527553496F09937F25C86D0762232C5A9C3CF9CE9AC7C43412295A180DAEF55C01053F71242257610F9F2AC7AD80A220991BA632B4DF1CCB43865C7469BB54A24A12D759943921B62563A1665F864C710B947C629B6AC169A05FEBA11DC87BA54420D35C9FBB364CBE5449E7B53A48B6361CF417CBB825AF93CB61D987442C500FF147B8703D6600CBC6789A83119C51D460EE759638D5302DB28B62A1950F6C63311917A51BAE092756D8DA55DDFA066E544FB7C71BD6D512D258CCB0B6B19B6693BEC15BDDD3706FEC949FD2B5AC81327A3349B62475F642927440C757266F8736B995C996DAE6685FDB89285377A38384EED54622052D197098E5D30C74D65F24B8C1F479A78037CD1936868801A7E688B9E8BB5BE333BA48540DE80AC9B12912F017961DF81CD02700CA6B30ED19325C7A507C5C1004652568F6240A060FF0316E1554168C5190DD32895B389580BCB7A1F5712EC3192B601A7FBC7F3EC9A3CA6106BD99B2800CB7E332CFB3824539801D87545711D6ABE408BE2963AA4AEBB76C9814B7808559831CFB84CB9DAA80104C51532B4867C731B53766932453E7058C23B27E7D63CF303ACD0D9A2F69B4A7B07360456A8EAA14C5142824DE66565335B589B01AE667C9CCF79EB5251A11305CF2349A8C9681A17B415F5190B602A5753B917F693B61A04B96D8C4B5AACD873B1B900B940F3C25E568C8293A679E2020C7B88F162B641891C9263116ED94C5A726078325955825CD697440729087C4D67F0CB126395C1E19824BAB9A1A1AABCC8D570CCA3BB4FF1B6F416A6E239926768657BE34B154129982006DD6B5048B2CC06679C27F070D7746870A976F1699802A61882B65B7D66736574C2A4065352F06C056703295291968237850FAAD14D56EAC4816AE5CA72BC59E04AB07FF4185B29B9B4A19970A81ABAA505FB6F4A3C1E632B2421EEA069EA40C450E3AC2435428A18C412F209509D83C69717963C62748C130E5D1CEC8A924F1E81BA852B3780B38B44AC17B3B70F4694E550B497F9A5DD740A2A7651085261A7840B52D3C95D2AAA899160A6AF7289127606A85233044B6F72C2261F3C2B89946053BA91C6120EDA024C70435B8917C1C2413324A179BE34EBA570C4C0762A8490CB76B021C2978B6237A792973EB994DF4714F230374D20C89C2E5A58E777075CC83EADB309EFA73ECF34771205B3B67C2BC848444A247D4F857D8C6679306AABAA3BD2D4A22E9B7B889574A68168B659B8F2C19560EA178BFDCC509996E8A992B2FBCBB6703478F6C3D6E3020AE0086DFE2408D56468AF66722123D6654C7BB1B246C35B9AC8C2CFDB35370D19C9FF173CD999BFC3BBCDAD053B00C941F8173D2621AEC1571DA391654FC50B9F99E51F26C5313B85E73506A0CA7D156012A63C51D228005B26FFB6427D8C279E9BC6A520ABDB38981D04A2B7ED5311F023572D57A01E3BCE0375A51FA5408C193AA80BBA033A20750C2A09521DD94B7F023271EF2A561089E170531DA641CFF4525233B93FCCA87617C685E00D0CB1317C540B770514A97279C04119436F872DE43A96747856339064794801FD00460DA5663ECDDC739C783608FA59F2E27E4AB3DEEC74061C16465780B59E4DC8644132D7CEA4F7CB9B06AA59C4213FA6293563C4516CF033491742C389AF38643AE51639EF7F26FD2215AD11CBE1EDEB3B943D668EEEFEE13ED5B0DA3E0A5F3ED\"\n        },\n        {\n          \"tcId\": 33,\n          \"deferred\": false,\n          \"z\": \"6F9FF5654FDA78774498E2643E935D21412CEB49BC393532C80C47A982418F66\",\n          \"d\": \"3D63BD6C310AFCF684292E5F8E1B98CC75B5A27B21526268444144AB24AB2967\",\n          \"ek\": \"FF22387C34AC69033FB8CB685DF308A47B6A8C9978A152A7003B583DEB864C3C26432780D56785BB511EB2507E40BA80100A1E4FEA67A2290F26F75AAF655827E95C1624A127795D88F34927989594D228F1497CF998BBCF012ECF6156FDC63870EC49E38C29C6C35ACB89B031419A0BD835425A255400B05167758F56474D8B83466738383320E22514C69203AFF561DB2B1596C937EEB49044817949B726FDA5B46D6980F9099B17E6BBC4272F4185739A687E8171BA53497E26BBB215163FE18448F1D005E6827C2177CFA0A3CC516B48F3D1BA1484C33169963F071E13C20BEF371DD4013B1D5B0608C0C0C52241AEEB42F81B5D6BC09A6627C6846AC962328A54DA2573CB4A4D71C729A4AEC7411EACD1053B71B8D932C3AA6A759198B26516843B9C344E1A1874AA22EA88353E7C6C410121026CC75DEBA8C5E3083D83BCA0A4C3EBA5C653021AF25C6251FA486CD19AAB6A6458FA6D29C02A7FE6CB2192553193AB92857AB1141458916653432A71FA4667FAB3CD1A602AAA7294148B17C2B82F86900FF55EB4CC0A8A14940982496F417809956874077A4F256448F2B661FB0163C687CE8B08EFE14C1F4279CAF8BE239338535836FE5B12C1A8A4A9EA2BAAB732F3E36262AB028BB30EB88B7042297BF92B22757A0AB0A817DF03707892043DEB2FD8963FB369A8C76A8FFA17237EDB8CC0450B2E9726491A36FFC2B58E7C9CA803517FAC9C047C4CB056B0316C3CAB239FFAC13315A3675BE2B042938C079844F72601CB6A8353205211304DE0229A249148BA13C7E969CA28E95183863AB3F838EFA3BF9159CE989358FC1A69BE3C764C87A4FB8B1739510E7211617806ABF4C19FC48C6112F3C67AD92C342A40B7F5468A559563B2C2BBB7869B221959526B22032059F093BF3287FD652CF51C7A4233C17DE638E8C92EDB41C77ADC5ADA927BDB12A7EE2B00CB2577A6937C0EBA364F9093E8E94AA334545D1797071A85EE0BC9037B352CDA8D2D874E16B344E02894635A3B5380CDCB6674951C0399B10062429593573D39A06817E84F7D517332F92903C7CFF7651D7F1C37F648C22A872CC007BCF33A4E991CBD34069CCC6771B9582D83136F52790E74A24CC86C439AB7C5FF1088CCCB81FACAB6E1D0092D14A05DDC999AE4C006899D6FA6186940925BCB614DF83C5C95555358894A5A4ED2B1A8D8C7AF8B5C63CC917B849B4B544995635845B2F50BCADB9E25BA6AE2C10458D2C1358430A8EB27884B8EE27B6408265CD24C59F9B7B3E0CAA48D744BE36A3906F488C432634D88123A981B79A54C50237275262A5AB111FB51AFE1847C9E284A91733DE5F38C6D39A51987CDD368CB3E136D92A312F515A243F999C04495DD7B3EBB3CA1B82B3647935C77951672BC9520092D203AAD318852F93866E6F6494E695F4C7A97D035956C68B3A56C5A51298F1CB0997528376DD5CBCB5B15AA19A1A5B77B98F1355210543A0C0366F5328B2A49647021CC3246F1856847C637AF4B390B521B890C346CE5B4E3F1A51A874BA7DB1D2D8B8356BB0EDF48C9EA01177E801BFEDAB6444B4B911549BBEA1230FA6183949862A06954B04F4C9192B9EBCBA8151E20D48FA3CA8943F13734D9C4E642A7A7DD2BF360600ABD6E84E29BBA27F96C191480EB71CA68\",\n          \"dk\": \"33D925297A649E979A40147093E74DFFD1749917C75E088AB9800AAAD81ABEEB293FF88634706904220E90490D968C097E33A2925C251904871E78975E564606617D0228227DFC3613CA0BD6F1B6402476AFB25BC4D0AE47A2A401C95C721A04D0536E42DA76244908E8D0769D5524B1A96FC9548A7C7A2C6BAB0B081618F2C19ABC358A1DD18D96AACBAF943A4CC6A7FC316FC629A25346B82B42A746A9C4D27C951D363AC0669AB3D6368D619F17EA93DC2401DF36BDA6835DB6EB02DD211AC6927B918071DEFAB2B9F671DE554E727875746475EB886286A165749504B5919A39BBC4536C8154E5B7A9D3076E0A1F068B2C8305595ED2A1469321A7476D107273221C6C5C9A2224D4AE615991D67869674B3A5994255470BE7383C1FF636AD276B7B810794B3885C1788BDA26256DE13F8AC1179C3614ED27B0A7F220DC22425BB7B8B1A8AE2CA6057CA5BEB447042A43CC82543D43F6134E9C5DE6D52388AB9D4C746202EC759781B4AB0C684B4B270570B23078195D4233A71A451E196131EB8AF598BE86B1511DC2C85CB50D001150CA2A7A3692C7E2CA09C03A243B5B0176E9BEBD629B5F4CB599D15FCAE7A7D7F44566E0A4571740864633FACBAD195AADCDFBA9B322AFEF119436B96624AB410F764083395FCB4C81FE6616CA80CFFCD6A4CB9B35BD8557D0AC2A61AA1042B414AFB0020EE1006E593DF29A625E010C15F7A5DFBA4FE459060F90882EC91AF77181C3E32FB027AD397B8CDBF52D397654CF68B2335A3A75EC24C87852B846BC9FD07FD9D767396A5511402A1993C9AD9138FF800C6B6051A6B94DD2BB457E9883F2954980715706B07A15A186CA57A5F0F83A2CFA01D22C5AD79176C9AABCFC481B67989648000DAE714D49E6C5D9E0396339BA1C22216084BAA7B403DD998F0B9600BCF419698A2CC853CD145156FCB0C193928CC32631A2B3954AABCA05D43E1E4C2A3EE34815C8A0095102E8487A82397D2283562B19A5D88993AEF77B57886B1129B9F442986461A33293AD5B41AAFB25A8D8DC48F128B67B07C047B1CB52603082E422425059A585A7B84BC948D6B7FCA6A9E7BC17B44251343202EC892D2DB09F6EECB9483A03640C4C56B32A5B8AA44D847703C988B1C02FC02C7BCFA7642AA7C1B67B539708743517444D470F273A112EC87FB7145789744F5037095EB88015932EC4B3CE62A4314B97935CDC2F252CB839442D800816CD6AA0FEC81251F6CAF6C4A1E1A9C7CEDA798135639311ACDEC014E4BA0335A9919501C42A729E6E9556AB9BA1A1EC7D96E225159886F7C67F520305436864C9A70FE7272315A7CEEFFA6F39AA8FFEA3CEDBC8CEAB1318A64335C1C5530FFC89C0CC1D6F7905C5809E61363719F54FE7E8844D382BCA679554D368D3976206289B1A4BB987A6AD72B59CA2BC2D91A61902144065B762CDA31255119C9E19C579F4CD2A7620BCE0546565C0C82A35001900E614C62D2B7856142349B25E32CBAF0634BEB9D2B603B67019AC8FECEA9F5144782EE47E2EF823E9E646A253ADA9B8B809435C33F890B0D80A565C7FB5A19E5DCAA39E4983FC149BDC1B8477B808A5FC6379C7018134C73FA197F1E87E7443373D108AFF22387C34AC69033FB8CB685DF308A47B6A8C9978A152A7003B583DEB864C3C26432780D56785BB511EB2507E40BA80100A1E4FEA67A2290F26F75AAF655827E95C1624A127795D88F34927989594D228F1497CF998BBCF012ECF6156FDC63870EC49E38C29C6C35ACB89B031419A0BD835425A255400B05167758F56474D8B83466738383320E22514C69203AFF561DB2B1596C937EEB49044817949B726FDA5B46D6980F9099B17E6BBC4272F4185739A687E8171BA53497E26BBB215163FE18448F1D005E6827C2177CFA0A3CC516B48F3D1BA1484C33169963F071E13C20BEF371DD4013B1D5B0608C0C0C52241AEEB42F81B5D6BC09A6627C6846AC962328A54DA2573CB4A4D71C729A4AEC7411EACD1053B71B8D932C3AA6A759198B26516843B9C344E1A1874AA22EA88353E7C6C410121026CC75DEBA8C5E3083D83BCA0A4C3EBA5C653021AF25C6251FA486CD19AAB6A6458FA6D29C02A7FE6CB2192553193AB92857AB1141458916653432A71FA4667FAB3CD1A602AAA7294148B17C2B82F86900FF55EB4CC0A8A14940982496F417809956874077A4F256448F2B661FB0163C687CE8B08EFE14C1F4279CAF8BE239338535836FE5B12C1A8A4A9EA2BAAB732F3E36262AB028BB30EB88B7042297BF92B22757A0AB0A817DF03707892043DEB2FD8963FB369A8C76A8FFA17237EDB8CC0450B2E9726491A36FFC2B58E7C9CA803517FAC9C047C4CB056B0316C3CAB239FFAC13315A3675BE2B042938C079844F72601CB6A8353205211304DE0229A249148BA13C7E969CA28E95183863AB3F838EFA3BF9159CE989358FC1A69BE3C764C87A4FB8B1739510E7211617806ABF4C19FC48C6112F3C67AD92C342A40B7F5468A559563B2C2BBB7869B221959526B22032059F093BF3287FD652CF51C7A4233C17DE638E8C92EDB41C77ADC5ADA927BDB12A7EE2B00CB2577A6937C0EBA364F9093E8E94AA334545D1797071A85EE0BC9037B352CDA8D2D874E16B344E02894635A3B5380CDCB6674951C0399B10062429593573D39A06817E84F7D517332F92903C7CFF7651D7F1C37F648C22A872CC007BCF33A4E991CBD34069CCC6771B9582D83136F52790E74A24CC86C439AB7C5FF1088CCCB81FACAB6E1D0092D14A05DDC999AE4C006899D6FA6186940925BCB614DF83C5C95555358894A5A4ED2B1A8D8C7AF8B5C63CC917B849B4B544995635845B2F50BCADB9E25BA6AE2C10458D2C1358430A8EB27884B8EE27B6408265CD24C59F9B7B3E0CAA48D744BE36A3906F488C432634D88123A981B79A54C50237275262A5AB111FB51AFE1847C9E284A91733DE5F38C6D39A51987CDD368CB3E136D92A312F515A243F999C04495DD7B3EBB3CA1B82B3647935C77951672BC9520092D203AAD318852F93866E6F6494E695F4C7A97D035956C68B3A56C5A51298F1CB0997528376DD5CBCB5B15AA19A1A5B77B98F1355210543A0C0366F5328B2A49647021CC3246F1856847C637AF4B390B521B890C346CE5B4E3F1A51A874BA7DB1D2D8B8356BB0EDF48C9EA01177E801BFEDAB6444B4B911549BBEA1230FA6183949862A06954B04F4C9192B9EBCBA8151E20D48FA3CA8943F13734D9C4E642A7A7DD2BF360600ABD6E84E29BBA27F96C191480EB71CA68D4F2A9B485FFC544CD3DF67D23C80150AAF7A45CD946F4B7DB2B67F4F8B222536F9FF5654FDA78774498E2643E935D21412CEB49BC393532C80C47A982418F66\"\n        },\n        {\n          \"tcId\": 34,\n          \"deferred\": false,\n          \"z\": \"D083E6922EF0A818308FD7FE7CF5AD3A96942442BE327B0A307685C2D4315901\",\n          \"d\": \"249D48941ABC01C9290719FB34D91B05E774E70E6F0181E1783F2586E2499536\",\n          \"ek\": \"11709FB3C60AA689CFC149954EE25C7071A4DFD724D0C4CAB2F4B3FF715FC16302ED2BAD6A926443A617AA2B44FE2283A660C0951A0A41C56EBBF5AC1945454B8A878E3C4FEB22C10FF5B0A3C7C177A84EFFD670622479A401A5909399F131909B349799CB116C51BD02778774764EE6B02EA64929311281FB821D074B1385A6A965E71F9A065DB2B78908048AA46118BCF95CFE0BC6F0318B1E3C4C2F895B98613DA1141170680191D896044C40EB4741D2D21E0C506410D9092F821DBDC04839C6C6C73487F9F239DAA0346563433C32B9A1F7254717A456B04F65965C614973DDB5C43B9072A120294021A21275007360940A5B8613C01E703128B9F27B037CBF88049FAAD7A10C2213F39A82510A872C7C2850D3CE5A48CBE5DA4B3547AAA3B8612F64840C00ABBB09BCB4F031EC6C2AC284A35D84796D4B23FA621C9E0CBA6E04632B8BC5D840765168C08F591871986FD44BBD78E56C30AC80E32229A0F57420872528308C52A028B1993A93003A9E40A1B75B6CE4ABB8EDC109C77B3D2A80B21F1827904C94666AB80AC9702CA78963F36510CC7CCC6AAC6E9489342371933AA4C6329926437A58CB8BD5CB64929929FE9707A13805B621A95803C7EC68A83676578C039C8F273B12C8A2C1853D3AC6169CE95364F791F7A6986EF765C5F0829FA888C8DB3FFFC3A701E3B4D8246B62B3A4FB637FF532BF152452A824AC25AC24E416CC21667B09055C77C12925E77FA67CCAF2C9602091149A730CD3F627A8613B149A2D3789C19CB6B09E5CB64E42A82628C4BB168A492529680B1BAD2A6F0F7350708030495768DD9C82D6E9812DE35FA90689087B8DF242414752AAD8E15BB3BAAFFA05A1057A8262FBB1749C9A6C4272750134F72682CA753B1E555A60D91B26728EE540132DE67958D3928C3A72CFF02BF92997B0B39F06FAA333F5987713BD113026D327AFA167B040436D9940C9AE7799A4D9CA86278EFFC846FC102625583B42234EA114B75057531A07A4E23C340E04A066F3ABD69AAEC83CAA5A88BF27A0592F5B443223864A341E2F3B21CD0262AC3B89A1E735D6B9001A1C98C12210A8E45E377A8424132A10D317BF02939B6877DEE6AE1D7818C3115076B741C307B805C4860862382D1770678B4006402B93979B2383868BA1901F50776A28978A016C4F95787E8C875F694F94E3116DD60F284158BC283B79C7CE377C3F1197AD3EEB3BDB2C8E0505250E58BA75816A21E171F9A8730FA2CB8CCA44D02902F07B6BCCDB6AF8705A78E6854935299A340E62A12418B6693A0909CE5A8F1349A993E75CC4806DFC045F4C28B4DE8C9AACA04713E62DBBE3AC857B5445D6A5F5C115D5C29318F2BA9FD126F46960DBEB3B3DE53350051980E7CD5DF450087BA97CF36B7A566F0DC1479545BCEA5C950AA4CD4EA324B04036BB6A93D8519C66B12AB2EBC05F849209A603BEC768C96587A33963BAF3A8F5D8309DB35ABBA81423C365371A9DFFA649A726C4A84604F4B893F3E30934D065CCB0383A2058C0E121C3133C7FAA4C3AB4B7321C3EFC7741400004D279C5D76AA512692774D9C319A59320BA18D5C116838C8CE96A43AB562CED703A05357CDC20AD5A2A32211A2949927F642B278E71BF94390BC90A3969017B88B8EB63FF6AC90AC92362\",\n          \"dk\": \"88A386E9E2401A9A264FABA1EA2B6A13B5BEE6207582309D39C43BD37CAF668AC6D1D36E8003B85FC37755441F94E60719E92294A88B4E72170D1992B5382B7FEB489DD93A60383A2D29316B7179B8462346F85662E1C7AC72C86A496CA1635B55B535CD4348D6C97D3F5A74FB60C6ECD77A62B83CCE9BAF0693952C86CCE257AF464033EDD5AFDB07333C2A7FF358B45097A8FD87BD5642B12E1B5EAFD73761BAC07BA748429426C98961AF89771FB709EAE812F9724697B4C970583012C2C4B12C369BDA8EDED8A1AAD2C042CC7F2FB86C74B82521C55FEEC5C1FB5087A3E1B93DC1750133386A491695428F09F0696158659817C66C86AD2BF388E2F74B848941AD5A8D790081D3A63E85683AB65499C0EC41C8FB146664CF3FB9311C3786B1D90DDB54B1390AC902F42B30C29F02C236DC313026772825FA70DFB8572C3BC5B968B143719A5B132620D811AFE27F3F096BE180817E36B3F1F78700F00AC574CD8287A25EE1CDFB844F9224C39ADB10B99044CB7009DC1A67EF397B23F2BB9AF5A0AE6833210728449026EBF20E354C14C856C930D61A07CABCC510042E079415D67F6CFA0E692344F3397D0A381CD3E56DF6398C6730C346502039FCCDCB0389DD97C07F9C22FFE5AF7D22CAF6150A7D7A4D60787A67C3582EC82AE8A62FD3A175F8E27C653CAB2A2C69F1D844B1972110B2AC3163BC119A69E4D628DF2C0C76266599D08BEB6B659176CDD07B1A50699DD92AB3EB2C787F90533C28156B8405CD46C1B6B67BE6399319A4BAC4A93FCB659C819A6EC9A1845F1C791C6915F7A40F3DC578C8E016A0E2703F776310B3BCD487B638671628BA508101800B94AF45AA0C3C49559A46979906A8BCA9A474D0AD63F035E95B918B0297440203E154A676572F21D0815BC45D3E7402E537669FFA65FD903A76219C94D275B71C3B9D69C024A17E4F0284179A5F4E668F981B7F968507FBD52797B19C89941BBCAA117786C202030C33F476656207B7C87CFDA16699842F72C5746D843501D9977F0804FEFC2D603C9BCE710547AC047F94980899BB1E167C26301FC146A6C89081BA492274C3C894936B57F94C12D1C3B47CBA740023A494C90942AE0290782850613DE10F52727CDD2204B106AE37B0BB3F3626DC41A6F0B3CD8646CB99F56F6ED137EF803E4439878C58B2EB280BB8FCB95F7021CCD41D2B1A40D6609830BB5E2401389733104A8077B3683EEAD98FE5EC5AC7679D3A2C6F227484D73B4F9E285E4B7C4F47E9071F637C5F8B713127CF2FE77378E2CFAFD148E433B2DBF74FE0475596051EF55007AEB8A59959C8B683A6B726BFD706C1F3E4A748430C01C893078178E3063F00F797AAE6394FD637AD155221C49C533977CE8014136873942114C54462B8750264248F04165853944B1F744EDA307B62998DFC0C63D5EB9D23DC2589210493A69C0B9820D12282E5689FDAFC1E1EAACBF88551D04B2983C2163C1A5F5CA647FC3288BCB5B543C89765B9203B5803A3006F5666BD73F5A44B60A5BDAB85B7C63556323EF82B45A4CBA06BEB388707873D28CC096651E817215485129154C15173B7A7AA826DF0649C96592C7AC8E1B27872C56CF0D3495CB54111709FB3C60AA689CFC149954EE25C7071A4DFD724D0C4CAB2F4B3FF715FC16302ED2BAD6A926443A617AA2B44FE2283A660C0951A0A41C56EBBF5AC1945454B8A878E3C4FEB22C10FF5B0A3C7C177A84EFFD670622479A401A5909399F131909B349799CB116C51BD02778774764EE6B02EA64929311281FB821D074B1385A6A965E71F9A065DB2B78908048AA46118BCF95CFE0BC6F0318B1E3C4C2F895B98613DA1141170680191D896044C40EB4741D2D21E0C506410D9092F821DBDC04839C6C6C73487F9F239DAA0346563433C32B9A1F7254717A456B04F65965C614973DDB5C43B9072A120294021A21275007360940A5B8613C01E703128B9F27B037CBF88049FAAD7A10C2213F39A82510A872C7C2850D3CE5A48CBE5DA4B3547AAA3B8612F64840C00ABBB09BCB4F031EC6C2AC284A35D84796D4B23FA621C9E0CBA6E04632B8BC5D840765168C08F591871986FD44BBD78E56C30AC80E32229A0F57420872528308C52A028B1993A93003A9E40A1B75B6CE4ABB8EDC109C77B3D2A80B21F1827904C94666AB80AC9702CA78963F36510CC7CCC6AAC6E9489342371933AA4C6329926437A58CB8BD5CB64929929FE9707A13805B621A95803C7EC68A83676578C039C8F273B12C8A2C1853D3AC6169CE95364F791F7A6986EF765C5F0829FA888C8DB3FFFC3A701E3B4D8246B62B3A4FB637FF532BF152452A824AC25AC24E416CC21667B09055C77C12925E77FA67CCAF2C9602091149A730CD3F627A8613B149A2D3789C19CB6B09E5CB64E42A82628C4BB168A492529680B1BAD2A6F0F7350708030495768DD9C82D6E9812DE35FA90689087B8DF242414752AAD8E15BB3BAAFFA05A1057A8262FBB1749C9A6C4272750134F72682CA753B1E555A60D91B26728EE540132DE67958D3928C3A72CFF02BF92997B0B39F06FAA333F5987713BD113026D327AFA167B040436D9940C9AE7799A4D9CA86278EFFC846FC102625583B42234EA114B75057531A07A4E23C340E04A066F3ABD69AAEC83CAA5A88BF27A0592F5B443223864A341E2F3B21CD0262AC3B89A1E735D6B9001A1C98C12210A8E45E377A8424132A10D317BF02939B6877DEE6AE1D7818C3115076B741C307B805C4860862382D1770678B4006402B93979B2383868BA1901F50776A28978A016C4F95787E8C875F694F94E3116DD60F284158BC283B79C7CE377C3F1197AD3EEB3BDB2C8E0505250E58BA75816A21E171F9A8730FA2CB8CCA44D02902F07B6BCCDB6AF8705A78E6854935299A340E62A12418B6693A0909CE5A8F1349A993E75CC4806DFC045F4C28B4DE8C9AACA04713E62DBBE3AC857B5445D6A5F5C115D5C29318F2BA9FD126F46960DBEB3B3DE53350051980E7CD5DF450087BA97CF36B7A566F0DC1479545BCEA5C950AA4CD4EA324B04036BB6A93D8519C66B12AB2EBC05F849209A603BEC768C96587A33963BAF3A8F5D8309DB35ABBA81423C365371A9DFFA649A726C4A84604F4B893F3E30934D065CCB0383A2058C0E121C3133C7FAA4C3AB4B7321C3EFC7741400004D279C5D76AA512692774D9C319A59320BA18D5C116838C8CE96A43AB562CED703A05357CDC20AD5A2A32211A2949927F642B278E71BF94390BC90A3969017B88B8EB63FF6AC90AC923625D0BB5F514CAC167BB2E2B5FE989CE88ED65315BC610D9A5BCC77BA80DFA2FF1D083E6922EF0A818308FD7FE7CF5AD3A96942442BE327B0A307685C2D4315901\"\n        },\n        {\n          \"tcId\": 35,\n          \"deferred\": false,\n          \"z\": \"A20ABA8A8DDC212DE825BE0D3BE57701A6B5B3A46A300D9B5945F579A59AFABE\",\n          \"d\": \"E1CFB8195877B2D4FF3363BAC3B4E7BEBA6DC3CBB789B1B24215393F6C9BBFAE\",\n          \"ek\": \"B749934F35347C7251B0359B6582502BABBEB5574F30C63568139B1CD854BE96B8A6F09069460205BA245182334F669E93F8C7D867945C3B75BF1CA810108E0F670249A62B9E6910D793C7F0A24BD3C40839D713F880B4EC6AA8AD8ACF6EB03E12FC5B1BF61ED2480A9CF68E30A441E3102611D25E9925503E704DA69393152A759DB92175E0343560B961759370BBAB159320EFA846E4A316CF8035D8EC991FC4529055A5C7F76F6FC5423AA041CC4691352A44E6E91E54DC129B000C0829C069C1538EAB1E5BEB58BF0733B64A17C5D159ED31AD4B15B36664465B6548CD4060B487C0C76890DF4B4EEC5705DE97BF47712C667023E627480A4533A9258162050F401CACD75232CF7621BB3B00F76A1CE2B42E65A91E65306F6C019C3F0C1E00FD2192E703EE5592A41C95E8263F53B16E54A952D72078A1589FCDD7BCEAB03A41D89E1D9071DB88C344F65C8D6714C367AF7C5287D6B68EA319A7DBE97F7604AFA67A27BA136C2B996B6A1C4647A8B56CB8C0D6A4BC9B33079D5B407522195ECB9FC23C778B27ABBFF026A8CB84ECB66AFDA43D40558990931736D32EC4937629FCCD01D7B14A1241D2ECB7929C443AAC703553C63D7C8410A891986C770200598F98C8AE008CA7D95CC347AC7BEC18ACD24028E6AA4DCC856BCB9D370195BCC85DD8496B62CC3A06B5A2A5961D781659C068143952C4329BBAE9A985DACAB8201B1854995FD2BC544BCA66FB927757B78207E9C44405C04002917B2BAFCAB59AA298B555717B79257AD5BC1AB1B10F92C229A9224CA16433D244770CA18A9CE12841EC38E8D131D75912C05372CF8C845E256DE2DC38283B0E2326A5DF7A3E4AA112EB91292709BA89F7672CA1BBD599859637782C7C25C61C03ED288CA6181F5E194A85C37615A1778BA51F62789614B715ADE62E85BB8490E7B7D1C81C9B578E411904D7C9C287442D406A1BD26CC759DA58FC867B0A39954F7B29702B74BEDB4C8DF87B7E57526C25C2123C1099A21CB3882C966191BAA5C89CB19B08DA6E20C588B68C954DC7CA9483B1B9FB508C694424D85C99E35AE8A084CED1C4D4B9CBB4246D28170BA94109853BB0224A2638E91FF3269AE2B013D515BD867A6BF7797B309342DA513DB1D18031BA29D72A0850669C27A1B27454B427785327225C73968FEA012D5389861B56631F89316F3B9D59C77E007B88607A53A5D7679BEB45320CD0EC605C2B0B6C7221BEFA0BBF1FD65888E5030AA18983AB1DED0979DE800DAF11B54C50971775C98BFB5B30772A6CE835021B4344D075022B4DA1A8A00E0BA140F26CC2A36F1685424642518F57217F60ACCA0B3B24BC2DFF61BF1A903210582B684178181B47116B4139E2478D37BBF006C9B8F849AF07497F493A8A0A0EF67268D8526F3D874A7EE3062E3590AA01CB09A4BB6AE73839602DFF6A0D335132864AA178C19D8D096E3094719D6ACEC6305CF3E59B190961C17B281077838E57805ABC615856C90BE8595B00C19D93B42247B925402ADB132E78C40E18C52459A542636208D0E068C12858BAA380FDB287D7E4B855716F2365354C63706EB5561C64A012F56BD64C06D2582E9CB34DCBBB837CC72C82A2CA557B328FBD9838D19A8BA5CAEF7F516F782E8BFDB53A793223A813F942BB5A6E0965E5\",\n          \"dk\": \"B325042B81C75CEBA41A96A16D9695F5842C3E6B9E7CE97B765A7B44E49B33501E72CB6F5F88A94C9A731A784A1DE1955A3337DD299DA82C5AC31AC581B46BADA8BB08C1736C785633610ECC5B68C2786D82C9A08D6277ED6914C43695F275286B5B5B0AB1C8D936AA569262B6358CA287A49E06392352706068484A14587C9B71084A35A5F8ACF78C47D34B43E84331835C4E75E17645E045ADA45D9F99C267B4345CB92241BCA8CA988353348A9DD73948A941D553BF444B0230A16F7F625DDEA463A2C49246273963BB1A9C2439B3DC18F3377A15414E775700BDC408353C7124E1BCA513AC5D415EAAD5A36D72A965357248E0AA5A8B84920C4BBE4104204449CD947484B42D49D93772C038D6554F0037BCDDCC50417025F4C0694EC7621ED82BE87BB3F3E9CDDE27388F55B730C40B722269D53C6BB388557AE11F960680F8F031443B6BA70C1CE45B329A6269F7B410D9C40ED4D18FF1B6CABFB20D89019F117032C5A2008484B078488D2AE065966C680F7098F860C896461BD1D0C2947B67431B3EBB3734664C4CF1213502E0C7826A4981FB8854A5A45B216F469C72CCA4637D9C61249232760B58D785B3B411683BCB33DBE175F9E240955A7B20D819C06C5AB567C8F289915CBC3C50204443F17EC096B399E11530505D94C93EEB9946E41362BAF7772C7B153AEA4620349BCEBB2243B85759093A98D13E5CDABF2BAA25A31464C19A358B1CCF6DCC10B5341E677C0B37C65EA52717FEB98C84470261E23FC7E02AF5D2CE086A326832174FB089B768B4701A9748225F01F98F1AC42DBD99594F34812DE15A09CAB805B695574145E0AB1D9600B18BE721E70C11978ABB91E159EAB67098A77D8F51ACE14CAAE9F3360645338F0A536450384452C89109AB3D9A5C8328B3CDF0334F748464A383883C033E673A6799678A860548FC4D47D970A8B24DA195CED6B52469AC36CE9BBD159A60FA6944014ABAF99868B963943685C7A15694D4B2BD6ED91734D601680C692314982C4445B6547CDE146B9B82C558F5ADDB84C2CFB93E91E61D4016728BF724377805F0060E3C4C943FB8AB1359979B495723C32AE096130E559604A5BB80817EC947A23785CBD7209D7B7672D1251CB234A196374364A0439623C51E997D2051C0C6630214DBB6FF1C88B1600BA11A0784D68807E830B885007C202376970B92625037677AD1B81270679349DC4CC56529B5BA205EA596AB489B8574CF668303EDA22578258739A525364A7A2C3439B6E74E9BB1A7462B2A51327719528358DA11D7BC856142A4118449F8B89C5BEBA43C117A27C65BB1E70DCF5B36A1E45416E2074BC1211F14AB957C4810C84818932653D17C4805BD6A00C80E0C0FF7C51295C670A8B6758C23A21CB21E6E097AF583AF2761AE3D62AF0EF56226E31492120B90322D76D6C1F657CFD0D2BD2F6CB4F8589012B99A73B82375C54E283C6F1DD12FD53B1DFEB540280375033B0DBA671A7D608FA3BBAF8422AACE1055A787C77506C199D19A01E1A0391798C8BB967A1C797C247C214B790B14606E3403EB91A57340557F872110C0185429AC9C454181375992D6C7C9F743DC832F2A13CCA8C8B42F63A609329DB749934F35347C7251B0359B6582502BABBEB5574F30C63568139B1CD854BE96B8A6F09069460205BA245182334F669E93F8C7D867945C3B75BF1CA810108E0F670249A62B9E6910D793C7F0A24BD3C40839D713F880B4EC6AA8AD8ACF6EB03E12FC5B1BF61ED2480A9CF68E30A441E3102611D25E9925503E704DA69393152A759DB92175E0343560B961759370BBAB159320EFA846E4A316CF8035D8EC991FC4529055A5C7F76F6FC5423AA041CC4691352A44E6E91E54DC129B000C0829C069C1538EAB1E5BEB58BF0733B64A17C5D159ED31AD4B15B36664465B6548CD4060B487C0C76890DF4B4EEC5705DE97BF47712C667023E627480A4533A9258162050F401CACD75232CF7621BB3B00F76A1CE2B42E65A91E65306F6C019C3F0C1E00FD2192E703EE5592A41C95E8263F53B16E54A952D72078A1589FCDD7BCEAB03A41D89E1D9071DB88C344F65C8D6714C367AF7C5287D6B68EA319A7DBE97F7604AFA67A27BA136C2B996B6A1C4647A8B56CB8C0D6A4BC9B33079D5B407522195ECB9FC23C778B27ABBFF026A8CB84ECB66AFDA43D40558990931736D32EC4937629FCCD01D7B14A1241D2ECB7929C443AAC703553C63D7C8410A891986C770200598F98C8AE008CA7D95CC347AC7BEC18ACD24028E6AA4DCC856BCB9D370195BCC85DD8496B62CC3A06B5A2A5961D781659C068143952C4329BBAE9A985DACAB8201B1854995FD2BC544BCA66FB927757B78207E9C44405C04002917B2BAFCAB59AA298B555717B79257AD5BC1AB1B10F92C229A9224CA16433D244770CA18A9CE12841EC38E8D131D75912C05372CF8C845E256DE2DC38283B0E2326A5DF7A3E4AA112EB91292709BA89F7672CA1BBD599859637782C7C25C61C03ED288CA6181F5E194A85C37615A1778BA51F62789614B715ADE62E85BB8490E7B7D1C81C9B578E411904D7C9C287442D406A1BD26CC759DA58FC867B0A39954F7B29702B74BEDB4C8DF87B7E57526C25C2123C1099A21CB3882C966191BAA5C89CB19B08DA6E20C588B68C954DC7CA9483B1B9FB508C694424D85C99E35AE8A084CED1C4D4B9CBB4246D28170BA94109853BB0224A2638E91FF3269AE2B013D515BD867A6BF7797B309342DA513DB1D18031BA29D72A0850669C27A1B27454B427785327225C73968FEA012D5389861B56631F89316F3B9D59C77E007B88607A53A5D7679BEB45320CD0EC605C2B0B6C7221BEFA0BBF1FD65888E5030AA18983AB1DED0979DE800DAF11B54C50971775C98BFB5B30772A6CE835021B4344D075022B4DA1A8A00E0BA140F26CC2A36F1685424642518F57217F60ACCA0B3B24BC2DFF61BF1A903210582B684178181B47116B4139E2478D37BBF006C9B8F849AF07497F493A8A0A0EF67268D8526F3D874A7EE3062E3590AA01CB09A4BB6AE73839602DFF6A0D335132864AA178C19D8D096E3094719D6ACEC6305CF3E59B190961C17B281077838E57805ABC615856C90BE8595B00C19D93B42247B925402ADB132E78C40E18C52459A542636208D0E068C12858BAA380FDB287D7E4B855716F2365354C63706EB5561C64A012F56BD64C06D2582E9CB34DCBBB837CC72C82A2CA557B328FBD9838D19A8BA5CAEF7F516F782E8BFDB53A793223A813F942BB5A6E0965E5B5E964695C24F57CD05B8BDC23949D382C7E9023CC1432BC131689528B1453B0A20ABA8A8DDC212DE825BE0D3BE57701A6B5B3A46A300D9B5945F579A59AFABE\"\n        },\n        {\n          \"tcId\": 36,\n          \"deferred\": false,\n          \"z\": \"7FB950A8F51DCEC4BC7A573EDDA56ECC049E5688476BD5FD6CD076A8F99A019A\",\n          \"d\": \"ADC4DA59D935DD87420ACEE52AEE19CB371FD0BB498D79BA680159EF7CE37C17\",\n          \"ek\": \"266B5ED5BB000FEB4C73CBCFF6E8A980326E798113223C1FD815922E1247992A2340B70B46A6CFF3638A9E741B478BC7E6C476D2F68CE244BBA73AA919C7A48770C5A9A798DB1901B2D93836F5C507D862811949CEB4BBC65CC2D6FC36E2215078149355B83B83635BEEFAADB6FAA387104213CB6609F8B36D291B3B567ADCD85EF569966E527102AA159F0912E5617E1B0C295D12838EB7790F0C4E4A93562EC21F94C517F5B6CC3F7B3F6E832F93893417A2797F9848D5333CE5741ADB867E64E898362249A2632F7ECA4BD38034BD5184BCAB71F38854A02A2C4693B32CE172C1FB41AF3A2B2AE370889C485F98587293C6758504189050CB437A991B8EA5E25569D1A9ED194D0E470A419B453120288D29763C3BAB00F5180CAB07DA070DB401CD28D69892B77A847A47C51A2D0B39A22074627CBC068D42988A60481BE5934E626201292AD073BFF81CCE044A33E3F3339C7B9446C607E666656A94A0034445635312B20A76CBC4C6763C338AA022D79A826D2841EECA89D433C5E7DC041ED488610A1F5A951FA966148D1B8597101C7CF8AC44F5183D42C689079164E90B5DC582A761571496B7A8C3543FF275D403B923B032CBB87D5EE90EF8853AE78590D0E8B200C3819705AD47E71568051C026B4B5C10760495994208AC61A08C23ABB921828A5D69740AF57507E339EF977649999330C6A9B584642F5539356590C6CB64872517996CAEF853219C5C2CC1BA04F0C8537039C9A3047DBF17CE18673B039B2A9EC083C7E53146863B61199169C9B1838CAB40508A6B709DD8502783654D55766E6B671B6C83201509395BC565B10904DC27BFE30842479528995A5888AA8E04374E2918A9EB642629A135B9306A21CB02C925BC99401EAD40285A7B3CF5A809FFF6502E727B332269E8C445C7D9303BF35C344615E19220E391AB4F2011634C292C22022AC0CD5FB51339B83833EC7404FC38108207B8A5B1416C2DD29785F3D87F0BC75F24C65864C31EDBE243EA0B8AD015A003BC9EAB583FDB5ACB32BA3BBAD6BE5A189472560B22614B5BD257249C4458C7601C8B0E48D0C72DBC5743E77350D64DD6A587B67280D04B20D1DB15275361DCA43CEC893EA5401FB3B459637817A18755876983818CC1C745CF70D2A8CBB36050601BFC34BA9685A02B6652535BA101A5193660A06CE54DB9700A977AB36E612549E51FDFAA5AB0490BA7A302DCB314ED026CA1D765D54145DA1623F9A7064D2926F25A0C9FDC4B8CEB8C765CC9B3A55A4BD79CFB9754ED5786F04395B7A80ABE92687D6900DD14C3AD743E2F9540534C63602858371C3D304263C2185E29261237A848DAE97507DCC5FDF60209112E08D269F56418F2FC875B693D5C9CABB9BC636FBCCE548227F42BB001EA6ED64057656566F51A927B178E17AAA24A0616CA362AD134AAC5D36C5447BF4DD0CB7DB95C9009B7109826B1D8ADEF95CE8B9B444848B41002426812700850CD7572A2530A2BDACB8CCCA5485032454E855116E91E6830A68EC04DF98370CDD18E12372320BB865AE818C6B32628489B59AB53B31ABBBC944861A0BD100B66FDC017AA0C79DD177CAE432024181820D23688BC991AE597981B56C3F45B5E4A92F34C7E72882C6B4F1D791C2719CF3DFBFB8F3FA04ADCC1D4FF07\",\n          \"dk\": \"24C6ABF468CF8893385B625491F2697ED724ED371DD9C76158418A6FFAC9E0A4158694AF115BB82549A0D02BCCB13282C4C37A2AFC617F347D7BC64DD6CB94C50922B8024ED4FA598B4912EE2ACE81FCCE893739678397C343C7DA4446A55687F236201E071B9C2C7715DCA6AC566702A809931B1D65919CDDB51A87B44B07629C659A104EF61BAA8702355128B9215A40E64F818C1CA297698EA14C00756B6D52974A7014E69B59E477362A63BCCD09BB474020FE51BF5337124C24C8E6F60DD2E495F3370E14A4CB1504234A4C8009B97365F166D385AC511A05EC06465D962D1BA88C191A9E28DAC4F517680D6586923BABA11BB30AC13D40466D52977929D62F48751458087830B02F5BD9B100A92DC6E602F3F24686365476C33CB9F5409856C80FA490DD6211AB9776487A3E15B33A92A347F3A92744C16DE3F8B3E942628C926BCDAB6CD80A068ED951F2689EF7066A9E2627F39C638A580441EB8FDFD9B2EA1B39D043AEEA1A0F48EA84490408818C602A828F869098FD2C429B35621DE9BC8039C6ED940BEBC32FD0C6C396B3248CFAC85C74325531A296A9C237FB2C4452457B4C43F0DAC214575A96A770170ACE6E0109E4A9C0E5F8985F4AC37CC84D206306AD7A2058EA58EA09AB3B7926941996B8125D7BD76472006C003784CF4479691BC422E00CA9976840D886DDC8A5BAFB87DB268EBD8248033703F6C6686CF928FFD736BDF3A26653319F22996C52387BF0A294944FDB1051315577B244C78578923F48B435362711B89585841FDFEC9FF047C76864055757CAC7EAC9D0F2999C341D314154DD58519B602D7855B6ED67049AE402F8913177914051E7C5A8400969F5CE9A485D95D544920AA7F169A198FA710FF73440D8949550A054562F16637D5A813C10BB29422AB193616687F062A1FA1693E3A9064982F9E96FDDB812C0763233687EE08928DC800FD2C8A3143148A0BA97633B5D97062A5B0572A89B2B265C4439357F96C01C6F464E72D0BF0CC1C421B3CD38610A9B0C89EE956AB896189C0B03AC173D31792ABD132F7174BE4D210B76E04B87E85B2E4A25F41332BC379B10D672862462F6AA3875DB6A22158EEE6B13135751DD49084862953CF275BA4580DA07921CF9482FC692643A6E9A5073A567C593AC01A57A75FC3735CD89029C99B3945A2BC7949B64377EECC96FCCFA2AC94CBE4C9AB4FC64673D042F416A80D867C9D5B3C9CB276FDB3C693181B071553B0EB66138656F33D0546322330980553BB2400D1256BECCC969937305553E8BC60C2D24AEACA90061CC7D38E3244253414222BC0D321C73EC6FD8804E71092995D48A6CD1C68CD1841011A45F4961BAB52EC176628B49CB0B840FC8B1120701227B0C92E10A1B34BB2475152BD72A82232345CFDB5B46E3403397B5D1276FE3E96703950FCFD49F1172CAF856A333497BB68C2ADAF026A7198BEE890718F474D7090151D21ECBA4B83E56069E1C6D34C5BCBFE94BF0A385702A9D2FC770D1905120A57FEF816AD941633558AEF7F96780C15112710DEDF8517CDB3314687BD4BA4C2B5A0BE253498894C7CA32401B517D812172A2B13146F30ADCBB6E79641A366BBB96B0688EB285266B5ED5BB000FEB4C73CBCFF6E8A980326E798113223C1FD815922E1247992A2340B70B46A6CFF3638A9E741B478BC7E6C476D2F68CE244BBA73AA919C7A48770C5A9A798DB1901B2D93836F5C507D862811949CEB4BBC65CC2D6FC36E2215078149355B83B83635BEEFAADB6FAA387104213CB6609F8B36D291B3B567ADCD85EF569966E527102AA159F0912E5617E1B0C295D12838EB7790F0C4E4A93562EC21F94C517F5B6CC3F7B3F6E832F93893417A2797F9848D5333CE5741ADB867E64E898362249A2632F7ECA4BD38034BD5184BCAB71F38854A02A2C4693B32CE172C1FB41AF3A2B2AE370889C485F98587293C6758504189050CB437A991B8EA5E25569D1A9ED194D0E470A419B453120288D29763C3BAB00F5180CAB07DA070DB401CD28D69892B77A847A47C51A2D0B39A22074627CBC068D42988A60481BE5934E626201292AD073BFF81CCE044A33E3F3339C7B9446C607E666656A94A0034445635312B20A76CBC4C6763C338AA022D79A826D2841EECA89D433C5E7DC041ED488610A1F5A951FA966148D1B8597101C7CF8AC44F5183D42C689079164E90B5DC582A761571496B7A8C3543FF275D403B923B032CBB87D5EE90EF8853AE78590D0E8B200C3819705AD47E71568051C026B4B5C10760495994208AC61A08C23ABB921828A5D69740AF57507E339EF977649999330C6A9B584642F5539356590C6CB64872517996CAEF853219C5C2CC1BA04F0C8537039C9A3047DBF17CE18673B039B2A9EC083C7E53146863B61199169C9B1838CAB40508A6B709DD8502783654D55766E6B671B6C83201509395BC565B10904DC27BFE30842479528995A5888AA8E04374E2918A9EB642629A135B9306A21CB02C925BC99401EAD40285A7B3CF5A809FFF6502E727B332269E8C445C7D9303BF35C344615E19220E391AB4F2011634C292C22022AC0CD5FB51339B83833EC7404FC38108207B8A5B1416C2DD29785F3D87F0BC75F24C65864C31EDBE243EA0B8AD015A003BC9EAB583FDB5ACB32BA3BBAD6BE5A189472560B22614B5BD257249C4458C7601C8B0E48D0C72DBC5743E77350D64DD6A587B67280D04B20D1DB15275361DCA43CEC893EA5401FB3B459637817A18755876983818CC1C745CF70D2A8CBB36050601BFC34BA9685A02B6652535BA101A5193660A06CE54DB9700A977AB36E612549E51FDFAA5AB0490BA7A302DCB314ED026CA1D765D54145DA1623F9A7064D2926F25A0C9FDC4B8CEB8C765CC9B3A55A4BD79CFB9754ED5786F04395B7A80ABE92687D6900DD14C3AD743E2F9540534C63602858371C3D304263C2185E29261237A848DAE97507DCC5FDF60209112E08D269F56418F2FC875B693D5C9CABB9BC636FBCCE548227F42BB001EA6ED64057656566F51A927B178E17AAA24A0616CA362AD134AAC5D36C5447BF4DD0CB7DB95C9009B7109826B1D8ADEF95CE8B9B444848B41002426812700850CD7572A2530A2BDACB8CCCA5485032454E855116E91E6830A68EC04DF98370CDD18E12372320BB865AE818C6B32628489B59AB53B31ABBBC944861A0BD100B66FDC017AA0C79DD177CAE432024181820D23688BC991AE597981B56C3F45B5E4A92F34C7E72882C6B4F1D791C2719CF3DFBFB8F3FA04ADCC1D4FF07BAF18B5A25081C8A9F526111B600954D39BADAB9044F59903D2A8F21F8E1D78B7FB950A8F51DCEC4BC7A573EDDA56ECC049E5688476BD5FD6CD076A8F99A019A\"\n        },\n        {\n          \"tcId\": 37,\n          \"deferred\": false,\n          \"z\": \"51D509CF26799741631099039F713B22551E2B0F0297BB809DF0CC8FC3E47EEE\",\n          \"d\": \"76CDCA53F781806D55CA8D3BAFB3F4D389D712F1221E85B5E29D6A46580F978C\",\n          \"ek\": \"FE97202108CD725098CDC892FA68301012B3156A2E5BD503F9D388B26B07EE099D5AB0BA6B4A5E7DF369F748BDAFC4C72B171747546B3957BCFE189E312058202885FE853966A7B3DD285E06D37000900228662910260CA1D783DCE2ACBC436BF3669C51A07FB990449AFC153C9C5630F8A2E4E4B7A8E8719A98920B104E72646F83D8C06AB631755239B4303B7AFBBA9DD72577FBB677C054B16813DCF373734A6E4D8B025B66C19936A13278B859165FCC6277E9F149FF979EFEBB9867A9124233A5107B5E29B2CAF81C84A3D05DE3DB316858C5FCB372FCF28705E2BF0B4855E6F97F17DB217AD6A7B7288310086AD129119FF88D20B35F2F287B1358A851185BC12046D5079A2CB3CCAF98BEB9E7C1E82A841F7B48B4F57B3B17467441865288BC7020CDB1B7564989AA3F47BAB9B0919C6819E671235CD7B39F39C71C870FF6A72612CB1BC58A86F565AFA6932C71F059B2F72BA83A8985DC26D91A0CCD49BD32080D30EAC67D96A061E906E2B15E5F43215A9160AD4682A27A2B1B9C3ED1C697CE7AC6A4B2B3963B9A99441FA9D6337739AEF5891D773B4C095C8E97A9BBEA918EFB6058AFBB2A63318972400B11B9523FD335E118CC1FA0BD0B03C8C3278787F9B7D1F752BF5B739FFB0EB0654E753C2F955351243C233E9CB149D49AA1F1459D61B03E452192B64AFD642D00800B4B815D0B9863CE2A4A8AF4C6E7BB3F845A32FD95184812896105946B7A8ED80A5ECAAB0348DC4D2C4C2B7DBA3D1EE6B6A9D35CF03A4585169FBEA45101B97BAD583F9CEB486612761A25AB393960DDF3C1A4E23E23134CE37B7E6833822AF3B278B1B69D27A4ED72221C055C7F006F3DC78BDD3C86759260E31C8D0503954EF31292EB1C8A877A00271109998D9A1A026C16482316994B2573D1673C5B727E75024786C1742414B746C1B2AC74B71920A2C79B0456C08BF1E519CF28AA70627A6A5C8496D9B061E4439B75080A763E50C1A4B32483E87AA5E67C25A8A7CE6FB864012C6A32535DB7123D24F7CA62750A18E96911EA0AD2ECABB7B02252D44DEBB2C55D552349D3798EF7812B991755A278CE158185171B696859968492CAD792DC82767E53333AF9812AA55C443959D14B1C385452367BCFB5A5955163B287254B9C4B0380EB818912BB175605EA55585F82AFEC62C2CB46BE6AC0CB99443E5676AD21EA08A467006E67C5FEF61193F3A54C4C5DE6A4314ABC3F62A83261A4B56AF18172B4A85C00C56CF28EA5C6BFEC6AA703530B7E641E792C35D1F1BBB1914D15708B88B87D161524B44B12167873C19237CAD32BB7C72B0CDB04C516142634101FD81B2B12ADBFE27512C0A091523732B82AA278CA6AD1AF28A03ACCFA28D591040FC950AE475976C5A7E192A9044099A4AC487D5A39E121B9A776CB190A2CA499403C3A5F9F027C49F627E6446D8FB53252E1B098AC8A5DD59EFAA22D2F57C83809003CE2B562231140542AF5D561ED7C74B4E145820CBE387254D78C79A8F6C241F10D94E26454764876631811492221B09B9C2A6C75520D03EA79CC129862A10AC2E0288446AF46165BD1F614C9D1C3606C9C8669C334534266A1946AFC0C580C19283CA1B59C88AD6B8ACAAFD7C3328CE3583325ECDD4B101DC28053788ECDDB4CB7A4B66997F9FD0995\",\n          \"dk\": \"D5DC5D1A0BA3C20670B1306E201A927764654F757026D15DAA35B77F5B81AFD975BE114CC9E9A31A4C5E83C28D86EBAFB1E65674EA7BFC5646EBB49C67D7B0EFD60FE283BD07D71EDD71B93C815C3CC066513A52C74081DFB0773EFB736D6C0E862CC5F11CB88FBA3AC62515C39B0692359AD457210371C65B6031745622BB68BE60337F5ECB065FA678CEA88165BC7AA4E002D3C892115A3504534F5DE12178FC17E2F513B3535813D30EFCCA68018128C5C3181225CACB1C76BF7A6E98195E0B793B78B0305121087A83A37D1989D0145BB4F11B5E579075B6C5AC845949DA2EA03BBDA446A6C7F5B88C8913454A605179004BD47BAF446BAA4705743A0B6E2833011CB4069664E80C58338C71BBF1C72CC89F77849F6FF354B280044DE60B6D16C31355B4AFA199FDBA078B018AAEF88D046B927ADB64935152273B16127701B0093EED2439CC89C3DA9A277D1AC5CAF65B48262A48521361192168425FB1C341D180CABCD7406154CF7F647F0A62700C630F093649E98936D12A1FFE00A368A2622D4579F521B57BB1510D6CC6D35C48AA96B07079C7C6E4265D768E4EF2C9FA463052E716B48053EE0521227544384C7AD1673389DC34029C02DF55199328376A4C07CE6C8AED221DFDB844F770282E537F15A83A5BE8873AA52FD17C324C6C4B763A7D6A3121AB384814D20B879A42E3149BDC58CE709B3DC6E87B6EC03375CB15584C0DFA6A05F48935DB376096293BFF5A311D4373A95AAC117A25C1FA95EB1C21658A2D313A3343F643969148B11727BA64882B8AB3EC772909EB9690FC72A8F1C6B249191317610F81999B161ACE6351AD330E89A18FA0986514848E80AC95BE41BAE18C1885D44CA30047FDC152EFB069D589296AF20337E12A1093CD54BAB36D252AA2A674A7C065A5676CCF507C1A07A2CC471C612500E124573C865F9C3C6D59AA02F54094141B5A3E8BB9CB7A7400A42AE4867440399306627AE9112DB909A1D6820C26ABA62428BCF27842F30BCB97D241D161CD17E8C3789BB94EE60238B2C987601F694A1BBA38581C261DBE2B94A89556F12620141889A7CAA997036D7693BC5EE961D06A1324EB7520953AB7E3BD00FB2B4E306312A6570277C71BF35DF403442325789846AC203B905F28127B3947780201D5B57B6BE7CAC796076D04B838A222C4F08004F585BF79645FE11EF52749B8C0A5656798F8F8BCDA89C29CF1B10B4429F8B65974164C1653B0B6EC8DCF39961A7B82DBE28604C579F3109F94577B42A4574AA36F2778CD424C6238996F4954605C7B25B50C455AC334D73343D90AC6A222324DF6BC4766B3D2C2A04C92A54F084DED24A9A4D5012AE741F12318D283B1CD668375129274149F33175C2E19453F3B4E60A1089E0647D37B0721254C86720DA0A97697B45C4CD5360AC513D4261BDF738A08F1BEA818C722367C30788EA2361384A02528E1423F4143C525B8C9A89A1A574A0D45AB9A8C4E02B01B894634C29A3FF9B1C1BADA025E8A99F38A092CE903CC03411FD25CE61110A4B423A01699BD922353FB65D8E796F4B77F226895EB822552F7AE0CB7C22788429F8132F158C824319030E770D6B9ABBBB912F3E592987826FE97202108CD725098CDC892FA68301012B3156A2E5BD503F9D388B26B07EE099D5AB0BA6B4A5E7DF369F748BDAFC4C72B171747546B3957BCFE189E312058202885FE853966A7B3DD285E06D37000900228662910260CA1D783DCE2ACBC436BF3669C51A07FB990449AFC153C9C5630F8A2E4E4B7A8E8719A98920B104E72646F83D8C06AB631755239B4303B7AFBBA9DD72577FBB677C054B16813DCF373734A6E4D8B025B66C19936A13278B859165FCC6277E9F149FF979EFEBB9867A9124233A5107B5E29B2CAF81C84A3D05DE3DB316858C5FCB372FCF28705E2BF0B4855E6F97F17DB217AD6A7B7288310086AD129119FF88D20B35F2F287B1358A851185BC12046D5079A2CB3CCAF98BEB9E7C1E82A841F7B48B4F57B3B17467441865288BC7020CDB1B7564989AA3F47BAB9B0919C6819E671235CD7B39F39C71C870FF6A72612CB1BC58A86F565AFA6932C71F059B2F72BA83A8985DC26D91A0CCD49BD32080D30EAC67D96A061E906E2B15E5F43215A9160AD4682A27A2B1B9C3ED1C697CE7AC6A4B2B3963B9A99441FA9D6337739AEF5891D773B4C095C8E97A9BBEA918EFB6058AFBB2A63318972400B11B9523FD335E118CC1FA0BD0B03C8C3278787F9B7D1F752BF5B739FFB0EB0654E753C2F955351243C233E9CB149D49AA1F1459D61B03E452192B64AFD642D00800B4B815D0B9863CE2A4A8AF4C6E7BB3F845A32FD95184812896105946B7A8ED80A5ECAAB0348DC4D2C4C2B7DBA3D1EE6B6A9D35CF03A4585169FBEA45101B97BAD583F9CEB486612761A25AB393960DDF3C1A4E23E23134CE37B7E6833822AF3B278B1B69D27A4ED72221C055C7F006F3DC78BDD3C86759260E31C8D0503954EF31292EB1C8A877A00271109998D9A1A026C16482316994B2573D1673C5B727E75024786C1742414B746C1B2AC74B71920A2C79B0456C08BF1E519CF28AA70627A6A5C8496D9B061E4439B75080A763E50C1A4B32483E87AA5E67C25A8A7CE6FB864012C6A32535DB7123D24F7CA62750A18E96911EA0AD2ECABB7B02252D44DEBB2C55D552349D3798EF7812B991755A278CE158185171B696859968492CAD792DC82767E53333AF9812AA55C443959D14B1C385452367BCFB5A5955163B287254B9C4B0380EB818912BB175605EA55585F82AFEC62C2CB46BE6AC0CB99443E5676AD21EA08A467006E67C5FEF61193F3A54C4C5DE6A4314ABC3F62A83261A4B56AF18172B4A85C00C56CF28EA5C6BFEC6AA703530B7E641E792C35D1F1BBB1914D15708B88B87D161524B44B12167873C19237CAD32BB7C72B0CDB04C516142634101FD81B2B12ADBFE27512C0A091523732B82AA278CA6AD1AF28A03ACCFA28D591040FC950AE475976C5A7E192A9044099A4AC487D5A39E121B9A776CB190A2CA499403C3A5F9F027C49F627E6446D8FB53252E1B098AC8A5DD59EFAA22D2F57C83809003CE2B562231140542AF5D561ED7C74B4E145820CBE387254D78C79A8F6C241F10D94E26454764876631811492221B09B9C2A6C75520D03EA79CC129862A10AC2E0288446AF46165BD1F614C9D1C3606C9C8669C334534266A1946AFC0C580C19283CA1B59C88AD6B8ACAAFD7C3328CE3583325ECDD4B101DC28053788ECDDB4CB7A4B66997F9FD0995CEFB593C11ED360F404732EA8B6542FA9796F2AEBB4C61EEA40B6D8A599C7F1351D509CF26799741631099039F713B22551E2B0F0297BB809DF0CC8FC3E47EEE\"\n        },\n        {\n          \"tcId\": 38,\n          \"deferred\": false,\n          \"z\": \"9C330AB4257D7B87C4742C6E95B66BDF805C6A145BF444836092C6B1D2C5FFFF\",\n          \"d\": \"78AB6C49354A018BD38A39926F822A1AC4ACC4FF32DFD7C047CE0887A3AC182C\",\n          \"ek\": \"85787649612C798187CD986E9578582F607DAED85C09B863A5476626016F88212EF2B70D81145C671162741C70C9DACF566A1B68025A7B66BAC9EB5ED8879D4CD40237D9C791B0A7E6C1C08A7354424B71D6739192B3BE48755CA513170BF3A6197400FBF94E9A707FE6D007E4062BA2D1BFF9192E294A981AB19839120401653208618C6DB25E40A28B8DD1A9E6D91A1408191C8C6638B04D2A50983C6B0640B934E5039729885DAECB0BCED2784B998CC296980626979DCC99A5726C4C07A6CAE1578089BAD68190AFA91491375C1C00624363C4A049A086794A046806117B36FC8BA8D38AA8097C727E3C59A6378703749C97D35CED818DB691783691C138C325541A1ADEE01CAC4800ACB2B008A0A5976C90B6BB8AC55B06B693390C30905866AF2014AB5D340A4EECCB4F91C0A2C700A68C67833C6FE10927D6CA935D8071F98BA31A1AB4B73128CB92BC1B8056372434A7B9524D95AD50508998A4AB179B0EB79C71392515E29BBD09682183394838A52734409EBEF5852DF7B9073098ACE127A7C95FFE48C30599540161C5D12CBF7BE203CD5AA4D861BF6768CBDB5C0401F70E4AC7B0ADC0305FB702C023AF65981A61E11A0B12C169E3AD569C90C417865F708C2A7B4B1C1866381C7EA6D17F4ED35730A84E5AFA975C728DE8B593D92251AEAA5841D8CE9A3037C1F47F8529CDE366BFC08C2A7337B8DEDA345008C43A5A901DF5C6B201B553F07FA799317A216205634130A2ADE04808032B0DC1E715C1B263C8FC07B251B14B9B1EF422C1BB171CA7D281C706A0251911ED345340DA4035D8947B058FE1D38F6EE02548B8056A24501E3B65B2568802417E98567322959EF118AB0C471B37496424FA327751C1DB40AD909424B66C811118A958782F3E57351C24934650B5A110B68C581C26CB7E634A283361B94D544E0EC713FCF52640CB180F25BD40D4114249828D5255B7EB9A1CF6589ED4BDE602317B4909950606F9C91E59E1B68599517A089466D939B43BA14B0A91987662FA63C27E1411DA78255111B961B45366515022421FBA4A04F48244449BCFEAF630CC4397A98226BC0B2A30778743EC2D2A9747A2127889794637F780A0ABC6122C79FE0830691A7EC277239190938F1648FD733C1761A42A84283E40BC7EFB707748CC0FAB76F5141122158B30655765257135EB2F08CB1E84067C64A503F58C8C54D765DADCC4C7181FFE1026E423B44AD717DCE8A6D765BE2A94C115434D16661F82410E7E19B8E3388972484638B6A52A058F2459414D3C57987215FD753434349BA001007940018EB49A52C78FFAF468D939C4AE585F2C1759E0A582BFDA316E53A363A1006BC26474F49C82B93EF4D7CA94A473D3788F5865B5D4323C3471888B06634AE325826144B5D9894CB59E8C6C6E3406459BCA1791FAA9743853175C48AD982F9775936FF68093AACBBCD87F6F30BDC1F19C93B2AECE6A3A4CD99ED3244323A7AF72C8598416543F50643B713AD2722BAFD092084BCF77385B6ECB2AC8F19BF9D97B353229496A0C6A5C73CCB06C2F2C5924E284EE3185D2160ED31B72D1F29B322C1DD11B5DABC5848933B4FB7B9AAFF1394AE76F624A231DB3C2DE108F5BDB1878551FF178901D67188E6B6F3A49C904A5539738F4305053044EEA5F9C\",\n          \"dk\": \"C4AC07C1B9ABFCA3BF31365671954516CB2E01E39337681D54262DB7B06E8CE8B4A98028F7176518098879083F64703207213945684CC4AB9CEC679834A35A8775509FE8CBBB279429D71A48FAA18485446A1A4FFD2996E24B76715A133C1486B2346055F22FB8666F487C44328688E5A6A6376682184B9057C9B65DF21101C3645C58393793B939C9B11C27360AE96E15A43BA3CA3A00358DFBD08A395C0A02C70FA45168BB6667A843A691A9C88FE11B79EB85F2034097166989024414770F0E2537A0F266F659591A06318947A715DB88A84BAD0434750E582A12C7C6981207239935C8529DBE0147E0E1CFB4D1BF8A6C1A699B736991CF2C88B22D466C5E481120A51F21A84877E710228766CEEC0168C2314DB6C85CAB02007875CFE3B5B613B1D50366EC450F1739712D8889D6653CDF41009CD42D3D6064DB930BEB551702813213C02110E435624920C242AEC784351228A847B1BF2C66AA1068364AA2BC674B2ABF2944018273DAEC324ABC94AB2AA5F21B318C84A77FC7A6D60570FB193A795993F2231497D8414B0082DB0A70AE81C8E5A61B461768B5653F51F31EC5E96F9041A49CA27A50598F23C575E0751E10C921A71676C2388C97FA83D481B9C466A265920DD9095005A1CDABA5B76E36AAEAB4BB58892F8A4C9590D2C96615A66F43A06FEC673C57B7376709CB759C61E37DE08C04D194A827173F5BC767FB788CE4A972EF148DADCB00B993A35C5369B3741D4C227E21112FAF923AB8C6478B856849A8B508BB4863D1C4239C0A469079CC677260FC8250C75887728B86A95F46B86E7C01214C1205C727707D9361E619CF792121F746815F9717DB937FAB2C094C90AA118C9D72801135AB87C99BBCB877915F8BA4E2E70B87303672D53EAA80A11A071C5AE307850C2864713287F976F7A465D8E4AC626B2390683ABE583E7AD4012F40778E4676BA30A163C70D5CFB5D755C0F806CCF0CCA7216BC16C2B7CA3F16239C147D81C4BBEAE635B2398E661B68EA863FEE4B94E6F2A44FB0A0BD8A75DEB9CCAF4AA14F14628EEBB7C88B63731587645B721ECA388EFB5495638863A13F4F6440C2E71636C9C1767B192932694C82740CB457D99B30C8396976A32AA5C027D3E36D0C3B09D24128B748CDB5F57BBF703E4173CA5C115B7000625FF06E280574EBB97E759302775093DB2878DB545C345B6525118D1C4458ADC9BD5F212B4D3BC77D66B9882A336231719736B448833B20F5448E759813FB856F95C60E29BA726B1D159B2BB342B737DC3FA7332B4492C583F83D9714029AE52A366C4734D29DB1E88397D22EAAD11D4A9B1FBEB7435F0B7016A400D4547A3C4B8F54B0A5E1F37082B4B751691E7CEB7C07D383705B8F42139326F0A985646B99DAA088C1BEBAB90AEC2B6B30E96229175C07AB4AB78A344A13C9A2F02B782860007A2D5611C7287410BDF0454F76B4F660B488FB94655B19B28B929B45664E6C8374F096B3B9A9A6262E2C2355A1DA73DBFA8160C5A16C08C68C647FDB408C223AB5D7D05820C3A1CF2994345305FBFC7D74CC20EE02C7C97C2FA5445AD4C9093CFAB7DE1C467768A01863347D1990A5882E355937694C0A01F7A9072A8B85787649612C798187CD986E9578582F607DAED85C09B863A5476626016F88212EF2B70D81145C671162741C70C9DACF566A1B68025A7B66BAC9EB5ED8879D4CD40237D9C791B0A7E6C1C08A7354424B71D6739192B3BE48755CA513170BF3A6197400FBF94E9A707FE6D007E4062BA2D1BFF9192E294A981AB19839120401653208618C6DB25E40A28B8DD1A9E6D91A1408191C8C6638B04D2A50983C6B0640B934E5039729885DAECB0BCED2784B998CC296980626979DCC99A5726C4C07A6CAE1578089BAD68190AFA91491375C1C00624363C4A049A086794A046806117B36FC8BA8D38AA8097C727E3C59A6378703749C97D35CED818DB691783691C138C325541A1ADEE01CAC4800ACB2B008A0A5976C90B6BB8AC55B06B693390C30905866AF2014AB5D340A4EECCB4F91C0A2C700A68C67833C6FE10927D6CA935D8071F98BA31A1AB4B73128CB92BC1B8056372434A7B9524D95AD50508998A4AB179B0EB79C71392515E29BBD09682183394838A52734409EBEF5852DF7B9073098ACE127A7C95FFE48C30599540161C5D12CBF7BE203CD5AA4D861BF6768CBDB5C0401F70E4AC7B0ADC0305FB702C023AF65981A61E11A0B12C169E3AD569C90C417865F708C2A7B4B1C1866381C7EA6D17F4ED35730A84E5AFA975C728DE8B593D92251AEAA5841D8CE9A3037C1F47F8529CDE366BFC08C2A7337B8DEDA345008C43A5A901DF5C6B201B553F07FA799317A216205634130A2ADE04808032B0DC1E715C1B263C8FC07B251B14B9B1EF422C1BB171CA7D281C706A0251911ED345340DA4035D8947B058FE1D38F6EE02548B8056A24501E3B65B2568802417E98567322959EF118AB0C471B37496424FA327751C1DB40AD909424B66C811118A958782F3E57351C24934650B5A110B68C581C26CB7E634A283361B94D544E0EC713FCF52640CB180F25BD40D4114249828D5255B7EB9A1CF6589ED4BDE602317B4909950606F9C91E59E1B68599517A089466D939B43BA14B0A91987662FA63C27E1411DA78255111B961B45366515022421FBA4A04F48244449BCFEAF630CC4397A98226BC0B2A30778743EC2D2A9747A2127889794637F780A0ABC6122C79FE0830691A7EC277239190938F1648FD733C1761A42A84283E40BC7EFB707748CC0FAB76F5141122158B30655765257135EB2F08CB1E84067C64A503F58C8C54D765DADCC4C7181FFE1026E423B44AD717DCE8A6D765BE2A94C115434D16661F82410E7E19B8E3388972484638B6A52A058F2459414D3C57987215FD753434349BA001007940018EB49A52C78FFAF468D939C4AE585F2C1759E0A582BFDA316E53A363A1006BC26474F49C82B93EF4D7CA94A473D3788F5865B5D4323C3471888B06634AE325826144B5D9894CB59E8C6C6E3406459BCA1791FAA9743853175C48AD982F9775936FF68093AACBBCD87F6F30BDC1F19C93B2AECE6A3A4CD99ED3244323A7AF72C8598416543F50643B713AD2722BAFD092084BCF77385B6ECB2AC8F19BF9D97B353229496A0C6A5C73CCB06C2F2C5924E284EE3185D2160ED31B72D1F29B322C1DD11B5DABC5848933B4FB7B9AAFF1394AE76F624A231DB3C2DE108F5BDB1878551FF178901D67188E6B6F3A49C904A5539738F4305053044EEA5F9CA8604CD90AAF5FB9BDA220814069AA00CB5B5FFB7B60E4BCC86F16ED0B49BA9B9C330AB4257D7B87C4742C6E95B66BDF805C6A145BF444836092C6B1D2C5FFFF\"\n        },\n        {\n          \"tcId\": 39,\n          \"deferred\": false,\n          \"z\": \"18EA1C7532F706B06870D0A1047AAE33D9E1FF9E9BCBBD302D8817EB7B022A77\",\n          \"d\": \"13B75620E4CB9AB9A6689F6E2BE44639BAE6C9CB7DD641AC1C9377242D99679A\",\n          \"ek\": \"AAF98BBEF422DAD045B9D3C2F134C71ACA3884604D3B87130EF84890D8870A4783DA18BD4C8B0667FB194393941C7508FE20AE5C077FD06A247DD7C19FD20940568B45938BEB39B1AE18CEA618BB8001C84DEC8CC170C12C77B6704AAE9B745083B3B00A020510A196EE2AA12195CCF650CB50600F35D61910397E645B886BFC58FB7C99C58AAD79552E13D31F05BA5904007E3B504D717156D652CB1D2353270CAFA6986846100B81C7816857AD7EB98D5CDA24E6DB3342F346AE5B162C11BB9E8C2AAF95C857277597398DA82A6DB7D7C34D7ABB1BB3155E72B0C6C811031A06D609C23306473317948E1B1AD9F7B59E22962BE33D750561146B135FF64058BC5524F26DBDE518C872A98D14862FF4599CB16C11D654F6762D531718C9B2BB51C34DDE019364F246891A3891F823AE85B6E54A4ED6A18673D4AFBC2C8C8F47C07C2B51DCFA56E5A57211914F437C2181A92723D5C09844951A0CC0470A594AB0C0E8A378A3015E39C5275E9B9D4E202ED6A4AC9AD1742C8455642932BA68C599E8883A3CB5FA11876023BD153166C7C33B3105C1BA9881F92C99CD8B41A475777F239AF2886570F94AA3626381A6B8B3C3AABDC39C2A6C0CAFF7CB95CB3162652BB0493726A81339377798D78AAE999BD9542565D5491556B716B09D0482C603B99501F8823AC6C5018543527928D2981C2957B9B7ECC33BF423F3B89DF352377DC554590A4BD6E7AEF624B873916CB638AB426A50F7789B25B420E4380413B10416C048A8069FCC62BC727AA9425700AFE835E12682215606F84C70DBAA4BD9B93D1E156913C62959C98A741A74810B996063624061B43D225B1AA71A02C50BF38016B4439FF156ACB652B278E0AAD91C455F03B29998317F596A1CB0051CFC21B1E76B59B52409164233D0C7B01A2B7C93120FC42BFB533347C2B4DA782BB55034C0A152CF51C44F39204A63C1F7867E57F27332791D8E3A1A13DB9A61BB0FE5660C6A8A2EB1365D30A83C3C1B9F03E66E58B0144CB6B292847F68CC3B0600C32FC45569158EBED0BAA50B26303707C1796BF9569193825603FB9237FC2AFBEA9D8D3B283D775583990E2672859155C70DDB7C0FEB84BC07B851F7B4BD44B84DACAC0FC913314113654567BE4370AE3179BB3BA56388A7EAFC3A1A10C25891B5A943C62A4297AD469D8040829029A8960CB9F65C21E10B64B07A713C40313E6A6EE0E939C741AA7BA93526022A8440858039A357EA2CEDB342C9299733381814E1C78FFBB1E289AA93C6ACD93C6DB2704F79922D277C9800EB3983865F65842D5BC8B346207C6C33612250110AB236DE202374132D60F5202C77367912C4FDFAC54DEC5FBC29076BA13FA013CE0FF70543B8989190AB63FA721F677FA5D0B248F2646E008830A06DDD613CB6063A8B9C37B4C263C4904E3B681EFA3568F0E429B947936EF41B2EF704751317EBE86A9597B8C0E6C81AA278FDC93F0379AFA3D1244D917401AB151A7805CE881216F208EB976FC442BB9DE86943143107B4AB479681F109AEDB28309067B9DCD6030DB4B558890FA1C5AD7FDBAA9BE39E6D6CAAED3C5286C73DEA3B6DBB940E6F647D43ABB364D92BCF099937706F8CD48F58BF77E3972CD846B20E9A1331AC0CC350080B65731270F9B2A951D93C68C98C\",\n          \"dk\": \"5C9B6453A08BA6B40F1F7C6C7CA464C76237EEDB9D74F0925D200A599684E85862AF8C29EF28871E7A55CC5824D381A9B7ACA2096B78081862D504010E1A9366000C022B3EE8AB91E6F7334287251F829C00221217823D641153B865A30F24B847154ADD5C9B4FF2350EA51D6C49A975AC2F65B541A52827DA25C93E9B36023C6129BA29E495B4914334E4D02CE21B66F1437874F1A56F03A6726C142F0B03D46543BED89D4597902C0C5B025895A04A9E22563560DB29793A353FD4528D0C6B0D0AC657358B04E7204C21A7378AAF3F0B93730C68D238695EA4CF3C9B35AE19B052B8CB2E041700902B4600BC30A846951BC0B9E42D9453B10ECC37165B08F22675657A6FC2023F0C1B42BBA175D329935B62468522BAC0944B83C5926A1C563D9323F175BB6EA9C2B1CC360FE8B60FAB81F5A1C9F31443D698B34336C0A1EB86E8E162FA21330E909CC0B69FD62738D823B6AC0462DE561DFF33094C1029ABC49381C406A316A3E2A887FFA9C3CDA9CA48E8AD802CB0231142C5F2CD83154DF65540CAFA16D39190FE100812816ACDD0720B4A44D337611651B1E505B7CAC0BF832841C573144BE42EF280B25C7B1700F344F82AB0C575B2D10C3A86449614B592CE42195F9C385E4CA257478E8D5320BAC24A68883DFC399AC236C497C27B5A80842AA018C0C0B459C2B14737197870C0E6D661510951D290B5549331E6C557DAB48F349B829AFBBEAB932E1E8638ECE2279C697220152FB84AA9DB5A8307F93438A51C22E8A1DDC54AB722A692EBCE6E719A82066E60B345C75729B4E7AD173BA7F363908411B6E724262BE0CB05F51E7739A5FE0C304690645C763CF7128DA2F71B2F7238A43003000995737A4DCFD8682BA8923C6752E53150DBD36468D6A2459A5A54450FA6D14A4295B573423E4BE95529C24D7D035C5F65C49D3684943AA505869CD525665545676A52958180538A521AF9274033BAB2C7AC83566041D9D40D92FB2DD2C147E456456DF0105264B0FB17853BF991789B6818F9BB75A381D4978D2EF7098626903C857CAB673353783C12C860258A363CAAA771ECC724B2332C1A254A3CB1547CCFFD149C187B345FA282DBB92D9793374CC67D0C0B5F946B78D81B08E521275AB7357050694C824033B9568BD335A8B837627B1494D9701181C7AA02A95DD3915E55A371F31C14F90C63075555BAAB65AA100AB99A6C1C85A1051D7E58A8E65BB312E02EB5485C05F302DDF814EED49D39E4A9BC688C989A17DF074C7FAC62FB73B0EEA85FD8177AFF41C4C3E40B407028C4F58E60DA14925385E4A0A6BA8B13C8618A8A08BF6A1630AF1B5200BA41596C9E440A5BA11C21F7C62A46379627CC4DB2E43A565A829FB605F186C405C519502C0115D9BDCB8857E03343707843A28047BDD6B445445560A686801C4D3F4C4C773537AFE7C5E2191853F56A8E75A9EA852552440B4E0642C3133FBB4CC678C2A35D093975F17BEC7972C938A7F0159A8BD8910E847DB09A1A2DE43576F9818A338787C10709F085987424C59546BA5493FE9696256C1B147105092A77A8A329563B061AF8CFEED35EA1E625F03716FA75B7839C325B94CB39C6CF1323CF4CA486F8703BAAF98BBEF422DAD045B9D3C2F134C71ACA3884604D3B87130EF84890D8870A4783DA18BD4C8B0667FB194393941C7508FE20AE5C077FD06A247DD7C19FD20940568B45938BEB39B1AE18CEA618BB8001C84DEC8CC170C12C77B6704AAE9B745083B3B00A020510A196EE2AA12195CCF650CB50600F35D61910397E645B886BFC58FB7C99C58AAD79552E13D31F05BA5904007E3B504D717156D652CB1D2353270CAFA6986846100B81C7816857AD7EB98D5CDA24E6DB3342F346AE5B162C11BB9E8C2AAF95C857277597398DA82A6DB7D7C34D7ABB1BB3155E72B0C6C811031A06D609C23306473317948E1B1AD9F7B59E22962BE33D750561146B135FF64058BC5524F26DBDE518C872A98D14862FF4599CB16C11D654F6762D531718C9B2BB51C34DDE019364F246891A3891F823AE85B6E54A4ED6A18673D4AFBC2C8C8F47C07C2B51DCFA56E5A57211914F437C2181A92723D5C09844951A0CC0470A594AB0C0E8A378A3015E39C5275E9B9D4E202ED6A4AC9AD1742C8455642932BA68C599E8883A3CB5FA11876023BD153166C7C33B3105C1BA9881F92C99CD8B41A475777F239AF2886570F94AA3626381A6B8B3C3AABDC39C2A6C0CAFF7CB95CB3162652BB0493726A81339377798D78AAE999BD9542565D5491556B716B09D0482C603B99501F8823AC6C5018543527928D2981C2957B9B7ECC33BF423F3B89DF352377DC554590A4BD6E7AEF624B873916CB638AB426A50F7789B25B420E4380413B10416C048A8069FCC62BC727AA9425700AFE835E12682215606F84C70DBAA4BD9B93D1E156913C62959C98A741A74810B996063624061B43D225B1AA71A02C50BF38016B4439FF156ACB652B278E0AAD91C455F03B29998317F596A1CB0051CFC21B1E76B59B52409164233D0C7B01A2B7C93120FC42BFB533347C2B4DA782BB55034C0A152CF51C44F39204A63C1F7867E57F27332791D8E3A1A13DB9A61BB0FE5660C6A8A2EB1365D30A83C3C1B9F03E66E58B0144CB6B292847F68CC3B0600C32FC45569158EBED0BAA50B26303707C1796BF9569193825603FB9237FC2AFBEA9D8D3B283D775583990E2672859155C70DDB7C0FEB84BC07B851F7B4BD44B84DACAC0FC913314113654567BE4370AE3179BB3BA56388A7EAFC3A1A10C25891B5A943C62A4297AD469D8040829029A8960CB9F65C21E10B64B07A713C40313E6A6EE0E939C741AA7BA93526022A8440858039A357EA2CEDB342C9299733381814E1C78FFBB1E289AA93C6ACD93C6DB2704F79922D277C9800EB3983865F65842D5BC8B346207C6C33612250110AB236DE202374132D60F5202C77367912C4FDFAC54DEC5FBC29076BA13FA013CE0FF70543B8989190AB63FA721F677FA5D0B248F2646E008830A06DDD613CB6063A8B9C37B4C263C4904E3B681EFA3568F0E429B947936EF41B2EF704751317EBE86A9597B8C0E6C81AA278FDC93F0379AFA3D1244D917401AB151A7805CE881216F208EB976FC442BB9DE86943143107B4AB479681F109AEDB28309067B9DCD6030DB4B558890FA1C5AD7FDBAA9BE39E6D6CAAED3C5286C73DEA3B6DBB940E6F647D43ABB364D92BCF099937706F8CD48F58BF77E3972CD846B20E9A1331AC0CC350080B65731270F9B2A951D93C68C98C1783913132F097618BB39BD4748B4EFE63DA07C26697F9B2F4E06CB2D27012AE18EA1C7532F706B06870D0A1047AAE33D9E1FF9E9BCBBD302D8817EB7B022A77\"\n        },\n        {\n          \"tcId\": 40,\n          \"deferred\": false,\n          \"z\": \"C71F7E44295978FC63BF8F6A68F8609E98D155FD7A74E1FB7982733FBF8A6C25\",\n          \"d\": \"7C345819C7C327AD9571E5DF882449DB243870D686A9764D4129B21E17AC86A9\",\n          \"ek\": \"D6525398362B71938C1C721695157F1A2BC24680BF6E265DE8726594C26CBAB9B040462B4DA30402D70EC050958FF5944D181374CC9A29E867C1B33CC36CBA66C66A75F44CD68112B543AEE0026D9C105EE556AD5FC2989346C7473C93E2641AA42904C7751D83CACACDA6B2175B1B9C1C41435C49027AA21119428C61482F655D4A0CCCFA5315100C314B680F702CAD99FAABEAF08335257F9380066A09BA0D4B04F91A52FBC09F3C348F0A49B840BCBFF51B63D380C53FD17DCA7B76E782580D926865A79AA4992FDCA771F4232AD68956FD1A32547588967B402B2C8E3F2264DC7BC8B5B770EAF4B413C1AC44A8A4A4061CC7FBB7ED809FD97C84B6190BCAD156E36018C44CC0C5A84E9A872EFCE63C370BB257C46AC285058FA09981D1CEF51A07C44994E68C9C3CD6394EEBC545F438858A6D18583E51DC6F15D7B0A3F02F26932928BA1F281012D93050A8D6685F5364FF952BE1C644965A3D255A57C95308E100C4AB8343E2311927212E6F7975A9FC7F72B4761B78069DC5553D740A5433108099BEBC850D5477CE3708474481026931082946A949E3B00DF640AA52BA6D04AB3C1115C91147BCC519DDE6BB953582DE996D38F35A51C7AF0AB999FD78A9B34BBB209869D3776C038B0211849A26674D5D5698904A9DCE362D6BD858B3848B9534B84DF672047930CF63B51D91C79321127FAC7778068E7AD26EF3E61E98B757F33127895A07BA6CCC0FF72A5D5288FEE49AD4D249B7E1B9E695B91EB147D0B1AB2CA4299F9A655BF25AD3B9ABC9B36EE98740FAE598F17113D3C8912BE694A30207E86A5633A32D2335ABE7D7267AEC476771B0A7FC8B0DE2BFE524423D0304AB19632166C790DB214D478726BC7A281685B2CC77E0BB9F8BA64245A80A5DB5570209B1FB0115EEC3CF67383F35E7C05ADC02E8A2BBD1994BBB8818646BB9B87349FDACAE5EC681D40A03A506754700665F4B372C2009711AB55D0C0D62506FD4701EC69C45EA8ABF7A20BBB2669AC460B6728B6845476F49DBCB927723DFFB7A224CA159DB7D1E63322F22BA31B6BD04DACA1E30AB53AA686B8704B483152516C94078916C32064FC638467329CA008784847275A61B44E758A6828D5377747B8848D2557DA23090865A58BB1C7E7448AB61796697DB1B5A70C881CB80A9438F13C8753D662ECD495E74B21CDFA177544A3D5F8912634143938935B4F26874231C2D58BBC0F38DAE4135955793596C4DD44798ED9A57FC178A3AE245DE19BE18C1604A06D041C5560E69093854999EB93D1F82447103B54D46383FD084907060FD756DE74B6FFB3675E0ACA92AC5A89DA53CEF6042FE0CABB55C68DDBA367CB999D7B0B935E752AF97196D465E57081E0378465CC6BA56234CCA1283E22334E65A7F56787BE509C1AAAB0942B39D58167DF5421E35B6C966965FEC61BBD8AAADA8B59DD0664DFCA12F88B92A1CA158E7D5B5B5401DA7D038CE9A70FB5C890E52A19CD32DE63219DD94981CA9B269F0C4BE40CAD533C6DB8687E4497526392B848C4BB706AAD7A68AFFB4AC3D21385A2CB4DDF369D41B6C978719C8D121D43551ADBC54F74CB3528655ADC64F62C59461C3531B6272A4AC9CA013A0203C6D8ECA72D67189DC07FCE68847F0053CA9F0F6AFC7D795AE4ECD3D8E6A02\",\n          \"dk\": \"E4028F81A3115D24B0661A525B546BBC901FF645B4A8D9127E500279A0B67C028DB6E660343012EEC35B8FC5B25B61374B19053D910BFC716E08517187B191582971C7A2C1564BA8B2E6C72F417681224DD3F663719032D0649C4F69229CCC9FB9E6B775D3376708704B5A715B67766CD045DCBBBF861B3170E44EDCAAB276C6190A5B188401353F61C4BBEB413811015BF273C760682F414CB74ACAE3624B9C75036BB32E6113BC59A2A9B0770E928C445B53030A8B262D34950C930BE7ABAF65376C0C90416333B2413C4F849A341BF0124CAA691CC2C2A8C328077302FBA57314E0747569BBE664339219867E42A0E1E92332A2103925CCB85C3ADD190CA3317E0DAB7CFAD92473CC5AD7C31A412417C858AA846721B268642F9641C05A338FA77ED5572A2AFAC80AB0746F07740BD8925E856225D61F73C1BE78381FFB751D6CC456FB488D143208C6E849C0DC10FE295148293251B4300FD72ADBA15BFE82406CAB7E0E4C4F7545869737B46B669EA45470BF9894BEB9CE8C57C4E18042D071C0D58C27AAB46C05446EBF69CA18E45E63039E12A12ABFB3C291692016182349DC805725334DE86E8971449EF7AF09D52089872314757110ABA0BF576C98C1A9D8355DD45A3D842455017A4156101931198920354B0BB66ABD7BC4A2741FB5B06424C14D07104460F3B1A7C54DE8BB6C7AC6091D5B7FDB91BE0A4616AA412C807799D23379756658B9F39345BC5DCF9168AC5B22B2E94BF43071D241129C06714FFAA4B74564B402521942769A391D9CB3C15F7572EE2407ABAB96ED161388C6B76346504650CBB58025155B1527850342E9354804A16626ADD0305532A6387F45781BC080011A1680716C7C1B3CF72717ACC91ECD189A5B03C7ED561DA771B68CF275684C2CCAFA8711F34104E4CFD8F6BD050B5E196564D6D710965740C9A533D1136F7009B9B69CB6BCC388CF7644681B4C2A475410934840A6AA0E316357FC8CFC612E0A57C7ECE8321264A8BE56C468E8C431A0CAE612056D520AC26847DAA4BC71BCA9B3E739D454A4C7B3808CB633A603C22644361611A9D363A407E99D55C459BA856F8295B742357B0BC7C53436A58B434A0280473775B3B3837503828B76D8BA5E182E3AC8154BC99A727A70BEF0B22E631BC56B631276898550443381C1DA223E2CB3A0C38236F35200E1F9C67EA42CA9E620D148209FFBBC07B333BB2B8013233F4C908706F87410C6829C44332C084DB2E0489894593D9627E9F370DECBA867788F9FC99B81127F6EF446412A36E3B2286FD013354C58A04B32E490B7AC53653BD2A15A096682D01689194A0232BFE522B7A85275A5834DBD105D9AC03C3B9416F344302B72C232890390D303DE975B0CE6AF800B0F587A17CA759BE4315EF9D4A3C27033FDD290646C4EBEF4964DB66BCDAB216A80737B2B674F5170B1620E868741792AC8290C1AA21B1460C61B367367A5A084800652E8C9528431AC16F73138A69678B218434463AB5587FDE45ED194B830F02996D81A2C92BC6F524448CAC8B4A5795C7A423625170DEC2120D437BFA96F0E8078771C9AF42943E8A51CC89C9759D3A92896C5494CC584559742FB564C068811ACB6D6525398362B71938C1C721695157F1A2BC24680BF6E265DE8726594C26CBAB9B040462B4DA30402D70EC050958FF5944D181374CC9A29E867C1B33CC36CBA66C66A75F44CD68112B543AEE0026D9C105EE556AD5FC2989346C7473C93E2641AA42904C7751D83CACACDA6B2175B1B9C1C41435C49027AA21119428C61482F655D4A0CCCFA5315100C314B680F702CAD99FAABEAF08335257F9380066A09BA0D4B04F91A52FBC09F3C348F0A49B840BCBFF51B63D380C53FD17DCA7B76E782580D926865A79AA4992FDCA771F4232AD68956FD1A32547588967B402B2C8E3F2264DC7BC8B5B770EAF4B413C1AC44A8A4A4061CC7FBB7ED809FD97C84B6190BCAD156E36018C44CC0C5A84E9A872EFCE63C370BB257C46AC285058FA09981D1CEF51A07C44994E68C9C3CD6394EEBC545F438858A6D18583E51DC6F15D7B0A3F02F26932928BA1F281012D93050A8D6685F5364FF952BE1C644965A3D255A57C95308E100C4AB8343E2311927212E6F7975A9FC7F72B4761B78069DC5553D740A5433108099BEBC850D5477CE3708474481026931082946A949E3B00DF640AA52BA6D04AB3C1115C91147BCC519DDE6BB953582DE996D38F35A51C7AF0AB999FD78A9B34BBB209869D3776C038B0211849A26674D5D5698904A9DCE362D6BD858B3848B9534B84DF672047930CF63B51D91C79321127FAC7778068E7AD26EF3E61E98B757F33127895A07BA6CCC0FF72A5D5288FEE49AD4D249B7E1B9E695B91EB147D0B1AB2CA4299F9A655BF25AD3B9ABC9B36EE98740FAE598F17113D3C8912BE694A30207E86A5633A32D2335ABE7D7267AEC476771B0A7FC8B0DE2BFE524423D0304AB19632166C790DB214D478726BC7A281685B2CC77E0BB9F8BA64245A80A5DB5570209B1FB0115EEC3CF67383F35E7C05ADC02E8A2BBD1994BBB8818646BB9B87349FDACAE5EC681D40A03A506754700665F4B372C2009711AB55D0C0D62506FD4701EC69C45EA8ABF7A20BBB2669AC460B6728B6845476F49DBCB927723DFFB7A224CA159DB7D1E63322F22BA31B6BD04DACA1E30AB53AA686B8704B483152516C94078916C32064FC638467329CA008784847275A61B44E758A6828D5377747B8848D2557DA23090865A58BB1C7E7448AB61796697DB1B5A70C881CB80A9438F13C8753D662ECD495E74B21CDFA177544A3D5F8912634143938935B4F26874231C2D58BBC0F38DAE4135955793596C4DD44798ED9A57FC178A3AE245DE19BE18C1604A06D041C5560E69093854999EB93D1F82447103B54D46383FD084907060FD756DE74B6FFB3675E0ACA92AC5A89DA53CEF6042FE0CABB55C68DDBA367CB999D7B0B935E752AF97196D465E57081E0378465CC6BA56234CCA1283E22334E65A7F56787BE509C1AAAB0942B39D58167DF5421E35B6C966965FEC61BBD8AAADA8B59DD0664DFCA12F88B92A1CA158E7D5B5B5401DA7D038CE9A70FB5C890E52A19CD32DE63219DD94981CA9B269F0C4BE40CAD533C6DB8687E4497526392B848C4BB706AAD7A68AFFB4AC3D21385A2CB4DDF369D41B6C978719C8D121D43551ADBC54F74CB3528655ADC64F62C59461C3531B6272A4AC9CA013A0203C6D8ECA72D67189DC07FCE68847F0053CA9F0F6AFC7D795AE4ECD3D8E6A023B1D861C34DA182BF4DD683ABE8D247898E71E95E27AF72494C02BA6FF3C8147C71F7E44295978FC63BF8F6A68F8609E98D155FD7A74E1FB7982733FBF8A6C25\"\n        },\n        {\n          \"tcId\": 41,\n          \"deferred\": false,\n          \"z\": \"EF668FB41F49E82EE0FE00919CC06507548321593A7ECD1D2112342608D95FFF\",\n          \"d\": \"8D6DF2EB3DDAF961FE5EB556842B758BEBC7ECB312B6D4628B323F483B77D6F9\",\n          \"ek\": \"499B0EFC9A67CF754833785F22E7AA1AAA43EE135195F42410FAC0F90A6C7658BE53354D81939036C88A44801707CB2C743363D1623DF1C1C0EA474521979497178361A264C5C68DFD5484D6B19B1734CF6C73521266B4B5D81872F059A22C2A9219CAB4055FD8B95661CBCE716694DE5414B4CA0AB0AB3231A896AEA0883E6A6454F0025CB79894D83179F1036378A423D55005C973A297ABAD1B7321CBB89B6875F3B9522A27C83D7716B01AA8523686B3EA5F9F3722CD715A833CBB7AAC7AE3E24F1F08474E096D31EC3B608C3C67C1340E01B9780A09155C03C7971D54E0C9C7A6265C196C0FDB8E0D6C23D549708B9C2E2E7366F146C2BBC290673030BAD304B67A6133B3B22A41359262AF706785EDB44AA6B58EF1B71C69B4C1097230B90702BFD7657464991C46C4BA0775F807C0ACC14FCA9C31FDFAAE5FE525BD9538933C843764B7FC130322235823D55637B721B20B8825603A57862064517D2DC54545A0A7369270B7B15ED2B5265A401532F4ADCFFA3C94A5B6E89660530800CD317B45522AB8CA8E3EC71259FA60E97A808FC49EC9E998DC703FBCB3308069019DF8138E3B9B69D71F9FC708EB1364E7F7C2DB73765F13A53506AD03205EB5AC5DA86B24E322898B79917753839C5A63F400547D75B426F70D479A6340278D0607C765857E44A2A32AAC4A3D324BAB221FACA212E1A9315B452C5FE25EB0344138A1B191F8044C9A4CCF0901A66C8FA56AB468BA4513E8A045C6621F22719762C37C37C6E8C5C04CDA7A02FA287A2B04BD88BAE4A94DFAB47733D74B056C7541EC992372104A470B60F20E394C6994A13932236532A74B716A8D86F5ACCAE10CF6C1708D079766A7B5760474A9E351ACD6774A627CBCF10E441894940164AAC6554CF73EFA6A7C487216FFC93939561130F107E605C5A20A8B3A280F27942F7CA353BEB66EE67A2CC2AC68CD94310E8B67D597AB33368C83179F8F8124180494EC253B395114FDCA9E27FCAC8C7A0B1AE40D31581946C5506B5063E2050D0CCB6AD713024A9C4D08D09D211486ECD371F5033C8804C0531180B99447FEDB91181C327DC000E7159C1634013EC00C03C4C5ECCBA79DD138F20B4975F27946566A629A4819E7C77EE4625CD91C86615AD1A876E7045196E6C09EA22439751304D92B9C2B3C9BD32A6CE010C1BC5608245B76008A58A4A4C9072EADCA4AE103B63487366D130A5135C18FC4C6766CC8E06B988C51AFA6D1CD44133ECFD539BB0B8CE92A3FDD36127B9C03259161E8441A22F0436F9B1A0C57BC8BDC06371078A2C9548442310B4A5479F0054ABC375E75A1D170A949C91865326A76768A2F117CA524CA8047B26F7416FF677DE0BC07D518C5F71686DD544823DBB9622706FCA2A2112472C352B6BBFA0A3A7C2280CC85C35203A6DBCE3D3C4E8F9C1D454C730192968892BD257CBFC06179EADA01362BB1D1258E9F1BB563565535908803822969B3B2A21633390961891A2DC436B55EF392F0379EC484967A9068E99BAEE1F461241070342221660101651B2A2FA896934141DBA9B905E8C309F8981CC5B080C26CB585090EF97C4E01B8ECDB7F5DD88C4F8B85765B7908256D350898450C3592526E6A99B504A0D3D19FF79680DADC9890E865104DA28FE012506D19180565\",\n          \"dk\": \"073C0C1BB67152B5AAB7A58461B1AB82C3BFA8CBCAB360B48CE83CE15B8998579542183C8106743238809A43B848688025D2CAE11857BD75986BC0B2516552F640807A4625D6E28816883BFD48966FA2A39A3486892382BC799FB0C6623D687A2D7A9C6CC0867C2C96C162C272425C503CBB3DF71611565B43777DC805A799FCAE6895C709760689E45FB61779F1FA72EB7628CE299DECFB6D25E883D431069A3002BCE8AE00145AE57785D2F82E76F59B98FA5A5D31426DAC1F394B954377A6ADC243E67A8C05B389E3C013A7F98131DC599B6918B79C665D73C0DD676DB7A864534A536A75C108EA79CA267684812912D8711D32225550788E22A23B861C261083B9A98A3D325A08C828DA42753AB959E4C795FD370A1379C9C926038DAB8BD281AB46FB3D8C1795D6144C25CA1073F00C85A27B441034BE185B43AA0782A70EFA995FDDD35E753B226454A0C5998A8B6C0604991E3942AA699B91C6B17C67B373CDB05A48B05CBBD8B3F1B1139DCB18B15704E221179FD95478A6280DF1518FD3ACE9837E9701503476C230E582AB4BA5F88CA5ED306EBA481BA51B5FB5454BC96100CEACCE14B11A43915E44B0067C44408980CA872B568ADA7432A6B50FC6416DCA06D62B96F701BCFD7A48DB251D6949AD368779FC2858C6108D0F7C0AFCF9A7A8C0A6BC27BF76603B74438B5DB833B3AC8516EB3D99BB24BA310D12EA23FCC0111B02409FC670A1DBA9AED13849C78DFF1229B8CB4B3742046C4744BAA3067F456E8625974E3387662C6ED3B2A1B24262BC8A484EF185F222124C9B3ECAB4B7C4202C64309277B0A28B314C702B72D1C950647C6A27D477E50AC3B675593123961F941750192212B663EA79BE4314096B7A74E7517FDC6818A8C913C59C75BF6A72A2F34F6A77816B9354B48989A5EB62E250372AF04DBB56332B6545AB254CBF9867E354A8F2C4B614646F6F316A8294AE497B3B7E12B6D6953D2A764BC4A22DCE9185C7AB0A939883B2A48531B374F3C499DC746170650BE2F08D0AF4785094C96F3423DC5A8D90F536AAE1C3688353D9A67FF2E01E94CCA7E67A5F9842762B30BF8FB729D0694DAF93397A360E8C200BCF8B7A68B9BFD43159C89C6C88E389B5237CAD95BC85D366DA09BD2AB93A7E8B8CE58AB44E93680201605EF434AF729E3015A3F0F5A69C273724B557BC218360A31C88EB150A545C4BF7922852BE48972D2F479530216F9E75B4F33967214CBA93E601D5D536F8FA3083FB3E5BC6A752F9C3F87413DF31B0235ACB53EA6C96A74D5E02A22FFA4EA8805B0B721DD3A554108000A0BB6F25F4B7FFD11F4CD15F5F34201FB958A8D13F9A530E66B452FC8A160923928B2B9C9BFCA4DBF374923A301751175621A3EAAA2C046AAB0CD42D3B693E4AC19E7171A0ED87186C7C8EE4C576B9EC9A249B7B346B2119B32C78324BCA7C51497458A6E46A24B60E84CB583305BA0A3194E74A5EFA26A1E221CD85B2CD963263C134B2768C3ED37636CE59262A1969C926B29481066C76C17CF80813AA2C30100AE0F20B3C99395E9B2B3438CDDA5857F652A646777F58B38C86EA3F7BF2021ABA4B32070CC8B40CDB7B361AE3219110A00E55249C498F499B0EFC9A67CF754833785F22E7AA1AAA43EE135195F42410FAC0F90A6C7658BE53354D81939036C88A44801707CB2C743363D1623DF1C1C0EA474521979497178361A264C5C68DFD5484D6B19B1734CF6C73521266B4B5D81872F059A22C2A9219CAB4055FD8B95661CBCE716694DE5414B4CA0AB0AB3231A896AEA0883E6A6454F0025CB79894D83179F1036378A423D55005C973A297ABAD1B7321CBB89B6875F3B9522A27C83D7716B01AA8523686B3EA5F9F3722CD715A833CBB7AAC7AE3E24F1F08474E096D31EC3B608C3C67C1340E01B9780A09155C03C7971D54E0C9C7A6265C196C0FDB8E0D6C23D549708B9C2E2E7366F146C2BBC290673030BAD304B67A6133B3B22A41359262AF706785EDB44AA6B58EF1B71C69B4C1097230B90702BFD7657464991C46C4BA0775F807C0ACC14FCA9C31FDFAAE5FE525BD9538933C843764B7FC130322235823D55637B721B20B8825603A57862064517D2DC54545A0A7369270B7B15ED2B5265A401532F4ADCFFA3C94A5B6E89660530800CD317B45522AB8CA8E3EC71259FA60E97A808FC49EC9E998DC703FBCB3308069019DF8138E3B9B69D71F9FC708EB1364E7F7C2DB73765F13A53506AD03205EB5AC5DA86B24E322898B79917753839C5A63F400547D75B426F70D479A6340278D0607C765857E44A2A32AAC4A3D324BAB221FACA212E1A9315B452C5FE25EB0344138A1B191F8044C9A4CCF0901A66C8FA56AB468BA4513E8A045C6621F22719762C37C37C6E8C5C04CDA7A02FA287A2B04BD88BAE4A94DFAB47733D74B056C7541EC992372104A470B60F20E394C6994A13932236532A74B716A8D86F5ACCAE10CF6C1708D079766A7B5760474A9E351ACD6774A627CBCF10E441894940164AAC6554CF73EFA6A7C487216FFC93939561130F107E605C5A20A8B3A280F27942F7CA353BEB66EE67A2CC2AC68CD94310E8B67D597AB33368C83179F8F8124180494EC253B395114FDCA9E27FCAC8C7A0B1AE40D31581946C5506B5063E2050D0CCB6AD713024A9C4D08D09D211486ECD371F5033C8804C0531180B99447FEDB91181C327DC000E7159C1634013EC00C03C4C5ECCBA79DD138F20B4975F27946566A629A4819E7C77EE4625CD91C86615AD1A876E7045196E6C09EA22439751304D92B9C2B3C9BD32A6CE010C1BC5608245B76008A58A4A4C9072EADCA4AE103B63487366D130A5135C18FC4C6766CC8E06B988C51AFA6D1CD44133ECFD539BB0B8CE92A3FDD36127B9C03259161E8441A22F0436F9B1A0C57BC8BDC06371078A2C9548442310B4A5479F0054ABC375E75A1D170A949C91865326A76768A2F117CA524CA8047B26F7416FF677DE0BC07D518C5F71686DD544823DBB9622706FCA2A2112472C352B6BBFA0A3A7C2280CC85C35203A6DBCE3D3C4E8F9C1D454C730192968892BD257CBFC06179EADA01362BB1D1258E9F1BB563565535908803822969B3B2A21633390961891A2DC436B55EF392F0379EC484967A9068E99BAEE1F461241070342221660101651B2A2FA896934141DBA9B905E8C309F8981CC5B080C26CB585090EF97C4E01B8ECDB7F5DD88C4F8B85765B7908256D350898450C3592526E6A99B504A0D3D19FF79680DADC9890E865104DA28FE012506D19180565847F52D9587DA7DD37F7AE07BF1B9D4C94F03C702351FB4C5AF4200EFCA07F38EF668FB41F49E82EE0FE00919CC06507548321593A7ECD1D2112342608D95FFF\"\n        },\n        {\n          \"tcId\": 42,\n          \"deferred\": false,\n          \"z\": \"26345937ADC9104155275E7114E93D9F5847EEA73A9359358585B2D42301A294\",\n          \"d\": \"DB4ED8E9C3E1AC7A35EA4B67A4EFCFB46972A984D161F79F084125D6D4AEE7AF\",\n          \"ek\": \"7BC585BA01AEAF899394E3C00223562CA2163F977AF21CAF967A9C174B3719B097C440A60C5AB746683F15294E4CE40031F16A6FDA79C608095EEB6945241B25A77DAE55CD95161C94836940C93665D669745A013013875A722FF1B285047C9270687FBF2310D1510680406C70CB97A65CCAB59AC6C80C4937810A2488BA8C37C6CF022E4FC14135D2100B613E3BECC8D74261C0912683067429DB79CAA56469CB537E432257EC2EAF370590613716C64FDA2A0EFEF27727E5A437000C9145919158BE240B426E63AD426018FD3271D899846E6421277C511FDB14CDF1B16C48B99080B6FA68BBFB4C02DB837C65E6CB02235412AC3534FA91AAD35DCB3AA773FC2A050975ACB0C7928201636A7920F46DADAAAAF9072F6B23B138669BA4C9655A660F3911AEDF97633ED966CD1253CD7B947C09839CA53AF33B80A370AAA43C1ED6D26D64981D756060A17331A3D4B452FC34F186986936C2EDACCBE0629DB60493BFA49451D44BDEEA1D6605BE945625654A6A6C13943B2972D0A95CC07B70196579ED9013B8CB5BEC81AA61066B5AF6673B3681B21B65905C1E44B808B1995819B211D0BA9D4AF45E6C030D75D2C63E77264BE92DDBE1B54BD54060CC4E8A760DDC124EDBBA6DD8910922D18222AC42D802A1A377C0979B3D9CF8736ABA7EF223413E6712FA523716DC5A0C41CC3CC462C6FAAD0CF30674EA8469A06B1FFCA3D6E3AF0AE95689C70F3325B3BD6C3158BC36014B836F908D91269FB5B05CE6075A80D4A70562572A64AE090C9C39DCC5C6197D84C62752E30DF9A49657DB5ACA0084393A25C6047E629151C8BCC3C4897CA2BC8BD3EC4115B8230A155C15775C7CB7A3DAD6560E6798C57A607FEC8DF6493C24A11518280F081BAB5359AD5468B1717560F1E60273518ADA29819E11679D16CD20514EE44989C62582B0E79A1C09997CE67B9E46850F294BA940C7A6668B28EB6C953C81F50C2B94F14A0DA76DE5AAC603395ABC8866B92B88890B120A62CB06CA8F0F14806E19A794F73FDBB1A9638169FBC30BF4C764F66475395515683C9CA4E587312B4301547C25B58108408E64D6BC0A6B9BA8A4CEF76649290A185BF25DF5C14B81F633A399325380A91B5C2AB21399C4C88597202A50A966DDD8AAFD4A2A831897385AA5E19480437B5E1EA05F778BB8C616391C2766F1DAC500D1898AF957E022202C33BEE93001F7E56B0589C25DDBC5CACBBDBBA308B30228C5108019280812925E395329D3A72B6566694DE690746A1B244AA21B879E54590A4173197C2C930E683F645A83F6B288D3A48D00A9CA97180AEED5685691B8FCA95ACA9136EEE892B5F46CBF60BEE6D803DDC05555349D4FD1AF405C78D6B86E56C12A26657832D753B9D901557572E7960EADE5A9110CAD2C1353A415BD119BB8A42924B0BA004ECBCEBC430F13B33F82AA1D4727CBD8150F416A14AF1A6CBEE094D0EA963CC4C814E6A9CF097DF9B9BEC7D9B68D5BB23F767F1CE88FD44B5539F80F2EDA4B887576107C725A3A5D80240397907AA15842A400CB6200325B1542E268BA3C054E350364C836370DE786E5077EBB4B4C8961917870019C7A4794D27E332C14D26B6BC02202CABC83B8E9E01A155B3D735CAB97C80992BE75623F2B79E41453AEF4A09F6CCAFFAC73\",\n          \"dk\": \"EE528DC327151C18138C507974649A95121F768132E6C6647F2A193556C06040C80058394883C01D75763041146349BC8FA72E57BA7069656B2D138B85784C79B11E70025D5F3558EB8BB418CC8BEA139310F850F3557FD56A10F0D297BDC0C0DC3B4916CCCA4A419B5DD6A8F8F6B0F1AB6BB2B606C73AC36C0C93267A19B7C7CB82AB9C96CBA726770C5475A6B889CAAC178B8882BFC236CA32197523E95112697F7B75248B3C12F132B45535CD55E274706242A69644429A1C3BAB673B580C7DEC34FF7379AD073241156477D1798D1AB09AA5C53EF195E65A7C15E5569781A9B06376F77337A1E56506C785F5D4C95FE05BBCC736685370648A90320C059CE45E55841F08CC8024C24C473646B128451047B44F1C526FDC4B8D862DA60A97AD91CCE133B5BF7C70CC0A23B458380602882EA2741440CF05B1C960699C8A404444A19345F113DE341279C11B58A7BB6B903DE6054D55DAA4A501741E6953739564770205571CC93861A9D5D785F273338CC90432D00B64A488B023AA5EEAAB0543CB34916B55A5CD69380A330CC194640D41048745FC74CAEA0FC8C95A03FB40F701B763E73FFF6B1C8D2434D2646BA3C870C0A29CFA45A880380F6675CA36AC1A86C57C56A16BC6E5A9ABEC7404F86209B3AEC736A8E436407D2476CECB799A880AC8AC33A3F58C68CC047480BF85258B6C1C159B01BEFC73B7BE506BF75C67DE85893DD2425C467B2F627971925FBF666B80DA59DB285813D688916109EFD2951995953BA729DF127018B7043AA3A57BA33AFE2782F8F319C754C4E91780B446C4D3360A824223657405EA34AB1DE6181C702AD100CADE0380BE942EDB51A93D1A0A884308352CB2D93B920F1B467FC306F6A6AF9E41334F861360646CEFF6770BC1B7211A1D2B95377546A0AC4BA431F824A1953BBAA8BC9CD66789E55EB2C8CCBE4B51233C2578B8115061276B48651B63097BE21BE45868EA513F69B91F6455CA569C1C26317E0E1807C7B7B2D4D43F16958C2BA32CE0585E2DD1991B357C72239817B0484B3258ABF39988933710528E82F11BB8E798CCD5C323DAA16C9C0786C1C73AE396946A0EEDE43FC7568C98D056A1766AD702B4F6478CAC056410FB9B95A9427C32A87C84BE3C9A21C0CB46567ABCB3D1CCDC3B8ABA0A1BBF18A6D23474E3659BA696197E059B6F536523E60C0C69314A12312B37CE51686B5CB742715BBC4F241ED12B342045BA69010CBD24597DA695A331C4B8E7C693157A348C9E16A0A307900D86A032E08B5E802A29842C13DC7509D432C5AA7B679BA10BD090CF9FC086E0C5C2AAAB208F39179E693645672123D894EDA35C5E32031DDA9DF410C32F72BCA22B5645E7A5A9C804A45BC67A861A6FF5C821D9CF79E6BC4D8386BF77AB31C6BDA9DC6E99866B5E43193D9210A790CAA629BAD25304E6A946B3D87235CC39F5B66EE8B4C6EA73022B867C6CDAA133A0962579612C8CAE71BA151754B4862C7193994934F04FC3BB2C20A71CBB6962C5D16224D1719555143E33A0E70B79A2D6134CE0BA1797C8A2F98B71882D5BD214E73A950C849FEA2006B242B83F5C8998113BD27C01C3276FC20041ADE77AB144CBF6368D45C18C7BC585BA01AEAF899394E3C00223562CA2163F977AF21CAF967A9C174B3719B097C440A60C5AB746683F15294E4CE40031F16A6FDA79C608095EEB6945241B25A77DAE55CD95161C94836940C93665D669745A013013875A722FF1B285047C9270687FBF2310D1510680406C70CB97A65CCAB59AC6C80C4937810A2488BA8C37C6CF022E4FC14135D2100B613E3BECC8D74261C0912683067429DB79CAA56469CB537E432257EC2EAF370590613716C64FDA2A0EFEF27727E5A437000C9145919158BE240B426E63AD426018FD3271D899846E6421277C511FDB14CDF1B16C48B99080B6FA68BBFB4C02DB837C65E6CB02235412AC3534FA91AAD35DCB3AA773FC2A050975ACB0C7928201636A7920F46DADAAAAF9072F6B23B138669BA4C9655A660F3911AEDF97633ED966CD1253CD7B947C09839CA53AF33B80A370AAA43C1ED6D26D64981D756060A17331A3D4B452FC34F186986936C2EDACCBE0629DB60493BFA49451D44BDEEA1D6605BE945625654A6A6C13943B2972D0A95CC07B70196579ED9013B8CB5BEC81AA61066B5AF6673B3681B21B65905C1E44B808B1995819B211D0BA9D4AF45E6C030D75D2C63E77264BE92DDBE1B54BD54060CC4E8A760DDC124EDBBA6DD8910922D18222AC42D802A1A377C0979B3D9CF8736ABA7EF223413E6712FA523716DC5A0C41CC3CC462C6FAAD0CF30674EA8469A06B1FFCA3D6E3AF0AE95689C70F3325B3BD6C3158BC36014B836F908D91269FB5B05CE6075A80D4A70562572A64AE090C9C39DCC5C6197D84C62752E30DF9A49657DB5ACA0084393A25C6047E629151C8BCC3C4897CA2BC8BD3EC4115B8230A155C15775C7CB7A3DAD6560E6798C57A607FEC8DF6493C24A11518280F081BAB5359AD5468B1717560F1E60273518ADA29819E11679D16CD20514EE44989C62582B0E79A1C09997CE67B9E46850F294BA940C7A6668B28EB6C953C81F50C2B94F14A0DA76DE5AAC603395ABC8866B92B88890B120A62CB06CA8F0F14806E19A794F73FDBB1A9638169FBC30BF4C764F66475395515683C9CA4E587312B4301547C25B58108408E64D6BC0A6B9BA8A4CEF76649290A185BF25DF5C14B81F633A399325380A91B5C2AB21399C4C88597202A50A966DDD8AAFD4A2A831897385AA5E19480437B5E1EA05F778BB8C616391C2766F1DAC500D1898AF957E022202C33BEE93001F7E56B0589C25DDBC5CACBBDBBA308B30228C5108019280812925E395329D3A72B6566694DE690746A1B244AA21B879E54590A4173197C2C930E683F645A83F6B288D3A48D00A9CA97180AEED5685691B8FCA95ACA9136EEE892B5F46CBF60BEE6D803DDC05555349D4FD1AF405C78D6B86E56C12A26657832D753B9D901557572E7960EADE5A9110CAD2C1353A415BD119BB8A42924B0BA004ECBCEBC430F13B33F82AA1D4727CBD8150F416A14AF1A6CBEE094D0EA963CC4C814E6A9CF097DF9B9BEC7D9B68D5BB23F767F1CE88FD44B5539F80F2EDA4B887576107C725A3A5D80240397907AA15842A400CB6200325B1542E268BA3C054E350364C836370DE786E5077EBB4B4C8961917870019C7A4794D27E332C14D26B6BC02202CABC83B8E9E01A155B3D735CAB97C80992BE75623F2B79E41453AEF4A09F6CCAFFAC7316161113DF646837A28818D9C34EDAD57472944528FFBEC6B1BD204262DCA04F26345937ADC9104155275E7114E93D9F5847EEA73A9359358585B2D42301A294\"\n        },\n        {\n          \"tcId\": 43,\n          \"deferred\": false,\n          \"z\": \"63435E06C2AA3DFB3477120710D5E7FF0DC0DA68D4644A24F66A8012FB193697\",\n          \"d\": \"C6EFA7D5D500E5BF857D80EAE2A6EE6414159947FD4BE589350724FAE5E51805\",\n          \"ek\": \"B07C141713A0AAD60845A07EFCC54C4DD073348261F7485FF93CCFA7000967A24E7F03BE00D4C0AAF13995F2C19D0C92B3F372D75802891B0A97E78917301D429B0AF9C7693882024AD301C5B1448474BD2B39CBEB18332CB6252FA7920D94AEE849C10A907B5FC827B94A58E37378161A4407647D025B590D1A1C50880B2DECB17774B22D3139445588B9BC93F7020FF6A9959E9A04FFD15669F9A8CFD412EAC051B940B2ED02561A4B9F4B57753423C10DBA0587287D9DD068C4660138E6C3EDB94664797B1C702D9D89A1C9D086C1F7A21238B4A22A8F6BE9075D0B712F4567E55C0CC1BB4788F2A693D6088D36331350B3969B66C35CAEE9A015FC80CCCC556F1C034431FBCD2796BCBD9347B39C472B27CC8F36468BB10D4D1767AFF24C33ACB86D7191AB359903F43E73DB80B5F39AD25933E0645013B2AD56456081495B7036B28B652F577C1D251CCF45D3BEA72539D359B94DB50B150AA5D4E56FDC91B2654A4367C650E3E1310B72403135C185B90C5005A0FA11C569FB82A3DCC71AE774D97B171987B9D128516801B9A7D91FA6DBA6E5341A0147963BD29415E90DDDBBAACB242056E487724285BE3756C37538D60BCD7DDC44FB25AED0C24CAA663BD650CA4E806BC8A259E9794AC4642447F225BE560B541BA90C227629B2ADA4245D83E41117A095874CB676144782439CCCE1893D26B7D427CE0060A106903891687C17D636CB865EBE4C28134885CDB7722D84B0BE41638D5A14E18023191B35AE228C1EF92914A505F93901ECF598E5093E31A41611F6726C42A96DC2200C5178CC320EEEB8B223C8746B72847F9B586A229929A3B5617A54F6E56C6CEB018AA0B02DA6CBFB1278015B68BF15BEE2F223DFE4025349ACBBA505BCF592623C6D3FA975AA661E74948757000CB476757B732F8647C4F8C431C85975A09BAA591B95CFFA3D89FB3AED59330164510A303609D0726BF63479116AB2EA92DB8B89859C82D3A0966CD5348BE479AA15B6BB395165777515688D172265C7018D08D59F72C74A682290CD771C557018A686CE1B3451D6AC0D4149BAF16C871779031BBB9C4E70C9602B38A82200D2D251C5B22C60CAAA98FACB66CC3DF2A3770EE282A9DC328658A2D582B894DB7B5EDBB63A7B90788786072698C2904343A1B9F6DB107E5A6CE1129A444B3DEF542E35728F080294330343B4843AFFE144827499539B0C79B2CC29961158F61A20502BF3BB38A784BC8A673F3E680C5F8406056A035FD1A3A5E445DBAC0282E1B7885A891BFB367D236AEAD15E4F5B5CE26A6ABB4840DD177000806FA066380CD381D5BA64CBD3B048D60CCE49975EF778C1F8240A1484D073CF3B5625E23A0E02D22875465687987FA6EA8D42C5CE28443BF726551F6CBE108732DB119581CC9CFED76D6D1CCC5FE132A9B51D6D4723FE2B64C70B65F7DABB8E71C3A876545CD1AE11B835C1FA4629E69DE1D489D085575FC3A677585CA06C594FFCBD17E76162366092C03F2AB951FFB012E89446F047A4925BA350AB51C1526FD79A853B2273C2B68A590816CA0536C4C467D0918DFED84230E9AA89063530204F096B6E18C29381CC9CE4BB391D6312AA804E84B9170E59272C4835BE92CE2AEE6D55F2AF72FC20B0DD71E5A81C39766E3BD54B78372F1C6A\",\n          \"dk\": \"B4FC40D80B09DECC317933826E7985C4E074C65CA16B6C7184F30CF1AA705ED7CDEF878624007ED6017BF9D4170C20928A71388ECB8FD2906D612525F3A8139261419B1799040BA0F5F29BDF683E1BCCCAA819A65FD3286EA55B98B3B0AEA03972A0988E352112F0984E64A36760317EA9BA7267949BB02423D5A53EFCADEA178FC568464859BD634259C6B8AF7C2BB9A462A3F0BB66A15B2BF0956DD7829DBB2BCFFF47A40AB487E5D76A57AB71A2AC6C5B651597F631F37A9EAAE10E42D4588711B1CCF1331ADA44EF532301900A6BA2764721C81B2ABB3280672386BD64D52356737055EA8F9409756A03A5E0B410EBD4463FEA79ABF65254957447B6BCB9989F733BCD58E8CC34D74500DA128247264E0253F0EC811DF199DF1298F87A34710998045BCCB5205441EA20BFD776EC02CC70075B272533C011A25EBC541CE65B54D03149A4C04E950834D8A33996ADBE386411D168C4F735354171B3BA908A208B8B0C8DD95113BBB301D361189DA703372A7458337027C27682C04A0CE6C0C0098CAFE1594BA01787568B944C58BE9A3EE4600444BCB15376648222C167FA317866745D2495D9B30522C830B6C447F935A38588BEF31CA014C5106402933B098F8432938A1267ED4BCCBE24B4CBB45AB3317BC60C8525FB21AF8B5C35546D49A41E35ACC7B24466E1A7B7138A045BCBCB2D98C0EF242C5762CF38EB29A61AB7CCDB94880710B9E7AA0366AE25034BF131012FD0AFCF211312066A667C3739941BE616B3EE1139D890BF4CC499E6720BD00103B29A05E5B993C6B89E281760B853418A2AC0E6A278A2D2C3A213514D486D2B01AC73A8C21A01AEE891A324E2AD45338431147988799FB8D6AF44F62BC386431F3C664E9100B3E4B86B195F31125D58DB59E7858F45506CC5EA11A9902668601AF0B7599508AFAD51799C389BE4C164691515F59C4D02A2CA092A3CD06181C804AC81A651EE142866B563089BA9A6B459DAA746DBC0263E04050AB458FA97682566C3E4949ABA5CB3DE8548AF3BCE2A253F8043C0EEFC96E3E83CE3A427FAC302AEBB2AD104454790BE531B77604A910640313E5CA5EDB6AD8729AFF6C37705E2BDBC9A9917D36B1925AF45A22C7EEBA2C26544FE5821218A19207990FBE4881FB8B4CA76424FF9022C4C61D680B0F1F668B537103CAB1E1640396050CA524465A9F80221B8A3CB8CCA8979B7C85A141E26649C26BB3DF4666280961CC72060814FB6720DA96979F7E25C4A68588A00801A121E60F217D565460D7C1AD47BA858585A7EA696AFC91544B533A7D86AB68C7F81856F03135912BA1D0F69AA73921C0EC87E8B770C1C332B378A9387C0BC64A64D4FE6AB5FEC50CDF41FEA9883BC44A9175A3DFC0CCAFBE434881CA7E2B53A825ACA950AC2050B20D1FA9B36536A34E83B860850DC90B9568547B7246A115A9394455818A2B081843CBFE129D0389CFE27B192673E13C56B9FA38E99784CA5E1CC0529960534C9AC2497524948C4401259B09628A605FA659661C96CADE04B82894116931C63207F6B46675E14549DC767291B579BDA32B6EA16121B97C19BB4A90287C9B6199C904DA6B6A1278BB7A7B77405A9696493A45C8C27B07C141713A0AAD60845A07EFCC54C4DD073348261F7485FF93CCFA7000967A24E7F03BE00D4C0AAF13995F2C19D0C92B3F372D75802891B0A97E78917301D429B0AF9C7693882024AD301C5B1448474BD2B39CBEB18332CB6252FA7920D94AEE849C10A907B5FC827B94A58E37378161A4407647D025B590D1A1C50880B2DECB17774B22D3139445588B9BC93F7020FF6A9959E9A04FFD15669F9A8CFD412EAC051B940B2ED02561A4B9F4B57753423C10DBA0587287D9DD068C4660138E6C3EDB94664797B1C702D9D89A1C9D086C1F7A21238B4A22A8F6BE9075D0B712F4567E55C0CC1BB4788F2A693D6088D36331350B3969B66C35CAEE9A015FC80CCCC556F1C034431FBCD2796BCBD9347B39C472B27CC8F36468BB10D4D1767AFF24C33ACB86D7191AB359903F43E73DB80B5F39AD25933E0645013B2AD56456081495B7036B28B652F577C1D251CCF45D3BEA72539D359B94DB50B150AA5D4E56FDC91B2654A4367C650E3E1310B72403135C185B90C5005A0FA11C569FB82A3DCC71AE774D97B171987B9D128516801B9A7D91FA6DBA6E5341A0147963BD29415E90DDDBBAACB242056E487724285BE3756C37538D60BCD7DDC44FB25AED0C24CAA663BD650CA4E806BC8A259E9794AC4642447F225BE560B541BA90C227629B2ADA4245D83E41117A095874CB676144782439CCCE1893D26B7D427CE0060A106903891687C17D636CB865EBE4C28134885CDB7722D84B0BE41638D5A14E18023191B35AE228C1EF92914A505F93901ECF598E5093E31A41611F6726C42A96DC2200C5178CC320EEEB8B223C8746B72847F9B586A229929A3B5617A54F6E56C6CEB018AA0B02DA6CBFB1278015B68BF15BEE2F223DFE4025349ACBBA505BCF592623C6D3FA975AA661E74948757000CB476757B732F8647C4F8C431C85975A09BAA591B95CFFA3D89FB3AED59330164510A303609D0726BF63479116AB2EA92DB8B89859C82D3A0966CD5348BE479AA15B6BB395165777515688D172265C7018D08D59F72C74A682290CD771C557018A686CE1B3451D6AC0D4149BAF16C871779031BBB9C4E70C9602B38A82200D2D251C5B22C60CAAA98FACB66CC3DF2A3770EE282A9DC328658A2D582B894DB7B5EDBB63A7B90788786072698C2904343A1B9F6DB107E5A6CE1129A444B3DEF542E35728F080294330343B4843AFFE144827499539B0C79B2CC29961158F61A20502BF3BB38A784BC8A673F3E680C5F8406056A035FD1A3A5E445DBAC0282E1B7885A891BFB367D236AEAD15E4F5B5CE26A6ABB4840DD177000806FA066380CD381D5BA64CBD3B048D60CCE49975EF778C1F8240A1484D073CF3B5625E23A0E02D22875465687987FA6EA8D42C5CE28443BF726551F6CBE108732DB119581CC9CFED76D6D1CCC5FE132A9B51D6D4723FE2B64C70B65F7DABB8E71C3A876545CD1AE11B835C1FA4629E69DE1D489D085575FC3A677585CA06C594FFCBD17E76162366092C03F2AB951FFB012E89446F047A4925BA350AB51C1526FD79A853B2273C2B68A590816CA0536C4C467D0918DFED84230E9AA89063530204F096B6E18C29381CC9CE4BB391D6312AA804E84B9170E59272C4835BE92CE2AEE6D55F2AF72FC20B0DD71E5A81C39766E3BD54B78372F1C6A0B2CEE55AB09D33BEBC1119E3D8268D321CE675CA8233E6AEE598C7652298B0163435E06C2AA3DFB3477120710D5E7FF0DC0DA68D4644A24F66A8012FB193697\"\n        },\n        {\n          \"tcId\": 44,\n          \"deferred\": false,\n          \"z\": \"8C2942B7207C2C59BD56FF9EE0B120B1DAD81B05602623623CBC7E0C20C9B709\",\n          \"d\": \"20859B01DFC60B6109E0234F3CAC7A247D8386099D83D2D447E9A21AF9DE48BD\",\n          \"ek\": \"33C5396C96C17503A1F0D509FF56A842767B24B4C3D2C080D428090E61C88EFBC69D086B83E56F5F898D99EC04F588567B2077B0270BBA8263132C7690645D38AB584C9773AC35389B88C67E34424E003AE741521545039E1C3D2BEB4BD05674A700C150573E0781CE8EEC7D2AD17F2D214FFDA22BE21A55A9570488108B0A555C6D225A521045F6D002F1B330588049FA6BA2D79B552285542BFC3BF3BCCF76D922E7AB9039189A66B76D58D9C82443A32DD26A6B134DD3783EE3480018B04DE6D9561426287C9A8F35ABAFBFC9CFB69107AFD639E9C622A3DC19D0D19EF2C4394CB10E0F519E9C8A9A40F6C33754279ED5C1DF06B065B4760453A4CEE04C67E571565C82D5882211361B0D15C9C0F8A75892A2F1A79673CB31BFF12EA352A484E6CC82B44D1C7C20EFD72F112CA0C25450C672BC34C2B8ABB84C52FAB5B0C66FC2288C48957664ABC079296401C3A470D6BB0D6B231A591ADB1763EF1C522DC95DD4E47728E5BCCBC05A73D7C856309DFEA90E25B7A4ACB4705FD29715441565682620F992034A0DA861C51873212470BEAEA6096B737235CC2C94165723495D7EF43C6327AD9ED398EDF9BE0A29B83D595AC8C46A374B4F02C4B1820095E223092498503AB926B9302C948A439539ACF3C31E91B50190980BAE4411CC652D3108302BB1C4376975AB510D12642A3098BB7FA5B6C14354D6D490FB247FD9C1BF83D8C4DE9C7DC5D58780115D7E0005874BC813B40A6B6ABD1C802C9ABA01A9D893EAEC7EC3AB011EF44AC42154AE40066A72159A9015286C2115213921AC3182372236E7059D6A352626A6DC0C1F7A728FB35A34BCE2C7FBD0659CB6496CF57455E7034E979DF8F134AC398020F153D2624980625DB9AC24707ACE0D7490335561F8EB5CB0468F88DC934B4763846535701B3671C53D30038CBB2896426AA7460338266160173798E36C0C04FA70FF68A56F52A65379031F2565CCF30929A948A6C222D610BAEE964A32C2BB4AE389C7F85ECD0BB733BA1798BA347D142AEB482AE9200B9B4C589264B64A915B3158401B343AACB256D9E8CA942A9770AC4652066B4E16414C78152C3C1D123C2057EA4FD5A511CEA8B3CAB25B9F4258020144924878CED8ADBC9B4409F34DC1F87ACC4165A0F92A8EB364F469751CA87EF9B87B9674CAD2B8628966C7E7D6BEBDBB0B4E6CB51D017EDE9735CD3C3F12B83BF9651788ECCCB2D9B237A330B13B516BB40BD806974528AD133C8E68D839EFC852F3F04E960102E8A58B40615E3D53152C528291826216125907E352CFB53EAD5A33613AA78ED20F9CA3437FD3615980AC118963C8482AC806CE6237853EEC6FF62657A2B19EB6F8678722A06C2698FD15085ADA9222D211A86B4E1BF007EC5906704A69289A8E1F6C4F49A5CFDB90C13BE4771ED4641CB4987BDB80B09724F9A9982AA27AEBFB69D984A5CC04BD94D335500359095214616184993A37EB0422A263ACBE53A6DF5B316378C22E830E2A968661A840C09ACD6711507DA822FE9749DD4C522CD11CA2E57B225BC2C398BB05A975D7E72EAA2BBBCC6CBD88721CCC762049A44A70926778340F5FB7296BC33A8C010089011CB947C5B5E299D0DB66C9C89C5C3581222435C5EAF79EB6C152255ACF9BDFF03781400C9D599A905605\",\n          \"dk\": \"7C63447D4668CBCB42035803A4C9350E38AF8EA8A6296658D7A545B40215A2E7C70AF7726D32CC4B7062509AB15F776E1FB812C22062ECE079492900F0B18DB3D66B4CB2B871766145E237C51023286700238537DE2187A5F45F7671027B88A7CD372A54621D52063705E1B4C07786360805E8CBAA60D9AE953708C201B70DB39DCFCC88FAA2C0FF855C30F31D9C7193616333AF72C382B92DFDE56F48E20689C2C61E940264027923980ED787CD98AAB8476C4C7511A9EC9C0B08D9523D61BECDA2054E1B01B90947AED40961C7A7FF6BBEF64156B243159E4B3D343363DBD58B2060505CB066A31C629084B9E658425BB28D84642B1E216BED180D688415C81C2BCA4A49354709F4ACBB16B8AAF8263329314818FA98FCB7139E673AE702706D3274E020BF469C651FD5A08E6086F36C6451D00ACD94CA59248FD35C91CCD856056439AF18580C066C34DCCEF1C9700DB7C8206384E25531BBAA39CB3760FB346FF8F77DE85461F7198E8C396AA8CAACB8A42C37B1879B80953C1257E82770F098C4896C76A506B8605742C58385DE754797833FD1E08D3E5B45AB828666F15608B6C81DC7166CCC678D1CCF3CE5967B57ADD583C1CCB089C02CCE05B15F810AB3A43335BB0041F0699302972E23A5C229A67578AC9D5B1061CE5B59E72B81EAE01E0740490D84AF88EB572810CEB18C17008308FC860C65B4C1B9065DF9E2C37DF9AD096C96AA0722B19106984A24804ACFB36A9925B5A32D2019E933C54579959FD536992327BE519CD03190CF9595DC5303D4A24AA7D604FCC7BEFEC4C244E7836F63A876073CB757A61B919973696BE0EC1CC15041ACDB2CD9077E15C66FDDC9343CA2A2ECCBCC3FD2319C842F0099969FFA5DB4EA416DD83DAD971197785A5C3173981700ACAC5232C626A5C52B7AF217BD45A555C4898231B256866098518CA1D379822CBEBA148412A664E5094D24D1B6D77BCA7FD176B88856659B9E8513A078A1BD07781FAF1A697A7778C2B8A4A8A0CA48FB0AD8A03E39F7753C742CCA3911B948167DCA397F935BBE0CCEB9D1AD173A8A64E318F9DCA501B031C745939DE94CDAD55722EB2F022495C063259268CF5193B486315083F3B43997C732BC9DA99324252CB503F409340B5A64E74C11733DF256ADB10631C3C002B4E46C340BB99AE3246E794ED09A5524A742DA01076A78B967BC9204F764EC61B528E97176F92F440252FC1838D187A8ECD821E996B5239A1A0F320B12F11CB5C446001D6D765CB11C4A2B24B41F23894A9945B27902A6433CC4BB5B9AE19637B5E1BCD52A29745693F5742970606A89DAB78CCCB61EEA78B0CA49D678C2477B036C48C3D2A514005459728669F8743A23A26865B04A3AE930B6E9C67148A5A2A3643DE2B837610402E6039319969CD928D39794E1311739C0780A393E076C2DC89038A10C1079275F765CCAA064436BF5052051476C1C3BD4167739E2CBE63545148350CAB4134825B2C5D27A68524BBD937957C40F1C00BC06D677E2D5527550876BD7CA3F036DC0B5625F3976BF63CDA2E9B2FD4BCBBAEB911A01B0D3716CE5369F18A285E4BCC6269B407D42A7BDBB88D724C5AA6188B9716BA3D3A3B962A933C5396C96C17503A1F0D509FF56A842767B24B4C3D2C080D428090E61C88EFBC69D086B83E56F5F898D99EC04F588567B2077B0270BBA8263132C7690645D38AB584C9773AC35389B88C67E34424E003AE741521545039E1C3D2BEB4BD05674A700C150573E0781CE8EEC7D2AD17F2D214FFDA22BE21A55A9570488108B0A555C6D225A521045F6D002F1B330588049FA6BA2D79B552285542BFC3BF3BCCF76D922E7AB9039189A66B76D58D9C82443A32DD26A6B134DD3783EE3480018B04DE6D9561426287C9A8F35ABAFBFC9CFB69107AFD639E9C622A3DC19D0D19EF2C4394CB10E0F519E9C8A9A40F6C33754279ED5C1DF06B065B4760453A4CEE04C67E571565C82D5882211361B0D15C9C0F8A75892A2F1A79673CB31BFF12EA352A484E6CC82B44D1C7C20EFD72F112CA0C25450C672BC34C2B8ABB84C52FAB5B0C66FC2288C48957664ABC079296401C3A470D6BB0D6B231A591ADB1763EF1C522DC95DD4E47728E5BCCBC05A73D7C856309DFEA90E25B7A4ACB4705FD29715441565682620F992034A0DA861C51873212470BEAEA6096B737235CC2C94165723495D7EF43C6327AD9ED398EDF9BE0A29B83D595AC8C46A374B4F02C4B1820095E223092498503AB926B9302C948A439539ACF3C31E91B50190980BAE4411CC652D3108302BB1C4376975AB510D12642A3098BB7FA5B6C14354D6D490FB247FD9C1BF83D8C4DE9C7DC5D58780115D7E0005874BC813B40A6B6ABD1C802C9ABA01A9D893EAEC7EC3AB011EF44AC42154AE40066A72159A9015286C2115213921AC3182372236E7059D6A352626A6DC0C1F7A728FB35A34BCE2C7FBD0659CB6496CF57455E7034E979DF8F134AC398020F153D2624980625DB9AC24707ACE0D7490335561F8EB5CB0468F88DC934B4763846535701B3671C53D30038CBB2896426AA7460338266160173798E36C0C04FA70FF68A56F52A65379031F2565CCF30929A948A6C222D610BAEE964A32C2BB4AE389C7F85ECD0BB733BA1798BA347D142AEB482AE9200B9B4C589264B64A915B3158401B343AACB256D9E8CA942A9770AC4652066B4E16414C78152C3C1D123C2057EA4FD5A511CEA8B3CAB25B9F4258020144924878CED8ADBC9B4409F34DC1F87ACC4165A0F92A8EB364F469751CA87EF9B87B9674CAD2B8628966C7E7D6BEBDBB0B4E6CB51D017EDE9735CD3C3F12B83BF9651788ECCCB2D9B237A330B13B516BB40BD806974528AD133C8E68D839EFC852F3F04E960102E8A58B40615E3D53152C528291826216125907E352CFB53EAD5A33613AA78ED20F9CA3437FD3615980AC118963C8482AC806CE6237853EEC6FF62657A2B19EB6F8678722A06C2698FD15085ADA9222D211A86B4E1BF007EC5906704A69289A8E1F6C4F49A5CFDB90C13BE4771ED4641CB4987BDB80B09724F9A9982AA27AEBFB69D984A5CC04BD94D335500359095214616184993A37EB0422A263ACBE53A6DF5B316378C22E830E2A968661A840C09ACD6711507DA822FE9749DD4C522CD11CA2E57B225BC2C398BB05A975D7E72EAA2BBBCC6CBD88721CCC762049A44A70926778340F5FB7296BC33A8C010089011CB947C5B5E299D0DB66C9C89C5C3581222435C5EAF79EB6C152255ACF9BDFF03781400C9D599A905605EAFE2B26CB96B97C22564B28329B64A206331FF842BFED4ADFE3C7A0C4A471BA8C2942B7207C2C59BD56FF9EE0B120B1DAD81B05602623623CBC7E0C20C9B709\"\n        },\n        {\n          \"tcId\": 45,\n          \"deferred\": false,\n          \"z\": \"EAE318341D06E0801C0CA4B873520C714740AD017FE5A158D3BD40960D907AB7\",\n          \"d\": \"409E9F3AB58D736E122EFCC4240BF8388FDFDA6759004D42457018014A335BE4\",\n          \"ek\": \"E74A4568457E0C7727A53682E50CBF74082F01C9892F60B9DC57C79A82123703A8375C7EFBE299FC3352584CADA79726D8F039E7885508A897B9C54EE4478568168820DB7FFD82CBD0191FAB477199AC0FFF615475AA96E28980888B34A1E2C76115CDA09C2E1AF83FD7004DBD14AFA76773F755ACD40358A774045B09075703C2C08537C9E09727E23902CC453965CA74F9CDB865A02E228BE92850F078596F64841D093BC7BB9354E50FBFB31FD463762B800B88C6A691F22A815B46EDF848C174108DD7435F178B90690D5C8B0C430935C69C6A4451629958614CF00F5FCA7E88C08230E142C26C991F5782C1BB84A6C8750EE00348582F12365A761150B79489BEF53BE9BABB468A7F72F65B2D70068D5313D5011016CAA97C6961C554CE6419204AD28A7E5A7E807333DB1C759B6A546B087E9704B63A573752A0ABB7C702548CBF15100F3A6A807C016687C05058F76A42BAB1BE88C12FD574F4FBC051DA21234806FC279D122A4DD879175DC24E20570B435A14FAB908CFA5AA91A009164530417655799C0B379A10E7592EECA96F7AA84F9D718B95F595BF5887F2135D2BB106D599AAB77C5DFD5B4733506450923815F451EAEA6DA2FB0EF3C7AE6EB12543F8429B7B1BA00C5B05585964D334741AAAB7B2C787778709D2CBD9D1C42A061398939869FC0B21C37FA1A146965066C50889C5798B3EAACC0B5C6D7721CEDEC86BDF67A17BA37666A4226BE80EFEDC5262373117777B6995262F795735872B5CD4B52A59677B3ABFA302BA89E61DF76866CBF5C8F97C2620258570735C9B70265CB9AC98445416A6A1AA8252F89667EFE277C968101C58ABEC009902497D932C0D33F52C69221F96EB589E231877837491757BF2B05BE03C909AF3B780B8088F75610A02C45CE28097AAB34FE19CF364A865D77A9D402501C843684C1F7825C20BA3ABAF711172024FE6CB6F58591E4EEC4E40CC0CA891725927C56A55CAF5B1CC61F510ACCAA54371085FC9B03C169293E16FE74735648B4E752847CEF130A3FC031BE4C04EEC8614A7749FB7B41C2C69D2D23D73F205575655BD6A0F3FE19CF333A0F28A9BFE69B0DA112771EB2BBC30586AB0BC0176A400345986104F0832CE81A60E198B3180840A8BC528B0960CD2F26564D3797BE64DFD602BEAD0C94E1904773BCCD0162F0A17B3B4868B5CF57FCAD264E3F339C6E240C2861C6B6A0429616D88F5B41E991491652852441817033DD8F95A4B7C40EC5A74781A761D8ABEDA618D53D2CD86989AFCC54E5ED2CE26F8244E0247052364B0A11F42DC14B6B8A72ECC76754208560A479C2C104941404E22C800B0BB52E26449238235F113A23C4E46AAA4E54B6B7CD2A3802A2EBC3799B9556FB0164500C14CEC67BD36C31971291EE891134B9CB3E2DC01EF15143AC04EA1F41D9084B7B2EB91F96B7C9A97774A044E037285C820601E424F761BCCC712172B774FFB900EFF1158D6EBBE13775F27720FADB93CD98111E4E2788D86C0151B1458344025580C509738E490874B773849308B8D36C649373F9147781D58C31938B923218BC2E85F4C9B1233BCAE99D35C67DB3A85647801260409E6CF45924E4DA141B6C3A65B857A3D3DD5F476EF377E54214616593BC8CD4D05915F4F4FB9A98223663407787254\",\n          \"dk\": \"0D1B18A4D52F6BB6367CB50F0005BC6D9CC52E0872E86AB6B0B9132BA90A309B524E7B3EF93B696D9149CC17BF57A1A1F8371004127AA97A79E4B55CCD079470B04FF38833BBD732FE937B4877C3D643B5E489AC312271A2F9AE7B665FEB0AB13829696EF532F43CA2A848BE9787A873A4646CE5CABA271D82CBAEEFF095F153C7DD66C4484384D44663B1E75CCA0B479DEC62D7CCC97C5495D5CB3A1FA0C6DF6309A4E1B443051B00135166D77433C30B17A68B1723B83129B72BB28C61BC561FC25EEBB53E5E98C86B3B3D852B60D75402D636470E0171FC44716CCC4B9931178FD17B80A4895431492BBB1C98317E45C03D787A30A7C6530B814207C64CA1807D92530BF1EBB232460B68D52021A03A0C603AB6497F16267640316A8E5712DBAC68F3E1528A5B1E37D00856A32083E51958A265532B51DBEB02EB7820430C2974716582E92542E29255863DF251419777142691384C876263A544CE03039710B5A407B8BD7428307A739BA4B84E762C643A383B7A3CF266A1B6C7043D040B0A475B2A6402DDE1AAC192321D910553E2CC8A18CDB9D9B27C5CA0AD272F3145120A266C443A484E829DF079C98DA9CD25F4AA20B29F12A79BE03C7E86F4C3EE277C050831D8135DE1C3227627A732C1C337D047524BB0EB487EF75CA0365520C3E67324C5B2AF45A768D3986A395BFC7A65E673AA41057C8F698436C35D655A9D9FB54F441C1BF54A57E73B335D7B40EC992B4AEC0978267CA30387618C76BDB843A5461578C6938BEAAA0F8719E18469D7F76EA6FC5D73C59399689C1BAA8BA4812111EBBA47CAAD17D050B1E506247050FB62C530D0A1BCEB6D1C45C1DF048F0B14511A709D91F62BA10B4FB42591596456E2B4271FE817914B54DA4181ED7A9BCCC84DDF6614716AA561D99267DC1F8D596AF769482DE440F32B72A7B60DCB0035E94588C02CA9E8628D4A4B071007703E67AC0D87B2CD21B4502782A8BA6E201B6497C80A103003EC360374915E57154771C59ED6CA2FF272393DDC9449038B96B77E0E6C50EFCC7837996EBB3B4F1CD0079C474D9B2172E4947F2ADB7BB947090539AA43B48079133FCCBB92ED878ED76364F31CC094B68FDC371FA8DA78931C326F26961BB20F0DE742039A547C6C79B84B93CBC238C2252AFD504EDFDC14E9FCBF8051A43C75CC28897AD318CFFBE4B70A01373B324B4C732DAF4BA2F3019B5722ABB9D452AE092B89A5CCF7E8776C3222F03C50643B3FC3F8937D437DB19A10EB771FB755CD4939B73A24CFCD01BF6879A5905125C2EBC4903B8F3E91906ED479BA8B83DBC1A21BD6A8222A717FB6398AF89312170362D0486E0969F8E9A417B0C7C7069D8A41A8D9D3C3BEC2847ABC722D3851605C8671C23E0E82A96C1B1AC82C3A1DD758A0D63394F26F20BB6A6669883A87A1AB73AF02549F032A6C51E803DDB131EC47524DA9A283C39EE79AAFC78C569B07B403A89A5AFCAC7EB4400A6A8E6397B3BD49318AA5C37E13757B7145DCEC210C92AFC0186902FA74A604B7DE321488B357A8D9904913335E5A7CBE838E43309DE6C90264803F32338F92B6BAF7D89A112554F23C4A64FAA671D24201C61D8DA67D83AB37E25982E74A4568457E0C7727A53682E50CBF74082F01C9892F60B9DC57C79A82123703A8375C7EFBE299FC3352584CADA79726D8F039E7885508A897B9C54EE4478568168820DB7FFD82CBD0191FAB477199AC0FFF615475AA96E28980888B34A1E2C76115CDA09C2E1AF83FD7004DBD14AFA76773F755ACD40358A774045B09075703C2C08537C9E09727E23902CC453965CA74F9CDB865A02E228BE92850F078596F64841D093BC7BB9354E50FBFB31FD463762B800B88C6A691F22A815B46EDF848C174108DD7435F178B90690D5C8B0C430935C69C6A4451629958614CF00F5FCA7E88C08230E142C26C991F5782C1BB84A6C8750EE00348582F12365A761150B79489BEF53BE9BABB468A7F72F65B2D70068D5313D5011016CAA97C6961C554CE6419204AD28A7E5A7E807333DB1C759B6A546B087E9704B63A573752A0ABB7C702548CBF15100F3A6A807C016687C05058F76A42BAB1BE88C12FD574F4FBC051DA21234806FC279D122A4DD879175DC24E20570B435A14FAB908CFA5AA91A009164530417655799C0B379A10E7592EECA96F7AA84F9D718B95F595BF5887F2135D2BB106D599AAB77C5DFD5B4733506450923815F451EAEA6DA2FB0EF3C7AE6EB12543F8429B7B1BA00C5B05585964D334741AAAB7B2C787778709D2CBD9D1C42A061398939869FC0B21C37FA1A146965066C50889C5798B3EAACC0B5C6D7721CEDEC86BDF67A17BA37666A4226BE80EFEDC5262373117777B6995262F795735872B5CD4B52A59677B3ABFA302BA89E61DF76866CBF5C8F97C2620258570735C9B70265CB9AC98445416A6A1AA8252F89667EFE277C968101C58ABEC009902497D932C0D33F52C69221F96EB589E231877837491757BF2B05BE03C909AF3B780B8088F75610A02C45CE28097AAB34FE19CF364A865D77A9D402501C843684C1F7825C20BA3ABAF711172024FE6CB6F58591E4EEC4E40CC0CA891725927C56A55CAF5B1CC61F510ACCAA54371085FC9B03C169293E16FE74735648B4E752847CEF130A3FC031BE4C04EEC8614A7749FB7B41C2C69D2D23D73F205575655BD6A0F3FE19CF333A0F28A9BFE69B0DA112771EB2BBC30586AB0BC0176A400345986104F0832CE81A60E198B3180840A8BC528B0960CD2F26564D3797BE64DFD602BEAD0C94E1904773BCCD0162F0A17B3B4868B5CF57FCAD264E3F339C6E240C2861C6B6A0429616D88F5B41E991491652852441817033DD8F95A4B7C40EC5A74781A761D8ABEDA618D53D2CD86989AFCC54E5ED2CE26F8244E0247052364B0A11F42DC14B6B8A72ECC76754208560A479C2C104941404E22C800B0BB52E26449238235F113A23C4E46AAA4E54B6B7CD2A3802A2EBC3799B9556FB0164500C14CEC67BD36C31971291EE891134B9CB3E2DC01EF15143AC04EA1F41D9084B7B2EB91F96B7C9A97774A044E037285C820601E424F761BCCC712172B774FFB900EFF1158D6EBBE13775F27720FADB93CD98111E4E2788D86C0151B1458344025580C509738E490874B773849308B8D36C649373F9147781D58C31938B923218BC2E85F4C9B1233BCAE99D35C67DB3A85647801260409E6CF45924E4DA141B6C3A65B857A3D3DD5F476EF377E54214616593BC8CD4D05915F4F4FB9A982236634077872549E2FE7DD646C145484E163D6C36DC6EA5D802A0EEE6ADAC932C20FDAABB8BDD1EAE318341D06E0801C0CA4B873520C714740AD017FE5A158D3BD40960D907AB7\"\n        },\n        {\n          \"tcId\": 46,\n          \"deferred\": false,\n          \"z\": \"EF38264520685080F52975BC957C5FB609FB0E1BD06D26F572CC5425CAE7DE5C\",\n          \"d\": \"CE2CACEBD54AF1B4E71588DE9F22A6AF2C2E2AD7FD66B9FEC0DF19182E7F57EC\",\n          \"ek\": \"3F476A07BA0E59113595E59410B41C61CC5D509895725A7010E20C83CA906EA13C1C58A604F3A2590727D3538ADB2754B879A3F2640C929388DF38A559108A5519B221E3A2DA515EC34B7CDFD704A04C18DB604CCDB2705CF273EFD4B464D4A3A9773FBBD82840C002D8529BE9377009F4558BC747AD33BE9DB2C290EBBE06972544A827BAC51C131684FA3C0111360F59C72E5D1660269672389748E2B8546E648DA50BB8243B832C150ABC1277B4F7A350028EBEA582202917B8247E4A054C84D31B7BC97837C5117D796655A098C1D26BCD6C506DA164F362CD3B231C089371E711890B795B2DB0C5E916A3A169060969001BD88AF165021FBC9B1D649F9A740CC48AA2EE0348D46394C48A776C040D18D867F3506962E4B619C21DB56C9B3042ABD1B78BA814064979C643E6CEB8025B4289BC1716C6E07873B52C545A9488102200E2877C24806E15F790FF15C9677882EAC857C620297B9B9155044D9C26626F6B97FF8491C0FA33742C785DFB3D401BC0B8C50D17D857A13C4918BAB9BDF5BC8695AE1C581334559E4F83B076CA492DE0A9D97409DD676B051C7C97E9655E2BCFC0E88DDB85480F1A5FE60C7E5A002BD33B0F13F7B4897A9B2D3B140468B9114BA041D952B08B340D2B3B970BA44738BDDD3CCFB3039F7C04C0264875C9B0B5B05115C4749B29DB6BD801328DF89307307F6689A43F72C83940B3C7D72BA35B812C660DF39A329867A9727119BD9A379C6C39522B669B461F7C6A5383C23289B4C065862DFD430E7D9B4385401CD870228EE1828C9660F1CC2F9DB63E5318A97FA8AD4B693505B859C5FA86816473080C4AE6C297767A210D355A4DE59AF60844227781BF7B3B09C624C8587B0C2A200BA1196F5C84B99630A03B07C86A1BA50C002E523C8DC498BB89A4DE17CC8BD2A1DE3250E282961C956C6B197904C1269080280E2CC636112CCFE88195536E52D84AC33332C7612A4FDAB8BBFBADA5149E05F1CCBF28C706E5B037F65C7EA47EF5E1792F90B7BA4C01EA027D54AC44C60373EC931BB5E5772AEC45581C613D1497D227C0F65470C05187FF214593B44EE8CB18BFF8195365BC56B3608AD6B228E755D8F52449E7A15E81A26E372E1DC19DED021088B8A2247A33E0C1857C22CAC798575A7ACDBB09B4A018084293CB2E766CB2A8C5D1845B0B878F74BCC41667AD93137B66F352453051EC360559402CED1A1A73EB9D45A97589E05A5C991082F3498D1A75708A3FE582B0EE3B95785C73F7679B374095E1EB47567C41E630A971D60C7A6B9A02C372A9A56B0206144DA6556EF37DACF9002CDCA7A3DAB5319259E06B7F5473643E975BB93B079A451C5AA87CC0B8ABCEE48BC1448482F9769BB27EBCE597214AA73CF8734B9C6C3E79A53FD83F120C642A690C8220008D834DDCB99DF152C8456C817E106717B5CAF96262F2CB3D33892B1506C18D25B065C0CA1017BA4D466D0AC849A112C42E218C7978A14F362BCAEBCA9A5602F1C96AAD325C9BDCB6420B76926157C02A4CAA659134908EB024CC88387FF64317354B87D7383A7317643D71713DA3B25F3B939B060B6BA0CE5838697BDA102310AC67A1080B0C11C4FBB44701CAF7A360730F6EDECDE8635ABC55FAD14FF51E2A271F7D44190ED0D0BA95131BE7DAB95F\",\n          \"dk\": \"9F487527A4789D287401760DE02688DE23B049C0BB20DC05AC651478A591D4178C7E25CC578832B05B1E136BCFB28157AC8A4D75BBAE195CA628028C2BF806EFC1A132D93F40850D64445BC892CE03159E1376A1701C171AB6B18F437184C671158CAF77A9BA9B1B63EB4422011C7E0E602273D94C42820666E18313C61844C403F7127C6E47B17A41C7970CAE2BD78E8BBB353451269C13628C605295AB52F263C655831AE338B1B71AA25C195FB1096E0573CC7AEBBDE9E7751B22A74726354E88322F1BC2FDF03E65342F06C6359B672E679C25FBA03840FC53F147B9A778AE421506C09AB17242CE41735E2168044EF95EC4C4410AA284B79B5B9354C419B5A9663A7C3DFC155BC57A04AB22E7578059994D33D8C76C578C7A98B5B7351D5850BC48820A199B3411350C7DF650B9D1A3E8EA2A0C32A9E88030D4AC3C22078D4950CBBBDA707800293F487D023C6476223FA5DB85E229BEFC13B6B98561EA80C30C8BCD3A48994D255AB6A134AE25A40D4B719BE2BD4FB050C477CA417406FE9A3197495CAC610D6E861CC5A6CFF7BBB8BF84B71701CE6BEB77F273505559752A6B96133A5776B784343797DF2C024C41A9E05ABC38688ED303C5A0553232D2721A13152E9A0154152BD561AA7C1991887912DD79235A8B6E04AA18C1A903280417E42B07DCC0310067BC40633FE441082BF78FE4859D8892A973B78C4F9623EF88C05901AB6C1A039C494CCC4B542474C4B369386842726F13C62E72339A37B472B538FC881711CBBC32A88D72FC93C0E2A8362C7AD8D236C2EA6724616040D799440228EAF306B655754DF78FE941315D419342B426CB273FC4604245459367F30A6A071C4C73718ED1019B1BAA65F273D665CFE79A8967A60367F470BC74C7881A516487BB1A4B4FC38AC6CC21CFEE071CB67119D54A8C077BCBA3E3103EF8724CBA5406519122EB9884F645E7248FA64489FD9763459275BD80787681433C1219C5B31DE587487FD10112274C196A4AAC7794F28A0DE98A8852F37E96A0C0FBCB5CA3B8A33678C71E469B0BE1B7D134100D8A4DA009503F800A56714D06D6BF5ED7A1D1AC8ABA3B458FDB6DAFC28EDE9CC7744A7B3353CD36569A7DF2CC0A4726E5B8AA01093A9B7994B9473E12F346B992C718E825E8F1B05404433B20AE2B464CAC95BD0833C313D96BA607C09998929776A5DEB17D259B4D14E6C1616914B07CBEE1000D11BAA276F64663F23D5CE670C01AB6BBBC9A5BEAA07994099250A54BE9A164EC67174B8C83E7115A72756164491D233A0B38239A90ABD7DB1DD0EBAC93327574DA1B242B4B98B794B8B77E5C9B684F618111EC8A259279A867066B43C4D4540C92FC5E764276A1DA8AFA378F3287C7EDA09A8FD41A8495566E5B1C8C4260EF96B123160221308E97E23F7D6B281EF94F5315CE86F8768FA2270E8953F1851E20A790F0F60ECB03AA414600B7575109FC1632FAA73E173987FBC2EB3BC94B7BB2BEA78ECA17AAA9416CD2905845DC699236517F3A45B06975EC19544C4792E9248C1FC59709C460E907031F831326867FBBE55CB32627822187D93740C2DA9AAA8606FE5979D340B97707144BD437C70C768D292CDA79C23F476A07BA0E59113595E59410B41C61CC5D509895725A7010E20C83CA906EA13C1C58A604F3A2590727D3538ADB2754B879A3F2640C929388DF38A559108A5519B221E3A2DA515EC34B7CDFD704A04C18DB604CCDB2705CF273EFD4B464D4A3A9773FBBD82840C002D8529BE9377009F4558BC747AD33BE9DB2C290EBBE06972544A827BAC51C131684FA3C0111360F59C72E5D1660269672389748E2B8546E648DA50BB8243B832C150ABC1277B4F7A350028EBEA582202917B8247E4A054C84D31B7BC97837C5117D796655A098C1D26BCD6C506DA164F362CD3B231C089371E711890B795B2DB0C5E916A3A169060969001BD88AF165021FBC9B1D649F9A740CC48AA2EE0348D46394C48A776C040D18D867F3506962E4B619C21DB56C9B3042ABD1B78BA814064979C643E6CEB8025B4289BC1716C6E07873B52C545A9488102200E2877C24806E15F790FF15C9677882EAC857C620297B9B9155044D9C26626F6B97FF8491C0FA33742C785DFB3D401BC0B8C50D17D857A13C4918BAB9BDF5BC8695AE1C581334559E4F83B076CA492DE0A9D97409DD676B051C7C97E9655E2BCFC0E88DDB85480F1A5FE60C7E5A002BD33B0F13F7B4897A9B2D3B140468B9114BA041D952B08B340D2B3B970BA44738BDDD3CCFB3039F7C04C0264875C9B0B5B05115C4749B29DB6BD801328DF89307307F6689A43F72C83940B3C7D72BA35B812C660DF39A329867A9727119BD9A379C6C39522B669B461F7C6A5383C23289B4C065862DFD430E7D9B4385401CD870228EE1828C9660F1CC2F9DB63E5318A97FA8AD4B693505B859C5FA86816473080C4AE6C297767A210D355A4DE59AF60844227781BF7B3B09C624C8587B0C2A200BA1196F5C84B99630A03B07C86A1BA50C002E523C8DC498BB89A4DE17CC8BD2A1DE3250E282961C956C6B197904C1269080280E2CC636112CCFE88195536E52D84AC33332C7612A4FDAB8BBFBADA5149E05F1CCBF28C706E5B037F65C7EA47EF5E1792F90B7BA4C01EA027D54AC44C60373EC931BB5E5772AEC45581C613D1497D227C0F65470C05187FF214593B44EE8CB18BFF8195365BC56B3608AD6B228E755D8F52449E7A15E81A26E372E1DC19DED021088B8A2247A33E0C1857C22CAC798575A7ACDBB09B4A018084293CB2E766CB2A8C5D1845B0B878F74BCC41667AD93137B66F352453051EC360559402CED1A1A73EB9D45A97589E05A5C991082F3498D1A75708A3FE582B0EE3B95785C73F7679B374095E1EB47567C41E630A971D60C7A6B9A02C372A9A56B0206144DA6556EF37DACF9002CDCA7A3DAB5319259E06B7F5473643E975BB93B079A451C5AA87CC0B8ABCEE48BC1448482F9769BB27EBCE597214AA73CF8734B9C6C3E79A53FD83F120C642A690C8220008D834DDCB99DF152C8456C817E106717B5CAF96262F2CB3D33892B1506C18D25B065C0CA1017BA4D466D0AC849A112C42E218C7978A14F362BCAEBCA9A5602F1C96AAD325C9BDCB6420B76926157C02A4CAA659134908EB024CC88387FF64317354B87D7383A7317643D71713DA3B25F3B939B060B6BA0CE5838697BDA102310AC67A1080B0C11C4FBB44701CAF7A360730F6EDECDE8635ABC55FAD14FF51E2A271F7D44190ED0D0BA95131BE7DAB95FA5A66716D011EEDF9E6A541F9438F8309660657EAFFCDB01A172998E56D9A60BEF38264520685080F52975BC957C5FB609FB0E1BD06D26F572CC5425CAE7DE5C\"\n        },\n        {\n          \"tcId\": 47,\n          \"deferred\": false,\n          \"z\": \"17E5AE70771674BE8903CC21B3A90248D993C261B6CEEF2C747873D113869B55\",\n          \"d\": \"7E03015C5D55FD9888E730C1E60F90C5F6C2E3B1E8C7C08D869F0C1D15B540ED\",\n          \"ek\": \"BE62BD6BECB1FBA42DCEB479C8B8AF9D740F272661045437E6D0C13BE05D87821365509758B075C7D15A421876CF83C9BA1458F3C2B85002238DA64ADCD77FE7F1B33F3798B6C87609708DDD1C4F41AB9781A3BEAE2910D3D0A2496927C4907139CBA8D5B791FEDA3CD472538D9AC0FB9895DF079AF86B439096783D2BC6E967C211B3969B876CC0322E59C80A761495F50567276349EA736A43E5BAF638971C355FA4D3621FC60BEF5408067B5968E56DB7F2A2438233EED3AB2792443618A535389E3E7A3A1C061A8AC72423508C2747CBA968B7220911D0324522CAAFBBA85B5D1A830FBB0FA20CCF10F78656B512B34B28815C23DF718AD044937BD73198798858E41FE5AC16EEF5AC6E234DE5D7AA5AF49ED0BBCFA35843C7753613988707C2025F8654A9C8A37B4320BFD243BF8B1143B2644A715910C46DA9C88AB8F033EA233DAF3660F8630ECE308DE44B4699D6CEAF275065BA4DCA584026D261F753CCD0A860F705201B64B32F159C86558E633312DB15C2C3164889F49F181435A1877760C50206816F3502CDC3229107F92355467ECA127B51912EFBC911C5591DEBECA2AA6015274382BA5734F64B08E1160A4CDCA78CF722A1A31A590B5E90185AF809CD39A56A0D945BCDF0769E923C45E52DE4B45E2E22093E856FAD9B9C00641970B31BC0481D91884782D08979AA02484CAA2957CC6CCC4434EB5C814973B7E3A284DBAB2EC068A5DC047336CAD2990D37231604C584E05C689E30CDCD7668BC764E3D96184E7C45D6D988AA9B03CD3275747B2E1F0C68CB76326F263EBB910366D6C4A94A8252F6BA0F5269531188811A7286D6A2AFA3158F8243B7695805B8BDA1322B1A30049C684C6DCB574C20A9574BA78677B1135135ACF200A22749115C7E1BFB6A1614A7BCF612562A3CF6702742F586B6F55169C5AE1D14621074034C109518C86EBF191881D4B692E4027BC9C195E20F54A36B8387845F47BF23482359D90BAAC0B24E091D6937AF4049A043529EB884189D2B6FD4B05473E3696512910E9189C1508DF56450BFCBB6E97747AFA545C64B346CCB04B6F30733A1071BB84DF18A2A762460F8E6A1D9F0AC2840474FB61FE7087818F6A53E7CC799B0508CE97D06987E24BCB4E4B559161C5F1AA628D68B129CD178CE6069236C863AF8A2480C149EE5754AC04719E01D86855201554F922167B623236B417FAC5948D182B12E8210FBE23FFFEB81AF88532F0193FD61BD82D59FE646209D39AD75831EDC605AA6485CE90023DA218CBB19AD8FA858E7A661D85A3ECD136809E70B25F166339C307C350E4D5303AC844257872A2845A7F6CAB996918AFEC669C9A19D43F72A5862BEEA66AAB0D708C59C260DF09EC0BC2A265A311309CCD2ECCA4FEABD6176633863A71D75B0E7957266584D000A6824D467EC50421AA1369193B418072815562928D35C16227D22C821A8552ACD83A513AB683CC39D12537DC4456E1476C4ED9B4014C42A7BA6A78C9B72F3437228A918BCC6C13FCA16BD04C7B2B277AA4C2B903517FB8397598554C9C8885380B4199450A46635E7B4B338075E15DA4A3630862CDC2B811578626C5BBE0B73C0F375376708B4D8654560782F3D56064B96E318E3FAF0D63CBD17F9966EE839107DD8D6530A49F344E194B7\",\n          \"dk\": \"4CFB54B237989DB51A1AA97F36A3B2F4337F46AA0D2C05BB1D043A5513CE6FE84A98D62251360EC740221D1B7BE04098FC37A4AD633E95332075394E1C09AF426C146BF34290F50F318B48076653E9E66E972734CC9422750202995305827736D1F03464F37F8E468886CC8ECCD74F14706AD940B1CA73257451898ADB1476D0C813A0B06648A1DBC3150DC742D4B4CAA8EC12770974F87020B80BA9AA5274E4C63B747051AEEB5A1CF310EBEA23F17035DE2B4D51C226766ABD33F9443AFB1B21563C70E431E4D579E1AB26E5F6A2792A8ECF158C53C737F0582CCF50320FF65E634B26D3F9438E94981927ABBE2899DEF3055DC8AF679569A7AAA9FA4152697454D6DC04E2B5B2A9850AD75A1B9E060F11D817152C043500CE8391C0438BA1670B19D8333881647F5ACB813CAC66AE2065A603457EF3535B7338F32759E95923C4EC84D2248A66802D92A01EFA16CA74619C771048C1111ACF6BCD1508C667CA8C471409FA9C37F4A4AE23D505B9291511C1AD7996488CF7C611B7B3CA3855CB293BDDB19B7A1614017B1953BA7A218C50236A2F7D1B3B39BA1399D251D4D6394BDC244AA9C6A6A29FE0502E9563BEEEFA6066D5A9E52000BE59CB2D246E8A559A3B7B4B2E956BE12C2A6EB4C7156324BFA17EA8DB75A844234545147AB0AF65005D7E2470D71453224399F6C51A970973CCD3339C270A5615528FF1968811C652BA8A991C13FDFCCA4CC661F7895F54BBCC07E95E6AA6178BF56FF496C1F3576A42529A69E713A44A2C9F5B7445F794996C7F944325B34574FF364357910863D99060544606A619515B7103B76E2E4869300750D4766834750FC7867A0D98470AA502193131FC7C001B49A102AA135C7224E937973CB02BA1118520833892327428FA88BDE81E3A19BB030C6EA7E6BA69B7A82F6B20F19CAA2D20507ED3C2BB0A4BF814263D17324814017F7012EFF6886257615AE2B896B045231455235C09F706235A628FDD27685C6491EC91A1C8803BC6820C9788AE919C466F836C8CF50512D149F47322398226F7B791E568BC3B6C3E52B021AF3976E9562F582734796977890AA62DE021C0431527C7C15F4AAFCA3AC6B5B61DBED8652644523029495E268890F36E145C2060B84E2C110DDC0018F2678F99726E4A981D258C14BA453E73858492E786E5B96F6C31687E702D95110A68A77E7847C9B53C181E34434C2A51FEB36DDA926F60556202371A52A04391E662A26289A80774273BC8477968D76717BBAC9D5028965C9A9D537005C6A3CADB62206E5B2E7DFA877411C6DFDB1337FC6DFBD28BE42B774746C625D41D7ABA2D29E21ED3E13D1BF5AE3A12BB25650B480C309079CFC2E1C2114B6618E9BCC0B73D5E4202C992CB375B3A8DE0A35985C235B314DDAB4C4B888B216C761224780F885241F0C1F62837B247B06B719FA4AB85E9B63A52A947972C0678DA85B2298E81414AC5B42D41E04171438A25B56AB30C61DEEA1328562079131A52805C63DA0B8A6C324D56BA6F351B6D91041772A227B17384131A30BBCB9367319721C48743C7A9786252749082623780E4828601AB15BC08A9E103C5170181B0240EB4C00B7B085ACA634626B4069703BE62BD6BECB1FBA42DCEB479C8B8AF9D740F272661045437E6D0C13BE05D87821365509758B075C7D15A421876CF83C9BA1458F3C2B85002238DA64ADCD77FE7F1B33F3798B6C87609708DDD1C4F41AB9781A3BEAE2910D3D0A2496927C4907139CBA8D5B791FEDA3CD472538D9AC0FB9895DF079AF86B439096783D2BC6E967C211B3969B876CC0322E59C80A761495F50567276349EA736A43E5BAF638971C355FA4D3621FC60BEF5408067B5968E56DB7F2A2438233EED3AB2792443618A535389E3E7A3A1C061A8AC72423508C2747CBA968B7220911D0324522CAAFBBA85B5D1A830FBB0FA20CCF10F78656B512B34B28815C23DF718AD044937BD73198798858E41FE5AC16EEF5AC6E234DE5D7AA5AF49ED0BBCFA35843C7753613988707C2025F8654A9C8A37B4320BFD243BF8B1143B2644A715910C46DA9C88AB8F033EA233DAF3660F8630ECE308DE44B4699D6CEAF275065BA4DCA584026D261F753CCD0A860F705201B64B32F159C86558E633312DB15C2C3164889F49F181435A1877760C50206816F3502CDC3229107F92355467ECA127B51912EFBC911C5591DEBECA2AA6015274382BA5734F64B08E1160A4CDCA78CF722A1A31A590B5E90185AF809CD39A56A0D945BCDF0769E923C45E52DE4B45E2E22093E856FAD9B9C00641970B31BC0481D91884782D08979AA02484CAA2957CC6CCC4434EB5C814973B7E3A284DBAB2EC068A5DC047336CAD2990D37231604C584E05C689E30CDCD7668BC764E3D96184E7C45D6D988AA9B03CD3275747B2E1F0C68CB76326F263EBB910366D6C4A94A8252F6BA0F5269531188811A7286D6A2AFA3158F8243B7695805B8BDA1322B1A30049C684C6DCB574C20A9574BA78677B1135135ACF200A22749115C7E1BFB6A1614A7BCF612562A3CF6702742F586B6F55169C5AE1D14621074034C109518C86EBF191881D4B692E4027BC9C195E20F54A36B8387845F47BF23482359D90BAAC0B24E091D6937AF4049A043529EB884189D2B6FD4B05473E3696512910E9189C1508DF56450BFCBB6E97747AFA545C64B346CCB04B6F30733A1071BB84DF18A2A762460F8E6A1D9F0AC2840474FB61FE7087818F6A53E7CC799B0508CE97D06987E24BCB4E4B559161C5F1AA628D68B129CD178CE6069236C863AF8A2480C149EE5754AC04719E01D86855201554F922167B623236B417FAC5948D182B12E8210FBE23FFFEB81AF88532F0193FD61BD82D59FE646209D39AD75831EDC605AA6485CE90023DA218CBB19AD8FA858E7A661D85A3ECD136809E70B25F166339C307C350E4D5303AC844257872A2845A7F6CAB996918AFEC669C9A19D43F72A5862BEEA66AAB0D708C59C260DF09EC0BC2A265A311309CCD2ECCA4FEABD6176633863A71D75B0E7957266584D000A6824D467EC50421AA1369193B418072815562928D35C16227D22C821A8552ACD83A513AB683CC39D12537DC4456E1476C4ED9B4014C42A7BA6A78C9B72F3437228A918BCC6C13FCA16BD04C7B2B277AA4C2B903517FB8397598554C9C8885380B4199450A46635E7B4B338075E15DA4A3630862CDC2B811578626C5BBE0B73C0F375376708B4D8654560782F3D56064B96E318E3FAF0D63CBD17F9966EE839107DD8D6530A49F344E194B76A22A9BE6B0A57E59B2F2194C4AF45A76286DAB2B0E0FE8DD37AF72ED021ACA617E5AE70771674BE8903CC21B3A90248D993C261B6CEEF2C747873D113869B55\"\n        },\n        {\n          \"tcId\": 48,\n          \"deferred\": false,\n          \"z\": \"BF83E3048B021F22DB57076A885729F95119CE63FAF51A69954BCCC51E014686\",\n          \"d\": \"8590BFC9A6FC25EE7E6DAB4870DBF4B51A1F141B7C9E96230C0403E799BC68E0\",\n          \"ek\": \"1CD8880468590163A4B0692D9569089E24873F9A5738826AA8A326B3D013C0F0C65B41CD81A42290F628F174006E08C3CDF9B012050C2FB083F97A7770F8A3B120972676C6944B06320A56605409E33535ECBC1B1EE6201A457309102D845CC631450C2D802FD62A793AFA82C0C9BAFFB22F8FEAC2B9680DA32A51AC1C75F7E3899FA01471F9668DEC6CD765221E765D7C714D46085CF9B9B6FCC2891EF16E246489CE87B9028A5DEC713B84594847A4554AB4A39985213D009C617C0524217AA1A51DABC9CBF689B66325517726CA3E955F742126F13B72CE936EE1CA2D913B785B4704F975C221437C6A890E354810E27B57689A8564DB22783B3D46488F8646B50C366BE75566EDC0422C2784B7A33D10588BD83A89A07519B754C1EFE901FA9001C0B120A70C0BDC8CC5342C8D20281B8A7A24FF4240FF6990BFAA3197CAA594F63EE4C75B3014AF834BC4FB8249604855597B5ACEF45304D75C035981A8A7267FB04867E9C28EDB8BA6C64BC6B70B218CC738B097FD6527915018EBF712E7044BC7B50CB9151395814278089E9A003965B4AAAE46A19E5791331876B985BF23DA8BE19CC4EF857C98C91084C1CF0441A1B212580E271FEED51C98D88D4003B909B42627860E5B788F99521CB3953371F1686A437A2F3055F8D05B7FB01BA29886DD28A7269A99DF979DD80BCA5D399AC18A919985C643D68910F9C0C5923CC3A30C25E8B9F71935503513AC28A520D194CFA7129BD2A08CE76491B886B7531ED100700EA011D09319EB66B8B44A0C09913A0E40A2B1166DF676C205E648872386A221B45A428B9BDB0C8281032D52C22028BDCC96AC2CA6470F407F98D479ADE32B1D202F1F1141C3E6453084B551554989E00BFC2A44E7F5ABC8530FC174B6FA30B3F26B5A467A39322612B1C9C3CE01CFC540CEF912058FBACCEDD8BED38AAB401B88EE7058195CBCB7D8405CB71DC5B44EA19B34C9D1652E7192D50A03136BB87CB207C9DA8ADD47AD870A3AD5DA51B5E8874F71560C5490AD7137522823B167166E89BFCDD3492AB836ABC952A5E957E7A0CA963A2C697AC1F42664D7379E6AB2361D657D47CB34236B70F9658E71FA7CB2D51BABD2C67187022380335D9014176BC52878C1CB721CFA6B63FB92C55A9A53EA635C7522A93DF572DF5B01E4C4931DD47ECC73514EB5A6085206B79209A712C60BA4B338254411D08DC7C289E00C0C3F230406B47CCF4183D97005A0687F6BA6B7A02A4E9E781C99C28DBA8A32DE80AF53075CACE4629F03872F3606228335424A1311BB58BA3309D4087F839960DC3CC63D83832F0768BD13BA41EC2ECFB03D882799D10653A4F6BAEF00709169346FB83232F9685E309E36B23DF53AC8D0996FAF14440641AD59E3991054678596849FA44436454BD535A2692922A3C57BFFDBAFE0913240EAA29646A8B5E7461C269F58F92680401B70A82529634325D128AD23A452EC9A5E52A622A524F3205B6E9130BAC6830C3B109164A65CA9091226666DEC395AA63B0899BC6C798F86F74E3E5265CABC600C193AB7D1AAD5085D2C198DE66A4BD4649A26349AE5C88FA44BAE97B91D2A17B8578942B7D61E25E367CBDC29841B0116458C97F72499AE6B74006261E57B749B66ABDF0FF71F8CCEA4ED010C6A97739D7351\",\n          \"dk\": \"F14A379CCC4C2B873E4C6366D676712F1B15BC620AAE40C3B6F31739FC2889ECCED39507D9148082B2B37C70BF95E42A98CA9B13C52A22604644839DA14B67B3BB472E376F52E20641E06E3E6104B551353FF24646C97E0D1C8A0008478158CCE8E8B6D522896A395DD8F6AACED41A58D3A9C0C0BB23C55484A9CB9BE11E9BF370BBF045505C99A08C6D8B62825D1020B3794999F63BB083BDF260714D16ABC01C741C48776067C373CBBD24558A1A4938C3369955EB0D8673C1323A243CB72781C68BB64C1E754C702C832D68F5C0A1E1A1CA04A0077955CF131B74CAA9BAE30284BA467AAC056CDB8F47B5848CC03383B275C4E62B34881E8FF504E30893A5A7C6CD9038149C0F83BB0410578A3BF42A7B7BA7CC174ED0613FEBB88E5CFC7A8E9204D30A48D4C33FC0752169874963A88A1019BE594B502FF4A341D3C59E14100AB3773AC43A92F0C901A04A1FCC256DDB73D6F97AE58ABFA75528E6CAC933E2B3A5A84F8C24137670C531E6BE9CD9758BC5BB043266A22104825845196895D6966577325769220775C9C2B480C36E6C2C18B7255BB5277CD40DEA96806858299C201FE60C5A8E590F565863DC254FE8431A3E891149D86D8CC24EF5114103D8812FE282285C3A2806432AC7629716CC5DEA1EB9576E2B9562809A0AE66790F654278C16B355E1A4087A9600382F35BB3BF9F9541B4C5A98382D47D4B569685537662D4D891B7C949FCC4A27C81CA7A594BD66AA09AADB27AD43420863C2CBFC7A5D818984E343785C863085A97B2B77E8C5054418BF43BA61E62594E7011C1B3360F278130595959FA026C14A893579363BDACE7C291065C27C73340D400747BC29458FA324CD8880DD84A09FC973487358E68BB3221C4E5C4133914A13D2C53297BA10F02327FE37C43681A028A865576410F7DA2A7FA8867D563399F59DE8569485043623A1448C613B6424CCDAE8C59CCB53F1E8B0EF4A5751367F193637B6F78E0AFC2013FCBA7FE8AB5D7C70385A6869221A8FD88E246C7DEC6C83187C9680A8654E02269CFABF7438A234037BEAA7AF8D54448035BFE7A7430E264CF3F607DB07C3EB750BBB1926D5ACC839F02612C4BE88422AD9731FACC22F6DB1213DA36ECCAA97F7B5CC94146A0C50671F7725388712B30A1E8EFBBE72F693B7652F7FE5A9C3E10415168F6F9273AF55109BC9BAFFCA3E80989BA88905C1DAB706BB77ABE38D4235C44B43515DBB6CB15813EB4B0D8EF97647B331C689AB67E3921B840FB78804F83B0DE72239EBFA22CA799BD88107C36A505DA09E9EA4C4EE5802DB5047D2FB6B9B2C499EE55CCE4347A33C7A6EB2337D3589A3D099939B0C8619AE7A2414F0698B4B06D07648B2059C8BF8583095426A0776C81D4C5D24EB5AFF450E79FA07FB7026378B1E86D6AD2BA7227475261F37A6C58AABC6447E054AAB9C61A6E19B571AA57005E8906BDB6960A425B8B95671407145837FDD9B5CF51A4E42592CDE24332673CE7D023A3E2544970AB55857A1F0200521114B8096A366417052094175B549F474173E723A2377525EEC95C0D40C94988981792A8C324B61EB381F8ACD3BA987023B2E64D29C0FF63D5134527FA8AA5199667ED2331CD8880468590163A4B0692D9569089E24873F9A5738826AA8A326B3D013C0F0C65B41CD81A42290F628F174006E08C3CDF9B012050C2FB083F97A7770F8A3B120972676C6944B06320A56605409E33535ECBC1B1EE6201A457309102D845CC631450C2D802FD62A793AFA82C0C9BAFFB22F8FEAC2B9680DA32A51AC1C75F7E3899FA01471F9668DEC6CD765221E765D7C714D46085CF9B9B6FCC2891EF16E246489CE87B9028A5DEC713B84594847A4554AB4A39985213D009C617C0524217AA1A51DABC9CBF689B66325517726CA3E955F742126F13B72CE936EE1CA2D913B785B4704F975C221437C6A890E354810E27B57689A8564DB22783B3D46488F8646B50C366BE75566EDC0422C2784B7A33D10588BD83A89A07519B754C1EFE901FA9001C0B120A70C0BDC8CC5342C8D20281B8A7A24FF4240FF6990BFAA3197CAA594F63EE4C75B3014AF834BC4FB8249604855597B5ACEF45304D75C035981A8A7267FB04867E9C28EDB8BA6C64BC6B70B218CC738B097FD6527915018EBF712E7044BC7B50CB9151395814278089E9A003965B4AAAE46A19E5791331876B985BF23DA8BE19CC4EF857C98C91084C1CF0441A1B212580E271FEED51C98D88D4003B909B42627860E5B788F99521CB3953371F1686A437A2F3055F8D05B7FB01BA29886DD28A7269A99DF979DD80BCA5D399AC18A919985C643D68910F9C0C5923CC3A30C25E8B9F71935503513AC28A520D194CFA7129BD2A08CE76491B886B7531ED100700EA011D09319EB66B8B44A0C09913A0E40A2B1166DF676C205E648872386A221B45A428B9BDB0C8281032D52C22028BDCC96AC2CA6470F407F98D479ADE32B1D202F1F1141C3E6453084B551554989E00BFC2A44E7F5ABC8530FC174B6FA30B3F26B5A467A39322612B1C9C3CE01CFC540CEF912058FBACCEDD8BED38AAB401B88EE7058195CBCB7D8405CB71DC5B44EA19B34C9D1652E7192D50A03136BB87CB207C9DA8ADD47AD870A3AD5DA51B5E8874F71560C5490AD7137522823B167166E89BFCDD3492AB836ABC952A5E957E7A0CA963A2C697AC1F42664D7379E6AB2361D657D47CB34236B70F9658E71FA7CB2D51BABD2C67187022380335D9014176BC52878C1CB721CFA6B63FB92C55A9A53EA635C7522A93DF572DF5B01E4C4931DD47ECC73514EB5A6085206B79209A712C60BA4B338254411D08DC7C289E00C0C3F230406B47CCF4183D97005A0687F6BA6B7A02A4E9E781C99C28DBA8A32DE80AF53075CACE4629F03872F3606228335424A1311BB58BA3309D4087F839960DC3CC63D83832F0768BD13BA41EC2ECFB03D882799D10653A4F6BAEF00709169346FB83232F9685E309E36B23DF53AC8D0996FAF14440641AD59E3991054678596849FA44436454BD535A2692922A3C57BFFDBAFE0913240EAA29646A8B5E7461C269F58F92680401B70A82529634325D128AD23A452EC9A5E52A622A524F3205B6E9130BAC6830C3B109164A65CA9091226666DEC395AA63B0899BC6C798F86F74E3E5265CABC600C193AB7D1AAD5085D2C198DE66A4BD4649A26349AE5C88FA44BAE97B91D2A17B8578942B7D61E25E367CBDC29841B0116458C97F72499AE6B74006261E57B749B66ABDF0FF71F8CCEA4ED010C6A97739D7351C57B9807586DB3D99C6AFFAFB04CD2551A4B1DF17FCCB8D7D94C103EE6656B14BF83E3048B021F22DB57076A885729F95119CE63FAF51A69954BCCC51E014686\"\n        },\n        {\n          \"tcId\": 49,\n          \"deferred\": false,\n          \"z\": \"F42861EFF7691614C3E8975AFB4E353F8C8C39E6F41BB637EC79BAA976D1ADC1\",\n          \"d\": \"D5FD815092620DC42A223909E387369A74AF7DCA285138CF217BC29F29C42C41\",\n          \"ek\": \"A9D674C360C5B1E36D78F66C722392F167A126F09FCC4310A9320137EBA898E786F95B9CC5D0B9114206EC66159BB3A687427D3B2298F1317ADE160EBF44CAFAA62B38871C6239C38F67CA0922C9DD952D8AA55AA6A481386771FD003A271B659F4B730B589B717635E6EB62603C5BD6C96756DB4947C5B1A71B970CD16B6D2519205C08147685B7103BB7C130B40C11CFE9C73866567175CCCDF40522C10A80140D78F9A71C714BADB1CE2732B86BD0518BF4783FAA00694208C3C2BD4BA34DEC08ADD425A0687A0D8B392702DA57504A28E9099912A8AE4A964765D35E739C1C3158330F4C21DC3A58969AAEBC788EC8177443732EAA527DC1E5009E010D8F980369FCCB48527AA27C6553393709F0B252E9346671465D33C072E352B2619175878F946488AEE3B5EE201C753244E6C85A1DE48BC994B4B1F9AB4F81B9C9F6C243C4A57BAB99781CAB1FD152A205A893F07F16431401F746B5EC790EA89C8C672EB7267EC34036F5F541C334B499DAB565387C2341C27F4904546917295B8F640CBCDF9B254502B58A669D36AA3D834793CC2A4D6578397D3A6DA893710F6A1839D8CAD7067A020108BF98415569A5C5856C5678C9A41A3075F077E6150F54B57FE91691A1C05F5022939A322CB919ACDA1236FEC9885AEB2C3A975E5BA238F5E713AA751093846915B003E624577007C7048B23E29445EE123AD3D6604D5006332CCD238A0E23020244D99A2C8A28CB58A76A69A39CC05927767ACBF892E3B6CB27231472C9189FBC1BADD18CB9A0CCB9E9CB08D0BA65C9924BCB2D88EA9739835C3B388A7B416A5BC5A27B01260D732F237474292133F0E458F33CBC3A687F149A1F8D7B08FE76A0C7C9330F8582D378BE0E2B135097CC9527A24FA3913BC3C98F04298CA86A08F6969D98C6EEDC19D6026D18D93479698A237B319B8CBC7E266DD34162FDB07C04A677C7B27E758A9D42DB0E35B4369C325C1F5639E0AA30B87433BC9C977447A2F97413E351BB98242D68E02527053A75A0CA93AB8E1E8B521F4BA09EB1B32D6797C5D7BD2AC76E81946CA48BCC787A9D35022C31F0938F710482B1A5DDF91A9024900D019AE33B182D6424D86B558109158B188C3978B5345A7D85B70B30888083C50CFAEA947258891A192B05D8589A0B7415F6945D1275B9C12DC25055B372553D21ABC7B96BD7F821B0C78D6AC3CD5BEA024720625611126220A5A467A60C268B748632C6B19403391391D649BE701B85592FF7F256937843547339E01179E2D79F9D88168528CE5FD5A819108456A789F094093E3980413A14F11B71C57BC71617CA3234008B2907447B8EF8A58B87450AD9506BB84C9E3F66BCE6CBC5D503434D406387D8CDC22A64D53786C79A90C911723D8799B3A7B9AB20672DA92572FB609AC14D57435736055B89F9851DB4B621AA42610317F15802FFB457620B4A7712212DA5725C049E1FD3670DF15ED5A945549C38DE4826BEC844500A313B41921E21AA5EF186940497462117EBD8B383520680DC2646683E0E819B2DE992A82380F400BAA20560AB7A58D2176D4885204ED705F21084390C5AAB8879214286A1F199B3949A59E11B86582B5C13080766C7E2091EA99F1352D4488C7EA1547DD87024156A9FB2DF0C77B684540084890847AA6D85\",\n          \"dk\": \"49B061657A8EDBA51C06A38F4995CC0D68BD97C515A7576125F7961317046D3B0A21B44415B07592991570D05291AB34DCB808E57041FF249D97F471B57A8B7924BB8B699B9772360A8157CE513C58882B89F79E37287042A353A887761D73B2AA3A5185D999FD1A4609AB7FB3ACC8EA083E6DDB2A9592413CA11E7D51AD6378132AF88D977762A473A4849A303C648183D03773C610B8C07A6F741D2E887768E20FA9B749072CB64292B791731658E7297F8A584EFA2A9446911E155E0E27BD71DB8B1D52395982C40C4C65C7FCCEE20B684C2432A83579468B8A82A730305A157FF46F5C8781CD621143E31A20311803185335855116712E34498045093BAD6B80A8F332AB8C5A90025AD0B1CA6EC53401AA83DD01595540141C749687A53D775B4A691B69F6D32C275187A6CB1A4A336C341CC36CCB295C21609EAB1636EC0BB031345A3BAA345878EF43A46B82A9DBB7019658A43AA0368CAB4310103BD5589499B2C1E89A5C2A297ED707B7FAF8170FD2CAE245146929060CA5BD10D8316980C18824CDA429302B64C14385CBE36AA0DC6CB0941B204A55A710D2C68052C900C34C35A7AE7834CF5552264AA2583913348CDC79A08847ACE64024464E759AA99573BFC7B40F8CC4449379A40558B756B22410934C7969B42ACCCCC7D8231E5CA2DF5574384B9E0712C436FBC67AF74234E49B59480B0CF47C8DC31D47776BA9B000C0E80856B889FF09916CF8CE199680A45B16B1AC1438000D6472C79497014B6384AD069FA8AB1A004B518AA97066DCAC32D00893301D327008020431F2B01A4004523F0453DB14B3A84001EC3749D25473EB219AF1E0A7628BAE4A24BE7A1A5E24C3AB5D0A6661C4C1DE754755EBBC91865E8147C1D0233DEA643C4E648DBE3C50B96B898D81B4DB71CD9CD38BFFCB9E7C384EC58A7A27F29C7C5B20255149EDD02442E4CF8C8A34B73B6D36319F6FD3425BEAB4A8EB1698E03A1A344C88808FCF183AF5F6905B1B445CC9235BE74C1DB07D794BB867331EAEFB52AA165E31DB2048A62CA4D5197B3AB327281B132B0EAA18B1D8699EDCFB730B905A517036E3EC9BF40AC4F47440BFC231693025CA297B4F5198DC094066919934F32E5EA6712937CFE458A2BF916D382933A95487603B4AB303CC81BA94E467576787B7D8095B059020E6C43069745CA03C2B31229F2CE021F4D9BC38FC2886EC9570F2671872C766D40C970A0825083A01961FE97C9214533F3A8562E620A0C8B5354E8989420B4F639B323E415D8AD4732F299EFFF99A7AB47A100CB05F31C158F37FBF8B7B7F477DD3398A20F07F9BA0605AC67CE4379932D0B2E7E236882093846AC42B64806F4C3E035BCD1EBAB1D54084B63C870217B210D2CE816A35BAEA8CCF2C7FB4938FDDC320D9B81751116C4DBC2269C37413206704B631BE653BD20B7F920305F9EC8C46805E95FC504B1C33BDB5A1AD912F8D65221A784922A63578201FF3E869DDE76AFDC9068F657CD0B66E8D441DF7E642A01220F8001D6A1558F5F73F49C13F3E1BB8A9E7BFB56B677CD6AE6FE8C85C24CCD7A78790E1AD8809A8F964B43228155D2872A9B11849514035F154799202A0299C41F80D9D1A67A9D674C360C5B1E36D78F66C722392F167A126F09FCC4310A9320137EBA898E786F95B9CC5D0B9114206EC66159BB3A687427D3B2298F1317ADE160EBF44CAFAA62B38871C6239C38F67CA0922C9DD952D8AA55AA6A481386771FD003A271B659F4B730B589B717635E6EB62603C5BD6C96756DB4947C5B1A71B970CD16B6D2519205C08147685B7103BB7C130B40C11CFE9C73866567175CCCDF40522C10A80140D78F9A71C714BADB1CE2732B86BD0518BF4783FAA00694208C3C2BD4BA34DEC08ADD425A0687A0D8B392702DA57504A28E9099912A8AE4A964765D35E739C1C3158330F4C21DC3A58969AAEBC788EC8177443732EAA527DC1E5009E010D8F980369FCCB48527AA27C6553393709F0B252E9346671465D33C072E352B2619175878F946488AEE3B5EE201C753244E6C85A1DE48BC994B4B1F9AB4F81B9C9F6C243C4A57BAB99781CAB1FD152A205A893F07F16431401F746B5EC790EA89C8C672EB7267EC34036F5F541C334B499DAB565387C2341C27F4904546917295B8F640CBCDF9B254502B58A669D36AA3D834793CC2A4D6578397D3A6DA893710F6A1839D8CAD7067A020108BF98415569A5C5856C5678C9A41A3075F077E6150F54B57FE91691A1C05F5022939A322CB919ACDA1236FEC9885AEB2C3A975E5BA238F5E713AA751093846915B003E624577007C7048B23E29445EE123AD3D6604D5006332CCD238A0E23020244D99A2C8A28CB58A76A69A39CC05927767ACBF892E3B6CB27231472C9189FBC1BADD18CB9A0CCB9E9CB08D0BA65C9924BCB2D88EA9739835C3B388A7B416A5BC5A27B01260D732F237474292133F0E458F33CBC3A687F149A1F8D7B08FE76A0C7C9330F8582D378BE0E2B135097CC9527A24FA3913BC3C98F04298CA86A08F6969D98C6EEDC19D6026D18D93479698A237B319B8CBC7E266DD34162FDB07C04A677C7B27E758A9D42DB0E35B4369C325C1F5639E0AA30B87433BC9C977447A2F97413E351BB98242D68E02527053A75A0CA93AB8E1E8B521F4BA09EB1B32D6797C5D7BD2AC76E81946CA48BCC787A9D35022C31F0938F710482B1A5DDF91A9024900D019AE33B182D6424D86B558109158B188C3978B5345A7D85B70B30888083C50CFAEA947258891A192B05D8589A0B7415F6945D1275B9C12DC25055B372553D21ABC7B96BD7F821B0C78D6AC3CD5BEA024720625611126220A5A467A60C268B748632C6B19403391391D649BE701B85592FF7F256937843547339E01179E2D79F9D88168528CE5FD5A819108456A789F094093E3980413A14F11B71C57BC71617CA3234008B2907447B8EF8A58B87450AD9506BB84C9E3F66BCE6CBC5D503434D406387D8CDC22A64D53786C79A90C911723D8799B3A7B9AB20672DA92572FB609AC14D57435736055B89F9851DB4B621AA42610317F15802FFB457620B4A7712212DA5725C049E1FD3670DF15ED5A945549C38DE4826BEC844500A313B41921E21AA5EF186940497462117EBD8B383520680DC2646683E0E819B2DE992A82380F400BAA20560AB7A58D2176D4885204ED705F21084390C5AAB8879214286A1F199B3949A59E11B86582B5C13080766C7E2091EA99F1352D4488C7EA1547DD87024156A9FB2DF0C77B684540084890847AA6D85E8BEB5E40DA16CD0B6771A006BD6CC2A5BA77C278E3EDF52912210F80A5E1759F42861EFF7691614C3E8975AFB4E353F8C8C39E6F41BB637EC79BAA976D1ADC1\"\n        },\n        {\n          \"tcId\": 50,\n          \"deferred\": false,\n          \"z\": \"4DD0E86091649A0A08EA44DAB85DF56797F8BF46222C2DBA7DEC6374B9B2268E\",\n          \"d\": \"D21D5AFED9AFAA3B49FB45245B2BCA1505E4000CDC29094A3600F5CAA49A7B3A\",\n          \"ek\": \"744793A77A843520C9EB8B057FF28854193916F711902B1F8B4B9C235251FBDC8040C8A38C022557DB50CB28CC8DF99A859C2EC017C24015877D2A3572571FC783BF1E984401C11158E32A95351BE407A8AE6B5E054A33C2427F4A346761559F776671095B495074AF5CAC98D94297FB05624443CAA8151AF3A497F73203EF184CC7336650427CDFD47B10245EDDE30C3075933E431DC1B1C2A77B2073257C2AB73ABE706DE138CD54922CDB09CEFA60BA473092E43BC3E3CB951939B1C4CB2AFDCB4F8A5310FE9A790E560B25A40AC9E18D35A27238A60A126B398980BE8425C8A01B930FB336CB359F58131BBEE6AC450B8C811B2DDEA836F9D0C2C72A709E134321DCA727641A1D678A8E0CAE6BD6BBDAD8021AE3C1B6067FB1ECA42D215EC2C4297F6891567712F75047775B741713097B1271AFF026323A134DC20DEBFACE46D5B01820A9A862BA93E3AFCB5AC4C2204AF2E15787D0C857896D24DCBD6393C1FDDC12CDDA6A1F337B18D6C3CCD5283B37239A80424780399281C3FF1967B3BB6DAE499D42F511997077F38A8D63EB319342B1BC749924E467C6A2A11728826BEB52A39BA7A1A03C93C613D36A3F1F544B72A8BEC24009FDE07E677101456A2B76EC3353CB8944C339081A4B59672CD1B5446FFA6FC0B468D9FC83EBF896CE12158AFAA6812028F54B7920636784D3B3854030D05A9C19D936C6FACC5B81070CD524728A96A58A2E1207C73A4C533DDB00098141158B11DB674500781F47DB9B45F3CDE244502D377886418287C22075B2BD172606CCFA6F4914844525C113BCC7C049B3D14A2DF91B6C62E0A2EE5922DA3194DFC0C506D48ECCF5697BA31BD0839A4D3B3078CB7C2936CE1C507B0B54023585CADA6BA5657726075659DE16A6403110CE38A247CA85CE02C1DAC79FD175BCAAFA116F710516A4B73D78BFC6F9854228B6C5F035946B1F61B629F31328A65556C8F0661D58622C6021F58C1ACF154C6E78BCF563352AFB89B824513A35502F117861A34C52B1011F696A85185186BBA23B518C720763529C181D902CBBB58CFEF77CD2E72F4780979FD46126C0A4BC1965A6E070FC284B1F856852110E19C268390CA9494116EB293F1A111ABEC1AF04F706B114617F0362B3DB4B99E076287AAFB7127FE2C68904F61664D63D4FB267CE07AD5A659FB81886F8E0A9C26261A0055F48F432E9F8242C439F58B5A21EB777483B2E68B3B1118562430270340243F1E87C22CB5BE2F167DC39CF4C69B8B8E9B726256CBE605342F47E23A871BA7250BDA8997FD474B3375920522F2C511B3EA35CF459342893AA12B128CC2184E304335AC7BA04653898590A664672FE744F301651442BB341D32F36262C576A50F803B02F91105FEBCABEF7030D2A76CBEC6C2CF36283590BB44842853102D3D350801167D7A96968648C0CAB9AFD39CB7F658909283CB83CA78544929AD90069CA3E660196792A654A911CAD7C23024C90E25AA61B30A4D568C5A5C12A53565A93E910D5940EF3583856017BC25B05C4473F48584C6908C13D2B19E7A3A7992976D31A464807199AE3A6307CA55765590F6B4721632D70FA47E2C1098E36898BE3125E43C61CBB7A6C246CBA0002CF325A5D337989289BCCDA54835511DE9656287363DEE85033410AEAE1\",\n          \"dk\": \"045AA22AEAA4A8E0C66661A183700F04025D413CCDCBE1B7CD9B839BA61671B08A859B1AECA9014CB3695B29703CD4314954C48CC19A89E106079720EF23B597FA6A833638B7A7B1E74A40C8BA7A005AA799D22DC5AA97627A037BA2623C659621EA2FF40651DAA0A95B2218CF934DB132BDE8A29F889BA084DA3EA9C8B58FD1BEF4D45EB4A2501447640C7869F21C9B031718209288E442018F63097459AFCEB75815829B16EC48FF822B1F69B6B48C3AA3C65441E60F552A154BD85A71397AB8290564F527FA8B1E84AB4EE6450EB5CA658B2120164B7D84637CC7488FFDF717F9349734E6B2F585C040065A2E910CCCA070C0070C2C215BC8060930D30D7A73A0FADA94E46257BE169B73D7A35DD43EB9B0183589AD657A429D983564031166253B4EE513AFE986E007727D75BA22F41A0AEA4939081C7CE1BEA5AA8D8DFB692AD37726E05A9DBA9D70102A1DE64479A928D9789035D4B645F088ABE8A51B06826BA64079C4AD3EC2C8A7EBC9F5EA6A0C707E88B2224DEA55FC0C39EC258088E319CA48C3F0C86AA203669F164E5FD0BA467548050CA496579BD4099D705A82BF9634995ACE60DCAC96C9115226AF2B6A9D86497DA1C49FEEE35F5639BA1A8363DF5114EA06C190179161C3C4238115DD55200BD28A03EACCAA46C7A9126B6F0CC2BFD9047D9BBF7A2043BD965F244158C7F10548FBBC39E040F040145A45B12BC90A74065BD3E34925C94FFA968BEAD241A79B6B0F1C3911C444E3958906DB8834984897CA886DC1B9D63CA43803C74C5633F003BD148A691D396C98BA81918A560542CC6435082957A70A312DAD0AC54F81903B2280BEF3638A013654D26E0D2705939A31EF39547FB80BE113A336E277E04B8B905C9DD90509C434277246916A1A0A7E66BC1C74C4719751BD8879E07AB97721C0B27BC08B69B284F2BB47E43290F2BB434C489570C22AC867F06643AB100457D336CB944B6B5A185B639A403A332FC755584A9F2EAA315C319D3099B30710B7EFAA5BBE8A3981A9B158E0C7484595AA1B7CA3F335BB38A7E5AB51F2799E0EC2049AB781514A82CA7CC5A53BCFF3029320636285902EC1D24432B5292EF7600097929D374F3485AB2AA5C8A154CC3171693DBBC4382A5E44600AC8B1646A84A47B078D06EC554A309A1FC2BB473C69EA4732B6485BE812A1004D583D9A0B9953312CAB9B014A482D8C7BD1097231D0041FF599D0E043C7CA08FD0CA71AEB977EF271EC13AF122766566BCAEAA26554F87394847C50DC2B8D3885184069B1A173D1183E68C55867E55EA78610CC3183A7F60181D1751A12107225C7156B99F86846FDF5777502A201FB583202B28EB78EEF356918D2B5B7594AE364902D45C2E8691137C98CA7B2425274068D765AFF29095ED546309C2D4CD6A5DE0571E67566DB14C386A87F15379CD1190EF02806D6B8387C87C73B805E01DB128C0A86C1013FAA7CB7D46770A0835C5343C931AB8CFDF56AE4241108C55976F127334AC3E2B52360661B61532CCB641CA043AE9B3173D0133CB0E4581748B6F1AC00BA0BAE4960376782A25F36765A1A4318D73C18781C90755C657847ED6849CE1301B11A2FD75616B4F326DAD545744793A77A843520C9EB8B057FF28854193916F711902B1F8B4B9C235251FBDC8040C8A38C022557DB50CB28CC8DF99A859C2EC017C24015877D2A3572571FC783BF1E984401C11158E32A95351BE407A8AE6B5E054A33C2427F4A346761559F776671095B495074AF5CAC98D94297FB05624443CAA8151AF3A497F73203EF184CC7336650427CDFD47B10245EDDE30C3075933E431DC1B1C2A77B2073257C2AB73ABE706DE138CD54922CDB09CEFA60BA473092E43BC3E3CB951939B1C4CB2AFDCB4F8A5310FE9A790E560B25A40AC9E18D35A27238A60A126B398980BE8425C8A01B930FB336CB359F58131BBEE6AC450B8C811B2DDEA836F9D0C2C72A709E134321DCA727641A1D678A8E0CAE6BD6BBDAD8021AE3C1B6067FB1ECA42D215EC2C4297F6891567712F75047775B741713097B1271AFF026323A134DC20DEBFACE46D5B01820A9A862BA93E3AFCB5AC4C2204AF2E15787D0C857896D24DCBD6393C1FDDC12CDDA6A1F337B18D6C3CCD5283B37239A80424780399281C3FF1967B3BB6DAE499D42F511997077F38A8D63EB319342B1BC749924E467C6A2A11728826BEB52A39BA7A1A03C93C613D36A3F1F544B72A8BEC24009FDE07E677101456A2B76EC3353CB8944C339081A4B59672CD1B5446FFA6FC0B468D9FC83EBF896CE12158AFAA6812028F54B7920636784D3B3854030D05A9C19D936C6FACC5B81070CD524728A96A58A2E1207C73A4C533DDB00098141158B11DB674500781F47DB9B45F3CDE244502D377886418287C22075B2BD172606CCFA6F4914844525C113BCC7C049B3D14A2DF91B6C62E0A2EE5922DA3194DFC0C506D48ECCF5697BA31BD0839A4D3B3078CB7C2936CE1C507B0B54023585CADA6BA5657726075659DE16A6403110CE38A247CA85CE02C1DAC79FD175BCAAFA116F710516A4B73D78BFC6F9854228B6C5F035946B1F61B629F31328A65556C8F0661D58622C6021F58C1ACF154C6E78BCF563352AFB89B824513A35502F117861A34C52B1011F696A85185186BBA23B518C720763529C181D902CBBB58CFEF77CD2E72F4780979FD46126C0A4BC1965A6E070FC284B1F856852110E19C268390CA9494116EB293F1A111ABEC1AF04F706B114617F0362B3DB4B99E076287AAFB7127FE2C68904F61664D63D4FB267CE07AD5A659FB81886F8E0A9C26261A0055F48F432E9F8242C439F58B5A21EB777483B2E68B3B1118562430270340243F1E87C22CB5BE2F167DC39CF4C69B8B8E9B726256CBE605342F47E23A871BA7250BDA8997FD474B3375920522F2C511B3EA35CF459342893AA12B128CC2184E304335AC7BA04653898590A664672FE744F301651442BB341D32F36262C576A50F803B02F91105FEBCABEF7030D2A76CBEC6C2CF36283590BB44842853102D3D350801167D7A96968648C0CAB9AFD39CB7F658909283CB83CA78544929AD90069CA3E660196792A654A911CAD7C23024C90E25AA61B30A4D568C5A5C12A53565A93E910D5940EF3583856017BC25B05C4473F48584C6908C13D2B19E7A3A7992976D31A464807199AE3A6307CA55765590F6B4721632D70FA47E2C1098E36898BE3125E43C61CBB7A6C246CBA0002CF325A5D337989289BCCDA54835511DE9656287363DEE85033410AEAE16C770D1FA4C0F5DBB660530772FCC2297F59BC9DEE338CD124F0924CF7E3762D4DD0E86091649A0A08EA44DAB85DF56797F8BF46222C2DBA7DEC6374B9B2268E\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 3,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-1024\",\n      \"tests\": [\n        {\n          \"tcId\": 51,\n          \"deferred\": false,\n          \"z\": \"99E3246884181F8E1DD44E0C7629093330221FD67D9B7D6E1510B2DBAD8762F7\",\n          \"d\": \"49AC8B99BB1E6A8EA818261F8BE68BDEAA52897E7EC6C40B530BC760AB77DCE3\",\n          \"ek\": \"A04184D4BC7B532A0F70A54D7757CDE6175A6843B861CB2BC4830C0012554CFC5D2C8A2027AA3CD967130E9B96241B11C4320C7649CC23A71BAFE691AFC08E680BCEF42907000718E4EACE8DA28214197BE1C269DA9CB541E1A3CE97CFADF9C6058780FE6793DBFA8218A2760B802B8DA2AA271A38772523A76736A7A31B9D3037AD21CEBB11A472B8792EB17558B940E70883F264592C689B240BB43D5408BF446432F412F4B9A5F6865CC252A43CF40A320391555591D67561FDD05353AB6B019B3A08A73353D51B6113AB2FA51D975648EE254AF89A230504A236A4658257740BDCBBE1708AB022C3C588A410DB3B9C308A06275BDF5B4859D3A2617A295E1A22F90198BAD0166F4A943417C5B831736CB2C8580ABFDE5714B586ABEEC0A175A08BC710C7A2895DE93AC438061BF7765D0D21CD418167CAF89D1EFC3448BCBB96D69B3E010C82D15CAB6CACC6799D3639669A5B21A633C865F8593B5B7BC800262BB837A924A6C5440E4FC73B41B23092C3912F4C6BEBB4C7B4C62908B03775666C22220DF9C88823E344C7308332345C8B795D34E8C051F21F5A21C214B69841358709B1C305B32CC2C3806AE9CCD3819FFF4507FE520FBFC27199BC23BE6B9B2D2AC1717579AC769279E2A7AAC68A371A47BA3A7DBE016F14E1A727333663C4A5CD1A0F8836CF7B5C49AC51485CA60345C990E06888720003731322C5B8CD5E6907FDA1157F468FD3FC20FA8175EEC95C291A262BA8C5BE990872418930852339D88A19B37FEFA3CFE82175C224407CA414BAEB37923B4D2D83134AE154E490A9B45A0563B06C953C3301450A2176A07C614A74E3478E48509F9A60AE945A8EBC7815121D90A3B0E07091A096CF02C57B25BCA58126AD0C629CE166A7EDB4B33221A0D3F72B85D562EC698B7D0A913D73806F1C5C87B38EC003CB303A3DC51B4B35356A67826D6EDAA8FEB93B98493B2D1C11B676A6AD9506A1AAAE13A824C7C08D1C6C2C4DBA9642C76EA7F6C8264B64A23CCCA9A74635FCBF03E00F1B5722B214376790793B2C4F0A13B5C40760B4218E1D2594DCB30A70D9C1782A5DD30576FA4144BFC8416EDA8118FC6472F56A979586F33BB070FB0F1B0B10BC4897EBE01BCA3893D4E16ADB25093A7417D0708C83A26322E22E6330091E30152BF823597C04CCF4CFC7331578F43A2726CCB428289A90C863259DD180C5FF142BEF41C7717094BE07856DA2B140FA67710967356AA47DFBC8D255B4722AB86D439B7E0A6090251D2D4C1ED5F20BBE6807BF65A90B7CB2EC0102AF02809DC9AC7D0A3ABC69C18365BCFF59185F33996887746185906C0191AED4407E139446459BE29C6822717644353D24AB6339156A9C424909F0A9025BB74720779BE43F16D81C8CC666E99710D8C68BB5CC4E12F314E925A551F09CC59003A1F88103C254BB978D75F394D3540E31E771CDA36E39EC54A62B5832664D821A72F1E6AFBBA27F84295B2694C498498E812BC8E9378FE541CEC5891B25062901CB7212E3CDC46179EC5BCEC10BC0B9311DE05074290687FD6A5392671654284CD9C8CC3EBA80EB3B662EB53EB75116704A1FEB5C2D056338532868DDF24EB8992AB8565D9E490CADF14804360DAA90718EAB616BAB0765D33987B47EFB6599C5563235E61E4BE670E97955AB292D9732CB8930948AC82DF230AC72297A23679D6B94C17F1359483254FEDC2F05819F0D069A443B78E3FC6C3EF4714B05A3FCA81CBBA60242A7060CD885D8F39981BB18092B23DAA59FD9578388688A09BBA079BC809A54843A60385E2310BBCBCC0213CE3DFAAB33B47F9D6305BC95C6107813C585C4B657BF30542833B14949F573C0612AD524BAAE69590C1277B86C286571BF66B3CFF46A3858C09906A794DF4A06E9D4B0A2E43F10F72A6C6C47E5646E2C799B71C33ED2F01EEB45938EB7A4E2E2908C53558A540D350369FA189C616943F7981D7618CF02A5B0A2BCC422E857D1A47871253D08293C1C179BCDC0437069107418205FDB9856623B8CA6B694C96C084B17F13BB6DF12B2CFBBC2B0E0C34B00D0FCD0AECFB27924F6984E747BE2A09D83A8664590A8077331491A4F7D720843F23E652C6FA840308DB4020337AAD37967034A9FB523B67CA70330F02D9EA20C1E84CB8E5757C9E1896B60581441ED618AA5B26DA56C0A5A73C4DCFD755E610B4FC81FF84E21\",\n          \"dk\": \"8C8B3722A82E550565521611EBBC63079944C9B1ABB3B0020FF12F631891A9C468D3A67BF6271280DA58D03CB042B3A461441637F929C273469AD15311E910DE18CB9537BA1BE42E98BB59E498A13FD440D0E69EE832B45CD95C382177D67096A18C07F1781663651BDCAC90DEDA3DDD143485864181C91FA2080F6DAB3F86204CEB64A7B4446895C03987A031CB4B6D9E0462FDA829172B6C012C638B29B5CD75A2C930A5596A3181C33A22D574D30261196BC350738D4FD9183A763336243ACED99B3221C71D8866895C4E52C119BF3280DAF80A95E15209A795C4435FBB3570FDB8AA9BF9AEFD43B094B781D5A81136DAB88B8799696556FEC6AE14B0BB8BE4695E9A124C2AB8FF4AB1229B8AAA8C6F41A60C34C7B56182C55C2C685E737C6CA00A23FB8A68C1CD61F30D3993A1653C1675AC5F0901A7160A73966408B8876B715396CFA4903FC69D60491F8146808C97CD5C533E71017909E97B835B86FF847B42A696375435E006061CF7A479463272114A89EB3EAF2246F0F8C104A14986828E0AD20420C9B37EA23F5C514949E77AD9E9AD12290DD1215E11DA274457AC86B1CE6864B122677F3718AA31B02580E64317178D38F25F609BC6C55BC374A1BF78EA8ECC219B30B74CBB3272A599238C93985170048F176775FB19962AC3B135AA59DB104F7114DBC2C2D42949ADECA6A85B323EE2B2B23A77D9DB235979A8E2D67CF7D2136BBBA71F269574B38888E1541340C19284074F9B7C8CF37EB01384E6E3822EC4882DFBBEC4E6098EF2B2FC177A1F0BCB65A57FDAA89315461BEB7885FB68B3CD096EDA596AC0E61DD7A9C507BC6345E0827DFCC8A3AC2DCE51AD731AA0EB932A6D0983992347CBEB3CD0D9C9719797CC21CF0062B0AD94CAD734C63E6B5D859CBE19F0368245351BF464D7505569790D2BB724D8659A9FEB1C7C473DC4D061E29863A2714BAC42ADCD1A8372776556F7928A7A44E94B6A25322D03C0A1622A7FD261522B7358F085BDFB60758762CB901031901B5EECF4920C81020A9B1781BCB9DD19A9DFB66458E7757C52CEC75B4BA740A24099CB56BB60A76B6901AA3E0169C9E83496D73C4C99435A28D613E97A1177F58B6CC595D3B2331E9CA7B57B74DC2C5277D26F2FE19240A55C35D6CFCA26C73E9A2D7C980D97960AE1A04698C16B398A5F20C35A0914145CE1674B71ABC6066A909A3E4B911E69D5A849430361F731B07246A6329B52361904225082D0AAC5B21D6B34862481A890C3C360766F04263603A6B73E802B1F70B2EB00046836B8F493BF10B90B8737C6C548449B294C47253BE26CA72336A632063AD3D0B48C8B0F4A34447EF13B764020DE739EB79ABA20E2BE1951825F293BEDD1089FCB0A91F560C8E17CDF52541DC2B81F972A7375B201F10C08D9B5BC8B95100054A3D0AAFF89BD08D6A0E7F2115A435231290460C9AD435A3B3CF35E52091EDD1890047BCC0AABB1ACEBC75F4A32BC1451ACC4969940788E89412188946C9143C5046BD1B458DF617C5DF533B052CD6038B7754034A23C2F7720134C7B4EACE01FAC0A2853A9285847ABBD06A3343A778AC6062E458BC5E61ECE1C0DE0206E6FE8A84034A7C5F1B005FB0A584051D3229B86C909AC5647B3D75569E05A88279D80E5C30F574DC327512C6BBE8101239EC62861F4BE67B05B9CDA9C545C13E7EB53CFF260AD9870199C21F8C63D64F0458A7141285023FEB829290872389644B0C3B73AC2C8E121A29BB1C43C19A233D56BED82740EB021C97B8EBBA40FF328B541760FCC372B52D3BC4FCBC06F424EAF253804D4CB46F41FF254C0C5BA483B44A87C219654555EC7C163C79B9CB760A2AD9BB722B93E0C28BD4B1685949C496EAB1AFF90919E3761B346838ABB2F01A91E554375AFDAAAF3826E6DB79FE7353A7A578A7C0598CE28B6D9915214236BBFFA6D45B6376A07924A39A7BE818286715C8A3C110CD76C02E0417AF138BDB95C3CCA798AC809ED69CFB672B6FDDC24D89C06A6558814AB0C21C62B2F84C0E3E0803DB337A4E0C7127A6B4C8C08B1D1A76BF07EB6E5B5BB47A16C74BC548375FB29CD789A5CFF91BDBD071859F4846E355BB0D29484E264DFF36C9177A7ACA78908879695CA87F25436BC12630724BB22F0CB64897FE5C41195280DA04184D4BC7B532A0F70A54D7757CDE6175A6843B861CB2BC4830C0012554CFC5D2C8A2027AA3CD967130E9B96241B11C4320C7649CC23A71BAFE691AFC08E680BCEF42907000718E4EACE8DA28214197BE1C269DA9CB541E1A3CE97CFADF9C6058780FE6793DBFA8218A2760B802B8DA2AA271A38772523A76736A7A31B9D3037AD21CEBB11A472B8792EB17558B940E70883F264592C689B240BB43D5408BF446432F412F4B9A5F6865CC252A43CF40A320391555591D67561FDD05353AB6B019B3A08A73353D51B6113AB2FA51D975648EE254AF89A230504A236A4658257740BDCBBE1708AB022C3C588A410DB3B9C308A06275BDF5B4859D3A2617A295E1A22F90198BAD0166F4A943417C5B831736CB2C8580ABFDE5714B586ABEEC0A175A08BC710C7A2895DE93AC438061BF7765D0D21CD418167CAF89D1EFC3448BCBB96D69B3E010C82D15CAB6CACC6799D3639669A5B21A633C865F8593B5B7BC800262BB837A924A6C5440E4FC73B41B23092C3912F4C6BEBB4C7B4C62908B03775666C22220DF9C88823E344C7308332345C8B795D34E8C051F21F5A21C214B69841358709B1C305B32CC2C3806AE9CCD3819FFF4507FE520FBFC27199BC23BE6B9B2D2AC1717579AC769279E2A7AAC68A371A47BA3A7DBE016F14E1A727333663C4A5CD1A0F8836CF7B5C49AC51485CA60345C990E06888720003731322C5B8CD5E6907FDA1157F468FD3FC20FA8175EEC95C291A262BA8C5BE990872418930852339D88A19B37FEFA3CFE82175C224407CA414BAEB37923B4D2D83134AE154E490A9B45A0563B06C953C3301450A2176A07C614A74E3478E48509F9A60AE945A8EBC7815121D90A3B0E07091A096CF02C57B25BCA58126AD0C629CE166A7EDB4B33221A0D3F72B85D562EC698B7D0A913D73806F1C5C87B38EC003CB303A3DC51B4B35356A67826D6EDAA8FEB93B98493B2D1C11B676A6AD9506A1AAAE13A824C7C08D1C6C2C4DBA9642C76EA7F6C8264B64A23CCCA9A74635FCBF03E00F1B5722B214376790793B2C4F0A13B5C40760B4218E1D2594DCB30A70D9C1782A5DD30576FA4144BFC8416EDA8118FC6472F56A979586F33BB070FB0F1B0B10BC4897EBE01BCA3893D4E16ADB25093A7417D0708C83A26322E22E6330091E30152BF823597C04CCF4CFC7331578F43A2726CCB428289A90C863259DD180C5FF142BEF41C7717094BE07856DA2B140FA67710967356AA47DFBC8D255B4722AB86D439B7E0A6090251D2D4C1ED5F20BBE6807BF65A90B7CB2EC0102AF02809DC9AC7D0A3ABC69C18365BCFF59185F33996887746185906C0191AED4407E139446459BE29C6822717644353D24AB6339156A9C424909F0A9025BB74720779BE43F16D81C8CC666E99710D8C68BB5CC4E12F314E925A551F09CC59003A1F88103C254BB978D75F394D3540E31E771CDA36E39EC54A62B5832664D821A72F1E6AFBBA27F84295B2694C498498E812BC8E9378FE541CEC5891B25062901CB7212E3CDC46179EC5BCEC10BC0B9311DE05074290687FD6A5392671654284CD9C8CC3EBA80EB3B662EB53EB75116704A1FEB5C2D056338532868DDF24EB8992AB8565D9E490CADF14804360DAA90718EAB616BAB0765D33987B47EFB6599C5563235E61E4BE670E97955AB292D9732CB8930948AC82DF230AC72297A23679D6B94C17F1359483254FEDC2F05819F0D069A443B78E3FC6C3EF4714B05A3FCA81CBBA60242A7060CD885D8F39981BB18092B23DAA59FD9578388688A09BBA079BC809A54843A60385E2310BBCBCC0213CE3DFAAB33B47F9D6305BC95C6107813C585C4B657BF30542833B14949F573C0612AD524BAAE69590C1277B86C286571BF66B3CFF46A3858C09906A794DF4A06E9D4B0A2E43F10F72A6C6C47E5646E2C799B71C33ED2F01EEB45938EB7A4E2E2908C53558A540D350369FA189C616943F7981D7618CF02A5B0A2BCC422E857D1A47871253D08293C1C179BCDC0437069107418205FDB9856623B8CA6B694C96C084B17F13BB6DF12B2CFBBC2B0E0C34B00D0FCD0AECFB27924F6984E747BE2A09D83A8664590A8077331491A4F7D720843F23E652C6FA840308DB4020337AAD37967034A9FB523B67CA70330F02D9EA20C1E84CB8E5757C9E1896B60581441ED618AA5B26DA56C0A5A73C4DCFD755E610B4FC81FF84E21D2E574DFD8CD0AE893AA7E125B44B924F45223EC09F2AD1141EA93A68050DBF699E3246884181F8E1DD44E0C7629093330221FD67D9B7D6E1510B2DBAD8762F7\"\n        },\n        {\n          \"tcId\": 52,\n          \"deferred\": false,\n          \"z\": \"007BF379B97DA0947F2E9BFDE3359E282C9CF1D2E68A80209B533104E90F432D\",\n          \"d\": \"2D229AB46354901491476CCE8FA96E4A5FBA65AB2F538FEDAA528E35687A782B\",\n          \"ek\": \"C5712512984D94A039FC87739DFCAE09934E7658A82FB0895A060D54F900C5AC1161DA09E2D833D5B60E60FB000AF1BF4F43B059B8272E79AF4572349940209BB21BA3BC3B1B6ACC281A35DAA15923496D0FDB32A8505DC8626847627BDE759175F11B457539465CCE3E591933D8B458F561EBA446711CBDF2B604E53B7EE0E0C2C0A15C35AC2A2C91BAC918170E5372C542636D7526BAFAABD10CC6F4382B01C74AE28B47289AB5E463A584465C9994B739367C9F82639801A3681768E134185C9A0DEB8965079A99451418EC051D0D723FECE5B53488207FF7994082C16043B13D278ED530640BE0B4F9AC75B52429EDCA9BC4FA7BDCB43FAB630DB25A5EF576461313CCAD5B2E85E36EBF9594689201458C9B2D96261221C8D3C21D91F53D83F0676ED7A78A6177791557DDFA33FE39699C19339AA9ACD70B34D9036D5391AB57ABB2A5EA368675A565D24A796193351A37C69A5866F4C99482CE4BB3B7795B83E584761EDAC6BFD8CF2433AFC53641E4689571B999E8236A151B6E42855F7E9BBFB8040FFA59CDE707612C9C717F5827DC2B51766889784A6942E8957E6AAAA5D8413F76A37FE69F6259CCFFDC7BECCCC1DAE419D969620C0AC674367558F532EF697058250113DCA01C051A88FABE2CC65795949166857F0F89104A1187C9D30517F25F49308BE4634AAE29B30C8360FF3CC38B5A7BE717584C10A79929B36C1516DE545566B76EACE143E011A4FD42702E95139EB2A746FC04AC99C5E9F07344C83020C34165F9572CD86F50BB9A55B13C6DF33305C8601FE1B103057519BA43B8EC1BF37603C0495F40087CC68A808848429F64BEC6EB336C37AC50F2B5CAC04D6B59870E4ABFBE773664C3926D2954E3D57F2C8147683A519B7264DF40CAB6F3BF262B760BF794416A5D601776E5165FD50C4BA4B07C49AC494C699C4705254A450B36CB38EAF96D6B0270492B84E5A5C208D6ABED761F033138D3BC9FCE42C17B160696C7CA9726BBD2B1C1E42C92556A06A5018EDF605B2D789688CB85066CAA0528BDA4E32542621727301B90333C1E4393FDB539ACF8AFC202BBC42546BB88A04AF9C089717F4073360B567D3967620BD8ACD0BA1762C56603647DEE371F552C92C82A69B1E461E4D1572FBC881AB526B49358F21A69DD3C7CE32BACFEDA9D5CCC34E09B9443EB189F69798FC80B61011B76239EEDC7C77F1B78D3077C5549C48BA8BC720CC2C8B88FC85A9A5CB6C1DA0829C504A9FA502899926BF0DC8FF9C02DC9FC005676A84CF16E2B23B7A5946289E400D0D2387E36841A227B7F10822572BD62F134EEDBCF1A66B6FCC907F9E0AF8D349FA8B5C4251C66B3690BB21A3253F3916020934381B46F1BB9C5F638BF8C8B256300B62B5D6F3A7FF680B514F6B3352A1994C8511957976836BF65979E13002AF1453C1FC037669B3465A0366B7B5F94F92C7707675FB08B2632AEF3D725CC4B3B6496B4BCEA2C865C982F7946079287D63931C8940B130776F5A7629A64915BC4B1FB09CD4C9114B1018937A83047EB3F22EB7EF5C866E9909CC89072E69C973EB22BEE6A3B1E383DA4006CCA560100C72BBA81237C1C7AB0A48A0CC58ACCE826B735C8BA19A87C9AC74E77295A8B26BDBB7685053C5A1572A09425CAE97D7F246D8D0B85AF20350999356ADA86628A787482393FD85A2166245B442F64B5516D595C471BA4CB577644738F87853F65236FF46ABAABEB9236616CF5999EADC9BA80F1C0FE8B6C45BCB543AB8E9097AF977612CF5A4E22C274A278472FA93E2B817706E11813F2B3865851C96683C83B52D2369DF3F74C111B4F4B01202277A918660B9641691412B637B7991973035F77B02D75A2143813BD49847F082C16E31EC89A2F8A588B2D40519892C939D782FFE18BE5D0BE1B5A41D594C32E246F886C37D43145DB8334B0E3364F65A76E0533FE052535DC7945669019E7310587C4C71A3883E1123A9A5BEA542F6D8CAB83CB905D26C82EF72A84285A07687ED90A2A32083F1D8519AC6289C9F6A5FE994C96ACBE0303BEB3B7A5A7457BC0118AE7008A0AD860310CCEA57BC313595A68CC8B682328D8C4440BA57E749BA40E968D09A0783CEA0CCA59B43FE9B42F157F38B67ED0379802ABC1CD50288D73581CCB59E3768C9801138B658FDAA87AC02DF5B5386C2DEFBB8605988CF7B1BC6CDF5C8F1F770EBE3E49\",\n          \"dk\": \"81D65577F87BECBC2A8975A7FB237049AC574D9C934FDC9764FB79597C0CFD236E8F516C3DB4AC0F627A02DC8426C051A6F0421B6A2689ECC469E92A0D816E85990E9298483902A6CAB76E74D476A9300E8121958306959AA362263C885B483E326285CF970BD84A694A553E9BB3AC3209AAE0F0521F3564CF352890289A530717F5E080A916613EB88304A7340A2413C20B02F62B58D68A3C97F57C8A11B1E58611A2A18B23FB3222E84D2ED287E002C1FDCA2D47C03DD5BC0B69210789241AC177907CC3916088B6D5E6B9B7C4C8C975725E38C5A3F54964057114D7CA565BB71F5C8C866A83F0E62AF7866D94C50E89B1BD8F1A8596B477AB743A427252D2128967B962F2E7590ED47670542BDF2162F8C2B1CBE434862670926330B90002E4C490B80A57CC4B02EC03B40CF6250E727C8E1C05E2C36E9E0AAC4FC0C4C4D89EEA2837408B53542513E5C898E62722415CB71B7A9EE7E8634A00028D549A2F912797C84778B7C5D4559A885430124A5D161789EA8972EC9A5298693F4857A4AC905E53A8866148117AA60D43938F84BA60F8C15B7BC88824611AB8EB74852155BFA82127D052B6138E8A7ACF28774DC2C798EA9097723BCD2EA4A0A1B38CB666830008A256F05057D126E9C440AAD52AC7C4B2E370914C2883ECB13AA5F53E43A25D59661809C960545186BB6931BB45561307A4D45C1A0EB80EB0B4166AB12EAAF8CF4D25CA4A8454B179246D3019B8EB5CD86B2BE40513C7828653BC08CC652FA59665DDDB0B94E4AEA22431F7557329434C688B5CC0789E675B0A9395AD2CAC7DDF166E0F5245DF835A00CC172B3A6192808741652C4025566C43177F5A9911B317009A38ADA45F8339693971AA1D773D6FA240F6D7880BB5244115AC2EAB5FC2408BAF4705C7E016E9B1B6A47567DF5298F47437F1C74949232EC496C3054B0A4805C8CAC2255950A8B7F683CF5B531C4C798554963875928CA4204AE8755AF433C2D52784A36404E1A368CC4FAD29BFD879565CA3CB52D56B3F273E7CEC08A1D7180F5037CA61B289828AA3838D01F9958388779ED2881FE894A9D3699542BC0A2C497F7251986B50EFF284474794C845A53E12205FCB823872640B1A8583856DBE11BD6DE49AFC0142AD940EE43B06D9D7141674212297B8478B784FCB8A886508451B376822726E00CD7FA1CA16DB9F591007B2689E0C827929612035F5150800692ECB83B244964FE6922402297F244E430A06AB897DDEFC70742BCA5AF7B634A1B3C8C719EE2909A99C19EA602B7E22C66001CFE5401440139DD6398B26141C9B23914E5940EB35105131451DD3CBEF8654F0483887004D22AABD57559DFFC11038D3BEDE5CBD44DA0119D87610E0CBE392415B33A35F57364C1177DC514AD94570140217982593CD20BEF5E43BD0638EFAD1478CF9943DA093AFD278037010C7086C04A53C0F607D8B867222DB98A56436E6FC3B28D7382116706DB679D9316C473C6D86F85DC40B0A0FE24D8905321336488E20739FB11652EB62C7A05CFDA115791CAE294A0534491CA5EA8F5BA6730E06AF33964469983770D13C858CB66D091B02DA5181ECAEB9C4A241A2222DAA77B6E5020530474C891D440BA6E3F2137B215526001BE17185F0048F3DEBA1ECF7BBF1A55EC9D57485969B43821930DA7672B33630209F8257B8DD749FA9F6C73CE3C903B2300D7304FBBBC6F5304F6DAB322117A621A851CDB66877A82C350B4F42525E16328C7B8A07948794CECAB06D7A7B13CB070C61A983647319E1B6B4E27FC900BD50F485B98121DB4180CB62AE4C3C68A8D59F18885EFEBC90B3F1C9F480B068DACDAD813A28EB200EAAAABD9A0DC5D72E9B4505778807AA6A52AD6142DC517443FC55CDFA56B2A6AACDB28B4B5045344CB418795241756943CC5BA1A4B73A2C90A2122121559FB15B015DB43B621B20F01A4731438B148395CAE4A7C36888FBF01603336991572753F35DEF2264D93BA831A46AD5FB3F663704617B712B81595A585123F03180F46285397551F27E98970DEBC2BF8298329BC87402869DF5B207C7415D1BA9615300B67FACB7A4AB1287E37938C2347C172F96A8826C944CA75C63488A9BDFD206B41C8E6E854B2D9C59D169361BA549254142387337A89C919C84512B2394C5712512984D94A039FC87739DFCAE09934E7658A82FB0895A060D54F900C5AC1161DA09E2D833D5B60E60FB000AF1BF4F43B059B8272E79AF4572349940209BB21BA3BC3B1B6ACC281A35DAA15923496D0FDB32A8505DC8626847627BDE759175F11B457539465CCE3E591933D8B458F561EBA446711CBDF2B604E53B7EE0E0C2C0A15C35AC2A2C91BAC918170E5372C542636D7526BAFAABD10CC6F4382B01C74AE28B47289AB5E463A584465C9994B739367C9F82639801A3681768E134185C9A0DEB8965079A99451418EC051D0D723FECE5B53488207FF7994082C16043B13D278ED530640BE0B4F9AC75B52429EDCA9BC4FA7BDCB43FAB630DB25A5EF576461313CCAD5B2E85E36EBF9594689201458C9B2D96261221C8D3C21D91F53D83F0676ED7A78A6177791557DDFA33FE39699C19339AA9ACD70B34D9036D5391AB57ABB2A5EA368675A565D24A796193351A37C69A5866F4C99482CE4BB3B7795B83E584761EDAC6BFD8CF2433AFC53641E4689571B999E8236A151B6E42855F7E9BBFB8040FFA59CDE707612C9C717F5827DC2B51766889784A6942E8957E6AAAA5D8413F76A37FE69F6259CCFFDC7BECCCC1DAE419D969620C0AC674367558F532EF697058250113DCA01C051A88FABE2CC65795949166857F0F89104A1187C9D30517F25F49308BE4634AAE29B30C8360FF3CC38B5A7BE717584C10A79929B36C1516DE545566B76EACE143E011A4FD42702E95139EB2A746FC04AC99C5E9F07344C83020C34165F9572CD86F50BB9A55B13C6DF33305C8601FE1B103057519BA43B8EC1BF37603C0495F40087CC68A808848429F64BEC6EB336C37AC50F2B5CAC04D6B59870E4ABFBE773664C3926D2954E3D57F2C8147683A519B7264DF40CAB6F3BF262B760BF794416A5D601776E5165FD50C4BA4B07C49AC494C699C4705254A450B36CB38EAF96D6B0270492B84E5A5C208D6ABED761F033138D3BC9FCE42C17B160696C7CA9726BBD2B1C1E42C92556A06A5018EDF605B2D789688CB85066CAA0528BDA4E32542621727301B90333C1E4393FDB539ACF8AFC202BBC42546BB88A04AF9C089717F4073360B567D3967620BD8ACD0BA1762C56603647DEE371F552C92C82A69B1E461E4D1572FBC881AB526B49358F21A69DD3C7CE32BACFEDA9D5CCC34E09B9443EB189F69798FC80B61011B76239EEDC7C77F1B78D3077C5549C48BA8BC720CC2C8B88FC85A9A5CB6C1DA0829C504A9FA502899926BF0DC8FF9C02DC9FC005676A84CF16E2B23B7A5946289E400D0D2387E36841A227B7F10822572BD62F134EEDBCF1A66B6FCC907F9E0AF8D349FA8B5C4251C66B3690BB21A3253F3916020934381B46F1BB9C5F638BF8C8B256300B62B5D6F3A7FF680B514F6B3352A1994C8511957976836BF65979E13002AF1453C1FC037669B3465A0366B7B5F94F92C7707675FB08B2632AEF3D725CC4B3B6496B4BCEA2C865C982F7946079287D63931C8940B130776F5A7629A64915BC4B1FB09CD4C9114B1018937A83047EB3F22EB7EF5C866E9909CC89072E69C973EB22BEE6A3B1E383DA4006CCA560100C72BBA81237C1C7AB0A48A0CC58ACCE826B735C8BA19A87C9AC74E77295A8B26BDBB7685053C5A1572A09425CAE97D7F246D8D0B85AF20350999356ADA86628A787482393FD85A2166245B442F64B5516D595C471BA4CB577644738F87853F65236FF46ABAABEB9236616CF5999EADC9BA80F1C0FE8B6C45BCB543AB8E9097AF977612CF5A4E22C274A278472FA93E2B817706E11813F2B3865851C96683C83B52D2369DF3F74C111B4F4B01202277A918660B9641691412B637B7991973035F77B02D75A2143813BD49847F082C16E31EC89A2F8A588B2D40519892C939D782FFE18BE5D0BE1B5A41D594C32E246F886C37D43145DB8334B0E3364F65A76E0533FE052535DC7945669019E7310587C4C71A3883E1123A9A5BEA542F6D8CAB83CB905D26C82EF72A84285A07687ED90A2A32083F1D8519AC6289C9F6A5FE994C96ACBE0303BEB3B7A5A7457BC0118AE7008A0AD860310CCEA57BC313595A68CC8B682328D8C4440BA57E749BA40E968D09A0783CEA0CCA59B43FE9B42F157F38B67ED0379802ABC1CD50288D73581CCB59E3768C9801138B658FDAA87AC02DF5B5386C2DEFBB8605988CF7B1BC6CDF5C8F1F770EBE3E4987A74BAADEC58CB97414E0D82652052055EEE3E3B64001A0DC6172A2A48DDD91007BF379B97DA0947F2E9BFDE3359E282C9CF1D2E68A80209B533104E90F432D\"\n        },\n        {\n          \"tcId\": 53,\n          \"deferred\": false,\n          \"z\": \"E94F4E83E6CAABCA9E319D40F6CE0E3691B77C92D9E3766BE9B6F4B6DF2E640E\",\n          \"d\": \"1D65D0290B15903371D616D7AC3F2FADA8CB24E6C84D52C039A10BC1288C1110\",\n          \"ek\": \"F4A4800C492B0472295F4B65471C6170C96DBD90130867CC68369DF450214ECCA22420CE39341A321682C054719B33469BA78C5F0ACD0CF466F2A188713B5154B279E6A6BC1A91032D4948B2D521E3090E450ACBAFC250EC72B7E6DBA3FED99F22730B0588042F33C50F7A81F368ADCB348864AC6DE5479EB1A041F9E3B04304CA38694B5A5B0A674659D6F9BD49AC964ABC4BCC532DB8CAC7A437371DB665AE4CC5B68CAD6AE475472A05F21981998CBEECA435E14C72293127A12659AAA5572042A2789A3B8FCC1D81AA1B95929BDD1A8E13192CFC700AF0E99C6A839EA71297A2A496D9045FBB9425FACC6CC5111868D48AEF176B7462B321308AB199B02CB3A6783749CA845DAAFC1C1C3BB2BA6B0F6C29BA3B428F971C750223B84A5ABAB4B77285A04E03E78D686B38990208A0FA740D01BD05B828F0D36EF5B1056183B93F8238F4AB7AAF872CB7916BD63AA6288337DABA6A5593BBE431ABE3497314F9529A9B5A7B8212C4BC23989A87419903E7B9A2962B310F865969264F2C0A0E6BD99E21C15EB717626D642FC2C6375578780EE13D05A6B3581C70B32C96E4D4178A7920F3A012D3C4C9FCD16556E5965F12AD08B294582A3F441467A3185111E53DEAC9272869671C2CB435E0B60DE5AE0A1A2A53818667500E986395A997722A626ABFA5310E4A43379C15CD3174F4F209379626EF6C4A1F076C73539F26E7C4651317C4545709BC757A92BAF5F417F6B4235A3535D5CB9A6D903D64C89B857A841EA25747F7A0D207729CBBB3CA9B32CB1A38D2AAAD124658C216533F4380ED108040559C8C92CB343C30A5B6A156903E69BA14A3B3C4E9B8C73B2C1ACFA302DB96B9C724B28E6AB1EE489867AAB84BEBC3B417048DDB0304EA8910C55BEC887E59EC336BD96947DA832B213FCF8C68D9832EE0289DFCD8B91DB20BBC81C2833C144A044E917904E9968D0D096482549C55062C0DFA6FB1415FE491051BD9A1DBE65417CA39A0A5CFC79B1A8D909123D287B1E9B7F2A683CB864D693C65BD770C54FB6C243441328C95C6EC7EE5C0802FC0973CE88AD589353A430A543738FC9A5F9EB1730C7060A8279C61527B486669E1C2995F242DE5C42DD0021858C2A3E07250FEB268BBB38FAEF7BE2C4C6CC8B5CCEC0005627874666055091283B7E522B2D96F52A60B031BC6549410AB769BDDB15717A0557561ACE7082FDC463209E63F94919BF3C33BCD012AB9321A0134C017441B578410204A5BF70C57D572C7D7A17F12A2176668A8D9E0C9C216AE9F2466B4F961CBD1256F247EE3B0B23F2AC39ED8757E8A497E161B551CAE1A2C6B88629EE4788DEF79249606195E186398F01CFA070E8B57156922AE4823468DF5C7C5F9065F38639BC77B78A4ACCE5496459346B6D07C49128F0C9A08BBD3C448B97A31255F0B6A388BC8A221009EDDA1AF9C819840C67DAAC24F54D2220058C378E8A4A23825A49A5D4B631FB3080ACAB9BE2DE3A258EA76B01C47B4D8703CD22EB78B02AA2260302192021A0FC031A6FACB03A33055F25A2BB1D6698B4A8DCA9238DD5ACD6787C8912066EACA67FDE86FB6C14687B076D19450CCF75EE328BF9DD6ABB89030AD08BB917B738024022E69A243E779355850B18A7C97F99039271F6EC47418661779C0A528D4079C5ABED21475B864078AA92F69E64F7F12B0BA661B066114F1F43EFBA89B67C05518C15537115DA0909B4A097361586C064A82B6790766944BF99176D090AA8DF599AAD37922B16A2553503AF1B4D7B90FB967AD5A637EFB019E257A6CF2AABD470157352A3196B9339715A1B254B5FF37C8A799473CAC6D0AC6A4AB56BF03271831A33EBA29AF1AE689FF54A2431B5A3F3BA83DB5A51B0492CC2C0DE31962293539278A11B408BF50C0B6D82A9764280DE4F8155201424DDC63F58AB413813FFA011EDEEB4B8B005E707880AF88392A472AD487CB6B6CBAE8787F5F8B4A5822B269E74FD91635EDE9541B5C2A5C9A6FE15722AB2BB61F225D08A84D4A9577D52617EDEC650E429A45B0397147920DE7201925913F029985C229D1044DC1895BEA961F936165CB24BC9DF884F6F49267A19B9B623FE0095EED29B0C75805BD9AA29EE857A9970533344DA99963C057337D4C25D517A341CA4BDB86C74DF0B23A56762B5838F5C68FACD1433B948824CB86D88A1560E77F4EB4A2A95E140B648DB88D\",\n          \"dk\": \"25ACA925E757A4BBA64CE712864669DD8BC9A55007B2C3510AE778DC85A877AB08EC454C61F32205E76C509B81040466BC47C9EAE77B10067C14F241C2E992A16708D1C1604B0C3F5885BD2C86B7B1E8BBD90903B0CA242AE206A6D31846973FA38B9D09402A35E45E98E84D1B125155D86E667960CC058AB25C2B053352C64BC03A78CD86D53BC298A3D586A08C1506103B301AE502176804C328AE1F24CD270AA76CC223709AB7E4B8AF3F28043F5C21B8721F8C228B7F908ACD7C4F1DE5C8939B643B22BDCC1576B65901FCC0720C622C4FA5BADFB7442CF63CCC513A8C7709D3C399469907DF740DDB3104CEB9C92C33113D258AF7EB220EB9AED0B2C1BCBA70CB9C76B840276EF11FAD35BED30CAFE633CCC4E3606F758A0F993D27F80F84473F770565B371ADF5E61689B0402D681F0D93745D625DAD286356A152F689413FB89A569CCC39E71912B3BD345AC5643725969B896328B24568428E5C651401447B490180003CFA674A5093982FA465FFE017B62BB042CC9C67C955B3123B0755430666330547C333196322843744B9213C5528585B4080265CEA0A4931D77C7D81BFE73008B37ABD62082287608ECB2CA6E50CC4C9529E32DB7BB70917A5BC3F8BDAB897E52320395749629104F3B60E27A0D3C09508E422497B7D6F711DE43C315BD83836538F88C30B7AE4A5B5B122D9394E34479D698C8B535354C25B5BE572513DD24A64985D78517AC2F3861330663C1A2AF6C135E0E49E5B930AC6A6A81A203E0A3A4BD1642A304C654681A8769C6C6AB009690A7BFF3B5B9FB64B30C2AFB9280FCF10377601B9047BC6ED0A07310091A97A8016735695EC704FD8493273B2DFD8C1A372C6B7A1A91DA08D3F2767B372A688228135F69D77D1A20CB03C69482A7B67A1E59249FD098E19BC84CEC247922A52A3EAC641F9383EA19200E16874E876AFA5C2598355F5EC01CDA86BE976A7420061C8D9A2F1E341F1378FBBF31DDFE3699DC60637138DC3A19B5FE17D27F0352743C76477363B7C72CBAA2E0F83CB53A82EEBDACF0642C5C39868CDE28F5E83A08046C38B9A1367B2B68C50869E6A9B44379AF1111133ACBF3E51738D2ACCE510A1B5F40B5D17CF5B5710C70B20A1A4734934123C2BC8B1D3714123A4159021B0688484398CA1346D0B5A9CE6AB299F439CE0B83A29261E67B905B944C8E74041431738D4272FB11254CB483907998F364235668C960AB0BE12580908093C31C37BC769BFB285451FF96155102376B8803DB777C15683B963765F2BC40B29558275864B0668F4C269F36B1D6E935C16A2B160E6B9A922080039A2946198BE02A82DFC1B86B310539502B61BC4B8DB533D939B973830EDD6382DD02AEB876ED7332954D2B0F1831953614CE6622BC91C3729F7BC6C45780974B4A40C07B5E4614C858CFCCC4C42E32CB3492F4F36546FB6ABFF9C6AD8C96B54818B3EE3545D2A2687E14C13B29248C69403A63D79A886195C630BA97AA52539C8949DEBE323F68C53EF5AB121D4AA0AC10D2A28A90B39C03187761DE728FDEB17FEF21257DB7199D665D5A5C37823ACE4830F09B3440A345C3627A136FBB416033BB0E73AD9F1B2417075D1F0C928778DEABB59C03A8813FC8BFC8B27797CAE5467C6FC934AF36922F8983E4A8B4F9BA79E2C1B7D9D2297AB273289B8A1EE2CAF8740A201A9C6F2ACCFAC8A0DDF51BDF076521E22AB196811E0834E96AB84606A1E1F018868604BA32C832FDA967CAB1C3021AA4D59A3E731242D7792729CA646D8B976370C2F93332500A9AF32684EF6B6EE7B3690B57409C4A78AC92F9E58CBCF9854B18A737114045EF72C5B808DB039126A8C22C605287292AF93B932AE153EB932CCC06CA4B8DC18AE8C0237EB1122A1C0C1C95E4A5118CE674990707AFA146B5AE620FB44A3FBEAB22649A37495825485634EAA6B7565BFAEA2493D3605D9AB5140C27FEB1350C93998CA8C5E52B14BDB9679763C20D8CA516CB8B5F089BC30E5B9DAE631D6C571AA572F40922E13E8C9488375603605C1D3505FF27033F57040B904D4B71A30098A260169B49495A1B52C76E924FDA088ACD56B90565A49BA8E0A1C66F186367AA7710157CF4A28B2223C2D034079E5ACAA88DB0E9DFB585C7773142A43535CC6083B1EFAB0A8F4A4800C492B0472295F4B65471C6170C96DBD90130867CC68369DF450214ECCA22420CE39341A321682C054719B33469BA78C5F0ACD0CF466F2A188713B5154B279E6A6BC1A91032D4948B2D521E3090E450ACBAFC250EC72B7E6DBA3FED99F22730B0588042F33C50F7A81F368ADCB348864AC6DE5479EB1A041F9E3B04304CA38694B5A5B0A674659D6F9BD49AC964ABC4BCC532DB8CAC7A437371DB665AE4CC5B68CAD6AE475472A05F21981998CBEECA435E14C72293127A12659AAA5572042A2789A3B8FCC1D81AA1B95929BDD1A8E13192CFC700AF0E99C6A839EA71297A2A496D9045FBB9425FACC6CC5111868D48AEF176B7462B321308AB199B02CB3A6783749CA845DAAFC1C1C3BB2BA6B0F6C29BA3B428F971C750223B84A5ABAB4B77285A04E03E78D686B38990208A0FA740D01BD05B828F0D36EF5B1056183B93F8238F4AB7AAF872CB7916BD63AA6288337DABA6A5593BBE431ABE3497314F9529A9B5A7B8212C4BC23989A87419903E7B9A2962B310F865969264F2C0A0E6BD99E21C15EB717626D642FC2C6375578780EE13D05A6B3581C70B32C96E4D4178A7920F3A012D3C4C9FCD16556E5965F12AD08B294582A3F441467A3185111E53DEAC9272869671C2CB435E0B60DE5AE0A1A2A53818667500E986395A997722A626ABFA5310E4A43379C15CD3174F4F209379626EF6C4A1F076C73539F26E7C4651317C4545709BC757A92BAF5F417F6B4235A3535D5CB9A6D903D64C89B857A841EA25747F7A0D207729CBBB3CA9B32CB1A38D2AAAD124658C216533F4380ED108040559C8C92CB343C30A5B6A156903E69BA14A3B3C4E9B8C73B2C1ACFA302DB96B9C724B28E6AB1EE489867AAB84BEBC3B417048DDB0304EA8910C55BEC887E59EC336BD96947DA832B213FCF8C68D9832EE0289DFCD8B91DB20BBC81C2833C144A044E917904E9968D0D096482549C55062C0DFA6FB1415FE491051BD9A1DBE65417CA39A0A5CFC79B1A8D909123D287B1E9B7F2A683CB864D693C65BD770C54FB6C243441328C95C6EC7EE5C0802FC0973CE88AD589353A430A543738FC9A5F9EB1730C7060A8279C61527B486669E1C2995F242DE5C42DD0021858C2A3E07250FEB268BBB38FAEF7BE2C4C6CC8B5CCEC0005627874666055091283B7E522B2D96F52A60B031BC6549410AB769BDDB15717A0557561ACE7082FDC463209E63F94919BF3C33BCD012AB9321A0134C017441B578410204A5BF70C57D572C7D7A17F12A2176668A8D9E0C9C216AE9F2466B4F961CBD1256F247EE3B0B23F2AC39ED8757E8A497E161B551CAE1A2C6B88629EE4788DEF79249606195E186398F01CFA070E8B57156922AE4823468DF5C7C5F9065F38639BC77B78A4ACCE5496459346B6D07C49128F0C9A08BBD3C448B97A31255F0B6A388BC8A221009EDDA1AF9C819840C67DAAC24F54D2220058C378E8A4A23825A49A5D4B631FB3080ACAB9BE2DE3A258EA76B01C47B4D8703CD22EB78B02AA2260302192021A0FC031A6FACB03A33055F25A2BB1D6698B4A8DCA9238DD5ACD6787C8912066EACA67FDE86FB6C14687B076D19450CCF75EE328BF9DD6ABB89030AD08BB917B738024022E69A243E779355850B18A7C97F99039271F6EC47418661779C0A528D4079C5ABED21475B864078AA92F69E64F7F12B0BA661B066114F1F43EFBA89B67C05518C15537115DA0909B4A097361586C064A82B6790766944BF99176D090AA8DF599AAD37922B16A2553503AF1B4D7B90FB967AD5A637EFB019E257A6CF2AABD470157352A3196B9339715A1B254B5FF37C8A799473CAC6D0AC6A4AB56BF03271831A33EBA29AF1AE689FF54A2431B5A3F3BA83DB5A51B0492CC2C0DE31962293539278A11B408BF50C0B6D82A9764280DE4F8155201424DDC63F58AB413813FFA011EDEEB4B8B005E707880AF88392A472AD487CB6B6CBAE8787F5F8B4A5822B269E74FD91635EDE9541B5C2A5C9A6FE15722AB2BB61F225D08A84D4A9577D52617EDEC650E429A45B0397147920DE7201925913F029985C229D1044DC1895BEA961F936165CB24BC9DF884F6F49267A19B9B623FE0095EED29B0C75805BD9AA29EE857A9970533344DA99963C057337D4C25D517A341CA4BDB86C74DF0B23A56762B5838F5C68FACD1433B948824CB86D88A1560E77F4EB4A2A95E140B648DB88D7456EFF3A15CD68111A12974CB06566E9007C376E09CB10D47C73E43546AB16AE94F4E83E6CAABCA9E319D40F6CE0E3691B77C92D9E3766BE9B6F4B6DF2E640E\"\n        },\n        {\n          \"tcId\": 54,\n          \"deferred\": false,\n          \"z\": \"EC54F6E1E7FB12B796D0E56BE6FE3BA6EDAAB49B08712318B27D229606D2AC70\",\n          \"d\": \"22D19527844F3CDB8A342620A96E902AC7C36E54677ADA6FE8DB08DF4EF3B36B\",\n          \"ek\": \"0E4C2891EB5497C1BEA0A62A0AE207018A2C0FAA8991D76CD14C47916853C326123A3C2997BC27A0B7073F97881EDA9CA18996779064AB768749869E66C4CA6AA58A8B155371B4729BE2327165B103393B23941FC6941A2BBC3C9D1489D6692E146B73A428241172C1EE63161C82A6666B7F5F757BEA328CE6AB45D0D87FBD007333EA927C3810A0A1790DDCC14A607D45A046E23C043CD96196659337FA940E67C565E70CD7D45511BA4C01631D43DCAD1C09833D64BE170A0681E0978D1B4F27E253BBBC5A4C2377299041333A1FA1FA0329B9B6FF24647E3B03954A85BDD637697CA834C95E1A992011D52D2719AD5FC410A780291CC575E82A2AC229BD8C6C42D4673E958ACBB6DC6EA644AAE5B10D5D90907E82614881182B127A53356A26A62C8463B26E5415C131BD72B16D2BA1175FEC0B6E59723FF7CF917968AFC91FE0698F40F55A9C730A1A493314991CD0329083B84292DB23D706755CEB4CF645A706E43512432848639AA757C61E727E8112A06B590488C4917C74041B92C1F34194A849211785C10FF71D27917D45A3B2A67966BA454A3B4829DCCC3CA8493612AB1EB554469E9950B4DB78E8D96E2352CC1AB2895FB16362143A25B3171D4C05A2AA5E5DB845220AB77EBA1166A5842436B226966988BCA3CAFC7997B0A261F97AA24C70BEC48CDDD491A1D2CFCC4455D06AA956348478580D1C70008C0B4AEA3CCFE9EA6C37A3B467652179B446F4D060B532AAB668554ABB88974BA6AF174861D52124EB9D05E60E62E11D67F34F1747951C2C6CE385BCEDF5476B882571579937F57C5C605351D12AEF9C6CD9B8BF35E95F7AC70D50C62B3E68899C39173101AC8EA326DF755BCEDA4B746BACEBE9CB8B233AFAC73075738BE4EB905D26438C5A68D8A14645462DE90783CCBB841431A452B632AD672494377CCE80974247951711A94CB907611C7E96E501EAA13BF225A041FCCFC0318CAC430C58CA043719668E59B4CAB34E2FC2ADE662C7DAD744CB5963ED6A9EF2449A000A6080C555DE2B3EA80C5AB7091DC18C4811697FF6E0A6D2D98489FB211B6B786ECB0ED793448351B80668A46ECB41FB8A642F10A0CC41A1F836303023C33B3B54BA6C2599268C433C6C0832052174739267CFD80C74C3225AC4D75CA8235108165E69F233C213B5AC547794E7CE59DC8D21D76B2CE0CECE43578FD0A1CD38200DF0C020D39563B519BC138B39D819A7A8AAF7D9C931EA75FA9B69369A7A6038B4D3937F7EFB14CE779CC1227B7732172AD10DB932531E426938D067144B6BF4F67C3DB0135573C219552719CB67ECFC07A489BB81F4A0773988E5AB3ADF7C75445868673B6D255B72AA73B32EFC015EA83010F96148A289B10101990290843CBA81E27AC58495F12CB05F5A8454E66F5B10CF0425BB6A1A8580C4C4110304C4717C84431447E510B837C8D70C8B589241592A107E6920FDF8627357C3C0D931F1F69EEEF1C3C518BFCF4305C98A86CE950FA40195F04B79C1471D0389792502C344DA14C884890898262E8292C3E5912A595D0E561CB3594FB4391BB5F4B6571969C86A8457633A5D8212393773D5D736380707BE35895948AE4AE8A672A16BE1E536549A194427C24080BDE083A63AB65240936F3FCA52DCE473EC08B765246AABD6C1F9549C0236676AA2AD234065C65672ADA92281696CD10A1AF1580A88E602ADBA75BC84B10A57072D1695BAE791E26114FCB408576516B7741D0CA9038D2C100884C417F2739930C50E408CC0A83023E6B13C236965802F62B00BFDD0032F98BBE7084AE0B5C6A26613470A8BE5B6C6BAC59B9B725C55C71E4E8A381BF88A0341943B942228FA6E3B3BB614887846F05461D632685486BC73A8E1794483125E58031DCE0A5C8E175C3B73AE08C01D2EEC8677C32F7A2B0D620ACA1D128E6B42095D8B9179369EA88B5697380B4C13552EEA86D9F8188A1C9093B794A9538EEC3A6D5F621A9D991DA6974E6A804D00A2864450C5AE59C4FFE2AEDC23AC522939D5D2204168CD8F8C59E87723DDD4809D2913F6825043EA6C7C9340A91914FBD13C65384B06B955AA221D302112C1F0802F71443FA9ACA95CB236E64886B4B0332BAF527C4AC3A9B4D8956D5FC2904911B944EC3F9FA07267342FFC4CB9CF15CEFE7325CD4C34B9A742B4214F50A50E58B9BD03C053806F0677A35438CF5EFCD8\",\n          \"dk\": \"3E793CFC068B5ED1B1E6D8ACB44ACF89C78CFCA91603E5182B598ED7937F9CA4973DDC1FD3A5023AC5A7C5B34DDD98B3CAFC2B548C9670C18EA16C15982B8B46F74D5A56AB08A1C517FB3543F678C3A97AB45A7A0FB9BFD12AA9BDA96E695674E5A78F3BAC74E033C5A3687A27A13D889B4A36BA339AA25730DAC64C464970FCB268F20F79F055EFC06964F6679AC56EDFB065C39B475370C98C4670387B97B2FC488E567E3B6109FCF28B764890B1E8529DC3BF58851EDA97358AA7B42C872EBFD832FF17B31E2471B091C3254707FC526521B9256997369927286B332CF3657CF3EA584F46ACDC2901B98A117C86430E00474611BA4043953685449D415237BA4E63D99F92835F3BC04C575008FF02885CE26027D914C67CC7F61CBD1C492C07AB1B278B68AB37A13A9C3CCF0BB84CA18DB371C8C4415D29A47F20DB1BC18B52AE4A831DA34B4A541C0D0C9EB17CB2675845CF809709338242A29C9E6239F7797A790A00CA407CBC536A079201C1E2672E343CCE0714A5E8C446D413BAD40E569280B2C2A255481A169A43F5AA501E61914EE35C2964B295914FC4158F7D3678D9B4C3869782DF96189FAB4FD7C171CF3210B7D27E34A593A5A215A4966CF73BC1045402ED8C0E7E548363600A871A0330B81B6CE482CF33C368956E1A49B848755B910A609B77389AAB1E6CF60D75555D21B16336589B474B059813828A8925C681524D9A5FFEF972736856ED169A5E6096E430466D03598619B3709CC91CF24DD4F55AD484B081356A2241B438C37CB250CDDE46072C16454706922F35C5267614943A9155169483D82CA716327501CE7DB313C771AA197367E8E11E5092A519DB2D26FA390BD8070390294589871EBB26CFD636D3D438BB9B0055A8C99F31AFC91245650483945C2C265170D6724102B0B9D0EAAA914CA85C941A6D90668A39582C515C5EC10200C1458D53B0A5B012B4D8B27F917D151C0896C9328903C49DB6B4FBC4526722ABD17C2CDA19A3519BC781C00DF97C5642798359DB358DC153B2885AD8C4BC59E94242BCB3436460048A248CA40F8AF3BF04799B603037567C0A85D65DF17CB9C520B4B56C01DF4C26BFD39FF4D27A1984358465AF96558054A89320434910A54E80D860BC38168D7C1E0F978720CA64C57B7ADDC98B713813F7098D548631BB062B069682E30547AE8BBE394551CCA1A506ACCF37A8B052E86F03DBBFB3025482EA1B2FDC2A9A9B077024B2F0827A33E7661C199201365A840A11F3F649A4097F630B7EFB734438E83C67284E12014282804C46049AE7438352A9AFAAEA2F843A347D753972728B4AF1954248291B4AC278A8813FF5083C582FBB1B8D4D49A227A49A99FA04827265F8C9902B98B299CA4A22668096DA77F7651DF1DC723CB98DB9B921ECDA5634451BC5B94663440F35A358FD8174057A38EEC579E49209B11CCD2DE729E704749E70AD13678BFD365CA26192965076F1F3868503C450DB964EF48061159B0841A06E9A2D51FB78C6033E5A063D30D2C160526531B3BE8EC561D32857136C3E1FF757D150A7E48B876928A1D4BAB34162180E94545285865A6C6988645D1A746C6C744B196C317E6AC95163353EDC381FB7AAD5E07993BC3141A64489762F6B9041CA438F2C135FCD737503A64FE1E5592FA68587B40A7F9913DC560D1104C1A08CAD82275B4AE0838B3BAB51283078D619B77C82980569C524A50158B8F3D352F3060530E9AF6CBC9613C67110773F1A6892E3A83D30B918DE545FA48B19A797BC2A7246D29B251A46325F453EABC1B8EF795A24E488F3C269CE04C3EBC8C0CAC82DF837B4EC46AD747721695C5C18A45EBB52BE0CF608AA08A759F236BC59357387006968C913E97856810DBF266E16F46DB5585D8DC6510A954D3C090B4283318633609A7B3625C54EFC975A127C625AD768F1C03D57584B7D2012E680CCB4EB7A1C822E4BC3182D0483A6D894AA77AC35795717674EB0FACAA0B9C172DB95D257058E7A31B7F6951A3ACB8AA531EE9516C097AE8981671E2381BB523388082D2B07CC9B576401A5C195C05FD1C0849D02869B2834772A1EE6776EE66A970CE2B9021193D13C2642A2A3271C093DC858424115B9C1628AFC31BCF7AE18783602813012750A4CCABB1E609C2A649E0E4C2891EB5497C1BEA0A62A0AE207018A2C0FAA8991D76CD14C47916853C326123A3C2997BC27A0B7073F97881EDA9CA18996779064AB768749869E66C4CA6AA58A8B155371B4729BE2327165B103393B23941FC6941A2BBC3C9D1489D6692E146B73A428241172C1EE63161C82A6666B7F5F757BEA328CE6AB45D0D87FBD007333EA927C3810A0A1790DDCC14A607D45A046E23C043CD96196659337FA940E67C565E70CD7D45511BA4C01631D43DCAD1C09833D64BE170A0681E0978D1B4F27E253BBBC5A4C2377299041333A1FA1FA0329B9B6FF24647E3B03954A85BDD637697CA834C95E1A992011D52D2719AD5FC410A780291CC575E82A2AC229BD8C6C42D4673E958ACBB6DC6EA644AAE5B10D5D90907E82614881182B127A53356A26A62C8463B26E5415C131BD72B16D2BA1175FEC0B6E59723FF7CF917968AFC91FE0698F40F55A9C730A1A493314991CD0329083B84292DB23D706755CEB4CF645A706E43512432848639AA757C61E727E8112A06B590488C4917C74041B92C1F34194A849211785C10FF71D27917D45A3B2A67966BA454A3B4829DCCC3CA8493612AB1EB554469E9950B4DB78E8D96E2352CC1AB2895FB16362143A25B3171D4C05A2AA5E5DB845220AB77EBA1166A5842436B226966988BCA3CAFC7997B0A261F97AA24C70BEC48CDDD491A1D2CFCC4455D06AA956348478580D1C70008C0B4AEA3CCFE9EA6C37A3B467652179B446F4D060B532AAB668554ABB88974BA6AF174861D52124EB9D05E60E62E11D67F34F1747951C2C6CE385BCEDF5476B882571579937F57C5C605351D12AEF9C6CD9B8BF35E95F7AC70D50C62B3E68899C39173101AC8EA326DF755BCEDA4B746BACEBE9CB8B233AFAC73075738BE4EB905D26438C5A68D8A14645462DE90783CCBB841431A452B632AD672494377CCE80974247951711A94CB907611C7E96E501EAA13BF225A041FCCFC0318CAC430C58CA043719668E59B4CAB34E2FC2ADE662C7DAD744CB5963ED6A9EF2449A000A6080C555DE2B3EA80C5AB7091DC18C4811697FF6E0A6D2D98489FB211B6B786ECB0ED793448351B80668A46ECB41FB8A642F10A0CC41A1F836303023C33B3B54BA6C2599268C433C6C0832052174739267CFD80C74C3225AC4D75CA8235108165E69F233C213B5AC547794E7CE59DC8D21D76B2CE0CECE43578FD0A1CD38200DF0C020D39563B519BC138B39D819A7A8AAF7D9C931EA75FA9B69369A7A6038B4D3937F7EFB14CE779CC1227B7732172AD10DB932531E426938D067144B6BF4F67C3DB0135573C219552719CB67ECFC07A489BB81F4A0773988E5AB3ADF7C75445868673B6D255B72AA73B32EFC015EA83010F96148A289B10101990290843CBA81E27AC58495F12CB05F5A8454E66F5B10CF0425BB6A1A8580C4C4110304C4717C84431447E510B837C8D70C8B589241592A107E6920FDF8627357C3C0D931F1F69EEEF1C3C518BFCF4305C98A86CE950FA40195F04B79C1471D0389792502C344DA14C884890898262E8292C3E5912A595D0E561CB3594FB4391BB5F4B6571969C86A8457633A5D8212393773D5D736380707BE35895948AE4AE8A672A16BE1E536549A194427C24080BDE083A63AB65240936F3FCA52DCE473EC08B765246AABD6C1F9549C0236676AA2AD234065C65672ADA92281696CD10A1AF1580A88E602ADBA75BC84B10A57072D1695BAE791E26114FCB408576516B7741D0CA9038D2C100884C417F2739930C50E408CC0A83023E6B13C236965802F62B00BFDD0032F98BBE7084AE0B5C6A26613470A8BE5B6C6BAC59B9B725C55C71E4E8A381BF88A0341943B942228FA6E3B3BB614887846F05461D632685486BC73A8E1794483125E58031DCE0A5C8E175C3B73AE08C01D2EEC8677C32F7A2B0D620ACA1D128E6B42095D8B9179369EA88B5697380B4C13552EEA86D9F8188A1C9093B794A9538EEC3A6D5F621A9D991DA6974E6A804D00A2864450C5AE59C4FFE2AEDC23AC522939D5D2204168CD8F8C59E87723DDD4809D2913F6825043EA6C7C9340A91914FBD13C65384B06B955AA221D302112C1F0802F71443FA9ACA95CB236E64886B4B0332BAF527C4AC3A9B4D8956D5FC2904911B944EC3F9FA07267342FFC4CB9CF15CEFE7325CD4C34B9A742B4214F50A50E58B9BD03C053806F0677A35438CF5EFCD8CC8CB55EEE0FF5BA0F84F958550BE099B0E692A35E0908A5FD21A36B521C0F1EEC54F6E1E7FB12B796D0E56BE6FE3BA6EDAAB49B08712318B27D229606D2AC70\"\n        },\n        {\n          \"tcId\": 55,\n          \"deferred\": false,\n          \"z\": \"5B78F8D30AADB59FA617EF807D5C23113A9908342F08E898E02991CA1D7B934D\",\n          \"d\": \"A00D1EE4147DD57B5E76C58A928DED0B720FB2FB6353780B380B5FBC76712E5C\",\n          \"ek\": \"B3C014D82BC4BFA186D2E2A4FCFB57E2205FE91195E1ACAA18A3699D1BA700770482C18160082946D85BE262136B7B1F5BBC00D4D20A1351C66966CE3C1A06EBF35A43036A5498424C73BA01C250C61399CE951AF176C96F317B0A23395164075A23A6D6A55916F5032310B5E28B1B970C3AB75B4058C86DD9A73ACE89B9F5A272A86B433AEB9AD755B166105AE0095645910E0AD6045C075B82596140213DCD231C03D0786C186C95342C01265B03A2A113374307B49062727C82F2CBA44862C7691703374AD76B772DA400C283A1AAE348EDA9582B9B42196216A8218305309C367648F58785B74A9E4DC068318BCE4CA87000DC86ECC52F2D465762A58BCCBA755E7A881AE5704C643278E784E646C48AFC3F240A728C4A6D2B6523EA3499C9344294293497D3A4A3E2373BD2CB48B0575DA052253693D1DB696DA0057B2243525AAB17770D61D09D0F3554D10AA7A56B6CB9A81F59EC73A1FB9A08D7BB08D90F70117A2988409B870AA2F82447E14DE7B3C16503CE645BA54EE5C6E71909642A3F76620AE0D479A6440AA5E457A946A884D0C741A2C7F27C9EA9E716EE1C01154120E777930E991C63E0A3134B030A8A0E220758C074484FC7019C5AAC212246F990C41A5B5E468888A5280A23057CA0B23947F332213A86910912C090421D4A008CC219C9C571AB09A9C59C248FE71A2C928D83E6C4DD5929D4D1B8F0CA98F4608F89EA1D59F063D827123026C7EC093A6360590BF54BA041C2C21473DD89314321B334680CE65B5737857D5FB557F1967075C9889266298F332A19C5888A3497F0580B8BE1AB4F7A300F6248B4D99431E4B9CF163E9B969FD7B664AC8248789C2866D05D01C579B491BC50C8099C30A0E6076800B00CB27929ECC8C9B7C44B32781070188B747C6EC1997017E8435035CAC06B2E74D54EC8B64E2A9B9BD6351604D3875C394C183B4B7D362866C96A78569A9E88C2D8767FE0CC0C55C506E5A50B1FB9CC18910D9F22648681CA8C66529EB3665E08BDDD725146C343BD7B9E51D37F57B602E012A1096793931097228A1536715923CA93D8C41816E04FA27A16F6EB50BDEC8A26D99FCC1901938B7475731DAF85C2058832315B93E9DA81B415B707F95EDC44BAD6AC3016E4A16E787E3FC59DF904AC385ACF28EBA6EE61188883CC72E9B298F02D2D49CEE59B6DED348038C7055202B47CBC0B40774BA78CAE78079B9E8A9392CBB57CE4400E9214C3784BCCF94151D510B182403D990C2F8C984B6AAA3AFC21F9F6711EF91EEB008482845FDF60BB5917720EC9C774943E04A7A5FAF161ED557701BC26717ACB2CC059E1308E2AA76D04919B30A70CC698402FA58D927C8A02773815A144128C5BD183A38C3182BCF19DB7A3158B9393C767926F5737B6B60EDDC01FE8930ADC2C4414146A9968311C0B367647911F4AAA4443AD0648C634828C62C0BA01C161162B5EEAA78CAA9A1425F55950EAAF71841250008FE1CA3B8F8421FB4438E65AAC914AAADCB45FE303CD9165CF867A7E9304CABF0B3E1DA63A5FCB56F03753D7074525425131D555B5E583B303B6616AB6EFB68CD5994E9296608700BD3239B03AA6A662FC354E2A49314B63D9E1801EA5127FE653E459B90E94C039D812DC031D536C400A2A70B860A9AED7BCC286182933C62F7C1E73B615F1525F6A165DFD4461AE0B856B298B96CB6756AA2255949255E6C95C12A969A698D37B6FC8B7B16BF1AAA1734DCFE1160D319B25236F591C7CFC8A08FBA48B42586F3371B0AB061C92F9C0E3E350DE5941C9F380D876480C2623F526111D676AB33ACA5D66185655A94E13418576B632DB8A0D3830738C44AF9C979514254ED8C82E34AE5DD6458E2039690B8492C353E2F0937887088B4786BAE016104648C0544482C562E0D2AADE390DC03765C7C1353634B83AB66AC7E1306378B5032B3148BB799E8B4907188EFA993FE72C73E3162F2DA43F0EC7C2F1C162EFFB5659A947A3AC6203B1C825125023926115605CA1909D912A0108C0115FD75D967BACF32B656EA64BB1808D1C1B43E8D7BE02EB6432D89B59F9A3FD109AEA36ACED2A137214BDD8686D977A68A6E4B6ED6C916A8594CFA3700AF608CB6A91AF150077D5A40EA4471AC19A7177B1E6A6BBD8A357F416BA8935AE73829A5B035B79AE8D108ED5D05A089242ECDBE94664DA47F187D3D493AC23E7\",\n          \"dk\": \"91200CC986B3575202DB535E04BC2E37A499076781ECFC2E5CB0259BE2A5618C1CED22128D194B0BF4397D6AB0E2F30F6AB23260BCCA10507CA7015C80D9409B4BC06DABC661D3A653208A505136926A554CE331E4B692105044DB060576C1C36A726C0E1379C463CFF2183166052813A6BC4B93365F2798825387209C8C53A1C2DE453887B20061E71245CA6C1C27809242C14215924FDB901EA035AAC45EB29B518D0477F5D8CCB7D39E16239EB0A5254F6380F328C1ACFBCDE08310804195B944AE8D98BC1D75325E062973D84C5AD958C14076720C4876A5B903E80DDFC7BD66E1B55356B896263FC3451E2A865212CA3B9C510FF0CA4865B6C1F2791C1A77A67336A429C9569AFABF535888D1075ED2B1A31CA78B9E3353020800FC18C1D47CA6C326529A375FDA667C06134986B891DCBA7F6AAC901768A156270A6E46490B33378EE75648BC4DCB1451A2212931F2096CD8500F23817975C25FDC164F41CB5D8A3613EC993A57711585BDE822B519683F8C9CB95ED701D6905F20A46E29C0C90C448B42742FD1BA05BFF590D2D97317C2473688A4B6AC63CC1CA28AD71FB7C5252C42534D27AF11D5947E1AB196CB2AFBCB41E0A8C50AE274D8CA1971B605520503E9C17AB6F41232FA9130BC951DC031A66A02A663756ADC7C15BC513ABC1592CCBCA9F5CD3AB29B2305B32D98C877217A1B92321930B34A096641070428B65FF4108A36C998E895375640B57788A72302931DB058ECB52F4D7A0623D74AC223C4ED6024515A364D280EE056760CB451280772AA0CADD1E0333AEAAB5357479D01696DE52AE2C8B71705706BF15C05064CA434280D4C8CAC411B0A390C6D6BBDF5E684BE3B850D3A83BC31C5C823604E533242E80DB57B2598CCAB30E53282F140C9897C85EA7BC9FC378761BDAB629E46336B714C0189E214EDB254512AAC558541D30CADEBB9665A1914A9D001F43504A0E0A566B311E746379F56558A760618C753A57B6B1A4251090795466A178A39904268A10F2451484C6B11C037953817AB156EF7383878B1BC38790CABEB4991EB2768A83F580945D46C49A8C0CA26BB99F72C40B73439253180DEE2A425C54C23FC6718A40611455BF4652F8CD91067B42131EC03CC5C0B59EBC5FF30B097D764306CB692ABB95D192CE44B1C0E981162F73997148DF270630AB6B37129813599239AFB46CDAA40CE472310EA67C2E0BA18765D34EC9FBFB82CBDB4BB6797B849BC778C9710BCD0BB2C611990B3232AAA4D1B898371738AAB9375813913DBD178A4CA2EEE4A8D09E72EDE6189272B1EF503AC8302A63396798FE5731C223E53E90EBA3805D1F8A65DF168C71408682C2790A167557A15E697A72CD2CB613B7371838EC85BBFB510B3344C3B61664BAA8B90079C30181ACBA83AB049808CE6421DB67393865C43D43C3825281492AA68413BA560B123E2140DC114060F93101CC08C63D848B172010C56C2F5377AB4B4A31CBCB265FA607F504147C0009EF812788A7EC53514460A8C8225A664348A5347291B73B423C8385CDCCD34A277913841F2C2434F2A34A2EB02B7270274E4AFB3F19933402E1A340D8A8B6F50EC4930086D462A85C9AA80950711C092BA7135A45CE30B37FBB21C574C6A1263D0554AADF36D9A6567716B914F41617047973D54C3F573805CD38170F004BD014989D33D939132A39B44D49345B857792B241B0A870396D95CCA133E7A7001579930DF70C201A71951E11257356129255A044678BDFCA5A7FC32001167155659F41B4022A9B15B37A197FC7C567CA63D74B4E9D286100861658139A98C868EBC9207237A5E4BC26EA867DA305A3C3A36083C9F79523B45F53B8FD40591304836B8520B227905413FBCAB24B22A7D1334452045AF354A4690455144390A381C1172C18D865C86B159AC944906F298A00571B61420BAA7E67787AC2256EC7217FB0C00C48960165CA6E4416059A4CAC2CA9535C8DD2265387B1E18950A7C917EC73AAD76A753E0D4A285B19089E264B6A53A35AC523FA955A4B48FCA59344503B12675B6C6C99593EC996DD9C456D842054A52DDE1106E89A30B5732FBF5C88BA98833A26FB47350D0509B17E8BA76878C2FB485B7CB5E7B4C73F6929FE09B54A09454A7145A4A812C26240EBF4126B3C014D82BC4BFA186D2E2A4FCFB57E2205FE91195E1ACAA18A3699D1BA700770482C18160082946D85BE262136B7B1F5BBC00D4D20A1351C66966CE3C1A06EBF35A43036A5498424C73BA01C250C61399CE951AF176C96F317B0A23395164075A23A6D6A55916F5032310B5E28B1B970C3AB75B4058C86DD9A73ACE89B9F5A272A86B433AEB9AD755B166105AE0095645910E0AD6045C075B82596140213DCD231C03D0786C186C95342C01265B03A2A113374307B49062727C82F2CBA44862C7691703374AD76B772DA400C283A1AAE348EDA9582B9B42196216A8218305309C367648F58785B74A9E4DC068318BCE4CA87000DC86ECC52F2D465762A58BCCBA755E7A881AE5704C643278E784E646C48AFC3F240A728C4A6D2B6523EA3499C9344294293497D3A4A3E2373BD2CB48B0575DA052253693D1DB696DA0057B2243525AAB17770D61D09D0F3554D10AA7A56B6CB9A81F59EC73A1FB9A08D7BB08D90F70117A2988409B870AA2F82447E14DE7B3C16503CE645BA54EE5C6E71909642A3F76620AE0D479A6440AA5E457A946A884D0C741A2C7F27C9EA9E716EE1C01154120E777930E991C63E0A3134B030A8A0E220758C074484FC7019C5AAC212246F990C41A5B5E468888A5280A23057CA0B23947F332213A86910912C090421D4A008CC219C9C571AB09A9C59C248FE71A2C928D83E6C4DD5929D4D1B8F0CA98F4608F89EA1D59F063D827123026C7EC093A6360590BF54BA041C2C21473DD89314321B334680CE65B5737857D5FB557F1967075C9889266298F332A19C5888A3497F0580B8BE1AB4F7A300F6248B4D99431E4B9CF163E9B969FD7B664AC8248789C2866D05D01C579B491BC50C8099C30A0E6076800B00CB27929ECC8C9B7C44B32781070188B747C6EC1997017E8435035CAC06B2E74D54EC8B64E2A9B9BD6351604D3875C394C183B4B7D362866C96A78569A9E88C2D8767FE0CC0C55C506E5A50B1FB9CC18910D9F22648681CA8C66529EB3665E08BDDD725146C343BD7B9E51D37F57B602E012A1096793931097228A1536715923CA93D8C41816E04FA27A16F6EB50BDEC8A26D99FCC1901938B7475731DAF85C2058832315B93E9DA81B415B707F95EDC44BAD6AC3016E4A16E787E3FC59DF904AC385ACF28EBA6EE61188883CC72E9B298F02D2D49CEE59B6DED348038C7055202B47CBC0B40774BA78CAE78079B9E8A9392CBB57CE4400E9214C3784BCCF94151D510B182403D990C2F8C984B6AAA3AFC21F9F6711EF91EEB008482845FDF60BB5917720EC9C774943E04A7A5FAF161ED557701BC26717ACB2CC059E1308E2AA76D04919B30A70CC698402FA58D927C8A02773815A144128C5BD183A38C3182BCF19DB7A3158B9393C767926F5737B6B60EDDC01FE8930ADC2C4414146A9968311C0B367647911F4AAA4443AD0648C634828C62C0BA01C161162B5EEAA78CAA9A1425F55950EAAF71841250008FE1CA3B8F8421FB4438E65AAC914AAADCB45FE303CD9165CF867A7E9304CABF0B3E1DA63A5FCB56F03753D7074525425131D555B5E583B303B6616AB6EFB68CD5994E9296608700BD3239B03AA6A662FC354E2A49314B63D9E1801EA5127FE653E459B90E94C039D812DC031D536C400A2A70B860A9AED7BCC286182933C62F7C1E73B615F1525F6A165DFD4461AE0B856B298B96CB6756AA2255949255E6C95C12A969A698D37B6FC8B7B16BF1AAA1734DCFE1160D319B25236F591C7CFC8A08FBA48B42586F3371B0AB061C92F9C0E3E350DE5941C9F380D876480C2623F526111D676AB33ACA5D66185655A94E13418576B632DB8A0D3830738C44AF9C979514254ED8C82E34AE5DD6458E2039690B8492C353E2F0937887088B4786BAE016104648C0544482C562E0D2AADE390DC03765C7C1353634B83AB66AC7E1306378B5032B3148BB799E8B4907188EFA993FE72C73E3162F2DA43F0EC7C2F1C162EFFB5659A947A3AC6203B1C825125023926115605CA1909D912A0108C0115FD75D967BACF32B656EA64BB1808D1C1B43E8D7BE02EB6432D89B59F9A3FD109AEA36ACED2A137214BDD8686D977A68A6E4B6ED6C916A8594CFA3700AF608CB6A91AF150077D5A40EA4471AC19A7177B1E6A6BBD8A357F416BA8935AE73829A5B035B79AE8D108ED5D05A089242ECDBE94664DA47F187D3D493AC23E7DE32CCA3941492845F6502143FBF02028F22B12F1ABADF29BD12458E5B698A875B78F8D30AADB59FA617EF807D5C23113A9908342F08E898E02991CA1D7B934D\"\n        },\n        {\n          \"tcId\": 56,\n          \"deferred\": false,\n          \"z\": \"384509DB0E97D4689A3CED953CFBFFA9D3B3B87CCB0C6A360FC0DF3CBCA399F9\",\n          \"d\": \"2C34B1476753095D0C8A48A00136F358A98D1416E5069CBA4540C6E26FA3634D\",\n          \"ek\": \"6A04C9598C985A554021C437246C8E1DB67F69B6092F7A18BA3309245480FEA533B478726E4C761568A00DECB368F104B1E908F1E9BE85022D6998581428881C6C1B06A89141831DCEC1A139303FBFD04174F06E81D09DA38251ABC33C32422917EA165FE821793BB13D553EADD9AE86C750A7C307AC6467F117B778ACA8E1CB8360152388EC20742011FA66C786675AA4813C8CD58EA2FC9ADBC4AECE94C1193B1799306D6DE532EB32928BD5957484B6E38272172B902AFCC4207958A433058280713FF055A9FB0FE6A3C79564A75C32AF13B4338CE1ADCC5825E9475C66201B8E6393D493001C29A44BB20326DB06983BCBA96805EF0573DAB1A506C33150A974EC96462473CA7AC7066F374A131854EF01A99A30770B0B508CDCB2B5D2B5F2208005378AD0935243EA61C8D876A1602CC047871425CF33BC22E07A94C71737BE251E75F85594F523B4CB241E97AC0306B8663075B8B18ECE09332991C774E225D7722DC1E87052E19293F8B802E7366A3C76D388351B63A11AF5703B4862699124E574B0117788DD89C8D7D2BC34806DCD183F28784E54B2725EDC9A797B1A9F110C0C91154BC927B06C77335A07E372B5C10C4DC8C5BD80EC79BB68AEAF8BAC1C583D4055C5EC4592462C60165C636F2A5FC3F9A0035B0094755AEEC7B076A7C84F8C3DA994C9C2C10C1E17CE56943933D733944753DF2546342890BAF305BA3B08628727BBF492F7123CA6DB9E067A02A7FC20B9138E5011A9E08116E77382C9AA594BE18470D0B9B3CA82FC652398A3B7466065732030F8622AFB4C29A88B1F8539976902AE18C9C0EF10780D8B405EBB37D873C6FE4534B8AC444D8A7817F6CE0C36B8C53AC03328B8D4B30DDA4111A7749B6476516B56147B3B9841E29D20520EAFC9247D9C6171A1165C971941A06CE6F58EC0A5B4FCF21CA78857BD6B2F0706BE3F568E380C24E537C31D74B0202014A24321AEE877B7E457FB669903A30D39AC9188239BA29157ACC82AE671C75887C0F1C9AC11DA5974E438EFA438467755EDB0C949536274DA3989972356A845811273E5308DDD4A3774047E7F347330133322521586635622937867DC549E058AC1D65BCAA8C6DED93C52FC329DD038DD244428EC602D0B11D5707AEA029328B61BFDE51B4EE3BA4CA8561031BD939ACBC70276A309D028A7674C736668D1735EF83B555689DD5790852916F450812F0A809C183A4BD2AA79B74FFEB232F3D67EBD0B3F97037ED8EA8A7C6CCAA4D300F3E01F2B8661AAC39FAFA78197A2482ED4745C56C757A692B1900D7DF921D6C35580F9882CFA294DFA0DBFC29BA21C9F648515EB2A81D2BB0F75941C1C1C6541D949A5454D96FB486C79B11BD9B56F45651B3567C75A261CB5C0B077352486C37715CA8D4CB727B167567BC58A530543047C1FA2459F87C837623C5B2939FC53AABCD5B1B8271A67C95B3E33C2D9922836A25BD1E61692BB36FF87229779AB75A883B26CB6B27553FB5782DC3C62D70C4AEFE86A3A36934770834142398187983D766BDE4916EB76852D74010E221460C4C5FEC132AA77B618AA0F81EC6A8E83BE7BE37239F1CEDD206EB8961F0D2AA3FFCB272A0785A36537240274C9993C4904A731995A3019CF6133C714185A449376565A65B5C2B1DEC8896C1BC46A1575CDF80958F2B96329791C208F51B4A8F41A8356000E0F1C51EC584ACE4434BF0035B5127478C11D659B842CAC49AE873F8192659A5735F87B7517B118748A74B8020D2823A25952CA3F43C7D3614741E054DC0B5E5AC11C8FE15E7F908A7B64B3016A39218134FB3092E7482E7399310709BE369CA383E95AD08718CFBA21F9DBCC8E0600DAD2C39B368A24E79DB0DB02395949CAE835AEE683C4A6AA6024B3109C18DC2BBAD65BBDAA52ABA6D9487EE3194579494B2441177019C0BCB78F8A2FCF7BA875548C1C3603AE3A874D2454C11074047310D136AB4EAA348F05B4662989CC6896DAAA24A94154CDBB30FB5023A37779B1E29F71A841830274B733ACAEA69D84FBAD4F138861FBC35F0545489AC8342A4F6F8C584A8442447062D35BAF44B3C92A38CE11A173FD5A3D8D6B24BB390ACE212E41042E75D50E318C6FB037AFCCB2B069E2BA66AC1233D53031279ABA1683AE42A745D0002805374165EAB94A17EF6881F0BA9392CEAC27250E82622E6F40D3810AE40CFF1F8496\",\n          \"dk\": \"BC00ADC7652EA7DCA970F2C234A87D9C3340EF2994A79A71B7200F9168CF76D9A301B85A01E6AE69D8959533A53F4890378067A6068C2C8201D373BA49C69FE6EB1716FB6BFAA4ABCC071B191515ECB758EE859D40A12429DA703052947F4097E7E40180477C01B62944CC2CB7E63952921E1EBBC800E1A7385B01E528548A31B0BBD0A5B0641E9160C36236CB58A36555D88AF118C35EDA2276B38D5A8C0C091795C1254D69AB5D4DD309E376BDD7E4A3E1333B3FFA378FDA8E62525201F9458F53443A255ED79B71102A2F3EC347166CAB38361B3E668BEE7588AEEACB5436B1F8E360F1D73999E290BC25C036ABAD878890C54BC8DBB863F8E23E5323ABDB004141C9C3C0963B7FFB7EDBCB545114A61A6B58F8487A07F388F4F5C00E9CABD425B7BEAA2B9947A0482A21572B48E65C8FCDBA214C0204E246CC3EAA069DD42DAF7285ABB55DAB8B2A016805A1C17D46C131ABD4483BAC6B5CB8C36A810DDD100E4A66A58209CA60877E40E2BF58E37BE97C8122D7097ED173FB272AADEA18EF91120405435365BF670C17B99068226597DDD7C4A6A16F844616C885666AB482B7858FA31405B86789CDA046D6561E997689C0D0A461E776C7313E811AC02D1BA8A95038B12430748644D0B933C9E35F685229D97BC810B3203C874673D18E8E0131FBD368C47466A9882F4A41473166C2E240A82D36B983273208EC4008231773D57FC9D4330703565CA8759E7CA9E64130BC5493C9F52C93AB4A7B3754B0F55EE9779CD08B2BFAD71899B75CD274ACA7C2BA9575AE56DCADFF13A6E6C5266BB7AD0D8BB820660023E9BE0FF759202CCEDFD7C3F2048D6B345CAA1C82D479A4D4E2C48A1818DA022435BC3D64B38C811B0BCFC147FAA505AF13740B78945F973BFD2A21A6A040B65214F934154E722D7F195877C5476EE615344462C2A95744956DCB5B2657F27DC2C6AC14E92E8404082D8A0EB6E488F2E56FD0292FDE931057D534051700AD0CA85ACBB5C6C02077DA856931899BE82CFA0A794AD1182CD3AB5E5188C357C3A2D9BD130A62223293DD249E477271C9E14AC48017D013A1435A5976C4165E8932CA032493B33814F90830A5AD75E32F747B94B4C52BF5EB30F73C02BCEC5903BA01F99ACF8B9ABB671B8D219C029BA1A2A6E847E86091C08C00B3914DF8D075C3913C805104A3B4BBC798B9F1D20AFB8118B7F26C32633B39FC792B3358606054BE3693A6E7B9C6B6B3A848A2D0B2ACD1C406706CB590B4B79B82BB88415CFF45376B8102B644193FA1890040127B58BB351862F3227AF18A50A9A983387A8646C99860BA2373337FDDF25CF8916E00252AD07516D1BC5629245EB341B1ED4B63F93044B131C01E301E8FBA5DE8AABA76F70F7D2A3ED4F78AD5210B47D8756E334D32CAAD88C22DBC537FB766447B09A05F1685C24A80F8C3A3202790B277A6CDC836767793D81955BC90A3DB3B71BD8C2FF01A12A59541A666202CA98689528DF9179A26156A6A414BB8DB350393B6B0F35DC163CBA4C94242A33C9778BB35563A8D213A9914225A0C6876B75035201DD14048DE28AF97D77260476DC399B37DBB48F3C943F3C990ABA7068D4299F7D289C6D66D4243570AF38D8FE95384B27E6B0C01258262CD0465F5FB5F424CBC9C39CF5948804F7034C632CB8A7B4ABC3095B174951C42364B05396E140524B989A889CDCFCA4F222830A98B041338A0273478E3661A5D44214A52A54EC65E933011EB7148EAB94E58EC1C9CB9A0E61C788880B15413B8365A929342B667005D6A35BEC272955F5CBB633798094A0DD84A09B6A04B16BA9D1C005D6B1708CD4300D26B506748CB7B4CCFB59818FFFCAC2A141ABCC1AB85BBA36C69608CCA475BDB61A56BA2FD6B2F75F502FCBA8AE05B11B73A381A810681EB0B1F446007267F7C12526BC3B430CB4BBED0694C290B0EB238056B5C64C1AA072C97F7173647BCBEB75965BD71C0747503929ACA0FD98EFE45C744A4B5BCC748616A8B24C5BDA4F0B9FCD42767420971A00F32415976B5A4DE31C03B10858CF1995B7B8526FC352A0C99A57095EEE0B8E7E40364587623B00B1799C807289DCFAB746B06C3E2912A6D3778FFFA8C1B43A39A433708817E7058C03698268E9275BCF21FB73052874307FE56CC8A384E6A04C9598C985A554021C437246C8E1DB67F69B6092F7A18BA3309245480FEA533B478726E4C761568A00DECB368F104B1E908F1E9BE85022D6998581428881C6C1B06A89141831DCEC1A139303FBFD04174F06E81D09DA38251ABC33C32422917EA165FE821793BB13D553EADD9AE86C750A7C307AC6467F117B778ACA8E1CB8360152388EC20742011FA66C786675AA4813C8CD58EA2FC9ADBC4AECE94C1193B1799306D6DE532EB32928BD5957484B6E38272172B902AFCC4207958A433058280713FF055A9FB0FE6A3C79564A75C32AF13B4338CE1ADCC5825E9475C66201B8E6393D493001C29A44BB20326DB06983BCBA96805EF0573DAB1A506C33150A974EC96462473CA7AC7066F374A131854EF01A99A30770B0B508CDCB2B5D2B5F2208005378AD0935243EA61C8D876A1602CC047871425CF33BC22E07A94C71737BE251E75F85594F523B4CB241E97AC0306B8663075B8B18ECE09332991C774E225D7722DC1E87052E19293F8B802E7366A3C76D388351B63A11AF5703B4862699124E574B0117788DD89C8D7D2BC34806DCD183F28784E54B2725EDC9A797B1A9F110C0C91154BC927B06C77335A07E372B5C10C4DC8C5BD80EC79BB68AEAF8BAC1C583D4055C5EC4592462C60165C636F2A5FC3F9A0035B0094755AEEC7B076A7C84F8C3DA994C9C2C10C1E17CE56943933D733944753DF2546342890BAF305BA3B08628727BBF492F7123CA6DB9E067A02A7FC20B9138E5011A9E08116E77382C9AA594BE18470D0B9B3CA82FC652398A3B7466065732030F8622AFB4C29A88B1F8539976902AE18C9C0EF10780D8B405EBB37D873C6FE4534B8AC444D8A7817F6CE0C36B8C53AC03328B8D4B30DDA4111A7749B6476516B56147B3B9841E29D20520EAFC9247D9C6171A1165C971941A06CE6F58EC0A5B4FCF21CA78857BD6B2F0706BE3F568E380C24E537C31D74B0202014A24321AEE877B7E457FB669903A30D39AC9188239BA29157ACC82AE671C75887C0F1C9AC11DA5974E438EFA438467755EDB0C949536274DA3989972356A845811273E5308DDD4A3774047E7F347330133322521586635622937867DC549E058AC1D65BCAA8C6DED93C52FC329DD038DD244428EC602D0B11D5707AEA029328B61BFDE51B4EE3BA4CA8561031BD939ACBC70276A309D028A7674C736668D1735EF83B555689DD5790852916F450812F0A809C183A4BD2AA79B74FFEB232F3D67EBD0B3F97037ED8EA8A7C6CCAA4D300F3E01F2B8661AAC39FAFA78197A2482ED4745C56C757A692B1900D7DF921D6C35580F9882CFA294DFA0DBFC29BA21C9F648515EB2A81D2BB0F75941C1C1C6541D949A5454D96FB486C79B11BD9B56F45651B3567C75A261CB5C0B077352486C37715CA8D4CB727B167567BC58A530543047C1FA2459F87C837623C5B2939FC53AABCD5B1B8271A67C95B3E33C2D9922836A25BD1E61692BB36FF87229779AB75A883B26CB6B27553FB5782DC3C62D70C4AEFE86A3A36934770834142398187983D766BDE4916EB76852D74010E221460C4C5FEC132AA77B618AA0F81EC6A8E83BE7BE37239F1CEDD206EB8961F0D2AA3FFCB272A0785A36537240274C9993C4904A731995A3019CF6133C714185A449376565A65B5C2B1DEC8896C1BC46A1575CDF80958F2B96329791C208F51B4A8F41A8356000E0F1C51EC584ACE4434BF0035B5127478C11D659B842CAC49AE873F8192659A5735F87B7517B118748A74B8020D2823A25952CA3F43C7D3614741E054DC0B5E5AC11C8FE15E7F908A7B64B3016A39218134FB3092E7482E7399310709BE369CA383E95AD08718CFBA21F9DBCC8E0600DAD2C39B368A24E79DB0DB02395949CAE835AEE683C4A6AA6024B3109C18DC2BBAD65BBDAA52ABA6D9487EE3194579494B2441177019C0BCB78F8A2FCF7BA875548C1C3603AE3A874D2454C11074047310D136AB4EAA348F05B4662989CC6896DAAA24A94154CDBB30FB5023A37779B1E29F71A841830274B733ACAEA69D84FBAD4F138861FBC35F0545489AC8342A4F6F8C584A8442447062D35BAF44B3C92A38CE11A173FD5A3D8D6B24BB390ACE212E41042E75D50E318C6FB037AFCCB2B069E2BA66AC1233D53031279ABA1683AE42A745D0002805374165EAB94A17EF6881F0BA9392CEAC27250E82622E6F40D3810AE40CFF1F84963DA07CBAFFA3C26C86115A24F33F1FAF547933AD64AFA40EF5F0DB03D53B340E384509DB0E97D4689A3CED953CFBFFA9D3B3B87CCB0C6A360FC0DF3CBCA399F9\"\n        },\n        {\n          \"tcId\": 57,\n          \"deferred\": false,\n          \"z\": \"63DAD9B127F98E72A3C65ACF4B172FDBD9B9C39F24F728D1F40EB02C9949419D\",\n          \"d\": \"F742E7B69E27A57A43E1034CEB5834CAD57C380ABE259F432F96FAAF27F981A9\",\n          \"ek\": \"0C25AFFB80C5BC12C3CA5357097716E75B025C298F040C5AB8C92F93A08A91953277A7CAAA1776440C9E3A5C819D7B9BD58AC0A53C83D9635D3A882C36CC3E48959D01E67A4E363273F05504274BC144C5D4B4774FC98638895EF894A78113362EC428E0314FD5119C40811D22313D42362176E001616901952C1100C59E33428EDF13C68086571E8A65D042556A6A3B18582F79B443EE22B53631C02FC03FA6CA4BF4BB0B8B97410443C0A3CC958AD42BEDB3118254C19752AA70076FF8F312E50433884A72F287266BB83A417134CED7C66EEC3A9C90043044B61C795CA69B8EFEEC56D808610EF5B6FF764A59D308E60B64B577CA70487D340BA3C8027C45766D270969B284B54DB8A29831939174A2FF274F7EC131858355C23624CBE7B6967288C0B660ABAB4C11C2106F98909AA7AAF50BB1CA0221E1CC00B31696DD05C3E46C7CF5A22B28E404B171A8BAB14146B740B946148591C0505CBDA8E2456809D019E6183D4B10E5F9C6122A7E17D579C5AC4FFC62CDC9AA88102CC386E50770E36A257BCCAB1826BC07D0CC58AF9C49291DB9A63753CE3F9C2AE190834AA77662BB5072F89FA29A2388E532361537BC28310A6A4133B5A0A4795304117839832C8AB7A397141B8A3C47A52689BCC26E3851BE6524CF2FACCD8037B69279021539BD084B31FFF084ED7AC403EB8BD3EB5F409B8778783697B5512C0B91B426714C426882F10DD165C91EFC240C2B129500595D08A5AF198FD8C6A1A9D497654A80EAD40E8B3347B7C51F2E220C1B950788139DD07B6F00B1789EB0A7AEA60844339F04DA7811824F9544320064725B809862447397786381615D3EF4AF575A4CB8FCCE137A4C85241A72BBC424343A3C916C6E51BA58768855C29EB17AB4C8C8C3F3A0174F5979AC02795B60716FE96BFCE517821C254F2A8DD8D77FE778B82E3A53C8592048104FC959B23FE36450913BB1265BF91978FBDACFB3601DDB978CDD77314C8102DAA75D1EB8266137CBC2956714294659809E029067D7D6CB6C93148FB102C34450118835C85509E3F980446767FEA734AAF7C8ADB7CB11435E7600842FF96ED8C75FEBD643BC1A34D1666D8B6B9EDB2B8C72AC5614C603AC200D00461EBBA069EC751CB3C36A60616C73CBB8B2C17798358CC4C588BA3122FAD04D97D51687FA3B19D465F6E9AF95C68AE5225FE04A14BDD61B54A502F820C9DB0256B11AA524F463C11A21F1610DBD800A3D690E0139000B026B7C1628FCA3365A358FF8AA01429CAB2106055144CF0597C2DAA1A623DB1A85730AA0046127287BC407073E9C7C74042DDAAB008CEA58B8DC7765C85844B3395561A4772B5D4D551ADF12687B18C63260518576440EC7BD699843F5651F5E04B25085448CD138257C2E2612733ADBA6FE7357F1C366FBB1457C2458C4F848A24A1BAD74B9A2AB1A603408C540C091C6A100850B4A327211A7CE336426E7B250D50012D7262EDA4218C3B1A55D50337C63A9790406A846CEC038B15D157DDD030FDB293B187A63BB38208F075599803E29149B1F097B1867BF65EC69ADB00ACEB21345E1ADE7E5A36E20B69062C5CCA550C95331F069CFEC59429531B4380A079669C777764DCA8ACD8087AC091CC2B6075CB9436A3B4C22AFACBC0CFB95D361C2232390515A21C37AAAA839005AB97D6962BFDC8959A51ABDD427A15BE830319CCEADF3BB23B59B63452F6EE2956D11C47E721FFAD1724CFA7C1ACAC3455C6E0A75834A23AAB786079947ACA32113A2223B2446790738B464A94683D47357E8847063539080987BC742BD149756541F94D5539E886C13FC1FB291BD1FD8445DC47347072D063A71F9224A85366D3364A1B97ACA267C717BECBDCBA78FAE3BB589BC8794FA2D20A8CD8D7B58124843B7E46FF0F629EF92105DC43DF6645ED57BBDA265874B63A193D351BBD0A5A48CB819979C2A57A486A109957487B51AB7D8283076585E6F06AF66A7B7776BB219911391B67380215FA2216FC061C441B87AF6AA077E77777AB3C7101911B3DC5C1D59952879C1DE1B8741A24C4CD28239132E89024C357C76BCA7CD4696423EB6C578C53C32098E8CCC41321C93CDF79A0DF21B6F69C3DE80C9EBD838B7FB0759165DFD7A6200B88588E992CC173E0A6B0B87554CD2C3C4D42B6B575C9180F1A418D8E400CFF37C23D4C3E3EC2627627F6BCDD1E1F45D7E\",\n          \"dk\": \"BE49618EF5BCFD880DC312BFFDC519EFB4269D0B2391B89C01D6A6B05063346A4BBAFACFC8C091B64CC1AAA08D6F40BCB4043BAD7416858BB4121567BC380C042C2D90244943028DFACABBC6219060721832241591BC030410064E45149CF96C36A6A1FAEC889382340A5BBFC9193DBAF8A43CBA44BE25143AE10A33C6AF8541AC2503835E0743BD74CB899373BE77987492A071E57027C688A2123F56A5B3D268C1EB70A21F6A98BD239F9EE5927F21254FA5BF11F41578AA84770C78B9642E2F3C94EAB1CA5D10651F90A1E49033074B78B6F2C9D0D784C7A931E2EB8336F8802EAC29DE8737EF897FD0A31A04F76B30B5C6B68B7C3A2C3CF0C5B4B313A83F70A59A84901C8B819F8502F624368C493231C19340D67EDDE503ED9805A05A3E23102E6256117D9BA283619F0CB3373E743B2771A7D1B020949600098A40BBBBC364F6AE171BC6FBFBBFACF25B0BD37102D336EE1438022AAA2DEC0ABDA6689A4595A18161C7A9728A0C9B4F74A65D8A432A100B1F46263BF525024876B334CD8D9A1BAC2599F1F0AAADF04E7C83C7EFA4663CE59873533A0CB84AB8EA28BC9B808CE71832D45E4ACA223D7CAD92699B2DF37E080B6F3B6861C95A210362C5FB849EFAB8A845C25C375078247C397143C839F45569786E3EAB14A05679098B25785ACBA2D49C4FC47796271949795925A80EC53A8F7DF6A2A6488EB4E991CD7976584A2FE301CDAB93A57070148F8AAA81141BD932951EEA748D6066FE3B9D60143CAA0339CB8AC2530A0AD0EB444ACBBDA16B0E6A0A6E54885FAB6170A7723F4E3B23D18615B6AC580B2401E485C3543519BE6A5590C7469FF47A3DB91338A5AF6FD28A91720402F70DB8A491946192AB573A6E668CDC7B702231986C114EE18902C78A3D92D1CE616511A66999CF5C0071E615E8A7857D561B0B56C3C35560DDA90A18601B8650AE2520784D1257B3A7339C09B5CE95138E57B69FE90C1F97C03280C0806BC366E75E0D3CA2D355A2E2BA661EF0B96F556C50656724567745C1B650634A8DFA4699222010C59257C38C7D202328767663127DF2F294B00390F674C021B1934F65514AB125132B23A0A427919946D1A96A84491C353405F505937FFB8FC0813700E37C8151386E248C0BDABF1961B2859BCCD9B33960E424A87042D45A6AF180B2CF77B25B7912001C958EF55306927E8DE71488C0BB2C7C8B71767403B6185B41C22DE1BCA99AA86297A2A1E17015F553B967720B159160843DE88B3D033B4D5D563326547A404B352466C9A82415B08A54456B37037971B77A937A540A2DE05B91EA5E540996C8D15452467D0F81568DB2482019B783309F77867175A4CED0A0660FF2AFC1F09B09A03A7BDC1621A613AA4C2571B23FD7C1CECD5B75559C37CDB598AC108255479667E117F51B0E30866F20B087D9088765E7613F3584CCDA8EA7A13CF8E5583EA353661AB7A42A264F9AC6EC529C2CB7230CF45D2AC1B95D729F00B8792983AFECD44B3E88A197D34E2447490729BDAE7547B7119A970B0C3153B379148C4BB61472662669E2510C76549102187D891E56C20A3328C03AFB32760BBF09F604AB0B5C5DCABC83E80CB17A7BFE4B147F6670128A80E93B359191424C80C47CE001D926C6830962083B8BF1C03FF9F1A1034139D974229A581035E73F7CC10D56B281FE96755F214738B70AD6351F8F30CE5DC5555F6B69B806C5D2574553E7C94362A623F2C4B31502FF8094F6E08394C880EDA1299B2C03F1C696D80C3640584C13789E32192A27B38896884F6DF0BDE386A1A327C5C52314A4BACA2D7928AC761AD898CB4298905AA76ABE3C5B7BE75704317D44A9C98FE59C812C88A6E334C0163EDE8AB622F351D9F3926375291B5510C6E3AF2641A65FBA17C7778B9B2A6C2E5BBA6BA225EA40CF4ABA7639D488ADC0A970F6581213352A409065CA92AA8694916C57796B42C96C830FA84C6C70C2640C495F10A48D1A210BDB8BD0C20EF2074825651C5D9B3EC606351B34796FC578307CCCE54A2EBD648FF051CA6FC9AB0636CB6ACC49109343F1465052B57404D46EFDF856CF330E539402DF699E0576953C497A9D662E768ACDB8A9822E3A9271C401A364B1D060924B84939F7110B45012EA36121A4A91CD860258D559ADC0530C25AFFB80C5BC12C3CA5357097716E75B025C298F040C5AB8C92F93A08A91953277A7CAAA1776440C9E3A5C819D7B9BD58AC0A53C83D9635D3A882C36CC3E48959D01E67A4E363273F05504274BC144C5D4B4774FC98638895EF894A78113362EC428E0314FD5119C40811D22313D42362176E001616901952C1100C59E33428EDF13C68086571E8A65D042556A6A3B18582F79B443EE22B53631C02FC03FA6CA4BF4BB0B8B97410443C0A3CC958AD42BEDB3118254C19752AA70076FF8F312E50433884A72F287266BB83A417134CED7C66EEC3A9C90043044B61C795CA69B8EFEEC56D808610EF5B6FF764A59D308E60B64B577CA70487D340BA3C8027C45766D270969B284B54DB8A29831939174A2FF274F7EC131858355C23624CBE7B6967288C0B660ABAB4C11C2106F98909AA7AAF50BB1CA0221E1CC00B31696DD05C3E46C7CF5A22B28E404B171A8BAB14146B740B946148591C0505CBDA8E2456809D019E6183D4B10E5F9C6122A7E17D579C5AC4FFC62CDC9AA88102CC386E50770E36A257BCCAB1826BC07D0CC58AF9C49291DB9A63753CE3F9C2AE190834AA77662BB5072F89FA29A2388E532361537BC28310A6A4133B5A0A4795304117839832C8AB7A397141B8A3C47A52689BCC26E3851BE6524CF2FACCD8037B69279021539BD084B31FFF084ED7AC403EB8BD3EB5F409B8778783697B5512C0B91B426714C426882F10DD165C91EFC240C2B129500595D08A5AF198FD8C6A1A9D497654A80EAD40E8B3347B7C51F2E220C1B950788139DD07B6F00B1789EB0A7AEA60844339F04DA7811824F9544320064725B809862447397786381615D3EF4AF575A4CB8FCCE137A4C85241A72BBC424343A3C916C6E51BA58768855C29EB17AB4C8C8C3F3A0174F5979AC02795B60716FE96BFCE517821C254F2A8DD8D77FE778B82E3A53C8592048104FC959B23FE36450913BB1265BF91978FBDACFB3601DDB978CDD77314C8102DAA75D1EB8266137CBC2956714294659809E029067D7D6CB6C93148FB102C34450118835C85509E3F980446767FEA734AAF7C8ADB7CB11435E7600842FF96ED8C75FEBD643BC1A34D1666D8B6B9EDB2B8C72AC5614C603AC200D00461EBBA069EC751CB3C36A60616C73CBB8B2C17798358CC4C588BA3122FAD04D97D51687FA3B19D465F6E9AF95C68AE5225FE04A14BDD61B54A502F820C9DB0256B11AA524F463C11A21F1610DBD800A3D690E0139000B026B7C1628FCA3365A358FF8AA01429CAB2106055144CF0597C2DAA1A623DB1A85730AA0046127287BC407073E9C7C74042DDAAB008CEA58B8DC7765C85844B3395561A4772B5D4D551ADF12687B18C63260518576440EC7BD699843F5651F5E04B25085448CD138257C2E2612733ADBA6FE7357F1C366FBB1457C2458C4F848A24A1BAD74B9A2AB1A603408C540C091C6A100850B4A327211A7CE336426E7B250D50012D7262EDA4218C3B1A55D50337C63A9790406A846CEC038B15D157DDD030FDB293B187A63BB38208F075599803E29149B1F097B1867BF65EC69ADB00ACEB21345E1ADE7E5A36E20B69062C5CCA550C95331F069CFEC59429531B4380A079669C777764DCA8ACD8087AC091CC2B6075CB9436A3B4C22AFACBC0CFB95D361C2232390515A21C37AAAA839005AB97D6962BFDC8959A51ABDD427A15BE830319CCEADF3BB23B59B63452F6EE2956D11C47E721FFAD1724CFA7C1ACAC3455C6E0A75834A23AAB786079947ACA32113A2223B2446790738B464A94683D47357E8847063539080987BC742BD149756541F94D5539E886C13FC1FB291BD1FD8445DC47347072D063A71F9224A85366D3364A1B97ACA267C717BECBDCBA78FAE3BB589BC8794FA2D20A8CD8D7B58124843B7E46FF0F629EF92105DC43DF6645ED57BBDA265874B63A193D351BBD0A5A48CB819979C2A57A486A109957487B51AB7D8283076585E6F06AF66A7B7776BB219911391B67380215FA2216FC061C441B87AF6AA077E77777AB3C7101911B3DC5C1D59952879C1DE1B8741A24C4CD28239132E89024C357C76BCA7CD4696423EB6C578C53C32098E8CCC41321C93CDF79A0DF21B6F69C3DE80C9EBD838B7FB0759165DFD7A6200B88588E992CC173E0A6B0B87554CD2C3C4D42B6B575C9180F1A418D8E400CFF37C23D4C3E3EC2627627F6BCDD1E1F45D7EB647A2888D86D41D8661A91766BA969E80B9741B21D1EC6E349B52DE8191901B63DAD9B127F98E72A3C65ACF4B172FDBD9B9C39F24F728D1F40EB02C9949419D\"\n        },\n        {\n          \"tcId\": 58,\n          \"deferred\": false,\n          \"z\": \"0A755A829F05597B2F2A90974F22FB1AEAB42892101222967E3A0AD612CEEBCA\",\n          \"d\": \"3BFC9A057D979EC03A705A9CC406DD8A46C106941AF6777B1D7F79C1508D7B24\",\n          \"ek\": \"5BDC0216639954EB53EF77250D2853878BC0183C0EB5359414CAC928E3ADE2E78213904D7DA453E96969A89259822C205FA46CA6703B6D324890E59F0C377C778ABB5ED2466AC929E8AA2E9D47A8CCE606F52B18A7C35C20F4C2AFC99AC327802B856F13BC1D9E3646BF2A2038C454095C147BD1474663274DD50041C9939EECCE460B7FF1605743D4243553A0752465E93C628324BB2D3783EB1B63AE85AA53311C5574071E56991211426DAC6F6AAC3EEA1569B04654D4193DD689A32432447E1A3F0F5595FB37A35FECAE5C55CA839A3CED94AE6FA436A7D34C2A81587CA84018933FDF39A931B03D81491624651A5F6AC75E08077FAAAE8AB023D6344557D15FFCB91C10B06BDA114C5C133F3782C309C347F2A4434DAA89F202817A227C1E1240FF41A4F3CCACFA497CDDC53AC5B0C55614907865118C2C7AC0D5031CBC106CB17A26B97755271C86E871BCC9590E1C2684F2C24A172B10844CA44346FB969CEBA1A02B9922F3AB68457CBB1F2BC6B5F5CC4EACB9D0072EAC617B31D27301E002E978925966B642B00AC53C912EA85DB59C5506F4724B2C356CEB40BD58AE51843537C537EC97B24475318A5B30AE750691765FEE1014644B1CACC77C9C334805A58DE18121FEAC6BDB61184D2C273AB725C32091CA3441E5140433435AEB0B51B9C57066C43F308A12F782BCA5237D7B12119CDC9AE2E7C65EBC66B3F02A0BE7361E08838114BF0D5250304A36935135C61597A03903EF215C6A223DF8A9515E622932F7A29CFA74807B145A20ADAA7987B0B981B7A62D115927504A090F61B37592A5B4E51B977821E4D92228531D2BA1523BAB3D6C850FB0660646892BE2A006DF51743FA383AC47572F651AB02A272A458B81441C8E774547E1773EE29A6A27BA4E839C8E023A13635922009B1111801872C391641F0E239ACA70CE3FC01E5C3736FEE2AB3F85C4AFE3A0BB2529D6EC047337819084222F0ABE8F859925708AE3E62815083196248720EC28174C01D716AD472A0CB2747EDB932F4A58ACE9E2BADFE468BA2A138EAB864EE00C034C1207F92F73AB084E32BC244951477549B2189568BA28553937E5F1CE6ED1B7F0C1012560296442AA6DA0879705A65D77460A86C0C06422A2E6949BB78B955355EEA2749BA0948ED094E5D9CE7C79B9F4047BF4E2128E41CA5AB1BAB1A87A2565CDC018890D426B37F01713DC923CD372605763C3B51A5AD58109F031BD665192F38EED496EAB15AF89DB622DB73A88BB6104D44C49577483F468658A9C72C1A2046252DF7840325978589A85F07C7572E44445F9A8854825E784706A25A70ED0CEC3F5A372C41C54185294975A70AB759B4250E1F2CB35E860ECC25932F55E6991041AC24D56BCCBDA579E421C905469052A34225AD50B69E191B4A36EB7DB1DC1216BB41524497B279230053E910EAB49859D1A99DB3C56CD70995BF031FB71B0AA40CDF031183E5B3D8D8799CFC3ACDDA54010AA6169A80D62479583111FEB337354F6BDF36A33279B7472800EB604710AB94A0D584942E41B78F2C387993A5EA079F3C74207F7972646CFD4E055FB6B10BC69B369D166143B8A8E7400134999F05C6A26DBBEC3B23D92CB90F77156A0C8A96CE93F73962C17F8104B52AFA2B68B62810F998577E07CB16FAC499F38AD628B57FC1C9CC7A568410478176BBC14C8C7A594A07A3568B503BFE26BAD88D2CAE92B90F1E1472E9697F930B6E775A1A9B93EF62757B2A28F6962CF2C1478626A9B65B5AB89CC24EE4C9F765B8071737C25F383DD57598D2289DEE439D28AAE91A3B21E389267FB3D146276B7810D126AA5D096A16F679E28D5C91A580777D9CEA13C46234653634ABD964C720A11A6514A7FB7F5ACB0992BFC7C16FA84CD9686ACDFF2283A89625BC65FEADC9171ECA359BA50BEE412D31414EE110AD1D8748237941683BF292618F740445C0B728D51347928559E569EBFE896DC6295F87517AB31A8639B50C0F3A08E96926816B7B17C8ACB476C4C67B1DF7631DF7540276C99053B102738BFA2876E42A028869C688C18AAA9B00DF0F3A74CD81D86D161912C09362AB35BD8731A48CB45CB92EE8A3374B811EEF93A37F17DD7D8AFF88958E13012E6F7BD6DC63E27BC47F506048CE24BB7411538EB4B969656DCE8DF5ACF28D1BDF5ECD14A44A98350CA45699F033EFA44D25E93FC2094C49E\",\n          \"dk\": \"18A2405BD102699A2B22168A31D2C0E7F946160428EE787E8C6C57ECBCA0FD170DF93BC66203A8F06A3292310EE08495D1BC0DAF2A2D92C243DAC2B558291276217430A08D11B9C5DA411F2D55406BD4182913086489B43D74C602780701295A4C0054765338695231E6798020E7CBF1697573474AB35B27817BB38BC2313E70AFB914B5ABCBB28B7124302B338B471FC89336D1C35FB6D59C007402D1D19A1F96682F7551D8527EAA07389E55A8080791178407301ACF22EB32E0F7119DFA0E623035364CB0243B19918792011023A086016CE39BDAF0868FB08A4E1753B00B444A807787EB973CE52F63650B12A6B51EF435048A9F7136A5A77A99238763F433C00813C5CDF8CAB21B445630AE2D733F7683701242751F5C17A4F15FAFA5AC7E5A231097AD06299DD6AC194D1ABE895A0315E834BB364C286C8CD0216BB032B59D797245F40D624872036BA369355AF73C6FA5540BEC829112F28294191096A8AF53D37C5DA8B151330098582AA06C5455408765D43FE5B24C9C0471C9C85243C205747A43ECE8CEB31CA87BB51B39D15433F99D6FA638BF02373D42A09079399B4CB0A371984061370078A677E166D3D7B82E3174B58885D280A1D44911EB0482C58C7216D5A22694C767B9CB35BB0A1B0271471C558546037BE5585D43C40F6705D8C0C4579025898A2E21A0C5B0A4A42AB290E1F37D58486631210EFA0669819A90742926CEC68B1801B44D597E2CC84661917C9BBB6E2FD4B00D11AC616A245088B6AE323332FA40CFC46845842B02328D6E9A9246632DB8FC960B9340DE367A25D3996F48943ACA2656250D823BC999250C357C718A7CAC1197868D68C70F324391A75EB88C09AE835FD732B9047AC4DE843A6D00BA1EF79AEBC350B1D3541B874C3105B901441BE9F9663C306E085B0B0F98A4132586111417346B85AB5C26FB0522AFCCCA2168308803243BD1C1611574533466A1F709027485D680BE09624B03944EA5399E62A3A8A005B74227041E6C394CA22DBE3A6666541377573AC5357106400B35473B6672A7D4517F1C91A6A1266D25809B57720207E4CAFD88751CCA82AB54593CA126A0C60771CBC49535CC9A83882DA67D6F94BCCF7A5B605215B874C4786B24995A83C679BC12E870C61A3820DA5B89C0863B369A6C79B13ED67099EA02CCA1B9523529992A215C525BD5D37A39E69891FABAB70594AF8119F356A85587604AF35CC3A01692047576D97456BC1BA23639D4452A91C40A732509C70C83CFB080CE2BA6F29A80B0066D10A96A038C72CA7B6860594EE3C7C592016E147C7FE478B90F8A4CBEC166A3C474758642EB76CA3B0A806B16CDA19013986B10A3844A0CF86455F2808429B371A0592129414FD31BA24C995E303C776B014BD35CC644095B7140F2AB3336D55DE6D54F9FF244B6CC8580111C7DB2492A32A073E0196FA256A92CAF6B431B6F6053E52C308D6C0F8D86001258B78290AAAA85C9B2059A931833A6C82825B332943ABABF65451272AFF4BBC0FC279B603CC1E97C18F40641DF91776EDCB7DE245DF98099A9DCBE5B1C6C67FCAE6FB18A4F950833197364D94D224B8D29AB8860199CCF8CA3C8D1217D99484A631DC29923AA582FBB2566A44BAA1FC037C139CA3AF90D9D0516E7B0732F552BF264325A121F20D393C8E794D1E32A3C12BF68C849A6548BB308753524CE72130F037BA1C02A50162CC10437A5337C6056E850E2319474AA21AC223F8330C43E3A0BE3B0A42227266E6639252271CB2023A55866B004B8B43CCF7E212457157893247201B1AA4728BA476AC7D3B303A6F3BDB818641151BAB769C7BF2A1EBF027A79F81214687D0886C99FD5A46C8B4A1F5697A98BC9667CA574A1CA283BB4F424237B37C96A5836709C7FC0717537CA95213322E3396A55439E1F8C7756A0625FC23CD2745E63A054C47259025C92AAD22E2DC52045A8467AC069E6E22B65D25C2DCA2BDD31AF5057B0DBE93E3D921341046E89E5618A62C562349F2A646E65EBB64091ADECE8CFE68460FF95842EA6648C89BFB0E5AB1CE11103904AA04C4F55F7C64DD810917B756B02BC1054A8DB7116D0718E584ACB47C679E39232E1C87934D84111B0087C04C86ADC5C7F060D5B639FD9B1BC85775461100FA6BACCAD22495BDC0216639954EB53EF77250D2853878BC0183C0EB5359414CAC928E3ADE2E78213904D7DA453E96969A89259822C205FA46CA6703B6D324890E59F0C377C778ABB5ED2466AC929E8AA2E9D47A8CCE606F52B18A7C35C20F4C2AFC99AC327802B856F13BC1D9E3646BF2A2038C454095C147BD1474663274DD50041C9939EECCE460B7FF1605743D4243553A0752465E93C628324BB2D3783EB1B63AE85AA53311C5574071E56991211426DAC6F6AAC3EEA1569B04654D4193DD689A32432447E1A3F0F5595FB37A35FECAE5C55CA839A3CED94AE6FA436A7D34C2A81587CA84018933FDF39A931B03D81491624651A5F6AC75E08077FAAAE8AB023D6344557D15FFCB91C10B06BDA114C5C133F3782C309C347F2A4434DAA89F202817A227C1E1240FF41A4F3CCACFA497CDDC53AC5B0C55614907865118C2C7AC0D5031CBC106CB17A26B97755271C86E871BCC9590E1C2684F2C24A172B10844CA44346FB969CEBA1A02B9922F3AB68457CBB1F2BC6B5F5CC4EACB9D0072EAC617B31D27301E002E978925966B642B00AC53C912EA85DB59C5506F4724B2C356CEB40BD58AE51843537C537EC97B24475318A5B30AE750691765FEE1014644B1CACC77C9C334805A58DE18121FEAC6BDB61184D2C273AB725C32091CA3441E5140433435AEB0B51B9C57066C43F308A12F782BCA5237D7B12119CDC9AE2E7C65EBC66B3F02A0BE7361E08838114BF0D5250304A36935135C61597A03903EF215C6A223DF8A9515E622932F7A29CFA74807B145A20ADAA7987B0B981B7A62D115927504A090F61B37592A5B4E51B977821E4D92228531D2BA1523BAB3D6C850FB0660646892BE2A006DF51743FA383AC47572F651AB02A272A458B81441C8E774547E1773EE29A6A27BA4E839C8E023A13635922009B1111801872C391641F0E239ACA70CE3FC01E5C3736FEE2AB3F85C4AFE3A0BB2529D6EC047337819084222F0ABE8F859925708AE3E62815083196248720EC28174C01D716AD472A0CB2747EDB932F4A58ACE9E2BADFE468BA2A138EAB864EE00C034C1207F92F73AB084E32BC244951477549B2189568BA28553937E5F1CE6ED1B7F0C1012560296442AA6DA0879705A65D77460A86C0C06422A2E6949BB78B955355EEA2749BA0948ED094E5D9CE7C79B9F4047BF4E2128E41CA5AB1BAB1A87A2565CDC018890D426B37F01713DC923CD372605763C3B51A5AD58109F031BD665192F38EED496EAB15AF89DB622DB73A88BB6104D44C49577483F468658A9C72C1A2046252DF7840325978589A85F07C7572E44445F9A8854825E784706A25A70ED0CEC3F5A372C41C54185294975A70AB759B4250E1F2CB35E860ECC25932F55E6991041AC24D56BCCBDA579E421C905469052A34225AD50B69E191B4A36EB7DB1DC1216BB41524497B279230053E910EAB49859D1A99DB3C56CD70995BF031FB71B0AA40CDF031183E5B3D8D8799CFC3ACDDA54010AA6169A80D62479583111FEB337354F6BDF36A33279B7472800EB604710AB94A0D584942E41B78F2C387993A5EA079F3C74207F7972646CFD4E055FB6B10BC69B369D166143B8A8E7400134999F05C6A26DBBEC3B23D92CB90F77156A0C8A96CE93F73962C17F8104B52AFA2B68B62810F998577E07CB16FAC499F38AD628B57FC1C9CC7A568410478176BBC14C8C7A594A07A3568B503BFE26BAD88D2CAE92B90F1E1472E9697F930B6E775A1A9B93EF62757B2A28F6962CF2C1478626A9B65B5AB89CC24EE4C9F765B8071737C25F383DD57598D2289DEE439D28AAE91A3B21E389267FB3D146276B7810D126AA5D096A16F679E28D5C91A580777D9CEA13C46234653634ABD964C720A11A6514A7FB7F5ACB0992BFC7C16FA84CD9686ACDFF2283A89625BC65FEADC9171ECA359BA50BEE412D31414EE110AD1D8748237941683BF292618F740445C0B728D51347928559E569EBFE896DC6295F87517AB31A8639B50C0F3A08E96926816B7B17C8ACB476C4C67B1DF7631DF7540276C99053B102738BFA2876E42A028869C688C18AAA9B00DF0F3A74CD81D86D161912C09362AB35BD8731A48CB45CB92EE8A3374B811EEF93A37F17DD7D8AFF88958E13012E6F7BD6DC63E27BC47F506048CE24BB7411538EB4B969656DCE8DF5ACF28D1BDF5ECD14A44A98350CA45699F033EFA44D25E93FC2094C49E47269D7A3C68DB2C273EA465A5A30D6CE94BFF775EF4CB5F323C7EF064701B690A755A829F05597B2F2A90974F22FB1AEAB42892101222967E3A0AD612CEEBCA\"\n        },\n        {\n          \"tcId\": 59,\n          \"deferred\": false,\n          \"z\": \"681F088AD6962FC397A1B9071852848CE9A7EDAE65A81485CEC87D0974707B7E\",\n          \"d\": \"7C43F2E7D9B1D8D9C41D9F315E052A254CE3A1F098671773B53717A95220AD55\",\n          \"ek\": \"9ADB4E6D612D58585179C9965CF816553BCEAED61651C13CAE0672540BA9763333A25273B59864FB94AB650C6C41A6969EBBAAC0156A9A3621A1104EF578CF1185001316AF57F75C9CF401C4D2C9C8AA15BA8855C730577F128FA32122FB3B29E3813BA9C6CEA1B38BA069662C954794050C1F18C126542644D04EABB9656C446F8C08C6C6214AF2772264872E894086144B30712811A60C27E24C7C51A99A001D296B3CCB5CB1B9CBF4370208C23294442A9C229D1944B59C03CDD663E06CC73AC493D1D75CC1525993059EB4F61C6EE160AD229F3D015BCC919A90449FE8054890006E6BC98D19B602F142515371B8BAF9B456D04B3A429051006997A961422483B2D61857330F74D5117559213CD766E183210DF1100DA49DBD992A9FCAA1AC5834310AA9E9F44B4B2293BDDA76E22C585DB53CB904BD3292C84FDBC38D793136B12C2AF7A58D88C45B0212F0A083DB0929B1B070C74194EA0620A396BB43E6BA9AE43BAB85C589E93986C746EF4643DBDC1F0EF302CAB02512286F846702D4A3AB69FCA5D2B54C7B603CF5D4AE90705F0EA36A6479329B6461DF47026CA86765A79577F2AE1D8A2013E977FF49296247A3717401F7B961090165E34023E8E373CFC8CA5BE146A5690138532A01F9B18C138D9C7B110A66BC47161B708661FB5B5C507A9F3ED396A163A281B2809EDA52C78511C26CC8F478980DC429042A2CBE73600A8A06972260E8F2C387434FB0087435E711E14896B8841BC751A79FF9B3776217E53287270B26023ACD1DC95395F52546E94E287538D2E9539D8587DBE606748A7CDB76449EC2B0ABAC7170C439A4F2BD3F1A3F349B90DDCC13878024087AC33C38B64EE04427EA249D3C7845629CD8D0C82189814E6B2260CBC100B64039599B1BF91938C2B2C35CA2EBA70B8474522FA6C37A917D7B589F646266D2D07B348169F84968C466C9B90A7B00A6505F6A567D5A14BDD2477CFC0B515C7931B3BC1A7021DE7B2CC7C650097B0D2A94A7B024B207B6969E22C720683084CB998E0C422D1B2FD9969DA2BA9096358AE11A5E101C6DE23C8DC041AAA2F441F8693A01A872E6356D9907420EEC3F55B945E34562C0F3838695643DA00336C1CF1802199E093C07347F80F0959CFCA72CE65E2C675BF9CA8B01F97600AD177133CA15932C79C9781AD01B36F606DC74A648054AFE1B74352A4EEA2095A0120D7C5332D1B7A61DB284A3A0519AF63D891A2546D9748FE768604C27827B6E45E1AA1335AC8F2295A83A6AC9E42D0047AF55242EAB2534C7B38AB65749CD21B0441781E643BC16D03D0C437E03E33BCD4B92A9D32BAD6513FBF595DF30BE0475B69A632928A0B4E2A867E4229525953CF303C47CB83F609A1D20EA1D3146C80BF82061E9C5AD670E4288C01E889E95F937611B419C38563228BB9FA85D3E184109A004DEFA472848AC510CB81E18C5301B149B0264942CBB88F8995737C425E44F50F53B9B2A4C44FB0E5C7145775900D7708D51F21173A9170AB2902CB911BFB82D2D4B0333B1832F043A79A6A07530CA00E5CC58E6B0C2706CAE6A6A52D69C6C0B23C90812116AA971398D154404EC531F3712348D0AC62DFA9A1CC0B01C470E16693327AB9BBA8083F22B8314551712475E187400EB801F1BA34F78024A5DEC89B4C7811E68B30EBC91E08373BB92AC0FE8A861F4BF0A0B5877088371209D3294B866A86BB6B470344A069503AF99730773853236AA12CB3C2F679B45D0025876199C68E676298AB1773C92E33276A60C02FF7415EB10838A5035B7114E9834C5BCCA795330009EE57368B139F9D71135D1C86B0833D2CB81D660C56FCB3EDB848721C72A59A8CF45B6543CA7895E686C26A86A5AA7A8C1734C5F576D648482026391CD611389C44807EC771DEC4FE373B483B00D68F34AF776244EC07874A5C22C6C566CCC5C70369FB92A22B6BBC4F4144BDCE6C3A5051268DC8D6E8A2B8F8A8D48020804880FFA32BD01899CA9D3A70FD62A91B74ECEC51BB58594E7F162C7E94D44D64D66592880C55011A8550833A599375CF9207605784C11FABA854BBE6971065479B6D26589D2C756DB93C83E50936CC0C0089CCE8EE0A08149B732559586D07C4FAB56D7A263CAC656314268F55C64A1D6982E665DC42766D84206BAF103D14014221F2914A06162064F7E475811518022B301C262A125BB439D32\",\n          \"dk\": \"92D719984988C65BC4E3E60514E25F64C70318D049A14B110C2196176BB7F9183946C24894F075DE191261544ABE9BABD3280DB7A41B47A937C9597EE995CADC33C1AF87CA2D740F09249CCF376B7D5CBD1B69179323673BD6CD8CD9A84C47B7D232A912F4752BD94B93914427F2CD20C6391CF71F729472095C5D1C3C57E03689D57AB4ABE677909A9895638B790903E5E50D2421117F6A23E7A5CD6A3053FBAA42F386BBD3D82D8B788FF7E652D5830EE676BA978C60342694150A5B28D8A5AAA66C8172AD93D9B3B334C0BB4672DEC182E4395157A0997895BC44C48F3B222C5EABCBF7F2C2973C61B09B3889F0891E35934D4116663A60F751C48E22186B72969EC3BAC346B505CC6699C86BE8B1836152CD6BA37962A23E29B256C555845EF24F52C43DF0C20326841C77E3B87D16C59BEAC267056F40F7117E44C5C957465C09787A52594D1565825243FDE9C568115029F3C378B99F8E28AF395A2119590837B06B75E293682441CD7C5773F5BEFE7CCA172B8DD6EC0E792999D3B40A0439336F2629A1DB78227BB870F44896E112D8094524962F68370AD7092F60375C7A986340D32CC52633B1A714529425DF69BC918A30BBD577E938B6C1D06519078F3D934B7CF77FEB7A179BD8463829AA37D6C449877823231D0EE81B51FB12751946AA1C594ED175607B36D1B7B94CEB8D23B6AA680B9A72857BA61A445AE80C1CF45EF70B44AF6B3C428C8A25689AB8228218C6C0CC858E6A98823E3673913B9163A339327609FD459AFB05CADFCC2943E45A965B54F1A065F68B1F153CC8912329C0F0C0DC605FE6E52905C8B3F1EA3BD0E99418C645A8E6622DD577E9F2BCFB649C1DA09FB8E20DF470CC6317613D788FB544921C325B7B3B7A0A1C3ECAB62B866A513609590E871B17D7866F8A96B838C33A562DD1D47787D795BCE61AD3A9CFBEA5B03290320612A76BC198B0B84D8953909183A14FB318840469E2A909FCFA2C8A9B59E12C77B14A6503829AE8D3B493FB9A6BA6BABDFA7BE215B23ABB5B62C2C6042A909FA9999CFC2A23790548B6055D57B3FCE2C155A6C6115B05855249BEC0887F00369F38379111846946CB25F39305054BAF669E75830A505AC3148299E5AC2B4BF59C68986408270A8CA3A87A70A380EA7AD5871E7DD3165A733AA1D55F1184441DD84183B1002BCA518F370D02D5806498482B4236C8E1B1BF07870C2C7E1D44435077905A5C461238970B603F234B9351FA4213E0CB8DEC764475534AFA18AF07518C573DB0D9790C873A33293424A434B9A933AC644B37B14435D0AFE71B443F83BCEEEC9574016214DA8E66ECCA10668684EB5D776C5B4B2C05C2E03598A602717A72EC4B18E5F550992ABB76A1994B4842FADA39FDAB9DADAB09D6BB9B46AB4EEE69AEBC419F1587B8D7061BE188623F4471852C9294146261A67345F5A2347B206D2870DDA681C44CCFB420708AA398864469B8E08E76081C41F5977893813653A744766125F27C28E6C1DE51978448BB8770B8660B6E4767458DB70990E24FAA35B4DC7571B5EB69BE48C8646586F0ACB48A3B3252A02EF48A7DBD44A5E19177069C6F3F678794B8488C313F70EB4AD6FC09BF078467D362A436411AA689885B987104BC50E00340F8294A06AB8C02B0569B1A6426C13CB9B651677585BA213F7C297A949096B27A02E9510C803EBA002EA423AB3C810595D71F6201366591B60091B305EC7E80371E6DD171DB235D389B480F9C32CC7AA52DEA7714D3004DF5172E034DED271B9C679846A6AC4BE4580E4414EE42CAFED11387CBC98938BA6A59A07E2238B2B54FF6F18D3F48C2FB0636B5CAA3A581948067B1FDB083E92A46EED9B090032655A9BBA2B207EED10D419991AB813140CC898D11B24566970A601326072E15A31D73C0A625FC3E6B697575D293D1B3142F12190B28AD4DB6278CD9A92E50AEB3B60E687A14D2C48623955724582373F16DDE3919A8C7B98CBC514977C5E96895FDB17A952279B8222548EA4B08D4659517A7809C0E786986C2D69A1F7963CE2672726C7CA4A91509856476C13B9488A2A429804815126954C2CAA43F1EA7877BB10346C022A1407CDEEA6322D74C760283CA8699B6433AD16A0DC2238B9714887ACB0315D67139573EAB39A403C3459ADB4E6D612D58585179C9965CF816553BCEAED61651C13CAE0672540BA9763333A25273B59864FB94AB650C6C41A6969EBBAAC0156A9A3621A1104EF578CF1185001316AF57F75C9CF401C4D2C9C8AA15BA8855C730577F128FA32122FB3B29E3813BA9C6CEA1B38BA069662C954794050C1F18C126542644D04EABB9656C446F8C08C6C6214AF2772264872E894086144B30712811A60C27E24C7C51A99A001D296B3CCB5CB1B9CBF4370208C23294442A9C229D1944B59C03CDD663E06CC73AC493D1D75CC1525993059EB4F61C6EE160AD229F3D015BCC919A90449FE8054890006E6BC98D19B602F142515371B8BAF9B456D04B3A429051006997A961422483B2D61857330F74D5117559213CD766E183210DF1100DA49DBD992A9FCAA1AC5834310AA9E9F44B4B2293BDDA76E22C585DB53CB904BD3292C84FDBC38D793136B12C2AF7A58D88C45B0212F0A083DB0929B1B070C74194EA0620A396BB43E6BA9AE43BAB85C589E93986C746EF4643DBDC1F0EF302CAB02512286F846702D4A3AB69FCA5D2B54C7B603CF5D4AE90705F0EA36A6479329B6461DF47026CA86765A79577F2AE1D8A2013E977FF49296247A3717401F7B961090165E34023E8E373CFC8CA5BE146A5690138532A01F9B18C138D9C7B110A66BC47161B708661FB5B5C507A9F3ED396A163A281B2809EDA52C78511C26CC8F478980DC429042A2CBE73600A8A06972260E8F2C387434FB0087435E711E14896B8841BC751A79FF9B3776217E53287270B26023ACD1DC95395F52546E94E287538D2E9539D8587DBE606748A7CDB76449EC2B0ABAC7170C439A4F2BD3F1A3F349B90DDCC13878024087AC33C38B64EE04427EA249D3C7845629CD8D0C82189814E6B2260CBC100B64039599B1BF91938C2B2C35CA2EBA70B8474522FA6C37A917D7B589F646266D2D07B348169F84968C466C9B90A7B00A6505F6A567D5A14BDD2477CFC0B515C7931B3BC1A7021DE7B2CC7C650097B0D2A94A7B024B207B6969E22C720683084CB998E0C422D1B2FD9969DA2BA9096358AE11A5E101C6DE23C8DC041AAA2F441F8693A01A872E6356D9907420EEC3F55B945E34562C0F3838695643DA00336C1CF1802199E093C07347F80F0959CFCA72CE65E2C675BF9CA8B01F97600AD177133CA15932C79C9781AD01B36F606DC74A648054AFE1B74352A4EEA2095A0120D7C5332D1B7A61DB284A3A0519AF63D891A2546D9748FE768604C27827B6E45E1AA1335AC8F2295A83A6AC9E42D0047AF55242EAB2534C7B38AB65749CD21B0441781E643BC16D03D0C437E03E33BCD4B92A9D32BAD6513FBF595DF30BE0475B69A632928A0B4E2A867E4229525953CF303C47CB83F609A1D20EA1D3146C80BF82061E9C5AD670E4288C01E889E95F937611B419C38563228BB9FA85D3E184109A004DEFA472848AC510CB81E18C5301B149B0264942CBB88F8995737C425E44F50F53B9B2A4C44FB0E5C7145775900D7708D51F21173A9170AB2902CB911BFB82D2D4B0333B1832F043A79A6A07530CA00E5CC58E6B0C2706CAE6A6A52D69C6C0B23C90812116AA971398D154404EC531F3712348D0AC62DFA9A1CC0B01C470E16693327AB9BBA8083F22B8314551712475E187400EB801F1BA34F78024A5DEC89B4C7811E68B30EBC91E08373BB92AC0FE8A861F4BF0A0B5877088371209D3294B866A86BB6B470344A069503AF99730773853236AA12CB3C2F679B45D0025876199C68E676298AB1773C92E33276A60C02FF7415EB10838A5035B7114E9834C5BCCA795330009EE57368B139F9D71135D1C86B0833D2CB81D660C56FCB3EDB848721C72A59A8CF45B6543CA7895E686C26A86A5AA7A8C1734C5F576D648482026391CD611389C44807EC771DEC4FE373B483B00D68F34AF776244EC07874A5C22C6C566CCC5C70369FB92A22B6BBC4F4144BDCE6C3A5051268DC8D6E8A2B8F8A8D48020804880FFA32BD01899CA9D3A70FD62A91B74ECEC51BB58594E7F162C7E94D44D64D66592880C55011A8550833A599375CF9207605784C11FABA854BBE6971065479B6D26589D2C756DB93C83E50936CC0C0089CCE8EE0A08149B732559586D07C4FAB56D7A263CAC656314268F55C64A1D6982E665DC42766D84206BAF103D14014221F2914A06162064F7E475811518022B301C262A125BB439D3225F6DF8F68FACBDCE4839DCEEDC2B96D6191CA1DB11F347EA0D66F8C2458A848681F088AD6962FC397A1B9071852848CE9A7EDAE65A81485CEC87D0974707B7E\"\n        },\n        {\n          \"tcId\": 60,\n          \"deferred\": false,\n          \"z\": \"40BBB2C581B2D694E369C0DA567371E8E53C328A59BCE775A625C9F5CC185E0F\",\n          \"d\": \"C2E1A3161F3734F44F3C2F1736E149803F71321122242A1E95E55E5652A91F55\",\n          \"ek\": \"F93584C1B0237F83AE90B664950B471E763CB7939C28D93604C83F73A30F80B3AA8F948086A142A132442712CBD71A9D495587CAC62F98F922E96C9D0095B1502B42DA76B59B1C183338C1C06A730DFC2E889273326593362006EF948AEE1A521E159FF6283DF3D11DEDB1655FC706DEB2C6BE879D3605C3AA463355FC726CE84962500B72776EBC9AA3D960A8084C3182EB8A43918F80EBCE5425257C2237CAC811022792485772BFA51CC7171F86F4C847782BFF123763FA650CB1B173D1AC78F7C19E764F5039736A3B2BF37B67C9783DA9EA6CD0A96FD7DA758E411575F6744DACB42AE2148B7BC9FBF49B4D25CCE02BB906B025EAB98171B80D55C8A328D15C2668A8FDC3A463CA6508E1818437498AA27B04D8514795BC4675C1B94ACEFD54603C498056038D3445745D44BD6F1A4316191B905C650668A3E2402D74E560CBB7A667A27EA4D38495010A71085957421923E21718E611CD5205FFF6AB91C65407C67E7EE19EF0A885EB88B02901478DAB0456D94E4073576AF0542E33C9286048E491ACD2EC2A02E5181436BE7D829AFCA68FE0A78588D886DF77B6F1F94A959B2AFDE328B8B5888569AC34D23771F0AA12C7954EC82A6E71C1760654C34A926DE4B0F07025B9517B2A036577728C7E35653CEC816C3677F703B0EB3C18AEC948368009CB013E365A1E643244289B0AE11440877266C27C955C7773D4377BA2D7C6B8DA7D23C48A19EAA7374A5A9B931F42E144A6CBB8CA06BBD42396B02C6AD16BA6B1E36B2161903EA4A654A31C507825347B9BB177C655B848BD943162F41818F113E864B456D504573B8C4E5B8A07F14713612F78537128253B1B9712C417CCAFB271DCD9683EB82C55D171723A32A5D40EDF9252D01C7ADB6664D6467421920C5B345B2D93C3DFCBCB1DC538D23454068AAA2B85CE41574079136476CB1CA401C1134977E97C72D2C85C37E872CB725A2AB55BB6C37EF2E517608A95C14A970480856A8A5737A06B46E93D03D6384AB8562AF2C37DBB1D52737CA8C5404632C4E9AA714A4A5CD3D073AF069CCDA490C64C42974B57D3BA6C500588C39BCFC4B26CE6119ECDB6A2A47141DA96BA2331996EA93C5D7618F4FC9A27FB6F5BA336F6801938EC26ADE456FDE01D41C22E1424611FB4487D8A2DA212A09749524F004CDC76B8F6339B11041A3174B8FD58B2C6E7B0166AB4246347D25AC489C43288A23A7E59C3CE320E3FCACE6B6277400ACF9E924FD12C0805B70354F3AD8DE92644F8B4936C81CF301FB87A7DC4578628868ABF501626B045D48537F2212A3FA5396C1C857B1084B21B1EB968AD60B43181C77712001290300C9CC2A3D6562A00FCB4E5F11C2AE412DD29181A034725A0CC75E2BDFB851DBFC28AB3362B8CF378BAC9939D340317C64469C27D8F3675C2A962DC7A00E9F67BA1EB8094F0B205388C59223C7BB963D8E21D933B779B360706AC5E06033C1907A8C8EB6E6017C285A8827E28B8CC7A5891CA381FD50D5EA231FEB5A7A4E4380FB5067A6505FA627D19715667FC5602B041CDF9074A3C58BDB9C77CF23F6AD1005C4A6F2581695F444578A734F8FC3E9FCCC07A873A5AD04CB2C724F6820AB1C081306B429F92834FE8BFE401448431BEBC4C7335EA0EC7050A0A49459DB286B774069B161DCBF3BABB5430FF727A419B1AF2371E1441BDD53C03E5445391A401668B693C6503F35CA6CC7A04CA298FFB81CFEAB4B8DF400E1F670801E7014DFCC8B3D8BD953BACC1D61F184A07B383A8325C1251A56E8BFCA75C04CF8653082D52A8D6C944191155AE886AD9AC010AC50BBD2BBA9144785F02B7B7BBBF7F1AB8DF210241F4AEA1700D840AAD5E2761C4066165A43C487B3AFF68BF3370891933BF46C13BE82331F189B678150748AC609B3378C3581AFF91C9A4F2B9C040661E11A917F7A604100E5B9863B8D3B55BE32CC1F40DC47C4F02622863A560F25B03864502F4304F1401B90E4969EC110331A418770B1CA626B3A86659AE9B7B630C6DE1576286976453377663950353BC8067691BD1E1C24C006204E3502C171BD43C33271548EDDAA641677D8D1161152216A4944D480440E3B418042184194C6E39FBCCD353C58A6C3BAD16A33D666D601237F341B7AEF3C98F237E585C8E152B444DF15A768B20104AFC8CE55CDA630D36C048C7D5206708B4699726C1E2FC3A722CB514\",\n          \"dk\": \"AAE64B6CDBAC89352E7D1311EA15C4C43635D40B98F471A9BB1C1A0B086868D2651546A3F6C1A1F6FB8E75010B8661BE3EF1251737126DBB8157B6A1AD3611B04B95DFE46510BB94E7B0B87B55315A98AB61946FE926984FB5A4BDA10333221B92EAC1CFC2A15BE83ADF9297FF61CF87991EEF977A584A3D4CC765D710CF3C630687990A85F4CD1973861868A8FA1A1D3C26A777371CF6108D1B83243B058F729448ECA2A5665B2B6B032BD83CAD34EB5CA394C6DE96AB3552BEF3B28DB6390978DB65A30C6B0528ADA091C80D42AE470078C1D3C45EA21AB418A865FCC2FA4C3D78B76E84B3C3AFC0BD9B2721A092AA274B7A4BC3B8A9DC57332C81B0695522316755A5B7F1F992F5E22C00B29CDC6281ECAA0E74A24C0C56573AB8453F469872722C8796BFD1B64D8AC93F123C74740511A0B983C20C153F005BCBD04965EC95975858045681DB4B8B70327CCDFC9FF8377D599384C15875EE346FC08066EF3BC09D46016B8754B8C0B6BD944D2B157CFE3392A25B98D48850536B5BFE119F58A0739A48654CC8BBA8EC1C1AF4A197EC758396CE4C0C4E66D0BD43A03CD65A6028C185C6C1C59FCBC4EE7017568061BE551BC0BB3509102E8F14829B603BA1CB910DEC374CEA99C301851111A3DACB2292CB95ED1C8B53B965C6D33307B40C538C63872B83DF8C3922385AA8CA075FC118DBB323035C768149221E2C135291070671270CA5342B89044DB59346314F95B95BCB06D0A1F66645B907862C60B4C8CEC097CA87C7B3F2720DDCB0C5D317B443437CEDD32F2357015F9350778A360219B7FC178531B2442F114F2024595EC4CAB1F6CA26F827542096F8B08C4492156424504CF46DFCAB2DB7074FBA212F72284194C59240EACDE348BFC22394985A09874B5BDCF788DFB9B38031B1AB762BC337195597A662673514603E49C2A485E61D37691B31C46EF67CA30C79A6037726735861BF5A31FE102B448CAF32B7103D3549AAE16BB8186670FCBEFEC61D47E923E4460B8CDB029AF81C99F80456033C1B1351F7FC5932902E791C8D65466A36523BA6D58FD370905F3B21465B88C125B23BEBA9F3DA624685833B4A189CE2480D4739AAD85E3EF41E0683B0376C561AA6685F2289891114CA4C380F43289C201CC6FB6E77213E1FB813F1A12266E9812BE91EED08ABDAD5477801201F313CE6A11B9C155F8E07310D6C7769E637EB123524342DFBC67FC0E20A3D09AB9B5C06B149C39FA12E21870273C8C97FD56A1CD346F627593A01606043A18F35C6A323A22A48B3DB49923D3A2BBC92051BD4A9301CAA0D9A386569A37261CC12172839499F928A6A70806B9D8A24AE304D98876AFE6C3E24D500BA08A8F285C9633414825AA01AFC5E55CB2AB91A53DCB47DC7904F6EA77196A8BB9E057A092208BD81C2B9675E1C263D0BA024D2D92B5E85C164E149572B0288531B6BF4BB1E629D7295842A5709D06810CCC896E64A5516130959881B58633F8C411F5032AADE7879CA3385715C8E590B6AAED47D38A708822AA7570848439867D6CAC7EF7A76DD3B890662CE65271EA065302D3A4B4FAA2143F5530E890A1B80B75935BC3C06569C7365A4166AB4D7C1B26030C2A777A121C0099921C77AAE3644BBDA315429C06CADB33BAA3CA419D5064A5BC9C7BC6C8D34C7FB0A254409372B679FCAB46D47C16B94BB2E8253475C0CC5BA6C7DA39141ECE51EDB844A4EA4AC6E3866625679FFF71CFEE1477A918CD82B0883B67BAF2A383B436FDEE7C58AD372E698778866377561BAFA8C9B596AB48676B08E2BA9EE1AAE07BB194D841586662DC64B1065CC8A52D899D09C6F96286E272895F8865B54DA8B33FB67AEA1133E611BF9ABB459AA4442A60CD09A4E39006616C3981604662F7006E0C9130739A66E79BFD123C679A368B64C331C285AC1CC721512A52148A382C09474773BADCA77CDDCB6E4C40022CA10E35A6BB205239074AD6B6524C2329103D281E8CC479AB4AA2E5C2960A903C1E05B48F07B5C88C384533D93A9BF07EB8B7376163532AFAF32B4C481694C11C97ECA8AF0E714F0427D16CBC331E9821E60C735392AE047303DD637AE5AAD915232EBF481BD03ABC845372C60AC586009BC717B6D1A1E78FBB7ADAC5DFA89C410E786AB4422A87BA21A088EF93584C1B0237F83AE90B664950B471E763CB7939C28D93604C83F73A30F80B3AA8F948086A142A132442712CBD71A9D495587CAC62F98F922E96C9D0095B1502B42DA76B59B1C183338C1C06A730DFC2E889273326593362006EF948AEE1A521E159FF6283DF3D11DEDB1655FC706DEB2C6BE879D3605C3AA463355FC726CE84962500B72776EBC9AA3D960A8084C3182EB8A43918F80EBCE5425257C2237CAC811022792485772BFA51CC7171F86F4C847782BFF123763FA650CB1B173D1AC78F7C19E764F5039736A3B2BF37B67C9783DA9EA6CD0A96FD7DA758E411575F6744DACB42AE2148B7BC9FBF49B4D25CCE02BB906B025EAB98171B80D55C8A328D15C2668A8FDC3A463CA6508E1818437498AA27B04D8514795BC4675C1B94ACEFD54603C498056038D3445745D44BD6F1A4316191B905C650668A3E2402D74E560CBB7A667A27EA4D38495010A71085957421923E21718E611CD5205FFF6AB91C65407C67E7EE19EF0A885EB88B02901478DAB0456D94E4073576AF0542E33C9286048E491ACD2EC2A02E5181436BE7D829AFCA68FE0A78588D886DF77B6F1F94A959B2AFDE328B8B5888569AC34D23771F0AA12C7954EC82A6E71C1760654C34A926DE4B0F07025B9517B2A036577728C7E35653CEC816C3677F703B0EB3C18AEC948368009CB013E365A1E643244289B0AE11440877266C27C955C7773D4377BA2D7C6B8DA7D23C48A19EAA7374A5A9B931F42E144A6CBB8CA06BBD42396B02C6AD16BA6B1E36B2161903EA4A654A31C507825347B9BB177C655B848BD943162F41818F113E864B456D504573B8C4E5B8A07F14713612F78537128253B1B9712C417CCAFB271DCD9683EB82C55D171723A32A5D40EDF9252D01C7ADB6664D6467421920C5B345B2D93C3DFCBCB1DC538D23454068AAA2B85CE41574079136476CB1CA401C1134977E97C72D2C85C37E872CB725A2AB55BB6C37EF2E517608A95C14A970480856A8A5737A06B46E93D03D6384AB8562AF2C37DBB1D52737CA8C5404632C4E9AA714A4A5CD3D073AF069CCDA490C64C42974B57D3BA6C500588C39BCFC4B26CE6119ECDB6A2A47141DA96BA2331996EA93C5D7618F4FC9A27FB6F5BA336F6801938EC26ADE456FDE01D41C22E1424611FB4487D8A2DA212A09749524F004CDC76B8F6339B11041A3174B8FD58B2C6E7B0166AB4246347D25AC489C43288A23A7E59C3CE320E3FCACE6B6277400ACF9E924FD12C0805B70354F3AD8DE92644F8B4936C81CF301FB87A7DC4578628868ABF501626B045D48537F2212A3FA5396C1C857B1084B21B1EB968AD60B43181C77712001290300C9CC2A3D6562A00FCB4E5F11C2AE412DD29181A034725A0CC75E2BDFB851DBFC28AB3362B8CF378BAC9939D340317C64469C27D8F3675C2A962DC7A00E9F67BA1EB8094F0B205388C59223C7BB963D8E21D933B779B360706AC5E06033C1907A8C8EB6E6017C285A8827E28B8CC7A5891CA381FD50D5EA231FEB5A7A4E4380FB5067A6505FA627D19715667FC5602B041CDF9074A3C58BDB9C77CF23F6AD1005C4A6F2581695F444578A734F8FC3E9FCCC07A873A5AD04CB2C724F6820AB1C081306B429F92834FE8BFE401448431BEBC4C7335EA0EC7050A0A49459DB286B774069B161DCBF3BABB5430FF727A419B1AF2371E1441BDD53C03E5445391A401668B693C6503F35CA6CC7A04CA298FFB81CFEAB4B8DF400E1F670801E7014DFCC8B3D8BD953BACC1D61F184A07B383A8325C1251A56E8BFCA75C04CF8653082D52A8D6C944191155AE886AD9AC010AC50BBD2BBA9144785F02B7B7BBBF7F1AB8DF210241F4AEA1700D840AAD5E2761C4066165A43C487B3AFF68BF3370891933BF46C13BE82331F189B678150748AC609B3378C3581AFF91C9A4F2B9C040661E11A917F7A604100E5B9863B8D3B55BE32CC1F40DC47C4F02622863A560F25B03864502F4304F1401B90E4969EC110331A418770B1CA626B3A86659AE9B7B630C6DE1576286976453377663950353BC8067691BD1E1C24C006204E3502C171BD43C33271548EDDAA641677D8D1161152216A4944D480440E3B418042184194C6E39FBCCD353C58A6C3BAD16A33D666D601237F341B7AEF3C98F237E585C8E152B444DF15A768B20104AFC8CE55CDA630D36C048C7D5206708B4699726C1E2FC3A722CB514936B2729D96EFF6FBF9B05E34251304A92EA873A21654F70C4632113C36F62CF40BBB2C581B2D694E369C0DA567371E8E53C328A59BCE775A625C9F5CC185E0F\"\n        },\n        {\n          \"tcId\": 61,\n          \"deferred\": false,\n          \"z\": \"E15F322315265F9B847960B7185D962761ED79C62286A0DFDB13DBF550CE0107\",\n          \"d\": \"ACB7FDB596B44A88A60ED74A3FAD9EF745BF5BFA4902CADB891EC5CA45F685F5\",\n          \"ek\": \"E42705D23A30A72638425280BCE72CD596BA2AA6B736A922740C52A0F470C0A1311ED82E15F4C5D865B6BE18B4810127282C1BE22A9BC580784EF371F18482F2E43777396A3E6A77FA284CBC6C663D63005DA53232B06A27D65547FA58E263C00AC701B3B6566F199F5EC172AD4BC12FEBA212C7159C90C589551110066D87E01D54924ADFE0CBEC921AC7B273B1FC400F6B81F23073D6AC0D7AC34F2DC60C08777C5559B013F23B91A743443B1B38375E91C254D8721F1AD2B02E9C566FAA294ED75BE16C0B0D82C1DCC8AB4A6C1D6E88B567C11A49AA055EEA87117A4C5B1C030F8ACD3AFC69D5FA8F082210E356A426C973A17492E8C3C64D87087A54589BF468071C0DAE016B6B51C52B628BEB986845B484FA213A27C6A1D505AE09D499FB92C17DF2AF010C71C1FA0FCD89A59EBCBDA4B7AFAB2CA68421AED1C464892C5ACFAA556DCC227929B6670904A01C5AB2AA0A5A246566C61A7BB9CA07630DD005CF8705B1B414CE8070A43D30CCB45572DEEB253E19ABCDE790A7F4BADD7863CB187FE8AAB9B0CC1C8309415F54AF4BA6BD6DC74C3AA13B0126254DA08F833C71810092E7332DD8CA4CC3199D4EB25405D310A3E9A6E65C8BFED0C6973A6F324CCC3EE16CC3E405D1C036086C5BE644A1A3709DB22A392EAC889CFCB6D9A03CCED44AB241AE735490EBE44A8B333C462891AFF72F2482C9B52B98106BB010A4CF03214330C5AEADB9B7314273D88880358A568DAA16DD8B496B6CA9C758B74F207B5001826391175EAA2DD9912E84B77EA257052065364949CE57365CC457854681729B389A7CC4782F7590EA6ABC49C40B4E23A2EF916F53240CA7BCA58370657490C52AC535E7CBA1CC58CAF52585DC148811264C2E6AA6728A254BBCC983D71716E82B7A53A2ABE376A5362E74F57BEB1979E2FA77998C869D6A799D0934D8475C8CB28AEBB3522387251D11352520C9A76697519B2DB6093CCC3A5A99D75E136C9A3D305F46F3176EC07FE4C30E25B21BDC2996DB413E0350AEE4A96125C5337AA70469F392942037674561D85969AC6664F6D92DD9B5108CDAC8A6E86E8BC1C1DF319AF330701E23AC743232680B3370B471CCD91761C110E309B5D77A48F4E06A681A2B38ACC3185946E7DB0D6EB1837D3199D83C676D667AD1CB51DC9982D3D36BCC121627D21AADF01928F32DF347CE495175F84641CC0704A24B7D099C416D3A56CC5CCD3ACA34F3F670B64C57AFD0591A628E4A19A83914C14212BB4D43AB1548CC772C25F38619692A5DD88730B093AFF5968B9AE60DC8AB8BE3D13F16928E4CD0CF604835F23B101BE7903876C8BB9970BC74A0E4752A67C4BE2F20C10BFB7765D863828565D190C5384273DED40E331168A4AC2A6CB8B0C5D600F5359BE70AB0B74C3C427857B34A9D2B7731DA6ABA7206253A67C22AAA10F7ACBFDB093BB7647B50D6AD449257AAA9CF29E5C5AA2982D6E0B67813BEF12B4E2A9669FB94394E34BD9583142456CA048763E0EABA7DDC48E3CB80736C6121F1888DB3CF85177942F322C3F029B427A6B8DB4976E1B80E9047BF91BDF2449F0B20C336E041B587182DD4A459C4800CA85308658E3BA78D66F47639AB1B70C45B43F70CF8197803BC1EC4A4424DD78C26039F4AC3C84F98A5AFB45A6970158E41296B925D33F68ECCD3B5A0B415F5B7CDE3473859DC48279255F9995561C205B244728DFC4F625316776267BD468BFBB287097911F8C2A32A0CBF14C3C708F9C4BF70C612C61FBA3720EC62918149165BBCA3CCD48CC8B272E538A1E818843B272DC694AD8E265E830034ACE5708CF22AE3D256745B50EC328CBF03760CA73E7E35241776864856BFD7C18577D411AAF99AD412CAACF44456228DE0176FA0E13CB925140F8A5BE9589F4B6B199F751B7A58751DB35680C78C69F83333155D80E08FA92A5B0FDAB909F9223D167333765D033213E3A65EB2B709BA200D861012D7145A9039C4A6B032D9D5802846441C379978EBA4B9970B275271292379B96717E48680FAE36DDC7018AFA8B3C1929AD62268F12AB87FEB92535A655158CE4957A1BE37C9922B4E08D57D85525D6F2B7698331FB3F1219D91A1272B7E92D3245E4649A8257BC362BA0E0462991A481A0A602809C46F4342ED40AD99FC74366406ACFCE7708AE11AA2C3436EE06121FE6CD52FACE80AA5FA2B65A0B1B28F8B28DE\",\n          \"dk\": \"8F841DB58339CB208908CA123BFA4FB00413A60A9EA76648FD69BEECE0B731042AB206368858B0AE798A7A26A1C96AA302AB998F352634CBAF93F81EA4C280830B8DE435CD4DFC3C3674CE2338AAF6543A57BAAE1232780B578BC3FB4230919CA56065A6E466FC19B9322404499199ACCCADB0236D6026BC927C58EA225D34A628721C746D2008C7B2A600CB94E5F93351088A1C6609CC350E5E0920D4D3139AC08A28A0A5DCFAACF057C3DCD933DB7A8159A657CE257481AA1AEA69BDCCBA961B82C56AC15A74DA139BF388610879EA716035372042984AE027C910E149FACC954762B75FE41214A7B808B168FA728F4D52500C88B2E96A121AC9249C3761A97C7333332CFFA182018A15D4B550A930BFE60AB73F20573345522162A5DF021C19F290567A95ACF4622A919AC20BB42B11BE06852094947897B4A7987981F989A29A13C31288B8C30B36244894CE502DA3967DC0636C94810A2A0B9EF72B215E936E330556F0309604B99093B246AB102553117CA48458E845CB87399A42499A0F7913A15358F334577D84004CF23522695EBC6630503B7ED77076EA7C0ABFA4218943413AD664F761C429011F8F0A66E5C22794F60E9FE2B995C004FE21AEF73AB339468735731A969A3E0E192998933F2864C6BC9A82B1A5A542CCCFB85CC629EAC5BF2CBF1F1438ED5109BE13B7F182280EFC07D937783FC5407215742329281AB7B8BA687DF328A3380CA3E4C09523B056EED254A4B7AE6C241EEE61A340528308D3C11F96A315D0B4B02BA47AC14388F2C005A0B15C75CC3D72717B2B13AE8A830AE7706064630A1C0B97858B75B228E7E6C12F1344F656A2D0405E3E932BDE3846F91AB60E183F2469C2A0B47E59E20B89303DFCF89EC1368243B03A4864C26D58B691026B2E627E9C36BAC877C048708B058B02B5327B3325423EEA5C52E311A6E208880A688D540127DBA7FF46CBBC35784BF538B5B04CE774BF5FF82BAFC844445719CE532D928339A19744E9F6391852B5B1E2C06F61CC2C28A16D3BB08081825F040CF0AA489751CE8C99CC0930379A426D3E9A15278B7D40021E5F890B06A767E65088311A8818B5088946CE9871A55D602B8FF389FD9B2E80060E053B82275436FC9690D1453B256CC510735CCE3331D109C1247B6875DBA71079A353293B624C6494762BE23B135DB14599F41CF3BA8EE07CA032E652E00B57A6214106C587B96097B45390A4431ED2B524C641112A95C65F93CCE87816E85B7955A91741650C4E8A944FA088CBF274E31244F77A4C50A98D44B96FB8625FFACCA584AB18740C8278A8ACA2E12989E60CE7A70145B394B7137EA86901D629C63ECB42BC59673DD73515DA3BE2C74D595B6E15AB4324786574D842A2518F2247ACCAE3012AE36CCBC48038BB5C69B3BF95D59016492E2E923CA562C8F8F444CDD3756A69336EE972786B88F2DB42DBB2309228279D6AA29474C2C94721556131DFD95E0520B0C23C2C1B49683D425EB313954154935CC06109182BE7C8A5D45A46B97196B6B794F4944EBE2052B3454EDC814938868FA1FC7ED4D76342591BA5EB96C73166AD311F7C80CFE7C149A2301DF1B8BB96C7742F03B5B3541A8E05418A7A3A89E1841B627D85CA7B6D56897DF5C5977A1E67C2B5D97894245620DCDA7B3183771081B4FD77476440013E417FBCFC4C38A51C1045219FEABD3C1258203A716FDC922F95CB71C15BAFCA2C777812C1AC390D540803B917D153B75D7A947AF328B620B93348CE7342212A286FB63812C2930920933473C95BB383B7AC054E4DF8CAF7D2A2C7C8CB3FDC5CA949177B40AF5DE102D9DB382D3C6D918155FAC94EE088A067C43BA21778D4E78FA0157C8457586E1AB6A07922FCE01407D63E7366BF68116EECEC8F01C262F766008C9A33C2A5A7C4575E95DB458E9C8289A30EA4E32E015856FCFC03C0EABD2021473B251C279ABC66D87EA1F66AEF5A8189544DE7A95C6F8544D8481786C72BF0EA329EEC4D2423846770707D922DDA38601B228E12451657725923B25CE29A10398B5D16492061D77E48EA023A929A4EC3634FA0073CB929CD1A42577B1E6F89CB9522A28B6A9248D243D423884E03799E6413F963462B7B3BF0276BAFE5AFE2F161EA76566FF4406649BA3C1B5811188FE42705D23A30A72638425280BCE72CD596BA2AA6B736A922740C52A0F470C0A1311ED82E15F4C5D865B6BE18B4810127282C1BE22A9BC580784EF371F18482F2E43777396A3E6A77FA284CBC6C663D63005DA53232B06A27D65547FA58E263C00AC701B3B6566F199F5EC172AD4BC12FEBA212C7159C90C589551110066D87E01D54924ADFE0CBEC921AC7B273B1FC400F6B81F23073D6AC0D7AC34F2DC60C08777C5559B013F23B91A743443B1B38375E91C254D8721F1AD2B02E9C566FAA294ED75BE16C0B0D82C1DCC8AB4A6C1D6E88B567C11A49AA055EEA87117A4C5B1C030F8ACD3AFC69D5FA8F082210E356A426C973A17492E8C3C64D87087A54589BF468071C0DAE016B6B51C52B628BEB986845B484FA213A27C6A1D505AE09D499FB92C17DF2AF010C71C1FA0FCD89A59EBCBDA4B7AFAB2CA68421AED1C464892C5ACFAA556DCC227929B6670904A01C5AB2AA0A5A246566C61A7BB9CA07630DD005CF8705B1B414CE8070A43D30CCB45572DEEB253E19ABCDE790A7F4BADD7863CB187FE8AAB9B0CC1C8309415F54AF4BA6BD6DC74C3AA13B0126254DA08F833C71810092E7332DD8CA4CC3199D4EB25405D310A3E9A6E65C8BFED0C6973A6F324CCC3EE16CC3E405D1C036086C5BE644A1A3709DB22A392EAC889CFCB6D9A03CCED44AB241AE735490EBE44A8B333C462891AFF72F2482C9B52B98106BB010A4CF03214330C5AEADB9B7314273D88880358A568DAA16DD8B496B6CA9C758B74F207B5001826391175EAA2DD9912E84B77EA257052065364949CE57365CC457854681729B389A7CC4782F7590EA6ABC49C40B4E23A2EF916F53240CA7BCA58370657490C52AC535E7CBA1CC58CAF52585DC148811264C2E6AA6728A254BBCC983D71716E82B7A53A2ABE376A5362E74F57BEB1979E2FA77998C869D6A799D0934D8475C8CB28AEBB3522387251D11352520C9A76697519B2DB6093CCC3A5A99D75E136C9A3D305F46F3176EC07FE4C30E25B21BDC2996DB413E0350AEE4A96125C5337AA70469F392942037674561D85969AC6664F6D92DD9B5108CDAC8A6E86E8BC1C1DF319AF330701E23AC743232680B3370B471CCD91761C110E309B5D77A48F4E06A681A2B38ACC3185946E7DB0D6EB1837D3199D83C676D667AD1CB51DC9982D3D36BCC121627D21AADF01928F32DF347CE495175F84641CC0704A24B7D099C416D3A56CC5CCD3ACA34F3F670B64C57AFD0591A628E4A19A83914C14212BB4D43AB1548CC772C25F38619692A5DD88730B093AFF5968B9AE60DC8AB8BE3D13F16928E4CD0CF604835F23B101BE7903876C8BB9970BC74A0E4752A67C4BE2F20C10BFB7765D863828565D190C5384273DED40E331168A4AC2A6CB8B0C5D600F5359BE70AB0B74C3C427857B34A9D2B7731DA6ABA7206253A67C22AAA10F7ACBFDB093BB7647B50D6AD449257AAA9CF29E5C5AA2982D6E0B67813BEF12B4E2A9669FB94394E34BD9583142456CA048763E0EABA7DDC48E3CB80736C6121F1888DB3CF85177942F322C3F029B427A6B8DB4976E1B80E9047BF91BDF2449F0B20C336E041B587182DD4A459C4800CA85308658E3BA78D66F47639AB1B70C45B43F70CF8197803BC1EC4A4424DD78C26039F4AC3C84F98A5AFB45A6970158E41296B925D33F68ECCD3B5A0B415F5B7CDE3473859DC48279255F9995561C205B244728DFC4F625316776267BD468BFBB287097911F8C2A32A0CBF14C3C708F9C4BF70C612C61FBA3720EC62918149165BBCA3CCD48CC8B272E538A1E818843B272DC694AD8E265E830034ACE5708CF22AE3D256745B50EC328CBF03760CA73E7E35241776864856BFD7C18577D411AAF99AD412CAACF44456228DE0176FA0E13CB925140F8A5BE9589F4B6B199F751B7A58751DB35680C78C69F83333155D80E08FA92A5B0FDAB909F9223D167333765D033213E3A65EB2B709BA200D861012D7145A9039C4A6B032D9D5802846441C379978EBA4B9970B275271292379B96717E48680FAE36DDC7018AFA8B3C1929AD62268F12AB87FEB92535A655158CE4957A1BE37C9922B4E08D57D85525D6F2B7698331FB3F1219D91A1272B7E92D3245E4649A8257BC362BA0E0462991A481A0A602809C46F4342ED40AD99FC74366406ACFCE7708AE11AA2C3436EE06121FE6CD52FACE80AA5FA2B65A0B1B28F8B28DE9213ED7BAA4999FD5812E87439CD569F1510F0536CB5A34D77C48FCD82BE86D8E15F322315265F9B847960B7185D962761ED79C62286A0DFDB13DBF550CE0107\"\n        },\n        {\n          \"tcId\": 62,\n          \"deferred\": false,\n          \"z\": \"ABD71039AE2E2700391011D9CC8265C2D5C9779002D54E1BDD9607402054CA95\",\n          \"d\": \"0AA4E8D918201BB98464963B076E35337FF3265810723E01C435954DB18B14FF\",\n          \"ek\": \"0902C611CC1D7395142836692CAC578AA1AC7B12970599690FF9958A7048B7D276DE2088D0B06E5C3971190909C6F216287B2344E3922AF9A4BFC1CEB7E5AD35302E555A504B6B65F710356CC30DF957BD60E974FD31729EB63620E30154636FEBD84DA66BBE270151787B7A8B2BA3431491860C8837B5A93C35C62AE2BDBA43BB58D4126EE8139FFB5BFA110C4075C34F9AC648673EECCB59AF60CB8A6372B69C6E39D418AF88BC23161812486A5F2BB9B2D970B811C360329593CC31FA47BEC0FCA95C226E46A6BD5F39674458BD1683CA4673C39D158D9EE13E9736A6863229156BAE9340532D1A1BFBF98145930EE0A5229323468F7B5A9AE9755F6B603A4360CAEC054276BD3E5A4BF29C85F1DACA2C61A514C1449F94A63C4A3CCCA06D195945EDB3C0DAB07FBF75BFDE57A454C564B99C7DA3E5BA095A462C741E634B3D2DD9B7DF837E5FC076A0C363389C8248036EE55477F55BA27F17740E0AB891D0305E77A284ECA7F84CB3C5483FB3992610384E42A731DB8C3B65B6BBAFF04039D09A9BE4494637A9599CA7A4C42FCCA473D4E88B830A4D2358A324620F9B765A0141CB57DC1B70918053611B7AAA7D6D973C8D4C2CC135940C3CA9B7385823C8A86755717A63BD8EDC5712335786B21CA4106C3E6AB14381A87C6C6FB70635159077BE8369699823F54BCCEFEA3D3AD61613409B41C007947015364BCF42F184BF3BC8B03C6976749347EA7564076CAE9360F1CAA3BDF183889766FD428D13936B25EAA8156776C813705B79CF11030ABDE33B87FA4200544740598EA451AF9ED65CE3D0088F948AE07134BC2830253C9A680477C26180621BA679CB78F4E1304743A2AF124054499A21A9B510C78B2F875445B8AD58A559CF624DB8F8ABD1188812C122EAE81AB0F50435C77D72918E03DBAC7FC61DE848B4BC1729140602E738734B43CC62CB48B8131A2E257D82CB58F69AA6C4A348EA48277A0C8504D07323C61C42B68E5A212B01D66DB645BCBB6B34AF9B8AD7F709EB3720C1B79EF09352A078892787AE73D0B5DF546CC5F35D84940999D9CC59FB39C41762AD4AB260E6795F37372BEAC07005BF80F4A5620B4F23A2255D212746F4CFF73995E53C075C6790CAF4745A2AB8CAE2A89D94A601673023819166A709010C9D4A42CE9AA8C96381B1A7A236781C0C0F10C653F71353224242D3A8F0786A31703CD2063929393DAC152186C84253825481FC86BDFC70E3C5975D3A1DE8F23002126A05DB29FDD143A1F60222D7409B468138A8AAE2A34C1FFB6CEAA863CBC9666DBA1FE7138DE19AAED65256B15CBB08110E25FBC7209750C2CB225F6C5C97836AA04BA1B636BDE9235709483998E85BC1A28F713CA3935505E840A82284B879100389EC2D2A5778C990CCDE45B44D26A9AB74C28705807312681BD662F3E5437772CB7C383D72A53537404FB551CC87274294A85C8BB8484C8113E29708664190F923A387E81EBDAC3CE5890A2C440B9D93323E0588CE003606A5204B93288EB6B62F657C6E511059AC4B3DD57D402064B9C086DB955A7958C399F17AB353108E821A1C1084E11B20F2123C1CA6600AD330587988586C8F698BA258165FA3199A423548EED0B46535935C964BB4A0988B2346BD64BA7FA23D42FAAEF424901878012A772FC4815C26D54037F66F8BBC0E82C7753E16C40E1C17C2BC35F470251393B3B7E84534226DB0237DA2960CFDC99EEB4CA4AFEAA15B9271D9F74DF6CA5631935579AA5E189A715827220CE26BB1C07D719751560765E5A2296CB4BD9C44C4D6376E39A89AA8B70AF987AA0C3C10E714931059AFBC6B07B3523B1175AB9E0902BAAA56754A76C09C4CE232B5EED28F58F097BE3A0F5C290615DC7A4C4C328E413D8E43487E124F8C28444E70A7AE02B64D618D8831609CC939859BB100FC31513B90CE30B59061C38BD17DD57A6A2B0A3640E591524A06A6063259467D1F0821813411E62713A742C0ADE48FE3575338356F4CE99C1995667C34CD2582C0F9B7C4A3DA412C737253A3C3957929D5E4CFEFECAC09BB557231A11ED94F3C7A1BE5060313F540EE5576221386DA4C6A7CB71E307B395DB9C6D7DCB256C42D6F52908C565F06DB823463A634AC57299687641A556F5599F961B6E2F0A30053BE44150E1BEA7DC6D1A4488855127512CE9B2B1F4C07FFABDDF9E7FC37FB0A738DED0707\",\n          \"dk\": \"D4B51192A45D0D05BA48D20D7F8210DE386535D2A9204105AF184B50C81E89889DDF30942A9352C6723D2C94C31B709D452266772A8D69D2784C8332687507A1F678142C390737AD55C42CB7D37A28E9B9A18C374D220FC5A9560F655588439D69616620237225B543E6FB67ACE6B149BAA6AB5C45E334B65C87BFA368C581389B1246639B8550F9D94A61BC806850222869AB9041858DD30C16D0B62B53C85A345F26E3113F51C76F137FECACBB26C6AB1C3898E4AA73889039DD61CDEE46379812CB68BA0A03252199F102C661427088A89EB64D9E6CCE4E9929C19CB91BFA2A816340BE89A636339E67A124F659A45E6A061027533BA635854A0CAA45998E6BA21CC10E53A1A9BEB42DF0107F6765C7B9C8A8E98448CF2A6ACC6514765935064417C5E094B038123AF395B78746E4B66AB3E7BCEA783B4F4B5169478C78B82194961F80FC64AD314CD9D089C559815DD91533DC04B9359FD7936D7B8BB5BED435C77057316440926766F3A665F361BC9314230BAC46AEA6AC7EF648C9901DF017BBDE558668A606809CC717C49EFCE5B9F469BE1A0A9853451B130A952C137F6812978270A194C5CFD899CE2E656FE9CBBE8C3C769C700218489DAC47A07CDAA36EF22EE7125EC5223FD959C335B70C56AB8D52333B4C6A1C12DB76A4C02F7E7410CB646E75A5301F19A080561FDE627C9E42B275FBA0800715B1D77643A8193EFB61DDE898305B4B53140A76CC851FB956FE766C8450100A1C9EC0044C32BC17948C3D4CF44C70077447147BCA735EC9F2251A423F96D694F0F01ED4813713545A791282F7086B025BC4B133B86EB4856A3C99F21129AEC57175818360D678FD549ED769365E4720B3A4A433217DBEBA261C48B75C26828199904820C8FC80098DA6A0A9463B24FA5F43A445F33B38985637AA30CE6FC83D7127BFF9DA633E25A362323564D25622064AF20C16D90620AA0875D65217A389438D236EC8F2C18AE3BFC95296CB951A604B9FC15C69A5927F86D98582C3169BD16C7EE1403BCA5BB1C2238B93B5B0F4C6E3969D98325444A09DE2636DA4A046367358CBA66B28531616733FD816B32BB714CAD1B10DF570062223F6366424BB0388DC1A6CC9AD72CCAD22EC51C671BB859C4A3BCAC9525342B6EC97DB62ADE9949339DA5338D07EE433A868060570E087C34B8BF62ACF15F05657869556CC00713CAA9E5C2AF40474F472B3E117B9F8DC4D148183952A0553F49EEF609F3CF12DB7B102855677A443BD8C94429C9168BDC06CA0FB3781E450DAE94A96E4239DF01A74FB3C016B4AA3463AF3E18D8EC653E838249937B50DB57ADA1CC1215972B410AE459BA481836215E12655534A6F183ACE939DEE553435E9BDD00B4273F0A99CD42607E903E85864F0D27CB4A26474265E2E7A56A576971D69C657D237C05CA7C8070F0FE3A8A1AB8549866D32A253F7B4AEBE6521E557131D361999C5CD6B9837299A2100A52450C9AF85890833A9AC8A3B1B1C0B90839295BA6997BDF262AA93196AEA9F9EB1485E8415FF7006CAEC9A0548AFCF6816938A5E77D5A383F6B3C140786A3626207218883597BC3670903364BD275AE420BAE20522D5E69F9D292E7C5A6A3F3134F767004D052E1476B2A95B790B295AE6F68689828F69EC7965F495E7667E7B420EA2E5C738964FC0257E9D423B98C409E9A27B41E0B87A396EC079338019AE8DEC20DFE0477E940B6233C7D6244A201A0DA4B45AE785158C545AE6B28B49F799E030C762F0BAC8987E12F079D28C4BF7074BED316AB4580D21A8CEF4C30D637275FA2C6884E61B6843C4BC696E8615C47D573FF20BA1053C4259076153BB9314E6CA62F1AF91F594ABD444BBD2B3B203A075083AE8036109710F2B169A688CBCF6D563FAE94D3DF65BFA92B1D20546B5A3A0FF6B3FAB634712566B0E021D617232C8029EB2F76DF624C8DC77AB3789739AB12AE3AB59A7B612A40970D5526023F690C8F1A9BEB4A49F066AA8073E1F865DF9313A63464C65510E078B61320A34E3624E2A6A3E16B7BC3FD995436C92C28B18B149C26754C0DFBA3121778D8E345E6FA4CE8F104F71B77FFE64C0AD945F1F227A031522D07990F88BA91FECB244B5936E87298FDC1836E82C0CB178175CAC5A36C27D3B71EFF5ACAA39200902C611CC1D7395142836692CAC578AA1AC7B12970599690FF9958A7048B7D276DE2088D0B06E5C3971190909C6F216287B2344E3922AF9A4BFC1CEB7E5AD35302E555A504B6B65F710356CC30DF957BD60E974FD31729EB63620E30154636FEBD84DA66BBE270151787B7A8B2BA3431491860C8837B5A93C35C62AE2BDBA43BB58D4126EE8139FFB5BFA110C4075C34F9AC648673EECCB59AF60CB8A6372B69C6E39D418AF88BC23161812486A5F2BB9B2D970B811C360329593CC31FA47BEC0FCA95C226E46A6BD5F39674458BD1683CA4673C39D158D9EE13E9736A6863229156BAE9340532D1A1BFBF98145930EE0A5229323468F7B5A9AE9755F6B603A4360CAEC054276BD3E5A4BF29C85F1DACA2C61A514C1449F94A63C4A3CCCA06D195945EDB3C0DAB07FBF75BFDE57A454C564B99C7DA3E5BA095A462C741E634B3D2DD9B7DF837E5FC076A0C363389C8248036EE55477F55BA27F17740E0AB891D0305E77A284ECA7F84CB3C5483FB3992610384E42A731DB8C3B65B6BBAFF04039D09A9BE4494637A9599CA7A4C42FCCA473D4E88B830A4D2358A324620F9B765A0141CB57DC1B70918053611B7AAA7D6D973C8D4C2CC135940C3CA9B7385823C8A86755717A63BD8EDC5712335786B21CA4106C3E6AB14381A87C6C6FB70635159077BE8369699823F54BCCEFEA3D3AD61613409B41C007947015364BCF42F184BF3BC8B03C6976749347EA7564076CAE9360F1CAA3BDF183889766FD428D13936B25EAA8156776C813705B79CF11030ABDE33B87FA4200544740598EA451AF9ED65CE3D0088F948AE07134BC2830253C9A680477C26180621BA679CB78F4E1304743A2AF124054499A21A9B510C78B2F875445B8AD58A559CF624DB8F8ABD1188812C122EAE81AB0F50435C77D72918E03DBAC7FC61DE848B4BC1729140602E738734B43CC62CB48B8131A2E257D82CB58F69AA6C4A348EA48277A0C8504D07323C61C42B68E5A212B01D66DB645BCBB6B34AF9B8AD7F709EB3720C1B79EF09352A078892787AE73D0B5DF546CC5F35D84940999D9CC59FB39C41762AD4AB260E6795F37372BEAC07005BF80F4A5620B4F23A2255D212746F4CFF73995E53C075C6790CAF4745A2AB8CAE2A89D94A601673023819166A709010C9D4A42CE9AA8C96381B1A7A236781C0C0F10C653F71353224242D3A8F0786A31703CD2063929393DAC152186C84253825481FC86BDFC70E3C5975D3A1DE8F23002126A05DB29FDD143A1F60222D7409B468138A8AAE2A34C1FFB6CEAA863CBC9666DBA1FE7138DE19AAED65256B15CBB08110E25FBC7209750C2CB225F6C5C97836AA04BA1B636BDE9235709483998E85BC1A28F713CA3935505E840A82284B879100389EC2D2A5778C990CCDE45B44D26A9AB74C28705807312681BD662F3E5437772CB7C383D72A53537404FB551CC87274294A85C8BB8484C8113E29708664190F923A387E81EBDAC3CE5890A2C440B9D93323E0588CE003606A5204B93288EB6B62F657C6E511059AC4B3DD57D402064B9C086DB955A7958C399F17AB353108E821A1C1084E11B20F2123C1CA6600AD330587988586C8F698BA258165FA3199A423548EED0B46535935C964BB4A0988B2346BD64BA7FA23D42FAAEF424901878012A772FC4815C26D54037F66F8BBC0E82C7753E16C40E1C17C2BC35F470251393B3B7E84534226DB0237DA2960CFDC99EEB4CA4AFEAA15B9271D9F74DF6CA5631935579AA5E189A715827220CE26BB1C07D719751560765E5A2296CB4BD9C44C4D6376E39A89AA8B70AF987AA0C3C10E714931059AFBC6B07B3523B1175AB9E0902BAAA56754A76C09C4CE232B5EED28F58F097BE3A0F5C290615DC7A4C4C328E413D8E43487E124F8C28444E70A7AE02B64D618D8831609CC939859BB100FC31513B90CE30B59061C38BD17DD57A6A2B0A3640E591524A06A6063259467D1F0821813411E62713A742C0ADE48FE3575338356F4CE99C1995667C34CD2582C0F9B7C4A3DA412C737253A3C3957929D5E4CFEFECAC09BB557231A11ED94F3C7A1BE5060313F540EE5576221386DA4C6A7CB71E307B395DB9C6D7DCB256C42D6F52908C565F06DB823463A634AC57299687641A556F5599F961B6E2F0A30053BE44150E1BEA7DC6D1A4488855127512CE9B2B1F4C07FFABDDF9E7FC37FB0A738DED0707C266F50028D4382821B206CE45306AC320BAE56F49DFDD86F37E1B36C23DC86DABD71039AE2E2700391011D9CC8265C2D5C9779002D54E1BDD9607402054CA95\"\n        },\n        {\n          \"tcId\": 63,\n          \"deferred\": false,\n          \"z\": \"177A8DA7AF8DB3F712E1653D05A47D61B59F4F4950549382E56F761D7126F8F9\",\n          \"d\": \"F43EC0E96A791317938761FFBE97332D5D85F52D22BDA6303FE7E7107DB608A6\",\n          \"ek\": \"F4C227FF124D62F0269297261583A7D7EB570E6A52C8B9240D1B1A79946FF3990DDD20AD71284E7B004656270DFBA0681DD2C5C41939D6391F4AAABA0ED3C6AC55B4907534DE05B775CC6284D53D045BB611F5BC62F603327C143E100B6678176A40A4949A923CA651F1D2589896BA74E41722B3A0C1C652BCC7ACA769119332A080211CCA8AACD4799513BB0906A063F2AC750C99A805B792699400F609960D177BC006227BA40CF1C547BD81C9F5D67947B6A7BFABA98415C81A4137CD2CBF024322633A8B1B242594581ED8720A794293FDB282EBA064DF25301FEABBBD14BA4FB8970B52B4E3574BC57218AEBC5FB9E731F5A85CFD1154B42575A0B3BEE2FC97CB691AE9F4361F11692D9CB07D793A3C6588ADF24FE098B0CEA860E0173C35223C678A10CD3752E97766B28BA562D1CB93935E97E73B6DFC3248217E44FB08684B5419D749D9350F143CC66E9921FA6C4180D43A721424C06864EA98CD3E60C12020AE7123ADBC2CCF40577BA4F87E10B755A1567941F98E0D0A0D26A9303383026D021F2284CE313AAC3C0A765D11747EEA8BC2832EC87B22EF31AD0D1409FC66A782B2B09AC43EBFE215862B6462996A3703525281BA8328621E436DBD533AEA69B62B429F83A78843051AA0D003C6F8BA224148E15A5AB39A6EAD69262627C7B0C46730C66D2109512D424849A53CE58B9FF463B87945CF9E253130A64CB1F18A2ED875D43026BB8AA2B87ACB00186889C082391C95273BB864E548ADD29A876B3445A471B685C659E213853C62E1A802E2B3C4C392575620597C7131F2764AE68B95AE190927E110A10A553E008F39054DE62C16912064F0B7711EF59E85E50E30395A36A11EAED23CB48A01C8F67729F6B638E3B6820A0E530ABDE31A97E7DB117B035897952F0A0473AAE5897A86725C53855AA43A16BB31BE7C564FDBA0BDDB44D6E64E7959C871728C7C5111D989CFB9026902B82544B528D4FA8BE3196C6318CA1BB791B7069EF2D225277C60B03C48C183484250B4FB0A5B05E260E8A9C958CA51F9BC69DA7ACEB006569B427D2A60C070D81C410114E753BF85651E3504418E747B8D787F0237B184834FF8689519034FF6956313120A0495267B754BA5015D8A134688B11160A2CDE5E8B18D4400AB95AC6CFBCF5CE72020C60761128CBCD5BD36817360663F83A7A263B9420A654C7280381092B4D295B3DAC03E918926B5C29BEB0478A01A39A8B0CCCEC05125D84D2872CAFC97A8627866DC7380DE17709FAC24EA1A0E00DAB5990A919E0262DA522CCAF9B0D3F11268D0A2B2BC4D85E641F64048A3557887A266A8DB13EB555608960E49618B1E39708B65151C4BB8D2154CE7B6A13F86B75367B005C9A69DA98B82ABB17EF9678EB50F3F097A6292930191A15FE075BC304D49926B3D71B675C26869B6BB93264F74702E17102DB5BCC881751120CB8D3E05215E4B34A505529BAB1A2A259B7BC7999404BD15272E44B9AA4BDC891E01A21B8A71B46725DD187E2041C07092CFDDC902D3DBAFC29379691644D0E15A8F7378298A8834D1C1B078384C140F460B144826188F51852AE0ACBAA1ADC4D86147C02F61C1B154193B2627C609682A6E656725774A2FC60DE52A3837537C421A58312A7E6BD1CB7B7661FDB3C1D9C51C3A6A44F260982BDCC01FA149697B24B00B76C5DA2908181E8CE21A28D9142F12659023536F392AB3781C74569DBA10506CB79BA214C49CD18A1D9A693734791FDB76360A142B71252B9020099B389AEA8A25796CA43BB5FA881277D742B6C7CC4651CDF47B91C73B7CC816B16F040DCEEBB3E27C68F0388ABE1A4B2B8B88820B30C9D03C1ED75A9E46774B9612C6C039C1F0404F40CAB35369BCAA80FA2C53975C63F75211B1154F94C3AEE4DBB3EEE500091BA629900A54245CBB4C92296769D979530371A0EEC19DC6718AFACCA91D219E37788802B5C162237A7274C3BE821BC969493E884DF2E915ADC3AD6F1919218C844AF94C983ACA8038CC3874C0C080161D5B4F1266420881C906C145ED15692E8C24F2574F80EB3265157EF3E63BDA96390AC160CEE8B6A79B363C290B4B35AEF49CA4DFFAC0F0423249F27A6CF77558F0631581B36E58AD1DB09F4781B8F813B0ED6578FFBA5A73FABA29D94B6F9238A9089CF41BEE300083ABC96CF379FBD0190841D981FE9CFAD1C7FAF23D2426AE\",\n          \"dk\": \"95937E82B41B65D97063375BF2516320419CE2DA063E41B62E50AAB029407A98044163C56670C75B368D6771AB590A40177073DF486D083C62C5698BC02B97DA59CFEDE6305E3ACC6DCA183093622EE80177BC507DCB8C5780A398FAB838A7CBB7126883A006A848B743FB7D0FD4CE1E4CA026FACC5F981AFDF0AD72FACFA69904BAA9944335B14B9521B492876FE30D07F81BDD7162AFD5A4BAB76E13901CCCA7700C45B385C11F23951DA37740FF58818CEC668992C46B5A478AD221A3EC989F77A5D9B1095F2A7AA0849A731AAD51A6B51B0CBEED2A8531B9A3D3C39E55597E1076AE4B4146740A8A16051A9EFC5C4E016695BA97A26AAE6D850590773D00449CA68A4E65D0463EC15DD46813A7810051203ACC7A42814A7121AA6B45EC9E37DC2F40AB96A1C025835CA7C8198F292390E3D5C9967865F2108DD17402DA657175C65AC47842E1473F9BD29DDE44388695109453B823E204A05A9B3AD6A398B69185028460F8230E3241E4012659E05AAA9CAAB8D8079C65919C8304EB6AA78330BDDBCCA763B91729F81B05BAC53BD31332FABC69D4CDB85CC0DF0B47961731D26A2FC1649A5B53A05CD13109479A75B48F67898C63713EC7883BC3744C9A60C23AF5320C645DE8C242C0B17EB97974CCF76D57180E40330663F9BC737A40C2202F47B22580F5CA070C4488B1B139D0155B1006A7B31A79F0BAB9239B9E4B7DBACB534790ABD0F28141D43CA52792503B4C0CE96B0F505A4E263D1E9429F631447719B8580C902385B221FA2C174866E8872D1B299706B860343BC9AAE6660B77C7E2BA5C8C1CAE4A054A60038743655378080044EC7D45D36411768595BC1D30A43AE3805BD77211E1EAB4C97C9A4F6408FC48551D880B4728388F55519757B9B258781AD2915D40C9DA27961145AABC1666253171A62425C8BA9667322C7C9599104C034C1168A50A47A44A7BD078B24726561488661CF70FB2B7242BC354DEEA94324622B55607F06221A65255C2924982964A2FAC5BBA75CAD6B4677DA45F96DB2F91FA7517AB968A66AF8800AD431423F03936F3614D3ED7236C8A32F16B4DED28285CD46A5AC06A42397A2CDAB0FFE1A70208A226233A84B4AA6B0CC8BC1A4913B44322FACAA7917D119556412880286405EB6C9A7E75CE74C75EDF794A067747AA0B31E99C1F68C25696207C90B97CB57C5088982129811E08773249C5898FD4B86D79BCA5A02C1D389530EBB1AA2ACA1DF1A60FE19649F7026B9229B39A60F29982EAA639D9830074CAC4C375295719C4FCD03A36D99DC6306DFF96A4AA1B0110D37A1E716DDCFB0A322134A978C1D0998CD5A8943B267E2B6B0D6936111BF630B0CC89A549405CB295038ACBF22B6E15788CC8966060EC4BD71255511826CF163A6C45AE24FA8536C39F8C19C7856A3700972FDB959AAD019C04269C97D7CA55121A567BBA5711A7F7E28A13F955DD06988AE2BFF90C4C3C605E0B200DCBF139E576BC367C1D151A8F7297B5C76CB826208EDCA1CB23183397A37A92B233F65494E6E3644A364EB8F05FD93248DABAB286991297EA1A87B947A8C0C1E32A9B336B44E5D045E4418C8BD366DF77C2AD90CA2D690C516B032060C32314CB7DDA818E80456E588248609E4E4BAEC2B30FC1E8A984082C23555093D375FF129CDE4498BFD4A40AD1678AE78C5647B6998212CB0010241703A781727FD755B5907B12350A853440B9019F7BC3738A1A2BB291A549A9892D74BD2FC4B13BBA718F9877BDBA32B7D07F0DF33BC0C19AB3C2B567D7110BD3592274723F5A69AFA632FE8C41AE7CADAA115E61B897C4444FA7099AEF0290A6A13E8D551A3DF565490A825DEB6D808AA7AF377B27355324C355C32358955B883F6BBD42C66801CC443BF34F2FA040FCE4B7237C4ECEAACA821AA8B248841D242B6EA5561B070E06F711528109693AA289740F97A4169758151ED57A15381EE35297016B0699C34F9AE039ABA26ABB11148BB4344359CF14466841C7CEE7B3541B6072BF021E3B8348E078510DE0839EC55C38290F5C5095B429AF7BC970CE46110F54967C42A2AF4A3CD5BAAA5BBB500EA382E9427FE02595B0E6811FE32E932BA6B479114F5B7224F98EFDE80CDDE06BC580B30A5175B9B38958393B3BF0573A7086EDD0A1F4C227FF124D62F0269297261583A7D7EB570E6A52C8B9240D1B1A79946FF3990DDD20AD71284E7B004656270DFBA0681DD2C5C41939D6391F4AAABA0ED3C6AC55B4907534DE05B775CC6284D53D045BB611F5BC62F603327C143E100B6678176A40A4949A923CA651F1D2589896BA74E41722B3A0C1C652BCC7ACA769119332A080211CCA8AACD4799513BB0906A063F2AC750C99A805B792699400F609960D177BC006227BA40CF1C547BD81C9F5D67947B6A7BFABA98415C81A4137CD2CBF024322633A8B1B242594581ED8720A794293FDB282EBA064DF25301FEABBBD14BA4FB8970B52B4E3574BC57218AEBC5FB9E731F5A85CFD1154B42575A0B3BEE2FC97CB691AE9F4361F11692D9CB07D793A3C6588ADF24FE098B0CEA860E0173C35223C678A10CD3752E97766B28BA562D1CB93935E97E73B6DFC3248217E44FB08684B5419D749D9350F143CC66E9921FA6C4180D43A721424C06864EA98CD3E60C12020AE7123ADBC2CCF40577BA4F87E10B755A1567941F98E0D0A0D26A9303383026D021F2284CE313AAC3C0A765D11747EEA8BC2832EC87B22EF31AD0D1409FC66A782B2B09AC43EBFE215862B6462996A3703525281BA8328621E436DBD533AEA69B62B429F83A78843051AA0D003C6F8BA224148E15A5AB39A6EAD69262627C7B0C46730C66D2109512D424849A53CE58B9FF463B87945CF9E253130A64CB1F18A2ED875D43026BB8AA2B87ACB00186889C082391C95273BB864E548ADD29A876B3445A471B685C659E213853C62E1A802E2B3C4C392575620597C7131F2764AE68B95AE190927E110A10A553E008F39054DE62C16912064F0B7711EF59E85E50E30395A36A11EAED23CB48A01C8F67729F6B638E3B6820A0E530ABDE31A97E7DB117B035897952F0A0473AAE5897A86725C53855AA43A16BB31BE7C564FDBA0BDDB44D6E64E7959C871728C7C5111D989CFB9026902B82544B528D4FA8BE3196C6318CA1BB791B7069EF2D225277C60B03C48C183484250B4FB0A5B05E260E8A9C958CA51F9BC69DA7ACEB006569B427D2A60C070D81C410114E753BF85651E3504418E747B8D787F0237B184834FF8689519034FF6956313120A0495267B754BA5015D8A134688B11160A2CDE5E8B18D4400AB95AC6CFBCF5CE72020C60761128CBCD5BD36817360663F83A7A263B9420A654C7280381092B4D295B3DAC03E918926B5C29BEB0478A01A39A8B0CCCEC05125D84D2872CAFC97A8627866DC7380DE17709FAC24EA1A0E00DAB5990A919E0262DA522CCAF9B0D3F11268D0A2B2BC4D85E641F64048A3557887A266A8DB13EB555608960E49618B1E39708B65151C4BB8D2154CE7B6A13F86B75367B005C9A69DA98B82ABB17EF9678EB50F3F097A6292930191A15FE075BC304D49926B3D71B675C26869B6BB93264F74702E17102DB5BCC881751120CB8D3E05215E4B34A505529BAB1A2A259B7BC7999404BD15272E44B9AA4BDC891E01A21B8A71B46725DD187E2041C07092CFDDC902D3DBAFC29379691644D0E15A8F7378298A8834D1C1B078384C140F460B144826188F51852AE0ACBAA1ADC4D86147C02F61C1B154193B2627C609682A6E656725774A2FC60DE52A3837537C421A58312A7E6BD1CB7B7661FDB3C1D9C51C3A6A44F260982BDCC01FA149697B24B00B76C5DA2908181E8CE21A28D9142F12659023536F392AB3781C74569DBA10506CB79BA214C49CD18A1D9A693734791FDB76360A142B71252B9020099B389AEA8A25796CA43BB5FA881277D742B6C7CC4651CDF47B91C73B7CC816B16F040DCEEBB3E27C68F0388ABE1A4B2B8B88820B30C9D03C1ED75A9E46774B9612C6C039C1F0404F40CAB35369BCAA80FA2C53975C63F75211B1154F94C3AEE4DBB3EEE500091BA629900A54245CBB4C92296769D979530371A0EEC19DC6718AFACCA91D219E37788802B5C162237A7274C3BE821BC969493E884DF2E915ADC3AD6F1919218C844AF94C983ACA8038CC3874C0C080161D5B4F1266420881C906C145ED15692E8C24F2574F80EB3265157EF3E63BDA96390AC160CEE8B6A79B363C290B4B35AEF49CA4DFFAC0F0423249F27A6CF77558F0631581B36E58AD1DB09F4781B8F813B0ED6578FFBA5A73FABA29D94B6F9238A9089CF41BEE300083ABC96CF379FBD0190841D981FE9CFAD1C7FAF23D2426AE2A959860220DFD26FEE86E0F4EB1D8E31B240EFFB9EF6091AA0BCF551A09B2B9177A8DA7AF8DB3F712E1653D05A47D61B59F4F4950549382E56F761D7126F8F9\"\n        },\n        {\n          \"tcId\": 64,\n          \"deferred\": false,\n          \"z\": \"79E3B0D4F4AF344ED06FDE8BF4E104753E832294A3D2E4B66BE59149006A7B95\",\n          \"d\": \"0596F1E214B29A0CB7A641EA0BB157FE01FAB73B4A9BCDC165FA44C8FD5FBF71\",\n          \"ek\": \"BF2A86AD3020C241CE4A00913BDB13F82266DD873742A3519E44C39F20874575A780370BDBC32F50F892366C3D3E5A3CBD2A1A9EDAC8D5468A4FD28D18281C4ADB485B033E76B3B604A5394EB6A280F81C93B86839C2198430558889696CA715CA0B901D762A498875DE991479A45725E12340953D95461ED32AA018E33BBA0157F3A6BD1BF07FBE2207D81166F5410AC20096E95B6F48E09190ACB51A86AD4C51AD1733729272BE79BCB14834A230F00C64E0416AF77C2F155776D6528BD0C9762B60350430A3BCC51DEA9966D791A3904A1F6284A32B4C865CC67C1B66A7F00BA3C9971AD846DF61348D2C418DE0901ED19D32582ACA784AC96A55AAE69D27B58973B4816714B2139C2DF5A380739C43CDF0A161AB8535F678FE344C5E4881BAB8785805C615711ADEA09B702201360B2479CC7FBA69868903CED7074D7F93AA7772B3359644B0A3A169A59C177CC60E8B4360978FC7C513A95C355643563BA0371D973F7A991AD099BF94239815C7041A312CC3497E9A40A5D3F9504BE107FDEA73D8769025D75479575F5A45974FAC400F0189076457A2A96D9BC2C1B19C5382C423B3333F0C9B7E26FCA8F2DB01265460A4C253EAC707B39109AF07241DD6A8FC5A9D18494C3D715424108114280F3A5C23CEF53C5507BDF3BA5EDA6327AC15B484793011241305462DADE02D4459419909075C09BA1044B896A28E17866FEBCC0A4970B1734C4EFA42780C380EAAB17C82B96D72CBAA83127FFCD7998071BDF8EC59881AC74177C1E194A20BF5A4DD71C4E03A82D6E40B70986D2A368C53C54BD7B0CCE1B5A161140E01047443497EFB4C4D3FE126686BA354B504B9537C27B21B56355BC0A91F82E94853801801AA5FD8287EB5A1BAEA773EDF16B376311DB6089AF1950A385037DEABA09ECC491F4882CD484604F304730C11EB3C561F36B85D1B1ECAE15FA1326F9EA486CE32978E9C267C70AA33E0113F4C3B618B405A160B4C93BF943C806FB41C62F83200B81AB2CC57CEE96364D86D4630BCA5A70064151545EA9D9A419E41242B6135AF3A516C8DA4447AB71478266E5BF69F2B846075C30F3368B30912BE48A00CA3365BBAE0597A93942396515A72B675C460BF4403872995AF19CC25C737F86B642ED740C9E5477C0154D05860A30C2DD49B14E8DA87600C4E8CA63401DA998D7689D9CB364397C4DD056474C2BEE16060D4408A3E19BF6FF8A331466209F0B428F0316CA89188B9C7B2AC25A7940DB7A1766799361BA984A1BB7E789B2438D6777F92A15CE7A2D156369AB819B116CD99076095C70D265651550BCC1F4307497BB77E75023AF91D31F4414B38CB64CB570AFB56701C17CB692A46A444455C8063A3616590AF6064BC337237ED1824CCD7CFB96B55FFF91D7D4714CBBA74DEE5AB48A28389458E2D43476CEA2B1A0B6EC499A83CEB8EB4C7539BD93D3AC987AD07A1C9F079850C1444D0B5C9CC619614301CD77776661357FA80F29A3CE4A8AF8DF55FB259B754504687508AC12AA81DB8CD19C93875AC5C4CF13E115221EBA49F3BE008D4B80E9EBB8AE0CA997313872010497CF9556DF06D8F766D2025B03099BAC8202C5D90B52C5397CF55BFACEB5A1C079CC02960FB976A735357DF16B90210194627A5089B1D29CA4FE8A03B4FFBB544A645C5FA95EB386408DC65AAD89CDF0285F3745EDA596D070356D350775FA7A088D732A61C1AC57306359B4A642B8AA8BB91A347152A916D33427DAB37143AE14B2D804B0FC033EE2B9D996853ACD202B2689AD945CDA48938BCDB2D8DBA5449C80891B103172381200A68BB7B505EC5A373E15DF301BB7547770A5B21A571CF4CE87B1AF38D3D38329BE9AD366159FF1919DB40233E86BB497652379B9099F68B22EA91552273AB1801CE6BA9FE7B07ACE9A81DBB09B11CC4AECA25B5090C6FD885B7E19941D8561A15CC35046EC191157C393168C80311432C899B0A0AA578460963E7767750560064B93AB148AEF2FC2FBB029C89126535C6BF2DC48A04748D23A6225EE6C3EEA9B68C65BCA2E9C804EA5BDC441BD05293B8C19D96E369B5C3459DABCB20565405824EC3F57475AA4E19DA21A8EAC0DD312E9FC10B56D6C6EF40A130844D8FD68769E2CC08288D770CD00556578D853FE4896886536AF0633EFF38C1439E3E7950091189960045B3B3B18BD45BE846AA63E4D3845BA0\",\n          \"dk\": \"C62AA41327AC132035DF838111878AE1BC660EB53459381290580906A45376761BEDA32D498611D642173E7CA019DA1AF1E60EE43601F1262D3B396FB5611FDEC023C891407AF4A8ADF85D4C010F7DE70829138F0F520EFFB4C8D5230253CA72A6FA3D4C4158A65C12FB9A36BA440A40B476D8C65CED2968BB18A11751993B725920399933F3B18DFA9A4ED87A7AB277DC027CC33A83E43B9DB68C8F68D72FEE7210E9E1471869B347C79B848C623834C586B341B23203D2CB1AE5E9CFB87A1794D43598EB0CC8836BDD42371AC9B7D0513F4A7840C0371D73BA88A7DBC5A5753020298E3D616A8DB46C81C80045A553984939D7593535D6BF2A330BE54A3D1CC0B09774279F952A59F673298C6B1C078E0FC87038D03304E0C4585577A6842815D9C9F41534F216B5B0474F1574BE15051F3DD69CE3E9870AB13FAAD180C82A1ADC2927A1D20EEA168A97DA7ABCFB617B7570009AC7D212B6D05483B7F54F7C201588A879235823038834685BA5C7649CA7931D3D2B155A3410A76900E4738A24A614AAF119D8AB6111E383BA33A4614823063C31EDB9B031953AF6FACFD3D65ED1557EC879B89F3709D177A8818094B35500A10B30E10C7917956A0181BD6B0817F0D85347430EE6724B5380BEF508AB3B468AB6C8868FC48A62F3445104A41C8C288DDC5746C84301E5315C603F80E9B83F6C16920142611CC32B18BD7AFC28D3D343AAEB8AE7133F88E388AE6C7C4911318A3957904AA51E2905D5A02C30CB57DA0551B4134182C060DBE80BAAC4981B410D1D5104D92BB47D3BB6846937CF6B961FE66BDB5C069A0304D91AA22D8898F8067295DA9BD6DC3408F3636DE5CC6948C9DB2B17DD4651F1419668AABEF031CD06FC63BB9749C4DAB8A160B5A4C6A13F3A4F8941712EFA60E5B426E1BB29F65940368594B681CD30D41377F58254964FF0AA4D3A5B553327762036757429CD47D57DD73698BA0C765B3C539CE41A04A58775E70D4EF38D2B9775D203982479937E5135FF1B944F6CAA0E87269E52B52DDC3958C99C668170879B7FD03427C4F38E147B6ADEA1BF0123B01CBB199644AAEA95B76DA7C5E60839C3EC0D50C4A01E79810BBC1220F612FE34A89B173C7E6042BD876D0EDC2D9A03559027AE0844492B539F2843A2A49C6E969C121CC64E4CDBC624167C22911A27656109643F695334F3C73694758F06E94CBC15385F181EA1B012D93C5AF375034514074C92401A455BC21C8B97AB7D8CA64B58C415B53AB4D322CF28F69882A4C0F5BC060AB09AD643311F29398205753D623EA424C7F2D38CF0E1074F1CB5A9E91B19D91A637A83E15B3DF5B7C7987A4D5FC151E9012021F00CA8E31069F9451E4B6E5D7331E85980077367990AA632009EDBE0432722A825829074953E6BB752C7D86490D7C760E7177F54806CDAC557C70C3DA151B9F30877B22FF8742BE8077C7C82A212BA077C148D90E86D5AFA15AE30362CB287D3BC318CC28FF74AA74536A73009C00E9B8A7F8AC11BEB5CB6CC965D672141DB7784B8BAFA395996FA4DF8C5C014C8B73C3302AA6542950A163AAC35D7C222B1A2CF5C9A820D682B6E98242B28B8B9D32E973B823027211784415BF27E2A0C60125295BC35007E21A3CDB4BEB6E26FF26069DC1BB675032FFFF93924870F0616BCF6679353200152369881182937565B3BF4711882C9823B800EA93CF7889B14F521C5A34C81A6354B70CF02D0C195853846505DC2E82B3F3651815480CB739AA49C55E0B763C9982C69D0669DCC76A3D262C304843FD322E973159EA358E5811D4C324914BB89DC7BB1BA61C4C2E9C25B0886B6F5ABEB827992B6798250975BE205B6252CE3DC5DC2E00D4CB427FB9338D6A19166C2978EEC81A645714765095E89799082AB42D08FD2305159B42C80B13F44AA9FEB46B8F3A5552BA534B9F26A2E5B2C00822CFA573106555C9E8C5B1475A38E4782675B9A6FFA294A7B0A2FB26AA8387C46C20363B2BE83E9AF100211B02701CC0A10220134DF6230874A83C49CA846E1106591AC21B82BB587583C33BC97BC62C53C316AA58C525750225C795424B780EA39E9E79588AA9556B2AB9BE98057046EEEC406FB4A81ED364964C1559EFA623307A891A9B7F06433681B2D5F583257E2968786A77CB6C9BF2A86AD3020C241CE4A00913BDB13F82266DD873742A3519E44C39F20874575A780370BDBC32F50F892366C3D3E5A3CBD2A1A9EDAC8D5468A4FD28D18281C4ADB485B033E76B3B604A5394EB6A280F81C93B86839C2198430558889696CA715CA0B901D762A498875DE991479A45725E12340953D95461ED32AA018E33BBA0157F3A6BD1BF07FBE2207D81166F5410AC20096E95B6F48E09190ACB51A86AD4C51AD1733729272BE79BCB14834A230F00C64E0416AF77C2F155776D6528BD0C9762B60350430A3BCC51DEA9966D791A3904A1F6284A32B4C865CC67C1B66A7F00BA3C9971AD846DF61348D2C418DE0901ED19D32582ACA784AC96A55AAE69D27B58973B4816714B2139C2DF5A380739C43CDF0A161AB8535F678FE344C5E4881BAB8785805C615711ADEA09B702201360B2479CC7FBA69868903CED7074D7F93AA7772B3359644B0A3A169A59C177CC60E8B4360978FC7C513A95C355643563BA0371D973F7A991AD099BF94239815C7041A312CC3497E9A40A5D3F9504BE107FDEA73D8769025D75479575F5A45974FAC400F0189076457A2A96D9BC2C1B19C5382C423B3333F0C9B7E26FCA8F2DB01265460A4C253EAC707B39109AF07241DD6A8FC5A9D18494C3D715424108114280F3A5C23CEF53C5507BDF3BA5EDA6327AC15B484793011241305462DADE02D4459419909075C09BA1044B896A28E17866FEBCC0A4970B1734C4EFA42780C380EAAB17C82B96D72CBAA83127FFCD7998071BDF8EC59881AC74177C1E194A20BF5A4DD71C4E03A82D6E40B70986D2A368C53C54BD7B0CCE1B5A161140E01047443497EFB4C4D3FE126686BA354B504B9537C27B21B56355BC0A91F82E94853801801AA5FD8287EB5A1BAEA773EDF16B376311DB6089AF1950A385037DEABA09ECC491F4882CD484604F304730C11EB3C561F36B85D1B1ECAE15FA1326F9EA486CE32978E9C267C70AA33E0113F4C3B618B405A160B4C93BF943C806FB41C62F83200B81AB2CC57CEE96364D86D4630BCA5A70064151545EA9D9A419E41242B6135AF3A516C8DA4447AB71478266E5BF69F2B846075C30F3368B30912BE48A00CA3365BBAE0597A93942396515A72B675C460BF4403872995AF19CC25C737F86B642ED740C9E5477C0154D05860A30C2DD49B14E8DA87600C4E8CA63401DA998D7689D9CB364397C4DD056474C2BEE16060D4408A3E19BF6FF8A331466209F0B428F0316CA89188B9C7B2AC25A7940DB7A1766799361BA984A1BB7E789B2438D6777F92A15CE7A2D156369AB819B116CD99076095C70D265651550BCC1F4307497BB77E75023AF91D31F4414B38CB64CB570AFB56701C17CB692A46A444455C8063A3616590AF6064BC337237ED1824CCD7CFB96B55FFF91D7D4714CBBA74DEE5AB48A28389458E2D43476CEA2B1A0B6EC499A83CEB8EB4C7539BD93D3AC987AD07A1C9F079850C1444D0B5C9CC619614301CD77776661357FA80F29A3CE4A8AF8DF55FB259B754504687508AC12AA81DB8CD19C93875AC5C4CF13E115221EBA49F3BE008D4B80E9EBB8AE0CA997313872010497CF9556DF06D8F766D2025B03099BAC8202C5D90B52C5397CF55BFACEB5A1C079CC02960FB976A735357DF16B90210194627A5089B1D29CA4FE8A03B4FFBB544A645C5FA95EB386408DC65AAD89CDF0285F3745EDA596D070356D350775FA7A088D732A61C1AC57306359B4A642B8AA8BB91A347152A916D33427DAB37143AE14B2D804B0FC033EE2B9D996853ACD202B2689AD945CDA48938BCDB2D8DBA5449C80891B103172381200A68BB7B505EC5A373E15DF301BB7547770A5B21A571CF4CE87B1AF38D3D38329BE9AD366159FF1919DB40233E86BB497652379B9099F68B22EA91552273AB1801CE6BA9FE7B07ACE9A81DBB09B11CC4AECA25B5090C6FD885B7E19941D8561A15CC35046EC191157C393168C80311432C899B0A0AA578460963E7767750560064B93AB148AEF2FC2FBB029C89126535C6BF2DC48A04748D23A6225EE6C3EEA9B68C65BCA2E9C804EA5BDC441BD05293B8C19D96E369B5C3459DABCB20565405824EC3F57475AA4E19DA21A8EAC0DD312E9FC10B56D6C6EF40A130844D8FD68769E2CC08288D770CD00556578D853FE4896886536AF0633EFF38C1439E3E7950091189960045B3B3B18BD45BE846AA63E4D3845BA001699FB3EF1CB24186CF884DBF62F4BC68D598BEB013F7C438C66E180500AD0579E3B0D4F4AF344ED06FDE8BF4E104753E832294A3D2E4B66BE59149006A7B95\"\n        },\n        {\n          \"tcId\": 65,\n          \"deferred\": false,\n          \"z\": \"EF0F95F630F41B3AF911A30E543822DFA6B7684FEE36956D2BCF8FF080C9FA26\",\n          \"d\": \"D7349F9AD546CFE9830E1197072B6ED9CA21E8E0423F145F1DB84A5AEBA230EC\",\n          \"ek\": \"4D09B276CC63BC453FF20B31FFDCA2FC3C34AEA706769A58AB6CC210551683C2BABC76B5CA69256816172BD926BA7C91A4DC12AC5A3FB087B6E1D3C0042BA5E0F98E5031A6E5077108153CCE342B1F832EFBBAC43A9C0B5A01283522271E649C3AE8C43C541C88603E58F99C1E8502DB34CF70AA48EAD6802844A7B25594308537BF657A07547CE1380AEB994F4F676FF6EA3B9A728AAE5B4E4FF34A5B68530C582194320E6BF76B997A8CF42819DE5B0D1C82491E741CEDA8AA6280094E332D95FA799FBBB769B2BF769798854899D67C1007FA29CD7245F727CB206456E4020F80A9600DE948DD2AB469C1883151A440C49570430D003C97642601CF010C200198ADB7ABC04379864C7FE6CC8BBC29CC1A6335869ACDE9DA9DFFA17C7B8747848CB7DF12B52E8C5EE7144F2EFC3ED00846B9F72598219793A6397E7726F8242B5EB92EBE2577E686B980F34859B07FD24084057B13A98880C93458D59BCD5AA68314EA18E80A94CD826FDBDAC1F05A472DE0872027C1CF924D5BDC9F236B1893DA61486757FC758806F580279919862C0333B15A66C138E4DC6ED50B17EAFABE9074AF14270C20806AFF0B58ED97631884435A55A8AB394EEEF5347B2640E6980C41A30A18E85A86074E03DBAC48855DE4E94538939A4264593B649E5BD981E0917C26D563D287A600EA89CEEA7021E754384157C8ABC5BE47C7DAF72E15787D1CE62723547B0AD86F852B150A25660A4A074E707884CA7934F9BEE2555A0F574B7F750DD293496FF681A09A4976671210FAC3C8C84A03F99F39A7B649A19F3E05A1D0E7A3728B253C5784F92CA7348A4BC086460A24B620845662246F1C0AB9BF91C8CB301D7DC362A3137643C92C8797CBA7E59A17DB9B99D08977D56A4F34AAEBC16FFBC8822822931FC73DC6BAB320972D96C16DAA41CCC89312E43275856856C4598313417C1B5261D9C1430A16C1D2F3CED08C862A21678A170A2E1A643E139873C878AA469F163C97AF7C7A1DA41034878D7E144DB826A7AE48B97A5A131185813D775BEA667CE0518B11429768F58BC30946766A6FE5B7BBCDDA7CE18C29F396307BB456AEC16FCB989F32660CA348A83FF36D1AF452C4584E117A3105C662135997142AB16CBB7C7D28560B087BA4B7B66A00563EFA3DA1C4764F8B7B5D128EC6202C505AC9446015620C359555268BF6AF74144EBB26351E98AF7DA51571F16A01B320595754C0DC23BBC75A8F036A5F6C3AD76907FBEC72530855891381B9F2B719429D4B6BC04F014F50FAB14131C60CBB279B61469E1B8664A38917CC308FE0C7AFF637DD4A76BAB66F08487739B20B9EF850F7F20DD83A1A47B53F00471A1617AD164AC1C04A8180BA68984B4F5DA9BBE1D0CA352509BC0211DA0B321789A61C98222508A03E0A6A7CF3849126435F36575D63B342FC2F02DA45FB8B5C9BBB6E064328550CBB6DA1AF16424A3AE9C1EBE566F4C4A883F966479C0BCB6585D3A2B1E14C3608E05AC5555454EBAF80E5C7BDE47B12F66118027E19F325024A5D51779C17D1B933A1CCE61003D018A3BBD67EA16B02EF997EF0678CB776479C52203875774C05B0EA9586FDE02964B8C3E1968B9940C88763822AE563D814BEEDE38A7D1653B2131558231AE3470608B51FB78CAA99C7252EC8AEAD74025CA8BD5C038283181E9381C73188C403A0B6F88B855FB5673B180F7581AD69513163379B34582128575DCA936A614A17C84C36222C3893D4B5F97829E132449490096605B61925844E31982BD601538739D8E7B378E180FA6A7EB9874CFBB8069447A96CB81169F3514CC20A93A42E0C9CC321EAC10B8AB21A893FC247BD557B6485A9C433390076573EEF854C2EF08AE116C58FC75CBC03BF8FE7312EA5305781CAC7273A78F01C73319B0B26670F26513E57660340563ACB16CEE7CE7CF0BA52671E23A44C85EB994A7A7BA15941F4C301D3B87E0D2A75D2EAAF1789237BB5B02930CE6AE5B965DC5E7277C8EEC0676C55436014AEFB8AA3B1BBA2D07B1F91487C99673A9135611BCC3B3546C42B23CE0A8099D6666FDEF62D402925B4561DEE3986B30C1C6278609496A474928D0A8351DF30C83CB4BF4BD826CB4319BD620C7AD5A7F1EBA1B8913A46D0365285A38C4C1574FA4287C02D71E91A43EF856ECCF60978C7B83099574EAE8C27A0571C4E51B320B34ED55E8B1E576E\",\n          \"dk\": \"CA16BB08B42E527B707E18B2ADDC64F2FB9542C89FF37B4B9187C72DC602FD83CA619836032C19D3497D49FA2AD23C0939721984DA633C14BE7300029CEB1AEE6631F236060D378852F6B1453214D1C87806E42E956286D2356B7B42731B054829B72B6A89BA4C16308A00C35D59A717E72BCFE69DB53C77D6295243FB599FB3BCA5E6C179205E15EAC020826EE8800A97EA3B51400103A362D0669550D1B56C466535B148862258EF25C3FE14A16362B5C940362B30C2E9A660BA66837071AC18D802392BCF17E62AE718288D175B0A61A3010943131070F84374A47C2EA3E690F7660E67E1B813DA13B9E52BB422A2145482C1A536D0E81BDA906A45DA576D09350FE9C223BC31D89571FFAC999EFB71FA59B29B0431FCB68B1FA37DC9585012414212883B640A10E0937CCF3BBA568A2A519B1EB02B515A26335FB52A1D6B9D122C212A913520661A795855A4D65695D4469C3766FEBA11AA4CCB77B4AAE7608A82EA2E22EABD4105075DAB6D88591A2B2B5B11918C15735DAE6546BEB95656F195F0BC70A3FA3D6FE9898BA1284CD368666C8D8A51A7440411525C3F6C694CC586CA15EAC1CD4B88A1A8CCC713233CC5946C009799DA4EC0294CAFB09D94580EE4E705FB842CDE2429694141C0062508C79D69A2515B843204B3857F77A352764A88A5677C885F00A88615E3524C7A609547184FE029EE7572DB3C821C8552FE499478DA9DD708BF602B73F180B5C801C9D7465E77C7B42A97CAA3F4CD7B62BF0AF7A7F2BA6187129012E5C7443738572C1BBDDB784459B9921B9B39D020F169BB7FC6CCC2B71C231A27462BAA34531C3838A1C13B12F353326D870D541895E259208AAC459D44C7CFC44324AC3CC2B7610C96BFAD5806692CA9F567500894A073629D66741A3EB33F56720FC80305F60791BE328DC8C28AA10097CCC03500D1601C25CE03550974F12F8E384C8B310F32A5CA60DCCD230B89788B41CCD6825251C601B54DBEE636CFD491E5B071F8A1BB599144B402C007B80C73B3AA4E6A591AFA2F108C1BDABA34FCE43866661954A046F0D5AEB0F681602490B6118C2ABB67EFF83C349933AF394185F72BCBC80F0C1751688A42E247451DA4BCE4DC5BB5204121236623095B25E91B95E0B5C196699E2CC284B88FFB11426F8944AB174DB91484A2FC7787A97172596DBBE0AF50C68CB9726C11ABB6FF6366CC280A23D457B638A307328FB5E70403EAA029D2B121118AAAA39089E655857391C0D4A85FB7AAA84448CC12A4CE428A37D5B6DE999C7CA82879112E17EAC572001215EB8F3F07C153E6B251E699B3103855C747A6B2A1FF44ADEE996EE5F545373C972D431854DA74DD744CB3C7CCE52A31E332112D70736EE859CD47C381691C9275B8FBB46299A48BEC07506D65337BF64CC5F50E2C3A2FFEE688AF4B52A4DAB2E2E4267851AF5C2CB55255629036674E0185E069266667B9472C8DCDA95B4B16B5845940CB7A378E3CCE2FA9CCDB787415F58C30C46E691B6B6DC4A8B0C892DFBC7699599CE25812A478874568260AB482C5F69D7E773CD928BEDCE7067217C0454310B1694264E3A9D3E41300B87543B4235CF432597A5638EA41637CA894080437766C472047BF5BBFBAEA557B658174A576697613A8A73254DA332F6B232A65B9017928288A44FCF8889CE79718A329CED152E35B4E4EDCCE1B197DACC7942D59C2B0E80A0FC74FC726BB951898AA15CC1B106A383AC10EFB4D48445F9CF59C4BA17D5817618BB27879067FE2615EAD1A2BDABA6C17D422DCA06C3C4578B873B17714A6B8F2AFAF8CB445F04238C954B38B28D1028D451A25C3797872601B98543029F57671F52F8F25AE3C88144D011B22B88F9E60841C3BB52205A00EB24D3254B8C37BB845E10A14B528A8D6C1D07B7AE4E66E4F88C26D116A853B4E41ECC6AFFCA83068188189063030B5BA854541433CC454133FA2B5A3407852A64D9277883DA9AED1D982695854E8D3B114DA709E1BC3B926170C4A2ADECBCF8402A3D9253851274BFED70BDBC817DDE56A166B61DA0793AE0818159C0626BAB102CAC3EAA05D8F996D6EC01FE5E9061147A98EF4A5BA361508B385A5060D5FD0C462F340B943914D117B7EB032348003E89C3D888485E3FA9A3489CACD855F4E55864D09B276CC63BC453FF20B31FFDCA2FC3C34AEA706769A58AB6CC210551683C2BABC76B5CA69256816172BD926BA7C91A4DC12AC5A3FB087B6E1D3C0042BA5E0F98E5031A6E5077108153CCE342B1F832EFBBAC43A9C0B5A01283522271E649C3AE8C43C541C88603E58F99C1E8502DB34CF70AA48EAD6802844A7B25594308537BF657A07547CE1380AEB994F4F676FF6EA3B9A728AAE5B4E4FF34A5B68530C582194320E6BF76B997A8CF42819DE5B0D1C82491E741CEDA8AA6280094E332D95FA799FBBB769B2BF769798854899D67C1007FA29CD7245F727CB206456E4020F80A9600DE948DD2AB469C1883151A440C49570430D003C97642601CF010C200198ADB7ABC04379864C7FE6CC8BBC29CC1A6335869ACDE9DA9DFFA17C7B8747848CB7DF12B52E8C5EE7144F2EFC3ED00846B9F72598219793A6397E7726F8242B5EB92EBE2577E686B980F34859B07FD24084057B13A98880C93458D59BCD5AA68314EA18E80A94CD826FDBDAC1F05A472DE0872027C1CF924D5BDC9F236B1893DA61486757FC758806F580279919862C0333B15A66C138E4DC6ED50B17EAFABE9074AF14270C20806AFF0B58ED97631884435A55A8AB394EEEF5347B2640E6980C41A30A18E85A86074E03DBAC48855DE4E94538939A4264593B649E5BD981E0917C26D563D287A600EA89CEEA7021E754384157C8ABC5BE47C7DAF72E15787D1CE62723547B0AD86F852B150A25660A4A074E707884CA7934F9BEE2555A0F574B7F750DD293496FF681A09A4976671210FAC3C8C84A03F99F39A7B649A19F3E05A1D0E7A3728B253C5784F92CA7348A4BC086460A24B620845662246F1C0AB9BF91C8CB301D7DC362A3137643C92C8797CBA7E59A17DB9B99D08977D56A4F34AAEBC16FFBC8822822931FC73DC6BAB320972D96C16DAA41CCC89312E43275856856C4598313417C1B5261D9C1430A16C1D2F3CED08C862A21678A170A2E1A643E139873C878AA469F163C97AF7C7A1DA41034878D7E144DB826A7AE48B97A5A131185813D775BEA667CE0518B11429768F58BC30946766A6FE5B7BBCDDA7CE18C29F396307BB456AEC16FCB989F32660CA348A83FF36D1AF452C4584E117A3105C662135997142AB16CBB7C7D28560B087BA4B7B66A00563EFA3DA1C4764F8B7B5D128EC6202C505AC9446015620C359555268BF6AF74144EBB26351E98AF7DA51571F16A01B320595754C0DC23BBC75A8F036A5F6C3AD76907FBEC72530855891381B9F2B719429D4B6BC04F014F50FAB14131C60CBB279B61469E1B8664A38917CC308FE0C7AFF637DD4A76BAB66F08487739B20B9EF850F7F20DD83A1A47B53F00471A1617AD164AC1C04A8180BA68984B4F5DA9BBE1D0CA352509BC0211DA0B321789A61C98222508A03E0A6A7CF3849126435F36575D63B342FC2F02DA45FB8B5C9BBB6E064328550CBB6DA1AF16424A3AE9C1EBE566F4C4A883F966479C0BCB6585D3A2B1E14C3608E05AC5555454EBAF80E5C7BDE47B12F66118027E19F325024A5D51779C17D1B933A1CCE61003D018A3BBD67EA16B02EF997EF0678CB776479C52203875774C05B0EA9586FDE02964B8C3E1968B9940C88763822AE563D814BEEDE38A7D1653B2131558231AE3470608B51FB78CAA99C7252EC8AEAD74025CA8BD5C038283181E9381C73188C403A0B6F88B855FB5673B180F7581AD69513163379B34582128575DCA936A614A17C84C36222C3893D4B5F97829E132449490096605B61925844E31982BD601538739D8E7B378E180FA6A7EB9874CFBB8069447A96CB81169F3514CC20A93A42E0C9CC321EAC10B8AB21A893FC247BD557B6485A9C433390076573EEF854C2EF08AE116C58FC75CBC03BF8FE7312EA5305781CAC7273A78F01C73319B0B26670F26513E57660340563ACB16CEE7CE7CF0BA52671E23A44C85EB994A7A7BA15941F4C301D3B87E0D2A75D2EAAF1789237BB5B02930CE6AE5B965DC5E7277C8EEC0676C55436014AEFB8AA3B1BBA2D07B1F91487C99673A9135611BCC3B3546C42B23CE0A8099D6666FDEF62D402925B4561DEE3986B30C1C6278609496A474928D0A8351DF30C83CB4BF4BD826CB4319BD620C7AD5A7F1EBA1B8913A46D0365285A38C4C1574FA4287C02D71E91A43EF856ECCF60978C7B83099574EAE8C27A0571C4E51B320B34ED55E8B1E576E82D819925EC1B1F45E255B12DE1637697CDDD47F41DDAC13484983D75BAEDFB2EF0F95F630F41B3AF911A30E543822DFA6B7684FEE36956D2BCF8FF080C9FA26\"\n        },\n        {\n          \"tcId\": 66,\n          \"deferred\": false,\n          \"z\": \"DDD4871080BD4F761D972085851DE0A0408A2F5EEC3CD3786297A782402CA440\",\n          \"d\": \"F05117E932CA0E0C202732DFD4F674BF5848219A76C64A0650C27E2E55095513\",\n          \"ek\": \"FB28C9266161FFA370AF3C5C163A9B187A5D2499115A1120F2B84D71948E00A7969685C8FB6B8185566ECF337E1E20AF4E4A87D1F4BEEB1BBAEA51499960683C45B53057891A288E5A8393C4F19A06C222D4C9AFEE5AA984B6798D45A3F271793E77A5404677B08A1BC5D3CAB9D9A20A4880A11B71B7D4221C1302B8BC24FE65C4C63BC7D1B80D8BB40A7FB535C672BA32496904A9976B328A2002B14E9A071713CBD9B48E55359D451C3EEC47C4A7B9CE61AA5A9516C34009A8085A98725C13949644A72A338DBB7621530858FA1B40D6BE5DA89E9A422A08031CA3C003A0E31AFC4B36D97B4508BA3537DB1B16D1A5B5A98E6BC1CB70696F887940A60498E276C1C900036A30735B20B9762A3517C136A9DC20A8F5CD73BBB047A00DDAC6A6DB40BCAE42C1D6329FF54B9C37F82CBE6214C2A99E31DB80AFDA9BAF06916BD3CF83643D27828130495CA2FC67EA441E38A8032CF0003926901245973EA549EF48A9C65ACDD5E25D5F9834CCA805AEC7CD9F687D5CEB8C2CF04EB6A1356B68AA7AB9CCE7452A20C5344F11BA6197343886A80798098C63AC5AA6095FA2A1D2B0A07195ABB3E98057953AA0B20C00FC06FE553FEDE713DD4662DBDA4C7806B44904411D85C8B9C57912A72A2D25460467CA25027BC74C3066C7A941858EE4D04026914B45354D342B6643A280CAC8C9ED60B9AE021D38303799387D12A98419C26010FB9492262002B0002FDB167FB668B2D8C453987E83F570A43AABDFA5762015B9FC196FFF6542B189395BCB5C4AE598A858597622240E01856B17511C480627B4818C413ED065C68F359266008E888B9EC435A906C6721CF8964885A160C0512FC4B9B454A10E9954048B3212AA9C37A27D2697B1AD530FB33974AB25AAA9F33B85358454610E94410D47F85920F2A7F5D13C74CA4C6108B768A476C5BACB57F17AFADB60D437C21D397A09929B2E97915254A0FAB1848779726D766CE6C83639C4172DD5A1306CCCBB1491D56C1A5FB7CB5FC7836F0765246815F05A2A13385920D0B2C518395AA997E1D33A2AF588B7EAC1DEF4BA688555EC4961596873CD0A0A443551269C73F0B97B6F8CBBDD45BC5A15356BE370FAD74805032940AC0E9C2269C0FAAC67A647FED78E83094D08F58C23745E2BA70366F24A42635A944771C3E13C8EA25B8BD6B8B0783487F1A7A4A15776DAB44EF4192880BAE74C62DBA92A24782CB91237CEDA6E84A0C68244B9CD350DA0A67CA5477EBDFC4D40F59EAE1A88E9043653F5882CF614611641380CBAB15B6CB4189491ACA9D1BC22C56928A7B37E678A4819D48A7D27887F12B4E3878E8318BFFCD93B2B9C4ADA1676BB72B93DE19295DB59909315DB1209E3A02193C1126DC30BF77C6DF59AA4EF921020253FE2045BAE6760C206A7FDDB596EF802B1F3C620D353CDB008AA0A4B8393744E25A34ADAA31D2313B480C29DCC8DB110049DF796DF298CEA91015C9686FF3A300A7298F4B7237A893E02BC373641CA88F2345CCB4A6E7441D30B244814A665F08A18146AB65C3761BC5A87860B2E20482EC89D444180D10337C45C702D82A51DD036B5EAB0FA81B017BA82E16AACDB3A96A53775B9874EE8079CEA7845C3CABE0C847B7B4BAB1FE0680B12986D821968579D963703D90AB8DE020A7CF28DA8395887B2ABA5C09BD7102B8662554D1C472578A9F80CC15F431F18980BA1E26754243102D03D42E69C52AC25EB3A19FCF57FF9E1429E78CF6AE0A501AB3EC26C0D729B142636B873E9B68E277370366C0FE0543E44C08716C4F9B06546B1CB3DD243B505B1A15976B33788E8D1987F810D5797A8E9CBA8B126923A1A81855151780CA9CD4055D9495603A84CD7886BBFBCAFF499066496C967501DF3D861B97C5D250016BD3B3A18946C0CACAC1238CD235A6A609611EAE792E0560B831B636309517AF489A30269E172BBE7470C2D158F83C4B20F15C524025D6CF164200B3870663809A00E6A982835C87F80A68AFC1B5393B006D220A0AA26B9666690549523A0A9711E03B2419914FF76CA69A62671260B34F1C1A11A6E64067908839B87D2940D433162393A6F65B08E4CA643201023C63DD3C3808E9625CFF4C346491E34153937589310437069622208C646AB58584DF10F9930BA2F761DE4C2564BAE2B31A9A645135536FCB58B94489E4D993C9FBD4A89198AD91BE052B5E8FF\",\n          \"dk\": \"A6247A7F4938C4589C972C928D7C65B74634ABE136E1C732A094A539E4892780BF56A5A7CA52AFFAC643B7168B997809AB95C8DD4740954164819C4D94453053634316F3186DCA4A03E813BCF763E6436E05DA840EAC0FB65B11D2D97EA80B156966C5A0DC43799B74F3797D483486E4647C8EA61569880774D68C9EE2A5907910839A95E8E33CB10B2AF59C22128B0C8BA9AA7D0B7A48E26C6E157D58122A63E571B059073AF35AF40282D1C8AC2ED5B15B2C980D6437C27212B5CBCDE888BD8F4B1B3AE398182243468C98F453569C417CF3C043BE9A9BBC03D058212523F0A6A2A263381967A562B63E7C1A83A4BBD1D86226B5083F8B54D93413350B39270C9E784430AD30752ACB0CE0F2B7300B478A3801C1133A84009106DBA42AEC03AE413F137812A35B4CB29207A7FC332BD07064019AE8CA3F6A723191058D88A460D40522FFE0841AD5507B1942434939260228CD993266DA1C35B920012C0E349C224D492DD6D197CE7473FA25187AB87C192A5382FC3DB61C817DF62221666D6D4858F807C852853EB1AA8A7DFB4A79935711698572E12485294DF538257F44AB92A60D5D23AC91F56E138025BE0C93F995BB3A9620ED0A7588857B4F847F996C9D7DF40BEB05C87FF79BCBF222956CCA23F377FC36140FA379CB89365532490D470C3C5B4CB8F3560A7A0C7FF9A19AA0B0E03C100B5090AB42A9C652399FE79716B61921DA7211ECABE486A3E95B3EC0E79A4DB614F5282F2C8087F8A1CCEE9417D4F99C108202C92499DACA10E3AA9B46D254BD8593316B9D220A96496372DEE61B51E93D51E9AAFDE006AC267037044E92A3440A469BE8AB36C3A70B070C43BF889F1DAA6E975CA464F32BBA725D75016B86C8089D9338C9816FDA6220169CBD445C7C78136E9F56A431957068373DD3480F7AF88ECE4941A2FC3BA9C2A474FC1D583A59AA42795365B6C0954F3AD810F55B5B0B959F414137B7C3AC0351812E9B00E8B6BE2953C7AA839F24F02D8CDB356CC64B5A361EEF29A2FE12117D9CCB86D4A399F51CA8D796A29778474838D903830706A854E83BD0267C7CD43B927B83DBB29C63E32FF5ACAF1A2A0C50609F4381C32E78B583808841F6300AC40302CCAB18A945FD0CB510588796C61540FC559800A7A1657736B347C50657E559685A226B19F62B34050FA4D76CCEB3A9BDF5A3815A7F4558AAF108958806C16A264132968DC63CC40342239E7640BD394CF418CC3298B7ABE9CFEFFC1714E96486CC73BA380E5A79900AD19558C68EF1BCC3DADC3E7829BC7BC09F407C6384E51620492E5B643B10B45E3BE52CA851709582C9DE447A501A07026528239054901882F5B74E6DC791AA9397DE9403D5519F1368084EC65D8010390F23C7DF604CAD8092E950901F9860A267578AC5043773616BD1792307AC8626201CE2A4FD5613CD97A074F3476094BDA4EC32A161855A4888223327978A8F74625C74A572C37C027E619C7D5577335B3D61E3B53191517C5A50FE62C63A08B6ED65C054EA98CE22C7464CA2DB3C23724C9868007133A235A36A7C56BA237FB58237F7C61E4A66F7B51A2751CF3706367413AD57A01B687107A513A46ED70025B41B785354BF986D4687369F97759D12551C6BB203A609F28931DDC076E6E38FFD083EF4E99B8C3919DE18BC7601260CE74EB75779FD1C6EDD4C843F770F6504B4C468A80C8C3C37D80902C736A15539406A7BF01206173066D29902348708799C2A7C88B751D62BD2760488357836B4019DA55B4A3050242791EF3C94AAC0A821BB71A90840B7E06F842398CD50282A15718E29BB4B75796AE81C76503629056CAABB2A0A319761C10EAEBA23BCD3033D911ADF754C25EB3923331BA0C332CF357758469D765A2C492565923872FB1322F5E1672E010DFEB7815B940595CB4FB6B64FDE59A63C689BE4B7828458B31BB5103EC4C8F1F905FEEC2AD1D10C7689C5C0528A56E4444C958107868E477662C25071E28404AFA86411910D33B618276A5CE51850F3B67584949F75651180298E3C791ACA85590E6BC59C0C8365DC6D5AE02D34D91B1E6C7BEFCA8C7F042070E32BD4883ED1D15128D516A40235A086A6D385784C457FEEB1C704C426CA4C48155445E414200A7CB6A0B9C0DF343CBBF95C7655B4FB28C9266161FFA370AF3C5C163A9B187A5D2499115A1120F2B84D71948E00A7969685C8FB6B8185566ECF337E1E20AF4E4A87D1F4BEEB1BBAEA51499960683C45B53057891A288E5A8393C4F19A06C222D4C9AFEE5AA984B6798D45A3F271793E77A5404677B08A1BC5D3CAB9D9A20A4880A11B71B7D4221C1302B8BC24FE65C4C63BC7D1B80D8BB40A7FB535C672BA32496904A9976B328A2002B14E9A071713CBD9B48E55359D451C3EEC47C4A7B9CE61AA5A9516C34009A8085A98725C13949644A72A338DBB7621530858FA1B40D6BE5DA89E9A422A08031CA3C003A0E31AFC4B36D97B4508BA3537DB1B16D1A5B5A98E6BC1CB70696F887940A60498E276C1C900036A30735B20B9762A3517C136A9DC20A8F5CD73BBB047A00DDAC6A6DB40BCAE42C1D6329FF54B9C37F82CBE6214C2A99E31DB80AFDA9BAF06916BD3CF83643D27828130495CA2FC67EA441E38A8032CF0003926901245973EA549EF48A9C65ACDD5E25D5F9834CCA805AEC7CD9F687D5CEB8C2CF04EB6A1356B68AA7AB9CCE7452A20C5344F11BA6197343886A80798098C63AC5AA6095FA2A1D2B0A07195ABB3E98057953AA0B20C00FC06FE553FEDE713DD4662DBDA4C7806B44904411D85C8B9C57912A72A2D25460467CA25027BC74C3066C7A941858EE4D04026914B45354D342B6643A280CAC8C9ED60B9AE021D38303799387D12A98419C26010FB9492262002B0002FDB167FB668B2D8C453987E83F570A43AABDFA5762015B9FC196FFF6542B189395BCB5C4AE598A858597622240E01856B17511C480627B4818C413ED065C68F359266008E888B9EC435A906C6721CF8964885A160C0512FC4B9B454A10E9954048B3212AA9C37A27D2697B1AD530FB33974AB25AAA9F33B85358454610E94410D47F85920F2A7F5D13C74CA4C6108B768A476C5BACB57F17AFADB60D437C21D397A09929B2E97915254A0FAB1848779726D766CE6C83639C4172DD5A1306CCCBB1491D56C1A5FB7CB5FC7836F0765246815F05A2A13385920D0B2C518395AA997E1D33A2AF588B7EAC1DEF4BA688555EC4961596873CD0A0A443551269C73F0B97B6F8CBBDD45BC5A15356BE370FAD74805032940AC0E9C2269C0FAAC67A647FED78E83094D08F58C23745E2BA70366F24A42635A944771C3E13C8EA25B8BD6B8B0783487F1A7A4A15776DAB44EF4192880BAE74C62DBA92A24782CB91237CEDA6E84A0C68244B9CD350DA0A67CA5477EBDFC4D40F59EAE1A88E9043653F5882CF614611641380CBAB15B6CB4189491ACA9D1BC22C56928A7B37E678A4819D48A7D27887F12B4E3878E8318BFFCD93B2B9C4ADA1676BB72B93DE19295DB59909315DB1209E3A02193C1126DC30BF77C6DF59AA4EF921020253FE2045BAE6760C206A7FDDB596EF802B1F3C620D353CDB008AA0A4B8393744E25A34ADAA31D2313B480C29DCC8DB110049DF796DF298CEA91015C9686FF3A300A7298F4B7237A893E02BC373641CA88F2345CCB4A6E7441D30B244814A665F08A18146AB65C3761BC5A87860B2E20482EC89D444180D10337C45C702D82A51DD036B5EAB0FA81B017BA82E16AACDB3A96A53775B9874EE8079CEA7845C3CABE0C847B7B4BAB1FE0680B12986D821968579D963703D90AB8DE020A7CF28DA8395887B2ABA5C09BD7102B8662554D1C472578A9F80CC15F431F18980BA1E26754243102D03D42E69C52AC25EB3A19FCF57FF9E1429E78CF6AE0A501AB3EC26C0D729B142636B873E9B68E277370366C0FE0543E44C08716C4F9B06546B1CB3DD243B505B1A15976B33788E8D1987F810D5797A8E9CBA8B126923A1A81855151780CA9CD4055D9495603A84CD7886BBFBCAFF499066496C967501DF3D861B97C5D250016BD3B3A18946C0CACAC1238CD235A6A609611EAE792E0560B831B636309517AF489A30269E172BBE7470C2D158F83C4B20F15C524025D6CF164200B3870663809A00E6A982835C87F80A68AFC1B5393B006D220A0AA26B9666690549523A0A9711E03B2419914FF76CA69A62671260B34F1C1A11A6E64067908839B87D2940D433162393A6F65B08E4CA643201023C63DD3C3808E9625CFF4C346491E34153937589310437069622208C646AB58584DF10F9930BA2F761DE4C2564BAE2B31A9A645135536FCB58B94489E4D993C9FBD4A89198AD91BE052B5E8FFF2F75EA69691E4E53E952F98536718602B96B7E5A2FB218648F9353EA65FEABCDDD4871080BD4F761D972085851DE0A0408A2F5EEC3CD3786297A782402CA440\"\n        },\n        {\n          \"tcId\": 67,\n          \"deferred\": false,\n          \"z\": \"FA29BDC28D989B8C4BE84706A3CF21B36A1C6E355C88A361C7664818E4BC8E03\",\n          \"d\": \"A405D9B07C5771A5BBDA2BE9F8A40D9566CAD7DA1761ED8076A289063DB4A8E2\",\n          \"ek\": \"DD0C664328C53E5B29F9A9CC55D97BA265A18C02AB73427307E78AC23C51A56478CAA21DB400A3AB081A0532729C40BE3FF9ACA2DA1799E29BB2A8325F1A9E7BB99D44F3C27427816B3BBAC0FB3DB6B23B7323A8CF4587D505B1D1FA9D71696064CB6A7D806872D68040714D94162658780295FC056B269D4F16AA4D98A5245C7B33462129130425712798C879079217CC3195E5CA67D137865181B6C2F92EAD04945E97720DE0CF7531BC46847D68E3C13840B94DB2242A048F0F22331050950B8A4B3FAB51A487134A2C9A95FB659342867D252FAF091D1D966836F1CE964428C63CA3950BBCC3D8A800191FF628680D23A8EBF6BE727225E4B91602E46949889D910CC46D6418411B394AF072FF49B894A1586AC7C1823BB89AD2997C726C3FBC65609CC37656BEC80A6B10336A346412FE34075DA69D829B8B937A87F76968BC93BF5D5639AD76448E084DD0D02AF4A7028D7C0892A9728CF1114622494D8B28D05065E6DA0813043504293A17D29273572E34A14816A01F5DF58BDF44AA1AE8CD0C6C6622CBBC07540405D5AAE8C77BB3418A91B982BAAB4F8B388224EC3E345764D9E66C2AFAA27B924A07039720D7B73084098457C488137B75339F1D7A6919A29FCAF71F4DB8A17AF6161488C02813191AA2B98FA51A6FA07BE8F288C51B538292683A4C52C7F7B2863C52C0843452B583CB583B98414B4CF97185082768855206871CC76A0A409802E0DC23036B3586D9991D241FDCE67D4986CECED74EA7E77BCC479EE4768952E0297568528AA1712468B267D474D069CAF9CB11E5B0CD23DA09D5095349E3614194B1C63596E7E65BEB6769516B7B0759C8FEA02C7AAA1466A7B41C759135965ACE8857074B95DD6A5ED9D36FDABB83341A841DB163C1332881322F5D2374EE11ACB0616A6B1CCD9AC7CA6673619BCCABAAE562D4C433147A2802C26B6431BDD73A455CF8CC242174AB5123BF9C26F8280E5755A4BDFC1A98CBBB92C152FD11655EE4459ADB2F163928EAF24ED09C72839145C96179D8C94E6B3227AC793F5FD990DD638ED150020934894684A38BB1AA859A3AAFC49EC5A53B8646CF60F5585F043CD71B13F9BA41DB563677B04EC6D8270FB55C78F8A58ED494D9348B4A774930C00752121081514596D5B86FC0CEFEFB4AFD2682EE056BEBCC0CC9446CC3C1A0F5F34DCCE8AFF0949FF16005D121275A5915C6D4B9407357AFFAA09CF6783ABB260FD53A333879433B45FF25B9D71A500BE315D2635DD4F42B47881803666441871083859D266B96C1327F96B40454D17ED5363A27CB612A69064B268DA9185B4506119D7A71FA1386AF6BA9789AC06E9A0F04DA5C6CF478F18044457418E0FC986681B81BE1A7AC1583AE95155DB64FDCF442E5EB39515B06DB280A23AA5DE2C19A1051A4F9404F712955ADD66A9D1A7E963BAE57BA75190B7447B70EC0170EC0C2CEF480132B375D3991B90B729F9A4356F062C00D030BF8AC4A82032D5F42B7D47B6009B274C4D2A5DD5553D29AC86F148ADE931A81534226EAAD33612AE89BAAB7729F3855339625742AEBAC2B042AF2F5B4F3A017A74342501C274832B314BCCAADF59DC52A0ADFC061F2256875D960CB654CD78BC46C709B2B9133F8801CD6766800E12516D80CA41011D68733901AC540533E0CE532CC82AEDB5CA0DAB476CC5A81188B54E0AC9FE66A1D977A6631519810128F98C3044BAA5991920C381BCF3D63B785701421733FA25C521DC10BA007040B917E55B863F95A1A3E924D3FD0AD3975662D38476BD3B47E46769820C3BF4705DF9A89DAA53A58AA77B65258A444B2002B681031BA765659D9C18D92D826733BB575705D6D7C7DE3D19240339AB4B12BBAD51ECF8638E6456E5CB350FD551E5A5C67DF050DC7E7866F4A10989C40BD3C22FE480343E07DB22734CE89870B6804273447A85169A170BC53B861B9545CC186501E8A541575A480562E5D7198B4D905B8EA51A48688A8E488A2A36F7EB15AAA398E6BF44B69622A14B3473B0AC09401223CE775761B8931D53D25E0AD51426970403B0A243E9307502574B1D6323C11F7CB5D855ADE147211438AA7FAB0DF89CEE8286618D8181DE196A311390BF171EDA88DAF1C0FBF540EF20504209482BF3B7D023100D8795634BC28CCE86A4131048041A23549B016EF00A31156458CC5808590AD8FD2EF59DFCC0C\",\n          \"dk\": \"73831E2C2993800A26266635B586921217B9AF812D5656B9CADA071A384F69D48A210660F51C4AE1F2A6F2E18AC7FC2AF9901318A56E36A483662579736943B7855BCD664BC7441FDEC83DAD5C54187A2FEE338DC9A4AA55780F07F3A94ACA4DAF9145FB099F05648DAC3C19EF450EFF4633500B9205424C2BC4081A017D52BC3302B28BC53668185B147B117C9A7C427249037CC165134B6A9A4663CB13AB0DA3B501C67866201485C5645D4006F929AA4A68831038961B61C950110106CC98C03287D291B080A7212BB4A7AF92AB3A2A06EE19200A531040949C5D57B2D8B09D637638B3690C4F1457A1E9A5BAF903DEEB894300984B76A03F0116D570CDE59981F10A2DD8590EC91765ADF252183C10FBA093004D5700715215427F2FC85D700747C8029B9CAC9A1A4AB8E0641D124540DCC3C1DDD62DBFC89D884C7DA2E515CA87A92DC99B1BA97B0596153EB53D4B8818D2644F638545D94258F5441D5C940A7CDBB3B3E66D3A886457316B15808F0AD71421C34266073B1500CE5279315A00A17FACAA04660A556266C3F44275C67B49793CC0679DE3B29BDAE8C9B15458AFB9B2395547C3DA2FA6010A8171001A50863376C172E64F51F87354C99465796789A854F8362C78588DCFCC98250C4524E51A401203E5D4680C5261CA3440C3077CCC6AAC3E809315781C9081629C4199D925679F02B70A200B3F8C98A3E0B1D3B0CF15442E55A76EF25C0B3734993510B7465C0081B82BA8136C7251C40EFAA6E0819046D1CBAE353A9C8547B9E160DFBCCE85627E2B16C0E5A51DBB50565B3A81DCD27FC9698BB742356CCA03CBA3678028534FD0B1B3C852F734ABE2452BC583A103C3C09445396F1A01F3FCCD951299509B2EE2A7468F15B927B0B2D435B56AC12C548C8107EA5D58682B481A568DB4A4B1209E9E36B879D31876F708A039A39DF58A0F0A2C04A19622C359FD395C99B3C5F497304CC11F5FAAB83842B7DF06344657243968156AC67E7938900D636BFB1C4224F677E6D119827ACBE0BA6E32C590AAFB140A041714B5BD3E06A2D7851BF91098C1647133118CC747AF18B8ADCF01C530C91E805966427A3EC4234272BB9D38A8908285C46EB759F176AE6B7395010878089A21C3FB5132AA789B92C7724C8217E9A5E66923552C4514A915C0B43499F8192324A50BE4679C586A646CB0B9808659792984726EE2420B5FB102B96A3D7898B7C98900913237D5D8885B9580556549C1B59CB4F4ACDB481D92A8CC5D447282F5C7BBA6BD0C7CA0D8EA2D3BDA78633C8FEA1561DE37AE94D51C24414F9178C224A92E79138FCD0337BF00121A51BCF5D27177B0CAD3A6665EB77033B10A00037B908062D381AE5C84C711D4B5CA9888180A0FA742B506D86C03A0C54CA488FA404BC0493C47D531D420360DEAB873C000A87340A2915073429C001A088B212EE46A80852C3B839561CFE5A336557EB689BDD3CB75C70C6FE279AB134A6ED3B20A03750C1CC30BB1D15FCFB37105B34F34026B660905D640BF06D097F0C06BB7E6C628BBBA9386CE6AC212A3370AC4FA65C64C5B235BAF13011E7928B896D78754335E2EF90AC5DB53DF83BFC6340D85D444D469C3B9A329567243E1A2B900A76C756C46E26C6936A0C772F88AA38C03D2137E2BA4179D228F04229F5AF755B3C7128AA16BFFB9354395B575D81EA4F266407A206C0CAF14D5151CC15C178B7D567B8C7DB637FB26A62C048CE9049710B07AD28B6168756234539952282D521313AFA65959432E7235C9991487AAE661B6B7108D48C9E4760D6315C14C1725CCF69CE69C2E407799DB8507A0C66CBE15BC65E4692CD5910B5975A3E3416204949C601F55A438B104026D36A71B5B1399BA6DED58166F210847D863D302AB4C915E969317D806643BB85912B9A0C07AC80FFB588ED86B359840D8B58EEAA0494266A11ACC201C6130DCD523A2C5CE2262A243665AAA45BA30D2CB06D4850CF65CBBFC772E0C20F9F652AB92950F0A3B33F448D6C014F5548A241B53B2C348E494C883409B99B257367BAC0CD42037921F8298647D91C06FB1589A17C1D0A99458938570C871B7C865C98734E1A88C4C112294913F3F0A35E8A26B17F85AFE567780B919C0D94C6ABC5D6EF1C4318A6EF21A1F8FA75653F612DD0C664328C53E5B29F9A9CC55D97BA265A18C02AB73427307E78AC23C51A56478CAA21DB400A3AB081A0532729C40BE3FF9ACA2DA1799E29BB2A8325F1A9E7BB99D44F3C27427816B3BBAC0FB3DB6B23B7323A8CF4587D505B1D1FA9D71696064CB6A7D806872D68040714D94162658780295FC056B269D4F16AA4D98A5245C7B33462129130425712798C879079217CC3195E5CA67D137865181B6C2F92EAD04945E97720DE0CF7531BC46847D68E3C13840B94DB2242A048F0F22331050950B8A4B3FAB51A487134A2C9A95FB659342867D252FAF091D1D966836F1CE964428C63CA3950BBCC3D8A800191FF628680D23A8EBF6BE727225E4B91602E46949889D910CC46D6418411B394AF072FF49B894A1586AC7C1823BB89AD2997C726C3FBC65609CC37656BEC80A6B10336A346412FE34075DA69D829B8B937A87F76968BC93BF5D5639AD76448E084DD0D02AF4A7028D7C0892A9728CF1114622494D8B28D05065E6DA0813043504293A17D29273572E34A14816A01F5DF58BDF44AA1AE8CD0C6C6622CBBC07540405D5AAE8C77BB3418A91B982BAAB4F8B388224EC3E345764D9E66C2AFAA27B924A07039720D7B73084098457C488137B75339F1D7A6919A29FCAF71F4DB8A17AF6161488C02813191AA2B98FA51A6FA07BE8F288C51B538292683A4C52C7F7B2863C52C0843452B583CB583B98414B4CF97185082768855206871CC76A0A409802E0DC23036B3586D9991D241FDCE67D4986CECED74EA7E77BCC479EE4768952E0297568528AA1712468B267D474D069CAF9CB11E5B0CD23DA09D5095349E3614194B1C63596E7E65BEB6769516B7B0759C8FEA02C7AAA1466A7B41C759135965ACE8857074B95DD6A5ED9D36FDABB83341A841DB163C1332881322F5D2374EE11ACB0616A6B1CCD9AC7CA6673619BCCABAAE562D4C433147A2802C26B6431BDD73A455CF8CC242174AB5123BF9C26F8280E5755A4BDFC1A98CBBB92C152FD11655EE4459ADB2F163928EAF24ED09C72839145C96179D8C94E6B3227AC793F5FD990DD638ED150020934894684A38BB1AA859A3AAFC49EC5A53B8646CF60F5585F043CD71B13F9BA41DB563677B04EC6D8270FB55C78F8A58ED494D9348B4A774930C00752121081514596D5B86FC0CEFEFB4AFD2682EE056BEBCC0CC9446CC3C1A0F5F34DCCE8AFF0949FF16005D121275A5915C6D4B9407357AFFAA09CF6783ABB260FD53A333879433B45FF25B9D71A500BE315D2635DD4F42B47881803666441871083859D266B96C1327F96B40454D17ED5363A27CB612A69064B268DA9185B4506119D7A71FA1386AF6BA9789AC06E9A0F04DA5C6CF478F18044457418E0FC986681B81BE1A7AC1583AE95155DB64FDCF442E5EB39515B06DB280A23AA5DE2C19A1051A4F9404F712955ADD66A9D1A7E963BAE57BA75190B7447B70EC0170EC0C2CEF480132B375D3991B90B729F9A4356F062C00D030BF8AC4A82032D5F42B7D47B6009B274C4D2A5DD5553D29AC86F148ADE931A81534226EAAD33612AE89BAAB7729F3855339625742AEBAC2B042AF2F5B4F3A017A74342501C274832B314BCCAADF59DC52A0ADFC061F2256875D960CB654CD78BC46C709B2B9133F8801CD6766800E12516D80CA41011D68733901AC540533E0CE532CC82AEDB5CA0DAB476CC5A81188B54E0AC9FE66A1D977A6631519810128F98C3044BAA5991920C381BCF3D63B785701421733FA25C521DC10BA007040B917E55B863F95A1A3E924D3FD0AD3975662D38476BD3B47E46769820C3BF4705DF9A89DAA53A58AA77B65258A444B2002B681031BA765659D9C18D92D826733BB575705D6D7C7DE3D19240339AB4B12BBAD51ECF8638E6456E5CB350FD551E5A5C67DF050DC7E7866F4A10989C40BD3C22FE480343E07DB22734CE89870B6804273447A85169A170BC53B861B9545CC186501E8A541575A480562E5D7198B4D905B8EA51A48688A8E488A2A36F7EB15AAA398E6BF44B69622A14B3473B0AC09401223CE775761B8931D53D25E0AD51426970403B0A243E9307502574B1D6323C11F7CB5D855ADE147211438AA7FAB0DF89CEE8286618D8181DE196A311390BF171EDA88DAF1C0FBF540EF20504209482BF3B7D023100D8795634BC28CCE86A4131048041A23549B016EF00A31156458CC5808590AD8FD2EF59DFCC0C3D74CF5CC0859F5089855A7EA2267CDBE04185599344C8E93EFCB5B3DC588FC6FA29BDC28D989B8C4BE84706A3CF21B36A1C6E355C88A361C7664818E4BC8E03\"\n        },\n        {\n          \"tcId\": 68,\n          \"deferred\": false,\n          \"z\": \"08FED872D91297D8059743D3E7B6EE47548357E7F882B5BFE2F04314187ED424\",\n          \"d\": \"E66F17317C40783CE0594CFB5920FF86062591C5EA4254021495749642C0D968\",\n          \"ek\": \"E067AADA829618E4C683C404EA51ADB1A7A116C7A0AAEB995AF1BAB8D0751FBA714957AD6C7A0A9C73ACB8D347F9A9A469D85DD94B1B1E4AB74C94048F9AB578A2BDFCC75A213A65345129F78B26E3D901B3428A0F87952B91517460930386856AE25D00259D58EBBB403621335791EC6C2C6EC50711C07F56714B0ED3753F7459F07B83D1F838CA1875017B24FDC2CA6224B74423842ACC5C4B03D0658C7EED5A713A365CF523C9C2254E9C79059331BD730B123F53538955BC0FBA92FF9388192C950BC46C25339C82E98314D616B50993BB6241224AAE7EEB610EDAACC43ACD775941FD8088E03931F40C999D92360B74B86B857CFC6B5A74367DC72033941C78484031FFF53874308D52AA6FFA3491E0CB98366B8FF2866ACD1AC64B63770BC25BC33531175A4112D7684C01A8C4500F9FC083FE652FF78910B75093A34CADA85947D2602FF0716624FC9617EC71B9E87CACAB31CB3836A022523C3A3BF9F542EC306C22EB815E1038A48CC4F6596A1CB1482C7BADD7390D8DFB1FA01C5C30E458AD31BC5FA717503C0186F5A3B936C2D0AA7197733C2EB708F32B9FF611537773BC38B904CECB5D2BE29C988B22E415C7944726EDE09ACB76175448712F4BB8AE5AAF4498CF4189B0F0332FE774170F343A121071961CA130694B964033BC015F490B85DEA4A81756A95F3526A856327CA68F66E828A66B138C052115C0B6EB910BA703581617299855318BA9AFE6B152CC538836278D781771FA4C033A65AC5630B3EBA8B02A69717FFA1E73E77AFE06441D91CD45289927C40A1425AF113B179CC46C6606CA0EEB0D86765128A726AD50AE8DDC1AED2A07129ABBB3AA67B4E0192DA99D2C36686D3807E0B50CC9E30A82D97B0895B6C7670C01FC214DA38FFC54970D8399EDE259900A081954172F385E037C9B72B43A3ECC7FDB0510F6A930B837618ED756A62A0B3CF58B980C94210BCE63FB9CE88745FE8BC34D57248979398CB0C1C9B83AC979A107C955E97543756221AFF06C553A1F0E764AF19B93E0DCBBB9C327F76AB4BB890D179C90839430A749A2C43A66DC36394FF06DDC54070A2A2F152261E79597AC9B2AD32A39FA489382718E04DA6369C4AA0D4823BE87A60561CF49232C8AA20811EC7B29EC8A90A363F3B367CF9A81A08645C70A17EA41A1AC4C0E9407763D2A91B6732E46C6BA160972ED35CBE6000B7C1B83CA21623859CFB4317306593003B2A90BE01FB5A4030CC062C308C14924CA47A24FA5D13B82FA7153988DCD1077E2759FF250C198ECC4C860170E93BCC48CB009CA7535C62EE2A3AE61D07C828C68D19A55F6A7224F716E77A381E06933AC03A1B0D01B6D231E38D4A19EC9C6E8DCA42C218B290130BCB583F7E958FC3531037716B356948D726E602329A322A410945568BCACBAB75D65DCBB38B80CD20865563699838659E065CE749A6DF66542E8474BD140B50EBB5983F546FB393A0BF10C5891767B935E0F5171CC97A00607332088AED99B13EEF557CEF73429704244FA2B915CC7731BB9BD351FA131A726C837FE19047443A16BEBB02F4A5020878AFCDC72CB780485A793CE23BC6DBC11E04B0DBDA82F51B4327814471C01042A1119D5513408C119266A0476DB89B20B18EBB44A231CC5BC39C6BCFC4DD2A61EF9180BD76327AB05A37ECC4B2E8ABAAA307A6C58040484BF73B06AB4A65F7FD99660A759FA903A56DA3E8DEC389262AFF8BABE1B85168BD21762C86046290F35D51E4BCB8403F160D9DC4E00CA8987E46CE05A5FB04A112EC352B1B03219A4B50345C6F31915FEF4C2EF870D30A244EA4C20D82849628CB61ED677AC9890CB5463EB90BCCFEBAD9FAB23C26A33E6667D76C96220147685EAA84CCC3140104AAA4A559E51209DB124337A2795925C5E0230E7920D4E9338DA23ACCC8693D5952CF49AC26A74CAA6E05011E6385F787A7071CA08C61F59CA017ACBC15E9BAEB78837B99B93908A8CD6D7BD30CB6C23F9CE5E15204E0225B55C120B31C1276695C909C94AA1B5D0C0496278435352B50343A9974A55EAB5105FB4436570181F2B7AE82238CFB9C967F424C1D146BCF74A2349583D36C1F7629E70E0C4702C133F7475985B2DCD923465F39772E8AB76F3634C6A1F6E8088BBB0BD3A050CBF323F9EB76BBC4603617908BE85E6EA5E40A862180D9EF1380B7A859947562EF3A845669146\",\n          \"dk\": \"DDA5A4C182BC2917C5AEEB973FE853A397753BF576B753AD82583E6EF89CDE2B7B9B6B6FC65A349F66BCF4903902D22418E123BE13A3E6A29489362308452991448E53370B9F1315A83540D14A0CB60B9C80549556CB6F839B6062365693CA9665A01CD66574242092954B945D996ADDAA51F106A454002491FC0881D0489722979D90CEFD5235B9A1852D334B6ECB7CB3294C5B50A951E835DCFB24EF9BC124601567C0C05E1C194753CAB73A73E114C91C0341C7DCA0C904217F6BA200752FBD0B74E5503D141C17338AC6B6110E78F5592ED1B599B3A8FE65292779A473D95B3A1860DE955ABBC99349D0A4F85C97A32A154C89AF603799A438890E692370030AF4B69A3AFC91ED283EF4290524CC88E6E67D9CD00790F8B3E8626F528AC4A236570218B1710B82A86A308B4C4068E07422643BB8031E0D556700123F8F948463291257280CFCE2749812AC26A31C145667E9E94FEAE011C037C00E268F617ABC5DE656A8F8C858CA0A392201E144B9101BCD29401C98992E7F4ABDD02705E0F0AAE6D2BF2BF9081813CB1CF78454535146FAA994C666E628A4E1FB15CA5663551962380727A5933F91495B4EF290DE64A1D6D35CFD4A5279EC0460622C2E1B8409619D31144E674B4AE08951B25416F2839AFC4095D7257F34D54D3742997B77893C1C009C55A715D13BAC3092B9911F1BD962645C1A0A91639207BB98E15A37B570E39AA15011280E8B64B2E674CEC8931D26587D003CFC89498C997982AA60FCD8A3ECE93D784B0F9E7C10A6C380B76C1D6E91ACFD4672D4D9485B9C857B92C283B596D714A404444478117FECA0C14B641A0DD1B027DB7BF832316F0BBC256324BD4345C0A7CF3B98B2E6D01B1A9644F29010CEA70BBA08BE6877BFB1570CDC464A049C90C2B28C0A329904C1CFE6C92F076098C300288397CA65581A3BB8BA7D92A3D22C24E8691CFC610C9D3409B73CC2CE78A1C7E5AD07720CC30C984F73272CE4B43541AABF616714400D6D040609286AF1330F342A71BD3276454B3C33B5508817644DF14C42E93199E27FAD15309FD93BC8F88DB9078350797BE9CC027F03A7A18965682C83DDF182C411CB20EC3EF5C435973714255CC8CEE376654CB1AA6554BAB32F7E7587DEA22EB3C2565E760F503B6F103A54B2191FF3EB6E30126B951091B010BCB60C0A8A1C3FC958BC4DE5504A3C50BB6A2A5726B6B8E25024DBB7C02BCB2FE31D7CC581E42185FFA6503BD0A0D1C907FD13302EC37D191C141CCC0BF4D7A371A8C3389C5A799663709949B17195B4EC74B2DA225002CFB13668F83A1092A6C609522C07529BEA7A454EEAB74E15246AA17452451672A6314E5AA5AE0C57C2AB74653005A8F2B122859C0E8A0829CB0240BC0B5213AC9B035E9366BD3A4BC680D3CC45D96EDD33AFA02C747132001C0CA4FEAA29059AB0C1ACB1979B93C087B53911897B928422D0732E134DA0186794B4CD2E242C0EBBB5A7E10A82C44F874B749ADA0F91C6A26290B1CC49B7BFA150EE40886C9ACC8DCA75F813B13E732E94F85F6908C0523CA70C71A5BD394673D22E4648A443B25DB6DA4EB11C50A8AB30CB342A39B130AAD1A421B567601001B86ABA44A1311F05423A939B3F3404E5657476819D9E406FC265935542B48AF47FA78482C4443697358E784BB72B54B373E813E19186D5BA8997053189B314B5BB8DB699059CEC5FC2D58191B2A729D4A3BCA3AA7B377464939CC8E548A7789E56F67D08C1A0772C9836102321F659FF7B088E9968661609003DB687A295FB2BC6AA593DE4C9CFD6C55D2395ABAB916D3A8559451A6BEA299519654E8C8CBB56E455A85C37CA5A74FBF25252812F16B1B9CCA649718C00DDA596C506BB525159659C272FC1C7B5EC8272DA85D506959B231D1394A71D2C421E743964A58F38D52A60407FB8605961A63FA394431D35641991CB17DA5637732A5ABB2F34B81B7384974030ACD43886021A50B70924606BA1A295A489842FE3D10E92FB1507E240AE0B5616F8AA05299C94301E29A638A742028F9321F3955C61A59C021168FAEB17829025071B4015FA167FB6220DE0B29AA3BE2E88A171E32C9499B9B00BB867DB0C9915384F0A49BF3C5CFE539D22D3B998315CB8D09191634C248495C1977837A7B76C9B35E067AADA829618E4C683C404EA51ADB1A7A116C7A0AAEB995AF1BAB8D0751FBA714957AD6C7A0A9C73ACB8D347F9A9A469D85DD94B1B1E4AB74C94048F9AB578A2BDFCC75A213A65345129F78B26E3D901B3428A0F87952B91517460930386856AE25D00259D58EBBB403621335791EC6C2C6EC50711C07F56714B0ED3753F7459F07B83D1F838CA1875017B24FDC2CA6224B74423842ACC5C4B03D0658C7EED5A713A365CF523C9C2254E9C79059331BD730B123F53538955BC0FBA92FF9388192C950BC46C25339C82E98314D616B50993BB6241224AAE7EEB610EDAACC43ACD775941FD8088E03931F40C999D92360B74B86B857CFC6B5A74367DC72033941C78484031FFF53874308D52AA6FFA3491E0CB98366B8FF2866ACD1AC64B63770BC25BC33531175A4112D7684C01A8C4500F9FC083FE652FF78910B75093A34CADA85947D2602FF0716624FC9617EC71B9E87CACAB31CB3836A022523C3A3BF9F542EC306C22EB815E1038A48CC4F6596A1CB1482C7BADD7390D8DFB1FA01C5C30E458AD31BC5FA717503C0186F5A3B936C2D0AA7197733C2EB708F32B9FF611537773BC38B904CECB5D2BE29C988B22E415C7944726EDE09ACB76175448712F4BB8AE5AAF4498CF4189B0F0332FE774170F343A121071961CA130694B964033BC015F490B85DEA4A81756A95F3526A856327CA68F66E828A66B138C052115C0B6EB910BA703581617299855318BA9AFE6B152CC538836278D781771FA4C033A65AC5630B3EBA8B02A69717FFA1E73E77AFE06441D91CD45289927C40A1425AF113B179CC46C6606CA0EEB0D86765128A726AD50AE8DDC1AED2A07129ABBB3AA67B4E0192DA99D2C36686D3807E0B50CC9E30A82D97B0895B6C7670C01FC214DA38FFC54970D8399EDE259900A081954172F385E037C9B72B43A3ECC7FDB0510F6A930B837618ED756A62A0B3CF58B980C94210BCE63FB9CE88745FE8BC34D57248979398CB0C1C9B83AC979A107C955E97543756221AFF06C553A1F0E764AF19B93E0DCBBB9C327F76AB4BB890D179C90839430A749A2C43A66DC36394FF06DDC54070A2A2F152261E79597AC9B2AD32A39FA489382718E04DA6369C4AA0D4823BE87A60561CF49232C8AA20811EC7B29EC8A90A363F3B367CF9A81A08645C70A17EA41A1AC4C0E9407763D2A91B6732E46C6BA160972ED35CBE6000B7C1B83CA21623859CFB4317306593003B2A90BE01FB5A4030CC062C308C14924CA47A24FA5D13B82FA7153988DCD1077E2759FF250C198ECC4C860170E93BCC48CB009CA7535C62EE2A3AE61D07C828C68D19A55F6A7224F716E77A381E06933AC03A1B0D01B6D231E38D4A19EC9C6E8DCA42C218B290130BCB583F7E958FC3531037716B356948D726E602329A322A410945568BCACBAB75D65DCBB38B80CD20865563699838659E065CE749A6DF66542E8474BD140B50EBB5983F546FB393A0BF10C5891767B935E0F5171CC97A00607332088AED99B13EEF557CEF73429704244FA2B915CC7731BB9BD351FA131A726C837FE19047443A16BEBB02F4A5020878AFCDC72CB780485A793CE23BC6DBC11E04B0DBDA82F51B4327814471C01042A1119D5513408C119266A0476DB89B20B18EBB44A231CC5BC39C6BCFC4DD2A61EF9180BD76327AB05A37ECC4B2E8ABAAA307A6C58040484BF73B06AB4A65F7FD99660A759FA903A56DA3E8DEC389262AFF8BABE1B85168BD21762C86046290F35D51E4BCB8403F160D9DC4E00CA8987E46CE05A5FB04A112EC352B1B03219A4B50345C6F31915FEF4C2EF870D30A244EA4C20D82849628CB61ED677AC9890CB5463EB90BCCFEBAD9FAB23C26A33E6667D76C96220147685EAA84CCC3140104AAA4A559E51209DB124337A2795925C5E0230E7920D4E9338DA23ACCC8693D5952CF49AC26A74CAA6E05011E6385F787A7071CA08C61F59CA017ACBC15E9BAEB78837B99B93908A8CD6D7BD30CB6C23F9CE5E15204E0225B55C120B31C1276695C909C94AA1B5D0C0496278435352B50343A9974A55EAB5105FB4436570181F2B7AE82238CFB9C967F424C1D146BCF74A2349583D36C1F7629E70E0C4702C133F7475985B2DCD923465F39772E8AB76F3634C6A1F6E8088BBB0BD3A050CBF323F9EB76BBC4603617908BE85E6EA5E40A862180D9EF1380B7A859947562EF3A845669146A128CDD9B684F4A0907E80ABE2B7584BE10833A4DAF89DE5DCCAB7C001116B5208FED872D91297D8059743D3E7B6EE47548357E7F882B5BFE2F04314187ED424\"\n        },\n        {\n          \"tcId\": 69,\n          \"deferred\": false,\n          \"z\": \"EB8EA5E8C5EABACCFF162556DA53F0C02F72EE7A7DEA8E9EB70FC51C777645E6\",\n          \"d\": \"F8CF49DA62AA762EC020F3766237520E7FDA4CA3AC11FBE50E6C5F9CAB3CA7B8\",\n          \"ek\": \"36FC681397CD63113EF7756444A3CD1FB693BEA7905A507CABA24AEEFC637104C90D311BCAEBCEDB1335AB26C82F255984118B17E28F2266C5D73818D5ECAC6DA8380189196C9568EC949DDF76694A4B39145758FE66A14301CD64A10CCFE46C3CA8419EE5766160465154680F8C435DC523F5136065555F503883165A525568878EEB4DC675445F300F50A1C42EFBB5F1C9A0A46684414CB4C8187255A2CBAD4321082698F2714F234678D4418E33A70CFD76313EDBADA57C82C65801C0DB89D4F7BE19857E31E16ADAE95F95CA0473082F7FAA0855723B5AE4186A6C26D482905C00689D1737CBA52583893995065B5C76C7FC7839CDFA9BACF1B8F7A68307B459CA96BD7091A06A83AE3F3146FA81296932291B2158E9B09EFADA0A2D5AA8F1DB2489E40E04B0C768335595960CCD586B78844F0E940F2007B3B9EB9747254D83C58443C2B7A347B86FAB1ED4354DA2D9669AA21DC2A938E0FBBA0EB57E4CCA4C2666B79559B70F9B61DA060E03C59850AB2D129064F8D6ADFDCB7C457235CEE9473B003DDC9252AED7C444644832A4AF310C431AB26292488C73C38BC03550F7569909417041E2924AC9338ED8251BF6CD18F2B9D1569FC688AD095A728C574AB8EC549AAC0ED13A6EEF5AB87EE622034CB87045997A6657AED6439AF8951946ABC47BB9BBB55A80C92F40E126D78B4353538B6995830044B6E7220030861E47A5CF0291BDCEF8ACECA5C9FB0B43901CB6CE99246D2A2CFBEA014F902046836CB0B58A6DCB79CB9275519A3295C134289880A0D6CEB91356428294AB0B11CA150B9E362872BC8C25E2A78748C9D245C2D36A05F9D87CB5D0C306A5C0DA74AEF98BB1DC45801B4B9EC5B73A0ABA407B0AB8065B23B8345A6147A258326450E399BFEB86E754561F09CEFD1C832E355939175A347303E0D7815C4056F862B08F7914051834423972C84010CAD31B34321066396D47D5C9A77B9F45111B93988C0692039B5A060805AE4365AF1F8B138CC83F2D681F740886C7F07CD153989DEA4A6B2A3597374F7D5BA8E7D01424BCB721071A59FAA49F74AEF15910E84CC138F09F31574DF9A7000175C80073A10CBA6ECFCB84FA680131B824241BB96FF17BF76C0243A03DE9D49895D640E841A07C2299A7C52E22D37C8EE1AF81847781BB2F25240CF91851A5634AE285825C8809C716B6DB795361CC374B87BF34687E18EB413D21ADCEA883D4837CE874693159BDD2920405FBBBE6674F19D0B3C3D6B6F4095A946A5F7E6B0C5B7252A8F72EA357BAEB475FEA0141621A607324B4B17AA13FB7AF2ED529E1728F4BFC083198628DE3C8968A9A92C8863E08AB34A00EFDC73EEE7A8F29F4906E848B8F56474DB2BDE054A660966C955418E8D387CEF43CCA3B5DFD949E91AA5D75C50C38C96BCFEA7740147FDCD8406C0536325A1B74576392EC1BB1B181D584AFD841B43B0034ACAC33416C8DA02777E1169AD4045CD51674342C468874B16B12CB13776E6E8C702D84C5D13933EC161EC10B82A9F339AB3B1D1A9519F342506A3C0E7DB20ED3D7029CE444C5FB4FDB88525AF4C565DAAED6A51B7776246CA880D0F72D5A40B88FF348E9677B37D19F0088997F77429EDBB63840070D71A60FB34FA5CB59F47B06BB03C3AEE80F393A9DE283604635411151222C696FA6401783431E9690BB8958135537332EFA805ABB5D69C9C8F9084EC38862A68082266CA6A0C3514A71727AB519C359CDBC663066FAC14600132D1216BE63AEC0CA96804096D22904F4194D87D35C6C2C0D5539C93BB909A5287C64DCC1246A48CD9CCD80311089CA694120ABFD5A7F16D06A0BE42C48878BE76253198A703080A463053D43867B73B67B26B99A551A3C0C41115D007E0EA0AEF1446FF88BA7FDD32261FC0776FC427C121B2D231E3AC843015B12583777CCD5AAC502A7FFF9B03FB614246848742342C2E0A871E1AD6D44847D0A5512566098C712AD89904EE051425358CA5A964643577B8C8ED09450E97B55F660C2F2F608FBD87CB59585FEC3218219B1C7C18200DD446402898E065879FB17C97914C1E83F262801A4E256E798598A176BBE952149F8AD5B992CE791559CE6AC90539C096A209B9A4F87AB73E158801EF2B939A511F416BD4DC195D406A55A66B3FEC2925C5474525026488ABE90196230EC15B09CC55CE383DA8ABED781DC5085BC8E58\",\n          \"dk\": \"67973C3154342CA60AE94B0B60FB2CA4DCB70BF13BBEF7CC9B54790AB3CBD882385BBC888321366F4AABE4696D0126B724A3AB8FE40E03137A28D1B9DE280C49342B162196B13C364FBC5E26651702651A15369F1B83A25B945BF7BA7259A329F107C91E13802B0119CBE59C463B3A53B51837683A6E75674174210A871A1F582A6AF0053EB862BDDB6FE53B21924A7A783C4712AC5A6A38062D83A31DDB6537A82EC135616947A7D6456DB50C95DF678B113275238A1A8312100270A730435E06E554268921A04A9B7FF5025884486D817D4D81610C2805B4207A013A4E4B785CCC0052F70318188380FA554E41A64BE315B3294C3EEAACBF46DB8C87105DFAD68575044F59CA15A379579D381ADC65101FBA2EF3CB2F4A02BFEF8A5B28A51A2283979CD45B753C06D3653B9124A04AA64577562C34DBBDC9A5AAE358A5C73259568A3B18E48EB536C6ECB05A00DD727582B2F51C5562D53311C09D26218DEF54455EF86066B76CEBC41584173472A6008C55C94DE4261116CB21D325B514C83FD07C49C30967D23FEDA60CB9373AB9CABD3D528A99E6898E7A6F4E4C9644022060C30757F5AD961369D537551312172ABCC1C228B9F4F642C952006D7C45C40CCC96FB7BD28731A2F277E7284375D03A35F68101278E70AA882AD82940EA1E1DE9616AE99EAEC6BBED52B1B110502062783B102297A144A54C10F43B2D4C529398EB0739DA57F183B195A39E3178501C9539355B8E456760FAF99D5C3C55B1AB3CCA017C04B94581B18AAD4594236456A02B4D99A00412F818D6EC1387680847D10539E0C063A334A2A86EE1794B49DC895EF95C3D801AB33748417C8F60245E69A8AB68C9BD04BB28783293C52051F9C5A532008CA9C0063FC77519709F05358BCD28955C4A9ED3D9319E1939CED04AEF489C0EF2AFF1EA05FF2C8189093B14C6917ACBA94A8C546063B2FCDAA6C77C1A424A81F3717D9DD88C7A3C64FC593AD66748523744BECB2434822F23632D6E362E5F38C6329958AA20096BBB4FE4F43167301D57332BAE31110E186FA5C41597B92922C79CCE8C59FF341A08E56CF7A1B610605D910510DC03308967CC7CEC7A4328600C601566D8766B601855CCCA887BC84ECB05AA1529DF77058EC0760377BF8E3AB30FE477C33594873B59F02CA226966DC9841569564F9A5557BEC87C2C81784664CB2C8A0770C7C15DD61F48B762CAF11E9C189C9E9947D788A13FE554E76CB14A631440AB2560AA747B447B08E20A5C8BADAD31BD79FB27091938C4EC7AFF409E69730E152048D540958458C219C007FB1A7711D9401B86A10685A66E3C439C005563746221F50382825E3D6A779E692022B2B98150B690723B8ECA370283611ACA06B5881F38A7757A38A25EA06FB5176788C68C01D33822DC9CF7248CB81B7511832845CB06FBA36931CC07BDE6BD299A69A18945DF4057ED724292B76FC1A4432C1C16F451CD1BD796451957C5734B90574D809B69A9B19F07837028C97FC56B10CE6779E81CC4B4714A34A254A1100CA2044399658DD878BE873036B8639609786FEB608B5134A79FD74EB3D485F1D44F39854B3C6552965B4F08F998DFA4721B6107C20433AF509FF0E571FB58B8AB15727AE24AA189CCAB216953F72899D823886364994CBA31EA12AEE12B85460D056C761024987F7683B40A692959513C346E3674CA736226A748345D5931D3250949DBAA2F5BBFD7361E748A6D69419A24D3CCF7F7A849A4301497A2BC1588B021314B0784ADF1C5D01B562B89B3863A92D9103638CA305146912A3588F3A6A816224686268FA692031F427036043090A6B93CA944B4DAB10B63421E69C948619229D6323720C773B900A8393BDAD30547E0BE24D26795DAB3F1806779FA13B1A09A4BFA93BCEA34F522430E1BC923E378EF4631BF76A0F1D3B6A6D8BDD6811293F02CC2096C0257A957261E87220169330BE5559B48698EECC58D151C50C85CA19C9471E7754A8524A8C742495D15365189A2156046CD7A916C2816D9F1626306C6575114703091CC7721CB848CA66108746B1C10341BE8608FAC87B869519ED8C6CDB0530EAA6C801110B8BFB8C4C0184660F79AC2485BEA7AC2A2C3C683D483113C8B5747309FF1AC459CB83560C8800502942B09D3870036FC681397CD63113EF7756444A3CD1FB693BEA7905A507CABA24AEEFC637104C90D311BCAEBCEDB1335AB26C82F255984118B17E28F2266C5D73818D5ECAC6DA8380189196C9568EC949DDF76694A4B39145758FE66A14301CD64A10CCFE46C3CA8419EE5766160465154680F8C435DC523F5136065555F503883165A525568878EEB4DC675445F300F50A1C42EFBB5F1C9A0A46684414CB4C8187255A2CBAD4321082698F2714F234678D4418E33A70CFD76313EDBADA57C82C65801C0DB89D4F7BE19857E31E16ADAE95F95CA0473082F7FAA0855723B5AE4186A6C26D482905C00689D1737CBA52583893995065B5C76C7FC7839CDFA9BACF1B8F7A68307B459CA96BD7091A06A83AE3F3146FA81296932291B2158E9B09EFADA0A2D5AA8F1DB2489E40E04B0C768335595960CCD586B78844F0E940F2007B3B9EB9747254D83C58443C2B7A347B86FAB1ED4354DA2D9669AA21DC2A938E0FBBA0EB57E4CCA4C2666B79559B70F9B61DA060E03C59850AB2D129064F8D6ADFDCB7C457235CEE9473B003DDC9252AED7C444644832A4AF310C431AB26292488C73C38BC03550F7569909417041E2924AC9338ED8251BF6CD18F2B9D1569FC688AD095A728C574AB8EC549AAC0ED13A6EEF5AB87EE622034CB87045997A6657AED6439AF8951946ABC47BB9BBB55A80C92F40E126D78B4353538B6995830044B6E7220030861E47A5CF0291BDCEF8ACECA5C9FB0B43901CB6CE99246D2A2CFBEA014F902046836CB0B58A6DCB79CB9275519A3295C134289880A0D6CEB91356428294AB0B11CA150B9E362872BC8C25E2A78748C9D245C2D36A05F9D87CB5D0C306A5C0DA74AEF98BB1DC45801B4B9EC5B73A0ABA407B0AB8065B23B8345A6147A258326450E399BFEB86E754561F09CEFD1C832E355939175A347303E0D7815C4056F862B08F7914051834423972C84010CAD31B34321066396D47D5C9A77B9F45111B93988C0692039B5A060805AE4365AF1F8B138CC83F2D681F740886C7F07CD153989DEA4A6B2A3597374F7D5BA8E7D01424BCB721071A59FAA49F74AEF15910E84CC138F09F31574DF9A7000175C80073A10CBA6ECFCB84FA680131B824241BB96FF17BF76C0243A03DE9D49895D640E841A07C2299A7C52E22D37C8EE1AF81847781BB2F25240CF91851A5634AE285825C8809C716B6DB795361CC374B87BF34687E18EB413D21ADCEA883D4837CE874693159BDD2920405FBBBE6674F19D0B3C3D6B6F4095A946A5F7E6B0C5B7252A8F72EA357BAEB475FEA0141621A607324B4B17AA13FB7AF2ED529E1728F4BFC083198628DE3C8968A9A92C8863E08AB34A00EFDC73EEE7A8F29F4906E848B8F56474DB2BDE054A660966C955418E8D387CEF43CCA3B5DFD949E91AA5D75C50C38C96BCFEA7740147FDCD8406C0536325A1B74576392EC1BB1B181D584AFD841B43B0034ACAC33416C8DA02777E1169AD4045CD51674342C468874B16B12CB13776E6E8C702D84C5D13933EC161EC10B82A9F339AB3B1D1A9519F342506A3C0E7DB20ED3D7029CE444C5FB4FDB88525AF4C565DAAED6A51B7776246CA880D0F72D5A40B88FF348E9677B37D19F0088997F77429EDBB63840070D71A60FB34FA5CB59F47B06BB03C3AEE80F393A9DE283604635411151222C696FA6401783431E9690BB8958135537332EFA805ABB5D69C9C8F9084EC38862A68082266CA6A0C3514A71727AB519C359CDBC663066FAC14600132D1216BE63AEC0CA96804096D22904F4194D87D35C6C2C0D5539C93BB909A5287C64DCC1246A48CD9CCD80311089CA694120ABFD5A7F16D06A0BE42C48878BE76253198A703080A463053D43867B73B67B26B99A551A3C0C41115D007E0EA0AEF1446FF88BA7FDD32261FC0776FC427C121B2D231E3AC843015B12583777CCD5AAC502A7FFF9B03FB614246848742342C2E0A871E1AD6D44847D0A5512566098C712AD89904EE051425358CA5A964643577B8C8ED09450E97B55F660C2F2F608FBD87CB59585FEC3218219B1C7C18200DD446402898E065879FB17C97914C1E83F262801A4E256E798598A176BBE952149F8AD5B992CE791559CE6AC90539C096A209B9A4F87AB73E158801EF2B939A511F416BD4DC195D406A55A66B3FEC2925C5474525026488ABE90196230EC15B09CC55CE383DA8ABED781DC5085BC8E587FE45A6DB8C05EA8FFC788FD2F73C26CEF305BFBC9BF7C5B32466B5417DB33ACEB8EA5E8C5EABACCFF162556DA53F0C02F72EE7A7DEA8E9EB70FC51C777645E6\"\n        },\n        {\n          \"tcId\": 70,\n          \"deferred\": false,\n          \"z\": \"DAC056B9A373687E44CCAB8751BD334F4942696B9076155F9D0E5BC0E89D85CF\",\n          \"d\": \"08E36AE8586A59B8249A80D7F43506F9711FA4B00A49D182CE06DAD0CF985809\",\n          \"ek\": \"3201469EC68BC7EB2BD457769C69C09F1646DA20B3DB7A8756962DEB905FE387084A58B069901A9FE1A0939C38E6E414E23B516FD0651E174DC0F6388DC2CB8D605D3D5B1F6AEA3425A03B9A903C846ACC128C87CD5A17A46BB474A0BAB64112DDF1C35355C58164B7C6F9B8A5879CE8E1CEB4FCC26EA55E4833A401B54EA5C29D92E03A8FD5AED6C51E2437C7A89221ED73B2BA3BC5DC30276A0692FFFA3E28B8CB9025566D54A3DD84A9004730B2009A73B354644045B44224D9D96B6A137D8F61513BF8555FAA8F61F00215C8C8841091F7647E53055E6762A504C121474120BC70BED5C4102C82813AF656B9248ED6F630BB269CEE33AF26D474C8B3CDD8D959D10A5D2AE221D5C20D9628A26F1A11EDE680F8C8333A54AF8D79B67AB0BD9DA1680927B0B31B1D6653410C4CC0C6E95164C33506D8CD19A73E1031C733C55EFCA33F76BBAB03182BAFECBAEAD364CE38B22B74A0A244A15725B232616EF2CC0A3010758A698880A23FD58BB2199A3946F5CCACCC2D61AA5985488C0EB60D465566B94C762057ACB163CA636B548254C26F139F51A96E492035B9D725AC16B94C537E2FD78B1641882AD20DCD6C5BB21B79921A59631130E7885340F5311F9973CBD31783DA443BF7566AB3BACEB88232F83E7077845061C09D425C7AA01733CC63D5B7C17C6B3309408D1FA31AD909BC1B0AB6535B0AB6064722D847B07811098C7316071A751623654605FA453FDDE8176B42B2ED07CDA4144640EB5984B301390C64D520071F1C66AD69C550AC41D74915DC2156F78127A704CD678A655C87CEB43748F0989AAE1730D3E4B3374A72CE364F1E2ABA2E5CCEBBA440FDA8AD50A749FD908AA1578C02B35342BB96C3350392C5C25CD71093BB2E87A3A466B697489A3C9E9C84805160A7396D001633EE00AE5641A893BB374CF5CF62417AFE67682C08A10E0A59B8346ADD006F2B40669042616D57936593A62D5578808A1E9D55B1EF4C90EB049987DBAB69B3C8C4A596901BA2BFA74DD4B937547A82B7F5290BFB79E5651B2B4AAF3692CCFBA987D5EC778E61281B0B388A71222DA27A90776E66BBAD25118AFDAC425F15599176C5D5F1B491FA1FBBD9005A0AB93AF1C8280292AA468FF0B1218726C52E4AC8FA403C91D12EB1134D6B08ADEE2A065F191DDEC390A1232D63306AE1C60AE1908CA2294ECAC6AB5257644DA45D70E96C6FAA5E1DD02AEC276C5EE2A8D36A2A13EA21359934C8285498370460CCC2A4C886C72A9FF17AA214553F6826177630ACC2F70619D64CE3F04DF17A6599E9AE5D2BB97B731432421012FB229124CD52BB9E2EA7B33CCCACBD9029A69AB76F00CE84A0C0D2C80F54A77D6064AE85465FA74271BDC2C62CB940E9C32A67F9C94AFB7322B93016E31C4E34993C7A65D58117495C44A8EBCEA635AA6C29889F3AB1E39661925136A06BB269483C1D291F90478713A91A178193B25877D416C03242682E76071B78964EA626C6A85E4ED8A45CF573BE56369B9476A2FC76713AA11573A083D0A0B89BAC15228A42B9BE2F8617CF8BBBA21562FEE3313066A8E86063EDF365799B7EFCBCC11570196B535D40F65F59C0972E057E2C70048CC10DD292611028277B325791B865B9D863972AB1725B6BF1546DFE0030CD85621F92472CB0064CC2CD11411127231181E6745A3580F6E8383150C8C098A3EB455F5CD05725719D0F844BE76743F7D962261A5FE5C29E46D58EEBCAAF0C40C04BFA43089C6236AB1AB574CF28A9178A4634C5848259191C21401C05A00A48D0420BB2737816CC72BB3D5BCB5EABD5531ADBB2E875045E861114D2C5AD5997B104A4F3BBA26A6372EA6062C8D6229C196E5B7A572F898D94A307BA051F899C16A34981B818034CA84A14930AF2D38BE095BE4B304FA1BB69DDB72AD605294BD273FD314A4D1940161B97626B5C725935D358153B4A1318F1273C7C6A93FCB372196593D9655F3C41FFD4B7B72B7131D7C2607A8763778556EC2D3E09969A4C968E92240077051446375B2C4C9BA9CEE84563C4491673667C026843C1602A9BAA0705E27918A27E2B01B9E756A77F7A304345117ECC52E81823A0A8170086B89EC64A39C60EA6A78363FB79E7C0B886B1854C5A929DF98077F5AD847650926273A9495000E688124118F1D83CC93D0E4D2565D307F4CD0C166E9083E8CB47F6979CD0C6F05D5A\",\n          \"dk\": \"6F148AB8F31718811FA8326B85A430341A9CB4827004D5897659554F661B37551033D7B51D416CEC7921FFE4711A575BA6EB73643B78856B9C230768B6369A9D9C7C91B7620E4A709D38A73CD7A2CE48712EC03F0C94070B783A4EB3186051A56D469D01152D3799934A69C672600E0B223740F937607B8923768B20F7CBB9712BFC633654119B7299770C764C471399BFF2CE457918F3C3AB3A323CAB53BE1F491EEA526EE95C12A50C2A3D8C75589BB6919B97D37AC96753ACBE493109F60957E268F8CC27C497A89C12CBC7109CCDEA54E174B7AB6C4E9D68326261280D582DF8E976B2F67617B69FD2D58CFBC44F641C9598B293182939D4AB384C6B7920266E6C7BAA133A1A9E841AD0018ECF6010F0D292D7699177864D063641EE8BB1A2C7CCCD7C33C5574E9C499EE9E7417A5C072596C4FB76C2E56950B0907BD653B47966420E866FC3FA367C965A1D096FDA16C3CC1CB651DC693B960443617C58D13FC2595FC267720BB88D0633579BE84DC0232D973AAF92140B20CAB7D4444A96EB6A4235C0665460470994D94005B659BA5A6A345E51677402B69D378A0444B345340BE80230C7730F4342833543BA8079692333128E45B9DC48830C1892E69AB37C632771EA677A47B1518815E4976367888AE552A397576A6838B26426A587828B08EAC520E47463D00D1EF5CDD50838A961832647561BA75D6B1A5C93121949C46CBEE11938C5718013724131C18B0BAB806097C14445FCD303BC8BA3FBE2C0AA2C3FE45A74F5E22F77D6B405A962B1F95CC1E0620E6239AD57042E139955C97C359B09F6990D1C7747DEDB6F02313BDA02839AF16D7F181BDBD3245B272988432B35916144BABDB7F3CBB8A4575677484822B76739577CF945FC944C21D4956B0B2E3C86195B5724A03A896552BEEF5804D583BA6B2B9A67106A47621DBF911E62F677F933527A0095D6F4C0D5C3392548C23584CF91888A51269D15606542E601D8A7B915418F164A6AFEAA7AF60345D7A603A018838237861EF1C99194323773A8A18591F7539821D57856C44D3ABC334D266DFFA64AAD3B56DD7C7B1ED2B5026142CF508D9F25A2CA6199F7937E6AFB66B1C486D5512F1F12754ED5B70871160B8814986C078F006962C956404B6E5EE9C486E786C3C448B902B1841697F1208313F8543B40566E18746EF181DA1195711040E7268CC474A1AF4388A8FA11AB9A54B228A92E569415D12C06B246F8180F4B80395C708089DBA946B459ABB598F6F32708B8316AA73DBFE15D231C2D69627D434CC1674778B4FCAF23227527B2966F0718AC1C9D6D3B374829C46D063A4794C15A189DDA693FB6C08E46C0B7970B8AECB8B52EC35428E8826CF6B1FBC9A988E13ACD089E4DB2967DF64DBCA475BEDA31DA74C3BEE4BF74568EB257763C371BD252A525AC3325941C62EB68BE79701C11C18EFBB1DD010F84BCAF5EBACD35C67671C968A6B5098E0535D7932002EA7739C85F45085247032DE8EBB7109B9A522112DFB89C2904C1B611AF46762E4854AE2652B4A5580B6D902C12788E2F6C72A59A0A5C4385CAC77777C64D9920A5827568734370E891A2638AC12BD164C15C01A3DA0292F325DFA4069068329DDCC25A3964A845694DD6AA99F17AC9C12DADB4431C8707949CBE2E4B9764B65BAA0451146725B6A7B6CD6726E819AA8AE2971E334A66B0501F210C0A1975110BB8647CC8299C2F7D4C16DE4791B25A36B2592E3AFA0D059B9D4B3B50D9981160C23EB96BB1EA0A984D922C1D31A9C105CF5CE64D86222A12773E7ADC01C99B5F4754610A2B3F24C66D8E3212B8584DDF7B68128B215C600336409733049A54A719D0E5A657AC223136B1C9FC5D3F33545A051902AB0FF4B1CE5DA81BC8B3A78EA40828348210F953250211F85C1F3196418A32CAC6158850EA8B44A0CB72EA5C0B165DA1A0751DA41D58514439461942E478D3A3951E3273F35B938A842EAF331471B9B1417672DB5CA82F246B937448A08AA720060C5F2BC084D3157C25CA2F2BB02E2BC4289466C8721D7198B01F92C3EAB020BB92B318E78074218A48180EE799948501B84FDBB447FB009436CD20A69378E4BD87831630329AC0D76460E5134C60561180274E8C94B64C5F17BC4AB39625B49A13F1887B9AF06E3201469EC68BC7EB2BD457769C69C09F1646DA20B3DB7A8756962DEB905FE387084A58B069901A9FE1A0939C38E6E414E23B516FD0651E174DC0F6388DC2CB8D605D3D5B1F6AEA3425A03B9A903C846ACC128C87CD5A17A46BB474A0BAB64112DDF1C35355C58164B7C6F9B8A5879CE8E1CEB4FCC26EA55E4833A401B54EA5C29D92E03A8FD5AED6C51E2437C7A89221ED73B2BA3BC5DC30276A0692FFFA3E28B8CB9025566D54A3DD84A9004730B2009A73B354644045B44224D9D96B6A137D8F61513BF8555FAA8F61F00215C8C8841091F7647E53055E6762A504C121474120BC70BED5C4102C82813AF656B9248ED6F630BB269CEE33AF26D474C8B3CDD8D959D10A5D2AE221D5C20D9628A26F1A11EDE680F8C8333A54AF8D79B67AB0BD9DA1680927B0B31B1D6653410C4CC0C6E95164C33506D8CD19A73E1031C733C55EFCA33F76BBAB03182BAFECBAEAD364CE38B22B74A0A244A15725B232616EF2CC0A3010758A698880A23FD58BB2199A3946F5CCACCC2D61AA5985488C0EB60D465566B94C762057ACB163CA636B548254C26F139F51A96E492035B9D725AC16B94C537E2FD78B1641882AD20DCD6C5BB21B79921A59631130E7885340F5311F9973CBD31783DA443BF7566AB3BACEB88232F83E7077845061C09D425C7AA01733CC63D5B7C17C6B3309408D1FA31AD909BC1B0AB6535B0AB6064722D847B07811098C7316071A751623654605FA453FDDE8176B42B2ED07CDA4144640EB5984B301390C64D520071F1C66AD69C550AC41D74915DC2156F78127A704CD678A655C87CEB43748F0989AAE1730D3E4B3374A72CE364F1E2ABA2E5CCEBBA440FDA8AD50A749FD908AA1578C02B35342BB96C3350392C5C25CD71093BB2E87A3A466B697489A3C9E9C84805160A7396D001633EE00AE5641A893BB374CF5CF62417AFE67682C08A10E0A59B8346ADD006F2B40669042616D57936593A62D5578808A1E9D55B1EF4C90EB049987DBAB69B3C8C4A596901BA2BFA74DD4B937547A82B7F5290BFB79E5651B2B4AAF3692CCFBA987D5EC778E61281B0B388A71222DA27A90776E66BBAD25118AFDAC425F15599176C5D5F1B491FA1FBBD9005A0AB93AF1C8280292AA468FF0B1218726C52E4AC8FA403C91D12EB1134D6B08ADEE2A065F191DDEC390A1232D63306AE1C60AE1908CA2294ECAC6AB5257644DA45D70E96C6FAA5E1DD02AEC276C5EE2A8D36A2A13EA21359934C8285498370460CCC2A4C886C72A9FF17AA214553F6826177630ACC2F70619D64CE3F04DF17A6599E9AE5D2BB97B731432421012FB229124CD52BB9E2EA7B33CCCACBD9029A69AB76F00CE84A0C0D2C80F54A77D6064AE85465FA74271BDC2C62CB940E9C32A67F9C94AFB7322B93016E31C4E34993C7A65D58117495C44A8EBCEA635AA6C29889F3AB1E39661925136A06BB269483C1D291F90478713A91A178193B25877D416C03242682E76071B78964EA626C6A85E4ED8A45CF573BE56369B9476A2FC76713AA11573A083D0A0B89BAC15228A42B9BE2F8617CF8BBBA21562FEE3313066A8E86063EDF365799B7EFCBCC11570196B535D40F65F59C0972E057E2C70048CC10DD292611028277B325791B865B9D863972AB1725B6BF1546DFE0030CD85621F92472CB0064CC2CD11411127231181E6745A3580F6E8383150C8C098A3EB455F5CD05725719D0F844BE76743F7D962261A5FE5C29E46D58EEBCAAF0C40C04BFA43089C6236AB1AB574CF28A9178A4634C5848259191C21401C05A00A48D0420BB2737816CC72BB3D5BCB5EABD5531ADBB2E875045E861114D2C5AD5997B104A4F3BBA26A6372EA6062C8D6229C196E5B7A572F898D94A307BA051F899C16A34981B818034CA84A14930AF2D38BE095BE4B304FA1BB69DDB72AD605294BD273FD314A4D1940161B97626B5C725935D358153B4A1318F1273C7C6A93FCB372196593D9655F3C41FFD4B7B72B7131D7C2607A8763778556EC2D3E09969A4C968E92240077051446375B2C4C9BA9CEE84563C4491673667C026843C1602A9BAA0705E27918A27E2B01B9E756A77F7A304345117ECC52E81823A0A8170086B89EC64A39C60EA6A78363FB79E7C0B886B1854C5A929DF98077F5AD847650926273A9495000E688124118F1D83CC93D0E4D2565D307F4CD0C166E9083E8CB47F6979CD0C6F05D5AA184CD5ADDE3E9D68D66C7AD3ADAD382D8642BF03B85F068AEE861FA55B6340CDAC056B9A373687E44CCAB8751BD334F4942696B9076155F9D0E5BC0E89D85CF\"\n        },\n        {\n          \"tcId\": 71,\n          \"deferred\": false,\n          \"z\": \"4D727ACABD44DC48980691E0268B5B3FC1E476B3FDF9571F5CBC8DDFD400AB99\",\n          \"d\": \"A491FF48028B67A407F1054D5B1CBA733B665DE667E22596EDCC31C227C2DE1B\",\n          \"ek\": \"3563BADA724011CA9F7154964DB8092AEA60242B273BA94ACD33B61BE47B5AC2115E95BDBE56B214E04166356496A698C81744C06B114E2B37CC441A9AD8C90B936E1D86B9E1D9B17F352B393B9A1DDA0FEBC548989C12C120C7988A460B151895961733F84111058116C427E75A8FB6798B9691626F7A5274F46BCF5A160F0BC59B01C5D5E634EE9C1FD5D665AB59A4B495030855C538C86BE779CCD8CC076D83407A5C9AA3757841DC30F47592233A3AA8F3A721A5902FF8225F51764D59087D5B928E34572C23C511E236ABA0CD710A7EA0FA0CBA999F22445AF63CA783205E4D935CC9304BE9E92364C97993A1BC4DE491C5ACA2932575C99153A0BAA814644DF6A4922F426268EA1F7D97AB7629462F0C8465900AF523CF2FA978CDA56F4FC936BDE529553B56735701FAB3453C1A8ED0492222022E7C799EA11837C952CFF9A059992B26CE774460DAC2D23803A913CDE96662CDD78B4842A09E3A09A8D6A5B4052A25D39F63A43D9B9473FF184EB9B49B78B7165C62141866C2D4D866232A952E89CFC422CA5E510390DB98DE3A5DD0E3728BD5076656941894BD2C85C1B4FC400A9365F744B7EED6637C694D7C3104329AB526193E0CB2BF5E9B8E5D34630C16B097EB027C33621F23BA78E4470DA48354C29C114C242EEB6C62D15395D40B716A6137CB5B5AE2C27CD49D176B5D70416E254753F1150D5D8807F07916330C69B5F236F0672709834523092746897CE489B3FC878FAB716042509AA96BCFB839544449280C7C1CC3F79A22A25BBF47A33228CF1CB619E2492A95EA5F3D12C80BEB2412B81D6EC7CF6147287FF36449205CAAFB3625CC0A6FD4AEE3898D8FE56F62B06F6A7B7F1325B83A30B72FFCBFC9A672767BB2659B1B6500B4A3629F00286B82F9597E6CB21FDBA9AAAAC5F24273B2AC22EA4023219C7B32FCA7694693A2208791E1AC33C00C0DE64E2241001DC04249CCBEB797A1C38CAE3DA81C07364374033917A04AF6A19BE424C07DF33237B9223B7C04890CCB57780126B9A68AEB5783993F6C08783DC254B14482AD37AC90609BF3B0CEC7B39DB4241E0D9502DB60AFAE068749A295BEF8AABF861EA1292542927F6DB939AB7365142A37962A46230C37FB9519642119E03C45CEAC2F0B40198D6211758346E2FB284FF807A4C06552D7B8DF604D2EC771D2F74D0A258261280C60E71C9FE66168DC9493539EC1F018A520C02E6B8562C55831C18900A1C55FCC6A772480BEF2C75322C1CD116483562E6B7A0D16D351E2BC58032B5267325183148CA61079B2E8A8AA293863E0946D6772716053DF4A0260E3A36ADABD19713B46E89ACD650EA7932050724356D1261DA37916E85BF552C8AA628ADB08383781245EDA4A82E27D099496A26B5C893927DF02BEA47742B3C40B0E31B5D621012D1BC0F3B085E5BA6228E9B5E875901F1711CD12516EAAA4FFB16AF44AC384E0AB5469B222666FDFE32B1F5A4F3C0C022F6B4ED770572DB6B9B7FB715CEBA1C0EAAA19FB2361D83DD2465375A2BBE2793255619592A6044B71CFCA2916B0C6C895A38D7A28245398465432878D96446BF71A6C21C0A62500B030BE6B528646C860AEAB7B28FCCD87381C61D7106B518E22A2A03F5B7F48729A50672A4B039FB67B9DB0A803D304BFCF83782BD5A10250A8C8D96A9EA993E692CDE5871C6BF79FED564C5834634B88280D3A4CB18025E332532BC65FBC169DE0308012C08A581A1A5C9B1A89C75162B990FDAAA768A35DBD9ACE608981D589116BC45BD7D49425C7B11B573B8F554976152C9A17C273E4A3CAC15D5DF6724C1605D9A77CE487438248178C10486FD7C7E1EA52BB5467B066245560A2C4523B53304445F956E968C5AFF602B7F9378CB290BA0B4707841D175C01AC0BBEB18435E03850FCA479C1E8B7CF0730A5F23690F862C1970A5E9A8DD3F51E0655B2CE7BBD4B38264929C9499BADB505625B7A7BF260597F1239176C4892E37C3EA0770068C49E14A8DCE526D64791B6E3A1EB70A8FDBA17FF8476CA5B1BC9FB533CE7B1F0373D760AC691DC598899A3798A0D6F967699CCA69420116BE625CFC8C23879215C320B354412DC7311E21A3784A3445134977E995282033BD0080B84487BBF23A499CC1D17172BD029406523707B671E25F9DD605C8473C42FB131B08A1E33F1E85055C2DCBA8B2B04F9B8C07D906384\",\n          \"dk\": \"385CACF0957318BCCDE555CB6933660544AFDAF8BC43D4AED82135F8CA2131201E03C7A42C7875EF309F227684284275F590CAB8078AA51AB95A133BFD9910AD0218260309CE43360ED01926D13255D37FD837462FC685F64350CAD95F9B757B56C79564C57C10B31F25514C570C1EEEC1223AAC91353971D915635E3A0A8F4BC5E1F241B3F31F457B3984CC160E13AB6C46B41808CB067C8396D4425FFA0C2E208E95522D65D0583FE2C50F40C9888066CF3377D494BBCCA2CF5DB55ECF711AA48ACFD6F859F859992E5B57C9C56D9430C6360B076A8C0EDDD544F8BB5B7950A7DDC006E705597FC08AF695AF0765331CC8913D8944EC4262E195B6FAEBCE2A3B7472D44A320B1533AC3917FA8DF7283D33E999DE4756FD37005104C8E2D50C4356BB35A79C021C45A05C05B10688F5A53AB52255D9B6BF8FAA036F09923AF89CC6F66BDACAB817A6C9F88B2339170FDD478A7C674350209C2D0773A413134D9709DA988F743134FA89AFDA3C94C4F75DFEE2656AE2312DE6A42A0B43910A4CF061922456346039BB4AC466B8F9AD7867103C165041220C23F09F264383D183B66C7BA90B93BFF8CB9E736881EEBC21917AAFC9067F8814C17AD8A302184FEC862083D1B39C810B92E3CDDF937773CA9EAA2C342014CF4652A5E5D2816AF4B9586467CC702A4AF342DB0B5908AC2F9B0C4490A8962D011BCCEAB20EE46DE2B6B08D473D79E7A10AA52211191784551E7B289C2FC85318017844674AED2917B3ECCA9C814F9F988CA8A927029AC5A6E8582078220FE44AB8A63EBAD736C4496D726BA3A1B422F1265212503B686A5367279E9661702D21887AE81F283966A5C91A8638C63608CBB59CA80A679F2ECA74D828AB15592A15070CC6915D8B6A1A6CD13723D410276B6F6644A9D0610869347386012340A4C6B1207B57903527C010FFC1C8B59599DAF124999CC4FAE39164A5868107C198519B59FC38E1229021A901F569A009987D76606A1F66C88FD88AA4D552E7C277510316DAD731727928FBB4495560958042773E1B8252BBB14F3328E5079E2206743BFB18A799918E241AF3A808155262A8F9A1CB46C60696763AB1206666BE2659BD1C78B6ACA15FB1F823C6E15A70930D21BC07F306094715A62805C486513837C56F332B97CEA067318C0C8E222B5762968DCA48DAE6C8297C2FA322913EA947B06418096C22D1EC3F2CEC48A7F850BF01B5702A8085108FA53127DE0B949E6352365336BEECA65395106BE79AAA91623931828436A67C0C3917160837532E0949503F7891BED485B5056B211A1E354105E3661AB7A1BFF354BBEC695204F2A8EF55073FF27A313662C1E7C7F4E8AB46399D29E2BE910425CE92976B491A1696CA56827F4571C98B328046269379110A559B463F1B9770050F1750262542A6C6BB728B4332C555CF45468795F2884F10303E212D333A0445EBAC52D669DA8B1BC3033081D0516F01CF22C5BE8CA020C8F671FC27A71A518D08B7A2C1C3AC62D874293A1D33729626968638C85761E78ED704CB86F054BE86C35F6A5233253C0F097B34A557C3A0B7B5D16B0266718AB7BB5DC27AAC3A96BD0B807BE5392FB0C6804739800206C7287C130B2A77E5045D5491FA50C1C68629C538078A3091E7B967B4767C3758A9A4E871C6129D78F0B9BC7963CC110641F7C8350A860B611C3DEC98FBBB8FE4F40BD10BA1F9F6C7061B12E7541EFB527B8E4349469987E4FA42D8F4958D6243A1C032EEB5860DD0BEC0C9A1E4D88967B748BC536020524BF5D06C594199DBD6B6CBB795F64B0F6656655B9B36B7E84316D918E094078D1C4E03C75617197306192338747E32F46551ABC23E74AB757B12C85899C28238FC74C089E4101F0A42BD440E28BB832BD523F03C070FE065D7B96652D680DE68223971ACAF5B5C29B8AF836C41CF80AACE794800FD944B6224B8E207BE27AEF24BAA4B2436B28B6A85AA19612A4F11935DEC482D7EEBB006B35860D13A7FFB159F57812B518E2F3161A9E35C5B272DA3529284DC278664AEDF05B0E1D602BFB60436F026FDBC0A30F2579F1C4252021A132C31C73249065C5BBCF03A5D418DEBE578E5F4A9074685C68491ECEA6DC9EAA7BC819DD2C43F42049B92864A001D533741515EA5090F0870F455B03563BADA724011CA9F7154964DB8092AEA60242B273BA94ACD33B61BE47B5AC2115E95BDBE56B214E04166356496A698C81744C06B114E2B37CC441A9AD8C90B936E1D86B9E1D9B17F352B393B9A1DDA0FEBC548989C12C120C7988A460B151895961733F84111058116C427E75A8FB6798B9691626F7A5274F46BCF5A160F0BC59B01C5D5E634EE9C1FD5D665AB59A4B495030855C538C86BE779CCD8CC076D83407A5C9AA3757841DC30F47592233A3AA8F3A721A5902FF8225F51764D59087D5B928E34572C23C511E236ABA0CD710A7EA0FA0CBA999F22445AF63CA783205E4D935CC9304BE9E92364C97993A1BC4DE491C5ACA2932575C99153A0BAA814644DF6A4922F426268EA1F7D97AB7629462F0C8465900AF523CF2FA978CDA56F4FC936BDE529553B56735701FAB3453C1A8ED0492222022E7C799EA11837C952CFF9A059992B26CE774460DAC2D23803A913CDE96662CDD78B4842A09E3A09A8D6A5B4052A25D39F63A43D9B9473FF184EB9B49B78B7165C62141866C2D4D866232A952E89CFC422CA5E510390DB98DE3A5DD0E3728BD5076656941894BD2C85C1B4FC400A9365F744B7EED6637C694D7C3104329AB526193E0CB2BF5E9B8E5D34630C16B097EB027C33621F23BA78E4470DA48354C29C114C242EEB6C62D15395D40B716A6137CB5B5AE2C27CD49D176B5D70416E254753F1150D5D8807F07916330C69B5F236F0672709834523092746897CE489B3FC878FAB716042509AA96BCFB839544449280C7C1CC3F79A22A25BBF47A33228CF1CB619E2492A95EA5F3D12C80BEB2412B81D6EC7CF6147287FF36449205CAAFB3625CC0A6FD4AEE3898D8FE56F62B06F6A7B7F1325B83A30B72FFCBFC9A672767BB2659B1B6500B4A3629F00286B82F9597E6CB21FDBA9AAAAC5F24273B2AC22EA4023219C7B32FCA7694693A2208791E1AC33C00C0DE64E2241001DC04249CCBEB797A1C38CAE3DA81C07364374033917A04AF6A19BE424C07DF33237B9223B7C04890CCB57780126B9A68AEB5783993F6C08783DC254B14482AD37AC90609BF3B0CEC7B39DB4241E0D9502DB60AFAE068749A295BEF8AABF861EA1292542927F6DB939AB7365142A37962A46230C37FB9519642119E03C45CEAC2F0B40198D6211758346E2FB284FF807A4C06552D7B8DF604D2EC771D2F74D0A258261280C60E71C9FE66168DC9493539EC1F018A520C02E6B8562C55831C18900A1C55FCC6A772480BEF2C75322C1CD116483562E6B7A0D16D351E2BC58032B5267325183148CA61079B2E8A8AA293863E0946D6772716053DF4A0260E3A36ADABD19713B46E89ACD650EA7932050724356D1261DA37916E85BF552C8AA628ADB08383781245EDA4A82E27D099496A26B5C893927DF02BEA47742B3C40B0E31B5D621012D1BC0F3B085E5BA6228E9B5E875901F1711CD12516EAAA4FFB16AF44AC384E0AB5469B222666FDFE32B1F5A4F3C0C022F6B4ED770572DB6B9B7FB715CEBA1C0EAAA19FB2361D83DD2465375A2BBE2793255619592A6044B71CFCA2916B0C6C895A38D7A28245398465432878D96446BF71A6C21C0A62500B030BE6B528646C860AEAB7B28FCCD87381C61D7106B518E22A2A03F5B7F48729A50672A4B039FB67B9DB0A803D304BFCF83782BD5A10250A8C8D96A9EA993E692CDE5871C6BF79FED564C5834634B88280D3A4CB18025E332532BC65FBC169DE0308012C08A581A1A5C9B1A89C75162B990FDAAA768A35DBD9ACE608981D589116BC45BD7D49425C7B11B573B8F554976152C9A17C273E4A3CAC15D5DF6724C1605D9A77CE487438248178C10486FD7C7E1EA52BB5467B066245560A2C4523B53304445F956E968C5AFF602B7F9378CB290BA0B4707841D175C01AC0BBEB18435E03850FCA479C1E8B7CF0730A5F23690F862C1970A5E9A8DD3F51E0655B2CE7BBD4B38264929C9499BADB505625B7A7BF260597F1239176C4892E37C3EA0770068C49E14A8DCE526D64791B6E3A1EB70A8FDBA17FF8476CA5B1BC9FB533CE7B1F0373D760AC691DC598899A3798A0D6F967699CCA69420116BE625CFC8C23879215C320B354412DC7311E21A3784A3445134977E995282033BD0080B84487BBF23A499CC1D17172BD029406523707B671E25F9DD605C8473C42FB131B08A1E33F1E85055C2DCBA8B2B04F9B8C07D906384861D9A8CDDC54069D3E53E033E2530CF83C284A49AD15019F061C40B2D00AC7A4D727ACABD44DC48980691E0268B5B3FC1E476B3FDF9571F5CBC8DDFD400AB99\"\n        },\n        {\n          \"tcId\": 72,\n          \"deferred\": false,\n          \"z\": \"4E638D8AC3662450E09D8500DED751060B7990D54F137508B9897277F65EA952\",\n          \"d\": \"7B2EC50C53A67E0BCCBA98C2E319F5AB46B6E593D2465F14B23FFA03D0E5BE0D\",\n          \"ek\": \"E8A65EF0C7C7D7391DAB233EB277243732CAA45B53315B645FF9721D8AB0CFE54FF717157663B9098C2267633EFE3544A88014AD15C7B10A4551CB5F9BBA3D74946371B1AA54E6419CA7B4611149B5F02C73819A8C625AC52618F771202338AFF05969ED670BDD0B9DB562B8055884CBEA7A208A2DA850A05484C6B84109A5C2B498E8B8640ACC6F7AA6E1D7A95415469CC1C06D17176146CEFBA58E2B8929770851EBD6CA5DBA444548C622673DFBEBBC95767BEE2B86FF5BB37CF306B2945071EC1FA80C7540C56A36EB684D71C5E8B90AAA8C2421E21D64E0638D63C96E2B0BD9457BD5062199A01253D240C712315778AF487AAFBE28C650532A493C1B3E0251BC0841392839905AAD3D12289506742073862E8549E7E87EC28102DB7B89EBE91627CB95C0928C921978CB8C94875A94F1557498132200B4B76AA3C3AC9755C3B6BC5E5C59C9B3C6F7FB2CBB5AA457904FEFD2C370E29EEB2417BC45977BEA02A0DB9DE57937372298102755BA5B007EC88340B28AE7B819F9E07182F76070043CF3AA7E9256CF1CC3A6442B1726EB889CAB193222BCD3D8141843C1EF9921B8254712C04529D38846F595BC011B3288BCE0231ABC96985145043B579BB0864F40EBA6A66144E0972CDB10A71E44A088112B3C7907E5591D6EE85B34D4AF8DC58633081BA1A0A6EFE133B4C70ED215A22F451A7117B8C5F6A3D3FC4455214F5DCB8C31370546B4C4BA365A25017A9C2296AA641CBC4745BCAA72D17C9EAAB0155627BB45770D6D259BDF5B044053B4C7C8784FC433E89942B5D6C807A0707842CA4FBB98693303C118C9F4952123582FC4E22BB853944BFC1584810153F3AF5DBB74BB2928785C542AB647E9321D724AAC9EB9091707B5075C700A4817CAA2C355D4C99B8C5037B5288FF5279725BEBDAA5CE23609E5320981F11C8A116501D5ADD2D2B057C41E469257057B7A9C508017245C20C13A0C104FE1A517EE4621C367CE283183C07675BBE5895FBA4F30CC406772635982CEBC3153D046161C7B8BC4D87B648236263A44ED50B92DEBB624528C7A984C4C5047737A8021665279E9CF30069AADCC3F4A9245700436E646AF721A612828A07DC463D61B70223C7658F22190C83405999D92889CB0E5020052C31066665C4A021643C20764C370B9A5D1975D47646E1329072335432A19A03590B071482A3774BCA0601D8DBCC35A63742CD127F6384AFC6A971EE8CDD3E13687588D84C358A5537E753B2736840FD4E76A1CD0C68F9A990E5644404BBA84491E5F3B6F23EC45DF91538BD54070A702E69B66BF6C88CBDB8FDE693A2487681379A2AF94C592472FF167BD17966F9DF67E8FC51152AC613512930BA2BC2F453EEBCA91399B6D8F12AE891B3DEAECCFF120751866369BF8902D439D19F77B264002E5901B1C939FD86CA623277E6B09A72A93B52AB00754EAAA74833B8E366C8F0752639437F83CCC00F92EB7057C43AB94A75C31D5F854DCD3784920A36CA0C2DE244D1AE3A9E578B98E0634635549A8932A6BEC1F2EB321572A060EB110AF473F3E04323FD868A241891227222414C663285B05777BE82585DF3AB0ACFC121A642C541B3C3D7BB3157302EF62A1229156DB3609678B3B3E2576902145E5ECC78B65CF69BC8A7E6631494B804C4C89A9B595A8D95C039A9BA356670A536220C1CB54AC5BFCB7CE0125606A40C4EB4BAFC4C43BECFBAC84812F70C66796D6407F887CB0DB41F0FA7062E255896582B2B218FD96B6E5581A6FC26ED40272836950CB348AFE78CB135ABB6D015F299377632C72A7BBB2CDA7CBEC689E7551168483085B6B7426EB6967F729AD695D4E09C49B7A0010F80EAB54CBBAF4B641C956231663DB83512C97067CC7BFBEDCA93CC9A739A813B48C4793354C2DC642F869307AA191B77C2BC72BB048300B54906DFBD45255E92D81A98CD87149FEC3A428540D4E03C757260F3731CB7C4743A37194054AAE5FD6902D48A911F661EAD29321722E8243CE62E75ED82657423163A86362CAC2931954092909B0082BAD382A013A5044FAE9A12BF300670CA5771042933764E700B43259B130F033B023CBDA36815E2C6BC7C0601CEB7586A95DA4033E80E32123AB86C6135222EBAC0EDC17F5549D5B66760E07B2B16BA4FD8040E642990128A39A636B19FD3EDA4611BC1CDFD552AD1DB338FE3700F0920D56F3\",\n          \"dk\": \"0CC61046A844640446F07C1683D649F1168A00F1C859798944790CE1F884B2A99C177C579E314C91B33E483B86B64143EEC23F1587C8E038A42C85397A47CA7600C772D492E4CA5F7DC815100B9155449E72A0A526D24035FB20074941F0622FA9369E79D46378E45D5564245B0C10D9D873420895059428965CC22E064B349A4E40346F77A84BF7734C598C5775D39E1ECBCE2A6BCE6FA5CEA4F41A05949ED766C1A290299D9BB47DDC2E7A782F386C42D2AA04C991304BDB108801AB6802B04259757A63A88B7492A2FB2E93A1AE7646267F434C724134154C407808CEE0A7983C32727E44766E5C34EFD16C52086519FA0DC6C7098282555C069F76832F7707477F69566280BB6D406ED1714205B73A5711C449B91B4F8A2C2638B65221493BD940B5370A4C661F6F9C27AEA6A69DD0B61CEAA861E78328941091A95300602878222290CBC67E907D6C4CB2BD6623CCC5A23E46322498BAC5945E6B28283D7401B30401753859A2D4818ACC8D71437961DA02F2E8B5BA94B494E7A215F518C6BC8214C57237108C6B183F1551696A909C828A0E076341A5C141ED62A9987932D002A714366D8621CCDB0272E7923C6F3461AD341C145A864C6655C8E02AC0C91CC0613CE7D48027206C7D4988D82A1340A7BD337B82FA26400A4B97B5D819AF154CDFA639DA3A522DAA0F524B2B77FB422B8216FA83B8D94903452BCF5054A5081741B2E93F40548793B39B7197AAF027907B763179269CB06671B0CC2820F6015FA158CE93770A1A51CBF033ABA1A5C4363A1AABCA93F2800DC011B19B952774C4F90991001C19F3A688B2E01C934A7BFA705CD093AF85BA5D20FCB498E5C91B186378B63C04123D19A8C2B5209D7D8799EA7192BC3876D1943F17FB63B8185D89A37483AB74E62BB03C27BA845CCE57309DA7A8055642B9AC51B820AA8F61F88B819804DCD8C3CDCA2AA816BDABE9725BF83C61796ECB069D183600FEBC41114182338C97B67618DF818DCA151AED240E36C1A0DCA654886C6CD3731A92E3CE81D7CBB4BB1CCDD238C54C44E89126641AAEE1145BBC378C92C56E400B5DE838A18FB66D365501B406287ED8AF6D03CBCA4BB617B42DBEFB4A0435319A00133F4AB70631C1045515C6C23310F50406780F9E891187FC96736296B9E7AB1609C51A103AC34667925901EE3A9116CA97FB46B8325B13A64B008A88B03E0C383E152D7F8593B6638BA7F9AB500968A830042CBB50638A58AEC5B6BBC1938E5C7A605A80B161BC18841873ABB5F38ABCDDE99464736D3505C9EEB86E8A95747CF9CF3B9A52BB115505B156928A79DC9B9E1F110987D4808AA2C27B806D7B746014476E571AC2F416AABADB36CF994813887195CB63C15C678D480443CC0D2C60365E358F1FA9897A12279CE928E2CA8D67006D48592F54EA4CDEAC2748940AF558356760C420EB71DE2ACADB064586C5C01A698330266DC78C95DB8CB51907C6DD94B507C107E9FA7210C60FA5D8888A3CAD7AC1B6FEE59159D6212403B2B21944D0AC35B0137989F002D4F13E8B873F8B53B3ACB09478F4C00A7486AF0B5FCBD42E21654FF3DB786F2307E7E60236EBC89C36971E2920A27C5E7125BA1DC77B4F944BFDA8C4D3E09BC8A00B3099B6EFEC5F7F59AD925B79C3311A7F02433FB9133F845B58E0BEEF5611DE6890E45AA75645C34F832B08B7655CD6424F733FEDB9825EF3340A010E72781D16F8180F227110402816E6524D8B341EF5CBD098A8CBEB52C02B715D1789B0D38250EA6730E506E569A9E32A5A35C7341181B9E0C46296E91F3FE9C5A7159F9126BE378BAC03D467C7A39AD1605DDB2A3C7FF930A85C0710114ED2094956E005DD770699F4CD50767E53C15D5251B2621336CD6974CCD446C1C4028C62839EB699D869758E502CA158A39798688D95B1C4509EE9C57383209D69D105D56AB55A59955F78A65E49B5A4FC4A5791AE5D79118CE2B22AA9BA2DCB963BD672A6861D78C30D6873893E15C62A428BA3E77D09C94822693166E39C32E32666F4537E70A641181C13609F1C3535A91486FC3ACCCB29A0EC0133DF7917B71B8EE4509264788356E967335942014213E4862AA8B7691AC207A996711AD1AAFC30BCBADC0A35514F3FFAB45E53197E89B918AC75B504647A3684E8A65EF0C7C7D7391DAB233EB277243732CAA45B53315B645FF9721D8AB0CFE54FF717157663B9098C2267633EFE3544A88014AD15C7B10A4551CB5F9BBA3D74946371B1AA54E6419CA7B4611149B5F02C73819A8C625AC52618F771202338AFF05969ED670BDD0B9DB562B8055884CBEA7A208A2DA850A05484C6B84109A5C2B498E8B8640ACC6F7AA6E1D7A95415469CC1C06D17176146CEFBA58E2B8929770851EBD6CA5DBA444548C622673DFBEBBC95767BEE2B86FF5BB37CF306B2945071EC1FA80C7540C56A36EB684D71C5E8B90AAA8C2421E21D64E0638D63C96E2B0BD9457BD5062199A01253D240C712315778AF487AAFBE28C650532A493C1B3E0251BC0841392839905AAD3D12289506742073862E8549E7E87EC28102DB7B89EBE91627CB95C0928C921978CB8C94875A94F1557498132200B4B76AA3C3AC9755C3B6BC5E5C59C9B3C6F7FB2CBB5AA457904FEFD2C370E29EEB2417BC45977BEA02A0DB9DE57937372298102755BA5B007EC88340B28AE7B819F9E07182F76070043CF3AA7E9256CF1CC3A6442B1726EB889CAB193222BCD3D8141843C1EF9921B8254712C04529D38846F595BC011B3288BCE0231ABC96985145043B579BB0864F40EBA6A66144E0972CDB10A71E44A088112B3C7907E5591D6EE85B34D4AF8DC58633081BA1A0A6EFE133B4C70ED215A22F451A7117B8C5F6A3D3FC4455214F5DCB8C31370546B4C4BA365A25017A9C2296AA641CBC4745BCAA72D17C9EAAB0155627BB45770D6D259BDF5B044053B4C7C8784FC433E89942B5D6C807A0707842CA4FBB98693303C118C9F4952123582FC4E22BB853944BFC1584810153F3AF5DBB74BB2928785C542AB647E9321D724AAC9EB9091707B5075C700A4817CAA2C355D4C99B8C5037B5288FF5279725BEBDAA5CE23609E5320981F11C8A116501D5ADD2D2B057C41E469257057B7A9C508017245C20C13A0C104FE1A517EE4621C367CE283183C07675BBE5895FBA4F30CC406772635982CEBC3153D046161C7B8BC4D87B648236263A44ED50B92DEBB624528C7A984C4C5047737A8021665279E9CF30069AADCC3F4A9245700436E646AF721A612828A07DC463D61B70223C7658F22190C83405999D92889CB0E5020052C31066665C4A021643C20764C370B9A5D1975D47646E1329072335432A19A03590B071482A3774BCA0601D8DBCC35A63742CD127F6384AFC6A971EE8CDD3E13687588D84C358A5537E753B2736840FD4E76A1CD0C68F9A990E5644404BBA84491E5F3B6F23EC45DF91538BD54070A702E69B66BF6C88CBDB8FDE693A2487681379A2AF94C592472FF167BD17966F9DF67E8FC51152AC613512930BA2BC2F453EEBCA91399B6D8F12AE891B3DEAECCFF120751866369BF8902D439D19F77B264002E5901B1C939FD86CA623277E6B09A72A93B52AB00754EAAA74833B8E366C8F0752639437F83CCC00F92EB7057C43AB94A75C31D5F854DCD3784920A36CA0C2DE244D1AE3A9E578B98E0634635549A8932A6BEC1F2EB321572A060EB110AF473F3E04323FD868A241891227222414C663285B05777BE82585DF3AB0ACFC121A642C541B3C3D7BB3157302EF62A1229156DB3609678B3B3E2576902145E5ECC78B65CF69BC8A7E6631494B804C4C89A9B595A8D95C039A9BA356670A536220C1CB54AC5BFCB7CE0125606A40C4EB4BAFC4C43BECFBAC84812F70C66796D6407F887CB0DB41F0FA7062E255896582B2B218FD96B6E5581A6FC26ED40272836950CB348AFE78CB135ABB6D015F299377632C72A7BBB2CDA7CBEC689E7551168483085B6B7426EB6967F729AD695D4E09C49B7A0010F80EAB54CBBAF4B641C956231663DB83512C97067CC7BFBEDCA93CC9A739A813B48C4793354C2DC642F869307AA191B77C2BC72BB048300B54906DFBD45255E92D81A98CD87149FEC3A428540D4E03C757260F3731CB7C4743A37194054AAE5FD6902D48A911F661EAD29321722E8243CE62E75ED82657423163A86362CAC2931954092909B0082BAD382A013A5044FAE9A12BF300670CA5771042933764E700B43259B130F033B023CBDA36815E2C6BC7C0601CEB7586A95DA4033E80E32123AB86C6135222EBAC0EDC17F5549D5B66760E07B2B16BA4FD8040E642990128A39A636B19FD3EDA4611BC1CDFD552AD1DB338FE3700F0920D56F3771F1733A4C185573FFD9BC77988A1458D28A64F15512217C7B95C24D7CF48904E638D8AC3662450E09D8500DED751060B7990D54F137508B9897277F65EA952\"\n        },\n        {\n          \"tcId\": 73,\n          \"deferred\": false,\n          \"z\": \"7459AB99D24C1254EEECC035874BF19A64EFC8EDC9D369C11F5DF4DC83AB5FBC\",\n          \"d\": \"16858AA7C92EBD72FB8CCD0A99D0435EDB2A6EB1B936DBCB637CF43F25D221B1\",\n          \"ek\": \"379C74CE8940E12A1790659FD6C2431D7C1043A8049A3B0AC1000273A0A68EC34AC6435018C19F3A5A18CD28AFF1254C27E92E25149E488A5CF5C724873A7CDEE0182CB874E1564F6036CCC6E5C02FD19937D459ACAA589BC3706BB60E82C93047030524A73C3831232659565FF49DBAB60BC8C18E203B68DE7784D96B4C1969AD8F164EFD531F8A297F46425CBBD0C4E3C858A01982DD4686860B0253D1067AC66353F7554DF00A2FFC46858C6693F04499054458864A4A7AA82230A4B04C1EFF8A2B9C5B1A43F0038A027CCDF73EDA1B16CE878D14B27DBC2AA0019BAED2C86F9741A9DFF624071545A4CABBC2895D9E9602C1027D05EB1C70DC2365423A65740F777565503C53B31C9D7DD06DF115BC2CC1BA80D992D185C485A78FB6A0B725E2A93897CF04F1B421093FA098BAF4B829FED6CC07073F0D13502E430F892A5E344B431DDA1EC9270A0DB64CBF40CFDC78293826BF4701181594B89F8CA52F3168DB931E7242121A71747564AF2DF0312BA4659A8BA2106983957877673269313B953534824E035AB75A5007B7BB5832B37936683B23B563FAB86EB749819616AC467B42D4B11922A9719067039480849B13E125353F54179B5524C7B5905A673E54EC888614AFA7781B918951C2C30E89043119351965256B26CC5B8E1973C1E838C6297D1305D02351A1C2E394DDA666D918545BB8CB69CB8141645AC7CB606A0500E0F94961CC8B33A5250E191169C3B4CBB927438764B356928431A7D5E73067346C9EA02D7F687A9DF1BE9018473CC7442F801B43F2307FD2380329C5D3DB8802823864A8895C0262A97547682A61A1652F4611717C09AC6DF8886D67B1213739DCE29271F65564CC642851005E224D634C996556AE35E3CCA9A6C746F7609CBB526C9B9CB144CEF1F39DBCD11207F43FB7296193B12F1E7B043DAC724FF9A32581192E13166FC91F10CC5330D6A7C4F04BD3248C61D227E70543E041354D55B1E52AC1BDA0657ED3C6F5F3444B3775DE1B0C337409C34B75A5B9A58DA8B7445C2E3A6BCCF1E02A0C1A20DC619893E6370D35BA4CB615D5B2BF12885749865675B9A925B21EE138830E20B2BAD258349A44747A08FC962476A0A7A1676415539086A543A087737DCC8E2565161F8697043819206202AB240C00C496D5254E8B0610D6C55CE52B20B0F63D65092F95879736D989728AA6EC91588F60682512CC50BCAA756A12F13C6C502BBB6CF210A7877DE46831A1E91D7549B50AC391A439BCDF2135FE81C81B25874189726D77AF828876A4F34E19E33009B8217E511DB2A7724B251109646E112823242C964B933F8B4625EDCB1471DC714684045AE79811A56B113803EB272AAB94709421239B009C3D4CC8463906DE646E7374C865103AACD4190206C43BD2334D496005427FDA19CF31872A941C75A6B1663F6C57754CA0D9D8A23A1B9592A1B3FF76A035F89B72B29E6207D05BAC0C349608E71B2E4A3CB3EA7651A26B374CE516C787976691BE124AB671049F3B573905D19906CA6E9C5912F6311410CA3A7B25973895C0E8733557A463E311BA20191EB77B3EDDDA437BC75D5F358AC883B2E8242449834AB6F5ACBF0422ECB438AA4009BB678E88D033C701322DC01D0F9B47EC135A25CB2B491B7F45B33686788F603B28E0A215AA1AA49BC2B2B10906F70004DAEB16F4E5805DB455BD794861BA1F8D55BA6CE29591728CBA492B89255762B25DDB71557E8366601535DD38609652CF47506379673B0124796C3CA7EB384EA0830DBB318E553525DEA69187A2151A227016DC53684C1922A25A90107C64E04CE1C7AF8481A437601D7C4AA014D18CB397B4079C660736453B35A5BAAB9371F0B5D4F867CE15AAA8739F6659B12FB93667DA30ABA01CE923853DD26187BB20A845691AB48785B22767ACC0D7389BA260BF115873CB784CB4C715E67776759511DEEC2B565361B84A8833C7A16C16AA3F29750397B0ABAA9B73A580527100511686A19A7AE757833EF826F4D21E4C020618809A48B67156711E70D5171BCCCF997CC048BBBB2AE064FFB9143F66A2E81A9341831999D822AD947CE88CB153BA595D5B0EF5D08EB856929412B9D2E551C959482AB545B1A0AD8583B7C0D42A95A3A080B772C77424A1AA1852DCA0662BCBA7B96CFAC5B1B689F01A5CEAEB2A0D52EB0FE9BF752B36B37830846812FFB88D\",\n          \"dk\": \"2ED75F448A5247A0BDE73C1A09DA83F27512A9E1998A48C80A0962ED307C0F8C4D221584D3A90D6C7B003EABBA2A2013F4D74CEF9A84AD123E430B316C08AFB04A64EFB31278F4CFAF1156285679203A275B267AAF7786346B40566B5BEC24B122C89108683431D0A8E8531EC70116F648004A9BA3DA807E54755BB10B9DDE973DD1E62C1A6C752D09B18DC00178C52A6F0504ABAA2B3E54B1D1A495A9A38BBF29812C472D28A8386F04BC41F555263A8DF34536E1925434407B3F885F7349A2C20BA5F275BBA62039F77A0BDD236EE4A7205B660AC04A48D6EC0927EC8986C73539754545E96B3F681F461A5424B954C5C69F1899BB8301B0138BBBD934A764168951B0BB42043FC6A441BC329574C726E9D31286063DA4556C02087FFC473C3B4307986A59AA595139A46351FBAAC3E1664B5A4A74D5916EB3BCB0F570B330ADF4574A92589C782A3CA520C43C4BC7987367BC64B9EC1629C41C80223154FFA3161A158647468B8DC670DA26496FB53F1BAB59261B8B9A3A725AC992567ABAECD027856650A9D78FA4656E3BA89BFEB80F7007562AC84EAB2917E18432CC5A4BCB969DEA5C73F1AC620612574F3106D1646FC266959723004C425BEA6B8B8E6326365C540C2844F666A482116D16C1272D90A439567706159C90623B1A79B62B14860926568C32360EDC9C868211E0036C9A3979B6F39126A150A9259CABC8631D9214CB43A8564A6DDBF785AFD44EDCCC7D2428A92766AEBA969D9E065D05E772C6DA572F9B0A7C5A7C0E54020B331CDF321CE7E48EAD96C4EE5062509113E274578F50A7AD131641BB3CEC76CA79B39529F92886B40F22086A6F527623E37797F1009E082277CA420C3310C0F35F2BD61776B8C5FCE14136DA388FE6CC37F3A7496C0062D0ADDC91B8ABA38817F45A7996AE0F7BA41D32C1FC255A43A6BBD9DC6A65A6BB72A306A3446418A77BC58C1D3B1B05437C6F85D01A300C9FADBA40F8F4B8DEA2CC358547165A467AD12BEC5C80C4F7AD6F308550B419BB06AE54CA29011A55502215CB0397C005544404B9A413CF74F5AFAA532C909A3D00FD37146721FB0B35363B9C52B71511D6C45B719E33CC43E016770936BDF6D2056AC21FF65CC813321144B98C965530A80654EFB350EF973AD505791766BDB4734C3E1A0394A89902416C3DE269B9A99E4B6CC39D64BCFAD64A552728905882278A238B214B94F62856C14C6294B8FFE80F3E62B3C91A687503094C2849BE8254314B56CDD35617D61F27C9960388811C830290808F9D0B2DA6AA4728606F55CABCB92AA9384CC3E03537BAA41BE636C7EFB036B5114019E40C74984884DBB512D61CC8A973D8E6C8EAD45E235C7E6235C181C062E82CBD26A2859BD9B510514D83114847ECA0DB8554663B1D74E5084460C396E43F3FF47D379A4574C77E8F1C11CDC5974471663244CE2EC399AAE83CF96889BE7628ED028E477C0658993941AB737861A9F8388AD1309BBBB23EE72181A1EC7BF445B1FCC5527E9C0F7D278362548131CB2132691E232515CB5C5CB6640E01F8B75D96A9E60ABB6929A7582219A8A8CCDAB2C4A03A035B8CB416114FF7604692427B35C135DB956F6BA796145CAFFB066663293FEEB462EF5C4D8C521B86436F011157245A891F233AA122C51AAA0CB0D5100B01575EB04B9A1B86D6A1808357265DA0570DDCCA2766C5802333C64B0D9E5A254A8645328C394F85B337A447F17753BB465F584462B40518705094D891C6143114522BA327547B61C928CF50758FA5C0C6FB2255A51E13E9AAC37000053C180EC7A6E597BF57DC656123793EC0CB24E9306FC0CA5CB723C8F46174179923E819A1DC760229018D7348AAD75263DA565129A39006299CD979BC73062003D0773611B07141FF2265F8E5583284201A4111F5719BD4397909D892AE8AA438121C5DCBABB9413194542E6D98AF6655871DF23C6A7A43B6B6C9D0790F53705F419B226497BBE92A3EC6315C6A443878B283A252423B13505DA298AE100A6E34BDEB734DAF0588BD54B2D5C1198C653B34A8C4D0447C5B6B137D203BB2612E46AC4B82BB97931550F3F835B57B8521828718498A3405C36A345A1323924221505906723847571C6985B0A59F1FE15C961B5F11630D04CA79FFE22F371A96379C74CE8940E12A1790659FD6C2431D7C1043A8049A3B0AC1000273A0A68EC34AC6435018C19F3A5A18CD28AFF1254C27E92E25149E488A5CF5C724873A7CDEE0182CB874E1564F6036CCC6E5C02FD19937D459ACAA589BC3706BB60E82C93047030524A73C3831232659565FF49DBAB60BC8C18E203B68DE7784D96B4C1969AD8F164EFD531F8A297F46425CBBD0C4E3C858A01982DD4686860B0253D1067AC66353F7554DF00A2FFC46858C6693F04499054458864A4A7AA82230A4B04C1EFF8A2B9C5B1A43F0038A027CCDF73EDA1B16CE878D14B27DBC2AA0019BAED2C86F9741A9DFF624071545A4CABBC2895D9E9602C1027D05EB1C70DC2365423A65740F777565503C53B31C9D7DD06DF115BC2CC1BA80D992D185C485A78FB6A0B725E2A93897CF04F1B421093FA098BAF4B829FED6CC07073F0D13502E430F892A5E344B431DDA1EC9270A0DB64CBF40CFDC78293826BF4701181594B89F8CA52F3168DB931E7242121A71747564AF2DF0312BA4659A8BA2106983957877673269313B953534824E035AB75A5007B7BB5832B37936683B23B563FAB86EB749819616AC467B42D4B11922A9719067039480849B13E125353F54179B5524C7B5905A673E54EC888614AFA7781B918951C2C30E89043119351965256B26CC5B8E1973C1E838C6297D1305D02351A1C2E394DDA666D918545BB8CB69CB8141645AC7CB606A0500E0F94961CC8B33A5250E191169C3B4CBB927438764B356928431A7D5E73067346C9EA02D7F687A9DF1BE9018473CC7442F801B43F2307FD2380329C5D3DB8802823864A8895C0262A97547682A61A1652F4611717C09AC6DF8886D67B1213739DCE29271F65564CC642851005E224D634C996556AE35E3CCA9A6C746F7609CBB526C9B9CB144CEF1F39DBCD11207F43FB7296193B12F1E7B043DAC724FF9A32581192E13166FC91F10CC5330D6A7C4F04BD3248C61D227E70543E041354D55B1E52AC1BDA0657ED3C6F5F3444B3775DE1B0C337409C34B75A5B9A58DA8B7445C2E3A6BCCF1E02A0C1A20DC619893E6370D35BA4CB615D5B2BF12885749865675B9A925B21EE138830E20B2BAD258349A44747A08FC962476A0A7A1676415539086A543A087737DCC8E2565161F8697043819206202AB240C00C496D5254E8B0610D6C55CE52B20B0F63D65092F95879736D989728AA6EC91588F60682512CC50BCAA756A12F13C6C502BBB6CF210A7877DE46831A1E91D7549B50AC391A439BCDF2135FE81C81B25874189726D77AF828876A4F34E19E33009B8217E511DB2A7724B251109646E112823242C964B933F8B4625EDCB1471DC714684045AE79811A56B113803EB272AAB94709421239B009C3D4CC8463906DE646E7374C865103AACD4190206C43BD2334D496005427FDA19CF31872A941C75A6B1663F6C57754CA0D9D8A23A1B9592A1B3FF76A035F89B72B29E6207D05BAC0C349608E71B2E4A3CB3EA7651A26B374CE516C787976691BE124AB671049F3B573905D19906CA6E9C5912F6311410CA3A7B25973895C0E8733557A463E311BA20191EB77B3EDDDA437BC75D5F358AC883B2E8242449834AB6F5ACBF0422ECB438AA4009BB678E88D033C701322DC01D0F9B47EC135A25CB2B491B7F45B33686788F603B28E0A215AA1AA49BC2B2B10906F70004DAEB16F4E5805DB455BD794861BA1F8D55BA6CE29591728CBA492B89255762B25DDB71557E8366601535DD38609652CF47506379673B0124796C3CA7EB384EA0830DBB318E553525DEA69187A2151A227016DC53684C1922A25A90107C64E04CE1C7AF8481A437601D7C4AA014D18CB397B4079C660736453B35A5BAAB9371F0B5D4F867CE15AAA8739F6659B12FB93667DA30ABA01CE923853DD26187BB20A845691AB48785B22767ACC0D7389BA260BF115873CB784CB4C715E67776759511DEEC2B565361B84A8833C7A16C16AA3F29750397B0ABAA9B73A580527100511686A19A7AE757833EF826F4D21E4C020618809A48B67156711E70D5171BCCCF997CC048BBBB2AE064FFB9143F66A2E81A9341831999D822AD947CE88CB153BA595D5B0EF5D08EB856929412B9D2E551C959482AB545B1A0AD8583B7C0D42A95A3A080B772C77424A1AA1852DCA0662BCBA7B96CFAC5B1B689F01A5CEAEB2A0D52EB0FE9BF752B36B37830846812FFB88DD27339E75E5E384EBA68A71FE2E52EC7AB0C15CFE33BBAFC892DB62D84ED070E7459AB99D24C1254EEECC035874BF19A64EFC8EDC9D369C11F5DF4DC83AB5FBC\"\n        },\n        {\n          \"tcId\": 74,\n          \"deferred\": false,\n          \"z\": \"4CC1CA6B662A4CE499EBE66D933CEAE58EE244CBDCAAE3C1F45A0D6947802B76\",\n          \"d\": \"F788F3E21D62E74090582F310BD4FDC8065E56E8D946142B9B9CF8F338F330E8\",\n          \"ek\": \"26160EB381AE1A868F10FA05934958B39790C5858085A82036224D2455823F11A3A5C537DB483A3AA7725ACA567CAC610B4618E3CC0381701FB60917F5DA63CF712618535210D102238A5768F876A17C4437AB8789662F426C5CE69258C3462D70636CD556AD9EF0A2CD9C1DC3C546A0B396CBE2C94BB24C40756815F796AEE7045B6082DCC49EA56849C639419871C480185E51E4AECFA1945DA10B43C366916531ABA8A01502881B76C6E6984F7EA6058DE460C1E3511A06631670B35ED6A8D5D7BC6589A62DE377BAD1582C085A3DB3BC47F176A27604035B9ED13853758A77AA1391E881214894013F1BAB687A699DF18B373A64073BCCBEE469E1C867D221C4675791F40102E1E2B12593152C87A47A9367833854EF5427DE49043308586673274DEA8C2AC8AC5EF5886D06205284831170BC6B9C0844248DF1CB40C5B627A29421AE900EF5F5666720304520ABD826AEC0476E0D32213D550D55442E863B0FB8ECB85A1CBDA002792F6C199838613780BA4CB22605B30372304F88897EEDFA09B6B0C4B4892C0A05746DB80F602C016230129C2B0C83351413D4C2197B92E608485301BFA2EBA2BC5C2E1EFB42109B0119F079560A87F01BC6CFF916E81A1AA4F520C0687C41C91A3BB5C4C1D712449C6E927C0490B2AB4B9422BF5655E332C4A1D1626E587F085A631C250C122537F5B213A0C231DC48CA7B88A4C21706FE701645F43A8A00CFA0F0A13900B3FB5C1B886C66F22520FD685AD0CC396991B0ECE81635BC27AD2813FC377E7A542D9A9602E22775CA34CE85B834ED755048286B82692E3F9AA15E89A8D8568FA2AC47AA32C748582B7889B447C079F404329FE03066E8A46412820EDC8B8878840DE54C80CC2851E08374A89FE8F7417CF90638726ECA61072C893CAF8C501D04CFD2F3BFFC094503234693C530A732324C0516E412137E2B39AFF1292B925205C972F1B6805E67A292B069E8CA73FFFB1DD2B1A5FB2057ABB67ED396494D123D05A579C23263B47ACEAC5862982B20E6BC5E32E03BCA175CDD8996044AA9D90B3A0488500DB340C06CC9B84749B4497AB6A3CDF05B58FD5850CFD911B5C58819D2444CB9386B18784762B16DF6BFBCE86BEDC9CA6EDC191171C88C5060648B419109C5BDDA9A7E98C48B1649B342B7ECA411959A9D62E5C0BEFC5B62F4BB6EE608CA18B43AF78784B8C7384ABC77426B5CD28EE70B93F808317230A686900C83512E2A83CD10749C3E5B7EE6826C1E01CC5FB27FC47B52DA462AE5EB65390A1A33CC9749941D530210AD070AB71487C2176B17621ADB11CFA1F3AA9C379C9FB0CDF00186608246C56584DB61AAA8C157A28B6161F23EDEB7365928C1EA8C962785C338E66FF1897B56154475E17F81C9845C4607E5294E21EA23EE0CC47E4BB9D3539D46D410A3149E098B539B196A2C90C06D5C103AAB08E1110D3F565AA8CC7E4F2167E6969BEDD5C3CC8A6F68F53E18931F8EF0723FE33A2789661C248B15A35ABD5919343AADC93BC6777539E952A3F49A3E34F51291709F6DBCBDCB65B4760B8795B7985DF8249EEA132A473CAB736E72D45A2A100FB560B70E600EB24AA596879F1B1A9DB705AAA489791A503E9463C19EDAA4ADAAADB77A6FB0BABE16973E63333F03B444C85C980D55727532765397B84870BB759469CEA72E28A75BBA8A5098B6A95750ADAB54361C802ADEA678FB6733D3766F7C50BAFC081A384A768E60AA2E6648ED81612778033C6038E7863887F19C1509518E61ABCFF77F1ED9459D10C53C0CC6AD60C95704138B9C579317714F393AE8287909ABAB7FA167B8B237CA52056A199D7558AD5F16821DD364B11B9307421D362BA42AA3002AD2025E347BCD7223EE4BA62C354C1CE9822C8C4B3B70A1708462453292C73ABB177949EA3106FE3075CBA15E2E143DD13A6600E55D801A440004621F6ACDC3CB8D53236FCCDC15B9F9CD321398AA230DA6722DDACBB98106367B3723FEE198E1AA7161C004C9BA1840F19721AA7D99B99A7057B1B33A28F2033904750ED1899551C1C8A9132D86B3781264CAC231C3A21440580A92F4C683CA90BC1CA78CE41B332EEA7D2F461425C9B91FB6867B4270AB785329218D74356A3B8423A044C3C6653B09E60821A1CA6032A0BC526228F18B9E38B3A7B69EA6C1C5C0C39DE56DA9763517FA3F65CEBED43C7B61282772DBC9\",\n          \"dk\": \"A0CBC9A9EB303971AE19D11CB1023CAE83B0578A8BADF6BBCFB444000041941120984930702ABE4D0442E79921D92CCD08364112D41433013110D74ADF2B53A8164582F56E2DF03FC1D283424A16603CCA86C8BB3211030574553A221A4236817DAB9935899201465FCED39A2E0845DDF09F2B346FD3FBB2B6EB72C7C6CB8EA1784A010AC17A79041408A1B7853BDA03161A478D48AC4D19C32F728C36D2CAC5C4784449A0BB5C02C4E506EDF7A7A1A8548F931FF36014A8AA651527A4AFF04AB5E52A29058DA78388192BA72D17A9F3428E784999C9F59ADC561CA971021C7C9DB552926D572F44F9B2B014ACA32A267EFBCB62D1962F48B6D867C12FA25723736088E80CD3024A2610C727CA96AA8C495A8688DD4C5F804C308D4A1693E6B2C676B3B44C127A4911F8522308E20EB5E816A8949D93303BBBA150744511A9C23259F663AB3444AE787FF766A3D76859637588C21634F7338FE98596911A92E1692E75D66CF37AAD1C02720DD47315B1AD9DA95AEDF383D6863D7F06214E626B3299A5EEA648CAC40067C5C271A81A8328C7612858BE4C736ABC35E3F982CD84C596685A3F974D61F450D130C1159993BADC5AEF7B71F9E1503BF2525FD2B191E95E35A852D65082928C47DD989646FC5FA9E92292EBB03FA4B52473B57FF59E52ACC2311844C59104F51147CB600D0DFB42205A225DB32CE6843557F61E3199BE1E8A237AEB1240B51113233C92E453FED3138501327948A68373457AD6271ABBB6CCF860DA15241F89C27D5074B6A7170407B3024708FB3689318AC024E0CCA2836D3D786737D298CA7559FDA75F38CC8CD3D4845A165E18EAA56547928B0875AF564F763C365839879AEB3962F520D37755A17983826843E55567CF2B45F2B59D716666528256651694FD4B5913D90A25637369779991E9246E6A59EE1437D7DBB62466A759301CD0F4CC551161C2662E0D0992DDF07CB4BC61EFAB0D4F81700C0694F971AF7CB0974367AA53A8A914770D03892B639746AF79179AB8405C08A5DF9525E5958D26C4543A82349A192A86D3529F660BFC49490B2147D9170FB5CB78FCB40863550E9AC42DD3215A2D86B3F3A4B0188A9F7F02949CA48A63DBA56224B436B2C22C085EF826AB521B0DBAA4A7ADE8355FA3C4BD708EA1AB68B1FA714D5841208BA9CF3611A9854F69A21E1B67C84903CC382B3663ECC2AA4B069A8639618536FB6B562CCA390C103645D9A06481666E92C1FDE91AD8291596286501B154C8E3AA6E0608E4CA6FB1512EBD25C9EB789E7765B342AABAC415B7B1E65FD38ACC880C61E1C01E96BB8FAC50972D536D49A20B46BAA303DAA481A39D68C71286495F248045B1B82020544B864C3E1951812D01169B4AB79510A284F058CFBC963431BA0FB7209BE995672A264DA3A6EF592EB121093D9B09F35A94BAF125C4F61508B1C339C6B58C5BA65A207571191253C3512503C400C70BEDD9859E363A7A1193CBB2C222782B77B62CDDD09F67BA9F4D632930E73E08F37F24279965C74BB402439E0A8289A43630983BAC924D0261BA07335B3F734428F538B2803DD47B26B2D5183533807CC43B67E79680E6C5E60709406B04548781CF497E8B890423F42186292FB8C988639C2F6DACC513014B91EA2D54393CFF768ACC42504DA84F21A21D372A2C7935334654856BD66C77423F10501CBCD39907EB7478FC0D7D0439C9739B43A363E324B319B219BD25BC1153AABA81B0F6A84B1D5A8C92885EBAC488099BCF47E91C8E4AA8EFACC0748B25B2969EDE004B4BDC3278B139C2445CDCA181FF8A3FD20777DA97CA9D182AC494336D424D87B53402596A0BA339611B5EF459B87C0163744007B008467387B44A680F3D916C8AD9352D7B6643509225874A05E03D2B0297739592AAA37772472162393CE7238482B51E8B6297E665CD9FEAC4B8664BA62CAB68FA25DA4459816922AD50AD06A347E40C4E5E55CF9A5CB738343DFE222FE46349E68A6526B1B1AA8CB5549BAEE622C2DB3C8806AB22F3C92449A67D3C7C5AF7A9C970839AA386A6D5518E00D8B11F42002F52C873DB6A4DB50047F815EE15C3D017676857023C57A017E431CE8555CEBA2BF48950F89095C2D278A1330D9F9416BE6442EC49A21C79BCFCF832677A4CEC8ACE26160EB381AE1A868F10FA05934958B39790C5858085A82036224D2455823F11A3A5C537DB483A3AA7725ACA567CAC610B4618E3CC0381701FB60917F5DA63CF712618535210D102238A5768F876A17C4437AB8789662F426C5CE69258C3462D70636CD556AD9EF0A2CD9C1DC3C546A0B396CBE2C94BB24C40756815F796AEE7045B6082DCC49EA56849C639419871C480185E51E4AECFA1945DA10B43C366916531ABA8A01502881B76C6E6984F7EA6058DE460C1E3511A06631670B35ED6A8D5D7BC6589A62DE377BAD1582C085A3DB3BC47F176A27604035B9ED13853758A77AA1391E881214894013F1BAB687A699DF18B373A64073BCCBEE469E1C867D221C4675791F40102E1E2B12593152C87A47A9367833854EF5427DE49043308586673274DEA8C2AC8AC5EF5886D06205284831170BC6B9C0844248DF1CB40C5B627A29421AE900EF5F5666720304520ABD826AEC0476E0D32213D550D55442E863B0FB8ECB85A1CBDA002792F6C199838613780BA4CB22605B30372304F88897EEDFA09B6B0C4B4892C0A05746DB80F602C016230129C2B0C83351413D4C2197B92E608485301BFA2EBA2BC5C2E1EFB42109B0119F079560A87F01BC6CFF916E81A1AA4F520C0687C41C91A3BB5C4C1D712449C6E927C0490B2AB4B9422BF5655E332C4A1D1626E587F085A631C250C122537F5B213A0C231DC48CA7B88A4C21706FE701645F43A8A00CFA0F0A13900B3FB5C1B886C66F22520FD685AD0CC396991B0ECE81635BC27AD2813FC377E7A542D9A9602E22775CA34CE85B834ED755048286B82692E3F9AA15E89A8D8568FA2AC47AA32C748582B7889B447C079F404329FE03066E8A46412820EDC8B8878840DE54C80CC2851E08374A89FE8F7417CF90638726ECA61072C893CAF8C501D04CFD2F3BFFC094503234693C530A732324C0516E412137E2B39AFF1292B925205C972F1B6805E67A292B069E8CA73FFFB1DD2B1A5FB2057ABB67ED396494D123D05A579C23263B47ACEAC5862982B20E6BC5E32E03BCA175CDD8996044AA9D90B3A0488500DB340C06CC9B84749B4497AB6A3CDF05B58FD5850CFD911B5C58819D2444CB9386B18784762B16DF6BFBCE86BEDC9CA6EDC191171C88C5060648B419109C5BDDA9A7E98C48B1649B342B7ECA411959A9D62E5C0BEFC5B62F4BB6EE608CA18B43AF78784B8C7384ABC77426B5CD28EE70B93F808317230A686900C83512E2A83CD10749C3E5B7EE6826C1E01CC5FB27FC47B52DA462AE5EB65390A1A33CC9749941D530210AD070AB71487C2176B17621ADB11CFA1F3AA9C379C9FB0CDF00186608246C56584DB61AAA8C157A28B6161F23EDEB7365928C1EA8C962785C338E66FF1897B56154475E17F81C9845C4607E5294E21EA23EE0CC47E4BB9D3539D46D410A3149E098B539B196A2C90C06D5C103AAB08E1110D3F565AA8CC7E4F2167E6969BEDD5C3CC8A6F68F53E18931F8EF0723FE33A2789661C248B15A35ABD5919343AADC93BC6777539E952A3F49A3E34F51291709F6DBCBDCB65B4760B8795B7985DF8249EEA132A473CAB736E72D45A2A100FB560B70E600EB24AA596879F1B1A9DB705AAA489791A503E9463C19EDAA4ADAAADB77A6FB0BABE16973E63333F03B444C85C980D55727532765397B84870BB759469CEA72E28A75BBA8A5098B6A95750ADAB54361C802ADEA678FB6733D3766F7C50BAFC081A384A768E60AA2E6648ED81612778033C6038E7863887F19C1509518E61ABCFF77F1ED9459D10C53C0CC6AD60C95704138B9C579317714F393AE8287909ABAB7FA167B8B237CA52056A199D7558AD5F16821DD364B11B9307421D362BA42AA3002AD2025E347BCD7223EE4BA62C354C1CE9822C8C4B3B70A1708462453292C73ABB177949EA3106FE3075CBA15E2E143DD13A6600E55D801A440004621F6ACDC3CB8D53236FCCDC15B9F9CD321398AA230DA6722DDACBB98106367B3723FEE198E1AA7161C004C9BA1840F19721AA7D99B99A7057B1B33A28F2033904750ED1899551C1C8A9132D86B3781264CAC231C3A21440580A92F4C683CA90BC1CA78CE41B332EEA7D2F461425C9B91FB6867B4270AB785329218D74356A3B8423A044C3C6653B09E60821A1CA6032A0BC526228F18B9E38B3A7B69EA6C1C5C0C39DE56DA9763517FA3F65CEBED43C7B61282772DBC9C49E09D937D24CFD29FF7B285F7B478AE4E219BBBD89A54C8B127CB0C65803144CC1CA6B662A4CE499EBE66D933CEAE58EE244CBDCAAE3C1F45A0D6947802B76\"\n        },\n        {\n          \"tcId\": 75,\n          \"deferred\": false,\n          \"z\": \"D16CC70224474A4D71E1F950C2D5CA72D8F08AF80E0C7F6E292C265A50CC30E8\",\n          \"d\": \"A72608DF0F025B4FEE7D94BAE77BE94CB974F20DD55006A70FD39F3397A8EF90\",\n          \"ek\": \"44AC5FB94668AD165BC9B4B09C50148831B0BFC0563D97B66913AB32846C9206BD2BEA024FB44B6DF239A8A82AD42C5BB69A77A3195AB8A7BD5C3131C9079D05E248D84167B6F569C812CDB1212F0584AEF9C0251FFA0E23A407BD592C7829C5D2A673DF01696AF02DD2A07A9342157EE608A2A91241DBB3F8725CD5D8BDA020C9107747F82A0FC7A97AC30272B694257B135095B27481CC769BB0834586CF70CAA72C2696B105A4B6440FB4D045B61A3D0BDA48490B22CE215C985B313F7C74155015A7D079DE9A2EF4DAB3ADD1A1CB684CA377B428B90D2BBB0980249586056434E2088EE250D065951EF228362B1D8028BC793CC8DCB8805D86C5DA71B4C1396158C79711DCC2F56359D197ABDB890387FA795ED9288FD08851CC620D0357A3450B4C50CF61EABA54A44FD61203F557946027B36224148EB4AEEE709401296BE0B617604B6A48244938279D66CAC75F03809ED49421F518A3270F42273E37793D6162B626771D69C8A27AF27AD0A3B89B6C756068565FB65D245A88D180741A1AA507E116DC504C11358C7EB444180792D210438F96CCB3DC17D8E8B8E79918A106787509B8B570B093C325B8CB81A12A686CACADA32389B8207108E7AEF5447217843932559F1893B250DB8060D068D6D2B06999C36F412D47519C795A7284E3CDD42CA6304C348AAA96E72B8C64C37C92C34F78C56A58B32630812A95F53FFE4B3987B5146E0321AC254573933D03E7356628727F448CF3D9A328C1CA72B59B73AC3F0A3671FF58A604AA5FA7F63ED7C72965A833B5916266796757186784882CE8670CE4F717F057A1C08563B9832B89A759F2380C74821379B343E7E28BEAE6334CDB896587C2585508A78947A54B08055C3C6CAC9357102CDAE58748A337D511BD0BE93C88964720C3AB5367478A4BB82FC148539C1D54F54CD7B1605C113D4E526BEDC4CD8526892198336F344B1A59A83F3ABC890C9AC386954A6B521FC76FFC03B614C32E7F68868400142BD041347614A80A7A59F2511A28B1E6F74053842C8F0A49FA17C378FC0992E14282E394034081AA4164C2E9934E5C8246E334AC606B9BA723237144AB2AAA8E86B2ED33B6750CA239F82C9105CEC46888BD59B2AE6331F47501C01805BF867A63F1B211306009C7B60FE4AB7EC5982B1955059A5D69BB728FAB31681CB0418C033B676158501FD3011A3B6B44503378C2FA1C6EC254861532DCC16F78D743DD065C8F8781F798633B0CAE5172173AE78A02DA472C0BB4DDCB2EC88B70E69C24FF277837646802774C98D87ED20382E4A82C0EB905CCAAA977295AA085A26717578E04B27139B98D260155C0B6189A3A6AA77D0CA76B6409032D3868967C1A5B5AB747B2B1660715F15161E00C383B957062509C495C5B25C26B2EA26B3A74597B913D295728B33A9EC914A2E646ADF0968E07592E48BA1BF0817995B321F05A7DD8FA892A385ED4C052B0CAB7C84945FABA63F2099BBD960843F4ADB512A48991A38439B71D291512333483E8A511FB925A7A770E033EFE6A933F105DF2B5555BE5179F3B8DD8F216B0EC87AD297B5AE941BBD80BFB0A417824A420489CC9281DD1D545BF15C8459627C4E8684D018473EACB1C49C27564614417A9EF6984D708B1FDC03F08453A769C470156884DCC30C18A1970022B01CA2DD0928F50404A9B19CA41FC8C325323EAE97C484015289A5383B5A46DA19641A99DB8763B8E716DF656204616B07BA5C06EF11ABD428D9EC989D848A8CAF65B4CAA41A4F741A497A08EBC2FF8525149FB029FE867939255AB836DB23C87BE120559A99B64F4BE533C6F914149C45A221BE8A3F9C724FC9BB5782C9489346ACCF5CC69A94734C2AB332A272BF135E507163BB3019D9544151A56C1892706F86D40439BF311AC4A477BE17287F829015CCA7A2F416AA178493E9CC86E607C2266924256377E3171B1610F6CBC4654DB870E2A1A2F11774F6147810A44523036EEC6211410091AF54EC71ACC0212BB96CB3CADE37C55A67A3DDBB12FCB6E6DC758EB8B807F75CFB4E4AED460A67C9546C9D668E8F1A560A80D20707AA6A0CE9433BD8D283BCC743FCC9C9D23719C1A05A874339C80F079A5364842AC0F21F804B9BB5D6FF4387D19442C4BAD15A31FB9F5C9A5A0CF4C59A16FA309A69AAAC01C9F1F8817AC8B7DC0153478452C0A379D65A78BB3307E3CA4\",\n          \"dk\": \"275C73EE58A9FFD22DD3EB2B1590CD79407D35E950370892C6B2AAB97392D8200DA10AB7188B9909B191D32073926860F5F5A4D2C21C52F4AF2EF5066BF182A7156D0400319CB462CEA5A2C65126943158823B085B6118449401D5C17A67D9A0CE2C5FEEF515787A423C7446EE795C14B61D33AC8BD06A3D8CA5A78AE5A4BCAB1E5BAA336A4B8DF423A9072A511A98993DF205971C70D84CADF5ABAF5917741B34AE09D280375819059618D748AC48CB425EC54F7A7435C40442FD594EB1C081AE4A80F9D023FDC968968A482BE95A9CD03041CB8B8F3918A5B7957E5CA6C95053A55CB7DA014B15425C6637000C131A664318834C09DE92BC04172209D945C97AAB3240A93559569D03793560AD9C552FBC467516556E4A07A1DCF2A2D2C5332E5074128207E4E4491A8866D760791A18CBC7939835F02DDEC71964428D80AC730FCBB0F0C206434746F75C2A4D95CFD4C398507729845C257D17549AA2983290558453445C3CA35B6BB139B007B003A890996ABF463D5E12CF3DF0728B8259A7854F6A06999C92868AC16C9BB78F571972AA122975C25A68C31B73710DD2EC07C2103BDBFC1A67E7370444CD2394913218816AEA4E1043BF53B83377D4775BAC602871C79A28AD930316FA091E5678582A552B6A5CBEBE25CB6F9C9002469E29F1833FB049D2CA7040499170F4B6BE2915E7C10081DB012DC8B9A8AA2F1FA6A6D3FABFD49B7873C234A21B0C9B572401F80908D7370C4A5BB1D04CF9218CCC5C8BEE0660CB2ABC39501E3F2AA441168B6BC9435A9599EC571D39A35B49FBA16DDC4A3C88828D4B89390B77006026E85A46335354B2B422DE7AAEF60087E2F975A7453FDD6C56B75181F317969F95B7A024A1AE12B66FDBBF27186BB0C0896DE69C5DA5AE4CB638E43A8FA8C283B92687DA372AEF4569826803613824A5DC288E94345209A4D2B42DCF2A251917735636669C3AC06E72A835042504CB6A83026CA4B1A10DB29F9C229575599295881A7358C5949469ACC8B43AC80B4D0CAAB50846CC999B42E0299159049DD370A9B52878AC4B64547E3EC070B4D370B19924D90C6EF153232CF12E64D0BD04E124C55CB2B8D1376A46CE2B91A225985F7BD72D3B316767B3BD3206C5C8B311122300225A02F0E53664CB02B1350859D579A09710EAB927C5207256AC1128C6B0E09C7BBA408A2DC34C1160A8B13766BAC2A747A6BF20B1A6A22992451BA49CB5B97F7597F98B95E44B9B598AA993073ADBD6A9B5C81E10E21AAEA573A95A6716A3298412BE1D661746BB81C2F17744D587E3522143091FA215AC2D9B7C3C00397A1C3807F24AB4E968B0EA1A0E5A6601B7274469CD1A52AE5FAB7BAAB3C4A6D1A5D27A873E21A14DF52AE2C344BFE21B0F8A71A0511828E86F1FB08141C30C842538C61C7ADEB9251E33C73CEA9E7C02CF46E3C3E548355643000D3B715F339DE10745141B8D2A201A4D8C514185A6DC06375E35750E246D4CF1C8937955631CC7350CBC4C34C22B35C4336B95A12478419B8309E0137317B6CCDBAF70630BFD711783087967A93E1F95321EC319CA31004CF677D44359465564A889193FE04A7F8944EF30B5C0B43903279F07E2C04D116BE8322E9FAA4FA2562025910A264A814EA488D72CC03EB5853B40A406E1A7631933EC725D4E71516C751C3C0A3560BC0AD97078ACE63E9FC41A0F2B92B6E46A0836A39AE9B67C6B6F23A2A098987F3B6500ED179C6F14A0E0564166C5CF5C322A1E5654F8E083CD5B13ECD761B727020CDB5EAC5B9DF509AC0D9BCFD2A2976E586F90FC3949AAC845440FB929B9D5743723926FB2E6290EF3ABE663565AF1CC74587CDCB3034F7692D9488142231BE8651C5280A52EF048FD4C1C776210A39463CA0127A2F9065D6AB320E253A1A0C596E8797887844D8471A32795E6DA8D5774648AF25194C3AC8447C534B61C77F364D3136FB358861B05465AB09077912F536500F35002DF56015441A341E30D6458289F47B578B1BFE4C574228B0D16890108BA7D4DBC7EA8C9A5118AA6F6E48ED8E19B19F6629114551442CAB1E88541F09E85B79E123C912C967CBD4C273C47485EF552ACF2744AF83D72007137342D33A00F41EC9E0FE8AF0B641AC0F3BE42984D4D9C2E4D9393AB9992E78913A7DC3444AC5FB94668AD165BC9B4B09C50148831B0BFC0563D97B66913AB32846C9206BD2BEA024FB44B6DF239A8A82AD42C5BB69A77A3195AB8A7BD5C3131C9079D05E248D84167B6F569C812CDB1212F0584AEF9C0251FFA0E23A407BD592C7829C5D2A673DF01696AF02DD2A07A9342157EE608A2A91241DBB3F8725CD5D8BDA020C9107747F82A0FC7A97AC30272B694257B135095B27481CC769BB0834586CF70CAA72C2696B105A4B6440FB4D045B61A3D0BDA48490B22CE215C985B313F7C74155015A7D079DE9A2EF4DAB3ADD1A1CB684CA377B428B90D2BBB0980249586056434E2088EE250D065951EF228362B1D8028BC793CC8DCB8805D86C5DA71B4C1396158C79711DCC2F56359D197ABDB890387FA795ED9288FD08851CC620D0357A3450B4C50CF61EABA54A44FD61203F557946027B36224148EB4AEEE709401296BE0B617604B6A48244938279D66CAC75F03809ED49421F518A3270F42273E37793D6162B626771D69C8A27AF27AD0A3B89B6C756068565FB65D245A88D180741A1AA507E116DC504C11358C7EB444180792D210438F96CCB3DC17D8E8B8E79918A106787509B8B570B093C325B8CB81A12A686CACADA32389B8207108E7AEF5447217843932559F1893B250DB8060D068D6D2B06999C36F412D47519C795A7284E3CDD42CA6304C348AAA96E72B8C64C37C92C34F78C56A58B32630812A95F53FFE4B3987B5146E0321AC254573933D03E7356628727F448CF3D9A328C1CA72B59B73AC3F0A3671FF58A604AA5FA7F63ED7C72965A833B5916266796757186784882CE8670CE4F717F057A1C08563B9832B89A759F2380C74821379B343E7E28BEAE6334CDB896587C2585508A78947A54B08055C3C6CAC9357102CDAE58748A337D511BD0BE93C88964720C3AB5367478A4BB82FC148539C1D54F54CD7B1605C113D4E526BEDC4CD8526892198336F344B1A59A83F3ABC890C9AC386954A6B521FC76FFC03B614C32E7F68868400142BD041347614A80A7A59F2511A28B1E6F74053842C8F0A49FA17C378FC0992E14282E394034081AA4164C2E9934E5C8246E334AC606B9BA723237144AB2AAA8E86B2ED33B6750CA239F82C9105CEC46888BD59B2AE6331F47501C01805BF867A63F1B211306009C7B60FE4AB7EC5982B1955059A5D69BB728FAB31681CB0418C033B676158501FD3011A3B6B44503378C2FA1C6EC254861532DCC16F78D743DD065C8F8781F798633B0CAE5172173AE78A02DA472C0BB4DDCB2EC88B70E69C24FF277837646802774C98D87ED20382E4A82C0EB905CCAAA977295AA085A26717578E04B27139B98D260155C0B6189A3A6AA77D0CA76B6409032D3868967C1A5B5AB747B2B1660715F15161E00C383B957062509C495C5B25C26B2EA26B3A74597B913D295728B33A9EC914A2E646ADF0968E07592E48BA1BF0817995B321F05A7DD8FA892A385ED4C052B0CAB7C84945FABA63F2099BBD960843F4ADB512A48991A38439B71D291512333483E8A511FB925A7A770E033EFE6A933F105DF2B5555BE5179F3B8DD8F216B0EC87AD297B5AE941BBD80BFB0A417824A420489CC9281DD1D545BF15C8459627C4E8684D018473EACB1C49C27564614417A9EF6984D708B1FDC03F08453A769C470156884DCC30C18A1970022B01CA2DD0928F50404A9B19CA41FC8C325323EAE97C484015289A5383B5A46DA19641A99DB8763B8E716DF656204616B07BA5C06EF11ABD428D9EC989D848A8CAF65B4CAA41A4F741A497A08EBC2FF8525149FB029FE867939255AB836DB23C87BE120559A99B64F4BE533C6F914149C45A221BE8A3F9C724FC9BB5782C9489346ACCF5CC69A94734C2AB332A272BF135E507163BB3019D9544151A56C1892706F86D40439BF311AC4A477BE17287F829015CCA7A2F416AA178493E9CC86E607C2266924256377E3171B1610F6CBC4654DB870E2A1A2F11774F6147810A44523036EEC6211410091AF54EC71ACC0212BB96CB3CADE37C55A67A3DDBB12FCB6E6DC758EB8B807F75CFB4E4AED460A67C9546C9D668E8F1A560A80D20707AA6A0CE9433BD8D283BCC743FCC9C9D23719C1A05A874339C80F079A5364842AC0F21F804B9BB5D6FF4387D19442C4BAD15A31FB9F5C9A5A0CF4C59A16FA309A69AAAC01C9F1F8817AC8B7DC0153478452C0A379D65A78BB3307E3CA4D4F2CEEBE65173867CDDEC350D15A72CF1FEE868A9B819DD1DEB4E7478C00DECD16CC70224474A4D71E1F950C2D5CA72D8F08AF80E0C7F6E292C265A50CC30E8\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/peergos/server/tests/fips203/key/gen/mlkem/prompt.json",
    "content": "{\n  \"vsId\": 42,\n  \"algorithm\": \"ML-KEM\",\n  \"mode\": \"keyGen\",\n  \"revision\": \"FIPS203\",\n  \"isSample\": false,\n  \"testGroups\": [\n    {\n      \"tgId\": 1,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-512\",\n      \"tests\": [\n        {\n          \"tcId\": 1,\n          \"z\": \"84CC9121AE56FBF39E67ADBD83AD2D3E3BB80843645206BDD9F2F629E3CC49B7\",\n          \"d\": \"2CB843A02EF02EE109305F39119FABF49AB90A57FFECB3A0E75E179450F52761\"\n        },\n        {\n          \"tcId\": 2,\n          \"z\": \"5D473027666FECF7024ABAF175B9BC42E84768C00AE2C5CF27A668121B02CD3A\",\n          \"d\": \"9EFF3FF8252400827F3B4389E4EC07E67948257C744278048C889D0789C5BFFA\"\n        },\n        {\n          \"tcId\": 3,\n          \"z\": \"7A7FC526215D5AE3262985D17B00726462D1479CB038DE8C8A8FEA896A037B2C\",\n          \"d\": \"C6636E8C2F87DD52A7F165A2A3BAD562ADB28CF738AA56B996B6062E95F66148\"\n        },\n        {\n          \"tcId\": 4,\n          \"z\": \"6E584B168BB5399D52B458A8BD122DE14EEF214515B70F38F972F41783005755\",\n          \"d\": \"EDE2E63FDEE6ADA2FC6EA906AA8D92DE87FA6199AC15446B0B6F075BF9F76148\"\n        },\n        {\n          \"tcId\": 5,\n          \"z\": \"37B87F960BF862D8B81AB5F56E9E24ED8EB011A05867A04DEC9BAA519AF45E22\",\n          \"d\": \"CD568FB1EEC23C436C011A55BE2FD4362EF000C890BDE7611EB5C4618AB74F8B\"\n        },\n        {\n          \"tcId\": 6,\n          \"z\": \"4B0A877F51434F70E2D8DB0A51BEB0A7572EF0DB7AC26ABC5D333C503B68BD5E\",\n          \"d\": \"35DEE1F800CA85E482BB12AFDB882FAE62CC77A338E65CA2265D77243ADAE3F3\"\n        },\n        {\n          \"tcId\": 7,\n          \"z\": \"B1EF909D94C56C134107B913B0ED29BC0851CCE424D0FB69EDC04C685A540871\",\n          \"d\": \"D9502C86FB461300B8D142A906B766B0B42481EA9C83AAE2BB74390F882B0509\"\n        },\n        {\n          \"tcId\": 8,\n          \"z\": \"671C8C054A52A67BEF8015DFDB5711C9197E84A5A553E794AE0811C8432FEF6A\",\n          \"d\": \"07A9BEBF21C83F6E5417A73D8CF5B527568C903B5883CEC8347B4ADE73AD92D6\"\n        },\n        {\n          \"tcId\": 9,\n          \"z\": \"C02D5CAD9E565727E19B2EFE4FA2E083F93EA0F5ADAF97522F33F416F786765F\",\n          \"d\": \"F682949EBFCFA5DA31368E3F177DD146448D0E62178959FCBA4CD4F02CD8B17E\"\n        },\n        {\n          \"tcId\": 10,\n          \"z\": \"70567D6DFD6622814417BBF673812F2D02E5BFA897D464957AA4219841A93C19\",\n          \"d\": \"170CA6BB76C065255DFDCA3EB93C772E57EBEF8C9A291C8F0BC4444BF008C868\"\n        },\n        {\n          \"tcId\": 11,\n          \"z\": \"71A6E59B13B36CAA406DBEC53F3FF2F0CC529098A4C8FBFD032C8BDB8B0E16FE\",\n          \"d\": \"176719D76EE1CEA83F7751BC4E3DDD00868B5C504C79AF8730B9F7595E7914A4\"\n        },\n        {\n          \"tcId\": 12,\n          \"z\": \"B63478F2FC887334C707E9D836E3104892566B3568CD32B583F8C9A0DE1A1F0C\",\n          \"d\": \"3C90FC402DA953172300194876B3B3BC958268747751346DE7134566CB8FAA5A\"\n        },\n        {\n          \"tcId\": 13,\n          \"z\": \"4EA6EC5384C51903758B807395181F6D6B4CCA3FA1CA24110B08A8AB1742C411\",\n          \"d\": \"24B783E39214CC39910799ADECE53B32408C19CD9ED10DEC039A9FA2CFC1CA30\"\n        },\n        {\n          \"tcId\": 14,\n          \"z\": \"9FA6AA53F505506BE269CE201A1A6EF95692DD1350A7188F468D34C5DAE5EAD7\",\n          \"d\": \"E4F2972F746E028108A5BB98EC97A307DC9363909DEAFC491F040B964675B9FC\"\n        },\n        {\n          \"tcId\": 15,\n          \"z\": \"A9EE7619E4F0250147ADC188649A45EB6D82DE5EACD5643CDC52E6DF8DF2F8EB\",\n          \"d\": \"C5C26DF5BA8BAB4A293292BD070986A8063F736469F6ABBAB684F7127575172B\"\n        },\n        {\n          \"tcId\": 16,\n          \"z\": \"80CE5D65D1795C90B637C10360B04A4C21A70851F0A59D4D753F54CC00103FF4\",\n          \"d\": \"EF0F6EDB707059073378E3419C8D9031D0732CFA931190EBD07FE291B1A3EBD3\"\n        },\n        {\n          \"tcId\": 17,\n          \"z\": \"B923CFEEC804B8C6A9E36B77B38A2886C45B1C731A33528ED2CB5A1F65E792F6\",\n          \"d\": \"BEE40356679E3EAE8B0C3FA07C1BFDC8835CEC26CA194D5EFC4301481C256C0E\"\n        },\n        {\n          \"tcId\": 18,\n          \"z\": \"1F4863F16E38DFD2C42A9322FA1ACB941DF3BDFA000A202AC621936FCC5FE33A\",\n          \"d\": \"C6D5B35B90FA9AB9A7B438B57942D653CAE67B314C7FD152013B4E90BEF8201B\"\n        },\n        {\n          \"tcId\": 19,\n          \"z\": \"53F5EE39A553E831BE32EB490A6E1DE62FD4FE486EF58A4B99F6347759BB8905\",\n          \"d\": \"5C6051E18E28FC5719E3172B967D25BB1649D87743440F7715E860AA212A256C\"\n        },\n        {\n          \"tcId\": 20,\n          \"z\": \"9C7C3E68F827936D8DC435942DC4925D180E6D5C911550089E1337D8BA77A06C\",\n          \"d\": \"CA351B0F454DE9DB364E1DAB8AEF6E49C2E69439941935B24C00BB9952E65BB3\"\n        },\n        {\n          \"tcId\": 21,\n          \"z\": \"97A4C9A65A82BAEC15FF165E10490976EBB19FAFBA8F9E8E0DFFBDB4D5E1ACE5\",\n          \"d\": \"C467A43BF9E9CCADCE4581B53F8CA0B605583775AFCD0EBBB587907B3A813D94\"\n        },\n        {\n          \"tcId\": 22,\n          \"z\": \"973DBB6EAF76AF0C96F0F24EF9AE65ACD854301B5F7A7892A17FBB8601DE78D3\",\n          \"d\": \"D732CF45D7F44788E17C3B6DA9987495AB1AEFA233F74EEF8D3BE5B6C0C04E00\"\n        },\n        {\n          \"tcId\": 23,\n          \"z\": \"D525CCE60C3E300ED36298A1C0D0165C147CB84197C4028257DAF39239E6EA5D\",\n          \"d\": \"B670CEB5612A1287C4653B158A3CC522AAA1AA45B34A4C770DCA1E5BF3988F3D\"\n        },\n        {\n          \"tcId\": 24,\n          \"z\": \"9F2FC49CD848BA72FC17854B18D88ED65B630BA94A1BC5F6D3A458E1087D3A13\",\n          \"d\": \"3236CB10279681238E5B0E2F5138A7F743443379F5F1A845F3D76B75D2C2A9DF\"\n        },\n        {\n          \"tcId\": 25,\n          \"z\": \"0FB831AFA34B124F7456D0D09E4ED8607DE407101E6E75F305F9D67EF7C2FAE7\",\n          \"d\": \"C155568B6BA74DA317388423F8FB28585977EB858EE306CAE4174120F02A8D72\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 2,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-768\",\n      \"tests\": [\n        {\n          \"tcId\": 26,\n          \"z\": \"A85768F3486BD32A01BF9A8F21EA938E648EAE4E5448C34C3EB88820B159EEDD\",\n          \"d\": \"E34A701C4C87582F42264EE422D3C684D97611F2523EFE0C998AF05056D693DC\"\n        },\n        {\n          \"tcId\": 27,\n          \"z\": \"DF0F282411F4A071489A8F618E2AE5AEF40131CAC5233D6D731522720C2FEB1C\",\n          \"d\": \"444F032DD19AE7518C4B35B0732A41DC567845ABA8BD7B04A9C413A0CF2DE0B5\"\n        },\n        {\n          \"tcId\": 28,\n          \"z\": \"5AA6DC620A6E9A60CF19A7B4F0FF805BDA8219522A548EE5857C3FF6060C7A2F\",\n          \"d\": \"092271D05CA63C60880AF404D60BC4BB9539E2EA12969581898D56E0AC9A5A68\"\n        },\n        {\n          \"tcId\": 29,\n          \"z\": \"7CF50F7237A97072F03F31CFD59FA8E863BCA3AF7375E0CA698FF665661C24CF\",\n          \"d\": \"BBF7574CF5F32BE49E1F39CE33870D9D6384056D60D223003B6B0C10D5C42180\"\n        },\n        {\n          \"tcId\": 30,\n          \"z\": \"C593627807074684B7D363441F80F6A3D185D67878702D33A4E0BDA2000F857D\",\n          \"d\": \"D12CD9B65B7C58B2195AE0BE0282527BAC06C2D25CB0472628D64715F7F6A378\"\n        },\n        {\n          \"tcId\": 31,\n          \"z\": \"E01702E1228F530AC96DB053A415BE97749A109A1FD4057BA128649B17EC07AD\",\n          \"d\": \"79C006D5470C229AFCE7588546E52204B09F5086974865B426AAAA198C6CBA7A\"\n        },\n        {\n          \"tcId\": 32,\n          \"z\": \"AE51639EF7F26FD2215AD11CBE1EDEB3B943D668EEEFEE13ED5B0DA3E0A5F3ED\",\n          \"d\": \"B04F631B330D83991B5C01E7F69452DFC394F9689632F8C7F60DBFAB92A9CEA5\"\n        },\n        {\n          \"tcId\": 33,\n          \"z\": \"6F9FF5654FDA78774498E2643E935D21412CEB49BC393532C80C47A982418F66\",\n          \"d\": \"3D63BD6C310AFCF684292E5F8E1B98CC75B5A27B21526268444144AB24AB2967\"\n        },\n        {\n          \"tcId\": 34,\n          \"z\": \"D083E6922EF0A818308FD7FE7CF5AD3A96942442BE327B0A307685C2D4315901\",\n          \"d\": \"249D48941ABC01C9290719FB34D91B05E774E70E6F0181E1783F2586E2499536\"\n        },\n        {\n          \"tcId\": 35,\n          \"z\": \"A20ABA8A8DDC212DE825BE0D3BE57701A6B5B3A46A300D9B5945F579A59AFABE\",\n          \"d\": \"E1CFB8195877B2D4FF3363BAC3B4E7BEBA6DC3CBB789B1B24215393F6C9BBFAE\"\n        },\n        {\n          \"tcId\": 36,\n          \"z\": \"7FB950A8F51DCEC4BC7A573EDDA56ECC049E5688476BD5FD6CD076A8F99A019A\",\n          \"d\": \"ADC4DA59D935DD87420ACEE52AEE19CB371FD0BB498D79BA680159EF7CE37C17\"\n        },\n        {\n          \"tcId\": 37,\n          \"z\": \"51D509CF26799741631099039F713B22551E2B0F0297BB809DF0CC8FC3E47EEE\",\n          \"d\": \"76CDCA53F781806D55CA8D3BAFB3F4D389D712F1221E85B5E29D6A46580F978C\"\n        },\n        {\n          \"tcId\": 38,\n          \"z\": \"9C330AB4257D7B87C4742C6E95B66BDF805C6A145BF444836092C6B1D2C5FFFF\",\n          \"d\": \"78AB6C49354A018BD38A39926F822A1AC4ACC4FF32DFD7C047CE0887A3AC182C\"\n        },\n        {\n          \"tcId\": 39,\n          \"z\": \"18EA1C7532F706B06870D0A1047AAE33D9E1FF9E9BCBBD302D8817EB7B022A77\",\n          \"d\": \"13B75620E4CB9AB9A6689F6E2BE44639BAE6C9CB7DD641AC1C9377242D99679A\"\n        },\n        {\n          \"tcId\": 40,\n          \"z\": \"C71F7E44295978FC63BF8F6A68F8609E98D155FD7A74E1FB7982733FBF8A6C25\",\n          \"d\": \"7C345819C7C327AD9571E5DF882449DB243870D686A9764D4129B21E17AC86A9\"\n        },\n        {\n          \"tcId\": 41,\n          \"z\": \"EF668FB41F49E82EE0FE00919CC06507548321593A7ECD1D2112342608D95FFF\",\n          \"d\": \"8D6DF2EB3DDAF961FE5EB556842B758BEBC7ECB312B6D4628B323F483B77D6F9\"\n        },\n        {\n          \"tcId\": 42,\n          \"z\": \"26345937ADC9104155275E7114E93D9F5847EEA73A9359358585B2D42301A294\",\n          \"d\": \"DB4ED8E9C3E1AC7A35EA4B67A4EFCFB46972A984D161F79F084125D6D4AEE7AF\"\n        },\n        {\n          \"tcId\": 43,\n          \"z\": \"63435E06C2AA3DFB3477120710D5E7FF0DC0DA68D4644A24F66A8012FB193697\",\n          \"d\": \"C6EFA7D5D500E5BF857D80EAE2A6EE6414159947FD4BE589350724FAE5E51805\"\n        },\n        {\n          \"tcId\": 44,\n          \"z\": \"8C2942B7207C2C59BD56FF9EE0B120B1DAD81B05602623623CBC7E0C20C9B709\",\n          \"d\": \"20859B01DFC60B6109E0234F3CAC7A247D8386099D83D2D447E9A21AF9DE48BD\"\n        },\n        {\n          \"tcId\": 45,\n          \"z\": \"EAE318341D06E0801C0CA4B873520C714740AD017FE5A158D3BD40960D907AB7\",\n          \"d\": \"409E9F3AB58D736E122EFCC4240BF8388FDFDA6759004D42457018014A335BE4\"\n        },\n        {\n          \"tcId\": 46,\n          \"z\": \"EF38264520685080F52975BC957C5FB609FB0E1BD06D26F572CC5425CAE7DE5C\",\n          \"d\": \"CE2CACEBD54AF1B4E71588DE9F22A6AF2C2E2AD7FD66B9FEC0DF19182E7F57EC\"\n        },\n        {\n          \"tcId\": 47,\n          \"z\": \"17E5AE70771674BE8903CC21B3A90248D993C261B6CEEF2C747873D113869B55\",\n          \"d\": \"7E03015C5D55FD9888E730C1E60F90C5F6C2E3B1E8C7C08D869F0C1D15B540ED\"\n        },\n        {\n          \"tcId\": 48,\n          \"z\": \"BF83E3048B021F22DB57076A885729F95119CE63FAF51A69954BCCC51E014686\",\n          \"d\": \"8590BFC9A6FC25EE7E6DAB4870DBF4B51A1F141B7C9E96230C0403E799BC68E0\"\n        },\n        {\n          \"tcId\": 49,\n          \"z\": \"F42861EFF7691614C3E8975AFB4E353F8C8C39E6F41BB637EC79BAA976D1ADC1\",\n          \"d\": \"D5FD815092620DC42A223909E387369A74AF7DCA285138CF217BC29F29C42C41\"\n        },\n        {\n          \"tcId\": 50,\n          \"z\": \"4DD0E86091649A0A08EA44DAB85DF56797F8BF46222C2DBA7DEC6374B9B2268E\",\n          \"d\": \"D21D5AFED9AFAA3B49FB45245B2BCA1505E4000CDC29094A3600F5CAA49A7B3A\"\n        }\n      ]\n    },\n    {\n      \"tgId\": 3,\n      \"testType\": \"AFT\",\n      \"parameterSet\": \"ML-KEM-1024\",\n      \"tests\": [\n        {\n          \"tcId\": 51,\n          \"z\": \"99E3246884181F8E1DD44E0C7629093330221FD67D9B7D6E1510B2DBAD8762F7\",\n          \"d\": \"49AC8B99BB1E6A8EA818261F8BE68BDEAA52897E7EC6C40B530BC760AB77DCE3\"\n        },\n        {\n          \"tcId\": 52,\n          \"z\": \"007BF379B97DA0947F2E9BFDE3359E282C9CF1D2E68A80209B533104E90F432D\",\n          \"d\": \"2D229AB46354901491476CCE8FA96E4A5FBA65AB2F538FEDAA528E35687A782B\"\n        },\n        {\n          \"tcId\": 53,\n          \"z\": \"E94F4E83E6CAABCA9E319D40F6CE0E3691B77C92D9E3766BE9B6F4B6DF2E640E\",\n          \"d\": \"1D65D0290B15903371D616D7AC3F2FADA8CB24E6C84D52C039A10BC1288C1110\"\n        },\n        {\n          \"tcId\": 54,\n          \"z\": \"EC54F6E1E7FB12B796D0E56BE6FE3BA6EDAAB49B08712318B27D229606D2AC70\",\n          \"d\": \"22D19527844F3CDB8A342620A96E902AC7C36E54677ADA6FE8DB08DF4EF3B36B\"\n        },\n        {\n          \"tcId\": 55,\n          \"z\": \"5B78F8D30AADB59FA617EF807D5C23113A9908342F08E898E02991CA1D7B934D\",\n          \"d\": \"A00D1EE4147DD57B5E76C58A928DED0B720FB2FB6353780B380B5FBC76712E5C\"\n        },\n        {\n          \"tcId\": 56,\n          \"z\": \"384509DB0E97D4689A3CED953CFBFFA9D3B3B87CCB0C6A360FC0DF3CBCA399F9\",\n          \"d\": \"2C34B1476753095D0C8A48A00136F358A98D1416E5069CBA4540C6E26FA3634D\"\n        },\n        {\n          \"tcId\": 57,\n          \"z\": \"63DAD9B127F98E72A3C65ACF4B172FDBD9B9C39F24F728D1F40EB02C9949419D\",\n          \"d\": \"F742E7B69E27A57A43E1034CEB5834CAD57C380ABE259F432F96FAAF27F981A9\"\n        },\n        {\n          \"tcId\": 58,\n          \"z\": \"0A755A829F05597B2F2A90974F22FB1AEAB42892101222967E3A0AD612CEEBCA\",\n          \"d\": \"3BFC9A057D979EC03A705A9CC406DD8A46C106941AF6777B1D7F79C1508D7B24\"\n        },\n        {\n          \"tcId\": 59,\n          \"z\": \"681F088AD6962FC397A1B9071852848CE9A7EDAE65A81485CEC87D0974707B7E\",\n          \"d\": \"7C43F2E7D9B1D8D9C41D9F315E052A254CE3A1F098671773B53717A95220AD55\"\n        },\n        {\n          \"tcId\": 60,\n          \"z\": \"40BBB2C581B2D694E369C0DA567371E8E53C328A59BCE775A625C9F5CC185E0F\",\n          \"d\": \"C2E1A3161F3734F44F3C2F1736E149803F71321122242A1E95E55E5652A91F55\"\n        },\n        {\n          \"tcId\": 61,\n          \"z\": \"E15F322315265F9B847960B7185D962761ED79C62286A0DFDB13DBF550CE0107\",\n          \"d\": \"ACB7FDB596B44A88A60ED74A3FAD9EF745BF5BFA4902CADB891EC5CA45F685F5\"\n        },\n        {\n          \"tcId\": 62,\n          \"z\": \"ABD71039AE2E2700391011D9CC8265C2D5C9779002D54E1BDD9607402054CA95\",\n          \"d\": \"0AA4E8D918201BB98464963B076E35337FF3265810723E01C435954DB18B14FF\"\n        },\n        {\n          \"tcId\": 63,\n          \"z\": \"177A8DA7AF8DB3F712E1653D05A47D61B59F4F4950549382E56F761D7126F8F9\",\n          \"d\": \"F43EC0E96A791317938761FFBE97332D5D85F52D22BDA6303FE7E7107DB608A6\"\n        },\n        {\n          \"tcId\": 64,\n          \"z\": \"79E3B0D4F4AF344ED06FDE8BF4E104753E832294A3D2E4B66BE59149006A7B95\",\n          \"d\": \"0596F1E214B29A0CB7A641EA0BB157FE01FAB73B4A9BCDC165FA44C8FD5FBF71\"\n        },\n        {\n          \"tcId\": 65,\n          \"z\": \"EF0F95F630F41B3AF911A30E543822DFA6B7684FEE36956D2BCF8FF080C9FA26\",\n          \"d\": \"D7349F9AD546CFE9830E1197072B6ED9CA21E8E0423F145F1DB84A5AEBA230EC\"\n        },\n        {\n          \"tcId\": 66,\n          \"z\": \"DDD4871080BD4F761D972085851DE0A0408A2F5EEC3CD3786297A782402CA440\",\n          \"d\": \"F05117E932CA0E0C202732DFD4F674BF5848219A76C64A0650C27E2E55095513\"\n        },\n        {\n          \"tcId\": 67,\n          \"z\": \"FA29BDC28D989B8C4BE84706A3CF21B36A1C6E355C88A361C7664818E4BC8E03\",\n          \"d\": \"A405D9B07C5771A5BBDA2BE9F8A40D9566CAD7DA1761ED8076A289063DB4A8E2\"\n        },\n        {\n          \"tcId\": 68,\n          \"z\": \"08FED872D91297D8059743D3E7B6EE47548357E7F882B5BFE2F04314187ED424\",\n          \"d\": \"E66F17317C40783CE0594CFB5920FF86062591C5EA4254021495749642C0D968\"\n        },\n        {\n          \"tcId\": 69,\n          \"z\": \"EB8EA5E8C5EABACCFF162556DA53F0C02F72EE7A7DEA8E9EB70FC51C777645E6\",\n          \"d\": \"F8CF49DA62AA762EC020F3766237520E7FDA4CA3AC11FBE50E6C5F9CAB3CA7B8\"\n        },\n        {\n          \"tcId\": 70,\n          \"z\": \"DAC056B9A373687E44CCAB8751BD334F4942696B9076155F9D0E5BC0E89D85CF\",\n          \"d\": \"08E36AE8586A59B8249A80D7F43506F9711FA4B00A49D182CE06DAD0CF985809\"\n        },\n        {\n          \"tcId\": 71,\n          \"z\": \"4D727ACABD44DC48980691E0268B5B3FC1E476B3FDF9571F5CBC8DDFD400AB99\",\n          \"d\": \"A491FF48028B67A407F1054D5B1CBA733B665DE667E22596EDCC31C227C2DE1B\"\n        },\n        {\n          \"tcId\": 72,\n          \"z\": \"4E638D8AC3662450E09D8500DED751060B7990D54F137508B9897277F65EA952\",\n          \"d\": \"7B2EC50C53A67E0BCCBA98C2E319F5AB46B6E593D2465F14B23FFA03D0E5BE0D\"\n        },\n        {\n          \"tcId\": 73,\n          \"z\": \"7459AB99D24C1254EEECC035874BF19A64EFC8EDC9D369C11F5DF4DC83AB5FBC\",\n          \"d\": \"16858AA7C92EBD72FB8CCD0A99D0435EDB2A6EB1B936DBCB637CF43F25D221B1\"\n        },\n        {\n          \"tcId\": 74,\n          \"z\": \"4CC1CA6B662A4CE499EBE66D933CEAE58EE244CBDCAAE3C1F45A0D6947802B76\",\n          \"d\": \"F788F3E21D62E74090582F310BD4FDC8065E56E8D946142B9B9CF8F338F330E8\"\n        },\n        {\n          \"tcId\": 75,\n          \"z\": \"D16CC70224474A4D71E1F950C2D5CA72D8F08AF80E0C7F6E292C265A50CC30E8\",\n          \"d\": \"A72608DF0F025B4FEE7D94BAE77BE94CB974F20DD55006A70FD39F3397A8EF90\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/peergos/server/tests/fips203/provider/MimicloneSecurityProviderTests.java",
    "content": "package peergos.server.tests.fips203.provider;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.provider.MimicloneSecurityProvider;\n\nimport java.security.Provider;\nimport java.security.Security;\n\nimport static org.junit.Assert.assertNotNull;\n\npublic class MimicloneSecurityProviderTests {\n\n    @Before\n    public void setUp() {\n        new MimicloneSecurityProvider().install();\n    }\n\n    @Test\n    public void testSecurityProviders() {\n\n        Provider mimicloneProvider = Security.getProvider(MimicloneSecurityProvider.PROVIDER_NAME);\n        assertNotNull(mimicloneProvider);\n\n        for (Provider provider : Security.getProviders()) {\n            System.out.printf(\"Security Provider: [%s]:%n\", provider.getName());\n            for (Provider.Service service : provider.getServices()) {\n               System.out.printf(\" --> [%s] Service [%s]%n\", service.getType(), service.getAlgorithm());\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/tests/fips203/reduce/barrett/BarrettReducerTests.java",
    "content": "package peergos.server.tests.fips203.reduce.barrett;\n\nimport org.junit.Test;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.ParameterSet;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.reduce.Reducer;\nimport peergos.server.crypto.asymmetric.mlkem.fips203.reduce.barrett.BarrettReducer;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\n\npublic class BarrettReducerTests {\n    \n    @Test\n    public void testModulusMultiplication() {\n\n        Reducer barrettReducer = BarrettReducer.create(ParameterSet.ML_KEM_1024);\n\n        long min = -1L;\n        long max = -1L;\n\n        for (int a = 0; a < 3329; a++) {\n            for (int b = 0; b < 3329; b++) {\n\n                int mul = a * b;\n                int knownValue = mul % 3329;\n\n                long start = System.nanoTime();\n                int barretValue = barrettReducer.reduce(mul);\n                long stop = System.nanoTime();\n                long time = stop - start;\n\n                if (min == -1L || time < min) { min = time; }\n                if (max == -1L || time > max) { max = time; }\n\n                assertEquals(knownValue, barretValue);\n\n            }\n        }\n\n        // Ensure we captured min and max values\n        assertTrue(min > -1);\n        assertTrue(max > min);\n\n        // Calculate the spread between min and max\n        long spread = max - min;\n        System.out.printf(\"Metrics: min=%d, max=%d, spread=%d\\n\", min, max, spread);\n\n        // Ensure the variance in execution times is less than 1 millisecond\n        assertTrue(spread < 1_000_000);\n        \n    }\n\n    @Test\n    public void testModulusAddition() {\n\n        Reducer barrettReducer = BarrettReducer.create(ParameterSet.ML_KEM_1024);\n\n        for (int a = 0; a < 3329; a++) {\n            for (int b = 0; b < 3329; b++) {\n\n                int mul = a + b;\n                int knownValue = mul % 3329;\n                int barretValue = barrettReducer.reduce(mul);\n\n                assertEquals(knownValue, barretValue);\n\n            }\n        }\n\n    }\n\n    @Test\n    public void testModulusSubtraction() {\n\n        Reducer barrettReducer = BarrettReducer.create(ParameterSet.ML_KEM_1024);\n\n        for (int a = 0; a < 3329; a++) {\n            for (int b = 0; b < 3329; b++) {\n\n                int mul = a - b;\n                int knownValue = (mul + 3329) % 3329;\n                int barretValue = barrettReducer.reduce(mul);\n\n                assertEquals(knownValue, barretValue);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/linux/LocalS3ServerTest.java",
    "content": "package peergos.server.tests.linux;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.util.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\n\npublic class LocalS3ServerTest {\n    private static final Hasher hasher = JavaCrypto.init().hasher;\n    private static final String BUCKET = \"testbucket\";\n    private static final String ACCESS_KEY = \"testaccesskey\";\n    private static final String SECRET_KEY = \"testsecretkey\";\n    private static final int PORT = 19878;\n\n    private LocalS3Server server;\n    private S3Config config;\n    private String host;\n\n    @Before\n    public void start() throws Exception {\n        Path dir = Files.createTempDirectory(\"local-s3-test\");\n        server = new LocalS3Server(dir, BUCKET, ACCESS_KEY, SECRET_KEY, PORT);\n        server.start();\n        config = LocalS3Server.getConfig(BUCKET, ACCESS_KEY, SECRET_KEY, PORT);\n        host = config.getHost(); // \"testbucket.localhost:PORT\"\n    }\n\n    @After\n    public void stop() {\n        server.stop();\n    }\n\n    @Test\n    public void putAndGet() throws Exception {\n        String key = BUCKET + \"/blocks/hello\";\n        byte[] data = \"hello world\".getBytes();\n        String sha = ArrayOps.bytesToHex(Hash.sha256(data));\n\n        PresignedUrl put = S3Request.preSignPut(key, data.length, sha, Optional.empty(), false,\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host,\n                new HashMap<>(), config.region, config.accessKey, config.secretKey, false, hasher).join();\n        HttpUtil.putWithVersion(put, data);\n\n        PresignedUrl get = S3Request.preSignGet(key, Optional.of(600), Optional.empty(),\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, config.region,\n                Optional.empty(), config.accessKey, config.secretKey, false, hasher).join();\n        byte[] result = HttpUtil.get(get);\n        Assert.assertArrayEquals(data, result);\n    }\n\n    @Test\n    public void head() throws Exception {\n        String key = BUCKET + \"/blocks/headtest\";\n        byte[] data = \"headdata\".getBytes();\n        String sha = ArrayOps.bytesToHex(Hash.sha256(data));\n\n        PresignedUrl put = S3Request.preSignPut(key, data.length, sha, Optional.empty(), false,\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host,\n                new HashMap<>(), config.region, config.accessKey, config.secretKey, false, hasher).join();\n        HttpUtil.putWithVersion(put, data);\n\n        PresignedUrl head = S3Request.preSignHead(key, Optional.of(600),\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, config.region,\n                Optional.empty(), config.accessKey, config.secretKey, false, hasher).join();\n        Map<String, List<String>> headers = HttpUtil.head(head);\n        String contentLength = headers.entrySet().stream()\n                .filter(e -> \"content-length\".equalsIgnoreCase(e.getKey()))\n                .map(e -> e.getValue().get(0))\n                .findFirst().orElse(null);\n        Assert.assertEquals(String.valueOf(data.length), contentLength);\n    }\n\n    @Test\n    public void listVersions() throws Exception {\n        String prefix = BUCKET + \"/blocks/alice/\";\n        String key1 = prefix + \"AAA\";\n        String key2 = prefix + \"BBB\";\n        byte[] data = \"block\".getBytes();\n        String sha = ArrayOps.bytesToHex(Hash.sha256(data));\n\n        for (String key : List.of(key1, key2)) {\n            PresignedUrl put = S3Request.preSignPut(key, data.length, sha, Optional.empty(), false,\n                    S3AdminRequests.asAwsDate(ZonedDateTime.now()), host,\n                    new HashMap<>(), config.region, config.accessKey, config.secretKey, false, hasher).join();\n            HttpUtil.putWithVersion(put, data);\n        }\n\n        S3AdminRequests.ListObjectVersionsReply reply = S3AdminRequests.listObjectVersions(\n                prefix, 1000, Optional.empty(), Optional.empty(),\n                ZonedDateTime.now(), host, config.region, Optional.empty(),\n                config.accessKey, config.secretKey,\n                url -> { try { return HttpUtil.get(url); } catch (java.io.IOException e) { throw new RuntimeException(e); } },\n                S3AdminRequests.builder::get, false, hasher);\n\n        Assert.assertEquals(2, reply.versions.size());\n        Assert.assertTrue(reply.versions.stream().anyMatch(v -> v.key.equals(key1)));\n        Assert.assertTrue(reply.versions.stream().anyMatch(v -> v.key.equals(key2)));\n    }\n\n    @Test\n    public void delete() throws Exception {\n        String key = BUCKET + \"/blocks/todelete\";\n        byte[] data = \"bye\".getBytes();\n        String sha = ArrayOps.bytesToHex(Hash.sha256(data));\n\n        PresignedUrl put = S3Request.preSignPut(key, data.length, sha, Optional.empty(), false,\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host,\n                new HashMap<>(), config.region, config.accessKey, config.secretKey, false, hasher).join();\n        HttpUtil.putWithVersion(put, data);\n\n        PresignedUrl del = S3AdminRequests.preSignDelete(key, Optional.empty(),\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, config.region,\n                Optional.empty(), config.accessKey, config.secretKey, false, hasher).join();\n        HttpUtil.delete(del);\n\n        PresignedUrl get = S3Request.preSignGet(key, Optional.of(600), Optional.empty(),\n                S3AdminRequests.asAwsDate(ZonedDateTime.now()), host, config.region,\n                Optional.empty(), config.accessKey, config.secretKey, false, hasher).join();\n        try {\n            HttpUtil.get(get);\n            Assert.fail(\"Expected 404\");\n        } catch (java.io.IOException e) {\n            // expected\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/linux/S3UserTests.java",
    "content": "package peergos.server.tests.linux;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.tests.*;\nimport peergos.server.tests.util.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\n\npublic class S3UserTests extends UserTests {\n\n    private static Random random = new Random(1);\n\n    private static final String S3_BUCKET = \"local-s3\";\n    private static final String S3_ACCESS_KEY = \"test\";\n    private static final String S3_SECRET_KEY = \"testdslocal\";\n    private static LocalS3Server localS3;\n    private static int s3Port;\n\n    private static Args pkiArgs = buildArgs()\n            .with(\"useIPFS\", \"true\")\n            .with(\"async-bootstrap\", \"true\")\n            .removeArg(IpfsWrapper.IPFS_BOOTSTRAP_NODES); // no bootstrapping\n\n    private static Args withS3(Args in) {\n        S3Config cfg = LocalS3Server.getConfig(S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, s3Port);\n        return in.with(\"s3.bucket\", cfg.bucket)\n                .with(\"s3.region\", cfg.region)\n                .with(\"s3.region.endpoint\", cfg.regionEndpoint)\n                .with(\"direct-s3-writes\", \"true\")\n                .with(\"authed-s3-reads\", \"true\")\n                .with(\"s3.accessKey\", cfg.accessKey)\n                .with(\"s3.secretKey\", cfg.secretKey)\n                .with(\"allow-external-login\", \"true\");\n    }\n\n    private static final List<Args> argsToCleanUp = new ArrayList<>();\n    private static List<ServerProcesses> services = new ArrayList<>();\n\n    public S3UserTests() {\n        super(getNetwork(), services.get(1).localApi);\n    }\n\n    private static NetworkAccess getNetwork() {\n        try {\n            return Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + argsToCleanUp.get(argsToCleanUp.size() - 1).getInt(\"port\")), false, Optional.empty(), Optional.empty()).join();\n        } catch (MalformedURLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @BeforeClass\n    public static void init() throws Exception {\n        // start local S3 server\n        s3Port = TestPorts.getPort();\n        Path s3Dir = Files.createTempDirectory(\"peergos-s3-test\");\n        localS3 = new LocalS3Server(s3Dir, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, s3Port);\n        localS3.start();\n\n        // start pki node\n        ServerProcesses pki = Main.PKI_INIT.main(pkiArgs);\n        PublicKeyHash peergosId = pki.localApi.coreNode.getPublicKeyHash(\"peergos\").join().get();\n        pkiArgs = pkiArgs.setArg(\"peergos.identity.hash\", peergosId.toString());\n        NetworkAccess toPki = buildApi(pkiArgs);\n        Cid pkiNodeId = toPki.dhtClient.id().get();\n        int bootstrapSwarmPort = pkiArgs.getInt(\"ipfs-swarm-port\");\n        services.add(pki);\n        UserContext peergos = UserContext.signIn(\"peergos\", \"testpassword\", m -> Futures.errored(new IllegalStateException(\"No MFA\")),\n                toPki, crypto).join();\n        BatWithId mirrorBat = peergos.getMirrorBat().join().get();\n\n        // start ipfs S3 node\n        int ipfsApiPort = TestPorts.getPort();\n        int ipfsGatewayPort = TestPorts.getPort();\n        int ipfsSwarmPort = TestPorts.getPort();\n        int proxyTargetPort = TestPorts.getPort();\n        int allowPort = TestPorts.getPort();\n        Args ipfsArgs = withS3(buildArgs())\n                .with(\"useIPFS\", \"true\")\n                .with(\"async-bootstrap\", \"true\")\n                .with(\"ipfs-api-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsApiPort)\n                .with(\"ipfs-gateway-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsGatewayPort)\n                .with(\"allow-target\", \"/ip4/127.0.0.1/tcp/\" + allowPort)\n                .with(\"ipfs-swarm-port\", \"\" + ipfsSwarmPort)\n                .with(IpfsWrapper.IPFS_BOOTSTRAP_NODES, \"\" + Main.getLocalBootstrapAddress(bootstrapSwarmPort, pkiNodeId))\n                .with(\"proxy-target\", Main.getLocalMultiAddress(proxyTargetPort).toString())\n                .with(\"ipfs-api-address\", Main.getLocalMultiAddress(ipfsApiPort).toString());\n        IpfsWrapper ipfs = Main.IPFS.main(ipfsArgs);\n        argsToCleanUp.add(ipfsArgs);\n\n        // start direct S3 node\n        int peergosPort = TestPorts.getPort();\n        Cid ourId = new ContentAddressedStorage.HTTP(new JavaPoster(new URL(\"http://localhost:\" + ipfsApiPort), false), false, crypto.hasher).id().get();\n        Args peergosArgs = ipfsArgs\n                .with(\"port\", \"\" + peergosPort)\n                .with(\"useIPFS\", \"false\")\n                .with(\"enable-gc\", \"false\")\n                .with(\"mirror.username\", \"peergos\")\n                .with(\"mirror.bat\", mirrorBat.encode())\n                .with(\"ipfs-api-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsApiPort)\n                .with(\"ipfs-gateway-address\", \"/ip4/127.0.0.1/tcp/\" + ipfsGatewayPort)\n                .with(\"allow-target\", \"/ip4/127.0.0.1/tcp/\" + allowPort)\n                .with(\"proxy-target\", Main.getLocalMultiAddress(proxyTargetPort).toString())\n                .with(\"ipfs.id\", ourId.toString())\n                .with(\"pki-node-id\", pkiNodeId.toString())\n                .with(\"peergos.identity.hash\", peergosId.toString());\n        ServerProcesses peergosS3 = Main.PEERGOS.main(peergosArgs);\n        argsToCleanUp.add(peergosArgs);\n        services.add(peergosS3);\n    }\n\n    private static NetworkAccess buildApi(Args args) throws Exception {\n        URL local = new URL(\"http://localhost:\" + args.getInt(\"port\"));\n        return Builder.buildNonCachingJavaNetworkAccess(local, false, 1_000, Optional.empty(), Optional.empty(), Optional.empty()).get();\n    }\n\n    @Test\n    public void grantWriteToFileAndDeleteParent() throws IOException {\n        PeergosNetworkUtils.grantWriteToFileAndDeleteParent(network, new Random(1));\n    }\n\n    @AfterClass\n    public static void cleanup() {\n        try {Thread.sleep(2000);}catch (InterruptedException e) {}\n        if (localS3 != null)\n            localS3.stop();\n        argsToCleanUp.add(pkiArgs);\n        for (Args toClean : argsToCleanUp) {\n            Path peergosDir = toClean.fromPeergosDir(\"\", \"\");\n            System.out.println(\"Deleting \" + peergosDir);\n            deleteFiles(peergosDir.toFile());\n        }\n    }\n\n    @Override\n    public Args getArgs() {\n        return pkiArgs;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/simulation/NativeFileSystemImpl.java",
    "content": "package peergos.server.tests.simulation;\n\nimport peergos.server.simulation.AccessControl;\nimport peergos.server.simulation.FileAsyncReader;\nimport peergos.server.simulation.FileSystem;\nimport peergos.server.simulation.Stat;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.nio.file.attribute.FileTime;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class NativeFileSystemImpl implements FileSystem {\n\n    private final Path root;\n    private final String user;\n    private final AccessControl accessControl;\n\n    public NativeFileSystemImpl(Path root, String user, AccessControl accessControl) {\n        this.root = root;\n        this.user = user;\n        this.accessControl =  accessControl;\n        init();\n    }\n\n    private void init() {\n        Path userRoot = PathUtil.get(\"/\" + user);\n\n        for (Path path : Arrays.asList(\n                userRoot\n//                , sharedRoot,\n//                peergosShare\n        )) {\n            mkdir(path);\n        }\n\n    }\n\n    @Override\n    public String user() {\n        return user;\n    }\n\n    private void ensureCan(Path path, Permission permission) {\n        ensureCan(path, permission, user());\n    }\n\n    private void ensureCan(Path path, Permission permission, String user) {\n        Path nativePath = virtualToNative(path);\n        if (! Files.exists(nativePath) && permission == Permission.READ)\n            throw new IllegalStateException(\"Cannot read \"+ path +\" : native file \"+ nativePath + \" does not exist.\");\n\n        if (! accessControl.can(path, user, permission))\n            throw new IllegalStateException(\"User \" + user() +\" not permitted to \"+ permission + \" \" + path);\n    }\n\n    @Override\n    public byte[] read(Path path, BiConsumer<Long, Long> pc) {\n        Path nativePath = virtualToNative(path);\n        ensureCan(path, Permission.READ);\n        try {\n            return Files.readAllBytes(nativePath);\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe);\n        }\n    }\n\n    @Override\n    public AsyncReader reader(Path path) throws FileNotFoundException {\n        return new FileAsyncReader(path.toFile());\n    }\n\n    @Override\n    public void write(Path path, AsyncReader data, long size, Consumer<Long> progress, boolean resumeUpload) {\n        Path nativePath = virtualToNative(path);\n        ensureCan(path.getParent(), Permission.READ);\n        ensureCan(path, Permission.WRITE);\n\n        try (RandomAccessFile raf = new RandomAccessFile(nativePath.toFile(), \"rw\")) {\n            raf.seek(0);\n            byte[] buf = new byte[4096];\n            long done = 0;\n            while (done < size) {\n                int read = data.readIntoArray(buf, 0, (int) Math.min(buf.length, size - done)).join();\n                raf.write(buf, 0, read);\n                done += read;\n                progress.accept(Integer.toUnsignedLong(read));\n            }\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe);\n        }\n    }\n\n    @Override\n    public void writeSubtree(Path path, Stream<FileWrapper.FolderUploadProperties> folders, Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public void modify(Path path, byte[] data, Consumer<Long> progressConsumer) {\n        Path nativePath = virtualToNative(path);\n        ensureCan(path, Permission.WRITE);\n        try {\n            Files.write(nativePath, data);\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe);\n        }\n    }\n\n    @Override\n    public void delete(Path path) {\n        ensureCan(path, Permission.WRITE);\n\n        walk(path, p -> {\n                try {\n                    Files.delete(virtualToNative(p));\n                } catch (IOException ioe) {\n                    throw new IllegalStateException(ioe);\n                }\n                accessControl.remove(p);\n        });\n\n    }\n\n    private boolean isOwner(Path path) {\n        return user().equals(AccessControl.getOwner(path));\n    }\n\n    @Override\n    public void grant(Path path, String otherUser, FileSystem.Permission permission) {\n        if (! isOwner(path))\n            throw new IllegalStateException();\n        accessControl.add(path, otherUser, permission);\n    }\n\n    @Override\n    public void revoke(Path path, String user, FileSystem.Permission permission) {\n        if (! isOwner(path))\n            throw new IllegalStateException();\n\n        accessControl.remove(path, user, permission);\n    }\n\n    @Override\n    public Stat stat(Path path) {\n        return new Stat() {\n            @Override\n            public String user() {\n                return user;\n            }\n\n            @Override\n            public FileProperties fileProperties() {\n                File file = virtualToNative(path).toFile();\n\n                long length = file.length();\n\n                file.lastModified();\n                if (Integer.MAX_VALUE < length)\n                    throw new IllegalStateException(\"Large files not supported\");\n                int sizeLo = (int) length;\n                int sizeHi = 0;\n\n                LocalDateTime lastModified = LocalDateTime.ofInstant(Instant.ofEpochSecond(file.lastModified() / 1000),\n                        ZoneOffset.systemDefault());\n\n                LocalDateTime created = lastModified;\n                try {\n                    BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class);\n                    FileTime creationTime = attr.creationTime();\n                    created = LocalDateTime.ofInstant(creationTime.toInstant(), ZoneId.systemDefault());\n                } catch (IOException ioe) {\n                    System.err.println(\"Unable to extract file creation time\");\n                }\n                // These things are a bit awkward, not supporting them for now\n                Optional<Thumbnail> thumbnail = Optional.empty();\n                boolean isHidden = false;\n                String mimeType=\"NOT_SUPPORTED\";\n\n                //TODO make files use the new format with a stream secret\n                Optional<byte[]> streamSecret = file.isDirectory() ? Optional.empty() : Optional.empty();\n                return new FileProperties(file.getName(), file.isDirectory(), false, mimeType, sizeHi, sizeLo, lastModified,\n                        created, isHidden, thumbnail, streamSecret, Optional.empty());\n\n            }\n\n            @Override\n            public boolean isReadable() {\n                return accessControl.can(path, user(), Permission.READ);\n            }\n\n            @Override\n            public boolean isWritable() {\n                return accessControl.can(path, user(), Permission.WRITE);\n            }\n        };\n    }\n\n    private Path virtualToNative(Path path) {\n        Path relativePath = PathUtil.get(\"/\").relativize(path);\n        return PathUtil.get(root.toString(), relativePath.toString());\n    }\n\n    @Override\n    public void mkdir(Path path) {\n        if (! path.equals(PathUtil.get(\"/\"+ user()))) {\n            Path parentDir = path.getParent();\n            ensureCan(parentDir, Permission.WRITE);\n        }\n\n\n        Path nativePath = virtualToNative(path);\n        boolean mkdir = nativePath.toFile().mkdir();\n        if (! mkdir)\n            throw new IllegalStateException(\"Could not make dir \"+ nativePath);\n    }\n\n    @Override\n    public List<Path> ls(Path path, boolean showHidden) {\n        ensureCan(path, Permission.READ);\n        if (! showHidden)\n            throw new IllegalStateException();\n\n        Path nativePath = virtualToNative(path);\n        try {\n            return Files.list(nativePath)\n                    .map(e -> path.resolve(e.getFileName().toString()))\n                    .collect(Collectors.toList());\n\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe);\n        }\n    }\n\n    public static void main(String[] args) {\n        System.out.println(\"HELO\");\n\n        Path p1 = PathUtil.get(\"/something/else\");\n        Path p2 = PathUtil.get(\"/another/thing\");\n\n        Path p3 = PathUtil.get(\"/\").relativize(p2);\n        Path p4 = PathUtil.get(p1.toString(), p3.toString());\n\n        System.out.println(p4);\n\n        Path p5 = PathUtil.get(\"/some/thing/else\");\n        System.out.println(p5.getName(1));\n\n    }\n\n    @Override\n    public void follow(FileSystem other, boolean reciprocate) {\n        return; // this isn't being tested... yet\n    }\n\n\n    @Override\n    public Path getRandomSharedPath(Random random, Permission permission, String sharee) {\n        return accessControl.getRandomSharedPath(random, permission, sharee);\n    }\n\n    @Override\n    public List<String> getSharees(Path path, Permission permission) {\n        return accessControl.get(path, permission);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/simulation/Simulator.java",
    "content": "package peergos.server.tests.simulation;\n\nimport peergos.server.*;\nimport peergos.server.simulation.AccessControl;\nimport peergos.server.simulation.FileSystem;\nimport peergos.server.simulation.PeergosFileSystemImpl;\nimport peergos.server.simulation.Stat;\nimport peergos.server.storage.IpfsWrapper;\nimport peergos.server.util.Args;\nimport peergos.server.util.Logging;\nimport peergos.server.tests.PeergosNetworkUtils;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.cryptree.CryptreeNode;\nimport peergos.shared.util.*;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\nimport static peergos.server.tests.UserTests.buildArgs;\n\n/**\n * Run some I/O and then check the file-system is as expected.\n */\npublic class Simulator {\n    private class SimulationRecord {\n        private final List<Simulation> simuations = new ArrayList<>();\n        private final List<Long> timestamps = new ArrayList<>();\n\n        public void add(Simulation simulation) {\n            long now = System.currentTimeMillis();\n            timestamps.add(now);\n            simuations.add(simulation);\n        }\n    }\n\n    public static class FileSystemIndex {\n        //user -> [dir -> file-name]\n        private final Random random;\n        private final Map<String, Map<Path, Set<String>>> index = new HashMap<>();\n\n        public FileSystemIndex(Random random) {\n            this.random = random;\n        }\n\n        private Path getRandomExistingDirectory(String user,  boolean skipRoot) {\n            List<Path> dirs = new ArrayList<>(index.get(user).keySet());\n            Collections.sort(dirs);\n            //skip the root folder - it is special\n            if (skipRoot)\n                dirs.remove(PathUtil.get(\"/\"+user));\n            int pos = random.nextInt(dirs.size());\n            return dirs.get(pos);\n        }\n\n        private Path getRandomExistingFile(String user) {\n\n            Path dir = getRandomExistingDirectory(user, false);\n            List<String> fileNames = new ArrayList<>(index.get(user).get(dir));\n\n            if (fileNames.isEmpty())\n                return getRandomExistingFile(user);\n            int pos = random.nextInt(fileNames.size());\n            String fileName = fileNames.get(pos);\n            return dir.resolve(fileName);\n        }\n\n        public void addUser(String user) {\n            index.put(user, new HashMap<>());\n        }\n\n        public Map<Path, Set<String>> getDirToFiles(String user) {\n            return index.get(user);\n        }\n    }\n\n    private static final Logger LOG = Logging.LOG();\n    private static final int MIN_FILE_LENGTH = 256;\n    private static final int MAX_FILE_LENGTH = Integer.MAX_VALUE;\n\n    enum Simulation {\n        READ_OWN_FILE,\n        WRITE_OWN_FILE,\n        READ_SHARED_FILE,\n        READ_SHARED_DIRECTORY,\n        WRITE_SHARED_FILE,\n        WRITE_SHARED_DIRECTORY,\n        MKDIR,\n        RM,\n        RMDIR,\n        GRANT_READ_FILE,\n        GRANT_READ_DIR,\n        GRANT_WRITE_FILE,\n        GRANT_WRITE_DIR,\n        REVOKE_READ,\n        REVOKE_WRITE;\n\n        public FileSystem.Permission permission() {\n            switch (this) {\n                case GRANT_READ_FILE:\n                case GRANT_READ_DIR:\n                case REVOKE_READ:\n                case READ_SHARED_FILE:\n                case READ_SHARED_DIRECTORY:\n                    return FileSystem.Permission.READ;\n\n                case GRANT_WRITE_DIR:\n                case GRANT_WRITE_FILE:\n                case REVOKE_WRITE:\n                case WRITE_SHARED_FILE:\n                case WRITE_SHARED_DIRECTORY:\n                    return FileSystem.Permission.WRITE;\n            }\n            return null;\n        }\n    }\n\n    private final int opCount;\n    private final Random random;\n    private final Supplier<Simulation> getNextSimulation;\n    private final long meanFileLength;\n    private final boolean randomizeFriendNetwork;\n    private final SimulationRecord simulationRecord = new SimulationRecord();\n    private final FileSystems fileSystems;\n    private final FileSystemIndex index;\n\n\n    long fileNameCounter;\n\n    private String getNextName() {\n        return \"\" + fileNameCounter++;\n    }\n\n    private void rm(String user) {\n        Path path = index.getRandomExistingFile(user);\n        index.getDirToFiles(user).get(path.getParent()).remove(path.getFileName().toString());\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n        FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(user);\n        log(user, Simulation.RM, path);\n        testFileSystem.delete(path);\n        referenceFileSystem.delete(path);\n    }\n\n    private void rmdir(String user) {\n        Path path = index.getRandomExistingDirectory(user, false);\n        log(user, Simulation.RMDIR, path);\n\n        Map<Path, Set<String>> dirsToFiles = index.getDirToFiles(user);\n        for (Path p : new ArrayList<>(dirsToFiles.keySet())) {\n            if (p.startsWith(path))\n                dirsToFiles.remove(p);\n        }\n\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n        FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(user);\n\n        testFileSystem.delete(path);\n        referenceFileSystem.delete(path);\n    }\n\n    private Path mkdir(String user) {\n        String dirBaseName = getNextName();\n        Path path = index.getRandomExistingDirectory(user, false).resolve(dirBaseName);\n        return mkdir(user, path);\n    }\n\n    private Path mkdir(String user, Path path) {\n        index.getDirToFiles(user).putIfAbsent(path, new HashSet<>());\n        log(user, Simulation.MKDIR, path);\n\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n        FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(user);\n        testFileSystem.mkdir(path);\n        referenceFileSystem.mkdir(path);\n        return path;\n    }\n\n    private Path getAvailableFilePath(String user) {\n        return index.getRandomExistingDirectory(user, false).resolve(getNextName());\n    }\n\n    private int getNextFileLength() {\n        double pos = random.nextGaussian();\n        int targetLength = (int) (pos * meanFileLength);\n        return Math.min(MAX_FILE_LENGTH,\n                Math.max(targetLength, MIN_FILE_LENGTH));\n    }\n\n    private byte[] getNextFileContents() {\n        byte[] bytes = new byte[getNextFileLength()];\n        random.nextBytes(bytes);\n        return bytes;\n    }\n\n\n    private static class FileSystems {\n        private final List<Pair<FileSystem, FileSystem>> peergosAndNativeFileSystemPair;\n        private final Random random;\n\n        public FileSystems(List<Pair<FileSystem, FileSystem>> peergosAndNativeFileSystemPair, Random random) {\n            for (Pair<FileSystem, FileSystem> userFileSystem : peergosAndNativeFileSystemPair) {\n                boolean usersMatch = userFileSystem.left.user().equals(userFileSystem.right.user());\n                if (!usersMatch)\n                    throw new IllegalStateException();\n            }\n            this.peergosAndNativeFileSystemPair = peergosAndNativeFileSystemPair;\n            this.random = random;\n        }\n\n        public String getNextUser(String notThisUser) {\n            do {\n                int pos = random.nextInt(peergosAndNativeFileSystemPair.size());\n                String user = peergosAndNativeFileSystemPair.get(pos).right.user();\n                if (user.equals(notThisUser))\n                    continue;\n                return user;\n            } while (true);\n        }\n\n        public String getNextUser() {\n            return getNextUser(null);\n        }\n\n        public NativeFileSystemImpl getReferenceFileSystem(String user) {\n            return peergosAndNativeFileSystemPair.stream()\n                    .filter(e -> e.right.user().equals(user))\n                    .map(e -> (NativeFileSystemImpl) e.right)\n                    .findFirst()\n                    .orElseThrow(() -> new IllegalStateException());\n        }\n\n        public PeergosFileSystemImpl getTestFileSystem(String user) {\n            return peergosAndNativeFileSystemPair.stream()\n                    .filter(e -> e.right.user().equals(user))\n                    .map(e -> (PeergosFileSystemImpl) e.left)\n                    .findFirst()\n                    .orElseThrow(() -> new IllegalStateException());\n        }\n\n        public List<String> getUsers() {\n            return peergosAndNativeFileSystemPair.stream()\n                    .map(e -> e.left.user())\n                    .collect(Collectors.toList());\n        }\n    }\n\n    public Simulator(int opCount, Random random, long meanFileLength,\n                     Supplier<Simulation> getNextSimulation,\n                     FileSystems fileSystems,\n                     boolean randomizeFriendNetwork) {\n\n        this.fileSystems = fileSystems;\n        this.opCount = opCount;\n        this.random = random;\n        this.getNextSimulation = getNextSimulation;\n        this.meanFileLength = meanFileLength;\n        this.randomizeFriendNetwork = randomizeFriendNetwork;\n        this.index = new FileSystemIndex(random);\n    }\n\n    private void readFile(String user, Path path) {\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n        FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(user);\n        referenceFileSystem.read(path);\n        testFileSystem.read(path);\n    }\n    private void readSharedFile(String user, Path path) {\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n        testFileSystem.read(path);\n    }\n    private void readSharedDirectory(String user, Path path) {\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n        List<Path> paths = testFileSystem.ls(path.getParent());\n        if (paths.isEmpty()) {\n            testFileSystem.ls(path.getParent());\n            throw new IllegalStateException(\"Unable to read shared directory. user:\" + user + \" directory:\" + path);\n        }\n    }\n\n    private void log(String user, Simulation simulation, Path path, String... extra) {\n        String extraS = Stream.of(extra)\n                .collect(Collectors.joining(\",\"));\n\n        String msg = \"OP: <\" + user + \"> \" + simulation + \" \" + path + \" \" + extraS;\n        System.out.println(msg);\n    }\n\n    private void write(String user, Path path) {\n\n        byte[] fileContents = getNextFileContents();\n\n        Path dirName = path.getParent();\n        String fileName = path.getFileName().toString();\n        Map<Path, Set<String>> dirsToFiles = index.getDirToFiles(user);\n        Set<String> existingFiles = dirsToFiles.get(dirName);\n        existingFiles.add(fileName);\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n        FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(user);\n\n        testFileSystem.write(path, fileContents);\n        referenceFileSystem.write(path, fileContents);\n    }\n\n    private boolean grantPermission(String granter, String grantee, Path path, FileSystem.Permission permission) {\n\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(granter);\n        FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(granter);\n\n        Set<String> testExistingWriters = new TreeSet<>(testFileSystem.getSharees(path, FileSystem.Permission.WRITE));\n        Set<String> refExistingWriters = new TreeSet<>(referenceFileSystem.getSharees(path, FileSystem.Permission.WRITE));\n        if (! testExistingWriters.toString().equals(refExistingWriters.toString())) {\n            throw new IllegalStateException(\"WRITE sharing mismatch. test:\" + testExistingWriters + \" ref:\" + refExistingWriters);\n        }\n        if(testExistingWriters.contains(grantee)) {\n            LOG.info(\"First revoke WRITE permission: user:\" + granter + \" grantee:\" + grantee);\n            revokePermission(granter, grantee, path, FileSystem.Permission.WRITE);\n        }\n\n        Set<String> testExistingReaders = new TreeSet<>(testFileSystem.getSharees(path, FileSystem.Permission.READ));\n        Set<String> refExistingReaders = new TreeSet<>(referenceFileSystem.getSharees(path, FileSystem.Permission.READ));\n        if (! testExistingReaders.equals(refExistingReaders)) {\n            throw new IllegalStateException(\"READ sharing mismatch. test:\" + testExistingReaders + \" ref:\" + refExistingReaders);\n        }\n        if(testExistingReaders.contains(grantee)) {\n            LOG.info(\"First revoke READ permission: user:\" + granter + \" grantee:\" + grantee);\n            revokePermission(granter, grantee, path, FileSystem.Permission.READ);\n        }\n\n        testFileSystem.grant(path, grantee, permission);\n        referenceFileSystem.grant(path, grantee, permission);\n\n        return true;\n    }\n\n    private boolean revokePermission(String revoker, String revokee, Path path, FileSystem.Permission permission) {\n        FileSystem testFileSystem = fileSystems.getTestFileSystem(revoker);\n        FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(revoker);\n\n        testFileSystem.revoke(path, revokee, permission);\n        referenceFileSystem.revoke(path, revokee, permission);\n\n        return true;\n    }\n\n    private void init() {\n        List<String> users = this.fileSystems.getUsers();\n        Collections.sort(users);\n\n        for (String leftUser : users) {\n            index.addUser(leftUser);\n            index.getDirToFiles(leftUser).put(PathUtil.get(\"/\" + leftUser), new HashSet<>());\n            // seed the file-system\n            run(Simulation.MKDIR, leftUser);\n            run(Simulation.WRITE_OWN_FILE, leftUser);\n\n            for (String rightUser : users) {\n                if (leftUser.compareTo(rightUser) >= 0)\n                    continue;\n\n                if (! randomizeFriendNetwork) {\n                    fileSystems.getTestFileSystem(leftUser).follow(fileSystems.getTestFileSystem(rightUser), true);\n                    fileSystems.getReferenceFileSystem(leftUser).follow(fileSystems.getReferenceFileSystem(rightUser), true);\n                }\n                else {\n                    // If this seems overly-complicated see https://github.com/Peergos/Peergos/issues/638\n                    boolean leftFollowRight = random.nextBoolean();\n                    boolean rightFollowLeft= random.nextBoolean();\n                    if (leftFollowRight && rightFollowLeft) {\n                        fileSystems.getTestFileSystem(leftUser).follow(fileSystems.getTestFileSystem(rightUser), true);\n                        fileSystems.getReferenceFileSystem(leftUser).follow(fileSystems.getReferenceFileSystem(rightUser), true);\n                    }\n                    else if (leftFollowRight) {\n                        fileSystems.getTestFileSystem(leftUser).follow(fileSystems.getTestFileSystem(rightUser), false);\n                        fileSystems.getReferenceFileSystem(leftUser).follow(fileSystems.getReferenceFileSystem(rightUser), false);\n                    }\n                    else if (rightFollowLeft) {\n                        fileSystems.getTestFileSystem(rightUser).follow(fileSystems.getTestFileSystem(leftUser), false);\n                        fileSystems.getReferenceFileSystem(rightUser).follow(fileSystems.getReferenceFileSystem(leftUser), false);\n                    }\n                }\n            }\n\n        }\n    }\n\n    private void run(Simulation simulation) {\n        String nextUser = fileSystems.getNextUser();\n        run(simulation, nextUser);\n    }\n\n    private void run(final Simulation simulation, final String user) {\n        Supplier<String> otherUser = () -> fileSystems.getNextUser(user);\n        Supplier<Path> randomFilePathForUser = () -> index.getRandomExistingFile(user);\n        Supplier<Path> randomFolderPathForUser = () -> index.getRandomExistingDirectory(user, true);\n        BiFunction<String, String, Optional<Path>> randomSharedPath = (owner, sharee) -> {\n            try {\n                Path path = fileSystems.getReferenceFileSystem(owner).getRandomSharedPath(random, simulation.permission(), sharee);\n                return Optional.of(path);\n            } catch (IllegalStateException ile) {\n                //Nothing  shared yet\n                return Optional.empty();\n            }\n        };\n        TriFunction<String, String, Boolean, Optional<Path>> randomSharedPathWithRetries = (owner, sharee, isDirectory) -> {\n            int keepTrying = 0;\n            while (keepTrying < 3) {\n                Optional<Path> path = randomSharedPath.apply(owner, sharee);\n                if (path.isPresent()) {\n                    Stat stat = fileSystems.getReferenceFileSystem(owner).stat(path.get());\n                    if (isDirectory && stat.fileProperties().isDirectory) {\n                        return path;\n                    } else if (! isDirectory && ! stat.fileProperties().isDirectory) {\n                        return path;\n                    }\n                }\n                keepTrying++;\n            }\n            return Optional.empty();\n        };\n        BiFunction<String, String, Optional<Path>> randomSharedDirectoryPath = (owner, sharee) ->\n             randomSharedPathWithRetries.apply(owner, sharee, true);\n\n        BiFunction<String, String, Optional<Path>> randomSharedFilePath = (owner, sharee) ->\n             randomSharedPathWithRetries.apply(owner, sharee, false);\n\n\n        switch (simulation) {\n            case READ_OWN_FILE:\n                Path readPath = randomFilePathForUser.get();\n                log(user, simulation, readPath);\n                readFile(user, readPath);\n                break;\n            case READ_SHARED_FILE:\n                Optional<Path> sharedOpt = randomSharedFilePath.apply(otherUser.get(), user);\n                if (! sharedOpt.isPresent())\n                    return;\n                Path sharedPathToRead = sharedOpt.get();\n                log(user, Simulation.READ_SHARED_FILE, sharedPathToRead);\n                readSharedFile(user, sharedPathToRead);\n                break;\n            case READ_SHARED_DIRECTORY:\n                Optional<Path> sharedDirOpt = randomSharedDirectoryPath.apply(otherUser.get(), user);\n                if (! sharedDirOpt.isPresent())\n                    return;\n                Path sharedDirPathToRead = sharedDirOpt.get();\n                log(user, Simulation.READ_SHARED_DIRECTORY, sharedDirPathToRead);\n                readSharedDirectory(user, sharedDirPathToRead);\n                break;\n            case WRITE_OWN_FILE:\n                Path path = getAvailableFilePath(user);\n                log(user, Simulation.WRITE_OWN_FILE, path);\n                write(user, path);\n                break;\n            case WRITE_SHARED_FILE:\n                Optional<Path> sharedOpt2 = randomSharedFilePath.apply(user, otherUser.get());\n                if (! sharedOpt2.isPresent())\n                    return;\n                Path  sharedPathToWrite = sharedOpt2.get();\n                log(user, Simulation.WRITE_SHARED_FILE, sharedPathToWrite);\n                write(user, sharedPathToWrite);\n                break;\n            case WRITE_SHARED_DIRECTORY:\n                Optional<Path> sharedOpt3 = randomSharedDirectoryPath.apply(user, otherUser.get());\n                if (! sharedOpt3.isPresent())\n                    return;\n                Path  sharedDirectoryPathToWrite = sharedOpt3.get();\n                log(user, Simulation.WRITE_SHARED_DIRECTORY, sharedDirectoryPathToWrite);\n                mkdir(user, sharedDirectoryPathToWrite.resolve(getNextName()));\n                break;\n            case MKDIR:\n                mkdir(user);\n                break;\n            case RM:\n                rm(user);\n                break;\n            case RMDIR:\n                rmdir(user);\n                //ensure not shared\n\n                break;\n            case GRANT_READ_FILE:\n            case GRANT_WRITE_FILE:\n                Path grantFilePath = randomFilePathForUser.get();\n                String fileGrantee = otherUser.get();\n                log(user, simulation, grantFilePath, \"with grantee \"+ fileGrantee);\n                grantPermission(user, fileGrantee, grantFilePath, simulation.permission());\n                break;\n            case GRANT_READ_DIR:\n            case GRANT_WRITE_DIR:\n                Path grantDirPath = randomFolderPathForUser.get();\n                String dirGrantee = otherUser.get();\n                log(user, simulation, grantDirPath, \"with grantee \"+ dirGrantee);\n                grantPermission(user, dirGrantee, grantDirPath, simulation.permission());\n                break;\n            case REVOKE_READ:\n            case REVOKE_WRITE:\n                String revokee = otherUser.get();\n                Optional<Path> revokeOpt = randomSharedPath.apply(user, revokee);\n                if (! revokeOpt.isPresent())\n                    return;\n                Path revokePath = revokeOpt.get();\n                log(user, simulation, revokePath);\n                revokePermission(user, revokee, revokePath, simulation.permission());\n                break;\n            default:\n                throw new IllegalStateException(\"Unexpected simulation \" + simulation);\n        }\n        simulationRecord.add(simulation);\n    }\n\n    public void run(boolean verifyAll) {\n        LOG.info(\"Running file-system IO-simulation\");\n\n        init();\n\n        for (int iOp = 2; iOp < opCount; iOp++) {\n            Simulation simulation = getNextSimulation.get();\n            System.out.println(\"iOp=\" + iOp + \" \" + simulation);\n            run(simulation);\n\n            if (verifyAll) {\n                boolean isVerified = verify();\n                if (!isVerified) {\n                    isVerified = verify();\n                    throw new Error(\"FAILED VERIFICATION!\");\n                }\n            }\n        }\n        LOG.info(\"Running file-system verification\");\n        boolean isVerified = verify();\n        LOG.info(\"System verified =  \" + isVerified);\n    }\n\n    /**\n     * This  will  overwrite the content @ path with [0].\n     * @param user\n     * @param path\n     * @return\n     */\n    private boolean verifySharingPermissions(String user, Path path) {\n        FileSystem peergosFileSystem = fileSystems.getTestFileSystem(user);\n        FileSystem nativeFileSystem = fileSystems.getReferenceFileSystem(user);\n        Stat stat = fileSystems.getReferenceFileSystem(user).stat(path);\n        boolean isDir = stat.fileProperties().isDirectory;\n\n        boolean isVerified = true;\n        for (FileSystem.Permission permission : FileSystem.Permission.values()) {\n            Set<String> shareesInPeergos = new HashSet<>(peergosFileSystem.getSharees(path, permission));\n            Set<String> shareesInLocal = new HashSet<>(nativeFileSystem.getSharees(path, permission));\n            if (! shareesInPeergos.equals(shareesInLocal)) {\n                LOG.info(\"User \" + peergosFileSystem.user() + \" path \" + path + \" has peergos-fs \" + permission.name() + \"ers \" +\n                        shareesInPeergos + \" and local-fs \" + permission.name() + \"ers \" + shareesInLocal);\n                isVerified = false;\n            }\n\n            //check they can actually read\n            for (String sharee : shareesInPeergos) {\n                byte[] read = null;\n                PeergosFileSystemImpl fs = fileSystems.getTestFileSystem(sharee);\n                if (isDir) {\n                    switch (permission) {\n                        case READ:\n                            try {\n                                //can read?\n                                fs.ls(path);\n                            } catch (Exception ex) {\n                                LOG.log(Level.SEVERE, \"User \" + sharee + \" could not read shared-path \" + path + \"!\", ex);\n                                isVerified = false;\n                            }\n                            break;\n                        case WRITE:\n                            try {\n                                //can read?\n                                fs.ls(path);\n                            } catch (Exception ex) {\n                                LOG.log(Level.SEVERE, \"User \" + sharee + \" could not read a writable shared-path \" + path + \"!\", ex);\n                                isVerified = false;\n                            }\n                            if (isVerified) {\n                                try {\n                                    mkdir(user, path.resolve(getNextName()));\n                                } catch (Exception ex) {\n                                    LOG.log(Level.SEVERE, \"User \" + sharee + \" could not write  shared-path \" + path + \"!\", ex);\n                                    isVerified = false;\n                                }\n                            }\n                            break;\n                        default:\n                            throw new IllegalStateException();\n                    }\n                } else {\n                    switch (permission) {\n                        case READ:\n                            try {\n                                //can read?\n                                read = fs.read(path);\n                            } catch (Exception ex) {\n                                LOG.log(Level.SEVERE, \"User \" + sharee + \" could not read shared-path \" + path + \"!\", ex);\n                                isVerified = false;\n                            }\n                            break;\n                        case WRITE:\n                            try {\n                                //can read?\n                                read = fs.read(path);\n                            } catch (Exception ex) {\n                                LOG.log(Level.SEVERE, \"User \" + sharee + \" could not read shared-path \" + path + \"!\", ex);\n                                isVerified = false;\n                            }\n                            if (isVerified) {\n                                try {\n                                    // can overwrite?\n                                    fs.modify(path, read);\n                                } catch (Exception ex) {\n                                    LOG.log(Level.SEVERE, \"User \" + sharee + \" could not write  shared-path \" + path + \"!\", ex);\n                                    isVerified = false;\n                                }\n                            }\n                            break;\n                        default:\n                            throw new IllegalStateException();\n                    }\n                }\n                if (! isVerified) {\n                    break;\n                }\n            }\n\n\n        }\n        return isVerified;\n    }\n\n    private boolean verifyContents(String user, Path path) {\n        FileSystem peergosFileSystem = fileSystems.getTestFileSystem(user);\n        FileSystem nativeFileSystem = fileSystems.getReferenceFileSystem(user);\n        Stat stat = fileSystems.getReferenceFileSystem(user).stat(path);\n        boolean isDir = stat.fileProperties().isDirectory;\n        try {\n            if (isDir) {\n                Set<Path> refDirectoryListing = new TreeSet<>(nativeFileSystem.ls(path, true));\n                Set<Path> testDirectoryListing = new TreeSet<>(peergosFileSystem.ls(path, false));\n                if (!refDirectoryListing.toString().equalsIgnoreCase(testDirectoryListing.toString())) {\n                    LOG.info(\"Path \" + path + \" has different directory contents between the file-systems\");\n                    return false;\n                }\n            } else {\n                byte[] testData = peergosFileSystem.read(path);\n                byte[] refData = nativeFileSystem.read(path);\n                if (!Arrays.equals(testData, refData)) {\n                    LOG.info(\"Path \" + path + \" has different contents between the file-systems\");\n                    return false;\n                }\n            }\n        } catch (Exception ex) {\n            LOG.info(\"Failed to read path + \" + path);\n            return false;\n        }\n        return true;\n    }\n\n    private boolean verify() {\n\n        boolean isGlobalVerified = true;\n\n        for (String user : fileSystems.getUsers()) {\n\n            Map<Path, Set<String>> dirToFiles = index.getDirToFiles(user);\n            Set<Path> expectedFilesForUser = dirToFiles.entrySet().stream()\n                    .flatMap(ee -> ee.getValue().stream()\n                            .map(file -> ee.getKey().resolve(file)))\n                    .collect(Collectors.toSet());\n\n\n            FileSystem testFileSystem = fileSystems.getTestFileSystem(user);\n            FileSystem referenceFileSystem = fileSystems.getReferenceFileSystem(user);\n\n            boolean isUserVerified = true;\n            for (FileSystem fs : Arrays.asList(testFileSystem, referenceFileSystem)) {\n\n                Set<Path> paths = new HashSet<>();\n                fs.walk(paths::add);\n\n                Set<Path> expectedFilesAndFolders = new HashSet<>(expectedFilesForUser);\n                expectedFilesAndFolders.addAll(dirToFiles.keySet());\n\n                // extras?\n                Set<Path> extras = new HashSet<>(paths);\n                extras.removeAll(expectedFilesAndFolders);\n\n                for (Path extra : extras) {\n                    LOG.info(\"filesystem \" + fs + \" has an extra path \" + extra);\n                    isUserVerified = false;\n                }\n\n                // missing?\n                expectedFilesAndFolders.removeAll(paths);\n                for (Path missing : expectedFilesAndFolders) {\n                    LOG.info(\"filesystem \" + fs + \" is missing  the path \" + missing);\n                    isUserVerified = false;\n                }\n            }\n\n            // contents\n            Set<Path> allFilesAndFolders = new HashSet<>(expectedFilesForUser);\n            allFilesAndFolders.addAll(dirToFiles.keySet());\n            for (Path path : allFilesAndFolders) {\n                boolean verifyContents = verifyContents(user, path);\n                if (! verifyContents)\n                    verifyContents(user, path);\n                isUserVerified &= verifyContents;\n                boolean sharingPermissionsAreVerified = verifySharingPermissions(user, path);\n                isUserVerified &= sharingPermissionsAreVerified;\n                if (! sharingPermissionsAreVerified)\n                    verifySharingPermissions(user, path);\n            }\n            if (! isUserVerified) {\n                LOG.info(\"User \" + user + \" is not verified!\");\n                isGlobalVerified = false;\n                break;\n            }\n\n\n        }\n        return isGlobalVerified;\n    }\n\n    private static String usernameToPassword(String username) {\n        return username + \"_password\";\n    }\n\n    public static void main(String[] a) throws Exception {\n        Crypto crypto = Main.initCrypto();\n        Args args = buildArgs()\n//                .with(\"useIPFS\", \"true\")\n                .with(\"useIPFS\", \"false\")\n                .with(\"peergos.password\", \"testpassword\")\n                .with(\"pki.keygen.password\", \"testpkipassword\")\n                .with(\"pki.keyfile.password\", \"testpassword\")\n                .with(IpfsWrapper.IPFS_BOOTSTRAP_NODES, \"\"); // no bootstrapping\n        UserService service = Main.PKI_INIT.main(args).localApi;\n        LOG.info(\"***NETWORK READY***\");\n\n        Function<String, Pair<FileSystem, FileSystem>> fsPairBuilder = username -> {\n            try {\n                WriteSynchronizer synchronizer = new WriteSynchronizer(service.mutable, service.storage, crypto.hasher);\n                MutableTree mutableTree = new MutableTreeImpl(service.mutable, service.storage, crypto.hasher, synchronizer);\n                NetworkAccess networkAccess = new NetworkAccess(service.coreNode, service.account, service.social, service.storage,\n                        service.bats, Optional.empty(), service.mutable, mutableTree, synchronizer, service.controller, service.usage,\n                        service.serverMessages, service.crypto.hasher, Arrays.asList(\"peergos\"), false);\n                UserContext userContext = PeergosNetworkUtils.ensureSignedUp(username, usernameToPassword(username), networkAccess, crypto);\n                PeergosFileSystemImpl peergosFileSystem = new PeergosFileSystemImpl(userContext);\n                Path root = Files.createTempDirectory(\"test_filesystem-\" + username);\n                AccessControl accessControl = new AccessControl.MemoryImpl();\n                NativeFileSystemImpl nativeFileSystem = new NativeFileSystemImpl(root, username, accessControl);\n                return new Pair<>(peergosFileSystem, nativeFileSystem);\n            } catch (Exception ioe) {\n                throw new IllegalStateException(ioe);\n            }\n        };\n\n        Map<Simulation, Double> probabilities = Stream.of(\n                new Pair<>(Simulation.READ_OWN_FILE, 0.0),\n                new Pair<>(Simulation.READ_SHARED_FILE, 0.1),\n                new Pair<>(Simulation.READ_SHARED_DIRECTORY, 0.1),\n                new Pair<>(Simulation.WRITE_OWN_FILE, 0.4),\n                new Pair<>(Simulation.WRITE_SHARED_FILE, 0.1),\n                new Pair<>(Simulation.WRITE_SHARED_DIRECTORY, 0.1),\n                new Pair<>(Simulation.RM, 0.0),\n                new Pair<>(Simulation.MKDIR, 0.1),\n                new Pair<>(Simulation.RMDIR, 0.0),\n                new Pair<>(Simulation.GRANT_READ_FILE, 0.2),\n                new Pair<>(Simulation.GRANT_WRITE_FILE, 0.1),\n                new Pair<>(Simulation.GRANT_READ_DIR, 0.05),\n                new Pair<>(Simulation.GRANT_WRITE_DIR, 0.05),\n                new Pair<>(Simulation.REVOKE_READ, 0.05),\n                new Pair<>(Simulation.REVOKE_WRITE, 0.05)\n        ).collect(\n                Collectors.toMap(e -> e.left, e -> e.right));\n\n        class SimulationSupplier implements Supplier<Simulation> {\n            private final Simulation[] simulations;\n            private final double[] cumulativeProbabilities;\n            private final Random random;\n            private final double probabililtyNorm;\n\n            @Override\n            public Simulation get() {\n                double v = random.nextDouble() * probabililtyNorm;\n                int pos = Arrays.binarySearch(cumulativeProbabilities, v);\n                if (pos < 0)\n                    pos = Math.max(0, -pos -1-1);\n                return simulations[pos];\n            }\n\n            public SimulationSupplier(Map<Simulation, Double> probabilities, Random random) {\n                // remove simulations with empty probabilities\n                probabilities = probabilities.entrySet()\n                        .stream()\n                        .filter(e -> e.getValue()  > 0)\n                        .collect(Collectors.toMap(e -> e.getKey(), e ->  e.getValue()));\n\n                this.simulations = new Simulation[probabilities.size()];\n                this.cumulativeProbabilities = new double[probabilities.size()];\n                this.random  = random;\n                int pos = 0;\n                double acc = 0;\n\n                List<Simulation> sortedSims = probabilities.keySet()\n                        .stream()\n                        .sorted()\n                        .collect(Collectors.toList());\n\n                for (Simulation sim : sortedSims) {\n                    Double prob = probabilities.get(sim);\n                    acc += prob;\n                    simulations[pos] = sim;\n                    cumulativeProbabilities[pos] = acc;\n                    pos++;\n                }\n                this.probabililtyNorm = acc;\n\n\n            }\n        }\n        Args simulatorArgs = Args.parse(a);\n        int opCount = 2000;\n        boolean verifyAll = true;\n        int seed = simulatorArgs.getInt(\"random-seed\", 1);\n        int nUsers = simulatorArgs.getInt(\"n-users\", 3);\n        int meanFileLength  = simulatorArgs.getInt(\"mean-file-length\", 256);\n        boolean randomizeFriendNetwork = simulatorArgs.getBoolean(\"randomize-friend-network\", false);\n        final Random random = new Random(seed);\n\n        Supplier<Simulation> getNextSimulation = new SimulationSupplier(probabilities, random);\n\n        //hard-mode\n        CryptreeNode.setMaxChildLinkPerBlob(10);\n\n        List<Pair<FileSystem, FileSystem>> fs = IntStream.range(0, nUsers)\n                .mapToObj(i -> String.format(\"user-%d\", i))\n                .map(fsPairBuilder).collect(Collectors.toList());;\n\n        FileSystems fileSystems = new FileSystems(fs, random);\n        Simulator simulator = new Simulator(opCount, random, meanFileLength, getNextSimulation, fileSystems, randomizeFriendNetwork);\n\n        try {\n            simulator.run(verifyAll);\n        } catch (Throwable t) {\n            t.printStackTrace();\n            LOG.log(Level.SEVERE, t, () -> \"So long\");\n        } finally {\n            System.exit(0);\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/ChatBenchmark.java",
    "content": "package peergos.server.tests.slow;\r\n\r\nimport org.junit.Assert;\r\nimport org.junit.Test;\r\nimport org.junit.runner.RunWith;\r\nimport org.junit.runners.Parameterized;\r\nimport peergos.server.Builder;\r\nimport peergos.server.Main;\r\nimport peergos.server.UserService;\r\nimport peergos.server.storage.DelayingStorage;\r\nimport peergos.server.tests.PeergosNetworkUtils;\r\nimport peergos.server.tests.UserTests;\r\nimport peergos.server.util.Args;\r\nimport peergos.shared.Crypto;\r\nimport peergos.shared.NetworkAccess;\r\nimport peergos.shared.crypto.symmetric.SymmetricKey;\r\nimport peergos.shared.messaging.ChatController;\r\nimport peergos.shared.messaging.MessageEnvelope;\r\nimport peergos.shared.messaging.MessageRef;\r\nimport peergos.shared.messaging.Messenger;\r\nimport peergos.shared.messaging.messages.ApplicationMessage;\r\nimport peergos.shared.messaging.messages.ReplyTo;\r\nimport peergos.shared.social.FollowRequestWithCipherText;\r\nimport peergos.shared.social.SharedItem;\r\nimport peergos.shared.user.UserContext;\r\nimport peergos.shared.user.fs.AbsoluteCapability;\r\nimport peergos.shared.user.fs.AsyncReader;\r\nimport peergos.shared.user.fs.FileWrapper;\r\nimport peergos.shared.util.Futures;\r\nimport peergos.shared.util.Pair;\r\nimport peergos.shared.util.PathUtil;\r\n\r\nimport java.net.URL;\r\nimport java.util.*;\r\nimport java.util.function.Supplier;\r\nimport java.util.stream.Collectors;\r\nimport java.util.stream.IntStream;\r\n\r\n@RunWith(Parameterized.class)\r\npublic class ChatBenchmark {\r\n\r\n    private static int RANDOM_SEED = 666;\r\n    private final UserService service;\r\n    private final NetworkAccess network;\r\n    private final Crypto crypto = Main.initCrypto();\r\n\r\n    private static Random random = new Random(RANDOM_SEED);\r\n\r\n    public ChatBenchmark(String useIPFS, Random r) throws Exception {\r\n        Pair<UserService, NetworkAccess> pair = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\r\n        this.service = pair.left;\r\n        this.network = pair.right;\r\n    }\r\n\r\n    private static Pair<UserService, NetworkAccess> buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\r\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\r\n        UserService service = Main.PKI_INIT.main(args).localApi;\r\n        NetworkAccess net = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).join();\r\n        int delayMillis = 50;\r\n        NetworkAccess delayed = net.withStorage(s -> new DelayingStorage(s, delayMillis, delayMillis));\r\n        return new Pair<>(service, delayed);\r\n    }\r\n\r\n    @Parameterized.Parameters()\r\n    public static Collection<Object[]> parameters() {\r\n        return Arrays.asList(new Object[][] {\r\n//                {\"IPFS\", new Random(0)}\r\n                {\"NOTIPFS\", new Random(0)}\r\n        });\r\n    }\r\n\r\n    public List<UserContext> getUserContexts(NetworkAccess network, int size, List<String> passwords) {\r\n        return IntStream.range(0, size)\r\n                .mapToObj(e -> {\r\n                    String username = generateUsername();\r\n                    String password = passwords.get(e);\r\n                    try {\r\n                        return ensureSignedUp(username, password, network.clear(), crypto);\r\n                    } catch (Exception ioe) {\r\n                        throw new IllegalStateException(ioe);\r\n                    }\r\n                }).collect(Collectors.toList());\r\n    }\r\n\r\n    public static void friendBetweenGroups(List<UserContext> a, List<UserContext> b) {\r\n        for (UserContext userA : a) {\r\n            for (UserContext userB : b) {\r\n                // send initial request\r\n                userA.sendFollowRequest(userB.username, SymmetricKey.random()).join();\r\n\r\n                // make sharer reciprocate all the follow requests\r\n                List<FollowRequestWithCipherText> sharerRequests = userB.processFollowRequests().join();\r\n                for (FollowRequestWithCipherText u1Request : sharerRequests) {\r\n                    AbsoluteCapability pointer = u1Request.req.entry.get().pointer;\r\n                    Assert.assertTrue(\"Read only capabilities are shared\", ! pointer.wBaseKey.isPresent());\r\n                    boolean accept = true;\r\n                    boolean reciprocate = true;\r\n                    userB.sendReplyFollowRequest(u1Request, accept, reciprocate).join();\r\n                }\r\n\r\n                // complete the friendship connection\r\n                userA.processFollowRequests().join();\r\n            }\r\n        }\r\n    }\r\n\r\n    // createChat(9) duration: 8571 mS, best: 6011 mS, worst: 9163 mS, av: 7669 mS\r\n    // invite(9) duration: 2138 mS, best: 1215 mS, worst: 2358 mS, av: 1942 mS\r\n    // sendMessage*3(9) duration: 1890 mS, best: 1443 mS, worst: 2164 mS, av: 1893 mS\r\n    // cloneLocallyAndJoin(9) duration: 10388 mS, best: 9556 mS, worst: 11258 mS, av: 10145 mS\r\n    // mergeMessages(9) duration: 5359 mS, best: 4446 mS, worst: 5560 mS, av: 5080 mS\r\n    @Test\r\n    public void createChat() {\r\n        String username = generateUsername();\r\n        String password = \"test01\";\r\n        UserContext a = ensureSignedUp(username, password, network, crypto);\r\n\r\n        List<UserContext> shareeUsers = getUserContexts(network, 1, Arrays.asList(password));\r\n        UserContext b = shareeUsers.get(0);\r\n\r\n        // friend sharer with others\r\n        friendBetweenGroups(Arrays.asList(a), shareeUsers);\r\n\r\n        Messenger msgA = new Messenger(a);\r\n\r\n        long worst1 = 0, best1 = Long.MAX_VALUE, accum1 = 0;\r\n        long worst2 = 0, best2 = Long.MAX_VALUE, accum2 = 0;\r\n        long worst3 = 0, best3 = Long.MAX_VALUE, accum3 = 0;\r\n        int limit = 10;\r\n        for (int i = 0; i < limit; i++) {\r\n            long t1 = System.currentTimeMillis();\r\n            ChatController controllerA = msgA.createChat().join();\r\n            long duration1 = System.currentTimeMillis() - t1;\r\n            accum1 = accum1 + duration1;\r\n            worst1 = Math.max(worst1, duration1);\r\n            best1 = Math.min(best1, duration1);\r\n            System.err.printf(\"createChat(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\r\n                    duration1, best1, worst1, (accum1) / (i + 1));\r\n\r\n            long t2 = System.currentTimeMillis();\r\n            controllerA = msgA.invite(controllerA, Arrays.asList(b.username), Arrays.asList(b.signer.publicKeyHash)).join();\r\n            long duration2 = System.currentTimeMillis() - t2;\r\n            accum2 = accum2 + duration2;\r\n            worst2 = Math.max(worst2, duration2);\r\n            best2 = Math.min(best2, duration2);\r\n            System.err.printf(\"invite(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\r\n                    duration2, best2, worst2, (accum2) / (i + 1));\r\n\r\n            ApplicationMessage msg1 = ApplicationMessage.text(\"message 1/3 in chat: \" + i);\r\n            ApplicationMessage msg2 = ApplicationMessage.text(\"message 2/3 in chat: \" + i);\r\n            ApplicationMessage msg3 = ApplicationMessage.text(\"message 3/3 in chat: \" + i);\r\n            long t3 = System.currentTimeMillis();\r\n            controllerA = msgA.sendMessage(controllerA, msg1).join();\r\n            controllerA = msgA.sendMessage(controllerA, msg2).join();\r\n            controllerA = msgA.sendMessage(controllerA, msg3).join();\r\n            long duration3 = System.currentTimeMillis() - t3;\r\n            accum3 = accum3 + duration3;\r\n            worst3 = Math.max(worst3, duration3);\r\n            best3 = Math.min(best3, duration3);\r\n            System.err.printf(\"sendMessage*3(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\r\n                    duration3, best3, worst3, (accum3) / (i + 1));\r\n        }\r\n        List<Pair<SharedItem, FileWrapper>> feed = b.getSocialFeed().join().update().join().getSharedFiles(0, limit + 10).join();\r\n        Messenger msgB = new Messenger(b);\r\n        long worst4 = 0, best4 = Long.MAX_VALUE, accum4 = 0;\r\n        long worst5 = 0, best5 = Long.MAX_VALUE, accum5 = 0;\r\n        for (int i = 0; i < limit; i++) {\r\n            FileWrapper chatSharedDir = feed.get(2 + i).right;\r\n            long t4 = System.currentTimeMillis();\r\n            ChatController controllerB = msgB.cloneLocallyAndJoin(chatSharedDir).join();\r\n            long duration4 = System.currentTimeMillis() - t4;\r\n            accum4 = accum4 + duration4;\r\n            worst4 = Math.max(worst4, duration4);\r\n            best4 = Math.min(best4, duration4);\r\n            System.err.printf(\"cloneLocallyAndJoin(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\r\n                    duration4, best4, worst4, (accum4) / (i + 1));\r\n\r\n            long t5 = System.currentTimeMillis();\r\n            controllerB = msgB.mergeMessages(controllerB, a.username).join();\r\n            long duration5 = System.currentTimeMillis() - t5;\r\n            accum5 = accum5 + duration5;\r\n            worst5 = Math.max(worst5, duration5);\r\n            best5 = Math.min(best5, duration5);\r\n            System.err.printf(\"mergeMessages(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\r\n                    duration5, best5, worst5, (accum5) / (i + 1));\r\n            List<MessageEnvelope> initialMessages = controllerB.getMessages(0, 10).join();\r\n            Assert.assertEquals(initialMessages.size(), 7);\r\n        }\r\n        System.currentTimeMillis();\r\n    }\r\n\r\n    private String generateUsername() {\r\n        return \"test\" + (random.nextInt() % 10000);\r\n    }\r\n\r\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) {\r\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\r\n    }\r\n\r\n    public static <V> Pair<V, Long> time(Supplier<V> work) {\r\n        long t0 = System.currentTimeMillis();\r\n        V res = work.get();\r\n        long t1 = System.currentTimeMillis();\r\n        return new Pair<>(res, t1 - t0);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/peergos/server/tests/slow/DeleteBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class DeleteBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private static final Crypto crypto = Main.initCrypto();\n    private static final Random random = new Random(RANDOM_SEED);\n    private static final Args args = UserTests.buildArgs().with(\"useIPFS\", \"false\");\n\n    public DeleteBenchmark() throws Exception {\n        Main.PKI_INIT.main(args);\n        this.network = buildHttpNetworkAccess();\n    }\n\n    private static NetworkAccess buildHttpNetworkAccess() throws Exception {\n        NetworkAccess base = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get()\n                .withStorage(s -> new UnauthedCachingStorage(s, new RamBlockCache(1024*1204, 1000), crypto.hasher));\n        int delayMillis = 50;\n        return base.withStorage(s -> new DelayingStorage(s, delayMillis, delayMillis));\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n                {}\n        });\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    // DELETE(100) duration: 15552 mS\n    @Test\n    public void deleteFolderOfSmallFiles() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 100).forEach(i -> names.add(randomString()));\n        byte[] data = new byte[1024];\n        random.nextBytes(data);\n\n        List<FileWrapper.FileUploadProperties> files = names.stream()\n                .map(n -> new FileWrapper.FileUploadProperties(n, () -> AsyncReader.build(data), 0, data.length, Optional.empty(), Optional.empty(), false, false, x -> {}))\n                .collect(Collectors.toList());\n        String dirName = \"folder\";\n        userRoot.uploadSubtree(Stream.of(new FileWrapper.FolderUploadProperties(Arrays.asList(dirName), files)),\n                userRoot.mirrorBatId(), context.network, crypto, context.getTransactionService(), f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        Path dirPath = PathUtil.get(username, dirName);\n        FileWrapper folder = context.getByPath(dirPath).join().get();\n\n        long start = System.currentTimeMillis();\n        folder.remove(context.getUserRoot().join(), dirPath, context).join();\n        long duration = System.currentTimeMillis() - start;\n        System.err.printf(\"DELETE(\"+names.size()+\") duration: %d mS\\n\", duration);\n    }\n\n    @Test\n    public void deleteFolderOfSmallFilesWithEmptyCache() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 100).forEach(i -> names.add(randomString()));\n        byte[] data = new byte[1024];\n        random.nextBytes(data);\n\n        List<FileWrapper.FileUploadProperties> files = names.stream()\n                .map(n -> new FileWrapper.FileUploadProperties(n, () -> AsyncReader.build(data), 0, data.length, Optional.empty(), Optional.empty(), false, false, x -> {}))\n                .collect(Collectors.toList());\n        String dirName = \"folder\";\n        userRoot.uploadSubtree(Stream.of(new FileWrapper.FolderUploadProperties(Arrays.asList(dirName), files)),\n                userRoot.mirrorBatId(), context.network, crypto, context.getTransactionService(), f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        Path dirPath = PathUtil.get(username, dirName);\n        FileWrapper folder = context.getByPath(dirPath).join().get();\n\n        context = ensureSignedUp(username, password, buildHttpNetworkAccess(), crypto);\n        System.out.println(\"Start DELETE\");\n        long start = System.currentTimeMillis();\n        folder.remove(context.getUserRoot().join(), dirPath, context).join();\n        long duration = System.currentTimeMillis() - start;\n        System.err.printf(\"DELETE(\"+names.size()+\") duration: %d mS\\n\", duration);\n    }\n\n    // DELETE_FILE(200) duration: 7445 mS old\n    // DELETE_FILE(200) duration: 3058 mS new\n    // DELETE_FILE(200) duration: 4058 mS parallel new\n    @Test\n    public void deleteLargeFile() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n        int size = 200*1024*1024;\n        byte[] data = new byte[size];\n        random.nextBytes(data);\n\n        String filename = \"file.bin\";\n        userRoot.uploadFileJS(filename, AsyncReader.build(data), 0, size, false,\n                userRoot.mirrorBatId(), context.network, crypto, x -> {}, context.getTransactionService(), f -> Futures.of(false)).join();\n        Path filePath = PathUtil.get(username, filename);\n        FileWrapper file = context.getByPath(filePath).join().get();\n\n        long start = System.currentTimeMillis();\n        file.remove(context.getUserRoot().join(), filePath, context).join();\n        long duration = System.currentTimeMillis() - start;\n        System.err.printf(\"DELETE_FILE(\"+(size/1024/1024)+\") duration: %d mS\\n\", duration);\n    }\n\n    private static String randomString() {\n        return UUID.randomUUID().toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/EfficiencyComparison.java",
    "content": "package peergos.server.tests.slow;\nimport peergos.server.*;\nimport peergos.server.util.Logging;\nimport java.util.logging.*;\n\nimport peergos.server.storage.*;\nimport peergos.server.tests.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.function.*;\n\npublic class EfficiencyComparison {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private static final Crypto crypto = Main.initCrypto();\n\n    public static void main(String[] a) throws Exception {\n        Random r = new Random(28);\n\n        Supplier<Multihash> randomHash = () -> {\n            byte[] hash = new byte[32];\n            r.nextBytes(hash);\n            return new Multihash(Multihash.Type.sha2_256, hash);\n        };\n\n        Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> state = new HashMap<>();\n\n        // build a random tree and keep track of the state\n        int nKeys = 10000;\n        for (int i = 0; i < nKeys; i++) {\n            ByteArrayWrapper key = new ByteArrayWrapper(randomHash.get().getHash());\n            Multihash value = randomHash.get();\n            state.put(key, Optional.of(new CborObject.CborMerkleLink(value)));\n        }\n        calculateChampOverhead(state);\n    }\n\n    public static void calculateChampOverhead(Map<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> state) throws Exception {\n        for (int bitWidth = 2; bitWidth <= 8; bitWidth++) {\n            for (int maxCollisions = 1; maxCollisions <= 6; maxCollisions++) {\n                RAMStorage champStorage = new RAMStorage(crypto.hasher);\n                SigningPrivateKeyAndPublicHash champUser = ChampTests.createUser(champStorage, crypto);\n                Pair<Champ<CborObject.CborMerkleLink>, Multihash> current = new Pair<>(Champ.empty(c -> (CborObject.CborMerkleLink)c), champStorage.put(champUser.publicKeyHash,\n                        champUser, Champ.empty(c -> (CborObject.CborMerkleLink)c).serialize(), crypto.hasher,\n                        champStorage.startTransaction(champUser.publicKeyHash).get()).get());\n\n                for (Map.Entry<ByteArrayWrapper, Optional<CborObject.CborMerkleLink>> e : state.entrySet()) {\n                    current = current.left.put(champUser.publicKeyHash, champUser, e.getKey(), e.getKey().data, 0, Optional.empty(),\n                            e.getValue(), bitWidth, maxCollisions, Optional.empty(), x -> Futures.of(x.data),\n                            champStorage.startTransaction(champUser.publicKeyHash).get(), champStorage, crypto.hasher,\n                            current.right).get();\n                }\n\n                int champSize = champStorage.totalSize();\n                long champUsage = champStorage.getRecursiveBlockSize(null, (Cid)current.right, Arrays.asList(new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, new byte[32]))).get();\n\n                int idealUsage = state.size() * (32 + 34);\n                LOG.info(bitWidth + \"-bit champ, \" + maxCollisions + \" max-collisions\");\n                LOG.info(\"Champ used size: \" + champSize + \", Champ usage after gc: \" + champUsage + \", ideal: \"\n                        + idealUsage + \", champ overhead: \" + (double) (champUsage * 100 / idealUsage) / 100);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/FileBlockCacheTests.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.Main;\nimport peergos.server.storage.FileBlockCache;\nimport peergos.shared.Crypto;\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.FileVisitor;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.Comparator;\nimport java.util.Random;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.stream.Stream;\n\npublic class FileBlockCacheTests {\n\n    @Test\n    public void limitSize() throws IOException {\n        Crypto crypto = Main.initCrypto();\n        Path tmpDir = Files.createTempDirectory(\"peergos-test\");\n        int limit = 10_000_000;\n        FileBlockCache cache = new FileBlockCache(tmpDir, limit);\n        byte[] data = new byte[1_000];\n        Random rnd = new Random(42);\n        for (int i=0; i < limit/1_000; i++) {\n            rnd.nextBytes(data);\n            Cid hash = crypto.hasher.hash(data, false).join();\n            cache.put(hash, data).join();\n        }\n        long fullsize = getSize(tmpDir);\n        Assert.assertTrue(fullsize == limit);\n\n        cache.ensureWithinSizeLimit(limit);\n        for (int i=0; i < 100; i++) {\n            rnd.nextBytes(data);\n            Cid hash = crypto.hasher.hash(data, false).join();\n            cache.put(hash, data).join();\n        }\n        cache.ensureWithinSizeLimit(limit);\n        long finalsize = getSize(tmpDir);\n        Assert.assertTrue(finalsize <= limit);\n    }\n\n    @Test\n    public void cleanEmptyDirs() throws IOException {\n        Crypto crypto = Main.initCrypto();\n        Path tmpDir = Files.createTempDirectory(\"peergos-test\");\n        try {\n            int limit = 10_000_000;\n            FileBlockCache cache = new FileBlockCache(tmpDir, limit);\n            byte[] data = new byte[1_000];\n            Random rnd = new Random(42);\n            for (int i = 0; i < limit / 1_000; i++) {\n                rnd.nextBytes(data);\n                Cid hash = crypto.hasher.hash(data, false).join();\n                cache.put(hash, data).join();\n            }\n\n            cache.ensureWithinSizeLimit(0);\n            long finalsize = getSize(tmpDir);\n            Assert.assertTrue(finalsize == 0);\n            long dirCount = getDirs(tmpDir);\n            Assert.assertTrue(dirCount == 0);\n        } finally {\n            try (Stream<Path> paths = Files.walk(tmpDir)) {\n                paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);\n            }\n        }\n    }\n\n    private static long getSize(Path dir) throws IOException {\n        AtomicLong size = new AtomicLong(0);\n        Files.walkFileTree(dir, new FileVisitor<Path>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException {\n                size.addAndGet(basicFileAttributes.size());\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n        });\n        return size.get();\n    }\n\n    private static long getDirs(Path dir) throws IOException {\n        AtomicLong dirs = new AtomicLong(-1);\n        Files.walkFileTree(dir, new FileVisitor<Path>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException {\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException {\n                dirs.incrementAndGet();\n                return FileVisitResult.CONTINUE;\n            }\n        });\n        return dirs.get();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/GCBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.server.corenode.*;\nimport peergos.server.space.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class GCBenchmark {\n    private static final Crypto crypto = Main.initCrypto();\n    private static final Random r = new Random(28);\n\n    @Test\n    public void millionObjects() throws IOException {\n        DeletableContentAddressedStorage storage = new FileContentAddressedStorage(Files.createTempDirectory(\"peergos-tmp\" + System.currentTimeMillis()),\n                new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, RAMStorage.hash(\"FileStorage\".getBytes())),\n                JdbcTransactionStore.build(Main.buildEphemeralSqlite(), new SqliteCommands()),\n                (a, b, c, d) -> Futures.of(true), PartitionStatus.DONE, crypto.hasher);\n        JdbcIpnsAndSocial pointers = new JdbcIpnsAndSocial(Main.buildEphemeralSqlite(), new SqliteCommands());\n        UsageStore usage = new JdbcUsageStore(Main.buildEphemeralSqlite(), new SqliteCommands());\n\n        int nLeavesPerUser = 1_000;\n        int nPointers = 200;\n        for (int i=0; i < nPointers; i++) {\n            SigningKeyPair pair = SigningKeyPair.random(crypto.random, crypto.signer);\n            PublicKeyHash owner = ContentAddressedStorage.hashKey(pair.publicSigningKey);\n            TransactionId tid = storage.startTransaction(owner).join();\n            Multihash root = generateTree(r, owner, storage, nLeavesPerUser, tid);\n            PointerUpdate cas = new PointerUpdate(MaybeMultihash.empty(), MaybeMultihash.of(root), Optional.of(Long.valueOf(i)));\n            pointers.setPointer(owner, Optional.empty(), pair.signMessage(cas.serialize()).join()).join();\n            generateTree(r, owner, storage, nLeavesPerUser/2, tid); // garbage tree\n            storage.closeTransaction(owner, tid).join();\n        }\n\n        GarbageCollector.collect(storage, pointers, usage, Paths.get(\"reachability.sql\"), s -> Futures.of(true),\n                new RamBlockMetadataStore(), (cd, rd, c) -> Futures.of(true), false);\n    }\n\n    private static Multihash generateTree(Random r, PublicKeyHash owner, ContentAddressedStorage storage, int nLeaves, TransactionId tid) {\n        List<Multihash> leaves = new ArrayList<>();\n        for (int i=0; i < nLeaves; i++) {\n            byte[] leaf = (r.nextInt() + \"block-\" + i).getBytes();\n            leaves.add(storage.put(owner, null, null, leaf, tid).join());\n        }\n        List<Multihash> internal = generateSublayer(r, owner, storage, leaves, tid);\n        while (internal.size() > 1)\n            internal = generateSublayer(r, owner, storage, internal, tid);\n        return internal.get(0);\n    }\n\n    private static List<Multihash> generateSublayer(Random r, PublicKeyHash owner, ContentAddressedStorage storage, List<Multihash> childLayer, TransactionId tid) {\n        List<Multihash> nodes = new ArrayList<>();\n        int branchRatio = 4;\n        for (int i=0; i < childLayer.size(); i+=branchRatio) {\n            Map<String, Cborable> links = childLayer.subList(i, Math.min(i + branchRatio, childLayer.size()))\n                    .stream()\n                    .collect(Collectors.toMap(Multihash::toString, CborObject.CborMerkleLink::new));\n            byte[] block = CborObject.CborMap.build(links).serialize();\n            nodes.add(storage.put(owner, null, null, block, tid).join());\n        }\n        return nodes;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/IpfsStressTest.java",
    "content": "package peergos.server.tests.slow;\nimport java.util.logging.*;\n\nimport peergos.server.tests.*;\nimport peergos.server.util.Args;\nimport peergos.server.util.Logging;\n\nimport org.junit.*;\nimport peergos.server.*;\nimport peergos.shared.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\nimport static org.junit.Assert.*;\n\npublic class IpfsStressTest {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private final Random random;\n\n    public IpfsStressTest(Random r) throws Exception {\n        this.random = r;\n        int portMin = 9000;\n        int portRange = 4000;\n        int webPort = portMin + r.nextInt(portRange);\n        int corePort = portMin + portRange + r.nextInt(portRange);\n        Args args = Args.parse(new String[]{\"useIPFS\", \"true\", \"-port\", Integer.toString(webPort), \"-corenodePort\", Integer.toString(corePort)});\n        Main.PKI_INIT.main(args);\n        this.network = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + webPort), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    public static void main(String[] args) throws Exception {\n        new IpfsStressTest(new Random(0)).stressTest();\n    }\n\n    private String generateUsername() {\n        return \"test\" + Math.abs(random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    @Test\n    public void stressTest() throws Exception {\n        String username = generateUsername();\n        String password = \"test\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n\n        // first generate a random file tree\n        LOG.info(\"Building file tree...\");\n        int depth = 4;\n        int filesAndFolders = generateFileTree(context, PathUtil.get(username), random, depth);\n        LOG.info(\"Built file tree with \" + filesAndFolders + \" files/dirs to depth \" + depth);\n    }\n\n    public static int generateFileTree(UserContext context, Path root, Random rnd, int maxDepth) throws Exception {\n        int files = 1 + rnd.nextInt(4);\n        List<String> fileNames = randomNames(files, rnd);\n        for (String filename: fileNames)\n            generateFile(context, root, filename, rnd);\n        if (maxDepth == 0)\n            return files;\n\n        int folders = 2 + rnd.nextInt(3);\n        List<String> folderNames = randomNames(folders, rnd);\n        int total = files + folders;\n        for (String folderName: folderNames) {\n            mkdir(context, root, folderName, rnd);\n            total += generateFileTree(context, root.resolve(folderName), rnd, maxDepth - 1);\n        }\n        return total;\n    }\n\n    private static List<String> randomNames(int n, Random rnd) {\n        return IntStream.range(0, n)\n                .mapToObj(i -> randomString(rnd, 16))\n                .collect(Collectors.toList());\n    }\n\n    public static void mkdir(UserContext context, Path parentPath, String name, Random rnd) throws Exception {\n        context.getByPath(parentPath.toString()).get().get()\n                .mkdir(name, context.network, false, context.mirrorBatId(), context.crypto).get();\n    }\n\n    public static void generateFile(UserContext context, Path parentPath, String name, Random rnd) throws Exception {\n        int size = rnd.nextInt(15*1024*1024);\n        FileWrapper parent = context.getByPath(parentPath.toString()).get().get();\n        parent.uploadOrReplaceFile(name, new AsyncReader.ArrayBacked(randomData(rnd, size)), size,\n                        context.network, context.crypto, () -> false, x -> {}).get();\n    }\n\n    public static void checkFileContents(byte[] expected, FileWrapper f, UserContext context) throws Exception {\n        long size = f.getFileProperties().size;\n        byte[] retrievedData = Serialize.readFully(f.getInputStream(context.network, context.crypto,\n            size, l-> {}).get(), f.getSize()).get();\n        assertEquals(expected.length, size);\n        assertTrue(\"Correct contents\", Arrays.equals(retrievedData, expected));\n    }\n\n    private static void checkFileContentsChunked(byte[] expected, FileWrapper f, UserContext context, int  nReads) throws Exception {\n\n        AsyncReader in = f.getInputStream(context.network, context.crypto,\n                f.getFileProperties().size, l -> {}).get();\n        assertTrue(nReads > 1);\n\n        long size = f.getSize();\n        long readLength = size/nReads;\n\n\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n\n        for (int i = 0; i < nReads; i++) {\n            long pos = i * readLength;\n            long len = Math.min(readLength , expected.length - pos);\n            LOG.info(\"Reading from \"+ pos +\" to \"+ (pos + len) +\" with total \"+ expected.length);\n            byte[] retrievedData = Serialize.readFully(\n                    in,\n                    len).get();\n            bout.write(retrievedData);\n        }\n        byte[] readBytes = bout.toByteArray();\n        assertEquals(\"Lengths correct\", readBytes.length, expected.length);\n\n        String start = ArrayOps.bytesToHex(Arrays.copyOfRange(expected, 0, 10));\n\n        for (int i = 0; i < readBytes.length; i++)\n            assertEquals(\"position  \" + i + \" out of \" + readBytes.length + \", start of file \" + start,\n                    ArrayOps.byteToHex(readBytes[i] & 0xFF),\n                    ArrayOps.byteToHex(expected[i] & 0xFF));\n\n        assertTrue(\"Correct contents\", Arrays.equals(readBytes, expected));\n    }\n\n\n    public static String randomString(Random r, int length) {\n        return ArrayOps.bytesToHex(randomData(r, length/2));\n    }\n\n    public static byte[] randomData(Random r, int length) {\n        byte[] data = new byte[length];\n        r.nextBytes(data);\n        return data;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/LoginBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.user.*;\n\nimport java.net.*;\nimport java.util.*;\n\n@RunWith(Parameterized.class)\npublic class LoginBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public LoginBenchmark(String useIPFS, Random r) throws Exception {\n        this.network = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n    }\n\n    private static NetworkAccess buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        Main.PKI_INIT.main(args);\n        return Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    // LOGIN(19) duration: 1326 mS, best: 1292 mS, worst: 1375 mS, av: 1322 mS\n    @Test\n    public void login() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        ensureSignedUp(username, password, network, crypto);\n\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n        for (int i=0; i < 20; i++) {\n            long t1 = System.currentTimeMillis();\n            ensureSignedUp(username, password, network, crypto);\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"LOGIN(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/MediumFileBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class MediumFileBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public MediumFileBenchmark(String useIPFS, Random r) throws Exception {\n        this.network = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n    }\n\n    private static NetworkAccess buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        Main.PKI_INIT.main(args);\n        return Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    // UPLOAD(0) duration: 7451 mS, best: 7451 mS, worst: 7451 mS, av: 7451 mS\n    // to\n    // UPLOAD(99) duration: 8327 mS, best: 7113 mS, worst: 9623 mS, av: 7740 mS or 1.3 MiB/s\n    //\n    // GetData(10) duration: 1057 mS, best: 822 mS, worst: 1057 mS, av: 896 mS or 11.2 MiB/s\n    @Test\n    public void mediumFiles() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 100).forEach(i -> names.add(randomString()));\n        byte[] data = new byte[10*1024*1024];\n        random.nextBytes(data);\n\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n        for (int i=0; i < names.size(); i++) {\n            String filename = names.get(i);\n            long t1 = System.currentTimeMillis();\n            userRoot = userRoot.uploadOrReplaceFile(filename, AsyncReader.build(data), data.length, context.network,\n                    crypto, () -> false, x-> {}).join();\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"UPLOAD(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n\n        long worstRead = 0, bestRead = Long.MAX_VALUE, startRead = System.currentTimeMillis();\n        for (int i=0; i < 100; i++) {\n            long t1 = System.currentTimeMillis();\n            FileWrapper file = context.getByPath(PathUtil.get(username, names.get(random.nextInt(names.size()))))\n                    .join().get();\n            AsyncReader reader = file.getInputStream(network, crypto, x -> {}).join();\n            byte[] readData = Serialize.readFully(reader, data.length).join();\n            long duration = System.currentTimeMillis() - t1;\n            Assert.assertTrue(Arrays.equals(readData, data));\n            worstRead = Math.max(worstRead, duration);\n            bestRead = Math.min(bestRead, duration);\n            System.err.printf(\"GetData(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, bestRead, worstRead, (t1 + duration - startRead) / (i + 1));\n        }\n    }\n\n    private static String randomString() {\n        return UUID.randomUUID().toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/MkdirBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.Args;\nimport peergos.shared.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.util.*;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class MkdirBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public MkdirBenchmark(String useIPFS, Random r) throws Exception {\n        this.network = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n    }\n\n    private static NetworkAccess buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        Main.PKI_INIT.main(args);\n        return Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    // (0 - 100 mkdirs)\n    // All ram, not http =>  40 -   54 ms\n    // All ram, http     => 400 -  416 ms\n    // IPFS, http        => 660 -  840 ms\n    //\n    // current baselines:\n    // MKDIR(99) duration: 853 mS, best: 683 mS, worst: 1200 mS, av: 855 mS\n    //    mutable.set 130 mS\n    //    block.put 62 mS\n    // GetByPath(99) duration: 21 mS, best: 19 mS, worst: 38 mS, av: 21 mS\n    @Test\n    public void hugeFolder() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 100).forEach(i -> names.add(randomString()));\n\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n        for (int i=0; i < names.size(); i++) {\n            String filename = names.get(i);\n            long t1 = System.currentTimeMillis();\n            userRoot = userRoot.mkdir(filename, context.network, false, userRoot.mirrorBatId(), crypto).join();\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"MKDIR(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n\n        long worstRead = 0, bestRead = Long.MAX_VALUE, startRead = System.currentTimeMillis();\n        for (int i=0; i < 100; i++) {\n            long t1 = System.currentTimeMillis();\n            context.getByPath(PathUtil.get(username, names.get(random.nextInt(names.size())))).join();\n            long duration = System.currentTimeMillis() - t1;\n            worstRead = Math.max(worstRead, duration);\n            bestRead = Math.min(bestRead, duration);\n            System.err.printf(\"GetByPath(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, bestRead, worstRead, (t1 + duration - startRead) / (i + 1));\n        }\n    }\n\n    private static String randomString() {\n        return UUID.randomUUID().toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/MultipartBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\n\nimport java.net.*;\nimport java.util.*;\n\n@RunWith(Parameterized.class)\npublic class MultipartBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public MultipartBenchmark(String useIPFS, Random r) throws Exception {\n        this.network = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n    }\n\n    private static NetworkAccess buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        Main.PKI_INIT.main(args);\n        return Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    // 70 KB/S throughput\n    @Test\n    public void smallFragments() throws Exception {\n        testWriteThroughput(4096);\n    }\n\n    // 1500 KB/S throughput\n    @Test\n    public void largeFragments() throws Exception {\n        testWriteThroughput(128*1024);\n    }\n\n    public void testWriteThroughput(int fragmentSize) throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n        TransactionId tid = network.dhtClient.startTransaction(context.signer.publicKeyHash).join();\n        byte[] data = new byte[fragmentSize];\n        for (int i=0; i < 10000; i++) {\n            random.nextBytes(data);\n            long t1 = System.currentTimeMillis();\n            network.dhtClient.put(context.signer.publicKeyHash, context.signer, data, network.hasher, tid).join();\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"PUT(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS %d KB/S\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1), data.length / duration);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/MultipartProfiling.java",
    "content": "package peergos.server.tests.slow;\nimport java.net.http.HttpClient;\nimport java.time.Duration;\nimport java.util.logging.*;\n\nimport peergos.server.util.Logging;\n\nimport com.sun.net.httpserver.*;\nimport org.junit.*;\nimport peergos.server.net.*;\nimport peergos.shared.io.ipfs.api.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class MultipartProfiling {\n\tprivate static final Logger LOG = Logging.LOG();\n\n    private final int port;\n    private final HttpServer server;\n    private final Queue<List<byte[]>> received = new LinkedBlockingQueue<>();\n    private final Random r = new Random(1);\n\n    public MultipartProfiling() throws IOException {\n        this.port = 5679;\n        InetSocketAddress localhost = new InetSocketAddress(\"localhost\", port);\n        this.server = HttpServer.create(localhost, 10);\n        server.createContext(\"/multipart\", this::handle);\n        server.setExecutor(Executors.newFixedThreadPool(10));\n        server.start();\n    }\n\n    @After\n    public void finish() {\n        server.stop(0);\n    }\n\n    public void handle(HttpExchange httpExchange) throws IOException {\n        try {\n            String boundary = httpExchange.getRequestHeaders().get(\"Content-Type\")\n                    .stream()\n                    .filter(s -> s.contains(\"boundary=\"))\n                    .map(s -> s.substring(s.indexOf(\"=\") + 1))\n                    .findAny()\n                    .get();\n            List<byte[]> data = MultipartReceiver.extractFiles(httpExchange.getRequestBody(), boundary);\n            received.add(data);\n            httpExchange.sendResponseHeaders(200, 0);\n            DataOutputStream dout = new DataOutputStream(httpExchange.getResponseBody());\n            dout.write(\"true\".getBytes());\n            dout.flush();\n            dout.close();\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n\n    private byte[] randomArray(int len) {\n        byte[] res = new byte[len];\n        r.nextBytes(res);\n        return res;\n    }\n\n    @Test\n    public void profileAll() throws IOException {\n        long t1 = System.currentTimeMillis();\n        int requests = 1_000;\n        for (int i = 0; i < requests; i++)\n            profile(100 * 1024, 1);\n        long t2 = System.currentTimeMillis();\n        System.out.printf(\"Did %d multipart requests, averaging %d mS each.\\n\", requests, (t2 - t1) / requests);\n    }\n\n    private void profile(int size, int count) throws IOException {\n        HttpClient client = HttpClient.newBuilder()\n                .connectTimeout(Duration.ofMillis(1_000))\n                .build();\n        Multipart sender = new Multipart(client, \"http://localhost:\" + port + \"/multipart\", \"UTF-8\", Collections.emptyMap(), 0);\n        byte[] data = randomArray(size);\n        for (int i = 0; i < count; i++)\n            sender.addFilePart(\"file\", new NamedStreamable.ByteArrayWrapper(data));\n\n        String res = new String(sender.finish().join());\n\n        List<byte[]> result = received.poll();\n\n        boolean sameLength = result.size() == count && result.get(0).length == size;\n\n        Assert.assertTrue(\"Same length on other end\", sameLength);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/PostgresUserTests.java",
    "content": "package peergos.server.tests.slow;\n\nimport io.libp2p.core.*;\nimport io.libp2p.core.crypto.*;\nimport io.libp2p.crypto.keys.*;\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport org.peergos.protocol.ipns.*;\nimport peergos.server.*;\nimport peergos.server.sql.*;\nimport peergos.server.storage.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.resolution.*;\nimport peergos.shared.storage.IpnsEntry;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.nio.charset.*;\nimport java.nio.file.*;\nimport java.util.*;\n\n@RunWith(Parameterized.class)\npublic class PostgresUserTests extends UserTests {\n    /** To use this run a local postgres on the default port\n     *\n     *  sudo apt install postgresql postgresql-contrib\n     *\n     *  sudo -i -u postgres\n     *  psql\n     *\n     *  create user testuser with encrypted password 'testpassword';\n     *  create database peergostest;\n     *  grant all privileges on database peergostest to testuser;\n     *\n     *  # Between test runs\n     *  drop database peergostest;create database peergostest;grant all privileges on database peergostest to testuser;\n     */\n    private static Args args = buildArgs()\n            .with(\"useIPFS\", \"false\")\n            .with(\"use-postgres\", \"true\")\n            .with(\"postgres.host\", \"localhost\")\n            .with(\"postgres.database\", \"peergostest\")\n            .with(\"postgres.username\", \"testuser\")\n            .with(\"postgres.password\", \"testpassword\");\n\n    public PostgresUserTests(NetworkAccess network, UserService service) {\n        super(network, service);\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        UserService service = Main.PKI_INIT.main(args).localApi;\n        WriteSynchronizer synchronizer = new WriteSynchronizer(service.mutable, service.storage, crypto.hasher);\n        MutableTree mutableTree = new MutableTreeImpl(service.mutable, service.storage, crypto.hasher, synchronizer);\n        NetworkAccess network = new NetworkAccess(service.coreNode, service.account, service.social, service.storage,\n                service.bats, Optional.empty(), service.mutable, mutableTree, synchronizer, service.controller, service.usage, service.serverMessages,\n                service.crypto.hasher, Arrays.asList(\"peergos\"), false);\n        return Arrays.asList(new Object[][] {\n                {network, service}\n        });\n    }\n\n    @AfterClass\n    public static void cleanup() {\n        Path peergosDir = args.fromPeergosDir(\"\", \"\");\n        System.out.println(\"Deleting \" + peergosDir);\n        deleteFiles(peergosDir.toFile());\n    }\n\n    @Override\n    public Args getArgs() {\n        return args;\n    }\n\n    @Test\n    public void rsaRotation() {\n        JdbcServerIdentityStore idstore = JdbcServerIdentityStore.build(Builder.getPostgresConnector(args, \"\"), new PostgresCommands(), Main.initCrypto());\n        // create a new identity\n        PrivKey currentPrivate = RsaKt.generateRsaKeyPair(2048).getFirst();\n        byte[] signedRecord = ServerIdentity.generateSignedIpnsRecord(currentPrivate, Optional.empty(), false,  1);\n        idstore.addIdentity(PeerId.fromPubKey(currentPrivate.publicKey()), signedRecord);\n\n        List<PeerId> ids = idstore.getIdentities();\n        Assert.assertEquals(1, ids.size());\n        PeerId current = ids.get(0);\n        Assert.assertEquals(current, PeerId.fromPubKey(currentPrivate.publicKey()));\n\n        String password = Passwords.generate();\n        Crypto crypto = Main.initCrypto();\n        PrivKey nextPriv = ServerIdentity.generateNextIdentity(password, current, crypto);\n        PeerId nextPeerId = PeerId.fromPubKey(nextPriv.publicKey());\n        idstore.setPrivateKey(currentPrivate);\n        idstore.setRecord(current, ServerIdentity.generateSignedIpnsRecord(currentPrivate, Optional.of(Multihash.decode(nextPeerId.getBytes())), true,2));\n        idstore.addIdentity(nextPeerId, ServerIdentity.generateSignedIpnsRecord(nextPriv, Optional.empty(), false, 1));\n\n        List<PeerId> updated = idstore.getIdentities();\n        Assert.assertEquals(2, updated.size());\n        Assert.assertEquals(nextPeerId, updated.get(1));\n        byte[] prevRecord = idstore.getRecord(current);\n        Optional<IpnsMapping> prevIpnsMapping = IPNS.parseAndValidateIpnsEntry(\n                ArrayOps.concat(\"/ipns/\".getBytes(StandardCharsets.UTF_8), current.getBytes()),\n                prevRecord);\n        IpnsEntry ipnsData = new IpnsEntry(prevIpnsMapping.get().getSignature(), prevIpnsMapping.get().getData());\n        ResolutionRecord prevRes = ipnsData.getValue();\n        Assert.assertEquals(prevRes.moved, true);\n        Assert.assertEquals(prevRes.host, Optional.of(Multihash.fromBase58(nextPeerId.toBase58())));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/SignupBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.user.*;\n\nimport java.net.*;\nimport java.util.*;\n\n@RunWith(Parameterized.class)\npublic class SignupBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public SignupBenchmark(String useIPFS, Random r) throws Exception {\n        this.network = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n    }\n\n    private static NetworkAccess buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        Main.PKI_INIT.main(args);\n        return Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    // SIGNUP(10) duration: 6800 mS, best: 6748 mS, worst: 7109 mS, av: 6839 mS\n    @Test\n    public void signup() throws Exception {\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n\n        for (int i=0; i < 20; i++) {\n            long t1 = System.currentTimeMillis();\n            String username = generateUsername();\n            String password = \"test01\";\n            ensureSignedUp(username, password, network, crypto);\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"SIGNUP(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/SmallFileBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class SmallFileBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public SmallFileBenchmark(String useIPFS, Random r) throws Exception {\n        this.network = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n    }\n\n    private static NetworkAccess buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        Main.PKI_INIT.main(args);\n        NetworkAccess base = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).get();\n        int delayMillis = 50;\n        return base.withStorage(s -> new DelayingStorage(s, delayMillis, delayMillis));\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    // UPLOAD(100) duration: 6137 mS, av: 61 mS\n    @Test\n    public void smallFilesBulk() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 100).forEach(i -> names.add(randomString()));\n        byte[] data = new byte[1024];\n        random.nextBytes(data);\n\n        long start = System.currentTimeMillis();\n        List<Long> progressReceived = new ArrayList<>();\n        ProgressConsumer<Long> progressCounter = (num) -> {\n            progressReceived.add(num);\n        };\n        List<FileWrapper.FileUploadProperties> files = names.stream()\n                .map(n -> new FileWrapper.FileUploadProperties(n, () -> AsyncReader.build(data), 0, data.length, Optional.empty(), Optional.empty(), false, false, progressCounter))\n                .collect(Collectors.toList());\n        userRoot.uploadSubtree(Stream.of(new FileWrapper.FolderUploadProperties(Collections.emptyList(), files)),\n                userRoot.mirrorBatId(), context.network, crypto, context.getTransactionService(), f -> Futures.of(false), f -> Futures.of(true), () -> true).join();\n        long duration = System.currentTimeMillis() - start;\n        System.err.printf(\"UPLOAD(\"+names.size()+\") duration: %d mS, av: %d mS\\n\", duration, (duration) / names.size());\n        Assert.assertTrue(\"Correct progress\", progressReceived.stream().mapToLong(i -> i).sum() == data.length * names.size());\n    }\n\n    // UPLOAD(0) duration: 1085 mS, best: 1085 mS, worst: 1085 mS, av: 1085 mS\n    // to\n    // UPLOAD(99) duration: 1240 mS, best: 1015 mS, worst: 1655 mS, av: 1230 mS\n    //\n    // GetData(99) duration: 28 mS, best: 27 mS, worst: 43 mS, av: 29 mS\n    // ****** delayed ******\n    // UPLOAD(0) duration: 1209 mS, best: 1209 mS, worst: 1209 mS, av: 1209 mS\n    // to\n    // UPLOAD(99) duration: 1598 mS, best: 1130 mS, worst: 2101 mS, av: 1610 mS\n    //\n    // GetData(99) duration: 70 mS, best: 60 mS, worst: 114 mS, av: 69 mS\n    @Test\n    public void smallFiles() throws Exception {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        FileWrapper userRoot = context.getUserRoot().get();\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 100).forEach(i -> names.add(randomString()));\n        byte[] data = new byte[1024];\n        random.nextBytes(data);\n\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n        for (int i=0; i < names.size(); i++) {\n            String filename = names.get(i);\n            long t1 = System.currentTimeMillis();\n            userRoot = userRoot.uploadOrReplaceFile(filename, AsyncReader.build(data), data.length, context.network,\n                    crypto, () -> false, x-> {}).join();\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"UPLOAD(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n\n        long worstRead = 0, bestRead = Long.MAX_VALUE, startRead = System.currentTimeMillis();\n        for (int i=0; i < 100; i++) {\n            long t1 = System.currentTimeMillis();\n            FileWrapper file = context.getByPath(PathUtil.get(username, names.get(random.nextInt(names.size()))))\n                    .join().get();\n            AsyncReader reader = file.getInputStream(network, crypto, x -> {}).join();\n            byte[] readData = Serialize.readFully(reader, data.length).join();\n            long duration = System.currentTimeMillis() - t1;\n            Assert.assertTrue(Arrays.equals(readData, data));\n            worstRead = Math.max(worstRead, duration);\n            bestRead = Math.min(bestRead, duration);\n            System.err.printf(\"GetData(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, bestRead, worstRead, (t1 + duration - startRead) / (i + 1));\n        }\n    }\n\n    private static String randomString() {\n        return UUID.randomUUID().toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/SocialBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class SocialBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final UserService service;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public SocialBenchmark(String useIPFS, Random r) throws Exception {\n        Pair<UserService, NetworkAccess> pair = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n        this.service = pair.left;\n        this.network = pair.right;\n    }\n\n    private static Pair<UserService, NetworkAccess> buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        UserService service = Main.PKI_INIT.main(args).localApi;\n        NetworkAccess net = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).join();\n        int delayMillis = 50;\n        NetworkAccess delayed = net.withStorage(s -> new DelayingStorage(s, delayMillis, delayMillis));\n        return new Pair<>(service, delayed);\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    // SendFollowRequest(19) duration: 2340 mS, best: 1519 mS, worst: 2340 mS, av: 1734 mS\n    @Test\n    public void social() {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 20).forEach(i -> names.add(generateUsername()));\n        names.forEach(name -> ensureSignedUp(name, password, network, crypto));\n\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n\n        for (int i = 0; i < 20; i++) {\n            long t1 = System.currentTimeMillis();\n            context.sendInitialFollowRequest(names.get(i)).join();\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"SendFollowRequest(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n    }\n\n    // ReplyToFollowRequest(19) duration: 4291 mS, best: 3392 mS, worst: 5239 mS, av: 3898 mS\n    @Test\n    public void replyToFollowRequest() {\n        String username = generateUsername();\n        String password = \"test01\";\n        UserContext context = ensureSignedUp(username, password, network, crypto);\n        List<String> names = new ArrayList<>();\n        IntStream.range(0, 20).forEach(i -> names.add(generateUsername()));\n        List<UserContext> users = names.stream()\n                .map(name -> ensureSignedUp(name, password, network, crypto))\n                .collect(Collectors.toList());\n\n        for (int i = 0; i < 20; i++) {\n            users.get(i).sendInitialFollowRequest(username).join();\n        }\n\n        List<FollowRequestWithCipherText> pending = context.getSocialState().join().pendingIncoming;\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n        // Profile accepting the requests\n        for (int i = 0; i < 20; i++) {\n            FollowRequestWithCipherText req = pending.get(i);\n            long t1 = System.currentTimeMillis();\n            context.sendReplyFollowRequest(req, true, true).join();\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"ReplyToFollowRequest(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n    }\n\n    @Test\n    public void manyFriendsAndShares() {\n        String username = generateUsername();\n        String password = \"password\";\n        Pair<UserContext, Long> initial = time(() -> ensureSignedUp(username, password, network, crypto));\n        long initialTime = initial.right;\n        UserContext us = initial.left;\n//        Assert.assertTrue(initialTime < 30_000);\n\n        int nFriends = 20;\n        List<Pair<String, String>> otherUsers = IntStream.range(0, nFriends)\n                .mapToObj(x -> new Pair<>(generateUsername(), password))\n                .collect(Collectors.toList());\n        List<UserContext> friends = otherUsers.stream()\n                .map(p -> ensureSignedUp(p.left, p.right, network, crypto))\n                .collect(Collectors.toList());\n\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(us), friends);\n        long withFriends = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n//        Assert.assertTrue(withFriends < 3_000);\n\n        // Add n files, each shared read only with a random friend\n        int nFiles = 40;\n        for (int i=0; i < nFiles; i++) {\n            byte[] fileData = \"dataaaa\".getBytes();\n            String filename = \"File\" + i;\n            us.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(fileData),\n                    fileData.length, network, crypto, () -> false, x -> {}).join();\n            String sharee = otherUsers.get(random.nextInt(otherUsers.size())).left;\n            us.shareReadAccessWith(PathUtil.get(username, filename), Collections.singleton(sharee)).join();\n        }\n\n        long initialWithReadSharingOut = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n        long withReadSharingOut = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n\n        // Add n files owned by friends, each shared read only with us\n        for (int i=0; i < nFiles; i++) {\n            byte[] fileData = \"dataaaa\".getBytes();\n            String filename = \"File\" + i;\n            UserContext friend = friends.get(random.nextInt(friends.size()));\n            friend.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(fileData),\n                    fileData.length, network, crypto, () -> false, x -> {}).join();\n            friend.shareReadAccessWith(PathUtil.get(friend.username, filename), Collections.singleton(username)).join();\n        }\n\n        long initialWithReadSharingIn = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n        long withReadSharingIn = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n//        Assert.assertTrue(withReadSharingIn < 4_000);\n\n        for (int i=0; i < nFiles; i++) {\n            byte[] fileData = \"dataaaa\".getBytes();\n            String filename = \"FileW\" + i;\n            us.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(fileData),\n                    fileData.length, network, crypto, () -> false, x -> {}).join();\n            String sharee = otherUsers.get(random.nextInt(otherUsers.size())).left;\n            us.shareWriteAccessWith(PathUtil.get(username, filename), Collections.singleton(sharee)).join();\n        }\n\n        long initialWithWriteSharingOut = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n        long withWriteSharingOut = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n\n        // Add n files owned by friends, each shared read only with us\n        for (int i=0; i < nFiles; i++) {\n            byte[] fileData = \"dataaaa\".getBytes();\n            String filename = \"FileW\" + i;\n            UserContext friend = friends.get(random.nextInt(friends.size()));\n            friend.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(fileData),\n                    fileData.length, network, crypto, () -> false, x -> {}).join();\n            friend.shareWriteAccessWith(PathUtil.get(friend.username, filename), Collections.singleton(username)).join();\n        }\n\n        long initialWithWriteSharingIn = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n        long withWriteSharingIn = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n//        Assert.assertTrue(withWriteSharingIn < 4_000);\n\n        int nFriendsLeaving = 5;\n        for (int i=0; i < nFriendsLeaving; i++) {\n            int index = random.nextInt(friends.size());\n            UserContext friend = friends.get(index);\n            friend.deleteAccount(otherUsers.get(index).right, UserTests::noMfa).join();\n            friends.remove(friend);\n        }\n\n        long afterLeaving = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n//        Assert.assertTrue(afterLeaving < 4_000);\n\n        // Time login after a GC\n        service.gc.collect(s -> Futures.of(true));\n        long afterGc = time(() -> ensureSignedUp(username, password, network.clear(), crypto)).right;\n        Assert.assertTrue(afterGc < 4_000);\n    }\n\n    @Test\n    public void readingSharedFiles() {\n        String username = generateUsername();\n        String password = \"password\";\n        Pair<UserContext, Long> initial = time(() -> ensureSignedUp(username, password, network, crypto));\n        UserContext us = initial.left;\n\n        int nFriends = 1;\n        List<Pair<String, String>> otherUsers = IntStream.range(0, nFriends)\n                .mapToObj(x -> new Pair<>(generateUsername(), password))\n                .collect(Collectors.toList());\n        List<UserContext> friends = otherUsers.stream()\n                .map(p -> ensureSignedUp(p.left, p.right, network, crypto))\n                .collect(Collectors.toList());\n\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(us), friends);\n\n        // Add a few files, shared read only with a random friend friend\n        int nFiles = 40;\n        for (int i=0; i < nFiles; i++) {\n            byte[] fileData = \"dataaaa\".getBytes();\n            String filename = \"File\" + i;\n            us.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(fileData),\n                    fileData.length, network, crypto, () -> false, x -> {}).join();\n            String sharee = otherUsers.get(random.nextInt(otherUsers.size())).left;\n            us.shareReadAccessWith(PathUtil.get(username, filename), Collections.singleton(sharee)).join();\n        }\n\n        int reps = 100;\n        long start = System.currentTimeMillis();\n        for (int j=0; j < reps; j++) {\n            for (int i = 0; i < nFiles; i++) {\n                String name = \"File\" + i;\n                System.out.println(\"getByPath took \" + time(() -> friends.get(0).getByPath(PathUtil.get(username, name)).join()).right);\n            }\n        }\n        long end = System.currentTimeMillis();\n        System.out.println(\"Took \" + (end - start) + \"mS\");\n    }\n\n    @Test\n    public void groupSharing() {\n        String username = generateUsername();\n        String password = \"password\";\n        Pair<UserContext, Long> initial = time(() -> ensureSignedUp(username, password, network, crypto));\n        UserContext us = initial.left;\n\n        int nFriends = 20;\n        List<Pair<String, String>> otherUsers = IntStream.range(0, nFriends)\n                .mapToObj(x -> new Pair<>(generateUsername(), password))\n                .collect(Collectors.toList());\n        List<UserContext> friends = otherUsers.stream()\n                .map(p -> ensureSignedUp(p.left, p.right, network, crypto))\n                .collect(Collectors.toList());\n\n        PeergosNetworkUtils.friendBetweenGroups(Arrays.asList(us), friends);\n\n        // Share a file with all friends individually\\\n        String filename = \"File1\";\n        byte[] fileData = \"dataaaa\".getBytes();\n        us.getUserRoot().join().uploadOrReplaceFile(filename, AsyncReader.build(fileData),\n                fileData.length, network, crypto, () -> false, x -> {}).join();\n        long t0 = System.currentTimeMillis();\n        for (Pair<String, String> sharee : otherUsers) {\n            us.shareReadAccessWith(PathUtil.get(username, filename), Collections.singleton(sharee.left)).join();\n        }\n        long t1 = System.currentTimeMillis();\n\n        // Share a file with all friend via a group\n        String file2name = \"File2\";\n        String friendsGroup = us.getSocialState().join().getFriendsGroupUid();\n        us.getUserRoot().join().uploadOrReplaceFile(file2name, AsyncReader.build(fileData),\n                fileData.length, network, crypto, () -> false, x -> {}).join();\n        long groupShareDuration = time(() -> us.shareReadAccessWith(PathUtil.get(username, file2name), Collections.singleton(friendsGroup)).join()).right;\n        double ratio = (double) (t1 - t0) / groupShareDuration;\n        Assert.assertTrue(ratio > nFriends - 1);\n    }\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    public static <V> Pair<V, Long> time(Supplier<V> work) {\n        long t0 = System.currentTimeMillis();\n        V res = work.get();\n        long t1 = System.currentTimeMillis();\n        return new Pair<>(res, t1 - t0);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/slow/SocialfeedBenchmark.java",
    "content": "package peergos.server.tests.slow;\n\nimport org.junit.*;\nimport org.junit.runner.*;\nimport org.junit.runners.*;\nimport peergos.server.*;\nimport peergos.server.storage.*;\nimport peergos.server.tests.*;\nimport peergos.server.util.*;\nimport peergos.shared.*;\nimport peergos.shared.display.*;\nimport peergos.shared.social.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n@RunWith(Parameterized.class)\npublic class SocialfeedBenchmark {\n\n    private static int RANDOM_SEED = 666;\n    private final UserService service;\n    private final NetworkAccess network;\n    private final Crypto crypto = Main.initCrypto();\n\n    private static Random random = new Random(RANDOM_SEED);\n\n    public SocialfeedBenchmark(String useIPFS, Random r) throws Exception {\n        Pair<UserService, NetworkAccess> pair = buildHttpNetworkAccess(useIPFS.equals(\"IPFS\"), r);\n        this.service = pair.left;\n        this.network = pair.right;\n    }\n\n    private static Pair<UserService, NetworkAccess> buildHttpNetworkAccess(boolean useIpfs, Random r) throws Exception {\n        Args args = UserTests.buildArgs().with(\"useIPFS\", \"\" + useIpfs);\n        UserService service = Main.PKI_INIT.main(args).localApi;\n        NetworkAccess net = Builder.buildJavaNetworkAccess(new URL(\"http://localhost:\" + args.getInt(\"port\")), false, Optional.empty(), Optional.empty()).join();\n        int delayMillis = 50;\n        NetworkAccess delayed = net.withStorage(s -> new DelayingStorage(s, delayMillis, delayMillis));\n        return new Pair<>(service, delayed);\n    }\n\n    @Parameterized.Parameters()\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(new Object[][] {\n//                {\"IPFS\", new Random(0)}\n                {\"NOTIPFS\", new Random(0)}\n        });\n    }\n\n    // UpdateFeed(0) duration: 148704 mS, best: 148704 mS, worst: 148704 mS, av: 355258 mS\n    // UpdateFeed(1) duration: 60547 mS, best: 60547 mS, worst: 148704 mS, av: 222145 mS => 55s\n    // UpdateFeed(3) duration: 62495 mS, best: 60456 mS, worst: 148704 mS, av: 156035 mS => 57s\n    // UpdateFeed(10) duration: 69855 mS, best: 62662 mS, worst: 156153 mS, av: 125725 mS\n    // UpdateFeed(19) duration: 74355 mS, best: 62662 mS, worst: 156153 mS, av: 118671 mS\n    @Test\n    public void social() {\n        String password = \"test01\";\n        List<Pair<String, String>> logins = IntStream.range(0, 20)\n                .mapToObj(i -> new Pair<>(generateUsername(), password))\n                .collect(Collectors.toList());\n        List<UserContext> users = logins.stream()\n                .map(p -> ensureSignedUp(p.left, p.right, network.clear(), crypto))\n                .collect(Collectors.toList());\n\n        UserContext user = users.get(0);\n        List<UserContext> friends = users.stream().skip(1).collect(Collectors.toList());\n        PeergosNetworkUtils.friendBetweenGroups(List.of(user), friends);\n\n        long worst = 0, best = Long.MAX_VALUE, start = System.currentTimeMillis();\n\n        for (int i = 0; i < 4; i++) {\n            // send 1 post from each friend\n            friends.stream().forEach(f -> f.getSocialFeed().join()\n                    .createNewPost(SocialPost.createInitialPost(f.username, List.of(new Text(\"Msg \" + System.currentTimeMillis())), SocialPost.Resharing.Friends)).join());\n            long t1 = System.currentTimeMillis();\n\n            user.getSocialFeed().join().update().join();\n\n            long duration = System.currentTimeMillis() - t1;\n            worst = Math.max(worst, duration);\n            best = Math.min(best, duration);\n            System.err.printf(\"UpdateFeed(%d) duration: %d mS, best: %d mS, worst: %d mS, av: %d mS\\n\", i,\n                    duration, best, worst, (t1 + duration - start) / (i + 1));\n        }\n    }\n\n\n\n    private String generateUsername() {\n        return \"test\" + (random.nextInt() % 10000);\n    }\n\n    public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) {\n        return PeergosNetworkUtils.ensureSignedUp(username, password, network, crypto);\n    }\n\n    public static <V> Pair<V, Long> time(Supplier<V> work) {\n        long t0 = System.currentTimeMillis();\n        V res = work.get();\n        long t1 = System.currentTimeMillis();\n        return new Pair<>(res, t1 - t0);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/util/HashTreeTests.java",
    "content": "package peergos.server.tests.util;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport peergos.server.Main;\nimport peergos.server.sync.FileState;\nimport peergos.shared.Crypto;\nimport peergos.shared.user.fs.AsyncReader;\nimport peergos.shared.user.fs.Chunk;\nimport peergos.shared.user.fs.HashTree;\nimport peergos.shared.util.Pair;\n\nimport java.util.List;\nimport java.util.Random;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\npublic class HashTreeTests {\n    private static Crypto crypto = Main.initCrypto();\n\n    @Test\n    public void chunks1K() {\n        List<byte[]> chunkHashes = IntStream.range(0, 1024).mapToObj(i -> new byte[32]).collect(Collectors.toList());\n        HashTree tree = HashTree.build(chunkHashes, crypto.hasher).join();\n        Assert.assertTrue(tree.level1.size() == 1);\n        Assert.assertTrue(tree.level2.size() == 0);\n        Assert.assertTrue(tree.level3.size() == 0);\n        Assert.assertTrue(tree.level1.get(0).chunkHashes.length == 1024*32);\n    }\n\n    @Test\n    public void chunks2K() {\n        List<byte[]> chunkHashes = IntStream.range(0, 2*1024).mapToObj(i -> new byte[32]).collect(Collectors.toList());\n        HashTree tree = HashTree.build(chunkHashes, crypto.hasher).join();\n        Assert.assertTrue(tree.level1.size() == 2);\n        Assert.assertTrue(tree.level2.size() == 1);\n        Assert.assertTrue(tree.level3.size() == 0);\n        Assert.assertTrue(tree.level1.get(0).chunkHashes.length == 1024*32);\n    }\n\n    @Test\n    public void chunks1M() {\n        List<byte[]> chunkHashes = IntStream.range(0, 1024*1024).mapToObj(i -> new byte[32]).collect(Collectors.toList());\n        HashTree tree = HashTree.build(chunkHashes, crypto.hasher).join();\n        Assert.assertTrue(tree.level1.size() == 1024);\n        Assert.assertTrue(tree.level2.size() == 1);\n        Assert.assertTrue(tree.level3.size() == 0);\n        Assert.assertTrue(tree.level1.get(0).chunkHashes.length == 1024*32);\n    }\n\n    @Test\n    public void chunks2M() { // A 2 TiB file\n        List<byte[]> chunkHashes = IntStream.range(0, 2*1024*1024).mapToObj(i -> new byte[32]).collect(Collectors.toList());\n        HashTree tree = HashTree.build(chunkHashes, crypto.hasher).join();\n        Assert.assertTrue(tree.level1.size() == 2*1024);\n        Assert.assertTrue(tree.level2.size() == 2);\n        Assert.assertTrue(tree.level3.size() == 1);\n        Assert.assertTrue(tree.level1.get(0).chunkHashes.length == 1024*32);\n    }\n\n    @Test\n    public void chunks7M() { // A 7 TiB file\n        List<byte[]> chunkHashes = IntStream.range(0, 7*1024*1024).mapToObj(i -> new byte[32]).collect(Collectors.toList());\n        HashTree tree = HashTree.build(chunkHashes, crypto.hasher).join();\n        Assert.assertTrue(tree.level1.size() == 7*1024);\n        Assert.assertTrue(tree.level2.size() == 7);\n        Assert.assertTrue(tree.level3.size() == 1);\n        Assert.assertTrue(tree.level1.get(0).chunkHashes.length == 1024*32);\n    }\n\n    @Test\n    public void diff7M() { // A 7 TiB file\n        int nChunks = 7 * 1024 * 1024;\n        List<byte[]> chunkHashes = IntStream.range(0, nChunks).mapToObj(i -> new byte[32]).collect(Collectors.toList());\n        HashTree tree = HashTree.build(chunkHashes, crypto.hasher).join();\n\n        int diffChunk = 124667;\n        chunkHashes.get(diffChunk)[0] = 5;\n        HashTree tree2 = HashTree.build(chunkHashes, crypto.hasher).join();\n\n        long fileSize = ((long)nChunks) * Chunk.MAX_SIZE;\n        FileState updated = new FileState(\"\", 0, fileSize, tree);\n        FileState old = new FileState(\"\", 0, fileSize, tree2);\n        List<Pair<Long, Long>> diff = updated.diffRanges(old);\n        Assert.assertTrue(diff.size() == 1);\n        Assert.assertTrue(diff.get(0).equals(new Pair<>(diffChunk * (long)Chunk.MAX_SIZE, (diffChunk + 1)* (long)Chunk.MAX_SIZE)));\n    }\n\n    @Test\n    public void JsHashes() {\n        Random rnd = new Random(42);\n        for (int s: List.of(0, 1024,\n                5*1024*1024, 6*1024*1024,\n                10*1024*1024, 11*1024*1024,\n                40*1024*1024, 41*1024*1024,\n                80*1024*1024, 81*1024*1024,\n                150*1024*1024, 161*1024*1024\n                )) {\n            byte[] data = new byte[s];\n            rnd.nextBytes(data);\n            AsyncReader reader = AsyncReader.build(data);\n            HashTree serial = HashTree.build(reader, 0, data.length, crypto.hasher).join();\n            HashTree parallel = HashTree.buildParallel(i -> AsyncReader.build(data), 0, data.length, crypto.hasher, 8).join();\n            Assert.assertEquals(serial, parallel);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/util/NonClosingTransactionService.java",
    "content": "package peergos.server.tests.util;\n\nimport peergos.shared.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\n\nimport java.util.concurrent.*;\n\npublic class NonClosingTransactionService extends TransactionServiceImpl {\n\n    public NonClosingTransactionService(NetworkAccess network,\n                                        Crypto crypto,\n                                        FileWrapper transactionsDir) {\n        super(network, crypto, transactionsDir);\n    }\n\n    @Override\n    public CompletableFuture<Snapshot> close(Snapshot version, Committer committer, Transaction transaction) {\n        return Futures.of(version);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/tests/util/TestPorts.java",
    "content": "package peergos.server.tests.util;\n\nimport java.util.*;\nimport java.util.concurrent.atomic.*;\n\npublic class TestPorts {\n\n    private static final AtomicInteger port = new AtomicInteger(new Random().nextInt(50_000));\n    public static int getPort() {\n        return 9050 + (port.incrementAndGet() % 50_000);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/user/JavaImageThumbnailer.java",
    "content": "package peergos.server.user;\n\nimport peergos.server.util.Logging;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.user.fs.*;\n\nimport javax.imageio.*;\nimport java.awt.*;\nimport java.awt.image.*;\nimport java.io.*;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.logging.*;\n\npublic class JavaImageThumbnailer implements ThumbnailGenerator.Generator {\n    private final static int THUMBNAIL_SIZE = 400;\n\n    public Optional<Thumbnail> generateThumbnail(byte[] imageBlob) {\n        try {\n            BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBlob));\n            if (image == null) // e.g. svg files\n                return Optional.empty();\n            int size = THUMBNAIL_SIZE;\n            int height = image.getHeight();\n            int width = image.getWidth();\n            boolean tall = height > width;\n            int canvasWidth = tall ? size : width*size/height;\n            int canvasHeight = tall ? height*size/width : size;\n            BufferedImage thumbnailImage = new BufferedImage(THUMBNAIL_SIZE, THUMBNAIL_SIZE, image.getType());\n            Graphics2D g = thumbnailImage.createGraphics();\n            g.setComposite(AlphaComposite.Src);\n            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);\n            g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);\n            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\n            int x = tall ? 0 : -(canvasWidth - THUMBNAIL_SIZE) / 2;\n            int y = tall ? -(canvasHeight - THUMBNAIL_SIZE) / 2 : 0;\n            g.drawImage(image, x, y, canvasWidth, canvasHeight, null);\n            g.dispose();\n\n            // try webp first\n            try {\n                ByteArrayOutputStream webp = new ByteArrayOutputStream();\n                ImageIO.write(thumbnailImage, \"webp\", webp);\n                webp.close();\n                if (webp.size() > 0)\n                    return Optional.of(new Thumbnail(\"image/webp\", webp.toByteArray()));\n            } catch (Throwable t) {\n                // webp library doesn't support all OS+ARCH combos\n                Logging.LOG().log(Level.WARNING, t.getMessage(), t);\n            }\n\n            // try jpeg\n            try {\n                ByteArrayOutputStream jpg = new ByteArrayOutputStream();\n                ImageIO.write(thumbnailImage, \"JPG\", jpg);\n                jpg.close();\n                if (jpg.size() > 0)\n                    return Optional.of(new Thumbnail(\"image/jpeg\", jpg.toByteArray()));\n            } catch (Exception e) {}\n\n            // try png\n            ByteArrayOutputStream png = new ByteArrayOutputStream();\n            ImageIO.write(thumbnailImage, \"png\", png);\n            png.close();\n            return Optional.of(new Thumbnail(\"image/png\", png.toByteArray()));\n        } catch (Throwable t) {\n            Logging.LOG().log(Level.WARNING, t.getMessage(), t);\n        }\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/AddressUtil.java",
    "content": "package peergos.server.util;\n\nimport peergos.shared.io.ipfs.MultiAddress;\n\nimport java.net.*;\n\npublic class AddressUtil {\n    public static URL getLocalAddress(int port) {\n        try {\n            return new URI(\"http://localhost:\" + port).toURL();\n        } catch (URISyntaxException | MalformedURLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static int getListenPort(String uri) {\n        try {\n            URL url = new URI(uri).toURL();\n            return url.getPort();\n        } catch (URISyntaxException | MalformedURLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static URL getAddress(MultiAddress addr) {\n        try {\n            return new URI(\"http://\" + addr.getHost() + \":\" + addr.getTCPPort()).toURL();\n        } catch (URISyntaxException | MalformedURLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/AlphanumComparator.java",
    "content": "package peergos.server.util;\n\n/*\n * The Alphanum Algorithm is an improved sorting algorithm for strings\n * containing numbers.  Instead of sorting numbers in ASCII order like\n * a standard sort, this algorithm sorts numbers in numeric order.\n *\n * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com\n *\n * Released under the MIT License - https://opensource.org/licenses/MIT\n *\n * Copyright 2007-2017 David Koelle\n *\n * Permission is hereby granted, free of charge, to any person obtaining\n * a copy of this software and associated documentation files (the \"Software\"),\n * to deal in the Software without restriction, including without limitation\n * the rights to use, copy, modify, merge, publish, distribute, sublicense,\n * and/or sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included\n * in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n * USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport java.util.Comparator;\n\n/**\n * This is an updated version with enhancements made by Daniel Migowski,\n * Andre Bogus, and David Koelle. Updated by David Koelle in 2017.\n *\n * To use this class:\n *   Use the static \"sort\" method from the java.util.Collections class:\n *   Collections.sort(your list, new AlphanumComparator());\n */\npublic class AlphanumComparator implements Comparator<String>\n{\n    private final boolean isDigit(char ch)\n    {\n        return ((ch >= 48) && (ch <= 57));\n    }\n\n    /** Length of string is passed in for improved efficiency (only need to calculate it once) **/\n    private final String getChunk(String s, int slength, int marker)\n    {\n        StringBuilder chunk = new StringBuilder();\n        char c = s.charAt(marker);\n        chunk.append(c);\n        marker++;\n        if (isDigit(c))\n        {\n            while (marker < slength)\n            {\n                c = s.charAt(marker);\n                if (!isDigit(c))\n                    break;\n                chunk.append(c);\n                marker++;\n            }\n        } else\n        {\n            while (marker < slength)\n            {\n                c = s.charAt(marker);\n                if (isDigit(c))\n                    break;\n                chunk.append(c);\n                marker++;\n            }\n        }\n        return chunk.toString();\n    }\n\n    public int compare(String s1, String s2)\n    {\n    \tif ((s1 == null) || (s2 == null))\n    \t{\n    \t\treturn 0;\n    \t}\n\n        int thisMarker = 0;\n        int thatMarker = 0;\n        int s1Length = s1.length();\n        int s2Length = s2.length();\n\n        while (thisMarker < s1Length && thatMarker < s2Length)\n        {\n            String thisChunk = getChunk(s1, s1Length, thisMarker);\n            thisMarker += thisChunk.length();\n\n            String thatChunk = getChunk(s2, s2Length, thatMarker);\n            thatMarker += thatChunk.length();\n\n            // If both chunks contain numeric characters, sort them numerically\n            int result = 0;\n            if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0)))\n            {\n                // Simple chunk comparison by length.\n                int thisChunkLength = thisChunk.length();\n                result = thisChunkLength - thatChunk.length();\n                // If equal, the first different number counts\n                if (result == 0)\n                {\n                    for (int i = 0; i < thisChunkLength; i++)\n                    {\n                        result = thisChunk.charAt(i) - thatChunk.charAt(i);\n                        if (result != 0)\n                        {\n                            return result;\n                        }\n                    }\n                }\n            }\n            else\n            {\n                result = thisChunk.compareTo(thatChunk);\n            }\n\n            if (result != 0)\n                return result;\n        }\n\n        return s1Length - s2Length;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/Args.java",
    "content": "package peergos.server.util;\n\nimport peergos.server.*;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.util.*;\n\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class Args {\n    private static final String CONFIG_FILENAME = \"config\";\n\n    private final List<String> commands;\n    private final Map<String, String> params, envOnly;//insertion order\n\n    public Args(List<String> commands, Map<String, String> params, Map<String, String> envOnly) {\n        this.commands = commands;\n        this.params = params;\n        this.envOnly = envOnly;\n    }\n\n    public List<String> commands() {\n        return new ArrayList<>(commands);\n    }\n\n    public List<String> getAllArgs() {\n        Map<String, String> env = System.getenv();\n        return params.entrySet().stream()\n                .filter(e -> ! env.containsKey(e.getKey()))\n                .flatMap(e -> Stream.of(\"-\" + e.getKey(), e.getValue()))\n                .collect(Collectors.toList());\n    }\n\n    public String getArg(String param, String def) {\n        if (!params.containsKey(param))\n            return def;\n        return params.get(param);\n    }\n\n    public String getArg(String param) {\n        if (!params.containsKey(param))\n            throw new IllegalStateException(\"No parameter: \" + param);\n        return params.get(param);\n    }\n\n    public Optional<String> getOptionalArg(String param) {\n        return Optional.ofNullable(params.get(param));\n    }\n\n    public Args setArg(String param, String value) {\n        Map<String, String> newParams = paramMap();\n        newParams.putAll(params);\n        newParams.put(param, value);\n        return new Args(commands, newParams, envOnly);\n    }\n\n    public Args setParameter(String param) {\n        return setArg(param, \"true\");\n    }\n\n    public Args removeArg(String param) {\n        Map<String, String> newParams = paramMap();\n        newParams.putAll(params);\n        newParams.remove(param);\n        return new Args(commands, newParams, envOnly);\n    }\n\n    public boolean hasArg(String arg) {\n        return params.containsKey(arg);\n    }\n\n    public boolean getBoolean(String param, boolean def) {\n        if (!params.containsKey(param))\n            return def;\n        return \"true\".equals(params.get(param));\n    }\n\n    public boolean getBoolean(String param) {\n        if (!params.containsKey(param))\n            throw new IllegalStateException(\"Missing parameter for \" + param);\n        return \"true\".equals(params.get(param));\n    }\n\n    public int getInt(String param, int def) {\n        if (!params.containsKey(param))\n            return def;\n        return Integer.parseInt(params.get(param));\n    }\n\n    public int getInt(String param) {\n        if (!params.containsKey(param))\n            throw new IllegalStateException(\"No parameter: \" + param);\n        return Integer.parseInt(params.get(param));\n    }\n\n    public long getLong(String param) {\n        if (!params.containsKey(param))\n            throw new IllegalStateException(\"No parameter: \" + param);\n        return Long.parseLong(params.get(param));\n    }\n\n    public long getLong(String param, long def) {\n        if (!params.containsKey(param))\n            return def;\n        return Long.parseLong(params.get(param));\n    }\n\n    public double getDouble(String param) {\n        if (!params.containsKey(param))\n            throw new IllegalStateException(\"No parameter: \" + param);\n        return Double.parseDouble(params.get(param));\n    }\n\n    public String getFirstArg(String[] paramNames, String def) {\n        for (int i = 0; i < paramNames.length; i++) {\n            String result = getArg(paramNames[i], null);\n            if (result != null)\n                return result;\n        }\n        return def;\n    }\n\n    public Args tail() {\n        return new Args(commands.subList(1, commands.size()), params, envOnly);\n    }\n\n    public Optional<String> head() {\n        return commands.stream()\n                .findFirst();\n    }\n\n    public Args setIfAbsent(String key, String value) {\n        if (params.containsKey(key))\n            return this;\n        return setArg(key, value);\n    }\n\n    public Args with(String key, String value) {\n        Map<String, String> map = paramMap();\n        map.putAll(params);\n        map.put(key, value);\n        return new Args(commands, map, envOnly);\n    }\n\n    public Args with(Args overrides) {\n        Map<String, String> map = paramMap();\n        map.putAll(params);\n        map.putAll(overrides.params);\n        return new Args(commands, map, envOnly);\n    }\n\n    public Path fromPeergosDir(String fileName) {\n        return fromPeergosDir(fileName, null);\n    }\n\n    /**\n     * Get the path to a file-name in the PEERGOS_PATH\n     *\n     * @param fileName\n     * @param defaultName\n     * @return\n     */\n    public Path fromPeergosDir(String fileName, String defaultName) {\n        Path peergosDir = getPeergosDir();\n        String fName = defaultName == null ? getArg(fileName) : getArg(fileName, defaultName);\n        return peergosDir.resolve(fName);\n    }\n\n    public Path getPeergosDirChild(String filename) {\n        return getPeergosDir().resolve(filename);\n    }\n\n    public Path getPeergosDir() {\n        return hasArg(Main.PEERGOS_PATH) ? Paths.get(getArg(Main.PEERGOS_PATH)) : Main.DEFAULT_PEERGOS_DIR_PATH;\n    }\n\n    private static Map<String, String> parseEnv() {\n        Map<String, String> map = paramMap();\n        map.putAll(System.getenv());\n        return map;\n    }\n\n    private static Map<String, String> parseFile(Map<String, String> args, Map<String, String> env) {\n        Path toFile = (\n                args.containsKey(Main.PEERGOS_PATH) ?\n                        Paths.get(args.get(Main.PEERGOS_PATH)) :\n                        env.containsKey(Main.PEERGOS_PATH) ?\n                                Paths.get(env.get(Main.PEERGOS_PATH)) :\n                                Main.DEFAULT_PEERGOS_DIR_PATH\n        ).resolve(CONFIG_FILENAME);\n        return parseFile(toFile);\n    }\n\n    private static Map<String, String> parseJSONFile(Path path) {\n        try {\n            if (! path.toFile().exists())\n                return Collections.emptyMap();\n            String lines = new String(Files.readAllBytes(path));\n\n            return (Map)JSONParser.parse(lines);\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe.getMessage(), ioe);\n        }\n    }\n\n    private static Map<String, String> parseFile(Path path) {\n        try {\n            if (! path.toFile().exists())\n                return Collections.emptyMap();\n            List<String> lines = Files.readAllLines(path);\n\n            return lines.stream()\n                    .filter(line -> !line.isEmpty())\n                    .filter(line -> !line.matches(\"\\\\s+\"))\n                    .map(Args::parseLine)\n                    .filter(Optional::isPresent)\n                    .map(Optional::get)\n                    .collect(Collectors.toMap(\n                            e -> e.left,\n                            e -> e.right));\n\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe.getMessage(), ioe);\n        }\n    }\n\n    /** Save all parameters to a config file if it doesn't exist already\n     *\n     */\n    public void saveToFileIfAbsent() {\n        Path config = fromPeergosDir(CONFIG_FILENAME, CONFIG_FILENAME);\n        if (! config.toFile().exists())\n            saveToFile(config);\n    }\n\n    /** Save all parameters to a config file, excluding environment vars\n     *\n     */\n    public void saveToJSONFile() {\n        saveToJSONFile(fromPeergosDir(CONFIG_FILENAME, CONFIG_FILENAME));\n    }\n\n    private void saveToJSONFile(Path file) {\n        String text = JSONParser.toString(new TreeMap<>(params).entrySet().stream()\n                .filter(e -> ! envOnly.containsKey(e.getKey()))\n                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())));\n        try {\n            Files.write(file, text.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /** Save all parameters to a config file, excluding environment vars\n     *\n     */\n    public void saveToFile() {\n        saveToFile(fromPeergosDir(CONFIG_FILENAME, CONFIG_FILENAME));\n    }\n\n    private void saveToFile(Path file) {\n        String text = new TreeMap<>(params).entrySet().stream()\n                .filter(e -> ! envOnly.containsKey(e.getKey()))\n                .map(e -> e.getKey() + \" = \" + e.getValue())\n                .collect(Collectors.joining(\"\\n\"));\n        try {\n            Files.write(file, text.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static Map<String, String> parseParams(String[] args) {\n        Map<String, String> map = paramMap();\n        for (int i = 0; i < args.length; i++) {\n            String argName = args[i];\n            if (argName.startsWith(\"-\"))\n                argName = argName.substring(1);\n\n            if ((i == args.length - 1) || args[i + 1].startsWith(\"-\"))\n                map.put(argName, \"true\");\n            else\n                map.put(argName, args[++i]);\n        }\n        return map;\n    }\n\n    private static List<String> parseCommands(String[] args) {\n        List<String> commands = new ArrayList<>();\n        for (String arg: args)\n            if (! arg.startsWith(\"-\"))\n                commands.add(arg);\n            else break;\n        return commands;\n    }\n\n    /**\n     * params overrides configFile overrides env\n     *\n     * @param params\n     * @param configFile\n     * @param includeEnv\n     * @return\n     */\n    public static Args parse(String[] params, Optional<Path> configFile, boolean includeEnv) {\n        List<String> commands = parseCommands(params);\n        Map<String, String> fromEnv = includeEnv ? parseEnv() : Collections.emptyMap();\n        Map<String, String> fromParams = parseParams(params);\n        Map<String, String> fromFile = configFile.isPresent() ?\n                (configFile.get().toString().endsWith(\".json\") ?\n                        parseJSONFile(configFile.get()) :\n                        parseFile(configFile.get())) :\n                parseFile(fromParams, fromEnv);\n\n        Map<String, String> combined = paramMap();\n\n        Stream.of(\n                fromParams.entrySet(),\n                fromFile.entrySet(),\n                fromEnv.entrySet()\n        )\n                .flatMap(e -> e.stream())\n                .forEach(e -> combined.putIfAbsent(e.getKey(), e.getValue()));\n\n        return new Args(commands, combined, fromEnv);\n    }\n\n    public static Args parse(String[] args) {\n        return parse(args, Optional.empty(), true);\n    }\n\n    /**\n     * Parses a string with the schema \"\\\\s*\\\\w+\\\\s*=\\\\s*\\\\w*\\\\s*#?\\\\s*\"\n     * Omits full comments or partial comments\n     *\n     * @param originalLine\n     * @return\n     */\n    private static Optional<Pair<String, String>> parseLine(String originalLine) {\n        String line = originalLine.trim();\n\n        int commentPos = line.indexOf(\"#\");\n        // This line is a pure comment\n        if (commentPos == 0)\n            return Optional.empty();\n\n        // Enforce a space before # for a comment not at start of line\n        if (commentPos != -1 && line.charAt(commentPos - 1) == ' ')\n            // line ends with a comment\n            line = line.substring(0, commentPos).trim();\n\n        String[] split = line.split(\"=\");\n        if (split.length != 2)\n            throw new IllegalStateException(\"Illegal line '\" + line + \"'\");\n\n        String left = split[0].trim();\n        String right = split[1].trim();\n\n        return Optional.of(new Pair<>(left, right));\n    }\n\n    private static <K, V> Map<K, V> paramMap() {\n        return new LinkedHashMap<>(16, 0.75f, false);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/util/DifficultyGenerator.java",
    "content": "package peergos.server.util;\n\nimport peergos.shared.crypto.*;\n\n/** DifficultyGenerator is used to monitor a particular event and choose a difficulty level for a proof of work to\n *  rate limit it.\n *\n *  It is created with the desired maximum number of events per day to calibrate the difficulty.\n *\n *  The main assumption is that the proof of work scales exponentially with the difficulty.\n */\npublic class DifficultyGenerator {\n\n    private final RateMonitor queryRate;\n    private int difficulty = ProofOfWork.MIN_DIFFICULTY;\n    private long timeOfLastUpdateMillis;\n    private final double[] maxPerBucket;\n\n    public DifficultyGenerator(long startTimeMillis, int maxPerDay) {\n        this.timeOfLastUpdateMillis = startTimeMillis;\n        // This covers a days worth of queries if the time unit is 0.1 seconds (2^20 > 864000)\n        int nBuckets = 20;\n        this.queryRate = new RateMonitor(nBuckets);\n        double maxPerTimeStep = ((double)maxPerDay) / 864000;\n        this.maxPerBucket = new double[nBuckets];\n        for (int i=0; i < nBuckets; i++)\n            maxPerBucket[i] = maxPerTimeStep * timeStepsForBucket(i);\n        if (maxPerDay == 0)\n            difficulty = ProofOfWork.MAX_DIFFICULTY;\n    }\n\n    private static long timeStepsForBucket(int bucket) {\n        return (1L << (bucket + 1)) - (1L << bucket);\n    }\n\n    private static long millisToTimeSteps(long millis) {\n        return millis / 100; // assumes 0.1s time step\n    }\n\n    public synchronized int currentDifficulty() {\n        return difficulty;\n    }\n\n    public synchronized void updateTime(long epochMillis) {\n        // update rate monitor\n        long timeStepsSinceLastUpdate = millisToTimeSteps(epochMillis - timeOfLastUpdateMillis);\n        if (timeStepsSinceLastUpdate > 0) {\n            queryRate.timeSteps(timeStepsSinceLastUpdate);\n            timeOfLastUpdateMillis = epochMillis;\n        }\n        updateDifficulty();\n    }\n\n    public synchronized void addEvent() {\n        queryRate.addEvent();\n    }\n\n    private synchronized void updateDifficulty() {\n        difficulty = calculateDifficulty(queryRate.getRates(), maxPerBucket);\n    }\n\n    private static int calculateDifficulty(long[] rates, double[] maxPerBucket) {\n        double newDifficulty = 0.0;\n        int includedBuckets = 0;\n        for (int i=0; i < rates.length; i++) {\n            double maxForIndex = maxPerBucket[i];\n            if (rates[i] > maxForIndex) {\n                double log = Math.log((double) rates[i] * 2 / maxForIndex);\n                newDifficulty += log;\n                includedBuckets++;\n            }\n        }\n        if (includedBuckets == 0)\n            return ProofOfWork.MIN_DIFFICULTY;\n        int result = (int) (2 * newDifficulty + ProofOfWork.DEFAULT_DIFFICULTY);\n        return Math.min(Math.max(ProofOfWork.MIN_DIFFICULTY, result), ProofOfWork.MAX_DIFFICULTY);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/HtmlUtil.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.util;\n\nimport java.io.*;\nimport java.util.*;\n\npublic class HtmlUtil {\n\n    /**\n     * A Map&lt;CharSequence, CharSequence&gt; to escape the basic XML and HTML\n     * character entities.\n     *\n     * Namely: {@code \" & < >}\n     */\n    public static final Map<CharSequence, CharSequence> BASIC_ESCAPE;\n    static {\n        final Map<CharSequence, CharSequence> initialMap = new HashMap<>();\n        initialMap.put(\"\\\"\", \"&quot;\"); // \" - double-quote\n        initialMap.put(\"&\", \"&amp;\");   // & - ampersand\n        initialMap.put(\"<\", \"&lt;\");    // < - less-than\n        initialMap.put(\">\", \"&gt;\");    // > - greater-than\n        BASIC_ESCAPE = Collections.unmodifiableMap(initialMap);\n    }\n\n    /**\n     * A Map&lt;CharSequence, CharSequence&gt; to to escape\n     * <a href=\"https://secure.wikimedia.org/wikipedia/en/wiki/ISO/IEC_8859-1\">ISO-8859-1</a>\n     * characters to their named HTML 3.x equivalents.\n     */\n    public static final Map<CharSequence, CharSequence> ISO8859_1_ESCAPE;\n    static {\n        final Map<CharSequence, CharSequence> initialMap = new HashMap<>();\n        initialMap.put(\"\\u00A0\", \"&nbsp;\"); // non-breaking space\n        initialMap.put(\"\\u00A1\", \"&iexcl;\"); // inverted exclamation mark\n        initialMap.put(\"\\u00A2\", \"&cent;\"); // cent sign\n        initialMap.put(\"\\u00A3\", \"&pound;\"); // pound sign\n        initialMap.put(\"\\u00A4\", \"&curren;\"); // currency sign\n        initialMap.put(\"\\u00A5\", \"&yen;\"); // yen sign = yuan sign\n        initialMap.put(\"\\u00A6\", \"&brvbar;\"); // broken bar = broken vertical bar\n        initialMap.put(\"\\u00A7\", \"&sect;\"); // section sign\n        initialMap.put(\"\\u00A8\", \"&uml;\"); // diaeresis = spacing diaeresis\n        initialMap.put(\"\\u00A9\", \"&copy;\"); // © - copyright sign\n        initialMap.put(\"\\u00AA\", \"&ordf;\"); // feminine ordinal indicator\n        initialMap.put(\"\\u00AB\", \"&laquo;\"); // left-pointing double angle quotation mark = left pointing guillemet\n        initialMap.put(\"\\u00AC\", \"&not;\"); // not sign\n        initialMap.put(\"\\u00AD\", \"&shy;\"); // soft hyphen = discretionary hyphen\n        initialMap.put(\"\\u00AE\", \"&reg;\"); // ® - registered trademark sign\n        initialMap.put(\"\\u00AF\", \"&macr;\"); // macron = spacing macron = overline = APL overbar\n        initialMap.put(\"\\u00B0\", \"&deg;\"); // degree sign\n        initialMap.put(\"\\u00B1\", \"&plusmn;\"); // plus-minus sign = plus-or-minus sign\n        initialMap.put(\"\\u00B2\", \"&sup2;\"); // superscript two = superscript digit two = squared\n        initialMap.put(\"\\u00B3\", \"&sup3;\"); // superscript three = superscript digit three = cubed\n        initialMap.put(\"\\u00B4\", \"&acute;\"); // acute accent = spacing acute\n        initialMap.put(\"\\u00B5\", \"&micro;\"); // micro sign\n        initialMap.put(\"\\u00B6\", \"&para;\"); // pilcrow sign = paragraph sign\n        initialMap.put(\"\\u00B7\", \"&middot;\"); // middle dot = Georgian comma = Greek middle dot\n        initialMap.put(\"\\u00B8\", \"&cedil;\"); // cedilla = spacing cedilla\n        initialMap.put(\"\\u00B9\", \"&sup1;\"); // superscript one = superscript digit one\n        initialMap.put(\"\\u00BA\", \"&ordm;\"); // masculine ordinal indicator\n        initialMap.put(\"\\u00BB\", \"&raquo;\"); // right-pointing double angle quotation mark = right pointing guillemet\n        initialMap.put(\"\\u00BC\", \"&frac14;\"); // vulgar fraction one quarter = fraction one quarter\n        initialMap.put(\"\\u00BD\", \"&frac12;\"); // vulgar fraction one half = fraction one half\n        initialMap.put(\"\\u00BE\", \"&frac34;\"); // vulgar fraction three quarters = fraction three quarters\n        initialMap.put(\"\\u00BF\", \"&iquest;\"); // inverted question mark = turned question mark\n        initialMap.put(\"\\u00C0\", \"&Agrave;\"); // À - uppercase A, grave accent\n        initialMap.put(\"\\u00C1\", \"&Aacute;\"); // Á - uppercase A, acute accent\n        initialMap.put(\"\\u00C2\", \"&Acirc;\"); // Â - uppercase A, circumflex accent\n        initialMap.put(\"\\u00C3\", \"&Atilde;\"); // Ã - uppercase A, tilde\n        initialMap.put(\"\\u00C4\", \"&Auml;\"); // Ä - uppercase A, umlaut\n        initialMap.put(\"\\u00C5\", \"&Aring;\"); // Å - uppercase A, ring\n        initialMap.put(\"\\u00C6\", \"&AElig;\"); // Æ - uppercase AE\n        initialMap.put(\"\\u00C7\", \"&Ccedil;\"); // Ç - uppercase C, cedilla\n        initialMap.put(\"\\u00C8\", \"&Egrave;\"); // È - uppercase E, grave accent\n        initialMap.put(\"\\u00C9\", \"&Eacute;\"); // É - uppercase E, acute accent\n        initialMap.put(\"\\u00CA\", \"&Ecirc;\"); // Ê - uppercase E, circumflex accent\n        initialMap.put(\"\\u00CB\", \"&Euml;\"); // Ë - uppercase E, umlaut\n        initialMap.put(\"\\u00CC\", \"&Igrave;\"); // Ì - uppercase I, grave accent\n        initialMap.put(\"\\u00CD\", \"&Iacute;\"); // Í - uppercase I, acute accent\n        initialMap.put(\"\\u00CE\", \"&Icirc;\"); // Î - uppercase I, circumflex accent\n        initialMap.put(\"\\u00CF\", \"&Iuml;\"); // Ï - uppercase I, umlaut\n        initialMap.put(\"\\u00D0\", \"&ETH;\"); // Ð - uppercase Eth, Icelandic\n        initialMap.put(\"\\u00D1\", \"&Ntilde;\"); // Ñ - uppercase N, tilde\n        initialMap.put(\"\\u00D2\", \"&Ograve;\"); // Ò - uppercase O, grave accent\n        initialMap.put(\"\\u00D3\", \"&Oacute;\"); // Ó - uppercase O, acute accent\n        initialMap.put(\"\\u00D4\", \"&Ocirc;\"); // Ô - uppercase O, circumflex accent\n        initialMap.put(\"\\u00D5\", \"&Otilde;\"); // Õ - uppercase O, tilde\n        initialMap.put(\"\\u00D6\", \"&Ouml;\"); // Ö - uppercase O, umlaut\n        initialMap.put(\"\\u00D7\", \"&times;\"); // multiplication sign\n        initialMap.put(\"\\u00D8\", \"&Oslash;\"); // Ø - uppercase O, slash\n        initialMap.put(\"\\u00D9\", \"&Ugrave;\"); // Ù - uppercase U, grave accent\n        initialMap.put(\"\\u00DA\", \"&Uacute;\"); // Ú - uppercase U, acute accent\n        initialMap.put(\"\\u00DB\", \"&Ucirc;\"); // Û - uppercase U, circumflex accent\n        initialMap.put(\"\\u00DC\", \"&Uuml;\"); // Ü - uppercase U, umlaut\n        initialMap.put(\"\\u00DD\", \"&Yacute;\"); // Ý - uppercase Y, acute accent\n        initialMap.put(\"\\u00DE\", \"&THORN;\"); // Þ - uppercase THORN, Icelandic\n        initialMap.put(\"\\u00DF\", \"&szlig;\"); // ß - lowercase sharps, German\n        initialMap.put(\"\\u00E0\", \"&agrave;\"); // à - lowercase a, grave accent\n        initialMap.put(\"\\u00E1\", \"&aacute;\"); // á - lowercase a, acute accent\n        initialMap.put(\"\\u00E2\", \"&acirc;\"); // â - lowercase a, circumflex accent\n        initialMap.put(\"\\u00E3\", \"&atilde;\"); // ã - lowercase a, tilde\n        initialMap.put(\"\\u00E4\", \"&auml;\"); // ä - lowercase a, umlaut\n        initialMap.put(\"\\u00E5\", \"&aring;\"); // å - lowercase a, ring\n        initialMap.put(\"\\u00E6\", \"&aelig;\"); // æ - lowercase ae\n        initialMap.put(\"\\u00E7\", \"&ccedil;\"); // ç - lowercase c, cedilla\n        initialMap.put(\"\\u00E8\", \"&egrave;\"); // è - lowercase e, grave accent\n        initialMap.put(\"\\u00E9\", \"&eacute;\"); // é - lowercase e, acute accent\n        initialMap.put(\"\\u00EA\", \"&ecirc;\"); // ê - lowercase e, circumflex accent\n        initialMap.put(\"\\u00EB\", \"&euml;\"); // ë - lowercase e, umlaut\n        initialMap.put(\"\\u00EC\", \"&igrave;\"); // ì - lowercase i, grave accent\n        initialMap.put(\"\\u00ED\", \"&iacute;\"); // í - lowercase i, acute accent\n        initialMap.put(\"\\u00EE\", \"&icirc;\"); // î - lowercase i, circumflex accent\n        initialMap.put(\"\\u00EF\", \"&iuml;\"); // ï - lowercase i, umlaut\n        initialMap.put(\"\\u00F0\", \"&eth;\"); // ð - lowercase eth, Icelandic\n        initialMap.put(\"\\u00F1\", \"&ntilde;\"); // ñ - lowercase n, tilde\n        initialMap.put(\"\\u00F2\", \"&ograve;\"); // ò - lowercase o, grave accent\n        initialMap.put(\"\\u00F3\", \"&oacute;\"); // ó - lowercase o, acute accent\n        initialMap.put(\"\\u00F4\", \"&ocirc;\"); // ô - lowercase o, circumflex accent\n        initialMap.put(\"\\u00F5\", \"&otilde;\"); // õ - lowercase o, tilde\n        initialMap.put(\"\\u00F6\", \"&ouml;\"); // ö - lowercase o, umlaut\n        initialMap.put(\"\\u00F7\", \"&divide;\"); // division sign\n        initialMap.put(\"\\u00F8\", \"&oslash;\"); // ø - lowercase o, slash\n        initialMap.put(\"\\u00F9\", \"&ugrave;\"); // ù - lowercase u, grave accent\n        initialMap.put(\"\\u00FA\", \"&uacute;\"); // ú - lowercase u, acute accent\n        initialMap.put(\"\\u00FB\", \"&ucirc;\"); // û - lowercase u, circumflex accent\n        initialMap.put(\"\\u00FC\", \"&uuml;\"); // ü - lowercase u, umlaut\n        initialMap.put(\"\\u00FD\", \"&yacute;\"); // ý - lowercase y, acute accent\n        initialMap.put(\"\\u00FE\", \"&thorn;\"); // þ - lowercase thorn, Icelandic\n        initialMap.put(\"\\u00FF\", \"&yuml;\"); // ÿ - lowercase y, umlaut\n        ISO8859_1_ESCAPE = Collections.unmodifiableMap(initialMap);\n    }\n\n    /**\n     * A Map&lt;CharSequence, CharSequence&gt; to escape additional\n     * <a href=\"http://www.w3.org/TR/REC-html40/sgml/entities.html\">character entity\n     * references</a>. Note that this must be used with {@link #ISO8859_1_ESCAPE} to get the full list of\n     * HTML 4.0 character entities.\n     */\n    public static final Map<CharSequence, CharSequence> HTML40_EXTENDED_ESCAPE;\n    static {\n        final Map<CharSequence, CharSequence> initialMap = new HashMap<>();\n        // <!-- Latin Extended-B -->\n        initialMap.put(\"\\u0192\", \"&fnof;\"); // latin small f with hook = function= florin, U+0192 ISOtech -->\n        // <!-- Greek -->\n        initialMap.put(\"\\u0391\", \"&Alpha;\"); // greek capital letter alpha, U+0391 -->\n        initialMap.put(\"\\u0392\", \"&Beta;\"); // greek capital letter beta, U+0392 -->\n        initialMap.put(\"\\u0393\", \"&Gamma;\"); // greek capital letter gamma,U+0393 ISOgrk3 -->\n        initialMap.put(\"\\u0394\", \"&Delta;\"); // greek capital letter delta,U+0394 ISOgrk3 -->\n        initialMap.put(\"\\u0395\", \"&Epsilon;\"); // greek capital letter epsilon, U+0395 -->\n        initialMap.put(\"\\u0396\", \"&Zeta;\"); // greek capital letter zeta, U+0396 -->\n        initialMap.put(\"\\u0397\", \"&Eta;\"); // greek capital letter eta, U+0397 -->\n        initialMap.put(\"\\u0398\", \"&Theta;\"); // greek capital letter theta,U+0398 ISOgrk3 -->\n        initialMap.put(\"\\u0399\", \"&Iota;\"); // greek capital letter iota, U+0399 -->\n        initialMap.put(\"\\u039A\", \"&Kappa;\"); // greek capital letter kappa, U+039A -->\n        initialMap.put(\"\\u039B\", \"&Lambda;\"); // greek capital letter lambda,U+039B ISOgrk3 -->\n        initialMap.put(\"\\u039C\", \"&Mu;\"); // greek capital letter mu, U+039C -->\n        initialMap.put(\"\\u039D\", \"&Nu;\"); // greek capital letter nu, U+039D -->\n        initialMap.put(\"\\u039E\", \"&Xi;\"); // greek capital letter xi, U+039E ISOgrk3 -->\n        initialMap.put(\"\\u039F\", \"&Omicron;\"); // greek capital letter omicron, U+039F -->\n        initialMap.put(\"\\u03A0\", \"&Pi;\"); // greek capital letter pi, U+03A0 ISOgrk3 -->\n        initialMap.put(\"\\u03A1\", \"&Rho;\"); // greek capital letter rho, U+03A1 -->\n        // <!-- there is no Sigmaf, and no U+03A2 character either -->\n        initialMap.put(\"\\u03A3\", \"&Sigma;\"); // greek capital letter sigma,U+03A3 ISOgrk3 -->\n        initialMap.put(\"\\u03A4\", \"&Tau;\"); // greek capital letter tau, U+03A4 -->\n        initialMap.put(\"\\u03A5\", \"&Upsilon;\"); // greek capital letter upsilon,U+03A5 ISOgrk3 -->\n        initialMap.put(\"\\u03A6\", \"&Phi;\"); // greek capital letter phi,U+03A6 ISOgrk3 -->\n        initialMap.put(\"\\u03A7\", \"&Chi;\"); // greek capital letter chi, U+03A7 -->\n        initialMap.put(\"\\u03A8\", \"&Psi;\"); // greek capital letter psi,U+03A8 ISOgrk3 -->\n        initialMap.put(\"\\u03A9\", \"&Omega;\"); // greek capital letter omega,U+03A9 ISOgrk3 -->\n        initialMap.put(\"\\u03B1\", \"&alpha;\"); // greek small letter alpha,U+03B1 ISOgrk3 -->\n        initialMap.put(\"\\u03B2\", \"&beta;\"); // greek small letter beta, U+03B2 ISOgrk3 -->\n        initialMap.put(\"\\u03B3\", \"&gamma;\"); // greek small letter gamma,U+03B3 ISOgrk3 -->\n        initialMap.put(\"\\u03B4\", \"&delta;\"); // greek small letter delta,U+03B4 ISOgrk3 -->\n        initialMap.put(\"\\u03B5\", \"&epsilon;\"); // greek small letter epsilon,U+03B5 ISOgrk3 -->\n        initialMap.put(\"\\u03B6\", \"&zeta;\"); // greek small letter zeta, U+03B6 ISOgrk3 -->\n        initialMap.put(\"\\u03B7\", \"&eta;\"); // greek small letter eta, U+03B7 ISOgrk3 -->\n        initialMap.put(\"\\u03B8\", \"&theta;\"); // greek small letter theta,U+03B8 ISOgrk3 -->\n        initialMap.put(\"\\u03B9\", \"&iota;\"); // greek small letter iota, U+03B9 ISOgrk3 -->\n        initialMap.put(\"\\u03BA\", \"&kappa;\"); // greek small letter kappa,U+03BA ISOgrk3 -->\n        initialMap.put(\"\\u03BB\", \"&lambda;\"); // greek small letter lambda,U+03BB ISOgrk3 -->\n        initialMap.put(\"\\u03BC\", \"&mu;\"); // greek small letter mu, U+03BC ISOgrk3 -->\n        initialMap.put(\"\\u03BD\", \"&nu;\"); // greek small letter nu, U+03BD ISOgrk3 -->\n        initialMap.put(\"\\u03BE\", \"&xi;\"); // greek small letter xi, U+03BE ISOgrk3 -->\n        initialMap.put(\"\\u03BF\", \"&omicron;\"); // greek small letter omicron, U+03BF NEW -->\n        initialMap.put(\"\\u03C0\", \"&pi;\"); // greek small letter pi, U+03C0 ISOgrk3 -->\n        initialMap.put(\"\\u03C1\", \"&rho;\"); // greek small letter rho, U+03C1 ISOgrk3 -->\n        initialMap.put(\"\\u03C2\", \"&sigmaf;\"); // greek small letter final sigma,U+03C2 ISOgrk3 -->\n        initialMap.put(\"\\u03C3\", \"&sigma;\"); // greek small letter sigma,U+03C3 ISOgrk3 -->\n        initialMap.put(\"\\u03C4\", \"&tau;\"); // greek small letter tau, U+03C4 ISOgrk3 -->\n        initialMap.put(\"\\u03C5\", \"&upsilon;\"); // greek small letter upsilon,U+03C5 ISOgrk3 -->\n        initialMap.put(\"\\u03C6\", \"&phi;\"); // greek small letter phi, U+03C6 ISOgrk3 -->\n        initialMap.put(\"\\u03C7\", \"&chi;\"); // greek small letter chi, U+03C7 ISOgrk3 -->\n        initialMap.put(\"\\u03C8\", \"&psi;\"); // greek small letter psi, U+03C8 ISOgrk3 -->\n        initialMap.put(\"\\u03C9\", \"&omega;\"); // greek small letter omega,U+03C9 ISOgrk3 -->\n        initialMap.put(\"\\u03D1\", \"&thetasym;\"); // greek small letter theta symbol,U+03D1 NEW -->\n        initialMap.put(\"\\u03D2\", \"&upsih;\"); // greek upsilon with hook symbol,U+03D2 NEW -->\n        initialMap.put(\"\\u03D6\", \"&piv;\"); // greek pi symbol, U+03D6 ISOgrk3 -->\n        // <!-- General Punctuation -->\n        initialMap.put(\"\\u2022\", \"&bull;\"); // bullet = black small circle,U+2022 ISOpub -->\n        // <!-- bullet is NOT the same as bullet operator, U+2219 -->\n        initialMap.put(\"\\u2026\", \"&hellip;\"); // horizontal ellipsis = three dot leader,U+2026 ISOpub -->\n        initialMap.put(\"\\u2032\", \"&prime;\"); // prime = minutes = feet, U+2032 ISOtech -->\n        initialMap.put(\"\\u2033\", \"&Prime;\"); // double prime = seconds = inches,U+2033 ISOtech -->\n        initialMap.put(\"\\u203E\", \"&oline;\"); // overline = spacing overscore,U+203E NEW -->\n        initialMap.put(\"\\u2044\", \"&frasl;\"); // fraction slash, U+2044 NEW -->\n        // <!-- Letterlike Symbols -->\n        initialMap.put(\"\\u2118\", \"&weierp;\"); // script capital P = power set= Weierstrass p, U+2118 ISOamso -->\n        initialMap.put(\"\\u2111\", \"&image;\"); // blackletter capital I = imaginary part,U+2111 ISOamso -->\n        initialMap.put(\"\\u211C\", \"&real;\"); // blackletter capital R = real part symbol,U+211C ISOamso -->\n        initialMap.put(\"\\u2122\", \"&trade;\"); // trade mark sign, U+2122 ISOnum -->\n        initialMap.put(\"\\u2135\", \"&alefsym;\"); // alef symbol = first transfinite cardinal,U+2135 NEW -->\n        // <!-- alef symbol is NOT the same as hebrew letter alef,U+05D0 although the\n        // same glyph could be used to depict both characters -->\n        // <!-- Arrows -->\n        initialMap.put(\"\\u2190\", \"&larr;\"); // leftwards arrow, U+2190 ISOnum -->\n        initialMap.put(\"\\u2191\", \"&uarr;\"); // upwards arrow, U+2191 ISOnum-->\n        initialMap.put(\"\\u2192\", \"&rarr;\"); // rightwards arrow, U+2192 ISOnum -->\n        initialMap.put(\"\\u2193\", \"&darr;\"); // downwards arrow, U+2193 ISOnum -->\n        initialMap.put(\"\\u2194\", \"&harr;\"); // left right arrow, U+2194 ISOamsa -->\n        initialMap.put(\"\\u21B5\", \"&crarr;\"); // downwards arrow with corner leftwards= carriage return, U+21B5 NEW -->\n        initialMap.put(\"\\u21D0\", \"&lArr;\"); // leftwards double arrow, U+21D0 ISOtech -->\n        // <!-- ISO 10646 does not say that lArr is the same as the 'is implied by'\n        // arrow but also does not have any other character for that function.\n        // So ? lArr canbe used for 'is implied by' as ISOtech suggests -->\n        initialMap.put(\"\\u21D1\", \"&uArr;\"); // upwards double arrow, U+21D1 ISOamsa -->\n        initialMap.put(\"\\u21D2\", \"&rArr;\"); // rightwards double arrow,U+21D2 ISOtech -->\n        // <!-- ISO 10646 does not say this is the 'implies' character but does not\n        // have another character with this function so ?rArr can be used for\n        // 'implies' as ISOtech suggests -->\n        initialMap.put(\"\\u21D3\", \"&dArr;\"); // downwards double arrow, U+21D3 ISOamsa -->\n        initialMap.put(\"\\u21D4\", \"&hArr;\"); // left right double arrow,U+21D4 ISOamsa -->\n        // <!-- Mathematical Operators -->\n        initialMap.put(\"\\u2200\", \"&forall;\"); // for all, U+2200 ISOtech -->\n        initialMap.put(\"\\u2202\", \"&part;\"); // partial differential, U+2202 ISOtech -->\n        initialMap.put(\"\\u2203\", \"&exist;\"); // there exists, U+2203 ISOtech -->\n        initialMap.put(\"\\u2205\", \"&empty;\"); // empty set = null set = diameter,U+2205 ISOamso -->\n        initialMap.put(\"\\u2207\", \"&nabla;\"); // nabla = backward difference,U+2207 ISOtech -->\n        initialMap.put(\"\\u2208\", \"&isin;\"); // element of, U+2208 ISOtech -->\n        initialMap.put(\"\\u2209\", \"&notin;\"); // not an element of, U+2209 ISOtech -->\n        initialMap.put(\"\\u220B\", \"&ni;\"); // contains as member, U+220B ISOtech -->\n        // <!-- should there be a more memorable name than 'ni'? -->\n        initialMap.put(\"\\u220F\", \"&prod;\"); // n-ary product = product sign,U+220F ISOamsb -->\n        // <!-- prod is NOT the same character as U+03A0 'greek capital letter pi'\n        // though the same glyph might be used for both -->\n        initialMap.put(\"\\u2211\", \"&sum;\"); // n-ary summation, U+2211 ISOamsb -->\n        // <!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'\n        // though the same glyph might be used for both -->\n        initialMap.put(\"\\u2212\", \"&minus;\"); // minus sign, U+2212 ISOtech -->\n        initialMap.put(\"\\u2217\", \"&lowast;\"); // asterisk operator, U+2217 ISOtech -->\n        initialMap.put(\"\\u221A\", \"&radic;\"); // square root = radical sign,U+221A ISOtech -->\n        initialMap.put(\"\\u221D\", \"&prop;\"); // proportional to, U+221D ISOtech -->\n        initialMap.put(\"\\u221E\", \"&infin;\"); // infinity, U+221E ISOtech -->\n        initialMap.put(\"\\u2220\", \"&ang;\"); // angle, U+2220 ISOamso -->\n        initialMap.put(\"\\u2227\", \"&and;\"); // logical and = wedge, U+2227 ISOtech -->\n        initialMap.put(\"\\u2228\", \"&or;\"); // logical or = vee, U+2228 ISOtech -->\n        initialMap.put(\"\\u2229\", \"&cap;\"); // intersection = cap, U+2229 ISOtech -->\n        initialMap.put(\"\\u222A\", \"&cup;\"); // union = cup, U+222A ISOtech -->\n        initialMap.put(\"\\u222B\", \"&int;\"); // integral, U+222B ISOtech -->\n        initialMap.put(\"\\u2234\", \"&there4;\"); // therefore, U+2234 ISOtech -->\n        initialMap.put(\"\\u223C\", \"&sim;\"); // tilde operator = varies with = similar to,U+223C ISOtech -->\n        // <!-- tilde operator is NOT the same character as the tilde, U+007E,although\n        // the same glyph might be used to represent both -->\n        initialMap.put(\"\\u2245\", \"&cong;\"); // approximately equal to, U+2245 ISOtech -->\n        initialMap.put(\"\\u2248\", \"&asymp;\"); // almost equal to = asymptotic to,U+2248 ISOamsr -->\n        initialMap.put(\"\\u2260\", \"&ne;\"); // not equal to, U+2260 ISOtech -->\n        initialMap.put(\"\\u2261\", \"&equiv;\"); // identical to, U+2261 ISOtech -->\n        initialMap.put(\"\\u2264\", \"&le;\"); // less-than or equal to, U+2264 ISOtech -->\n        initialMap.put(\"\\u2265\", \"&ge;\"); // greater-than or equal to,U+2265 ISOtech -->\n        initialMap.put(\"\\u2282\", \"&sub;\"); // subset of, U+2282 ISOtech -->\n        initialMap.put(\"\\u2283\", \"&sup;\"); // superset of, U+2283 ISOtech -->\n        // <!-- note that nsup, 'not a superset of, U+2283' is not covered by the\n        // Symbol font encoding and is not included. Should it be, for symmetry?\n        // It is in ISOamsn -->,\n        initialMap.put(\"\\u2284\", \"&nsub;\"); // not a subset of, U+2284 ISOamsn -->\n        initialMap.put(\"\\u2286\", \"&sube;\"); // subset of or equal to, U+2286 ISOtech -->\n        initialMap.put(\"\\u2287\", \"&supe;\"); // superset of or equal to,U+2287 ISOtech -->\n        initialMap.put(\"\\u2295\", \"&oplus;\"); // circled plus = direct sum,U+2295 ISOamsb -->\n        initialMap.put(\"\\u2297\", \"&otimes;\"); // circled times = vector product,U+2297 ISOamsb -->\n        initialMap.put(\"\\u22A5\", \"&perp;\"); // up tack = orthogonal to = perpendicular,U+22A5 ISOtech -->\n        initialMap.put(\"\\u22C5\", \"&sdot;\"); // dot operator, U+22C5 ISOamsb -->\n        // <!-- dot operator is NOT the same character as U+00B7 middle dot -->\n        // <!-- Miscellaneous Technical -->\n        initialMap.put(\"\\u2308\", \"&lceil;\"); // left ceiling = apl upstile,U+2308 ISOamsc -->\n        initialMap.put(\"\\u2309\", \"&rceil;\"); // right ceiling, U+2309 ISOamsc -->\n        initialMap.put(\"\\u230A\", \"&lfloor;\"); // left floor = apl downstile,U+230A ISOamsc -->\n        initialMap.put(\"\\u230B\", \"&rfloor;\"); // right floor, U+230B ISOamsc -->\n        initialMap.put(\"\\u2329\", \"&lang;\"); // left-pointing angle bracket = bra,U+2329 ISOtech -->\n        // <!-- lang is NOT the same character as U+003C 'less than' or U+2039 'single left-pointing angle quotation\n        // mark' -->\n        initialMap.put(\"\\u232A\", \"&rang;\"); // right-pointing angle bracket = ket,U+232A ISOtech -->\n        // <!-- rang is NOT the same character as U+003E 'greater than' or U+203A\n        // 'single right-pointing angle quotation mark' -->\n        // <!-- Geometric Shapes -->\n        initialMap.put(\"\\u25CA\", \"&loz;\"); // lozenge, U+25CA ISOpub -->\n        // <!-- Miscellaneous Symbols -->\n        initialMap.put(\"\\u2660\", \"&spades;\"); // black spade suit, U+2660 ISOpub -->\n        // <!-- black here seems to mean filled as opposed to hollow -->\n        initialMap.put(\"\\u2663\", \"&clubs;\"); // black club suit = shamrock,U+2663 ISOpub -->\n        initialMap.put(\"\\u2665\", \"&hearts;\"); // black heart suit = valentine,U+2665 ISOpub -->\n        initialMap.put(\"\\u2666\", \"&diams;\"); // black diamond suit, U+2666 ISOpub -->\n\n        // <!-- Latin Extended-A -->\n        initialMap.put(\"\\u0152\", \"&OElig;\"); // -- latin capital ligature OE,U+0152 ISOlat2 -->\n        initialMap.put(\"\\u0153\", \"&oelig;\"); // -- latin small ligature oe, U+0153 ISOlat2 -->\n        // <!-- ligature is a misnomer, this is a separate character in some languages -->\n        initialMap.put(\"\\u0160\", \"&Scaron;\"); // -- latin capital letter S with caron,U+0160 ISOlat2 -->\n        initialMap.put(\"\\u0161\", \"&scaron;\"); // -- latin small letter s with caron,U+0161 ISOlat2 -->\n        initialMap.put(\"\\u0178\", \"&Yuml;\"); // -- latin capital letter Y with diaeresis,U+0178 ISOlat2 -->\n        // <!-- Spacing Modifier Letters -->\n        initialMap.put(\"\\u02C6\", \"&circ;\"); // -- modifier letter circumflex accent,U+02C6 ISOpub -->\n        initialMap.put(\"\\u02DC\", \"&tilde;\"); // small tilde, U+02DC ISOdia -->\n        // <!-- General Punctuation -->\n        initialMap.put(\"\\u2002\", \"&ensp;\"); // en space, U+2002 ISOpub -->\n        initialMap.put(\"\\u2003\", \"&emsp;\"); // em space, U+2003 ISOpub -->\n        initialMap.put(\"\\u2009\", \"&thinsp;\"); // thin space, U+2009 ISOpub -->\n        initialMap.put(\"\\u200C\", \"&zwnj;\"); // zero width non-joiner,U+200C NEW RFC 2070 -->\n        initialMap.put(\"\\u200D\", \"&zwj;\"); // zero width joiner, U+200D NEW RFC 2070 -->\n        initialMap.put(\"\\u200E\", \"&lrm;\"); // left-to-right mark, U+200E NEW RFC 2070 -->\n        initialMap.put(\"\\u200F\", \"&rlm;\"); // right-to-left mark, U+200F NEW RFC 2070 -->\n        initialMap.put(\"\\u2013\", \"&ndash;\"); // en dash, U+2013 ISOpub -->\n        initialMap.put(\"\\u2014\", \"&mdash;\"); // em dash, U+2014 ISOpub -->\n        initialMap.put(\"\\u2018\", \"&lsquo;\"); // left single quotation mark,U+2018 ISOnum -->\n        initialMap.put(\"\\u2019\", \"&rsquo;\"); // right single quotation mark,U+2019 ISOnum -->\n        initialMap.put(\"\\u201A\", \"&sbquo;\"); // single low-9 quotation mark, U+201A NEW -->\n        initialMap.put(\"\\u201C\", \"&ldquo;\"); // left double quotation mark,U+201C ISOnum -->\n        initialMap.put(\"\\u201D\", \"&rdquo;\"); // right double quotation mark,U+201D ISOnum -->\n        initialMap.put(\"\\u201E\", \"&bdquo;\"); // double low-9 quotation mark, U+201E NEW -->\n        initialMap.put(\"\\u2020\", \"&dagger;\"); // dagger, U+2020 ISOpub -->\n        initialMap.put(\"\\u2021\", \"&Dagger;\"); // double dagger, U+2021 ISOpub -->\n        initialMap.put(\"\\u2030\", \"&permil;\"); // per mille sign, U+2030 ISOtech -->\n        initialMap.put(\"\\u2039\", \"&lsaquo;\"); // single left-pointing angle quotation mark,U+2039 ISO proposed -->\n        // <!-- lsaquo is proposed but not yet ISO standardized -->\n        initialMap.put(\"\\u203A\", \"&rsaquo;\"); // single right-pointing angle quotation mark,U+203A ISO proposed -->\n        // <!-- rsaquo is proposed but not yet ISO standardized -->\n        initialMap.put(\"\\u20AC\", \"&euro;\"); // -- euro sign, U+20AC NEW -->\n        HTML40_EXTENDED_ESCAPE = Collections.unmodifiableMap(initialMap);\n    }\n\n    /**\n     * An API for translating text.\n     * Its core use is to escape and unescape text. Because escaping and unescaping\n     * is completely contextual, the API does not present two separate signatures.\n     *\n     * @since 1.0\n     */\n    public static abstract class CharSequenceTranslator {\n\n        /**\n         * Array containing the hexadecimal alphabet.\n         */\n        static final char[] HEX_DIGITS = new char[] {'0', '1', '2', '3',\n                '4', '5', '6', '7',\n                '8', '9', 'A', 'B',\n                'C', 'D', 'E', 'F'};\n\n        /**\n         * Translate a set of codepoints, represented by an int index into a CharSequence,\n         * into another set of codepoints. The number of codepoints consumed must be returned,\n         * and the only IOExceptions thrown must be from interacting with the Writer so that\n         * the top level API may reliably ignore StringWriter IOExceptions.\n         *\n         * @param input CharSequence that is being translated\n         * @param index int representing the current point of translation\n         * @param out Writer to translate the text to\n         * @return int count of codepoints consumed\n         * @throws IOException if and only if the Writer produces an IOException\n         */\n        public abstract int translate(CharSequence input, int index, Writer out) throws IOException;\n\n        /**\n         * Helper for non-Writer usage.\n         * @param input CharSequence to be translated\n         * @return String output of translation\n         */\n        public final String translate(final CharSequence input) {\n            if (input == null) {\n                return null;\n            }\n            try {\n                final StringWriter writer = new StringWriter(input.length() * 2);\n                translate(input, writer);\n                return writer.toString();\n            } catch (final IOException ioe) {\n                // this should never ever happen while writing to a StringWriter\n                throw new RuntimeException(ioe);\n            }\n        }\n\n        /**\n         * Translate an input onto a Writer. This is intentionally final as its algorithm is\n         * tightly coupled with the abstract method of this class.\n         *\n         * @param input CharSequence that is being translated\n         * @param out Writer to translate the text to\n         * @throws IOException if and only if the Writer produces an IOException\n         */\n        public final void translate(final CharSequence input, final Writer out) throws IOException {\n            if (out == null)\n                throw new IllegalStateException(\"The Writer must not be null\");\n            if (input == null) {\n                return;\n            }\n            int pos = 0;\n            final int len = input.length();\n            while (pos < len) {\n                final int consumed = translate(input, pos, out);\n                if (consumed == 0) {\n                    // inlined implementation of Character.toChars(Character.codePointAt(input, pos))\n                    // avoids allocating temp char arrays and duplicate checks\n                    final char c1 = input.charAt(pos);\n                    out.write(c1);\n                    pos++;\n                    if (Character.isHighSurrogate(c1) && pos < len) {\n                        final char c2 = input.charAt(pos);\n                        if (Character.isLowSurrogate(c2)) {\n                            out.write(c2);\n                            pos++;\n                        }\n                    }\n                    continue;\n                }\n                // contract with translators is that they have to understand codepoints\n                // and they just took care of a surrogate pair\n                for (int pt = 0; pt < consumed; pt++) {\n                    pos += Character.charCount(Character.codePointAt(input, pos));\n                }\n            }\n        }\n\n        /**\n         * Helper method to create a merger of this translator with another set of\n         * translators. Useful in customizing the standard functionality.\n         *\n         * @param translators CharSequenceTranslator array of translators to merge with this one\n         * @return CharSequenceTranslator merging this translator with the others\n         */\n        public final CharSequenceTranslator with(final CharSequenceTranslator... translators) {\n            final CharSequenceTranslator[] newArray = new CharSequenceTranslator[translators.length + 1];\n            newArray[0] = this;\n            System.arraycopy(translators, 0, newArray, 1, translators.length);\n            return new AggregateTranslator(newArray);\n        }\n\n        /**\n         * <p>Returns an upper case hexadecimal <code>String</code> for the given\n         * character.</p>\n         *\n         * @param codepoint The codepoint to convert.\n         * @return An upper case hexadecimal <code>String</code>\n         */\n        public static String hex(final int codepoint) {\n            return Integer.toHexString(codepoint).toUpperCase(Locale.ENGLISH);\n        }\n\n    }\n\n    /**\n     * Translates a value using a lookup table.\n     *\n     * @since 1.0\n     */\n    public static class LookupTranslator extends CharSequenceTranslator {\n\n        /** The mapping to be used in translation. */\n        private final Map<String, String> lookupMap;\n        /** The first character of each key in the lookupMap. */\n        private final BitSet prefixSet;\n        /** The length of the shortest key in the lookupMap. */\n        private final int shortest;\n        /** The length of the longest key in the lookupMap. */\n        private final int longest;\n\n        /**\n         * Define the lookup table to be used in translation\n         *\n         * Note that, as of Lang 3.1 (the origin of this code), the key to the lookup\n         * table is converted to a java.lang.String. This is because we need the key\n         * to support hashCode and equals(Object), allowing it to be the key for a\n         * HashMap. See LANG-882.\n         *\n         * @param lookupMap Map&lt;CharSequence, CharSequence&gt; table of translator\n         *                  mappings\n         */\n        public LookupTranslator(final Map<CharSequence, CharSequence> lookupMap) {\n            if (lookupMap == null) {\n                throw new IllegalStateException(\"lookupMap cannot be null\");\n            }\n            this.lookupMap = new HashMap<>();\n            this.prefixSet = new BitSet();\n            int currentShortest = Integer.MAX_VALUE;\n            int currentLongest = 0;\n\n            for (final Map.Entry<CharSequence, CharSequence> pair : lookupMap.entrySet()) {\n                this.lookupMap.put(pair.getKey().toString(), pair.getValue().toString());\n                this.prefixSet.set(pair.getKey().charAt(0));\n                final int sz = pair.getKey().length();\n                if (sz < currentShortest) {\n                    currentShortest = sz;\n                }\n                if (sz > currentLongest) {\n                    currentLongest = sz;\n                }\n            }\n            this.shortest = currentShortest;\n            this.longest = currentLongest;\n        }\n\n        /**\n         * {@inheritDoc}\n         */\n        @Override\n        public int translate(final CharSequence input, final int index, final Writer out) throws IOException {\n            // check if translation exists for the input at position index\n            if (prefixSet.get(input.charAt(index))) {\n                int max = longest;\n                if (index + longest > input.length()) {\n                    max = input.length() - index;\n                }\n                // implement greedy algorithm by trying maximum match first\n                for (int i = max; i >= shortest; i--) {\n                    final CharSequence subSeq = input.subSequence(index, index + i);\n                    final String result = lookupMap.get(subSeq.toString());\n\n                    if (result != null) {\n                        out.write(result);\n                        return i;\n                    }\n                }\n            }\n            return 0;\n        }\n    }\n\n    /**\n     * Executes a sequence of translators one after the other. Execution ends whenever\n     * the first translator consumes codepoints from the input.\n     *\n     * @since 1.0\n     */\n    public static class AggregateTranslator extends CharSequenceTranslator {\n\n        /**\n         * Translator list.\n         */\n        private final List<CharSequenceTranslator> translators = new ArrayList<>();\n\n        /**\n         * Specify the translators to be used at creation time.\n         *\n         * @param translators CharSequenceTranslator array to aggregate\n         */\n        public AggregateTranslator(final CharSequenceTranslator... translators) {\n            if (translators != null) {\n                for (final CharSequenceTranslator translator : translators) {\n                    if (translator != null) {\n                        this.translators.add(translator);\n                    }\n                }\n            }\n        }\n\n        /**\n         * The first translator to consume codepoints from the input is the 'winner'.\n         * Execution stops with the number of consumed codepoints being returned.\n         * {@inheritDoc}\n         */\n        @Override\n        public int translate(final CharSequence input, final int index, final Writer out) throws IOException {\n            for (final CharSequenceTranslator translator : translators) {\n                final int consumed = translator.translate(input, index, out);\n                if (consumed != 0) {\n                    return consumed;\n                }\n            }\n            return 0;\n        }\n\n    }\n\n    /**\n     * Translator object for escaping HTML version 4.0.\n     */\n    private static final CharSequenceTranslator ESCAPE_HTML4 =\n            new AggregateTranslator(\n                    new LookupTranslator(BASIC_ESCAPE),\n                    new LookupTranslator(ISO8859_1_ESCAPE),\n                    new LookupTranslator(HTML40_EXTENDED_ESCAPE)\n            );\n\n    /**\n     * <p>Escapes the characters in a {@code String} using HTML entities.</p>\n     *\n     * <p>\n     * For example:\n     * </p>\n     * <p><code>\"bread\" &amp; \"butter\"</code></p>\n     * becomes:\n     * <p>\n     * <code>&amp;quot;bread&amp;quot; &amp;amp; &amp;quot;butter&amp;quot;</code>.\n     * </p>\n     *\n     * <p>Supports all known HTML 4.0 entities, including funky accents.\n     * Note that the commonly used apostrophe escape character (&amp;apos;)\n     * is not a legal entity and so is not supported). </p>\n     *\n     * @param input  the {@code String} to escape, may be null\n     * @return a new escaped {@code String}, {@code null} if null string input\n     *\n     * @see <a href=\"http://hotwired.lycos.com/webmonkey/reference/special_characters/\">ISO Entities</a>\n     * @see <a href=\"http://www.w3.org/TR/REC-html32#latin1\">HTML 3.2 Character Entities for ISO Latin-1</a>\n     * @see <a href=\"http://www.w3.org/TR/REC-html40/sgml/entities.html\">HTML 4.0 Character entity references</a>\n     * @see <a href=\"http://www.w3.org/TR/html401/charset.html#h-5.3\">HTML 4.01 Character References</a>\n     * @see <a href=\"http://www.w3.org/TR/html401/charset.html#code-position\">HTML 4.01 Code positions</a>\n     */\n    public static final String escapeHtml4(final String input) {\n        return ESCAPE_HTML4.translate(input);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/HttpUtil.java",
    "content": "package peergos.server.util;\n\nimport com.sun.net.httpserver.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class HttpUtil {\n\n    public static boolean allowedQuery(HttpExchange exchange, boolean isPublicServer) {\n        // only allow http POST requests unless we are a public server (not localhost)\n        if (! exchange.getRequestMethod().equals(\"POST\") && ! isPublicServer) {\n            return false;\n        }\n        return true;\n    }\n\n    /** Parse a url query string ignoring encoding\n     *\n     * @param query\n     * @return\n     */\n    public static Map<String, List<String>> parseQuery(String query) {\n        if (query == null)\n            return Collections.emptyMap();\n        if (query.startsWith(\"?\"))\n            query = query.substring(1);\n        String[] parts = query.split(\"&\");\n        Map<String, List<String>> res = new HashMap<>();\n        for (String part : parts) {\n            int sep = part.indexOf(\"=\");\n            String key = part.substring(0, sep);\n            String value = part.substring(sep + 1);\n            res.putIfAbsent(key, new ArrayList<>());\n            res.get(key).add(value);\n        }\n        return res;\n    }\n\n    public static void replyError(HttpExchange exchange, Throwable t) {\n        try {\n            Logging.LOG().log(Level.WARNING, t.getMessage(), t);\n            Throwable cause = t.getCause();\n            if (cause != null)\n                exchange.getResponseHeaders().set(\"Trailer\", URLEncoder.encode(cause.getMessage(), \"UTF-8\"));\n            else\n                exchange.getResponseHeaders().set(\"Trailer\", URLEncoder.encode(t.getMessage(), \"UTF-8\"));\n\n            exchange.getResponseHeaders().set(\"Content-Type\", \"text/plain\");\n            exchange.sendResponseHeaders(400, 0);\n        } catch (IOException e) {\n            Logging.LOG().log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n\n    public static void replyErrorWithCode(HttpExchange exchange, int httpErrorCode, String message) {\n        try {\n            exchange.getResponseHeaders().set(\"Trailer\", URLEncoder.encode(message, \"UTF-8\"));\n            exchange.getResponseHeaders().set(\"Content-Type\", \"text/plain\");\n            exchange.sendResponseHeaders(httpErrorCode, 0);\n        } catch (IOException e) {\n            Logging.LOG().log(Level.WARNING, e.getMessage(), e);\n        }\n    }\n\n    public static byte[] get(PresignedUrl url) throws IOException {\n        return getWithVersion(url).left;\n    }\n\n    public static Pair<byte[], String> getWithVersion(PresignedUrl url) throws IOException {\n        try {\n            if (url.base.startsWith(\"https://\"))\n                return getWithVersionHttps(url);\n            HttpURLConnection conn = (HttpURLConnection) new URI(url.base).toURL().openConnection();\n            conn.setConnectTimeout(10_000);\n            conn.setReadTimeout(60_000);\n            conn.setRequestMethod(\"GET\");\n            for (Map.Entry<String, String> e : url.fields.entrySet()) {\n                conn.setRequestProperty(e.getKey(), e.getValue());\n            }\n\n            try {\n                int respCode = conn.getResponseCode();\n                if (respCode == 502 || respCode == 503)\n                    throw new RateLimitException();\n                if (respCode == 404)\n                    throw new FileNotFoundException();\n                try (InputStream in = conn.getInputStream()) {\n                    Map<String, String> headers = conn.getHeaderFields().entrySet()\n                            .stream()\n                            .filter(e -> e.getKey() != null)\n                            .collect(Collectors.toMap(e -> e.getKey().toLowerCase(), e -> e.getValue().get(0)));\n                    String version = headers.getOrDefault(\"x-amz-version-id\", null);\n                    return new Pair<>(Serialize.readFully(in), version);\n                }\n            } catch (IOException e) {\n                try (InputStream err = conn.getErrorStream()){\n                    if (err == null)\n                        throw e;\n                    byte[] errBody = Serialize.readFully(err);\n                    throw new IOException(new String(errBody), e);\n                }\n            }\n        } catch (URISyntaxException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static Pair<byte[], String> getWithVersionHttps(PresignedUrl url) throws IOException {\n        URI original;\n        try {\n            original = new URI(url.base);\n        } catch (URISyntaxException e) {\n            throw new IllegalArgumentException(e);\n        }\n        try {\n            return NettyPinnedHttps.get(original, url.fields);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static Map<String, List<String>> head(PresignedUrl head) throws IOException {\n        if (head.base.startsWith(\"https://\")) {\n            URI original;\n            try {\n                original = new URI(head.base);\n            } catch (URISyntaxException e) {\n                throw new IllegalArgumentException(e);\n            }\n            try {\n                return NettyPinnedHttps.head(original, head.fields);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        try {\n            HttpURLConnection conn = (HttpURLConnection) new URI(head.base).toURL().openConnection();\n            conn.setRequestMethod(\"HEAD\");\n            for (Map.Entry<String, String> e : head.fields.entrySet()) {\n                conn.setRequestProperty(e.getKey(), e.getValue());\n            }\n\n            try {\n                int respCode = conn.getResponseCode();\n                if (respCode == 200)\n                    return conn.getHeaderFields();\n                if (respCode == 502 || respCode == 503)\n                    throw new RateLimitException();\n                if (respCode == 404)\n                    throw new FileNotFoundException();\n                throw new IllegalStateException(\"HTTP \" + respCode);\n            } catch (IOException e) {\n                try (InputStream err = conn.getErrorStream()) {\n                    if (err == null)\n                        throw e;\n                    byte[] errBody = Serialize.readFully(err);\n                    throw new IOException(new String(errBody));\n                }\n            }\n        } catch (URISyntaxException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static Pair<byte[], String> putWithVersion(PresignedUrl target, byte[] body) throws IOException {\n        return putOrPostWithVersion(\"PUT\", target, body);\n    }\n\n    public static byte[] post(PresignedUrl target, byte[] body) throws IOException {\n        return putOrPostWithVersion(\"POST\", target, body).left;\n    }\n\n    private static Pair<byte[], String> putOrPostWithVersion(String method, PresignedUrl target, byte[] body) throws IOException {\n        if (target.base.startsWith(\"https://\")) {\n            URI original;\n            try {\n                original = new URI(target.base);\n            } catch (URISyntaxException e) {\n                throw new IllegalArgumentException(e);\n            }\n\n            try {\n                return NettyPinnedHttps.putOrPost(method, original, target.fields, body);\n            } catch (InterruptedException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        HttpURLConnection conn = null;\n        try {\n            conn = (HttpURLConnection) new URI(target.base).toURL().openConnection();\n            conn.setRequestMethod(method);\n            for (Map.Entry<String, String> e : target.fields.entrySet()) {\n                conn.setRequestProperty(e.getKey(), e.getValue());\n            }\n            conn.setDoOutput(true);\n            OutputStream out = conn.getOutputStream();\n            out.write(body);\n            out.flush();\n            out.close();\n\n            int httpCode = conn.getResponseCode();\n            if (httpCode == 502 || httpCode == 503)\n                throw new RateLimitException();\n            try (InputStream in = conn.getInputStream()) {\n                Map<String, String> headers = conn.getHeaderFields().entrySet()\n                        .stream()\n                        .filter(e -> e.getKey() != null)\n                        .collect(Collectors.toMap(e -> e.getKey().toLowerCase(), e -> e.getValue().get(0)));\n                String version = headers.getOrDefault(\"x-amz-version-id\", null);\n                return new Pair(Serialize.readFully(in), version);\n            }\n        } catch (ConnectException e) {\n            throw new RateLimitException();\n        } catch (IOException e) {\n            if (conn != null) {\n                try (InputStream err = conn.getErrorStream()) {\n                    if (err != null) {\n                        byte[] errBody = Serialize.readFully(err);\n                        throw new IOException(new String(errBody));\n                    }\n                }\n            }\n            throw new RuntimeException(e);\n        } catch (URISyntaxException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void delete(PresignedUrl target) throws Exception {\n        HttpURLConnection conn = (HttpURLConnection) new URI(target.base).toURL().openConnection();\n        conn.setRequestMethod(\"DELETE\");\n        for (Map.Entry<String, String> e : target.fields.entrySet()) {\n            conn.setRequestProperty(e.getKey(), e.getValue());\n        }\n\n        try {\n            int code = conn.getResponseCode();\n            if (code == 204)\n                return;\n            if (code == 502 || code == 503)\n                throw new RateLimitException();\n            try (InputStream in = conn.getInputStream()) {\n                byte[] body = Serialize.readFully(in);\n                throw new IllegalStateException(\"HTTP \" + code + \"-\" + body);\n            }\n        } catch (IOException e) {\n            try (InputStream err = conn.getErrorStream()) {\n                byte[] errBody = Serialize.readFully(err);\n                throw new IllegalStateException(new String(errBody), e);\n            }\n        }\n    }\n\n    public static URL toURL(String url) {\n        try {\n            return new URI(url).toURL();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/JavaPoster.java",
    "content": "package peergos.server.util;\n\nimport peergos.server.net.Multipart;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.storage.PointerCasException;\nimport peergos.shared.storage.RateLimitException;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.nio.charset.StandardCharsets;\nimport java.net.http.*;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.zip.*;\n\npublic class JavaPoster implements HttpPoster {\n\n    private static final ExecutorService reqPool = Threads.newPool(100, \"JavaPoster\");\n    private final Semaphore http2Streams = new Semaphore(50);\n    private final URL dht;\n    private final boolean useGet;\n    private final Optional<String> basicAuth;\n    private final HttpClient client;\n    private final Optional<String> userAgent;\n\n    public JavaPoster(URL dht, boolean isPublicServer, Optional<String> basicAuth, Optional<String> userAgent, Optional<ProxySelector> proxy) {\n        this.dht = dht;\n        this.useGet = isPublicServer;\n        this.basicAuth = basicAuth;\n        this.userAgent = userAgent;\n        if (proxy.isEmpty())\n            client = HttpClient.newBuilder()\n                    .connectTimeout(Duration.ofMillis(10_000))\n                    .build();\n        else\n            client = HttpClient.newBuilder()\n                    .proxy(proxy.get())\n                    .connectTimeout(Duration.ofMillis(10_000))\n                    .build();\n    }\n\n    public JavaPoster(URL dht, boolean isPublicServer) {\n        this(dht, isPublicServer, Optional.empty(), Optional.empty(), Optional.empty());\n    }\n\n    public URL buildURL(String method) throws IOException {\n        try {\n            return new URL(dht, method);\n        } catch (MalformedURLException mexican) {\n            throw new IOException(mexican);\n        }\n    }\n\n    @Override\n    public CompletableFuture<byte[]> postUnzip(String url, byte[] payload, int timeoutMillis) {\n        return post(url, payload, true, timeoutMillis);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> post(String url, byte[] payload, boolean unzip, int timeoutMillis) {\n        return post(url, payload, unzip, Collections.emptyMap(), timeoutMillis);\n    }\n\n    private CompletableFuture<byte[]> post(String url, byte[] payload, boolean unzip, Map<String, String> headers, int timeoutMillis) {\n        CompletableFuture<byte[]> res = new CompletableFuture<>();\n        HttpResponse<InputStream> response = null;\n        try\n        {\n            URI uri = URI.create(buildURL(url).toString());\n            HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(uri);\n            userAgent.ifPresent(agent -> requestBuilder.setHeader(\"User-Agent\", agent));\n            if (payload.length == 0) {\n                requestBuilder.POST(HttpRequest.BodyPublishers.noBody());\n            } else {\n                requestBuilder.POST(HttpRequest.BodyPublishers.ofByteArray(payload));\n            }\n            if (timeoutMillis >= 0)\n                requestBuilder.timeout(Duration.ofMillis(timeoutMillis));\n            for (Map.Entry<String, String> e : headers.entrySet()) {\n                if (! e.getKey().equals(\"Host\") && ! e.getKey().equals(\"Content-Length\"))\n                    requestBuilder.setHeader(e.getKey(), e.getValue());\n            }\n            if (basicAuth.isPresent())\n                requestBuilder.setHeader(\"Authorization\", basicAuth.get());\n\n            HttpRequest request  = requestBuilder.build();\n            http2Streams.acquire();\n            try {\n                response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());\n            } finally {\n                http2Streams.release();\n            }\n            HttpHeaders responseHeaders = response.headers();\n            Optional<String> contentEncodingOpt = responseHeaders.firstValue(\"content-encoding\");\n            boolean isGzipped = contentEncodingOpt.isPresent() && \"gzip\".equals(contentEncodingOpt.get());\n            DataInputStream din = new DataInputStream(isGzipped && unzip ? new GZIPInputStream(response.body()) : response.body());\n            byte[] resp = Serialize.readFully(din);\n            din.close();\n            int statusCode = response.statusCode();\n            if (statusCode == 429 || statusCode == 502 || statusCode == 503 || statusCode == 504) {\n                res.completeExceptionally(new RateLimitException());\n            } else if (statusCode != 200) {\n                handleError(url, res, response, new IOException(resp.length == 0 ?\n                        \"Unexpected Error. Status code: \" + statusCode\n                        : new String(resp)));\n            } else {\n                res.complete(resp);\n            }\n        } catch (HttpTimeoutException e) {\n            res.completeExceptionally(new SocketTimeoutException(\"Socket timeout on: \" + dht.toString() + url));\n        } catch (InterruptedException ex) {\n            res.completeExceptionally(new RuntimeException(ex));\n        } catch (IOException e) {\n            handleError(url, res, response, e);\n        } catch (Exception e) {\n            res.completeExceptionally(e);\n        }\n        return res;\n    }\n\n    public static void handleError(String url, CompletableFuture<byte[]> res, HttpResponse<InputStream> response, Exception e) {\n        if (response != null) {\n            HttpHeaders responseHeaders = response.headers();\n            Optional<String> trailer = responseHeaders.firstValue(\"Trailer\");\n            String trailerDecoded = trailer.isEmpty() ? \"\" : URLDecoder.decode(trailer.get(), StandardCharsets.UTF_8);\n            Throwable rese = trailer.isEmpty() ?\n                    e :\n                    trailerDecoded.startsWith(\"PointerCAS:\") ?\n                            PointerCasException.fromString(trailerDecoded) :\n                            trailerDecoded.contains(\"Queue full\") ?\n                                    new RateLimitException() :\n                                    new RuntimeException(trailerDecoded);\n            res.completeExceptionally(rese);\n        } else\n            res.completeExceptionally(e);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> postMultipart(String url, List<byte[]> files, int timeoutMillis) {\n        try {\n            Map<String, String> headers = new HashMap<>();\n            if (basicAuth.isPresent())\n                headers.put(\"Authorization\", basicAuth.get());\n            userAgent.ifPresent(agent -> headers.put(\"User-Agent\", agent));\n            Multipart mPost = new Multipart(client, buildURL(url).toString(), \"UTF-8\", headers, timeoutMillis);\n            int i = 0;\n            for (byte[] file : files) {\n                String fieldName = \"file\" + i++;\n                mPost.addFilePart(fieldName, new NamedStreamable.ByteArrayWrapper(Optional.of(fieldName), file));\n            }\n            return mPost.finish();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public CompletableFuture<byte[]> postMultipart(String url, List<byte[]> files) {\n        return postMultipart(url, files, 20_000);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> put(String url, byte[] body, Map<String, String> headers) {\n        return put(url, body, headers, 60_000);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> put(String url, byte[] body, Map<String, String> headers, int timeoutMillis) {\n        CompletableFuture<byte[]> res = new CompletableFuture<>();\n        CompletableFuture.runAsync(() -> {\n            HttpResponse<InputStream> response = null;\n            try {\n                URI uri = URI.create(buildURL(url).toString());\n                HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(uri);\n                userAgent.ifPresent(agent -> requestBuilder.setHeader(\"User-Agent\", agent));\n                requestBuilder.PUT(HttpRequest.BodyPublishers.ofByteArray(body));\n                requestBuilder.timeout(Duration.ofMillis(timeoutMillis));\n                for (Map.Entry<String, String> e : headers.entrySet()) {\n                    if (! e.getKey().equals(\"Host\") && ! e.getKey().equals(\"Content-Length\"))\n                        requestBuilder.setHeader(e.getKey(), e.getValue());\n                }\n                if (basicAuth.isPresent())\n                    requestBuilder.setHeader(\"Authorization\", basicAuth.get());\n\n                HttpRequest request = requestBuilder.build();\n                http2Streams.acquire();\n                try {\n                    response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());\n                } finally {\n                    http2Streams.release();\n                }\n                HttpHeaders responseHeaders = response.headers();\n                Optional<String> contentEncodingOpt = responseHeaders.firstValue(\"content-encoding\");\n                boolean isGzipped = contentEncodingOpt.isPresent() && \"gzip\".equals(contentEncodingOpt.get());\n                DataInputStream din = new DataInputStream(isGzipped ?\n                        new GZIPInputStream(response.body()) :\n                        response.body());\n                byte[] resp = Serialize.readFully(din);\n                din.close();\n                int statusCode = response.statusCode();\n                if (statusCode != 200) {\n                    handleError(url, res, response, new IOException(resp.length == 0 ?\n                            \"Unexpected Error. Status code: \" + statusCode\n                            : new String(resp)));\n                } else {\n                    res.complete(resp);\n                }\n            } catch (HttpTimeoutException e) {\n                res.completeExceptionally(new SocketTimeoutException(\"Socket timeout on: \" + dht.toString() + url));\n            } catch (InterruptedException ex) {\n                res.completeExceptionally(new RuntimeException(ex));\n            } catch (IOException e) {\n                handleError(url, res, response, e);\n            } catch (Exception e) {\n                res.completeExceptionally(e);\n            }\n        }, reqPool);\n        return res;\n    }\n\n    @Override\n    public CompletableFuture<byte[]> get(String url) {\n        return get(url, Collections.emptyMap());\n    }\n\n    @Override\n    public CompletableFuture<byte[]> get(String url, Map<String, String> headers) {\n        if (useGet) {\n            return publicGet(url, headers);\n        } else {\n            // This changes to a POST with an empty body\n            // The reason for this is browsers allow any website to do a get request to localhost\n            // but they block POST requests. So this prevents random websites from calling APIs on localhost\n            return postUnzip(url, new byte[0]);\n        }\n    }\n\n    private CompletableFuture<byte[]> publicGet(String url, Map<String, String> headers) {\n        CompletableFuture<byte[]> res = new CompletableFuture<>();\n        CompletableFuture.runAsync(() -> {\n            HttpResponse<InputStream> response = null;\n            try {\n                URI uri = URI.create(buildURL(url).toString());\n                HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(uri);\n                userAgent.ifPresent(agent -> requestBuilder.setHeader(\"User-Agent\", agent));\n                requestBuilder.GET();\n                requestBuilder.timeout(Duration.ofMillis(60_000));\n                for (Map.Entry<String, String> e : headers.entrySet()) {\n                    if (! e.getKey().equals(\"Host\") && ! e.getKey().equals(\"Content-Length\"))\n                        requestBuilder.setHeader(e.getKey(), e.getValue());\n                }\n                if (basicAuth.isPresent())\n                    requestBuilder.setHeader(\"Authorization\", basicAuth.get());\n\n                HttpRequest request = requestBuilder.build();\n                http2Streams.acquire();\n                try {\n                    response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());\n                } finally {\n                    http2Streams.release();\n                }\n                HttpHeaders responseHeaders = response.headers();\n                Optional<String> contentEncodingOpt = responseHeaders.firstValue(\"content-encoding\");\n                boolean isGzipped = contentEncodingOpt.isPresent() && \"gzip\".equals(contentEncodingOpt.get());\n                DataInputStream din = new DataInputStream(isGzipped ?\n                        new GZIPInputStream(response.body()) :\n                        response.body());\n                byte[] resp = Serialize.readFully(din);\n                din.close();\n                int statusCode = response.statusCode();\n                if (statusCode == 429 || statusCode == 502 || statusCode == 503 || statusCode == 504) {\n                    res.completeExceptionally(new RateLimitException());\n                } else if (statusCode != 200) {\n                    handleError(url, res, response, new IOException(resp.length == 0 ?\n                            \"Unexpected Error. Status code: \" + statusCode\n                            : new String(resp)));\n                } else {\n                    res.complete(resp);\n                }\n            } catch (HttpTimeoutException e) {\n                res.completeExceptionally(new SocketTimeoutException(\"Socket timeout on: \" + dht.toString() + url));\n            } catch (InterruptedException ex) {\n                res.completeExceptionally(new RuntimeException(ex));\n            } catch (IOException e) {\n                handleError(url, res, response, e);\n            } catch (Exception e) {\n                res.completeExceptionally(e);\n            }\n        }, reqPool);\n        return res;\n    }\n\n    @Override\n    public String toString() {\n        return dht.toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/JvmThumbnailer.java",
    "content": "package peergos.server.util;\n\nimport peergos.server.user.JavaImageThumbnailer;\nimport peergos.shared.user.fs.ThumbnailGenerator;\n\npublic class JvmThumbnailer {\n\n    public static void initJava() {\n        ThumbnailGenerator.setInstance(new JavaImageThumbnailer());\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/Logging.java",
    "content": "package peergos.server.util;\n\n\nimport java.io.*;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.logging.*;\n\npublic class  Logging {\n    private static final Logger LOG = Logger.getGlobal();\n    private  static final String NULL_FORMAT =  \"NULL_FORMAT\";\n    private static final Logger NULL_LOG = Logger.getLogger(NULL_FORMAT);\n\n    private static boolean isInitialised = false;\n    public static Logger LOG() {\n        return LOG;\n    }\n    private static Logger nullLog() {\n        return NULL_LOG;\n    }\n\n    /**\n     * Initialise logging to a file in PEERGOS_PATH\n     * @param a\n     */\n    public static synchronized void init(Args a) {\n        Path logPath = a.fromPeergosDir(\"log-name\", \"peergos.%g.log\");\n        logPath.toFile().getParentFile().mkdirs();\n        int logLimit = a.getInt(\"log-limit\", 1024 * 1024);\n        int logCount = a.getInt(\"log-count\", 10);\n        boolean logAppend = a.getBoolean(\"log-append\", true);\n        boolean logToConsole = a.getBoolean(\"log-to-console\", false);\n        boolean logToFile = a.getBoolean(\"log-to-file\", true);\n        boolean printLogLocation = a.getBoolean(\"print-log-location\", true);\n\n        NULL_LOG.setParent(LOG());\n\n        init(logPath, logLimit, logCount, logAppend, logToConsole, logToFile, printLogLocation);\n    }\n\n    public static synchronized void init(Path logPath,\n                                         int logLimit,\n                                         int logCount,\n                                         boolean logAppend,\n                                         boolean logToConsole,\n                                         boolean logToFile,\n                                         boolean printLocation) {\n\n        if (isInitialised)\n            return;\n\n        try {\n            // also logging to stdout?\n            if (! logToConsole)\n                LOG().setUseParentHandlers(false);\n            if (! logToFile)\n                return;\n\n            String logPathS = logPath.toString();\n            FileHandler fileHandler = new FileHandler(logPathS, logLimit, logCount, logAppend);\n            fileHandler.setFormatter(new WithNullFormatter());\n\n            // tell console where we're logging to\n            if (printLocation && logToFile)\n                LOG().info(\"Logging to \"+ logPathS.replace(\"%g\", \"0\"));\n            nullLog().setParent(LOG());\n\n            Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {\n                long id = thread.getId();\n                String name = thread.getName();\n                String msg = \"Uncaught Exception in thread \" + id + \":\" + name;\n                LOG().log(Level.SEVERE, msg, throwable);\n            });\n\n            LOG().addHandler(fileHandler);\n        } catch (IOException ioe) {\n            throw new IllegalStateException(ioe.getMessage(), ioe);\n        } finally {\n            isInitialised = true;\n        }\n    }\n\n    private static final Formatter SIMPLE_FORMATTER = new SimpleFormatter();\n    private static class WithNullFormatter  extends Formatter {\n\n        /**\n         * If the logger-name is NULL_FORMAT just post the message, otherwise use SimpleFormatter.format.\n         * @param logRecord\n         * @return\n         */\n        @Override\n        public String format(LogRecord logRecord) {\n            boolean noFormatting = NULL_FORMAT.equals(logRecord.getLoggerName());\n\n            if (noFormatting)\n                return logRecord.getMessage() + \"\\n\";\n            return SIMPLE_FORMATTER.format(logRecord);\n        }\n    }\n\n    /**\n     * Stream an InputStream to LOG without any formatting.\n     *\n     * This is useful when logging from another processes stdout/stderr.\n     *\n     * @param in Inputstream to be logged\n     *\n     */\n    public static void log(InputStream in, String prefix) {\n        BufferedReader bin = new BufferedReader(new InputStreamReader(in));\n        try {\n            while(true) {\n                String s = bin.readLine();\n                // reached end of stream?\n                if (s ==  null)\n                    return;\n                if (prefix != null)\n                    s = prefix + s;\n\n                nullLog().info(s);\n            }\n        } catch (EOFException eofe) {\n\n        } catch (IOException ioe) {\n            if (! \"Stream closed\".equals(ioe.getMessage()))\n                LOG().log(Level.WARNING, \"Failed to read log message from stream\", ioe);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/NettyPinnedHttps.java",
    "content": "package peergos.server.util;\n\nimport io.netty.bootstrap.Bootstrap;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.*;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport io.netty.handler.codec.http.*;\nimport io.netty.handler.ssl.*;\nimport io.netty.handler.timeout.ReadTimeoutException;\nimport io.netty.handler.timeout.ReadTimeoutHandler;\nimport io.netty.resolver.NoopAddressResolverGroup;\nimport peergos.shared.storage.RateLimitException;\nimport peergos.shared.util.LRUCache;\nimport peergos.shared.util.Pair;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.net.*;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\n\npublic final class NettyPinnedHttps {\n\n    private static final EventLoopGroup GROUP = new NioEventLoopGroup();\n\n    static LRUCache<String, PinnedHost> dnsCache = new LRUCache<>(10);\n\n    public static PinnedHost getHost(URI original) throws IOException {\n        synchronized (dnsCache) {\n            String host = original.getHost();\n            PinnedHost cached = dnsCache.get(host);\n            if (cached != null) {\n                cached.ensureRefreshed();\n                return cached;\n            }\n            PinnedHost pinned = new PinnedHost(host);\n            dnsCache.put(host, pinned);\n            return pinned;\n        }\n    }\n\n    public static Pair<byte[], String> get(URI uri, Map<String, String> fields) throws InterruptedException, IOException {\n        if (!\"https\".equalsIgnoreCase(uri.getScheme()))\n            throw new IllegalArgumentException(\"HTTPS only\");\n\n        PinnedHost pinned = getHost(uri);\n        InetAddress addr = pinned.next();\n\n        CompletableFuture<Pair<byte[], String>> result = new CompletableFuture<>();\n\n        SslContext sslCtx = SslContextBuilder.forClient().build();\n\n        Bootstrap bootstrap = new Bootstrap()\n                .group(GROUP)\n                .channel(NioSocketChannel.class)\n                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5_000)\n                .resolver(NoopAddressResolverGroup.INSTANCE)\n                .handler(new ChannelInitializer<SocketChannel>() {\n                    @Override\n                    protected void initChannel(SocketChannel ch) {\n                        ChannelPipeline p = ch.pipeline();\n\n                        p.addLast(sslCtx.newHandler(\n                                ch.alloc(),\n                                uri.getHost(),\n                                uri.getPort() == -1 ? 443 : uri.getPort()));\n\n                        p.addLast(new ReadTimeoutHandler(6));\n                        p.addLast(new HttpClientCodec());\n                        p.addLast(new HttpObjectAggregator(2 * 1024 * 1024));\n\n                        p.addLast(new SimpleChannelInboundHandler<FullHttpResponse>() {\n                            @Override\n                            protected void channelRead0(ChannelHandlerContext ctx,\n                                                        FullHttpResponse resp) {\n                                int respCode = resp.status().code();\n                                if (respCode != 200) {\n                                    if (respCode == 500 || respCode == 502 || respCode == 503 || respCode == 504)\n                                        result.completeExceptionally(new RateLimitException());\n                                    else if (respCode == 404) {\n                                        result.completeExceptionally(new FileNotFoundException());\n                                    } else {\n                                        ByteBuf reply = resp.content();\n                                        byte[] body = new byte[reply.readableBytes()];\n                                        reply.readBytes(body);\n                                        result.completeExceptionally(\n                                                new RuntimeException(\"HTTP \" + resp.status() + new String(body)));\n                                    }\n                                } else {\n                                    byte[] body = new byte[resp.content().readableBytes()];\n                                    resp.content().readBytes(body);\n                                    String version = resp.headers().contains(\"x-amz-version-id\") ?\n                                            resp.headers().get(\"x-amz-version-id\") :\n                                            null;\n                                    result.complete(new Pair<>(body, version));\n                                }\n                                ctx.close();\n                            }\n\n                            @Override\n                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {\n                                if (t instanceof SslClosedEngineException\n                                        || t instanceof SocketException\n                                        || t instanceof ReadTimeoutException)\n                                    result.completeExceptionally(new RateLimitException());\n                                else\n                                    result.completeExceptionally(t);\n                                ctx.close();\n                            }\n                        });\n                    }\n                });\n\n        InetSocketAddress remote =\n                new InetSocketAddress(addr, uri.getPort() == -1 ? 443 : uri.getPort());\n\n        Channel ch = bootstrap.connect(remote).sync().channel();\n\n        String path = uri.getRawPath()\n                + (uri.getRawQuery() != null ? \"?\" + uri.getRawQuery() : \"\");\n\n        FullHttpRequest req = new DefaultFullHttpRequest(\n                HttpVersion.HTTP_1_1,\n                HttpMethod.GET,\n                path);\n\n        req.headers()\n                .set(HttpHeaderNames.HOST, uri.getHost())\n                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);\n        fields.forEach((k, v) -> req.headers().set(k, v));\n\n        try {\n            ch.writeAndFlush(req).sync();\n        } catch (InterruptedException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RateLimitException();\n        }\n\n        return result.join();\n    }\n\n    public static Map<String, List<String>> head(URI uri, Map<String, String> fields) throws InterruptedException, IOException {\n        if (!\"https\".equalsIgnoreCase(uri.getScheme()))\n            throw new IllegalArgumentException(\"HTTPS only\");\n\n        PinnedHost pinned = getHost(uri);\n        InetAddress addr = pinned.next();\n\n        CompletableFuture<Map<String, List<String>>> result = new CompletableFuture<>();\n\n        SslContext sslCtx = SslContextBuilder.forClient().build();\n\n        Bootstrap bootstrap = new Bootstrap()\n                .group(GROUP)\n                .channel(NioSocketChannel.class)\n                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5_000)\n                .resolver(NoopAddressResolverGroup.INSTANCE)\n                .handler(new ChannelInitializer<SocketChannel>() {\n                    @Override\n                    protected void initChannel(SocketChannel ch) {\n                        ChannelPipeline p = ch.pipeline();\n\n                        p.addLast(sslCtx.newHandler(\n                                ch.alloc(),\n                                uri.getHost(),\n                                uri.getPort() == -1 ? 443 : uri.getPort()));\n\n                        p.addLast(new ReadTimeoutHandler(6));\n                        p.addLast(new HttpClientCodec());\n                        p.addLast(new HttpObjectAggregator(2 * 1024 * 1024));\n\n                        p.addLast(new SimpleChannelInboundHandler<FullHttpResponse>() {\n                            @Override\n                            protected void channelRead0(ChannelHandlerContext ctx,\n                                                        FullHttpResponse resp) {\n                                int respCode = resp.status().code();\n                                if (respCode != 200) {\n                                    if (respCode == 502 || respCode == 503)\n                                        result.completeExceptionally(new RateLimitException());\n                                    else if (respCode == 404) {\n                                        result.completeExceptionally(new FileNotFoundException());\n                                    } else\n                                        result.completeExceptionally(\n                                                new RuntimeException(\"HTTP \" + resp.status()));\n                                } else {\n                                    HttpHeaders headers = resp.headers();\n                                    Map<String, List<String>> res = new HashMap<>();\n                                    headers.forEach(e -> res.put(e.getKey(), List.of(e.getValue())));\n                                    result.complete(res);\n                                }\n                                ctx.close();\n                            }\n\n                            @Override\n                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {\n                                if (t instanceof SslClosedEngineException\n                                        || t instanceof SocketException\n                                        || t instanceof ReadTimeoutException)\n                                    result.completeExceptionally(new RateLimitException());\n                                else\n                                    result.completeExceptionally(t);\n                                ctx.close();\n                            }\n                        });\n                    }\n                });\n\n        InetSocketAddress remote =\n                new InetSocketAddress(addr, uri.getPort() == -1 ? 443 : uri.getPort());\n\n        Channel ch = bootstrap.connect(remote).sync().channel();\n\n        String path = uri.getRawPath()\n                + (uri.getRawQuery() != null ? \"?\" + uri.getRawQuery() : \"\");\n\n        FullHttpRequest req = new DefaultFullHttpRequest(\n                HttpVersion.HTTP_1_1,\n                HttpMethod.HEAD,\n                path);\n\n        req.headers()\n                .set(HttpHeaderNames.HOST, uri.getHost())\n                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);\n        fields.forEach((k, v) -> req.headers().set(k, v));\n\n        try {\n            ch.writeAndFlush(req).sync();\n        } catch (InterruptedException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RateLimitException();\n        }\n\n        return result.join();\n    }\n\n    public static Pair<byte[], String> putOrPost(String method,\n                                                 URI uri,\n                                                 Map<String, String> headers,\n                                                 byte[] body) throws IOException, InterruptedException {\n        if (!\"https\".equalsIgnoreCase(uri.getScheme()))\n            throw new IllegalArgumentException(\"HTTPS only\");\n\n        PinnedHost pinned = getHost(uri);\n        InetAddress addr = pinned.next();\n\n        CompletableFuture<Pair<byte[], String>> result = new CompletableFuture<>();\n\n        SslContext sslCtx = SslContextBuilder.forClient().build();\n\n        Bootstrap bootstrap = new Bootstrap()\n                .group(GROUP)\n                .channel(NioSocketChannel.class)\n                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5_000)\n                .resolver(NoopAddressResolverGroup.INSTANCE)\n                .handler(new ChannelInitializer<SocketChannel>() {\n                    @Override\n                    protected void initChannel(SocketChannel ch) {\n                        ChannelPipeline p = ch.pipeline();\n\n                        p.addLast(sslCtx.newHandler(\n                                ch.alloc(),\n                                uri.getHost(),\n                                uri.getPort() == -1 ? 443 : uri.getPort()));\n\n                        p.addLast(new HttpClientCodec());\n                        p.addLast(new HttpObjectAggregator(2 * 1024 * 1024));\n\n                        p.addLast(new SimpleChannelInboundHandler<FullHttpResponse>() {\n                            @Override\n                            protected void channelRead0(ChannelHandlerContext ctx,\n                                                        FullHttpResponse resp) {\n                                int respCode = resp.status().code();\n                                if (respCode != 200) {\n                                    if (respCode == 500 || respCode == 502 || respCode == 503 || respCode == 504)\n                                        result.completeExceptionally(new RateLimitException());\n                                    else if (respCode == 404) {\n                                        result.completeExceptionally(new FileNotFoundException());\n                                    } else\n                                        result.completeExceptionally(\n                                                new RuntimeException(\"HTTP \" + resp.status()));\n                                } else {\n                                    byte[] body = new byte[resp.content().readableBytes()];\n                                    resp.content().readBytes(body);\n                                    String version = resp.headers().contains(\"x-amz-version-id\") ?\n                                            resp.headers().get(\"x-amz-version-id\") :\n                                            null;\n                                    result.complete(new Pair<>(body, version));\n                                }\n                                ctx.close();\n                            }\n\n                            @Override\n                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {\n                                if (t instanceof SslClosedEngineException\n                                        || t instanceof SocketException\n                                        || t instanceof ReadTimeoutException)\n                                    result.completeExceptionally(new RateLimitException());\n                                else if (t.getMessage() != null && t.getMessage().contains(\"Connection reset\"))\n                                    result.completeExceptionally(new RateLimitException());\n                                else\n                                    result.completeExceptionally(t);\n                                ctx.close();\n                            }\n                        });\n                    }\n                });\n\n        InetSocketAddress remote =\n                new InetSocketAddress(addr, uri.getPort() == -1 ? 443 : uri.getPort());\n\n        Channel ch = bootstrap.connect(remote).sync().channel();\n\n        String path = uri.getRawPath()\n                + (uri.getRawQuery() != null ? \"?\" + uri.getRawQuery() : \"\");\n\n        FullHttpRequest req = new DefaultFullHttpRequest(\n                HttpVersion.HTTP_1_1,\n                method.equals(\"PUT\") ? HttpMethod.PUT : HttpMethod.POST,\n                path,\n                Unpooled.wrappedBuffer(body));\n\n        req.headers()\n                .set(HttpHeaderNames.HOST, uri.getHost())\n                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);\n        headers.forEach((k, v) -> req.headers().set(k, v));\n\n        try {\n            ch.writeAndFlush(req).sync();\n        } catch (InterruptedException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RateLimitException();\n        }\n\n        return result.join();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/Passwords.java",
    "content": "package peergos.server.util;\n\nimport java.security.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class Passwords {\n\n    public static String generate() {\n        SecureRandom rnd = new SecureRandom();\n        return IntStream.range(0, 7)\n                .mapToObj(i -> WORDS.get(rnd.nextInt(WORDS.size())))\n                .collect(Collectors.joining(\"-\"));\n    }\n\n    // 2048 word list from BIP-0039-english\n    private static List<String> WORDS = Arrays.asList(\n            \"abandon\",\"ability\",\"able\",\"about\",\"above\",\"absent\",\"absorb\",\"abstract\",\"absurd\",\"abuse\",\"access\",\"accident\",\"account\",\"accuse\",\"achieve\",\"acid\",\"acoustic\",\"acquire\",\"across\",\"act\",\"action\",\"actor\",\"actress\",\"actual\",\"adapt\",\"add\",\"addict\",\"address\",\"adjust\",\"admit\",\"adult\",\"advance\",\"advice\",\"aerobic\",\"affair\",\"afford\",\"afraid\",\"again\",\"age\",\"agent\",\"agree\",\"ahead\",\"aim\",\"air\",\"airport\",\"aisle\",\"alarm\",\"album\",\"alcohol\",\"alert\",\"alien\",\"all\",\"alley\",\"allow\",\"almost\",\"alone\",\"alpha\",\"already\",\"also\",\"alter\",\"always\",\"amateur\",\"amazing\",\"among\",\"amount\",\"amused\",\"analyst\",\"anchor\",\"ancient\",\"anger\",\"angle\",\"angry\",\"animal\",\"ankle\",\"announce\",\"annual\",\"another\",\"answer\",\"antenna\",\"antique\",\"anxiety\",\"any\",\"apart\",\"apology\",\"appear\",\"apple\",\"approve\",\"april\",\"arch\",\"arctic\",\"area\",\"arena\",\"argue\",\"arm\",\"armed\",\"armor\",\"army\",\"around\",\"arrange\",\"arrest\",\"arrive\",\"arrow\",\"art\",\"artefact\",\"artist\",\"artwork\",\"ask\",\"aspect\",\"assault\",\"asset\",\"assist\",\"assume\",\"asthma\",\"athlete\",\"atom\",\"attack\",\"attend\",\"attitude\",\"attract\",\"auction\",\"audit\",\"august\",\"aunt\",\"author\",\"auto\",\"autumn\",\"average\",\"avocado\",\"avoid\",\"awake\",\"aware\",\"away\",\"awesome\",\"awful\",\"awkward\",\"axis\",\"baby\",\"bachelor\",\"bacon\",\"badge\",\"bag\",\"balance\",\"balcony\",\"ball\",\"bamboo\",\"banana\",\"banner\",\"bar\",\"barely\",\"bargain\",\"barrel\",\"base\",\"basic\",\"basket\",\"battle\",\"beach\",\"bean\",\"beauty\",\"because\",\"become\",\"beef\",\"before\",\"begin\",\"behave\",\"behind\",\"believe\",\"below\",\"belt\",\"bench\",\"benefit\",\"best\",\"betray\",\"better\",\"between\",\"beyond\",\"bicycle\",\"bid\",\"bike\",\"bind\",\"biology\",\"bird\",\"birth\",\"bitter\",\"black\",\"blade\",\"blame\",\"blanket\",\"blast\",\"bleak\",\"bless\",\"blind\",\"blood\",\"blossom\",\"blouse\",\"blue\",\"blur\",\"blush\",\"board\",\"boat\",\"body\",\"boil\",\"bomb\",\"bone\",\"bonus\",\"book\",\"boost\",\"border\",\"boring\",\"borrow\",\"boss\",\"bottom\",\"bounce\",\"box\",\"boy\",\"bracket\",\"brain\",\"brand\",\"brass\",\"brave\",\"bread\",\"breeze\",\"brick\",\"bridge\",\"brief\",\"bright\",\"bring\",\"brisk\",\"broccoli\",\"broken\",\"bronze\",\"broom\",\"brother\",\"brown\",\"brush\",\"bubble\",\"buddy\",\"budget\",\"buffalo\",\"build\",\"bulb\",\"bulk\",\"bullet\",\"bundle\",\"bunker\",\"burden\",\"burger\",\"burst\",\"bus\",\"business\",\"busy\",\"butter\",\"buyer\",\"buzz\",\"cabbage\",\"cabin\",\"cable\",\"cactus\",\"cage\",\"cake\",\"call\",\"calm\",\"camera\",\"camp\",\"can\",\"canal\",\"cancel\",\"candy\",\"cannon\",\"canoe\",\"canvas\",\"canyon\",\"capable\",\"capital\",\"captain\",\"car\",\"carbon\",\"card\",\"cargo\",\"carpet\",\"carry\",\"cart\",\"case\",\"cash\",\"casino\",\"castle\",\"casual\",\"cat\",\"catalog\",\"catch\",\"category\",\"cattle\",\"caught\",\"cause\",\"caution\",\"cave\",\"ceiling\",\"celery\",\"cement\",\"census\",\"century\",\"cereal\",\"certain\",\"chair\",\"chalk\",\"champion\",\"change\",\"chaos\",\"chapter\",\"charge\",\"chase\",\"chat\",\"cheap\",\"check\",\"cheese\",\"chef\",\"cherry\",\"chest\",\"chicken\",\"chief\",\"child\",\"chimney\",\"choice\",\"choose\",\"chronic\",\"chuckle\",\"chunk\",\"churn\",\"cigar\",\"cinnamon\",\"circle\",\"citizen\",\"city\",\"civil\",\"claim\",\"clap\",\"clarify\",\"claw\",\"clay\",\"clean\",\"clerk\",\"clever\",\"click\",\"client\",\"cliff\",\"climb\",\"clinic\",\"clip\",\"clock\",\"clog\",\"close\",\"cloth\",\"cloud\",\"clown\",\"club\",\"clump\",\"cluster\",\"clutch\",\"coach\",\"coast\",\"coconut\",\"code\",\"coffee\",\"coil\",\"coin\",\"collect\",\"color\",\"column\",\"combine\",\"come\",\"comfort\",\"comic\",\"common\",\"company\",\"concert\",\"conduct\",\"confirm\",\"congress\",\"connect\",\"consider\",\"control\",\"convince\",\"cook\",\"cool\",\"copper\",\"copy\",\"coral\",\"core\",\"corn\",\"correct\",\"cost\",\"cotton\",\"couch\",\"country\",\"couple\",\"course\",\"cousin\",\"cover\",\"coyote\",\"crack\",\"cradle\",\"craft\",\"cram\",\"crane\",\"crash\",\"crater\",\"crawl\",\"crazy\",\"cream\",\"credit\",\"creek\",\"crew\",\"cricket\",\"crime\",\"crisp\",\"critic\",\"crop\",\"cross\",\"crouch\",\"crowd\",\"crucial\",\"cruel\",\"cruise\",\"crumble\",\"crunch\",\"crush\",\"cry\",\"crystal\",\"cube\",\"culture\",\"cup\",\"cupboard\",\"curious\",\"current\",\"curtain\",\"curve\",\"cushion\",\"custom\",\"cute\",\"cycle\",\"dad\",\"damage\",\"damp\",\"dance\",\"danger\",\"daring\",\"dash\",\"daughter\",\"dawn\",\"day\",\"deal\",\"debate\",\"debris\",\"decade\",\"december\",\"decide\",\"decline\",\"decorate\",\"decrease\",\"deer\",\"defense\",\"define\",\"defy\",\"degree\",\"delay\",\"deliver\",\"demand\",\"demise\",\"denial\",\"dentist\",\"deny\",\"depart\",\"depend\",\"deposit\",\"depth\",\"deputy\",\"derive\",\"describe\",\"desert\",\"design\",\"desk\",\"despair\",\"destroy\",\"detail\",\"detect\",\"develop\",\"device\",\"devote\",\"diagram\",\"dial\",\"diamond\",\"diary\",\"dice\",\"diesel\",\"diet\",\"differ\",\"digital\",\"dignity\",\"dilemma\",\"dinner\",\"dinosaur\",\"direct\",\"dirt\",\"disagree\",\"discover\",\"disease\",\"dish\",\"dismiss\",\"disorder\",\"display\",\"distance\",\"divert\",\"divide\",\"divorce\",\"dizzy\",\"doctor\",\"document\",\"dog\",\"doll\",\"dolphin\",\"domain\",\"donate\",\"donkey\",\"donor\",\"door\",\"dose\",\"double\",\"dove\",\"draft\",\"dragon\",\"drama\",\"drastic\",\"draw\",\"dream\",\"dress\",\"drift\",\"drill\",\"drink\",\"drip\",\"drive\",\"drop\",\"drum\",\"dry\",\"duck\",\"dumb\",\"dune\",\"during\",\"dust\",\"dutch\",\"duty\",\"dwarf\",\"dynamic\",\"eager\",\"eagle\",\"early\",\"earn\",\"earth\",\"easily\",\"east\",\"easy\",\"echo\",\"ecology\",\"economy\",\"edge\",\"edit\",\"educate\",\"effort\",\"egg\",\"eight\",\"either\",\"elbow\",\"elder\",\"electric\",\"elegant\",\"element\",\"elephant\",\"elevator\",\"elite\",\"else\",\"embark\",\"embody\",\"embrace\",\"emerge\",\"emotion\",\"employ\",\"empower\",\"empty\",\"enable\",\"enact\",\"end\",\"endless\",\"endorse\",\"enemy\",\"energy\",\"enforce\",\"engage\",\"engine\",\"enhance\",\"enjoy\",\"enlist\",\"enough\",\"enrich\",\"enroll\",\"ensure\",\"enter\",\"entire\",\"entry\",\"envelope\",\"episode\",\"equal\",\"equip\",\"era\",\"erase\",\"erode\",\"erosion\",\"error\",\"erupt\",\"escape\",\"essay\",\"essence\",\"estate\",\"eternal\",\"ethics\",\"evidence\",\"evil\",\"evoke\",\"evolve\",\"exact\",\"example\",\"excess\",\"exchange\",\"excite\",\"exclude\",\"excuse\",\"execute\",\"exercise\",\"exhaust\",\"exhibit\",\"exile\",\"exist\",\"exit\",\"exotic\",\"expand\",\"expect\",\"expire\",\"explain\",\"expose\",\"express\",\"extend\",\"extra\",\"eye\",\"eyebrow\",\"fabric\",\"face\",\"faculty\",\"fade\",\"faint\",\"faith\",\"fall\",\"false\",\"fame\",\"family\",\"famous\",\"fan\",\"fancy\",\"fantasy\",\"farm\",\"fashion\",\"fat\",\"fatal\",\"father\",\"fatigue\",\"fault\",\"favorite\",\"feature\",\"february\",\"federal\",\"fee\",\"feed\",\"feel\",\"female\",\"fence\",\"festival\",\"fetch\",\"fever\",\"few\",\"fiber\",\"fiction\",\"field\",\"figure\",\"file\",\"film\",\"filter\",\"final\",\"find\",\"fine\",\"finger\",\"finish\",\"fire\",\"firm\",\"first\",\"fiscal\",\"fish\",\"fit\",\"fitness\",\"fix\",\"flag\",\"flame\",\"flash\",\"flat\",\"flavor\",\"flee\",\"flight\",\"flip\",\"float\",\"flock\",\"floor\",\"flower\",\"fluid\",\"flush\",\"fly\",\"foam\",\"focus\",\"fog\",\"foil\",\"fold\",\"follow\",\"food\",\"foot\",\"force\",\"forest\",\"forget\",\"fork\",\"fortune\",\"forum\",\"forward\",\"fossil\",\"foster\",\"found\",\"fox\",\"fragile\",\"frame\",\"frequent\",\"fresh\",\"friend\",\"fringe\",\"frog\",\"front\",\"frost\",\"frown\",\"frozen\",\"fruit\",\"fuel\",\"fun\",\"funny\",\"furnace\",\"fury\",\"future\",\"gadget\",\"gain\",\"galaxy\",\"gallery\",\"game\",\"gap\",\"garage\",\"garbage\",\"garden\",\"garlic\",\"garment\",\"gas\",\"gasp\",\"gate\",\"gather\",\"gauge\",\"gaze\",\"general\",\"genius\",\"genre\",\"gentle\",\"genuine\",\"gesture\",\"ghost\",\"giant\",\"gift\",\"giggle\",\"ginger\",\"giraffe\",\"girl\",\"give\",\"glad\",\"glance\",\"glare\",\"glass\",\"glide\",\"glimpse\",\"globe\",\"gloom\",\"glory\",\"glove\",\"glow\",\"glue\",\"goat\",\"goddess\",\"gold\",\"good\",\"goose\",\"gorilla\",\"gospel\",\"gossip\",\"govern\",\"gown\",\"grab\",\"grace\",\"grain\",\"grant\",\"grape\",\"grass\",\"gravity\",\"great\",\"green\",\"grid\",\"grief\",\"grit\",\"grocery\",\"group\",\"grow\",\"grunt\",\"guard\",\"guess\",\"guide\",\"guilt\",\"guitar\",\"gun\",\"gym\",\"habit\",\"hair\",\"half\",\"hammer\",\"hamster\",\"hand\",\"happy\",\"harbor\",\"hard\",\"harsh\",\"harvest\",\"hat\",\"have\",\"hawk\",\"hazard\",\"head\",\"health\",\"heart\",\"heavy\",\"hedgehog\",\"height\",\"hello\",\"helmet\",\"help\",\"hen\",\"hero\",\"hidden\",\"high\",\"hill\",\"hint\",\"hip\",\"hire\",\"history\",\"hobby\",\"hockey\",\"hold\",\"hole\",\"holiday\",\"hollow\",\"home\",\"honey\",\"hood\",\"hope\",\"horn\",\"horror\",\"horse\",\"hospital\",\"host\",\"hotel\",\"hour\",\"hover\",\"hub\",\"huge\",\"human\",\"humble\",\"humor\",\"hundred\",\"hungry\",\"hunt\",\"hurdle\",\"hurry\",\"hurt\",\"husband\",\"hybrid\",\"ice\",\"icon\",\"idea\",\"identify\",\"idle\",\"ignore\",\"ill\",\"illegal\",\"illness\",\"image\",\"imitate\",\"immense\",\"immune\",\"impact\",\"impose\",\"improve\",\"impulse\",\"inch\",\"include\",\"income\",\"increase\",\"index\",\"indicate\",\"indoor\",\"industry\",\"infant\",\"inflict\",\"inform\",\"inhale\",\"inherit\",\"initial\",\"inject\",\"injury\",\"inmate\",\"inner\",\"innocent\",\"input\",\"inquiry\",\"insane\",\"insect\",\"inside\",\"inspire\",\"install\",\"intact\",\"interest\",\"into\",\"invest\",\"invite\",\"involve\",\"iron\",\"island\",\"isolate\",\"issue\",\"item\",\"ivory\",\"jacket\",\"jaguar\",\"jar\",\"jazz\",\"jealous\",\"jeans\",\"jelly\",\"jewel\",\"job\",\"join\",\"joke\",\"journey\",\"joy\",\"judge\",\"juice\",\"jump\",\"jungle\",\"junior\",\"junk\",\"just\",\"kangaroo\",\"keen\",\"keep\",\"ketchup\",\"key\",\"kick\",\"kid\",\"kidney\",\"kind\",\"kingdom\",\"kiss\",\"kit\",\"kitchen\",\"kite\",\"kitten\",\"kiwi\",\"knee\",\"knife\",\"knock\",\"know\",\"lab\",\"label\",\"labor\",\"ladder\",\"lady\",\"lake\",\"lamp\",\"language\",\"laptop\",\"large\",\"later\",\"latin\",\"laugh\",\"laundry\",\"lava\",\"law\",\"lawn\",\"lawsuit\",\"layer\",\"lazy\",\"leader\",\"leaf\",\"learn\",\"leave\",\"lecture\",\"left\",\"leg\",\"legal\",\"legend\",\"leisure\",\"lemon\",\"lend\",\"length\",\"lens\",\"leopard\",\"lesson\",\"letter\",\"level\",\"liar\",\"liberty\",\"library\",\"license\",\"life\",\"lift\",\"light\",\"like\",\"limb\",\"limit\",\"link\",\"lion\",\"liquid\",\"list\",\"little\",\"live\",\"lizard\",\"load\",\"loan\",\"lobster\",\"local\",\"lock\",\"logic\",\"lonely\",\"long\",\"loop\",\"lottery\",\"loud\",\"lounge\",\"love\",\"loyal\",\"lucky\",\"luggage\",\"lumber\",\"lunar\",\"lunch\",\"luxury\",\"lyrics\",\"machine\",\"mad\",\"magic\",\"magnet\",\"maid\",\"mail\",\"main\",\"major\",\"make\",\"mammal\",\"man\",\"manage\",\"mandate\",\"mango\",\"mansion\",\"manual\",\"maple\",\"marble\",\"march\",\"margin\",\"marine\",\"market\",\"marriage\",\"mask\",\"mass\",\"master\",\"match\",\"material\",\"math\",\"matrix\",\"matter\",\"maximum\",\"maze\",\"meadow\",\"mean\",\"measure\",\"meat\",\"mechanic\",\"medal\",\"media\",\"melody\",\"melt\",\"member\",\"memory\",\"mention\",\"menu\",\"mercy\",\"merge\",\"merit\",\"merry\",\"mesh\",\"message\",\"metal\",\"method\",\"middle\",\"midnight\",\"milk\",\"million\",\"mimic\",\"mind\",\"minimum\",\"minor\",\"minute\",\"miracle\",\"mirror\",\"misery\",\"miss\",\"mistake\",\"mix\",\"mixed\",\"mixture\",\"mobile\",\"model\",\"modify\",\"mom\",\"moment\",\"monitor\",\"monkey\",\"monster\",\"month\",\"moon\",\"moral\",\"more\",\"morning\",\"mosquito\",\"mother\",\"motion\",\"motor\",\"mountain\",\"mouse\",\"move\",\"movie\",\"much\",\"muffin\",\"mule\",\"multiply\",\"muscle\",\"museum\",\"mushroom\",\"music\",\"must\",\"mutual\",\"myself\",\"mystery\",\"myth\",\"naive\",\"name\",\"napkin\",\"narrow\",\"nasty\",\"nation\",\"nature\",\"near\",\"neck\",\"need\",\"negative\",\"neglect\",\"neither\",\"nephew\",\"nerve\",\"nest\",\"net\",\"network\",\"neutral\",\"never\",\"news\",\"next\",\"nice\",\"night\",\"noble\",\"noise\",\"nominee\",\"noodle\",\"normal\",\"north\",\"nose\",\"notable\",\"note\",\"nothing\",\"notice\",\"novel\",\"now\",\"nuclear\",\"number\",\"nurse\",\"nut\",\"oak\",\"obey\",\"object\",\"oblige\",\"obscure\",\"observe\",\"obtain\",\"obvious\",\"occur\",\"ocean\",\"october\",\"odor\",\"off\",\"offer\",\"office\",\"often\",\"oil\",\"okay\",\"old\",\"olive\",\"olympic\",\"omit\",\"once\",\"one\",\"onion\",\"online\",\"only\",\"open\",\"opera\",\"opinion\",\"oppose\",\"option\",\"orange\",\"orbit\",\"orchard\",\"order\",\"ordinary\",\"organ\",\"orient\",\"original\",\"orphan\",\"ostrich\",\"other\",\"outdoor\",\"outer\",\"output\",\"outside\",\"oval\",\"oven\",\"over\",\"own\",\"owner\",\"oxygen\",\"oyster\",\"ozone\",\"pact\",\"paddle\",\"page\",\"pair\",\"palace\",\"palm\",\"panda\",\"panel\",\"panic\",\"panther\",\"paper\",\"parade\",\"parent\",\"park\",\"parrot\",\"party\",\"pass\",\"patch\",\"path\",\"patient\",\"patrol\",\"pattern\",\"pause\",\"pave\",\"payment\",\"peace\",\"peanut\",\"pear\",\"peasant\",\"pelican\",\"pen\",\"penalty\",\"pencil\",\"people\",\"pepper\",\"perfect\",\"permit\",\"person\",\"pet\",\"phone\",\"photo\",\"phrase\",\"physical\",\"piano\",\"picnic\",\"picture\",\"piece\",\"pig\",\"pigeon\",\"pill\",\"pilot\",\"pink\",\"pioneer\",\"pipe\",\"pistol\",\"pitch\",\"pizza\",\"place\",\"planet\",\"plastic\",\"plate\",\"play\",\"please\",\"pledge\",\"pluck\",\"plug\",\"plunge\",\"poem\",\"poet\",\"point\",\"polar\",\"pole\",\"police\",\"pond\",\"pony\",\"pool\",\"popular\",\"portion\",\"position\",\"possible\",\"post\",\"potato\",\"pottery\",\"poverty\",\"powder\",\"power\",\"practice\",\"praise\",\"predict\",\"prefer\",\"prepare\",\"present\",\"pretty\",\"prevent\",\"price\",\"pride\",\"primary\",\"print\",\"priority\",\"prison\",\"private\",\"prize\",\"problem\",\"process\",\"produce\",\"profit\",\"program\",\"project\",\"promote\",\"proof\",\"property\",\"prosper\",\"protect\",\"proud\",\"provide\",\"public\",\"pudding\",\"pull\",\"pulp\",\"pulse\",\"pumpkin\",\"punch\",\"pupil\",\"puppy\",\"purchase\",\"purity\",\"purpose\",\"purse\",\"push\",\"put\",\"puzzle\",\"pyramid\",\"quality\",\"quantum\",\"quarter\",\"question\",\"quick\",\"quit\",\"quiz\",\"quote\",\"rabbit\",\"raccoon\",\"race\",\"rack\",\"radar\",\"radio\",\"rail\",\"rain\",\"raise\",\"rally\",\"ramp\",\"ranch\",\"random\",\"range\",\"rapid\",\"rare\",\"rate\",\"rather\",\"raven\",\"raw\",\"razor\",\"ready\",\"real\",\"reason\",\"rebel\",\"rebuild\",\"recall\",\"receive\",\"recipe\",\"record\",\"recycle\",\"reduce\",\"reflect\",\"reform\",\"refuse\",\"region\",\"regret\",\"regular\",\"reject\",\"relax\",\"release\",\"relief\",\"rely\",\"remain\",\"remember\",\"remind\",\"remove\",\"render\",\"renew\",\"rent\",\"reopen\",\"repair\",\"repeat\",\"replace\",\"report\",\"require\",\"rescue\",\"resemble\",\"resist\",\"resource\",\"response\",\"result\",\"retire\",\"retreat\",\"return\",\"reunion\",\"reveal\",\"review\",\"reward\",\"rhythm\",\"rib\",\"ribbon\",\"rice\",\"rich\",\"ride\",\"ridge\",\"rifle\",\"right\",\"rigid\",\"ring\",\"riot\",\"ripple\",\"risk\",\"ritual\",\"rival\",\"river\",\"road\",\"roast\",\"robot\",\"robust\",\"rocket\",\"romance\",\"roof\",\"rookie\",\"room\",\"rose\",\"rotate\",\"rough\",\"round\",\"route\",\"royal\",\"rubber\",\"rude\",\"rug\",\"rule\",\"run\",\"runway\",\"rural\",\"sad\",\"saddle\",\"sadness\",\"safe\",\"sail\",\"salad\",\"salmon\",\"salon\",\"salt\",\"salute\",\"same\",\"sample\",\"sand\",\"satisfy\",\"satoshi\",\"sauce\",\"sausage\",\"save\",\"say\",\"scale\",\"scan\",\"scare\",\"scatter\",\"scene\",\"scheme\",\"school\",\"science\",\"scissors\",\"scorpion\",\"scout\",\"scrap\",\"screen\",\"script\",\"scrub\",\"sea\",\"search\",\"season\",\"seat\",\"second\",\"secret\",\"section\",\"security\",\"seed\",\"seek\",\"segment\",\"select\",\"sell\",\"seminar\",\"senior\",\"sense\",\"sentence\",\"series\",\"service\",\"session\",\"settle\",\"setup\",\"seven\",\"shadow\",\"shaft\",\"shallow\",\"share\",\"shed\",\"shell\",\"sheriff\",\"shield\",\"shift\",\"shine\",\"ship\",\"shiver\",\"shock\",\"shoe\",\"shoot\",\"shop\",\"short\",\"shoulder\",\"shove\",\"shrimp\",\"shrug\",\"shuffle\",\"shy\",\"sibling\",\"sick\",\"side\",\"siege\",\"sight\",\"sign\",\"silent\",\"silk\",\"silly\",\"silver\",\"similar\",\"simple\",\"since\",\"sing\",\"siren\",\"sister\",\"situate\",\"six\",\"size\",\"skate\",\"sketch\",\"ski\",\"skill\",\"skin\",\"skirt\",\"skull\",\"slab\",\"slam\",\"sleep\",\"slender\",\"slice\",\"slide\",\"slight\",\"slim\",\"slogan\",\"slot\",\"slow\",\"slush\",\"small\",\"smart\",\"smile\",\"smoke\",\"smooth\",\"snack\",\"snake\",\"snap\",\"sniff\",\"snow\",\"soap\",\"soccer\",\"social\",\"sock\",\"soda\",\"soft\",\"solar\",\"soldier\",\"solid\",\"solution\",\"solve\",\"someone\",\"song\",\"soon\",\"sorry\",\"sort\",\"soul\",\"sound\",\"soup\",\"source\",\"south\",\"space\",\"spare\",\"spatial\",\"spawn\",\"speak\",\"special\",\"speed\",\"spell\",\"spend\",\"sphere\",\"spice\",\"spider\",\"spike\",\"spin\",\"spirit\",\"split\",\"spoil\",\"sponsor\",\"spoon\",\"sport\",\"spot\",\"spray\",\"spread\",\"spring\",\"spy\",\"square\",\"squeeze\",\"squirrel\",\"stable\",\"stadium\",\"staff\",\"stage\",\"stairs\",\"stamp\",\"stand\",\"start\",\"state\",\"stay\",\"steak\",\"steel\",\"stem\",\"step\",\"stereo\",\"stick\",\"still\",\"sting\",\"stock\",\"stomach\",\"stone\",\"stool\",\"story\",\"stove\",\"strategy\",\"street\",\"strike\",\"strong\",\"struggle\",\"student\",\"stuff\",\"stumble\",\"style\",\"subject\",\"submit\",\"subway\",\"success\",\"such\",\"sudden\",\"suffer\",\"sugar\",\"suggest\",\"suit\",\"summer\",\"sun\",\"sunny\",\"sunset\",\"super\",\"supply\",\"supreme\",\"sure\",\"surface\",\"surge\",\"surprise\",\"surround\",\"survey\",\"suspect\",\"sustain\",\"swallow\",\"swamp\",\"swap\",\"swarm\",\"swear\",\"sweet\",\"swift\",\"swim\",\"swing\",\"switch\",\"sword\",\"symbol\",\"symptom\",\"syrup\",\"system\",\"table\",\"tackle\",\"tag\",\"tail\",\"talent\",\"talk\",\"tank\",\"tape\",\"target\",\"task\",\"taste\",\"tattoo\",\"taxi\",\"teach\",\"team\",\"tell\",\"ten\",\"tenant\",\"tennis\",\"tent\",\"term\",\"test\",\"text\",\"thank\",\"that\",\"theme\",\"then\",\"theory\",\"there\",\"they\",\"thing\",\"this\",\"thought\",\"three\",\"thrive\",\"throw\",\"thumb\",\"thunder\",\"ticket\",\"tide\",\"tiger\",\"tilt\",\"timber\",\"time\",\"tiny\",\"tip\",\"tired\",\"tissue\",\"title\",\"toast\",\"tobacco\",\"today\",\"toddler\",\"toe\",\"together\",\"toilet\",\"token\",\"tomato\",\"tomorrow\",\"tone\",\"tongue\",\"tonight\",\"tool\",\"tooth\",\"top\",\"topic\",\"topple\",\"torch\",\"tornado\",\"tortoise\",\"toss\",\"total\",\"tourist\",\"toward\",\"tower\",\"town\",\"toy\",\"track\",\"trade\",\"traffic\",\"tragic\",\"train\",\"transfer\",\"trap\",\"trash\",\"travel\",\"tray\",\"treat\",\"tree\",\"trend\",\"trial\",\"tribe\",\"trick\",\"trigger\",\"trim\",\"trip\",\"trophy\",\"trouble\",\"truck\",\"true\",\"truly\",\"trumpet\",\"trust\",\"truth\",\"try\",\"tube\",\"tuition\",\"tumble\",\"tuna\",\"tunnel\",\"turkey\",\"turn\",\"turtle\",\"twelve\",\"twenty\",\"twice\",\"twin\",\"twist\",\"two\",\"type\",\"typical\",\"ugly\",\"umbrella\",\"unable\",\"unaware\",\"uncle\",\"uncover\",\"under\",\"undo\",\"unfair\",\"unfold\",\"unhappy\",\"uniform\",\"unique\",\"unit\",\"universe\",\"unknown\",\"unlock\",\"until\",\"unusual\",\"unveil\",\"update\",\"upgrade\",\"uphold\",\"upon\",\"upper\",\"upset\",\"urban\",\"urge\",\"usage\",\"use\",\"used\",\"useful\",\"useless\",\"usual\",\"utility\",\"vacant\",\"vacuum\",\"vague\",\"valid\",\"valley\",\"valve\",\"van\",\"vanish\",\"vapor\",\"various\",\"vast\",\"vault\",\"vehicle\",\"velvet\",\"vendor\",\"venture\",\"venue\",\"verb\",\"verify\",\"version\",\"very\",\"vessel\",\"veteran\",\"viable\",\"vibrant\",\"vicious\",\"victory\",\"video\",\"view\",\"village\",\"vintage\",\"violin\",\"virtual\",\"virus\",\"visa\",\"visit\",\"visual\",\"vital\",\"vivid\",\"vocal\",\"voice\",\"void\",\"volcano\",\"volume\",\"vote\",\"voyage\",\"wage\",\"wagon\",\"wait\",\"walk\",\"wall\",\"walnut\",\"want\",\"warfare\",\"warm\",\"warrior\",\"wash\",\"wasp\",\"waste\",\"water\",\"wave\",\"way\",\"wealth\",\"weapon\",\"wear\",\"weasel\",\"weather\",\"web\",\"wedding\",\"weekend\",\"weird\",\"welcome\",\"west\",\"wet\",\"whale\",\"what\",\"wheat\",\"wheel\",\"when\",\"where\",\"whip\",\"whisper\",\"wide\",\"width\",\"wife\",\"wild\",\"will\",\"win\",\"window\",\"wine\",\"wing\",\"wink\",\"winner\",\"winter\",\"wire\",\"wisdom\",\"wise\",\"wish\",\"witness\",\"wolf\",\"woman\",\"wonder\",\"wood\",\"wool\",\"word\",\"work\",\"world\",\"worry\",\"worth\",\"wrap\",\"wreck\",\"wrestle\",\"wrist\",\"write\",\"wrong\",\"yard\",\"year\",\"yellow\",\"you\",\"young\",\"youth\",\"zebra\",\"zero\",\"zone\",\"zoo\"\n    );\n}\n"
  },
  {
    "path": "src/peergos/server/util/PinnedHost.java",
    "content": "package peergos.server.util;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.net.Socket;\nimport java.net.UnknownHostException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic final class PinnedHost {\n    final String host;\n    volatile InetAddress[] addresses;\n    final AtomicInteger rr = new AtomicInteger();\n    long lastRefresh;\n\n    PinnedHost(String host) throws IOException {\n        this.host = host;\n        this.addresses = getReachableIPs();\n        if (addresses.length == 0)\n            throw new IllegalStateException(\"Couldn't reach any addresses from \" + Arrays.asList(addresses));\n\n        this.lastRefresh = System.nanoTime();\n    }\n\n    private InetAddress[] getReachableIPs() throws IOException {\n        InetAddress[] all = InetAddress.getAllByName(host);\n        List<InetAddress> reachable = new ArrayList<>();\n\n        for (InetAddress address : all) {\n            if (isReachable(address.getHostAddress(), 443, 1_000))\n                reachable.add(address);\n        }\n        Logging.LOG().info(\"Reachable IPs for \" + host + \": \" + reachable);\n        return reachable.toArray(InetAddress[]::new);\n    }\n\n    InetAddress next() {\n        InetAddress[] a = addresses;\n        return a[Math.floorMod(rr.getAndIncrement(), a.length)];\n    }\n\n    void ensureRefreshed() throws IOException {\n//            if (System.nanoTime() - lastRefresh > 60*60*1000_000_000L)\n//                refresh();\n    }\n\n    void refresh() throws IOException {\n        this.addresses = getReachableIPs();\n        if (addresses.length == 0)\n            throw new IllegalStateException(\"Couldn't reach any addresses for \" + host);\n        this.lastRefresh = System.nanoTime();\n    }\n\n    private static boolean isReachable(String addr, int openPort, int timeOutMillis) {\n        try (Socket soc = new Socket()) {\n            soc.connect(new InetSocketAddress(addr, openPort), timeOutMillis);\n            return true;\n        } catch (IOException ex) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/Postgres.java",
    "content": "package peergos.server.util;\n\nimport org.postgresql.Driver;\n\nimport java.sql.*;\nimport java.util.*;\n\npublic class Postgres {\n\n    public static Connection build(String host,\n                                   int port,\n                                   String database,\n                                   String user,\n                                   String password) throws SQLException {\n        Logging.LOG().warning(\"This postgres client is not using TLS you should be using a VPN to secure traffic!\");\n        Driver driver = new Driver();\n        Properties props = new Properties();\n        props.setProperty(\"user\", user);\n        props.setProperty(\"password\", password);\n        return driver.connect(\"jdbc:postgresql://\" + host + \":\" + port + \"/\" + database, props);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/RateMonitor.java",
    "content": "package peergos.server.util;\n\nimport java.util.*;\n\npublic class RateMonitor {\n\n    private long timeSteps = 0;\n    // ith element is # requests between 2^i and 2^(i+1) time steps ...\n    private final long[] buckets;\n\n    public RateMonitor(int nBuckets) {\n        this.buckets = new long[nBuckets];\n    }\n\n    public synchronized void addEvent() {\n        buckets[0]++;\n    }\n\n    private void moveBucket(int from, int to) {\n        if (to < buckets.length)\n            buckets[to] += buckets[from];\n        buckets[from] = 0;\n    }\n\n    public synchronized void timeStep() {\n        timeSteps++;\n        for (int i= buckets.length - 1; i >=0; i--) {\n            if (timeSteps % (1L << i) == 0) moveBucket(i, i + 1);\n        }\n    }\n\n    public synchronized void timeSteps(long steps) {\n        if (steps > 1L << buckets.length)\n            Arrays.fill(buckets, 0);\n        else\n            for (long i=0; i < steps; i++)\n                timeStep();\n    }\n\n    public synchronized long[] getRates() {\n        return Arrays.copyOfRange(buckets, 0, buckets.length);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/SlidingWindowCounter.java",
    "content": "package peergos.server.util;\n\nimport java.util.function.Supplier;\n\npublic class SlidingWindowCounter {\n\n    private final long windowSizeInSeconds;\n    private final long maxRequestsPerWindow;\n    private final Supplier<Long> clock;\n    private long currentWindowStart;\n    private long previousWindowTotal;\n    private long currentWindowTotal;\n\n    public SlidingWindowCounter(long windowSizeInSeconds,\n                                long maxRequestsPerWindow,\n                                Supplier<Long> clock) {\n        this.windowSizeInSeconds = windowSizeInSeconds;\n        this.maxRequestsPerWindow = maxRequestsPerWindow;\n        this.clock = clock;\n        this.currentWindowStart = clock.get();\n        this.previousWindowTotal = 0;\n        this.currentWindowTotal = 0;\n    }\n\n    public SlidingWindowCounter(long windowSizeInSeconds,\n                                long maxRequestsPerWindow) {\n        this(windowSizeInSeconds, maxRequestsPerWindow, () -> System.nanoTime() / 1_000_000_000);\n    }\n\n    public synchronized boolean allowRequest(long increment) {\n        long now = clock.get();\n        long timePassedInWindow = now - currentWindowStart;\n\n        if (timePassedInWindow >= 2 * windowSizeInSeconds) {\n            previousWindowTotal = 0;\n            currentWindowTotal = 0;\n            currentWindowStart = now;\n            timePassedInWindow = 0;\n        } else if (timePassedInWindow >= windowSizeInSeconds) {\n            previousWindowTotal = currentWindowTotal;\n            currentWindowTotal = 0;\n            currentWindowStart = currentWindowStart + windowSizeInSeconds;\n            timePassedInWindow -= windowSizeInSeconds;\n        }\n\n        double weightedCount = currentWindowTotal + previousWindowTotal *\n                ((windowSizeInSeconds - timePassedInWindow) / (double) windowSizeInSeconds);\n\n        if (weightedCount < maxRequestsPerWindow) {\n            currentWindowTotal += increment;\n            return true;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/Sqlite.java",
    "content": "package peergos.server.util;\n\nimport org.sqlite.*;\n\nimport java.io.IOException;\nimport java.sql.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class Sqlite {\n\n    public static Connection build(String dbPath) throws SQLException {\n        String url = \"jdbc:sqlite:\"+dbPath;\n        SQLiteDataSource dc = new SQLiteDataSource();\n        dc.setUrl(url);\n\n        Connection conn = dc.getConnection();\n        conn.setAutoCommit(true);\n        return conn;\n    }\n\n    public static String getDbPath(Args a, String type) {\n        String sqlFile = a.getArg(type);\n        return sqlFile.equals(\":memory:\") ? sqlFile : a.fromPeergosDir(type).toString();\n    }\n\n    public static class UncloseableConnection implements Connection {\n\n        private final Connection target;\n\n        public UncloseableConnection(Connection target) {\n            this.target = target;\n        }\n\n        public void closeTarget() throws SQLException {\n            target.close();\n        }\n\n        @Override\n        public Statement createStatement() throws SQLException {\n            return target.createStatement();\n        }\n\n        @Override\n        public PreparedStatement prepareStatement(String s) throws SQLException {\n            return target.prepareStatement(s);\n        }\n\n        @Override\n        public CallableStatement prepareCall(String s) throws SQLException {\n            return target.prepareCall(s);\n        }\n\n        @Override\n        public String nativeSQL(String s) throws SQLException {\n            return target.nativeSQL(s);\n        }\n\n        @Override\n        public void setAutoCommit(boolean b) throws SQLException {\n            target.setAutoCommit(b);\n        }\n\n        @Override\n        public boolean getAutoCommit() throws SQLException {\n            return target.getAutoCommit();\n        }\n\n        @Override\n        public void commit() throws SQLException {\n            target.commit();\n        }\n\n        @Override\n        public void rollback() throws SQLException {\n            target.rollback();\n        }\n\n        @Override\n        public void close() throws SQLException {\n            // Do nothing\n        }\n\n        @Override\n        public boolean isClosed() throws SQLException {\n            return false;\n        }\n\n        @Override\n        public DatabaseMetaData getMetaData() throws SQLException {\n            return target.getMetaData();\n        }\n\n        @Override\n        public void setReadOnly(boolean b) throws SQLException {\n            target.setReadOnly(b);\n        }\n\n        @Override\n        public boolean isReadOnly() throws SQLException {\n            return target.isReadOnly();\n        }\n\n        @Override\n        public void setCatalog(String s) throws SQLException {\n            target.setCatalog(s);\n        }\n\n        @Override\n        public String getCatalog() throws SQLException {\n            return target.getCatalog();\n        }\n\n        @Override\n        public void setTransactionIsolation(int i) throws SQLException {\n            target.setTransactionIsolation(i);\n        }\n\n        @Override\n        public int getTransactionIsolation() throws SQLException {\n            return target.getTransactionIsolation();\n        }\n\n        @Override\n        public SQLWarning getWarnings() throws SQLException {\n            return target.getWarnings();\n        }\n\n        @Override\n        public void clearWarnings() throws SQLException {\n            target.clearWarnings();\n        }\n\n        @Override\n        public Statement createStatement(int i, int i1) throws SQLException {\n            return target.createStatement(i, i1);\n        }\n\n        @Override\n        public PreparedStatement prepareStatement(String s, int i, int i1) throws SQLException {\n            return target.prepareStatement(s, i, i1);\n        }\n\n        @Override\n        public CallableStatement prepareCall(String s, int i, int i1) throws SQLException {\n            return target.prepareCall(s, i, i1);\n        }\n\n        @Override\n        public Map<String, Class<?>> getTypeMap() throws SQLException {\n            return target.getTypeMap();\n        }\n\n        @Override\n        public void setTypeMap(Map<String, Class<?>> map) throws SQLException {\n            target.setTypeMap(map);\n        }\n\n        @Override\n        public void setHoldability(int i) throws SQLException {\n            target.setHoldability(i);\n        }\n\n        @Override\n        public int getHoldability() throws SQLException {\n            return target.getHoldability();\n        }\n\n        @Override\n        public Savepoint setSavepoint() throws SQLException {\n            return target.setSavepoint();\n        }\n\n        @Override\n        public Savepoint setSavepoint(String s) throws SQLException {\n            return target.setSavepoint(s);\n        }\n\n        @Override\n        public void rollback(Savepoint savepoint) throws SQLException {\n            target.rollback(savepoint);\n        }\n\n        @Override\n        public void releaseSavepoint(Savepoint savepoint) throws SQLException {\n            target.releaseSavepoint(savepoint);\n        }\n\n        @Override\n        public Statement createStatement(int i, int i1, int i2) throws SQLException {\n            return target.createStatement(i, i1, i2);\n        }\n\n        @Override\n        public PreparedStatement prepareStatement(String s, int i, int i1, int i2) throws SQLException {\n            return target.prepareStatement(s, i, i1, i2);\n        }\n\n        @Override\n        public CallableStatement prepareCall(String s, int i, int i1, int i2) throws SQLException {\n            return target.prepareCall(s, i, i1, i2);\n        }\n\n        @Override\n        public PreparedStatement prepareStatement(String s, int i) throws SQLException {\n            return target.prepareStatement(s, i);\n        }\n\n        @Override\n        public PreparedStatement prepareStatement(String s, int[] ints) throws SQLException {\n            return target.prepareStatement(s, ints);\n        }\n\n        @Override\n        public PreparedStatement prepareStatement(String s, String[] strings) throws SQLException {\n            return target.prepareStatement(s, strings);\n        }\n\n        @Override\n        public Clob createClob() throws SQLException {\n            return target.createClob();\n        }\n\n        @Override\n        public Blob createBlob() throws SQLException {\n            return target.createBlob();\n        }\n\n        @Override\n        public NClob createNClob() throws SQLException {\n            return target.createNClob();\n        }\n\n        @Override\n        public SQLXML createSQLXML() throws SQLException {\n            return target.createSQLXML();\n        }\n\n        @Override\n        public boolean isValid(int i) throws SQLException {\n            return target.isValid(i);\n        }\n\n        @Override\n        public void setClientInfo(String s, String s1) throws SQLClientInfoException {\n            target.setClientInfo(s, s1);\n        }\n\n        @Override\n        public void setClientInfo(Properties properties) throws SQLClientInfoException {\n            target.setClientInfo(properties);\n        }\n\n        @Override\n        public String getClientInfo(String s) throws SQLException {\n            return target.getClientInfo(s);\n        }\n\n        @Override\n        public Properties getClientInfo() throws SQLException {\n            return target.getClientInfo();\n        }\n\n        @Override\n        public Array createArrayOf(String s, Object[] objects) throws SQLException {\n            return target.createArrayOf(s, objects);\n        }\n\n        @Override\n        public Struct createStruct(String s, Object[] objects) throws SQLException {\n            return target.createStruct(s, objects);\n        }\n\n        @Override\n        public void setSchema(String s) throws SQLException {\n            target.setSchema(s);\n        }\n\n        @Override\n        public String getSchema() throws SQLException {\n            return target.getSchema();\n        }\n\n        @Override\n        public void abort(Executor executor) throws SQLException {\n            target.abort(executor);\n        }\n\n        @Override\n        public void setNetworkTimeout(Executor executor, int i) throws SQLException {\n            target.setNetworkTimeout(executor, i);\n        }\n\n        @Override\n        public int getNetworkTimeout() throws SQLException {\n            return target.getNetworkTimeout();\n        }\n\n        @Override\n        public <T> T unwrap(Class<T> aClass) throws SQLException {\n            return target.unwrap(aClass);\n        }\n\n        @Override\n        public boolean isWrapperFor(Class<?> aClass) throws SQLException {\n            return target.isWrapperFor(aClass);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/Threads.java",
    "content": "package peergos.server.util;\n\nimport java.util.concurrent.*;\n\npublic class Threads {\n\n    private static ForkJoinPool.ForkJoinWorkerThreadFactory getThreadFactory(String namePrefix) {\n        return new ForkJoinPool.ForkJoinWorkerThreadFactory() {\n            @Override\n            public ForkJoinWorkerThread newThread(ForkJoinPool pool) {\n                final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);\n                worker.setName(namePrefix + worker.getPoolIndex());\n                return worker;\n            }\n        };\n    }\n\n    public static ExecutorService newPool(int threads, String threadNamePrefix) {\n        boolean isAndroid = \"The Android Project\".equals(System.getProperty(\"java.vm.vendor\"));\n        if (isAndroid)\n            return newFJPool(threads, threadNamePrefix);\n        else {\n            try {\n                return Executors.newVirtualThreadPerTaskExecutor();\n            } catch (Throwable t) {\n                return newFJPool(threads, threadNamePrefix);\n            }\n        }\n    }\n\n    public static ForkJoinPool newFJPool(int threads, String threadNamePrefix) {\n        return new ForkJoinPool(threads, getThreadFactory(threadNamePrefix), new Thread.UncaughtExceptionHandler() {\n            @Override\n            public void uncaughtException(Thread thread, Throwable t) {\n                t.printStackTrace();\n            }\n        }, true);\n    }\n\n    public static void sleep(long millis) {\n        try {\n            Thread.sleep(millis);\n        } catch (InterruptedException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/util/TimeLimited.java",
    "content": "package peergos.server.util;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic class TimeLimited {\n\n    /**\n     *\n     * @param signedTime\n     * @param durationSeconds\n     * @param ipfs\n     * @param owner\n     * @return The time in milliseconds UTC that was signed and valid\n     */\n    public static long isAllowedTime(byte[] signedTime, int durationSeconds, ContentAddressedStorage ipfs, PublicKeyHash owner) {\n        try {\n            Optional<PublicSigningKey> ownerOpt = ipfs.getSigningKey(owner, owner).join();\n            if (! ownerOpt.isPresent())\n                throw new IllegalStateException(\"Couldn't retrieve owner key!\");\n            return isAllowedTime(signedTime, durationSeconds, ownerOpt.get());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static long isAllowedTime(byte[] signedTime, int durationSeconds, PublicSigningKey pubKey) {\n        byte[] raw = pubKey.unsignMessage(signedTime).join();\n        CborObject cbor = CborObject.fromByteArray(raw);\n        if (! (cbor instanceof CborObject.CborLong))\n            throw new IllegalStateException(\"Invalid cbor for time in authorisation!\");\n        long utcMillis = ((CborObject.CborLong) cbor).value;\n        long now = System.currentTimeMillis();\n        if (Math.abs(now - utcMillis) > durationSeconds * 1_000)\n            throw new IllegalStateException(\"Stale auth time, is your clock accurate?\");\n        // This is a valid request\n        return utcMillis;\n    }\n\n    /**\n     *\n     * @param expectedPath\n     * @param signedReq\n     * @param durationSeconds\n     * @param ipfs\n     * @param owner\n     * @return The time in milliseconds UTC that was signed and valid\n     */\n    public static long isAllowed(String expectedPath, byte[] signedReq, int durationSeconds, ContentAddressedStorage ipfs, PublicKeyHash owner) {\n        try {\n            Optional<PublicSigningKey> ownerOpt = ipfs.getSigningKey(owner, owner).join();\n            if (! ownerOpt.isPresent())\n                throw new IllegalStateException(\"Couldn't retrieve owner key!\");\n            byte[] raw = ownerOpt.get().unsignMessage(signedReq).join();\n            CborObject cbor = CborObject.fromByteArray(raw);\n\n            TimeLimitedClient.SignedRequest req = TimeLimitedClient.SignedRequest.fromCbor(cbor);\n            long utcMillis = req.createdEpochMillis;\n            long now = System.currentTimeMillis();\n            if (Math.abs(now - utcMillis) > durationSeconds * 1_000)\n                throw new IllegalStateException(\"Stale auth time, is your clock accurate?\");\n            if (! expectedPath.equals(req.path))\n                throw new IllegalStateException(\"Illegal path for signed request: \" + req.path);\n            // This is a valid request\n            return utcMillis;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/WebdavFileSystem.java",
    "content": "/*\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n */\npackage peergos.server.webdav;\n\nimport org.peergos.util.Pair;\nimport peergos.server.UserService;\nimport peergos.server.net.ProxyChooser;\nimport peergos.server.util.Logging;\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.Builder;\nimport peergos.server.Main;\nimport peergos.shared.Crypto;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.storage.RetryStorage;\nimport peergos.shared.user.UserContext;\nimport peergos.shared.user.fs.AsyncReader;\nimport peergos.shared.user.fs.FileWrapper;\nimport peergos.shared.user.fs.HashTree;\nimport peergos.shared.user.fs.HashTreeBuilder;\nimport peergos.shared.util.PathUtil;\nimport peergos.shared.util.Futures;\n\nimport java.io.*;\nimport java.net.ProxySelector;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.security.Principal;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\n\n/**\n * Integration with Peergos File system\n * This class was based on org.modeshape.webdav.LocalFileSystemStore\n * which in turn was based on reference Implementation of WebdavStore\n * Originally by:\n * @author joa\n * @author re\n * @author hchiorea@redhat.com\n */\npublic class WebdavFileSystem implements IWebdavStore {\n\n    private static final Logger LOG = Logging.LOG();\n\n    private final UserContext context;\n\n    public WebdavFileSystem(String username, String password, String peergosUrl) {\n        Crypto crypto = Main.initCrypto();\n        try {\n            NetworkAccess unbuffered = Builder.buildJavaNetworkAccess(new URL(peergosUrl), peergosUrl.startsWith(\"https\"), Optional.of(\"Peergos-\" + UserService.CURRENT_VERSION + \"-webdav\"), Optional.empty()).join();\n            NetworkAccess network = NetworkAccess.buildBuffered(new RetryStorage(unbuffered.dhtClient, 5), unbuffered.batCave, unbuffered.coreNode, unbuffered.account,\n                    unbuffered.mutable, 5_000, unbuffered.social, unbuffered.instanceAdmin, unbuffered.spaceUsage, unbuffered.serverMessager,\n                    crypto.hasher, Collections.emptyList(), false);\n            context = UserContext.signIn(username, password, Main::getMfaResponseCLI, network, crypto).join();\n        } catch (Exception ex) {\n            LOG.log(Level.WARNING, ex, () -> \"Unable to connect to Peergos account\");\n            throw new IllegalStateException(\"Unable to connect to Peergos account: \", ex);\n        }\n    }\n\n    @Override\n    public void destroy() {\n    }\n\n    @Override\n    public ITransaction begin(Principal principal ) throws WebdavException {\n        LOG.fine(\"PeergosFileSystem.begin()\");\n        return null;\n    }\n\n    @Override\n    public void checkAuthentication( ITransaction transaction ) throws SecurityException {\n        LOG.fine(\"PeergosFileSystem.checkAuthentication()\");\n    }\n\n    @Override\n    public void commit( ITransaction transaction ) throws WebdavException {\n        LOG.fine(\"PeergosFileSystem.commit()\");\n    }\n\n    @Override\n    public void rollback( ITransaction transaction ) throws WebdavException {\n        LOG.fine(\"PeergosFileSystem.rollback()\");\n    }\n\n    private Optional<FileWrapper> getByPath(Path path) {\n        return context.getByPath(path.toString().replace('\\\\', '/')).join();\n    }\n\n    @Override\n    public void createFolder( ITransaction transaction,\n                              String uri ) throws WebdavException {\n        LOG.fine(\"PeergosFileSystem.createFolder(\" + uri + \")\");\n        Path path = new File(uri).toPath();\n        Optional<FileWrapper> parentFolder = getByPath(path.getParent());\n        if (parentFolder.isEmpty() || parentFolder.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find parent of folder: \" + uri);\n        }\n        try {\n            parentFolder.get().mkdir(path.getFileName().toString(), context.network, false, context.mirrorBatId(), context.crypto).join();\n        } catch (Exception ex) {\n            LOG.log(Level.WARNING, ex, () -> \"cannot create folder\");\n            throw new WebdavException(\"cannot create folder: \" + uri);\n        }\n    }\n\n    @Override\n    public void createResource( ITransaction transaction,\n                                String uri ) throws WebdavException {\n        LOG.fine(\"PeergosFileSystem.createResource(\" + uri + \")\");\n        Path path = new File(uri).toPath();\n        if (path.getFileName().toString().startsWith(\"._\") || path.getFileName().toString().equals(\".DS_Store\")) { // MacOS rubbish!\n            return;\n        }\n        Optional<FileWrapper> parentFolder = getByPath(path.getParent());\n        if (parentFolder.isEmpty() || parentFolder.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find parent of file: \" + uri);\n        }\n        byte[] contents = new byte[0];\n        try {\n            HashTreeBuilder hash = new HashTreeBuilder(0);\n            hash.setChunk(0, contents, context.crypto.hasher).join();\n            HashTree h = hash.complete(context.crypto.hasher).join();\n            parentFolder.get().uploadFileWithHash(path.getFileName().toString(), new AsyncReader.ArrayBacked(contents),\n                    contents.length, Optional.of(h), Optional.empty(), Optional.empty(), context.network, context.crypto, l -> {}).join();\n        } catch (Exception e) {\n            LOG.warning(\"PeergosFileSystem.createResource(\" + uri + \") failed\");\n            throw new WebdavException(e);\n        }\n    }\n\n    @Override\n    public long setResourceContent( ITransaction transaction,\n                                    String uri,\n                                    Pair<AsyncReader, Long> readerPair,\n                                    String contentType,\n                                    String characterEncoding ) throws WebdavException {\n\n        LOG.fine(\"PeergosFileSystem.setResourceContent(\" + uri + \")\");\n        Path path = new File(uri).toPath();\n        if (path.getFileName().toString().startsWith(\"._\") || path.getFileName().toString().equals(\".DS_Store\")) { // MacOS rubbish!\n            return 0;\n        }\n        Optional<FileWrapper> parentFolder = getByPath(path.getParent());\n        if (parentFolder.isEmpty() || parentFolder.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find parent of file: \" + uri);\n        }\n        try {\n            parentFolder.get().uploadOrReplaceFile(path.getFileName().toString(), readerPair.left, readerPair.right, context.network, context.crypto, () -> false, l -> {}).join();\n            return readerPair.right;\n        } catch (Exception e) {\n            LOG.warning(\"PeergosFileSystem.setResourceContent(\" + uri + \") failed\");\n            throw new WebdavException(e);\n        }\n    }\n\n    @Override\n    public void moveResource( ITransaction transaction,\n                              String sourcePath,\n                              String destPath) throws WebdavException {\n\n        LOG.fine(\"PeergosFileSystem.moveResource(\" + sourcePath + \")\");\n        Path path = PathUtil.get(sourcePath);\n        if (path.getFileName().toString().startsWith(\"._\") || path.getFileName().toString().equals(\".DS_Store\")) { // MacOS rubbish!\n            return;\n        }\n        Optional<FileWrapper> sourceFile = getByPath(path);\n        if (sourceFile.isEmpty() || sourceFile.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find source file: \" + sourcePath);\n        }\n        Optional<FileWrapper> parent = getByPath(path.getParent());\n        if (parent.isEmpty() || parent.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find source parent: \" + sourcePath);\n        }\n        Path targetPath = PathUtil.get(destPath);\n        Path targetParentPath = targetPath.getParent();\n        Optional<FileWrapper> targetParent = getByPath(targetParentPath);\n        if (targetParent.isEmpty() || targetParent.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find target dir: \" + destPath);\n        }\n        try {\n            if (targetParentPath.toString().equals(path.getParent().toString())) { // rename\n                sourceFile.get().rename(targetPath.getFileName().toString(), targetParent.get(), path, context).join();\n            } else {\n                sourceFile.get().moveTo(targetParent.get(), parent.get(), PathUtil.get(sourcePath), context, () -> Futures.of(true)).join();\n            }\n        } catch (Exception e) {\n            LOG.warning(\"PeergosFileSystem.setResourceContent(\" + sourcePath + \") failed\");\n            throw new WebdavException(e);\n        }\n    }\n\n    @Override\n    public String[] getChildrenNames( ITransaction transaction,\n                                      String uri ) throws WebdavException {\n        LOG.fine(\"PeergosFileSystem.getChildrenNames(\" + uri + \")\");\n        Path path = new File(uri).toPath();\n        Optional<FileWrapper> folder = getByPath(path);\n        if (folder.isEmpty() || !folder.get().isDirectory() || Optional.ofNullable(folder.get().getFileProperties()).map(p -> p.isHidden).orElse(false)) {\n            return new String[0];\n        }\n        Set<FileWrapper> children = folder.get().getChildren(context.crypto.hasher, context.network).join();\n        List<String> filenames = children.stream()\n                .filter(f -> !f.getFileProperties().isHidden)\n                .map(f -> f.getName()).collect(Collectors.toList());\n        String[] childrenNames = new String[filenames.size()];\n        return filenames.toArray(childrenNames);\n    }\n\n    @Override\n    public void removeObject( ITransaction transaction,\n                              String uri ) throws WebdavException {\n        Path path = new File(uri).toPath();\n        Optional<FileWrapper> fw = getByPath(path);\n        if (fw.isEmpty() || fw.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find: \" + uri);\n        }\n        Optional<FileWrapper> parentFolder = getByPath(path.getParent());\n        if (parentFolder.isEmpty()) {\n            throw new WebdavException(\"cannot find parent folder of: \" + uri);\n        }\n        try {\n            fw.get().remove(parentFolder.get(), path, context).join();\n        } catch (Exception ex) {\n            throw new WebdavException(\"cannot delete object: \" + uri);\n        }\n    }\n\n    @Override\n    public Pair<AsyncReader, Long> getResourceContent(ITransaction transaction,\n                                                      String uri ) throws WebdavException {\n        LOG.fine(\"PeergosFileSystem.getResourceContent(\" + uri + \")\");\n        Path path = new File(uri).toPath();\n        Optional<FileWrapper> fw = context.getByPath(path.toString()).join();\n        if (fw.isEmpty() || fw.get().isDirectory() || fw.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find file: \" + uri);\n        }\n        try {\n            return new Pair<>(fw.get().getInputStream(context.network, context.crypto,\n                    fw.get().getSize(), l-> {}).join(), fw.get().getSize());\n        } catch (Exception e) {\n            LOG.warning(\"PeergosFileSystem.getResourceContent(\" + uri + \") failed\");\n            throw new WebdavException(e);\n        }\n    }\n\n    @Override\n    public long getResourceLength( ITransaction transaction,\n                                   String uri) throws WebdavException {\n        Path path = new File(uri).toPath();\n        Optional<FileWrapper> fw = context.getByPath(path.toString()).join();\n        if (fw.isEmpty() || fw.get().isDirectory() || fw.get().getFileProperties().isHidden) {\n            throw new WebdavException(\"cannot find file: \" + uri);\n        }\n        return fw.get().getFileProperties().size;\n    }\n\n    @Override\n    public StoredObject getStoredObject(ITransaction transaction,\n                                        String uri ) {\n        StoredObject so = null;\n        Path path = new File(uri).toPath();\n        Optional<FileWrapper> fwOpt = context.getByPath(path.toString()).join();\n        if (fwOpt.isPresent() && fwOpt.get().isRoot()) {\n            so = new StoredObject();\n            so.setFolder(true);\n            so.setLastModified(new Date(0));\n            so.setCreationDate(new Date(0));\n            so.setResourceLength(0);\n        } else if (fwOpt.isPresent() && !fwOpt.get().getFileProperties().isHidden) {\n            so = new StoredObject();\n            FileWrapper fw = fwOpt.get();\n            so.setFolder(fw.isDirectory());\n            so.setLastModified(new Date(fw.getFileProperties().modified.toEpochSecond(ZoneOffset.UTC) * 1000));\n            so.setCreationDate(new Date(fw.getFileProperties().created.toEpochSecond(ZoneOffset.UTC) * 1000));\n            so.setResourceLength(fw.getFileProperties().size);\n        }\n        return so;\n    }\n\n    @Override\n    public Map<String, String> setCustomProperties( ITransaction transaction,\n                                                    String resourceUri,\n                                                    Map<String, Object> propertiesToSet,\n                                                    List<String> propertiesToRemove ) {\n        LOG.fine(\"PeergosFileSystem.setCustomProperties(\" + resourceUri + \")\");\n        return null;\n    }\n\n    @Override\n    public Map<String, Object> getCustomProperties( ITransaction transaction,\n                                                    String resourceUri ) {\n        LOG.fine(\"PeergosFileSystem.getCustomProperties(\" + resourceUri + \")\");\n        return Collections.emptyMap();\n    }\n\n    @Override\n    public Map<String, String> getCustomNamespaces( ITransaction transaction,\n                                                    String resourceUri ) {\n        return Collections.emptyMap();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/WebdavServer.java",
    "content": "package peergos.server.webdav;\n\nimport org.eclipse.jetty.security.ConstraintMapping;\nimport org.eclipse.jetty.security.ConstraintSecurityHandler;\nimport org.eclipse.jetty.security.HashLoginService;\nimport org.eclipse.jetty.security.UserStore;\nimport org.eclipse.jetty.security.authentication.BasicAuthenticator;\nimport org.eclipse.jetty.security.authentication.DigestAuthenticator;\nimport org.eclipse.jetty.server.Connector;\nimport org.eclipse.jetty.server.Server;\nimport org.eclipse.jetty.server.ServerConnector;\nimport org.eclipse.jetty.servlet.ServletContextHandler;\nimport org.eclipse.jetty.servlet.ServletHolder;\nimport org.eclipse.jetty.util.security.Constraint;\nimport org.eclipse.jetty.util.security.Password;\nimport peergos.server.Main;\nimport peergos.server.user.JavaImageThumbnailer;\nimport peergos.server.util.Logging;\nimport peergos.server.webdav.modeshape.webdav.WebdavServlet;\nimport peergos.server.util.Args;\nimport peergos.shared.Crypto;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.user.fs.ThumbnailGenerator;\n\nimport java.util.Collections;\nimport java.util.Optional;\nimport java.util.logging.Logger;\n\npublic class WebdavServer {\n\n    private static final String VERSION= \"0.1\";\n    private static final Logger logger = Logging.LOG();\n\n    public static Server start(Args args) {\n        int port = args.getInt(\"webdav.port\", 8090);\n        logger.info( \"Starting WEBDAV server version: \" + VERSION + \" on port: \" + port);\n        Crypto crypto = Main.initCrypto();\n        PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, crypto.signer);\n        ThumbnailGenerator.setInstance(new JavaImageThumbnailer());\n        Server server = new Server();\n        ServerConnector connector = new ServerConnector(server);\n        connector.setPort(port);\n        server.setConnectors(new Connector[] {connector});\n\n        String webdavUser = args.getArg(\"webdav.username\");\n        String webdavPWD = args.getArg(\"PEERGOS_WEBDAV_PASSWORD\");\n        String username = args.getArg(\"username\");\n        String password = args.getArg(\"PEERGOS_PASSWORD\");\n        Optional<String> authorization = args.getOptionalArg(\"webdav.authorization.scheme\");\n\n        //info from:\n        //https://stackoverflow.com/questions/44263651/hashloginservice-and-jetty9\n        //https://git.eclipse.org/c/jetty/org.eclipse.jetty.project.git/tree/examples/embedded/src/main/java/org/eclipse/jetty/embedded/SecuredHelloHandler.java\n        HashLoginService loginService = new HashLoginService(\"MyRealm\");\n        UserStore userStore = new UserStore();\n        userStore.addUser(webdavUser, new Password(webdavPWD), new String[] { \"user\"});\n        loginService.setUserStore(userStore);\n        server.addBean(loginService);\n\n        ConstraintSecurityHandler security = new ConstraintSecurityHandler();\n        server.setHandler(security);\n\n        Constraint constraint = new Constraint();\n        constraint.setName(\"auth\");\n        constraint.setAuthenticate(true);\n        constraint.setRoles(new String[] { \"user\"});\n\n        ConstraintMapping mapping = new ConstraintMapping();\n        mapping.setPathSpec(\"/*\");\n        mapping.setConstraint(constraint);\n\n        security.setConstraintMappings(Collections.singletonList(mapping));\n        if (authorization.isEmpty() || authorization.get().toLowerCase().equals(\"digest\")) {\n            logger.info( \"Using DIGEST authorization\");\n            security.setAuthenticator(new DigestAuthenticator());\n        } else if(authorization.get().toLowerCase().equals(\"basic\")) {\n            logger.info( \"Using BASIC authorization\");\n            security.setAuthenticator(new BasicAuthenticator());\n        } else {\n            throw new RuntimeException(\"Unknown authorization scheme:\" + authorization.get());\n        }\n        security.setLoginService(loginService);\n\n        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);\n        context.setContextPath(\"/\");\n        security.setHandler(context);\n\n        ServletHolder holderDef = new ServletHolder(\"default\", new WebdavServlet(username, password, args.getArg(\"peergos-url\")));\n        holderDef.setInitParameter(\"rootpath\",\"\");\n        context.addServlet(holderDef,\"/*\");\n\n        try {\n            server.start();\n            System.out.println(\"Webdav bridge started and ready to use at localhost:\" + port);\n            server.join();\n            return server;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/IMethodExecutor.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav;\n\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic interface IMethodExecutor {\n\n    void execute( ITransaction transaction,\n                  HttpServletRequest req,\n                  HttpServletResponse resp ) throws IOException, LockFailedException;\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/IMimeTyper.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav;\n\npublic interface IMimeTyper {\n\n    /**\n     * Detect the mime type of this object\n     * \n     * @param transaction\n     * @param path\n     * @return the MIME type\n     */\n    String getMimeType( ITransaction transaction,\n                        String path );\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/ITransaction.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav;\n\nimport java.security.Principal;\n\npublic interface ITransaction {\n\n    Principal getPrincipal();\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/IWebdavStore.java",
    "content": "/*\n * Copyright 2004 The Apache Software Foundation\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n */\n\npackage peergos.server.webdav.modeshape.webdav;\n\nimport org.peergos.util.Pair;\nimport peergos.server.simulation.InputStreamAsyncReader;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.shared.user.fs.AsyncReader;\n\nimport java.io.InputStream;\nimport java.security.Principal;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Interface for simple implementation of any store for the WebdavServlet\n * <p>\n * based on the BasicWebdavStore from Oliver Zeigermann, that was part of the Webdav Construcktion Kit from slide\n */\npublic interface IWebdavStore {\n\n    /**\n     * Life cycle method, called by WebdavServlet's destroy() method. Should be used to clean up resources.\n     */\n    void destroy();\n\n    /**\n     * Indicates that a new request or transaction with this store involved has been started. The request will be terminated by\n     * either {@link #commit(ITransaction)} or {@link #rollback(ITransaction)}. If only non-read methods have been called, the\n     * request will be terminated by a {@link #commit(ITransaction)}. This method will be called by (@link WebdavStoreAdapter} at\n     * the beginning of each request.\n     * \n     * @param principal the principal that started this request or <code>null</code> if there is non available\n     * @throws WebdavException\n     * @return a new {@link ITransaction transaction}\n     */\n    ITransaction begin( Principal principal );\n\n    /**\n     * Checks if authentication information passed in is valid. If not throws an exception.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     */\n    void checkAuthentication( ITransaction transaction );\n\n    /**\n     * Indicates that all changes done inside this request shall be made permanent and any transactions, connections and other\n     * temporary resources shall be terminated.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    void commit( ITransaction transaction );\n\n    /**\n     * Indicates that all changes done inside this request shall be undone and any transactions, connections and other temporary\n     * resources shall be terminated.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    void rollback( ITransaction transaction );\n\n    /**\n     * Creates a folder at the position specified by <code>folderUri</code>.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param folderUri URI of the folder\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    void createFolder( ITransaction transaction,\n                       String folderUri );\n\n    /**\n     * Creates a content resource at the position specified by <code>resourceUri</code>.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param resourceUri URI of the content resource\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    void createResource( ITransaction transaction,\n                         String resourceUri );\n\n    /**\n     * Gets the content of the resource specified by <code>resourceUri</code>.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param resourceUri URI of the content resource\n     * @return input stream you can read the content of the resource from\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    Pair<AsyncReader, Long> getResourceContent(ITransaction transaction,\n                                               String resourceUri );\n\n    /**\n     * Sets / stores the content of the resource specified by <code>resourceUri</code>.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param resourceUri URI of the resource where the content will be stored\n     * @param content input stream from which the content will be read from\n     * @param contentType content type of the resource or <code>null</code> if unknown\n     * @param characterEncoding character encoding of the resource or <code>null</code> if unknown or not applicable\n     * @return lenght of resource\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    long setResourceContent( ITransaction transaction,\n                             String resourceUri,\n                             Pair<AsyncReader, Long> readerPair,\n                             String contentType,\n                             String characterEncoding );\n\n    /**\n     *\n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param sourcePath URI of the resource where the source content is\n     * @param destPath URI of the resource where the content will be moved to\n     * @throws WebdavException\n     */\n    void moveResource( ITransaction transaction,\n                       String sourcePath,\n                       String destPath) throws WebdavException;\n\n    /**\n     * Gets the names of the children of the folder specified by <code>folderUri</code>.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param folderUri URI of the folder\n     * @return a (possibly empty) list of children, or <code>null</code> if the uri points to a file\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    String[] getChildrenNames( ITransaction transaction,\n                               String folderUri );\n\n    /**\n     * Gets the length of the content resource specified by <code>resourceUri</code>.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param resourceUri URI of the resource for which the length should be retrieved\n     * @return length of the resource in bytes, <code>-1</code> declares this value as invalid and asks the adapter to try to set\n     *         it from the properties if possible\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    long getResourceLength( ITransaction transaction,\n                            String resourceUri );\n\n    /**\n     * Removes the object specified by <code>uri</code>.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param uri URI of the object, i.e. content resource or folder\n     * @throws WebdavException if something goes wrong on the store level\n     */\n    void removeObject( ITransaction transaction,\n                       String uri );\n\n    /**\n     * Gets the storedObject specified by <code>uri</code>\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param uri URI\n     * @return StoredObject\n     */\n    StoredObject getStoredObject( ITransaction transaction,\n                                  String uri );\n\n    /**\n     * Updates the custom properties on the given resource. NOTE: Nested properties are <b>not</b> supported\n     * \n     * @param transaction the {@link peergos.server.webdav.modeshape.webdav.ITransaction} within which the operation takes place; may not be null\n     * @param resourceUri the URI of the object on which the properties should be updated; may not be null\n     * @param propertiesToSet a map of (propertyName, propertyValue) pairs which should be set on the object; may not be null; If\n     *        the name of a property contains a namespace, it is expected to be in the [namespaceUri]:[localPropertyName] format.\n     * @param propertiesToRemove a set of property name representing the properties which should be removed\n     * @return a Map of (property name, error message) for the properties which could not changed (either set or removed). If the\n     *         operation was successful, this may be null.\n     */\n    Map<String, String> setCustomProperties( ITransaction transaction,\n                                             String resourceUri,\n                                             Map<String, Object> propertiesToSet,\n                                             List<String> propertiesToRemove );\n\n    /**\n     * Returns the map of (propertyName, propertyValue) of custom properties of the given resource. NOTE: Nested properties are\n     * <b>not</b> supported\n     * \n     * @param transaction the {@link ITransaction} within which the operation takes place; may not be null\n     * @param resourceUri the URI of the object on which the properties should be updated; may not be null\n     * @return a Map of (property name, property value) pairs; may not be null; if the property name is namespace-aware it is\n     *         expected to have the [namespaceUri]:[localPropertyName] format\n     */\n    Map<String, Object> getCustomProperties( ITransaction transaction,\n                                             String resourceUri );\n\n    /**\n     * Returns a map of custom namespaces that are specific to the store.\n     * \n     * @param transaction the {@link ITransaction} within which the operation takes place; may not be null\n     * @param resourceUri resourceUri the URI of the object on which the properties should be updated; may not be null\n     * @return a Map of (namespaceUri, namespacePrefix) pairs;may not be null;\n     */\n    Map<String, String> getCustomNamespaces( ITransaction transaction,\n                                             String resourceUri );\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/StoredObject.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav;\n\nimport java.util.Date;\n\npublic class StoredObject {\n\n    private boolean isFolder;\n    private Date lastModified;\n    private Date creationDate;\n    private long contentLength;\n    private String mimeType;\n\n    private boolean isNullRessource;\n\n    /**\n     * Determines whether the StoredObject is a folder or a resource\n     * \n     * @return true if the StoredObject is a collection\n     */\n    public boolean isFolder() {\n        return (isFolder);\n    }\n\n    /**\n     * Determines whether the StoredObject is a folder or a resource\n     * \n     * @return true if the StoredObject is a resource\n     */\n    public boolean isResource() {\n        return (!isFolder);\n    }\n\n    /**\n     * Sets a new StoredObject as a collection or resource\n     * \n     * @param f true - collection ; false - resource\n     */\n    public void setFolder( boolean f ) {\n        this.isFolder = f;\n    }\n\n    /**\n     * Gets the date of the last modification\n     * \n     * @return last modification Date\n     */\n    public Date getLastModified() {\n        return (lastModified);\n    }\n\n    /**\n     * Sets the date of the last modification\n     * \n     * @param d date of the last modification\n     */\n    public void setLastModified( Date d ) {\n        this.lastModified = d;\n    }\n\n    /**\n     * Gets the date of the creation\n     * \n     * @return creation Date\n     */\n    public Date getCreationDate() {\n        return (creationDate);\n    }\n\n    /**\n     * Sets the date of the creation\n     * \n     * @param c date of the creation\n     */\n    public void setCreationDate( Date c ) {\n        this.creationDate = c;\n    }\n\n    /**\n     * Gets the length of the resource content\n     * \n     * @return length of the resource content\n     */\n    public long getResourceLength() {\n        return (contentLength);\n    }\n\n    /**\n     * Sets the length of the resource content\n     * \n     * @param l the length of the resource content\n     */\n    public void setResourceLength( long l ) {\n        this.contentLength = l;\n    }\n\n    /**\n     * Gets the state of the resource\n     * \n     * @return true if the resource is in lock-null state\n     */\n    public boolean isNullResource() {\n        return isNullRessource;\n    }\n\n    /**\n     * Sets a StoredObject as a lock-null resource\n     * \n     * @param f true to set the resource as lock-null resource\n     */\n    public void setNullResource( boolean f ) {\n        this.isNullRessource = f;\n        this.isFolder = false;\n        this.creationDate = null;\n        this.lastModified = null;\n        // this.content = null;\n        this.contentLength = 0;\n        this.mimeType = null;\n    }\n\n    /**\n     * Retrieve the mime type from the store object. Can also return NULL if the store does not handle mime type stuff. In that\n     * case the mime type is determined by the servletcontext\n     * \n     * @return the mimeType\n     */\n    public String getMimeType() {\n        return mimeType;\n    }\n\n    /**\n     * Set the mime type of this object\n     * \n     * @param mimeType the mimeType to set\n     */\n    public void setMimeType( String mimeType ) {\n        this.mimeType = mimeType;\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/WebDavServletBean.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav;\n\nimport peergos.server.util.Logging;\nimport peergos.server.webdav.modeshape.webdav.exceptions.*;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.RequestUtil;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\nimport peergos.server.webdav.modeshape.webdav.methods.*;\n\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServlet;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.Principal;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\npublic class WebDavServletBean extends HttpServlet {\n\n    private static final long serialVersionUID = 1L;\n\n    private static Logger LOG = Logging.LOG();\n\n    /**\n     * MD5 message digest provider.\n     */\n    protected static MessageDigest MD5_HELPER;\n\n    private static final boolean READ_ONLY = false;\n    protected final ResourceLocks resLocks;\n    protected IWebdavStore store;\n    private Map<String, IMethodExecutor> methodMap = new HashMap<String, IMethodExecutor>();\n\n    public WebDavServletBean() {\n        resLocks = new ResourceLocks();\n\n        try {\n            MD5_HELPER = MessageDigest.getInstance(\"MD5\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new IllegalStateException(e);\n        }\n    }\n\n    @SuppressWarnings( \"unused\" )\n    public void init( IWebdavStore store,\n                      String dftIndexFile,\n                      String insteadOf404,\n                      int nocontentLenghHeaders,\n                      boolean lazyFolderCreationOnPut ) throws ServletException {\n\n        this.store = store;\n\n        IMimeTyper mimeTyper = new IMimeTyper() {\n            @Override\n            public String getMimeType( ITransaction transaction,\n                                       String path ) {\n                String retVal = WebDavServletBean.this.store.getStoredObject(transaction, path).getMimeType();\n                if (retVal == null) {\n                    retVal = getServletContext().getMimeType(path);\n                }\n                return retVal;\n            }\n        };\n\n        register(\"GET\", new DoGet(store, dftIndexFile, insteadOf404, resLocks, mimeTyper, nocontentLenghHeaders));\n        register(\"HEAD\", new DoHead(store, dftIndexFile, insteadOf404, resLocks, mimeTyper, nocontentLenghHeaders));\n        DoDelete doDelete = (DoDelete)register(\"DELETE\", new DoDelete(store, resLocks, READ_ONLY));\n        DoCopy doCopy = (DoCopy)register(\"COPY\", new DoCopy(store, resLocks, doDelete, READ_ONLY));\n        register(\"LOCK\", new DoLock(store, resLocks, READ_ONLY));\n        register(\"UNLOCK\", new DoUnlock(store, resLocks, READ_ONLY));\n        register(\"MOVE\", new DoMove(resLocks, store, doDelete, READ_ONLY));\n        register(\"MKCOL\", new DoMkcol(store, resLocks, READ_ONLY));\n        register(\"OPTIONS\", new DoOptions(store, resLocks));\n        register(\"PUT\", new DoPut(store, resLocks, READ_ONLY, lazyFolderCreationOnPut));\n        register(\"PROPFIND\", new DoPropfind(store, resLocks, mimeTyper));\n        register(\"PROPPATCH\", new DoProppatch(store, resLocks, READ_ONLY));\n        register(\"*NO*IMPL*\", new DoNotImplemented(READ_ONLY));\n    }\n\n    @Override\n    public void destroy() {\n        if (store != null) {\n            store.destroy();\n        }\n        super.destroy();\n    }\n\n    protected IMethodExecutor register( String methodName,\n                                        IMethodExecutor method ) {\n        methodMap.put(methodName, method);\n        return method;\n    }\n\n    /**\n     * Handles the special WebDAV methods.\n     */\n    @Override\n    protected void service( HttpServletRequest req,\n                            HttpServletResponse resp ) throws ServletException, IOException {\n\n        String methodName = req.getMethod();\n        ITransaction transaction = null;\n        boolean needRollback = false;\n\n        debugRequest(methodName, req);\n\n        try {\n            Principal userPrincipal = req.getUserPrincipal();\n            transaction = store.begin(userPrincipal);\n            needRollback = true;\n            store.checkAuthentication(transaction);\n            resp.setStatus(WebdavStatus.SC_OK);\n\n            try {\n                IMethodExecutor methodExecutor = methodMap.get(methodName);\n                if (methodExecutor == null) {\n                    methodExecutor = methodMap.get(\"*NO*IMPL*\");\n                }\n\n                methodExecutor.execute(transaction, req, resp);\n\n                store.commit(transaction);\n                /**\n                 * Clear not consumed data Clear input stream if available otherwise later access include current input. These\n                 * cases occure if the client sends a request with body to an not existing resource.\n                 */\n                if (RequestUtil.streamNotConsumed(req)) {\n                    LOG.fine(\"Clear not consumed data!\");\n                    try {\n                        while (req.getInputStream().available() > 0) {\n                            req.getInputStream().read();\n                        }\n                    } catch (IOException e) {\n                        //ignore\n                    }\n                }\n                needRollback = false;\n            } catch (IOException e) {\n                LOG.log(Level.WARNING, e, () -> \"IOException\");\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                store.rollback(transaction);\n                throw new ServletException(e);\n            }\n\n        } catch (UnauthenticatedException e) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } catch (AccessDeniedException e) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } catch (LockFailedException e) {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n        } catch (ObjectAlreadyExistsException e) {\n            resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);\n        } catch (ObjectNotFoundException e) {\n            resp.sendError(WebdavStatus.SC_NOT_FOUND);\n        } catch (WebdavException e) {\n            throw new ServletException(e);\n        } catch (Throwable t) {\n            t = translate(t);\n            if (t instanceof UnauthenticatedException) {\n                resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            } else if (t instanceof AccessDeniedException) {\n                resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            } else if (t instanceof LockFailedException) {\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            } else if (t instanceof ObjectAlreadyExistsException) {\n                resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);\n            } else if (t instanceof ObjectNotFoundException) {\n                resp.sendError(WebdavStatus.SC_NOT_FOUND);\n            } else if (t instanceof WebdavException) {\n                throw new ServletException(t);\n            } else {\n                throw new ServletException(t);\n            }\n        } finally {\n            if (needRollback) {\n                store.rollback(transaction);\n            }\n        }\n    }\n\n    protected Throwable translate( Throwable t ) {\n        return t;\n    }\n\n    private void debugRequest( String methodName,\n                               HttpServletRequest req ) {\n        LOG.fine(\"-----------\");\n        LOG.fine(\"WebdavServlet\\n request: methodName = \" + methodName);\n        LOG.fine(\"time: \" + System.currentTimeMillis());\n        LOG.fine(\"path: \" + req.getRequestURI());\n        LOG.fine(\"-----------\");\n        Enumeration<?> e = req.getHeaderNames();\n        while (e.hasMoreElements()) {\n            String s = (String)e.nextElement();\n            LOG.fine(\"header: \" + s + \" \" + req.getHeader(s));\n        }\n        e = req.getAttributeNames();\n        while (e.hasMoreElements()) {\n            String s = (String)e.nextElement();\n            LOG.fine(\"attribute: \" + s + \" \" + req.getAttribute(s));\n        }\n        e = req.getParameterNames();\n        while (e.hasMoreElements()) {\n            String s = (String)e.nextElement();\n            LOG.fine(\"parameter: \" + s + \" \" + req.getParameter(s));\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/WebdavServlet.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav;\n\nimport jakarta.servlet.ServletException;\nimport peergos.server.webdav.WebdavFileSystem;\n\n/**\n * Servlet which provides support for WebDAV level 2. the original class is org.apache.catalina.servlets.WebdavServlet by Remy\n * Maucherat, which was heavily changed\n * \n * @author Remy Maucherat\n */\n\npublic class WebdavServlet extends WebDavServletBean {\n\n    private static final long serialVersionUID = 1L;\n    private static final String ROOTPATH_PARAMETER = \"rootpath\";\n\n    private final IWebdavStore webdavStore;\n\n    public WebdavServlet(String username, String password, String peergosUrl) {\n        webdavStore = new WebdavFileSystem(username, password, peergosUrl);\n    }\n\n    @Override\n    public void init() throws ServletException {\n\n\n\n        boolean lazyFolderCreationOnPut = getInitParameter(\"lazyFolderCreationOnPut\") != null\n                                          && getInitParameter(\"lazyFolderCreationOnPut\").equals(\"1\");\n\n        String dftIndexFile = getInitParameter(\"default-index-file\");\n        String insteadOf404 = getInitParameter(\"instead-of-404\");\n\n        int noContentLengthHeader = getIntInitParameter(\"no-content-length-headers\");\n\n        super.init(webdavStore, dftIndexFile, insteadOf404, noContentLengthHeader, lazyFolderCreationOnPut);\n    }\n\n    private int getIntInitParameter( String key ) {\n        return getInitParameter(key) == null ? -1 : Integer.parseInt(getInitParameter(key));\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/WebdavStatus.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav;\n\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.util.Hashtable;\n\n/**\n * Wraps the HttpServletResponse class to abstract the specific protocol used. To support other protocols we would only need to\n * modify this class and the WebDavRetCode classes.\n * \n * @author Marc Eaddy\n * @version 1.0, 16 Nov 1997\n */\npublic class WebdavStatus {\n\n    // ----------------------------------------------------- Instance Variables\n\n    /**\n     * This Hashtable contains the mapping of HTTP and WebDAV status codes to descriptive text. This is a static variable.\n     */\n    private static Hashtable<Integer, String> mapStatusCodes = new Hashtable<Integer, String>();\n\n    // ------------------------------------------------------ HTTP Status Codes\n\n    /**\n     * Status code (200) indicating the request succeeded normally.\n     */\n    public static final int SC_OK = HttpServletResponse.SC_OK;\n\n    /**\n     * Status code (201) indicating the request succeeded and created a new resource on the server.\n     */\n    public static final int SC_CREATED = HttpServletResponse.SC_CREATED;\n\n    /**\n     * Status code (202) indicating that a request was accepted for processing, but was not completed.\n     */\n    public static final int SC_ACCEPTED = HttpServletResponse.SC_ACCEPTED;\n\n    /**\n     * Status code (204) indicating that the request succeeded but that there was no new information to return.\n     */\n    public static final int SC_NO_CONTENT = HttpServletResponse.SC_NO_CONTENT;\n\n    /**\n     * Status code (301) indicating that the resource has permanently moved to a new location, and that future references should\n     * use a new URI with their requests.\n     */\n    public static final int SC_MOVED_PERMANENTLY = HttpServletResponse.SC_MOVED_PERMANENTLY;\n\n    /**\n     * Status code (302) indicating that the resource has temporarily moved to another location, but that future references should\n     * still use the original URI to access the resource.\n     */\n    public static final int SC_MOVED_TEMPORARILY = HttpServletResponse.SC_MOVED_TEMPORARILY;\n\n    /**\n     * Status code (304) indicating that a conditional GET operation found that the resource was available and not modified.\n     */\n    public static final int SC_NOT_MODIFIED = HttpServletResponse.SC_NOT_MODIFIED;\n\n    /**\n     * Status code (400) indicating the request sent by the client was syntactically incorrect.\n     */\n    public static final int SC_BAD_REQUEST = HttpServletResponse.SC_BAD_REQUEST;\n\n    /**\n     * Status code (401) indicating that the request requires HTTP authentication.\n     */\n    public static final int SC_UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED;\n\n    /**\n     * Status code (403) indicating the server understood the request but refused to fulfill it.\n     */\n    public static final int SC_FORBIDDEN = HttpServletResponse.SC_FORBIDDEN;\n\n    /**\n     * Status code (404) indicating that the requested resource is not available.\n     */\n    public static final int SC_NOT_FOUND = HttpServletResponse.SC_NOT_FOUND;\n\n    /**\n     * Status code (500) indicating an error inside the HTTP service which prevented it from fulfilling the request.\n     */\n    public static final int SC_INTERNAL_SERVER_ERROR = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;\n\n    /**\n     * Status code (501) indicating the HTTP service does not support the functionality needed to fulfill the request.\n     */\n    public static final int SC_NOT_IMPLEMENTED = HttpServletResponse.SC_NOT_IMPLEMENTED;\n\n    /**\n     * Status code (502) indicating that the HTTP server received an invalid response from a server it consulted when acting as a\n     * proxy or gateway.\n     */\n    public static final int SC_BAD_GATEWAY = HttpServletResponse.SC_BAD_GATEWAY;\n\n    /**\n     * Status code (503) indicating that the HTTP service is temporarily overloaded, and unable to handle the request.\n     */\n    public static final int SC_SERVICE_UNAVAILABLE = HttpServletResponse.SC_SERVICE_UNAVAILABLE;\n\n    /**\n     * Status code (100) indicating the client may continue with its request. This interim response is used to inform the client\n     * that the initial part of the request has been received and has not yet been rejected by the server.\n     */\n    public static final int SC_CONTINUE = 100;\n\n    /**\n     * Status code (405) indicating the method specified is not allowed for the resource.\n     */\n    public static final int SC_METHOD_NOT_ALLOWED = 405;\n\n    /**\n     * Status code (409) indicating that the request could not be completed due to a conflict with the current state of the\n     * resource.\n     */\n    public static final int SC_CONFLICT = 409;\n\n    /**\n     * Status code (412) indicating the precondition given in one or more of the request-header fields evaluated to false when it\n     * was tested on the server.\n     */\n    public static final int SC_PRECONDITION_FAILED = 412;\n\n    /**\n     * Status code (413) indicating the server is refusing to process a request because the request entity is larger than the\n     * server is willing or able to process.\n     */\n    public static final int SC_REQUEST_TOO_LONG = 413;\n\n    /**\n     * Status code (415) indicating the server is refusing to service the request because the entity of the request is in a format\n     * not supported by the requested resource for the requested method.\n     */\n    public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;\n\n    /**\n     * The 424 (Failed Dependency) status code means that the method could not be performed on the resource because the requested\n     * action depended on another action and that action failed. For example, if a command in a PROPPATCH method fails then, at\n     * minimum the rest of the commands will also fail with 424 (Failed Dependency).\n     */\n    public static final int SC_FAILED_DEPENDENCY = 424;\n\n    // -------------------------------------------- Extended WebDav status code\n\n    /**\n     * Status code (207) indicating that the response requires providing status for multiple independent operations.\n     */\n    public static final int SC_MULTI_STATUS = 207;\n\n    // This one colides with HTTP 1.1\n    // \"207 Parital Update OK\"\n\n    /**\n     * Status code (418) indicating the entity body submitted with the PATCH method was not understood by the resource.\n     */\n    public static final int SC_UNPROCESSABLE_ENTITY = 418;\n\n    // This one colides with HTTP 1.1\n    // \"418 Reauthentication Required\"\n\n    /**\n     * Status code (419) indicating that the resource does not have sufficient space to record the state of the resource after the\n     * execution of this method.\n     */\n    public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;\n\n    // This one colides with HTTP 1.1\n    // \"419 Proxy Reauthentication Required\"\n\n    /**\n     * Status code (420) indicating the method was not executed on a particular resource within its scope because some part of the\n     * method's execution failed causing the entire method to be aborted.\n     */\n    public static final int SC_METHOD_FAILURE = 420;\n\n    /**\n     * Status code (423) indicating the destination resource of a method is locked, and either the request did not contain a valid\n     * Lock-Info header, or the Lock-Info header identifies a lock held by another principal.\n     */\n    public static final int SC_LOCKED = 423;\n\n    // ------------------------------------------------------------ Initializer\n\n    static {\n        // HTTP 1.0 Status Code\n        addStatusCodeMap(SC_OK, \"OK\");\n        addStatusCodeMap(SC_CREATED, \"Created\");\n        addStatusCodeMap(SC_ACCEPTED, \"Accepted\");\n        addStatusCodeMap(SC_NO_CONTENT, \"No Content\");\n        addStatusCodeMap(SC_MOVED_PERMANENTLY, \"Moved Permanently\");\n        addStatusCodeMap(SC_MOVED_TEMPORARILY, \"Moved Temporarily\");\n        addStatusCodeMap(SC_NOT_MODIFIED, \"Not Modified\");\n        addStatusCodeMap(SC_BAD_REQUEST, \"Bad Request\");\n        addStatusCodeMap(SC_UNAUTHORIZED, \"Unauthorized\");\n        addStatusCodeMap(SC_FORBIDDEN, \"Forbidden\");\n        addStatusCodeMap(SC_NOT_FOUND, \"Not Found\");\n        addStatusCodeMap(SC_INTERNAL_SERVER_ERROR, \"Internal Server Error\");\n        addStatusCodeMap(SC_NOT_IMPLEMENTED, \"Not Implemented\");\n        addStatusCodeMap(SC_BAD_GATEWAY, \"Bad Gateway\");\n        addStatusCodeMap(SC_SERVICE_UNAVAILABLE, \"Service Unavailable\");\n        addStatusCodeMap(SC_CONTINUE, \"Continue\");\n        addStatusCodeMap(SC_METHOD_NOT_ALLOWED, \"Method Not Allowed\");\n        addStatusCodeMap(SC_CONFLICT, \"Conflict\");\n        addStatusCodeMap(SC_PRECONDITION_FAILED, \"Precondition Failed\");\n        addStatusCodeMap(SC_REQUEST_TOO_LONG, \"Request Too Long\");\n        addStatusCodeMap(SC_UNSUPPORTED_MEDIA_TYPE, \"Unsupported Media Type\");\n        // WebDav Status Codes\n        addStatusCodeMap(SC_MULTI_STATUS, \"Multi-Status\");\n        addStatusCodeMap(SC_UNPROCESSABLE_ENTITY, \"Unprocessable Entity\");\n        addStatusCodeMap(SC_INSUFFICIENT_SPACE_ON_RESOURCE, \"Insufficient Space On Resource\");\n        addStatusCodeMap(SC_METHOD_FAILURE, \"Method Failure\");\n        addStatusCodeMap(SC_LOCKED, \"Locked\");\n        addStatusCodeMap(SC_FAILED_DEPENDENCY, \"Failed Dependency\");\n    }\n\n    // --------------------------------------------------------- Public Methods\n\n    /**\n     * Returns the HTTP status text for the HTTP or WebDav status code specified by looking it up in the static mapping. This is a\n     * static function.\n     * \n     * @param nHttpStatusCode [IN] HTTP or WebDAV status code\n     * @return A string with a short descriptive phrase for the HTTP status code (e.g., \"OK\").\n     */\n    public static String getStatusText( int nHttpStatusCode ) {\n        if (!mapStatusCodes.containsKey(nHttpStatusCode)) {\n            return \"\";\n        }\n        return mapStatusCodes.get(nHttpStatusCode);\n    }\n\n    // -------------------------------------------------------- Private Methods\n\n    /**\n     * Adds a new status code -> status text mapping. This is a static method because the mapping is a static variable.\n     * \n     * @param nKey [IN] HTTP or WebDAV status code\n     * @param strVal [IN] HTTP status text\n     */\n    private static void addStatusCodeMap( int nKey,\n                                          String strVal ) {\n        mapStatusCodes.put(nKey, strVal);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/exceptions/AccessDeniedException.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.exceptions;\n\npublic class AccessDeniedException extends WebdavException {\n\n    private static final long serialVersionUID = 1L;\n\n    public AccessDeniedException() {\n        super();\n    }\n\n    public AccessDeniedException( String message ) {\n        super(message);\n    }\n\n    public AccessDeniedException( String message,\n                                  Throwable cause ) {\n        super(message, cause);\n    }\n\n    public AccessDeniedException( Throwable cause ) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/exceptions/LockFailedException.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.exceptions;\n\npublic class LockFailedException extends WebdavException {\n\n    private static final long serialVersionUID = 1L;\n\n    public LockFailedException() {\n        super();\n    }\n\n    public LockFailedException( String message ) {\n        super(message);\n    }\n\n    public LockFailedException( String message,\n                                Throwable cause ) {\n        super(message, cause);\n    }\n\n    public LockFailedException( Throwable cause ) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/exceptions/ObjectAlreadyExistsException.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.exceptions;\n\npublic class ObjectAlreadyExistsException extends WebdavException {\n\n    private static final long serialVersionUID = 1L;\n\n    public ObjectAlreadyExistsException() {\n        super();\n    }\n\n    public ObjectAlreadyExistsException( String message ) {\n        super(message);\n    }\n\n    public ObjectAlreadyExistsException( String message,\n                                         Throwable cause ) {\n        super(message, cause);\n    }\n\n    public ObjectAlreadyExistsException( Throwable cause ) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/exceptions/ObjectNotFoundException.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.exceptions;\n\npublic class ObjectNotFoundException extends WebdavException {\n\n    private static final long serialVersionUID = 1L;\n\n    public ObjectNotFoundException() {\n        super();\n    }\n\n    public ObjectNotFoundException( String message ) {\n        super(message);\n    }\n\n    public ObjectNotFoundException( String message,\n                                    Throwable cause ) {\n        super(message, cause);\n    }\n\n    public ObjectNotFoundException( Throwable cause ) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/exceptions/UnauthenticatedException.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.exceptions;\n\npublic class UnauthenticatedException extends WebdavException {\n\n    private static final long serialVersionUID = 1L;\n\n    public UnauthenticatedException() {\n        super();\n    }\n\n    public UnauthenticatedException( String message ) {\n        super(message);\n    }\n\n    public UnauthenticatedException( String message,\n                                     Throwable cause ) {\n        super(message, cause);\n    }\n\n    public UnauthenticatedException( Throwable cause ) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/exceptions/WebdavException.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.exceptions;\n\npublic class WebdavException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public WebdavException() {\n        super();\n    }\n\n    public WebdavException( String message ) {\n        super(message);\n    }\n\n    public WebdavException( String message,\n                            Throwable cause ) {\n        super(message, cause);\n    }\n\n    public WebdavException( Throwable cause ) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/fromcatalina/RequestUtil.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.fromcatalina;\n\nimport jakarta.servlet.ServletInputStream;\nimport jakarta.servlet.http.Cookie;\nimport jakarta.servlet.http.HttpServletRequest;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.util.ArrayList;\nimport java.util.Map;\n\n/**\n * General purpose request parsing and encoding utility methods.\n * \n * @author Craig R. McClanahan\n * @author Tim Tye\n */\n\npublic final class RequestUtil {\n\n    /**\n     * Encode a cookie as per RFC 2109. The resulting string can be used as the value for a <code>Set-Cookie</code> header.\n     * \n     * @param cookie The cookie to encode.\n     * @return A string following RFC 2109.\n     */\n    public static String encodeCookie( Cookie cookie ) {\n\n        StringBuilder buf = new StringBuilder(cookie.getName());\n        buf.append(\"=\");\n        buf.append(cookie.getValue());\n\n        String comment = cookie.getComment();\n        if (comment != null) {\n            buf.append(\"; Comment=\\\"\");\n            buf.append(comment);\n            buf.append(\"\\\"\");\n        }\n\n        String domain = cookie.getDomain();\n        if (domain != null) {\n            buf.append(\"; Domain=\\\"\");\n            buf.append(domain);\n            buf.append(\"\\\"\");\n        }\n\n        int age = cookie.getMaxAge();\n        if (age >= 0) {\n            buf.append(\"; Max-Age=\\\"\");\n            buf.append(age);\n            buf.append(\"\\\"\");\n        }\n\n        String path = cookie.getPath();\n        if (path != null) {\n            buf.append(\"; Path=\\\"\");\n            buf.append(path);\n            buf.append(\"\\\"\");\n        }\n\n        if (cookie.getSecure()) {\n            buf.append(\"; Secure\");\n        }\n\n        int version = cookie.getVersion();\n        if (version > 0) {\n            buf.append(\"; Version=\\\"\");\n            buf.append(version);\n            buf.append(\"\\\"\");\n        }\n\n        return (buf.toString());\n    }\n\n    /**\n     * Filter the specified message string for characters that are sensitive in HTML. This avoids potential attacks caused by\n     * including JavaScript codes in the request URL that is often reported in error messages.\n     * \n     * @param message The message string to be filtered\n     * @return the filtered message\n     */\n    public static String filter( String message ) {\n\n        if (message == null) {\n            return (null);\n        }\n\n        char content[] = new char[message.length()];\n        message.getChars(0, message.length(), content, 0);\n        StringBuilder result = new StringBuilder(content.length + 50);\n        for (int i = 0; i < content.length; i++) {\n            switch (content[i]) {\n                case '<':\n                    result.append(\"&lt;\");\n                    break;\n                case '>':\n                    result.append(\"&gt;\");\n                    break;\n                case '&':\n                    result.append(\"&amp;\");\n                    break;\n                case '\"':\n                    result.append(\"&quot;\");\n                    break;\n                default:\n                    result.append(content[i]);\n            }\n        }\n        return (result.toString());\n\n    }\n\n    /**\n     * Normalize a relative URI path that may have relative values (\"/./\", \"/../\", and so on ) it it. <strong>WARNING</strong> -\n     * This method is useful only for normalizing application-generated paths. It does not try to perform security checks for\n     * malicious input.\n     * \n     * @param path Relative path to be normalized\n     * @return the normalized path\n     */\n    public static String normalize( String path ) {\n\n        if (path == null) {\n            return null;\n        }\n\n        // Create a place for the normalized path\n        String normalized = path;\n\n        if (normalized.equals(\"/.\")) {\n            return \"/\";\n        }\n\n        // Add a leading \"/\" if necessary\n        if (!normalized.startsWith(\"/\")) {\n            normalized = \"/\" + normalized;\n        }\n\n        // Resolve occurrences of \"//\" in the normalized path\n        while (true) {\n            int index = normalized.indexOf(\"//\");\n            if (index < 0) {\n                break;\n            }\n            normalized = normalized.substring(0, index) + normalized.substring(index + 1);\n        }\n\n        // Resolve occurrences of \"/./\" in the normalized path\n        while (true) {\n            int index = normalized.indexOf(\"/./\");\n            if (index < 0) {\n                break;\n            }\n            normalized = normalized.substring(0, index) + normalized.substring(index + 2);\n        }\n\n        // Resolve occurrences of \"/../\" in the normalized path\n        while (true) {\n            int index = normalized.indexOf(\"/../\");\n            if (index < 0) {\n                break;\n            }\n            if (index == 0) {\n                return (null); // Trying to go outside our context\n            }\n            int index2 = normalized.lastIndexOf('/', index - 1);\n            normalized = normalized.substring(0, index2) + normalized.substring(index + 3);\n        }\n\n        // Return the normalized path that we have completed\n        return (normalized);\n\n    }\n\n    /**\n     * Parse the character encoding from the specified content type header. If the content type is null, or there is no explicit\n     * character encoding, <code>null</code> is returned.\n     * \n     * @param contentType a content type header\n     * @return the character encoding\n     */\n    public static String parseCharacterEncoding( String contentType ) {\n\n        if (contentType == null) {\n            return (null);\n        }\n        int start = contentType.indexOf(\"charset=\");\n        if (start < 0) {\n            return (null);\n        }\n        String encoding = contentType.substring(start + 8);\n        int end = encoding.indexOf(';');\n        if (end >= 0) {\n            encoding = encoding.substring(0, end);\n        }\n        encoding = encoding.trim();\n        if ((encoding.length() > 2) && (encoding.startsWith(\"\\\"\")) && (encoding.endsWith(\"\\\"\"))) {\n            encoding = encoding.substring(1, encoding.length() - 1);\n        }\n        return (encoding.trim());\n\n    }\n\n    /**\n     * Parse a cookie header into an array of cookies according to RFC 2109.\n     * \n     * @param header Value of an HTTP \"Cookie\" header\n     * @return the cookies\n     */\n    public static Cookie[] parseCookieHeader( String header ) {\n\n        if ((header == null) || (header.length() < 1)) {\n            return (new Cookie[0]);\n        }\n\n        ArrayList<Cookie> cookies = new ArrayList<Cookie>();\n        while (header.length() > 0) {\n            int semicolon = header.indexOf(';');\n            if (semicolon < 0) {\n                semicolon = header.length();\n            }\n            if (semicolon == 0) {\n                break;\n            }\n            String token = header.substring(0, semicolon);\n            if (semicolon < header.length()) {\n                header = header.substring(semicolon + 1);\n            } else {\n                header = \"\";\n            }\n            try {\n                int equals = token.indexOf('=');\n                if (equals > 0) {\n                    String name = token.substring(0, equals).trim();\n                    String value = token.substring(equals + 1).trim();\n                    cookies.add(new Cookie(name, value));\n                }\n            } catch (Throwable e) {\n                // do nothing ?!\n            }\n        }\n\n        return cookies.toArray(new Cookie[cookies.size()]);\n    }\n\n    /**\n     * Append request parameters from the specified String to the specified Map. It is presumed that the specified Map is not\n     * accessed from any other thread, so no synchronization is performed.\n     * <p>\n     * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed individually on the parsed name and value elements, rather\n     * than on the entire query string ahead of time, to properly deal with the case where the name or value includes an encoded\n     * \"=\" or \"&\" character that would otherwise be interpreted as a delimiter.\n     * \n     * @param map Map that accumulates the resulting parameters\n     * @param data Input string containing request parameters\n     * @param encoding\n     * @throws UnsupportedEncodingException if the data is malformed\n     */\n    public static void parseParameters( Map<String, String[]> map,\n                                        String data,\n                                        String encoding ) throws UnsupportedEncodingException {\n\n        if ((data != null) && (data.length() > 0)) {\n\n            // use the specified encoding to extract bytes out of the\n            // given string so that the encoding is not lost. If an\n            // encoding is not specified, let it use platform default\n            byte[] bytes = null;\n            try {\n                if (encoding == null) {\n                    bytes = data.getBytes();\n                } else {\n                    bytes = data.getBytes(encoding);\n                }\n            } catch (UnsupportedEncodingException uee) {\n            }\n\n            parseParameters(map, bytes, encoding);\n        }\n\n    }\n\n    /**\n     * Decode and return the specified URL-encoded String. When the byte array is converted to a string, the system default\n     * character encoding is used... This may be different than some other servers.\n     * \n     * @param str The url-encoded string\n     * @return the decoded URL\n     * @throws IllegalArgumentException if a '%' character is not followed by a valid 2-digit hexadecimal number\n     */\n    public static String URLDecode( String str ) {\n\n        return URLDecode(str, null);\n\n    }\n\n    /**\n     * Decode and return the specified URL-encoded String.\n     * \n     * @param str The url-encoded string\n     * @param enc The encoding to use; if null, the default encoding is used\n     * @return the decoded URL\n     * @throws IllegalArgumentException if a '%' character is not followed by a valid 2-digit hexadecimal number\n     */\n    public static String URLDecode( String str,\n                                    String enc ) {\n\n        if (str == null) {\n            return (null);\n        }\n\n        // use the specified encoding to extract bytes out of the\n        // given string so that the encoding is not lost. If an\n        // encoding is not specified, let it use platform default\n        byte[] bytes = null;\n        try {\n            if (enc == null) {\n                bytes = str.getBytes();\n            } else {\n                bytes = str.getBytes(enc);\n            }\n        } catch (UnsupportedEncodingException uee) {\n        }\n\n        return URLDecode(bytes, enc);\n\n    }\n\n    /**\n     * Decode and return the specified URL-encoded byte array.\n     * \n     * @param bytes The url-encoded byte array\n     * @return the decoded URL\n     * @throws IllegalArgumentException if a '%' character is not followed by a valid 2-digit hexadecimal number\n     */\n    public static String URLDecode( byte[] bytes ) {\n        return URLDecode(bytes, null);\n    }\n\n    /**\n     * Decode and return the specified URL-encoded byte array.\n     * \n     * @param bytes The url-encoded byte array\n     * @param enc The encoding to use; if null, the default encoding is used\n     * @return the decoded URL\n     * @throws IllegalArgumentException if a '%' character is not followed by a valid 2-digit hexadecimal number\n     */\n    public static String URLDecode( byte[] bytes,\n                                    String enc ) {\n\n        if (bytes == null) {\n            return (null);\n        }\n\n        int len = bytes.length;\n        int ix = 0;\n        int ox = 0;\n        while (ix < len) {\n            byte b = bytes[ix++]; // Get byte to test\n            if (b == '+') {\n                b = (byte)' ';\n            } else if (b == '%') {\n                b = (byte)((convertHexDigit(bytes[ix++]) << 4) + convertHexDigit(bytes[ix++]));\n            }\n            bytes[ox++] = b;\n        }\n        if (enc != null) {\n            try {\n                return new String(bytes, 0, ox, enc);\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        }\n        return new String(bytes, 0, ox);\n\n    }\n\n    /**\n     * Convert a byte character value to hexidecimal digit value.\n     * \n     * @param b the character value byte\n     * @return the hexadecimal digit value\n     */\n    private static byte convertHexDigit( byte b ) {\n        if ((b >= '0') && (b <= '9')) {\n            return (byte)(b - '0');\n        }\n        if ((b >= 'a') && (b <= 'f')) {\n            return (byte)(b - 'a' + 10);\n        }\n        if ((b >= 'A') && (b <= 'F')) {\n            return (byte)(b - 'A' + 10);\n        }\n        return 0;\n    }\n\n    /**\n     * Put name and value pair in map. When name already exist, add value to array of values.\n     * \n     * @param map The map to populate\n     * @param name The parameter name\n     * @param value The parameter value\n     */\n    private static void putMapEntry( Map<String, String[]> map,\n                                     String name,\n                                     String value ) {\n        String[] newValues = null;\n        String[] oldValues = map.get(name);\n        if (oldValues == null) {\n            newValues = new String[1];\n            newValues[0] = value;\n        } else {\n            newValues = new String[oldValues.length + 1];\n            System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);\n            newValues[oldValues.length] = value;\n        }\n        map.put(name, newValues);\n    }\n\n    /**\n     * Append request parameters from the specified String to the specified Map. It is presumed that the specified Map is not\n     * accessed from any other thread, so no synchronization is performed.\n     * <p>\n     * <strong>IMPLEMENTATION NOTE</strong>: URL decoding is performed individually on the parsed name and value elements, rather\n     * than on the entire query string ahead of time, to properly deal with the case where the name or value includes an encoded\n     * \"=\" or \"&\" character that would otherwise be interpreted as a delimiter. NOTE: byte array data is modified by this method.\n     * Caller beware.\n     * \n     * @param map Map that accumulates the resulting parameters\n     * @param data Input string containing request parameters\n     * @param encoding Encoding to use for converting hex\n     * @throws UnsupportedEncodingException if the data is malformed\n     */\n    public static void parseParameters( Map<String, String[]> map,\n                                        byte[] data,\n                                        String encoding ) throws UnsupportedEncodingException {\n\n        if (data != null && data.length > 0) {\n            int ix = 0;\n            int ox = 0;\n            String key = null;\n            String value = null;\n            while (ix < data.length) {\n                byte c = data[ix++];\n                switch ((char)c) {\n                    case '&':\n                        value = new String(data, 0, ox, encoding);\n                        if (key != null) {\n                            putMapEntry(map, key, value);\n                            key = null;\n                        }\n                        ox = 0;\n                        break;\n                    case '=':\n                        if (key == null) {\n                            key = new String(data, 0, ox, encoding);\n                            ox = 0;\n                        } else {\n                            data[ox++] = c;\n                        }\n                        break;\n                    case '+':\n                        data[ox++] = (byte)' ';\n                        break;\n                    case '%':\n                        data[ox++] = (byte)((convertHexDigit(data[ix++]) << 4) + convertHexDigit(data[ix++]));\n                        break;\n                    default:\n                        data[ox++] = c;\n                }\n            }\n            // The last value does not end in '&'. So save it now.\n            if (key != null) {\n                value = new String(data, 0, ox, encoding);\n                putMapEntry(map, key, value);\n            }\n        }\n\n    }\n\n    /**\n     * Checks if the input stream of the given request is nor isn't consumed. This method is backwards-compatible with Servlet 2.x,\n     * as in Servlet 3.x there is a \"isFinished\" method.\n     *\n     * @param request a {@code HttpServletRequest}, never {@code null}\n     * @return {@code true} if the request stream has been consumed, {@code false} otherwise.\n     */\n    public static boolean streamNotConsumed( HttpServletRequest request ) {\n        try {\n            ServletInputStream servletInputStream = request.getInputStream();\n            //in servlet >= 3.0, available will throw an exception (while previously it didn't)\n            return request.getContentLength() != 0 && servletInputStream.available() > 0;\n        } catch (IOException e) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/fromcatalina/URLEncoder.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.fromcatalina;\n\nimport peergos.server.util.Logging;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.util.BitSet;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\n/**\n * This class is very similar to the java.net.URLEncoder class. Unfortunately, with java.net.URLEncoder there is no way to specify\n * to the java.net.URLEncoder which characters should NOT be encoded. This code was moved from DefaultServlet.java\n * \n * @author Craig R. McClanahan\n * @author Remy Maucherat\n */\npublic class URLEncoder {\n    private static Logger LOG = Logging.LOG();\n\n    protected static final char[] HEXADECIMAL = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};\n\n    // Array containing the safe characters set.\n    protected BitSet safeCharacters = new BitSet(256);\n\n    public URLEncoder() {\n        for (char i = 'a'; i <= 'z'; i++) {\n            addSafeCharacter(i);\n        }\n        for (char i = 'A'; i <= 'Z'; i++) {\n            addSafeCharacter(i);\n        }\n        for (char i = '0'; i <= '9'; i++) {\n            addSafeCharacter(i);\n        }\n        for (char c : \"$-_.+!*'(),\".toCharArray()) {\n            addSafeCharacter(c);\n        }\n    }\n\n    public void addSafeCharacter( char c ) {\n        safeCharacters.set(c);\n    }\n\n    public String encode( String path ) {\n        int maxBytesPerChar = 10;\n        // int caseDiff = ('a' - 'A');\n        StringBuilder rewrittenPath = new StringBuilder(path.length());\n        ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar);\n        OutputStreamWriter writer = null;\n        try {\n            writer = new OutputStreamWriter(buf, \"UTF-8\");\n        } catch (Exception e) {\n            LOG.log(Level.WARNING, e, () -> \"Error in encode <\" + path + \">\");\n            writer = new OutputStreamWriter(buf);\n        }\n\n        for (int i = 0; i < path.length(); i++) {\n            int c = path.charAt(i);\n            if (safeCharacters.get(c)) {\n                rewrittenPath.append((char)c);\n            } else {\n                // convert to external encoding before hex conversion\n                try {\n                    writer.write((char)c);\n                    writer.flush();\n                } catch (IOException e) {\n                    buf.reset();\n                    continue;\n                }\n                byte[] ba = buf.toByteArray();\n                for (int j = 0; j < ba.length; j++) {\n                    // Converting each byte in the buffer\n                    byte toEncode = ba[j];\n                    rewrittenPath.append('%');\n                    int low = toEncode & 0x0f;\n                    int high = (toEncode & 0xf0) >> 4;\n                    rewrittenPath.append(HEXADECIMAL[high]);\n                    rewrittenPath.append(HEXADECIMAL[low]);\n                }\n                buf.reset();\n            }\n        }\n        return rewrittenPath.toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/fromcatalina/XMLHelper.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.fromcatalina;\n\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\nimport java.util.*;\n\npublic class XMLHelper {\n\n    public static Node findSubElement( Node parent,\n                                       String localName ) {\n        if (parent == null) {\n            return null;\n        }\n        Node child = parent.getFirstChild();\n        while (child != null) {\n            if ((child.getNodeType() == Node.ELEMENT_NODE) && (child.getLocalName().equals(localName))) {\n                return child;\n            }\n            child = child.getNextSibling();\n        }\n        return null;\n    }\n\n    public static Vector<String> getPropertiesFromXML( Node propNode ) {\n        Vector<String> properties;\n        properties = new Vector<String>();\n        NodeList childList = propNode.getChildNodes();\n\n        for (int i = 0; i < childList.getLength(); i++) {\n            Node currentNode = childList.item(i);\n            if (currentNode.getNodeType() == Node.ELEMENT_NODE) {\n                String nodeName = currentNode.getLocalName();\n                String namespace = currentNode.getNamespaceURI();\n                // href is a live property which is handled differently\n                properties.addElement(namespace + \":\" + nodeName);\n            }\n        }\n        return properties;\n    }\n\n    public static Map<String, Object> getPropertiesWithValuesFromXML( Node propNode ) {\n        Map<String, Object> propertiesWithValues = new HashMap<String, Object>();\n\n        NodeList childList = propNode.getChildNodes();\n        for (int i = 0; i < childList.getLength(); i++) {\n            Node currentNode = childList.item(i);\n            if (currentNode.getNodeType() == Node.ELEMENT_NODE) {\n                String nodeName = currentNode.getLocalName();\n                String namespace = currentNode.getNamespaceURI();\n                // href is a live property which is handled differently\n                String fqn = namespace + \":\" + nodeName;\n                propertiesWithValues.put(fqn, nodeValue(currentNode));\n\n            }\n        }\n        return propertiesWithValues;\n    }\n\n    private static Object nodeValue( Node node ) {\n        NodeList childList = node.getChildNodes();\n        if (childList.getLength() == 0) {\n            return \"\";\n        } else if (childList.getLength() == 1 && childList.item(0).getNodeType() == Node.TEXT_NODE) {\n            return node.getTextContent().trim();\n        } else {\n            List<Object> value = new ArrayList<Object>();\n            for (int i = 0; i < childList.getLength(); i++) {\n                value.add(nodeValue(childList.item(i)));\n            }\n            return value;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/fromcatalina/XMLWriter.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.fromcatalina;\n\nimport java.io.IOException;\nimport java.io.Writer;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * XMLWriter helper class.\n * \n * @author <a href=\"mailto:remm@apache.org\">Remy Maucherat</a>\n */\npublic class XMLWriter {\n\n    // -------------------------------------------------------------- Constants\n\n    /**\n     * Opening tag.\n     */\n    public static final int OPENING = 0;\n\n    /**\n     * Closing tag.\n     */\n    public static final int CLOSING = 1;\n\n    /**\n     * Element with no content.\n     */\n    public static final int NO_CONTENT = 2;\n\n    // ----------------------------------------------------- Instance Variables\n\n    /**\n     * Buffer.\n     */\n    protected StringBuilder buffer = new StringBuilder();\n\n    /**\n     * Writer.\n     */\n    protected Writer writer = null;\n\n    /**\n     * Namespaces to be declared in the root element\n     */\n    protected Map<String, String> namespaces;\n\n    /**\n     * Is true until the root element is written\n     */\n    protected boolean isRootElement = true;\n\n    // ----------------------------------------------------------- Constructors\n\n    public XMLWriter( Map<String, String> namespaces ) {\n        this.namespaces = namespaces;\n    }\n\n    public XMLWriter( Writer writer,\n                      Map<String, String> namespaces ) {\n        this.writer = writer;\n        this.namespaces = namespaces;\n    }\n\n    // --------------------------------------------------------- Public Methods\n\n    /**\n     * Retrieve generated XML.\n     * \n     * @return String containing the generated XML\n     */\n    @Override\n    public String toString() {\n        return buffer.toString();\n    }\n\n    /**\n     * Write property to the XML.\n     * \n     * @param name Property name\n     * @param value Property value\n     */\n    public void writeProperty( String name,\n                               String value ) {\n        writeElement(name, OPENING);\n        buffer.append(value);\n        writeElement(name, CLOSING);\n    }\n\n    /**\n     * Write property to the XML.\n     * \n     * @param name Property name\n     */\n    public void writeProperty( String name ) {\n        writeElement(name, NO_CONTENT);\n    }\n\n    /**\n     * Write an element.\n     * \n     * @param name Element name\n     * @param type Element type\n     */\n    public void writeElement( String name,\n                              int type ) {\n        StringBuilder nsdecl = new StringBuilder();\n\n        if (isRootElement) {\n            for (Iterator<String> iter = namespaces.keySet().iterator(); iter.hasNext();) {\n                String fullName = iter.next();\n                String abbrev = namespaces.get(fullName);\n                nsdecl.append(\" xmlns:\").append(abbrev).append(\"=\\\"\").append(fullName).append(\"\\\"\");\n            }\n            isRootElement = false;\n        }\n\n        int pos = name.lastIndexOf(':');\n        if (pos >= 0) {\n            // lookup prefix for namespace\n            String fullns = name.substring(0, pos);\n            String prefix = namespaces.get(fullns);\n            // check if instead of a full URI, the prefix is used\n            if (prefix == null && namespaces.containsValue(fullns)) {\n                prefix = fullns;\n            }\n            if (prefix == null) {\n                // there is no prefix for this namespace\n                name = name.substring(pos + 1);\n                nsdecl.append(\" xmlns=\\\"\").append(fullns).append(\"\\\"\");\n            } else {\n                // there is a prefix\n                name = prefix + \":\" + name.substring(pos + 1);\n            }\n        }\n\n        switch (type) {\n            case OPENING:\n                buffer.append(\"<\" + name + nsdecl + \">\");\n                break;\n            case CLOSING:\n                buffer.append(\"</\" + name + \">\\n\");\n                break;\n            case NO_CONTENT:\n            default:\n                buffer.append(\"<\" + name + nsdecl + \"/>\");\n                break;\n        }\n    }\n\n    /**\n     * Write text.\n     * \n     * @param text Text to append\n     */\n    public void writeText( String text ) {\n        buffer.append(text);\n    }\n\n    /**\n     * Write data.\n     * \n     * @param data Data to append\n     */\n    public void writeData( String data ) {\n        buffer.append(\"<![CDATA[\" + data + \"]]>\");\n    }\n\n    /**\n     * Write XML Header.\n     */\n    public void writeXMLHeader() {\n        buffer.append(\"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\" ?>\\n\");\n    }\n\n    /**\n     * Send data and reinitializes buffer.\n     * \n     * @throws IOException\n     */\n    public void sendData() throws IOException {\n        if (writer != null) {\n            writer.write(buffer.toString());\n            writer.flush();\n            buffer = new StringBuilder();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/locking/IResourceLocks.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.locking;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\n\npublic interface IResourceLocks {\n\n    /**\n     * Tries to lock the resource at \"path\".\n     * \n     * @param transaction\n     * @param path what resource to lock\n     * @param owner the owner of the lock\n     * @param exclusive if the lock should be exclusive (or shared)\n     * @param depth depth\n     * @param timeout Lock Duration in seconds.\n     * @param temporary\n     * @return true if the resource at path was successfully locked, false if an existing lock prevented this\n     * @throws LockFailedException\n     */\n    boolean lock( ITransaction transaction,\n                  String path,\n                  String owner,\n                  boolean exclusive,\n                  int depth,\n                  int timeout,\n                  boolean temporary ) throws LockFailedException;\n\n    /**\n     * Unlocks all resources at \"path\" (and all subfolders if existing)\n     * <p/>\n     * that have the same owner.\n     * \n     * @param transaction\n     * @param id id to the resource to unlock\n     * @param owner who wants to unlock\n     * @return true if the resources were unlocked, or false otherwise\n     */\n    boolean unlock( ITransaction transaction,\n                    String id,\n                    String owner );\n\n    /**\n     * Unlocks all resources at \"path\" (and all subfolders if existing)\n     * <p/>\n     * that have the same owner.\n     * \n     * @param transaction\n     * @param path what resource to unlock\n     * @param owner who wants to unlock\n     */\n    void unlockTemporaryLockedObjects( ITransaction transaction,\n                                       String path,\n                                       String owner );\n\n    /**\n     * Deletes LockedObjects, where timeout has reached.\n     * \n     * @param transaction\n     * @param temporary Check timeout on temporary or real locks\n     */\n    void checkTimeouts( ITransaction transaction,\n                        boolean temporary );\n\n    /**\n     * Tries to lock the resource at \"path\" exclusively.\n     * \n     * @param transaction Transaction\n     * @param path what resource to lock\n     * @param owner the owner of the lock\n     * @param depth depth\n     * @param timeout Lock Duration in seconds.\n     * @return true if the resource at path was successfully locked, false if an existing lock prevented this\n     * @throws LockFailedException\n     */\n    boolean exclusiveLock( ITransaction transaction,\n                           String path,\n                           String owner,\n                           int depth,\n                           int timeout ) throws LockFailedException;\n\n    /**\n     * Tries to lock the resource at \"path\" shared.\n     * \n     * @param transaction Transaction\n     * @param path what resource to lock\n     * @param owner the owner of the lock\n     * @param depth depth\n     * @param timeout Lock Duration in seconds.\n     * @return true if the resource at path was successfully locked, false if an existing lock prevented this\n     * @throws LockFailedException\n     */\n    boolean sharedLock( ITransaction transaction,\n                        String path,\n                        String owner,\n                        int depth,\n                        int timeout ) throws LockFailedException;\n\n    /**\n     * Gets the LockedObject corresponding to specified id.\n     * \n     * @param transaction\n     * @param id LockToken to requested resource\n     * @return LockedObject or null if no LockedObject on specified path exists\n     */\n    LockedObject getLockedObjectByID( ITransaction transaction,\n                                      String id );\n\n    /**\n     * Gets the LockedObject on specified path.\n     * \n     * @param transaction\n     * @param path Path to requested resource\n     * @return LockedObject or null if no LockedObject on specified path exists\n     */\n    LockedObject getLockedObjectByPath( ITransaction transaction,\n                                        String path );\n\n    /**\n     * Gets the LockedObject corresponding to specified id (locktoken).\n     * \n     * @param transaction\n     * @param id LockToken to requested resource\n     * @return LockedObject or null if no LockedObject on specified path exists\n     */\n    LockedObject getTempLockedObjectByID( ITransaction transaction,\n                                          String id );\n\n    /**\n     * Gets the LockedObject on specified path.\n     * \n     * @param transaction\n     * @param path Path to requested resource\n     * @return LockedObject or null if no LockedObject on specified path exists\n     */\n    LockedObject getTempLockedObjectByPath( ITransaction transaction,\n                                            String path );\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/locking/LockedObject.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.locking;\n\nimport peergos.server.util.Logging;\n\nimport java.util.UUID;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\n/**\n * a helper class for ResourceLocks, represents the Locks\n * \n * @author re\n */\npublic class LockedObject {\n\n    private static final Logger LOGGER = Logging.LOG();\n\n    private ResourceLocks resourceLocks;\n\n    private String path;\n\n    private String id;\n\n    /**\n     * Describing the depth of a locked collection. If the locked resource is not a collection, depth is 0 / doesn't matter.\n     */\n    protected int lockDepth;\n\n    /**\n     * Describing the timeout of a locked object (ms)\n     */\n    protected long expiresAt;\n\n    /**\n     * owner of the lock. shared locks can have multiple owners. is null if no owner is present\n     */\n    protected String[] owner = null;\n\n    /**\n     * children of that lock\n     */\n    protected LockedObject[] children = null;\n\n    protected LockedObject parent = null;\n\n    /**\n     * weather the lock is exclusive or not. if owner=null the exclusive value doesn't matter\n     */\n    protected boolean exclusive = false;\n\n    /**\n     * weather the lock is a write or read lock\n     */\n    protected String type = null;\n\n    /**\n     * @param resLocks the resourceLocks where locks are stored\n     * @param path the path to the locked object\n     * @param temporary indicates if the LockedObject should be temporary or not\n     */\n    public LockedObject( ResourceLocks resLocks,\n                         String path,\n                         boolean temporary ) {\n        this.path = path;\n        id = UUID.randomUUID().toString();\n        resourceLocks = resLocks;\n\n        if (!temporary) {\n            resourceLocks.locks.put(path, this);\n            resourceLocks.locksByID.put(id, this);\n        } else {\n            resourceLocks.tempLocks.put(path, this);\n            resourceLocks.tempLocksByID.put(id, this);\n        }\n        resourceLocks.cleanupCounter++;\n    }\n\n    /**\n     * adds a new owner to a lock\n     * \n     * @param owner string that represents the owner\n     * @return true if the owner was added, false otherwise\n     */\n    public boolean addLockedObjectOwner( String owner ) {\n\n        if (this.owner == null) {\n            this.owner = new String[1];\n        } else {\n\n            int size = this.owner.length;\n            String[] newLockObjectOwner = new String[size + 1];\n\n            // check if the owner is already here (that should actually not\n            // happen)\n            for (int i = 0; i < size; i++) {\n                if (this.owner[i].equals(owner)) {\n                    return false;\n                }\n            }\n\n            System.arraycopy(this.owner, 0, newLockObjectOwner, 0, size);\n            this.owner = newLockObjectOwner;\n        }\n\n        this.owner[this.owner.length - 1] = owner;\n        return true;\n    }\n\n    /**\n     * tries to remove the owner from the lock\n     * \n     * @param owner string that represents the owner\n     */\n    public void removeLockedObjectOwner( String owner ) {\n\n        try {\n            if (this.owner != null) {\n                int size = this.owner.length;\n                for (int i = 0; i < size; i++) {\n                    // check every owner if it is the requested one\n                    if (this.owner[i].equals(owner)) {\n                        // remove the owner\n                        size -= 1;\n                        String[] newLockedObjectOwner = new String[size];\n                        for (int j = 0; j < size; j++) {\n                            if (j < i) {\n                                newLockedObjectOwner[j] = this.owner[j];\n                            } else {\n                                newLockedObjectOwner[j] = this.owner[j + 1];\n                            }\n                        }\n                        this.owner = newLockedObjectOwner;\n\n                    }\n                }\n                if (this.owner.length == 0) {\n                    this.owner = null;\n                }\n            }\n        } catch (ArrayIndexOutOfBoundsException e) {\n            LOGGER.log(Level.WARNING, e, () -> \"LockedObject.removeLockedObjectOwner()\");\n        }\n    }\n\n    /**\n     * adds a new child lock to this lock\n     * \n     * @param newChild new child\n     */\n    public void addChild( LockedObject newChild ) {\n        if (children == null) {\n            children = new LockedObject[0];\n        }\n        int size = children.length;\n        LockedObject[] newChildren = new LockedObject[size + 1];\n        System.arraycopy(children, 0, newChildren, 0, size);\n        newChildren[size] = newChild;\n        children = newChildren;\n    }\n\n    /**\n     * deletes this Lock object. assumes that it has no children and no owners (does not check this itself)\n     */\n    public void removeLockedObject() {\n        if (this != resourceLocks.root && !this.getPath().equals(\"/\")) {\n\n            int size = parent.children.length;\n            for (int i = 0; i < size; i++) {\n                if (parent.children[i].equals(this)) {\n                    LockedObject[] newChildren = new LockedObject[size - 1];\n                    for (int i2 = 0; i2 < (size - 1); i2++) {\n                        if (i2 < i) {\n                            newChildren[i2] = parent.children[i2];\n                        } else {\n                            newChildren[i2] = parent.children[i2 + 1];\n                        }\n                    }\n                    if (newChildren.length != 0) {\n                        parent.children = newChildren;\n                    } else {\n                        parent.children = null;\n                    }\n                    break;\n                }\n            }\n\n            // removing from hashtable\n            resourceLocks.locksByID.remove(getID());\n            resourceLocks.locks.remove(getPath());\n\n            // now the garbage collector has some work to do\n        }\n    }\n\n    /**\n     * deletes this Lock object. assumes that it has no children and no owners (does not check this itself)\n     */\n    public void removeTempLockedObject() {\n        if (this != resourceLocks.tempRoot) {\n            // removing from tree\n            if (parent != null && parent.children != null) {\n                int size = parent.children.length;\n                for (int i = 0; i < size; i++) {\n                    if (parent.children[i].equals(this)) {\n                        LockedObject[] newChildren = new LockedObject[size - 1];\n                        for (int i2 = 0; i2 < (size - 1); i2++) {\n                            if (i2 < i) {\n                                newChildren[i2] = parent.children[i2];\n                            } else {\n                                newChildren[i2] = parent.children[i2 + 1];\n                            }\n                        }\n                        if (newChildren.length != 0) {\n                            parent.children = newChildren;\n                        } else {\n                            parent.children = null;\n                        }\n                        break;\n                    }\n                }\n\n                // removing from hashtable\n                resourceLocks.tempLocksByID.remove(getID());\n                resourceLocks.tempLocks.remove(getPath());\n\n                // now the garbage collector has some work to do\n            }\n        }\n    }\n\n    /**\n     * checks if a lock of the given exclusivity can be placed, only considering children up to \"depth\"\n     * \n     * @param exclusive wheather the new lock should be exclusive\n     * @param depth the depth to which should be checked\n     * @return true if the lock can be placed\n     */\n    public boolean checkLocks( boolean exclusive,\n                               int depth ) {\n        if (checkParents(exclusive) && checkChildren(exclusive, depth)) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * helper of checkLocks(). looks if the parents are locked\n     * \n     * @param exclusive wheather the new lock should be exclusive\n     * @return true if no locks at the parent path are forbidding a new lock\n     */\n    private boolean checkParents( boolean exclusive ) {\n        if (path.equals(\"/\")) {\n            return true;\n        }\n        if (owner == null) {\n            // no owner, checking parents\n            return parent != null && parent.checkParents(exclusive);\n        }\n        // there already is a owner\n        return !(this.exclusive || exclusive) && parent.checkParents(exclusive);\n    }\n\n    /**\n     * helper of checkLocks(). looks if the children are locked\n     * \n     * @param exclusive whether the new lock should be exclusive\n     * @param depth depth\n     * @return true if no locks at the children paths are forbidding a new lock\n     */\n    private boolean checkChildren( boolean exclusive,\n                                   int depth ) {\n        if (children == null) {\n            // a file\n\n            return owner == null || !(this.exclusive || exclusive);\n        }\n        // a folder\n\n        if (owner == null) {\n            // no owner, checking children\n\n            if (depth != 0) {\n                boolean canLock = true;\n                int limit = children.length;\n                for (int i = 0; i < limit; i++) {\n                    if (!children[i].checkChildren(exclusive, depth - 1)) {\n                        canLock = false;\n                    }\n                }\n                return canLock;\n            }\n            // depth == 0 -> we don't care for children\n            return true;\n        }\n        // there already is a owner\n        return !(this.exclusive || exclusive);\n\n    }\n\n    /**\n     * Sets a new timeout for the LockedObject\n     * \n     * @param timeout\n     */\n    public void refreshTimeout( int timeout ) {\n        expiresAt = System.currentTimeMillis() + (timeout * 1000);\n    }\n\n    /**\n     * Gets the timeout for the LockedObject\n     * \n     * @return timeout\n     */\n    public long getTimeoutMillis() {\n        return (expiresAt - System.currentTimeMillis());\n    }\n\n    /**\n     * Return true if the lock has expired.\n     * \n     * @return true if timeout has passed\n     */\n    public boolean hasExpired() {\n        if (expiresAt != 0) {\n            return (System.currentTimeMillis() > expiresAt);\n        }\n        return true;\n    }\n\n    /**\n     * Gets the LockID (locktoken) for the LockedObject\n     * \n     * @return locktoken\n     */\n    public String getID() {\n        return id;\n    }\n\n    /**\n     * Gets the owners for the LockedObject\n     * \n     * @return owners\n     */\n    public String[] getOwner() {\n        return owner;\n    }\n\n    /**\n     * Gets the path for the LockedObject\n     * \n     * @return path\n     */\n    public String getPath() {\n        return path;\n    }\n\n    /**\n     * Sets the exclusivity for the LockedObject\n     * \n     * @param exclusive\n     */\n    public void setExclusive( boolean exclusive ) {\n        this.exclusive = exclusive;\n    }\n\n    /**\n     * Gets the exclusivity for the LockedObject\n     * \n     * @return exclusivity\n     */\n    public boolean isExclusive() {\n        return exclusive;\n    }\n\n    /**\n     * Gets the exclusivity for the LockedObject\n     * \n     * @return exclusivity\n     */\n    public boolean isShared() {\n        return !exclusive;\n    }\n\n    /**\n     * Gets the type of the lock\n     * \n     * @return type\n     */\n    public String getType() {\n        return type;\n    }\n\n    /**\n     * Gets the depth of the lock\n     * \n     * @return depth\n     */\n    public int getLockDepth() {\n        return lockDepth;\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/locking/ResourceLocks.java",
    "content": "/*\n * Copyright 2005-2006 webdav-servlet group.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.locking;\n\nimport peergos.server.util.Logging;\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\n\nimport java.util.Enumeration;\nimport java.util.Hashtable;\nimport java.util.logging.Logger;\n\n/**\n * simple locking management for concurrent data access, NOT the webdav locking. ( could that be used instead? ) IT IS ACTUALLY\n * USED FOR DOLOCK\n * \n * @author re\n */\npublic class ResourceLocks implements IResourceLocks {\n\n    private static Logger LOG = Logging.LOG();\n\n    /**\n     * after creating this much LockedObjects, a cleanup deletes unused LockedObjects\n     */\n    private final int cleanupLimit = 100000;\n\n    protected int cleanupCounter = 0;\n\n    /**\n     * keys: path value: LockedObject from that path\n     */\n    protected Hashtable<String, LockedObject> locks = new Hashtable<String, LockedObject>();\n\n    /**\n     * keys: id value: LockedObject from that id\n     */\n    protected Hashtable<String, LockedObject> locksByID = new Hashtable<String, LockedObject>();\n\n    /**\n     * keys: path value: Temporary LockedObject from that path\n     */\n    protected Hashtable<String, LockedObject> tempLocks = new Hashtable<String, LockedObject>();\n\n    /**\n     * keys: id value: Temporary LockedObject from that id\n     */\n    protected Hashtable<String, LockedObject> tempLocksByID = new Hashtable<String, LockedObject>();\n\n    // REMEMBER TO REMOVE UNUSED LOCKS FROM THE HASHTABLE AS WELL\n\n    protected LockedObject root = null;\n\n    protected LockedObject tempRoot = null;\n\n    private boolean temporary = true;\n\n    public ResourceLocks() {\n        root = new LockedObject(this, \"/\", true);\n        tempRoot = new LockedObject(this, \"/\", false);\n    }\n\n    @Override\n    public synchronized boolean lock( ITransaction transaction,\n                                      String path,\n                                      String owner,\n                                      boolean exclusive,\n                                      int depth,\n                                      int timeout,\n                                      boolean temporary ) throws LockFailedException {\n\n        LockedObject lo = null;\n\n        if (temporary) {\n            lo = generateTempLockedObjects(path);\n            lo.type = \"read\";\n        } else {\n            lo = generateLockedObjects(path);\n            lo.type = \"write\";\n        }\n\n        if (lo.checkLocks(exclusive, depth)) {\n\n            lo.exclusive = exclusive;\n            lo.lockDepth = depth;\n            lo.expiresAt = System.currentTimeMillis() + (timeout * 1000);\n            if (lo.parent != null) {\n                lo.parent.expiresAt = lo.expiresAt;\n                if (lo.parent.equals(root)) {\n                    LockedObject rootLo = getLockedObjectByPath(transaction, root.getPath());\n                    rootLo.expiresAt = lo.expiresAt;\n                } else if (lo.parent.equals(tempRoot)) {\n                    LockedObject tempRootLo = getTempLockedObjectByPath(transaction, tempRoot.getPath());\n                    tempRootLo.expiresAt = lo.expiresAt;\n                }\n            }\n            if (lo.addLockedObjectOwner(owner)) {\n                return true;\n            }\n            LOG.fine(\"Couldn't set owner \\\"\" + owner + \"\\\" to resource at '\" + path + \"'\");\n            return false;\n        }\n        // can not lock\n        LOG.fine(\"Lock resource at \" + path + \" failed because\" + \"\\na parent or child resource is currently locked\");\n        return false;\n    }\n\n    @Override\n    public synchronized boolean unlock( ITransaction transaction,\n                                        String id,\n                                        String owner ) {\n\n        if (locksByID.containsKey(id)) {\n            String path = locksByID.get(id).getPath();\n            if (locks.containsKey(path)) {\n                LockedObject lo = locks.get(path);\n                lo.removeLockedObjectOwner(owner);\n\n                if (lo.children == null && lo.owner == null) {\n                    lo.removeLockedObject();\n                }\n\n            } else {\n                // there is no lock at that path. someone tried to unlock it\n                // anyway. could point to a problem\n                LOG.fine(\"org.modeshape.web.webdav.locking.ResourceLocks.unlock(): no lock for path \" + path);\n                return false;\n            }\n\n            if (cleanupCounter > cleanupLimit) {\n                cleanupCounter = 0;\n                cleanLockedObjects(root, !temporary);\n            }\n        }\n        checkTimeouts(transaction, !temporary);\n\n        return true;\n\n    }\n\n    @Override\n    public synchronized void unlockTemporaryLockedObjects( ITransaction transaction,\n                                                           String path,\n                                                           String owner ) {\n        if (tempLocks.containsKey(path)) {\n            LockedObject lo = tempLocks.get(path);\n            lo.removeLockedObjectOwner(owner);\n\n        } else {\n            // there is no lock at that path. someone tried to unlock it\n            // anyway. could point to a problem\n            LOG.fine(\"org.modeshape.web.webdav.locking.ResourceLocks.unlock(): no lock for path \" + path);\n        }\n\n        if (cleanupCounter > cleanupLimit) {\n            cleanupCounter = 0;\n            cleanLockedObjects(tempRoot, temporary);\n        }\n\n        checkTimeouts(transaction, temporary);\n\n    }\n\n    @Override\n    public void checkTimeouts( ITransaction transaction,\n                               boolean temporary ) {\n        if (!temporary) {\n            Enumeration<LockedObject> lockedObjects = locks.elements();\n            while (lockedObjects.hasMoreElements()) {\n                LockedObject currentLockedObject = lockedObjects.nextElement();\n\n                if (currentLockedObject.expiresAt < System.currentTimeMillis()) {\n                    currentLockedObject.removeLockedObject();\n                }\n            }\n        } else {\n            Enumeration<LockedObject> lockedObjects = tempLocks.elements();\n            while (lockedObjects.hasMoreElements()) {\n                LockedObject currentLockedObject = lockedObjects.nextElement();\n\n                if (currentLockedObject.expiresAt < System.currentTimeMillis()) {\n                    currentLockedObject.removeTempLockedObject();\n                }\n            }\n        }\n\n    }\n\n    @Override\n    public boolean exclusiveLock( ITransaction transaction,\n                                  String path,\n                                  String owner,\n                                  int depth,\n                                  int timeout ) throws LockFailedException {\n        return lock(transaction, path, owner, true, depth, timeout, false);\n    }\n\n    @Override\n    public boolean sharedLock( ITransaction transaction,\n                               String path,\n                               String owner,\n                               int depth,\n                               int timeout ) throws LockFailedException {\n        return lock(transaction, path, owner, false, depth, timeout, false);\n    }\n\n    @Override\n    public LockedObject getLockedObjectByID( ITransaction transaction,\n                                             String id ) {\n        if (locksByID.containsKey(id)) {\n            return locksByID.get(id);\n        }\n        return null;\n    }\n\n    @Override\n    public LockedObject getLockedObjectByPath( ITransaction transaction,\n                                               String path ) {\n        if (locks.containsKey(path)) {\n            return this.locks.get(path);\n        }\n        return null;\n    }\n\n    @Override\n    public LockedObject getTempLockedObjectByID( ITransaction transaction,\n                                                 String id ) {\n        if (tempLocksByID.containsKey(id)) {\n            return tempLocksByID.get(id);\n        }\n        return null;\n    }\n\n    @Override\n    public LockedObject getTempLockedObjectByPath( ITransaction transaction,\n                                                   String path ) {\n        if (tempLocks.containsKey(path)) {\n            return this.tempLocks.get(path);\n        }\n        return null;\n    }\n\n    /**\n     * generates real LockedObjects for the resource at path and its parent folders. does not create new LockedObjects if they\n     * already exist\n     * \n     * @param path path to the (new) LockedObject\n     * @return the LockedObject for path.\n     */\n    private LockedObject generateLockedObjects( String path ) {\n        if (!locks.containsKey(path)) {\n            LockedObject returnObject = new LockedObject(this, path, !temporary);\n            String parentPath = getParentPath(path);\n            if (parentPath != null) {\n                LockedObject parentLockedObject = generateLockedObjects(parentPath);\n                parentLockedObject.addChild(returnObject);\n                returnObject.parent = parentLockedObject;\n            }\n            return returnObject;\n        }\n        // there is already a LockedObject on the specified path\n        return this.locks.get(path);\n    }\n\n    /**\n     * generates temporary LockedObjects for the resource at path and its parent folders. does not create new LockedObjects if\n     * they already exist\n     * \n     * @param path path to the (new) LockedObject\n     * @return the LockedObject for path.\n     */\n    private LockedObject generateTempLockedObjects( String path ) {\n        if (!tempLocks.containsKey(path)) {\n            LockedObject returnObject = new LockedObject(this, path, temporary);\n            String parentPath = getParentPath(path);\n            if (parentPath != null) {\n                LockedObject parentLockedObject = generateTempLockedObjects(parentPath);\n                parentLockedObject.addChild(returnObject);\n                returnObject.parent = parentLockedObject;\n            }\n            return returnObject;\n        }\n        // there is already a LockedObject on the specified path\n        return this.tempLocks.get(path);\n    }\n\n    /**\n     * deletes unused LockedObjects and resets the counter. works recursively starting at the given LockedObject\n     * \n     * @param lo LockedObject\n     * @param temporary Clean temporary or real locks\n     * @return if cleaned\n     */\n    private boolean cleanLockedObjects( LockedObject lo,\n                                        boolean temporary ) {\n\n        if (lo.children == null) {\n            if (lo.owner == null) {\n                if (temporary) {\n                    lo.removeTempLockedObject();\n                } else {\n                    lo.removeLockedObject();\n                }\n\n                return true;\n            }\n            return false;\n        }\n        boolean canDelete = true;\n        int limit = lo.children.length;\n        for (int i = 0; i < limit; i++) {\n            if (!cleanLockedObjects(lo.children[i], temporary)) {\n                canDelete = false;\n            } else {\n\n                // because the deleting shifts the array\n                i--;\n                limit--;\n            }\n        }\n        if (canDelete) {\n            if (lo.owner == null) {\n                if (temporary) {\n                    lo.removeTempLockedObject();\n                } else {\n                    lo.removeLockedObject();\n                }\n                return true;\n            }\n            return false;\n        }\n        return false;\n    }\n\n    /**\n     * creates the parent path from the given path by removing the last '/' and everything after that\n     * \n     * @param path the path\n     * @return parent path\n     */\n    private String getParentPath( String path ) {\n        int slash = path.lastIndexOf('/');\n        if (slash == -1) {\n            return null;\n        }\n        if (slash == 0) {\n            // return \"root\" if parent path is empty string\n            return \"/\";\n        }\n        return path.substring(0, slash);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/AbstractMethod.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.util.Logging;\nimport peergos.server.webdav.modeshape.webdav.IMethodExecutor;\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.URLEncoder;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.XMLWriter;\nimport peergos.server.webdav.modeshape.webdav.locking.IResourceLocks;\nimport peergos.server.webdav.modeshape.webdav.locking.LockedObject;\n\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\nimport java.io.IOException;\nimport java.io.Writer;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.logging.Logger;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic abstract class AbstractMethod implements IMethodExecutor {\n    \n    \n    private static final Pattern MULTI_LOCK_PATTERN = Pattern.compile(\"(.*<.*locktoken\\\\:.+\\\\>.*).*(<.*locktoken\\\\:.+\\\\>.*)\");\n    private static final Pattern SINGLE_LOCK_PATTERN = Pattern.compile(\"(.*<.*locktoken\\\\:.+\\\\>.*)\");\n    private static final Pattern LOCK_TOKEN_PATTERN = Pattern.compile(\"(.*<??.*locktoken:)([a-zA-z0-9\\\\-]+)(>??.*)\");\n    \n    /**\n     * Array containing the safe characters set.\n     */\n    protected static URLEncoder URL_ENCODER;\n\n    /**\n     * Default depth is infite.\n     */\n    protected static final int INFINITY = 3;\n\n    /**\n     * Simple date format for the creation date ISO 8601 representation (partial).\n     */\n    protected static final DateTimeFormatter CREATION_DATE_FORMAT = DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss'Z'\", Locale.US)\n                                                                              .withZone(ZoneId.of(\"GMT\")); \n\n    /**\n     * Simple date format for the last modified date. (RFC 822 updated by RFC 1123)\n     */\n    protected static final DateTimeFormatter LAST_MODIFIED_DATE_FORMAT = DateTimeFormatter.ofPattern(\"EEE, dd MMM yyyy HH:mm:ss z\",\n                                                                                                     Locale.US).withZone(ZoneId.of(\"GMT\")); \n\n    static {\n        /**\n         * GMT timezone - all HTTP dates are on GMT\n         */\n        URL_ENCODER = new URLEncoder();\n        URL_ENCODER.addSafeCharacter('-');\n        URL_ENCODER.addSafeCharacter('_');\n        URL_ENCODER.addSafeCharacter('.');\n        URL_ENCODER.addSafeCharacter('*');\n        URL_ENCODER.addSafeCharacter('/');\n    }\n\n    /**\n     * size of the io-buffer\n     */\n    protected static int BUF_SIZE = 65536;\n\n    /**\n     * Default lock timeout value.\n     */\n    protected static final int DEFAULT_TIMEOUT = 3600;\n\n    /**\n     * Maximum lock timeout.\n     */\n    protected static final int MAX_TIMEOUT = 604800;\n\n    /**\n     * Boolean value to temporary lock resources (for method locks)\n     */\n    protected static final boolean TEMPORARY = true;\n\n    /**\n     * Timeout for temporary locks\n     */\n    protected static final int TEMP_TIMEOUT = 10;\n\n    protected Logger logger;\n\n    protected AbstractMethod() {\n        logger = Logging.LOG();\n    }\n\n    public static String lastModifiedDateFormat( final Date date ) {\n        return LAST_MODIFIED_DATE_FORMAT.format(date.toInstant());\n    }\n\n    public static String creationDateFormat( final Date date ) {\n        return CREATION_DATE_FORMAT.format(date.toInstant());\n    }\n\n    /**\n     * Return the relative path associated with this servlet.\n     * \n     * @param request The servlet request we are processing\n     * @return the relative path\n     */\n    protected String getRelativePath( HttpServletRequest request ) {\n\n        // Are we being processed by a RequestDispatcher.include()?\n        if (request.getAttribute(\"jakarta.servlet.include.request_uri\") != null) {\n            String result = (String)request.getAttribute(\"jakarta.servlet.include.path_info\");\n            // if (result == null)\n            // result = (String) request\n            // .getAttribute(\"jakarta.servlet.include.servlet_path\");\n            if ((result == null) || (result.equals(\"\"))) {\n                result = \"/\";\n            }\n            return result;\n        }\n\n        // No, extract the desired path directly from the request\n        String result = request.getPathInfo();\n\n        if ((result == null) || (result.equals(\"\"))) {\n            result = \"/\";\n        }\n        return result;\n    }\n\n    /**\n     * creates the parent path from the given path by removing the last '/' and everything after that\n     * \n     * @param path the path\n     * @return parent path\n     */\n    protected String getParentPath( String path ) {\n        int slash = path.lastIndexOf('/');\n        if (slash != -1) {\n            return path.substring(0, slash);\n        }\n        return null;\n    }\n\n    /**\n     * removes a / at the end of the path string, if present\n     * \n     * @param path the path\n     * @return the path without trailing /\n     */\n    protected String getCleanPath( String path ) {\n\n        if (path.endsWith(\"/\") && path.length() > 1) {\n            path = path.substring(0, path.length() - 1);\n        }\n        return path;\n    }\n\n    /**\n     * Return JAXP document builder instance.\n     * \n     * @return the builder\n     * @throws ServletException\n     */\n    protected DocumentBuilder getDocumentBuilder() throws ServletException {\n        DocumentBuilder documentBuilder = null;\n        DocumentBuilderFactory documentBuilderFactory = null;\n        try {\n            documentBuilderFactory = DocumentBuilderFactory.newInstance();\n            documentBuilderFactory.setNamespaceAware(true);\n            documentBuilderFactory.setExpandEntityReferences(false);\n            documentBuilder = documentBuilderFactory.newDocumentBuilder();\n        } catch (ParserConfigurationException e) {\n            throw new ServletException(\"jaxp failed\");\n        }\n        return documentBuilder;\n    }\n\n    /**\n     * reads the depth header from the request and returns it as a int\n     * \n     * @param req\n     * @return the depth from the depth header\n     */\n    protected int getDepth( HttpServletRequest req ) {\n        int depth = INFINITY;\n        String depthStr = req.getHeader(\"Depth\");\n        if (depthStr != null) {\n            if (depthStr.equals(\"0\")) {\n                depth = 0;\n            } else if (depthStr.equals(\"1\")) {\n                depth = 1;\n            }\n        }\n        return depth;\n    }\n\n    /**\n     * URL rewriter.\n     * \n     * @param path Path which has to be rewiten\n     * @return the rewritten path\n     */\n    protected String rewriteUrl( String path ) {\n        return URL_ENCODER.encode(path);\n    }\n\n    /**\n     * Get the ETag associated with a file.\n     * \n     * @param so StoredObject to get resourceLength, lastModified and a hashCode of StoredObject\n     * @return the ETag\n     */\n    protected String getETag( StoredObject so ) {\n\n        String resourceLength = \"\";\n        String lastModified = \"\";\n\n        if (so != null && so.isResource()) {\n            resourceLength = Long.toString(so.getResourceLength());\n            lastModified = Long.toString(so.getLastModified().getTime());\n        }\n\n        return \"W/\\\"\" + resourceLength + \"-\" + lastModified + \"\\\"\";\n\n    }\n\n    protected String[] getLockIdFromIfHeader( HttpServletRequest req ) {\n        String id = req.getHeader(\"If\");\n        if (id == null) {\n            return null;\n        }\n        id = id.trim();\n        if (id.length() == 0) {\n            return null;\n        }\n        return lockTokensFrom(id.trim());\n    }\n    \n    protected String[] lockTokensFrom(String id) {\n        Matcher matcher = MULTI_LOCK_PATTERN.matcher(id);\n        if (matcher.matches()) {\n            return new String[] {lockIDFromToken(matcher.group(1)), lockIDFromToken(matcher.group(2))};\n        }\n        matcher = SINGLE_LOCK_PATTERN.matcher(id);\n        if (matcher.matches()) {\n            return new String[] {lockIDFromToken(matcher.group(1))};\n        }\n        return null;\n    }\n    \n    protected String lockIDFromToken(String token) {\n        Matcher matcher = LOCK_TOKEN_PATTERN.matcher(token);\n        return matcher.matches() ? matcher.group(2) : token;\n    }\n\n    protected String getLockIdFromLockTokenHeader( HttpServletRequest req ) {\n        String id = req.getHeader(\"Lock-Token\");\n        if (id == null || id.trim().length() == 0) {\n            return id;\n        }\n        return lockIDFromToken(id);\n    }\n\n    /**\n     * Checks if locks on resources at the given path exists and if so checks the If-Header to make sure the If-Header corresponds\n     * to the locked resource. Returning true if no lock exists or the If-Header is corresponding to the locked resource\n     * \n     * @param transaction\n     * @param req Servlet request\n     * @param resourceLocks\n     * @param path path to the resource\n     * @return true if no lock on a resource with the given path exists or if the If-Header corresponds to the locked resource\n     * @throws IOException\n     * @throws LockFailedException\n     */\n    protected boolean isUnlocked( ITransaction transaction,\n                                  HttpServletRequest req,\n                                  IResourceLocks resourceLocks,\n                                  String path ) throws IOException, LockFailedException {\n\n        LockedObject resourceLock = resourceLocks.getLockedObjectByPath(transaction, path);\n        if (resourceLock == null || resourceLock.isShared()) {\n            return true;\n        }\n\n        // the resource is locked\n        String[] requestLockTokens = getLockIdFromIfHeader(req);\n        String requestLockToken = null;\n        if (requestLockTokens != null) {\n            requestLockToken = requestLockTokens[0];\n            LockedObject lockedObjectByToken = resourceLocks.getLockedObjectByID(transaction, requestLockToken);\n            return lockedObjectByToken != null && lockedObjectByToken.equals(resourceLock);\n        }\n        return false;\n    }\n\n    /**\n     * Send a multistatus element containing a complete error report to the client. If the errorList contains only one error, send\n     * the error directly without wrapping it in a multistatus message.\n     * \n     * @param req Servlet request\n     * @param resp Servlet response\n     * @param errorList List of error to be displayed\n     * @throws IOException\n     */\n    protected void sendReport( HttpServletRequest req,\n                               HttpServletResponse resp,\n                               Hashtable<String, Integer> errorList ) throws IOException {\n\n        if (errorList.size() == 1) {\n            int code = errorList.elements().nextElement();\n            String statusText = WebdavStatus.getStatusText(code);\n            if (statusText != null && !statusText.trim().isEmpty()) {\n                resp.sendError(code, statusText);\n            } else {\n                resp.sendError(code);\n            }\n        } else {\n            resp.setStatus(WebdavStatus.SC_MULTI_STATUS);\n\n            String absoluteUri = req.getRequestURI();\n            // String relativePath = getRelativePath(req);\n\n            HashMap<String, String> namespaces = new HashMap<String, String>();\n            namespaces.put(\"DAV:\", \"D\");\n\n            XMLWriter generatedXML = new XMLWriter(namespaces);\n            generatedXML.writeXMLHeader();\n\n            generatedXML.writeElement(\"DAV::multistatus\", XMLWriter.OPENING);\n\n            Enumeration<String> pathList = errorList.keys();\n            while (pathList.hasMoreElements()) {\n\n                String errorPath = pathList.nextElement();\n                int errorCode = errorList.get(errorPath);\n\n                generatedXML.writeElement(\"DAV::response\", XMLWriter.OPENING);\n\n                generatedXML.writeElement(\"DAV::href\", XMLWriter.OPENING);\n                String toAppend = null;\n                if (absoluteUri.endsWith(errorPath)) {\n                    toAppend = absoluteUri;\n\n                } else if (absoluteUri.contains(errorPath)) {\n\n                    int endIndex = absoluteUri.indexOf(errorPath) + errorPath.length();\n                    toAppend = absoluteUri.substring(0, endIndex);\n                }\n\n                if (toAppend != null && !toAppend.startsWith(\"/\") && !toAppend.startsWith(\"http:\")) {\n                    toAppend = \"/\" + toAppend;\n                }\n                generatedXML.writeText(errorPath);\n                generatedXML.writeElement(\"DAV::href\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.OPENING);\n                generatedXML.writeText(\"HTTP/1.1 \" + errorCode + \" \" + WebdavStatus.getStatusText(errorCode));\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.CLOSING);\n\n                generatedXML.writeElement(\"DAV::response\", XMLWriter.CLOSING);\n\n            }\n\n            generatedXML.writeElement(\"DAV::multistatus\", XMLWriter.CLOSING);\n\n            Writer writer = resp.getWriter();\n            writer.write(generatedXML.toString());\n            writer.close();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DeterminableMethod.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\n\npublic abstract class DeterminableMethod extends AbstractMethod {\n\n    private static final String NULL_RESOURCE_METHODS_ALLOWED = \"OPTIONS, MKCOL, PUT, PROPFIND, LOCK, UNLOCK\";\n\n    private static final String RESOURCE_METHODS_ALLOWED = \"OPTIONS, GET, HEAD, POST, DELETE, TRACE\"\n                                                           + \", PROPPATCH, COPY, MOVE, LOCK, UNLOCK, PROPFIND\";\n\n    private static final String FOLDER_METHOD_ALLOWED = \", PUT\";\n\n    private static final String LESS_ALLOWED_METHODS = \"OPTIONS, MKCOL, PUT\";\n\n    /**\n     * Determines the methods normally allowed for the resource.\n     * \n     * @param so StoredObject representing the resource\n     * @return all allowed methods, separated by commas\n     */\n    protected static String determineMethodsAllowed( StoredObject so ) {\n\n        try {\n            if (so != null) {\n                if (so.isNullResource()) {\n\n                    return NULL_RESOURCE_METHODS_ALLOWED;\n\n                } else if (so.isFolder()) {\n                    return RESOURCE_METHODS_ALLOWED + FOLDER_METHOD_ALLOWED;\n                }\n                // else resource\n                return RESOURCE_METHODS_ALLOWED;\n            }\n        } catch (Exception e) {\n            // we do nothing, just return less allowed methods\n        }\n\n        return LESS_ALLOWED_METHODS;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoCopy.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.*;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.RequestUtil;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Hashtable;\nimport java.util.logging.Level;\n\npublic class DoCopy extends AbstractMethod {\n\n    private final IWebdavStore store;\n    private final ResourceLocks resourceLocks;\n    private final DoDelete doDelete;\n    private final boolean readOnly;\n\n    public DoCopy( IWebdavStore store,\n                   ResourceLocks resourceLocks,\n                   DoDelete doDelete,\n                   boolean readOnly ) {\n        this.store = store;\n        this.resourceLocks = resourceLocks;\n        this.doDelete = doDelete;\n        this.readOnly = readOnly;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        String path = getRelativePath(req);\n        if (readOnly) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            return;\n        }\n        String tempLockOwner = \"doCopy\" + System.currentTimeMillis() + req.toString();\n        try {\n            if (!resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                logger.finest(\"Resource lock failed.\");\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                return;\n            }\n            copyResource(transaction, req, resp);\n        } catch (AccessDeniedException e) {\n            logger.log(Level.FINEST, e, () -> \"Access denied for \" + path);\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } catch (ObjectAlreadyExistsException e) {\n            logger.log(Level.FINEST, e, () -> \"Conflict for \" + path);\n            resp.sendError(WebdavStatus.SC_CONFLICT, req.getRequestURI());\n        } catch (peergos.server.webdav.modeshape.webdav.exceptions.ObjectNotFoundException e) {\n            logger.log(Level.FINEST, e, () -> \"Not found for \" + path);\n            resp.sendError(WebdavStatus.SC_NOT_FOUND, req.getRequestURI());\n        } catch (WebdavException e) {\n            logger.log(Level.FINEST, e, () -> \"Error for \" + path);\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n        } finally {\n            resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n        }\n    }\n\n    /**\n     * Copy a resource.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param req Servlet request\n     * @param resp Servlet response\n     * @return true if the copy is successful\n     * @throws WebdavException if an error in the underlying store occurs\n     * @throws IOException when an error occurs while sending the response\n     * @throws LockFailedException\n     */\n    public boolean copyResource( ITransaction transaction,\n                                 HttpServletRequest req,\n                                 HttpServletResponse resp ) throws WebdavException, IOException, LockFailedException {\n\n        // Parsing destination header\n        String destinationPath = parseDestinationHeader(req, resp);\n\n        if (destinationPath == null) {\n            return false;\n        }\n\n        String path = getRelativePath(req);\n\n        if (path.equals(destinationPath)) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            return false;\n        }\n\n        Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n        String parentDestinationPath = getParentPath(getCleanPath(destinationPath));\n\n        if (!isUnlocked(transaction, req, resourceLocks, parentDestinationPath)) {\n            resp.setStatus(WebdavStatus.SC_LOCKED);\n            return false; // parentDestination is locked\n        }\n\n        if (!isUnlocked(transaction, req, resourceLocks, destinationPath)) {\n            resp.setStatus(WebdavStatus.SC_LOCKED);\n            return false; // destination is locked\n        }\n\n        // Parsing overwrite header\n        boolean overwrite = shouldOverwrite(req);\n\n        // Overwriting the destination\n        String lockOwner = \"copyResource\" + System.currentTimeMillis() + req.toString();\n\n        if (resourceLocks.lock(transaction, destinationPath, lockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n            StoredObject copySo, destinationSo = null;\n            try {\n                copySo = store.getStoredObject(transaction, path);\n                // Retrieve the resources\n                if (copySo == null) {\n                    resp.sendError(HttpServletResponse.SC_NOT_FOUND);\n                    return false;\n                }\n\n                if (copySo.isNullResource()) {\n                    String methodsAllowed = DeterminableMethod.determineMethodsAllowed(copySo);\n                    resp.addHeader(\"Allow\", methodsAllowed);\n                    resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n                    return false;\n                }\n\n                errorList = new Hashtable<String, Integer>();\n\n                destinationSo = store.getStoredObject(transaction, destinationPath);\n\n                if (overwrite) {\n\n                    // Delete destination resource, if it exists\n                    if (destinationSo != null) {\n                        doDelete.deleteResource(transaction, destinationPath, errorList, req, resp);\n                    } else {\n                        resp.setStatus(WebdavStatus.SC_CREATED);\n                    }\n                } else {\n                    // If the destination exists, then it's a conflict\n                    if (destinationSo != null) {\n                        resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);\n                        return false;\n                    }\n                    resp.setStatus(WebdavStatus.SC_CREATED);\n\n                }\n                copy(transaction, path, destinationPath, errorList, req, resp);\n\n                if (!errorList.isEmpty()) {\n                    sendReport(req, resp, errorList);\n                }\n\n            } finally {\n                resourceLocks.unlockTemporaryLockedObjects(transaction, destinationPath, lockOwner);\n            }\n        } else {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            return false;\n        }\n        return true;\n\n    }\n\n    private boolean shouldOverwrite( HttpServletRequest req ) {\n        boolean overwrite = true;\n        String overwriteHeader = req.getHeader(\"Overwrite\");\n\n        if (overwriteHeader != null) {\n            overwrite = overwriteHeader.equalsIgnoreCase(\"T\");\n        }\n        return overwrite;\n    }\n\n    /**\n     * copies the specified resource(s) to the specified destination. preconditions must be handled by the caller. Standard status\n     * codes must be handled by the caller. a multi status report in case of errors is created here.\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param sourcePath path from where to read\n     * @param destinationPath path where to write\n     * @param errorList\n     * @param req HttpServletRequest\n     * @param resp HttpServletResponse\n     * @throws WebdavException if an error in the underlying store occurs\n     * @throws IOException\n     */\n    private void copy( ITransaction transaction,\n                       String sourcePath,\n                       String destinationPath,\n                       Hashtable<String, Integer> errorList,\n                       HttpServletRequest req,\n                       HttpServletResponse resp ) throws WebdavException, IOException {\n\n        StoredObject sourceSo = store.getStoredObject(transaction, sourcePath);\n        if (sourceSo == null) {\n            resp.setStatus(WebdavStatus.SC_NOT_FOUND);\n            return;\n        }\n        if (sourceSo.isResource()) {\n            store.createResource(transaction, destinationPath);\n            long resourceLength = store.setResourceContent(transaction,\n                                                           destinationPath,\n                                                           store.getResourceContent(transaction, sourcePath),\n                                                           null,\n                                                           null);\n\n            if (resourceLength != -1) {\n                StoredObject destinationSo = store.getStoredObject(transaction, destinationPath);\n                destinationSo.setResourceLength(resourceLength);\n            }\n\n        } else {\n\n            if (sourceSo.isFolder()) {\n                copyFolder(transaction, sourcePath, destinationPath, errorList, req, resp);\n            } else {\n                resp.sendError(WebdavStatus.SC_NOT_FOUND);\n            }\n        }\n    }\n\n    /**\n     * helper method of copy() recursively copies the FOLDER at source path to destination path\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param sourcePath where to read\n     * @param destinationPath where to write\n     * @param errorList all errors that ocurred\n     * @param req HttpServletRequest\n     * @param resp HttpServletResponse\n     * @throws WebdavException if an error in the underlying store occurs\n     */\n    private void copyFolder( ITransaction transaction,\n                             String sourcePath,\n                             String destinationPath,\n                             Hashtable<String, Integer> errorList,\n                             HttpServletRequest req,\n                             HttpServletResponse resp ) throws WebdavException {\n\n        store.createFolder(transaction, destinationPath);\n        boolean infiniteDepth = true;\n        String depth = req.getHeader(\"Depth\");\n        if (depth != null) {\n            if (depth.equals(\"0\")) {\n                infiniteDepth = false;\n            }\n        }\n        if (infiniteDepth) {\n            String[] children = store.getChildrenNames(transaction, sourcePath);\n            children = children == null ? new String[] {} : children;\n\n            StoredObject childSo;\n            for (int i = children.length - 1; i >= 0; i--) {\n                children[i] = \"/\" + children[i];\n                try {\n                    childSo = store.getStoredObject(transaction, (sourcePath + children[i]));\n                    if (childSo == null) {\n                        errorList.put(destinationPath + children[i], WebdavStatus.SC_NOT_FOUND);\n                        continue;\n                    }\n                    if (childSo.isResource()) {\n                        store.createResource(transaction, destinationPath + children[i]);\n                        long resourceLength = store.setResourceContent(transaction,\n                                                                       destinationPath + children[i],\n                                                                       store.getResourceContent(transaction, sourcePath\n                                                                                                             + children[i]),\n                                                                       null,\n                                                                       null);\n\n                        if (resourceLength != -1) {\n                            StoredObject destinationSo = store.getStoredObject(transaction, destinationPath + children[i]);\n                            destinationSo.setResourceLength(resourceLength);\n                        }\n\n                    } else {\n                        copyFolder(transaction, sourcePath + children[i], destinationPath + children[i], errorList, req, resp);\n                    }\n                } catch (AccessDeniedException e) {\n                    errorList.put(destinationPath + children[i], WebdavStatus.SC_FORBIDDEN);\n                } catch (ObjectNotFoundException e) {\n                    errorList.put(destinationPath + children[i], WebdavStatus.SC_NOT_FOUND);\n                } catch (ObjectAlreadyExistsException e) {\n                    errorList.put(destinationPath + children[i], WebdavStatus.SC_CONFLICT);\n                } catch (WebdavException e) {\n                    errorList.put(destinationPath + children[i], WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                }\n            }\n        }\n    }\n\n    /**\n     * Parses and normalizes the destination header.\n     * \n     * @param req Servlet request\n     * @param resp Servlet response\n     * @return destinationPath\n     * @throws IOException if an error occurs while sending response\n     */\n    protected static String parseDestinationHeader( HttpServletRequest req,\n                                           HttpServletResponse resp ) throws IOException {\n        String destinationPath = req.getHeader(\"Destination\");\n\n        if (destinationPath == null) {\n            resp.sendError(WebdavStatus.SC_BAD_REQUEST);\n            return null;\n        }\n\n        // Remove url encoding from destination\n        destinationPath = RequestUtil.URLDecode(destinationPath, \"UTF-8\");\n\n        int protocolIndex = destinationPath.indexOf(\"://\");\n        if (protocolIndex >= 0) {\n            // if the Destination URL contains the protocol, we can safely\n            // trim everything upto the first \"/\" character after \"://\"\n            int firstSeparator = destinationPath.indexOf(\"/\", protocolIndex + 4);\n            if (firstSeparator < 0) {\n                destinationPath = \"/\";\n            } else {\n                destinationPath = destinationPath.substring(firstSeparator);\n            }\n        } else {\n            String hostName = req.getServerName();\n            if ((hostName != null) && (destinationPath.startsWith(hostName))) {\n                destinationPath = destinationPath.substring(hostName.length());\n            }\n\n            int portIndex = destinationPath.indexOf(\":\");\n            if (portIndex >= 0) {\n                destinationPath = destinationPath.substring(portIndex);\n            }\n\n            if (destinationPath.startsWith(\":\")) {\n                int firstSeparator = destinationPath.indexOf(\"/\");\n                if (firstSeparator < 0) {\n                    destinationPath = \"/\";\n                } else {\n                    destinationPath = destinationPath.substring(firstSeparator);\n                }\n            }\n        }\n\n        // Normalize destination path (remove '.' and' ..')\n        destinationPath = normalize(destinationPath);\n\n        String contextPath = req.getContextPath();\n        if ((contextPath != null) && (destinationPath.startsWith(contextPath))) {\n            destinationPath = destinationPath.substring(contextPath.length());\n        }\n\n        String pathInfo = req.getPathInfo();\n        if (pathInfo != null) {\n            String servletPath = req.getServletPath();\n            if ((servletPath != null) && (destinationPath.startsWith(servletPath))) {\n                destinationPath = destinationPath.substring(servletPath.length());\n            }\n        }\n\n        return destinationPath;\n    }\n\n    /**\n     * Return a context-relative path, beginning with a \"/\", that represents the canonical version of the specified path after\n     * \"..\" and \".\" elements are resolved out. If the specified path attempts to go outside the boundaries of the current context\n     * (i.e. too many \"..\" path elements are present), return <code>null</code> instead.\n     * \n     * @param path Path to be normalized\n     * @return normalized path\n     */\n    protected static String normalize( String path ) {\n\n        if (path == null) {\n            return null;\n        }\n\n        // Create a place for the normalized path\n        String normalized = path;\n\n        if (normalized.equals(\"/.\")) {\n            return \"/\";\n        }\n\n        // Normalize the slashes and add leading slash if necessary\n        if (normalized.indexOf('\\\\') >= 0) {\n            normalized = normalized.replace('\\\\', '/');\n        }\n        if (!normalized.startsWith(\"/\")) {\n            normalized = \"/\" + normalized;\n        }\n\n        // Resolve occurrences of \"//\" in the normalized path\n        while (true) {\n            int index = normalized.indexOf(\"//\");\n            if (index < 0) {\n                break;\n            }\n            normalized = normalized.substring(0, index) + normalized.substring(index + 1);\n        }\n\n        // Resolve occurrences of \"/./\" in the normalized path\n        while (true) {\n            int index = normalized.indexOf(\"/./\");\n            if (index < 0) {\n                break;\n            }\n            normalized = normalized.substring(0, index) + normalized.substring(index + 2);\n        }\n\n        // Resolve occurrences of \"/../\" in the normalized path\n        while (true) {\n            int index = normalized.indexOf(\"/../\");\n            if (index < 0) {\n                break;\n            }\n            if (index == 0) {\n                return (null); // Trying to go outside our context\n            }\n            int index2 = normalized.lastIndexOf('/', index - 1);\n            normalized = normalized.substring(0, index2) + normalized.substring(index + 3);\n        }\n\n        // Return the normalized path that we have completed\n        return (normalized);\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoDelete.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.*;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Hashtable;\nimport java.util.logging.Level;\n\npublic class DoDelete extends AbstractMethod {\n\n    private final IWebdavStore store;\n    private final ResourceLocks resourceLocks;\n    private final boolean readOnly;\n\n    public DoDelete( IWebdavStore store,\n                     ResourceLocks resourceLocks,\n                     boolean readOnly ) {\n        this.store = store;\n        this.resourceLocks = resourceLocks;\n        this.readOnly = readOnly;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        if (readOnly) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            return;\n        }\n        String path = getRelativePath(req);\n        String parentPath = getParentPath(getCleanPath(path));\n\n        if (!isUnlocked(transaction, req, resourceLocks, parentPath)) {\n            resp.setStatus(WebdavStatus.SC_LOCKED);\n            return; // parent is locked\n        }\n\n        if (!isUnlocked(transaction, req, resourceLocks, path)) {\n            resp.setStatus(WebdavStatus.SC_LOCKED);\n            return; // resource is locked\n        }\n\n        String tempLockOwner = \"doDelete\" + System.currentTimeMillis() + req;\n\n        try {\n            if (!resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                logger.finest(\"Resource lock failed.\");\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                return;\n            }\n            Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n            deleteResource(transaction, path, errorList, req, resp);\n            if (!errorList.isEmpty()) {\n                sendReport(req, resp, errorList);\n            }\n        } catch (AccessDeniedException e) {\n            logger.log(Level.FINEST, e, () -> \"Access denied for \" + path);\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } catch (ObjectAlreadyExistsException e) {\n            logger.log(Level.FINEST, e, () -> \"Conflict for \" + path);\n            resp.sendError(WebdavStatus.SC_NOT_FOUND, req.getRequestURI());\n        } catch (WebdavException e) {\n            logger.log(Level.FINEST, e, () -> \"Error for \" + path);\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n        } finally {\n            resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n        }\n    }\n\n    /**\n     * deletes the recources at \"path\"\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param path the folder to be deleted\n     * @param errorList all errors that ocurred\n     * @param req HttpServletRequest\n     * @param resp HttpServletResponse\n     * @throws WebdavException if an error in the underlying store occurs\n     * @throws IOException when an error occurs while sending the response\n     */\n    public void deleteResource( ITransaction transaction,\n                                String path,\n                                Hashtable<String, Integer> errorList,\n                                HttpServletRequest req,\n                                HttpServletResponse resp ) throws IOException, WebdavException {\n\n        resp.setStatus(WebdavStatus.SC_NO_CONTENT);\n\n        if (!readOnly) {\n            StoredObject so = store.getStoredObject(transaction, path);\n            if (so != null) {\n                if (so.isResource()) {\n                    store.removeObject(transaction, path);\n                } else {\n                    if (so.isFolder()) {\n                        deleteFolder(transaction, path, errorList);\n                        store.removeObject(transaction, path);\n                    } else {\n                        resp.sendError(WebdavStatus.SC_NOT_FOUND);\n                    }\n                }\n            } else {\n                resp.sendError(WebdavStatus.SC_NOT_FOUND);\n            }\n        } else {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        }\n    }\n\n    /**\n     * helper method of deleteResource() deletes the folder and all of its contents\n     * \n     * @param transaction indicates that the method is within the scope of a WebDAV transaction\n     * @param path the folder to be deleted\n     * @param errorList all errors that ocurred\n     * @throws WebdavException if an error in the underlying store occurs\n     */\n    private void deleteFolder( ITransaction transaction,\n                               String path,\n                               Hashtable<String, Integer> errorList ) throws WebdavException {\n\n        String[] children = store.getChildrenNames(transaction, path);\n        children = children == null ? new String[] {} : children;\n        StoredObject so = null;\n        for (int i = children.length - 1; i >= 0; i--) {\n            children[i] = \"/\" + children[i];\n            try {\n                so = store.getStoredObject(transaction, path + children[i]);\n                if (so == null) {\n                    errorList.put(path + children[i], WebdavStatus.SC_NOT_FOUND);\n                    continue;\n                }\n                if (so.isResource()) {\n                    store.removeObject(transaction, path + children[i]);\n\n                } else {\n                    deleteFolder(transaction, path + children[i], errorList);\n                    store.removeObject(transaction, path + children[i]);\n                }\n            } catch (AccessDeniedException e) {\n                errorList.put(path + children[i], WebdavStatus.SC_FORBIDDEN);\n            } catch (ObjectNotFoundException e) {\n                errorList.put(path + children[i], WebdavStatus.SC_NOT_FOUND);\n            } catch (WebdavException e) {\n                errorList.put(path + children[i], WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoGet.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport org.peergos.util.Pair;\nimport peergos.server.webdav.modeshape.webdav.*;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport peergos.shared.user.fs.AsyncReader;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.text.DateFormat;\nimport java.util.Arrays;\nimport java.util.Locale;\nimport java.util.logging.Level;\n\npublic class DoGet extends DoHead {\n\n    public DoGet( IWebdavStore store,\n                  String dftIndexFile,\n                  String insteadOf404,\n                  ResourceLocks resourceLocks,\n                  IMimeTyper mimeTyper,\n                  int contentLengthHeader ) {\n        super(store, dftIndexFile, insteadOf404, resourceLocks, mimeTyper, contentLengthHeader);\n\n    }\n\n    @Override\n    protected void doBody( ITransaction transaction,\n                           HttpServletResponse resp,\n                           String path ) {\n\n        try {\n            StoredObject so = store.getStoredObject(transaction, path);\n            if (so == null) {\n                resp.sendError(HttpServletResponse.SC_NOT_FOUND);\n                return;\n            }\n            if (so.isNullResource()) {\n                String methodsAllowed = DeterminableMethod.determineMethodsAllowed(so);\n                resp.addHeader(\"Allow\", methodsAllowed);\n                resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n                return;\n            }\n            OutputStream out = resp.getOutputStream();\n            Pair<AsyncReader, Long> reader = store.getResourceContent(transaction, path);\n            AsyncReader in = reader.left;\n            try {\n                byte[] copyBuffer = new byte[BUF_SIZE];\n                long remaining = reader.right;\n                while (remaining > 0 ) {\n                    int read = in.readIntoArray(copyBuffer, 0, (int)Math.min(remaining, copyBuffer.length)).join();\n                    remaining -= read;\n                    out.write(copyBuffer, 0, read);\n                }\n            } finally {\n                // flushing causes a IOE if a file is opened on the webserver\n                // client disconnected before server finished sending response\n                try {\n                    in.close();\n                } catch (Exception e) {\n                    logger.log(Level.WARNING, e, () -> \"Closing InputStream causes Exception!\");\n                }\n                try {\n                    out.flush();\n                    out.close();\n                } catch (Exception e) {\n                    logger.log(Level.WARNING, e, () -> \"Flushing OutputStream causes Exception!\");\n                }\n            }\n        } catch (Exception e) {\n            logger.fine(e.toString());\n        }\n    }\n\n    @Override\n    protected void folderBody( ITransaction transaction,\n                               String path,\n                               HttpServletResponse resp,\n                               HttpServletRequest req ) throws IOException {\n\n        StoredObject so = store.getStoredObject(transaction, path);\n        if (so == null) {\n            resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());\n        } else {\n\n            if (so.isNullResource()) {\n                String methodsAllowed = DeterminableMethod.determineMethodsAllowed(so);\n                resp.addHeader(\"Allow\", methodsAllowed);\n                resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n                return;\n            }\n\n            if (so.isFolder()) {\n                // TODO some folder response (for browsers, DAV tools use propfind) in html?\n                DateFormat shortDF = getDateTimeFormat(req.getLocale());\n                resp.setContentType(\"text/html\");\n                resp.setCharacterEncoding(\"UTF-8\");\n                OutputStream out = resp.getOutputStream();\n                String[] children = store.getChildrenNames(transaction, path);\n                // Make sure it's not null\n                children = children == null ? new String[] {} : children;\n                // Sort by name\n                Arrays.sort(children);\n                StringBuilder childrenTemp = new StringBuilder();\n                childrenTemp.append(\"<html><head><title>Content of folder\");\n                childrenTemp.append(path);\n                childrenTemp.append(\"</title><style type=\\\"text/css\\\">\");\n                childrenTemp.append(getCSS());\n                childrenTemp.append(\"</style></head>\");\n                childrenTemp.append(\"<body>\");\n                childrenTemp.append(getHeader(transaction, path, resp, req));\n                childrenTemp.append(\"<table>\");\n                childrenTemp.append(\"<tr><th>Name</th><th>Size</th><th>Created</th><th>Modified</th></tr>\");\n                childrenTemp.append(\"<tr>\");\n                childrenTemp.append(\"<td colspan=\\\"4\\\"><a href=\\\"../\\\">Parent</a></td></tr>\");\n                boolean isEven = false;\n                for (String child : children) {\n                    isEven = !isEven;\n                    childrenTemp.append(\"<tr class=\\\"\");\n                    childrenTemp.append(isEven ? \"even\" : \"odd\");\n                    childrenTemp.append(\"\\\">\");\n                    childrenTemp.append(\"<td>\");\n                    childrenTemp.append(\"<a href=\\\"\");\n\n                    // CHECKSTYLE IGNORE check FOR NEXT 1 LINES\n                    StringBuffer childURL = req.getRequestURL();\n                    if (!(childURL.charAt(childURL.length() - 1) == '/')) {\n                        childURL.append(\"/\");\n                    }\n\n                    // we need to URL encode the child, but just the special chars, UTF-8 encoding is done at the end of this\n                    // method\n                    childURL.append(URL_ENCODER.encode(child));\n\n                    StoredObject obj = store.getStoredObject(transaction, path + \"/\" + child);\n                    if (obj == null) {\n                        logger.warning(\"Should not return null for \" + path + \"/\" + child);\n                    }\n                    if (obj != null && obj.isFolder()) {\n                        childURL.append(\"/\");\n                    }\n\n                    childrenTemp.append(childURL);\n\n                    childrenTemp.append(\"\\\">\");\n                    childrenTemp.append(child);\n                    childrenTemp.append(\"</a></td>\");\n                    if (obj != null && obj.isFolder()) {\n                        childrenTemp.append(\"<td>Folder</td>\");\n                    } else {\n                        childrenTemp.append(\"<td>\");\n                        if (obj != null) {\n                            childrenTemp.append(obj.getResourceLength());\n                        } else {\n                            childrenTemp.append(\"Unknown\");\n                        }\n                        childrenTemp.append(\" Bytes</td>\");\n                    }\n                    if (obj != null && obj.getCreationDate() != null) {\n                        childrenTemp.append(\"<td>\");\n                        childrenTemp.append(shortDF.format(obj.getCreationDate()));\n                        childrenTemp.append(\"</td>\");\n                    } else {\n                        childrenTemp.append(\"<td></td>\");\n                    }\n                    if (obj != null && obj.getLastModified() != null) {\n                        childrenTemp.append(\"<td>\");\n                        childrenTemp.append(shortDF.format(obj.getLastModified()));\n                        childrenTemp.append(\"</td>\");\n                    } else {\n                        childrenTemp.append(\"<td></td>\");\n                    }\n                    childrenTemp.append(\"</tr>\");\n                }\n                childrenTemp.append(\"</table>\");\n                childrenTemp.append(getFooter(transaction, path, resp, req));\n                childrenTemp.append(\"</body></html>\");\n                String response = childrenTemp.toString();\n                logger.fine(\"Sending response \" + response);\n                out.write(response.getBytes(\"UTF-8\"));\n            }\n        }\n    }\n\n    /**\n     * Return the CSS styles used to display the HTML representation of the webdav content.\n     * \n     * @return the HTML body\n     */\n    protected String getCSS() {\n        // The default styles to use\n        String retVal = \"body {\\n\" + \"    font-family: Arial, Helvetica, sans-serif;\\n\" + \"}\\n\" + \"h1 {\\n\" + \"    font-size: 1.5em;\\n\"\n                        + \"}\\n\" + \"th {\\n\" + \"    background-color: #9DACBF;\\n\" + \"}\\n\" + \"table {\\n\"\n                        + \"    border-top-style: solid;\\n\" + \"    border-right-style: solid;\\n\" + \"    border-bottom-style: solid;\\n\"\n                        + \"    border-left-style: solid;\\n\" + \"}\\n\" + \"td {\\n\" + \"    margin: 0px;\\n\" + \"    padding-top: 2px;\\n\"\n                        + \"    padding-right: 5px;\\n\" + \"    padding-bottom: 2px;\\n\" + \"    padding-left: 5px;\\n\" + \"}\\n\" + \"tr.even {\\n\"\n                        + \"    background-color: #CCCCCC;\\n\" + \"}\\n\" + \"tr.odd {\\n\" + \"    background-color: #FFFFFF;\\n\" + \"}\\n\" + \"\";\n        try {\n            // Try loading one via class loader and use that one instead\n            ClassLoader cl = getClass().getClassLoader();\n            InputStream iStream = cl.getResourceAsStream(\"webdav.css\");\n            if (iStream != null) {\n                // Found css via class loader, use that one\n                StringBuilder out = new StringBuilder();\n                byte[] b = new byte[4096];\n                for (int n; (n = iStream.read(b)) != -1;) {\n                    out.append(new String(b, 0, n));\n                }\n                retVal = out.toString();\n            }\n        } catch (Exception ex) {\n            logger.log(Level.WARNING, ex, () -> \"Error in reading webdav.css\");\n        }\n\n        return retVal;\n    }\n\n    /**\n     * Return the header to be displayed in front of the folder content\n     * \n     * @param transaction\n     * @param path\n     * @param resp\n     * @param req\n     * @return the header string\n     */\n    protected String getHeader( ITransaction transaction,\n                                String path,\n                                HttpServletResponse resp,\n                                HttpServletRequest req ) {\n        return \"<h1>Content of folder \" + path + \"</h1>\";\n    }\n\n    /**\n     * Return the footer to be displayed after the folder content\n     * \n     * @param transaction\n     * @param path\n     * @param resp\n     * @param req\n     * @return the footer string\n     */\n    protected String getFooter( ITransaction transaction,\n                                String path,\n                                HttpServletResponse resp,\n                                HttpServletRequest req ) {\n        return \"\";\n    }\n\n    /**\n     * Return this as the Date/Time format for displaying Creation + Modification dates\n     * \n     * @param browserLocale\n     * @return DateFormat used to display creation and modification dates\n     */\n    protected DateFormat getDateTimeFormat( Locale browserLocale ) {\n        return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, browserLocale);\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoHead.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.*;\nimport peergos.server.webdav.modeshape.webdav.exceptions.AccessDeniedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.ObjectAlreadyExistsException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic class DoHead extends AbstractMethod {\n\n    protected final String dftIndexFile;\n    protected final IWebdavStore store;\n    protected final String insteadOf404;\n    protected final ResourceLocks resourceLocks;\n    protected final IMimeTyper mimeTyper;\n    protected final int contentLength;\n\n    public DoHead( IWebdavStore store,\n                   String dftIndexFile,\n                   String insteadOf404,\n                   ResourceLocks resourceLocks,\n                   IMimeTyper mimeTyper,\n                   int contentLengthHeader ) {\n        this.store = store;\n        this.dftIndexFile = dftIndexFile;\n        this.insteadOf404 = insteadOf404;\n        this.resourceLocks = resourceLocks;\n        this.mimeTyper = mimeTyper;\n        this.contentLength = contentLengthHeader;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n\n        // determines if the uri exists.\n\n        boolean bUriExists = false;\n\n        String path = getRelativePath(req);\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        StoredObject so = store.getStoredObject(transaction, path);\n        if (so == null) {\n            if (this.insteadOf404 != null && !insteadOf404.trim().equals(\"\")) {\n                path = this.insteadOf404;\n                so = store.getStoredObject(transaction, this.insteadOf404);\n            }\n        } else {\n            bUriExists = true;\n        }\n\n        if (so != null) {\n            if (so.isFolder()) {\n                if (dftIndexFile != null && !dftIndexFile.trim().equals(\"\")) {\n                    resp.sendRedirect(resp.encodeRedirectURL(req.getRequestURI() + this.dftIndexFile));\n                    return;\n                }\n            } else if (so.isNullResource()) {\n                String methodsAllowed = DeterminableMethod.determineMethodsAllowed(so);\n                resp.addHeader(\"Allow\", methodsAllowed);\n                resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n                return;\n            }\n\n            String tempLockOwner = \"doGet\" + System.currentTimeMillis() + req.toString();\n\n            if (resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                try {\n                    String eTagMatch = req.getHeader(\"If-None-Match\");\n                    if (eTagMatch != null) {\n                        if (eTagMatch.equals(getETag(so))) {\n                            resp.setStatus(WebdavStatus.SC_NOT_MODIFIED);\n                            return;\n                        }\n                    }\n\n                    if (so.isResource()) {\n                        // path points to a file but ends with / or \\\n                        if (path.endsWith(\"/\") || (path.endsWith(\"\\\\\"))) {\n                            resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());\n                        } else {\n\n                            // setting headers\n                            long lastModified = so.getLastModified().getTime();\n                            resp.setDateHeader(\"last-modified\", lastModified);\n\n                            String eTag = getETag(so);\n                            resp.addHeader(\"ETag\", eTag);\n\n                            long resourceLength = so.getResourceLength();\n\n                            if (contentLength == 1) {\n                                if (resourceLength > 0) {\n                                    if (resourceLength <= Integer.MAX_VALUE) {\n                                        resp.setContentLength((int)resourceLength);\n                                    } else {\n                                        resp.setHeader(\"content-length\", \"\" + resourceLength);\n                                        // is \"content-length\" the right header?\n                                        // is long a valid format?\n                                    }\n                                }\n                            }\n\n                            String mimeType = mimeTyper.getMimeType(transaction, path);\n                            if (mimeType != null) {\n                                resp.setContentType(mimeType);\n                            } else {\n                                int lastSlash = path.replace('\\\\', '/').lastIndexOf('/');\n                                int lastDot = path.indexOf(\".\", lastSlash);\n                                if (lastDot == -1) {\n                                    resp.setContentType(\"text/html\");\n                                }\n                            }\n\n                            doBody(transaction, resp, path);\n                        }\n                    } else {\n                        folderBody(transaction, path, resp, req);\n                    }\n                } catch (AccessDeniedException e) {\n                    resp.sendError(WebdavStatus.SC_FORBIDDEN);\n                } catch (ObjectAlreadyExistsException e) {\n                    resp.sendError(WebdavStatus.SC_NOT_FOUND, req.getRequestURI());\n                } catch (WebdavException e) {\n                    resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                } finally {\n                    resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n                }\n            } else {\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            }\n        } else {\n            folderBody(transaction, path, resp, req);\n        }\n\n        if (!bUriExists) {\n            resp.setStatus(WebdavStatus.SC_NOT_FOUND);\n        }\n\n    }\n\n    @SuppressWarnings( \"unused\" )\n    protected void folderBody( ITransaction transaction,\n                               String path,\n                               HttpServletResponse resp,\n                               HttpServletRequest req ) throws IOException {\n        // no body for HEAD\n    }\n\n    @SuppressWarnings( \"unused\" )\n    protected void doBody( ITransaction transaction,\n                           HttpServletResponse resp,\n                           String path ) throws IOException {\n        // no body for HEAD\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoLock.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.XMLWriter;\nimport peergos.server.webdav.modeshape.webdav.locking.IResourceLocks;\nimport peergos.server.webdav.modeshape.webdav.locking.LockedObject;\nimport org.w3c.dom.DOMException;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.xml.sax.InputSource;\nimport org.xml.sax.SAXException;\n\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport javax.xml.parsers.DocumentBuilder;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Hashtable;\nimport java.util.logging.Level;\n\npublic class DoLock extends AbstractMethod {\n\n    private final IWebdavStore store;\n    private final IResourceLocks resourceLocks;\n    private final boolean readOnly;\n\n    private boolean macLockRequest = false;\n\n    private boolean exclusive = false;\n    private String type = null;\n    private String lockOwner = null;\n\n    private String path = null;\n    private String parentPath = null;\n\n    private String userAgent = null;\n\n    public DoLock( IWebdavStore store,\n                   IResourceLocks resourceLocks,\n                   boolean readOnly ) {\n        this.store = store;\n        this.resourceLocks = resourceLocks;\n        this.readOnly = readOnly;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        if (readOnly) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } else {\n            path = getRelativePath(req);\n            parentPath = getParentPath(getCleanPath(path));\n\n            if (!isUnlocked(transaction, req, resourceLocks, path)) {\n                resp.setStatus(WebdavStatus.SC_LOCKED);\n                return; // resource is locked\n            }\n\n            if (!isUnlocked(transaction, req, resourceLocks, parentPath)) {\n                resp.setStatus(WebdavStatus.SC_LOCKED);\n                return; // parent is locked\n            }\n\n            // Mac OS Finder (whether 10.4.x or 10.5) can't store files\n            // because executing a LOCK without lock information causes a\n            // SC_BAD_REQUEST\n            userAgent = req.getHeader(\"User-Agent\");\n            if (userAgent != null && userAgent.contains(\"Darwin\")) {\n                macLockRequest = true;\n\n                String timeString = Long.toString(System.currentTimeMillis());\n                lockOwner = userAgent.concat(timeString);\n            }\n\n            String tempLockOwner = \"doLock\" + System.currentTimeMillis() + req.toString();\n            if (resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                try {\n                    if (req.getHeader(\"If\") != null) {\n                        doRefreshLock(transaction, req, resp);\n                    } else {\n                        doLock(transaction, req, resp);\n                    }\n                } catch (LockFailedException e) {\n                    resp.sendError(WebdavStatus.SC_LOCKED);\n                    logger.log(Level.WARNING, e, () -> \"Lockfailed exception\");\n                } finally {\n                    resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n                }\n            }\n        }\n    }\n\n    private void doLock( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n\n        StoredObject so = store.getStoredObject(transaction, path);\n\n        if (so != null) {\n            doLocking(transaction, req, resp);\n        } else {\n            // resource doesn't exist, null-resource lock\n            doNullResourceLock(transaction, req, resp);\n        }\n\n        so = null;\n        exclusive = false;\n        type = null;\n        lockOwner = null;\n\n    }\n\n    private void doLocking( ITransaction transaction,\n                            HttpServletRequest req,\n                            HttpServletResponse resp ) throws IOException {\n\n        // Tests if LockObject on requested path exists, and if so, tests\n        // exclusivity\n        LockedObject lo = resourceLocks.getLockedObjectByPath(transaction, path);\n        if (lo != null) {\n            if (lo.isExclusive()) {\n                sendLockFailError(req, resp);\n                return;\n            }\n        }\n        try {\n            // Thats the locking itself\n            executeLock(transaction, req, resp);\n\n        } catch (ServletException e) {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            logger.fine(e.toString());\n        } catch (LockFailedException e) {\n            sendLockFailError(req, resp);\n        } finally {\n            lo = null;\n        }\n\n    }\n\n    private void doNullResourceLock( ITransaction transaction,\n                                     HttpServletRequest req,\n                                     HttpServletResponse resp ) throws IOException {\n\n        StoredObject parentSo, nullSo = null;\n\n        try {\n            parentSo = store.getStoredObject(transaction, parentPath);\n            if (parentPath != null && parentSo == null) {\n                store.createFolder(transaction, parentPath);\n            } else if (parentPath != null && parentSo != null && parentSo.isResource()) {\n                resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);\n                return;\n            }\n\n            nullSo = store.getStoredObject(transaction, path);\n            if (nullSo == null) {\n                // resource doesn't exist\n                store.createResource(transaction, path);\n\n                // Transmit expects 204 response-code, not 201\n                if (userAgent != null && userAgent.contains(\"Transmit\")) {\n                    logger.fine(\"DoLock.execute() : do workaround for user agent '\" + userAgent + \"'\");\n                    resp.setStatus(WebdavStatus.SC_NO_CONTENT);\n                } else {\n                    resp.setStatus(WebdavStatus.SC_CREATED);\n                }\n\n            } else {\n                // resource already exists, could not execute null-resource lock\n                sendLockFailError(req, resp);\n                return;\n            }\n            nullSo = store.getStoredObject(transaction, path);\n            if (nullSo == null) {\n                resp.setStatus(WebdavStatus.SC_NOT_FOUND);\n            } else {\n                // define the newly created resource as null-resource\n                nullSo.setNullResource(true);\n\n                // Thats the locking itself\n                executeLock(transaction, req, resp);\n            }\n        } catch (LockFailedException e) {\n            sendLockFailError(req, resp);\n        } catch (WebdavException e) {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            logger.log(Level.WARNING, e, () -> \"Webdav exception\");\n        } catch (ServletException e) {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            logger.log(Level.WARNING, e, () -> \"Servlet exception\");\n        } finally {\n            parentSo = null;\n            nullSo = null;\n        }\n    }\n\n    private void doRefreshLock( ITransaction transaction,\n                                HttpServletRequest req,\n                                HttpServletResponse resp ) throws IOException, LockFailedException {\n\n        String[] lockTokens = getLockIdFromIfHeader(req);\n        String lockToken = null;\n        if (lockTokens != null) {\n            lockToken = lockTokens[0];\n        }\n\n        if (lockToken != null) {\n            // Getting LockObject of specified lockToken in If header\n            LockedObject refreshLo = resourceLocks.getLockedObjectByID(transaction, lockToken);\n            if (refreshLo != null) {\n                int timeout = getTimeout(req);\n\n                refreshLo.refreshTimeout(timeout);\n                // sending success response\n                generateXMLReport(resp, refreshLo);\n\n                refreshLo = null;\n            } else {\n                // no LockObject to given lockToken\n                resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);\n            }\n\n        } else {\n            resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);\n        }\n    }\n\n    // ------------------------------------------------- helper methods\n\n    private void executeLock( ITransaction transaction,\n                              HttpServletRequest req,\n                              HttpServletResponse resp ) throws LockFailedException, IOException, ServletException {\n\n        // Mac OS lock request workaround\n        if (macLockRequest) {\n            logger.fine(\"DoLock.execute() : do workaround for user agent '\" + userAgent + \"'\");\n\n            doMacLockRequestWorkaround(transaction, req, resp);\n        } else {\n            // Getting LockInformation from request\n            if (getLockInformation(req, resp)) {\n                int depth = getDepth(req);\n                int lockDuration = getTimeout(req);\n\n                boolean lockSuccess = false;\n                if (exclusive) {\n                    lockSuccess = resourceLocks.exclusiveLock(transaction, path, lockOwner, depth, lockDuration);\n                } else {\n                    lockSuccess = resourceLocks.sharedLock(transaction, path, lockOwner, depth, lockDuration);\n                }\n\n                if (lockSuccess) {\n                    // Locks successfully placed - return information about\n                    LockedObject lo = resourceLocks.getLockedObjectByPath(transaction, path);\n                    if (lo != null) {\n                        generateXMLReport(resp, lo);\n                    } else {\n                        resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                    }\n                } else {\n                    sendLockFailError(req, resp);\n\n                    throw new LockFailedException();\n                }\n            } else {\n                // information for LOCK could not be read successfully\n                resp.setContentType(\"text/xml; charset=UTF-8\");\n                resp.sendError(WebdavStatus.SC_BAD_REQUEST);\n            }\n        }\n    }\n\n    private boolean getLockInformation( HttpServletRequest req,\n                                        HttpServletResponse resp ) throws ServletException, IOException {\n\n        Node lockInfoNode = null;\n        DocumentBuilder documentBuilder = null;\n\n        documentBuilder = getDocumentBuilder();\n        try {\n            Document document = documentBuilder.parse(new InputSource(req.getInputStream()));\n\n            // Get the root element of the document\n            lockInfoNode = document.getDocumentElement();\n\n            if (lockInfoNode != null) {\n                NodeList childList = lockInfoNode.getChildNodes();\n                Node lockScopeNode = null;\n                Node lockTypeNode = null;\n                Node lockOwnerNode = null;\n\n                Node currentNode = null;\n                String nodeName = null;\n\n                for (int i = 0; i < childList.getLength(); i++) {\n                    currentNode = childList.item(i);\n\n                    if (currentNode.getNodeType() == Node.ELEMENT_NODE || currentNode.getNodeType() == Node.TEXT_NODE) {\n\n                        nodeName = currentNode.getNodeName();\n\n                        if (nodeName.endsWith(\"locktype\")) {\n                            lockTypeNode = currentNode;\n                        }\n                        if (nodeName.endsWith(\"lockscope\")) {\n                            lockScopeNode = currentNode;\n                        }\n                        if (nodeName.endsWith(\"owner\")) {\n                            lockOwnerNode = currentNode;\n                        }\n                    } else {\n                        return false;\n                    }\n                }\n\n                if (lockScopeNode != null) {\n                    String scope = null;\n                    childList = lockScopeNode.getChildNodes();\n                    for (int i = 0; i < childList.getLength(); i++) {\n                        currentNode = childList.item(i);\n\n                        if (currentNode.getNodeType() == Node.ELEMENT_NODE) {\n                            scope = currentNode.getNodeName();\n\n                            if (scope.endsWith(\"exclusive\")) {\n                                exclusive = true;\n                            } else if (scope.equals(\"shared\")) {\n                                exclusive = false;\n                            }\n                        }\n                    }\n                    if (scope == null) {\n                        return false;\n                    }\n\n                } else {\n                    return false;\n                }\n\n                if (lockTypeNode != null) {\n                    childList = lockTypeNode.getChildNodes();\n                    for (int i = 0; i < childList.getLength(); i++) {\n                        currentNode = childList.item(i);\n\n                        if (currentNode.getNodeType() == Node.ELEMENT_NODE) {\n                            type = currentNode.getNodeName();\n\n                            if (type.endsWith(\"write\")) {\n                                type = \"write\";\n                            } else if (type.equals(\"read\")) {\n                                type = \"read\";\n                            }\n                        }\n                    }\n                    if (type == null) {\n                        return false;\n                    }\n                } else {\n                    return false;\n                }\n\n                if (lockOwnerNode != null) {\n                    childList = lockOwnerNode.getChildNodes();\n                    for (int i = 0; i < childList.getLength(); i++) {\n                        currentNode = childList.item(i);\n\n                        switch (currentNode.getNodeType()) {\n                            case Node.ELEMENT_NODE:\n                                lockOwner = currentNode.getFirstChild().getNodeValue();\n                                break;\n                            case Node.TEXT_NODE:\n                                lockOwner = currentNode.getNodeValue();\n                                break;\n                        }\n                    }\n                }\n                if (lockOwner == null) {\n                    return false;\n                }\n            } else {\n                return false;\n            }\n\n        } catch (DOMException e) {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            logger.log(Level.WARNING, e, () -> \"DOM exception\");\n            return false;\n        } catch (SAXException e) {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            logger.log(Level.WARNING, e, () -> \"SAX exception\");\n            return false;\n        }\n\n        return true;\n    }\n\n    private int getTimeout( HttpServletRequest req ) {\n\n        int lockDuration = DEFAULT_TIMEOUT;\n        String lockDurationStr = req.getHeader(\"Timeout\");\n\n        if (lockDurationStr == null) {\n            lockDuration = DEFAULT_TIMEOUT;\n        } else {\n            int commaPos = lockDurationStr.indexOf(',');\n            // if multiple timeouts, just use the first one\n            if (commaPos != -1) {\n                lockDurationStr = lockDurationStr.substring(0, commaPos);\n            }\n            if (lockDurationStr.startsWith(\"Second-\")) {\n                lockDuration = Integer.valueOf(lockDurationStr.substring(7));\n            } else {\n                if (lockDurationStr.equalsIgnoreCase(\"infinity\")) {\n                    lockDuration = MAX_TIMEOUT;\n                } else {\n                    try {\n                        lockDuration = Integer.valueOf(lockDurationStr);\n                    } catch (NumberFormatException e) {\n                        lockDuration = MAX_TIMEOUT;\n                    }\n                }\n            }\n            if (lockDuration <= 0) {\n                lockDuration = DEFAULT_TIMEOUT;\n            }\n            if (lockDuration > MAX_TIMEOUT) {\n                lockDuration = MAX_TIMEOUT;\n            }\n        }\n        return lockDuration;\n    }\n\n    private void generateXMLReport( HttpServletResponse resp,\n                                    LockedObject lockedObject ) throws IOException {\n\n        HashMap<String, String> namespaces = new HashMap<String, String>();\n        namespaces.put(\"DAV:\", \"D\");\n\n        resp.setStatus(WebdavStatus.SC_OK);\n        resp.setContentType(\"text/xml; charset=UTF-8\");\n\n        XMLWriter generatedXML = new XMLWriter(resp.getWriter(), namespaces);\n        generatedXML.writeXMLHeader();\n        generatedXML.writeElement(\"DAV::prop\", XMLWriter.OPENING);\n        generatedXML.writeElement(\"DAV::lockdiscovery\", XMLWriter.OPENING);\n        generatedXML.writeElement(\"DAV::activelock\", XMLWriter.OPENING);\n\n        generatedXML.writeElement(\"DAV::locktype\", XMLWriter.OPENING);\n        generatedXML.writeProperty(\"DAV::\" + type);\n        generatedXML.writeElement(\"DAV::locktype\", XMLWriter.CLOSING);\n\n        generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.OPENING);\n        if (exclusive) {\n            generatedXML.writeProperty(\"DAV::exclusive\");\n        } else {\n            generatedXML.writeProperty(\"DAV::shared\");\n        }\n        generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.CLOSING);\n\n        int depth = lockedObject.getLockDepth();\n\n        generatedXML.writeElement(\"DAV::depth\", XMLWriter.OPENING);\n        if (depth == INFINITY) {\n            generatedXML.writeText(\"Infinity\");\n        } else {\n            generatedXML.writeText(String.valueOf(depth));\n        }\n        generatedXML.writeElement(\"DAV::depth\", XMLWriter.CLOSING);\n\n        generatedXML.writeElement(\"DAV::owner\", XMLWriter.OPENING);\n        generatedXML.writeElement(\"DAV::href\", XMLWriter.OPENING);\n        generatedXML.writeText(lockOwner);\n        generatedXML.writeElement(\"DAV::href\", XMLWriter.CLOSING);\n        generatedXML.writeElement(\"DAV::owner\", XMLWriter.CLOSING);\n\n        long timeout = lockedObject.getTimeoutMillis();\n        generatedXML.writeElement(\"DAV::timeout\", XMLWriter.OPENING);\n        generatedXML.writeText(\"Second-\" + timeout / 1000);\n        generatedXML.writeElement(\"DAV::timeout\", XMLWriter.CLOSING);\n\n        String lockToken = lockedObject.getID();\n        generatedXML.writeElement(\"DAV::locktoken\", XMLWriter.OPENING);\n        generatedXML.writeElement(\"DAV::href\", XMLWriter.OPENING);\n        generatedXML.writeText(\"opaquelocktoken:\" + lockToken);\n        generatedXML.writeElement(\"DAV::href\", XMLWriter.CLOSING);\n        generatedXML.writeElement(\"DAV::locktoken\", XMLWriter.CLOSING);\n\n        generatedXML.writeElement(\"DAV::activelock\", XMLWriter.CLOSING);\n        generatedXML.writeElement(\"DAV::lockdiscovery\", XMLWriter.CLOSING);\n        generatedXML.writeElement(\"DAV::prop\", XMLWriter.CLOSING);\n\n        resp.addHeader(\"Lock-Token\", \"<opaquelocktoken:\" + lockToken + \">\");\n\n        generatedXML.sendData();\n\n    }\n\n    private void doMacLockRequestWorkaround( ITransaction transaction,\n                                             HttpServletRequest req,\n                                             HttpServletResponse resp ) throws LockFailedException, IOException {\n        LockedObject lo;\n        int depth = getDepth(req);\n        int lockDuration = getTimeout(req);\n        if (lockDuration < 0 || lockDuration > MAX_TIMEOUT) {\n            lockDuration = DEFAULT_TIMEOUT;\n        }\n\n        boolean lockSuccess = false;\n        lockSuccess = resourceLocks.exclusiveLock(transaction, path, lockOwner, depth, lockDuration);\n\n        if (lockSuccess) {\n            // Locks successfully placed - return information about\n            lo = resourceLocks.getLockedObjectByPath(transaction, path);\n            if (lo != null) {\n                generateXMLReport(resp, lo);\n            } else {\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            }\n        } else {\n            // Locking was not successful\n            sendLockFailError(req, resp);\n        }\n    }\n\n    private void sendLockFailError( HttpServletRequest req,\n                                    HttpServletResponse resp ) throws IOException {\n        Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n        errorList.put(path, WebdavStatus.SC_LOCKED);\n        sendReport(req, resp, errorList);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoMkcol.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.AccessDeniedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.locking.IResourceLocks;\nimport peergos.server.webdav.modeshape.webdav.locking.LockedObject;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Hashtable;\nimport java.util.logging.Level;\n\npublic class DoMkcol extends AbstractMethod {\n\n    private final IWebdavStore store;\n    private final IResourceLocks resourceLocks;\n    private boolean readOnly;\n\n    public DoMkcol( IWebdavStore store,\n                    IResourceLocks resourceLocks,\n                    boolean readOnly ) {\n        this.store = store;\n        this.resourceLocks = resourceLocks;\n        this.readOnly = readOnly;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        if (readOnly) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            return;\n        }\n        String path = getRelativePath(req);\n        String parentPath = getParentPath(getCleanPath(path));\n\n        Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n\n        if (!isUnlocked(transaction, req, resourceLocks, parentPath)) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            return;\n        }\n\n        String tempLockOwner = \"doMkcol\" + System.currentTimeMillis() + req.toString();\n\n        StoredObject parentSo, so = null;\n        try {\n            if (!resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                logger.finest(\"Resource lock failed.\");\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                return;\n            }\n            parentSo = store.getStoredObject(transaction, parentPath);\n            if (parentSo == null) {\n                // parent not exists\n                logger.finest(\"Parent not exists for \" + path);\n                resp.sendError(WebdavStatus.SC_CONFLICT);\n                return;\n            }\n            if (parentPath != null && parentSo.isFolder()) {\n                so = store.getStoredObject(transaction, path);\n                if (so == null) {\n                    store.createFolder(transaction, path);\n                    resp.setStatus(WebdavStatus.SC_CREATED);\n                    return;\n                }\n                // object already exists\n                if (so.isNullResource()) {\n                    LockedObject nullResourceLo = resourceLocks.getLockedObjectByPath(transaction, path);\n                    if (nullResourceLo == null) {\n                        resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                        return;\n                    }\n                    String nullResourceLockToken = nullResourceLo.getID();\n                    String[] lockTokens = getLockIdFromIfHeader(req);\n                    String lockToken = null;\n                    if (lockTokens != null) {\n                        lockToken = lockTokens[0];\n                    } else {\n                        resp.sendError(WebdavStatus.SC_BAD_REQUEST);\n                        return;\n                    }\n                    if (lockToken.equals(nullResourceLockToken)) {\n                        so.setNullResource(false);\n                        so.setFolder(true);\n\n                        String[] nullResourceLockOwners = nullResourceLo.getOwner();\n                        String owner = null;\n                        if (nullResourceLockOwners != null) {\n                            owner = nullResourceLockOwners[0];\n                        }\n\n                        if (resourceLocks.unlock(transaction, lockToken, owner)) {\n                            resp.setStatus(WebdavStatus.SC_CREATED);\n                        } else {\n                            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                        }\n\n                    } else {\n                        errorList.put(path, WebdavStatus.SC_LOCKED);\n                        sendReport(req, resp, errorList);\n                    }\n\n                } else {\n                    String methodsAllowed = DeterminableMethod.determineMethodsAllowed(so);\n                    resp.addHeader(\"Allow\", methodsAllowed);\n                    resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n                }\n\n            } else if (parentPath != null && parentSo.isResource()) {\n                String methodsAllowed = DeterminableMethod.determineMethodsAllowed(parentSo);\n                resp.addHeader(\"Allow\", methodsAllowed);\n                resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n\n            } else {\n                resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            }\n        } catch (AccessDeniedException e) {\n            logger.log(Level.FINEST, e, () -> \"Access denied for \" + path);\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } catch (WebdavException e) {\n            logger.log(Level.FINEST, e, () -> \"Error for \" + path);\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n        } finally {\n            resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoMove.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.AccessDeniedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.ObjectAlreadyExistsException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport peergos.shared.util.PathUtil;\n\nimport java.io.IOException;\nimport java.util.Hashtable;\n\npublic class DoMove extends AbstractMethod {\n\n    private final ResourceLocks resourceLocks;\n    private final IWebdavStore store;\n    private final DoDelete doDelete;\n    private final boolean readOnly;\n\n    public DoMove( ResourceLocks resourceLocks,\n                   IWebdavStore store,\n                   DoDelete doDelete,\n                   boolean readOnly ) {\n        this.resourceLocks = resourceLocks;\n        this.store = store;\n        this.doDelete = doDelete;\n        this.readOnly = readOnly;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n\n        if (!readOnly) {\n            logger.fine(\"-- \" + this.getClass().getName());\n\n            String sourcePath = getRelativePath(req);\n            Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n\n            if (!isUnlocked(transaction, req, resourceLocks, sourcePath)) {\n                resp.setStatus(WebdavStatus.SC_LOCKED);\n                return;\n            }\n\n            if (!isUnlocked(transaction, req, resourceLocks, PathUtil.get(sourcePath).getParent().toString())) {\n                resp.setStatus(WebdavStatus.SC_LOCKED);\n                return;\n            }\n\n            String destinationPath = DoCopy.parseDestinationHeader(req, resp);\n\n            if (sourcePath.equals(destinationPath)) {\n                resp.sendError(WebdavStatus.SC_FORBIDDEN);\n                return;\n            }\n\n            if (!isUnlocked(transaction, req, resourceLocks, PathUtil.get(destinationPath).getParent().toString())) {\n                resp.setStatus(WebdavStatus.SC_LOCKED);\n                return; // parentDestination is locked\n            }\n\n            if (!isUnlocked(transaction, req, resourceLocks, destinationPath)) {\n                resp.setStatus(WebdavStatus.SC_LOCKED);\n                return; // destination is locked\n            }\n            boolean overwrite = shouldOverwrite(req);\n\n            String tempLockOwner = \"doMove\" + System.currentTimeMillis() + req.toString();\n\n            if (resourceLocks.lock(transaction, sourcePath, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                try {\n                    if (!resourceLocks.lock(transaction, destinationPath, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                        logger.finest(\"Resource lock failed.\");\n                        resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                        return;\n                    }\n                    StoredObject sourceSo = store.getStoredObject(transaction, sourcePath);\n                    // Retrieve the resources\n                    if (sourceSo == null) {\n                        resp.sendError(HttpServletResponse.SC_NOT_FOUND);\n                        return;\n                    }\n\n                    if (sourceSo.isNullResource()) {\n                        String methodsAllowed = DeterminableMethod.determineMethodsAllowed(sourceSo);\n                        resp.addHeader(\"Allow\", methodsAllowed);\n                        resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n                        return;\n                    }\n\n                    errorList = new Hashtable<String, Integer>();\n\n                    StoredObject destinationSo = store.getStoredObject(transaction, destinationPath);\n\n                    if (overwrite) {\n                        // Delete destination resource, if it exists\n                        if (destinationSo != null) {\n                            doDelete.deleteResource(transaction, destinationPath, errorList, req, resp);\n                        } else {\n                            resp.setStatus(WebdavStatus.SC_CREATED);\n                        }\n                    } else {\n                        // If the destination exists, then it's a conflict\n                        if (destinationSo != null) {\n                            resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);\n                            return;\n                        }\n                        resp.setStatus(WebdavStatus.SC_CREATED);\n                    }\n\n                    store.moveResource(transaction, sourcePath, destinationPath);\n\n                } catch (AccessDeniedException e) {\n                    resp.sendError(WebdavStatus.SC_FORBIDDEN);\n                } catch (ObjectAlreadyExistsException e) {\n                    resp.sendError(WebdavStatus.SC_NOT_FOUND, req.getRequestURI());\n                } catch (WebdavException e) {\n                    resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                } finally {\n                    resourceLocks.unlockTemporaryLockedObjects(transaction, sourcePath, tempLockOwner);\n                    resourceLocks.unlockTemporaryLockedObjects(transaction, destinationPath, tempLockOwner);\n                }\n            } else {\n                errorList.put(req.getHeader(\"Destination\"), WebdavStatus.SC_LOCKED);\n                sendReport(req, resp, errorList);\n            }\n        } else {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n\n        }\n    }\n\n    private boolean shouldOverwrite( HttpServletRequest req ) {\n        boolean overwrite = true;\n        String overwriteHeader = req.getHeader(\"Overwrite\");\n\n        if (overwriteHeader != null) {\n            overwrite = overwriteHeader.equalsIgnoreCase(\"T\");\n        }\n        return overwrite;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoNotImplemented.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.util.Logging;\nimport peergos.server.webdav.modeshape.webdav.IMethodExecutor;\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.logging.Logger;\n\npublic class DoNotImplemented implements IMethodExecutor {\n\n    private static Logger LOG = Logging.LOG();\n\n    private final boolean readOnly;\n\n    public DoNotImplemented( boolean readOnly ) {\n        this.readOnly = readOnly;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException {\n        LOG.fine(\"-- \" + req.getMethod());\n        if (readOnly) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } else {\n            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoOptions.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.AccessDeniedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic class DoOptions extends DeterminableMethod {\n\n    private IWebdavStore store;\n    private ResourceLocks resourceLocks;\n\n    public DoOptions( IWebdavStore store,\n                      ResourceLocks resLocks ) {\n        this.store = store;\n        resourceLocks = resLocks;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        String tempLockOwner = \"doOptions\" + System.currentTimeMillis() + req.toString();\n        String path = getRelativePath(req);\n        if (resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n            StoredObject so = null;\n            try {\n                resp.addHeader(\"DAV\", \"1, 2\");\n\n                so = store.getStoredObject(transaction, path);\n                String methodsAllowed = determineMethodsAllowed(so);\n                resp.addHeader(\"Allow\", methodsAllowed);\n                resp.addHeader(\"MS-Author-Via\", \"DAV\");\n            } catch (AccessDeniedException e) {\n                resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            } catch (WebdavException e) {\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            } finally {\n                resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n            }\n        } else {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoPropfind.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.*;\nimport peergos.server.webdav.modeshape.webdav.exceptions.AccessDeniedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.RequestUtil;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.XMLHelper;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.XMLWriter;\nimport peergos.server.webdav.modeshape.webdav.locking.LockedObject;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.xml.sax.InputSource;\n\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport javax.xml.parsers.DocumentBuilder;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.logging.Level;\n\npublic class DoPropfind extends AbstractMethod {\n\n    /**\n     * PROPFIND - Specify a property mask.\n     */\n    private static final int FIND_BY_PROPERTY = 0;\n\n    /**\n     * PROPFIND - Display all properties.\n     */\n    private static final int FIND_ALL_PROP = 1;\n\n    /**\n     * PROPFIND - Return property names.\n     */\n    private static final int FIND_PROPERTY_NAMES = 2;\n\n    private final IWebdavStore store;\n    private final ResourceLocks resourceLocks;\n    private final IMimeTyper mimeTyper;\n\n    private int depth;\n\n    public DoPropfind( IWebdavStore store,\n                       ResourceLocks resLocks,\n                       IMimeTyper mimeTyper ) {\n        this.store = store;\n        this.resourceLocks = resLocks;\n        this.mimeTyper = mimeTyper;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        // Retrieve the resources\n        String path = getCleanPath(getRelativePath(req));\n        String tempLockOwner = \"doPropfind\" + System.currentTimeMillis() + req.toString();\n\n        String userAgent = req.getHeader(\"User-Agent\");\n        if (ignoreRequest(userAgent, path)) {\n            resp.sendError(HttpServletResponse.SC_NOT_FOUND);\n            return;\n        }\n        depth = getDepth(req);\n\n        if (resourceLocks.lock(transaction, path, tempLockOwner, false, depth, TEMP_TIMEOUT, TEMPORARY)) {\n\n            StoredObject so = null;\n            try {\n                so = store.getStoredObject(transaction, path);\n                if (so == null) {\n                    resp.setContentType(\"text/xml; charset=UTF-8\");\n                    resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());\n                    return;\n                }\n\n                Vector<String> properties = null;\n                path = getCleanPath(getRelativePath(req));\n\n                int propertyFindType = FIND_ALL_PROP;\n                Node propNode = null;\n\n                if (RequestUtil.streamNotConsumed(req)) {\n                    DocumentBuilder documentBuilder = getDocumentBuilder();\n                    try {\n                        Document document = documentBuilder.parse(new InputSource(req.getInputStream()));\n                        // Get the root element of the document\n                        Element rootElement = document.getDocumentElement();\n\n                        propNode = XMLHelper.findSubElement(rootElement, \"prop\");\n                        if (propNode != null) {\n                            propertyFindType = FIND_BY_PROPERTY;\n                        } else if (XMLHelper.findSubElement(rootElement, \"propname\") != null) {\n                            propertyFindType = FIND_PROPERTY_NAMES;\n                        } else if (XMLHelper.findSubElement(rootElement, \"allprop\") != null) {\n                            propertyFindType = FIND_ALL_PROP;\n                        }\n                    } catch (Exception e) {\n                        resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                        return;\n                    }\n                } else {\n                    // no content, which means it is a allprop request\n                    propertyFindType = FIND_ALL_PROP;\n                }\n\n                HashMap<String, String> namespaces = new HashMap<String, String>();\n                namespaces.put(\"DAV:\", \"D\");\n                namespaces.putAll(store.getCustomNamespaces(transaction, path));\n\n                if (propertyFindType == FIND_BY_PROPERTY) {\n                    propertyFindType = 0;\n                    properties = XMLHelper.getPropertiesFromXML(propNode);\n                }\n\n                resp.setStatus(WebdavStatus.SC_MULTI_STATUS);\n                resp.setContentType(\"text/xml; charset=UTF-8\");\n\n                // Create multistatus object\n                XMLWriter generatedXML = new XMLWriter(resp.getWriter(), namespaces);\n                generatedXML.writeXMLHeader();\n                generatedXML.writeElement(\"DAV::multistatus\", XMLWriter.OPENING);\n                if (depth == 0) {\n                    parseProperties(transaction,\n                                    req,\n                                    generatedXML,\n                                    path,\n                                    propertyFindType,\n                                    properties,\n                                    mimeTyper.getMimeType(transaction, path));\n                } else {\n                    recursiveParseProperties(transaction,\n                                             path,\n                                             req,\n                                             generatedXML,\n                                             propertyFindType,\n                                             properties,\n                                             depth,\n                                             mimeTyper.getMimeType(transaction, path));\n                }\n                generatedXML.writeElement(\"DAV::multistatus\", XMLWriter.CLOSING);\n                logger.fine(\"Sending response: \" + generatedXML.toString());\n                generatedXML.sendData();\n            } catch (AccessDeniedException e) {\n                resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            } catch (WebdavException e) {\n                logger.log(Level.WARNING, e, () -> \"Sending internal error!\");\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            } catch (ServletException e) {\n                logger.log(Level.WARNING, e, () -> \"Cannot create the xml document builder\");\n            } finally {\n                resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n            }\n        } else {\n            Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n            errorList.put(path, WebdavStatus.SC_LOCKED);\n            sendReport(req, resp, errorList);\n        }\n    }\n\n    /**\n     * goes recursive through all folders. used by propfind\n     * \n     * @param transaction\n     * @param currentPath the current path\n     * @param req HttpServletRequest\n     * @param generatedXML\n     * @param propertyFindType\n     * @param properties\n     * @param depth depth of the propfind\n     * @param mimeType\n     * @throws WebdavException\n     */\n    private void recursiveParseProperties( ITransaction transaction,\n                                           String currentPath,\n                                           HttpServletRequest req,\n                                           XMLWriter generatedXML,\n                                           int propertyFindType,\n                                           Vector<String> properties,\n                                           int depth,\n                                           String mimeType ) throws WebdavException {\n\n        parseProperties(transaction, req, generatedXML, currentPath, propertyFindType, properties, mimeType);\n\n        if (depth > 0) {\n            // no need to get name if depth is already zero\n            String[] names = store.getChildrenNames(transaction, currentPath);\n            names = names == null ? new String[] {} : names;\n            String newPath = null;\n\n            for (String name : names) {\n                newPath = currentPath;\n                if (!(newPath.endsWith(\"/\"))) {\n                    newPath += \"/\";\n                }\n                newPath += name;\n                recursiveParseProperties(transaction,\n                                         newPath,\n                                         req,\n                                         generatedXML,\n                                         propertyFindType,\n                                         properties,\n                                         depth - 1,\n                                         mimeType);\n            }\n        }\n    }\n\n    /**\n     * Propfind helper method.\n     * \n     * @param transaction\n     * @param req The servlet request\n     * @param generatedXML XML response to the Propfind request\n     * @param path Path of the current resource\n     * @param type Propfind type\n     * @param propertiesVector If the propfind type is find properties by name, then this Vector contains those properties\n     * @param mimeType\n     * @throws WebdavException\n     */\n    private void parseProperties( ITransaction transaction,\n                                  HttpServletRequest req,\n                                  XMLWriter generatedXML,\n                                  String path,\n                                  int type,\n                                  Vector<String> propertiesVector,\n                                  String mimeType ) throws WebdavException {\n\n        StoredObject so = store.getStoredObject(transaction, path);\n        if (so == null) return;\n\n        boolean isFolder = so.isFolder();\n        final String creationdate = creationDateFormat(so.getCreationDate());\n        final String lastModified = lastModifiedDateFormat(so.getLastModified());\n        String resourceLength = String.valueOf(so.getResourceLength());\n\n        // ResourceInfo resourceInfo = new ResourceInfo(path, resources);\n\n        generatedXML.writeElement(\"DAV::response\", XMLWriter.OPENING);\n        String status = \"HTTP/1.1 \" + WebdavStatus.SC_OK + \" \" + WebdavStatus.getStatusText(WebdavStatus.SC_OK);\n\n        // Generating href element\n        generatedXML.writeElement(\"DAV::href\", XMLWriter.OPENING);\n\n        String href = req.getContextPath();\n        String servletPath = req.getServletPath();\n        if (servletPath != null) {\n            if ((href.endsWith(\"/\")) && (servletPath.startsWith(\"/\"))) {\n                href += servletPath.substring(1);\n            } else {\n                href += servletPath;\n            }\n        }\n        if ((href.endsWith(\"/\")) && (path.startsWith(\"/\"))) {\n            href += path.substring(1);\n        } else {\n            href += path;\n        }\n        if ((isFolder) && (!href.endsWith(\"/\"))) {\n            href += \"/\";\n        }\n\n        generatedXML.writeText(rewriteUrl(href));\n\n        generatedXML.writeElement(\"DAV::href\", XMLWriter.CLOSING);\n\n        String resourceName = path;\n        int lastSlash = path.lastIndexOf('/');\n        if (lastSlash != -1) {\n            resourceName = resourceName.substring(lastSlash + 1);\n        }\n\n        switch (type) {\n\n            case FIND_ALL_PROP:\n                generatedXML.writeElement(\"DAV::propstat\", XMLWriter.OPENING);\n\n                generatedXML.writeElement(\"DAV::prop\", XMLWriter.OPENING);\n                writeCustomProperties(transaction, generatedXML, path, true, propertiesVector);\n\n                generatedXML.writeProperty(\"DAV::creationdate\", creationdate);\n                generatedXML.writeElement(\"DAV::displayname\", XMLWriter.OPENING);\n                generatedXML.writeData(resourceName);\n                generatedXML.writeElement(\"DAV::displayname\", XMLWriter.CLOSING);\n                if (!isFolder) {\n                    generatedXML.writeProperty(\"DAV::getlastmodified\", lastModified);\n                    generatedXML.writeProperty(\"DAV::getcontentlength\", resourceLength);\n                    if (mimeType != null) {\n                        generatedXML.writeProperty(\"DAV::getcontenttype\", mimeType);\n                    }\n                    generatedXML.writeProperty(\"DAV::getetag\", getETag(so));\n                    generatedXML.writeElement(\"DAV::resourcetype\", XMLWriter.NO_CONTENT);\n                } else {\n                    generatedXML.writeElement(\"DAV::resourcetype\", XMLWriter.OPENING);\n                    generatedXML.writeElement(\"DAV::collection\", XMLWriter.NO_CONTENT);\n                    generatedXML.writeElement(\"DAV::resourcetype\", XMLWriter.CLOSING);\n                }\n\n                writeSupportedLockElements(transaction, generatedXML, path);\n\n                writeLockDiscoveryElements(transaction, generatedXML, path);\n\n                generatedXML.writeProperty(\"DAV::source\", \"\");\n                generatedXML.writeElement(\"DAV::prop\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.OPENING);\n                generatedXML.writeText(status);\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::propstat\", XMLWriter.CLOSING);\n\n                break;\n\n            case FIND_PROPERTY_NAMES:\n                generatedXML.writeElement(\"DAV::propstat\", XMLWriter.OPENING);\n                generatedXML.writeElement(\"DAV::prop\", XMLWriter.OPENING);\n\n                writeCustomProperties(transaction, generatedXML, path, false, propertiesVector);\n                generatedXML.writeElement(\"DAV::creationdate\", XMLWriter.NO_CONTENT);\n                generatedXML.writeElement(\"DAV::displayname\", XMLWriter.NO_CONTENT);\n                if (!isFolder) {\n                    generatedXML.writeElement(\"DAV::getcontentlanguage\", XMLWriter.NO_CONTENT);\n                    generatedXML.writeElement(\"DAV::getcontentlength\", XMLWriter.NO_CONTENT);\n                    generatedXML.writeElement(\"DAV::getcontenttype\", XMLWriter.NO_CONTENT);\n                    generatedXML.writeElement(\"DAV::getetag\", XMLWriter.NO_CONTENT);\n                    generatedXML.writeElement(\"DAV::getlastmodified\", XMLWriter.NO_CONTENT);\n                }\n                generatedXML.writeElement(\"DAV::resourcetype\", XMLWriter.NO_CONTENT);\n                generatedXML.writeElement(\"DAV::supportedlock\", XMLWriter.NO_CONTENT);\n                generatedXML.writeElement(\"DAV::source\", XMLWriter.NO_CONTENT);\n\n                generatedXML.writeElement(\"DAV::prop\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.OPENING);\n                generatedXML.writeText(status);\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::propstat\", XMLWriter.CLOSING);\n\n                break;\n\n            case FIND_BY_PROPERTY:\n\n                Vector<String> propertiesNotFound = new Vector<String>();\n\n                // Parse the list of properties\n\n                generatedXML.writeElement(\"DAV::propstat\", XMLWriter.OPENING);\n                generatedXML.writeElement(\"DAV::prop\", XMLWriter.OPENING);\n\n                writeCustomProperties(transaction, generatedXML, path, true, propertiesVector);\n\n                Enumeration<String> properties = propertiesVector.elements();\n\n                while (properties.hasMoreElements()) {\n\n                    String property = properties.nextElement();\n\n                    if (property.equals(\"DAV::creationdate\")) {\n                        generatedXML.writeProperty(\"DAV::creationdate\", creationdate);\n                    } else if (property.equals(\"DAV::displayname\")) {\n                        generatedXML.writeElement(\"DAV::displayname\", XMLWriter.OPENING);\n                        generatedXML.writeData(resourceName);\n                        generatedXML.writeElement(\"DAV::displayname\", XMLWriter.CLOSING);\n                    } else if (property.equals(\"DAV::getcontentlanguage\")) {\n                        if (isFolder) {\n                            propertiesNotFound.addElement(property);\n                        } else {\n                            generatedXML.writeElement(\"DAV::getcontentlanguage\", XMLWriter.NO_CONTENT);\n                        }\n                    } else if (property.equals(\"DAV::getcontentlength\")) {\n                        if (isFolder) {\n                            propertiesNotFound.addElement(property);\n                        } else {\n                            generatedXML.writeProperty(\"DAV::getcontentlength\", resourceLength);\n                        }\n                    } else if (property.equals(\"DAV::getcontenttype\")) {\n                        if (isFolder) {\n                            propertiesNotFound.addElement(property);\n                        } else {\n                            generatedXML.writeProperty(\"DAV::getcontenttype\", mimeType);\n                        }\n                    } else if (property.equals(\"DAV::getetag\")) {\n                        if (isFolder || so.isNullResource()) {\n                            propertiesNotFound.addElement(property);\n                        } else {\n                            generatedXML.writeProperty(\"DAV::getetag\", getETag(so));\n                        }\n                    } else if (property.equals(\"DAV::getlastmodified\")) {\n                        if (isFolder) {\n                            propertiesNotFound.addElement(property);\n                        } else {\n                            generatedXML.writeProperty(\"DAV::getlastmodified\", lastModified);\n                        }\n                    } else if (property.equals(\"DAV::resourcetype\")) {\n                        if (isFolder) {\n                            generatedXML.writeElement(\"DAV::resourcetype\", XMLWriter.OPENING);\n                            generatedXML.writeElement(\"DAV::collection\", XMLWriter.NO_CONTENT);\n                            generatedXML.writeElement(\"DAV::resourcetype\", XMLWriter.CLOSING);\n                        } else {\n                            generatedXML.writeElement(\"DAV::resourcetype\", XMLWriter.NO_CONTENT);\n                        }\n                    } else if (property.equals(\"DAV::source\")) {\n                        generatedXML.writeProperty(\"DAV::source\", \"\");\n                    } else if (property.equals(\"DAV::supportedlock\")) {\n\n                        writeSupportedLockElements(transaction, generatedXML, path);\n\n                    } else if (property.equals(\"DAV::lockdiscovery\")) {\n\n                        writeLockDiscoveryElements(transaction, generatedXML, path);\n\n                    } else {\n                        propertiesNotFound.addElement(property);\n                    }\n\n                }\n\n                generatedXML.writeElement(\"DAV::prop\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.OPENING);\n                generatedXML.writeText(status);\n                generatedXML.writeElement(\"DAV::status\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::propstat\", XMLWriter.CLOSING);\n\n                Enumeration<String> propertiesNotFoundList = propertiesNotFound.elements();\n\n                if (propertiesNotFoundList.hasMoreElements()) {\n\n                    status = \"HTTP/1.1 \" + WebdavStatus.SC_NOT_FOUND + \" \"\n                             + WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND);\n\n                    generatedXML.writeElement(\"DAV::propstat\", XMLWriter.OPENING);\n                    generatedXML.writeElement(\"DAV::prop\", XMLWriter.OPENING);\n\n                    while (propertiesNotFoundList.hasMoreElements()) {\n                        generatedXML.writeElement(propertiesNotFoundList.nextElement(), XMLWriter.NO_CONTENT);\n                    }\n\n                    generatedXML.writeElement(\"DAV::prop\", XMLWriter.CLOSING);\n                    generatedXML.writeElement(\"DAV::status\", XMLWriter.OPENING);\n                    generatedXML.writeText(status);\n                    generatedXML.writeElement(\"DAV::status\", XMLWriter.CLOSING);\n                    generatedXML.writeElement(\"DAV::propstat\", XMLWriter.CLOSING);\n\n                }\n\n                break;\n\n        }\n\n        generatedXML.writeElement(\"DAV::response\", XMLWriter.CLOSING);\n\n        so = null;\n    }\n\n    private void writeSupportedLockElements( ITransaction transaction,\n                                             XMLWriter generatedXML,\n                                             String path ) {\n        LockedObject lo = resourceLocks.getLockedObjectByPath(transaction, path);\n\n        generatedXML.writeElement(\"DAV::supportedlock\", XMLWriter.OPENING);\n\n        if (lo == null) {\n            // both locks (shared/exclusive) can be granted\n            generatedXML.writeElement(\"DAV::lockentry\", XMLWriter.OPENING);\n\n            generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.OPENING);\n            generatedXML.writeElement(\"DAV::exclusive\", XMLWriter.NO_CONTENT);\n            generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::locktype\", XMLWriter.OPENING);\n            generatedXML.writeElement(\"DAV::write\", XMLWriter.NO_CONTENT);\n            generatedXML.writeElement(\"DAV::locktype\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::lockentry\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::lockentry\", XMLWriter.OPENING);\n\n            generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.OPENING);\n            generatedXML.writeElement(\"DAV::shared\", XMLWriter.NO_CONTENT);\n            generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::locktype\", XMLWriter.OPENING);\n            generatedXML.writeElement(\"DAV::write\", XMLWriter.NO_CONTENT);\n            generatedXML.writeElement(\"DAV::locktype\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::lockentry\", XMLWriter.CLOSING);\n\n        } else {\n            // LockObject exists, checking lock state\n            // if an exclusive lock exists, no further lock is possible\n            if (lo.isShared()) {\n\n                generatedXML.writeElement(\"DAV::lockentry\", XMLWriter.OPENING);\n\n                generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.OPENING);\n                generatedXML.writeElement(\"DAV::shared\", XMLWriter.NO_CONTENT);\n                generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.CLOSING);\n\n                generatedXML.writeElement(\"DAV::locktype\", XMLWriter.OPENING);\n                generatedXML.writeElement(\"DAV::\" + lo.getType(), XMLWriter.NO_CONTENT);\n                generatedXML.writeElement(\"DAV::locktype\", XMLWriter.CLOSING);\n\n                generatedXML.writeElement(\"DAV::lockentry\", XMLWriter.CLOSING);\n            }\n        }\n\n        generatedXML.writeElement(\"DAV::supportedlock\", XMLWriter.CLOSING);\n\n        lo = null;\n    }\n\n    private void writeLockDiscoveryElements( ITransaction transaction,\n                                             XMLWriter generatedXML,\n                                             String path ) {\n\n        LockedObject lo = resourceLocks.getLockedObjectByPath(transaction, path);\n\n        if (lo != null && !lo.hasExpired()) {\n\n            generatedXML.writeElement(\"DAV::lockdiscovery\", XMLWriter.OPENING);\n            generatedXML.writeElement(\"DAV::activelock\", XMLWriter.OPENING);\n\n            generatedXML.writeElement(\"DAV::locktype\", XMLWriter.OPENING);\n            generatedXML.writeProperty(\"DAV::\" + lo.getType());\n            generatedXML.writeElement(\"DAV::locktype\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.OPENING);\n            if (lo.isExclusive()) {\n                generatedXML.writeProperty(\"DAV::exclusive\");\n            } else {\n                generatedXML.writeProperty(\"DAV::shared\");\n            }\n            generatedXML.writeElement(\"DAV::lockscope\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::depth\", XMLWriter.OPENING);\n            if (depth == INFINITY) {\n                generatedXML.writeText(\"Infinity\");\n            } else {\n                generatedXML.writeText(String.valueOf(depth));\n            }\n            generatedXML.writeElement(\"DAV::depth\", XMLWriter.CLOSING);\n\n            String[] owners = lo.getOwner();\n            if (owners != null) {\n                for (int i = 0; i < owners.length; i++) {\n                    generatedXML.writeElement(\"DAV::owner\", XMLWriter.OPENING);\n                    generatedXML.writeElement(\"DAV::href\", XMLWriter.OPENING);\n                    generatedXML.writeText(owners[i]);\n                    generatedXML.writeElement(\"DAV::href\", XMLWriter.CLOSING);\n                    generatedXML.writeElement(\"DAV::owner\", XMLWriter.CLOSING);\n                }\n            } else {\n                generatedXML.writeElement(\"DAV::owner\", XMLWriter.NO_CONTENT);\n            }\n\n            int timeout = (int)(lo.getTimeoutMillis() / 1000);\n            String timeoutStr = Integer.toString(timeout);\n            generatedXML.writeElement(\"DAV::timeout\", XMLWriter.OPENING);\n            generatedXML.writeText(\"Second-\" + timeoutStr);\n            generatedXML.writeElement(\"DAV::timeout\", XMLWriter.CLOSING);\n\n            String lockToken = lo.getID();\n\n            generatedXML.writeElement(\"DAV::locktoken\", XMLWriter.OPENING);\n            generatedXML.writeElement(\"DAV::href\", XMLWriter.OPENING);\n            generatedXML.writeText(\"opaquelocktoken:\" + lockToken);\n            generatedXML.writeElement(\"DAV::href\", XMLWriter.CLOSING);\n            generatedXML.writeElement(\"DAV::locktoken\", XMLWriter.CLOSING);\n\n            generatedXML.writeElement(\"DAV::activelock\", XMLWriter.CLOSING);\n            generatedXML.writeElement(\"DAV::lockdiscovery\", XMLWriter.CLOSING);\n\n        } else {\n            generatedXML.writeElement(\"DAV::lockdiscovery\", XMLWriter.NO_CONTENT);\n        }\n\n        lo = null;\n    }\n\n    private void writeCustomProperties( ITransaction transaction,\n                                        XMLWriter generatedXML,\n                                        String path,\n                                        boolean includeValue,\n                                        Vector<String> propertiesFilter ) {\n        Map<String, Object> customProperties = store.getCustomProperties(transaction, path);\n        if (customProperties.isEmpty()) {\n            return;\n        }\n        for (String propertyName : customProperties.keySet()) {\n            if (propertiesFilter != null && !propertiesFilter.contains(propertyName)) {\n                continue;\n            }\n            if (includeValue) {\n                generatedXML.writeElement(propertyName, XMLWriter.OPENING);\n                final String value = customProperties.get(propertyName).toString();\n                generatedXML.writeData(value);\n                generatedXML.writeElement(propertyName, XMLWriter.CLOSING);\n            } else {\n                generatedXML.writeElement(propertyName, XMLWriter.NO_CONTENT);\n            }\n        }\n    }\n\n    private boolean ignoreRequest( String userAgent, String requestPath ) {\n        if (userAgent != null && ! userAgent.trim().isEmpty() && userAgent.toLowerCase().startsWith(\"microsoft\")) {\n            //microsoft web explorer sends some funky propfind requests which we need to ignore\n             if (requestPath.endsWith(\"desktop.ini\") || requestPath.endsWith(\"folder.jpg\") || requestPath.endsWith(\"folder.gif\")) {\n                 return true;\n             }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoProppatch.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.AccessDeniedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.RequestUtil;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.XMLHelper;\nimport peergos.server.webdav.modeshape.webdav.fromcatalina.XMLWriter;\nimport peergos.server.webdav.modeshape.webdav.locking.LockedObject;\nimport peergos.server.webdav.modeshape.webdav.locking.ResourceLocks;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.xml.sax.InputSource;\n\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport javax.xml.parsers.DocumentBuilder;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.logging.Level;\n\npublic class DoProppatch extends AbstractMethod {\n\n    private boolean readOnly;\n    private IWebdavStore store;\n    private ResourceLocks resourceLocks;\n\n    public DoProppatch( IWebdavStore store,\n                        ResourceLocks resLocks,\n                        boolean readOnly ) {\n        this.readOnly = readOnly;\n        this.store = store;\n        this.resourceLocks = resLocks;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        if (readOnly) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            return;\n        }\n\n        String path = getRelativePath(req);\n        String parentPath = getParentPath(getCleanPath(path));\n\n        Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n\n        if (!isUnlocked(transaction, req, resourceLocks, parentPath)) {\n            resp.setStatus(WebdavStatus.SC_LOCKED);\n            return; // parent is locked\n        }\n\n        if (!isUnlocked(transaction, req, resourceLocks, path)) {\n            resp.setStatus(WebdavStatus.SC_LOCKED);\n            return; // resource is locked\n        }\n\n        // Retrieve the resources\n        String tempLockOwner = \"doProppatch\" + System.currentTimeMillis() + req.toString();\n\n        if (resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n            StoredObject so = null;\n            LockedObject lo = null;\n            try {\n                so = store.getStoredObject(transaction, path);\n                lo = resourceLocks.getLockedObjectByPath(transaction, getCleanPath(path));\n\n                if (so == null) {\n                    resp.sendError(HttpServletResponse.SC_NOT_FOUND);\n                    return;\n                    // we do not to continue since there is no root\n                    // resource\n                }\n\n                if (so.isNullResource()) {\n                    String methodsAllowed = DeterminableMethod.determineMethodsAllowed(so);\n                    resp.addHeader(\"Allow\", methodsAllowed);\n                    resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);\n                    return;\n                }\n\n                String[] lockTokens = getLockIdFromIfHeader(req);\n                boolean lockTokenMatchesIfHeader = (lockTokens != null && lockTokens[0].equals(lo.getID()));\n                if (lo != null && lo.isExclusive() && !lockTokenMatchesIfHeader) {\n                    // Object on specified path is LOCKED\n                    errorList = new Hashtable<String, Integer>();\n                    errorList.put(path, WebdavStatus.SC_LOCKED);\n                    sendReport(req, resp, errorList);\n                    return;\n                }\n\n                Map<String, Object> propertiesToSet = new HashMap<String, Object>();\n                List<String> propertiesToRemove = new ArrayList<String>();\n                List<String> allProperties = new Vector<String>();\n                Map<String, String> response = null;\n\n                path = getCleanPath(getRelativePath(req));\n\n                Node tosetNode = null;\n                Node toremoveNode = null;\n\n                if (RequestUtil.streamNotConsumed(req)) {\n                    DocumentBuilder documentBuilder = getDocumentBuilder();\n                    try {\n                        Document document = documentBuilder.parse(new InputSource(req.getInputStream()));\n                        // Get the root element of the document\n                        Element rootElement = document.getDocumentElement();\n\n                        tosetNode = XMLHelper.findSubElement(XMLHelper.findSubElement(rootElement, \"set\"), \"prop\");\n                        if (tosetNode != null) {\n                            propertiesToSet = XMLHelper.getPropertiesWithValuesFromXML(tosetNode);\n                        }\n\n                        toremoveNode = XMLHelper.findSubElement(XMLHelper.findSubElement(rootElement, \"remove\"), \"prop\");\n                        if (toremoveNode != null) {\n                            propertiesToRemove = XMLHelper.getPropertiesFromXML(toremoveNode);\n                        }\n                        if (!propertiesToSet.isEmpty() || !propertiesToRemove.isEmpty()) {\n                            response = store.setCustomProperties(transaction, path, propertiesToSet, propertiesToRemove);\n                        }\n                    } catch (Exception e) {\n                        resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                        return;\n                    }\n                } else {\n                    // no content: error\n                    resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                    return;\n                }\n\n                allProperties.addAll(propertiesToSet.keySet());\n                allProperties.addAll(propertiesToRemove);\n\n                HashMap<String, String> namespaces = new HashMap<String, String>();\n                namespaces.put(\"DAV:\", \"D\");\n\n                resp.setStatus(WebdavStatus.SC_MULTI_STATUS);\n                resp.setContentType(\"text/xml; charset=UTF-8\");\n\n                // Create multistatus object\n                XMLWriter generatedXML = new XMLWriter(resp.getWriter(), namespaces);\n                generatedXML.writeXMLHeader();\n                generatedXML.writeElement(\"DAV::multistatus\", XMLWriter.OPENING);\n\n                generatedXML.writeElement(\"DAV::response\", XMLWriter.OPENING);\n\n                // Generating href element\n                generatedXML.writeElement(\"DAV::href\", XMLWriter.OPENING);\n\n                String href = req.getContextPath();\n                if ((href.endsWith(\"/\")) && (path.startsWith(\"/\"))) {\n                    href += path.substring(1);\n                } else {\n                    href += path;\n                }\n                if ((so.isFolder()) && (!href.endsWith(\"/\"))) {\n                    href += \"/\";\n                }\n\n                generatedXML.writeText(rewriteUrl(href));\n\n                generatedXML.writeElement(\"DAV::href\", XMLWriter.CLOSING);\n\n                for (String property : allProperties) {\n                    generatedXML.writeElement(\"DAV::propstat\", XMLWriter.OPENING);\n\n                    generatedXML.writeElement(\"DAV::prop\", XMLWriter.OPENING);\n                    generatedXML.writeElement(property, XMLWriter.NO_CONTENT);\n                    generatedXML.writeElement(\"DAV::prop\", XMLWriter.CLOSING);\n\n                    generatedXML.writeElement(\"DAV::status\", XMLWriter.OPENING);\n                    generatedXML.writeText(statusForProperty(property, response));\n                    generatedXML.writeElement(\"DAV::status\", XMLWriter.CLOSING);\n\n                    generatedXML.writeElement(\"DAV::propstat\", XMLWriter.CLOSING);\n                }\n\n                if (response != null && !response.isEmpty()) {\n                    String firstErrorMessage = response.entrySet().iterator().next().getValue();\n                    generatedXML.writeProperty(\"DAV::responsedescription\", firstErrorMessage);\n                }\n                generatedXML.writeElement(\"DAV::response\", XMLWriter.CLOSING);\n                generatedXML.writeElement(\"DAV::multistatus\", XMLWriter.CLOSING);\n\n                    logger.fine(\"Sending response: \" + generatedXML);\n                generatedXML.sendData();\n            } catch (AccessDeniedException e) {\n                resp.sendError(WebdavStatus.SC_FORBIDDEN);\n            } catch (WebdavException e) {\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            } catch (ServletException e) {\n                logger.log(Level.WARNING, e, () -> \"Cannot create document builder\");\n            } finally {\n                resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n            }\n        } else {\n            resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n        }\n    }\n\n    private String statusForProperty( String propertyName,\n                                      Map<String, String> response ) {\n        if (response == null || response.isEmpty()) {\n            return \"HTTP/1.1 \" + WebdavStatus.SC_OK + \" \" + WebdavStatus.getStatusText(WebdavStatus.SC_OK);\n        } else if (response.containsKey(propertyName)) {\n            return \"HTTP/1.1 \" + WebdavStatus.SC_BAD_REQUEST + \" \" + WebdavStatus.getStatusText(WebdavStatus.SC_BAD_REQUEST);\n        } else {\n            return \"HTTP/1.1 \" + WebdavStatus.SC_FAILED_DEPENDENCY + \" \"\n                   + WebdavStatus.getStatusText(WebdavStatus.SC_FAILED_DEPENDENCY);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoPut.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport org.peergos.util.Pair;\nimport peergos.server.simulation.InputStreamAsyncReader;\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.AccessDeniedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.exceptions.WebdavException;\nimport peergos.server.webdav.modeshape.webdav.locking.IResourceLocks;\nimport peergos.server.webdav.modeshape.webdav.locking.LockedObject;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Hashtable;\nimport java.util.Iterator;\nimport java.util.Optional;\nimport java.util.logging.Level;\n\npublic class DoPut extends AbstractMethod {\n\n    private final IWebdavStore store;\n    private final IResourceLocks resourceLocks;\n    private final boolean readOnly;\n    private final boolean lazyFolderCreationOnPut;\n\n    private String userAgent;\n\n    public DoPut( IWebdavStore store,\n                  IResourceLocks resLocks,\n                  boolean readOnly,\n                  boolean lazyFolderCreationOnPut ) {\n        this.store = store;\n        this.resourceLocks = resLocks;\n        this.readOnly = readOnly;\n        this.lazyFolderCreationOnPut = lazyFolderCreationOnPut;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        if (!readOnly) {\n            String path = getRelativePath(req);\n            String parentPath = getParentPath(path);\n\n            userAgent = req.getHeader(\"User-Agent\");\n\n            if (isOSXFinder() && req.getContentLength() == 0) {\n                // OS X Finder sends 2 PUTs; first has 0 content, second has content.\n                // This is the first one, so we'll ignore it ...\n                logger.fine(\"-- First of multiple OS-X Finder PUT calls at \" + path);\n            }\n\n            Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();\n\n            if (isOSXFinder()) {\n                // OS X Finder sends 2 PUTs; first has 0 content, second has content.\n                // This is the second one that was preceded by a LOCK, so don't need to check the locks ...\n            } else {\n                if (!isUnlocked(transaction, req, resourceLocks, parentPath)) {\n                    logger.fine(\"-- Locked parent at \" + path);\n                    resp.setStatus(WebdavStatus.SC_LOCKED);\n                    return; // parent is locked\n                }\n\n                if (!isUnlocked(transaction, req, resourceLocks, path)) {\n                    logger.fine(\"-- Locked resource at \" + path);\n                    resp.setStatus(WebdavStatus.SC_LOCKED);\n                    return; // resource is locked\n                }\n            }\n\n            String tempLockOwner = \"doPut\" + System.currentTimeMillis() + req.toString();\n            if (resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n                StoredObject parentSo, so = null;\n                try {\n                    parentSo = store.getStoredObject(transaction, parentPath);\n                    if (parentPath != null && parentSo != null && parentSo.isResource()) {\n                        resp.sendError(WebdavStatus.SC_FORBIDDEN);\n                        return;\n\n                    } else if (parentPath != null && parentSo == null && lazyFolderCreationOnPut) {\n                        store.createFolder(transaction, parentPath);\n\n                    } else if (parentPath != null && parentSo == null && !lazyFolderCreationOnPut) {\n                        errorList.put(parentPath, WebdavStatus.SC_NOT_FOUND);\n                        sendReport(req, resp, errorList);\n                        return;\n                    }\n\n                    logger.fine(\"-- Looking for the stored object at \" + path);\n                    so = store.getStoredObject(transaction, path);\n\n                    if (so == null) {\n                        logger.fine(\"-- Creating resource in the store at \" + path);\n                        store.createResource(transaction, path);\n                        // resp.setStatus(WebdavStatus.SC_CREATED);\n                    } else {\n                        // This has already been created, just update the data\n                        logger.fine(\"-- There is already a resource at \" + path);\n                        if (so.isNullResource()) {\n\n                            LockedObject nullResourceLo = resourceLocks.getLockedObjectByPath(transaction, path);\n                            if (nullResourceLo == null) {\n                                logger.fine(\"-- Unable to obtain resource lock object at \" + path);\n                                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                                return;\n                            }\n                            logger.fine(\"-- Found resource lock object at \" + path);\n                            String nullResourceLockToken = nullResourceLo.getID();\n                            String[] lockTokens = getLockIdFromIfHeader(req);\n                            String lockToken = null;\n                            if (lockTokens != null) {\n                                lockToken = lockTokens[0];\n                            } else {\n                                logger.fine(\"-- No lock tokens found in resource lock object at \" + path);\n                                resp.sendError(WebdavStatus.SC_BAD_REQUEST);\n                                return;\n                            }\n                            if (lockToken.equals(nullResourceLockToken)) {\n                                so.setNullResource(false);\n                                so.setFolder(false);\n\n                                String[] nullResourceLockOwners = nullResourceLo.getOwner();\n                                String owner = null;\n                                if (nullResourceLockOwners != null) {\n                                    owner = nullResourceLockOwners[0];\n                                }\n\n                                if (!resourceLocks.unlock(transaction, lockToken, owner)) {\n                                    resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                                }\n                            } else {\n                                errorList.put(path, WebdavStatus.SC_LOCKED);\n                                sendReport(req, resp, errorList);\n                            }\n                        } else {\n                            logger.fine(\"-- Found a lock for the (existing) resource at \" + path);\n                        }\n                    }\n                    // User-Agent workarounds\n                    doUserAgentWorkaround(resp);\n\n                    // setting resourceContent\n                    Optional<String> header = Optional.ofNullable(req.getHeader(\"Content-Range\")).map(t -> t.trim());\n                    long start=0, end=0, length=Optional.ofNullable(req.getHeader(\"X-Expected-Entity-Length\"))\n                            .map(Long::parseLong)\n                            .orElse(0L);\n                    if (header.isPresent() && ! header.get().contains(\"-\")) {\n                        start = Long.parseLong(header.get().substring(\"bytes\".length()));\n                    } else if (header.isPresent()){\n                        String[] split = header.get().split(\"-\");\n                        start = Long.parseLong(split[0].substring(\"bytes\".length()));\n                        if (split[1].contains(\"/\")) {\n                            int slash = split[1].indexOf(\"/\");\n                            end = Long.parseLong(split[1].substring(0, slash));\n                            length = Long.parseLong(split[1].substring(slash + 1));\n                        } else {\n                            end = Long.parseLong(split[1]);\n                            length = end-start;\n                        }\n                    }\n                    Optional<String> lengthHeader = Optional.ofNullable(req.getHeader(\"Content-Length\")).map(t -> t.trim());\n                    if (end == 0 && lengthHeader.isPresent()) {\n                        end = Long.parseLong(lengthHeader.get());\n                        length = end;\n                    }\n                    System.out.println(\"start, end, length = \" + start + \",\" + end + \",\" + length);\n                    long resourceLength = store.setResourceContent(transaction, path, new Pair<>(new InputStreamAsyncReader(req.getInputStream()), length), null, null);\n\n                    so = store.getStoredObject(transaction, path);\n                    if (so == null) {\n                        resp.setStatus(WebdavStatus.SC_NOT_FOUND);\n                    } else if (resourceLength != -1) {\n                        so.setResourceLength(resourceLength);\n                    }\n                    // Now lets report back what was actually saved\n\n                } catch (AccessDeniedException e) {\n                    logger.log(Level.FINE, e, () -> \"Access denied when working with \" + path);\n                    resp.sendError(WebdavStatus.SC_FORBIDDEN);\n                } catch (WebdavException e) {\n                    logger.log(Level.FINE, e, () -> \"WebDAV exception when working with \" + path);\n                    resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n                } finally {\n                    resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n                }\n            } else {\n                logger.fine(\"Lock was not acquired when working with \" + path);\n                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);\n            }\n        } else {\n            logger.fine(\"Readonly=\" + readOnly);\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        }\n\n    }\n\n    /**\n     * @param resp\n     */\n    private void doUserAgentWorkaround( HttpServletResponse resp ) {\n        if (isOSXFinder()) {\n            logger.fine(\"DoPut.execute() : do workaround for user agent '\" + userAgent + \"'\");\n            resp.setStatus(WebdavStatus.SC_CREATED);\n        } else if (userAgent != null && userAgent.contains(\"Transmit\")) {\n            // Transmit also uses WEBDAVFS 1.x.x but crashes\n            // with SC_CREATED response\n            logger.fine(\"DoPut.execute() : do workaround for user agent '\" + userAgent + \"'\");\n            resp.setStatus(WebdavStatus.SC_NO_CONTENT);\n        } else {\n            resp.setStatus(WebdavStatus.SC_CREATED);\n        }\n    }\n\n    private boolean isOSXFinder() {\n        return (userAgent != null && userAgent.contains(\"WebDAVFS\") && !userAgent.contains(\"Transmit\"));\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/server/webdav/modeshape/webdav/methods/DoUnlock.java",
    "content": "/*\n * Copyright 1999,2004 The Apache Software Foundation.\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.server.webdav.modeshape.webdav.methods;\n\nimport peergos.server.webdav.modeshape.webdav.ITransaction;\nimport peergos.server.webdav.modeshape.webdav.IWebdavStore;\nimport peergos.server.webdav.modeshape.webdav.StoredObject;\nimport peergos.server.webdav.modeshape.webdav.WebdavStatus;\nimport peergos.server.webdav.modeshape.webdav.exceptions.LockFailedException;\nimport peergos.server.webdav.modeshape.webdav.locking.IResourceLocks;\nimport peergos.server.webdav.modeshape.webdav.locking.LockedObject;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.logging.Level;\n\npublic class DoUnlock extends DeterminableMethod {\n\n    private final IWebdavStore store;\n    private final IResourceLocks resourceLocks;\n    private final boolean readOnly;\n\n    public DoUnlock( IWebdavStore store,\n                     IResourceLocks resourceLocks,\n                     boolean readOnly ) {\n        this.store = store;\n        this.resourceLocks = resourceLocks;\n        this.readOnly = readOnly;\n    }\n\n    @Override\n    public void execute( ITransaction transaction,\n                         HttpServletRequest req,\n                         HttpServletResponse resp ) throws IOException, LockFailedException {\n        logger.fine(\"-- \" + this.getClass().getName());\n\n        if (readOnly) {\n            resp.sendError(WebdavStatus.SC_FORBIDDEN);\n        } else {\n\n            String path = getRelativePath(req);\n            String tempLockOwner = \"doUnlock\" + System.currentTimeMillis() + req.toString();\n            try {\n                if (resourceLocks.lock(transaction, path, tempLockOwner, false, 0, TEMP_TIMEOUT, TEMPORARY)) {\n\n                    String lockId = getLockIdFromLockTokenHeader(req);\n                    LockedObject lo;\n                    if (lockId != null && ((lo = resourceLocks.getLockedObjectByID(transaction, lockId)) != null)) {\n\n                        String[] owners = lo.getOwner();\n                        String owner = null;\n                        if (lo.isShared()) {\n                            // more than one owner is possible\n                            if (owners != null) {\n                                for (int i = 0; i < owners.length; i++) {\n                                    // remove owner from LockedObject\n                                    lo.removeLockedObjectOwner(owners[i]);\n                                }\n                            }\n                        } else {\n                            // exclusive, only one lock owner\n                            if (owners != null) {\n                                owner = owners[0];\n                            }\n                        }\n\n                        if (resourceLocks.unlock(transaction, lockId, owner)) {\n                            StoredObject so = store.getStoredObject(transaction, path);\n                            if (so == null) {\n                                resp.setStatus(WebdavStatus.SC_NOT_FOUND);\n                            } else {\n                                if (so.isNullResource()) {\n                                    store.removeObject(transaction, path);\n                                }\n                                resp.setStatus(WebdavStatus.SC_NO_CONTENT);\n                            }\n                        } else {\n                            logger.fine(\"DoUnlock failure at \" + lo.getPath());\n                            resp.sendError(WebdavStatus.SC_METHOD_FAILURE);\n                        }\n\n                    } else {\n                        resp.sendError(WebdavStatus.SC_BAD_REQUEST);\n                    }\n                }\n            } catch (LockFailedException e) {\n                logger.log(Level.WARNING, e, () -> \"Cannot unlock resource\");\n            } finally {\n                resourceLocks.unlockTemporaryLockedObjects(transaction, path, tempLockOwner);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/BufferedNetworkAccess.java",
    "content": "package peergos.shared;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.ChampUtil;\nimport peergos.shared.hamt.ChampWrapper;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** This will buffer block writes, and mutable pointer updates and commit in bulk\n *\n */\npublic class BufferedNetworkAccess extends NetworkAccess {\n\n    public interface Flusher {\n        CompletableFuture<Snapshot> commit(PublicKeyHash owner, Snapshot v, Supplier<Boolean> commitWatcher);\n    }\n\n    private final BufferedStorage blockBuffer;\n    private final BufferedPointers pointerBuffer;\n    private final int bufferSize;\n    private boolean safeToCommit = true;\n\n    public BufferedNetworkAccess(BufferedStorage blockBuffer,\n                                 BufferedPointers mutableBuffer,\n                                 int bufferSize,\n                                 CoreNode coreNode,\n                                 Account account,\n                                 SocialNetwork social,\n                                 MutablePointers unbufferedMutable,\n                                 BatCave batCave,\n                                 Optional<EncryptedBatCache> batCache,\n                                 MutableTree tree,\n                                 WriteSynchronizer synchronizer,\n                                 InstanceAdmin instanceAdmin,\n                                 SpaceUsage spaceUsage,\n                                 ServerMessager serverMessager,\n                                 Hasher hasher,\n                                 List<String> usernames,\n                                 boolean isJavascript) {\n        super(coreNode, account, social, blockBuffer, batCave, batCache, unbufferedMutable, tree, synchronizer, instanceAdmin, spaceUsage,\n                serverMessager, hasher, usernames, isJavascript);\n        this.blockBuffer = blockBuffer;\n        this.pointerBuffer = mutableBuffer;\n        this.bufferSize = bufferSize;\n        synchronizer.setCommitterBuilder(this::buildCommitter);\n        synchronizer.setFlusher((o, v, w) -> commit(o, w).thenApply(b -> v));\n    }\n\n    @Override\n    public Committer buildCommitter(Committer c, PublicKeyHash owner, Supplier<Boolean> commitWatcher) {\n        return (o, w, wd, e, tid) -> (wd.isEmpty() ? Futures.of(MaybeMultihash.empty()) :\n                blockBuffer.put(o, w.publicKeyHash, new byte[0], wd.get().serialize(), tid).thenApply(MaybeMultihash::new))\n                .thenCompose(newHash -> {\n                    PointerUpdate update = pointerBuffer.addWrite(w, newHash, e.hash, e.sequence);\n                    return maybeCommit(o, commitWatcher)\n                            .thenApply(x -> new Snapshot(w.publicKeyHash, new CommittedWriterData(newHash, wd, update.sequence)));\n                });\n    }\n\n    public int bufferedSize() {\n        return blockBuffer.totalSize();\n    }\n\n    public NetworkAccess disableCommits() {\n        safeToCommit = false;\n        return this;\n    }\n\n    public NetworkAccess enableCommits() {\n        safeToCommit = true;\n        return this;\n    }\n\n    @Override\n    public NetworkAccess clear() {\n        if (!blockBuffer.isEmpty())\n            throw new IllegalStateException(\"Unwritten blocks!\");\n        NetworkAccess base = super.clear();\n        BufferedStorage blockBuffer = this.blockBuffer.clone();\n        WriteSynchronizer synchronizer = new WriteSynchronizer(base.mutable, blockBuffer, hasher);\n        MutableTree tree = new MutableTreeImpl(base.mutable, blockBuffer, hasher, synchronizer);\n        return new BufferedNetworkAccess(blockBuffer, pointerBuffer, bufferSize, base.coreNode, base.account, base.social,\n                base.mutable, base.batCave, base.batCache, tree, synchronizer, base.instanceAdmin, base.spaceUsage, base.serverMessager, hasher, usernames, isJavascript());\n    }\n\n    public void forceClear() {\n        blockBuffer.clear();\n        pointerBuffer.clear();\n        synchronizer.clear();\n    }\n\n    @Override\n    public NetworkAccess withStorage(Function<ContentAddressedStorage, ContentAddressedStorage> modifiedStorage) {\n        BufferedStorage blockBuffer = this.blockBuffer.withStorage(modifiedStorage);\n        WriteSynchronizer synchronizer = new WriteSynchronizer(super.mutable, blockBuffer, hasher);\n        MutableTree tree = new MutableTreeImpl(mutable, blockBuffer, hasher, synchronizer);\n        return new BufferedNetworkAccess(blockBuffer, pointerBuffer, bufferSize, coreNode, account, social,\n                mutable, batCave, batCache, tree, synchronizer, instanceAdmin, spaceUsage, serverMessager, hasher, usernames, isJavascript());\n    }\n\n    @Override\n    public NetworkAccess withMutablePointerOfflineCache(Function<MutablePointers, MutablePointers> modifiedPointers) {\n        MutablePointers newMutable = modifiedPointers.apply(mutable);\n        BufferedPointers pointerBuffer = new BufferedPointers(newMutable);\n        WriteSynchronizer synchronizer = new WriteSynchronizer(newMutable, blockBuffer, hasher);\n        MutableTree tree = new MutableTreeImpl(newMutable, blockBuffer, hasher, synchronizer);\n        return new BufferedNetworkAccess(blockBuffer, pointerBuffer, bufferSize, coreNode, account, social,\n                newMutable, batCave, batCache, tree, synchronizer, instanceAdmin, spaceUsage, serverMessager, hasher, usernames, isJavascript());\n    }\n\n    @Override\n    public NetworkAccess withBatOfflineCache(Optional<EncryptedBatCache> batCache) {\n        return new BufferedNetworkAccess(blockBuffer, pointerBuffer, bufferSize, coreNode, account, social,\n                mutable, batCave, batCache, tree, synchronizer, instanceAdmin, spaceUsage, serverMessager, hasher, usernames, isJavascript());\n    }\n\n    @Override\n    public NetworkAccess withAccountCache(Function<Account, Account> wrapper) {\n        return new BufferedNetworkAccess(blockBuffer, pointerBuffer, bufferSize, coreNode, wrapper.apply(account), social,\n                mutable, batCave, batCache, tree, synchronizer, instanceAdmin, spaceUsage, serverMessager, hasher, usernames, isJavascript());\n    }\n\n    public NetworkAccess withCorenode(CoreNode newCore) {\n        return new BufferedNetworkAccess(blockBuffer, pointerBuffer, bufferSize, newCore, account, social,\n                mutable, batCave, batCache, tree, synchronizer, instanceAdmin, spaceUsage, serverMessager, hasher, usernames, isJavascript());\n    }\n\n    @Override\n    public CompletableFuture<Optional<Cid>> getLastCommittedRoot(PublicKeyHash writer, CommittedWriterData base) {\n        Optional<Pair<Optional<Cid>, Optional<Long>>> lastCommitTarget = pointerBuffer.getCommittedPointerTarget(writer);\n        if (lastCommitTarget.isEmpty()) {\n            return Futures.of(base.hash.toOptional().map(c -> (Cid) c));\n        }\n        boolean higherBaseVersion = base.sequence.orElse(-1L) > lastCommitTarget.get().right.orElse(-1L);\n        // If a later commit is not in local buffer or a merged pointer,\n        // then there must have been an external commit, use it\n        boolean isBufferedWrite = pointerBuffer.isBufferedWrite(writer, base.hash);\n        if (higherBaseVersion && base.hash.isPresent() && !isBufferedWrite && ! blockBuffer.hasBufferedBlock((Cid) base.hash.get()))\n            return Futures.of(base.hash.toOptional().map(c -> (Cid) c));\n        return Futures.of(lastCommitTarget.get().left);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CryptreeNode>> getMetadata(CommittedWriterData base, AbsoluteCapability cap) {\n        return getLastCommittedRoot(cap.writer, base)\n                .thenCompose(committed -> super.getMetadata(base, cap, committed));\n    }\n\n    public boolean isFull() {\n        return bufferedSize() >= bufferSize;\n    }\n\n    private CompletableFuture<Boolean> maybeCommit(PublicKeyHash owner, Supplier<Boolean> commitWatcher) {\n        if (safeToCommit && isFull())\n            return commit(owner, commitWatcher);\n        return Futures.of(true);\n    }\n\n    /** Commit a single writer's pointer with CAS merge fallback */\n    private CompletableFuture<Boolean> commitPointerWithMerge(\n            PublicKeyHash owner,\n            Pair<BufferedPointers.WriterUpdate, Optional<CommittedWriterData>> u,\n            Map<PublicKeyHash, SigningPrivateKeyAndPublicHash> writers,\n            TransactionId tid) {\n        return Futures.asyncExceptionally(\n                () -> pointerBuffer.commit(owner, writers.get(u.left.writer),\n                        new PointerUpdate(u.left.prevHash, u.left.currentHash, u.left.currentSequence)),\n                conflictT -> {\n                    Throwable conflictCause = Exceptions.getRootCause(conflictT);\n                    if (!(conflictCause instanceof PointerCasException))\n                        return Futures.errored(conflictT);\n                    PointerCasException cas = (PointerCasException) conflictCause;\n                    MaybeMultihash actualExisting = cas.existing;\n                    if (actualExisting.equals(u.left.currentHash))\n                        return Futures.of(true);\n                    return WriterData.getWriterData(owner, (Cid) u.left.prevHash.get(), Optional.empty(), blockBuffer)\n                            .thenCompose(original -> WriterData.getWriterData(owner, (Cid) u.left.currentHash.get(), Optional.empty(), blockBuffer)\n                                    .thenCompose(updated -> WriterData.getWriterData(owner, (Cid) actualExisting.get(), Optional.empty(), blockBuffer)\n                                            .thenCompose(remote -> ChampUtil.merge(owner, writers.get(u.left.writer),\n                                                            MaybeMultihash.of(original.props.get().tree.get()),\n                                                            MaybeMultihash.of(updated.props.get().tree.get()),\n                                                            MaybeMultihash.of(remote.props.get().tree.get()),\n                                                            Optional.empty(), tid, ChampWrapper.BIT_WIDTH,\n                                                            ChampWrapper.MAX_HASH_COLLISIONS_PER_LEVEL, y -> Futures.of(y.data),\n                                                            c -> (CborObject.CborMerkleLink) c, blockBuffer, hasher)\n                                                    .thenApply(p -> remote.props.get().withChamp(p.right)))))\n                            .thenCompose(newWD -> {\n                                Optional<Long> seq = cas.sequence;\n                                return blockBuffer.put(owner, writers.get(u.left.writer), newWD.serialize(), hasher, tid)\n                                        .thenCompose(mergedRoot -> blockBuffer.signBlocks(writers)\n                                                .thenCompose(signedMore -> blockBuffer.commit(owner, u.left.writer, tid, signedMore))\n                                                .thenCompose(z -> pointerBuffer.commit(owner, writers.get(u.left.writer),\n                                                        new PointerUpdate(actualExisting, MaybeMultihash.of(mergedRoot), seq.map(s -> s + 1)))));\n                            });\n                });\n    }\n\n    @Override\n    public synchronized CompletableFuture<Boolean> commit(PublicKeyHash owner, Supplier<Boolean> commitWatcher) {\n        List<BufferedPointers.WriterUpdate> writerUpdates = pointerBuffer.getUpdates();\n        if (blockBuffer.isEmpty() && writerUpdates.isEmpty())\n            return Futures.of(true);\n        // Condense pointers and do a mini GC to remove superfluous work\n        List<Cid> roots = pointerBuffer.getRoots();\n        if (roots.isEmpty())\n            return Futures.of(true);\n        blockBuffer.gc(roots);\n        Map<PublicKeyHash, SigningPrivateKeyAndPublicHash> writers = pointerBuffer.getSigners();\n        List<Pair<BufferedPointers.WriterUpdate, Optional<CommittedWriterData>>> writes = blockBuffer.getAllWriterData(writerUpdates);\n\n        boolean hasNewWriters = writes.stream().anyMatch(u -> !u.left.prevHash.isPresent());\n\n        CompletableFuture<Boolean> res = new CompletableFuture<>();\n        blockBuffer.signBlocks(writers)\n                .thenCompose(signed -> blockBuffer.target().startTransaction(owner)\n                        .thenCompose(tid -> (hasNewWriters\n                                // Sequential path: preserves the invariant that parent pointer commits before child blocks\n                                ? Futures.reduceAll(writes.stream(), true,\n                                        (a, u) -> blockBuffer.commit(owner, u.left.writer, tid, signed)\n                                                .thenCompose(b -> commitPointerWithMerge(owner, u, writers, tid)),\n                                        (x, y) -> x && y)\n                                // Atomic path: no new writers, commit all pointer updates in one transaction\n                                : Futures.reduceAll(writes.stream(), true,\n                                        (a, u) -> blockBuffer.commit(owner, u.left.writer, tid, signed),\n                                        (x, y) -> x && y)\n                                .thenCompose(x -> Futures.combineAllInOrder(writes.stream()\n                                        .map(u -> writers.get(u.left.writer).secret\n                                                .signMessage(new PointerUpdate(u.left.prevHash, u.left.currentHash, u.left.currentSequence).serialize())\n                                                .thenApply(sig -> new SignedPointerUpdate(u.left.writer, sig)))\n                                        .collect(Collectors.toList())))\n                                .thenCompose(batch -> Futures.asyncExceptionally(\n                                        () -> mutable.setPointers(owner, batch).thenApply(ok -> {\n                                            pointerBuffer.recordCommitted(writes.stream().map(u -> u.left).collect(Collectors.toList()));\n                                            return ok;\n                                        }),\n                                        t -> {\n                                            Throwable cause = Exceptions.getRootCause(t);\n                                            if (!(cause instanceof PointerCasException))\n                                                return Futures.errored(t);\n                                            return Futures.reduceAll(writes.stream(), true,\n                                                    (a, u) -> commitPointerWithMerge(owner, u, writers, tid),\n                                                    (x, y) -> x && y);\n                                        }\n                                )))\n                        .thenCompose(ok -> Futures.reduceAll(writes.stream(), true,\n                                (a, u) -> u.right\n                                        .map(cwd -> synchronizer.updateWriterState(owner, u.left.writer, new Snapshot(u.left.writer, cwd)))\n                                        .orElse(Futures.of(true)),\n                                (x, y) -> x && y))\n                        .thenCompose(x -> blockBuffer.target().closeTransaction(owner, tid))\n                        .thenApply(x -> {\n                            pointerBuffer.clear();\n                            blockBuffer.clear();\n                            return commitWatcher.get();\n                        }))).thenApply(res::complete)\n                .exceptionally(t -> {\n                    pointerBuffer.clear();\n                    blockBuffer.clear();\n                    res.completeExceptionally(t);\n                    return true;\n                });\n        return res;\n    }\n\n    @Override\n    public String toString() {\n        return \"Blocks(\" + blockBuffer.size() + \"),Pointers(\" + pointerBuffer.getUpdates().size()+\")\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/Crypto.java",
    "content": "package peergos.shared;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.crypto.asymmetric.mlkem.Mlkem;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\n\nimport java.util.function.*;\n\npublic class Crypto {\n\n    private static Crypto INSTANCE;\n\n    @JsProperty\n    public final SafeRandom random;\n    public final Hasher hasher;\n    public final Salsa20Poly1305 symmetricProvider;\n    public final Ed25519 signer;\n    public final Curve25519 boxer;\n    public final Mlkem mlkem;\n\n    public Crypto(SafeRandom random, Hasher hasher, Salsa20Poly1305 symmetricProvider, Ed25519 signer, Curve25519 boxer, Mlkem mlkem) {\n        this.random = random;\n        this.hasher = hasher;\n        this.symmetricProvider = symmetricProvider;\n        this.signer = signer;\n        this.boxer = boxer;\n        this.mlkem = mlkem;\n    }\n\n    public static synchronized Crypto init(Supplier<Crypto> instanceCreator) {\n        if (INSTANCE != null)\n            return INSTANCE;\n        Crypto instance = instanceCreator.get();\n        INSTANCE = instance;\n        SymmetricKey.addProvider(SymmetricKey.Type.TweetNaCl, instance.symmetricProvider);\n        PublicSigningKey.addProvider(PublicSigningKey.Type.Ed25519, instance.signer);\n        SymmetricKey.setRng(SymmetricKey.Type.TweetNaCl, instance.random);\n        PublicBoxingKey.addProvider(PublicBoxingKey.Type.Curve25519, instance.boxer);\n        PublicBoxingKey.addMlkemProvider(PublicBoxingKey.Type.HybridCurve25519MLKEM, instance);\n        PublicBoxingKey.setRng(PublicBoxingKey.Type.Curve25519, instance.random);\n        return instance;\n    }\n\n    @JsMethod\n    public static Crypto initJS() {\n        SafeRandom.Javascript random = new SafeRandom.Javascript();\n        Salsa20Poly1305.Javascript symmetricProvider = new Salsa20Poly1305.Javascript();\n        Ed25519.Javascript signer = new Ed25519.Javascript();\n        Curve25519.Javascript boxer = new Curve25519.Javascript();\n        Mlkem mlkem = new Mlkem.Javascript();\n        return init(() -> new Crypto(random, new ScryptJS(), symmetricProvider, signer, boxer, mlkem));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/CryptreeCache.java",
    "content": "package peergos.shared;\n\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic class CryptreeCache {\n\n    private final LRUCache<Pair<Multihash, ByteArrayWrapper>, Optional<CryptreeNode>> cache;\n\n    public CryptreeCache() {\n        int cacheSize = 1_000;\n        this.cache = new LRUCache<>(cacheSize);\n    }\n\n    public CryptreeCache(int cacheSize) {\n        this.cache = new LRUCache<>(cacheSize);\n    }\n\n    public synchronized boolean containsKey(Pair<Multihash, ByteArrayWrapper> cacheKey) {\n        return cache.containsKey(cacheKey);\n    }\n\n    public synchronized Optional<CryptreeNode> get(Pair<Multihash, ByteArrayWrapper> cacheKey) {\n        return cache.get(cacheKey);\n    }\n\n    public synchronized void put(Pair<Multihash, ByteArrayWrapper> cacheKey, Optional<CryptreeNode> val) {\n        cache.put(cacheKey, val);\n    }\n\n    public synchronized void update(Optional<Multihash> priorRoot, Pair<Multihash, ByteArrayWrapper> cacheKey, Optional<CryptreeNode> val) {\n        // update other mappings in cache from same root and different map key as they have not changed\n        if (priorRoot.isPresent()) {\n            new HashMap<>(cache).entrySet().stream()\n                    .filter(e -> e.getKey().left.equals(priorRoot.get()))\n                    .forEach(e -> cache.put(new Pair<>(cacheKey.left, e.getKey().right), e.getValue()));\n        }\n        cache.put(cacheKey, val);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/MaybeMultihash.java",
    "content": "package peergos.shared;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class MaybeMultihash implements Cborable {\n    private final Multihash hash;\n\n    public MaybeMultihash(Multihash hash) {\n        this.hash = hash;\n    }\n\n    public boolean isPresent() {\n        return hash != null;\n    }\n\n    public Optional<Multihash> toOptional() {\n        return isPresent() ? Optional.of(hash) : Optional.empty();\n    }\n\n    public <T> Optional<T> map(Function<Multihash, T> func) {\n        return isPresent() ? Optional.of(func.apply(hash)) : Optional.empty();\n    }\n\n    public Multihash get() {\n        if (! isPresent())\n            throw new IllegalStateException(\"hash not present\");\n        return hash;\n    }\n\n    public CompletableFuture<Boolean> ifPresent(Function<Multihash, CompletableFuture<Boolean>> con) {\n        if (isPresent())\n            return con.apply(hash);\n        return CompletableFuture.completedFuture(true);\n    }\n\n    public String toString() {\n        return hash != null ? hash.toString() : \"EMPTY\";\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        MaybeMultihash that = (MaybeMultihash) o;\n\n        return hash != null ? hash.equals(that.hash) : that.hash == null;\n    }\n\n    @Override\n    public int hashCode() {\n        return hash != null ? hash.hashCode() : 0;\n    }\n\n    public static MaybeMultihash fromCbor(Cborable cbor) {\n        if (cbor instanceof CborObject.CborNull)\n            return MaybeMultihash.empty();\n\n        if (! (cbor instanceof CborObject.CborByteArray))\n            throw new IllegalStateException(\"Incorrect cbor for MaybeMultihash: \" + cbor);\n        return MaybeMultihash.of(Cid.cast(((CborObject.CborByteArray) cbor).value));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return isPresent() ? new CborObject.CborByteArray(hash.toBytes()) : new CborObject.CborNull();\n    }\n\n    private static MaybeMultihash EMPTY = new MaybeMultihash(null);\n\n    public static MaybeMultihash empty() {\n        return EMPTY;\n    }\n\n    public static MaybeMultihash of(Multihash hash) {\n        return new MaybeMultihash(hash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/NetworkAccess.java",
    "content": "package peergos.shared;\nimport java.util.function.*;\nimport java.util.logging.*;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.login.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.storage.controller.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/**\n *  This class is unprivileged - doesn't have any private keys\n */\npublic class NetworkAccess {\n    private static final Logger LOG = Logger.getLogger(NetworkAccess.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n\n    public final Hasher hasher;\n    public final CoreNode coreNode;\n    public final Account account;\n    public final SocialNetwork social;\n    public final ContentAddressedStorage dhtClient;\n    public final BatCave batCave;\n    public final Optional<EncryptedBatCache> batCache;\n    public final MutablePointers mutable;\n    public final MutableTree tree;\n    public final WriteSynchronizer synchronizer;\n    public final InstanceAdmin instanceAdmin;\n    public final SpaceUsage spaceUsage;\n    public final ServerMessager serverMessager;\n\n    @JsProperty\n    public final List<String> usernames;\n    public final CryptreeCache cache;\n    private final LocalDateTime creationTime;\n    private final boolean isJavascript;\n\n    public NetworkAccess(CoreNode coreNode,\n                         Account account,\n                         SocialNetwork social,\n                         ContentAddressedStorage dhtClient,\n                         BatCave batCave,\n                         Optional<EncryptedBatCache> batCache,\n                         MutablePointers mutable,\n                         MutableTree tree,\n                         WriteSynchronizer synchronizer,\n                         InstanceAdmin instanceAdmin,\n                         SpaceUsage spaceUsage,\n                         ServerMessager serverMessager,\n                         Hasher hasher,\n                         List<String> usernames,\n                         CryptreeCache cache,\n                         boolean isJavascript) {\n        this.coreNode = coreNode;\n        this.account = account;\n        this.social = social;\n        this.dhtClient = dhtClient;\n        this.batCave = batCave;\n        this.batCache = batCache;\n        this.mutable = mutable;\n        this.tree = tree;\n        this.synchronizer = synchronizer;\n        this.instanceAdmin = instanceAdmin;\n        this.spaceUsage = spaceUsage;\n        this.serverMessager = serverMessager;\n        this.hasher = hasher;\n        this.usernames = usernames;\n        this.cache = cache;\n        this.creationTime = LocalDateTime.now();\n        this.isJavascript = isJavascript;\n    }\n\n    public NetworkAccess(CoreNode coreNode,\n                         Account account,\n                         SocialNetwork social,\n                         ContentAddressedStorage dhtClient,\n                         BatCave batCave,\n                         Optional<EncryptedBatCache> batCache,\n                         MutablePointers mutable,\n                         MutableTree tree,\n                         WriteSynchronizer synchronizer,\n                         InstanceAdmin instanceAdmin,\n                         SpaceUsage spaceUsage,\n                         ServerMessager serverMessager,\n                         Hasher hasher,\n                         List<String> usernames,\n                         boolean isJavascript) {\n        this(coreNode, account, social, dhtClient, batCave, batCache, mutable, tree, synchronizer, instanceAdmin, spaceUsage, serverMessager,\n                hasher, usernames, new CryptreeCache(), isJavascript);\n    }\n\n    public boolean isJavascript() {\n    \treturn isJavascript;\n    }\n\n    public NetworkAccess withStorage(Function<ContentAddressedStorage, ContentAddressedStorage> modifiedStorage) {\n        return new NetworkAccess(coreNode, account, social, modifiedStorage.apply(dhtClient), batCave, batCache, mutable, tree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, cache, isJavascript);\n    }\n\n    public NetworkAccess withMutablePointerOfflineCache(Function<MutablePointers, MutablePointers> modifiedPointers) {\n        return new NetworkAccess(coreNode, account, social, dhtClient, batCave, batCache, modifiedPointers.apply(mutable),\n                tree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, cache, isJavascript);\n    }\n\n    public NetworkAccess withBatOfflineCache(Optional<EncryptedBatCache> batCache) {\n        return new NetworkAccess(coreNode, account, social, dhtClient, batCave, batCache, mutable,\n                tree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, cache, isJavascript);\n    }\n\n    public NetworkAccess withAccountCache(Function<Account, Account> wrapper) {\n        return new NetworkAccess(coreNode, wrapper.apply(account), social, dhtClient, batCave, batCache, mutable,\n                tree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, cache, isJavascript);\n    }\n\n    public NetworkAccess withCorenode(CoreNode newCore) {\n        return new NetworkAccess(newCore, account, social, dhtClient, batCave, batCache, mutable, tree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, cache, isJavascript);\n    }\n\n    public NetworkAccess withoutS3BlockStore() {\n        ContentAddressedStorage directDht = dhtClient.directToOrigin();\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutable, directDht, hasher);\n        MutableTree tree = new MutableTreeImpl(mutable, directDht, hasher, synchronizer);\n        return new NetworkAccess(coreNode, account, social, directDht, batCave, batCache, mutable, tree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, isJavascript);\n    }\n\n    public static BufferedNetworkAccess nonCommittingForSignup(Account account,\n                                                               ContentAddressedStorage dht,\n                                                               MutablePointers mutable,\n                                                               BatCave bats,\n                                                               Hasher hasher) {\n        BufferedStorage blockBuffer = new BufferedStorage(dht, hasher);\n        MutablePointers unbufferedMutable = mutable;\n        BufferedPointers mutableBuffer = new BufferedPointers(unbufferedMutable);\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutableBuffer, blockBuffer, hasher);\n        MutableTree tree = new MutableTreeImpl(mutableBuffer, blockBuffer, hasher, synchronizer);\n\n        int bufferSize = 1024 * 1024;\n        return new BufferedNetworkAccess(blockBuffer, mutableBuffer, bufferSize, null, account,\n                null, unbufferedMutable, bats, Optional.empty(), tree, synchronizer, null,\n                null, null, hasher, Collections.emptyList(), false);\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<String>> otherDomain() {\n        return dhtClient.blockStoreProperties()\n                .thenApply(props -> props.baseAuthedUrl);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> isUsernameRegistered(String username) {\n        if (usernames.contains(username))\n            return CompletableFuture.completedFuture(true);\n        return coreNode.getChain(username).thenApply(chain -> chain.size() > 0);\n    }\n\n    public NetworkAccess clear() {\n        MutablePointers mutable = this.mutable.clearCache();\n        dhtClient.clearBlockCache();\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutable, dhtClient, hasher);\n        MutableTree mutableTree = new MutableTreeImpl(mutable, dhtClient, hasher, synchronizer);\n        return new NetworkAccess(coreNode, account, social, dhtClient, batCave, batCache, mutable, mutableTree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, isJavascript);\n    }\n\n    public NetworkAccess withMutablePointerCache(int ttl) {\n        CachingPointers mutable = new CachingPointers(this.mutable, ttl);\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutable, dhtClient, hasher);\n        MutableTree mutableTree = new MutableTreeImpl(mutable, dhtClient, hasher, synchronizer);\n        return new NetworkAccess(coreNode, account, social, dhtClient, batCave, batCache, mutable, mutableTree, synchronizer, instanceAdmin,\n                spaceUsage, serverMessager, hasher, usernames, isJavascript);\n    }\n\n    public static CoreNode buildProxyingCorenode(HttpPoster poster, Multihash pkiServerNodeId) {\n        return new HTTPCoreNode(poster, pkiServerNodeId);\n    }\n\n    public static CoreNode buildDirectCorenode(HttpPoster poster) {\n        return new HTTPCoreNode(poster);\n    }\n\n    public static ContentAddressedStorage buildLocalDht(HttpPoster apiPoster, boolean isPeergosServer, Hasher hasher) {\n        return new ContentAddressedStorage.HTTP(apiPoster, isPeergosServer, hasher);\n    }\n\n    public static CompletableFuture<ContentAddressedStorage> buildDirectS3Blockstore(ContentAddressedStorage localDht,\n                                                                                     CoreNode core,\n                                                                                     HttpPoster direct,\n                                                                                     boolean isPeergosServer,\n                                                                                     Hasher hasher) {\n        if (! isPeergosServer)\n            return Futures.of(localDht);\n        return localDht.blockStoreProperties()\n                .exceptionally(t -> BlockStoreProperties.empty())\n                .thenCompose(bp -> bp.useDirectBlockStore() ?\n                        localDht.ids().thenApply(ids -> new DirectS3BlockStore(bp, direct, localDht, ids, core, hasher)) :\n                        Futures.of(localDht));\n    }\n\n    @JsMethod\n    public static CompletableFuture<NetworkAccess> buildJS(boolean isPublic,\n                                                           int cacheSizeKiB,\n                                                           boolean allowOfflineLogin) {\n        JavaScriptPoster relative = new JavaScriptPoster(true, isPublic);\n        ScryptJS hasher = new ScryptJS();\n        boolean isPeergosServer = true; // we used to support using web ui through an ipfs gateway directly\n        ContentAddressedStorage localDht = buildLocalDht(relative, isPeergosServer, hasher);\n        OnlineState onlineState = new OnlineState(() -> localDht.id()\n                .thenApply(x -> true)\n                .exceptionally(t -> false));\n        return buildViaPeergosInstance(relative, relative, localDht, 7_000, hasher, true)\n                .thenApply(net -> net.withStorage(s ->\n                        new UnauthedCachingStorage(s, new JSBlockCache(cacheSizeKiB/1024), hasher))\n                        .withMutablePointerOfflineCache(m -> new OfflinePointerCache(m,\n                                new JSPointerCache(2000, net.dhtClient),\n                                onlineState)))\n                .thenApply(net -> ! allowOfflineLogin ?\n                        net :\n                        net.withBatOfflineCache(Optional.of(new JSBatCache()))\n                                .withAccountCache(a -> new OfflineAccountStore(net.account, new JSAccountCache(), onlineState))\n                                .withCorenode(new OfflineCorenode(net.coreNode, new JSPkiCache(), onlineState)));\n    }\n\n    private static CompletableFuture<Boolean> isPeergosServer(HttpPoster poster) {\n        CoreNode direct = buildDirectCorenode(poster);\n        CompletableFuture<Boolean> res = new CompletableFuture<>();\n        direct.getChain(\"peergos\")\n                .thenApply(x -> res.complete(true))\n                .exceptionally(t -> res.complete(false));\n        return res;\n    }\n\n    public static CompletableFuture<NetworkAccess> build(List<String> usernames,\n                                                         CoreNode core,\n                                                         HttpPoster apiPoster,\n                                                         HttpPoster p2pPoster,\n                                                         ContentAddressedStorage localDht,\n                                                         int mutableCacheTime,\n                                                         Hasher hasher,\n                                                         boolean isJavascript) {\n        return buildDirectS3Blockstore(localDht, core, apiPoster, true, hasher)\n                .thenCompose(dht -> build(core, dht, apiPoster, p2pPoster, mutableCacheTime, hasher, usernames, true, isJavascript));\n    }\n\n    public static CompletableFuture<NetworkAccess> buildViaPeergosInstance(HttpPoster apiPoster,\n                                                                            HttpPoster p2pPoster,\n                                                                            ContentAddressedStorage localDht,\n                                                                            int mutableCacheTime,\n                                                                            Hasher hasher,\n                                                                            boolean isJavascript) {\n        CoreNode direct = buildDirectCorenode(apiPoster);\n        return direct.getUsernames(\"\")\n                .exceptionally(t -> Collections.emptyList())\n                .thenCompose(usernames -> build(usernames, direct, apiPoster, p2pPoster, localDht,\n                        mutableCacheTime, hasher, isJavascript));\n    }\n\n    public static CompletableFuture<NetworkAccess> buildViaGateway(HttpPoster apiPoster,\n                                                                   HttpPoster p2pPoster,\n                                                                   Multihash pkiServerNodeId,\n                                                                   int mutableCacheTime,\n                                                                   Hasher hasher,\n                                                                   boolean isJavascript) {\n        // We are not on a Peergos server, hopefully an IPFS gateway\n        ContentAddressedStorage localIpfs = buildLocalDht(apiPoster, false, hasher);\n        CoreNode core = buildProxyingCorenode(p2pPoster, pkiServerNodeId);\n        return core.getUsernames(\"\").thenCompose(usernames ->\n                        build(core, localIpfs, apiPoster, p2pPoster, mutableCacheTime, hasher,\n                                usernames, false, isJavascript));\n    }\n\n    private static CompletableFuture<NetworkAccess> build(CoreNode core,\n                                                          ContentAddressedStorage localDht,\n                                                          HttpPoster apiPoster,\n                                                          HttpPoster p2pPoster,\n                                                          int mutableCacheTime,\n                                                          Hasher hasher,\n                                                          List<String> usernames,\n                                                          boolean isPeergosServer,\n                                                          boolean isJavascript) {\n        return (isPeergosServer ? localDht.ids() : Futures.of(Collections.singletonList(Proxy.ZERO)))\n                .thenApply(nodeIds -> {\n                    if (isPeergosServer)\n                        return buildToPeergosServer(nodeIds, core, localDht, apiPoster, p2pPoster, mutableCacheTime, hasher, usernames, isJavascript);\n                    ContentAddressedStorageProxy proxingDht = new ContentAddressedStorageProxy.HTTP(p2pPoster);\n                    ContentAddressedStorage storage = new ContentAddressedStorage.Proxying(localDht, proxingDht, nodeIds, core, true, n -> true);\n                    ContentAddressedStorage p2pDht = new CachingVerifyingStorage(new RetryStorage(storage, 7),\n                            100 * 1024, 1_000, nodeIds, hasher);\n                    MutablePointersProxy httpMutable = new HttpMutablePointers(apiPoster, p2pPoster);\n                    Account account = new HttpAccount(apiPoster, p2pPoster);\n                    MutablePointers p2pMutable = new ProxyingMutablePointers(nodeIds, core, httpMutable, httpMutable);\n\n                    SocialNetworkProxy httpSocial = new HttpSocialNetwork(apiPoster, p2pPoster);\n                    SocialNetwork p2pSocial = new ProxyingSocialNetwork(nodeIds, core, httpSocial, httpSocial);\n                    SpaceUsageProxy httpUsage = new HttpSpaceUsage(apiPoster, p2pPoster);\n                    SpaceUsage p2pUsage = new ProxyingSpaceUsage(nodeIds, core, httpUsage, httpUsage);\n                    ServerMessager serverMessager = new ServerMessager.HTTP(apiPoster);\n                    BatCave batCave = new HttpBatCave(apiPoster, p2pPoster);\n                    RetryMutablePointers retryMutable = new RetryMutablePointers(p2pMutable);\n                    return buildBuffered(p2pDht, batCave, core, account, retryMutable, mutableCacheTime, p2pSocial,\n                            new HttpInstanceAdmin(apiPoster), p2pUsage, serverMessager, hasher, usernames, isJavascript);\n                });\n    }\n\n    public static NetworkAccess buildToPeergosServer(List<Cid> nodeIds,\n                                                     CoreNode core,\n                                                     ContentAddressedStorage localDht,\n                                                     HttpPoster apiPoster,\n                                                     HttpPoster p2pPoster,\n                                                     int mutableCacheTime,\n                                                     Hasher hasher,\n                                                     List<String> usernames,\n                                                     boolean isJavascript) {\n        ContentAddressedStorage p2pDht = new CachingVerifyingStorage(new RetryStorage(localDht, 7),\n                100 * 1024, 1_000, nodeIds, hasher);\n        MutablePointersProxy httpMutable = new HttpMutablePointers(apiPoster, p2pPoster);\n        Account account = new HttpAccount(apiPoster, p2pPoster);\n\n        SocialNetworkProxy httpSocial = new HttpSocialNetwork(apiPoster, p2pPoster);\n        SpaceUsageProxy httpUsage = new HttpSpaceUsage(apiPoster, p2pPoster);\n        ServerMessager serverMessager = new ServerMessager.HTTP(apiPoster);\n        BatCave batCave = new HttpBatCave(apiPoster, p2pPoster);\n        RetryMutablePointers retryMutable = new RetryMutablePointers(httpMutable);\n        return buildBuffered(p2pDht, batCave, core, account, retryMutable, mutableCacheTime, httpSocial,\n                new HttpInstanceAdmin(apiPoster), httpUsage, serverMessager, hasher, usernames, isJavascript);\n    }\n\n    public static NetworkAccess buildBuffered(ContentAddressedStorage dht,\n                                               BatCave batCave,\n                                               CoreNode coreNode,\n                                               Account account,\n                                               MutablePointers mutable,\n                                               int mutableCacheTime,\n                                               SocialNetwork social,\n                                               InstanceAdmin instanceAdmin,\n                                               SpaceUsage usage,\n                                               ServerMessager serverMessager,\n                                               Hasher hasher,\n                                               List<String> usernames,\n                                               boolean isJavascript) {\n        BufferedStorage blockBuffer = new BufferedStorage(dht, hasher);\n        MutablePointers unbufferedMutable = mutableCacheTime > 0 ? new CachingPointers(mutable, mutableCacheTime) : mutable;\n        BufferedPointers mutableBuffer = new BufferedPointers(unbufferedMutable);\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutableBuffer, blockBuffer, hasher);\n        MutableTree tree = new MutableTreeImpl(mutableBuffer, blockBuffer, hasher, synchronizer);\n\n        int bufferSize = 20 * 1024 * 1024;\n        return new BufferedNetworkAccess(blockBuffer, mutableBuffer, bufferSize, coreNode, account,\n                social, unbufferedMutable, batCave, Optional.empty(), tree, synchronizer, instanceAdmin, usage, serverMessager, hasher, usernames, isJavascript);\n    }\n\n    public static CompletableFuture<NetworkAccess> buildPublicNetworkAccess(Hasher hasher,\n                                                                            CoreNode core,\n                                                                            MutablePointers mutable,\n                                                                            ContentAddressedStorage storage) {\n        WriteSynchronizer synchronizer = new WriteSynchronizer(mutable, storage, hasher);\n        MutableTree mutableTree = new MutableTreeImpl(mutable, storage, null, synchronizer);\n        return CompletableFuture.completedFuture(new NetworkAccess(core, null, null, storage, null, Optional.empty(), mutable, mutableTree,\n                synchronizer, null, null, null, hasher, Collections.emptyList(), false));\n    }\n\n    public boolean isFull() {\n        return false;\n    }\n\n    public NetworkAccess disableCommits() {\n        return this;\n    }\n\n    public NetworkAccess enableCommits() {\n        return this;\n    }\n\n    public Committer buildCommitter(Committer com, PublicKeyHash owner, Supplier<Boolean> commitWatcher) {\n        return (o, w, updated, e, tid) -> Futures.asyncExceptionally(() -> com.commit(o, w, updated, e, tid),\n                t -> {\n                    Throwable cause = Exceptions.getRootCause(t);\n                    if (cause instanceof PointerCasException) {\n                        PointerCasException cas = (PointerCasException) cause;\n                        MaybeMultihash actualExisting = cas.existing;\n                        return WriterData.getWriterData(owner, (Cid) e.hash.get(), Optional.empty(), dhtClient)\n                                .thenCompose(original -> WriterData.getWriterData(owner, (Cid) actualExisting.get(), Optional.empty(), dhtClient)\n                                                .thenCompose(remote -> ChampUtil.merge(owner, w,\n                                                                MaybeMultihash.of(original.props.get().tree.get()),\n                                                                MaybeMultihash.of(updated.get().tree.get()),\n                                                                MaybeMultihash.of(remote.props.get().tree.get()),\n                                                                Optional.empty(), tid, ChampWrapper.BIT_WIDTH,\n                                                                ChampWrapper.MAX_HASH_COLLISIONS_PER_LEVEL, x -> Futures.of(x.data),\n                                                                c -> (CborObject.CborMerkleLink)c, dhtClient, hasher)\n                                                        .thenApply(p -> remote.props.get().withChamp(p.right))))\n                                .thenCompose(newWD -> {\n                                    // 1. write the new writer data for the merged champs\n                                    // 2. commit the new pointer\n                                    Optional<Long> seq = cas.sequence;\n                                    return dhtClient.put(owner, w, newWD.serialize(), hasher, tid)\n                                            .thenCompose(mergedRoot -> mutable.setPointer(owner, w,\n                                                            new PointerUpdate(actualExisting, MaybeMultihash.of(mergedRoot), seq.map(s -> s + 1)))\n                                                    .thenApply(x -> new Snapshot(w.publicKeyHash, new CommittedWriterData(MaybeMultihash.of(mergedRoot), newWD, seq.map(s -> s + 1)))));\n                                });\n                    }\n                    return Futures.errored(t);\n                });\n    }\n\n    public CompletableFuture<Boolean> commit(PublicKeyHash owner) {\n        return commit(owner, () -> true);\n    }\n\n    public CompletableFuture<Boolean> commit(PublicKeyHash owner, Supplier<Boolean> commitWatcher) {\n        return Futures.of(true);\n    }\n\n    public CompletableFuture<Optional<RetrievedCapability>> retrieveMetadata(AbsoluteCapability cap, Snapshot version) {\n        return retrieveAllMetadata(Collections.singletonList(cap), version)\n                .thenApply(p -> p.left.isEmpty() ? Optional.empty() : Optional.of(p.left.get(0)));\n    }\n\n    public CompletableFuture<Optional<Cid>> getLastCommittedRoot(PublicKeyHash writer, CommittedWriterData base) {\n        return Futures.of(Optional.empty());\n    }\n\n    /**\n     *\n     * @param links\n     * @param current\n     * @return The retrieved capabilities and the list of absent ones\n     */\n    public CompletableFuture<Pair<List<RetrievedCapability>, List<AbsoluteCapability>>> retrieveAllMetadata(\n            List<AbsoluteCapability> links,\n            Snapshot current) {\n        Map<PublicKeyHash, List<AbsoluteCapability>> grouped = links.stream().collect(Collectors.groupingBy(link -> link.writer));\n        return Futures.combineAllInOrder(grouped.values().stream()\n                        .map(byWriter -> retrieveAllMetadataSingleWriter(byWriter, current))\n                        .collect(Collectors.toList()))\n                .thenApply(res -> new Pair<>(\n                        res.stream().flatMap(p -> p.left.stream()).collect(Collectors.toList()),\n                        res.stream().flatMap(p -> p.right.stream()).collect(Collectors.toList())));\n    }\n\n    public CompletableFuture<Pair<List<RetrievedCapability>, List<AbsoluteCapability>>> retrieveAllMetadataSingleWriter(\n            List<AbsoluteCapability> links,\n            Snapshot current) {\n        if (links.isEmpty())\n            return Futures.of(new Pair<>(Collections.emptyList(), Collections.emptyList()));\n        PublicKeyHash owner = links.get(0).owner;\n        // All links should have same owner and writer\n        PublicKeyHash writer = links.get(0).writer;\n        return current.withWriter(owner, writer, this)\n                .thenCompose(v -> {\n                    List<RetrievedCapability> fromCache = new ArrayList<>();\n                    List<AbsoluteCapability> remaining = new ArrayList<>();\n                    for (AbsoluteCapability link : links) {\n                        Pair<Multihash, ByteArrayWrapper> cacheKey = new Pair<>(v.get(writer).props.get().tree.get(), new ByteArrayWrapper(link.getMapKey()));\n                        Optional<CryptreeNode> cached = cache.get(cacheKey);\n                        if (cached != null && cached.isPresent())\n                            fromCache.add(new RetrievedCapability(link, cached.get()));\n                        else\n                            remaining.add(link);\n                    }\n\n                    if (remaining.isEmpty())\n                        return Futures.of(new Pair<>(fromCache, Collections.emptyList()));\n\n                    return Futures.combineAllInOrder(remaining.stream()\n                                    .filter(link -> link.writer.equals(writer))\n                                    .map(link -> link.bat.map(b -> b.calculateId(hasher)\n                                                    .thenApply(id -> Optional.of(new BatWithId(b, id.id))))\n                                            .orElse(Futures.of(Optional.empty()))\n                                            .thenApply(bid -> new ChunkMirrorCap(link.getMapKey(), bid))).collect(Collectors.toList()))\n                            .thenCompose(caps -> {\n                                if (caps.size() != remaining.size())\n                                    throw new IllegalStateException(\"All caps must have same writer in bulk champ get!\");\n\n                                List<List<ChunkMirrorCap>> grouped = new ArrayList<>();\n                                int groupSize = ContentAddressedStorage.MAX_CHAMP_GETS;\n                                for (int i = 0; i < caps.size(); i += groupSize)\n                                    grouped.add(caps.subList(i, Math.min(i + groupSize, caps.size())));\n                                return getLastCommittedRoot(writer, v.get(writer))\n                                        .thenCompose(committedRoot -> Futures.combineAllInOrder(grouped.stream().map(group ->\n                                                Futures.asyncExceptionally(\n                                                        () -> dhtClient.getChampLookup(owner, (Cid) v.get(writer).props.get().tree.get(), group, committedRoot),\n                                                        t -> dhtClient.getChampLookup(owner, (Cid) v.get(writer).props.get().tree.get(), group, committedRoot, hasher)\n                                                )).collect(Collectors.toList()))\n                                                .thenApply(all -> all.stream().flatMap(List::stream).collect(Collectors.toList()))\n                                                .thenCompose(blocks -> LocalRamStorage.build(hasher, blocks))\n                                                .thenCompose(fromBlocks -> {\n                                                    // combined: pre-fetched committed blocks first (fast path),\n                                                    // then dhtClient (BufferedStorage) for any buffered nodes.\n                                                    ContentAddressedStorage combined = new DelegatingStorage(dhtClient) {\n                                                        @Override\n                                                        public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash o, Cid hash, Optional<BatWithId> bat) {\n                                                            return fromBlocks.getRaw(o, hash, bat)\n                                                                    .thenCompose(opt -> opt.isPresent() ? Futures.of(opt) : dhtClient.getRaw(o, hash, bat));\n                                                        }\n                                                        @Override\n                                                        public ContentAddressedStorage directToOrigin() {\n                                                            return dhtClient.directToOrigin();\n                                                        }\n                                                        @Override\n                                                        public CompletableFuture<Optional<CborObject>> get(PublicKeyHash o, Cid hash, Optional<BatWithId> bat) {\n                                                            return getRaw(o, hash, bat).thenApply(opt -> opt.map(CborObject::fromByteArray));\n                                                        }\n                                                    };\n                                                    List<CompletableFuture<Either<RetrievedCapability, AbsoluteCapability>>> all = remaining.stream()\n                                                            .map(link -> current.withWriter(link.owner, link.writer, this)\n                                                                    .thenCompose(version -> getMetadata(version.get(link.writer), link, committedRoot, combined, hasher, cache)\n                                                                            .thenApply(copt -> copt.isPresent() ?\n                                                                                    Either.<RetrievedCapability, AbsoluteCapability>a(new RetrievedCapability(link, copt.get())) :\n                                                                                    Either.<RetrievedCapability, AbsoluteCapability>b(link))))\n                                                            .collect(Collectors.toList());\n\n                                                    return Futures.combineAll(all)\n                                                            .thenApply(res -> new Pair<>(\n                                                                    Stream.concat(res.stream()\n                                                                                            .filter(Either::isA)\n                                                                                            .map(e -> e.a()),\n                                                                                    fromCache.stream())\n                                                                            .collect(Collectors.toList()),\n                                                                    res.stream()\n                                                                            .filter(Either::isB)\n                                                                            .map(e -> e.b())\n                                                                            .collect(Collectors.toList())));\n                                                }));\n                            });\n                });\n    }\n\n    public CompletableFuture<Set<FileWrapper>> retrieveAll(List<EntryPoint> entries) {\n        return Futures.reduceAll(entries, Collections.emptySet(),\n                (set, entry) -> retrieveEntryPoint(entry)\n                        .thenApply(opt ->\n                                opt.map(f -> Stream.concat(set.stream(), Stream.of(f)).collect(Collectors.toSet()))\n                                        .orElse(set)),\n                (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toSet()));\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> retrieveEntryPoint(EntryPoint e) {\n        return synchronizer.getValue(e.pointer.owner, e.pointer.writer)\n                .thenCompose(version -> getFile(version, e.pointer, Optional.empty(), e.ownerName))\n                .exceptionally(t -> {\n                    LOG.log(Level.SEVERE, t.getMessage(), t);\n                    return Optional.empty();\n                });\n    }\n\n    public static CompletableFuture<RetrievedEntryPoint> retrieveEntryPoint(EntryPoint e, NetworkAccess network) {\n        return network.retrieveEntryPoint(e)\n                .thenCompose(fileOpt -> {\n                    if (! fileOpt.isPresent())\n                        throw new IllegalStateException(\"Couldn't retrieve entry point\");\n                    return fileOpt.get().getPath(network)\n                            .thenApply(path -> new RetrievedEntryPoint(e, path, fileOpt.get()));\n                });\n    }\n\n    public static CompletableFuture<RetrievedEntryPoint> getLatestEntryPoint(EntryPoint e, NetworkAccess network) {\n        return Futures.asyncExceptionally(() -> retrieveEntryPoint(e, network),\n                ex -> getUptodateEntryPoint(e, network)\n                        .thenCompose(updated -> retrieveEntryPoint(updated, network)));\n    }\n\n    private static CompletableFuture<EntryPoint> getUptodateEntryPoint(EntryPoint e, NetworkAccess network) {\n        // User might have changed their identity key, check for an update\n        return network.coreNode.updateUser(e.ownerName)\n                .thenCompose(x -> network.coreNode.getPublicKeyHash(e.ownerName))\n                .thenApply(currentIdOpt -> {\n                    if (!currentIdOpt.isPresent() || currentIdOpt.get().equals(e.pointer.owner))\n                        throw new IllegalStateException(\"Couldn't retrieve entry point for user \" + e.ownerName);\n                    return new EntryPoint(e.pointer.withOwner(currentIdOpt.get()), e.ownerName);\n                });\n    }\n\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        return dhtClient.getSecretLink(link);\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<FileWrapper>> getFile(AbsoluteCapability cap, String owner) {\n        return synchronizer.getValue(cap.owner, cap.writer)\n                .thenCompose(version -> getFile(version, cap, Optional.empty(), owner))\n                .exceptionally(t -> {\n                    LOG.log(Level.SEVERE, t.getMessage(), t);\n                    return Optional.empty();\n                });\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getFile(EntryPoint e, Snapshot version) {\n        if (version.contains(e.pointer.writer))\n            return getFile(version, e.pointer, Optional.empty(), e.ownerName);\n        return retrieveEntryPoint(e);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getFile(Snapshot version,\n                                                            AbsoluteCapability cap,\n                                                            Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                            String ownerName) {\n        return version.withWriter(cap.owner, cap.writer, this)\n                .thenCompose(v -> getMetadata(v.get(cap.writer), cap)\n                .thenCompose(faOpt -> {\n                    if (! faOpt.isPresent())\n                        return Futures.of(Optional.empty());\n                    CryptreeNode fa = faOpt.get();\n                    RetrievedCapability rc = new RetrievedCapability(cap, fa);\n                    try {\n                        FileProperties props = rc.getProperties();\n                        if (!props.isLink)\n                            return Futures.of(Optional.of(new FileWrapper(Optional.empty(),\n                                    rc,\n                                    Optional.empty(),\n                                    cap.wBaseKey.map(wBase -> fa.getSigner(cap.rBaseKey, wBase, entryWriter)), ownerName, v)));\n                        return getFileFromLink(cap.owner, rc, entryWriter, ownerName, this, v)\n                                .thenApply(f -> Optional.of(f));\n                    } catch (InvalidCipherTextException e) {\n                        LOG.info(\"Couldn't decrypt file from friend: \" + ownerName);\n                        return Futures.of(Optional.empty());\n                    }\n                }));\n    }\n\n    public static CompletableFuture<FileWrapper> getFileFromLink(PublicKeyHash owner,\n                                                                 RetrievedCapability link,\n                                                                 Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                                 String ownername,\n                                                                 NetworkAccess network,\n                                                                 Snapshot version) {\n        // the link is formatted as a directory cryptree node and the target is the solitary child\n        return link.fileAccess.getDirectChildrenCapabilities(link.capability, version, network)\n                .thenCompose(children -> {\n                    if (children.size() != 1)\n                        throw new IllegalStateException(\"Link cryptree nodes must have exactly one child link!\");\n                    AbsoluteCapability cap = children.stream().findFirst().get().cap;\n                    Set<PublicKeyHash> childWriters = Collections.singleton(cap.writer);\n                    return version.withWriters(owner, childWriters, network)\n                            .thenCompose(fullVersion -> network.retrieveAllMetadata(Collections.singletonList(cap), fullVersion)\n                                    .thenCompose(p -> p.left.isEmpty() ? // try again once\n                                            network.retrieveAllMetadata(Collections.singletonList(cap), fullVersion) :\n                                            Futures.of(p))\n                                    .thenApply(p -> p.left)\n                                    .thenCompose(rcs -> {\n                                        RetrievedCapability rc = rcs.get(0);\n                                        FileProperties props = rc.getProperties();\n                                        if (!props.isLink)\n                                            return Futures.of(new FileWrapper(rc, Optional.of(link), entryWriter, ownername, fullVersion));\n                                        // We have a link chain! Be sure to use the name from the first link,\n                                        // and remaining properties from the final target\n                                        return getFileFromLink(owner, rc, entryWriter, ownername, network, fullVersion)\n                                                .thenApply(f -> new FileWrapper(f.getPointer(), Optional.of(link), entryWriter, ownername, fullVersion));\n                                    }));\n                });\n    }\n\n\n    public CompletableFuture<Optional<CryptreeNode>> getMetadata(CommittedWriterData base, AbsoluteCapability cap) {\n        return getMetadata(base, cap, Optional.empty());\n    }\n\n    public CompletableFuture<Optional<CryptreeNode>> getMetadata(CommittedWriterData base, AbsoluteCapability cap, Optional<Cid> committedRoot) {\n        return getMetadata(base, cap, committedRoot, dhtClient, hasher, cache);\n    }\n\n    public static CompletableFuture<Optional<CryptreeNode>> getMetadata(CommittedWriterData base,\n                                                                        AbsoluteCapability cap,\n                                                                        Optional<Cid> committedRoot,\n                                                                        ContentAddressedStorage dhtClient,\n                                                                        Hasher hasher,\n                                                                        CryptreeCache cache) {\n        if (base.props.isEmpty() || base.props.get().tree.isEmpty())\n            return Futures.of(Optional.empty());\n        Multihash root = base.props.get().tree.get();\n        Pair<Multihash, ByteArrayWrapper> cacheKey = new Pair<>(root, new ByteArrayWrapper(cap.getMapKey()));\n        if (cache.containsKey(cacheKey))\n            return Futures.of(cache.get(cacheKey));\n        return cap.bat.map(b -> b.calculateId(hasher).thenApply(id -> Optional.of(new BatWithId(b, id.id)))).orElse(Futures.of(Optional.empty()))\n                .thenCompose(bat -> {\n                    return Futures.asyncExceptionally(\n                            () -> dhtClient.getChampLookup(cap.owner, (Cid) root, Arrays.asList(new ChunkMirrorCap(cap.getMapKey(), bat)), committedRoot),\n                            t -> dhtClient.getChampLookup(cap.owner, (Cid) root, Arrays.asList(new ChunkMirrorCap(cap.getMapKey(), bat)), committedRoot, hasher)\n                    ).thenCompose(blocks -> LocalRamStorage.build(hasher, blocks))\n                            .thenCompose(bstore -> {\n                                // Use bstore (pre-fetched blocks, fast) with dhtClient as fallback for\n                                // any buffered intermediate CHAMP nodes not returned by getChampLookup.\n                                ContentAddressedStorage champStorage = new DelegatingStorage(dhtClient) {\n                                    @Override\n                                    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash o, Cid hash, Optional<BatWithId> b) {\n                                        return bstore.getRaw(o, hash, b)\n                                                .thenCompose(opt -> opt.isPresent() ? Futures.of(opt) : dhtClient.getRaw(o, hash, b));\n                                    }\n                                    @Override\n                                    public ContentAddressedStorage directToOrigin() { return dhtClient.directToOrigin(); }\n                                    @Override\n                                    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash o, Cid hash, Optional<BatWithId> b) {\n                                        return getRaw(o, hash, b).thenApply(opt -> opt.map(CborObject::fromByteArray));\n                                    }\n                                };\n                                return Futures.asyncExceptionally(\n                                        () -> ChampWrapper.create(cap.owner, (Cid) root, Optional.empty(), x -> Futures.of(x.data), champStorage, hasher, c -> (CborObject.CborMerkleLink) c),\n                                        t -> dhtClient.getChampRoot(committedRoot, (Cid) root, cap.owner, dhtClient)\n                                                .thenCompose(champRoot -> ChampWrapper.create(cap.owner, champRoot, Optional.empty(), x -> Futures.of(x.data), champStorage, hasher, c -> (CborObject.CborMerkleLink) c))\n                                )\n                                        .thenCompose(tree -> tree.get(cap.getMapKey()))\n                                        .thenApply(c -> c.map(x -> x.target))\n                                        .thenCompose(btreeValue -> {\n                                            if (btreeValue.isPresent()) {\n                                                return champStorage.get(cap.owner, (Cid) btreeValue.get(), bat)\n                                                        .thenApply(value -> value.map(cbor -> CryptreeNode.fromCbor(cbor, cap.rBaseKey, btreeValue.get())))\n                                                        .thenApply(res -> {\n                                                            if (res.isPresent())\n                                                                cache.put(cacheKey, res);\n                                                            return res;\n                                                        });\n                                            }\n                                            cache.put(cacheKey, Optional.empty());\n                                            return CompletableFuture.completedFuture(Optional.empty());\n                                        });\n                            });\n                });\n    }\n\n    private CompletableFuture<List<Cid>> bulkUploadFragments(List<Fragment> fragments,\n                                                             PublicKeyHash owner,\n                                                             PublicKeyHash writer,\n                                                             List<byte[]> signatures,\n                                                             TransactionId tid,\n                                                             ProgressConsumer<Long> progressCounter) {\n        return dhtClient.putRaw(owner, writer, signatures, fragments\n                .stream()\n                .map(f -> f.data)\n                .collect(Collectors.toList()), tid, progressCounter);\n    }\n\n    public CompletableFuture<List<Cid>> uploadFragments(List<Fragment> fragments,\n                                                        PublicKeyHash owner,\n                                                        SigningPrivateKeyAndPublicHash writer,\n                                                        ProgressConsumer<Long> progressCounter,\n                                                        TransactionId tid) {\n        if (fragments.isEmpty())\n            return CompletableFuture.completedFuture(Collections.emptyList());\n\n        return Futures.combineAllInOrder(fragments.stream()\n                .map(f -> hasher.sha256(f.data))\n                .collect(Collectors.toList()))\n                .thenCompose(hashes -> Futures.combineAllInOrder(hashes.stream()\n                        .map(writer.secret::signMessage)\n                        .collect(Collectors.toList()))\n                        .thenCompose(signedHashes -> bulkUploadFragments(\n                                fragments,\n                                owner,\n                                writer.publicKeyHash,\n                                signedHashes,\n                                tid,\n                                progressCounter\n                        )));\n    }\n\n    public CompletableFuture<Snapshot> uploadChunk(Snapshot current,\n                                                   Committer committer,\n                                                   CryptreeNode metadata,\n                                                   PublicKeyHash owner,\n                                                   byte[] mapKey,\n                                                   SigningPrivateKeyAndPublicHash writer,\n                                                   TransactionId tid) {\n        if (! current.versions.containsKey(writer.publicKeyHash))\n            throw new IllegalStateException(\"Trying to commit to incorrect writer!\");\n        try {\n            LOG.info(\"Uploading chunk: \" + (metadata.isDirectory() ? \"dir\" : \"file\")\n                    + \" at \" + ArrayOps.bytesToHex(mapKey)\n                    + \" with \" + metadata.toCbor().links().size() + \" fragments\");\n            byte[] metaBlob = metadata.serialize();\n            CommittedWriterData version = current.get(writer);\n            return hasher.sha256(metaBlob)\n                    .thenCompose(blobSha -> writer.secret.signMessage(blobSha))\n                    .thenCompose(sig -> dhtClient.put(owner, writer.publicKeyHash,\n                            sig, metaBlob, tid))\n                    .thenCompose(blobHash -> tree.put(version.props.get(), owner, writer, mapKey,\n                            metadata.committedHash(), blobHash, tid)\n                            .thenCompose(wd -> committer.commit(owner, writer, wd, version, tid)\n                                    .thenApply(s -> {\n                                        cache.update(version.props.get().tree, new Pair<>(wd.tree.get(), new ByteArrayWrapper(mapKey)), Optional.of(metadata.withHash(blobHash)));\n                                        return s;\n                                    })))\n                    .thenApply(committed -> current.withVersion(writer.publicKeyHash, committed.get(writer)));\n        } catch (Exception e) {\n            LOG.severe(e.getMessage());\n            throw new RuntimeException(e);\n        }\n    }\n\n    public CompletableFuture<Snapshot> addPreexistingChunk(CryptreeNode metadata,\n                                                           PublicKeyHash owner,\n                                                           byte[] mapKey,\n                                                           SigningPrivateKeyAndPublicHash writer,\n                                                           TransactionId tid,\n                                                           Snapshot current,\n                                                           Committer committer) {\n        CommittedWriterData version = current.get(writer);\n        return tree.put(version.props.get(), owner, writer, mapKey, metadata.committedHash(), metadata.committedHash().get(), tid)\n                .thenCompose(wd -> committer.commit(owner, writer, wd, version, tid));\n    }\n\n    public CompletableFuture<Snapshot> deleteChunk(Snapshot current,\n                                                   Committer committer,\n                                                   CryptreeNode metadata,\n                                                   PublicKeyHash owner,\n                                                   byte[] mapKey,\n                                                   SigningPrivateKeyAndPublicHash writer,\n                                                   TransactionId tid) {\n        CommittedWriterData version = current.get(writer);\n        return tree.remove(version.props.get(), owner, writer, mapKey, metadata.committedHash(), tid)\n                .thenCompose(wd -> committer.commit(owner, writer, wd, version, tid))\n                .thenApply(committed -> current.withVersion(writer.publicKeyHash, committed.get(writer)));\n    }\n\n    public CompletableFuture<Snapshot> deleteChunkIfPresent(Snapshot current,\n                                                            Committer committer,\n                                                            PublicKeyHash owner,\n                                                            SigningPrivateKeyAndPublicHash writer,\n                                                            byte[] mapKey,\n                                                            TransactionId tid) {\n        CommittedWriterData version = current.get(writer);\n        return tree.get(version.props.get(), owner, writer.publicKeyHash, mapKey)\n                .thenCompose(valueHash ->\n                        ! valueHash.isPresent() ? CompletableFuture.completedFuture(current) :\n                                tree.remove(version.props.get(), owner, writer, mapKey, valueHash, tid)\n                                        .thenCompose(wd -> committer.commit(owner, writer, wd, version, tid)))\n                .thenApply(committed -> current.withVersion(writer.publicKeyHash, committed.get(writer)));\n    }\n\n    public CompletableFuture<Snapshot> deleteAllChunksIfPresent(Snapshot current,\n                                                                Committer committer,\n                                                                PublicKeyHash owner,\n                                                                SigningPrivateKeyAndPublicHash writer,\n                                                                List<Pair<byte[], Optional<Bat>>> mapKeysAndBats,\n                                                                TransactionId tid) {\n        return deleteAllChunksIfPresent(current, committer, owner, writer, mapKeysAndBats, Collections.emptyMap(), tid);\n    }\n\n    /**\n     * Delete all chunks identified by mapKeysAndBats from the writer's CHAMP.\n     *\n     * knownValues maps a chunk's map-key to its pre-known existing CHAMP value (the block CID stored\n     * under that key).  When every key in the call has a known value the entire getChampLookup round-trip\n     * is skipped, saving one batch of network calls per 50 keys.  Callers that already hold the\n     * FileWrapper (and therefore its committedHash) should populate this map for the first chunk of\n     * each file; subsequent chunks of multi-chunk files can be omitted and will be looked up normally.\n     */\n    public CompletableFuture<Snapshot> deleteAllChunksIfPresent(Snapshot current,\n                                                                Committer committer,\n                                                                PublicKeyHash owner,\n                                                                SigningPrivateKeyAndPublicHash writer,\n                                                                List<Pair<byte[], Optional<Bat>>> mapKeysAndBats,\n                                                                Map<ByteArrayWrapper, MaybeMultihash> knownValues,\n                                                                TransactionId tid) {\n        if (mapKeysAndBats.isEmpty())\n            return Futures.of(current);\n        CommittedWriterData version = current.get(writer);\n        if (version.props.isEmpty() || version.props.get().tree.isEmpty())\n            return Futures.of(current);\n        Cid root = (Cid) version.props.get().tree.get();\n\n        // Keys whose existing CHAMP value is not already known and must be fetched remotely.\n        List<Pair<byte[], Optional<Bat>>> unknownKeys = mapKeysAndBats.stream()\n                .filter(p -> !knownValues.containsKey(new ByteArrayWrapper(p.left)))\n                .collect(Collectors.toList());\n\n        CompletableFuture<Map<ByteArrayWrapper, MaybeMultihash>> resolvedValues;\n        if (unknownKeys.isEmpty()) {\n            // All values are pre-known — skip getChampLookup entirely.\n            resolvedValues = Futures.of(knownValues);\n        } else {\n            resolvedValues = Futures.combineAllInOrder(unknownKeys.stream()\n                            .map(p -> p.right.map(b -> b.calculateId(hasher)\n                                            .thenApply(id -> Optional.of(new BatWithId(b, id.id))))\n                                    .orElse(Futures.of(Optional.<BatWithId>empty()))\n                                    .thenApply(bid -> new ChunkMirrorCap(p.left, bid)))\n                            .collect(Collectors.toList()))\n                    .thenCompose(caps -> {\n                        List<List<ChunkMirrorCap>> grouped = new ArrayList<>();\n                        for (int i = 0; i < caps.size(); i += ContentAddressedStorage.MAX_CHAMP_GETS)\n                            grouped.add(caps.subList(i, Math.min(i + ContentAddressedStorage.MAX_CHAMP_GETS, caps.size())));\n                        // Pass the committed WriterData CID as committedRoot so BufferedStorage can resolve\n                        // the committed CHAMP root when the current root is still in the write buffer.\n                        return getLastCommittedRoot(writer.publicKeyHash, version)\n                                .thenCompose(committedWdHash -> Futures.combineAllInOrder(grouped.stream()\n                                        .map(group -> Futures.asyncExceptionally(\n                                                () -> dhtClient.getChampLookup(owner, root, group, committedWdHash),\n                                                t -> dhtClient.getChampLookup(owner, root, group, committedWdHash, hasher)))\n                                        .collect(Collectors.toList())))\n                                .thenApply(all -> all.stream().flatMap(List::stream).collect(Collectors.toList()))\n                                .thenCompose(blocks -> LocalRamStorage.build(hasher, blocks))\n                                .thenCompose(bstore -> {\n                                    ContentAddressedStorage combined = new DelegatingStorage(dhtClient) {\n                                        @Override\n                                        public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash o, Cid hash, Optional<BatWithId> bat) {\n                                            return bstore.getRaw(o, hash, bat)\n                                                    .thenCompose(opt -> opt.isPresent() ? Futures.of(opt) : dhtClient.getRaw(o, hash, bat));\n                                        }\n                                        @Override\n                                        public ContentAddressedStorage directToOrigin() {\n                                            return dhtClient.directToOrigin();\n                                        }\n                                        @Override\n                                        public CompletableFuture<Optional<CborObject>> get(PublicKeyHash o, Cid hash, Optional<BatWithId> bat) {\n                                            return getRaw(o, hash, bat).thenApply(opt -> opt.map(CborObject::fromByteArray));\n                                        }\n                                    };\n                                    return ChampWrapper.create(owner, root, Optional.empty(),\n                                            x -> Futures.of(x.data), combined, hasher, c -> (CborObject.CborMerkleLink) c);\n                                })\n                                .thenCompose(champ -> Futures.combineAllInOrder(unknownKeys.stream()\n                                        .map(p -> champ.get(p.left)\n                                                .thenApply(opt -> new Pair<>(new ByteArrayWrapper(p.left),\n                                                        opt.map(x -> MaybeMultihash.of(x.target)).orElse(MaybeMultihash.empty()))))\n                                        .collect(Collectors.toList())))\n                                .thenApply(lookedUp -> {\n                                    Map<ByteArrayWrapper, MaybeMultihash> combined2 = new HashMap<>(knownValues);\n                                    lookedUp.forEach(p -> combined2.put(p.left, p.right));\n                                    return combined2;\n                                });\n                    });\n        }\n\n        return resolvedValues.thenCompose(allValues -> {\n            List<Pair<byte[], MaybeMultihash>> keysAndValues = mapKeysAndBats.stream()\n                    .map(p -> new Pair<>(p.left, allValues.getOrDefault(new ByteArrayWrapper(p.left), MaybeMultihash.empty())))\n                    .filter(p -> p.right.isPresent())\n                    .collect(Collectors.toList());\n            if (keysAndValues.isEmpty())\n                return Futures.of(current);\n            return tree.removeAll(version.props.get(), owner, writer, keysAndValues, tid)\n                    .thenCompose(wd -> committer.commit(owner, writer, wd, version, tid))\n                    .thenApply(committed -> current.withVersion(writer.publicKeyHash, committed.get(writer)));\n        });\n    }\n\n    public CompletableFuture<List<Boolean>> chunksArePresent(Snapshot current,\n                                                             PublicKeyHash owner,\n                                                             PublicKeyHash writer,\n                                                             List<Pair<byte[], Optional<Bat>>> mapKeysAndBats) {\n        CommittedWriterData base = current.get(writer);\n        if (base.props.isEmpty() || base.props.get().tree.isEmpty())\n            return Futures.of(mapKeysAndBats.stream().map(k -> false).collect(Collectors.toList()));\n        Cid root = (Cid) base.props.get().tree.get();\n        return Futures.combineAllInOrder(mapKeysAndBats.stream()\n                        .map(p -> p.right.map(b -> b.calculateId(hasher)\n                                        .thenApply(id -> Optional.of(new BatWithId(b, id.id))))\n                                .orElse(Futures.of(Optional.<BatWithId>empty()))\n                                .thenApply(bid -> new ChunkMirrorCap(p.left, bid)))\n                        .collect(Collectors.toList()))\n                .thenCompose(caps -> getLastCommittedRoot(writer, base)\n                        .thenCompose(committedRoot -> Futures.asyncExceptionally(\n                                () -> dhtClient.getChampLookup(owner, root, caps, committedRoot),\n                                t -> dhtClient.getChampLookup(owner, root, caps, committedRoot, hasher)))\n                        .thenCompose(blocks -> LocalRamStorage.build(hasher, blocks))\n                        .thenCompose(bstore -> {\n                            ContentAddressedStorage combined = new DelegatingStorage(dhtClient) {\n                                @Override\n                                public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash o, Cid hash, Optional<BatWithId> bat) {\n                                    return bstore.getRaw(o, hash, bat)\n                                            .thenCompose(opt -> opt.isPresent() ? Futures.of(opt) : dhtClient.getRaw(o, hash, bat));\n                                }\n                                @Override\n                                public ContentAddressedStorage directToOrigin() {\n                                    return dhtClient.directToOrigin();\n                                }\n                                @Override\n                                public CompletableFuture<Optional<CborObject>> get(PublicKeyHash o, Cid hash, Optional<BatWithId> bat) {\n                                    return getRaw(o, hash, bat).thenApply(opt -> opt.map(CborObject::fromByteArray));\n                                }\n                            };\n                            return ChampWrapper.create(owner, root, Optional.empty(),\n                                    x -> Futures.of(x.data), combined, hasher, c -> (CborObject.CborMerkleLink) c);\n                        })\n                        .thenCompose(champ -> Futures.combineAllInOrder(mapKeysAndBats.stream()\n                                .map(p -> champ.get(p.left).thenApply(Optional::isPresent))\n                                .collect(Collectors.toList()))));\n    }\n\n    public static CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                              List<Cid> hashes,\n                                                                              List<BatWithId> bats,\n                                                                              ContentAddressedStorage dhtClient,\n                                                                              Hasher hasher,\n                                                                              ProgressConsumer<Long> monitor,\n                                                                              double spaceIncreaseFactor) {\n        return downloadFragments(owner, hashes, bats, dhtClient, hasher, monitor, spaceIncreaseFactor, 1);\n    }\n\n    private static CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                               List<Cid> hashes,\n                                                                               List<BatWithId> bats,\n                                                                               ContentAddressedStorage dhtClient,\n                                                                               Hasher hasher,\n                                                                               ProgressConsumer<Long> monitor,\n                                                                               double spaceIncreaseFactor,\n                                                                               int retriesLeft) {\n\n        List<CompletableFuture<Optional<FragmentWithHash>>> futures = IntStream.range(0, hashes.size()).mapToObj(i -> i)\n                .parallel()\n                .map(i -> {\n                    Cid h = hashes.get(i);\n                    return (h.isIdentity() ?\n                            CompletableFuture.completedFuture(Optional.of(h.getHash())) :\n                            h.codec == Cid.Codec.Raw ?\n                                    dhtClient.getRaw(owner, h, i < bats.size() ? Optional.of(bats.get(i)) : Optional.empty()) :\n                                    dhtClient.get(owner, h, i < bats.size() ? Optional.of(bats.get(i)) : Optional.empty())\n                                            .thenApply(cborOpt -> cborOpt.map(cbor -> ((CborObject.CborByteArray) cbor).value))) // for backwards compatibility\n                            .thenApply(dataOpt -> {\n                                Optional<byte[]> bytes = dataOpt;\n                                bytes.ifPresent(arr -> monitor.accept((long) (arr.length / spaceIncreaseFactor)));\n                                return bytes.map(data -> new FragmentWithHash(new Fragment(data), h.isIdentity() ? Optional.empty() : Optional.of(h)));\n                            });\n                }).collect(Collectors.toList());\n\n        return Futures.combineAllInOrder(futures)\n                .thenCompose(optList -> {\n                    if (optList.stream().anyMatch(Optional::isEmpty)) {\n                        if (retriesLeft > 0)\n                            return downloadFragments(owner, hashes, bats, dhtClient, hasher, monitor, spaceIncreaseFactor, retriesLeft - 1);\n                        throw new IllegalStateException(\"Couldn't retrieve blocks! \" + hashes);\n                    }\n                    return Futures.of(optList.stream()\n                            .filter(Optional::isPresent)\n                            .map(Optional::get)\n                            .collect(Collectors.toList()));\n                });\n    }\n}"
  },
  {
    "path": "src/peergos/shared/OnlineState.java",
    "content": "package peergos.shared;\n\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\n\npublic class OnlineState {\n\n    private final AtomicBoolean testedState = new AtomicBoolean(true);\n    private final Supplier<CompletableFuture<Boolean>> updater;\n    private long lastUpdate = 0;\n\n    public OnlineState(Supplier<CompletableFuture<Boolean>> updater) {\n        this.updater = updater;\n    }\n\n    public boolean isOnline() {\n        return testedState.get();\n    }\n\n    public synchronized void update() {\n        if (! testedState.get()) {\n            long now = System.currentTimeMillis();\n            if (now > lastUpdate + 10_000) {\n                lastUpdate = now;\n                updater.get().thenAccept(testedState::set);\n            }\n        }\n    }\n\n    public void updateAsync() {\n        ForkJoinPool.commonPool().execute(this::update);\n    }\n\n    public boolean isOfflineException(Throwable t) {\n        String err = t.toString();\n        if (err.contains(\"ConnectException\") || err.contains(\"UnknownHostException\")) {\n            testedState.set(false);\n            return true;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/QRCodeEncoder.java",
    "content": "package peergos.shared;\n\nimport peergos.shared.zxing.common.BitMatrix;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.util.zip.CRC32;\nimport java.util.zip.Deflater;\nimport java.util.zip.DeflaterOutputStream;\n\npublic class QRCodeEncoder {\n\n\n    public static final int BW_MODE = 0;\n    public static final int GREYSCALE_MODE = 1;\n    public static final int COLOR_MODE = 2;\n\n    private static void write(int i, OutputStream out, CRC32 crc) throws IOException {\n        byte b[]={(byte)((i>>24)&0xff),(byte)((i>>16)&0xff),(byte)((i>>8)&0xff),(byte)(i&0xff)};\n        write(b, out, crc);\n    }\n\n    private static void write(byte b[], OutputStream out, CRC32 crc) throws IOException {\n        out.write(b);\n        crc.update(b);\n    }\n\n    public static byte[] encodeToPng(int mode, int width, int height, BitMatrix pixels) throws IOException {\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        final byte id[] = {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13};\n        CRC32 crc = new CRC32();\n        write(id, bout, crc);\n        crc.reset();\n        write(\"IHDR\".getBytes(), bout, crc);\n        write(width, bout, crc);\n        write(height, bout, crc);\n        byte head[]=null;\n        switch (mode) {\n            case BW_MODE: head=new byte[]{1, 0, 0, 0, 0}; break;\n            case GREYSCALE_MODE: head=new byte[]{8, 0, 0, 0, 0}; break;\n            case COLOR_MODE: head=new byte[]{8, 2, 0, 0, 0}; break;\n        }\n        write(head, bout, crc);\n        write((int) crc.getValue(), bout, crc);\n        ByteArrayOutputStream compressed = new ByteArrayOutputStream();\n        OutputStream dos =  new DeflaterOutputStream(compressed, new Deflater(9));\n        int pixel;\n        int color;\n        int colorset;\n        switch (mode) {\n            case BW_MODE:\n                int rest = width % 8;\n                int bytes = width / 8;\n                for (int y=0; y < height; y++) {\n                    dos.write(0);\n                    for (int x=0; x < bytes; x++) {\n                        colorset=0;\n                        for (int sh=0; sh < 8; sh++) {\n                            pixel = getPixel(x*8 + sh,y, pixels);\n                            color = ((pixel >> 16) & 0xff);\n                            color += ((pixel >> 8) & 0xff);\n                            color += (pixel & 0xff);\n                            colorset <<= 1;\n                            if (color >= 3*128)\n                                colorset |= 1;\n                        }\n                        dos.write((byte)colorset);\n                    }\n                    if (rest>0) {\n                        colorset=0;\n                        for (int sh=0; sh < width % 8; sh++) {\n                            pixel = getPixel(bytes*8 + sh,y, pixels);\n                            color = ((pixel >> 16) & 0xff);\n                            color += ((pixel >> 8) & 0xff);\n                            color += (pixel & 0xff);\n                            colorset <<= 1;\n                            if (color >= 3*128)\n                                colorset |= 1;\n                        }\n                        colorset <<= 8-rest;\n                        dos.write((byte)colorset);\n                    }\n                }\n                break;\n            case GREYSCALE_MODE:\n                for (int y=0; y < height; y++) {\n                    dos.write(0);\n                    for (int x=0; x < width; x++) {\n                        pixel = getPixel(x,y, pixels);\n                        color = ((pixel >> 16) & 0xff);\n                        color += ((pixel >> 8) & 0xff);\n                        color += (pixel & 0xff);\n                        dos.write((byte)(color/3));\n                    }\n                }\n                break;\n             case COLOR_MODE:\n                for (int y=0; y < height; y++) {\n                    dos.write(0);\n                    for (int x=0; x < width; x++) {\n                        pixel = getPixel(x,y, pixels);\n                        dos.write((byte)((pixel >> 16) & 0xff));\n                        dos.write((byte)((pixel >> 8) & 0xff));\n                        dos.write((byte)(pixel & 0xff));\n                    }\n                }\n                break;\n        }\n        dos.close();\n        write(compressed.size(), bout, crc);\n        crc.reset();\n        write(\"IDAT\".getBytes(), bout, crc);\n        write(compressed.toByteArray(), bout, crc);\n        write((int) crc.getValue(), bout, crc);\n        write(0, bout, crc);\n        crc.reset();\n        write(\"IEND\".getBytes(), bout, crc);\n        write((int) crc.getValue(), bout, crc);\n        return bout.toByteArray();\n    }\n\n    private static int getPixel(int x, int y, BitMatrix pixels) {\n        if (pixels.get(x, y))\n            return  0xff000000;\n        else\n            return  0xffffffff;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/SecretLinkQRCode.java",
    "content": "package peergos.shared;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.zxing.BarcodeFormat;\nimport peergos.shared.zxing.BinaryBitmap;\nimport peergos.shared.zxing.RGBLuminanceSource;\nimport peergos.shared.zxing.WriterException;\nimport peergos.shared.zxing.common.BitMatrix;\nimport peergos.shared.zxing.common.HybridBinarizer;\nimport peergos.shared.zxing.qrcode.QRCodeReader;\nimport peergos.shared.zxing.qrcode.QRCodeWriter;\n\nimport java.io.IOException;\nimport java.util.*;\n\npublic class SecretLinkQRCode {\n\n    private final String  contents;\n\n    private SecretLinkQRCode(String contents) {\n        this.contents = contents;\n    }\n\n    @JsMethod\n    public String getBase64Thumbnail() {\n        String base64Data = Base64.getEncoder().encodeToString(getQrCodeData());\n        return \"data:image/png;base64,\" + base64Data;\n    }\n\n    @JsMethod\n    public static SecretLinkQRCode generate(String link) {\n            return new SecretLinkQRCode(link);\n    }\n\n    public static String decodeFromPixels(int[] pixels, int width, int height) {\n        // This source doesn't handle rotations or dilations\n        RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels);\n\n        BinaryBitmap readBitmap = new BinaryBitmap(new HybridBinarizer(source));\n        QRCodeReader reader = new QRCodeReader();\n        try {\n            return reader.decode(readBitmap).getText();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public byte[] getQrCodeData() {\n        try {\n            QRCodeWriter writer = new QRCodeWriter();\n            BitMatrix result = writer.encode(contents, BarcodeFormat.QR_CODE, 512, 512);\n            return QRCodeEncoder.encodeToPng(QRCodeEncoder.BW_MODE, result.getWidth(), result.getHeight(), result);\n        } catch (WriterException | IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        SecretLinkQRCode that = (SecretLinkQRCode) o;\n        return contents.equals(that.contents);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(contents);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/cbor/CborConstants.java",
    "content": "package peergos.shared.cbor;\n\n/*\n * JACOB - CBOR implementation in Java.\n *\n * (C) Copyright - 2013 - J.W. Janssen <j.w.janssen@lxtreme.nl>\n *\n * Licensed under Apache License v2.0.\n */\n\n/**\n * Constant values used by the CBOR format.\n */\npublic interface CborConstants {\n    /** Major type 0: unsigned integers. */\n    int TYPE_UNSIGNED_INTEGER = 0x00;\n    /** Major type 1: negative integers. */\n    int TYPE_NEGATIVE_INTEGER = 0x01;\n    /** Major type 2: byte string. */\n    int TYPE_BYTE_STRING = 0x02;\n    /** Major type 3: text/UTF8 string. */\n    int TYPE_TEXT_STRING = 0x03;\n    /** Major type 4: array of items. */\n    int TYPE_ARRAY = 0x04;\n    /** Major type 5: map of pairs. */\n    int TYPE_MAP = 0x05;\n    /** Major type 6: semantic tags. */\n    int TYPE_TAG = 0x06;\n    /** Major type 7: floating point, simple data types. */\n    int TYPE_FLOAT_SIMPLE = 0x07;\n\n    /** Denotes a one-byte value (uint8). */\n    int ONE_BYTE = 0x18;\n    /** Denotes a two-byte value (uint16). */\n    int TWO_BYTES = 0x19;\n    /** Denotes a four-byte value (uint32). */\n    int FOUR_BYTES = 0x1a;\n    /** Denotes a eight-byte value (uint64). */\n    int EIGHT_BYTES = 0x1b;\n\n    /** The CBOR-encoded boolean <code>false</code> value (encoded as \"simple value\": {@link #MT_SIMPLE}). */\n    int FALSE = 0x14;\n    /** The CBOR-encoded boolean <code>true</code> value (encoded as \"simple value\": {@link #MT_SIMPLE}). */\n    int TRUE = 0x15;\n    /** The CBOR-encoded <code>null</code> value (encoded as \"simple value\": {@link #MT_SIMPLE}). */\n    int NULL = 0x16;\n    /** The CBOR-encoded \"undefined\" value (encoded as \"simple value\": {@link #MT_SIMPLE}). */\n    int UNDEFINED = 0x17;\n    /** Denotes a half-precision float (two-byte IEEE 754, see {@link #MT_FLOAT}). */\n    int HALF_PRECISION_FLOAT = 0x19;\n    /** Denotes a single-precision float (four-byte IEEE 754, see {@link #MT_FLOAT}). */\n    int SINGLE_PRECISION_FLOAT = 0x1a;\n    /** Denotes a double-precision float (eight-byte IEEE 754, see {@link #MT_FLOAT}). */\n    int DOUBLE_PRECISION_FLOAT = 0x1b;\n    /** The CBOR-encoded \"break\" stop code for unlimited arrays/maps. */\n    int BREAK = 0x1f;\n\n    /** Semantic tag value describing date/time values in the standard format (UTF8 string, RFC3339). */\n    int TAG_STANDARD_DATE_TIME = 0;\n    /** Semantic tag value describing date/time values as Epoch timestamp (numeric, RFC3339). */\n    int TAG_EPOCH_DATE_TIME = 1;\n    /** Semantic tag value describing a positive big integer value (byte string). */\n    int TAG_POSITIVE_BIGINT = 2;\n    /** Semantic tag value describing a negative big integer value (byte string). */\n    int TAG_NEGATIVE_BIGINT = 3;\n    /** Semantic tag value describing a decimal fraction value (two-element array, base 10). */\n    int TAG_DECIMAL_FRACTION = 4;\n    /** Semantic tag value describing a big decimal value (two-element array, base 2). */\n    int TAG_BIGDECIMAL = 5;\n    /** Semantic tag value describing an expected conversion to base64url encoding. */\n    int TAG_EXPECTED_BASE64_URL_ENCODED = 21;\n    /** Semantic tag value describing an expected conversion to base64 encoding. */\n    int TAG_EXPECTED_BASE64_ENCODED = 22;\n    /** Semantic tag value describing an expected conversion to base16 encoding. */\n    int TAG_EXPECTED_BASE16_ENCODED = 23;\n    /** Semantic tag value describing an encoded CBOR data item (byte string). */\n    int TAG_CBOR_ENCODED = 24;\n    /** Semantic tag value describing an URL (UTF8 string). */\n    int TAG_URI = 32;\n    /** Semantic tag value describing a base64url encoded string (UTF8 string). */\n    int TAG_BASE64_URL_ENCODED = 33;\n    /** Semantic tag value describing a base64 encoded string (UTF8 string). */\n    int TAG_BASE64_ENCODED = 34;\n    /** Semantic tag value describing a regular expression string (UTF8 string, PCRE). */\n    int TAG_REGEXP = 35;\n    /** Semantic tag value describing a MIME message (UTF8 string, RFC2045). */\n    int TAG_MIME_MESSAGE = 36;\n    /** Semantic tag value describing CBOR content. */\n    int TAG_CBOR_MARKER = 55799;\n}\n"
  },
  {
    "path": "src/peergos/shared/cbor/CborDecoder.java",
    "content": "package peergos.shared.cbor;\n\n/*\n * JACOB - CBOR implementation in Java.\n *\n * (C) Copyright - 2013 - J.W. Janssen <j.w.janssen@lxtreme.nl>\n * Apache Public License v2.0\n */\n\nimport static peergos.shared.cbor.CborConstants.*;\nimport static peergos.shared.cbor.CborType.*;\n\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.PushbackInputStream;\nimport java.util.*;\n\n/**\n * Provides a decoder capable of handling CBOR encoded data from a {@link InputStream}.\n */\npublic class CborDecoder {\n    protected final PushbackInputStream m_is;\n\n    /**\n     * Creates a new {@link CborDecoder} instance.\n     *\n     * @param is the actual input stream to read the CBOR-encoded data from, cannot be <code>null</code>.\n     */\n    public CborDecoder(InputStream is) {\n        if (is == null) {\n            throw new IllegalArgumentException(\"InputStream cannot be null!\");\n        }\n        m_is = (is instanceof PushbackInputStream) ? (PushbackInputStream) is : new PushbackInputStream(is);\n    }\n\n    private static void fail(String msg, Object... args) throws IOException {\n        throw new IOException(msg + Arrays.toString(args));\n    }\n\n    private static String lengthToString(int len) {\n        return (len < 0) ? \"no payload\" : (len == ONE_BYTE) ? \"one byte\" : (len == TWO_BYTES) ? \"two bytes\"\n                : (len == FOUR_BYTES) ? \"four bytes\" : (len == EIGHT_BYTES) ? \"eight bytes\" : \"(unknown)\";\n    }\n\n    /**\n     * Peeks in the input stream for the upcoming type.\n     *\n     * @return the upcoming type in the stream, or <code>null</code> in case of an end-of-stream.\n     * @throws IOException in case of I/O problems reading the CBOR-type from the underlying input stream.\n     */\n    public CborType peekType() throws IOException {\n        int p = m_is.read();\n        if (p < 0) {\n            // EOF, nothing to peek at...\n            return null;\n        }\n        m_is.unread(p);\n        return valueOf(p);\n    }\n\n    /**\n     * Prolog to reading an array value in CBOR format.\n     *\n     * @return the number of elements in the array to read, or <tt>-1</tt> in case of infinite-length arrays.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public long readArrayLength() throws IOException {\n        return readMajorTypeWithSize(TYPE_ARRAY);\n    }\n\n    /**\n     * Reads a boolean value in CBOR format.\n     *\n     * @return the read boolean.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public boolean readBoolean() throws IOException {\n        int b = readMajorType(TYPE_FLOAT_SIMPLE);\n        if (b != FALSE && b != TRUE) {\n            fail(\"Unexpected boolean value: %d!\", b);\n        }\n        return b == TRUE;\n    }\n\n    /**\n     * Reads a \"break\"/stop value in CBOR format.\n     *\n     * @return always <code>null</code>.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public Object readBreak() throws IOException {\n        readMajorTypeExact(TYPE_FLOAT_SIMPLE, BREAK);\n\n        return null;\n    }\n\n    /**\n     * Reads a byte string value in CBOR format.\n     *\n     * @return the read byte string, never <code>null</code>. In case the encoded string has a length of <tt>0</tt>, an empty string is returned.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public byte[] readByteString(int maxLen) throws IOException {\n        long len = readMajorTypeWithSize(TYPE_BYTE_STRING);\n        if (len < 0)\n            fail(\"Infinite-length byte strings not supported!\");\n        if (len > Integer.MAX_VALUE)\n            fail(\"String length too long!\");\n        if (len > maxLen)\n            fail(\"Invalid cbor: byte string longer than original bytes!\");\n        return readFully(new byte[(int) len]);\n    }\n\n    /**\n     * Prolog to reading a byte string value in CBOR format.\n     *\n     * @return the number of bytes in the string to read, or <tt>-1</tt> in case of infinite-length strings.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public long readByteStringLength() throws IOException {\n        return readMajorTypeWithSize(TYPE_BYTE_STRING);\n    }\n\n    /**\n     * Reads a double-precision float value in CBOR format.\n     *\n     * @return the read double value, values from {@link Float#MIN_VALUE} to {@link Float#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public double readDouble() throws IOException {\n        readMajorTypeExact(TYPE_FLOAT_SIMPLE, DOUBLE_PRECISION_FLOAT);\n\n        return Double.longBitsToDouble(readUInt64());\n    }\n\n    /**\n     * Reads a single-precision float value in CBOR format.\n     *\n     * @return the read float value, values from {@link Float#MIN_VALUE} to {@link Float#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public float readFloat() throws IOException {\n        readMajorTypeExact(TYPE_FLOAT_SIMPLE, SINGLE_PRECISION_FLOAT);\n\n        return Float.intBitsToFloat((int) readUInt32());\n    }\n\n    /**\n     * Reads a half-precision float value in CBOR format.\n     *\n     * @return the read half-precision float value, values from {@link Float#MIN_VALUE} to {@link Float#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public double readHalfPrecisionFloat() throws IOException {\n        readMajorTypeExact(TYPE_FLOAT_SIMPLE, HALF_PRECISION_FLOAT);\n\n        int half = readUInt16();\n        int exp = (half >> 10) & 0x1f;\n        int mant = half & 0x3ff;\n\n        double val;\n        if (exp == 0) {\n            val = mant * Math.pow(2, -24);\n        } else if (exp != 31) {\n            val = (mant + 1024) * Math.pow(2, exp - 25);\n        } else if (mant != 0) {\n            val = Double.NaN;\n        } else {\n            val = Double.POSITIVE_INFINITY;\n        }\n\n        return ((half & 0x8000) == 0) ? val : -val;\n    }\n\n    /**\n     * Reads a signed or unsigned integer value in CBOR format.\n     *\n     * @return the read integer value, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public long readInt() throws IOException {\n        int ib = m_is.read();\n\n        // in case of negative integers, extends the sign to all bits; otherwise zero...\n        long ui = expectIntegerType(ib);\n        // in case of negative integers does a ones complement\n        return ui ^ readUInt(ib & 0x1f, false /* breakAllowed */);\n    }\n\n    /**\n     * Reads a signed or unsigned 16-bit integer value in CBOR format.\n     *\n     * @read the small integer value, values from <tt>[-65536..65535]</tt> are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying output stream.\n     */\n    public int readInt16() throws IOException {\n        int ib = m_is.read();\n\n        // in case of negative integers, extends the sign to all bits; otherwise zero...\n        long ui = expectIntegerType(ib);\n        // in case of negative integers does a ones complement\n        return (int) (ui ^ readUIntExact(TWO_BYTES, ib & 0x1f));\n    }\n\n    /**\n     * Reads a signed or unsigned 32-bit integer value in CBOR format.\n     *\n     * @read the small integer value, values in the range <tt>[-4294967296..4294967295]</tt> are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying output stream.\n     */\n    public long readInt32() throws IOException {\n        int ib = m_is.read();\n\n        // in case of negative integers, extends the sign to all bits; otherwise zero...\n        long ui = expectIntegerType(ib);\n        // in case of negative integers does a ones complement\n        return ui ^ readUIntExact(FOUR_BYTES, ib & 0x1f);\n    }\n\n    /**\n     * Reads a signed or unsigned 64-bit integer value in CBOR format.\n     *\n     * @read the small integer value, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying output stream.\n     */\n    public long readInt64() throws IOException {\n        int ib = m_is.read();\n\n        // in case of negative integers, extends the sign to all bits; otherwise zero...\n        long ui = expectIntegerType(ib);\n        // in case of negative integers does a ones complement\n        return ui ^ readUIntExact(EIGHT_BYTES, ib & 0x1f);\n    }\n\n    /**\n     * Reads a signed or unsigned 8-bit integer value in CBOR format.\n     *\n     * @read the small integer value, values in the range <tt>[-256..255]</tt> are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying output stream.\n     */\n    public int readInt8() throws IOException {\n        int ib = m_is.read();\n\n        // in case of negative integers, extends the sign to all bits; otherwise zero...\n        long ui = expectIntegerType(ib);\n        // in case of negative integers does a ones complement\n        return (int) (ui ^ readUIntExact(ONE_BYTE, ib & 0x1f));\n    }\n\n    /**\n     * Prolog to reading a map of key-value pairs in CBOR format.\n     *\n     * @return the number of entries in the map, >= 0.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public long readMapLength() throws IOException {\n        return readMajorTypeWithSize(TYPE_MAP);\n    }\n\n    /**\n     * Reads a <code>null</code>-value in CBOR format.\n     *\n     * @return always <code>null</code>.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public Object readNull() throws IOException {\n        readMajorTypeExact(TYPE_FLOAT_SIMPLE, NULL);\n        return null;\n    }\n\n    /**\n     * Reads a single byte value in CBOR format.\n     *\n     * @return the read byte value.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public byte readSimpleValue() throws IOException {\n        readMajorTypeExact(TYPE_FLOAT_SIMPLE, ONE_BYTE);\n        return (byte) readUInt8();\n    }\n\n    /**\n     * Reads a signed or unsigned small (&lt;= 23) integer value in CBOR format.\n     *\n     * @read the small integer value, values in the range <tt>[-24..23]</tt> are supported.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying output stream.\n     */\n    public int readSmallInt() throws IOException {\n        int ib = m_is.read();\n\n        // in case of negative integers, extends the sign to all bits; otherwise zero...\n        long ui = expectIntegerType(ib);\n        // in case of negative integers does a ones complement\n        return (int) (ui ^ readUIntExact(-1, ib & 0x1f));\n    }\n\n    /**\n     * Reads a semantic tag value in CBOR format.\n     *\n     * @return the read tag value.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public long readTag() throws IOException {\n        return readUInt(readMajorType(TYPE_TAG), false /* breakAllowed */);\n    }\n\n    /**\n     * Reads an UTF-8 encoded string value in CBOR format.\n     *\n     * @return the read UTF-8 encoded string, never <code>null</code>. In case the encoded string has a length of <tt>0</tt>, an empty string is returned.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public String readTextString(int maxLen) throws IOException {\n        long len = readMajorTypeWithSize(TYPE_TEXT_STRING);\n        if (len < 0)\n            fail(\"Infinite-length text strings not supported!\");\n        if (len > Integer.MAX_VALUE)\n            fail(\"String length too long!\");\n        if (len > maxLen)\n            fail(\"Invalid cbor: text string longer than original bytes!\");\n        return new String(readFully(new byte[(int) len]), \"UTF-8\");\n    }\n\n    /**\n     * Prolog to reading an UTF-8 encoded string value in CBOR format.\n     *\n     * @return the length of the string to read, or <tt>-1</tt> in case of infinite-length strings.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public long readTextStringLength() throws IOException {\n        return readMajorTypeWithSize(TYPE_TEXT_STRING);\n    }\n\n    /**\n     * Reads an undefined value in CBOR format.\n     *\n     * @return always <code>null</code>.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    public Object readUndefined() throws IOException {\n        readMajorTypeExact(TYPE_FLOAT_SIMPLE, UNDEFINED);\n        return null;\n    }\n\n    protected long expectIntegerType(int ib) throws IOException {\n        int majorType = ((ib & 0xFF) >>> 5);\n        if ((majorType != TYPE_UNSIGNED_INTEGER) && (majorType != TYPE_NEGATIVE_INTEGER)) {\n            fail(\"Unexpected type: %s, expected type %s or %s!\", getName(majorType), getName(TYPE_UNSIGNED_INTEGER),\n                    getName(TYPE_NEGATIVE_INTEGER));\n        }\n        return -majorType;\n    }\n\n    /**\n     * Reads the next major type from the underlying input stream, and verifies whether it matches the given expectation.\n     *\n     * @param majorType the expected major type, cannot be <code>null</code> (unchecked).\n     * @return the read subtype, or payload, of the read major type.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    protected int readMajorType(int majorType) throws IOException {\n        int ib = m_is.read();\n        if (majorType != ((ib >>> 5) & 0x07)) {\n            fail(\"Unexpected type: %s, expected: %s!\", getName(ib), getName(majorType));\n        }\n        return ib & 0x1F;\n    }\n\n    /**\n     * Reads the next major type from the underlying input stream, and verifies whether it matches the given expectations.\n     *\n     * @param majorType the expected major type, cannot be <code>null</code> (unchecked);\n     * @param subtype the expected subtype.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    protected void readMajorTypeExact(int majorType, int subtype) throws IOException {\n        int st = readMajorType(majorType);\n        if ((st ^ subtype) != 0) {\n            fail(\"Unexpected subtype: %d, expected: %d!\", st, subtype);\n        }\n    }\n\n    /**\n     * Reads the next major type from the underlying input stream, verifies whether it matches the given expectation, and decodes the payload into a size.\n     *\n     * @param majorType the expected major type, cannot be <code>null</code> (unchecked).\n     * @return the number of succeeding bytes, &gt;= 0, or <tt>-1</tt> if an infinite-length type is read.\n     * @throws IOException in case of I/O problems reading the CBOR-encoded value from the underlying input stream.\n     */\n    protected long readMajorTypeWithSize(int majorType) throws IOException {\n        return readUInt(readMajorType(majorType), true /* breakAllowed */);\n    }\n\n    /**\n     * Reads an unsigned integer with a given length-indicator.\n     *\n     * @param length the length indicator to use;\n     * @return the read unsigned integer, as long value.\n     * @throws IOException in case of I/O problems reading the unsigned integer from the underlying input stream.\n     */\n    protected long readUInt(int length, boolean breakAllowed) throws IOException {\n        long result = -1;\n        if (length < ONE_BYTE) {\n            result = length;\n        } else if (length == ONE_BYTE) {\n            result = readUInt8();\n        } else if (length == TWO_BYTES) {\n            result = readUInt16();\n        } else if (length == FOUR_BYTES) {\n            result = readUInt32();\n        } else if (length == EIGHT_BYTES) {\n            result = readUInt64();\n        } else if (breakAllowed && length == BREAK) {\n            return -1;\n        }\n        if (result < 0) {\n            fail(\"Not well-formed CBOR integer found, invalid length: %d!\", result);\n        }\n        return result;\n    }\n\n    /**\n     * Reads an unsigned 16-bit integer value\n     *\n     * @return value the read value, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected int readUInt16() throws IOException {\n        byte[] buf = readFully(new byte[2]);\n        return (buf[0] & 0xFF) << 8 | (buf[1] & 0xFF);\n    }\n\n    /**\n     * Reads an unsigned 32-bit integer value\n     *\n     * @return value the read value, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected long readUInt32() throws IOException {\n        byte[] buf = readFully(new byte[4]);\n        return ((buf[0] & 0xFF) << 24 | (buf[1] & 0xFF) << 16 | (buf[2] & 0xFF) << 8 | (buf[3] & 0xFF)) & 0xffffffffL;\n    }\n\n    /**\n     * Reads an unsigned 64-bit integer value\n     *\n     * @return value the read value, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected long readUInt64() throws IOException {\n        byte[] buf = readFully(new byte[8]);\n        return (buf[0] & 0xFFL) << 56 | (buf[1] & 0xFFL) << 48 | (buf[2] & 0xFFL) << 40 | (buf[3] & 0xFFL) << 32 | //\n                (buf[4] & 0xFFL) << 24 | (buf[5] & 0xFFL) << 16 | (buf[6] & 0xFFL) << 8 | (buf[7] & 0xFFL);\n    }\n\n    /**\n     * Reads an unsigned 8-bit integer value\n     *\n     * @return value the read value, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected int readUInt8() throws IOException {\n        return m_is.read() & 0xff;\n    }\n\n    /**\n     * Reads an unsigned integer with a given length-indicator.\n     *\n     * @param length the length indicator to use;\n     * @return the read unsigned integer, as long value.\n     * @throws IOException in case of I/O problems reading the unsigned integer from the underlying input stream.\n     */\n    protected long readUIntExact(int expectedLength, int length) throws IOException {\n        if (((expectedLength == -1) && (length >= ONE_BYTE)) || ((expectedLength >= 0) && (length != expectedLength))) {\n            fail(\"Unexpected payload/length! Expected %s, but got %s.\", lengthToString(expectedLength),\n                    lengthToString(length));\n        }\n        return readUInt(length, false /* breakAllowed */);\n    }\n\n    private byte[] readFully(byte[] buf) throws IOException {\n        int len = buf.length;\n        int n = 0, off = 0;\n        while (n < len) {\n            int count = m_is.read(buf, off + n, len - n);\n            if (count < 0) {\n                throw new EOFException();\n            }\n            n += count;\n        }\n        return buf;\n    }\n}"
  },
  {
    "path": "src/peergos/shared/cbor/CborEncoder.java",
    "content": "package peergos.shared.cbor;\n\n/*\n * JACOB - CBOR implementation in Java.\n *\n * (C) Copyright - 2013 - J.W. Janssen <j.w.janssen@lxtreme.nl>\n *\n * Licensed under Apache License v2.0.\n */\n\nimport static peergos.shared.cbor.CborConstants.*;\n\nimport java.io.*;\n\n/**\n * Provides an encoder capable of encoding data into CBOR format to a given {@link OutputStream}.\n */\npublic class CborEncoder {\n    private static final int NEG_INT_MASK = TYPE_NEGATIVE_INTEGER << 5;\n\n    private final OutputStream m_os;\n\n    /**\n     * Creates a new {@link CborEncoder} instance.\n     *\n     * @param os the actual output stream to write the CBOR-encoded data to, cannot be <code>null</code>.\n     */\n    public CborEncoder(OutputStream os) {\n        if (os == null) {\n            throw new IllegalArgumentException(\"OutputStream cannot be null!\");\n        }\n        m_os = os;\n    }\n\n    /**\n     * Interprets a given float-value as a half-precision float value and\n     * converts it to its raw integer form, as defined in IEEE 754.\n     * <p>\n     * Taken from: <a href=\"http://stackoverflow.com/a/6162687/229140\">this Stack Overflow answer</a>.\n     * </p>\n     *\n     * @param fval the value to convert.\n     * @return the raw integer representation of the given float value.\n     */\n    static int halfPrecisionToRawIntBits(float fval) {\n        int fbits = Float.floatToIntBits(fval);\n        int sign = (fbits >>> 16) & 0x8000;\n        int val = (fbits & 0x7fffffff) + 0x1000;\n\n        // might be or become NaN/Inf\n        if (val >= 0x47800000) {\n            if ((fbits & 0x7fffffff) >= 0x47800000) { // is or must become NaN/Inf\n                if (val < 0x7f800000) {\n                    // was value but too large, make it +/-Inf\n                    return sign | 0x7c00;\n                }\n                return sign | 0x7c00 | (fbits & 0x007fffff) >>> 13; // keep NaN (and Inf) bits\n            }\n            return sign | 0x7bff; // unrounded not quite Inf\n        }\n        if (val >= 0x38800000) {\n            // remains normalized value\n            return sign | val - 0x38000000 >>> 13; // exp - 127 + 15\n        }\n        if (val < 0x33000000) {\n            // too small for subnormal\n            return sign; // becomes +/-0\n        }\n\n        val = (fbits & 0x7fffffff) >>> 23;\n        // add subnormal bit, round depending on cut off and div by 2^(1-(exp-127+15)) and >> 13 | exp=0\n        return sign | ((fbits & 0x7fffff | 0x800000) + (0x800000 >>> val - 102) >>> 126 - val);\n    }\n\n    /**\n     * Writes the start of an indefinite-length array.\n     * <p>\n     * After calling this method, one is expected to write the given number of array elements, which can be of any type. No length checks are performed.<br/>\n     * After all array elements are written, one should write a single break value to end the array, see {@link #writeBreak()}.\n     * </p>\n     *\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeArrayStart() throws IOException {\n        writeSimpleType(TYPE_ARRAY, BREAK);\n    }\n\n    /**\n     * Writes the start of a definite-length array.\n     * <p>\n     * After calling this method, one is expected to write the given number of array elements, which can be of any type. No length checks are performed.\n     * </p>\n     *\n     * @param length the number of array elements to write, should &gt;= 0.\n     * @throws IllegalArgumentException in case the given length was negative;\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeArrayStart(int length) throws IOException {\n        if (length < 0) {\n            throw new IllegalArgumentException(\"Invalid array-length!\");\n        }\n        writeType(TYPE_ARRAY, length);\n    }\n\n    /**\n     * Writes a boolean value in canonical CBOR format.\n     *\n     * @param value the boolean to write.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeBoolean(boolean value) throws IOException {\n        writeSimpleType(TYPE_FLOAT_SIMPLE, value ? TRUE : FALSE);\n    }\n\n    /**\n     * Writes a \"break\" stop-value in canonical CBOR format.\n     *\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeBreak() throws IOException {\n        writeSimpleType(TYPE_FLOAT_SIMPLE, BREAK);\n    }\n\n    /**\n     * Writes a byte string in canonical CBOR-format.\n     *\n     * @param bytes the byte string to write, can be <code>null</code> in which case a byte-string of length <tt>0</tt> is written.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeByteString(byte[] bytes) throws IOException {\n        writeString(TYPE_BYTE_STRING, bytes);\n    }\n\n    /**\n     * Writes the start of an indefinite-length byte string.\n     * <p>\n     * After calling this method, one is expected to write the given number of string parts. No length checks are performed.<br/>\n     * After all string parts are written, one should write a single break value to end the string, see {@link #writeBreak()}.\n     * </p>\n     *\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeByteStringStart() throws IOException {\n        writeSimpleType(TYPE_BYTE_STRING, BREAK);\n    }\n\n    /**\n     * Writes a double-precision float value in canonical CBOR format.\n     *\n     * @param value the value to write, values from {@link Double#MIN_VALUE} to {@link Double#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeDouble(double value) throws IOException {\n        throw new IllegalStateException(\"Unimplemented!\");\n//        writeUInt64(TYPE_FLOAT_SIMPLE << 5, Double.doubleToRawLongBits(value));\n    }\n\n    /**\n     * Writes a single-precision float value in canonical CBOR format.\n     *\n     * @param value the value to write, values from {@link Float#MIN_VALUE} to {@link Float#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeFloat(float value) throws IOException {\n        throw new IllegalStateException(\"Unimplemented!\");\n//        writeUInt32(TYPE_FLOAT_SIMPLE << 5, Float.floatToRawIntBits(value));\n    }\n\n    /**\n     * Writes a half-precision float value in canonical CBOR format.\n     *\n     * @param value the value to write, values from {@link Float#MIN_VALUE} to {@link Float#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeHalfPrecisionFloat(float value) throws IOException {\n        writeUInt16(TYPE_FLOAT_SIMPLE << 5, halfPrecisionToRawIntBits(value));\n    }\n\n    /**\n     * Writes a signed or unsigned integer value in canonical CBOR format, that is, tries to encode it in a little bytes as possible..\n     *\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeInt(long value) throws IOException {\n        // extends the sign over all bits...\n        long sign = value >> 63;\n        // in case value is negative, this bit should be set...\n        int mt = (int) (sign & NEG_INT_MASK);\n        // complement negative value...\n        value = (sign ^ value);\n\n        writeUInt(mt, value);\n    }\n\n    /**\n     * Writes a signed or unsigned 16-bit integer value in CBOR format.\n     *\n     * @param value the value to write, values from <tt>[-65536..65535]</tt> are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeInt16(int value) throws IOException {\n        // extends the sign over all bits...\n        int sign = value >> 31;\n        // in case value is negative, this bit should be set...\n        int mt = (int) (sign & NEG_INT_MASK);\n        // complement negative value...\n        writeUInt16(mt, (sign ^ value) & 0xffff);\n    }\n\n    /**\n     * Writes a signed or unsigned 32-bit integer value in CBOR format.\n     *\n     * @param value the value to write, values in the range <tt>[-4294967296..4294967295]</tt> are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeInt32(long value) throws IOException {\n        // extends the sign over all bits...\n        long sign = value >> 63;\n        // in case value is negative, this bit should be set...\n        int mt = (int) (sign & NEG_INT_MASK);\n        // complement negative value...\n        writeUInt32(mt, (int) ((sign ^ value) & 0xffffffffL));\n    }\n\n    /**\n     * Writes a signed or unsigned 64-bit integer value in CBOR format.\n     *\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeInt64(long value) throws IOException {\n        // extends the sign over all bits...\n        long sign = value >> 63;\n        // in case value is negative, this bit should be set...\n        int mt = (int) (sign & NEG_INT_MASK);\n        // complement negative value...\n        writeUInt64(mt, sign ^ value);\n    }\n\n    /**\n     * Writes a signed or unsigned 8-bit integer value in CBOR format.\n     *\n     * @param value the value to write, values in the range <tt>[-256..255]</tt> are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeInt8(int value) throws IOException {\n        // extends the sign over all bits...\n        int sign = value >> 31;\n        // in case value is negative, this bit should be set...\n        int mt = (int) (sign & NEG_INT_MASK);\n        // complement negative value...\n        writeUInt8(mt, (sign ^ value) & 0xff);\n    }\n\n    /**\n     * Writes the start of an indefinite-length map.\n     * <p>\n     * After calling this method, one is expected to write any number of map entries, as separate key and value. Keys and values can both be of any type. No length checks are performed.<br/>\n     * After all map entries are written, one should write a single break value to end the map, see {@link #writeBreak()}.\n     * </p>\n     *\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeMapStart() throws IOException {\n        writeSimpleType(TYPE_MAP, BREAK);\n    }\n\n    /**\n     * Writes the start of a finite-length map.\n     * <p>\n     * After calling this method, one is expected to write any number of map entries, as separate key and value. Keys and values can both be of any type. No length checks are performed.\n     * </p>\n     *\n     * @param length the number of map entries to write, should &gt;= 0.\n     * @throws IllegalArgumentException in case the given length was negative;\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeMapStart(int length) throws IOException {\n        if (length < 0) {\n            throw new IllegalArgumentException(\"Invalid length of map!\");\n        }\n        writeType(TYPE_MAP, length);\n    }\n\n    /**\n     * Writes a <code>null</code> value in canonical CBOR format.\n     *\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeNull() throws IOException {\n        writeSimpleType(TYPE_FLOAT_SIMPLE, NULL);\n    }\n\n    /**\n     * Writes a simple value, i.e., an \"atom\" or \"constant\" value in canonical CBOR format.\n     *\n     * @param value the (unsigned byte) value to write, values from <tt>32</tt> to <tt>255</tt> are supported (though not enforced).\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeSimpleValue(byte simpleValue) throws IOException {\n        // convert to unsigned value...\n        int value = (simpleValue & 0xff);\n        writeType(TYPE_FLOAT_SIMPLE, value);\n    }\n\n    /**\n     * Writes a signed or unsigned small (&lt;= 23) integer value in CBOR format.\n     *\n     * @param value the value to write, values in the range <tt>[-24..23]</tt> are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeSmallInt(int value) throws IOException {\n        // extends the sign over all bits...\n        int sign = value >> 31;\n        // in case value is negative, this bit should be set...\n        int mt = (int) (sign & NEG_INT_MASK);\n        // complement negative value...\n        value = Math.min(0x17, (sign ^ value));\n\n        m_os.write((int) (mt | value));\n    }\n\n    /**\n     * Writes a semantic tag in canonical CBOR format.\n     *\n     * @param tag the tag to write, should &gt;= 0.\n     * @throws IllegalArgumentException in case the given tag was negative;\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeTag(long tag) throws IOException {\n        if (tag < 0) {\n            throw new IllegalArgumentException(\"Invalid tag specification, cannot be negative!\");\n        }\n        writeType(TYPE_TAG, tag);\n    }\n\n    /**\n     * Writes an UTF-8 string in canonical CBOR-format.\n     * <p>\n     * Note that this method is <em>platform</em> specific, as the given string value will be encoded in a byte array\n     * using the <em>platform</em> encoding! This means that the encoding must be standardized and known.\n     * </p>\n     *\n     * @param value the UTF-8 string to write, can be <code>null</code> in which case an UTF-8 string of length <tt>0</tt> is written.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeTextString(String value) throws IOException {\n        writeString(TYPE_TEXT_STRING, value == null ? null : value.getBytes(\"UTF-8\"));\n    }\n\n    /**\n     * Writes the start of an indefinite-length UTF-8 string.\n     * <p>\n     * After calling this method, one is expected to write the given number of string parts. No length checks are performed.<br/>\n     * After all string parts are written, one should write a single break value to end the string, see {@link #writeBreak()}.\n     * </p>\n     *\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeTextStringStart() throws IOException {\n        writeSimpleType(TYPE_TEXT_STRING, BREAK);\n    }\n\n    /**\n     * Writes an \"undefined\" value in canonical CBOR format.\n     *\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    public void writeUndefined() throws IOException {\n        writeSimpleType(TYPE_FLOAT_SIMPLE, UNDEFINED);\n    }\n\n    /**\n     * Encodes and writes the major type and value as a simple type.\n     *\n     * @param majorType the major type of the value to write, denotes what semantics the written value has;\n     * @param value the value to write, values from [0..31] are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeSimpleType(int majorType, int value) throws IOException {\n        m_os.write((majorType << 5) | (value & 0x1f));\n    }\n\n    /**\n     * Writes a byte string in canonical CBOR-format.\n     *\n     * @param majorType the major type of the string, should be either 0x40 or 0x60;\n     * @param bytes the byte string to write, can be <code>null</code> in which case a byte-string of length <tt>0</tt> is written.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeString(int majorType, byte[] bytes) throws IOException {\n        int len = (bytes == null) ? 0 : bytes.length;\n        writeType(majorType, len);\n        if (len > 0){\n            m_os.write(bytes);\n        }\n    }\n\n    /**\n     * Encodes and writes the major type indicator with a given payload (length).\n     *\n     * @param majorType the major type of the value to write, denotes what semantics the written value has;\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeType(int majorType, long value) throws IOException {\n        writeUInt((majorType << 5), value);\n    }\n\n    /**\n     * Encodes and writes an unsigned integer value, that is, tries to encode it in a little bytes as possible.\n     *\n     * @param mt the major type of the value to write, denotes what semantics the written value has;\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeUInt(int mt, long value) throws IOException {\n        if (value < 0x18L) {\n            m_os.write((int) (mt | value));\n        } else if (value < 0x100L) {\n            writeUInt8(mt, (int) value);\n        } else if (value < 0x10000L) {\n            writeUInt16(mt, (int) value);\n        } else if (value < 0x100000000L) {\n            writeUInt32(mt, (int) value);\n        } else {\n            writeUInt64(mt, value);\n        }\n    }\n\n    /**\n     * Encodes and writes an unsigned 16-bit integer value\n     *\n     * @param mt the major type of the value to write, denotes what semantics the written value has;\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeUInt16(int mt, int value) throws IOException {\n        m_os.write(mt | TWO_BYTES);\n        m_os.write(value >> 8);\n        m_os.write(value & 0xFF);\n    }\n\n    /**\n     * Encodes and writes an unsigned 32-bit integer value\n     *\n     * @param mt the major type of the value to write, denotes what semantics the written value has;\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeUInt32(int mt, int value) throws IOException {\n        m_os.write(mt | FOUR_BYTES);\n        m_os.write(value >> 24);\n        m_os.write(value >> 16);\n        m_os.write(value >> 8);\n        m_os.write(value & 0xFF);\n    }\n\n    /**\n     * Encodes and writes an unsigned 64-bit integer value\n     *\n     * @param mt the major type of the value to write, denotes what semantics the written value has;\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeUInt64(int mt, long value) throws IOException {\n        m_os.write(mt | EIGHT_BYTES);\n        m_os.write((int) (value >> 56));\n        m_os.write((int) (value >> 48));\n        m_os.write((int) (value >> 40));\n        m_os.write((int) (value >> 32));\n        m_os.write((int) (value >> 24));\n        m_os.write((int) (value >> 16));\n        m_os.write((int) (value >> 8));\n        m_os.write((int) (value & 0xFF));\n    }\n\n    /**\n     * Encodes and writes an unsigned 8-bit integer value\n     *\n     * @param mt the major type of the value to write, denotes what semantics the written value has;\n     * @param value the value to write, values from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} are supported.\n     * @throws IOException in case of I/O problems writing the CBOR-encoded value to the underlying output stream.\n     */\n    protected void writeUInt8(int mt, int value) throws IOException {\n        m_os.write(mt | ONE_BYTE);\n        m_os.write(value & 0xFF);\n    }\n}"
  },
  {
    "path": "src/peergos/shared/cbor/CborObject.java",
    "content": "package peergos.shared.cbor;\n\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\nimport static peergos.shared.cbor.CborConstants.TYPE_TEXT_STRING;\n\npublic interface CborObject extends Cborable {\n\n    void serialize(CborEncoder encoder);\n\n    List<Multihash> links();\n\n    default byte[] toByteArray() {\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        CborEncoder encoder = new CborEncoder(bout);\n        serialize(encoder);\n        return bout.toByteArray();\n    }\n\n    @Override\n    default CborObject toCbor() {\n        return this;\n    }\n\n    int LINK_TAG = 42;\n\n    static List<Cid> getLinks(Cid h, byte[] data) {\n        return h.isRaw() ?\n                Collections.emptyList() :\n                CborObject.fromByteArray(data)\n                        .links()\n                        .stream()\n                        .map(m -> (Cid) m)\n                        .collect(Collectors.toList());\n    }\n\n    static CborObject fromByteArray(byte[] cbor) {\n        if (cbor.length == 0)\n            throw new IllegalArgumentException(\"Empty cbor byte array!\");\n        return deserialize(new CborDecoder(new ByteArrayInputStream(cbor)), cbor.length);\n    }\n\n    static CborObject read(InputStream in, int maxBytes) {\n        return deserialize(new CborDecoder(in), maxBytes);\n    }\n\n    static CborObject deserialize(CborDecoder decoder, int maxGroupSize) {\n        try {\n            CborType type = decoder.peekType();\n            switch (type.getMajorType()) {\n                case TYPE_TEXT_STRING:\n                    return new CborString(decoder.readTextString(maxGroupSize));\n                case CborConstants.TYPE_BYTE_STRING:\n                    return new CborByteArray(decoder.readByteString(maxGroupSize));\n                case CborConstants.TYPE_UNSIGNED_INTEGER:\n                    return new CborLong(decoder.readInt());\n                case CborConstants.TYPE_NEGATIVE_INTEGER:\n                    return new CborLong(decoder.readInt());\n                case CborConstants.TYPE_FLOAT_SIMPLE:\n                    if (type.getAdditionalInfo() == CborConstants.NULL) {\n                        decoder.readNull();\n                        return new CborNull();\n                    }\n                    if (type.getAdditionalInfo() == CborConstants.TRUE) {\n                        decoder.readBoolean();\n                        return new CborBoolean(true);\n                    }\n                    if (type.getAdditionalInfo() == CborConstants.FALSE) {\n                        decoder.readBoolean();\n                        return new CborBoolean(false);\n                    }\n                    throw new IllegalStateException(\"Unimplemented simple type! \" + type.getAdditionalInfo());\n                case CborConstants.TYPE_MAP: {\n                    long nValues = decoder.readMapLength();\n                    if (nValues > maxGroupSize)\n                        throw new IllegalStateException(\"Invalid cbor: more map elements than original bytes!\");\n                    SortedMap<CborString, CborObject> result = new TreeMap<>();\n                    for (long i=0; i < nValues; i++) {\n                        CborString key = (CborString) deserialize(decoder, maxGroupSize);\n                        CborObject value = deserialize(decoder, maxGroupSize);\n                        CborObject existing = result.put(key, value);\n                        if (existing != null)\n                            throw new IllegalStateException(\"Duplicate map key in cbor!\");\n                    }\n                    return new CborMap(result);\n                }\n                case CborConstants.TYPE_ARRAY:\n                    long nItems = decoder.readArrayLength();\n                    if (nItems > maxGroupSize)\n                        throw new IllegalStateException(\"Invalid cbor: more array elements than original bytes!\");\n                    List<CborObject> res = new ArrayList<>((int) nItems);\n                    for (long i=0; i < nItems; i++)\n                        res.add(deserialize(decoder, maxGroupSize));\n                    return new CborList(res);\n                case CborConstants.TYPE_TAG:\n                    long tag = decoder.readTag();\n                    if (tag == LINK_TAG) {\n                        CborObject value = deserialize(decoder, maxGroupSize);\n                        if (value instanceof CborString)\n                            return new CborMerkleLink(Cid.decode(((CborString) value).value));\n                        if (value instanceof CborByteArray) {\n                            byte[] bytes = ((CborByteArray) value).value;\n                            if (bytes[0] == 0) // multibase for binary\n                                return new CborMerkleLink(Cid.cast(Arrays.copyOfRange(bytes, 1, bytes.length)));\n                            throw new IllegalStateException(\"Unknown Multibase decoding Merkle link: \" + bytes[0]);\n                        }\n                        throw new IllegalStateException(\"Invalid type for merkle link: \" + value);\n                    }\n                    throw new IllegalStateException(\"Unknown TAG in CBOR: \" + type.getAdditionalInfo());\n                default:\n                    throw new IllegalStateException(\"Unimplemented cbor type: \" + type);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    final class CborMap implements CborObject {\n        // Only String keys should be used in IPLD dag-cbor maps\n        private final SortedMap<CborString, CborObject> values;\n\n        private CborMap(SortedMap<CborString, CborObject> values) {\n            this.values = values;\n        }\n\n        public static CborMap build(Map<String, Cborable> values) {\n            SortedMap<CborString, CborObject> transformed = values.entrySet()\n                    .stream()\n                    .collect(Collectors.toMap(\n                            e -> new CborString(e.getKey()),\n                            e -> e.getValue().toCbor(),\n                            (a, b) -> a, TreeMap::new));\n            return new CborMap(transformed);\n        }\n\n        public void put(String key, CborObject val) {\n            values.put(new CborString(key), val);\n        }\n\n        public boolean containsKey(String key) {\n            return values.containsKey(new CborString(key));\n        }\n\n        public Set<String> keySet() {\n            return values.keySet().stream()\n                    .map(c -> c.value)\n                    .collect(Collectors.toSet());\n        }\n\n        public Cborable get(String key) {\n            return values.get(new CborString(key));\n        }\n\n        public <T> T getObject(String key, Function<Cborable, T> fromCbor) {\n            return fromCbor.apply(get(key));\n        }\n\n        public String getString(String key) {\n            return ((CborString) get(key)).value;\n        }\n\n        public String getString(String key, String defaultValue) {\n            CborString cborKey = new CborString(key);\n            Cborable val = values.get(cborKey);\n            return val != null ? ((CborString) val).value : defaultValue;\n        }\n\n        public long getLong(String key) {\n            return ((CborLong) get(key)).value;\n        }\n\n        public Multihash getMerkleLink(String key) {\n            return ((CborMerkleLink) get(key)).target;\n        }\n\n        public boolean getBoolean(String key) {\n            CborBoolean val = (CborBoolean) get(key);\n            return val != null && val.value;\n        }\n\n        public boolean getBoolean(String key, boolean def) {\n            Cborable val = get(key);\n            if (val == null)\n                return def;\n            return ((CborBoolean) val).value;\n        }\n\n        public Optional<byte[]> getOptionalByteArray(String key) {\n            return Optional.ofNullable((CborByteArray) get(key)).map(c -> c.value);\n        }\n\n        public byte[] getByteArray(String key) {\n            return ((CborByteArray) get(key)).value;\n        }\n\n        public Optional<Cborable> getOptional(String key) {\n            return Optional.ofNullable(get(key));\n        }\n\n        public <T> Optional<T> getOptional(String key, Function<Cborable, T> fromCbor) {\n            return Optional.ofNullable(get(key)).map(fromCbor);\n        }\n\n        public Optional<Long> getOptionalLong(String key) {\n            return Optional.ofNullable((CborLong) get(key)).map(c -> c.value);\n        }\n\n        public Optional<String> getOptionalString(String key) {\n            return Optional.ofNullable((CborString) get(key)).map(c -> c.value);\n        }\n\n        public <T> List<T> getList(String key, Function<Cborable, T> fromCbor) {\n            CborList cborList = (CborList) get(key);\n            if (cborList == null)\n                return Collections.emptyList();\n            return cborList.value\n                    .stream()\n                    .map(fromCbor)\n                    .collect(Collectors.toList());\n        }\n\n        public void applyToAll(BiConsumer<String, Cborable> func) {\n            values.entrySet().forEach(e -> func.accept(e.getKey().value, e.getValue()));\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeMapStart(values.size());\n                for (Map.Entry<CborString, CborObject>  entry : values.entrySet()) {\n                    entry.getKey().serialize(encoder);\n                    entry.getValue().toCbor().serialize(encoder);\n                }\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return values.values().stream()\n                    .flatMap(cbor -> cbor.toCbor().links().stream())\n                    .collect(Collectors.toList());\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CborMap cborMap = (CborMap) o;\n\n            return values != null ? values.equals(cborMap.values) : cborMap.values == null;\n\n        }\n\n        @Override\n        public int hashCode() {\n            return values != null ? values.hashCode() : 0;\n        }\n\n        public CborList getList(String key) {\n            return (CborList) get(key);\n        }\n\n        public <T> T get(String key, Function<? super Cborable, T> fromCbor) {\n            return fromCbor.apply(get(key));\n        }\n\n        public <K,V> Map<K,V> toMap(Function<? super Cborable, K> toKey, Function<? super Cborable, V> toValue) {\n            return values.entrySet().stream()\n                .collect(Collectors.toMap(\n                    e -> toKey.apply(e.getKey()),\n                    e -> toValue.apply(e.getValue())\n                ));\n        }\n\n        public <K,V> Map<K,V> getMap(String key, Function<? super Cborable, K> toKey, Function<? super Cborable, V> toValue) {\n            CborMap val = (CborMap) get(key);\n            if (val == null)\n                return Collections.emptyMap();\n            return val.toMap(toKey, toValue);\n        }\n\n        public <K,V> Map<K,V> getListMap(String key, Function<? super Cborable, K> toKey, Function<? super Cborable, V> toValue) {\n            CborList val = (CborList) get(key);\n            if (val == null)\n                return Collections.emptyMap();\n            return val.getMap(toKey, toValue);\n        }\n    }\n\n    final class CborMerkleLink implements CborObject, Comparable<CborMerkleLink> {\n        public final Multihash target;\n\n        public CborMerkleLink(Multihash target) {\n            this.target = target;\n        }\n\n        @Override\n        public int compareTo(CborMerkleLink that) {\n            return this.target.compareTo(that.target);\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeTag(LINK_TAG);\n                byte[] cid = target.toBytes();\n                byte[] withMultibaseHeader = new byte[cid.length + 1];\n                System.arraycopy(cid, 0, withMultibaseHeader, 1, cid.length);\n                encoder.writeByteString(withMultibaseHeader);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return Collections.singletonList(target);\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CborMerkleLink that = (CborMerkleLink) o;\n\n            return target != null ? target.equals(that.target) : that.target == null;\n\n        }\n\n        @Override\n        public int hashCode() {\n            return target != null ? target.hashCode() : 0;\n        }\n\n        @Override\n        public String toString() {\n            return target.toString();\n        }\n    }\n\n    final class CborList implements CborObject, Cborable {\n        public final List<? extends Cborable> value;\n\n        public CborList(List<? extends Cborable> value) {\n            this.value = value;\n        }\n\n        public CborList(Map<? extends Cborable, ? extends Cborable> map) {\n            this.value = map.entrySet().stream()\n                .flatMap(e -> Stream.of(e.getKey(), e.getValue()))\n                .collect(Collectors.toList());\n        }\n\n        public static <T> CborList build(List<T> in, Function<T, Cborable> toCbor) {\n            return new CborList(in.stream().map(toCbor).collect(Collectors.toList()));\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeArrayStart(value.size());\n                for (Cborable object : value) {\n                    object.toCbor().serialize(encoder);\n                }\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return value.stream()\n                    .flatMap(cbor -> cbor.toCbor().links().stream())\n                    .collect(Collectors.toList());\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CborList cborList = (CborList) o;\n\n            return value != null ? value.equals(cborList.value) : cborList.value == null;\n        }\n\n        @Override\n        public int hashCode() {\n            return value != null ? value.hashCode() : 0;\n        }\n\n        public <T> List<T> map(Function<? super Cborable, T> fromCbor) {\n            return value.stream()\n                .map(fromCbor)\n                .collect(Collectors.toList());\n        }\n\n        public long getLong(int index) {\n            return ((CborLong)value.get(index)).value;\n        }\n\n        public <T> T get(int index, Function<? super Cborable, T> fromCbor) {\n            return fromCbor.apply(value.get(index));\n        }\n\n        public <K,V> Map<K, V> getMap(Function<? super Cborable, K> toKey, Function<? super Cborable, V> toValue) {\n            if (value.size() % 2 != 0)\n                throw new IllegalStateException();\n\n            Map<K, V> map = new HashMap<>();\n            for (int i = 0; i < value.size(); i += 2) {\n                K key = toKey.apply(value.get(i));\n                V _value = toValue.apply(value.get(i + 1));\n                map.put(key, _value);\n            }\n            return map;\n        }\n    }\n\n    final class CborBoolean implements CborObject {\n        public final boolean value;\n\n        public CborBoolean(boolean value) {\n            this.value = value;\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeBoolean(value);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CborBoolean that = (CborBoolean) o;\n\n            return value == that.value;\n\n        }\n\n        @Override\n        public int hashCode() {\n            return (value ? 1 : 0);\n        }\n\n        @Override\n        public String toString() {\n            return \"CborBoolean{\" +\n                    value +\n                    '}';\n        }\n    }\n\n    final class CborByteArray implements CborObject, Comparable<CborByteArray> {\n        public final byte[] value;\n\n        public CborByteArray(byte[] value) {\n            this.value = value;\n        }\n\n        @Override\n        public int compareTo(CborByteArray other) {\n            return compare(value, other.value);\n        }\n\n        /** This only matter so that we can have byte[]'s as keys in a sorted map deterministically\n         *\n         * @param a\n         * @param b\n         * @return\n         */\n        public static int compare(byte[] a, byte[] b)\n        {\n            if (a.length != b.length)\n                return a.length - b.length;\n            for (int i=0; i < a.length; i++)\n                if (a[i] != b[i])\n                    return (a[i] & 0xff) - (b[i] & 0xff);\n            return 0;\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeByteString(value);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CborByteArray that = (CborByteArray) o;\n\n            return Arrays.equals(value, that.value);\n\n        }\n\n        @Override\n        public int hashCode() {\n            return Arrays.hashCode(value);\n        }\n    }\n\n    final class CborString implements CborObject, Comparable<CborString> {\n\n        public final String value;\n\n        public CborString(String value) {\n            this.value = value;\n        }\n\n        @Override\n        public int compareTo(CborString cborString) {\n            int lenDiff = value.length() - cborString.value.length();\n            if (lenDiff != 0)\n                return lenDiff;\n            return value.compareTo(cborString.value);\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeTextString(value);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CborString that = (CborString) o;\n\n            return value.equals(that.value);\n\n        }\n\n        @Override\n        public int hashCode() {\n            return value.hashCode();\n        }\n\n        @Override\n        public String toString() {\n            return \"CborString{\\\"\" +\n                    value +\n                    \"\\\"}\";\n        }\n\n        public static String getString(Cborable cbor) {\n            return ((CborString)cbor).value;\n        }\n    }\n\n    final class CborLong implements CborObject, Comparable<CborLong> {\n        public final long value;\n\n        public CborLong(long value) {\n            this.value = value;\n        }\n\n        @Override\n        public int compareTo(CborLong other) {\n            return Long.compare(value, other.value);\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeInt(value);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            CborLong cborLong = (CborLong) o;\n\n            return value == cborLong.value;\n\n        }\n\n        @Override\n        public int hashCode() {\n            return (int) (value ^ (value >>> 32));\n        }\n\n        @Override\n        public String toString() {\n            return \"CborLong{\" +\n                    value +\n                    '}';\n        }\n    }\n\n    final class CborNull implements CborObject, Comparable<CborNull> {\n        public CborNull() {}\n\n        @Override\n        public int compareTo(CborNull cborNull) {\n            return 0;\n        }\n\n        @Override\n        public void serialize(CborEncoder encoder) {\n            try {\n                encoder.writeNull();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<Multihash> links() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            return true;\n        }\n\n        @Override\n        public int hashCode() {\n            return 0;\n        }\n\n        @Override\n        public String toString() {\n            return \"CborNull{}\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/cbor/CborType.java",
    "content": "package peergos.shared.cbor;\n\n/*\n * JACOB - CBOR implementation in Java.\n *\n * (C) Copyright - 2013 - J.W. Janssen <j.w.janssen@lxtreme.nl>\n *\n * Licensed under Apache License v2.0.\n */\n\nimport static peergos.shared.cbor.CborConstants.*;\n\n/**\n * Represents the various major types in CBOR, along with their .\n * <p>\n * The major type is encoded in the upper three bits of each initial byte. The lower 5 bytes represent any additional information.\n * </p>\n */\npublic class CborType {\n    private final int m_major;\n    private final int m_additional;\n\n    private CborType(int major, int additional) {\n        m_major = major;\n        m_additional = additional;\n    }\n\n    /**\n     * Returns a descriptive string for the given major type.\n     *\n     * @param mt the major type to return as string, values from [0..7] are supported.\n     * @return the name of the given major type, as String, never <code>null</code>.\n     * @throws IllegalArgumentException in case the given major type is not supported.\n     */\n    public static String getName(int mt) {\n        switch (mt) {\n            case TYPE_ARRAY:\n                return \"array\";\n            case TYPE_BYTE_STRING:\n                return \"byte string\";\n            case TYPE_FLOAT_SIMPLE:\n                return \"float/simple value\";\n            case TYPE_MAP:\n                return \"map\";\n            case TYPE_NEGATIVE_INTEGER:\n                return \"negative integer\";\n            case TYPE_TAG:\n                return \"tag\";\n            case TYPE_TEXT_STRING:\n                return \"text string\";\n            case TYPE_UNSIGNED_INTEGER:\n                return \"unsigned integer\";\n            default:\n                throw new IllegalArgumentException(\"Invalid major type: \" + mt);\n        }\n    }\n\n    /**\n     * Decodes a given byte value to a {@link CborType} value.\n     *\n     * @param i the input byte (8-bit) to decode into a {@link CborType} instance.\n     * @return a {@link CborType} instance, never <code>null</code>.\n     */\n    public static CborType valueOf(int i) {\n        return new CborType((i & 0xff) >>> 5, i & 0x1f);\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (obj == null || getClass() != obj.getClass()) {\n            return false;\n        }\n\n        CborType other = (CborType) obj;\n        return (m_major == other.m_major) && (m_additional == other.m_additional);\n    }\n\n    /**\n     * @return the additional information of this type, as integer value from [0..31].\n     */\n    public int getAdditionalInfo() {\n        return m_additional;\n    }\n\n    /**\n     * @return the major type, as integer value from [0..7].\n     */\n    public int getMajorType() {\n        return m_major;\n    }\n\n    @Override\n    public int hashCode() {\n        final int prime = 31;\n        int result = 1;\n        result = prime * result + m_additional;\n        result = prime * result + m_major;\n        return result;\n    }\n\n    /**\n     * @return <code>true</code> if this type allows for an infinite-length payload,\n     *         <code>false</code> if only definite-length payloads are allowed.\n     */\n    public boolean isBreakAllowed() {\n        return m_major == TYPE_ARRAY || m_major == TYPE_BYTE_STRING || m_major == TYPE_MAP\n                || m_major == TYPE_TEXT_STRING;\n    }\n\n    /**\n     * Determines whether the major type of a given {@link CborType} equals the major type of this {@link CborType}.\n     *\n     * @param other the {@link CborType} to compare against, cannot be <code>null</code>.\n     * @return <code>true</code> if the given {@link CborType} is of the same major type as this {@link CborType}, <code>false</code> otherwise.\n     * @throws IllegalArgumentException in case the given argument was <code>null</code>.\n     */\n    public boolean isEqualType(CborType other) {\n        if (other == null) {\n            throw new IllegalArgumentException(\"Parameter cannot be null!\");\n        }\n        return m_major == other.m_major;\n    }\n\n    /**\n     * Determines whether the major type of a given byte value (representing an encoded {@link CborType}) equals the major type of this {@link CborType}.\n     *\n     * @param encoded the encoded CBOR type to compare.\n     * @return <code>true</code> if the given byte value represents the same major type as this {@link CborType}, <code>false</code> otherwise.\n     */\n    public boolean isEqualType(int encoded) {\n        return m_major == ((encoded & 0xff) >>> 5);\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(getName(m_major)).append('(').append(m_additional).append(')');\n        return sb.toString();\n    }\n}"
  },
  {
    "path": "src/peergos/shared/cbor/Cborable.java",
    "content": "package peergos.shared.cbor;\n\nimport jsinterop.annotations.JsType;\n\nimport java.util.function.*;\n\n@JsType\npublic interface Cborable {\n\n    CborObject toCbor();\n\n    default byte[] serialize() {\n        return toCbor().toByteArray();\n    }\n\n    static <T> Function<byte[], T> parser(Function<Cborable, T> parser) {\n        return arr -> parser.apply(CborObject.fromByteArray(arr));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/CoreNode.java",
    "content": "package peergos.shared.corenode;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface CoreNode {\n    int MAX_USERNAME_SIZE = 64;\n\n    default void initialize(boolean mirrorUsers) {}\n\n    CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                           UserPublicKeyLink chain,\n                                                           OpLog setupOperations,\n                                                           ProofOfWork proof,\n                                                           String token);\n\n    CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username,\n                                                                                     UserPublicKeyLink chain,\n                                                                                     ProofOfWork proof);\n\n    CompletableFuture<PaymentProperties> completePaidSignup(String username,\n                                                            UserPublicKeyLink chain,\n                                                            OpLog setupOperations,\n                                                            byte[] signedSpaceRequest,\n                                                            ProofOfWork proof);\n\n    CompletableFuture<Boolean> startMirror(String username,\n                                           BatWithId mirrorBat,\n                                           byte[] auth,\n                                           ProofOfWork proof);\n\n    CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username,\n                                                                                     byte[] auth,\n                                                                                     ProofOfWork proof);\n\n    CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate);\n\n    CompletableFuture<PaymentProperties> completePaidMirror(String username,\n                                                            BatWithId mirrorBat,\n                                                            byte[] signedSpaceRequest,\n                                                            ProofOfWork proof);\n\n    /**\n     *\n     * @param username\n     * @return the key chain proving the claim of the requested username and the ipfs node id of their storage\n     */\n    CompletableFuture<List<UserPublicKeyLink>> getChain(String username);\n\n    /** Claim a username, or change the public key owning a username\n     *\n     * @param username\n     * @param chain The changed links of the chain\n     * @param proof Any required proof of work\n     * @param token Any required token to authorise signup on this server\n     * @return Optional.empty() if successfully updated, otherwise the required difficulty\n     */\n    CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                List<UserPublicKeyLink> chain,\n                                                                ProofOfWork proof,\n                                                                String token);\n\n    /**\n     *\n     * @param key the hash of the public identity key of a user\n     * @return the username claimed by a given public key\n     */\n    CompletableFuture<String> getUsername(PublicKeyHash key);\n\n    /**\n     *\n     * @param prefix\n     * @return All usernames starting with prefix\n     */\n    CompletableFuture<List<String>> getUsernames(String prefix);\n\n    CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                List<UserPublicKeyLink> newChain,\n                                                Multihash currentStorageId,\n                                                Optional<BatWithId> mirrorBat,\n                                                LocalDateTime latestLinkCountUpdate,\n                                                long usage,\n                                                boolean commitToPki);\n\n    CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId);\n\n    /** This is only implemented by caching corenodes\n     *\n     * @param username\n     * @return\n     */\n    default CompletableFuture<Boolean> updateUser(String username) {return CompletableFuture.completedFuture(true);}\n\n    /**\n     *\n     * @param username\n     * @return the public key for a username, if present\n     */\n    default CompletableFuture<Optional<PublicKeyHash>> getPublicKeyHash(String username) {\n        return getChain(username).thenApply(chain -> {\n            if (chain.size() == 0)\n                return Optional.empty();\n            else\n                return Optional.of(chain.get(chain.size() - 1).owner);\n        });\n    }\n\n    default CompletableFuture<Optional<Multihash>> getHomeServer(String username) {\n        return getChain(username).thenApply(chain -> {\n            if (chain.size() == 0)\n                return Optional.empty();\n            else\n                return Optional.of(chain.get(chain.size() - 1).claim.storageProviders.get(0));\n        });\n    }\n\n    default List<Multihash> getStorageProviders(PublicKeyHash owner) {\n        String username = getUsername(owner).join();\n        List<UserPublicKeyLink> chain = getChain(username).join();\n        if (chain.isEmpty())\n            return Collections.emptyList();\n        return chain.get(chain.size() - 1).claim.storageProviders;\n    }\n\n    void close() throws IOException;\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/CoreNodeUtils.java",
    "content": "package peergos.shared.corenode;\n\nimport java.io.DataInputStream;\nimport java.io.IOException;\n\nimport peergos.shared.util.Serialize;\n\npublic class CoreNodeUtils {\n\tpublic static final int MAX_KEY_LENGTH = 1024*1024;\n\t\n    public static byte[] deserializeByteArray(DataInputStream din) throws IOException\n    {\n        return Serialize.deserializeByteArray(din, MAX_KEY_LENGTH);\n    }\n\n    public static byte[] getByteArray(int len) throws IOException\n    {\n        return Serialize.getByteArray(len, MAX_KEY_LENGTH);\n    }\n\n    public static String deserializeString(DataInputStream din) throws IOException\n    {\n        return Serialize.deserializeString(din, 1024);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/HTTPCoreNode.java",
    "content": "\npackage peergos.shared.corenode;\nimport java.time.*;\nimport java.util.logging.*;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class HTTPCoreNode implements CoreNode {\n\tprivate static final Logger LOG = Logger.getLogger(HTTPCoreNode.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n\tprivate static final String P2P_PROXY_PROTOCOL = \"/http\";\n\n    private final HttpPoster poster;\n    private final String urlPrefix;\n\n    public HTTPCoreNode(HttpPoster p2p, Multihash pkiServerNodeId)\n    {\n        if (pkiServerNodeId == null)\n            throw new IllegalStateException(\"Null pki server node id!\");\n        this.poster = p2p;\n        this.urlPrefix = getProxyUrlPrefix(pkiServerNodeId);\n    }\n\n    public HTTPCoreNode(HttpPoster direct)\n    {\n        this.poster = direct;\n        this.urlPrefix = \"\";\n    }\n\n    private static String getProxyUrlPrefix(Multihash targetId) {\n        return \"/p2p/\" + targetId.toString() + P2P_PROXY_PROTOCOL + \"/\";\n    }\n\n    @Override\n    public CompletableFuture<Optional<PublicKeyHash>> getPublicKeyHash(String username) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            dout.flush();\n\n            CompletableFuture<byte[]> fut = poster.postUnzip(urlPrefix + Constants.CORE_URL + \"getPublicKey\", bout.toByteArray());\n            return fut.thenApply(res -> {\n                DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n\n                try {\n                    if (!din.readBoolean())\n                        return Optional.empty();\n                    byte[] publicKey = CoreNodeUtils.deserializeByteArray(din);\n                    return Optional.of(PublicKeyHash.fromCbor(CborObject.fromByteArray(publicKey)));\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return CompletableFuture.completedFuture(Optional.empty());\n        }\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash owner) {\n        try\n        {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(owner.serialize(), dout);\n            dout.flush();\n            CompletableFuture<byte[]> fut = poster.post(urlPrefix + Constants.CORE_URL + \"getUsername\", bout.toByteArray(), true);\n            return fut.thenApply(res -> {\n                DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n                try {\n                    String username = Serialize.deserializeString(din, CoreNode.MAX_USERNAME_SIZE);\n                    return username;\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        } catch (IOException ioe) {\n            LOG.severe(\"Couldn't connect to \" + poster);\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return null;\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        try\n        {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"getChain\", bout.toByteArray()).thenApply(res -> {\n                CborObject cbor = CborObject.fromByteArray(res);\n                if (! (cbor instanceof CborObject.CborList))\n                    throw new IllegalStateException(\"Invalid cbor for claim chain: \" + cbor);\n                return ((CborObject.CborList) cbor).value.stream()\n                        .map(UserPublicKeyLink::fromCbor)\n                        .collect(Collectors.toList());\n            });\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            throw new IllegalStateException(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink chain,\n                                                                  OpLog ops,\n                                                                  ProofOfWork proof,\n                                                                  String token) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(chain.serialize(), dout);\n            Serialize.serialize(ops.serialize(), dout);\n            Serialize.serialize(proof.serialize(), dout);\n            Serialize.serialize(token, dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"signup\", bout.toByteArray())\n                    .thenApply(res -> {\n                        DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n                        try {\n                            boolean success = din.readBoolean();\n                            if (success)\n                                return Optional.empty();\n                            return Optional.of(new RequiredDifficulty(din.readInt()));\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    });\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username, UserPublicKeyLink chain, ProofOfWork proof) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(chain.serialize(), dout);\n            Serialize.serialize(proof.serialize(), dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"startPaidSignup\", bout.toByteArray())\n                    .thenApply(res -> {\n                        DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n                        try {\n                            boolean success = din.readBoolean();\n                            if (success) {\n                                return Either.a(PaymentProperties.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 1024))));\n                            }\n                            return Either.b(new RequiredDifficulty(din.readInt()));\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    });\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username,\n                                                                   UserPublicKeyLink chain,\n                                                                   OpLog setupOperations,\n                                                                   byte[] signedspaceRequest,\n                                                                   ProofOfWork proof) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(chain.serialize(), dout);\n            Serialize.serialize(setupOperations.serialize(), dout);\n            Serialize.serialize(proof.serialize(), dout);\n            Serialize.serialize(signedspaceRequest, dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"completePaidSignup\", bout.toByteArray())\n                    .thenApply(res -> PaymentProperties.fromCbor(CborObject.fromByteArray(res)));\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(mirrorBat.serialize(), dout);\n            Serialize.serialize(auth, dout);\n            Serialize.serialize(proof.serialize(), dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"mirror\", bout.toByteArray())\n                    .thenApply(res -> {\n                        DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n                        try {\n                            return din.readBoolean();\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    });\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(auth, dout);\n            Serialize.serialize(proof.serialize(), dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"startPaidMirror\", bout.toByteArray())\n                    .thenApply(res -> {\n                        DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n                        try {\n                            boolean success = din.readBoolean();\n                            if (success) {\n                                return Either.a(PaymentProperties.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(din, 1024))));\n                            }\n                            return Either.b(new RequiredDifficulty(din.readInt()));\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    });\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(mirrorBat.serialize(), dout);\n            Serialize.serialize(proof.serialize(), dout);\n            Serialize.serialize(signedSpaceRequest, dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"completePaidMirror\", bout.toByteArray())\n                    .thenApply(res -> PaymentProperties.fromCbor(CborObject.fromByteArray(res)));\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(prefix, dout);\n            Serialize.serialize(instanceBat.serialize(), dout);\n            dout.writeLong(lastLinkCountsUpdate.toEpochSecond(ZoneOffset.UTC));\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"getUserSnapshots\", bout.toByteArray(), -1)\n                    .thenApply(res -> ((CborObject.CborList)CborObject.fromByteArray(res))\n                            .map(UserSnapshot::fromCbor));\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                       List<UserPublicKeyLink> chain,\n                                                                       ProofOfWork proof,\n                                                                       String token) {\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(new CborObject.CborList(chain).serialize(), dout);\n            Serialize.serialize(proof.serialize(), dout);\n            Serialize.serialize(token, dout);\n            dout.flush();\n\n            return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"updateChain\", bout.toByteArray())\n                    .thenApply(res -> {\n                        DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n                        try {\n                            boolean success = din.readBoolean();\n                            if (success)\n                                return Optional.empty();\n                            return Optional.of(new RequiredDifficulty(din.readInt()));\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    });\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        return poster.postUnzip(urlPrefix + Constants.CORE_URL + \"getUsernamesGzip/\"+prefix, new byte[0])\n                .thenApply(raw -> (List) JSONParser.parse(new String(raw)));\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime latestLinkCountUpdate,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        String modifiedPrefix = urlPrefix.isEmpty() ? \"\" : getProxyUrlPrefix(currentStorageId);\n        try {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            DataOutputStream dout = new DataOutputStream(bout);\n\n            Serialize.serialize(username, dout);\n            Serialize.serialize(new CborObject.CborList(newChain).serialize(), dout);\n            Serialize.serialize(currentStorageId.toBytes(), dout);\n            dout.writeBoolean(mirrorBat.isPresent());\n            if (mirrorBat.isPresent())\n                Serialize.serialize(mirrorBat.get().serialize(), dout);\n            dout.writeLong(latestLinkCountUpdate.toEpochSecond(ZoneOffset.UTC));\n            dout.writeLong(currentUsage);\n            dout.writeBoolean(commitToPki);\n            dout.flush();\n\n            return poster.postUnzip(modifiedPrefix + Constants.CORE_URL + \"migrateUser\", bout.toByteArray(), -1)\n                    .thenApply(res -> UserSnapshot.fromCbor(CborObject.fromByteArray(res)));\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return Futures.errored(ioe);\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        throw new IllegalStateException(\"getNextServerId cannot be called remotely!\");\n    }\n\n    @Override public void close() {}\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/OfflineCorenode.java",
    "content": "package peergos.shared.corenode;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class OfflineCorenode implements CoreNode {\n\n    private final CoreNode target;\n    private final PkiCache pkiCache;\n    private final OnlineState online;\n\n    public OfflineCorenode(CoreNode target, PkiCache pkiCache, OnlineState online) {\n        this.target = target;\n        this.pkiCache = pkiCache;\n        this.online = online;\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink userPublicKeyLink,\n                                                                  OpLog opLog,\n                                                                  ProofOfWork proofOfWork,\n                                                                  String token) {\n        return target.signup(username, userPublicKeyLink, opLog, proofOfWork, token);\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username,\n                                                                                            UserPublicKeyLink chain,\n                                                                                            ProofOfWork proof) {\n        return target.startPaidSignup(username, chain, proof);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username,\n                                                                   UserPublicKeyLink chain,\n                                                                   OpLog setupOperations,\n                                                                   byte[] signedSpaceRequest,\n                                                                   ProofOfWork proof) {\n        return target.completePaidSignup(username, chain, setupOperations, signedSpaceRequest, proof);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        return target.startMirror(username, mirrorBat, auth, proof);\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        return target.startPaidMirror(username, auth, proof);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        return target.completePaidMirror(username, mirrorBat, signedSpaceRequest, proof);\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        return Futures.asyncExceptionally(\n                () -> {\n                    if (online.isOnline())\n                        return target.getChain(username).thenApply(chain -> {\n                            if (!chain.isEmpty())\n                                pkiCache.setChain(username, chain);\n                            return chain;\n                        });\n                    online.updateAsync();\n                    return pkiCache.getChain(username);\n                },\n                t -> {\n                    if (online.isOfflineException(t))\n                        return pkiCache.getChain(username);\n                    return Futures.errored(t);\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                       List<UserPublicKeyLink> chain,\n                                                                       ProofOfWork proofOfWork,\n                                                                       String token) {\n        return target.updateChain(username, chain, proofOfWork, token)\n                .thenApply(work -> {\n                    if (work.isEmpty())\n                        target.getChain(username)\n                                .thenCompose(updated -> pkiCache.setChain(username, updated));\n                    return work;\n        });\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash identity) {\n        return Futures.asyncExceptionally(\n                () -> target.getUsername(identity),\n                t -> pkiCache.getUsername(identity));\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        return target.getUsernames(prefix)\n                .exceptionally(t -> Collections.emptyList());\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime latestLinkCountUpdate,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        return target.migrateUser(username, newChain, currentStorageId, mirrorBat, latestLinkCountUpdate, currentUsage, commitToPki);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return target.getNextServerId(serverId);\n    }\n\n    @Override\n    public void close() throws IOException {\n        target.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/OpLog.java",
    "content": "package peergos.shared.corenode;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/** Oplog is used  during signup to atomically apply a set of operations\n *\n */\npublic class OpLog implements Cborable, Account, MutablePointers, ContentAddressedStorage, BatCave {\n    private static final int ED25519_SIGNATURE_SIZE = 64;\n\n    public final List<Either<PointerWrite, BlockWrite>> operations;\n    private final Map<Multihash, byte[]> storage = new HashMap<>();\n    public Pair<LoginData, byte[]> loginData;\n    public Optional<Pair<BatWithId, byte[]>> mirrorBat;\n\n    public OpLog(List<Either<PointerWrite, BlockWrite>> operations,\n                 Pair<LoginData, byte[]> loginData,\n                 Optional<Pair<BatWithId, byte[]>> mirrorBat) {\n        this.operations = operations;\n        this.loginData = loginData;\n        this.mirrorBat = mirrorBat;\n    }\n\n    @Override\n    public synchronized CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        operations.add(Either.a(new PointerWrite(writer, writerSignedBtreeRootHash)));\n        return Futures.of(true);\n    }\n\n    @Override\n    public synchronized CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        updates.forEach(u -> operations.add(Either.a(new PointerWrite(u.writer, u.signed))));\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        for (int i= operations.size() - 1; i>=0; i--) {\n            Either<PointerWrite, BlockWrite> op = operations.get(i);\n            if (op.isA() && op.a().writer.equals(writer))\n                return Futures.of(Optional.of(op.a().writerSignedChampRootCas));\n        }\n        throw new IllegalStateException(\"Unknown writer: \" + writer);\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return this;\n    }\n\n    @Override\n    public synchronized CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        loginData = new Pair<>(login, auth);\n        return Futures.of(true);\n    }\n\n    @Override\n    public synchronized CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                                       PublicSigningKey authorisedReader,\n                                                                                                       byte[] auth,\n                                                                                                       Optional<MultiFactorAuthResponse> mfa,\n                                                                                                       boolean cacheMfaLoginData,\n                                                                                                       boolean forceProxy,\n                                                                                                       boolean forceNoCache) {\n        if (loginData == null)\n            throw new IllegalStateException(\"No login data present!\");\n        if (! loginData.left.username.equals(username))\n            throw new IllegalStateException(\"No login data present for \" + username);\n        if (! loginData.left.authorisedReader.equals(authorisedReader))\n            throw new IllegalStateException(\"You are not authorised to login as \" + username);\n        return Futures.of(Either.a(loginData.left.entryPoints));\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public Optional<Bat> getBat(BatId id) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username, byte[] auth) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, byte[] auth) {\n        if (mirrorBat.isPresent())\n            throw new IllegalStateException(\"Only 1 mirror BAT allowed in OpLog!\");\n        mirrorBat = Optional.of(new Pair<>(new BatWithId(bat, id.id), auth));\n        return Futures.of(true);\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return Futures.of(new TransactionId(\"1\"));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                                  PublicKeyHash writer,\n                                                  List<byte[]> signedHashes,\n                                                  List<byte[]> blocks,\n                                                  TransactionId tid) {\n        return Futures.combineAllInOrder(IntStream.range(0, blocks.size())\n                .mapToObj(i -> put(writer, signedHashes.get(i), blocks.get(i), false))\n                .collect(Collectors.toList()));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return Futures.of(Optional.ofNullable(storage.get(hash)).map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                                     PublicKeyHash writer,\n                                                     List<byte[]> signedHashes,\n                                                     List<byte[]> blocks,\n                                                     TransactionId tid,\n                                                     ProgressConsumer<Long> progressCounter) {\n        return Futures.combineAllInOrder(IntStream.range(0, blocks.size())\n                .mapToObj(i -> put(writer, signedHashes.get(i), blocks.get(i), true))\n                .collect(Collectors.toList()));\n    }\n\n    private CompletableFuture<Cid> put(PublicKeyHash writer, byte[] signedHash, byte[] block, boolean isRaw) {\n        // Assume we are using ed25519 for now\n        byte[] hash = Arrays.copyOfRange(signedHash, ED25519_SIGNATURE_SIZE, signedHash.length);\n        Cid h = new Cid(1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash);\n        if (! storage.containsKey(h)) {\n            storage.put(h, block);\n            operations.add(Either.b(new BlockWrite(writer, signedHash, block, isRaw, Optional.empty())));\n        }\n        return Futures.of(h);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return Futures.of(Optional.ofNullable(storage.get(hash)));\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return Futures.of(new ArrayList<>(storage.values()));\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    public static final class BlockWrite implements Cborable {\n        public final PublicKeyHash writer;\n        public final byte[] signature, block;\n        public final boolean isRaw;\n        public final Optional<ProgressConsumer<Long>> progressMonitor;\n\n        public BlockWrite(PublicKeyHash writer, byte[] signature, byte[] block, boolean isRaw, Optional<ProgressConsumer<Long>> progressMonitor) {\n            if (block.length == 0)\n                throw new IllegalArgumentException(\"Empty byte array in block write!\");\n            this.writer = writer;\n            this.signature = signature;\n            this.block = block;\n            this.isRaw = isRaw;\n            this.progressMonitor = progressMonitor;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"w\", writer);\n            state.put(\"s\", new CborObject.CborByteArray(signature));\n            state.put(\"b\", new CborObject.CborByteArray(block));\n            state.put(\"r\", new CborObject.CborBoolean(isRaw));\n            return CborObject.CborMap.build(state);\n        }\n\n        public static BlockWrite fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor!\");\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n            PublicKeyHash writer = m.get(\"w\", PublicKeyHash::fromCbor);\n            byte[] signature = m.getByteArray(\"s\");\n            byte[] block = m.getByteArray(\"b\");\n            boolean isRaw = m.getBoolean(\"r\");\n            return new BlockWrite(writer, signature, block, isRaw, Optional.empty());\n        }\n\n        @Override\n        public String toString() {\n            return \"BlockWrite{block[\" + block.length +\n                    \"], \" + (isRaw ? \"raw\" : \"cbor\") +\n                    '}';\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            BlockWrite that = (BlockWrite) o;\n            return isRaw == that.isRaw && Objects.equals(writer, that.writer) && Objects.deepEquals(signature, that.signature) && Objects.deepEquals(block, that.block) && Objects.equals(progressMonitor, that.progressMonitor);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(writer, Arrays.hashCode(signature), Arrays.hashCode(block), isRaw, progressMonitor);\n        }\n    }\n\n    public static final class PointerWrite implements Cborable {\n        public final PublicKeyHash writer;\n        public final byte[] writerSignedChampRootCas;\n\n        public PointerWrite(PublicKeyHash writer, byte[] writerSignedChampRootCas) {\n            this.writer = writer;\n            this.writerSignedChampRootCas = writerSignedChampRootCas;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"w\", writer);\n            state.put(\"s\", new CborObject.CborByteArray(writerSignedChampRootCas));\n            return CborObject.CborMap.build(state);\n        }\n\n        public static PointerWrite fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor!\");\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n            PublicKeyHash writer = m.get(\"w\", PublicKeyHash::fromCbor);\n            byte[] signedUpdate = m.getByteArray(\"s\");\n            return new PointerWrite(writer, signedUpdate);\n        }\n\n        @Override\n        public String toString() {\n            return writer.toString();\n        }\n    }\n\n    private static Either<PointerWrite, BlockWrite> parseOperation(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for OpLog!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        if (m.containsKey(\"r\"))\n            return Either.b(BlockWrite.fromCbor(cbor));\n        return Either.a(PointerWrite.fromCbor(cbor));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"ops\", new CborObject.CborList(operations.stream()\n                .map(e -> e.map(PointerWrite::toCbor, BlockWrite::toCbor))\n                .collect(Collectors.toList())));\n        if (loginData != null) {\n            state.put(\"login\", loginData.left);\n            state.put(\"loginAuth\", new CborObject.CborByteArray(loginData.right));\n        }\n        mirrorBat.ifPresent(t -> {\n            state.put(\"b\", t.left);\n            state.put(\"a\", new CborObject.CborByteArray(t.right));\n        });\n        return CborObject.CborMap.build(state);\n    }\n\n    public static OpLog fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for OpLog!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        List<Either<PointerWrite, BlockWrite>> ops = m.getList(\"ops\", OpLog::parseOperation);\n        Optional<Pair<BatWithId, byte[]>> mirrorBat;\n        if (m.containsKey(\"b\")) {\n            mirrorBat = Optional.of(new Pair<>(m.get(\"b\", BatWithId::fromCbor), m.getByteArray(\"a\")));\n        } else\n            mirrorBat = Optional.empty();\n\n        if (m.containsKey(\"login\")) {\n            LoginData login = m.get(\"login\", LoginData::fromCbor);\n            byte[] loginAuth = m.getByteArray(\"loginAuth\");\n            return new OpLog(ops, new Pair<>(login, loginAuth), mirrorBat);\n        }\n        return new OpLog(ops, null, mirrorBat);\n    }\n\n    public static OpLog empty() {\n        return new OpLog(Collections.emptyList(), null, Optional.empty());\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/PkiCache.java",
    "content": "package peergos.shared.corenode;\n\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface PkiCache {\n\n    CompletableFuture<List<UserPublicKeyLink>> getChain(String username);\n\n    CompletableFuture<Boolean> setChain(String username, List<UserPublicKeyLink> chain);\n\n    CompletableFuture<String> getUsername(PublicKeyHash key);\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/Proxy.java",
    "content": "package peergos.shared.corenode;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class Proxy {\n    public static final Cid ZERO = Cid.decode(\"zdvgq1QytJnbSZiAGiduxkd7hnhdnpHqBSfYGiCyG1YQjLEij\");\n\n    public static final <V> CompletableFuture<V> redirectCall(CoreNode core,\n                                                              List<Cid> serverIds,\n                                                              PublicKeyHash ownerKey,\n                                                              Supplier<CompletableFuture<V>> direct,\n                                                              Function<Multihash, CompletableFuture<V>> proxied) {\n        List<Multihash> storageIds = core.getStorageProviders(ownerKey);\n        if (storageIds.isEmpty())\n            throw new IllegalStateException(\"Unable to find home server to send request to for \" + ownerKey);\n        Multihash target = storageIds.get(0);\n        if (serverIds.stream()\n                .map(Cid::bareMultihash)\n                .anyMatch(c -> c.equals(target.bareMultihash()))\n                || target.equals(ZERO)) { // signup error assume local user\n            // don't proxy\n            return direct.get();\n        } else {\n            return Futures.asyncExceptionally(() -> proxied.apply(target),\n                    t -> {\n                        // check if the server has rotated their identity\n                        Multihash newServerIdentity = core.getNextServerId(target.bareMultihash()).join().get();\n                        return proxied.apply(new Cid(1, Cid.Codec.LibP2pKey, newServerIdentity.type, newServerIdentity.getHash()));\n                    });\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/TofuCoreNode.java",
    "content": "package peergos.shared.corenode;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** The TOFU core node stores a local copy of all identity key mappings retrieved from the pki ina TOFU manner.\n *  The store is at /$username/.keystore in the user's Peergos space.\n */\npublic class TofuCoreNode implements CoreNode {\n\n    public static final String KEY_STORE_NAME = \".keystore\";\n    private final CoreNode source;\n    private final TofuKeyStore tofu;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private FileWrapper backingFile;\n\n    public TofuCoreNode(CoreNode source, TofuKeyStore tofu, FileWrapper backingFile, NetworkAccess network, Crypto crypto) {\n        // make sure we don't nest tofu core nodes, or their commits will clash\n        this.source = source instanceof TofuCoreNode ? ((TofuCoreNode) source).source : source;\n        this.tofu = tofu;\n        this.backingFile = backingFile;\n        this.network = network;\n        this.crypto = crypto;\n    }\n\n    /**\n     *\n     * @param username\n     * @param root\n     * @param network\n     * @param crypto\n     * @return The TOFU core node for this user\n     */\n    public static CompletableFuture<TofuCoreNode> load(String username, TrieNode root, NetworkAccess network, Crypto crypto) {\n        if (username == null)\n            throw new IllegalStateException(\"Cannot build a tofu keystore if not logged in!\");\n\n        return root.getByPath(username, crypto.hasher, network)\n                .thenApply(Optional::get)\n                .thenCompose(homeDir -> homeDir.getChild(KEY_STORE_NAME, crypto.hasher, network)\n                        .thenCompose(keystoreOpt -> {\n                            if (keystoreOpt.isEmpty()) {\n                                // initialize empty keystore\n                                TofuKeyStore store = new TofuKeyStore();\n                                byte[] raw = store.serialize();\n                                return homeDir.uploadAndReturnFile(KEY_STORE_NAME, AsyncReader.build(raw), raw.length,\n                                        true, homeDir.mirrorBatId(), network, crypto)\n                                        .thenApply(f -> new TofuCoreNode(network.coreNode, store, f, network, crypto));\n                            }\n\n                            return keystoreOpt.get().getInputStream(network, crypto, x -> {}).thenCompose(reader -> {\n                                byte[] storeData = new byte[(int) keystoreOpt.get().getSize()];\n                                return reader.readIntoArray(storeData, 0, storeData.length)\n                                        .thenApply(x -> new TofuCoreNode(network.coreNode,\n                                                TofuKeyStore.fromCbor(CborObject.fromByteArray(storeData)),\n                                                keystoreOpt.get(), network, crypto));\n                            });\n                        }));\n    }\n\n    private synchronized CompletableFuture<Boolean> commit() {\n        byte[] data = tofu.serialize();\n        AsyncReader.ArrayBacked dataReader = new AsyncReader.ArrayBacked(data);\n        return network.synchronizer.applyComplexUpdate(backingFile.owner(), backingFile.signingPair(),\n                        (s, committer) -> backingFile.overwriteFile(dataReader, data.length, network, crypto, x -> {}, s, committer))\n                .thenCompose(v -> backingFile.getUpdated(v, network))\n                .thenApply(f -> {\n                    this.backingFile = f;\n                    return true;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> updateUser(String username) {\n        return source.getChain(username)\n                .thenCompose(chain -> tofu.updateChain(username, chain, network.dhtClient)\n                        .thenCompose(changed -> {\n                            if (changed)\n                                return commit();\n                            return Futures.of(true);\n                        }));\n    }\n\n    @Override\n    public CompletableFuture<Optional<PublicKeyHash>> getPublicKeyHash(String username) {\n        Optional<PublicKeyHash> local = tofu.getPublicKey(username);\n        if (local.isPresent())\n            return CompletableFuture.completedFuture(local);\n        return source.getChain(username)\n                .thenCompose(chain -> {\n                        if(chain.isEmpty()) {\n                            return CompletableFuture.completedFuture(false);\n                        } else {\n                            return tofu.updateChain(username, chain, network.dhtClient)\n                                    .thenCompose(x -> commit());\n                        }\n                    }\n                ).thenApply(x -> x == false ? Optional.empty() : tofu.getPublicKey(username));\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash key) {\n        Optional<String> local = tofu.getUsername(key);\n        if (local.isPresent())\n            return CompletableFuture.completedFuture(local.get());\n        return source.getUsername(key)\n                .thenCompose(username -> source.getChain(username)\n                        .thenCompose(chain -> tofu.updateChain(username, chain, network.dhtClient)\n                                .thenCompose(x -> commit())\n                                .thenApply(x -> username)));\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        List<UserPublicKeyLink> localChain = tofu.getChain(username);\n        if (! localChain.isEmpty())\n            return CompletableFuture.completedFuture(localChain);\n        return source.getChain(username)\n                .thenCompose(chain -> tofu.updateChain(username, chain, network.dhtClient)\n                        .thenCompose(x -> commit())\n                        .thenApply(x -> tofu.getChain(username)));\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> signup(String username,\n                                                                  UserPublicKeyLink chain,\n                                                                  OpLog setupOperations,\n                                                                  ProofOfWork proof,\n                                                                  String token) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidSignup(String username, UserPublicKeyLink chain, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidSignup(String username, UserPublicKeyLink chain, OpLog setupOperations, byte[] signedSpaceRequest, ProofOfWork proof) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> startMirror(String username, BatWithId mirrorBat, byte[] auth, ProofOfWork proof) {\n        return source.startMirror(username, mirrorBat, auth, proof);\n    }\n\n    @Override\n    public CompletableFuture<Either<PaymentProperties, RequiredDifficulty>> startPaidMirror(String username, byte[] auth, ProofOfWork proof) {\n        return source.startPaidMirror(username, auth, proof);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> completePaidMirror(String username, BatWithId mirrorBat, byte[] signedSpaceRequest, ProofOfWork proof) {\n        return source.completePaidMirror(username, mirrorBat, signedSpaceRequest, proof);\n    }\n\n    @Override\n    public CompletableFuture<List<UserSnapshot>> getSnapshots(String prefix, BatWithId instanceBat, LocalDateTime lastLinkCountsUpdate) {\n        throw new IllegalStateException(\"Unsupported operation!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<RequiredDifficulty>> updateChain(String username,\n                                                                       List<UserPublicKeyLink> chain,\n                                                                       ProofOfWork proof,\n                                                                       String token) {\n        return source.updateChain(username, chain, proof, token).thenCompose(res -> {\n            if (res.isPresent())\n                return Futures.of(res);\n            return tofu.updateChain(username, chain, network.dhtClient)\n                    .thenCompose(x -> commit())\n                    .thenApply(x -> res);\n        });\n    }\n\n    @Override\n    public CompletableFuture<List<String>> getUsernames(String prefix) {\n        return source.getUsernames(prefix);\n    }\n\n    @Override\n    public CompletableFuture<UserSnapshot> migrateUser(String username,\n                                                       List<UserPublicKeyLink> newChain,\n                                                       Multihash currentStorageId,\n                                                       Optional<BatWithId> mirrorBat,\n                                                       LocalDateTime latestLinkCountUpdate,\n                                                       long currentUsage,\n                                                       boolean commitToPki) {\n        return source.migrateUser(username, newChain, currentStorageId, mirrorBat, latestLinkCountUpdate, currentUsage, commitToPki)\n                .thenCompose(res -> source.getChain(username)\n                        .thenCompose(chain -> tofu.updateChain(username, chain, network.dhtClient)\n                                .thenCompose(x -> commit())\n                                .thenApply(x -> res)));\n    }\n\n    @Override\n    public CompletableFuture<Optional<Multihash>> getNextServerId(Multihash serverId) {\n        return source.getNextServerId(serverId);\n    }\n\n    @Override\n    public void close() throws IOException {}\n}\n"
  },
  {
    "path": "src/peergos/shared/corenode/UserPublicKeyLink.java",
    "content": "package peergos.shared.corenode;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class UserPublicKeyLink implements Cborable {\n    public static final int MAX_SIZE = 2*1024*1024;\n    public static final int MAX_USERNAME_SIZE = CoreNode.MAX_USERNAME_SIZE;\n\n    public final PublicKeyHash owner;\n    public final Claim claim;\n    private final Optional<byte[]> keyChangeProof;\n\n    public UserPublicKeyLink(PublicKeyHash ownerHash, Claim claim, Optional<byte[]> keyChangeProof) {\n        this.owner = ownerHash;\n        this.claim = claim;\n        this.keyChangeProof = keyChangeProof;\n    }\n\n    public UserPublicKeyLink(PublicKeyHash owner, Claim claim) {\n        this(owner, claim, Optional.empty());\n    }\n\n    public Optional<byte[]> getKeyChangeProof() {\n        return keyChangeProof.map(x -> Arrays.copyOfRange(x, 0, x.length));\n    }\n\n    public UserPublicKeyLink withClaim(Claim claim) {\n        return new UserPublicKeyLink(owner, claim, keyChangeProof);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        UserPublicKeyLink that = (UserPublicKeyLink) o;\n\n        if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false;\n        if (claim != null ? !claim.equals(that.claim) : that.claim != null) return false;\n        return keyChangeProof.isPresent() ?\n                that.keyChangeProof.isPresent() &&\n                        Arrays.equals(keyChangeProof.get(), that.keyChangeProof.get()) :\n                ! that.keyChangeProof.isPresent();\n    }\n\n    @Override\n    public int hashCode() {\n        int result = owner != null ? owner.hashCode() : 0;\n        result = 31 * result + (claim != null ? claim.hashCode() : 0);\n        result = 31 * result + keyChangeProof.map(Arrays::hashCode).orElse(0);\n        return result;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> values = new TreeMap<>();\n        values.put(\"owner\", owner.toCbor());\n        values.put(\"claim\", claim.toCbor());\n        keyChangeProof.ifPresent(proof -> values.put(\"keychange\", new CborObject.CborByteArray(proof)));\n        return CborObject.CborMap.build(values);\n    }\n\n    public static UserPublicKeyLink fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for UserPublicKeyLink: \" + cbor);\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n\n        PublicKeyHash owner = PublicKeyHash.fromCbor(map.get(\"owner\"));\n        Claim claim  = Claim.fromCbor(map.get(\"claim\"));\n        Optional<byte[]> keyChangeProof = Optional.ofNullable(map.get(\"keychange\"))\n                .map(c -> ((CborObject.CborByteArray)c).value);\n        return new UserPublicKeyLink(owner, claim, keyChangeProof);\n    }\n\n    public static CompletableFuture<List<UserPublicKeyLink>> createChain(SigningPrivateKeyAndPublicHash oldUser,\n                                                                         SigningPrivateKeyAndPublicHash newUser,\n                                                                         String username,\n                                                                         LocalDate expiry,\n                                                                         List<Multihash> storageProviders) {\n        // sign new claim to username, with provided expiry\n        return Claim.build(username, newUser.secret, expiry, storageProviders).thenCompose(newClaim ->\n                // sign new key with old\n                oldUser.secret.signMessage(newUser.publicKeyHash.serialize()).thenCompose(link -> {\n\n                    // create link from old that never expires\n                    return Claim.build(username, oldUser.secret, LocalDate.MAX, Collections.emptyList())\n                            .thenApply(claim -> new UserPublicKeyLink(oldUser.publicKeyHash,\n                                    claim,\n                                    Optional.of(link))).thenApply(fromOld ->\n                                    Arrays.asList(fromOld, new UserPublicKeyLink(newUser.publicKeyHash, newClaim)));\n                }));\n    }\n\n    public static class Claim implements Cborable {\n        public final String username;\n        public final LocalDate expiry;\n        // a list of storage-node ids\n        public final List<Multihash> storageProviders;\n        private final byte[] signedContents;\n\n        public Claim(String username, LocalDate expiry, List<Multihash> storageProviders, byte[] signedContents) {\n            this.username = username;\n            this.expiry = expiry;\n            this.storageProviders = storageProviders;\n\n            this.signedContents = signedContents;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            return new CborObject.CborList(Arrays.asList(\n                    new CborObject.CborString(username),\n                    new CborObject.CborString(expiry.toString()),\n                    new CborObject.CborList(storageProviders.stream()\n                            .map(id -> new CborObject.CborByteArray(id.toBytes()))\n                            .collect(Collectors.toList())),\n                    new CborObject.CborByteArray(signedContents)));\n        }\n\n        public static Claim fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborList))\n                throw new IllegalStateException(\"Invalid cbor for Username claim: \" + cbor);\n            List<? extends Cborable> contents = ((CborObject.CborList) cbor).value;\n            String username = ((CborObject.CborString) contents.get(0)).value;\n            LocalDate expiry = LocalDate.parse(((CborObject.CborString) contents.get(1)).value);\n            List<Multihash> storageProviders = ((CborObject.CborList)contents.get(2))\n                    .value.stream()\n                    .map(x -> Cid.cast(((CborObject.CborByteArray) x).value))\n                    .collect(Collectors.toList());\n            byte[] signedContents = ((CborObject.CborByteArray) contents.get(3)).value;\n            return new Claim(username, expiry, storageProviders, signedContents);\n        }\n\n        public static CompletableFuture<Claim> build(String username, SecretSigningKey from, LocalDate expiryDate, List<Multihash> storageProviders) {\n            try {\n                ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                DataOutputStream dout = new DataOutputStream(bout);\n                Serialize.serialize(username, dout);\n                Serialize.serialize(expiryDate.toString(), dout);\n                dout.writeInt(storageProviders.size());\n                for (Multihash storageProvider : storageProviders) {\n                    Serialize.serialize(storageProvider.toBytes(), dout);\n                }\n                byte[] payload = bout.toByteArray();\n                return from.signMessage(payload)\n                        .thenApply(signed -> new Claim(username, expiryDate, storageProviders, signed));\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        public static CompletableFuture<Claim> deserialize(byte[] signedContents, PublicSigningKey signer) throws IOException {\n            return signer.unsignMessage(signedContents).thenApply(contents -> {\n                try {\n                    ByteArrayInputStream bin = new ByteArrayInputStream(contents);\n                    DataInputStream din = new DataInputStream(bin);\n                    String username = Serialize.deserializeString(din, MAX_USERNAME_SIZE);\n                    LocalDate expiry = LocalDate.parse(Serialize.deserializeString(din, 16));\n                    int nStorageProviders = din.readInt();\n                    List<Multihash> storageProviders = new ArrayList<>();\n                    for (int i = 0; i < nStorageProviders; i++) {\n                        storageProviders.add(Cid.cast(Serialize.deserializeByteArray(din, 100)));\n                    }\n                    return new Claim(username, expiry, storageProviders, signedContents);\n                } catch(IOException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            Claim that = (Claim) o;\n\n            if (username != null ? !username.equals(that.username) : that.username != null) return false;\n            if (expiry != null ? !expiry.equals(that.expiry) : that.expiry != null) return false;\n            if (! storageProviders.equals(that.storageProviders))\n                return false;\n            return Arrays.equals(signedContents, that.signedContents);\n        }\n\n        @Override\n        public int hashCode() {\n            int result = username != null ? username.hashCode() : 0;\n            result = 31 * result + (expiry != null ? expiry.hashCode() : 0);\n            result = 31 * result + storageProviders.hashCode();\n            result = 31 * result + Arrays.hashCode(signedContents);\n            return result;\n        }\n    }\n\n    public static CompletableFuture<List<UserPublicKeyLink>> createInitial(SigningPrivateKeyAndPublicHash signer,\n                                                        String username,\n                                                        LocalDate expiry,\n                                                        List<Multihash> storageProviders) {\n        return Claim.build(username, signer.secret, expiry, storageProviders)\n                .thenApply(newClaim -> Collections.singletonList(new UserPublicKeyLink(signer.publicKeyHash, newClaim)));\n    }\n\n    public static CompletableFuture<List<UserPublicKeyLink>> merge(List<UserPublicKeyLink> existing,\n                                                                   List<UserPublicKeyLink> updated,\n                                                                   ContentAddressedStorage ipfs) {\n        if (existing.size() == 0 || updated.equals(existing))\n            return CompletableFuture.completedFuture(updated);\n        int indexOfChange = 0;\n        for (int i=0; i < updated.size() && i < existing.size(); i++)\n            if (updated.get(i).equals(existing.get(i)))\n                indexOfChange++;\n            else\n                break;\n\n        List<UserPublicKeyLink> tail = updated.subList(indexOfChange, updated.size());\n\n        UserPublicKeyLink currentLast = existing.get(existing.size() - 1);\n        if (tail.get(0).claim.expiry.isBefore(currentLast.claim.expiry))\n            throw new IllegalStateException(\"New claim chain expiry before existing!\");\n\n        if (! tail.get(0).owner.equals(currentLast.owner)) {\n            if (tail.size() == 1)\n                return Futures.errored(new IllegalStateException(\"User already exists: Invalid key change attempt!\"));\n            else\n                return Futures.errored(new IllegalStateException(\"Different keys in merge chains intersection!\"));\n        }\n        Set<PublicKeyHash> previousKeys = existing.stream()\n                .limit(existing.size() - 1)\n                .map(k -> k.owner)\n                .collect(Collectors.toSet());\n        if (previousKeys.contains(tail.get(tail.size() - 1).owner)) {\n            // You cannot reuse a previous password\n            return Futures.errored(new IllegalStateException(\"You cannot reuse a previous password!\"));\n        }\n        List<UserPublicKeyLink> result = Stream.concat(\n                existing.subList(0, existing.size() - 1).stream(),\n                tail.stream())\n                .collect(Collectors.toList());\n        return validChain(result, tail.get(0).claim.username, ipfs)\n                .thenApply(valid -> {\n                    if (! valid)\n                        throw new IllegalStateException(\"Invalid key chain merge!\");\n                    return result;\n                });\n    }\n\n    public static CompletableFuture<Boolean> validChain(List<UserPublicKeyLink> chain, String username, ContentAddressedStorage ipfs) {\n        List<CompletableFuture<Boolean>> validities = new ArrayList<>();\n        for (int i=0; i < chain.size()-1; i++)\n            validities.add(validLink(chain.get(i), chain.get(i + 1).owner, username, ipfs));\n\n        // last claim must have storage providers, earlier ones must be empty\n        for (int i=0; i < chain.size(); i++) {\n            if (i == chain.size() - 1) {\n                if (chain.get(i).claim.storageProviders.size() != 1)\n                    throw new IllegalStateException(\"More than 1 storage providers in claim for \" + username);\n            } else {\n                if (! chain.get(i).claim.storageProviders.isEmpty())\n                    throw new IllegalStateException(\"Non empty storage providers in old claim for \" + username);\n            }\n        }\n        BiFunction<Boolean, CompletableFuture<Boolean>, CompletableFuture<Boolean>> composer = (b, valid) -> valid.thenApply(res -> res && b);\n        return Futures.reduceAll(validities,\n                true,\n                composer,\n                (a, b) -> a && b)\n                .thenCompose(valid -> {\n                    if (!valid)\n                        return CompletableFuture.completedFuture(false);\n                    UserPublicKeyLink last = chain.get(chain.size() - 1);\n                    return validClaim(last, username, ipfs);\n                });\n    }\n\n    static CompletableFuture<Boolean> validLink(UserPublicKeyLink from,\n                                                PublicKeyHash target,\n                                                String username,\n                                                ContentAddressedStorage ipfs) {\n        return validClaim(from, username, ipfs).thenCompose(valid -> {\n            if (!valid)\n                return CompletableFuture.completedFuture(false);\n\n            Optional<byte[]> keyChangeProof = from.getKeyChangeProof();\n            if (!keyChangeProof.isPresent())\n                return CompletableFuture.completedFuture(false);\n            return ipfs.getSigningKey(from.owner, from.owner).thenCompose(ownerKeyOpt -> {\n                if (!ownerKeyOpt.isPresent())\n                    return Futures.of(false);\n                return ownerKeyOpt.get().unsignMessage(keyChangeProof.get()).thenApply(unsigned -> {\n                    PublicKeyHash targetKey = PublicKeyHash.fromCbor(CborObject.fromByteArray(unsigned));\n                    if (!Arrays.equals(targetKey.serialize(), target.serialize()))\n                        return false;\n\n                    return true;\n                });\n            });\n        });\n    }\n\n    static CompletableFuture<Boolean> validClaim(UserPublicKeyLink from, String username, ContentAddressedStorage ipfs) {\n        if (username.contains(\" \") || username.contains(\"\\t\") || username.contains(\"\\n\"))\n            return CompletableFuture.completedFuture(false);\n        if (username.length() > MAX_USERNAME_SIZE)\n            return CompletableFuture.completedFuture(false);\n        if (!from.claim.username.equals(username))\n            return CompletableFuture.completedFuture(false);\n        if (from.claim.storageProviders.size() > 1)\n            return CompletableFuture.completedFuture(false);\n        return ipfs.getSigningKey(from.owner, from.owner).thenCompose(ownerKeyOpt -> {\n            if (!ownerKeyOpt.isPresent())\n                return Futures.of(false);\n            try {\n                return Claim.deserialize(from.claim.signedContents, ownerKeyOpt.get())\n                        .thenApply(decoded -> from.claim.equals(decoded));\n            } catch (Exception e) {\n                e.printStackTrace();\n                return Futures.of(false);\n            }\n        });\n    }\n\n    public static boolean isExpiredClaim(UserPublicKeyLink from) {\n        return from.claim.expiry.isBefore(LocalDate.now());\n    }\n}"
  },
  {
    "path": "src/peergos/shared/corenode/Usernames.java",
    "content": "package peergos.shared.corenode;\n\nimport jsinterop.annotations.*;\n\npublic class Usernames {\n\n    @JsProperty\n    public static final String REGEX = \"^[a-z0-9](?:[a-z0-9]|[-](?=[a-z0-9])){0,31}$\";\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/AsymmetricCipherText.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.symmetric.*;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.*;\n\npublic class AsymmetricCipherText implements Cborable {\n\n    private final byte[] cipherText;\n\n    public AsymmetricCipherText(byte[] cipherText) {\n        this.cipherText = cipherText;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborByteArray(cipherText);\n    }\n\n    public static AsymmetricCipherText fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborByteArray))\n            throw new IllegalStateException(\"Invalid cbor for asymmetric cipher text: \" + cbor);\n\n        return new AsymmetricCipherText(((CborObject.CborByteArray) cbor).value);\n    }\n\n    public static <T extends Cborable> CompletableFuture<AsymmetricCipherText> build(SecretBoxingKey from, PublicBoxingKey to, T secret) {\n        return to.encryptMessageFor(secret.serialize(), from)\n                .thenApply(cipherText -> new AsymmetricCipherText(cipherText));\n    }\n\n    public <T> CompletableFuture<T> decrypt(SecretBoxingKey to, PublicBoxingKey from, Function<Cborable, T> fromCbor) {\n        return to.decryptMessage(cipherText, from)\n                .thenApply(secret -> fromCbor.apply(CborObject.fromByteArray(secret)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/BoxingKeyPair.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.Crypto;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMPublicKey;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMSecretKey;\nimport peergos.shared.crypto.random.*;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class BoxingKeyPair implements Cborable\n{\n    public final PublicBoxingKey publicBoxingKey;\n    public final SecretBoxingKey secretBoxingKey;\n\n    public BoxingKeyPair(PublicBoxingKey publicBoxingKey, SecretBoxingKey secretBoxingKey) {\n        this.publicBoxingKey = publicBoxingKey;\n        this.secretBoxingKey = secretBoxingKey;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(\n                publicBoxingKey.toCbor(),\n                secretBoxingKey.toCbor()));\n    }\n\n    public static BoxingKeyPair fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Incorrect cbor for SigningKeyPair: \" + cbor);\n\n        List<? extends Cborable> values = ((CborObject.CborList) cbor).value;\n        PublicBoxingKey pub = PublicBoxingKey.fromCbor(values.get(0));\n        SecretBoxingKey secret = SecretBoxingKey.fromCbor(values.get(1));\n        return new BoxingKeyPair(pub, secret);\n    }\n\n    public static CompletableFuture<BoxingKeyPair> randomHybrid(Crypto crypto) {\n        BoxingKeyPair curve25519 = randomCurve25519(crypto.random, crypto.boxer);\n        return crypto.mlkem.generateKeyPair().thenApply(mlkemKeyPair -> {\n            HybridCurve25519MLKEMPublicKey hybridPublic = new HybridCurve25519MLKEMPublicKey(\n                    (Curve25519PublicKey) curve25519.publicBoxingKey, mlkemKeyPair.publicKey, crypto);\n            HybridCurve25519MLKEMSecretKey hybridSecret = new HybridCurve25519MLKEMSecretKey(\n                    (Curve25519SecretKey) curve25519.secretBoxingKey, mlkemKeyPair.secretKey, crypto);\n            return new BoxingKeyPair(hybridPublic, hybridSecret);\n        });\n    }\n\n    public static BoxingKeyPair randomCurve25519(SafeRandom random, Curve25519 boxer) {\n\n        byte[] secretBoxBytes = new byte[32];\n        byte[] publicBoxBytes = new byte[32];\n\n        random.randombytes(secretBoxBytes, 0, 32);\n\n        return randomCurve25519(secretBoxBytes, publicBoxBytes, boxer, random);\n    }\n\n    private static BoxingKeyPair randomCurve25519(byte[] secretBoxBytes, byte[] publicBoxBytes,\n                                                  Curve25519 boxer, SafeRandom random) {\n        boxer.crypto_box_keypair(publicBoxBytes, secretBoxBytes);\n\n        return new BoxingKeyPair(\n                new Curve25519PublicKey(publicBoxBytes, boxer, random),\n                new Curve25519SecretKey(secretBoxBytes, boxer));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/CipherText.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.util.ProgressConsumer;\n\nimport java.util.*;\nimport java.util.function.*;\n\npublic class CipherText implements Cborable {\n\n    private final byte[] nonce, cipherText;\n\n    public CipherText(byte[] nonce, byte[] cipherText) {\n        this.nonce = nonce;\n        this.cipherText = cipherText;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(\n                new CborObject.CborByteArray(nonce),\n                new CborObject.CborByteArray(cipherText)\n        ));\n    }\n\n    public static CipherText fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for cipher text: \" + cbor);\n\n        List<? extends Cborable> parts = ((CborObject.CborList) cbor).value;\n        byte[] nonce = ((CborObject.CborByteArray) parts.get(0)).value;\n        byte[] cipherText = ((CborObject.CborByteArray) parts.get(1)).value;\n        return new CipherText(nonce, cipherText);\n    }\n\n    public static <T extends Cborable> CipherText build(SymmetricKey from, T secret) {\n        byte[] nonce = from.createNonce();\n        byte[] cipherText = from.encrypt(secret.serialize(), nonce);\n        return new CipherText(nonce, cipherText);\n    }\n\n    public <T> T decrypt(SymmetricKey from, Function<CborObject, T> fromCbor) {\n        byte[] secret = from.decrypt(cipherText, nonce);\n        return fromCbor.apply(CborObject.fromByteArray(secret));\n    }\n\n    public <T> T decrypt(SymmetricKey from, Function<CborObject, T> fromCbor, ProgressConsumer<Long> monitor) {\n        byte[] secret = from.decrypt(cipherText, nonce);\n        monitor.accept((long)secret.length); //note: this is not accurate at all\n        return fromCbor.apply(CborObject.fromByteArray(secret));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/FragmentedPaddedCipherText.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** This class pads the secret up to a multiple of the given block size before encrypting and splits the ciphertext into\n * fragments which are referenced by merkle links in the serialization.\n *\n */\npublic class FragmentedPaddedCipherText implements Cborable {\n\n    private final byte[] nonce;\n    private final Optional<byte[]> header; // Present on all but legacy or inlined chunks, contains secretbox auth and cbor padding\n    private final List<Cid> cipherTextFragments;\n    private final List<BatWithId> bats;\n    private final Optional<byte[]> inlinedCipherText;\n\n    public FragmentedPaddedCipherText(byte[] nonce,\n                                      Optional<byte[]> header,\n                                      List<Cid> cipherTextFragments,\n                                      List<BatWithId> bats,\n                                      Optional<byte[]> inlinedCipherText) {\n        this.nonce = nonce;\n        this.header = header;\n        this.cipherTextFragments = cipherTextFragments;\n        this.bats = bats;\n        this.inlinedCipherText = inlinedCipherText;\n        if (inlinedCipherText.isPresent() && ! cipherTextFragments.isEmpty())\n            throw new IllegalStateException(\"Cannot have an inlined block and merkle linked blocks!\");\n    }\n\n    public boolean isInline() {\n        return inlinedCipherText.isPresent();\n    }\n\n    public List<Cid> getFragments() {\n        return cipherTextFragments;\n    }\n\n    public List<BatWithId> getBats() {\n        return bats;\n    }\n\n    public FragmentedPaddedCipherText withFragments(List<Cid> fragments) {\n        return new FragmentedPaddedCipherText(nonce, header, fragments, bats, inlinedCipherText);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"n\", new CborObject.CborByteArray(nonce));\n        header.ifPresent(h -> state.put(\"h\", new CborObject.CborByteArray(h)));\n        // The following change is because of a breaking change in ipfs to limit identity multihash size\n        if (cipherTextFragments.size() == 1 && cipherTextFragments.get(0).isIdentity() || inlinedCipherText.isPresent()) {\n            List<CborObject.CborByteArray> legacy = cipherTextFragments\n                    .stream()\n                    .map(h -> new CborObject.CborByteArray(h.getHash()))\n                    .collect(Collectors.toList());\n            List<CborObject.CborByteArray> value = inlinedCipherText\n                    .map(arr -> Collections.singletonList(new CborObject.CborByteArray(arr)))\n                    .orElse(legacy);\n            state.put(\"f\", new CborObject.CborList(value));\n        } else {\n            state.put(\"f\", new CborObject.CborList(cipherTextFragments\n                    .stream()\n                    .map(CborObject.CborMerkleLink::new)\n                    .collect(Collectors.toList())));\n            state.put(\"bats\", new CborObject.CborList(bats));\n        }\n        return CborObject.CborMap.build(state);\n    }\n\n    public static FragmentedPaddedCipherText fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for FragmentedPaddedCipherText: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        byte[] nonce =  m.getByteArray(\"n\");\n        Optional<byte[]> header = m.getOptionalByteArray(\"h\");\n        List<Cid> fragmentHashes = m.getList(\"f\").value\n                .stream()\n                .filter(c -> c instanceof CborObject.CborMerkleLink)\n                .map(c -> (Cid) ((CborObject.CborMerkleLink)c).target)\n                .collect(Collectors.toList());\n        Optional<byte[]> inlinedCipherText = m.getList(\"f\").value\n                .stream()\n                .filter(c -> c instanceof CborObject.CborByteArray)\n                .map(c -> ((CborObject.CborByteArray)c).value)\n                .findFirst();\n        List<BatWithId> bats = m.containsKey(\"bats\") ? m.getList(\"bats\", BatWithId::fromCbor) : Collections.emptyList();\n        return new FragmentedPaddedCipherText(nonce, header, fragmentHashes, bats, inlinedCipherText);\n    }\n\n    protected static byte[] pad(byte[] input, int excluded, int blockSize) {\n        int nBlocks = (input.length - excluded + blockSize - 1) / blockSize;\n        return Arrays.copyOfRange(input, 0, nBlocks * blockSize + excluded);\n    }\n\n    public static <T extends Cborable>\n    CompletableFuture<Pair<FragmentedPaddedCipherText, List<FragmentWithHash>>> build(SymmetricKey from,\n                                                                                      T secret,\n                                                                                      int paddingBlockSize,\n                                                                                      int maxFragmentSize,\n                                                                                      Optional<BatId> mirrorBat,\n                                                                                      SafeRandom random,\n                                                                                      Hasher hasher,\n                                                                                      boolean allowArrayCache) {\n        if (paddingBlockSize < 1)\n            throw new IllegalStateException(\"Invalid padding block size: \" + paddingBlockSize);\n        byte[] nonce = from.createNonce();\n        byte[] plainText = secret.serialize();\n        // input chunk size: 0,    5,    4090, 4096, 4097\n        // padded to:        4096, 4096, 4096, 4096, 8192\n        int maxCborOverhead = 6;\n        int serializationOverhead = plainText.length <= paddingBlockSize ? 0 : maxCborOverhead;\n        byte[] padded = pad(plainText, serializationOverhead, paddingBlockSize);\n        byte[] cipherText = from.encrypt(padded, nonce);\n\n        if (padded.length <= 4096 + maxCborOverhead) {\n            // inline small amounts of data (small files or directories)\n            FragmentedPaddedCipherText chunk = new FragmentedPaddedCipherText(nonce, Optional.empty(),\n                    Collections.emptyList(), Collections.emptyList(), Optional.of(cipherText));\n            return Futures.of(new Pair<>(chunk, Collections.emptyList()));\n        }\n\n        int headerSize = cipherText.length % paddingBlockSize;\n        Optional<byte[]> header = Optional.of(Arrays.copyOfRange(cipherText, 0, headerSize));\n        byte[][] split = split(cipherText, headerSize, maxFragmentSize, allowArrayCache);\n        int nBlocks = split.length;\n        List<Bat> blockBats = IntStream.range(0, nBlocks)\n                .mapToObj(i -> Bat.random(random))\n                .collect(Collectors.toList());\n\n        return Futures.combineAllInOrder(IntStream.range(0, nBlocks)\n                .mapToObj(i -> ArrayOps.concat(Bat.createRawBlockPrefix(blockBats.get(i), mirrorBat), split[i]))\n                .map(d -> hasher.hash(d, true).thenApply(h -> new FragmentWithHash(new Fragment(d), Optional.of(h))))\n                .collect(Collectors.toList()))\n                .thenCompose(frags -> {\n                    List<Cid> hashes = frags.stream()\n                            .map(f -> f.hash.get())\n                            .collect(Collectors.toList());\n                    return Futures.combineAllInOrder(blockBats.stream()\n                            .map(b -> b.calculateId(hasher).thenApply(id -> new BatWithId(b, id.id)))\n                            .collect(Collectors.toList()))\n                            .thenApply(batsAndIds -> new Pair<>(new FragmentedPaddedCipherText(nonce, header, hashes, batsAndIds, Optional.empty()), frags));\n                });\n    }\n\n    public <T> CompletableFuture<T> getAndDecrypt(PublicKeyHash owner,\n                                                  SymmetricKey from,\n                                                  Function<CborObject, T> fromCbor,\n                                                  Hasher h,\n                                                  NetworkAccess network,\n                                                  ProgressConsumer<Long> monitor) {\n        if (inlinedCipherText.isPresent()) {\n            if (header.isPresent())\n                return Futures.of(new CipherText(nonce, ArrayOps.concat(header.get(), inlinedCipherText.get())).decrypt(from, fromCbor, monitor));\n            return Futures.of(new CipherText(nonce, inlinedCipherText.get()).decrypt(from, fromCbor, monitor));\n        }\n        return network.dhtClient.downloadFragments(owner, cipherTextFragments, bats, h, monitor, 1.0)\n                .thenApply(frags -> frags.stream()\n                        .map(f -> new FragmentWithHash(new Fragment(Bat.removeRawBlockBatPrefix(f.fragment.data)), f.hash))\n                        .collect(Collectors.toList()))\n                .thenApply(fargs -> new CipherText(nonce, recombine(header, fargs)).decrypt(from, fromCbor));\n    }\n\n    private static byte[][] generateCache() {\n        return new byte[Chunk.MAX_SIZE/Fragment.MAX_LENGTH][Fragment.MAX_LENGTH];\n    }\n\n    private static ThreadLocal<byte[][]> arrayCache = ThreadLocal.withInitial(FragmentedPaddedCipherText::generateCache);\n\n    private static byte[][] split(byte[] input, int inputStartIndex, int maxFragmentSize, boolean allowCache) {\n        //calculate padding length to align to 256 bytes\n        int padding = 0;\n        int mod = (input.length - inputStartIndex) % 256;\n        if (mod != 0 || (input.length - inputStartIndex) == 0)\n            padding = 256 - mod;\n        //align to 256 bytes\n        int len = input.length - inputStartIndex + padding;\n\n        //calculate the number  of fragments\n        int nFragments =  len / maxFragmentSize;\n        if (len % maxFragmentSize > 0)\n            nFragments++;\n\n        byte[][] split = new  byte[nFragments][];\n\n        byte[][] cache = arrayCache.get();\n        int cacheIndex = 0;\n        for (int i= 0; i< nFragments; ++i) {\n            int start = inputStartIndex + maxFragmentSize * i;\n            int end = Math.min(input.length, start + maxFragmentSize);\n            int length = end - start;\n            boolean useCache = allowCache && length == Fragment.MAX_LENGTH;\n            byte[] b = useCache ? cache[cacheIndex++] : new byte[length];\n            System.arraycopy(input, start, b, 0, length);\n            split[i] = b;\n        }\n        return split;\n    }\n\n    private static byte[] recombine(Optional<byte[]> header, List<FragmentWithHash> encoded) {\n        int length = 0;\n\n        for (int i=0; i < encoded.size(); i++)\n            length += encoded.get(i).fragment.data.length;\n\n        int headerSize = header.map(h -> h.length).orElse(0);\n        byte[] output = new byte[headerSize + length];\n        header.ifPresent(h -> System.arraycopy(h, 0, output, 0, headerSize));\n        int pos = headerSize;\n        for (int i=0; i < encoded.size(); i++) {\n            byte[] b = encoded.get(i).fragment.data;\n            System.arraycopy(b, 0, output, pos, b.length);\n            pos += b.length;\n        }\n        return output;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/InvalidCipherTextException.java",
    "content": "package peergos.shared.crypto;\n\npublic class InvalidCipherTextException extends IllegalStateException {\n    public InvalidCipherTextException() {\n    }\n\n    public InvalidCipherTextException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/OwnerProof.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class OwnerProof implements Cborable {\n    public final PublicKeyHash ownedKey;\n    public final byte[] signedOwner;\n\n    public OwnerProof(PublicKeyHash ownedKey, byte[] signedOwner) {\n        this.ownedKey = ownedKey;\n        this.signedOwner = signedOwner;\n    }\n\n    public CompletableFuture<PublicKeyHash> getAndVerifyOwner(PublicKeyHash owner, ContentAddressedStorage ipfs) {\n        return ipfs.getSigningKey(owner, ownedKey)\n                .thenCompose(signer -> signer\n                        .map(k -> k.unsignMessage(signedOwner)\n                                .thenApply(unsigned -> PublicKeyHash.fromCbor(CborObject.fromByteArray(unsigned))))\n                        .orElseThrow(() -> new IllegalStateException(\"Couldn't retrieve owned key: \" + ownedKey)));\n    }\n\n    public static CompletableFuture<OwnerProof> build(SigningPrivateKeyAndPublicHash ownedKeypair, PublicKeyHash owner) {\n        return ownedKeypair.secret.signMessage(owner.serialize())\n                .thenApply(signed -> new OwnerProof(ownedKeypair.publicKeyHash, signed));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"o\", new CborObject.CborMerkleLink(ownedKey));\n        result.put(\"p\", new CborObject.CborByteArray(signedOwner));\n        return CborObject.CborMap.build(result);\n    }\n\n    public static OwnerProof fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for OwnerProof: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        PublicKeyHash ownedKey = m.get(\"o\", PublicKeyHash::fromCbor);\n        byte[] proof = m.get(\"p\", c -> (CborObject.CborByteArray) c).value;\n        return new OwnerProof(ownedKey, proof);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        OwnerProof that = (OwnerProof) o;\n        return Objects.equals(ownedKey, that.ownedKey) &&\n                Arrays.equals(signedOwner, that.signedOwner);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Objects.hash(ownedKey);\n        result = 31 * result + Arrays.hashCode(signedOwner);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/PaddedAsymmetricCipherText.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\n\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.*;\n\npublic class PaddedAsymmetricCipherText implements Cborable {\n\n    private final AsymmetricCipherText cipherText;\n\n    public PaddedAsymmetricCipherText(AsymmetricCipherText cipherText) {\n        this.cipherText = cipherText;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return cipherText.toCbor();\n    }\n\n    public static PaddedAsymmetricCipherText fromCbor(Cborable cbor) {\n        return new PaddedAsymmetricCipherText(AsymmetricCipherText.fromCbor(cbor));\n    }\n\n    public static <T extends Cborable> CompletableFuture<PaddedAsymmetricCipherText> build(SecretBoxingKey from, PublicBoxingKey to, T secret, int paddingBlockSize) {\n        return to.encryptMessageFor(PaddedCipherText.pad(secret.serialize(), paddingBlockSize), from)\n                .thenApply(cipherText -> new PaddedAsymmetricCipherText(new AsymmetricCipherText(cipherText)));\n    }\n\n    public <T> CompletableFuture<T> decrypt(SecretBoxingKey to, PublicBoxingKey from, Function<Cborable, T> fromCbor) {\n        return cipherText.decrypt(to, from, fromCbor);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/PaddedCipherText.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.symmetric.*;\n\nimport java.util.*;\nimport java.util.function.*;\n\n/** This class pads the secret up to a multiple of the given block size before encrypting.\n *\n * This hides the exact size of the secret.\n */\npublic class PaddedCipherText implements Cborable {\n\n    private final CipherText cipherText;\n\n    public PaddedCipherText(CipherText cipherText) {\n        this.cipherText = cipherText;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return cipherText.toCbor();\n    }\n\n    public static PaddedCipherText fromCbor(Cborable cbor) {\n        return new PaddedCipherText(CipherText.fromCbor(cbor));\n    }\n\n    protected static byte[] pad(byte[] input, int blockSize) {\n        int nBlocks = (input.length + blockSize - 1) / blockSize;\n        return Arrays.copyOfRange(input, 0, nBlocks * blockSize);\n    }\n\n    public static <T extends Cborable> PaddedCipherText build(SymmetricKey from, T secret, int paddingBlockSize) {\n        if (paddingBlockSize < 1)\n            throw new IllegalStateException(\"Invalid padding block size: \" + paddingBlockSize);\n        byte[] nonce = from.createNonce();\n        byte[] cipherText = from.encrypt(pad(secret.serialize(), paddingBlockSize), nonce);\n        return new PaddedCipherText(new CipherText(nonce, cipherText));\n    }\n\n    public <T> T decrypt(SymmetricKey from, Function<CborObject, T> fromCbor) {\n        return cipherText.decrypt(from, fromCbor);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/ProofOfWork.java",
    "content": "package peergos.shared.crypto;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\n\npublic class ProofOfWork implements Cborable {\n    public final static int PREFIX_BYTES = 8;\n    public final static int MIN_DIFFICULTY = 0;\n    public final static int DEFAULT_DIFFICULTY = 11;\n    public final static int MAX_DIFFICULTY = 256;\n\n    public final byte[] prefix;\n    public final Multihash.Type type;\n\n    public ProofOfWork(byte[] prefix, Multihash.Type type) {\n        this.prefix = prefix;\n        this.type = type;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> values = new TreeMap<>();\n        values.put(\"prefix\", new CborObject.CborByteArray(prefix));\n        values.put(\"type\", new CborObject.CborLong(type.index));\n        return CborObject.CborMap.build(values);\n    }\n\n    public static ProofOfWork fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for ProofOfWork: \" + cbor);\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        byte[] prefix = map.getByteArray(\"prefix\");\n        int index = (int) map.getLong(\"type\");\n        return new ProofOfWork(prefix, Multihash.Type.lookup(index));\n    }\n\n    @JsMethod\n    public static ProofOfWork buildSha256(byte[] prefix, byte[] data) {\n        return new ProofOfWork(prefix, Multihash.Type.sha2_256);\n    }\n\n    /** This tests whether the calculated hash has at least *difficulty* leading zero bits\n     *\n     * @param difficulty\n     * @param hash\n     * @return\n     */\n    @JsMethod\n    public static boolean satisfiesDifficulty(int difficulty, byte[] hash) {\n        for (int i=0; i < difficulty; i+= 8) {\n            if (i <= difficulty - 8) {\n                if (hash[i/8] != 0)\n                    return false;\n            } else {\n                int raw = hash[i / 8] & 0xFF;\n                return (0xFF & (raw << (8 - difficulty + i))) == 0;\n            }\n        }\n        return true;\n    }\n\n    public static ProofOfWork empty() {\n        return new ProofOfWork(new byte[0], Multihash.Type.sha2_256);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/RequiredDifficulty.java",
    "content": "package peergos.shared.crypto;\n\npublic class RequiredDifficulty {\n    public final int requiredDifficulty;\n\n    public RequiredDifficulty(int requiredDifficulty) {\n        this.requiredDifficulty = requiredDifficulty;\n    }\n\n    @Override\n    public String toString() {\n        return \"Difficulty: \" + requiredDifficulty;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/SigningKeyPair.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.asymmetric.SecretSigningKey;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.io.ipfs.bases.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class SigningKeyPair implements Cborable\n{\n    public final PublicSigningKey publicSigningKey;\n    public final SecretSigningKey secretSigningKey;\n\n    public SigningKeyPair(PublicSigningKey publicSigningKey, SecretSigningKey secretSigningKey)\n    {\n        this.publicSigningKey = publicSigningKey;\n        this.secretSigningKey = secretSigningKey;\n    }\n\n    public CompletableFuture<byte[]> signMessage(byte[] message)\n    {\n        return secretSigningKey.signMessage(message);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        SigningKeyPair that = (SigningKeyPair) o;\n\n        if (publicSigningKey != null ? !publicSigningKey.equals(that.publicSigningKey) : that.publicSigningKey != null)\n            return false;\n        return secretSigningKey != null ? secretSigningKey.equals(that.secretSigningKey) : that.secretSigningKey == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = publicSigningKey != null ? publicSigningKey.hashCode() : 0;\n        result = 31 * result + (secretSigningKey != null ? secretSigningKey.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(\n                publicSigningKey.toCbor(),\n                secretSigningKey.toCbor()));\n    }\n\n    @Override\n    public String toString() {\n        return Multibase.encode(Multibase.Base.Base58BTC, serialize());\n    }\n\n    public static SigningKeyPair fromString(String multibaseEncoded) {\n        return fromByteArray(Multibase.decode(multibaseEncoded));\n    }\n\n    public static SigningKeyPair fromByteArray(byte[] raw) {\n        return fromCbor(CborObject.fromByteArray(raw));\n    }\n\n    public static SigningKeyPair fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Incorrect cbor for SigningKeyPair: \" + cbor);\n\n        List<? extends Cborable> values = ((CborObject.CborList) cbor).value;\n        PublicSigningKey pub = PublicSigningKey.fromCbor(values.get(0));\n        SecretSigningKey secret = SecretSigningKey.fromCbor(values.get(1));\n        return new SigningKeyPair(pub, secret);\n    }\n\n    public static SigningKeyPair random(SafeRandom random, Ed25519 signer) {\n\n        byte[] secretSignBytes = new byte[64];\n        byte[] publicSignBytes = new byte[32];\n\n        random.randombytes(secretSignBytes, 0, 32);\n\n        return random(secretSignBytes, publicSignBytes, signer);\n    }\n\n    private static SigningKeyPair random(byte[] secretSignBytes, byte[] publicSignBytes, Ed25519 signer) {\n        signer.crypto_sign_keypair(publicSignBytes, secretSignBytes);\n\n        return new SigningKeyPair(\n                new Ed25519PublicKey(publicSignBytes, signer),\n                new Ed25519SecretKey(secretSignBytes, signer));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/SigningPrivateKeyAndPublicHash.java",
    "content": "package peergos.shared.crypto;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.*;\n\n@JsType\npublic class SigningPrivateKeyAndPublicHash implements Cborable {\n    public final PublicKeyHash publicKeyHash;\n    public final SecretSigningKey secret;\n\n    public SigningPrivateKeyAndPublicHash(PublicKeyHash publicKeyHash, SecretSigningKey secret) {\n        this.publicKeyHash = publicKeyHash;\n        this.secret = secret;\n    }\n\n    @Override\n    @SuppressWarnings(\"unusable-by-js\")\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"p\", publicKeyHash.toCbor());\n        result.put(\"s\", secret.toCbor());\n        return CborObject.CborMap.build(result);\n    }\n\n    @SuppressWarnings(\"unusable-by-js\")\n    public static SigningPrivateKeyAndPublicHash fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for SigningPrivateKeyAndPublicHash: \" + cbor);\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        PublicKeyHash publicKeyHash = PublicKeyHash.fromCbor(map.get(\"p\"));\n        SecretSigningKey secretKey = SecretSigningKey.fromCbor(map.get(\"s\"));\n        return new SigningPrivateKeyAndPublicHash(publicKeyHash, secretKey);\n    }\n\n    @Override\n    public String toString() {\n        return publicKeyHash.toString();\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(publicKeyHash, secret);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        SigningPrivateKeyAndPublicHash that = (SigningPrivateKeyAndPublicHash) o;\n        return Objects.equals(publicKeyHash, that.publicKeyHash) && Objects.equals(secret, that.secret);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/SourcedAsymmetricCipherText.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.*;\n\npublic class SourcedAsymmetricCipherText implements Cborable {\n    public final PublicBoxingKey from;\n    public final AsymmetricCipherText cipherText;\n\n    public SourcedAsymmetricCipherText(PublicBoxingKey from, AsymmetricCipherText cipherText) {\n        this.from = from;\n        this.cipherText = cipherText;\n    }\n\n    public static <T extends Cborable> CompletableFuture<SourcedAsymmetricCipherText> build(\n            BoxingKeyPair from,\n            PublicBoxingKey to,\n            T secret) {\n        return to.encryptMessageFor(secret.serialize(), from.secretBoxingKey)\n                .thenApply(cipherText -> new SourcedAsymmetricCipherText(from.publicBoxingKey, new AsymmetricCipherText(cipherText)));\n    }\n\n    public <T> CompletableFuture<T> decrypt(SecretBoxingKey to, Function<Cborable, T> fromCbor) {\n        return cipherText.decrypt(to, from, fromCbor);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"k\", from);\n        result.put(\"c\", cipherText);\n        return CborObject.CborMap.build(result);\n    }\n\n    public static SourcedAsymmetricCipherText fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for SourcedAsymmetricCipherText: \"+  cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        PublicBoxingKey source = m.get(\"k\", PublicBoxingKey::fromCbor);\n        AsymmetricCipherText cipherText = m.get(\"c\", AsymmetricCipherText::fromCbor);\n        return new SourcedAsymmetricCipherText(source, cipherText);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/SymmetricLink.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\n\n/** A symmetric link is a link from one symmetric key to another, as defined in cryptree.\n *\n * This means the target key is encrypted with the source key.\n *\n */\npublic class SymmetricLink implements Cborable\n{\n    private final CipherText cipherText;\n\n    public SymmetricLink(CipherText cipherText) {\n        this.cipherText = cipherText;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return cipherText.toCbor();\n    }\n\n    public SymmetricKey target(SymmetricKey from) {\n        return cipherText.decrypt(from, SymmetricKey::fromCbor);\n    }\n\n    public static SymmetricLink fromCbor(Cborable cbor) {\n        return new SymmetricLink(CipherText.fromCbor(cbor));\n    }\n\n    public static SymmetricLink fromPair(SymmetricKey from, SymmetricKey to) {\n        return new SymmetricLink(CipherText.build(from, to));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/SymmetricLinkToSigner.java",
    "content": "package peergos.shared.crypto;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.symmetric.*;\n\n/** A symmetric link is a link from a symmetric key to a signing keypair, as defined in cryptree.\n *\n * This means the target keys are encrypted with the source key.\n *\n */\npublic class SymmetricLinkToSigner implements Cborable\n{\n    private final CipherText cipherText;\n\n    public SymmetricLinkToSigner(CipherText cipherText) {\n        this.cipherText = cipherText;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return cipherText.toCbor();\n    }\n\n    public SigningPrivateKeyAndPublicHash target(SymmetricKey from) {\n        return cipherText.decrypt(from, SigningPrivateKeyAndPublicHash::fromCbor);\n    }\n\n    public static SymmetricLinkToSigner fromCbor(Cborable cbor) {\n        return new SymmetricLinkToSigner(CipherText.fromCbor(cbor));\n    }\n\n    public static SymmetricLinkToSigner fromPair(SymmetricKey from, SigningPrivateKeyAndPublicHash to) {\n        return new SymmetricLinkToSigner(CipherText.build(from, to));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/PublicBoxingKey.java",
    "content": "package peergos.shared.crypto.asymmetric;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.Crypto;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMPublicKey;\nimport peergos.shared.crypto.asymmetric.mlkem.Mlkem;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\n\npublic interface PublicBoxingKey extends Cborable {\n    Map<Integer, Type> byValue = new HashMap<>();\n    enum Type {\n        Curve25519(0x1),\n        HybridCurve25519MLKEM(0x2);\n\n        public final int value;\n        Type(int value)\n        {\n            this.value = value;\n            byValue.put(value, this);\n        }\n\n        public static Type byValue(int val) {\n            if (!byValue.containsKey(val))\n                throw new IllegalStateException(\"Unknown public boxing key type: \" + ArrayOps.byteToHex(val));\n            return byValue.get(val);\n        }\n    }\n\n    Map<Type, Curve25519> PROVIDERS = new HashMap<>();\n    Map<Type, Crypto> MLKEM_PROVIDERS = new HashMap<>();\n\n    static void addProvider(Type t, Curve25519 provider) {\n        PROVIDERS.put(t, provider);\n    }\n\n    static void addMlkemProvider(Type t, Crypto provider) {\n        MLKEM_PROVIDERS.put(t, provider);\n    }\n\n    Map<Type, SafeRandom> RNG_PROVIDERS = new HashMap<>();\n\n    static void setRng(Type t, SafeRandom rng) {\n        RNG_PROVIDERS.put(t, rng);\n    }\n\n    Type type();\n\n    @JsMethod\n    byte[] getPublicBoxingKey();\n\n    @JsMethod\n    CompletableFuture<byte[]> encryptMessageFor(byte[] input, SecretBoxingKey from);\n\n    @JsMethod\n    byte[] createNonce();\n\n    @JsMethod\n    static PublicBoxingKey fromByteArray(byte[] raw) {\n        return fromCbor(CborObject.fromByteArray(raw));\n    }\n\n    static PublicBoxingKey fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for PublicBoxingKey! \" + cbor);\n        CborObject.CborLong type = (CborObject.CborLong) ((CborObject.CborList) cbor).value.get(0);\n        Type t = Type.byValue((int) type.value);\n        switch (t) {\n            case Curve25519:\n                return Curve25519PublicKey.fromCbor(cbor, PROVIDERS.get(t), RNG_PROVIDERS.get(t));\n            case HybridCurve25519MLKEM:\n                return HybridCurve25519MLKEMPublicKey.fromCbor(cbor, MLKEM_PROVIDERS.get(t));\n            default:\n                throw new IllegalStateException(\"Unknown Public Boxing Key type: \" + t.name());\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/PublicSigningKey.java",
    "content": "package peergos.shared.crypto.asymmetric;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface PublicSigningKey extends Cborable {\n    int MAX_SIZE = 10*1024*1024;\n\n    Map<Integer, Type> byValue = new HashMap<>();\n    @JsType\n    enum Type {\n        Ed25519(0x1);\n\n        public final int value;\n\n        Type(int value) {\n            this.value = value;\n            byValue.put(value, this);\n        }\n\n        public static Type byValue(int val) {\n            if (!byValue.containsKey(val))\n                throw new IllegalStateException(\"Unknown public signing key type: \" + ArrayOps.byteToHex(val));\n            return byValue.get(val);\n        }\n    }\n\n    Map<Type, Ed25519> PROVIDERS = new HashMap<>();\n\n    static void addProvider(Type t, Ed25519 provider) {\n        PROVIDERS.put(t, provider);\n    }\n\n    Type type();\n\n    @JsMethod\n    CompletableFuture<byte[]> unsignMessage(byte[] signed);\n\n    static PublicSigningKey fromString(String b64) {\n        return fromByteArray(Base64.getDecoder().decode(b64));\n    }\n\n    @JsMethod\n    static PublicSigningKey fromByteArray(byte[] raw) {\n        return fromCbor(CborObject.fromByteArray(raw));\n    }\n\n    static PublicSigningKey fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for PublicSigningKey! \" + cbor);\n        CborObject.CborLong type = (CborObject.CborLong) ((CborObject.CborList) cbor).value.get(0);\n        Type t = Type.byValue((int) type.value);\n        switch (t) {\n            case Ed25519:\n                return Ed25519PublicKey.fromCbor(cbor, PROVIDERS.get(t));\n            default:\n                throw new IllegalStateException(\"Unknown Public Signing Key type: \" + t.name());\n        }\n    }\n\n    static PublicSigningKey createNull() {\n        return new Ed25519PublicKey(new byte[32], PublicSigningKey.PROVIDERS.get(PublicSigningKey.Type.Ed25519));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/SecretBoxingKey.java",
    "content": "package peergos.shared.crypto.asymmetric;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.curve25519.Curve25519SecretKey;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMSecretKey;\n\nimport java.io.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic interface SecretBoxingKey extends Cborable {\n\n    PublicBoxingKey.Type type();\n\n    @JsMethod\n    byte[] getSecretBoxingKey();\n\n    @JsMethod\n    CompletableFuture<byte[]> decryptMessage(byte[] cipher, PublicBoxingKey from);\n\n    static SecretBoxingKey fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for SecretBoxingKey! \" + cbor);\n        CborObject.CborLong type = (CborObject.CborLong) ((CborObject.CborList) cbor).value.get(0);\n        PublicBoxingKey.Type t = PublicBoxingKey.Type.byValue((int) type.value);\n        switch (t) {\n            case Curve25519:\n                return Curve25519SecretKey.fromCbor(cbor, PublicBoxingKey.PROVIDERS.get(PublicBoxingKey.Type.Curve25519));\n            case HybridCurve25519MLKEM:\n                return HybridCurve25519MLKEMSecretKey.fromCbor(cbor, PublicBoxingKey.MLKEM_PROVIDERS.get(PublicBoxingKey.Type.HybridCurve25519MLKEM));\n            default: throw new IllegalStateException(\"Unknown Secret Boxing Key type: \"+t.name());\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/SecretSigningKey.java",
    "content": "package peergos.shared.crypto.asymmetric;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\n\nimport java.util.concurrent.*;\n\n@JsType\npublic interface SecretSigningKey extends Cborable {\n\n    PublicSigningKey.Type type();\n\n    /**\n     *\n     * @param message\n     * @return The signature + message\n     */\n    CompletableFuture<byte[]> signMessage(byte[] message);\n\n    /**\n     *\n     * @param message\n     * @return Only the signature, excluding the original message\n     */\n    CompletableFuture<byte[]> signatureOnly(byte[] message);\n\n    @SuppressWarnings(\"unusable-by-js\")\n    static SecretSigningKey fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for SecretSigningKey! \" + cbor);\n        CborObject.CborLong type = (CborObject.CborLong) ((CborObject.CborList) cbor).value.get(0);\n        PublicSigningKey.Type t = PublicSigningKey.Type.byValue((int) type.value);\n        switch (t) {\n            case Ed25519:\n                return Ed25519SecretKey.fromCbor(cbor, PublicSigningKey.PROVIDERS.get(t));\n            default: throw new IllegalStateException(\"Unknown Secret Signing Key type: \"+t.name());\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/curve25519/Curve25519.java",
    "content": "package peergos.shared.crypto.asymmetric.curve25519;\n\nimport peergos.shared.crypto.random.JSNaCl;\n\npublic interface Curve25519 {\n\n    byte[] crypto_box_open(byte[] cipher, byte[] nonce, byte[] theirPublicBoxingKey, byte[] secretBoxingKey);\n\n    byte[] crypto_box(byte[] message, byte[] nonce, byte[] theirPublicBoxingKey, byte[] ourSecretBoxingKey);\n\n    void crypto_box_keypair(byte[] pk, byte[] sk);\n\n    class Javascript implements Curve25519 {\n\n        JSNaCl scriptJS = new JSNaCl();\n\n        @Override\n        public byte[] crypto_box_open(byte[] cipher, byte[] nonce, byte[] theirPublicBoxingKey, byte[] secretBoxingKey) {\n            return scriptJS.crypto_box_open(cipher, nonce, theirPublicBoxingKey, secretBoxingKey);\n        }\n\n        @Override\n        public byte[] crypto_box(byte[] message, byte[] nonce, byte[] theirPublicBoxingKey, byte[] ourSecretBoxingKey) {\n            return scriptJS.crypto_box(message, nonce, theirPublicBoxingKey, ourSecretBoxingKey);\n        }\n\n        @Override\n        public void crypto_box_keypair(byte[] pk, byte[] sk) {\n            byte[] bytes = scriptJS.crypto_box_keypair(pk, sk);\n            for(int i=0; i < bytes.length; i++) {\n                pk[i] = bytes[i];\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/curve25519/Curve25519PublicKey.java",
    "content": "package peergos.shared.crypto.asymmetric.curve25519;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.PublicBoxingKey;\nimport peergos.shared.crypto.asymmetric.SecretBoxingKey;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.util.ArrayOps;\nimport peergos.shared.util.Futures;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class Curve25519PublicKey implements PublicBoxingKey {\n    public static final int BOX_NONCE_BYTES = 24;\n    private final byte[] publicKey;\n    private final Curve25519 implementation;\n    private final SafeRandom random;\n\n    public Curve25519PublicKey(byte[] publicKey, Curve25519 provider, SafeRandom random) {\n        if (publicKey.length != 32)\n            throw new IllegalArgumentException(\"Incorrect curve25519 public key length! \" + publicKey.length);\n        this.publicKey = publicKey;\n        this.implementation = provider;\n        this.random = random;\n    }\n\n    public PublicBoxingKey.Type type() {\n        return Type.Curve25519;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Curve25519PublicKey that = (Curve25519PublicKey) o;\n\n        return Arrays.equals(publicKey, that.publicKey);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(publicKey);\n    }\n\n    public byte[] getPublicBoxingKey() {\n        return Arrays.copyOfRange(publicKey, 0, publicKey.length);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> encryptMessageFor(byte[] input, SecretBoxingKey from) {\n        byte[] nonce = createNonce();\n        return Futures.of(ArrayOps.concat(implementation.crypto_box(input, nonce, publicKey, from.getSecretBoxingKey()), nonce));\n    }\n\n    public byte[] createNonce() {\n        byte[] nonce = new byte[BOX_NONCE_BYTES];\n        random.randombytes(nonce, 0, nonce.length);\n        return nonce;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(new CborObject.CborLong(type().value), new CborObject.CborByteArray(publicKey)));\n    }\n\n    public static Curve25519PublicKey fromCbor(Cborable cbor, Curve25519 provider, SafeRandom random) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for PublicBoxingKey! \" + cbor);\n        CborObject.CborByteArray key = (CborObject.CborByteArray) ((CborObject.CborList) cbor).value.get(1);\n        return new Curve25519PublicKey(key.value, provider, random);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/curve25519/Curve25519SecretKey.java",
    "content": "package peergos.shared.crypto.asymmetric.curve25519;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.PublicBoxingKey;\nimport peergos.shared.crypto.asymmetric.SecretBoxingKey;\nimport peergos.shared.util.Futures;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class Curve25519SecretKey implements SecretBoxingKey {\n\n    private final byte[] secretKey;\n    private final Curve25519 implementation;\n\n    public Curve25519SecretKey(byte[] secretKey, Curve25519 provider) {\n        this.secretKey = secretKey;\n        this.implementation = provider;\n    }\n\n    public PublicBoxingKey.Type type() {\n        return PublicBoxingKey.Type.Curve25519;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Curve25519SecretKey that = (Curve25519SecretKey) o;\n\n        return Arrays.equals(secretKey, that.secretKey);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(secretKey);\n    }\n\n    @Override\n    public byte[] getSecretBoxingKey() {\n        return Arrays.copyOfRange(secretKey, 0, secretKey.length);\n    }\n\n    public CompletableFuture<byte[]> decryptMessage(byte[] cipher, PublicBoxingKey from) {\n        byte[] nonce = Arrays.copyOfRange(cipher, cipher.length - Curve25519PublicKey.BOX_NONCE_BYTES, cipher.length);\n        cipher = Arrays.copyOfRange(cipher, 0, cipher.length - Curve25519PublicKey.BOX_NONCE_BYTES);\n        return Futures.of(implementation.crypto_box_open(cipher, nonce, from.getPublicBoxingKey(), secretKey));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(new CborObject.CborLong(type().value), new CborObject.CborByteArray(secretKey)));\n    }\n\n    public static Curve25519SecretKey fromCbor(Cborable cbor, Curve25519 provider) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for SecretBoxingKey! \" + cbor);\n        CborObject.CborByteArray key = (CborObject.CborByteArray) ((CborObject.CborList) cbor).value.get(1);\n        return new Curve25519SecretKey(key.value, provider);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/curve25519/Ed25519.java",
    "content": "package peergos.shared.crypto.asymmetric.curve25519;\n\nimport peergos.shared.crypto.random.JSNaCl;\n\nimport java.util.concurrent.*;\n\npublic interface Ed25519 {\n\n    CompletableFuture<byte[]> crypto_sign_open(byte[] signed, byte[] publicSigningKey);\n\n    CompletableFuture<byte[]> crypto_sign(byte[] message, byte[] secretSigningKey);\n\n    void crypto_sign_keypair(byte[] pk, byte[] sk);\n\n    class Javascript implements Ed25519 {\n        JSNaCl scriptJS = new JSNaCl();\n\n        @Override\n        public CompletableFuture<byte[]> crypto_sign_open(byte[] signed, byte[] publicSigningKey) {\n            return scriptJS.crypto_sign_open(signed, publicSigningKey);\n        }\n\n        @Override\n        public CompletableFuture<byte[]> crypto_sign(byte[] message, byte[] secretSigningKey) {\n            return scriptJS.crypto_sign(message, secretSigningKey);\n        }\n\n        @Override\n        public void crypto_sign_keypair(byte[] pk, byte[] sk) {\n            byte[][] bytes = scriptJS.crypto_sign_keypair(pk, sk);\n            for(int i=0; i < bytes[0].length; i++) {\n                pk[i] = bytes[0][i];\n            }\n            for(int i=0; i < bytes[1].length; i++) {\n                sk[i] = bytes[1][i];\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/curve25519/Ed25519PublicKey.java",
    "content": "package peergos.shared.crypto.asymmetric.curve25519;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class Ed25519PublicKey implements PublicSigningKey {\n    public static final int SIGNATURE_SIZE_BYTES = 64;\n\n    private final byte[] publicKey;\n    private final Ed25519 implementation;\n\n    public Ed25519PublicKey(byte[] publicKey, Ed25519 provider) {\n        this.publicKey = publicKey;\n        this.implementation = provider;\n    }\n\n    public PublicSigningKey.Type type() {\n        return PublicSigningKey.Type.Ed25519;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Ed25519PublicKey that = (Ed25519PublicKey) o;\n\n        return Arrays.equals(publicKey, that.publicKey);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(publicKey);\n    }\n\n    @Override\n    public String toString() {\n        return Base64.getEncoder().encodeToString(serialize());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(new CborObject.CborLong(type().value), new CborObject.CborByteArray(publicKey)));\n    }\n\n    public CompletableFuture<byte[]> unsignMessage(byte[] signed) {\n        if (implementation == null)\n            throw new IllegalStateException(\"Uninitialized crypto-implementation: call peergos.shared.Crypto::init\");\n        return implementation.crypto_sign_open(signed, publicKey);\n    }\n\n    public static Ed25519PublicKey fromCbor(Cborable cbor, Ed25519 provider) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for Ed25519 public key! \" + cbor);\n        CborObject.CborByteArray key = (CborObject.CborByteArray) ((CborObject.CborList) cbor).value.get(1);\n        return new Ed25519PublicKey(key.value, provider);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/curve25519/Ed25519SecretKey.java",
    "content": "package peergos.shared.crypto.asymmetric.curve25519;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.crypto.asymmetric.SecretSigningKey;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class Ed25519SecretKey implements SecretSigningKey {\n\n    private final byte[] secretKey;\n    private final Ed25519 implementation;\n\n    public Ed25519SecretKey(byte[] secretKey, Ed25519 provider) {\n        this.secretKey = secretKey;\n        this.implementation = provider;\n    }\n\n    public PublicSigningKey.Type type() {\n        return PublicSigningKey.Type.Ed25519;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Ed25519SecretKey that = (Ed25519SecretKey) o;\n\n        return Arrays.equals(secretKey, that.secretKey);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(secretKey);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(new CborObject.CborLong(type().value), new CborObject.CborByteArray(secretKey)));\n    }\n\n    @Override\n    public CompletableFuture<byte[]> signMessage(byte[] message) {\n        return implementation.crypto_sign(message, secretKey);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> signatureOnly(byte[] message) {\n        return implementation.crypto_sign(message, secretKey)\n                .thenApply(res -> Arrays.copyOf(res, Ed25519PublicKey.SIGNATURE_SIZE_BYTES));\n    }\n\n    public static SecretSigningKey fromCbor(Cborable cbor, Ed25519 provider) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for Ed25519 secret key! \" + cbor);\n        CborObject.CborByteArray key = (CborObject.CborByteArray) ((CborObject.CborList) cbor).value.get(1);\n        return new Ed25519SecretKey(key.value, provider);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/mlkem/HybridCipherText.java",
    "content": "package peergos.shared.crypto.asymmetric.mlkem;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.time.ZoneOffset;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class HybridCipherText implements Cborable {\n    public final byte[] curve25519Ciphertext, mlkemCipherText, encryptedInput, nonce;\n\n    public HybridCipherText(byte[] curve25519Ciphertext, byte[] mlkemCipherText, byte[] encryptedInput, byte[] nonce) {\n        this.curve25519Ciphertext = curve25519Ciphertext;\n        this.mlkemCipherText = mlkemCipherText;\n        this.encryptedInput = encryptedInput;\n        this.nonce = nonce;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborByteArray(curve25519Ciphertext));\n        state.put(\"m\", new CborObject.CborByteArray(mlkemCipherText));\n        state.put(\"i\", new CborObject.CborByteArray(encryptedInput));\n        state.put(\"n\", new CborObject.CborByteArray(nonce));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static HybridCipherText fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for HybridCipherText! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new HybridCipherText(m.getByteArray(\"c\"), m.getByteArray(\"m\"), m.getByteArray(\"i\"), m.getByteArray(\"n\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/mlkem/HybridCurve25519MLKEMPublicKey.java",
    "content": "package peergos.shared.crypto.asymmetric.mlkem;\n\nimport peergos.shared.Crypto;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.asymmetric.PublicBoxingKey;\nimport peergos.shared.crypto.asymmetric.SecretBoxingKey;\nimport peergos.shared.crypto.asymmetric.curve25519.Curve25519PublicKey;\nimport peergos.shared.crypto.symmetric.TweetNaClKey;\nimport peergos.shared.util.ArrayOps;\n\nimport java.util.Arrays;\nimport java.util.Objects;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport java.util.concurrent.CompletableFuture;\n\npublic class HybridCurve25519MLKEMPublicKey implements PublicBoxingKey {\n\n    public final Curve25519PublicKey curve25519;\n    public final MlkemPublicKey mlkem;\n    public final Crypto crypto;\n\n    public HybridCurve25519MLKEMPublicKey(Curve25519PublicKey curve25519, MlkemPublicKey mlkem, Crypto crypto) {\n        this.curve25519 = curve25519;\n        this.mlkem = mlkem;\n        this.crypto = crypto;\n    }\n\n    @Override\n    public Type type() {\n        return Type.HybridCurve25519MLKEM;\n    }\n\n    @Override\n    public byte[] getPublicBoxingKey() {\n        throw new IllegalStateException(\"This should not be called!\");\n    }\n\n    @Override\n    public CompletableFuture<byte[]> encryptMessageFor(byte[] input, SecretBoxingKey from) {\n        if (!(from instanceof HybridCurve25519MLKEMSecretKey))\n            throw new IllegalStateException(\"Didn't provide a HybridCurve25519MLKEMSecretKey!\");\n        byte[] curve25519SharedSecret = crypto.random.randomBytes(32);\n        return mlkem.encapsulate().thenCompose(encapsulated -> {\n            byte[] mlkemSharedSecret = encapsulated.sharedSecret;\n            return crypto.hasher.hkdfKey(ArrayOps.concat(curve25519SharedSecret, mlkemSharedSecret)).thenCompose(combinedSecret -> {\n                TweetNaClKey combinedSecretKey = new TweetNaClKey(combinedSecret, false, crypto.symmetricProvider, crypto.random);\n                return curve25519.encryptMessageFor(curve25519SharedSecret, ((HybridCurve25519MLKEMSecretKey) from).curve25519).thenApply(curve25519Ciphertext -> {\n                    byte[] mlkemCipherText = encapsulated.cipherText;\n                    byte[] symmetricNonce = combinedSecretKey.createNonce();\n                    byte[] encryptedInput = combinedSecretKey.encrypt(input, symmetricNonce);\n                    // now combine the 3 ciphertexts with cbor\n                    return new HybridCipherText(curve25519Ciphertext, mlkemCipherText, encryptedInput, symmetricNonce).serialize();\n                });\n            });\n        });\n    }\n\n    @Override\n    public byte[] createNonce() {\n        throw new IllegalStateException(\"This should not be called!\");\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        HybridCurve25519MLKEMPublicKey that = (HybridCurve25519MLKEMPublicKey) o;\n        return Objects.equals(curve25519, that.curve25519) &&\n                Objects.equals(mlkem, that.mlkem);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(curve25519, mlkem);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", curve25519.toCbor());\n        state.put(\"m\", mlkem.toCbor());\n        return new CborObject.CborList(Arrays.asList(new CborObject.CborLong(type().value), CborObject.CborMap.build(state)));\n    }\n\n    public static HybridCurve25519MLKEMPublicKey fromCbor(Cborable cbor, Crypto crypto) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for HybridCurve25519MLKEMPublicKey! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) ((CborObject.CborList) cbor).value.get(1);\n        Curve25519PublicKey curve25519 = m.get(\"c\", c -> Curve25519PublicKey.fromCbor(c, crypto.boxer, crypto.random));\n        MlkemPublicKey mlkem = m.get(\"m\", c -> MlkemPublicKey.fromCbor(c, crypto.mlkem));\n        return new HybridCurve25519MLKEMPublicKey(curve25519, mlkem, crypto);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/mlkem/HybridCurve25519MLKEMSecretKey.java",
    "content": "package peergos.shared.crypto.asymmetric.mlkem;\n\nimport peergos.shared.Crypto;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.asymmetric.PublicBoxingKey;\nimport peergos.shared.crypto.asymmetric.SecretBoxingKey;\nimport peergos.shared.crypto.asymmetric.curve25519.Curve25519PublicKey;\nimport peergos.shared.crypto.asymmetric.curve25519.Curve25519SecretKey;\nimport peergos.shared.crypto.symmetric.TweetNaClKey;\nimport peergos.shared.util.ArrayOps;\nimport peergos.shared.util.Futures;\n\nimport java.util.Arrays;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport java.util.concurrent.CompletableFuture;\n\npublic class HybridCurve25519MLKEMSecretKey implements SecretBoxingKey {\n\n    public final Curve25519SecretKey curve25519;\n    public final MlkemSecretKey mlkem;\n    public final Crypto crypto;\n\n    public HybridCurve25519MLKEMSecretKey(Curve25519SecretKey curve25519, MlkemSecretKey mlkem, Crypto crypto) {\n        this.curve25519 = curve25519;\n        this.mlkem = mlkem;\n        this.crypto = crypto;\n    }\n\n    @Override\n    public PublicBoxingKey.Type type() {\n        return PublicBoxingKey.Type.HybridCurve25519MLKEM;\n    }\n\n    @Override\n    public byte[] getSecretBoxingKey() {\n        throw new IllegalStateException(\"This should not be called!\");\n    }\n\n    @Override\n    public CompletableFuture<byte[]> decryptMessage(byte[] hybridCipher, PublicBoxingKey from) {\n        if (!(from instanceof HybridCurve25519MLKEMPublicKey))\n            return Futures.errored(new IllegalStateException(\"Didn't provide a HybridCurve25519MLKEMPublicKey!\"));\n        HybridCipherText hybrid = HybridCipherText.fromCbor(CborObject.fromByteArray(hybridCipher));\n        return curve25519.decryptMessage(hybrid.curve25519Ciphertext, ((HybridCurve25519MLKEMPublicKey) from).curve25519)\n                .thenCompose(curve25519SharedSecret -> mlkem.decapsulate(hybrid.mlkemCipherText)\n                        .thenCompose(mlkemSharedSecret -> crypto.hasher.hkdfKey(ArrayOps.concat(curve25519SharedSecret, mlkemSharedSecret))\n                                .thenApply(combinedSecret -> new TweetNaClKey(combinedSecret, false, crypto.symmetricProvider, crypto.random))\n                                .thenApply(combinedSecretKey -> combinedSecretKey.decrypt(hybrid.encryptedInput, hybrid.nonce))));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", curve25519.toCbor());\n        state.put(\"m\", mlkem.toCbor());\n        return new CborObject.CborList(Arrays.asList(new CborObject.CborLong(type().value), CborObject.CborMap.build(state)));\n    }\n\n    public static HybridCurve25519MLKEMSecretKey fromCbor(Cborable cbor, Crypto crypto) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for HybridCurve25519MLKEMSecretKey! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) ((CborObject.CborList) cbor).value.get(1);\n        Curve25519SecretKey curve25519 = m.get(\"c\", c -> Curve25519SecretKey.fromCbor(c, crypto.boxer));\n        MlkemSecretKey mlkem = m.get(\"m\", c -> MlkemSecretKey.fromCbor(c, crypto.mlkem));\n        return new HybridCurve25519MLKEMSecretKey(curve25519, mlkem, crypto);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/mlkem/Mlkem.java",
    "content": "package peergos.shared.crypto.asymmetric.mlkem;\n\nimport peergos.shared.crypto.random.JSNaCl;\n\nimport java.util.concurrent.CompletableFuture;\n\npublic interface Mlkem {\n\n    CompletableFuture<Encapsulation> encapsulate(byte[] publicKeyBytes);\n\n    CompletableFuture<byte[]> decapsulate(byte[] cipherTextBytes, byte[] secretKeyBytes);\n\n    CompletableFuture<MlkemKeyPair> generateKeyPair();\n\n    class Encapsulation {\n        public final byte[] sharedSecret, cipherText;\n\n        public Encapsulation(byte[] sharedSecret, byte[] cipherText) {\n            this.sharedSecret = sharedSecret;\n            this.cipherText = cipherText;\n        }\n    }\n\n    class Javascript implements Mlkem {\n        JSNaCl mlJs = new JSNaCl();\n\n        @Override\n        public CompletableFuture<Encapsulation> encapsulate(byte[] publicKeyBytes) {\n            return mlJs.encapsulate(publicKeyBytes)\n                    .thenApply(encapsulated -> {\n                        byte[] sharedSecret = new byte[encapsulated[0].length];\n                        for (int i=0; i < sharedSecret.length; i++)\n                            sharedSecret[i] = encapsulated[0][i];\n                        byte[] cipherText = new byte[encapsulated[1].length];\n                        for (int i=0; i < cipherText.length; i++)\n                            cipherText[i] = encapsulated[1][i];\n                        return new Encapsulation(sharedSecret, cipherText);\n                    });\n        }\n\n        @Override\n        public CompletableFuture<byte[]> decapsulate(byte[] cipherTextBytes, byte[] secretKeyBytes) {\n            return mlJs.decapsulate(cipherTextBytes, secretKeyBytes);\n        }\n\n        @Override\n        public CompletableFuture<MlkemKeyPair> generateKeyPair() {\n            return mlJs.generateMlkemKeyPair().thenApply(keyPair -> {\n                MlkemPublicKey publicKey = new MlkemPublicKey(keyPair[0], this);\n                MlkemSecretKey secretKey = new MlkemSecretKey(keyPair[1], this);\n                return new MlkemKeyPair(publicKey, secretKey);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/mlkem/MlkemKeyPair.java",
    "content": "package peergos.shared.crypto.asymmetric.mlkem;\n\npublic class MlkemKeyPair {\n    public final MlkemPublicKey publicKey;\n    public final MlkemSecretKey secretKey;\n\n    public MlkemKeyPair(MlkemPublicKey publicKey, MlkemSecretKey secretKey) {\n        this.publicKey = publicKey;\n        this.secretKey = secretKey;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/mlkem/MlkemPublicKey.java",
    "content": "package peergos.shared.crypto.asymmetric.mlkem;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.util.Arrays;\nimport java.util.Objects;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport java.util.concurrent.CompletableFuture;\n\npublic class MlkemPublicKey implements Cborable {\n\n    private final Mlkem implementation;\n    private final byte[] keyBytes;\n\n    public MlkemPublicKey(byte[] keyBytes, Mlkem implementation) {\n        this.keyBytes = keyBytes;\n        this.implementation = implementation;\n    }\n\n    public CompletableFuture<Mlkem.Encapsulation> encapsulate() {\n        return implementation.encapsulate(keyBytes);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        MlkemPublicKey that = (MlkemPublicKey) o;\n        return Objects.deepEquals(keyBytes, that.keyBytes);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(keyBytes);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"p\", new CborObject.CborByteArray(keyBytes));\n        return CborObject.CborMap.build(state);\n    }\n\n    static MlkemPublicKey fromCbor(Cborable cbor, Mlkem implementation) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for MlkemPublicKey! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new MlkemPublicKey(m.getByteArray(\"p\"), implementation);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/asymmetric/mlkem/MlkemSecretKey.java",
    "content": "package peergos.shared.crypto.asymmetric.mlkem;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport java.util.concurrent.CompletableFuture;\n\npublic class MlkemSecretKey implements Cborable {\n\n    private final byte[] secretKeyBytes;\n    private final Mlkem implementation;\n\n    public MlkemSecretKey(byte[] secretKeyBytes, Mlkem implementation) {\n        this.secretKeyBytes = secretKeyBytes;\n        this.implementation = implementation;\n    }\n\n    public CompletableFuture<byte[]> decapsulate(byte[] cipherText) {\n        return implementation.decapsulate(cipherText, secretKeyBytes);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"s\", new CborObject.CborByteArray(secretKeyBytes));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static MlkemSecretKey fromCbor(Cborable cbor, Mlkem implementation) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for MlkemSecretKey! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new MlkemSecretKey(m.getByteArray(\"s\"), implementation);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/hash/BaseHash.java",
    "content": "package peergos.shared.crypto.hash;\n\n/* BaseHash.java --\n   Copyright (C) 2001, 2002, 2006 Free Software Foundation, Inc.\n\nThis file is a part of GNU Classpath.\n\nGNU Classpath is free software; you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 2 of the License, or (at\nyour option) any later version.\n\nGNU Classpath is distributed in the hope that it will be useful, but\nWITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\nGeneral Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with GNU Classpath; if not, write to the Free Software\nFoundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301\nUSA\n\nLinking this library statically or dynamically with other modules is\nmaking a combined work based on this library.  Thus, the terms and\nconditions of the GNU General Public License cover the whole\ncombination.\n\nAs a special exception, the copyright holders of this library give you\npermission to link this library with independent modules to produce an\nexecutable, regardless of the license terms of these independent\nmodules, and to copy and distribute the resulting executable under\nterms of your choice, provided that you also meet, for each linked\nindependent module, the terms and conditions of the license of that\nmodule.  An independent module is a module which is not derived from\nor based on this library.  If you modify this library, you may extend\nthis exception to your version of the library, but you are not\nobligated to do so.  If you do not wish to do so, delete this\nexception statement from your version.  */\n\n\n/**\n * A base abstract class to facilitate hash implementations.\n */\npublic abstract class BaseHash\n{\n    /** The canonical name prefix of the hash. */\n    protected String name;\n\n    /** The hash (output) size in bytes. */\n    protected int hashSize;\n\n    /** The hash (inner) block size in bytes. */\n    protected int blockSize;\n\n    /** Number of bytes processed so far. */\n    protected long count;\n\n    /** Temporary input buffer. */\n    protected byte[] buffer;\n\n    /**\n     * Trivial constructor for use by concrete subclasses.\n     *\n     * @param name the canonical name prefix of this instance.\n     * @param hashSize the block size of the output in bytes.\n     * @param blockSize the block size of the internal transform.\n     */\n    protected BaseHash(String name, int hashSize, int blockSize)\n    {\n        super();\n\n        this.name = name;\n        this.hashSize = hashSize;\n        this.blockSize = blockSize;\n        this.buffer = new byte[blockSize];\n\n        resetContext();\n    }\n\n    public String name()\n    {\n        return name;\n    }\n\n    public int hashSize()\n    {\n        return hashSize;\n    }\n\n    public int blockSize()\n    {\n        return blockSize;\n    }\n\n    public void update(byte b)\n    {\n        // compute number of bytes still unhashed; ie. present in buffer\n        int i = (int) (count % blockSize);\n        count++;\n        buffer[i] = b;\n        if (i == (blockSize - 1))\n            transform(buffer, 0);\n    }\n\n    public void update(byte[] b)\n    {\n        update(b, 0, b.length);\n    }\n\n    public void update(byte[] b, int offset, int len)\n    {\n        int n = (int) (count % blockSize);\n        count += len;\n        int partLen = blockSize - n;\n        int i = 0;\n\n        if (len >= partLen)\n        {\n            System.arraycopy(b, offset, buffer, n, partLen);\n            transform(buffer, 0);\n            for (i = partLen; i + blockSize - 1 < len; i += blockSize)\n                transform(b, offset + i);\n\n            n = 0;\n        }\n\n        if (i < len)\n            System.arraycopy(b, offset + i, buffer, n, len - i);\n    }\n\n    public byte[] digest()\n    {\n        byte[] tail = padBuffer(); // pad remaining bytes in buffer\n        update(tail, 0, tail.length); // last transform of a message\n        byte[] result = getResult(); // make a result out of context\n\n        reset(); // reset this instance for future re-use\n\n        return result;\n    }\n\n    public void reset()\n    { // reset this instance for future re-use\n        count = 0L;\n        for (int i = 0; i < blockSize;)\n            buffer[i++] = 0;\n\n        resetContext();\n    }\n\n    public abstract Object clone();\n\n    public abstract boolean selfTest();\n\n    /**\n     * Returns the byte array to use as padding before completing a hash\n     * operation.\n     *\n     * @return the bytes to pad the remaining bytes in the buffer before\n     *         completing a hash operation.\n     */\n    protected abstract byte[] padBuffer();\n\n    /**\n     * Constructs the result from the contents of the current context.\n     *\n     * @return the output of the completed hash operation.\n     */\n    protected abstract byte[] getResult();\n\n    /** Resets the instance for future re-use. */\n    protected abstract void resetContext();\n\n    /**\n     * The block digest transformation per se.\n     *\n     * @param in the <i>blockSize</i> long block, as an array of bytes to digest.\n     * @param offset the index where the data to digest is located within the\n     *          input buffer.\n     */\n    protected abstract void transform(byte[] in, int offset);\n}"
  },
  {
    "path": "src/peergos/shared/crypto/hash/Blake2b.java",
    "content": "package peergos.shared.crypto.hash;\n\n// From https://github.com/alphazero/Blake2b\n/* !!! Doost !!! */\n\n/*\n   A Java implementation of BLAKE2B cryptographic digest algorithm.\n\n   Joubin Mohammad Houshyar <alphazero@sensesay.net>\n   bushwick, nyc\n   02-14-2014\n\n   --\n\n   To the extent possible under law, the author(s) have dedicated all copyright\n   and related and neighboring rights to this software to the public domain\n   worldwide. This software is distributed without any warranty.\n\n   You should have received a copy of the CC0 Public Domain Dedication along with\n   this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.\n*/\n\nimport java.io.Serializable;\nimport java.util.Arrays;\n\nimport static peergos.shared.crypto.hash.Blake2b.Engine.Assert.*;\nimport static peergos.shared.crypto.hash.Blake2b.Engine.LittleEndian.*;\n\n\n/**  */\npublic interface Blake2b {\n    // ---------------------------------------------------------------------\n    // Specification\n    // ---------------------------------------------------------------------\n    public interface Spec {\n        /** pblock size of blake2b */\n        int param_bytes \t\t= 64;\n\n        /** pblock size of blake2b */\n        int block_bytes \t\t= 128;\n\n        /** maximum digest size */\n        int max_digest_bytes \t= 64;\n\n        /** maximum key sie */\n        int max_key_bytes \t\t= 64;\n\n        /** maximum salt size */\n        int max_salt_bytes \t\t= 16;\n\n        /** maximum personalization string size */\n        int max_personalization_bytes = 16;\n\n        /** length of h space vector array */\n        int state_space_len \t\t= 8;\n\n        /** max tree fanout value */\n        int max_tree_fantout \t\t= 0xFF;\n\n        /** max tree depth value */\n        int max_tree_depth \t\t\t= 0xFF;\n\n        /** max tree leaf length value.Note that this has uint32 semantics\n         and thus 0xFFFFFFFF is used as max value limit. */\n        int max_tree_leaf_length \t= 0xFFFFFFFF;\n\n        /** max node offset value. Note that this has uint64 semantics\n         and thus 0xFFFFFFFFFFFFFFFFL is used as max value limit. */\n        long max_node_offset \t\t= 0xFFFFFFFFFFFFFFFFL;\n\n        /** max tree inner length value */\n        int max_tree_inner_length \t= 0xFF;\n\n        /** initialization values map ref-Spec IV[i] -> slice iv[i*8:i*8+7] */\n        long[] IV = {\n                0x6a09e667f3bcc908L,\n                0xbb67ae8584caa73bL,\n                0x3c6ef372fe94f82bL,\n                0xa54ff53a5f1d36f1L,\n                0x510e527fade682d1L,\n                0x9b05688c2b3e6c1fL,\n                0x1f83d9abfb41bd6bL,\n                0x5be0cd19137e2179L\n        };\n    }\n\n    // ---------------------------------------------------------------------\n    // API\n    // ---------------------------------------------------------------------\n    // TODO add ByteBuffer variants\n\n    /**\n     * A serializable / JSON-izable object usable for pausing a hash-in-process\n     * which can then be resumed with the same Parameter the original digest\n     * was constructed with, and fed additional bytes to conclude the hash.\n     */\n    public static final class ResumeHandle implements Serializable {\n        /** per spec */\n        public   long[]  h = new long [ 8 ];\n        /** per spec */\n        public   long[]  t = new long [ 2 ];\n        /** per spec */\n        public   long[]  f = new long [ 2 ];\n        /** per spec (tree) */\n        public         boolean last_node \t= false;\n        /** pulled up 2b optimal */\n        public   long[]  m = new long [16];\n        /** pulled up 2b optimal */\n        public   long[]  v = new long [16];\n\n        /** compressor cache buffer */\n        public   byte[]  buffer;\n        /** compressor cache buffer offset/cached data length */\n        public         int buflen;\n\n        /** digest length from init param - copied here on init */\n        public   int outlen;\n\n        public int type;\n\n        /**\n         * Create a reconstituted Black2b digest from\n         *\n         * @param param\n         * @return\n         */\n        public Blake2b resume(Param param) {\n            assert this.buffer != null && this.buffer.length == Spec.block_bytes;\n            assert this.h != null && this.h.length == 8\n                    && this.t != null && this.t.length == 2\n                    && this.f != null && this.f.length == 2\n                    && this.m != null && this.m.length == 16\n                    && this.v != null && this.v.length == 16 : \"Data is corrupted\";\n            assert this.outlen == param.getDigestLength() : \"Not originally initialized from this param\";\n            assert this.type == 1 || this.type == 2 : \"Unknown type \" + this.type;\n            Engine.State state = new Engine.State(outlen, this.type == 1);\n            System.arraycopy(this.h, 0, state.h, 0, state.h.length);\n            System.arraycopy(this.t, 0, state.t, 0, state.t.length);\n            System.arraycopy(this.f, 0, state.f, 0, state.f.length);\n            System.arraycopy(this.m, 0, m, 0, m.length);\n            System.arraycopy(this.v, 0, v, 0, v.length);\n            System.arraycopy(this.buffer, 0, state.buffer, 0, state.buffer.length);\n            state.buflen = buflen;\n            return type == 1 ?  new Mac(param, state) : new Digest(param, state);\n        }\n    }\n\n    /** */\n    void update (byte[] input) ;\n\n    /** */\n    void update (byte input) ;\n\n    /** */\n    void update (byte[] input, int offset, int len) ;\n\n    /** */\n    byte[] digest () ;\n\n    /** */\n    byte[] digest (byte[] input) ;\n\n    /** */\n    void digest (byte[] output, int offset, int len) ;\n\n    /** */\n    void reset () ;\n\n    ResumeHandle state();\n    // ---------------------------------------------------------------------\n    // Blake2b Message Digest\n    // ---------------------------------------------------------------------\n\n    /** Generalized Blake2b digest. */\n    public static class Digest extends Engine implements Blake2b {\n        private Digest (final Param p) { super (p); }\n        private Digest () { super (); }\n        private Digest(Param p, State state) {\n            super(state, p);\n        }\n\n        public static Digest newInstance () {\n            return new Digest ();\n        }\n        public static Digest newInstance (final int digestLength) {\n            return new Digest (new Param().setDigestLength(digestLength));\n        }\n        public static Digest newInstance (Param p) {\n            return new Digest (p);\n        }\n        public static Digest newInstance (Param p, State state) {\n            return new Digest(p, state);\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Blake2b Message Authentication Code\n    // ---------------------------------------------------------------------\n\n    /** Message Authentication Code (MAC) digest. */\n    public static class Mac extends Engine implements Blake2b {\n        private Mac (final Param p, State state) { super (state, p); }\n        private Mac (final Param p) { super (p); }\n        private Mac () { super (); }\n\n        /** Blake2b.MAC 512 - using default Blake2b.Spec settings with given key */\n        public static Mac newInstance (final byte[] key) {\n            return new Mac (new Param().setKey(key));\n        }\n        /** Blake2b.MAC - using default Blake2b.Spec settings with given key, with given digest length */\n        public static Mac newInstance (final byte[] key, final int digestLength) {\n            return new Mac (new Param().setKey(key).setDigestLength(digestLength));\n        }\n        /** Blake2b.MAC - using the specified Parameters.\n         * @param p asserted valid configured Param with key */\n        public static Mac newInstance (Param p) {\n            assert p != null : \"Param (p) is null\";\n            assert p.hasKey() : \"Param (p) not configured with a key\";\n            return new Mac (p);\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Blake2b Incremental Message Digest (Tree)\n    // ---------------------------------------------------------------------\n\n    /**\n     *  Note that Tree is just a convenience class; incremental hash (tree)\n     *  can be done directly with the Digest class.\n     *  <br>\n     *  Further node, that tree does NOT accumulate the leaf hashes --\n     *  you need to do that\n     */\n    public static class Tree {\n\n        final int     depth;\n        final int     fanout;\n        final int     leaf_length;\n        final int     inner_length;\n        final int     digest_length;\n\n        /**\n         *\n         * @param fanout\n         * @param depth\n         * @param leaf_length size of data input for leaf nodes.\n         * @param inner_length note this is used also as digest-length for non-root nodes.\n         * @param digest_length final hash out digest-length for the tree\n         */\n        public Tree  (\n                final int     depth,\n                final int     fanout,\n                final int     leaf_length,\n                final int     inner_length,\n                final int     digest_length\n        ) {\n            this.fanout = fanout;\n            this.depth = depth;\n            this.leaf_length = leaf_length;\n            this.inner_length = inner_length;\n            this.digest_length = digest_length;\n        }\n        private Param treeParam() {\n            return new Param().\n                    setDepth(depth).setFanout(fanout).setLeafLength(leaf_length).setInnerLength(inner_length);\n        }\n        /** returns the Digest for tree node @ (depth, offset) */\n        public final Digest getNode (final int depth, final int offset) {\n            final Param nodeParam = treeParam().setNodeDepth(depth).setNodeOffset(offset).setDigestLength(inner_length);\n            return Digest.newInstance(nodeParam);\n        }\n        /** returns the Digest for root node */\n        public final Digest getRoot () {\n            final int depth = this.depth - 1;\n            final Param rootParam = treeParam().setNodeDepth(depth).setNodeOffset(0L).setDigestLength(digest_length);\n            return Digest.newInstance(rootParam);\n        }\n    }\n\n    // ---------------------------------------------------------------------\n    // Engine\n    // ---------------------------------------------------------------------\n    static class Engine implements Blake2b {\n\n        // ---------------------------------------------------------------------\n        // Blake2b State(+) per reference implementation\n        // ---------------------------------------------------------------------\n        // REVU: address last_node TODO part of the Tree/incremental\n        static final class State {\n            /** per spec */\n            private final   long[]  h = new long [ 8 ];\n            /** per spec */\n            private final   long[]  t = new long [ 2 ];\n            /** per spec */\n            private final   long[]  f = new long [ 2 ];\n            /** per spec (tree) */\n            private         boolean last_node \t= false;\n            /** pulled up 2b optimal */\n            private final   long[]  m = new long [16];\n            /** pulled up 2b optimal */\n            private final   long[]  v = new long [16];\n\n            /** compressor cache buffer */\n            private final   byte[]  buffer;\n            /** compressor cache buffer offset/cached data length */\n            private         int buflen;\n\n            /** digest length from init param - copied here on init */\n            private final   int outlen;\n\n            private final int digestType;\n\n            State(int digestLength, boolean isMac) {\n                this.buffer = new byte [ Spec.block_bytes ];\n                this.outlen = digestLength;\n                // do not use zero, so we can detect serialization errors\n                this.digestType = isMac ? 1 : 2;\n            }\n\n            public ResumeHandle toResumableForm() {\n                ResumeHandle state = new ResumeHandle();\n                state.h = h;\n                state.t = t;\n                state.f = f;\n                state.last_node = last_node;\n                state.m = m;\n                state.v = v;\n                state.buffer = buffer;\n                state.buflen = buflen;\n                state.outlen = outlen;\n                state.type = digestType;\n                return state;\n            }\n        }\n\n        private State state;\n        /** configuration params */\n        private final   Param param;\n\n        /** read only */\n        private static byte[] zeropad = new byte [ Spec.block_bytes ];\n\n        /** a little bit of semantics */\n        interface flag {\n            int last_block \t= 0;\n            int last_node \t= 1;\n        }\n        /** to support update(byte) */\n        private final\tbyte[] oneByte = new byte[1];\n\n        // ---------------------------------------------------------------------\n        // Ctor & Initialization\n        // ---------------------------------------------------------------------\n\n        /** Basic use constructor pending (TODO) JCA/JCE compliance */\n        Engine () {\n            this( new Param() );\n        }\n\n        Engine(State state, Param param) {\n            assert state != null : \"state is null\";\n            assert param != null : \"param is null\";\n            this.state = state;\n            this.param = param;\n        }\n\n        /** User provided Param for custom configurations */\n        Engine (final Param param) {\n            assert param != null : \"param is null\";\n            this.param = param;\n            state  = new State(param.getDigestLength(), this instanceof Mac);\n            if ( param.getDepth() > Param.Default.depth ) {\n                final int ndepth = param.getNodeDepth();\n                final long nxoff = param.getNodeOffset();\n                if (ndepth == param.getDepth() - 1) {\n                    state.last_node = true;\n                    assert nxoff == 0 : \"root must have offset of zero\";\n                } else if ( nxoff == param.getFanout() - 1) {\n                    this.state.last_node = true;\n                }\n            }\n\n            initialize();\n        }\n\n        public ResumeHandle state() {\n            return state.toResumableForm();\n        }\n\n        private void initialize () {\n            // state vector h - copy values to address reset() requests\n            System.arraycopy( param.initialized_H(), 0, this.state.h, 0, Spec.state_space_len);\n\n            // if we have a key update initial block\n            // Note param has zero padded key_bytes to Spec.max_key_bytes\n            if(param.hasKey){\n                this.update (param.key_bytes, 0, Spec.block_bytes);\n            }\n        }\n\n        // ---------------------------------------------------------------------\n        // interface: Blake2b API\n        // ---------------------------------------------------------------------\n\n        /** {@inheritDoc} */\n        @Override final public void reset () {\n            // reset cache\n            this.state.buflen = 0;\n            for(int i=0; i < state.buffer.length; i++){\n                state.buffer[ i ] = (byte) 0;\n            }\n\n            // reset flags\n            this.state.f[ 0 ] = 0L;\n            this.state.f[ 1 ] = 0L;\n\n            // reset counters\n            this.state.t[ 0 ] = 0L;\n            this.state.t[ 1 ] = 0L;\n\n            // reset state vector\n            // NOTE: keep as last stmt as init calls update0 for MACs.\n            initialize();\n        }\n\n        /** {@inheritDoc} */\n        @Override final public void update (final byte[] b, int off, int len) {\n            if (b == null) {\n                throw new IllegalArgumentException(\"input buffer (b) is null\");\n            }\n            /* zero or more calls to compress */\n            final long[] t = state.t;\n            final byte[] buffer = state.buffer;\n            while (len > 0) {\n                if ( state.buflen == 0) {\n                    /* try compressing direct from input ? */\n                    while ( len > Spec.block_bytes ) {\n                        t[0] += Spec.block_bytes;\n                        t[1] += (t[0] < 0 && state.buflen > -t[0]) ? 1 : 0;\n                        compress( b, off);\n                        len -= Spec.block_bytes;\n                        off += Spec.block_bytes;\n                    }\n                } else if ( state.buflen == Spec.block_bytes ) {\n                    /* flush */\n                    t[0] += Spec.block_bytes;\n                    t[1] += t[0] == 0 ? 1 : 0;\n                    compress( buffer, 0 );\n                    state.buflen = 0;\n                    continue;\n                }\n\n                // \"are we there yet?\"\n                if( len == 0 ) return;\n\n                final int cap = Spec.block_bytes - state.buflen;\n                final int fill = len > cap ? cap : len;\n                System.arraycopy( b, off, buffer, state.buflen, fill );\n                state.buflen += fill;\n                len -= fill;\n                off += fill;\n            }\n        }\n\n        /** {@inheritDoc} */\n        @Override final public void update (byte b) {\n            oneByte[0] = b;\n            update (oneByte, 0, 1);\n        }\n\n        /** {@inheritDoc} */\n        @Override final public void update(byte[] input) {\n            update (input, 0, input.length);\n        }\n\n        /** {@inheritDoc} */\n        @Override final public void digest(byte[] output, int off, int len) {\n            // zero pad last block; set last block flags; and compress\n            System.arraycopy( zeropad, 0, state.buffer, state.buflen, Spec.block_bytes - state.buflen);\n            if(state.buflen > 0) {\n                this.state.t[0] += state.buflen;\n                this.state.t[1] += this.state.t[0] == 0 ? 1 : 0;\n            }\n\n            this.state.f[ flag.last_block ] = 0xFFFFFFFFFFFFFFFFL;\n            this.state.f[ flag.last_node ] = this.state.last_node ? 0xFFFFFFFFFFFFFFFFL : 0x0L;\n\n            // compres and write final out (truncated to len) to output\n            compress( state.buffer, 0 );\n            hashout( output, off, len );\n\n            reset();\n        }\n\n        /** {@inheritDoc} */\n        @Override final public byte[] digest () throws IllegalArgumentException {\n            final byte[] out = new byte [state.outlen];\n            digest ( out, 0, state.outlen );\n            return out;\n        }\n\n        /** {@inheritDoc} */\n        @Override final public byte[] digest (byte[] input) {\n            update(input, 0, input.length);\n            return digest();\n        }\n\n        // ---------------------------------------------------------------------\n        // Internal Ops\n        // ---------------------------------------------------------------------\n\n        /**\n         * write out the digest output from the 'h' registers.\n         * truncate full output if necessary.\n         */\n        private void hashout (final byte[] out, final int offset, final int hashlen) {\n            // write max number of whole longs\n            final int lcnt = hashlen >>> 3;\n            long v = 0;\n            int i = offset;\n            final long[] h = state.h;\n            for (int w = 0; w < lcnt; w++) {\n                v = h [ w ];\n                out [ i ] = (byte) v; v >>>= 8;\n                out [ i+1 ] = (byte) v; v >>>= 8;\n                out [ i+2 ] = (byte) v; v >>>= 8;\n                out [ i+3 ] = (byte) v; v >>>= 8;\n                out [ i+4 ] = (byte) v; v >>>= 8;\n                out [ i+5 ] = (byte) v; v >>>= 8;\n                out [ i+6 ] = (byte) v; v >>>= 8;\n                out [ i+7 ] = (byte) v;\n                i+=8;\n            }\n\n            // basta?\n            if( hashlen == Spec.max_digest_bytes) return;\n\n            // write the remaining bytes of a partial long value\n            v = h [ lcnt ];\n            i = lcnt << 3;\n            while( i < hashlen ) {\n                out [ offset + i ] = (byte) v; v >>>= 8; ++i;\n            }\n        }\n\n        ////////////////////////////////////////////////////////////////////////\n        /// Compression Kernel /////////////////////////////////////////// BEGIN\n        ////////////////////////////////////////////////////////////////////////\n\n        /** compress Spec.block_bytes data from b, from offset */\n        private void compress (final byte[] b, final int offset) {\n\n            // set m registers\n            final long[] m = state.m;\n            m[ 0] = LittleEndian.readLong(b, offset);\n            m[ 1] = LittleEndian.readLong(b, offset + 8);\n            m[ 2] = LittleEndian.readLong(b, offset + 16);\n            m[ 3] = LittleEndian.readLong(b, offset + 24);\n            m[ 4] = LittleEndian.readLong(b, offset + 32);\n            m[ 5] = LittleEndian.readLong(b, offset + 40);\n            m[ 6] = LittleEndian.readLong(b, offset + 48);\n            m[ 7] = LittleEndian.readLong(b, offset + 56);\n            m[ 8] = LittleEndian.readLong(b, offset + 64);\n            m[ 9] = LittleEndian.readLong(b, offset + 72);\n            m[10] = LittleEndian.readLong(b, offset + 80);\n            m[11] = LittleEndian.readLong(b, offset + 88);\n            m[12] = LittleEndian.readLong(b, offset + 96);\n            m[13] = LittleEndian.readLong(b, offset + 104);\n            m[14] = LittleEndian.readLong(b, offset + 112);\n            m[15] = LittleEndian.readLong(b, offset + 120);\n\n            // set v registers\n            final   long[]  v = state.v;\n            final   long[]  h = state.h;\n            final   long[]  t = state.t;\n            final   long[]  f = state.f;\n            v[ 0] = h[0];\n            v[ 1] = h[1];\n            v[ 2] = h[2];\n            v[ 3] = h[3];\n            v[ 4] = h[4];\n            v[ 5] = h[5];\n            v[ 6] = h[6];\n            v[ 7] = h[7];\n            v[ 8] =         0x6a09e667f3bcc908L;\n            v[ 9] =         0xbb67ae8584caa73bL;\n            v[10] =         0x3c6ef372fe94f82bL;\n            v[11] =         0xa54ff53a5f1d36f1L;\n            v[12] = t [0] ^ 0x510e527fade682d1L;\n            v[13] = t [1] ^ 0x9b05688c2b3e6c1fL;\n            v[14] = f [0] ^ 0x1f83d9abfb41bd6bL;\n            v[15] = f [1] ^ 0x5be0cd19137e2179L;\n\n            // do the rounds\n            round_0(v, m);\n            round_1(v, m);\n            round_2(v, m);\n            round_3(v, m);\n            round_4(v, m);\n            round_5(v, m);\n            round_6(v, m);\n            round_7(v, m);\n            round_8(v, m);\n            round_9(v, m);\n            round_0(v, m); // round 10 is identical to round 0\n            round_1(v, m); // round 11 is identical to round 1\n\n            // Update state vector h\n            h[0] ^= v[0] ^ v[8];\n            h[1] ^= v[1] ^ v[9];\n            h[2] ^= v[2] ^ v[10];\n            h[3] ^= v[3] ^ v[11];\n            h[4] ^= v[4] ^ v[12];\n            h[5] ^= v[5] ^ v[13];\n            h[6] ^= v[6] ^ v[14];\n            h[7] ^= v[7] ^ v[15];\n\n            /* kaamil */\n        }\n        private void round_0(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[0];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [1];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[2];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[3];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[4];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[5];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[6];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[7];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[8];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[9];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[10];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[11];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[12];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[13];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[14];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[15];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_1(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[14];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [10];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[4];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[8];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[9];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[15];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[13];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[6];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[1];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[12];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[0];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[2];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[11];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[7];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[5];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[3];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_2(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[11];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [8];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[12];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[0];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[5];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[2];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[15];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[13];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[10];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[14];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[3];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[6];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[7];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[1];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[9];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[4];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_3(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[7];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [9];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[3];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[1];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[13];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[12];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[11];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[14];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[2];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[6];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[5];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[10];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[4];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[0];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[15];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[8];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_4(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[9];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [0];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[5];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[7];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[2];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[4];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[10];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[15];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[14];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[1];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[11];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[12];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[6];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[8];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[3];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[13];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_5(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[2];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [12];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[6];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[10];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[0];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[11];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[8];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[3];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[4];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[13];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[7];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[5];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[15];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[14];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[1];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[9];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_6(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[12];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [5];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[1];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[15];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[14];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[13];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[4];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[10];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[0];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[7];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[6];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[3];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[9];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[2];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[8];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[11];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_7(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[13];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [11];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[7];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[14];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[12];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[1];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[3];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[9];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[5];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[0];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[15];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[4];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[8];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[6];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[2];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[10];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_8(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[6];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [15];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[14];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[9];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[11];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[3];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[0];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[8];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[12];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[2];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[13];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[7];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[1];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[4];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[10];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[5];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n        private void round_9(final long[] v, final long[] m) {\n            v[ 0] = v[ 0] + v[ 4] + m[10];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 0] = v[ 0] + v[ 4] + m [2];\n            v[12] ^= v[ 0];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[ 8] = v[ 8] + v[12];\n            v[ 4] ^= v[ 8];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 5] + m[8];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 1] = v[ 1] + v[ 5] + m[4];\n            v[13] ^= v[ 1];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 9] = v[ 9] + v[13];\n            v[ 5] ^= v[ 9];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 6] + m[7];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 2] = v[ 2] + v[ 6] + m[6];\n            v[14] ^= v[ 2];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[10] = v[10] + v[14];\n            v[ 6] ^= v[10];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 7] + m[1];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 3] = v[ 3] + v[ 7] + m[5];\n            v[15] ^= v[ 3];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[11] = v[11] + v[15];\n            v[ 7] ^= v[11];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 0] = v[ 0] + v[ 5] + m[15];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 );\n            v[ 0] = v[ 0] + v[ 5] + m[11];\n            v[15] ^= v[ 0];\n            v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 );\n            v[10] = v[10] + v[15];\n            v[ 5] ^= v[10];\n            v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 );\n\n            v[ 1] = v[ 1] + v[ 6] + m[9];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 );\n            v[ 1] = v[ 1] + v[ 6] + + m[14];\n            v[12] ^= v[ 1];\n            v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 );\n            v[11] = v[11] + v[12];\n            v[ 6] ^= v[11];\n            v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 );\n\n            v[ 2] = v[ 2] + v[ 7] + m[3];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 );\n            v[ 2] = v[ 2] + v[ 7] + m[12];\n            v[13] ^= v[ 2];\n            v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 );\n            v[ 8] = v[ 8] + v[13];\n            v[ 7] ^= v[ 8];\n            v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 );\n\n            v[ 3] = v[ 3] + v[ 4] + m[13];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 );\n            v[ 3] = v[ 3] + v[ 4] + m[0];\n            v[14] ^= v[ 3];\n            v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 );\n            v[ 9] = v[ 9] + v[14];\n            v[ 4] ^= v[ 9];\n            v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 );\n        }\n\n        ////////////////////////////////////////////////////////////////////////\n        /// Compression Kernel //////////////////////////////////////////// FINI\n        ////////////////////////////////////////////////////////////////////////\n\n        // ---------------------------------------------------------------------\n        // Helper for assert error messages\n        // ---------------------------------------------------------------------\n        public static final class Assert {\n            public final static String exclusiveUpperBound = \" >= \";\n            public final static String inclusiveUpperBound = \" > \";\n            public final static String exclusiveLowerBound = \" <= \";\n            public final static String inclusiveLowerBound = \" < \";\n            static <T extends Number> String assertFail(final String name, final T v, final String err, final T spec) {\n                new Exception().printStackTrace();\n                return \"'\" + name + \"' \" + v + \" is\" + err + spec;\n            }\n        }\n        // ---------------------------------------------------------------------\n        // Little Endian Codecs (inlined in the compressor)\n        /*\n         * impl note: these are not library funcs and used in hot loops, so no\n         * null or bounds checks are performed. For our purposes, this is OK.\n         */\n        // ---------------------------------------------------------------------\n\n        public static class LittleEndian {\n            private static final byte[] hex_digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};\n            private static final byte[] HEX_digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};\n            /** @return hex rep of byte (lower case). */\n            static public String toHexStr (final byte[] b) {\n                return toHexStr (b, false); // because String class is slower.\n            }\n            static public String toHexStr (final byte[] b, boolean upperCase) {\n                final int len = b.length;\n                final byte[] digits = new byte[ len * 2 ];\n                final byte[] hex_rep = upperCase ? HEX_digits : hex_digits ;\n                for (int i = 0; i < len; i++) {\n                    digits [ i*2   ] = hex_rep [ (byte) (b[i] >> 4 & 0x0F)  ];\n                    digits [ i*2+1 ] = hex_rep [ (byte) (b[i]      & 0x0F) ];\n                }\n                return new String(digits);\n            }\n            public static int readInt (final byte[] b, final int off) {\n                int v0 = ((int)b [ off ] & 0xFF );\n                v0 |= ((int)b [ off + 1 ] & 0xFF ) <<  8;\n                v0 |= ((int)b [ off + 2 ] & 0xFF ) << 16;\n                return v0 | ((int)b [ off + 3 ]  ) << 24;\n            }\n            /** Little endian - byte[] to long */\n            public static long readLong (final byte[] b, final int off) {\n                long v0 = ((long)b [ off ] & 0xFF );\n                v0 |= ((long)b [ off + 1 ] & 0xFF ) <<  8;\n                v0 |= ((long)b [ off + 2 ] & 0xFF ) << 16;\n                v0 |= ((long)b [ off + 3 ] & 0xFF ) << 24;\n                v0 |= ((long)b [ off + 4 ] & 0xFF ) << 32;\n                v0 |= ((long)b [ off + 5 ] & 0xFF ) << 40;\n                v0 |= ((long)b [ off + 6 ] & 0xFF ) << 48;\n                return v0 | ((long)b [ off + 7 ] )  << 56;\n            }\n            /** Little endian - long to byte[] */\n            public static void writeLong (long v, final byte[] b, final int off) {\n                b [ off ]     = (byte) v; v >>>= 8;\n                b [ off + 1 ] = (byte) v; v >>>= 8;\n                b [ off + 2 ] = (byte) v; v >>>= 8;\n                b [ off + 3 ] = (byte) v; v >>>= 8;\n                b [ off + 4 ] = (byte) v; v >>>= 8;\n                b [ off + 5 ] = (byte) v; v >>>= 8;\n                b [ off + 6 ] = (byte) v; v >>>= 8;\n                b [ off + 7 ] = (byte) v;\n            }\n            /** Little endian - int to byte[] */\n            public static void writeInt (int v, final byte[] b, final int off) {\n                b [ off ]     = (byte) v; v >>>= 8;\n                b [ off + 1 ] = (byte) v; v >>>= 8;\n                b [ off + 2 ] = (byte) v; v >>>= 8;\n                b [ off + 3 ] = (byte) v;\n            }\n        }\n    }\n    // ---------------------------------------------------------------------\n    // digest parameter (block)\n    // ---------------------------------------------------------------------\n    /** Blake2b configuration parameters block per spec */\n    // REVU: need to review a revert back to non-lazy impl TODO: do & bench\n    public static class Param {\n        interface Xoff {\n            int digest_length   = 0;\n            int key_length      = 1;\n            int fanout          = 2;\n            int depth           = 3;\n            int leaf_length     = 4;\n            int node_offset     = 8;\n            int node_depth      = 16;\n            int inner_length    = 17;\n            int reserved        = 18;\n            int salt            = 32;\n            int personal        = 48;\n        }\n        public interface Default {\n            byte    digest_length   = Spec.max_digest_bytes;\n            byte    key_length      = 0;\n            byte    fanout          = 1;\n            byte    depth           = 1;\n            int     leaf_length     = 0;\n            long    node_offset     = 0;\n            byte    node_depth      = 0;\n            byte    inner_length    = 0;\n        }\n        /** default bytes of Blake2b parameter block */\n        final static byte[] default_bytes = new byte[ Spec.param_bytes ];\n        /** initialize default_bytes */\n        static {\n            default_bytes [ Xoff.digest_length ] = Default.digest_length;\n            default_bytes [ Xoff.key_length ] = Default.key_length;\n            default_bytes [ Xoff.fanout ] = Default.fanout;\n            default_bytes [ Xoff.depth ] = Default.depth;\n            /* def. leaf_length is 0 fill and already set by new byte[] */\n            /* def. node_offset is 0 fill and already set by new byte[] */\n            default_bytes [ Xoff.node_depth ] = Default.node_depth;\n            default_bytes [ Xoff.inner_length] = Default.inner_length;\n            /* def. salt is 0 fill and already set by new byte[] */\n            /* def. personal is 0 fill and already set by new byte[] */\n        }\n        /** default Blake2b h vector */\n        final static long[] default_h = new long [ Spec.state_space_len ];\n        static {\n            default_h [0] = readLong( default_bytes, 0  );\n            default_h [1] = readLong( default_bytes, 8  );\n            default_h [2] = readLong( default_bytes, 16 );\n            default_h [3] = readLong( default_bytes, 24 );\n            default_h [4] = readLong( default_bytes, 32 );\n            default_h [5] = readLong( default_bytes, 40 );\n            default_h [6] = readLong( default_bytes, 48 );\n            default_h [7] = readLong( default_bytes, 56 );\n\n            default_h [0] ^= Spec.IV [0];\n            default_h [1] ^= Spec.IV [1];\n            default_h [2] ^= Spec.IV [2];\n            default_h [3] ^= Spec.IV [3];\n            default_h [4] ^= Spec.IV [4];\n            default_h [5] ^= Spec.IV [5];\n            default_h [6] ^= Spec.IV [6];\n            default_h [7] ^= Spec.IV [7];\n        }\n\n        /** */\n        private boolean hasKey = false;\n        /** not sure how to make this secure - TODO */\n        private byte[] key_bytes = null;\n        /** */\n        private byte[] bytes = null;\n        /** */\n        private  final long[] h = new long [ Spec.state_space_len ];\n\n        /** */\n        public Param() {\n            System.arraycopy( default_h, 0, h, 0, Spec.state_space_len );\n        }\n        /** */\n        public long[] initialized_H () {\n            return h;\n        }\n        /** package only - copy returned - do not use in functional loops */\n        public byte[] getBytes() {\n            lazyInitBytes();\n            byte[] copy = new byte[ bytes.length ];\n            System.arraycopy( bytes, 0, copy, 0, bytes.length );\n            return copy;\n        }\n\n        final byte getByteParam (final int xoffset) {\n            byte[] _bytes = bytes;\n            if(_bytes == null) _bytes = Param.default_bytes;\n            return _bytes[ xoffset];\n        }\n        final int getIntParam (final int xoffset) {\n            byte[] _bytes = bytes;\n            if(_bytes == null) _bytes = Param.default_bytes;\n            return readInt ( _bytes, xoffset);\n        }\n        final long getLongParam (final int xoffset) {\n            byte[] _bytes = bytes;\n            if(_bytes == null) _bytes = Param.default_bytes;\n            return readLong ( _bytes, xoffset);\n        }\n        // TODO same for tree params depth, fanout, inner, node-depth, node-offset\n        public final int getDigestLength() {\n            return (int) getByteParam ( Xoff.digest_length );\n        }\n        public final int getKeyLength() {\n            return (int) getByteParam ( Xoff.key_length );\n        }\n        public final int getFanout() {\n            return (int) getByteParam ( Xoff.fanout );\n        }\n        public final int getDepth() {\n            return (int) getByteParam ( Xoff.depth );\n        }\n        public final int getLeafLength() {\n            return getIntParam ( Xoff.leaf_length );\n        }\n        public final long getNodeOffset() {\n            return getLongParam ( Xoff.node_offset );\n        }\n        public final int getNodeDepth() {\n            return (int) getByteParam ( Xoff.node_depth );\n        }\n        public final int getInnerLength() {\n            return (int) getByteParam ( Xoff.inner_length );\n        }\n\n        public final boolean hasKey() { return this.hasKey; }\n\n        public Param clone() {\n            final Param clone = new Param();\n            System.arraycopy(this.h, 0, clone.h, 0, h.length);\n            clone.lazyInitBytes();\n            System.arraycopy(this.bytes, 0, clone.bytes, 0, this.bytes.length);\n\n            if(this.hasKey){\n                clone.hasKey = this.hasKey;\n                clone.key_bytes = new byte [Spec.max_key_bytes * 2];\n                System.arraycopy(this.key_bytes, 0, clone.key_bytes, 0, this.key_bytes.length);\n            }\n            return clone;\n        }\n        ////////////////////////////////////////////////////////////////////////\n        /// lazy setters - write directly to the bytes image of param block ////\n        ////////////////////////////////////////////////////////////////////////\n        final void lazyInitBytes () {\n            if( bytes == null ) {\n                bytes = new byte [ Spec.param_bytes ];\n                System.arraycopy(Param.default_bytes, 0, bytes, 0, Spec.param_bytes);\n            }\n        }\n        /* 0-7 inclusive */\n        public final Param setDigestLength(int len) {\n            assert len > 0 : assertFail(\"len\", len, exclusiveLowerBound, 0);\n            assert len <= Spec.max_digest_bytes : assertFail(\"len\", len, inclusiveUpperBound, Spec.max_digest_bytes);\n\n            lazyInitBytes();\n            bytes[ Xoff.digest_length ] = (byte) len;\n            h[ 0 ] = readLong( bytes, 0  );\n            h[ 0 ] ^= Spec.IV [ 0 ];\n            return this;\n        }\n        public final Param setKey (final byte[] key) {\n            assert key != null : \"key is null\";\n            assert key.length <= Spec.max_key_bytes : assertFail(\"key.length\", key.length, inclusiveUpperBound, Spec.max_key_bytes);\n\n            // zeropad keybytes\n            this.key_bytes = new byte [Spec.max_key_bytes * 2];\n            System.arraycopy ( key, 0, this.key_bytes, 0, key.length );\n            lazyInitBytes();\n            bytes[ Xoff.key_length ] = (byte) key.length; // checked c ref; this is correct\n            h[ 0 ] = readLong( bytes, 0  );\n            h[ 0 ] ^= Spec.IV [ 0 ];\n            this.hasKey  = true;\n            return this;\n        }\n        public final Param setFanout(int fanout) {\n            assert fanout > 0 : assertFail(\"fanout\", fanout, exclusiveLowerBound, 0);\n\n            lazyInitBytes();\n            bytes[ Xoff.fanout ] = (byte) fanout;\n            h[ 0 ] = readLong( bytes, 0  );\n            h[ 0 ] ^= Spec.IV [ 0 ];\n            return this;\n        }\n        public final Param setDepth(int depth) {\n            assert depth > 0 : assertFail(\"depth\", depth, exclusiveLowerBound, 0);\n\n            lazyInitBytes();\n            bytes[ Xoff.depth ] = (byte) depth;\n            h[ 0 ] = readLong( bytes, 0  );\n            h[ 0 ] ^= Spec.IV [ 0 ];\n            return this;\n        }\n        public final Param setLeafLength(int leaf_length) {\n            assert leaf_length >= 0 : assertFail(\"leaf_length\", leaf_length, inclusiveLowerBound, 0);\n\n            lazyInitBytes();\n            writeInt (leaf_length, bytes, Xoff.leaf_length);\n            h[ 0 ] = readLong( bytes, 0  );\n            h[ 0 ] ^= Spec.IV [ 0 ];\n            return this;\n        }\n\n        /* 8-15 inclusive */\n        public final Param setNodeOffset(long node_offset) {\n            assert node_offset >= 0 : assertFail(\"node_offset\", node_offset, inclusiveLowerBound, 0);\n\n            lazyInitBytes();\n            writeLong(node_offset, bytes, Xoff.node_offset);\n            h[ 1 ] = readLong( bytes, Xoff.node_offset );\n            h[ 1 ] ^= Spec.IV [ 1 ];\n            return this;\n        }\n\n        /* 16-23 inclusive */\n        public final Param setNodeDepth(int node_depth) {\n            assert node_depth >= 0 : assertFail(\"node_depth\", node_depth, inclusiveLowerBound, 0);\n\n            lazyInitBytes();\n            bytes[ Xoff.node_depth ] = (byte) node_depth;\n            h[ 2 ] = readLong( bytes, Xoff.node_depth );\n            h[ 2 ] ^= Spec.IV [ 2 ];\n            h[ 3 ] = readLong( bytes, Xoff.node_depth + 8);\n            h[ 3 ] ^= Spec.IV [ 3 ];\n            return this;\n        }\n        public final Param setInnerLength(int inner_length) {\n            assert inner_length >= 0 : assertFail(\"inner_length\", inner_length, inclusiveLowerBound, 0);\n\n            lazyInitBytes();\n            bytes[ Xoff.inner_length] = (byte) inner_length;\n            h[ 2 ] = readLong( bytes, Xoff.node_depth );\n            h[ 2 ] ^= Spec.IV [ 2 ];\n            h[ 3 ] = readLong( bytes, Xoff.node_depth + 8);\n            h[ 3 ] ^= Spec.IV [ 3 ];\n            return this;\n        }\n\n        /* 24-31 masked by reserved and remain unchanged */\n\n        /* 32-47 inclusive */\n        public final Param setSalt(final byte[] salt) {\n            assert salt != null : \"salt is null\";\n            assert salt.length <= Spec.max_salt_bytes : assertFail(\"salt.length\", salt.length, inclusiveUpperBound, Spec.max_salt_bytes);\n\n            lazyInitBytes();\n            Arrays.fill ( bytes, Xoff.salt, Xoff.salt + Spec.max_salt_bytes, (byte)0);\n            System.arraycopy( salt, 0, bytes, Xoff.salt, salt.length );\n            h[ 4 ] = readLong( bytes, Xoff.salt );\n            h[ 4 ] ^= Spec.IV [ 4 ];\n            h[ 5 ] = readLong( bytes, Xoff.salt + 8 );\n            h[ 5 ] ^= Spec.IV [ 5 ];\n            return this;\n        }\n\n        /* 48-63 inclusive */\n        public final Param setPersonal(byte[] personal) {\n            assert personal != null : \"personal is null\";\n            assert personal.length <= Spec.max_personalization_bytes : assertFail(\"personal.length\", personal.length, inclusiveUpperBound, Spec.max_personalization_bytes);\n\n            lazyInitBytes();\n            Arrays.fill ( bytes, Xoff.personal, Xoff.personal + Spec.max_personalization_bytes, (byte)0);\n            System.arraycopy( personal, 0, bytes, Xoff.personal, personal.length );\n            h[ 6 ] = readLong( bytes, Xoff.personal );\n            h[ 6 ] ^= Spec.IV [ 6 ];\n            h[ 7 ] = readLong( bytes, Xoff.personal + 8 );\n            h[ 7 ] ^= Spec.IV [ 7 ];\n            return this;\n        }\n        ////////////////////////////////////////////////////////////////////////\n        /// lazy setters /////////////////////////////////////////////////// END\n        ////////////////////////////////////////////////////////////////////////\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/hash/Hash.java",
    "content": "package peergos.shared.crypto.hash;\n\nimport peergos.shared.user.fs.*;\n\nimport java.io.UnsupportedEncodingException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.concurrent.*;\n\npublic class Hash {\n    public static final String HASH = \"SHA-256\";\n\n    public static byte[] sha256(byte[] input) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(HASH);\n            md.update(input);\n            return md.digest();\n        } catch (NoSuchAlgorithmException e) {\n            // This is only here to work around a bug in Doppio JVM\n            Sha256 sha256 = new Sha256();\n            sha256.update(input);\n            byte[] hash = sha256.digest();\n            return hash;\n        }\n    }\n\n    public static CompletableFuture<byte[]> sha256(AsyncReader input, long length) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(HASH);\n            return sha256(input, length, md, new byte[Chunk.MAX_SIZE]);\n        } catch (NoSuchAlgorithmException e) {\n            // This is only here to work around a bug in Doppio JVM\n            Sha256 sha256 = new Sha256();\n            return sha256(input, length, sha256, new byte[Chunk.MAX_SIZE]);\n        }\n    }\n\n    private static CompletableFuture<byte[]> sha256(AsyncReader input, long length, MessageDigest md, byte[] buf) {\n        if (length == 0)\n            return CompletableFuture.completedFuture(md.digest());\n        return input.readIntoArray(buf, 0, (int) Math.min((long) buf.length, length))\n                .thenCompose(read -> {\n                    md.update(buf, 0, read);\n                    return sha256(input, length - read, md, buf);\n                });\n    }\n\n    private static CompletableFuture<byte[]> sha256(AsyncReader input, long length, Sha256 md, byte[] buf) {\n        if (length == 0)\n            return CompletableFuture.completedFuture(md.digest());\n        return input.readIntoArray(buf, 0, (int) Math.min((long) buf.length, length))\n                .thenCompose(read -> {\n                    md.update(buf, 0, read);\n                    return sha256(input, length - read, md, buf);\n                });\n    }\n\n    public static byte[] sha256(String password) {\n        try {\n            return sha256(password.getBytes(\"UTF-8\"));\n        } catch (UnsupportedEncodingException e) {\n            throw new IllegalStateException(\"couldn't hash password\");\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/hash/Hasher.java",
    "content": "package peergos.shared.crypto.hash;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.crypto.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.charset.*;\nimport java.util.concurrent.CompletableFuture;\n@JsType\npublic interface Hasher {\n\n    CompletableFuture<byte[]> hashToKeyBytes(String username, String password, SecretGenerationAlgorithm algorithm);\n\n    CompletableFuture<ProofOfWork> generateProofOfWork(int difficulty, byte[] data);\n\n    CompletableFuture<byte[]> sha256(byte[] input);\n\n    default CompletableFuture<byte[]> sha256Section(AsyncReader reader, long start, long end) {\n        int length = (int)(end - start);\n        byte[] buf = new byte[length];\n        return reader.seek(start)\n                .thenCompose(seeked -> readFully(seeked, buf, 0, length))\n                .thenCompose(this::sha256);\n    }\n\n    private static CompletableFuture<byte[]> readFully(AsyncReader reader, byte[] buf, int offset, int remaining) {\n        if (remaining == 0)\n            return CompletableFuture.completedFuture(buf);\n        return reader.readIntoArray(buf, offset, remaining)\n                .thenCompose(n -> readFully(reader, buf, offset + n, remaining - n));\n    }\n\n    CompletableFuture<byte[]> hmacSha256(byte[] secretKey, byte[] message);\n\n    @SuppressWarnings(\"unusable-by-js\")\n    CompletableFuture<Multihash> hashFromStream(AsyncReader stream, long length);\n\n    byte[] blake2b(byte[] input, int outputBytes);\n\n    default CompletableFuture<Cid> hash(byte[] input, boolean isRaw) {\n        return sha256(input)\n                .thenApply(h -> Cid.buildCidV1(isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.sha2_256, h));\n    }\n\n    default CompletableFuture<Multihash> bareHash(byte[] input) {\n        return sha256(input)\n                .thenApply(h -> new Multihash(Multihash.Type.sha2_256, h));\n    }\n\n    byte[] hmacInfo = ArrayOps.concat(\"peergos\".getBytes(StandardCharsets.UTF_8), new byte[]{1});\n\n    default CompletableFuture<byte[]> hkdfKey(byte[] ikm) {\n        // See https://soatok.blog/2021/11/17/understanding-hkdf/ for why salt is the secret key to hmac\n        byte[] salt = new byte[32];\n        return hmacSha256(salt, ikm)\n                .thenCompose(prk -> hmacSha256(prk, hmacInfo));\n    }\n\n    default Cid identityHash(byte[] input, boolean isRaw) {\n        if (input.length > Multihash.MAX_IDENTITY_HASH_SIZE)\n            throw new IllegalStateException(\"Exceeded maximum size for identity multihashes!\");\n        return Cid.buildCidV1(isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.id, input);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/hash/NativeScryptJS.java",
    "content": "package peergos.shared.crypto.hash;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.concurrent.*;\n\n@JsType(namespace = \"scryptJS\", isNative = true)\npublic class NativeScryptJS {\n\n    public native CompletableFuture<byte[]> hashToKeyBytes(String username, String password, SecretGenerationAlgorithm algorithm);\n\n    public native CompletableFuture<ProofOfWork> generateProofOfWork(int difficulty, byte[] data);\n\n    public native CompletableFuture<byte[]> sha256(byte[] input);\n\n    public native CompletableFuture<byte[]> hmacSha256(byte[] secretKey, byte[] message);\n\n    public native byte[] blake2b(byte[] input, int outputBytes);\n\n    public native CompletableFuture<byte[]> streamSha256(AsyncReader stream, int length);\n\n    public native CompletableFuture<byte[]> sha256FileSection(JSFileReader reader, int startHi, int startLo, int endHi, int endLo);\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/hash/PublicKeyHash.java",
    "content": "package peergos.shared.crypto.hash;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\n\n@JsType\npublic class PublicKeyHash extends Cid implements Cborable {\n    public static final int MAX_KEY_HASH_SIZE = 1024;\n    public static final PublicKeyHash NULL = new PublicKeyHash(new Cid(1, Codec.DagCbor, Type.sha2_256, new byte[32]));\n\n    public final Cid target;\n\n    public PublicKeyHash(Cid target) {\n        super(target.version, target.codec, target.type, target.getHash());\n        if (! isSafe(target))\n            throw new IllegalStateException(\"Must use a safe hash for a public key!\");\n        this.target = target;\n    }\n\n    public static boolean isSafe(Multihash h) {\n        return h.type == Type.sha2_256 || h.type == Type.id; // we can add other hashes later\n    }\n\n    @Override\n    public byte[] toBytes() {\n        return target.toBytes();\n    }\n\n    @Override\n    public String toString() {\n        return target.toString();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        return o instanceof PublicKeyHash && target.equals(((PublicKeyHash) o).target);\n    }\n\n    @Override\n    public int hashCode() {\n        return target.hashCode();\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborMerkleLink(target);\n    }\n\n    public static PublicKeyHash decode(byte[] raw) {\n        return new PublicKeyHash(Cid.cast(raw));\n    }\n\n    public static PublicKeyHash fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMerkleLink))\n            throw new IllegalStateException(\"Invalid cbor for PublicKeyHash! \" + cbor);\n        return new PublicKeyHash((Cid)((CborObject.CborMerkleLink) cbor).target);\n    }\n\n    public static PublicKeyHash fromString(String cid) {\n        return new PublicKeyHash(Cid.decode(cid));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/hash/ScryptJS.java",
    "content": "package peergos.shared.crypto.hash;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.concurrent.CompletableFuture;\n\npublic class ScryptJS implements Hasher {\n\n    NativeScryptJS scriptJS = new NativeScryptJS();\n    \n    @Override\n    public CompletableFuture<byte[]> hashToKeyBytes(String username, String password, SecretGenerationAlgorithm algorithm) {\n        return scriptJS.hashToKeyBytes(username, password, algorithm);\n    }\n\n    @Override\n    public CompletableFuture<ProofOfWork> generateProofOfWork(int difficulty, byte[] data) {\n        return scriptJS.generateProofOfWork(difficulty, data);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> sha256(byte[] input) {\n        return scriptJS.sha256(input);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> hmacSha256(byte[] secretKey, byte[] message) {\n        return scriptJS.hmacSha256(secretKey, message);\n    }\n\n    @Override\n    public byte[] blake2b(byte[] input, int outputBytes) {\n        return scriptJS.blake2b(input, outputBytes);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> sha256Section(AsyncReader reader, long start, long end) {\n        if (reader instanceof BrowserFileReader) {\n            JSFileReader jsReader = ((BrowserFileReader) reader).getReader();\n            return scriptJS.sha256FileSection(jsReader,\n                    (int)(start >> 32), (int)start, (int)(end >> 32), (int)end);\n        }\n        return Hasher.super.sha256Section(reader, start, end);\n    }\n\n    @Override\n    @SuppressWarnings(\"unusable-by-js\")\n    public CompletableFuture<Multihash> hashFromStream(AsyncReader stream, long length) {\n        return scriptJS.streamSha256(stream, (int) length)\n                .thenApply(h -> new Multihash(Multihash.Type.sha2_256, h));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/hash/Sha256.java",
    "content": "package peergos.shared.crypto.hash;\n\n/* Sha256.java --\nCopyright (C) 2003, 2006 Free Software Foundation, Inc.\n\n        This file is a part of GNU Classpath.\n\n        GNU Classpath is free software; you can redistribute it and/or modify\n        it under the terms of the GNU General Public License as published by\n        the Free Software Foundation; either version 2 of the License, or (at\n        your option) any later version.\n\n        GNU Classpath is distributed in the hope that it will be useful, but\n        WITHOUT ANY WARRANTY; without even the implied warranty of\n        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n        General Public License for more details.\n\n        You should have received a copy of the GNU General Public License\n        along with GNU Classpath; if not, write to the Free Software\n        Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301\n        USA\n\n        Linking this library statically or dynamically with other modules is\n        making a combined work based on this library.  Thus, the terms and\n        conditions of the GNU General Public License cover the whole\n        combination.\n\n        As a special exception, the copyright holders of this library give you\n        permission to link this library with independent modules to produce an\n        executable, regardless of the license terms of these independent\n        modules, and to copy and distribute the resulting executable under\n        terms of your choice, provided that you also meet, for each linked\n        independent module, the terms and conditions of the license of that\n        module.  An independent module is a module which is not derived from\n        or based on this library.  If you modify this library, you may extend\n        this exception to your version of the library, but you are not\n        obligated to do so.  If you do not wish to do so, delete this\n        exception statement from your version.  */\n\n\n\nimport peergos.shared.util.*;\n\n/**\n * Implementation of SHA2-1 [SHA-256] per the IETF Draft Specification.\n * <p>\n * References:\n * <ol>\n *    <li><a href=\"http://ftp.ipv4.heanet.ie/pub/ietf/internet-drafts/draft-ietf-ipsec-ciph-aes-cbc-03.txt\">\n *    Descriptions of SHA-256, SHA-384, and SHA-512</a>,</li>\n *    <li>http://csrc.nist.gov/cryptval/shs/sha256-384-512.pdf</li>\n * </ol>\n */\npublic class Sha256\n        extends BaseHash\n{\n    private static final int[] k = {\n            0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,\n            0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n            0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,\n            0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n            0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,\n            0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n            0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,\n            0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n            0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,\n            0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n            0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,\n            0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n            0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,\n            0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n            0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,\n            0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2\n    };\n\n    private static final int BLOCK_SIZE = 64; // inner block size in bytes\n\n    private static final String DIGEST0 =\n            \"BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD\";\n\n    private static final int[] w = new int[64];\n\n    /** caches the result of the correctness test, once executed. */\n    private static Boolean valid;\n\n    /** 256-bit interim result. */\n    private int h0, h1, h2, h3, h4, h5, h6, h7;\n\n    /** Trivial 0-arguments constructor. */\n    public Sha256()\n    {\n        super(\"SHA-256\", 32, BLOCK_SIZE);\n    }\n\n    /**\n     * Private constructor for cloning purposes.\n     *\n     * @param md the instance to clone.\n     */\n    private Sha256(Sha256 md)\n    {\n        this();\n\n        this.h0 = md.h0;\n        this.h1 = md.h1;\n        this.h2 = md.h2;\n        this.h3 = md.h3;\n        this.h4 = md.h4;\n        this.h5 = md.h5;\n        this.h6 = md.h6;\n        this.h7 = md.h7;\n        this.count = md.count;\n        System.arraycopy(md.buffer, 0, this.buffer, 0, md.buffer.length);\n    }\n\n    public static final int[] G(int hh0, int hh1, int hh2, int hh3, int hh4,\n                                int hh5, int hh6, int hh7, byte[] in, int offset)\n    {\n        return sha(hh0, hh1, hh2, hh3, hh4, hh5, hh6, hh7, in, offset);\n    }\n\n    public Object clone()\n    {\n        return new Sha256(this);\n    }\n\n    protected void transform(byte[] in, int offset)\n    {\n        int[] result = sha(h0, h1, h2, h3, h4, h5, h6, h7, in, offset);\n        h0 = result[0];\n        h1 = result[1];\n        h2 = result[2];\n        h3 = result[3];\n        h4 = result[4];\n        h5 = result[5];\n        h6 = result[6];\n        h7 = result[7];\n    }\n\n    protected byte[] padBuffer()\n    {\n        int n = (int)(count % BLOCK_SIZE);\n        int padding = (n < 56) ? (56 - n) : (120 - n);\n        byte[] result = new byte[padding + 8];\n        // padding is always binary 1 followed by binary 0s\n        result[0] = (byte) 0x80;\n        // save number of bits, casting the long to an array of 8 bytes\n        long bits = count << 3;\n        result[padding++] = (byte)(bits >>> 56);\n        result[padding++] = (byte)(bits >>> 48);\n        result[padding++] = (byte)(bits >>> 40);\n        result[padding++] = (byte)(bits >>> 32);\n        result[padding++] = (byte)(bits >>> 24);\n        result[padding++] = (byte)(bits >>> 16);\n        result[padding++] = (byte)(bits >>> 8);\n        result[padding  ] = (byte) bits;\n        return result;\n    }\n\n    protected byte[] getResult()\n    {\n        return new byte[] {\n                (byte)(h0 >>> 24), (byte)(h0 >>> 16), (byte)(h0 >>> 8), (byte) h0,\n                (byte)(h1 >>> 24), (byte)(h1 >>> 16), (byte)(h1 >>> 8), (byte) h1,\n                (byte)(h2 >>> 24), (byte)(h2 >>> 16), (byte)(h2 >>> 8), (byte) h2,\n                (byte)(h3 >>> 24), (byte)(h3 >>> 16), (byte)(h3 >>> 8), (byte) h3,\n                (byte)(h4 >>> 24), (byte)(h4 >>> 16), (byte)(h4 >>> 8), (byte) h4,\n                (byte)(h5 >>> 24), (byte)(h5 >>> 16), (byte)(h5 >>> 8), (byte) h5,\n                (byte)(h6 >>> 24), (byte)(h6 >>> 16), (byte)(h6 >>> 8), (byte) h6,\n                (byte)(h7 >>> 24), (byte)(h7 >>> 16), (byte)(h7 >>> 8), (byte) h7 };\n    }\n\n    protected void resetContext()\n    {\n        // magic SHA-256 initialisation constants\n        h0 = 0x6a09e667;\n        h1 = 0xbb67ae85;\n        h2 = 0x3c6ef372;\n        h3 = 0xa54ff53a;\n        h4 = 0x510e527f;\n        h5 = 0x9b05688c;\n        h6 = 0x1f83d9ab;\n        h7 = 0x5be0cd19;\n    }\n\n    public boolean selfTest()\n    {\n        if (valid == null)\n        {\n            Sha256 md = new Sha256();\n            md.update((byte) 0x61); // a\n            md.update((byte) 0x62); // b\n            md.update((byte) 0x63); // c\n            String result = ArrayOps.bytesToHex(md.digest());\n            valid = Boolean.valueOf(DIGEST0.equals(result));\n        }\n        return valid.booleanValue();\n    }\n\n    private static synchronized final int[] sha(int hh0, int hh1, int hh2,\n                                                int hh3, int hh4, int hh5,\n                                                int hh6, int hh7, byte[] in,\n                                                int offset)\n    {\n        int A = hh0;\n        int B = hh1;\n        int C = hh2;\n        int D = hh3;\n        int E = hh4;\n        int F = hh5;\n        int G = hh6;\n        int H = hh7;\n        int r, T, T2;\n        for (r = 0; r < 16; r++)\n            w[r] = (in[offset++]         << 24\n                    | (in[offset++] & 0xFF) << 16\n                    | (in[offset++] & 0xFF) << 8\n                    | (in[offset++] & 0xFF));\n        for (r = 16; r < 64; r++)\n        {\n            T =  w[r -  2];\n            T2 = w[r - 15];\n            w[r] = ((((T >>> 17) | (T << 15)) ^ ((T >>> 19) | (T << 13)) ^ (T >>> 10))\n                    + w[r - 7]\n                    + (((T2 >>> 7) | (T2 << 25))\n                    ^ ((T2 >>> 18) | (T2 << 14))\n                    ^ (T2 >>> 3)) + w[r - 16]);\n        }\n        for (r = 0; r < 64; r++)\n        {\n            T = (H\n                    + (((E >>> 6) | (E << 26))\n                    ^ ((E >>> 11) | (E << 21))\n                    ^ ((E >>> 25) | (E << 7)))\n                    + ((E & F) ^ (~E & G)) + k[r] + w[r]);\n            T2 = ((((A >>> 2) | (A << 30))\n                    ^ ((A >>> 13) | (A << 19))\n                    ^ ((A >>> 22) | (A << 10))) + ((A & B) ^ (A & C) ^ (B & C)));\n            H = G;\n            G = F;\n            F = E;\n            E = D + T;\n            D = C;\n            C = B;\n            B = A;\n            A = T + T2;\n        }\n        return new int[] {\n                hh0 + A, hh1 + B, hh2 + C, hh3 + D,\n                hh4 + E, hh5 + F, hh6 + G, hh7 + H };\n    }\n}"
  },
  {
    "path": "src/peergos/shared/crypto/password/PasswordProtected.java",
    "content": "package peergos.shared.crypto.password;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic class PasswordProtected {\n\n    public static SecretGenerationAlgorithm getDefault() {\n        return new ScryptGenerator(ScryptGenerator.LOGIN_MEMORY_COST, 8, 1, 32, \"\");\n    }\n\n    public static Cborable encryptWithPassword(byte[] cleartext,\n                                               String password,\n                                               Hasher hasher,\n                                               Salsa20Poly1305 provider,\n                                               SafeRandom random) {\n        return encryptWithPassword(cleartext, password, getDefault(), hasher, provider, random);\n    }\n\n    public static Cborable encryptWithPassword(byte[] cleartext,\n                                               String password,\n                                               SecretGenerationAlgorithm algorithm,\n                                               Hasher hasher,\n                                               Salsa20Poly1305 provider,\n                                               SafeRandom random) {\n        List<Cborable> elements = new ArrayList<>();\n        byte[] saltBytes = random.randomBytes(32);\n        String salt = ArrayOps.bytesToHex(saltBytes);\n\n        try {\n            byte[] derivedKeyBytes = hasher.hashToKeyBytes(salt, password, algorithm).join();\n            SymmetricKey key = new TweetNaClKey(derivedKeyBytes, false, provider, random);\n            byte[] nonce = key.createNonce();\n            byte[] cipherText = key.encrypt(cleartext, nonce);\n            elements.add(algorithm.toCbor());\n            elements.add(new CborObject.CborByteArray(saltBytes));\n            elements.add(new CborObject.CborByteArray(nonce));\n            elements.add(new CborObject.CborByteArray(cipherText));\n            return new CborObject.CborList(elements);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n\n    public static byte[] decryptWithPassword(Cborable wrappedCipherText,\n                                             String password,\n                                             Hasher hasher,\n                                             Salsa20Poly1305 provider,\n                                             SafeRandom random) {\n        if (! (wrappedCipherText instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for SecretSigningKey! \" + wrappedCipherText);\n        List<? extends Cborable> list = ((CborObject.CborList) wrappedCipherText).value;\n\n        SecretGenerationAlgorithm algorithm = SecretGenerationAlgorithm.fromCbor(list.get(0));\n        String salt = ArrayOps.bytesToHex(((CborObject.CborByteArray) list.get(1)).value);\n        byte[] nonce = ((CborObject.CborByteArray) list.get(2)).value;\n        byte[] cipherText = ((CborObject.CborByteArray) list.get(3)).value;\n\n        try {\n            byte[] derivedKeyBytes = hasher.hashToKeyBytes(salt, password, algorithm).join();\n\n            SymmetricKey key = new TweetNaClKey(derivedKeyBytes, false, provider, random);\n            return key.decrypt(cipherText, nonce);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/random/JSNaCl.java",
    "content": "package peergos.shared.crypto.random;\n\nimport jsinterop.annotations.JsType;\n\nimport java.util.concurrent.CompletableFuture;\n\n@JsType(namespace = \"tweetNaCl\", isNative = true)\npublic class JSNaCl {\n    native public byte[] randombytes(int len);\n\n    native public byte[] secretbox(byte[] data, byte[] nonce, byte[] key);\n    native public byte[] secretbox_open(byte[] cipher, byte[] nonce, byte[] key);\n\n    native public CompletableFuture<byte[]> crypto_sign_open(byte[] signed, byte[] publicSigningKey);\n    native public CompletableFuture<byte[]> crypto_sign(byte[] message, byte[] secretSigningKey);\n    native public byte[][] crypto_sign_keypair(byte[] pk, byte[] sk);\n\n    native public byte[] crypto_box_open(byte[] cipher, byte[] nonce, byte[] theirPublicBoxingKey, byte[] secretBoxingKey);\n    native public byte[] crypto_box(byte[] message, byte[] nonce, byte[] theirPublicBoxingKey, byte[] ourSecretBoxingKey);\n    native public byte[] crypto_box_keypair(byte[] pk, byte[] sk);\n\n    native public CompletableFuture<byte[][]> generateMlkemKeyPair();\n    native public CompletableFuture<byte[][]> encapsulate(byte[] publicKeyBytes);\n    native public CompletableFuture<byte[]> decapsulate(byte[] cipherTextBytes, byte[] secretKeyBytes);\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/random/SafeRandom.java",
    "content": "package peergos.shared.crypto.random;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.crypto.*;\n\n@JsType\npublic interface SafeRandom {\n\n    void randombytes(byte[] b, int offset, int len);\n\n    default byte[] randomBytes(int len) {\n        byte[] res = new byte[len];\n        randombytes(res, 0, len);\n        return res;\n    }\n\n    class Javascript implements SafeRandom {\n        JSNaCl scriptJS = new JSNaCl();\n\n        @Override\n        public void randombytes(byte[] b, int offset, int len) {\n            byte[] r = scriptJS.randombytes(len);\n            System.arraycopy(r, 0, b, offset, len);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/symmetric/Salsa20Poly1305.java",
    "content": "package peergos.shared.crypto.symmetric;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.random.JSNaCl;\n\nimport java.util.concurrent.CompletableFuture;\n\npublic interface Salsa20Poly1305 {\n\n    byte[] secretbox(byte[] data, byte[] nonce, byte[] key);\n\n    byte[] secretbox_open(byte[] cipher, byte[] nonce, byte[] key);\n\n    class Javascript implements Salsa20Poly1305 {\n        JSNaCl scriptJS = new JSNaCl();\n\n        @Override\n        public byte[] secretbox(byte[] data, byte[] nonce, byte[] key) {\n            return scriptJS.secretbox(data, nonce, key);\n        }\n\n        @Override\n        public byte[] secretbox_open(byte[] cipher, byte[] nonce, byte[] key) {\n            return scriptJS.secretbox_open(cipher, nonce, key);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/symmetric/SymmetricKey.java",
    "content": "package peergos.shared.crypto.symmetric;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic interface SymmetricKey extends Cborable\n{\n    Map<Integer, Type> byValue = new HashMap<>();\n    enum Type {\n        TweetNaCl(0x1);\n\n        public final int value;\n\n        Type(int value) {\n            this.value = value;\n            byValue.put(value, this);\n        }\n\n        public static Type byValue(int val) {\n            return byValue.get(val);\n        }\n    }\n\n    Map<Type, Salsa20Poly1305> PROVIDERS = new HashMap<>();\n\n    Map<Type, SafeRandom> RNG_PROVIDERS = new HashMap<>();\n\n    static void addProvider(Type t, Salsa20Poly1305 provider) {\n        PROVIDERS.put(t, provider);\n    }\n\n    static void setRng(Type t, SafeRandom rng) {\n        RNG_PROVIDERS.put(t, rng);\n    }\n\n    Type type();\n\n    byte[] getKey();\n\n    @JsMethod\n    byte[] encrypt(byte[] data, byte[] nonce);\n\n    @JsMethod\n    byte[] decrypt(byte[] data, byte[] nonce);\n\n    @JsMethod\n    byte[] createNonce();\n\n    @JsMethod\n    boolean isDirty();\n\n    @JsMethod\n    SymmetricKey makeDirty();\n\n    @JsMethod\n    static SymmetricKey fromByteArray(byte[] raw) {\n        return fromCbor(CborObject.fromByteArray(raw));\n    }\n\n    @JsMethod\n    default byte[] toByteArray() {\n        return serialize();\n    }\n\n    static SymmetricKey fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for SymmetricKey! \" + cbor);\n        CborObject.CborLong type = (CborObject.CborLong) ((CborObject.CborList) cbor).value.get(0);\n        Type t = Type.byValue((int) type.value);\n        switch (t) {\n            case TweetNaCl:\n                return TweetNaClKey.fromCbor(cbor, PROVIDERS.get(t), RNG_PROVIDERS.get(t));\n            default: throw new IllegalStateException(\"Unknown Symmetric Key type: \"+t.name());\n        }\n    }\n\n    @JsMethod\n    static SymmetricKey random() {\n        return TweetNaClKey.random(PROVIDERS.get(Type.TweetNaCl), RNG_PROVIDERS.get(Type.TweetNaCl));\n    }\n\n    static SymmetricKey createNull() {\n        return new TweetNaClKey(new byte[TweetNaClKey.KEY_BYTES], false, PROVIDERS.get(Type.TweetNaCl), RNG_PROVIDERS.get(Type.TweetNaCl));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/crypto/symmetric/TweetNaClKey.java",
    "content": "package peergos.shared.crypto.symmetric;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.random.*;\n\nimport java.util.*;\n\npublic class TweetNaClKey implements SymmetricKey\n{\n    public static final int KEY_BYTES = 32;\n    public static final int NONCE_BYTES = 24;\n\n    private final byte[] secretKey;\n    private final boolean isDirty;\n    private final Salsa20Poly1305 implementation;\n    private final SafeRandom random;\n\n    public TweetNaClKey(byte[] secretKey, boolean isDirty, Salsa20Poly1305 implementation, SafeRandom random)\n    {\n        if (secretKey.length != KEY_BYTES)\n            throw new IllegalStateException(\"Incorrect key size! (\"+secretKey.length+\")\");\n        this.secretKey = secretKey;\n        this.isDirty = isDirty;\n        this.implementation = implementation;\n        this.random = random;\n    }\n\n    public Type type() {\n        return Type.TweetNaCl;\n    }\n\n    public byte[] getKey()\n    {\n        return secretKey;\n    }\n\n    public boolean isDirty() {\n        return isDirty;\n    }\n\n    public SymmetricKey makeDirty() {\n        return new TweetNaClKey(secretKey, true, implementation, random);\n    }\n\n    public byte[] encrypt(byte[] data, byte[] nonce)\n    {\n        return encrypt(secretKey, data, nonce, implementation);\n    }\n\n    public byte[] decrypt(byte[] data, byte[] nonce)\n    {\n        return decrypt(secretKey, data, nonce, implementation);\n    }\n\n    private static byte[] encrypt(byte[] key, byte[] data, byte[] nonce, Salsa20Poly1305 implementation)\n    {\n        return implementation.secretbox(data, nonce, key);\n    }\n\n    private static byte[] decrypt(byte[] key, byte[] cipher, byte[] nonce, Salsa20Poly1305 implementation)\n    {\n        return implementation.secretbox_open(cipher, nonce, key);\n    }\n\n    public byte[] createNonce()\n    {\n        byte[] res = new byte[NONCE_BYTES];\n        random.randombytes(res, 0, res.length);\n        return res;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        TweetNaClKey that = (TweetNaClKey) o;\n\n        if (isDirty != that.isDirty) return false;\n        return Arrays.equals(secretKey, that.secretKey);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Arrays.hashCode(secretKey);\n        result = 31 * result + (isDirty ? 1 : 0);\n        return result;\n    }\n\n    public CborObject toCbor() {\n        return  new CborObject.CborList(Arrays.asList(\n                new CborObject.CborLong(type().value),\n                new CborObject.CborByteArray(secretKey),\n                new CborObject.CborBoolean(isDirty)));\n    }\n\n    public static TweetNaClKey fromCbor(Cborable cbor, Salsa20Poly1305 provider, SafeRandom random) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for PublicBoxingKey! \" + cbor);\n        CborObject.CborByteArray secretKey = (CborObject.CborByteArray) ((CborObject.CborList) cbor).value.get(1);\n        CborObject.CborBoolean isDirty = (CborObject.CborBoolean) ((CborObject.CborList) cbor).value.get(2);\n        return new TweetNaClKey(secretKey.value, isDirty.value, provider, random);\n    }\n\n    public static TweetNaClKey random(Salsa20Poly1305 provider, SafeRandom random)\n    {\n        byte[] key = new byte[KEY_BYTES];\n        random.randombytes(key, 0, KEY_BYTES);\n        return new TweetNaClKey(key, false, provider, random);\n    }\n}"
  },
  {
    "path": "src/peergos/shared/display/Content.java",
    "content": "package peergos.shared.display;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic interface Content extends Cborable {\n\n    @JsMethod\n    String inlineText();\n\n    @JsMethod\n    Optional<FileRef> reference();\n\n    static Content fromCbor(Cborable cbor) {\n        if (cbor instanceof CborObject.CborString)\n            return new Text(((CborObject.CborString) cbor).value);\n        if (cbor instanceof CborObject.CborMap) {\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n            String type = m.getString(\"t\");\n            switch (type) {\n                case \"Ref\":\n                    return new Reference(m.get(\"r\", FileRef::fromCbor));\n                default:\n                    throw new IllegalStateException(\"Unknown content type in Social Post: \" + type);\n            }\n        }\n        throw new IllegalStateException(\"Unknown Content type in Social Post\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/display/FileRef.java",
    "content": "package peergos.shared.display;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\n@JsType\npublic class FileRef implements Cborable {\n    public final String path;\n    public final AbsoluteCapability cap;\n    public final Multihash contentHash;\n\n    @JsConstructor\n    public FileRef(String path, AbsoluteCapability cap, Multihash contentHash) {\n        if (path.contains(\"/../\") || path.startsWith(\"../\"))\n            throw new IllegalStateException(\"Invalid path containing /../\");\n        this.path = path;\n        this.cap = cap;\n        this.contentHash = contentHash;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"p\", new CborObject.CborString(path));\n        state.put(\"c\", cap);\n        state.put(\"h\", new CborObject.CborMerkleLink(contentHash));\n\n        return CborObject.CborMap.build(state);\n    }\n\n    public String toJson() {\n        return \"{\\\"path\\\":\\\"\" + path + \"\\\", \\\"cap\\\":\\\"\" + cap.toLink() + \"\\\", \\\"contentHash\\\":\\\"\" + contentHash.toString() + \"\\\"}\";\n    }\n\n    public static FileRef fromJson(String json) {\n        Map<String, Object> jsonMap = (Map) JSONParser.parse(json);\n        String path = (String) jsonMap.get(\"path\");\n        String capStr = (String) jsonMap.get(\"cap\");\n        String contentHashStr = (String) jsonMap.get(\"contentHash\");\n        AbsoluteCapability cap = AbsoluteCapability.fromLink(capStr);\n        Multihash contentHash = Multihash.fromBase58(contentHashStr);\n        return new FileRef(path, cap, contentHash);\n    }\n\n    public static FileRef fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        String path = m.getString(\"p\");\n        AbsoluteCapability cap = m.get(\"c\", AbsoluteCapability::fromCbor);\n        Multihash contentHash = m.getMerkleLink(\"h\");\n        return new FileRef(path, cap, contentHash);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        FileRef ref = (FileRef) o;\n        return Objects.equals(path, ref.path) && Objects.equals(cap, ref.cap) && Objects.equals(contentHash, ref.contentHash);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(path, cap, contentHash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/display/Reference.java",
    "content": "package peergos.shared.display;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\n@JsType\npublic\nclass Reference implements Content {\n    public final FileRef ref;\n\n    public Reference(FileRef ref) {\n        this.ref = ref;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"t\", new CborObject.CborString(\"Ref\"));\n        state.put(\"r\", ref);\n        return CborObject.CborMap.build(state);\n    }\n\n    @Override\n    public String inlineText() {\n        return \"\";\n    }\n\n    @Override\n    public Optional<FileRef> reference() {\n        return Optional.of(ref);\n    }\n\n    @Override\n    public String toString() {\n        return \"REFERENCE(\" + ref.path + \")\";\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        peergos.shared.display.Reference reference = (peergos.shared.display.Reference) o;\n        return Objects.equals(ref, reference.ref);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(ref);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/display/Text.java",
    "content": "package peergos.shared.display;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\n@JsType\npublic\nclass Text implements Content {\n    public final String content;\n\n    public Text(String content) {\n        this.content = content;\n    }\n\n    @Override\n    public String inlineText() {\n        return content;\n    }\n\n    @Override\n    public Optional<FileRef> reference() {\n        return Optional.empty();\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborString(content);\n    }\n\n    @Override\n    public String toString() {\n        return content;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        peergos.shared.display.Text text = (peergos.shared.display.Text) o;\n        return Objects.equals(content, text.content);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(content);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/email/Attachment.java",
    "content": "package peergos.shared.email;\n\nimport jsinterop.annotations.JsConstructor;\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.display.FileRef;\n\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\n@JsType\npublic class Attachment implements Cborable {\n\n    public final String filename;\n    public final int size;\n    public final String type;\n    public final String uuid;\n\n    @JsConstructor\n    public Attachment(String filename, int size,\n                      String type, String uuid\n    ) {\n        this.filename = filename;\n        this.size = size;\n        this.type = type;\n        this.uuid = uuid;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"f\", new CborObject.CborString(filename));\n        state.put(\"s\", new CborObject.CborLong(size));\n        state.put(\"t\", new CborObject.CborString(type));\n        state.put(\"u\", new CborObject.CborString(uuid));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static Attachment fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        String filename = m.getString(\"f\");\n        int size = (int) m.getLong(\"s\");\n        String type = m.getString(\"t\");\n        String uuid = m.getString(\"u\");\n        return new Attachment(filename, size, type, uuid);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/email/EmailClient.java",
    "content": "package peergos.shared.email;\n\nimport jsinterop.annotations.*;\nimport peergos.client.PathUtils;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.io.ipfs.api.JSONParser;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.SecretLink;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/**\n *  All email data is stored under $BASE = /$username/.apps/email/data/$ACCOUNT\n *  $ACCOUNT = 'default' until multiple email addresses are supported\n *  Emails are stored in #BASE/{inbox, sent, $custom...}\n *  Attachments are stored in $BASE/attachments\n *  The email bridge has write access to $BASE/pending\n *  Attachments are encrypted in $BASE/pending/inbox/attachments and $BASE/pending/sent/attachments\n *  Attachments are non encrypted in $BASE/pending/outbox/attachments\n *\n *  INCOMING:\n *  The bridge encrypts incoming emails and writes to $BASE/pending/inbox,\n *  putting attachments in $BASE/pending/inbox/attachments\n *  At this point a breach of the bridge server can't read anything.\n *  The client then decrypts and moves emails and attachments to $BASE/inbox and $BASE/attachments\n *\n *  OUTGOING:\n *  The client puts unencrypted emails and attachments in $BASE/pending/outbox and $BASE/pending/outbox/attachments\n *  The bridge sends each email from outbox and writes the encrypted result in $BASE/pending/sent and $BASE/pending/sent/attachments\n *  The client decrypts and moves emails and attachments from $BASE/pending/sent to $BASE/sent and $BASE/attachments\n *\n *  Attachments filenames are uuids. Email file names are uuids.\n *  The public key for the bridge to encrypt things to is at $BASE/pending/encryption.publickey.cbor,\n *  the full key pair is in $BASE/encryption.keypair.cbor\n *\n *  When emails/attachments are encrypted they will be in the format of SourcedAsymmetricCipherText\n *  This means we can only decrypt attachments that we can fit into RAM, but that should be fine as entire email (including all attachments) is limited\n *  to ~25 MiB anyway.\n */\npublic class EmailClient {\n    private static final String ENCRYPTION_KEYPAIR_PATH = \"encryption.keypair.cbor\";\n    private static final String PUBLIC_KEY_FILENAME = \"encryption.publickey.cbor\";\n    private static final String CLIENT_EMAIL_FILENAME = \"email.json\"; //Email bridge will write client's email address to this file\n\n    private final Crypto crypto;\n    private final BoxingKeyPair encryptionKeys;\n    private final App emailApp;\n\n    public EmailClient(App emailApp, Crypto crypto, BoxingKeyPair encryptionKeys) {\n        this.emailApp = emailApp;\n        this.crypto = crypto;\n        this.encryptionKeys = encryptionKeys;\n    }\n\n    private CompletableFuture<EmailMessage> decryptEmail(SourcedAsymmetricCipherText cipherText) {\n        return cipherText.decrypt(encryptionKeys.secretBoxingKey, EmailMessage::fromCbor);\n    }\n\n    private CompletableFuture<byte[]> decryptAttachment(SourcedAsymmetricCipherText cipherText) {\n        return cipherText.decrypt(encryptionKeys.secretBoxingKey, c -> ((CborObject.CborByteArray)c).value);\n    }\n\n    private CompletableFuture<Boolean> uploadForwardedAttachments(EmailMessage data) {\n        CompletableFuture<Boolean> future = peergos.shared.util.Futures.incomplete();\n        if (data.forwardingToEmail.isEmpty()) {\n            future.complete(true);\n        } else {\n            this.reduceMovingForwardedAttachments(data.forwardingToEmail.get().attachments, 0, future);\n        }\n        return future;\n    }\n    private CompletableFuture<Boolean> reduceMovingForwardedAttachments(List<Attachment>attachments, int index, CompletableFuture<Boolean> future) {\n        if (index >= attachments.size()) {\n            future.complete(true);\n            return future;\n        } else {\n            Attachment attachment = attachments.get(index);\n            String srcDirStr = \"default/attachments/\" + attachment.uuid;\n            Path srcFilePath = PathUtils.directoryToPath(srcDirStr.split(\"/\"));\n            return emailApp.readInternal(srcFilePath, null).thenCompose(bytes -> {\n                    String destDirStr = \"default/pending/outbox/attachments/\" + attachment.uuid;\n                    Path destFilePath = PathUtils.directoryToPath(destDirStr.split(\"/\"));\n                    return emailApp.writeInternal(destFilePath, bytes, null).thenCompose(res ->\n                        reduceMovingForwardedAttachments(attachments, index +1, future)\n                    );\n            });\n        }\n    }\n\n    @JsMethod\n    public CompletableFuture<String> uploadAttachment(byte[] attachment) {\n        String uuid = UUID.randomUUID().toString();\n        Path outboundAttachment = PathUtil.get(\"default\", \"pending\", \"outbox\", \"attachments\", uuid);\n        return emailApp.writeInternal(outboundAttachment, attachment, null)\n                .thenApply(x -> uuid);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> send(EmailMessage msg) {\n        return uploadForwardedAttachments(msg).thenCompose(res ->\n            saveEmail(\"pending/outbox\", msg, msg.id)\n        );\n    }\n\n    @JsMethod\n    public CompletableFuture<List<EmailMessage>> getNewIncoming() {\n        Path inbox = PathUtil.get(\"default\", \"pending\", \"inbox\");\n        return listFiles(inbox);\n    }\n\n    @JsMethod\n    public CompletableFuture<List<EmailMessage>> getNewSent() {\n        Path inbox = PathUtil.get(\"default\", \"pending\", \"sent\");\n        return listFiles(inbox);\n    }\n\n    @JsMethod\n    public CompletableFuture<byte[]> getAttachment(String uid) {\n        Path attachment = PathUtil.get(\"default\", \"attachments\", uid);\n        return emailApp.readInternal(attachment, null);\n    }\n\n    public CompletableFuture<List<EmailMessage>> listFiles(Path internalPath) {\n        return emailApp.dirInternal(internalPath, null)\n                .thenApply(filenames -> filenames.stream().filter(n -> n.endsWith(\".cbor\")).collect(Collectors.toList()))\n                .thenCompose(filenames -> Futures.combineAllInOrder(filenames.stream()\n                        .map(n -> emailApp.readInternal(internalPath.resolve(n), null)\n                                .thenApply(bytes -> SourcedAsymmetricCipherText.fromCbor(CborObject.fromByteArray(bytes)))\n                                .thenCompose(this::decryptEmail)).collect(Collectors.toList())));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> moveToPrivateSent(EmailMessage emailMessage) {\n        CompletableFuture<Boolean> future = Futures.incomplete();\n        return reduceMovingAttachmentsToFolder(emailMessage.attachments, \"sent\", 0, future).thenCompose( res ->\n            moveToPrivateDir(\"default\", emailMessage, PathUtil.get(\"default\", \"pending\", \"sent\").resolve(emailMessage.id + \".cbor\"))\n        );\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> moveToPrivateInbox(EmailMessage emailMessage) {\n        CompletableFuture<Boolean> future = Futures.incomplete();\n        return reduceMovingAttachmentsToFolder(emailMessage.attachments, \"inbox\",0, future).thenCompose( res ->\n             moveToPrivateDir(\"default\", emailMessage, PathUtil.get(\"default\", \"pending\", \"inbox\").resolve(emailMessage.id + \".cbor\"))\n        );\n    }\n\n    private CompletableFuture<Boolean> reduceMovingAttachmentsToFolder(List<Attachment> attachments,\n                                                                       String folder,\n                                                                       int index,\n                                                                       CompletableFuture<Boolean> future) {\n        if (index >= attachments.size()) {\n            future.complete(true);\n            return future;\n        } else {\n            Attachment attachment = attachments.get(index);\n            Path srcFilePath = PathUtil.get(\"default\", \"pending\", folder, \"attachments\", attachment.uuid);\n            Path destFilePath = PathUtil.get(\"default\", \"attachments\", attachment.uuid);\n\n            return Futures.asyncExceptionally(() -> emailApp.readInternal(srcFilePath, null).thenCompose(bytes -> {\n                        SourcedAsymmetricCipherText cipherText = SourcedAsymmetricCipherText.fromCbor(CborObject.fromByteArray(bytes));\n                        return decryptAttachment(cipherText)\n                                .thenCompose(decryptedAttachment -> emailApp.writeInternal(destFilePath, decryptedAttachment, null))\n                                .thenCompose(res -> emailApp.deleteInternal(srcFilePath, null).thenCompose(bool ->\n                                        reduceMovingAttachmentsToFolder(attachments, folder, index + 1, future)\n                                )\n                                );\n                    }),\n                    // If the read failed because it has already been copied, we have nothing to do\n                    t -> emailApp.readInternal(destFilePath, null).thenApply(x -> true));\n        }\n    }\n\n    public CompletableFuture<Boolean> moveToPrivateDir(String account, EmailMessage m, Path original) {\n        Path dirAndFile = original.subpath(original.getNameCount() - 2, original.getNameCount());\n        Path dest = PathUtil.get(account).resolve(dirAndFile);\n        // TODO make this move atomic\n        return emailApp.writeInternal(dest, m.serialize(), null)\n                .thenCompose(b -> emailApp.deleteInternal(original, null));\n    }\n\n    private CompletableFuture<Boolean> saveEmail(String folder, EmailMessage email, String id) {\n        Path filePath = PathUtil.get(\"default\", folder, id + \".cbor\");\n        return emailApp.writeInternal(filePath, email.serialize(), null);\n    }\n\n    /** Setup all the necessary directories, generate key pair, and store public key separately for bridge to read\n     *  N.B. The pending directory still needs to be shared with the email user after initialization.\n     *\n     * @param crypto\n     * @param emailApp\n     * @return\n     */\n    public static CompletableFuture<EmailClient> initialise(Crypto crypto, App emailApp) {\n        List<String> dirs = Arrays.asList(\"inbox\",\"sent\",\"pending\", \"attachments\",\n                \"pending/inbox\", \"pending/outbox\", \"pending/sent\",\n                \"pending/inbox/attachments\", \"pending/outbox/attachments\", \"pending/sent/attachments\");\n        String account = \"default\";\n        return Futures.reduceAll(dirs, true,\n                (b, d) -> emailApp.createDirectoryInternal(PathUtil.get(account, d), null),\n                (a, b) -> a && b).thenCompose(x -> {\n            BoxingKeyPair encryptionKeys = BoxingKeyPair.randomCurve25519(crypto.random, crypto.boxer);\n            return emailApp.writeInternal(PathUtil.get(account, ENCRYPTION_KEYPAIR_PATH), encryptionKeys.serialize(), null)\n                    .thenCompose(b -> emailApp.writeInternal(PathUtil.get(account, \"pending\", PUBLIC_KEY_FILENAME),\n                            encryptionKeys.publicBoxingKey.serialize(), null))\n                    .thenApply(b -> new EmailClient(emailApp, crypto, encryptionKeys));\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<String>> getEmailAddress() {\n        Path relativeEmailPath = PathUtil.get(\"default\", \"pending\", CLIENT_EMAIL_FILENAME);\n        return emailApp.readInternal(relativeEmailPath, null).thenApply(data -> {\n            Map<String, String> props = (Map<String, String>) JSONParser.parse(new String(data));\n            String email = props.get(\"email\");\n            return Optional.of(email);\n        }).exceptionally(t -> Optional.empty());\n    }\n\n    @JsMethod\n    public static CompletableFuture<EmailClient> load(App emailApp, Crypto crypto) {\n        String account = \"default\";\n        return emailApp.dirInternal(PathUtil.get(account), null)\n                .thenCompose(children -> {\n                    if (children.contains(ENCRYPTION_KEYPAIR_PATH)) {\n                        return emailApp.readInternal(PathUtil.get(account, ENCRYPTION_KEYPAIR_PATH), null)\n                                .thenApply(bytes -> BoxingKeyPair.fromCbor(CborObject.fromByteArray(bytes)))\n                                .thenApply(keys -> new EmailClient(emailApp, crypto, keys));\n                    }\n\n                    return initialise(crypto, emailApp);\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<SecretLink> connectToBridge(UserContext context) {\n        Path pendingDir = App.getDataDir(\"email\", context.username)\n                .resolve(PathUtil.get(\"default\", \"pending\"));\n\n        return context.createSecretLink(pendingDir.toString(), true, Optional.empty(),\n                Optional.empty(), \"\", false)\n                .thenCompose(linkProps -> context.getUserRoot()\n                        .thenApply(userRoot -> linkProps.toLink(userRoot.owner())));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/email/EmailMessage.java",
    "content": "package peergos.shared.email;\n\nimport jsinterop.annotations.JsConstructor;\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.user.fs.MimeTypes;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@JsType\npublic class EmailMessage implements Cborable {\n\n    private static final String VERSION_1 = \"1\";\n\n    public final String id;\n    public final String msgId;\n    public final String from;\n    public final String subject;\n    public final List<String> to;\n    public final List<String> cc;\n    public final List<String> bcc;\n    public final String content;\n    public final boolean unread;\n    public final boolean star;\n    public final LocalDateTime created;\n    public final List<Attachment> attachments;\n    public final String icalEvent;\n    public final Optional<String> sendError;\n    public final Optional<EmailMessage> replyingToEmail;\n    public final Optional<EmailMessage> forwardingToEmail;\n\n    @JsConstructor\n    public EmailMessage(String id, String msgId, String from, String subject, LocalDateTime created,\n                        List<String> to, List<String> cc, List<String> bcc,\n                        String content, boolean unread, boolean star,\n                        List<Attachment> attachments,\n                        String icalEvent, Optional<EmailMessage> replyingToEmail, Optional<EmailMessage> forwardingToEmail,\n                        Optional<String> sendError\n    ) {\n        this.id = id;\n        this.msgId = msgId;\n        this.from = from;\n        this.subject = subject;\n        this.created = created;\n        this.to = to;\n        this.cc = cc;\n        this.bcc = bcc;\n        this.content = content;\n        this.unread = unread;\n        this.star = star;\n        this.attachments = new ArrayList<>(attachments);\n        this.icalEvent = icalEvent == null ? \"\" : icalEvent;\n        this.replyingToEmail = replyingToEmail;\n        this.forwardingToEmail = forwardingToEmail;\n        this.sendError = sendError;\n    }\n    public EmailMessage prepare(String generatedMsgId, String fromEmailAddress, LocalDateTime emailSent) {\n        return new EmailMessage(id, generatedMsgId, fromEmailAddress, subject, emailSent, to, cc, bcc, content, unread, star,\n                attachments, icalEvent, replyingToEmail, forwardingToEmail, sendError);\n    }\n\n    public EmailMessage withAttachments(List<Attachment> suppliedAttachments) {\n        return new EmailMessage(id, msgId, from, subject, created, to, cc, bcc, content, unread, star,\n                suppliedAttachments, icalEvent, replyingToEmail, forwardingToEmail, sendError);\n    }\n\n    public EmailMessage withError(String error) {\n        return new EmailMessage(id, msgId, from, subject, created, to, cc, bcc, content, unread, star,\n                attachments, icalEvent, replyingToEmail, forwardingToEmail, Optional.of(error));\n    }\n\n    public byte[] toBytes() {\n        return this.serialize();\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"v\", new CborObject.CborString(VERSION_1));\n        state.put(\"i\", new CborObject.CborString(id));\n        state.put(\"m\", new CborObject.CborString(msgId));\n        state.put(\"f\", new CborObject.CborString(from));\n        state.put(\"h\", new CborObject.CborString(subject));\n        state.put(\"t\", new CborObject.CborLong(created.toEpochSecond(ZoneOffset.UTC)));\n        state.put(\"d\", new CborObject.CborList(to.stream()\n                .map(CborObject.CborString::new)\n                .collect(Collectors.toList())));\n        state.put(\"c\", new CborObject.CborList(cc.stream()\n                .map(CborObject.CborString::new)\n                .collect(Collectors.toList())));\n        state.put(\"b\", new CborObject.CborList(bcc.stream()\n                .map(CborObject.CborString::new)\n                .collect(Collectors.toList())));\n        state.put(\"z\", new CborObject.CborString(content));\n        state.put(\"u\", new CborObject.CborBoolean(unread));\n        state.put(\"s\", new CborObject.CborBoolean(star));\n\n        state.put(\"a\", new CborObject.CborList(attachments));\n        state.put(\"e\", new CborObject.CborString(icalEvent));\n\n        replyingToEmail.ifPresent(r -> state.put(\"r\", replyingToEmail.get().toCbor()));\n        forwardingToEmail.ifPresent(o -> state.put(\"o\", forwardingToEmail.get().toCbor()));\n\n        sendError.ifPresent(o -> state.put(\"x\", new CborObject.CborString(sendError.get())));\n\n        List<CborObject> withMimeType = new ArrayList<>();\n        withMimeType.add(new CborObject.CborLong(MimeTypes.CBOR_PEERGOS_EMAIL_INT));\n        withMimeType.add(CborObject.CborMap.build(state));\n\n        return new CborObject.CborList(withMimeType);\n    }\n\n    public static EmailMessage fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborList withMimeType = (CborObject.CborList) cbor;\n        long mimeType = withMimeType.getLong(0);\n        if (mimeType != MimeTypes.CBOR_PEERGOS_EMAIL_INT)\n            throw new IllegalStateException(\"Invalid mimetype for Email: \" + mimeType);\n\n        CborObject.CborMap m = withMimeType.get(1, c -> (CborObject.CborMap)c);\n\n        String version = m.getString(\"v\");\n        if (! version.equals(VERSION_1)) {\n            throw new IllegalStateException(\"Unsupported version:\" + version);\n        }\n        String id = m.getString(\"i\");\n        String msgId = m.getString(\"m\");\n        String from = m.getString(\"f\");\n        String subject = m.getString(\"h\");\n        LocalDateTime created = m.get(\"t\", c -> LocalDateTime.ofEpochSecond(((CborObject.CborLong)c).value, 0, ZoneOffset.UTC));\n        List<String> to = m.getList(\"d\", n -> ((CborObject.CborString)n).value);\n        List<String> cc = m.getList(\"c\", n -> ((CborObject.CborString)n).value);\n        List<String> bcc = m.getList(\"b\", n -> ((CborObject.CborString)n).value);\n        String content = m.getString(\"z\");\n        boolean unread = m.getBoolean(\"u\");\n        boolean star = m.getBoolean(\"s\");\n        List<Attachment> attachments = m.getList(\"a\", Attachment::fromCbor);\n        String icalEvent = m.getString(\"e\");\n\n        Optional<EmailMessage> replyingToEmail = Optional.ofNullable(m.get(\"r\"))\n                .map(c -> EmailMessage.fromCbor(c));\n        Optional<EmailMessage> forwardingToEmail = Optional.ofNullable(m.get(\"o\"))\n                .map(c -> EmailMessage.fromCbor(c));\n\n        Optional<String> sendError = Optional.ofNullable(m.get(\"x\"))\n                .map(c -> m.getString(\"x\"));\n\n        return new EmailMessage(id, msgId, from, subject, created, to, cc, bcc, content, unread, star, attachments, icalEvent,\n                replyingToEmail, forwardingToEmail, sendError);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/fingerprint/FingerPrint.java",
    "content": "package peergos.shared.fingerprint;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.QRCodeEncoder;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.util.*;\nimport peergos.shared.zxing.*;\nimport peergos.shared.zxing.common.*;\nimport peergos.shared.zxing.qrcode.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class FingerPrint implements Cborable {\n    private static final byte[] VERSION = new byte[] {0, 0};\n    private static final long COMBINED_VERSION = 1;\n\n    private final long version;\n    private final byte[] ourFingerPrint, friendsFingerPrint;\n\n    public FingerPrint(byte[] ourFingerPrint, byte[] friendsFingerPrint, long version) {\n        this.ourFingerPrint = ourFingerPrint;\n        this.friendsFingerPrint = friendsFingerPrint;\n        this.version = version;\n    }\n\n    @JsMethod\n    public String getDisplayString() {\n        return calculateDisplayString(ourFingerPrint, friendsFingerPrint);\n    }\n\n    @JsMethod\n    public String getBase64Thumbnail() {\n        String base64Data = Base64.getEncoder().encodeToString(getQrCodeData());\n        return \"data:image/png;base64,\" + base64Data;\n    }\n\n    public static FingerPrint generate(String ourname,\n                                       List<PublicKeyHash> ourIdentityKey,\n                                       String friendsName,\n                                       List<PublicKeyHash> friendsIdentityKey,\n                                       Hasher h) {\n        try {\n            byte[] us = calculateHalfFingerprint(ourname, ourIdentityKey, h);\n            byte[] friend = calculateHalfFingerprint(friendsName, friendsIdentityKey, h);\n            return new FingerPrint(us, friend, COMBINED_VERSION);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @JsMethod\n    public static FingerPrint decodeFromPixels(int[] pixels, int width, int height) {\n        // This source doesn't handle rotations or dilations\n        RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels);\n\n        BinaryBitmap readBitmap = new BinaryBitmap(new HybridBinarizer(source));\n        QRCodeReader reader = new QRCodeReader();\n        try {\n            return fromString(reader.decode(readBitmap).getText());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @JsMethod\n    public boolean matches(FingerPrint other) {\n        return version == other.version &&\n                Arrays.equals(ourFingerPrint, other.friendsFingerPrint) &&\n                Arrays.equals(friendsFingerPrint, other.ourFingerPrint);\n    }\n\n    public byte[] getQrCodeData() {\n        try {\n            QRCodeWriter writer = new QRCodeWriter();\n            String contents = ArrayOps.bytesToHex(toCbor().toByteArray());\n            BitMatrix result = writer.encode(contents,\n                    BarcodeFormat.QR_CODE, 512, 512);\n\n            return QRCodeEncoder.encodeToPng(QRCodeEncoder.BW_MODE, result.getWidth(), result.getHeight(), result);\n        } catch (WriterException | IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        FingerPrint that = (FingerPrint) o;\n        return version == that.version &&\n                Arrays.equals(ourFingerPrint, that.ourFingerPrint) &&\n                Arrays.equals(friendsFingerPrint, that.friendsFingerPrint);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Objects.hash(version);\n        result = 31 * result + Arrays.hashCode(ourFingerPrint);\n        result = 31 * result + Arrays.hashCode(friendsFingerPrint);\n        return result;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"v\", new CborObject.CborLong(version));\n        state.put(\"u\", new CborObject.CborByteArray(ourFingerPrint));\n        state.put(\"f\", new CborObject.CborByteArray(friendsFingerPrint));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static FingerPrint fromCbor(CborObject cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for FingerPrint! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        long version = m.getLong(\"v\");\n        byte[] us = m.getByteArray(\"u\");\n        byte[] friend = m.getByteArray(\"f\");\n        return new FingerPrint(us, friend, version);\n    }\n\n    public static FingerPrint fromString(String scanned) {\n        byte[] bytes = ArrayOps.hexToBytes(scanned);\n        return fromCbor(CborObject.fromByteArray(bytes));\n    }\n\n    private static String calculateDisplayString(byte[] us, byte[] friend) {\n        String ourString = getDisplayStringFor(us);\n        String friendString = getDisplayStringFor(friend);\n        if (ourString.compareTo(friendString) <= 0)\n            return ourString + friendString;\n        return friendString + ourString;\n    }\n\n    private static int compareArrays(byte[] a, byte[] b) {\n        if (a.length != b.length)\n            return a.length - b.length;\n        for (int i=0; i < a.length; i++)\n            if (a[i] != b[i])\n                return a[i] - b[i];\n        return 0;\n    }\n\n    private static byte[] calculateHalfFingerprint(String name,\n                                                   List<PublicKeyHash> identityKeys,\n                                                   Hasher h) throws IOException {\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        bout.write(VERSION);\n        bout.write(name.getBytes(\"UTF-8\"));\n        List<byte[]> serializedKeys = identityKeys.stream()\n                .map(x -> x.serialize())\n                .sorted(FingerPrint::compareArrays)\n                .collect(Collectors.toList());\n        for (byte[] serializedKey : serializedKeys) {\n            bout.write(serializedKey);\n        }\n        byte[] initial = bout.toByteArray();\n        return hash(initial, 5200, h); // 112 bits of security\n    }\n\n    private static byte[] hash(byte[] input, int iterations, Hasher h) {\n        for (int i=0; i < iterations; i++)\n            input = h.blake2b(input, 64);\n        return Arrays.copyOfRange(input, 0, 32);\n    }\n\n    private static String getDisplayStringFor(byte[] fingerprint) {\n        return getEncodedChunk(fingerprint, 0)  +\n                getEncodedChunk(fingerprint, 5)  +\n                getEncodedChunk(fingerprint, 10) +\n                getEncodedChunk(fingerprint, 15) +\n                getEncodedChunk(fingerprint, 20) +\n                getEncodedChunk(fingerprint, 25);\n    }\n\n    private static String getEncodedChunk(byte[] hash, int offset) {\n        long chunk = byteArray5ToLong(hash, offset) % 100000;\n        if (chunk < 10)\n            return \"0000\" + chunk;\n        if (chunk < 100)\n            return \"000\" + chunk;\n        if (chunk < 1000)\n            return \"00\" + chunk;\n        if (chunk < 10000)\n            return \"0\" + chunk;\n        return \"\" + chunk;\n    }\n\n    private static long byteArray5ToLong(byte[] in, int start) {\n        return in[start] & 0xffL |\n                ((in[start +  1] & 0xffL) << 8) |\n                ((in[start +  2] & 0xffL) << 16) |\n                ((in[start +  3] & 0xffL) << 24) |\n                ((in[start +  4] & 0xffL) << 32);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/hamt/Champ.java",
    "content": "package peergos.shared.hamt;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/**\n * A Compressed Hash-Array Mapped Prefix-tree (CHAMP), a refinement of a Hash Array Mapped Trie (HAMT)\n */\npublic class Champ<V extends Cborable> implements Cborable {\n\n    private static final int HASH_CODE_LENGTH = 32;\n\n    public static class KeyElement<V extends Cborable> {\n        public final ByteArrayWrapper key;\n        public final Optional<V> valueHash;\n\n        public KeyElement(ByteArrayWrapper key, Optional<V> valueHash) {\n            this.key = key;\n            this.valueHash = valueHash;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            KeyElement<?> that = (KeyElement<?>) o;\n            return key.equals(that.key) &&\n                    valueHash.equals(that.valueHash);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(key, valueHash);\n        }\n    }\n\n    public static class HashPrefixPayload<V extends Cborable> {\n        public final KeyElement<V>[] mappings;\n        public final MaybeMultihash link;\n\n        public HashPrefixPayload(KeyElement<V>[] mappings, MaybeMultihash link) {\n            this.mappings = mappings;\n            this.link = link;\n            if ((mappings == null) ^ (link != null))\n                throw new IllegalStateException(\"Payload can either be mappings or a link, not both!\");\n        }\n\n        public HashPrefixPayload(KeyElement<V>[] mappings) {\n            this(mappings, null);\n        }\n\n        public HashPrefixPayload(MaybeMultihash link) {\n            this(null, link);\n        }\n\n        public boolean isShard() {\n            return link != null;\n        }\n\n        public int keyCount() {\n            return mappings.length;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            HashPrefixPayload<?> that = (HashPrefixPayload<?>) o;\n            return Arrays.equals(mappings, that.mappings) &&\n                    Objects.equals(link, that.link);\n        }\n\n        @Override\n        public int hashCode() {\n            int result = Objects.hash(link);\n            result = 31 * result + Arrays.hashCode(mappings);\n            return result;\n        }\n    }\n\n    public static <V extends Cborable> Champ<V> empty(Function<Cborable, V> fromCbor) {\n        return new Champ<>(new BitSet(), new BitSet(), new HashPrefixPayload[0], fromCbor, Optional.empty());\n    }\n\n    public final BitSet dataMap, nodeMap;\n    private final HashPrefixPayload<V>[] contents;\n    private final Function<Cborable, V> fromCbor;\n    public final Optional<BatId> mirrorBat;\n\n    public Champ(BitSet dataMap, BitSet nodeMap, HashPrefixPayload<V>[] contents, Function<Cborable, V> fromCbor, Optional<BatId> mirrorBat) {\n        this.dataMap = dataMap;\n        this.nodeMap = nodeMap;\n        this.contents = contents;\n        this.fromCbor = fromCbor;\n        this.mirrorBat = mirrorBat;\n        for (int i=0; i< contents.length; i++)\n            if (contents[i] == null)\n                throw new IllegalStateException();\n    }\n\n    public Champ<V> withBat(Optional<BatId> newMirrorBat) {\n        return new Champ<V>(dataMap, nodeMap, contents, fromCbor, newMirrorBat);\n    }\n\n    private int keyCount() {\n        int count = 0;\n        for (HashPrefixPayload<V> payload : contents) {\n            if (! payload.isShard())\n                    count += payload.keyCount();\n        }\n        return count;\n    }\n\n    private int nodeCount() {\n        int count = 0;\n        for (HashPrefixPayload<V> payload : contents)\n            if (payload.isShard())\n                count++;\n\n        return count;\n    }\n\n    private static int mask(byte[] hash, int depth, int nbits) {\n        int index = (depth * nbits) / 8;\n        int shift = (depth * nbits) % 8;\n        int lowBits = Math.min(nbits, 8 - shift);\n        int hiBits = nbits - lowBits;\n        byte val1 = index < hash.length ? hash[index] : 0;\n        byte val2 = index + 1 < hash.length ? hash[index + 1] : 0;\n        return ((val1 >> shift) & ((1 << lowBits) - 1)) |\n                ((val2 & ((1 << hiBits) - 1)) << lowBits);\n    }\n\n    private static int getIndex(BitSet bitmap, int bitpos) {\n        int total = 0;\n        for (int i = 0; i < bitpos;) {\n            int next = bitmap.nextSetBit(i);\n            if (next == -1 || next >= bitpos)\n                return total;\n            total++;\n            i = next + 1;\n        }\n        return total;\n    }\n\n    CompletableFuture<Pair<Multihash, Optional<Champ<V>>>> getChild(PublicKeyHash owner, byte[] hash, int depth, int bitWidth, ContentAddressedStorage storage) {\n        int bitpos = mask(hash, depth, bitWidth);\n        int index = contents.length - 1 - getIndex(this.nodeMap, bitpos);\n        Multihash childHash = contents[index].link.get();\n        return storage.get(owner, (Cid) childHash, Optional.empty())\n                .thenApply(x -> new Pair<>(childHash, x.map(y -> Champ.fromCbor(y, fromCbor))));\n    }\n\n    public CompletableFuture<Long> size(PublicKeyHash owner, int depth, ContentAddressedStorage storage) {\n        long keys = keyCount();\n        if (nodeCount() == 0)\n            return CompletableFuture.completedFuture(keys);\n\n        List<CompletableFuture<Long>> childCounts = new ArrayList<>();\n        for (int i = contents.length - 1; i >= 0; i--) {\n            HashPrefixPayload<V> pointer = contents[i];\n            if (! pointer.isShard())\n                break; // we reach the key section\n            childCounts.add(storage.get(owner, (Cid) pointer.link.get(), Optional.empty())\n                    .thenApply(x -> new Pair<>(pointer.link.get(), x.map(y -> Champ.fromCbor(y, fromCbor))))\n                    .thenCompose(child -> child.right.map(c -> c.size(owner, depth + 1, storage))\n                            .orElse(CompletableFuture.completedFuture(0L)))\n            );\n        }\n        List<Integer> indices = IntStream.range(0, childCounts.size())\n                .mapToObj(x -> x)\n                .collect(Collectors.toList());\n        return Futures.reduceAll(indices, keys, (t, index) -> childCounts.get(index).thenApply(c -> c + t), (a, b) -> a + b);\n    }\n\n    public boolean hasMultipleMappings() {\n        return keyCount() > 1 || nodeCount() > 0;\n    }\n\n    /**\n     *\n     * @param key The key to get the value for\n     * @param hash The hash of the key\n     * @param depth The current depth in the champ (top = 0)\n     * @param bitWidth The champ bitwidth\n     * @param storage The storage\n     * @return The value, if any, that this key maps to\n     */\n    public CompletableFuture<Optional<V>> get(PublicKeyHash owner, ByteArrayWrapper key, byte[] hash, int depth, int bitWidth, ContentAddressedStorage storage) {\n        final int bitpos = mask(hash, depth, bitWidth);\n\n        if (dataMap.get(bitpos)) { // local value\n            int index = getIndex(this.dataMap, bitpos);\n            HashPrefixPayload<V> payload = contents[index];\n            for (KeyElement<V> candidate : payload.mappings) {\n                if (candidate.key.equals(key)) {\n                    return CompletableFuture.completedFuture(candidate.valueHash);\n                }\n            }\n\n            return CompletableFuture.completedFuture(Optional.empty());\n        }\n\n        if (nodeMap.get(bitpos)) { // child node\n            return getChild(owner, hash, depth, bitWidth, storage)\n                    .thenCompose(child -> child.right.map(c -> c.get(owner, key, hash, depth + 1, bitWidth, storage))\n                            .orElse(CompletableFuture.completedFuture(Optional.empty())));\n        }\n\n        return CompletableFuture.completedFuture(Optional.empty());\n    }\n\n    private Champ<V> withMirrorBat(Optional<BatId> mirrorBat, int depth) {\n        if (depth > 0 || mirrorBat.isEmpty())\n            return this;\n        return new Champ<>(dataMap, nodeMap, contents, fromCbor, mirrorBat);\n    }\n\n    /**\n     *\n     * @param writer The writer key with permission to write\n     * @param key The key to set the value for\n     * @param hash The hash of the key\n     * @param depth The current depth in the champ (top = 0)\n     * @param expected The expected value, if any, currently stored for this key\n     * @param value The new value to map this key to\n     * @param bitWidth The champ bitwidth\n     * @param maxCollisions The maximum number of hash collision per layer in this champ\n     * @param hasher The function to calculate the hash of keys\n     * @param tid The transaction id for this write operation\n     * @param storage The storage\n     * @param ourHash The hash of the current champ node\n     * @return A new champ and its hash after the put\n     */\n    public CompletableFuture<Pair<Champ<V>, Multihash>> put(PublicKeyHash owner,\n                                                            SigningPrivateKeyAndPublicHash writer,\n                                                            ByteArrayWrapper key,\n                                                            byte[] hash,\n                                                            int depth,\n                                                            Optional<V> expected,\n                                                            Optional<V> value,\n                                                            int bitWidth,\n                                                            int maxCollisions,\n                                                            Optional<BatId> mirrorBat,\n                                                            Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher,\n                                                            TransactionId tid,\n                                                            ContentAddressedStorage storage,\n                                                            Hasher writeHasher,\n                                                            Multihash ourHash) {\n        int bitpos = mask(hash, depth, bitWidth);\n\n        if (dataMap.get(bitpos)) { // local value\n            int index = getIndex(this.dataMap, bitpos);\n            HashPrefixPayload<V> payload = contents[index];\n            KeyElement<V>[] mappings = payload.mappings;\n            for (int payloadIndex = 0; payloadIndex < mappings.length; payloadIndex++) {\n                KeyElement<V> mapping = mappings[payloadIndex];\n                final ByteArrayWrapper currentKey = mapping.key;\n                final Optional<V> currentVal = mapping.valueHash;\n                if (currentKey.equals(key)) {\n                    if (! currentVal.equals(expected)) {\n                        CompletableFuture<Pair<Champ<V>, Multihash>> err = new CompletableFuture<>();\n                        err.completeExceptionally(new CasException(currentVal, expected));\n                        return err;\n                    }\n\n                    // update mapping\n                    Champ<V> champ = copyAndSetValue(index, payloadIndex, value).withMirrorBat(mirrorBat, depth);\n                    return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n                }\n            }\n            if (mappings.length < maxCollisions) {\n                Champ<V> champ = insertIntoPrefix(index, key, value).withMirrorBat(mirrorBat, depth);\n                return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n            }\n\n            return pushMappingsDownALevel(owner, writer, mappings,\n                    key, hash, value, depth + 1, bitWidth, maxCollisions, mirrorBat, hasher, tid, storage, writeHasher)\n                    .thenCompose(p -> {\n                        Champ<V> champ = copyAndMigrateFromInlineToNode(bitpos, p).withMirrorBat(mirrorBat, depth);\n                        return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n                    });\n        } else if (nodeMap.get(bitpos)) { // child node\n            return getChild(owner, hash, depth, bitWidth, storage)\n                    .thenCompose(child -> child.right.get().put(owner, writer, key, hash, depth + 1, expected, value,\n                            bitWidth, maxCollisions, mirrorBat, hasher, tid, storage, writeHasher, child.left)\n                            .thenCompose(newChild -> {\n                                if (newChild.right.equals(child.left))\n                                    return CompletableFuture.completedFuture(new Pair<>(this, ourHash));\n                                Champ<V> champ = overwriteChildLink(bitpos, newChild);\n                                return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n                            }));\n        } else {\n            // no value\n            Champ<V> champ = addNewPrefix(bitpos, key, value).withMirrorBat(mirrorBat, depth);\n            return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n        }\n    }\n\n    private CompletableFuture<Pair<Champ<V>, Multihash>> pushMappingsDownALevel(PublicKeyHash owner,\n                                                                                SigningPrivateKeyAndPublicHash writer,\n                                                                                KeyElement<V>[] mappings,\n                                                                                ByteArrayWrapper key1,\n                                                                                byte[] hash1,\n                                                                                Optional<V> val1,\n                                                                                final int depth,\n                                                                                int bitWidth,\n                                                                                int maxCollisions,\n                                                                                Optional<BatId> mirrorBat,\n                                                                                Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher,\n                                                                                TransactionId tid,\n                                                                                ContentAddressedStorage storage,\n                                                                                Hasher writeHasher) {\n        if (depth >= HASH_CODE_LENGTH) {\n             throw new IllegalStateException(\"Hash collision!\");\n        }\n\n        Champ<V> empty = empty(fromCbor);\n        return storage.put(owner, writer, empty.serialize(), writeHasher, tid)\n                .thenApply(h -> new Pair<>(empty, h))\n                .thenCompose(p -> p.left.put(owner, writer, key1, hash1, depth, Optional.empty(), val1,\n                        bitWidth, maxCollisions, mirrorBat, hasher, tid, storage, writeHasher, p.right))\n                .thenCompose(one -> Futures.reduceAll(\n                        Arrays.stream(mappings).collect(Collectors.toList()),\n                        one,\n                        (p, e) -> hasher.apply(e.key)\n                                .thenCompose(eHash -> p.left.put(owner, writer, e.key, eHash, depth, Optional.empty(),\n                                        e.valueHash, bitWidth, maxCollisions, mirrorBat, hasher, tid, storage, writeHasher, p.right)),\n                        (a, b) -> a)\n                );\n    }\n\n    private Champ<V> copyAndSetValue(final int setIndex, final int payloadIndex, final Optional<V> val) {\n        final HashPrefixPayload<V>[] src = this.contents;\n        final HashPrefixPayload<V>[] dst = Arrays.copyOf(src, src.length);\n\n        HashPrefixPayload<V> existing = dst[setIndex];\n        KeyElement<V>[] updated = new KeyElement[existing.mappings.length];\n        System.arraycopy(existing.mappings, 0, updated, 0, existing.mappings.length);\n        updated[payloadIndex] = new KeyElement<>(existing.mappings[payloadIndex].key, val);\n        dst[setIndex] = new HashPrefixPayload<>(updated);\n\n        return new Champ<>(dataMap, nodeMap, dst, fromCbor, mirrorBat);\n    }\n\n    private Champ<V> insertIntoPrefix(final int index, final ByteArrayWrapper key, final Optional<V> val) {\n        final HashPrefixPayload<V>[] src = this.contents;\n        final HashPrefixPayload<V>[] result = Arrays.copyOf(src, src.length);\n\n        KeyElement<V>[] prefix = new KeyElement[src[index].mappings.length + 1];\n        System.arraycopy(src[index].mappings, 0, prefix, 0, src[index].mappings.length);\n        prefix[prefix.length - 1] = new KeyElement<>(key, val);\n        // ensure canonical structure\n        Arrays.sort(prefix, Comparator.comparing(m -> m.key));\n        result[index] = new HashPrefixPayload<>(prefix);\n\n        return new Champ<>(dataMap, nodeMap, result, fromCbor, mirrorBat);\n    }\n\n    private Champ<V> addNewPrefix(final int bitpos, final ByteArrayWrapper key, final Optional<V> val) {\n        final int insertIndex = getIndex(dataMap, bitpos);\n\n        final HashPrefixPayload<V>[] src = this.contents;\n        final HashPrefixPayload<V>[] result = new HashPrefixPayload[src.length + 1];\n\n        System.arraycopy(src, 0, result, 0, insertIndex);\n        System.arraycopy(src, insertIndex, result, insertIndex + 1, src.length - insertIndex);\n        result[insertIndex] = new HashPrefixPayload<>(new KeyElement[]{new KeyElement<>(key, val)});\n\n        BitSet newDataMap = BitSet.valueOf(dataMap.toByteArray());\n        newDataMap.set(bitpos);\n        return new Champ<>(newDataMap, nodeMap, result, fromCbor, mirrorBat);\n    }\n\n    private Champ<V> copyAndMigrateFromInlineToNode(final int bitpos, final Pair<Champ<V>, Multihash> node) {\n\n        final int oldIndex = getIndex(dataMap, bitpos);\n        final int newIndex = this.contents.length - 1 - getIndex(nodeMap, bitpos);\n\n        final HashPrefixPayload<V>[] src = this.contents;\n        final HashPrefixPayload<V>[] dst = new HashPrefixPayload[src.length];\n\n        // copy 'src' and remove 1 element at position oldIndex and insert 1 element at position newIndex\n        if (oldIndex > newIndex)\n            throw new IllegalStateException(\"Invalid champ!\");\n        System.arraycopy(src, 0, dst, 0, oldIndex);\n        System.arraycopy(src, oldIndex + 1, dst, oldIndex, newIndex - oldIndex);\n        dst[newIndex] = new HashPrefixPayload<>(MaybeMultihash.of(node.right));\n        System.arraycopy(src, newIndex + 1, dst, newIndex + 1, src.length - newIndex - 1);\n\n        BitSet newNodeMap = BitSet.valueOf(nodeMap.toByteArray());\n        newNodeMap.set(bitpos);\n        BitSet newDataMap = BitSet.valueOf(dataMap.toByteArray());\n        newDataMap.set(bitpos, false);\n        return new Champ<>(newDataMap, newNodeMap, dst, fromCbor, mirrorBat);\n    }\n\n    private Champ<V> overwriteChildLink(final int bitpos, final Pair<Champ<V>, Multihash> node) {\n\n        final int setIndex = this.contents.length - 1 - getIndex(nodeMap, bitpos);\n\n        final HashPrefixPayload<V>[] src = this.contents;\n        final HashPrefixPayload<V>[] dst = Arrays.copyOf(src, src.length);\n\n        dst[setIndex] = new HashPrefixPayload<>(MaybeMultihash.of(node.right));\n\n        return new Champ<>(dataMap, nodeMap, dst, fromCbor, mirrorBat);\n    }\n\n    /**\n     *\n     * @param writer The writer key with permission to write\n     * @param key The key to remove the value for\n     * @param hash The hash of the key\n     * @param depth The current depth in the champ (top = 0)\n     * @param expected The expected value, if any, currently stored for this key\n     * @param bitWidth The champ bitwidth\n     * @param maxCollisions The maximum number of hash collision per layer in this champ\n     * @param storage The storage\n     * @param ourHash The hash of the current champ node\n     * @return A new champ and its hash after the remove\n     */\n    public CompletableFuture<Pair<Champ<V>, Multihash>> remove(PublicKeyHash owner,\n                                                               SigningPrivateKeyAndPublicHash writer,\n                                                               ByteArrayWrapper key,\n                                                               byte[] hash,\n                                                               int depth,\n                                                               Optional<V> expected,\n                                                               int bitWidth,\n                                                               int maxCollisions,\n                                                               Optional<BatId> mirrorBat,\n                                                               TransactionId tid,\n                                                               ContentAddressedStorage storage,\n                                                               Hasher writeHasher,\n                                                               Multihash ourHash) {\n        int bitpos = mask(hash, depth, bitWidth);\n\n        if (dataMap.get(bitpos)) { // in place value\n            final int dataIndex = getIndex(dataMap, bitpos);\n\n            HashPrefixPayload<V> payload = contents[dataIndex];\n            KeyElement<V>[] mappings = payload.mappings;\n            for (int payloadIndex = 0; payloadIndex < mappings.length; payloadIndex++) {\n                KeyElement<V> mapping = mappings[payloadIndex];\n                final ByteArrayWrapper currentKey = mapping.key;\n                final Optional<V> currentVal = mapping.valueHash;\n                if (Objects.equals(currentKey, key)) {\n                    if (!currentVal.equals(expected)) {\n                        CompletableFuture<Pair<Champ<V>, Multihash>> err = new CompletableFuture<>();\n                        err.completeExceptionally(new CasException(currentVal, expected));\n                        return err;\n                    }\n\n                    if (this.keyCount() == maxCollisions + 1 && this.nodeCount() == 0) {\n                        /*\n\t\t\t\t         * Create new node with remaining pairs. The new node\n\t\t\t\t\t     * will either\n\t\t\t\t\t     * a) become the new root returned, or\n\t\t\t\t\t     * b) be unwrapped and inlined during returning.\n\t\t\t\t\t     */\n                        Champ<V> champ;\n                        if (depth > 0) {\n                            // inline all mappings into a single node because at a higher level, all mappings have the\n                            // same hash prefix\n                            final BitSet newDataMap = new BitSet();\n                            newDataMap.set(mask(hash, 0, bitWidth));\n\n                            KeyElement<V>[] remainingMappings = new KeyElement[maxCollisions];\n                            int nextIndex = 0;\n                            for (HashPrefixPayload<V> grouped : contents) {\n                                for (KeyElement<V> pair : grouped.mappings) {\n                                    if (!pair.key.equals(key))\n                                        remainingMappings[nextIndex++] = pair;\n                                }\n                            }\n                            Arrays.sort(remainingMappings, Comparator.comparing(x -> x.key));\n                            HashPrefixPayload<V>[] oneBucket = new HashPrefixPayload[]{new HashPrefixPayload(remainingMappings)};\n\n                            champ = new Champ<>(newDataMap, new BitSet(), oneBucket, fromCbor, Optional.empty()).withMirrorBat(mirrorBat, 0);\n                        } else {\n                            final BitSet newDataMap = BitSet.valueOf(dataMap.toByteArray());\n                            boolean lastInPrefix = mappings.length == 1;\n                            if (lastInPrefix)\n                                newDataMap.clear(bitpos);\n                            else\n                                newDataMap.set(mask(hash, 0, bitWidth));\n\n                            HashPrefixPayload<V>[] src = this.contents;\n                            HashPrefixPayload<V>[] dst = new HashPrefixPayload[src.length - (lastInPrefix ? 1 : 0)];\n                            System.arraycopy(src, 0, dst, 0, dataIndex);\n                            System.arraycopy(src, dataIndex + 1, dst, dataIndex + (lastInPrefix ? 0 : 1), src.length - dataIndex - 1);\n                            if (! lastInPrefix) {\n                                KeyElement<V>[] remaining = new KeyElement[mappings.length - 1];\n                                System.arraycopy(mappings, 0, remaining, 0, payloadIndex);\n                                System.arraycopy(mappings, payloadIndex + 1, remaining, payloadIndex, mappings.length - payloadIndex - 1);\n                                dst[dataIndex] = new HashPrefixPayload<>(remaining);\n                            }\n\n                            champ = new Champ(newDataMap, new BitSet(), dst, fromCbor, mirrorBat);\n                        }\n                        return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n                    } else {\n                        Champ<V> champ = removeMapping(bitpos, payloadIndex).withMirrorBat(mirrorBat, depth);\n                        return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n                    }\n                }\n            }\n            CompletableFuture<Pair<Champ<V>, Multihash>> err = new CompletableFuture<>();\n            err.completeExceptionally(new CasException(Optional.empty(), expected));\n            return err;\n        } else if (nodeMap.get(bitpos)) { // node (not value)\n            return getChild(owner, hash, depth, bitWidth, storage)\n                    .thenCompose(child -> child.right.get().remove(owner, writer, key, hash, depth + 1, expected,\n                            bitWidth, maxCollisions, mirrorBat, tid, storage, writeHasher, child.left)\n                            .thenCompose(newChild -> {\n                                if (child.left.equals(newChild.right))\n                                    return CompletableFuture.completedFuture(new Pair<>(this, ourHash));\n\n                                if (newChild.left.contents.length == 0) {\n                                    throw new IllegalStateException(\"Sub-node must have at least one element.\");\n                                } else if (newChild.left.nodeCount() == 0 && newChild.left.keyCount() == maxCollisions) {\n                                    if (this.keyCount() == 0 && this.nodeCount() == 1) {\n                                        // escalate singleton result (the child already has the depth corrected index and mirror bat)\n                                        return CompletableFuture.completedFuture(newChild);\n                                    } else {\n                                        // inline value (move to front)\n                                        Champ<V> champ = copyAndMigrateFromNodeToInline(bitpos, newChild.left);\n                                        return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n                                    }\n                                } else {\n                                    // modify current node (set replacement node)\n                                    Champ<V> champ = overwriteChildLink(bitpos, newChild);\n                                    return storage.put(owner, writer, champ.serialize(), writeHasher, tid).thenApply(h -> new Pair<>(champ, h));\n                                }\n                            }));\n        }\n\n        return CompletableFuture.completedFuture(new Pair<>(this, ourHash));\n    }\n\n    /**\n     * Remove all specified keys from the CHAMP in a single recursive descent,\n     * substantially faster than N individual {@link #remove} calls.\n     *\n     * @param keysAndHashes each pair is (key, hash-of-key)\n     * @param expectedValues maps key → expected current value; unused for CAS here, reserved for callers\n     */\n    public CompletableFuture<Pair<Champ<V>, Multihash>> removeAll(\n            PublicKeyHash owner,\n            SigningPrivateKeyAndPublicHash writer,\n            List<Pair<ByteArrayWrapper, byte[]>> keysAndHashes,\n            Map<ByteArrayWrapper, Optional<V>> expectedValues,\n            int depth,\n            int bitWidth,\n            int maxCollisions,\n            Optional<BatId> mirrorBat,\n            TransactionId tid,\n            ContentAddressedStorage storage,\n            Hasher writeHasher,\n            Multihash ourHash) {\n\n        if (keysAndHashes.isEmpty())\n            return CompletableFuture.completedFuture(new Pair<>(this, ourHash));\n\n        // Group keys by bitpos at the current depth\n        Map<Integer, List<Pair<ByteArrayWrapper, byte[]>>> byBitpos = new HashMap<>();\n        for (Pair<ByteArrayWrapper, byte[]> kh : keysAndHashes) {\n            int bitpos = mask(kh.right, depth, bitWidth);\n            byBitpos.computeIfAbsent(bitpos, k -> new ArrayList<>()).add(kh);\n        }\n\n        // Phase 1: Process inline data removals; build new data section in ascending bitpos order.\n        BitSet newDataMap = new BitSet();\n        Map<Integer, HashPrefixPayload<V>> newDataByBitpos = new LinkedHashMap<>();\n        {\n            int di = 0;\n            for (int bp = dataMap.nextSetBit(0); bp >= 0; bp = dataMap.nextSetBit(bp + 1)) {\n                HashPrefixPayload<V> payload = contents[di++];\n                List<Pair<ByteArrayWrapper, byte[]>> toRemove = byBitpos.get(bp);\n                if (toRemove == null) {\n                    newDataMap.set(bp);\n                    newDataByBitpos.put(bp, payload);\n                } else {\n                    Set<ByteArrayWrapper> removing = new HashSet<>();\n                    for (Pair<ByteArrayWrapper, byte[]> kh : toRemove) removing.add(kh.left);\n                    List<KeyElement<V>> remaining = new ArrayList<>();\n                    for (KeyElement<V> elem : payload.mappings)\n                        if (!removing.contains(elem.key)) remaining.add(elem);\n                    if (!remaining.isEmpty()) {\n                        newDataMap.set(bp);\n                        newDataByBitpos.put(bp, new HashPrefixPayload<>(remaining.toArray(new KeyElement[0])));\n                    }\n                }\n            }\n        }\n\n        // Phase 2: Collect nodeMap hits for async processing.\n        Map<Integer, List<Pair<ByteArrayWrapper, byte[]>>> nodeMapHits = new HashMap<>();\n        for (Map.Entry<Integer, List<Pair<ByteArrayWrapper, byte[]>>> e : byBitpos.entrySet())\n            if (nodeMap.get(e.getKey()))\n                nodeMapHits.put(e.getKey(), e.getValue());\n\n        if (nodeMapHits.isEmpty()) {\n            // Only data changes — build and write updated node.\n            return buildAndWrite(owner, writer, newDataMap, newDataByBitpos,\n                    BitSet.valueOf(nodeMap.toByteArray()), Collections.emptyMap(),\n                    mirrorBat, depth, storage, writeHasher, tid);\n        }\n\n        // Phase 3: Recurse into affected children in parallel.\n        List<CompletableFuture<Pair<Integer, Pair<Champ<V>, Multihash>>>> childFutures = new ArrayList<>();\n        for (Map.Entry<Integer, List<Pair<ByteArrayWrapper, byte[]>>> e : nodeMapHits.entrySet()) {\n            final int bp = e.getKey();\n            final List<Pair<ByteArrayWrapper, byte[]>> childKeys = e.getValue();\n            int nodeIdx = contents.length - 1 - getIndex(nodeMap, bp);\n            Multihash childHash = contents[nodeIdx].link.get();\n            childFutures.add(\n                storage.get(owner, (Cid) childHash, Optional.empty())\n                    .thenCompose(rawOpt -> {\n                        Champ<V> child = Champ.fromCbor(rawOpt.get(), fromCbor);\n                        return child.removeAll(owner, writer, childKeys, expectedValues, depth + 1,\n                                bitWidth, maxCollisions, mirrorBat, tid, storage, writeHasher, childHash);\n                    })\n                    .thenApply(result -> new Pair<>(bp, result))\n            );\n        }\n\n        return Futures.combineAllInOrder(childFutures)\n                .thenCompose(childResults -> {\n                    // Phase 4: Integrate child results into data/node sections.\n                    BitSet newNodeMap = BitSet.valueOf(nodeMap.toByteArray());\n                    Map<Integer, MaybeMultihash> nodeUpdates = new HashMap<>();\n                    for (Pair<Integer, Pair<Champ<V>, Multihash>> r : childResults) {\n                        int bp = r.left;\n                        Champ<V> newChild = r.right.left;\n                        Multihash newChildHash = r.right.right;\n                        if (newChild.keyCount() == 0 && newChild.nodeCount() == 0) {\n                            // Child became empty — remove it entirely.\n                            newNodeMap.set(bp, false);\n                        } else if (newChild.nodeCount() == 0 && newChild.keyCount() <= maxCollisions) {\n                            // Child is inlineable — migrate into data section.\n                            newNodeMap.set(bp, false);\n                            newDataMap.set(bp);\n                            newDataByBitpos.put(bp, new HashPrefixPayload<>(collectAllMappings(newChild)));\n                        } else {\n                            // Update the child link.\n                            nodeUpdates.put(bp, MaybeMultihash.of(newChildHash));\n                        }\n                    }\n                    return buildAndWrite(owner, writer, newDataMap, newDataByBitpos,\n                            newNodeMap, nodeUpdates, mirrorBat, depth, storage, writeHasher, tid);\n                });\n    }\n\n    /**\n     * Assemble a new CHAMP node from the supplied data and node sections and write it to storage.\n     * {@code nodeUpdates} maps bitpos → updated child link for changed children; unchanged children\n     * are read directly from {@code this.contents} using {@code this.nodeMap}.\n     */\n    private CompletableFuture<Pair<Champ<V>, Multihash>> buildAndWrite(\n            PublicKeyHash owner,\n            SigningPrivateKeyAndPublicHash writer,\n            BitSet newDataMap,\n            Map<Integer, HashPrefixPayload<V>> newDataByBitpos,\n            BitSet newNodeMap,\n            Map<Integer, MaybeMultihash> nodeUpdates,\n            Optional<BatId> mirrorBat,\n            int depth,\n            ContentAddressedStorage storage,\n            Hasher writeHasher,\n            TransactionId tid) {\n\n        // Data payloads in ascending bitpos order\n        List<HashPrefixPayload<V>> dataPayloads = new ArrayList<>();\n        for (int bp = newDataMap.nextSetBit(0); bp >= 0; bp = newDataMap.nextSetBit(bp + 1))\n            dataPayloads.add(newDataByBitpos.get(bp));\n\n        // Node links in ascending bitpos order (stored reversed at end of contents)\n        List<MaybeMultihash> nodeLinks = new ArrayList<>();\n        for (int bp = newNodeMap.nextSetBit(0); bp >= 0; bp = newNodeMap.nextSetBit(bp + 1)) {\n            if (nodeUpdates.containsKey(bp)) {\n                nodeLinks.add(nodeUpdates.get(bp));\n            } else {\n                // Unchanged node — look up original position via this.nodeMap / this.contents\n                nodeLinks.add(contents[contents.length - 1 - getIndex(nodeMap, bp)].link);\n            }\n        }\n\n        int D = dataPayloads.size(), N = nodeLinks.size();\n        HashPrefixPayload<V>[] fc = new HashPrefixPayload[D + N];\n        for (int i = 0; i < D; i++) fc[i] = dataPayloads.get(i);\n        // Node entries at end in REVERSE bitpos order (first bitpos ↔ last index)\n        for (int i = 0; i < N; i++) fc[D + N - 1 - i] = new HashPrefixPayload<>(nodeLinks.get(i));\n\n        Champ<V> updated = new Champ<>(newDataMap, newNodeMap, fc, fromCbor, mirrorBat).withMirrorBat(mirrorBat, depth);\n        return storage.put(owner, writer, updated.serialize(), writeHasher, tid)\n                .thenApply(h -> new Pair<>(updated, h));\n    }\n\n    /** Collect all inline key-element mappings from a node that has no child nodes. */\n    @SuppressWarnings(\"unchecked\")\n    private KeyElement<V>[] collectAllMappings(Champ<V> node) {\n        List<KeyElement<V>> all = new ArrayList<>();\n        for (HashPrefixPayload<V> payload : node.contents)\n            if (!payload.isShard())\n                Collections.addAll(all, payload.mappings);\n        all.sort(Comparator.comparing(x -> x.key));\n        return all.toArray(new KeyElement[0]);\n    }\n\n    private Champ<V> copyAndMigrateFromNodeToInline(final int bitpos, final Champ<V> node) {\n\n        final int oldIndex = this.contents.length - 1 - getIndex(nodeMap, bitpos);\n        final int newIndex = getIndex(dataMap, bitpos);\n\n        final HashPrefixPayload<V>[] src = this.contents;\n        final HashPrefixPayload<V>[] dst = new HashPrefixPayload[src.length];\n\n        // copy src and remove element at position oldIndex and insert element at position newIndex\n        if (oldIndex < newIndex)\n            throw new IllegalStateException(\"Invalid champ!\");\n        System.arraycopy(src, 0, dst, 0, newIndex);\n        KeyElement<V>[] merged = new KeyElement[node.keyCount()];\n        int count = 0;\n        for (int i=0; i < node.contents.length; i++) {\n            KeyElement<V>[] toAdd = node.contents[i].mappings;\n            System.arraycopy(toAdd, 0, merged, count, toAdd.length);\n            count += toAdd.length;\n        }\n        Arrays.sort(merged, Comparator.comparing(x -> x.key));\n        dst[newIndex] = new HashPrefixPayload<>(merged);\n        System.arraycopy(src, newIndex, dst, newIndex + 1, oldIndex - newIndex);\n        System.arraycopy(src, oldIndex + 1, dst, oldIndex + 1, src.length - oldIndex - 1);\n\n        BitSet newNodeMap = BitSet.valueOf(nodeMap.toByteArray());\n        newNodeMap.set(bitpos, false);\n        BitSet newDataMap = BitSet.valueOf(dataMap.toByteArray());\n        newDataMap.set(bitpos, true);\n        return new Champ<>(newDataMap, newNodeMap, dst, fromCbor, mirrorBat);\n    }\n\n    private Champ<V> removeMapping(final int bitpos, final int payloadIndex) {\n        final int index = getIndex(dataMap, bitpos);\n        final HashPrefixPayload<V>[] src = this.contents;\n        KeyElement<V>[] existing = src[index].mappings;\n        boolean lastInPrefix = existing.length == 1;\n        final HashPrefixPayload<V>[] dst = new HashPrefixPayload[src.length - (lastInPrefix ? 1 : 0)];\n\n        // copy src and remove element at position index\n        System.arraycopy(src, 0, dst, 0, index);\n        System.arraycopy(src, index + 1, dst, lastInPrefix ? index : index + 1, src.length - index - 1);\n        if (! lastInPrefix) {\n            KeyElement<V>[] remaining = new KeyElement[existing.length - 1];\n            System.arraycopy(existing, 0, remaining, 0, payloadIndex);\n            System.arraycopy(existing, payloadIndex + 1, remaining, payloadIndex, existing.length - payloadIndex - 1);\n            dst[index] = new HashPrefixPayload<>(remaining);\n        }\n\n        BitSet newDataMap = BitSet.valueOf(dataMap.toByteArray());\n        if (lastInPrefix)\n            newDataMap.clear(bitpos);\n        return new Champ<>(newDataMap, nodeMap, dst, fromCbor, mirrorBat);\n    }\n\n    public <T> CompletableFuture<T> reduceAllMappings(PublicKeyHash owner,\n                                                      T identity,\n                                                      BiFunction<T, Pair<ByteArrayWrapper, Optional<V>>, CompletableFuture<T>> consumer,\n                                                      ContentAddressedStorage storage) {\n        return Futures.reduceAll(Arrays.stream(contents).collect(Collectors.toList()), identity, (res, payload) ->\n                (! payload.isShard() ?\n                        Futures.reduceAll(\n                                Arrays.stream(payload.mappings).collect(Collectors.toList()),\n                                res,\n                                (x, mapping) -> consumer.apply(x, new Pair<>(mapping.key, mapping.valueHash)),\n                                (a, b) ->  a) :\n                        CompletableFuture.completedFuture(res)\n                ).thenCompose(newRes ->\n                        payload.isShard() && payload.link.isPresent() ?\n                                storage.get(owner, (Cid)payload.link.get(), Optional.empty())\n                                        .thenApply(rawOpt -> Champ.fromCbor(rawOpt.orElseThrow(() -> new IllegalStateException(\"Hash not present! \" + payload.link)), fromCbor))\n                                        .thenCompose(child -> child.reduceAllMappings(owner, newRes, consumer, storage)) :\n                                CompletableFuture.completedFuture(newRes)\n                ), (a, b) -> a);\n    }\n\n    public CompletableFuture<Boolean> applyToAllMappings(PublicKeyHash owner,\n                                                         Function<Pair<ByteArrayWrapper, Optional<V>>, CompletableFuture<Boolean>> mapper,\n                                                         ContentAddressedStorage storage) {\n        return Futures.combineAll(Arrays.stream(contents).parallel().map(payload ->\n                        (! payload.isShard() ?\n                                Futures.combineAll(\n                                                Arrays.stream(payload.mappings).parallel()\n                                                        .map(mapping -> mapper.apply(new Pair<>(mapping.key, mapping.valueHash)))\n                                                        .collect(Collectors.toList()))\n                                        .thenApply(x -> true) :\n                                Futures.of(true)\n                        ).thenCompose(newRes ->\n                                payload.isShard() && payload.link.isPresent() ?\n                                        storage.get(owner, (Cid)payload.link.get(), Optional.empty())\n                                                .thenApply(rawOpt -> Champ.fromCbor(rawOpt.orElseThrow(() -> new IllegalStateException(\"Hash not present! \" + payload.link)), fromCbor))\n                                                .thenCompose(child -> child.applyToAllMappings(owner, mapper, storage)) :\n                                        Futures.of(true)\n                        )).collect(Collectors.toList()))\n                .thenApply(x -> true);\n    }\n\n    private List<KeyElement<V>> getMappings() {\n        return Arrays.stream(contents)\n                .filter(p -> !p.isShard())\n                .flatMap(p -> Arrays.stream(p.mappings))\n                .collect(Collectors.toList());\n    }\n\n    private List<HashPrefixPayload<V>> getLinks() {\n        return Arrays.stream(contents)\n                .filter(p -> p.isShard())\n                .collect(Collectors.toList());\n    }\n\n    public static <V extends Cborable> Optional<HashPrefixPayload<V>> getElement(int bitIndex,\n                                                                                 int dataIndex,\n                                                                                 int nodeIndex,\n                                                                                 Optional<Champ<V>> c) {\n        if (! c.isPresent())\n            return Optional.empty();\n        Champ<V> champ = c.get();\n        if (champ.dataMap.get(bitIndex))\n            return Optional.of(champ.contents[dataIndex]);\n        if (champ.nodeMap.get(bitIndex))\n            return Optional.of(champ.contents[champ.contents.length - 1 - nodeIndex]);\n        return Optional.empty();\n    }\n\n    public static <V extends Cborable> CompletableFuture<Map<Integer, List<KeyElement<V>>>> hashAndMaskKeys(\n            List<KeyElement<V>> mappings,\n            int depth,\n            int bitWidth,\n            Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher) {\n        List<Pair<KeyElement<V>, Integer>> empty = Collections.emptyList();\n        return Futures.reduceAll(mappings, empty,\n                (acc, m) -> hasher.apply(m.key)\n                        .thenApply(hash -> new Pair<>(m, mask(hash, depth, bitWidth)))\n                        .thenApply(p -> Stream.concat(acc.stream(), Stream.of(p)).collect(Collectors.toList())),\n                (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toList()))\n                .thenApply(hashed -> hashed.stream().collect(Collectors.groupingBy(p -> p.right)))\n                .thenApply(grouped -> grouped.entrySet().stream()\n                        .map(e -> new Pair<>(e.getKey(), e.getValue().stream().map(p -> p.left).collect(Collectors.toList())))\n                        .collect(Collectors.toMap(p -> p.left, p -> p.right))\n                );\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Champ<?> champ = (Champ<?>) o;\n        return dataMap.equals(champ.dataMap) &&\n                nodeMap.equals(champ.nodeMap) &&\n                Arrays.equals(contents, champ.contents);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Objects.hash(dataMap, nodeMap);\n        result = 31 * result + Arrays.hashCode(contents);\n        return result;\n    }\n\n    private CborObject.CborList toCborList() {\n        return new CborObject.CborList(Arrays.asList(\n                new CborObject.CborByteArray(dataMap.toByteArray()),\n                new CborObject.CborByteArray(nodeMap.toByteArray()),\n                new CborObject.CborList(Arrays.stream(contents)\n                        .flatMap(e -> e.link != null ?\n                                Stream.of(new CborObject.CborMerkleLink(e.link.get())) :\n                                Stream.of(new CborObject.CborList(Arrays.stream(e.mappings)\n                                        .flatMap(m -> Stream.of(\n                                                new CborObject.CborByteArray(m.key.data),\n                                                m.valueHash.isPresent() ?\n                                                        m.valueHash.get().toCbor() :\n                                                        new CborObject.CborNull()\n                                        ))\n                                        .collect(Collectors.toList()))))\n                        .collect(Collectors.toList()))\n        ));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        if (mirrorBat.isPresent()) {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"d\", toCborList());\n            state.put(\"bats\", new CborObject.CborList(Arrays.asList(mirrorBat.get())));\n            return CborObject.CborMap.build(state);\n        }\n        return toCborList();\n    }\n\n    public static <V extends Cborable> Champ<V> fromCbor(Cborable cbor, Function<Cborable, V> fromCbor) {\n        if (cbor instanceof CborObject.CborMap) {\n            Optional<BatId> mirrorBat = ((CborObject.CborMap) cbor).getList(\"bats\", BatId::fromCbor).stream().findFirst();\n            return fromCborList(((CborObject.CborMap) cbor).get(\"d\"), fromCbor).withBat(mirrorBat);\n        }\n        return fromCborList(cbor, fromCbor);\n    }\n\n    public static <V extends Cborable> Champ<V> fromCborList(Cborable cbor, Function<Cborable, V> fromCbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for CHAMP! \" + cbor);\n        List<? extends Cborable> list = ((CborObject.CborList) cbor).value;\n\n        if (! (list.get(0) instanceof CborObject.CborByteArray))\n            throw new IllegalStateException(\"Invalid cbor for a champ, is this a btree?\");\n        BitSet dataMap = BitSet.valueOf(((CborObject.CborByteArray)list.get(0)).value);\n        BitSet nodeMap = BitSet.valueOf(((CborObject.CborByteArray)list.get(1)).value);\n        List<? extends Cborable> contentsCbor = ((CborObject.CborList) list.get(2)).value;\n\n        List<HashPrefixPayload<V>> contents = new ArrayList<>();\n        for (int i=0; i < contentsCbor.size(); i++) {\n            Cborable keyOrHash = contentsCbor.get(i);\n            if (keyOrHash instanceof CborObject.CborList) {\n                List<KeyElement<V>> mappings = new ArrayList<>();\n                List<? extends Cborable> mappingsCbor = ((CborObject.CborList) keyOrHash).value;\n                if (mappingsCbor.size() % 2 != 0)\n                    throw new IllegalStateException(\"Invalid cbor for CHAMP mappings: odd number of elements \" + mappingsCbor.size());\n                for (int j=0; j < mappingsCbor.size(); j += 2) {\n                    byte[] key = ((CborObject.CborByteArray) mappingsCbor.get(j)).value;\n                    Cborable value = mappingsCbor.get(j + 1);\n                    mappings.add(new KeyElement<>(new ByteArrayWrapper(key),\n                        value instanceof CborObject.CborNull ?\n                                Optional.empty() :\n                                Optional.of(fromCbor.apply(value))));\n                }\n                contents.add(new HashPrefixPayload<>(mappings.toArray(new KeyElement[0])));\n            } else {\n                contents.add(new HashPrefixPayload<>(MaybeMultihash.of(((CborObject.CborMerkleLink)keyOrHash).target)));\n            }\n        }\n        return new Champ<>(dataMap, nodeMap, contents.toArray(new HashPrefixPayload[contents.size()]), fromCbor, Optional.empty());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/hamt/ChampUtil.java",
    "content": "package peergos.shared.hamt;\n\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.SigningPrivateKeyAndPublicHash;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.storage.TransactionId;\nimport peergos.shared.storage.auth.BatId;\nimport peergos.shared.util.ByteArrayWrapper;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.Pair;\nimport peergos.shared.util.Triple;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\npublic class ChampUtil {\n\n    public static <V extends Cborable> CompletableFuture<Pair<Champ<V>, Multihash>> merge(\n            PublicKeyHash owner,\n            SigningPrivateKeyAndPublicHash writer,\n            MaybeMultihash original,\n            MaybeMultihash updated,\n            MaybeMultihash remote,\n            Optional<BatId> mirrorBat,\n            TransactionId tid,\n            int bitWidth,\n            int maxHashCollisionsPerLevel,\n            Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher,\n            Function<Cborable, V> fromCbor,\n            ContentAddressedStorage storage,\n            Hasher writeHasher) {\n        Set<ByteArrayWrapper> ourChangedKeys = new HashSet<>();\n        Set<ByteArrayWrapper> remoteChangedKeys = new HashSet<>();\n        List<Triple<ByteArrayWrapper, Optional<V>, Optional<V>>> remoteUpdates = new ArrayList<>();\n        return applyToDiff(owner, original, updated, 0, keyHasher, Collections.emptyList(), Collections.emptyList(),\n                    t -> ourChangedKeys.add(t.left), bitWidth, storage, fromCbor)\n                .thenCompose(b -> applyToDiff(owner, original, remote, 0, keyHasher, Collections.emptyList(), Collections.emptyList(),\n                    t -> {\n                        remoteChangedKeys.add(t.left);\n                        remoteUpdates.add(t);\n                    }, bitWidth, storage, fromCbor))\n                .thenCompose(x -> {\n                    ourChangedKeys.retainAll(remoteChangedKeys);\n                    if (! ourChangedKeys.isEmpty())\n                        throw new IllegalStateException(\"Concurrent modification of a file or directory!\");\n                    return updated.map(h -> storage.get(owner, (Cid)h, Optional.empty()))\n                            .orElseGet(() -> CompletableFuture.completedFuture(Optional.empty()))\n                            .thenApply(rawOpt -> rawOpt.map(y -> Champ.fromCbor(y, fromCbor)))\n                            .thenCompose(champ -> Futures.reduceAll(remoteUpdates,\n                                    new Pair<>(champ.get(), updated.get()),\n                                    (p, t) -> keyHasher.apply(t.left)\n                                            .thenCompose(hash -> p.left.put(owner, writer, t.left, hash, 0, t.middle, t.right,\n                                                    bitWidth, maxHashCollisionsPerLevel,\n                                                    mirrorBat, keyHasher, tid, storage, writeHasher, p.right)),\n                                    (a, b) -> b));\n                });\n    }\n\n    public static <V extends Cborable> CompletableFuture<Boolean> applyToDiff(\n            PublicKeyHash owner,\n            MaybeMultihash original,\n            MaybeMultihash updated,\n            int depth,\n            Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher,\n            List<Champ.KeyElement<V>> higherLeftMappings,\n            List<Champ.KeyElement<V>> higherRightMappings,\n            Consumer<Triple<ByteArrayWrapper, Optional<V>, Optional<V>>> consumer,\n            int bitWidth,\n            ContentAddressedStorage storage,\n            Function<Cborable, V> fromCbor) {\n\n        if (updated.equals(original))\n            return CompletableFuture.completedFuture(true);\n        return original.map(h -> storage.get(owner, (Cid)h, Optional.empty())).orElseGet(() -> CompletableFuture.completedFuture(Optional.empty()))\n                .thenApply(rawOpt -> rawOpt.map(y -> Champ.fromCbor(y, fromCbor)))\n                .thenCompose(left -> updated.map(h -> storage.get(owner, (Cid)h, Optional.empty())).orElseGet(() -> CompletableFuture.completedFuture(Optional.empty()))\n                        .thenApply(rawOpt -> rawOpt.map(y -> Champ.fromCbor(y, fromCbor)))\n                        .thenCompose(right -> Champ.hashAndMaskKeys(higherLeftMappings, depth, bitWidth, hasher)\n                                .thenCompose(leftHigherMappingsByBit -> Champ.hashAndMaskKeys(higherRightMappings, depth, bitWidth, hasher)\n                                        .thenCompose(rightHigherMappingsByBit -> {\n\n                                            int leftMax = left.map(c -> Math.max(c.dataMap.length(), c.nodeMap.length())).orElse(0);\n                                            int rightMax = right.map(c -> Math.max(c.dataMap.length(), c.nodeMap.length())).orElse(0);\n                                            int maxBit = Math.max(leftMax, rightMax);\n                                            int leftDataIndex = 0, rightDataIndex = 0, leftNodeCount = 0, rightNodeCount = 0;\n\n                                            List<CompletableFuture<Boolean>> deeperLayers = new ArrayList<>();\n\n                                            for (int i = 0; i < maxBit; i++) {\n                                                // either the payload is present OR higher mappings are non empty OR the champ is absent\n                                                Optional<Champ.HashPrefixPayload<V>> leftPayload = Champ.getElement(i, leftDataIndex, leftNodeCount, left);\n                                                Optional<Champ.HashPrefixPayload<V>> rightPayload = Champ.getElement(i, rightDataIndex, rightNodeCount, right);\n\n                                                List<Champ.KeyElement<V>> leftHigherMappings = leftHigherMappingsByBit.getOrDefault(i, Collections.emptyList());\n                                                List<Champ.KeyElement<V>> leftMappings = leftPayload\n                                                        .filter(p -> !p.isShard())\n                                                        .map(p -> Arrays.asList(p.mappings))\n                                                        .orElse(leftHigherMappings);\n                                                List<Champ.KeyElement<V>> rightHigherMappings = rightHigherMappingsByBit.getOrDefault(i, Collections.emptyList());\n                                                List<Champ.KeyElement<V>> rightMappings = rightPayload\n                                                        .filter(p -> !p.isShard())\n                                                        .map(p -> Arrays.asList(p.mappings))\n                                                        .orElse(rightHigherMappings);\n\n                                                Optional<MaybeMultihash> leftShard = leftPayload\n                                                        .filter(p -> p.isShard())\n                                                        .map(p -> p.link);\n\n                                                Optional<MaybeMultihash> rightShard = rightPayload\n                                                        .filter(p -> p.isShard())\n                                                        .map(p -> p.link);\n\n                                                if (leftShard.isPresent() || rightShard.isPresent()) {\n                                                    deeperLayers.add(applyToDiff(owner,\n                                                            leftShard.orElse(MaybeMultihash.empty()),\n                                                            rightShard.orElse(MaybeMultihash.empty()), depth + 1, hasher,\n                                                            leftMappings, rightMappings, consumer, bitWidth, storage, fromCbor));\n                                                } else {\n                                                    Map<ByteArrayWrapper, Optional<V>> leftMap = leftMappings.stream()\n                                                            .collect(Collectors.toMap(e -> e.key, e -> e.valueHash));\n                                                    Map<ByteArrayWrapper, Optional<V>> rightMap = rightMappings.stream()\n                                                            .collect(Collectors.toMap(e -> e.key, e -> e.valueHash));\n\n                                                    HashSet<ByteArrayWrapper> both = new HashSet<>(leftMap.keySet());\n                                                    both.retainAll(rightMap.keySet());\n\n                                                    for (Map.Entry<ByteArrayWrapper, Optional<V>> entry : leftMap.entrySet()) {\n                                                        if (! both.contains(entry.getKey()))\n                                                            consumer.accept(new Triple<>(entry.getKey(), entry.getValue(), Optional.empty()));\n                                                        else if (! entry.getValue().equals(rightMap.get(entry.getKey())))\n                                                            consumer.accept(new Triple<>(entry.getKey(), entry.getValue(), rightMap.get(entry.getKey())));\n                                                    }\n                                                    for (Map.Entry<ByteArrayWrapper, Optional<V>> entry : rightMap.entrySet()) {\n                                                        if (! both.contains(entry.getKey()))\n                                                            consumer.accept(new Triple<>(entry.getKey(), Optional.empty(), entry.getValue()));\n                                                    }\n                                                }\n\n                                                if (leftPayload.isPresent()) {\n                                                    if (leftPayload.get().isShard())\n                                                        leftNodeCount++;\n                                                    else\n                                                        leftDataIndex++;\n                                                }\n                                                if (rightPayload.isPresent()) {\n                                                    if (rightPayload.get().isShard())\n                                                        rightNodeCount++;\n                                                    else\n                                                        rightDataIndex++;\n                                                }\n                                            }\n\n                                            return Futures.combineAll(deeperLayers).thenApply(x -> true);\n                                        })))\n                );\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/hamt/ChampWrapper.java",
    "content": "package peergos.shared.hamt;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class ChampWrapper<V extends Cborable> implements ImmutableTree<V>\n{\n    public static final int BIT_WIDTH = 3;\n    public static final int MAX_HASH_COLLISIONS_PER_LEVEL = 4;\n\n    public final ContentAddressedStorage storage;\n    public final Hasher writeHasher;\n    public final int bitWidth;\n    public final Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher;\n    public final PublicKeyHash owner;\n    private Pair<Champ<V>, Multihash> root;\n\n    public ChampWrapper(Champ<V> root,\n                        Multihash rootHash,\n                        PublicKeyHash owner,\n                        Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher,\n                        ContentAddressedStorage storage,\n                        Hasher writeHasher,\n                        int bitWidth) {\n        this.storage = storage;\n        this.writeHasher = writeHasher;\n        this.owner = owner;\n        this.keyHasher = keyHasher;\n        this.root = new Pair<>(root, rootHash);\n        this.bitWidth = bitWidth;\n    }\n\n    public Multihash getRoot() {\n        return root.right;\n    }\n\n    public static <V extends Cborable> CompletableFuture<ChampWrapper<V>> create(PublicKeyHash owner,\n                                                                                 Cid rootHash,\n                                                                                 Optional<BatWithId> bat,\n                                                                                 Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher,\n                                                                                 ContentAddressedStorage dht,\n                                                                                 Hasher writeHasher,\n                                                                                 Function<Cborable, V> fromCbor) {\n        return dht.get(owner, rootHash, bat).thenApply(rawOpt -> {\n            if (! rawOpt.isPresent())\n                throw new IllegalStateException(\"Champ root not present: \" + rootHash);\n            return new ChampWrapper<>(Champ.fromCbor(rawOpt.get(), fromCbor), rootHash, owner, hasher, dht, writeHasher, BIT_WIDTH);\n        });\n    }\n\n    public static <V extends Cborable> CompletableFuture<ChampWrapper<V>> create(PublicKeyHash owner,\n                                                                                 SigningPrivateKeyAndPublicHash writer,\n                                                                                 Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher,\n                                                                                 TransactionId tid,\n                                                                                 ContentAddressedStorage dht,\n                                                                                 Hasher writeHasher,\n                                                                                 Function<Cborable, V> fromCbor) {\n        Champ<V> newRoot = Champ.empty(fromCbor);\n        byte[] raw = newRoot.serialize();\n        return writeHasher.sha256(raw)\n                .thenCompose(hash -> writer.secret.signMessage(hash))\n                .thenCompose(signed -> dht.put(owner, writer.publicKeyHash, signed, raw, tid))\n                .thenApply(put -> new ChampWrapper<>(newRoot, put, owner, hasher, dht, writeHasher, BIT_WIDTH));\n    }\n\n    /**\n     *\n     * @param rawKey\n     * @return value stored under rawKey\n     * @throws IOException\n     */\n    @Override\n    public CompletableFuture<Optional<V>> get(byte[] rawKey) {\n        ByteArrayWrapper key = new ByteArrayWrapper(rawKey);\n        return keyHasher.apply(key)\n                .thenCompose(keyHash -> root.left.get(owner, key, keyHash, 0, BIT_WIDTH, storage));\n    }\n\n    /**\n     *\n     * @param rawKey\n     * @param value\n     * @return hash of new tree root\n     * @throws IOException\n     */\n    @Override\n    public CompletableFuture<Multihash> put(PublicKeyHash owner,\n                                            SigningPrivateKeyAndPublicHash writer,\n                                            byte[] rawKey,\n                                            Optional<V> existing,\n                                            V value,\n                                            Optional<BatId> mirrorBat,\n                                            TransactionId tid) {\n        ByteArrayWrapper key = new ByteArrayWrapper(rawKey);\n        return keyHasher.apply(key)\n                .thenCompose(keyHash -> root.left.put(owner, writer, key, keyHash, 0, existing, Optional.of(value),\n                        BIT_WIDTH, MAX_HASH_COLLISIONS_PER_LEVEL, mirrorBat, keyHasher, tid, storage, writeHasher, root.right))\n                .thenCompose(newRoot -> commit(writer, newRoot));\n    }\n\n    /**\n     *\n     * @param rawKey\n     * @return hash of new tree root\n     * @throws IOException\n     */\n    @Override\n    public CompletableFuture<Multihash> remove(PublicKeyHash owner,\n                                               SigningPrivateKeyAndPublicHash writer,\n                                               byte[] rawKey,\n                                               Optional<V> existing,\n                                               Optional<BatId> mirrorBat,\n                                               TransactionId tid) {\n        ByteArrayWrapper key = new ByteArrayWrapper(rawKey);\n        return keyHasher.apply(key)\n                .thenCompose(keyHash -> root.left.remove(owner, writer, key, keyHash, 0, existing,\n                        BIT_WIDTH, MAX_HASH_COLLISIONS_PER_LEVEL, mirrorBat, tid, storage, writeHasher, root.right))\n                .thenCompose(newRoot -> commit(writer, newRoot));\n    }\n\n    public CompletableFuture<Multihash> removeAll(\n            PublicKeyHash owner,\n            SigningPrivateKeyAndPublicHash writer,\n            List<Pair<byte[], Optional<V>>> keysAndExpected,\n            Optional<BatId> mirrorBat,\n            TransactionId tid) {\n        if (keysAndExpected.isEmpty())\n            return CompletableFuture.completedFuture(root.right);\n        List<CompletableFuture<Pair<ByteArrayWrapper, byte[]>>> hashFutures = keysAndExpected.stream()\n                .map(p -> {\n                    ByteArrayWrapper key = new ByteArrayWrapper(p.left);\n                    return keyHasher.apply(key).thenApply(h -> new Pair<>(key, h));\n                })\n                .collect(Collectors.toList());\n        Map<ByteArrayWrapper, Optional<V>> expectedMap = new HashMap<>();\n        for (Pair<byte[], Optional<V>> p : keysAndExpected)\n            expectedMap.put(new ByteArrayWrapper(p.left), p.right);\n        return Futures.combineAllInOrder(hashFutures)\n                .thenCompose(keysAndHashes ->\n                    root.left.removeAll(owner, writer, keysAndHashes, expectedMap, 0, BIT_WIDTH,\n                            MAX_HASH_COLLISIONS_PER_LEVEL, mirrorBat, tid, storage, writeHasher, root.right))\n                .thenCompose(newRoot -> commit(writer, newRoot));\n    }\n\n    private CompletableFuture<Multihash> commit(SigningPrivateKeyAndPublicHash writer, Pair<Champ<V>, Multihash> newRoot) {\n        root = newRoot;\n        return CompletableFuture.completedFuture(newRoot.right);\n    }\n\n    /**\n     *\n     * @return number of keys stored in tree\n     * @throws IOException\n     */\n    public CompletableFuture<Long> size() {\n        return root.left.size(owner, 0, storage);\n    }\n\n    /**\n     *\n     * @return The combined result of applying the map to all mappings\n     * @throws IOException\n     */\n    public <T> CompletableFuture<T> reduceAllMappings(PublicKeyHash owner,\n                                                      T identity,\n                                                      BiFunction<T, Pair<ByteArrayWrapper, Optional<V>>, CompletableFuture<T>> mapper) {\n        return root.left.reduceAllMappings(owner, identity, mapper, storage);\n    }\n\n    /**\n     *\n     * @return true\n     * @throws IOException\n     */\n    public CompletableFuture<Boolean> applyToAllMappings(PublicKeyHash owner, Function<Pair<ByteArrayWrapper, Optional<V>>, CompletableFuture<Boolean>> mapper) {\n        return root.left.applyToAllMappings(owner, mapper, storage);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/inode/DirectoryInode.java",
    "content": "package peergos.shared.inode;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class DirectoryInode implements Cborable {\n    // Inodes caps are < 512 bytes, champs can have 32 mappings max per node. 512 KiB block size limit => 32 max inlined\n    public static final int MAX_CHILDREN_INLINED = 32;\n\n    public final Either<List<InodeCap>, Champ<InodeCap>> children;\n    private final Hasher writeHasher;\n    public final int bitWidth;\n    public final PublicKeyHash owner;\n    private final Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher;\n    private final ContentAddressedStorage storage;\n\n    public DirectoryInode(List<InodeCap> children,\n                          Hasher writeHasher,\n                          int bitWidth,\n                          PublicKeyHash owner,\n                          Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher,\n                          ContentAddressedStorage storage) {\n        this.children = Either.a(children);\n        this.writeHasher = writeHasher;\n        this.bitWidth = bitWidth;\n        this.owner = owner;\n        this.keyHasher = keyHasher;\n        this.storage = storage;\n    }\n\n    public DirectoryInode(Champ<InodeCap> children,\n                          Hasher writeHasher,\n                          int bitWidth,\n                          PublicKeyHash owner,\n                          Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher,\n                          ContentAddressedStorage storage) {\n        this.children = Either.b(children);\n        this.writeHasher = writeHasher;\n        this.bitWidth = bitWidth;\n        this.owner = owner;\n        this.keyHasher = keyHasher;\n        this.storage = storage;\n    }\n\n    private static Champ<InodeCap> buildChamp(Cborable rootCbor) {\n        return Champ.fromCbor(rootCbor, InodeCap::fromCbor);\n    }\n\n    public CompletableFuture<Optional<InodeCap>> getChild(String name) {\n        if (children.isA())\n            return Futures.of(children.a().stream().filter(i -> i.inode.name.name.equals(name)).findFirst());\n        ByteArrayWrapper key = new ByteArrayWrapper(name.getBytes());\n        return keyHasher.apply(key).thenCompose(keyHash -> children.b().get(owner, key, keyHash, 0, bitWidth, storage));\n    }\n\n    public CompletableFuture<Boolean> hasMoreThanOneChild() {\n        if (children.isA())\n            return Futures.of(children.a().size() > 1);\n        return Futures.of(children.b().hasMultipleMappings());\n    }\n\n    public CompletableFuture<List<InodeCap>> getChildren() {\n        if (children.isA())\n            return Futures.of(children.a());\n        return children.b().reduceAllMappings(owner, new ArrayList<>(), (acc, p) -> {\n            p.right.ifPresent(acc::add);\n            return Futures.of(acc);\n        }, storage);\n    }\n\n    public CompletableFuture<DirectoryInode> addChild(InodeCap child,\n                                                      PublicKeyHash owner,\n                                                      SigningPrivateKeyAndPublicHash writer,\n                                                      TransactionId tid) {\n        if (children.isA() && children.a().size() < MAX_CHILDREN_INLINED)\n            return Futures.of(new DirectoryInode(Stream.concat(children.a().stream().filter(c -> ! c.inode.equals(child.inode)), Stream.of(child))\n                    .collect(Collectors.toList()), writeHasher, bitWidth, owner, keyHasher, storage));\n\n        ByteArrayWrapper key = toChampKey(child);\n        return keyHasher.apply(key).thenCompose(keyHash ->\n                (children.isA() ?\n                        buildChamp(children.a(), owner, writer, writeHasher, bitWidth, keyHasher, storage, tid)\n                                .thenApply(d -> d.children.b()) :\n                        Futures.of(children.b())\n                ).thenCompose(champ -> champ.put(owner, writer, key, keyHash, 0, Optional.empty(), Optional.of(child), bitWidth,\n                        ChampWrapper.MAX_HASH_COLLISIONS_PER_LEVEL, Optional.empty(), keyHasher, tid, storage, writeHasher, null)\n                        .thenApply(rootPair -> new DirectoryInode(rootPair.left, writeHasher, bitWidth, owner, keyHasher, storage)))\n        );\n    }\n\n    private static ByteArrayWrapper toChampKey(InodeCap val) {\n        return new ByteArrayWrapper(val.inode.name.name.getBytes());\n    }\n\n    public CompletableFuture<DirectoryInode> removeChild(InodeCap child,\n                                                         PublicKeyHash owner,\n                                                         SigningPrivateKeyAndPublicHash writer,\n                                                         TransactionId tid) {\n        if (children.isA())\n            return Futures.of(new DirectoryInode(children.a().stream()\n                    .filter(c -> ! c.equals(child))\n                    .collect(Collectors.toList()), writeHasher, bitWidth, owner, keyHasher, storage));\n        ByteArrayWrapper key = new ByteArrayWrapper(child.inode.name.name.getBytes());\n        return keyHasher.apply(key).thenCompose(keyHash ->\n                children.b().remove(owner, writer, key, keyHash, 0, Optional.of(child), bitWidth,\n                        ChampWrapper.MAX_HASH_COLLISIONS_PER_LEVEL, Optional.empty(), tid, storage, writeHasher, null)\n                        .thenApply(rootPair -> new DirectoryInode(rootPair.left, writeHasher, bitWidth, owner, keyHasher, storage)));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        DirectoryInode that = (DirectoryInode) o;\n        return children.equals(that.children);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(children);\n    }\n\n    public static CompletableFuture<DirectoryInode> buildChamp(List<InodeCap> children,\n                                                               PublicKeyHash owner,\n                                                               SigningPrivateKeyAndPublicHash writer,\n                                                               Hasher writeHasher,\n                                                               int bitWidth,\n                                                               Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher,\n                                                               ContentAddressedStorage storage,\n                                                               TransactionId tid) {\n        return Futures.reduceAll(children, Champ.empty(InodeCap::fromCbor),\n                (c, v) -> keyHasher.apply(toChampKey(v)).thenCompose(keyHash ->\n                        c.put(owner, writer, toChampKey(v), keyHash, 0, Optional.empty(), Optional.of(v), bitWidth,\n                                ChampWrapper.MAX_HASH_COLLISIONS_PER_LEVEL, Optional.empty(), keyHasher, tid, storage, writeHasher, null))\n                        .thenApply(p -> p.left),\n                (a, b) -> b)\n                .thenApply(champ -> new DirectoryInode(champ, writeHasher, bitWidth, owner, keyHasher, storage));\n    }\n\n    public static DirectoryInode empty(Hasher writeHasher,\n                                       int bitWidth,\n                                       PublicKeyHash owner,\n                                       Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher,\n                                       ContentAddressedStorage storage) {\n        return new DirectoryInode(Collections.emptyList(), writeHasher, bitWidth, owner, keyHasher, storage);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        if (children.isA()) {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"c\", new CborObject.CborList(children.a().stream().map(InodeCap::toCbor)\n                    .collect(Collectors.toList())));\n            return CborObject.CborMap.build(state);\n        }\n        return children.b().toCbor();\n    }\n\n    public static DirectoryInode fromCbor(Cborable cbor,\n                                          Hasher writeHasher,\n                                          int bitWidth,\n                                          PublicKeyHash owner,\n                                          Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher,\n                                          ContentAddressedStorage storage) {\n        if (cbor instanceof CborObject.CborMap)\n            return new DirectoryInode(((CborObject.CborMap) cbor).getList(\"c\").value.stream()\n                    .map(InodeCap::fromCbor)\n                    .collect(Collectors.toList()), writeHasher, bitWidth, owner, keyHasher, storage);\n        return new DirectoryInode(buildChamp(cbor), writeHasher, bitWidth, owner, keyHasher, storage);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/inode/Inode.java",
    "content": "package peergos.shared.inode;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class Inode implements Cborable, Comparable<Inode> {\n    public final long inode;\n    public final PathElement name;\n\n    public Inode(long inode, PathElement name) {\n        if (inode < 0)\n            throw new IllegalStateException(\"Inode must be positive!\");\n        this.inode = inode;\n        this.name = name;\n    }\n\n    public Inode(long inode, String name) {\n        this(inode, new PathElement(name));\n    }\n\n    @Override\n    public int compareTo(Inode inode) {\n        return name.name.compareTo(inode.name.name);\n    }\n\n    public InodeCap withoutCap() {\n        return new InodeCap(this, Optional.empty());\n    }\n\n    public String toString() {\n        return inode + \"/\" + name;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Inode inode1 = (Inode) o;\n        return inode == inode1.inode &&\n                name.equals(inode1.name);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(inode, name);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"i\", new CborObject.CborLong(inode));\n        state.put(\"n\", new CborObject.CborString(name.name));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static Inode fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        long inode = m.getLong(\"i\");\n        String name = m.getString(\"n\");\n        return new Inode(inode, new PathElement(name));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/inode/InodeCap.java",
    "content": "package peergos.shared.inode;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\n\npublic class InodeCap implements Cborable {\n    public final Inode inode;\n    public final Optional<AbsoluteCapability> cap;\n\n    public InodeCap(Inode inode, Optional<AbsoluteCapability> cap) {\n        this.inode = inode;\n        this.cap = cap;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        InodeCap inodeCap = (InodeCap) o;\n        return inode.equals(inodeCap.inode) &&\n                cap.equals(inodeCap.cap);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(inode, cap);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"i\", inode.toCbor());\n        cap.map(c -> state.put(\"c\", c.toCbor()));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static InodeCap fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        Inode inode = m.get(\"i\", Inode::fromCbor);\n        Optional<AbsoluteCapability> cap = m.getOptional(\"c\", AbsoluteCapability::fromCbor);\n        return new InodeCap(inode, cap);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/inode/InodeFileSystem.java",
    "content": "package peergos.shared.inode;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** This implements an async Map<Inode, List<InodeCap>>\n *\n */\npublic class InodeFileSystem implements Cborable {\n\n    public final long inodeCount;\n    private final ChampWrapper<DirectoryInode> champ;\n    private final ContentAddressedStorage storage;\n\n    public InodeFileSystem(long inodeCount, ChampWrapper<DirectoryInode> champ, ContentAddressedStorage storage) {\n        this.inodeCount = inodeCount;\n        this.champ = champ;\n        this.storage = storage;\n    }\n\n    private CompletableFuture<InodeFileSystem> putValue(PublicKeyHash owner,\n                                                        SigningPrivateKeyAndPublicHash writer,\n                                                        Inode key,\n                                                        Optional<DirectoryInode> existing,\n                                                        DirectoryInode value,\n                                                        TransactionId tid) {\n        byte[] raw = key.serialize();\n        return champ.put(owner, writer, raw, existing, value, Optional.empty(), tid)\n                .thenApply(h -> new InodeFileSystem(existing.isPresent() ? inodeCount : inodeCount + 1, champ, storage));\n    }\n\n    private CompletableFuture<InodeFileSystem> remove(PublicKeyHash owner,\n                                                      SigningPrivateKeyAndPublicHash writer,\n                                                      Inode key,\n                                                      Optional<DirectoryInode> existing,\n                                                      TransactionId tid) {\n        byte[] raw = key.serialize();\n        return champ.remove(owner, writer, raw, existing, Optional.empty(), tid)\n                .thenApply(h -> new InodeFileSystem(inodeCount, champ, storage));\n    }\n\n    private CompletableFuture<Optional<DirectoryInode>> getValue(Inode key) {\n        byte[] raw = key.serialize();\n        return champ.get(raw);\n    }\n\n    public CompletableFuture<InodeFileSystem> addCap(PublicKeyHash owner,\n                                                     SigningPrivateKeyAndPublicHash writer,\n                                                     String path,\n                                                     AbsoluteCapability cap,\n                                                     TransactionId tid) {\n        String canonPath = TrieNode.canonicalise(path);\n        String[] elements = canonPath.split(\"/\");\n        if (elements.length == 1)\n            throw new IllegalStateException(\"You cannot publish your root directory!\");\n        Inode rootKey = rootKey();\n        return getOrMkdir(owner, writer, Optional.empty(), rootKey, tid)\n                .thenCompose(p -> p.left.addCapRecurse(owner, writer, rootKey, p.right, elements, cap, tid));\n    }\n\n    public static Inode rootKey() {\n        return new Inode(0, \"\");\n    }\n\n    public CompletableFuture<InodeFileSystem> removeCap(PublicKeyHash owner,\n                                                        SigningPrivateKeyAndPublicHash writer,\n                                                        String path,\n                                                        TransactionId tid) {\n        String canonPath = TrieNode.canonicalise(path);\n        String[] elements = canonPath.split(\"/\");\n        Inode rootKey = rootKey();\n        return getValue(rootKey).thenCompose(dirOpt -> {\n            if (dirOpt.isEmpty())\n                return Futures.of(this);\n            return removeCapRecurse(owner, writer, rootKey, dirOpt.get(), elements, tid)\n                    .thenApply(p -> p.left);\n        });\n    }\n\n    /**\n     *\n     * @param owner\n     * @param writer\n     * @param dir\n     * @param remainingPath\n     * @param tid\n     * @return The resulting filesystem, and whether to remove the child from the parent\n     */\n    private CompletableFuture<Pair<InodeFileSystem, Boolean>> removeCapRecurse(PublicKeyHash owner,\n                                                                               SigningPrivateKeyAndPublicHash writer,\n                                                                               Inode dirKey,\n                                                                               DirectoryInode dir,\n                                                                               String[] remainingPath,\n                                                                               TransactionId tid) {\n        if (remainingPath.length == 0) {\n            return Futures.of(new Pair<>(this, true));\n        }\n        return dir.hasMoreThanOneChild()\n                .thenCompose(hasOtherChildren -> dir.getChild(remainingPath[0]).thenCompose(childOpt -> {\n                    if (childOpt.isEmpty())\n                        return Futures.of(new Pair<>(this, false));\n                    if (remainingPath.length == 1)\n                        return dir.removeChild(childOpt.get(), owner, writer, tid)\n                                .thenCompose(updatedDir -> putValue(owner,\n                                        writer, dirKey, Optional.of(dir), updatedDir, tid))\n                                .thenApply(f -> new Pair<>(f, ! hasOtherChildren));\n                    return getValue(childOpt.get().inode)\n                            .thenCompose(childDir ->\n                                    childDir.isPresent() ?\n                                            removeCapRecurse(owner, writer, childOpt.get().inode, childDir.get(), tail(remainingPath), tid)\n                                                    .thenCompose(p -> p.right ?\n                                                            dir.removeChild(childOpt.get(), owner, writer, tid)\n                                                                    .thenCompose(updatedDir -> p.left.putValue(owner,\n                                                                            writer, dirKey, Optional.of(dir), updatedDir, tid))\n                                                                    .thenApply(f -> new Pair<>(f, ! hasOtherChildren)) :\n                                                            Futures.of(new Pair<>(p.left, false))) :\n                                            Futures.of(new Pair<>(this, false)));\n                }));\n    }\n\n    private CompletableFuture<Pair<InodeFileSystem, DirectoryInode>> getOrMkdir(PublicKeyHash owner,\n                                                                                SigningPrivateKeyAndPublicHash writer,\n                                                                                Optional<Pair<Inode, DirectoryInode>> parent,\n                                                                                Inode childDirKey,\n                                                                                TransactionId tid) {\n        return getValue(childDirKey).thenCompose(opt -> {\n            if (opt.isPresent())\n                return Futures.of(new Pair<>(this, opt.get()));\n            DirectoryInode empty = DirectoryInode.empty(champ.writeHasher, champ.bitWidth, champ.owner, champ.keyHasher, storage);\n            return putValue(owner, writer, childDirKey, Optional.empty(), empty, tid)\n                    .thenCompose(f -> {\n                        if (parent.isEmpty()) // we are the root\n                            return Futures.of(f);\n                        return parent.get().right.addChild(childDirKey.withoutCap(), owner, writer, tid)\n                                .thenCompose(updatedParent -> f.putValue(owner, writer, parent.get().left,\n                                        Optional.of(parent.get().right), updatedParent, tid));\n                    }).thenApply(f -> new Pair<>(f, empty));\n        });\n    }\n\n    private CompletableFuture<InodeFileSystem> addCapRecurse(PublicKeyHash owner,\n                                                             SigningPrivateKeyAndPublicHash writer,\n                                                             Inode dirKey,\n                                                             DirectoryInode dir,\n                                                             String[] remainingPath,\n                                                             AbsoluteCapability cap,\n                                                             TransactionId tid) {\n        if (remainingPath.length == 1) {\n            // add the cap to this directory\n            return dir.getChild(remainingPath[0])\n                    .thenCompose(existing -> {\n                        Inode childKey = existing.map(ic -> ic.inode)\n                                .orElseGet(() -> new Inode(inodeCount, remainingPath[0]));\n                        return dir.addChild(new InodeCap(childKey, Optional.of(cap)), owner, writer, tid)\n                                .thenCompose(updatedDir -> putValue(owner, writer, dirKey, Optional.of(dir), updatedDir, tid));\n                    });\n        }\n        return dir.getChild(remainingPath[0])\n                .thenCompose(childCapOpt -> {\n                    if (childCapOpt.isPresent())\n                        return getValue(childCapOpt.get().inode)\n                                .thenCompose(childOpt -> {\n                                    if (childOpt.isPresent())\n                                        return addCapRecurse(owner, writer, childCapOpt.get().inode,\n                                                childOpt.get(), tail(remainingPath), cap, tid);\n                                    // Here a cap was published to a child dir, but not to any descendants of it yet\n                                    Inode newDir = new Inode(inodeCount, remainingPath[0]);\n                                    // parent is absent so we don't overwrite existing entry there\n                                    Optional<Pair<Inode, DirectoryInode>> parent = Optional.empty();\n                                    return getOrMkdir(owner, writer, parent, newDir, tid)\n                                            .thenCompose(p -> p.left.addCapRecurse(owner, writer, newDir, p.right, tail(remainingPath), cap, tid));\n                                });\n                    Inode newDir = new Inode(inodeCount, remainingPath[0]);\n                    return getOrMkdir(owner, writer, Optional.of(new Pair<>(dirKey, dir)), newDir, tid)\n                            .thenCompose(p -> p.left.addCapRecurse(owner, writer, newDir, p.right, tail(remainingPath), cap, tid));\n                });\n    }\n\n    /**\n     *\n     * @param path\n     * @return The most privileged cap to access the requested path, and any remaining path from the cap\n     */\n    public CompletableFuture<Optional<Pair<InodeCap, String>>> getByPath(String path) {\n        String canonPath = TrieNode.canonicalise(path);\n        String[] elements = canonPath.split(\"/\");\n        InodeCap start = new InodeCap(rootKey(), Optional.empty());\n        return getByPathRecurse(start, elements);\n    }\n\n    public CompletableFuture<List<InodeCap>> listDirectory(String path) {\n        String canonPath = TrieNode.canonicalise(path);\n        String[] elements = canonPath.isEmpty() ? new String[0] : canonPath.split(\"/\");\n        InodeCap start = new InodeCap(rootKey(), Optional.empty());\n        return listDirectoryRecurse(start, elements);\n    }\n\n    private CompletableFuture<List<InodeCap>> listDirectoryRecurse(InodeCap current, String[] elements) {\n        return getValue(current.inode)\n                .thenCompose(dir -> {\n                    if (dir.isEmpty())\n                        return Futures.of(Collections.emptyList());\n                    if (elements.length == 0)\n                        return dir.get().getChildren();\n                    return dir.get().getChild(elements[0])\n                            .thenCompose(capOpt -> {\n                                if (capOpt.isEmpty())\n                                    return Futures.of(Collections.emptyList());\n\n                                String[] remainder = tail(elements);\n                                return listDirectoryRecurse(capOpt.get(), remainder);\n                            });\n                });\n    }\n\n    private CompletableFuture<Optional<Pair<InodeCap, String>>> getByPathRecurse(InodeCap current, String[] elements) {\n        if (elements.length == 0)\n            return Futures.of(Optional.of(new Pair<>(current, \"\")));\n        return getValue(current.inode)\n                .thenCompose(dir -> dir.isEmpty() ?\n                        Futures.of(Optional.empty()) :\n                        dir.get().getChild(elements[0]))\n                .thenCompose(capOpt -> {\n                    if (capOpt.isEmpty())\n                        return Futures.of(Optional.empty());\n                    // short circuit early if there is a more privileged cap\n                    String[] remainder = tail(elements);\n                    if (capOpt.get().cap.isPresent()) {\n                        String descendantPath = Arrays.stream(remainder).collect(Collectors.joining(\"/\"));\n                        return Futures.of(Optional.of(new Pair<>(capOpt.get(), descendantPath)));\n                    }\n                    return getByPathRecurse(capOpt.get(), remainder);\n                });\n    }\n\n    public Multihash getRoot() {\n        return champ.getRoot();\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(inodeCount));\n        state.put(\"r\", new CborObject.CborMerkleLink(champ.getRoot()));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static CompletableFuture<InodeFileSystem> build(PublicKeyHash owner,\n                                                           Cborable cbor,\n                                                           Hasher hasher,\n                                                           ContentAddressedStorage storage) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        long inodeCount = m.getLong(\"c\");\n        Multihash root = m.getMerkleLink(\"r\");\n        Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher = b -> hasher.sha256(b.data);\n        Function<Cborable, DirectoryInode> fromCbor =\n                c -> DirectoryInode.fromCbor(c, hasher, ChampWrapper.BIT_WIDTH, owner, keyHasher, storage);\n        return ChampWrapper.create(owner, (Cid)root, Optional.empty(), keyHasher, storage, hasher, fromCbor)\n                .thenApply(cw -> new InodeFileSystem(inodeCount, cw, storage));\n    }\n\n    public static CompletableFuture<InodeFileSystem> createEmpty(PublicKeyHash owner,\n                                                                 SigningPrivateKeyAndPublicHash writer,\n                                                                 ContentAddressedStorage storage,\n                                                                 Hasher hasher,\n                                                                 TransactionId tid) {\n        Function<ByteArrayWrapper, CompletableFuture<byte[]>> keyHasher = b -> hasher.sha256(b.data);\n        Function<Cborable, DirectoryInode> fromCbor =\n                c -> DirectoryInode.fromCbor(c, hasher, ChampWrapper.BIT_WIDTH, owner, keyHasher, storage);\n        return ChampWrapper.create(owner, writer, keyHasher, tid, storage, hasher, fromCbor)\n                .thenApply(cw -> new InodeFileSystem(0, cw, storage));\n    }\n\n    private static String[] tail(String[] in) {\n        return Arrays.copyOfRange(in, Math.min(1, in.length), in.length);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/inode/PathElement.java",
    "content": "package peergos.shared.inode;\n\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\n\npublic class PathElement {\n    public final String name;\n\n    public PathElement(String name) {\n        if (name.contains(\"/\") || name.length() > FileProperties.MAX_FILE_NAME_SIZE)\n            throw new IllegalStateException(\"Invalid path element\");\n        this.name = name;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        PathElement that = (PathElement) o;\n        return name.equals(that.name);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(name);\n    }\n\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/Cid.java",
    "content": "package peergos.shared.io.ipfs;\n\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\n\npublic class Cid extends Multihash {\n    public static final int V0 = 0;\n    public static final int V1 = 1;\n\n    public static final class CidEncodingException extends RuntimeException {\n\n        public CidEncodingException(String message) {\n            super(message);\n        }\n    }\n\n    public enum Codec {\n        Raw(0x55),\n        DagProtobuf(0x70),\n        DagCbor(0x71),\n        LibP2pKey(0x72);\n\n        public long type;\n\n        Codec(long type) {\n            this.type = type;\n        }\n\n        private static Map<Long, Codec> lookup = new TreeMap<>();\n        static {\n            for (Codec c: Codec.values())\n                lookup.put(c.type, c);\n        }\n\n        public static Codec lookup(long c) {\n            if (!lookup.containsKey(c))\n                throw new IllegalStateException(\"Unknown Codec type: \" + c);\n            return lookup.get(c);\n        }\n    }\n\n    public final long version;\n    public final Codec codec;\n\n    public Cid(long version, Codec codec, Multihash.Type type, byte[] hash) {\n        super(type, hash);\n        this.version = version;\n        this.codec = codec;\n    }\n\n    public static Cid build(long version, Codec codec, Multihash h) {\n        return new Cid(version, codec, h.type, h.getHash());\n    }\n\n    private byte[] toBytesV0() {\n        return super.toBytes();\n    }\n\n    private byte[] toBytesV1() {\n        try {\n            ByteArrayOutputStream res = new ByteArrayOutputStream();\n            putUvarint(res, version);\n            putUvarint(res, codec.type);\n            super.serializeObj(res);\n            return res.toByteArray();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public byte[] toBytes() {\n        if (version == V0)\n            return toBytesV0();\n        else if (version == V1)\n            return toBytesV1();\n        throw new IllegalStateException(\"Unknown cid version: \" + version);\n    }\n\n    public boolean isRaw() {\n        return codec == Codec.Raw;\n    }\n\n    @Override\n    public Multihash bareMultihash() {\n        return new Multihash(type, getHash());\n    }\n\n    @Override\n    public String toString() {\n        if (version == V0) {\n            return super.toString();\n        } else if (version == V1) {\n            return Multibase.encode(Multibase.Base.Base58BTC, toBytesV1());\n        }\n        throw new IllegalStateException(\"Unknown Cid version: \" + version);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (! (o instanceof Multihash)) return false;\n        if (!super.equals(o)) return false;\n\n        if (o instanceof Cid) {\n            Cid cid = (Cid) o;\n\n            if (version != cid.version) return false;\n            return codec == cid.codec;\n        }\n        // o must be a Multihash\n        return version == 0 && super.equals(o);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = super.hashCode();\n        if (version == 0)\n            return result;\n        result = 31 * result + (int) (version ^ (version >>> 32));\n        result = 31 * result + (codec != null ? codec.hashCode() : 0);\n        return result;\n    }\n\n    public static Cid buildV0(Multihash h) {\n        return Cid.build(V0, Codec.DagProtobuf, h);\n    }\n\n    public static Cid buildCidV1(Codec c, Multihash.Type type, byte[] hash) {\n        return new Cid(V1, c, type, hash);\n    }\n\n    public static Cid decode(String v) {\n        if (v.length() < 2)\n            throw new IllegalStateException(\"Cid too short!\");\n\n        // support legacy format\n        if (v.length() == 46 && v.startsWith(\"Qm\"))\n            return buildV0(Multihash.fromBase58(v));\n\n        byte[] data = Multibase.decode(v);\n        return cast(data);\n    }\n\n    public static Cid decodePeerId(String peerId) {\n        if (peerId.startsWith(\"1\")) {\n            // convert base58 encoded identity multihash to cidV1\n            Multihash hash = Multihash.decode(Base58.decode(peerId));\n            return new Cid(1, Cid.Codec.LibP2pKey, hash.type, hash.getHash());\n        }\n        return Cid.decode(peerId);\n    }\n\n    public static Cid cast(byte[] data) {\n        if (data.length == 34 && data[0] == 18 && data[1] == 32)\n            return buildV0(Multihash.decode(data));\n\n        InputStream in = new ByteArrayInputStream(data);\n        try {\n            long version = readVarint(in);\n            if (version != V0 && version != V1)\n                throw new CidEncodingException(\"Invalid Cid version number: \" + version);\n\n            long codec = readVarint(in);\n            Multihash hash = Multihash.deserializeObj(in);\n\n            return new Cid(version, Codec.lookup(codec), hash.type, hash.getHash());\n        } catch (Exception e) {\n            throw new CidEncodingException(\"Invalid cid bytes: \" + ArrayOps.bytesToHex(data));\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/MultiAddress.java",
    "content": "package peergos.shared.io.ipfs;\n\n\nimport java.io.*;\nimport java.util.*;\n\npublic class MultiAddress\n{\n    private final byte[] raw;\n\n    public MultiAddress(Multihash hash) {\n        this(\"/ipfs/\" + hash.toString());\n    }\n\n    public MultiAddress(String address) {\n        this(decodeFromString(address));\n    }\n\n    public MultiAddress(byte[] raw) {\n        encodeToString(raw); // check validity\n        this.raw = raw;\n    }\n\n    public byte[] getBytes() {\n        return Arrays.copyOfRange(raw, 0, raw.length);\n    }\n\n    public boolean isTCPIP() {\n        String[] parts = toString().substring(1).split(\"/\");\n        if (parts.length != 4)\n            return false;\n        if (!parts[0].startsWith(\"ip\"))\n            return false;\n        if (!parts[2].equals(\"tcp\"))\n            return false;\n        return true;\n    }\n\n    public String getHost() {\n        String[] parts = toString().substring(1).split(\"/\");\n        if (parts[0].startsWith(\"ip\"))\n            return parts[1];\n        throw new IllegalStateException(\"This multiaddress doesn't have a host: \"+toString());\n    }\n\n    public int getTCPPort() {\n        String[] parts = toString().substring(1).split(\"/\");\n        if (parts[2].startsWith(\"tcp\"))\n            return Integer.parseInt(parts[3]);\n        throw new IllegalStateException(\"This multiaddress doesn't have a tcp port: \"+toString());\n    }\n\n    private static byte[] decodeFromString(String addr) {\n        while (addr.endsWith(\"/\"))\n            addr = addr.substring(0, addr.length()-1);\n        String[] parts = addr.split(\"/\");\n        if (parts[0].length() != 0)\n            throw new IllegalStateException(\"MultiAddress must start with a /\");\n\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        try {\n            for (int i = 1; i < parts.length;) {\n                String part = parts[i++];\n                Protocol p = Protocol.get(part);\n                p.appendCode(bout);\n                if (p.size() == 0)\n                    continue;\n\n                String component = parts[i++];\n                if (component.length() == 0)\n                    throw new IllegalStateException(\"Protocol requires address, but non provided!\");\n\n                bout.write(p.addressToBytes(component));\n            }\n            return bout.toByteArray();\n        } catch (IOException e) {\n            throw new IllegalStateException(\"Error decoding multiaddress: \"+addr);\n        }\n    }\n\n    private static String encodeToString(byte[] raw) {\n        StringBuilder b = new StringBuilder();\n        InputStream in = new ByteArrayInputStream(raw);\n        try {\n            while (true) {\n                int code = (int)Protocol.readVarint(in);\n                Protocol p = Protocol.get(code);\n                b.append(\"/\" + p.name());\n                if (p.size() == 0)\n                    continue;\n\n                String addr = p.readAddress(in);\n                if (addr.length() > 0)\n                    b.append(\"/\" +addr);\n            }\n        }\n        catch (EOFException eof) {}\n        catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return b.toString();\n    }\n\n    @Override\n    public String toString() {\n        return encodeToString(raw);\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (!(other instanceof MultiAddress))\n            return false;\n        return Arrays.equals(raw, ((MultiAddress) other).raw);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(raw);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/Multihash.java",
    "content": "package peergos.shared.io.ipfs;\n\nimport jsinterop.annotations.JsConstructor;\nimport jsinterop.annotations.JsIgnore;\nimport jsinterop.annotations.JsType;\nimport peergos.shared.io.ipfs.bases.*;\n\nimport java.io.*;\nimport java.util.*;\n\n/** Note we don't support the full range of Multihash types, because some of them are insecure\n *\n */\n@JsType\npublic class Multihash implements Comparable<Multihash> {\n    public static final int LEGACY_MAX_IDENTITY_HASH_SIZE = 4112;\n    public static final int MAX_IDENTITY_HASH_SIZE = 36; // can handle 32 byte Ed25519/Curve25519 public keys plus our type annotation\n\n    @JsType\n    public enum Type {\n        id(0, -1),\n        sha2_256(0x12, 32),\n        sha2_512(0x13, 64),\n        sha3(0x14, 64),\n        blake2b(0x40, 64),\n        blake2s(0x41, 32);\n\n        public int index, length;\n\n        Type(int index, int length) {\n            this.index = index;\n            this.length = length;\n        }\n\n        private static Map<Integer, Type> lookup = new TreeMap<>();\n        static {\n            for (Type t: Type.values())\n                lookup.put(t.index, t);\n        }\n\n        public static Type lookup(int t) {\n            if (!lookup.containsKey(t))\n                throw new IllegalStateException(\"Unknown Multihash type: \"+t);\n            return lookup.get(t);\n        }\n    }\n\n    public final Type type;\n    private final byte[] hash;\n\n    @JsConstructor\n    public Multihash(Type type, byte[] hash) {\n        if (hash.length > 127 && type != Type.id)\n            throw new IllegalStateException(\"Unsupported hash size: \"+hash.length);\n        // This check can be changed to non legacy value once all existing data has been migrated\n        if (hash.length > LEGACY_MAX_IDENTITY_HASH_SIZE)\n            throw new IllegalStateException(\"Unsupported hash size: \"+hash.length);\n        if (hash.length != type.length && type != Type.id)\n            throw new IllegalStateException(\"Incorrect hash length: \" + hash.length + \" != \"+type.length);\n        this.type = type;\n        this.hash = hash;\n    }\n\n    public boolean isIdentity() {\n        return type == Type.id;\n    }\n\n    public Multihash bareMultihash() {\n        return this;\n    }\n\n    public static Multihash decode(byte[] multihash) {\n        return new Multihash(Type.lookup(multihash[0] & 0xff), Arrays.copyOfRange(multihash, 2, multihash.length));\n    }\n    @Override\n    public int compareTo(Multihash that) {\n        int compare = Integer.compare(this.hash.length, that.hash.length);\n        if (compare != 0)\n            return compare;\n        for (int i = 0; i < this.hash.length; i++) {\n            compare = Byte.compare(this.hash[i], that.hash[i]);\n            if (compare != 0)\n                return compare;\n        }\n        return Integer.compare(type.index, that.type.index);\n    }\n\n    public byte[] toBytes() {\n        try {\n            ByteArrayOutputStream res = new ByteArrayOutputStream();\n            putUvarint(res, type.index);\n            putUvarint(res, hash.length);\n            res.write(hash);\n            return res.toByteArray();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public byte[] getHash() {\n        return Arrays.copyOfRange(hash, 0, hash.length);\n    }\n    @SuppressWarnings(\"unusable-by-js\")\n    public void serializeObj(OutputStream out) {\n        try {\n            putUvarint(out, type.index);\n            putUvarint(out, hash.length);\n            out.write(hash);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n    @SuppressWarnings(\"unusable-by-js\")\n    public static Multihash deserializeObj(InputStream din) throws IOException {\n        int type = (int)readVarint(din);\n        int len = (int)readVarint(din);\n        Type t = Type.lookup(type);\n        byte[] hash = new byte[len];\n        int total = 0;\n        while (total < len) {\n            int read = din.read(hash);\n            if (read < 0)\n                throw new EOFException();\n            else\n                total += read;\n        }\n        return new Multihash(t, hash);\n    }\n\n    @Override\n    public String toString() {\n        return toBase58();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (!(o instanceof Multihash))\n            return false;\n        return type == ((Multihash) o).type && Arrays.equals(hash, ((Multihash) o).hash);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(hash) ^ type.hashCode();\n    }\n\n    public String toBase58() {\n        return Base58.encode(toBytes());\n    }\n\n    public static Multihash fromBase58(String base58) {\n        return Multihash.decode(Base58.decode(base58));\n    }\n\n    @JsIgnore\n    public static long readVarint(InputStream in) throws IOException {\n        long x = 0;\n        int s=0;\n        for (int i=0; i < 10; i++) {\n            int b = in.read();\n            if (b < 0x80) {\n                if (i == 9 && b > 1) {\n                    throw new IllegalStateException(\"Overflow reading varint!\");\n                } else if (b == 0 && s > 0) // We should never finish on a zero byte if there is more than 1 byte\n                    throw new IllegalStateException(\"Non minimal varint encoding!\");\n                return x | (((long)b) << s);\n            }\n            x |= ((long)b & 0x7f) << s;\n            s += 7;\n        }\n        throw new IllegalStateException(\"Varint too long!\");\n    }\n\n    @JsIgnore\n    public static void putUvarint(OutputStream out, long x) throws IOException {\n        while (x >= 0x80) {\n            out.write((byte)(x | 0x80));\n            x >>= 7;\n        }\n        out.write((byte)x);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/Protocol.java",
    "content": "package peergos.shared.io.ipfs;\n\nimport peergos.shared.io.ipfs.bases.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\n\npublic class Protocol {\n    public static int LENGTH_PREFIXED_VAR_SIZE = -1;\n    private static final String IPV4_REGEX = \"\\\\A(25[0-5]|2[0-4]\\\\d|[0-1]?\\\\d?\\\\d)(\\\\.(25[0-5]|2[0-4]\\\\d|[0-1]?\\\\d?\\\\d)){3}\\\\z\";\n\n    enum Type {\n        IP4(4, 32, \"ip4\"),\n        TCP(6, 16, \"tcp\"),\n        DCCP(33, 16, \"dccp\"),\n        IP6(41, 128, \"ip6\"),\n        DNS(53, LENGTH_PREFIXED_VAR_SIZE, \"dns\"),\n        DNS4(54, LENGTH_PREFIXED_VAR_SIZE, \"dns4\"),\n        DNS6(55, LENGTH_PREFIXED_VAR_SIZE, \"dns6\"),\n        DNSADDR(56, LENGTH_PREFIXED_VAR_SIZE, \"dnsaddr\"),\n        SCTP(132, 16, \"sctp\"),\n        UDP(273, 16, \"udp\"),\n        WEBRTC_DIRECT(280, 0, \"webrtc-direct\"),\n        WEBRTC(281, 0, \"webrtc\"),\n        UTP(301, 0, \"utp\"),\n        UDT(302, 0, \"udt\"),\n        UNIX(400, LENGTH_PREFIXED_VAR_SIZE, \"unix\"),\n        P2P(421, LENGTH_PREFIXED_VAR_SIZE, \"p2p\"),\n        IPFS(421, LENGTH_PREFIXED_VAR_SIZE, \"ipfs\"),\n        HTTPS(443, 0, \"https\"),\n        ONION(444, 80, \"onion\"),\n        ONION3(445, 296, \"onion3\"),\n        GARLIC64(446, LENGTH_PREFIXED_VAR_SIZE, \"garlic64\"),\n        GARLIC32(447, LENGTH_PREFIXED_VAR_SIZE, \"garlic32\"),\n        QUIC(460, 0, \"quic\"),\n        WS(477, 0, \"ws\"),\n        WSS(478, 0, \"wss\"),\n        P2PCIRCUIT(290, 0, \"p2p-circuit\"),\n        HTTP(480, 0, \"http\");\n\n        public final int code, size;\n        public final String name;\n        private final byte[] encoded;\n\n        Type(int code, int size, String name) {\n            this.code = code;\n            this.size = size;\n            this.name = name;\n            this.encoded = encode(code);\n        }\n\n        static byte[] encode(int code) {\n            byte[] varint = new byte[(32 - Integer.numberOfLeadingZeros(code)+6)/7];\n            putUvarint(varint, code);\n            return varint;\n        }\n    }\n\n    public final Type type;\n\n    public Protocol(Type type) {\n        this.type = type;\n    }\n\n    public void appendCode(OutputStream out) throws IOException {\n        out.write(type.encoded);\n    }\n\n    public boolean isTerminal() {\n        return type == Type.UNIX;\n    }\n\n    public int size() {\n        return type.size;\n    }\n\n    public String name() {\n        return type.name;\n    }\n\n    public int code() {\n        return type.code;\n    }\n\n    @Override\n    public String toString() {\n        return name();\n    }\n\n    public byte[] addressToBytes(String addr) {\n        try {\n            switch (type) {\n                case IP4:\n                    if (! addr.matches(IPV4_REGEX))\n                        throw new IllegalStateException(\"Invalid IPv4 address: \" + addr);\n                    return Inet4Address.getByName(addr).getAddress();\n                case IP6:\n                    return Inet6Address.getByName(addr).getAddress();\n                case TCP:\n                case UDP:\n                case DCCP:\n                case SCTP:\n                    int x = Integer.parseInt(addr);\n                    if (x > 65535)\n                        throw new IllegalStateException(\"Failed to parse \"+type.name+\" address \"+addr + \" (> 65535\");\n                    return new byte[]{(byte)(x >>8), (byte)x};\n                case P2P:\n                case IPFS: {\n                    Multihash hash = Cid.decodePeerId(addr);\n                    ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                    byte[] hashBytes = hash.toBytes();\n                    byte[] varint = new byte[(32 - Integer.numberOfLeadingZeros(hashBytes.length) + 6) / 7];\n                    putUvarint(varint, hashBytes.length);\n                    bout.write(varint);\n                    bout.write(hashBytes);\n                    return bout.toByteArray();\n                }\n                case ONION: {\n                    String[] split = addr.split(\":\");\n                    if (split.length != 2)\n                        throw new IllegalStateException(\"Onion address needs a port: \" + addr);\n\n                    // onion address without the \".onion\" substring\n                    if (split[0].length() != 16)\n                        throw new IllegalStateException(\"failed to parse \" + name() + \" addr: \" + addr + \" not a Tor onion address.\");\n\n                    byte[] onionHostBytes = Multibase.decode(Multibase.Base.Base32.prefix + split[0]);\n                    if (onionHostBytes.length != 10)\n                        throw new IllegalStateException(\"Invalid onion address host: \" + split[0]);\n                    int port = Integer.parseInt(split[1]);\n                    if (port > 65535)\n                        throw new IllegalStateException(\"Port is > 65535: \" + port);\n\n                    if (port < 1)\n                        throw new IllegalStateException(\"Port is < 1: \" + port);\n\n                    ByteArrayOutputStream b = new ByteArrayOutputStream();\n                    DataOutputStream dout = new DataOutputStream(b);\n                    dout.write(onionHostBytes);\n                    dout.writeShort(port);\n                    dout.flush();\n                    return b.toByteArray();\n                }\n                case ONION3: {\n                    String[] split = addr.split(\":\");\n                    if (split.length != 2)\n                        throw new IllegalStateException(\"Onion3 address needs a port: \" + addr);\n\n                    // onion3 address without the \".onion\" substring\n                    if (split[0].length() != 56)\n                        throw new IllegalStateException(\"failed to parse \" + name() + \" addr: \" + addr + \" not a Tor onion3 address.\");\n\n                    byte[] onionHostBytes = Multibase.decode(Multibase.Base.Base32.prefix + split[0]);\n                    if (onionHostBytes.length != 35)\n                        throw new IllegalStateException(\"Invalid onion3 address host: \" + split[0]);\n                    int port = Integer.parseInt(split[1]);\n                    if (port > 65535)\n                        throw new IllegalStateException(\"Port is > 65535: \" + port);\n\n                    if (port < 1)\n                        throw new IllegalStateException(\"Port is < 1: \" + port);\n\n                    ByteArrayOutputStream b = new ByteArrayOutputStream();\n                    DataOutputStream dout = new DataOutputStream(b);\n                    dout.write(onionHostBytes);\n                    dout.writeShort(port);\n                    dout.flush();\n                    return b.toByteArray();\n                } case GARLIC32: {\n                    // an i2p base32 address with a length of greater than 55 characters is\n                    // using an Encrypted Leaseset v2. all other base32 addresses will always be\n                    // exactly 52 characters\n                    if (addr.length() < 55 && addr.length() != 52 || addr.contains(\":\")) {\n                        throw new IllegalStateException(\"Invalid garlic addr: \" + addr + \" not a i2p base32 address. len: \" + addr.length());\n                    }\n\n                    while (addr.length() % 8 != 0) {\n                        addr += \"=\";\n                    }\n\n                    ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                    byte[] hashBytes =  Multibase.decode(Multibase.Base.Base32.prefix + addr);\n                    byte[] varint = new byte[(32 - Integer.numberOfLeadingZeros(hashBytes.length) + 6) / 7];\n                    putUvarint(varint, hashBytes.length);\n                    bout.write(varint);\n                    bout.write(hashBytes);\n                    return bout.toByteArray();\n                } case GARLIC64: {\n                    // i2p base64 address will be between 516 and 616 characters long, depending on certificate type\n                    if (addr.length() < 516 || addr.length() > 616 || addr.contains(\":\")) {\n                        throw new IllegalStateException(\"Invalid garlic addr: \" + addr + \" not a i2p base64 address. len: \" + addr.length());\n                    }\n\n                    ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                    byte[] hashBytes =  Multibase.decode(Multibase.Base.Base64.prefix + addr.replaceAll(\"-\", \"+\").replaceAll(\"~\", \"/\"));\n                    byte[] varint = new byte[(32 - Integer.numberOfLeadingZeros(hashBytes.length) + 6) / 7];\n                    putUvarint(varint, hashBytes.length);\n                    bout.write(varint);\n                    bout.write(hashBytes);\n                    return bout.toByteArray();\n                } case UNIX: {\n                    if (addr.startsWith(\"/\"))\n                        addr = addr.substring(1);\n                    byte[] path = addr.getBytes();\n                    ByteArrayOutputStream b = new ByteArrayOutputStream();\n                    DataOutputStream dout = new DataOutputStream(b);\n                    byte[] length = new byte[(32 - Integer.numberOfLeadingZeros(path.length)+6)/7];\n                    putUvarint(length, path.length);\n                    dout.write(length);\n                    dout.write(path);\n                    dout.flush();\n                    return b.toByteArray();\n                }\n                case DNS4:\n                case DNS6:\n                case DNSADDR: {\n                    ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                    byte[] hashBytes = addr.getBytes();\n                    byte[] varint = new byte[(32 - Integer.numberOfLeadingZeros(hashBytes.length) + 6) / 7];\n                    putUvarint(varint, hashBytes.length);\n                    bout.write(varint);\n                    bout.write(hashBytes);\n                    return bout.toByteArray();\n                }\n                default:\n                    throw new IllegalStateException(\"Unknown multiaddr type: \" + type);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String readAddress(InputStream in) throws IOException {\n        int sizeForAddress = sizeForAddress(in);\n        byte[] buf;\n        switch (type) {\n            case IP4:\n                buf = new byte[sizeForAddress];\n                read(in, buf);\n                return Inet4Address.getByAddress(buf).toString().substring(1);\n            case IP6:\n                buf = new byte[sizeForAddress];\n                read(in, buf);\n                return Inet6Address.getByAddress(buf).toString().substring(1);\n            case TCP:\n            case UDP:\n            case DCCP:\n            case SCTP:\n                return Integer.toString((in.read() << 8) | (in.read()));\n            case IPFS:\n                buf = new byte[sizeForAddress];\n                read(in, buf);\n                return Cid.cast(buf).toString();\n            case ONION: {\n                byte[] host = new byte[10];\n                read(in, host);\n                String port = Integer.toString((in.read() << 8) | (in.read()));\n                return Multibase.encode(Multibase.Base.Base32, host).substring(1) + \":\" + port;\n            } case ONION3: {\n                byte[] host = new byte[35];\n                read(in, host);\n                String port = Integer.toString((in.read() << 8) | (in.read()));\n                return Multibase.encode(Multibase.Base.Base32, host).substring(1) + \":\" + port;\n            } case GARLIC32: {\n                buf = new byte[sizeForAddress];\n                read(in, buf);\n                // an i2p base32 for an Encrypted Leaseset v2 will be at least 35 bytes\n                // long other than that, they will be exactly 32 bytes\n                if (buf.length < 35 && buf.length != 32) {\n                    throw new IllegalStateException(\"Invalid garlic addr length: \" + buf.length);\n                }\n                return Multibase.encode(Multibase.Base.Base32, buf).substring(1);\n            } case GARLIC64: {\n                buf = new byte[sizeForAddress];\n                read(in, buf);\n                // A garlic64 address will always be greater than 386 bytes\n                if (buf.length < 386) {\n                    throw new IllegalStateException(\"Invalid garlic64 addr length: \" + buf.length);\n                }\n                return Multibase.encode(Multibase.Base.Base64, buf).substring(1).replaceAll(\"\\\\+\", \"-\").replaceAll(\"/\", \"~\");\n            } case UNIX:\n                buf = new byte[sizeForAddress];\n                read(in, buf);\n                return new String(buf);\n            case DNS4:\n            case DNS6:\n            case DNSADDR:\n                buf = new byte[sizeForAddress];\n                read(in, buf);\n                return new String(buf);\n        }\n        throw new IllegalStateException(\"Unimplemented protocol type: \"+type.name);\n    }\n\n    private static void read(InputStream in, byte[] b) throws IOException {\n        read(in, b, 0, b.length);\n    }\n\n    private static void read(InputStream in, byte[] b, int offset, int len) throws IOException {\n        int total=0, r=0;\n        while (total < len && r != -1) {\n            r = in.read(b, offset + total, len - total);\n            if (r >=0)\n                total += r;\n        }\n    }\n\n    public int sizeForAddress(InputStream in) throws IOException {\n        if (type.size > 0)\n            return type.size/8;\n        if (type.size == 0)\n            return 0;\n        return (int)readVarint(in);\n    }\n\n    static int putUvarint(byte[] buf, long x) {\n        int i = 0;\n        while (x >= 0x80) {\n            buf[i] = (byte)(x | 0x80);\n            x >>= 7;\n            i++;\n        }\n        buf[i] = (byte)x;\n        return i + 1;\n    }\n\n    static long readVarint(InputStream in) throws IOException {\n        long x = 0;\n        int s=0;\n        for (int i=0; i < 10; i++) {\n            int b = in.read();\n            if (b == -1)\n                throw new EOFException();\n            if (b < 0x80) {\n                if (i > 9 || i == 9 && b > 1) {\n                    throw new IllegalStateException(\"Overflow reading varint\" +(-(i + 1)));\n                }\n                return x | (((long)b) << s);\n            }\n            x |= ((long)b & 0x7f) << s;\n            s += 7;\n        }\n        throw new IllegalStateException(\"Varint too long!\");\n    }\n\n    private static Map<String, Protocol> byName = new HashMap<>();\n    private static Map<Integer, Protocol> byCode = new HashMap<>();\n\n    static {\n        for (Protocol.Type t: Protocol.Type.values()) {\n            Protocol p = new Protocol(t);\n            byName.put(p.name(), p);\n            byCode.put(p.code(), p);\n        }\n\n    }\n\n    public static Protocol get(String name) {\n        if (byName.containsKey(name))\n            return byName.get(name);\n        throw new IllegalStateException(\"No protocol with name: \"+name);\n    }\n\n    public static Protocol get(int code) {\n        if (byCode.containsKey(code))\n            return byCode.get(code);\n        throw new IllegalStateException(\"No protocol with code: \"+code);\n    }\n}"
  },
  {
    "path": "src/peergos/shared/io/ipfs/api/JSONParser.java",
    "content": "package peergos.shared.io.ipfs.api;\n\nimport java.util.*;\n\nimport java.lang.reflect.*;\n\npublic class JSONParser\n{\n    private static char skipSpaces(String json, int[] pos)\n    {\n        while (true)\n        {\n            if (pos[0] >= json.length())\n                return 0;\n            char ch = json.charAt(pos[0]);\n            if (Character.isWhitespace(ch))\n                pos[0]++;\n            else\n                return ch;\n        }\n    }\n\n    private static Boolean parseBoolean(String json, int[] pos)\n    {\n        if (json.regionMatches(pos[0], \"true\", 0, 4))\n        {\n            pos[0] += 4;\n            return Boolean.TRUE;\n        }\n\n        if (json.regionMatches(pos[0], \"false\", 0, 5))\n        {\n            pos[0] += 5;\n            return Boolean.FALSE;\n        }\n\n        return null;\n    }\n\n    private static Number parseNumber(String json, int[] pos)\n    {\n        int endPos = json.length();\n        int startPos = pos[0];\n\n        boolean foundExp = false;\n        boolean foundDot = false;\n        boolean allowPM = true;\n        for (int i=startPos; i<endPos; i++)\n        {\n            char ch = json.charAt(i);\n            if ((ch == 'e') || (ch == 'E'))\n            {\n                if (foundExp)\n                    return null;\n                allowPM = true;\n                foundExp = true;\n                continue;\n            }\n\n            if ((ch == '+') || (ch == '-'))\n            {\n                if (allowPM)\n                {\n                    allowPM = false;\n                    ch = skipSpaces(json, pos);\n                    if (ch == 0)\n                        return null;\n                    else\n                        continue;\n                }\n                else\n                    return null;\n            }\n\n            allowPM = false;\n            if (ch == '.')\n            {\n                if (foundDot)\n                    return null;\n                foundDot = true;\n                continue;\n            }\n\n            if (!Character.isDigit(json.charAt(i)))\n            {\n                pos[0] = endPos = i;\n                break;\n            }\n        }\n\n        if (startPos == endPos)\n            return null;\n\n        String numericString = json.substring(startPos, endPos);\n        try\n        {\n            return Integer.parseInt(numericString);\n        }\n        catch (Exception e) {}\n\n        try\n        {\n            return Long.parseLong(numericString);\n        }\n        catch (Exception e) {}\n\n        try\n        {\n            return Double.parseDouble(numericString);\n        }\n        catch (Exception e) {}\n\n        throw new IllegalStateException(\"Failed to parse JSON number at \"+startPos+\" '\"+numericString+\"'\");\n    }\n\n    private static List parseArray(String json, int[] pos)\n    {\n        int start = pos[0];\n        if (json.charAt(start) != '[')\n            return null;\n        pos[0]++;\n\n        ArrayList result = new ArrayList();\n        while (true)\n        {\n            char ch = skipSpaces(json, pos);\n            if (ch == 0)\n                break;\n            else if (ch == ']')\n            {\n                pos[0]++;\n                return result;\n            }\n            else if (ch == ',')\n            {\n                pos[0]++;\n                if (skipSpaces(json, pos) == 0)\n                    break;\n            }\n\n            Object val = parse(json, pos);\n            result.add(val);\n        }\n        throw new IllegalStateException(\"json Array format at \"+start+\" [\"+(pos[0]-start)+\"]  '\"+json.substring(start)+\"'\");\n    }\n\n    private static Map parseObject(String json, int[] pos)\n    {\n        int start = pos[0];\n        if (json.charAt(start) != '{')\n            return null;\n        pos[0]++;\n\n        Map result = new LinkedHashMap();\n        while (true)\n        {\n            char ch = skipSpaces(json, pos);\n            if (ch == 0)\n                break;\n            else if (ch == '}')\n            {\n                pos[0]++;\n                return result;\n            }\n            else if (ch == ',')\n            {\n                pos[0]++;\n                if (skipSpaces(json, pos) == 0)\n                    break;\n            }\n\n            String key = parseString(json, pos);\n            ch = skipSpaces(json, pos);\n            if (ch == 0)\n                break;\n\n            pos[0]++;\n            if (ch != ':')\n                break;\n\n            Object val = parse(json, pos);\n            result.put(key, val);\n        }\n        throw new IllegalStateException(\"json Object format at \"+pos[0]+\"  [\"+start+\", \"+json.length()+\"]  '\"+json.substring(pos[0])+\"'\");\n    }\n\n    private static String parseString(String json, int[] pos)\n    {\n        int startPos = pos[0];\n        if (json.charAt(startPos) != '\"')\n            return null;\n        pos[0]++;\n\n        boolean isEscape = false;\n        for (int i=startPos+1; i<json.length(); i++)\n        {\n            char ch = json.charAt(i);\n            if (ch == '\\\\')\n            {\n                isEscape = !isEscape;\n                continue;\n            }\n\n            if (ch == '\"')\n            {\n                if (!isEscape)\n                {\n                    pos[0] = i+1;\n                    return json.substring(startPos+1, i);\n                }\n            }\n\n            isEscape = false;\n        }\n\n        throw new IllegalStateException(\"json string at at \"+startPos+\"  '\"+json+\"'\");\n    }\n\n    private static Object parse(String json, int[] pos)\n    {\n        char ch = skipSpaces(json, pos);\n        if (ch == 0)\n            return null;\n        int startPos = pos[0];\n        if (startPos == json.length())\n            return null;\n\n        Object result = parseArray(json, pos);\n        if (result != null)\n            return result;\n\n        result = parseObject(json, pos);\n        if (result != null)\n            return result;\n\n        result = parseBoolean(json, pos);\n        if (result != null)\n            return result;\n\n        result = parseString(json, pos);\n        if (result != null)\n            return result;\n\n        result = parseNumber(json, pos);\n        if (result != null)\n            return result;\n\n        if (json.regionMatches(pos[0], \"null\", 0, 4))\n        {\n            pos[0] += 4;\n            return null;\n        }\n\n        throw new IllegalStateException(\"json object at at \"+startPos+\"  '\"+json+\"'\");\n    }\n\n    public static Object parse(Object json)\n    {\n        if (json == null)\n            return null;\n        return parse(json.toString());\n    }\n\n    public static Object parse(String json)\n    {\n        if (json == null)\n            return null;\n        return parse(json, new int[1]);\n    }\n\n    public static List<Object> parseStream(String json)\n    {\n        if (json == null)\n            return null;\n        int[] pos = new int[1];\n        List<Object> res = new ArrayList<>();\n        json = json.trim();\n        while (pos[0] < json.length())\n            res.add(parse(json, pos));\n        return res;\n    }\n\n    private static void escapeString(String s, StringBuffer buf)\n    {\n        buf.append('\"');\n        for (int i=0; i<s.length(); i++)\n        {\n            char ch = s.charAt(i);\n            if ((ch == '\"') || (ch == '\\\\'))\n                buf.append('\\\\');\n            buf.append(ch);\n        }\n        buf.append('\"');\n    }\n\n    private static void toString(Object obj, StringBuffer buf)\n    {\n        if (obj == null)\n            buf.append(\"null\");\n        else if ((obj instanceof Boolean) || (obj instanceof Number))\n            buf.append(obj.toString());\n        else if (obj instanceof Map)\n        {\n            Map m = (Map) obj;\n            boolean first = true;\n            Iterator itt = m.keySet().iterator();\n\n            buf.append('{');\n            while (itt.hasNext())\n            {\n                if (!first)\n                    buf.append(',');\n\n                String s = (String) itt.next();\n                Object val = m.get(s);\n                escapeString(s, buf);\n                buf.append(\":\");\n                toString(val, buf);\n                first = false;\n            }\n            buf.append('}');\n        }\n        else if (obj instanceof Object[])\n        {\n            Object[] l = (Object[]) obj;\n            boolean first = true;\n\n            buf.append('[');\n            for (int i=0; i<l.length; i++)\n            {\n                if (!first)\n                    buf.append(',');\n\n                toString(l[i], buf);\n                first = false;\n            }\n            buf.append(']');\n        }\n        else if (obj instanceof List)\n        {\n            List l = (List) obj;\n            boolean first = true;\n            Iterator itt = l.iterator();\n\n            buf.append('[');\n            while (itt.hasNext())\n            {\n                if (!first)\n                    buf.append(',');\n\n                Object val = itt.next();\n                toString(val, buf);\n                first = false;\n            }\n            buf.append(']');\n        }\n        else if (obj instanceof String)\n            escapeString(obj.toString(), buf);\n        else\n        {\n        \t/* todo-fix\n            try\n            {\n                Class cls = obj.getClass();\n                Method m = cls.getDeclaredMethod(\"toJSON\", new Class[0]);\n                Object jsonObj = m.invoke(obj, new Object[0]);\n                buf.append(toString(jsonObj));\n            }\n            catch (Exception e)\n            {\n                escapeString(obj.toString(), buf);\n            }*/\n        }\n    }\n\n    public static String toString(Object obj)\n    {\n        StringBuffer buf = new StringBuffer();\n        toString(obj, buf);\n        return buf.toString();\n    }\n\n    public static String stripWhitespace(String src)\n    {\n        boolean inQuote = false, isEscaped = false;\n        StringBuffer buf = new StringBuffer();\n\n        for (int i=0; i<src.length(); i++)\n        {\n            char ch = src.charAt(i);\n\n            if (!inQuote)\n            {\n                if (ch == '\"')\n                {\n                    inQuote = true;\n                    isEscaped = false;\n                }\n                else if (Character.isWhitespace(ch))\n                    continue;\n            }\n            else if (inQuote)\n            {\n                if (ch == '\\\\')\n                    isEscaped = !isEscaped;\n                else if ((ch == '\"') && !isEscaped)\n                    inQuote = false;\n            }\n\n            buf.append(ch);\n        }\n\n        return buf.toString();\n    }\n\n    public static Object getValue(Object json, String path)\n    {\n        String[] parts = path.split(\"\\\\.\");\n\n        for (int i=0; i<parts.length; i++)\n        {\n            int index = -1;\n            String key = parts[i];\n\n            if (key.endsWith(\"]\"))\n            {\n                int b = key.indexOf(\"[\");\n                try\n                {\n                    index = Integer.parseInt(key.substring(b+1, key.length()-1));\n                    key = key.substring(0, b);\n                }\n                catch (Exception e)\n                {\n                    throw new IllegalStateException(\"Path syntax error - invalid index\");\n                }\n            }\n\n            if ((json != null) && (json instanceof Map))\n                json = ((Map) json).get(key);\n            else\n                return null;\n\n            if (index >= 0)\n            {\n                if ((json != null) && (json instanceof List))\n                    json = ((List) json).get(index);\n                else\n                    return null;\n            }\n        }\n\n        return json;\n    }\n}"
  },
  {
    "path": "src/peergos/shared/io/ipfs/api/MerkleNode.java",
    "content": "package peergos.shared.io.ipfs.api;\n\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class MerkleNode {\n    public final Multihash hash;\n    public final Optional<String> name;\n    public final Optional<Integer> size;\n    public final Optional<Integer> type;\n    public final List<MerkleNode> links;\n    public final Optional<byte[]> data;\n\n    public MerkleNode(String hash) {\n        this(hash, Optional.empty());\n    }\n\n    public MerkleNode(String hash, Optional<String> name) {\n        this(hash, name, Optional.empty(), Optional.empty(), Arrays.asList(), Optional.empty());\n    }\n\n    public MerkleNode(String hash, Optional<String> name, Optional<Integer> size, Optional<Integer> type, List<MerkleNode> links, Optional<byte[]> data) {\n        this.name = name;\n        this.hash = Cid.decode(hash);\n        this.size = size;\n        this.type = type;\n        this.links = links;\n        this.data = data;\n    }\n\n    @Override\n    public boolean equals(Object b) {\n        if (!(b instanceof MerkleNode))\n            return false;\n        MerkleNode other = (MerkleNode) b;\n        return hash.equals(other.hash); // ignore name hash says it all\n    }\n\n    @Override\n    public int hashCode() {\n        return hash.hashCode();\n    }\n\n    public static MerkleNode fromJSON(Object rawjson) {\n        if (rawjson instanceof String)\n            return new MerkleNode((String)rawjson);\n        Map json = (Map)rawjson;\n        String hash = (String)json.get(\"Hash\");\n        if (hash == null)\n            hash = (String)json.get(\"Key\");\n        Optional<String> name = json.containsKey(\"Name\") ? Optional.of((String) json.get(\"Name\")): Optional.empty();\n        Optional<Integer> size = json.containsKey(\"Size\") ? Optional.<Integer>empty().of((Integer) json.get(\"Size\")): Optional.<Integer>empty().empty();\n        Optional<Integer> type = json.containsKey(\"Type\") ? Optional.<Integer>empty().of((Integer) json.get(\"Type\")): Optional.<Integer>empty().empty();\n        List<Object> linksRaw = (List<Object>) json.get(\"Links\");\n        List<MerkleNode> links = linksRaw == null ? Collections.EMPTY_LIST : linksRaw.stream().map(x -> MerkleNode.fromJSON(x)).collect(Collectors.toList());\n        Optional<byte[]> data = json.containsKey(\"Data\") ? Optional.of(((String)json.get(\"Data\")).getBytes()): Optional.empty();\n        return new MerkleNode(hash, name, size, type, links, data);\n    }\n\n    public Object toJSON() {\n        Map res = new TreeMap<>();\n        res.put(\"Hash\", hash);\n        res.put(\"Links\", links.stream().map(x -> x.hash).collect(Collectors.toList()));\n        if (data.isPresent())\n            res.put(\"Data\", data.get());\n        if (name.isPresent())\n            res.put(\"Name\", name.get());\n        if (size.isPresent())\n            res.put(\"Size\", size.get());\n        if (type.isPresent())\n            res.put(\"Type\", type.get());\n        return res;\n    }\n\n    public String toJSONString() {\n        return JSONParser.toString(toJSON());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/api/NamedStreamable.java",
    "content": "package peergos.shared.io.ipfs.api;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\n\npublic interface NamedStreamable\n{\n    InputStream getInputStream() throws IOException;\n\n    Optional<String> getName();\n\n    boolean isDirectory();\n\n    default byte[] getContents() throws IOException {\n        InputStream in = getInputStream();\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        byte[] tmp = new byte[4096];\n        int r;\n        while ((r=in.read(tmp))>= 0)\n            bout.write(tmp, 0, r);\n        return bout.toByteArray();\n    }\n\n    class NativeFile implements NamedStreamable {\n        private final File source;\n        private final String relativePath;\n\n        public NativeFile(String relativePath, File source) {\n            this.source = source;\n            this.relativePath = relativePath;\n        }\n\n        public NativeFile(File source) {\n            this(\"\", source);\n        }\n\n        public InputStream getInputStream() throws IOException {\n            return new FileInputStream(source);\n        }\n\n        public boolean isDirectory() {\n            return source.isDirectory();\n        }\n\n        public File getFile() {\n            return source;\n        }\n\n        public Optional<String> getName() {\n            try {\n                return Optional.of(URLEncoder.encode(relativePath + source.getName(), \"UTF-8\"));\n            } catch (UnsupportedEncodingException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    class ByteArrayWrapper implements NamedStreamable {\n        private final Optional<String> name;\n        private final byte[] data;\n\n        public ByteArrayWrapper(byte[] data) {\n            this(Optional.empty(), data);\n        }\n\n        public ByteArrayWrapper(String name, byte[] data) {\n            this(Optional.of(name), data);\n        }\n\n        public ByteArrayWrapper(Optional<String> name, byte[] data) {\n            this.name = name;\n            this.data = data;\n        }\n\n        public boolean isDirectory() {\n            return false;\n        }\n\n        public InputStream getInputStream() throws IOException {\n            return new ByteArrayInputStream(data);\n        }\n\n        public Optional<String> getName() {\n            return name;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/api/Peer.java",
    "content": "package peergos.shared.io.ipfs.api;\n\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.MultiAddress;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\nimport java.util.function.*;\n\npublic class Peer {\n    public final MultiAddress address;\n    public final Multihash id;\n    public final long latency;\n    public final String muxer;\n    public final Object streams;\n\n    public Peer(MultiAddress address, Multihash id, long latency, String muxer, Object streams) {\n        this.address = address;\n        this.id = id;\n        this.latency = latency;\n        this.muxer = muxer;\n        this.streams = streams;\n    }\n\n    public static Peer fromJSON(Object json) {\n        if (! (json instanceof Map))\n            throw new IllegalStateException(\"Incorrect json for Peer: \" + JSONParser.toString(json));\n        Map m = (Map) json;\n        Function<String, String> val = key -> (String) m.get(key);\n        long latency = val.apply(\"Latency\").length() > 0 ? Long.parseLong(val.apply(\"Latency\")) : -1;\n        return new Peer(new MultiAddress(val.apply(\"Addr\")), Cid.decode(val.apply(\"Peer\")), latency, val.apply(\"Muxer\"), val.apply(\"Streams\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Base16.java",
    "content": "package peergos.shared.io.ipfs.bases;\n\nimport peergos.shared.util.*;\n\npublic class Base16 {\n    public static byte[] decode(String hex)\n    {\n        byte[] res = new byte[hex.length()/2];\n        for (int i=0; i < res.length; i++)\n            res[i] = (byte) Integer.parseInt(hex.substring(2*i, 2*i+2), 16);\n        return res;\n    }\n\n    public static String encode(byte[] data)\n    {\n        return ArrayOps.bytesToHex(data);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Base32.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Provides Base32 encoding and decoding as defined by <a href=\"http://www.ietf.org/rfc/rfc4648.txt\">RFC 4648</a>.\n *\n * From https://commons.apache.org/proper/commons-codec/\n *\n * <p>\n * The class can be parameterized in the following manner with various constructors:\n * </p>\n * <ul>\n * <li>Whether to use the \"base32hex\" variant instead of the default \"base32\"</li>\n * <li>Line length: Default 76. Line length that aren't multiples of 8 will still essentially end up being multiples of\n * 8 in the encoded data.\n * <li>Line separator: Default is CRLF (\"\\r\\n\")</li>\n * </ul>\n * <p>\n * This class operates directly on byte streams, and not character streams.\n * </p>\n * <p>\n * This class is thread-safe.\n * </p>\n *\n * @see <a href=\"http://www.ietf.org/rfc/rfc4648.txt\">RFC 4648</a>\n *\n * @since 1.5\n * @version $Id$\n */\npublic class Base32 extends BaseNCodec {\n\n    /**\n     * BASE32 characters are 5 bits in length.\n     * They are formed by taking a block of five octets to form a 40-bit string,\n     * which is converted into eight BASE32 characters.\n     */\n    private static final int BITS_PER_ENCODED_BYTE = 5;\n    private static final int BYTES_PER_ENCODED_BLOCK = 8;\n    private static final int BYTES_PER_UNENCODED_BLOCK = 5;\n\n    /**\n     * Chunk separator per RFC 2045 section 2.1.\n     *\n     * @see <a href=\"http://www.ietf.org/rfc/rfc2045.txt\">RFC 2045 section 2.1</a>\n     */\n    private static final byte[] CHUNK_SEPARATOR = {'\\r', '\\n'};\n\n    /**\n     * This array is a lookup table that translates Unicode characters drawn from the \"Base32 Alphabet\" (as specified\n     * in Table 3 of RFC 4648) into their 5-bit positive integer equivalents. Characters that are not in the Base32\n     * alphabet but fall within the bounds of the array are translated to -1.\n     */\n    private static final byte[] DECODE_TABLE = {\n         //  0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f\n            -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, // 30-3f 2-7\n            -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, // 40-4f A-O\n            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,                     // 50-5a P-Z\n                                                        -1, -1, -1, -1, -1, // 5b - 5f\n            -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, // 60 - 6f a-o\n            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,                     // 70 - 7a p-z/**/\n    };\n\n    /**\n     * This array is a lookup table that translates 5-bit positive integer index values into their \"Base32 Alphabet\"\n     * equivalents as specified in Table 3 of RFC 4648.\n     */\n    private static final byte[] ENCODE_TABLE = {\n            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',\n            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',\n            '2', '3', '4', '5', '6', '7',\n    };\n\n    /**\n     * This array is a lookup table that translates Unicode characters drawn from the \"Base32 Hex Alphabet\" (as\n     * specified in Table 4 of RFC 4648) into their 5-bit positive integer equivalents. Characters that are not in the\n     * Base32 Hex alphabet but fall within the bounds of the array are translated to -1.\n     */\n    private static final byte[] HEX_DECODE_TABLE = {\n         //  0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f\n             0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1, // 30-3f 2-7\n            -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 40-4f A-O\n            25, 26, 27, 28, 29, 30, 31,                                     // 50-56 P-V\n                                        -1, -1, -1, -1, -1, -1, -1, -1, -1, // 57-5f Z-_\n            -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 60-6f `-o\n            25, 26, 27, 28, 29, 30, 31                                      // 70-76 p-v\n    };\n\n    /**\n     * This array is a lookup table that translates 5-bit positive integer index values into their\n     * \"Base32 Hex Alphabet\" equivalents as specified in Table 4 of RFC 4648.\n     */\n    private static final byte[] HEX_ENCODE_TABLE = {\n            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',\n            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',\n            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',\n    };\n\n    /** Mask used to extract 5 bits, used when encoding Base32 bytes */\n    private static final int MASK_5BITS = 0x1f;\n\n    // The static final fields above are used for the original static byte[] methods on Base32.\n    // The private member fields below are used with the new streaming approach, which requires\n    // some state be preserved between calls of encode() and decode().\n\n    /**\n     * Place holder for the bytes we're dealing with for our based logic.\n     * Bitwise operations store and extract the encoding or decoding from this variable.\n     */\n\n    /**\n     * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.\n     * <code>decodeSize = {@link #BYTES_PER_ENCODED_BLOCK} - 1 + lineSeparator.length;</code>\n     */\n    private final int decodeSize;\n\n    /**\n     * Decode table to use.\n     */\n    private final byte[] decodeTable;\n\n    /**\n     * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.\n     * <code>encodeSize = {@link #BYTES_PER_ENCODED_BLOCK} + lineSeparator.length;</code>\n     */\n    private final int encodeSize;\n\n    /**\n     * Encode table to use.\n     */\n    private final byte[] encodeTable;\n\n    /**\n     * Line separator for encoding. Not used when decoding. Only used if lineLength &gt; 0.\n     */\n    private final byte[] lineSeparator;\n\n    /**\n     * Creates a Base32 codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length is 0 (no chunking).\n     * </p>\n     *\n     */\n    public Base32() {\n        this(false);\n    }\n\n    /**\n     * Creates a Base32 codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length is 0 (no chunking).\n     * </p>\n     * @param pad byte used as padding byte.\n     */\n    public Base32(final byte pad) {\n        this(false, pad);\n    }\n\n    /**\n     * Creates a Base32 codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length is 0 (no chunking).\n     * </p>\n     * @param useHex if {@code true} then use Base32 Hex alphabet\n     */\n    public Base32(final boolean useHex) {\n        this(0, null, useHex, PAD_DEFAULT);\n    }\n\n    /**\n     * Creates a Base32 codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length is 0 (no chunking).\n     * </p>\n     * @param useHex if {@code true} then use Base32 Hex alphabet\n     * @param pad byte used as padding byte.\n     */\n    public Base32(final boolean useHex, final byte pad) {\n        this(0, null, useHex, pad);\n    }\n\n    /**\n     * Creates a Base32 codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length is given in the constructor, the line separator is CRLF.\n     * </p>\n     *\n     * @param lineLength\n     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of\n     *            8). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when\n     *            decoding.\n     */\n    public Base32(final int lineLength) {\n        this(lineLength, CHUNK_SEPARATOR);\n    }\n\n    /**\n     * Creates a Base32 codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length and line separator are given in the constructor.\n     * </p>\n     * <p>\n     * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data.\n     * </p>\n     *\n     * @param lineLength\n     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of\n     *            8). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when\n     *            decoding.\n     * @param lineSeparator\n     *            Each line of encoded data will end with this sequence of bytes.\n     * @throws IllegalArgumentException\n     *             The provided lineSeparator included some Base32 characters. That's not going to work!\n     */\n    public Base32(final int lineLength, final byte[] lineSeparator) {\n        this(lineLength, lineSeparator, false, PAD_DEFAULT);\n    }\n\n    /**\n     * Creates a Base32 / Base32 Hex codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length and line separator are given in the constructor.\n     * </p>\n     * <p>\n     * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data.\n     * </p>\n     *\n     * @param lineLength\n     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of\n     *            8). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when\n     *            decoding.\n     * @param lineSeparator\n     *            Each line of encoded data will end with this sequence of bytes.\n     * @param useHex\n     *            if {@code true}, then use Base32 Hex alphabet, otherwise use Base32 alphabet\n     * @throws IllegalArgumentException\n     *             The provided lineSeparator included some Base32 characters. That's not going to work! Or the\n     *             lineLength &gt; 0 and lineSeparator is null.\n     */\n    public Base32(final int lineLength, final byte[] lineSeparator, final boolean useHex) {\n        this(lineLength, lineSeparator, useHex, PAD_DEFAULT);\n    }\n\n    /**\n     * Creates a Base32 / Base32 Hex codec used for decoding and encoding.\n     * <p>\n     * When encoding the line length and line separator are given in the constructor.\n     * </p>\n     * <p>\n     * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data.\n     * </p>\n     *\n     * @param lineLength\n     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of\n     *            8). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when\n     *            decoding.\n     * @param lineSeparator\n     *            Each line of encoded data will end with this sequence of bytes.\n     * @param useHex\n     *            if {@code true}, then use Base32 Hex alphabet, otherwise use Base32 alphabet\n     * @param pad byte used as padding byte.\n     * @throws IllegalArgumentException\n     *             The provided lineSeparator included some Base32 characters. That's not going to work! Or the\n     *             lineLength &gt; 0 and lineSeparator is null.\n     */\n    public Base32(final int lineLength, final byte[] lineSeparator, final boolean useHex, final byte pad) {\n        super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, lineLength,\n                lineSeparator == null ? 0 : lineSeparator.length, pad);\n        if (useHex) {\n            this.encodeTable = HEX_ENCODE_TABLE;\n            this.decodeTable = HEX_DECODE_TABLE;\n        } else {\n            this.encodeTable = ENCODE_TABLE;\n            this.decodeTable = DECODE_TABLE;\n        }\n        if (lineLength > 0) {\n            if (lineSeparator == null) {\n                throw new IllegalArgumentException(\"lineLength \" + lineLength + \" > 0, but lineSeparator is null\");\n            }\n            // Must be done after initializing the tables\n            if (containsAlphabetOrPad(lineSeparator)) {\n                final String sep = StringUtils.newStringUtf8(lineSeparator);\n                throw new IllegalArgumentException(\"lineSeparator must not contain Base32 characters: [\" + sep + \"]\");\n            }\n            this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length;\n            this.lineSeparator = new byte[lineSeparator.length];\n            System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);\n        } else {\n            this.encodeSize = BYTES_PER_ENCODED_BLOCK;\n            this.lineSeparator = null;\n        }\n        this.decodeSize = this.encodeSize - 1;\n\n        if (isInAlphabet(pad) || isWhiteSpace(pad)) {\n            throw new IllegalArgumentException(\"pad must not be in alphabet or whitespace\");\n        }\n    }\n\n    /**\n     * <p>\n     * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once\n     * with the data to decode, and once with inAvail set to \"-1\" to alert decoder that EOF has been reached. The \"-1\"\n     * call is not necessary when decoding, but it doesn't hurt, either.\n     * </p>\n     * <p>\n     * Ignores all non-Base32 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are\n     * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in,\n     * garbage-out philosophy: it will not check the provided data for validity.\n     * </p>\n     *\n     * @param in\n     *            byte[] array of ascii data to Base32 decode.\n     * @param inPos\n     *            Position to start reading data from.\n     * @param inAvail\n     *            Amount of bytes available from input for encoding.\n     * @param context the context to be used\n     *\n     * Output is written to {@link Context#buffer} as 8-bit octets, using {@link Context#pos} as the buffer position\n     */\n    @Override\n    void decode(final byte[] in, int inPos, final int inAvail, final Context context) {\n        // package protected for access from I/O streams\n\n        if (context.eof) {\n            return;\n        }\n        if (inAvail < 0) {\n            context.eof = true;\n        }\n        for (int i = 0; i < inAvail; i++) {\n            final byte b = in[inPos++];\n            if (b == pad) {\n                // We're done.\n                context.eof = true;\n                break;\n            }\n            final byte[] buffer = ensureBufferSize(decodeSize, context);\n            if (b >= 0 && b < this.decodeTable.length) {\n                final int result = this.decodeTable[b];\n                if (result >= 0) {\n                    context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK;\n                    // collect decoded bytes\n                    context.lbitWorkArea = (context.lbitWorkArea << BITS_PER_ENCODED_BYTE) + result;\n                    if (context.modulus == 0) { // we can output the 5 bytes\n                        buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 32) & MASK_8BITS);\n                        buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 24) & MASK_8BITS);\n                        buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS);\n                        buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);\n                        buffer[context.pos++] = (byte) (context.lbitWorkArea & MASK_8BITS);\n                    }\n                }\n            }\n        }\n\n        // Two forms of EOF as far as Base32 decoder is concerned: actual\n        // EOF (-1) and first time '=' character is encountered in stream.\n        // This approach makes the '=' padding characters completely optional.\n        if (context.eof && context.modulus >= 2) { // if modulus < 2, nothing to do\n            final byte[] buffer = ensureBufferSize(decodeSize, context);\n\n            //  we ignore partial bytes, i.e. only multiples of 8 count\n            switch (context.modulus) {\n                case 2 : // 10 bits, drop 2 and output one byte\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 2) & MASK_8BITS);\n                    break;\n                case 3 : // 15 bits, drop 7 and output 1 byte\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 7) & MASK_8BITS);\n                    break;\n                case 4 : // 20 bits = 2*8 + 4\n                    context.lbitWorkArea = context.lbitWorkArea >> 4; // drop 4 bits\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS);\n                    break;\n                case 5 : // 25bits = 3*8 + 1\n                    context.lbitWorkArea = context.lbitWorkArea >> 1;\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS);\n                    break;\n                case 6 : // 30bits = 3*8 + 6\n                    context.lbitWorkArea = context.lbitWorkArea >> 6;\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS);\n                    break;\n                case 7 : // 35 = 4*8 +3\n                    context.lbitWorkArea = context.lbitWorkArea >> 3;\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 24) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS);\n                    break;\n                default:\n                    // modulus can be 0-7, and we excluded 0,1 already\n                    throw new IllegalStateException(\"Impossible modulus \"+context.modulus);\n            }\n        }\n    }\n\n    /**\n     * <p>\n     * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with\n     * the data to encode, and once with inAvail set to \"-1\" to alert encoder that EOF has been reached, so flush last\n     * remaining bytes (if not multiple of 5).\n     * </p>\n     *\n     * @param in\n     *            byte[] array of binary data to Base32 encode.\n     * @param inPos\n     *            Position to start reading data from.\n     * @param inAvail\n     *            Amount of bytes available from input for encoding.\n     * @param context the context to be used\n     */\n    @Override\n    void encode(final byte[] in, int inPos, final int inAvail, final Context context) {\n        // package protected for access from I/O streams\n\n        if (context.eof) {\n            return;\n        }\n        // inAvail < 0 is how we're informed of EOF in the underlying data we're\n        // encoding.\n        if (inAvail < 0) {\n            context.eof = true;\n            if (0 == context.modulus && lineLength == 0) {\n                return; // no leftovers to process and not using chunking\n            }\n            final byte[] buffer = ensureBufferSize(encodeSize, context);\n            final int savedPos = context.pos;\n            switch (context.modulus) { // % 5\n                case 0 :\n                    break;\n                case 1 : // Only 1 octet; take top 5 bits then remainder\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 3) & MASK_5BITS]; // 8-1*5 = 3\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea << 2) & MASK_5BITS]; // 5-3=2\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    break;\n                case 2 : // 2 octets = 16 bits to use\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 11) & MASK_5BITS]; // 16-1*5 = 11\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >>  6) & MASK_5BITS]; // 16-2*5 = 6\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >>  1) & MASK_5BITS]; // 16-3*5 = 1\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea <<  4) & MASK_5BITS]; // 5-1 = 4\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    break;\n                case 3 : // 3 octets = 24 bits to use\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 19) & MASK_5BITS]; // 24-1*5 = 19\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 14) & MASK_5BITS]; // 24-2*5 = 14\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >>  9) & MASK_5BITS]; // 24-3*5 = 9\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >>  4) & MASK_5BITS]; // 24-4*5 = 4\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea <<  1) & MASK_5BITS]; // 5-4 = 1\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    buffer[context.pos++] = pad;\n                    break;\n                case 4 : // 4 octets = 32 bits to use\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 27) & MASK_5BITS]; // 32-1*5 = 27\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 22) & MASK_5BITS]; // 32-2*5 = 22\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 17) & MASK_5BITS]; // 32-3*5 = 17\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 12) & MASK_5BITS]; // 32-4*5 = 12\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >>  7) & MASK_5BITS]; // 32-5*5 =  7\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >>  2) & MASK_5BITS]; // 32-6*5 =  2\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea <<  3) & MASK_5BITS]; // 5-2 = 3\n                    buffer[context.pos++] = pad;\n                    break;\n                default:\n                    throw new IllegalStateException(\"Impossible modulus \"+context.modulus);\n            }\n            context.currentLinePos += context.pos - savedPos; // keep track of current line position\n            // if currentPos == 0 we are at the start of a line, so don't add CRLF\n            if (lineLength > 0 && context.currentLinePos > 0){ // add chunk separator if required\n                System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);\n                context.pos += lineSeparator.length;\n            }\n        } else {\n            for (int i = 0; i < inAvail; i++) {\n                final byte[] buffer = ensureBufferSize(encodeSize, context);\n                context.modulus = (context.modulus+1) % BYTES_PER_UNENCODED_BLOCK;\n                int b = in[inPos++];\n                if (b < 0) {\n                    b += 256;\n                }\n                context.lbitWorkArea = (context.lbitWorkArea << 8) + b; // BITS_PER_BYTE\n                if (0 == context.modulus) { // we have enough bytes to create our output\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 35) & MASK_5BITS];\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 30) & MASK_5BITS];\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 25) & MASK_5BITS];\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 20) & MASK_5BITS];\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 15) & MASK_5BITS];\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 10) & MASK_5BITS];\n                    buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 5) & MASK_5BITS];\n                    buffer[context.pos++] = encodeTable[(int)context.lbitWorkArea & MASK_5BITS];\n                    context.currentLinePos += BYTES_PER_ENCODED_BLOCK;\n                    if (lineLength > 0 && lineLength <= context.currentLinePos) {\n                        System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);\n                        context.pos += lineSeparator.length;\n                        context.currentLinePos = 0;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Returns whether or not the {@code octet} is in the Base32 alphabet.\n     *\n     * @param octet\n     *            The value to test\n     * @return {@code true} if the value is defined in the Base32 alphabet {@code false} otherwise.\n     */\n    @Override\n    public boolean isInAlphabet(final byte octet) {\n        return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Base36.java",
    "content": "package peergos.shared.io.ipfs.bases;\n\nimport java.math.*;\n\npublic class Base36 {\n\n    public static byte[] decode(String in) {\n        byte[] withoutLeadingZeroes = new BigInteger(in, 36).toByteArray();\n        int zeroPrefixLength = zeroPrefixLength(in);\n        byte[] res = new byte[zeroPrefixLength + withoutLeadingZeroes.length];\n        System.arraycopy(withoutLeadingZeroes, 0, res, zeroPrefixLength, withoutLeadingZeroes.length);\n        return res;\n    }\n\n    public static String encode(byte[] in) {\n        String withoutLeadingZeroes = new BigInteger(1, in).toString(36);\n        int zeroPrefixLength = zeroPrefixLength(in);\n        StringBuilder b = new StringBuilder();\n        for (int i=0; i < zeroPrefixLength; i++)\n            b.append(\"0\");\n        b.append(withoutLeadingZeroes);\n        return b.toString();\n    }\n\n    private static int zeroPrefixLength(byte[] bytes) {\n        for (int i = 0; i < bytes.length; i++) {\n            if (bytes[i] != 0) {\n                return i;\n            }\n        }\n        return bytes.length;\n    }\n\n    private static int zeroPrefixLength(String in) {\n        for (int i = 0; i < in.length(); i++) {\n            if (in.charAt(i) != '0') {\n                return i;\n            }\n        }\n        return in.length();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Base58.java",
    "content": "package peergos.shared.io.ipfs.bases;\n\n/*\n * Copyright 2011 Google Inc.\n * Copyright 2018 Andreas Schildbach\n *\n * From https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/Base58.java\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.math.BigInteger;\nimport java.util.Arrays;\n\n/**\n * Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings.\n * <p>\n * Note that this is not the same base58 as used by Flickr, which you may find referenced around the Internet.\n * <p>\n * Satoshi explains: why base-58 instead of standard base-64 encoding?\n * <ul>\n * <li>Don't want 0OIl characters that look the same in some fonts and\n *     could be used to create visually identical looking account numbers.</li>\n * <li>A string with non-alphanumeric characters is not as easily accepted as an account number.</li>\n * <li>E-mail usually won't line-break if there's no punctuation to break at.</li>\n * <li>Doubleclicking selects the whole number as one word if it's all alphanumeric.</li>\n * </ul>\n * <p>\n * However, note that the encoding/decoding runs in O(n&sup2;) time, so it is not useful for large data.\n * <p>\n * The basic idea of the encoding is to treat the data bytes as a large number represented using\n * base-256 digits, convert the number to be represented using base-58 digits, preserve the exact\n * number of leading zeros (which are otherwise lost during the mathematical operations on the\n * numbers), and finally represent the resulting base-58 digits as alphanumeric ASCII characters.\n */\npublic class Base58 {\n    public static final char[] ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\".toCharArray();\n    private static final char ENCODED_ZERO = ALPHABET[0];\n    private static final int[] INDEXES = new int[128];\n    static {\n        Arrays.fill(INDEXES, -1);\n        for (int i = 0; i < ALPHABET.length; i++) {\n            INDEXES[ALPHABET[i]] = i;\n        }\n    }\n\n    /**\n     * Encodes the given bytes as a base58 string (no checksum is appended).\n     *\n     * @param input the bytes to encode\n     * @return the base58-encoded string\n     */\n    public static String encode(byte[] input) {\n        if (input.length == 0) {\n            return \"\";\n        }\n        // Count leading zeros.\n        int zeros = 0;\n        while (zeros < input.length && input[zeros] == 0) {\n            ++zeros;\n        }\n        // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters)\n        input = Arrays.copyOf(input, input.length); // since we modify it in-place\n        char[] encoded = new char[input.length * 2]; // upper bound\n        int outputStart = encoded.length;\n        for (int inputStart = zeros; inputStart < input.length; ) {\n            encoded[--outputStart] = ALPHABET[divmod(input, inputStart, 256, 58)];\n            if (input[inputStart] == 0) {\n                ++inputStart; // optimization - skip leading zeros\n            }\n        }\n        // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.\n        while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) {\n            ++outputStart;\n        }\n        while (--zeros >= 0) {\n            encoded[--outputStart] = ENCODED_ZERO;\n        }\n        // Return encoded string (including encoded leading zeros).\n        return new String(encoded, outputStart, encoded.length - outputStart);\n    }\n\n    /**\n     * Decodes the given base58 string into the original data bytes.\n     *\n     * @param input the base58-encoded string to decode\n     * @return the decoded data bytes\n     */\n    public static byte[] decode(String input) {\n        if (input.length() == 0) {\n            return new byte[0];\n        }\n        // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).\n        byte[] input58 = new byte[input.length()];\n        for (int i = 0; i < input.length(); ++i) {\n            char c = input.charAt(i);\n            int digit = c < 128 ? INDEXES[c] : -1;\n            if (digit < 0) {\n                throw new IllegalStateException(\"InvalidCharacter in base 58\");\n            }\n            input58[i] = (byte) digit;\n        }\n        // Count leading zeros.\n        int zeros = 0;\n        while (zeros < input58.length && input58[zeros] == 0) {\n            ++zeros;\n        }\n        // Convert base-58 digits to base-256 digits.\n        byte[] decoded = new byte[input.length()];\n        int outputStart = decoded.length;\n        for (int inputStart = zeros; inputStart < input58.length; ) {\n            decoded[--outputStart] = divmod(input58, inputStart, 58, 256);\n            if (input58[inputStart] == 0) {\n                ++inputStart; // optimization - skip leading zeros\n            }\n        }\n        // Ignore extra leading zeroes that were added during the calculation.\n        while (outputStart < decoded.length && decoded[outputStart] == 0) {\n            ++outputStart;\n        }\n        // Return decoded data (including original number of leading zeros).\n        return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);\n    }\n\n    public static BigInteger decodeToBigInteger(String input) {\n        return new BigInteger(1, decode(input));\n    }\n\n    /**\n     * Divides a number, represented as an array of bytes each containing a single digit\n     * in the specified base, by the given divisor. The given number is modified in-place\n     * to contain the quotient, and the return value is the remainder.\n     *\n     * @param number the number to divide\n     * @param firstDigit the index within the array of the first non-zero digit\n     *        (this is used for optimization by skipping the leading zeros)\n     * @param base the base in which the number's digits are represented (up to 256)\n     * @param divisor the number to divide by (up to 256)\n     * @return the remainder of the division operation\n     */\n    private static byte divmod(byte[] number, int firstDigit, int base, int divisor) {\n        // this is just long division which accounts for the base of the input digits\n        int remainder = 0;\n        for (int i = firstDigit; i < number.length; i++) {\n            int digit = (int) number[i] & 0xFF;\n            int temp = remainder * base + digit;\n            number[i] = (byte) (temp / divisor);\n            remainder = temp % divisor;\n        }\n        return (byte) remainder;\n    }\n}"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Base64.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\nimport java.math.BigInteger;\n\n/**\n * Provides Base64 encoding and decoding as defined by <a href=\"http://www.ietf.org/rfc/rfc2045.txt\">RFC 2045</a>.\n *\n * From https://commons.apache.org/proper/commons-codec/\n *\n * <p>\n * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose\n * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.\n * </p>\n * <p>\n * The class can be parameterized in the following manner with various constructors:\n * </p>\n * <ul>\n * <li>URL-safe mode: Default off.</li>\n * <li>Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of\n * 4 in the encoded data.\n * <li>Line separator: Default is CRLF (\"\\r\\n\")</li>\n * </ul>\n * <p>\n * The URL-safe parameter is only applied to encode operations. Decoding seamlessly handles both modes.\n * </p>\n * <p>\n * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only\n * encode/decode character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252,\n * UTF-8, etc).\n * </p>\n * <p>\n * This class is thread-safe.\n * </p>\n *\n * @see <a href=\"http://www.ietf.org/rfc/rfc2045.txt\">RFC 2045</a>\n * @since 1.0\n * @version $Id$\n */\npublic class Base64 extends BaseNCodec {\n\n    /**\n     * BASE32 characters are 6 bits in length.\n     * They are formed by taking a block of 3 octets to form a 24-bit string,\n     * which is converted into 4 BASE64 characters.\n     */\n    private static final int BITS_PER_ENCODED_BYTE = 6;\n    private static final int BYTES_PER_UNENCODED_BLOCK = 3;\n    private static final int BYTES_PER_ENCODED_BLOCK = 4;\n\n    /**\n     * Chunk separator per RFC 2045 section 2.1.\n     *\n     * <p>\n     * N.B. The next major release may break compatibility and make this field private.\n     * </p>\n     *\n     * @see <a href=\"http://www.ietf.org/rfc/rfc2045.txt\">RFC 2045 section 2.1</a>\n     */\n    static final byte[] CHUNK_SEPARATOR = {'\\r', '\\n'};\n\n    /**\n     * This array is a lookup table that translates 6-bit positive integer index values into their \"Base64 Alphabet\"\n     * equivalents as specified in Table 1 of RFC 2045.\n     *\n     * Thanks to \"commons\" project in ws.apache.org for this code.\n     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/\n     */\n    private static final byte[] STANDARD_ENCODE_TABLE = {\n            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',\n            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',\n            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',\n            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',\n            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'\n    };\n\n    /**\n     * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and /\n     * changed to - and _ to make the encoded Base64 results more URL-SAFE.\n     * This table is only used when the Base64's mode is set to URL-SAFE.\n     */\n    private static final byte[] URL_SAFE_ENCODE_TABLE = {\n            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',\n            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',\n            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',\n            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',\n            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'\n    };\n\n    /**\n     * This array is a lookup table that translates Unicode characters drawn from the \"Base64 Alphabet\" (as specified\n     * in Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64\n     * alphabet but fall within the bounds of the array are translated to -1.\n     *\n     * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both\n     * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit).\n     *\n     * Thanks to \"commons\" project in ws.apache.org for this code.\n     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/\n     */\n    private static final byte[] DECODE_TABLE = {\n        //   0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f\n            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - /\n            52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 30-3f 0-9\n            -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, // 40-4f A-O\n            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _\n            -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o\n            41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51                      // 70-7a p-z\n    };\n\n    /**\n     * Base64 uses 6-bit fields.\n     */\n    /** Mask used to extract 6 bits, used when encoding */\n    private static final int MASK_6BITS = 0x3f;\n\n    // The static final fields above are used for the original static byte[] methods on Base64.\n    // The private member fields below are used with the new streaming approach, which requires\n    // some state be preserved between calls of encode() and decode().\n\n    /**\n     * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able\n     * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch\n     * between the two modes.\n     */\n    private final byte[] encodeTable;\n\n    // Only one decode table currently; keep for consistency with Base32 code\n    private final byte[] decodeTable = DECODE_TABLE;\n\n    /**\n     * Line separator for encoding. Not used when decoding. Only used if lineLength &gt; 0.\n     */\n    private final byte[] lineSeparator;\n\n    /**\n     * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.\n     * <code>decodeSize = 3 + lineSeparator.length;</code>\n     */\n    private final int decodeSize;\n\n    /**\n     * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.\n     * <code>encodeSize = 4 + lineSeparator.length;</code>\n     */\n    private final int encodeSize;\n\n    /**\n     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.\n     * <p>\n     * When encoding the line length is 0 (no chunking), and the encoding table is STANDARD_ENCODE_TABLE.\n     * </p>\n     *\n     * <p>\n     * When decoding all variants are supported.\n     * </p>\n     */\n    public Base64() {\n        this(0);\n    }\n\n    /**\n     * Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode.\n     * <p>\n     * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE.\n     * </p>\n     *\n     * <p>\n     * When decoding all variants are supported.\n     * </p>\n     *\n     * @param urlSafe\n     *            if <code>true</code>, URL-safe encoding is used. In most cases this should be set to\n     *            <code>false</code>.\n     * @since 1.4\n     */\n    public Base64(final boolean urlSafe) {\n        this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);\n    }\n\n    /**\n     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.\n     * <p>\n     * When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is\n     * STANDARD_ENCODE_TABLE.\n     * </p>\n     * <p>\n     * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.\n     * </p>\n     * <p>\n     * When decoding all variants are supported.\n     * </p>\n     *\n     * @param lineLength\n     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of\n     *            4). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when\n     *            decoding.\n     * @since 1.4\n     */\n    public Base64(final int lineLength) {\n        this(lineLength, CHUNK_SEPARATOR);\n    }\n\n    /**\n     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.\n     * <p>\n     * When encoding the line length and line separator are given in the constructor, and the encoding table is\n     * STANDARD_ENCODE_TABLE.\n     * </p>\n     * <p>\n     * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.\n     * </p>\n     * <p>\n     * When decoding all variants are supported.\n     * </p>\n     *\n     * @param lineLength\n     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of\n     *            4). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when\n     *            decoding.\n     * @param lineSeparator\n     *            Each line of encoded data will end with this sequence of bytes.\n     * @throws IllegalArgumentException\n     *             Thrown when the provided lineSeparator included some base64 characters.\n     * @since 1.4\n     */\n    public Base64(final int lineLength, final byte[] lineSeparator) {\n        this(lineLength, lineSeparator, false);\n    }\n\n    /**\n     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.\n     * <p>\n     * When encoding the line length and line separator are given in the constructor, and the encoding table is\n     * STANDARD_ENCODE_TABLE.\n     * </p>\n     * <p>\n     * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.\n     * </p>\n     * <p>\n     * When decoding all variants are supported.\n     * </p>\n     *\n     * @param lineLength\n     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of\n     *            4). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when\n     *            decoding.\n     * @param lineSeparator\n     *            Each line of encoded data will end with this sequence of bytes.\n     * @param urlSafe\n     *            Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode\n     *            operations. Decoding seamlessly handles both modes.\n     *            <b>Note: no padding is added when using the URL-safe alphabet.</b>\n     * @throws IllegalArgumentException\n     *             The provided lineSeparator included some base64 characters. That's not going to work!\n     * @since 1.4\n     */\n    public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) {\n        super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK,\n                lineLength,\n                lineSeparator == null ? 0 : lineSeparator.length);\n        // TODO could be simplified if there is no requirement to reject invalid line sep when length <=0\n        // @see test case Base64Test.testConstructors()\n        if (lineSeparator != null) {\n            if (containsAlphabetOrPad(lineSeparator)) {\n                final String sep = StringUtils.newStringUtf8(lineSeparator);\n                throw new IllegalArgumentException(\"lineSeparator must not contain base64 characters: [\" + sep + \"]\");\n            }\n            if (lineLength > 0){ // null line-sep forces no chunking rather than throwing IAE\n                this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length;\n                this.lineSeparator = new byte[lineSeparator.length];\n                System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);\n            } else {\n                this.encodeSize = BYTES_PER_ENCODED_BLOCK;\n                this.lineSeparator = null;\n            }\n        } else {\n            this.encodeSize = BYTES_PER_ENCODED_BLOCK;\n            this.lineSeparator = null;\n        }\n        this.decodeSize = this.encodeSize - 1;\n        this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;\n    }\n\n    /**\n     * Returns our current encode mode. True if we're URL-SAFE, false otherwise.\n     *\n     * @return true if we're in URL-SAFE mode, false otherwise.\n     * @since 1.4\n     */\n    public boolean isUrlSafe() {\n        return this.encodeTable == URL_SAFE_ENCODE_TABLE;\n    }\n\n    /**\n     * <p>\n     * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with\n     * the data to encode, and once with inAvail set to \"-1\" to alert encoder that EOF has been reached, to flush last\n     * remaining bytes (if not multiple of 3).\n     * </p>\n     * <p><b>Note: no padding is added when encoding using the URL-safe alphabet.</b></p>\n     * <p>\n     * Thanks to \"commons\" project in ws.apache.org for the bitwise operations, and general approach.\n     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/\n     * </p>\n     *\n     * @param in\n     *            byte[] array of binary data to base64 encode.\n     * @param inPos\n     *            Position to start reading data from.\n     * @param inAvail\n     *            Amount of bytes available from input for encoding.\n     * @param context\n     *            the context to be used\n     */\n    @Override\n    void encode(final byte[] in, int inPos, final int inAvail, final Context context) {\n        if (context.eof) {\n            return;\n        }\n        // inAvail < 0 is how we're informed of EOF in the underlying data we're\n        // encoding.\n        if (inAvail < 0) {\n            context.eof = true;\n            if (0 == context.modulus && lineLength == 0) {\n                return; // no leftovers to process and not using chunking\n            }\n            final byte[] buffer = ensureBufferSize(encodeSize, context);\n            final int savedPos = context.pos;\n            switch (context.modulus) { // 0-2\n                case 0 : // nothing to do here\n                    break;\n                case 1 : // 8 bits = 6 + 2\n                    // top 6 bits:\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS];\n                    // remaining 2:\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS];\n                    // URL-SAFE skips the padding to further reduce size.\n                    if (encodeTable == STANDARD_ENCODE_TABLE) {\n                        buffer[context.pos++] = pad;\n                        buffer[context.pos++] = pad;\n                    }\n                    break;\n\n                case 2 : // 16 bits = 6 + 6 + 4\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS];\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS];\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS];\n                    // URL-SAFE skips the padding to further reduce size.\n                    if (encodeTable == STANDARD_ENCODE_TABLE) {\n                        buffer[context.pos++] = pad;\n                    }\n                    break;\n                default:\n                    throw new IllegalStateException(\"Impossible modulus \"+context.modulus);\n            }\n            context.currentLinePos += context.pos - savedPos; // keep track of current line position\n            // if currentPos == 0 we are at the start of a line, so don't add CRLF\n            if (lineLength > 0 && context.currentLinePos > 0) {\n                System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);\n                context.pos += lineSeparator.length;\n            }\n        } else {\n            for (int i = 0; i < inAvail; i++) {\n                final byte[] buffer = ensureBufferSize(encodeSize, context);\n                context.modulus = (context.modulus+1) % BYTES_PER_UNENCODED_BLOCK;\n                int b = in[inPos++];\n                if (b < 0) {\n                    b += 256;\n                }\n                context.ibitWorkArea = (context.ibitWorkArea << 8) + b; //  BITS_PER_BYTE\n                if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS];\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS];\n                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS];\n                    buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS];\n                    context.currentLinePos += BYTES_PER_ENCODED_BLOCK;\n                    if (lineLength > 0 && lineLength <= context.currentLinePos) {\n                        System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);\n                        context.pos += lineSeparator.length;\n                        context.currentLinePos = 0;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * <p>\n     * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once\n     * with the data to decode, and once with inAvail set to \"-1\" to alert decoder that EOF has been reached. The \"-1\"\n     * call is not necessary when decoding, but it doesn't hurt, either.\n     * </p>\n     * <p>\n     * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are\n     * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in,\n     * garbage-out philosophy: it will not check the provided data for validity.\n     * </p>\n     * <p>\n     * Thanks to \"commons\" project in ws.apache.org for the bitwise operations, and general approach.\n     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/\n     * </p>\n     *\n     * @param in\n     *            byte[] array of ascii data to base64 decode.\n     * @param inPos\n     *            Position to start reading data from.\n     * @param inAvail\n     *            Amount of bytes available from input for encoding.\n     * @param context\n     *            the context to be used\n     */\n    @Override\n    void decode(final byte[] in, int inPos, final int inAvail, final Context context) {\n        if (context.eof) {\n            return;\n        }\n        if (inAvail < 0) {\n            context.eof = true;\n        }\n        for (int i = 0; i < inAvail; i++) {\n            final byte[] buffer = ensureBufferSize(decodeSize, context);\n            final byte b = in[inPos++];\n            if (b == pad) {\n                // We're done.\n                context.eof = true;\n                break;\n            }\n            if (b >= 0 && b < DECODE_TABLE.length) {\n                final int result = DECODE_TABLE[b];\n                if (result >= 0) {\n                    context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK;\n                    context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result;\n                    if (context.modulus == 0) {\n                        buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS);\n                        buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);\n                        buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);\n                    }\n                }\n            }\n        }\n\n        // Two forms of EOF as far as base64 decoder is concerned: actual\n        // EOF (-1) and first time '=' character is encountered in stream.\n        // This approach makes the '=' padding characters completely optional.\n        if (context.eof && context.modulus != 0) {\n            final byte[] buffer = ensureBufferSize(decodeSize, context);\n\n            // We have some spare bits remaining\n            // Output all whole multiples of 8 bits and ignore the rest\n            switch (context.modulus) {\n//              case 0 : // impossible, as excluded above\n                case 1 : // 6 bits - ignore entirely\n                    // TODO not currently tested; perhaps it is impossible?\n                    break;\n                case 2 : // 12 bits = 8 + 4\n                    context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits\n                    buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);\n                    break;\n                case 3 : // 18 bits = 8 + 8 + 2\n                    context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits\n                    buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);\n                    buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);\n                    break;\n                default:\n                    throw new IllegalStateException(\"Impossible modulus \"+context.modulus);\n            }\n        }\n    }\n\n    /**\n     * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the\n     * method treats whitespace as valid.\n     *\n     * @param arrayOctet\n     *            byte array to test\n     * @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is empty;\n     *         <code>false</code>, otherwise\n     * @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0.\n     */\n    @Deprecated\n    public static boolean isArrayByteBase64(final byte[] arrayOctet) {\n        return isBase64(arrayOctet);\n    }\n\n    /**\n     * Returns whether or not the <code>octet</code> is in the base 64 alphabet.\n     *\n     * @param octet\n     *            The value to test\n     * @return <code>true</code> if the value is defined in the base 64 alphabet, <code>false</code> otherwise.\n     * @since 1.4\n     */\n    public static boolean isBase64(final byte octet) {\n        return octet == PAD_DEFAULT || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1);\n    }\n\n    /**\n     * Tests a given String to see if it contains only valid characters within the Base64 alphabet. Currently the\n     * method treats whitespace as valid.\n     *\n     * @param base64\n     *            String to test\n     * @return <code>true</code> if all characters in the String are valid characters in the Base64 alphabet or if\n     *         the String is empty; <code>false</code>, otherwise\n     *  @since 1.5\n     */\n    public static boolean isBase64(final String base64) {\n        return isBase64(StringUtils.getBytesUtf8(base64));\n    }\n\n    /**\n     * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the\n     * method treats whitespace as valid.\n     *\n     * @param arrayOctet\n     *            byte array to test\n     * @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is empty;\n     *         <code>false</code>, otherwise\n     * @since 1.5\n     */\n    public static boolean isBase64(final byte[] arrayOctet) {\n        for (int i = 0; i < arrayOctet.length; i++) {\n            if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Encodes binary data using the base64 algorithm but does not chunk the output.\n     *\n     * @param binaryData\n     *            binary data to encode\n     * @return byte[] containing Base64 characters in their UTF-8 representation.\n     */\n    public static byte[] encodeBase64(final byte[] binaryData) {\n        return encodeBase64(binaryData, false);\n    }\n\n    /**\n     * Encodes binary data using the base64 algorithm but does not chunk the output.\n     *\n     * NOTE:  We changed the behaviour of this method from multi-line chunking (commons-codec-1.4) to\n     * single-line non-chunking (commons-codec-1.5).\n     *\n     * @param binaryData\n     *            binary data to encode\n     * @return String containing Base64 characters.\n     * @since 1.4 (NOTE:  1.4 chunked the output, whereas 1.5 does not).\n     */\n    public static String encodeBase64String(final byte[] binaryData) {\n        return StringUtils.newStringUsAscii(encodeBase64(binaryData, false));\n    }\n\n    /**\n     * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The\n     * url-safe variation emits - and _ instead of + and / characters.\n     * <b>Note: no padding is added.</b>\n     * @param binaryData\n     *            binary data to encode\n     * @return byte[] containing Base64 characters in their UTF-8 representation.\n     * @since 1.4\n     */\n    public static byte[] encodeBase64URLSafe(final byte[] binaryData) {\n        return encodeBase64(binaryData, false, true);\n    }\n\n    /**\n     * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The\n     * url-safe variation emits - and _ instead of + and / characters.\n     * <b>Note: no padding is added.</b>\n     * @param binaryData\n     *            binary data to encode\n     * @return String containing Base64 characters\n     * @since 1.4\n     */\n    public static String encodeBase64URLSafeString(final byte[] binaryData) {\n        return StringUtils.newStringUsAscii(encodeBase64(binaryData, false, true));\n    }\n\n    /**\n     * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks\n     *\n     * @param binaryData\n     *            binary data to encode\n     * @return Base64 characters chunked in 76 character blocks\n     */\n    public static byte[] encodeBase64Chunked(final byte[] binaryData) {\n        return encodeBase64(binaryData, true);\n    }\n\n    /**\n     * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.\n     *\n     * @param binaryData\n     *            Array containing binary data to encode.\n     * @param isChunked\n     *            if <code>true</code> this encoder will chunk the base64 output into 76 character blocks\n     * @return Base64-encoded data.\n     * @throws IllegalArgumentException\n     *             Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}\n     */\n    public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) {\n        return encodeBase64(binaryData, isChunked, false);\n    }\n\n    /**\n     * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.\n     *\n     * @param binaryData\n     *            Array containing binary data to encode.\n     * @param isChunked\n     *            if <code>true</code> this encoder will chunk the base64 output into 76 character blocks\n     * @param urlSafe\n     *            if <code>true</code> this encoder will emit - and _ instead of the usual + and / characters.\n     *            <b>Note: no padding is added when encoding using the URL-safe alphabet.</b>\n     * @return Base64-encoded data.\n     * @throws IllegalArgumentException\n     *             Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}\n     * @since 1.4\n     */\n    public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) {\n        return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);\n    }\n\n    /**\n     * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.\n     *\n     * @param binaryData\n     *            Array containing binary data to encode.\n     * @param isChunked\n     *            if <code>true</code> this encoder will chunk the base64 output into 76 character blocks\n     * @param urlSafe\n     *            if <code>true</code> this encoder will emit - and _ instead of the usual + and / characters.\n     *            <b>Note: no padding is added when encoding using the URL-safe alphabet.</b>\n     * @param maxResultSize\n     *            The maximum result size to accept.\n     * @return Base64-encoded data.\n     * @throws IllegalArgumentException\n     *             Thrown when the input array needs an output array bigger than maxResultSize\n     * @since 1.4\n     */\n    public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked,\n                                      final boolean urlSafe, final int maxResultSize) {\n        if (binaryData == null || binaryData.length == 0) {\n            return binaryData;\n        }\n\n        // Create this so can use the super-class method\n        // Also ensures that the same roundings are performed by the ctor and the code\n        final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe);\n        final long len = b64.getEncodedLength(binaryData);\n        if (len > maxResultSize) {\n            throw new IllegalArgumentException(\"Input array too big, the output array would be bigger (\" +\n                len +\n                \") than the specified maximum size of \" +\n                maxResultSize);\n        }\n\n        return b64.encode(binaryData);\n    }\n\n    /**\n     * Decodes a Base64 String into octets.\n     * <p>\n     * <b>Note:</b> this method seamlessly handles data encoded in URL-safe or normal mode.\n     * </p>\n     *\n     * @param base64String\n     *            String containing Base64 data\n     * @return Array containing decoded data.\n     * @since 1.4\n     */\n    public static byte[] decodeBase64(final String base64String) {\n        return new Base64().decode(base64String);\n    }\n\n    /**\n     * Decodes Base64 data into octets.\n     * <p>\n     * <b>Note:</b> this method seamlessly handles data encoded in URL-safe or normal mode.\n     * </p>\n     *\n     * @param base64Data\n     *            Byte array containing Base64 data\n     * @return Array containing decoded data.\n     */\n    public static byte[] decodeBase64(final byte[] base64Data) {\n        return new Base64().decode(base64Data);\n    }\n\n    // Implementation of the Encoder Interface\n\n    // Implementation of integer encoding used for crypto\n    /**\n     * Decodes a byte64-encoded integer according to crypto standards such as W3C's XML-Signature.\n     *\n     * @param pArray\n     *            a byte array containing base64 character data\n     * @return A BigInteger\n     * @since 1.4\n     */\n    public static BigInteger decodeInteger(final byte[] pArray) {\n        return new BigInteger(1, decodeBase64(pArray));\n    }\n\n    /**\n     * Encodes to a byte64-encoded integer according to crypto standards such as W3C's XML-Signature.\n     *\n     * @param bigInt\n     *            a BigInteger\n     * @return A byte array containing base64 character data\n     * @throws NullPointerException\n     *             if null is passed in\n     * @since 1.4\n     */\n    public static byte[] encodeInteger(final BigInteger bigInt) {\n        if (bigInt == null) {\n            throw new NullPointerException(\"encodeInteger called with null parameter\");\n        }\n        return encodeBase64(toIntegerBytes(bigInt), false);\n    }\n\n    /**\n     * Returns a byte-array representation of a <code>BigInteger</code> without sign bit.\n     *\n     * @param bigInt\n     *            <code>BigInteger</code> to be converted\n     * @return a byte array representation of the BigInteger parameter\n     */\n    static byte[] toIntegerBytes(final BigInteger bigInt) {\n        int bitlen = bigInt.bitLength();\n        // round bitlen\n        bitlen = ((bitlen + 7) >> 3) << 3;\n        final byte[] bigBytes = bigInt.toByteArray();\n\n        if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {\n            return bigBytes;\n        }\n        // set up params for copying everything but sign bit\n        int startSrc = 0;\n        int len = bigBytes.length;\n\n        // if bigInt is exactly byte-aligned, just skip signbit in copy\n        if ((bigInt.bitLength() % 8) == 0) {\n            startSrc = 1;\n            len--;\n        }\n        final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec\n        final byte[] resizedBytes = new byte[bitlen / 8];\n        System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);\n        return resizedBytes;\n    }\n\n    /**\n     * Returns whether or not the <code>octet</code> is in the Base64 alphabet.\n     *\n     * @param octet\n     *            The value to test\n     * @return <code>true</code> if the value is defined in the Base64 alphabet <code>false</code> otherwise.\n     */\n    @Override\n    protected boolean isInAlphabet(final byte octet) {\n        return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/BaseNCodec.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Abstract superclass for Base-N encoders and decoders.\n *\n * From https://commons.apache.org/proper/commons-codec/\n *\n * <p>\n * This class is thread-safe.\n * </p>\n *\n * @version $Id$\n */\npublic abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder {\n\n    /**\n     * Holds thread context so classes can be thread-safe.\n     *\n     * This class is not itself thread-safe; each thread must allocate its own copy.\n     *\n     * @since 1.7\n     */\n    static class Context {\n\n        /**\n         * Place holder for the bytes we're dealing with for our based logic.\n         * Bitwise operations store and extract the encoding or decoding from this variable.\n         */\n        int ibitWorkArea;\n\n        /**\n         * Place holder for the bytes we're dealing with for our based logic.\n         * Bitwise operations store and extract the encoding or decoding from this variable.\n         */\n        long lbitWorkArea;\n\n        /**\n         * Buffer for streaming.\n         */\n        byte[] buffer;\n\n        /**\n         * Position where next character should be written in the buffer.\n         */\n        int pos;\n\n        /**\n         * Position where next character should be read from the buffer.\n         */\n        int readPos;\n\n        /**\n         * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless,\n         * and must be thrown away.\n         */\n        boolean eof;\n\n        /**\n         * Variable tracks how many characters have been written to the current line. Only used when encoding. We use\n         * it to make sure each encoded line never goes beyond lineLength (if lineLength &gt; 0).\n         */\n        int currentLinePos;\n\n        /**\n         * Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding. This\n         * variable helps track that.\n         */\n        int modulus;\n\n        Context() {}\n    }\n\n    /**\n     * EOF\n     *\n     * @since 1.7\n     */\n    static final int EOF = -1;\n\n    /**\n     *  MIME chunk size per RFC 2045 section 6.8.\n     *\n     * <p>\n     * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any\n     * equal signs.\n     * </p>\n     *\n     * @see <a href=\"http://www.ietf.org/rfc/rfc2045.txt\">RFC 2045 section 6.8</a>\n     */\n    public static final int MIME_CHUNK_SIZE = 76;\n\n    /**\n     * PEM chunk size per RFC 1421 section 4.3.2.4.\n     *\n     * <p>\n     * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any\n     * equal signs.\n     * </p>\n     *\n     * @see <a href=\"http://tools.ietf.org/html/rfc1421\">RFC 1421 section 4.3.2.4</a>\n     */\n    public static final int PEM_CHUNK_SIZE = 64;\n\n    private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;\n\n    /**\n     * Defines the default buffer size - currently {@value}\n     * - must be large enough for at least one encoded block+separator\n     */\n    private static final int DEFAULT_BUFFER_SIZE = 8192;\n\n    /** Mask used to extract 8 bits, used in decoding bytes */\n    protected static final int MASK_8BITS = 0xff;\n\n    /**\n     * Byte used to pad output.\n     */\n    protected static final byte PAD_DEFAULT = '='; // Allow static access to default\n\n    /**\n     * @deprecated Use {@link #pad}. Will be removed in 2.0.\n     */\n    @Deprecated\n    protected final byte PAD = PAD_DEFAULT; // instance variable just in case it needs to vary later\n\n    protected final byte pad; // instance variable just in case it needs to vary later\n\n    /** Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for Base32 */\n    private final int unencodedBlockSize;\n\n    /** Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for Base32 */\n    private final int encodedBlockSize;\n\n    /**\n     * Chunksize for encoding. Not used when decoding.\n     * A value of zero or less implies no chunking of the encoded data.\n     * Rounded down to nearest multiple of encodedBlockSize.\n     */\n    protected final int lineLength;\n\n    /**\n     * Size of chunk separator. Not used unless {@link #lineLength} &gt; 0.\n     */\n    private final int chunkSeparatorLength;\n\n    /**\n     * Note <code>lineLength</code> is rounded down to the nearest multiple of {@link #encodedBlockSize}\n     * If <code>chunkSeparatorLength</code> is zero, then chunking is disabled.\n     * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)\n     * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)\n     * @param lineLength if &gt; 0, use chunking with a length <code>lineLength</code>\n     * @param chunkSeparatorLength the chunk separator length, if relevant\n     */\n    protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize,\n                         final int lineLength, final int chunkSeparatorLength) {\n        this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, PAD_DEFAULT);\n    }\n\n    /**\n     * Note <code>lineLength</code> is rounded down to the nearest multiple of {@link #encodedBlockSize}\n     * If <code>chunkSeparatorLength</code> is zero, then chunking is disabled.\n     * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)\n     * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)\n     * @param lineLength if &gt; 0, use chunking with a length <code>lineLength</code>\n     * @param chunkSeparatorLength the chunk separator length, if relevant\n     * @param pad byte used as padding byte.\n     */\n    protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize,\n                         final int lineLength, final int chunkSeparatorLength, final byte pad) {\n        this.unencodedBlockSize = unencodedBlockSize;\n        this.encodedBlockSize = encodedBlockSize;\n        final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0;\n        this.lineLength = useChunking ? (lineLength / encodedBlockSize) * encodedBlockSize : 0;\n        this.chunkSeparatorLength = chunkSeparatorLength;\n\n        this.pad = pad;\n    }\n\n    /**\n     * Returns true if this object has buffered data for reading.\n     *\n     * @param context the context to be used\n     * @return true if there is data still available for reading.\n     */\n    boolean hasData(final Context context) {  // package protected for access from I/O streams\n        return context.buffer != null;\n    }\n\n    /**\n     * Returns the amount of buffered data available for reading.\n     *\n     * @param context the context to be used\n     * @return The amount of buffered data available for reading.\n     */\n    int available(final Context context) {  // package protected for access from I/O streams\n        return context.buffer != null ? context.pos - context.readPos : 0;\n    }\n\n    /**\n     * Get the default buffer size. Can be overridden.\n     *\n     * @return {@link #DEFAULT_BUFFER_SIZE}\n     */\n    protected int getDefaultBufferSize() {\n        return DEFAULT_BUFFER_SIZE;\n    }\n\n    /**\n     * Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}.\n     * @param context the context to be used\n     */\n    private byte[] resizeBuffer(final Context context) {\n        if (context.buffer == null) {\n            context.buffer = new byte[getDefaultBufferSize()];\n            context.pos = 0;\n            context.readPos = 0;\n        } else {\n            final byte[] b = new byte[context.buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR];\n            System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);\n            context.buffer = b;\n        }\n        return context.buffer;\n    }\n\n    /**\n     * Ensure that the buffer has room for <code>size</code> bytes\n     *\n     * @param size minimum spare space required\n     * @param context the context to be used\n     * @return the buffer\n     */\n    protected byte[] ensureBufferSize(final int size, final Context context){\n        if ((context.buffer == null) || (context.buffer.length < context.pos + size)){\n            return resizeBuffer(context);\n        }\n        return context.buffer;\n    }\n\n    /**\n     * Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail\n     * bytes. Returns how many bytes were actually extracted.\n     * <p>\n     * Package protected for access from I/O streams.\n     *\n     * @param b\n     *            byte[] array to extract the buffered data into.\n     * @param bPos\n     *            position in byte[] array to start extraction at.\n     * @param bAvail\n     *            amount of bytes we're allowed to extract. We may extract fewer (if fewer are available).\n     * @param context\n     *            the context to be used\n     * @return The number of bytes successfully extracted into the provided byte[] array.\n     */\n    int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) {\n        if (context.buffer != null) {\n            final int len = Math.min(available(context), bAvail);\n            System.arraycopy(context.buffer, context.readPos, b, bPos, len);\n            context.readPos += len;\n            if (context.readPos >= context.pos) {\n                context.buffer = null; // so hasData() will return false, and this method can return -1\n            }\n            return len;\n        }\n        return context.eof ? EOF : 0;\n    }\n\n    /**\n     * Checks if a byte value is whitespace or not.\n     * Whitespace is taken to mean: space, tab, CR, LF\n     * @param byteToCheck\n     *            the byte to check\n     * @return true if byte is whitespace, false otherwise\n     */\n    protected static boolean isWhiteSpace(final byte byteToCheck) {\n        switch (byteToCheck) {\n            case ' ' :\n            case '\\n' :\n            case '\\r' :\n            case '\\t' :\n                return true;\n            default :\n                return false;\n        }\n    }\n\n    /**\n     * Encodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of\n     * the Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[].\n     *\n     * @param obj\n     *            Object to encode\n     * @return An object (of type byte[]) containing the Base-N encoded data which corresponds to the byte[] supplied.\n     * @throws EncoderException\n     *             if the parameter supplied is not of type byte[]\n     */\n    @Override\n    public Object encode(final Object obj) throws EncoderException {\n        if (!(obj instanceof byte[])) {\n            throw new EncoderException(\"Parameter supplied to Base-N encode is not a byte[]\");\n        }\n        return encode((byte[]) obj);\n    }\n\n    /**\n     * Encodes a byte[] containing binary data, into a String containing characters in the Base-N alphabet.\n     * Uses UTF8 encoding.\n     *\n     * @param pArray\n     *            a byte array containing binary data\n     * @return A String containing only Base-N character data\n     */\n    public String encodeToString(final byte[] pArray) {\n        return StringUtils.newStringUtf8(encode(pArray));\n    }\n\n    /**\n     * Encodes a byte[] containing binary data, into a String containing characters in the appropriate alphabet.\n     * Uses UTF8 encoding.\n     *\n     * @param pArray a byte array containing binary data\n     * @return String containing only character data in the appropriate alphabet.\n     * @since 1.5\n     * This is a duplicate of {@link #encodeToString(byte[])}; it was merged during refactoring.\n    */\n    public String encodeAsString(final byte[] pArray){\n        return StringUtils.newStringUtf8(encode(pArray));\n    }\n\n    /**\n     * Decodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of\n     * the Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[] or String.\n     *\n     * @param obj\n     *            Object to decode\n     * @return An object (of type byte[]) containing the binary data which corresponds to the byte[] or String\n     *         supplied.\n     * @throws DecoderException\n     *             if the parameter supplied is not of type byte[]\n     */\n    @Override\n    public Object decode(final Object obj) throws DecoderException {\n        if (obj instanceof byte[]) {\n            return decode((byte[]) obj);\n        } else if (obj instanceof String) {\n            return decode((String) obj);\n        } else {\n            throw new DecoderException(\"Parameter supplied to Base-N decode is not a byte[] or a String\");\n        }\n    }\n\n    /**\n     * Decodes a String containing characters in the Base-N alphabet.\n     *\n     * @param pArray\n     *            A String containing Base-N character data\n     * @return a byte array containing binary data\n     */\n    public byte[] decode(final String pArray) {\n        return decode(StringUtils.getBytesUtf8(pArray));\n    }\n\n    /**\n     * Decodes a byte[] containing characters in the Base-N alphabet.\n     *\n     * @param pArray\n     *            A byte array containing Base-N character data\n     * @return a byte array containing binary data\n     */\n    @Override\n    public byte[] decode(final byte[] pArray) {\n        if (pArray == null || pArray.length == 0) {\n            return pArray;\n        }\n        final Context context = new Context();\n        decode(pArray, 0, pArray.length, context);\n        decode(pArray, 0, EOF, context); // Notify decoder of EOF.\n        final byte[] result = new byte[context.pos];\n        readResults(result, 0, result.length, context);\n        return result;\n    }\n\n    /**\n     * Encodes a byte[] containing binary data, into a byte[] containing characters in the alphabet.\n     *\n     * @param pArray\n     *            a byte array containing binary data\n     * @return A byte array containing only the base N alphabetic character data\n     */\n    @Override\n    public byte[] encode(final byte[] pArray) {\n        if (pArray == null || pArray.length == 0) {\n            return pArray;\n        }\n        return encode(pArray, 0, pArray.length);\n    }\n\n    /**\n     * Encodes a byte[] containing binary data, into a byte[] containing\n     * characters in the alphabet.\n     *\n     * @param pArray\n     *            a byte array containing binary data\n     * @param offset\n     *            initial offset of the subarray.\n     * @param length\n     *            length of the subarray.\n     * @return A byte array containing only the base N alphabetic character data\n     * @since 1.11\n     */\n    public byte[] encode(final byte[] pArray, final int offset, final int length) {\n        if (pArray == null || pArray.length == 0) {\n            return pArray;\n        }\n        final Context context = new Context();\n        encode(pArray, offset, length, context);\n        encode(pArray, offset, EOF, context); // Notify encoder of EOF.\n        final byte[] buf = new byte[context.pos - context.readPos];\n        readResults(buf, 0, buf.length, context);\n        return buf;\n    }\n\n    // package protected for access from I/O streams\n    abstract void encode(byte[] pArray, int i, int length, Context context);\n\n    // package protected for access from I/O streams\n    abstract void decode(byte[] pArray, int i, int length, Context context);\n\n    /**\n     * Returns whether or not the <code>octet</code> is in the current alphabet.\n     * Does not allow whitespace or pad.\n     *\n     * @param value The value to test\n     *\n     * @return <code>true</code> if the value is defined in the current alphabet, <code>false</code> otherwise.\n     */\n    protected abstract boolean isInAlphabet(byte value);\n\n    /**\n     * Tests a given byte array to see if it contains only valid characters within the alphabet.\n     * The method optionally treats whitespace and pad as valid.\n     *\n     * @param arrayOctet byte array to test\n     * @param allowWSPad if <code>true</code>, then whitespace and PAD are also allowed\n     *\n     * @return <code>true</code> if all bytes are valid characters in the alphabet or if the byte array is empty;\n     *         <code>false</code>, otherwise\n     */\n    public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) {\n        for (final byte octet : arrayOctet) {\n            if (!isInAlphabet(octet) &&\n                    (!allowWSPad || (octet != pad) && !isWhiteSpace(octet))) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Tests a given String to see if it contains only valid characters within the alphabet.\n     * The method treats whitespace and PAD as valid.\n     *\n     * @param basen String to test\n     * @return <code>true</code> if all characters in the String are valid characters in the alphabet or if\n     *         the String is empty; <code>false</code>, otherwise\n     * @see #isInAlphabet(byte[], boolean)\n     */\n    public boolean isInAlphabet(final String basen) {\n        return isInAlphabet(StringUtils.getBytesUtf8(basen), true);\n    }\n\n    /**\n     * Tests a given byte array to see if it contains any characters within the alphabet or PAD.\n     *\n     * Intended for use in checking line-ending arrays\n     *\n     * @param arrayOctet\n     *            byte array to test\n     * @return <code>true</code> if any byte is a valid character in the alphabet or PAD; <code>false</code> otherwise\n     */\n    protected boolean containsAlphabetOrPad(final byte[] arrayOctet) {\n        if (arrayOctet == null) {\n            return false;\n        }\n        for (final byte element : arrayOctet) {\n            if (pad == element || isInAlphabet(element)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Calculates the amount of space needed to encode the supplied array.\n     *\n     * @param pArray byte[] array which will later be encoded\n     *\n     * @return amount of space needed to encoded the supplied array.\n     * Returns a long since a max-len array will require &gt; Integer.MAX_VALUE\n     */\n    public long getEncodedLength(final byte[] pArray) {\n        // Calculate non-chunked size - rounded up to allow for padding\n        // cast to long is needed to avoid possibility of overflow\n        long len = ((pArray.length + unencodedBlockSize-1)  / unencodedBlockSize) * (long) encodedBlockSize;\n        if (lineLength > 0) { // We're using chunking\n            // Round up to nearest multiple\n            len += ((len + lineLength-1) / lineLength) * chunkSeparatorLength;\n        }\n        return len;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/BinaryDecoder.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Defines common decoding methods for byte array decoders.\n *\n * @version $Id$\n */\npublic interface BinaryDecoder extends Decoder {\n\n    /**\n     * Decodes a byte array and returns the results as a byte array.\n     *\n     * @param source\n     *            A byte array which has been encoded with the appropriate encoder\n     * @return a byte array that contains decoded content\n     * @throws DecoderException\n     *             A decoder exception is thrown if a Decoder encounters a failure condition during the decode process.\n     */\n    byte[] decode(byte[] source) throws DecoderException;\n}\n\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/BinaryEncoder.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Defines common encoding methods for byte array encoders.\n *\n * @version $Id$\n */\npublic interface BinaryEncoder extends Encoder {\n\n    /**\n     * Encodes a byte array and return the encoded data as a byte array.\n     *\n     * @param source\n     *            Data to be encoded\n     * @return A byte array containing the encoded data\n     * @throws EncoderException\n     *             thrown if the Encoder encounters a failure condition during the encoding process.\n     */\n    byte[] encode(byte[] source) throws EncoderException;\n}\n\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/CharEncoding.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Character encoding names required of every implementation of the Java platform.\n *\n * From the Java documentation <a\n * href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>:\n * <p>\n * <cite>Every implementation of the Java platform is required to support the following character encodings. Consult the\n * release documentation for your implementation to see if any other encodings are supported. Consult the release\n * documentation for your implementation to see if any other encodings are supported.</cite>\n * </p>\n *\n * <ul>\n * <li><code>US-ASCII</code><br>\n * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.</li>\n * <li><code>ISO-8859-1</code><br>\n * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.</li>\n * <li><code>UTF-8</code><br>\n * Eight-bit Unicode Transformation Format.</li>\n * <li><code>UTF-16BE</code><br>\n * Sixteen-bit Unicode Transformation Format, big-endian byte order.</li>\n * <li><code>UTF-16LE</code><br>\n * Sixteen-bit Unicode Transformation Format, little-endian byte order.</li>\n * <li><code>UTF-16</code><br>\n * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order\n * accepted on input, big-endian used on output.)</li>\n * </ul>\n *\n * This perhaps would best belong in the [lang] project. Even if a similar interface is defined in [lang], it is not\n * foreseen that [codec] would be made to depend on [lang].\n *\n * <p>\n * This class is immutable and thread-safe.\n * </p>\n *\n * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n * @since 1.4\n * @version $Id$\n */\npublic class CharEncoding {\n    /**\n     * CharEncodingISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     *\n     * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final String ISO_8859_1 = \"ISO-8859-1\";\n\n    /**\n     * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set.\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     *\n     * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final String US_ASCII = \"US-ASCII\";\n\n    /**\n     * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark\n     * (either order accepted on input, big-endian used on output)\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     *\n     * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final String UTF_16 = \"UTF-16\";\n\n    /**\n     * Sixteen-bit Unicode Transformation Format, big-endian byte order.\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     *\n     * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final String UTF_16BE = \"UTF-16BE\";\n\n    /**\n     * Sixteen-bit Unicode Transformation Format, little-endian byte order.\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     *\n     * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final String UTF_16LE = \"UTF-16LE\";\n\n    /**\n     * Eight-bit Unicode Transformation Format.\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     *\n     * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final String UTF_8 = \"UTF-8\";\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Charsets.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage peergos.shared.io.ipfs.bases;\n\nimport java.nio.charset.Charset;\n\n/**\n * Charsets required of every implementation of the Java platform.\n *\n * From the Java documentation <a href=\"http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html\">Standard\n * charsets</a>:\n * <p>\n * <cite>Every implementation of the Java platform is required to support the following character encodings. Consult the\n * release documentation for your implementation to see if any other encodings are supported. Consult the release\n * documentation for your implementation to see if any other encodings are supported. </cite>\n * </p>\n *\n * <ul>\n * <li><code>US-ASCII</code><br>\n * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.</li>\n * <li><code>ISO-8859-1</code><br>\n * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.</li>\n * <li><code>UTF-8</code><br>\n * Eight-bit Unicode Transformation Format.</li>\n * <li><code>UTF-16BE</code><br>\n * Sixteen-bit Unicode Transformation Format, big-endian byte order.</li>\n * <li><code>UTF-16LE</code><br>\n * Sixteen-bit Unicode Transformation Format, little-endian byte order.</li>\n * <li><code>UTF-16</code><br>\n * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order\n * accepted on input, big-endian used on output.)</li>\n * </ul>\n *\n * This perhaps would best belong in the Commons Lang project. Even if a similar class is defined in Commons Lang, it is\n * not foreseen that Commons Codec would be made to depend on Commons Lang.\n *\n * <p>\n * This class is immutable and thread-safe.\n * </p>\n *\n * @see <a href=\"http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n * @since 1.7\n * @version $Id: CharEncoding.java 1173287 2011-09-20 18:16:19Z ggregory $\n */\npublic class Charsets {\n\n    //\n    // This class should only contain Charset instances for required encodings. This guarantees that it will load\n    // correctly and without delay on all Java platforms.\n    //\n\n    /**\n     * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set.\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     * </p>\n     * <p>\n     * On Java 7 or later, use {@link java.nio.charset.StandardCharsets#ISO_8859_1} instead.\n     * </p>\n     *\n     * @see <a href=\"http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final Charset US_ASCII = Charset.forName(CharEncoding.US_ASCII);\n\n    /**\n     * Eight-bit Unicode Transformation Format.\n     * <p>\n     * Every implementation of the Java platform is required to support this character encoding.\n     * </p>\n     * <p>\n     * On Java 7 or later, use {@link java.nio.charset.StandardCharsets#ISO_8859_1} instead.\n     * </p>\n     *\n     * @see <a href=\"http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static final Charset UTF_8 = Charset.forName(CharEncoding.UTF_8);\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Decoder.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Provides the highest level of abstraction for Decoders.\n * <p>\n * This is the sister interface of {@link Encoder}. All Decoders implement this common generic interface.\n * Allows a user to pass a generic Object to any Decoder implementation in the codec package.\n * <p>\n * One of the two interfaces at the center of the codec package.\n *\n * @version $Id$\n */\npublic interface Decoder {\n\n    /**\n     * Decodes an \"encoded\" Object and returns a \"decoded\" Object. Note that the implementation of this interface will\n     * try to cast the Object parameter to the specific type expected by a particular Decoder implementation. If a\n     * {@link ClassCastException} occurs this decode method will throw a DecoderException.\n     *\n     * @param source\n     *            the object to decode\n     * @return a 'decoded\" object\n     * @throws DecoderException\n     *             a decoder exception can be thrown for any number of reasons. Some good candidates are that the\n     *             parameter passed to this method is null, a param cannot be cast to the appropriate type for a\n     *             specific encoder.\n     */\n    Object decode(Object source) throws DecoderException;\n}\n\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/DecoderException.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Thrown when there is a failure condition during the decoding process. This exception is thrown when a {@link Decoder}\n * encounters a decoding specific exception such as invalid data, or characters outside of the expected range.\n *\n * @version $Id$\n */\npublic class DecoderException extends Exception {\n\n    /**\n     * Declares the Serial Version Uid.\n     *\n     * @see <a href=\"http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid\">Always Declare Serial Version Uid</a>\n     */\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * Constructs a new exception with <code>null</code> as its detail message. The cause is not initialized, and may\n     * subsequently be initialized by a call to {@link #initCause}.\n     *\n     * @since 1.4\n     */\n    public DecoderException() {\n        super();\n    }\n\n    /**\n     * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently\n     * be initialized by a call to {@link #initCause}.\n     *\n     * @param message\n     *            The detail message which is saved for later retrieval by the {@link #getMessage()} method.\n     */\n    public DecoderException(final String message) {\n        super(message);\n    }\n\n    /**\n     * Constructs a new exception with the specified detail message and cause.\n     * <p>\n     * Note that the detail message associated with <code>cause</code> is not automatically incorporated into this\n     * exception's detail message.\n     *\n     * @param message\n     *            The detail message which is saved for later retrieval by the {@link #getMessage()} method.\n     * @param cause\n     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>\n     *            value is permitted, and indicates that the cause is nonexistent or unknown.\n     * @since 1.4\n     */\n    public DecoderException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n\n    /**\n     * Constructs a new exception with the specified cause and a detail message of <code>(cause==null ?\n     * null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>).\n     * This constructor is useful for exceptions that are little more than wrappers for other throwables.\n     *\n     * @param cause\n     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>\n     *            value is permitted, and indicates that the cause is nonexistent or unknown.\n     * @since 1.4\n     */\n    public DecoderException(final Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Encoder.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Provides the highest level of abstraction for Encoders.\n * <p>\n * This is the sister interface of {@link Decoder}.  Every implementation of Encoder provides this\n * common generic interface which allows a user to pass a generic Object to any Encoder implementation\n * in the codec package.\n *\n * @version $Id$\n */\npublic interface Encoder {\n\n    /**\n     * Encodes an \"Object\" and returns the encoded content as an Object. The Objects here may just be\n     * <code>byte[]</code> or <code>String</code>s depending on the implementation used.\n     *\n     * @param source\n     *            An object to encode\n     * @return An \"encoded\" Object\n     * @throws EncoderException\n     *             An encoder exception is thrown if the encoder experiences a failure condition during the encoding\n     *             process.\n     */\n    Object encode(Object source) throws EncoderException;\n}\n\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/EncoderException.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\n/**\n * Thrown when there is a failure condition during the encoding process. This exception is thrown when an\n * {@link Encoder} encounters a encoding specific exception such as invalid data, inability to calculate a checksum,\n * characters outside of the expected range.\n *\n * @version $Id$\n */\npublic class EncoderException extends Exception {\n\n    /**\n     * Declares the Serial Version Uid.\n     *\n     * @see <a href=\"http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid\">Always Declare Serial Version Uid</a>\n     */\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * Constructs a new exception with <code>null</code> as its detail message. The cause is not initialized, and may\n     * subsequently be initialized by a call to {@link #initCause}.\n     *\n     * @since 1.4\n     */\n    public EncoderException() {\n        super();\n    }\n\n    /**\n     * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently\n     * be initialized by a call to {@link #initCause}.\n     *\n     * @param message\n     *            a useful message relating to the encoder specific error.\n     */\n    public EncoderException(final String message) {\n        super(message);\n    }\n\n    /**\n     * Constructs a new exception with the specified detail message and cause.\n     *\n     * <p>\n     * Note that the detail message associated with <code>cause</code> is not automatically incorporated into this\n     * exception's detail message.\n     * </p>\n     *\n     * @param message\n     *            The detail message which is saved for later retrieval by the {@link #getMessage()} method.\n     * @param cause\n     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>\n     *            value is permitted, and indicates that the cause is nonexistent or unknown.\n     * @since 1.4\n     */\n    public EncoderException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n\n    /**\n     * Constructs a new exception with the specified cause and a detail message of <code>(cause==null ?\n     * null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>).\n     * This constructor is useful for exceptions that are little more than wrappers for other throwables.\n     *\n     * @param cause\n     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>\n     *            value is permitted, and indicates that the cause is nonexistent or unknown.\n     * @since 1.4\n     */\n    public EncoderException(final Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/Multibase.java",
    "content": "package peergos.shared.io.ipfs.bases;\n\nimport java.util.*;\n\npublic class Multibase {\n\n    public enum Base {\n        Base1('1'),\n        Base2('0'),\n        Base8('7'),\n        Base10('9'),\n        Base16('f'),\n        Base32('b'),\n        Base32Upper('B'),\n        Base32Hex('v'),\n        Base32HexUpper('V'),\n        Base36('k'),\n        Base58Flickr('Z'),\n        Base58BTC('z'),\n        Base64('m'),\n        Base64Pad('M');\n\n        public char prefix;\n\n        Base(char prefix) {\n            this.prefix = prefix;\n        }\n\n        private static Map<Character, Base> lookup = new TreeMap<>();\n        static {\n            for (Base b: Base.values())\n                lookup.put(b.prefix, b);\n        }\n\n        public static Base lookup(char p) {\n            if (!lookup.containsKey(p))\n                throw new IllegalStateException(\"Unknown Multibase type: \" + p);\n            return lookup.get(p);\n        }\n    }\n\n    public static String encode(Base b, byte[] data) {\n        switch (b) {\n            case Base58BTC:\n                return b.prefix + Base58.encode(data);\n            case Base16:\n                return b.prefix + Base16.encode(data);\n            case Base32:\n                return b.prefix + new String(new Base32().encode(data)).toLowerCase().replaceAll(\"=\", \"\");\n            case Base32Upper:\n                return b.prefix + new String(new Base32().encode(data)).replaceAll(\"=\", \"\");\n            case Base32Hex:\n                return b.prefix + new String(new Base32(true).encode(data)).toLowerCase().replaceAll(\"=\", \"\");\n            case Base32HexUpper:\n                return b.prefix + new String(new Base32(true).encode(data)).replaceAll(\"=\", \"\");\n            case Base36:\n                return b.prefix + Base36.encode(data);\n            case Base64:\n                return b.prefix + Base64.encodeBase64String(data).replaceAll(\"=\", \"\");\n            case Base64Pad:\n                return b.prefix + Base64.encodeBase64String(data);\n            default:\n                throw new IllegalStateException(\"Unsupported base encoding: \" + b.name());\n        }\n    }\n\n    public static Base encoding(String data) {\n        return Base.lookup(data.charAt(0));\n    }\n\n    public static byte[] decode(String data) {\n        Base b = encoding(data);\n        String rest = data.substring(1);\n        switch (b) {\n            case Base58BTC:\n                return Base58.decode(rest);\n            case Base16:\n                return Base16.decode(rest);\n            case Base32:\n                return new Base32().decode(rest);\n            case Base32Upper:\n                return new Base32().decode(rest.toLowerCase());\n            case Base32Hex:\n                return new Base32(true).decode(rest);\n            case Base32HexUpper:\n                return new Base32(true).decode(rest.toLowerCase());\n            case Base36:\n                return Base36.decode(rest);\n            case Base64Pad:\n            case Base64:\n                return Base64.decodeBase64(rest);\n            default:\n                throw new IllegalStateException(\"Unsupported base encoding: \" + b.name());\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/io/ipfs/bases/StringUtils.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.io.ipfs.bases;\n\nimport java.nio.charset.Charset;\n\n/**\n * Converts String to and from bytes using the encodings required by the Java specification. These encodings are\n * specified in <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">\n * Standard charsets</a>.\n *\n * <p>This class is immutable and thread-safe.</p>\n *\n * @see CharEncoding\n * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n * @version $Id$\n * @since 1.4\n */\npublic class StringUtils {\n\n    /**\n     * Calls {@link String#getBytes(Charset)}\n     *\n     * @param string\n     *            The string to encode (if null, return null).\n     * @param charset\n     *            The {@link Charset} to encode the <code>String</code>\n     * @return the encoded bytes\n     */\n    private static byte[] getBytes(final String string, final Charset charset) {\n        if (string == null) {\n            return null;\n        }\n        return string.getBytes(charset);\n    }\n\n    /**\n     * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte\n     * array.\n     *\n     * @param string\n     *            the String to encode, may be <code>null</code>\n     * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>\n     * @throws NullPointerException\n     *             Thrown if {@link Charsets#UTF_8} is not initialized, which should never happen since it is\n     *             required by the Java platform specification.\n     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException\n     * @see <a href=\"http://download.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html\">Standard charsets</a>\n     */\n    public static byte[] getBytesUtf8(final String string) {\n        return getBytes(string, Charset.forName(\"UTF-8\"));\n    }\n\n    /**\n     * Constructs a new <code>String</code> by decoding the specified array of bytes using the given charset.\n     *\n     * @param bytes\n     *            The bytes to be decoded into characters\n     * @param charset\n     *            The {@link Charset} to encode the <code>String</code>; not {@code null}\n     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset,\n     *         or <code>null</code> if the input byte array was <code>null</code>.\n     * @throws NullPointerException\n     *             Thrown if charset is {@code null}\n     */\n    private static String newString(final byte[] bytes, final Charset charset) {\n        return bytes == null ? null : new String(bytes, charset);\n    }\n\n    /**\n     * Constructs a new <code>String</code> by decoding the specified array of bytes using the US-ASCII charset.\n     *\n     * @param bytes\n     *            The bytes to be decoded into characters\n     * @return A new <code>String</code> decoded from the specified array of bytes using the US-ASCII charset,\n     *         or <code>null</code> if the input byte array was <code>null</code>.\n     * @throws NullPointerException\n     *             Thrown if {@link Charsets#US_ASCII} is not initialized, which should never happen since it is\n     *             required by the Java platform specification.\n     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException\n     */\n    public static String newStringUsAscii(final byte[] bytes) {\n        return newString(bytes, Charset.forName(\"US-ASCII\"));\n    }\n\n    /**\n     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-8 charset.\n     *\n     * @param bytes\n     *            The bytes to be decoded into characters\n     * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-8 charset,\n     *         or <code>null</code> if the input byte array was <code>null</code>.\n     * @throws NullPointerException\n     *             Thrown if {@link Charsets#UTF_8} is not initialized, which should never happen since it is\n     *             required by the Java platform specification.\n     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException\n     */\n    public static String newStringUtf8(final byte[] bytes) {\n        return newString(bytes, Charset.forName(\"UTF-8\"));\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/login/LoginCache.java",
    "content": "package peergos.shared.login;\n\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.user.*;\n\nimport java.util.concurrent.*;\n\npublic interface LoginCache {\n\n    CompletableFuture<Boolean> setLoginData(LoginData login);\n\n    CompletableFuture<Boolean> removeLoginData(String username);\n\n    CompletableFuture<UserStaticData> getEntryData(String username, PublicSigningKey authorisedReader);\n}\n"
  },
  {
    "path": "src/peergos/shared/login/LoginResponse.java",
    "content": "package peergos.shared.login;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class LoginResponse implements Cborable {\n\n    public final Either<UserStaticData, MultiFactorAuthRequest> resp;\n\n    public LoginResponse(Either<UserStaticData, MultiFactorAuthRequest> resp) {\n        this.resp = resp;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"a\", new CborObject.CborBoolean(resp.isA()));\n        state.put(\"r\", resp.map(Cborable::toCbor, MultiFactorAuthRequest::toCbor));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static LoginResponse fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for LoginResponse! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        boolean isA = m.getBoolean(\"a\");\n        if (isA)\n            return new LoginResponse(Either.a(m.get(\"r\", UserStaticData::fromCbor)));\n        return new LoginResponse(Either.b(m.get(\"r\", MultiFactorAuthRequest::fromCbor)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/login/OfflineAccountStore.java",
    "content": "package peergos.shared.login;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class OfflineAccountStore implements Account {\n\n    private final Account target;\n    private final LoginCache local;\n    private final OnlineState online;\n\n    public OfflineAccountStore(Account target, LoginCache local, OnlineState online) {\n        this.target = target;\n        this.local = local;\n        this.online = online;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        return target.setLoginData(login, auth, forceLocal).thenCompose(r -> {\n            if (r)\n                return local.setLoginData(login);\n            return Futures.of(r);\n        });\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse>  mfa,\n                                                                                          boolean cacheMfaLoginData,\n                                                                                          boolean forceProxy,\n                                                                                          boolean forceNoCache) {\n        return Futures.asyncExceptionally(() -> {\n                    if (online.isOnline()) {\n                        CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> res = new CompletableFuture<>();\n                        target.getLoginData(username, authorisedReader, auth, mfa, cacheMfaLoginData, forceProxy, forceNoCache)\n                                .thenAccept(login -> {\n                                    if (login.isA() && (mfa.isEmpty() || cacheMfaLoginData))\n                                        local.setLoginData(new LoginData(username, login.a(), authorisedReader, Optional.empty()));\n                                    else // disable offline login if MFA is enabled\n                                        local.removeLoginData(username);\n                                    res.complete(login);\n                                }).exceptionally(t -> {\n                                    if (! res.isDone())\n                                        res.completeExceptionally(t);\n                                    return null;\n                                });\n                        if (! forceNoCache)\n                            local.getEntryData(username, authorisedReader)\n                                    .thenApply(cached -> res.complete(Either.a(cached)));\n                        return res;\n                    }\n                    online.updateAsync();\n                    return local.getEntryData(username, authorisedReader).thenApply(Either::a);\n                },\n                t -> {\n                    if (t.getMessage().contains(\"Incorrect+password\"))\n                        return Futures.errored(new IllegalStateException(\"Incorrect password!\"));\n                    if (online.isOfflineException(t))\n                        return local.getEntryData(username, authorisedReader).thenApply(Either::a);\n                    return Futures.errored(t);\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        return target.getSecondAuthMethods(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        return target.enableTotpFactor(username, credentialId, code, auth);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        return target.registerSecurityKeyStart(username, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        return target.registerSecurityKeyComplete(username, keyName, resp, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        return target.deleteSecondFactor(username, credentialId, auth);\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        return target.addTotpFactor(username, auth);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/login/mfa/MultiFactorAuthMethod.java",
    "content": "package peergos.shared.login.mfa;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\n@JsType\npublic class MultiFactorAuthMethod implements Cborable {\n\n    private static Map<Integer, MultiFactorAuthMethod.Type> byValue = new HashMap<>();\n\n    @JsType\n    public enum Type {\n        TOTP(0x1, false),\n        WEBAUTHN(0x2, true);\n\n        public final int value;\n        public final boolean hasChallenge;\n\n        Type(int value, boolean hasChallengeValue) {\n            this.value = value;\n            this.hasChallenge = hasChallengeValue;\n            byValue.put(value, this);\n        }\n\n        public static Type byValue(int val) {\n            return byValue.get(val);\n        }\n    }\n\n    public final String name;\n    public final byte[] credentialId;\n    public final LocalDate created;\n    public final Type type;\n    public final boolean enabled;\n\n    public MultiFactorAuthMethod(String name, byte[] credentialId, LocalDate created, Type type, boolean enabled) {\n        if (name.length() > 32)\n            throw new IllegalStateException(\"Second factor names must be smaller than 33 characters\");\n        this.name = name;\n        this.credentialId = credentialId;\n        this.created = created;\n        this.type = type;\n        this.enabled = enabled;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"n\", new CborObject.CborString(name));\n        state.put(\"i\", new CborObject.CborByteArray(credentialId));\n        state.put(\"c\", new CborObject.CborLong(created.toEpochDay()));\n        state.put(\"t\", new CborObject.CborLong(type.value));\n        state.put(\"e\", new CborObject.CborBoolean(enabled));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static MultiFactorAuthMethod fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for MultiFactorAuthMethod! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new MultiFactorAuthMethod(m.getString(\"n\"),\n                m.getByteArray(\"i\"),\n                LocalDate.ofEpochDay(m.getLong(\"c\")),\n                Type.byValue((int)m.getLong(\"t\")),\n                m.getBoolean(\"e\"));\n    }\n\n    @Override\n    public String toString() {\n        return \"MultiFactorAuthMethod{\" +\n                \"uid='\" + ArrayOps.bytesToHex(credentialId) + '\\'' +\n                \", type=\" + type +\n                \", enabled=\" + enabled +\n                '}';\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/login/mfa/MultiFactorAuthRequest.java",
    "content": "package peergos.shared.login.mfa;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\n@JsType\npublic class MultiFactorAuthRequest implements Cborable {\n\n    public final List<MultiFactorAuthMethod> methods;\n    public final byte[] challenge;\n\n    public MultiFactorAuthRequest(List<MultiFactorAuthMethod> methods, byte[] challenge) {\n        this.methods = methods;\n        this.challenge = challenge;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"m\", new CborObject.CborList(methods));\n        state.put(\"c\", new CborObject.CborByteArray(challenge));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static MultiFactorAuthRequest fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for MultiFactorAuthRequest! \");\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new MultiFactorAuthRequest(m.getList(\"m\", MultiFactorAuthMethod::fromCbor), m.getByteArray(\"c\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/login/mfa/MultiFactorAuthResponse.java",
    "content": "package peergos.shared.login.mfa;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.util.Either;\n\nimport java.util.*;\n\n@JsType\npublic class MultiFactorAuthResponse implements Cborable {\n    public final byte[] credentialId;\n    public final Either<String, WebauthnResponse> response;\n\n    public MultiFactorAuthResponse(byte[] credentialId, Either<String, WebauthnResponse> response) {\n        this.credentialId = credentialId;\n        this.response = response;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"i\", new CborObject.CborByteArray(credentialId));\n        state.put(\"r\", response.map(code -> new CborObject.CborString(code), x -> x));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static MultiFactorAuthResponse fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for MultiFactorAuthResponse! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        Cborable response = m.get(\"r\");\n        return new MultiFactorAuthResponse(m.getByteArray(\"i\"),\n                response instanceof CborObject.CborString ?\n                        Either.a(((CborObject.CborString) response).value) :\n                        Either.b(WebauthnResponse.fromCbor(response)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/login/mfa/TotpKey.java",
    "content": "package peergos.shared.login.mfa;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.QRCodeEncoder;\nimport peergos.shared.io.ipfs.bases.Base32;\nimport peergos.shared.zxing.BarcodeFormat;\nimport peergos.shared.zxing.common.BitMatrix;\nimport peergos.shared.zxing.qrcode.QRCodeWriter;\n\nimport java.util.Base64;\n\n@JsType\npublic class TotpKey {\n    public static final String ALGORITHM = \"HmacSHA1\"; // Can't change this because google authenticator ignores the algorithm!!\n\n    public final byte[] credentialId, key;\n\n    public TotpKey(byte[] credentialId, byte[] key) {\n        this.credentialId = credentialId;\n        this.key = key;\n    }\n\n    public String encode() {\n        return base32(credentialId) + \":\" + base32(key);\n    }\n\n    private static String base32(byte[] in) {\n        return new Base32().encodeToString(in).replaceAll(\"=\",\"\");\n    }\n\n    public static TotpKey fromString(String encoded) {\n        int endIndex = encoded.indexOf(\":\");\n        String base32credid = encoded.substring(0, endIndex);\n        String base32key = encoded.substring(endIndex + 1, encoded.length());\n        return new TotpKey(new Base32().decode(base32credid), new Base32().decode(base32key));\n    }\n\n    public String getQRCode(String username) {\n        QRCodeWriter writer = new QRCodeWriter();\n        String issuer = \"peergos\";\n        String label = issuer + \":\" + username + \"@peergos\";\n        String originalText = \"otpauth://totp/\" + label + \"?secret=\" + new Base32().encodeToString(key).replaceAll(\"=\",\"\")\n                + \"&issuer=\" + issuer;\n        try {\n            BitMatrix result = writer.encode(originalText, BarcodeFormat.QR_CODE, 512, 512);\n            byte[] png = QRCodeEncoder.encodeToPng(0, result.getWidth(), result.getHeight(), result);\n            String base64Data = Base64.getEncoder().encodeToString(png);\n            return \"data:image/png;base64,\" + base64Data;\n        } catch(Exception e) {\n            e.printStackTrace();\n            return \"\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/login/mfa/WebauthnResponse.java",
    "content": "package peergos.shared.login.mfa;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\n@JsType\npublic class WebauthnResponse implements Cborable {\n    public final byte[] authenticatorData, clientDataJson, signature;\n\n    public WebauthnResponse(byte[] authenticatorData, byte[] clientDataJson, byte[] signature) {\n        this.authenticatorData = authenticatorData;\n        this.clientDataJson = clientDataJson;\n        this.signature = signature;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"a\", new CborObject.CborByteArray(authenticatorData));\n        state.put(\"c\", new CborObject.CborByteArray(clientDataJson));\n        state.put(\"s\", new CborObject.CborByteArray(signature));\n\n        return CborObject.CborMap.build(state);\n    }\n\n    public static WebauthnResponse fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for WebauthnResponse! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new WebauthnResponse(m.getByteArray(\"a\"), m.getByteArray(\"c\"), m.getByteArray(\"s\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/Chat.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.display.*;\nimport peergos.shared.messaging.messages.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class Chat implements Cborable {\n\n    public final String chatUid;\n    public final Id host;\n    public final TreeClock current;\n    public final Map<Id, Member> members;\n    public final Map<String, GroupProperty> groupState;\n    private final List<MessageEnvelope> recentMessages;\n\n    public Chat(String chatUid,\n                Id host,\n                TreeClock current,\n                Map<Id, Member> members,\n                Map<String, GroupProperty> groupState,\n                List<MessageEnvelope> recentMessages) {\n        this.chatUid = chatUid;\n        this.host = host;\n        this.current = current;\n        this.members = Collections.unmodifiableMap(members);\n        this.groupState = Collections.unmodifiableMap(groupState);\n        this.recentMessages = Collections.unmodifiableList(recentMessages);\n    }\n\n    public String getTitle() {\n        GroupProperty prop = groupState.get(\"title\");\n        if (prop != null) {\n            return prop.value;\n        }\n        return \"\";\n    }\n\n    public Set<String> getAdmins() {\n        GroupProperty current = groupState.get(GroupProperty.ADMINS_STATE_KEY);\n        if (current == null)\n            return Collections.emptySet();\n        return new HashSet<>(Arrays.asList(current.value.split(\",\")));\n    }\n\n    public Member host() {\n        return members.get(host);\n    }\n\n    public Member getMember(Id id) {\n        return members.get(id);\n    }\n\n    public Member getMember(String username) {\n        List<Member> matching = members.values().stream()\n                .filter(m -> m.username.equals(username))\n                .collect(Collectors.toList());\n        return matching.stream().filter(m -> !m.removed).findFirst().orElse(matching.get(0));\n    }\n\n    public CompletableFuture<ChatUpdate> sendMessage(Message body,\n                                                     SigningPrivateKeyAndPublicHash signer,\n                                                     SigningPrivateKeyAndPublicHash userIdentity,\n                                                     MessageStore store,\n                                                     ContentAddressedStorage ipfs,\n                                                     Crypto crypto) {\n        boolean nonEmpty = host().messagesMergedUpto > 0;\n        return (nonEmpty ?\n                store.getMessages(host().messagesMergedUpto - 1, host().messagesMergedUpto) :\n                Futures.of(Collections.<SignedMessage>emptyList()))\n                .thenCompose(recent -> Futures.combineAllInOrder(recent.stream()\n                        .map(s -> crypto.hasher.bareHash(s.msg.serialize())\n                                .thenApply(MessageRef::new))\n                        .collect(Collectors.toList())))\n                .thenCompose(recentRefs -> sendMessage(body, signer, userIdentity, recentRefs, ipfs, crypto));\n    }\n\n    private CompletableFuture<ChatUpdate> sendMessage(Message body,\n                                                      SigningPrivateKeyAndPublicHash signer,\n                                                      SigningPrivateKeyAndPublicHash userIdentity,\n                                                      List<MessageRef> recentRefs,\n                                                      ContentAddressedStorage ipfs,\n                                                      Crypto crypto) {\n        TreeClock msgTime = current.increment(host);\n        MessageEnvelope msg = new MessageEnvelope(host, msgTime, LocalDateTime.now(ZoneOffset.UTC), recentRefs, body);\n        return signer.secret.signatureOnly(msg.serialize()).thenCompose(signature -> {\n            SignedMessage signed = new SignedMessage(signature, msg);\n            return mergeMessage(chatUid, signed, host(), userIdentity, ipfs, crypto);\n        });\n    }\n\n    public synchronized List<MessageEnvelope> getRecent() {\n        return new ArrayList<>(recentMessages);\n    }\n\n    private Chat withMembers(Map<Id, Member> updated) {\n        return new Chat(chatUid, host, current, updated, groupState, recentMessages);\n    }\n\n    private Chat withTime(TreeClock newTime) {\n        return new Chat(chatUid, host, newTime, members, groupState, recentMessages);\n    }\n\n    private Chat withHost(Id host) {\n        return new Chat(chatUid, host, current, members, groupState, recentMessages);\n    }\n\n    private Chat withProperties(Map<String, GroupProperty> updated) {\n        return new Chat(chatUid, host, current, members, updated, recentMessages);\n    }\n\n    private Chat addToRecent(MessageEnvelope m) {\n        ArrayList<MessageEnvelope> updated = new ArrayList<>(recentMessages);\n        if (updated.size() >= 10)\n            updated.remove(0);\n        updated.add(m);\n        return new Chat(chatUid, host, current, members, groupState, updated);\n    }\n\n    public static PrivateChatState generateChatIdentity(Crypto crypto) {\n        SigningKeyPair chatIdentity = SigningKeyPair.random(crypto.random, crypto.signer);\n        PublicKeyHash preHash = ContentAddressedStorage.hashKey(chatIdentity.publicSigningKey);\n        SigningPrivateKeyAndPublicHash chatIdWithHash =\n                new SigningPrivateKeyAndPublicHash(preHash, chatIdentity.secretSigningKey);\n        return new PrivateChatState(chatIdWithHash, chatIdentity.publicSigningKey, Collections.emptySet());\n    }\n\n    /** Apply message to this Chat's state\n     *\n     * @return The state after applying the message\n     */\n    public CompletableFuture<ChatUpdate> applyMessage(SignedMessage signed,\n                                                      String chatUid,\n                                                      SigningPrivateKeyAndPublicHash userIdentity,\n                                                      ContentAddressedStorage ipfs,\n                                                      Crypto crypto) {\n        MessageEnvelope msg = signed.msg;\n        Member author = members.get(msg.author);\n        switch (msg.payload.type()) {\n            case Invite: {\n                Invite invite = (Invite) msg.payload;\n                Id newMember = invite.recipientId;\n                if (members.containsKey(newMember))\n                    throw new IllegalStateException(\"Id already exists in this chat!\");\n                if (!newMember.parent().equals(author.id))\n                    throw new IllegalStateException(\"Invalid invite Id!\");\n                HashMap<Id, Member> updated = new HashMap<>(members);\n                updated.put(author.id, members.get(author.id).incrementInvited());\n                long indexIntoParent = getMember(newMember.parent()).messagesMergedUpto;\n                String username = invite.username;\n                PublicKeyHash identity = invite.identity;\n                // If we have been removed and re-invited, generate a new chat identity\n                Member host = host();\n                if (host.username.equals(username) && host.removed) {\n                    PrivateChatState newIdentity = generateChatIdentity(crypto);\n                    return OwnerProof.build(userIdentity, newIdentity.chatIdentity.publicKeyHash).thenCompose(chatId -> {\n                        Member newHost = new Member(username, newMember, identity, indexIntoParent, 0);\n                        updated.put(newMember, newHost);\n                        ChatUpdate afterInvite = new ChatUpdate(withMembers(updated).addToRecent(msg), Arrays.asList(signed), Collections.emptyList(), Collections.emptySet());\n                        Join joinMsg = new Join(host.username, host.identity, chatId, newIdentity.chatIdPublic);\n                        return crypto.hasher.bareHash(signed.msg.serialize())\n                                .thenApply(MessageRef::new)\n                                .thenCompose(ref -> afterInvite.state.withTime(afterInvite.state.current.withMember(newHost.id))\n                                        .withHost(newHost.id)\n                                        .sendMessage(joinMsg, userIdentity, userIdentity, Arrays.asList(ref), ipfs, crypto)\n                                        .thenApply(afterInvite::apply));\n                    });\n                }\n\n                updated.put(newMember, new Member(username, newMember, identity, indexIntoParent, 0));\n                return Futures.of(new ChatUpdate(withMembers(updated).addToRecent(msg), Arrays.asList(signed), Collections.emptyList(), Collections.emptySet()));\n            }\n            case Join:\n                if (author.chatIdentity.isEmpty()) {\n                    // This is a Join message from a new member\n                    Join join = (Join)msg.payload;\n                    OwnerProof chatIdentity = join.chatIdentity;\n                    if (!chatIdentity.ownedKey.equals(author.identity))\n                        throw new IllegalStateException(\"Identity keys don't match!\");\n                    // verify signature\n                    return chatIdentity.getAndVerifyOwner(author.identity, ipfs).thenApply(x -> {\n                        Map<Id, Member> updated = new HashMap<>(members);\n                        updated.put(author.id, author.withChatId(chatIdentity));\n                        return new ChatUpdate(withMembers(updated).addToRecent(msg), Arrays.asList(signed), Collections.emptyList(), Collections.emptySet());\n                    });\n                }\n                break;\n            case GroupState: {\n                SetGroupState update = (SetGroupState) msg.payload;\n                GroupProperty existing = groupState.get(update.key);\n                // only admins can update the list of admins\n                // concurrent allowed modifications are tie-broken by Id\n                if (existing == null ||\n                        ((!update.key.equals(GroupProperty.ADMINS_STATE_KEY) || getAdmins().contains(author.username)) &&\n                                (existing.updateTimestamp.isBeforeOrEqual(msg.timestamp) ||\n                                        (existing.updateTimestamp.isConcurrentWith(msg.timestamp) &&\n                                                msg.author.compareTo(existing.author) < 0)))) {\n                    Map<String, GroupProperty> updated = new HashMap<>(groupState);\n                    updated.put(update.key, new GroupProperty(msg.author, msg.timestamp, update.value));\n                    return Futures.of(new ChatUpdate(withProperties(updated).addToRecent(msg), Arrays.asList(signed), Collections.emptyList(), Collections.emptySet()));\n                }\n                break;\n            }\n            case Application: {\n                if (msg.author.equals(host))\n                    break; // Don't attempt to copy own own media\n                ApplicationMessage content = (ApplicationMessage) msg.payload;\n                List<FileRef> fileRefs = content.body.stream()\n                        .flatMap(c -> c.reference().stream())\n                        .collect(Collectors.toList());\n                // note media to mirror our storage\n                return Futures.of(new ChatUpdate(addToRecent(msg), Arrays.asList(signed), fileRefs, Collections.emptySet()));\n            }\n            case RemoveMember: {\n                RemoveMember rem = (RemoveMember) msg.payload;\n                if (!rem.chatUid.equals(chatUid))\n                    return Futures.of(new ChatUpdate(this, Collections.emptyList(), Collections.emptyList(), Collections.emptySet())); // ignore message from incorrect chat\n                // anyone can remove themselves\n                // an admin can remove anyone\n                if (rem.memberToRemove.equals(msg.author) || getAdmins().contains(author.username)) {\n                    String username = getMember(rem.memberToRemove).username;\n                    Member updatedMember = members.get(rem.memberToRemove).removed(true);\n                    Map<Id, Member> updated = new HashMap<>(members);\n                    updated.put(rem.memberToRemove, updatedMember);\n                    // revoke read access to shared chat state from removee (unless removee is us!)\n                    return Futures.of(new ChatUpdate(addToRecent(msg).withMembers(updated), Arrays.asList(signed), Collections.emptyList(),\n                                    username.equals(host().username) ? Collections.emptySet() : Collections.singleton(username)));\n                }\n                break;\n            }\n            case ReplyTo: {\n                ReplyTo content = (ReplyTo) msg.payload;\n                List<FileRef> fileRefs = content.content.body.stream()\n                        .flatMap(c -> c.reference().stream())\n                        .collect(Collectors.toList());\n                // note media to mirror our storage\n                return Futures.of(new ChatUpdate(addToRecent(msg), Arrays.asList(signed), fileRefs, Collections.emptySet()));\n            }\n            case Delete:\n                break;\n        }\n        return Futures.of(new ChatUpdate(addToRecent(msg), Arrays.asList(signed), Collections.emptyList(), Collections.emptySet()));\n    }\n\n    public CompletableFuture<ChatUpdate> merge(String chatUid,\n                                               Id mirrorHostId,\n                                               SigningPrivateKeyAndPublicHash userIdentity,\n                                               MessageStore mirrorStore,\n                                               ContentAddressedStorage ipfs,\n                                               Crypto crypto) {\n        Member host = getMember(mirrorHostId);\n        return mirrorStore.getMessagesFrom(host.messagesMergedUpto)\n                .thenCompose(newMessages -> Futures.reduceAll(newMessages,\n                        ChatUpdate.empty(this),\n                        (u, msg) -> u.state.mergeMessage(chatUid, msg, u.state.getMember(mirrorHostId), userIdentity, ipfs, crypto)\n                                .thenApply(u::apply),\n                        (a, b) -> a.apply(b)));\n    }\n\n    private CompletableFuture<ChatUpdate> mergeMessage(String chatUid,\n                                                       SignedMessage signed,\n                                                       Member host,\n                                                       SigningPrivateKeyAndPublicHash userIdentity,\n                                                       ContentAddressedStorage ipfs,\n                                                       Crypto crypto) {\n        Member author = members.get(signed.msg.author);\n        MessageEnvelope msg = signed.msg;\n        if (! msg.timestamp.isBeforeOrEqual(current) && !author.removed) {\n            // check signature\n            return (author.chatIdentity.isPresent() ?\n                    author.chatIdentity.get().getAndVerifyOwner(author.identity, ipfs) :\n                    Futures.of(author.identity))\n                    .thenCompose(hash -> ipfs.getSigningKey(hash, hash))\n                    .thenCompose(signerOpt -> {\n                        if (signerOpt.isEmpty())\n                            throw new IllegalStateException(\"Couldn't retrieve public signing key!\");\n                        signerOpt.get().unsignMessage(ArrayOps.concat(signed.signature, signed.msg.serialize()));\n                        return applyMessage(signed, chatUid, userIdentity, ipfs, crypto);\n                    }).thenApply(u -> u.withState(u.state.mergeMessageTimestamp(msg.timestamp, host)));\n        }\n        return Futures.of(ChatUpdate.empty(incrementHost(host)));\n    }\n\n    private Chat incrementHost(Member source) {\n        Map<Id, Member> updated = new HashMap<>(members);\n        updated.put(source.id, source.incrementMessages());\n        return new Chat(chatUid, host, current, updated, groupState, recentMessages);\n    }\n\n    private Chat mergeMessageTimestamp(TreeClock timestamp, Member source) {\n        TreeClock newTime = current.merge(timestamp);\n        Map<Id, Member> updated = new HashMap<>(members);\n        updated.put(source.id, members.get(source.id).incrementMessages());\n        updated.put(host, host().incrementMessages());\n        return new Chat(chatUid, host, newTime, updated, groupState, recentMessages);\n    }\n\n    public CompletableFuture<ChatUpdate> join(Member host,\n                                              OwnerProof chatId,\n                                              PublicSigningKey chatIdPublic,\n                                              SigningPrivateKeyAndPublicHash identity,\n                                              MessageStore ourStore,\n                                              ContentAddressedStorage ipfs,\n                                              Crypto crypto) {\n        Join joinMsg = new Join(host.username, host.identity, chatId, chatIdPublic);\n        return withTime(current.withMember(host.id)).sendMessage(joinMsg, identity, identity, ourStore, ipfs, crypto);\n    }\n\n    public Chat copy(Member host) {\n        if (! members.containsKey(host.id))\n            throw new IllegalStateException(\"Only an invited member can mirror a conversation!\");\n        Map<Id, Member> clonedMembers = members.entrySet().stream()\n                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().copy()));\n        clonedMembers.put(host.id, host.copy());\n        return new Chat(chatUid, host.id, current, clonedMembers, new HashMap<>(groupState), new ArrayList<>(recentMessages));\n    }\n\n    public CompletableFuture<ChatUpdate> inviteMember(String username,\n                                                      PublicKeyHash identity,\n                                                      SigningPrivateKeyAndPublicHash ourChatIdentity,\n                                                      SigningPrivateKeyAndPublicHash userIdentity,\n                                                      MessageStore ourStore,\n                                                      ContentAddressedStorage ipfs,\n                                                      Crypto crypto) {\n        return inviteMembers(Arrays.asList(username), Arrays.asList(identity), ourChatIdentity, userIdentity, ourStore, ipfs, crypto);\n    }\n\n    public CompletableFuture<ChatUpdate> inviteMembers(List<String> usernames,\n                                                       List<PublicKeyHash> identities,\n                                                       SigningPrivateKeyAndPublicHash ourChatIdentity,\n                                                       SigningPrivateKeyAndPublicHash userIdentity,\n                                                       MessageStore ourStore,\n                                                       ContentAddressedStorage ipfs,\n                                                       Crypto crypto) {\n        List<Integer> range = IntStream.range(0, usernames.size()).mapToObj(i -> i).collect(Collectors.toList());\n        return Futures.reduceAll(range, ChatUpdate.empty(this),\n                (u, i) -> {\n                    String username = usernames.get(i);\n                    PublicKeyHash identity = identities.get(i);\n\n                    Member us = u.state.host();\n                    Id newMember = u.state.host.fork(us.membersInvited);\n                    Invite invite = new Invite(username, identity, newMember);\n\n                    TreeClock newTime = u.state.current.withMember(newMember);\n                    return u.state.withTime(newTime).sendMessage(invite, ourChatIdentity, userIdentity, ourStore, ipfs, crypto)\n                            .thenApply(u::apply);\n                },\n                ChatUpdate::apply);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"v\", new CborObject.CborLong(0));\n        result.put(\"i\", new CborObject.CborString(chatUid));\n        result.put(\"h\", host);\n        result.put(\"c\", current);\n        result.put(\"m\", new CborObject.CborList(members));\n        result.put(\"g\", CborObject.CborMap.build(groupState.entrySet()\n                .stream()\n                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));\n        result.put(\"r\", new CborObject.CborList(recentMessages));\n        return CborObject.CborMap.build(result);\n    }\n\n    public static Chat fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        long version = m.getLong(\"v\");\n        String chatUid = m.getString(\"i\");\n        Id host = m.get(\"h\", Id::fromCbor);\n        TreeClock current = m.get(\"c\", TreeClock::fromCbor);\n        Map<Id, Member> members = m.getListMap(\"m\", Id::fromCbor, Member::fromCbor);\n        Map<String, GroupProperty> group = m.getMap(\"g\", CborObject.CborString::getString, GroupProperty::fromCbor);\n        List<MessageEnvelope> recent = new ArrayList<>(m.getList(\"r\", MessageEnvelope::fromCbor));\n        return new Chat(chatUid, host, current, members, group, recent);\n    }\n\n    public static Chat createNew(String uid, String username, PublicKeyHash identity) {\n        Id creator = Id.creator();\n        Member us = new Member(username, creator, identity, 0, 0);\n        HashMap<Id, Member> members = new HashMap<>();\n        members.put(creator, us);\n        TreeClock zero = TreeClock.init(Arrays.asList(us.id));\n        HashMap<String, GroupProperty> groupState = new HashMap<>();\n        return new Chat(uid, creator, zero, members, groupState, new ArrayList<>());\n    }\n\n    public static List<Chat> createNew(String uid, List<String> usernames, List<PublicKeyHash> identities) {\n        HashMap<Id, Member> members = new HashMap<>();\n        List<Id> initialMembers = new ArrayList<>();\n\n        for (int i=0; i < usernames.size(); i++) {\n            Id id = new Id(i);\n            initialMembers.add(id);\n            Member member = new Member(usernames.get(i), id, identities.get(i), 0, 0);\n            members.put(id, member);\n        }\n        TreeClock genesis = TreeClock.init(initialMembers);\n\n        return initialMembers.stream()\n                .map(id -> new Chat(uid, id, genesis, members.entrySet().stream()\n                        .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().copy())), new HashMap<>(), new ArrayList<>()))\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Chat chat = (Chat) o;\n        return Objects.equals(chatUid, chat.chatUid) &&\n                Objects.equals(host, chat.host) &&\n                Objects.equals(current, chat.current) &&\n                Objects.equals(members, chat.members) &&\n                Objects.equals(groupState, chat.groupState) &&\n                Objects.equals(recentMessages, chat.recentMessages);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(chatUid, host, current, members, groupState, recentMessages);\n    }\n}"
  },
  {
    "path": "src/peergos/shared/messaging/ChatController.java",
    "content": "package peergos.shared.messaging;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.display.*;\nimport peergos.shared.messaging.messages.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class ChatController {\n    public static final String SHARED_CHAT_STATE = \"peergos-chat-state.cbor\";\n    public static final String SHARED_MSG_LOG = \"peergos-chat-messages.cborstream\";\n    public static final String SHARED_MSG_LOG_INDEX = \"peergos-chat-messages.index.bin\";\n    public static final String PRIVATE_CHAT_STATE = \"private-chat-state.cbor\";\n\n    public final String chatUuid;\n    public final MessageStore store;\n    public final PrivateChatState privateChatState;\n    private final Chat state;\n    private final FileWrapper root; // includes the version that the state above was derived from\n    private final LRUCache<MessageRef, MessageEnvelope> cache;\n    private final Hasher hasher;\n    private final UserContext context;\n\n    public ChatController(String chatUuid,\n                          Chat state,\n                          MessageStore store,\n                          PrivateChatState privateChatState,\n                          FileWrapper root,\n                          LRUCache<MessageRef, MessageEnvelope> cache,\n                          UserContext context) {\n        this.chatUuid = chatUuid;\n        this.state = state;\n        this.store = store;\n        this.privateChatState = privateChatState;\n        this.root = root;\n        this.cache = cache;\n        this.hasher = context.crypto.hasher;\n        this.context = context;\n    }\n\n    public Member host() {\n        return state.host();\n    }\n\n    public Member getMember(String username) {\n        return state.getMember(username);\n    }\n\n    @JsMethod\n    public String getUsername(Id author) {\n        return state.getMember(author).username;\n    }\n\n    @JsMethod\n    public Set<String> getMemberNames() {\n        return state.members.values().stream()\n                .filter(m -> !m.removed)\n                .filter(m -> ! privateChatState.deletedMembers.contains(m.username))\n                .map(m -> m.username)\n                .collect(Collectors.toSet());\n    }\n\n    @JsMethod\n    public Set<String> getPendingMemberNames() {\n        return state.members.values().stream()\n                .filter(m -> !m.removed)\n                .filter(m -> m.chatIdentity.isEmpty())\n                .map(m -> m.username)\n                .collect(Collectors.toSet());\n    }\n\n    public ChatController with(PrivateChatState priv) {\n        return new ChatController(chatUuid, state, store, priv, root, cache, context);\n    }\n\n    @JsMethod\n    public Set<String> deletedMemberNames() {\n        return privateChatState.deletedMembers;\n    }\n\n    @JsMethod\n    public List<MessageEnvelope> getRecent() {\n        return state.getRecent();\n    }\n\n    @JsMethod\n    public CompletableFuture<MessageEnvelope> getMessageFromRef(MessageRef ref, int sourceIndex) {\n        MessageEnvelope cached = cache.get(ref);\n        if (cached != null)\n            return Futures.of(cached);\n        // Try 100 message prior to reference source first, then try previous chunks\n        return store.getMessages(Math.max(0, sourceIndex - 100), sourceIndex)\n                .thenCompose(allSigned -> Futures.findFirst(allSigned, s -> hashMessage(s.msg)\n                        .thenApply(h -> h.equals(ref) ? Optional.of(s.msg) : Optional.empty())))\n                .thenCompose(resOpt -> resOpt.map(Futures::of)\n                        .orElseGet(() -> getMessageFromRef(ref, sourceIndex - 100)));\n    }\n\n    @JsMethod\n    public CompletableFuture<MessageRef> generateHash(MessageEnvelope m) {\n        byte[] raw = m.serialize();\n        return hasher.bareHash(raw)\n                .thenApply(MessageRef::new);\n    }\n\n    private CompletableFuture<MessageRef> hashMessage(MessageEnvelope m) {\n        return generateHash(m)\n                .thenApply(r -> {\n                    cache.put(r, m);\n                    return r;\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<List<MessageEnvelope>> getMessages(int from, int to) {\n        return store.getMessages(from, to)\n                .thenApply(signed -> signed.stream().map(s -> s.msg).collect(Collectors.toList()));\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> sendMessage(Message message) {\n        return applyAndCommit(chat -> chat.sendMessage(message, privateChatState.chatIdentity, context.signer, store,\n                context.network.dhtClient, context.crypto), context.username);\n    }\n\n    @JsMethod\n    public String getGroupProperty(String key) {\n        return state.groupState.get(key).value;\n    }\n\n    @JsMethod\n    public boolean hasGroupProperty(String key) {\n        return state.groupState.containsKey(key);\n    }\n\n    @JsMethod\n    public String getTitle() {\n        return state.getTitle();\n    }\n\n    @JsMethod\n    public Set<String> getAdmins() {\n        return state.getAdmins();\n    }\n\n    @JsMethod\n    public boolean isAdmin() {\n        return state.getAdmins().contains(state.host().username);\n    }\n\n    private ChatController withState(Chat c) {\n        return new ChatController(chatUuid, c, store, privateChatState, root, cache, context);\n    }\n\n    private ChatController withStore(MessageStore newStore) {\n        return new ChatController(chatUuid, state, newStore, privateChatState, root, cache, context);\n    }\n\n    private ChatController withRoot(FileWrapper root) {\n        return new ChatController(chatUuid, state, store, privateChatState, root, cache, context);\n    }\n\n    public CompletableFuture<ChatController> join(SigningPrivateKeyAndPublicHash identity) {\n        return OwnerProof.build(identity, privateChatState.chatIdentity.publicKeyHash)\n                .thenCompose(chatId -> applyAndCommit(chat -> chat.join(state.host(), chatId, privateChatState.chatIdPublic,\n                        identity, store, context.network.dhtClient, context.crypto), context.username));\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> addAdmin(String username) {\n        Set<String> admins = new TreeSet<>(state.getAdmins());\n        if (! admins.isEmpty() && ! admins.contains(state.host().username))\n            throw new IllegalStateException(\"Only admins can modify the admin list!\");\n        admins.add(username);\n        SetGroupState msg = new SetGroupState(GroupProperty.ADMINS_STATE_KEY, admins.stream().collect(Collectors.joining(\",\")));\n        return sendMessage(msg);\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> removeAdmin(String username) {\n        Set<String> admins = new TreeSet<>(state.getAdmins());\n        if (! admins.contains(state.host().username))\n            throw new IllegalStateException(\"Only admins can modify the admin list!\");\n\n        admins.remove(username);\n        if (admins.isEmpty())\n            throw new IllegalStateException(\"A chat must always have at least 1 admin\");\n        SetGroupState msg = new SetGroupState(GroupProperty.ADMINS_STATE_KEY, admins.stream().collect(Collectors.joining(\",\")));\n        return sendMessage(msg);\n    }\n\n    public CompletableFuture<ChatController> invite(List<String> usernames,\n                                                    List<PublicKeyHash> identities) {\n        return applyAndCommit(chat -> chat.inviteMembers(usernames, identities, privateChatState.chatIdentity,\n                context.signer, store, context.network.dhtClient, context.crypto), context.username);\n    }\n\n    public CompletableFuture<ChatController> mergeMessages(String username,\n                                                           MessageStore mirrorStore) {\n        Member mirrorHost = state.getMember(username);\n        return applyAndCommit(chat -> chat.merge(chatUuid, mirrorHost.id, context.signer, mirrorStore, context.network.dhtClient, context.crypto), username);\n    }\n\n    private CompletableFuture<Snapshot> copyFile(FileWrapper dir, Path sourcePath, String mirrorUsername, Snapshot v, Committer c) {\n        // Try copying file from source first, and then fallback to mirror we are currently merging\n        return Futures.asyncExceptionally(() -> context.getByPath(sourcePath.toString(), v)\n                        .thenApply(Optional::get),\n                t -> context.getByPath(PathUtil.get(mirrorUsername).resolve(sourcePath.subpath(1, sourcePath.getNameCount())).toString(), v)\n                        .thenApply(Optional::get))\n                .thenCompose(f -> f.getInputStream(f.version.get(f.writer()), context.network, context.crypto, x -> {})\n                        .thenCompose(r -> dir.uploadFileSection(v, c, f.getName(), r, false, 0, f.getSize(),\n                                Optional.empty(), false, false, false, context.network, context.crypto, () -> false, x -> {},\n                                context.crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH), Optional.empty(),\n                                Optional.of(Bat.random(context.crypto.random)), dir.mirrorBatId())));\n    }\n\n    private Path getChatMediaDir(ChatController current) {\n        return PathUtil.get(Messenger.MESSAGING_BASE_DIR,\n                current.chatUuid,\n                \"shared\",\n                \"media\");\n    }\n\n    private CompletableFuture<Snapshot> mirrorMedia(FileRef ref, ChatController chat, String currentMirrorUsername, Snapshot in, Committer committer) {\n        if (currentMirrorUsername.equals(context.username))\n            return Futures.of(in);\n        Path mediaDir = getChatMediaDir(chat);\n        Path sourcePath = PathUtil.get(ref.path);\n        Path chatRelativePath = sourcePath.subpath(1 + mediaDir.getNameCount(), sourcePath.getNameCount());\n        Path ourCopy = mediaDir.resolve(chatRelativePath);\n        Path parent = ourCopy.getParent();\n        List<String> mediaFileParentPath = IntStream.range(0, parent.getNameCount())\n                .mapToObj(i -> parent.getName(i).toString())\n                .collect(Collectors.toList());\n        return context.getByPath(context.username, in)\n                .thenApply(Optional::get)\n                .thenCompose(home -> home.getOrMkdirs(mediaFileParentPath, false, home.mirrorBatId(), context.network, context.crypto, in, committer))\n                .thenCompose(dir -> copyFile(dir.right, sourcePath, currentMirrorUsername, dir.left, committer));\n    }\n\n    private CompletableFuture<Snapshot> overwriteState(FileWrapper root, Chat c, Snapshot v, Committer com) {\n        byte[] raw = c.serialize();\n        return root.getDescendentByPath(\"shared/\"+ SHARED_CHAT_STATE, context.crypto.hasher, context.network)\n                .thenCompose(file -> file.get().overwriteFile(AsyncReader.build(raw), raw.length, context.network, context.crypto, x -> {}, v, com));\n    }\n\n    private CompletableFuture<ChatController> applyAndCommit(Function<Chat, CompletableFuture<ChatUpdate>> modifier,\n                                                             String mirrorUsername) {\n        NetworkAccess network = context.network;\n        return network.synchronizer.applyComplexComputation(context.signer.publicKeyHash, root.signingPair(),\n                (s, c) -> root.getUpdated(s, network)\n                        .thenCompose(updated -> updated.getChild(\"shared\", hasher, network)\n                                .thenCompose(sharedDir -> getChatState(sharedDir.get(), network, context.crypto))\n                                .thenCompose(chatState -> modifier.apply(chatState)\n                                        .thenCompose(u -> commitUpdate(u, mirrorUsername, s, c)))))\n                .thenApply(res -> res.right);\n    }\n\n    private CompletableFuture<Pair<Snapshot, ChatController>> commitUpdate(ChatUpdate u, String mirrorUsername, Snapshot in, Committer c) {\n        if (u.isEmpty() && u.state.equals(state))\n            return Futures.of(new Pair<>(in, this));\n        // 1. rotate access control\n        // 2. copy media\n        // 3. commit any new private state\n        // 4. append messages\n        // 5. commit state file\n        boolean noRemovals = u.toRevokeAccess.isEmpty();\n        return (noRemovals ? Futures.of(in) : store.revokeAccess(u.toRevokeAccess, in, c))\n                .thenCompose(s -> Futures.reduceAll(u.mediaToCopy, s, (v, f) -> mirrorMedia(f, this, mirrorUsername, v, c),\n                        (a, b) -> a.merge(b))\n                        .thenCompose(s2 -> root.getUpdated(s2, context.network)\n                                .thenCompose(base -> commitPrivateState(u.priv, base, context.network, context.crypto, s2, c)\n                                        .thenCompose(s3 -> base.getUpdated(s3, context.network))))\n                        .thenCompose(base -> (noRemovals ? Futures.of(store) : getChatMessageStore(base, context))\n                                .thenCompose(newStore -> newStore.addMessages(base.version, c, state.host().messagesMergedUpto, u.newMessages))\n                                .thenCompose(s4 -> overwriteState(base, u.state, s4, c)))\n                        .thenCompose(s5 -> root.getUpdated(s5, context.network)\n                                .thenCompose(newRoot -> getChatMessageStore(newRoot, context)\n                                        .thenApply(newStore -> new Pair<>(s5, withState(u.state).withRoot(newRoot).withStore(newStore))))));\n    }\n\n    private static CompletableFuture<Snapshot> commitPrivateState(Optional<PrivateChatState> priv,\n                                                                  FileWrapper chatRoot,\n                                                                  NetworkAccess network,\n                                                                  Crypto crypto,\n                                                                  Snapshot in,\n                                                                  Committer c) {\n        if (priv.isEmpty())\n            return Futures.of(in);\n\n        byte[] rawPrivateChatState = priv.get().serialize();\n        return chatRoot.uploadFileSection(in, c, ChatController.PRIVATE_CHAT_STATE,\n                AsyncReader.build(rawPrivateChatState), false, 0, rawPrivateChatState.length,\n                Optional.empty(), false, true, true, network, crypto, () -> false, x -> {},\n                crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)), chatRoot.mirrorBatId());\n    }\n\n    private static CompletableFuture<Pair<FileWrapper, FileWrapper>> getSharedLogAndIndex(FileWrapper chatRoot, Hasher hasher, NetworkAccess network) {\n        return chatRoot.getDescendentByPath(\"shared/\" + SHARED_MSG_LOG, hasher, network)\n                .thenCompose(msgFile -> chatRoot.getDescendentByPath(\"shared/\" + SHARED_MSG_LOG_INDEX, hasher, network)\n                        .thenApply(index -> new Pair<>(msgFile.get(), index.get())));\n    }\n\n    public static CompletableFuture<MessageStore> getChatMessageStore(FileWrapper chatRoot, UserContext context) {\n        Path chatRootPath = Messenger.getChatPath(context.username, chatRoot.getName());\n        return getSharedLogAndIndex(chatRoot, context.crypto.hasher, context.network)\n                .thenApply(files -> new FileBackedMessageStore(files.left, files.right, context,\n                        chatRootPath.resolve(\"shared\"),\n                        () -> context.getByPath(chatRootPath)\n                                .thenApply(Optional::get)\n                                .thenCompose(d -> getSharedLogAndIndex(d, context.crypto.hasher, context.network))));\n    }\n\n    public static CompletableFuture<Chat> getChatState(FileWrapper chatSharedDir, NetworkAccess network, Crypto crypto) {\n        return chatSharedDir.getChild(SHARED_CHAT_STATE, crypto.hasher, network)\n                .thenCompose(chatStateOpt -> Serialize.parse(chatStateOpt.get(), Chat::fromCbor, network, crypto));\n    }\n\n    private static CompletableFuture<PrivateChatState> getPrivateChatState(FileWrapper chatRoot, NetworkAccess network, Crypto crypto) {\n        return chatRoot.getChild(PRIVATE_CHAT_STATE, crypto.hasher, network)\n                .thenCompose(priv -> Serialize.parse(priv.get(), PrivateChatState::fromCbor, network, crypto));\n    }\n\n    public static CompletableFuture<ChatController> getChatController(FileWrapper chatRoot,\n                                                                      UserContext context,\n                                                                      LRUCache<MessageRef, MessageEnvelope> cache) {\n        return chatRoot.getChild(\"shared\", context.crypto.hasher, context.network)\n                .thenCompose(sharedDir -> getChatState(sharedDir.get(), context.network, context.crypto))\n                .thenCompose(chat -> getPrivateChatState(chatRoot, context.network, context.crypto)\n                        .thenCompose(priv -> getChatMessageStore(chatRoot, context)\n                                .thenApply(msgStore -> new ChatController(chatRoot.getName(), chat, msgStore, priv,\n                                        chatRoot, cache, context))));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/ChatUpdate.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.display.*;\n\nimport java.util.*;\n\npublic class ChatUpdate {\n    public final Chat state;\n    public final List<SignedMessage> newMessages;\n    public final List<FileRef> mediaToCopy;\n    public final Set<String> toRevokeAccess;\n    public final Optional<PrivateChatState> priv;\n\n    public ChatUpdate(Chat state,\n                      List<SignedMessage> newMessages,\n                      List<FileRef> mediaToCopy,\n                      Set<String> toRevokeAccess,\n                      Optional<PrivateChatState> priv) {\n        this.state = state;\n        this.newMessages = newMessages;\n        this.mediaToCopy = mediaToCopy;\n        this.toRevokeAccess = toRevokeAccess;\n        this.priv = priv;\n    }\n\n    public boolean isEmpty() {\n        return newMessages.isEmpty() && mediaToCopy.isEmpty() && toRevokeAccess.isEmpty() && priv.isEmpty();\n    }\n\n    public ChatUpdate(Chat state,\n                      List<SignedMessage> newMessages,\n                      List<FileRef> mediaToCopy,\n                      Set<String> toRevokeAccess) {\n        this(state, newMessages, mediaToCopy, toRevokeAccess, Optional.empty());\n    }\n\n    public ChatUpdate apply(ChatUpdate next) {\n        ArrayList<SignedMessage> msgs = new ArrayList<>(newMessages);\n        msgs.addAll(next.newMessages);\n        ArrayList<FileRef> refs = new ArrayList<>(mediaToCopy);\n        refs.addAll(next.mediaToCopy);\n        Set<String> toRevoke = new HashSet(toRevokeAccess);\n        toRevoke.addAll(next.toRevokeAccess);\n        return new ChatUpdate(next.state, msgs, refs, toRevoke, priv.flatMap(a -> next.priv.map(a::apply)));\n    }\n\n    public ChatUpdate withState(Chat c) {\n        return new ChatUpdate(c, newMessages, mediaToCopy, toRevokeAccess, priv);\n    }\n\n    public static ChatUpdate empty(Chat c) {\n        return new ChatUpdate(c, Collections.emptyList(), Collections.emptyList(), Collections.emptySet(), Optional.empty());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/FileBackedMessageStore.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class FileBackedMessageStore implements MessageStore {\n\n    private final FileWrapper messages;\n    private final FileWrapper indexFile;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private final UserContext context;\n    private final Path sharedDir;\n    private final Supplier<CompletableFuture<Pair<FileWrapper, FileWrapper>>> filesUpdater;\n\n    public FileBackedMessageStore(FileWrapper messages,\n                                  FileWrapper indexFile,\n                                  UserContext context,\n                                  Path sharedDir,\n                                  Supplier<CompletableFuture<Pair<FileWrapper, FileWrapper>>> filesUpdater) {\n        this.messages = messages;\n        this.indexFile = indexFile;\n        this.network = context.network;\n        this.crypto = context.crypto;\n        this.context = context;\n        this.sharedDir = sharedDir;\n        this.filesUpdater = filesUpdater;\n    }\n\n    private CompletableFuture<Pair<Long, Integer>> getChunkByteOffset(long index) {\n        if (messages.getSize() < 5*1024*1024)\n            return Futures.of(new Pair<>(0L, (int) index));\n        return indexFile.getInputStream(indexFile.version.get(indexFile.writer()), network, crypto, x -> {})\n                        .thenCompose(reader -> findOffset(reader, new byte[1024],\n                                0L, 0L, index, indexFile.getSize()));\n    }\n\n    private CompletableFuture<Pair<Long, Integer>> findOffset(AsyncReader r,\n                                                              byte[] buf,\n                                                              long previousIndex,\n                                                              long previousByteOffset,\n                                                              long index,\n                                                              long remainingBytes) {\n        if (remainingBytes == 0)\n            return Futures.of(new Pair<>(previousByteOffset, (int)(index - previousIndex)));\n        int toRead = remainingBytes > buf.length ? buf.length : (int) remainingBytes;\n        DataInputStream din = new DataInputStream(new ByteArrayInputStream(buf));\n        return r.readIntoArray(buf, 0, toRead)\n                .thenCompose(read -> {\n                    long prevIndex = previousIndex;\n                    long prevBytes = previousByteOffset;\n                    for (int i=0; i < read / 16; i++) {\n                        try {\n                            long msgIndex = din.readLong();\n                            long byteOffset = din.readLong();\n                            if (msgIndex > index)\n                                return Futures.of(new Pair<>(prevBytes, (int)(index - prevIndex)));\n                            prevIndex = msgIndex;\n                            prevBytes = byteOffset;\n                        } catch (IOException e) {}\n                    }\n                    return findOffset(r, buf, prevIndex, prevBytes, index, remainingBytes - read);\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<SignedMessage>> getMessagesFrom(long index) {\n        List<SignedMessage> res = new ArrayList<>();\n        return messages.getInputStream(messages.version.get(messages.writer()), network, crypto, x -> {})\n                        .thenCompose(reader -> getChunkByteOffset(index)\n                                .thenCompose(p -> reader.seek(p.left)\n                                        .thenCompose(seeked -> seeked.parseLimitedStream(SignedMessage::fromCbor,\n                                        res::add, p.right, Integer.MAX_VALUE, messages.getSize() - p.left))))\n                        .thenApply(x -> res);\n    }\n\n    @Override\n    public CompletableFuture<List<SignedMessage>> getMessages(long fromIndex, long toIndex) {\n        List<SignedMessage> res = new ArrayList<>();\n        return messages.getInputStream(messages.version.get(messages.writer()), network, crypto, x -> {})\n                        .thenCompose(reader -> getChunkByteOffset(fromIndex)\n                                .thenCompose(p -> reader.seek(p.left)\n                                        .thenCompose(seeked -> seeked.parseLimitedStream(SignedMessage::fromCbor,\n                                                res::add, p.right, (int) (toIndex - fromIndex), messages.getSize() - p.left))))\n                        .thenApply(x -> res);\n    }\n\n    @Override\n    public CompletableFuture<Snapshot> addMessages(Snapshot initialVersion, Committer committer, long msgIndex, List<SignedMessage> msgs) {\n        ByteArrayOutputStream buf = new ByteArrayOutputStream();\n        List<Integer> sizes = new ArrayList<>();\n        for (SignedMessage msg : msgs) {\n            try {\n                byte[] msgData = msg.serialize();\n                buf.write(msgData);\n                sizes.add(msgData.length);\n            } catch (IOException e) {} // can't happen\n        }\n        byte[] raw = buf.toByteArray();\n        return messages.clean(initialVersion, committer, network, crypto)\n                .thenCompose(p -> p.left.overwriteSection(p.right, committer, AsyncReader.build(raw), p.left.getSize(),\n                        p.left.getSize() + raw.length, Optional.empty(), network, crypto, x -> {}).thenCompose(s2 -> {\n                    long size = p.left.getSize();\n                    boolean newChunk = (raw.length + size)/Chunk.MAX_SIZE > size/Chunk.MAX_SIZE;\n                    if (! newChunk)\n                        return Futures.of(s2);\n                    ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                    DataOutputStream dout = new DataOutputStream(bout);\n                    // find message that crossed chunk boundary\n                    int count=0;\n                    int totalSize = 0;\n                    while (count < sizes.size()) {\n                        totalSize += sizes.get(count);\n                        count++;\n                        if ((totalSize + size)/Chunk.MAX_SIZE > size/Chunk.MAX_SIZE)\n                            break;\n                    }\n                    try {\n                        dout.writeLong(msgIndex + count);\n                        dout.writeLong(size + totalSize);\n                    } catch (IOException e) {} // can't happen\n                    byte[] twoLongs = bout.toByteArray();\n                    return indexFile.overwriteSection(s2, committer,\n                                    AsyncReader.build(twoLongs), indexFile.getSize(),\n                            indexFile.getSize() + twoLongs.length, Optional.empty(), network, crypto, x -> {});\n\n                }));\n    }\n\n    @Override\n    public synchronized CompletableFuture<Snapshot> revokeAccess(Set<String> usernames, Snapshot s, Committer c) {\n        return context.unShareReadAccessWith(sharedDir, usernames, s, c);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/GroupProperty.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class GroupProperty implements Cborable {\n\n    public static final String ADMINS_STATE_KEY = \"admins\";\n\n    public final Id author;\n    public final TreeClock updateTimestamp;\n    public final String value;\n\n    public GroupProperty(Id author, TreeClock updateTimestamp, String value) {\n        this.author = author;\n        this.updateTimestamp = updateTimestamp;\n        this.value = value;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"a\", author);\n        result.put(\"t\", updateTimestamp);\n        result.put(\"v\", new CborObject.CborString(value));\n        return CborObject.CborMap.build(result);\n    }\n\n    public static GroupProperty fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        Id author = m.get(\"a\", Id::fromCbor);\n        TreeClock timestamp = m.get(\"t\", TreeClock::fromCbor);\n        String value = m.getString(\"v\");\n\n        return new GroupProperty(author, timestamp, value);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        GroupProperty that = (GroupProperty) o;\n        return Objects.equals(author, that.author) &&\n                Objects.equals(updateTimestamp, that.updateTimestamp) &&\n                Objects.equals(value, that.value);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(author, updateTimestamp, value);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/Id.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.util.ArrayOps;\n\nimport java.util.*;\nimport java.util.stream.*;\n\n/** Ids in a chat form a tree. The creator is the root, and each member is a child node of the member that invited them.\n *  They are fully concurrent - anyone can invite anyone at any time without any synchronization.\n *\n *  In the simple case of a fixed group know at creation time these are the same as the indices in a vector clock.\n */\npublic final class Id implements Comparable<Id>, Cborable {\n\n    public final int[] id;\n\n    public Id(int[] id) {\n        this.id = id;\n    }\n\n    public Id(int counter) {\n        this(new int[]{counter});\n    }\n\n    public static Id creator() {\n        return new Id(0);\n    }\n\n    public Id fork(int counter) {\n        int[] descendant = new int[id.length + 1];\n        System.arraycopy(id, 0, descendant, 0, id.length);\n        descendant[id.length] = counter;\n        return new Id(descendant);\n    }\n\n    public Id parent() {\n        return new Id(ArrayOps.copyOfRange(id, 0, id.length - 1));\n    }\n\n    @Override\n    public int compareTo(Id other) {\n        return compare(id, other.id);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.stream(id)\n                .mapToObj(CborObject.CborLong::new)\n                .collect(Collectors.toList()));\n    }\n\n    public static Id fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        return new Id(((CborObject.CborList) cbor)\n                .map(e -> (int) ((CborObject.CborLong)e).value)\n                .stream()\n                .mapToInt(i -> i)\n                .toArray());\n    }\n\n    @Override\n    public String toString() {\n        return \"[\" + Arrays.stream(id).boxed().map(x -> x.toString()).collect(Collectors.joining(\",\")) + \"]\";\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(id);\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (! (obj instanceof Id))\n            return false;\n        return Arrays.equals(id, ((Id) obj).id);\n    }\n\n    private static int compare(int[] a, int[] b) {\n        if (a == b) {\n            return 0;\n        } else if (a != null && b != null) {\n            int i = mismatch(a, b, Math.min(a.length, b.length));\n            return i >= 0 ? Integer.compare(a[i], b[i]) : a.length - b.length;\n        } else {\n            return a == null ? -1 : 1;\n        }\n    }\n\n    private static int mismatch(int[] a, int[] b, int length) {\n        int i=0;\n        while (i < length) {\n            if (a[i] != b[i]) {\n                return i;\n            }\n\n            ++i;\n        }\n\n        return -1;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/Member.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.*;\n\npublic class Member implements Cborable {\n    public final String username;\n    public final Id id;\n    public final PublicKeyHash identity;\n    public final Optional<OwnerProof> chatIdentity;\n    public final  long messagesMergedUpto;\n    public final int membersInvited;\n    public final boolean removed;\n\n    public Member(String username,\n                  Id id,\n                  PublicKeyHash identity,\n                  Optional<OwnerProof> chatIdentity,\n                  long messagesMergedUpto,\n                  int membersInvited,\n                  boolean removed) {\n        this.username = username;\n        this.id = id;\n        this.identity = identity;\n        this.chatIdentity = chatIdentity;\n        this.messagesMergedUpto = messagesMergedUpto;\n        this.membersInvited = membersInvited;\n        this.removed = removed;\n    }\n\n    public Member(String username, Id id, PublicKeyHash identity, long messagesMergedUpto, int membersInvited) {\n        this(username, id, identity, Optional.empty(), messagesMergedUpto, membersInvited, false);\n    }\n\n    public Member incrementInvited() {\n        return new Member(username, id, identity, chatIdentity, messagesMergedUpto, membersInvited + 1, removed);\n    }\n\n    public Member incrementMessages() {\n        return new Member(username, id, identity, chatIdentity, messagesMergedUpto + 1, membersInvited, removed);\n    }\n\n    public Member removed(boolean updated) {\n        return new Member(username, id, identity, chatIdentity, messagesMergedUpto, membersInvited, updated);\n    }\n\n    public Member withChatId(OwnerProof proof) {\n        return new Member(username, id, identity, Optional.of(proof), messagesMergedUpto, membersInvited, removed);\n    }\n\n    public Member copy() {\n        return new Member(username, id, identity, chatIdentity, messagesMergedUpto, membersInvited, removed);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"u\", new CborObject.CborString(username));\n        result.put(\"i\", id);\n        result.put(\"p\", identity);\n        chatIdentity.ifPresent(c -> result.put(\"s\", c));\n        result.put(\"m\", new CborObject.CborLong(messagesMergedUpto));\n        result.put(\"c\", new CborObject.CborLong(membersInvited));\n        result.put(\"r\", new CborObject.CborBoolean(removed));\n        return CborObject.CborMap.build(result);\n    }\n\n    public static Member fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        String username = m.getString(\"u\");\n        Id id = m.get(\"i\", Id::fromCbor);\n        PublicKeyHash publicIdentity = m.get(\"p\", PublicKeyHash::fromCbor);\n        Optional<OwnerProof> chatIdentity = m.getOptional(\"s\", OwnerProof::fromCbor);\n        long messagesMergedUpTo = m.getLong(\"m\");\n        int membersInvited = (int) m.getLong(\"c\");\n        boolean removed = m.getBoolean(\"r\");\n        return new Member(username, id, publicIdentity, chatIdentity, messagesMergedUpTo, membersInvited, removed);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Member member = (Member) o;\n        return messagesMergedUpto == member.messagesMergedUpto &&\n                membersInvited == member.membersInvited &&\n                removed == member.removed &&\n                Objects.equals(username, member.username) &&\n                Objects.equals(id, member.id) &&\n                Objects.equals(identity, member.identity) &&\n                Objects.equals(chatIdentity, member.chatIdentity);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(username, id, identity, chatIdentity, messagesMergedUpto, membersInvited, removed);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/MessageEnvelope.java",
    "content": "package peergos.shared.messaging;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.messaging.messages.*;\n\nimport java.time.*;\nimport java.util.*;\n\n@JsType\npublic class MessageEnvelope implements Cborable {\n\n    public final Id author;\n    public final TreeClock timestamp;\n    public final LocalDateTime creationTime; // Stored accurate to millisecond\n    // The most recent event(s) the sender had received at send time.\n    // This makes the message envelopes form a merkle DAG.\n    public final List<MessageRef> previousMessages;\n    public final Message payload;\n\n    public MessageEnvelope(Id author,\n                           TreeClock timestamp,\n                           LocalDateTime creationTime,\n                           List<MessageRef> previousMessages,\n                           Message payload) {\n        this.author = author;\n        this.timestamp = timestamp;\n        this.creationTime = creationTime;\n        this.previousMessages = previousMessages;\n        this.payload = payload;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"a\", author);\n        result.put(\"t\", timestamp);\n        result.put(\"u\", new CborObject.CborLong(1000*creationTime.toEpochSecond(ZoneOffset.UTC) + creationTime.getNano()/1_000_000));\n        result.put(\"r\", new CborObject.CborList(previousMessages));\n        result.put(\"p\", payload);\n        return CborObject.CborMap.build(result);\n    }\n\n    public static MessageEnvelope fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        Id author = m.get(\"a\", Id::fromCbor);\n        TreeClock timestamp = m.get(\"t\", TreeClock::fromCbor);\n        LocalDateTime creationTime = m.get(\"u\", c -> parseUtcMillis(((CborObject.CborLong)c).value));\n        List<MessageRef> previousMessages = m.getList(\"r\", MessageRef::fromCbor);\n        Message payload = m.get(\"p\", Message::fromCbor);\n        return new MessageEnvelope(author, timestamp, creationTime, previousMessages, payload);\n    }\n\n    private static LocalDateTime parseUtcMillis(long millis) {\n        return LocalDateTime.ofEpochSecond(millis/1_000, ((int)(millis % 1000)) * 1_000_000, ZoneOffset.UTC);\n    }\n\n    @Override\n    public String toString() {\n        return author + \"(\" + timestamp + \") - \" + payload;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        MessageEnvelope that = (MessageEnvelope) o;\n        return Objects.equals(author, that.author) &&\n                Objects.equals(timestamp, that.timestamp) &&\n                Objects.equals(creationTime, that.creationTime) &&\n                Objects.equals(previousMessages, that.previousMessages) &&\n                Objects.equals(payload, that.payload);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(author, timestamp, creationTime, previousMessages, payload);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/MessageRef.java",
    "content": "package peergos.shared.messaging;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\n@JsType\npublic class MessageRef implements Cborable {\n\n    public final Multihash envelopeHash;\n\n    public MessageRef(Multihash envelopeHash) {\n        this.envelopeHash = envelopeHash;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"h\", new CborObject.CborByteArray(envelopeHash.toBytes()));\n        return CborObject.CborMap.build(result);\n    }\n\n    public static MessageRef fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        Multihash h = Multihash.decode(m.getByteArray(\"h\"));\n        return new MessageRef(h);\n    }\n\n    @Override\n    public String toString() {\n        return envelopeHash.toString();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        MessageRef that = (MessageRef) o;\n        return Objects.equals(envelopeHash, that.envelopeHash);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(envelopeHash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/MessageStore.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.user.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface MessageStore {\n\n    CompletableFuture<List<SignedMessage>> getMessagesFrom(long index);\n\n    CompletableFuture<List<SignedMessage>> getMessages(long fromIndex, long toIndex);\n\n    CompletableFuture<Snapshot> addMessages(Snapshot initialVersion, Committer committer, long msgIndex, List<SignedMessage> msgs);\n\n    CompletableFuture<Snapshot> revokeAccess(Set<String> usernames, Snapshot initialVersion, Committer committer);\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/Messenger.java",
    "content": "package peergos.shared.messaging;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.display.*;\nimport peergos.shared.messaging.messages.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/** All the chats in /$username/.messaging/\n *\n *  Within this, each chat has a directory named with a uid and the following substructure:\n *  $uuid/shared/peergos-chat-messages.cborstream (append only, eventually consistent log of all messages in chat)\n *  $uuid/shared/peergos-chat-state.cbor (our view of the current state of the chat)\n *  $uuid/shared/media/$year/$month/$media-file (media files shared in chat)\n *  $uuid/private-state.cbor  (keypair for chat identity)\n *\n *  To invite a user we add an invite message to our log, and share the shared directory with them.\n *  To join they copy our state and message log, add a join message to their log,\n *  and share their shared directory with us.\n */\npublic class Messenger {\n    public static final String MESSAGING_BASE_DIR = \".messaging\";\n\n    private final UserContext context;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private final Hasher hasher;\n    private final LRUCache<MessageRef, MessageEnvelope> cache = new LRUCache<>(1_000);\n\n    @JsConstructor\n    public Messenger(UserContext context) {\n        this.context = context;\n        this.network = context.network;\n        this.crypto = context.crypto;\n        this.hasher = context.crypto.hasher;\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> createChat() {\n        return initChat(null);\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> createAppChat(String appName){\n        return initChat(appName);\n    }\n\n    private CompletableFuture<ChatController> initChat(String appName) {\n        String prefix = appName != null ? \"chat-\" + appName + \"$\" : \"chat$\";\n        String chatId = prefix + context.username + \"$\" + UUID.randomUUID().toString();\n        Chat chat = Chat.createNew(chatId, context.username, context.signer.publicKeyHash);\n        byte[] rawChat = chat.serialize();\n        PrivateChatState privateChatState = Chat.generateChatIdentity(crypto);\n        byte[] rawPrivateChatState = privateChatState.serialize();\n        return createChatRoot(chatId)\n                .thenCompose(chatRoot -> chatRoot.getOrMkdirs(PathUtil.get(\"shared\"), context.network, false, context.mirrorBatId(), crypto)\n                        .thenCompose(chatSharedDir -> chatRoot.getUpdated(network)\n                                .thenCompose(updatedChatRoot -> chatSharedDir.setProperties(chatSharedDir.getFileProperties(), hasher,\n                                        network, Optional.of(updatedChatRoot)).thenCompose(b -> chatSharedDir.getUpdated(network))))\n                        .thenCompose(chatSharedDir -> chatSharedDir.uploadOrReplaceFile(ChatController.SHARED_MSG_LOG,\n                                AsyncReader.build(new byte[0]), 0, network, crypto, () -> false, x -> {}, crypto.random.randomBytes(32),\n                                Optional.of(Bat.random(crypto.random)), chatSharedDir.mirrorBatId()))\n                        .thenCompose(chatSharedDir -> chatSharedDir.uploadOrReplaceFile(ChatController.SHARED_MSG_LOG_INDEX,\n                                AsyncReader.build(new byte[16]), 16, network, crypto, () -> false, x -> {}, crypto.random.randomBytes(32),\n                                Optional.of(Bat.random(crypto.random)), chatSharedDir.mirrorBatId()))\n                        .thenCompose(chatSharedDir -> chatSharedDir.uploadOrReplaceFile(ChatController.SHARED_CHAT_STATE,\n                                AsyncReader.build(rawChat), rawChat.length, network, crypto, () -> false, x -> {}, crypto.random.randomBytes(32),\n                                Optional.of(Bat.random(crypto.random)), chatSharedDir.mirrorBatId()))\n                        .thenCompose(x -> chatRoot.getUpdated(x.version, network))\n                        .thenCompose(updatedChatRoot -> updatedChatRoot.uploadOrReplaceFile(ChatController.PRIVATE_CHAT_STATE,\n                                AsyncReader.build(rawPrivateChatState), rawPrivateChatState.length, network, crypto, () -> false, x -> {},\n                                crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), updatedChatRoot.mirrorBatId()))\n                        .thenCompose(newRoot -> ChatController.getChatMessageStore(newRoot, context)\n                                .thenApply(messageStore -> new ChatController(chatId, chat, messageStore,\n                                        privateChatState, newRoot, cache, context))))\n                .thenCompose(controller -> controller.join(context.signer))\n                .thenCompose(controller -> controller.addAdmin(context.username));\n    }\n\n    private CompletableFuture<FileWrapper> createChatRoot(String chatId) {\n        return context.getUserRoot()\n                .thenCompose(home -> home.getOrMkdirs(PathUtil.get(MESSAGING_BASE_DIR), context.network, true, context.mirrorBatId(), context.crypto))\n                .thenCompose(chatsRoot -> chatsRoot.mkdir(chatId, context.network, false, chatsRoot.mirrorBatId(), crypto))\n                .thenCompose(updated -> updated.getChild(chatId, hasher, network))\n                .thenApply(Optional::get);\n    }\n    public static Path getChatPath(String hostUsername, String chatId) {\n        return PathUtil.get(hostUsername, MESSAGING_BASE_DIR, chatId);\n    }\n\n    private Path getChatSharedDir(String chatUid) {\n        return PathUtil.get(context.username, MESSAGING_BASE_DIR, chatUid, \"shared\");\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> invite(ChatController chat, List<String> usernames, List<PublicKeyHash> identities) {\n        Path chatSharedDir = getChatSharedDir(chat.chatUuid);\n        return chat.invite(usernames, identities)\n                .thenCompose(res -> context.shareReadAccessWith(chatSharedDir, new HashSet<>(usernames))\n                        .thenApply(x -> res));\n    }\n\n    /** Copy a chat to our space to join it.\n     *\n     * @param sourceChatSharedDir\n     * @return\n     */\n    @JsMethod\n    public CompletableFuture<ChatController> cloneLocallyAndJoin(FileWrapper sourceChatSharedDir) {\n        PrivateChatState privateChatState = Chat.generateChatIdentity(crypto);\n        byte[] rawPrivateChatState = privateChatState.serialize();\n        return sourceChatSharedDir.retrieveParent(network)\n                .thenApply(Optional::get)\n                .thenApply(parent -> parent.getName())\n                .thenCompose(chatId -> createChatRoot(chatId) // This will error if a chat with this chatId already exists\n                        .thenCompose(chatRoot -> chatRoot.getOrMkdirs(PathUtil.get(\"shared\"), network, false, context.mirrorBatId(), crypto)\n                                .thenCompose(shared -> ChatController.getChatState(sourceChatSharedDir, network, crypto)\n                                        .thenCompose(mirrorState -> {\n                                            Chat ourVersion = mirrorState.copy(new Member(context.username,\n                                                    mirrorState.getMember(context.username).id,\n                                                    context.signer.publicKeyHash, Optional.empty(),\n                                                    mirrorState.host().messagesMergedUpto, 0, false));\n\n                                            byte[] rawChat = ourVersion.serialize();\n                                            return shared.uploadOrReplaceFile(ChatController.SHARED_CHAT_STATE, AsyncReader.build(rawChat),\n                                                    rawChat.length, network, crypto, () -> false, x -> {}, crypto.random.randomBytes(32),\n                                                    Optional.of(Bat.random(crypto.random)), shared.mirrorBatId());\n                                        })\n                                        .thenCompose(b -> sourceChatSharedDir.getChild(ChatController.SHARED_MSG_LOG, hasher, network))\n                                        .thenCompose(msgs -> shared.getUpdated(network)\n                                                .thenCompose(updatedShared -> msgs.get().copyTo(updatedShared, context)))\n                                        .thenCompose(b -> sourceChatSharedDir.getChild(ChatController.SHARED_MSG_LOG_INDEX, hasher, network))\n                                        .thenCompose(msgsIndex -> shared.getUpdated(network)\n                                                .thenCompose(updatedShared -> msgsIndex.get().copyTo(updatedShared, context)))\n                                        .thenCompose(x -> chatRoot.uploadOrReplaceFile(ChatController.PRIVATE_CHAT_STATE,\n                                                AsyncReader.build(rawPrivateChatState), rawPrivateChatState.length, network,\n                                                crypto, () -> false, y -> {}, crypto.random.randomBytes(32),\n                                                Optional.of(Bat.random(crypto.random)), chatRoot.mirrorBatId()))\n                                )).thenCompose(b -> context.shareReadAccessWith(\n                                getChatPath(context.username, chatId).resolve(\"shared\"),\n                                Collections.singleton(sourceChatSharedDir.getOwnerName())))\n                        .thenCompose(b -> getChat(chatId))\n                        .thenCompose(controller -> controller.join(context.signer)));\n    }\n\n    private CompletableFuture<ChatController> updatePrivateState(PrivateChatState state, ChatController current) {\n        Path chatPath = getChatPath(context.username, current.chatUuid);\n        byte[] rawPrivateChatState = state.serialize();\n        return context.getByPath(chatPath)\n                .thenCompose(dopt -> dopt.get().uploadOrReplaceFile(ChatController.PRIVATE_CHAT_STATE,\n                        AsyncReader.build(rawPrivateChatState), rawPrivateChatState.length, network,\n                        crypto, () -> false, y -> {}, crypto.random.randomBytes(32),\n                        Optional.of(Bat.random(crypto.random)), dopt.get().mirrorBatId()))\n                .thenApply(f -> current.with(state));\n    }\n\n    public boolean allOtherMembersRemoved(ChatController current) {\n        Set<String> memberNames = current.getMemberNames();\n        return memberNames.contains(context.username) && memberNames.size() == 1;\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> mergeMessages(ChatController current, String mirrorUsername) {\n        if (mirrorUsername.equals(this.context.username) ||\n                (current.deletedMemberNames().contains(mirrorUsername) && ! allOtherMembersRemoved(current))) {\n            return Futures.of(current);\n        }\n        return Futures.asyncExceptionally(\n                () -> getMessageStoreMirror(mirrorUsername, current.chatUuid)\n                        .thenCompose(mirrorStore -> current.mergeMessages(mirrorUsername, mirrorStore)),\n                t -> {\n                    //if (t.getCause() instanceof NoSuchElementException) not GWT compatible\n                    if (! current.getPendingMemberNames().contains(mirrorUsername) && t.toString().indexOf(\"java.util.NoSuchElementException\") > -1) {\n                        // member server is online, but chat mirror is not accessible\n                        // This either means we have been removed, or they deleted their mirror\n                        // We add them to the deleted users list to stop polling them, or remove them is we are an admin\n                        if (current.isAdmin()) {\n                            return removeMember(current, mirrorUsername);\n                        } else {\n                            if (current.deletedMemberNames().contains(mirrorUsername))\n                                return Futures.of(current);\n                            PrivateChatState updatedPrivate = current.privateChatState.addDeleted(mirrorUsername);\n                            return updatePrivateState(updatedPrivate, current);\n                        }\n                    }\n                    return Futures.of(current);\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> mergeAllUpdates(ChatController current, SocialState soc) {\n        Set<String> following = soc.getFollowing();\n        List<String> toPullFrom = current.getMemberNames().stream()\n                .filter(following::contains)\n                .collect(Collectors.toList());\n        Set<String> pendingMembers = current.getPendingMemberNames().stream()\n                .filter(following::contains)\n                .collect(Collectors.toSet());\n        return Futures.reduceAll(toPullFrom, current,\n                (c, n) -> Futures.asyncExceptionally(\n                        () -> mergeMessages(c, n),\n                        t -> pendingMembers.contains(n) ? Futures.of(c) : Futures.errored(t)),\n                (a, b) -> {throw new IllegalStateException();});\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> removeMember(ChatController current, String username) {\n        Member member = current.getMember(username);\n        if (member == null)\n            throw new IllegalStateException(\"No member in chat with that name!\");\n        RemoveMember msg = new RemoveMember(current.chatUuid, member.id);\n        if (! username.equals(context.username) && ! current.getAdmins().contains(context.username))\n            throw new IllegalStateException(\"Only admins can remove other members!\");\n        return sendMessage(current, msg);\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> sendMessage(ChatController current, Message message) {\n        return current.sendMessage(message);\n    }\n\n    @JsMethod\n    public CompletableFuture<Pair<String, FileRef>> uploadMedia(ChatController current,\n                                                                AsyncReader media,\n                                                                String fileExtension,\n                                                                int length,\n                                                                LocalDateTime postTime,\n                                                                ProgressConsumer<Long> monitor) {\n        String uuid = UUID.randomUUID().toString() + \".\" + fileExtension;\n        return getOrMkdirToStoreMedia(current, postTime)\n                .thenCompose(p -> p.right.uploadAndReturnFile(uuid, media, length, false, () -> false, monitor,\n                        p.right.mirrorBatId(), network, crypto)\n                        .thenCompose(f ->  media.reset().thenCompose(r -> crypto.hasher.hashFromStream(r, length))\n                                .thenApply(hash -> new Pair<>(f.getFileProperties().getType(),\n                                        new FileRef(p.left.resolve(uuid).toString(), f.readOnlyPointer(), hash)))));\n    }\n\n    private Path getChatMediaDir(ChatController current) {\n        return PathUtil.get(MESSAGING_BASE_DIR,\n                current.chatUuid,\n                \"shared\",\n                \"media\");\n    }\n\n    private CompletableFuture<Pair<Path, FileWrapper>> getOrMkdirToStoreMedia(ChatController current,\n                                                                              LocalDateTime postTime) {\n        Path dirFromHome = getChatMediaDir(current).resolve(PathUtil.get(\n                Integer.toString(postTime.getYear()),\n                Integer.toString(postTime.getMonthValue())));\n        return context.getUserRoot()\n                .thenCompose(home -> home.getOrMkdirs(dirFromHome, network, true, context.mirrorBatId(), crypto)\n                .thenApply(dir -> new Pair<>(PathUtil.get(\"/\" + context.username).resolve(dirFromHome), dir)));\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> setGroupProperty(ChatController current, String key, String value) {\n        return current.sendMessage(new SetGroupState(key, value));\n    }\n\n    @JsMethod\n    public CompletableFuture<ChatController> getChat(String uuid) {\n        return context.getByPath(getChatPath(context.username, uuid))\n                .thenApply(Optional::get)\n                .thenCompose(d -> ChatController.getChatController(d, context, cache));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> deleteChat(ChatController chat) {\n        Path chatPath = getChatPath(context.username, chat.chatUuid);\n        Path parentPath = chatPath.getParent();\n        return context.getByPath(parentPath)\n                .thenCompose(popt -> popt.get().getChild(chatPath.getFileName().toString(), crypto.hasher, network)\n                        .thenCompose(copt -> copt.get().remove(popt.get(), chatPath, context)))\n                .thenApply(x -> true);\n    }\n\n    @JsMethod\n    public CompletableFuture<Set<ChatController>> listChats() {\n        return context.getUserRoot()\n                .thenCompose(home -> home.getOrMkdirs(PathUtil.get(MESSAGING_BASE_DIR), network, true, context.mirrorBatId(), crypto))\n                .thenCompose(chatsRoot -> chatsRoot.getChildren(hasher, network))\n                .thenCompose(chatDirs -> Futures.combineAll(chatDirs.stream()\n                        .map(d -> ChatController.getChatController(d, context, cache)\n                                .thenApply(Optional::of)\n                                .exceptionally(t -> Optional.empty()))\n                        .collect(Collectors.toList()))\n                        .thenApply(res -> res.stream()\n                                .flatMap(Optional::stream)\n                                .collect(Collectors.toSet())));\n    }\n\n    private CompletableFuture<MessageStore> getMessageStoreMirror(String username, String uuid) {\n        return context.getByPath(getChatPath(username, uuid))\n                .thenApply(Optional::get)\n                .thenCompose(d -> ChatController.getChatMessageStore(d, context));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/PrivateChatState.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class PrivateChatState implements Cborable {\n\n    public final SigningPrivateKeyAndPublicHash chatIdentity;\n    public final PublicSigningKey chatIdPublic;\n    public final Set<String> deletedMembers;\n\n    public PrivateChatState(SigningPrivateKeyAndPublicHash chatIdentity,\n                            PublicSigningKey chatIdPublic,\n                            Set<String> deletedMembers) {\n        this.chatIdentity = chatIdentity;\n        this.chatIdPublic = chatIdPublic;\n        this.deletedMembers = deletedMembers;\n    }\n\n    public PrivateChatState addDeleted(String username) {\n        HashSet<String> newDeleted = new HashSet<>(deletedMembers);\n        newDeleted.add(username);\n        return new PrivateChatState(chatIdentity, chatIdPublic, newDeleted);\n    }\n\n    public PrivateChatState apply(PrivateChatState newer) {\n        HashSet<String> newDeleted = new HashSet<>(deletedMembers);\n        newDeleted.addAll(newer.deletedMembers);\n        return new PrivateChatState(newer.chatIdentity, newer.chatIdPublic, newDeleted);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"ci\", chatIdentity);\n        result.put(\"p\", chatIdPublic);\n        List<CborObject.CborString> deleted = deletedMembers.stream()\n                .sorted()\n                .map(CborObject.CborString::new)\n                .collect(Collectors.toList());\n        result.put(\"d\", new CborObject.CborList(deleted));\n        return CborObject.CborMap.build(result);\n    }\n\n    public static PrivateChatState fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        SigningPrivateKeyAndPublicHash chatIdentity = m.get(\"ci\", SigningPrivateKeyAndPublicHash::fromCbor);\n        PublicSigningKey chatIdPublic = m.get(\"p\", PublicSigningKey::fromCbor);\n        List<String> deleted = m.getList(\"d\", CborObject.CborString::getString);\n        return new PrivateChatState(chatIdentity, chatIdPublic, new HashSet<>(deleted));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/SignedMessage.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic final class SignedMessage implements Cborable {\n    public final byte[] signature;\n    public final MessageEnvelope msg;\n\n    public SignedMessage(byte[] signature, MessageEnvelope msg) {\n        this.signature = signature;\n        this.msg = msg;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(new CborObject.CborByteArray(signature), new CborObject.CborByteArray(msg.serialize())));\n    }\n\n    public static SignedMessage fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborList list = (CborObject.CborList) cbor;\n        byte[] signature = list.get(0, c -> ((CborObject.CborByteArray) c).value);\n        MessageEnvelope msg = MessageEnvelope.fromCbor(CborObject.fromByteArray(list.get(1, c -> ((CborObject.CborByteArray) c).value)));\n        return new SignedMessage(signature, msg);\n    }\n\n    @Override\n    public String toString() {\n        return msg.toString();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        SignedMessage that = (SignedMessage) o;\n        return Arrays.equals(signature, that.signature) && Objects.equals(msg, that.msg);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Objects.hash(msg);\n        result = 31 * result + Arrays.hashCode(signature);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/TreeClock.java",
    "content": "package peergos.shared.messaging;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\n/** A generalization of a vector clock that allows changing group membership\n *\n */\npublic class TreeClock implements Cborable {\n\n    public final SortedMap<Id, Long> time;\n\n    public TreeClock(SortedMap<Id, Long> time) {\n        this.time = time;\n    }\n\n    public TreeClock merge(TreeClock other) {\n        TreeMap<Id, Long> res = new TreeMap<>(time);\n        for (Map.Entry<Id, Long> e : other.time.entrySet()) {\n            if (! res.containsKey(e.getKey()) || res.get(e.getKey()) < e.getValue())\n                res.put(e.getKey(), e.getValue());\n        }\n        return new TreeClock(res);\n    }\n\n    public boolean isBeforeOrEqual(TreeClock b) {\n        for (Map.Entry<Id, Long> e : time.entrySet()) {\n            if (!b.hasId(e.getKey()) || e.getValue() > b.getEventCounter(e.getKey()))\n                return false;\n        }\n        return true;\n    }\n\n    public boolean hasGreaterCounterThan(TreeClock b) {\n        for (Map.Entry<Id, Long> e : time.entrySet()) {\n            if (b.hasId(e.getKey()) && e.getValue() > b.getEventCounter(e.getKey()))\n                return true;\n        }\n        return false;\n    }\n\n    public boolean isConcurrentWith(TreeClock b) {\n        return hasGreaterCounterThan(b) && b.hasGreaterCounterThan(this);\n    }\n\n    public TreeClock removeMember(Id remover, Id toRemove) {\n        TreeMap<Id, Long> res = new TreeMap<>(time);\n        res.remove(toRemove);\n        res.put(remover, res.get(remover) + 1);\n        return new TreeClock(res);\n    }\n\n    public Set<Id> newMembersFrom(TreeClock other) {\n        HashSet<Id> ids = new HashSet<>(other.time.keySet());\n        ids.removeAll(time.keySet());\n        return ids;\n    }\n\n    public TreeClock withMember(Id member) {\n        TreeMap<Id, Long> res = new TreeMap<>(time);\n        res.put(member, 0L);\n        return new TreeClock(res);\n    }\n\n    public boolean hasId(Id member) {\n        return time.containsKey(member);\n    }\n\n    public long getEventCounter(Id member) {\n        Long res = time.get(member);\n        if (res == null)\n            throw new IllegalStateException(\"Id not present in clock!\");\n        return res;\n    }\n\n    public boolean isIncrementOf(TreeClock parent) {\n        // We can add or remove a single member per event\n        if (Math.abs(parent.time.size() - time.size()) > 1)\n            return false;\n        HashSet<Id> common = new HashSet<>(time.keySet());\n        common.retainAll(parent.time.keySet());\n        if (Math.abs(common.size() - time.size()) > 1 || Math.abs(common.size() - parent.time.size()) > 1)\n            return false;\n        // apart from an added or removed member, exactly 1 counter should increment\n        List<Id> changed = common.stream()\n                .filter(k -> ! time.get(k).equals(parent.time.get(k)))\n                .collect(Collectors.toList());\n        if (changed.size() != 1)\n            return false;\n        Id changeAuthor = changed.get(0);\n        if (! time.get(changeAuthor).equals(1 + parent.time.get(changeAuthor)))\n            return false;\n        HashSet<Id> added = new HashSet<>(time.keySet());\n        added.removeAll(parent.time.keySet());\n        if (! added.isEmpty() && ! added.stream().allMatch(id -> time.get(id).equals(0L)))\n            return false;\n        return true;\n    }\n\n    public TreeClock increment(Id member) {\n        Long counter = time.get(member);\n        TreeMap<Id, Long> res = new TreeMap<>(time);\n        res.put(member, counter + 1);\n        return new TreeClock(res);\n    }\n\n    public static TreeClock init(List<Id> members) {\n        TreeMap<Id, Long> res = new TreeMap<>();\n        for (Id member : members) {\n            res.put(member, 0L);\n        }\n        return new TreeClock(res);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        List<List<Long>> res = new ArrayList<>();\n        for (Map.Entry<Id, Long> e : time.entrySet()) {\n            List<Long> mapping = Stream.concat(Arrays.stream(e.getKey().id).mapToObj(i -> (long) i), Stream.of(e.getValue()))\n                    .collect(Collectors.toList());\n            res.add(mapping);\n        }\n        return CborObject.CborList.build(res, m -> CborObject.CborList.build(m, CborObject.CborLong::new));\n    }\n\n    public static TreeClock fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        SortedMap<Id, Long> mappings = new TreeMap<>();\n        ((CborObject.CborList) cbor)\n                .map(m -> ((CborObject.CborList)m).map(i -> ((CborObject.CborLong)i).value))\n                .forEach(m -> mappings.put(\n                        new Id(m.subList(0, m.size() - 1).stream().mapToInt(Long::intValue).toArray()),\n                        m.get(m.size() - 1)));\n        return new TreeClock(mappings);\n    }\n\n    @Override\n    public String toString() {\n        return time.entrySet().stream()\n                .map(p -> p.getKey() + \":\" + p.getValue())\n                .collect(Collectors.joining(\",\"));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        TreeClock treeClock = (TreeClock) o;\n        return Objects.equals(time, treeClock.time);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(time);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/ApplicationMessage.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\nimport peergos.shared.display.*;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@JsType\npublic class ApplicationMessage implements Message {\n    public final List<? extends Content> body;\n\n    public ApplicationMessage(List<? extends Content> body) {\n        this.body = body;\n    }\n\n    @Override\n    public String toString() {\n        return body.stream().map(Object::toString).collect(Collectors.joining());\n    }\n\n    @Override\n    public Type type() {\n        return Type.Application;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"b\", new CborObject.CborList(body));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ApplicationMessage fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        List<Content> body = m.getList(\"b\", Content::fromCbor);\n        return new ApplicationMessage(body);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        ApplicationMessage that = (ApplicationMessage) o;\n        return Objects.equals(body, that.body);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(body);\n    }\n\n    public static ApplicationMessage text(String text) {\n        return new ApplicationMessage(Collections.singletonList(new Text(text)));\n    }\n\n    public static ApplicationMessage attachment(String text, List<FileRef> attachments) {\n        List<Reference> attachmentList = attachments.stream().map(a -> new Reference(a)).collect(Collectors.toList());\n        List<Content> body = new ArrayList<>();\n        body.add(new Text(text));\n        body.addAll(attachmentList);\n        return new ApplicationMessage(body);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/DeleteMessage.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.messaging.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** A message to delete a prior message of ours.\n *\n */\n@JsType\npublic class DeleteMessage implements Message {\n\n    public final MessageRef target;\n\n    public DeleteMessage(MessageRef target) {\n        this.target = target;\n    }\n\n    @Override\n    public Type type() {\n        return Type.Delete;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"r\", target);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static DeleteMessage fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        MessageRef target = m.get(\"r\", MessageRef::fromCbor);\n        return new DeleteMessage(target);\n    }\n\n    public static CompletableFuture<DeleteMessage> build(MessageEnvelope target, Hasher hasher) {\n        return hasher.bareHash(target.serialize())\n                .thenApply(h -> new DeleteMessage(new MessageRef(h)));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        DeleteMessage that = (DeleteMessage) o;\n        return Objects.equals(target, that.target);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(target);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/EditMessage.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.messaging.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** A message to edit an earlier message.\n *\n */\n@JsType\npublic class EditMessage implements Message {\n\n    public final MessageRef priorVersion;\n    public final ApplicationMessage content;\n\n    public EditMessage(MessageRef priorVersion, ApplicationMessage content) {\n        this.priorVersion = priorVersion;\n        this.content = content;\n    }\n\n    @Override\n    public Type type() {\n        return Type.Edit;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"r\", priorVersion);\n        state.put(\"b\", content);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static EditMessage fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        MessageRef parent = m.get(\"r\", MessageRef::fromCbor);\n        ApplicationMessage content = m.get(\"b\", ApplicationMessage::fromCbor);\n        return new EditMessage(parent, content);\n    }\n\n    public static CompletableFuture<EditMessage> build(MessageEnvelope prior, ApplicationMessage content, Hasher hasher) {\n        return hasher.bareHash(prior.serialize())\n                .thenApply(h -> new EditMessage(new MessageRef(h), content));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        EditMessage replyTo = (EditMessage) o;\n        return Objects.equals(priorVersion, replyTo.priorVersion) &&\n                Objects.equals(content, replyTo.content);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(priorVersion, content);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/Invite.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.messaging.*;\n\nimport java.util.*;\n\npublic class Invite implements Message {\n    public final String username;\n    public final PublicKeyHash identity;\n    public final Id recipientId;\n\n    public Invite(String username, PublicKeyHash identity, Id recipientId) {\n        this.username = username;\n        this.identity = identity;\n        this.recipientId = recipientId;\n    }\n\n    @Override\n    public Type type() {\n        return Type.Invite;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"u\", new CborObject.CborString(username));\n        state.put(\"r\", recipientId);\n        state.put(\"i\", identity);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static Invite fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        String username = m.getString(\"u\");\n        Id recipientId = m.get(\"r\", Id::fromCbor);\n        PublicKeyHash identity = m.get(\"i\", PublicKeyHash::fromCbor);\n        return new Invite(username, identity, recipientId);\n    }\n\n    @Override\n    public String toString() {\n        return \"INVITE(\" + username + \")\";\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Invite invite = (Invite) o;\n        return Objects.equals(username, invite.username) &&\n                Objects.equals(identity, invite.identity) &&\n                Objects.equals(recipientId, invite.recipientId);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(username, identity, recipientId);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/Join.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.messaging.*;\n\nimport java.util.*;\n\npublic class Join implements Message {\n    public final String username;\n    public final PublicKeyHash identity;\n    public final OwnerProof chatIdentity;\n    public final PublicSigningKey chatIdPublic;\n\n    public Join(String username, PublicKeyHash identity, OwnerProof chatIdentity, PublicSigningKey chatIdPublic) {\n        this.username = username;\n        this.identity = identity;\n        this.chatIdentity = chatIdentity;\n        this.chatIdPublic = chatIdPublic;\n    }\n\n    @Override\n    public Type type() {\n        return Type.Join;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"u\", new CborObject.CborString(username));\n        state.put(\"i\", identity);\n        state.put(\"ci\", chatIdentity);\n        state.put(\"p\", chatIdPublic);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static Join fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        String username = m.getString(\"u\");\n        PublicKeyHash identity = m.get(\"i\", PublicKeyHash::fromCbor);\n        OwnerProof chatIdentity = m.get(\"ci\", OwnerProof::fromCbor);\n        PublicSigningKey chatIdPublic = m.get(\"p\", PublicSigningKey::fromCbor);\n        return new Join(username, identity, chatIdentity, chatIdPublic);\n    }\n\n    @Override\n    public String toString() {\n        return \"JOIN(\" + username + \")\";\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Join join = (Join) o;\n        return Objects.equals(username, join.username) &&\n                Objects.equals(identity, join.identity) &&\n                Objects.equals(chatIdentity, join.chatIdentity) &&\n                Objects.equals(chatIdPublic, join.chatIdPublic);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(username, identity, chatIdentity, chatIdPublic);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/Message.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n@JsType\npublic interface Message extends Cborable {\n\n    Type type();\n\n    static Message fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor: \" + cbor);\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n\n        Type category = map.get(\"c\", c -> Type.byValue((int) ((CborObject.CborLong) c).value));\n        switch (category) {\n            case Join: return Join.fromCbor(cbor);\n            case Invite: return Invite.fromCbor(cbor);\n            case Application: return ApplicationMessage.fromCbor(cbor);\n            case GroupState: return SetGroupState.fromCbor(cbor);\n            case ReplyTo: return ReplyTo.fromCbor(cbor);\n            case Edit: return EditMessage.fromCbor(cbor);\n            case Delete: return DeleteMessage.fromCbor(cbor);\n            case RemoveMember: return RemoveMember.fromCbor(cbor);\n            default: throw new IllegalStateException(\"Invalid message type!\");\n        }\n    }\n\n    Map<Integer, Type> byValue = new HashMap<>();\n    @JsType\n    enum Type {\n        Join(0),\n        Invite(1),\n        Application(2),\n        GroupState(3),\n        ReplyTo(4),\n        Delete(5),\n        Edit(6),\n        RemoveMember(7);\n\n        public final int value;\n\n        Type(int value) {\n            this.value = value;\n            byValue.put(value, this);\n        }\n\n        public static Type byValue(int val) {\n            if (!byValue.containsKey(val))\n                throw new IllegalStateException(\"Unknown message type: \" + val);\n            return byValue.get(val);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/RemoveMember.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.messaging.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** A message to remove a member from a chat. Anyone can remove themselves, or an admin can remove anyone.\n *\n */\n@JsType\npublic class RemoveMember implements Message {\n\n    public final String chatUid;\n    public final Id memberToRemove;\n\n    public RemoveMember(String chatUid, Id memberToRemove) {\n        this.chatUid = chatUid;\n        this.memberToRemove = memberToRemove;\n    }\n\n    @Override\n    public Type type() {\n        return Type.RemoveMember;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"u\", new CborObject.CborString(chatUid));\n        state.put(\"m\", memberToRemove);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static RemoveMember fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        String chatUid = m.getString(\"u\");\n        Id member = m.get(\"m\", Id::fromCbor);\n        return new RemoveMember(chatUid, member);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        RemoveMember that = (RemoveMember) o;\n        return Objects.equals(chatUid, that.chatUid) && Objects.equals(memberToRemove, that.memberToRemove);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(chatUid, memberToRemove);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/ReplyTo.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.messaging.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** A message to reply to another message.\n *\n */\n@JsType\npublic class ReplyTo implements Message {\n\n    public final MessageRef parent;\n    public final ApplicationMessage content;\n\n    public ReplyTo(MessageRef parent, ApplicationMessage content) {\n        this.parent = parent;\n        this.content = content;\n    }\n\n    @Override\n    public Type type() {\n        return Type.ReplyTo;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"r\", parent);\n        state.put(\"b\", content);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ReplyTo fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        MessageRef parent = m.get(\"r\", MessageRef::fromCbor);\n        ApplicationMessage content = m.get(\"b\", ApplicationMessage::fromCbor);\n        return new ReplyTo(parent, content);\n    }\n\n    public static CompletableFuture<ReplyTo> build(MessageEnvelope parent, ApplicationMessage content, Hasher hasher) {\n        return hasher.bareHash(parent.serialize())\n                .thenApply(h -> new ReplyTo(new MessageRef(h), content));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        ReplyTo replyTo = (ReplyTo) o;\n        return Objects.equals(parent, replyTo.parent) &&\n                Objects.equals(content, replyTo.content);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(parent, content);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/messaging/messages/SetGroupState.java",
    "content": "package peergos.shared.messaging.messages;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\n/** A message to set a key value pair in the shared group state.\n *\n * Concurrent sets are tie-broken by Id thus forming a CRDT.\n *\n */\n@JsType\npublic class SetGroupState implements Message {\n\n    public final String key, value;\n\n    public SetGroupState(String key, String value) {\n        this.key = key;\n        this.value = value;\n    }\n\n    @Override\n    public Type type() {\n        return Type.GroupState;\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", new CborObject.CborLong(type().value));\n        state.put(\"k\", new CborObject.CborString(key));\n        state.put(\"v\", new CborObject.CborString(value));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static SetGroupState fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        String key = m.get(\"k\", c -> ((CborObject.CborString)c).value);\n        String value = m.get(\"v\", c -> ((CborObject.CborString)c).value);\n        return new SetGroupState(key, value);\n    }\n\n    @Override\n    public String toString() {\n        return \"SET-GROUP-STATE(\" + key + \", \" + value + \")\";\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        SetGroupState that = (SetGroupState) o;\n        return Objects.equals(key, that.key) && Objects.equals(value, that.value);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(key, value);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/BufferedPointers.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class BufferedPointers implements MutablePointers {\n\n    public static class WriterUpdate {\n        public final PublicKeyHash writer;\n        public final MaybeMultihash prevHash;\n        public final MaybeMultihash currentHash;\n        public final Optional<Long> currentSequence;\n\n        public WriterUpdate(PublicKeyHash writer, MaybeMultihash prevHash, MaybeMultihash currentHash, Optional<Long> currentSequence) {\n            this.writer = writer;\n            this.prevHash = prevHash;\n            this.currentHash = currentHash;\n            this.currentSequence = currentSequence;\n        }\n\n        @Override\n        public String toString() {\n            return writer + \":\" + prevHash + \" => \" + currentSequence + currentHash;\n        }\n    }\n\n    private final MutablePointers target;\n    private final Map<PublicKeyHash, SigningPrivateKeyAndPublicHash> writers = new HashMap<>();\n    private final Map<PublicKeyHash, WriterUpdate> latest = new HashMap<>();\n    private final Map<PublicKeyHash, WriterUpdate> lastCommits = new LRUCache<>(20);\n    private final List<WriterUpdate> writerUpdates = new ArrayList<>();\n    private final Set<MaybeMultihash> mergedTargets = new HashSet();\n\n    public BufferedPointers(MutablePointers target) {\n        if (target instanceof BufferedPointers)\n            throw new IllegalStateException(\"Nested BufferedPointers!\");\n        this.target = target;\n    }\n\n    public boolean isBufferedWrite(PublicKeyHash writer, MaybeMultihash target) {\n        WriterUpdate latestWrite = latest.get(writer);\n        if (latestWrite == null)\n            return false;\n        if (mergedTargets.contains(target))\n            return true;\n        return latestWrite.currentHash.equals(target);\n    }\n\n    public Optional<Pair<Optional<Cid>, Optional<Long>>> getCommittedPointerTarget(PublicKeyHash writer) {\n        synchronized (writerUpdates) {\n            if (writerUpdates.isEmpty())\n                return Optional.ofNullable(lastCommits.get(writer))\n                        .map(w -> new Pair<>(w.currentHash.toOptional().map(h -> (Cid) h),\n                                w.currentSequence));\n            return writerUpdates.stream()\n                    .filter(u -> u.writer.equals(writer))\n                    .findFirst()\n                    .map(m -> new Pair<>(m.prevHash.toOptional().map(h -> (Cid) h),\n                            m.currentSequence.map(x -> x - 1)));\n        }\n    }\n\n    public List<WriterUpdate> getUpdates() {\n        synchronized (writerUpdates) {\n            return writerUpdates.subList(0, writerUpdates.size());\n        }\n    }\n\n    public Map<PublicKeyHash, SigningPrivateKeyAndPublicHash> getSigners() {\n        return new HashMap<>(writers);\n    }\n\n    public PointerUpdate addWrite(SigningPrivateKeyAndPublicHash w,\n                                  MaybeMultihash newHash,\n                                  MaybeMultihash prevHash,\n                                  Optional<Long> prevSequence) {\n        synchronized (writerUpdates) {\n            if (Objects.equals(prevHash, newHash) &&\n                    (writerUpdates.isEmpty() || !writerUpdates.get(writerUpdates.size() - 1).currentHash.equals(newHash)))\n                throw new IllegalStateException(\"Noop pointer update!\");\n            PublicKeyHash writer = w.publicKeyHash;\n            writers.put(writer, w);\n            if (writerUpdates.isEmpty()) {\n                writerUpdates.add(new WriterUpdate(writer, prevHash, newHash, PointerUpdate.increment(prevSequence)));\n            } else {\n                WriterUpdate last = writerUpdates.get(writerUpdates.size() - 1);\n                if (last.writer.equals(writer)) {\n                    mergedTargets.add(last.currentHash);\n                    writerUpdates.set(writerUpdates.size() - 1, new WriterUpdate(writer, last.prevHash, newHash, last.currentSequence));\n                } else {\n                    writerUpdates.add(new WriterUpdate(writer, prevHash, newHash, PointerUpdate.increment(prevSequence)));\n                }\n            }\n            WriterUpdate last = writerUpdates.get(writerUpdates.size() - 1);\n            latest.put(w.publicKeyHash, last);\n            return new PointerUpdate(last.prevHash, last.currentHash, last.currentSequence);\n        }\n    }\n\n    public boolean isEmpty() {\n        synchronized (writerUpdates) {\n            return writerUpdates.isEmpty();\n        }\n    }\n\n    @Override\n    public CompletableFuture<PointerUpdate> getPointerTarget(PublicKeyHash owner, PublicKeyHash writer, ContentAddressedStorage ipfs) {\n        WriterUpdate cached = latest.get(writer);\n        if (cached != null) {\n            return Futures.of(new PointerUpdate(cached.prevHash, cached.currentHash, cached.currentSequence));\n        }\n        return target.getPointerTarget(owner, writer, ipfs);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        throw new IllegalStateException(\"Shouldn't get here!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, SigningPrivateKeyAndPublicHash writer, PointerUpdate casUpdate) {\n        addWrite(writer, casUpdate.updated, casUpdate.original, casUpdate.sequence);\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        throw new IllegalStateException(\"Shouldn't get here!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        throw new IllegalStateException(\"Shouldn't get here!\");\n    }\n\n    public List<Cid> getRoots() {\n        synchronized (writerUpdates) {\n            return writerUpdates.stream()\n                    .flatMap(u -> u.currentHash.toOptional().stream())\n                    .map(c -> (Cid) c)\n                    .collect(Collectors.toList());\n        }\n    }\n\n    public CompletableFuture<Boolean> commit(PublicKeyHash owner,\n                                             SigningPrivateKeyAndPublicHash signer,\n                                             PointerUpdate casUpdate) {\n        return signer.secret.signMessage(casUpdate.serialize())\n                .thenCompose(signed -> target.setPointer(owner, signer.publicKeyHash, signed))\n                .thenApply(b -> {\n                    if (b)\n                        lastCommits.put(signer.publicKeyHash, new WriterUpdate(signer.publicKeyHash, casUpdate.original, casUpdate.updated, casUpdate.sequence));\n                    return b;\n                });\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return new BufferedPointers(target.clearCache());\n    }\n\n    public void recordCommitted(List<WriterUpdate> updates) {\n        updates.forEach(u -> lastCommits.put(u.writer, u));\n    }\n\n    public void clear() {\n        writers.clear();\n        mergedTargets.clear();\n        synchronized (writerUpdates) {\n            writerUpdates.clear();\n        }\n        latest.clear();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/CachingPointers.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class CachingPointers implements MutablePointers {\n\n    private final MutablePointers target;\n    private final int cacheTTL;\n    private final Map<PublicKeyHash, Pair<Optional<byte[]>, Long>> cache = new HashMap<>();\n    private final Map<PublicKeyHash, Pair<PointerUpdate, Long>> targetCache = new HashMap<>();\n\n    public CachingPointers(MutablePointers target, int cacheTTL) {\n        this.target = target;\n        this.cacheTTL = cacheTTL;\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        synchronized (cache) {\n            Pair<Optional<byte[]>, Long> cached = cache.get(writer);\n            if (cached != null && System.currentTimeMillis() - cached.right < cacheTTL)\n                return CompletableFuture.completedFuture(cached.left);\n        }\n        return target.getPointer(owner, writer).thenApply(m -> {\n            synchronized (cache) {\n                cache.put(writer, new Pair<>(m, System.currentTimeMillis()));\n            }\n            return m;\n        });\n    }\n\n    @Override\n    public CompletableFuture<PointerUpdate> getPointerTarget(PublicKeyHash owner, PublicKeyHash writer, ContentAddressedStorage ipfs) {\n        synchronized (targetCache) {\n            Pair<PointerUpdate, Long> cached = targetCache.get(writer);\n            if (cached != null && System.currentTimeMillis() - cached.right < cacheTTL)\n                return CompletableFuture.completedFuture(cached.left);\n        }\n        return getPointer(owner, writer)\n                .thenCompose(current -> current.isPresent() ?\n                        MutablePointers.parsePointerTarget(current.get(), owner, writer, ipfs) :\n                        Futures.of(PointerUpdate.empty())).thenApply(m -> {\n                    synchronized (targetCache) {\n                        targetCache.put(writer, new Pair<>(m, System.currentTimeMillis()));\n                    }\n                    return m;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, SigningPrivateKeyAndPublicHash writer, PointerUpdate casUpdate) {\n        return writer.secret.signMessage(casUpdate.serialize())\n                .thenCompose(signed -> setPointer(owner, writer.publicKeyHash, signed).thenApply(res -> {\n                    if (res) {\n                        synchronized (targetCache) {\n                            targetCache.put(writer.publicKeyHash, new Pair<>(casUpdate, System.currentTimeMillis()));\n                        }\n                    }\n                    return res;\n                }));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash ownerPublicKey, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        synchronized (cache) {\n            cache.remove(writer);\n        }\n        synchronized (targetCache) {\n            targetCache.remove(writer);\n        }\n        return target.setPointer(ownerPublicKey, writer, writerSignedBtreeRootHash).thenApply(res -> {\n            if (res) {\n                synchronized (cache) {\n                    cache.put(writer, new Pair<>(Optional.of(writerSignedBtreeRootHash), System.currentTimeMillis()));\n                }\n            }\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        synchronized (cache) {\n            updates.forEach(u -> cache.remove(u.writer));\n        }\n        synchronized (targetCache) {\n            updates.forEach(u -> targetCache.remove(u.writer));\n        }\n        return target.setPointers(owner, updates).thenApply(res -> {\n            if (res) {\n                synchronized (cache) {\n                    updates.forEach(u -> cache.put(u.writer, new Pair<>(Optional.of(u.signed), System.currentTimeMillis())));\n                }\n            }\n            return res;\n        });\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        synchronized (cache) {\n            cache.clear();\n        }\n        targetCache.clear();\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/HttpMutablePointers.java",
    "content": "\npackage peergos.shared.mutable;\nimport java.util.logging.*;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class HttpMutablePointers implements MutablePointersProxy {\n\tprivate static final Logger LOG = Logger.getLogger(HttpMutablePointers.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n    private static final String P2P_PROXY_PROTOCOL = \"/http\";\n\n    private static final boolean LOGGING = false;\n    private final HttpPoster direct, p2p;\n    private final String directUrlPrefix;\n\n    public HttpMutablePointers(HttpPoster direct, HttpPoster p2p)\n    {\n        this.directUrlPrefix = \"\";\n        this.direct = direct;\n        this.p2p = p2p;\n    }\n\n    /** create an instance that always proxies calls to the supplied node\n     *\n     * @param p2p\n     * @param targetNodeID\n     */\n    public HttpMutablePointers(HttpPoster p2p, Multihash targetNodeID)\n    {\n        LOG.info(\"Creating proxying Http Mutable Pointers API at \" + p2p);\n        this.directUrlPrefix = getProxyUrlPrefix(targetNodeID);\n        this.direct = p2p;\n        this.p2p = p2p;\n    }\n\n    private static String getProxyUrlPrefix(Multihash targetId) {\n        return \"/p2p/\" + targetId.toString() + P2P_PROXY_PROTOCOL + \"/\";\n    }\n   \n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash ownerPublicKey,\n                                                 PublicKeyHash sharingPublicKey,\n                                                 byte[] sharingKeySignedPayload) {\n        return setPointer(directUrlPrefix, direct, ownerPublicKey, sharingPublicKey, sharingKeySignedPayload);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(Multihash targetId,\n                                                 PublicKeyHash ownerPublicKey,\n                                                 PublicKeyHash sharingPublicKey,\n                                                 byte[] sharingKeySignedPayload) {\n        return setPointer(getProxyUrlPrefix(targetId), p2p, ownerPublicKey, sharingPublicKey, sharingKeySignedPayload);\n    }\n\n    private CompletableFuture<Boolean> setPointer(String urlPrefix,\n                                                  HttpPoster poster,\n                                                  PublicKeyHash owner,\n                                                  PublicKeyHash writer,\n                                                  byte[] writerSignedPayload) {\n        long t1 = System.currentTimeMillis();\n        try\n        {\n            return poster.postUnzip(urlPrefix + Constants.MUTABLE_POINTERS_URL + \"setPointer?owner=\" + owner + \"&writer=\" + writer, writerSignedPayload, 60_000).thenApply(res -> {\n                DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n                try {\n                    return din.readBoolean();\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        } finally {\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"HttpMutablePointers.set took \" + (t2 -t1) + \"mS\");\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        return getPointer(directUrlPrefix, direct, owner, writer);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(Multihash targetId, PublicKeyHash owner, PublicKeyHash writer) {\n        return getPointer(getProxyUrlPrefix(targetId), p2p, owner, writer);\n    }\n\n    public CompletableFuture<Optional<byte[]>> getPointer(String urlPrefix, HttpPoster poster, PublicKeyHash owner, PublicKeyHash writer) {\n        long t1 = System.currentTimeMillis();\n        try {\n            return poster.get(urlPrefix + Constants.MUTABLE_POINTERS_URL + \"getPointer?owner=\" + owner + \"&writer=\" + writer)\n                    .thenApply(meta -> meta.length == 0 ? Optional.empty() : Optional.of(meta));\n        } catch (Exception ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n            return CompletableFuture.completedFuture(Optional.empty());\n        } finally {\n            long t2 = System.currentTimeMillis();\n            if (LOGGING)\n                LOG.info(\"HttpMutablePointers.get took \" + (t2 -t1) + \"mS for (\" + owner + \", \" + writer + \")\");\n        }\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        return setPointers(directUrlPrefix, direct, owner, updates);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(Multihash targetId, PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        return setPointers(getProxyUrlPrefix(targetId), p2p, owner, updates);\n    }\n\n    private CompletableFuture<Boolean> setPointers(String urlPrefix,\n                                                   HttpPoster poster,\n                                                   PublicKeyHash owner,\n                                                   List<SignedPointerUpdate> updates) {\n        return poster.postUnzip(urlPrefix + Constants.MUTABLE_POINTERS_URL + \"setPointers?owner=\" + owner,\n                new MultiWriterCommit(updates).serialize(), 60_000).thenApply(res -> {\n            DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n            try {\n                return din.readBoolean();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/MultiWriterCommit.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.util.List;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class MultiWriterCommit implements Cborable {\n    public final List<SignedPointerUpdate> updates;\n\n    public MultiWriterCommit(List<SignedPointerUpdate> updates) {\n        this.updates = updates;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"p\", new CborObject.CborList(updates));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static MultiWriterCommit fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for MultiWriterCommit! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new MultiWriterCommit(m.getList(\"p\", SignedPointerUpdate::fromCbor));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/MutablePointers.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.storage.ContentAddressedStorage;\nimport peergos.shared.storage.PointerCasException;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic interface MutablePointers {\n\n    /** Update the hash that a public key maps to (doing a cas with the existing value)\n     *\n     * @param owner The owner of this signing key\n     * @param writer The public signing key\n     * @param writerSignedBtreeRootHash the signed serialization of the HashCasPair\n     * @return True when successfully completed\n     */\n    CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash);\n\n    /** Atomically update the hashes for a set of writers under the same owner.\n     *  All updates succeed or none do.\n     */\n    CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates);\n\n    default CompletableFuture<Boolean> setPointer(PublicKeyHash owner, SigningPrivateKeyAndPublicHash writer, PointerUpdate casUpdate) {\n        return writer.secret.signMessage(casUpdate.serialize())\n                .thenCompose(signed -> setPointer(owner, writer.publicKeyHash, signed));\n    }\n\n    /** Get the current hash a public key maps to\n     *\n     * @param writer The public signing key\n     * @return The signed cas of the pointer from its previous value to its current value\n     */\n    CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer);\n\n    /**\n     * Get the latest pointer state for a writer-key.\n     * @param writer\n     * @param ipfs\n     * @return\n     */\n    default CompletableFuture<PointerUpdate> getPointerTarget(PublicKeyHash owner, PublicKeyHash writer, ContentAddressedStorage ipfs) {\n        return getPointer(owner, writer)\n                .thenCompose(current -> current.isPresent() ?\n                        parsePointerTarget(current.get(), owner, writer, ipfs) :\n                        Futures.of(PointerUpdate.empty()));\n    }\n\n    MutablePointers clearCache();\n\n    static CompletableFuture<PointerUpdate> parsePointerTarget(byte[] pointerCas,\n                                                               PublicKeyHash owner,\n                                                               PublicKeyHash writerKeyHash,\n                                                               ContentAddressedStorage ipfs) {\n        return ipfs.getSigningKey(owner, writerKeyHash)\n                .thenCompose(writerOpt -> writerOpt.map(writerKey -> writerKey.unsignMessage(pointerCas)\n                                .thenApply(signed -> PointerUpdate.fromCbor(CborObject.fromByteArray(signed))))\n                        .orElse(Futures.of(PointerUpdate.empty())));\n    }\n\n    static CompletableFuture<Boolean> isValidUpdate(PublicSigningKey writerKey, Optional<byte[]> current, byte[] writerSignedBtreeRootHash) {\n        return writerKey.unsignMessage(writerSignedBtreeRootHash).thenCompose(bothHashes -> {\n            PointerUpdate cas = PointerUpdate.fromCbor(CborObject.fromByteArray(bothHashes));\n            MaybeMultihash claimedCurrentHash = cas.original;\n            Multihash newHash = cas.updated.get();\n\n            return isValidUpdate(writerKey, current, claimedCurrentHash, cas.sequence);\n        });\n    }\n\n    static CompletableFuture<Boolean> isValidUpdate(PublicSigningKey writerKey,\n                                                    Optional<byte[]> current,\n                                                    MaybeMultihash claimedCurrentHash,\n                                                    Optional<Long> newSequence) {\n        Optional<CompletableFuture<PointerUpdate>> decoded = current.map(signed ->\n                writerKey.unsignMessage(signed)\n                        .thenApply(msg -> PointerUpdate.fromCbor(CborObject.fromByteArray(msg))));\n        return decoded.map(f -> f.thenApply(p -> p.updated)).orElse(Futures.of(MaybeMultihash.empty()))\n                .thenCompose(existing -> decoded.map(f -> f.thenApply(p -> p.sequence))\n                        .orElse(Futures.of(Optional.empty()))\n                        .thenCompose(currentSequence -> {\n                            // check CAS [current hash, new hash]\n                            boolean validSequence = currentSequence.isEmpty() || (newSequence.isPresent() && newSequence.get() > currentSequence.get());\n                            if (!existing.equals(claimedCurrentHash))\n                                return Futures.errored(new PointerCasException(existing, currentSequence, claimedCurrentHash));\n                            if (!validSequence)\n                                return Futures.errored(new IllegalStateException(\"Invalid sequence number update in mutable pointer: \" + currentSequence + \" => \" + newSequence));\n                            return Futures.of(true);\n                        }));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/MutablePointersProxy.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** A Mutable Pointers extension that proxies all calls over a p2p stream\n *\n */\npublic interface MutablePointersProxy extends MutablePointers {\n\n    /** Update the hash that a public key maps to (doing a cas with the existing value)\n     *\n     * @param targetServerId\n     * @param owner\n     * @param writer\n     * @param writerSignedBtreeRootHash the signed serialization of the HashCasPair\n     * @return\n     */\n    CompletableFuture<Boolean> setPointer(Multihash targetServerId, PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash);\n\n    /** Get the current hash a public key maps to\n     *\n     * @param targetServerId\n     * @param writer\n     * @return\n     */\n    CompletableFuture<Optional<byte[]>> getPointer(Multihash targetServerId, PublicKeyHash owner, PublicKeyHash writer);\n\n    CompletableFuture<Boolean> setPointers(Multihash targetServerId, PublicKeyHash owner, List<SignedPointerUpdate> updates);\n\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/OfflinePointerCache.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class OfflinePointerCache implements MutablePointers {\n\n    private static final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);\n\n    private final MutablePointers target;\n    private final PointerCache cache;\n    private final OnlineState online;\n    private final LRUCache<PublicKeyHash, byte[]> ramCache = new LRUCache<>(10);\n\n    public OfflinePointerCache(MutablePointers target, PointerCache cache, OnlineState online) {\n        this.target = target;\n        this.cache = cache;\n        this.online = online;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        return target.setPointer(owner, writer, writerSignedBtreeRootHash).thenApply(res -> {\n            if (res) {\n                ramCache.put(writer, writerSignedBtreeRootHash);\n                cache.put(owner, writer, writerSignedBtreeRootHash);\n            }\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        return target.setPointers(owner, updates).thenApply(res -> {\n            if (res) {\n                updates.forEach(u -> {\n                    ramCache.put(u.writer, u.signed);\n                    cache.put(owner, u.writer, u.signed);\n                });\n            }\n            return res;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        return Futures.asyncExceptionally(() -> {\n                    if (online.isOnline()) {\n                        CompletableFuture<Optional<byte[]>> res = new CompletableFuture<>();\n                        // race the cache with the server after 1s\n                        target.getPointer(owner, writer)\n                                .thenAccept(pointer -> {\n                                    pointer.ifPresent(p -> {\n                                        ramCache.put(writer, p);\n                                        cache.put(owner, writer, p);\n                                    });\n                                    res.complete(pointer);\n                                }).exceptionally(t -> {\n                                    res.completeExceptionally(t);\n                                    return null;\n                                });\n                        executor.schedule(() -> {\n                            byte[] fromRam = ramCache.get(writer);\n                            if (fromRam != null) {\n                                if (!res.isDone())\n                                    res.complete(Optional.of(fromRam));\n                            } else\n                                cache.get(owner, writer).thenAccept(cached -> {\n                                    if (cached.isPresent()) {\n                                        if (!res.isDone())\n                                            res.complete(cached);\n                                    }\n                                });\n                            return true;\n                        }, 1_000, TimeUnit.MILLISECONDS);\n                        return res;\n                    }\n                    online.updateAsync();\n                    return cache.get(owner, writer);\n                },\n                t -> {\n                    if (online.isOfflineException(t))\n                        return cache.get(owner, writer);\n                    return Futures.errored(t);\n                });\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return new OfflinePointerCache(target.clearCache(), cache, online);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/PointerCache.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface PointerCache {\n\n    CompletableFuture<Boolean> put(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash);\n\n    CompletableFuture<Optional<byte[]>> get(PublicKeyHash owner, PublicKeyHash writer);\n\n    default CompletableFuture<Boolean> doUpdate(Optional<byte[]> current, byte[] update, PublicSigningKey signer) {\n        if (current.isPresent() && Arrays.equals(current.get(), update))\n            return Futures.of(false);\n        Optional<CompletableFuture<Optional<PointerUpdate>>> currentFut = current.map(signer::unsignMessage)\n                .map(f -> f.thenApply(CborObject::fromByteArray))\n                .map(f -> f.thenApply(PointerUpdate::fromCbor))\n                .map(f -> f.thenApply(Optional::of));\n\n        return signer.unsignMessage(update).thenCompose(unsigned -> {\n            PointerUpdate newVal = PointerUpdate.fromCbor(CborObject.fromByteArray(unsigned));\n\n            return currentFut.orElse(Futures.of(Optional.empty())).thenApply(currentVal -> {\n                if (currentVal.isPresent() && currentVal.get().sequence.isPresent()) {\n                    long currentSeq = currentVal.get().sequence.get();\n                    if (newVal.sequence.isEmpty() || newVal.sequence.get() < currentSeq)\n                        throw new IllegalStateException(\"Invalid pointer update! Sequence number must increase.\");\n                }\n                return true;\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/PointerUpdate.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class PointerUpdate implements Cborable {\n\n    public final MaybeMultihash original;\n    public final MaybeMultihash updated;\n    public final Optional<Long> sequence;\n\n    public PointerUpdate(MaybeMultihash original, MaybeMultihash updated, Optional<Long> sequence) {\n        if (Objects.equals(original, updated))\n            throw new IllegalStateException(\"Tried to create a CAS pair with original == target!\");\n        this.original = original;\n        this.updated = updated;\n        this.sequence = sequence;\n    }\n\n    public static Optional<Long> increment(Optional<Long> sequence) {\n        return sequence.map(s -> Optional.of(s+1)).orElse(Optional.of(1L));\n    }\n\n    public static PointerUpdate empty() {\n        return new PointerUpdate(null, MaybeMultihash.empty(), Optional.empty());\n    }\n\n    @Override\n    public String toString() {\n        return sequence.orElse(0L) + \"(\" + original.toString() + \", \" + updated.toString() + \")\";\n    }\n\n    @Override\n    public CborObject toCbor() {\n        if (sequence.isPresent())\n            return new CborObject.CborList(Arrays.asList(\n                    original.toCbor(),\n                    updated.toCbor(),\n                    new CborObject.CborLong(sequence.get())\n            ));\n        return new CborObject.CborList(Arrays.asList(\n                original.toCbor(),\n                updated.toCbor()\n        ));\n    }\n\n    public static PointerUpdate fromCbor(CborObject cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Incorrect cbor for HashCasPair: \" + cbor);\n\n        List<? extends Cborable> value = ((CborObject.CborList) cbor).value;\n        return new PointerUpdate(MaybeMultihash.fromCbor(value.get(0)),\n                MaybeMultihash.fromCbor(value.get(1)),\n                value.size() < 3 ? Optional.empty() : Optional.of(((CborObject.CborLong)value.get(2)).value));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/ProxyingMutablePointers.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class ProxyingMutablePointers implements MutablePointers {\n\n    private final List<Cid> serverIds;\n    private final CoreNode core;\n    private final MutablePointers local;\n    private final MutablePointersProxy p2p;\n\n    public ProxyingMutablePointers(List<Cid> serverIds, CoreNode core, MutablePointers local, MutablePointersProxy p2p) {\n        this.serverIds = serverIds;\n        this.core = core;\n        this.local = local;\n        this.p2p = p2p;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        return Proxy.redirectCall(core,\n                serverIds,\n                owner,\n                () -> local.setPointer(owner, writer, writerSignedBtreeRootHash),\n                target -> p2p.setPointer(target, owner, writer, writerSignedBtreeRootHash));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        return Proxy.redirectCall(core,\n                serverIds,\n                owner,\n                () -> local.setPointers(owner, updates),\n                target -> p2p.setPointers(target, owner, updates));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        return Proxy.redirectCall(core,\n                serverIds,\n                owner,\n                () -> local.getPointer(owner, writer),\n                target -> p2p.getPointer(target, owner, writer));\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return new ProxyingMutablePointers(serverIds, core, local.clearCache(), p2p);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/RetryMutablePointers.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.CasException;\nimport peergos.shared.storage.PointerCasException;\n\nimport java.net.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class RetryMutablePointers implements MutablePointers {\n\n    private final Random random = new Random(1);\n    private final MutablePointers target;\n    private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);\n    private final int maxAttempts = 3;\n\n    public RetryMutablePointers(MutablePointers target) {\n        this.target = target;\n    }\n\n    private <V> void retryAfter(Supplier<CompletableFuture<V>> method, int milliseconds) {\n        executor.schedule(method::get, milliseconds, TimeUnit.MILLISECONDS);\n    }\n\n    private int jitter(int minMilliseconds, int rangeMilliseconds) {\n        return minMilliseconds + random.nextInt(rangeMilliseconds);\n    }\n\n    private <V> CompletableFuture<V> runWithRetry(Supplier<CompletableFuture<V>> f) {\n        return recurse(maxAttempts, f);\n    }\n\n    private <V> CompletableFuture<V> recurse(int retriesLeft, Supplier<CompletableFuture<V>> f) {\n        CompletableFuture<V> res = new CompletableFuture<>();\n        try {\n            f.get()\n                    .thenAccept(res::complete)\n                    .exceptionally(e -> {\n                        if (retriesLeft == 1) {\n                            res.completeExceptionally(e);\n                        } else if (e instanceof ConnectException) {\n                            res.completeExceptionally(e);\n                        } else if (e instanceof PointerCasException) {\n                            res.completeExceptionally(e);\n                        } else if (e instanceof CasException) {\n                            res.completeExceptionally(e);\n                        } else {\n                            retryAfter(() -> recurse(retriesLeft - 1, f)\n                                            .thenAccept(res::complete)\n                                            .exceptionally(t -> {\n                                                res.completeExceptionally(t);\n                                                return null;\n                                            }),\n                                    jitter((maxAttempts + 1 - retriesLeft) * 1000, 500));\n                        }\n                        return null;\n                    });\n        } catch (Throwable t) {\n            res.completeExceptionally(t);\n        }\n        return res;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointer(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash) {\n        return runWithRetry(() -> target.setPointer(owner, writer, writerSignedBtreeRootHash));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setPointers(PublicKeyHash owner, List<SignedPointerUpdate> updates) {\n        return runWithRetry(() -> target.setPointers(owner, updates));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getPointer(PublicKeyHash owner, PublicKeyHash writer) {\n        return runWithRetry(() -> target.getPointer(owner, writer));\n    }\n\n    @Override\n    public MutablePointers clearCache() {\n        return new RetryMutablePointers(target.clearCache());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/mutable/SignedPointerUpdate.java",
    "content": "package peergos.shared.mutable;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.hash.PublicKeyHash;\n\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class SignedPointerUpdate implements Cborable {\n    public final PublicKeyHash writer;\n    public final byte[] signed;\n\n    public SignedPointerUpdate(PublicKeyHash writer, byte[] signed) {\n        this.writer = writer;\n        this.signed = signed;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"w\", writer);\n        state.put(\"s\", new CborObject.CborByteArray(signed));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static SignedPointerUpdate fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for SignedPointerUpdate! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new SignedPointerUpdate(m.get(\"w\", PublicKeyHash::fromCbor), m.get(\"s\", c -> ((CborObject.CborByteArray)c).value));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/resolution/ResolutionRecord.java",
    "content": "package peergos.shared.resolution;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\n\n/** This is the object that can be resolved from a public key via one or more DHTs, or intermediaries\n *  It contains the information required to resolve a capability, even if the host is offline.\n *\n *  These are published by server's peerids, user's owner keypair, and writer key pairs.\n *  1. server ids\n *  Here this is used to rotate the server identity to a new key pair. Upon setup, a server generates its next identity.\n *  The next identity key pair can then be stored offline, and only used in case of server compromise.\n *  If a server becomes unreachable, or if the moved field is set to true then we attempt to resolve the new identity.\n *\n *  2. owner/writer key pairs\n *  This includes the current host peerid which queries can be directed to, and the current mutable pointer\n */\npublic class ResolutionRecord implements Cborable {\n    public final Optional<Multihash> host; // For peer ids this can only be set, not removed or modified.\n    public final boolean moved; // For peer ids this can only be updated from false to true\n    public final Optional<byte[]> mutablePointer;\n    public final long sequence; // monotonic\n\n    public ResolutionRecord(Optional<Multihash> host,\n                            boolean moved,\n                            Optional<byte[]> mutablePointer,\n                            long sequence) {\n        this.host = host;\n        this.moved = moved;\n        this.mutablePointer = mutablePointer;\n        this.sequence = sequence;\n    }\n\n    public void ensureValidUpdateTo(ResolutionRecord existingValue) {\n        // sequence must be monotonic\n        if (sequence <= existingValue.sequence)\n            throw new IllegalStateException(\"Invalid update!\");\n        // moved can only change from  false to true\n        if (! moved && existingValue.moved)\n            throw new IllegalStateException(\"Invalid update!\");\n        //  host can only be set, not changed\n        if (existingValue.host.isPresent() && ! host.equals(existingValue.host))\n            throw new IllegalStateException(\"Invalid update!\");\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        host.ifPresent(h -> state.put(\"h\", new CborObject.CborByteArray(h.toBytes())));\n        if (moved)\n            state.put(\"m\", new CborObject.CborBoolean(true));\n        mutablePointer.ifPresent(p -> state.put(\"p\", new CborObject.CborByteArray(p)));\n        state.put(\"s\", new CborObject.CborLong(sequence));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ResolutionRecord fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for ResolutionData! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        Optional<Multihash> host = m.getOptionalByteArray(\"h\").map(Multihash::decode);\n        boolean moved = m.getBoolean(\"m\");\n        Optional<byte[]> mutablePointer = m.getOptionalByteArray(\"p\");\n        long seq = m.getLong(\"s\");\n        return new ResolutionRecord(host, moved, mutablePointer, seq);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/BlindFollowRequest.java",
    "content": "package peergos.shared.social;\n\nimport peergos.shared.Crypto;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMPublicKey;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.util.Futures;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class BlindFollowRequest implements Cborable {\n\n    public final PublicBoxingKey dummySource;\n    public final PaddedAsymmetricCipherText followRequest;\n\n    public BlindFollowRequest(PublicBoxingKey dummySource, PaddedAsymmetricCipherText followRequest) {\n        this.dummySource = dummySource;\n        this.followRequest = followRequest;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        result.put(\"k\", dummySource.toCbor());\n        result.put(\"f\", followRequest.toCbor());\n        return CborObject.CborMap.build(result);\n    }\n\n    public static BlindFollowRequest fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for BlindFollowRequest: \"+  cbor);\n        PublicBoxingKey dummysource = PublicBoxingKey.fromCbor(((CborObject.CborMap) cbor).get(\"k\"));\n        PaddedAsymmetricCipherText followRequest = PaddedAsymmetricCipherText.fromCbor(((CborObject.CborMap) cbor).get(\"f\"));\n        return new BlindFollowRequest(dummysource, followRequest);\n    }\n\n    public static CompletableFuture<BlindFollowRequest> build(PublicBoxingKey targetBoxer, FollowRequest request, Crypto crypto) {\n        // create a tmp keypair whose public key we can prepend to the request without leaking information\n        return (targetBoxer instanceof HybridCurve25519MLKEMPublicKey ?\n                BoxingKeyPair.randomHybrid(crypto) :\n                Futures.of(BoxingKeyPair.randomCurve25519(crypto.random, crypto.boxer)))\n                .thenCompose(tmp -> PaddedAsymmetricCipherText.build(tmp.secretBoxingKey, targetBoxer, request, 512)\n                        .thenApply(cipher -> new BlindFollowRequest(tmp.publicBoxingKey, cipher)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/FollowRequest.java",
    "content": "package peergos.shared.social;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.user.*;\n\nimport java.util.*;\n\n@JsType\npublic class FollowRequest implements Cborable {\n\n    public final Optional<EntryPoint> entry;\n    public final Optional<SymmetricKey> key;\n\n    public FollowRequest(Optional<EntryPoint> entry, Optional<SymmetricKey> key) {\n        this.entry = entry;\n        this.key = key;\n    }\n\n    public boolean isAccepted() {\n        return entry.isPresent();\n    }\n\n    public boolean isReciprocated() {\n        return key.isPresent();\n    }\n\n    @SuppressWarnings(\"unusable-by-js\")\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n        entry.ifPresent(e -> result.put(\"e\", e.toCbor()));\n        key.ifPresent(k -> result.put(\"k\", k.toCbor()));\n        return CborObject.CborMap.build(result);\n    }\n\n    @SuppressWarnings(\"unusable-by-js\")\n    public static FollowRequest fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for FollowRequest: \" + cbor);\n        Optional<EntryPoint> entryPoint = Optional.ofNullable(((CborObject.CborMap) cbor).get(\"e\"))\n                .map(EntryPoint::fromCbor);\n        Optional<SymmetricKey> key = Optional.ofNullable(((CborObject.CborMap) cbor).get(\"k\"))\n                .map(SymmetricKey::fromCbor);\n        return new FollowRequest(entryPoint, key);\n    }\n\n    @Override\n    public String toString() {\n        return entry + \" - \" + key.isPresent();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/FollowRequestWithCipherText.java",
    "content": "package peergos.shared.social;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.user.*;\n\nimport java.util.*;\n\npublic class FollowRequestWithCipherText {\n\n    public final FollowRequest req;\n    public final BlindFollowRequest cipher;\n\n    public FollowRequestWithCipherText(FollowRequest req, BlindFollowRequest cipher) {\n        this.req = req;\n        this.cipher = cipher;\n    }\n\n    public FollowRequestWithCipherText withEntryPoint(EntryPoint updated) {\n        return new FollowRequestWithCipherText(new FollowRequest(Optional.of(updated), req.key), cipher);\n    }\n\n    @JsMethod\n    public EntryPoint getEntry() {\n        return req.entry.get();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/HttpSocialNetwork.java",
    "content": "package peergos.shared.social;\nimport java.util.logging.*;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.concurrent.*;\n\npublic class HttpSocialNetwork implements SocialNetworkProxy {\n\tprivate static final Logger LOG = Logger.getLogger(HttpSocialNetwork.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n    private static final String P2P_PROXY_PROTOCOL = \"/http\";\n\n    private final HttpPoster direct, p2p;\n\n    public HttpSocialNetwork(HttpPoster direct, HttpPoster p2p)\n    {\n        this.direct = direct;\n        this.p2p = p2p;\n    }\n\n    private static String getProxyUrlPrefix(Multihash targetId) {\n        return \"/p2p/\" + targetId.toString() + P2P_PROXY_PROTOCOL + \"/\";\n    }\n\n    @Override\n    public CompletableFuture<Boolean> sendFollowRequest(PublicKeyHash target, byte[] encryptedPermission) {\n        return sendFollowRequest(\"\", direct, target, encryptedPermission);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> sendFollowRequest(Multihash targetServerId, PublicKeyHash target, byte[] encryptedPermission) {\n        return sendFollowRequest(getProxyUrlPrefix(targetServerId), p2p, target, encryptedPermission);\n    }\n\n    private CompletableFuture<Boolean> sendFollowRequest(String urlPrefix, HttpPoster poster, PublicKeyHash target, byte[] encryptedPermission)\n    {\n        return poster.postUnzip(urlPrefix + Constants.SOCIAL_URL + \"followRequest?owner=\" + encode(target.toString()), encryptedPermission).thenApply(res -> {\n            DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n            try {\n                return din.readBoolean();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    @Override\n    public CompletableFuture<byte[]> getFollowRequests(PublicKeyHash owner, byte[] signedTime) {\n        return getFollowRequests(\"\", direct, owner, signedTime);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> getFollowRequests(Multihash targetServerId, PublicKeyHash owner, byte[] signedTime) {\n        return getFollowRequests(getProxyUrlPrefix(targetServerId), p2p, owner, signedTime);\n    }\n\n    private CompletableFuture<byte[]> getFollowRequests(String urlPrefix, HttpPoster poster, PublicKeyHash owner, byte[] signedTime)\n    {\n        return poster.get(urlPrefix + Constants.SOCIAL_URL + \"getFollowRequests?owner=\" + encode(owner.toString())\n                + \"&auth=\" + ArrayOps.bytesToHex(signedTime)).thenApply(res -> {\n            DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n            try {\n                return CoreNodeUtils.deserializeByteArray(din);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> removeFollowRequest(PublicKeyHash owner, byte[] signedRequest) {\n        return removeFollowRequest(\"\", direct, owner, signedRequest);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> removeFollowRequest(Multihash targetServerId, PublicKeyHash owner, byte[] data) {\n        return removeFollowRequest(getProxyUrlPrefix(targetServerId), p2p, owner, data);\n    }\n\n    private CompletableFuture<Boolean> removeFollowRequest(String urlPrefix, HttpPoster poster, PublicKeyHash owner, byte[] signedRequest)\n    {\n        return poster.postUnzip(urlPrefix + Constants.SOCIAL_URL + \"removeFollowRequest?owner=\" + encode(owner.toString()), signedRequest).thenApply(res -> {\n            DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n            try {\n                return din.readBoolean();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    private static String encode(String component) {\n        try {\n            return URLEncoder.encode(component, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/ProcessedCaps.java",
    "content": "package peergos.shared.social;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.*;\n\nimport java.util.*;\n\n/** Each of the users you follow have one of these serialized and stored in your cap cache for them and social feed.\n *\n */\npublic class ProcessedCaps implements Cborable {\n    public final int readCaps, writeCaps;\n    public final long readCapBytes, writeCapBytes;\n    public final Map<String, ProcessedCaps> groups;\n\n    public ProcessedCaps(int readCaps, int writeCaps, long readCapBytes, long writeCapBytes, Map<String, ProcessedCaps> groups) {\n        this.readCaps = readCaps;\n        this.writeCaps = writeCaps;\n        this.readCapBytes = readCapBytes;\n        this.writeCapBytes = writeCapBytes;\n        this.groups = groups;\n    }\n\n    public ProcessedCaps add(CapsDiff diff) {\n        if (readCapBytes != diff.priorReadByteOffset)\n            throw new IllegalStateException(\"Applying cap diff to wrong base\");\n        if (writeCapBytes != diff.priorWriteByteOffset)\n            throw new IllegalStateException(\"Applying cap diff to wrong base\");\n\n        HashMap<String, ProcessedCaps> updated = new HashMap<>(groups);\n        for (Map.Entry<String, CapsDiff> e : diff.groupDiffs.entrySet()) {\n            ProcessedCaps current = groups.get(e.getKey());\n            CapsDiff gDiff = e.getValue();\n            if (current == null) {\n                updated.put(e.getKey(), new ProcessedCaps(gDiff.readCapCount(),\n                        gDiff.writeCapCount(), gDiff.updatedReadBytes(), gDiff.updatedWriteBytes(), Collections.emptyMap()));\n            } else {\n                updated.put(e.getKey(), current.add(gDiff));\n            }\n        }\n        return new ProcessedCaps(\n                readCaps + diff.readCapCount(),\n                writeCaps + diff.writeCapCount(),\n                diff.updatedReadBytes(),\n                diff.updatedWriteBytes(),\n                updated\n        );\n    }\n\n    public CapsDiff createGroupDiff(String name, CapsDiff diff) {\n        Map<String, CapsDiff> groups = new HashMap<>();\n        groups.put(name, diff);\n        return new CapsDiff(readCapBytes, writeCapBytes, CapsDiff.ReadAndWriteCaps.empty(), groups);\n    }\n\n    public static ProcessedCaps empty() {\n        return new ProcessedCaps(0, 0, 0L, 0L, Collections.emptyMap());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"rc\", new CborObject.CborLong(readCaps));\n        state.put(\"wc\", new CborObject.CborLong(writeCaps));\n        state.put(\"rb\", new CborObject.CborLong(readCapBytes));\n        state.put(\"wb\", new CborObject.CborLong(writeCapBytes));\n\n        SortedMap<String, Cborable> groups = new TreeMap<>();\n        for (Map.Entry<String, ProcessedCaps> e : this.groups.entrySet()) {\n            groups.put(e.getKey(), e.getValue().toCbor());\n        }\n        state.put(\"g\", CborObject.CborMap.build(groups));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ProcessedCaps fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        int readCaps = (int) m.getLong(\"rc\");\n        int writeCaps = (int) m.getLong(\"wc\");\n        long readCapBytes = m.getLong(\"rb\");\n        long writeCapBytes = m.getLong(\"wb\");\n\n        Map<String, ProcessedCaps> groups = m.getMap(\"g\", c -> ((CborObject.CborString) c).value, ProcessedCaps::fromCbor);\n        return new ProcessedCaps(readCaps, writeCaps, readCapBytes, writeCapBytes, groups);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/ProxyingSocialNetwork.java",
    "content": "package peergos.shared.social;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class ProxyingSocialNetwork implements SocialNetwork {\n\n    private final List<Cid> serverIds;\n    private final CoreNode core;\n    private final SocialNetwork local;\n    private final SocialNetworkProxy p2p;\n\n    public ProxyingSocialNetwork(List<Cid> serverIds, CoreNode core, SocialNetwork local, SocialNetworkProxy p2p) {\n        this.serverIds = serverIds;\n        this.core = core;\n        this.local = local;\n        this.p2p = p2p;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> sendFollowRequest(PublicKeyHash targetUser, byte[] encryptedPermission) {\n        return Proxy.redirectCall(core,\n                serverIds,\n                targetUser,\n                () -> local.sendFollowRequest(targetUser, encryptedPermission),\n                targetServer -> p2p.sendFollowRequest(targetServer, targetUser, encryptedPermission));\n    }\n\n    @Override\n    public CompletableFuture<byte[]> getFollowRequests(PublicKeyHash owner, byte[] signedTime) {\n        return Proxy.redirectCall(core,\n                serverIds,\n                owner,\n                () -> local.getFollowRequests(owner, signedTime),\n                targetServer -> p2p.getFollowRequests(targetServer, owner, signedTime));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> removeFollowRequest(PublicKeyHash owner, byte[] signedRequest) {\n        return Proxy.redirectCall(core,\n                serverIds,\n                owner,\n                () -> local.removeFollowRequest(owner, signedRequest),\n                targetServer -> p2p.removeFollowRequest(targetServer, owner, signedRequest));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/SharedItem.java",
    "content": "package peergos.shared.social;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\n\n@JsType\npublic class SharedItem implements Cborable {\n    public final AbsoluteCapability cap;\n    public final String owner, sharer, path;\n\n    public SharedItem(AbsoluteCapability cap, String owner, String sharer, String path) {\n        this.cap = cap;\n        this.owner = owner;\n        this.sharer = sharer;\n        this.path = path;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        SharedItem that = (SharedItem) o;\n        return Objects.equals(cap, that.cap) &&\n                Objects.equals(owner, that.owner) &&\n                Objects.equals(sharer, that.sharer) &&\n                Objects.equals(path, that.path);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(cap, owner, sharer, path);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", cap);\n        state.put(\"o\", new CborObject.CborString(owner));\n        state.put(\"s\", new CborObject.CborString(sharer));\n        state.put(\"p\", new CborObject.CborString(path));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static SharedItem fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        AbsoluteCapability cap = m.getObject(\"c\", AbsoluteCapability::fromCbor);\n        String owner = m.getString(\"o\");\n        String sharer = m.getString(\"s\");\n        String path = m.getString(\"p\");\n        return new SharedItem(cap, owner, sharer, path);\n    }\n\n    @Override\n    public String toString() {\n        return path + \" via \" + sharer;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/SocialFeed.java",
    "content": "package peergos.shared.social;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.display.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/** This social feed stores a list of caps shared with you.\n *\n *  Data is stored in /username/.feed/\n *                                    feed-state.cbor    - Your serialized FeedState\n *                                    feed-index.cbor    - An lookup from index in feed to byte offset in feed.cbor\n *                                    feed.cbor          - An append only list of serialized SharedItems\n *\n *  The FeedState stores how many bytes of the incoming cap file has been processed from each friend\n */\npublic class SocialFeed {\n    private static final String FEED_FILE = \"feed.cbor\";\n    private static final String FEED_INDEX = \"feed-index.cbor\";\n    private static final String FEED_STATE = \"feed-state.cbor\";\n\n    private FileWrapper dataDir, stateFile;\n    private int lastSeenIndex, feedSizeRecords;\n    private long feedSizeBytes;\n    private Map<String, ProcessedCaps> currentCapBytesProcessed;\n    private final UserContext context;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n\n    public SocialFeed(FileWrapper dataDir,\n                      FileWrapper stateFile,\n                      FeedState state,\n                      UserContext context) {\n        this.dataDir = dataDir;\n        this.stateFile = stateFile;\n        this.lastSeenIndex = state.lastSeenIndex;\n        this.feedSizeRecords = state.feedSizeRecords;\n        this.feedSizeBytes = state.feedSizeBytes;\n        this.currentCapBytesProcessed = new HashMap<>(state.currentCapBytesProcessed);\n        this.context = context;\n        this.network = context.network;\n        this.crypto = context.crypto;\n    }\n\n    /** Create a new post file under /username/.posts/$year/$month/#uuid\n     *\n     * @param post\n     * @return\n     */\n    @JsMethod\n    public CompletableFuture<Pair<Path, FileWrapper>> createNewPost(SocialPost post) {\n        if (! post.author.equals(context.username))\n            throw new IllegalStateException(\"You can only post as yourself!\");\n        String postFilename = UUID.randomUUID().toString() + \".cbor\";\n        Path dir = getDirFromHome(post);\n        byte[] raw = post.serialize();\n        AsyncReader reader = AsyncReader.build(raw);\n        return context.getUserRoot()\n                .thenCompose(home -> home.getOrMkdirs(dir, network, true, context.mirrorBatId(), crypto))\n                .thenCompose(postDir -> postDir.uploadAndReturnFile(postFilename, reader, raw.length, false,\n                        postDir.mirrorBatId(), network, crypto)\n                        .thenApply(f -> new Pair<>(PathUtil.get(post.author).resolve(dir).resolve(postFilename), f)))\n                .thenCompose(p -> context.network.synchronizer.applyComplexComputation(context.signer.publicKeyHash,\n                                dataDir.signingPair(),\n                                (v, c) -> addToFeed(v, c, Arrays.asList(new SharedItem(p.right.readOnlyPointer(),\n                                        context.username, context.username, p.left.toString()))))\n                        .thenApply(f -> p));\n    }\n\n    @JsMethod\n    public CompletableFuture<Pair<Path, FileWrapper>> updatePost(String uuid, SocialPost post) {\n        if (! post.author.equals(context.username))\n            throw new IllegalStateException(\"You can only post as yourself!\");\n        Path dir = getDirFromHome(post.previousVersions.get(0));\n        byte[] raw = post.serialize();\n        String completePath = context.username + \"/\" + dir.resolve(uuid).toString();\n        return context.getByPath(completePath).thenCompose(fopt ->\n            fopt.get().overwriteFile(AsyncReader.build(raw), raw.length, network, crypto, x -> {})\n                    .thenApply(f -> new Pair<>(PathUtil.get(post.author).resolve(dir).resolve(uuid), f))\n        );\n    }\n\n    @JsMethod\n    public CompletableFuture<Pair<String, FileRef>> uploadMediaForPost(AsyncReader media,\n                                                                       int length,\n                                                                       LocalDateTime postTime,\n                                                                       ProgressConsumer<Long> monitor) {\n        String uuid = UUID.randomUUID().toString();\n        return getOrMkdirToStoreMedia(\"media\", postTime)\n                .thenCompose(p -> p.right.uploadAndReturnFile(uuid, media, length, false, () -> false, monitor,\n                        p.right.mirrorBatId(), network, crypto)\n                        .thenCompose(f ->  media.reset().thenCompose(r -> crypto.hasher.hashFromStream(r, length))\n                                .thenApply(hash -> new Pair<>(f.getFileProperties().getType(),\n                                        new FileRef(p.left.resolve(uuid).toString(), f.readOnlyPointer(), hash)))));\n    }\n\n    private CompletableFuture<Pair<Path, FileWrapper>> getOrMkdirToStoreMedia(String mediaType, LocalDateTime postTime) {\n        Path dirFromHome = PathUtil.get(UserContext.POSTS_DIR_NAME,\n                Integer.toString(postTime.getYear()),\n                mediaType);\n        return context.getUserRoot()\n                .thenCompose(home -> home.getOrMkdirs(dirFromHome, network, true, context.mirrorBatId(), crypto)\n                .thenApply(dir -> new Pair<>(PathUtil.get(\"/\" + context.username).resolve(dirFromHome), dir)));\n    }\n\n    public static Path getDirFromHome(SocialPost post) {\n        return PathUtil.get(UserContext.POSTS_DIR_NAME,\n                Integer.toString(post.postTime.getYear()),\n                Integer.toString(post.postTime.getMonthValue()));\n    }\n\n    @JsMethod\n    public synchronized boolean hasUnseen() {\n        return lastSeenIndex < feedSizeRecords;\n    }\n\n    @JsMethod\n    public synchronized int getFeedSize() {\n        return feedSizeRecords;\n    }\n\n    @JsMethod\n    public synchronized int getLastSeenIndex() {\n        return lastSeenIndex;\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> setLastSeenIndex(int newLastSeenIndex) {\n        this.lastSeenIndex = newLastSeenIndex;\n        return commit();\n    }\n\n    /**\n     *\n     * @param index\n     * @return the byte offset and corresponding index of a prior object boundary, which ideally should be in the same chunk\n     */\n    private CompletableFuture<Pair<Long, Integer>> getPriorByteOffset(int index) {\n        return dataDir.getChild(FEED_INDEX, crypto.hasher, network)\n                .thenCompose(fopt -> {\n                    //TODO\n//                    if (fopt.isEmpty())\n//                        throw new IllegalStateException(\"Social feed state file not present!\");\n                    return Futures.of(new Pair<>(0L, 0));\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<List<SharedItem>> getShared(int from, int to, Crypto crypto, NetworkAccess network) {\n        return getPriorByteOffset(from)\n                .thenCompose(start -> dataDir.getChild(FEED_FILE, crypto.hasher, network)\n                        .thenCompose(fopt -> fopt.map(f -> f.getInputStream(network, crypto, x -> {})\n                                .thenCompose(stream -> stream.seek(start.left))\n                                .thenCompose(stream -> {\n                                    List<SharedItem> res = new ArrayList<>();\n                                    return stream.parseLimitedStream(SharedItem::fromCbor, res::add,\n                                            from - start.right, Math.min(feedSizeRecords, to) - from, feedSizeBytes)\n                                            .thenApply(x -> res);\n                                })).orElse(Futures.of(Collections.emptyList()))));\n    }\n\n    @JsMethod\n    public CompletableFuture<List<Pair<SharedItem, FileWrapper>>> getSharedFiles(int from, int to) {\n        return getShared(from, to, crypto, network)\n                .thenCompose(context::getFiles);\n    }\n\n    private CompletableFuture<Pair<Snapshot, List<Pair<SharedItem, FileWrapper>>>> mergeCommentReferences(\n            List<Pair<SharedItem, FileWrapper>> items,\n            Snapshot v,\n            Committer c) {\n        List<Pair<SharedItem, FileWrapper>> posts = items.stream()\n                .filter(p -> p.right.getFileProperties().isSocialPost())\n                .collect(Collectors.toList());\n\n        return Futures.combineAllInOrder(posts.stream()\n                .map(p -> Serialize.parse(p.right, SocialPost::fromCbor, network, crypto)\n                        .thenApply(sp -> new Triple<>(p.left, p.right, sp)))\n                .collect(Collectors.toList()))\n                .thenCompose(retrieved -> {\n                    Map<String, List<Triple<SharedItem, FileWrapper, SocialPost>>> commentsOnOurs = retrieved.stream()\n                            .filter(t -> t.right.parent.map(p -> p.path.startsWith(\"/\" + context.username)).orElse(false))\n                            .collect(Collectors.groupingBy(t -> t.right.parent.get().path));\n                    return Futures.reduceAll(commentsOnOurs.entrySet().stream(), v,\n                            (s, e) -> mergeCommentsIntoParent(e.getKey(), e.getValue(), s, c),\n                            (a, b) -> b);\n                }).thenApply(res -> new Pair<>(res, items));\n    }\n\n    private CompletableFuture<Snapshot> mergeCommentsIntoParent(String parentPath,\n                                                                List<Triple<SharedItem, FileWrapper, SocialPost>> comments,\n                                                                Snapshot v,\n                                                                Committer c) {\n        return Futures.combineAllInOrder(comments.stream().map(t -> t.middle.getInputStream(network, crypto, x -> {})\n                .thenCompose(reader -> crypto.hasher.hashFromStream(reader, t.middle.getSize())\n                        .thenApply(h -> new FileRef(t.left.path, t.left.cap, h))))\n                .collect(Collectors.toList())).thenCompose(refs ->\n                context.getByPath(parentPath, v).thenCompose(fopt -> {\n                    if (fopt.isEmpty())\n                        return Futures.of(v);\n                    if (! fopt.get().getFileProperties().isSocialPost())\n                        return Futures.of(v);\n                    return Serialize.parse(fopt.get(), SocialPost::fromCbor, network, crypto)\n                            .thenCompose(parent -> {\n                                SocialPost withComments = parent.addComments(refs);\n                                byte[] raw = withComments.serialize();\n                                return fopt.get().getUpdated(v, network)\n                                        .thenCompose(updated -> updated.overwriteFile(AsyncReader.build(raw), raw.length, network, crypto, x -> {}, v, c))\n                                        .thenApply(x -> {\n                                            dataDir = dataDir.withVersion(x);\n                                            return x;\n                                        });\n                            });\n                }));\n    }\n\n    private synchronized CompletableFuture<Boolean> commit() {\n        byte[] raw = new FeedState(lastSeenIndex, feedSizeRecords, feedSizeBytes, currentCapBytesProcessed).serialize();\n        return stateFile.overwriteFile(AsyncReader.build(raw), raw.length,\n                network, crypto, x -> {})\n                .thenApply(f -> {\n                    this.stateFile = f;\n                    return true;\n                });\n    }\n\n    /** Incorporate any new shares from friends into the feed\n     *\n     * @return\n     */\n    @JsMethod\n    public synchronized CompletableFuture<SocialFeed> update() {\n        PublicKeyHash owner = context.signer.publicKeyHash;\n        return network.synchronizer.applyComplexComputation(owner, dataDir.signingPair(),\n                (s, c) -> {\n                    return context.getFollowingNodes()\n                            .thenCompose(friends -> {\n                                List<CompletableFuture<Optional<Pair<FriendSourcedTrieNode,Snapshot>>>> pointers = friends.stream()\n                                        .map(f -> Futures.orTimeout(() -> f.getLatestVersion(network), 2_000)\n                                                .thenApply(v -> Optional.of(new Pair<>(f, v)))\n                                                .exceptionally(t -> Optional.empty()))\n                                        .collect(Collectors.toList());\n                                return Futures.combineAllInOrder(pointers)\n                                        .thenApply(pairs -> pairs.stream()\n                                                .flatMap(Optional::stream)\n                                                .map(p -> new Pair<>(Stream.of(p.left), p.right))\n                                                .reduce((a, b) -> new Pair<>(Stream.concat(a.left, b.left), a.right.merge(b.right))))\n                                        .thenApply(combined ->  new Pair<>(\n                                                combined.map(p ->  p.left.collect(Collectors.toSet())).orElse(Collections.emptySet()),\n                                                combined.map(p ->  p.right).map(s::mergeAndOverwriteWith).orElse(s)));\n                            }).\n                            thenCompose(fv -> {\n                                Snapshot s0 = fv.right;\n                                List<CompletableFuture<Pair<Snapshot, Optional<Update>>>> futures = fv.left.stream()\n                                        .map(friend -> getFriendUpdate(friend, s0, c, network))\n                                        .collect(Collectors.toList());\n                                return Futures.combineAllInOrder(futures)\n                                        .thenApply(results -> new Pair<>(\n                                                results.stream().map(p -> p.left).reduce(s0, Snapshot::mergeAndOverwriteWith),\n                                                results.stream().flatMap(r -> r.right.stream()).collect(Collectors.toList())));\n                            })\n                            .thenCompose(p -> mergeUpdates(p.left, c, p.right));\n                }).thenApply(p -> p.right);\n    }\n\n    private static class Update extends Triple<String, ProcessedCaps, CapsDiff> {\n        public Update(String left, ProcessedCaps middle, CapsDiff right) {\n            super(left, middle, right);\n        }\n    }\n\n    private CompletableFuture<Pair<Snapshot, Optional<Update>>> getFriendUpdate(FriendSourcedTrieNode friend,\n                                                                                Snapshot s,\n                                                                                Committer c,\n                                                                                NetworkAccess network) {\n        long t0 = System.currentTimeMillis();\n        ProcessedCaps current = currentCapBytesProcessed.getOrDefault(friend.ownerName, ProcessedCaps.empty());\n        return friend.updateIncludingGroups(s, c, network)\n                .thenCompose(p -> friend.getCaps(current, s,network)\n                        .thenApply(diff -> {\n                            long t1 = System.currentTimeMillis();\n                            System.out.println(\"GetFriendUpdate(\"+friend.ownerName +\") \" + (t1-t0));\n                            if (diff.isEmpty())\n                                return new Pair<>(p.left, Optional.<Update>empty());\n                            return new Pair<>(p.left, Optional.of(new Update(friend.ownerName, current, diff)));\n                        }));\n    }\n\n    private synchronized CompletableFuture<Pair<Snapshot, SocialFeed>> mergeUpdates(Snapshot v,\n                                                                                    Committer com,\n                                                                                    Collection<? extends Triple<String, ProcessedCaps, CapsDiff>> updates) {\n        List<SharedItem> forFeed = new ArrayList<>();\n        for (Triple<String, ProcessedCaps, CapsDiff> update : updates) {\n            ProcessedCaps updated = update.middle.add(update.right);\n            currentCapBytesProcessed.put(update.left, updated);\n            List<CapabilityWithPath> newCaps = update.right.getNewCaps();\n            newCaps.stream()\n                    .map(c -> new SharedItem(c.cap, extractOwner(c.path), update.left, c.path))\n                    .forEach(forFeed::add);\n        }\n        return addToFeed(v, com, forFeed);\n    }\n\n    private CompletableFuture<Pair<Snapshot, List<Pair<SharedItem, FileWrapper>>>> mergeInComments(List<SharedItem> shared,\n                                                                                                   Snapshot v,\n                                                                                                   Committer c) {\n        return context.getFiles(shared, v)\n                .thenCompose(retrieved -> mergeCommentReferences(retrieved, v, c));\n    }\n\n    private static String extractOwner(String path) {\n        return PathUtil.get(path).getName(0).toString();\n    }\n\n    private synchronized CompletableFuture<Pair<Snapshot, SocialFeed>> addToFriend(Snapshot v,\n                                                                                   Committer com,\n                                                                                   String friendName,\n                                                                                   ProcessedCaps current,\n                                                                                   CapsDiff diff) {\n        ProcessedCaps updated = current.add(diff);\n        currentCapBytesProcessed.put(friendName, updated);\n        List<CapabilityWithPath> newCaps = diff.getNewCaps();\n        List<SharedItem> forFeed = newCaps.stream()\n                .map(c -> new SharedItem(c.cap, extractOwner(c.path), friendName, c.path))\n                .collect(Collectors.toList());\n        return addToFeed(v, com, forFeed);\n    }\n\n    private synchronized CompletableFuture<Pair<Snapshot, SocialFeed>> addToFeed(Snapshot v,\n                                                                                 Committer c,\n                                                                                 List<SharedItem> newItems) {\n        if (newItems.isEmpty())\n            return setVersion(v).thenApply(x -> new Pair<>(v, this));\n        return mergeInComments(newItems, v, c).thenCompose(v2 -> {\n            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n            for (SharedItem item : newItems) {\n                try {\n                    bout.write(item.serialize());\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n            byte[] data = bout.toByteArray();\n            return Futures.asyncExceptionally(() -> appendToFeedAndCommitState(data, newItems.size(), v2.left, c),\n                    t -> ensureFeedUptodate().thenCompose(x -> appendToFeedAndCommitState(data, newItems.size(), v2.left, c)))\n                    .thenApply(s -> new Pair<>(s, this));\n        });\n    }\n\n    private synchronized CompletableFuture<Snapshot> appendToFeedAndCommitState(byte[] data, int records, Snapshot s, Committer c) {\n        PublicKeyHash owner = context.signer.publicKeyHash;\n        // use a buffered network to make this atomic across multiple files\n        return dataDir.getUpdated(s, network).thenCompose(updated ->\n                updated.getChild(FEED_FILE, crypto.hasher, network).thenCompose(feedOpt -> {\n                    if (feedOpt.isEmpty())\n                        return updated.uploadFileSection(updated.version, c, FEED_FILE, AsyncReader.build(data),\n                                false, 0, data.length, Optional.empty(), false, false,\n                                false, network, crypto, () -> false, x -> {},\n                                crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH),\n                                Optional.empty(),  Optional.of(Bat.random(crypto.random)), updated.mirrorBatId());\n                    if (feedOpt.get().getSize() != feedSizeBytes)\n                        throw new IllegalStateException(\"Feed size incorrect!\");\n                    return feedOpt.get().append(data, network, crypto, c, x -> {});\n                })).thenCompose(s2 -> {\n            feedSizeRecords += records;\n            feedSizeBytes += data.length;\n            byte[] raw = new FeedState(lastSeenIndex, feedSizeRecords, feedSizeBytes, currentCapBytesProcessed).serialize();\n            return stateFile.overwriteFile(AsyncReader.build(raw), raw.length, network, crypto, x -> {}, s2, c);\n        }).thenCompose(this::setVersion);\n    }\n\n    private CompletableFuture<Snapshot> setVersion(Snapshot s) {\n        return dataDir.getUpdated(s, network).thenApply(u -> {\n            this.dataDir = u;\n            return true;\n        }).thenCompose(x -> this.stateFile.getUpdated(s, network).thenApply(us -> {\n            this.stateFile = us;\n            return s;\n        }));\n    }\n\n    private CompletableFuture<Boolean> ensureFeedUptodate() {\n        return getUpdatedState(dataDir).thenApply(p -> {\n            this.dataDir = p.left;\n            this.feedSizeBytes = p.right.feedSizeBytes;\n            this.feedSizeRecords = p.right.feedSizeRecords;\n            this.lastSeenIndex = p.right.lastSeenIndex;\n            return true;\n        });\n    }\n\n    public static CompletableFuture<SocialFeed> load(FileWrapper dataDir, UserContext context) {\n        return dataDir.getChild(FEED_STATE, context.crypto.hasher, context.network)\n                .thenCompose(fopt -> {\n                    if (fopt.isEmpty())\n                        throw new IllegalStateException(\"Social feed state file not present!\");\n                    return Serialize.readFully(fopt.get(), context.crypto, context.network)\n                            .thenApply(arr -> FeedState.fromCbor(CborObject.fromByteArray(arr)))\n                            .thenApply(s -> new SocialFeed(dataDir, fopt.get(), s, context));\n                });\n    }\n\n    private CompletableFuture<Pair<FileWrapper, FeedState>> getUpdatedState(FileWrapper dataDir) {\n        return dataDir.getUpdated(network)\n                .thenCompose(updatedDataDir -> updatedDataDir.getChild(FEED_STATE, context.crypto.hasher, context.network)\n                .thenCompose(fopt -> {\n                    if (fopt.isEmpty())\n                        throw new IllegalStateException(\"Social feed state file not present!\");\n                    return Serialize.readFully(fopt.get(), context.crypto, context.network)\n                            .thenApply(arr -> new Pair<>(updatedDataDir, FeedState.fromCbor(CborObject.fromByteArray(arr))));\n                }));\n    }\n\n    public static CompletableFuture<SocialFeed> create(UserContext c) {\n        return c.getUserRoot()\n                .thenCompose(home -> home.getOrMkdirs(PathUtil.get(UserContext.FEED_DIR_NAME), c.network, true, c.mirrorBatId(), c.crypto))\n                .thenCompose(feedDir -> {\n                    FeedState empty = new FeedState(0, 0, 0L, Collections.emptyMap());\n                    byte[] rawEmpty = empty.serialize();\n                    return feedDir.uploadAndReturnFile(FEED_STATE, AsyncReader.build(rawEmpty), rawEmpty.length,\n                            false, feedDir.mirrorBatId(), c.network, c.crypto)\n                            .thenApply(stateFile -> new SocialFeed(feedDir, stateFile, empty, c))\n                            .thenCompose(SocialFeed::update);\n                });\n    }\n\n    private static class FeedState implements Cborable {\n        public final int lastSeenIndex, feedSizeRecords;\n        public final long feedSizeBytes;\n        public final Map<String, ProcessedCaps> currentCapBytesProcessed;\n\n        public FeedState(int lastSeenIndex, int feedSizeRecords, long feedSizeBytes, Map<String, ProcessedCaps> currentCapBytesProcessed) {\n            this.lastSeenIndex = lastSeenIndex;\n            this.feedSizeRecords = feedSizeRecords;\n            this.feedSizeBytes = feedSizeBytes;\n            this.currentCapBytesProcessed = currentCapBytesProcessed;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"s\", new CborObject.CborLong(lastSeenIndex));\n            state.put(\"r\", new CborObject.CborLong(feedSizeRecords));\n            state.put(\"b\", new CborObject.CborLong(feedSizeBytes));\n            SortedMap<String, Cborable> processed = new TreeMap<>();\n            for (Map.Entry<String, ProcessedCaps> e : currentCapBytesProcessed.entrySet()) {\n                processed.put(e.getKey(), e.getValue().toCbor());\n            }\n            state.put(\"p\", CborObject.CborMap.build(processed));\n            return CborObject.CborMap.build(state);\n        }\n\n        public static FeedState fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor for FeedState! \" + cbor);\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n            int lastSeenIndex = (int) m.getLong(\"s\");\n            int feedSizeRecords = (int) m.getLong(\"r\");\n            long feedSizeBytes = m.getLong(\"b\");\n            Map<String, ProcessedCaps> processedBytes = ((CborObject.CborMap)m.get(\"p\"))\n                    .toMap(c -> ((CborObject.CborString) c).value, ProcessedCaps::fromCbor);\n            return new FeedState(lastSeenIndex, feedSizeRecords, feedSizeBytes, processedBytes);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/social/SocialNetwork.java",
    "content": "package peergos.shared.social;\n\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.concurrent.*;\n\npublic interface SocialNetwork {\n\n    int MAX_PENDING_FOLLOWERS = 100;\n\n    /** Send a follow request to the target public key\n     *\n     * @param target The public identity key hash of the target user\n     * @param encryptedPermission The encrypted follow request\n     * @return True if successful\n     */\n    CompletableFuture<Boolean> sendFollowRequest(PublicKeyHash target, byte[] encryptedPermission);\n\n    /**\n     *\n     * @param owner The public identity key hash of user who's pending follow requests are being retrieved\n     * @param signedTime The current time signed by the owner\n     * @return all the pending follow requests for the given user\n     */\n    CompletableFuture<byte[]> getFollowRequests(PublicKeyHash owner, byte[] signedTime);\n\n    /** Delete a follow request for a given public key\n     *\n     * @param owner The public identity key hash of user who's follow request is being deleted\n     * @param data The original follow request data to delete, signed by the owner\n     * @return True if successful\n     */\n    CompletableFuture<Boolean> removeFollowRequest(PublicKeyHash owner, byte[] data);\n}\n"
  },
  {
    "path": "src/peergos/shared/social/SocialNetworkProxy.java",
    "content": "package peergos.shared.social;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.concurrent.*;\n\npublic interface SocialNetworkProxy extends SocialNetwork {\n\n\n    /** Send a follow request to the target public key\n     *\n     * @param target\n     * @param encryptedPermission\n     * @return\n     */\n    CompletableFuture<Boolean> sendFollowRequest(Multihash targetServerId, PublicKeyHash target, byte[] encryptedPermission);\n\n    /**\n     *\n     * @param owner\n     * @return all the pending follow requests for the given public key\n     */\n    CompletableFuture<byte[]> getFollowRequests(Multihash targetServerId, PublicKeyHash owner, byte[] signedTime);\n\n    /** Delete a follow request for a given public key\n     *\n     * @param owner\n     * @param data\n     * @return\n     */\n    CompletableFuture<Boolean> removeFollowRequest(Multihash targetServerId, PublicKeyHash owner, byte[] data);\n}\n"
  },
  {
    "path": "src/peergos/shared/social/SocialPost.java",
    "content": "package peergos.shared.social;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.display.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.fs.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n@JsType\npublic class SocialPost implements Cborable {\n\n    /** This enum describes the audience that a post is allowed to be shared with.\n     *\n     */\n    @JsType\n    public enum Resharing {\n        Author,\n        Friends,\n        Followers,\n        Public\n    }\n\n    public final String author;\n    public final List<? extends Content> body;\n    public final LocalDateTime postTime;\n    public final Resharing shareTo;\n    public final Optional<FileRef> parent;\n    public final List<SocialPost> previousVersions;\n    // this is excluded from hash calculation when replying\n    public final List<FileRef> comments;\n\n    @JsConstructor\n    public SocialPost(String author,\n                      List<? extends Content> body,\n                      LocalDateTime postTime,\n                      Resharing shareTo,\n                      Optional<FileRef> parent,\n                      List<SocialPost> previousVersions,\n                      List<FileRef> comments) {\n        this.author = author;\n        this.body = body;\n        this.postTime = postTime;\n        this.shareTo = shareTo;\n        this.parent = parent;\n        this.previousVersions = previousVersions;\n        this.comments = comments;\n    }\n\n    @JsMethod\n    public List<FileRef> references() {\n        return body.stream()\n                .flatMap(c -> c.reference().stream())\n                .collect(Collectors.toList());\n    }\n\n    public static SocialPost createInitialPost(String author, List<? extends Content> body, Resharing resharing) {\n        return new SocialPost(author, body, LocalDateTime.now(), resharing,\n                Optional.empty(), Collections.emptyList(), Collections.emptyList());\n    }\n\n    public static SocialPost createComment(FileRef parent, Resharing fromParent, String author, List<? extends Content> body) {\n        return new SocialPost(author, body, LocalDateTime.now(), fromParent,\n                Optional.of(parent), Collections.emptyList(), Collections.emptyList());\n    }\n\n    public SocialPost edit(List<? extends Content> body,\n                           LocalDateTime postTime) {\n        ArrayList<SocialPost> versions = new ArrayList<>(previousVersions);\n        versions.add(this);\n        return new SocialPost(author, body, postTime, shareTo, parent, versions, comments);\n    }\n\n    /** adding references to comments does not change the version of this comment (the hash ignores the comment refs)\n     *\n     * @param comment\n     * @return\n     */\n    public SocialPost addComment(FileRef comment) {\n        ArrayList<FileRef> updatedComments = new ArrayList<>(comments);\n        if (! comments.contains(comment))\n            updatedComments.add(comment);\n        return new SocialPost(author, body, postTime, shareTo, parent, previousVersions, updatedComments);\n    }\n\n    public SocialPost addComments(List<FileRef> newComments) {\n        ArrayList<FileRef> updatedComments = new ArrayList<>(comments);\n        for (FileRef comment : newComments) {\n            if (!updatedComments.contains(comment))\n                updatedComments.add(comment);\n        }\n        return new SocialPost(author, body, postTime, shareTo, parent, previousVersions, updatedComments);\n    }\n\n    private byte[] serializeWithoutComments() {\n        return new SocialPost(author, body, postTime, shareTo, parent,\n                previousVersions, Collections.emptyList()).serialize();\n    }\n\n    public CompletableFuture<Multihash> contentHash(Hasher h) {\n        return h.bareHash(serializeWithoutComments());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"a\", new CborObject.CborString(author));\n        state.put(\"b\", new CborObject.CborList(body));\n        state.put(\"t\", new CborObject.CborLong(postTime.toEpochSecond(ZoneOffset.UTC)));\n        state.put(\"s\", new CborObject.CborString(shareTo.name()));\n        parent.ifPresent(r -> state.put(\"p\", r));\n        if (! previousVersions.isEmpty())\n            state.put(\"v\", new CborObject.CborList(previousVersions));\n        if (! comments.isEmpty())\n            state.put(\"d\", new CborObject.CborList(comments));\n\n        List<CborObject> withMimeType = new ArrayList<>();\n        withMimeType.add(new CborObject.CborLong(MimeTypes.CBOR_PEERGOS_POST_INT));\n        withMimeType.add(CborObject.CborMap.build(state));\n\n        return new CborObject.CborList(withMimeType);\n    }\n\n    public static SocialPost fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborList withMimeType = (CborObject.CborList) cbor;\n        long mimeType = withMimeType.getLong(0);\n        if (mimeType != MimeTypes.CBOR_PEERGOS_POST_INT)\n            throw new IllegalStateException(\"Invalid mimetype for SocialPost: \" + mimeType);\n\n        CborObject.CborMap m = withMimeType.get(1, c -> (CborObject.CborMap)c);\n\n        String author = m.getString(\"a\");\n        List<Content> body = m.getList(\"b\", Content::fromCbor);\n        LocalDateTime postTime = m.get(\"t\", c -> LocalDateTime.ofEpochSecond(((CborObject.CborLong)c).value, 0, ZoneOffset.UTC));\n        Resharing shareTo = Resharing.valueOf(m.getString(\"s\"));\n        Optional<FileRef> parent = m.getOptional(\"p\", FileRef::fromCbor);\n        List<SocialPost> previousVersions = m.getList(\"v\", SocialPost::fromCbor);\n        List<FileRef> comments = m.getList(\"d\", FileRef::fromCbor);\n\n        return new SocialPost(author, body, postTime, shareTo, parent, previousVersions, comments);\n    }\n\n    public static class MutableRef implements Cborable {\n        public final String path;\n        public final AbsoluteCapability cap;\n\n        @JsConstructor\n        public MutableRef(String path, AbsoluteCapability cap) {\n            this.path = path;\n            this.cap = cap;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"p\", new CborObject.CborString(path));\n            state.put(\"c\", cap);\n            return CborObject.CborMap.build(state);\n        }\n\n        public static MutableRef fromCbor(Cborable cbor) {\n            if (!(cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n            String path = m.getString(\"p\");\n            AbsoluteCapability cap = m.get(\"c\", AbsoluteCapability::fromCbor);\n            return new MutableRef(path, cap);\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            MutableRef that = (MutableRef) o;\n            return path.equals(that.path) && cap.equals(that.cap);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(path, cap);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/BlockCache.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface BlockCache {\n\n    CompletableFuture<Boolean> put(Cid hash, byte[] data);\n\n    CompletableFuture<Optional<byte[]>> get(Cid hash);\n\n    boolean hasBlock(Cid hash);\n\n    CompletableFuture<Boolean> clear();\n\n    long getMaxSize();\n\n    void setMaxSize(long maxSizeBytes);\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/BlockMirrorCap.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.storage.auth.*;\n\nimport java.util.*;\n\npublic class BlockMirrorCap implements Cborable {\n\n    public final Cid hash;\n    public final Optional<BatWithId> bat;\n\n    public BlockMirrorCap(Cid hash, Optional<BatWithId> bat) {\n        this.hash = hash;\n        this.bat = bat;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"h\", new CborObject.CborByteArray(hash.toBytes()));\n        bat.ifPresent(b -> state.put(\"b\", b));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static BlockMirrorCap fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for BlockMirrorCap: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new BlockMirrorCap(m.get(\"h\", c -> Cid.cast(((CborObject.CborByteArray)c).value)),\n                m.getOptional(\"b\", BatWithId::fromCbor));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/BlockStoreProperties.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class BlockStoreProperties implements Cborable {\n    public final boolean directWrites, publicReads, authedReads;\n    public final Optional<String> basePublicReadUrl;\n    public final Optional<String> baseAuthedUrl;\n\n    public BlockStoreProperties(boolean directWrites,\n                                boolean publicReads,\n                                boolean authedReads,\n                                Optional<String> basePublicReadUrl,\n                                Optional<String> baseAuthedUrl) {\n        this.directWrites = directWrites;\n        this.publicReads = publicReads;\n        this.authedReads = authedReads;\n        this.basePublicReadUrl = basePublicReadUrl;\n        this.baseAuthedUrl = baseAuthedUrl;\n    }\n\n    public boolean useDirectBlockStore() {\n        return directWrites || publicReads;\n    }\n\n    public static BlockStoreProperties empty() {\n        return new BlockStoreProperties(false, false, false, Optional.empty(), Optional.empty());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> props = new TreeMap<>();\n        props.put(\"w\", new CborObject.CborBoolean(directWrites));\n        props.put(\"pr\", new CborObject.CborBoolean(publicReads));\n        props.put(\"ar\", new CborObject.CborBoolean(authedReads));\n        basePublicReadUrl.ifPresent(base -> props.put(\"b\", new CborObject.CborString(base)));\n        baseAuthedUrl.ifPresent(base -> props.put(\"ba\", new CborObject.CborString(base)));\n        return CborObject.CborMap.build(props);\n    }\n\n    public static BlockStoreProperties fromCbor(Cborable cbor) {\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        Optional<String> basePublic = map.getOptional(\"b\", c -> ((CborObject.CborString)c).value);\n        Optional<String> baseAuthed = map.getOptional(\"ba\", c -> ((CborObject.CborString)c).value);\n        boolean directWrites = map.getBoolean(\"w\");\n        boolean publicReads = map.getBoolean(\"pr\");\n        boolean authedReads = map.getBoolean(\"ar\");\n        return new BlockStoreProperties(directWrites, publicReads, authedReads, basePublic, baseAuthed);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/BlockWriteGroup.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.util.List;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport java.util.stream.Collectors;\n\npublic class BlockWriteGroup implements Cborable {\n\n    public final List<byte[]> blocks, signatures;\n\n    public BlockWriteGroup(List<byte[]> blocks, List<byte[]> signatures) {\n        if (blocks.size() != signatures.size())\n            throw new IllegalArgumentException(\"Different number of of blocks and signatures! \" + blocks.size() + \" != \" + signatures.size());\n        this.blocks = blocks;\n        this.signatures = signatures;\n    }\n\n    @Override\n    @SuppressWarnings(\"unusable-by-js\")\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"b\", new CborObject.CborList(blocks.stream().map(CborObject.CborByteArray::new).collect(Collectors.toList())));\n        state.put(\"s\", new CborObject.CborList(signatures.stream().map(CborObject.CborByteArray::new).collect(Collectors.toList())));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static BlockWriteGroup fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for FileProperties! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        List<byte[]> blocks = m.getList(\"b\", c -> ((CborObject.CborByteArray)c).value);\n        List<byte[]> signatures = m.getList(\"s\", c -> ((CborObject.CborByteArray)c).value);\n        return new BlockWriteGroup(blocks, signatures);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/BufferedStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class BufferedStorage extends DelegatingStorage {\n\n    private final Map<Cid, OpLog.BlockWrite> storage = new LinkedHashMap<>();\n    private final ContentAddressedStorage target;\n    private final Hasher hasher;\n\n    public BufferedStorage(ContentAddressedStorage target, Hasher hasher) {\n        super(target);\n        if (target instanceof BufferedStorage)\n            throw new IllegalStateException(\"Nested BufferedStorage!\");\n        this.target = target;\n        this.hasher = hasher;\n    }\n\n    public boolean hasBufferedBlock(Cid c) {\n        synchronized (storage) {\n            return storage.containsKey(c);\n        }\n    }\n\n    public boolean isEmpty() {\n        synchronized (storage) {\n            return storage.isEmpty();\n        }\n    }\n\n    public ContentAddressedStorage target() {\n        return target;\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        TransactionId tid = new TransactionId(Long.toString(System.currentTimeMillis()));\n        return CompletableFuture.completedFuture(tid);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return CompletableFuture.completedFuture(true);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner,\n                                                          Cid root,\n                                                          List<ChunkMirrorCap> caps,\n                                                          Optional<Cid> committedRoot) {\n        if (isEmpty() || ! hasBufferedBlock(root))\n            return Futures.asyncExceptionally(\n                    () -> target.getChampLookup(owner, root, caps, committedRoot),\n                    t -> getChampRoot(committedRoot, root, owner, this)\n                            .thenCompose(updatedRoot -> target.getChampLookup(owner, updatedRoot, caps, Optional.empty()))\n                    );\n        // If we are in a write transaction, first try local traversal for all caps using only\n        // buffered blocks (no HTTP). Caps that can't be resolved locally are then batched into\n        // a single remote call instead of making one HTTP call per cap.\n        List<CompletableFuture<Optional<List<byte[]>>>> localAttempts = caps.stream()\n                .map(cap -> tryLocalChampLookup(owner, root, cap.mapKey, cap.bat))\n                .collect(Collectors.toList());\n        return Futures.combineAllInOrder(localAttempts)\n                .thenCompose(localResults -> {\n                    List<byte[]> localBlocks = new ArrayList<>();\n                    List<ChunkMirrorCap> remoteCaps = new ArrayList<>();\n                    for (int i = 0; i < localResults.size(); i++) {\n                        if (localResults.get(i).isPresent())\n                            localBlocks.addAll(localResults.get(i).get());\n                        else\n                            remoteCaps.add(caps.get(i));\n                    }\n                    if (remoteCaps.isEmpty())\n                        return Futures.of(localBlocks);\n                    // Single batch HTTP call for all caps that couldn't be resolved from the buffer\n                    return Futures.asyncExceptionally(\n                            () -> getChampRoot(committedRoot, root, owner, this)\n                                    .thenCompose(champRoot -> target.getChampLookup(owner, champRoot, remoteCaps, Optional.empty())),\n                            t -> target.getChampLookup(owner, root, remoteCaps, committedRoot)\n                    ).thenApply(remoteBlocks -> {\n                        List<byte[]> all = new ArrayList<>(localBlocks);\n                        all.addAll(remoteBlocks);\n                        // The remote call used committedChampRoot (not root), so the buffered\n                        // root block itself is absent from remoteBlocks. Include it so that any\n                        // caller building a LocalRamStorage from these blocks can serve\n                        // ChampWrapper.create(root, ..., fromBlocks) without \"Champ root not present\".\n                        synchronized (storage) {\n                            OpLog.BlockWrite rootWrite = storage.get(root);\n                            if (rootWrite != null)\n                                all.add(rootWrite.block);\n                        }\n                        return all;\n                    });\n                });\n    }\n\n    /** Try a CHAMP lookup for a single key using only buffered blocks — no HTTP fallback.\n     *  Returns Optional.empty() if any required block is absent from the buffer. */\n    private CompletableFuture<Optional<List<byte[]>>> tryLocalChampLookup(PublicKeyHash owner, Cid root,\n                                                                           byte[] champKey, Optional<BatWithId> bat) {\n        BlockCache bufferOnlyCache = new BlockCache() {\n            final Map<Cid, byte[]> localCache = new HashMap<>();\n\n            @Override\n            public CompletableFuture<Boolean> put(Cid hash, byte[] data) {\n                localCache.put(hash, data);\n                return Futures.of(true);\n            }\n\n            @Override\n            public CompletableFuture<Optional<byte[]>> get(Cid hash) {\n                synchronized (storage) {\n                    return Futures.of(Optional.ofNullable(storage.get(hash))\n                            .map(b -> b.block)\n                            .or(() -> Optional.ofNullable(localCache.get(hash))));\n                }\n            }\n\n            @Override\n            public boolean hasBlock(Cid hash) {\n                synchronized (storage) {\n                    return storage.containsKey(hash) || localCache.containsKey(hash);\n                }\n            }\n\n            @Override\n            public CompletableFuture<Boolean> clear() {\n                throw new IllegalStateException(\"Unimplemented!\");\n            }\n\n            @Override\n            public long getMaxSize() { return 0; }\n\n            @Override\n            public void setMaxSize(long maxSizeBytes) {}\n        };\n        LocalOnlyStorage pureLocal = new LocalOnlyStorage(bufferOnlyCache,\n                () -> Futures.errored(new RuntimeException(\"block not in buffer\")), hasher);\n        CachingStorage cache = new CachingStorage(pureLocal, 100, 1024 * 1024);\n        return Futures.asyncExceptionally(\n                () -> Futures.asyncExceptionally(\n                        () -> ChampWrapper.create(owner, root, Optional.empty(), x -> Futures.of(x.data), cache, hasher, c -> (CborObject.CborMerkleLink) c),\n                        t -> getChampRoot(Optional.empty(), root, owner, pureLocal)\n                                .thenCompose(champRoot -> ChampWrapper.create(owner, champRoot, Optional.empty(), x -> Futures.of(x.data), cache, hasher, c -> (CborObject.CborMerkleLink) c))\n                )\n                .thenCompose(tree -> tree.get(champKey))\n                .thenApply(c -> c.map(x -> x.target).map(MaybeMultihash::of).orElse(MaybeMultihash.empty()))\n                .thenCompose(btreeValue -> {\n                    if (btreeValue.isPresent())\n                        return cache.get(owner, (Cid) btreeValue.get(), bat);\n                    return Futures.of(Optional.empty());\n                })\n                .thenApply(x -> Optional.of(new ArrayList<>(cache.getCached()))),\n                t -> Futures.of(Optional.empty()));\n    }\n\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner,\n                                                          Cid root,\n                                                          byte[] champKey,\n                                                          Optional<BatWithId> bat,\n                                                          Optional<Cid> committedRoot,\n                                                          Hasher hasher) {\n        BlockCache ramBlockCache = new BlockCache() {\n            Map<Cid, byte[]> localCache = new HashMap<>();\n\n            @Override\n            public CompletableFuture<Boolean> put(Cid hash, byte[] data) {\n                localCache.put(hash, data);\n                return Futures.of(true);\n            }\n\n            @Override\n            public CompletableFuture<Optional<byte[]>> get(Cid hash) {\n                synchronized (storage) {\n                    return Futures.of(Optional.ofNullable(storage.get(hash))\n                            .map(b -> b.block)\n                            .or(() -> Optional.ofNullable(localCache.get(hash))));\n                }\n            }\n\n            @Override\n            public boolean hasBlock(Cid hash) {\n                synchronized (storage) {\n                    return storage.containsKey(hash) || localCache.containsKey(hash);\n                }\n            }\n\n            @Override\n            public CompletableFuture<Boolean> clear() {\n                throw new IllegalStateException(\"Unimplemented!\");\n            }\n\n            @Override\n            public long getMaxSize() {\n                return 0;\n            }\n\n            @Override\n            public void setMaxSize(long maxSizeBytes) {\n\n            }\n        };\n        LocalOnlyStorage localStorage = new LocalOnlyStorage(ramBlockCache,\n                () -> committedRoot.isPresent() ?\n                        get(owner, committedRoot.get(), Optional.empty())\n                                .thenApply(ropt -> ropt.map(WriterData::fromCbor).flatMap(wd -> wd.tree))\n                                .thenCompose(champRoot -> target.getChampLookup(owner, (Cid) champRoot.get(), Arrays.asList(new ChunkMirrorCap(champKey, bat)), Optional.empty())) :\n                        target.getChampLookup(owner, root, Arrays.asList(new ChunkMirrorCap(champKey, bat)), Optional.empty()), hasher);\n        CachingStorage cache = new CachingStorage(localStorage, 100, 1024 * 1024);\n        return Futures.asyncExceptionally(() -> Futures.asyncExceptionally(\n                                () -> ChampWrapper.create(owner, root, Optional.empty(), x -> Futures.of(x.data), cache, hasher, c -> (CborObject.CborMerkleLink) c),\n                                t -> getChampRoot(committedRoot, root, owner, target)\n                                        .thenCompose(champRoot -> ChampWrapper.create(owner, champRoot, Optional.empty(), x -> Futures.of(x.data), cache, hasher, c -> (CborObject.CborMerkleLink) c))\n                        )\n                        .thenCompose(tree -> tree.get(champKey))\n                        .thenApply(c -> c.map(x -> x.target).map(MaybeMultihash::of).orElse(MaybeMultihash.empty()))\n                        .thenCompose(btreeValue -> {\n                            if (btreeValue.isPresent())\n                                return Futures.asyncExceptionally(\n                                        () -> cache.get(owner, (Cid) btreeValue.get(), bat),\n                                        t -> target.get(owner, (Cid) btreeValue.get(), bat).thenCompose(res -> {\n                                            if (res.isPresent()) {\n                                                // add directly retrieved block to results\n                                                ramBlockCache.put((Cid) btreeValue.get(), res.get().serialize());\n                                                return cache.get(owner, (Cid) btreeValue.get(), bat);\n                                            }\n                                            return Futures.of(res);\n                                        })\n                                );\n                            return Futures.of(Optional.empty());\n                        }).thenApply(x -> new ArrayList<>(cache.getCached())),\n                t -> getChampRoot(committedRoot, root, owner, this)\n                        .thenCompose(champRoot -> target.getChampLookup(owner, champRoot, Arrays.asList(new ChunkMirrorCap(champKey, bat)), Optional.empty()))\n        );\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return put(writer, blocks, signedHashes, false,Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return put(writer, blocks, signatures, true, Optional.of(progressConsumer));\n    }\n\n    private CompletableFuture<List<Cid>> put(PublicKeyHash writer,\n                                             List<byte[]> blocks,\n                                             List<byte[]> signatures,\n                                             boolean isRaw,\n                                             Optional<ProgressConsumer<Long>> progressConsumer) {\n        return Futures.combineAllInOrder(IntStream.range(0, blocks.size())\n                .mapToObj(i -> hashToCid(blocks.get(i), isRaw)\n                        .thenApply(cid -> put(cid, new OpLog.BlockWrite(writer, signatures.get(i), blocks.get(i), isRaw, progressConsumer))))\n                .collect(Collectors.toList()));\n    }\n\n    private synchronized Cid put(Cid cid, OpLog.BlockWrite block) {\n        synchronized (storage) {\n            storage.put(cid, block);\n            if (cid.isRaw())\n                block.progressMonitor.ifPresent(m -> m.accept((long)block.block.length));\n        }\n        return cid;\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        return NetworkAccess.downloadFragments(owner, hashes, bats, this, h, monitor, spaceIncreaseFactor);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        synchronized (storage) {\n            OpLog.BlockWrite local = storage.get(hash);\n            if (local != null)\n                return Futures.of(Optional.of(local.block));\n        }\n        return target.getRaw(owner, hash, bat);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return getRaw(owner, hash, bat)\n                .thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<Cid> put(PublicKeyHash owner,\n                                      SigningPrivateKeyAndPublicHash writer,\n                                      byte[] block,\n                                      Hasher hasher,\n                                      TransactionId tid) {\n        // Do NOT do signature as this block will likely be GC'd before being committed, so we can delay calculating signatures until commit\n        return put(writer.publicKeyHash, Collections.singletonList(block), Collections.singletonList(new byte[0]), false, Optional.empty())\n                .thenApply(hashes -> hashes.get(0));\n    }\n\n    public CompletableFuture<Map<Cid, OpLog.BlockWrite>> signBlocks(Map<PublicKeyHash, SigningPrivateKeyAndPublicHash> writers) {\n        synchronized (storage) {\n            List<Pair<Cid, OpLog.BlockWrite>> writes = storage.entrySet()\n                    .stream()\n                    .map(e -> new Pair<>(e.getKey(), e.getValue()))\n                    .collect(Collectors.toList());\n            return Futures.combineAllInOrder(writes.stream()\n                            .map(w -> {\n                                OpLog.BlockWrite block = w.right;\n                                return (block.signature.length > 0 ?\n                                        Futures.of(block.signature) :\n                                        writers.get(block.writer).secret.signMessage(w.left.getHash()))\n                                        .thenApply(sig -> new Pair<>(w.left, new OpLog.BlockWrite(block.writer,\n                                                sig,\n                                                block.block, block.isRaw, block.progressMonitor)));\n                            }).collect(Collectors.toList()))\n                    .thenApply(all -> {\n                        if (all.stream().map(p ->p.right).anyMatch(bw -> bw.signature.length == 0))\n                            throw new IllegalStateException(\"Blocks with empty signature!\");\n                        return all.stream()\n                                .collect(Collectors.toMap(p -> p.left, p -> p.right));\n                    });\n        }\n    }\n\n    public void gc(List<Cid> roots) {\n        synchronized (storage) {\n            List<Cid> all = new ArrayList<>(storage.keySet());\n            List<Boolean> reachable = new ArrayList<>();\n            for (int i = 0; i < all.size(); i++)\n                reachable.add(false);\n            for (Cid root : roots) {\n                markReachable(root, reachable, all, storage);\n            }\n            for (int i = 0; i < all.size(); i++) {\n                if (!reachable.get(i))\n                    storage.remove(all.get(i));\n            }\n        }\n    }\n\n    private static void markReachable(Cid current, List<Boolean> reachable, List<Cid> all, Map<Cid, OpLog.BlockWrite> storage) {\n        OpLog.BlockWrite block = storage.get(current);\n        if (block == null)\n            return;\n        int index = all.indexOf(current);\n        reachable.set(index, true);\n\n        if (current.isRaw())\n            return;\n        List<Multihash> links = CborObject.fromByteArray(block.block).links();\n        for (Multihash link : links) {\n            markReachable((Cid)link, reachable, all, storage);\n        }\n    }\n\n    public List<Pair<BufferedPointers.WriterUpdate, Optional<CommittedWriterData>>> getAllWriterData(List<BufferedPointers.WriterUpdate> updates) {\n        synchronized (storage) {\n            return updates.stream()\n                    .map(u -> new Pair<>(u, u.currentHash.map(h -> new CommittedWriterData(u.currentHash,\n                            WriterData.fromCbor(CborObject.fromByteArray(storage.get(h).block)), u.currentSequence))))\n                    .collect(Collectors.toList());\n        }\n    }\n\n    /** Commit the blocks for a given writer\n     *\n     * @param owner\n     * @param writer\n     * @param tid\n     * @return\n     */\n    public CompletableFuture<Boolean> commit(PublicKeyHash owner,\n                                             PublicKeyHash writer,\n                                             TransactionId tid,\n                                             Map<Cid, OpLog.BlockWrite> signed) {\n        // write blocks in batches of up to 50 all in 1 transaction\n        List<OpLog.BlockWrite> forWriter = new ArrayList<>();\n        Set<Cid> toRemove = new HashSet<>();\n        synchronized (storage) {\n            for (Map.Entry<Cid, OpLog.BlockWrite> e : signed.entrySet()) {\n                if (!Objects.equals(e.getValue().writer, writer))\n                    continue;\n                forWriter.add(e.getValue());\n                toRemove.add(e.getKey());\n            }\n            toRemove.forEach(storage::remove);\n        }\n\n        int maxBlocksPerBatch = ContentAddressedStorage.MAX_BLOCK_AUTHS;\n        int maxCborBatchSize = 1024*1024;\n        List<List<OpLog.BlockWrite>> cborBatches = new ArrayList<>();\n        List<List<OpLog.BlockWrite>> rawBatches = new ArrayList<>();\n        List<List<OpLog.BlockWrite>> smallRawBatches = new ArrayList<>();\n\n        int cborSize = 0, rawcount = 0, smallRawCount = 0;\n        int smallBlockMax = DirectS3BlockStore.MAX_SMALL_BLOCK_SIZE;\n        if (! cborBatches.isEmpty() && ! cborBatches.get(cborBatches.size() - 1).isEmpty())\n            cborBatches.add(new ArrayList<>());\n        if (! rawBatches.isEmpty() && ! rawBatches.get(rawBatches.size() - 1).isEmpty())\n            rawBatches.add(new ArrayList<>());\n        if (! smallRawBatches.isEmpty() && ! smallRawBatches.get(rawBatches.size() - 1).isEmpty())\n            smallRawBatches.add(new ArrayList<>());\n        for (OpLog.BlockWrite val : forWriter) {\n            List<List<OpLog.BlockWrite>> batches = val.isRaw ?\n                    val.block.length < smallBlockMax ? smallRawBatches : rawBatches : cborBatches;\n            int count = val.isRaw ? val.block.length < smallBlockMax ? smallRawCount : rawcount : cborSize;\n            int maxBatchCount = val.isRaw ? maxBlocksPerBatch : maxCborBatchSize;\n            if (val.isRaw && count % maxBatchCount == 0)\n                batches.add(new ArrayList<>());\n            if (! val.isRaw && (cborBatches.isEmpty() || cborSize + val.block.length > maxCborBatchSize)) {\n                cborBatches.add(new ArrayList<>());\n                cborSize = 0;\n            }\n            batches.get(batches.size() - 1).add(val);\n            count = (count + 1) % maxBatchCount;\n            if (val.isRaw) {\n                if (val.block.length < smallBlockMax)\n                    smallRawCount = count;\n                else\n                    rawcount = count;\n            } else\n                cborSize += val.block.length;\n        }\n        return Futures.combineAllInOrder(Stream.concat(\n                                rawBatches.stream().map(bs -> new Pair<>(true, bs)),\n                                Stream.concat(\n                                        smallRawBatches.stream().map(bs -> new Pair<>(true, bs)),\n                                        cborBatches.stream().map(bs -> new Pair<>(false, bs))))\n                        .filter(p -> ! p.right.isEmpty())\n                        .map(p -> p.left ?\n                                target.putRaw(owner, writer,\n                                                p.right.stream().map(w -> w.signature).collect(Collectors.toList()),\n                                                p.right.stream().map(w -> w.block).collect(Collectors.toList()), tid, x-> {})\n                                        .thenApply(res -> {\n                                            p.right.stream().forEach(w ->  w.progressMonitor.ifPresent(m -> m.accept((long)w.block.length)));\n                                            return res;\n                                        }) :\n                                target.put(owner, writer,\n                                        p.right.stream().map(w -> w.signature).collect(Collectors.toList()),\n                                        p.right.stream().map(w -> w.block).collect(Collectors.toList()), tid))\n                        .collect(Collectors.toList()))\n                .thenApply(a -> true);\n    }\n\n    public BufferedStorage clone() {\n        return new BufferedStorage(target, hasher);\n    }\n\n    public BufferedStorage withStorage(Function<ContentAddressedStorage, ContentAddressedStorage> modifiedStorage) {\n        return new BufferedStorage(modifiedStorage.apply(target), hasher);\n    }\n\n    public synchronized void clear() {\n        storage.clear();\n    }\n\n    public int size() {\n        synchronized (storage) {\n            return storage.size();\n        }\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        synchronized (storage) {\n            if (!storage.containsKey(block))\n                return target.getSize(owner, block);\n            return CompletableFuture.completedFuture(Optional.of(storage.get(block).block.length));\n        }\n    }\n\n    public CompletableFuture<Cid> hashToCid(byte[] input, boolean isRaw) {\n        return hasher.hash(input, isRaw);\n    }\n\n    public int totalSize() {\n        synchronized (storage) {\n            return storage.values().stream().mapToInt(a -> a.block.length).sum();\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/CachingStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class CachingStorage extends DelegatingStorage {\n    private final ContentAddressedStorage target;\n    private final LRUCache<Multihash, byte[]> cache;\n    private final LRUCache<Multihash, CompletableFuture<Optional<CborObject>>> pending;\n    private final LRUCache<Multihash, CompletableFuture<Optional<byte[]>>> pendingRaw;\n    private final int maxValueSize, cacheSize;\n\n    public CachingStorage(ContentAddressedStorage target, int cacheSize, int maxValueSize) {\n        super(target);\n        this.target = target;\n        this.cache = new LRUCache<>(cacheSize);\n        this.maxValueSize = maxValueSize;\n        this.cacheSize = cacheSize;\n        this.pending = new LRUCache<>(100);\n        this.pendingRaw = new LRUCache<>(100);\n    }\n\n    public Collection<byte[]> getCached() {\n        synchronized (cache) {\n            return cache.values();\n        }\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return target.blockStoreProperties();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new CachingStorage(target.directToOrigin(), cacheSize, maxValueSize);\n    }\n\n    @Override\n    public void clearBlockCache() {\n        synchronized (cache) {\n            cache.clear();\n        }\n        target.clearBlockCache();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return target.put(owner, writer, signedHashes, blocks, tid)\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        if (block.length < maxValueSize) {\n                            synchronized (cache) {\n                                cache.put(res.get(i), block);\n                            }\n                        }\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        synchronized (cache) {\n            byte[] cached = cache.get(key);\n            if (cached != null)\n                return CompletableFuture.completedFuture(Optional.of(CborObject.fromByteArray(cached)));\n        }\n\n        CompletableFuture<Optional<CborObject>> pipe = new CompletableFuture<>();\n        synchronized (pending) {\n            CompletableFuture<Optional<CborObject>> inProgress = pending.get(key);\n            if (inProgress != null)\n                return inProgress;\n\n            pending.put(key, pipe);\n        }\n\n        CompletableFuture<Optional<CborObject>> result = new CompletableFuture<>();\n        target.get(owner, key, bat).thenAccept(cborOpt -> {\n            if (cborOpt.isPresent()) {\n                byte[] value = cborOpt.get().toByteArray();\n                if (value.length > 0 && value.length < maxValueSize) {\n                    synchronized (cache) {\n                        cache.put(key, value);\n                    }\n                }\n            }\n            synchronized (pending) {\n                pending.remove(key);\n            }\n            pipe.complete(cborOpt);\n            result.complete(cborOpt);\n        }).exceptionally(t -> {\n            synchronized (pending) {\n                pending.remove(key);\n            }\n            pipe.completeExceptionally(t);\n            result.completeExceptionally(t);\n            return null;\n        });\n        return result;\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return target.putRaw(owner, writer, signatures, blocks, tid, progressConsumer)\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        if (block.length < maxValueSize) {\n                            synchronized (cache) {\n                                cache.put(res.get(i), block);\n                            }\n                        }\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        synchronized (cache) {\n            byte[] cached = cache.get(key);\n            if (cached != null)\n                return CompletableFuture.completedFuture(Optional.of(cached));\n        }\n\n        synchronized (pendingRaw) {\n            CompletableFuture<Optional<byte[]>> inProgress = pendingRaw.get(key);\n            if (inProgress != null)\n                return inProgress;\n        }\n\n        CompletableFuture<Optional<byte[]>> pipe = new CompletableFuture<>();\n        synchronized (pendingRaw) {\n            pendingRaw.put(key, pipe);\n        }\n        return target.getRaw(owner, key, bat).thenApply(rawOpt -> {\n            if (rawOpt.isPresent()) {\n                byte[] value = rawOpt.get();\n                if (value.length > 0 && value.length < maxValueSize) {\n                    synchronized (cache) {\n                        cache.put(key, value);\n                    }\n                }\n            }\n            synchronized (pendingRaw) {\n                pendingRaw.remove(key);\n            }\n            pipe.complete(rawOpt);\n            return rawOpt;\n        }).exceptionally(t -> {\n            synchronized (pendingRaw) {\n                pendingRaw.remove(key);\n            }\n            pipe.completeExceptionally(t);\n            return Optional.empty();\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/CachingVerifyingStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class CachingVerifyingStorage extends DelegatingStorage {\n\n    private final ContentAddressedStorage target;\n    private final LRUCache<Multihash, byte[]> cache;\n    private final LRUCache<Multihash, CompletableFuture<Optional<CborObject>>> pending;\n    private final LRUCache<Multihash, CompletableFuture<Optional<byte[]>>> pendingRaw;\n    private final int maxValueSize, cacheSize;\n    private final List<Cid> nodeIds;\n    private final Hasher hasher;\n\n    public CachingVerifyingStorage(ContentAddressedStorage target, int maxValueSize, int cacheSize, List<Cid> nodeIds, Hasher hasher) {\n        super(target);\n        this.target = target;\n        this.cache =  new LRUCache<>(cacheSize);\n        this.pending = new LRUCache<>(100);\n        this.pendingRaw = new LRUCache<>(100);\n        this.maxValueSize = maxValueSize;\n        this.cacheSize = cacheSize;\n        this.nodeIds = nodeIds;\n        this.hasher = hasher;\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(nodeIds.get(nodeIds.size() - 1));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return Futures.of(nodeIds);\n    }\n\n    private <T> CompletableFuture<T> verify(byte[] data, Multihash claimed, Supplier<T> result) {\n        switch (claimed.type) {\n            case sha2_256:\n                return hasher.sha256(data)\n                        .thenApply(hash -> {\n                            Multihash computed = new Multihash(Multihash.Type.sha2_256, hash);\n                            if (claimed instanceof Cid)\n                                computed = Cid.build(((Cid) claimed).version, ((Cid) claimed).codec, computed);\n\n                            if (computed.equals(claimed))\n                                return result.get();\n\n                            throw new IllegalStateException(\"Incorrect hash! Are you under attack? Expected: \" + claimed + \" actual: \" + computed);\n                        });\n            case id:\n                if (Arrays.equals(data, claimed.getHash()))\n                    return Futures.of(result.get());\n                throw new IllegalStateException(\"Incorrect identity hash! This shouldn't ever  happen.\");\n            default: throw new IllegalStateException(\"Unimplemented hash algorithm: \" + claimed.type);\n        }\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return target.blockStoreProperties();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new CachingVerifyingStorage(target.directToOrigin(), cacheSize, maxValueSize, nodeIds, hasher);\n    }\n\n    @Override\n    public void clearBlockCache() {\n        synchronized (cache) {\n            cache.clear();\n        }\n        target.clearBlockCache();\n    }\n\n    private boolean cache(Multihash h, byte[] block) {\n        if (block.length < maxValueSize) {\n            synchronized (cache) {\n                cache.put(h, block);\n            }\n        }\n        return true;\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return target.getChampLookup(owner, root, caps, committedRoot)\n                .thenCompose(blocks -> Futures.combineAllInOrder(blocks.stream()\n                        .map(b -> hasher.hash(b, false)\n                                .thenApply(h -> cache(h, b)))\n                        .collect(Collectors.toList()))\n                        .thenApply(x -> blocks));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return target.put(owner, writer, signedHashes, blocks, tid)\n                .thenCompose(hashes -> Futures.combineAllInOrder(hashes.stream()\n                        .map(h -> verify(blocks.get(hashes.indexOf(h)), h, () -> h))\n                        .collect(Collectors.toList())))\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        cache(res.get(i), block);\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        synchronized (cache) {\n            byte[] cached = cache.get(key);\n            if (cached != null)\n                return CompletableFuture.completedFuture(Optional.of(CborObject.fromByteArray(cached)));\n        }\n\n        CompletableFuture<Optional<CborObject>> pipe = new CompletableFuture<>();\n        synchronized (pending) {\n            CompletableFuture<Optional<CborObject>> pend = pending.get(key);\n            if (pend != null)\n                return pend;\n\n            pending.put(key, pipe);\n        }\n\n        CompletableFuture<Optional<CborObject>> result = new CompletableFuture<>();\n        target.get(owner, key, bat)\n                .thenCompose(cborOpt -> cborOpt.map(cbor -> verify(cbor.toByteArray(), key, () -> cbor)\n                        .thenApply(Optional::of))\n                        .orElseGet(() -> Futures.of(Optional.empty())))\n                .thenAccept(cborOpt -> {\n                    if (cborOpt.isPresent()) {\n                        byte[] value = cborOpt.get().toByteArray();\n                        if (value.length > 0)\n                            cache(key, value);\n                    }\n                    synchronized (pending) {\n                        pending.remove(key);\n                    }\n                    pipe.complete(cborOpt);\n                    result.complete(cborOpt);\n                }).exceptionally(t -> {\n            synchronized (pending) {\n                pending.remove(key);\n            }\n            pipe.completeExceptionally(t);\n            result.completeExceptionally(t);\n            return null;\n        });\n        return result;\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return target.putRaw(owner, writer, signatures, blocks, tid, progressConsumer)\n                .thenCompose(hashes -> Futures.combineAllInOrder(hashes.stream()\n                        .map(h -> verify(blocks.get(hashes.indexOf(h)), h, () -> h))\n                        .collect(Collectors.toList())))\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        cache(res.get(i), block);\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        synchronized (cache) {\n            byte[] cached = cache.get(key);\n            if (cached != null)\n                return CompletableFuture.completedFuture(Optional.of(cached));\n        }\n\n        CompletableFuture<Optional<byte[]>> pipe = new CompletableFuture<>();\n        synchronized (pendingRaw) {\n            CompletableFuture<Optional<byte[]>> pend = pendingRaw.get(key);\n            if (pend != null)\n                return pend;\n\n            pendingRaw.put(key, pipe);\n        }\n        target.getRaw(owner, key, bat)\n                .thenCompose(arrOpt -> arrOpt.map(bytes -> verify(bytes, key, () -> bytes)\n                                .thenApply(Optional::of))\n                        .orElseGet(() -> Futures.of(Optional.empty())))\n                .thenApply(rawOpt -> {\n                    rawOpt = rawOpt.filter(b -> b.length > 0);\n                    if (rawOpt.isPresent()) {\n                        byte[] value = rawOpt.get();\n                        if (value.length > 0)\n                            cache(key, value);\n                    }\n                    synchronized (pendingRaw) {\n                        pendingRaw.remove(key);\n                    }\n                    pipe.complete(rawOpt);\n                    return rawOpt;\n                }).exceptionally(t -> {\n                    synchronized (pendingRaw) {\n                        pendingRaw.remove(key);\n                    }\n                    pipe.completeExceptionally(t);\n                    return null;\n                });\n        return pipe;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/CasException.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.JsConstructor;\n\npublic class CasException extends RuntimeException {\n\n    @JsConstructor\n    public CasException(String msg) {\n        super(msg);\n    }\n\n    public CasException(Object actualExisting, Object claimedExisting) {\n        this(\"CAS exception updating cryptree node. existing: \" + actualExisting + \", claimed: \" + claimedExisting);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/ChunkMirrorCap.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.io.ipfs.bases.Multibase;\nimport peergos.shared.storage.auth.BatWithId;\n\nimport java.util.Optional;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class ChunkMirrorCap implements Cborable {\n\n    public final byte[] mapKey;\n    public final Optional<BatWithId> bat;\n\n    public ChunkMirrorCap(byte[] mapKey, Optional<BatWithId> bat) {\n        this.mapKey = mapKey;\n        this.bat = bat;\n    }\n\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"m\", new CborObject.CborByteArray(mapKey));\n        bat.ifPresent(b -> state.put(\"b\", b));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ChunkMirrorCap fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for ChunkMirrorCap: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new ChunkMirrorCap(m.get(\"m\", c -> ((CborObject.CborByteArray)c).value),\n                m.getOptional(\"b\", BatWithId::fromCbor));\n    }\n\n    public String encodeToString() {\n        return Multibase.encode(Multibase.Base.Base58BTC, toCbor().toByteArray());\n    }\n\n    public static ChunkMirrorCap fromString(String encoded) {\n        return fromCbor(CborObject.fromByteArray(Multibase.decode(encoded)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/ContentAddressedStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.corenode.Proxy;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.io.ipfs.bases.Multibase;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.Function;\nimport java.util.stream.*;\n\npublic interface ContentAddressedStorage {\n\n    boolean DEBUG_GC = false;\n    int MAX_BLOCK_SIZE  = Fragment.MAX_LENGTH_WITH_BAT_PREFIX;\n    int MAX_BLOCK_AUTHS = 50;\n    int MAX_CHAMP_GETS = 20;\n\n    default CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return Futures.of(BlockStoreProperties.empty());\n    }\n\n    CompletableFuture<String> linkHost(PublicKeyHash owner);\n\n    /**\n     *  Clear any block caches\n     */\n    default void clearBlockCache() {}\n\n    /**\n     *\n     * @return an instance of the same type that doesn't do any cross domain requests\n     */\n    ContentAddressedStorage directToOrigin();\n\n    Optional<BlockCache> getBlockCache();\n\n    default CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n        return Futures.errored(new IllegalStateException(\"Unimplemented call!\"));\n    }\n\n    default CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                             PublicKeyHash writer,\n                                                             List<byte[]> signedHashes,\n                                                             List<Integer> blockSizes,\n                                                             List<List<BatId>> batIds,\n                                                             boolean isRaw,\n                                                             TransactionId tid) {\n        return Futures.errored(new IllegalStateException(\"Unimplemented call!\"));\n    }\n\n    default CompletableFuture<Cid> put(PublicKeyHash owner,\n                                       SigningPrivateKeyAndPublicHash writer,\n                                       byte[] block,\n                                       Hasher hasher,\n                                       TransactionId tid) {\n        return hasher.sha256(block)\n                .thenCompose(hash -> writer.secret.signMessage(hash))\n                .thenCompose(sig -> put(owner, writer.publicKeyHash, sig, block, tid));\n    }\n\n    default CompletableFuture<Cid> put(PublicKeyHash owner,\n                                       PublicKeyHash writer,\n                                       byte[] signature,\n                                       byte[] block,\n                                       TransactionId tid) {\n        return put(owner, writer, Collections.singletonList(signature), Collections.singletonList(block), tid)\n                .thenApply(hashes -> hashes.get(0));\n    }\n\n    default CompletableFuture<Cid> putRaw(PublicKeyHash owner,\n                                          PublicKeyHash writer,\n                                          byte[] signature,\n                                          byte[] block,\n                                          TransactionId tid,\n                                          ProgressConsumer<Long> progressConsumer) {\n        return putRaw(owner, writer, Collections.singletonList(signature), Collections.singletonList(block), tid, progressConsumer)\n                .thenApply(hashes -> hashes.get(0));\n    }\n\n    /**\n     *\n     * @return The identity (hash of the public key) of this server\n     */\n    CompletableFuture<Cid> id();\n\n    /**\n     *\n     * @return All previous and current identities (hash of the public key) of this server\n     */\n    CompletableFuture<List<Cid>> ids();\n\n    /**\n     *\n     * @param owner\n     * @return A new transaction id that can be used to group writes together and protect them from being garbage\n     * collected before they have been pinned.\n     */\n    CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner);\n\n    /**\n     * Release all associated objects from this transaction to allow them to be garbage collected if they haven't been\n     * pinned.\n     * @param owner\n     * @param tid\n     * @return\n     */\n    CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid);\n\n    /**\n     *\n     * @param owner The owner of these blocks of data\n     * @param writer The public signing key authorizing these writes, which must be owned by the owner key\n     * @param signedHashes The signatures of the sha256 of each block being written (by the writer)\n     * @param blocks The blocks to write\n     * @param tid The transaction to group these writes under\n     * @return\n     */\n    CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                     PublicKeyHash writer,\n                                     List<byte[]> signedHashes,\n                                     List<byte[]> blocks,\n                                     TransactionId tid);\n\n\n    /**\n     * @param owner\n     * @param hash\n     * @return The data with the requested hash, deserialized into cbor, or Optional.empty() if no object can be found\n     */\n    CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat);\n\n    /**\n     * Write a block of data that is just raw bytes, not ipld structured cbor\n     * @param owner\n     * @param writer\n     * @param signedHashes\n     * @param blocks\n     * @param tid\n     * @param progressCounter\n     * @return\n     */\n    CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                        PublicKeyHash writer,\n                                        List<byte[]> signedHashes,\n                                        List<byte[]> blocks,\n                                        TransactionId tid,\n                                        ProgressConsumer<Long> progressCounter);\n\n    /**\n     * Get a block of data that is not in ipld cbor format, just raw bytes\n     *\n     * @param owner\n     * @param hash\n     * @return\n     */\n    CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat);\n\n    CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner,\n                                                   Cid root,\n                                                   List<ChunkMirrorCap> caps,\n                                                   Optional<Cid> committedRoot);\n\n    default CompletableFuture<Cid> getChampRoot(Optional<Cid> committedRoot,\n                                                Cid root,\n                                                PublicKeyHash owner,\n                                                ContentAddressedStorage target) {\n        return committedRoot.isPresent() ?\n                target.get(owner, committedRoot.get(), Optional.empty())\n                        .thenApply(ropt -> ropt.map(WriterData::fromCbor)\n                                .flatMap(wd -> wd.tree.map(t -> (Cid)t))\n                                .orElse(root)) :\n                Futures.of(root);\n    }\n\n    default CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner,\n                                                           Cid root,\n                                                           List<ChunkMirrorCap> caps,\n                                                           Optional<Cid> committedRoot,\n                                                           Hasher hasher) {\n        // Cache must hold all path nodes for every cap: depth * caps + value blocks.\n        // With BIT_WIDTH=3 (branching=8) and up to MAX_CHAMP_GETS caps, depth can reach ~10,\n        // so caps.size() * 16 gives a safe upper bound with room to spare.\n        CachingStorage cache = new CachingStorage(this, Math.max(100, caps.size() * 16), 1024 * 1024);\n        return Futures.combineAll(caps.stream().map(cap -> Futures.asyncExceptionally(\n                                () -> ChampWrapper.create(owner, root, Optional.empty(), x -> Futures.of(x.data), cache, hasher, c -> (CborObject.CborMerkleLink) c),\n                                t -> getChampRoot(committedRoot, root, owner, cache)\n                                        .thenCompose(champRoot -> ChampWrapper.create(owner, champRoot, Optional.empty(), x -> Futures.of(x.data), cache, hasher, c -> (CborObject.CborMerkleLink) c))\n                        )\n                        .thenCompose(tree -> tree.get(cap.mapKey))\n                        .thenApply(c -> c.map(x -> x.target).map(MaybeMultihash::of).orElse(MaybeMultihash.empty()))\n                        .thenCompose(btreeValue -> {\n                            if (btreeValue.isPresent())\n                                return cache.get(owner, (Cid) btreeValue.get(), cap.bat);\n                            return Futures.of(Optional.empty());\n                        })).collect(Collectors.toList()))\n                .thenApply(x -> new ArrayList<>(cache.getCached()));\n    }\n\n    /**\n     * Get the size in bytes of the object with the requested hash\n     * @param block The hash of the object\n     * @return The size in bytes, or Optional.empty() if it cannot be found.\n     */\n    CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block);\n\n    CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer);\n\n    CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link);\n\n    CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat);\n\n    default CompletableFuture<Cid> hashToCid(byte[] input, boolean isRaw, Hasher hasher) {\n        return hasher.sha256(input)\n                .thenApply(hash -> buildCid(hash, isRaw));\n    }\n\n    default Cid buildCid(byte[] sha256, boolean isRaw) {\n        return new Cid(Cid.V1, isRaw ? Cid.Codec.Raw : Cid.Codec.DagCbor, Multihash.Type.sha2_256, sha256);\n    }\n\n    default CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                        List<Cid> hashes,\n                                                                        List<BatWithId> bats,\n                                                                        Hasher h,\n                                                                        ProgressConsumer<Long> monitor,\n                                                                        double spaceIncreaseFactor) {\n        return NetworkAccess.downloadFragments(owner, hashes, bats, this, h, monitor, spaceIncreaseFactor);\n    }\n\n    default CompletableFuture<PublicKeyHash> putSigningKey(byte[] signature,\n                                                           PublicKeyHash owner,\n                                                           PublicSigningKey newKey,\n                                                           TransactionId tid) {\n        return putSigningKey(signature, owner, owner, newKey, tid);\n    }\n\n    default CompletableFuture<PublicKeyHash> putSigningKey(byte[] signature,\n                                                           PublicKeyHash owner,\n                                                           PublicKeyHash writer,\n                                                           PublicSigningKey newKey,\n                                                           TransactionId tid) {\n        return CompletableFuture.completedFuture(hashKey(newKey));\n    }\n\n    static PublicKeyHash hashKey(PublicSigningKey key) {\n        return new PublicKeyHash(new Cid(1, Cid.Codec.DagCbor, Multihash.Type.id, key.serialize()));\n    }\n\n    default CompletableFuture<PublicKeyHash> putBoxingKey(PublicKeyHash owner,\n                                                          byte[] signature,\n                                                          PublicBoxingKey key,\n                                                          TransactionId tid) {\n        byte[] rawKey = key.toCbor().toByteArray();\n        if (rawKey.length <= Multihash.MAX_IDENTITY_HASH_SIZE)\n            return Futures.of(new PublicKeyHash(Cid.buildCidV1(Cid.Codec.DagCbor, Multihash.Type.id, rawKey)));\n        return put(owner, owner, signature, rawKey, tid)\n                .thenApply(PublicKeyHash::new);\n    }\n\n    default CompletableFuture<Optional<PublicSigningKey>> getSigningKey(PublicKeyHash owner, PublicKeyHash hash) {\n        return (hash.isIdentity() ?\n                CompletableFuture.completedFuture(Optional.of(CborObject.fromByteArray(hash.getHash()))) :\n                get(owner, hash.target, Optional.empty()))\n                .thenApply(opt -> Optional.ofNullable(opt).orElse(Optional.empty()).map(PublicSigningKey::fromCbor));\n    }\n\n    default CompletableFuture<Optional<PublicBoxingKey>> getBoxingKey(PublicKeyHash owner, PublicKeyHash hash) {\n        return (hash.isIdentity() ?\n                CompletableFuture.completedFuture(Optional.of(CborObject.fromByteArray(hash.getHash()))) :\n                get(owner, hash.target, Optional.empty()))\n                .thenApply(opt -> Optional.ofNullable(opt).orElse(Optional.empty()).map(PublicBoxingKey::fromCbor));\n    }\n\n    class HTTP implements ContentAddressedStorage {\n\n        private final HttpPoster poster;\n        public static final String apiPrefix = \"api/v0/\";\n        public static final String ID = \"id\";\n        public static final String IDS = \"ids\";\n        public static final String LINK_HOST = \"link-host\";\n        public static final String BLOCKSTORE_PROPERTIES = \"blockstore/props\";\n        public static final String AUTH_READS = \"blockstore/auth-reads\";\n        public static final String AUTH_WRITES = \"blockstore/auth\";\n        public static final String TRANSACTION_START = \"transaction/start\";\n        public static final String TRANSACTION_CLOSE = \"transaction/close\";\n        public static final String CHAMP_GET = \"champ/get\";\n        public static final String CHAMP_GET_BULK = \"champ/get/bulk\";\n        public static final String LINK_GET = \"link/get\";\n        public static final String LINK_COUNTS = \"link/counts\";\n        public static final String BLOCK_PUT = \"block/put\";\n        public static final String BLOCK_PUT_BULK = \"block/put/bulk\";\n        public static final String BLOCK_GET = \"block/get\";\n        public static final String BLOCK_RM = \"block/rm\";\n        public static final String BLOCK_RM_BULK = \"block/rm/bulk\";\n        public static final String BLOOM_ADD = \"bloom/add\";\n        public static final String BLOCK_PRESENT = \"block/has\";\n        public static final String BLOCK_STAT = \"block/stat\";\n        public static final String BLOCK_STAT_BULK = \"block/stat/bulk\";\n        public static final String REFS_LOCAL = \"refs/local\";\n        public static final String IPNS_GET = \"ipns/get\";\n\n        private final boolean isPeergosServer;\n        private final Hasher hasher;\n        private final Random r = new Random();\n\n        public HTTP(HttpPoster poster, boolean isPeergosServer, Hasher hasher) {\n            this.poster = poster;\n            this.isPeergosServer = isPeergosServer;\n            this.hasher = hasher;\n        }\n\n        @Override\n        public ContentAddressedStorage directToOrigin() {\n            return this;\n        }\n\n        private static Cid getObjectHash(Object rawJson) {\n            Map json = (Map)rawJson;\n            String hash = (String)json.get(\"Hash\");\n            if (hash == null) {\n                Object val = json.get(\"Key\");\n                if (val instanceof  String)\n                    hash = (String) val;\n                else if (val instanceof Map)\n                    hash = (String) ((Map)val).get(\"/\");\n                else\n                    throw new IllegalStateException(\"Couldn't parse hash from response!\");\n            }\n            return Cid.decode(hash);\n        }\n\n        private static String encode(String component) {\n            try {\n                return URLEncoder.encode(component, \"UTF-8\");\n            } catch (UnsupportedEncodingException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public CompletableFuture<Cid> id() {\n            return poster.get(apiPrefix + ID)\n                    .thenApply(raw -> Cid.decodePeerId((String)((Map)JSONParser.parse(new String(raw))).get(\"ID\")));\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> ids() {\n            return poster.get(apiPrefix + IDS)\n                    .thenApply(raw -> ((List<String>)((Map)JSONParser.parse(new String(raw))).get(\"IDS\"))\n                            .stream()\n                            .map(Cid::decodePeerId)\n                            .collect(Collectors.toList()));\n        }\n\n        @Override\n        public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n            if (! isPeergosServer)\n                return Futures.of(\"localhost\");\n            return poster.get(apiPrefix + LINK_HOST + \"?owner=\" + encode(owner.toString()))\n                    .thenApply(raw -> new String(raw));\n        }\n\n        @Override\n        public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n            if (! isPeergosServer)\n                return Futures.of(BlockStoreProperties.empty());\n            return poster.get(apiPrefix + BLOCKSTORE_PROPERTIES)\n                    .thenApply(raw -> BlockStoreProperties.fromCbor(CborObject.fromByteArray(raw)));\n        }\n\n        @Override\n        public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n            if (! isPeergosServer)\n                return Futures.errored(new IllegalStateException(\"Cannot auth reads when not talking to a Peergos server!\"));\n            return poster.postUnzip(apiPrefix + AUTH_READS + \"?owner=\" + encode(owner.toString()), new CborObject.CborList(blocks).serialize())\n                    .thenApply(raw -> ((CborObject.CborList)CborObject.fromByteArray(raw)).value\n                            .stream()\n                            .map(PresignedUrl::fromCbor)\n                            .collect(Collectors.toList()));\n        }\n\n        @Override\n        public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                                PublicKeyHash writer,\n                                                                List<byte[]> signedHashes,\n                                                                List<Integer> blockSizes,\n                                                                List<List<BatId>> batIds,\n                                                                boolean isRaw,\n                                                                TransactionId tid) {\n            if (! isPeergosServer)\n                return Futures.errored(new IllegalStateException(\"Cannot auth writes when not talking to a Peergos server!\"));\n            List<Long> sizes = blockSizes.stream()\n                    .map(Integer::longValue)\n                    .collect(Collectors.toList());\n            WriteAuthRequest req = new WriteAuthRequest(signedHashes, sizes, batIds);\n            return poster.postUnzip(apiPrefix + AUTH_WRITES + \"?owner=\" + encode(owner.toString())\n                    + \"&writer=\" + encode(writer.toString())\n                    + \"&transaction=\" + encode(tid.toString())\n                    + \"&raw=\" + isRaw, req.serialize(), 60_000)\n                    .thenApply(raw -> ((CborObject.CborList)CborObject.fromByteArray(raw)).value\n                            .stream()\n                            .map(PresignedUrl::fromCbor)\n                            .collect(Collectors.toList()));\n        }\n\n        @Override\n        public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n            if (! isPeergosServer)\n                return CompletableFuture.completedFuture(new TransactionId(Long.toString(r.nextInt(Integer.MAX_VALUE))));\n            return poster.get(apiPrefix + TRANSACTION_START + \"?owner=\" + encode(owner.toString()))\n                    .thenApply(raw -> new TransactionId(new String(raw)));\n        }\n\n        @Override\n        public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n            if (! isPeergosServer)\n                return CompletableFuture.completedFuture(true);\n            return poster.get(apiPrefix + TRANSACTION_CLOSE + \"?arg=\" + tid.toString() + \"&owner=\" + encode(owner.toString()))\n                    .thenApply(raw -> new String(raw).equals(\"1\"));\n        }\n\n        @Override\n        public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n            if (! isPeergosServer) {\n                return getChampLookup(owner, root, caps, committedRoot, hasher);\n            }\n            if (caps.isEmpty())\n                return Futures.of(Collections.emptyList());\n            CborObject.CborList capsCbor = new CborObject.CborList(caps.stream()\n                    .map(ChunkMirrorCap::toCbor)\n                    .collect(Collectors.toList()));\n            return poster.get(apiPrefix + CHAMP_GET_BULK + \"?arg=\" + root.toString()\n                            + \"&owner=\" + encode(owner.toString())\n                            + \"&caps=\" + Multibase.encode(Multibase.Base.Base58BTC, capsCbor.serialize()))\n                    .thenApply(CborObject::fromByteArray)\n                    .thenApply(c -> (CborObject.CborList)c)\n                    .thenApply(res -> res.map(c -> ((CborObject.CborByteArray)c).value));\n        }\n\n        @Override\n        public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n            return poster.get(apiPrefix + LINK_GET\n                    + \"?label=\" + link.labelString()\n                    + \"&owner=\" + encode(link.owner.toString())\n            ).thenApply(CborObject::fromByteArray)\n                    .thenApply(EncryptedCapability::fromCbor);\n        }\n\n        @Override\n        public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n            return poster.get(apiPrefix + LINK_COUNTS\n                    + \"?after=\" + after.toEpochSecond(ZoneOffset.UTC)\n                    + \"?bat=\" + mirrorBat.encode()\n                    + \"&owner=\" + owner\n            ).thenApply(CborObject::fromByteArray)\n                    .thenApply(LinkCounts::fromCbor);\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                                PublicKeyHash writer,\n                                                List<byte[]> signedHashes,\n                                                List<byte[]> blocks,\n                                                TransactionId tid) {\n            return bulkPut(owner, writer, signedHashes, blocks, \"dag-cbor\", tid, x -> {});\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                                   PublicKeyHash writer,\n                                                   List<byte[]> signatures,\n                                                   List<byte[]> blocks,\n                                                   TransactionId tid,\n                                                   ProgressConsumer<Long> progressConsumer) {\n            return bulkPut(owner, writer, signatures, blocks, \"raw\", tid, progressConsumer)\n                    .thenApply(hashes -> {\n                        if (DEBUG_GC)\n                            System.out.println(\"Added blocks: \" + hashes);\n                        return hashes;\n                    });\n        }\n\n        private CompletableFuture<List<Cid>> bulkPut(PublicKeyHash owner,\n                                                     PublicKeyHash writer,\n                                                     List<byte[]> signatures,\n                                                     List<byte[]> blocks,\n                                                     String format,\n                                                     TransactionId tid,\n                                                     ProgressConsumer<Long> progressConsumer) {\n            if (isPeergosServer && signatures.stream().anyMatch(s -> s == null || s.length == 0))\n                throw new IllegalStateException(\"Empty signature in block write!\");\n            List<List<byte[]>> grouped = new ArrayList<>();\n            grouped.add(new ArrayList<>());\n            List<List<byte[]>> groupedSignatures = new ArrayList<>();\n            groupedSignatures.add(new ArrayList<>());\n            int totalSizeInGroup = 0;\n            for (int i=0; i < blocks.size(); i++) {\n                if (totalSizeInGroup + blocks.get(i).length > MAX_BLOCK_SIZE) {\n                    grouped.add(new ArrayList<>());\n                    groupedSignatures.add(new ArrayList<>());\n                    totalSizeInGroup = 0;\n                }\n                totalSizeInGroup += blocks.get(i).length;\n                grouped.get(grouped.size() - 1).add(blocks.get(i));\n                groupedSignatures.get(groupedSignatures.size() - 1).add(signatures.get(i));\n            }\n\n            List<Integer> sizes = grouped.stream()\n                    .map(frags -> frags.stream().mapToInt(f -> f.length).sum())\n                    .collect(Collectors.toList());\n            List<CompletableFuture<List<Cid>>> futures = IntStream.range(0, grouped.size())\n                    .parallel()\n                    .mapToObj(i -> put(\n                            owner,\n                            writer,\n                            groupedSignatures.get(i),\n                            grouped.get(i),\n                            format,\n                            tid\n                    ).thenApply(hash -> {\n                        if (progressConsumer != null)\n                            progressConsumer.accept((long) sizes.get(i));\n                        return hash;\n                    })).collect(Collectors.toList());\n            return Futures.combineAllInOrder(futures)\n                    .thenApply(groups -> groups.stream()\n                            .flatMap(g -> g.stream()).collect(Collectors.toList()));\n        }\n\n        private CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                                 PublicKeyHash writer,\n                                                 List<byte[]> signatures,\n                                                 List<byte[]> blocks,\n                                                 String format,\n                                                 TransactionId tid) {\n            for (byte[] block : blocks) {\n                if (block.length > MAX_BLOCK_SIZE)\n                    throw new IllegalStateException(\"Invalid block size: \" + block.length\n                            + \", blocks must be smaller than 1MiB!\");\n            }\n            int totalSize = blocks.stream().mapToInt(b -> b.length).sum();\n            if (totalSize > MAX_BLOCK_SIZE)\n                throw new IllegalStateException(\"Can't write group of blocks with total size bigger than \" + MAX_BLOCK_SIZE);\n            int timeoutMillis = blocks.size() > 1 ? 30_000 : -1;\n            byte[] body = new BlockWriteGroup(blocks, signatures).serialize();\n            return Futures.asyncExceptionally(() -> poster.post(apiPrefix + BLOCK_PUT_BULK + \"?format=\" + format\n                                    + \"&owner=\" + encode(owner.toString())\n                                    + \"&transaction=\" + encode(tid.toString())\n                                    + \"&writer=\" + encode(writer.toString()),\n                            body, false, timeoutMillis)\n                    .thenApply(bytes -> JSONParser.parseStream(new String(bytes))\n                            .stream()\n                            .map(json -> getObjectHash(json))\n                            .collect(Collectors.toList()))\n                    .thenApply(hashes -> {\n                        if (DEBUG_GC)\n                            System.out.println(\"Added blocks: \" + hashes);\n                        if (hashes.size() != blocks.size())\n                            throw new IllegalStateException(\"Incorrect number of hashes returned from bulk write: \" + hashes.size() + \" != \" + blocks.size());\n                        return hashes;\n                    }),\n                    t -> {\n                        String msg = t.getMessage();\n                        if (msg.contains(\"Storage+quota+reached\"))\n                            return Futures.errored(new StorageQuotaExceededException(msg));\n                        return Futures.errored(t);\n                    });\n        }\n\n        @Override\n        public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n            if (hash.isIdentity())\n                return CompletableFuture.completedFuture(Optional.of(CborObject.fromByteArray(hash.getHash())));\n            if (isPeergosServer)\n                return poster.get(apiPrefix + BLOCK_GET + \"?arg=\"\n                                + hash\n                                + \"&owner=\" + encode(owner.toString())\n                                + bat.map(b -> \"&bat=\" + b.encode()).orElse(\"\"))\n                        .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(CborObject.fromByteArray(raw)));\n\n            return id()\n                    .thenCompose(ourId -> bat.map(b -> b.bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, hasher)\n                            .thenApply(BlockAuth::encode)).orElse(Futures.of(\"\")))\n                    .thenCompose(auth -> poster.get(apiPrefix + BLOCK_GET + \"?arg=\" + hash\n                                    + \"&owner=\" + encode(owner.toString())\n                                    + \"&auth=\" + auth)\n                            .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(CborObject.fromByteArray(raw))));\n        }\n\n        @Override\n        public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n            if (hash.isIdentity())\n                return CompletableFuture.completedFuture(Optional.of(hash.getHash()));\n            if (isPeergosServer)\n                return poster.get(apiPrefix + BLOCK_GET + \"?arg=\" + hash\n                                + \"&owner=\" + encode(owner.toString())\n                                + bat.map(b -> \"&bat=\" + b.encode()).orElse(\"\"))\n                        .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(raw));\n\n            return id()\n                    .thenCompose(ourId -> bat.map(b -> b.bat.generateAuth(hash, ourId, 300, S3Request.currentDatetime(), bat.get().id, hasher)\n                            .thenApply(BlockAuth::encode)).orElse(Futures.of(\"\")))\n                    .thenCompose(auth -> poster.get(apiPrefix + BLOCK_GET + \"?arg=\" + hash\n                            + \"&owner=\" + encode(owner.toString())\n                            + \"&auth=\" + auth))\n                    .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(raw));\n        }\n\n        @Override\n        public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n            if (block.type == Multihash.Type.id)\n                return Futures.of(Optional.of(block.getHash().length));\n            return poster.get(apiPrefix + BLOCK_STAT + \"?arg=\" + block.toString() + \"&auth=letmein\")\n                    .thenApply(raw -> Optional.of((Integer)((Map)JSONParser.parse(new String(raw))).get(\"Size\")));\n        }\n\n        @Override\n        public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n            return poster.get(apiPrefix + IPNS_GET + \"?arg=\" + signer.toBase58())\n                    .thenApply(raw -> IpnsEntry.fromJson(JSONParser.parse(new String(raw))));\n        }\n\n        @Override\n        public Optional<BlockCache> getBlockCache() {\n            return Optional.empty();\n        }\n    }\n\n    class Proxying implements ContentAddressedStorage {\n        private final ContentAddressedStorage local;\n        private final ContentAddressedStorageProxy p2p;\n        private final List<Cid> ourNodeIds;\n        private final CoreNode core;\n        private final boolean allowNonLocalLinks;\n        private final Function<PublicKeyHash, Boolean> isLocal;\n\n        public Proxying(ContentAddressedStorage local,\n                        ContentAddressedStorageProxy p2p,\n                        List<Cid> ourNodeIds,\n                        CoreNode core,\n                        boolean allowNonLocalLinks,\n                        Function<PublicKeyHash, Boolean> isLocal) {\n            this.local = local;\n            this.p2p = p2p;\n            this.ourNodeIds = ourNodeIds;\n            this.core = core;\n            this.allowNonLocalLinks = allowNonLocalLinks;\n            this.isLocal = isLocal;\n        }\n\n        @Override\n        public ContentAddressedStorage directToOrigin() {\n            return new Proxying(local.directToOrigin(), p2p, ourNodeIds, core, allowNonLocalLinks, isLocal);\n        }\n\n        @Override\n        public CompletableFuture<Cid> id() {\n            return local.id();\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> ids() {\n            return local.ids();\n        }\n\n        @Override\n        public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n            return local.blockStoreProperties();\n        }\n\n        @Override\n        public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.linkHost(owner),\n                    target -> p2p.linkHost(target, owner));\n        }\n\n        @Override\n        public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n            return local.authReads(owner, blocks);\n        }\n\n        @Override\n        public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                                PublicKeyHash writer,\n                                                                List<byte[]> signedHashes,\n                                                                List<Integer> blockSizes,\n                                                                List<List<BatId>> batIds,\n                                                                boolean isRaw,\n                                                                TransactionId tid) {\n            return local.authWrites(owner, writer, signedHashes, blockSizes, batIds, isRaw, tid);\n        }\n\n        @Override\n        public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.startTransaction(owner),\n                    target -> p2p.startTransaction(target, owner));\n        }\n\n        @Override\n        public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.closeTransaction(owner, tid),\n                    target -> p2p.closeTransaction(target, owner, tid));\n        }\n\n        @Override\n        public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.getChampLookup(owner, root, caps, committedRoot),\n                    target -> p2p.getChampLookup(target, owner, root, caps));\n        }\n\n        @Override\n        public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n            PublicKeyHash owner = link.owner;\n            if (! allowNonLocalLinks && ! isLocal.apply(owner))\n                throw new IllegalStateException(\"Please use the link owner's server\");\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    link.owner,\n                    () -> local.getSecretLink(link),\n                    target -> p2p.getSecretLink(target, link));\n        }\n\n        @Override\n        public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n            return core.getPublicKeyHash(owner)\n                    .thenCompose(id -> Proxy.redirectCall(core,\n                            ourNodeIds,\n                            id.get(),\n                            () -> local.getLinkCounts(owner, after, mirrorBat),\n                            target -> p2p.getLinkCounts(target, owner, after, mirrorBat)));\n        }\n\n        @Override\n        public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid object, Optional<BatWithId> bat) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.get(owner, object, bat),\n                    target -> p2p.get(target, owner, object, bat));\n        }\n\n        @Override\n        public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid object, Optional<BatWithId> bat) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.getRaw(owner, object, bat),\n                    target -> p2p.getRaw(target, owner, object, bat));\n        }\n\n        @Override\n        public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n            return local.getSize(owner, block);\n        }\n\n        @Override\n        public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n            return local.getIpnsEntry(signer);\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                                PublicKeyHash writer,\n                                                List<byte[]> signedHashes,\n                                                List<byte[]> blocks,\n                                                TransactionId tid) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.put(owner, writer, signedHashes, blocks, tid),\n                    target -> p2p.put(target, owner, writer, signedHashes, blocks, tid));\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                                   PublicKeyHash writer,\n                                                   List<byte[]> signatures,\n                                                   List<byte[]> blocks,\n                                                   TransactionId tid,\n                                                   ProgressConsumer<Long> progressConsumer) {\n            return Proxy.redirectCall(core,\n                    ourNodeIds,\n                    owner,\n                    () -> local.putRaw(owner, writer, signatures, blocks, tid, progressConsumer),\n                    target -> p2p.putRaw(target, owner, writer, signatures, blocks, tid, progressConsumer));\n        }\n\n        @Override\n        public Optional<BlockCache> getBlockCache() {\n            return Optional.empty();\n        }\n    }\n\n    static CompletableFuture<CommittedWriterData> getWriterData(PublicKeyHash owner,\n                                                                Cid hash,\n                                                                Optional<Long> sequence,\n                                                                ContentAddressedStorage dht) {\n        return dht.get(owner, hash, Optional.empty())\n                .thenApply(cborOpt -> {\n                    if (! cborOpt.isPresent())\n                        throw new IllegalStateException(\"Couldn't retrieve WriterData from dht! \" + hash);\n                    return new CommittedWriterData(MaybeMultihash.of(hash), WriterData.fromCbor(cborOpt.get()), sequence);\n                });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/ContentAddressedStorageProxy.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.api.*;\nimport peergos.shared.io.ipfs.bases.Multibase;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\nimport static peergos.shared.storage.ContentAddressedStorage.HTTP.BLOCK_GET;\nimport static peergos.shared.storage.ContentAddressedStorage.HTTP.BLOCK_PUT_BULK;\n\npublic interface ContentAddressedStorageProxy {\n\n    CompletableFuture<String> linkHost(Multihash targetServerId, PublicKeyHash owner);\n\n    CompletableFuture<TransactionId> startTransaction(Multihash targetServerId, PublicKeyHash owner);\n\n    CompletableFuture<Boolean> closeTransaction(Multihash targetServerId, PublicKeyHash owner, TransactionId tid);\n\n    CompletableFuture<List<byte[]>> getChampLookup(Multihash targetServerId, PublicKeyHash owner, Multihash root, List<ChunkMirrorCap> caps);\n\n    CompletableFuture<EncryptedCapability> getSecretLink(Multihash targetServerId, SecretLink link);\n\n    CompletableFuture<LinkCounts> getLinkCounts(Multihash targetServerId, String owner, LocalDateTime after, BatWithId mirrorBat);\n\n    CompletableFuture<Optional<CborObject>> get(Multihash targetServerId, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat);\n\n    CompletableFuture<Optional<byte[]>> getRaw(Multihash targetServerId, PublicKeyHash owner, Cid hash, Optional<BatWithId> bat);\n\n    CompletableFuture<List<Cid>> put(Multihash targetServerId,\n                                     PublicKeyHash owner,\n                                     PublicKeyHash writer,\n                                     List<byte[]> signatures,\n                                     List<byte[]> blocks,\n                                     TransactionId tid);\n\n    CompletableFuture<List<Cid>> putRaw(Multihash targetServerId,\n                                        PublicKeyHash owner,\n                                        PublicKeyHash writer,\n                                        List<byte[]> signatures,\n                                        List<byte[]> blocks,\n                                        TransactionId tid,\n                                        ProgressConsumer<Long> progressConsumer);\n\n    class HTTP implements ContentAddressedStorageProxy {\n        private static final String P2P_PROXY_PROTOCOL = \"/http\";\n\n        private final HttpPoster poster;\n        private final String apiPrefix = \"api/v0/\";\n\n        public HTTP(HttpPoster poster) {\n            this.poster = poster;\n        }\n\n        private static Cid getObjectHash(Object rawJson) {\n            Map json = (Map)rawJson;\n            String hash = (String)json.get(\"Hash\");\n            if (hash == null)\n                hash = (String)json.get(\"Key\");\n            return Cid.decode(hash);\n        }\n\n        private static String encode(String component) {\n            try {\n                return URLEncoder.encode(component, \"UTF-8\");\n            } catch (UnsupportedEncodingException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        public static String getProxyUrlPrefix(Multihash targetId) {\n            return \"/p2p/\" + targetId.toString() + P2P_PROXY_PROTOCOL + \"/\";\n        }\n\n        @Override\n        public CompletableFuture<String> linkHost(Multihash targetServerId,\n                                                  PublicKeyHash owner) {\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix\n                            + ContentAddressedStorage.HTTP.LINK_HOST + \"?owner=\" + encode(owner.toString()))\n                    .thenApply(raw -> new String(raw));\n        }\n\n        @Override\n        public CompletableFuture<TransactionId> startTransaction(Multihash targetServerId,\n                                                                 PublicKeyHash owner) {\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix\n                    + \"transaction/start\" + \"?owner=\" + encode(owner.toString()))\n                    .thenApply(raw -> new TransactionId(new String(raw)));\n        }\n\n        @Override\n        public CompletableFuture<Boolean> closeTransaction(Multihash targetServerId,\n                                                           PublicKeyHash owner,\n                                                           TransactionId tid) {\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix\n                    + \"transaction/close?arg=\" + tid.toString() + \"&owner=\" + encode(owner.toString()))\n                    .thenApply(raw -> new String(raw).equals(\"1\"));\n        }\n\n        @Override\n        public CompletableFuture<List<byte[]>> getChampLookup(Multihash targetServerId,\n                                                              PublicKeyHash owner,\n                                                              Multihash root,\n                                                              List<ChunkMirrorCap> caps) {\n            CborObject.CborList capsCbor = new CborObject.CborList(caps.stream()\n                    .map(ChunkMirrorCap::toCbor)\n                    .collect(Collectors.toList()));\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix\n                            + ContentAddressedStorage.HTTP.CHAMP_GET_BULK + \"?arg=\" + root.toString()\n                            + \"&owner=\" + encode(owner.toString())\n                            + \"&caps=\" + Multibase.encode(Multibase.Base.Base58BTC, capsCbor.serialize()))\n                    .thenApply(CborObject::fromByteArray)\n                    .thenApply(c -> (CborObject.CborList)c)\n                    .thenApply(res -> res.map(c -> ((CborObject.CborByteArray)c).value));\n        }\n\n        @Override\n        public CompletableFuture<EncryptedCapability> getSecretLink(Multihash targetServerId,\n                                                                    SecretLink link) {\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix\n                    + \"link/get?label=\" + link.labelString()\n                    + \"&owner=\" + encode(link.owner.toString()))\n                    .thenApply(CborObject::fromByteArray)\n                    .thenApply(EncryptedCapability::fromCbor);\n        }\n\n        @Override\n        public CompletableFuture<LinkCounts> getLinkCounts(Multihash targetServerId,\n                                                           String owner,\n                                                           LocalDateTime after,\n                                                           BatWithId mirrorBat) {\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix\n                    + \"link/counts?after=\" + after.toEpochSecond(ZoneOffset.UTC)\n                    + \"?bat=\" + mirrorBat.encode()\n                    + \"&owner=\" + owner)\n                    .thenApply(CborObject::fromByteArray)\n                    .thenApply(LinkCounts::fromCbor);\n        }\n\n        @Override\n        public CompletableFuture<Optional<CborObject>> get(Multihash targetServerId,\n                                                           PublicKeyHash owner,\n                                                           Cid hash,\n                                                           Optional<BatWithId> bat) {\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix +\n                            BLOCK_GET + \"?arg=\"\n                            + hash\n                            + \"&owner=\" + encode(owner.toString())\n                            + bat.map(b -> \"&bat=\" + b.encode()).orElse(\"\"))\n                    .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(CborObject.fromByteArray(raw)));\n        }\n\n        @Override\n        public CompletableFuture<Optional<byte[]>> getRaw(Multihash targetServerId,\n                                                          PublicKeyHash owner,\n                                                          Cid hash,\n                                                          Optional<BatWithId> bat) {\n            return poster.get(getProxyUrlPrefix(targetServerId) + apiPrefix +\n                            BLOCK_GET + \"?arg=\" + hash\n                            + \"&owner=\" + encode(owner.toString())\n                            + bat.map(b -> \"&bat=\" + b.encode()).orElse(\"\"))\n                    .thenApply(raw -> raw.length == 0 ? Optional.empty() : Optional.of(raw));\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> put(Multihash targetServerId,\n                                                PublicKeyHash owner,\n                                                PublicKeyHash writer,\n                                                List<byte[]> signatures,\n                                                List<byte[]> blocks,\n                                                TransactionId tid) {\n            return put(targetServerId, owner, writer, signatures, blocks, \"dag-cbor\", tid);\n        }\n\n        @Override\n        public CompletableFuture<List<Cid>> putRaw(Multihash targetServerId,\n                                                   PublicKeyHash owner,\n                                                   PublicKeyHash writer,\n                                                   List<byte[]> signatures,\n                                                   List<byte[]> blocks,\n                                                   TransactionId tid,\n                                                   ProgressConsumer<Long> progressConsumer) {\n            return put(targetServerId, owner, writer, signatures, blocks, \"raw\", tid);\n        }\n\n        private CompletableFuture<List<Cid>> put(Multihash targetServerId,\n                                                 PublicKeyHash owner,\n                                                 PublicKeyHash writer,\n                                                 List<byte[]> signatures,\n                                                 List<byte[]> blocks,\n                                                 String format,\n                                                 TransactionId tid) {\n            byte[] body = new BlockWriteGroup(blocks, signatures).serialize();\n            return poster.post(getProxyUrlPrefix(targetServerId) + apiPrefix + BLOCK_PUT_BULK + \"?format=\" + format\n                    + \"&owner=\" + encode(owner.toString())\n                    + \"&transaction=\" + encode(tid.toString())\n                    + \"&writer=\" + encode(writer.toString()), body, false)\n                    .thenApply(bytes -> JSONParser.parseStream(new String(bytes))\n                            .stream()\n                            .map(json -> getObjectHash(json))\n                            .collect(Collectors.toList()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/DecodedSpaceRequest.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n@JsType\npublic class DecodedSpaceRequest {\n    public final QuotaControl.LabelledSignedSpaceRequest source;\n    public final QuotaControl.SpaceRequest decoded;\n\n    public DecodedSpaceRequest(QuotaControl.LabelledSignedSpaceRequest source, QuotaControl.SpaceRequest decoded) {\n        this.source = source;\n        this.decoded = decoded;\n    }\n\n    @JsMethod\n    public String getUsername() {\n        return source.getUsername();\n    }\n\n    @JsMethod\n    public int getSizeInMiB() {\n        return (int) (decoded.getSizeInBytes() / (1024 * 1024));\n    }\n\n    @Override\n    public String toString() {\n        return decoded.toString();\n    }\n\n    public static CompletableFuture<List<DecodedSpaceRequest>> decodeSpaceRequests(\n            List<QuotaControl.LabelledSignedSpaceRequest> in,\n            CoreNode core,\n            ContentAddressedStorage dht) {\n        return Futures.combineAllInOrder(in.stream()\n                .map(req -> core.getPublicKeyHash(req.username)\n                        .thenCompose(keyHashOpt -> {\n                            if (! keyHashOpt.isPresent())\n                                throw new IllegalStateException(\"Couldn't retrieve public key for \" + req.username);\n                            PublicKeyHash identityHash = keyHashOpt.get();\n                            return dht.getSigningKey(identityHash, identityHash);\n                        }).thenCompose(keyOpt -> {\n                            if (! keyOpt.isPresent())\n                                throw new IllegalStateException(\"Couldn't retrieve public key for \" + req.username);\n                            PublicSigningKey pubKey = keyOpt.get();\n                            return pubKey.unsignMessage(req.signedRequest).thenApply(raw -> {\n                                QuotaControl.SpaceRequest parsed = QuotaControl.SpaceRequest.fromCbor(CborObject.fromByteArray(raw));\n                                return Optional.of(new DecodedSpaceRequest(req, parsed));\n                            }).exceptionally(e -> {\n                                e.printStackTrace();\n                                return Optional.<DecodedSpaceRequest>empty();\n                            });\n                        }))\n                        .collect(Collectors.toList()))\n                .thenApply(all -> all.stream()\n                        .flatMap(Optional::stream)\n                        .collect(Collectors.toList()));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/DelegatingStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic abstract class DelegatingStorage implements ContentAddressedStorage {\n\n    private final ContentAddressedStorage target;\n\n    public DelegatingStorage(ContentAddressedStorage target) {\n        this.target = target;\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return target.blockStoreProperties();\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return target.linkHost(owner);\n    }\n\n    @Override\n    public void clearBlockCache() {\n        target.clearBlockCache();\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return target.id();\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return target.ids();\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return target.startTransaction(owner);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return target.closeTransaction(owner, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid) {\n        if (signedHashes.stream().anyMatch(s -> s.length == 0))\n            throw new IllegalStateException(\"Empty signature!\");\n        return target.put(owner, writer, signedHashes, blocks, tid);\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return target.get(owner, hash, bat);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressCounter) {\n        return target.putRaw(owner, writer, signatures, blocks, tid, progressCounter);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return target.getRaw(owner, hash, bat);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return target.getChampLookup(owner, root, caps, committedRoot);\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        return target.getSecretLink(link);\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        return target.getLinkCounts(owner, after, mirrorBat);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        return target.getSize(owner, block);\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        return target.getIpnsEntry(signer);\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        return target.downloadFragments(owner, hashes, bats, h, monitor, spaceIncreaseFactor);\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n        return target.authReads(owner, blocks);\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                            PublicKeyHash writer,\n                                                            List<byte[]> signedHashes,\n                                                            List<Integer> blockSizes,\n                                                            List<List<BatId>> batIds,\n                                                            boolean isRaw,\n                                                            TransactionId tid) {\n        return target.authWrites(owner, writer, signedHashes, blockSizes, batIds, isRaw, tid);\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return target.getBlockCache();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/DirectS3BlockStore.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.bases.Base32;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class DirectS3BlockStore implements ContentAddressedStorage {\n\n    public static final int MAX_SMALL_BLOCK_SIZE = 100 * 1024;\n\n    private final boolean directWrites, publicReads, authedReads;\n    private final Optional<String> basePublicReadUrl;\n    private final Optional<String> baseAuthedUrl;\n    private final HttpPoster direct;\n    private final ContentAddressedStorage fallback;\n    private final List<Cid> nodeIds;\n    private final LRUCache<PublicKeyHash, Multihash> storageNodeByOwner = new LRUCache<>(100);\n    private final CoreNode core;\n    private final Hasher hasher;\n\n    public DirectS3BlockStore(BlockStoreProperties blockStoreProperties,\n                              HttpPoster direct,\n                              ContentAddressedStorage fallback,\n                              List<Cid> nodeIds,\n                              CoreNode core,\n                              Hasher hasher) {\n        this.directWrites = blockStoreProperties.directWrites;\n        this.publicReads = blockStoreProperties.publicReads;\n        this.authedReads = blockStoreProperties.authedReads;\n        this.basePublicReadUrl = blockStoreProperties.basePublicReadUrl;\n        this.baseAuthedUrl = blockStoreProperties.baseAuthedUrl;\n        this.direct = direct;\n        this.fallback = fallback;\n        this.nodeIds = nodeIds;\n        this.core = core;\n        this.hasher = hasher;\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return Futures.of(new BlockStoreProperties(directWrites, publicReads, authedReads, basePublicReadUrl, baseAuthedUrl));\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return fallback.linkHost(owner);\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return fallback;\n    }\n\n    public static String hashToKey(Multihash hash) {\n        // To be compatible with IPFS we use the same scheme here, the cid bytes encoded as uppercase base32\n        String padded = new Base32().encodeAsString(hash.toBytes());\n        int padStart = padded.indexOf(\"=\");\n        return padStart > 0 ? padded.substring(0, padStart) : padded;\n    }\n\n    public static Cid keyToHash(String keyFileName) {\n        // To be compatible with IPFS we use the same scheme here, the cid bytes encoded as uppercase base32\n        byte[] decoded = new Base32().decode(keyFileName);\n        return Cid.cast(decoded);\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(nodeIds.get(nodeIds.size() - 1));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return Futures.of(nodeIds);\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return fallback.startTransaction(owner);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return fallback.closeTransaction(owner, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return fallback.put(owner, writer, signedHashes, blocks, tid);\n    }\n\n    private CompletableFuture<Boolean> onOwnersNode(PublicKeyHash owner) {\n        Multihash cached = storageNodeByOwner.get(owner);\n        if (cached != null)\n            return Futures.of(nodeIds.stream().map(Cid::bareMultihash).anyMatch(p -> p.equals(cached)));\n        return core.getUsername(owner)\n                .thenCompose(user -> core.getChain(user)\n                        .thenApply(chain -> {\n                            if (chain.isEmpty())\n                                throw new IllegalStateException(\"Empty chain returned for \" + user);\n                            List<Multihash> storageProviders = chain.get(chain.size() - 1).claim.storageProviders;\n                            Multihash mainNode = storageProviders.get(0);\n                            storageNodeByOwner.put(owner, mainNode.bareMultihash());\n                            Logger.getGlobal().info(\"Are we on owner's node? \" + mainNode + \" in \" + nodeIds);\n                            return nodeIds.stream().map(Cid::bareMultihash).anyMatch(p -> p.equals(mainNode.bareMultihash()));\n                        }));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressCounter) {\n        //  raw blocks smaller than 100 KiB are written directly to server rather than S3 (if S3 blockstore)\n        // otherwise we suffer disproportionally from latency to S3 from the client\n        if (blocks.stream().allMatch(b -> b.length < MAX_SMALL_BLOCK_SIZE))\n            return fallback.putRaw(owner, writer, signatures, blocks, tid, progressCounter);\n        return onOwnersNode(owner).thenCompose(ownersNode -> {\n            if (ownersNode && directWrites) {\n                // we have a trade-off here. The first block upload cannot start until the auth call returns.\n                // So we only auth 6 at once, to max out the 6 connections per host in a browser\n                int FRAGMENTS_PER_AUTH_QUERY = 6;\n                List<List<byte[]>> grouped = ArrayOps.group(blocks, FRAGMENTS_PER_AUTH_QUERY);\n                List<List<byte[]>> groupedSignatures = ArrayOps.group(signatures, FRAGMENTS_PER_AUTH_QUERY);\n                List<CompletableFuture<List<Cid>>> futures = IntStream.range(0, grouped.size())\n                        .parallel()\n                        .mapToObj(i -> bulkPutRaw(\n                                owner,\n                                writer,\n                                groupedSignatures.get(i),\n                                grouped.get(i),\n                                tid,\n                                progressCounter\n                        )).collect(Collectors.toList());\n                return Futures.combineAllInOrder(futures)\n                        .thenApply(groups -> groups.stream()\n                                .flatMap(g -> g.stream()).collect(Collectors.toList()));\n            }\n            return fallback.putRaw(owner, writer, signatures, blocks, tid, progressCounter);\n        });\n    }\n\n    private CompletableFuture<List<Cid>> bulkPutRaw(PublicKeyHash owner,\n                                                    PublicKeyHash writer,\n                                                    List<byte[]> signatures,\n                                                    List<byte[]> blocks,\n                                                    TransactionId tid,\n                                                    ProgressConsumer<Long> progressCounter) {\n        CompletableFuture<List<Cid>> res = new CompletableFuture<>();\n        List<Integer> sizes = blocks.stream().map(x -> x.length).collect(Collectors.toList());\n        List<List<BatId>> batIds = blocks.stream().map(Bat::getRawBlockBats).collect(Collectors.toList());\n        fallback.authWrites(owner, writer, signatures, sizes, batIds, true, tid)\n                .thenCompose(preAuthed -> {\n                    List<CompletableFuture<Cid>> futures = new ArrayList<>();\n                    for (int i = 0; i < blocks.size(); i++) {\n                        PresignedUrl url = preAuthed.get(i);\n                        Cid targetName = keyToHash(url.base.substring(url.base.lastIndexOf(\"/\") + 1));\n                        Long size = (long) blocks.get(i).length;\n                        int finalI = i;\n                        // Allow at least 60s, plus 1ms per 50 bytes (~20KB/s minimum assumed throughput)\n                        int timeoutMillis = (int) Math.max(60_000, size / 50);\n                        futures.add(RetryStorage.runWithRetry(7, () -> direct.put(url.base, blocks.get(finalI), url.fields, timeoutMillis))\n                                .thenApply(x -> {\n                                    progressCounter.accept(size);\n                                    return targetName;\n                                }));\n                    }\n                    return Futures.combineAllInOrder(futures);\n                }).thenApply(res::complete)\n                .exceptionally(res::completeExceptionally);\n        return res;\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        if (publicReads || ! authedReads)\n            return NetworkAccess.downloadFragments(owner, hashes, bats, fallback, h, monitor, spaceIncreaseFactor);\n\n        return onOwnersNode(owner).thenCompose(onOwners -> {\n            if (! onOwners)\n                return NetworkAccess.downloadFragments(owner, hashes, bats, fallback, h, monitor, spaceIncreaseFactor);\n\n            // Do a bulk auth in a single call\n            List<Pair<Integer, Cid>> indexAndHash = IntStream.range(0, hashes.size())\n                    .mapToObj(i -> new Pair<>(i, hashes.get(i)))\n                    .collect(Collectors.toList());\n            List<Pair<Integer, Cid>> nonIdentity = indexAndHash.stream()\n                    .filter(p -> !p.right.isIdentity())\n                    .collect(Collectors.toList());\n            CompletableFuture<List<PresignedUrl>> auths = nonIdentity.isEmpty() ?\n                    Futures.of(Collections.emptyList()) :\n                    fallback.authReads(owner, nonIdentity.stream()\n                            .map(p -> new BlockMirrorCap(p.right,\n                                    bats.size() > p.left ?\n                                            Optional.of(bats.get(p.left)) :\n                                            Optional.empty()))\n                            .collect(Collectors.toList()));\n            CompletableFuture<List<FragmentWithHash>> allResults = new CompletableFuture();\n            auths\n                    .thenCompose(preAuthedGets ->\n                            Futures.combineAllInOrder(IntStream.range(0, preAuthedGets.size())\n                                    .parallel()\n                                    .mapToObj(i -> direct.get(preAuthedGets.get(i).base, preAuthedGets.get(i).fields)\n                                            .thenApply(b -> {\n                                                monitor.accept((long) b.length);\n                                                Pair<Integer, Cid> hashAndIndex = nonIdentity.get(i);\n                                                return new Pair<>(hashAndIndex.left,\n                                                        new FragmentWithHash(new Fragment(b), Optional.of(hashAndIndex.right)));\n                                            }))\n                                    .collect(Collectors.toList())))\n                    .thenApply(retrieved -> {\n                        FragmentWithHash[] res = new FragmentWithHash[hashes.size()];\n                        for (Pair<Integer, FragmentWithHash> p : retrieved) {\n                            res[p.left] = p.right;\n                        }\n                        // This section is only relevant for legacy data that uses identity multihashes to inline fragments\n                        for (int i = 0; i < hashes.size(); i++)\n                            if (res[i] == null) {\n                                Multihash identity = hashes.get(i);\n                                if (!identity.isIdentity())\n                                    throw new IllegalStateException(\"Hash should be identity!\");\n                                res[i] = new FragmentWithHash(new Fragment(identity.getHash()), Optional.empty());\n                            }\n                        return Arrays.asList(res);\n                    }).thenAccept(allResults::complete)\n                    .exceptionally(t -> {\n                        if (t instanceof MajorRateLimitException) {\n                            allResults.completeExceptionally(t);\n                            return null;\n                        }\n                        NetworkAccess.downloadFragments(owner, hashes, bats, this, h, monitor, spaceIncreaseFactor)\n                                .thenAccept(allResults::complete)\n                                .exceptionally(e -> {\n                                    allResults.completeExceptionally(e);\n                                    return null;\n                                });\n                        return null;\n                    });\n            return allResults;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return getRaw(owner, hash, bat).thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        if (hash.isIdentity())\n                return CompletableFuture.completedFuture(Optional.of(hash.getHash()));\n        if (publicReads) {\n            CompletableFuture<Optional<byte[]>> res = new CompletableFuture<>();\n            direct.get(basePublicReadUrl.get() + hashToKey(hash))\n                    .thenApply(Optional::of)\n                    .thenAccept(res::complete)\n                    .exceptionally(t -> {\n                        fallback.authReads(owner, Arrays.asList(new BlockMirrorCap(hash, bat)))\n                                .thenCompose(preAuthedGet -> direct.get(preAuthedGet.get(0).base))\n                                .thenApply(Optional::of)\n                                .thenAccept(res::complete)\n                                .exceptionally(e -> {\n                                    fallback.getRaw(owner, hash, bat)\n                                            .thenAccept(res::complete)\n                                            .exceptionally(f -> {\n                                                res.completeExceptionally(f);\n                                                return null;\n                                            });\n                                    return null;\n                                });\n                        return null;\n                    });\n            return res;\n        }\n        if (authedReads && hash.isRaw()) {\n            CompletableFuture<Optional<byte[]>> res = new CompletableFuture<>();\n            fallback.authReads(owner, Arrays.asList(new BlockMirrorCap(hash, bat)))\n                    .thenCompose(preAuthedGet -> direct.get(preAuthedGet.get(0).base, preAuthedGet.get(0).fields))\n                    .thenApply(Optional::of)\n                    .thenApply(opt -> opt.filter(b -> b.length > 0))\n                    .thenAccept(res::complete)\n                    .exceptionally(t -> {\n                        String msg = t.getMessage();\n                        if (msg != null && msg.contains(\"exceeded\"))\n                            res.completeExceptionally(t);\n                        else\n                            fallback.getRaw(owner, hash, bat)\n                                    .thenAccept(res::complete)\n                                    .exceptionally(e -> {\n                                        res.completeExceptionally(e);\n                                        return null;\n                                    });\n                        return null;\n                    });\n            return res;\n        }\n        return fallback.getRaw(owner, hash, bat);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        return fallback.getSize(owner, block);\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        return fallback.getIpnsEntry(signer);\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return Futures.asyncExceptionally(\n                () -> fallback.getChampLookup(owner, root, caps, committedRoot),\n                t -> {\n                    if (!(t instanceof RateLimitException))\n                        return Futures.errored(t);\n                    return getChampLookup(owner, root, caps, committedRoot, hasher);\n                });\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        return fallback.getSecretLink(link);\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        return fallback.getLinkCounts(owner, after, mirrorBat);\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return fallback.getBlockCache();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/HashVerifyingStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class HashVerifyingStorage extends DelegatingStorage {\n\n    private final ContentAddressedStorage source;\n    private final Hasher hasher;\n\n    public HashVerifyingStorage(ContentAddressedStorage source, Hasher hasher) {\n        super(source);\n        this.source = source;\n        this.hasher = hasher;\n    }\n\n    private <T> CompletableFuture<T> verify(byte[] data, Multihash claimed, Supplier<T> result) {\n        switch (claimed.type) {\n            case sha2_256:\n                return hasher.sha256(data)\n                        .thenApply(hash -> {\n                            Multihash computed = new Multihash(Multihash.Type.sha2_256, hash);\n                            if (claimed instanceof Cid)\n                                computed = Cid.build(((Cid) claimed).version, ((Cid) claimed).codec, computed);\n\n                            if (computed.equals(claimed))\n                                return result.get();\n\n                            throw new IllegalStateException(\"Incorrect hash! Are you under attack? Expected: \" + claimed + \" actual: \" + computed);\n                        });\n            case id:\n                if (Arrays.equals(data, claimed.getHash()))\n                    return Futures.of(result.get());\n                throw new IllegalStateException(\"Incorrect identity hash! This shouldn't ever  happen.\");\n            default: throw new IllegalStateException(\"Unimplemented hash algorithm: \" + claimed.type);\n        }\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return source.blockStoreProperties();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new HashVerifyingStorage(source.directToOrigin(), hasher);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return source.put(owner, writer, signedHashes, blocks, tid)\n                .thenCompose(hashes -> Futures.combineAllInOrder(hashes.stream()\n                        .map(h -> verify(blocks.get(hashes.indexOf(h)), h, () -> h))\n                        .collect(Collectors.toList())));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return source.get(owner, hash, bat)\n                .thenCompose(cborOpt -> cborOpt.map(cbor -> verify(cbor.toByteArray(), hash, () -> cbor)\n                        .thenApply(Optional::of))\n                        .orElseGet(() -> Futures.of(Optional.empty())));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return source.putRaw(owner, writer, signatures, blocks, tid, progressConsumer)\n                .thenCompose(hashes -> Futures.combineAllInOrder(hashes.stream()\n                        .map(h -> verify(blocks.get(hashes.indexOf(h)), h, () -> h))\n                        .collect(Collectors.toList())));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return source.getRaw(owner, hash, bat)\n                .thenCompose(arrOpt -> arrOpt.map(bytes -> verify(bytes, hash, () -> bytes)\n                        .thenApply(Optional::of))\n                        .orElseGet(() -> Futures.of(Optional.empty())));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/HttpFileNotFoundException.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.*;\n\npublic class HttpFileNotFoundException extends RuntimeException {\n\n    @JsConstructor\n    public HttpFileNotFoundException() {\n        super(\"Http 404: File not found exception!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/HttpSpaceUsage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.concurrent.*;\nimport java.util.logging.*;\n\npublic class HttpSpaceUsage implements SpaceUsageProxy {\n\tprivate static final Logger LOG = Logger.getLogger(HttpSpaceUsage.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n    private static final String P2P_PROXY_PROTOCOL = \"/http\";\n\n    private final HttpPoster direct, p2p;\n\n    public HttpSpaceUsage(HttpPoster direct, HttpPoster p2p)\n    {\n        this.direct = direct;\n        this.p2p = p2p;\n    }\n\n    private static String getProxyUrlPrefix(Multihash targetId) {\n        return \"/p2p/\" + targetId.toString() + P2P_PROXY_PROTOCOL + \"/\";\n    }\n\n    @Override\n    public CompletableFuture<Long> getUsage(PublicKeyHash owner, byte[] signedTime, boolean local) {\n        return getUsage(\"\", direct, owner, signedTime, local);\n    }\n\n    @Override\n    public CompletableFuture<Long> getUsage(Multihash targetServerId, PublicKeyHash owner, byte[] signedTime) {\n        return getUsage(getProxyUrlPrefix(targetServerId), p2p, owner, signedTime, false);\n    }\n\n    private CompletableFuture<Long> getUsage(String urlPrefix, HttpPoster poster, PublicKeyHash owner, byte[] signedTime, boolean local) {\n        return poster.get(urlPrefix + Constants.SPACE_USAGE_URL\n                + \"usage?owner=\" + encode(owner.toString())\n                + \"&local=\" + local\n                + \"&auth=\" + ArrayOps.bytesToHex(signedTime)).thenApply(res -> {\n            return ((CborObject.CborLong)CborObject.fromByteArray(res)).value;\n        });\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> getPaymentProperties(PublicKeyHash owner, boolean newClientSecret, byte[] signedTime) {\n        return getPaymentProperties(\"\", direct, owner, newClientSecret, signedTime);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> getPaymentProperties(Multihash targetServerId, PublicKeyHash owner, boolean newClientSecret, byte[] signedTime) {\n        return getPaymentProperties(getProxyUrlPrefix(targetServerId), p2p, owner, newClientSecret, signedTime);\n    }\n\n    private CompletableFuture<PaymentProperties> getPaymentProperties(String urlPrefix, HttpPoster poster, PublicKeyHash owner, boolean newClientSecret, byte[] signedTime)\n    {\n        return poster.get(urlPrefix + Constants.SPACE_USAGE_URL + \"payment-properties?owner=\" + encode(owner.toString())\n                + \"&new-client-secret=\" + newClientSecret\n                + \"&auth=\" + ArrayOps.bytesToHex(signedTime))\n                .thenApply(res -> PaymentProperties.fromCbor(CborObject.fromByteArray(res)));\n    }\n\n    @Override\n    public CompletableFuture<Long> getQuota(PublicKeyHash owner, byte[] signedTime) {\n        return getQuota(\"\", direct, owner, signedTime);\n    }\n\n    @Override\n    public CompletableFuture<Long> getQuota(Multihash targetServerId, PublicKeyHash owner, byte[] signedTime) {\n        return getQuota(getProxyUrlPrefix(targetServerId), p2p, owner, signedTime);\n    }\n\n    private CompletableFuture<Long> getQuota(String urlPrefix, HttpPoster poster, PublicKeyHash owner, byte[] signedTime)\n    {\n        return poster.get(urlPrefix + Constants.SPACE_USAGE_URL + \"quota?owner=\" + encode(owner.toString())\n                + \"&auth=\" + ArrayOps.bytesToHex(signedTime)).thenApply(res -> {\n            return ((CborObject.CborLong)CborObject.fromByteArray(res)).value;\n        });\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> requestQuota(PublicKeyHash owner, byte[] signedRequest, long usage) {\n        return requestSpace(\"\", direct, owner, signedRequest);\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> requestSpace(Multihash targetServerId, PublicKeyHash owner, byte[] signedRequest) {\n        return requestSpace(getProxyUrlPrefix(targetServerId), p2p, owner, signedRequest);\n    }\n\n    public CompletableFuture<PaymentProperties> requestSpace(String urlPrefix, HttpPoster poster, PublicKeyHash owner, byte[] signedRequest) {\n        return poster.get(urlPrefix + Constants.SPACE_USAGE_URL + \"request?owner=\" + encode(owner.toString())\n                + \"&req=\" + ArrayOps.bytesToHex(signedRequest)).thenApply(res -> {\n            return PaymentProperties.fromCbor(CborObject.fromByteArray(res));\n        });\n    }\n\n    private static String encode(String component) {\n        try {\n            return URLEncoder.encode(component, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/IpfsTransaction.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class IpfsTransaction {\n\n    /** Run a series of operations under a transaction, ensuring that it is closed correctly\n     *\n     * @param owner\n     * @param processor\n     * @param ipfs\n     * @param <V>\n     * @return\n     */\n    public static <V> CompletableFuture<V> call(PublicKeyHash owner,\n                                                Function<TransactionId, CompletableFuture<V>> processor,\n                                                ContentAddressedStorage ipfs) {\n        CompletableFuture<V> res = new CompletableFuture<>();\n        ipfs.startTransaction(owner).thenCompose(tid -> processor.apply(tid)\n                .thenCompose(v -> ipfs.closeTransaction(owner, tid)\n                        .thenApply(x -> res.complete(v)))\n                .exceptionally(t -> {\n                    ipfs.closeTransaction(owner, tid)\n                            .thenApply(x -> res.completeExceptionally(t))\n                            .exceptionally(e -> res.completeExceptionally(e));\n                    return false;\n                })).exceptionally(e -> res.completeExceptionally(e));\n        return res;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/IpnsEntry.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.resolution.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class IpnsEntry {\n    public static final String RESOLUTION_RECORD_IPNS_SUFFIX = \"peergos_rr\";\n    public final byte[] signature, data;\n\n    public IpnsEntry(byte[] signature, byte[] data) {\n        this.signature = signature;\n        this.data = data;\n    }\n\n    private CompletableFuture<byte[]> verifySignature(Multihash signer, peergos.shared.Crypto crypto) {\n        if (! signer.isIdentity())\n            throw new IllegalStateException(\"Only Ed25519 keys are supported for IPNS in client!\");\n        byte[] pubKeymaterial = Arrays.copyOfRange(signer.getHash(), 4, 36);\n        Ed25519PublicKey pub = new Ed25519PublicKey(pubKeymaterial, crypto.signer);\n        return pub.unsignMessage(ArrayOps.concat(ArrayOps.concat(signature, \"ipns-signature:\".getBytes()), data));\n    }\n\n    public CompletableFuture<ResolutionRecord> getValue(Multihash signer, peergos.shared.Crypto crypto) {\n        ResolutionRecord result = getValue();\n        // hard code legacy RSA rotations to avoid an RSA implementation in client\n        if (signer.equals(Multihash.fromBase58(\"QmPqn9a1tJLpMtaCz1DSQNMAfsv6qXEx6XU2eLMTc2DVV4\")) &&\n                result.moved && result.host.isPresent() &&\n                result.host.get().equals(Multihash.fromBase58(\"12D3KooWEnCzE4uSeniFaCGXQuV1UnYkvqvbQJnYC363S2abgknr\")))\n            return Futures.of(result);\n        if (signer.equals(Multihash.fromBase58(\"QmcoDbhCiVXGrWs6rwBvB59Gm44veo7Qxn2zmRnPw7BaCH\")) &&\n                result.moved && result.host.isPresent() &&\n                result.host.get().equals(Multihash.fromBase58(\"12D3KooWFv6ZcoUKyaDBB7nR5SQg6HpmEbDXad48WyFSyEk7xrSR\")))\n            return Futures.of(result);\n        return verifySignature(signer, crypto).thenApply(x -> result);\n    }\n\n    public ResolutionRecord getValue() {\n        CborObject cbor = CborObject.fromByteArray(data);\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for IpnsEntry!\");\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        String RRKey = \"_\" + RESOLUTION_RECORD_IPNS_SUFFIX;\n        // support legacy records that put the RR in the value, this can be removed in the future\n        return ResolutionRecord.fromCbor(map.containsKey(RRKey) ? map.get(RRKey) : CborObject.fromByteArray(map.getByteArray(\"Value\")));\n    }\n\n    public long getIpnsSequence() {\n        CborObject cbor = CborObject.fromByteArray(data);\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for IpnsEntry!\");\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        return map.getLong(\"Sequence\");\n    }\n\n    public Map toJson() {\n        Map res = new HashMap<>();\n        res.put(\"sig\", ArrayOps.bytesToHex(signature));\n        res.put(\"data\", ArrayOps.bytesToHex(data));\n        return res;\n    }\n\n    public static IpnsEntry fromJson(Object json) {\n        Map m = (Map) json;\n        byte[] sig = ArrayOps.hexToBytes((String) m.get(\"sig\"));\n        byte[] data = ArrayOps.hexToBytes((String) m.get(\"data\"));\n        return new IpnsEntry(sig, data);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/JSAccountCache.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.asymmetric.PublicSigningKey;\nimport peergos.shared.login.LoginCache;\nimport peergos.shared.user.LoginData;\nimport peergos.shared.user.NativeJSAccountCache;\nimport peergos.shared.user.UserStaticData;\nimport peergos.shared.util.*;\n\nimport java.util.concurrent.CompletableFuture;\n\npublic class JSAccountCache implements LoginCache {\n\n    private final NativeJSAccountCache cache = new NativeJSAccountCache();\n\n    public JSAccountCache() {\n        cache.init();\n    }\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login) {\n        byte[] entrySerialized = login.entryPoints.toCbor().serialize();\n        return cache.setLoginData(login.username, entrySerialized);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> removeLoginData(String username) {\n        cache.remove(username);\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<UserStaticData> getEntryData(String username, PublicSigningKey authorisedReader) {\n        return cache.getEntryData(username).thenApply(entryPoints -> {\n            if (entryPoints == null) {\n                throw new RuntimeException(\"Client Offline!\");\n            }\n            return UserStaticData.fromCbor(CborObject.fromByteArray(entryPoints));\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/JSBatCache.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.NativeJSBatCache;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\npublic class JSBatCache implements EncryptedBatCache {\n\n    private final NativeJSBatCache cache = new NativeJSBatCache();\n\n    public JSBatCache() {\n        cache.init();\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username, SymmetricKey loginRoot) {\n        return cache.getUserBats(username).thenApply(encryptedBats -> {\n            if (encryptedBats == null) {\n                throw new RuntimeException(\"No BAT cached for user: \" + username);\n            }\n            return CipherText.fromCbor(CborObject.fromByteArray(encryptedBats)).decrypt(loginRoot, BatList::fromCbor).bats;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setUserBats(String username, List<BatWithId> bats, SymmetricKey loginRoot) {\n        return cache.setUserBats(username, CipherText.build(loginRoot, new BatList(bats)).serialize());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/JSBlockCache.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.user.NativeJSCache;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class JSBlockCache implements BlockCache {\n    private final NativeJSCache cache = new NativeJSCache();\n\n    public JSBlockCache(int maxSizeMiB) {\n        cache.init(maxSizeMiB);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> get(Cid hash) {\n        return cache.get(hash);\n    }\n\n    @Override\n    public boolean hasBlock(Cid hash) {\n        return cache.hasBlock(hash);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> put(Cid hash, byte[] data) {\n        return cache.put(hash, data);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> clear() {\n        return cache.clear();\n    }\n\n    @Override\n    public long getMaxSize() {\n        return 0;\n    }\n\n    @Override\n    public void setMaxSize(long maxSizeBytes) {\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/JSPkiCache.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.corenode.PkiCache;\nimport peergos.shared.corenode.UserPublicKeyLink;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.bases.Multibase;\nimport peergos.shared.user.NativeJSPkiCache;\n\nimport java.util.ArrayList;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\npublic class JSPkiCache implements PkiCache {\n\n    private final NativeJSPkiCache cache = new NativeJSPkiCache();\n\n    public JSPkiCache() {\n        cache.init();\n    }\n\n    @Override\n    public CompletableFuture<List<UserPublicKeyLink>> getChain(String username) {\n        return cache.getChain(username).thenApply(serialisedUserPublicKeyLinks -> {\n            if (serialisedUserPublicKeyLinks.isEmpty())\n                throw new RuntimeException(\"Client Offline!\");\n            List<UserPublicKeyLink> list = new ArrayList();\n            for(String userPublicKeyLink :  serialisedUserPublicKeyLinks) {\n                list.add(UserPublicKeyLink.fromCbor(CborObject.fromByteArray(Multibase.decode(userPublicKeyLink))));\n            }\n            return list;\n        });\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setChain(String username, List<UserPublicKeyLink> chain) {\n        String[] serialisedUserPublicKeyLinks = new String[chain.size()];\n        for(int i =0; i < chain.size(); i++) {\n            serialisedUserPublicKeyLinks[i] = Multibase.encode(Multibase.Base.Base58BTC, chain.get(i).serialize());\n        }\n        PublicKeyHash owner = chain.get(chain.size() - 1).owner;\n        String serialisedOwner = new String(Base64.getEncoder().encode(owner.serialize()));\n        return cache.setChain(username, serialisedUserPublicKeyLinks, serialisedOwner);\n    }\n\n    @Override\n    public CompletableFuture<String> getUsername(PublicKeyHash key) {\n        return cache.getUsername(new String(Base64.getEncoder().encode(key.serialize()))).thenApply(username -> {\n           if (username.isEmpty()) {\n               throw new RuntimeException(\"Client Offline!\");\n           }\n           return username;\n        });\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/JSPointerCache.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.mutable.PointerCache;\nimport peergos.shared.user.NativeJSPointerCache;\nimport peergos.shared.util.Futures;\n\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\n\npublic class JSPointerCache implements PointerCache {\n    private final NativeJSPointerCache cache = new NativeJSPointerCache();\n    private final ContentAddressedStorage storage;\n\n    public JSPointerCache(int maxItems, ContentAddressedStorage storage) {\n        cache.init(maxItems);\n        this.storage = storage;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> put(PublicKeyHash owner, PublicKeyHash writer, byte[] signedUpdate) {\n        return cache.get(owner, writer)\n                .thenCompose(current -> storage.getSigningKey(owner, writer).thenCompose(signerOpt -> {\n                    if (signerOpt.isEmpty())\n                        throw new IllegalStateException(\"Couldn't retrieve signing key!\");\n                    return doUpdate(current, signedUpdate, signerOpt.get()).thenCompose(res -> {\n                        if (res)\n                            return cache.put(owner, writer, signedUpdate);\n                        return Futures.of(false);\n                    });\n                }));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> get(PublicKeyHash owner, PublicKeyHash writer) {\n        return cache.get(owner, writer);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/LinkCounts.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.server.storage.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\n\npublic class LinkCounts implements Cborable {\n    public final Map<Long, Pair<Long, LocalDateTime>> counts;\n\n    public LinkCounts(Map<Long, Pair<Long, LocalDateTime>> counts) {\n        this.counts = counts;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        ArrayList<CborObject> data = new ArrayList<>(counts.size() * 3);\n        counts.forEach((k, v) -> {\n            data.add(new CborObject.CborLong(k));\n            data.add(new CborObject.CborLong(v.left));\n            data.add(new CborObject.CborLong(v.right.toEpochSecond(ZoneOffset.UTC)));\n        });\n        state.put(\"d\", new CborObject.CborList(data));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static LinkCounts fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for FileProperties! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        CborObject.CborList d = m.getList(\"d\");\n        Map<Long, Pair<Long, LocalDateTime>> counts = new HashMap<>();\n        if (d.value.size() % 3 != 0)\n            throw new IllegalStateException(\"Invalid cbor for LinkCounts: list size \" + d.value.size() + \" is not a multiple of 3\");\n        for (int i = 0; i < d.value.size(); i += 3) {\n            long key = ((CborObject.CborLong) d.value.get(i)).value;\n            long count = ((CborObject.CborLong) d.value.get(i + 1)).value;\n            long seconds = ((CborObject.CborLong) d.value.get(i + 2)).value;\n            counts.put(key, new Pair<>(count, LocalDateTime.ofEpochSecond(seconds, 0, ZoneOffset.UTC)));\n        }\n        return new LinkCounts(counts);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/LocalOnlyStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class LocalOnlyStorage implements ContentAddressedStorage {\n    private final BlockCache cache;\n    private final Supplier<CompletableFuture<List<byte[]>>> bulkFetcher;\n    private final Hasher h;\n\n    public LocalOnlyStorage(BlockCache cache, Supplier<CompletableFuture<List<byte[]>>> bulkFetcher, Hasher h) {\n        this.cache = cache;\n        this.bulkFetcher = bulkFetcher;\n        this.h = h;\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return cache.get(hash)\n                .thenCompose(opt -> {\n                    if (opt.isPresent())\n                        return Futures.of(opt);\n                    return bulkFetcher.get()\n                            .thenCompose(blocks -> Futures.combineAllInOrder(blocks.stream().map(data ->\n                                            h.sha256(data)\n                                                    .thenCompose(hashb -> {\n                                                        Cid c = new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hashb);\n                                                        return cache.put(c, data).thenApply(x -> new Pair<>(c, data));\n                                                    }))\n                                    .collect(Collectors.toList())))\n                            .thenApply(allBlocks -> allBlocks.stream()\n                                    .filter(p -> p.left.equals(hash))\n                                    .map(p -> p.right)\n                                    .findFirst());\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return getRaw(owner, hash, bat).thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return Futures.of(BlockStoreProperties.empty());\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public void clearBlockCache() {\n        cache.clear();\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        return Futures.of(new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, new byte[32]));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return Futures.of(Arrays.asList(new Cid(1, Cid.Codec.LibP2pKey, Multihash.Type.sha2_256, new byte[32])));\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressCounter) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                            PublicKeyHash writer,\n                                                            List<byte[]> signedHashes,\n                                                            List<Integer> blockSizes,\n                                                            List<List<BatId>> batIds,\n                                                            boolean isRaw,\n                                                            TransactionId tid) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return Optional.of(cache);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/LocalRamStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.crypto.hash.PublicKeyHash;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.BatId;\nimport peergos.shared.storage.auth.BatWithId;\nimport peergos.shared.user.fs.EncryptedCapability;\nimport peergos.shared.user.fs.FragmentWithHash;\nimport peergos.shared.user.fs.SecretLink;\nimport peergos.shared.util.EfficientHashMap;\nimport peergos.shared.util.Futures;\nimport peergos.shared.util.Pair;\nimport peergos.shared.util.ProgressConsumer;\n\nimport java.time.LocalDateTime;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class LocalRamStorage implements ContentAddressedStorage {\n    private final Hasher h;\n    private Map<Cid, byte[]> blocks;\n\n    private LocalRamStorage(Hasher h, EfficientHashMap<Cid, byte[]> blocks) {\n        this.h = h;\n        this.blocks = blocks;\n    }\n\n    public static CompletableFuture<LocalRamStorage> build(Hasher h, List<byte[]> cborBlocks) {\n        EfficientHashMap<Cid, byte[]> blocks = new EfficientHashMap<>();\n        return Futures.combineAllInOrder(cborBlocks.stream().map(b -> h.sha256(b)\n                        .thenApply(hash -> new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, hash))\n                        .thenApply(cid -> new Pair<>(cid, b)))\n                .collect(Collectors.toList()))\n                .thenApply(mappings -> {\n                    mappings.forEach(m -> blocks.put(m.left, m.right));\n                    return new LocalRamStorage(h, blocks);\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return Futures.of(Optional.ofNullable(blocks.get(hash)));\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return Futures.of(BlockStoreProperties.empty());\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public void clearBlockCache() {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Cid> id() {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signedHashes, List<byte[]> blocks, TransactionId tid) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return getRaw(owner, hash, bat).thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressCounter) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return getChampLookup(owner, root, caps, committedRoot, h);\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                            PublicKeyHash writer,\n                                                            List<byte[]> signedHashes,\n                                                            List<Integer> blockSizes,\n                                                            List<List<BatId>> batIds,\n                                                            boolean isRaw,\n                                                            TransactionId tid) {\n        throw new IllegalStateException(\"Unimplemented!\");\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/MajorRateLimitException.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.JsConstructor;\n\npublic class MajorRateLimitException extends RuntimeException {\n\n    @JsConstructor\n    public MajorRateLimitException(String msg) {\n        super(msg);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/PaymentProperties.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\n\npublic final class PaymentProperties  implements Cborable {\n\n    public final Optional<String> paymentServerUrl;\n    public final Optional<String> clientSecret;\n    public final Optional<String> error;\n    public final long freeQuota;\n    public final long desiredQuota;\n    public final boolean annual;\n    public final Optional<LocalDateTime> expiry;\n    public final long nextCharge;\n\n    private PaymentProperties(Optional<String> paymentServerUrl,\n                              Optional<String> error,\n                              Optional<String> clientSecret,\n                              long freeQuota,\n                              long desiredQuota,\n                              boolean annual,\n                              Optional<LocalDateTime> expiry,\n                              long nextCharge) {\n        this.paymentServerUrl = paymentServerUrl;\n        this.error = error;\n        this.clientSecret = clientSecret;\n        this.freeQuota = freeQuota;\n        this.desiredQuota = desiredQuota;\n        this.annual = annual;\n        this.expiry = expiry;\n        this.nextCharge = nextCharge;\n    }\n\n    public PaymentProperties(long freeQuota) {\n        this(Optional.empty(), Optional.empty(), Optional.empty(), freeQuota, 0, false, Optional.empty(), 0);\n    }\n\n    public PaymentProperties(String paymentServerUrl, Optional<String> clientSecret,\n                             long freeQuota, long desiredQuota, boolean annual,\n                             Optional<LocalDateTime> expiry, long nextCharge) {\n        this(Optional.of(paymentServerUrl), Optional.empty(), clientSecret, freeQuota, desiredQuota, annual, expiry, nextCharge);\n    }\n\n    @JsMethod\n    public Optional<String> getExpiry() {\n        return expiry.map(e -> e.toLocalDate().toString());\n    }\n\n    @JsMethod\n    public String getNextCharge() {\n        return Long.toString(nextCharge/100);\n    }\n\n    @JsMethod\n    public boolean isPaid() {\n        return paymentServerUrl.isPresent();\n    }\n\n    @JsMethod\n    public int freeMb() {\n        return (int)(freeQuota / (1000 * 1000));\n    }\n\n    @JsMethod\n    public int desiredMb() {\n        return (int)(desiredQuota / (1000 * 1000));\n    }\n\n    @JsMethod\n    public boolean isAnnual() {\n        return annual;\n    }\n\n    @JsMethod\n    public boolean hasError() {\n        return error.isPresent();\n    }\n\n    @JsMethod\n    public String getError() {\n        return error.get();\n    }\n\n    @JsMethod\n    public String getUrl() {\n        return paymentServerUrl.get();\n    }\n\n    @JsMethod\n    public String getClientSecret() {\n        return clientSecret.orElse(\"\");\n    }\n\n    public static PaymentProperties errored(String paymentServerUrl,\n                                            String error,\n                                            Optional<String> clientSecret,\n                                            long freeQuota,\n                                            long desiredQuota,\n                                            boolean annual) {\n        return new PaymentProperties(Optional.of(paymentServerUrl), Optional.of(error), clientSecret, freeQuota, desiredQuota, annual, Optional.empty(), 0);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"freeQuota\", new CborObject.CborLong(freeQuota));\n        state.put(\"desiredQuota\", new CborObject.CborLong(desiredQuota));\n        state.put(\"annual\", new CborObject.CborBoolean(annual));\n        paymentServerUrl.ifPresent(url -> state.put(\"url\", new CborObject.CborString(url)));\n        error.ifPresent(err -> state.put(\"err\", new CborObject.CborString(err)));\n        if (clientSecret.isPresent())\n            state.put(\"client_secret\", new CborObject.CborString(clientSecret.get()));\n        expiry.ifPresent(e -> state.put(\"expiry\", new CborObject.CborLong(e.toEpochSecond(ZoneOffset.UTC))));\n        state.put(\"nextCharge\", new CborObject.CborLong(nextCharge));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static PaymentProperties fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for FileProperties! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        Optional<String> url = m.getOptional(\"url\", c -> ((CborObject.CborString)c).value);\n        Optional<String> err = m.getOptional(\"err\", c -> ((CborObject.CborString)c).value);\n        Optional<String> client_secret = m.getOptional(\"client_secret\", c -> ((CborObject.CborString) c).value);\n        long freeQuota = m.getLong(\"freeQuota\");\n        long desiredQuota = m.getLong(\"desiredQuota\");\n        boolean annual = m.getBoolean(\"annual\", false);\n        long nextCharge = m.getOptionalLong(\"nextCharge\").orElse(0L);\n        Optional<LocalDateTime> expiry = m.getOptionalLong(\"expiry\").map(e -> LocalDateTime.ofEpochSecond(e, 0, ZoneOffset.UTC));\n        return new PaymentProperties(url, err, client_secret, freeQuota, desiredQuota, annual, expiry, nextCharge);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/PointerCasException.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.Optional;\n\npublic class PointerCasException extends RuntimeException {\n\n    public final MaybeMultihash existing, claimedExisting;\n    public final Optional<Long> sequence;\n\n    public PointerCasException(MaybeMultihash actualExisting, Optional<Long> actualSequence, MaybeMultihash claimedExisting) {\n        super(\"PointerCAS:\" + actualExisting + \",\" + actualSequence.map(Object::toString).orElse(\"\") + \",\" + claimedExisting);\n        this.existing = actualExisting;\n        this.claimedExisting = claimedExisting;\n        this.sequence = actualSequence;\n    }\n\n    @JsMethod\n    public static PointerCasException fromString(String msg) {\n        msg = msg.substring(\"PointerCAS:\".length());\n        String[] parts = msg.split(\",\");\n        MaybeMultihash actualExisting = MaybeMultihash.of(Cid.decode(parts[0]));\n        MaybeMultihash claimedExisting = MaybeMultihash.of(Cid.decode(parts[2]));\n        Optional<Long> actualSequence = parts[1].length() == 0 ?\n                Optional.empty() :\n                Optional.of(Long.parseLong(parts[1]));\n        return new PointerCasException(actualExisting, actualSequence, claimedExisting);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/PresignedUrl.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class PresignedUrl implements Cborable {\n\n    public final String base;\n    public final Map<String, String> fields;\n\n    public PresignedUrl(String base, Map<String, String> fields) {\n        this.base = base;\n        this.fields = fields;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> props = new TreeMap<>();\n        props.put(\"b\", new CborObject.CborString(base));\n        Map<String, Cborable> headers = new TreeMap<>();\n        for (Map.Entry<String, String> e : fields.entrySet()) {\n            headers.put(e.getKey(), new CborObject.CborString(e.getValue()));\n        }\n        props.put(\"h\", CborObject.CborMap.build(headers));\n        return CborObject.CborMap.build(props);\n    }\n\n    public static PresignedUrl fromCbor(Cborable cbor) {\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        String base = map.getString(\"b\");\n        Map<String, String> headers = ((CborObject.CborMap)map.get(\"h\"))\n                .toMap(k -> ((CborObject.CborString)k).value, k -> ((CborObject.CborString)k).value);\n        return new PresignedUrl(base, headers);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/ProxyingSpaceUsage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** Implements a SpaceUsage that will proxy calls to the owner's Peergos instance\n *\n */\npublic class ProxyingSpaceUsage implements SpaceUsage {\n\n    private final List<Cid> serverIds;\n    private final CoreNode core;\n    private final SpaceUsage local;\n    private final SpaceUsageProxy p2p;\n\n    public ProxyingSpaceUsage(List<Cid> serverIds, CoreNode core, SpaceUsage local, SpaceUsageProxy p2p) {\n        this.serverIds = serverIds;\n        this.core = core;\n        this.local = local;\n        this.p2p = p2p;\n    }\n\n    @Override\n    public CompletableFuture<Long> getUsage(PublicKeyHash targetUser, byte[] signedTime, boolean localUsage) {\n        if (localUsage)\n            return local.getUsage(targetUser, signedTime, localUsage);\n        return Proxy.redirectCall(core,\n                serverIds,\n                targetUser,\n                () -> local.getUsage(targetUser, signedTime, localUsage),\n                targetServer -> p2p.getUsage(targetServer, targetUser, signedTime));\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> getPaymentProperties(PublicKeyHash owner, boolean newClientSecret, byte[] signedTime) {\n        return local.getPaymentProperties(owner, newClientSecret, signedTime);\n    }\n\n    @Override\n    public CompletableFuture<Long> getQuota(PublicKeyHash owner, byte[] signedTime) {\n        return Proxy.redirectCall(core,\n                serverIds,\n                owner,\n                () -> local.getQuota(owner, signedTime),\n                targetServer -> p2p.getQuota(targetServer, owner, signedTime));\n    }\n\n    @Override\n    public CompletableFuture<PaymentProperties> requestQuota(PublicKeyHash owner, byte[] signedRequest, long usage) {\n        return local.requestQuota(owner, signedRequest, usage);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/QuotaControl.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** The interface for getting and requesting your space quota\n *\n */\npublic interface QuotaControl {\n\n    CompletableFuture<PaymentProperties> getPaymentProperties(PublicKeyHash owner, boolean newClientSecret, byte[] signedTime);\n\n    CompletableFuture<Long> getQuota(PublicKeyHash owner, byte[] signedTime);\n\n    CompletableFuture<PaymentProperties> requestQuota(PublicKeyHash owner, byte[] signedRequest, long usage);\n\n    default CompletableFuture<PaymentProperties> requestQuota(String username, SigningPrivateKeyAndPublicHash identity, long space, boolean annual) {\n        SpaceUsage.SpaceRequest req = new SpaceUsage.SpaceRequest(username, space, annual, System.currentTimeMillis(), Optional.empty());\n        return identity.secret.signMessage(req.serialize())\n                .thenCompose(signedRequest -> requestQuota(identity.publicKeyHash, signedRequest, 0));\n    }\n\n    class LabelledSignedSpaceRequest implements Cborable {\n        public final String username;\n        public final byte[] signedRequest;\n\n        public LabelledSignedSpaceRequest(String username, byte[] signedRequest) {\n            this.username = username;\n            this.signedRequest = signedRequest;\n        }\n\n        public String getUsername() {\n            return username;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            Map<String, Cborable> props = new TreeMap<>();\n            props.put(\"u\", new CborObject.CborString(username));\n            props.put(\"r\", new CborObject.CborByteArray(signedRequest));\n            return CborObject.CborMap.build(props);\n        }\n\n        public static LabelledSignedSpaceRequest fromCbor(Cborable cbor) {\n            CborObject.CborMap map = (CborObject.CborMap) cbor;\n            String username = map.getString(\"u\");\n            byte[] req = map.getByteArray(\"r\");\n            return new LabelledSignedSpaceRequest(username, req);\n        }\n    }\n\n    class SpaceRequest implements Cborable {\n        public final String username;\n        public final long bytes;\n        public final boolean annual;\n        public final long utcMillis;\n        Optional<byte[]> paymentProof;\n\n        public SpaceRequest(String username, long bytes, boolean annual, long utcMillis, Optional<byte[]> paymentProof) {\n            if (paymentProof.isPresent() && paymentProof.get().length > 4096)\n                throw new IllegalStateException(\"Payment proof too big!\");\n            this.username = username;\n            this.bytes = bytes;\n            this.annual = annual;\n            this.utcMillis = utcMillis;\n            this.paymentProof = paymentProof;\n        }\n\n        public long getSizeInBytes() {\n            return bytes;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            Map<String, Cborable> props = new TreeMap<>();\n            props.put(\"u\", new CborObject.CborString(username));\n            props.put(\"s\", new CborObject.CborLong(bytes));\n            props.put(\"a\", new CborObject.CborBoolean(annual));\n            props.put(\"t\", new CborObject.CborLong(utcMillis));\n            if (paymentProof.isPresent())\n                props.put(\"p\", new CborObject.CborByteArray(paymentProof.get()));\n            return CborObject.CborMap.build(props);\n        }\n\n        public static SpaceRequest fromCbor(Cborable cbor) {\n            CborObject.CborMap map = (CborObject.CborMap) cbor;\n            String username = map.getString(\"u\");\n            long bytes = map.getLong(\"s\");\n            boolean annual = map.getBoolean(\"a\", false);\n            long time = map.getLong(\"t\");\n            Optional<byte[]> proof = map.getOptionalByteArray(\"p\");\n            return new SpaceRequest(username, bytes, annual, time, proof);\n        }\n\n        @Override\n        public String toString() {\n            return username + \" \" + (bytes/1024/1024) + \" MiB \" + LocalDateTime.ofEpochSecond(utcMillis/1000, 0, ZoneOffset.UTC);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/RamBlockCache.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class RamBlockCache implements BlockCache {\n\n    private final LRUCache<Multihash, byte[]> cache;\n    private final int maxValueSize, cacheSize;\n\n    public RamBlockCache(int maxValueSize, int cacheSize) {\n        this.cache = new LRUCache<>(cacheSize);\n        this.maxValueSize = maxValueSize;\n        this.cacheSize = cacheSize;\n    }\n\n    @Override\n    public long getMaxSize() {\n        return Long.MAX_VALUE;\n    }\n\n    @Override\n    public void setMaxSize(long maxSizeBytes) {\n\n    }\n\n    public Collection<byte[]> getCached() {\n        return cache.values();\n    }\n\n    @Override\n    public CompletableFuture<Boolean> put(Cid hash, byte[] data) {\n        if (data.length < maxValueSize && data.length > 0)\n            cache.put(hash, data);\n        return Futures.of(true);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> get(Cid hash) {\n        return Futures.of(Optional.ofNullable(cache.get(hash)));\n    }\n\n    @Override\n    public boolean hasBlock(Cid hash) {\n        return cache.containsKey(hash);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> clear() {\n        cache.clear();\n        return Futures.of(true);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/RateLimitException.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.*;\n\npublic class RateLimitException extends RuntimeException {\n\n    @JsConstructor\n    public RateLimitException() {\n        super();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/RetryStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.net.*;\nimport java.time.*;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Random;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ScheduledThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Supplier;\n\npublic class RetryStorage implements ContentAddressedStorage {\n\n    private static final Random random = new Random(1);\n    private static final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);\n    private final ContentAddressedStorage target;\n    private final int maxAttempts;\n\n    public RetryStorage(ContentAddressedStorage target, int maxAttempts) {\n        this.target = target;\n        this.maxAttempts = maxAttempts;\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new RetryStorage(target.directToOrigin(), maxAttempts);\n    }\n\n    private static <V> void retryAfter(Supplier<CompletableFuture<V>> method, int milliseconds) {\n        executor.schedule(method::get, milliseconds, TimeUnit.MILLISECONDS);\n    }\n\n    private static int jitter(int minMilliseconds, int rangeMilliseconds) {\n        return minMilliseconds + random.nextInt(rangeMilliseconds);\n    }\n\n    private <V> CompletableFuture<V> runWithRetry(Supplier<CompletableFuture<V>> f) {\n        return recurse(maxAttempts, maxAttempts, f);\n    }\n\n    public static <V> CompletableFuture<V> runWithRetry(int maxAttempts, Supplier<CompletableFuture<V>> f) {\n        return recurse(maxAttempts, maxAttempts, f);\n    }\n\n    private static <V> CompletableFuture<V> recurse(int retriesLeft, int maxAttempts, Supplier<CompletableFuture<V>> f) {\n        CompletableFuture<V> res = new CompletableFuture<>();\n        try {\n            f.get()\n                    .thenAccept(res::complete)\n                    .exceptionally(g -> {\n                        Throwable e = Exceptions.getRootCause(g);\n                        if (retriesLeft == 1) {\n                            res.completeExceptionally(e);\n                        } else if (e instanceof StorageQuotaExceededException) {\n                            res.completeExceptionally(e);\n                        } else if (e instanceof HttpFileNotFoundException) {\n                            res.completeExceptionally(e);\n                        } else if (e instanceof MajorRateLimitException) {\n                            res.completeExceptionally(e);\n                        } else if (e.getMessage() != null && e.getMessage().contains(\"Champ+root+not+present\")) {\n                            res.completeExceptionally(e);\n                        } else {\n                            retryAfter(() -> recurse(retriesLeft - 1, maxAttempts, f)\n                                            .thenAccept(res::complete)\n                                            .exceptionally(t -> {\n                                                res.completeExceptionally(t);\n                                                return null;\n                                            }),\n                                    jitter(1000 << (maxAttempts - retriesLeft), 500 << (maxAttempts - retriesLeft)));\n                        }\n                        return null;\n                    });\n        } catch (Throwable t) {\n            res.completeExceptionally(t);\n        }\n        return res;\n    }\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return runWithRetry(target::blockStoreProperties);\n    }\n    @Override\n    public CompletableFuture<Cid> id() {\n        return runWithRetry(target::id);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> ids() {\n        return runWithRetry(target::ids);\n    }\n\n    @Override\n    public CompletableFuture<String> linkHost(PublicKeyHash owner) {\n        return runWithRetry(() -> target.linkHost(owner));\n    }\n\n    @Override\n    public CompletableFuture<TransactionId> startTransaction(PublicKeyHash owner) {\n        return runWithRetry(() -> target.startTransaction(owner));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> closeTransaction(PublicKeyHash owner, TransactionId tid) {\n        return runWithRetry(() -> target.closeTransaction(owner, tid));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner, PublicKeyHash writer, List<byte[]> signatures, List<byte[]> blocks, TransactionId tid) {\n        return runWithRetry(() -> target.put(owner, writer, signatures, blocks, tid));\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return runWithRetry(() -> target.get(owner, hash, bat));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressCounter) {\n        return runWithRetry(() -> target.putRaw(owner, writer, signatures, blocks, tid, progressCounter));\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid hash, Optional<BatWithId> bat) {\n        return runWithRetry(() -> target.getRaw(owner, hash, bat));\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner, Cid root, List<ChunkMirrorCap> caps, Optional<Cid> committedRoot) {\n        return runWithRetry(() -> target.getChampLookup(owner, root, caps, committedRoot));\n    }\n\n    @Override\n    public CompletableFuture<EncryptedCapability> getSecretLink(SecretLink link) {\n        return runWithRetry(() -> target.getSecretLink(link));\n    }\n\n    @Override\n    public CompletableFuture<LinkCounts> getLinkCounts(String owner, LocalDateTime after, BatWithId mirrorBat) {\n        return runWithRetry(() -> target.getLinkCounts(owner, after, mirrorBat));\n    }\n\n    @Override\n    public CompletableFuture<Optional<Integer>> getSize(PublicKeyHash owner, Multihash block) {\n        return runWithRetry(() -> target.getSize(owner, block));\n    }\n\n    @Override\n    public CompletableFuture<IpnsEntry> getIpnsEntry(Multihash signer) {\n        return runWithRetry(() -> target.getIpnsEntry(signer));\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        return runWithRetry(() -> target.downloadFragments(owner, hashes, bats, h, monitor, spaceIncreaseFactor));\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authReads(PublicKeyHash owner, List<BlockMirrorCap> blocks) {\n        return runWithRetry(() -> target.authReads(owner, blocks));\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                            PublicKeyHash writer,\n                                                            List<byte[]> signedHashes,\n                                                            List<Integer> blockSizes,\n                                                            List<List<BatId>> batIds,\n                                                            boolean isRaw,\n                                                            TransactionId tid) {\n        return runWithRetry(() -> target.authWrites(owner, writer, signedHashes, blockSizes, batIds, isRaw, tid));\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return target.getBlockCache();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/SpaceUsage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.concurrent.*;\n\npublic interface SpaceUsage extends QuotaControl {\n\n    CompletableFuture<Long> getUsage(PublicKeyHash owner, byte[] signedTime, boolean local);\n\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/SpaceUsageProxy.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.concurrent.*;\n\npublic interface SpaceUsageProxy extends SpaceUsage {\n\n    CompletableFuture<PaymentProperties> getPaymentProperties(Multihash targetServerId,\n                                                              PublicKeyHash owner,\n                                                              boolean newClientSecret,\n                                                              byte[] signedTime);\n\n    CompletableFuture<Long> getUsage(Multihash targetServerId, PublicKeyHash owner, byte[] signedTime);\n\n    CompletableFuture<Long> getQuota(Multihash targetServerId, PublicKeyHash owner, byte[] signedTime);\n\n    CompletableFuture<PaymentProperties> requestSpace(Multihash targetServerId, PublicKeyHash owner, byte[] signedRequest);\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/StorageQuotaExceededException.java",
    "content": "package peergos.shared.storage;\n\nimport jsinterop.annotations.JsConstructor;\n\npublic class StorageQuotaExceededException extends RuntimeException {\n\n    @JsConstructor\n    public StorageQuotaExceededException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/TransactionId.java",
    "content": "package peergos.shared.storage;\n\npublic final class TransactionId {\n    public final String id;\n\n    public TransactionId(String id) {\n        this.id = id;\n    }\n\n    @Override\n    public String toString() {\n        return id;\n    }\n\n    public static TransactionId build(String id) {\n        return new TransactionId(id);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/UnauthedCachingStorage.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.Fragment;\nimport peergos.shared.user.fs.FragmentWithHash;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class UnauthedCachingStorage extends DelegatingStorage {\n    private final ContentAddressedStorage target;\n    private final BlockCache cache;\n    private final LRUCache<Multihash, CompletableFuture<Optional<byte[]>>> pending;\n    private final Hasher hasher;\n\n    public UnauthedCachingStorage(ContentAddressedStorage target, BlockCache cache, Hasher hasher) {\n        super(target);\n        this.target = target;\n        this.cache = cache;\n        this.pending = new LRUCache<>(200);\n        this.hasher = hasher;\n    }\n\n    @Override\n    public CompletableFuture<BlockStoreProperties> blockStoreProperties() {\n        return target.blockStoreProperties();\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return new UnauthedCachingStorage(target.directToOrigin(), cache, hasher);\n    }\n\n    @Override\n    public void clearBlockCache() {\n        cache.clear();\n        target.clearBlockCache();\n    }\n\n    @Override\n    public Optional<BlockCache> getBlockCache() {\n        return Optional.of(cache);\n    }\n\n    private synchronized CompletableFuture<Optional<byte[]>> getPending(Cid key) {\n        return pending.get(key);\n    }\n\n    private synchronized void putPending(Cid key, CompletableFuture<Optional<byte[]>> val) {\n        pending.put(key, val);\n    }\n\n    private synchronized void removePending(Cid key) {\n        pending.remove(key);\n    }\n\n    @Override\n    public CompletableFuture<Optional<byte[]>> getRaw(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        return cache.get(key)\n                .thenCompose(res -> {\n                    if (res.isPresent())\n                        return Futures.of(res);\n\n                    CompletableFuture<Optional<byte[]>> inProgress = getPending(key);\n                    if (inProgress != null)\n                        return inProgress;\n\n                    CompletableFuture<Optional<byte[]>> pipe = new CompletableFuture<>();\n                    putPending(key, pipe);\n\n                    return Futures.asyncExceptionally(\n                            () -> target.getRaw(owner, key, bat).thenApply(blockOpt -> {\n                                if (blockOpt.isPresent()) {\n                                    byte[] value = blockOpt.get();\n                                    cache.put(key, value);\n                                }\n                                removePending(key);\n                                pipe.complete(blockOpt);\n                                return blockOpt;\n                            }),\n                            t -> {\n                                removePending(key);\n                                pipe.completeExceptionally(t);\n                                return Futures.errored(t);\n                            });\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner,\n                                                          Cid root,\n                                                          List<ChunkMirrorCap> caps,\n                                                          Optional<Cid> committedRoot) {\n        // Do local champ gets using cache, then do a single bulk call for those absent\n        CachingStorage cache = new CachingStorage(new LocalOnlyStorage(this.cache,\n                () -> Futures.errored(new IllegalStateException(\"Absent block\")), hasher),\n                100 * (1 + caps.size()), 1024*1024);\n\n        return ChampWrapper.create(owner, root, Optional.empty(), x -> Futures.of(x.data), cache, hasher, c -> (CborObject.CborMerkleLink) c)\n                .thenCompose(tree -> Futures.combineAll(caps.stream()\n                        .map(cap -> tree.get(cap.mapKey)\n                                .thenApply(c -> c.map(x -> x.target)\n                                        .map(MaybeMultihash::of).orElse(MaybeMultihash.empty()))\n                                .thenCompose(btreeValue -> {\n                                    if (btreeValue.isPresent())\n                                        return cache.get(owner, (Cid) btreeValue.get(), cap.bat)\n                                                .thenApply(x -> Optional.<ChunkMirrorCap>empty());\n                                    return Futures.of(Optional.of(cap));\n                                }).exceptionally(t -> Optional.of(cap)))\n                        .collect(Collectors.toList())))\n                .thenApply(missing -> missing.stream()\n                                .flatMap(Optional::stream)\n                                .collect(Collectors.toList()))\n                .exceptionally(t -> caps)\n                .thenCompose(missing -> committedRoot.isPresent() ?\n                        get(owner, committedRoot.get(), Optional.empty())\n                                .thenApply(ropt -> ropt.map(WriterData::fromCbor).flatMap(wd ->  wd.tree))\n                                .thenCompose(champRoot -> target.getChampLookup(owner, (Cid) champRoot.get(), missing, Optional.empty())) :\n                        target.getChampLookup(owner, root, missing, Optional.empty()))\n                .thenApply(blocks -> cacheBlocks(blocks, hasher))\n                .thenApply(remote -> {\n                    Collection<byte[]> cached = cache.getCached();\n                    ArrayList<byte[]> res = new ArrayList<>(cached.size() + remote.size());\n                    res.addAll(cached);\n                    res.addAll(remote);\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<byte[]>> getChampLookup(PublicKeyHash owner,\n                                                          Cid root,\n                                                          List<ChunkMirrorCap> caps,\n                                                          Optional<Cid> committedRoot,\n                                                          Hasher hasher) {\n        System.out.println(\"UnauthedCachingStorage::getChampLookup \" + root);\n        return Futures.asyncExceptionally(\n                () -> target.getChampLookup(owner, root, caps, committedRoot, hasher),\n                        t -> super.getChampLookup(owner, root, caps, committedRoot, hasher)\n        ).thenApply(blocks -> cacheBlocks(blocks, hasher));\n    }\n\n    private List<byte[]> cacheBlocks(List<byte[]> blocks, Hasher hasher) {\n        ForkJoinPool.commonPool().execute(() -> Futures.combineAll(blocks.stream()\n                        .map(b -> hasher.hash(b, false)\n                                .thenApply(c -> new Pair<>(c, new ByteArrayWrapper(b))))\n                        .collect(Collectors.toList()))\n                .thenAccept(hashed -> hashed.stream().forEach(p -> cache.put(p.left, p.right.data))));\n        return blocks;\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        return target.put(owner, writer, signedHashes, blocks, tid)\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        cache.put(res.get(i), block);\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<Optional<CborObject>> get(PublicKeyHash owner, Cid key, Optional<BatWithId> bat) {\n        return getRaw(owner, key, bat)\n                .thenApply(opt -> opt.map(CborObject::fromByteArray));\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        return target.putRaw(owner, writer, signatures, blocks, tid, progressConsumer)\n                .thenApply(res -> {\n                    for (int i=0; i < blocks.size(); i++) {\n                        byte[] block = blocks.get(i);\n                        cache.put(res.get(i), block);\n                    }\n                    return res;\n                });\n    }\n\n    @Override\n    public CompletableFuture<List<FragmentWithHash>> downloadFragments(PublicKeyHash owner,\n                                                                       List<Cid> hashes,\n                                                                       List<BatWithId> bats,\n                                                                       Hasher h,\n                                                                       ProgressConsumer<Long> monitor,\n                                                                       double spaceIncreaseFactor) {\n        return Futures.combineAllInOrder(IntStream.range(0, hashes.size())\n                        .mapToObj(i -> hashes.get(i))\n                        .map(c -> c.isIdentity() ? Futures.of(Optional.of(c.getHash())) : cache.get(c))\n                        .collect(Collectors.toList()))\n                .thenCompose(cached -> {\n                    List<Pair<Cid, Optional<BatWithId>>> toGet = IntStream.range(0, hashes.size())\n                            .filter(i -> cached.get(i).isEmpty())\n                            .mapToObj(i -> new Pair<>(hashes.get(i), i < bats.size() ? Optional.of(bats.get(i)) : Optional.<BatWithId>empty()))\n                            .collect(Collectors.toList());\n                    if (toGet.isEmpty())\n                        return Futures.of(IntStream.range(0, hashes.size())\n                                .mapToObj(i -> new FragmentWithHash(new Fragment(cached.get(i).get()), Optional.of(hashes.get(i))))\n                                .collect(Collectors.toList()));\n                    List<Cid> cidsToGet = toGet.stream().map(p -> p.left).collect(Collectors.toList());\n                    List<BatWithId> remainingBats = toGet.stream().flatMap(p -> p.right.stream()).collect(Collectors.toList());\n                    return target.downloadFragments(owner, cidsToGet, remainingBats, h, monitor, spaceIncreaseFactor)\n                            .thenApply(retrieved -> {\n                                retrieved.forEach(f -> cache.put(f.hash.get(), f.fragment.data));\n                                return IntStream.range(0, hashes.size())\n                                        .mapToObj(i -> new FragmentWithHash(cached.get(i).map(Fragment::new)\n                                                .orElseGet(() -> retrieved.stream()\n                                                        .filter(f -> f.hash.get().equals(hashes.get(i)))\n                                                        .findFirst()\n                                                        .orElseThrow(() -> new IllegalStateException(\"Missing fragment: \" + hashes.get(i)))\n                                                        .fragment), Optional.of(hashes.get(i))))\n                                        .collect(Collectors.toList());\n                            });\n                });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/WriteAuthRequest.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.storage.auth.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class WriteAuthRequest implements Cborable {\n\n    public final List<byte[]> signatures;\n    public final List<Long> sizes;\n    public final List<List<BatId>> batIds;\n\n    public WriteAuthRequest(List<byte[]> signatures, List<Long> sizes, List<List<BatId>> batIds) {\n        this.signatures = signatures;\n        this.sizes = sizes;\n        this.batIds = batIds;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> props = new TreeMap<>();\n        props.put(\"s\", new CborObject.CborList(signatures.stream()\n                .map(CborObject.CborByteArray::new)\n                .collect(Collectors.toList())));\n        props.put(\"l\", new CborObject.CborList(sizes.stream()\n                .map(CborObject.CborLong::new)\n                .collect(Collectors.toList())));\n        props.put(\"b\", new CborObject.CborList(batIds.stream()\n                .map(CborObject.CborList::new)\n                .collect(Collectors.toList())));\n        return CborObject.CborMap.build(props);\n    }\n\n    public static WriteAuthRequest fromCbor(Cborable cbor) {\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        List<byte[]> signatures = map.getList(\"s\", c -> ((CborObject.CborByteArray)c).value);\n        List<Long> sizes = map.getList(\"l\", c -> ((CborObject.CborLong)c).value);\n        List<List<BatId>> batIds = map.getList(\"b\", c -> ((CborObject.CborList)c).map(BatId::fromCbor));\n        return new WriteAuthRequest(signatures, sizes, batIds);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/WriteFilter.java",
    "content": "package peergos.shared.storage;\n\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class WriteFilter extends DelegatingStorage {\n\n    private final ContentAddressedStorage dht;\n    private final BiFunction<PublicKeyHash, Integer, Boolean> keyFilter;\n\n    public WriteFilter(ContentAddressedStorage dht, BiFunction<PublicKeyHash, Integer, Boolean> keyFilter) {\n        super(dht);\n        this.dht = dht;\n        this.keyFilter = keyFilter;\n    }\n\n    @Override\n    public CompletableFuture<List<PresignedUrl>> authWrites(PublicKeyHash owner,\n                                                            PublicKeyHash writer,\n                                                            List<byte[]> signedHashes,\n                                                            List<Integer> blockSizes,\n                                                            List<List<BatId>> batIds,\n                                                            boolean isRaw,\n                                                            TransactionId tid) {\n        long totalSize = blockSizes.stream().mapToLong(Integer::longValue).sum();\n        if (totalSize > Integer.MAX_VALUE)\n            throw new IllegalStateException(\"Total write size too large: \" + totalSize);\n        if (! keyFilter.apply(writer, (int) totalSize))\n            throw new IllegalStateException(\"Key not allowed to write to this server: \" + writer);\n        if (blockSizes.stream().anyMatch(s -> s > Fragment.MAX_LENGTH_WITH_BAT_PREFIX))\n            throw new IllegalStateException(\"Block too big!\");\n        return dht.authWrites(owner, writer, signedHashes, blockSizes, batIds, isRaw, tid);\n    }\n\n    @Override\n    public ContentAddressedStorage directToOrigin() {\n        return this;\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> put(PublicKeyHash owner,\n                                            PublicKeyHash writer,\n                                            List<byte[]> signedHashes,\n                                            List<byte[]> blocks,\n                                            TransactionId tid) {\n        long totalSize = blocks.stream().mapToLong(x -> x.length).sum();\n        if (totalSize > Integer.MAX_VALUE)\n            throw new IllegalStateException(\"Total write size too large: \" + totalSize);\n        if (! keyFilter.apply(writer, (int) totalSize))\n            throw new IllegalStateException(\"Key not allowed to write to this server: \" + writer);\n        if (blocks.stream().anyMatch(b -> b.length > Fragment.MAX_LENGTH_WITH_BAT_PREFIX))\n            throw new IllegalStateException(\"Block too big!\");\n        return dht.put(owner, writer, signedHashes, blocks, tid);\n    }\n\n    @Override\n    public CompletableFuture<List<Cid>> putRaw(PublicKeyHash owner,\n                                               PublicKeyHash writer,\n                                               List<byte[]> signatures,\n                                               List<byte[]> blocks,\n                                               TransactionId tid,\n                                               ProgressConsumer<Long> progressConsumer) {\n        long totalSize = blocks.stream().mapToLong(x -> x.length).sum();\n        if (totalSize > Integer.MAX_VALUE)\n            throw new IllegalStateException(\"Total write size too large: \" + totalSize);\n        if (! keyFilter.apply(writer, (int) totalSize))\n            throw new IllegalStateException(\"Key not allowed to write to this server: \" + writer);\n        if (blocks.stream().anyMatch(b -> b.length > Fragment.MAX_LENGTH_WITH_BAT_PREFIX))\n            throw new IllegalStateException(\"Block too big!\");\n        return dht.putRaw(owner, writer, signatures, blocks, tid, progressConsumer);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/Bat.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/** Block Access token\n *  This is used as a secret key in AWS S3 V4 signatures to authorise retrieving a block\n */\npublic class Bat implements Cborable {\n    public static final byte[] RAW_BLOCK_MAGIC_PREFIX = new byte[]{113, 29, 16, -49, 61, 50, 47, 43}; // generated randomly by a fair die roll (and not valid cbor at start)\n    public static final int MAX_RAW_BLOCK_PREFIX_SIZE = 100;\n    public static final int BAT_LENGTH = 32;\n\n    public final byte[] secret;\n\n    public Bat(byte[] secret) {\n        if (secret.length != BAT_LENGTH)\n            throw new IllegalStateException(\"Invalid BAT length: \" + secret.length);\n        this.secret = secret;\n    }\n\n    public String encodeSecret() {\n        return Multibase.encode(Multibase.Base.Base58BTC, secret);\n    }\n\n    public static Bat fromString(String encoded) {\n        return new Bat(Multibase.decode(encoded));\n    }\n\n    public CompletableFuture<BatId> calculateId(Hasher h) {\n        return h.hash(serialize(), true).thenApply(BatId::new);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"s\", new CborObject.CborByteArray(secret));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static Bat fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for Bat: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new Bat(m.getByteArray(\"s\"));\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(secret);\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (!(obj instanceof Bat))\n            return false;\n        return Arrays.equals(secret, ((Bat) obj).secret);\n    }\n\n    public CompletableFuture<BlockAuth> generateAuth(Cid block,\n                                                     Cid sourceNode,\n                                                     int expirySeconds,\n                                                     String datetime,\n                                                     Cid batId,\n                                                     Hasher h) {\n        if (batId.isIdentity())\n            throw new IllegalStateException(\"Cannot use identity multihash in S3 signatures!\");\n        S3Request req = new S3Request(\"GET\", sourceNode.bareMultihash().toBase58(), \"api/v0/block/get?arg=\" + block.toBase58(), S3Request.UNSIGNED,\n                Optional.empty(), Optional.of(expirySeconds), false, true,\n                Collections.emptyMap(), Collections.emptyMap(), batId.toBase58(), \"eu-central-1\", datetime);\n        return S3Request.computeSignature(req, encodeSecret(), h)\n                .thenApply(signature -> new BlockAuth(ArrayOps.hexToBytes(signature), expirySeconds, datetime, batId));\n    }\n\n    public static byte[] createRawBlockPrefix(Bat inlineBat, Optional<BatId> mirrorBat) {\n        ByteArrayOutputStream bout = new ByteArrayOutputStream();\n        try {\n            bout.write(RAW_BLOCK_MAGIC_PREFIX);\n            List<BatId> bats = Stream.concat(Stream.of(inlineBat).map(BatId::inline), mirrorBat.stream()).collect(Collectors.toList());\n            CborObject.CborList cbor = new CborObject.CborList(bats);\n            bout.write(cbor.serialize());\n            return bout.toByteArray();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static List<BatId> getBlockBats(Cid h, byte[] data) {\n        return h.isRaw() ? getRawBlockBats(data) : getCborBlockBats(data);\n    }\n\n    public static List<BatId> getRawBlockBats(byte[] block) {\n        int magicLength = RAW_BLOCK_MAGIC_PREFIX.length;\n        if (! ArrayOps.equalArrays(block, 0, magicLength, RAW_BLOCK_MAGIC_PREFIX, 0, magicLength))\n            return Collections.emptyList();\n        ByteArrayInputStream bin = new ByteArrayInputStream(block);\n        bin.skip(magicLength);\n        return ((CborObject.CborList) CborObject.read(bin, block.length)).map(BatId::fromCbor);\n    }\n\n    public static List<BatId> getCborBlockBats(byte[] data) {\n        CborObject cbor = CborObject.fromByteArray(data);\n        if (! (cbor instanceof CborObject.CborMap))\n            return Collections.emptyList();\n        return ((CborObject.CborMap) cbor).getList(\"bats\", BatId::fromCbor);\n    }\n\n    public static byte[] removeRawBlockBatPrefix(byte[] block) {\n        int magicLength = RAW_BLOCK_MAGIC_PREFIX.length;\n        if (! ArrayOps.equalArrays(block, 0, magicLength, RAW_BLOCK_MAGIC_PREFIX, 0, magicLength))\n            return block;\n        ByteArrayInputStream bin = new ByteArrayInputStream(block);\n        bin.skip(magicLength);\n        int start = magicLength + CborObject.read(bin, block.length).serialize().length;\n        return Arrays.copyOfRange(block, start, block.length);\n    }\n\n    public static Bat random(SafeRandom r) {\n        return new Bat(r.randomBytes(BAT_LENGTH));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/BatCache.java",
    "content": "package peergos.shared.storage.auth;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface BatCache {\n\n    CompletableFuture<List<BatWithId>> getUserBats(String username);\n\n    CompletableFuture<Boolean> setUserBats(String username, List<BatWithId> bats);\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/BatCave.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** This is used to store a mirror bat (or two during rotations) for each user.\n *\n */\npublic interface BatCave {\n\n    Optional<Bat> getBat(BatId id);\n\n    CompletableFuture<List<BatWithId>> getUserBats(String username, byte[] auth);\n\n    CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, byte[] auth);\n\n    default CompletableFuture<List<BatWithId>> getUserBats(String username, SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.BATS_URL + \"getUserBats\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> getUserBats(username, auth));\n    }\n\n    default CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.BATS_URL + \"addBat\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> addBat(username, id, bat, auth));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/BatCaveProxy.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** This is used to store a mirror bat (or two during rotations) for each user.\n *\n */\npublic interface BatCaveProxy {\n\n    CompletableFuture<List<BatWithId>> getUserBats(Multihash targetServerId, String username, byte[] auth);\n\n    CompletableFuture<Boolean> addBat(Multihash targetServerId, String username, BatId id, Bat bat, byte[] auth);\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/BatId.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class BatId implements Cborable {\n\n    public final Cid id;\n\n    public BatId(Cid id) {\n        this.id = id;\n    }\n\n    public boolean isInline() {\n        return id.isIdentity();\n    }\n\n    public Optional<Bat> getInline() {\n        if (id.isIdentity())\n            return Optional.of(new Bat(id.getHash()));\n        return Optional.empty();\n    }\n\n    public static BatId inline(Bat b) {\n        return new BatId(new Cid(1, Cid.Codec.Raw, Multihash.Type.id, b.secret));\n    }\n\n    public static CompletableFuture<BatId> sha256(Bat b, Hasher h) {\n        return h.sha256(b.secret)\n                .thenApply(hash -> new BatId(new Cid(1, Cid.Codec.Raw, Multihash.Type.sha2_256, hash)));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborByteArray(id.toBytes());\n    }\n\n    public static BatId fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborByteArray))\n            throw new IllegalStateException(\"Incorrect cbor for BatId: \" + cbor);\n        return new BatId(Cid.cast(((CborObject.CborByteArray) cbor).value));\n    }\n\n    @Override\n    public String toString() {\n        return id.toString();\n    }\n\n    @Override\n    public int hashCode() {\n        return id.hashCode();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (! (o instanceof BatId)) return false;\n        return id.equals(((BatId) o).id);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/BatList.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class BatList implements Cborable {\n\n    public final List<BatWithId> bats;\n\n    public BatList(List<BatWithId> bats) {\n        this.bats = bats;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(bats);\n    }\n\n    public static BatList fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for bat list: \" + cbor);\n\n        return new BatList(((CborObject.CborList) cbor).map(BatWithId::fromCbor));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/BatWithId.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.bases.*;\n\nimport java.util.*;\n\npublic class BatWithId implements Cborable {\n\n    public final Bat bat;\n    public final Cid id;\n\n    public BatWithId(Bat bat, Cid id) {\n        if (id.isIdentity())\n            throw new IllegalStateException(\"Cannot use identity cid here!\");\n        if (id.codec != Cid.Codec.Raw)\n            throw new IllegalStateException(\"BatId codec must be Raw!\");\n        this.bat = bat;\n        this.id = id;\n    }\n\n    public BatId id() {\n        return new BatId(id);\n    }\n\n    public String encode() {\n        return Multibase.encode(Multibase.Base.Base58BTC, serialize());\n    }\n\n    public static BatWithId decode(String in) {\n        return fromCbor(CborObject.fromByteArray(Multibase.decode(in)));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"b\", bat);\n        state.put(\"i\", new CborObject.CborByteArray(id.toBytes()));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static BatWithId fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for Bat: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new BatWithId(m.get(\"b\", Bat::fromCbor), m.get(\"i\", c -> Cid.cast(((CborObject.CborByteArray)c).value)));\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(bat, id);\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (!(obj instanceof BatWithId))\n            return false;\n        return bat.equals(((BatWithId) obj).bat) && id.equals(((BatWithId) obj).id);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/BlockAuth.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\n\npublic class BlockAuth implements Cborable {\n\n    public final byte[] signature;\n    public final int expirySeconds;\n    public final String awsDatetime;\n    public final Cid batId;\n\n    public BlockAuth(byte[] signature, int expirySeconds, String awsDatetime, Cid batId) {\n        if (batId.isIdentity())\n            throw new IllegalStateException(\"Cannot inline BAT in auth!\");\n        this.signature = signature;\n        this.expirySeconds = expirySeconds;\n        this.awsDatetime = awsDatetime;\n        this.batId = batId;\n    }\n\n    public String shortDate() {\n        return awsDatetime.substring(0, 8);\n    }\n\n    public String encode() {\n        return ArrayOps.bytesToHex(serialize());\n    }\n\n    public LocalDateTime timestamp() {\n        return LocalDateTime.of(\n                Integer.parseInt(awsDatetime.substring(0, 4)),\n                Integer.parseInt(awsDatetime.substring(4, 6)),\n                Integer.parseInt(awsDatetime.substring(6, 8)),\n                Integer.parseInt(awsDatetime.substring(9, 11)),\n                Integer.parseInt(awsDatetime.substring(11, 13)),\n                Integer.parseInt(awsDatetime.substring(13, 15))\n        );\n    }\n\n    public static BlockAuth fromString(String in) {\n        if (in.isEmpty())\n            throw new IllegalStateException(\"Empty block auth string!\");\n        return fromCbor(CborObject.fromByteArray(ArrayOps.hexToBytes(in)));\n    }\n\n    private static long timeToPackedLong(String t) {\n        int year = Integer.parseInt(t.substring(0, 4)) - 2000; // up to 38 bits\n        int month = Integer.parseInt(t.substring(4, 6)); // 4 bits\n        int day = Integer.parseInt(t.substring(6, 8)); // 5 bits\n        int hour = Integer.parseInt(t.substring(9, 11)); // 5 bits\n        int minute = Integer.parseInt(t.substring(11, 13)); // 6 bits\n        int second = Integer.parseInt(t.substring(13, 15)); // 6 bits\n        return second | (minute << 6) | (hour << 12) | (day << 17) | (month << 22) | (year << 26);\n    }\n\n    private static String packedLongToTime(long packed) {\n        int year = (int) (packed >> 26) + 2000;\n        int month = (int) (packed >> 22) & 0xF;\n        int day = (int) (packed >> 17) & 0x1F;\n        int hour = (int) (packed >> 12) & 0x1F;\n        int minute = (int) (packed >> 6) & 0x3F;\n        int second = (int) packed & 0x3F;\n        // Can't use String.format with gwtc :-(\n        return year + \"\" +\n                (month < 10 ? \"0\" + month : month) +\n                (day < 10 ? \"0\" + day : day) + \"T\" +\n                (hour < 10 ? \"0\" + hour : hour) +\n                (minute < 10 ? \"0\" + minute : minute) +\n                (second < 10 ? \"0\" + second : second) + \"Z\";\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"e\", new CborObject.CborLong(expirySeconds));\n        state.put(\"t\", new CborObject.CborLong(timeToPackedLong(awsDatetime)));\n        state.put(\"b\", new CborObject.CborByteArray(batId.toBytes()));\n        state.put(\"s\", new CborObject.CborByteArray(signature));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static BlockAuth fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for BlockAuth: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new BlockAuth(m.getByteArray(\"s\"), (int)m.getLong(\"e\"), packedLongToTime(m.getLong(\"t\")), Cid.cast(m.getByteArray(\"b\")));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/EncryptedBatCache.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.crypto.symmetric.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface EncryptedBatCache {\n\n    CompletableFuture<List<BatWithId>> getUserBats(String username, SymmetricKey loginRoot);\n\n    CompletableFuture<Boolean> setUserBats(String username, List<BatWithId> bats, SymmetricKey loginRoot);\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/HttpBatCave.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.logging.*;\nimport java.util.stream.*;\n\npublic class HttpBatCave implements BatCaveProxy, BatCave {\n    private static final String P2P_PROXY_PROTOCOL = \"/http\";\n\tprivate static final Logger LOG = Logger.getGlobal();\n\n    private final HttpPoster direct, p2p;\n\n    public HttpBatCave(HttpPoster direct, HttpPoster p2p)\n    {\n        this.direct = direct;\n        this.p2p = p2p;\n    }\n\n    private static String getProxyUrlPrefix(Multihash targetId) {\n        return \"/p2p/\" + targetId.toString() + P2P_PROXY_PROTOCOL + \"/\";\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username, byte[] auth) {\n        return getUserBats(\"\", direct, username, auth);\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(Multihash targetServerId, String username, byte[] auth) {\n        return getUserBats(getProxyUrlPrefix(targetServerId), p2p, username, auth);\n    }\n\n    private CompletableFuture<List<BatWithId>> getUserBats(String urlPrefix, HttpPoster poster, String username, byte[] auth) {\n        return poster.get(urlPrefix + Constants.BATS_URL + \"getUserBats?username=\" + username + \"&auth=\" + ArrayOps.bytesToHex(auth))\n                .thenApply(res ->\n                        ((CborObject.CborList)CborObject.fromByteArray(res)).value\n                                .stream()\n                                .map(BatWithId::fromCbor)\n                                .collect(Collectors.toList()));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, byte[] auth) {\n        return addBat(\"\", direct, username, id, bat, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addBat(Multihash targetServerId, String username, BatId id, Bat bat, byte[] auth) {\n        return addBat(getProxyUrlPrefix(targetServerId), p2p, username, id, bat, auth);\n    }\n\n    private CompletableFuture<Boolean> addBat(String urlPrefix, HttpPoster poster, String username, BatId id, Bat bat, byte[] auth)\n    {\n        return poster.get(urlPrefix + Constants.BATS_URL + \"addBat?username=\" + username\n                + \"&batid=\" + id.id\n                + \"&bat=\" + bat.encodeSecret()\n                + \"&auth=\" + ArrayOps.bytesToHex(auth))\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value);\n    }\n\n    @Override\n    public Optional<Bat> getBat(BatId id) {\n        throw new IllegalStateException(\"Cannot be called over http!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/OfflineBatCache.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** Offline bat cache used by mobile apps\n *\n */\npublic class OfflineBatCache implements BatCave {\n\n    private final BatCave target;\n    private final BatCache cache;\n\n    public OfflineBatCache(BatCave target, BatCache cache) {\n        this.target = target;\n        this.cache = cache;\n    }\n\n    @Override\n    public Optional<Bat> getBat(BatId id) {\n        return target.getBat(id);\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username, byte[] auth) {\n        return Futures.asyncExceptionally(\n                () -> target.getUserBats(username, auth).thenApply(bats -> {\n                    cache.setUserBats(username, bats);\n                    return bats;\n                }),\n                t -> cache.getUserBats(username));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, byte[] auth) {\n        return target.addBat(username, id, bat, auth).thenApply(res -> {\n            getUserBats(username, (byte[])null); // update cache\n            return res;\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/ProxyingBatCave.java",
    "content": "package peergos.shared.storage.auth;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class ProxyingBatCave implements BatCave {\n\n    private final List<Cid> serverIds;\n    private final CoreNode core;\n    private final BatCave local;\n    private final BatCaveProxy p2p;\n\n    public ProxyingBatCave(List<Cid> serverIds, CoreNode core, BatCave local, BatCaveProxy p2p) {\n        this.serverIds = serverIds;\n        this.core = core;\n        this.local = local;\n        this.p2p = p2p;\n    }\n\n    @Override\n    public CompletableFuture<List<BatWithId>> getUserBats(String username, byte[] auth) {\n        return core.getPublicKeyHash(username)\n                .thenCompose(owner -> Proxy.redirectCall(core,\n                        serverIds,\n                        owner.get(),\n                        () -> local.getUserBats(username, auth),\n                        target -> p2p.getUserBats(target, username, auth)));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addBat(String username, BatId id, Bat bat, byte[] auth) {\n        return core.getPublicKeyHash(username)\n                .thenCompose(owner -> Proxy.redirectCall(core,\n                        serverIds,\n                        owner.get(),\n                        () -> local.addBat(username, id, bat, auth),\n                        target -> p2p.addBat(target, username, id, bat, auth)));\n    }\n\n    @Override\n    public Optional<Bat> getBat(BatId id) {\n        throw new IllegalStateException(\"Not supported!\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/auth/S3Request.java",
    "content": "package peergos.shared.storage.auth;\n\nimport org.w3c.dom.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport javax.crypto.*;\nimport javax.crypto.spec.*;\nimport javax.xml.parsers.*;\nimport javax.xml.xpath.*;\nimport java.io.*;\nimport java.net.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** Presign requests to Amazon S3 or compatible\n *\n * @link https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html\n */\npublic class S3Request {\n\n    private static final String ALGORITHM = \"AWS4-HMAC-SHA256\";\n    public static final String UNSIGNED = \"UNSIGNED-PAYLOAD\";\n\n    public final String verb, host;\n    public final String key;\n    public final String contentSha256;\n    public final Optional<String> storageClass;\n    public final Optional<Integer> expiresSeconds;\n    public final boolean allowPublicReads;\n    public final boolean useAuthHeader;\n    public final String accessKeyId;\n    public final String region;\n    public final Map<String, String> extraQueryParameters;\n    public final Map<String, String> extraHeaders;\n    public final String shortDate, datetime;\n\n    public S3Request(String verb,\n                     String host,\n                     String key,\n                     String contentSha256,\n                     Optional<String> storageClass,\n                     Optional<Integer> expiresSeconds,\n                     boolean allowPublicReads,\n                     boolean useAuthHeader,\n                     Map<String, String> extraQueryParameters,\n                     Map<String, String> extraHeaders,\n                     String accessKeyId,\n                     String region,\n                     String datetime) {\n        if (datetime.length() != 16)\n            throw new IllegalStateException(\"Invalid datetime: \" + datetime);\n        this.verb = verb;\n        this.host = host;\n        this.key = key;\n        this.contentSha256 = contentSha256;\n        this.storageClass = storageClass;\n        this.expiresSeconds = expiresSeconds;\n        this.allowPublicReads = allowPublicReads;\n        this.useAuthHeader = useAuthHeader;\n        this.extraQueryParameters = extraQueryParameters;\n        this.extraHeaders = extraHeaders;\n        this.accessKeyId = accessKeyId;\n        this.region = region;\n        this.shortDate = datetime.substring(0, 8);\n        this.datetime = datetime;\n        if (storageClass.isPresent())\n            extraHeaders.put(\"x-amz-storage-class\", storageClass.get());\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignPut(String key,\n                                                             int size,\n                                                             String contentSha256,\n                                                             Optional<String> storageClass,\n                                                             boolean allowPublicReads,\n                                                             String datetime,\n                                                             String host,\n                                                             Map<String, String> extraHeaders,\n                                                             String region,\n                                                             String accessKeyId,\n                                                             String s3SecretKey,\n                                                             boolean useHttps,\n                                                             Hasher h) {\n        extraHeaders.put(\"Content-Length\", \"\" + size);\n        S3Request policy = new S3Request(\"PUT\", host, key, contentSha256, storageClass, Optional.empty(), allowPublicReads, true,\n                Collections.emptyMap(), extraHeaders, accessKeyId, region, datetime);\n        return preSignRequest(policy, key, host, s3SecretKey, useHttps, h);\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignCopy(String sourceBucket,\n                                                              String sourceKey,\n                                                              String targetKey,\n                                                              String datetime,\n                                                              String host,\n                                                              Optional<String> storageClass,\n                                                              Map<String, String> extraHeaders,\n                                                              String region,\n                                                              String accessKeyId,\n                                                              String s3SecretKey,\n                                                              boolean useHttps,\n                                                              Hasher h) {\n        Map<String, String> extras = new TreeMap<>();\n        extras.putAll(extraHeaders);\n        extras.put(\"x-amz-copy-source\", \"/\" + sourceBucket + \"/\" + sourceKey);\n        S3Request policy = new S3Request(\"PUT\", host, targetKey, UNSIGNED, storageClass, Optional.empty(), false, true,\n                Collections.emptyMap(), extras, accessKeyId, region, datetime);\n        return preSignRequest(policy, targetKey, host, s3SecretKey, useHttps, h);\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignGet(String key,\n                                                             Optional<Integer> expirySeconds,\n                                                             Optional<Pair<Integer, Integer>> range,\n                                                             String datetime,\n                                                             String host,\n                                                             String region,\n                                                             Optional<String> storageClass,\n                                                             String accessKeyId,\n                                                             String s3SecretKey,\n                                                             boolean useHttps,\n                                                             Hasher h) {\n        return preSignNulliPotent(\"GET\", key, expirySeconds, range, datetime, host, region, storageClass, accessKeyId, s3SecretKey, useHttps, h);\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignHead(String key,\n                                                              Optional<Integer> expirySeconds,\n                                                              String datetime,\n                                                              String host,\n                                                              String region,\n                                                              Optional<String> storageClass,\n                                                              String accessKeyId,\n                                                              String s3SecretKey,\n                                                              boolean useHttps,\n                                                              Hasher h) {\n        return preSignNulliPotent(\"HEAD\", key, expirySeconds, Optional.empty(), datetime, host, region, storageClass, accessKeyId, s3SecretKey, useHttps, h);\n    }\n\n    private static CompletableFuture<PresignedUrl> preSignNulliPotent(String verb,\n                                                                      String key,\n                                                                      Optional<Integer> expiresSeconds,\n                                                                      Optional<Pair<Integer, Integer>> range,\n                                                                      String datetime,\n                                                                      String host,\n                                                                      String region,\n                                                                      Optional<String> storageClass,\n                                                                      String accessKeyId,\n                                                                      String s3SecretKey,\n                                                                      boolean useHttps,\n                                                                      Hasher h) {\n\n        Map<String, String> extraHeaders = new HashMap<>(range\n                .map(p -> Stream.of(p).collect(Collectors.toMap(r -> \"Range\", r -> \"bytes=\"+r.left+\"-\"+r.right)))\n                .orElse(Collections.emptyMap()));\n        S3Request policy = new S3Request(verb, host, key, UNSIGNED, storageClass, expiresSeconds, false, false,\n                new HashMap<>(), extraHeaders, accessKeyId, region, datetime);\n        return preSignRequest(policy, key, host, s3SecretKey, useHttps, h);\n    }\n\n    public static CompletableFuture<PresignedUrl> preSignRequest(S3Request req,\n                                                                 String key,\n                                                                 String host,\n                                                                 String s3SecretKey,\n                                                                 boolean useHttps,\n                                                                 Hasher h) {\n        return computeSignature(req, s3SecretKey, h).thenApply(signature -> {\n            String query = req.getQueryString(signature);\n            String protocol =  useHttps ? \"https\" : \"http\";\n            return new PresignedUrl(protocol + \"://\" + host + \"/\" + key + query, req.getHeaders(signature));\n        });\n    }\n\n    /**\n     * Method for generating policy signature V4 for direct browser upload.\n     *\n     * @link https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html\n     */\n    public static CompletableFuture<String> computeSignature(S3Request policy,\n                                                             String s3SecretKey,\n                                                             Hasher hasher) {\n        String stringToSign = policy.stringToSign();\n        String shortDate = policy.shortDate;\n\n        return hasher.hmacSha256((\"AWS4\" + s3SecretKey).getBytes(), shortDate.getBytes())\n                .thenCompose(dateKey -> hasher.hmacSha256(dateKey, policy.region.getBytes()))\n                .thenCompose(dateRegionKey -> hasher.hmacSha256(dateRegionKey, \"s3\".getBytes()))\n                .thenCompose(dateRegionServiceKey -> hasher.hmacSha256(dateRegionServiceKey, \"aws4_request\".getBytes()))\n                .thenCompose(signingKey -> hasher.hmacSha256(signingKey, stringToSign.getBytes()))\n                .thenApply(ArrayOps::bytesToHex);\n    }\n\n    public String stringToSign() {\n        StringBuilder res = new StringBuilder();\n        res.append(ALGORITHM + \"\\n\");\n        res.append(datetime + \"\\n\");\n        res.append(scope() + \"\\n\");\n        res.append(ArrayOps.bytesToHex(Hash.sha256(toCanonicalRequest().getBytes())));\n        return res.toString();\n    }\n\n    private static String urlEncode(String in) {\n        try {\n            return URLEncoder.encode(in, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String toCanonicalRequest() {\n        StringBuilder res = new StringBuilder();\n        res.append(verb + \"\\n\");\n        res.append(\"/\" + key + \"\\n\");\n\n        res.append(getQueryParameters().entrySet()\n                .stream()\n                .map(e -> urlEncode(e.getKey()) + \"=\" + urlEncode(e.getValue()))\n                .collect(Collectors.joining(\"&\")));\n        res.append(\"\\n\");\n\n        Map<String, String> headers = getCanonicalHeaders();\n        for (Map.Entry<String, String> e : headers.entrySet()) {\n            res.append(e.getKey().toLowerCase() + \":\" + e.getValue() + \"\\n\");\n        }\n        res.append(\"\\n\");\n\n        res.append(headersToSign() + \"\\n\");\n        res.append(contentSha256);\n        return res.toString();\n    }\n\n    private Map<String, String> getHeaders(String signature) {\n        Map<String, String> headers = getOriginalHeaders();\n        if (! useAuthHeader)\n            return headers;\n        headers.put(\"Authorization\", ALGORITHM + \" Credential=\" + credential()\n                + \",SignedHeaders=\" + headersToSign() + \",Signature=\" + signature);\n        return headers;\n    }\n\n    private Map<String, String> getOriginalHeaders() {\n        Map<String, String> res = new LinkedHashMap<>();\n        res.put(\"Host\", host);\n        if (! useAuthHeader)\n            return res;\n        res.put(\"x-amz-date\", datetime);\n        res.put(\"x-amz-content-sha256\", contentSha256);\n        for (Map.Entry<String, String> e : extraHeaders.entrySet()) {\n            res.put(e.getKey(), e.getValue());\n        }\n        if (allowPublicReads)\n            res.put(\"x-amz-acl\", \"public-read\");\n        return res;\n    }\n\n    private String getQueryString(String signature) {\n        Map<String, String> res = getQueryParameters();\n        if (! useAuthHeader)\n            res.put(\"X-Amz-Signature\", signature);\n        if (res.isEmpty())\n            return \"\";\n        return \"?\" + res.entrySet()\n                .stream()\n                .map(e -> urlEncode(e.getKey()) + \"=\" + urlEncode(e.getValue()))\n                .collect(Collectors.joining(\"&\"));\n    }\n\n    private Map<String, String> getQueryParameters() {\n        Map<String, String> res = new TreeMap<>();\n        res.putAll(extraQueryParameters);\n        if (! useAuthHeader) {\n            res.put(\"X-Amz-Algorithm\", ALGORITHM);\n            res.put(\"X-Amz-Credential\", credential());\n            res.put(\"X-Amz-Date\", datetime);\n            expiresSeconds.ifPresent(seconds -> res.put(\"X-Amz-Expires\", \"\" + seconds));\n            res.put(\"X-Amz-SignedHeaders\", \"host\");\n        }\n        return res;\n    }\n\n    private SortedMap<String, String> getCanonicalHeaders() {\n        SortedMap<String, String> res = new TreeMap<>();\n        Map<String, String> originalHeaders = getOriginalHeaders();\n        for (Map.Entry<String, String> e : originalHeaders.entrySet()) {\n            res.put(e.getKey().toLowerCase(), e.getValue());\n        }\n        return res;\n    }\n\n    private String headersToSign() {\n        return getCanonicalHeaders().keySet()\n                .stream()\n                .sorted()\n                .collect(Collectors.joining(\";\"));\n    }\n\n    private String scope() {\n        return shortDate + \"/\" + region +\"/s3/aws4_request\";\n    }\n\n    private String credential() {\n        return accessKeyId +\"/\" + shortDate +\"/\" + region + \"/s3/aws4_request\";\n    }\n\n    public boolean isGet() {\n        return \"GET\".equals(verb);\n    }\n\n    public boolean isHead() {\n        return \"HEAD\".equals(verb);\n    }\n\n    public static String currentDatetime() {\n        LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);\n        return now.toString().substring(0, 19).replaceAll(\"-\", \"\").replaceAll(\":\", \"\") + \"Z\";\n    }\n}\n\n"
  },
  {
    "path": "src/peergos/shared/storage/controller/AllowedSignups.java",
    "content": "package peergos.shared.storage.controller;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\n@JsType\npublic\nclass AllowedSignups implements Cborable {\n    public final boolean free, paid;\n\n    public AllowedSignups(boolean free, boolean paid) {\n        this.free = free;\n        this.paid = paid;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"f\", new CborObject.CborBoolean(free));\n        state.put(\"p\", new CborObject.CborBoolean(paid));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static AllowedSignups fromCbor(CborObject cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for CryptreeNode: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new AllowedSignups(m.getBoolean(\"f\"), m.getBoolean(\"p\"));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/controller/HttpInstanceAdmin.java",
    "content": "package peergos.shared.storage.controller;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.net.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class HttpInstanceAdmin implements InstanceAdmin {\n    public static final String VERSION = \"version\";\n    public static final String PENDING = \"pending\";\n    public static final String APPROVE = \"approve\";\n    public static final String SIGNUPS = \"signups\";\n    public static final String WAIT_LIST = \"waitlist\";\n\n    private final HttpPoster poster;\n\n    public HttpInstanceAdmin(HttpPoster poster) {\n        this.poster = poster;\n    }\n\n    @Override\n    public CompletableFuture<VersionInfo> getVersionInfo() {\n        return poster.get(Constants.ADMIN_URL + VERSION)\n                .thenApply(raw -> VersionInfo.fromCbor(CborObject.fromByteArray(raw)));\n    }\n\n    @Override\n    public CompletableFuture<List<SpaceUsage.LabelledSignedSpaceRequest>> getPendingSpaceRequests(\n            PublicKeyHash adminIdentity,\n            Multihash instanceIdentity,\n            byte[] signedTime) {\n        return poster.get(Constants.ADMIN_URL + PENDING\n                + \"?admin=\" + encode(adminIdentity.toString())\n                + \"&instance=\" + encode(instanceIdentity.toString())\n                + \"&auth=\" + ArrayOps.bytesToHex(signedTime))\n                .thenApply(raw -> ((CborObject.CborList)CborObject.fromByteArray(raw))\n                        .map(QuotaControl.LabelledSignedSpaceRequest::fromCbor));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> approveSpaceRequest(PublicKeyHash adminIdentity, Multihash instanceIdentity, byte[] signedRequest) {\n        return poster.get(Constants.ADMIN_URL + APPROVE\n                + \"?admin=\" + encode(adminIdentity.toString())\n                + \"&instance=\" + encode(instanceIdentity.toString())\n                + \"&req=\" + ArrayOps.bytesToHex(signedRequest))\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value);\n    }\n\n    @Override\n    public CompletableFuture<AllowedSignups> acceptingSignups() {\n        return poster.get(Constants.ADMIN_URL + SIGNUPS)\n                .thenApply(res -> AllowedSignups.fromCbor(CborObject.fromByteArray(res)));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> addToWaitList(String email) {\n        return poster.get(Constants.ADMIN_URL + WAIT_LIST + \"?email=\" + email)\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value);\n    }\n\n    private static String encode(String component) {\n        try {\n            return URLEncoder.encode(component, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/storage/controller/InstanceAdmin.java",
    "content": "package peergos.shared.storage.controller;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/**\n * This is how the administrator of a Peergos instance can control it including:\n *\n * 1) Upgrade the Peergos version\n */\npublic interface InstanceAdmin {\n\n    CompletableFuture<VersionInfo> getVersionInfo();\n\n    CompletableFuture<List<QuotaControl.LabelledSignedSpaceRequest>> getPendingSpaceRequests(PublicKeyHash adminIdentity,\n                                                                                             Multihash instanceIdentity,\n                                                                                             byte[] signedTime);\n\n    CompletableFuture<Boolean> approveSpaceRequest(PublicKeyHash adminIdentity,\n                                                   Multihash instanceIdentity,\n                                                   byte[] signedRequest);\n\n    @JsMethod\n    CompletableFuture<AllowedSignups> acceptingSignups();\n\n    @JsMethod\n    CompletableFuture<Boolean> addToWaitList(String email);\n\n    class VersionInfo implements Cborable {\n        public final Version version;\n        public final String sourceVersion;\n\n        public VersionInfo(Version version, String sourceVersion) {\n            this.version = version;\n            this.sourceVersion = sourceVersion;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            Map<String, Cborable> props = new TreeMap<>();\n            props.put(\"v\", new CborObject.CborString(version.toString()));\n            props.put(\"s\", new CborObject.CborString(sourceVersion));\n            return CborObject.CborMap.build(props);\n        }\n\n        public static VersionInfo fromCbor(Cborable cbor) {\n            CborObject.CborMap map = (CborObject.CborMap) cbor;\n            String version = map.getString(\"v\");\n            String sourceVersion = map.getString(\"s\");\n            return new VersionInfo(Version.parse(version), sourceVersion);\n        }\n\n        @Override\n        public String toString() {\n            return version + \"-\" + sourceVersion;\n        }\n    }\n}"
  },
  {
    "path": "src/peergos/shared/user/Account.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface Account {\n    /** Auth signed by identity\n     *\n     * @param login\n     * @param auth\n     * @return\n     */\n    CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal);\n\n    default CompletableFuture<Boolean> setLoginData(LoginData login,\n                                                    SigningPrivateKeyAndPublicHash identity,\n                                                    boolean forceLocal) {\n        return identity.secret.signatureOnly(login.serialize())\n                .thenCompose(auth -> setLoginData(login, auth, forceLocal));\n    }\n\n    /** Auth signed by login keypair\n     *\n     * @param username\n     * @param authorisedReader\n     * @param auth\n     * @param mfa\n     * @return\n     */\n    CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                   PublicSigningKey authorisedReader,\n                                                                                   byte[] auth,\n                                                                                   Optional<MultiFactorAuthResponse>  mfa,\n                                                                                   boolean cacheMfaLoginData,\n                                                                                   boolean forceProxy,\n                                                                                   boolean forceNoCache);\n\n    /** Auth signed by identity\n     *\n     * @param username\n     * @param auth\n     * @return\n     */\n    CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth);\n\n    @JsMethod\n    default CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.LOGIN_URL + \"listMfa\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> getSecondAuthMethods(username, auth));\n    }\n\n    CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth);\n\n    @JsMethod\n    default CompletableFuture<Boolean> enableTotpFactor(String username,\n                                                        byte[] credentialId,\n                                                        String code,\n                                                        SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.LOGIN_URL + \"enableTotp\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> enableTotpFactor(username, credentialId, code, auth));\n    }\n\n    CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth);\n\n    @JsMethod\n    default CompletableFuture<TotpKey> addTotpFactor(String username, SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.LOGIN_URL + \"addTotp\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> addTotpFactor(username, auth));\n    }\n\n    CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth);\n\n    @JsMethod\n    default CompletableFuture<byte[]> registerSecurityKeyStart(String username, SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.LOGIN_URL + \"registerWebauthnStart\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> registerSecurityKeyStart(username, auth));\n    }\n\n    CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth);\n\n    @JsMethod\n    default CompletableFuture<Boolean> registerSecurityKeyComplete(String username,\n                                                                   String keyName,\n                                                                   MultiFactorAuthResponse resp,\n                                                                   SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.LOGIN_URL + \"registerWebauthnComplete\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> registerSecurityKeyComplete(username, keyName, resp, auth));\n    }\n\n    CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth);\n    @JsMethod\n    default CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, SigningPrivateKeyAndPublicHash identity) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.LOGIN_URL + \"deleteMfa\", System.currentTimeMillis());\n        return req.sign(identity.secret)\n                .thenCompose(auth -> deleteSecondFactor(username, credentialId, auth));\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/user/AccountProxy.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** A Mutable Pointers extension that proxies all calls over a p2p stream\n *\n */\npublic interface AccountProxy extends Account {\n\n    CompletableFuture<Boolean> setLoginData(Multihash targetServerId, LoginData login, byte[] auth);\n\n    CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(Multihash targetServerId,\n                                                                                   String username,\n                                                                                   PublicSigningKey authorisedReader,\n                                                                                   byte[] auth,\n                                                                                   Optional<MultiFactorAuthResponse> mfa);\n\n    CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(Multihash targetServerId, String username, byte[] auth);\n\n    CompletableFuture<TotpKey> addTotpFactor(Multihash targetServerId, String username, byte[] auth);\n\n    CompletableFuture<Boolean> enableTotpFactor(Multihash targetServerId,\n                                                String username,\n                                                byte[] credentialId,\n                                                String code,\n                                                byte[] auth);\n\n    CompletableFuture<byte[]> registerSecurityKeyStart(Multihash targetServerId, String username, byte[] auth);\n\n    CompletableFuture<Boolean> registerSecurityKeyComplete(Multihash targetServerId, String username, String keyName, MultiFactorAuthResponse resp, byte[] auth);\n\n    CompletableFuture<Boolean> deleteSecondFactor(Multihash targetServerId, String username, byte[] credentialId, byte[] auth);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/AcquaintanceSourcedTrieNode.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class AcquaintanceSourcedTrieNode implements TrieNode {\n\n    private final String ownerName;\n    private final IncomingCapCache cache;\n    private final Crypto crypto;\n\n    public AcquaintanceSourcedTrieNode(String ownerName,\n                                       IncomingCapCache cache,\n                                       Crypto crypto) {\n        this.ownerName = ownerName;\n        this.cache = cache;\n        this.crypto = crypto;\n    }\n\n    public static CompletableFuture<Optional<AcquaintanceSourcedTrieNode>> build(String ownerName,\n                                                                                 IncomingCapCache cache,\n                                                                                 Crypto crypto) {\n        return Futures.of(Optional.of(new AcquaintanceSourcedTrieNode(ownerName, cache, crypto)));\n    }\n\n    private FileWrapper convert(FileWrapper file, String path) {\n        return file.withTrieNode(new ExternalTrieNode(path, this));\n    }\n\n    @Override\n    public synchronized CompletableFuture<Optional<FileWrapper>> getByPath(String path,\n                                                                           Hasher hasher,\n                                                                           NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        Path file = PathUtil.get(ownerName + path);\n        return cache.getByPath(file, cache.getVersion(), hasher, network)\n                .thenApply(opt -> opt.map(f -> convert(f, path)));\n    }\n\n    @Override\n    public synchronized CompletableFuture<Optional<FileWrapper>> getByPath(String path,\n                                                                           Snapshot version,\n                                                                           Hasher hasher,\n                                                                           NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        Path file = PathUtil.get(ownerName + path);\n        return cache.getByPath(file, version, hasher, network)\n                .thenApply(opt -> opt.map(f -> convert(f, path)));\n    }\n\n    @Override\n    public synchronized CompletableFuture<Set<FileWrapper>> getChildren(String path,\n                                                                        Hasher hasher,\n                                                                        NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        Path dir = PathUtil.get(ownerName + path);\n        return cache.getChildren(dir, cache.getVersion(), hasher, network)\n                .thenApply(children -> children.stream()\n                        .map(f -> convert(f, path + \"/\" + f.getName()))\n                        .collect(Collectors.toSet()));\n    }\n\n    @Override\n    public synchronized CompletableFuture<Set<FileWrapper>> getChildren(String path,\n                                                                        Hasher hasher,\n                                                                        Snapshot version,\n                                                                        NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        Path dir = PathUtil.get(ownerName + path);\n        return cache.getChildren(dir, version, hasher, network)\n                .thenApply(children -> children.stream()\n                        .map(f -> convert(f, path + \"/\" + f.getName()))\n                        .collect(Collectors.toSet()));\n    }\n\n    @Override\n    public synchronized Set<String> getChildNames() {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public synchronized TrieNode put(String path, EntryPoint e) {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public synchronized TrieNode putNode(String path, TrieNode t) {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public synchronized TrieNode removeEntry(String path) {\n        if (TrieNode.canonicalise(path).isEmpty())\n            return TrieNodeImpl.empty();\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public Collection<TrieNode> getChildNodes() {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public TrieNode getChildNode(String name) {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public boolean isEmpty() {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/App.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.user.app.*;\nimport peergos.shared.user.fs.AsyncReader;\nimport peergos.shared.user.fs.FileWrapper;\nimport peergos.shared.util.*;\n\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.*;\n\n/** This is the trusted implementation of the API that will be presented to a sandboxed application in Peergos.\n *\n * An application without any privileges can be run without any arguments, equivalent to viewing a web page.\n * It can also be used to open a file selected by the user, in this case the app can save changes over the original file\n * after confirmation by the user. An app  cna only open files for which it has register a matching mimetype.\n *\n * An app granting the STORE_APP_DATA permission can store, and read files in a directory which is private to the\n * app.\n *\n * An app granted the SHARE_APP_DATA permission can request the user to share a file created by the app with a\n * friend, and receive incoming files share with the user from the same app and with matching mimetype. It can also\n * request a secret link be generated to a file generated by the app.\n *\n * When an app is installed a copy of its assets are stored in /$username/.apps/$appname/assets\n * The apps internal storage, if allowed, is in /$username/.apps/$appname/data\n * Any permissions granted by the user will be stored in /$username/.apps/$appname/permissions.cbor\n */\npublic class App implements StoreAppData {\n    public static final String APPS_DIR_NAME = \".apps\";\n    public static final String DATA_DIR_NAME = \"data\";\n\n    private final UserContext ctx;\n    private final String username;\n    private final Path appDataDirectoryWithoutUser;\n    private App(UserContext ctx, String username, Path appDataDirectory) {\n        this.ctx = ctx;\n        this.username = username;\n        validatePath(appDataDirectory);\n        this.appDataDirectoryWithoutUser = appDataDirectory;\n    }\n\n    public static Path getDataDir(String appName, String username) {\n        return PathUtil.get(username, APPS_DIR_NAME, appName, DATA_DIR_NAME);\n    }\n\n    @JsMethod\n    public static CompletableFuture<String> getAppSubdomain(String path, Hasher h) {\n        return h.bareHash(PathUtil.get(path).toString().getBytes())\n                .thenApply(m -> Multibase.encode(Multibase.Base.Base32, m.toBytes()));\n    }\n\n    @JsMethod\n    public static CompletableFuture<String> getAppSubdomainWithAnonymityClass(String appRootPath, String anonymityClass, Hasher h) {\n        CompletableFuture<Multihash> root = h.bareHash(PathUtil.get(appRootPath).toString().getBytes());\n        CompletableFuture<Multihash> anonClass = h.bareHash(PathUtil.get(anonymityClass).toString().getBytes());\n        return Futures.combineAllInOrder(Stream.of(root, anonClass).collect(Collectors.toList()))\n                .thenCompose(both -> h.bareHash(ArrayOps.concat(both.get(0).getHash(), both.get(1).getHash())))\n                .thenApply(m -> Multibase.encode(Multibase.Base.Base32, m.toBytes()));\n    }\n\n    @JsMethod\n    public static CompletableFuture<App> init(UserContext ctx, String appName) {\n        Path appDataDir = PathUtil.get(APPS_DIR_NAME, appName, DATA_DIR_NAME);\n        return (ctx.username != null ?\n                Futures.of(ctx.username) :\n                ctx.getEntryPath().thenApply(p -> p.substring(1, p.indexOf(\"/\", 1))))\n                .thenCompose(username -> {\n                    App app = new App(ctx, username, appDataDir);\n                    return ctx.username == null ? Futures.of(app) :\n                            ctx.getUserRoot()\n                                    .thenCompose(root -> root.getOrMkdirs(appDataDir, ctx.network, true, ctx.mirrorBatId(), ctx.crypto))\n                                    .thenApply(appDir -> app);\n                });\n    }\n\n    private void validatePath(Path path) {\n        String pathAsString = path.toString().trim().replace('\\\\', '/');\n        if (pathAsString.startsWith(\"//\")) {\n            throw new IllegalStateException(\"Path must be relative!\");\n        }\n        List<String> parts = Arrays.stream(pathAsString.split(\"/\"))\n                .filter(s -> pathAsString.length() > 0)\n                .collect(Collectors.toList());\n        for (int i = 0; i < parts.size(); i++) {\n            if (parts.get(i).equals(\"..\")) {\n                throw new IllegalStateException(\"Path element .. not allowed!\");\n            }\n        }\n    }\n\n    private Path normalisePath(Path path) {\n        validatePath(path);\n        return PathUtil.get(path.toString());\n    }\n\n    private Path fullPath(Path path, String username) {\n        Path relativePath = normalisePath(path);\n        Path result = PathUtil.get(username == null ? this.username : username).resolve(appDataDirectoryWithoutUser).resolve(relativePath);\n        return result;\n    }\n\n    private CompletableFuture<Boolean> appendFileContents(Path path, byte[] data) {\n        Path pathWithoutUsername = path.subpath(1, path.getNameCount());\n        return ctx.getByPath(username).thenCompose(userRoot -> userRoot.get().getOrMkdirs(pathWithoutUsername.getParent(), ctx.network, false, ctx.mirrorBatId(), ctx.crypto)\n                .thenCompose(dir -> dir.appendFileJS(path.getFileName().toString(), AsyncReader.build(data),\n                                0,data.length, ctx.network, ctx.crypto, x -> {})\n                        .thenApply(fw -> true)\n                ));\n    }\n\n    private CompletableFuture<Boolean> writeFileContents(Path path, byte[] data) {\n        Path pathWithoutUsername = path.subpath(1, path.getNameCount());\n        return ctx.getByPath(username).thenCompose(userRoot -> userRoot.get().getOrMkdirs(pathWithoutUsername.getParent(), ctx.network, false, ctx.mirrorBatId(), ctx.crypto)\n                .thenCompose(dir -> dir.uploadOrReplaceFile(path.getFileName().toString(), AsyncReader.build(data),\n                        data.length, ctx.network, ctx.crypto, () -> false, x -> {})\n                        .thenApply(fw -> true)\n                ));\n    }\n\n    @JsMethod\n    public CompletableFuture<byte[]> readInternal(Path relativePath, String username) {\n        Path path = fullPath(relativePath, username);\n        return readFileContents(path);\n    }\n    private CompletableFuture<byte[]> readFileContents(Path path) {\n        return ctx.getByPath(path).thenCompose(optFile -> {\n            if(optFile.isEmpty()) {\n                throw new IllegalStateException(\"File not found:\" + path.toString());\n            }\n            long len = optFile.get().getSize();\n            return optFile.get().getInputStream(ctx.network, ctx.crypto, len, 1, l-> {})\n                    .thenCompose(is -> Serialize.readFully(is, len)\n                            .thenApply(bytes -> bytes));\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> appendInternal(Path relativePath, byte[] data, String username) {\n        Path path = fullPath(relativePath, username);\n        return appendFileContents(path, data);\n    }\n    @JsMethod\n    public CompletableFuture<Boolean> writeInternal(Path relativePath, byte[] data, String username) {\n        Path path = fullPath(relativePath, username);\n        return writeFileContents(path, data);\n    }\n    @JsMethod\n    public CompletableFuture<Boolean> deleteInternal(Path relativePath, String username) {\n        Path path = fullPath(relativePath, username);\n        return ctx.getByPath(path.getParent()).thenCompose(dirOpt -> {\n            if(dirOpt.isEmpty()) {\n                throw new IllegalStateException(\"File not found:\" + path.toString());\n            }\n            FileWrapper dir = dirOpt.get();\n            String filename = path.getFileName().toString();\n            Path pathToFile = path.resolve(filename);\n            return dir.getChild(filename, ctx.crypto.hasher, ctx.network).thenCompose(file ->\n                    file.get().remove(dir, pathToFile, ctx).thenApply(fw -> true));\n        });\n    }\n    @JsMethod\n    public CompletableFuture<List<String>> dirInternal(Path relativePath, String username) {\n        Path path = relativePath == null ?\n                PathUtil.get(username == null ? this.username : username).resolve(appDataDirectoryWithoutUser)\n                : fullPath(relativePath, username);\n        return ctx.getByPath(path).thenCompose(dirOpt -> {\n            if(dirOpt.isEmpty()) {\n                return Futures.of(Collections.emptyList());\n            }\n            return dirOpt.get().getChildren(ctx.crypto.hasher, ctx.network).thenApply(files ->\n                    files.stream().map(fw -> fw.getName()).collect(Collectors.toList()));\n        });\n    }\n    @JsMethod\n    public CompletableFuture<String> mimeTypeInternal(Path relativePath, String username) {\n        Path path = relativePath == null ?\n                PathUtil.get(username == null ? this.username : username).resolve(appDataDirectoryWithoutUser)\n                : fullPath(relativePath, username);\n        return ctx.getByPath(path).thenApply(fileOpt -> {\n            if(fileOpt.isEmpty()) {\n                return \"\";\n            }\n            return fileOpt.get().getFileProperties().mimeType;\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> createDirectoryInternal(Path relativePath, String username) {\n        Path base = PathUtil.get(username == null ? this.username : username).resolve(appDataDirectoryWithoutUser);\n        return ctx.getByPath(base)\n                .thenCompose(baseOpt -> baseOpt.get().getOrMkdirs(normalisePath(relativePath), ctx.network, false, ctx.mirrorBatId(), ctx.crypto)\n                .thenApply(fw -> true));\n    }\n    /*\n    Tests if a path exists\n    @return -1 Does not exist (or not accessible), 0 File, 1 Directory\n     */\n    @JsMethod\n    public CompletableFuture<Integer> existsInternal(Path relativePath, String username) {\n        Path path = fullPath(relativePath, username);\n        return ctx.getByPath(path).thenCompose(opt -> {\n            if(opt.isEmpty()) {\n                return Futures.of(Integer.valueOf(-1));\n            }\n            return Futures.of(opt.get().getFileProperties().isDirectory ? 1 : 0);\n        });\n    }\n}\n\n"
  },
  {
    "path": "src/peergos/shared/user/CapsDiff.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class CapsDiff {\n    public final long priorReadByteOffset, priorWriteByteOffset;\n    public final ReadAndWriteCaps newCaps;\n    public final Map<String, CapsDiff> groupDiffs;\n\n    public CapsDiff(long priorReadByteOffset,\n                    long priorWriteByteOffset,\n                    ReadAndWriteCaps newCaps,\n                    Map<String, CapsDiff> groupDiffs) {\n        this.priorReadByteOffset = priorReadByteOffset;\n        this.priorWriteByteOffset = priorWriteByteOffset;\n        this.newCaps = newCaps;\n        this.groupDiffs = groupDiffs;\n    }\n\n    public int readCapCount() {\n        return newCaps.readCaps.getRetrievedCapabilities().size();\n    }\n\n    public int writeCapCount() {\n        return newCaps.writeCaps.getRetrievedCapabilities().size();\n    }\n\n    public CapsDiff flatten() {\n        Map<String, CapsDiff> flattenedGroups = groupDiffs.entrySet().stream()\n                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().flatten()));\n        return new CapsDiff(updatedReadBytes(), updatedWriteBytes(), ReadAndWriteCaps.empty(), flattenedGroups);\n    }\n\n    public List<CapabilityWithPath> getNewCaps() {\n        Stream<CapabilityWithPath> direct = Stream.concat(\n                newCaps.readCaps.getRetrievedCapabilities().stream(),\n                newCaps.writeCaps.getRetrievedCapabilities().stream());\n        return Stream.concat(direct, groupDiffs.values().stream().flatMap(d -> d.getNewCaps().stream()))\n                .collect(Collectors.toList());\n    }\n\n    public CapsDiff mergeGroups(CapsDiff other) {\n        HashMap<String, CapsDiff> combined = new HashMap<>(groupDiffs);\n        combined.putAll(other.groupDiffs);\n        return new CapsDiff(priorReadByteOffset, priorWriteByteOffset, newCaps,\n                combined);\n    }\n\n    public boolean isEmpty() {\n        return newCaps.readCaps.getBytesRead() == 0 &&\n                newCaps.writeCaps.getBytesRead() == 0 &&\n                groupDiffs.values().stream().allMatch(CapsDiff::isEmpty);\n    }\n\n    public long updatedReadBytes() {\n        return priorReadByteOffset + newCaps.readCaps.getBytesRead();\n    }\n\n    public long updatedWriteBytes() {\n        return priorWriteByteOffset + newCaps.writeCaps.getBytesRead();\n    }\n\n    public long priorBytes() {\n        return priorReadByteOffset + priorWriteByteOffset;\n    }\n\n    public static CapsDiff empty() {\n        return new CapsDiff(0, 0, ReadAndWriteCaps.empty(), Collections.emptyMap());\n    }\n\n    public static class ReadAndWriteCaps {\n        public final CapabilitiesFromUser readCaps, writeCaps;\n\n        public ReadAndWriteCaps(CapabilitiesFromUser readCaps, CapabilitiesFromUser writeCaps) {\n            this.readCaps = readCaps;\n            this.writeCaps = writeCaps;\n        }\n\n        public static ReadAndWriteCaps empty() {\n            CapabilitiesFromUser empty = new CapabilitiesFromUser(0, Collections.emptyList());\n            return new ReadAndWriteCaps(empty, empty);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/CommittedWriterData.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.io.ipfs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class CommittedWriterData implements Cborable {\n\n    public final MaybeMultihash hash;\n    public final Optional<Long> sequence;\n    public final Optional<WriterData> props;\n\n    public CommittedWriterData(MaybeMultihash hash, Optional<WriterData> props, Optional<Long> sequence) {\n        this.hash = hash;\n        this.props = props;\n        this.sequence = sequence;\n    }\n\n    public CommittedWriterData(MaybeMultihash hash, WriterData props, Optional<Long> sequence) {\n        this(hash, Optional.of(props), sequence);\n    }\n\n    @Override\n    public String toString() {\n        return sequence.map(Object::toString).orElse(\"\") + \":\" + hash.toString();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        CommittedWriterData that = (CommittedWriterData) o;\n        return Objects.equals(hash, that.hash) && sequence.equals(that.sequence);\n    }\n\n    @Override\n    public int hashCode() {\n        return hash.hashCode() ^ sequence.hashCode();\n    }\n\n    public interface Retriever {\n        CompletableFuture<CommittedWriterData> getWriterData(Cid hash, Optional<Long> sequence);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"h\", hash.toCbor());\n        sequence.ifPresent(seq -> state.put(\"s\", new CborObject.CborLong(seq)));\n        props.ifPresent(p -> state.put(\"p\", p.toCbor()));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static CommittedWriterData fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for CommittedWriterData! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        MaybeMultihash hash = m.get(\"h\", MaybeMultihash::fromCbor);\n        Optional<Long> sequence = m.getOptionalLong(\"s\");\n        Optional<WriterData> props = m.getOptional(\"p\", WriterData::fromCbor);\n        return new CommittedWriterData(hash, props, sequence);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/Committer.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface Committer {\n\n    CompletableFuture<Snapshot> commit(PublicKeyHash owner,\n                                       SigningPrivateKeyAndPublicHash signer,\n                                       Optional<WriterData> wd,\n                                       CommittedWriterData existing,\n                                       TransactionId tid);\n\n    default CompletableFuture<Snapshot> commit(PublicKeyHash owner,\n                                               SigningPrivateKeyAndPublicHash signer,\n                                               WriterData wd,\n                                               CommittedWriterData existing,\n                                               TransactionId tid) {\n        return commit(owner, signer, Optional.of(wd), existing, tid);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/CommitterBuilder.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.function.*;\n\npublic interface CommitterBuilder {\n\n    Committer buildCommitter(Committer c, PublicKeyHash owner, Supplier<Boolean> commitWatcher);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ComplexComputation.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.util.*;\n\nimport java.util.concurrent.*;\n\npublic interface ComplexComputation<V> {\n\n    CompletableFuture<Pair<Snapshot, V>> apply(Snapshot input, Committer committer);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ComplexMutation.java",
    "content": "package peergos.shared.user;\n\nimport java.util.concurrent.*;\n\npublic interface ComplexMutation {\n\n    CompletableFuture<Snapshot> apply(Snapshot input, Committer committer);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/EntryPoint.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n@JsType\npublic class EntryPoint implements Cborable {\n\n    public final AbsoluteCapability pointer;\n    public final String ownerName;\n\n    public EntryPoint(AbsoluteCapability pointer, String ownerName) {\n        this.pointer = pointer;\n        this.ownerName = ownerName;\n    }\n\n    public EntryPoint withOwner(PublicKeyHash newOwner) {\n        return new EntryPoint(pointer.withOwner(newOwner), ownerName);\n    }\n\n    public byte[] serializeAndSymmetricallyEncrypt(SymmetricKey key) {\n        byte[] nonce = key.createNonce();\n        return ArrayOps.concat(nonce, key.encrypt(serialize(), nonce));\n    }\n\n    /**\n     *\n     * @param path The path of the file this entry point corresponds to\n     * @param network\n     * @return\n     */\n    public CompletableFuture<Boolean> isValid(String path, NetworkAccess network) {\n        String[] parts = path.split(\"/\");\n        String claimedOwner = parts[1];\n        // check claimed owner actually owns the signing key\n        PublicKeyHash entryWriter = pointer.writer;\n        return network.coreNode.getPublicKeyHash(claimedOwner).thenCompose(ownerKey -> {\n            if (! ownerKey.isPresent())\n                throw new IllegalStateException(\"No owner key present for user \" + claimedOwner);\n            return UserContext.getWriterData(network, ownerKey.get(), ownerKey.get())\n                    .thenCompose(wd -> wd.props.get().ownsKey(ownerKey.get(), entryWriter, network.dhtClient, network.mutable, network.hasher));\n        });\n    }\n\n    @Override\n    @SuppressWarnings(\"unusable-by-js\")\n    public CborObject toCbor() {\n        Map<String, Cborable> cbor = new TreeMap<>();\n        cbor.put(\"c\", pointer.toCbor());\n        cbor.put(\"n\", new CborObject.CborString(ownerName));\n        return CborObject.CborMap.build(cbor);\n    }\n\n    @SuppressWarnings(\"unusable-by-js\")\n    public static EntryPoint fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor type for EntryPoint: \" + cbor);\n\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        AbsoluteCapability pointer = map.getObject(\"c\", AbsoluteCapability::fromCbor);\n        String ownerName = map.getString(\"n\");\n        return new EntryPoint(pointer, ownerName);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        EntryPoint that = (EntryPoint) o;\n        return Objects.equals(pointer, that.pointer) &&\n                Objects.equals(ownerName, that.ownerName);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(pointer, ownerName);\n    }\n\n    static EntryPoint symmetricallyDecryptAndDeserialize(byte[] input, SymmetricKey key) {\n        byte[] nonce = Arrays.copyOfRange(input, 0, 24);\n        byte[] raw = key.decrypt(Arrays.copyOfRange(input, 24, input.length), nonce);\n        return fromCbor(CborObject.fromByteArray(raw));\n    }\n\n    @Override\n    public String toString() {\n        return ownerName;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ExternalTrieNode.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class ExternalTrieNode implements TrieNode {\n    private final String dirPath; // without owner\n    private final TrieNode root;\n\n    public ExternalTrieNode(String dirPath, TrieNode root) {\n        if (dirPath.startsWith(\"//\"))\n            dirPath = dirPath.substring(1);\n        this.dirPath = dirPath;\n        this.root = root;\n    }\n\n    private String subPath(String relative) {\n        if (dirPath.endsWith(\"/\") && relative.startsWith(\"/\"))\n            return dirPath + relative.substring(1);\n        return dirPath + relative;\n    }\n\n    @Override\n    public CompletableFuture<Optional<FileWrapper>> getByPath(String path, Hasher hasher, NetworkAccess network) {\n        return root.getByPath(subPath(path), hasher, network);\n    }\n\n    @Override\n    public CompletableFuture<Optional<FileWrapper>> getByPath(String path, Snapshot version, Hasher hasher, NetworkAccess network) {\n        return root.getByPath(subPath(path), version, hasher, network);\n    }\n\n    @Override\n    public CompletableFuture<Set<FileWrapper>> getChildren(String path, Hasher hasher, NetworkAccess network) {\n        return root.getChildren(subPath(path), hasher, network);\n    }\n\n    @Override\n    public CompletableFuture<Set<FileWrapper>> getChildren(String path, Hasher hasher, Snapshot version, NetworkAccess network) {\n        return root.getChildren(subPath(path), hasher, version, network);\n    }\n\n    @Override\n    public Set<String> getChildNames() {\n        throw new IllegalStateException(\"Invalid operation\");\n    }\n\n    @Override\n    public TrieNode put(String path, EntryPoint e) {\n        throw new IllegalStateException(\"Invalid operation\");\n    }\n\n    @Override\n    public TrieNode putNode(String path, TrieNode t) {\n        throw new IllegalStateException(\"Invalid operation\");\n    }\n\n    @Override\n    public TrieNode removeEntry(String path) {\n        throw new IllegalStateException(\"Invalid operation\");\n    }\n\n    @Override\n    public Collection<TrieNode> getChildNodes() {\n        throw new IllegalStateException(\"Invalid operation\");\n    }\n\n    @Override\n    public TrieNode getChildNode(String name) {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public boolean isEmpty() {\n        throw new IllegalStateException(\"Invalid operation\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/FileSharedWithState.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\n\nimport java.util.*;\n@JsType\npublic class FileSharedWithState {\n    public static final FileSharedWithState EMPTY = new FileSharedWithState(Collections.emptySet(),\n            Collections.emptySet(), Collections.emptySet());\n    public final Set<String> readAccess, writeAccess;\n    public final Set<LinkProperties> links;\n\n    public FileSharedWithState(Set<String> readAccess, Set<String> writeAccess, Set<LinkProperties> links) {\n        this.readAccess = readAccess;\n        this.writeAccess = writeAccess;\n        this.links = links;\n    }\n\n    public Set<String> get(SharedWithCache.Access type) {\n        if (type == SharedWithCache.Access.READ)\n            return readAccess;\n        return writeAccess;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/FriendAnnotation.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\n\nimport java.util.*;\n\npublic class FriendAnnotation implements Cborable {\n\n    private final String username;\n    private final boolean isVerified;\n    private final List<PublicKeyHash> keysAtVerification;\n\n    @JsConstructor\n    public FriendAnnotation(String username, boolean isVerified, List<PublicKeyHash> keysAtVerification) {\n        this.username = username;\n        this.isVerified = isVerified;\n        this.keysAtVerification = keysAtVerification;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    @JsMethod\n    public boolean isVerified() {\n        return isVerified;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"u\", new CborObject.CborString(username));\n        state.put(\"v\", new CborObject.CborBoolean(isVerified));\n        state.put(\"k\", new CborObject.CborList(keysAtVerification));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static FriendAnnotation fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for FriendAnnotation: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        String username = m.getString(\"u\");\n        boolean isVerified = m.getBoolean(\"v\");\n        List<PublicKeyHash> keys = m.getList(\"k\", PublicKeyHash::fromCbor);\n        return new FriendAnnotation(username, isVerified, keys);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/FriendSourcedTrieNode.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.social.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class FriendSourcedTrieNode implements TrieNode {\n\n    public final String ownerName;\n    private final IncomingCapCache cache;\n    private final EntryPoint sharedDir;\n    private final Crypto crypto;\n    private final List<EntryPoint> groups;\n    private GroupAdder groupAdder;\n\n    public FriendSourcedTrieNode(IncomingCapCache cache,\n                                 String ownerName,\n                                 EntryPoint sharedDir,\n                                 GroupAdder groupAdder,\n                                 Crypto crypto) {\n        this.cache = cache;\n        this.ownerName = ownerName;\n        this.sharedDir = sharedDir;\n        this.crypto = crypto;\n        this.groups = new ArrayList<>();\n        this.groupAdder = groupAdder;\n    }\n\n    public interface GroupAdder {\n        CompletableFuture<Snapshot> add(CapabilityWithPath cap, String owner, NetworkAccess network, Snapshot s, Committer c);\n    }\n\n    public synchronized void addGroup(EntryPoint group) {\n        if (! groups.contains(group))\n            groups.add(group);\n    }\n\n    public static CompletableFuture<Optional<FriendSourcedTrieNode>> build(IncomingCapCache cache,\n                                                                           EntryPoint e,\n                                                                           NetworkAccess network,\n                                                                           Crypto crypto) {\n        return Futures.of(Optional.of(new FriendSourcedTrieNode(cache, e.ownerName, e, null, crypto)));\n    }\n\n    public static CompletableFuture<Optional<FriendSourcedTrieNode>> build(IncomingCapCache cache,\n                                                                           EntryPoint e,\n                                                                           GroupAdder groupAdder,\n                                                                           NetworkAccess network,\n                                                                           Crypto crypto) {\n        return Futures.of(Optional.of(new FriendSourcedTrieNode(cache, e.ownerName, e, groupAdder, crypto)));\n    }\n\n    public CompletableFuture<Snapshot> getLatestVersion(NetworkAccess network) {\n        return cache.getLatestVersion(sharedDir, network);\n    }\n\n    /**\n     *\n     * @param crypto\n     * @param network\n     * @return Any new capabilities from the friend and the previously processed size of caps in bytes\n     */\n    public synchronized CompletableFuture<Pair<Snapshot, CapsDiff>> ensureUptodate(Snapshot s,\n                                                                                   Committer c,\n                                                                                   Crypto crypto,\n                                                                                   NetworkAccess network) {\n        return cache.ensureFriendUptodate(ownerName, sharedDir, groups, s, c, network);\n    }\n\n    public synchronized CompletableFuture<CapsDiff> getCaps(ProcessedCaps current,\n                                                            Snapshot s,\n                                                            NetworkAccess network) {\n        return cache.getCapsFrom(ownerName, sharedDir, groups, current, s,network);\n    }\n\n    private CompletableFuture<Optional<FileWrapper>> getFriendRoot(NetworkAccess network) {\n        return NetworkAccess.getLatestEntryPoint(sharedDir, network)\n                .thenCompose(sharedDir -> {\n                    return sharedDir.file.retrieveParent(network)\n                            .thenCompose(sharedOpt -> {\n                                if (sharedOpt.isEmpty()) {\n                                    CompletableFuture<Optional<FileWrapper>> empty = CompletableFuture.completedFuture(Optional.empty());\n                                    return empty;\n                                }\n                                return sharedOpt.get().retrieveParent(network);\n                            });\n                }).exceptionally(t -> {\n                    System.out.println(\"Couldn't retrieve entry point for friend: \" + sharedDir.ownerName + \". Did they remove you as a follower?\");\n                    return Optional.empty();\n                });\n    }\n\n    private FileWrapper convert(FileWrapper file, String path) {\n        return file.withTrieNode(new ExternalTrieNode(path, this));\n    }\n\n    public CompletableFuture<Pair<Snapshot, CapsDiff>> updateIncludingGroups(Snapshot s,\n                                                                             Committer c,\n                                                                             NetworkAccess network) {\n        return ensureUptodate(s, c, crypto, network)\n                .thenCompose(p -> {\n                    List<CapabilityWithPath> newGroups = p.right.getNewCaps().stream()\n                            .filter(cap -> cap.path.startsWith(\"/\" + ownerName + \"/\" + UserContext.SHARED_DIR_NAME))\n                            .collect(Collectors.toList());\n                    for (CapabilityWithPath groupCap : newGroups) {\n                        addGroup(new EntryPoint(groupCap.cap, ownerName));\n                    }\n                    if (newGroups.isEmpty())\n                        return Futures.of(p);\n                    return Futures.reduceAll(newGroups, p.left, (b, cap) -> groupAdder.add(cap, ownerName, network, b, c), (a, b) -> b)\n                            .thenCompose(res -> ensureUptodate(res, c, crypto, network));\n                });\n    }\n\n    @Override\n    public synchronized CompletableFuture<Optional<FileWrapper>> getByPath(String path,\n                                                                           Hasher hasher,\n                                                                           NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        if (path.isEmpty() || path.equals(\"/\"))\n            return getFriendRoot(network)\n                    .thenApply(opt -> opt.map(f -> f.withTrieNode(this)));\n        Path file = PathUtil.get(ownerName + path);\n        return network.synchronizer.applyComplexUpdate(cache.owner(), cache.signingPair(), (v, c) -> getLatestVersion(network)\n                .thenCompose(s -> updateIncludingGroups(v.mergeAndOverwriteWith(s), c, network)).thenApply(p -> p.left))\n                .thenCompose(v -> cache.getByPath(file, v, hasher, network))\n                .thenApply(opt -> opt.map(f -> convert(f, path)))\n                .exceptionally(t ->  Optional.empty());\n    }\n\n    @Override\n    public synchronized CompletableFuture<Optional<FileWrapper>> getByPath(String path,\n                                                                           Snapshot version,\n                                                                           Hasher hasher,\n                                                                           NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        if (path.isEmpty() || path.equals(\"/\"))\n            return getFriendRoot(network)\n                    .thenApply(opt -> opt.map(f -> f.withTrieNode(this)));\n        Path file = PathUtil.get(ownerName + path);\n        return cache.getByPath(file, version, hasher, network)\n                .thenApply(opt -> opt.map(f -> convert(f, path)));\n    }\n\n    private static String canonicalise(String path) {\n        if (path.endsWith(\"/\"))\n            return path.substring(0, path.length() - 1);\n        return path;\n    }\n\n    @Override\n    public synchronized CompletableFuture<Set<FileWrapper>> getChildren(String path,\n                                                                        Hasher hasher,\n                                                                        NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        Path dir = PathUtil.get(ownerName + path);\n        return network.synchronizer.applyComplexUpdate(cache.owner(), cache.signingPair(), (v, c) -> getLatestVersion(network)\n                .thenCompose(s -> updateIncludingGroups(v.mergeAndOverwriteWith(s), c, network)).thenApply(p -> p.left))\n                .thenCompose(v -> cache.getChildren(dir, v, hasher, network))\n                .thenApply(children -> children.stream()\n                        .map(f -> convert(f, canonicalise(path) + \"/\" + f.getName()))\n                        .collect(Collectors.toSet()))\n                .exceptionally(t -> Collections.emptySet());\n    }\n\n    @Override\n    public synchronized CompletableFuture<Set<FileWrapper>> getChildren(String path,\n                                                                        Hasher hasher,\n                                                                        Snapshot version,\n                                                                        NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        Path dir = PathUtil.get(ownerName + path);\n        return cache.getChildren(dir, version, hasher, network)\n                .thenApply(children -> children.stream()\n                        .map(f -> convert(f, canonicalise(path) + \"/\" + f.getName()))\n                        .collect(Collectors.toSet()));\n    }\n\n    @Override\n    public synchronized Set<String> getChildNames() {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public synchronized TrieNode put(String path, EntryPoint e) {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public synchronized TrieNode putNode(String path, TrieNode t) {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public synchronized TrieNode removeEntry(String path) {\n        if (TrieNode.canonicalise(path).isEmpty())\n            return TrieNodeImpl.empty();\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public Collection<TrieNode> getChildNodes() {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public TrieNode getChildNode(String name) {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n\n    @Override\n    public boolean isEmpty() {\n        throw new IllegalStateException(\"Not valid operation on FriendSourcedTrieNode.\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/FriendsGroups.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class FriendsGroups implements Cborable {\n\n    public final Map<String, EntryPoint> pathToGroup;\n\n    public FriendsGroups(Map<String, EntryPoint> pathToGroup) {\n        this.pathToGroup = pathToGroup;\n    }\n\n    public Set<EntryPoint> getFriends(String friend) {\n        return pathToGroup.entrySet().stream()\n                .filter(e -> e.getKey().startsWith(friend) || e.getKey().startsWith(\"/\" + friend))\n                .map(e -> e.getValue())\n                .collect(Collectors.toSet());\n    }\n\n    public FriendsGroups addGroup(CapabilityWithPath group, String owner) {\n        HashMap<String, EntryPoint> updated = new HashMap<>(pathToGroup);\n        updated.put(group.path, new EntryPoint(group.cap, owner));\n        return new FriendsGroups(updated);\n    }\n\n    public static FriendsGroups empty() {\n        return new FriendsGroups(Collections.emptyMap());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> cbor = new TreeMap<>();\n\n        Map<String, Cborable> groups = pathToGroup.entrySet().stream()\n                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));\n        cbor.put(\"g\", CborObject.CborMap.build(groups));\n        return CborObject.CborMap.build(cbor);\n    }\n\n    public static FriendsGroups fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        Map<String, EntryPoint> groups = m.getMap(\"g\", c -> ((CborObject.CborString) c).value, EntryPoint::fromCbor);\n        return new FriendsGroups(groups);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/Groups.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.function.*;\n\npublic class Groups implements Cborable {\n    public final Map<String, String> uidToGroupName;\n\n    public Groups(Map<String, String> uidToGroupName) {\n        this.uidToGroupName = uidToGroupName;\n    }\n\n    public static Groups generate(SafeRandom r) {\n        Map<String, String> uidToNames = new TreeMap<>();\n        uidToNames.put(generateUid(r), SocialState.FRIENDS_GROUP_NAME);\n        uidToNames.put(generateUid(r), SocialState.FOLLOWERS_GROUP_NAME);\n        return new Groups(uidToNames);\n    }\n\n    /* Generate a uid that cannot clash with a username, but which is a valid filename\n     */\n    public static String generateUid(SafeRandom r) {\n        return \".\" + ArrayOps.bytesToHex(r.randomBytes(32));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        SortedMap<String, Cborable> uidToNames = new TreeMap<>();\n        for (Map.Entry<String, String> e : uidToGroupName.entrySet()) {\n            uidToNames.put(e.getKey(), new CborObject.CborString(e.getValue()));\n        }\n\n        state.put(\"n\", CborObject.CborMap.build(uidToNames));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static Groups fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for Groups!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        CborObject.CborMap r = m.get(\"n\", c -> (CborObject.CborMap) c);\n        Function<Cborable, String> getString = c -> ((CborObject.CborString) c).value;\n        Map<String, String> uidToNames = r.toMap(getString, getString);\n\n        return new Groups(uidToNames);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/HttpAccount.java",
    "content": "\npackage peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.login.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class HttpAccount implements AccountProxy {\n    private static final String P2P_PROXY_PROTOCOL = \"/http\";\n\n    private final HttpPoster direct, p2p;\n    private final String directUrlPrefix;\n\n    public HttpAccount(HttpPoster direct, HttpPoster p2p) {\n        this.direct = direct;\n        this.p2p = p2p;\n        this.directUrlPrefix = \"\";\n    }\n\n    public HttpAccount(HttpPoster p2p, Multihash targetNodeID) {\n        this.directUrlPrefix = getProxyUrlPrefix(targetNodeID);\n        this.direct = p2p;\n        this.p2p = p2p;\n    }\n\n    private static String getProxyUrlPrefix(Multihash targetId) {\n        return \"/p2p/\" + targetId.toString() + P2P_PROXY_PROTOCOL + \"/\";\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        return setLoginData(directUrlPrefix, direct, login, auth, forceLocal);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(Multihash targetServerId, LoginData login, byte[] auth) {\n        return setLoginData(getProxyUrlPrefix(targetServerId), p2p, login, auth, false);\n    }\n\n    private CompletableFuture<Boolean> setLoginData(String urlPrefix,\n                                                    HttpPoster poster,\n                                                    LoginData login,\n                                                    byte[] auth,\n                                                    boolean forceLocal) {\n        return poster.postUnzip(urlPrefix + Constants.LOGIN_URL + \"setLogin?username=\" + login.username\n                + \"&auth=\" + ArrayOps.bytesToHex(auth)\n                + \"&local=\" + forceLocal, login.serialize()).thenApply(res -> {\n            DataInputStream din = new DataInputStream(new ByteArrayInputStream(res));\n            try {\n                return din.readBoolean();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse>  mfa,\n                                                                                          boolean cacheMfaLoginData,\n                                                                                          boolean forceProxy,\n                                                                                          boolean forceNoCache) {\n        return getLoginData(directUrlPrefix, direct, username, authorisedReader, auth, mfa, forceProxy);\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(Multihash targetServerId,\n                                                                                          String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse> mfa) {\n        return getLoginData(getProxyUrlPrefix(targetServerId), p2p, username, authorisedReader, auth, mfa, true);\n    }\n\n    private CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String urlPrefix,\n                                                                                           HttpPoster poster,\n                                                                                           String username,\n                                                                                           PublicSigningKey authorisedReader,\n                                                                                           byte[] auth,\n                                                                                           Optional<MultiFactorAuthResponse> mfa,\n                                                                                           boolean forceProxy) {\n        return poster.get(urlPrefix + Constants.LOGIN_URL + \"getLogin?username=\" + username\n                        + \"&author=\" + ArrayOps.bytesToHex(authorisedReader.serialize())\n                        + \"&auth=\" + ArrayOps.bytesToHex(auth)\n                        + \"&proxy=\" + forceProxy\n                        + mfa.map(mfaCode -> \"&mfa=\" + ArrayOps.bytesToHex(mfaCode.serialize())).orElse(\"\"))\n                .thenApply(res -> LoginResponse.fromCbor(CborObject.fromByteArray(res)).resp);\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        return getSecondAuthMethods(directUrlPrefix, direct, username, auth);\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(Multihash targetServerId, String username, byte[] auth) {\n        return getSecondAuthMethods(getProxyUrlPrefix(targetServerId), p2p, username, auth);\n    }\n\n    private CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String urlPrefix,\n                                                                                HttpPoster poster,\n                                                                                String username,\n                                                                                byte[] auth) {\n        return poster.get(urlPrefix + Constants.LOGIN_URL + \"listMfa?username=\" + username\n                        + \"&auth=\" + ArrayOps.bytesToHex(auth))\n                .thenApply(res -> ((CborObject.CborList)CborObject.fromByteArray(res)).map(MultiFactorAuthMethod::fromCbor));\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        return addTotpFactor(directUrlPrefix, direct, username, auth);\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(Multihash targetServerId, String username, byte[] auth) {\n        return addTotpFactor(getProxyUrlPrefix(targetServerId), p2p, username, auth);\n    }\n\n    private CompletableFuture<TotpKey> addTotpFactor(String urlPrefix, HttpPoster poster, String username, byte[] auth) {\n        return poster.get(urlPrefix + Constants.LOGIN_URL + \"addTotp?username=\" + username\n                        + \"&auth=\" + ArrayOps.bytesToHex(auth))\n                .thenApply(res -> TotpKey.fromString(new String(res)));\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        return registerSecurityKeyStart(directUrlPrefix, direct, username, auth);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(Multihash targetServerId, String username, byte[] auth) {\n        return registerSecurityKeyStart(getProxyUrlPrefix(targetServerId), p2p, username, auth);\n    }\n\n    private CompletableFuture<byte[]> registerSecurityKeyStart(String urlPrefix, HttpPoster poster, String username, byte[] auth) {\n        return poster.get(urlPrefix + Constants.LOGIN_URL + \"registerWebauthnStart?username=\" + username\n                + \"&auth=\" + ArrayOps.bytesToHex(auth));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        return registerSecurityKeyComplete(directUrlPrefix, direct, username, keyName, resp, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(Multihash targetServerId, String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        return registerSecurityKeyComplete(getProxyUrlPrefix(targetServerId), p2p, username, keyName, resp, auth);\n    }\n\n    private CompletableFuture<Boolean> registerSecurityKeyComplete(String urlPrefix, HttpPoster poster, String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        return poster.post(urlPrefix + Constants.LOGIN_URL + \"registerWebauthnComplete?username=\" + username\n                        + \"&keyname=\" + keyName\n                        + \"&auth=\" + ArrayOps.bytesToHex(auth), resp.serialize(), true)\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        return enableTotpFactor(directUrlPrefix, direct, username, credentialId, code, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(Multihash targetServerId, String username, byte[] credentialId, String code, byte[] auth) {\n        return enableTotpFactor(getProxyUrlPrefix(targetServerId), p2p, username, credentialId, code, auth);\n    }\n\n    private CompletableFuture<Boolean> enableTotpFactor(String urlPrefix,\n                                                        HttpPoster poster,\n                                                        String username,\n                                                        byte[] credentialId,\n                                                        String code,\n                                                        byte[] auth) {\n        return poster.get(urlPrefix + Constants.LOGIN_URL + \"enableTotp?username=\" + username\n                        + \"&credid=\" + ArrayOps.bytesToHex(credentialId)\n                        + \"&auth=\" + ArrayOps.bytesToHex(auth)\n                        + \"&code=\" + code)\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        return deleteSecondFactor(directUrlPrefix, direct, username, credentialId, auth);\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(Multihash targetServerId, String username, byte[] credentialId, byte[] auth) {\n        return deleteSecondFactor(getProxyUrlPrefix(targetServerId), p2p, username, credentialId, auth);\n    }\n\n    private CompletableFuture<Boolean> deleteSecondFactor(String urlPrefix,\n                                                          HttpPoster poster,\n                                                          String username,\n                                                          byte[] credentialId,\n                                                          byte[] auth) {\n        return poster.get(urlPrefix + Constants.LOGIN_URL + \"deleteMfa?username=\" + username\n                        + \"&credid=\" + ArrayOps.bytesToHex(credentialId)\n                        + \"&auth=\" + ArrayOps.bytesToHex(auth))\n                .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/HttpPoster.java",
    "content": "package peergos.shared.user;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface HttpPoster {\n\n    CompletableFuture<byte[]> post(String url, byte[] payload, boolean unzip, int timeoutMillis);\n\n    default CompletableFuture<byte[]> post(String url, byte[] payload, boolean unzip) {\n        return post(url, payload, unzip, 15_000);\n    }\n\n    CompletableFuture<byte[]> postUnzip(String url, byte[] payload, int timeoutMillis);\n\n    default CompletableFuture<byte[]> postUnzip(String url, byte[] payload) {\n        return postUnzip(url, payload, 15_000);\n    }\n\n    default CompletableFuture<byte[]> postMultipart(String url, List<byte[]> files) {\n        return postMultipart(url, files, -1);\n    }\n\n    CompletableFuture<byte[]> postMultipart(String url, List<byte[]> files, int timeoutMillis);\n\n    CompletableFuture<byte[]> put(String url, byte[] payload, Map<String, String> headers);\n\n    default CompletableFuture<byte[]> put(String url, byte[] payload, Map<String, String> headers, int timeoutMillis) {\n        return put(url, payload, headers);\n    }\n\n    CompletableFuture<byte[]> get(String url, Map<String, String> headers);\n\n    default CompletableFuture<byte[]> get(String url) {\n        // This changes to a POST with an empty body\n        // The reason for this is browsers allow any website to do a get request to localhost\n        // but they block POST requests. So this prevents random websites from calling APIs on localhost\n        return postUnzip(url, new byte[0]);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/user/IdentityLink.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class IdentityLink implements Cborable {\n\n    @JsType\n    public enum KnownService {\n        Peergos(0, Usernames.REGEX),\n        Twitter(1, \"^[A-Za-z0-9_]{1,15}$\"),\n        Facebook(2, \"^[a-z\\\\d.]{5,}$\"),\n        Website(3, \"^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\\\.)+[A-Za-z]{2,6}$\"),\n        Reddit(4, \"^[A-Za-z0-9]{1,20}$\"),\n        Github(5, \"^[a-z\\\\d](?:[a-z\\\\d]|-(?=[a-z\\\\d])){0,38}$\"),\n        HackerNews(6, \"^[a-z0-9_-]{2,15}$\"),\n        Lobsters(7, \"^[a-z0-9]{1,18}$\"),\n        LinkedIn(8, \"^[A-Za-z0-9-]{5,30}$\"),\n        Mastodon(9, \"[a-z0-9_]{1,30}@((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\\\.)+[A-Za-z]{2,6}\"),\n        Matrix(10, \"@[a-z0-9_.-=/]{1,249}:((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\\\.)+[A-Za-z]{2,6}\");\n\n        public int code;\n        public String usernameRegex;\n\n        KnownService(int code, String usernameRegex) {\n            this.code = code;\n            this.usernameRegex = usernameRegex;\n        }\n\n        private static Map<Integer, KnownService> lookup = new TreeMap<>();\n        static {\n            for (KnownService t: KnownService.values())\n                lookup.put(t.code, t);\n        }\n        private static Map<String, KnownService> lookupByName = new TreeMap<>();\n        static {\n            for (KnownService t: KnownService.values())\n                lookupByName.put(t.name().toLowerCase(), t);\n        }\n\n        public static KnownService byCode(int t) {\n            if (!lookup.containsKey(t))\n                throw new IllegalStateException(\"Unknown Identity Service code: \" + t);\n            return lookup.get(t);\n        }\n\n        public static KnownService byName(String name) {\n            KnownService result = lookupByName.get(name.toLowerCase());\n            if (result == null)\n                throw new IllegalStateException(\"Unknown Identity Service: \" + name);\n            return result;\n        }\n    }\n\n    public static class IdentityService implements Cborable {\n        public final Either<KnownService, String> name;\n\n        public IdentityService(Either<KnownService, String> name) {\n            this.name = name;\n        }\n\n        public String usernameRegex() {\n            if (name.isB())\n                return \".+\";\n            KnownService service = name.a();\n            return service.usernameRegex;\n        }\n\n        @JsMethod\n        public String name() {\n            return name.map(s -> s.name(), s -> s);\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            if (name.isA()) {\n                state.put(\"t\", new CborObject.CborLong(0));\n                state.put(\"c\", new CborObject.CborLong(name.a().code));\n            } else {\n                state.put(\"t\", new CborObject.CborLong(1));\n                state.put(\"n\", new CborObject.CborString(name.b()));\n            }\n            return CborObject.CborMap.build(state);\n        }\n\n        public static IdentityService fromCbor(Cborable cbor) {\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n            long type = m.getLong(\"t\");\n            if (type == 0) {\n                return new IdentityService(Either.a(KnownService.byCode((int)m.getLong(\"c\"))));\n            } else if (type == 1) {\n                return new IdentityService(Either.b(m.getString(\"n\")));\n            } else throw new IllegalStateException(\"Unknown IdentityService type: \" + type);\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            IdentityService that = (IdentityService) o;\n            return Objects.equals(name, that.name);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(name);\n        }\n\n        public static IdentityService parse(String name) {\n            try {\n                return new IdentityService(Either.a(KnownService.byName(name)));\n            } catch (Exception e) {\n                return new IdentityService(Either.b(name));\n            }\n        }\n    }\n\n    @JsProperty\n    public final String usernameA, usernameB;\n    @JsProperty\n    public final IdentityService serviceA, serviceB;\n\n    public IdentityLink(String usernameA, IdentityService serviceA, String usernameB, IdentityService serviceB) {\n        this.usernameA = usernameA;\n        this.serviceA = serviceA;\n        this.usernameB = usernameB;\n        this.serviceB = serviceB;\n    }\n\n    public String textToPost() {\n        return \"I am \" + usernameA + \" on \" + serviceA.name() + \" and \" + usernameB + \" on \" + serviceB.name();\n    }\n\n    public static IdentityLink parse(String firstLine) {\n        String[] parts = firstLine.trim().split(\" \");\n        if (!parts[0].equals(\"I\") || !parts[1].equals(\"am\") || !parts[3].equals(\"on\") || !parts[5].equals(\"and\") || !parts[7].equals(\"on\"))\n            throw new IllegalStateException(\"Invalid text for IdentityLink\");\n        String usernameA = parts[2];\n        IdentityService serviceA = IdentityService.parse(parts[4]);\n        String usernameB = parts[6];\n        IdentityService serviceB = IdentityService.parse(parts[8]);\n        return new IdentityLink(usernameA, serviceA, usernameB, serviceB);\n    }\n\n    public static CompletableFuture<IdentityLink> decrypt(String encryptedPost, SymmetricKey key, PublicSigningKey identity) {\n        CipherText parsed = CipherText.fromCbor(CborObject.fromByteArray(Base58.decode(encryptedPost.trim())));\n        byte[] decrypted = parsed.decrypt(key, c -> ((CborObject.CborByteArray) c).value);\n        return identity.unsignMessage(decrypted)\n                .thenApply(unsigned -> IdentityLink.fromCbor(CborObject.fromByteArray(unsigned)));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> cborData = new TreeMap<>();\n        cborData.put(\"ua\", new CborObject.CborString(usernameA));\n        cborData.put(\"sa\", serviceA);\n        cborData.put(\"ub\", new CborObject.CborString(usernameB));\n        cborData.put(\"sb\", serviceB);\n        return CborObject.CborMap.build(cborData);\n    }\n\n    public static IdentityLink fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for AlternativeIdentityClaim: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        String usernameA = m.getString(\"ua\");\n        IdentityService serviceA = m.get(\"sa\", IdentityService::fromCbor);\n        String usernameB = m.getString(\"ub\");\n        IdentityService serviceB = m.get(\"sb\", IdentityService::fromCbor);\n\n        return new IdentityLink(usernameA, serviceA, usernameB, serviceB);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        IdentityLink that = (IdentityLink) o;\n        return Objects.equals(usernameA, that.usernameA) &&\n                Objects.equals(usernameB, that.usernameB) &&\n                Objects.equals(serviceA, that.serviceA) &&\n                Objects.equals(serviceB, that.serviceB);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(usernameA, usernameB, serviceA, serviceB);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/IdentityLinkProof.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class IdentityLinkProof implements Cborable {\n    public static final String SIG_PREFIX = \"\\nsig: \";\n\n    @JsProperty\n    public final IdentityLink claim;\n    public final byte[] signature;\n    // This allows us to post proofs to other services that reveal nothing to someone without this key\n    public final Optional<SymmetricKey> encryptionKey;\n    @JsProperty\n    public final Optional<String> postUrl;\n\n    public IdentityLinkProof(IdentityLink claim,\n                             byte[] signature,\n                             Optional<SymmetricKey> encryptionKey,\n                             Optional<String> postUrl) {\n        this.claim = claim;\n        this.signature = signature;\n        this.encryptionKey = encryptionKey;\n        this.postUrl = postUrl;\n    }\n\n    @JsMethod\n    public boolean hasUrl() {\n        return postUrl.isPresent();\n    }\n\n    public byte[] signedClaim() {\n        byte[] body = claim.serialize();\n        return ArrayOps.concat(signature, body);\n    }\n\n    public CompletableFuture<Boolean> isValid(PublicSigningKey peergosIdentity) {\n        return peergosIdentity.unsignMessage(signedClaim()).thenApply(unsigned -> {\n            IdentityLink signedClaim = IdentityLink.fromCbor(CborObject.fromByteArray(unsigned));\n            if (!signedClaim.equals(claim))\n                throw new IllegalStateException(\"Signature invalid!\");\n            return true;\n        });\n    }\n\n    @JsMethod\n    public String encodedSignature() {\n        return Base58.encode(signature);\n    }\n\n    public String postText(String urlToPeergosPost) {\n        return claim.textToPost() + SIG_PREFIX + encodedSignature() + \"\\nproof: \" + urlToPeergosPost;\n    }\n\n    public String getFilename() {\n        return claim.usernameB + \".\" + claim.serviceB.name() + \".id.cbor\";\n    }\n\n    public String getUrlToPost(String peergosServerUrl, FileWrapper proofFile, boolean isPublic) {\n        if (peergosServerUrl.endsWith(\"/\"))\n            peergosServerUrl = peergosServerUrl.substring(0, peergosServerUrl.length() - 1);\n        if (isPublic) {\n            String pathToProof = claim.usernameA + \"/.profile/ids/\" + getFilename();\n            String path = \"/public/\" + pathToProof + \"?open=true\";\n            return peergosServerUrl + path;\n        }\n        return peergosServerUrl + \"/#%7B%22secretLink%22:true%2c%22link%22:%22\" + proofFile.toLink() + \"%22%2c%22open%22:true%7D\";\n    }\n\n    public String encryptedPostText() {\n        if (encryptionKey.isEmpty())\n            throw new IllegalStateException(\"No encryption key present on Identity proof!\");\n\n        SymmetricKey key = encryptionKey.get();\n        CipherText encrypted = CipherText.build(key, new CborObject.CborByteArray(signedClaim()));\n        return Base58.encode(encrypted.serialize());\n    }\n\n    public IdentityLinkProof withPostUrl(String postUrl) {\n        return new IdentityLinkProof(claim, signature, encryptionKey, Optional.of(postUrl));\n    }\n\n    public IdentityLinkProof withKey(SymmetricKey key) {\n        return new IdentityLinkProof(claim, signature, Optional.of(key), postUrl);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> cborData = new TreeMap<>();\n        cborData.put(\"c\", claim);\n        cborData.put(\"s\", new CborObject.CborByteArray(signature));\n        encryptionKey.ifPresent(k -> cborData.put(\"k\", k));\n        postUrl.ifPresent(ap -> cborData.put(\"bu\", new CborObject.CborString(ap)));\n\n        List<CborObject> contents = new ArrayList<>();\n        contents.add(new CborObject.CborLong(MimeTypes.CBOR_PEERGOS_IDENTITY_PROOF_INT));\n        contents.add(CborObject.CborMap.build(cborData));\n\n        return new CborObject.CborList(contents);\n    }\n\n    @JsMethod\n    public static IdentityLinkProof fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for TodoList: \" + cbor);\n\n        List<? extends Cborable> contents = ((CborObject.CborList) cbor).value;\n        long mimeType = ((CborObject.CborLong) contents.get(0)).value;\n        if (mimeType != MimeTypes.CBOR_PEERGOS_IDENTITY_PROOF_INT)\n            throw new IllegalStateException(\"Invalid mimetype for AlternativeIdentityProof: \" + mimeType);\n\n        CborObject.CborMap m = (CborObject.CborMap) contents.get(1);\n        IdentityLink claim = m.get(\"c\", IdentityLink::fromCbor);\n        Optional<SymmetricKey> encryptionKey = m.getOptional(\"k\", SymmetricKey::fromCbor);\n        byte[] signature = m.getByteArray(\"s\");\n        Optional<String> alternativeUrl = m.getOptional(\"bu\", c -> ((CborObject.CborString) c).value);\n\n        return new IdentityLinkProof(claim, signature, encryptionKey, alternativeUrl);\n    }\n\n    @JsMethod\n    public static IdentityLinkProof parse(String postContents) {\n        String line1 = postContents.trim().split(\"\\n\")[0];\n        IdentityLink claim = IdentityLink.parse(line1);\n        int signatureStart = postContents.indexOf(SIG_PREFIX) + SIG_PREFIX.length();\n        String signatureText = postContents.substring(signatureStart, postContents.indexOf(\"\\n\", signatureStart)).trim();\n        byte[] signature = Base58.decode(signatureText);\n        return new IdentityLinkProof(claim, signature, Optional.empty(), Optional.empty());\n    }\n\n    @JsMethod\n    public static CompletableFuture<IdentityLinkProof> buildAndSign(SigningPrivateKeyAndPublicHash signer,\n                                                                   String peergosUsername,\n                                                                   String alternateUsername,\n                                                                   String alternateService) {\n        IdentityLink.IdentityService serviceA = new IdentityLink.IdentityService(Either.a(IdentityLink.KnownService.Peergos));\n        IdentityLink.IdentityService serviceB = IdentityLink.IdentityService.parse(alternateService);\n        IdentityLink claim = new IdentityLink(peergosUsername, serviceA, alternateUsername, serviceB);\n        return signer.secret.signatureOnly(claim.serialize())\n                .thenApply(signature -> new IdentityLinkProof(claim, signature, Optional.empty(), Optional.empty()));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ImmutableTree.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/**\n * A content-addressed version of a Map&lt;byte[], Multihash&gt;\n */\npublic interface ImmutableTree<V extends Cborable> {\n\n    /**\n     *\n     * @param rawKey\n     * @return value stored under rawKey\n     * @throws IOException\n     */\n    CompletableFuture<Optional<V>> get(byte[] rawKey);\n\n    /**\n     *\n     * @param rawKey\n     * @param value\n     * @return hash of new tree root\n     * @throws IOException\n     */\n    CompletableFuture<Multihash> put(PublicKeyHash owner,\n                                     SigningPrivateKeyAndPublicHash writer,\n                                     byte[] rawKey,\n                                     Optional<V> existing,\n                                     V value,\n                                     Optional<BatId> mirrorBat,\n                                     TransactionId tid);\n\n    /**\n     *\n     * @param rawKey\n     * @return hash of new tree root\n     * @throws IOException\n     */\n    CompletableFuture<Multihash> remove(PublicKeyHash owner,\n                                        SigningPrivateKeyAndPublicHash writer,\n                                        byte[] rawKey,\n                                        Optional<V> existing,\n                                        Optional<BatId> mirrorBat,\n                                        TransactionId tid);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/IncomingCapCache.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.inode.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** A lookup from path to capabilities shared with you\n *\n *  To avoid reparsing the entire capability list at every login, new capabilities are retrieved along with their path\n *  and stored in a mirror directory tree that is your view of the world. This world mirror is rooted at\n *  /recipient_user/.capabilitycache/world/\n *  If user B shares /A/a-path/some-file.txt (from A) with C, then C will store the SharedItem in\n *  /C/.capabilitycache/world/A/a-path/\n *\n *  How many records and bytes we have processed from each friend (ProcessedCaps) is stored in\n *  /C/.capabilitycache/friend-name$incoming.cbor\n */\npublic class IncomingCapCache {\n    private static final String WORLD_ROOT_NAME = \"world\";\n    private static final String FRIEND_STATE_SUFFIX = \"$incoming.cbor\";\n    private static final String DIR_STATE = \"items.cbor\";\n\n    private FileWrapper cacheRoot, worldRoot;\n    private final Map<PublicKeyHash, Pair<MaybeMultihash, CapsDiff>> pointerCache;\n    private final Crypto crypto;\n    private final Hasher hasher;\n\n    public IncomingCapCache(FileWrapper cacheRoot, FileWrapper worldRoot, Crypto crypto) {\n        this.cacheRoot = cacheRoot;\n        this.worldRoot = worldRoot;\n        this.crypto = crypto;\n        this.hasher = crypto.hasher;\n        this.pointerCache = new HashMap<>();\n    }\n\n    public static CompletableFuture<IncomingCapCache> build(FileWrapper cacheRoot, Optional<BatId> mirrorBatId, Crypto crypto, NetworkAccess network) {\n        return cacheRoot.getOrMkdirs(PathUtil.get(WORLD_ROOT_NAME), network, true, mirrorBatId, crypto)\n                .thenApply(worldRoot -> new IncomingCapCache(cacheRoot, worldRoot, crypto));\n    }\n\n    public PublicKeyHash owner() {\n        return worldRoot.owner();\n    }\n\n    public SigningPrivateKeyAndPublicHash signingPair() {\n        return worldRoot.signingPair();\n    }\n\n    public static class ChildElement implements Cborable {\n        public final PathElement name;\n        public final AbsoluteCapability cap;\n        public final List<String> sharers;\n\n        public ChildElement(PathElement name, AbsoluteCapability cap, List<String> sharers) {\n            this.name = name;\n            this.cap = cap;\n            this.sharers = sharers;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"n\", new CborObject.CborString(name.name));\n            state.put(\"c\", cap);\n            state.put(\"s\", new CborObject.CborList(sharers.stream()\n                    .map(CborObject.CborString::new)\n                    .collect(Collectors.toList())));\n            return CborObject.CborMap.build(state);\n        }\n\n        public static ChildElement fromCbor(Cborable cbor) {\n            if (!(cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n            String name = m.getString(\"n\");\n            AbsoluteCapability cap = m.getObject(\"c\", AbsoluteCapability::fromCbor);\n            List<String> sharers = m.getList(\"s\", n -> ((CborObject.CborString)n).value);\n            return new ChildElement(new PathElement(name), cap, sharers);\n        }\n    }\n\n    public static class CapsInDirectory implements Cborable {\n        public final List<ChildElement> children;\n\n        public CapsInDirectory(List<ChildElement> children) {\n            this.children = children;\n        }\n\n        public CompletableFuture<CapsInDirectory> addChild(String filename,\n                                                           AbsoluteCapability cap,\n                                                           String sharer,\n                                                           String owner,\n                                                           NetworkAccess network) {\n            Optional<ChildElement> existing = children.stream()\n                    .filter(c -> c.name.name.equals(filename))\n                    .findFirst();\n            PathElement name = new PathElement(filename);\n            if (existing.isPresent()) {\n                ChildElement current = existing.get();\n                List<ChildElement> remainder = children.stream()\n                        .filter(c -> !c.name.name.equals(filename))\n                        .collect(Collectors.toList());\n                if (current.cap.equals(cap)) {\n                    List<String> combinedSharers = Stream.concat(existing.get().sharers.stream(), Stream.of(sharer))\n                            .collect(Collectors.toList());\n                    ChildElement updatedChild = new ChildElement(name, cap, combinedSharers);\n                    return Futures.of(new CapsInDirectory(Stream.concat(remainder.stream(),\n                            Stream.of(updatedChild))\n                            .collect(Collectors.toList())));\n                }\n                if (current.sharers.equals(Arrays.asList(sharer)) && ! current.cap.isWritable()) {\n                    ChildElement updatedChild = new ChildElement(name, cap, current.sharers);\n                    return Futures.of(new CapsInDirectory(Stream.concat(remainder.stream(),\n                            Stream.of(updatedChild))\n                            .collect(Collectors.toList())));\n                }\n                // need to find highest privilege cap that is still valid\n                // the cap could have been rotated, downgraded, or upgraded, or a friend shared a less privileged cap\n                return network.retrieveEntryPoint(new EntryPoint(current.cap, owner))\n                        .thenApply(oldOpt -> {\n                            if (oldOpt.isPresent() && current.cap.isWritable() && !cap.isWritable()) {\n                                // use the old cap\n                                return this;\n                            }\n                            ChildElement updatedChild = new ChildElement(name, cap, Collections.singletonList(sharer));\n                            return new CapsInDirectory(Stream.concat(remainder.stream(),\n                                    Stream.of(updatedChild))\n                                    .collect(Collectors.toList()));\n                        });\n            } else {\n                ChildElement newChild = new ChildElement(name, cap, Collections.singletonList(sharer));\n                return Futures.of(new CapsInDirectory(Stream.concat(children.stream(), Stream.of(newChild))\n                        .collect(Collectors.toList())));\n            }\n        }\n\n        public Optional<AbsoluteCapability> getChild(String name) {\n            return children.stream()\n                    .filter(c -> c.name.name.equals(name))\n                    .map(c -> c.cap)\n                    .findFirst();\n        }\n\n        public Set<AbsoluteCapability> getChildren() {\n            return children.stream()\n                    .map(c -> c.cap)\n                    .collect(Collectors.toSet());\n        }\n\n        public Set<String> getChildNames() {\n            return children.stream()\n                    .map(c -> c.name.name)\n                    .collect(Collectors.toSet());\n        }\n\n        public static CapsInDirectory empty() {\n            return new CapsInDirectory(Collections.emptyList());\n        }\n\n        public static CapsInDirectory of(String filename, AbsoluteCapability cap, String sharer) {\n            return new CapsInDirectory(Collections.singletonList(new ChildElement(new PathElement(filename), cap, Collections.singletonList(sharer))));\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"c\", new CborObject.CborList(children));\n            return CborObject.CborMap.build(state);\n        }\n\n        public static CapsInDirectory fromCbor(Cborable cbor) {\n            if (!(cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor! \" + cbor);\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n            List<ChildElement> children = m.getList(\"c\", ChildElement::fromCbor);\n            return new CapsInDirectory(children);\n        }\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getByPath(Path file,\n                                                              Snapshot version,\n                                                              Hasher hasher,\n                                                              NetworkAccess network) {\n        String finalPath = TrieNode.canonicalise(file.toString());\n        List<String> elements = Arrays.asList(finalPath.split(\"/\"));\n        Snapshot union = worldRoot.version.mergeAndOverwriteWith(version);\n        return worldRoot.getDescendentByPath(elements.get(0), union, hasher, network)\n                .thenCompose(dirOpt -> {\n                    if (dirOpt.isEmpty())\n                        return Futures.of(Optional.empty());\n                    return getByPath(dirOpt.get(), elements, 1, union, network);\n                });\n    }\n\n    private static String pathSuffix(List<String> path, int from) {\n        if (from >= path.size())\n            return \"\";\n        return String.join(\"/\", path.subList(from, path.size()));\n    }\n\n    private CompletableFuture<Optional<FileWrapper>> getByPath(FileWrapper mirrorDir,\n                                                               List<String> path,\n                                                               int childIndex,\n                                                               Snapshot version,\n                                                               NetworkAccess network) {\n        Supplier<CompletableFuture<Optional<FileWrapper>>> recurse =\n                () -> mirrorDir.getChild(version, path.get(childIndex), network)\n                        .thenCompose(childOpt -> childOpt.map(c ->\n                                getByPath(c, path, childIndex + 1, version, network))\n                                .orElseGet(() -> Futures.of(Optional.empty())));\n        if (childIndex >= path.size())\n            return getAnyValidParentOfAChild(mirrorDir, hasher, network);\n        return mirrorDir.getChild(version, DIR_STATE, network)\n                .thenCompose(capsOpt -> {\n                    if (capsOpt.isEmpty())\n                        return recurse.get();\n                    FileWrapper capsFile = capsOpt.get();\n                    return capsFile.getInputStream(capsFile.version.get(capsFile.writer()), network, crypto, x-> {})\n                            .thenCompose(r -> Serialize.readFully(r, capsFile.getSize()))\n                            .thenApply(CborObject::fromByteArray)\n                            .thenApply(CapsInDirectory::fromCbor)\n                            .thenCompose(caps -> caps.getChild(path.get(childIndex))\n                                    .map(cap -> network.getFile(new EntryPoint(cap, path.get(0)), version)\n                                            .thenCompose(fopt -> {\n                                                Function<Optional<FileWrapper>, CompletableFuture<Optional<FileWrapper>>> getDescendant =\n                                                        dir -> dir.map(f ->\n                                                                f.getDescendentByPath(pathSuffix(path, childIndex + 1), f.version, hasher, network))\n                                                                .orElseGet(recurse);\n\n                                                if (fopt.isPresent()) {\n                                                    if (fopt.get().isWritable())\n                                                        return fopt.get().getAnyLinkPointer(network)\n                                                                .thenApply(linkOpt -> fopt.map(g -> g.withLinkPointer(linkOpt)))\n                                                                .thenCompose(getDescendant);\n\n                                                    // there might be a descendant that is writable, though they might be\n                                                    // descendant caps that have since been revoked\n                                                    return mirrorDir.hasChild(path.get(childIndex), hasher, network)\n                                                            .thenCompose(hasDescendantCaps -> hasDescendantCaps ?\n                                                                    recurse.get()\n                                                                            .thenCompose(dopt ->\n                                                                                    dopt.map(res -> Futures.of(dopt))\n                                                                                            .orElseGet(() -> getDescendant.apply(fopt))) :\n                                                                    getDescendant.apply(fopt));\n                                                }\n                                                return recurse.get();\n                                            }))\n                                    .orElseGet(recurse::get));\n                });\n    }\n\n    private CompletableFuture<CapsInDirectory> getCaps(FileWrapper dir, Snapshot version, NetworkAccess network) {\n        return dir.getChild(version, DIR_STATE, network)\n                .thenCompose(capsOpt -> {\n                    if (capsOpt.isEmpty())\n                        return Futures.of(CapsInDirectory.empty());\n                    return Serialize.readFully(capsOpt.get(), crypto, network)\n                            .thenApply(CborObject::fromByteArray)\n                            .thenApply(CapsInDirectory::fromCbor);\n                });\n    }\n\n    private CompletableFuture<Optional<FileWrapper>> getAnyValidParentOfAChild(FileWrapper dir,\n                                                                              Hasher hasher,\n                                                                              NetworkAccess network) {\n        Supplier<CompletableFuture<Optional<FileWrapper>>> recurse =\n                () -> dir.getChildren(hasher, network)\n                        .thenCompose(children -> Futures.findFirst(children,\n                                c -> getAnyValidParentOfAChild(c, hasher, network)))\n                        .thenCompose(copt -> copt.map(f -> f.retrieveParent(network))\n                                .orElse(Futures.of(Optional.empty())));\n\n        return dir.getChild(DIR_STATE, hasher, network)\n                .thenCompose(capsOpt -> {\n                    String ownerName = dir.getOwnerName();\n                    if (capsOpt.isPresent())\n                        return Serialize.readFully(capsOpt.get(), crypto, network)\n                                .thenApply(CborObject::fromByteArray)\n                                .thenApply(CapsInDirectory::fromCbor)\n                                .thenCompose(caps -> Futures.findFirst(caps.children,\n                                        c -> network.retrieveEntryPoint(new EntryPoint(c.cap, ownerName))))\n                                .thenCompose(fileOpt -> Futures.asyncExceptionally(() -> fileOpt.map(f -> f.retrieveParent(network))\n                                        .orElseGet(recurse), t -> recurse.get()));\n                    return recurse.get();\n                });\n    }\n\n    private CompletableFuture<Set<FileWrapper>> getIndirectChildren(FileWrapper mirrorDir,\n                                                                    Set<String> toExclude,\n                                                                    Hasher hasher,\n                                                                    NetworkAccess network) {\n        return mirrorDir.getChildren(hasher, network)\n                .thenApply(children -> children.stream()\n                        .filter(c -> ! toExclude.contains(c.getName()) && ! c.getName().equals(DIR_STATE))\n                        .collect(Collectors.toSet()))\n                .thenCompose(remainingDirs -> Futures.combineAll(remainingDirs.stream()\n                        .map(child -> getAnyValidParentOfAChild(child, hasher, network))\n                        .collect(Collectors.toList())))\n                .thenApply(res -> res.stream()\n                        .flatMap(Optional::stream)\n                        .collect(Collectors.toSet()));\n    }\n\n    public CompletableFuture<Set<FileWrapper>> getChildren(Path dir,\n                                                           Snapshot version,\n                                                           Hasher hasher,\n                                                           NetworkAccess network) {\n        String finalPath = TrieNode.canonicalise(dir.toString());\n        List<String> elements = Arrays.asList(finalPath.split(\"/\"));\n        Snapshot union = worldRoot.version.mergeAndOverwriteWith(version);\n        return worldRoot.getDescendentByPath(elements.get(0), union, hasher, network)\n                .thenCompose(dirOpt -> {\n                    if (dirOpt.isEmpty())\n                        return Futures.of(Collections.emptySet());\n                    return getChildren(dirOpt.get(), elements, 1, union, hasher, network);\n                });\n    }\n\n    private CompletableFuture<Set<FileWrapper>> getChildren(FileWrapper mirrorDir,\n                                                            List<String> path,\n                                                            int childIndex,\n                                                            Snapshot version,\n                                                            Hasher hasher,\n                                                            NetworkAccess network) {\n        Supplier<CompletableFuture<Set<FileWrapper>>> recurse =\n                () -> mirrorDir.getChild(version, path.get(childIndex), network)\n                        .thenCompose(childOpt -> childOpt.map(c ->\n                                getChildren(c, path, childIndex + 1, version, hasher, network))\n                                .orElseGet(() -> Futures.of(Collections.emptySet())));\n        if (childIndex == path.size())\n            return getCaps(mirrorDir, version, network)\n                    .thenCompose(caps -> Futures.combineAll(caps.getChildren().stream()\n                            .map(cap -> network.retrieveEntryPoint(new EntryPoint(cap, path.get(0))))\n                            .collect(Collectors.toSet()))\n                            .thenApply(kids -> kids.stream().flatMap(Optional::stream).collect(Collectors.toSet()))\n                            .thenCompose(direct -> getIndirectChildren(mirrorDir,\n                                    direct.stream()\n                                            .map(FileWrapper::getName)\n                                            .collect(Collectors.toSet()), hasher, network)\n                                    .thenApply(indirectChildren -> Stream.concat(direct.stream(), indirectChildren.stream())\n                                            .collect(Collectors.toSet()))));\n\n        return mirrorDir.getChild(version, DIR_STATE, network)\n                .thenCompose(capsOpt -> {\n                    if (capsOpt.isEmpty())\n                        return recurse.get();\n                    return Serialize.readFully(capsOpt.get(), crypto, network)\n                            .thenApply(CborObject::fromByteArray)\n                            .thenApply(CapsInDirectory::fromCbor)\n                            .thenCompose(caps -> caps.getChild(path.get(childIndex))\n                                    .map(cap -> network.getFile(new EntryPoint(cap, path.get(0)), version)\n                                            .thenCompose(fopt -> fopt.map(f ->\n                                                    f.getDescendentByPath(pathSuffix(path, childIndex + 1), version.mergeAndOverwriteWith(f.version), hasher, network)\n                                                            .thenCompose(dir -> dir.map(d -> d.getChildren(hasher, network))\n                                                                    .orElse(Futures.of(Collections.emptySet()))))\n                                                    .orElseGet(recurse)))\n                                    .orElseGet(recurse));\n                });\n    }\n\n    public Snapshot getVersion() {\n        return worldRoot.version;\n    }\n\n    public CompletableFuture<Snapshot> getLatestVersion(EntryPoint sharedDir, NetworkAccess network) {\n        return NetworkAccess.getLatestEntryPoint(sharedDir, network)\n                .thenApply(r ->  r.file.version);\n    }\n\n    public synchronized CompletableFuture<Pair<Snapshot, CapsDiff>> ensureFriendUptodate(String friend,\n                                                                                         EntryPoint sharedDir,\n                                                                                         List<EntryPoint> groups,\n                                                                                         Snapshot s,\n                                                                                         Committer c,\n                                                                                         NetworkAccess network) {\n        // if the friend's mutable pointer hasn't changed since our last update we can short circuit early\n        PublicKeyHash owner = sharedDir.pointer.owner;\n        PublicKeyHash writer = sharedDir.pointer.writer;\n        if (! s.contains(writer))\n            return Futures.of(new Pair<>(s, CapsDiff.empty()));\n        CommittedWriterData latestCwd = s.get(writer);\n        MaybeMultihash latestRoot = latestCwd.hash;\n        Pair<MaybeMultihash, CapsDiff> cached = pointerCache.get(writer);\n        boolean equal = cached != null && latestRoot.equals(cached.left);\n        if (equal) {\n            if (cached.right.groupDiffs.size() == groups.size())\n                return Futures.of(new Pair<>(s, cached.right));\n        }\n\n        return getAndUpdateRoot(s, network)\n                .thenCompose(root -> root.getDescendentByPath(friend + FRIEND_STATE_SUFFIX, s, hasher, network)\n                        .thenCompose(stateOpt -> {\n                            if (stateOpt.isEmpty())\n                                return Futures.of(ProcessedCaps.empty());\n                            return Serialize.readFully(stateOpt.get(), crypto, network)\n                                    .thenApply(arr -> ProcessedCaps.fromCbor(CborObject.fromByteArray(arr)));\n                        }))\n                .thenCompose(currentState -> ensureUptodate(friend, sharedDir, groups, currentState, s, c, crypto, network))\n                .thenApply(res -> {\n                    pointerCache.put(writer, new Pair<>(latestRoot, res.right.flatten()));\n                    return res;\n                });\n    }\n\n    public CompletableFuture<CapsDiff> getCapsFrom(String friend,\n                                                   EntryPoint originalSharedDir,\n                                                   List<EntryPoint> groups,\n                                                   ProcessedCaps current,\n                                                   Snapshot s,\n                                                   NetworkAccess network) {\n        return network.getFile(originalSharedDir, s)\n                .thenCompose(shared -> shared.isEmpty() ?\n                        Futures.of(CapsDiff.empty()) :\n                        retrieveNewCaps(shared.get(), current, network, crypto)\n                                .thenCompose(direct -> Futures.combineAll(groups.stream()\n                                                .parallel()\n                                                .map(e -> network.getFile(e, s)\n                                                        .thenApply(Optional::get)\n                                                        .thenCompose(sharedDir -> retrieveNewCaps(sharedDir,\n                                                                current.groups.getOrDefault(sharedDir.getName(), ProcessedCaps.empty()), network, crypto)\n                                                                .thenApply(diff -> Optional.of(new Pair<>(sharedDir.getName(), diff))))\n                                                        .exceptionally(t -> Optional.empty()))\n                                                .collect(Collectors.toList()))\n                                        .thenApply(groupDiffs -> groupDiffs.stream()\n                                                .flatMap(Optional::stream)\n                                                .reduce(direct,\n                                                        (a, p) -> a.mergeGroups(current.createGroupDiff(p.left, p.right)),\n                                                        CapsDiff::mergeGroups))));\n    }\n\n    private static CompletableFuture<CapsDiff> retrieveNewCaps(FileWrapper sharedDir,\n                                                               ProcessedCaps current,\n                                                               NetworkAccess network,\n                                                               Crypto crypto) {\n        return retrieveNewCaps(sharedDir, current.readCapBytes, current.writeCapBytes, network, crypto)\n                .exceptionally(t -> {\n                    // we might have been removed from a group or similar\n                    t.printStackTrace();\n                    return CapsDiff.empty();\n                });\n    }\n\n    private static CompletableFuture<CapsDiff> retrieveNewCaps(FileWrapper sharedDir,\n                                                               long readCapBytes,\n                                                               long writeCapBytes,\n                                                               NetworkAccess network,\n                                                               Crypto crypto) {\n        return CapabilityStore.loadReadAccessSharingLinksFromIndex(null, sharedDir,\n                null, network, crypto, readCapBytes, false, true)\n                .thenCompose(newReadCaps ->\n                        getWritableCaps(sharedDir, writeCapBytes, crypto, network)\n                                .thenApply(writeable ->\n                                        new CapsDiff.ReadAndWriteCaps(newReadCaps, writeable)))\n                .thenApply(newCaps -> new CapsDiff(readCapBytes, writeCapBytes, newCaps, Collections.emptyMap()));\n    }\n\n    private synchronized CompletableFuture<Pair<Snapshot, CapsDiff>> ensureUptodate(String friend,\n                                                                                    EntryPoint originalSharedDir,\n                                                                                    List<EntryPoint> groups,\n                                                                                    ProcessedCaps current,\n                                                                                    Snapshot s,\n                                                                                    Committer c,\n                                                                                    Crypto crypto,\n                                                                                    NetworkAccess network) {\n        // check there are no new capabilities in the friend's shared directory, or any of their groups\n        return getCapsFrom(friend, originalSharedDir, groups, current, s, network)\n                .thenCompose(diff -> addNewCapsToMirror(friend, current, diff, s, c, network))\n                .thenCompose(p -> getAndUpdateWorldRoot(p.left, network)\n                        .thenApply(y -> p));\n    }\n\n    private static synchronized CompletableFuture<CapabilitiesFromUser> getWritableCaps(FileWrapper sharedDir,\n                                                                                        long byteOffsetWrite,\n                                                                                        Crypto crypto,\n                                                                                        NetworkAccess network) {\n        return CapabilityStore.getEditableCapabilityFileSize(sharedDir, crypto, network)\n                .thenCompose(editFilesize -> {\n                    if (editFilesize == byteOffsetWrite)\n                        return CompletableFuture.completedFuture(CapabilitiesFromUser.empty());\n                    return CapabilityStore.loadWriteAccessSharingLinksFromIndex(null, sharedDir,\n                            null, network, crypto, byteOffsetWrite, false, true);\n                });\n    }\n\n    private CompletableFuture<Pair<Snapshot, CapsDiff>> addNewCapsToMirror(String friend,\n                                                                           ProcessedCaps current,\n                                                                           CapsDiff diff,\n                                                                           Snapshot s,\n                                                                           Committer c,\n                                                                           NetworkAccess network) {\n        if (diff.isEmpty())\n            return Futures.of(new Pair<>(s, diff));\n\n        List<CapabilityWithPath> all = diff.getNewCaps();\n        // Add all new caps to mirror tree\n        return worldRoot.getUpdated(s, network)\n                .thenCompose(updatedWorldRoot -> Futures.reduceAll(all, updatedWorldRoot,\n                        (r, cap) -> addCapToMirror(friend, r, cap, r.version, c, crypto, network),\n                        (a, b) -> b))\n                .thenCompose(updatedRoot -> {\n                    this.worldRoot = updatedRoot;\n                    // Commit our position in the friend's cap stream\n                    ProcessedCaps updated = current.add(diff);\n                    byte[] raw = updated.serialize();\n                    AsyncReader reader = AsyncReader.build(raw);\n                    return getAndUpdateRoot(updatedRoot.version, network)\n                            .thenCompose(root -> root.uploadOrReplaceFile(friend + FRIEND_STATE_SUFFIX, reader, raw.length,\n                                    false, updatedRoot.version, c, network, crypto, () -> false, x -> {}, crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH),\n                                    Optional.of(Bat.random(crypto.random)),\n                                    root.mirrorBatId()))\n                            .thenApply(v -> new Pair<>(s.mergeAndOverwriteWith(v), diff));\n                });\n    }\n\n    private static CompletableFuture<FileWrapper> addCapToMirror(String friend,\n                                                                 FileWrapper root,\n                                                                 CapabilityWithPath cap,\n                                                                 Snapshot s,\n                                                                 Committer c,\n                                                                 Crypto crypto,\n                                                                 NetworkAccess network) {\n        Path fullPath = PathUtil.get(cap.path);\n        Path parentPath = fullPath.getParent();\n        String owner = fullPath.getName(0).toString();\n        String filename = fullPath.getFileName().toString();\n        return root.getUpdated(s, network)\n                .thenCompose(freshRoot -> freshRoot.getOrMkdirs(PathUtil.components(parentPath), false, root.mirrorBatId(), network, crypto, s, c))\n                .thenCompose(p -> p.right.getChild(p.left, DIR_STATE, network)\n                        .thenCompose(capsOpt -> {\n                            if (capsOpt.isEmpty()) {\n                                CapsInDirectory single = CapsInDirectory.of(filename, cap.cap, friend);\n                                byte[] raw = single.serialize();\n                                AsyncReader reader = AsyncReader.build(raw);\n                                return p.right.uploadOrReplaceFile(DIR_STATE, reader, raw.length, false, p.left, c, network, crypto,\n                                        () -> false, x -> {}, crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH),\n                                        Optional.of(Bat.random(crypto.random)), root.mirrorBatId());\n                            }\n                            return Serialize.readFully(capsOpt.get(), crypto, network)\n                                    .thenApply(CborObject::fromByteArray)\n                                    .thenApply(CapsInDirectory::fromCbor)\n                                    .thenCompose(existing -> existing.addChild(filename, cap.cap, friend, owner, network))\n                                    .thenCompose(updated -> {\n                                        byte[] raw = updated.serialize();\n                                        AsyncReader reader = AsyncReader.build(raw);\n                                        return capsOpt.get().overwriteFile(reader, raw.length, network, crypto, x -> {}, p.left, c);\n                                    });\n                        })\n                ).thenCompose(v -> root.getUpdated(v, network));\n    }\n\n    private synchronized CompletableFuture<FileWrapper> getAndUpdateWorldRoot(Snapshot s, NetworkAccess network) {\n        return worldRoot.getUpdated(s, network).thenApply(updated -> {\n            this.worldRoot = updated;\n            return updated;\n        });\n    }\n\n    private synchronized CompletableFuture<FileWrapper> getAndUpdateRoot(Snapshot s, NetworkAccess network) {\n        return cacheRoot.getUpdated(s, network).thenApply(updated -> {\n            this.cacheRoot = updated;\n            return updated;\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/JavaScriptPoster.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class JavaScriptPoster implements HttpPoster {\n\n    private final NativeJSHttp http = new NativeJSHttp();\n    private final boolean isAbsolute, useGet;\n\n    public JavaScriptPoster(boolean isAbsolute, boolean useGet) {\n        this.isAbsolute = isAbsolute;\n        this.useGet = useGet;\n    }\n\n    private String canonicalise(String url) {\n        if (isAbsolute && ! url.startsWith(\"/\") && ! url.startsWith(\"https://\") && ! url.startsWith(\"http://\"))\n            return \"/\" + url;\n        return url;\n    }\n\n    @Override\n    public CompletableFuture<byte[]> post(String url, byte[] payload, boolean unzip, int timeoutMillis) {\n        return http.post(canonicalise(url), payload, timeoutMillis);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> post(String url, byte[] payload, boolean unzip) {\n        return post(url, payload, unzip, 30_000);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> postUnzip(String url, byte[] payload, int timeoutMillis) {\n        return post(canonicalise(url), payload, true, timeoutMillis);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> postUnzip(String url, byte[] payload) {\n        return postUnzip(url, payload, 30_000);\n    }\n    \n    @Override\n    public CompletableFuture<byte[]> postMultipart(String url, List<byte[]> files, int timeoutMillis) {\n        return http.postMultipart(canonicalise(url), files,  timeoutMillis);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> put(String url, byte[] payload, Map<String, String> headers) {\n        String[] headersArray = new String[headers.size() * 2];\n        int index = 0;\n        for (Map.Entry<String, String> e : headers.entrySet()) {\n            headersArray[index++] = e.getKey();\n            headersArray[index++] = e.getValue();\n        }\n        return http.put(url, payload, headersArray);\n    }\n\n    @Override\n    public CompletableFuture<byte[]> get(String url) {\n        return get(url, Collections.emptyMap());\n    }\n\n    @Override\n    public CompletableFuture<byte[]> get(String url, Map<String, String> headers) {\n        if (useGet) {\n            String[] headersArray = new String[headers.size() * 2];\n            int index = 0;\n            for (Map.Entry<String, String> e : headers.entrySet()) {\n                headersArray[index++] = e.getKey();\n                headersArray[index++] = e.getValue();\n            }\n            return http.getWithHeaders(canonicalise(url), headersArray);\n        }\n        return postUnzip(url, new byte[0]);\n    }\n\n    @JsMethod\n    public static byte[] emptyArray() {\n        return new byte[0];\n    }\n\n    // This is an ugly hack to convert Uint8Array to a valid byte[]\n    @JsMethod\n    public static byte[] convertToBytes(short[] uints) {\n        byte[] res = new byte[uints.length];\n        for (int i=0; i < res.length; i++)\n            res[i] = (byte) uints[i];\n        return res;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/LinkProperties.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.user.fs.*;\n\nimport java.time.*;\nimport java.util.*;\n\n\npublic class LinkProperties implements Cborable {\n    public final long label;\n    public final String linkPassword, userPassword;\n    public final boolean isLinkWritable, open;\n    public final Optional<Integer> maxRetrievals;\n    public final Optional<LocalDateTime> expiry;\n    public final Optional<Multihash> existing;\n\n\n    public LinkProperties(long label, String linkPassword, String userPassword, boolean isLinkWritable,\n                          Optional<Integer> maxRetrievals, Optional<LocalDateTime> expiry, boolean open, Optional<Multihash> existing) {\n        this.label = label;\n        this.linkPassword = linkPassword;\n        this.userPassword = userPassword;\n        this.isLinkWritable = isLinkWritable;\n        this.maxRetrievals = maxRetrievals;\n        this.expiry = expiry;\n        this.open = open;\n        this.existing = existing;\n    }\n\n    @JsMethod\n    public LinkProperties with(String userPassword, String maxRetrievals, Optional<LocalDateTime> expiry, boolean newOpen) {\n        Optional<Integer> maxRetrievalsOpt = maxRetrievals.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(maxRetrievals));\n        return new LinkProperties(label, linkPassword, userPassword, isLinkWritable, maxRetrievalsOpt, expiry, newOpen, existing);\n    }\n\n    public LinkProperties withExisting(Optional<Multihash> existing) {\n        return new LinkProperties(label, linkPassword, userPassword, isLinkWritable, maxRetrievals, expiry, open, existing);\n    }\n\n    public SecretLink toLink(PublicKeyHash owner) {\n        return new SecretLink(owner, label, linkPassword);\n    }\n\n    @JsMethod\n    public boolean autoOpen() {\n        return open;\n    }\n\n    @JsMethod\n    public String maxRetrievalsString() {\n        return maxRetrievals.map(Long::toString).orElse(\"\");\n    }\n\n    @JsMethod\n    public String toLinkString(PublicKeyHash owner) {\n        return toLink(owner).toLink();\n    }\n\n    @JsMethod\n    public long getLinkLabel() {\n        return label;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"l\", new CborObject.CborLong(label));\n        state.put(\"p\", new CborObject.CborString(linkPassword));\n        state.put(\"u\", new CborObject.CborString(userPassword));\n        state.put(\"w\", new CborObject.CborBoolean(isLinkWritable));\n        state.put(\"o\", new CborObject.CborBoolean(open));\n        existing.ifPresent(e -> state.put(\"h\", new CborObject.CborMerkleLink(e)));\n        maxRetrievals.ifPresent(m -> state.put(\"m\", new CborObject.CborLong(m)));\n        expiry.ifPresent(e -> state.put(\"e\", new CborObject.CborLong(e.toEpochSecond(ZoneOffset.UTC))));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static LinkProperties fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for LinkProperties! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        long label = m.getLong(\"l\");\n        String password = m.getString(\"p\");\n        String userPassword = m.getString(\"u\");\n        boolean isWritable = m.getBoolean(\"w\");\n        boolean open = m.getBoolean(\"o\", false);\n        Optional<Integer> maxCount = m.getOptionalLong(\"m\").map(Long::intValue);\n        Optional<LocalDateTime> expiry = m.getOptionalLong(\"e\").map(s -> LocalDateTime.ofEpochSecond(s, 0, ZoneOffset.UTC));\n        return new LinkProperties(label, password, userPassword, isWritable, maxCount, expiry, open, m.getOptional(\"h\", c -> ((CborObject.CborMerkleLink)c).target));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/LoginData.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic class LoginData implements Cborable {\n\n    public final String username;\n    public final UserStaticData entryPoints;\n    public final PublicSigningKey authorisedReader;\n    public final Optional<Pair<OpLog.BlockWrite, OpLog.PointerWrite>> identityUpdate;\n\n    public LoginData(String username,\n                     UserStaticData entryPoints,\n                     PublicSigningKey authorisedReader,\n                     Optional<Pair<OpLog.BlockWrite, OpLog.PointerWrite>> identityUpdate) {\n        this.username = username;\n        this.entryPoints = entryPoints;\n        this.authorisedReader = authorisedReader;\n        this.identityUpdate = identityUpdate;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"u\", new CborObject.CborString(username));\n        state.put(\"e\", entryPoints);\n        state.put(\"r\", authorisedReader);\n        identityUpdate.ifPresent(p -> {\n            state.put(\"b\", p.left);\n            state.put(\"p\", p.right);\n        });\n        return CborObject.CborMap.build(state);\n    }\n\n    public static LoginData fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for LoginData!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        String username = m.getString(\"u\");\n        UserStaticData entry = m.get(\"e\", UserStaticData::fromCbor);\n        PublicSigningKey authorisedReader = m.get(\"r\", PublicSigningKey::fromCbor);\n        Optional<Pair<OpLog.BlockWrite, OpLog.PointerWrite>> identityUpdate = m.containsKey(\"b\") && m.containsKey(\"p\") ?\n                Optional.of(new Pair<>(m.get(\"b\", OpLog.BlockWrite::fromCbor), m.get(\"p\", OpLog.PointerWrite::fromCbor))) :\n                Optional.empty();\n        return new LoginData(username, entry, authorisedReader, identityUpdate);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/Migrate.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.io.ipfs.Multihash;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.*;\n\npublic class Migrate {\n\n    public static CompletableFuture<List<UserPublicKeyLink>> buildMigrationChain(List<UserPublicKeyLink> existing,\n                                                                                 Multihash newStorageId,\n                                                                                 SecretSigningKey signer) {\n        UserPublicKeyLink last = existing.get(existing.size() - 1);\n        return UserPublicKeyLink.Claim.build(last.claim.username, signer,\n                last.claim.expiry.plusDays(1), Arrays.asList(newStorageId)).thenApply(newClaim -> {\n            UserPublicKeyLink updatedLast = last.withClaim(newClaim);\n            return Stream.concat(\n                            existing.stream().limit(existing.size() - 1),\n                            Stream.of(updatedLast))\n                    .collect(Collectors.toList());\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/MutableTree.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.MaybeMultihash;\nimport peergos.shared.storage.*;\n\nimport java.io.*;\nimport java.util.List;\nimport java.util.concurrent.*;\nimport peergos.shared.util.Pair;\n\npublic interface MutableTree {\n\n    /**\n     *\n     * @param base\n     * @param owner\n     * @param sharingKey\n     * @param mapKey\n     * @param value\n     * @return the new root WriterData\n     * @throws IOException\n     */\n    CompletableFuture<WriterData> put(WriterData base,\n                                      PublicKeyHash owner,\n                                      SigningPrivateKeyAndPublicHash sharingKey,\n                                      byte[] mapKey,\n                                      MaybeMultihash existing,\n                                      Multihash value,\n                                      TransactionId tid);\n\n    /**\n     *\n     * @param base The WriterData at the current mutable pointer for the writer\n     * @param owner\n     * @param writer\n     * @param mapKey\n     * @return  the value stored under mapKey for sharingKey\n     * @throws IOException\n     */\n    CompletableFuture<MaybeMultihash> get(WriterData base, PublicKeyHash owner, PublicKeyHash writer, byte[] mapKey);\n\n    /**\n     *\n     * @param owner\n     * @param sharingKey\n     * @param mapKey\n     * @return  hash(sharingKey.metadata) | the new root hash of the tree\n     * @throws IOException\n     */\n    CompletableFuture<WriterData> remove(WriterData base,\n                                         PublicKeyHash owner,\n                                         SigningPrivateKeyAndPublicHash sharingKey,\n                                         byte[] mapKey,\n                                         MaybeMultihash existing,\n                                         TransactionId tid);\n\n    /**\n     * Remove all specified keys from the writer's CHAMP in a single batch operation.\n     * Substantially faster than N individual {@link #remove} calls.\n     */\n    CompletableFuture<WriterData> removeAll(WriterData base,\n                                             PublicKeyHash owner,\n                                             SigningPrivateKeyAndPublicHash sharingKey,\n                                             List<Pair<byte[], MaybeMultihash>> keysAndExisting,\n                                             TransactionId tid);\n\n}\n"
  },
  {
    "path": "src/peergos/shared/user/MutableTreeImpl.java",
    "content": "package peergos.shared.user;\nimport java.util.*;\nimport java.util.logging.*;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class MutableTreeImpl implements MutableTree {\n\tprivate static final Logger LOG = Logger.getGlobal();\n    private final MutablePointers mutable;\n    private final ContentAddressedStorage dht;\n    private final Hasher writeHasher;\n    private static final boolean LOGGING = false;\n    private final WriteSynchronizer synchronizer;\n    private final Function<ByteArrayWrapper, CompletableFuture<byte[]>> hasher = x -> Futures.of(x.data);\n\n    public MutableTreeImpl(MutablePointers mutable,\n                           ContentAddressedStorage dht,\n                           Hasher writeHasher,\n                           WriteSynchronizer synchronizer) {\n        this.mutable = mutable;\n        this.dht = dht;\n        this.writeHasher = writeHasher;\n        this.synchronizer = synchronizer;\n    }\n\n    private <T> T log(T result, String toPrint) {\n        if (LOGGING)\n            LOG.info(toPrint);\n        return result;\n    }\n\n    @Override\n    public CompletableFuture<WriterData> put(WriterData base,\n                                             PublicKeyHash owner,\n                                             SigningPrivateKeyAndPublicHash writer,\n                                             byte[] mapKey,\n                                             MaybeMultihash existing,\n                                             Multihash value,\n                                             TransactionId tid) {\n        return (base.tree.isPresent() ?\n                ChampWrapper.create(owner, (Cid)base.tree.get(), Optional.empty(), hasher, dht, writeHasher, c -> (CborObject.CborMerkleLink)c) :\n                ChampWrapper.create(owner, writer, hasher, tid, dht, writeHasher, c -> (CborObject.CborMerkleLink)c)\n        ).thenCompose(tree -> tree.put(owner, writer, mapKey, existing.map(CborObject.CborMerkleLink::new), new CborObject.CborMerkleLink(value), Optional.empty(), tid))\n                .thenApply(newRoot -> LOGGING ? log(newRoot, \"TREE.put \" + writer.publicKeyHash + \" :== (\" + ArrayOps.bytesToHex(mapKey)\n                        + \", \" + value + \") => CAS(\" + base.tree + \", \" + newRoot + \")\") : newRoot)\n                .thenApply(base::withChamp);\n    }\n\n    @Override\n    public CompletableFuture<MaybeMultihash> get(WriterData base, PublicKeyHash owner, PublicKeyHash writer, byte[] mapKey) {\n        if (! base.tree.isPresent())\n            throw new IllegalStateException(\"Tree root not present for \" + writer);\n        return ChampWrapper.create(owner, (Cid)base.tree.get(), Optional.empty(), hasher, dht, writeHasher, c -> (CborObject.CborMerkleLink)c)\n                .thenCompose(tree -> tree.get(mapKey))\n                .thenApply(c -> c.map(x -> x.target).map(MaybeMultihash::of).orElse(MaybeMultihash.empty()))\n                .thenApply(maybe -> LOGGING ?\n                        log(maybe, \"TREE.get (\" + ArrayOps.bytesToHex(mapKey)\n                                + \", root=\"+base.tree.get()+\" => \" + maybe) : maybe);\n    }\n\n    @Override\n    public CompletableFuture<WriterData> remove(WriterData base,\n                                                PublicKeyHash owner,\n                                                SigningPrivateKeyAndPublicHash writer,\n                                                byte[] mapKey,\n                                                MaybeMultihash existing,\n                                                TransactionId tid) {\n        if (! base.tree.isPresent())\n            throw new IllegalStateException(\"Tree root not present!\");\n        return ChampWrapper.create(owner, (Cid)base.tree.get(), Optional.empty(), hasher, dht, writeHasher, c -> (CborObject.CborMerkleLink)c)\n                .thenCompose(tree -> tree.remove(owner, writer, mapKey, existing.map(CborObject.CborMerkleLink::new), Optional.empty(), tid))\n                .thenApply(root -> LOGGING ? log(root, \"TREE.rm \" + writer.publicKeyHash + \" :== (\"\n                        + ArrayOps.bytesToHex(mapKey) + \", \" + existing + \") CAS(\" + base.tree.get() + \", \" + root + \")\") : root)\n                .thenApply(newTreeRoot -> base.withChamp(newTreeRoot));\n    }\n\n    @Override\n    public CompletableFuture<WriterData> removeAll(WriterData base,\n                                                    PublicKeyHash owner,\n                                                    SigningPrivateKeyAndPublicHash writer,\n                                                    List<Pair<byte[], MaybeMultihash>> keysAndExisting,\n                                                    TransactionId tid) {\n        if (! base.tree.isPresent())\n            throw new IllegalStateException(\"Tree root not present!\");\n        List<Pair<byte[], Optional<CborObject.CborMerkleLink>>> champKeys = keysAndExisting.stream()\n                .map(p -> new Pair<>(p.left, p.right.map(CborObject.CborMerkleLink::new)))\n                .collect(Collectors.toList());\n        return ChampWrapper.create(owner, (Cid) base.tree.get(), Optional.empty(), hasher, dht, writeHasher,\n                        c -> (CborObject.CborMerkleLink) c)\n                .thenCompose(tree -> tree.removeAll(owner, writer, champKeys, Optional.empty(), tid))\n                .thenApply(base::withChamp);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/Mutation.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.storage.*;\n\nimport java.util.concurrent.*;\n\npublic interface Mutation {\n\n    CompletableFuture<WriterData> apply(WriterData input, TransactionId tid);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/NativeJSAccountCache.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.storage.auth.BatWithId;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\n@JsType(namespace = \"accountCache\", isNative = true)\npublic class NativeJSAccountCache {\n\n    public native void init();\n\n    public native CompletableFuture<Boolean> setLoginData(String key, byte[] entryPoints);\n\n    public native CompletableFuture<Boolean> remove(String key);\n\n    public native CompletableFuture<byte[]> getEntryData(String key);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/NativeJSBatCache.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.storage.auth.BatWithId;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\n@JsType(namespace = \"batCache\", isNative = true)\npublic class NativeJSBatCache {\n\n    public native void init();\n\n    public native CompletableFuture<byte[]> getUserBats(String username);\n\n    public native CompletableFuture<Boolean> setUserBats(String username, byte[] serialisedBats);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/NativeJSCache.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\n\n@JsType(namespace = \"cache\", isNative = true)\npublic class NativeJSCache {\n\n    public native void init(int maxSizeMiB);\n\n    public native CompletableFuture<Boolean> put(Cid hash, byte[] data);\n\n    public native CompletableFuture<Optional<byte[]>> get(Cid hash);\n\n    public native boolean hasBlock(Cid hash);\n\n    public native CompletableFuture<Boolean> clear();\n\n}\n"
  },
  {
    "path": "src/peergos/shared/user/NativeJSHttp.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n@JsType(namespace = \"http\", isNative = true)\npublic class NativeJSHttp {\n\n//    public static <T> CompletableFuture<T> incomplete() {\n//        return new CompletableFuture<>();\n//    }\n\n    public native CompletableFuture<byte[]> post(String url, byte[] payload, int timeoutMillis) ;/*-{\n        console.log(\"postProm\");\n        var future = this.incomplete();\n        new Promise(function(resolve, reject) {\n\t        console.log(\"making http post request\");\n\t        var req = new XMLHttpRequest();\n\t        req.open('POST', window.location.origin + \"/\" + url);\n\t        req.responseType = 'arraybuffer';\n\n\t        req.onload = function() {\n    \t        console.log(\"http post returned retrieving \" + url);\n                // This is called even on 404 etc\n                // so check the status\n                if (req.status == 200) {\n\t        \t    resolve(new Uint8Array(req.response));\n                } else {\n\t\t            reject(Error(req.statusText));\n                }\n    \t    };\n\n    \t    req.onerror = function() {\n                reject(Error(\"Network Error\"));\n\t        };\n\n\t        req.send(new Uint8Array(data));\n        }).then(function(result, err) {\n            if (err != null)\n                future.completeExceptionally(err);\n            else\n                future.complete(peergos.shared.user.JavaScriptPoster.convertToBytes(result));\n        });\n        return future;\n    }-*/;\n\n    public native CompletableFuture<byte[]> get(String url) ;/*-{\n        console.log(\"getProm\");\n        var future = this.incomplete();\n        new Promise(function(resolve, reject) {\n\t        var req = new XMLHttpRequest();\n\t        req.open('GET', url);\n\t        req.responseType = 'arraybuffer';\n\n\t        req.onload = function() {\n                // This is called even on 404 etc\n                // so check the status\n                if (req.status == 200) {\n\t\t            resolve(new Uint8Array(req.response));\n                } else {\n\t\t            reject(Error(req.statusText));\n                }\n\t        };\n\n\t        req.onerror = function() {\n                reject(Error(\"Network Error\"));\n\t        };\n\n\t        req.send();\n        }).then(function(result, err) {\n            if (err != null)\n                future.completeExceptionally(err);\n            else\n                future.complete(result);\n        });\n        return future;\n    }-*/;\n\n    public native CompletableFuture<byte[]> getWithHeaders(String url, String[] headers);\n\n    public native CompletableFuture<byte[]> postMultipart(String url, List<byte[]> payload, int timeoutMillis);\n\n    public native CompletableFuture<byte[]> put(String url, byte[] payload, String[] headers);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/NativeJSPkiCache.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\n\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\n@JsType(namespace = \"pkiCache\", isNative = true)\npublic class NativeJSPkiCache {\n\n    public native void init();\n\n    public native CompletableFuture<List<String>> getChain(String username);\n\n    public native CompletableFuture<Boolean> setChain(String username, String[] serialisedUserPublicKeyLinkChain, String serialisedOwner);\n\n    public native CompletableFuture<String> getUsername(String serialisedPublicKeyHash);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/NativeJSPointerCache.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.crypto.hash.PublicKeyHash;\n\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\n\n@JsType(namespace = \"pointerCache\", isNative = true)\npublic class NativeJSPointerCache {\n\n    public native void init(int size);\n\n    public native CompletableFuture<Boolean> put(PublicKeyHash owner, PublicKeyHash writer, byte[] writerSignedBtreeRootHash);\n\n    public native CompletableFuture<Optional<byte[]>> get(PublicKeyHash owner, PublicKeyHash writer);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/OwnedKeyChamp.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class OwnedKeyChamp {\n\n    public final Multihash root;\n    private final ChampWrapper<CborObject.CborMerkleLink> champ;\n    private final ContentAddressedStorage ipfs;\n\n    private OwnedKeyChamp(Multihash root, ChampWrapper<CborObject.CborMerkleLink> champ, ContentAddressedStorage ipfs) {\n        this.root = root;\n        this.champ = champ;\n        this.ipfs = ipfs;\n    }\n\n    public static CompletableFuture<Cid> createEmpty(PublicKeyHash owner,\n                                                     SigningPrivateKeyAndPublicHash writer,\n                                                     ContentAddressedStorage ipfs,\n                                                     Hasher hasher,\n                                                     TransactionId tid) {\n        Champ<CborObject.CborMerkleLink> newRoot = Champ.empty(c -> (CborObject.CborMerkleLink)c);\n        byte[] raw = newRoot.serialize();\n        return hasher.sha256(raw)\n                .thenCompose(writer.secret::signMessage)\n                .thenCompose(signed -> ipfs.put(owner, writer.publicKeyHash, signed, raw, tid));\n    }\n\n    public static CompletableFuture<OwnedKeyChamp> build(PublicKeyHash owner, Cid root, ContentAddressedStorage ipfs, Hasher hasher) {\n        return ChampWrapper.create(owner, root, Optional.empty(), b -> Futures.of(b.data), ipfs, hasher, c -> (CborObject.CborMerkleLink)c)\n                .thenApply(c -> new OwnedKeyChamp(root, c, ipfs));\n    }\n\n    private static byte[] reverse(byte[] in) {\n        byte[] reversed = new byte[in.length];\n        for (int i=0; i < in.length; i++)\n            reversed[i] = in[in.length - i - 1];\n        return reversed;\n    }\n\n    private static byte[] keyToBytes(PublicKeyHash key) {\n        return reverse(key.serialize());\n    }\n\n    public CompletableFuture<Optional<OwnerProof>> get(PublicKeyHash owner,\n                                                       PublicKeyHash ownedKey) {\n        return champ.get(keyToBytes(ownedKey))\n                .thenCompose(res -> res.isPresent() ?\n                        ipfs.get(owner, (Cid)res.get().target, Optional.empty()).thenApply(raw -> raw.map(OwnerProof::fromCbor)) :\n                        CompletableFuture.completedFuture(Optional.empty()));\n    }\n\n    public CompletableFuture<Multihash> add(PublicKeyHash owner,\n                                            SigningPrivateKeyAndPublicHash writer,\n                                            OwnerProof proof,\n                                            Hasher hasher,\n                                            TransactionId tid) {\n        return ipfs.put(owner, writer, proof.serialize(), hasher, tid)\n                .thenCompose(valueHash ->\n                        champ.put(owner, writer, keyToBytes(proof.ownedKey), Optional.empty(), new CborObject.CborMerkleLink(valueHash), Optional.empty(), tid));\n    }\n\n    public CompletableFuture<Multihash> remove(PublicKeyHash owner,\n                                               SigningPrivateKeyAndPublicHash writer,\n                                               PublicKeyHash key,\n                                               TransactionId tid) {\n        byte[] keyBytes = keyToBytes(key);\n        return champ.get(keyBytes)\n                .thenCompose(existing -> existing.isPresent() ?\n                        champ.remove(owner, writer, keyBytes, existing, Optional.empty(), tid) :\n                        Futures.of(champ.getRoot()));\n    }\n\n    public CompletableFuture<Boolean> contains(PublicKeyHash ownedKey) {\n        return champ.get(keyToBytes(ownedKey))\n                .thenApply(Optional::isPresent);\n    }\n\n    public <T> CompletableFuture<T> applyToAllMappings(PublicKeyHash owner,\n                                                       T identity,\n                                                       BiFunction<T, Pair<PublicKeyHash, OwnerProof>, CompletableFuture<T>> consumer,\n                                                       ContentAddressedStorage ipfs) {\n        return champ.reduceAllMappings(owner, identity,\n                (acc, pair) -> ! pair.right.isPresent() ? CompletableFuture.completedFuture(acc) :\n                        ipfs.get(owner, (Cid)pair.right.get().target, Optional.empty())\n                                .thenApply(raw -> OwnerProof.fromCbor(raw.get()))\n                                .thenCompose(proof -> consumer.apply(acc,\n                                        new Pair<>(PublicKeyHash.fromCbor(CborObject.fromByteArray(reverse(pair.left.data))), proof))));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/PendingSocialState.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class PendingSocialState implements Cborable {\n\n    public final Set<String> pendingOutgoingFollowRequests;\n\n    public PendingSocialState(Set<String> pendingOutgoingFollowRequests) {\n        this.pendingOutgoingFollowRequests = pendingOutgoingFollowRequests;\n    }\n\n    public PendingSocialState withPending(String username) {\n        HashSet<String> updated = new HashSet<>(pendingOutgoingFollowRequests);\n        updated.add(username);\n        return new PendingSocialState(updated);\n    }\n\n    public PendingSocialState withoutPending(String username) {\n        HashSet<String> updated = new HashSet<>(pendingOutgoingFollowRequests);\n        updated.remove(username);\n        return new PendingSocialState(updated);\n    }\n\n    public static PendingSocialState empty() {\n        return new PendingSocialState(Collections.emptySet());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> cbor = new TreeMap<>();\n        cbor.put(\"pendingOutgoing\", new CborObject.CborList(pendingOutgoingFollowRequests.stream()\n                .map(CborObject.CborString::new)\n                .collect(Collectors.toList())));\n        return CborObject.CborMap.build(cbor);\n    }\n\n    public static PendingSocialState fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for PendingSocialState!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        List<String> pendingOutgoing = m.getList(\"pendingOutgoing\", c -> ((CborObject.CborString)c).value);\n        return new PendingSocialState(new HashSet<>(pendingOutgoing));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/Profile.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\n\nimport java.util.Optional;\n\n@JsType\npublic class Profile {\n    public final Optional<byte[]> profilePhoto;\n    public final Optional<String> bio;\n    public final Optional<String> status;\n    public final Optional<String> firstName;\n    public final Optional<String> lastName;\n    public final Optional<String> phone;\n    public final Optional<String> email;\n    public final Optional<String> webRoot;\n\n    public Profile(Optional<byte[]> profilePhoto,\n                   Optional<String> bio,\n                   Optional<String> status,\n                   Optional<String> firstName,\n                   Optional<String> lastName,\n                   Optional<String> phone,\n                   Optional<String> email,\n                   Optional<String> webRoot) {\n        this.profilePhoto = profilePhoto;\n        this.bio = bio;\n        this.status = status;\n        this.firstName = firstName;\n        this.lastName = lastName;\n        this.phone = phone;\n        this.email = email;\n        this.webRoot = webRoot;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ProfilePaths.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\n/** This class stores locations of the different components of a user's profile\n *\n *  Each component is a separate file and can thus be shared or made public individually.\n */\npublic class ProfilePaths {\n\n    public static final Path ROOT = PathUtil.get(\".profile\");\n    private static final Path PHOTO = ROOT.resolve(\"photo\");\n    public static final Path PHOTO_HIGH_RES = ROOT.resolve(\"highres\");\n    public static final Path BIO = ROOT.resolve(\"bio\");\n    public static final Path STATUS = ROOT.resolve(\"status\");\n    public static final Path FIRSTNAME = ROOT.resolve(\"firstname\");\n    public static final Path LASTNAME = ROOT.resolve(\"lastname\");\n    public static final Path PHONE = ROOT.resolve(\"phone\");\n    public static final Path EMAIL = ROOT.resolve(\"email\");\n    public static final Path WEBROOT = ROOT.resolve(\"webroot\"); // The path in Peergos to this users web root\n\n    private static <V> CompletableFuture<Optional<V>> getAndParse(Path p, Function<byte[], V> parser, UserContext viewer) {\n        return viewer.getByPath(p)\n                .thenCompose(opt -> parse(opt, parser, viewer));\n    }\n\n    private static <V> CompletableFuture<Optional<V>> parse(Optional<FileWrapper> opt, Function<byte[], V> parser, UserContext viewer) {\n        return opt.map(f -> Serialize.readFully(f, viewer.crypto, viewer.network)\n            .thenApply(parser)\n            .thenApply(Optional::of))\n            .orElse(Futures.of(Optional.empty()));\n    }\n\n    private static <V> CompletableFuture<Boolean> serializeAndSet(Path p, V val, Function<V, byte[]> serialize, UserContext user) {\n        byte[] raw = serialize.apply(val);\n        return user.getUserRoot()\n                .thenCompose(home -> home.getOrMkdirs(p.getParent(), user.network, true, user.mirrorBatId(), user.crypto))\n                .thenCompose(parent -> parent.uploadOrReplaceFile(p.getFileName().toString(),\n                        AsyncReader.build(raw), raw.length, user.network, user.crypto, () -> false, x -> {}))\n                .thenApply(x -> true);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Profile> getProfile(String username, UserContext context) {\n        return context.getChildren(username + \"/\" + ROOT.toString()).thenCompose(files -> {\n            Function<Path, Optional<FileWrapper>> resolve = filePath ->\n                    files.stream().filter(f -> f.getName().equals(filePath.getFileName().toString())).findFirst();\n            return getProfileFields(resolve, username, context);\n        });\n    }\n\n    public static CompletableFuture<Profile> getProfileFields(Function<Path, Optional<FileWrapper>> resolveFunc, String username, UserContext context) {\n        return parse(resolveFunc.apply(FIRSTNAME), String::new, context)\n            .thenCompose(firstNameOpt -> parse(resolveFunc.apply(LASTNAME), String::new, context)\n                    .thenCompose(lastNameOpt -> parse(resolveFunc.apply(BIO), String::new, context)\n                            .thenCompose(bioOpt -> parse(resolveFunc.apply(PHONE), String::new, context)\n                                    .thenCompose(phoneOpt -> parse(resolveFunc.apply(EMAIL), String::new, context)\n                                            .thenCompose(emailOpt -> parse(resolveFunc.apply(PHOTO), x -> x, context)\n                                                    .thenCompose(imageOpt -> parse(resolveFunc.apply(STATUS), String::new, context)\n                                                            .thenCompose(statusOpt -> parse(resolveFunc.apply(WEBROOT), String::new, context)\n                                                                    .thenApply(webRootOpt -> new Profile(imageOpt, bioOpt, statusOpt, firstNameOpt, lastNameOpt, phoneOpt, emailOpt, webRootOpt))\n                                                            )\n                                                    )\n                                            )\n                                    )\n                            )\n                    )\n            );\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<byte[]>> getHighResProfilePhoto(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(PHOTO_HIGH_RES), x -> x, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setHighResProfilePhoto(UserContext user, byte[] image) {\n        return serializeAndSet(PHOTO_HIGH_RES, image, x -> x, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<byte[]>> getProfilePhoto(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(PHOTO), x -> x, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setProfilePhoto(UserContext user, byte[] base64Str) {\n        return serializeAndSet(PHOTO, base64Str, x -> x, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<String>> getBio(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(BIO), String::new, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setBio(UserContext user, String bio) {\n        return serializeAndSet(BIO, bio, String::getBytes, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<String>> getStatus(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(STATUS), String::new, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setStatus(UserContext user, String status) {\n        return serializeAndSet(STATUS, status, String::getBytes, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<String>> getFirstName(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(FIRSTNAME), String::new, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setFirstName(UserContext user, String firstname) {\n        return serializeAndSet(FIRSTNAME, firstname, String::getBytes, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<String>> getLastName(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(LASTNAME), String::new, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setLastName(UserContext user, String lastname) {\n        return serializeAndSet(LASTNAME, lastname, String::getBytes, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<String>> getPhone(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(PHONE), String::new, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setPhone(UserContext user, String phone) {\n        return serializeAndSet(PHONE, phone, String::getBytes, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<String>> getEmail(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(EMAIL), String::new, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setEmail(UserContext user, String email) {\n        return serializeAndSet(EMAIL, email, String::getBytes, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Optional<String>> getWebRoot(String user, UserContext viewer) {\n        return getAndParse(PathUtil.get(user).resolve(WEBROOT), String::new, viewer);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> setWebRoot(UserContext user, String webroot) {\n        return serializeAndSet(WEBROOT, webroot, String::getBytes, user);\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> unpublishWebRoot(UserContext user) {\n        return getWebRoot(user.username, user)\n                .thenCompose(popt -> {\n                    if (popt.isEmpty())\n                        return Futures.of(false);\n                    String path = popt.get();\n                    if (path.length() == 0) {\n                        return Futures.of(false);\n                    }\n                    return user.unPublishFile(PathUtil.get(path)).thenApply(x -> true);\n                });\n    }\n\n    @JsMethod\n    public static CompletableFuture<Boolean> publishWebroot(UserContext user) {\n        // first publish the actual web root, then publish the profile entry linking to the webroot\n        return getWebRoot(user.username, user)\n                .thenCompose(popt -> {\n                    if (popt.isEmpty())\n                        return Futures.of(Optional.empty());\n                    return user.getByPath(popt.get())\n                            .thenCompose(fopt -> fopt.map(user::makePublic)\n                                    .map(f -> f.thenApply(Optional::of))\n                                    .orElse(Futures.of(Optional.empty())));\n                }).thenCompose(res -> {\n                    if (res.isEmpty())\n                        return Futures.of(Optional.empty());\n                    Path profileEntry = PathUtil.get(user.username).resolve(WEBROOT);\n                    return user.getPublicFile(profileEntry)\n                            .thenCompose(existing -> existing.isPresent() ?\n                                    Futures.of(Optional.of(existing.get().getVersionRoot())) :\n                                    user.getByPath(profileEntry)\n                                            .thenCompose(opt -> opt.map(user::makePublic)\n                                                    .map(f -> f.thenApply(Optional::of))\n                                                    .orElse(Futures.of(Optional.empty()))));\n                }).thenApply(x -> x.isPresent());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ProxyingAccount.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class ProxyingAccount implements Account {\n\n    private final List<Cid> serverIds;\n    private final CoreNode core;\n    private final Account local;\n    private final AccountProxy p2p;\n\n    public ProxyingAccount(List<Cid> serverIds, CoreNode core, Account local, AccountProxy p2p) {\n        this.serverIds = serverIds;\n        this.core = core;\n        this.local = local;\n        this.p2p = p2p;\n    }\n\n    @Override\n    public CompletableFuture<Boolean> setLoginData(LoginData login, byte[] auth, boolean forceLocal) {\n        return core.getPublicKeyHash(login.username).thenCompose(idOpt -> forceLocal ?\n                local.setLoginData(login, auth, true) :\n                Proxy.redirectCall(core,\n                        serverIds,\n                        idOpt.get(),\n                        () -> local.setLoginData(login, auth, false),\n                        target -> p2p.setLoginData(target, login, auth)));\n    }\n\n    @Override\n    public CompletableFuture<Either<UserStaticData, MultiFactorAuthRequest>> getLoginData(String username,\n                                                                                          PublicSigningKey authorisedReader,\n                                                                                          byte[] auth,\n                                                                                          Optional<MultiFactorAuthResponse>  mfa,\n                                                                                          boolean cacheMfaLoginData,\n                                                                                          boolean forceProxy,\n                                                                                          boolean forceNoCache) {\n        return core.getPublicKeyHash(username).thenCompose(idOpt -> Proxy.redirectCall(core,\n                serverIds,\n                idOpt.get(),\n                () -> local.getLoginData(username, authorisedReader, auth, mfa, false, forceProxy, forceNoCache),\n                target -> p2p.getLoginData(target, username, authorisedReader, auth, mfa)));\n    }\n\n    @Override\n    public CompletableFuture<TotpKey> addTotpFactor(String username, byte[] auth) {\n        return core.getPublicKeyHash(username).thenCompose(idOpt -> Proxy.redirectCall(core,\n                serverIds,\n                idOpt.get(),\n                () -> local.addTotpFactor(username, auth),\n                target -> p2p.addTotpFactor(target, username, auth)));\n    }\n\n    @Override\n    public CompletableFuture<byte[]> registerSecurityKeyStart(String username, byte[] auth) {\n        return core.getPublicKeyHash(username).thenCompose(idOpt -> Proxy.redirectCall(core,\n                serverIds,\n                idOpt.get(),\n                () -> local.registerSecurityKeyStart(username, auth),\n                target -> p2p.registerSecurityKeyStart(target, username, auth)));\n    }\n\n\n\n    @Override\n    public CompletableFuture<Boolean> registerSecurityKeyComplete(String username, String keyName, MultiFactorAuthResponse resp, byte[] auth) {\n        return core.getPublicKeyHash(username).thenCompose(idOpt -> Proxy.redirectCall(core,\n                serverIds,\n                idOpt.get(),\n                () -> local.registerSecurityKeyComplete(username, keyName, resp, auth),\n                target -> p2p.registerSecurityKeyComplete(target, username, keyName, resp, auth)));\n    }\n\n    @Override\n    public CompletableFuture<List<MultiFactorAuthMethod>> getSecondAuthMethods(String username, byte[] auth) {\n        return core.getPublicKeyHash(username).thenCompose(idOpt -> Proxy.redirectCall(core,\n                serverIds,\n                idOpt.get(),\n                () -> local.getSecondAuthMethods(username, auth),\n                target -> p2p.getSecondAuthMethods(target, username, auth)));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> enableTotpFactor(String username, byte[] credentialId, String code, byte[] auth) {\n        return core.getPublicKeyHash(username).thenCompose(idOpt -> Proxy.redirectCall(core,\n                serverIds,\n                idOpt.get(),\n                () -> local.enableTotpFactor(username, credentialId, code, auth),\n                target -> p2p.enableTotpFactor(target, username, credentialId, code, auth)));\n    }\n\n    @Override\n    public CompletableFuture<Boolean> deleteSecondFactor(String username, byte[] credentialId, byte[] auth) {\n        return core.getPublicKeyHash(username).thenCompose(idOpt -> Proxy.redirectCall(core,\n                serverIds,\n                idOpt.get(),\n                () -> local.deleteSecondFactor(username, credentialId, auth),\n                target -> p2p.deleteSecondFactor(username, credentialId, auth)));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/RandomSecretType.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class RandomSecretType implements SecretGenerationAlgorithm {\n\n    public RandomSecretType() {\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> props = new TreeMap<>();\n        props.put(\"type\", new CborObject.CborLong(getType().value));\n        return CborObject.CborMap.build(props);\n    }\n\n    static RandomSecretType fromCbor(CborObject cbor) {\n        return new RandomSecretType();\n    }\n\n    @Override\n    public boolean generateBoxerAndIdentity() {\n        return false;\n    }\n\n    @Override\n    public SecretGenerationAlgorithm withoutBoxerOrIdentity() {\n        return this;\n    }\n\n    @Override\n    public Type getType() {\n        return Type.Random;\n    }\n\n    @Override\n    public String getExtraSalt() {\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/RetrievedEntryPoint.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.user.fs.*;\n\npublic class RetrievedEntryPoint {\n\n    public final EntryPoint entry;\n    public final String path;\n    public final FileWrapper file;\n\n    public RetrievedEntryPoint(EntryPoint entry, String path, FileWrapper file) {\n        this.entry = entry;\n        this.path = path;\n        this.file = file;\n    }\n\n    public String getPath() {\n        return path;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ScryptGenerator.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class ScryptGenerator implements SecretGenerationAlgorithm {\n\n    public static final int MIN_MEMORY_COST = 15; // 15 is only used for secret links, 17 is used for login\n    public static final int LOGIN_MEMORY_COST = 17;\n\n    @JsProperty\n    public final int memoryCost, cpuCost, parallelism, outputBytes;\n    @JsProperty\n    public final String extraSalt;\n\n    public ScryptGenerator(int memoryCost, int cpuCost, int parallelism, int outputBytes, String extraSalt) {\n        if (memoryCost < MIN_MEMORY_COST)\n            throw new IllegalStateException(\"Scrypt memory cost must be >= 17\");\n        this.memoryCost = memoryCost;\n        this.cpuCost = cpuCost;\n        this.parallelism = parallelism;\n        this.outputBytes = outputBytes;\n        this.extraSalt = extraSalt;\n    }\n\n    @Override\n    public boolean generateBoxerAndIdentity() {\n        return outputBytes == 96;\n    }\n\n    @Override\n    public SecretGenerationAlgorithm withoutBoxerOrIdentity() {\n        return new ScryptGenerator(memoryCost, cpuCost, parallelism, 64, extraSalt);\n    }\n\n    @Override\n    public String getExtraSalt() {\n        return extraSalt;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> props = new TreeMap<>();\n        props.put(\"type\", new CborObject.CborLong(getType().value));\n        props.put(\"m\", new CborObject.CborLong(memoryCost));\n        props.put(\"c\", new CborObject.CborLong(cpuCost));\n        props.put(\"p\", new CborObject.CborLong(parallelism));\n        props.put(\"o\", new CborObject.CborLong(outputBytes));\n        props.put(\"s\", new CborObject.CborString(extraSalt));\n        return CborObject.CborMap.build(props);\n    }\n\n    static ScryptGenerator fromCbor(Cborable cbor) {\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        int memoryCost = (int) map.getLong(\"m\");\n        int cpuCost = (int) map.getLong(\"c\");\n        int parallelsm = (int) map.getLong(\"p\");\n        int outputBytes = (int) map.getLong(\"o\");\n        String extraSalt = map.getString(\"s\", \"\");\n        return new ScryptGenerator(memoryCost, cpuCost, parallelsm, outputBytes, extraSalt);\n    }\n\n    @Override\n    public Type getType() {\n        return Type.Scrypt;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/SecretGenerationAlgorithm.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic interface SecretGenerationAlgorithm extends Cborable {\n    Map<Integer, Type> byValue = new HashMap<>();\n    @JsType\n    enum Type {\n        Random(0x0),\n        Scrypt(0x1);\n\n        public final int value;\n\n        Type(int value) {\n            this.value = value;\n            byValue.put(value, this);\n        }\n\n        public static Type byValue(int val) {\n            if (!byValue.containsKey(val))\n                throw new IllegalStateException(\"Unknown User Generation Algorithm type: \" + ArrayOps.byteToHex(val));\n            return byValue.get(val);\n        }\n    }\n\n    @JsMethod\n    Type getType();\n\n    String getExtraSalt();\n\n    boolean generateBoxerAndIdentity();\n\n    SecretGenerationAlgorithm withoutBoxerOrIdentity();\n\n    static SecretGenerationAlgorithm getDefault(SafeRandom rnd) {\n        return new ScryptGenerator(ScryptGenerator.LOGIN_MEMORY_COST, 8, 1, 64, generateSalt(rnd));\n    }\n\n    static SecretGenerationAlgorithm getLegacy(SafeRandom rnd) {\n        return new ScryptGenerator(ScryptGenerator.LOGIN_MEMORY_COST, 8, 1, 96, generateSalt(rnd));\n    }\n\n    static SecretGenerationAlgorithm getDefaultWithoutExtraSalt() {\n        return new ScryptGenerator(ScryptGenerator.LOGIN_MEMORY_COST, 8, 1, 64, \"\");\n    }\n\n    static String generateSalt(SafeRandom rnd) {\n        return ArrayOps.bytesToHex(rnd.randomBytes(32));\n    }\n\n    static SecretGenerationAlgorithm withNewSalt(SecretGenerationAlgorithm alg, SafeRandom rnd) {\n        if (alg instanceof ScryptGenerator) {\n            ScryptGenerator scrypt = (ScryptGenerator) alg;\n            return new ScryptGenerator(scrypt.memoryCost, scrypt.cpuCost, scrypt.parallelism, scrypt.outputBytes, generateSalt(rnd));\n        }\n        return alg;\n    }\n\n    static SecretGenerationAlgorithm fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor type for SecretGenerationAlgorithm: \" + cbor);\n        Type type = Type.byValue((int)((CborObject.CborMap) cbor).getLong(\"type\"));\n        if (type == Type.Scrypt)\n            return ScryptGenerator.fromCbor(cbor);\n        if (type == Type.Random)\n            return new RandomSecretType();\n        throw new IllegalStateException(\"Unimplemented UserGeneration type algorithm: \" + type);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/SecretLinkChamp.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.hamt.*;\nimport peergos.shared.io.ipfs.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class SecretLinkChamp {\n\n    public final Multihash root;\n    private final ChampWrapper<CborObject.CborMerkleLink> champ;\n    private final ContentAddressedStorage ipfs;\n    private final Hasher hasher;\n\n    private SecretLinkChamp(Multihash root, ChampWrapper<CborObject.CborMerkleLink> champ, ContentAddressedStorage ipfs, Hasher hasher) {\n        this.root = root;\n        this.champ = champ;\n        this.ipfs = ipfs;\n        this.hasher = hasher;\n    }\n\n    public static CompletableFuture<Cid> createEmpty(PublicKeyHash owner,\n                                                     SigningPrivateKeyAndPublicHash writer,\n                                                     Optional<BatId> mirrorBat,\n                                                     ContentAddressedStorage ipfs,\n                                                     Hasher hasher,\n                                                     TransactionId tid) {\n        Champ<CborObject.CborMerkleLink> newRoot = Champ.empty(c -> (CborObject.CborMerkleLink)c).withBat(mirrorBat);\n        byte[] raw = newRoot.serialize();\n        return hasher.sha256(raw)\n                .thenCompose(hash -> writer.secret.signMessage(hash)\n                        .thenCompose(sig -> ipfs.put(owner, writer.publicKeyHash, sig, raw, tid)));\n    }\n\n    public static CompletableFuture<SecretLinkChamp> build(PublicKeyHash owner, Cid root, Optional<BatWithId> mirrorBat, ContentAddressedStorage ipfs, Hasher hasher) {\n        return ChampWrapper.create(owner, root, mirrorBat, b -> Futures.of(b.data), ipfs, hasher, c -> (CborObject.CborMerkleLink)c)\n                .thenApply(c -> new SecretLinkChamp(root, c, ipfs, hasher));\n    }\n\n    private CompletableFuture<byte[]> keyToBytes(long key) {\n        byte[] copy = new byte[8];\n        for (int i=0; i < 8; i++) {\n            copy[i] = (byte) (key >> (8 * i));\n        }\n        return hasher.sha256(copy);\n    }\n\n    public CompletableFuture<Optional<SecretLinkTarget>> get(PublicKeyHash owner,\n                                                             long label) {\n        return keyToBytes(label)\n                .thenCompose(champ::get)\n                .thenCompose(res -> res.isPresent() ?\n                        ipfs.get(owner, (Cid)res.get().target, Optional.empty()).thenApply(raw -> raw.map(SecretLinkTarget::fromCbor)) :\n                        CompletableFuture.completedFuture(Optional.empty()));\n    }\n\n    public CompletableFuture<Pair<Multihash, Cid>> add(SigningPrivateKeyAndPublicHash owner,\n                                            long label,\n                                            SecretLinkTarget target,\n                                            Optional<CborObject.CborMerkleLink> existing,\n                                            Optional<BatId> mirrorBat,\n                                            Hasher hasher,\n                                            TransactionId tid) {\n        return keyToBytes(label)\n                .thenCompose(key -> ipfs.put(owner.publicKeyHash, owner, target.serialize(), hasher, tid)\n                        .thenCompose(valueHash ->\n                                champ.put(owner.publicKeyHash, owner, key, existing, new CborObject.CborMerkleLink(valueHash), mirrorBat, tid).thenApply(root -> new Pair<>(root, valueHash))));\n    }\n\n    public CompletableFuture<Multihash> remove(PublicKeyHash owner,\n                                               SigningPrivateKeyAndPublicHash writer,\n                                               long label,\n                                               Optional<BatId> mirrorBat,\n                                               TransactionId tid) {\n        return keyToBytes(label)\n                .thenCompose(key -> champ.get(key)\n                        .thenCompose(existing -> existing.isPresent() ?\n                                champ.remove(owner, writer, key, existing, mirrorBat, tid) :\n                                Futures.of(champ.getRoot())));\n    }\n\n    public CompletableFuture<Boolean> contains(long label) {\n        return keyToBytes(label)\n                .thenCompose(champ::get)\n                .thenApply(Optional::isPresent);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ServerConversation.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\n\nimport java.util.*;\n\npublic class ServerConversation implements Comparable<ServerConversation> {\n\n    public final long latestEpochMillis;\n    @JsProperty\n    public final List<ServerMessage> messages;\n    public final boolean isDisplayable;\n\n    public ServerConversation(List<ServerMessage> messages) {\n        this.messages = messages;\n        long latest = 0;\n        for (ServerMessage message : messages) {\n            if (message.sentEpochMillis > latest)\n                latest = message.sentEpochMillis;\n        }\n        ServerMessage lastMessage = messages.get(messages.size() - 1);\n        this.isDisplayable = !lastMessage.isDismissed && lastMessage.type != ServerMessage.Type.FromUser;\n        this.latestEpochMillis = latest;\n    }\n\n    public ServerMessage lastMessage() {\n        return messages.get(messages.size() - 1);\n    }\n\n    @Override\n    public int compareTo(ServerConversation other) {\n        return Long.compare(latestEpochMillis, other.latestEpochMillis);\n    }\n\n    public static List<ServerConversation> combine(List<ServerMessage> all) {\n        Map<Long, List<ServerMessage>> lookup = new HashMap<>();\n        List<ServerConversation> res = new ArrayList<>();\n        for (ServerMessage msg : all) {\n            List<ServerMessage> conv;\n            if (msg.replyToId.isPresent()) {\n                lookup.putIfAbsent(msg.replyToId.get(), new ArrayList<>());\n                conv = lookup.get(msg.replyToId.get());\n                lookup.put(msg.id, conv);\n            } else {\n                conv = lookup.computeIfAbsent(msg.id, x -> new ArrayList<>());\n            }\n            conv.add(msg);\n        }\n        new HashSet<>(lookup.values()).forEach(c -> res.add(new ServerConversation(c)));\n        Collections.sort(res);\n        return res;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ServerMessage.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\n\nimport java.time.*;\nimport java.util.*;\n\n@JsType\npublic class ServerMessage implements Comparable<ServerMessage>, Cborable {\n    public static final int MAX_CONTENT_SIZE = 4096;\n    private static final Map<Integer, Type> byValue = new HashMap<>();\n    @JsType\n    public enum Type {\n        FromServer(1),\n        FromUser(2),\n        Dismiss(3);\n\n        public final int value;\n        Type(int value) {\n            this.value = value;\n            byValue.put(value, this);\n        }\n\n        public static Type byValue(int val) {\n            if (!byValue.containsKey(val))\n                throw new IllegalStateException(\"Unknown server message type: \" + val);\n            return byValue.get(val);\n        }\n    }\n\n    // id is unique to server. Allocated by server.\n    // For replies/dismissals this is the id of the message being replied to.\n    // For messages from user that aren't replies this is -1.\n    @JsIgnore\n    public final long id;\n    public final Type type;\n    @JsIgnore\n    public final long sentEpochMillis;\n    public final String contents;\n    public final Optional<Long> replyToId;\n    public final boolean isDismissed;\n\n    public ServerMessage(long id, Type type, long sentEpochMillis, String contents, Optional<Long> replyToId, boolean isDismissed) {\n        if (contents.length() > MAX_CONTENT_SIZE)\n            throw new IllegalStateException(\"Message body is longer than maximum allowed size of \" + MAX_CONTENT_SIZE + \" characters!\");\n        this.id = id;\n        this.type = type;\n        this.sentEpochMillis = sentEpochMillis;\n        this.contents = contents;\n        this.replyToId = replyToId;\n        this.isDismissed = isDismissed;\n    }\n\n    public String summary() {\n        return \"### \" + id + \": \" + type.name() + \" \" + getSendTime().toString() + \" dismissed:\" + isDismissed +\n                (replyToId.map(id -> \" <==\" + id).orElse(\"\"));\n    }\n\n    @JsMethod\n    public String getAuthor() {\n        return type.name();\n    }\n\n    @JsMethod\n    public String id() {\n        return Long.toString(id);\n    }\n\n    @JsMethod\n    public String getPreviousMessageId() {\n        return replyToId.isPresent() ? replyToId.get().toString() : null;\n    }\n\n    @JsMethod\n    public String getContents() {\n        return contents;\n    }\n\n    @JsMethod\n    public LocalDateTime getSendTime() {\n        return LocalDateTime.ofEpochSecond(sentEpochMillis/1000, (int)(sentEpochMillis % 1000)*1000, ZoneOffset.UTC);\n    }\n\n    @Override\n    public int compareTo(ServerMessage other) {\n        return Long.compare(sentEpochMillis, other.sentEpochMillis);\n    }\n\n    public static ServerMessage buildUserMessage(String body) {\n        return new ServerMessage(-1, Type.FromUser, System.currentTimeMillis(), body, Optional.empty(), false);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"id\", new CborObject.CborLong(id));\n        state.put(\"s\", new CborObject.CborLong(sentEpochMillis));\n        state.put(\"t\", new CborObject.CborLong(type.value));\n        state.put(\"b\", new CborObject.CborString(contents));\n        state.put(\"d\", new CborObject.CborBoolean(isDismissed));\n        replyToId.ifPresent(rid -> state.put(\"r\", new CborObject.CborLong(rid)));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ServerMessage fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for ServerMessage! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        long id = m.getLong(\"id\");\n        long sentMillis = m.getLong(\"s\");\n        Type type = Type.byValue((int)m.getLong(\"t\"));\n        String contents = m.getString(\"b\");\n        boolean isDismissed = m.getBoolean(\"d\");\n        Optional<Long> replyToId = m.getOptionalLong(\"r\");\n        return new ServerMessage(id, type, sentMillis, contents, replyToId, isDismissed);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/ServerMessager.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic interface ServerMessager {\n\n    CompletableFuture<List<ServerMessage>> getMessages(String username, byte[] auth);\n\n    CompletableFuture<Boolean> sendMessage(String username, byte[] signedBody);\n\n    default CompletableFuture<List<ServerMessage>> getMessages(String username, SecretSigningKey signer) {\n        TimeLimitedClient.SignedRequest req =\n                new TimeLimitedClient.SignedRequest(Constants.SERVER_MESSAGE_URL + \"retrieve\", System.currentTimeMillis());\n        return req.sign(signer)\n                .thenCompose(auth -> getMessages(username, auth));\n    }\n\n    default CompletableFuture<Boolean> sendMessage(String username, ServerMessage message, SecretSigningKey signer) {\n        return signer.signMessage(message.serialize())\n                .thenCompose(signedBody -> sendMessage(username, signedBody));\n    }\n\n    class HTTP implements ServerMessager {\n        private final HttpPoster poster;\n\n        public HTTP(HttpPoster poster) {\n            this.poster = poster;\n        }\n\n        @Override\n        public CompletableFuture<List<ServerMessage>> getMessages(String username, byte[] auth) {\n            return poster.get(Constants.SERVER_MESSAGE_URL + \"retrieve?username=\" + username + \"&auth=\" + ArrayOps.bytesToHex(auth))\n                    .thenApply(res ->\n                            ((CborObject.CborList)CborObject.fromByteArray(res)).value\n                                    .stream()\n                                    .map(ServerMessage::fromCbor)\n                                    .collect(Collectors.toList()));\n        }\n\n        @Override\n        public CompletableFuture<Boolean> sendMessage(String username, byte[] signedBody) {\n            return poster.post(Constants.SERVER_MESSAGE_URL + \"send?username=\" + username, signedBody, false)\n                    .thenApply(res -> ((CborObject.CborBoolean)CborObject.fromByteArray(res)).value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/SharedWithCache.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** Who we've shared each file with is stored in a parallel directory tree under the CACHE_BASE dir.\n *  A serialized SharedWithState for all the children of a directory is stored at\n *  CACHE_BASE/$path-to-dir/sharedWith.cbor\n */\npublic class SharedWithCache {\n\n    @JsType\n    public enum Access { READ, WRITE }\n\n    private static final String DIR_CACHE_FILENAME = \"sharedWith.cbor\";\n    private static final String CACHE_BASE_NAME = \"outbound\";\n    private static final Path CACHE_BASE = PathUtil.get(CapabilityStore.CAPABILITY_CACHE_DIR, CACHE_BASE_NAME);\n\n    private final FileWrapper base;\n    private final String ourname;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n\n    public SharedWithCache(FileWrapper base,\n                           String ourname,\n                           NetworkAccess network,\n                           Crypto crypto) {\n        this.base = base;\n        this.ourname = ourname;\n        this.network = network;\n        this.crypto = crypto;\n    }\n\n    private static Path canonicalise(Path p) {\n        return p.isAbsolute() ? p : PathUtil.get(\"/\").resolve(p);\n    }\n\n    private static Path toRelative(Path p) {\n        return p.isAbsolute() ? PathUtil.get(p.toString().substring(1)) : p;\n    }\n\n    private static Path cacheBase(String username) {\n        return PathUtil.get(username).resolve(CACHE_BASE);\n    }\n\n    private CompletableFuture<Optional<SharedWithState>> retrieve(Path dir, Snapshot s) {\n        return retrieveWithFile(dir, s).thenApply(opt -> opt.map(p -> p.right));\n    }\n\n    private CompletableFuture<Optional<Pair<FileWrapper, SharedWithState>>> retrieveWithFile(Path dir, Snapshot s) {\n        return base.getUpdated(s, network)\n                .thenCompose(updated -> updated.getDescendentByPath(toRelative(dir).resolve(DIR_CACHE_FILENAME).toString(), crypto.hasher, network))\n                .thenCompose(opt -> opt.isEmpty() ?\n                        Futures.of(Optional.empty()) :\n                        parseCacheFile(opt.get(), network, crypto)\n                                .thenApply(state -> new Pair<>(opt.get(), state))\n                                .thenApply(Optional::of)\n                );\n    }\n\n    private static CompletableFuture<Snapshot> addSharedWith(FileWrapper base,\n                                                             Access access,\n                                                             Path toFile,\n                                                             Set<String> names,\n                                                             NetworkAccess network,\n                                                             Crypto crypto,\n                                                             Snapshot in,\n                                                             Committer committer) {\n        return base.getUpdated(in, network)\n                .thenCompose(updateed -> retrieveWithFileOrCreate(updateed, toFile.getParent(), network, crypto, in, committer))\n                .thenCompose(p -> {\n                    FileWrapper source = p.left;\n                    SharedWithState current = p.right;\n                    SharedWithState updated = current.add(access, getFilename(toFile), names);\n                    byte[] raw = updated.serialize();\n                    return source.overwriteFile(AsyncReader.build(raw), raw.length, network, crypto, x -> {}, source.version, committer);\n                });\n    }\n\n    private static CompletableFuture<Boolean> buildSharedWithCache(TrieNode root, String ourname, NetworkAccess network, Crypto crypto) {\n        return root.getByPath(PathUtil.get(ourname, CapabilityStore.CAPABILITY_CACHE_DIR).toString(), crypto.hasher, network)\n                .thenCompose(cacheDirOpt -> root.getByPath(PathUtil.get(ourname, UserContext.SHARED_DIR_NAME).toString(), crypto.hasher, network)\n                        .thenCompose(shared -> shared.get().getChildren(crypto.hasher, network))\n                        .thenCompose(children ->\n                                Futures.reduceAll(children,\n                                        true,\n                                        (x, friendDirectory) ->\n                                                friendDirectory.getUpdated(network)\n                                                        .thenCompose(updatedFriendDir -> CapabilityStore.loadReadOnlyLinks(cacheDirOpt.get(), updatedFriendDir,\n                                                                ourname, network, crypto, false))\n                                                        .thenCompose(readCaps ->\n                                                                network.synchronizer.applyComplexUpdate(friendDirectory.owner(),\n                                                                        friendDirectory.signingPair(),\n                                                                        (s, c) -> Futures.reduceAll(readCaps.getRetrievedCapabilities(),\n                                                                                s,\n                                                                                (v, rc) -> addSharedWith(cacheDirOpt.get(), Access.READ, PathUtil.get(rc.path), Collections.singleton(friendDirectory.getName()), network, crypto, v, c),\n                                                                                (a, b) -> b)\n                                                                                .thenCompose(s2 -> friendDirectory.getUpdated(network)\n                                                                                        .thenCompose(updatedFriendDir -> CapabilityStore.loadWriteableLinks(cacheDirOpt.get(), updatedFriendDir,\n                                                                                                ourname, network, crypto, false))\n                                                                                        .thenCompose(writeCaps ->\n                                                                                                Futures.reduceAll(writeCaps.getRetrievedCapabilities(), s2,\n                                                                                                        (v, rc) -> addSharedWith(cacheDirOpt.get(), Access.WRITE, PathUtil.get(rc.path), Collections.singleton(friendDirectory.getName()), network, crypto, v, c),\n                                                                                                        (a, b) -> b)\n                                                                                        ))))\n                                                        .thenApply(y -> true),\n                                        (a, b) -> a && b)));\n    }\n\n    private static CompletableFuture<SharedWithCache> initializeCache(TrieNode root, String username, NetworkAccess network, Crypto crypto) {\n        return root.getByPath(PathUtil.get(username).toString(), crypto.hasher, network)\n                .thenCompose(userRoot -> network.synchronizer.applyComplexUpdate(userRoot.get().owner(), userRoot.get().signingPair(),\n                        (s, c) -> getOrMkdir(userRoot.get(), CapabilityStore.CAPABILITY_CACHE_DIR, network, crypto, s, c)\n                                .thenApply(f -> f.version))\n                        .thenCompose(s -> userRoot.get().getUpdated(s, network)\n                                .thenCompose(home -> home.getChild(CapabilityStore.CAPABILITY_CACHE_DIR, crypto.hasher, network))\n                                .thenCompose(cacheRootOpt -> cacheRootOpt.get().mkdir(CACHE_BASE_NAME, network, true,\n                                        cacheRootOpt.get().mirrorBatId(), crypto)))\n                        .thenCompose(x -> buildSharedWithCache(root, username, network, crypto)) // build from outbound cap files\n                        .thenCompose(x -> root.getByPath(cacheBase(username).toString(), crypto.hasher, network)\n                                .thenApply(Optional::get)\n                                .thenApply(f -> new SharedWithCache(f, username, network, crypto))));\n    }\n\n    public static CompletableFuture<SharedWithCache> initOrBuild(TrieNode root, String username, NetworkAccess network, Crypto crypto) {\n        return root.getByPath(cacheBase(username).toString(), crypto.hasher, network)\n                .thenCompose(opt -> {\n                    if (opt.isPresent())\n                        return Futures.of(new SharedWithCache(opt.get(), username, network, crypto));\n                    return initializeCache(root, username, network, crypto);\n                });\n    }\n\n    private static CompletableFuture<FileWrapper> getOrMkdir(FileWrapper parent, String dirName, NetworkAccess network, Crypto crypto, Snapshot s, Committer c) {\n        return parent.getChild(dirName, crypto.hasher, network)\n                .thenCompose(opt -> opt.isPresent() ?\n                        Futures.of(opt.get()) :\n                        parent.mkdir(dirName, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), true,\n                                parent.mirrorBatId(), network, crypto, parent.version.mergeAndOverwriteWith(s), c)\n                                .thenCompose(s2 -> parent.getUpdated(s2, network)\n                                        .thenCompose(p -> p.getChild(dirName, crypto.hasher, network)))\n                                .thenApply(Optional::get));\n    }\n\n    private static CompletableFuture<FileWrapper> getOrMkdirs(FileWrapper parent, List<String> remaining, NetworkAccess network, Crypto crypto, Snapshot s, Committer c) {\n        if (remaining.isEmpty())\n            return Futures.of(parent);\n        return getOrMkdir(parent, remaining.get(0), network, crypto, s, c)\n                .thenCompose(child -> getOrMkdirs(child, remaining.subList(1, remaining.size()), network, crypto, child.version, c));\n    }\n\n    public CompletableFuture<SharedWithState> getDirSharingState(Path dir, Snapshot in) {\n        if (this.ourname == null) {\n            return Futures.of(SharedWithState.empty());\n        }\n        return base.getUpdated(base.version.mergeAndOverwriteWith(in), network)\n                .thenCompose(updated -> updated.getDescendentByPath(dir.resolve(DIR_CACHE_FILENAME).toString(), crypto.hasher, network))\n                .thenCompose(fopt -> fopt.map(f -> parseCacheFile(f, network, crypto)).orElse(Futures.of(SharedWithState.empty())));\n    }\n\n    public CompletableFuture<Boolean> processShared(BiConsumer<String, SharedWithState> processor, String username, Snapshot in) {\n        return base.getUpdated(base.version.mergeAndOverwriteWith(in), network)\n                .thenCompose(updated -> updated.getChild(username, crypto.hasher, network)\n                        .thenCompose(home -> processShared(processor, home.get(), PathUtil.get(\"\"))));\n    }\n\n    private CompletableFuture<Boolean> processShared(BiConsumer<String, SharedWithState> processor, FileWrapper currentDir, Path current) {\n        return currentDir.getChildren(crypto.hasher, network)\n                .thenCompose(children -> {\n                    Optional<FileWrapper> sharedInDir = children.stream().filter(c -> c.getName().equals(DIR_CACHE_FILENAME)).findFirst();\n                    sharedInDir.ifPresent(swf -> parseCacheFile(swf, network, crypto).thenAccept(sw -> processor.accept(current.toString(), sw)));\n                    return Futures.combineAll(children.stream().filter(c -> c.isDirectory()).map(d -> processShared(processor, d, current.resolve(d.getName()))).collect(Collectors.toList()));\n                }).thenApply(x -> true);\n    }\n\n    private static CompletableFuture<Optional<Pair<FileWrapper, SharedWithState>>> retrieveWithFile(FileWrapper base,\n                                                                                                    Path dir,\n                                                                                                    NetworkAccess network,\n                                                                                                    Crypto crypto,\n                                                                                                    Snapshot in) {\n        return base.getDescendentByPath(dir.toString(), in, crypto.hasher, network)\n                .thenCompose(parent -> parent.isEmpty() ?\n                        Futures.of(Optional.empty()) :\n                        parent.get().getChild(DIR_CACHE_FILENAME, crypto.hasher, network)\n                                .thenCompose(fopt -> {\n                                    if (fopt.isPresent())\n                                        return parseCacheFile(fopt.get(), network, crypto)\n                                                .thenApply(c -> Optional.of(new Pair<>(fopt.get(), c)));\n                                    return Futures.of(Optional.empty());\n                                }));\n    }\n\n    private static CompletableFuture<Pair<FileWrapper, SharedWithState>> retrieveWithFileOrCreate(FileWrapper base,\n                                                                                                  Path dir,\n                                                                                                  NetworkAccess network,\n                                                                                                  Crypto crypto,\n                                                                                                  Snapshot in,\n                                                                                                  Committer committer) {\n        return getOrMkdirs(base, toList(dir), network, crypto, in, committer)\n                .thenCompose(parent -> parent.getChild(DIR_CACHE_FILENAME, crypto.hasher, network)\n                        .thenCompose(fopt -> {\n                            if (fopt.isPresent())\n                                return parseCacheFile(fopt.get(), network, crypto)\n                                        .thenApply(c -> new Pair<>(fopt.get(), c));\n                            SharedWithState empty = SharedWithState.empty();\n                            byte[] raw = empty.serialize();\n                            // upload or replace file\n                            return parent.uploadFileSection(parent.version, committer, DIR_CACHE_FILENAME, AsyncReader.build(raw), false, 0, raw.length,\n                                    Optional.empty(), false, true, true, network, crypto, () -> false, x -> {},\n                                    crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)), parent.mirrorBatId())\n                                    .thenCompose(s -> parent.getUpdated(s, network)\n                                            .thenCompose(updatedParent -> updatedParent.getChild(DIR_CACHE_FILENAME, crypto.hasher, network)))\n                                    .thenApply(copt -> new Pair<>(copt.get(), empty));\n                        }));\n    }\n\n    private static List<String> toList(Path p) {\n        return PathUtil.components(p);\n    }\n\n    private static CompletableFuture<SharedWithState> parseCacheFile(FileWrapper cache, NetworkAccess network, Crypto crypto) {\n        return cache.getInputStream(cache.version.get(cache.writer()), network, crypto, x -> {})\n                .thenCompose(in -> Serialize.readFully(in, cache.getSize()))\n                .thenApply(CborObject::fromByteArray)\n                .thenApply(SharedWithState::fromCbor);\n    }\n\n    private static String getFilename(Path p) {\n        return p.getName(p.getNameCount() - 1).toString();\n    }\n\n    public CompletableFuture<Map<Path, SharedWithState>> getAllDescendantShares(Path start, Snapshot in) {\n        if (base == null) // in a secret link\n            return Futures.of(Collections.emptyMap());\n        return in.withWriter(base.owner(), base.writer(), network)\n                .thenCompose(s -> base.getUpdated(base.version.mergeAndOverwriteWith(s), network))\n                .thenCompose(freshBase -> freshBase.getDescendentByPath(toRelative(start.getParent()).toString(), crypto.hasher, network))\n                .thenCompose(opt -> {\n                    if (opt.isEmpty())\n                        return Futures.of(Collections.emptyMap());\n                    FileWrapper parent = opt.get();\n                    String filename = getFilename(start);\n                    return parent.getChild(DIR_CACHE_FILENAME, crypto.hasher, network)\n                            .thenCompose(fopt -> fopt.isEmpty() ?\n                                    Futures.of(Collections.<Path, SharedWithState>emptyMap()) :\n                                    parseCacheFile(fopt.get(), network, crypto)\n                                            .thenApply(c -> c.filter(filename)\n                                                    .map(r -> Collections.singletonMap(start.getParent(), r))\n                                                    .orElse(Collections.emptyMap()))\n                            ).thenCompose(m -> parent.getChild(filename, crypto.hasher, network)\n                                    .thenCompose(copt -> copt.isEmpty() ?\n                                            Futures.of(m) :\n                                            getAllDescendantSharesRecurse(copt.get(), start)\n                                                    .thenApply(d -> merge(d, m))));\n                });\n    }\n\n    private <K, V> Map<K, V> merge(Map<K, V> a, Map<K, V> b) {\n        HashMap<K, V> res = new HashMap<>(a);\n        res.putAll(b); // no key conflicts\n        return res;\n    }\n\n    public CompletableFuture<Map<Path, SharedWithState>> getAllDescendantSharesRecurse(FileWrapper f, Path toUs) {\n        if (! f.isDirectory()) {\n            if (! f.getName().equals(DIR_CACHE_FILENAME))\n                throw new IllegalStateException(\"Invalid shared with cache!\");\n            return parseCacheFile(f, network, crypto)\n                    .thenApply(c -> Collections.singletonMap(toUs.getParent(), c));\n        }\n        return f.getChildren(crypto.hasher, network)\n                .thenCompose(children -> Futures.combineAll(children.stream()\n                        .map(c -> getAllDescendantSharesRecurse(c, toUs.resolve(c.getName())))\n                        .collect(Collectors.toList())))\n                .thenApply(s -> s.stream()\n                        .flatMap(m -> m.entrySet().stream())\n                        .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())));\n    }\n\n    public CompletableFuture<Map<Path, Set<String>>> getAllReadShares(Path start, Snapshot s) {\n        return getAllDescendantShares(start, s)\n                .thenApply(m -> m.entrySet().stream()\n                        .flatMap(e -> e.getValue().readShares().entrySet()\n                                .stream()\n                                .map(e2 -> new Pair<>(e.getKey().resolve(e2.getKey()), e2.getValue())))\n                        .collect(Collectors.toMap(p -> p.left, p -> p.right)));\n    }\n\n    public CompletableFuture<Map<Path, Set<String>>> getAllWriteShares(Path start, Snapshot s) {\n        return getAllDescendantShares(start, s)\n                .thenApply(m -> m.entrySet().stream()\n                        .flatMap(e -> e.getValue().writeShares().entrySet()\n                                .stream()\n                                .map(e2 -> new Pair<>(e.getKey().resolve(e2.getKey()), e2.getValue())))\n                        .collect(Collectors.toMap(p -> p.left, p -> p.right)));\n    }\n\n    public CompletableFuture<FileSharedWithState> getSharedWith(Path p, Snapshot v) {\n        return retrieve(p.getParent(), v)\n                .thenApply(opt -> opt.map(s -> s.get(getFilename(p))).orElse(FileSharedWithState.EMPTY));\n    }\n\n    public CompletableFuture<Snapshot> applyAndCommit(Path toFile, Function<SharedWithState, SharedWithState> transform, Snapshot in, Committer committer, NetworkAccess network) {\n        return base.getUpdated(in, network)\n                .thenCompose(updated -> retrieveWithFileOrCreate(updated, toFile.getParent(), network, crypto, in, committer))\n                .thenCompose(p -> {\n                    FileWrapper source = p.left;\n                    SharedWithState current = p.right;\n                    SharedWithState updated = transform.apply(current);\n                    if (current.equals(updated))\n                        return Futures.of(p.left.version);\n                    byte[] raw = updated.serialize();\n                    return source.overwriteFile(AsyncReader.build(raw), raw.length, network, crypto, x -> {}, source.version, committer);\n                }).thenApply(v -> in.mergeAndOverwriteWith(v));\n    }\n\n    public CompletableFuture<Snapshot> applyIfPresentAndCommit(Path toFile, Function<SharedWithState, SharedWithState> transform, Snapshot in, Committer committer, NetworkAccess network) {\n        return in.withWriter(base.owner(), base.writer(), network)\n                .thenCompose(v -> base.getUpdated(v, network))\n                .thenCompose(updated -> retrieveWithFile(updated, toFile.getParent(), network, crypto, updated.version)\n                .thenCompose(popt -> {\n                    if (popt.isEmpty())\n                        return Futures.of(updated.version);\n                    Pair<FileWrapper, SharedWithState> p = popt.get();\n                    FileWrapper source = p.left;\n                    SharedWithState current = p.right;\n                    SharedWithState modified = transform.apply(current);\n                    if (current.equals(modified))\n                        return Futures.of(p.left.version);\n                    byte[] raw = modified.serialize();\n                    return source.overwriteFile(AsyncReader.build(raw), raw.length, network, crypto, x -> {}, source.version, committer);\n                })).thenApply(v -> in.mergeAndOverwriteWith(v));\n    }\n\n    public CompletableFuture<Snapshot> deleteDirIfPresent(Path toDir, Snapshot in, Committer c, NetworkAccess network) {\n        return in.withWriter(base.owner(), base.writer(), network)\n                .thenCompose(v -> base.getUpdated(v, network))\n                .thenCompose(updated -> updated.getDescendentByPath(toDir.getParent().toString(), in, crypto.hasher, network))\n                .thenCompose(popt -> {\n                    if (popt.isEmpty())\n                        return Futures.of(in);\n                    return popt.get().getChild(getFilename(toDir), crypto.hasher, network)\n                            .thenCompose(copt -> {\n                                if (copt.isEmpty())\n                                    return Futures.of(in);\n                                return popt.get().removeChild(in, c, copt.get(), network, crypto.random, crypto.hasher);\n                            });\n                });\n    }\n\n    public CompletableFuture<Snapshot> rename(Path initial, Path after, Snapshot in, Committer committer, NetworkAccess network) {\n        if (! initial.getParent().equals(after.getParent()))\n            throw new IllegalStateException(\"Not a valid rename!\");\n        String initialFilename = getFilename(initial);\n        String newFilename = getFilename(after);\n        return getSharedWith(initial, in)\n                .thenCompose(sharees -> applyAndCommit(after, current ->\n                        current.add(Access.READ, newFilename, sharees.readAccess)\n                                .add(Access.WRITE, newFilename, sharees.writeAccess)\n                                .addLinks(newFilename, current.get(initialFilename).links)\n                                .clear(initialFilename), in, committer, network));\n    }\n\n    public CompletableFuture<Snapshot> addSecretLink(Path p, LinkProperties link,\n                                                     Snapshot in, Committer committer, NetworkAccess network) {\n        return applyAndCommit(p, current -> current.addLink(getFilename(p), link), in, committer, network);\n    }\n\n    public CompletableFuture<Snapshot> removeSecretLink(Path p, long label, Snapshot in, Committer committer, NetworkAccess network) {\n        return applyAndCommit(p, current -> current.removeLink(getFilename(p), label), in, committer, network);\n    }\n\n    public CompletableFuture<Snapshot> addSharedWith(Access access, Path p, Set<String> names, Snapshot in, Committer committer, NetworkAccess network) {\n        return applyAndCommit(p, current -> current.add(access, getFilename(p), names), in, committer, network);\n    }\n\n    public CompletableFuture<Snapshot> addAllSharedWith(Map<Path, SharedWithState> access, Snapshot in, Committer committer, NetworkAccess network) {\n        return Futures.reduceAll(access.entrySet(), in, (s, e) -> applyAndCommit(e.getKey(), existing -> existing.addAll(e.getValue()), s, committer, network), (a,  b) -> b);\n    }\n\n    public CompletableFuture<Snapshot> clearSharedWith(Path p, Snapshot in, Committer c, NetworkAccess network) {\n        return applyIfPresentAndCommit(p, current -> current.clear(getFilename(p)), in, c, network)\n                .thenCompose(v -> deleteDirIfPresent(p, v, c, network));\n    }\n\n    public CompletableFuture<Snapshot> removeSharedWith(Access access, Path p, Set<String> names, Snapshot in, Committer committer, NetworkAccess network) {\n        return applyAndCommit(p, current -> current.remove(access, getFilename(p), names), in, committer, network);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/SharedWithState.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** Holds the sharing state for all the children of a directory\n *\n */\npublic class SharedWithState implements Cborable {\n    private final Map<String, Set<String>> readShares;\n    private final Map<String, Set<String>> writeShares;\n    private final Map<String, Set<LinkProperties>> links;\n\n    public SharedWithState(Map<String, Set<String>> readShares,\n                           Map<String, Set<String>> writeShares,\n                           Map<String, Set<LinkProperties>> links) {\n        this.readShares = readShares;\n        this.writeShares = writeShares;\n        this.links = links;\n    }\n\n    public boolean isEmpty() {\n        return readShares.isEmpty() && writeShares.isEmpty();\n    }\n\n    public static SharedWithState empty() {\n        return new SharedWithState(new HashMap<>(), new HashMap<>(), new HashMap<>());\n    }\n\n    public Map<String, Set<String>> readShares() {\n        return readShares;\n    }\n\n    public Map<String, Set<LinkProperties>> links() {\n        return links;\n    }\n\n    public Map<String, Set<String>> writeShares() {\n        return writeShares;\n    }\n\n    @JsMethod\n    public FileSharedWithState get(String filename) {\n        return new FileSharedWithState(\n                readShares.getOrDefault(filename, Collections.emptySet()),\n                writeShares.getOrDefault(filename, Collections.emptySet()),\n                links.getOrDefault(filename, Collections.emptySet())\n        );\n    }\n\n    public Optional<SharedWithState> filter(String childName) {\n        if (! readShares.containsKey(childName) && ! writeShares.containsKey(childName) && ! links.containsKey(childName))\n            return Optional.empty();\n        Map<String, Set<String>> newReads = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : readShares.entrySet()) {\n            if (e.getKey().equals(childName))\n                newReads.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n        Map<String, Set<String>> newWrites = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : writeShares.entrySet()) {\n            if (e.getKey().equals(childName))\n                newWrites.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n\n        Map<String, Set<LinkProperties>> newLinks = new HashMap<>();\n        links.forEach((k, v) -> {\n            if (k.equals(childName))\n                newLinks.put(k, new HashSet<>(v));\n        });\n\n        return Optional.of(new SharedWithState(newReads, newWrites, newLinks));\n    }\n\n    public SharedWithState addLink(String filename, LinkProperties link) {\n        Map<String, Set<LinkProperties>> newLinks = new HashMap<>();\n        links.forEach((k, v) -> {\n            newLinks.put(k, new HashSet<>(v));\n        });\n\n        newLinks.putIfAbsent(filename, new HashSet<>());\n        newLinks.get(filename).removeIf(p -> p.label == link.label); // make sure we replace any old version\n        newLinks.get(filename).add(link);\n\n        return new SharedWithState(readShares, writeShares, newLinks);\n    }\n\n    public SharedWithState addLinks(String filename, Set<LinkProperties> newFileLinks) {\n        Map<String, Set<LinkProperties>> newLinks = new HashMap<>();\n        links.forEach((k, v) -> {\n            newLinks.put(k, new HashSet<>(v));\n        });\n\n        if (! newFileLinks.isEmpty()) {\n            newLinks.putIfAbsent(filename, new HashSet<>());\n            newLinks.get(filename).addAll(newFileLinks);\n        }\n        return new SharedWithState(readShares, writeShares, newLinks);\n    }\n\n    public SharedWithState removeLink(String filename, long label) {\n        Map<String, Set<LinkProperties>> newLinks = new HashMap<>();\n        links.forEach((k, v) -> {\n            newLinks.put(k, new HashSet<>(v));\n        });\n\n        Set<LinkProperties> val = newLinks.get(filename);\n        Set<LinkProperties> updated = val.stream().filter(lp -> lp.label != label).collect(Collectors.toSet());\n        if (updated.isEmpty())\n            newLinks.remove(filename);\n        else\n            newLinks.put(filename, updated);\n\n        return new SharedWithState(readShares, writeShares, newLinks);\n    }\n\n    public SharedWithState add(SharedWithCache.Access access, String filename, Set<String> names) {\n        if (names.isEmpty())\n            return this;\n        Map<String, Set<String>> newReads = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : readShares.entrySet()) {\n            newReads.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n        Map<String, Set<String>> newWrites = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : writeShares.entrySet()) {\n            newWrites.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n\n        if (access == SharedWithCache.Access.READ) {\n            newReads.putIfAbsent(filename, new HashSet<>());\n            newReads.get(filename).addAll(names);\n        } else if (access == SharedWithCache.Access.WRITE) {\n            newWrites.putIfAbsent(filename, new HashSet<>());\n            newWrites.get(filename).addAll(names);\n        }\n\n        return new SharedWithState(newReads, newWrites, links);\n    }\n\n    public SharedWithState addAll(SharedWithState other) {\n        Map<String, Set<String>> newReads = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : readShares.entrySet()) {\n            newReads.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n        for (Map.Entry<String, Set<String>> newRead : other.readShares.entrySet()) {\n            newReads.putIfAbsent(newRead.getKey(), new HashSet<>());\n            newReads.get(newRead.getKey()).addAll(newRead.getValue());\n        }\n\n        Map<String, Set<String>> newWrites = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : writeShares.entrySet()) {\n            newWrites.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n        for (Map.Entry<String, Set<String>> newWrite : other.writeShares.entrySet()) {\n            newWrites.putIfAbsent(newWrite.getKey(), new HashSet<>());\n            newWrites.get(newWrite.getKey()).addAll(newWrite.getValue());\n        }\n\n        Map<String, Set<LinkProperties>> newLinks = new HashMap<>();\n        links.forEach((k, v) -> {\n            newLinks.put(k, new HashSet<>(v));\n        });\n        for (Map.Entry<String, Set<LinkProperties>> newLink : other.links.entrySet()) {\n            newLinks.putIfAbsent(newLink.getKey(),  new HashSet<>());\n            newLinks.get(newLink.getKey()).addAll(newLink.getValue());\n        }\n\n        return new SharedWithState(newReads, newWrites, newLinks);\n    }\n\n    public SharedWithState remove(SharedWithCache.Access access, String filename, Set<String> names) {\n        Map<String, Set<String>> newReads = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : readShares.entrySet()) {\n            newReads.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n        Map<String, Set<String>> newWrites = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : writeShares.entrySet()) {\n            newWrites.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n\n        Set<String> val = access == SharedWithCache.Access.READ ? newReads.get(filename) : newWrites.get(filename);\n        if (val != null) {\n            val.removeAll(names);\n            if (val.isEmpty()) {\n                if (access == SharedWithCache.Access.READ) {\n                    newReads.remove(filename);\n                } else {\n                    newWrites.remove(filename);\n                }\n            }\n        }\n\n        return new SharedWithState(newReads, newWrites, links);\n    }\n\n    public SharedWithState clear(String filename) {\n        Map<String, Set<String>> newReads = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : readShares.entrySet()) {\n            newReads.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n        Map<String, Set<String>> newWrites = new HashMap<>();\n        for (Map.Entry<String, Set<String>> e : writeShares.entrySet()) {\n            newWrites.put(e.getKey(), new HashSet<>(e.getValue()));\n        }\n\n        newReads.remove(filename);\n        newWrites.remove(filename);\n\n        Map<String, Set<LinkProperties>> newLinks = new HashMap<>();\n        links.forEach((k, v) -> {\n            newLinks.put(k, new HashSet<>(v));\n        });\n\n        newLinks.remove(filename);\n\n        return new SharedWithState(newReads, newWrites, newLinks);\n    }\n\n    @JsMethod\n    public boolean isShared(String filename) {\n        return readShares.containsKey(filename) || writeShares.containsKey(filename);\n    }\n\n    @JsMethod\n    public boolean hasLink(String filename) {\n        return links.containsKey(filename);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        SortedMap<String, Cborable> readState = new TreeMap<>();\n        for (Map.Entry<String, Set<String>> e : readShares.entrySet()) {\n            readState.put(e.getKey(), new CborObject.CborList(e.getValue().stream().map(CborObject.CborString::new).collect(Collectors.toList())));\n        }\n        SortedMap<String, Cborable> writeState = new TreeMap<>();\n        for (Map.Entry<String, Set<String>> e : writeShares.entrySet()) {\n            writeState.put(e.getKey(), new CborObject.CborList(e.getValue().stream().map(CborObject.CborString::new).collect(Collectors.toList())));\n        }\n\n        state.put(\"r\", CborObject.CborMap.build(readState));\n        state.put(\"w\", CborObject.CborMap.build(writeState));\n\n        SortedMap<String, Cborable> linksState = new TreeMap<>();\n        links.forEach((k, v) -> {\n            linksState.put(k, new CborObject.CborList(v.stream().map(LinkProperties::toCbor).collect(Collectors.toList())));\n        });\n\n        state.put(\"l\", CborObject.CborMap.build(linksState));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static SharedWithState fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for SharedWithState!\");\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        CborObject.CborMap r = m.get(\"r\", c -> (CborObject.CborMap) c);\n        Function<Cborable, String> getString = c -> ((CborObject.CborString) c).value;\n        Map<String, Set<String>> readShares = r.toMap(\n                getString,\n                c -> new HashSet<>(((CborObject.CborList)c).map(getString)));\n\n        CborObject.CborMap w = m.get(\"w\", c -> (CborObject.CborMap) c);\n        Map<String, Set<String>> writehares = w.toMap(\n                getString,\n                c -> new HashSet<>(((CborObject.CborList)c).map(getString)));\n        if (! m.containsKey(\"l\"))\n            return new SharedWithState(readShares, writehares, Collections.emptyMap());\n\n        CborObject.CborMap l = m.get(\"l\", c -> (CborObject.CborMap) c);\n        Map<String, Set<LinkProperties>> links = l.toMap(\n                getString,\n                c -> new HashSet<>(((CborObject.CborList)c).map(LinkProperties::fromCbor)));\n\n        return new SharedWithState(readShares, writehares, links);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        SharedWithState that = (SharedWithState) o;\n        return Objects.equals(readShares, that.readShares) && Objects.equals(writeShares, that.writeShares) && Objects.equals(links, that.links);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(readShares, writeShares, links);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/Snapshot.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/** This class represents a snapshot of a group of signing subspaces.\n *\n */\npublic class Snapshot implements Cborable {\n\n    public final Map<PublicKeyHash, CommittedWriterData> versions;\n\n    public Snapshot(Map<PublicKeyHash, CommittedWriterData> versions) {\n        this.versions = Collections.unmodifiableMap(versions);\n    }\n\n    public Snapshot(PublicKeyHash writer, CommittedWriterData base) {\n        HashMap<PublicKeyHash, CommittedWriterData> state = new HashMap<>();\n        state.put(writer, base);\n        this.versions = Collections.unmodifiableMap(state);\n    }\n\n    public Snapshot merge(Snapshot other) {\n        HashMap<PublicKeyHash, CommittedWriterData> merge = new HashMap<>(versions);\n        for (Map.Entry<PublicKeyHash, CommittedWriterData> entry : other.versions.entrySet()) {\n            if (merge.containsKey(entry.getKey()) && ! merge.get(entry.getKey()).equals(other.versions.get(entry.getKey())))\n                throw new IllegalStateException(\"Conflicting merge of Snapshots!\");\n            merge.put(entry.getKey(), entry.getValue());\n        }\n        return new Snapshot(merge);\n    }\n\n    public Snapshot mergeAndOverwriteWith(Snapshot other) {\n        HashMap<PublicKeyHash, CommittedWriterData> merge = new HashMap<>(versions);\n        for (Map.Entry<PublicKeyHash, CommittedWriterData> entry : other.versions.entrySet()) {\n            merge.put(entry.getKey(), entry.getValue());\n        }\n        return new Snapshot(merge);\n    }\n\n    public Snapshot retainOnly(PublicKeyHash writer) {\n        return new Snapshot(writer, versions.get(writer));\n    }\n\n    public boolean contains(PublicKeyHash writer) {\n        return versions.containsKey(writer);\n    }\n\n    public CommittedWriterData get(PublicKeyHash writer) {\n        if (! versions.containsKey(writer))\n            throw new IllegalStateException(\"writer not present in snapshot!\");\n        return versions.get(writer);\n    }\n\n    public CommittedWriterData get(SigningPrivateKeyAndPublicHash writer) {\n        if (! versions.containsKey(writer.publicKeyHash))\n            throw new IllegalStateException(\"writer not present in snapshot!\");\n        return versions.get(writer.publicKeyHash);\n    }\n\n    public Snapshot remove(PublicKeyHash w) {\n        HashMap<PublicKeyHash, CommittedWriterData> removed = new HashMap<>(versions);\n        removed.remove(w);\n        return new Snapshot(removed);\n    }\n\n    public Snapshot withVersion(PublicKeyHash writer, CommittedWriterData version) {\n        HashMap<PublicKeyHash, CommittedWriterData> result = new HashMap<>(versions);\n        result.put(writer, version);\n        return new Snapshot(result);\n    }\n\n    public CompletableFuture<Snapshot> withWriter(PublicKeyHash owner, PublicKeyHash writer, NetworkAccess network) {\n        if (versions.containsKey(writer))\n            return CompletableFuture.completedFuture(this);\n        return network.synchronizer.getValue(owner, writer).thenApply(s -> s.merge(this));\n    }\n\n    public CompletableFuture<Snapshot> withWriters(PublicKeyHash owner, Set<PublicKeyHash> writers, NetworkAccess network) {\n        return Futures.reduceAll(writers, this,\n                (s, writer) -> s.withWriter(owner, writer, network), (a, b) -> b);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return new CborObject.CborList(versions.entrySet()\n                .stream()\n                .sorted((a, b) -> a.getKey().target.compareTo(b.getKey()))\n                .flatMap(e -> Stream.of(e.getKey().toCbor(), e.getValue().toCbor()))\n                .collect(Collectors.toList()));\n    }\n\n    public static Snapshot fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Invalid cbor for Snapshot!\");\n        CborObject.CborList list = (CborObject.CborList) cbor;\n        if (list.value.size() % 2 != 0)\n            throw new IllegalStateException(\"Invalid cbor list length for Snapshot!\");\n        HashMap<PublicKeyHash, CommittedWriterData> res = new HashMap<>();\n        for (int i=0; i < list.value.size()/2; i++)\n            res.put(list.get(2*i, PublicKeyHash::fromCbor), list.get(2*i + 1, CommittedWriterData::fromCbor));\n        return new Snapshot(res);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Snapshot snapshot = (Snapshot) o;\n        return Objects.equals(versions, snapshot.versions);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(versions);\n    }\n\n    @Override\n    public String toString() {\n        return versions.toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/SocialState.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.social.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\n@JsType\npublic class SocialState {\n    public static final String FRIENDS_GROUP_NAME = \"friends\";\n    public static final String FOLLOWERS_GROUP_NAME = \"followers\";\n\n    public final List<FollowRequestWithCipherText> pendingIncoming;\n    public final Set<String> pendingOutgoing;\n    public final Map<String, FileWrapper> followerRoots;\n    public final Set<FileWrapper> followingRoots;\n    public final Set<String> blocked;\n    public final Map<String, FriendAnnotation> friendAnnotations;\n    public final Map<String, String> uidToGroupName, groupNameToUid;\n\n    public SocialState(List<FollowRequestWithCipherText> pendingIncoming,\n                       Set<String> pendingOutgoing,\n                       Set<String> actualFollowers,\n                       Map<String, FileWrapper> followerRoots,\n                       Set<FileWrapper> followingRoots,\n                       Set<String> blocked,\n                       Map<String, FriendAnnotation> friendAnnotations,\n                       Map<String, String> uidToGroupName) {\n        this.pendingIncoming = pendingIncoming;\n        this.pendingOutgoing = pendingOutgoing;\n        Map<String, FileWrapper> actualFollowerRoots = followerRoots.entrySet()\n                .stream()\n                .filter(e -> actualFollowers.contains(e.getKey()))\n                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));\n        this.followerRoots = new TreeMap<>(actualFollowerRoots);\n        TreeSet<FileWrapper> sortedByName = new TreeSet<>((a, b) -> a.getName().compareTo(b.getName()));\n        sortedByName.addAll(followingRoots);\n        this.followingRoots = sortedByName;\n        this.blocked = blocked;\n        this.friendAnnotations = friendAnnotations;\n        this.uidToGroupName = uidToGroupName;\n        this.groupNameToUid = uidToGroupName.entrySet()\n                .stream()\n                .collect(Collectors.toMap(e -> e.getValue(), e -> e.getKey()));\n    }\n\n    public Set<String> getFollowers() {\n        return followerRoots.keySet();\n    }\n\n    public Set<String> getFollowing() {\n        return followingRoots.stream().map(f -> f.getFileProperties().name).collect(Collectors.toSet());\n    }\n\n    public Set<String> getFriends() {\n        HashSet<String> res = new HashSet<>(getFollowing());\n        res.retainAll(getFollowers());\n        return res;\n    }\n\n    public String getFriendsGroupUid() {\n        return groupNameToUid.get(FRIENDS_GROUP_NAME);\n    }\n\n    public String getFollowersGroupUid() {\n        return groupNameToUid.get(FOLLOWERS_GROUP_NAME);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/TofuKeyStore.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class TofuKeyStore implements Cborable {\n\n    private final Map<String, List<UserPublicKeyLink>> chains;\n    private final Map<String, List<UserPublicKeyLink>> expired;\n    private final Map<PublicKeyHash, String> reverseLookup = new HashMap<>();\n\n    public TofuKeyStore(Map<String, List<UserPublicKeyLink>> chains, Map<String, List<UserPublicKeyLink>> expired) {\n        this.chains = chains;\n        this.expired = expired;\n        updateReverseLookup();\n    }\n\n    public TofuKeyStore() {\n        this(new HashMap<>(), new HashMap<>());\n    }\n\n    public Optional<PublicKeyHash> getPublicKey(String username) {\n        List<UserPublicKeyLink> chain = chains.get(username);\n        if (chain == null) {\n            List<UserPublicKeyLink> expiredChain = expired.get(username);\n            if (expiredChain == null)\n                return Optional.empty();\n            return Optional.of(expiredChain.get(expiredChain.size() - 1).owner);\n        }\n        return Optional.of(chain.get(chain.size() - 1).owner);\n    }\n\n    public Optional<String> getUsername(PublicKeyHash signer) {\n        String name = reverseLookup.get(signer);\n        return Optional.ofNullable(name);\n    }\n\n    public List<UserPublicKeyLink> getChain(String username) {\n        return chains.getOrDefault(username, expired.getOrDefault(username, Collections.emptyList()));\n    }\n\n    /**\n     *\n     * @param username\n     * @param tail\n     * @param ipfs\n     * @return if there was any change\n     */\n    public CompletableFuture<Boolean> updateChain(String username, List<UserPublicKeyLink> tail, ContentAddressedStorage ipfs) {\n        return UserPublicKeyLink.validChain(tail, username, ipfs)\n                .thenCompose(valid -> {\n                    if (!valid)\n                        throw new IllegalStateException(\"Trying to update with invalid keychain!\");\n                    UserPublicKeyLink last = tail.get(tail.size() - 1);\n                    // we are allowing expired chains to be stored\n\n                    List<UserPublicKeyLink> existing = getChain(username);\n                    if (! existing.isEmpty() && existing.get(existing.size() - 1).equals(tail.get(tail.size() - 1)))\n                        return Futures.of(false);\n                    boolean isExpired =\n                            (existing.size() > 0 && UserPublicKeyLink.isExpiredClaim(existing.get(existing.size() - 1)));\n\n                    CompletableFuture<List<UserPublicKeyLink>> mergedFuture;\n                    if (isExpired) {\n                        List<UserPublicKeyLink> withoutExpiredClaim = existing.subList(0, existing.size() - 1);\n                        if (withoutExpiredClaim.size() == 0 &&\n                                !Arrays.equals(existing.get(0).owner.toCbor().toByteArray(), tail.get(0).owner.toCbor().toByteArray()))\n                            throw new IllegalStateException(\"Trying to update a username claim with a different key! \"\n                                    + ArrayOps.bytesToHex(existing.get(0).owner.toCbor().toByteArray()) + \" != \"\n                                    + ArrayOps.bytesToHex(tail.get(0).owner.toCbor().toByteArray()));\n                        expired.remove(username);\n                    }\n                    mergedFuture = UserPublicKeyLink.merge(existing, tail, ipfs);\n\n                    return mergedFuture.thenApply(merged -> {\n                        chains.put(username, merged);\n                        PublicKeyHash owner = last.owner;\n                        reverseLookup.put(owner, username);\n                        return true;\n                    });\n                });\n    }\n\n    private void updateReverseLookup() {\n        reverseLookup.clear();\n        reverseLookup.putAll(\n                Stream.concat(chains.entrySet().stream(), expired.entrySet().stream())\n                        .collect(Collectors.toMap(\n                                e -> e.getValue().get(e.getValue().size() - 1).owner,\n                                e -> e.getKey())));\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        Consumer<Map<String, List<UserPublicKeyLink>>> serialise = map -> map.forEach((name, chain) -> state.put(name,\n                new CborObject.CborList(chain.stream()\n                        .map(link -> link.toCbor())\n                        .collect(Collectors.toList()))));\n        serialise.accept(chains);\n        serialise.accept(expired);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static TofuKeyStore fromCbor(CborObject cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for Tofu key store: \" + cbor);\n\n        Map<String, List<UserPublicKeyLink>> chains = new HashMap<>();\n        Map<String, List<UserPublicKeyLink>> expired = new HashMap<>();\n        ((CborObject.CborMap) cbor).applyToAll((name, value) ->\n        {\n            if (value instanceof CborObject.CborList) {\n                List<UserPublicKeyLink> chain = ((CborObject.CborList) value).value.stream()\n                        .map(UserPublicKeyLink::fromCbor)\n                        .collect(Collectors.toList());\n                if (UserPublicKeyLink.isExpiredClaim(chain.get(chain.size() - 1)))\n                    expired.put(name, chain);\n                else\n                    chains.put(name, chain);\n            } else throw new IllegalStateException(\"Invalid value in Tofu key store map: \" + value);\n        });\n        return new TofuKeyStore(chains, expired);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/TrieNode.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n@JsType\npublic interface TrieNode {\n\n    CompletableFuture<Optional<FileWrapper>> getByPath(String path, Hasher hasher, NetworkAccess network);\n\n    @JsIgnore\n    CompletableFuture<Optional<FileWrapper>> getByPath(String path, Snapshot version, Hasher hasher, NetworkAccess network);\n\n    CompletableFuture<Set<FileWrapper>> getChildren(String path, Hasher hasher, NetworkAccess network);\n\n    @JsIgnore\n    CompletableFuture<Set<FileWrapper>> getChildren(String path, Hasher hasher, Snapshot version, NetworkAccess network);\n\n    Set<String> getChildNames();\n\n    TrieNode put(String path, EntryPoint e);\n\n    TrieNode putNode(String path, TrieNode t);\n\n    TrieNode removeEntry(String path);\n\n    Collection<TrieNode> getChildNodes();\n\n    TrieNode getChildNode(String name);\n\n    boolean isEmpty();\n\n    static String canonicalise(String path) {\n        path = path.replaceAll(\"\\\\\\\\\", \"/\");\n        return (path.startsWith(\"/\") ? path.substring(1) : path);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/TrieNodeImpl.java",
    "content": "package peergos.shared.user;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.Futures;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.Collectors;\n\n@JsType\npublic class TrieNodeImpl implements TrieNode {\n\tprivate static final Logger LOG = Logger.getLogger(TrieNodeImpl.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n    private final Map<String, TrieNode> children;\n    private final Optional<EntryPoint> value;\n\n    @JsConstructor\n    private TrieNodeImpl(Map<String, TrieNode> children, Optional<EntryPoint> value) {\n        this.children = Collections.unmodifiableMap(children);\n        this.value = value;\n    }\n\n    private CompletableFuture<Optional<FileWrapper>> getAnyValidParentOfAChild(Hasher hasher, NetworkAccess network) {\n        return Futures.findFirst(children.values(),\n                        n -> n.getByPath(\"\", hasher, network)\n                                .thenCompose(child -> child.map(c -> c\n                                        .retrieveParent(network)\n                                        .thenApply(opt -> opt.map(f -> f.withTrieNode(this))))\n                                        .orElseGet(() -> Futures.of(Optional.empty())))\n                                .exceptionally(t -> Futures.logAndReturn(t, Optional.empty())));\n    }\n\n    @Override\n    public CompletableFuture<Optional<FileWrapper>> getByPath(String path, Hasher hasher, NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n//        LOG.info(\"GetByPath: \" + path);\n        String finalPath = TrieNode.canonicalise(path);\n        if (finalPath.length() == 0) {\n            if (! value.isPresent()) { // find a valid child entry and traverse parent links\n                return getAnyValidParentOfAChild(hasher, network);\n            }\n            return network.retrieveEntryPoint(value.get())\n                    .thenCompose(opt -> {\n                        if (opt.isPresent()) {\n                            if (opt.get().isWritable())\n                                return opt.get().getAnyLinkPointer(network)\n                                        .thenApply(linkOpt -> opt.map(f -> f.withLinkPointer(linkOpt)));\n                            // there may be children which are writable directly if this dir is read only\n                            return Futures.of(opt.map(f -> f.withTrieNode(this)));\n                        }\n                        return getAnyValidParentOfAChild(hasher, network);\n                    });\n        }\n        String[] elements = finalPath.split(\"/\");\n        // There may be an entry point further down the tree, but it will have <= permission than this one, unless this is read only\n        if (value.isPresent() && (value.get().pointer.isWritable() || !children.containsKey(elements[0])))\n            return network.retrieveEntryPoint(value.get())\n                    .thenCompose(dir -> dir.get().getDescendentByPath(finalPath, hasher, network));\n        if (!children.containsKey(elements[0]))\n            return CompletableFuture.completedFuture(Optional.empty());\n        return children.get(elements[0]).getByPath(finalPath.substring(elements[0].length()), hasher, network);\n    }\n\n    private CompletableFuture<Set<FileWrapper>> indirectlyRetrieveChildren(Hasher hasher, NetworkAccess network) {\n        Set<CompletableFuture<Optional<FileWrapper>>> kids = children.values().stream()\n                .map(t -> t.getByPath(\"\", hasher, network)).collect(Collectors.toSet());\n        return Futures.combineAll(kids)\n                .thenApply(set -> set.stream()\n                        .filter(opt -> opt.isPresent())\n                        .map(opt -> opt.get())\n                        .collect(Collectors.toSet()));\n    }\n\n    @Override\n    @JsIgnore\n    public CompletableFuture<Optional<FileWrapper>> getByPath(String path, Snapshot version, Hasher hasher, NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        LOG.info(\"GetByPath: \" + path);\n        String finalPath = TrieNode.canonicalise(path);\n        if (finalPath.length() == 0) {\n            if (! value.isPresent()) { // find a valid child entry and traverse parent links\n                return getAnyValidParentOfAChild(hasher, network);\n            }\n            return network.getFile(value.get(), version)\n                    .thenCompose(opt -> {\n                        if (opt.isPresent()) {\n                            if (opt.get().isWritable())\n                                return opt.get().getAnyLinkPointer(network)\n                                        .thenApply(linkOpt -> opt.map(f -> f.withLinkPointer(linkOpt)));\n                            // there may be children which are writable directly if this dir is read only\n                            return Futures.of(opt.map(f -> f.withTrieNode(this)));\n                        }\n                        return getAnyValidParentOfAChild(hasher, network);\n                    });\n        }\n        String[] elements = finalPath.split(\"/\");\n        // There may be an entry point further down the tree, but it will have <= permission than this one, unless this is read only\n        if (value.isPresent() && (value.get().pointer.isWritable() || !children.containsKey(elements[0])))\n            return network.getFile(version, value.get().pointer, Optional.empty(), value.get().ownerName)\n                    .thenCompose(dir -> dir.get().getDescendentByPath(finalPath, hasher, network));\n        if (!children.containsKey(elements[0]))\n            return CompletableFuture.completedFuture(Optional.empty());\n        return children.get(elements[0]).getByPath(finalPath.substring(elements[0].length()), version, hasher, network);\n    }\n\n    @Override\n    public CompletableFuture<Set<FileWrapper>> getChildren(String path, Hasher hasher, NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        String trimmedPath = TrieNode.canonicalise(path);\n        if (trimmedPath.length() == 0) {\n            if (! value.isPresent()) { // find a child entry and traverse parent links\n                return indirectlyRetrieveChildren(hasher, network);\n            }\n            return network.retrieveEntryPoint(value.get())\n                    .thenCompose(dir -> {\n                        if (dir.isPresent())\n                            return dir.get().getChildren(hasher, network)\n                                    .thenCompose(kids -> {\n                                        if (dir.get().isWritable())\n                                            return Futures.of(kids);\n                                        Set<CompletableFuture<FileWrapper>> futures = kids.stream()\n                                                .map(child -> children.containsKey(child.getName()) ?\n                                                        children.get(child.getName())\n                                                                .getByPath(\"\", hasher, network)\n                                                                .thenApply(fopt -> fopt.get()) :\n                                                        Futures.of(child))\n                                                .collect(Collectors.toSet());\n                                        return Futures.combineAll(futures);\n                                    });\n                        return indirectlyRetrieveChildren(hasher, network);\n                    });\n        }\n        String[] elements = trimmedPath.split(\"/\");\n        if (!children.containsKey(elements[0]))\n            return network.retrieveEntryPoint(value.get())\n                    .thenCompose(dir -> dir.get().getDescendentByPath(trimmedPath, hasher, network)\n                            .thenCompose(parent -> parent.map(p -> p.getChildren(hasher, network))\n                                    .orElseGet(() -> Futures.of(Collections.emptySet()))));\n        return children.get(elements[0]).getChildren(trimmedPath.substring(elements[0].length()), hasher, network);\n    }\n\n    @Override\n    @JsIgnore\n    public CompletableFuture<Set<FileWrapper>> getChildren(String path, Hasher hasher, Snapshot version, NetworkAccess network) {\n        FileProperties.ensureValidPath(path);\n        String trimmedPath = TrieNode.canonicalise(path);\n        if (trimmedPath.length() == 0) {\n            if (! value.isPresent()) { // find a child entry and traverse parent links\n                return indirectlyRetrieveChildren(hasher, network);\n            }\n            return network.getFile(value.get(), version)\n                    .thenCompose(dir -> {\n                        if (dir.isPresent())\n                            return dir.get().getChildren(version, hasher, network, true)\n                                    .thenCompose(kids -> {\n                                        if (dir.get().isWritable())\n                                            return Futures.of(kids);\n                                        Set<CompletableFuture<FileWrapper>> futures = kids.stream()\n                                                .map(child -> children.containsKey(child.getName()) ?\n                                                        children.get(child.getName())\n                                                                .getByPath(\"\", version, hasher, network)\n                                                                .thenApply(fopt -> fopt.get()) :\n                                                        Futures.of(child))\n                                                .collect(Collectors.toSet());\n                                        return Futures.combineAll(futures);\n                                    });\n                        return indirectlyRetrieveChildren(hasher, network);\n                    });\n        }\n        String[] elements = trimmedPath.split(\"/\");\n        if (!children.containsKey(elements[0]))\n            return network.getFile(value.get(), version)\n                    .thenCompose(dir -> dir.get().getDescendentByPath(trimmedPath, hasher, network)\n                            .thenCompose(parent -> parent.map(p -> p.getChildren(version, hasher, network, true))\n                                    .orElseGet(() -> Futures.of(Collections.emptySet()))));\n        return children.get(elements[0]).getChildren(trimmedPath.substring(elements[0].length()), hasher, version, network);\n    }\n\n    @Override\n    public Set<String> getChildNames() {\n        return children.keySet();\n    }\n\n    @Override\n    public TrieNodeImpl put(String path, EntryPoint e) {\n        FileProperties.ensureValidPath(path);\n        path = TrieNode.canonicalise(path);\n        if (path.length() == 0) {\n            return new TrieNodeImpl(children, Optional.of(e));\n        }\n        String[] elements = path.split(\"/\");\n        TrieNode existing = children.getOrDefault(elements[0], TrieNodeImpl.empty());\n        TrieNode newChild = existing.put(path.substring(elements[0].length()), e);\n\n        HashMap<String, TrieNode> newChildren = new HashMap<>(children);\n        newChildren.put(elements[0], newChild);\n        return new TrieNodeImpl(newChildren, value);\n    }\n\n    @Override\n    public TrieNode putNode(String path, TrieNode t) {\n        FileProperties.ensureValidPath(path);\n        path = TrieNode.canonicalise(path);\n        if (path.length() == 0) {\n            return t;\n        }\n        String[] elements = path.split(\"/\");\n        TrieNode existing = children.getOrDefault(elements[0], TrieNodeImpl.empty());\n        String subPath = path.substring(elements[0].length());\n        TrieNode newChild = subPath.isEmpty() ? t : existing.putNode(subPath, t);\n\n        HashMap<String, TrieNode> newChildren = new HashMap<>(children);\n        newChildren.put(elements[0], newChild);\n        return new TrieNodeImpl(newChildren, value);\n    }\n\n    @Override\n    public TrieNodeImpl removeEntry(String path) {\n        LOG.info(\"Entrie.rm(\" + path + \")\");\n        path = TrieNode.canonicalise(path);\n        if (path.length() == 0) {\n            return new TrieNodeImpl(children, Optional.empty());\n        }\n        String[] elements = path.split(\"/\");\n        TrieNode existing = children.getOrDefault(elements[0], TrieNodeImpl.empty());\n        TrieNode newChild = existing.removeEntry(path.substring(elements[0].length()));\n\n        HashMap<String, TrieNode> newChildren = new HashMap<>(children);\n        if (newChild.isEmpty())\n            newChildren.remove(elements[0]);\n        else\n            newChildren.put(elements[0], newChild);\n        return new TrieNodeImpl(newChildren, value);\n    }\n\n    @Override\n    public Collection<TrieNode> getChildNodes() {\n        return children.values();\n    }\n\n    @Override\n    public TrieNode getChildNode(String name) {\n        return children.get(name);\n    }\n\n    public static TrieNodeImpl empty() {\n        return new TrieNodeImpl(Collections.emptyMap(), Optional.empty());\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return children.size() == 0 && !value.isPresent();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/UserContext.java",
    "content": "package peergos.shared.user;\n\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.util.logging.*;\n\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMPublicKey;\nimport peergos.shared.fingerprint.*;\nimport peergos.shared.inode.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.login.mfa.*;\nimport peergos.shared.resolution.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.storage.controller.InstanceAdmin;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\nimport jsinterop.annotations.*;\n\n/**\n * The UserContext class represents a logged in user, or a retrieved secret link and the resulting view of the global\n * filesystem.\n */\npublic class UserContext {\n    private static final Logger LOG = Logger.getGlobal();\n\n    public static final String SHARED_DIR_NAME = \"shared\";\n    public static final String POSTS_DIR_NAME = \".posts\";\n    public static final String GROUPS_FILENAME = \".groups.cbor\"; // no clash possible with usernames possible because of dot\n    public static final String FEED_DIR_NAME = \".feed\";\n    public static final String TRANSACTIONS_DIR_NAME = \".transactions\";\n    public static final String FRIEND_ANNOTATIONS_FILE_NAME = \".annotations\";\n\n    public static final String ENTRY_POINTS_FROM_FRIENDS_FILENAME = \".from-friends.cborstream\";\n    public static final String ENTRY_POINTS_FROM_FRIENDS_GROUPS_FILENAME = \".groups-from-friends.cborstream\";\n    public static final String SOCIAL_STATE_FILENAME = \".social-state.cbor\";\n    public static final String BLOCKED_USERNAMES_FILE = \".blocked-usernames.txt\";\n\n    @JsProperty\n    public final String username;\n    public final SigningPrivateKeyAndPublicHash signer;\n    private final BoxingKeyPair boxer;\n    @JsProperty\n    private final SymmetricKey rootKey;\n\n    private final WriteSynchronizer writeSynchronizer;\n    private final TransactionService transactions;\n    private final IncomingCapCache capCache;\n    private final Optional<BatWithId> mirrorBat;\n    public final SharedWithCache sharedWithCache;\n\n    // The root of the global filesystem as viewed by this context\n    @JsProperty\n    public TrieNode entrie; // ba dum che!\n\n    // Contact external world\n    @JsProperty\n    public final NetworkAccess network;\n\n    // In process only\n    @JsProperty\n    public final Crypto crypto;\n\n    public UserContext(String username,\n                       SigningPrivateKeyAndPublicHash signer,\n                       BoxingKeyPair boxer,\n                       SymmetricKey rootKey,\n                       NetworkAccess network,\n                       Crypto crypto,\n                       CommittedWriterData userData,\n                       TrieNode entrie,\n                       TransactionService transactions,\n                       IncomingCapCache capCache,\n                       SharedWithCache sharedWithCache,\n                       Optional<BatWithId> mirrorBat) {\n        this.username = username;\n        this.signer = signer;\n        this.boxer = boxer;\n        this.rootKey = rootKey;\n        this.network = network;\n        this.crypto = crypto;\n        this.entrie = entrie;\n        this.writeSynchronizer = network.synchronizer;\n        if (signer != null) {\n            writeSynchronizer.put(signer.publicKeyHash, signer.publicKeyHash, userData);\n        }\n        this.transactions = transactions;\n        this.capCache = capCache;\n        this.sharedWithCache = sharedWithCache;\n        this.mirrorBat = mirrorBat;\n    }\n\n    private static CompletableFuture<TransactionService> buildTransactionService(TrieNode root,\n                                                                                 String username,\n                                                                                 NetworkAccess network,\n                                                                                 Crypto crypto) {\n        return root.getByPath(username, crypto.hasher, network)\n                .thenApply(Optional::get)\n                .thenCompose(home -> home.getChild(TRANSACTIONS_DIR_NAME, crypto.hasher, network))\n                .thenApply(Optional::get)\n                .thenApply(txnDir -> new TransactionServiceImpl(network, crypto, txnDir));\n    }\n\n    private static CompletableFuture<IncomingCapCache> buildCapCache(TrieNode root,\n                                                                     String username,\n                                                                     Optional<BatId> mirrorBatId,\n                                                                     NetworkAccess network,\n                                                                     Crypto crypto) {\n        return root.getByPath(username, crypto.hasher, network)\n                .thenApply(Optional::get)\n                .thenCompose(home -> home.getOrMkdirs(PathUtil.get(CapabilityStore.CAPABILITY_CACHE_DIR), network, true, mirrorBatId, crypto))\n                .thenCompose(cacheRoot -> IncomingCapCache.build(cacheRoot, mirrorBatId, crypto, network));\n    }\n\n    @JsMethod\n    public TransactionService getTransactionService() {\n        return transactions;\n    }\n\n    public static CompletableFuture<UserContext> signIn(String username,\n                                                        String password,\n                                                        Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa,\n                                                        NetworkAccess network,\n                                                        Crypto crypto) {\n        return signIn(username, password, mfa, false, false, network, crypto, t -> {});\n    }\n\n    @JsMethod\n    public static CompletableFuture<UserContext> signIn(String username,\n                                                        String password,\n                                                        Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa,\n                                                        boolean cacheMfaLoginData,\n                                                        boolean forceProxy,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        Consumer<String> progressCallback) {\n        return Futures.asyncExceptionally(() -> getWriterDataCbor(network, username),\n                e ->  {\n                    if (e.getMessage().contains(\"hash not present\"))\n                        return Futures.errored(new IllegalStateException(\"User has been deleted. Did you mean a different username?\"));\n                    else if (e.getMessage().contains(\"No public-key for user\"))\n                        return Futures.errored(new IllegalStateException(\"Unknown username. Did you enter it correctly?\"));\n                    else\n                        return Futures.errored(e);\n                }).thenCompose(pair -> {\n            SecretGenerationAlgorithm algorithm = WriterData.fromCbor(pair.right).generationAlgorithm\n                    .orElseThrow(() -> new IllegalStateException(\"No login algorithm specified in user data!\"));\n            progressCallback.accept(\"Generating keys\");\n            return UserUtil.generateUser(username, password, crypto, algorithm)\n                    .thenCompose(userWithRoot -> {\n                        progressCallback.accept(\"Logging in\");\n                        return login(username, userWithRoot, mfa, cacheMfaLoginData, forceProxy, pair, network, crypto, progressCallback);\n                    });\n                }).thenCompose(ctx -> ctx.isPostQuantum() ? Futures.of(ctx) : ctx.ensurePostQuantum(password, mfa, progressCallback)\n                        .thenCompose(updated -> updated ?\n                                signIn(username, password, mfa, false, forceProxy, network, crypto, t -> {}):\n                                Futures.of(ctx)))\n                .exceptionally(Futures::logAndThrow);\n    }\n\n    public static CompletableFuture<UserContext> signIn(String username,\n                                                        UserWithRoot userWithRoot,\n                                                        Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        Consumer<String> progressCallback) {\n        progressCallback.accept(\"Logging in\");\n        return getWriterDataCbor(network, username)\n                .thenCompose(pair -> login(username, userWithRoot, mfa, false, false, pair, network, crypto, progressCallback))\n                .exceptionally(Futures::logAndThrow);\n    }\n\n    @JsMethod\n    public static CompletableFuture<UserContext> restoreContext(String username,\n                                                                SymmetricKey loginRoot,\n                                                                UserStaticData entryData,\n                                                                NetworkAccess network,\n                                                                Crypto crypto,\n                                                                Consumer<String> progressCallback) {\n        progressCallback.accept(\"Logging in\");\n        return getWriterDataCbor(network, username)\n                .thenCompose(pair -> {\n                    WriterData userData = WriterData.fromCbor(pair.right);\n                    boolean legacyAccount = userData.staticData.isPresent();\n                    if (legacyAccount)\n                        throw new IllegalStateException(\"Legacy accounts can't stay logged in. Please change your password to upgrade your account.\");\n\n                    UserStaticData.EntryPoints staticData = entryData.getData(loginRoot);\n                    SigningPrivateKeyAndPublicHash signer =\n                            new SigningPrivateKeyAndPublicHash(userData.controller, staticData.identity.get().secretSigningKey);\n                    BoxingKeyPair boxer = staticData.boxer.orElseThrow(() -> new IllegalStateException(\"No social keypair present in login data!\"));\n                    return login(username, userData, staticData, signer, boxer, loginRoot, pair, network, crypto, progressCallback);\n                })\n                .exceptionally(Futures::logAndThrow);\n    }\n\n    private static CompletableFuture<Pair<UserStaticData, UserStaticData.EntryPoints>> getLoginData(String username,\n                                                                                                    PublicSigningKey loginPub,\n                                                                                                    SecretSigningKey loginSecret,\n                                                                                                    SymmetricKey loginRoot,\n                                                                                                    Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa,\n                                                                                                    boolean cacheMfaLoginData,\n                                                                                                    boolean forceProxy,\n                                                                                                    NetworkAccess network) {\n        return TimeLimitedClient.signNow(loginSecret)\n                .thenCompose(signedTime -> network.account.getLoginData(username, loginPub, signedTime, Optional.empty(), cacheMfaLoginData, forceProxy, false))\n                .thenCompose(res -> {\n                    if (res.isA())\n                        return Futures.of(res.a());\n                    MultiFactorAuthRequest authReq = res.b();\n                    return mfa.apply(authReq)\n                            .thenCompose(authResp -> TimeLimitedClient.signNow(loginSecret)\n                                    .thenCompose(signedTime -> network.account.getLoginData(username, loginPub, signedTime, Optional.of(authResp), cacheMfaLoginData, forceProxy, false)))\n                            .thenApply(login -> {\n                                if (login.isB())\n                                    throw new IllegalStateException(\"Server rejected second factor auth\");\n                                return login.a();\n                            });\n                }).thenCompose(entryData -> {\n                    try {\n                        return Futures.of(new Pair<>(entryData, entryData.getData(loginRoot)));\n                    } catch (Exception e) {\n                        // try to get entry data avoiding the cache\n                        return TimeLimitedClient.signNow(loginSecret)\n                                .thenCompose(signedTime -> network.account.getLoginData(username, loginPub,\n                                        signedTime, Optional.empty(), cacheMfaLoginData, forceProxy, true))\n                                .thenApply(entryData2 -> {\n                                    try {\n                                        return new Pair<>(entryData2.a(), entryData2.a().getData(loginRoot));\n                                    } catch (Exception f) {\n                                        throw new IllegalStateException(\"Incorrect username or password\");\n                                    }\n                                });\n                    }\n                });\n    }\n\n    private static CompletableFuture<UserContext> login(String username,\n                                                        UserWithRoot generatedCredentials,\n                                                        Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa,\n                                                        boolean cacheMfaLoginData,\n                                                        boolean forceProxy,\n                                                        Pair<PointerUpdate, CborObject> pair,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        Consumer<String> progressCallback) {\n        try {\n            WriterData userData = WriterData.fromCbor(pair.right);\n            boolean legacyAccount = userData.staticData.isPresent();\n            SymmetricKey loginRoot = generatedCredentials.getRoot();\n            PublicSigningKey loginPub = generatedCredentials.getUser().publicSigningKey;\n            SecretSigningKey loginSecret = generatedCredentials.getUser().secretSigningKey;\n            return (legacyAccount ?\n                    Futures.of(userData.staticData.get().getData(loginRoot)) :\n                    getLoginData(username, loginPub, loginSecret, loginRoot, mfa, cacheMfaLoginData, forceProxy, network)\n                            .thenApply(p -> p.right)).thenCompose(staticData -> {\n\n                // Use generated signer for legacy logins, or get from UserStaticData for newer logins\n                SigningPrivateKeyAndPublicHash signer =\n                        new SigningPrivateKeyAndPublicHash(userData.controller,\n                                legacyAccount ? loginSecret : staticData.identity.get().secretSigningKey);\n                BoxingKeyPair boxer = staticData.boxer.orElse(generatedCredentials.getBoxingPair());\n                return login(username, userData, staticData, signer, boxer, loginRoot, pair, network, crypto, progressCallback);\n            });\n        } catch (Throwable t) {\n            throw new IllegalStateException(\"Incorrect password\");\n        }\n    }\n\n    private static CompletableFuture<UserContext> login(String username,\n                                                        WriterData userData,\n                                                        UserStaticData.EntryPoints staticData,\n                                                        SigningPrivateKeyAndPublicHash signer,\n                                                        BoxingKeyPair boxer,\n                                                        SymmetricKey login,\n                                                        Pair<PointerUpdate, CborObject> pair,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        Consumer<String> progressCallback) {\n        try {\n            return createOurFileTreeOnly(username, staticData, userData, network)\n                    .thenCompose(root -> TofuCoreNode.load(username, root, network, crypto)\n                            .thenCompose(tofuCorenode -> {\n                                return buildTransactionService(root, username, network, crypto)\n                                        .thenCompose(transactions -> getMirrorBat(username, signer, login, network)\n                                                .thenCompose(mirrorBatId -> buildCapCache(root, username, mirrorBatId.map(BatWithId::id), network, crypto)\n                                                        .thenCompose(capCache -> SharedWithCache.initOrBuild(root, username, network, crypto)\n                                                                .thenCompose(sharedWith -> {\n                                                                    UserContext result = new UserContext(username,\n                                                                            signer,\n                                                                            boxer,\n                                                                            login,\n                                                                            network.withCorenode(tofuCorenode),\n                                                                            crypto,\n                                                                            new CommittedWriterData(pair.left.updated, userData, pair.left.sequence),\n                                                                            root,\n                                                                            transactions,\n                                                                            capCache,\n                                                                            sharedWith,\n                                                                            mirrorBatId);\n\n                                                                    return result.init(progressCallback)\n                                                                            .exceptionally(Futures::logAndThrow);\n                                                                }))));\n                            }));\n        } catch (Throwable t) {\n            throw new IllegalStateException(\"Incorrect password\");\n        }\n    }\n\n    @JsMethod\n    public static CompletableFuture<UserContext> signUp(String username,\n                                                        String password,\n                                                        String token,\n                                                        Optional<String> existingIdentity,\n                                                        Consumer<String> identityStorer,\n                                                        Optional<Function<PaymentProperties, CompletableFuture<Plan>>> addCard,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        Consumer<String> progressCallback) {\n        // set claim expiry to two months from now\n        LocalDate expiry = LocalDate.now().plusMonths(2);\n        SecretGenerationAlgorithm algorithm = SecretGenerationAlgorithm.getDefault(crypto.random);\n        Optional<SigningKeyPair> existingKeyPair = existingIdentity.map(ArrayOps::hexToBytes)\n                .map(CborObject::fromByteArray)\n                .map(SigningKeyPair::fromCbor);\n        return signUpGeneral(username, password, token, existingKeyPair, identityStorer,\n                addCard.map(f -> (p, i) -> f.apply(p).thenCompose(s -> signSpaceRequest(username, i, s.desiredQuota, s.annual))),\n                expiry, network, crypto, algorithm, progressCallback);\n    }\n\n    /** Ensure we have signed the current peerid for our home server, verifying any key rotations\n     *\n     * @return whether we updated anything\n     */\n    @JsMethod\n    public CompletableFuture<Boolean> ensureCurrentHost() {\n        return network.coreNode.getChain(username)\n                .thenCompose(chain -> network.dhtClient.ids()\n                        .thenCompose(ids -> {\n                            Multihash pkiCurrent = chain.get(chain.size() - 1).claim.storageProviders.get(0).bareMultihash();\n                            List<Multihash> peerIds = ids.stream().map(c -> c.bareMultihash()).collect(Collectors.toList());\n                            boolean onHome = peerIds.contains(pkiCurrent) || pkiCurrent.equals(Proxy.ZERO);\n                            Multihash latest = peerIds.get(peerIds.size() - 1);\n                            if (! onHome || latest.equals(pkiCurrent))\n                                return Futures.of(false);\n                            if (pkiCurrent.equals(Proxy.ZERO))\n                                return updateHostInPki(username, signer, LocalDate.now().plusMonths(2), latest, crypto.hasher, network);\n                            // Need to check peerid chain and update our pki entry\n                            return getAndVerifyServerIdChain(pkiCurrent, latest)\n                                    .thenCompose(x -> updateHostInPki(username, signer, LocalDate.now().plusMonths(2), latest, crypto.hasher, network));\n                        }));\n    }\n\n    @JsMethod\n    public boolean isPostQuantum() {\n        return boxer.publicBoxingKey instanceof HybridCurve25519MLKEMPublicKey;\n    }\n\n    /**\n     *\n     * @return if we changed our boxing key\n     */\n    @JsMethod\n    public CompletableFuture<Boolean> ensurePostQuantum(String password,\n                                                        Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa,\n                                                        Consumer<String> progressCallback) {\n        if (boxer.publicBoxingKey instanceof HybridCurve25519MLKEMPublicKey)\n            return Futures.of(false);\n        progressCallback.accept(\"Upgrading account to post-quantum encryption..\");\n        return getFollowRequests().thenCompose(followReqs -> {\n            if (! followReqs.isEmpty())\n                return Futures.of(false);\n            return WriterData.getWriterData(this.signer.publicKeyHash, this.signer.publicKeyHash, network.mutable, network.dhtClient).thenCompose(cwd -> {\n                SecretGenerationAlgorithm algorithm = cwd.props.flatMap(wd -> wd.generationAlgorithm)\n                        .orElseThrow(() -> new IllegalStateException(\"No login algorithm specified in user data!\"));\n\n                return UserUtil.generateUser(username, password, crypto, algorithm)\n                        .thenCompose(generatedCredentials -> {\n                            WriterData userData = cwd.props.get();\n                            boolean legacyAccount = userData.staticData.isPresent();\n                            SymmetricKey loginRoot = generatedCredentials.getRoot();\n                            PublicSigningKey loginPub = generatedCredentials.getUser().publicSigningKey;\n                            SecretSigningKey loginSecret = generatedCredentials.getUser().secretSigningKey;\n                            return (legacyAccount ?\n                                    Futures.of(new Pair<>(userData.staticData.get(), userData.staticData.get())) :\n                                    getLoginData(username, loginPub, loginSecret, loginRoot, mfa, false, false, network))\n                                    .thenCompose(entryData -> BoxingKeyPair.randomHybrid(crypto)\n                                            .thenCompose(newBoxer -> writeSynchronizer.applyComplexUpdate(signer.publicKeyHash, signer,\n                                                    (s, c) -> IpfsTransaction.call(signer.publicKeyHash,\n                                                            tid -> updateBoxerAndCommit(s, username, newBoxer, entryData.left,\n                                                                    loginPub, signer, loginRoot, c, crypto, network, tid), network.dhtClient)))\n                                            .thenApply(x -> true)\n                                    );\n                        });\n            });\n        });\n    }\n\n    public CompletableFuture<Boolean> getAndVerifyServerIdChain(Multihash from, Multihash to) {\n        return network.dhtClient.getIpnsEntry(from)\n                .thenCompose(res -> validateResolutionRecord(res, from))\n                .thenCompose(record -> {\n                    if (record.host.isEmpty() || ! record.moved)\n                        return Futures.errored(new IllegalStateException(\"Invalid server id update!\"));\n                    if (record.host.get().equals(to))\n                        return Futures.of(true);\n                    return getAndVerifyServerIdChain(record.host.get(), to);\n                });\n    }\n\n    public CompletableFuture<ResolutionRecord> validateResolutionRecord(IpnsEntry signedRecord, Multihash signer) {\n        return signedRecord.getValue(signer, crypto);\n    }\n\n    @JsMethod\n    public CompletableFuture<String> getVersion() {\n        return network.instanceAdmin.getVersionInfo()\n                .thenApply(InstanceAdmin.VersionInfo::toString);\n    }\n\n    private static CompletableFuture<byte[]> signSpaceRequest(String username, SigningPrivateKeyAndPublicHash identity, long desiredQuota, boolean annual) {\n        SpaceUsage.SpaceRequest req = new SpaceUsage.SpaceRequest(username, desiredQuota, annual, System.currentTimeMillis(), Optional.empty());\n        return identity.secret.signMessage(req.serialize());\n    }\n\n    public static CompletableFuture<UserContext> signUp(String username,\n                                                        String password,\n                                                        String token,\n                                                        NetworkAccess network,\n                                                        Crypto crypto) {\n        // set claim expiry to two months from now\n        LocalDate expiry = LocalDate.now().plusMonths(2);\n        SecretGenerationAlgorithm algorithm = SecretGenerationAlgorithm.getDefault(crypto.random);\n        return signUpGeneral(username, password, token, Optional.empty(), id -> {}, Optional.empty(), expiry,\n                network, crypto, algorithm, t -> {});\n    }\n\n    public static CompletableFuture<UserContext> signUpGeneral(String username,\n                                                               String password,\n                                                               String token,\n                                                               Optional<SigningKeyPair> existingIdentity,\n                                                               Consumer<String> tmpIdentityStore,\n                                                               Optional<BiFunction<PaymentProperties, SigningPrivateKeyAndPublicHash, CompletableFuture<byte[]>>> addCard,\n                                                               LocalDate expiry,\n                                                               NetworkAccess initialNetwork,\n                                                               Crypto crypto,\n                                                               SecretGenerationAlgorithm algorithm,\n                                                               Consumer<String> progressCallback) {\n        // Using a local OpLog that doesn't commit anything allows us to group all the updates into a single atomic call\n        OpLog opLog = new OpLog(new ArrayList<>(), null, Optional.empty());\n        BufferedNetworkAccess network = NetworkAccess.nonCommittingForSignup(opLog, opLog, opLog, opLog, crypto.hasher);\n        network.synchronizer.setFlusher((o, v, w) -> Futures.of(v)); // disable final commit\n        progressCallback.accept(\"Generating keys\");\n        return initialNetwork.coreNode.getChain(username)\n                .thenApply(existing -> {\n                    if (existing.size() > 0)\n                        throw new IllegalStateException(\"User already exists!\");\n                    return true;\n                })\n                .thenCompose(x -> UserUtil.generateUser(username, password, crypto, algorithm))\n                .thenCompose(userWithRoot -> {\n                    PublicSigningKey loginPublicKey = userWithRoot.getUser().publicSigningKey;\n\n                    boolean isLegacy = username.equals(\"peergos\") || algorithm.generateBoxerAndIdentity();\n                    SigningKeyPair identityPair = isLegacy ?\n                            userWithRoot.getUser() :\n                            existingIdentity.orElseGet(() -> SigningKeyPair.random(crypto.random, crypto.signer));\n                    if (addCard.isPresent())\n                        tmpIdentityStore.accept(ArrayOps.bytesToHex(identityPair.serialize()));\n                    PublicKeyHash identityHash = ContentAddressedStorage.hashKey(identityPair.publicSigningKey);\n                    SigningPrivateKeyAndPublicHash identity = new SigningPrivateKeyAndPublicHash(identityHash, identityPair.secretSigningKey);\n\n                    Optional<BoxingKeyPair> boxer = isLegacy ? Optional.empty() : Optional.of(userWithRoot.getBoxingPair());\n                    Optional<SigningKeyPair> loginDataIdentity = isLegacy ? Optional.empty() : Optional.of(identityPair);\n                    UserStaticData entryData = new UserStaticData(Collections.emptyList(), userWithRoot.getRoot(), loginDataIdentity, boxer);\n                    progressCallback.accept(\"Registering username\");\n                    Bat mirror = Bat.random(crypto.random);\n                    return BatId.sha256(mirror, crypto.hasher)\n                            .thenCompose(batid -> opLog.addBat(username, batid, mirror, identity)\n                                    .thenCompose(b -> initialNetwork.dhtClient.id()\n                            .thenCompose(id -> UserPublicKeyLink.createInitial(identity, username, expiry, Arrays.asList(id)))\n                            .thenCompose(chain -> IpfsTransaction.call(identityHash, tid -> identityPair.secretSigningKey.signMessage(identityPair.publicSigningKey.serialize())\n                                    .thenCompose(signedIdentity -> network.dhtClient.putSigningKey(\n                                    signedIdentity,\n                                    identityHash,\n                                    identityPair.publicSigningKey, tid)).thenCompose(returnedIdentityHash -> {\n                                PublicBoxingKey publicBoxingKey = userWithRoot.getBoxingPair().publicBoxingKey;\n                                return crypto.hasher.sha256(publicBoxingKey.serialize())\n                                        .thenCompose(boxerHash -> identityPair.secretSigningKey.signMessage(boxerHash)\n                                                .thenCompose(signedBoxerHash -> network.dhtClient.putBoxingKey(identityHash,\n                                                        signedBoxerHash, publicBoxingKey, tid)));\n                                            }).thenCompose(boxerHash -> {\n                                progressCallback.accept(\"Creating filesystem\");\n                                return WriterData.createIdentity(identityHash,\n                                        identity,\n                                        Optional.of(new PublicKeyHash(boxerHash)),\n                                        isLegacy ? Optional.of(entryData) : Optional.empty(),\n                                        algorithm,\n                                        network.dhtClient, network.hasher, tid).thenCompose(newUserData -> {\n\n                                    CommittedWriterData notCommitted = new CommittedWriterData(MaybeMultihash.empty(), newUserData, Optional.empty());\n                                    network.synchronizer.put(identity.publicKeyHash, identity.publicKeyHash, notCommitted);\n                                    return network.synchronizer.applyComplexUpdate(identityHash, identity,\n                                            (s, committer) -> committer.commit(identityHash, identity, newUserData, notCommitted, tid));\n                                });\n                            }), network.dhtClient)\n                                    .thenCompose(snapshot -> {\n                                        LOG.info(\"Creating user's root directory\");\n                                        long t1 = System.currentTimeMillis();\n                                        return createEntryDirectory(identity, username, entryData, loginPublicKey, userWithRoot.getRoot(), Optional.of(batid), network, crypto)\n                                                .thenCompose(globalRoot -> {\n                                                    LOG.info(\"Creating root directory took \" + (System.currentTimeMillis() - t1) + \" mS\");\n                                                    return createSpecialDirectory(globalRoot, username, SHARED_DIR_NAME, Optional.of(batid), network, crypto);\n                                                })\n                                                .thenCompose(globalRoot -> createSpecialDirectory(globalRoot, username,\n                                                        TRANSACTIONS_DIR_NAME, Optional.of(batid), network, crypto))\n                                                .thenCompose(globalRoot -> createSpecialDirectory(globalRoot, username,\n                                                        CapabilityStore.CAPABILITY_CACHE_DIR, Optional.of(batid), network, crypto))\n                                                .thenCompose(x -> network.commit(identityHash))\n                                                .thenCompose(c -> completeSignup(username, chain, identity, token, addCard, progressCallback, opLog, initialNetwork, crypto))\n                                                .thenCompose(y -> signIn(username, userWithRoot, mfa -> null, initialNetwork, crypto, progressCallback));\n                                    }))));\n                }).exceptionally(Futures::logAndThrow);\n    }\n\n    private static CompletableFuture<Boolean> completeSignup(String username,\n                                                             List<UserPublicKeyLink> chain,\n                                                             SigningPrivateKeyAndPublicHash identity,\n                                                             String token,\n                                                             Optional<BiFunction<PaymentProperties, SigningPrivateKeyAndPublicHash, CompletableFuture<byte[]>>> addCard,\n                                                             Consumer<String> progressCallback,\n                                                             OpLog opLog,\n                                                             NetworkAccess initialNetwork,\n                                                             Crypto crypto) {\n        return signupWithRetry(chain.get(0), addCard.isPresent() ?\n                        proof -> initialNetwork.coreNode.startPaidSignup(username, chain.get(0), proof)\n                                .thenCompose(toPayOrRetry -> toPayOrRetry.isB() ?\n                                        Futures.of(Optional.of(toPayOrRetry.b())) :\n                                        addCard.get().apply(toPayOrRetry.a(), identity)\n                                                .thenCompose(signedSpaceReq -> initialNetwork.coreNode.completePaidSignup(username, chain.get(0), opLog, signedSpaceReq, proof)\n                                                        .thenCompose(paid -> paid.error.isPresent() ?\n                                                                Futures.<Optional<RequiredDifficulty>>errored(new RuntimeException(paid.error.get())) :\n                                                                retryUntilPositiveQuota(initialNetwork, identity,\n                                                                        () -> initialNetwork.coreNode.completePaidSignup(username, chain.get(0), opLog, signedSpaceReq, proof), 1_000, 5).thenApply(z -> Optional.<RequiredDifficulty>empty())))) :\n                        proof -> initialNetwork.coreNode.signup(username, chain.get(0), opLog, proof, token),\n                crypto.hasher, progressCallback);\n    }\n\n    private static CompletableFuture<Boolean> startMirror(String username,\n                                                          Function<ProofOfWork, CompletableFuture<Optional<RequiredDifficulty>>> startMirror,\n                                                          Hasher hasher,\n                                                          Consumer<String> progressCallback) {\n        byte[] data = username.getBytes(StandardCharsets.UTF_8);\n        return time(() -> hasher.generateProofOfWork(ProofOfWork.MIN_DIFFICULTY, data), \"Proof of work\")\n                .thenCompose(startMirror)\n                .thenCompose(diff -> {\n                    if (diff.isPresent()) {\n                        progressCallback.accept(\"The server is currently under load, retrying...\");\n                        return time(() -> hasher.generateProofOfWork(diff.get().requiredDifficulty, data), \"Proof of work\")\n                                .thenCompose(startMirror)\n                                .thenApply(d -> {\n                                    if (d.isPresent())\n                                        throw new IllegalStateException(\"Server is under load please try again later\");\n                                    return true;\n                                });\n                    }\n                    return Futures.of(true);\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> mirrorLoginData(\n            String password,\n            Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa,\n            Consumer<String> progressCallback) {\n        return Futures.asyncExceptionally(() -> getWriterDataCbor(network, username),\n                e ->  {\n                    if (e.getMessage().contains(\"hash not present\"))\n                        return Futures.errored(new IllegalStateException(\"User has been deleted. Did you mean a different username?\"));\n                    else if (e.getMessage().contains(\"No public-key for user\"))\n                        return Futures.errored(new IllegalStateException(\"Unknown username. Did you enter it correctly?\"));\n                    else\n                        return Futures.errored(e);\n                }).thenCompose(pair -> {\n            SecretGenerationAlgorithm algorithm = WriterData.fromCbor(pair.right).generationAlgorithm\n                    .orElseThrow(() -> new IllegalStateException(\"No login algorithm specified in user data!\"));\n            progressCallback.accept(\"Generating keys\");\n            return UserUtil.generateUser(username, password, crypto, algorithm)\n                    .thenCompose(generatedCredentials -> {\n                        progressCallback.accept(\"Retrieving login data\");\n                        WriterData userData = WriterData.fromCbor(pair.right);\n                        boolean legacyAccount = userData.staticData.isPresent();\n                        if (legacyAccount)\n                            throw new IllegalStateException(\"Legacy accounts do not have login data, change your password to upgrade your account.\");\n                        PublicSigningKey loginPub = generatedCredentials.getUser().publicSigningKey;\n                        SecretSigningKey loginSecret = generatedCredentials.getUser().secretSigningKey;\n                        SymmetricKey loginRoot = generatedCredentials.getRoot();\n                        return getLoginData(username, loginPub, loginSecret, loginRoot, mfa, false, false, network)\n                                .thenCompose(p -> network.account.setLoginData(\n                                        new LoginData(username, p.left, loginPub, Optional.empty()), signer, true));\n                    });\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> mirrorOnThisServer(\n            Optional<Function<PaymentProperties, CompletableFuture<Plan>>> addCard,\n            Consumer<String> progressCallback) {\n        BatWithId mirrorBat = this.mirrorBat.orElseThrow(() -> new IllegalStateException(\"You need a mirror bat!\"));\n        return startMirror(username,\n                addCard.isPresent() ?\n                        proof -> TimeLimitedClient.signNow(signer.secret)\n                                .thenCompose(signedTime -> network.coreNode.startPaidMirror(username, signedTime, proof))\n                                .thenCompose(toPayOrRetry -> toPayOrRetry.isB() ?\n                                        Futures.of(Optional.of(toPayOrRetry.b())) :\n                                        addCard.get().apply(toPayOrRetry.a())\n                                                .thenCompose(s -> signSpaceRequest(username, signer, s.desiredQuota, s.annual))\n                                                .thenCompose(signedSpaceReq -> network.coreNode.completePaidMirror(username, mirrorBat, signedSpaceReq, proof)\n                                                        .thenCompose(paid -> paid.error.isPresent() ?\n                                                                Futures.<Optional<RequiredDifficulty>>errored(new RuntimeException(paid.error.get())) :\n                                                                retryUntilPositiveQuota(network, signer,\n                                                                        () -> network.coreNode.completePaidMirror(username, mirrorBat, signedSpaceReq, proof), 1_000, 5)\n                                                                        .thenApply(z -> Optional.<RequiredDifficulty>empty())))) :\n                        proof -> TimeLimitedClient.signNow(signer.secret)\n                                .thenCompose(signedTime -> network.coreNode.startMirror(username, mirrorBat, signedTime, proof))\n                                .thenApply(x -> Optional.<RequiredDifficulty>empty()),\n                crypto.hasher, progressCallback);\n    }\n\n    @JsMethod\n    public CompletableFuture<String> getMigrationId() {\n        return network.coreNode.getHomeServer(username)\n                .thenApply(home -> username + \"@\" + home.orElseThrow(() -> new IllegalStateException(\"No home server!\")));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> isHome() {\n        return network.coreNode.getHomeServer(username)\n                .thenCompose(home -> network.dhtClient.ids()\n                        .thenApply(thisServersIds -> thisServersIds.stream()\n                                .anyMatch(c -> c.bareMultihash().equals(home.get().bareMultihash()))));\n    }\n\n    @JsMethod\n    public CompletableFuture<UserSnapshot> migrateToThisServer(String password,\n                                                               Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa) {\n        return signIn(username, password, mfa, network, crypto)\n                .thenCompose(x -> network.coreNode.getChain(username))\n                .thenCompose(existing -> getSpaceUsage(false).thenCompose(usage ->\n                        network.dhtClient.id().thenCompose(thisServer -> {\n                            Multihash originalNodeId = existing.get(existing.size() - 1)\n                                    .claim.storageProviders.stream().findFirst().get();\n                            Cid newStorageNodeId = thisServer;\n                            return Migrate.buildMigrationChain(existing, newStorageNodeId, signer.secret)\n                                    .thenCompose(newChain -> network.coreNode.migrateUser(username, newChain, originalNodeId, mirrorBat, LocalDateTime.now(ZoneOffset.UTC), usage, true));\n                        })));\n    }\n\n    private static CompletableFuture<Boolean> retryUntilPositiveQuota(NetworkAccess network,\n                                                                          SigningPrivateKeyAndPublicHash identity,\n                                                                          Supplier<CompletableFuture<PaymentProperties>> retry,\n                                                                          long sleepMillis,\n                                                                          int attemptsLeft) {\n        return TimeLimitedClient.signNow(identity.secret)\n                .thenCompose(signedTime -> network.spaceUsage.getQuota(identity.publicKeyHash, signedTime))\n                .thenCompose(quota -> {\n                    if (quota > 0)\n                        return Futures.of(true);\n                    if (attemptsLeft <= 0)\n                        return Futures.errored(new IllegalStateException(\"Unable to complete paid signup. Did you add your payment card?\"));\n                    try {Thread.sleep(sleepMillis);} catch (InterruptedException e) {}\n                    return retry.get()\n                            .thenCompose(b -> retryUntilPositiveQuota(network, identity, retry, sleepMillis * 2, attemptsLeft - 1));\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> ensureUsernameClaimRenewed() {\n        return getUsernameClaimExpiry()\n                .thenCompose(expiry -> expiry.isBefore(LocalDate.now().plusMonths(1)) ?\n                        renewUsernameClaim(LocalDate.now().plusMonths(2)) :\n                        CompletableFuture.completedFuture(true));\n    }\n\n    private static CompletableFuture<TrieNode> createSpecialDirectory(TrieNode globalRoot,\n                                                                      String username,\n                                                                      String dirName,\n                                                                      Optional<BatId> mirrorBatId,\n                                                                      NetworkAccess network,\n                                                                      Crypto crypto) {\n        return globalRoot.getByPath(username, crypto.hasher, network)\n                .thenCompose(root -> root.get().mkdir(dirName, network, true, mirrorBatId, crypto))\n                .thenApply(x -> globalRoot);\n    }\n\n    @JsType\n    public static class EncryptedURL {\n        public static final int PAD_TO_SIZE = 50;\n        public final String base64Nonce, base64Ciphertext;\n\n        public EncryptedURL(String base64Nonce, String base64Ciphertext) {\n            this.base64Nonce = base64Nonce;\n            this.base64Ciphertext = base64Ciphertext;\n        }\n    }\n\n    @JsMethod\n    public EncryptedURL encryptURL(String url) {\n        // pad url to avoid leaking length\n        while (url.length() % EncryptedURL.PAD_TO_SIZE != 0)\n            url = url + \" \";\n        byte[] nonce = rootKey.createNonce();\n        byte[] cipherText = rootKey.encrypt(url.getBytes(), nonce);\n        Base64.Encoder encoder = Base64.getEncoder();\n        return new EncryptedURL(encoder.encodeToString(nonce), encoder.encodeToString(cipherText));\n    }\n\n    @JsMethod\n    public String decryptURL(String cipherTextBase64, String nonceBase64) {\n        Base64.Decoder decoder = Base64.getDecoder();\n        byte[] nonce = decoder.decode(nonceBase64);\n        byte[] ciphertext = decoder.decode(cipherTextBase64);\n        return new String(rootKey.decrypt(ciphertext, nonce)).trim();\n    }\n\n    /**\n     *\n     * @param friendName\n     * @return a pair of the friends keys used to generate the fingerprint, and the resulting fingerprint\n     */\n    @JsMethod\n    public CompletableFuture<Pair<List<PublicKeyHash>, FingerPrint>> generateFingerPrint(String friendName) {\n        return getPublicKeyHashes(username)\n                .thenCompose(ourKeys -> getPublicKeyHashes(friendName)\n                        .thenApply(friendKeysPair -> {\n                            PublicKeyHash friendId = friendKeysPair.left;\n                            PublicKeyHash friendBox = friendKeysPair.right;\n                            List<PublicKeyHash> friendKeys = Arrays.asList(friendId, friendBox);\n                            return new Pair<>(friendKeys, FingerPrint.generate(\n                                    username,\n                                    Arrays.asList(ourKeys.left, ourKeys.right),\n                                    friendName,\n                                    friendKeys,\n                                    crypto.hasher));\n                        }));\n    }\n\n    @JsMethod\n    public boolean isSecretLink() {\n        return username == null;\n    }\n\n    public static CompletableFuture<UserContext> fromSecretLinksV2(List<String> linkStrings,\n                                                                   List<Supplier<CompletableFuture<String>>> userPasswords,\n                                                                   NetworkAccess network,\n                                                                   Crypto crypto) {\n        List<SecretLink> links = linkStrings.stream().map(SecretLink::fromLink).collect(Collectors.toList());\n        return Futures.combineAllInOrder(IntStream.range(0, links.size()).mapToObj(i -> network.getSecretLink(links.get(i))\n                        .thenCompose(retrieved -> (retrieved.hasUserPassword ? userPasswords.get(i).get() : Futures.of(\"\"))\n                                .thenCompose(upass -> retrieved.decryptFromPassword(links.get(i).labelString(), links.get(i).linkPassword + upass, crypto)))\n        ).collect(Collectors.toList()))\n                .thenCompose(caps -> fromSecretLinks(caps, network, crypto));\n    }\n\n    @JsMethod\n    public static CompletableFuture<UserContext> fromSecretLinkV2(String linkString,\n                                                                  Supplier<CompletableFuture<String>> userPassword,\n                                                                  NetworkAccess network,\n                                                                  Crypto crypto) {\n        SecretLink link = SecretLink.fromLink(linkString);\n        return network.getSecretLink(link)\n                .thenCompose(retrieved -> (retrieved.hasUserPassword ? userPassword.get() : Futures.of(\"\"))\n                        .thenCompose(upass -> retrieved.decryptFromPassword(link.labelString(), link.linkPassword + upass, crypto)))\n                .thenCompose(cap -> fromSecretLink(cap, network, crypto));\n    }\n\n    @JsMethod\n    public static CompletableFuture<UserContext> fromSecretLink(String link, NetworkAccess network, Crypto crypto) {\n        AbsoluteCapability cap;\n        try {\n            cap = AbsoluteCapability.fromLink(link);\n        } catch (Exception e) { //link was invalid\n            CompletableFuture<UserContext> invalidLink = new CompletableFuture<>();\n            invalidLink.completeExceptionally(e);\n            return invalidLink;\n        }\n        return fromSecretLink(cap, network, crypto);\n    }\n\n    private static CompletableFuture<UserContext> fromSecretLink(AbsoluteCapability cap, NetworkAccess network, Crypto crypto) {\n        WriterData empty = new WriterData(cap.owner,\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty(),\n                Collections.emptyMap(),\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty());\n        CommittedWriterData userData = new CommittedWriterData(MaybeMultihash.empty(), empty, Optional.empty());\n        UserContext context = new UserContext(null, null, null, null, network,\n                crypto, userData, TrieNodeImpl.empty(), null, null,\n                new SharedWithCache(null, null, network, crypto), Optional.empty());\n        return buildTrieFromCap(cap, context.entrie, network, crypto)\n                .thenApply(trieNode -> {\n                    context.entrie = trieNode;\n                    return context;\n                });\n    }\n\n    private static CompletableFuture<UserContext> fromSecretLinks(List<AbsoluteCapability> caps,\n                                                                  NetworkAccess network,\n                                                                  Crypto crypto) {\n        WriterData empty = new WriterData(caps.get(0).owner,\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty(),\n                Collections.emptyMap(),\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty());\n        CommittedWriterData userData = new CommittedWriterData(MaybeMultihash.empty(), empty, Optional.empty());\n        UserContext context = new UserContext(null, null, null, null, network,\n                crypto, userData, TrieNodeImpl.empty(), null, null,\n                new SharedWithCache(null, null, network, crypto), Optional.empty());\n        return buildTrieFromCaps(caps, context.entrie, network)\n                .thenApply(trieNode -> {\n                    context.entrie = trieNode;\n                    return context;\n                });\n    }\n\n    private static CompletableFuture<TrieNode> buildTrieFromCap(AbsoluteCapability cap,\n                                                                TrieNode currentRoot,\n                                                                NetworkAccess network,\n                                                                Crypto crypto) {\n        EntryPoint entry = new EntryPoint(cap, \"\");\n        return Futures.asyncExceptionally(\n                () -> NetworkAccess.retrieveEntryPoint(entry, network)\n                        .thenCompose(r -> entry.isValid(r.getPath(), network).thenApply(valid -> {\n                            if (! valid)\n                                throw new IllegalStateException(\"Invalid link!\");\n                            return currentRoot.put(r.getPath(), entry);\n                        })),\n                t -> NetworkAccess.retrieveEntryPoint(entry, network)\n                        .thenCompose(r -> entry.isValid(r.getPath(), network).thenApply(valid -> {\n                            if (! valid)\n                                throw new IllegalStateException(\"Invalid link!\");\n                            return currentRoot.put(r.getPath(), entry);\n                        })));\n    }\n\n    private static CompletableFuture<TrieNode> buildTrieFromCaps(List<AbsoluteCapability> caps,\n                                                                TrieNode currentRoot,\n                                                                NetworkAccess network) {\n        List<CompletableFuture<RetrievedEntryPoint>> retrieved = caps.stream()\n                .map(cap -> NetworkAccess.retrieveEntryPoint(new EntryPoint(cap, \"\"), network))\n                .collect(Collectors.toList());\n        return Futures.reduceAll(retrieved, currentRoot,\n                (c, f) -> f.thenCompose(r -> r.entry.isValid(r.getPath(), network)\n                        .thenApply(valid -> {\n                            if (! valid)\n                                throw new IllegalStateException(\"Invalid link!\");\n                            return c.put(r.getPath(), r.entry);\n                        })),\n                (a, b) -> b);\n    }\n\n    @JsMethod\n    public String getLinkString(LinkProperties props) {\n        return props.toLinkString(signer.publicKeyHash);\n    }\n\n    @JsMethod\n    public CompletableFuture<String> getLinkHost() {\n        return network.dhtClient.linkHost(signer.publicKeyHash);\n    }\n\n    @JsMethod\n    public CompletableFuture<LinkProperties> createSecretLink(String filePath,\n                                                              boolean isWritable,\n                                                              Optional<LocalDateTime> expiry,\n                                                              String maxRetrievals,\n                                                              String userPassword,\n                                                              boolean open) {\n        return createSecretLink(filePath, isWritable, expiry,\n                maxRetrievals.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(maxRetrievals)),\n                userPassword, open);\n    }\n\n    public CompletableFuture<LinkProperties> createSecretLink(String filePath,\n                                                              boolean isWritable,\n                                                              Optional<LocalDateTime> expiry,\n                                                              Optional<Integer> maxRetrievals,\n                                                              String userPassword,\n                                                              boolean open) {\n        SecretLink res = SecretLink.create(signer.publicKeyHash, crypto.random);\n        LinkProperties props = new LinkProperties(res.label, res.linkPassword, userPassword, isWritable, maxRetrievals, expiry, open, Optional.empty());\n        Path toFile = PathUtil.get(filePath);\n        if (! isWritable)\n            return updateSecretLink(filePath, props);\n        return getByPath(toFile.getParent())\n                .thenCompose(parent -> parent.get().getChild(toFile.getFileName().toString(), crypto.hasher, network)\n                        .thenCompose(fopt -> shareWriteAccessWith(toFile, Collections.emptySet())))\n                .thenCompose(s -> writeSynchronizer.applyComplexComputation(signer.publicKeyHash, signer, (v, c) -> updateSecretLink(filePath, props, v.mergeAndOverwriteWith(s), c)))\n                .thenApply(x -> x.right);\n    }\n\n    @JsMethod\n    public CompletableFuture<LinkProperties> updateSecretLink(String filePath,\n                                                              LinkProperties props) {\n        return writeSynchronizer.applyComplexComputation(signer.publicKeyHash, signer, (v, c) -> updateSecretLink(filePath, props, v, c))\n                .thenApply(p -> p.right);\n    }\n\n    private CompletableFuture<Pair<Snapshot, LinkProperties>> updateSecretLink(String filePath,\n                                                                               LinkProperties props,\n                                                                               Snapshot v1,\n                                                                               Committer c) {\n        // put encrypted secret link in champ on identity, champ root must have mirror bat to make it private\n        PublicKeyHash id = signer.publicKeyHash;\n        return getByPath(filePath, v1)\n                .thenApply(opt -> opt.orElseThrow(() -> new IllegalStateException(\"Couldn't retrieve \" + filePath)))\n                .thenCompose(file -> {\n                    boolean differentWriter = file.getPointer().getParentCap().writer.map(parentWriter -> ! parentWriter.equals(file.writer())).orElse(false);\n                    if (props.isLinkWritable && ! differentWriter)\n                        throw new IllegalStateException(\"To generate a writable secret link, the target must already be in a different writing space!\");\n                    AbsoluteCapability cap = props.isLinkWritable ? file.getLinkPointer().capability : file.getPointer().capability.readOnly();\n                    SecretLink res = new SecretLink(id, props.label, props.linkPassword);\n                    String fullPassword = props.linkPassword + props.userPassword;\n                    return EncryptedCapability.createFromPassword(cap, res.labelString(), fullPassword, !props.userPassword.isEmpty(), crypto)\n                            .thenApply(payload -> new SecretLinkTarget(payload, props.expiry, props.maxRetrievals))\n                            .thenCompose(value -> IpfsTransaction.call(id,\n                                    tid -> v1.withWriter(id, id, network).thenCompose(v2 -> v2.get(id).props.get().addLink(signer, props.label, value,\n                                                    props.existing.map(CborObject.CborMerkleLink::new), mirrorBat, tid, network.dhtClient, network.hasher)\n                                            .thenCompose(p -> c.commit(id, signer, p.left, v2.get(id), tid)\n                                                    .thenCompose(v3 -> sharedWithCache.addSecretLink(PathUtil.get(filePath),\n                                                                    props.withExisting(Optional.of(p.right)), v2.mergeAndOverwriteWith(v3), c, network)\n                                                            .thenApply(v4 -> new Pair<>(new Snapshot(id, v3.get(id)), props.withExisting(Optional.of(p.right))))))), network.dhtClient));\n                });\n    }\n    @JsMethod\n    public CompletableFuture<Snapshot> deleteSecretLink(long label, Path toFile, boolean isWritable) {\n        PublicKeyHash id = signer.publicKeyHash;\n        return writeSynchronizer.applyComplexUpdate(id, signer, (v, c) -> deleteSecretLink(label, toFile, v, c)\n                .thenCompose(s -> sharedWithCache.removeSecretLink(toFile, label, s, c, network)));\n    }\n\n    public CompletableFuture<Snapshot> deleteSecretLink(long label, Path toFile, Snapshot in, Committer c) {\n        PublicKeyHash id = signer.publicKeyHash;\n        return in.withWriter(id, id, network).thenCompose(v -> IpfsTransaction.call(id,\n                tid -> {\n                    WriterData intial = v.get(id).props.get();\n                    return intial.removeLink(signer, label, mirrorBat, tid, network.dhtClient, network.hasher)\n                            .thenCompose(wd -> wd.equals(intial) ? Futures.of(v) : c.commit(id, signer, wd, v.get(id), tid))\n                            .thenApply(res -> v.mergeAndOverwriteWith(res));\n                }, network.dhtClient));\n    }\n\n    public static CompletableFuture<AbsoluteCapability> getPublicCapability(Path originalPath, NetworkAccess network) {\n        String ownerName = originalPath.getName(0).toString();\n\n        return network.coreNode.getPublicKeyHash(ownerName).thenCompose(ownerOpt -> {\n            if (!ownerOpt.isPresent())\n                throw new IllegalStateException(\"Owner doesn't exist for path \" + originalPath);\n            PublicKeyHash owner = ownerOpt.get();\n            return WriterData.getWriterData(owner, owner, network.mutable, network.dhtClient).thenCompose(userData -> {\n                Optional<Multihash> publicData = userData.props.get().publicData;\n                if (! publicData.isPresent())\n                    throw new IllegalStateException(\"User \" + ownerName + \" has not made any files public.\");\n\n                return network.dhtClient.get(owner, (Cid)publicData.get(), Optional.empty())\n                        .thenCompose(rootCbor -> InodeFileSystem.build(owner, rootCbor.get(), network.hasher, network.dhtClient))\n                        .thenCompose(publicCaps -> publicCaps.getByPath(originalPath.toString()))\n                        .thenApply(resOpt -> {\n                            if (resOpt.isEmpty() || resOpt.get().left.cap.isEmpty())\n                                throw new IllegalStateException(\"User \" + ownerName + \" has not published a file at \" + originalPath);\n                            return resOpt.get().left.cap.get();\n                        });\n            });\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<FileWrapper>> getPublicFile(Path file) {\n        FileProperties.ensureValidParsedPath(file);\n        return getPublicCapability(file, network)\n                .thenCompose(cap -> buildTrieFromCap(cap, TrieNodeImpl.empty(), network, crypto)\n                .thenCompose(t -> t.getByPath(file.toString(), crypto.hasher, network)))\n                .exceptionally(e -> Optional.empty());\n    }\n\n    @JsMethod\n    public CompletableFuture<String> getEntryPath() {\n        if (username != null)\n            return CompletableFuture.completedFuture(\"/\");\n\n        CompletableFuture<Optional<FileWrapper>> dir = getByPath(\"/\");\n        return dir.thenCompose(opt -> getLinkPath(opt.get()))\n                .thenApply(path -> path.substring(1)); // strip off extra slash at root\n    }\n\n    private CompletableFuture<String> getLinkPath(FileWrapper file) {\n        if (!file.isDirectory())\n            return CompletableFuture.completedFuture(\"\");\n        return file.getChildren(crypto.hasher, network)\n                .thenCompose(children -> {\n                    if (children.size() != 1)\n                        return CompletableFuture.completedFuture(file.getName());\n                    FileWrapper child = children.stream().findAny().get();\n                    if (child.isReadable()) // case where a directory was shared with exactly one direct child\n                        return CompletableFuture.completedFuture(file.getName() + (child.isDirectory() ?\n                                \"/\" + child.getName() : \"\"));\n                    return getLinkPath(child)\n                            .thenApply(p -> file.getName() + (p.length() > 0 ? \"/\" + p : \"\"));\n                });\n    }\n\n    private CompletableFuture<UserContext> init(Consumer<String> progressCallback) {\n        progressCallback.accept(\"Retrieving Friends\");\n        return writeSynchronizer.getValue(signer.publicKeyHash, signer.publicKeyHash)\n                .thenCompose(wd -> time(() -> createFileTree(entrie, username, network, crypto), \"Creating filetree\")\n                        .thenApply(root -> {\n                            this.entrie = root;\n                            return this;\n                        })\n                );\n    }\n\n    public static CompletableFuture<Optional<BatWithId>> getMirrorBat(String username,\n                                                                      SigningPrivateKeyAndPublicHash identity,\n                                                                      SymmetricKey loginRoot,\n                                                                      NetworkAccess network) {\n        return Futures.asyncExceptionally(\n                        () -> {\n                            CompletableFuture<List<BatWithId>> res = new CompletableFuture<>();\n                            // race the cache\n                            network.batCave.getUserBats(username, identity).thenAccept(bats -> {\n                                if (!bats.isEmpty() && network.batCache.isPresent())\n                                    network.batCache.get().setUserBats(username, bats, loginRoot);\n                                res.complete(bats);\n                            }).exceptionally(t -> {\n                                res.completeExceptionally(t);\n                                return null;\n                            });\n                            if (network.batCache.isPresent())\n                                network.batCache.get().getUserBats(username, loginRoot).thenAccept(res::complete);\n                            return res;\n                        },\n                        t -> {\n                            if (network.batCache.isPresent() &&\n                            (t.toString().contains(\"ConnectException\") || t.toString().contains(\"RateLimitException\")))\n                                return network.batCache.get().getUserBats(username, loginRoot);\n                            return Futures.errored(t);\n                        })\n                .thenApply(bats -> bats.isEmpty() ?\n                        Optional.empty() :\n                        Optional.of(bats.get(bats.size() - 1)));\n    }\n\n    public CompletableFuture<Optional<BatWithId>> getMirrorBat() {\n        return getMirrorBat(username, signer, rootKey, network);\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<BatId>> ensureMirrorId() {\n        Optional<BatId> mirrorBatId = mirrorBatId();\n        if (mirrorBatId.isPresent())\n            return Futures.of(mirrorBatId);\n        return getMirrorBat()\n                .thenCompose(current -> {\n                    if (current.isPresent())\n                        return Futures.of(current.map(BatWithId::id));\n                    // generate a mirror bat\n                    Bat mirror = Bat.random(crypto.random);\n                    return BatId.sha256(mirror, crypto.hasher)\n                            .thenCompose(id -> network.batCave.addBat(username, id, mirror, signer)\n                                    .thenApply(b -> Optional.of(id)));\n                });\n    }\n\n    public Optional<BatId> mirrorBatId() {\n        return mirrorBat.map(BatWithId::id);\n    }\n\n    public CompletableFuture<FileWrapper> getSharingFolder() {\n        return getByPath(\"/\" + username + \"/shared\").thenApply(opt -> opt.get());\n    }\n\n    /**\n     *\n     * @return The pending storage requests on the server we are talking to if we are an admin\n     */\n    @JsMethod\n    public CompletableFuture<List<DecodedSpaceRequest>> getAndDecodePendingSpaceRequests() {\n        return getPendingSpaceRequests().thenCompose(this::decodeSpaceRequests);\n    }\n\n    /**\n     *\n     * @return The pending storage requests on the server we are talking to if we are an admin\n     */\n    @JsMethod\n    public CompletableFuture<List<SpaceUsage.LabelledSignedSpaceRequest>> getPendingSpaceRequests() {\n        return TimeLimitedClient.signNow(signer.secret)\n                .thenCompose(signedTime -> network.dhtClient.id()\n                        .thenCompose(id -> network.instanceAdmin.getPendingSpaceRequests(signer.publicKeyHash, id, signedTime)));\n    }\n\n    /**\n     *\n     * @param in raw space requests\n     * @return raw space requests paired with their decoded request\n     */\n    @JsMethod\n    public CompletableFuture<List<DecodedSpaceRequest>> decodeSpaceRequests(List<QuotaControl.LabelledSignedSpaceRequest> in) {\n        return DecodedSpaceRequest.decodeSpaceRequests(in, network.coreNode, network.dhtClient);\n    }\n\n    /**\n     *\n     * @param req\n     * @return true when completed successfully\n     */\n    @JsMethod\n    public CompletableFuture<Boolean> approveSpaceRequest(DecodedSpaceRequest req) {\n        return signer.secret.signMessage(req.source.serialize())\n                .thenCompose(adminSignedRequest -> network.dhtClient.id()\n                        .thenCompose(instanceId -> network.instanceAdmin\n                                .approveSpaceRequest(signer.publicKeyHash, instanceId, adminSignedRequest)));\n    }\n\n    /**\n     *\n     * @param req\n     * @return true when completed successfully\n     */\n    @JsMethod\n    public CompletableFuture<Boolean> rejectSpaceRequest(DecodedSpaceRequest req) {\n        throw new IllegalStateException(\"Unimplemented!\");\n//        byte[] adminSignedRequest = signer.secret.signMessage(req.source.serialize());\n//        return network.dhtClient.id()\n//                .thenCompose(instanceId -> network.instanceAdmin\n//                        .rejectSpaceRequest(signer.publicKeyHash, instanceId, adminSignedRequest));\n    }\n\n    @JsMethod\n    public CompletableFuture<PaymentProperties> getPaymentProperties(boolean newClientSecret) {\n        return TimeLimitedClient.signNow(signer.secret)\n                .thenCompose(signedTime -> network.spaceUsage.getPaymentProperties(signer.publicKeyHash, newClientSecret, signedTime));\n    }\n\n    /**\n     *\n     * @return The maximum amount of space this user is allowed to use in bytes\n     */\n    @JsMethod\n    public CompletableFuture<Long> getQuota() {\n        return TimeLimitedClient.signNow(signer.secret)\n                .thenCompose(signedTime -> network.spaceUsage.getQuota(signer.publicKeyHash, signedTime));\n    }\n\n    /**\n     *\n     * @return The total amount of space used by this account in bytes\n     */\n    @JsMethod\n    public CompletableFuture<Long> getSpaceUsage(boolean localUsage) {\n        return TimeLimitedClient.signNow(signer.secret)\n                .thenCompose(signedTime -> network.spaceUsage.getUsage(signer.publicKeyHash, signedTime, localUsage));\n    }\n\n    /**\n     *\n     * @return true when completed successfully\n     */\n    @JsMethod\n    public CompletableFuture<PaymentProperties> requestSpace(long requestedQuota, boolean annual) {\n        return network.spaceUsage.requestQuota(username, signer, requestedQuota, annual);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> isRegistered() {\n        LOG.info(\"isRegistered\");\n        return network.coreNode.getUsername(signer.publicKeyHash).thenApply(registeredUsername -> {\n            LOG.info(\"got username \\\"\" + registeredUsername + \"\\\"\");\n            return this.username.equals(registeredUsername);\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> isAvailable() {\n        return network.coreNode.getPublicKeyHash(username)\n                .thenApply(publicKey -> !publicKey.isPresent());\n    }\n\n    public CompletableFuture<LocalDate> getUsernameClaimExpiry() {\n        return network.coreNode.getChain(username)\n                .thenApply(chain -> chain.get(chain.size() - 1).claim.expiry);\n    }\n\n    public CompletableFuture<Boolean> usernameIsExpired() {\n        return network.coreNode.getChain(username)\n                .thenApply(chain -> UserPublicKeyLink.isExpiredClaim(chain.get(chain.size() - 1)));\n    }\n\n    public CompletableFuture<Boolean> renewUsernameClaim(LocalDate expiry) {\n        return renewUsernameClaim(username, signer, expiry, crypto.hasher, network);\n    }\n\n    private static CompletableFuture<Boolean> signupWithRetry(UserPublicKeyLink chain,\n                                                              Function<ProofOfWork, CompletableFuture<Optional<RequiredDifficulty>>> signup,\n                                                              Hasher hasher,\n                                                              Consumer<String> progressCallback) {\n        byte[] data = new CborObject.CborList(Arrays.asList(chain)).serialize();\n        return time(() -> hasher.generateProofOfWork(ProofOfWork.MIN_DIFFICULTY, data), \"Proof of work\")\n                .thenCompose(signup)\n                .thenCompose(diff -> {\n                    if (diff.isPresent()) {\n                        progressCallback.accept(\"The server is currently under load, retrying...\");\n                        return time(() -> hasher.generateProofOfWork(diff.get().requiredDifficulty, data), \"Proof of work\")\n                                .thenCompose(signup)\n                                .thenApply(d -> {\n                                    if (d.isPresent())\n                                        throw new IllegalStateException(\"Server is under load please try again later\");\n                                    return true;\n                                });\n                    }\n                    return Futures.of(true);\n                });\n    }\n\n    private static CompletableFuture<Boolean> updateChainWithRetry(String username,\n                                                                   List<UserPublicKeyLink> claimChain,\n                                                                   String token,\n                                                                   Hasher hasher,\n                                                                   NetworkAccess network,\n                                                                   Consumer<String> progressCallback) {\n        byte[] data = new CborObject.CborList(claimChain).serialize();\n        return time(() -> hasher.generateProofOfWork(ProofOfWork.MIN_DIFFICULTY, data), \"Proof of work\")\n                .thenCompose(proof -> network.coreNode.updateChain(username, claimChain, proof, token))\n                .thenCompose(diff -> {\n                    if (diff.isPresent()) {\n                        progressCallback.accept(\"The server is currently under load, retrying...\");\n                        return time(() -> hasher.generateProofOfWork(diff.get().requiredDifficulty, data), \"Proof of work\")\n                                .thenCompose(proof -> network.coreNode.updateChain(username, claimChain, proof, token))\n                                .thenApply(d -> {\n                                    if (d.isPresent())\n                                        throw new IllegalStateException(\"Server is under load please try again later\");\n                                    return true;\n                                });\n                    }\n                    return Futures.of(true);\n                });\n    }\n\n    public static CompletableFuture<Boolean> updateHostInPki(String username,\n                                                             SigningPrivateKeyAndPublicHash signer,\n                                                             LocalDate expiry,\n                                                             Multihash newHost,\n                                                             Hasher hasher,\n                                                             NetworkAccess network) {\n        LOG.info(\"updating host for username: \" + username + \" to \" + newHost);\n        return network.coreNode.getChain(username).thenCompose(existing -> {\n            List<Multihash> storage = Arrays.asList(new Cid(1, Cid.Codec.LibP2pKey, newHost.type, newHost.getHash()));\n            return UserPublicKeyLink.Claim.build(username, signer.secret, expiry, storage).thenCompose(newClaim -> {\n                List<UserPublicKeyLink> updated = new ArrayList<>(existing.subList(0, existing.size() - 1));\n                updated.add(new UserPublicKeyLink(signer.publicKeyHash, newClaim, Optional.empty()));\n                return updateChainWithRetry(username, updated, \"\", hasher, network, x -> {\n                });\n            });\n        });\n    }\n\n    public static CompletableFuture<Boolean> renewUsernameClaim(String username,\n                                                                SigningPrivateKeyAndPublicHash signer,\n                                                                LocalDate expiry,\n                                                                Hasher hasher,\n                                                                NetworkAccess network) {\n        LOG.info(\"renewing username: \" + username + \" with expiry \" + expiry);\n        return network.coreNode.getChain(username).thenCompose(existing -> {\n            UserPublicKeyLink last = existing.get(existing.size() - 1);\n            List<Multihash> storage = last.claim.storageProviders;\n            return UserPublicKeyLink.Claim.build(username, signer.secret, expiry, storage).thenCompose(newClaim -> {\n                List<UserPublicKeyLink> updated = new ArrayList<>(existing.subList(0, existing.size() - 1));\n                updated.add(new UserPublicKeyLink(signer.publicKeyHash, newClaim, Optional.empty()));\n                return updateChainWithRetry(username, updated, \"\", hasher, network, x -> {\n                });\n            });\n        });\n    }\n\n    public CompletableFuture<SecretGenerationAlgorithm> getKeyGenAlgorithm() {\n        return getWriterData(network, signer.publicKeyHash, signer.publicKeyHash)\n                .thenApply(wd -> wd.props.get().generationAlgorithm\n                        .orElseThrow(() -> new IllegalStateException(\"No login algorithm specified in user data!\")));\n    }\n\n    public CompletableFuture<Optional<PublicKeyHash>> getNamedKey(String name) {\n        return getWriterData(network, signer.publicKeyHash, signer.publicKeyHash)\n                .thenApply(wd -> wd.props.get().namedOwnedKeys.get(name))\n                .thenApply(res -> Optional.ofNullable(res).map(p -> p.ownedKey));\n    }\n\n    @JsMethod\n    public CompletableFuture<UserContext> changePassword(String oldPassword,\n                                                         String newPassword,\n                                                         Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa) {\n        return getKeyGenAlgorithm().thenCompose(alg -> {\n            if (oldPassword.equals(newPassword))\n                throw new IllegalStateException(\"You must change to a different password.\");\n            // Use a new salt, and if this is a legacy account with generated boxer, remove it from generation and use\n            // a new generated identity independent of the password\n            SecretGenerationAlgorithm newAlgorithm = SecretGenerationAlgorithm.withNewSalt(alg, crypto.random).withoutBoxerOrIdentity();\n            // set claim expiry to two months from now\n            return changePassword(oldPassword, newPassword, alg, newAlgorithm, LocalDate.now().plusMonths(2), mfa);\n        });\n    }\n\n    public CompletableFuture<UserContext> changePassword(String oldPassword,\n                                                         String newPassword,\n                                                         SecretGenerationAlgorithm existingAlgorithm,\n                                                         SecretGenerationAlgorithm newAlgorithm,\n                                                         LocalDate expiry,\n                                                         Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa) {\n        LOG.info(\"Changing password and setting expiry to: \" + expiry);\n        boolean isLegacy = existingAlgorithm.generateBoxerAndIdentity();\n        if (! isLegacy && newAlgorithm.generateBoxerAndIdentity())\n            throw new IllegalStateException(\"Cannot migrate from an upgraded style account to a legacy style account!\");\n\n        return UserUtil.generateUser(username, oldPassword, crypto, existingAlgorithm)\n                .thenCompose(existingLogin -> {\n                    SecretSigningKey existingLoginSecret = existingLogin.getUser().secretSigningKey;\n                    if (isLegacy && !existingLoginSecret.equals(this.signer.secret))\n                        throw new IllegalArgumentException(\"Incorrect existing password during change password attempt!\");\n\n                    return UserUtil.generateUser(username, newPassword, crypto, newAlgorithm)\n                            .thenCompose(updatedLogin -> {\n                                PublicSigningKey newLoginPublicKey = updatedLogin.getUser().publicSigningKey;\n                                PublicKeyHash existingOwner = ContentAddressedStorage.hashKey(existingLogin.getUser().publicSigningKey);\n                                if (! isLegacy) {\n                                    // identity doesn't change here, just need to update the secret UserStaticData\n                                    return getLoginData(username, existingLogin.getUser().publicSigningKey, existingLoginSecret,\n                                            existingLogin.getRoot(), mfa, false, false, network)\n                                            .thenApply(p -> p.right).thenCompose(entry -> {\n                                        UserStaticData updatedEntry = new UserStaticData(entry.entries, updatedLogin.getRoot(), entry.identity, entry.boxer);\n                                        // need to commit new login algorithm too in the same call\n                                        return WriterData.getWriterData(signer.publicKeyHash, signer.publicKeyHash, network.mutable, network.dhtClient).thenCompose(cwd -> {\n                                            WriterData newIdBlock = cwd.props.get().withAlgorithm(newAlgorithm);\n                                            byte[] rawBlock = newIdBlock.serialize();\n                                            return crypto.hasher.sha256(rawBlock).thenCompose(blockHash -> {\n                                                return signer.secret.signMessage(blockHash)\n                                                        .thenCompose(signed -> {\n                                                            OpLog.BlockWrite blockWrite = new OpLog.BlockWrite(signer.publicKeyHash, signed, rawBlock, false, Optional.empty());\n                                                            MaybeMultihash newHash = MaybeMultihash.of(new Cid(1, Cid.Codec.DagCbor, Multihash.Type.sha2_256, blockHash));\n                                                            PointerUpdate pointerCas = new PointerUpdate(cwd.hash, newHash, PointerUpdate.increment(cwd.sequence));\n                                                            return signer.secret.signMessage(pointerCas.serialize())\n                                                                    .thenCompose(signedCas -> {\n                                                                        OpLog.PointerWrite pointerWrite = new OpLog.PointerWrite(signer.publicKeyHash, signedCas);\n                                                                        LoginData updatedLoginData = new LoginData(username, updatedEntry, newLoginPublicKey, Optional.of(new Pair<>(blockWrite, pointerWrite)));\n                                                                        return network.account.setLoginData(updatedLoginData, signer, false)\n                                                                                .thenCompose(b -> UserContext.login(username, newIdBlock, entry,\n                                                                                        signer, entry.boxer.get(), updatedLogin.getRoot(), new Pair<>(pointerCas, newIdBlock.toCbor()), network.clear(), crypto, p -> {}));\n                                                                    });\n                                                        });\n                                            });\n                                        });\n                                    });\n                                }\n                                // upgrading a legacy account to new style\n                                BoxingKeyPair newBoxingKeypair = newAlgorithm.generateBoxerAndIdentity() ? updatedLogin.getBoxingPair() : boxer;\n                                SigningKeyPair newIdentityPair = SigningKeyPair.random(crypto.random, crypto.signer);\n                                PublicSigningKey newIdentityPub = newIdentityPair.publicSigningKey;\n                                return existingLoginSecret.signMessage(newIdentityPub.serialize())\n                                        .thenCompose(signed -> IpfsTransaction.call(existingOwner,\n                                                tid -> network.dhtClient.putSigningKey(\n                                                        signed,\n                                                        existingOwner,\n                                                        newIdentityPub,\n                                                        tid),\n                                                network.dhtClient\n                                        )).thenCompose(newIdentityHash -> {\n                                            SigningPrivateKeyAndPublicHash newIdentity =\n                                                    new SigningPrivateKeyAndPublicHash(newIdentityHash, newIdentityPair.secretSigningKey);\n                                            Map<PublicKeyHash, SigningPrivateKeyAndPublicHash> ownedKeys = new HashMap<>();\n                                            return getUserRoot().thenCompose(homeDir -> {\n                                                // If we ever implement plausibly deniable dual (N) login this will need to include all the other keys\n                                                ownedKeys.put(homeDir.writer(), homeDir.signingPair());\n                                                // Add any named owned key to lookup as well\n                                                // TODO need to get the pki keypair here if we are the 'peergos' user\n\n                                                // auth new key by adding to existing writer data first\n                                                return OwnerProof.build(newIdentity, signer.publicKeyHash).thenCompose(proof -> {\n                                                    return writeSynchronizer.applyUpdate(signer.publicKeyHash, signer, (wd, tid) ->\n                                                                    wd.addOwnedKey(signer.publicKeyHash, signer, proof, network.dhtClient, network.hasher))\n                                                            .thenCompose(version -> version.get(signer).props.get().changeKeys(username,\n                                                                    signer,\n                                                                    newIdentity,\n                                                                    newIdentityPair,\n                                                                    newLoginPublicKey,\n                                                                    newBoxingKeypair,\n                                                                    existingLogin.getRoot(),\n                                                                    updatedLogin.getRoot(),\n                                                                    newAlgorithm,\n                                                                    ownedKeys,\n                                                                    network)).thenCompose(writerData -> {\n                                                                return network.coreNode.getChain(username).thenCompose(existing -> {\n                                                                    List<Multihash> storage = existing.get(existing.size() - 1).claim.storageProviders;\n                                                                    return UserPublicKeyLink.createChain(signer, newIdentity, username, expiry, storage).thenCompose(claimChain ->\n                                                                                    updateChainWithRetry(username, claimChain, \"\", crypto.hasher, network, x -> {\n                                                                                    }))\n                                                                            .thenCompose(updatedChain -> {\n                                                                                if (!updatedChain)\n                                                                                    throw new IllegalStateException(\"Couldn't register new public keys during password change!\");\n\n                                                                                return UserContext.signIn(username, newPassword, mfas -> null, network, crypto);\n                                                                            });\n                                                                });\n                                                            });\n                                                });\n                                            });\n                                        });\n                            });\n                });\n    }\n\n    private static CompletableFuture<TrieNode> createEntryDirectory(SigningPrivateKeyAndPublicHash owner,\n                                                                    String directoryName,\n                                                                    UserStaticData current,\n                                                                    PublicSigningKey loginPublic,\n                                                                    SymmetricKey userRootKey,\n                                                                    Optional<BatId> mirrorBatId,\n                                                                    NetworkAccess network,\n                                                                    Crypto crypto) {\n        long t1 = System.currentTimeMillis();\n        SigningKeyPair writer = SigningKeyPair.random(crypto.random, crypto.signer);\n        LOG.info(\"Random User generation took \" + (System.currentTimeMillis() - t1) + \" mS\");\n        // Steps (All in 1 transaction):\n        // 1. Add signing key authorised by owner\n        // 2. Commit authorisation for writer to owner WriterData\n        // 3. Add empty WriterData for writer\n        // 4. Add root directory writer's WriterData\n        // 5. Add entry point to root dir to owner WriterData's UserStaticData (legacy account) or LoginData\n\n        byte[] rootMapKey = crypto.random.randomBytes(32); // root will be stored under this label\n        Optional<Bat> rootBat = Optional.of(Bat.random(crypto.random));\n        SymmetricKey rootRKey = SymmetricKey.random();\n        SymmetricKey rootWKey = SymmetricKey.random();\n        LOG.info(\"Random keys generation took \" + (System.currentTimeMillis() - t1) + \" mS\");\n\n        PublicKeyHash preHash = ContentAddressedStorage.hashKey(writer.publicSigningKey);\n        SigningPrivateKeyAndPublicHash writerPair =\n                new SigningPrivateKeyAndPublicHash(preHash, writer.secretSigningKey);\n        WritableAbsoluteCapability rootPointer =\n                new WritableAbsoluteCapability(owner.publicKeyHash, preHash, rootMapKey, rootBat, rootRKey, rootWKey);\n        EntryPoint entry = new EntryPoint(rootPointer, directoryName);\n        return owner.secret.signMessage(writer.publicSigningKey.serialize()).thenCompose(signed -> IpfsTransaction.call(owner.publicKeyHash, tid -> network.dhtClient.putSigningKey(\n                signed,\n                owner.publicKeyHash,\n                writer.publicSigningKey,\n                tid).thenCompose(writerHash -> {\n\n            // and authorise the writer key\n            return network.synchronizer.applyComplexUpdate(owner.publicKeyHash, owner,\n                    (s, committer) -> OwnerProof.build(writerPair, owner.publicKeyHash)\n                            .thenCompose(proof -> s.get(owner.publicKeyHash).props.get().addOwnedKeyAndCommit(owner.publicKeyHash, owner,\n                                    proof,\n                                    s.get(owner.publicKeyHash).hash, s.get(owner.publicKeyHash).sequence, network, committer, tid))\n                            .thenCompose(s2 -> {\n                                long t2 = System.currentTimeMillis();\n                                RelativeCapability nextChunk =\n                                        RelativeCapability.buildSubsequentChunk(crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), rootRKey);\n                                LocalDateTime timestamp = LocalDateTime.now();\n                                return CryptreeNode.createEmptyDir(MaybeMultihash.empty(), rootRKey, rootWKey, Optional.of(writerPair),\n                                                new FileProperties(directoryName, true, false, \"\", 0, timestamp, timestamp,\n                                                        false, Optional.empty(), Optional.empty(), Optional.empty()),\n                                                Optional.empty(), SymmetricKey.random(), nextChunk, rootBat, mirrorBatId, crypto.random, crypto.hasher)\n                                        .thenCompose(root -> {\n                                            LOG.info(\"Uploading entry point directory\");\n                                            return WriterData.createEmpty(owner.publicKeyHash, writerPair,\n                                                            network.dhtClient, network.hasher, tid)\n                                                    .thenCompose(empty -> committer.commit(owner.publicKeyHash, writerPair, empty, new CommittedWriterData(MaybeMultihash.empty(), empty, Optional.empty()), tid))\n                                                    .thenCompose(s3 -> root.commit(s3, committer, rootPointer, Optional.of(writerPair), network, tid)\n                                                            .thenApply(finalSnapshot -> {\n                                                                long t3 = System.currentTimeMillis();\n                                                                LOG.info(\"Uploading root dir metadata took \" + (t3 - t2) + \" mS\");\n                                                                return finalSnapshot;\n                                                            }))\n                                                    .thenCompose(x -> addRootEntryPointAndCommit(x.merge(s2), entry, current, loginPublic, owner, userRootKey, committer, network, tid));\n                                        });\n                            }));\n        }), network.dhtClient).thenApply(s -> TrieNodeImpl.empty().put(\"/\" + directoryName, entry)));\n    }\n\n    public CompletableFuture<PublicSigningKey> getSigningKey(PublicKeyHash owner) {\n        return network.dhtClient.get(owner, owner, Optional.empty()).thenApply(cborOpt -> cborOpt.map(PublicSigningKey::fromCbor).get());\n    }\n\n    public CompletableFuture<PublicBoxingKey> getBoxingKey(PublicKeyHash owner, PublicKeyHash keyhash) {\n        return network.dhtClient.get(owner, keyhash, Optional.empty()).thenApply(cborOpt -> cborOpt.map(PublicBoxingKey::fromCbor).get());\n    }\n\n    /**\n     *\n     * @param username\n     * @return A pair containing the identity key hash and the boxing key hash of the user\n     */\n    public CompletableFuture<Pair<PublicKeyHash, PublicKeyHash>> getPublicKeyHashes(String username) {\n        return network.coreNode.getPublicKeyHash(username)\n                .thenCompose(signerOpt -> {\n                    if (! signerOpt.isPresent())\n                        throw new IllegalStateException(\"Couldn't retrieve identity key for \" + username);\n                    return getSigningKey(signerOpt.get())\n                            .thenCompose(signer2 -> getWriterData(network, signerOpt.get(), signerOpt.get())\n                                    .thenApply(wd -> new Pair<>(signerOpt.get(), wd.props.get().followRequestReceiver.get())));\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<Pair<PublicKeyHash, PublicBoxingKey>>> getPublicKeys(String username) {\n        return network.coreNode.getPublicKeyHash(username)\n                .thenCompose(signerOpt ->\n                        signerOpt.map(signer -> getSigningKey(signer)\n                                .thenCompose(signer2 -> getWriterData(network, signerOpt.get(), signerOpt.get())\n                                        .thenCompose(wd -> getBoxingKey(signer, wd.props.get().followRequestReceiver.get())\n                                                .thenApply(boxer -> Optional.of(new Pair<>(signerOpt.get(), boxer))))))\n                                .orElse(CompletableFuture.completedFuture(Optional.empty())));\n    }\n\n    public CompletableFuture<CommittedWriterData> addNamedOwnedKeyAndCommit(String keyName,\n                                                                            SigningPrivateKeyAndPublicHash owned) {\n        return OwnerProof.build(owned, signer.publicKeyHash).thenCompose(proof -> writeSynchronizer.applyUpdate(signer.publicKeyHash, signer,\n                        (wd, tid) -> CompletableFuture.completedFuture(wd.addNamedKey(keyName, proof)))\n                .thenApply(v -> v.get(signer)));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> deleteAccount(String password,\n                                                    Function<MultiFactorAuthRequest, CompletableFuture<MultiFactorAuthResponse>> mfa) {\n        return signIn(username, password, mfa, network, crypto)\n                .thenCompose(user -> {\n                    // set mutable pointer of root dir writer and owner to EMPTY\n                    SigningPrivateKeyAndPublicHash identity = user.signer;\n                    PublicKeyHash owner = identity.publicKeyHash;\n                    return user.getUserRoot().thenCompose(root -> {\n                        SigningPrivateKeyAndPublicHash pair = root.signingPair();\n                        CommittedWriterData current = root.getVersionRoot();\n                        PointerUpdate cas = new PointerUpdate(current.hash, MaybeMultihash.empty(), PointerUpdate.increment(current.sequence));\n                        return pair.secret.signMessage(cas.serialize())\n                                .thenCompose(signed -> network.mutable.setPointer(this.signer.publicKeyHash, pair.publicKeyHash, signed));\n                    }).thenCompose(x -> network.spaceUsage.requestQuota(username, identity, 1_000_000, false))\n                            .thenCompose(x -> network.mutable.getPointerTarget(owner, owner, network.dhtClient)\n                                    .thenCompose(current -> {\n                                        PointerUpdate cas = new PointerUpdate(current.updated, MaybeMultihash.empty(), PointerUpdate.increment(current.sequence));\n                                        return identity.secret.signMessage(cas.serialize())\n                                                .thenCompose(signed -> network.mutable.setPointer(owner, owner, signed));\n                                    })\n                            );\n                });\n    }\n\n    public CompletableFuture<Snapshot> unPublishFile(Path path) {\n        return sharedWith(path).thenCompose(sharedWithState -> getByPath(path)\n                .thenCompose(opt -> {\n                    FileWrapper toUnshare = opt.get();\n                    return getByPath(path.getParent())\n                            .thenCompose(parentOpt -> {\n                                FileWrapper parent = parentOpt.get();\n                                return removePublicCap(path.toString())\n                                        .thenCompose(x -> network.synchronizer.applyComplexUpdate(signer.publicKeyHash,\n                                                parent.signingPair(),\n                                                (s, c) -> rotateAllKeys(toUnshare, parent, false, s, c)\n                                                        .thenCompose(markedDirty ->\n                                                                sharedWithCache.removeSharedWith(SharedWithCache.Access.READ, path, sharedWithState.readAccess, markedDirty, c, network)\n                                                                        .thenCompose(s2 ->\n                                                                                sharedWithCache.removeSharedWith(SharedWithCache.Access.WRITE, path, sharedWithState.writeAccess, s2, c, network))\n                                                        )\n                                        ));\n                            });\n                }));\n    }\n\n    private CompletableFuture<CommittedWriterData> removePublicCap(String path) {\n        return writeSynchronizer.applyUpdate(signer.publicKeyHash, signer, (wd, tid) -> {\n            Optional<Multihash> publicData = wd.publicData;\n            if (publicData.isEmpty())\n                return Futures.of(wd);\n            return network.dhtClient.get(signer.publicKeyHash, (Cid)publicData.get(), Optional.empty())\n                    .thenCompose(rootCbor -> InodeFileSystem.build(signer.publicKeyHash, rootCbor.get(), crypto.hasher, network.dhtClient))\n                    .thenCompose(pubCaps -> pubCaps.removeCap(signer.publicKeyHash, signer, path, tid))\n                    .thenCompose(updated -> network.dhtClient.put(signer.publicKeyHash, signer, updated.serialize(), crypto.hasher, tid))\n                    .thenApply(newRoot -> wd.withPublicRoot(newRoot));\n        }).thenApply(v -> v.get(signer));\n    }\n\n    public CompletableFuture<CommittedWriterData> makePublic(FileWrapper file) {\n        if (! file.getOwnerName().equals(username))\n            return Futures.errored(new IllegalStateException(\"Only the owner of a file can make it public!\"));\n        if (file.isUserRoot())\n            return Futures.errored(new IllegalStateException(\"You cannot publish your home directory!\"));\n        return writeSynchronizer.applyUpdate(signer.publicKeyHash, signer, (wd, tid) -> file.getPath(network).thenCompose(path -> {\n            ensureAllowedToShare(file, username, false);\n            Optional<Multihash> publicData = wd.publicData;\n\n            CompletableFuture<InodeFileSystem> publicCaps = publicData.isPresent() ?\n                    network.dhtClient.get(signer.publicKeyHash, (Cid)publicData.get(), Optional.empty())\n                            .thenCompose(rootCbor -> InodeFileSystem.build(signer.publicKeyHash, rootCbor.get(), crypto.hasher, network.dhtClient)) :\n                    InodeFileSystem.createEmpty(signer.publicKeyHash, signer, network.dhtClient, crypto.hasher, tid);\n\n            AbsoluteCapability cap = file.getPointer().capability.readOnly();\n            return publicCaps.thenCompose(pubCaps -> pubCaps.addCap(signer.publicKeyHash, signer, path, cap, tid))\n                    .thenCompose(updated -> network.dhtClient.put(signer.publicKeyHash, signer, updated.serialize(), crypto.hasher, tid))\n                    .thenApply(newRoot -> wd.withPublicRoot(newRoot));\n        })).thenApply(v -> v.get(signer));\n    }\n\n    private static void ensureAllowedToShare(FileWrapper file, String ourname, boolean isWrite) {\n        if (file.isUserRoot())\n            throw new IllegalStateException(\"You cannot share your home directory public!\");\n        if (isWrite && ! file.getOwnerName().equals(ourname))\n            throw new IllegalStateException(\"Only the owner of a file can grant write access!\");\n    }\n\n    @JsMethod\n    public CompletableFuture<Set<FileWrapper>> getFriendRoots() {\n        return getChildren(\"/\").thenApply(c -> c.stream()\n                .filter(f -> ! f.getName().equals(username))\n                .collect(Collectors.toSet()));\n    }\n\n    @JsMethod\n    public CompletableFuture<Set<String>> getFollowing() {\n        return getFriendRoots()\n                .thenApply(set -> set.stream()\n                        .map(FileWrapper::getOwnerName)\n                        .collect(Collectors.toSet()));\n    }\n\n    @JsMethod\n    public CompletableFuture<Set<String>> getFollowerNames() {\n        return getFollowerRoots(true).thenApply(Map::keySet);\n    }\n\n    public CompletableFuture<Map<String, FileWrapper>> getFollowerRoots(boolean filterPending) {\n        return (filterPending ?\n                getPendingOutgoingFollowRequests()\n                        .thenApply(p -> p.pendingOutgoingFollowRequests)\n                : Futures.of(Collections.<String>emptySet()))\n                .thenCompose(pendingOutgoing -> getFollowerRoots(pendingOutgoing));\n    }\n\n    private CompletableFuture<Map<String, FileWrapper>> getFollowerRoots(Set<String> pendingOutgoing) {\n        return getSharingFolder()\n                .thenCompose(sharing -> sharing.getChildren(crypto.hasher, network))\n                .thenApply(children -> children.stream()\n                        .filter(c -> ! c.getName().startsWith(\".\") &&\n                                ! c.getName().startsWith(GROUPS_FILENAME) &&\n                                ! pendingOutgoing.contains(c.getName()))\n                        .collect(Collectors.toMap(e -> e.getFileProperties().name, e -> e)));\n    }\n\n    public CompletableFuture<Set<FriendSourcedTrieNode>> getFollowingNodes() {\n        return Futures.of(entrie.getChildNodes()\n                .stream()\n                .filter(n -> n instanceof FriendSourcedTrieNode)\n                .map(n -> (FriendSourcedTrieNode)n)\n                .filter(n -> ! n.ownerName.equals(username))\n                .collect(Collectors.toSet()));\n    }\n\n    public CompletableFuture<Map<String, FriendAnnotation>> getFriendAnnotations() {\n        return getUserRoot()\n                .thenCompose(home -> home.hasChild(FRIEND_ANNOTATIONS_FILE_NAME, crypto.hasher, network)\n                        .thenCompose(exists ->  {\n                            if (! exists)\n                                return CompletableFuture.completedFuture(Collections.emptyMap());\n                            Map<String, FriendAnnotation> res = new TreeMap<>();\n                            return home.getChild(FRIEND_ANNOTATIONS_FILE_NAME, crypto.hasher, network)\n                                    .thenCompose(fileOpt ->\n                                            fileOpt.get().getInputStream(network, crypto, x -> {})\n                                    .thenCompose(reader -> reader.parseStream(FriendAnnotation::fromCbor,\n                                            anno -> res.put(anno.getUsername(), anno),\n                                            fileOpt.get().getSize())))\n                                    .thenApply(x -> res);\n                        }));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> addFriendAnnotation(FriendAnnotation annotation) {\n        return getFriendAnnotations().thenCompose(existing -> {\n            Map<String, FriendAnnotation> updated = new TreeMap<>(existing);\n            updated.put(annotation.getUsername(), annotation);\n            List<FriendAnnotation> values = updated.values()\n                    .stream()\n                    .collect(Collectors.toList());\n            ByteArrayOutputStream serialized = new ByteArrayOutputStream();\n            for (FriendAnnotation value : values) {\n                try {\n                    serialized.write(value.serialize());\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n            return getUserRoot().thenCompose(home -> home.uploadFileSection(\n                            FRIEND_ANNOTATIONS_FILE_NAME,\n                            AsyncReader.build(serialized.toByteArray()),\n                            true,\n                            0,\n                            serialized.size(),\n                            Optional.empty(),\n                            true,\n                            network,\n                            crypto,\n                            () -> false,\n                            x -> {},\n                            crypto.random.randomBytes(32),\n                            Optional.empty(),\n                            Optional.of(Bat.random(crypto.random)),\n                            mirrorBatId()))\n                    .thenApply(x -> true);\n        });\n    }\n\n    public CompletableFuture<Groups> getGroupNameMappings() {\n        return FileUtils.getOrCreateObject(this,\n                PathUtil.get(username, SHARED_DIR_NAME, GROUPS_FILENAME),\n                () -> Groups.generate(crypto.random),\n                this::initialiseGroups,\n                Cborable.parser(Groups::fromCbor));\n    }\n\n    private CompletableFuture<Set<String>> getFriendNames() {\n        return getFriendRoots()\n                .thenApply(dirs -> dirs.stream()\n                        .map(FileWrapper::getName)\n                        .collect(Collectors.toSet()));\n    }\n\n    private CompletableFuture<Boolean> initialiseGroups(Groups g) {\n        return getFollowerNames().thenCompose(followers -> getFriendNames()\n                .thenCompose(friends -> {\n                    return Futures.reduceAll(g.uidToGroupName.entrySet(), true,\n                            (b, e) -> getUserRoot()\n                                    .thenCompose(home -> home.getOrMkdirs(PathUtil.get(SHARED_DIR_NAME, e.getKey()), network, true, mirrorBatId(), crypto))\n                                    .thenCompose(x -> getUserRoot()\n                                            .thenCompose(home -> network.synchronizer.applyComplexUpdate(signer.publicKeyHash, home.signingPair(),\n                                                    (s, c) -> shareReadAccessWith(PathUtil.get(username, SHARED_DIR_NAME, e.getKey()),\n                                                            e.getValue().equals(SocialState.FOLLOWERS_GROUP_NAME) ? followers : friends, s, c))))\n                                    .thenApply(x -> true),\n                            (a, b) -> a && b);\n                }));\n    }\n\n    private CompletableFuture<PendingSocialState> getPendingOutgoingFollowRequests() {\n        return getUserRoot()\n                .thenCompose(home -> home.hasChild(SOCIAL_STATE_FILENAME, crypto.hasher, network)\n                        .thenCompose(exists ->  {\n                            if (! exists)\n                                return CompletableFuture.completedFuture(PendingSocialState.empty());\n                            return home.getChild(SOCIAL_STATE_FILENAME, crypto.hasher, network)\n                                    .thenCompose(fileOpt ->\n                                            fileOpt.get().getInputStream(network, crypto, x -> {})\n                                                    .thenCompose(reader -> Serialize.parse(reader, fileOpt.get().getSize(),\n                                                            PendingSocialState::fromCbor)));\n                        }));\n    }\n\n    private CompletableFuture<Boolean> removeFromPendingOutgoing(String usernameToRemove) {\n        return getUserRoot()\n                .thenCompose(home -> home.hasChild(SOCIAL_STATE_FILENAME, crypto.hasher, network)\n                        .thenCompose(exists ->  {\n                            if (! exists)\n                                return CompletableFuture.completedFuture(true);\n                            return home.getChild(SOCIAL_STATE_FILENAME, crypto.hasher, network)\n                                    .thenCompose(fileOpt ->\n                                            fileOpt.get().getInputStream(network, crypto, x -> {})\n                                                    .thenCompose(reader -> Serialize.parse(reader, fileOpt.get().getSize(),\n                                                            PendingSocialState::fromCbor))\n                                                    .thenCompose(current -> {\n                                                        PendingSocialState updated = current.withoutPending(usernameToRemove);\n                                                        byte[] raw = updated.serialize();\n                                                        return fileOpt.get().overwriteFile(AsyncReader.build(raw), raw.length,\n                                                                network, crypto, x -> {})\n                                                                .thenApply(x -> true);\n                                                    }));\n                        }));\n    }\n\n    @JsMethod\n    public CompletableFuture<SocialState> getSocialState() {\n        return processFollowRequests()\n                .thenCompose(pendingIncoming -> getPendingOutgoingFollowRequests()\n                        .thenCompose(pendingOutgoing -> getFollowerRoots(pendingOutgoing.pendingOutgoingFollowRequests)\n                                .thenCompose(followerRoots -> getFriendRoots().thenCompose(\n                                        followingRoots -> getFollowerNames().thenCompose(\n                                                followers -> getBlocked().thenCompose(\n                                                        blocked -> getFriendAnnotations().thenCompose(\n                                                                annotations -> getGroupNameMappings().thenApply(\n                                                                        groups -> new SocialState(pendingIncoming,\n                                                                                pendingOutgoing.pendingOutgoingFollowRequests,\n                                                                                followers, followerRoots, followingRoots,\n                                                                                blocked, annotations, groups.uidToGroupName)))))))));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> sendInitialFollowRequest(String targetUsername) {\n        if (username.equals(targetUsername)) {\n            return CompletableFuture.completedFuture(false);\n        }\n        return sendFollowRequest(targetUsername, SymmetricKey.random());\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> sendInitialFollowRequests(String[] targetUsernames) {\n        Set<String> usernames = new HashSet<>(Arrays.asList(targetUsernames));\n        return Futures.reduceAll(usernames,\n                true,\n                (b, targetUsername) -> sendFollowRequest(targetUsername, SymmetricKey.random()),\n                (a, b) -> a);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> sendReplyFollowRequest(FollowRequestWithCipherText initialRequestAndRaw, boolean accept, boolean reciprocate) {\n        FollowRequest initialRequest = initialRequestAndRaw.req;\n        String theirUsername = initialRequest.entry.get().ownerName;\n        // if accept, create directory to share with them, note in entry points (they follow us)\n        if (!accept && !reciprocate) {\n            // send a null entry and absent key (full rejection)\n            // write a null entry point and tell them we're not reciprocating with an absent key\n            EntryPoint entry = new EntryPoint(AbsoluteCapability.createNull(), username);\n            FollowRequest reply = new FollowRequest(Optional.of(entry), Optional.empty());\n\n            return getPublicKeys(initialRequest.entry.get().ownerName).thenCompose(pair -> {\n                PublicBoxingKey targetUser = pair.get().right;\n\n                return blindAndSendFollowRequest(initialRequest.entry.get().pointer.owner, targetUser, reply)\n                        .thenCompose(b ->\n                                // remove pending follow request from them\n                                signer.secret.signMessage(initialRequestAndRaw.cipher.serialize())\n                                        .thenCompose(signed -> network.social.removeFollowRequest(signer.publicKeyHash, signed))\n                        );\n            });\n        }\n\n        return CompletableFuture.completedFuture(true).thenCompose(b -> {\n            if (accept) {\n                return getSharingFolder().thenCompose(sharing -> sharing.getChild(theirUsername, crypto.hasher, network)\n                        .thenCompose(existingOpt -> {\n                            if (existingOpt.isPresent())\n                                return Futures.of(existingOpt);\n                            return sharing.getChild(theirUsername, crypto.hasher, network)\n                                    .thenCompose(existingFriendDir -> {\n                                        if (existingFriendDir.isEmpty())\n                                            return sharing.mkdir(theirUsername, network, initialRequest.key.get(), Optional.of(Bat.random(crypto.random)), true, mirrorBatId(), crypto)\n                                                    .thenCompose(updatedSharing -> updatedSharing.getChild(theirUsername, crypto.hasher, network));\n                                        // If we already have a sharing dir for them, don't rotate the keys\n                                        return Futures.of(existingFriendDir);\n                                    });\n                        }).thenCompose(friendRootOpt -> {\n                            FileWrapper friendRoot = friendRootOpt.get();\n                            // add a note to our entry point store so we know who we sent the read access to\n                            EntryPoint entry = new EntryPoint(friendRoot.getPointer().capability.readOnly(),\n                                    username);\n                            // add them to our followers group\n                            return getGroupUid(SocialState.FOLLOWERS_GROUP_NAME)\n                                    .thenCompose(followersUidOpt -> shareReadAccessWith(PathUtil.get(username,\n                                            SHARED_DIR_NAME, followersUidOpt.get()), Collections.singleton(theirUsername)))\n                                    .thenApply(x -> entry);\n                        }));\n            } else {\n                EntryPoint entry = new EntryPoint(AbsoluteCapability.createNull(), username);\n                return CompletableFuture.completedFuture(entry);\n            }\n        }).thenCompose(entry -> {\n\n            Optional<SymmetricKey> baseKey;\n            if (!reciprocate) {\n                baseKey = Optional.empty(); // tell them we're not reciprocating\n            } else {\n                // if reciprocate, add entry point to their shared directory (we follow them) and then\n                baseKey = Optional.of(initialRequest.entry.get().pointer.rBaseKey); // tell them we are reciprocating\n            }\n            FollowRequest reply = new FollowRequest(Optional.of(entry), baseKey);\n\n            return (accept && reciprocate ?\n                    getGroupUid(SocialState.FRIENDS_GROUP_NAME)\n                            .thenCompose(friendsUidOpt -> shareReadAccessWith(PathUtil.get(username,\n                                    SHARED_DIR_NAME, friendsUidOpt.get()), Collections.singleton(theirUsername))): // put them in our friends group\n                    Futures.of(null))\n                    .thenCompose(x -> getPublicKeys(initialRequest.entry.get().ownerName))\n                    .thenCompose(pair -> {\n                        PublicBoxingKey targetUser = pair.get().right;\n                        return blindAndSendFollowRequest(initialRequest.entry.get().pointer.owner, targetUser, reply);\n                    });\n        }).thenCompose(b -> {\n            if (reciprocate)\n                return addExternalEntryPoint(initialRequest.entry.get())\n                        .thenCompose(x -> retrieveAndAddEntryPointToTrie(entrie, initialRequest.entry.get()));\n            return CompletableFuture.completedFuture(entrie);\n        }).thenCompose(trie -> {\n            // remove original request\n            entrie = trie;\n            return signer.secret.signMessage(initialRequestAndRaw.cipher.serialize())\n                    .thenCompose(signed -> network.social.removeFollowRequest(signer.publicKeyHash, signed));\n        });\n    }\n\n    /**\n     * Send details to allow friend to follow us, and optionally let us follow them\n     * create a tmp keypair whose public key we can prepend to the request without leaking information\n     *\n     * @param targetIdentity\n     * @param targetBoxer\n     * @param req\n     * @return\n     */\n    private CompletableFuture<Boolean> blindAndSendFollowRequest(PublicKeyHash targetIdentity, PublicBoxingKey targetBoxer, FollowRequest req) {\n        return BlindFollowRequest.build(targetBoxer, req, crypto)\n                .thenCompose(blindRequest -> network.social.sendFollowRequest(targetIdentity, blindRequest.serialize()));\n    }\n\n    public CompletableFuture<Boolean> sendFollowRequest(String targetUsername, SymmetricKey requestedKey) {\n        return getSharingFolder().thenCompose(sharing -> {\n            // check for them not reciprocating\n            return getFollowing().thenCompose(following -> {\n                boolean alreadyFollowing = following.stream()\n                        .filter(x -> x.equals(targetUsername))\n                        .findAny()\n                        .isPresent();\n                if (alreadyFollowing) {\n                    return Futures.errored(new Exception(\"User \" + targetUsername +\" is already a follower!\"));\n                }\n                return getPublicKeys(targetUsername).thenCompose(targetUserOpt -> {\n                    if (! targetUserOpt.isPresent()) {\n                        return Futures.errored(new Exception(\"User \" + targetUsername + \" does not exist!\"));\n                    }\n                    PublicBoxingKey targetUser = targetUserOpt.get().right;\n                    return sharing.getOrMkdirs(PathUtil.get(targetUsername), network, true, mirrorBatId(), crypto)\n                            .thenCompose(friendRoot -> {\n\n                                EntryPoint entry = new EntryPoint(friendRoot.getPointer().capability.readOnly(), username);\n                                FollowRequest followReq = new FollowRequest(Optional.of(entry), Optional.ofNullable(requestedKey));\n\n                                PublicKeyHash targetSigner = targetUserOpt.get().left;\n                                return getPendingOutgoingFollowRequests()\n                                        .thenCompose(pending -> blindAndSendFollowRequest(targetSigner, targetUser, followReq)\n                                                .thenCompose(b -> {\n                                                    // note that we have a pending request sent to them\n                                                    PendingSocialState updated = pending.withPending(targetUsername);\n                                                    byte[] raw = updated.toCbor().serialize();\n                                                    return getUserRoot().thenCompose(home -> home.uploadFileSection(\n                                                                    SOCIAL_STATE_FILENAME, AsyncReader.build(raw), true, 0, raw.length, Optional.empty(),\n                                                                    true, network, crypto, () -> false, x -> {}, crypto.random.randomBytes(32),\n                                                                    Optional.empty(), Optional.of(Bat.random(crypto.random)), mirrorBatId()))\n                                                            .thenApply(x -> b);\n                                                }));\n                            });\n                });\n            });\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<SocialFeed> getSocialFeed() {\n        return getByPath(PathUtil.get(username, FEED_DIR_NAME))\n                .thenCompose(feedDirOpt -> {\n                    if (feedDirOpt.isEmpty())\n                        return SocialFeed.create(this);\n                    return SocialFeed.load(feedDirOpt.get(), this);\n                });\n    }\n\n    public CompletableFuture<Boolean> unShareReadAccess(Path path, String readerToRemove) {\n        return unShareReadAccessWith(path, Collections.singleton(readerToRemove)).thenApply(x -> true);\n    }\n\n    public CompletableFuture<Boolean> unShareWriteAccess(Path path, String writerToRemove) {\n        return unShareWriteAccessWith(path, Collections.singleton(writerToRemove)).thenApply(x -> true);\n    }\n\n    private CompletableFuture<Snapshot> rotateAllKeys(FileWrapper file,\n                                                      FileWrapper parent,\n                                                      boolean rotateSigners,\n                                                      Snapshot initial,\n                                                      Committer c) {\n        // 1) rotate all the symmetric keys and optionally signers\n        // 2) if parent signer is different, add a link node pointing to the new child\n        // 2) update parent pointer to new child/link\n        // 3) delete old subtree\n        PublicKeyHash owner = parent.owner();\n        SigningPrivateKeyAndPublicHash parentSigner = parent.signingPair();\n        AbsoluteCapability parentCap = parent.getPointer().capability;\n        AbsoluteCapability originalCap = file.getPointer().capability;\n        return (rotateSigners ?\n                CryptreeNode.initAndAuthoriseSigner(\n                        owner,\n                        parentSigner,\n                        SigningKeyPair.random(crypto.random, crypto.signer), network, initial, c) :\n                Futures.of(new Pair<>(initial, file.signingPair())))\n                .thenCompose(p -> {\n                    Optional<RelativeCapability> newParentLink = Optional.of(\n                            rotateSigners ?\n                                    new RelativeCapability(\n                                            Optional.of(parent.writer()),\n                                            crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH),\n                                            Optional.of(Bat.random(crypto.random)),\n                                            SymmetricKey.random(),\n                                            Optional.empty()):\n                                    new RelativeCapability(\n                                            Optional.empty(),\n                                            parent.getPointer().capability.getMapKey(),\n                                            parent.getPointer().capability.bat,\n                                            parent.getParentKey(),\n                                            Optional.empty())\n                    );\n                    return file.getPointer().fileAccess.rotateAllKeys(\n                            true,\n                            new CryptreeNode.CapAndSigner((WritableAbsoluteCapability)\n                                    originalCap, file.signingPair()),\n                            new CryptreeNode.CapAndSigner(new WritableAbsoluteCapability(\n                                    owner,\n                                    p.right.publicKeyHash,\n                                    crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH),\n                                    Optional.of(Bat.random(crypto.random)),\n                                    SymmetricKey.random(),\n                                    SymmetricKey.random()),\n                                    p.right),\n                            new CryptreeNode.CapAndSigner((WritableAbsoluteCapability) parentCap, parent.signingPair()),\n                            new CryptreeNode.CapAndSigner((WritableAbsoluteCapability) parentCap, parent.signingPair()),\n                            newParentLink,\n                            Optional.empty(),\n                            mirrorBatId(),\n                            rotateSigners,\n                            network,\n                            crypto,\n                            p.left,\n                            c)\n                            .thenCompose(rotated -> {\n                                // add a link in same writing space as parent to restrict rename access\n                                if (rotateSigners) {\n                                    SymmetricKey linkRBase = SymmetricKey.random();\n                                    SymmetricKey linkParent = newParentLink.get().rBaseKey;\n                                    SymmetricKey linkWBase = SymmetricKey.random();\n                                    byte[] linkMapKey = newParentLink.get().getMapKey();\n                                    Optional<Bat> linkBat = newParentLink.get().bat;\n                                    WritableAbsoluteCapability linkCap = new WritableAbsoluteCapability(owner,\n                                            parentCap.writer, linkMapKey, linkBat, linkRBase, linkWBase);\n                                    return CryptreeNode.createAndCommitLink(parent, rotated.right,\n                                            file.getFileProperties(), linkCap, linkParent,\n                                            mirrorBatId(), crypto, network, rotated.left, c)\n                                            .thenApply(newSnapshot -> new Pair<>(newSnapshot, linkCap));\n                                } else\n                                    return Futures.of(rotated);\n                            });\n                }).thenCompose(rotated ->\n                                parent.getPointer().fileAccess.updateChildLink(\n                                        rotated.left, c, (WritableAbsoluteCapability) parentCap,\n                                        parentSigner, file.isLink() ? file.getLinkPointer().capability : originalCap,\n                                        new NamedAbsoluteCapability(file.getName(), rotated.right,\n                                                Optional.of(file.getFileProperties().isDirectory),\n                                                Optional.of(file.getFileProperties().mimeType),\n                                                Optional.of(file.getFileProperties().created)),\n                                        network, crypto.random, crypto.hasher)\n                                        .thenCompose(s -> IpfsTransaction.call(owner,\n                                                tid -> FileWrapper.deleteAllChunks(\n                                                        file.writableFilePointer(),\n                                                        file.signingPair(),\n                                                        tid, crypto.hasher, network, s, c), network.dhtClient))\n                                        .thenCompose(s -> rotateSigners ?\n                                                CryptreeNode.deAuthoriseSigner(owner, parentSigner, file.writer(),\n                                                        network, s, c) :\n                                                Futures.of(s)));\n    }\n\n    /**\n     * Remove read access to a file for the supplied readers.\n     * The readers can include the inbuilt friend groupUid and followers groupUid\n     * If the friend group is supplied - in addition to removing read access for the friend group,\n     * all individual friends that currently have read access will lose read access\n     * If the followers group is supplied - in addition to removing read access for the follower group AND friend\n     * group all individual users that currently have read access will lose read access\n     *\n     * @param path - The path to the file/dir to revoke access to\n     * @param initialReadersToRemove - The usernames or groupUids to revoke read access to\n     * @return The resulting snapshot of the filesystem after rotating keys\n     */\n    @JsMethod\n    public CompletableFuture<Snapshot> unShareReadAccessWith(Path path, Set<String> initialReadersToRemove) {\n        //does list of readers include groups?\n        boolean hasGroups = initialReadersToRemove.stream().anyMatch(i -> i.startsWith(\".\"));\n        return (hasGroups ?\n                getSocialState().thenCompose(social -> sharedWith(path)\n                        .thenApply(fileSharingState ->\n                                gatherAllUsernamesToUnshare(social, fileSharingState.readAccess, initialReadersToRemove)\n                        )) :\n                Futures.of(initialReadersToRemove))\n                .thenCompose(users -> getUserRoot()\n                        .thenCompose(home -> network.synchronizer.applyComplexUpdate(signer.publicKeyHash, home.signingPair(),\n                                (s, c) -> unShareReadAccessWith(path, users, s, c))));\n    }\n\n    public CompletableFuture<Snapshot> unShareReadAccessWith(Path path,\n                                                             Set<String> readersToRemove,\n                                                             Snapshot s,\n                                                             Committer c) {\n        String pathString = path.toString();\n        String absolutePathString = pathString.startsWith(\"/\") ? pathString : \"/\" + pathString;\n        return getByPath(absolutePathString, s).thenCompose(opt -> {\n            FileWrapper toUnshare = opt.orElseThrow(() -> new IllegalStateException(\"Specified un-shareWith path \" + absolutePathString + \" does not exist\"));\n            // now change to new base keys, clean some keys and mark others as dirty\n            return getByPath(path.getParent().toString(), s)\n                    .thenCompose(parent -> rotateAllKeys(toUnshare, parent.get(), false, toUnshare.version, c)\n                            .thenCompose(markedDirty -> {\n                                return sharedWithCache.removeSharedWith(SharedWithCache.Access.READ, path, readersToRemove, markedDirty, c, network)\n                                        .thenCompose(s2 -> reSendAllSharesAndLinksRecursive(path, s2, c));\n                            }));\n        });\n    }\n\n    /**\n     * Remove write access to a file for the supplied writers.\n     * The readers can include the inbuilt friend groupUid and followers groupUid\n     * If the friend group is supplied - in addition to removing write access for the friend group,\n     * all individual friends that currently have write access will lose write access\n     * If the followers group is supplied - in addition to removing write access for the follower group AND friend\n     * group all individual users that currently have write access will lose write access\n     *\n     * @param path - The path to the file/dir to revoke access to\n     * @param initialWritersToRemove - The usernames or groupUids to revoke write access to\n     * @return\n     */\n    @JsMethod\n    public CompletableFuture<Snapshot> unShareWriteAccessWith(Path path, Set<String> initialWritersToRemove) {\n        //does list of readers include groups?\n        boolean hasGroups = initialWritersToRemove.stream().anyMatch(i -> i.startsWith(\".\"));\n        return (hasGroups ?\n                getSocialState().thenCompose(social -> sharedWith(path)\n                        .thenApply(fileSharingState ->\n                            gatherAllUsernamesToUnshare(social, fileSharingState.writeAccess, initialWritersToRemove)\n                        )) :\n                Futures.of(initialWritersToRemove))\n                .thenCompose(writersToRemove -> {\n                    // 1. Authorise new writer pair as an owned key to parent's writer\n                    // 2. Rotate all keys (except data keys which are marked as dirty)\n                    // 3. Update link from parent to point to new rotated child\n                    // 4. Delete old file and subtree\n                    // 5. Remove old writer from parent owned keys\n                    String pathString = path.toString();\n                    String absolutePathString = pathString.startsWith(\"/\") ? pathString : \"/\" + pathString;\n                    return getByPath(absolutePathString).thenCompose(opt -> {\n                        FileWrapper toUnshare = opt.orElseThrow(() -> new IllegalStateException(\"Specified un-shareWith path \" + absolutePathString + \" does not exist\"));\n                        return getByPath(path.getParent().toString())\n                                .thenCompose(parentOpt -> {\n                                    FileWrapper parent = parentOpt.get();\n                                    return network.synchronizer.applyComplexUpdate(signer.publicKeyHash,\n                                            parent.signingPair(), (s, c) -> rotateAllKeys(toUnshare, parent, true, s, c)\n                                            .thenCompose(s2 ->\n                                                    sharedWithCache.removeSharedWith(SharedWithCache.Access.WRITE,\n                                                            path, writersToRemove, s2, c, network))\n                                                    .thenCompose(s3 -> reSendAllSharesAndLinksRecursive(path, s3, c)));\n                                });\n                    });\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<FileSharedWithState> sharedWith(Path p) {\n        return getUserRoot().thenCompose(home -> sharedWithCache.getSharedWith(p, home.version));\n    }\n\n    @JsMethod\n    public CompletableFuture<SharedWithState> getDirectorySharingState(Path dir) {\n        // The global root and home folders cannot be shared\n        if (dir.getNameCount() == 0 || username == null)\n            return Futures.of(SharedWithState.empty());\n        return getUserRoot().thenCompose(home -> sharedWithCache.getDirSharingState(dir, home.version));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> processShared(BiConsumer<String, SharedWithState> processor) {\n        return getUserRoot().thenCompose(home -> sharedWithCache.processShared(processor, username, home.version));\n    }\n\n    @JsMethod\n    public CompletableFuture<Snapshot> shareReadAccessWithFriends(Path path) {\n        return getSocialState()\n                .thenApply(s -> s.getFriendsGroupUid())\n                .thenCompose(friendsGroupUid -> getByPath(path.toString())\n                        .thenCompose(file -> getUserRoot()\n                                .thenCompose(home -> network.synchronizer.applyComplexUpdate(signer.publicKeyHash, home.signingPair(),\n                                        (s, c) -> shareReadAccessWith(file.orElseThrow(() ->\n                                new IllegalStateException(\"Could not find path \" + path)), path, Collections.singleton(friendsGroupUid), s, c)))));\n    }\n\n    @JsMethod\n    public CompletableFuture<Snapshot> shareReadAccessWithFollowers(Path path) {\n        return getSocialState()\n                .thenApply(s -> s.getFollowersGroupUid())\n                .thenCompose(followersGroupUid -> getByPath(path.toString())\n                        .thenCompose(file -> getUserRoot()\n                                .thenCompose(home -> network.synchronizer.applyComplexUpdate(signer.publicKeyHash, home.signingPair(),\n                                        (s, c) -> shareReadAccessWith(file.orElseThrow(() ->\n                                                new IllegalStateException(\"Could not find path \" + path.toString())),\n                                                path, Collections.singleton(followersGroupUid),s , c)))));\n    }\n\n    /*\n        Taking into account currently shared users/groups and users/groups selected for unsharing, build a list that is group aware\n        Note: Only inbuilt groups of friends and followers are currently handled\n     */\n    private Set<String> gatherAllUsernamesToUnshare(SocialState social,\n                                                    Set<String> currentSharedWithUsernames,\n                                                    Set<String> usernamesToUnshare) {\n\n        Set<String> followers = social.getFollowers();\n        Set<String> friends = social.getFriends();\n\n        String friendGroupUid = social.getFriendsGroupUid();\n        String followersGroupUid = social.getFollowersGroupUid();\n\n        Set<String> usersToUnshare = new HashSet<>(usernamesToUnshare);\n        if (usernamesToUnshare.contains(friendGroupUid)) {\n            HashSet<String> toAdd = new HashSet<>(currentSharedWithUsernames);\n            toAdd.retainAll(friends);\n            usersToUnshare.addAll(toAdd);\n        }\n        if (usernamesToUnshare.contains(followersGroupUid)) {\n            HashSet<String> toAdd = new HashSet<>(currentSharedWithUsernames);\n            toAdd.retainAll(followers);\n            usersToUnshare.addAll(toAdd);\n            if (currentSharedWithUsernames.contains(friendGroupUid)) {\n                usersToUnshare.add(friendGroupUid);\n            }\n        }\n        return usersToUnshare;\n    }\n\n    @JsMethod\n    public CompletableFuture<Snapshot> shareReadAccessWith(Path path, Set<String> readersToAdd) {\n        return getUserRoot()\n                .thenCompose(home -> network.synchronizer.applyComplexUpdate(signer.publicKeyHash, home.signingPair(),\n                        (s, c) -> shareReadAccessWith(path, readersToAdd, s, c)));\n    }\n\n    public CompletableFuture<Snapshot> shareReadAccessWith(Path path, Set<String> readersToAdd, Snapshot s, Committer c) {\n        if (readersToAdd.isEmpty())\n            return Futures.of(s);\n\n        return getByPath(path.toString(), s)\n                .thenCompose(file -> shareReadAccessWith(file.orElseThrow(() ->\n                        new IllegalStateException(\"Could not find path \" + path)), path, readersToAdd, s, c));\n    }\n\n    public CompletableFuture<Snapshot> reSendAllSharesAndLinksRecursive(Path start, Snapshot in, Committer c) {\n        return sharedWithCache.getAllDescendantShares(start, in)\n                .thenCompose(toReshare -> Futures.reduceAll(toReshare.entrySet(),\n                        in,\n                        (s, e) -> reshareAndUpdateLinks(e.getKey(), e.getValue(), s, c),\n                        (a, b) -> b));\n    }\n\n    private CompletableFuture<Snapshot> reshareAndUpdateLinks(Path start, SharedWithState file, Snapshot in, Committer c) {\n        return Futures.reduceAll(file.readShares().entrySet(), in,\n                        (s, e) -> shareReadAccessWith(start.resolve(e.getKey()), e.getValue(), s, c),\n                        (a, b) -> b)\n                .thenCompose(s2 -> Futures.reduceAll(file.writeShares().entrySet(),\n                        s2,\n                        (s, e) -> sendWriteCapToAll(start.resolve(e.getKey()), e.getValue(), s, c),\n                        (a, b) -> b))\n                .thenCompose(s3 -> Futures.reduceAll(file.links().entrySet(), s3,\n                        (s, e) -> Futures.reduceAll(e.getValue(),\n                                s,\n                                (v, p) -> updateSecretLink(start.resolve(e.getKey()).toString(), p, v, c).thenApply(x -> x.left),\n                                (a, b) -> b),\n                        (a, b) -> b));\n    }\n\n    private CompletableFuture<Snapshot> shareReadAccessWith(FileWrapper file,\n                                                            Path p,\n                                                            Set<String> readersToAdd,\n                                                            Snapshot in,\n                                                            Committer c) {\n        ensureAllowedToShare(file, username, false);\n        BiFunction<FileWrapper, FileWrapper, CompletableFuture<Snapshot>> sharingFunction = (sharedDir, fileWrapper) ->\n                CapabilityStore.addReadOnlySharingLinkTo(sharedDir, fileWrapper.getPointer().capability,\n                        c, network, crypto);\n        return Futures.reduceAll(readersToAdd,\n                in,\n                (s, username) -> shareAccessWith(file, username, sharingFunction, s),\n                (a, b) -> a.mergeAndOverwriteWith(b))\n                .thenCompose(result -> updatedSharedWithCache(file, p, readersToAdd, SharedWithCache.Access.READ, result, c));\n    }\n\n    @JsMethod\n    public CompletableFuture<Snapshot> shareWriteAccessWith(Path fileToShare,\n                                                            Set<String> writersToAdd) {\n        return getByPath(fileToShare.getParent())\n                .thenCompose(parentOpt -> ! parentOpt.isPresent() ?\n                                Futures.errored(new IllegalStateException(\"Unable to read \" + fileToShare.getParent())) :\n                parentOpt.get().getChild(fileToShare.getFileName().toString(), crypto.hasher, network)\n                        .thenCompose(fileOpt -> ! fileOpt.isPresent() ?\n                                Futures.errored(new IllegalStateException(\"Unable to read \" + fileToShare)) :\n                                shareWriteAccessWith(fileOpt.get(), fileToShare, parentOpt.get(), writersToAdd)));\n    }\n\n    private CompletableFuture<Snapshot> shareWriteAccessWith(FileWrapper file,\n                                                             Path pathToFile,\n                                                             FileWrapper parent,\n                                                             Set<String> writersToAdd) {\n        // There are 2 situations:\n        // a) file already has a different signing key than parent,\n        //    in which case, just share a write cap\n        //\n        // b) file needs to be moved to a different signing key (along with its subtree)\n        // To do this atomically,\n        // 1) rotate all the keys as if we were revoking write access\n        // 2) update parent pointer\n        // 3) delete old subtree\n        // 4) then reshare all sub sharees\n        System.out.println(\"Sharing write: \" + parent.writer() + \" child \" + file.writer());\n        ensureAllowedToShare(file, username, true);\n        SigningPrivateKeyAndPublicHash currentSigner = file.signingPair();\n        boolean changeSigner = currentSigner.publicKeyHash.equals(parent.signingPair().publicKeyHash);\n\n        if (! changeSigner) {\n            return network.synchronizer.applyComplexUpdate(signer.publicKeyHash,\n                    parent.signingPair(), (s, c) -> sharedWithCache.addSharedWith(SharedWithCache.Access.WRITE, pathToFile, writersToAdd, s, c, network)\n                    .thenCompose(s2 -> sendWriteCapToAll(pathToFile, writersToAdd, s2, c)));\n        }\n\n        return network.synchronizer.applyComplexUpdate(signer.publicKeyHash,\n                file.signingPair(), (s, c) -> rotateAllKeys(file, parent, true, s, c)\n                        .thenCompose(s2 -> getByPath(pathToFile.toString(), s2)\n                                .thenCompose(newFileOpt -> {\n                                    System.out.println(\"New child writer: \" + newFileOpt.get().writer());\n                                    return sharedWithCache\n                                            .addSharedWith(SharedWithCache.Access.WRITE, pathToFile, writersToAdd, s2, c, network);\n                                })\n                                .thenCompose(s3 -> reSendAllSharesAndLinksRecursive(pathToFile, s3, c))\n                        ));\n    }\n\n    public CompletableFuture<Snapshot> sendWriteCapToAll(Path toFile, Set<String> writersToAdd, Snapshot s, Committer c) {\n        if (writersToAdd.isEmpty())\n            return Futures.of(s);\n\n        System.out.println(\"Resharing WRITE cap to \" + toFile + \" with \" + writersToAdd);\n        return getByPath(toFile.getParent().toString(), s)\n                .thenCompose(parent -> getByPath(toFile.toString(), s)\n                        .thenCompose(fileOpt -> fileOpt.map(file -> sendWriteCapToAll(file, parent.get(), toFile, writersToAdd, s, c))\n                                .orElseGet(() -> Futures.errored(\n                                        new IllegalStateException(\"Couldn't retrieve file at \" + toFile)))));\n    }\n\n    public CompletableFuture<Snapshot> sendWriteCapToAll(FileWrapper file,\n                                                         FileWrapper parent,\n                                                         Path pathToFile,\n                                                         Set<String> writersToAdd,\n                                                         Snapshot in,\n                                                         Committer c) {\n        if (parent.writer().equals(file.writer()))\n            return Futures.errored(\n                    new IllegalStateException(\"A file must have different writer than its parent to grant write access!\"));\n        BiFunction<FileWrapper, FileWrapper, CompletableFuture<Snapshot>> sharingFunction =\n                (sharedDir, fileToShare) -> CapabilityStore.addEditSharingLinkTo(sharedDir,\n                        file.writableFilePointer(), c, network, crypto);\n        return Futures.reduceAll(writersToAdd,\n                in,\n                (s, username) -> shareAccessWith(file, username, sharingFunction, s),\n                (a, b) -> a.mergeAndOverwriteWith(b))\n                .thenCompose(result -> updatedSharedWithCache(file, pathToFile, writersToAdd, SharedWithCache.Access.WRITE, result, c));\n    }\n\n    private CompletableFuture<Snapshot> updatedSharedWithCache(FileWrapper file,\n                                                               Path pathToFile,\n                                                               Set<String> usersToAdd,\n                                                               SharedWithCache.Access access,\n                                                               Snapshot s,\n                                                               Committer c) {\n        return sharedWithCache.addSharedWith(access, pathToFile, usersToAdd, s, c, network);\n    }\n\n    public CompletableFuture<Snapshot> shareAccessWith(FileWrapper file,\n                                                       String usernameToGrantAccess,\n                                                       BiFunction<FileWrapper, FileWrapper, CompletableFuture<Snapshot>> sharingFunction,\n                                                       Snapshot s) {\n        return getByPath(\"/\" + username + \"/shared/\" + usernameToGrantAccess, s)\n                .thenCompose(shared -> {\n                    if (!shared.isPresent())\n                        return Futures.errored(new IllegalStateException(\"Unknown recipient for sharing: \" + usernameToGrantAccess));\n                    FileWrapper sharedDir = shared.get();\n                    return sharingFunction.apply(sharedDir, file);\n                });\n    }\n\n    private static CompletableFuture<Snapshot> addRootEntryPointAndCommit(Snapshot version,\n                                                                          EntryPoint entry,\n                                                                          UserStaticData current,\n                                                                          PublicSigningKey loginPublic,\n                                                                          SigningPrivateKeyAndPublicHash owner,\n                                                                          SymmetricKey rootKey,\n                                                                          Committer c,\n                                                                          NetworkAccess network,\n                                                                          TransactionId tid) {\n        CommittedWriterData cwd = version.get(owner.publicKeyHash);\n        WriterData wd = cwd.props.get();\n        if (wd.staticData.isEmpty()) {\n            UserStaticData updated = new UserStaticData(current.getData(rootKey).addEntryPoint(entry), rootKey);\n            return network.account.setLoginData(new LoginData(entry.ownerName, updated, loginPublic, Optional.empty()), owner, false)\n                    .thenApply(b -> version);\n        } else {\n            // legacy account\n            Optional<UserStaticData> updated = wd.staticData.map(sd -> new UserStaticData(sd.getData(rootKey).addEntryPoint(entry), rootKey));\n            return c.commit(owner.publicKeyHash, owner, wd.withStaticData(updated), cwd, tid);\n        }\n    }\n\n    private static CompletableFuture<Snapshot> updateBoxerAndCommit(Snapshot version,\n                                                                    String username,\n                                                                    BoxingKeyPair newBoxer,\n                                                                    UserStaticData current,\n                                                                    PublicSigningKey loginPublic,\n                                                                    SigningPrivateKeyAndPublicHash owner,\n                                                                    SymmetricKey rootKey,\n                                                                    Committer c,\n                                                                    Crypto crypto,\n                                                                    NetworkAccess network,\n                                                                    TransactionId tid) {\n        CommittedWriterData cwd = version.get(owner.publicKeyHash);\n        WriterData wd = cwd.props.get();\n        if (wd.staticData.isEmpty()) {\n            UserStaticData updated = new UserStaticData(current.getData(rootKey).withBoxer(newBoxer), rootKey);\n            return network.account.setLoginData(new LoginData(username, updated, loginPublic, Optional.empty()), owner, false)\n                    .thenCompose(x -> crypto.hasher.sha256(newBoxer.publicBoxingKey.serialize())\n                            .thenCompose(boxerHash -> owner.secret.signMessage(boxerHash)\n                                    .thenCompose(signedBoxerHash -> network.dhtClient.putBoxingKey(owner.publicKeyHash, signedBoxerHash, newBoxer.publicBoxingKey, tid)\n                                            .thenCompose(kh -> c.commit(owner.publicKeyHash, owner, wd.withBoxer(Optional.of(kh)), cwd, tid)))))\n                    .thenApply(b -> version);\n        } else {\n            // legacy account\n            Optional<UserStaticData> updated = wd.staticData.map(sd -> new UserStaticData(sd.getData(rootKey).withBoxer(newBoxer), rootKey));\n            return crypto.hasher.sha256(newBoxer.publicBoxingKey.serialize())\n                    .thenCompose(boxerHash -> owner.secret.signMessage(boxerHash)\n                            .thenCompose(signedBoxerHash -> network.dhtClient.putBoxingKey(owner.publicKeyHash, signedBoxerHash, newBoxer.publicBoxingKey, tid)\n                                    .thenCompose(kh -> c.commit(owner.publicKeyHash, owner,\n                                            wd.withStaticData(updated).withBoxer(Optional.of(kh)), cwd, tid))));\n        }\n    }\n\n    private synchronized CompletableFuture<FileWrapper> addExternalEntryPoint(EntryPoint entry) {\n        boolean isOurs = username.equals(entry.ownerName);\n        if (isOurs)\n            throw new IllegalStateException(\"Cannot add an entry point to your own filesystem!\");\n        String filename = ENTRY_POINTS_FROM_FRIENDS_FILENAME;\n        // verify owner before adding\n        return entry.isValid(\"/\" + entry.ownerName, network)\n                .thenCompose(valid -> valid ?\n                        getByPath(PathUtil.get(username, filename)) :\n                        Futures.errored(new IllegalStateException(\"Incorrect claimed owner for entry point\")))\n                .thenCompose(existing -> {\n                    long offset = existing.map(f -> f.getSize()).orElse(0L);\n                    byte[] data = entry.serialize();\n                    AsyncReader reader = AsyncReader.build(data);\n                    Optional<SymmetricKey> base = existing.map(f -> f.getPointer().capability.rBaseKey);\n                    return getUserRoot().thenCompose(home ->\n                            home.uploadFileSection(filename, reader, true, offset,\n                                    offset + data.length, base, true, network, crypto, () -> false, x -> {},\n                                    crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)), mirrorBatId()));\n                });\n    }\n\n    public CompletableFuture<List<BlindFollowRequest>> getFollowRequests() {\n        return TimeLimitedClient.signNow(signer.secret)\n                .thenCompose(auth -> network.social.getFollowRequests(signer.publicKeyHash, auth)).thenApply(reqs -> {\n                    CborObject cbor = CborObject.fromByteArray(reqs);\n                    if (!(cbor instanceof CborObject.CborList))\n                        throw new IllegalStateException(\"Invalid cbor for list of follow requests: \" + cbor);\n                    return ((CborObject.CborList) cbor).value.stream()\n                            .map(BlindFollowRequest::fromCbor)\n                            .collect(Collectors.toList());\n                });\n    }\n\n    /**\n     * Process any responses to our follow requests.\n     *\n     * @return initial follow requests\n     */\n    public CompletableFuture<List<FollowRequestWithCipherText>> processFollowRequests() {\n        return getFollowRequests().thenCompose(this::processFollowRequests);\n    }\n\n    private CompletableFuture<List<FollowRequestWithCipherText>> processFollowRequests(List<BlindFollowRequest> all) {\n        return getSharingFolder().thenCompose(sharing ->\n                getFollowerRoots(false).thenCompose(followerRoots -> getPendingOutgoingFollowRequests()\n                        .thenCompose(pendingOut -> Futures.combineAllInOrder(all.stream()\n                                        .map(b -> b.followRequest.decrypt(boxer.secretBoxingKey, b.dummySource, FollowRequest::fromCbor)\n                                                .thenApply(decrypted -> Optional.of(new FollowRequestWithCipherText(decrypted, b)))\n                                                .exceptionally(t -> Optional.empty()))\n                                        .collect(Collectors.toList()))\n                                .thenApply(opts -> opts.stream().flatMap(Optional::stream).collect(Collectors.toList())).thenCompose(withDecrypted -> {\n\n                            List<FollowRequestWithCipherText> replies = withDecrypted.stream()\n                                    .filter(p -> pendingOut.pendingOutgoingFollowRequests.contains(p.req.entry.get().ownerName))\n                                    .collect(Collectors.toList());\n\n                            BiFunction<TrieNode, FollowRequestWithCipherText, CompletableFuture<TrieNode>> addToStatic = (root, p) -> {\n                                FollowRequest freq = p.req;\n                                if (! Arrays.equals(freq.entry.get().pointer.rBaseKey.serialize(), SymmetricKey.createNull().serialize())) {\n                                    CompletableFuture<TrieNode> updatedRoot = freq.entry.get().ownerName.equals(username) ?\n                                            CompletableFuture.completedFuture(root) : // ignore responses claiming to be owned by us\n                                            addExternalEntryPoint(freq.entry.get())\n                                                    .thenCompose(x -> removeFromPendingOutgoing(freq.entry.get().ownerName))\n                                                    .thenCompose(x -> retrieveAndAddEntryPointToTrie(root, freq.entry.get()));\n                                    return updatedRoot.thenCompose(newRoot -> {\n                                        entrie = newRoot;\n                                        // clear their response follow req too\n                                        return signer.secret.signMessage(p.cipher.serialize())\n                                                .thenCompose(signed -> network.social.removeFollowRequest(signer.publicKeyHash, signed))\n                                                .thenApply(b -> newRoot);\n                                    });\n                                }\n                                return signer.secret.signMessage(p.cipher.serialize())\n                                        .thenCompose(signed -> network.social.removeFollowRequest(signer.publicKeyHash, signed))\n                                        .thenApply(b -> root);\n                            };\n\n                            BiFunction<TrieNode, FollowRequestWithCipherText, CompletableFuture<TrieNode>> mozart = (trie, p) -> {\n                                FollowRequest freq = p.req;\n                                // delete our folder if they didn't reciprocate\n                                String theirName = freq.entry.get().ownerName;\n                                FileWrapper ourDirForThem = followerRoots.get(theirName);\n                                byte[] ourKeyForThem = ourDirForThem.getKey().serialize();\n                                Optional<byte[]> keyFromResponse = freq.key.map(Cborable::serialize);\n                                if (keyFromResponse.isEmpty()) {\n                                    // They didn't reciprocate (follow us)\n                                    CompletableFuture<FileWrapper> removeDir = ourDirForThem.remove(sharing,\n                                            PathUtil.get(username, SHARED_DIR_NAME, theirName), this);\n\n                                    return removeDir.thenCompose(x -> removeFromPendingOutgoing(freq.entry.get().ownerName))\n                                            .thenCompose(b -> addToStatic.apply(trie, p));\n                                } else if (freq.entry.get().pointer.isNull()) {\n                                    // They reciprocated, but didn't accept (they follow us, but we can't follow them)\n                                    // add entry point to static data to signify their acceptance\n                                    // and finally remove the follow request\n                                    EntryPoint entryWeSentToThem = new EntryPoint(ourDirForThem.getPointer().capability.readOnly(),\n                                            username);\n                                    // add them to followers group\n                                    return getGroupUid(SocialState.FOLLOWERS_GROUP_NAME)\n                                            .thenCompose(followersUidOpt -> shareReadAccessWith(PathUtil.get(username,\n                                                    SHARED_DIR_NAME, followersUidOpt.get()), Collections.singleton(theirName)))\n                                            .thenCompose(x -> signer.secret.signMessage(p.cipher.serialize())\n                                                    .thenCompose(signed -> network.social.removeFollowRequest(signer.publicKeyHash, signed)))\n                                            .thenApply(x -> trie);\n                                } else {\n                                    // they accepted and reciprocated\n                                    // add entry point to static data to signify their acceptance\n                                    EntryPoint entryWeSentToThem = new EntryPoint(ourDirForThem.getPointer().capability.readOnly(),\n                                            username);\n\n                                    // add new entry point to tree root\n                                    EntryPoint entry = freq.entry.get();\n                                    if (entry.ownerName.equals(username))\n                                        throw new IllegalStateException(\"Received a follow request claiming to be owned by us!\");\n                                    // add them to followers and friends group\n                                    return getGroupUid(SocialState.FOLLOWERS_GROUP_NAME)\n                                            .thenCompose(followersUidOpt -> shareReadAccessWith(PathUtil.get(username,\n                                                    SHARED_DIR_NAME, followersUidOpt.get()), Collections.singleton(theirName)))\n                                            .thenCompose(x -> getGroupUid(SocialState.FRIENDS_GROUP_NAME)\n                                                    .thenCompose(friendsUidOpt -> shareReadAccessWith(PathUtil.get(username,\n                                                            SHARED_DIR_NAME, friendsUidOpt.get()), Collections.singleton(theirName))))\n                                            .thenCompose(x -> addToStatic.apply(trie, p.withEntryPoint(entry)))\n                                            .exceptionally(t -> trie);\n                                }\n                            };\n                            List<FollowRequestWithCipherText> initialRequests = withDecrypted.stream()\n                                    .filter(p -> !followerRoots.containsKey(p.req.entry.get().ownerName))\n                                    .collect(Collectors.toList());\n                            return Futures.reduceAll(replies, entrie, mozart, (a, b) -> a)\n                                    .thenApply(newRoot -> {\n                                        entrie = newRoot;\n                                        return initialRequests;\n                                    });\n                        }))\n                ));\n    }\n\n    @JsMethod\n    public CompletableFuture<List<Pair<SharedItem, FileWrapper>>> getFiles(List<SharedItem> pointers) {\n        return Futures.combineAllInOrder(pointers.stream()\n                .map(s -> Futures.asyncExceptionally(() -> network.getFile(s.cap, s.owner)\n                                .thenCompose(fopt -> fopt.map(f -> Futures.of(Optional.of(f)))\n                                        .orElseGet(() -> getByPath(s.path))),\n                        t -> getByPath(s.path))\n                        .thenApply(opt -> opt.map(f -> new Pair<>(s, f))))\n                .collect(Collectors.toList()))\n                .thenApply(res -> res.stream()\n                        .flatMap(Optional::stream)\n                        .collect(Collectors.toList()));\n    }\n\n    public CompletableFuture<List<Pair<SharedItem, FileWrapper>>> getFiles(List<SharedItem> pointers, Snapshot v) {\n        return Futures.combineAllInOrder(pointers.stream()\n                .map(s -> Futures.asyncExceptionally(() -> network.getFile(v, s.cap, Optional.empty(), s.owner)\n                                .thenCompose(fopt -> fopt.map(f -> Futures.of(Optional.of(f)))\n                                        .orElseGet(() -> getByPath(s.path))),\n                        t -> getByPath(s.path))\n                        .thenApply(opt -> opt.map(f -> new Pair<>(s, f))))\n                .collect(Collectors.toList()))\n                .thenApply(res -> res.stream()\n                        .flatMap(Optional::stream)\n                        .collect(Collectors.toList()));\n    }\n\n    public CompletableFuture<Set<FileWrapper>> getChildren(String path) {\n        FileProperties.ensureValidPath(path);\n        return entrie.getChildren(path, crypto.hasher, network);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getByPath(Path path) {\n        String pathString = IntStream.range(0, path.getNameCount())\n                .mapToObj(path::getName)\n                .map(Path::toString)\n                .collect(Collectors.joining(\"/\"));\n        return getByPath(pathString);\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<FileWrapper>> getByPath(String path) {\n        if (path.equals(\"/\"))\n            return CompletableFuture.completedFuture(Optional.of(FileWrapper.createRoot(entrie)));\n        FileProperties.ensureValidPath(path);\n        String absolutePath = path.startsWith(\"/\") ? path : \"/\" + path;\n        return entrie.getByPath(absolutePath, crypto.hasher, network)\n                .thenCompose(res -> {\n                    if (res.isPresent())\n                        return Futures.of(res);\n                    return getPublicFile(PathUtil.get(path));\n                });\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getByPath(String path, Snapshot version) {\n        if (path.equals(\"/\"))\n            return CompletableFuture.completedFuture(Optional.of(FileWrapper.createRoot(entrie)));\n        FileProperties.ensureValidPath(path);\n        String absolutePath = path.startsWith(\"/\") ? path : \"/\" + path;\n        return entrie.getByPath(absolutePath, version, crypto.hasher, network);\n    }\n\n    public CompletableFuture<FileWrapper> getUserRoot() {\n        return getByPath(\"/\" + username).thenApply(opt -> opt.get());\n    }\n\n    /**\n     * @return TrieNode for root of filesystem containing only our files\n     */\n    private static CompletableFuture<TrieNode> createOurFileTreeOnly(String ourName,\n                                                                     UserStaticData.EntryPoints entry,\n                                                                     WriterData userData,\n                                                                     NetworkAccess network) {\n        TrieNode root = TrieNodeImpl.empty();\n        List<EntryPoint> ourFileSystemEntries = entry.entries\n                .stream()\n                .filter(e -> e.ownerName.equals(ourName))\n                .map(e -> e.withOwner(userData.controller))\n                .collect(Collectors.toList());\n        return Futures.reduceAll(ourFileSystemEntries, root,\n                (t, e) -> NetworkAccess.getLatestEntryPoint(e, network)\n                        .thenApply(r -> t.put(r.getPath(), r.entry)),\n                (a, b) -> a)\n                .exceptionally(Futures::logAndThrow);\n    }\n\n    private CompletableFuture<TrieNode> buildFileTree(TrieNode ourRoot,\n                                                      FileWrapper homeDir,\n                                                      Predicate<String> includeUser,\n                                                      NetworkAccess network,\n                                                      Crypto crypto) {\n        return time(() -> getFriendsEntryPoints(homeDir), \"Get friend's entry points\")\n                .thenCompose(friendEntries -> {\n                            List<EntryPoint> friendsOnly = friendEntries.stream()\n                                    .filter(e -> includeUser.test(e.ownerName))\n                                    .collect(Collectors.toList());\n\n                            List<CompletableFuture<Optional<FriendSourcedTrieNode>>> friendNodes = friendsOnly.stream()\n                                    .parallel()\n                                    .map(e -> FriendSourcedTrieNode.build(capCache, e,\n                                            (cap, o, n, s, c) -> addFriendGroupCap(cap, o, n, s, c), network, crypto))\n                                    .collect(Collectors.toList());\n                            return Futures.reduceAll(friendNodes, ourRoot,\n                                    (t, e) -> e.thenApply(fromUser -> fromUser.map(userEntrie -> t.putNode(userEntrie.ownerName, userEntrie))\n                                            .orElse(t)).exceptionally(ex -> t),\n                                    (a, b) -> a);\n                        }).thenCompose(root -> getFriendsGroupCaps(homeDir, homeDir.version, network)\n                        .thenApply(groups -> { // now add the groups from each friend\n                            Set<String> friendNames = root.getChildNames()\n                                    .stream()\n                                    .filter(includeUser)\n                                    .collect(Collectors.toSet());\n                            for (String friendName : friendNames) {\n                                FriendSourcedTrieNode friend = (FriendSourcedTrieNode) root.getChildNode(friendName);\n                                for (EntryPoint group : groups.left.getFriends(friendName)) {\n                                    friend.addGroup(group);\n                                }\n                            }\n                            return root;\n                        }));\n    }\n\n    /**\n     * @return TrieNode for root of filesystem\n     */\n    private CompletableFuture<TrieNode> createFileTree(TrieNode ourRoot,\n                                                       String ourName,\n                                                       NetworkAccess network,\n                                                       Crypto crypto) {\n        // need to retrieve all the entry points of our friends and any of their groups\n        return ourRoot.getByPath(ourName, crypto.hasher, network)\n                        .thenApply(Optional::get)\n                .thenCompose(homeDir -> buildFileTree(ourRoot, homeDir, n -> ! n.equals(ourName), network, crypto))\n                .exceptionally(Futures::logAndThrow);\n    }\n\n    private CompletableFuture<TrieNode> retrieveAndAddEntryPointToTrie(TrieNode root, EntryPoint e) {\n        return NetworkAccess.retrieveEntryPoint(e, network)\n                .thenCompose(r -> addRetrievedEntryPointToTrie(username, root, r.entry, r.getPath(), false,\n                        capCache, this, network, crypto));\n    }\n\n    private CompletableFuture<List<EntryPoint>> getFriendsEntryPoints(FileWrapper homeDir) {\n        return homeDir.getChild(ENTRY_POINTS_FROM_FRIENDS_FILENAME, crypto.hasher, network)\n                .thenCompose(fopt -> {\n                    return fopt.map(f -> {\n                        List<EntryPoint> res = new ArrayList<>();\n                        return f.getInputStream(network, crypto, x -> {})\n                                .thenCompose(reader -> reader.parseStream(EntryPoint::fromCbor, res::add, f.getSize())\n                                        .thenApply(x -> res));\n                    }).orElse(CompletableFuture.completedFuture(Collections.emptyList()))\n                            .thenCompose(fromFriends -> {\n                                // filter out blocked friends\n                                return homeDir.getChild(BLOCKED_USERNAMES_FILE, crypto.hasher, network)\n                                        .thenCompose(bopt -> bopt.map(f -> f.getInputStream(network, crypto, x -> {})\n                                                .thenCompose(in -> Serialize.readFully(in, f.getSize()))\n                                                .thenApply(data -> new HashSet<>(Arrays.asList(new String(data).split(\"\\n\")))\n                                                        .stream()\n                                                        .collect(Collectors.toSet())))\n                                                .orElse(CompletableFuture.completedFuture(Collections.emptySet()))\n                                                .thenApply(toRemove -> fromFriends.stream()\n                                                        .filter(e -> !toRemove.contains(e.ownerName))\n                                                        .collect(Collectors.toList())));\n                            });\n                }).thenApply(entries -> {\n                    // Only take the most recent version of each entry\n                    Map<PublicKeyHash, EntryPoint> latest = new LinkedHashMap<>();\n                    entries.forEach(e -> latest.put(e.pointer.writer, e));\n                    return latest.values()\n                            .stream()\n                            .collect(Collectors.toList());\n                });\n    }\n\n    private CompletableFuture<Pair<FriendsGroups, Optional<FileWrapper>>> getFriendsGroupCaps(FileWrapper homeDir,\n                                                                                              Snapshot s,\n                                                                                              NetworkAccess network) {\n        return homeDir.getChild(ENTRY_POINTS_FROM_FRIENDS_GROUPS_FILENAME, crypto.hasher, network)\n                .thenCompose(fopt -> fopt.map(f -> f.getInputStream(s.get(f.writer()), network, crypto, x -> {})\n                        .thenCompose(reader -> Serialize.parse(reader, f.getSize(), FriendsGroups::fromCbor))\n                        .thenApply(g -> new Pair<>(g, fopt)))\n                        .orElse(CompletableFuture.completedFuture(new Pair<>(FriendsGroups.empty(), Optional.empty()))));\n    }\n\n    public CompletableFuture<Snapshot> addFriendGroupCap(CapabilityWithPath group,\n                                                         String owner,\n                                                         NetworkAccess network,\n                                                         Snapshot s,\n                                                         Committer c) {\n        return entrie.getByPath(\"/\" + username, s, crypto.hasher, network).thenApply(Optional::get)\n                .thenCompose(home -> getFriendsGroupCaps(home, s, network)\n                        .thenCompose(p -> {\n                            FriendsGroups updated = p.left.addGroup(group, owner);\n                            byte[] raw = updated.serialize();\n                            AsyncReader reader = AsyncReader.build(raw);\n                            if (p.right.isPresent())\n                                return p.right.get().overwriteFile(reader, raw.length, network, crypto, x -> {}, s, c);\n\n                            return home.uploadOrReplaceFile(ENTRY_POINTS_FROM_FRIENDS_GROUPS_FILENAME, reader, raw.length, true, s, c, network, crypto, () -> false, x -> {},\n                                    crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH), Optional.of(Bat.random(crypto.random)), mirrorBatId());\n                        }));\n    }\n\n    private static CompletableFuture<TrieNode> addRetrievedEntryPointToTrie(String ourName,\n                                                                            TrieNode root,\n                                                                            EntryPoint fileCap,\n                                                                            String path,\n                                                                            boolean checkOwner,\n                                                                            IncomingCapCache capCache,\n                                                                            UserContext context,\n                                                                            NetworkAccess network,\n                                                                            Crypto crypto) {\n        // check entrypoint doesn't forge the owner\n        return (fileCap.ownerName.equals(ourName) || ! checkOwner ? CompletableFuture.completedFuture(true) :\n                fileCap.isValid(path, network)).thenCompose(valid -> {\n            if (! valid)\n                return Futures.errored(new IllegalStateException(\"Capability claims incorrect owner!\"));\n            String[] parts = path.split(\"/\");\n            if (parts.length < 3 || !parts[2].equals(SHARED_DIR_NAME))\n                return CompletableFuture.completedFuture(root.put(path, fileCap));\n            String username = parts[1];\n            if (username.equals(ourName)) // This is a sharing directory of ours for a friend\n                return CompletableFuture.completedFuture(root);\n            // This is a friend's sharing directory, create a wrapper to read the capabilities lazily from it\n            return root.getByPath(PathUtil.get(ourName, CapabilityStore.CAPABILITY_CACHE_DIR).toString(), crypto.hasher, network)\n                    .thenApply(opt -> opt.get())\n                    .thenCompose(cacheDir -> FriendSourcedTrieNode.build(capCache, fileCap, context::addFriendGroupCap, network, crypto))\n                    .thenApply(fromUser -> fromUser.map(userEntrie -> root.putNode(username, userEntrie)).orElse(root));\n        });\n    }\n\n    public static CompletableFuture<CommittedWriterData> getWriterData(NetworkAccess network,\n                                                                       PublicKeyHash owner,\n                                                                       PublicKeyHash writer) {\n        return getWriterDataCbor(network.dhtClient, network.mutable, owner, writer)\n                .thenApply(pair -> new CommittedWriterData(pair.left.updated, WriterData.fromCbor(pair.right), pair.left.sequence));\n    }\n\n    public static CompletableFuture<CommittedWriterData> getWriterData(ContentAddressedStorage ipfs,\n                                                                       MutablePointers mutable,\n                                                                       PublicKeyHash owner,\n                                                                       PublicKeyHash writer) {\n        return getWriterDataCbor(ipfs, mutable, owner, writer)\n                .thenApply(pair -> new CommittedWriterData(pair.left.updated, WriterData.fromCbor(pair.right), pair.left.sequence));\n    }\n\n    public static CompletableFuture<Pair<PointerUpdate, CborObject>> getWriterDataCbor(NetworkAccess network, String username) {\n        return network.coreNode.getPublicKeyHash(username)\n                .thenCompose(signer -> {\n                    PublicKeyHash owner = signer.orElseThrow(\n                            () -> new IllegalStateException(\"No public-key for user \" + username));\n                    return getWriterDataCbor(network.dhtClient, network.mutable, owner, owner);\n                });\n    }\n\n    private static CompletableFuture<Pair<PointerUpdate, CborObject>> getWriterDataCbor(ContentAddressedStorage ipfs,\n                                                                                        MutablePointers mutable,\n                                                                                        PublicKeyHash owner,\n                                                                                        PublicKeyHash writer) {\n        return mutable.getPointer(owner, writer)\n                .thenCompose(casOpt -> ipfs.getSigningKey(owner, writer)\n                        .thenCompose(signer -> casOpt.map(raw -> signer.get().unsignMessage(raw)\n                                        .thenApply(unsigned -> PointerUpdate.fromCbor(CborObject.fromByteArray(unsigned))))\n                                .orElse(Futures.of(PointerUpdate.empty()))))\n                .thenCompose(pointer -> ipfs.get(owner, (Cid)pointer.updated.get(), Optional.empty())\n                        .thenApply(Optional::get)\n                        .thenApply(cbor -> new Pair<>(pointer, cbor))\n                );\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> unfollow(String friendName) {\n        LOG.info(\"Unfollowing: \" + friendName);\n        return getUserRoot()\n                .thenCompose(home -> home.getChild(BLOCKED_USERNAMES_FILE, crypto.hasher, network)\n                        .thenCompose(fopt -> home.appendToChild(BLOCKED_USERNAMES_FILE,\n                                fopt.map(f -> f.getSize()).orElse(0L), (friendName + \"\\n\").getBytes(), true,\n                                mirrorBatId(), network, crypto, x -> {})))\n                .thenApply(b -> {\n                    entrie = entrie.removeEntry(\"/\" + friendName + \"/\");\n                    return true;\n                });\n    }\n\n    public CompletableFuture<Set<String>> getBlocked() {\n        return getUserRoot()\n                .thenCompose(home -> home.getChild(BLOCKED_USERNAMES_FILE, crypto.hasher, network))\n                .thenCompose(this::getBlocked);\n    }\n\n    private CompletableFuture<Set<String>> getBlocked(Optional<FileWrapper> blockedUsernamesFile) {\n        return blockedUsernamesFile.isEmpty() ?\n                Futures.of(Collections.emptySet()) :\n                blockedUsernamesFile.get().getInputStream(network, crypto, x -> {})\n                        .thenCompose(in -> Serialize.readFully(in, blockedUsernamesFile.get().getSize()))\n                        .thenApply(data -> new HashSet<>(Arrays.asList(new String(data).split(\"\\n\"))));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> unblock(String username) {\n        return getUserRoot()\n                .thenCompose(home -> home.getChild(BLOCKED_USERNAMES_FILE, crypto.hasher, network)\n                        .thenCompose(bopt -> bopt.isEmpty() ?\n                                Futures.of(true) :\n                                getBlocked(bopt)\n                                        .thenCompose(all -> {\n                                            byte[] updated = all.stream()\n                                                    .filter(u -> !u.equals(username))\n                                                    .sorted()\n                                                    .map(u -> u + \"\\n\")\n                                                    .collect(Collectors.joining())\n                                                    .getBytes();\n\n                                            return bopt.get().overwriteFile(AsyncReader.build(updated), updated.length, network, crypto, x -> {})\n                                                    .thenApply(x -> true);\n                                        })\n                        )).thenCompose(x -> getUserRoot()\n                        .thenCompose(home -> buildFileTree(entrie, home, n -> n.equals(username), network, crypto)).thenApply(updated -> {\n                            this.entrie = updated;\n                            return true;\n                        }));\n    }\n\n    public CompletableFuture<Optional<String>> getGroupUid(String groupName) {\n        return getGroupNameMappings()\n                .thenApply(m -> m.uidToGroupName.entrySet().stream()\n                        .filter(e -> e.getValue().equals(groupName))\n                        .map(e -> e.getKey())\n                        .findFirst());\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> removeFollower(String usernameToRemove) {\n        LOG.info(\"Remove follower: \" + usernameToRemove);\n        // remove /$us/shared/$them\n        Path sharingDir = PathUtil.get(this.username, SHARED_DIR_NAME, usernameToRemove);\n        return removeFromFriendGroup(usernameToRemove)\n                .thenCompose(x1 -> removeFromFollowersGroup(usernameToRemove))\n                .thenCompose(x2 -> unshareItemsInSharingFolder(usernameToRemove, usernameToRemove)) // revoke access to everything ever shared with this user!\n                .thenCompose(x3 -> getSharingFolder())\n                .thenCompose(sharing -> getByPath(sharingDir)\n                        .thenCompose(dir -> dir.get().remove(sharing, sharingDir, this)))\n                .thenApply(x4 -> true);\n    }\n\n    private CompletableFuture<Boolean> removeFromFriendGroup(String usernameToRemove) {\n        return getGroupUid(SocialState.FRIENDS_GROUP_NAME)\n                .thenCompose(friendsUid -> friendsUid.isPresent() ?\n                        removeFromGroup(friendsUid.get(), usernameToRemove) :\n                        Futures.of(true));\n    }\n\n    private CompletableFuture<Boolean> removeFromFollowersGroup(String usernameToRemove) {\n        return getGroupUid(SocialState.FOLLOWERS_GROUP_NAME)\n                .thenCompose(followersUid -> followersUid.isPresent() ?\n                        removeFromGroup(followersUid.get(), usernameToRemove) :\n                        Futures.of(true));\n    }\n\n    /** Remove a user from a group. This involves rotating the keys to the group sharing dir,\n     *  and then also rotating the keys for everything ever shared with the group.\n     *\n     * @param groupUid\n     * @param username\n     * @return\n     */\n    public CompletableFuture<Boolean> removeFromGroup(String groupUid, String username) {\n        return unShareReadAccess(PathUtil.get(this.username, SHARED_DIR_NAME, groupUid), username)\n                .thenCompose(x -> unshareItemsInSharingFolder(groupUid, username));\n    }\n\n    public CompletableFuture<Boolean> unshareItemsInSharingFolder(String folderName, String usernameToRevoke) {\n        return getByPath(PathUtil.get(username, SHARED_DIR_NAME, folderName))\n                .thenCompose(opt -> {\n                    if (opt.isEmpty())\n                        return Futures.of(true);\n                    return CapabilityStore.loadReadAccessSharingLinksFromIndex(null, opt.get(), null,\n                            network, crypto, 0, false, false)\n                            .thenCompose(readCaps -> revokeAllReadCaps(readCaps.getRetrievedCapabilities(), usernameToRevoke))\n                            .thenCompose(x -> CapabilityStore.loadWriteAccessSharingLinksFromIndex(null, opt.get(), null,\n                                    network, crypto, 0, false, false)\n                                    .thenCompose(writeCaps -> revokeAllWriteCaps(writeCaps.getRetrievedCapabilities(), usernameToRevoke)));\n                });\n    }\n\n    private CompletableFuture<Boolean> revokeAllReadCaps(List<CapabilityWithPath> caps, String usernameToRevoke) {\n        return Futures.reduceAll(caps, true,\n                (b, c) -> unShareReadAccess(PathUtil.get(c.path), usernameToRevoke),\n                (a, b) -> a && b);\n    }\n\n    private CompletableFuture<Boolean> revokeAllWriteCaps(List<CapabilityWithPath> caps, String usernameToRevoke) {\n        return Futures.reduceAll(caps, true,\n                (b, c) -> unShareWriteAccess(PathUtil.get(c.path), usernameToRevoke),\n                (a, b) -> a && b);\n    }\n\n    @JsMethod\n    public CompletableFuture<Snapshot> cleanPartialUploads() {\n        // clear any partial upload started more than a day ago\n        return cleanPartialUploads(t -> t.startTimeEpochMillis() < System.currentTimeMillis() - 24*3600_000L);\n    }\n\n    public CompletableFuture<Snapshot> cleanPartialUploads(Predicate<Transaction> filter) {\n        TransactionService txns = getTransactionService();\n        return getUserRoot().thenCompose(home -> network.synchronizer\n                .applyComplexUpdate(home.owner(), txns.getSigner(),\n                        (s, comitter) -> txns.clearAndClosePendingTransactions(s, comitter, filter)));\n    }\n\n    public void logout() {\n        entrie = TrieNodeImpl.empty();\n    }\n\n    @JsMethod\n    public CompletableFuture<List<ServerMessage>> getNewMessages() {\n        //get all messages not dismissed\n        return network.serverMessager.getMessages(username, signer.secret);\n    }\n\n    @JsMethod\n    public CompletableFuture<List<ServerConversation>> getServerConversations() {\n        if (this.username == null) {\n            return Futures.of(Collections.emptyList());\n        }\n        return network.serverMessager.getMessages(username, signer.secret).thenApply(ServerConversation::combine);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> dismissMessage(ServerMessage message) {\n        ServerMessage dismiss = new ServerMessage(message.id, ServerMessage.Type.Dismiss, System.currentTimeMillis(), \"\", Optional.empty(), true);\n        return network.serverMessager.sendMessage(username, dismiss, signer.secret);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> sendReply(ServerMessage prior, String message) {\n        ServerMessage msg = new ServerMessage(prior.id, ServerMessage.Type.FromUser, System.currentTimeMillis(), message, Optional.of(prior.id), true);\n        return network.serverMessager.sendMessage(username, msg, signer.secret);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> sendFeedback(String message) {\n        ServerMessage msg = ServerMessage.buildUserMessage(message);\n        return network.serverMessager.sendMessage(username, msg, signer.secret);\n    }\n\n    public static <V> CompletableFuture<V> time(Supplier<CompletableFuture<V>> f, String name) {\n        long t0 = System.currentTimeMillis();\n        return f.get().thenApply(x -> {\n            long t1 = System.currentTimeMillis();\n            LOG.log(Level.INFO, name + \" took \" + (t1 - t0) + \"ms\");\n            return x;\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/UserSnapshot.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.social.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class UserSnapshot implements Cborable {\n\n    public final String username;\n    public final PublicKeyHash owner;\n    public final Map<PublicKeyHash, byte[]> pointerState;\n    public final List<BlindFollowRequest> pendingFollowReqs;\n    public final List<BatWithId> mirrorBats;\n    public final Optional<LoginData> login;\n    public final LinkCounts linkCounts;\n\n    public UserSnapshot(String username,\n                        PublicKeyHash owner,\n                        Map<PublicKeyHash, byte[]> pointerState,\n                        List<BlindFollowRequest> pendingFollowReqs,\n                        List<BatWithId> mirrorBats,\n                        Optional<LoginData> login,\n                        LinkCounts linkCounts) {\n        this.username = username;\n        this.owner = owner;\n        this.pointerState = pointerState;\n        this.pendingFollowReqs = pendingFollowReqs;\n        this.mirrorBats = mirrorBats;\n        this.login = login;\n        this.linkCounts = linkCounts;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"n\", new CborObject.CborString(username));\n        state.put(\"o\", owner.toCbor());\n        state.put(\"f\", new CborObject.CborList(pendingFollowReqs));\n        TreeMap<CborObject, Cborable> pointerMap = pointerState.entrySet()\n                .stream()\n                .collect(Collectors.toMap(\n                    e -> e.getKey().toCbor(),\n                    e -> new CborObject.CborByteArray(e.getValue()),\n                    (a,b) -> a,\n                    TreeMap::new\n                ));\n        state.put(\"p\", new CborObject.CborList(pointerMap));\n        state.put(\"b\", new CborObject.CborList(mirrorBats));\n        login.ifPresent(d -> state.put(\"l\", d));\n        state.put(\"lc\", linkCounts.toCbor());\n        return CborObject.CborMap.build(state);\n    }\n\n    public static UserSnapshot fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for FileProperties! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        String username = m.getString(\"n\");\n        PublicKeyHash owner = m.get(\"o\", PublicKeyHash::fromCbor);\n        List<BlindFollowRequest> pendingFollowReqs = m.getList(\"f\", BlindFollowRequest::fromCbor);\n        Map<PublicKeyHash, byte[]> pointerState = ((CborObject.CborList)m.get(\"p\"))\n                .getMap(PublicKeyHash::fromCbor, c -> ((CborObject.CborByteArray)c).value);\n        List<BatWithId> mirrorBats = m.getList(\"b\", BatWithId::fromCbor);\n        Optional<LoginData> login = m.getOptional(\"l\", LoginData::fromCbor);\n        LinkCounts lc = m.get(\"lc\", LinkCounts::fromCbor);\n\n        return new UserSnapshot(username, owner, pointerState, pendingFollowReqs, mirrorBats, login, lc);\n    }\n\n    @Override\n    public String toString() {\n        return username + \"(\"+pointerState.size()+\")\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/UserStaticData.java",
    "content": "package peergos.shared.user;\nimport java.util.logging.*;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.mlkem.HybridCurve25519MLKEMPublicKey;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class UserStaticData implements Cborable {\n\tprivate static final Logger LOG = Logger.getGlobal();\n\tprivate static final int PADDING_BLOCK_SIZE = 4096;\n\n    private final PaddedCipherText allEntryPoints;\n\n    public UserStaticData(PaddedCipherText allEntryPoints) {\n        this.allEntryPoints = allEntryPoints;\n    }\n\n    public UserStaticData(EntryPoints login, SymmetricKey rootKey) {\n        this(PaddedCipherText.build(rootKey, login, PADDING_BLOCK_SIZE));\n    }\n\n    public UserStaticData(List<EntryPoint> staticData,\n                          SymmetricKey rootKey,\n                          Optional<SigningKeyPair> identity,\n                          Optional<BoxingKeyPair> boxer) {\n        this(new EntryPoints(EntryPoints.CURRENT_VERSION, staticData, identity, boxer), rootKey);\n    }\n\n    public EntryPoints getData(SymmetricKey rootKey) {\n        try {\n            return allEntryPoints.decrypt(rootKey, EntryPoints::fromCbor);\n        } catch (Exception e) {\n            throw new IllegalStateException(\"Incorrect username or password\");\n        }\n    }\n\n    @Override\n    public CborObject toCbor() {\n        return allEntryPoints.toCbor();\n    }\n\n    @JsMethod\n    public static UserStaticData fromByteArray(byte[] entryPoints) {\n        return UserStaticData.fromCbor(CborObject.fromByteArray(entryPoints));\n    }\n\n    public static UserStaticData fromCbor(Cborable cbor) {\n        return new UserStaticData(PaddedCipherText.fromCbor(cbor));\n    }\n\n    public static class EntryPoints implements Cborable {\n        private static final int CURRENT_VERSION = 2;\n\n        private final long version;\n        public final List<EntryPoint> entries;\n        public final Optional<BoxingKeyPair> boxer; // this is only absent on legacy accounts\n        public final Optional<SigningKeyPair> identity; // this is only absent on legacy accounts\n\n        public EntryPoints(long version,\n                           List<EntryPoint> entries,\n                           Optional<SigningKeyPair> identity,\n                           Optional<BoxingKeyPair> boxer) {\n            this.version = version;\n            this.entries = entries;\n            this.identity = identity;\n            this.boxer = boxer;\n        }\n\n        public EntryPoints withBoxer(BoxingKeyPair newBoxer) {\n            if (!(newBoxer.publicBoxingKey instanceof HybridCurve25519MLKEMPublicKey))\n                throw new IllegalStateException(\"You can only upgrade to post-quantum friending!\");\n            return new EntryPoints(version, entries, identity, Optional.of(newBoxer));\n        }\n\n        public EntryPoints addEntryPoint(EntryPoint entry) {\n            List<EntryPoint> updated = Stream.concat(entries.stream(), Stream.of(entry)).collect(Collectors.toList());\n            return new EntryPoints(version, updated, identity, boxer);\n        }\n\n        @Override\n        public CborObject toCbor() {\n            Map<String, Cborable> res = new TreeMap<>();\n            res.put(\"v\", new CborObject.CborLong(version));\n            res.put(\"e\", new CborObject.CborList(entries.stream()\n                    .map(EntryPoint::toCbor)\n                    .collect(Collectors.toList())));\n            boxer.ifPresent(p -> res.put(\"b\", p));\n            identity.ifPresent(p -> res.put(\"i\", p));\n            return CborObject.CborMap.build(res);\n        }\n\n        public static EntryPoints fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Incorrect cbor type for EntryPoints: \" + cbor);\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n            long version = m.getLong(\"v\");\n            if (version > CURRENT_VERSION)\n                throw new IllegalStateException(\"Unknown UserStaticData version: \" + version);\n            Optional<BoxingKeyPair> boxer = m.getOptional(\"b\", BoxingKeyPair::fromCbor);\n            Optional<SigningKeyPair> identity = m.getOptional(\"i\", SigningKeyPair::fromCbor);\n            return new EntryPoints(version,\n                    m.getList(\"e\")\n                            .value.stream()\n                            .map(EntryPoint::fromCbor)\n                            .collect(Collectors.toList()),\n                    identity,\n                    boxer);\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/UserUtil.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.Crypto;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.curve25519.*;\nimport peergos.shared.crypto.asymmetric.mlkem.Mlkem;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.util.*;\n\nimport java.util.Arrays;\nimport java.util.concurrent.CompletableFuture;\n\npublic class UserUtil {\n\n    public static CompletableFuture<UserWithRoot> generateUser(String username,\n                                                               String password,\n                                                               Crypto crypto,\n                                                               SecretGenerationAlgorithm algorithm) {\n        if (password.equals(username))\n            return Futures.errored(new IllegalStateException(\"Your password cannot be the same as your username!\"));\n        CompletableFuture<byte[]> fut = crypto.hasher.hashToKeyBytes(username + algorithm.getExtraSalt(), password, algorithm);\n        return fut.thenCompose(keyBytes -> {\n            byte[] signBytesSeed = Arrays.copyOfRange(keyBytes, 0, 32);\n            boolean hasBoxer = algorithm.generateBoxerAndIdentity();\n            byte[] secretBoxBytes = hasBoxer ? Arrays.copyOfRange(keyBytes, 32, 64) : crypto.random.randomBytes(32);\n\n            byte[] rootKeyBytes = Arrays.copyOfRange(keyBytes, hasBoxer ? 64 : 32, hasBoxer ? 96 : 64);\n\t\n            byte[] secretSignBytes = Arrays.copyOf(signBytesSeed, 64);\n            byte[] publicSignBytes = new byte[32];\n\t\n            crypto.signer.crypto_sign_keypair(publicSignBytes, secretSignBytes);\n\t\n            byte[] publicBoxBytes = new byte[32];\n            crypto.boxer.crypto_box_keypair(publicBoxBytes, secretBoxBytes);\n\t\n            SigningKeyPair signingKeyPair = new SigningKeyPair(new Ed25519PublicKey(publicSignBytes, crypto.signer), new Ed25519SecretKey(secretSignBytes, crypto.signer));\n\n            return (hasBoxer ?\n                    Futures.of(new BoxingKeyPair(new Curve25519PublicKey(publicBoxBytes, crypto.boxer, crypto.random), new Curve25519SecretKey(secretBoxBytes, crypto.boxer))) :\n                    BoxingKeyPair.randomHybrid(crypto)).thenApply(boxingKeyPair -> {\n                SymmetricKey root = new TweetNaClKey(rootKeyBytes, false, crypto.symmetricProvider, crypto.random);\n                return new UserWithRoot(signingKeyPair, boxingKeyPair, root);\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/UserWithRoot.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\n\npublic class UserWithRoot {\n    private final SigningKeyPair signer;\n    private final BoxingKeyPair boxer;\n    private final SymmetricKey root;\n\n    public UserWithRoot(SigningKeyPair signer, BoxingKeyPair boxer, SymmetricKey root) {\n        this.signer = signer;\n        this.boxer = boxer;\n        this.root = root;\n    }\n\n    public SigningKeyPair getUser() {\n        return signer;\n    }\n\n    public BoxingKeyPair getBoxingPair() {\n        return boxer;\n    }\n    public SymmetricKey getRoot() {\n        return root;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/WriteSynchronizer.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.mutable.MutablePointers;\nimport peergos.shared.storage.*;\nimport peergos.shared.util.*;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.*;\n\npublic class WriteSynchronizer {\n\n    private final MutablePointers mutable;\n    private final ContentAddressedStorage dht;\n    private final Hasher hasher;\n    // The keys are <owner, writer> pairs. The owner is only needed to handle identity changes\n    private final Map<Pair<PublicKeyHash, PublicKeyHash>, AsyncLock<Snapshot>> pending = new ConcurrentHashMap<>();\n    private CommitterBuilder committerBuilder = (c, o, w) -> c;\n    private BufferedNetworkAccess.Flusher flusher = (o, v, w) -> Futures.of(v);\n\n    public WriteSynchronizer(MutablePointers mutable, ContentAddressedStorage dht, Hasher hasher) {\n        this.mutable = mutable;\n        this.dht = dht;\n        this.hasher = hasher;\n    }\n\n    public void clear() {\n        pending.clear();\n    }\n\n    public void setCommitterBuilder(CommitterBuilder committerBuilder) {\n        this.committerBuilder = committerBuilder;\n    }\n\n    public void setFlusher(BufferedNetworkAccess.Flusher flusher) {\n        this.flusher = flusher;\n    }\n\n    public void put(PublicKeyHash owner, PublicKeyHash writer, CommittedWriterData val) {\n        pending.put(new Pair<>(owner, writer),\n                new AsyncLock<>(CompletableFuture.completedFuture(new Snapshot(writer, val))));\n    }\n\n    public void putEmpty(PublicKeyHash owner, PublicKeyHash writer) {\n        WriterData emptyWD = new WriterData(writer,\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty(),\n                Collections.emptyMap(),\n                Optional.empty(),\n                Optional.empty(),\n                Optional.empty());\n        CommittedWriterData emptyUserData = new CommittedWriterData(MaybeMultihash.empty(), emptyWD, Optional.empty());\n        put(owner, writer, emptyUserData);\n    }\n\n    public CompletableFuture<Snapshot> getWriterData(PublicKeyHash owner, PublicKeyHash writer) {\n        return mutable.getPointerTarget(owner, writer, dht)\n                .thenCompose(x -> x.updated.isPresent() ?\n                        WriterData.getWriterData(owner, (Cid)x.updated.get(), x.sequence, dht)\n                                .thenApply(cwd -> new Snapshot(writer, cwd)) :\n                        Futures.of(new Snapshot(Collections.emptyMap()))\n                );\n    }\n\n    /**\n     *\n     * @param owner\n     * @param writer\n     * @return The current version committed by writer\n     */\n    public CompletableFuture<Snapshot> getValue(PublicKeyHash owner, PublicKeyHash writer) {\n        return pending.computeIfAbsent(new Pair<>(owner, writer), p -> new AsyncLock<>(getWriterData(owner, p.right)))\n                .runWithLock(x -> getWriterData(owner, writer), () -> getWriterData(owner, writer));\n    }\n\n    public CompletableFuture<Snapshot> applyUpdate(PublicKeyHash owner,\n                                                   SigningPrivateKeyAndPublicHash writer,\n                                                   Mutation transformer) {\n        // This is subtle, but we need to ensure that there is only ever 1 thenAble waiting on the future for a given key\n        // otherwise when the future completes, then the two or more waiters will both proceed with the existing hash,\n        // and whoever commits first will win. We also need to retrieve the writer data again from the network after\n        // a previous transaction has completed (another node/user with write access may have concurrently updated the mapping)\n        return pending.computeIfAbsent(new Pair<>(owner, writer.publicKeyHash), p -> new AsyncLock<>(getWriterData(owner, p.right)))\n                .runWithLock(current -> IpfsTransaction.call(owner, tid -> transformer.apply(current.get(writer).props.get(), tid)\n                                .thenCompose(wd -> committerBuilder.buildCommitter((aOwner, signer, wdr, existing, t) -> wdr.get().commit(aOwner, signer,\n                                        existing.hash, existing.sequence, mutable, dht, hasher, t), owner, () -> true)\n                                        .commit(owner, writer, wd, current.get(writer), tid)\n                                        .thenCompose(v -> flusher.commit(owner, v, () -> true))), dht),\n                        () -> getWriterData(owner, writer.publicKeyHash));\n    }\n\n    /** Apply an update\n     *\n     * @param owner\n     * @param writer\n     * @param transformer\n     * @return\n     */\n    public CompletableFuture<Snapshot> applyComplexUpdate(PublicKeyHash owner,\n                                                          SigningPrivateKeyAndPublicHash writer,\n                                                          ComplexMutation transformer,\n                                                          Supplier<Boolean> commitWatcher) {\n        return pending.computeIfAbsent(new Pair<>(owner, writer.publicKeyHash), p -> new AsyncLock<>(getWriterData(owner, p.right)))\n                .runWithLock(current -> transformer.apply(current,\n                                        committerBuilder.buildCommitter((aOwner, signer, wd, existing, tid) -> (wd.isPresent() ?\n                                                wd.get().commit(aOwner, signer, existing.hash, existing.sequence, mutable, dht, hasher, tid) :\n                                                WriterData.commitDeletion(aOwner, signer, existing.hash, existing.sequence, mutable))\n                                                .thenCompose(s -> updateWriterState(owner, signer.publicKeyHash, s).thenApply(x -> s)), owner, commitWatcher))\n                                .thenCompose(v -> flusher.commit(owner, v, commitWatcher)),\n                        () -> getWriterData(owner, writer.publicKeyHash));\n    }\n\n    public CompletableFuture<Snapshot> applyComplexUpdate(PublicKeyHash owner,\n                                                          SigningPrivateKeyAndPublicHash writer,\n                                                          ComplexMutation transformer) {\n        return applyComplexUpdate(owner, writer, transformer, () -> true);\n    }\n\n    public CompletableFuture<Boolean> updateWriterState(PublicKeyHash owner,\n                                                        PublicKeyHash writer,\n                                                        Snapshot value) {\n        AsyncLock<Snapshot> existing = pending.get(new Pair<>(owner, writer));\n        if (existing != null && ! existing.isDone()) // don't modify the lock we are in\n            return CompletableFuture.completedFuture(true);\n        // need to update local queue for other writer\n        return pending.computeIfAbsent(\n                        new Pair<>(owner, writer),\n                        p -> new AsyncLock<>(getWriterData(owner, p.right))\n                ).runWithLock(v -> CompletableFuture.completedFuture(v.withVersion(writer, value.get(writer))))\n                .thenApply(x -> true);\n    }\n\n    /** Apply an update and return a computed value\n     *\n     * @param owner\n     * @param writer\n     * @param transformer\n     * @param <V>\n     * @return\n     */\n    public <V> CompletableFuture<Pair<Snapshot, V>> applyComplexComputation(PublicKeyHash owner,\n                                                                            SigningPrivateKeyAndPublicHash writer,\n                                                                            ComplexComputation<V> transformer) {\n        CompletableFuture<Pair<Snapshot, V>> res = new CompletableFuture<>();\n        return pending.computeIfAbsent(new Pair<>(owner, writer.publicKeyHash), p -> new AsyncLock<>(getWriterData(owner, p.right)))\n                .runWithLock(current -> transformer.apply(current,\n                                committerBuilder.buildCommitter((aOwner, signer, wd, existing, tid) ->\n                                                (wd.isPresent() ?\n                                                        wd.get().commit(aOwner, signer, existing.hash, existing.sequence, mutable, dht, hasher, tid) :\n                                                        WriterData.commitDeletion(aOwner, signer, existing.hash, existing.sequence, mutable))\n                                                        .thenCompose(s -> updateWriterState(owner, signer.publicKeyHash, s)\n                                                                .thenApply(x -> s)),\n                                        owner, () -> true))\n                                .thenCompose(p -> flusher.commit(owner, p.left, () -> true).thenApply(x -> p))\n                                .thenApply(p -> {\n                                    res.complete(p);\n                                    return p.left;\n                                }),\n                        () -> getWriterData(owner, writer.publicKeyHash))\n                .thenCompose(x -> res);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/WriterData.java",
    "content": "package peergos.shared.user;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.corenode.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.mutable.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class WriterData implements Cborable {\n    /**\n     *  Represents the root block that a public key maps to\n     */\n\n    // the public signing key controlling this subspace\n    public final PublicKeyHash controller;\n\n    // publicly readable and present on owner keys\n    public final Optional<SecretGenerationAlgorithm> generationAlgorithm;\n    // This is the root of an InodeFileSystem containing read only caps to publicly shared files and folders\n    public final Optional<Multihash> publicData;\n    // The public boxing key to encrypt follow requests to\n    public final Optional<PublicKeyHash> followRequestReceiver;\n    // Any keys directly owned by the controller, that aren't named, in a champ<key hash, owner proof> - only used by the pki user \"peergos\"\n    public final Optional<Multihash> ownedKeys;\n\n    // Any keys directly owned by the controller that have specific labels\n    public final Map<String, OwnerProof> namedOwnedKeys;\n\n    // Encrypted entry points to our file systems (present on legacy owner keys)\n    public final Optional<UserStaticData> staticData;\n    // This is the root of a champ containing the controller's filesystem (present on writer keys)\n    public final Optional<Multihash> tree;\n    // This is the root of a private champ containing encrypted secret links (present on identity keys)\n    public final Optional<Multihash> secretLinks;\n\n    /**\n     *\n     * @param controller the public signing key that controls writing to this subspace\n     * @param generationAlgorithm The algorithm used to create the users key pairs and root key from the username and password\n     * @param publicData A readable pointer to a subtree made public by this key\n     * @param ownedKeys Any public keys owned by this key\n     * @param staticData Any static data owner by this key (list of entry points)\n     * @param tree Any file tree owned by this key\n     */\n    public WriterData(PublicKeyHash controller,\n                      Optional<SecretGenerationAlgorithm> generationAlgorithm,\n                      Optional<Multihash> publicData,\n                      Optional<PublicKeyHash> followRequestReceiver,\n                      Optional<Multihash> ownedKeys,\n                      Map<String, OwnerProof> namedOwnedKeys,\n                      Optional<UserStaticData> staticData,\n                      Optional<Multihash> tree,\n                      Optional<Multihash> secretLinks) {\n        this.controller = controller;\n        this.generationAlgorithm = generationAlgorithm;\n        this.publicData = publicData;\n        this.followRequestReceiver = followRequestReceiver;\n        this.ownedKeys = ownedKeys;\n        this.namedOwnedKeys = namedOwnedKeys;\n        this.staticData = staticData;\n        this.tree = tree;\n        this.secretLinks = secretLinks;\n    }\n\n    public WriterData withChamp(Multihash treeRoot) {\n        return new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver, ownedKeys, namedOwnedKeys, staticData, Optional.of(treeRoot), secretLinks);\n    }\n\n    public WriterData withBoxer(Optional<PublicKeyHash> followRequestReceiver) {\n        return new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver, ownedKeys, namedOwnedKeys, staticData, tree, secretLinks);\n    }\n\n    public WriterData withPublicRoot(Multihash publicChampRoot) {\n        return new WriterData(controller, generationAlgorithm, Optional.of(publicChampRoot), followRequestReceiver, ownedKeys, namedOwnedKeys, staticData, tree, secretLinks);\n    }\n\n    public WriterData withOwnedRoot(Multihash ownedRoot) {\n        return new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver, Optional.of(ownedRoot), namedOwnedKeys, staticData, tree, secretLinks);\n    }\n\n    public WriterData withAlgorithm(SecretGenerationAlgorithm newAlg) {\n        return new WriterData(controller, Optional.of(newAlg), publicData, followRequestReceiver, ownedKeys, namedOwnedKeys, staticData, tree, secretLinks);\n    }\n\n    public CompletableFuture<WriterData> addOwnedKey(PublicKeyHash owner,\n                                                     SigningPrivateKeyAndPublicHash signer,\n                                                     OwnerProof newOwned,\n                                                     ContentAddressedStorage ipfs,\n                                                     Hasher hasher) {\n        return IpfsTransaction.call(owner,\n                tid -> (! ownedKeys.isPresent() ?\n                        OwnedKeyChamp.createEmpty(owner, signer, ipfs, hasher, tid)\n                                .thenCompose(root -> OwnedKeyChamp.build(owner, root, ipfs, hasher)) :\n                        getOwnedKeyChamp(owner, ipfs, hasher))\n                        .thenCompose(champ -> champ.add(owner, signer, newOwned, hasher, tid))\n                        .thenApply(newRoot -> new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver,\n                                Optional.of(newRoot), namedOwnedKeys, staticData, tree, secretLinks)), ipfs);\n    }\n\n    public CompletableFuture<Pair<WriterData, Cid>> addLink(SigningPrivateKeyAndPublicHash owner,\n                                                            long label,\n                                                            SecretLinkTarget value,\n                                                            Optional<CborObject.CborMerkleLink> existing,\n                                                            Optional<BatWithId> mirrorBat,\n                                                            TransactionId tid,\n                                                            ContentAddressedStorage ipfs,\n                                                            Hasher hasher) {\n        return (secretLinks.isEmpty() ?\n                SecretLinkChamp.createEmpty(owner.publicKeyHash, owner, mirrorBat.map(BatWithId::id), ipfs, hasher, tid)\n                        .thenCompose(root -> SecretLinkChamp.build(owner.publicKeyHash, root, mirrorBat, ipfs, hasher)) :\n                getSecretLinkChamp(owner.publicKeyHash, mirrorBat, ipfs, hasher))\n                .thenCompose(champ -> champ.add(owner, label, value, existing, mirrorBat.map(BatWithId::id), hasher, tid))\n                .thenApply(newLinksRoot -> new Pair<>(new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver,\n                        ownedKeys, namedOwnedKeys, staticData, tree, Optional.of(newLinksRoot.left)), newLinksRoot.right));\n    }\n\n    public CompletableFuture<WriterData> removeLink(SigningPrivateKeyAndPublicHash owner,\n                                                    long label,\n                                                    Optional<BatWithId> mirrorBat,\n                                                    TransactionId tid,\n                                                    ContentAddressedStorage ipfs,\n                                                    Hasher hasher) {\n        if (secretLinks.isEmpty())\n            return Futures.of(this);\n        return getSecretLinkChamp(owner.publicKeyHash, mirrorBat, ipfs, hasher)\n                .thenCompose(champ -> champ.remove(owner.publicKeyHash, owner, label, mirrorBat.map(BatWithId::id), tid))\n                .thenApply(newLinksRoot -> new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver,\n                        ownedKeys, namedOwnedKeys, staticData, tree, Optional.of(newLinksRoot)));\n    }\n\n    public CompletableFuture<Snapshot> addOwnedKeyAndCommit(PublicKeyHash owner,\n                                                            SigningPrivateKeyAndPublicHash signer,\n                                                            OwnerProof newOwned,\n                                                            MaybeMultihash currentHash,\n                                                            Optional<Long> currentSequence,\n                                                            NetworkAccess network,\n                                                            Committer c,\n                                                            TransactionId tid) {\n        return getOwnedKeyChamp(owner, network.dhtClient, network.hasher)\n                .thenCompose(champ -> champ.add(owner, signer, newOwned, network.hasher, tid)\n                        .thenApply(newRoot -> new WriterData(controller, generationAlgorithm, publicData,\n                                followRequestReceiver, Optional.of(newRoot), namedOwnedKeys, staticData, tree, secretLinks)))\n                .thenCompose(wd -> c.commit(owner, signer, wd, new CommittedWriterData(currentHash,this, currentSequence), tid));\n    }\n\n    public CompletableFuture<WriterData> removeOwnedKey(PublicKeyHash owner,\n                                                        SigningPrivateKeyAndPublicHash signer,\n                                                        PublicKeyHash ownedKey,\n                                                        ContentAddressedStorage ipfs,\n                                                        Hasher hasher) {\n\n        return getOwnedKeyChamp(owner, ipfs, hasher)\n                .thenCompose(champ -> IpfsTransaction.call(owner,\n                        tid -> champ.remove(owner, signer, ownedKey, tid), ipfs)\n                        .thenApply(newRoot -> new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver,\n                                Optional.of(newRoot), namedOwnedKeys, staticData, tree, secretLinks)));\n    }\n\n    public CompletableFuture<Boolean> ownsKey(PublicKeyHash identityKey,\n                                              PublicKeyHash ownedKey,\n                                              ContentAddressedStorage ipfs,\n                                              MutablePointers mutable,\n                                              Hasher hasher) {\n        return getOwnedKeyChamp(identityKey, ipfs, hasher)\n                .thenCompose(champ -> champ.get(identityKey, ownedKey)\n                        .thenApply(Optional::isPresent)\n                        .thenCompose(direct -> {\n                            if (direct)\n                                return Futures.of(true);\n                            return champ.applyToAllMappings(identityKey, false,\n                                    (b, p) -> {\n                                        if (b) // exit early if we find a match\n                                            return Futures.of(b);\n                                        PublicKeyHash childKey = p.left;\n                                        return UserContext.getWriterData(ipfs, mutable, identityKey, childKey)\n                                                .thenCompose(wd -> wd.props.map(w -> w.ownsKey(identityKey, ownedKey, ipfs, mutable, hasher))\n                                                        .orElse(Futures.of(false)));\n                                    },\n                                    ipfs);\n                        }));\n    }\n\n    public CompletableFuture<OwnedKeyChamp> getOwnedKeyChamp(PublicKeyHash owner, ContentAddressedStorage ipfs, Hasher hasher) {\n        return ownedKeys.map(root -> OwnedKeyChamp.build(owner, (Cid)root, ipfs, hasher))\n                .orElseThrow(() -> new IllegalStateException(\"Owned key champ absent!\"));\n    }\n\n    public CompletableFuture<SecretLinkChamp> getSecretLinkChamp(PublicKeyHash owner, Optional<BatWithId> mirrorBat, ContentAddressedStorage ipfs, Hasher hasher) {\n        return secretLinks.map(root -> SecretLinkChamp.build(owner, (Cid)root, mirrorBat, ipfs, hasher))\n                .orElseThrow(() -> new IllegalStateException(\"Owned key champ absent!\"));\n    }\n\n    public <T> CompletableFuture<Set<T>> applyToOwnedKeys(PublicKeyHash owner,\n                                                           Function<OwnedKeyChamp, CompletableFuture<Set<T>>> processor,\n                                                           ContentAddressedStorage ipfs,\n                                                           Hasher hasher) {\n        return ownedKeys.map(x -> getOwnedKeyChamp(owner, ipfs, hasher).thenCompose(processor))\n                .orElse(CompletableFuture.completedFuture(Collections.emptySet()));\n    }\n\n    public WriterData addNamedKey(String name, OwnerProof newNamedKey) {\n        Map<String, OwnerProof> updated = new TreeMap<>(namedOwnedKeys);\n        updated.put(name, newNamedKey);\n        return new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver, ownedKeys, updated, staticData, tree, secretLinks);\n    }\n\n    public WriterData withStaticData(Optional<UserStaticData> entryPoints) {\n        return new WriterData(controller, generationAlgorithm, publicData, followRequestReceiver, ownedKeys, namedOwnedKeys, entryPoints, tree, secretLinks);\n    }\n\n    public static CompletableFuture<WriterData> createEmpty(PublicKeyHash owner,\n                                                            SigningPrivateKeyAndPublicHash writer,\n                                                            ContentAddressedStorage ipfs,\n                                                            Hasher hasher,\n                                                            TransactionId tid) {\n        return OwnedKeyChamp.createEmpty(owner, writer, ipfs, hasher, tid)\n                .thenApply(ownedRoot -> new WriterData(writer.publicKeyHash,\n                        Optional.empty(),\n                        Optional.empty(),\n                        Optional.empty(),\n                        Optional.of(ownedRoot),\n                        Collections.emptyMap(),\n                        Optional.empty(),\n                        Optional.empty(),\n                        Optional.empty()));\n    }\n\n    public static CompletableFuture<WriterData> createIdentity(PublicKeyHash owner,\n                                                               SigningPrivateKeyAndPublicHash writer,\n                                                               Optional<PublicKeyHash> followRequestReceiver,\n                                                               Optional<UserStaticData> entryData,\n                                                               SecretGenerationAlgorithm algorithm,\n                                                               ContentAddressedStorage ipfs,\n                                                               Hasher hasher,\n                                                               TransactionId tid) {\n        return OwnedKeyChamp.createEmpty(owner, writer, ipfs, hasher, tid)\n                .thenApply(ownedRoot -> new WriterData(writer.publicKeyHash,\n                        Optional.of(algorithm),\n                        Optional.empty(),\n                        followRequestReceiver,\n                        Optional.of(ownedRoot),\n                        Collections.emptyMap(),\n                        entryData,\n                        Optional.empty(),\n                        Optional.empty()));\n    }\n\n    public CommittedWriterData committed(MaybeMultihash hash, Optional<Long> sequence) {\n        return new CommittedWriterData(hash, this, sequence);\n    }\n\n    public CompletableFuture<WriterData> changeKeys(String username,\n                                                    SigningPrivateKeyAndPublicHash oldSigner,\n                                                    SigningPrivateKeyAndPublicHash signer,\n                                                    SigningKeyPair newIdentity,\n                                                    PublicSigningKey newLogin,\n                                                    BoxingKeyPair followRequestReceiver,\n                                                    SymmetricKey currentKey,\n                                                    SymmetricKey newKey,\n                                                    SecretGenerationAlgorithm newAlgorithm,\n                                                    Map<PublicKeyHash, SigningPrivateKeyAndPublicHash> ownedKeys,\n                                                    NetworkAccess network) {\n        // This will upgrade legacy accounts to the new structure with secret UserStaticData\n        network.synchronizer.putEmpty(oldSigner.publicKeyHash, signer.publicKeyHash);\n        return network.synchronizer.applyUpdate(oldSigner.publicKeyHash, signer,\n                (wd, tid) -> {\n                    Optional<UserStaticData> newEntryPoints = staticData\n                            .map(sd -> {\n                                UserStaticData.EntryPoints staticData = sd.getData(currentKey);\n                                Optional<BoxingKeyPair> boxer = Optional.of(staticData.boxer.orElse(followRequestReceiver));\n                                Optional<SigningKeyPair> identity = Optional.of(staticData.identity.orElse(newIdentity));\n                                return new UserStaticData(staticData.entries, newKey, identity, boxer);\n                            });\n                    return network.account.setLoginData(new LoginData(username, newEntryPoints.get(), newLogin, Optional.empty()), oldSigner, false)\n                            .thenCompose(b -> network.hasher.sha256(followRequestReceiver.serialize())\n                                    .thenCompose(boxerHash -> oldSigner.secret.signMessage(boxerHash)\n                                            .thenCompose(signedBoxer -> network.dhtClient.putBoxingKey(oldSigner.publicKeyHash,\n                                                    signedBoxer,\n                                                    followRequestReceiver.publicBoxingKey, tid\n                                            )))).thenCompose(boxerHash -> OwnedKeyChamp.createEmpty(oldSigner.publicKeyHash, oldSigner,\n                                            network.dhtClient, network.hasher, tid)\n                                    .thenCompose(ownedRoot -> {\n                                        return Futures.combineAll(namedOwnedKeys.entrySet()\n                                                .stream()\n                                                        .map(e -> OwnerProof.build(ownedKeys.get(e.getValue().ownedKey), signer.publicKeyHash).thenApply(proof -> new Pair<>(e.getKey(), proof)))\n                                                .collect(Collectors.toList())).thenApply(res -> res.stream()\n                                                .collect(Collectors.toMap(p -> p.left, p -> p.right))).thenCompose(newNamedOwnedKeys -> {\n\n                                            // need to add all our owned keys back with the new owner, except for the new signer itself\n                                            WriterData base = new WriterData(signer.publicKeyHash,\n                                                    Optional.of(newAlgorithm),\n                                                    publicData,\n                                                    Optional.of(new PublicKeyHash(boxerHash)),\n                                                    Optional.of(ownedRoot),\n                                                    newNamedOwnedKeys,\n                                                    Optional.empty(),\n                                                    tree,\n                                        secretLinks);\n                                            return getOwnedKeyChamp(oldSigner.publicKeyHash, network.dhtClient, network.hasher)\n                                                    .thenCompose(okChamp -> okChamp.applyToAllMappings(oldSigner.publicKeyHash, base, (nwd, p) ->\n                                                            p.left.equals(signer.publicKeyHash) ? Futures.of(nwd) :\n                                                                    OwnerProof.build(ownedKeys.get(p.left), signer.publicKeyHash)\n                                                                            .thenCompose(proof -> nwd.addOwnedKey(oldSigner.publicKeyHash, signer,\n                                                                                    proof,\n                                                                                    network.dhtClient, network.hasher)), network.dhtClient)\n                                                    );\n                                        });\n                                    }));\n                })\n                .thenApply(version -> version.get(signer).props.get())\n                .exceptionally(t -> {\n                    if (t.getMessage().contains(\"cas+failed\"))\n                        throw new IllegalStateException(\"You cannot reuse a previous password!\");\n                    throw new RuntimeException(t.getCause());\n                });\n    }\n\n    public CompletableFuture<Snapshot> commit(PublicKeyHash owner,\n                                              SigningPrivateKeyAndPublicHash signer,\n                                              MaybeMultihash currentHash,\n                                              Optional<Long> currentSequence,\n                                              NetworkAccess network,\n                                              TransactionId tid) {\n        return commit(owner, signer, currentHash, currentSequence, network.mutable, network.dhtClient, network.hasher, tid)\n                .thenCompose(s -> network.commit(owner).thenApply(x -> s));\n    }\n\n    public CompletableFuture<Snapshot> commit(PublicKeyHash owner,\n                                              SigningPrivateKeyAndPublicHash signer,\n                                              MaybeMultihash currentHash,\n                                              Optional<Long> currentSequence,\n                                              MutablePointers mutable,\n                                              ContentAddressedStorage immutable,\n                                              Hasher hasher,\n                                              TransactionId tid) {\n        byte[] raw = serialize();\n\n        return hasher.sha256(raw)\n                .thenCompose(hash -> signer.secret.signMessage(hash))\n                .thenCompose(sig -> immutable.put(owner, signer.publicKeyHash, sig, raw, tid))\n                .thenCompose(blobHash -> {\n                    MaybeMultihash newHash = MaybeMultihash.of(blobHash);\n                    if (newHash.equals(currentHash)) {\n                        // nothing has changed\n                        CommittedWriterData committed = committed(newHash, currentSequence);\n                        return CompletableFuture.completedFuture(new Snapshot(signer.publicKeyHash, committed));\n                    }\n                    PointerUpdate cas = new PointerUpdate(currentHash, newHash, PointerUpdate.increment(currentSequence));\n                    return Futures.asyncExceptionally(\n                            () -> mutable.setPointer(owner, signer, cas),\n                            t -> {\n                                Throwable cause = Exceptions.getRootCause(t);\n                                if (cause instanceof PointerCasException) {\n                                    PointerCasException pce = (PointerCasException) cause;\n                                    // Server is already at newHash: a previous timed-out attempt succeeded\n                                    if (pce.existing.equals(newHash))\n                                        return Futures.of(true);\n                                }\n                                return Futures.errored(t);\n                            })\n                            .thenApply(res -> {\n                                if (!res)\n                                    throw new IllegalStateException(\"Mutable pointer update failed! Concurrent Modification.\");\n                                CommittedWriterData committed = committed(newHash, cas.sequence);\n                                return new Snapshot(signer.publicKeyHash, committed);\n                            });\n                });\n    }\n\n    public static CompletableFuture<Snapshot> commitDeletion(PublicKeyHash owner,\n                                                             SigningPrivateKeyAndPublicHash signer,\n                                                             MaybeMultihash currentHash,\n                                                             Optional<Long> currentSequence,\n                                                             MutablePointers mutable) {\n        MaybeMultihash newHash = MaybeMultihash.empty();\n        PointerUpdate cas = new PointerUpdate(currentHash, newHash, PointerUpdate.increment(currentSequence));\n        return mutable.setPointer(owner, signer, cas)\n                .thenApply(res -> {\n                    if (!res)\n                        throw new IllegalStateException(\"Mutable pointer update failed! Concurrent Modification.\");\n                    CommittedWriterData committed = new CommittedWriterData(newHash, Optional.empty(), cas.sequence);\n                    return new Snapshot(signer.publicKeyHash, committed);\n                });\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> result = new TreeMap<>();\n\n        result.put(\"controller\", new CborObject.CborMerkleLink(controller));\n        generationAlgorithm.ifPresent(alg -> result.put(\"algorithm\", alg.toCbor()));\n        publicData.ifPresent(root -> result.put(\"public\", new CborObject.CborMerkleLink(root)));\n        followRequestReceiver.ifPresent(boxer -> result.put(\"inbound\", new CborObject.CborMerkleLink(boxer)));\n        ownedKeys.ifPresent(root -> result.put(\"owned\", new CborObject.CborMerkleLink(root)));\n        if (! namedOwnedKeys.isEmpty())\n            result.put(\"named\", CborObject.CborMap.build(new HashMap<>(namedOwnedKeys)));\n        staticData.ifPresent(sd -> result.put(\"static\", sd.toCbor()));\n        tree.ifPresent(tree -> result.put(\"tree\", new CborObject.CborMerkleLink(tree)));\n        secretLinks.ifPresent(links -> result.put(\"links\", new CborObject.CborMerkleLink(links)));\n        return CborObject.CborMap.build(result);\n    }\n\n    public static WriterData fromCbor(Cborable cbor) {\n        if (! ( cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Cbor for WriterData should be a map! \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n\n        PublicKeyHash controller = m.get(\"controller\", PublicKeyHash::fromCbor);\n        Optional<SecretGenerationAlgorithm> algo  = m.getOptional(\"algorithm\", SecretGenerationAlgorithm::fromCbor);\n        Optional<Multihash> publicData = m.getOptional(\"public\", val -> ((CborObject.CborMerkleLink)val).target);\n        Optional<PublicKeyHash> followRequestReceiver = m.getOptional(\"inbound\", PublicKeyHash::fromCbor);\n        Optional<Multihash> owned = m.getOptional(\"owned\", val -> ((CborObject.CborMerkleLink)val).target);\n\n        Map<String, OwnerProof> named = m.getOptional(\"named\", c -> (CborObject.CborMap)c)\n                .map(map -> map.toMap(k -> ((CborObject.CborString)k).value, OwnerProof::fromCbor))\n                .orElseGet(() -> Collections.emptyMap());\n\n        Optional<UserStaticData> staticData = m.getOptional(\"static\", UserStaticData::fromCbor);\n        Optional<Multihash> tree = m.getOptional(\"tree\", val -> ((CborObject.CborMerkleLink)val).target);\n        Optional<Multihash> secretLinks = m.getOptional(\"links\", val -> ((CborObject.CborMerkleLink)val).target);\n        return new WriterData(controller, algo, publicData, followRequestReceiver, owned, named, staticData, tree, secretLinks);\n    }\n\n    public static CompletableFuture<CommittedWriterData> getWriterData(PublicKeyHash owner,\n                                                                       PublicKeyHash controller,\n                                                                       MutablePointers mutable,\n                                                                       ContentAddressedStorage dht) {\n        return mutable.getPointerTarget(owner, controller, dht)\n                .thenCompose(opt -> {\n                    if (! opt.updated.isPresent())\n                        throw new IllegalStateException(\"No root pointer present for controller \" + controller);\n                    return getWriterData(owner, (Cid)opt.updated.get(), opt.sequence, dht);\n                });\n    }\n\n    public static CompletableFuture<CommittedWriterData> getWriterData(PublicKeyHash owner,\n                                                                       Cid hash,\n                                                                       Optional<Long> sequence,\n                                                                       ContentAddressedStorage dht) {\n        return dht.get(owner, hash, Optional.empty())\n                .thenApply(cborOpt -> {\n                    if (! cborOpt.isPresent())\n                        throw new IllegalStateException(\"Couldn't retrieve WriterData from dht! \" + hash);\n                    return new CommittedWriterData(MaybeMultihash.of(hash), WriterData.fromCbor(cborOpt.get()), sequence);\n                });\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        WriterData that = (WriterData) o;\n        return Objects.equals(controller, that.controller) && Objects.equals(generationAlgorithm, that.generationAlgorithm) && Objects.equals(publicData, that.publicData) && Objects.equals(followRequestReceiver, that.followRequestReceiver) && Objects.equals(ownedKeys, that.ownedKeys) && Objects.equals(namedOwnedKeys, that.namedOwnedKeys) && Objects.equals(staticData, that.staticData) && Objects.equals(tree, that.tree) && Objects.equals(secretLinks, that.secretLinks);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(controller, generationAlgorithm, publicData, followRequestReceiver, ownedKeys, namedOwnedKeys, staticData, tree, secretLinks);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/app/ShareLocalAppData.java",
    "content": "package peergos.shared.user.app;\n\nimport java.nio.file.*;\nimport java.util.concurrent.*;\n\n/**\n * These are available to apps that have been granted the SHARE_LOCAL_APP_DATA permission\n */\npublic interface ShareLocalAppData {\n\n    CompletableFuture<Boolean> shareInternal(Path relativePath);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/app/StoreAppData.java",
    "content": "package peergos.shared.user.app;\n\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/**\n * These are available to apps that have been granted the STORE_APP_DATA permission\n */\npublic interface StoreAppData {\n\n    CompletableFuture<Integer> existsInternal(Path relativePath, String username);\n\n    CompletableFuture<List<String>> dirInternal(Path relativePath, String username);\n\n    CompletableFuture<byte[]> readInternal(Path relativePath, String username);\n\n    CompletableFuture<Boolean> writeInternal(Path relativePath, byte[] data, String username);\n\n    CompletableFuture<Boolean> deleteInternal(Path relativePath, String username);\n\n    CompletableFuture<String> mimeTypeInternal(Path relativePath, String username);\n\n    CompletableFuture<Boolean> createDirectoryInternal(Path relativePath, String username);\n\n    CompletableFuture<Boolean> appendInternal(Path relativePath, byte[] data, String username);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/AbsoluteCapability.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\n/** This a complete cryptographic capability to read a file or folder\n *\n */\npublic class AbsoluteCapability implements Cborable {\n\n    public final PublicKeyHash owner, writer;\n    private final byte[] mapKey;\n    public final Optional<Bat> bat; // Only absent on legacy data\n    public final SymmetricKey rBaseKey;\n    public final Optional<SymmetricKey> wBaseKey;\n\n    public AbsoluteCapability(PublicKeyHash owner,\n                              PublicKeyHash writer,\n                              byte[] mapKey,\n                              Optional<Bat> bat,\n                              SymmetricKey rBaseKey,\n                              Optional<SymmetricKey> wBaseKey) {\n        if (mapKey.length != RelativeCapability.MAP_KEY_LENGTH)\n            throw new IllegalStateException(\"Invalid map key length: \" + mapKey.length);\n        this.owner = owner;\n        this.writer = writer;\n        this.mapKey = mapKey;\n        this.bat = bat;\n        this.rBaseKey = rBaseKey;\n        this.wBaseKey = wBaseKey;\n    }\n\n    public AbsoluteCapability(PublicKeyHash owner, PublicKeyHash writer, byte[] mapKey, Optional<Bat> bat, SymmetricKey rBaseKey) {\n        this(owner, writer, mapKey, bat, rBaseKey, Optional.empty());\n    }\n\n    @JsMethod\n    public byte[] getMapKey() {\n        return Arrays.copyOf(mapKey, mapKey.length);\n    }\n\n    public Location getLocation() {\n        return new Location(owner, writer, mapKey);\n    }\n\n    @JsMethod\n    public boolean isWritable() {\n        return wBaseKey.isPresent();\n    }\n\n    public WritableAbsoluteCapability toWritable(SymmetricKey writeBaseKey) {\n        return new WritableAbsoluteCapability(owner, writer, mapKey, bat, rBaseKey, writeBaseKey);\n    }\n\n    /*  Return a capability link of the form #$owner/$writer/$mapkey+$bat/$baseKey\n     */\n    public String toLink() {\n        String encodedOwnerKey = Base58.encode(owner.serialize());\n        String encodedWriterKey = Base58.encode(writer.serialize());\n        String encodedMapKeyAndBat = Base58.encode(ArrayOps.concat(mapKey, bat.map(Bat::serialize).orElse(new byte[0])));\n        String encodedBaseKey = Base58.encode(rBaseKey.serialize());\n        return Stream.of(encodedOwnerKey, encodedWriterKey, encodedMapKeyAndBat, encodedBaseKey)\n                .collect(Collectors.joining(\"/\", \"#\", \"\"));\n    }\n\n    public static AbsoluteCapability fromLink(String keysString) {\n        if (keysString.startsWith(\"#\"))\n            keysString = keysString.substring(1);\n\n        String[] split = keysString.split(\"/\");\n        if (split.length == 4 || split.length == 5) {\n            PublicKeyHash owner = PublicKeyHash.fromCbor(CborObject.fromByteArray(Base58.decode(split[0])));\n            PublicKeyHash writer = PublicKeyHash.fromCbor(CborObject.fromByteArray(Base58.decode(split[1])));\n            byte[] mapKeyAndBat = Base58.decode(split[2]);\n            Optional<Bat> bat = mapKeyAndBat.length == 32 ?\n                    Optional.empty() :\n                    Optional.of(Bat.fromCbor(CborObject.fromByteArray(Arrays.copyOfRange(mapKeyAndBat, 32, mapKeyAndBat.length))));\n            byte[] mapKey = Arrays.copyOfRange(mapKeyAndBat, 0, 32);\n            SymmetricKey baseKey = SymmetricKey.fromByteArray(Base58.decode(split[3]));\n            if (split.length == 4)\n                return new AbsoluteCapability(owner, writer, mapKey, bat, baseKey, Optional.empty());\n            SymmetricKey baseWKey = SymmetricKey.fromByteArray(Base58.decode(split[4]));\n            return new WritableAbsoluteCapability(owner, writer, mapKey, bat, baseKey, baseWKey);\n        } else\n            throw new IllegalStateException(\"Invalid public link \"+ keysString);\n    }\n\n    public static AbsoluteCapability build(Location loc, Optional<Bat> bat, SymmetricKey key) {\n        return new AbsoluteCapability(loc.owner, loc.writer, loc.getMapKey(), bat, key);\n    }\n\n    public AbsoluteCapability readOnly() {\n        return new AbsoluteCapability(owner, writer, mapKey, bat, rBaseKey, Optional.empty());\n    }\n\n    public AbsoluteCapability withMapKey(byte[] newMapKey, Optional<Bat> newBat) {\n        return new AbsoluteCapability(owner, writer, newMapKey, newBat, rBaseKey, wBaseKey);\n    }\n\n    public AbsoluteCapability withBaseKey(SymmetricKey newBaseKey) {\n        return new AbsoluteCapability(owner, writer, mapKey, bat, newBaseKey, wBaseKey);\n    }\n\n    public AbsoluteCapability withOwner(PublicKeyHash owner) {\n        return new AbsoluteCapability(owner, writer, mapKey, bat, rBaseKey, wBaseKey);\n    }\n\n    public boolean isNull() {\n        PublicKeyHash nullUser = PublicKeyHash.NULL;\n        return nullUser.equals(owner) &&\n                nullUser.equals(writer) &&\n                Arrays.equals(getMapKey(), new byte[32]) &&\n                bat.isEmpty() &&\n                rBaseKey.equals(SymmetricKey.createNull());\n    }\n\n    public static AbsoluteCapability createNull() {\n        return new AbsoluteCapability(PublicKeyHash.NULL, PublicKeyHash.NULL, new byte[32], Optional.empty(), SymmetricKey.createNull());\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        Map<String, Cborable> cbor = new TreeMap<>();\n        cbor.put(\"o\", owner.toCbor());\n        cbor.put(\"w\", writer.toCbor());\n        cbor.put(\"m\", new CborObject.CborByteArray(mapKey));\n        bat.ifPresent(b -> cbor.put(\"a\", b));\n        cbor.put(\"k\", rBaseKey.toCbor());\n        wBaseKey.ifPresent(wk -> cbor.put(\"b\", wk.toCbor()));\n        return CborObject.CborMap.build(cbor);\n    }\n\n    public static AbsoluteCapability fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for AbsoluteCapability: \" + cbor);\n        CborObject.CborMap map = ((CborObject.CborMap) cbor);\n\n        PublicKeyHash owner = PublicKeyHash.fromCbor(map.get(\"o\"));\n        PublicKeyHash writer = PublicKeyHash.fromCbor(map.get(\"w\"));\n        byte[] mapKey = ((CborObject.CborByteArray)map.get(\"m\")).value;\n        Optional<Bat> bat = map.getOptional(\"a\", Bat::fromCbor);\n        SymmetricKey baseKey = SymmetricKey.fromCbor(map.get(\"k\"));\n        Optional<SymmetricKey> writerBaseKey = Optional.ofNullable(map.get(\"b\")).map(SymmetricKey::fromCbor);\n        if (writerBaseKey.isPresent())\n            return new WritableAbsoluteCapability(owner, writer, mapKey, bat, baseKey, writerBaseKey.get());\n        return new AbsoluteCapability(owner, writer, mapKey, bat, baseKey, writerBaseKey);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        AbsoluteCapability that = (AbsoluteCapability) o;\n        return Objects.equals(owner, that.owner) &&\n                Objects.equals(writer, that.writer) &&\n                Arrays.equals(mapKey, that.mapKey) &&\n                Objects.equals(bat, that.bat) &&\n                Objects.equals(rBaseKey, that.rBaseKey) &&\n                Objects.equals(wBaseKey, that.wBaseKey);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Objects.hash(owner, writer, bat, rBaseKey, wBaseKey);\n        result = 31 * result + Arrays.hashCode(mapKey);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/AsyncReader.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\n@JsType\npublic interface AsyncReader extends AutoCloseable {\n\n    default CompletableFuture<AsyncReader> seekJS(int high32, int low32) {\n        return seek(low32 | (((long)high32)) << 32);\n    }\n\n    @JsIgnore\n    default CompletableFuture<AsyncReader> seek(long offset) {\n        return seekJS((int)(offset >> 32), (int)offset);\n    }\n\n    /**\n     *\n     * @param res array to store data in\n     * @param offset initial index to store data in res\n     * @param length number of bytes to read\n     * @return number of bytes read\n     */\n    CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length);\n\n    /**\n     *  reset to original starting position\n     * @return\n     */\n    CompletableFuture<AsyncReader> reset();\n\n    /**\n     * Close and dispose of any resources\n     */\n    void close();\n\n    @JsIgnore\n    default <T> CompletableFuture<Long> parseStream(Function<Cborable, T> fromCbor, Consumer<T> accumulator, long maxBytesToRead) {\n        return parseStreamRecurse(new byte[0], fromCbor, accumulator, maxBytesToRead);\n    }\n\n    /** Convert reader into a stream of CborObjects\n     *\n     * @param prefix any bytes from a partial object read that will form the prefix of this read\n     * @param fromCbor The cbor converter\n     * @param accumulator The results consumer\n     * @param maxBytesToRead There must be at least this many bytes left in this stream or an EOF will result\n     * @param <T>\n     * @return\n     */\n    @JsIgnore\n    default <T> CompletableFuture<Long> parseStreamRecurse(byte[] prefix, Function<Cborable, T> fromCbor, Consumer<T> accumulator, long maxBytesToRead) {\n        if (maxBytesToRead == 0)\n            return CompletableFuture.completedFuture(0L);\n        byte[] buf = new byte[Chunk.MAX_SIZE];\n        System.arraycopy(prefix, 0, buf, 0, prefix.length);\n        ByteArrayInputStream in = new ByteArrayInputStream(buf);\n        return readIntoArray(buf, prefix.length, (int) Math.min((long)(buf.length - prefix.length), maxBytesToRead))\n                .thenCompose(bytesRead -> {\n                    for (int localOffset = 0; localOffset < bytesRead;) {\n                        try {\n                            CborObject readObject = CborObject.read(in, bytesRead);\n                            accumulator.accept(fromCbor.apply(readObject));\n                            localOffset += readObject.toByteArray().length;\n                        } catch (RuntimeException e) {\n                            int fromThisChunk = localOffset;\n                            return parseStreamRecurse(Arrays.copyOfRange(buf, localOffset, bytesRead), fromCbor, accumulator,\n                                    maxBytesToRead - bytesRead)\n                                    .thenApply(rest -> rest + fromThisChunk);\n                        }\n                    }\n                    return parseStream(fromCbor, accumulator, maxBytesToRead - bytesRead)\n                            .thenApply(rest -> rest + bytesRead);\n                });\n    }\n\n    @JsIgnore\n    default <T> CompletableFuture<Long> parseLimitedStream(Function<Cborable, T> fromCbor,\n                                                           Consumer<T> accumulator,\n                                                           int objectsToSkip,\n                                                           int maxObjectsToRead,\n                                                           long maxBytesToRead) {\n        return parseLimitedStreamRecurse(new byte[0], fromCbor, accumulator, objectsToSkip, maxObjectsToRead, maxBytesToRead);\n    }\n\n    /** Convert reader into a stream of CborObjects\n     *\n     * @param prefix any bytes from a partial object read that will form the prefix of this read\n     * @param fromCbor The cbor converter\n     * @param accumulator The results consumer\n     * @param objectsToSkip The number of objects to skip\n     * @param maxObjectsToRead There must be at least this many objects left in this stream or an EOF will result\n     * @param maxBytesToRead There must be at least this many bytes left in this stream or an EOF will result\n     * @param <T>\n     * @return\n     */\n    @JsIgnore\n    default <T> CompletableFuture<Long> parseLimitedStreamRecurse(byte[] prefix,\n                                                                  Function<Cborable, T> fromCbor,\n                                                                  Consumer<T> accumulator,\n                                                                  int objectsToSkip,\n                                                                  int maxObjectsToRead,\n                                                                  long maxBytesToRead) {\n        if (maxObjectsToRead == 0 || maxBytesToRead == 0)\n            return CompletableFuture.completedFuture(0L);\n        int toRead = (int) Math.min(Chunk.MAX_SIZE - prefix.length, maxBytesToRead);\n        byte[] buf = new byte[prefix.length + toRead];\n        System.arraycopy(prefix, 0, buf, 0, prefix.length);\n        ByteArrayInputStream in = new ByteArrayInputStream(buf);\n\n        return readIntoArray(buf, prefix.length, toRead)\n                .thenCompose(bytesRead -> {\n                    int toSkip = objectsToSkip;\n                    int objectsToRead = maxObjectsToRead;\n                    for (int localOffset = 0; localOffset < bytesRead + prefix.length;) {\n                        try {\n                            CborObject readObject = CborObject.read(in, prefix.length + bytesRead);\n                            if (toSkip > 0)\n                                toSkip--;\n                            else {\n                                objectsToRead--;\n                                accumulator.accept(fromCbor.apply(readObject));\n                            }\n                            localOffset += readObject.toByteArray().length;\n                            if (objectsToRead == 0)\n                                return Futures.of((long)localOffset);\n                        } catch (RuntimeException e) {\n                            int fromThisChunk = localOffset;\n                            return parseLimitedStreamRecurse(Arrays.copyOfRange(buf, localOffset, prefix.length + bytesRead), fromCbor,\n                                    accumulator, toSkip, objectsToRead, maxBytesToRead - bytesRead)\n                                    .thenApply(rest -> rest + fromThisChunk);\n                        }\n                    }\n                    return parseLimitedStream(fromCbor, accumulator, toSkip, objectsToRead, maxBytesToRead - bytesRead)\n                            .thenApply(rest -> rest + bytesRead);\n                });\n    }\n\n    @JsType\n    class ArrayBacked implements AsyncReader {\n        private final byte[] data;\n        private int index = 0;\n\n        public ArrayBacked(byte[] data) {\n            this.data = data;\n        }\n\n        @Override\n        public CompletableFuture<AsyncReader> seekJS(int high32, int low32) {\n            if (high32 != 0)\n                throw new IllegalArgumentException(\"Cannot have arrays larger than 4GiB!\");\n            index = low32;\n            return CompletableFuture.completedFuture(this);\n        }\n\n        @Override\n        public CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length) {\n            System.arraycopy(data, index, res, offset, length);\n            index += length;\n            return CompletableFuture.completedFuture(length);\n        }\n\n        @Override\n        public CompletableFuture<AsyncReader> reset() {\n            index = 0;\n            return CompletableFuture.completedFuture(this);\n        }\n\n        @Override\n        public void close() {\n        }\n    }\n\n    static AsyncReader build(byte[] data) {\n        return new ArrayBacked(data);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/Blake3state.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.util.ArrayOps;\n\nimport java.util.Arrays;\nimport java.util.Objects;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class Blake3state implements Cborable {\n    public final byte[] hash;\n\n    public Blake3state(byte[] hash) {\n        this.hash = hash;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"h\", new CborObject.CborByteArray(hash));\n\n        return CborObject.CborMap.build(state);\n    }\n\n    public static Blake3state fromCbor(Cborable c) {\n        byte[] hash = ((CborObject.CborMap) c).getByteArray(\"h\");\n        return new Blake3state(hash);\n    }\n\n    @Override\n    public String toString() {\n        return ArrayOps.bytesToHex(hash);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Blake3state that = (Blake3state) o;\n        return Objects.deepEquals(hash, that.hash);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(hash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/BrowserFileReader.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\n\nimport java.util.concurrent.*;\n\npublic class BrowserFileReader implements AsyncReader {\n\n    private final JSFileReader reader;\n\n    @JsConstructor\n    public BrowserFileReader(JSFileReader reader) {\n        this.reader = reader;\n    }\n\n    public JSFileReader getReader() {\n        return reader;\n    }\n\n    public CompletableFuture<AsyncReader> seekJS(int high32, int low32) {\n        return reader.seek(high32, low32).thenApply(x -> this);\n    }\n\n    /**\n     *\n     * @param res array to store data in\n     * @param offset initial index to store data in res\n     * @param length number of bytes to read\n     * @return number of bytes read\n     */\n    public CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length) {\n        return reader.readIntoArray(res, offset, length);\n    }\n\n    /**\n     *  reset to original starting position\n     * @return\n     */\n    public CompletableFuture<AsyncReader> reset() {\n        return reader.reset().thenApply(x -> this);\n    }\n\n    /**\n     * Close and dispose of any resources\n     */\n    public void close() {\n        reader.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/CapabilitiesFromUser.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class CapabilitiesFromUser implements Cborable {\n\n    private final long bytesRead;\n    private final List<CapabilityWithPath> retrievedCapabilities;\n\n    public CapabilitiesFromUser(long bytesRead, List<CapabilityWithPath> retrievedCapabilities) {\n        this.bytesRead = bytesRead;\n        this.retrievedCapabilities = retrievedCapabilities;\n    }\n\n    public long getBytesRead() {\n        return bytesRead;\n    }\n\n    public List<CapabilityWithPath> getRetrievedCapabilities() {\n        return retrievedCapabilities;\n    }\n\n    public static CapabilitiesFromUser empty() {\n        return new CapabilitiesFromUser(0L, Collections.emptyList());\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> cbor = new TreeMap<>();\n        cbor.put(\"bytes\", new CborObject.CborLong(bytesRead));\n        cbor.put(\"caps\", new CborObject.CborList(retrievedCapabilities));\n        return CborObject.CborMap.build(cbor);\n    }\n\n    public static CapabilitiesFromUser fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"CapabilitiesFromUser cbor must be a Map! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        long bytesRead = m.getLong(\"bytes\");\n        List<CapabilityWithPath> caps = m.getList(\"caps\")\n                .value.stream()\n                .map(CapabilityWithPath::fromCbor)\n                .collect(Collectors.toList());\n        return new CapabilitiesFromUser(bytesRead, caps);\n    }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/CapabilityStore.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** This class implements the mechanism by which users share Capabilities with each other\n *\n * Each unidirectional sharing relationship has a sharing folder /source_user/sharing/recipient_user/\n * In this sharing directory is an append only list of capabilities which the source user has granted to the recipient\n * user. This is implemented as a series of numbered files in the directory with a maximum number of capabilities per\n * file. Knowing the index of the capability in the overall list you can calculate the file name, and the offset in the\n * file at which the capability is stored. Write and read capabilities form logically separate append only lists.\n */\npublic class CapabilityStore {\n    public static final String CAPABILITY_CACHE_DIR = \".capabilitycache\";\n    private static final String READ_SHARING_FILE_NAME = \"sharing.r\";\n    private static final String EDIT_SHARING_FILE_NAME = \"sharing.w\";\n\n    public static CompletableFuture<Snapshot> addReadOnlySharingLinkTo(FileWrapper sharedDir,\n                                                                       AbsoluteCapability capability,\n                                                                       Committer c,\n                                                                       NetworkAccess network,\n                                                                       Crypto crypto) {\n        return addSharingLinkTo(sharedDir, capability.readOnly(), network, crypto, CapabilityStore.READ_SHARING_FILE_NAME, c);\n    }\n\n    public static CompletableFuture<Snapshot> addEditSharingLinkTo(FileWrapper sharedDir,\n                                                                   WritableAbsoluteCapability capability,\n                                                                   Committer c,\n                                                                   NetworkAccess network,\n                                                                   Crypto crypto) {\n        return addSharingLinkTo(sharedDir, capability, network, crypto, CapabilityStore.EDIT_SHARING_FILE_NAME, c);\n    }\n\n    private static CompletableFuture<Snapshot> addSharingLinkTo(FileWrapper sharedDir,\n                                                                AbsoluteCapability capability,\n                                                                NetworkAccess network,\n                                                                Crypto crypto,\n                                                                String capStoreFilename,\n                                                                Committer c) {\n        if (! sharedDir.isDirectory() || ! sharedDir.isWritable()) {\n            return Futures.errored(new IllegalArgumentException(\"Can only add link to a writable directory!\"));\n        }\n\n        return sharedDir.getChild(capStoreFilename, crypto.hasher, network)\n                .thenCompose(capStore -> {\n                    byte[] serializedCapability = capability.toCbor().toByteArray();\n                    AsyncReader.ArrayBacked newCapability = new AsyncReader.ArrayBacked(serializedCapability);\n                    long startIndex = capStore.map(f -> f.getSize()).orElse(0L);\n                    return sharedDir.uploadFileSection(sharedDir.version, c, capStoreFilename, newCapability, false,\n                            startIndex, startIndex + serializedCapability.length, Optional.empty(), false, true,\n                            false, network, crypto, () -> false, x -> {}, crypto.random.randomBytes(32),\n                            Optional.empty(), Optional.of(Bat.random(crypto.random)), sharedDir.mirrorBatId());\n                });\n    }\n\n    /**\n     *\n     * @param cacheDir\n     * @param friendSharedDir\n     * @param friendName\n     * @param network\n     * @param crypto\n     * @return the current byte index, and the valid capabilities\n     */\n    public static CompletableFuture<CapabilitiesFromUser> loadReadOnlyLinks(FileWrapper cacheDir,\n                                                                            FileWrapper friendSharedDir,\n                                                                            String friendName,\n                                                                            NetworkAccess network,\n                                                                            Crypto crypto,\n                                                                            boolean inbound) {\n        return loadSharingLinks(cacheDir, friendSharedDir, friendName, network, crypto,\n                inbound, READ_SHARING_FILE_NAME);\n    }\n\n    /**\n     *\n     * @param cacheDir\n     * @param friendName\n     * @param network\n     * @param crypto\n     * @return the current byte index, and the valid capabilities\n     */\n    public static CompletableFuture<CapabilitiesFromUser> loadCachedReadOnlyLinks(FileWrapper cacheDir,\n                                                                                  String friendName,\n                                                                                  NetworkAccess network,\n                                                                                  Crypto crypto) {\n        return loadSharingLinksCache(cacheDir, friendName, network, crypto, cacheFilename(true, READ_SHARING_FILE_NAME));\n    }\n\n    /**\n     *\n     * @param cacheDir\n     * @param friendSharedDir\n     * @param friendName\n     * @param network\n     * @param crypto\n     * @return the current byte index, and the valid capabilities\n     */\n    public static CompletableFuture<CapabilitiesFromUser> loadWriteableLinks(FileWrapper cacheDir,\n                                                                             FileWrapper friendSharedDir,\n                                                                             String friendName,\n                                                                             NetworkAccess network,\n                                                                             Crypto crypto,\n                                                                             boolean inbound) {\n\n        return loadSharingLinks(cacheDir, friendSharedDir, friendName, network, crypto,\n                inbound, EDIT_SHARING_FILE_NAME);\n    }\n\n    /**\n     *\n     * @param cacheDir\n     * @param friendName\n     * @param network\n     * @param crypto\n     * @return the current byte index, and the valid capabilities\n     */\n    public static CompletableFuture<CapabilitiesFromUser> loadCachedWriteableLinks(FileWrapper cacheDir,\n                                                                                   String friendName,\n                                                                                   NetworkAccess network,\n                                                                                   Crypto crypto) {\n\n        return loadSharingLinksCache(cacheDir, friendName, network, crypto, cacheFilename(true, EDIT_SHARING_FILE_NAME));\n    }\n\n    private static CompletableFuture<CapabilitiesFromUser> loadSharingLinks(FileWrapper cacheDir,\n                                                                            FileWrapper friendSharedDir,\n                                                                            String friendName,\n                                                                            NetworkAccess network,\n                                                                            Crypto crypto,\n                                                                            boolean inbound,\n                                                                            String capStoreFilename) {\n        return friendSharedDir.getChild(capStoreFilename, crypto.hasher, network)\n                .thenCompose(capFile -> {\n                    if (! capFile.isPresent())\n                        return CompletableFuture.completedFuture(new CapabilitiesFromUser(0, Collections.emptyList()));\n                    long capFilesize = capFile.get().getSize();\n                    String cacheFilenameSuffix = cacheFilename(inbound, capStoreFilename);\n                    return getSharingCacheFile(friendName, cacheDir, network, crypto, cacheFilenameSuffix).thenCompose(optCachedFile -> {\n                        if (! optCachedFile.isPresent()) {\n                            return readSharingFile(friendSharedDir.getName(), friendSharedDir.owner(), capFile.get(), network, crypto)\n                                    .thenCompose(res -> Futures.of(new CapabilitiesFromUser(capFilesize, res)));\n                        } else {\n                            FileWrapper cachedFile = optCachedFile.get();\n                            return readRetrievedCapabilityCache(cachedFile, network, crypto).thenCompose(cache -> {\n                                if (capFilesize == cache.getBytesRead())\n                                    return CompletableFuture.completedFuture(cache);\n                                return readSharingFile(cache.getBytesRead(), friendSharedDir.getName(),\n                                        friendSharedDir.owner(), capFile.get(), network, crypto)\n                                        .thenCompose(res -> Futures.of(new CapabilitiesFromUser(capFilesize, res)));\n                            });\n                        }\n                    });\n                });\n    }\n\n\n    private static CompletableFuture<CapabilitiesFromUser> loadSharingLinksCache(FileWrapper cacheDir,\n                                                                                 String friendName,\n                                                                                 NetworkAccess network,\n                                                                                 Crypto crypto,\n                                                                                 String capStoreFilename) {\n        return getSharingCacheFile(friendName, cacheDir, network, crypto, capStoreFilename)\n                .thenCompose(optCachedFile -> {\n                    if(! optCachedFile.isPresent()) {\n                        return CompletableFuture.completedFuture(new CapabilitiesFromUser(0, Collections.emptyList()));\n                    } else {\n                        FileWrapper cachedFile = optCachedFile.get();\n                        return readRetrievedCapabilityCache(cachedFile, network, crypto);\n                    }\n                });\n    }\n\n\n    public static CompletableFuture<CapabilitiesFromUser> loadReadAccessSharingLinksFromIndex(FileWrapper cacheDir,\n                                                                                              FileWrapper friendSharedDir,\n                                                                                              String friendName,\n                                                                                              NetworkAccess network,\n                                                                                              Crypto crypto,\n                                                                                              long startOffset,\n                                                                                              boolean saveCache,\n                                                                                              boolean inbound) {\n\n        return loadSharingLinksFromIndex(cacheDir, friendSharedDir, friendName, network, crypto,\n                startOffset, saveCache, inbound, READ_SHARING_FILE_NAME);\n    }\n\n    public static CompletableFuture<CapabilitiesFromUser> loadWriteAccessSharingLinksFromIndex(FileWrapper cacheDir,\n                                                                                               FileWrapper friendSharedDir,\n                                                                                               String friendName,\n                                                                                               NetworkAccess network,\n                                                                                               Crypto crypto,\n                                                                                               long startOffset,\n                                                                                               boolean saveCache,\n                                                                                               boolean inbound) {\n\n        return loadSharingLinksFromIndex(cacheDir, friendSharedDir, friendName, network, crypto,\n                startOffset, saveCache, inbound, EDIT_SHARING_FILE_NAME);\n    }\n\n    private static CompletableFuture<CapabilitiesFromUser> loadSharingLinksFromIndex(FileWrapper cacheDir,\n                                                                                     FileWrapper friendSharedDir,\n                                                                                     String friendName,\n                                                                                     NetworkAccess network,\n                                                                                     Crypto crypto,\n                                                                                     long startOffset,\n                                                                                     boolean saveCache,\n                                                                                     boolean inbound,\n                                                                                     String capFilename) {\n        return friendSharedDir.getChild(capFilename, crypto.hasher, network)\n                .thenCompose(file -> {\n                    if (! file.isPresent())\n                        return Futures.of(CapabilitiesFromUser.empty());\n                    long capFileSize = file.get().getSize();\n                    if (capFileSize == startOffset)\n                        return Futures.of(CapabilitiesFromUser.empty());\n                    return readSharingFile(startOffset, friendSharedDir.getName(), friendSharedDir.owner(), file.get(), network, crypto)\n                            .thenCompose(res -> Futures.of(new CapabilitiesFromUser(capFileSize - startOffset, res)));\n                });\n    }\n\n    private static String cacheFilename(boolean inbound, String suffix) {\n        return (inbound ? \"-in-\" : \"-out-\") + suffix;\n    }\n\n    public static CompletableFuture<Long> getReadOnlyCapabilityFileSize(FileWrapper friendSharedDir,\n                                                                        Crypto crypto,\n                                                                        NetworkAccess network) {\n        return getCapabilityFileSize(READ_SHARING_FILE_NAME, friendSharedDir, crypto, network);\n    }\n\n    public static CompletableFuture<Long> getEditableCapabilityFileSize(FileWrapper friendSharedDir,\n                                                                        Crypto crypto,\n                                                                        NetworkAccess network) {\n        return getCapabilityFileSize(EDIT_SHARING_FILE_NAME, friendSharedDir, crypto, network);\n    }\n\n    private static CompletableFuture<Long> getCapabilityFileSize(String filename,\n                                                                 FileWrapper friendSharedDir,\n                                                                 Crypto crypto,\n                                                                 NetworkAccess network) {\n        return friendSharedDir.getChild(filename, crypto.hasher, network)\n                .thenApply(capFile -> capFile.map(f -> f.getFileProperties().size).orElse(0L));\n    }\n\n    public static CompletableFuture<List<CapabilityWithPath>> readSharingFile(String ownerName,\n                                                                              PublicKeyHash owner,\n                                                                              FileWrapper file,\n                                                                              NetworkAccess network,\n                                                                              Crypto crypto) {\n        return readSharingFile(0, ownerName, owner, file, network, crypto);\n    }\n\n    public static CompletableFuture<List<CapabilityWithPath>> readSharingFile(long startOffset,\n                                                                              String ownerName,\n                                                                              PublicKeyHash owner,\n                                                                              FileWrapper file,\n                                                                              NetworkAccess network,\n                                                                              Crypto crypto) {\n        return file.getInputStream(network, crypto, x -> {})\n                .thenCompose(reader -> reader.seek(startOffset))\n                .thenCompose(seeked -> readSharingRecords(ownerName, owner, seeked, file.getSize() - startOffset, network));\n    }\n\n    private static CompletableFuture<List<CapabilityWithPath>> readSharingRecords(String ownerName,\n                                                                                  PublicKeyHash owner,\n                                                                                  AsyncReader reader,\n                                                                                  long maxBytesToRead,\n                                                                                  NetworkAccess network) {\n        if (maxBytesToRead == 0)\n            return CompletableFuture.completedFuture(Collections.emptyList());\n\n        List<AbsoluteCapability> caps = new ArrayList<>();\n        return reader.parseStream(AbsoluteCapability::fromCbor, caps::add, maxBytesToRead)\n                .thenCompose(bytesRead -> {\n                    return Futures.combineAllInOrder(caps.stream().map(pointer -> {\n                        EntryPoint entry = new EntryPoint(pointer, ownerName);\n                        return network.retrieveEntryPoint(entry).thenCompose(fileOpt -> {\n                            if (fileOpt.isPresent()) {\n                                try {\n                                    CompletableFuture<List<CapabilityWithPath>> res = fileOpt.get().getPath(network)\n                                            .thenApply(path -> Collections.singletonList(new CapabilityWithPath(path, pointer)));\n                                    return res;\n                                } catch (NoSuchElementException nsee) {\n                                    return Futures.errored(nsee); //a file ancestor no longer exists!?\n                                }\n                            } else {\n                                return CompletableFuture.completedFuture(Collections.<CapabilityWithPath>emptyList());\n                            }\n                        }).exceptionally(t -> Collections.<CapabilityWithPath>emptyList());\n                    }).collect(Collectors.toList()))\n                            .thenApply(res -> res.stream().flatMap(x -> x.stream()).collect(Collectors.toList()))\n                            .thenCompose(results -> readSharingRecords(ownerName, owner, reader,\n                                    maxBytesToRead - bytesRead, network)\n                                    .thenApply(recurse -> Stream.concat(results.stream(), recurse.stream())\n                                            .collect(Collectors.toList())));\n                });\n    }\n\n    private static CompletableFuture<Optional<FileWrapper>> getSharingCacheFile(String friendName,\n                                                                                FileWrapper cacheDir,\n                                                                                NetworkAccess network,\n                                                                                Crypto crypto,\n                                                                                String filenameSuffix) {\n        return cacheDir.getUpdated(network)\n                .thenCompose(updated -> updated.getChild(friendName + filenameSuffix, crypto.hasher, network));\n    }\n\n    private static CompletableFuture<CapabilitiesFromUser> readRetrievedCapabilityCache(FileWrapper cacheFile,\n                                                                                        NetworkAccess network,\n                                                                                        Crypto crypto) {\n        return cacheFile.getInputStream(network, crypto, x -> { })\n                .thenCompose(reader -> {\n                    byte[] storeData = new byte[(int) cacheFile.getSize()];\n                    return reader.readIntoArray(storeData, 0, storeData.length)\n                            .thenApply(x -> CapabilitiesFromUser.fromCbor(CborObject.fromByteArray(storeData)));\n                });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/CapabilityWithPath.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport java.util.Map;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class CapabilityWithPath implements Cborable {\n    public String path;\n    public AbsoluteCapability cap;\n\n    public CapabilityWithPath(String path, AbsoluteCapability cap) {\n        this.path = path;\n        this.cap = cap;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> cbor = new TreeMap<>();\n        cbor.put(\"p\", new CborObject.CborString(path));\n        cbor.put(\"c\", cap.toCbor());\n        return CborObject.CborMap.build(cbor);\n    }\n\n    public static CapabilityWithPath fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for CapabilityWithPath: \" + cbor);\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        String path = map.getString(\"p\");\n        AbsoluteCapability fp = map.getObject(\"c\", AbsoluteCapability::fromCbor);\n        return new CapabilityWithPath(path, fp);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        CapabilityWithPath that = (CapabilityWithPath) o;\n\n        if (path != null ? !path.equals(that.path) : that.path != null) return false;\n        return cap != null ? cap.equals(that.cap) : that.cap == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = (path != null ? path.hashCode() : 0);\n        result = 31 * result + (cap != null ? cap.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \" path:\" + path;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/Chunk.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.crypto.symmetric.SymmetricKey;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic class Chunk {\n\n    public static final int MAX_SIZE = 5 * 1024 * 1024;\n\n    private final SymmetricKey dataKey;\n    private final byte[] data, mapKey;\n    private final byte[] nonce;\n\n    public Chunk(byte[] data, SymmetricKey dataKey, byte[] mapKey, byte[] nonce) {\n        this.data = data;\n        this.dataKey = dataKey;\n        this.mapKey = mapKey;\n        this.nonce = nonce;\n    }\n\n    public SymmetricKey key() {\n        return dataKey;\n    }\n\n    public byte[] mapKey() {\n        return Arrays.copyOfRange(mapKey, 0, mapKey.length);\n    }\n\n    public byte[] nonce() {\n        return Arrays.copyOfRange(nonce, 0, nonce.length);\n    }\n\n    public byte[] data() {\n        return data;\n    }\n\n    public int length() {\n        return data.length;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/ChunkHashList.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.util.Pair;\n\nimport java.util.*;\n\npublic class ChunkHashList implements Cborable {\n\n    public final byte[] chunkHashes;\n\n    public ChunkHashList(byte[] chunkHashes) {\n        if (chunkHashes.length > 32*1024)\n            throw new IllegalStateException(\"Chunk hash list too large! \" + chunkHashes.length);\n        this.chunkHashes = chunkHashes;\n    }\n\n    public int nChunks() {\n        return chunkHashes.length/32;\n    }\n\n    public boolean equalAt(int chunkIndex, ChunkHashList other) {\n        if (other.chunkHashes.length < (chunkIndex + 1) * 32)\n            return false;\n        for (int i= chunkIndex* 32; i < (chunkIndex + 1) * 32; i++)\n            if (chunkHashes[i] != other.chunkHashes[i])\n                return false;\n        return true;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"h\", new CborObject.CborByteArray(chunkHashes));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ChunkHashList fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for ChunkHashList! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new ChunkHashList(m.getByteArray(\"h\"));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        ChunkHashList that = (ChunkHashList) o;\n        return Objects.deepEquals(chunkHashes, that.chunkHashes);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(chunkHashes);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/EncryptedCapability.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.user.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class EncryptedCapability implements Cborable {\n    public static final ScryptGenerator LINK_KEY_GENERATOR = new ScryptGenerator(15, 8, 1, 32, \"\");\n\n    public final CipherText payload;\n    public final boolean hasUserPassword;\n\n    public EncryptedCapability(CipherText payload, boolean hasUserPassword) {\n        this.payload = payload;\n        this.hasUserPassword = hasUserPassword;\n    }\n\n    @JsMethod\n    public CompletableFuture<AbsoluteCapability> decryptFromPassword(String salt, String password, Crypto c) {\n        return deriveKey(salt, password, c)\n                .thenApply(this::decrypt);\n    }\n\n    private AbsoluteCapability decrypt(SymmetricKey k) {\n        return payload.decrypt(k, AbsoluteCapability::fromCbor);\n    }\n\n    private static CompletableFuture<SymmetricKey> deriveKey(String label, String password, Crypto c) {\n        return c.hasher.hashToKeyBytes(label, password, LINK_KEY_GENERATOR)\n                .thenApply(b -> new TweetNaClKey(b, false, c.symmetricProvider, c.random));\n    }\n\n    private static EncryptedCapability create(AbsoluteCapability raw, SymmetricKey k, boolean hasUserPassword) {\n        return new EncryptedCapability(CipherText.build(k, raw), hasUserPassword);\n    }\n\n    @JsMethod\n    public static CompletableFuture<EncryptedCapability> createFromPassword(AbsoluteCapability raw, String salt, String password, boolean hasUserPassword, Crypto c) {\n        return deriveKey(salt, password, c)\n                .thenApply(k -> create(raw, k, hasUserPassword));\n    }\n\n    private static int randomInt(int limit, SafeRandom r) {\n        if (limit > 256)\n            throw new IllegalStateException(\"Limit too large!\");\n        int val = r.randomBytes(1)[0] & 0xFF;\n        if (val < limit)\n            return val;\n        return randomInt(limit, r);\n    }\n    private static final String passwordCharacters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n    public static String createLinkPassword(SafeRandom r) {\n        // want 12 characters from a-zA-Z0-9, so 62^12 ~ 2^72 possibilities,\n        // or take 100 years to crack with 1M GPUs, each trying 1M scrypt hashes/S\n        // any user supplied password is in addition to this\n        String pw = \"\";\n        for (int i=0; i < 12; i++)\n            pw += passwordCharacters.charAt(randomInt(passwordCharacters.length(), r));\n        return pw;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"c\", payload.toCbor());\n        if (hasUserPassword)\n            state.put(\"p\", new CborObject.CborBoolean(hasUserPassword));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static EncryptedCapability fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for EncryptedCapability! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new EncryptedCapability(m.get(\"c\", CipherText::fromCbor), m.getBoolean(\"p\", false));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/EncryptedChunkRetriever.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/** An instance of EncryptedChunkRetriever holds a list of fragment hashes for a chunk, and the nonce used in\n *  decrypting the resulting chunk, along with a link to the next chunk (if any).\n *\n */\npublic class EncryptedChunkRetriever implements FileRetriever {\n\n    private final FragmentedPaddedCipherText linksToData;\n    private final byte[] nextChunkLabel;\n    private final Optional<Bat> nextChunkBat;\n    private final SymmetricKey dataKey;\n\n    public EncryptedChunkRetriever(FragmentedPaddedCipherText linksToData,\n                                   byte[] nextChunkLabel,\n                                   Optional<Bat> nextChunkBat,\n                                   SymmetricKey dataKey) {\n        this.linksToData = linksToData;\n        this.nextChunkLabel = nextChunkLabel;\n        this.nextChunkBat = nextChunkBat;\n        this.dataKey = dataKey;\n    }\n\n    @Override\n    public CompletableFuture<AsyncReader> getFile(CommittedWriterData version,\n                                                  NetworkAccess network,\n                                                  Crypto crypto,\n                                                  AbsoluteCapability ourCap,\n                                                  Optional<byte[]> streamSecret,\n                                                  long fileSize,\n                                                  MaybeMultihash ourExistingHash,\n                                                  int nBufferedChunks,\n                                                  ProgressConsumer<Long> monitor) {\n        return getChunk(version, network, crypto, 0, fileSize, ourCap, streamSecret, ourExistingHash, monitor)\n                .thenApply(chunk -> {\n                    AbsoluteCapability nextChunk = ourCap.withMapKey(nextChunkLabel, nextChunkBat);\n                    Location nextChunkPointer = nextChunk.getLocation();\n                    return new LazyInputStreamCombiner(version, 0,\n                            chunk.get().chunk.data(), nextChunkPointer, nextChunkBat,\n                            chunk.get().chunk.data(), ourCap.getMapKey(), ourCap.bat, streamSecret, nextChunkPointer,\n                            nextChunkBat, network, crypto, ourCap.rBaseKey, fileSize, nBufferedChunks, monitor);\n                });\n    }\n\n    public CompletableFuture<Optional<Pair<byte[], Optional<Bat>>>> getMapLabelAt(CommittedWriterData version,\n                                                                                  AbsoluteCapability startCap,\n                                                                                  Optional<byte[]> streamSecret,\n                                                                                  long offset,\n                                                                                  Hasher hasher,\n                                                                                  NetworkAccess network) {\n        if (offset < Chunk.MAX_SIZE)\n            return CompletableFuture.completedFuture(Optional.of(new Pair<>(startCap.getMapKey(), startCap.bat)));\n        if (offset < 2*Chunk.MAX_SIZE)\n            return CompletableFuture.completedFuture(Optional.of(new Pair<>(nextChunkLabel, nextChunkBat))); // chunk at this location hasn't been written yet, only referenced by previous chunk\n        if (streamSecret.isPresent()) {\n            return FileProperties.calculateMapKey(streamSecret.get(), startCap.getMapKey(), startCap.bat, offset, hasher)\n                    .thenApply(Optional::of);\n        }\n        return network.getMetadata(version, startCap.withMapKey(nextChunkLabel, nextChunkBat))\n                .thenCompose(meta -> meta.isPresent() ?\n                        meta.get().retriever(startCap.rBaseKey, streamSecret, nextChunkLabel, nextChunkBat, hasher)\n                                .thenCompose(retriever ->\n                                        retriever.getMapLabelAt(version, startCap.withMapKey(nextChunkLabel, nextChunkBat), streamSecret,\n                                                offset - Chunk.MAX_SIZE, hasher, network)) :\n                        CompletableFuture.completedFuture(Optional.empty())\n                );\n    }\n\n    public CompletableFuture<Optional<LocatedChunk>> getChunk(CommittedWriterData version,\n                                                              NetworkAccess network,\n                                                              Crypto crypto,\n                                                              long startIndex,\n                                                              long truncateTo,\n                                                              AbsoluteCapability ourCap,\n                                                              Optional<byte[]> streamSecret,\n                                                              MaybeMultihash ourExistingHash,\n                                                              ProgressConsumer<Long> monitor) {\n        if (startIndex >= Chunk.MAX_SIZE) {\n            AbsoluteCapability nextChunkCap = ourCap.withMapKey(nextChunkLabel, nextChunkBat);\n            return network.getMetadata(version, nextChunkCap)\n                    .thenCompose(meta -> {\n                        if (meta.isPresent())\n                            return meta.get().retriever(ourCap.rBaseKey, streamSecret, nextChunkLabel, nextChunkBat, crypto.hasher)\n                                    .thenCompose(retriever -> retriever\n                                            .getChunk(version, network, crypto, startIndex - Chunk.MAX_SIZE,\n                                                    truncateTo - Chunk.MAX_SIZE,\n                                                    nextChunkCap, streamSecret, meta.get().committedHash(), l -> {}));\n                        Chunk newEmptyChunk = new Chunk(new byte[0], dataKey, nextChunkLabel, dataKey.createNonce());\n                        LocatedChunk withLocation = new LocatedChunk(nextChunkCap.getLocation(), nextChunkBat,\n                                MaybeMultihash.empty(), newEmptyChunk);\n                        return CompletableFuture.completedFuture(Optional.of(withLocation));\n                    });\n        }\n        return linksToData.getAndDecrypt(ourCap.owner, dataKey, c -> ((CborObject.CborByteArray)c).value, crypto.hasher, network, monitor)\n                .thenApply(data ->  Optional.of(new LocatedChunk(ourCap.getLocation(), ourCap.bat, ourExistingHash,\n                        new Chunk(truncate(data, (int) Math.min(Chunk.MAX_SIZE, truncateTo)),\n                                dataKey, ourCap.getMapKey(), ourCap.rBaseKey.createNonce()))));\n    }\n\n    public static byte[] truncate(byte[] in, int length) {\n        if (in.length == length)\n            return in;\n        return Arrays.copyOfRange(in, 0, length);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/ErasureFragmenter.java",
    "content": "package peergos.shared.user.fs;\n\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.fs.erasure.Erasure;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class ErasureFragmenter implements Fragmenter {\n\n    private final int nOriginalFragments;\n    private final int nAllowedFailures;\n\n    public ErasureFragmenter(int nOriginalFragments, int nAllowedFailures) {\n        this.nOriginalFragments = nOriginalFragments;\n        this.nAllowedFailures = nAllowedFailures;\n    }\n\n    @Override\n    public double storageIncreaseFactor() {\n        return ((double)(2*nAllowedFailures + nOriginalFragments)) / nOriginalFragments;\n    }\n\n    public byte[][] split(byte[] input) {\n        return Erasure.split(input, nOriginalFragments, nAllowedFailures);\n    }\n\n    public byte[] recombine(byte[][] encoded, int startOffset, int truncateLength) {\n        // truncateTo should be  input.length\n        byte[] withoutPrefix = Erasure.recombine(encoded, truncateLength, nOriginalFragments, nAllowedFailures);\n        byte[] withPrefix = new byte[startOffset + withoutPrefix.length];\n        System.arraycopy(withoutPrefix, 0, withPrefix, startOffset, withoutPrefix.length);\n        return withPrefix;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> res = new HashMap<>();\n        res.put(\"t\", new CborObject.CborLong(Type.ERASURE_CODING.val));\n        res.put(\"o\", new CborObject.CborLong(nOriginalFragments));\n        res.put(\"a\", new CborObject.CborLong(nAllowedFailures));\n        return CborObject.CborMap.build(res);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        ErasureFragmenter that = (ErasureFragmenter) o;\n\n        if (nOriginalFragments != that.nOriginalFragments) return false;\n        return nAllowedFailures == that.nAllowedFailures;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = nOriginalFragments;\n        result = 31 * result + nAllowedFailures;\n        return result;\n    }\n\n    public static final Set<Integer> ALLOWED_ORIGINAL = Stream.of(5, 10, 20, 40, 80).collect(Collectors.toSet());\n    public static final Set<Integer> ALLOWED_FAILURES = Stream.of(5, 10, 20, 40, 80).collect(Collectors.toSet());\n    public static final int ERASURE_ORIGINAL = 40; // mean 128 KiB fragments, could also use 80, 20, 10, 5\n    public static final int ERASURE_ALLOWED_FAILURES = 10; // generates twice this extra fragments\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/FileExistsException.java",
    "content": "package peergos.shared.user.fs;\n\npublic class FileExistsException extends RuntimeException {\n\n    public FileExistsException(String filename){\n        super(\"File already exists with name \" + filename);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/FileProperties.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\n/** The FileProperties class represents metadata for a file or directory\n *\n *  In the case of a directory, the only properties present are the name, modification time and isHidden.\n *\n */\n@JsType\npublic class FileProperties implements Cborable {\n    public static final int MAX_FILE_NAME_SIZE = 255;\n    public static final int MAX_PATH_SIZE = 4096;\n    public static final FileProperties EMPTY = new FileProperties(\".subsequent-dir-chunk\", true, false, \"\", 0,\n            LocalDateTime.MIN, LocalDateTime.MIN, false, Optional.empty(), Optional.empty(), Optional.empty());\n\n    public final String name;\n    public final boolean isDirectory;\n    public final boolean isLink;\n    public final String mimeType;\n    @JsIgnore\n    public final long size;\n    public final LocalDateTime modified;\n    public final LocalDateTime created;\n    public final boolean isHidden;\n    public final Optional<Thumbnail> thumbnail;\n    public final Optional<byte[]> streamSecret;\n    public final Optional<HashBranch> treeHash;\n\n    public FileProperties(String name,\n                          boolean isDirectory,\n                          boolean isLink,\n                          String mimeType,\n                          int sizeHi, int sizeLo,\n                          LocalDateTime modified,\n                          LocalDateTime created,\n                          boolean isHidden,\n                          Optional<Thumbnail> thumbnail,\n                          Optional<byte[]> streamSecret,\n                          Optional<HashBranch> treeHash) {\n        if (name.length() > MAX_FILE_NAME_SIZE)\n            throw new IllegalStateException(\"File and directory names must be less than 256 characters.\");\n        if (isDirectory && streamSecret.isPresent())\n            throw new IllegalStateException(\"Directories cannot have stream secrets!\");\n        if (name.contains(\"/\"))\n            throw new IllegalStateException(\"Invalid character in filename!\");\n        if (name.equals(\".\") || name.equals(\"..\") || name.isEmpty())\n            throw new IllegalStateException(\"Invalid filename: \" + name);\n        this.name = name;\n        this.isDirectory = isDirectory;\n        this.isLink = isLink;\n        this.mimeType = mimeType;\n        this.size = (sizeLo & 0xFFFFFFFFL) | ((sizeHi | 0L) << 32);\n        this.modified = modified;\n        this.created = created;\n        this.isHidden = isHidden;\n        this.thumbnail = thumbnail;\n        this.streamSecret = streamSecret;\n        this.treeHash = treeHash;\n    }\n\n    @JsIgnore\n    public FileProperties(String name,\n                          boolean isDirectory,\n                          boolean isLink,\n                          String mimeType,\n                          long size,\n                          LocalDateTime modified,\n                          LocalDateTime created,\n                          boolean isHidden,\n                          Optional<Thumbnail> thumbnail,\n                          Optional<byte[]> streamSecret,\n                          Optional<HashBranch> treeHash) {\n        this(name, isDirectory, isLink, mimeType, (int)(size >> 32), (int) size, modified, created, isHidden, thumbnail,\n                streamSecret, treeHash);\n    }\n\n    /** Override this properties name with the link's name\n     *\n     * @param link\n     * @return\n     */\n    public FileProperties withLink(FileProperties link) {\n        return new FileProperties(link.name, isDirectory, false, mimeType, size, modified, created, isHidden, thumbnail, streamSecret, treeHash);\n    }\n\n    public static void ensureValidParsedPath(Path path) {\n        ensureValidPath(path.toString());\n    }\n\n    @JsMethod\n    public static void ensureValidPath(String path) {\n        if (path.length() > MAX_PATH_SIZE)\n            throw new IllegalArgumentException(\"Path too long! Paths must be smaller than \" + MAX_PATH_SIZE);\n    }\n\n    public static CompletableFuture<Pair<byte[], Optional<Bat>>> calculateMapKey(byte[] streamSecret,\n                                                                                 byte[] firstMapKey,\n                                                                                 Optional<Bat> firstBat,\n                                                                                 long offset,\n                                                                                 Hasher h) {\n        long iterations = offset / Chunk.MAX_SIZE;\n        List<Long> counter = new ArrayList<>();\n        for (long i=0; i < iterations; i++)\n            counter.add(i);\n        return Futures.reduceAll(counter, new Pair<>(firstMapKey, firstBat),\n                (current, i) -> calculateNextMapKey(streamSecret, current.left, current.right, h), (a, b) -> b);\n    }\n\n    private static <V> List<V> list(V elem) {\n        List<V> res = new ArrayList<>();\n        res.add(elem);\n        return res;\n    }\n\n    private static <V> List<V> add(List<V> start, V elem) { // needed for gwt\n        start.add(elem);\n        return start;\n    }\n    public static CompletableFuture<List<Pair<byte[], Optional<Bat>>>> calculateSubsequentMapKeys(byte[] streamSecret,\n                                                                                                  byte[] firstMapKey,\n                                                                                                  Optional<Bat> firstBat,\n                                                                                                  int nChunks,\n                                                                                                  Hasher h) {\n        List<Long> counter = new ArrayList<>();\n        for (long i=0; i < nChunks; i++)\n            counter.add(i);\n        List<Pair<byte[], Optional<Bat>>> first = list(new Pair<>(firstMapKey, firstBat));\n        return Futures.reduceAll(counter, first,\n                (current, i) -> calculateNextMapKey(streamSecret,\n                        current.get(current.size() - 1).left,\n                        current.get(current.size() - 1).right, h)\n                        .thenApply(next -> add(current, next)),\n        (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toList()));\n    }\n\n    public static CompletableFuture<Pair<byte[], Optional<Bat>>> calculateNextMapKey(byte[] streamSecret,\n                                                                                     byte[] currentMapKey,\n                                                                                     Optional<Bat> currentBat,\n                                                                                     Hasher h) {\n        return h.sha256(ArrayOps.concat(streamSecret, currentMapKey))\n                .thenCompose(nextMapKey -> (currentBat.isPresent() ?\n                        h.sha256(ArrayOps.concat(streamSecret, currentBat.get().secret))\n                                .thenApply(Bat::new).thenApply(Optional::of) :\n                        Futures.of(Optional.<Bat>empty()))\n                        .thenApply(nextBat -> new Pair<>(nextMapKey, nextBat)));\n    }\n\n    public int sizeLow() {\n        return (int) size;\n    }\n\n    public int sizeHigh() {\n        return (int) (size >> 32);\n    }\n\n    public int chunkCount() {\n        return FileWrapper.getNumberOfChunks(size);\n    }\n\n    @JsMethod\n    public boolean isSocialPost() {\n        return MimeTypes.PEERGOS_POST.equals(mimeType);\n    }\n\n    @Override\n    @SuppressWarnings(\"unusable-by-js\")\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"d\", new CborObject.CborBoolean(isDirectory));\n        state.put(\"l\", new CborObject.CborBoolean(isLink));\n        state.put(\"n\", new CborObject.CborString(name));\n        state.put(\"m\", new CborObject.CborString(mimeType));\n        state.put(\"s\", new CborObject.CborLong(size));\n        state.put(\"t\", new CborObject.CborLong(modified.toEpochSecond(ZoneOffset.UTC)));\n        state.put(\"tn\", new CborObject.CborLong(modified.getNano()));\n        state.put(\"c\", new CborObject.CborLong(created.toEpochSecond(ZoneOffset.UTC)));\n        state.put(\"cn\", new CborObject.CborLong(created.getNano()));\n        state.put(\"h\", new CborObject.CborBoolean(isHidden));\n        treeHash.ifPresent(b -> state.put(\"th\", b.toCbor()));\n        thumbnail.ifPresent(thumb -> state.put(\"i\", new CborObject.CborByteArray(thumb.data)));\n        thumbnail.ifPresent(thumb -> state.put(\"im\", new CborObject.CborString(thumb.mimeType)));\n        streamSecret.ifPresent(secret -> state.put(\"p\", new CborObject.CborByteArray(secret)));\n        return CborObject.CborMap.build(state);\n    }\n\n    @SuppressWarnings(\"unusable-by-js\")\n    public static FileProperties fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for FileProperties! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        boolean isDirectory = m.getBoolean(\"d\");\n        boolean isLink = m.getBoolean(\"l\", false);\n        String name = m.getString(\"n\");\n        String mimeType = m.getString(\"m\");\n        long size = m.getLong(\"s\");\n        long modifiedEpochSeconds = m.getLong(\"t\");\n        int modifiedNano = m.getOptionalLong(\"tn\").orElse(0L).intValue();\n        long createdEpochSeconds = m.getOptionalLong(\"c\").orElse(modifiedEpochSeconds);\n        int createdNano = m.getOptionalLong(\"cn\").orElse(0L).intValue();\n        boolean isHidden = m.getBoolean(\"h\");\n        Optional<byte[]> thumbnailData = m.getOptionalByteArray(\"i\");\n        Optional<Thumbnail> thumbnail = thumbnailData.map(d -> new Thumbnail(m.getString(\"im\", \"image/png\"), d));\n        Optional<byte[]> streamSecret = m.getOptionalByteArray(\"p\");\n        Optional<HashBranch> th = m.getOptional(\"th\", HashBranch::fromCbor);\n\n        LocalDateTime modified = LocalDateTime.ofEpochSecond(modifiedEpochSeconds, modifiedNano, ZoneOffset.UTC);\n        LocalDateTime created = LocalDateTime.ofEpochSecond(createdEpochSeconds, createdNano, ZoneOffset.UTC);\n        return new FileProperties(name, isDirectory, isLink, mimeType, size, modified, created, isHidden, thumbnail,\n                streamSecret, th);\n    }\n\n    @JsIgnore\n    public FileProperties withSize(long newSize) {\n        return new FileProperties(name, isDirectory, isLink, mimeType, newSize, modified, created, isHidden, thumbnail, streamSecret, Optional.empty());\n    }\n\n    public FileProperties withHash(Optional<HashBranch> treeHash) {\n        return new FileProperties(name, isDirectory, isLink, mimeType, size, modified, created, isHidden, thumbnail, streamSecret, treeHash);\n    }\n\n    public FileProperties withNoThumbnail() {\n        return new FileProperties(name, isDirectory, isLink, mimeType, size, modified, created, isHidden, Optional.empty(), streamSecret, treeHash);\n    }\n    public FileProperties withThumbnail(Optional<Thumbnail> newThumbnail) {\n        return new FileProperties(name, isDirectory, isLink, mimeType, size, modified, created, isHidden, newThumbnail, streamSecret, treeHash);\n    }\n\n    public FileProperties withModified(LocalDateTime modified) {\n        return new FileProperties(name, isDirectory, isLink, mimeType, size, modified, created, isHidden, thumbnail, streamSecret, treeHash);\n    }\n\n    public FileProperties withNewStreamSecret(byte[] streamSecret) {\n        return new FileProperties(name, isDirectory, isLink, mimeType, size, modified, created, isHidden, thumbnail, Optional.of(streamSecret), treeHash);\n    }\n\n    public FileProperties asLink() {\n        return new FileProperties(name, isDirectory, true, mimeType, size, modified, created, isHidden, thumbnail, streamSecret, treeHash);\n    }\n\n    public String getType() {\n        return getType(mimeType, isDirectory);\n    }\n\n    @JsMethod\n    public static String getType(String mimeType, boolean isDirectory) {\n        if (isDirectory)\n            return \"dir\";\n        if (mimeType.equals(\"text/calendar\"))\n            return \"calendar\";\n        if (mimeType.equals(\"text/vcard\"))\n            return \"contact file\";\n        if (mimeType.startsWith(\"image\"))\n            return \"image\";\n        if (mimeType.startsWith(\"audio\"))\n            return \"audio\";\n        if (mimeType.startsWith(\"video\"))\n            return \"video\";\n        if (mimeType.startsWith(\"text\"))\n            return \"text\";\n        if (mimeType.equals(\"application/pdf\"))\n            return \"pdf\";\n        if (mimeType.equals(\"application/zip\"))\n            return \"zip\";\n        if (mimeType.equals(\"application/json\"))\n            return \"text\";\n        if (mimeType.equals(\"application/java-archive\"))\n            return \"java-archive\";\n\n        if (mimeType.equals(\"application/vnd.openxmlformats-officedocument.presentationml.presentation\"))\n            return \"powerpoint presentation\";\n        if (mimeType.equals(\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"))\n            return \"word document\";\n        if (mimeType.equals(\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"))\n            return \"excel spreadsheet\";\n\n        if (mimeType.equals(\"application/vnd.oasis.opendocument.text\"))\n            return \"text document\";\n        if (mimeType.equals(\"application/vnd.oasis.opendocument.spreadsheet\"))\n            return \"spreadsheet\";\n        if (mimeType.equals(\"application/vnd.oasis.opendocument.presentation\"))\n            return \"presentation\";\n        return \"file\";\n    }\n\n    @Override\n    public String toString() {\n        return \"FileProperties{\" +\n                \"name='\" + name + '\\'' +\n                \", size=\" + size +\n                \", modified=\" + modified +\n                \", created=\" + modified +\n                \", isHidden=\" + isHidden +\n                \", thumbnail=\" + thumbnail +\n                '}';\n    }\n}"
  },
  {
    "path": "src/peergos/shared/user/fs/FileRetriever.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic interface FileRetriever {\n\n    CompletableFuture<AsyncReader> getFile(CommittedWriterData version,\n                                           NetworkAccess network,\n                                           Crypto crypto,\n                                           AbsoluteCapability ourCap,\n                                           Optional<byte[]> streamSecret,\n                                           long fileSize,\n                                           MaybeMultihash ourExistingHash,\n                                           int nBufferedChunks,\n                                           ProgressConsumer<Long> monitor);\n\n    CompletableFuture<Optional<Pair<byte[], Optional<Bat>>>> getMapLabelAt(CommittedWriterData version,\n                                                                           AbsoluteCapability startCap,\n                                                                           Optional<byte[]> streamSecret,\n                                                                           long offset,\n                                                                           Hasher hasher,\n                                                                           NetworkAccess network);\n\n    CompletableFuture<Optional<LocatedChunk>> getChunk(CommittedWriterData version,\n                                                       NetworkAccess network,\n                                                       Crypto crypto,\n                                                       long startIndex,\n                                                       long truncateTo,\n                                                       AbsoluteCapability ourCap,\n                                                       Optional<byte[]> streamSecret,\n                                                       MaybeMultihash ourExistingHash,\n                                                       ProgressConsumer<Long> monitor);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/FileUploader.java",
    "content": "package peergos.shared.user.fs;\nimport java.util.function.*;\nimport java.util.logging.*;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.*;\n\npublic class FileUploader implements AutoCloseable {\n\tprivate static final Logger LOG = Logger.getLogger(FileUploader.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n\n    private final String name;\n    private final long offset, length;\n    private final FileProperties props;\n    private final Optional<HashTree> hash;\n    private final SymmetricKey baseKey;\n    private final SymmetricKey dataKey;\n    private final long nchunks;\n    private final Location parentLocation;\n    private final Optional<Bat> parentBat;\n    private final SymmetricKey parentparentKey;\n    private final ProgressConsumer<Long> monitor;\n    private final AsyncReader reader; // resettable input stream\n    private final byte[] firstLocation;\n    private final Optional<Bat> firstBat;\n    private final Supplier<Boolean> isCancelled;\n\n    public FileUploader(String name, AsyncReader fileData,\n                        int offsetHi, int offsetLow, int lengthHi, int lengthLow,\n                        SymmetricKey baseKey,\n                        SymmetricKey dataKey,\n                        Location parentLocation,\n                        Optional<Bat> parentBat,\n                        SymmetricKey parentparentKey,\n                        ProgressConsumer<Long> monitor,\n                        FileProperties fileProperties,\n                        Optional<HashTree> hash,\n                        byte[] firstLocation,\n                        Optional<Bat> firstBat,\n                        Supplier<Boolean> isCancelled) {\n        long length = (lengthLow & 0xFFFFFFFFL) + ((lengthHi & 0xFFFFFFFFL) << 32);\n        this.props = fileProperties;\n        this.hash = hash;\n        if (baseKey == null) baseKey = SymmetricKey.random();\n\n        long offset = (offsetLow & 0xFFFFFFFFL) + ((offsetHi & 0xFFFFFFFFL) << 32);\n\n        // Process and upload chunk by chunk to avoid running out of RAM, in reverse order to build linked list\n        this.nchunks = length > 0 ? (length + Chunk.MAX_SIZE - 1) / Chunk.MAX_SIZE : 1;\n        this.name = name;\n        this.offset = offset;\n        this.length = length;\n        this.reader = fileData;\n        this.baseKey = baseKey;\n        this.dataKey = dataKey;\n        this.parentLocation = parentLocation;\n        this.parentBat = parentBat;\n        this.parentparentKey = parentparentKey;\n        this.monitor = monitor;\n        this.firstLocation = firstLocation;\n        this.firstBat = firstBat;\n        this.isCancelled = isCancelled;\n    }\n\n    public FileUploader(String name, AsyncReader fileData, long offset, long length,\n                        SymmetricKey baseKey, SymmetricKey dataKey, Location parentLocation, Optional<Bat> parentBat,\n                        SymmetricKey parentparentKey, ProgressConsumer<Long> monitor, FileProperties fileProperties,\n                        Optional<HashTree> hash, byte[] firstLocation, Optional<Bat> firstBat, Supplier<Boolean> isCancelled) {\n        this(name, fileData, (int)(offset >> 32), (int) offset, (int) (length >> 32), (int) length,\n                baseKey, dataKey, parentLocation, parentBat, parentparentKey, monitor, fileProperties, hash, firstLocation, firstBat, isCancelled);\n    }\n\n    private static class AsyncUploadQueue {\n        private final LinkedList<CompletableFuture<ChunkUpload>> toUpload = new LinkedList<>();\n        private final LinkedList<CompletableFuture<Boolean>> waitingWorkers = new LinkedList<>();\n        private final LinkedList<CompletableFuture<ChunkUpload>> waitingUploaders = new LinkedList<>();\n        private static final int MAX_QUEUE_SIZE = 10;\n\n        public synchronized CompletableFuture<Boolean> add(ChunkUpload chunk) {\n            if (! waitingUploaders.isEmpty()) {\n                waitingUploaders.poll().complete(chunk);\n                return Futures.of(true);\n            }\n            toUpload.add(Futures.of(chunk));\n            if (toUpload.size() < MAX_QUEUE_SIZE) {\n                return Futures.of(true);\n            }\n            CompletableFuture<Boolean> wait = new CompletableFuture<>();\n            waitingWorkers.add(wait);\n            return wait;\n        }\n\n        public synchronized CompletableFuture<ChunkUpload> poll() {\n            if (! toUpload.isEmpty()) {\n                CompletableFuture<ChunkUpload> res = toUpload.poll();\n                if (! waitingWorkers.isEmpty()) {\n                    CompletableFuture<Boolean> worker = waitingWorkers.poll();\n                    Futures.runAsync(() -> Futures.of(worker.complete(true)));\n                }\n                return res;\n            }\n            CompletableFuture<ChunkUpload> wait = new CompletableFuture<>();\n            waitingUploaders.add(wait);\n            return wait;\n        }\n    }\n\n    public CompletableFuture<Snapshot> upload(Snapshot current,\n                                              Committer c,\n                                              NetworkAccess network,\n                                              PublicKeyHash owner,\n                                              SigningPrivateKeyAndPublicHash writer,\n                                              Optional<BatId> mirrorBat,\n                                              SafeRandom random,\n                                              Hasher hasher) {\n        return uploadFrom(current, c, network, 0, owner, writer, mirrorBat, random, hasher);\n    }\n\n    public CompletableFuture<Snapshot> uploadFrom(Snapshot current,\n                                                  Committer c,\n                                                  NetworkAccess network,\n                                                  int startChunkIndex,\n                                                  PublicKeyHash owner,\n                                                  SigningPrivateKeyAndPublicHash writer,\n                                                  Optional<BatId> mirrorBat,\n                                                  SafeRandom random,\n                                                  Hasher hasher) {\n        return reader.seek(startChunkIndex * Chunk.MAX_SIZE).thenCompose(seeked -> {\n            long t1 = System.currentTimeMillis();\n\n            AsyncUploadQueue queue = new AsyncUploadQueue();\n            List<Integer> input = IntStream.range(startChunkIndex, (int) nchunks).mapToObj(i -> Integer.valueOf(i)).collect(Collectors.toList());\n            CompletableFuture<Snapshot> res = new CompletableFuture<>();\n            Futures.reduceAll(input, true,\n                            (p, i) -> Futures.runAsync(() -> encryptChunk(i, owner, writer, mirrorBat, MaybeMultihash.empty(), random, hasher, network.isJavascript())\n                                    .thenCompose(queue::add)),\n                            (a, b) -> b)\n                    .exceptionally(res::completeExceptionally);\n            Futures.reduceAll(input, current,\n                            (s, i) -> queue.poll().thenCompose(chunk -> uploadChunk(s, c, chunk, writer, network, monitor)),\n                            (a, b) -> b)\n                    .thenApply(x -> {\n                        LOG.info(\"File encryption, upload took: \" + (System.currentTimeMillis() - t1) + \" mS\");\n                        return x;\n                    }).thenApply(res::complete)\n                    .exceptionally(res::completeExceptionally);\n\n            return res;\n        });\n    }\n\n    private static class ChunkUpload {\n        public final LocatedChunk chunk;\n        public final CryptreeNode metadata;\n        public final List<FragmentWithHash> fragments;\n\n        public ChunkUpload(LocatedChunk chunk, CryptreeNode metadata, List<FragmentWithHash> fragments) {\n            this.chunk = chunk;\n            this.metadata = metadata;\n            this.fragments = fragments;\n        }\n    }\n\n    public CompletableFuture<ChunkUpload> encryptChunk(\n            long chunkIndex,\n            PublicKeyHash owner,\n            SigningPrivateKeyAndPublicHash writer,\n            Optional<BatId> mirrorBat,\n            MaybeMultihash ourExistingHash,\n            SafeRandom random,\n            Hasher hasher,\n            boolean isJS) {\n        if (isCancelled.get())\n            throw new IllegalStateException(\"Upload cancelled!\");\n        LOG.info(\"encrypting chunk: \"+chunkIndex + \" of \"+name);\n        long position = chunkIndex * Chunk.MAX_SIZE;\n\n        long fileLength = length;\n        boolean isLastChunk = fileLength < position + Chunk.MAX_SIZE;\n        int length =  isLastChunk ? (int)(fileLength -  position) : Chunk.MAX_SIZE;\n        byte[] data = new byte[length];\n        return reader.readIntoArray(data, 0, data.length).thenCompose(b -> {\n            byte[] nonce = baseKey.createNonce();\n            return FileProperties.calculateMapKey(props.streamSecret.get(), firstLocation, firstBat,\n                    chunkIndex * Chunk.MAX_SIZE, hasher)\n                    .thenCompose(mapKeyAndBat -> {\n                        Chunk rawChunk = new Chunk(data, dataKey, mapKeyAndBat.left, nonce);\n                        LocatedChunk chunk = new LocatedChunk(new Location(owner, writer.publicKeyHash, rawChunk.mapKey()), mapKeyAndBat.right, ourExistingHash, rawChunk);\n                        return FileProperties.calculateNextMapKey(props.streamSecret.get(), mapKeyAndBat.left, mapKeyAndBat.right, hasher)\n                                .thenCompose(nextMapKeyAndBat -> {\n                                    Optional<Bat> nextChunkBat = nextMapKeyAndBat.right;\n                                    Location nextChunkLocation = new Location(owner, writer.publicKeyHash, nextMapKeyAndBat.left);\n                                    if (! writer.publicKeyHash.equals(chunk.location.writer))\n                                        throw new IllegalStateException(\"Trying to write a chunk to the wrong signing key space!\");\n                                    RelativeCapability nextChunk = RelativeCapability.buildSubsequentChunk(nextChunkLocation.getMapKey(), nextChunkBat, baseKey);\n                                    return CryptreeNode.createFile(chunk.existingHash, chunk.location.writer, baseKey,\n                                                    chunk.chunk.key(), chunkIndex % 1024 == 0 ?\n                                                            props.withHash(hash.map(t -> t.branch(chunkIndex))) :\n                                                            props, chunk.chunk.data(), parentLocation, parentBat, parentparentKey, nextChunk,\n                                                    chunk.bat, mirrorBat, random, hasher, isJS)\n                                            .thenApply(p -> new ChunkUpload(chunk, p.left, p.right));\n                                });\n                    });\n        });\n    }\n\n    public static CompletableFuture<Snapshot> uploadChunk(Snapshot current,\n                                                          Committer committer,\n                                                          ChunkUpload file,\n                                                          SigningPrivateKeyAndPublicHash writer,\n                                                          NetworkAccess network,\n                                                          ProgressConsumer<Long> monitor) {\n        CryptreeNode metadata = file.metadata;\n        LocatedChunk chunk = file.chunk;\n\n        List<Fragment> fragments = file.fragments.stream()\n                .filter(f -> !f.isInlined())\n                .map(f -> f.fragment)\n                .collect(Collectors.toList());\n        CappedProgressConsumer progress = new CappedProgressConsumer(monitor, chunk.chunk.length());\n        if (fragments.size() < file.fragments.size() || fragments.isEmpty())\n            progress.accept((long) chunk.chunk.length());\n        LOG.info(\"Uploading chunk with \" + fragments.size() + \" fragments to mapkey \" + chunk.location.toString() + \"\\n\");\n        return IpfsTransaction.call(chunk.location.owner,\n                tid -> network.uploadFragments(fragments, chunk.location.owner, writer, progress, tid)\n                        .thenCompose(hashes -> network.uploadChunk(current, committer, metadata, chunk.location.owner,\n                                chunk.chunk.mapKey(), writer, tid)),\n                network.dhtClient);\n    }\n\n    public static CompletableFuture<Snapshot> uploadChunk(Snapshot current,\n                                                          Committer committer,\n                                                          SigningPrivateKeyAndPublicHash writer,\n                                                          FileProperties props,\n                                                          Location parentLocation,\n                                                          Optional<Bat> parentBat,\n                                                          SymmetricKey parentparentKey,\n                                                          SymmetricKey baseKey,\n                                                          LocatedChunk chunk,\n                                                          Location nextChunkLocation,\n                                                          Optional<Bat> nextChunkBat,\n                                                          Optional<SymmetricLinkToSigner> writerLink,\n                                                          Optional<BatId> mirrorBat,\n                                                          SafeRandom random,\n                                                          Hasher hasher,\n                                                          NetworkAccess network,\n                                                          ProgressConsumer<Long> monitor) {\n        CappedProgressConsumer progress = new CappedProgressConsumer(monitor, chunk.chunk.length());\n        if (! writer.publicKeyHash.equals(chunk.location.writer))\n            throw new IllegalStateException(\"Trying to write a chunk to the wrong signing key space!\");\n        RelativeCapability nextChunk = RelativeCapability.buildSubsequentChunk(nextChunkLocation.getMapKey(), nextChunkBat, baseKey);\n        return CryptreeNode.createFile(chunk.existingHash, chunk.location.writer, baseKey,\n                chunk.chunk.key(), props, chunk.chunk.data(), parentLocation, parentBat, parentparentKey, nextChunk,\n                chunk.bat, mirrorBat, random, hasher, network.isJavascript())\n                .thenCompose(file -> uploadChunk(current, committer, new ChunkUpload(chunk, file.left.withWriterLink(baseKey, writerLink), file.right),\n                        writer, network, progress));\n    }\n\n    public void close() {\n        reader.close();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/FileWrapper.java",
    "content": "package peergos.shared.user.fs;\nimport java.nio.file.*;\nimport java.util.logging.*;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.user.fs.transaction.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** This class is used to read and modify files and directories and represents a single file or directory and the keys\n *  to access it.\n *\n */\npublic class FileWrapper {\n    public static final long THUMBNAIL_PROGRESS_OFFSET = 20*1024;\n\tprivate static final Logger LOG = Logger.getGlobal();\n\n    private final static int THUMBNAIL_SIZE = 400;\n    private static final NativeJSThumbnail thumbnail = new NativeJSThumbnail();\n\n    private final RetrievedCapability pointer;\n    private final Optional<RetrievedCapability> linkPointer;\n    private final Optional<SigningPrivateKeyAndPublicHash> entryWriter;\n    private final FileProperties props;\n    private final String ownername;\n    private final Optional<TrieNode> capTrie;\n    public final Snapshot version;\n    private final boolean isWritable;\n    private AtomicBoolean modified = new AtomicBoolean(); // This only used as a guard against concurrent modifications\n\n    /**\n     *\n     * @param capTrie This is only present if this is the global root, or this is read only\n     * @param pointer\n     * @param ownername\n     */\n    public FileWrapper(Optional<TrieNode> capTrie,\n                       RetrievedCapability pointer,\n                       Optional<RetrievedCapability> linkPointer,\n                       Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                       String ownername,\n                       Snapshot version) {\n        this.capTrie = capTrie;\n        this.pointer = pointer;\n        this.linkPointer = linkPointer;\n        this.entryWriter = entryWriter;\n        this.ownername = ownername;\n        this.version = version;\n        this.isWritable = pointer != null &&\n                pointer.capability instanceof WritableAbsoluteCapability ||\n                entryWriter.map(s -> s.publicKeyHash.equals(pointer.capability.writer)).orElse(false);\n        if (pointer != null) {\n            SymmetricKey parentKey = this.getParentKey();\n            FileProperties directProps = pointer.fileAccess.getProperties(parentKey);\n            if (linkPointer.isPresent()) {\n                RetrievedCapability link = linkPointer.get();\n                FileProperties linkProps = link.getProperties();\n                this.props = directProps.withLink(linkProps);\n            } else {\n                this.props = directProps;\n            }\n        } else\n            props = null;\n        if (pointer != null && ! version.contains(pointer.capability.writer))\n            throw new IllegalStateException(\"File version doesn't include its own writer!\");\n        if (isWritable() && !signingPair().publicKeyHash.equals(pointer.capability.writer))\n            throw new IllegalStateException(\"Invalid FileWrapper! public writing keys don't match!\");\n    }\n\n    public FileWrapper(RetrievedCapability pointer,\n                       Optional<RetrievedCapability> linkPointer,\n                       Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                       String ownername,\n                       Snapshot version) {\n        this(Optional.empty(), pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withTrieNode(TrieNode trie) {\n        return new FileWrapper(Optional.of(trie), pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withTrieNodeOpt(Optional<TrieNode> trie) {\n        return new FileWrapper(trie, pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withLinkPointer(Optional<RetrievedCapability> link) {\n        return new FileWrapper(capTrie, pointer, link, entryWriter, ownername, version);\n    }\n\n    public FileWrapper withVersion(Snapshot version) {\n        return new FileWrapper(capTrie, pointer, linkPointer, entryWriter, ownername, version);\n    }\n\n    public CommittedWriterData getVersionRoot() {\n        return version.get(writer());\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> getLatest(NetworkAccess network) {\n        if (isRoot())\n            return Futures.of(this);\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(latest -> getUpdated(latest, network));\n\n    }\n\n    @JsMethod\n    public boolean samePointer(FileWrapper other) {\n        return pointer.equals(other.getPointer());\n    }\n\n    public CompletableFuture<FileWrapper> getUpdated(NetworkAccess network) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(v -> getUpdated(v, network));\n    }\n\n    public CompletableFuture<FileWrapper> getUpdated(Snapshot version, NetworkAccess network) {\n        return version.withWriter(owner(), writer(), network).thenCompose(v -> {\n            if (this.version.get(writer()).equals(v.get(writer()))) {\n                return CompletableFuture.completedFuture(this);\n            }\n            return network.getFile(v, pointer.capability, entryWriter, ownername)\n                    .thenApply(Optional::get)\n                    .thenApply(f -> f.withTrieNodeOpt(capTrie));\n        });\n    }\n\n    public PublicKeyHash owner() {\n        return pointer.capability.owner;\n    }\n\n    public PublicKeyHash writer() {\n        return pointer.capability.writer;\n    }\n\n    public RetrievedCapability getPointer() {\n        return pointer;\n    }\n\n    public RetrievedCapability getLinkPointer() {\n        return linkPointer.get();\n    }\n\n    public boolean isRoot() {\n        return pointer == null;\n    }\n\n    public CompletableFuture<String> getPath(NetworkAccess network) {\n        return retrieveParent(network).thenCompose(parent -> {\n            if (!parent.isPresent() || parent.get().isRoot())\n                return CompletableFuture.completedFuture(\"/\" + props.name);\n            return parent.get().getPath(network).thenApply(parentPath -> parentPath + \"/\" + props.name);\n        });\n    }\n\n    @JsMethod\n    public CompletableFuture<Multihash> getContentHash(NetworkAccess network, Crypto crypto) {\n        return getInputStream(network, crypto, x -> {})\n                .thenCompose(reader -> crypto.hasher.hashFromStream(reader, getSize()));\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getDescendentByPath(String path,\n                                                                        Hasher hasher,\n                                                                        NetworkAccess network) {\n        return getDescendentByPath(path, version, hasher, network);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getDescendentByPath(String path,\n                                                                        Snapshot version,\n                                                                        Hasher hasher,\n                                                                        NetworkAccess network) {\n        ensureUnmodified();\n        if (path.length() == 0)\n            return CompletableFuture.completedFuture(Optional.of(this));\n\n        if (path.equals(\"/\"))\n            if (isDirectory())\n                return CompletableFuture.completedFuture(Optional.of(this));\n            else\n                return CompletableFuture.completedFuture(Optional.empty());\n\n        Path canon = PathUtil.get(path);\n        return getChild(version, canon.getName(0).toString(), network).thenCompose(child -> {\n            if (child.isPresent()) {\n                int names = canon.getNameCount();\n                if (names == 1)\n                    return Futures.of(child);\n                return child.get().getDescendentByPath(canon.subpath(1, names).toString(), child.get().version, hasher, network);\n            }\n            return CompletableFuture.completedFuture(Optional.empty());\n        });\n    }\n\n    private void ensureUnmodified() {\n        if (modified.get())\n            throw new IllegalStateException(\"This file has already been modified, use the returned instance\");\n    }\n\n    private void setModified() {\n        if (modified.get())\n            throw new IllegalStateException(\"This file has already been modified, use the returned instance\");\n        modified.set(true);\n    }\n\n    public CompletableFuture<Snapshot> updateChildLinks(\n            Snapshot version,\n            Committer committer,\n            Collection<Pair<AbsoluteCapability, NamedAbsoluteCapability>> childCases,\n            NetworkAccess network,\n            SafeRandom random,\n            Hasher hasher) {\n        return pointer.fileAccess\n                .updateChildLinks(version, committer, (WritableAbsoluteCapability) pointer.capability, signingPair(),\n                        childCases, network, random, hasher);\n    }\n\n    public CompletableFuture<Boolean> hasChildWithName(Snapshot version, String name, Hasher hasher, NetworkAccess network) {\n        ensureUnmodified();\n        return getChild(version, name, network)\n                .thenApply(Optional::isPresent);\n    }\n\n    /**\n     *\n     * @param child\n     * @param network\n     * @return Updated version of this directory without the child\n     */\n    public CompletableFuture<FileWrapper> removeChild(FileWrapper child, NetworkAccess network, SafeRandom random, Hasher hasher) {\n        setModified();\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (cwd, committer) -> pointer.fileAccess\n                .removeChildren(cwd, committer, Arrays.asList(child.isLink() ? child.linkPointer.get().capability : child.getPointer().capability), writableFilePointer(), entryWriter, network, random, hasher))\n                .thenCompose(newRoot -> getUpdated(newRoot, network));\n    }\n\n    public CompletableFuture<Snapshot> removeChild(Snapshot version,\n                                                   Committer committer,\n                                                   FileWrapper child,\n                                                   NetworkAccess network,\n                                                   SafeRandom random,\n                                                   Hasher hasher) {\n        return pointer.fileAccess.removeChildren(version, committer,\n                Arrays.asList(child.isLink() ? child.linkPointer.get().capability : child.getPointer().capability), writableFilePointer(), entryWriter, network, random, hasher);\n    }\n\n    @JsMethod\n    public String toLink() {\n        return pointer.capability.readOnly().toLink();\n    }\n\n    @JsMethod\n    public String toWritableLink() {\n        if (! isWritable())\n            throw new IllegalStateException(\"You do not have write access to \" + getName());\n        return pointer.capability.toLink();\n    }\n\n    @JsMethod\n    public boolean isWritable() {\n        return isWritable;\n    }\n\n    @JsMethod\n    public boolean isReadable() {\n        return pointer.fileAccess.isReadable(pointer.capability.rBaseKey);\n    }\n\n    public SymmetricKey getKey() {\n        return pointer.capability.rBaseKey;\n    }\n\n    public Location getLocation() {\n        return new Location(pointer.capability.owner, pointer.capability.writer, pointer.capability.getMapKey());\n    }\n\n    @JsMethod\n    public CompletableFuture<Integer> getDirectChildrenCount(NetworkAccess network) {\n        ensureUnmodified();\n        if (!this.isDirectory())\n            return CompletableFuture.completedFuture(0);\n        return pointer.fileAccess.getDirectChildrenCapabilities(pointer.capability, version, network)\n                .thenApply(Set::size);\n    }\n\n    @JsMethod\n    public CompletableFuture<Set<NamedAbsoluteCapability>> getChildrenCapabilities(Hasher hasher, NetworkAccess network) {\n        ensureUnmodified();\n        if (!this.isDirectory())\n            return CompletableFuture.completedFuture(Collections.emptySet());\n        return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, hasher, network);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> retrieveParent(NetworkAccess network) {\n        ensureUnmodified();\n        return retrieveParent(linkPointer.orElse(pointer), ownername, version, network);\n    }\n\n    public CompletableFuture<Optional<RetrievedCapability>> getAnyLinkPointer(NetworkAccess network) {\n        if (pointer == null)\n            return CompletableFuture.completedFuture(Optional.empty());\n        AbsoluteCapability cap = pointer.capability;\n        CompletableFuture<RetrievedCapability> parent = pointer.fileAccess.getParent(cap.owner, cap.writer, cap.rBaseKey, network, version);\n        return parent.thenApply(parentRFP -> {\n            if (parentRFP == null)\n                return Optional.empty();\n            FileProperties parentProps = parentRFP.getProperties();\n            if (! parentProps.isLink)\n                return Optional.empty();\n            return Optional.of(parentRFP);\n        });\n    }\n\n    public static CompletableFuture<Optional<FileWrapper>> retrieveParent(RetrievedCapability pointer,\n                                                                          String ownerName,\n                                                                          Snapshot version,\n                                                                          NetworkAccess network) {\n        if (pointer == null)\n            return CompletableFuture.completedFuture(Optional.empty());\n        AbsoluteCapability cap = pointer.capability;\n        CompletableFuture<RetrievedCapability> parent = pointer.fileAccess.getParent(cap.owner, cap.writer, cap.rBaseKey, network, version);\n        return parent.thenCompose(parentRFP -> {\n            if (parentRFP == null)\n                return Futures.of(Optional.empty());\n            FileProperties parentProps = parentRFP.getProperties();\n            if (! parentProps.isLink)\n                return version.withWriter(parentRFP.capability.owner, parentRFP.capability.writer, network)\n                        .thenApply(fullVersion -> Optional.of(new FileWrapper(parentRFP, Optional.empty(),\n                                Optional.empty(), ownerName, fullVersion)));\n            return retrieveParent(parentRFP, ownerName, version, network);\n        });\n    }\n\n    @JsMethod\n    public boolean isUserRoot() {\n        if (pointer == null)\n            return false;\n        return ! pointer.fileAccess.hasParentLink(pointer.capability.rBaseKey);\n    }\n\n    public SymmetricKey getParentKey() {\n        return pointer.getParentKey();\n    }\n\n    private Optional<SigningPrivateKeyAndPublicHash> getChildsEntryWriter() {\n        return pointer.capability.wBaseKey\n                .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n    }\n\n    @JsMethod\n    public CompletableFuture<Set<FileWrapper>> getChildren(Hasher hasher, NetworkAccess network) {\n        if (capTrie.isPresent())\n            return capTrie.get().getChildren(\"\", hasher, network);\n        return getChildren(version, hasher, network, true);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> getChildrenFromCaps(Set<NamedAbsoluteCapability> caps,\n                                                          Consumer<Set<FileWrapper>> results,\n                                                          Hasher hasher,\n                                                          NetworkAccess network) {\n        if (capTrie.isPresent()) {\n            return capTrie.get().getChildren(\"\", hasher, network)\n                    .thenAccept(results)\n                    .thenApply(x -> true);\n        }\n        Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey\n                .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n        List<Set<NamedAbsoluteCapability>> batched = new ArrayList<>();\n        Set<NamedAbsoluteCapability> currentBatch = new HashSet<>();\n        batched.add(currentBatch);\n        for (NamedAbsoluteCapability cap : caps) {\n            currentBatch.add(cap);\n            if (currentBatch.size() == ContentAddressedStorage.MAX_CHAMP_GETS) {\n                currentBatch = new HashSet<>();\n                batched.add(currentBatch);\n            }\n        }\n        return Futures.combineAllInOrder(batched.stream()\n                        .filter(b -> !b.isEmpty())\n                        .map(b -> getFiles(owner(), b, childsEntryWriter, ownername, network, version)\n                                .thenAccept(p -> results.accept(p.left)))\n                        .collect(Collectors.toList()))\n                .thenApply(x -> true);\n    }\n\n    public CompletableFuture<Set<FileWrapper>> getChildren(Snapshot version, Hasher hasher, NetworkAccess network, boolean allowDanglingLinks) {\n        if (capTrie.isPresent())\n            return capTrie.get().getChildren(\"\", hasher, version.merge(this.version), network);\n        if (isReadable()) {\n            Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey\n                    .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n            return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, hasher, network)\n                    .thenCompose(childCaps -> getFiles(owner(), childCaps, childsEntryWriter, ownername, network, version)\n                            .thenCompose(p -> {\n                                if (! p.right.isEmpty()) {\n                                    List<NamedAbsoluteCapability> dangling = p.right.stream()\n                                            .map(c -> childCaps.stream().filter(nc -> nc.cap.equals(c)).findFirst().get())\n                                            .collect(Collectors.toList());\n                                    // try once more\n                                    return getFiles(owner(), new HashSet<>(dangling), childsEntryWriter, ownername, network, version)\n                                            .thenApply(retry -> {\n                                                if (! retry.right.isEmpty()) {\n                                                    List<NamedAbsoluteCapability> retryDangling = retry.right.stream()\n                                                            .map(c -> childCaps.stream().filter(nc -> nc.cap.equals(c)).findFirst().get())\n                                                            .collect(Collectors.toList());\n                                                    List<String> names = retryDangling.stream().map(nc -> nc.name.name).collect(Collectors.toList());\n                                                    if (! allowDanglingLinks) {\n                                                        throw new IllegalStateException(\"Couldn't retrieve children \" + names + \" in dir \" + getName());\n                                                    }\n                                                    LOG.info(\"Couldn't retrieve children \" + names + \" in dir \" + getName());\n                                                }\n                                                HashSet<FileWrapper> res = new HashSet<>(p.left);\n                                                res.addAll(retry.left);\n                                                return res;\n                                            });\n                                }\n                                return Futures.of(p.left);\n                            }));\n        }\n        throw new IllegalStateException(\"Unreadable FileWrapper!\");\n    }\n\n    public CompletableFuture<Set<FileWrapper>> getChildren(Set<String> names, Hasher hasher, NetworkAccess network, boolean allowDanglingLinks) {\n        if (capTrie.isPresent())\n            return capTrie.get().getChildren(\"\", hasher, network)\n                    .thenApply(kids -> kids.stream()\n                            .filter(f -> names.contains(f.getName()))\n                            .collect(Collectors.toSet()));\n\n        if (isReadable()) {\n            Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey\n                    .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n            return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, hasher, network)\n                    .thenCompose(childCaps -> getFiles(owner(), childCaps.stream()\n                            .filter(c -> names.contains(c.name.name))\n                            .collect(Collectors.toSet()), childsEntryWriter, ownername, network, version)\n                            .thenCompose(p -> {\n                                if (! p.right.isEmpty()) {\n                                    List<NamedAbsoluteCapability> dangling = p.right.stream()\n                                            .map(c -> childCaps.stream().filter(nc -> nc.cap.equals(c)).findFirst().get())\n                                            .collect(Collectors.toList());\n                                    // try once more\n                                    return getFiles(owner(), new HashSet<>(dangling), childsEntryWriter, ownername, network, version)\n                                            .thenApply(retry -> {\n                                                if (! retry.right.isEmpty()) {\n                                                    List<NamedAbsoluteCapability> retryDangling = retry.right.stream()\n                                                            .map(c -> childCaps.stream().filter(nc -> nc.cap.equals(c)).findFirst().get())\n                                                            .collect(Collectors.toList());\n                                                    List<String> failednames = retryDangling.stream().map(nc -> nc.name.name).collect(Collectors.toList());\n                                                    if (! allowDanglingLinks) {\n                                                        throw new IllegalStateException(\"Couldn't retrieve children \" + failednames + \" in dir \" + getName());\n                                                    }\n                                                    LOG.info(\"Couldn't retrieve children \" + failednames + \" in dir \" + getName());\n                                                }\n                                                p.left.addAll(retry.left);\n                                                return p.left;\n                                            });\n                                }\n                                return Futures.of(p.left);\n                            }));\n        }\n        throw new IllegalStateException(\"Unreadable FileWrapper!\");\n    }\n\n    /**\n     *\n     * @param owner\n     * @param caps\n     * @param entryWriter\n     * @param ownername\n     * @param network\n     * @param version\n     * @return the children, and the list of caps pointing to deleted files\n     */\n    private static CompletableFuture<Pair<Set<FileWrapper>, List<AbsoluteCapability>>> getFiles(PublicKeyHash owner,\n                                                                                                Set<NamedAbsoluteCapability> caps,\n                                                                                                Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                                                                String ownername,\n                                                                                                NetworkAccess network,\n                                                                                                Snapshot version) {\n        if (caps.isEmpty())\n            return Futures.of(new Pair<>(Collections.emptySet(), Collections.emptyList()));\n        Set<PublicKeyHash> childWriters = caps.stream()\n                .map(c -> c.cap.writer)\n                .collect(Collectors.toSet());\n        return version.withWriters(owner, childWriters, network)\n                .thenCompose(fullVersion -> network.retrieveAllMetadata(caps.stream().map(n -> n.cap).collect(Collectors.toList()), fullVersion)\n                        .thenCompose(p -> Futures.combineAll(p.left.stream()\n                                .map(rc -> {\n                                    FileProperties props = rc.getProperties();\n                                    if (! props.isLink)\n                                        return Futures.of(new FileWrapper(rc, Optional.empty(), entryWriter, ownername, fullVersion));\n                                    return NetworkAccess.getFileFromLink(owner, rc, entryWriter, ownername, network, version);\n                                })\n                                .collect(Collectors.toSet())).thenApply(set -> new Pair<>(set, p.right))));\n    }\n\n    @JsMethod\n    public CompletableFuture<Optional<FileWrapper>> getChild(String name, Hasher hasher, NetworkAccess network) {\n        return getChild(version, name, network);\n    }\n\n    public CompletableFuture<Optional<FileWrapper>> getChild(Snapshot version,\n                                                             String name,\n                                                             NetworkAccess network) {\n        if (capTrie.isPresent())\n            return capTrie.get().getByPath(\"/\" + name, version.mergeAndOverwriteWith(this.version), network.hasher, network);\n        return pointer.fileAccess.getChild(name, pointer.capability, version, network.hasher, network)\n                .thenCompose(rcOpt -> {\n                    if (rcOpt.isEmpty())\n                        return Futures.of(Optional.empty());\n                    RetrievedCapability rc = rcOpt.get();\n                    FileProperties props = rc.getProperties();\n                    Optional<SigningPrivateKeyAndPublicHash> childsEntryWriter = pointer.capability.wBaseKey\n                            .map(wBase -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, wBase, entryWriter));\n                    if (! props.isLink)\n                        return version.withWriter(owner(), rc.capability.writer, network)\n                                .thenApply(fullVersion -> Optional.of(new FileWrapper(rc, Optional.empty(),\n                                        childsEntryWriter, ownername, fullVersion)));\n                    return version.withWriter(owner(), rc.capability.writer, network)\n                            .thenCompose(fullVersion ->\n                                    NetworkAccess.getFileFromLink(owner(), rc, childsEntryWriter, ownername, network, fullVersion)\n                                            .thenApply(Optional::of));\n                });\n    }\n\n    @JsMethod\n    public String getOwnerName() {\n        return ownername;\n    }\n\n    @JsMethod\n    public boolean isDirectory() {\n        boolean isNull = pointer == null;\n        return isNull || pointer.fileAccess.isDirectory();\n    }\n\n    public boolean isLink() {\n        return linkPointer.isPresent();\n    }\n\n    public boolean isDirty() {\n        ensureUnmodified();\n        return pointer.fileAccess.isDirty(pointer.capability.rBaseKey);\n    }\n\n    /**\n     *\n     * @param current\n     * @param committer\n     * @param network\n     * @param crypto\n     * @return updated cleaned version\n     */\n    public CompletableFuture<Pair<FileWrapper, Snapshot>> clean(Snapshot current,\n                                                                Committer committer,\n                                                                NetworkAccess network,\n                                                                Crypto crypto) {\n        if (!isDirty())\n            return CompletableFuture.completedFuture(new Pair<>(this, current));\n        if (isDirectory()) {\n            throw new IllegalStateException(\"Directories are never dirty (they are cleaned immediately)!\");\n        } else {\n            WritableAbsoluteCapability currentCap = writableFilePointer();\n            boolean isLink = isLink();\n            Optional<RelativeCapability> parentCap = pointer.fileAccess.getParentCapability(pointer.capability.rBaseKey);\n            Location parentOrLinkLocation = isLink ?\n                    linkPointer.get().capability.getLocation() :\n                    parentCap.get().getLocation(owner(), writer());\n            SymmetricKey parentOrLinkParentKey = isLink ?\n                    linkPointer.get().getParentKey() :\n                    parentCap.get().rBaseKey;\n            Optional<Bat> parentOrLinkBat = isLink ?\n                    linkPointer.get().capability.bat :\n                    parentCap.get().bat;\n            return pointer.fileAccess.cleanAndCommit(current, committer, currentCap, currentCap, props.streamSecret,\n                    Optional.empty(), signingPair(), SymmetricKey.random(),\n                    parentOrLinkLocation, parentOrLinkBat, parentOrLinkParentKey, network, crypto)\n                    .thenCompose(cwd -> {\n                        setModified();\n                        return getUpdated(cwd, network).thenApply(updated -> new Pair<>(updated, cwd));\n                    });\n        }\n    }\n\n    public CompletableFuture<Pair<byte[], Optional<Bat>>> getMapKey(long offset, NetworkAccess network, Crypto crypto) {\n        CryptreeNode fileAccess = pointer.fileAccess;\n        return fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, crypto.hasher)\n                .thenCompose(retriever -> retriever\n                        .getMapLabelAt(version.get(writer()), writableFilePointer(),\n                                getFileProperties().streamSecret, offset, crypto.hasher, network)\n                        .thenApply(Optional::get));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> truncate(long newSize, NetworkAccess network, Crypto crypto) {\n        if (getSize() <= newSize)\n            return Futures.of(this);\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(), (current, committer) ->\n                truncate(current, committer, newSize, network, crypto)\n        ).thenCompose(finished -> getUpdated(finished, network));\n    }\n\n    public CompletableFuture<Snapshot> truncate(Snapshot initialVersion, Committer committer, long newSize, NetworkAccess network, Crypto crypto) {\n        if (isDirectory())\n            return Futures.errored(new IllegalStateException(\"You cannot truncate a directory!\"));\n        FileProperties props = getFileProperties();\n        if (props.size <= newSize)\n            return CompletableFuture.completedFuture(initialVersion);\n\n        return initialVersion.withWriter(owner(), writer(), network)\n                .thenCompose(snapshot -> getMapKey(newSize, network, crypto).thenCompose(endMapKey ->\n                        getInputStream(snapshot.get(writer()), network, crypto, props.size, 1, x -> {}).thenCompose(originalReader -> {\n                            long startOfLastChunk = newSize - (newSize % Chunk.MAX_SIZE);\n                            return originalReader.seek(startOfLastChunk).thenCompose(seekedOriginal -> {\n                                byte[] lastChunk = new byte[(int)(newSize % Chunk.MAX_SIZE)];\n                                return seekedOriginal.readIntoArray(lastChunk, 0, lastChunk.length).thenCompose(read -> {\n                                    if (newSize <= Chunk.MAX_SIZE)\n                                        return CompletableFuture.completedFuture(snapshot);\n                                    int currentChunk = (int) (newSize / Chunk.MAX_SIZE);\n                                    return IpfsTransaction.call(owner(), tid ->\n                                                    deleteFileChunks(props.streamSecret.get(), props.chunkCount() - currentChunk, writableFilePointer().withMapKey(endMapKey.left, endMapKey.right),\n                                                            signingPair(), tid, crypto.hasher, network, snapshot, committer),\n                                            network.dhtClient);\n                                }).thenCompose(deleted -> pointer.fileAccess.updateProperties(deleted, committer, writableFilePointer(),\n                                        entryWriter, props.withSize(startOfLastChunk), network).thenCompose(resized ->\n                                        getUpdated(resized, network).thenCompose(f -> f.clean(resized, committer, network, crypto)\n                                                .thenCompose(p -> p.left.overwriteSection(p.right, committer,\n                                                AsyncReader.build(lastChunk), startOfLastChunk, newSize, Optional.empty(), network, crypto, x -> {})))));\n                            });\n                        }))\n                );\n    }\n\n    public static int getNumberOfChunks(long size) {\n        if (size == 0)\n            return 1;\n        return (int)((size + Chunk.MAX_SIZE - 1)/Chunk.MAX_SIZE);\n    }\n\n    public List<Location> generateChildLocationsFromSize(long fileSize, SafeRandom random) {\n        return generateChildLocations(getNumberOfChunks(fileSize), random);\n    }\n\n    public List<Location> generateChildLocations(int numberOfChunks,\n                                                        SafeRandom random) {\n        return IntStream.range(0, numberOfChunks + 1) //have to have one extra location\n                .mapToObj(e -> new Location(owner(), writer(), random.randomBytes(32)))\n                .collect(Collectors.toList());\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> appendFileJS(String filename,\n                                                       AsyncReader fileData,\n                                                       int lengthHi,\n                                                       int lengthLow,\n                                                       NetworkAccess network,\n                                                       Crypto crypto,\n                                                       ProgressConsumer<Long> monitor) {\n        long fileSize = LongUtil.intsToLong(lengthHi, lengthLow);\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> getChild(s, filename, network).thenCompose(childOpt -> {\n                if (childOpt.isEmpty()) {\n                    throw new IllegalStateException(\"File does not exists with name \" + filename);\n                } else {\n                    FileProperties props = childOpt.get().getFileProperties();\n                    if (props.isHidden) {\n                        throw new IllegalStateException(\"File is hidden \" + filename);\n                    }\n                    long startIndex = props.size;\n                    return uploadFileSection(s, committer, filename, fileData,\n                            false, startIndex, startIndex + fileSize, Optional.empty(), false, true, false,\n                            network, crypto, () -> false, monitor, crypto.random.randomBytes(32), Optional.empty(),\n                            Optional.of(Bat.random(crypto.random)), mirrorBatId());\n                }\n        })).thenCompose(finished -> getUpdated(finished, network));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> uploadFileJS(String filename,\n                                                       AsyncReader fileData,\n                                                       int lengthHi,\n                                                       int lengthLow,\n                                                       boolean overwriteExisting,\n                                                       Optional<BatId> mirrorBat,\n                                                       NetworkAccess network,\n                                                       Crypto crypto,\n                                                       ProgressConsumer<Long> monitor,\n                                                       TransactionService transactions,\n                                                       Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile) {\n        FileUploadProperties fileProps = new FileUploadProperties(filename, () -> fileData, lengthHi, lengthLow, Optional.empty(), Optional.empty(), false, overwriteExisting, monitor);\n        FolderUploadProperties currentFolder = new FolderUploadProperties(Collections.emptyList(), Collections.singletonList(fileProps));\n        return uploadSubtree(Stream.of(currentFolder), mirrorBat, network, crypto, transactions, resumeFile, f -> Futures.of(true), () -> true);\n    }\n\n    public CompletableFuture<Pair<Snapshot, Optional<NamedRelativeCapability>>> resumeUpload(FileUploadTransaction txn,\n                                                                                             AsyncReader data,\n                                                                                             Supplier<Boolean> isCancelled,\n                                                                                             ProgressConsumer<Long> monitor,\n                                                                                             Snapshot s,\n                                                                                             Committer c,\n                                                                                             NetworkAccess network,\n                                                                                             Crypto crypto) {\n        RelativeCapability fromParent = writableFilePointer().relativise(txn.writeCap());\n        FileProperties props = txn.props;\n        // first find how many chunks were already uploaded, then seek reader to that offset and continue\n        long totalChunks = (txn.size() + Chunk.MAX_SIZE - 1) / Chunk.MAX_SIZE;\n        return findFirstAbsentChunkIndex(txn.streamSecret(), txn.getFirstLocation(), txn.firstBat, totalChunks, s, network, crypto)\n                .thenCompose(startChunkIndex -> {\n                    monitor.accept(startChunkIndex * Chunk.MAX_SIZE);\n                    FileUploader uploader = new FileUploader(txn.targetFilename(), data, startChunkIndex*Chunk.MAX_SIZE,\n                            txn.size(), txn.baseKey, txn.dataKey, getLocation(), getPointer().capability.bat, getParentKey(),\n                            monitor, props, Optional.empty(), txn.getFirstLocation().getMapKey(), txn.firstBat, isCancelled);\n                    return uploader.uploadFrom(s, c, network, startChunkIndex.intValue(), txn.getFirstLocation().owner,\n                            signingPair(), mirrorBatId(), crypto.random, crypto.hasher);\n                }).thenApply(v -> new Pair<>(v, Optional.of(new NamedRelativeCapability(txn.targetFilename(), fromParent,\n                        Optional.of(props.isDirectory), Optional.of(props.mimeType), Optional.of(props.created)))));\n    }\n\n    // 8-ary search: PROBE_COUNT probes per CHAMP call, narrowing the range by ~8x each round.\n    private static final int PROBE_COUNT = 8;\n\n    /**\n     * Finds the first chunk index in [0, totalChunks] that has not yet been uploaded, using an\n     * 8-ary search so each round issues PROBE_COUNT lookups in a single ChampWrapper call and\n     * narrows the range by ~8x.\n     *\n     * The first call probes at chunk 0, totalChunks/8, 2*totalChunks/8, ..., 7*totalChunks/8\n     * simultaneously — chunk 0 is included so no separate round-trip is needed even when\n     * the upload has not started yet.\n     *\n     * Key derivation is cumulative left-to-right so total hash work is O(totalChunks).\n     */\n    private CompletableFuture<Long> findFirstAbsentChunkIndex(byte[] streamSecret,\n                                                              Location firstLoc,\n                                                              Optional<Bat> firstBat,\n                                                              long totalChunks,\n                                                              Snapshot s,\n                                                              NetworkAccess network,\n                                                              Crypto crypto) {\n        if (totalChunks == 0)\n            return Futures.of(0L);\n        Function<List<Pair<byte[], Optional<Bat>>>, CompletableFuture<List<Boolean>>> lookup =\n                caps -> network.chunksArePresent(s, firstLoc.owner, firstLoc.writer, caps);\n        return binarySearchAbsentChunk(streamSecret, 0L, totalChunks,\n                firstLoc.getMapKey(), firstBat, lookup, crypto.hasher);\n    }\n\n    /**\n     * 8-ary search step.  Invariant: the answer is in [lo, hi], chunk[hi] is absent.\n     * chunk[lo] is included as the first probe (probe[0] = lo) so an absent first chunk\n     * is detected immediately without any prior invariant assumption.\n     *\n     * probeIndices[i] = lo + i * rangeSize / batchSize  (i = 0..batchSize-1)\n     * → probe[0] = lo, probe[1] ≈ lo + 1/8 range, …, probe[7] ≈ lo + 7/8 range.\n     */\n    public static CompletableFuture<Long> binarySearchAbsentChunk(byte[] streamSecret,\n                                                                  long lo,\n                                                                  long hi,\n                                                                  byte[] loKey,\n                                                                  Optional<Bat> loBat,\n                                                                  Function<List<Pair<byte[], Optional<Bat>>>, CompletableFuture<List<Boolean>>> lookup,\n                                                                  Hasher hasher) {\n        if (lo >= hi)\n            return Futures.of(lo);\n\n        long rangeSize = hi - lo;\n        int batchSize = (int) Math.min(rangeSize, PROBE_COUNT);\n\n        long[] probeIndices = new long[batchSize];\n        for (int i = 0; i < batchSize; i++)\n            probeIndices[i] = lo + (long) i * rangeSize / batchSize;\n        // probe[0] = lo; probe[batchSize-1] = lo+(batchSize-1)*rangeSize/batchSize < hi ✓\n\n        Pair<byte[], Optional<Bat>>[] probes = new Pair[batchSize];\n        probes[0] = new Pair<>(loKey, loBat);  // 0 steps from lo — no derivation needed\n        return deriveProbesForIndices(streamSecret, loKey, loBat, lo, probeIndices, 1, probes, hasher)\n                .thenCompose(ps -> lookup.apply(Arrays.asList(ps))\n                        .thenCompose(presentFlags -> {\n                            for (int i = 0; i < batchSize; i++) {\n                                if (!presentFlags.get(i)) {\n                                    if (i == 0) return Futures.of(lo);  // chunk lo itself is absent\n                                    // probe[i-1] present, probe[i] absent → answer in (probe[i-1], probe[i]]\n                                    return binarySearchAbsentChunk(streamSecret,\n                                            probeIndices[i - 1], probeIndices[i], ps[i - 1].left, ps[i - 1].right, lookup, hasher);\n                                }\n                            }\n                            // All probes present → advance lo to last probe; guard against rangeSize=1 loop\n                            long newLo = probeIndices[batchSize - 1];\n                            if (newLo + 1 >= hi) return Futures.of(hi);\n                            return binarySearchAbsentChunk(streamSecret,\n                                    newLo, hi, ps[batchSize - 1].left, ps[batchSize - 1].right, lookup, hasher);\n                        }));\n    }\n\n    /** Derives (mapKey, bat) pairs for each probe index cumulatively. */\n    private static CompletableFuture<Pair<byte[], Optional<Bat>>[]> deriveProbesForIndices(\n            byte[] streamSecret,\n            byte[] prevKey,\n            Optional<Bat> prevBat,\n            long prevIndex,\n            long[] probeIndices,\n            int pos,\n            Pair<byte[], Optional<Bat>>[] probes,\n            Hasher hasher) {\n        if (pos == probes.length)\n            return Futures.of(probes);\n        long steps = probeIndices[pos] - prevIndex;\n        return FileProperties.calculateMapKey(streamSecret, prevKey, prevBat, steps * Chunk.MAX_SIZE, hasher)\n                .thenCompose(kp -> {\n                    probes[pos] = kp;\n                    return deriveProbesForIndices(streamSecret, kp.left, kp.right, probeIndices[pos], probeIndices, pos + 1, probes, hasher);\n                });\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,\n                                                              AsyncReader fileData,\n                                                              int fileSizeHi,\n                                                              int fileSizeLow,\n                                                              NetworkAccess network,\n                                                              Crypto crypto,\n                                                              ProgressConsumer<Long> monitor) {\n        long fileSize = (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);\n        Supplier<Boolean> isCancelled = () -> false; // TODO support in UI\n        return uploadOrReplaceFile(filename, fileData, fileSize, network, crypto, isCancelled, monitor,\n                crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), mirrorBatId());\n    }\n\n    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              NetworkAccess network,\n                                                              Crypto crypto,\n                                                              Supplier<Boolean> isCancelled,\n                                                              ProgressConsumer<Long> monitor) {\n        return uploadOrReplaceFile(filename, fileData, length, network, crypto, isCancelled, monitor,\n                crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), mirrorBatId());\n    }\n\n    public CompletableFuture<FileWrapper> uploadFileWithHash(String filename,\n                                                             AsyncReader fileData,\n                                                             long length,\n                                                             Optional<HashTree> hash,\n                                                             Optional<LocalDateTime> modificationTime,\n                                                             Optional<Thumbnail> thumbnail,\n                                                             NetworkAccess network,\n                                                             Crypto crypto,\n                                                             ProgressConsumer<Long> monitor) {\n        return uploadFileWithHash(filename, fileData, length, hash, modificationTime, thumbnail, Optional.empty(),\n                network, crypto, () -> false, monitor);\n    }\n\n    public CompletableFuture<FileWrapper> uploadFileWithHash(String filename,\n                                                             AsyncReader fileData,\n                                                             long length,\n                                                             Optional<HashTree> hash,\n                                                             Optional<LocalDateTime> modificationTime,\n                                                             Optional<Thumbnail> thumbnail,\n                                                             Optional<ResumeUploadProps> props,\n                                                             NetworkAccess network,\n                                                             Crypto crypto,\n                                                             Supplier<Boolean> isCancelled,\n                                                             ProgressConsumer<Long> monitor) {\n        if (! isWritable())\n            throw new IllegalStateException(\"Folder not writable!\");\n\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(), (current, committer) ->\n                        (props.isPresent() ?\n                                calculateMimeType(fileData, length, filename)\n                                        .thenCompose(mimeType -> fileData.reset().thenCompose(resetData -> resumeUpload(new FileUploadTransaction(System.currentTimeMillis(),\n                                                filename, filename, new FileProperties(filename,\n                                                false, false, mimeType, length, modificationTime.orElseGet(() -> LocalDateTime.now(ZoneOffset.UTC)),\n                                                modificationTime.orElseGet(() -> LocalDateTime.now(ZoneOffset.UTC)), false, thumbnail, props.map(p -> p.streamSecret), hash.map(t -> t.branch(0))),\n                                                signingPair(), new Location(owner(), writer(), props.get().firstChunkMapKey),\n                                                props.map(p -> p.firstChunkBat), length, props.get().baseKey, props.get().dataKey,\n                                                props.get().writeKey, props.get().streamSecret), resetData, isCancelled, monitor, current, committer, network, crypto))) :\n                                uploadFileSection(current, committer, filename, fileData, thumbnail, false, 0, length, hash,\n                                        modificationTime, Optional.empty(), Optional.empty(), Optional.empty(),\n                                        false, false, false,\n                                        network, crypto, isCancelled, monitor, crypto.random.randomBytes(32),\n                                        Optional.of(crypto.random.randomBytes(32)), Optional.of(Bat.random(crypto.random)), mirrorBatId()))\n                                .thenCompose(p -> getUpdated(p.left, network)\n                                        .thenCompose(latest -> p.right.isEmpty() ?\n                                                Futures.of(p.left) :\n                                                latest.addChildPointer(p.left, committer, p.right.get(), network, crypto))))\n                .thenCompose(finalBase -> getUpdated(finalBase, network));\n    }\n\n    public CompletableFuture<FileWrapper> uploadOrReplaceFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              NetworkAccess network,\n                                                              Crypto crypto,\n                                                              Supplier<Boolean> isCancelled,\n                                                              ProgressConsumer<Long> monitor,\n                                                              byte[] firstChunkMapKey,\n                                                              Optional<Bat> firstChunkBat,\n                                                              Optional<BatId> mirrorBat) {\n        return uploadFileSection(filename, fileData, false, 0, length, Optional.empty(),\n                true, network, crypto, isCancelled, monitor, firstChunkMapKey, Optional.empty(), firstChunkBat, mirrorBat)\n                .thenCompose(f -> f.getChild(filename, crypto.hasher, network)\n                        .thenCompose(childOpt -> childOpt.get().truncate(length, network, crypto))\n                        .thenCompose(c -> f.getUpdated(f.version.mergeAndOverwriteWith(c.version), network)));\n    }\n\n    public CompletableFuture<Snapshot> uploadOrReplaceFile(String filename,\n                                                           AsyncReader fileData,\n                                                           long length,\n                                                           boolean isHidden,\n                                                           Snapshot s,\n                                                           Committer c,\n                                                           NetworkAccess network,\n                                                           Crypto crypto,\n                                                           Supplier<Boolean> isCancelled,\n                                                           ProgressConsumer<Long> monitor,\n                                                           byte[] firstChunkMapKey,\n                                                           Optional<Bat> firstChunkBat,\n                                                           Optional<BatId> mirrorBat) {\n        return uploadFileSection(s, c, filename, fileData, isHidden, 0, length,\n                Optional.empty(), false, true, true, network, crypto,\n                isCancelled, monitor, firstChunkMapKey, Optional.empty(), firstChunkBat, mirrorBat);\n    }\n\n    public CompletableFuture<FileWrapper> uploadAndReturnFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              boolean isHidden,\n                                                              Optional<BatId> mirrorBat,\n                                                              NetworkAccess network,\n                                                              Crypto crypto) {\n        return uploadAndReturnFile(filename, fileData, length, isHidden, () -> false, x -> {}, mirrorBat, network, crypto);\n    }\n\n    public CompletableFuture<FileWrapper> uploadAndReturnFile(String filename,\n                                                              AsyncReader fileData,\n                                                              long length,\n                                                              boolean isHidden,\n                                                              Supplier<Boolean> isCancelled,\n                                                              ProgressConsumer<Long> progressMonitor,\n                                                              Optional<BatId> mirrorBat,\n                                                              NetworkAccess network,\n                                                              Crypto crypto) {\n        return uploadFileSection(filename, fileData, isHidden, 0, length, Optional.empty(),\n                true, network, crypto, isCancelled, progressMonitor, crypto.random.randomBytes(32), Optional.empty(),\n                Optional.of(Bat.random(crypto.random)), mirrorBat)\n                .thenCompose(f -> f.getChild(filename, crypto.hasher, network)\n                        .thenCompose(childOpt -> childOpt.get().truncate(length, network, crypto)));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> overwriteFileJS(AsyncReader fileData,\n                                                          int endHigh,\n                                                          int endLow,\n                                                          NetworkAccess network,\n                                                          Crypto crypto,\n                                                          ProgressConsumer<Long> monitor) {\n        long newSize = LongUtil.intsToLong(endHigh, endLow);\n        return overwriteFile(fileData, newSize, network, crypto, monitor);\n    }\n\n    public CompletableFuture<Snapshot> overwriteFile(AsyncReader fileData,\n                                                     long newSize,\n                                                     NetworkAccess network,\n                                                     Crypto crypto,\n                                                     ProgressConsumer<Long> monitor,\n                                                     Snapshot s,\n                                                     Committer committer) {\n        long size = getSize();\n        return clean(s, committer, network, crypto)\n                .thenCompose(u -> u.left.overwriteSection(u.right, committer, fileData,\n                        0L, newSize, Optional.empty(), network, crypto, monitor))\n                .thenCompose(v -> newSize >= size ?\n                        Futures.of(v) :\n                        getUpdated(v, network)\n                                .thenCompose(f -> f.truncate(v, committer, newSize, network, crypto)));\n    }\n\n    public CompletableFuture<FileWrapper> overwriteFile(AsyncReader fileData,\n                                                        long newSize,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        ProgressConsumer<Long> monitor) {\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> overwriteFile(fileData, newSize, network, crypto, monitor, version, committer))\n                .thenCompose(v -> getUpdated(v, network));\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> overwriteSectionJS(AsyncReader fileData,\n                                                             int startHigh,\n                                                             int startLow,\n                                                             int endHigh,\n                                                             int endLow,\n                                                             Optional<LocalDateTime> modified,\n                                                             NetworkAccess network,\n                                                             Crypto crypto,\n                                                             ProgressConsumer<Long> monitor) {\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> overwriteSection(s, committer, fileData,\n                        LongUtil.intsToLong(startHigh, startLow),\n                        LongUtil.intsToLong(endHigh, endLow), modified, network, crypto, monitor))\n                .thenCompose(v -> getUpdated(v, network));\n    }\n\n    public static final class FileUploadProperties {\n        public final String filename;\n        public final Supplier<AsyncReader> fileData;\n        public final long length;\n        public final Optional<LocalDateTime> modifiedTime;\n        public final Optional<HashTree> hash;\n        public final boolean skipExisting;\n        public final boolean overwriteExisting;\n        public final ProgressConsumer<Long> monitor;\n\n        @JsConstructor\n        public FileUploadProperties(String filename,\n                                    Supplier<AsyncReader> fileData,\n                                    int lengthHi,\n                                    int lengthLow,\n                                    Optional<LocalDateTime> modifiedTime,\n                                    Optional<HashTree> hash,\n                                    boolean skipExisting,\n                                    boolean overwriteExisting,\n                                    ProgressConsumer<Long> monitor) {\n            this.filename = filename;\n            this.fileData = fileData;\n            this.length = (((long)lengthHi) << 32) | (lengthLow & 0xFFFFFFFFL);\n            this.modifiedTime = modifiedTime;\n            this.hash = hash;\n            this.skipExisting = skipExisting;\n            this.overwriteExisting = overwriteExisting;\n            this.monitor = monitor;\n        }\n\n        public FileUploadProperties withOverwriteExisting() {\n            return new FileUploadProperties(filename, fileData,\n                    (int)(length >> 32), (int) length,\n                    modifiedTime, hash, skipExisting, true, monitor);\n        }\n\n        @Override\n        public String toString() {\n            return filename + \" [\" + length + \"]\";\n        }\n    }\n\n    public static class FolderUploadProperties {\n        public final List<String> relativePath;\n        public final List<FileUploadProperties> files;\n\n        @JsConstructor\n        public FolderUploadProperties(List<String> relativePath, List<FileUploadProperties> files) {\n            this.relativePath = relativePath;\n            this.files = files;\n        }\n\n        public Path path() {\n            return PathUtil.get(relativePath.stream().collect(Collectors.joining(\"/\")));\n        }\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> uploadSubtree(Stream<FolderUploadProperties> directories,\n                                                        Optional<BatId> mirrorBat,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        TransactionService txns,\n                                                        Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile,\n                                                        Function<String, CompletableFuture<Boolean>> replaceFile,\n                                                        Supplier<Boolean> commitWatcher) {\n        Supplier<Boolean> isCancelled = () -> false; // TODO support this in UI\n        // only use the supplied mirror BAT if the parent doesn't have a mirror BAT\n        Optional<BatId> mirror = mirrorBatId().or(() -> mirrorBat);\n        return getPath(network).thenCompose(path ->\n                network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                        (s, c) -> {\n                            return getUpdated(s, network).thenCompose(us -> Futures.reduceAll(directories, us,\n                                            (dir, children) -> dir.getOrMkdirs(children.relativePath, false, mirror, network, crypto, dir.version, c)\n                                                    .thenCompose(p -> uploadFolder(PathUtil.get(path).resolve(children.path()), p.right,\n                                                            children, mirrorBat, txns, resumeFile, replaceFile, commitWatcher, isCancelled, network, crypto, c)\n                                                            .thenCompose(v -> dir.getUpdated(v, network))),\n                                            (a, b) -> b))\n                                    .thenApply(d -> d.version);\n                        },\n                        commitWatcher\n                )).thenCompose(finished -> getUpdated(finished, network));\n    }\n\n    public static CompletableFuture<Snapshot> uploadFolder(Path toParent,\n                                                           FileWrapper parent,\n                                                           FolderUploadProperties children,\n                                                           Optional<BatId> mirrorBat,\n                                                           TransactionService transactions,\n                                                           Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile,\n                                                           Function<String, CompletableFuture<Boolean>> replaceFile,\n                                                           Supplier<Boolean> commitWatcher,\n                                                           Supplier<Boolean> isCancelled,\n                                                           NetworkAccess network,\n                                                           Crypto crypto,\n                                                           Committer c) {\n        Pair<Snapshot, List<NamedRelativeCapability>> identity = new Pair<>(parent.version, Collections.emptyList());\n\n        // Upload in order of ascending file size\n        List<FileUploadProperties> sortedChildren = children.files.stream()\n                .sorted(Comparator.comparingLong(a -> a.length))\n                .collect(Collectors.toList());\n        // split into batches to see partial progress with many ~MiB files\n        List<List<FileUploadProperties>> groupedChildren = new ArrayList<>();\n        int currentTotal = 0;\n        int batchSize = 10 * 1024 * 1024;\n        groupedChildren.add(new ArrayList<>());\n        for (int i=0; i < sortedChildren.size(); i++) {\n            FileUploadProperties next = sortedChildren.get(i);\n            if (currentTotal + next.length > batchSize) {\n                groupedChildren.add(new ArrayList<>());\n                currentTotal = 0;\n            }\n            groupedChildren.get(groupedChildren.size() - 1).add(next);\n            currentTotal += next.length;\n        }\n\n        // Pre-load the existing children of this directory in one parallel batch so we\n        // can compare hashes without a per-file CHAMP lookup inside the reduce loop.\n        // We must NOT use parent.getChildren(Set<String>) here because uploadFolder\n        // runs inside applyComplexUpdate which already holds the WriteSynchronizer\n        // lock; that path calls getAllChildrenCapabilities() → withWriter() →\n        // getValue() which tries to re-acquire the same lock, causing an async\n        // deadlock.  CryptreeNode.getChildren(Snapshot,...) traverses all CHAMP\n        // chunks using the provided Snapshot directly and never calls withWriter(),\n        // so it is safe to call while the lock is held.\n        Set<String> filenames = sortedChildren.stream().map(f -> f.filename).collect(Collectors.toSet());\n        RetrievedCapability parentRc = parent.getPointer();\n        return parentRc.fileAccess.getChildren(parent.version, crypto.hasher, network, parentRc.capability)\n                .thenApply(existing -> existing.stream()\n                        .filter(rc -> filenames.contains(rc.getProperties().name))\n                        .collect(Collectors.toMap(rc -> rc.getProperties().name, RetrievedCapability::getProperties)))\n                .thenCompose(existingByName -> Futures.reduceAll(groupedChildren, identity, (id, group) -> Futures.reduceAll(group, id,\n                        (p, f) -> {\n                            // Fast path: compare hash against the pre-loaded remote state before\n                            // doing any per-file network work.\n                            FileProperties existingProps = existingByName.get(f.filename);\n                            if (existingProps != null) {\n                                Optional<HashBranch> remoteHash = existingProps.treeHash;\n                                if (f.hash.isPresent() && remoteHash.isPresent()\n                                        && f.hash.get().rootHash.equals(remoteHash.get().rootHash)) {\n                                    // Identical content already uploaded — skip silently.\n                                    f.monitor.accept(f.length + THUMBNAIL_PROGRESS_OFFSET);\n                                    return Futures.of(new Pair<>(p.left, p.right));\n                                }\n                                // Remote file exists but content differs — ask the caller.\n                                return replaceFile.apply(toParent.resolve(f.filename).toString())\n                                        .thenCompose(replace -> replace ?\n                                                uploadFilePart(toParent, parent, p, f.withOverwriteExisting(),\n                                                        mirrorBat, transactions, resumeFile, commitWatcher, isCancelled, network, crypto, c) :\n                                                Futures.of(new Pair<>(p.left, p.right)));\n                            }\n                            return uploadFilePart(toParent, parent, p, f,\n                                    mirrorBat, transactions, resumeFile, commitWatcher, isCancelled, network, crypto, c);\n                        },\n                        (a, b) -> new Pair<>(b.left, Stream.concat(a.right.stream(), b.right.stream()).collect(Collectors.toList())))\n                .thenCompose(r -> atomicallyClearTransactionsAndAddToParent(Collections.emptyList(), r.right, parent, transactions, r.left, c, commitWatcher, network, crypto)),\n                        (a, b) -> new Pair<>(b.left, Stream.concat(a.right.stream(), b.right.stream()).collect(Collectors.toList())))\n                .thenApply(x -> x.left));\n    }\n\n    private static CompletableFuture<Pair<Snapshot, List<NamedRelativeCapability>>> uploadFilePart(\n            Path toParent,\n            FileWrapper parent,\n            Pair<Snapshot, List<NamedRelativeCapability>> p,\n            FileUploadProperties f,\n            Optional<BatId> mirrorBat,\n            TransactionService transactions,\n            Function<FileUploadTransaction, CompletableFuture<Boolean>> resumeFile,\n            Supplier<Boolean> commitWatcher,\n            Supplier<Boolean> isCancelled,\n            NetworkAccess network,\n            Crypto crypto,\n            Committer c) {\n        AsyncReader fileData = f.fileData.get();\n        if (f.length <= Chunk.MAX_SIZE || transactions == null) // small files or writable public links\n            return parent.uploadFileSection(p.left, c, f.filename, fileData, Optional.empty(), false, 0, f.length, f.hash, f.modifiedTime,\n                            Optional.empty(), Optional.empty(), Optional.empty(), f.skipExisting,\n                            f.overwriteExisting, true, network.disableCommits(), crypto, isCancelled, f.monitor,\n                            crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)), mirrorBat)\n                    .thenApply(pair -> new Pair<>(pair.left, Stream.concat(p.right.stream(), pair.right.stream()).collect(Collectors.toList())))\n                    .thenCompose(r -> {\n                        fileData.close();\n                        if (! network.isFull())\n                            return Futures.of(r);\n                        return atomicallyClearTransactionsAndAddToParent(Collections.emptyList(), r.right, parent, transactions, r.left, c, commitWatcher, network, crypto);\n                    });\n\n        // Before enabling commits, flush any directory entries accumulated from\n        // preceding small files into the buffer while commits are still disabled.\n        // This ensures a subsequent auto-commit cannot permanently commit a small\n        // file's chunk without its parent directory entry.\n        CompletableFuture<Snapshot> preFlush = p.right.isEmpty() ?\n                Futures.of(p.left) :\n                parent.getUpdated(p.left, network)\n                      .thenCompose(latest -> latest.addChildPointers(p.left, c, p.right, network.disableCommits(), crypto));\n        return preFlush.thenCompose(flushedVersion -> {\n            network.enableCommits();\n            List<FileUploadTransaction> toClose = new ArrayList<>();\n            LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);\n            return calculateMimeType(fileData, f.length, f.filename).thenCompose(mimeType -> {\n                FileProperties props = new FileProperties(f.filename,\n                        false, false, mimeType, f.length,\n                        now, now, false, Optional.empty(), Optional.of(crypto.random.randomBytes(32)), f.hash.map(t -> t.branch(0)));\n                return Transaction.buildFileUploadTransaction(toParent.resolve(f.filename).toString(), f.length,\n                        props, props.streamSecret.get(), SymmetricKey.random(), SymmetricKey.random(),\n                        SymmetricKey.random(), parent.signingPair(), new Location(parent.owner(), parent.writer(),\n                                crypto.random.randomBytes(32)), Optional.of(Bat.random(crypto.random)), crypto.hasher);\n            }).thenCompose(txn -> transactions.open(flushedVersion, c, txn)\n                            .thenCompose(r -> {\n                                if (r.isB()) // we must clear legacy transactions which can't be resumed or ones whose parent has rotated writer\n                                    return (r.b().isLegacy() || ! parent.writer().equals(r.b().writer()) ?\n                                            Futures.of(false) :\n                                            (r.b().props.treeHash.isPresent() &&\n                                                    f.hash.isPresent() &&\n                                                    r.b().props.treeHash.get().rootHash.equals(f.hash.get().rootHash)) ?\n                                                    Futures.of(true) : resumeFile.apply(r.b()))\n                                            .thenCompose(resume -> {\n                                                if (resume) {\n                                                    toClose.add(r.b());\n                                                    return parent.resumeUpload(r.b(), fileData, isCancelled, f.monitor, flushedVersion, c, network, crypto)\n                                                            .thenCompose(res -> fileData.reset().thenCompose(resetAgain ->\n                                                                            parent.generateThumbnailAndUpdate(res.left, c, r.b().writeCap(), f.filename, resetAgain,\n                                                                                    network, false, r.b().props.mimeType,\n                                                                                    f.length, r.b().startTime(), r.b().startTime(), Optional.of(r.b().streamSecret()), f.monitor))\n                                                                    .thenApply(s -> new Pair<>(s, res.right)));\n                                                }\n                                                return transactions.close(flushedVersion, c, r.b())\n                                                        .thenCompose(s2 -> transactions.open(s2, c, txn))\n                                                        .thenCompose(r2 -> {\n                                                            if (r2.isB())\n                                                                throw new IllegalStateException(\"Error uploading file - concurrent upload of same file?\");\n                                                            toClose.add(txn);\n                                                            return fileData.reset().thenCompose(reset -> parent.uploadFileSection(r2.a(), c, f.filename, reset, Optional.empty(),\n                                                                    false, 0, f.length, f.hash, f.modifiedTime, Optional.of(txn.baseKey), Optional.of(txn.dataKey), Optional.of(txn.writeKey),\n                                                                    f.skipExisting, f.overwriteExisting, true,\n                                                                    network, crypto, isCancelled, f.monitor, txn.firstMapKey(),\n                                                                    Optional.of(txn.streamSecret()), txn.firstBat, mirrorBat));\n                                                        });\n                                            }).thenApply(pair -> new Pair<>(pair.left, pair.right.stream().collect(Collectors.toList())));\n                                toClose.add(txn);\n                                return fileData.reset().thenCompose(reset -> parent.uploadFileSection(r.a(), c, f.filename, fileData, Optional.empty(), false,\n                                                0, f.length, f.hash, f.modifiedTime, Optional.of(txn.baseKey), Optional.of(txn.dataKey), Optional.of(txn.writeKey), f.skipExisting, f.overwriteExisting, true,\n                                                network, crypto, isCancelled, f.monitor, txn.firstMapKey(), Optional.of(txn.streamSecret()), txn.firstBat, mirrorBat))\n                                        .thenApply(pair -> new Pair<>(pair.left, pair.right.stream().collect(Collectors.toList())));\n                            })\n            ).thenCompose(r -> atomicallyClearTransactionsAndAddToParent(toClose, r.right, parent, transactions, r.left, c, commitWatcher, network, crypto))\n                    .thenApply(res -> {\n                        fileData.close();\n                        return res;\n                    });\n        });\n    }\n\n    private static CompletableFuture<Pair<Snapshot, List<NamedRelativeCapability>>> atomicallyClearTransactionsAndAddToParent(\n            List<FileUploadTransaction> toClose,\n            List<NamedRelativeCapability> childLinks,\n            FileWrapper parent,\n            TransactionService transactions,\n            Snapshot in,\n            Committer c,\n            Supplier<Boolean> commitWatcher,\n            NetworkAccess network,\n            Crypto crypto) {\n        if (toClose.isEmpty() && childLinks.isEmpty())\n            return Futures.of(new Pair<>(in, childLinks));\n        return parent.getUpdated(in, network)\n                .thenCompose(latest -> latest.addChildPointers(in, c, childLinks, network.disableCommits(), crypto))\n                .thenCompose(res -> Futures.reduceAll(toClose, res, (v, f) -> transactions.close(v, c, f), (a, b) -> b))\n                .thenApply(s -> {network.enableCommits(); return s;})\n                .thenCompose(s -> network.isFull() ?\n                        network.commit(parent.owner(), commitWatcher).thenApply(x -> s) :\n                        Futures.of(s))\n                .thenApply(s -> new Pair<>(s, Collections.<NamedRelativeCapability>emptyList()));\n    }\n\n    public Optional<BatId> mirrorBatId() {\n        return pointer.fileAccess.mirrorBatId();\n    }\n\n    public CompletableFuture<Snapshot> overwriteSection(Snapshot current,\n                                                        Committer committer,\n                                                        AsyncReader fileData,\n                                                        long inputStartIndex,\n                                                        long endIndex,\n                                                        Optional<LocalDateTime> modified,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        ProgressConsumer<Long> monitor) {\n        if (! isWritable())\n            return Futures.errored(new IllegalStateException(\"Unable to modify file without write access!\"));\n        if (isDirty())\n            return Futures.errored(new IllegalStateException(\"File needs cleaning before modification.\"));\n\n        FileProperties props = getFileProperties();\n        String filename = props.name;\n        LOG.info(\"Overwriting section [\" + Long.toHexString(inputStartIndex) + \", \" + Long.toHexString(endIndex) + \"] of: \" + filename);\n\n        Supplier<Location> legacyLocs = () -> new Location(getLocation().owner, getLocation().writer, crypto.random.randomBytes(32));\n\n        SymmetricKey parentParentKey = getPointer().getParentParentKey();\n        Location parentLocation = getPointer().getParentCap().getLocation(owner(), writer());\n        Optional<Bat> parentBat = getPointer().getParentCap().bat;\n        WritableAbsoluteCapability ourCap = writableFilePointer();\n        boolean updateTreeHash = inputStartIndex == 0 && endIndex >= props.size;\n        HashTreeBuilder treeHasher = updateTreeHash ? new HashTreeBuilder(endIndex) : null;\n        return current.withWriter(owner(), writer(), network)\n                .thenCompose(base -> {\n                    FileWrapper us = this;\n                    final AtomicLong filesSize = new AtomicLong(props.size);\n                    return us.getRetriever(crypto.hasher).thenCompose(retriever -> {\n                        SymmetricKey baseKey = us.pointer.capability.rBaseKey;\n                        CryptreeNode fileAccess = us.pointer.fileAccess;\n                        SymmetricKey dataKey = fileAccess.getDataKey(baseKey);\n\n                        List<Long> startIndexes = new ArrayList<>();\n\n                        for (long startIndex = inputStartIndex; startIndex < endIndex; startIndex = startIndex + Chunk.MAX_SIZE - (startIndex % Chunk.MAX_SIZE))\n                            startIndexes.add(startIndex);\n\n                        BiFunction<Snapshot, Long, CompletableFuture<Snapshot>> composer = (version, startIndex) -> {\n                            MaybeMultihash currentHash = us.pointer.fileAccess.committedHash();\n                            return retriever.getChunk(version.get(us.writer()), network, crypto, startIndex,\n                                    filesSize.get(), ourCap, props.streamSecret, currentHash, monitor)\n                                    .thenCompose(currentLocation -> {\n                                                CompletableFuture<Optional<Pair<Location, Optional<Bat>>>> locationAt = retriever\n                                                        .getMapLabelAt(version.get(us.writer()), ourCap,\n                                                                props.streamSecret, startIndex + Chunk.MAX_SIZE, crypto.hasher, network)\n                                                        .thenApply(x -> x.map(mb -> new Pair<>(getLocation().withMapKey(mb.left), mb.right)));\n                                                return locationAt.thenCompose(locationAndBat ->\n                                                        CompletableFuture.completedFuture(new Pair<>(currentLocation, locationAndBat)));\n                                            }\n                                    ).thenCompose(pair -> {\n\n                                        if (!pair.left.isPresent()) {\n                                            CompletableFuture<Snapshot> result = new CompletableFuture<>();\n                                            result.completeExceptionally(new IllegalStateException(\"Current chunk not present\"));\n                                            return result;\n                                        }\n\n                                        LocatedChunk currentOriginal = pair.left.get();\n                                        Optional<Pair<Location, Optional<Bat>>> nextChunkLocationOpt = pair.right;\n                                        CompletableFuture<Pair<Location, Optional<Bat>>> nextChunkLocationFut = nextChunkLocationOpt\n                                                .map(Futures::of)\n                                                .orElseGet(() -> props.streamSecret\n                                                        .map(streamSecret -> FileProperties.calculateNextMapKey(streamSecret,\n                                                                currentOriginal.location.getMapKey(), currentOriginal.bat, crypto.hasher)\n                                                                .thenApply(nextMapKeyAndBat -> new Pair<>(us.getLocation().withMapKey(nextMapKeyAndBat.left), nextMapKeyAndBat.right)))\n                                                        .orElseGet(() -> Futures.of(new Pair<>(legacyLocs.get(), Optional.empty()))));\n                                        return nextChunkLocationFut.thenCompose(nextChunkLocationAndBat -> {\n                                            Location nextChunkLocation = nextChunkLocationAndBat.left;\n                                            Optional<Bat> nextChunkBat = nextChunkLocationAndBat.right;\n                                            LOG.info(\"********** Writing to chunk at mapkey: \" + ArrayOps.bytesToHex(currentOriginal.location.getMapKey()) + \" next: \" + nextChunkLocation);\n\n                                            // modify chunk, re-encrypt and upload\n                                            int internalStart = (int) (startIndex % Chunk.MAX_SIZE);\n                                            int internalEnd = endIndex - (startIndex - internalStart) > Chunk.MAX_SIZE ?\n                                                    Chunk.MAX_SIZE : (int) (endIndex - (startIndex - internalStart));\n                                            byte[] rawData = currentOriginal.chunk.data();\n                                            // extend data array if necessary\n                                            if (rawData.length < internalEnd)\n                                                rawData = Arrays.copyOfRange(rawData, 0, internalEnd);\n                                            byte[] raw = rawData;\n                                            Optional<SymmetricLinkToSigner> writerLink = startIndex < Chunk.MAX_SIZE ?\n                                                    us.pointer.fileAccess.getWriterLink(us.pointer.capability.rBaseKey) :\n                                                    Optional.empty();\n\n                                            return fileData.readIntoArray(raw, internalStart, internalEnd - internalStart)\n                                                    .thenCompose(read -> updateTreeHash ?\n                                                            treeHasher.setChunk((int)(startIndex / Chunk.MAX_SIZE), raw, crypto.hasher).thenApply(x -> read) :\n                                                            Futures.of(read))\n                                                    .thenCompose(read -> {\n\n                                                Chunk updated = new Chunk(raw, dataKey, currentOriginal.location.getMapKey(), dataKey.createNonce());\n                                                LocatedChunk located = new LocatedChunk(currentOriginal.location, currentOriginal.bat, currentOriginal.existingHash, updated);\n                                                long currentSize = filesSize.get();\n                                                // remove hash from properties as we are changing the file\n                                                FileProperties newProps = new FileProperties(props.name, false,\n                                                        props.isLink, props.mimeType,\n                                                        endIndex > currentSize ? endIndex : currentSize,\n                                                        modified.orElseGet(() -> LocalDateTime.now(ZoneOffset.UTC)), props.created, props.isHidden,\n                                                        props.thumbnail, props.streamSecret, Optional.empty());\n\n                                                Optional<BatId> mirrorBat = mirrorBatId();\n                                                CompletableFuture<Snapshot> chunkUploaded = FileUploader.uploadChunk(version, committer, us.signingPair(),\n                                                        newProps, parentLocation, parentBat, parentParentKey, baseKey, located,\n                                                        nextChunkLocation, nextChunkBat, writerLink, mirrorBat,\n                                                        crypto.random, crypto.hasher, network, monitor);\n\n                                                return chunkUploaded.thenCompose(updatedBase -> {\n                                                    //update indices to be relative to next chunk\n                                                    long updatedLength = startIndex + internalEnd - internalStart;\n                                                    if (updatedLength > filesSize.get()) {\n                                                        filesSize.set(updatedLength);\n\n                                                        if (updatedLength > Chunk.MAX_SIZE) {\n                                                            // update file size and remove treehash in FileProperties of first chunk\n                                                            return network.getFile(updatedBase, ourCap, entryWriter, ownername)\n                                                                    .thenCompose(updatedUs -> {\n                                                                        FileProperties correctedSize = updatedUs.get()\n                                                                                .getPointer().fileAccess.getProperties(ourCap.rBaseKey)\n                                                                                .withSize(endIndex)\n                                                                                .withHash(Optional.empty());\n                                                                        return updatedUs.get()\n                                                                                .getPointer().fileAccess.updateProperties(updatedBase,\n                                                                                        committer, ourCap, entryWriter, correctedSize, network);\n                                                                    });\n                                                        }\n                                                    }\n                                                    return CompletableFuture.completedFuture(updatedBase);\n                                                });\n                                            });\n                                        });\n                                    });\n                        };\n\n                        return Futures.reduceAll(startIndexes, base, composer, (a, b) -> b)\n                                .thenCompose(preHashVersion -> {\n                                    if (! updateTreeHash)\n                                        return Futures.of(preHashVersion);\n                                    // update hash branches every 5 GiB\n                                    return (endIndex == 0 ? treeHasher.setChunk(0, new byte[0], crypto.hasher) : Futures.of(true))\n                                            .thenCompose(x -> treeHasher.complete(crypto.hasher))\n                                            .thenCompose(treeHash -> network.getFile(preHashVersion, ourCap, entryWriter, ownername)\n                                                    .thenCompose(updatedUs -> updatedUs.get()\n                                                            .getHashUpdates(treeHash, network, crypto.hasher))\n                                                    .thenCompose(hashUpdates -> FileWrapper.bulkSetSameNameProperties(preHashVersion, committer, owner(), hashUpdates, network))\n                                            );\n                                });\n                    });\n                });\n    }\n\n    public CompletableFuture<FileWrapper> uploadFileSection(String filename,\n                                                            AsyncReader fileData,\n                                                            boolean isHidden,\n                                                            long startIndex,\n                                                            long endIndex,\n                                                            Optional<SymmetricKey> baseKey,\n                                                            boolean overwriteExisting,\n                                                            NetworkAccess network,\n                                                            Crypto crypto,\n                                                            Supplier<Boolean> isCancelled,\n                                                            ProgressConsumer<Long> monitor) {\n        return uploadFileSection(filename, fileData, isHidden, startIndex, endIndex, baseKey, overwriteExisting,\n                network, crypto, isCancelled, monitor, crypto.random.randomBytes(32), Optional.empty(),\n                Optional.of(Bat.random(crypto.random)), mirrorBatId());\n    }\n\n    /**\n     *\n     * @param filename\n     * @param fileData\n     * @param isHidden\n     * @param startIndex\n     * @param endIndex\n     * @param baseKey The desired base key for the uploaded file. If absent a random key is generated.\n     * @param overwriteExisting\n     * @param network\n     * @param crypto\n     * @param monitor A way to report back progress in number of bytes of file written\n     * @param firstChunkMapKey The planned location for the first chunk\n     * @return The updated version of this directory after the upload\n     */\n    public CompletableFuture<FileWrapper> uploadFileSection(String filename,\n                                                            AsyncReader fileData,\n                                                            boolean isHidden,\n                                                            long startIndex,\n                                                            long endIndex,\n                                                            Optional<SymmetricKey> baseKey,\n                                                            boolean overwriteExisting,\n                                                            NetworkAccess network,\n                                                            Crypto crypto,\n                                                            Supplier<Boolean> isCancelled,\n                                                            ProgressConsumer<Long> monitor,\n                                                            byte[] firstChunkMapKey,\n                                                            Optional<byte[]> streamSecret,\n                                                            Optional<Bat> firstBat,\n                                                            Optional<BatId> mirrorBat) {\n        if (isWritable())\n            return network.synchronizer.applyComplexUpdate(owner(), signingPair(), (current, committer) ->\n                    uploadFileSection(current, committer, filename, fileData, isHidden, startIndex, endIndex,\n                            baseKey, false, overwriteExisting, false, network, crypto, isCancelled, monitor, firstChunkMapKey, streamSecret, firstBat, mirrorBat))\n                    .thenCompose(finalBase -> getUpdated(finalBase, network));\n\n        if (! overwriteExisting)\n            return Futures.errored(new IllegalStateException(\"Cannot upload a file to a directory without write access!\"));\n        return getChild(filename, crypto.hasher, network)\n                .thenCompose(c -> {\n                    if (! c.isPresent())\n                        return Futures.errored(new IllegalStateException(\"No child with name \" + filename));\n                    FileWrapper child = c.get();\n                    return network.synchronizer.applyComplexUpdate(owner(), child.signingPair(),\n                            (current, committer) -> updateExistingChild(current, committer, child,\n                                    fileData, startIndex, endIndex, network, crypto, monitor))\n                            .thenApply(childVersion -> withVersion(version.mergeAndOverwriteWith(childVersion)));\n                });\n    }\n\n    private CompletableFuture<Snapshot> updateSize(Committer committer,\n                                                   long newSize,\n                                                   NetworkAccess network) {\n        FileProperties newProps = getFileProperties().withSize(newSize);\n        return updateProperties(version, committer, newProps, network);\n    }\n\n    @JsMethod\n    public CompletableFuture<FileWrapper> updateThumbnail(String base64Str, NetworkAccess network) {\n        Optional<Thumbnail> thumbData = Optional.empty();\n        if (base64Str != null && base64Str.length() > 0) {\n            thumbData = convertFromBase64(base64Str);\n        }\n        FileProperties updatedProperties = this.props.withThumbnail(thumbData);\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> updateProperties(s, committer, updatedProperties, network)\n        ).thenCompose(finished -> getUpdated(finished, network));\n    }\n\n    public CompletableFuture<Snapshot> uploadFileSection(Snapshot initialVersion,\n                                                         Committer committer,\n                                                         String filename,\n                                                         AsyncReader fileData,\n                                                         boolean isHidden,\n                                                         long startIndex,\n                                                         long endIndex,\n                                                         Optional<SymmetricKey> baseKey,\n                                                         boolean skipExisting,\n                                                         boolean overwriteExisting,\n                                                         boolean truncateExisting,\n                                                         NetworkAccess network,\n                                                         Crypto crypto,\n                                                         Supplier<Boolean> isCancelled,\n                                                         ProgressConsumer<Long> monitor,\n                                                         byte[] firstChunkMapKey,\n                                                         Optional<byte[]> streamSecret,\n                                                         Optional<Bat> firstBat,\n                                                         Optional<BatId> requestedMirrorBat) {\n        return uploadFileSection(initialVersion, committer, filename, fileData, Optional.empty(), isHidden, startIndex, endIndex, Optional.empty(), Optional.empty(),\n                baseKey, Optional.empty(), Optional.empty(), skipExisting, overwriteExisting, truncateExisting, network, crypto, isCancelled, monitor, firstChunkMapKey, streamSecret, firstBat, requestedMirrorBat)\n                .thenCompose(p -> getUpdated(p.left, network)\n                        .thenCompose(latest -> p.right.isEmpty() ?\n                                Futures.of(p.left) :\n                                latest.addChildPointer(p.left, committer, p.right.get(), network, crypto)));\n    }\n\n    private CompletableFuture<Pair<Snapshot, Optional<NamedRelativeCapability>>> uploadFileSection(\n            Snapshot initialVersion,\n            Committer committer,\n            String filename,\n            AsyncReader fileData,\n            Optional<Thumbnail> existingThumbnail,\n            boolean isHidden,\n            long startIndex,\n            long endIndex,\n            Optional<HashTree> hash,\n            Optional<LocalDateTime> modificationTime,\n            Optional<SymmetricKey> requestedBaseKey,\n            Optional<SymmetricKey> requestedDataKey,\n            Optional<SymmetricKey> requestedWriteKey,\n            boolean skipExisting,\n            boolean overwriteExisting,\n            boolean truncateExisting,\n            NetworkAccess network,\n            Crypto crypto,\n            Supplier<Boolean> isCancelled,\n            ProgressConsumer<Long> monitor,\n            byte[] firstChunkMapKey,\n            Optional<byte[]> streamSecret,\n            Optional<Bat> firstBat,\n            Optional<BatId> requestedMirrorBat) {\n        if (!isLegalName(filename)) {\n            return Futures.errored(new IllegalStateException(\"Illegal filename: \" + filename));\n        }\n        if (! isDirectory()) {\n            return Futures.errored(new IllegalStateException(\"Cannot upload a sub file to a file!\"));\n        }\n        Optional<BatId> mirrorBat = mirrorBatId().or(() -> requestedMirrorBat);\n        return initialVersion.withWriter(owner(), writer(), network)\n                .thenCompose(current -> getUpdated(current, network)\n                        .thenCompose(latest -> latest.getChild(current, filename, network)\n                                        .thenCompose(childOpt -> {\n                                            if (childOpt.isPresent()) {\n                                                if (skipExisting)\n                                                    return Futures.of(new Pair<>(current, Optional.empty()));\n                                                FileWrapper child = childOpt.get();\n                                                FileProperties childProps = child.getFileProperties();\n                                                Optional<HashBranch> existingHash = childProps.treeHash;\n                                                if (existingHash.isPresent() &&\n                                                        hash.isPresent() &&\n                                                        existingHash.get().rootHash.equals(hash.get().rootHash)) {\n                                                    monitor.accept(endIndex - startIndex + THUMBNAIL_PROGRESS_OFFSET);\n                                                    return Futures.of(new Pair<>(current, Optional.<NamedRelativeCapability>empty()));\n                                                }\n                                                if (! overwriteExisting)\n                                                    throw new FileExistsException(filename);\n\n                                                TriFunction<FileWrapper, Snapshot, Long, CompletableFuture<Snapshot>> updatePropsIfNecessary =\n                                                        (updatedChild, latestSnapshot, writeEnd) -> {\n                                                    if (childProps.thumbnail.isEmpty()) {\n                                                        if (writeEnd <= childProps.size)\n                                                            return Futures.of(latestSnapshot);\n                                                        // update size only\n                                                        return updatedChild.updateSize(committer, writeEnd, network);\n                                                    }\n                                                    return updatedChild.getInputStream(latestSnapshot.get(updatedChild.writer()), network, crypto, l -> {})\n                                                            .thenCompose(is -> updatedChild.recalculateThumbnail(\n                                                                latestSnapshot, committer, filename, is, isHidden,\n                                                                updatedChild.getSize(), updatedChild.props.created, network, (WritableAbsoluteCapability)updatedChild.pointer.capability,\n                                                                updatedChild.getFileProperties().streamSecret));\n                                                };\n                                                boolean redoMimetypeAndThumbnail = startIndex < 24;\n\n                                                if (truncateExisting && endIndex < childProps.size) {\n                                                    return child.truncate(current, committer, endIndex, network, crypto).thenCompose( updatedSnapshot ->\n                                                        getUpdated(updatedSnapshot, network).thenCompose( updatedParent ->\n                                                                child.getUpdated(updatedSnapshot, network).thenCompose( updatedChild ->\n                                                                    updatedParent.updateExistingChild(updatedSnapshot, committer, updatedChild, fileData,\n                                                                        startIndex, endIndex, network, crypto, monitor)\n                                                                            .thenCompose(latestSnapshot ->  updatePropsIfNecessary.apply(updatedChild, latestSnapshot, endIndex)))))\n                                                            .thenApply(s -> new Pair<>(s, Optional.<NamedRelativeCapability>empty()));\n                                                } else {\n                                                    return updateExistingChild(current, committer, child, fileData,\n                                                            startIndex, endIndex, network, crypto, monitor)\n                                                            .thenCompose(s -> redoMimetypeAndThumbnail ?\n                                                                    child.getUpdated(s, network)\n                                                                            .thenCompose(updatedChild -> updatedChild.getInputStream(s.get(updatedChild.writer()), network, crypto, l -> {})\n                                                                                    .thenCompose(is -> updatedChild.recalculateThumbnail(s, committer, filename, is, isHidden,\n                                                                                            updatedChild.getSize(), updatedChild.props.created, network, (WritableAbsoluteCapability)updatedChild.pointer.capability,\n                                                                                            updatedChild.getFileProperties().streamSecret))) :\n                                                                    Futures.of(s))\n                                                            .thenApply(s -> {\n                                                                monitor.accept(THUMBNAIL_PROGRESS_OFFSET);\n                                                                return new Pair<>(s, Optional.<NamedRelativeCapability>empty());\n                                                            });\n                                                }\n                                            }\n                                            if (startIndex > 0) {\n                                                // TODO if startIndex > 0 prepend with a zero section\n                                                throw new IllegalStateException(\"Unimplemented!\");\n                                            }\n                                            SymmetricKey fileWriteKey = requestedWriteKey.orElseGet(SymmetricKey::random);\n                                            SymmetricKey fileKey = requestedBaseKey.orElseGet(SymmetricKey::random);\n                                            SymmetricKey dataKey = requestedDataKey.orElseGet(SymmetricKey::random);\n                                            SymmetricKey rootRKey = latest.pointer.capability.rBaseKey;\n                                            CryptreeNode dirAccess = latest.pointer.fileAccess;\n                                            SymmetricKey dirParentKey = dirAccess.getParentKey(rootRKey);\n                                            Location parentLocation = getLocation();\n                                            Optional<Bat> parentBat = writableFilePointer().bat;\n                                            LocalDateTime timestamp = modificationTime.orElseGet(() -> LocalDateTime.now(ZoneOffset.UTC));\n                                            return fileData.reset()\n                                                    .thenCompose(reset -> calculateMimeType(reset, endIndex, filename)).thenCompose(mimeType -> fileData.reset()\n                                                    .thenCompose(resetReader -> {\n                                                        Optional<byte[]> actualStreamSecret = streamSecret.isPresent() ?\n                                                                streamSecret :\n                                                                Optional.of(crypto.random.randomBytes(32));\n                                                        FileProperties fileProps = new FileProperties(filename,\n                                                                false, false, mimeType, endIndex,\n                                                                timestamp, timestamp, isHidden, existingThumbnail, actualStreamSecret, hash.map(t -> t.branch(0)));\n\n                                                        FileUploader chunks = new FileUploader(filename, resetReader,\n                                                                startIndex, endIndex, fileKey, dataKey, parentLocation, parentBat,\n                                                                dirParentKey, monitor, fileProps, hash, firstChunkMapKey, firstBat, isCancelled);\n\n                                                        SigningPrivateKeyAndPublicHash signer = signingPair();\n                                                        WritableAbsoluteCapability fileWriteCap = new\n                                                                WritableAbsoluteCapability(owner(),\n                                                                signer.publicKeyHash,\n                                                                firstChunkMapKey, firstBat, fileKey,\n                                                                fileWriteKey);\n\n                                                        return chunks.upload(current, committer, network, parentLocation.owner,\n                                                                signer, mirrorBat, crypto.random, crypto.hasher)\n                                                                .thenCompose(cwd -> fileData.reset().thenCompose(resetAgain ->\n                                                                        generateThumbnailAndUpdate(cwd, committer, fileWriteCap, filename, resetAgain,\n                                                                                network, isHidden, mimeType,\n                                                                                endIndex, timestamp, timestamp, actualStreamSecret, monitor)))\n                                                                .thenApply(s -> new Pair<>(s, Optional.of(new NamedRelativeCapability(filename,\n                                                                        writableFilePointer().relativise(fileWriteCap), Optional.of(false), Optional.of(mimeType), Optional.of(timestamp)))));\n                                                    }));\n                                        })\n                        )\n                );\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> calculateAndUpdateThumbnail(NetworkAccess network, Crypto crypto) {\n        return network.synchronizer.applyComplexComputation(owner(), signingPair(),\n                (latestSnapshot, committer) -> getInputStream(latestSnapshot.get(writer()), network, crypto, l -> {})\n                        .thenCompose(is -> recalculateThumbnail(\n                                latestSnapshot, committer, getName(), is, props.isHidden,\n                                getSize(), props.created, network, (WritableAbsoluteCapability)pointer.capability,\n                                getFileProperties().streamSecret))\n                        .thenApply(res -> new Pair<>(res, true))\n                        .exceptionally(ex -> new Pair<>(latestSnapshot, false))\n        ).thenApply(p -> p.right);\n    }\n\n    private CompletableFuture<Snapshot> recalculateThumbnail(Snapshot snapshot, Committer committer, String filename,\n                                                             AsyncReader fileData, boolean isHidden, long fileSize,\n                                                             LocalDateTime createdDateTime, NetworkAccess network,\n                                                             WritableAbsoluteCapability fileWriteCap, Optional<byte[]> streamSecret\n    ) {\n        return fileData.reset()\n                .thenCompose(fileData2 -> calculateMimeType(fileData2, fileSize, filename)\n                        .thenCompose(mimeType -> fileData.reset()\n                                .thenCompose(resetAgain ->\n                                    generateThumbnailAndUpdate(snapshot, committer, fileWriteCap, filename, resetAgain,\n                                            network, isHidden, mimeType, fileSize, getFileProperties().modified, createdDateTime, streamSecret, true, x -> {}))));\n    }\n\n    private CompletableFuture<Snapshot> generateThumbnailAndUpdate(Snapshot base,\n                                                                   Committer committer,\n                                                                   WritableAbsoluteCapability cap,\n                                                                   String fileName,\n                                                                   AsyncReader fileData,\n                                                                   NetworkAccess network,\n                                                                   Boolean isHidden,\n                                                                   String mimeType,\n                                                                   long fileSize,\n                                                                   LocalDateTime updatedDateTime,\n                                                                   LocalDateTime createdDateTime,\n                                                                   Optional<byte[]> streamSecret,\n                                                                   ProgressConsumer<Long> monitor) {\n        return generateThumbnailAndUpdate(base, committer, cap, fileName, fileData, network, isHidden,\n                mimeType, fileSize, updatedDateTime, createdDateTime, streamSecret, false, monitor);\n    }\n\n    private CompletableFuture<Snapshot> generateThumbnailAndUpdate(Snapshot base,\n                                                                   Committer committer,\n                                                                   WritableAbsoluteCapability cap,\n                                                                   String fileName,\n                                                                   AsyncReader fileData,\n                                                                   NetworkAccess network,\n                                                                   Boolean isHidden,\n                                                                   String mimeType,\n                                                                   long fileSize,\n                                                                   LocalDateTime updatedDateTime,\n                                                                   LocalDateTime createdDateTime,\n                                                                   Optional<byte[]> streamSecret,\n                                                                   boolean replaceExistingThumbnail,\n                                                                   ProgressConsumer<Long> monitor) {\n        return network.getFile(base, cap, getChildsEntryWriter(), ownername).thenCompose(fileOpt -> {\n            if (replaceExistingThumbnail || fileOpt.get().props.thumbnail.isEmpty()) {\n                return generateThumbnail(network, fileData, (int) Math.min(fileSize, Integer.MAX_VALUE), fileName, mimeType)\n                        .thenCompose(thumbData -> {\n                            if (thumbData.isEmpty() && mimeType.equals(fileOpt.get().getFileProperties().mimeType))\n                                return Futures.of(base);\n                            FileProperties fileProps = new FileProperties(fileName, false, props.isLink, mimeType, fileSize,\n                                    updatedDateTime, createdDateTime, isHidden, thumbData, streamSecret, fileOpt.get().props.treeHash);\n\n                            return fileOpt.get().updateProperties(base, committer, fileProps, network);\n                        });\n            } else {\n                return Futures.of(base);\n            }\n        }).thenApply(s -> {\n            monitor.accept(THUMBNAIL_PROGRESS_OFFSET);\n            return s;\n        });\n    }\n\n    private CompletableFuture<Snapshot> updateProperties(Snapshot base,\n                                                         Committer committer,\n                                                         FileProperties newProps,\n                                                         NetworkAccess network) {\n        return getPointer().fileAccess.updateProperties(base, committer, writableFilePointer(),\n                getChildsEntryWriter(), newProps, network);\n    }\n\n    private CompletableFuture<Snapshot> addChildPointer(Snapshot current,\n                                                        Committer committer,\n                                                        NamedRelativeCapability childCap,\n                                                        NetworkAccess network,\n                                                        Crypto crypto) {\n        return addChildPointers(current, committer, Collections.singletonList(childCap), network, crypto);\n    }\n\n    private CompletableFuture<Snapshot> addChildPointers(Snapshot current,\n                                                        Committer committer,\n                                                        List<NamedRelativeCapability> childCaps,\n                                                        NetworkAccess network,\n                                                        Crypto crypto) {\n        return pointer.fileAccess.addChildrenAndCommit(current, committer,\n                childCaps, writableFilePointer(), signingPair(), mirrorBatId(), network, crypto)\n                .thenApply(newBase -> {\n                    setModified();\n                    return newBase;\n                });\n    }\n\n    public CompletableFuture<FileWrapper> appendToChild(String filename,\n                                                        long expectedSize,\n                                                        byte[] fileData,\n                                                        boolean isHidden,\n                                                        Optional<BatId> mirrorBat,\n                                                        NetworkAccess network,\n                                                        Crypto crypto,\n                                                        ProgressConsumer<Long> monitor) {\n        return getChild(filename, crypto.hasher, network)\n                .thenCompose(child -> child\n                        .flatMap(c -> c.getFileProperties().streamSecret)\n                        .map(secret -> FileProperties.calculateMapKey(secret,\n                                child.get().getLocation().getMapKey(),\n                                child.get().pointer.capability.bat,\n                                child.get().getFileProperties().size, crypto.hasher)\n                                .thenApply(p -> new Triple<>(p.left, p.right, Optional.of(secret))))\n                        .orElseGet(() -> Futures.of(new Triple<>(crypto.random.randomBytes(32),\n                                Optional.of(Bat.random(crypto.random)), Optional.empty())))\n                        .thenCompose(x -> {\n                            long size = child.map(f -> f.getSize()).orElse(0L);\n                            if (size != expectedSize)\n                                throw new IllegalStateException(\"File has been concurrently modified!\");\n                            return uploadFileSection(filename, AsyncReader.build(fileData), isHidden,\n                                    size,\n                                    fileData.length + size,\n                                    child.map(f -> f.getPointer().capability.rBaseKey), true, network, crypto,\n                                    () -> false, monitor, x.left, x.right, x.middle, mirrorBat);\n                        }));\n    }\n\n    public CompletableFuture<Snapshot> append(byte[] fileData,\n                                              NetworkAccess network,\n                                              Crypto crypto,\n                                              Committer committer,\n                                              ProgressConsumer<Long> progress) {\n        long size = getSize();\n        return overwriteSection(version, committer, AsyncReader.build(fileData), size, size + fileData.length, Optional.empty(), network, crypto, progress);\n    }\n\n    /**\n     *\n     * @param current\n     * @param committer\n     * @param existingChild\n     * @param fileData\n     * @param inputStartIndex\n     * @param endIndex\n     * @param network\n     * @param crypto\n     * @param monitor\n     * @return The committed root for the parent (this) directory\n     */\n    private CompletableFuture<Snapshot> updateExistingChild(Snapshot current,\n                                                            Committer committer,\n                                                            FileWrapper existingChild,\n                                                            AsyncReader fileData,\n                                                            long inputStartIndex,\n                                                            long endIndex,\n                                                            NetworkAccess network,\n                                                            Crypto crypto,\n                                                            ProgressConsumer<Long> monitor) {\n\n        FileProperties existingProps = existingChild.getFileProperties();\n        String filename = existingProps.name;\n        LOG.info(\"Overwriting section [\" + Long.toHexString(inputStartIndex) + \", \" + Long.toHexString(endIndex) + \"] of child with name: \" + filename);\n\n        return current.withWriter(existingChild.owner(), existingChild.writer(), network)\n                .thenCompose(state ->\n                        existingChild.clean(state, committer, network, crypto)\n                                .thenCompose(pair -> pair.left.overwriteSection(pair.right, committer, fileData,\n                        inputStartIndex, endIndex, Optional.empty(), network, crypto, monitor)));\n    }\n\n    static boolean isLegalName(String name) {\n        return !name.contains(\"/\") && ! name.equals(\".\") && ! name.equals(\"..\") && ! name.isEmpty();\n    }\n\n    /**\n     *\n     * @param newFolderName\n     * @param network\n     * @param isSystemFolder\n     * @param crypto\n     * @return An updated version of this directory\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> mkdir(String newFolderName,\n                                                NetworkAccess network,\n                                                boolean isSystemFolder,\n                                                Optional<BatId> mirrorBat,\n                                                Crypto crypto) {\n        return mkdir(newFolderName, network, null, Optional.empty(), isSystemFolder, mirrorBatId().or(() -> mirrorBat), crypto);\n    }\n\n    public CompletableFuture<FileWrapper> mkdir(String newFolderName,\n                                                NetworkAccess network,\n                                                SymmetricKey requestedBaseSymmetricKey,\n                                                Optional<Bat> desiredBat,\n                                                boolean isSystemFolder,\n                                                Optional<BatId> mirrorBat,\n                                                Crypto crypto) {\n\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (state, committer) -> mkdir(newFolderName, Optional.ofNullable(requestedBaseSymmetricKey),\n                        Optional.empty(), Optional.empty(), desiredBat, isSystemFolder, mirrorBatId().or(() -> mirrorBat), network, crypto, state, committer))\n                .thenCompose(version -> getUpdated(version, network));\n    }\n\n    public CompletableFuture<Snapshot> mkdir(String newFolderName,\n                                             Optional<SymmetricKey> requestedBaseReadKey,\n                                             Optional<SymmetricKey> requestedBaseWriteKey,\n                                             Optional<byte[]> desiredMapKey,\n                                             Optional<Bat> desiredBat,\n                                             boolean isSystemFolder,\n                                             Optional<BatId> mirrorBat,\n                                             NetworkAccess network,\n                                             Crypto crypto,\n                                             Snapshot version,\n                                             Committer committer) {\n\n        if (!this.isDirectory()) {\n            return Futures.errored(new IllegalStateException(\"Cannot mkdir in a file!\"));\n        }\n        if (!isLegalName(newFolderName)) {\n            return Futures.errored(new IllegalStateException(\"Illegal directory name: \" + newFolderName));\n        }\n        Snapshot fullVersion = this.version.mergeAndOverwriteWith(version);\n        return hasChildWithName(fullVersion, newFolderName, crypto.hasher, network).thenCompose(hasChild -> {\n            if (hasChild) {\n                return Futures.errored(new IllegalStateException(\"Child already exists with name: \" + newFolderName));\n            }\n            return pointer.fileAccess.mkdir(fullVersion, committer, newFolderName, network, writableFilePointer(), getChildsEntryWriter(),\n                    requestedBaseReadKey, requestedBaseWriteKey, desiredMapKey, desiredBat, isSystemFolder, mirrorBatId().or(() -> mirrorBat), crypto).thenApply(x -> {\n                setModified();\n                return x;\n            });\n        });\n    }\n\n    /** Get or create a descendant directory\n     *\n     * @param subPath\n     * @param network\n     * @param isSystemFolder\n     * @param crypto\n     * @return\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> getOrMkdirs(Path subPath,\n                                                      NetworkAccess network,\n                                                      boolean isSystemFolder,\n                                                      Optional<BatId> mirrorBat,\n                                                      Crypto crypto) {\n        String finalPath = TrieNode.canonicalise(subPath.toString());\n        List<String> elements = Arrays.asList(finalPath.split(\"/\"));\n        if (! isWritable())\n            return getChild(elements.get(0), crypto.hasher, network)\n                    .thenCompose(kid -> kid.get().getOrMkdirs(PathUtil.get(elements.subList(1, elements.size())\n                            .stream().collect(Collectors.joining(\"/\"))), network, isSystemFolder, mirrorBat, crypto));\n        return network.synchronizer.applyComplexComputation(owner(), signingPair(),\n                (state, committer) -> getOrMkdirs(elements, isSystemFolder, mirrorBat, network, crypto, state, committer))\n                .thenApply(p -> p.right);\n    }\n\n    public CompletableFuture<Pair<Snapshot, FileWrapper>> getOrMkdirs(List<String> subPath,\n                                                                      boolean isSystemFolder,\n                                                                      Optional<BatId> mirrorBat,\n                                                                      NetworkAccess network,\n                                                                      Crypto crypto,\n                                                                      Snapshot version,\n                                                                      Committer committer) {\n        return Futures.reduceAll(subPath, new Pair<>(version, this.withVersion(version)),\n                (p, name) -> p.right.getOrMkdir(name, Optional.empty(), Optional.empty(), Optional.empty(),\n                                Optional.empty(), isSystemFolder, p.right.mirrorBatId().or(() -> mirrorBat), network, crypto, p.left, committer),\n                (a, b) -> b);\n    }\n\n    private CompletableFuture<Pair<Snapshot, FileWrapper>> getOrMkdir(String newFolderName,\n                                                                      Optional<SymmetricKey> requestedBaseReadKey,\n                                                                      Optional<SymmetricKey> requestedBaseWriteKey,\n                                                                      Optional<byte[]> desiredMapKey,\n                                                                      Optional<Bat> desiredBat,\n                                                                      boolean isSystemFolder,\n                                                                      Optional<BatId> mirrorBat,\n                                                                      NetworkAccess network,\n                                                                      Crypto crypto,\n                                                                      Snapshot version,\n                                                                      Committer committer) {\n\n        if (! this.isDirectory()) {\n            return Futures.errored(new IllegalStateException(\"Cannot mkdir in a file!\"));\n        }\n        if (! isLegalName(newFolderName)) {\n            return Futures.errored(new IllegalStateException(\"Illegal directory name: \" + newFolderName));\n        }\n        Snapshot fullVersion = this.version.mergeAndOverwriteWith(version);\n        return getChild(fullVersion, newFolderName, network).thenCompose(childOpt -> {\n            if (childOpt.isPresent()) {\n                return Futures.of(new Pair<>(fullVersion, childOpt.get()));\n            }\n            return pointer.fileAccess.mkdir(fullVersion, committer, newFolderName, network, writableFilePointer(), getChildsEntryWriter(),\n                    requestedBaseReadKey, requestedBaseWriteKey, desiredMapKey, desiredBat, isSystemFolder, mirrorBat, crypto).thenCompose(x -> {\n                setModified();\n                return getUpdated(x, network).thenCompose(us -> us.getChild(newFolderName, crypto.hasher, network))\n                        .thenApply(child -> new Pair<>(x, child.get()));\n            });\n        });\n    }\n\n    /**\n     * @param newFilename\n     * @param parent\n     * @param userContext\n     * @return the updated parent\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> rename(String newFilename,\n                                                 FileWrapper parent,\n                                                 Path ourPath,\n                                                 UserContext userContext) {\n        if (! isLegalName(newFilename))\n            return CompletableFuture.completedFuture(parent);\n        if (! parent.isWritable())\n            return Futures.errored(new IllegalStateException(\"Unable to rename something without write access to the parent!\"));\n        CompletableFuture<Optional<FileWrapper>> childExists = parent == null ?\n                CompletableFuture.completedFuture(Optional.empty()) :\n                parent.getDescendentByPath(newFilename, userContext.crypto.hasher, userContext.network);\n        ensureUnmodified();\n        FileProperties currentProps = getFileProperties();\n        setModified();\n        return childExists\n                .thenCompose(existing -> {\n                    if (existing.isPresent())\n                        throw new IllegalStateException(\"Cannot rename, child already exists with name: \" + newFilename);\n\n                    //get current props\n                    RetrievedCapability ourPointer = linkPointer.orElse(pointer);\n                    WritableAbsoluteCapability us = (WritableAbsoluteCapability) ourPointer.capability;\n                    CryptreeNode nodeToUpdate = ourPointer.fileAccess;\n\n                    boolean isDir = this.isDirectory();\n                    boolean isLink = ourPointer.getProperties().isLink;\n                    FileProperties newProps = new FileProperties(newFilename, isDir, isLink,\n                            currentProps.mimeType, currentProps.size,\n                            currentProps.modified, currentProps.created, currentProps.isHidden,\n                            currentProps.thumbnail, currentProps.streamSecret, currentProps.treeHash);\n                    SigningPrivateKeyAndPublicHash signer = isLink ? parent.signingPair() : signingPair();\n                    return userContext.network.synchronizer.applyComplexUpdate(owner(), signer,\n                            (s, committer) -> nodeToUpdate.updateProperties(s, committer, us,\n                                    entryWriter, newProps, userContext.network)\n                                    .thenCompose(updated -> parent.updateChildLinks(updated, committer,\n                                            Arrays.asList(new Pair<>(us, new NamedAbsoluteCapability(newFilename, us,\n                                                    Optional.of(isDir),\n                                                    Optional.of(currentProps.mimeType),\n                                                    Optional.of(currentProps.created)))),\n                                            userContext.network, userContext.crypto.random, userContext.crypto.hasher))\n                                    .thenCompose(v -> userContext.isSecretLink() ? Futures.of(v) :\n                                            userContext.sharedWithCache.rename(ourPath,\n                                                    ourPath.getParent().resolve(newFilename), v, committer, userContext.network))\n                                    .thenCompose(v -> {\n                                        if (! isLink)\n                                            return Futures.of(v);\n                                        // make sure to update name in link target for writable links, otherwise old name will leak\n                                        RetrievedCapability nonLinkPointer = pointer;\n                                        WritableAbsoluteCapability nonLinkUs = (WritableAbsoluteCapability) nonLinkPointer.capability;\n                                        CryptreeNode nonLinkNodeToUpdate = nonLinkPointer.fileAccess;\n                                        FileProperties nonLinkProps = new FileProperties(newFilename, isDir, false,\n                                                currentProps.mimeType, currentProps.size,\n                                                currentProps.modified, currentProps.created, currentProps.isHidden,\n                                                currentProps.thumbnail, currentProps.streamSecret, currentProps.treeHash);\n                                        return v.withWriter(owner(), nonLinkUs.writer, userContext.network)\n                                                .thenCompose(v2 -> nonLinkNodeToUpdate.updateProperties(v2, committer, nonLinkUs,\n                                                        Optional.of(signingPair()), nonLinkProps, userContext.network));\n                                    })\n                    ).thenCompose(newVersion -> parent.getUpdated(newVersion, userContext.network));\n                });\n    }\n\n    public CompletableFuture<Snapshot> addMirrorBat(BatId mirrorBat, boolean addToFragmentsOnly, NetworkAccess network) {\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, committer) -> getPointer().fileAccess.addMirrorBat(s, committer, writableFilePointer(),\n                        entryWriter, getFileProperties().streamSecret, mirrorBat, ! addToFragmentsOnly, network));\n    }\n\n    public CompletableFuture<Boolean> setProperties(FileProperties updatedProperties,\n                                                    Hasher hasher,\n                                                    NetworkAccess network,\n                                                    Optional<FileWrapper> parent) {\n        setModified();\n        String newName = updatedProperties.name;\n        if (!isLegalName(newName)) {\n            return Futures.errored(new IllegalArgumentException(\"Illegal file name: \" + newName));\n        }\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (s, comitter) -> (! parent.isPresent() ?\n                        CompletableFuture.completedFuture(s) :\n                        s.withWriter(owner(), parent.get().writer(), network)\n                ).thenCompose(withParent -> parent.get().hasChildWithName(withParent, newName, hasher, network))\n                        .thenCompose(hasChild -> ! hasChild ?\n                                CompletableFuture.completedFuture(true) :\n                                parent.get().getChildrenCapabilities(hasher, network)\n                                        .thenApply(childCaps -> {\n                                            if (! childCaps.stream()\n                                                    .map(l -> new ByteArrayWrapper(l.cap.getMapKey()))\n                                                    .collect(Collectors.toSet())\n                                                    .contains(new ByteArrayWrapper(pointer.capability.getMapKey())))\n                                                throw new IllegalStateException(\"Cannot rename to same name as an existing file\");\n                                            return true;\n                                        })).thenCompose(x -> {\n                            CryptreeNode fileAccess = pointer.fileAccess;\n                            return fileAccess.updateProperties(s, comitter, writableFilePointer(),\n                                    entryWriter, updatedProperties, network);\n                        }))\n                .thenApply(fa -> true);\n    }\n\n    public CompletableFuture<Boolean> setSameNameProperties(FileProperties updatedProperties,\n                                                            NetworkAccess network) {\n        String name = getName();\n        setModified();\n        String newName = updatedProperties.name;\n        if (! newName.equals(name)) {\n            return Futures.errored(new IllegalArgumentException(\"Can't rename file here: \" + newName));\n        }\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                        (s, c) -> pointer.fileAccess.updateProperties(s, c, writableFilePointer(), entryWriter, updatedProperties, network))\n                .thenApply(fa -> true);\n    }\n\n    public static class PropsUpdate {\n        public final WritableAbsoluteCapability cap;\n        public final CryptreeNode meta;\n        public final Optional<SigningPrivateKeyAndPublicHash> entryWriter;\n        public final FileProperties newProps;\n\n        public PropsUpdate(WritableAbsoluteCapability cap, CryptreeNode meta, Optional<SigningPrivateKeyAndPublicHash> entryWriter, FileProperties newProps) {\n            this.cap = cap;\n            this.meta = meta;\n            this.entryWriter = entryWriter;\n            this.newProps = newProps;\n        }\n    }\n\n    public CompletableFuture<List<PropsUpdate>> getHashUpdates(HashTree hash, NetworkAccess network, Hasher hasher) {\n        WritableAbsoluteCapability cap = writableFilePointer();\n        if (getFileProperties().streamSecret.isEmpty())\n            return Futures.of(Collections.emptyList());\n        long fileSize = getSize();\n        long nBranches = fileSize == 0 ? 1 : (fileSize + 1024L * Chunk.MAX_SIZE - 1) / (1024L * Chunk.MAX_SIZE);\n        byte[] streamSecret = getFileProperties().streamSecret.get();\n        return Futures.combineAllInOrder(LongStream.range(0, nBranches)\n                .mapToObj(b -> FileProperties.calculateMapKey(streamSecret, cap.getMapKey(), cap.bat, b * 1024 * Chunk.MAX_SIZE, hasher)\n                        .thenCompose(loc -> {\n                            WritableAbsoluteCapability chunkCap = cap.withMapKey(loc.left, loc.right);\n                            long chunkIndex = b * 1024;\n                            return network.getMetadata(version.get(writer()), chunkCap)\n                                    .thenApply(meta -> new PropsUpdate(chunkCap, meta.get(), entryWriter, meta.get().getProperties(chunkCap.rBaseKey)\n                                            .withHash(Optional.of(hash.branch(chunkIndex)))));\n                        }))\n                .collect(Collectors.toList()));\n    }\n\n    public static CompletableFuture<Snapshot> bulkSetSameNameProperties(Snapshot s,\n                                                                        Committer c,\n                                                                        PublicKeyHash owner,\n                                                                        List<PropsUpdate> updates,\n                                                                        NetworkAccess network) {\n        return Futures.reduceAll(updates, s,\n                (v, p) -> v.withWriter(owner, p.cap.writer, network)\n                        .thenCompose(v2 -> p.meta.updateProperties(v2, c, p.cap, p.entryWriter, p.newProps, network)),\n                (a, b) -> a.mergeAndOverwriteWith(b));\n    }\n\n    public static CompletableFuture<Boolean> bulkSetSameNameProperties(List<PropsUpdate> updates,\n                                                                       NetworkAccess network) {\n        PropsUpdate first = updates.get(0);\n        PublicKeyHash owner = first.cap.owner;\n        SigningPrivateKeyAndPublicHash signer = first.meta.getSigner(first.cap.rBaseKey, first.cap.wBaseKey.get(), first.entryWriter);\n        return network.synchronizer.applyComplexUpdate(owner, signer,\n                        (s, c) -> bulkSetSameNameProperties(s, c, owner, updates, network))\n                .thenApply(fa -> true);\n    }\n\n    @JsMethod\n    public AbsoluteCapability readOnlyPointer() {\n        return pointer.capability.readOnly();\n    }\n\n    public WritableAbsoluteCapability writableFilePointer() {\n        if (! isWritable())\n            throw new IllegalStateException(\"File is not writable!\");\n        return (WritableAbsoluteCapability) pointer.capability;\n    }\n\n    public SigningPrivateKeyAndPublicHash signingPair() {\n        if (! isWritable())\n            throw new IllegalStateException(\"File is not writable!\");\n        return pointer.capability.wBaseKey\n                .map(w -> pointer.fileAccess.getSigner(pointer.capability.rBaseKey, w, entryWriter))\n                .orElseGet(entryWriter::get);\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> moveTo(FileWrapper target, FileWrapper parent, Path ourPath, UserContext context, Supplier<CompletableFuture<Boolean>> preserveAccess) {\n        ensureUnmodified();\n\n        if (! parent.isWritable())\n            return Futures.errored(new IllegalStateException(\"Cannot move file without write access to parent\"));\n        if (! target.isDirectory()) {\n            return Futures.errored(new IllegalStateException(\"CopyTo target \" + target + \" must be a directory\"));\n        }\n        return target.hasChild(getName(), context.crypto.hasher, context.network).thenApply(nameClash -> {\n            if (nameClash)\n                throw new IllegalStateException(\"Child already exists with name \" + getName() + \" in destination folder.\");\n            return true;\n        }).thenCompose(x -> {\n            // ensure we aren't trying to move a folder to a descendant folder, which will result in data loss!\n            return target.getPath(context.network).thenApply(targetPath -> {\n                        Path targetP = PathUtil.get(targetPath);\n                        if (targetP.startsWith(ourPath))\n                            throw new IllegalStateException(\"You cannot move a folder to a descendant folder\");\n                        return true;\n                    });\n        }).thenCompose(x -> {\n            Optional<BatId> targetMirrorBatId = target.mirrorBatId()\n                    .or(() -> target.owner().equals(context.signer.publicKeyHash) ?\n                            context.mirrorBatId() :\n                            Optional.empty());\n\n            NetworkAccess net = context.network;\n            Hasher hasher = context.crypto.hasher;\n\n            return context.getPublicFile(ourPath).thenApply(opt -> opt.isPresent())\n                    .thenCompose(isPublic -> net.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                            (v, c) -> context.sharedWithCache.getAllDescendantShares(ourPath, v)\n                                    .thenCompose(shared -> {\n                                        return (isPublic || !owner().equals(target.owner()) ?\n                                                Futures.of(false) :\n                                                shared.isEmpty() // fast path\n                                                        ? Futures.of(true) : preserveAccess.get())\n                                                .thenCompose(keepAccess -> {\n                                                    boolean differentParentWriter = !target.writer().equals(parent.writer());\n                                                    // TODO optimise different parent writer case by correcting owned keys\n                                                    if (keepAccess && !differentParentWriter) {\n                                                        // just update parent and child pointers, no need to re-upload, rotate keys etc.\n                                                        boolean differentWriter = !target.writer().equals(writer());\n                                                        boolean ourFile = context.signer != null && target.owner().equals(context.signer.publicKeyHash);\n                                                        RelativeCapability newParentLink = new RelativeCapability(differentWriter ?\n                                                                Optional.of(target.writer()) :\n                                                                Optional.empty(),\n                                                                target.getLocation().getMapKey(), target.writableFilePointer().bat, target.getParentKey(), Optional.empty());\n                                                        CryptreeNode newMetadata = pointer.fileAccess.withParentLink(getParentKey(), newParentLink);\n                                                        RelativeCapability ourNewcap = target.writableFilePointer().relativise(pointer.capability);\n                                                        return IpfsTransaction.call(owner(),\n                                                                tid -> target.getPath(net).thenCompose(newPath -> v.withWriter(owner(), target.writer(), net)\n                                                                        .thenCompose(w -> net.uploadChunk(w, c, newMetadata, owner(), pointer.capability.getMapKey(), signingPair(), tid))\n                                                                        .thenCompose(v2 -> target.pointer.fileAccess.addChildrenAndCommit(v2, c,\n                                                                                Arrays.asList(new NamedRelativeCapability(getName(), ourNewcap, Optional.of(isDirectory()), Optional.of(getFileProperties().mimeType), Optional.of(getFileProperties().created))),\n                                                                                target.writableFilePointer(), target.signingPair(), targetMirrorBatId, net, context.crypto))\n                                                                        .thenCompose(v3 -> parent.pointer.fileAccess\n                                                                                .removeChildren(v3, c, Arrays.asList(isLink() ? linkPointer.get().capability : getPointer().capability), parent.writableFilePointer(),\n                                                                                        parent.entryWriter, net, context.crypto.random, hasher))\n                                                                        .thenCompose(v4 -> !ourFile || shared.isEmpty() ? Futures.of(v4) : context.sharedWithCache.clearSharedWith(ourPath, v4, c, net))\n                                                                        .thenCompose(v5 -> !ourFile || shared.isEmpty() ? Futures.of(v5) : context.sharedWithCache.addAllSharedWith(shared.entrySet().stream()\n                                                                                .collect(Collectors.toMap(e -> PathUtil.get(newPath).resolve(e.getKey().relativize(ourPath)), e -> e.getValue())), v5, c, net))),\n                                                                net.dhtClient);\n\n                                                    }\n                                                    return version.withWriter(owner(), target.writer(), net)\n                                                            .thenCompose(both -> copyTo(target, this.props.thumbnail, targetMirrorBatId, net, context.crypto, both, c))\n                                                            .thenCompose(v2 -> version.withWriter(owner(), parent.writer(), net)\n                                                                    .thenCompose(v3 -> parent.pointer.fileAccess\n                                                                            .removeChildren(v2, c, isLink() ? Arrays.asList(linkPointer.get().capability) : Arrays.asList(getPointer().capability), parent.writableFilePointer(),\n                                                                                    parent.entryWriter, net, context.crypto.random, hasher))\n                                                                    .thenCompose(v4 -> IpfsTransaction.call(owner(),\n                                                                                    tid -> FileWrapper.deleteAllChunks(\n                                                                                            isLink() ?\n                                                                                                    (WritableAbsoluteCapability) getLinkPointer().capability :\n                                                                                                    writableFilePointer(),\n                                                                                            parent.signingPair(), tid, hasher, net, v4, c), net.dhtClient)\n                                                                            .thenCompose(v5 -> context.isSecretLink() ? Futures.of(v5) :\n                                                                                    context.sharedWithCache.clearSharedWith(ourPath, v5, c, net)))\n                                                            );\n                                                });\n                                    }))).thenApply(s -> true);\n        });\n}\n\n@JsMethod\npublic CompletableFuture<Boolean> copyTo(FileWrapper target, UserContext context) {\n    ensureUnmodified();\n    NetworkAccess network = context.network;\n    Crypto crypto = context.crypto;\n    if (! target.isDirectory()) {\n        return Futures.errored(new IllegalStateException(\"CopyTo target \" + target + \" must be a directory\"));\n    }\n\n    Optional<BatId> targetMirrorBatId = target.mirrorBatId()\n            .or(() -> target.owner().equals(context.signer.publicKeyHash) ?\n                    context.mirrorBatId() :\n                    Optional.empty());\n    return context.network.synchronizer.applyComplexUpdate(target.owner(), target.signingPair(),\n                    (version, committer) -> version.withWriter(owner(), writer(), network)\n                            .thenCompose(both -> copyTo(target, this.props.thumbnail, targetMirrorBatId, network, crypto, both, committer)))\n            .thenApply(newAccess -> true);\n    }\n\n    public CompletableFuture<Optional<HashTree>> getHash(NetworkAccess network, Hasher hasher) {\n        long size = getSize();\n        Optional<HashBranch> hash = getFileProperties().treeHash;\n        if (hash.isEmpty())\n            return Futures.of(Optional.empty());\n        if (size < 1024 * Chunk.MAX_SIZE)\n            return Futures.of(hash.map(t -> new HashTree(t.rootHash, t\n                    .level1.stream().collect(Collectors.toList()),\n                    t.level2.stream().collect(Collectors.toList()),\n                    t.level3.stream().collect(Collectors.toList()))));\n        long nBranches = (size + 1024 * Chunk.MAX_SIZE - 1) / (1024 * Chunk.MAX_SIZE);\n        return Futures.combineAllInOrder(LongStream.range(0, nBranches).mapToObj(b -> {\n            WritableAbsoluteCapability cap = writableFilePointer();\n            return FileProperties.calculateMapKey(getFileProperties().streamSecret.get(),\n                    cap.getMapKey(), cap.bat, b * 1024 * Chunk.MAX_SIZE, hasher).thenCompose(loc -> {\n                WritableAbsoluteCapability chunkCap = cap.withMapKey(loc.left, loc.right);\n                return network.getMetadata(version.get(writer()), chunkCap)\n                        .thenApply(meta -> meta.get().getProperties(chunkCap.rBaseKey).treeHash.get());\n            });\n        }).collect(Collectors.toList()))\n                .thenApply(HashTree::fromBranches)\n                .thenApply(Optional::of);\n    }\n\n    private CompletableFuture<Snapshot> copyTo(FileWrapper target,\n                                              Optional<Thumbnail> existingThumbnail,\n                                              Optional<BatId> targetMirrorBat,\n                                              NetworkAccess network,\n                                              Crypto crypto,\n                                              Snapshot version,\n                                              Committer committer) {\n        if (! target.isDirectory()) {\n            return Futures.errored(new IllegalStateException(\"CopyTo target \" + target + \" must be a directory\"));\n        }\n        Supplier<Boolean> isCancelled = () -> false;\n\n        return pickUniqueCopyName(target, version, crypto.hasher, network).thenCompose(uniqueName -> {\n            if (isDirectory()) {\n                byte[] newMapKey = crypto.random.randomBytes(32);\n                Optional<Bat> newBat = Optional.of(Bat.random(crypto.random));\n                SymmetricKey newBaseR = SymmetricKey.random();\n                SymmetricKey newBaseW = SymmetricKey.random();\n                WritableAbsoluteCapability newCap = ((WritableAbsoluteCapability)target.getPointer().capability)\n                        .withMapKey(newMapKey, newBat)\n                        .withBaseKey(newBaseR)\n                        .withBaseWriteKey(newBaseW);\n                return withVersion(this.version.mergeAndOverwriteWith(version))\n                        .getChildren(version, crypto.hasher, network, false).thenCompose(children ->\n                        target.mkdir(uniqueName, Optional.of(newBaseR), Optional.of(newBaseW), Optional.of(newMapKey),\n                                newBat, getFileProperties().isHidden, targetMirrorBat, network, crypto, version, committer)\n                                .thenCompose(versionWithDir ->\n                                        network.getFile(versionWithDir, newCap, target.getChildsEntryWriter(), target.ownername)\n                                                .thenCompose(subTargetOpt -> {\n                                                    FileWrapper newTarget = subTargetOpt.get();\n                                                    return Futures.reduceAll(children, versionWithDir,\n                                                            (s, child) -> newTarget.getUpdated(s, network)\n                                                                    .thenCompose(updated ->\n                                                                            child.copyTo(updated, child.getFileProperties().thumbnail, targetMirrorBat, network, crypto, s, committer)),\n                                                            (a, b) -> a.merge(b));\n                                                })));\n            } else {\n                return version.withWriter(owner(), writer(), network).thenCompose(snapshot ->\n                        getInputStream(snapshot.get(writer()), network, crypto, x -> {})\n                                .thenCompose(stream -> getHash(network, crypto.hasher).thenCompose(hashTree -> target.uploadFileSection(snapshot, committer,\n                                                uniqueName, stream, existingThumbnail, false, 0, getSize(), hashTree,\n                                                Optional.of(getFileProperties().modified),\n                                                Optional.empty(), Optional.empty(), Optional.empty(), false, false, false, network, crypto, isCancelled, x -> {},\n                                                crypto.random.randomBytes(32), Optional.empty(),\n                                                Optional.of(Bat.random(crypto.random)), targetMirrorBat))\n                                        .thenCompose(p -> p.right.isEmpty() ?\n                                                Futures.of(p.left) :\n                                                target.addChildPointer(p.left, committer, p.right.get(), network, crypto))));\n            }\n        });\n    }\n\n\n    private CompletableFuture<String> pickUniqueCopyName(FileWrapper target,\n                                                         Snapshot version,\n                                                         Hasher hasher,\n                                                         NetworkAccess network) {\n        String originalName = getFileProperties().name;\n        int dot = originalName.lastIndexOf('.');\n        String base;\n        String ext;\n        if (dot > 0 && dot < originalName.length() - 1) {\n            base = originalName.substring(0, dot);\n            ext = originalName.substring(dot);\n        } else {\n            base = originalName;\n            ext = \"\";\n        }\n        return pickUniqueCopyName(target, version, hasher, network, base, ext, 0);\n    }\n\n    private CompletableFuture<String> pickUniqueCopyName(FileWrapper target,\n                                                         Snapshot version,\n                                                         Hasher hasher,\n                                                         NetworkAccess network,\n                                                         String base,\n                                                         String ext,\n                                                         int n) {\n        String candidate;\n        if (n == 0) {\n            candidate = base + ext;\n        } else if (n == 1) {\n            candidate = base + \" (copy)\" + ext;\n        } else {\n            candidate = base + \" (copy \" + n + \")\" + ext;\n        }\n        return target.hasChildWithName(version, candidate, hasher, network)\n                .thenCompose(exists -> exists ?\n                        pickUniqueCopyName(target, version, hasher, network, base, ext, n + 1) :\n                        Futures.of(candidate));\n    }\n\n    @JsMethod\n    public CompletableFuture<Boolean> hasChild(String fileName, Hasher hasher, NetworkAccess network) {\n        if (!isLegalName(fileName)) {\n            return Futures.errored(new IllegalArgumentException(\"Illegal file/directory name: \" + fileName));\n        }\n        return this.hasChildWithName(version, fileName, hasher, network);\n    }\n\n    public CompletableFuture<Boolean> hasChildren(NetworkAccess network) {\n        return hasChildren(version, network);\n    }\n\n    public CompletableFuture<Boolean> hasChildren(Snapshot version,\n                                                  NetworkAccess network) {\n        if (capTrie.isPresent())\n            return Futures.of(capTrie.get().isEmpty());\n        return pointer.fileAccess.getAllChildrenCapabilities(version, pointer.capability, network.hasher, network)\n                .thenApply(caps -> !caps.isEmpty());\n    }\n\n    /**\n     * Move this file/dir and subtree to a new signing key pair.\n     * @param signer\n     * @param parent\n     * @param network\n     * @return The updated version of this file/dir and its parent\n     */\n    public CompletableFuture<Pair<FileWrapper, FileWrapper>> changeSigningKey(SigningPrivateKeyAndPublicHash signer,\n                                                                              FileWrapper parent,\n                                                                              NetworkAccess network,\n                                                                              SafeRandom random,\n                                                                              Hasher hasher) {\n        ensureUnmodified();\n        WritableAbsoluteCapability cap = (WritableAbsoluteCapability)getPointer().capability;\n        SymmetricLinkToSigner signerLink = SymmetricLinkToSigner.fromPair(cap.wBaseKey.get(), signer);\n        CryptreeNode fileAccess = getPointer().fileAccess;\n\n        RelativeCapability newParentLink = new RelativeCapability(Optional.of(parent.writer()),\n                parent.getLocation().getMapKey(), parent.writableFilePointer().bat, parent.getParentKey(), Optional.empty());\n        CryptreeNode newFileAccess = fileAccess\n                .withWriterLink(cap.rBaseKey, signerLink)\n                .withParentLink(getParentKey(), newParentLink);\n        WritableAbsoluteCapability ourNewCap = cap.withSigner(signer.publicKeyHash);\n        RetrievedCapability newRetrievedCapability = new RetrievedCapability(ourNewCap, newFileAccess);\n\n        // create the new signing subspace move subtree to it\n        PublicKeyHash owner = owner();\n\n        network.synchronizer.putEmpty(owner, signer.publicKeyHash);\n        return network.synchronizer.applyComplexUpdate(owner, signer, (version, committer) -> IpfsTransaction.call(owner,\n                tid -> network.uploadChunk(version, committer, newFileAccess, owner, getPointer().capability.getMapKey(), signer, tid)\n                        .thenCompose(newVersion -> copyAllChunks(false, cap, signer, hasher, network, newVersion, committer))\n                        .thenCompose(copiedVersion -> copiedVersion.withWriter(owner, parent.writer(), network))\n                        .thenCompose(withParent -> parent.getPointer().fileAccess\n                                .updateChildLink(withParent, committer, parent.writableFilePointer(),\n                                        parent.signingPair(),\n                                        getPointer(),\n                                        newRetrievedCapability, network, random, hasher))\n                        .thenCompose(updatedParentVersion -> deleteAllChunks(cap, signingPair(), tid, hasher, network,\n                                updatedParentVersion, committer)),\n                network.dhtClient)\n        ).thenCompose(finalVersion -> parent.getUpdated(finalVersion, network)\n                .thenCompose(updatedParent -> network.getFile(finalVersion, ourNewCap, Optional.of(signer), ownername)\n                .thenApply(updatedUs -> new Pair<>(updatedUs.get(), updatedParent))));\n    }\n\n    /** This copies all the cryptree nodes from one signing key to another for a file or subtree\n     *\n     * @param includeFirst\n     * @param currentCap\n     * @param targetSigner\n     * @param network\n     * @return\n     */\n    private static CompletableFuture<Snapshot> copyAllChunks(boolean includeFirst,\n                                                             AbsoluteCapability currentCap,\n                                                             SigningPrivateKeyAndPublicHash targetSigner,\n                                                             Hasher hasher,\n                                                             NetworkAccess network,\n                                                             Snapshot initialVersion,\n                                                             Committer committer) {\n\n        return initialVersion.withWriter(currentCap.owner, currentCap.writer, network)\n                .thenCompose(version -> network.getMetadata(version.get(currentCap.writer), currentCap)\n                        .thenCompose(mOpt -> {\n                            if (! mOpt.isPresent()) {\n                                return CompletableFuture.completedFuture(version);\n                            }\n                            return (includeFirst ?\n                                    IpfsTransaction.call(currentCap.owner,\n                                            tid -> network.addPreexistingChunk(mOpt.get(), currentCap.owner, currentCap.getMapKey(),\n                                                    targetSigner, tid, version, committer), network.dhtClient) :\n                                    CompletableFuture.completedFuture(version))\n                                    .thenCompose(updated -> {\n                                        CryptreeNode chunk = mOpt.get();\n                                        Optional<byte[]> streamSecret = chunk.getProperties(chunk\n                                                .getParentKey(currentCap.rBaseKey)).streamSecret;\n                                        return chunk.getNextChunkLocation(currentCap.rBaseKey,\n                                                streamSecret, currentCap.getMapKey(), currentCap.bat, hasher)\n                                                .thenCompose(nextChunkMapKeyAndBat ->\n                                                        copyAllChunks(true, currentCap.withMapKey(nextChunkMapKeyAndBat.left, nextChunkMapKeyAndBat.right),\n                                                                targetSigner, hasher, network, updated, committer));\n                                    })\n                                    .thenCompose(updatedVersion -> {\n                                        if (! mOpt.get().isDirectory())\n                                            return CompletableFuture.completedFuture(updatedVersion);\n                                        return mOpt.get().getDirectChildrenCapabilities(currentCap, updatedVersion, network)\n                                                .thenCompose(childCaps ->\n                                                        Futures.reduceAll(childCaps,\n                                                                updatedVersion,\n                                                                (v, cap) -> copyAllChunks(true, cap.cap,\n                                                                        targetSigner, hasher, network, v, committer),\n                                                                (x, y) -> y));\n                                    });\n                        }));\n    }\n\n    public static CompletableFuture<Snapshot> deleteAllChunks(WritableAbsoluteCapability currentCap,\n                                                              SigningPrivateKeyAndPublicHash signer,\n                                                              TransactionId tid,\n                                                              Hasher hasher,\n                                                              NetworkAccess network,\n                                                              Snapshot version,\n                                                              Committer committer) {\n        return version.withWriter(currentCap.owner, currentCap.writer, network)\n                .thenCompose(current -> network.getMetadata(current.get(currentCap.writer), currentCap)\n                        .thenCompose(mOpt -> {\n                            if (! mOpt.isPresent()) {\n                                return CompletableFuture.completedFuture(current);\n                            }\n                            CryptreeNode chunk = mOpt.get();\n                            SigningPrivateKeyAndPublicHash ourSigner = chunk\n                                    .getSigner(currentCap.rBaseKey, currentCap.wBaseKey.get(), Optional.of(signer));\n                            FileProperties props = chunk.getProperties(chunk\n                                    .getParentKey(currentCap.rBaseKey));\n                            Optional<byte[]> streamSecret = props.streamSecret;\n\n                            boolean normalFile = ! chunk.isDirectory() && streamSecret.isPresent();\n                            if (normalFile)\n                                return deleteFileChunks(props.streamSecret.get(), props.chunkCount(), currentCap, ourSigner, tid, hasher, network, current, committer)\n                                        .thenCompose(s -> removeSigningKey(ourSigner, signer, currentCap.owner, network, s, committer));\n                            if (! chunk.isDirectory())\n                                // legacy file without stream secret\n                                return network.deleteChunk(current, committer, chunk, currentCap.owner,\n                                                        currentCap.getMapKey(), ourSigner, tid)\n                                                .thenCompose(deletedVersion -> chunk.getNextChunkLocation(currentCap.rBaseKey, streamSecret,\n                                                                currentCap.getMapKey(), currentCap.bat, hasher)\n                                                        .thenCompose(nextChunkMapKeyAndBat ->\n                                                                deleteAllChunks(currentCap.withMapKey(nextChunkMapKeyAndBat.left, nextChunkMapKeyAndBat.right), ourSigner, tid, hasher,\n                                                                        network, deletedVersion, committer)))\n                                        .thenCompose(s -> removeSigningKey(ourSigner, signer, currentCap.owner, network, s, committer));\n                            // Directory: bottom-up. Collect children from ALL chunks first so that\n                            // descendants are committed before the directory's own CHAMP entries.\n                            // Any partial commit then leaves only reachable entries in the CHAMP.\n                            return chunk.getAllChildrenCapabilities(current, currentCap, hasher, network)\n                                    .thenCompose(childCaps -> {\n                                        List<AbsoluteCapability> childCapList = childCaps.stream()\n                                                .map(c -> c.cap).collect(Collectors.toList());\n                                        return network.retrieveAllMetadata(childCapList, current)\n                                                .thenCompose(retrieved -> {\n                                                    Map<ByteArrayWrapper, RetrievedCapability> retrievedMap = new HashMap<>();\n                                                    for (RetrievedCapability rc : retrieved.left)\n                                                        retrievedMap.put(new ByteArrayWrapper(rc.capability.getMapKey()), rc);\n\n                                                    List<NamedAbsoluteCapability> otherCaps = new ArrayList<>();\n                                                    List<CompletableFuture<List<Pair<byte[], Optional<Bat>>>>> locationFutures = new ArrayList<>();\n                                                    Map<ByteArrayWrapper, MaybeMultihash> knownValues = new HashMap<>();\n\n                                                    for (NamedAbsoluteCapability namedCap : childCaps) {\n                                                        WritableAbsoluteCapability wcap = (WritableAbsoluteCapability) namedCap.cap;\n                                                        RetrievedCapability rc = retrievedMap.get(new ByteArrayWrapper(wcap.getMapKey()));\n                                                        if (rc != null) {\n                                                            FileProperties childProps = rc.getProperties();\n                                                            boolean isNormalFile = !rc.fileAccess.isDirectory() && childProps.streamSecret.isPresent();\n                                                            SigningPrivateKeyAndPublicHash childSigner = rc.fileAccess.getSigner(wcap.rBaseKey, wcap.wBaseKey.get(), Optional.of(ourSigner));\n                                                            if (isNormalFile && childSigner.publicKeyHash.equals(ourSigner.publicKeyHash)) {\n                                                                MaybeMultihash hash = rc.fileAccess.committedHash();\n                                                                if (hash.isPresent())\n                                                                    knownValues.put(new ByteArrayWrapper(wcap.getMapKey()), hash);\n                                                                locationFutures.add(getAllChunkLocations(wcap.getMapKey(), wcap.bat, childProps.streamSecret.get(), childProps.chunkCount(), hasher));\n                                                                continue;\n                                                            }\n                                                        }\n                                                        otherCaps.add(namedCap);\n                                                    }\n\n                                                    // 1. Cross-writer / dir children first\n                                                    return Futures.reduceAll(otherCaps, current,\n                                                                    (s, cap) -> deleteAllChunks((WritableAbsoluteCapability) cap.cap, ourSigner,\n                                                                            tid, hasher, network, s, committer),\n                                                                    (x, y) -> y)\n                                                            // 2. Same-writer batchable files second\n                                                            .thenCompose(v -> Futures.combineAllInOrder(locationFutures)\n                                                                    .thenCompose(allLocLists -> {\n                                                                        List<Pair<byte[], Optional<Bat>>> allLocs = allLocLists.stream()\n                                                                                .flatMap(List::stream)\n                                                                                .collect(Collectors.toList());\n                                                                        return allLocs.isEmpty() ? Futures.of(v) :\n                                                                                network.deleteAllChunksIfPresent(v, committer, currentCap.owner, ourSigner, allLocs, knownValues, tid);\n                                                                    }))\n                                                            // 3. Own chunk chain last\n                                                            .thenCompose(v -> deleteChunkChain(currentCap, ourSigner, chunk, streamSecret, tid, hasher, network, v, committer));\n                                                });\n                                    })\n                                    .thenCompose(s -> removeSigningKey(ourSigner, signer, currentCap.owner, network, s, committer));\n                        }));\n    }\n\n    private static CompletableFuture<Snapshot> deleteChunkChain(WritableAbsoluteCapability currentCap,\n                                                                SigningPrivateKeyAndPublicHash ourSigner,\n                                                                CryptreeNode chunk,\n                                                                Optional<byte[]> streamSecret,\n                                                                TransactionId tid,\n                                                                Hasher hasher,\n                                                                NetworkAccess network,\n                                                                Snapshot version,\n                                                                Committer committer) {\n        return network.deleteChunk(version, committer, chunk, currentCap.owner, currentCap.getMapKey(), ourSigner, tid)\n                .thenCompose(v -> chunk.getNextChunkLocation(currentCap.rBaseKey, streamSecret,\n                                currentCap.getMapKey(), currentCap.bat, hasher)\n                        .thenCompose(next -> {\n                            WritableAbsoluteCapability nextCap = currentCap.withMapKey(next.left, next.right);\n                            return v.withWriter(nextCap.owner, nextCap.writer, network)\n                                    .thenCompose(vs -> network.getMetadata(vs.get(nextCap.writer), nextCap)\n                                            .thenCompose(nextChunk -> nextChunk.isPresent() ?\n                                                    deleteChunkChain(nextCap, ourSigner, nextChunk.get(), streamSecret, tid, hasher, network, v, committer) :\n                                                    Futures.of(v)));\n                        }));\n    }\n\n    private static CompletableFuture<Snapshot> deleteFileChunks(byte[] streamSecret,\n                                                                int nChunks,\n                                                                WritableAbsoluteCapability startCap,\n                                                                SigningPrivateKeyAndPublicHash ourSigner,\n                                                                TransactionId tid,\n                                                                Hasher hasher,\n                                                                NetworkAccess network,\n                                                                Snapshot current,\n                                                                Committer c) {\n        return getAllChunkLocations(startCap.getMapKey(), startCap.bat, streamSecret, nChunks, hasher)\n                .thenCompose(labels -> network.deleteAllChunksIfPresent(current, c, startCap.owner, ourSigner, labels, tid));\n    }\n\n    private static CompletableFuture<List<Pair<byte[], Optional<Bat>>>> getAllChunkLocations(byte[] first,\n                                                                                             Optional<Bat> firstBat,\n                                                                                             byte[] streamSecret,\n                                                                                             int nChunks,\n                                                                                             Hasher h) {\n        List<Pair<byte[], Optional<Bat>>> res = new ArrayList<>(nChunks);\n        res.add(new Pair<>(first, firstBat));\n        return Futures.reduceAll(IntStream.range(1, nChunks).mapToObj(i -> i), res,\n                (labels, i) -> FileProperties.calculateNextMapKey(streamSecret,\n                                labels.get(labels.size() - 1).left, labels.get(labels.size() - 1).right, h)\n                        .thenApply(next -> {\n                            labels.add(next);\n                            return labels;\n                        }),\n                (a, b) -> b);\n    }\n\n    @JsMethod\n    public static CompletableFuture<FileWrapper> deleteChildren(FileWrapper parent,\n                                                                Collection<FileWrapper> childrenToDelete,\n                                                                Path parentPath,\n                                                                UserContext context) {\n        NetworkAccess network = context.network;\n        Hasher hasher = context.crypto.hasher;\n        parent.ensureUnmodified();\n        if (! parent.pointer.capability.isWritable())\n            return Futures.errored(new IllegalStateException(\"Cannot delete file without write access to it\"));\n\n        parent.setModified();\n        network.disableCommits();\n        PublicKeyHash owner = parent.owner();\n        SigningPrivateKeyAndPublicHash parentSigner = parent.signingPair();\n        // Partition children before the lambda so we can look up CHAMP values using the\n        // pre-removeChildren snapshot (v2). Plain files sharing the parent's writer can be\n        // batch-deleted; directories, links, and cross-writer files are handled per-child.\n        Map<Boolean, List<FileWrapper>> partitioned = childrenToDelete.stream()\n                .collect(Collectors.partitioningBy(f -> !f.isDirectory() && !f.isLink()\n                        && f.getFileProperties() != null\n                        && f.getFileProperties().streamSecret.isPresent()\n                        && f.writableFilePointer().writer.equals(parentSigner.publicKeyHash)));\n        List<FileWrapper> batchableFiles = partitioned.get(true);\n        List<FileWrapper> otherChildren = partitioned.get(false);\n        return network.synchronizer.applyComplexUpdate(owner, parentSigner,\n                (version, c) -> version.withWriter(owner, parent.writer(), network)\n                .thenCompose(v2 -> {\n                    // Look up committed CHAMP values for batchable files BEFORE removeChildren\n                    // changes the CHAMP root.  retrieveAllMetadata uses the CryptreeCache —\n                    // if the directory was recently displayed this costs 0 network calls (cache\n                    // hits).  The returned RetrievedCapability objects are proper Java objects\n                    // whose committedHash() is guaranteed to return the correct CID, avoiding a\n                    // second bulk getChampLookup round-trip inside deleteAllChunksIfPresent.\n                    // CIDs from R0 remain valid for CAS at R1/R2 because removeChildren only\n                    // updates directory chunk entries, not individual file entries.\n                    return network.retrieveAllMetadata(\n                                    batchableFiles.stream()\n                                            .map(f -> (AbsoluteCapability) f.writableFilePointer())\n                                            .collect(Collectors.toList()), v2)\n                            .thenCompose(retrieved -> {\n                                Map<ByteArrayWrapper, MaybeMultihash> knownValues = new HashMap<>();\n                                for (RetrievedCapability rc : retrieved.left) {\n                                    MaybeMultihash hash = rc.fileAccess.committedHash();\n                                    if (hash.isPresent())\n                                        knownValues.put(new ByteArrayWrapper(rc.capability.getMapKey()), hash);\n                                }\n                                // Pre-compute chunk locations (read-only key derivation, no network writes)\n                                CompletableFuture<List<Pair<byte[], Optional<Bat>>>> allLocsFuture =\n                                        Futures.combineAllInOrder(batchableFiles.stream()\n                                                .map(f -> {\n                                                    WritableAbsoluteCapability cap = f.writableFilePointer();\n                                                    FileProperties props = f.getFileProperties();\n                                                    return getAllChunkLocations(cap.getMapKey(), cap.bat,\n                                                            props.streamSecret.get(), props.chunkCount(), hasher);\n                                                })\n                                                .collect(Collectors.toList()))\n                                        .thenApply(allLocs -> allLocs.stream().flatMap(List::stream).collect(Collectors.toList()));\n                                // 1. Cross-writer / dir children first (puts W in pointerBuffer before P)\n                                return Futures.reduceAll(otherChildren, v2,\n                                                (s, f) -> deleteChild(owner, parent, parentPath, f, s, c, context),\n                                                (a, b) -> a.mergeAndOverwriteWith(b))\n                                        // 2. Same-writer batchable files second\n                                        .thenCompose(v3 -> allLocsFuture\n                                                .thenCompose(allKeys -> allKeys.isEmpty() ? Futures.of(v3) :\n                                                        IpfsTransaction.call(owner,\n                                                                tid -> network.deleteAllChunksIfPresent(v3, c, owner, parentSigner, allKeys, knownValues, tid),\n                                                                network.dhtClient)))\n                                        // 3. Parent listing update last\n                                        .thenCompose(v4 -> parent.pointer.fileAccess\n                                                .removeChildren(v4, c, childrenToDelete.stream()\n                                                                .map(f -> f.isLink() ? f.linkPointer.get().capability : f.getPointer().capability)\n                                                                .collect(Collectors.toList()),\n                                                        parent.writableFilePointer(),\n                                                        parent.entryWriter, network, context.crypto.random, hasher))\n                                        .thenCompose(v5 -> context.isSecretLink() ? Futures.of(v5) :\n                                                Futures.reduceAll(batchableFiles, v5,\n                                                        (s, f) -> context.sharedWithCache.clearSharedWith(parentPath.resolve(f.getName()), s, c, network),\n                                                        (a, b) -> a.mergeAndOverwriteWith(b)));\n                            });\n                }))\n                .thenCompose(s -> parent.getUpdated(s, network));\n    }\n\n    private static CompletableFuture<Snapshot> deleteChild(PublicKeyHash owner,\n                                                           FileWrapper parent,\n                                                           Path parentPath,\n                                                           FileWrapper child,\n                                                           Snapshot version,\n                                                           Committer c,\n                                                           UserContext context) {\n        Hasher hasher = context.crypto.hasher;\n        NetworkAccess network = context.network;\n        return IpfsTransaction.call(owner,\n                        tid -> FileWrapper.deleteAllChunks(\n                                child.isLink() ?\n                                        (WritableAbsoluteCapability) child.getLinkPointer().capability :\n                                        child.writableFilePointer(),\n                                parent.isWritable() ?\n                                        parent.signingPair() :\n                                        child.signingPair(), tid, hasher, network, version, c), network.dhtClient)\n                .thenCompose(s -> context.isSecretLink() ? Futures.of(s) :\n                        context.sharedWithCache.clearSharedWith(parentPath.resolve(child.getName()), s, c, network));\n    }\n\n    /**\n     * @param parent\n     * @param context\n     * @return updated parent\n     */\n    @JsMethod\n    public CompletableFuture<FileWrapper> remove(FileWrapper parent, Path ourPath, UserContext context) {\n        NetworkAccess network = context.network;\n        Hasher hasher = context.crypto.hasher;\n        ensureUnmodified();\n        if (! pointer.capability.isWritable())\n            return Futures.errored(new IllegalStateException(\"Cannot delete file without write access to it\"));\n\n        boolean writableParent = parent.isWritable();\n        parent.setModified();\n        network.disableCommits();\n        return network.synchronizer.applyComplexUpdate(owner(), signingPair(),\n                (v0, c) -> {\n                    return (context.isSecretLink() ?\n                            Futures.of(v0) :\n                            context.sharedWithCache.getAllDescendantShares(ourPath, v0)\n                                    .thenCompose(shares -> Futures.reduceAll(shares.entrySet().stream()\n                                                    .flatMap(swe -> swe.getValue().links().entrySet().stream()\n                                                            .flatMap(e -> e.getValue().stream().map(p -> new Pair<>(swe.getKey().resolve(e.getKey()), p)))), v0,\n                                            (v, linkp) -> context.deleteSecretLink(linkp.right.label, linkp.left, v, c),\n                                            (a, b) -> a.mergeAndOverwriteWith(b))))\n                            .thenCompose(v1 -> IpfsTransaction.call(owner(),\n                                            tid -> FileWrapper.deleteAllChunks(\n                                                    isLink() ?\n                                                            (WritableAbsoluteCapability) getLinkPointer().capability :\n                                                            writableFilePointer(),\n                                                    writableParent ?\n                                                            parent.signingPair() :\n                                                            signingPair(), tid, hasher, network, v1, c), network.dhtClient))\n                            .thenCompose(v2 -> (writableParent ? v2.withWriter(owner(), parent.writer(), network)\n                                    .thenCompose(v3 -> parent.pointer.fileAccess\n                                            .removeChildren(v3, c, Arrays.asList(isLink() ? linkPointer.get().capability : getPointer().capability), parent.writableFilePointer(),\n                                                    parent.entryWriter, network, context.crypto.random, hasher)) :\n                                    Futures.of(v2)))\n                            .thenCompose(s -> context.isSecretLink() ? Futures.of(s) :\n                                    context.sharedWithCache.clearSharedWith(ourPath, s, c, network));\n                })\n                .thenCompose(s -> parent.getUpdated(s, network));\n    }\n\n    public static CompletableFuture<Snapshot> removeSigningKey(SigningPrivateKeyAndPublicHash signerToRemove,\n                                                               SigningPrivateKeyAndPublicHash parentSigner,\n                                                               PublicKeyHash owner,\n                                                               NetworkAccess network,\n                                                               Snapshot current,\n                                                               Committer committer) {\n        PublicKeyHash parentWriter = parentSigner.publicKeyHash;\n        if (parentWriter.equals(signerToRemove.publicKeyHash))\n            return CompletableFuture.completedFuture(current);\n        CommittedWriterData toRemove = current.get(signerToRemove.publicKeyHash);\n\n        return current.withWriter(owner, parentWriter, network)\n                .thenCompose(s -> s.get(parentSigner).props.get()\n                        .removeOwnedKey(owner, parentSigner, signerToRemove.publicKeyHash, network.dhtClient, network.hasher)\n                        .thenCompose(removed -> IpfsTransaction.call(\n                                owner,\n                                tid -> committer.commit(owner, parentSigner, removed, s.get(parentSigner), tid),\n                                network.dhtClient))\n                        .thenApply(committed -> s.withVersion(parentWriter, committed.get(parentWriter))))\n                .thenCompose(s -> IpfsTransaction.call(owner,\n                        tid -> committer.commit(owner, signerToRemove, Optional.empty(), toRemove, tid), network.dhtClient)\n                        .thenApply(s::mergeAndOverwriteWith));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   ProgressConsumer<Long> monitor) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(state -> getInputStream(state.get(writer()), network, crypto, getFileProperties().size, 1, monitor));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(CommittedWriterData version,\n                                                                   NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   ProgressConsumer<Long> monitor) {\n        return getInputStream(version, network, crypto, getFileProperties().size, 1, monitor);\n    }\n\n    @JsMethod\n    public CompletableFuture<? extends AsyncReader> getBufferedInputStream(NetworkAccess network,\n                                                                           Crypto crypto,\n                                                                           int fileSizeHi,\n                                                                           int fileSizeLow,\n                                                                           int nBufferedChunks,\n                                                                           ProgressConsumer<Long> monitor) {\n        return getInputStream(network, crypto, fileSize(fileSizeHi, fileSizeLow), nBufferedChunks, monitor);\n    }\n\n    private static long fileSize(int fileSizeHi, int fileSizeLow) {\n        return (fileSizeLow & 0xFFFFFFFFL) + ((fileSizeHi & 0xFFFFFFFFL) << 32);\n    }\n\n    @JsMethod\n    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   int fileSizeHi,\n                                                                   int fileSizeLow,\n                                                                   ProgressConsumer<Long> monitor) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(state -> getInputStream(state.get(writer()), network, crypto, fileSize(fileSizeHi, fileSizeLow), 1, monitor));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   long fileSize,\n                                                                   ProgressConsumer<Long> monitor) {\n        return getInputStream(network, crypto, fileSize, 1, monitor);\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   long fileSize,\n                                                                   int nBufferedChunks,\n                                                                   ProgressConsumer<Long> monitor) {\n        return network.synchronizer.getValue(owner(), writer())\n                .thenCompose(state -> getInputStream(state.get(writer()), network, crypto, fileSize, nBufferedChunks, monitor));\n    }\n\n    public CompletableFuture<? extends AsyncReader> getInputStream(CommittedWriterData version,\n                                                                   NetworkAccess network,\n                                                                   Crypto crypto,\n                                                                   long fileSize,\n                                                                   int nBufferedChunks,\n                                                                   ProgressConsumer<Long> monitor) {\n        ensureUnmodified();\n        if (pointer.fileAccess.isDirectory())\n            throw new IllegalStateException(\"Cannot get input stream for a directory!\");\n        CryptreeNode fileAccess = pointer.fileAccess;\n        return fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, crypto.hasher)\n                .thenCompose(retriever ->\n                        retriever.getFile(version, network, crypto, pointer.capability, props.streamSecret,\n                                fileSize, fileAccess.committedHash(), nBufferedChunks, monitor));\n    }\n\n    private CompletableFuture<FileRetriever> getRetriever(Hasher hasher) {\n        if (pointer.fileAccess.isDirectory())\n            throw new IllegalStateException(\"Cannot get input stream for a directory!\");\n        return pointer.fileAccess.retriever(pointer.capability.rBaseKey, props.streamSecret, getLocation().getMapKey(), pointer.capability.bat, hasher);\n    }\n\n    @JsMethod\n    public String getBase64Thumbnail() {\n        Optional<Thumbnail> thumbnail = props.thumbnail;\n        if (thumbnail.isPresent()) {\n            Thumbnail thumb = thumbnail.get();\n            String base64Data = Base64.getEncoder().encodeToString(thumb.data);\n            if (thumb.mimeType.equals(\"image/webp\"))\n                return \"data:image/webp;base64,\" + base64Data;\n            if (thumb.mimeType.equals(\"image/jpeg\"))\n                return \"data:image/jpeg;base64,\" + base64Data;\n            if (thumb.mimeType.equals(\"image/png\"))\n                return \"data:image/png;base64,\" + base64Data;\n            throw new IllegalStateException(\"Unknown thumbnail mimetype: \" + thumb.mimeType);\n        } else {\n            return \"\";\n        }\n    }\n\n    @JsMethod\n    public FileProperties getFileProperties() {\n        ensureUnmodified();\n        return props;\n    }\n\n    @JsMethod\n    public String getName() {\n        return Optional.ofNullable(getFileProperties()).map(p -> p.name).orElse(\"/\");\n    }\n\n    public long getSize() {\n        return Optional.ofNullable(getFileProperties()).map(p -> p.size).orElse(0L);\n    }\n\n    public String toString() {\n        return getName();\n    }\n\n    public static FileWrapper createRoot(TrieNode root) {\n        return new FileWrapper(Optional.of(root), null, Optional.empty(), Optional.empty(), null, new Snapshot(new HashMap<>()));\n    }\n\n    public static Optional<Thumbnail> generateVideoThumbnail(byte[] videoBlob) {\n        File tempFile = null;\n        try {\n            tempFile = File.createTempFile(UUID.randomUUID().toString(), \".mp4\");\n            Files.write(tempFile.toPath(), videoBlob, StandardOpenOption.WRITE);\n            return VideoThumbnail.create(tempFile.getAbsolutePath(), THUMBNAIL_SIZE, THUMBNAIL_SIZE);\n        } catch (IOException ioe) {\n            LOG.log(Level.WARNING, ioe.getMessage(), ioe);\n        } finally {\n            if(tempFile != null) {\n                try {\n                    Files.delete(tempFile.toPath());\n                }catch(IOException ioe){\n\n                }\n            }\n        }\n        return Optional.empty();\n    }\n\n    private static Optional<Thumbnail> convertFromBase64(String base64Url) {\n        String base64data = base64Url.substring(base64Url.indexOf(\",\") + 1);\n        byte[] data = Base64.getDecoder().decode(base64data);\n        if (data.length == 0)\n            return Optional.empty();\n        if (base64Url.startsWith(\"data:image/jpeg;base64,\"))\n            return Optional.of(new Thumbnail(\"image/jpeg\", data));\n        if (base64Url.startsWith(\"data:image/webp;base64,\"))\n            return Optional.of(new Thumbnail(\"image/webp\", data));\n        throw new IllegalStateException(\"Unknown image type for generated thumbnail!\");\n    }\n\n    private CompletableFuture<Optional<Thumbnail>> generateThumbnail(NetworkAccess network, AsyncReader fileData, int fileSize, String filename, String mimeType) {\n        CompletableFuture<Optional<Thumbnail>> fut = new CompletableFuture<>();\n        if (fileSize > MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE) {\n            if (mimeType.startsWith(\"image\") && !mimeType.equals(\"image/svg+xml\")) {\n                if (network.isJavascript()) {\n                    thumbnail.generateThumbnail(fileData, fileSize, filename).thenAccept(base64Str -> {\n                        fut.complete(convertFromBase64(base64Str));\n                    });\n                } else {\n                    byte[] bytes = new byte[fileSize];\n                    fileData.readIntoArray(bytes, 0, fileSize).thenAccept(data -> {\n                        fut.complete(ThumbnailGenerator.get().generateThumbnail(bytes));\n                    }).exceptionally(t -> {\n                        fut.complete(Optional.empty());\n                        return null;\n                    });\n                }\n            } else if (mimeType.startsWith(\"video\")) {\n                if (network.isJavascript()) {\n                    thumbnail.generateVideoThumbnail(fileData, fileSize, filename, mimeType).thenAccept(base64Str -> {\n                        if(base64Str == null) {\n                            fut.complete(Optional.empty());\n                        }\n                        fut.complete(convertFromBase64(base64Str));\n                    });\n                } else {\n                    // TODO find a cross platform way to generate (streaming) video thumbnails in Java\n                    fut.complete(Optional.empty());\n//                    byte[] bytes = new byte[fileSize];\n//                    fileData.readIntoArray(bytes, 0, fileSize).thenAccept(data -> {\n//                        fut.complete(generateVideoThumbnail(bytes));\n//                    }).exceptionally(t -> {\n//                        fut.complete(Optional.empty());\n//                        return null;\n//                    });\n                }\n            } else if (mimeType.startsWith(\"audio/mpeg\")) {\n                byte[] mp3Data = new byte[fileSize];\n                fileData.readIntoArray(mp3Data, 0, fileSize).thenAccept(read -> {\n                    try {\n                        Mp3CoverImage mp3CoverImage = Mp3CoverImage.extractCoverArt(mp3Data);\n                        if (mp3CoverImage.imageData == null) {\n                            fut.complete(Optional.empty());\n                        } else {\n                            if (network.isJavascript()) {\n                                AsyncReader.ArrayBacked imageBlob = new AsyncReader.ArrayBacked(mp3CoverImage.imageData);\n                                thumbnail.generateThumbnail(imageBlob, mp3CoverImage.imageData.length, filename)\n                                        .thenAccept(base64Str -> {\n                                            fut.complete(convertFromBase64(base64Str));\n                                        });\n                            } else {\n                                fut.complete(ThumbnailGenerator.get().generateThumbnail(mp3CoverImage.imageData));\n                            }\n                        }\n                    } catch(Exception ex) {\n                        fut.complete(Optional.empty());\n                    }\n                });\n            } else {\n                fut.complete(Optional.empty());\n            }\n        } else {\n            fut.complete(Optional.empty());\n        }\n        return fut;\n    }\n\n    private static CompletableFuture<String> getFileType(AsyncReader imageBlob, String filename) {\n        CompletableFuture<String> result = new CompletableFuture<>();\n        byte[] data = new byte[MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE];\n        imageBlob.readIntoArray(data, 0, data.length).thenAccept(numBytesRead -> {\n            imageBlob.reset().thenAccept(resetResult -> {\n                if (numBytesRead < data.length) {\n                    result.complete(\"\");\n                } else {\n                    String mimeType = MimeTypes.calculateMimeType(data, filename);\n                    result.complete(mimeType);\n                }\n            });\n        });\n        return result;\n    }\n\n    public static CompletableFuture<String> calculateMimeType(AsyncReader data, long fileSize, String filename) {\n        byte[] header = new byte[(int) Math.min(fileSize, MimeTypes.HEADER_BYTES_TO_IDENTIFY_MIME_TYPE)];\n        return data.readIntoArray(header, 0, header.length)\n                .thenApply(read -> MimeTypes.calculateMimeType(header, filename));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/Fragment.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.storage.auth.*;\n\n/** A Fragment is a part of an EncryptedChunk which is stored directly in IPFS in a raw format block\n *\n */\npublic class Fragment {\n    // max message size allowed by bitswap protocol is 2 MiB, and the block must fit within that\n    public static final int MAX_LENGTH = 1024*1024;\n    public static final int MAX_LENGTH_WITH_BAT_PREFIX = MAX_LENGTH + Bat.MAX_RAW_BLOCK_PREFIX_SIZE;\n\n    public final byte[] data;\n\n    public Fragment(byte[] data) {\n        if (MAX_LENGTH_WITH_BAT_PREFIX < data.length)\n            throw new IllegalStateException(\"fragment size \"+ data.length +\" greater than max \"+ MAX_LENGTH);\n        this.data = data;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/FragmentWithHash.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.io.ipfs.Cid;\n\nimport java.util.*;\n\npublic class FragmentWithHash {\n    public final Fragment fragment;\n    public final Optional<Cid> hash;\n\n    public FragmentWithHash(Fragment fragment, Optional<Cid> hash) {\n        this.fragment = fragment;\n        this.hash = hash;\n    }\n\n    public boolean isInlined() {\n        return hash.isEmpty();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/Fragmenter.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.JsType;\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/** A Fragmenter decides how the fragments of a chunk are created.\n *\n *  The default implementation, SplitFragmenter simply splits the chunk into 128 KiB fragments\n *\n *  The ErasureFragmenter uses a Reed-Solomon erasure code to also generate more fragments according to the parameters.\n *\n */\n@JsType\npublic interface Fragmenter extends Cborable {\n\n    /** The amount of extra space required by this fragmenter compared to the original file\n     *\n     * @return\n     */\n    double storageIncreaseFactor();\n\n    byte[][] split(byte[] input);\n\n    byte[] recombine(byte[][] encoded, int startOffset, int inputLength);\n\n    @SuppressWarnings(\"unusable-by-js\")\n    static Fragmenter fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for Fragmenter: \" + cbor);\n\n        CborObject.CborMap map = (CborObject.CborMap) cbor;\n        long t = map.getLong(\"t\");\n        Type type = Type.ofVal((int) t);\n        if (type == Type.SIMPLE)\n            return new SplitFragmenter();\n        int originalFragments = (int)(map.getLong(\"o\"));\n        int allowedFailures = (int)(map.getLong(\"a\"));\n        return new ErasureFragmenter(originalFragments, allowedFailures);\n    }\n\n    enum Type  {\n        SIMPLE(0),\n        ERASURE_CODING(1);\n\n        public final int val;\n\n        Type(int val) {\n            this.val = val;\n        }\n\n        private static Map<Integer, Type> MAP = Stream.of(values())\n                .collect(Collectors.toMap(\n                                e -> e.val,\n                                e -> e));\n\n        public static Type ofVal(int val) {\n            Type type = MAP.get(val);\n            if (type == null)\n                throw new IllegalStateException(\"No Fragmenter type for value \"+ val);\n            return type;\n        }\n    }\n\n    static Fragmenter getInstance() {\n        return new SplitFragmenter();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/HashBranch.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\n\nimport java.util.*;\n\n/** A branch of a hash tree using sha256\n *\n */\npublic class HashBranch implements Cborable {\n\n    public final RootHash rootHash;\n    public final Optional<ChunkHashList> level1; // This is present on the first chunk of every 1024 chunks (5 GiB with 5 MiB chunks)\n    public final Optional<ChunkHashList> level2; // This is present on the first chunk of every 1024*1024 chunks (5 TiB with 5 MiB chunks)\n    public final Optional<ChunkHashList> level3; // This is present on the first chunk of every 1024*1024*1024 chunks (5 PiB with 5 MiB chunks)\n\n    public HashBranch(RootHash rootHash, Optional<ChunkHashList> level1, Optional<ChunkHashList> level2, Optional<ChunkHashList> level3) {\n        if (level2.isPresent() && level1.isEmpty())\n            throw new IllegalArgumentException(\"Invalid chunk hash tree state!\");\n        if (level3.isPresent() && level2.isEmpty())\n            throw new IllegalArgumentException(\"Invalid chunk hash tree state!\");\n        this.rootHash = rootHash;\n        this.level1 = level1;\n        this.level2 = level2;\n        this.level3 = level3;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"r\", rootHash.toCbor());\n        level1.ifPresent(b -> state.put(\"l1\", b.toCbor()));\n        level2.ifPresent(b -> state.put(\"l2\", b.toCbor()));\n        level3.ifPresent(b -> state.put(\"l3\", b.toCbor()));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static HashBranch fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for HashBranch! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        RootHash hash = m.get(\"r\", RootHash::fromCbor);\n        Optional<ChunkHashList> level1 = m.getOptional(\"l1\", ChunkHashList::fromCbor);\n        Optional<ChunkHashList> level2 = m.getOptional(\"l2\", ChunkHashList::fromCbor);\n        Optional<ChunkHashList> level3 = m.getOptional(\"l3\", ChunkHashList::fromCbor);\n        return new HashBranch(hash, level1, level2, level3);\n    }\n\n    @Override\n    public String toString() {\n        return rootHash.toString();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        HashBranch that = (HashBranch) o;\n        return Objects.equals(rootHash, that.rootHash) && Objects.equals(level1, that.level1) && Objects.equals(level2, that.level2) && Objects.equals(level3, that.level3);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(rootHash, level1, level2, level3);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/HashTree.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.hash.Hasher;\nimport peergos.shared.util.Futures;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.LongStream;\nimport java.util.stream.Stream;\n\npublic class HashTree implements Cborable {\n\n    public final RootHash rootHash;\n    public final List<ChunkHashList> level1;\n    public final List<ChunkHashList> level2;\n    public final List<ChunkHashList> level3;\n\n    public HashTree(RootHash rootHash, List<ChunkHashList> level1, List<ChunkHashList> level2, List<ChunkHashList> level3) {\n        this.rootHash = rootHash;\n        this.level1 = level1;\n        this.level2 = level2;\n        this.level3 = level3;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"r\", rootHash.toCbor());\n        state.put(\"ll1\", new CborObject.CborList(level1));\n        state.put(\"ll2\", new CborObject.CborList(level2));\n        state.put(\"ll3\", new CborObject.CborList(level3));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static HashTree fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for HashTree! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        RootHash root = m.get(\"r\", RootHash::fromCbor);\n        List<ChunkHashList> level1 = m.getList(\"ll1\", ChunkHashList::fromCbor);\n        List<ChunkHashList> level2 = m.getList(\"ll2\", ChunkHashList::fromCbor);\n        List<ChunkHashList> level3 = m.getList(\"ll3\", ChunkHashList::fromCbor);\n        return new HashTree(root, level1, level2, level3);\n    }\n\n    public HashBranch branch(long chunkIndex) {\n        return new HashBranch(rootHash,\n                level1.stream().skip(chunkIndex / 1024).findFirst(),\n                level2.stream().skip(chunkIndex / 1024 / 1024).findFirst(),\n                level3.stream().skip(chunkIndex / 1024 / 1024 / 1024).findFirst());\n    }\n\n    @Override\n    public String toString() {\n        return rootHash.toString();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        HashTree hashTree = (HashTree) o;\n        return Objects.equals(rootHash, hashTree.rootHash) && Objects.equals(level1, hashTree.level1) && Objects.equals(level2, hashTree.level2) && Objects.equals(level3, hashTree.level3);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(rootHash, level1, level2, level3);\n    }\n\n    public static HashTree fromBranches(List<HashBranch> branches) {\n        List<ChunkHashList> level1 = branches.stream().flatMap(b -> b.level1.stream()).collect(Collectors.toList());\n        List<ChunkHashList> level2 = branches.stream().flatMap(b -> b.level2.stream()).collect(Collectors.toList());\n        List<ChunkHashList> level3 = branches.stream().flatMap(b -> b.level3.stream()).collect(Collectors.toList());\n        return new HashTree(branches.get(0).rootHash, level1, level2, level3);\n    }\n\n    private static CompletableFuture<byte[]> readChunk(AsyncReader f, byte[] buf, int offset, int remaining) {\n        if (remaining == 0)\n            return Futures.of(buf);\n        return f.readIntoArray(buf, offset, remaining)\n                .thenCompose(read -> read == remaining ?\n                        Futures.of(buf) :\n                        readChunk(f, buf, offset + read, remaining - read));\n    }\n\n    @JsMethod\n    public static CompletableFuture<HashTree> buildParallel(Function<Integer, AsyncReader> f,\n                                                            int sizeHi,\n                                                            int sizeLow,\n                                                            Hasher hasher,\n                                                            int parallelism) {\n        long size = ((long)sizeHi) << 32 | (sizeLow & 0xFFFFFFFFL);\n        long nChunks = size == 0 ? 1 : (size + Chunk.MAX_SIZE - 1) / Chunk.MAX_SIZE;\n        long chunksPerThread = (nChunks + parallelism - 1) / parallelism;\n        int actualParallelism = (int) Math.min((nChunks + chunksPerThread - 1)/chunksPerThread, Math.min(parallelism, nChunks));\n        long chunksInLastThread = nChunks - ((actualParallelism - 1) * chunksPerThread);\n        return Futures.combineAllInOrder(IntStream.range(0, actualParallelism).mapToObj(p -> {\n                    boolean lastThread = p == actualParallelism - 1;\n                    AsyncReader reader = f.apply(p);\n                    long nChunksForThread = lastThread ? chunksInLastThread : chunksPerThread;\n                    return Futures.reduceAll(\n                            LongStream.range(0, nChunksForThread).boxed(),\n                            new ArrayList<byte[]>(),\n                            (hashes, i) -> {\n                                long chunkIndex = p * chunksPerThread + i;\n                                long chunkStart = chunkIndex * Chunk.MAX_SIZE;\n                                long chunkEnd = Math.min(chunkStart + Chunk.MAX_SIZE, size);\n                                return hasher.sha256Section(reader, chunkStart, chunkEnd)\n                                        .thenApply(hash -> {\n                                            ArrayList<byte[]> next = new ArrayList<>(hashes);\n                                            next.add(hash);\n                                            return next;\n                                        });\n                            },\n                            (a, b) -> { ArrayList<byte[]> res = new ArrayList<>(a); res.addAll(b); return res; }\n                    );\n                }).collect(Collectors.toList()))\n                .thenApply(nested -> nested.stream()\n                        .flatMap(Collection::stream)\n                        .collect(Collectors.toList()))\n                .thenCompose(level1 -> build(level1, hasher));\n    }\n\n    @JsMethod\n    public static CompletableFuture<HashTree> build(AsyncReader f, int sizeHi, int sizeLow, Hasher hasher) {\n        long size = ((long)sizeHi) << 32 | (sizeLow & 0xFFFFFFFFL);\n        long nChunks = size == 0 ? 1 : (size + Chunk.MAX_SIZE - 1) / Chunk.MAX_SIZE;\n        byte[] chunk = new byte[(int) Math.min(Chunk.MAX_SIZE, size)];\n        return Futures.combineAllInOrder(LongStream.range(0, nChunks)\n                        .mapToObj(i -> {\n                            boolean lastOfMultiChunk = i == nChunks - 1 && nChunks > 1;\n                            long lastChunkSize = size % Chunk.MAX_SIZE;\n                            int remaining = lastOfMultiChunk ? (int) (lastChunkSize == 0 ? Chunk.MAX_SIZE : lastChunkSize) : chunk.length;\n                            return readChunk(f, lastOfMultiChunk ? new byte[remaining] : chunk, 0, remaining)\n                                    .thenCompose(data -> hasher.sha256(data));\n                        })\n                        .collect(Collectors.toList()))\n                .thenCompose(level1 -> build(level1, hasher));\n    }\n\n    public static CompletableFuture<HashTree> build(List<byte[]> chunkHashes,\n                                                    Hasher hasher) {\n        if (chunkHashes.isEmpty())\n            throw new IllegalStateException(\"A file cannot have no chunk hashes.\");\n        List<ChunkHashList> level1 = buildLevel(chunkHashes);\n        if (level1.size() == 1) {\n            return hasher.sha256(new CborObject.CborList(level1).serialize())\n                    .thenApply(RootHash::new)\n                    .thenApply(r -> new HashTree(r, level1, Collections.emptyList(), Collections.emptyList()));\n        }\n        return buildLevel(level1, hasher)\n                .thenCompose(level2 -> {\n                    if (level2.size() == 1) {\n                        return hasher.sha256(new CborObject.CborList(level2).serialize())\n                                .thenApply(RootHash::new)\n                                .thenApply(r -> new HashTree(r, level1, level2, Collections.emptyList()));\n                    }\n                    return buildLevel(level2, hasher)\n                            .thenCompose(level3 -> {\n                                if (level3.size() == 1) {\n                                    return hasher.sha256(new CborObject.CborList(level3).serialize())\n                                            .thenApply(RootHash::new)\n                                            .thenApply(r -> new HashTree(r, level1, level2, level3));\n                                }\n                                return buildLevel(level3, hasher)\n                                        .thenCompose(level4 -> {\n                                            if (level4.size() == 1) {\n                                                return hasher.sha256(new CborObject.CborList(level4).serialize())\n                                                        .thenApply(RootHash::new)\n                                                        .thenApply(r -> new HashTree(r, level1, level2, level3));\n                                            }\n                                            throw new IllegalStateException(\"Files bigger than 5 PiB are not supported in HashTree!\");\n                                        });\n                            });\n                });\n    }\n\n    private static List<ChunkHashList> buildLevel(List<byte[]> chunkHashes) {\n        List<ChunkHashList> level = new ArrayList<>();\n\n        for (int i=0; i < chunkHashes.size(); i += 1024) {\n            int nChunks = Math.min(1024, chunkHashes.size() - i);\n            byte[] chunkHashesBytes = new byte[nChunks * 32];\n            for (int c=0; c < nChunks; c++)\n                System.arraycopy(chunkHashes.get(i + c), 0, chunkHashesBytes, c * 32, 32);\n            ChunkHashList level1Section = new ChunkHashList(chunkHashesBytes);\n            level.add(level1Section);\n        }\n        return level;\n    }\n\n    private static CompletableFuture<List<ChunkHashList>> buildLevel(List<ChunkHashList> level, Hasher h) {\n        return Futures.combineAllInOrder(level.stream()\n                        .map(l1 -> h.sha256(l1.serialize()))\n                        .collect(Collectors.toList()))\n                .thenApply(HashTree::buildLevel);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/HashTreeBuilder.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.crypto.hash.Hasher;\n\nimport java.util.Arrays;\nimport java.util.concurrent.CompletableFuture;\n\npublic class HashTreeBuilder {\n\n    private final byte[][] chunkHashes;\n\n    public HashTreeBuilder(long filesize) {\n        this.chunkHashes = new byte[filesize == 0 ? 1 : ((int)((filesize + Chunk.MAX_SIZE - 1) / Chunk.MAX_SIZE))][];\n    }\n\n    public CompletableFuture<Boolean> setChunk(int chunkIndex, byte[] chunk, Hasher h) {\n        return h.sha256(chunk)\n                .thenApply(hash -> {\n                    chunkHashes[chunkIndex] = hash;\n                    return true;\n                });\n    }\n\n    public void setChunkHash(int chunkIndex, byte[] hash) {\n        chunkHashes[chunkIndex] = hash;\n    }\n\n    public CompletableFuture<HashTree> complete(Hasher h) {\n        for (int i=0; i < chunkHashes.length; i++)\n            if (chunkHashes[i] == null)\n                throw new IllegalStateException(\"Incomplete tree hash state!\");\n        return HashTree.build(Arrays.asList(chunkHashes), h);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/JSFileReader.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\n\nimport java.util.concurrent.*;\n\n@JsType(isNative = true, namespace = \"browserio\")\npublic class JSFileReader {\n\n    public native CompletableFuture<Boolean> seek(int high32, int low32);\n    /**\n     *\n     * @param res array to store data in\n     * @param offset initial index to store data in res\n     * @param length number of bytes to read\n     * @return number of bytes read\n     */\n    public native CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length);\n\n    /**\n     *  reset to original starting position\n     * @return\n     */\n    public native CompletableFuture<Boolean> reset();\n\n    /**\n     * Close and dispose of any resources\n     */\n    public native void close();\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/LazyInputStreamCombiner.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.cryptree.*;\nimport peergos.shared.util.*;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport java.util.stream.*;\n\npublic class LazyInputStreamCombiner implements AsyncReader {\n    private static final Logger LOG = Logger.getLogger(AsyncReader.class.getName());\n    public static void disableLog() {\n        LOG.setLevel(Level.OFF);\n    }\n    private final CommittedWriterData version;\n    private final NetworkAccess network;\n    private final Crypto crypto;\n    private final SymmetricKey baseKey;\n    private final ProgressConsumer<Long> monitor;\n    private final long totalLength;\n    private final long totalChunks;\n\n    private final byte[] originalChunk;\n    private final byte[] originalChunkLocation;\n    private final Optional<Bat> originalChunkBat;\n    private final Optional<byte[]> streamSecret;\n    private final AbsoluteCapability originalNextPointer;\n\n    private final Map<Long, Pair<byte[], AbsoluteCapability>> bufferedChunks = new ConcurrentHashMap<>(); // and next chunk pointer\n    private final Map<Long, CompletableFuture<Pair<byte[], AbsoluteCapability>>> inProgress = new ConcurrentHashMap<>();\n    private final int nBufferedChunks;\n    private long globalIndex; // index of beginning of current chunk in file\n    private byte[] currentChunk;\n    private AbsoluteCapability currentNextChunkPointer;\n    private int index; // index within current chunk\n\n    public LazyInputStreamCombiner(CommittedWriterData version,\n                                   long globalIndex,\n                                   byte[] chunk,\n                                   Location nextChunkLoc,\n                                   Optional<Bat> nextChunkBat,\n                                   byte[] originalChunk,\n                                   byte[] originalChunkLocation,\n                                   Optional<Bat> originalChunkBat,\n                                   Optional<byte[]> streamSecret,\n                                   Location originalNextChunkPointer,\n                                   Optional<Bat> originalNextChunkBat,\n                                   NetworkAccess network,\n                                   Crypto crypto,\n                                   SymmetricKey baseKey,\n                                   long totalLength,\n                                   int nBufferedChunks,\n                                   ProgressConsumer<Long> monitor) {\n        if (chunk == null)\n            throw new IllegalStateException(\"Null initial chunk!\");\n        this.version = version;\n        this.network = network;\n        this.crypto = crypto;\n        this.baseKey = baseKey;\n        this.monitor = monitor;\n        this.totalLength = totalLength;\n        this.totalChunks = (totalLength + Chunk.MAX_SIZE - 1) / Chunk.MAX_SIZE;\n        this.originalChunk = originalChunk;\n        this.originalChunkLocation = originalChunkLocation;\n        this.originalChunkBat = originalChunkBat;\n        this.streamSecret = streamSecret;\n        this.originalNextPointer = AbsoluteCapability.build(originalNextChunkPointer, originalNextChunkBat, baseKey);\n        this.globalIndex = globalIndex;\n        this.currentChunk = chunk;\n        this.currentNextChunkPointer = AbsoluteCapability.build(nextChunkLoc, nextChunkBat, baseKey);\n        bufferedChunks.put(globalIndex, new Pair<>(chunk, this.currentNextChunkPointer));\n        this.index = 0;\n        this.nBufferedChunks = nBufferedChunks;\n    }\n\n    private void prefetch(int nChunks) {\n        ForkJoinPool.commonPool().execute(() -> syncPrefetch(nChunks));\n    }\n\n    private void syncPrefetch(int nChunks) {\n        if (streamSecret.isEmpty()) // can only parallelise download in non legacy files\n            return;\n\n        long globalIndexCopy = globalIndex;\n        if (globalIndexCopy + Chunk.MAX_SIZE > totalLength)\n            return;\n\n        long lastBufferedChunkInSequence = globalIndexCopy;\n        for (int i=1; i <= nChunks; i++) {\n            if (! bufferedChunks.containsKey(lastBufferedChunkInSequence + i * Chunk.MAX_SIZE)) {\n                lastBufferedChunkInSequence = lastBufferedChunkInSequence + (i-1) * Chunk.MAX_SIZE;\n                break;\n            }\n        }\n        if (lastBufferedChunkInSequence + nChunks * Chunk.MAX_SIZE >= totalLength)\n            nChunks = (int) ((totalLength - lastBufferedChunkInSequence - 1) / Chunk.MAX_SIZE);\n        if (nChunks == 0)\n            return;\n\n        int finalCount = nChunks;\n        AbsoluteCapability nextChunkCap = bufferedChunks.get(lastBufferedChunkInSequence).right;\n\n        long finalBufferedChunk = lastBufferedChunkInSequence;\n//        LOG.info(\"Prefetching \" + finalCount + \" chunks, starting from chunk \" + (lastBufferedChunkInSequence / Chunk.MAX_SIZE + 1));\n        FileProperties.calculateSubsequentMapKeys(streamSecret.get(), nextChunkCap.getMapKey(), nextChunkCap.bat, finalCount - 1, crypto.hasher)\n                .thenAccept(mapKeys -> parallelChunksDownload(finalCount, finalBufferedChunk, mapKeys, nextChunkCap));\n    }\n\n    private void parallelChunksDownload(int finalCount,\n                                        long lastBufferedChunk,\n                                        List<Pair<byte[], Optional<Bat>>> mapKeys,\n                                        AbsoluteCapability nextChunkCap) {\n        for (int i=1; i < finalCount + 1; i++) {\n            long lastChunkLen = totalLength % Chunk.MAX_SIZE;\n            int size = lastBufferedChunk / Chunk.MAX_SIZE + i < totalChunks ? Chunk.MAX_SIZE : (int) (lastChunkLen == 0 ? Chunk.MAX_SIZE : lastChunkLen);\n            Pair<byte[], Optional<Bat>> mapKey = mapKeys.get(i - 1);\n            long chunkOffset = lastBufferedChunk + (i * Chunk.MAX_SIZE);\n            if (inProgress.containsKey(chunkOffset) || bufferedChunks.containsKey(chunkOffset))\n                continue;\n\n//            LOG.info(\"Submitting chunk download \" + (chunkOffset / Chunk.MAX_SIZE));\n            ForkJoinPool.commonPool().execute(() -> getChunk(nextChunkCap.withMapKey(mapKey.left, mapKey.right), chunkOffset, size));\n        }\n    }\n\n    private CompletableFuture<Pair<byte[], AbsoluteCapability>> getChunk(AbsoluteCapability cap, long chunkOffset, int len) {\n        Pair<byte[], AbsoluteCapability> existing = bufferedChunks.get(chunkOffset);\n        if (existing != null)\n            return Futures.of(existing);\n        CompletableFuture<Pair<byte[], AbsoluteCapability>> pending = inProgress.get(chunkOffset);\n        if (pending != null)\n            return pending;\n        inProgress.put(chunkOffset, new CompletableFuture<>());\n\n//        LOG.info(\"Downloading chunk \" + (chunkOffset / Chunk.MAX_SIZE));\n        return getSubsequentMetadata(cap, 0)\n                .thenCompose(access -> getChunk(access, cap.getMapKey(), cap.bat, len))\n                .thenApply(p -> {\n                    Pair<byte[], AbsoluteCapability> res = new Pair<>(p.left, p.right);\n                    bufferedChunks.put(chunkOffset, res);\n                    CompletableFuture<Pair<byte[], AbsoluteCapability>> fut = inProgress.remove(chunkOffset);\n                    if (fut != null)\n                        fut.complete(res);\n//                    LOG.info(\"Completed chunk \" + (chunkOffset / Chunk.MAX_SIZE));\n                    return res;\n                }).exceptionally(t -> {\n                    CompletableFuture<Pair<byte[], AbsoluteCapability>> fut = inProgress.remove(chunkOffset);\n                    if (fut != null)\n                        fut.completeExceptionally(t);\n                    throw new RuntimeException(t);\n                });\n    }\n\n    private CompletableFuture<Pair<byte[], AbsoluteCapability>> getChunk(CryptreeNode access, byte[] chunkLocation, Optional<Bat> bat, int truncateTo) {\n        if (access.isDirectory())\n                throw new IllegalStateException(\"File linked to a directory for its next chunk!\");\n        return access.retriever(baseKey, streamSecret, chunkLocation, bat, crypto.hasher)\n                .thenCompose(retriever -> {\n                    return access.getNextChunkLocation(baseKey, streamSecret, chunkLocation, bat, crypto.hasher)\n                            .thenCompose(mapKeyAndBat -> {\n                                AbsoluteCapability newNextChunkPointer = originalNextPointer.withMapKey(mapKeyAndBat.left, mapKeyAndBat.right);\n                                return retriever.getChunk(version, network, crypto, 0, truncateTo,\n                                                originalNextPointer.withMapKey(chunkLocation, bat), streamSecret, access.committedHash(), monitor)\n                                        .thenApply(x -> {\n                                            byte[] nextData = x.get().chunk.data();\n                                            return new Pair<>(nextData, newNextChunkPointer);\n                                        });\n                            });\n                });\n    }\n\n    private CompletableFuture<CryptreeNode> getSubsequentMetadata(AbsoluteCapability nextCap, long chunks) {\n        if (nextCap == null) {\n            CompletableFuture<CryptreeNode> err = new CompletableFuture<>();\n            err.completeExceptionally(new EOFException());\n            return err;\n        }\n\n        return network.getMetadata(version, nextCap)\n                .thenCompose(meta -> {\n                    if (!meta.isPresent()) {\n                        CompletableFuture<CryptreeNode> err = new CompletableFuture<>();\n                        err.completeExceptionally(new EOFException());\n                        return err;\n                    }\n                    return CompletableFuture.completedFuture(meta.get());\n                }).thenCompose(access -> {\n                    if (chunks == 0)\n                        return CompletableFuture.completedFuture(access);\n                    return access.getNextChunkLocation(baseKey, streamSecret, nextCap.getMapKey(), nextCap.bat, crypto.hasher)\n                            .thenCompose(mapKeyAndBat -> {\n                                AbsoluteCapability newNextCap = nextCap.withMapKey(mapKeyAndBat.left, mapKeyAndBat.right);\n                                return getSubsequentMetadata(newNextCap, chunks - 1);\n                            });\n                });\n    }\n\n    private CompletableFuture<AsyncReader> skip(long skip) {\n        long available = bytesReady();\n\n        if (skip <= available) {\n            index += (int) skip;\n            return CompletableFuture.completedFuture(this);\n        }\n\n        long toRead = Math.min(available, skip);\n\n        long toSkipAfterThisChunk = skip - toRead;\n            // skip through the cryptree nodes without downloading the data\n            long finalOffset = index + globalIndex + skip;\n            long finalInternalIndex = finalOffset % Chunk.MAX_SIZE;\n            long startOfTargetChunk = finalOffset - finalInternalIndex;\n            long chunksToSkip = toSkipAfterThisChunk / Chunk.MAX_SIZE;\n            int truncateTo = (int) Math.min(Chunk.MAX_SIZE, totalLength - startOfTargetChunk);\n            // short circuit for files in the new deterministic (but still secret) format\n            if (streamSecret.isPresent()) {\n                return FileProperties.calculateMapKey(streamSecret.get(), originalChunkLocation, originalChunkBat,\n                        finalOffset, crypto.hasher)\n                        .thenCompose(targetChunkLocation -> {\n                            AbsoluteCapability targetPointer = nextChunkPointer().withMapKey(targetChunkLocation.left, targetChunkLocation.right);\n                            return getSubsequentMetadata(targetPointer, 0)\n                                    .thenCompose(access -> getChunk(access, targetPointer.getMapKey(), targetPointer.bat, truncateTo))\n                                    .thenCompose(p -> {\n                                        updateState(0, startOfTargetChunk, p.left, p.right);\n                                        return skip(finalInternalIndex);});\n                        });\n            }\n            return getSubsequentMetadata(nextChunkPointer(), chunksToSkip)\n                    .thenCompose(access -> getChunk(access, nextChunkPointer().getMapKey(), nextChunkPointer().bat, truncateTo))\n                    .thenCompose(p -> {\n                        updateState(0, startOfTargetChunk, p.left, p.right);\n                        return skip(finalInternalIndex);\n                    });\n    }\n\n    @Override\n    public CompletableFuture<AsyncReader> seekJS(int hi32, int low32) {\n        long seek = ((long) (hi32) << 32) | (low32 & 0xFFFFFFFFL);\n\n        if (totalLength < seek)\n            throw new IllegalStateException(\"Cannot seek to position \"+ seek + \" in file of length \" + totalLength);\n        long globalOffset = globalIndex + index;\n        if (seek > globalOffset)\n            return skip(seek - globalOffset);\n        return reset().thenCompose(x -> ((LazyInputStreamCombiner)x).skip(seek));\n    }\n\n    private AbsoluteCapability nextChunkPointer() {\n        return bufferedChunks.get(globalIndex).right;\n    }\n\n    private synchronized int bytesReady() {\n        return currentChunk.length - index;\n    }\n\n    public void close() {}\n\n    private void resetBuffer() {\n        bufferedChunks.put(0L, new Pair<>(originalChunk, originalNextPointer));\n    }\n\n    public synchronized CompletableFuture<AsyncReader> reset() {\n        resetBuffer();\n        this.globalIndex = 0;\n        this.currentChunk = originalChunk;\n        this.currentNextChunkPointer = originalNextPointer;\n        this.index = 0;\n        return CompletableFuture.completedFuture(this);\n    }\n\n    /**\n     *\n     * @param res array to store data in\n     * @param offset initial index to store data in res\n     * @param length number of bytes to read\n     * @return number of bytes read\n     */\n    public CompletableFuture<Integer> readIntoArray(byte[] res, int offset, int length) {\n        int available = bytesReady();\n        int toRead = Math.min(available, length);\n        synchronized (this) {\n            System.arraycopy(currentChunk, index, res, offset, toRead);\n            index += toRead;\n        }\n        long globalOffset = globalIndex + index;\n\n        prefetch(Math.min(5, nBufferedChunks));\n\n        if (available >= length) // we are done\n            return CompletableFuture.completedFuture(length);\n        if (globalOffset > totalLength) {\n            CompletableFuture<Integer> err=  new CompletableFuture<>();\n            err.completeExceptionally(new EOFException());\n            return err;\n        }\n        int nextChunkSize = totalLength - globalOffset > Chunk.MAX_SIZE ?\n                Chunk.MAX_SIZE :\n                (int) (totalLength - globalOffset);\n        long nextChunk = globalIndex + Chunk.MAX_SIZE;\n        return getChunk(nextChunkPointer(), nextChunk, nextChunkSize).thenCompose(current -> {\n            index = 0;\n            globalIndex = nextChunk;\n            currentChunk = current.left;\n            currentNextChunkPointer = current.right;\n            ensureBufferWithinLimit();\n            return this.readIntoArray(res, offset + toRead, length - toRead).thenApply(bytesRead -> bytesRead + toRead);\n        });\n    }\n\n    private void ensureBufferWithinLimit() {\n        if (bufferedChunks.size() > nBufferedChunks) {\n            List<Long> sorted = bufferedChunks.keySet().stream()\n                    .sorted()\n                    .collect(Collectors.toList());\n            long first = sorted.get(0);\n            if (first < globalIndex)\n                bufferedChunks.remove(first);\n            else {\n                long last = sorted.get(sorted.size() - 1);\n                if (last > globalIndex)\n                    bufferedChunks.remove(last);\n            }\n        }\n    }\n\n    private synchronized void updateState(int index,\n                                          long globalIndex,\n                                          byte[] chunk,\n                                          AbsoluteCapability nextChunkPointer) {\n        this.index = index;\n        this.globalIndex = globalIndex;\n        this.currentChunk = chunk;\n        this.currentNextChunkPointer = nextChunkPointer;\n        bufferedChunks.put(globalIndex, new Pair<>(chunk, nextChunkPointer));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/LocatedChunk.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.*;\nimport peergos.shared.storage.auth.*;\n\nimport java.util.*;\n\npublic class LocatedChunk {\n    public final Location location;\n    public final Optional<Bat> bat;\n    public final MaybeMultihash existingHash;\n    public final Chunk chunk;\n\n    public LocatedChunk(Location location, Optional<Bat> bat, MaybeMultihash existingHash, Chunk chunk) {\n        this.location = location;\n        this.bat = bat;\n        this.existingHash = existingHash;\n        this.chunk = chunk;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/Location.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\npublic class Location implements Cborable {\n\n    @JsProperty\n    public final PublicKeyHash owner, writer;\n    private final byte[] mapKey;\n\n    public Location(PublicKeyHash owner, PublicKeyHash writer, byte[] mapKey) {\n        if (mapKey.length != RelativeCapability.MAP_KEY_LENGTH)\n            throw  new IllegalArgumentException(\"map key length \"+ mapKey.length +\" is not \"+ RelativeCapability.MAP_KEY_LENGTH);\n        this.owner = owner;\n        this.writer = writer;\n        this.mapKey = mapKey;\n    }\n\n    public CborObject toCbor() {\n        return new CborObject.CborList(Arrays.asList(\n                owner.toCbor(),\n                writer.toCbor(),\n                new CborObject.CborByteArray(mapKey)\n        ));\n    }\n\n    @JsMethod\n    public byte[] getMapKey() {\n        return Arrays.copyOf(mapKey, mapKey.length);\n    }\n\n    public String toString() {\n        return new ByteArrayWrapper(mapKey).toString();\n    }\n\n    public static Location fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborList))\n            throw new IllegalStateException(\"Incorrect cbor for Location: \" + cbor);\n        List<? extends Cborable> values = ((CborObject.CborList) cbor).value;\n        return new Location(\n                PublicKeyHash.fromCbor(values.get(0)),\n                PublicKeyHash.fromCbor(values.get(1)),\n                ((CborObject.CborByteArray) values.get(2)).value);\n    }\n\n    public Location withMapKey(byte[] newMapKey) {\n        return new Location(owner, writer, newMapKey);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Location location = (Location) o;\n\n        if (owner != null ? !owner.equals(location.owner) : location.owner != null) return false;\n        if (writer != null ? !writer.equals(location.writer) : location.writer != null) return false;\n        return Arrays.equals(mapKey, location.mapKey);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = owner != null ? owner.hashCode() : 0;\n        result = 31 * result + (writer != null ? writer.hashCode() : 0);\n        result = 31 * result + Arrays.hashCode(mapKey);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/MimeTypes.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.util.Pair;\n\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class MimeTypes {\n    final static int[] MID = new int[]{'M', 'T', 'h', 'd'};\n    final static int[] ID3 = new int[]{'I', 'D', '3'};\n    final static int[] MP3 = new int[]{0xff, 0xfb};\n    final static int[] MP3_2 = new int[]{0xff, 0xfa};\n    final static int[] RIFF = new int[]{'R', 'I', 'F', 'F'};\n    final static int[] WAV_2 = new int[]{'W', 'A', 'V', 'E'};\n    final static int[] FLAC = new int[]{'f', 'L', 'a', 'C'};\n    final static int[] OPUS = new int[]{'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};\n    final static int[] OGG_FLAC = new int[]{0x7f, 'F', 'L', 'A', 'C'};\n    final static int[] VORBIS = new int[]{1, 'v', 'o', 'r', 'b', 'i', 's'};\n    final static int[] SPEEX = new int[]{'S', 'p', 'e', 'e', 'x', 0x20, 0x20, 0x20};\n\n    final static int[] MP4 = new int[]{'f', 't', 'y', 'p'};\n    final static int[] ISO2 = new int[]{'i', 's', 'o', '2'};\n    final static int[] ISOM = new int[]{'i', 's', 'o', 'm'};\n    final static int[] DASH = new int[]{'d', 'a', 's', 'h'};\n    final static int[] MP41 = new int[]{'m', 'p', '4', '1'};\n    final static int[] MP42 = new int[]{'m', 'p', '4', '2'};\n    final static int[] M4V = new int[]{'M', '4', 'V', ' '};\n    final static int[] AVIF = new int[]{'a', 'v', 'i', 'f'};\n    final static int[] HEIC = new int[]{'h', 'e', 'i', 'c'};\n    final static int[] AVC1 = new int[]{'a', 'v', 'c', '1'};\n    final static int[] M4A = new int[]{'M', '4', 'A', ' '};\n    final static int[] QT = new int[]{'q', 't', ' ', ' '};\n    final static int[] QT2 = new int[]{'p', 'n', 'o', 't'};\n    final static int[] QT3 = new int[]{'m', 'o', 'o', 'v'};\n    final static int[] QT4 = new int[]{'m', 'd', 'a', 't'};\n    final static int[] THREEGP = new int[]{'3', 'g', 'p'};\n\n    final static int[] FLV = new int[]{'F', 'L', 'V'};\n    final static int[] FORM = new int[]{'F', 'O', 'R', 'M'};\n    final static int[] AIFF = new int[]{'A', 'I', 'F', 'F'};\n    final static int[] AVI = new int[]{'A', 'V', 'I', ' '};\n    final static int[] OGG = new int[]{'O', 'g', 'g', 'S', 0, 2};\n    final static int[] THEORA = new int[]{0x80, 't', 'h', 'e', 'o', 'r', 'a'};\n    final static int[] FISHEAD = new int[]{'f', 'i', 's', 'h', 'e', 'a', 'd', 0};\n    final static int[] OGM_VIDEO = new int[]{1, 'v', 'i', 'd', 'e', 'o', 0, 0, 0};\n    final static int[] WEBM = new int[]{'w', 'e', 'b', 'm'};\n    final static int[] MATROSKA_START = new int[]{0x1a, 0x45, 0xdf, 0xa3};\n\n    final static int[] ICO = new int[]{0, 0, 1, 0};\n    final static int[] CUR = new int[]{0, 0, 2, 0};\n    final static int[] BMP = new int[]{'B', 'M'};\n    final static int[] GIF = new int[]{'G', 'I', 'F'};\n    final static int[] JPEG = new int[]{255, 216};\n    final static int[] TIFF1 = new int[]{'I', 'I', 0x2A, 0};\n    final static int[] TIFF2 = new int[]{'M', 'M', 0, 0x2A};\n    final static int[] PNG = new int[]{137, 'P', 'N', 'G', 13, 10, 26, 10};\n    final static int[] WEBP = new int[]{'W', 'E', 'B', 'P'};\n    final static int[] JPEGXL = new int[]{0xff, 0x0a};\n    final static int[] JPEGXL2 = new int[]{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A};\n\n    final static int[] PDF = new int[]{0x25, 'P', 'D', 'F'};\n    final static int[] PS = new int[]{'%', '!', 'P', 'S', '-', 'A', 'd', 'o', 'b', 'e', '-'};\n    final static int[] ZIP = new int[]{'P', 'K', 3, 4};\n    final static int[] GZIP = new int[]{0x1f, 0x8b, 0x08};\n    final static int[] RAR = new int[]{'R', 'a', 'r', '!', 0x1a, 0x07};\n    final static int[] WASM = new int[]{0, 'a', 's', 'm'};\n\n    final static int[] ICS = new int[]{'B','E','G','I','N',':','V','C','A','L','E','N','D','A','R'};\n    final static int[] VCF = new int[]{'B','E','G','I','N',':','V','C','A','R','D'};\n    final static int[] XML = new int[]{'<','?','x','m','l'};\n    final static int[] SVG = new int[]{'<','s','v','g',' '};\n    final static int[] WOFF = new int[]{'w','O','F','F'};\n    final static int[] WOFF2 = new int[]{'w','O','F','2'};\n    final static int[] OTF = new int[]{'O','T','T', 'O'};\n    final static int[] TTF = new int[]{0, 1, 0, 0};\n\n    // mimetypes for files that are cbor list(mimetype int, map(data)), mimetypes < 24 use a single byte\n\n    private static final int CBOR_PEERGOS_TODO_INT = 10; //legacy\n    final static int[] CBOR_PEERGOS_TODO = new int[]{0x82 /* cbor list with 2 elements*/, CBOR_PEERGOS_TODO_INT};\n\n    public static final String PEERGOS_POST = \"application/vnd.peergos-post\";\n    public static final int CBOR_PEERGOS_POST_INT = 17;\n    final static int[] CBOR_PEERGOS_POST = new int[]{0x82 /* cbor list with 2 elements*/, CBOR_PEERGOS_POST_INT};\n\n    public static final String PEERGOS_IDENTITY = \"application/vnd.peergos-identity-proof\";\n    public static final int CBOR_PEERGOS_IDENTITY_PROOF_INT = 24;\n    final static int[] CBOR_PEERGOS_IDENTITY_PROOF = new int[]{0x82 /* cbor list with 2 elements*/, 0x18 /*single byte int*/, CBOR_PEERGOS_IDENTITY_PROOF_INT};\n\n    public static final String PEERGOS_EMAIL = \"application/vnd.peergos-email\";\n    public static final int CBOR_PEERGOS_EMAIL_INT = 18;\n    final static int[] CBOR_PEERGOS_EMAIL = new int[]{0x82 /* cbor list with 2 elements*/, CBOR_PEERGOS_EMAIL_INT};\n\n    final static int HEADER_BYTES_TO_IDENTIFY_MIME_TYPE = 40;\n\n    final static Map<String, String> TEXT_MIMETYPES = Stream.of(\n                    new Pair<>(\"md\", \"md\"),\n                    new Pair<>(\"csv\", \"csv\"),\n                    new Pair<>(\"edn\", \"x-clojure\"),\n                    new Pair<>(\"excalidraw\", \"application/vnd.excalidraw+json\"),\n                    new Pair<>(\"xml\", \"xml\"),\n                    new Pair<>(\"asp\", \"asp\"),\n                    new Pair<>(\"rt\", \"richtext\"),\n                    new Pair<>(\"rtf\", \"rtf\"),\n                    new Pair<>(\"rtx\", \"richtext\"),\n                    new Pair<>(\"java\", \"x-java-source\"),\n                    new Pair<>(\"mjs\", \"javascript\"),\n                    new Pair<>(\"gv\", \"vnd.graphviz\"),\n                    new Pair<>(\"f\", \"x-fortran\"),\n                    new Pair<>(\"s\", \"x-asm\"),\n                    new Pair<>(\"p\", \"x-pascal\"),\n                    new Pair<>(\"yaml\", \"yaml\"),\n                    new Pair<>(\"c\", \"x-c\")\n            ).collect(Collectors.toMap(p -> p.left, p -> p.right));\n\n    public static String calculateMimeType(byte[] start, String filename) {\n        if (equalArrays(start, BMP))\n            return \"image/bmp\";\n        if (equalArrays(start, GIF))\n            return \"image/gif\";\n        if (equalArrays(start, PNG)) {\n            if (filename.endsWith(\".ico\"))\n                return \"image/vnd.microsoft.icon\";\n            return \"image/png\";\n        }\n        if (equalArrays(start, JPEG))\n            return \"image/jpg\";\n        if (equalArrays(start, ICO))\n            return \"image/x-icon\";\n        if (equalArrays(start, CUR))\n            return \"image/x-icon\";\n        if (equalArrays(start, RIFF) && equalArrays(start, 8, WEBP))\n            return \"image/webp\";\n        if (equalArrays(start, JPEGXL) || equalArrays(start, JPEGXL2))\n            return \"image/jxl\";\n        // many browsers don't support tiff\n        if (equalArrays(start, TIFF1))\n            return \"image/tiff\";\n        if (equalArrays(start, TIFF2))\n            return \"image/tiff\";\n\n        if (equalArrays(start, 4, MP4)) {\n            if (equalArrays(start, 8, ISO2)\n                    || equalArrays(start, 8, ISOM)\n                    || equalArrays(start, 8, DASH)\n                    || equalArrays(start, 8, MP42)\n                    || equalArrays(start, 8, MP41)\n                    || equalArrays(start, 16, ISOM))\n                return \"video/mp4\";\n            if (equalArrays(start, 8, M4V))\n                return \"video/m4v\";\n            if (equalArrays(start, 8, AVIF))\n                return \"image/avif\";\n            if (equalArrays(start, 8, HEIC))\n                return \"image/heic\";\n            if (equalArrays(start, 8, M4A))\n                return \"audio/mp4\";\n            if (equalArrays(start, 8, AVC1))\n                return \"video/h264\";\n            if (equalArrays(start, 8, QT))\n                return \"video/quicktime\";\n            if (equalArrays(start, 8, THREEGP))\n                return \"video/3gpp\";\n        }\n        if (equalArrays(start, 4, QT2))\n                return \"video/quicktime\";\n        if (equalArrays(start, 4, QT3))\n                return \"video/quicktime\";\n        if (equalArrays(start, 4, QT4))\n                return \"video/quicktime\";\n        if (equalArrays(start, 24, WEBM))\n            return \"video/webm\";\n        if (equalArrays(start, OGG) &&\n                (equalArrays(start, 28, THEORA) ||\n                        equalArrays(start, 28, FISHEAD) ||\n                        equalArrays(start, 28, OGM_VIDEO)))\n            return \"video/ogg\";\n        if (equalArrays(start, MATROSKA_START))\n            return \"video/x-matroska\";\n        if (equalArrays(start, FLV))\n            return \"video/x-flv\";\n        if (equalArrays(start, 8, AVI))\n            return \"video/avi\";\n\n        if (equalArrays(start, MID))\n            return \"audio/midi\";\n        if (equalArrays(start, ID3))\n            return \"audio/mpeg\";\n        if (equalArrays(start, MP3))\n            return \"audio/mpeg\";\n        if (equalArrays(start, MP3_2))\n            return \"audio/mpeg\";\n        if (equalArrays(start, FLAC))\n            return \"audio/flac\";\n        if (equalArrays(start, OGG) &&\n                (equalArrays(start, 28, OPUS) ||\n                        equalArrays(start, 28, OGG_FLAC) ||\n                        equalArrays(start, 28, VORBIS) ||\n                        equalArrays(start, 28, SPEEX))) // not sure how to distinguish from ogg video easily\n            return \"audio/ogg\";\n        if (equalArrays(start, RIFF) && equalArrays(start, 8, WAV_2))\n            return \"audio/wav\";\n        if (equalArrays(start, FORM) && equalArrays(start, 8, AIFF))\n            return \"audio/aiff\";\n\n        if (equalArrays(start, PDF))\n            return \"application/pdf\";\n\n        if (equalArrays(start, PS))\n            return \"application/postscript\";\n\n        if (equalArrays(start, WASM))\n            return \"application/wasm\";\n\n        if (equalArrays(start, ZIP)) {\n            if (filename.endsWith(\".jar\"))\n                return \"application/java-archive\";\n            if (filename.endsWith(\".epub\"))\n                return \"application/epub+zip\";\n\n            if (filename.endsWith(\".pptx\"))\n                return \"application/vnd.openxmlformats-officedocument.presentationml.presentation\";\n            if (filename.endsWith(\".docx\"))\n                return \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\";\n            if (filename.endsWith(\".xlsx\"))\n                return \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\";\n\n            if (filename.endsWith(\".odt\"))\n                return \"application/vnd.oasis.opendocument.text\";\n            if (filename.endsWith(\".ods\"))\n                return \"application/vnd.oasis.opendocument.spreadsheet\";\n            if (filename.endsWith(\".odp\"))\n                return \"application/vnd.oasis.opendocument.presentation\";\n\n            if (filename.endsWith(\".apk\"))\n                return \"application/vnd.android.package-archive\";\n\n            return \"application/zip\";\n        }\n\n        if (equalArrays(start, GZIP))\n            return \"application/x-gzip\";\n\n        if (equalArrays(start, RAR))\n            return \"application/x-rar-compressed\";\n\n        if (equalArrays(start, WOFF))\n            return \"font/woff\";\n        if (equalArrays(start, WOFF2))\n            return \"font/woff2\";\n        if (equalArrays(start, OTF))\n            return \"font/otf\";\n        if (equalArrays(start, TTF))\n            return \"font/ttf\";\n\n        if (equalArrays(start, CBOR_PEERGOS_POST))\n            return PEERGOS_POST;\n        if (equalArrays(start, CBOR_PEERGOS_IDENTITY_PROOF))\n            return PEERGOS_IDENTITY;\n\n        if (validUtf8(start)) {\n            if (filename.endsWith(\".ics\") && equalArrays(start, ICS))\n                return \"text/calendar\";\n            if (filename.endsWith(\".vcf\") && equalArrays(start, VCF))\n                return \"text/vcard\";\n            if (filename.endsWith(\".html\"))\n                return \"text/html\";\n            if (filename.endsWith(\".css\"))\n                return \"text/css\";\n            if (filename.endsWith(\".js\"))\n                return \"text/javascript\";\n            if (filename.endsWith(\".svg\") && (equalArrays(start, XML) || equalArrays(start, SVG)))\n                return \"image/svg+xml\";\n            if (filename.endsWith(\".json\"))\n                return \"application/json\";\n            if (filename.contains(\".\")) {\n                String extension = filename.substring(filename.lastIndexOf(\".\") + 1);\n                if (TEXT_MIMETYPES.containsKey(extension))\n                    return \"text/\" + TEXT_MIMETYPES.get(extension);\n                if (extension.equals(\"c9r\") || extension.equals(\"c9s\") || extension.equals(\"bkup\") || extension.equals(\"cryptomator\"))\n                    return \"application/vnd.cryptomator.encrypted\";\n            }\n            try {\n                String prefix = new String(start).trim().toLowerCase();\n                if (prefix.contains(\"html>\") || prefix.contains(\"<html\"))\n                    return \"text/html\";\n            } catch (Exception e) {\n                // invalid utf8\n            }\n            return \"text/plain\";\n        }\n        return \"application/octet-stream\";\n    }\n\n    private static boolean isContinuationByte(byte b) {\n        return (b & 0xc0) == 0x80;\n    }\n\n    private static boolean validUtf8(byte[] data) {\n        // UTF-8 is 1-4 bytes, 1 byte chars are ascii\n        // number of leading 1 bits in first byte determine number of bytes\n        for (int i=0; i < data.length; i++) {\n            byte b = data[i];\n            if ((b & 0xff) < 0x80)\n                continue; // ASCII\n\n            // check rest of character\n            int len = Integer.numberOfLeadingZeros(~b << 24);\n            if (len > 4 || len < 2) // can't start with a continuation byte\n                return false;\n            if (i + len > data.length) {\n                for (int x = i + 1; x < data.length; x++)\n                    if (! isContinuationByte(data[x]))\n                        return false;\n                return true; // tolerate partial final chars as this is a prefix\n            }\n            for (int x = 1; x < len; x++)\n                if (! isContinuationByte(data[i + x]))\n                    return false;\n            int val;\n            if (len == 2) {\n                val = ((b & 0x1f) << 6) | (data[i + 1] & 0x3f);\n                if (val <= 0x7f)\n                    return false;\n            } else if (len == 3) {\n                val = ((b & 0xf) << 12) | ((data[i + 1] & 0x3f) << 6) | (data[i + 2] & 0x3f);\n                if (val <= 0x7ff)\n                    return false;\n            } else { // len == 4\n                val = ((b & 0x7) << 18) | ((data[i + 1] & 0x3f) << 12) | ((data[i + 2] & 0x3f) << 6) | (data[i + 3] & 0x3f);\n                if (val <= 0xffff)\n                    return false;\n            }\n\n            if (val > 0x10ffff)\n                return false;\n            if (val > 0xd800 && val <= 0xdfff)\n                return false;\n            i += len-1;\n        }\n        return true;\n    }\n\n    private static boolean equalArrays(byte[] a, int[] target) {\n        return equalArrays(a, 0, target);\n    }\n\n    private static boolean equalArrays(byte[] a, int aOffset, int[] target) {\n        if (a == null || target == null || aOffset + target.length > a.length){\n            return false;\n        }\n\n        for (int i=0; i < target.length; i++) {\n            if ((a[i + aOffset] & 0xff) != (target[i] & 0xff)) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/Mp3CoverImage.java",
    "content": "package peergos.shared.user.fs;\n\nimport java.io.*;\nimport java.nio.charset.*;\nimport java.util.*;\n\n/** This is derived from https://github.com/mpatric/mp3agic and maintains the original MIT license\n *\n */\npublic class Mp3CoverImage {\n\n    public final byte[] imageData;\n    public final String mimeType;\n\n    public Mp3CoverImage(byte[] imageData, String mimeType) {\n        this.imageData = imageData;\n        this.mimeType = mimeType;\n    }\n\n    public static Mp3CoverImage extractCoverArt(byte[] rawMp3) throws  NoSuchTagException, UnsupportedTagException, InvalidDataException {\n        byte[] bytes = Arrays.copyOfRange(rawMp3, 0, 10);\n\n        sanityCheckTag(bytes);\n        int fileStart = AbstractID3v2Tag.HEADER_LENGTH +\n                unpackSynchsafeInteger(\n                        bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET],\n                        bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET + 1],\n                        bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET + 2],\n                        bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET + 3]);\n\n        byte[] headers = Arrays.copyOfRange(rawMp3, 0, fileStart);\n        AbstractID3v2Tag tag = createTag(headers);\n        return new Mp3CoverImage(tag.getAlbumImage(), tag.getAlbumImageMimeType());\n    }\n\n    public static class NoSuchTagException extends Exception {\n        public NoSuchTagException(String msg) {super(msg);}\n        public NoSuchTagException() {super();}\n    }\n    public static class UnsupportedTagException extends Exception {\n        public UnsupportedTagException(String msg) {super(msg);}\n    }\n    public static class InvalidDataException extends Exception {\n        public InvalidDataException(String msg) {super(msg);}\n\n        public InvalidDataException(String msg, Throwable cause) {super(msg, cause);}\n    }\n\n    private static int unpackSynchsafeInteger(byte b1, byte b2, byte b3, byte b4) {\n        int value = ((byte) (b4 & 0x7f));\n        value += shiftByte((byte) (b3 & 0x7f), -7);\n        value += shiftByte((byte) (b2 & 0x7f), -14);\n        value += shiftByte((byte) (b1 & 0x7f), -21);\n        return value;\n    }\n\n    private static int unpackInteger(byte b1, byte b2, byte b3, byte b4) {\n        int value = b4 & 0xff;\n        value += shiftByte(b3, -8);\n        value += shiftByte(b2, -16);\n        value += shiftByte(b1, -24);\n        return value;\n    }\n\n    private static int shiftByte(byte c, int places) {\n        int i = c & 0xff;\n        if (places < 0) {\n            return i << -places;\n        } else if (places > 0) {\n            return i >> places;\n        }\n        return i;\n    }\n\n    private static AbstractID3v2Tag createTag(byte[] bytes) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n        sanityCheckTag(bytes);\n        int majorVersion = bytes[AbstractID3v2Tag.MAJOR_VERSION_OFFSET];\n        switch (majorVersion) {\n            case 2:\n                ID3v22Tag tag = new ID3v22Tag(bytes);\n                if (tag.getFrameSets().isEmpty()) {\n                    tag = new ID3v22Tag(bytes, true);\n                }\n                return tag;\n            case 3:\n                return new ID3v23Tag(bytes);\n            case 4:\n                return new ID3v24Tag(bytes);\n        }\n        throw new UnsupportedTagException(\"Tag version not supported\");\n    }\n\n    private static boolean checkBit(byte b, int bitPosition) {\n        return ((b & (0x01 << bitPosition)) != 0);\n    }\n\n    private static final String defaultCharsetName = \"ISO-8859-1\";\n\n    private static String byteBufferToStringIgnoringEncodingIssues(byte[] bytes, int offset, int length) {\n        try {\n            return byteBufferToString(bytes, offset, length, defaultCharsetName);\n        } catch (UnsupportedEncodingException e) {\n            return null;\n        }\n    }\n\n    private static String byteBufferToString(byte[] bytes, int offset, int length) throws UnsupportedEncodingException {\n        return byteBufferToString(bytes, offset, length, defaultCharsetName);\n    }\n\n    private static String byteBufferToString(byte[] bytes, int offset, int length, String charsetName) throws UnsupportedEncodingException {\n        if (length < 1) return \"\";\n        return new String(bytes, offset, length, charsetName);\n    }\n\n    private static void sanityCheckTag(byte[] bytes) throws NoSuchTagException, UnsupportedTagException {\n        if (bytes.length < AbstractID3v2Tag.HEADER_LENGTH) {\n            throw new NoSuchTagException(\"Buffer too short\");\n        }\n        if (!AbstractID3v2Tag.TAG.equals(byteBufferToStringIgnoringEncodingIssues(bytes, 0, AbstractID3v2Tag.TAG.length()))) {\n            throw new NoSuchTagException();\n        }\n        int majorVersion = bytes[AbstractID3v2Tag.MAJOR_VERSION_OFFSET];\n        if (majorVersion != 2 && majorVersion != 3 && majorVersion != 4) {\n            int minorVersion = bytes[AbstractID3v2Tag.MINOR_VERSION_OFFSET];\n            throw new UnsupportedTagException(\"Unsupported version 2.\" + majorVersion + \".\" + minorVersion);\n        }\n    }\n\n    private static int indexOfTerminator(byte[] bytes, int fromIndex, int terminatorLength) {\n        int marker = -1;\n        for (int i = fromIndex; i <= bytes.length - terminatorLength; i++) {\n            if ((i - fromIndex) % terminatorLength == 0) {\n                int matched;\n                for (matched = 0; matched < terminatorLength; matched++) {\n                    if (bytes[i + matched] != 0) break;\n                }\n                if (matched == terminatorLength) {\n                    marker = i;\n                    break;\n                }\n            }\n        }\n        return marker;\n    }\n\n    private static int indexOfTerminatorForEncoding(byte[] bytes, int fromIndex, int encoding) {\n        int terminatorLength = (encoding == EncodedText.TEXT_ENCODING_UTF_16 || encoding == EncodedText.TEXT_ENCODING_UTF_16BE) ? 2 : 1;\n        return indexOfTerminator(bytes, fromIndex, terminatorLength);\n    }\n\n    private static int sizeSynchronisationWouldSubtract(byte[] bytes) {\n        int count = 0;\n        for (int i = 0; i < bytes.length - 2; i++) {\n            if (bytes[i] == (byte) 0xff && bytes[i + 1] == 0 && ((bytes[i + 2] & (byte) 0xe0) == (byte) 0xe0 || bytes[i + 2] == 0)) {\n                count++;\n            }\n        }\n        if (bytes.length > 1 && bytes[bytes.length - 2] == (byte) 0xff && bytes[bytes.length - 1] == 0) count++;\n        return count;\n    }\n\n    private static byte[] synchroniseBuffer(byte[] bytes) {\n        // synchronisation is replacing instances of:\n        // 11111111 00000000 111xxxxx with 11111111 111xxxxx and\n        // 11111111 00000000 00000000 with 11111111 00000000\n        int count = sizeSynchronisationWouldSubtract(bytes);\n        if (count == 0) return bytes;\n        byte[] newBuffer = new byte[bytes.length - count];\n        int i = 0;\n        for (int j = 0; j < newBuffer.length - 1; j++) {\n            newBuffer[j] = bytes[i];\n            if (bytes[i] == (byte) 0xff && bytes[i + 1] == 0 && ((bytes[i + 2] & (byte) 0xe0) == (byte) 0xe0 || bytes[i + 2] == 0)) {\n                i++;\n            }\n            i++;\n        }\n        newBuffer[newBuffer.length - 1] = bytes[i];\n        return newBuffer;\n    }\n\n    private static class ID3v22Tag extends AbstractID3v2Tag {\n\n        public ID3v22Tag(byte[] buffer) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n            super(buffer);\n        }\n\n        public ID3v22Tag(byte[] buffer, boolean obseleteFormat) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n            super(buffer, obseleteFormat);\n        }\n\n        @Override\n        protected void unpackFlags(byte[] bytes) {\n            unsynchronisation = checkBit(bytes[FLAGS_OFFSET], UNSYNCHRONISATION_BIT);\n            compression = checkBit(bytes[FLAGS_OFFSET], COMPRESSION_BIT);\n        }\n    }\n\n    private static class ID3v23Tag extends AbstractID3v2Tag {\n\n        public ID3v23Tag(byte[] buffer) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n            super(buffer);\n        }\n\n        @Override\n        protected void unpackFlags(byte[] buffer) {\n            unsynchronisation = checkBit(buffer[FLAGS_OFFSET], UNSYNCHRONISATION_BIT);\n            extendedHeader = checkBit(buffer[FLAGS_OFFSET], EXTENDED_HEADER_BIT);\n            experimental = checkBit(buffer[FLAGS_OFFSET], EXPERIMENTAL_BIT);\n        }\n    }\n\n    private static class ID3v2Frame {\n\n        private static final int HEADER_LENGTH = 10;\n        private static final int ID_OFFSET = 0;\n        private static final int ID_LENGTH = 4;\n        private static final int DATA_LENGTH_OFFSET = 4;\n        private static final int FLAGS1_OFFSET = 8;\n        private static final int FLAGS2_OFFSET = 9;\n        private static final int PRESERVE_TAG_BIT = 6;\n        private static final int PRESERVE_FILE_BIT = 5;\n        private static final int READ_ONLY_BIT = 4;\n        private static final int GROUP_BIT = 6;\n        private static final int COMPRESSION_BIT = 3;\n        private static final int ENCRYPTION_BIT = 2;\n        private static final int UNSYNCHRONISATION_BIT = 1;\n        private static final int DATA_LENGTH_INDICATOR_BIT = 0;\n\n        protected String id;\n        protected int dataLength = 0;\n        protected byte[] data = null;\n        private boolean preserveTag = false;\n        private boolean preserveFile = false;\n        private boolean readOnly = false;\n        private boolean group = false;\n        private boolean compression = false;\n        private boolean encryption = false;\n        private boolean unsynchronisation = false;\n        private boolean dataLengthIndicator = false;\n\n        public ID3v2Frame(byte[] buffer, int offset) throws InvalidDataException {\n            unpackFrame(buffer, offset);\n        }\n\n        public ID3v2Frame(String id, byte[] data) {\n            this.id = id;\n            this.data = data;\n            dataLength = data.length;\n        }\n\n        protected final void unpackFrame(byte[] buffer, int offset) throws InvalidDataException {\n            int dataOffset = unpackHeader(buffer, offset);\n            sanityCheckUnpackedHeader();\n            data = Arrays.copyOfRange(buffer, dataOffset, dataOffset + dataLength);\n        }\n\n        protected int unpackHeader(byte[] buffer, int offset) {\n            id = byteBufferToStringIgnoringEncodingIssues(buffer, offset + ID_OFFSET, ID_LENGTH);\n            unpackDataLength(buffer, offset);\n            unpackFlags(buffer, offset);\n            return offset + HEADER_LENGTH;\n        }\n\n        protected void unpackDataLength(byte[] buffer, int offset) {\n            dataLength = unpackInteger(buffer[offset + DATA_LENGTH_OFFSET], buffer[offset + DATA_LENGTH_OFFSET + 1], buffer[offset + DATA_LENGTH_OFFSET + 2], buffer[offset + DATA_LENGTH_OFFSET + 3]);\n        }\n\n        private void unpackFlags(byte[] buffer, int offset) {\n            preserveTag = checkBit(buffer[offset + FLAGS1_OFFSET], PRESERVE_TAG_BIT);\n            preserveFile = checkBit(buffer[offset + FLAGS1_OFFSET], PRESERVE_FILE_BIT);\n            readOnly = checkBit(buffer[offset + FLAGS1_OFFSET], READ_ONLY_BIT);\n            group = checkBit(buffer[offset + FLAGS2_OFFSET], GROUP_BIT);\n            compression = checkBit(buffer[offset + FLAGS2_OFFSET], COMPRESSION_BIT);\n            encryption = checkBit(buffer[offset + FLAGS2_OFFSET], ENCRYPTION_BIT);\n            unsynchronisation = checkBit(buffer[offset + FLAGS2_OFFSET], UNSYNCHRONISATION_BIT);\n            dataLengthIndicator = checkBit(buffer[offset + FLAGS2_OFFSET], DATA_LENGTH_INDICATOR_BIT);\n        }\n\n        protected void sanityCheckUnpackedHeader() throws InvalidDataException {\n            for (int i = 0; i < id.length(); i++) {\n                if (!((id.charAt(i) >= 'A' && id.charAt(i) <= 'Z') || (id.charAt(i) >= '0' && id.charAt(i) <= '9'))) {\n                    throw new InvalidDataException(\"Not a valid frame - invalid tag \" + id);\n                }\n            }\n        }\n\n        public String getId() {\n            return id;\n        }\n\n        public int getLength() {\n            return dataLength + HEADER_LENGTH;\n        }\n\n        public byte[] getData() {\n            return data;\n        }\n    }\n\n    private static class ID3v24Tag extends AbstractID3v2Tag {\n\n        public ID3v24Tag(byte[] buffer) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n            super(buffer);\n        }\n\n        @Override\n        protected void unpackFlags(byte[] buffer) {\n            unsynchronisation = checkBit(buffer[FLAGS_OFFSET], UNSYNCHRONISATION_BIT);\n            extendedHeader = checkBit(buffer[FLAGS_OFFSET], EXTENDED_HEADER_BIT);\n            experimental = checkBit(buffer[FLAGS_OFFSET], EXPERIMENTAL_BIT);\n            footer = checkBit(buffer[FLAGS_OFFSET], FOOTER_BIT);\n        }\n\n        @Override\n        protected boolean useFrameUnsynchronisation() {\n            return unsynchronisation;\n        }\n    }\n\n    private static abstract class AbstractID3v2FrameData {\n\n        boolean unsynchronisation;\n\n        public AbstractID3v2FrameData(boolean unsynchronisation) {\n            this.unsynchronisation = unsynchronisation;\n        }\n\n        protected final void synchroniseAndUnpackFrameData(byte[] bytes) throws InvalidDataException {\n            if (unsynchronisation && sizeSynchronisationWouldSubtract(bytes) > 0) {\n                byte[] synchronisedBytes = synchroniseBuffer(bytes);\n                unpackFrameData(synchronisedBytes);\n            } else {\n                unpackFrameData(bytes);\n            }\n        }\n\n        protected abstract void unpackFrameData(byte[] bytes) throws InvalidDataException;\n    }\n\n    private static class EncodedText {\n\n        public static final byte TEXT_ENCODING_ISO_8859_1 = 0;\n        public static final byte TEXT_ENCODING_UTF_16 = 1;\n        public static final byte TEXT_ENCODING_UTF_16BE = 2;\n        public static final byte TEXT_ENCODING_UTF_8 = 3;\n\n        public static final String CHARSET_ISO_8859_1 = \"ISO-8859-1\";\n        public static final String CHARSET_UTF_16 = \"UTF-16LE\";\n        public static final String CHARSET_UTF_16BE = \"UTF-16BE\";\n        public static final String CHARSET_UTF_8 = \"UTF-8\";\n\n        private static final String[] characterSets = {\n                CHARSET_ISO_8859_1,\n                CHARSET_UTF_16,\n                CHARSET_UTF_16BE,\n                CHARSET_UTF_8\n        };\n\n        private static final byte[][] terminators = {\n                {0},\n                {0, 0},\n                {0, 0},\n                {0}\n        };\n\n        private byte[] value;\n        private byte textEncoding;\n\n        public EncodedText(byte textEncoding, byte[] value) {\n            // if encoding type 1 and big endian BOM is present, switch to big endian\n            if ((textEncoding == TEXT_ENCODING_UTF_16) &&\n                    (textEncodingForBytesFromBOM(value) == TEXT_ENCODING_UTF_16BE)) {\n                this.textEncoding = TEXT_ENCODING_UTF_16BE;\n            } else {\n                this.textEncoding = textEncoding;\n            }\n            this.value = value;\n            this.stripBomAndTerminator();\n        }\n\n        public EncodedText(byte textEncoding, String string) {\n            this.textEncoding = textEncoding;\n            value = stringToBytes(string, characterSetForTextEncoding(textEncoding));\n            this.stripBomAndTerminator();\n        }\n\n        private static byte textEncodingForBytesFromBOM(byte[] value) {\n            if (value.length >= 2 && value[0] == (byte) 0xff && value[1] == (byte) 0xfe) {\n                return TEXT_ENCODING_UTF_16;\n            } else if (value.length >= 2 && value[0] == (byte) 0xfe && value[1] == (byte) 0xff) {\n                return TEXT_ENCODING_UTF_16BE;\n            } else if (value.length >= 3 && (value[0] == (byte) 0xef && value[1] == (byte) 0xbb && value[2] == (byte) 0xbf)) {\n                return TEXT_ENCODING_UTF_8;\n            } else {\n                return TEXT_ENCODING_ISO_8859_1;\n            }\n        }\n\n        private static String characterSetForTextEncoding(byte textEncoding) {\n            try {\n                return characterSets[textEncoding];\n            } catch (ArrayIndexOutOfBoundsException e) {\n                throw new IllegalArgumentException(\"Invalid text encoding \" + textEncoding);\n            }\n        }\n\n        private void stripBomAndTerminator() {\n            int leadingCharsToRemove = 0;\n            if (value.length >= 2 && ((value[0] == (byte) 0xfe && value[1] == (byte) 0xff) || (value[0] == (byte) 0xff && value[1] == (byte) 0xfe))) {\n                leadingCharsToRemove = 2;\n            } else if (value.length >= 3 && (value[0] == (byte) 0xef && value[1] == (byte) 0xbb && value[2] == (byte) 0xbf)) {\n                leadingCharsToRemove = 3;\n            }\n            int trailingCharsToRemove = 0;\n            byte[] terminator = terminators[textEncoding];\n            if (value.length - leadingCharsToRemove >= terminator.length) {\n                boolean haveTerminator = true;\n                for (int i = 0; i < terminator.length; i++) {\n                    if (value[value.length - terminator.length + i] != terminator[i]) {\n                        haveTerminator = false;\n                        break;\n                    }\n                }\n                if (haveTerminator) trailingCharsToRemove = terminator.length;\n            }\n            if (leadingCharsToRemove + trailingCharsToRemove > 0) {\n                int newLength = value.length - leadingCharsToRemove - trailingCharsToRemove;\n                byte[] newValue = new byte[newLength];\n                if (newLength > 0) {\n                    System.arraycopy(value, leadingCharsToRemove, newValue, 0, newValue.length);\n                }\n                value = newValue;\n            }\n        }\n\n        public byte[] getTerminator() {\n            return terminators[textEncoding];\n        }\n\n        private static byte[] stringToBytes(String s, String characterSet) {\n            return s.getBytes(Charset.forName(characterSet));\n        }\n    }\n\n    private static class ID3v2PictureFrameData extends AbstractID3v2FrameData {\n\n        protected String mimeType;\n        protected byte pictureType;\n        protected EncodedText description;\n        protected byte[] imageData;\n\n        public ID3v2PictureFrameData(boolean unsynchronisation, byte[] bytes) throws InvalidDataException {\n            super(unsynchronisation);\n            synchroniseAndUnpackFrameData(bytes);\n        }\n\n        @Override\n        protected void unpackFrameData(byte[] bytes) throws InvalidDataException {\n            int marker = indexOfTerminator(bytes, 1, 1);\n            if (marker >= 0) {\n                try {\n                    mimeType = byteBufferToString(bytes, 1, marker - 1);\n                } catch (UnsupportedEncodingException e) {\n                    mimeType = \"image/unknown\";\n                }\n            } else {\n                mimeType = \"image/unknown\";\n            }\n            pictureType = bytes[marker + 1];\n            marker += 2;\n            int marker2 = indexOfTerminatorForEncoding(bytes, marker, bytes[0]);\n            if (marker2 >= 0) {\n                description = new EncodedText(bytes[0], Arrays.copyOfRange(bytes, marker, marker2));\n                marker2 += description.getTerminator().length;\n            } else {\n                description = new EncodedText(bytes[0], \"\");\n                marker2 = marker;\n            }\n            imageData = Arrays.copyOfRange(bytes, marker2, bytes.length);\n        }\n\n        public String getMimeType() {\n            return mimeType;\n        }\n\n        public EncodedText getDescription() {\n            return description;\n        }\n\n        public byte[] getImageData() {\n            return imageData;\n        }\n    }\n\n    private static class ID3v2FrameSet {\n\n        private String id;\n        private ArrayList<ID3v2Frame> frames;\n\n        public ID3v2FrameSet(String id) {\n            this.id = id;\n            frames = new ArrayList<>();\n        }\n\n        public void clear() {\n            frames.clear();\n        }\n\n        public void addFrame(ID3v2Frame frame) {\n            frames.add(frame);\n        }\n\n        public List<ID3v2Frame> getFrames() {\n            return frames;\n        }\n\n        @Override\n        public String toString() {\n            return this.id + \": \" + frames.size();\n        }\n    }\n\n    private static class ID3v2ObseletePictureFrameData extends ID3v2PictureFrameData {\n\n        public ID3v2ObseletePictureFrameData(boolean unsynchronisation, byte[] bytes) throws InvalidDataException {\n            super(unsynchronisation, bytes);\n        }\n\n        @Override\n        protected void unpackFrameData(byte[] bytes) throws InvalidDataException {\n            String filetype;\n            try {\n                filetype = byteBufferToString(bytes, 1, 3);\n            } catch (UnsupportedEncodingException e) {\n                filetype = \"unknown\";\n            }\n            mimeType = \"image/\" + filetype.toLowerCase();\n            pictureType = bytes[4];\n            int marker = indexOfTerminatorForEncoding(bytes, 5, bytes[0]);\n            if (marker >= 0) {\n                description = new EncodedText(bytes[0], Arrays.copyOfRange(bytes, 5, marker));\n                marker += description.getTerminator().length;\n            } else {\n                description = new EncodedText(bytes[0], \"\");\n                marker = 1;\n            }\n            imageData = Arrays.copyOfRange(bytes, marker, bytes.length);\n        }\n    }\n\n    private static class ID3v2ObseleteFrame extends ID3v2Frame {\n\n        private static final int HEADER_LENGTH = 6;\n        private static final int ID_OFFSET = 0;\n        private static final int ID_LENGTH = 3;\n        protected static final int DATA_LENGTH_OFFSET = 3;\n\n        public ID3v2ObseleteFrame(byte[] buffer, int offset) throws InvalidDataException {\n            super(buffer, offset);\n        }\n\n        @Override\n        protected int unpackHeader(byte[] buffer, int offset) {\n            id = byteBufferToStringIgnoringEncodingIssues(buffer, offset + ID_OFFSET, ID_LENGTH);\n            unpackDataLength(buffer, offset);\n            return offset + HEADER_LENGTH;\n        }\n\n        @Override\n        protected void unpackDataLength(byte[] buffer, int offset) {\n            dataLength = unpackInteger((byte) 0, buffer[offset + DATA_LENGTH_OFFSET], buffer[offset + DATA_LENGTH_OFFSET + 1], buffer[offset + DATA_LENGTH_OFFSET + 2]);\n        }\n\n        @Override\n        public int getLength() {\n            return dataLength + HEADER_LENGTH;\n        }\n    }\n\n    private static abstract class AbstractID3v2Tag {\n\n        public static final String ID_IMAGE = \"APIC\";\n        public static final String ID_IMAGE_OBSELETE = \"PIC\";\n\n        protected static final String TAG = \"ID3\";\n        protected static final String FOOTER_TAG = \"3DI\";\n        protected static final int HEADER_LENGTH = 10;\n        protected static final int FOOTER_LENGTH = 10;\n        protected static final int MAJOR_VERSION_OFFSET = 3;\n        protected static final int MINOR_VERSION_OFFSET = 4;\n        protected static final int FLAGS_OFFSET = 5;\n        protected static final int DATA_LENGTH_OFFSET = 6;\n        protected static final int FOOTER_BIT = 4;\n        protected static final int EXPERIMENTAL_BIT = 5;\n        protected static final int EXTENDED_HEADER_BIT = 6;\n        protected static final int COMPRESSION_BIT = 6;\n        protected static final int UNSYNCHRONISATION_BIT = 7;\n        protected static final int PADDING_LENGTH = 256;\n\n        protected boolean unsynchronisation = false;\n        protected boolean extendedHeader = false;\n        protected boolean experimental = false;\n        protected boolean footer = false;\n        protected boolean compression = false;\n        protected boolean padding = false;\n        protected String version = null;\n        private int dataLength = 0;\n        private int extendedHeaderLength;\n        private byte[] extendedHeaderData;\n        private boolean obseleteFormat = false;\n\n        private final Map<String, ID3v2FrameSet> frameSets;\n\n        public AbstractID3v2Tag(byte[] bytes) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n            this(bytes, false);\n        }\n\n        public AbstractID3v2Tag(byte[] bytes, boolean obseleteFormat) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n            frameSets = new TreeMap<>();\n            this.obseleteFormat = obseleteFormat;\n            unpackTag(bytes);\n        }\n\n        private void unpackTag(byte[] bytes) throws NoSuchTagException, UnsupportedTagException, InvalidDataException {\n            sanityCheckTag(bytes);\n            int offset = unpackHeader(bytes);\n            try {\n                if (extendedHeader) {\n                    offset = unpackExtendedHeader(bytes, offset);\n                }\n                int framesLength = dataLength;\n                if (footer) framesLength -= 10;\n                offset = unpackFrames(bytes, offset, framesLength);\n                if (footer) {\n                    offset = unpackFooter(bytes, dataLength);\n                }\n            } catch (ArrayIndexOutOfBoundsException e) {\n                throw new InvalidDataException(\"Premature end of tag\", e);\n            }\n        }\n\n        private int unpackHeader(byte[] bytes) throws UnsupportedTagException, InvalidDataException {\n            int majorVersion = bytes[MAJOR_VERSION_OFFSET];\n            int minorVersion = bytes[MINOR_VERSION_OFFSET];\n            version = majorVersion + \".\" + minorVersion;\n            if (majorVersion != 2 && majorVersion != 3 && majorVersion != 4) {\n                throw new UnsupportedTagException(\"Unsupported version \" + version);\n            }\n            unpackFlags(bytes);\n            if ((bytes[FLAGS_OFFSET] & 0x0F) != 0) throw new UnsupportedTagException(\"Unrecognised bits in header\");\n            dataLength = unpackSynchsafeInteger(bytes[DATA_LENGTH_OFFSET], bytes[DATA_LENGTH_OFFSET + 1], bytes[DATA_LENGTH_OFFSET + 2], bytes[DATA_LENGTH_OFFSET + 3]);\n            if (dataLength < 1) throw new InvalidDataException(\"Zero size tag\");\n            return HEADER_LENGTH;\n        }\n\n        protected abstract void unpackFlags(byte[] bytes);\n\n        private int unpackExtendedHeader(byte[] bytes, int offset) {\n            extendedHeaderLength = unpackSynchsafeInteger(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]) + 4;\n            extendedHeaderData = Arrays.copyOfRange(bytes, offset + 4, offset + 4 + extendedHeaderLength);\n            return extendedHeaderLength;\n        }\n\n        protected int unpackFrames(byte[] bytes, int offset, int framesLength) {\n            int currentOffset = offset;\n            while (currentOffset <= framesLength) {\n                ID3v2Frame frame;\n                try {\n                    frame = createFrame(bytes, currentOffset);\n                    addFrame(frame, false);\n                    currentOffset += frame.getLength();\n                } catch (InvalidDataException e) {\n                    break;\n                }\n            }\n            return currentOffset;\n        }\n\n        protected void addFrame(ID3v2Frame frame, boolean replace) {\n            ID3v2FrameSet frameSet = frameSets.get(frame.getId());\n            if (frameSet == null) {\n                frameSet = new ID3v2FrameSet(frame.getId());\n                frameSet.addFrame(frame);\n                frameSets.put(frame.getId(), frameSet);\n            } else if (replace) {\n                frameSet.clear();\n                frameSet.addFrame(frame);\n            } else {\n                frameSet.addFrame(frame);\n            }\n        }\n\n        protected ID3v2Frame createFrame(byte[] bytes, int currentOffset) throws InvalidDataException {\n            if (obseleteFormat) return new ID3v2ObseleteFrame(bytes, currentOffset);\n            return new ID3v2Frame(bytes, currentOffset);\n        }\n\n        private int unpackFooter(byte[] bytes, int offset) throws InvalidDataException {\n            if (!FOOTER_TAG.equals(byteBufferToStringIgnoringEncodingIssues(bytes, offset, FOOTER_TAG.length()))) {\n                throw new InvalidDataException(\"Invalid footer\");\n            }\n            return FOOTER_LENGTH;\n        }\n\n        protected boolean useFrameUnsynchronisation() {\n            return false;\n        }\n\n        public Map<String, ID3v2FrameSet> getFrameSets() {\n            return frameSets;\n        }\n\n        public byte[] getAlbumImage() {\n            ID3v2PictureFrameData frameData = createPictureFrameData(obseleteFormat ? ID_IMAGE_OBSELETE : ID_IMAGE);\n            if (frameData != null) return frameData.getImageData();\n            return null;\n        }\n\n        public String getAlbumImageMimeType() {\n            ID3v2PictureFrameData frameData = createPictureFrameData(obseleteFormat ? ID_IMAGE_OBSELETE : ID_IMAGE);\n            if (frameData != null && frameData.getMimeType() != null) return frameData.getMimeType();\n            return null;\n        }\n\n        private ID3v2PictureFrameData createPictureFrameData(String id) {\n            ID3v2FrameSet frameSet = frameSets.get(id);\n            if (frameSet != null) {\n                ID3v2Frame frame = frameSet.getFrames().get(0);\n                ID3v2PictureFrameData frameData;\n                try {\n                    if (obseleteFormat)\n                        frameData = new ID3v2ObseletePictureFrameData(useFrameUnsynchronisation(), frame.getData());\n                    else frameData = new ID3v2PictureFrameData(useFrameUnsynchronisation(), frame.getData());\n                    return frameData;\n                } catch (InvalidDataException e) {\n                    // do nothing\n                }\n            }\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/NamedAbsoluteCapability.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.cbor.*;\nimport peergos.shared.inode.*;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\n\npublic class NamedAbsoluteCapability implements Cborable {\n    public final PathElement name;\n    public final AbsoluteCapability cap;\n    public final Optional<Boolean> isDir;\n    public final Optional<String> mimetype;\n    public final Optional<LocalDateTime> created;\n\n    public NamedAbsoluteCapability(PathElement name,\n                                   AbsoluteCapability cap,\n                                   Optional<Boolean> isDir,\n                                   Optional<String> mimetype,\n                                   Optional<LocalDateTime> created) {\n        this.name = name;\n        this.cap = cap;\n        this.isDir = isDir;\n        this.mimetype = mimetype;\n        this.created = created;\n    }\n\n    public NamedAbsoluteCapability(String name,\n                                   AbsoluteCapability cap,\n                                   Optional<Boolean> isDir,\n                                   Optional<String> mimetype,\n                                   Optional<LocalDateTime> created) {\n        this(new PathElement(name), cap, isDir, mimetype, created);\n    }\n\n    @JsMethod\n    public boolean isWritable() {\n        return cap.isWritable();\n    }\n\n    @JsMethod\n    public boolean isDir() {\n        return isDir.orElse(false);\n    }\n\n    @JsMethod\n    public LocalDateTime created() {\n        return created.orElse(LocalDateTime.of(2025, 1, 1, 0, 0, 0));\n    }\n\n    @JsMethod\n    public String mimeType() {\n        return mimetype.orElse(\"application/octet-stream\");\n    }\n\n    private void addCbor(String key, CborObject val, CborObject.CborMap m) {\n        if (m.containsKey(key))\n            throw new IllegalStateException(\"Incompatible cbor\");\n        m.put(key, val);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        CborObject.CborMap cbor = cap.toCbor(); // This is to ensure binary compatibility for old code with new data\n        if (cbor.containsKey(\"n\"))\n            throw new IllegalStateException(\"Incompatible cbor\");\n        cbor.put(\"n\", new CborObject.CborString(name.name));\n        isDir.ifPresent(d -> addCbor(\"d\", new CborObject.CborBoolean(d), cbor));\n        mimetype.ifPresent(m -> addCbor(\"t\", new CborObject.CborString(m), cbor));\n        created.ifPresent(c -> addCbor(\"c\", new CborObject.CborLong(c.toEpochSecond(ZoneOffset.UTC)), cbor));\n        return cbor;\n    }\n\n    public static NamedAbsoluteCapability fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor\");\n        CborObject.CborMap map = ((CborObject.CborMap) cbor);\n\n        String name = map.getString(\"n\");\n        AbsoluteCapability cap = AbsoluteCapability.fromCbor(cbor);\n        Optional<Boolean> isDir = map.getOptional(\"d\", c -> ((CborObject.CborBoolean)c).value);\n        Optional<String> mimetype = map.getOptional(\"t\", c -> ((CborObject.CborString)c).value);\n        Optional<LocalDateTime> created = map.getOptional(\"c\", c -> LocalDateTime.ofEpochSecond(((CborObject.CborLong)c).value, 0, ZoneOffset.UTC));\n        return new NamedAbsoluteCapability(new PathElement(name), cap, isDir, mimetype, created);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        NamedAbsoluteCapability that = (NamedAbsoluteCapability) o;\n        return Objects.equals(name, that.name) &&\n                Objects.equals(cap, that.cap);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(name, cap);\n    }\n\n    @Override\n    public String toString() {\n        return name + \"/\" + cap;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/NamedRelativeCapability.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.cbor.*;\nimport peergos.shared.inode.*;\n\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.Optional;\n\npublic class NamedRelativeCapability implements Cborable {\n    public final PathElement name;\n    public final RelativeCapability cap;\n    public final Optional<Boolean> isDir;\n    public final Optional<String> mimetype;\n    public final Optional<LocalDateTime> created;\n\n    public NamedRelativeCapability(PathElement name,\n                                   RelativeCapability cap,\n                                   Optional<Boolean> isDir,\n                                   Optional<String> mimetype,\n                                   Optional<LocalDateTime> created) {\n        this.name = name;\n        this.cap = cap;\n        this.isDir = isDir;\n        this.mimetype = mimetype;\n        this.created = created;\n    }\n\n    public NamedRelativeCapability(String name,\n                                   RelativeCapability cap,\n                                   Optional<Boolean> isDir,\n                                   Optional<String> mimetype,\n                                   Optional<LocalDateTime> created) {\n        this(new PathElement(name), cap, isDir, mimetype, created);\n    }\n\n    public NamedAbsoluteCapability toAbsolute(AbsoluteCapability source) {\n        return new NamedAbsoluteCapability(name, cap.toAbsolute(source), isDir, mimetype, created);\n    }\n\n    @JsMethod\n    public String name() {\n        return name.name;\n    }\n\n    @JsMethod\n    public boolean isDir() {\n        return isDir.orElse(false);\n    }\n\n    @JsMethod\n    public LocalDateTime created() {\n        return created.orElse(LocalDateTime.MIN);\n    }\n\n    @JsMethod\n    public String mimeType() {\n        return mimetype.orElse(\"application/octet-stream\");\n    }\n\n    private void addCbor(String key, CborObject val, CborObject.CborMap m) {\n        if (m.containsKey(key))\n            throw new IllegalStateException(\"Incompatible cbor\");\n        m.put(key, val);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        CborObject.CborMap cbor = cap.toCbor(); // This is to ensure binary compatibility for old code with new data\n        // w, m, a, k, l are taken\n        addCbor(\"n\", new CborObject.CborString(name.name), cbor);\n        isDir.ifPresent(d -> addCbor(\"d\", new CborObject.CborBoolean(d), cbor));\n        mimetype.ifPresent(m -> addCbor(\"t\", new CborObject.CborString(m), cbor));\n        created.ifPresent(c -> addCbor(\"c\", new CborObject.CborLong(c.toEpochSecond(ZoneOffset.UTC)), cbor));\n        return cbor;\n    }\n\n    public static NamedRelativeCapability fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor\");\n        CborObject.CborMap map = ((CborObject.CborMap) cbor);\n\n        String name = map.getString(\"n\");\n        RelativeCapability cap = RelativeCapability.fromCbor(cbor);\n        Optional<Boolean> isDir = map.getOptional(\"d\", c -> ((CborObject.CborBoolean)c).value);\n        Optional<String> mimetype = map.getOptional(\"t\", c -> ((CborObject.CborString)c).value);\n        Optional<LocalDateTime> created = map.getOptional(\"c\", c -> LocalDateTime.ofEpochSecond(((CborObject.CborLong)c).value, 0, ZoneOffset.UTC));\n        return new NamedRelativeCapability(new PathElement(name), cap, isDir, mimetype, created);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/NativeJSThumbnail.java",
    "content": "package peergos.shared.user.fs;\n\nimport java.util.concurrent.CompletableFuture;\n\nimport jsinterop.annotations.JsType;\n\n@JsType(namespace = \"thumbnail\", isNative = true)\npublic class NativeJSThumbnail {\n\tpublic native CompletableFuture<String> generateThumbnail(AsyncReader imageBlob, int fileSize, String fileName) ;\n\tpublic native CompletableFuture<String> generateVideoThumbnail(AsyncReader imageBlob, int fileSize, String fileName, String mimeType) ;\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/RelativeCapability.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\n\n/** This provides a relative cryptographic capability (read only or read and write) for a file or folder.\n *\n *  Here, relative means that the holder has the owner and writer public key from another source, typically the parent\n *  folder or entry point. This also includes the relative link between the symmetric write base keys if they are different.\n */\npublic class RelativeCapability implements Cborable {\n    public static final int MAP_KEY_LENGTH = 32;\n\n    // writer is only present when it is not implicit (an entry point, or a child link to a different writing key)\n    public final Optional<PublicKeyHash> writer;\n    private final byte[] mapKey;\n    public final Optional<Bat> bat; // Only absent on legacy data\n    public final SymmetricKey rBaseKey;\n    public final Optional<SymmetricLink> wBaseKeyLink;\n\n    @JsConstructor\n    public RelativeCapability(Optional<PublicKeyHash> writer,\n                              byte[] mapKey,\n                              Optional<Bat> bat,\n                              SymmetricKey rBaseKey,\n                              Optional<SymmetricLink> wBaseKeyLink) {\n        this.writer = writer;\n        if (mapKey.length != RelativeCapability.MAP_KEY_LENGTH)\n            throw new IllegalStateException(\"Invalid map key length: \" + mapKey.length);\n        this.mapKey = mapKey;\n        this.bat = bat;\n        this.rBaseKey = rBaseKey;\n        this.wBaseKeyLink = wBaseKeyLink;\n    }\n\n    @JsMethod\n    public byte[] getMapKey() {\n        return Arrays.copyOf(mapKey, mapKey.length);\n    }\n\n    public Location getLocation(PublicKeyHash owner, PublicKeyHash writer) {\n        return new Location(owner, this.writer.orElse(writer), mapKey);\n    }\n\n    public SymmetricKey getWriteBaseKey(SymmetricKey sourceBaseKey) {\n        return wBaseKeyLink.get().target(sourceBaseKey);\n    }\n\n    public AbsoluteCapability toAbsolute(AbsoluteCapability source) {\n        Optional<SymmetricKey> wBaseKey = source.wBaseKey.flatMap(w -> wBaseKeyLink.map(link -> link.target(w)));\n        PublicKeyHash writer = this.writer.orElse(source.writer);\n        if (wBaseKey.isPresent())\n            return new WritableAbsoluteCapability(source.owner, writer, mapKey, bat, rBaseKey, wBaseKey.get());\n        return new AbsoluteCapability(source.owner, writer, mapKey, bat, rBaseKey, wBaseKey);\n    }\n\n    public RelativeCapability withWritingKey(Optional<PublicKeyHash> writingKey) {\n        return new RelativeCapability(writingKey, mapKey, bat, rBaseKey, wBaseKeyLink);\n    }\n\n    public static RelativeCapability buildSubsequentChunk(byte[] mapkey, Optional<Bat> bat, SymmetricKey baseKey) {\n        return new RelativeCapability(Optional.empty(), mapkey, bat, baseKey, Optional.empty());\n    }\n\n    @Override\n    public CborObject.CborMap toCbor() {\n        Map<String, Cborable> cbor = new TreeMap<>();\n        writer.ifPresent(w -> cbor.put(\"w\", w.toCbor()));\n        cbor.put(\"m\", new CborObject.CborByteArray(mapKey));\n        bat.ifPresent(b -> cbor.put(\"a\", b));\n        cbor.put(\"k\", rBaseKey.toCbor());\n        wBaseKeyLink.ifPresent(w -> cbor.put(\"l\", w.toCbor()));\n        return CborObject.CborMap.build(cbor);\n    }\n\n    public static RelativeCapability fromByteArray(byte[] raw) {\n        return fromCbor(CborObject.fromByteArray(raw));\n    }\n\n    public static RelativeCapability fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for RelativeCapability: \" + cbor);\n        CborObject.CborMap map = ((CborObject.CborMap) cbor);\n\n        Optional<PublicKeyHash> writer = Optional.ofNullable(map.get(\"w\"))\n                .map(PublicKeyHash::fromCbor);\n        byte[] mapKey = ((CborObject.CborByteArray)map.get(\"m\")).value;\n        Optional<Bat> bat = map.getOptional(\"a\", Bat::fromCbor);\n        SymmetricKey baseKey = SymmetricKey.fromCbor(map.get(\"k\"));\n        Optional<SymmetricLink> writerLink = Optional.ofNullable(map.get(\"l\")).map(SymmetricLink::fromCbor);\n        return new RelativeCapability(writer, mapKey, bat, baseKey, writerLink);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        RelativeCapability that = (RelativeCapability) o;\n        return Objects.equals(writer, that.writer) &&\n                Arrays.equals(mapKey, that.mapKey) &&\n                Objects.equals(bat, that.bat) &&\n                Objects.equals(rBaseKey, that.rBaseKey) &&\n                Objects.equals(wBaseKeyLink, that.wBaseKeyLink);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Objects.hash(writer, bat, rBaseKey, wBaseKeyLink);\n        result = 31 * result + Arrays.hashCode(mapKey);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return ArrayOps.bytesToHex(getMapKey());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/ResumeUploadProps.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.Crypto;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.symmetric.SymmetricKey;\nimport peergos.shared.storage.auth.Bat;\n\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class ResumeUploadProps implements Cborable {\n    public final SymmetricKey baseKey, dataKey, writeKey;\n    public final byte[] streamSecret;\n    public final Bat firstChunkBat;\n    public final byte[] firstChunkMapKey;\n\n    public ResumeUploadProps(SymmetricKey baseKey,\n                             SymmetricKey dataKey,\n                             SymmetricKey writeKey,\n                             byte[] streamSecret,\n                             Bat firstChunkBat,\n                             byte[] firstChunkMapKey) {\n        this.baseKey = baseKey;\n        this.dataKey = dataKey;\n        this.writeKey = writeKey;\n        this.streamSecret = streamSecret;\n        this.firstChunkBat = firstChunkBat;\n        this.firstChunkMapKey = firstChunkMapKey;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"b\", baseKey.toCbor());\n        state.put(\"d\", dataKey.toCbor());\n        state.put(\"w\", writeKey.toCbor());\n        state.put(\"s\", new CborObject.CborByteArray(streamSecret));\n        state.put(\"ib\", firstChunkBat.toCbor());\n        state.put(\"m\", new CborObject.CborByteArray(firstChunkMapKey));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static ResumeUploadProps fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for PartialUploadProps! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        SymmetricKey baseKey = m.get(\"b\", SymmetricKey::fromCbor);\n        SymmetricKey dataKey = m.get(\"d\", SymmetricKey::fromCbor);\n        SymmetricKey writeKey = m.get(\"w\", SymmetricKey::fromCbor);\n        byte[] streamSecret = m.getByteArray(\"s\");\n        Bat initialBat = m.get(\"ib\", Bat::fromCbor);\n        byte[] initialMapKey = m.getByteArray(\"m\");\n        return new ResumeUploadProps(baseKey, dataKey, writeKey, streamSecret, initialBat, initialMapKey);\n    }\n\n    public static ResumeUploadProps random(Crypto crypto) {\n        SymmetricKey baseKey = SymmetricKey.random();\n        SymmetricKey dataKey = SymmetricKey.random();\n        SymmetricKey writeKey = SymmetricKey.random();\n        byte[] streamSecret = crypto.random.randomBytes(32);\n        Bat firstChunkBat = Bat.random(crypto.random);\n        byte[] firstChunkMapKey = crypto.random.randomBytes(32);\n        return new ResumeUploadProps(baseKey, dataKey, writeKey, streamSecret, firstChunkBat, firstChunkMapKey);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/RetrievedCapability.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.user.fs.cryptree.*;\n\npublic class RetrievedCapability {\n    public final AbsoluteCapability capability;\n    public final CryptreeNode fileAccess;\n\n    public RetrievedCapability(AbsoluteCapability capability, CryptreeNode fileAccess) {\n        if (fileAccess == null)\n            throw new IllegalStateException(\"Null FileAccess!\");\n        this.capability = capability;\n        this.fileAccess = fileAccess;\n    }\n\n    public SymmetricKey getParentKey() {\n        return getParentKey(fileAccess, capability.rBaseKey);\n    }\n\n    public FileProperties getProperties() {\n        return fileAccess.getProperties(getParentKey());\n    }\n\n    public SymmetricKey getParentParentKey() {\n        return fileAccess.getParentCapability(capability.rBaseKey).get().rBaseKey;\n    }\n\n    public RelativeCapability getParentCap() {\n        return fileAccess.getParentCapability(capability.rBaseKey).get();\n    }\n\n    private static SymmetricKey getParentKey(CryptreeNode node, SymmetricKey baseKey) {\n        if (node.isDirectory())\n            try {\n                return node.getParentKey(baseKey);\n            } catch (Exception e) {\n                // if we don't have read access to this folder, then we must just have the parent key already\n            }\n        return baseKey;\n    }\n\n    public RetrievedCapability withCryptree(CryptreeNode fileAccess) {\n        return new RetrievedCapability(capability, fileAccess);\n    }\n\n    public RetrievedCapability withHash(Multihash newHash) {\n        return new RetrievedCapability(capability, fileAccess.withHash(newHash));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/RootHash.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.util.ArrayOps;\n\nimport java.util.Arrays;\nimport java.util.Objects;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\npublic class RootHash implements Cborable {\n    public final byte[] hash;\n\n    public RootHash(byte[] hash) {\n        if (hash.length != 32)\n            throw new IllegalArgumentException(\"Incorrect hash length: \" + hash.length);\n        this.hash = hash;\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"h\", new CborObject.CborByteArray(hash));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static RootHash fromCbor(Cborable cbor) {\n        if (!(cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for HashBranch! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new RootHash(m.getByteArray(\"h\"));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        RootHash rootHash = (RootHash) o;\n        return Objects.deepEquals(hash, rootHash.hash);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(hash);\n    }\n\n    @Override\n    public String toString() {\n        return ArrayOps.bytesToHex(hash);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/SecretLink.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.io.ipfs.bases.*;\n\npublic class SecretLink {\n\n    public final PublicKeyHash owner;\n    public final long label;\n    public final String linkPassword;\n\n    public SecretLink(PublicKeyHash owner, long label, String linkPassword) {\n        if (label <= 0)\n            throw new IllegalStateException(\"Link labels must be positive!\");\n        this.owner = owner;\n        this.label = label;\n        this.linkPassword = linkPassword;\n    }\n\n    @JsMethod\n    public String toLink() {\n        return \"secret/\" + Multibase.Base.Base58BTC.prefix + owner.toBase58() + \"/\" + labelString() + \"#\" + linkPassword;\n    }\n\n    public String labelString() {\n        return \"\" + label;\n    }\n\n    @JsMethod\n    public static SecretLink fromLink(String link) {\n        // e.g secret/z59vuwzfFDorjWRiEtcEu6BQWWsLYCAJpmkAcVkuV8P5b4ykYwm1NE6/8057131#moCvfdkPxWLb\n        if (link.startsWith(\"/\"))\n            link = link.substring(1);\n        int hashIndex = link.indexOf(\"#\");\n        String fragment = link.substring(hashIndex + 1);\n        if (fragment.contains(\"?\"))\n            fragment = fragment.substring(0, fragment.indexOf(\"?\"));\n        String[] parts = link.substring(0, hashIndex).split(\"/\");\n        if (parts.length != 3)\n            throw new IllegalStateException(\"Invalid secret link\");\n        PublicKeyHash owner = PublicKeyHash.fromString(parts[1]);\n        long ref = Long.parseLong(parts[2]);\n        return new SecretLink(owner, ref, fragment);\n    }\n\n    public static SecretLink create(PublicKeyHash owner, SafeRandom r) {\n        byte[] labelBytes = r.randomBytes(4);\n        long label = (labelBytes[0] & 0xFF) | ((labelBytes[1] & 0xFF) << 8) | ((labelBytes[2] & 0xFF) << 16) | (((labelBytes[3] & 0xFF) << 24) & 0xFFFFFFFFL);\n        String linkPassword = EncryptedCapability.createLinkPassword(r);\n        return new SecretLink(owner, label, linkPassword);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/SecretLinkTarget.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class SecretLinkTarget implements Cborable {\n\n    public final EncryptedCapability cap;\n    public final Optional<LocalDateTime> expiry;\n    public final Optional<Integer> maxRetrievals;\n\n    public SecretLinkTarget(EncryptedCapability cap, Optional<LocalDateTime> expiry, Optional<Integer> maxRetrievals) {\n        this.cap = cap;\n        this.expiry = expiry;\n        this.maxRetrievals = maxRetrievals;\n    }\n\n    public CompletableFuture<AbsoluteCapability> decrypt(String label, String password, Crypto c) {\n        return cap.decryptFromPassword(label, password, c);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"cap\", cap.toCbor());\n        expiry.ifPresent(e -> state.put(\"expiry\", new CborObject.CborLong(e.toEpochSecond(ZoneOffset.UTC))));\n        maxRetrievals.ifPresent(m -> state.put(\"max\", new CborObject.CborLong(m)));\n        return CborObject.CborMap.build(state);\n    }\n\n    public static SecretLinkTarget fromCbor(Cborable cbor) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Invalid cbor for SecretLinkTarget! \" + cbor);\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        return new SecretLinkTarget(m.get(\"cap\", EncryptedCapability::fromCbor),\n                m.getOptionalLong(\"expiry\").map(l -> LocalDateTime.ofEpochSecond(l, 0, ZoneOffset.UTC)),\n                m.getOptionalLong(\"max\").map(Long::intValue));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/SplitFragmenter.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.cbor.*;\n\nimport java.util.*;\n\npublic class SplitFragmenter implements Fragmenter {\n\n    @Override\n    public double storageIncreaseFactor() {\n        return 1.0;\n    }\n\n    public byte[][] split(byte[] input) {\n        //calculate padding length to align to 256 bytes\n        int padding = 0;\n        int mod = input.length % 256;\n        if (mod != 0 || input.length == 0)\n            padding = 256 - mod;\n        //align to 256 bytes\n        int len = input.length + padding;\n\n        //calculate the number  of fragments\n        int nFragments =  len / Fragment.MAX_LENGTH;\n        if (len % Fragment.MAX_LENGTH > 0)\n            nFragments++;\n\n        byte[][] split = new  byte[nFragments][];\n        for(int i= 0; i< nFragments; ++i) {\n            int start = Fragment.MAX_LENGTH * i;\n            int end = Math.min(input.length, start + Fragment.MAX_LENGTH);\n            int length = end - start;\n            byte[] b = new byte[length];\n            System.arraycopy(input, start, b, 0, length);\n            split[i] = b;\n        }\n        return split;\n\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> res = new HashMap<>();\n        res.put(\"t\", new CborObject.CborLong(Fragmenter.Type.SIMPLE.val));\n        return CborObject.CborMap.build(res);\n    }\n\n    @Override\n    public byte[] recombine(byte[][] encoded, int startOffset, int truncateTo) {\n        int length = 0;\n\n        for (int i=0; i < encoded.length; i++)\n            length += encoded[i].length;\n\n        byte[] output = new byte[startOffset + length];\n        int pos =  0;\n        for (int i=0; i < encoded.length && pos < truncateTo; i++) {\n            byte[] b = encoded[i];\n            int copyLength = Math.max(0, Math.min(b.length, truncateTo - pos));\n            System.arraycopy(b, 0, output, startOffset + pos, copyLength);\n            pos += copyLength;\n        }\n        return output;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        return  ! (o == null || getClass() != o.getClass());\n    }\n\n    @Override\n    public int hashCode() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/Thumbnail.java",
    "content": "package peergos.shared.user.fs;\n\nimport jsinterop.annotations.*;\n\npublic class Thumbnail {\n    public final String mimeType;\n    public final byte[] data;\n\n    @JsConstructor\n    public Thumbnail(String mimeType, byte[] data) {\n        if (data.length > 100*1024)\n            throw new IllegalStateException(\"Image thumbnails must be < 100 KiB! Mimetype: \" + mimeType);\n        this.mimeType = mimeType;\n        this.data = data;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/ThumbnailGenerator.java",
    "content": "package peergos.shared.user.fs;\n\nimport java.util.*;\nimport java.io.*;\n\npublic class ThumbnailGenerator {\n\n    public interface Generator {\n        Optional<Thumbnail> generateThumbnail(byte[] data);\n    }\n\n    public interface VideoGenerator {\n        Optional<Thumbnail> generateVideoThumbnail(File video);\n    }\n\n    private static Generator instance;\n\n    public static synchronized void setInstance(Generator instance) {\n        ThumbnailGenerator.instance = instance;\n    }\n\n    public static synchronized Generator get() {\n        if (instance == null)\n            throw new IllegalStateException(\"Thumbnail generator hasn't been set!\");\n        return instance;\n    }\n\n    static class NoopVideoThumbnailer implements VideoGenerator {\n        @Override\n        public Optional<Thumbnail> generateVideoThumbnail(File video) {\n            return Optional.empty();\n        }\n    }\n\n    private static VideoGenerator videoInstance = new NoopVideoThumbnailer();\n\n    public static synchronized void setVideoInstance(VideoGenerator instance) {\n        ThumbnailGenerator.videoInstance = instance;\n    }\n\n    public static synchronized VideoGenerator getVideo() {\n        if (videoInstance == null)\n            throw new IllegalStateException(\"Video thumbnail generator hasn't been set!\");\n        return videoInstance;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/VideoThumbnail.java",
    "content": "package peergos.shared.user.fs;\n\n//import javafx.application.Platform;\n//import javafx.embed.swing.JFXPanel;\n//import javafx.embed.swing.SwingFXUtils;\n//import javafx.scene.image.WritableImage;\n//import javafx.scene.media.Media;\n//import javafx.scene.media.MediaPlayer;\n//import javafx.scene.media.MediaView;\n//import javafx.util.Duration;\n\nimport javax.imageio.ImageIO;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutionException;\n\npublic class VideoThumbnail {\n\n    private static void sleep(int duration) {\n        try {\n            Thread.sleep(duration);\n        }catch(InterruptedException ie) {\n        }\n    }\n\n//    private static double getDuration(Media video) {\n//        boolean finished = false;\n//        int counter = 0;\n//        while(!finished) {\n//            Duration duration = video.getDuration();\n//            if(duration.isUnknown()) {\n//                sleep(1000);\n//                counter++;\n//                if(counter > 10) {\n//                    return 0;\n//                }\n//            } else {\n//                return duration.toSeconds();\n//            }\n//        }\n//        return 0;\n//    }\n    private static boolean isLikelyValidImage(BufferedImage image) {\n        int width = image.getWidth();\n        int height = image.getHeight();\n        int threshold = width * height / 10 * 8;\n        int blackCount = 0;\n        int whiteCount = 0;\n        for( int i = 0; i < width; i++) {\n            for (int j = 0; j < height; j++) {\n                int val = image.getRGB(i, j);\n                int r = (val >> 16) & 0xFF;\n                int g = (val >> 8) & 0xFF;\n                int b = val & 0xFF;\n                int total = r + g + b;\n                if(total < 10) {\n                    if(++blackCount > threshold) {\n                        return false;\n                    }\n                } else if(total > 760) {\n                    if(++whiteCount > threshold) {\n                        return false;\n                    }\n                }\n            }\n        }\n        return true;\n    }\n\n    public static Optional<Thumbnail> create(String filename, int height, int width) {\n//        new JFXPanel(); // initialises toolkit\n//        Media video = new Media(\"file://\" + filename);\n//        MediaPlayer mediaPlayer = new MediaPlayer(video);\n//        WritableImage wim = new WritableImage(width, height);\n//        MediaView mv = new MediaView();\n//        mv.setFitWidth(width);\n//        mv.setFitHeight(height);\n//        mv.setMediaPlayer(mediaPlayer);\n//        mv.setPreserveRatio(false);\n//\n//        double duration = getDuration(video);\n//        double increment = duration/10; //seconds\n//        double currentIncrement = 0;\n//        while(currentIncrement < duration) {\n//            currentIncrement = currentIncrement + increment;\n//            mediaPlayer.seek(Duration.seconds(currentIncrement));\n//            CompletableFuture<BufferedImage> res = new CompletableFuture<>();\n//            Platform.runLater(() -> {\n//                mv.snapshot(null, wim);\n//                BufferedImage image = SwingFXUtils.fromFXImage(wim, null);\n//                res.complete(image);\n//            });\n//            ByteArrayOutputStream bout = new ByteArrayOutputStream();\n//            try {\n//                BufferedImage image = res.get();\n//                if(isLikelyValidImage(image)){\n//                    ImageIO.write(image, \"png\", bout);\n//                    bout.flush();\n//                    bout.close();\n//                    return bout.toByteArray();\n//                }\n//            }catch(IOException | InterruptedException | ExecutionException ex){\n//                ex.printStackTrace();\n//            }\n//        }\n        return Optional.empty();\n    }\n}\n\n"
  },
  {
    "path": "src/peergos/shared/user/fs/WritableAbsoluteCapability.java",
    "content": "package peergos.shared.user.fs;\n\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.io.ipfs.bases.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class WritableAbsoluteCapability extends AbsoluteCapability {\n\n    public WritableAbsoluteCapability(PublicKeyHash owner, PublicKeyHash writer, byte[] mapKey, Optional<Bat> bat, SymmetricKey baseKey, SymmetricKey wBaseKey) {\n        super(owner, writer, mapKey, bat, baseKey, Optional.of(wBaseKey));\n    }\n\n    public RelativeCapability relativise(AbsoluteCapability descendant) {\n        if (! Objects.equals(owner, descendant.owner))\n            throw new IllegalStateException(\"Files with different owners can't be descendant of each other!\");\n\n        Optional<SymmetricLink> writerLink = descendant.wBaseKey.flatMap(dWrite ->\n                dWrite.equals(wBaseKey.get()) ?\n                        Optional.empty() :\n                        Optional.of(SymmetricLink.fromPair(wBaseKey.get(), dWrite)));\n        Optional<PublicKeyHash> writerOpt = Objects.equals(writer, descendant.writer) ?\n                Optional.empty() : Optional.of(descendant.writer);\n\n        return new RelativeCapability(writerOpt, descendant.getMapKey(), descendant.bat, descendant.rBaseKey, writerLink);\n    }\n\n    @Override\n    public WritableAbsoluteCapability withBaseKey(SymmetricKey newBaseKey) {\n        return new WritableAbsoluteCapability(owner, writer, getMapKey(), bat, newBaseKey, wBaseKey.get());\n    }\n\n    public WritableAbsoluteCapability withBaseWriteKey(SymmetricKey newBaseWriteKey) {\n        return new WritableAbsoluteCapability(owner, writer, getMapKey(), bat, rBaseKey, newBaseWriteKey);\n    }\n\n    @Override\n    public WritableAbsoluteCapability withMapKey(byte[] newMapKey, Optional<Bat> newBat) {\n        return new WritableAbsoluteCapability(owner, writer, newMapKey, newBat, rBaseKey, wBaseKey.get());\n    }\n\n    public AbsoluteCapability withOwner(PublicKeyHash owner) {\n        return new WritableAbsoluteCapability(owner, writer, getMapKey(), bat, rBaseKey, wBaseKey.get());\n    }\n\n    public WritableAbsoluteCapability withSigner(PublicKeyHash newSigner) {\n        return new WritableAbsoluteCapability(owner, newSigner, getMapKey(), bat, rBaseKey, wBaseKey.get());\n    }\n\n    /*  Return a capability link of the form #$owner/$writer/$mapkey+$bat/$baseKey/$baseWkey\n     */\n    public String toLink() {\n        String encodedOwnerKey = Base58.encode(owner.serialize());\n        String encodedWriterKey = Base58.encode(writer.serialize());\n        String encodedMapKeyAndBat = Base58.encode(ArrayOps.concat(getMapKey(), bat.map(Bat::serialize).orElse(new byte[0])));\n        String encodedBaseKey = Base58.encode(rBaseKey.serialize());\n        String encodedWBaseKey = Base58.encode(wBaseKey.get().serialize());\n        return Stream.of(encodedOwnerKey, encodedWriterKey, encodedMapKeyAndBat, encodedBaseKey, encodedWBaseKey)\n                .collect(Collectors.joining(\"/\", \"#\", \"\"));\n    }\n\n    @Override\n    public String toString() {\n        return writer + \".\" + ArrayOps.bytesToHex(getMapKey());\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/cryptree/CryptreeNode.java",
    "content": "package peergos.shared.user.fs.cryptree;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.random.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.inode.*;\nimport peergos.shared.io.ipfs.Cid;\nimport peergos.shared.io.ipfs.Multihash;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\n/** A cryptree node controls read and write access to a directory or file.\n *\n * A directory contains the following distinct symmetric read keys {base, parent}, and file contains {base == parent, data}\n * A directory or file also has a single base symmetric write key\n *\n * A link node is a special node that behaves like a directory with a single child, and contains only the filename.\n * These are used when granting write access to prevent the recipient from being able to rename the file/dir to\n * potentially clash with a sibling that they cannot see. This means you cannot rename something unless you have write\n * access to the parent directory, which is in line with unix et al.\n *\n * The serialized encrypted form stores a link from the base key to the other key.\n * For a directory, the base key encrypts the links to child directories and files. For a file the datakey encrypts the\n * file's data. The parent key encrypts the link to the parent directory's parent key and the metadata (FileProperties).\n *\n * There are three network visible components to the serialization:\n * 1) A fixed size block encrypted with the base key, containing the second key (parent or data key), the location of\n *       the next chunk, and an optional symmetric link to a signing pair\n * 2) A fragmented padded cipher text, padded to a multiple of 4096,\n *       containing the relative child links of a directory, or the data of a file chunk\n * 3) A padded cipher text (to a multiple of 16 bytes) of an optional relative parent link, and file properties\n *       The parent link is present on the first chunk of all files and directories except your home directory\n */\npublic class CryptreeNode implements Cborable {\n    private static final int CURRENT_VERSION = 1;\n    private static final int META_DATA_PADDING_BLOCKSIZE = 16;\n    private static final int BASE_BLOCK_PADDING_BLOCKSIZE = 64;\n    private static final int MIN_FRAGMENT_SIZE = 4096;\n    private static int MAX_CHILD_LINKS_PER_BLOB = 500;\n\n    public static synchronized void setMaxChildLinkPerBlob(int newValue) {\n        MAX_CHILD_LINKS_PER_BLOB = newValue;\n    }\n\n    public static synchronized int getMaxChildLinksPerBlob() {\n        return MAX_CHILD_LINKS_PER_BLOB;\n    }\n\n    private transient final MaybeMultihash lastCommittedHash;\n    private transient final boolean isDirectory;\n    public final List<BatId> bats;\n    private final PaddedCipherText fromBaseKey;\n    private final FragmentedPaddedCipherText childrenOrData;\n    private final PaddedCipherText fromParentKey;\n\n    public CryptreeNode(MaybeMultihash lastCommittedHash,\n                        boolean isDirectory,\n                        List<BatId> bats,\n                        PaddedCipherText fromBaseKey,\n                        FragmentedPaddedCipherText childrenOrData,\n                        PaddedCipherText fromParentKey) {\n        this.lastCommittedHash = lastCommittedHash;\n        this.isDirectory = isDirectory;\n        this.bats = bats;\n        this.fromBaseKey = fromBaseKey;\n        this.childrenOrData = childrenOrData;\n        this.fromParentKey = fromParentKey;\n    }\n\n    public int getVersion() {\n        return CURRENT_VERSION;\n    }\n\n    private static class FromBase implements Cborable {\n        public final SymmetricKey parentOrData;\n        public final Optional<SymmetricLinkToSigner> signer;\n        public final RelativeCapability nextChunk;\n\n        public FromBase(SymmetricKey parentOrData,\n                        Optional<SymmetricLinkToSigner> signer,\n                        RelativeCapability nextChunk) {\n            this.parentOrData = parentOrData;\n            this.signer = signer;\n            this.nextChunk = nextChunk;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"k\", parentOrData);\n            signer.ifPresent(w -> state.put(\"w\", w));\n            state.put(\"n\", nextChunk);\n            return CborObject.CborMap.build(state);\n        }\n\n        public static FromBase fromCbor(CborObject cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Incorrect cbor for FromBase: \" + cbor);\n\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n            SymmetricKey k = m.get(\"k\", SymmetricKey::fromCbor);\n            Optional<SymmetricLinkToSigner> w = m.getOptional(\"w\", SymmetricLinkToSigner::fromCbor);\n            RelativeCapability nextChunk = m.get(\"n\", RelativeCapability::fromCbor);\n            return new FromBase(k, w, nextChunk);\n        }\n    }\n\n    private FromBase getBaseBlock(SymmetricKey baseKey) {\n        return fromBaseKey.decrypt(baseKey, FromBase::fromCbor);\n    }\n\n    private static class FromParent implements Cborable {\n        public final Optional<RelativeCapability> parentLink;\n        public final FileProperties properties;\n\n        public FromParent(Optional<RelativeCapability> parentLink, FileProperties properties) {\n            this.parentLink = parentLink;\n            this.properties = properties;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            parentLink.ifPresent(p -> state.put(\"p\", p));\n            state.put(\"s\", properties);\n            return CborObject.CborMap.build(state);\n        }\n\n        public static FromParent fromCbor(CborObject cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Incorrect cbor for FromParent: \" + cbor);\n\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n            Optional<RelativeCapability> parentLink = m.getOptional(\"p\", RelativeCapability::fromCbor);\n            FileProperties properties = m.get(\"s\", FileProperties::fromCbor);\n            return new FromParent(parentLink, properties);\n        }\n    }\n\n    private FromParent getParentBlock(SymmetricKey parentKey) {\n        return fromParentKey.decrypt(parentKey, FromParent::fromCbor);\n    }\n\n    public static class DirAndChildren {\n        public final CryptreeNode dir;\n        public final List<FragmentWithHash> childData;\n\n        public DirAndChildren(CryptreeNode dir, List<FragmentWithHash> childData) {\n            this.dir = dir;\n            this.childData = childData;\n        }\n\n        public CompletableFuture<Snapshot> commit(Snapshot current,\n                                                  Committer committer,\n                                                  WritableAbsoluteCapability us,\n                                                  Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                  NetworkAccess network,\n                                                  TransactionId tid) {\n            SigningPrivateKeyAndPublicHash signer = dir.getSigner(us.rBaseKey, us.wBaseKey.get(), entryWriter);\n            return commit(current, committer, us, signer, network, tid);\n        }\n\n        public CompletableFuture<Snapshot> commit(Snapshot current,\n                                                  Committer committer,\n                                                  WritableAbsoluteCapability us,\n                                                  SigningPrivateKeyAndPublicHash signer,\n                                                  NetworkAccess network,\n                                                  TransactionId tid) {\n            return commitChildrenLinks(us, signer, network, tid)\n                    .thenCompose(hashes -> dir.commit(current, committer, us, signer, network, tid));\n        }\n\n        public CompletableFuture<List<Cid>> commitChildrenLinks(WritableAbsoluteCapability us,\n                                                                Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                                NetworkAccess network,\n                                                                TransactionId tid) {\n            SigningPrivateKeyAndPublicHash signer = dir.getSigner(us.rBaseKey, us.wBaseKey.get(), entryWriter);\n            return commitChildrenLinks(us, signer, network, tid);\n        }\n\n        public CompletableFuture<List<Cid>> commitChildrenLinks(WritableAbsoluteCapability us,\n                                                                SigningPrivateKeyAndPublicHash signer,\n                                                                NetworkAccess network,\n                                                                TransactionId tid) {\n            List<Fragment> frags = childData.stream()\n                    .filter(f -> ! f.isInlined())\n                    .map(f -> f.fragment)\n                    .collect(Collectors.toList());\n            return network.uploadFragments(frags, us.owner, signer, l -> {}, tid);\n        }\n    }\n\n    public static class ChildrenLinks implements Cborable {\n        public final Either<List<RelativeCapability>, List<NamedRelativeCapability>> children;\n\n        public ChildrenLinks(List<NamedRelativeCapability> children) {\n            this.children = Either.b(children);\n        }\n\n        private ChildrenLinks(Either<List<RelativeCapability>, List<NamedRelativeCapability>> children) {\n            this.children = children;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            List<CborObject> mapped = children.map(\n                    x -> x.stream().map(Cborable::toCbor).collect(Collectors.toList()),\n                    x -> x.stream().map(Cborable::toCbor).collect(Collectors.toList()));\n            return new CborObject.CborList(mapped);\n        }\n\n        public static ChildrenLinks fromCbor(CborObject cbor) {\n            if (! (cbor instanceof CborObject.CborList))\n                throw new IllegalStateException(\"Incorrect cbor for ChildrenLinks: \" + cbor);\n\n            List<? extends Cborable> cborList = ((CborObject.CborList) cbor).value;\n            if (cborList.isEmpty())\n                return empty();\n            CborObject.CborMap firstMap = (CborObject.CborMap) cborList.get(0);\n            if (firstMap.containsKey(\"n\"))\n                return new ChildrenLinks(Either.b(cborList\n                        .stream()\n                        .map(NamedRelativeCapability::fromCbor)\n                        .collect(Collectors.toList())));\n            return new ChildrenLinks(Either.a(cborList\n                        .stream()\n                        .map(RelativeCapability::fromCbor)\n                        .collect(Collectors.toList())));\n        }\n\n        public static ChildrenLinks empty() {\n            return new ChildrenLinks(Either.b(Collections.emptyList()));\n        }\n    }\n\n    public MaybeMultihash committedHash() {\n        return lastCommittedHash;\n    }\n\n    public boolean isDirectory() {\n        return isDirectory;\n    }\n\n    public boolean isReadable(SymmetricKey baseKey) {\n        try {\n            getBaseBlock(baseKey);\n            return true;\n        } catch (Exception e) {}\n        return false;\n    }\n\n    public Optional<SymmetricLinkToSigner> getWriterLink(SymmetricKey baseKey) {\n        return getBaseBlock(baseKey).signer;\n    }\n\n    public SymmetricKey getParentKey(SymmetricKey baseKey) {\n        if (isDirectory())\n            try {\n                return getBaseBlock(baseKey).parentOrData;\n            } catch (Exception e) {\n                // if we don't have read access to this folder, then we must just have the parent key already\n            }\n        return baseKey;\n    }\n\n    public SymmetricKey getDataKey(SymmetricKey baseKey) {\n        if (isDirectory())\n            throw new IllegalStateException(\"Directories don't have a data key!\");\n        return getBaseBlock(baseKey).parentOrData;\n    }\n\n    public FileProperties getProperties(SymmetricKey parentKey) {\n        return getParentBlock(parentKey).properties;\n    }\n\n    public SigningPrivateKeyAndPublicHash getSigner(SymmetricKey rBaseKey,\n                                                    SymmetricKey wBaseKey,\n                                                    Optional<SigningPrivateKeyAndPublicHash> entrySigner) {\n        return getBaseBlock(rBaseKey).signer\n                .map(link -> link.target(wBaseKey))\n                .orElseGet(() -> entrySigner.orElseThrow(() ->\n                        new IllegalStateException(\"No link to private signing key present on directory!\")));\n    }\n\n    public CompletableFuture<FileRetriever> retriever(SymmetricKey baseKey,\n                                                      Optional<byte[]> streamSecret,\n                                                      byte[] currentMapKey,\n                                                      Optional<Bat> currentBat,\n                                                      Hasher hasher) {\n        return getNextChunkLocation(baseKey, streamSecret, currentMapKey, currentBat, hasher)\n                .thenApply(nextChunkLocation ->\n                        new EncryptedChunkRetriever(childrenOrData, nextChunkLocation.left, nextChunkLocation.right, getDataKey(baseKey)));\n    }\n\n    public CompletableFuture<List<NamedRelativeCapability>> getDirectChildren(AbsoluteCapability us,\n                                                                              Snapshot version,\n                                                                              NetworkAccess network) {\n        if (! isDirectory)\n            return CompletableFuture.completedFuture(Collections.emptyList());\n        return getLinkedData(us.owner, us.rBaseKey, ChildrenLinks::fromCbor, network.hasher, network, x -> {})\n                .thenCompose(c -> {\n                    if (c.children.isB())\n                        return Futures.of(c.children.b());\n                    // Only get here on legacy format directories\n                    return Futures.combineAllInOrder(c.children.a().stream()\n                            .map(r -> network.retrieveMetadata(r.toAbsolute(us), version)\n                                    .thenApply(opt -> opt.map(ret -> new NamedRelativeCapability(\n                                            new PathElement(ret.getProperties().name),\n                                            r, Optional.empty(), Optional.empty(), Optional.empty()))))\n                            .collect(Collectors.toList()))\n                            .thenApply(res -> res.stream()\n                                    .flatMap(Optional::stream)\n                                    .collect(Collectors.toList()));\n                });\n    }\n\n    public CompletableFuture<Set<NamedAbsoluteCapability>> getDirectChildrenCapabilities(AbsoluteCapability us,\n                                                                                         Snapshot version,\n                                                                                         NetworkAccess network) {\n        return getDirectChildren(us, version, network)\n                .thenApply(c ->c.stream()\n                        .map(cap -> cap.toAbsolute(us))\n                        .collect(Collectors.toSet()));\n    }\n\n    public CompletableFuture<Set<NamedAbsoluteCapability>> getAllChildrenCapabilities(Snapshot inVersion,\n                                                                                      AbsoluteCapability us,\n                                                                                      Hasher hasher,\n                                                                                      NetworkAccess network) {\n        return inVersion.withWriter(us.owner, us.writer, network).thenCompose(version -> {\n            CompletableFuture<Set<NamedAbsoluteCapability>> childrenFuture = getDirectChildrenCapabilities(us, version, network);\n\n            CompletableFuture<Optional<RetrievedCapability>> moreChildrenFuture = getNextChunk(version, us, network,\n                    Optional.empty(), hasher);\n\n            return childrenFuture.thenCompose(children -> moreChildrenFuture.thenCompose(moreChildrenSource -> {\n                        CompletableFuture<Set<NamedAbsoluteCapability>> moreChildren = moreChildrenSource\n                                .map(d -> d.fileAccess.getAllChildrenCapabilities(version, d.capability, hasher, network))\n                                .orElse(CompletableFuture.completedFuture(Collections.emptySet()));\n                        return moreChildren.thenApply(moreRetrievedChildren -> {\n                            Set<NamedAbsoluteCapability> results = Stream.concat(\n                                    children.stream(),\n                                    moreRetrievedChildren.stream())\n                                    .collect(Collectors.toSet());\n                            return results;\n                        });\n                    })\n            );\n        });\n    }\n\n    public CompletableFuture<Set<RetrievedCapability>> getDirectChildren(NetworkAccess network,\n                                                                         AbsoluteCapability us,\n                                                                         Snapshot version) {\n        return getDirectChildrenCapabilities(us, version, network)\n                .thenCompose(c -> network.retrieveAllMetadata(c.stream()\n                                .map(n -> n.cap)\n                                .collect(Collectors.toList()), version)\n                        .thenApply(p -> p.left)\n                        .thenApply(HashSet::new));\n    }\n\n    public CompletableFuture<Optional<RetrievedCapability>> getDirectChild(NetworkAccess network,\n                                                                           String name,\n                                                                           AbsoluteCapability us,\n                                                                           Snapshot version) {\n        return getDirectChildrenCapabilities(us, version, network)\n                .thenCompose(c -> {\n                    Optional<NamedAbsoluteCapability> matching = c.stream()\n                            .filter(n -> n.name.name.equals(name)).findFirst();\n                    if (matching.isEmpty())\n                        return Futures.of(Optional.empty());\n                    return network.retrieveMetadata(matching.get().cap, version);\n                });\n    }\n\n    public CompletableFuture<Set<RetrievedCapability>> getChildren(Snapshot version,\n                                                                   Hasher hasher,\n                                                                   NetworkAccess network,\n                                                                   AbsoluteCapability us) {\n        CompletableFuture<Set<RetrievedCapability>> childrenFuture = getDirectChildren(network, us, version);\n\n        CompletableFuture<Optional<RetrievedCapability>> moreChildrenFuture = getNextChunk(version, us, network,\n                Optional.empty(), hasher);\n\n        return childrenFuture.thenCompose(children -> moreChildrenFuture.thenCompose(moreChildrenSource -> {\n                    CompletableFuture<Set<RetrievedCapability>> moreChildren = moreChildrenSource\n                            .map(d -> d.fileAccess.getChildren(version, hasher, network, d.capability))\n                            .orElse(CompletableFuture.completedFuture(Collections.emptySet()));\n                    return moreChildren.thenApply(moreRetrievedChildren -> {\n                        Set<RetrievedCapability> results = Stream.concat(\n                                children.stream(),\n                                moreRetrievedChildren.stream())\n                                .collect(Collectors.toSet());\n                        return results;\n                    });\n                })\n        );\n    }\n\n    public CompletableFuture<Optional<RetrievedCapability>> getChild(String name,\n                                                                     AbsoluteCapability us,\n                                                                     Snapshot version,\n                                                                     Hasher hasher,\n                                                                     NetworkAccess network) {\n        return getDirectChild(network, name, us, version)\n                .thenCompose(directOpt -> {\n                    if (directOpt.isPresent())\n                        return Futures.of(directOpt);\n                    return getNextChunk(version, us, network,\n                            Optional.empty(), hasher).thenCompose(nextOpt -> {\n                        if (nextOpt.isPresent())\n                            return nextOpt.get().fileAccess.getChild(name, nextOpt.get().capability, version, hasher, network);\n                        return Futures.of(Optional.empty());\n                    });\n                });\n    }\n\n    public CompletableFuture<Snapshot> updateProperties(Snapshot base,\n                                                        Committer committer,\n                                                        WritableAbsoluteCapability us,\n                                                        Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                        FileProperties newProps,\n                                                        NetworkAccess network) {\n\n        SymmetricKey parentKey = getParentKey(us.rBaseKey);\n        FromParent parentBlock = getParentBlock(parentKey);\n        FromParent newParentBlock = new FromParent(parentBlock.parentLink, newProps);\n        CryptreeNode updated = new CryptreeNode(lastCommittedHash, isDirectory, bats, fromBaseKey, childrenOrData,\n                PaddedCipherText.build(parentKey, newParentBlock, META_DATA_PADDING_BLOCKSIZE));\n        return IpfsTransaction.call(us.owner,\n                tid -> network.uploadChunk(base, committer, updated, us.owner, us.getMapKey(), getSigner(us.rBaseKey, us.wBaseKey.get(), entryWriter), tid),\n                network.dhtClient);\n    }\n\n    public CompletableFuture<Snapshot> addMirrorBat(Snapshot base,\n                                                    Committer committer,\n                                                    WritableAbsoluteCapability us,\n                                                    Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                    Optional<byte[]> streamSecret,\n                                                    BatId mirrorBat,\n                                                    boolean addToUs,\n                                                    NetworkAccess network) {\n\n        List<BatId> ourBats;\n        if (addToUs) {\n            List<BatId> newBats = new ArrayList<>(bats);\n            if (newBats.size() != 1)\n                throw new IllegalStateException(\"Can't add mirror bat unless chunk has exactly 1 BAT!\");\n            newBats.add(mirrorBat);\n            ourBats = newBats;\n        } else\n            ourBats = bats;\n        SigningPrivateKeyAndPublicHash signer = getSigner(us.rBaseKey, us.wBaseKey.get(), entryWriter);\n        List<Cid> fragments = childrenOrData.getFragments();\n        return addMirrorBatToFragments(mirrorBat, fragments, childrenOrData.getBats(), us.owner, signer, network)\n                .thenCompose(frags -> frags.equals(fragments) && ourBats.equals(bats) ? Futures.of(base) : IpfsTransaction.call(us.owner,\n                                tid -> network.uploadChunk(base, committer, new CryptreeNode(lastCommittedHash, isDirectory,\n                                        ourBats, fromBaseKey, childrenOrData.withFragments(frags),\n                                        fromParentKey), us.owner, us.getMapKey(), signer, tid),\n                                network.dhtClient)\n                        .thenCompose(s -> getNextChunk(s, us, network, streamSecret, network.hasher)\n                                .thenCompose(next -> {\n                                    if (next.isPresent()) {\n                                        return next.get().fileAccess.addMirrorBat(s, committer, (WritableAbsoluteCapability) next.get().capability,\n                                                entryWriter, streamSecret, mirrorBat, addToUs, network);\n                                    }\n                                    return Futures.of(s);\n                                }))\n                );\n    }\n\n    private CompletableFuture<List<Cid>> addMirrorBatToFragments(BatId mirrorBat,\n                                                                 List<Cid> fragments,\n                                                                 List<BatWithId> bats,\n                                                                 PublicKeyHash owner,\n                                                                 SigningPrivateKeyAndPublicHash signer,\n                                                                 NetworkAccess network) {\n        if (fragments.isEmpty())\n            return Futures.of(fragments);\n        if (bats.isEmpty()) // don't add mirror bat unless fragments already have an inline bat\n            return Futures.of(fragments);\n\n        return network.dhtClient.downloadFragments(owner, fragments, bats, network.hasher, x -> {}, 1.0)\n                .thenCompose(frags -> {\n                    if (frags.size() != bats.size())\n                        throw new IllegalStateException(\"Couldn't download all chunk fragments!\");\n                    List<BatId> rawBlockBats = Bat.getRawBlockBats(frags.get(0).fragment.data);\n                    int nBats = rawBlockBats.size();\n                    if (nBats == 1) {\n                        List<Fragment> withMirrorBat = frags.stream()\n                                .map(f -> new Fragment(addMirrorBat(f.fragment.data, bats.get(fragments.indexOf(f.hash.get())).bat, mirrorBat)))\n                                .collect(Collectors.toList());\n                        return IpfsTransaction.call(owner, tid -> network.uploadFragments(withMirrorBat, owner, signer, x -> {}, tid), network.dhtClient);\n                    }\n                    return Futures.of(fragments);\n                });\n    }\n\n    private byte[] addMirrorBat(byte[] existing, Bat inlineBat, BatId mirrorBatId) {\n        return ArrayOps.concat(Bat.createRawBlockPrefix(inlineBat, Optional.of(mirrorBatId)), Bat.removeRawBlockBatPrefix(existing));\n    }\n\n    public boolean isDirty(SymmetricKey baseKey) {\n        if (isDirectory())\n            return false;\n        return getBaseBlock(baseKey).parentOrData.isDirty();\n    }\n\n    public CryptreeNode withHash(Multihash hash) {\n        return new CryptreeNode(MaybeMultihash.of(hash), isDirectory, bats, fromBaseKey, childrenOrData, fromParentKey);\n    }\n\n    public CryptreeNode withMirrorBat(BatId mirrorBatId) {\n        if (bats.size() != 1)\n            throw new IllegalStateException(\"Can only add a mirror BAT to a cryptree node with 1 BAT.\");\n        ArrayList<BatId> newBats = new ArrayList<>(bats);\n        newBats.add(mirrorBatId);\n        return new CryptreeNode(lastCommittedHash, isDirectory, newBats, fromBaseKey, childrenOrData, fromParentKey);\n    }\n\n    public CryptreeNode withWriterLink(SymmetricKey baseKey, SymmetricLinkToSigner newWriterLink) {\n        return withWriterLink(baseKey, Optional.of(newWriterLink));\n    }\n\n    public CryptreeNode withWriterLink(SymmetricKey baseKey, Optional<SymmetricLinkToSigner> newWriterLink) {\n        FromBase baseBlock = getBaseBlock(baseKey);\n        FromBase newBaseBlock = new FromBase(baseBlock.parentOrData, newWriterLink, baseBlock.nextChunk);\n        PaddedCipherText encryptedBaseBlock = PaddedCipherText.build(baseKey, newBaseBlock, BASE_BLOCK_PADDING_BLOCKSIZE);\n        return new CryptreeNode(lastCommittedHash, isDirectory, bats, encryptedBaseBlock, childrenOrData, fromParentKey);\n    }\n\n    public CompletableFuture<DirAndChildren> withChildren(SymmetricKey baseKey,\n                                                          ChildrenLinks children,\n                                                          SafeRandom random,\n                                                          Hasher hasher) {\n        return buildChildren(children, baseKey, mirrorBatId(), random, hasher)\n                .thenApply(encryptedChildren -> {\n                    CryptreeNode cryptreeNode = new CryptreeNode(lastCommittedHash, isDirectory, bats, fromBaseKey, encryptedChildren.left, fromParentKey);\n                    return new DirAndChildren(cryptreeNode, encryptedChildren.right);\n                });\n    }\n\n    private static CompletableFuture<Pair<FragmentedPaddedCipherText, List<FragmentWithHash>>> buildChildren(ChildrenLinks children,\n                                                                                                             SymmetricKey rBaseKey,\n                                                                                                             Optional<BatId> mirrorBat,\n                                                                                                             SafeRandom random,\n                                                                                                             Hasher hasher) {\n        return FragmentedPaddedCipherText.build(rBaseKey, children, MIN_FRAGMENT_SIZE, Fragment.MAX_LENGTH, mirrorBat, random, hasher, false);\n    }\n\n    public <T> CompletableFuture<T> getLinkedData(PublicKeyHash owner,\n                                                  SymmetricKey baseOrDataKey,\n                                                  Function<CborObject, T> fromCbor,\n                                                  Hasher h,\n                                                  NetworkAccess network,\n                                                  ProgressConsumer<Long> progress) {\n        return childrenOrData.getAndDecrypt(owner, baseOrDataKey, fromCbor, h, network, progress);\n    }\n\n    public CryptreeNode withParentLink(SymmetricKey parentKey, RelativeCapability newParentLink) {\n        FromParent parentBlock = getParentBlock(parentKey);\n        FromParent newParentBlock = new FromParent(Optional.of(newParentLink), parentBlock.properties);\n        return new CryptreeNode(lastCommittedHash, isDirectory, bats, fromBaseKey, childrenOrData,\n                PaddedCipherText.build(parentKey, newParentBlock, META_DATA_PADDING_BLOCKSIZE));\n    }\n\n    /**\n     *\n     * @param rBaseKey\n     * @return the mapkey of the next chunk of this file or folder if present\n     */\n    public CompletableFuture<Pair<byte[], Optional<Bat>>> getNextChunkLocation(SymmetricKey rBaseKey,\n                                                                               Optional<byte[]> streamSecret,\n                                                                               byte[] currentMapKey,\n                                                                               Optional<Bat> currentBat,\n                                                                               Hasher hasher) {\n        // It is important to use the hash based subsequent chunk location generator if the first chunk is labelled as such\n        // Otherwise we are vulnerable to a downgrade attack where a malicious user with a malicious client\n        // could upload a file that claimed to be using the new hash based generator, but the sequential links did not\n        if (streamSecret.isPresent())\n            return FileProperties.calculateNextMapKey(streamSecret.get(), currentMapKey, currentBat, hasher);\n\n        // Support directories or legacy files uploaded before hash based seeking was implemented\n        RelativeCapability nextChunk = getBaseBlock(rBaseKey).nextChunk;\n        return Futures.of(new Pair<>(nextChunk.getMapKey(), nextChunk.bat));\n    }\n\n    public CompletableFuture<Optional<RetrievedCapability>> getNextChunk(Snapshot version,\n                                                                         AbsoluteCapability us,\n                                                                         NetworkAccess network,\n                                                                         Optional<byte[]> streamSecret,\n                                                                         Hasher hasher) {\n        return getNextChunkLocation(us.rBaseKey, streamSecret, us.getMapKey(), us.bat, hasher)\n                .thenCompose(mapkeyAndBat -> {\n                    AbsoluteCapability nextChunkCap = us.withMapKey(mapkeyAndBat.left, mapkeyAndBat.right);\n                    return getNextChunk(version, nextChunkCap, network);\n                });\n    }\n\n    public CompletableFuture<Optional<RetrievedCapability>> getNextChunk(Snapshot version,\n                                                                         AbsoluteCapability nextChunkCap,\n                                                                         NetworkAccess network) {\n        return network.getMetadata(version.get(nextChunkCap.writer), nextChunkCap)\n                .thenApply(faOpt -> faOpt.map(fa -> new RetrievedCapability(nextChunkCap, fa)));\n    }\n\n    public static class CapAndSigner {\n        public final WritableAbsoluteCapability cap;\n        public final SigningPrivateKeyAndPublicHash signer;\n\n        public CapAndSigner(WritableAbsoluteCapability cap, SigningPrivateKeyAndPublicHash signer) {\n            if (! cap.writer.equals(signer.publicKeyHash))\n                throw new IllegalStateException(\"Signer doesn't match writer!\");\n            this.cap = cap;\n            this.signer = signer;\n        }\n\n        public CapAndSigner withCap(WritableAbsoluteCapability newCap) {\n            return new CapAndSigner(newCap, signer);\n        }\n    }\n\n    private CompletableFuture<Pair<Snapshot, CapAndSigner>> generateNewChildCap(\n            CapAndSigner currentChild,\n            CapAndSigner currentParent,\n            CapAndSigner newParent,\n            boolean rotateSigner,\n            NetworkAccess network,\n            Crypto crypto,\n            Snapshot version,\n            Committer committer) {\n        SymmetricKey baseRead = SymmetricKey.random();\n        SymmetricKey baseWrite = SymmetricKey.random();\n        byte[] newMapKey = crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH);\n        Optional<Bat> newBat = Optional.of(Bat.random(crypto.random));\n        if (currentChild.cap.writer.equals(currentParent.cap.writer)) {\n            WritableAbsoluteCapability newChildCap = new WritableAbsoluteCapability(currentChild.cap.owner,\n                    newParent.cap.writer, newMapKey, newBat, baseRead, baseWrite);\n            return Futures.of(new Pair<>(version, newParent.withCap(newChildCap)));\n        }\n\n        if (! rotateSigner) {\n            WritableAbsoluteCapability newChildCap = new WritableAbsoluteCapability(currentChild.cap.owner,\n                    currentChild.cap.writer, newMapKey, newBat, baseRead, baseWrite);\n            return Futures.of(new Pair<>(version, currentChild.withCap(newChildCap)));\n        }\n        SigningKeyPair newSignerPair = SigningKeyPair.random(crypto.random, crypto.signer);\n        return initAndAuthoriseSigner(currentChild.cap.owner, newParent.signer, newSignerPair, network, version, committer)\n                .thenApply(p -> new Pair<>(p.left, new CapAndSigner(new WritableAbsoluteCapability(currentChild.cap.owner,\n                    p.right.publicKeyHash, newMapKey, newBat, baseRead, baseWrite), p.right)));\n    }\n\n    public static CompletableFuture<Pair<Snapshot, SigningPrivateKeyAndPublicHash>> initAndAuthoriseSigner(\n            PublicKeyHash owner,\n            SigningPrivateKeyAndPublicHash parentSigner,\n            SigningKeyPair newSignerPair,\n            NetworkAccess network,\n            Snapshot version,\n            Committer committer) {\n        return parentSigner.secret.signMessage(newSignerPair.publicSigningKey.serialize()).thenCompose(signature -> IpfsTransaction.call(owner,\n                tid -> network.dhtClient.putSigningKey(signature, owner, parentSigner.publicKeyHash,\n                                newSignerPair.publicSigningKey, tid)\n                        .thenCompose(newSignerHash -> {\n                            SigningPrivateKeyAndPublicHash newSigner =\n                                    new SigningPrivateKeyAndPublicHash(newSignerHash, newSignerPair.secretSigningKey);\n                            CommittedWriterData cwd = version.get(parentSigner);\n                            return OwnerProof.build(newSigner, parentSigner.publicKeyHash)\n                                    .thenCompose(proof -> cwd.props.get().addOwnedKeyAndCommit(owner, parentSigner, proof, cwd.hash, cwd.sequence, network, committer, tid))\n                                    .thenCompose(v -> WriterData.createEmpty(owner, newSigner, network.dhtClient,\n                                                    network.hasher, tid)\n                                            .thenCompose(wd -> committer.commit(owner, newSigner, wd, new CommittedWriterData(MaybeMultihash.empty(), Optional.empty(), Optional.empty()), tid))\n                                            .thenApply(s -> new Pair<>(version.mergeAndOverwriteWith(v).mergeAndOverwriteWith(s), newSigner)));\n                        }), network.dhtClient));\n    }\n\n    public static CompletableFuture<Snapshot> deAuthoriseSigner(\n            PublicKeyHash owner,\n            SigningPrivateKeyAndPublicHash parentSigner,\n            PublicKeyHash signer,\n            NetworkAccess network,\n            Snapshot version,\n            Committer committer) {\n        PublicKeyHash parentWriter = parentSigner.publicKeyHash;\n        CommittedWriterData cwd = version.get(parentSigner);\n        return IpfsTransaction.call(owner, tid -> cwd.props.get().removeOwnedKey(owner, parentSigner, signer,\n                network.dhtClient, network.hasher)\n                .thenCompose(wd -> wd.equals(cwd.props.get()) ?\n                        Futures.of(version) :\n                        committer.commit(owner, parentSigner, wd, cwd, tid)), network.dhtClient)\n                .thenApply(committed -> version.withVersion(parentWriter, committed.get(parentWriter)));\n    }\n\n    /** Rotate the base read key, base write key, map key and signing key of a file or directory recursively\n     *  This operation requires size(file/subtree)/1000 free space to complete\n     *\n     * @param network\n     * @param crypto\n     * @param version\n     * @param committer\n     * @return\n     */\n    public CompletableFuture<Pair<Snapshot, WritableAbsoluteCapability>> rotateAllKeys(\n            boolean isFirstChunk,\n            CapAndSigner us,\n            CapAndSigner newUs,\n            CapAndSigner parent,\n            CapAndSigner newParent,\n            Optional<RelativeCapability> firstChunkOrParentCap,\n            Optional<byte[]> fileStreamSecret,\n            Optional<BatId> mirrorBat,\n            boolean rotateSigner,\n            NetworkAccess network,\n            Crypto crypto,\n            Snapshot version,\n            Committer committer) {\n        // If our new signer is different from the parent signer then we first need to add the new signer as an owned\n        // key to authorise it to write to our storage. We also need to keep track of old signing keys to remove\n        // at the end\n\n        FileProperties props = getProperties(getParentKey(us.cap.rBaseKey));\n        return getNextChunkLocation(us.cap.rBaseKey, props.streamSecret,\n                us.cap.getMapKey(), us.cap.bat, crypto.hasher)\n        .thenCompose(nextMapKeyAndBat -> {\n            WritableAbsoluteCapability nextChunkCap = us.cap.withMapKey(nextMapKeyAndBat.left, nextMapKeyAndBat.right);\n            Optional<byte[]> streamSecret = !isFirstChunk ?\n                    fileStreamSecret :\n                    isDirectory ?\n                            Optional.empty() :\n                            Optional.of(crypto.random.randomBytes(32));\n            CompletableFuture<Pair<byte[], Optional<Bat>>> newNextChunkMapKeyFut = streamSecret.map(stream ->\n                    FileProperties.calculateNextMapKey(stream, newUs.cap.getMapKey(), newUs.cap.bat, crypto.hasher))\n                    .orElseGet(() -> Futures.of(new Pair<>(crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH), Optional.of(Bat.random(crypto.random)))));\n\n            return newNextChunkMapKeyFut.thenCompose(newNextChunkMapKeyAndBat -> {\n                WritableAbsoluteCapability newNextChunkCap = newUs.cap.withMapKey(newNextChunkMapKeyAndBat.left, newNextChunkMapKeyAndBat.right);\n                SymmetricKey newParentKey = isFirstChunk ? SymmetricKey.random() : firstChunkOrParentCap.get().rBaseKey;\n                // only first chunks have a link to their parent\n                Optional<RelativeCapability> newParentCap = isFirstChunk ?\n                        firstChunkOrParentCap.map(cap -> cap.withWritingKey(\n                                newParent.cap.writer.equals(newUs.cap.writer) ?\n                                        Optional.empty() : Optional.of(newParent.cap.writer))) :\n                        Optional.empty();\n\n                Optional<RelativeCapability> childCapToUs = isFirstChunk ?\n                        Optional.of(new RelativeCapability(Optional.empty(), newUs.cap.getMapKey(), newUs.cap.bat, newParentKey, Optional.empty())) :\n                        firstChunkOrParentCap;\n\n                // do for subsequent chunks first\n                return version.withWriter(us.cap.owner, us.cap.writer, network)\n                        .thenCompose(s -> getNextChunk(s, nextChunkCap, network)\n                                .thenCompose(opt -> {\n                                    if (!opt.isPresent())\n                                        return Futures.of(new Pair<>(s, newNextChunkCap));\n                                    return opt.get().fileAccess.rotateAllKeys(false,\n                                            us.withCap(nextChunkCap),\n                                            newUs.withCap(newNextChunkCap),\n                                            parent,\n                                            newParent,\n                                            childCapToUs,\n                                            streamSecret,\n                                            mirrorBat,\n                                            rotateSigner,\n                                            network,\n                                            crypto,\n                                            s,\n                                            committer);\n                                })).thenCompose(nextChunk -> {\n                            if (isDirectory()) {\n                                Set<NamedAbsoluteCapability> empty = Collections.emptySet();\n                                return getDirectChildren(network, us.cap, version)\n                                        .thenCompose(children -> Futures.reduceAll(children,\n                                                new Pair<>(nextChunk.left, empty),\n                                                (p, c) -> {\n                                                    SigningPrivateKeyAndPublicHash childSigner = c.fileAccess.getSigner(\n                                                            c.capability.rBaseKey,\n                                                            c.capability.wBaseKey.get(),\n                                                            Optional.of(us.signer));\n                                                    CapAndSigner child = new CapAndSigner((WritableAbsoluteCapability) c.capability,\n                                                            childSigner);\n                                                    return generateNewChildCap(child, us, newUs, rotateSigner, network, crypto, p.left, committer)\n                                                            .thenCompose(newChild -> c.fileAccess.rotateAllKeys(\n                                                                    true,\n                                                                    child,\n                                                                    newChild.right,\n                                                                    us,\n                                                                    newUs,\n                                                                    childCapToUs,\n                                                                    Optional.empty(),\n                                                                    mirrorBat,\n                                                                    rotateSigner,\n                                                                    network,\n                                                                    crypto,\n                                                                    newChild.left,\n                                                                    committer)\n                                                                    .thenApply(updatedChild -> new Pair<>(updatedChild.left,\n                                                                            Stream.concat(p.right.stream(),\n                                                                                    Stream.of(new NamedAbsoluteCapability(c.getProperties().name,\n                                                                                            updatedChild.right,\n                                                                                            Optional.of(c.getProperties().isDirectory),\n                                                                                            Optional.of(c.getProperties().mimeType),\n                                                                                            Optional.of(c.getProperties().created))))\n                                                                                    .collect(Collectors.toSet()))));\n                                                },\n                                                (x, y) -> new Pair<>(x.left.merge(y.left),\n                                                        Stream.concat(x.right.stream(), y.right.stream()).collect(Collectors.toSet()))))\n                                        .thenCompose(newChildCaps -> {\n                                            // Now rotate the current chunk, with the new child pointers\n                                            Optional<SigningPrivateKeyAndPublicHash> signer = !isFirstChunk |\n                                                    newUs.cap.writer.equals(newParent.cap.writer) ?\n                                                    Optional.empty() :\n                                                    Optional.of(newUs.signer);\n                                            RelativeCapability nextChunkRel = RelativeCapability.buildSubsequentChunk(\n                                                    nextChunk.right.getMapKey(), nextChunk.right.bat, newUs.cap.rBaseKey);\n                                            List<NamedRelativeCapability> relativeChildLinks = newChildCaps.right.stream()\n                                                    .map(n -> new NamedRelativeCapability(n.name, newUs.cap.relativise(n.cap), n.isDir, n.mimetype, n.created))\n                                                    .collect(Collectors.toList());\n                                            return createDir(MaybeMultihash.empty(), newUs.cap.rBaseKey,\n                                                    newUs.cap.wBaseKey.get(), signer, props, newParentCap, newParentKey,\n                                                    nextChunkRel, new ChildrenLinks(relativeChildLinks), newUs.cap.bat, mirrorBat, crypto.random, crypto.hasher)\n                                                    .thenCompose(newUsDir ->\n                                                            IpfsTransaction.call(us.cap.owner, tid -> newUsDir.commit(newChildCaps.left,\n                                                                    committer, newUs.cap, newUs.signer, network, tid), network.dhtClient));\n                                        });\n                            } else {\n                                Optional<SymmetricLinkToSigner> signerLink = !isFirstChunk |\n                                        newUs.cap.writer.equals(newParent.cap.writer) ?\n                                        Optional.empty() :\n                                        Optional.of(SymmetricLinkToSigner.fromPair(newUs.cap.wBaseKey.get(), newUs.signer));\n                                SymmetricKey dataKey = getDataKey(us.cap.rBaseKey).makeDirty();\n                                CryptreeNode newFileChunk = createFile(MaybeMultihash.empty(), signerLink, newUs.cap.rBaseKey,\n                                        dataKey,\n                                        streamSecret.map(props::withNewStreamSecret).orElse(props),\n                                        this.childrenOrData, newParentCap, RelativeCapability.buildSubsequentChunk(\n                                                nextChunk.right.getMapKey(), nextChunk.right.bat, nextChunk.right.rBaseKey),\n                                        newUs.cap.bat, mirrorBat, crypto.random);\n                                return IpfsTransaction.call(us.cap.owner, tid -> newFileChunk.commit(nextChunk.left,\n                                        committer, newUs.cap, newUs.signer, network, tid), network.dhtClient);\n                            }\n                        }).thenApply(v -> new Pair<>(v, newUs.cap));\n            });\n        });\n    }\n\n    public Optional<BatId> mirrorBatId() {\n        if (bats.size() < 2)\n            return Optional.empty();\n        return Optional.of(bats.get(bats.size() - 1));\n    }\n\n    public CompletableFuture<Snapshot> cleanAndCommit(Snapshot current,\n                                                      Committer committer,\n                                                      WritableAbsoluteCapability cap,\n                                                      WritableAbsoluteCapability newCap,\n                                                      Optional<byte[]> streamSecret,\n                                                      Optional<byte[]> newStreamSecret,\n                                                      SigningPrivateKeyAndPublicHash writer,\n                                                      SymmetricKey newDataKey,\n                                                      Location parentLocation,\n                                                      Optional<Bat> parentBat,\n                                                      SymmetricKey parentParentKey,\n                                                      NetworkAccess network,\n                                                      Crypto crypto) {\n        FileProperties props = getProperties(cap.rBaseKey);\n        Optional<byte[]> finalNewSecret = streamSecret.map(x -> newStreamSecret\n                .orElseGet(() -> crypto.random.randomBytes(32)));\n        FileProperties updatedFileProperties = finalNewSecret\n                .map(ns -> props.withNewStreamSecret(ns))\n                .orElse(props);\n        return getNextChunkLocation(cap.rBaseKey, streamSecret, cap.getMapKey(), cap.bat, crypto.hasher)\n                .thenCompose(nextMapKey -> {\n                    WritableAbsoluteCapability nextCap = cap.withMapKey(nextMapKey.left, nextMapKey.right);\n                    return getNextChunkLocation(cap.rBaseKey, finalNewSecret, newCap.getMapKey(), newCap.bat, crypto.hasher).thenCompose(newNextMapKey -> {\n                        WritableAbsoluteCapability newNextCap = cap.withMapKey(newNextMapKey.left, newNextMapKey.right);\n\n                        return retriever(cap.rBaseKey, streamSecret, cap.getMapKey(), cap.bat, crypto.hasher)\n                                .thenCompose(retriever ->\n                                        retriever.getFile(current.get(writer), network, crypto, cap, streamSecret, props.size, committedHash(), 1, x -> {})\n                                                .thenCompose(data -> {\n                                                    int chunkSize = (int) Math.min(props.size, Chunk.MAX_SIZE);\n                                                    byte[] chunkData = new byte[chunkSize];\n                                                    return data.readIntoArray(chunkData, 0, chunkSize)\n                                                            .thenCompose(read -> {\n                                                                byte[] nonce = cap.rBaseKey.createNonce();\n                                                                byte[] mapKey = newCap.getMapKey();\n\n                                                                Chunk chunk = new Chunk(chunkData, newDataKey, mapKey, nonce);\n                                                                LocatedChunk locatedChunk = new LocatedChunk(newCap.getLocation(), newCap.bat, lastCommittedHash, chunk);\n\n                                                                return FileUploader.uploadChunk(current, committer, writer, updatedFileProperties, parentLocation,\n                                                                        parentBat, parentParentKey, cap.rBaseKey, locatedChunk,\n                                                                        newNextCap.getLocation(), newNextCap.bat,\n                                                                        getWriterLink(cap.rBaseKey), mirrorBatId(),\n                                                                        crypto.random, crypto.hasher, network, x -> {});\n                                                            });\n                                                }).thenCompose(updated -> network.getMetadata(updated.get(nextCap.writer), nextCap)\n                                                .thenCompose(mOpt -> {\n                                                    if (!mOpt.isPresent())\n                                                        return CompletableFuture.completedFuture(updated);\n                                                    return mOpt.get().cleanAndCommit(updated, committer, nextCap, newNextCap,\n                                                            streamSecret, updatedFileProperties.streamSecret, writer, newDataKey,\n                                                            parentLocation, parentBat, parentParentKey, network, crypto)\n                                                            .thenCompose(snapshot ->\n                                                                    IpfsTransaction.call(cap.owner, tid -> network.deleteChunk(snapshot, committer, mOpt.get(),\n                                                                            cap.owner, nextCap.getMapKey(), writer, tid), network.dhtClient));\n                                                })));\n                    });\n                });\n    }\n\n    public CompletableFuture<Snapshot> addChildrenAndCommit(Snapshot current,\n                                                            Committer committer,\n                                                            List<NamedRelativeCapability> targetCAPs,\n                                                            WritableAbsoluteCapability us,\n                                                            SigningPrivateKeyAndPublicHash signer,\n                                                            Optional<BatId> mirrorBat,\n                                                            NetworkAccess network,\n                                                            Crypto crypto) {\n        if (targetCAPs.isEmpty())\n            return Futures.of(current);\n        // This assumes that the new child names do not already exist in this dir\n        // Make sure subsequent blobs use a different transaction to obscure linkage of different parts of this dir\n        return getDirectChildren(us, current, network).thenCompose(children -> {\n            if (children.size() + targetCAPs.size() > getMaxChildLinksPerBlob()) {\n                return getNextChunk(current, us, network, Optional.empty(), crypto.hasher).thenCompose(nextMetablob -> {\n\n                    if (nextMetablob.isPresent()) {\n                        AbsoluteCapability nextPointer = nextMetablob.get().capability;\n                        CryptreeNode nextBlob = nextMetablob.get().fileAccess;\n                        return nextBlob.addChildrenAndCommit(current, committer, targetCAPs,\n                                nextPointer.toWritable(us.wBaseKey.get()), signer, mirrorBat, network, crypto);\n                    } else {\n                        // first fill this directory, then overflow into a new one\n                        int freeSlots = getMaxChildLinksPerBlob() - children.size();\n                        List<NamedRelativeCapability> addToUs = targetCAPs.subList(0, freeSlots);\n                        List<NamedRelativeCapability> addToNext = targetCAPs.subList(freeSlots, targetCAPs.size());\n                        return (addToUs.isEmpty() ?\n                                CompletableFuture.completedFuture(current) :\n                                addChildrenAndCommit(current, committer, addToUs, us, signer, mirrorBat, network, crypto))\n                                .thenCompose(newBase -> {\n                                    // create and upload new metadata blob\n                                    SymmetricKey nextSubfoldersKey = us.rBaseKey;\n                                    SymmetricKey ourParentKey = getParentKey(us.rBaseKey);\n                                    Optional<RelativeCapability> parentCap = getParentBlock(ourParentKey).parentLink;\n                                    RelativeCapability nextChunk = RelativeCapability.buildSubsequentChunk(\n                                            crypto.random.randomBytes(32), Optional.of(Bat.random(crypto.random)), nextSubfoldersKey);\n                                    List<NamedRelativeCapability> addToNextChunk = addToNext.stream()\n                                            .limit(getMaxChildLinksPerBlob())\n                                            .collect(Collectors.toList());\n                                    List<NamedRelativeCapability> remaining = addToNext.stream()\n                                            .skip(getMaxChildLinksPerBlob())\n                                            .collect(Collectors.toList());\n                                    return getNextChunkLocation(us.rBaseKey, Optional.empty(), null, Optional.empty(), null)\n                                            .thenCompose(nextMapKeyAndBat -> CryptreeNode.createDir(MaybeMultihash.empty(), nextSubfoldersKey,\n                                                    null, Optional.empty(), FileProperties.EMPTY, parentCap,\n                                                    ourParentKey, nextChunk, new ChildrenLinks(addToNextChunk), nextMapKeyAndBat.right, mirrorBat, crypto.random, crypto.hasher)\n                                                    .thenCompose(next -> {\n                                                        WritableAbsoluteCapability nextPointer = new WritableAbsoluteCapability(us.owner,\n                                                                us.writer, nextMapKeyAndBat.left, nextMapKeyAndBat.right, nextSubfoldersKey, us.wBaseKey.get());\n                                                        return IpfsTransaction.call(us.owner,\n                                                                tid -> next.commit(newBase, committer, nextPointer, signer, network, tid)\n                                                                        .thenCompose(updatedBase ->\n                                                                                network.getMetadata(updatedBase.get(nextPointer.writer), nextPointer)\n                                                                                        .thenCompose(nextOpt -> nextOpt.get().\n                                                                                                addChildrenAndCommit(updatedBase, committer, remaining,\n                                                                                                        nextPointer, signer, mirrorBat, network, crypto)))\n                                                                , network.dhtClient);\n                                                    }));\n                                });\n                    }\n                });\n            } else {\n                Set<String> existing = children.stream().map(c -> c.name.name).collect(Collectors.toSet());\n                HashSet<String> toAdd = new HashSet<>(targetCAPs.stream().map(c -> c.name.name).collect(Collectors.toSet()));\n                toAdd.retainAll(existing);\n                if (! toAdd.isEmpty())\n                    throw new IllegalStateException(\"Name conflict in target directory \" + toAdd);\n                ArrayList<NamedRelativeCapability> newFiles = new ArrayList<>(children);\n                newFiles.addAll(targetCAPs);\n\n                return IpfsTransaction.call(us.owner,\n                        tid -> withChildren(us.rBaseKey, new ChildrenLinks(newFiles), crypto.random, crypto.hasher)\n                                .thenCompose(d ->\n                                        d.commit(current, committer, us, signer, network, tid)),\n                        network.dhtClient);\n            }\n        });\n    }\n\n    public CompletableFuture<Snapshot> mkdir(Snapshot base,\n                                             Committer committer,\n                                             String name,\n                                             NetworkAccess network,\n                                             WritableAbsoluteCapability us,\n                                             Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                             Optional<SymmetricKey> optionalBaseKey,\n                                             Optional<SymmetricKey> optionalBaseWriteKey,\n                                             Optional<byte[]> desiredMapKey,\n                                             Optional<Bat> desiredBat,\n                                             boolean isSystemFolder,\n                                             Optional<BatId> mirrorBat,\n                                             Crypto crypto) {\n        SymmetricKey dirReadKey = optionalBaseKey.orElseGet(SymmetricKey::random);\n        SymmetricKey dirWriteKey = optionalBaseWriteKey.orElseGet(SymmetricKey::random);\n        byte[] dirMapKey = desiredMapKey.orElseGet(() -> crypto.random.randomBytes(32)); // root will be stored under this in the tree\n        Optional<Bat> dirBat = Optional.of(desiredBat.orElseGet(() -> Bat.random(crypto.random)));\n        SymmetricKey ourParentKey = this.getParentKey(us.rBaseKey);\n        RelativeCapability ourCap = new RelativeCapability(Optional.empty(), us.getMapKey(), us.bat, ourParentKey, Optional.empty());\n        RelativeCapability nextChunk = new RelativeCapability(Optional.empty(), crypto.random.randomBytes(32),\n                Optional.of(Bat.random(crypto.random)), dirReadKey, Optional.empty());\n        WritableAbsoluteCapability childCap = us.withBaseKey(dirReadKey).withBaseWriteKey(dirWriteKey).withMapKey(dirMapKey, dirBat);\n        LocalDateTime timestamp = LocalDateTime.now(ZoneOffset.UTC);\n        return CryptreeNode.createEmptyDir(MaybeMultihash.empty(), dirReadKey, dirWriteKey, Optional.empty(),\n                new FileProperties(name, true, false, \"\", 0, timestamp, timestamp, isSystemFolder,\n                        Optional.empty(), Optional.empty(), Optional.empty()), Optional.of(ourCap), SymmetricKey.random(), nextChunk, dirBat, mirrorBat, crypto.random, crypto.hasher)\n                .thenCompose(child -> {\n\n                    SymmetricLink toChildWriteKey = SymmetricLink.fromPair(us.wBaseKey.get(), dirWriteKey);\n                    // Use two transactions to not expose the child linkage\n                    return IpfsTransaction.call(us.owner,\n                            tid -> child.commit(base, committer, childCap, entryWriter, network, tid), network.dhtClient)\n                            .thenCompose(updatedBase -> {\n                                RelativeCapability cap = new RelativeCapability(Optional.empty(), dirMapKey, dirBat, dirReadKey, Optional.of(toChildWriteKey));\n                                NamedRelativeCapability subdirPointer = new NamedRelativeCapability(new PathElement(name), cap, Optional.of(true), Optional.of(\"\"), Optional.of(timestamp));\n                                SigningPrivateKeyAndPublicHash signer = getSigner(us.rBaseKey, us.wBaseKey.get(), entryWriter);\n                                return addChildrenAndCommit(updatedBase, committer, Arrays.asList(subdirPointer), us, signer, mirrorBat, network, crypto);\n                            });\n                });\n    }\n\n    public CompletableFuture<Snapshot> updateChildLink(Snapshot base,\n                                                       Committer committer,\n                                                       WritableAbsoluteCapability ourPointer,\n                                                       SigningPrivateKeyAndPublicHash signer,\n                                                       RetrievedCapability original,\n                                                       RetrievedCapability modified,\n                                                       NetworkAccess network,\n                                                       SafeRandom random,\n                                                       Hasher hasher) {\n        NamedAbsoluteCapability newChild = new NamedAbsoluteCapability(modified.getProperties().name,\n                modified.capability,\n                Optional.of(original.getProperties().isDirectory),\n                Optional.of(original.getProperties().mimeType),\n                Optional.of(original.getProperties().created));\n        return updateChildLinks(base, committer, ourPointer, signer,\n                Arrays.asList(new Pair<>(original.capability, newChild)), network, random, hasher);\n    }\n\n    public CompletableFuture<Snapshot> updateChildLink(Snapshot base,\n                                                       Committer committer,\n                                                       WritableAbsoluteCapability ourPointer,\n                                                       SigningPrivateKeyAndPublicHash signer,\n                                                       AbsoluteCapability originalCap,\n                                                       NamedAbsoluteCapability modifiedCap,\n                                                       NetworkAccess network,\n                                                       SafeRandom random,\n                                                       Hasher hasher) {\n        return updateChildLinks(base, committer, ourPointer, signer,\n                Arrays.asList(new Pair<>(originalCap, modifiedCap)), network, random, hasher);\n    }\n\n    public CompletableFuture<Snapshot> updateChildLinks(Snapshot base,\n                                                        Committer committer,\n                                                        WritableAbsoluteCapability ourPointer,\n                                                        SigningPrivateKeyAndPublicHash signer,\n                                                        Collection<Pair<AbsoluteCapability, NamedAbsoluteCapability>> childCasPairs,\n                                                        NetworkAccess network,\n                                                        SafeRandom random,\n                                                        Hasher hasher) {\n        return getDirectChildren(network, ourPointer, base).thenCompose(children -> {\n\n            Set<Location> existingChildLocs = children.stream()\n                    .map(r -> r.capability.getLocation())\n                    .collect(Collectors.toSet());\n\n            Map<Location, NamedAbsoluteCapability> oldToNew = childCasPairs.stream()\n                    .collect(Collectors.toMap(p -> p.left.getLocation(), p -> p.right));\n\n            List<NamedRelativeCapability> unchanged = children.stream()\n                    .filter(e -> ! oldToNew.containsKey(e.capability.getLocation()))\n                    .map(c -> new NamedRelativeCapability(c.getProperties().name, ourPointer.relativise(c.capability),\n                            Optional.of(c.getProperties().isDirectory),\n                            Optional.of(c.getProperties().mimeType),\n                            Optional.of(c.getProperties().created)))\n                    .collect(Collectors.toList());\n\n            List<NamedRelativeCapability> updatedLinks = children.stream()\n                    .filter(e -> oldToNew.containsKey(e.capability.getLocation()))\n                    .map(c -> {\n                        NamedAbsoluteCapability newTarget = oldToNew.get(c.capability.getLocation());\n                        return new NamedRelativeCapability(newTarget.name,\n                                ourPointer.relativise(newTarget.cap),\n                                newTarget.isDir, newTarget.mimetype, newTarget.created);\n                    })\n                    .collect(Collectors.toList());\n\n            Collection<Pair<AbsoluteCapability, NamedAbsoluteCapability>> remaining = childCasPairs.stream()\n                    .filter(p -> ! existingChildLocs.contains(p.left.getLocation()))\n                    .collect(Collectors.toSet());\n\n            return (! updatedLinks.isEmpty() ?\n                    IpfsTransaction.call(ourPointer.owner,\n                            tid -> withChildren(ourPointer.rBaseKey, new ChildrenLinks(Stream.concat(unchanged.stream(), updatedLinks.stream())\n                                    .collect(Collectors.toList())), random, hasher)\n                                    .thenCompose(d -> d.commit(base, committer, ourPointer, signer, network, tid)),\n                            network.dhtClient) :\n                    CompletableFuture.completedFuture(base)).thenCompose(\n                    updated -> remaining.isEmpty() ?  CompletableFuture.completedFuture(updated) :\n                            getNextChunk(base, ourPointer, network, Optional.empty(), hasher)\n                                    .thenCompose(nextOpt -> {\n                                        return getNextChunkLocation(ourPointer.rBaseKey, Optional.empty(), ourPointer.getMapKey(), ourPointer.bat, hasher)\n                                                .thenCompose(nextChunkLocationAndBat -> {\n                                                    AbsoluteCapability nextChunkCap = ourPointer.withMapKey(nextChunkLocationAndBat.left, nextChunkLocationAndBat.right);\n                                                    WritableAbsoluteCapability writableNextPointer = nextChunkCap.toWritable(ourPointer.wBaseKey.get());\n                                                    if (! nextOpt.isPresent())\n                                                        throw new IllegalStateException(\"Child link not present!\");\n                                                    return nextOpt.get().fileAccess.updateChildLinks(updated, committer,\n                                                            writableNextPointer, signer, remaining, network, random, hasher);\n                                                });\n                                    }));\n        });\n    }\n\n    public CompletableFuture<Snapshot> removeChildren(Snapshot current,\n                                                      Committer committer,\n                                                      List<AbsoluteCapability> childrenToRemove,\n                                                      WritableAbsoluteCapability ourPointer,\n                                                      Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                                      NetworkAccess network,\n                                                      SafeRandom random,\n                                                      Hasher hasher) {\n        Set<Location> locsToRemove = childrenToRemove.stream()\n                .map(r -> r.getLocation())\n                .collect(Collectors.toSet());\n        return getDirectChildrenCapabilities(ourPointer, current, network).thenCompose(childCaps -> {\n            List<NamedRelativeCapability> withRemoval = childCaps.stream()\n                    .filter(e -> ! locsToRemove.contains(e.cap.getLocation()))\n                    .map(c -> new NamedRelativeCapability(new PathElement(c.name.name),\n                            ourPointer.relativise(c.cap), c.isDir, c.mimetype, c.created))\n                    .collect(Collectors.toList());\n            Set<Location> kidLocs = childCaps.stream()\n                    .map(r -> r.cap.getLocation())\n                    .collect(Collectors.toSet());\n\n            List<AbsoluteCapability> remaining = childrenToRemove.stream()\n                    .filter(c -> ! kidLocs.contains(c.getLocation()))\n                    .collect(Collectors.toList());\n\n            return IpfsTransaction.call(ourPointer.owner,\n                    tid -> withChildren(ourPointer.rBaseKey, new ChildrenLinks(withRemoval), random, hasher)\n                            .thenCompose(d -> d.commit(current, committer, ourPointer, entryWriter, network, tid)),\n                    network.dhtClient).thenCompose(s -> {\n                        if (remaining.isEmpty())\n                            return Futures.of(s);\n                        return getNextChunkLocation(ourPointer.rBaseKey, Optional.empty(), ourPointer.getMapKey(), ourPointer.bat, hasher)\n                                .thenCompose(nextLoc -> {\n                                    SigningPrivateKeyAndPublicHash signer = getSigner(ourPointer.rBaseKey, ourPointer.wBaseKey.get(), entryWriter);\n                                    WritableAbsoluteCapability nextChunkCap = ourPointer.withMapKey(nextLoc.left, nextLoc.right);\n                                    return getNextChunk(s, nextChunkCap, network)\n                                            .thenCompose(next -> {\n                                                if (next.isEmpty())\n                                                    throw new IllegalStateException(\"No subsequent dir chunk\");\n                                                RetrievedCapability rc = next.get();\n                                                return rc.fileAccess.removeChildren(s, committer, remaining, nextChunkCap, Optional.of(signer), network, random, hasher);\n                                            });\n                                });\n            });\n        });\n    }\n\n    public CompletableFuture<Snapshot> commit(Snapshot current,\n                                              Committer committer,\n                                              WritableAbsoluteCapability us,\n                                              Optional<SigningPrivateKeyAndPublicHash> entryWriter,\n                                              NetworkAccess network,\n                                              TransactionId tid) {\n        return commit(current, committer, us, getSigner(us.rBaseKey, us.wBaseKey.get(), entryWriter), network, tid);\n    }\n\n    public CompletableFuture<Snapshot> commit(Snapshot current,\n                                              Committer committer,\n                                              WritableAbsoluteCapability us,\n                                              SigningPrivateKeyAndPublicHash signer,\n                                              NetworkAccess network,\n                                              TransactionId tid) {\n        return network.uploadChunk(current, committer, this, us.owner, us.getMapKey(), signer, tid);\n    }\n\n    public boolean hasParentLink(SymmetricKey baseKey) {\n        SymmetricKey parentKey = getParentKey(baseKey);\n        return getParentBlock(parentKey).parentLink.isPresent();\n    }\n\n    public Optional<RelativeCapability> getParentCapability(SymmetricKey baseKey) {\n        SymmetricKey parentKey = getParentKey(baseKey);\n        return getParentBlock(parentKey).parentLink;\n    }\n\n    public CompletableFuture<RetrievedCapability> getParent(PublicKeyHash owner,\n                                                            PublicKeyHash writer,\n                                                            SymmetricKey baseKey,\n                                                            NetworkAccess network,\n                                                            Snapshot version) {\n        SymmetricKey parentKey = getParentKey(baseKey);\n        Optional<RelativeCapability> parentLink = getParentBlock(parentKey).parentLink;\n        if (! parentLink.isPresent())\n            return CompletableFuture.completedFuture(null);\n\n        RelativeCapability relCap = parentLink.get();\n        return network.retrieveMetadata(new AbsoluteCapability(owner, relCap.writer.orElse(writer), relCap.getMapKey(),\n                relCap.bat, relCap.rBaseKey, Optional.empty()), version).thenApply(res -> {\n            RetrievedCapability retrievedCapability = res.get();\n            return retrievedCapability;\n        });\n    }\n\n    public static CompletableFuture<Pair<CryptreeNode, List<FragmentWithHash>>> createFile(\n            MaybeMultihash existingHash,\n            PublicKeyHash ourWriter,\n            SymmetricKey parentKey,\n            SymmetricKey dataKey,\n            FileProperties props,\n            byte[] chunkData,\n            Location parentLocation,\n            Optional<Bat> parentBat,\n            SymmetricKey parentparentKey,\n            RelativeCapability nextChunk,\n            Optional<Bat> inlineBat,\n            Optional<BatId> mirrorBat,\n            SafeRandom random,\n            Hasher hasher,\n            boolean allowArrayCache) {\n        return FragmentedPaddedCipherText.build(dataKey, new CborObject.CborByteArray(chunkData),\n                        MIN_FRAGMENT_SIZE, Fragment.MAX_LENGTH, mirrorBat, random, hasher, allowArrayCache)\n                .thenApply(linksAndData -> {\n                    RelativeCapability toParent = new RelativeCapability(\n                            parentLocation.writer.equals(ourWriter) ? Optional.empty() : Optional.of(parentLocation.writer),\n                            parentLocation.getMapKey(),\n                            parentBat,\n                            parentparentKey,\n                            Optional.empty());\n                    CryptreeNode cryptree = createFile(existingHash, Optional.empty(), parentKey, dataKey, props,\n                            linksAndData.left, toParent, nextChunk, inlineBat, mirrorBat, random);\n                    return new Pair<>(cryptree, linksAndData.right);\n                });\n    }\n\n    public static CryptreeNode createFile(MaybeMultihash existingHash,\n                                          Optional<SymmetricLinkToSigner> signerLink,\n                                          SymmetricKey parentKey,\n                                          SymmetricKey dataKey,\n                                          FileProperties props,\n                                          FragmentedPaddedCipherText data,\n                                          RelativeCapability toParentDir,\n                                          RelativeCapability nextChunk,\n                                          Optional<Bat> inlineBat,\n                                          Optional<BatId> mirrorBat,\n                                          SafeRandom random) {\n        return createFile(existingHash, signerLink, parentKey, dataKey, props, data, Optional.of(toParentDir),\n                nextChunk, inlineBat, mirrorBat, random);\n    }\n\n    public static CryptreeNode createSubsequentFileChunk(MaybeMultihash existingHash,\n                                                         Optional<SymmetricLinkToSigner> signerLink,\n                                                         SymmetricKey parentKey,\n                                                         SymmetricKey dataKey,\n                                                         FileProperties props,\n                                                         FragmentedPaddedCipherText data,\n                                                         RelativeCapability nextChunk,\n                                                         Optional<Bat> inlineBat,\n                                                         Optional<BatId> mirrorBat,\n                                                         SafeRandom random) {\n        return createFile(existingHash, signerLink, parentKey, dataKey, props, data, Optional.empty(), nextChunk,\n                inlineBat, mirrorBat, random);\n    }\n\n    private static CryptreeNode createFile(MaybeMultihash existingHash,\n                                           Optional<SymmetricLinkToSigner> signerLink,\n                                           SymmetricKey parentKey,\n                                           SymmetricKey dataKey,\n                                           FileProperties props,\n                                           FragmentedPaddedCipherText data,\n                                           Optional<RelativeCapability> toParentDir,\n                                           RelativeCapability nextChunk,\n                                           Optional<Bat> inlineBat,\n                                           Optional<BatId> mirrorBat,\n                                           SafeRandom random) {\n        if (parentKey.equals(dataKey))\n            throw new IllegalStateException(\"A file's base key and data key must be different!\");\n        FromBase fromBase = new FromBase(dataKey, signerLink, nextChunk);\n        FromParent fromParent = new FromParent(toParentDir, props);\n\n        List<BatId> bats = inlineBat.isEmpty() ?\n                Collections.emptyList() :\n                Stream.concat(inlineBat.stream().map(BatId::inline), mirrorBat.stream()).collect(Collectors.toList());\n        PaddedCipherText encryptedBaseBlock = PaddedCipherText.build(parentKey, fromBase, BASE_BLOCK_PADDING_BLOCKSIZE);\n        PaddedCipherText encryptedParentBlock = PaddedCipherText.build(parentKey, fromParent, META_DATA_PADDING_BLOCKSIZE);\n        return new CryptreeNode(existingHash, false, bats, encryptedBaseBlock, data, encryptedParentBlock);\n    }\n\n    public static CompletableFuture<Snapshot> createAndCommitLink(FileWrapper parent,\n                                                                  WritableAbsoluteCapability target,\n                                                                  FileProperties targetProps,\n                                                                  WritableAbsoluteCapability linkCap,\n                                                                  SymmetricKey parentKey,\n                                                                  Optional<BatId> mirrorBat,\n                                                                  Crypto crypto,\n                                                                  NetworkAccess network,\n                                                                  Snapshot startVersion,\n                                                                  Committer committer) {\n        return createLink(parent, linkCap, target, targetProps, parentKey, mirrorBat, crypto)\n                .thenCompose(link -> IpfsTransaction.call(parent.owner(), tid -> link.commit(startVersion, committer,\n                        linkCap, parent.signingPair(), network, tid), network.dhtClient));\n    }\n\n    public static CompletableFuture<DirAndChildren> createLink(FileWrapper parent,\n                                                               WritableAbsoluteCapability linkCap,\n                                                               WritableAbsoluteCapability target,\n                                                               FileProperties targetProps,\n                                                               SymmetricKey parentKey,\n                                                               Optional<BatId> mirrorBat,\n                                                               Crypto crypto) {\n        RelativeCapability toTarget = linkCap.relativise(target);\n        RelativeCapability nextChunk = RelativeCapability.buildSubsequentChunk(\n                crypto.random.randomBytes(RelativeCapability.MAP_KEY_LENGTH), Optional.of(Bat.random(crypto.random)), linkCap.rBaseKey);\n        SymmetricKey parentParentKey = parent.getParentKey();\n        WritableAbsoluteCapability parentCap = parent.writableFilePointer();\n        RelativeCapability toParent = new RelativeCapability(Optional.empty(), parentCap.getMapKey(),\n                parentCap.bat, parentParentKey, Optional.empty());\n        // The link must be in the same writing subspace as the parent\n        Optional<SigningPrivateKeyAndPublicHash> empty = Optional.empty();\n        return createDir(MaybeMultihash.empty(), linkCap.rBaseKey, linkCap.wBaseKey.get(), empty, targetProps.asLink(),\n                Optional.of(toParent), parentKey, nextChunk,\n                new ChildrenLinks(Collections.singletonList(new NamedRelativeCapability(new PathElement(targetProps.name), toTarget,\n                        Optional.of(targetProps.isDirectory), Optional.of(targetProps.mimeType), Optional.of(targetProps.created)))),\n                linkCap.bat, mirrorBat, crypto.random, crypto.hasher);\n    }\n\n    public static CompletableFuture<DirAndChildren> createEmptyDir(\n            MaybeMultihash lastCommittedHash,\n            SymmetricKey rBaseKey,\n            SymmetricKey wBaseKey,\n            Optional<SigningPrivateKeyAndPublicHash> signingPair,\n            FileProperties props,\n            Optional<RelativeCapability> parentCap,\n            SymmetricKey parentKey,\n            RelativeCapability nextChunk,\n            Optional<Bat> inlineBat,\n            Optional<BatId> mirrorBat,\n            SafeRandom random,\n            Hasher hasher) {\n        return createDir(lastCommittedHash, rBaseKey, wBaseKey, signingPair, props, parentCap, parentKey, nextChunk,\n                ChildrenLinks.empty(), inlineBat, mirrorBat, random, hasher);\n    }\n\n    public static CompletableFuture<DirAndChildren> createDir(\n            MaybeMultihash lastCommittedHash,\n            SymmetricKey rBaseKey,\n            SymmetricKey wBaseKey,\n            Optional<SigningPrivateKeyAndPublicHash> signingPair,\n            FileProperties props,\n            Optional<RelativeCapability> parentCap,\n            SymmetricKey parentKey,\n            RelativeCapability nextChunk,\n            ChildrenLinks children,\n            Optional<Bat> inlineBat,\n            Optional<BatId> mirrorBat,\n            SafeRandom random,\n            Hasher hasher) {\n        if (rBaseKey.equals(parentKey))\n            throw new IllegalStateException(\"A directory's base key and parent key must be different!\");\n        Optional<SymmetricLinkToSigner> writerLink = signingPair.map(pair -> SymmetricLinkToSigner.fromPair(wBaseKey, pair));\n        FromBase fromBase = new FromBase(parentKey, writerLink, nextChunk);\n        FromParent fromParent = new FromParent(parentCap, props);\n\n        PaddedCipherText encryptedBaseBlock = PaddedCipherText.build(rBaseKey, fromBase, BASE_BLOCK_PADDING_BLOCKSIZE);\n        PaddedCipherText encryptedParentBlock = PaddedCipherText.build(parentKey, fromParent, META_DATA_PADDING_BLOCKSIZE);\n        List<BatId> bats = Stream.concat(inlineBat.stream().map(BatId::inline), mirrorBat.stream()).collect(Collectors.toList());\n        return FragmentedPaddedCipherText.build(rBaseKey, children, MIN_FRAGMENT_SIZE, Fragment.MAX_LENGTH, mirrorBat, random, hasher, false)\n                .thenApply(linksAndData -> {\n                    CryptreeNode metadata = new CryptreeNode(lastCommittedHash, true, bats, encryptedBaseBlock, linksAndData.left, encryptedParentBlock);\n                    return new DirAndChildren(metadata, linksAndData.right);\n                });\n    }\n\n    @Override\n    public CborObject toCbor() {\n        SortedMap<String, Cborable> state = new TreeMap<>();\n        state.put(\"v\", new CborObject.CborLong(getVersion()));\n        if (! bats.isEmpty())\n            state.put(\"bats\", new CborObject.CborList(bats));\n        state.put(\"b\", fromBaseKey);\n        state.put(\"p\", fromParentKey);\n        state.put(\"d\", childrenOrData);\n        return CborObject.CborMap.build(state);\n    }\n\n    public static CryptreeNode fromCbor(CborObject cbor, SymmetricKey base, Multihash hash) {\n        if (! (cbor instanceof CborObject.CborMap))\n            throw new IllegalStateException(\"Incorrect cbor for CryptreeNode: \" + cbor);\n\n        CborObject.CborMap m = (CborObject.CborMap) cbor;\n        int version = (int) m.getLong(\"v\");\n        if (version != CURRENT_VERSION)\n            throw new IllegalStateException(\"Unknown cryptree version: \" + version);\n\n        List<BatId> bats = m.getList(\"bats\", BatId::fromCbor);\n        PaddedCipherText fromBaseKey = m.get(\"b\", PaddedCipherText::fromCbor);\n        PaddedCipherText fromParentKey = m.get(\"p\", PaddedCipherText::fromCbor);\n        FragmentedPaddedCipherText childrenOrData = m.get(\"d\", FragmentedPaddedCipherText::fromCbor);\n\n        boolean isDirectory;\n        try {\n            // For a file the base key is the parent key\n            isDirectory = fromParentKey.decrypt(base, FromParent::fromCbor).properties.isDirectory;\n        } catch (Throwable t) {\n            isDirectory = true;\n        }\n        return new CryptreeNode(MaybeMultihash.of(hash), isDirectory, bats, fromBaseKey, childrenOrData, fromParentKey);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/Erasure.java",
    "content": "package peergos.shared.user.fs.erasure;\nimport peergos.shared.util.ArrayOps;\n\nimport java.util.logging.*;\n\nimport java.io.ByteArrayOutputStream;\nimport java.util.*;\n\npublic class Erasure {\n\tprivate static final Logger LOG = Logger.getGlobal();\n\n    public static byte[][] split(byte[] input, int originalBlobs, int allowedFailures)\n    {\n        return split(input, new GaloisField256(), originalBlobs, allowedFailures);\n    }\n\n    public static byte[][] split(byte[] input, GaloisField f, int originalBlobs, int allowedFailures)\n    {\n        long t1 = System.currentTimeMillis();\n        int[] ints = convert(input, f);\n\n        int n = originalBlobs + allowedFailures*2;\n        ByteArrayOutputStream[] bouts = new ByteArrayOutputStream[n];\n        for (int i=0; i < bouts.length; i++)\n            bouts[i] = new ByteArrayOutputStream();\n        int encodeSize = (f.size()/n)*n;\n        int inputSize = encodeSize*originalBlobs/n;\n        int nec = encodeSize-inputSize;\n        int symbolSize = inputSize/originalBlobs;\n        if (symbolSize * originalBlobs != inputSize)\n            throw new IllegalStateException(\"Bad alignment of bytes in chunking. \" +\n                    inputSize + \" != \" + symbolSize + \" * \" + originalBlobs);\n\n        for (int i=0; i < ints.length; i+=inputSize)\n        {\n            int[] copy = ArrayOps.copyOfRange(ints, i, i+inputSize);\n            byte[] encoded = convert(GaloisPolynomial.encode(copy, nec, f), f);\n            for (int j=0; j < n; j++)\n            {\n                bouts[j].write(encoded, j*symbolSize, symbolSize);\n            }\n        }\n\n        byte[][] res = new byte[n][];\n        for (int i=0; i < n; i++)\n            res[i] = bouts[i].toByteArray();\n        long t2 = System.currentTimeMillis();\n        LOG.info(\"Erasure encoding took \"+(t2-t1)+ \" mS\");\n        return res;\n    }\n\n    public static byte[] recombine(byte[][] encoded, int truncateTo, int originalBlobs, int allowedFailures)\n    {\n        return recombine(new GaloisField256(), encoded, truncateTo, originalBlobs, allowedFailures);\n    }\n\n    public static byte[] recombine(List<byte[]> encoded, int truncateTo, int originalBlobs, int allowedFailures)\n    {\n        return recombine(new GaloisField256(), encoded.toArray(new byte[0][]), truncateTo, originalBlobs, allowedFailures);\n    }\n\n    public static byte[] recombine(GaloisField f, byte[][] encoded, int truncateTo, int originalBlobs, int allowedFailures)\n    {\n        long t1 = System.currentTimeMillis();\n        try {\n            int n = originalBlobs + allowedFailures * 2;\n            int encodeSize = (f.size() / n) * n;\n            int inputSize = encodeSize * originalBlobs / n;\n            int nec = encodeSize - inputSize;\n            int symbolSize = inputSize / originalBlobs;\n            if (encoded.length == 0)\n                return new byte[0];\n            int tbSize = encoded[0].length;\n            // don't bother in the case where we haven't lost any of the original fragments\n            for (int k = 0; k < originalBlobs; k++) {\n                if (encoded[k] == null || encoded[k].length == 0)\n                    break;\n                if (k == originalBlobs - 1) {\n                    // shortcut\n                    ByteArrayOutputStream res = new ByteArrayOutputStream();\n                    for (int i = 0; i < tbSize; i += symbolSize) {\n                        for (int j = 0; j < originalBlobs; j++)\n                            res.write(encoded[j], i, symbolSize);\n                    }\n                    return Arrays.copyOfRange(res.toByteArray(), 0, truncateTo);\n                }\n            }\n\n            ByteArrayOutputStream res = new ByteArrayOutputStream();\n            for (int i = 0; i < tbSize; i += symbolSize) {\n                ByteArrayOutputStream bout = new ByteArrayOutputStream();\n                // take a symbol from each stream\n                for (int j = 0; j < n; j++)\n                    bout.write(encoded[j], i, symbolSize);\n                int[] decodedInts = GaloisPolynomial.decode(convert(bout.toByteArray(), f), nec, f);\n                byte[] raw = convert(decodedInts, f);\n                res.write(raw, 0, inputSize);\n            }\n            return Arrays.copyOfRange(res.toByteArray(), 0, truncateTo);\n        } finally {\n            long t2 = System.currentTimeMillis();\n            LOG.info(\"Erasure decoding took \" + (t2 - t1) + \" mS\");\n        }\n    }\n\n    public static int[] convert(byte[] in, GaloisField f)\n    {\n        if (f.size() >= 256) {\n            int[] res = new int[in.length];\n            for (int i = 0; i < in.length; i++)\n                res[i] = f.mask() & in[i];\n            return res;\n        }\n        if (f.size() == 16)\n        {\n            int[] res = new int[in.length*2];\n            for (int i = 0; i < in.length; i++) {\n                res[2*i] = f.mask() & in[i];\n                res[2*i+1] = f.mask() & (in[i] >> 4);\n            }\n            return res;\n        }\n        throw new IllegalStateException(\"Unimplemented GaloisField size conversion\");\n    }\n\n    public static byte[] convert(int[] in, GaloisField f)\n    {\n        if (f.size() >= 256) {\n            byte[] res = new byte[in.length];\n            for (int i = 0; i < in.length; i++)\n                res[i] = (byte) in[i];\n            return res;\n        }\n        if (f.size() == 16)\n        {\n            byte[] res = new byte[in.length/2];\n            for (int i = 0; i < res.length; i++)\n                res[i] = (byte) (in[2*i] | (in[2*i+1] << 4));\n            return res;\n        }\n        throw new IllegalStateException(\"Unimplemented GaloisField size conversion\");\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/ErrorTests.java",
    "content": "package peergos.shared.user.fs.erasure;\nimport java.util.logging.*;\n\nimport org.junit.*;\n\nimport java.math.*;\nimport java.util.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class ErrorTests {\n\tprivate static final Logger LOG = Logger.getGlobal();\n    public static class ErasureParameters {\n        @org.junit.Test\n        public void findAcceptableErasureParameters() {\n            int totalUserSizeInMB = 10000;\n            int nChunks = totalUserSizeInMB / 5;\n            Predicate<BigDecimal> acceptableFailure = pr -> pr.doubleValue() < 1.0/nChunks; // Expect not to lose a single chunk\n            Stream.of(60).forEach(n -> IntStream.range(1, 6).map(i -> i*n/6)\n                    .forEach(k -> Stream.of(0.5, 0.7, 0.8, 0.9).forEach(p -> {\n                        BigDecimal prFail = probabilityFailure(n, k, p);\n                        if (acceptableFailure.test(prFail))\n                            LOG.info(n + \", \" + k + \", \" + p + \" -> \" + prFail + \"\\n\");\n                    }\n                    )\n                    )\n            );\n\n            double p = 0.5;\n            int f = 40, e = 10;\n            int n = f + 2*e, k = f + e;\n\n            double min_p = 0.0, max_p = 1.0;\n            while (true) {\n                if (acceptableFailure.test(probabilityFailure(n, k, p))) {\n                    max_p = p;\n                    p = (p + min_p) / 2;\n                } else {\n                    min_p = p;\n                    p = (1 + p) / 2;\n                }\n                if (Math.abs(max_p - min_p) < 0.01)\n                    break;\n            }\n            LOG.info(n + \", \" + k + \", \" + p + \" -> \" + probabilityFailure(n, k, p) + \"\\n\");\n        }\n    }\n\n    /*\n    *  The probability of data loss using a k of n erasure code with each fragment having pr(correct) = p\n    * */\n    public static BigDecimal probabilityFailure(int n, int k, double p) {\n        List<BigDecimal> collect = IntStream.range(0, k)\n                .mapToObj(i -> new BigDecimal(p).pow(i).multiply(new BigDecimal(1 - p).pow(n - i)).multiply(choose(n, i))).collect(Collectors.toList());\n        return collect.stream()\n        .reduce(new BigDecimal(0), (a, b) -> a.add(b));\n    }\n\n    static BigInteger[][] choose = new BigInteger[200][200];\n    static {\n        for (int i=0; i < choose.length; i++) {\n            choose[i][0] = BigInteger.valueOf(1);\n            choose[i][i] = BigInteger.valueOf(1);\n        }\n        for (int i=1; i < choose.length; i++)\n            for (int j=1; j < i; j++)\n                choose[i][j] = choose[i-1][j-1].add(choose[i-1][j]);\n    }\n\n    private static BigDecimal choose(int n, int k) {\n        return new BigDecimal(choose[n][k]);\n    }\n\n    @Test\n    public void recoverFromErrors() {\n        // 40 -> 128 KiB fragments which is nice\n        recoverFromerrors(40, 30);\n    }\n\n    @Test\n    public void standardRecovery() {\n        recoverFromerrors(40, 10);\n    }\n\n    public void recoverFromerrors(int fragments, int maxErrors) {\n        // this is a fragments + maxErrors of fragments + 2*maxErrors erasure scheme\n        byte[] original = new byte[5 * 1024 * 1024];\n        new Random().nextBytes(original);\n\n        IntStream.range(0, maxErrors+1).forEach(e -> recover(original, fragments, maxErrors, e));\n        IntStream.range(maxErrors + 1, 2*maxErrors).forEach(e ->\n        {\n            try {\n                recover(original, fragments, maxErrors, e);\n                throw new RuntimeException(\"Should fail with this many errors!\");\n            } catch (IllegalStateException err) {}\n        }\n        );\n    }\n\n    public void recover(byte[] original, int fragments, int maxErrors, int actualErrors) {\n        byte[][] encoded = Erasure.split(original, fragments, maxErrors);\n        Set<Integer> done = new HashSet<>();\n        while (actualErrors - done.size() > 0) {\n            int index = new Random().nextInt(encoded.length);\n            if (!done.contains(index)) {\n                done.add(index);\n                encoded[index] = new byte[encoded[index].length];\n            }\n        }\n        byte[] recovered = Erasure.recombine(encoded, 5 * 1024 * 1024, fragments, maxErrors);\n        if (!Arrays.equals(original, recovered))\n            throw new IllegalStateException(\"Different result from original with \"+actualErrors+\" errors!\");\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/GaloisField.java",
    "content": "package peergos.shared.user.fs.erasure;\n\npublic abstract class GaloisField\n{\n    public abstract int size();\n\n    public abstract int mask();\n\n    public abstract int exp(int y);\n\n    public abstract int mul(int x, int y);\n\n    public abstract int div(int x, int y);\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/GaloisField1024.java",
    "content": "package peergos.shared.user.fs.erasure;\n\npublic class GaloisField1024 extends GaloisField\n{\n    private static final int POWER = 10;\n    private static final int SIZE = 1 << POWER;\n    private static final int[] exp = new int[2*SIZE];\n    private static final int[] log = new int[SIZE];\n    static {\n        exp[0] = 1;\n        int x = 1;\n        for (int i=1; i < SIZE-1; i++)\n        {\n            x <<= 1;\n            if ((x & SIZE) != 0)\n                x ^= (SIZE | 0b11011); // x^10 = 1 + x + x^3 + x^4\n            exp[i] = x;\n            log[x] = i;\n        }\n        for (int i=SIZE-1; i < 2*SIZE; i++)\n            exp[i] = exp[i+1-SIZE];\n        log[exp[SIZE-1]] = SIZE-1;\n        // check\n        for (int i=0; i < SIZE; i++) {\n            assert (log[exp[i]] == i);\n            assert (exp[log[i]] == i);\n        }\n    }\n\n    public int size()\n    {\n        return SIZE;\n    }\n\n    public int mask()\n    {\n        return SIZE-1;\n    }\n\n    public int exp(int y)\n    {\n        return exp[y];\n    }\n\n    public int mul(int x, int y)\n    {\n        if ((x==0) || (y==0))\n            return 0;\n        return exp[log[x]+log[y]];\n    }\n\n    public int div(int x, int y)\n    {\n        if (y==0)\n            throw new IllegalStateException(\"Divided by zero! Blackhole created.. \");\n        if (x==0)\n            return 0;\n        return exp[log[x]+SIZE-1-log[y]];\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/GaloisField16.java",
    "content": "package peergos.shared.user.fs.erasure;\n\npublic class GaloisField16 extends GaloisField\n{\n    private static final int POWER = 4;\n    private static final int SIZE = 1 << POWER;\n    private static final int[] exp = new int[2*SIZE];\n    private static final int[] log = new int[SIZE];\n    static {\n        exp[0] = 1;\n        int x = 1;\n        for (int i=1; i < SIZE-1; i++)\n        {\n            x <<= 1;\n            if ((x & SIZE) != 0)\n                x ^= (SIZE | 0x3); // x^n = 1 + x\n            exp[i] = x;\n            log[x] = i;\n        }\n        for (int i=SIZE-1; i < 2*SIZE; i++)\n            exp[i] = exp[i+1-SIZE];\n        log[exp[SIZE-1]] = SIZE-1;\n        // check\n        for (int i=0; i < SIZE; i++) {\n            assert (log[exp[i]] == i);\n            assert (exp[log[i]] == i);\n        }\n    }\n\n    public int size()\n    {\n        return SIZE;\n    }\n\n    public int mask()\n    {\n        return SIZE-1;\n    }\n\n    public int exp(int y)\n    {\n        return exp[y];\n    }\n\n    public int mul(int x, int y)\n    {\n        if ((x==0) || (y==0))\n            return 0;\n        return exp[log[x]+log[y]];\n    }\n\n    public int div(int x, int y)\n    {\n        if (y==0)\n            throw new IllegalStateException(\"Divided by zero! Blackhole created.. \");\n        if (x==0)\n            return 0;\n        return exp[log[x]+SIZE-1-log[y]];\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/GaloisField256.java",
    "content": "package peergos.shared.user.fs.erasure;\n\npublic class GaloisField256 extends GaloisField\n{\n    // Theory obtained from BBC White paper WHP 031 - Reed-solomon error correction, C.K.P. Clarke\n\n    private static final int SIZE = 256;\n    private static final int[] exp = new int[2*SIZE];\n    private static final int[] log = new int[SIZE];\n    static {\n        exp[0] = 1;\n        int x = 1;\n        for (int i=1; i < 255; i++)\n        {\n            x <<= 1;\n            // field generator polynomial is p(x) = x^8 + x^4 + x^3 + x^2 + 1\n            if ((x & SIZE) != 0)\n                x ^= (SIZE | 0x1D); // x^8 = x^4 + x^3 + x^2 + 1  ==> 0001_1101\n            exp[i] = x;\n            log[x] = i;\n        }\n        for (int i=255; i < 512; i++)\n            exp[i] = exp[i-255];\n        //log[exp[255]] = 255;\n        // check\n\n        for (int i=0; i < 255; i++) {\n            if (log[exp[i]] != i)\n                throw new IllegalStateException(\"log[exp[\"+i+\"]] != \"+i+\", exp[\"+i+\"] = \"+exp[i]+\", log[\"+exp[i]+\"] = \" + log[exp[i]]);\n            if (i > 0 && exp[log[i]] != i)\n                throw new IllegalStateException(\"exp[log[\"+i+\"]] != \"+ i);\n        }\n    }\n\n    public int size()\n    {\n        return SIZE;\n    }\n\n    public int mask()\n    {\n        return SIZE-1;\n    }\n\n    public int exp(int y)\n    {\n        return exp[y];\n    }\n\n    public int mul(int x, int y)\n    {\n        if ((x==0) || (y==0))\n            return 0;\n        return exp[log[x]+log[y]];\n    }\n\n    public int div(int x, int y)\n    {\n        if (y==0)\n            throw new IllegalStateException(\"Divided by zero! Blackhole created.. \");\n        if (x==0)\n            return 0;\n        return exp[log[x]+255-log[y]];\n    }\n\n    public static void main(String[] a) {\n\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/GaloisField4.java",
    "content": "package peergos.shared.user.fs.erasure;\n\npublic class GaloisField4 extends GaloisField\n{\n    private static final int POWER = 2;\n    private static final int SIZE = 1 << POWER;\n    private static final int[] exp = new int[2*SIZE];\n    private static final int[] log = new int[SIZE];\n    static {\n        exp[0] = 1;\n        int x = 1;\n        for (int i=1; i < SIZE-1; i++)\n        {\n            x <<= 1;\n            if ((x & SIZE) != 0)\n                x ^= (SIZE | 0x3); // x^n = 1 + x\n            exp[i] = x;\n            log[x] = i;\n        }\n        for (int i=SIZE-1; i < 2*SIZE; i++)\n            exp[i] = exp[i+1-SIZE];\n        log[exp[SIZE-1]] = SIZE-1;\n        // check\n        for (int i=0; i < SIZE; i++) {\n            assert (log[exp[i]] == i);\n            assert (exp[log[i]] == i);\n        }\n    }\n\n    public int size()\n    {\n        return SIZE;\n    }\n\n    public int mask()\n    {\n        return SIZE-1;\n    }\n\n    public int exp(int y)\n    {\n        return exp[y];\n    }\n\n    public int mul(int x, int y)\n    {\n        if ((x==0) || (y==0))\n            return 0;\n        return exp[log[x]+log[y]];\n    }\n\n    public int div(int x, int y)\n    {\n        if (y==0)\n            throw new IllegalStateException(\"Divided by zero! Blackhole created.. \");\n        if (x==0)\n            return 0;\n        return exp[log[x]+SIZE-1-log[y]];\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/GaloisField65536.java",
    "content": "package peergos.shared.user.fs.erasure;\n\npublic class GaloisField65536 extends GaloisField\n{\n    private static final int POWER = 16;\n    private static final int SIZE = 1 << POWER;\n    private static final int[] exp = new int[2*SIZE];\n    private static final int[] log = new int[SIZE];\n    static {\n        exp[0] = 1;\n        int x = 1;\n        for (int i=1; i < SIZE-1; i++)\n        {\n            x <<= 1;\n            if ((x & SIZE) != 0)\n                x ^= (SIZE | 0b10000101001); // x^16 = 1 + x^3 + x^5 + x^10\n            exp[i] = x;\n            log[x] = i;\n        }\n        for (int i=SIZE-1; i < 2*SIZE; i++)\n            exp[i] = exp[i+1-SIZE];\n        log[exp[SIZE-1]] = SIZE-1;\n        // check\n        for (int i=0; i < SIZE; i++) {\n            assert (log[exp[i]] == i);\n            assert (exp[log[i]] == i);\n        }\n    }\n\n    public int size()\n    {\n        return SIZE;\n    }\n\n    public int mask()\n    {\n        return SIZE-1;\n    }\n\n    public int exp(int y)\n    {\n        return exp[y];\n    }\n\n    public int mul(int x, int y)\n    {\n        if ((x==0) || (y==0))\n            return 0;\n        return exp[log[x]+log[y]];\n    }\n\n    public int div(int x, int y)\n    {\n        if (y==0)\n            throw new IllegalStateException(\"Divided by zero! Blackhole created.. \");\n        if (x==0)\n            return 0;\n        return exp[log[x]+SIZE-1-log[y]];\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/erasure/GaloisPolynomial.java",
    "content": "package peergos.shared.user.fs.erasure;\n\nimport java.util.*;\n\nimport peergos.shared.util.*;\n\npublic class GaloisPolynomial\n{\n    // TODO implement a Locally Repairable Erasure code, to reduce unnecessary bandwidth usage when repairing\n    private final int[] coefficients;\n    private final GaloisField f;\n\n    private GaloisPolynomial(int[] coefficients, GaloisField f)\n    {\n        if (coefficients.length > f.size())\n            throw new IllegalStateException(\"Polynomial order must be less than or equal to the degree of the Galois field.\");\n        this.coefficients = coefficients;\n        this.f = f;\n    }\n\n    private int order()\n    {\n        return coefficients.length;\n    }\n\n    private int eval(int x)\n    {\n        int y = coefficients[0];\n        for (int i=1; i < coefficients.length; i++)\n            y = f.mul(y, x) ^ coefficients[i];\n        return y;\n    }\n\n    private GaloisPolynomial scale(int x)\n    {\n        int[] res = new int[coefficients.length];\n        for (int i=0; i < res.length; i++)\n            res[i] = f.mul(x, coefficients[i]);\n        return new GaloisPolynomial(res, f);\n    }\n\n    private GaloisPolynomial add(GaloisPolynomial other)\n    {\n        int[] res = new int[Math.max(order(), other.order())];\n        for (int i=0; i < order(); i++)\n            res[i + res.length - order()] = coefficients[i];\n        for (int i=0; i < other.order(); i++)\n            res[i + res.length - other.order()] ^= other.coefficients[i];\n        return new GaloisPolynomial(res, f);\n    }\n\n    private GaloisPolynomial mul(GaloisPolynomial other)\n    {\n        int[] res = new int[order() + other.order() - 1];\n        for (int i=0; i < order(); i++)\n            for (int j=0; j < other.order(); j++)\n                res[i+j] ^= f.mul(coefficients[i], other.coefficients[j]);\n        return new GaloisPolynomial(res, f);\n    }\n\n    private GaloisPolynomial append(int x)\n    {\n        int[] res = new int[coefficients.length+1];\n        System.arraycopy(coefficients, 0, res, 0, coefficients.length);\n        res[res.length-1] = x;\n        return new GaloisPolynomial(res, f);\n    }\n\n    private static Map<Integer, GaloisPolynomial> memoized = new HashMap<>();\n\n    private static GaloisPolynomial generator(int nECSymbols, GaloisField f)\n    {\n        if (memoized.containsKey(nECSymbols))\n            return memoized.get(nECSymbols);\n        GaloisPolynomial g = new GaloisPolynomial(new int[] {1}, f);\n        for (int i=0; i < nECSymbols; i++)\n            g = g.mul(new GaloisPolynomial(new int[]{1, f.exp(i)}, f));\n        memoized.put(nECSymbols, g);\n        return g;\n    }\n\n    public static int[] encode(int[] input, int nEC, GaloisField f)\n    {\n        GaloisPolynomial gen = generator(nEC, f);\n        int[] res = new int[input.length + nEC];\n        System.arraycopy(input, 0, res, 0, input.length);\n        for (int i=0; i < input.length; i++)\n        {\n            int c = res[i];\n            if (c != 0)\n                for (int j=0; j < gen.order(); j++)\n                    res[i+j] ^= f.mul(gen.coefficients[j], c);\n        }\n        System.arraycopy(input, 0, res, 0, input.length);\n        return res;\n    }\n\n    private static int[] syndromes(int[] input, int nEC, GaloisField f)\n    {\n        int[] res = new int[nEC];\n        GaloisPolynomial poly = new GaloisPolynomial(input, f);\n        for (int i=0; i < nEC; i++)\n            res[i] = poly.eval(f.exp(i));\n        return res;\n    }\n\n    private static void correctErrata(int[] input, int[] synd, List<Integer> pos, GaloisField f)\n    {\n        if (pos.size() == 0)\n            return;\n        GaloisPolynomial q = new GaloisPolynomial(new int[]{1}, f);\n        for (int i: pos)\n        {\n            int x = f.exp(input.length - 1 - i);\n            q = q.mul(new GaloisPolynomial(new int[]{x, 1}, f));\n        }\n        int[] t = new int[pos.size()];\n        for (int i=0; i < t.length; i++)\n            t[i] = synd[t.length-1-i];\n        GaloisPolynomial p = new GaloisPolynomial(t, f).mul(q);\n        t = new int[pos.size()];\n        System.arraycopy(p.coefficients, p.order()-t.length, t, 0, t.length);\n        p = new GaloisPolynomial(t, f);\n        t = new int[(q.order()- (q.order() & 1))/2];\n        for (int i=q.order() & 1; i < q.order(); i+= 2)\n            t[i/2] = q.coefficients[i];\n        GaloisPolynomial qprime = new GaloisPolynomial(t,f);\n        for (int i: pos)\n        {\n            int x = f.exp(i + f.size() - input.length);\n            int y = p.eval(x);\n            int z = qprime.eval(f.mul(x, x));\n            input[i] ^= f.div(y, f.mul(x, z));\n        }\n    }\n\n    private static List<Integer> findErrors(int[] synd, final int nmess, GaloisField f)\n    {\n        GaloisPolynomial errPoly = new GaloisPolynomial(new int[]{1}, f);\n        GaloisPolynomial oldPoly = new GaloisPolynomial(new int[]{1}, f);\n        for (int i=0; i < synd.length; i++)\n        {\n            oldPoly = oldPoly.append(0);\n            int delta = synd[i];\n            for (int j=1; j < errPoly.order(); j++)\n                delta ^= f.mul(errPoly.coefficients[errPoly.order() - 1 - j], synd[i - j]);\n            if (delta != 0)\n            {\n                if (oldPoly.order() > errPoly.order())\n                {\n                    GaloisPolynomial newPoly = oldPoly.scale(delta);\n                    oldPoly = errPoly.scale(f.div(1, delta));\n                    errPoly = newPoly;\n                }\n                errPoly = errPoly.add(oldPoly.scale(delta));\n            }\n        }\n        int errs = errPoly.order()-1;\n        if (2*errs > synd.length)\n            throw new IllegalStateException(\"Too many errors to correct! (\"+errs+\")\");\n        List<Integer> errorPos = new LinkedList();\n        for (int i=0; i < nmess; i++)\n            if (errPoly.eval(f.exp(f.size() - 1 - i)) == 0)\n                    errorPos.add(nmess - 1 - i);\n        if (errorPos.size() != errs)\n            throw new IllegalStateException(\"couldn't find error positions! (\"+errorPos.size()+\"!=\"+errs+\") ( missing fragments)\");\n        return errorPos;\n    }\n\n    private static int[] forneySyndromes(int[] synd, List<Integer> pos, int nmess, GaloisField f)\n    {\n        int[] fsynd = Arrays.copyOf(synd, synd.length);\n        for (int i: pos)\n        {\n            int x = f.exp(nmess - 1 - i);\n            for (int j=0; j < fsynd.length-1; j++)\n                fsynd[j] = f.mul(fsynd[j], x) ^ fsynd[j+1];\n        }\n        int[] t = new int[fsynd.length-1];\n        System.arraycopy(fsynd, 1, t, 0, t.length);\n        return t;\n    }\n\n    public static int[] decode(int[] message, int nec, GaloisField f)\n    {\n        int[] out = Arrays.copyOf(message, message.length);\n        int[] synd = syndromes(out, nec, f);\n        int max = 0;\n        for (int i: synd)\n            if (i > max)\n                max = i;\n        if (max == 0)\n            return out;\n        List<Integer> errPos = findErrors(synd, out.length, f);\n        correctErrata(out, synd, errPos, f);\n        return out;\n    }\n    /* todo-test \n    public static class Test {\n        boolean print = true;\n        public Test() {\n        }\n\n         @org.junit.Test\n        public void compareFields()\n        {\n            int size = 10*1024*1024;\n            byte[] input = new byte[size];\n            Random r = new Random();\n            r.nextBytes(input);\n\n            // // GF(16) doesn't work here because we need to handle splitting of byte to 4-bit words, and reassembly\n//            GaloisField f4 = new GaloisField16();\n//            long t6 = System.nanoTime();\n//            byte[][] transmissionBlocks16 = split(input, f4);\n//            long t7 = System.nanoTime();\n//            System.out.printf(\"16 took %d mS encoding %d bytes\\n\", (t7-t6)/1000000, transmissionBlocks16.length*transmissionBlocks16[0].length);\n\n            int originalBlobs = 10;\n            int handleFailures = 4;\n            GaloisField f = new GaloisField256();\n            byte[][] transmissionBlocks = timeEncoding(f, input, originalBlobs, handleFailures);\n            byte[] decoded = timeDecoding(f, transmissionBlocks, input.length, originalBlobs, handleFailures);\n            System.out.println(\"Decoded = original? \" + Arrays.equals(decoded, input));\n            // i failures\n            for (int i=0; i< handleFailures; i++) {\n                transmissionBlocks[i] = new byte[transmissionBlocks[0].length];\n                decoded = timeDecoding(f, transmissionBlocks, input.length, originalBlobs, handleFailures);\n                System.out.println((i+1)+\" failure(s): Decoded = original? \" + Arrays.equals(decoded, input));\n            }\n\n//            GaloisField f2 = new GaloisField1024();\n//            long t2 = System.nanoTime();\n//            byte[][] transmissionBlocks1024 = split(input, f2);\n//            long t3 = System.nanoTime();\n//            System.out.printf(\"1024 took %d mS encoding %d bytes\\n\", (t3-t2)/1000000, transmissionBlocks1024.length*transmissionBlocks1024[0].length);\n//\n//            GaloisField f3 = new GaloisField65536();\n//            long t4 = System.nanoTime();\n//            byte[][] transmissionBlocks65536 = split(input, f3);\n//            long t5 = System.nanoTime();\n//            System.out.printf(\"65536 took %d mS encoding %d bytes\\n\", (t5-t4)/1000000, transmissionBlocks65536.length*transmissionBlocks65536[0].length);\n        }\n\n        public byte[][] timeEncoding(GaloisField f, byte[] input, int originalBlobs, int allowedFailures)\n        {\n            long t0 = System.nanoTime();\n            byte[][] transmissionBlocks = Erasure.split(input, f, originalBlobs, allowedFailures);\n            long t1 = System.nanoTime();\n            System.out.println(StringUtils.format(\"GF(%d) took %d mS encoding %d bytes to %d bytes\\n\", f.size(), (t1-t0)/1000000, input.length, transmissionBlocks.length*transmissionBlocks[0].length));\n            return transmissionBlocks;\n        }\n\n        public byte[] timeDecoding(GaloisField f, byte[][] encoded, int truncateTo, int originalBlobs, int allowedFailures)\n        {\n            long t0 = System.nanoTime();\n            byte[] original = Erasure.recombine(f, encoded, truncateTo, originalBlobs, allowedFailures);\n            long t1 = System.nanoTime();\n            System.out.println(StringUtils.format(\"GF(%d) took %d mS decoding\\n\", f.size(), (t1-t0)/1000000));\n            return original;\n        }\n\n        public void run()\n        {\n            long t1 = System.nanoTime();\n            GaloisField f = new GaloisField256();\n            long t2 = System.nanoTime();\n            System.out.println(StringUtils.format(\"Constructing field took %d mS\\n\", (t2-t1)/1000000));\n            errorFreeSyndrome(f);\n            long t3 = System.nanoTime();\n            System.out.println(StringUtils.format(\"Error free syndrome took %d mS\\n\", (t3-t2)/1000000));\n            singleError(f);\n            long t4 = System.nanoTime();\n            System.out.println(StringUtils.format(\"Single error took %d mS\\n\", (t4-t3)/1000000));\n            manyErrors(f);\n            long t5 = System.nanoTime();\n            System.out.println(StringUtils.format(\"Many error took %d mS\\n\", (t5-t4)/1000000));\n        }\n\n        public void errorFreeSyndrome(GaloisField f) {\n            Random r = new Random();\n            int size = (f.size()/14 * 10);\n            int nec = (f.size()/14 * 4);\n            byte[] bytes = new byte[size];\n            r.nextBytes(bytes);\n            int[] input = Erasure.convert(bytes, f);\n            int[] encoded = GaloisPolynomial.encode(input, nec, f);\n            int[] original = Arrays.copyOf(encoded, encoded.length);\n            if (print) {\n                System.out.println(\"Original:  \");\n                print(original);\n            }\n            int[] synd = syndromes(encoded, nec, f);\n            if (print) {\n                System.out.println(\"Syndrome:  \");\n                print(synd);\n            }\n            assert (Arrays.equals(synd, new int[synd.length]));\n        }\n\n        public void singleError(GaloisField f) {\n            Random r = new Random();\n            int size = (int)(f.size() * 0.6);\n            int nec = (int)(f.size() * 0.4);\n            byte[] bytes = new byte[size];\n            r.nextBytes(bytes);\n            int[] input = Erasure.convert(bytes, f);\n            int[] encoded = GaloisPolynomial.encode(input, nec, f);\n            int[] original = Arrays.copyOf(encoded, encoded.length);\n            if (print) {\n                System.out.println(\"Original:  \");\n                print(original);\n            }\n            encoded[0] ^= 1;\n\n            int[] synd = syndromes(encoded, nec, f);\n            if (print) {\n                System.out.println(\"Syndrome:  \");\n                print(synd);\n            }\n            List<Integer> errPos = findErrors(synd, encoded.length, f);\n            if (print) {\n                System.out.println(\"Error Positions: \");\n                for (int i : errPos)\n                    System.out.print(i + \" \");\n                System.out.println();\n            }\n            correctErrata(encoded, synd, errPos, f);\n            if (print) {\n                System.out.println(\"Corrected: \");\n                print(encoded);\n            }\n            assert (Arrays.equals(encoded, original));\n        }\n\n        public void manyErrors(GaloisField f) {\n            Random r = new Random();\n            int size = (int) (f.size() * 0.6);\n            int nec = (int) (f.size() * 0.4);\n            byte[] bytes = new byte[size];\n            r.nextBytes(bytes);\n            int[] input = Erasure.convert(bytes, f);\n            int[] encoded = GaloisPolynomial.encode(input, nec, f);\n            int[] original = Arrays.copyOf(encoded, encoded.length);\n            if (print) {\n                System.out.println(\"Original:  \");\n                print(original);\n                System.out.println(\"Inserted errors at: \");\n            }\n            List<Integer> epositions = new ArrayList();\n            for (int i=0; i < nec/2-1; i++) {\n                int index = r.nextInt(encoded.length);\n                epositions.add(index);\n                encoded[index] ^= 1;\n            }\n            if (print) {\n                Collections.sort(epositions);\n                for (Integer i : epositions)\n                    System.out.print(i + \" \");\n                System.out.println();\n            }\n\n            int[] synd = syndromes(encoded, nec, f);\n            if (print) {\n                System.out.println(\"Syndrome:  \");\n                print(synd);\n            }\n            List<Integer> errPos = findErrors(synd, encoded.length, f);\n            if (print) {\n                Collections.sort(errPos);\n                System.out.println(\"Found Error Positions: \");\n                for (int i : errPos)\n                    System.out.println(i + \" \");\n                System.out.println();\n            }\n            correctErrata(encoded, synd, errPos, f);\n            if (print) {\n                System.out.println(\"Corrected: \");\n                print(encoded);\n            }\n            assert (Arrays.equals(encoded, original));\n        }\n    }*/\n\n    public static void print(int[] d)\n    {\n        for (int i: d)\n            System.out.println(ArrayOps.byteToHex(i));\n        System.out.println();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/transaction/FileUploadTransaction.java",
    "content": "package peergos.shared.user.fs.transaction;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.NetworkAccess;\nimport peergos.shared.cbor.CborObject;\nimport peergos.shared.cbor.Cborable;\nimport peergos.shared.crypto.SigningPrivateKeyAndPublicHash;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.nio.file.*;\nimport java.time.*;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.*;\n\npublic class FileUploadTransaction implements Transaction {\n    private final long startTimeEpochMillis;\n    private final String path, name;\n    private final PublicKeyHash owner;\n    private final SigningPrivateKeyAndPublicHash writer;\n    private final Location firstChunk;\n    public final FileProperties props;\n    public final Optional<Bat> firstBat;\n    public final SymmetricKey baseKey, dataKey, writeKey;\n    private final long size;\n    private final byte[] streamSecret;\n\n    public FileUploadTransaction(long startTimeEpochMillis,\n                                 String path,\n                                 String name,\n                                 FileProperties props,\n                                 SigningPrivateKeyAndPublicHash writer,\n                                 Location firstChunk,\n                                 Optional<Bat> firstBat,\n                                 long size,\n                                 SymmetricKey baseKey,\n                                 SymmetricKey dataKey,\n                                 SymmetricKey writeKey,\n                                 byte[] streamSecret) {\n        this.startTimeEpochMillis = startTimeEpochMillis;\n        this.path = path;\n        this.name = name;\n        this.props = props;\n        this.writer = writer;\n        this.firstChunk = firstChunk;\n        this.firstBat = firstBat;\n        this.size = size;\n        this.baseKey = baseKey;\n        this.dataKey = dataKey;\n        this.writeKey = writeKey;\n        this.streamSecret = streamSecret;\n        this.owner = firstChunk.owner;\n    }\n\n    public boolean isLegacy() {\n        return props == null;\n    }\n\n    public long size() {\n        return size;\n    }\n\n    public byte[] streamSecret() {\n        return streamSecret;\n    }\n\n    public Location getFirstLocation() {\n        return firstChunk;\n    }\n\n    public PublicKeyHash writer() {\n        return writer.publicKeyHash;\n    }\n\n    public byte[] firstMapKey() {\n        return firstChunk.getMapKey();\n    }\n\n    private CompletableFuture<Snapshot> clear(Snapshot version, Committer committer, NetworkAccess networkAccess, Location location) {\n        return IpfsTransaction.call(owner,\n                tid -> version.withWriter(owner, writer.publicKeyHash, networkAccess)\n                        .thenCompose(v -> v.contains(writer.publicKeyHash) ?\n                                networkAccess.deleteChunkIfPresent(v, committer, location.owner, writer, location.getMapKey(), tid) :\n                                Futures.of(v)), networkAccess.dhtClient);\n    }\n\n    public CompletableFuture<Snapshot> clear(Snapshot version, Committer committer, NetworkAccess network, Hasher h) {\n        return Futures.reduceAll(LongStream.range(0, (size + Chunk.MAX_SIZE - 1)/Chunk.MAX_SIZE).boxed(),\n                        new Pair<>(version, firstChunk),\n                        (p, i) -> clear(p.left, committer, network, p.right)\n                                .thenCompose(s -> FileProperties.calculateNextMapKey(streamSecret, p.right.getMapKey(), Optional.empty(), h)\n                                        .thenApply(mapKey -> new Pair<>(s, firstChunk.withMapKey(mapKey.left)))),\n                        (a, b) -> b)\n                .thenApply(p -> p.left);\n    }\n\n    @JsMethod\n    public String getPath() {\n        return path;\n    }\n\n    @Override\n    public String name() {\n        return name;\n    }\n\n    @JsMethod\n    public String targetFilename() {\n        Path path = PathUtil.get(this.path);\n        return path.getFileName().toString();\n    }\n\n    @Override\n    public long startTimeEpochMillis() {\n        return startTimeEpochMillis;\n    }\n\n    public LocalDateTime startTime() {\n        return LocalDateTime.ofEpochSecond(startTimeEpochMillis / 1000, (int)(startTimeEpochMillis % 1000)* 1_000_000, ZoneOffset.UTC);\n    }\n\n    public WritableAbsoluteCapability writeCap() {\n        return new WritableAbsoluteCapability(owner, writer.publicKeyHash, firstMapKey(), firstBat, baseKey, writeKey);\n    }\n\n    @Override\n    public CborObject toCbor() {\n        Map<String, Cborable> map = new HashMap<>();\n        map.put(\"type\", new CborObject.CborString(Type.FILE_UPLOAD.name()));\n        map.put(\"path\", new CborObject.CborString(path));\n        map.put(\"startTimeEpochMs\", new CborObject.CborLong(startTimeEpochMillis()));\n        map.put(\"owner\", owner);\n        map.put(\"writer\", writer);\n        map.put(\"baseKey\", baseKey);\n        map.put(\"dataKey\", dataKey);\n        map.put(\"writeKey\", writeKey);\n        map.put(\"props\", props);\n        firstBat.ifPresent(b -> map.put(\"firstBat\", b));\n        map.put(\"mapKey\", new CborObject.CborByteArray(firstChunk.getMapKey()));\n        map.put(\"streamSecret\", new CborObject.CborByteArray(streamSecret));\n        map.put(\"size\", new CborObject.CborLong(size));\n\n        return CborObject.CborMap.build(map);\n    }\n\n    static Transaction fromCbor(CborObject.CborMap map, String filename) {\n        Type type = Type.valueOf(map.getString(\"type\"));\n        boolean isFileUpload = type.equals(Type.FILE_UPLOAD);\n        if (!isFileUpload)\n            throw new IllegalStateException(\"Cannot parse transaction: wrong type \" + type);\n\n        PublicKeyHash owner = map.getObject(\"owner\", PublicKeyHash::fromCbor);\n        SigningPrivateKeyAndPublicHash writer = map.getObject(\"writer\", SigningPrivateKeyAndPublicHash::fromCbor);\n\n        if (! map.containsKey(\"streamSecret\"))\n            throw new IllegalStateException(\"Invalid upload transaction\");\n\n        long startTimeEpochMs = map.getLong(\"startTimeEpochMs\");\n        String path = map.getString(\"path\");\n        long size = map.getLong(\"size\");\n        byte[] streamSecrets = map.getByteArray(\"streamSecret\");\n        if (! map.containsKey(\"props\")) // legacy transactions have enough information to delete, but not to continue\n            return new FileUploadTransaction(\n                    startTimeEpochMs,\n                    path,\n                    filename,\n                    null,\n                    writer,\n                    new Location(owner, writer.publicKeyHash, map.getByteArray(\"mapKey\")),\n                    null,\n                    size,\n                    null,\n                    null,\n                    null,\n                    streamSecrets);\n\n        return new FileUploadTransaction(\n                startTimeEpochMs,\n                path,\n                filename,\n                map.get(\"props\", FileProperties::fromCbor),\n                writer,\n                new Location(owner, writer.publicKeyHash, map.getByteArray(\"mapKey\")),\n                map.getOptional(\"firstBat\", Bat::fromCbor),\n                size,\n                map.getObject(\"baseKey\", SymmetricKey::fromCbor),\n                map.getObject(\"dataKey\", SymmetricKey::fromCbor),\n                map.getObject(\"writeKey\", SymmetricKey::fromCbor),\n                streamSecrets);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/transaction/Transaction.java",
    "content": "package peergos.shared.user.fs.transaction;\n\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.crypto.hash.*;\nimport peergos.shared.crypto.symmetric.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\npublic interface Transaction extends Cborable {\n\n    long startTimeEpochMillis();\n\n    String name();\n\n    /**\n     * Clear data associated with this transaction\n     */\n    CompletableFuture<Snapshot> clear(Snapshot version, Committer committer, NetworkAccess network, Hasher h);\n\n    static Transaction deserialize(byte[] data, String filename) {\n        CborObject cborObject = CborObject.fromByteArray(data);\n        CborObject.CborMap map =  (CborObject.CborMap) cborObject;\n        Type type = Type.valueOf(map.getString(\"type\"));\n        switch (type)  {\n            case FILE_UPLOAD:\n                return FileUploadTransaction.fromCbor(map, filename);\n            default:\n                throw new IllegalStateException(\"Unimplemented type \"+ type);\n        }\n    }\n\n    enum Type {\n        FILE_UPLOAD\n    }\n\n    static CompletableFuture<FileUploadTransaction> buildFileUploadTransaction(String path,\n                                                                               long fileSize,\n                                                                               FileProperties props,\n                                                                               byte[] streamSecret,\n                                                                               SymmetricKey baseKey,\n                                                                               SymmetricKey dataKey,\n                                                                               SymmetricKey writeKey,\n                                                                               SigningPrivateKeyAndPublicHash writer,\n                                                                               Location firstChunkLocation,\n                                                                               Optional<Bat> firstBat,\n                                                                               Hasher h) {\n        return h.hash(path.getBytes(), true)\n                .thenApply(cid -> new FileUploadTransaction(System.currentTimeMillis(), path, cid.toString(), props, writer,\n                        firstChunkLocation, firstBat, fileSize, baseKey, dataKey, writeKey, streamSecret));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/transaction/TransactionService.java",
    "content": "package peergos.shared.user.fs.transaction;\n\nimport jsinterop.annotations.JsMethod;\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.user.*;\nimport peergos.shared.util.*;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.*;\nimport java.util.stream.Collectors;\n\npublic interface TransactionService {\n\n    SigningPrivateKeyAndPublicHash getSigner();\n\n    @JsMethod\n    /**\n     *  Open a transaction or return an existing matching transaction\n     */\n    CompletableFuture<Either<Snapshot, FileUploadTransaction>> open(Snapshot version,\n                                                                    Committer committer,\n                                                                    Transaction transaction);\n\n    @JsMethod\n    CompletableFuture<Snapshot> close(Snapshot version, Committer committer, Transaction transaction);\n\n    /**\n     * Remove data associated with a transaction.\n     * @param transaction\n     * @return\n     */\n    CompletableFuture<Snapshot> clear(Snapshot version, Committer committer, Transaction transaction);\n\n    CompletableFuture<Set<Transaction>> getOpenTransactions(Snapshot version);\n\n    TransactionServiceImpl withNetwork(NetworkAccess net);\n\n    default CompletableFuture<Snapshot> clearAndClose(Snapshot version, Committer committer, Transaction transaction) {\n        return clear(version, committer, transaction)\n                .thenCompose(s -> close(s, committer, transaction));\n    }\n\n    default CompletableFuture<Snapshot> clearAndClosePendingTransactions(Snapshot version, Committer committer, Predicate<Transaction> filter) {\n        return getOpenTransactions(version)\n                .thenCompose(openTransactions -> {\n                    List<Transaction> toClose = openTransactions.stream()\n                            .filter(filter)\n                            .collect(Collectors.toList());\n                    System.out.println(\"Open file upload transactions: \" + openTransactions.size());\n                    System.out.println(\"Stale file upload transactions: \" + toClose.size());\n                    return Futures.reduceAll(toClose, version,\n                            (s, t) -> clearAndClose(s, committer, t),\n                            (a, b) -> b);\n                });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/user/fs/transaction/TransactionServiceImpl.java",
    "content": "package peergos.shared.user.fs.transaction;\n\nimport peergos.shared.*;\nimport peergos.shared.crypto.*;\nimport peergos.shared.storage.*;\nimport peergos.shared.storage.auth.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\nimport peergos.shared.util.*;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Collectors;\n\npublic class TransactionServiceImpl implements TransactionService {\n    private static ProgressConsumer<Long> VOID_PROGRESS = l -> {};\n\n    private final FileWrapper transactionsDir;\n    private final SigningPrivateKeyAndPublicHash signer;\n    private final NetworkAccess networkAccess;\n    private final Crypto crypto;\n\n    public TransactionServiceImpl(NetworkAccess networkAccess,\n                                  Crypto crypto,\n                                  FileWrapper transactionsDir) {\n        this.transactionsDir = transactionsDir;\n        this.signer = transactionsDir.signingPair();\n        this.networkAccess = networkAccess;\n        this.crypto = crypto;\n    }\n\n    public TransactionServiceImpl withNetwork(NetworkAccess net) {\n        return new TransactionServiceImpl(net, crypto, transactionsDir);\n    }\n\n    @Override\n    public SigningPrivateKeyAndPublicHash getSigner() {\n        return signer;\n    }\n\n    private CompletableFuture<FileWrapper> updatedTransactionDir(Snapshot v) {\n        return transactionsDir.getUpdated(v, networkAccess);\n    }\n\n    @Override\n    public CompletableFuture<Either<Snapshot, FileUploadTransaction>> open(Snapshot version,\n                                                                           Committer committer,\n                                                                           Transaction transaction) {\n        byte[] data = transaction.serialize();\n        AsyncReader asyncReader = AsyncReader.build(data);\n        return updatedTransactionDir(version).thenCompose(dir ->\n                Futures.asyncExceptionally(\n                        () -> dir.uploadFileSection(version, committer, transaction.name(), asyncReader, false,\n                                0, data.length, Optional.empty(), false, false, false, networkAccess,\n                                crypto, () -> false, VOID_PROGRESS, crypto.random.randomBytes(32), Optional.empty(), Optional.of(Bat.random(crypto.random)), dir.mirrorBatId())\n                                .thenApply(Either::a),\n                        t -> {\n                            if (!(Exceptions.getRootCause(t) instanceof FileExistsException))\n                                throw new RuntimeException(t);\n                            return dir.getChild(transaction.name(), crypto.hasher, networkAccess)\n                                    .thenCompose(fopt -> read(version, fopt.get()))\n                                    .thenApply(Optional::get)\n                                    .thenApply(txn -> {\n                                        if (txn instanceof FileUploadTransaction) {\n                                            return Either.b((FileUploadTransaction) txn);\n                                        }\n                                        throw new RuntimeException(Exceptions.getRootCause(t));\n                                    });\n                        }));\n    }\n\n    @Override\n    public CompletableFuture<Snapshot> close(Snapshot version, Committer committer, Transaction transaction) {\n        return updatedTransactionDir(version).thenCompose(dir ->\n                dir.getChild(transaction.name(), crypto.hasher, networkAccess).thenCompose(fileOpt -> {\n                    boolean hasChild = fileOpt.isPresent();\n                    if (!hasChild)\n                        return CompletableFuture.completedFuture(version);\n                    FileWrapper child = fileOpt.get();\n                    return dir.removeChild(version, committer, child, networkAccess, crypto.random, crypto.hasher)\n                            .thenCompose(v -> IpfsTransaction.call(child.owner(),\n                                    tid -> FileWrapper.deleteAllChunks(child.writableFilePointer(),\n                                            child.signingPair(), tid, crypto.hasher, networkAccess, v, committer), networkAccess.dhtClient));\n                }));\n    }\n\n    @Override\n    public CompletableFuture<Snapshot> clear(Snapshot version, Committer committer, Transaction transaction) {\n        return transaction.clear(version, committer, networkAccess, crypto.hasher);\n    }\n\n    private CompletableFuture<Optional<Transaction>> read(Snapshot version, FileWrapper txnFile) {\n        FileProperties props = txnFile.getFileProperties();\n        int size = (int) props.size;\n        byte[] data = new byte[size];\n\n        CommittedWriterData cwd = version.get(txnFile.writer());\n        return txnFile.getInputStream(cwd, networkAccess, crypto, VOID_PROGRESS)\n                .thenApply(reader -> Serialize.readFullArray(reader, data))\n                .thenApply(done -> Optional.of(Transaction.deserialize(data, txnFile.getName())))\n                .exceptionally(t -> Optional.empty());\n    }\n\n    @Override\n    public CompletableFuture<Set<Transaction>> getOpenTransactions(Snapshot version) {\n        return updatedTransactionDir(version)\n                .thenCompose(dir -> dir.getChildren(crypto.hasher, networkAccess)\n                        .thenCompose(children -> {\n                            List<CompletableFuture<Optional<Transaction>>> collect = children.stream()\n                                    .map(c -> read(version, c))\n                                    .collect(Collectors.toList());\n                            return Futures.combineAll(collect)\n                                    .thenApply(opts -> opts.stream()\n                                            .flatMap(Optional::stream)\n                                            .collect(Collectors.toSet()));\n                        })\n                );\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/ArrayOps.java",
    "content": "package peergos.shared.util;\n\nimport java.util.*;\nimport java.util.stream.*;\n\npublic class ArrayOps\n{\n    public static <X> List<List<X>> group(List<X> x, int maxGroupSize) {\n        return IntStream.range(0, (x.size() + maxGroupSize - 1) / maxGroupSize)\n                .mapToObj(i -> x.stream()\n                        .skip(maxGroupSize * i)\n                        .limit(maxGroupSize)\n                        .collect(Collectors.toList()))\n                .collect(Collectors.toList());\n    }\n\n    /*\n    Due to an unfortunate bug in GWT emulation for Arrays.copyPrimitiveArray (introduced by us), It is necessary to call this version instead.\n     */\n    public static int[] copyOfRange(int[] original, int from, int to) {\n        int length = to - from;\n        if (length < 0) {\n            throw new IllegalArgumentException(from + \" > \" + to);\n        }\n        int[] copy = new int[length];\n        System.arraycopy(original, from, copy, 0, Math.min(original.length - from, length));\n        return copy;\n    }\n\n    public static byte[] concat(byte[] one, byte[] two)\n    {\n        byte[] res = new byte[one.length+two.length];\n        System.arraycopy(one, 0, res, 0, one.length);\n        System.arraycopy(two, 0, res, one.length, two.length);\n        return res;\n    }\n\n    public static List<ByteArrayWrapper> split(byte[] data, int size) {\n        if (data.length % size != 0)\n            throw new IllegalStateException(\"Can only split an array that is multiple of split size! \" + data.length + \" !/ \" + size);\n        List<ByteArrayWrapper> res = new ArrayList<>(data.length/size);\n        for (int i=0; i < data.length/size; i++)\n            res.add(new ByteArrayWrapper(Arrays.copyOfRange(data, i*size, (i+1)*size)));\n        return res;\n    }\n\n    public static byte[] hexToBytes(String hex)\n    {\n        byte[] res = new byte[hex.length()/2];\n        for (int i=0; i < res.length; i++)\n            res[i] = (byte) Integer.parseInt(hex.substring(2*i, 2*i+2), 16);\n        return res;\n    }\n\n    private static String[] HEX_DIGITS = new String[]{\n            \"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\"};\n    private static String[] HEX = new String[256];\n    static {\n        for (int i=0; i < 256; i++)\n            HEX[i] = HEX_DIGITS[(i >> 4) & 0xF] + HEX_DIGITS[i & 0xF];\n    }\n\n    public static String byteToHex(byte b) {\n        return HEX[b & 0xFF];\n    }\n\n    public static String byteToHex(int b) {\n        return HEX[b & 0xFF];\n    }\n\n    public static String bytesToHex(byte[] data)\n    {\n        StringBuilder s = new StringBuilder();\n        for (byte b : data)\n            s.append(byteToHex(b));\n        return s.toString();\n    }\n\n    public static byte[] random(int length)\n    {\n        byte[] res = new byte[length];\n        Random r = new Random();\n        r.nextBytes(res);\n        return res;\n    }\n\n    public static int compare(byte[] a, byte[] b)\n    {\n        for (int i=0; i < Math.min(a.length, b.length); i++)\n            if (a[i] != b[i])\n                return (a[i] & 0xff) - (b[i] & 0xff);\n            return 0;\n    }\n\n    public static boolean equalArrays(byte[] a, int aStart, int aEnd, byte[] b, int bStart, int bEnd) {\n        int len = aEnd - aStart;\n        if (len != bEnd - bStart)\n            return false;\n        for (int i=0; i < len; i++) {\n            if (a[aStart + i] != b[bStart + i])\n                return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/AsyncLock.java",
    "content": "package peergos.shared.util;\n\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\n/** This class implements a lock that can be held across multiple dependent asynchronous tasks which return a new value\n * for the guarded object or to get the value after any pending updaters have completed\n *\n * @param <T>\n */\npublic class AsyncLock<T> {\n\n    private CompletableFuture<T> queueHead;\n\n    public AsyncLock(CompletableFuture<T> initialValue) {\n        this.queueHead = initialValue;\n    }\n\n    public synchronized boolean isDone() {\n        return queueHead.isDone();\n    }\n\n    public synchronized CompletableFuture<T> runWithLock(Function<T, CompletionStage<T>> processor) {\n        return runWithLock(processor, () -> queueHead);\n    }\n\n    /**\n     *\n     * @param processor\n     * @param updater a method to get a fresh value which is called if updater completes exceptionally\n     * @return A future completed with the result from a computation, or exceptionally completed on error\n     */\n    public synchronized CompletableFuture<T> runWithLock(Function<T, CompletionStage<T>> processor, Supplier<CompletableFuture<T>> updater) {\n        CompletableFuture<T> existing = queueHead;\n        CompletableFuture<T> newHead = new CompletableFuture<>();\n        this.queueHead = newHead;\n\n        CompletableFuture<T> result = new CompletableFuture<>();\n        existing.thenCompose(current -> processor.apply(current)\n                .thenApply(res -> newHead.complete(res) && result.complete(res))\n                .exceptionally(t -> {\n                    updater.get()\n                            .thenApply(res -> newHead.complete(res) && result.completeExceptionally(t))\n                            .exceptionally(e -> newHead.complete(current) && result.completeExceptionally(e));\n                    t.printStackTrace();\n                    return true;\n                }))\n                .exceptionally(t -> {\n                    // The previous queueHead failed - use updater to recover\n                    // so subsequent operations aren't permanently poisoned\n                    result.completeExceptionally(t);\n                    updater.get()\n                            .thenApply(newHead::complete)\n                            .exceptionally(e -> newHead.completeExceptionally(e));\n                    return true;\n                });\n\n        return result;\n    }\n\n    public synchronized CompletableFuture<T> getValue() {\n        return runWithLock(CompletableFuture::completedFuture);\n    }\n\n    @Override\n    public String toString() {\n        return queueHead.toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/ByteArrayWrapper.java",
    "content": "package peergos.shared.util;\n\nimport java.util.Arrays;\n\n/** A convenience wrapper for using byte arrays in collections and sorted collections.\n *\n */\npublic class ByteArrayWrapper implements Comparable<ByteArrayWrapper>\n{\n    public final byte[] data;\n\n    public ByteArrayWrapper(byte[] data)\n    {\n        this.data = data;\n    }\n\n    @Override\n    public int hashCode()\n    {\n        return java.util.Arrays.hashCode(data);\n    }\n\n    @Override\n    public boolean equals(Object obj)\n    {\n        if (this == obj)\n            return true;\n        if (obj == null)\n            return false;\n        if (getClass() != obj.getClass())\n            return false;\n        ByteArrayWrapper other = (ByteArrayWrapper) obj;\n        if (!Arrays.equals(data, other.data))\n            return false;\n        return true;\n    }\n\n    @Override\n    public int compareTo(ByteArrayWrapper o) {\n        if (data.length < o.data.length)\n            return -1;\n        if (data.length > o.data.length)\n            return 1;\n        for (int i=0; i < data.length; i++)\n            if (data[i] != o.data[i])\n                return (0xff & data[i]) - (0xff & o.data[i]);\n        return 0;\n    }\n\n    @Override\n    public String toString() {\n        return ArrayOps.bytesToHex(data);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/CappedProgressConsumer.java",
    "content": "package peergos.shared.util;\n\npublic final class CappedProgressConsumer implements ProgressConsumer<Long> {\n\n    private ProgressConsumer consumer;\n    private final int maxLength;\n    private int currentCount;\n\n    public CappedProgressConsumer(ProgressConsumer<Long> consumer, int maxLength) {\n        this.consumer = consumer;\n        this.maxLength = maxLength;\n        this.currentCount = 0;\n    }\n    public void accept(Long delta) {\n        if (delta > 0) {\n            int startValue = currentCount;\n            currentCount += delta.intValue();\n            currentCount = Math.min(currentCount, maxLength);\n            int diff = currentCount - startValue;\n            consumer.accept((long) diff);\n        }\n    }\n}"
  },
  {
    "path": "src/peergos/shared/util/Constants.java",
    "content": "package peergos.shared.util;\n\npublic class Constants {\n    public static final String DHT_URL = \"/api/v0/\";\n    public static final String PEERGOS_API_PREFIX = \"peergos/v0/\";\n    public static final String ADMIN_URL = PEERGOS_API_PREFIX + \"admin/\";\n    public static final String BATS_URL = PEERGOS_API_PREFIX + \"bats/\";\n    public static final String MUTABLE_POINTERS_URL = PEERGOS_API_PREFIX + \"mutable/\";\n    public static final String LOGIN_URL = PEERGOS_API_PREFIX + \"login/\";\n    public static final String CORE_URL = PEERGOS_API_PREFIX + \"core/\";\n    public static final String SOCIAL_URL = PEERGOS_API_PREFIX + \"social/\";\n    public static final String SPACE_USAGE_URL = PEERGOS_API_PREFIX + \"storage/\";\n    public static final String SERVER_MESSAGE_URL = PEERGOS_API_PREFIX + \"server-message/\";\n    public static final String ANDROID_FILE_REFLECTOR = PEERGOS_API_PREFIX + \"reflector/\";\n    public static final String STOP = PEERGOS_API_PREFIX + \"stop/\";\n    public static final String CONFIG = PEERGOS_API_PREFIX + \"config/\";\n    public static final String SYNC = PEERGOS_API_PREFIX + \"sync/\";\n\n    public static final String PUBLIC_FILES_URL = \"public/\";\n}\n"
  },
  {
    "path": "src/peergos/shared/util/EfficientHashMap.java",
    "content": "package peergos.shared.util;\n\n/*\n * Copyright 2009 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.util.AbstractCollection;\nimport java.util.AbstractSet;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.Set;\n\n/**\n * A memory-efficient hash map.\n *\n * @param <K> the key type\n * @param <V> the value type\n */\npublic class EfficientHashMap<K, V> implements Map<K, V>, Serializable {\n\n    /**\n     * In the interest of memory-savings, we start with the smallest feasible\n     * power-of-two table size that can hold three items without rehashing. If we\n     * started with a size of 2, we'd have to expand as soon as the second item\n     * was added.\n     */\n    private static final int INITIAL_TABLE_SIZE = 4;\n\n    private class EntryIterator implements Iterator<Entry<K, V>> {\n        private int index = 0;\n        private int last = -1;\n\n        {\n            advanceToItem();\n        }\n\n        public boolean hasNext() {\n            return index < keys.length;\n        }\n\n        public Entry<K, V> next() {\n            if (!hasNext()) {\n                throw new NoSuchElementException();\n            }\n            last = index;\n            Entry<K, V> toReturn = new HashEntry(index++);\n            advanceToItem();\n            return toReturn;\n        }\n\n        public void remove() {\n            if (last < 0) {\n                throw new IllegalStateException();\n            }\n            internalRemove(last);\n            if (keys[last] != null) {\n                index = last;\n            }\n            last = -1;\n        }\n\n        private void advanceToItem() {\n            for (; index < keys.length; ++index) {\n                if (keys[index] != null) {\n                    return;\n                }\n            }\n        }\n    }\n\n    private class EntrySet extends AbstractSet<Entry<K, V>> {\n        @Override\n        public boolean add(Entry<K, V> entry) {\n            boolean result = !EfficientHashMap.this.containsKey(entry.getKey());\n            EfficientHashMap.this.put(entry.getKey(), entry.getValue());\n            return result;\n        }\n\n        @Override\n        public boolean addAll(Collection<? extends Entry<K, V>> c) {\n            EfficientHashMap.this.ensureSizeFor(size() + c.size());\n            return super.addAll(c);\n        }\n\n        @Override\n        public void clear() {\n            EfficientHashMap.this.clear();\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public boolean contains(Object o) {\n            if (!(o instanceof Entry)) {\n                return false;\n            }\n            Entry<K, V> entry = (Entry<K, V>) o;\n            V value = EfficientHashMap.this.get(entry.getKey());\n            return EfficientHashMap.this.valueEquals(value, entry.getValue());\n        }\n\n        @Override\n        public int hashCode() {\n            return EfficientHashMap.this.hashCode();\n        }\n\n        @Override\n        public Iterator<java.util.Map.Entry<K, V>> iterator() {\n            return new EntryIterator();\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public boolean remove(Object o) {\n            if (!(o instanceof Entry)) {\n                return false;\n            }\n            Entry<K, V> entry = (Entry<K, V>) o;\n            int index = findKey(entry.getKey());\n            if (index >= 0 && valueEquals(values[index], entry.getValue())) {\n                internalRemove(index);\n                return true;\n            }\n            return false;\n        }\n\n        @Override\n        public boolean removeAll(Collection<?> c) {\n            boolean didRemove = false;\n            for (Object o : c) {\n                didRemove |= remove(o);\n            }\n            return didRemove;\n        }\n\n        @Override\n        public int size() {\n            return EfficientHashMap.this.size;\n        }\n    }\n\n    private class HashEntry implements Entry<K, V> {\n        private final int index;\n\n        public HashEntry(int index) {\n            this.index = index;\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public boolean equals(Object o) {\n            if (!(o instanceof Entry)) {\n                return false;\n            }\n            Entry<K, V> entry = (Entry<K, V>) o;\n            return keyEquals(getKey(), entry.getKey())\n                    && valueEquals(getValue(), entry.getValue());\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        public K getKey() {\n            return (K) unmaskNullKey(keys[index]);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        public V getValue() {\n            return (V) values[index];\n        }\n\n        @Override\n        public int hashCode() {\n            return keyHashCode(getKey()) ^ valueHashCode(getValue());\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        public V setValue(V value) {\n            V previous = (V) values[index];\n            values[index] = value;\n            return previous;\n        }\n\n        @Override\n        public String toString() {\n            return getKey() + \"=\" + getValue();\n        }\n    }\n\n    private class KeyIterator implements Iterator<K> {\n        private int index = 0;\n        private int last = -1;\n\n        {\n            advanceToItem();\n        }\n\n        public boolean hasNext() {\n            return index < keys.length;\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        public K next() {\n            if (!hasNext()) {\n                throw new NoSuchElementException();\n            }\n            last = index;\n            Object toReturn = unmaskNullKey(keys[index++]);\n            advanceToItem();\n            return (K) toReturn;\n        }\n\n        public void remove() {\n            if (last < 0) {\n                throw new IllegalStateException();\n            }\n            internalRemove(last);\n            if (keys[last] != null) {\n                index = last;\n            }\n            last = -1;\n        }\n\n        private void advanceToItem() {\n            for (; index < keys.length; ++index) {\n                if (keys[index] != null) {\n                    return;\n                }\n            }\n        }\n    }\n\n    private class KeySet extends AbstractSet<K> {\n        @Override\n        public void clear() {\n            EfficientHashMap.this.clear();\n        }\n\n        @Override\n        public boolean contains(Object o) {\n            return EfficientHashMap.this.containsKey(o);\n        }\n\n        @Override\n        public int hashCode() {\n            int result = 0;\n            for (int i = 0; i < keys.length; ++i) {\n                Object key = keys[i];\n                if (key != null) {\n                    result += keyHashCode(unmaskNullKey(key));\n                }\n            }\n            return result;\n        }\n\n        @Override\n        public Iterator<K> iterator() {\n            return new KeyIterator();\n        }\n\n        @Override\n        public boolean remove(Object o) {\n            int index = findKey(o);\n            if (index >= 0) {\n                internalRemove(index);\n                return true;\n            }\n            return false;\n        }\n\n        @Override\n        public boolean removeAll(Collection<?> c) {\n            boolean didRemove = false;\n            for (Object o : c) {\n                didRemove |= remove(o);\n            }\n            return didRemove;\n        }\n\n        @Override\n        public int size() {\n            return EfficientHashMap.this.size;\n        }\n    }\n\n    private class ValueIterator implements Iterator<V> {\n        private int index = 0;\n        private int last = -1;\n\n        {\n            advanceToItem();\n        }\n\n        public boolean hasNext() {\n            return index < keys.length;\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        public V next() {\n            if (!hasNext()) {\n                throw new NoSuchElementException();\n            }\n            last = index;\n            Object toReturn = values[index++];\n            advanceToItem();\n            return (V) toReturn;\n        }\n\n        public void remove() {\n            if (last < 0) {\n                throw new IllegalStateException();\n            }\n            internalRemove(last);\n            if (keys[last] != null) {\n                index = last;\n            }\n            last = -1;\n        }\n\n        private void advanceToItem() {\n            for (; index < keys.length; ++index) {\n                if (keys[index] != null) {\n                    return;\n                }\n            }\n        }\n    }\n\n    private class Values extends AbstractCollection<V> {\n        @Override\n        public void clear() {\n            EfficientHashMap.this.clear();\n        }\n\n        @Override\n        public boolean contains(Object o) {\n            return EfficientHashMap.this.containsValue(o);\n        }\n\n        @Override\n        public int hashCode() {\n            int result = 0;\n            for (int i = 0; i < keys.length; ++i) {\n                if (keys[i] != null) {\n                    result += valueHashCode(values[i]);\n                }\n            }\n            return result;\n        }\n\n        @Override\n        public Iterator<V> iterator() {\n            return new ValueIterator();\n        }\n\n        @Override\n        public boolean remove(Object o) {\n            if (o == null) {\n                for (int i = 0; i < keys.length; ++i) {\n                    if (keys[i] != null && values[i] == null) {\n                        internalRemove(i);\n                        return true;\n                    }\n                }\n            } else {\n                for (int i = 0; i < keys.length; ++i) {\n                    if (valueEquals(values[i], o)) {\n                        internalRemove(i);\n                        return true;\n                    }\n                }\n            }\n            return false;\n        }\n\n        @Override\n        public boolean removeAll(Collection<?> c) {\n            boolean didRemove = false;\n            for (Object o : c) {\n                didRemove |= remove(o);\n            }\n            return didRemove;\n        }\n\n        @Override\n        public int size() {\n            return EfficientHashMap.this.size;\n        }\n    }\n\n    private static final Object NULL_KEY = new Serializable() {\n        Object readResolve() {\n            return NULL_KEY;\n        }\n    };\n\n    static Object maskNullKey(Object k) {\n        return (k == null) ? NULL_KEY : k;\n    }\n\n    static Object unmaskNullKey(Object k) {\n        return (k == NULL_KEY) ? null : k;\n    }\n\n    /**\n     * Backing store for all the keys; transient due to custom serialization.\n     * Default access to avoid synthetic accessors from inner classes.\n     */\n    transient Object[] keys;\n\n    /**\n     * Number of pairs in this set; transient due to custom serialization. Default\n     * access to avoid synthetic accessors from inner classes.\n     */\n    transient int size = 0;\n\n    /**\n     * Backing store for all the values; transient due to custom serialization.\n     * Default access to avoid synthetic accessors from inner classes.\n     */\n    transient Object[] values;\n\n    public EfficientHashMap() {\n        initTable(INITIAL_TABLE_SIZE);\n    }\n\n    public EfficientHashMap(Map<? extends K, ? extends V> m) {\n        int newCapacity = INITIAL_TABLE_SIZE;\n        int expectedSize = m.size();\n        while (newCapacity * 3 < expectedSize * 4) {\n            newCapacity <<= 1;\n        }\n\n        initTable(newCapacity);\n        internalPutAll(m);\n    }\n\n    public void clear() {\n        initTable(INITIAL_TABLE_SIZE);\n        size = 0;\n    }\n\n    public boolean containsKey(Object key) {\n        return findKey(key) >= 0;\n    }\n\n    public boolean containsValue(Object value) {\n        if (value == null) {\n            for (int i = 0; i < keys.length; ++i) {\n                if (keys[i] != null && values[i] == null) {\n                    return true;\n                }\n            }\n        } else {\n            for (Object existing : values) {\n                if (valueEquals(existing, value)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public Set<Entry<K, V>> entrySet() {\n        return new EntrySet();\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public boolean equals(Object o) {\n        if (!(o instanceof Map)) {\n            return false;\n        }\n        Map<K, V> other = (Map<K, V>) o;\n        return entrySet().equals(other.entrySet());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public V get(Object key) {\n        int index = findKey(key);\n        return (index < 0) ? null : (V) values[index];\n    }\n\n    @Override\n    public int hashCode() {\n        int result = 0;\n        for (int i = 0; i < keys.length; ++i) {\n            Object key = keys[i];\n            if (key != null) {\n                result += keyHashCode(unmaskNullKey(key)) ^ valueHashCode(values[i]);\n            }\n        }\n        return result;\n    }\n\n    public boolean isEmpty() {\n        return size == 0;\n    }\n\n    public Set<K> keySet() {\n        return new KeySet();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public V put(K key, V value) {\n        ensureSizeFor(size + 1);\n        int index = findKeyOrEmpty(key);\n        if (keys[index] == null) {\n            ++size;\n            keys[index] = maskNullKey(key);\n            values[index] = value;\n            return null;\n        } else {\n            Object previousValue = values[index];\n            values[index] = value;\n            return (V) previousValue;\n        }\n    }\n\n    public void putAll(Map<? extends K, ? extends V> m) {\n        ensureSizeFor(size + m.size());\n        internalPutAll(m);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public V remove(Object key) {\n        int index = findKey(key);\n        if (index < 0) {\n            return null;\n        }\n        Object previousValue = values[index];\n        internalRemove(index);\n        return (V) previousValue;\n    }\n\n    public int size() {\n        return size;\n    }\n\n    @Override\n    public String toString() {\n        if (size == 0) {\n            return \"{}\";\n        }\n        StringBuilder buf = new StringBuilder(32 * size());\n        buf.append('{');\n\n        boolean needComma = false;\n        for (int i = 0; i < keys.length; ++i) {\n            Object key = keys[i];\n            if (key != null) {\n                if (needComma) {\n                    buf.append(',').append(' ');\n                }\n                key = unmaskNullKey(key);\n                Object value = values[i];\n                buf.append(key == this ? \"(this Map)\" : key).append('=').append(\n                        value == this ? \"(this Map)\" : value);\n                needComma = true;\n            }\n        }\n        buf.append('}');\n        return buf.toString();\n    }\n\n    public Collection<V> values() {\n        return new Values();\n    }\n\n    /**\n     * Returns whether two keys are equal for the purposes of this set.\n     */\n    protected boolean keyEquals(Object a, Object b) {\n        return (a == null) ? (b == null) : a.equals(b);\n    }\n\n    /**\n     * Returns the hashCode for a key.\n     */\n    protected int keyHashCode(Object k) {\n        return (k == null) ? 0 : k.hashCode();\n    }\n\n    /**\n     * Returns whether two values are equal for the purposes of this set.\n     */\n    protected boolean valueEquals(Object a, Object b) {\n        return (a == null) ? (b == null) : a.equals(b);\n    }\n\n    /**\n     * Returns the hashCode for a value.\n     */\n    protected int valueHashCode(Object v) {\n        return (v == null) ? 0 : v.hashCode();\n    }\n\n    /**\n     * Ensures the map is large enough to contain the specified number of entries.\n     * Default access to avoid synthetic accessors from inner classes.\n     */\n    void ensureSizeFor(int expectedSize) {\n        if (keys.length * 3 >= expectedSize * 4) {\n            return;\n        }\n\n        int newCapacity = keys.length << 1;\n        while (newCapacity * 3 < expectedSize * 4) {\n            newCapacity <<= 1;\n        }\n\n        Object[] oldKeys = keys;\n        Object[] oldValues = values;\n        initTable(newCapacity);\n        for (int i = 0; i < oldKeys.length; ++i) {\n            Object k = oldKeys[i];\n            if (k != null) {\n                int newIndex = getKeyIndex(unmaskNullKey(k));\n                while (keys[newIndex] != null) {\n                    if (++newIndex == keys.length) {\n                        newIndex = 0;\n                    }\n                }\n                keys[newIndex] = k;\n                values[newIndex] = oldValues[i];\n            }\n        }\n    }\n\n    /**\n     * Returns the index in the key table at which a particular key resides, or -1\n     * if the key is not in the table. Default access to avoid synthetic accessors\n     * from inner classes.\n     */\n    int findKey(Object k) {\n        int index = getKeyIndex(k);\n        while (true) {\n            Object existing = keys[index];\n            if (existing == null) {\n                return -1;\n            }\n            if (keyEquals(k, unmaskNullKey(existing))) {\n                return index;\n            }\n            if (++index == keys.length) {\n                index = 0;\n            }\n        }\n    }\n\n    /**\n     * Returns the index in the key table at which a particular key resides, or\n     * the index of an empty slot in the table where this key should be inserted\n     * if it is not already in the table. Default access to avoid synthetic\n     * accessors from inner classes.\n     */\n    int findKeyOrEmpty(Object k) {\n        int index = getKeyIndex(k);\n        while (true) {\n            Object existing = keys[index];\n            if (existing == null) {\n                return index;\n            }\n            if (keyEquals(k, unmaskNullKey(existing))) {\n                return index;\n            }\n            if (++index == keys.length) {\n                index = 0;\n            }\n        }\n    }\n\n    /**\n     * Removes the entry at the specified index, and performs internal management\n     * to make sure we don't wind up with a hole in the table. Default access to\n     * avoid synthetic accessors from inner classes.\n     */\n    void internalRemove(int index) {\n        keys[index] = null;\n        values[index] = null;\n        --size;\n        plugHole(index);\n    }\n\n    private int getKeyIndex(Object k) {\n        int h = keyHashCode(k);\n        // Copied from Apache's AbstractHashedMap; prevents power-of-two collisions.\n        h += ~(h << 9);\n        h ^= (h >>> 14);\n        h += (h << 4);\n        h ^= (h >>> 10);\n        // Power of two trick.\n        return h & (keys.length - 1);\n    }\n\n    private void initTable(int capacity) {\n        keys = new Object[capacity];\n        values = new Object[capacity];\n    }\n\n    private void internalPutAll(Map<? extends K, ? extends V> m) {\n        for (Entry<? extends K, ? extends V> entry : m.entrySet()) {\n            K key = entry.getKey();\n            V value = entry.getValue();\n            int index = findKeyOrEmpty(key);\n            if (keys[index] == null) {\n                ++size;\n                keys[index] = maskNullKey(key);\n                values[index] = value;\n            } else {\n                values[index] = value;\n            }\n        }\n    }\n\n    /**\n     * Tricky, we left a hole in the map, which we have to fill. The only way to\n     * do this is to search forwards through the map shuffling back values that\n     * match this index until we hit a null.\n     */\n    private void plugHole(int hole) {\n        int index = hole + 1;\n        if (index == keys.length) {\n            index = 0;\n        }\n        while (keys[index] != null) {\n            int targetIndex = getKeyIndex(unmaskNullKey(keys[index]));\n            if (hole < index) {\n                /*\n                 * \"Normal\" case, the index is past the hole and the \"bad range\" is from\n                 * hole (exclusive) to index (inclusive).\n                 */\n                if (!(hole < targetIndex && targetIndex <= index)) {\n                    // Plug it!\n                    keys[hole] = keys[index];\n                    values[hole] = values[index];\n                    keys[index] = null;\n                    values[index] = null;\n                    hole = index;\n                }\n            } else {\n                /*\n                 * \"Wrapped\" case, the index is before the hole (we've wrapped) and the\n                 * \"good range\" is from index (exclusive) to hole (inclusive).\n                 */\n                if (index < targetIndex && targetIndex <= hole) {\n                    // Plug it!\n                    keys[hole] = keys[index];\n                    values[hole] = values[index];\n                    keys[index] = null;\n                    values[index] = null;\n                    hole = index;\n                }\n            }\n            if (++index == keys.length) {\n                index = 0;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/Either.java",
    "content": "package peergos.shared.util;\n\nimport jsinterop.annotations.JsType;\n\nimport java.util.*;\nimport java.util.function.*;\n\n@JsType\npublic class Either<A, B> {\n    private final A a;\n    private final B b;\n\n    private Either(A a, B b) {\n        this.a = a;\n        this.b = b;\n    }\n\n    public <V> V map(Function<A, V> aMap, Function<B, V> bmap) {\n        if (isA())\n            return aMap.apply(a);\n        return bmap.apply(b);\n    }\n\n    public boolean isA() {\n        return a != null;\n    }\n\n    public boolean isB() {\n        return b != null;\n    }\n\n    public A a() {\n        if (a == null)\n            throw new IllegalStateException(\"Absent value!\");\n        return a;\n    }\n\n    public B b() {\n        if (b == null)\n            throw new IllegalStateException(\"Absent value!\");\n        return b;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Either<?, ?> either = (Either<?, ?>) o;\n        return Objects.equals(a, either.a) &&\n                Objects.equals(b, either.b);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(a, b);\n    }\n\n    public static <A, B> Either<A, B> a(A a) {\n        return new Either<>(a, null);\n    }\n\n    public static <A, B> Either<A, B> b(B b) {\n        return new Either<>(null, b);\n    }\n\n    @Override\n    public String toString() {\n        return isA() ? a.toString() : b.toString();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/Exceptions.java",
    "content": "package peergos.shared.util;\n\nimport java.util.concurrent.*;\n\npublic class Exceptions {\n    public static Throwable getRootCause(Throwable t) {\n        Throwable cause = t.getCause();\n        if (t instanceof ExecutionException)\n            return getRootCause(cause);\n        if (t instanceof RuntimeException && cause != null && cause != t)\n            return getRootCause(cause);\n        return t;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/FileUtils.java",
    "content": "package peergos.shared.util;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.*;\nimport peergos.shared.user.fs.*;\n\nimport java.nio.file.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class FileUtils {\n\n    /** Get and parse an object from a file, or create and save an object if file doesn't exist\n     *\n     * @param context\n     * @param file\n     * @param generator\n     * @param parser\n     * @param <T>\n     * @return\n     */\n    public static <T extends Cborable> CompletableFuture<T> getOrCreateObject(UserContext context,\n                                                                              Path file,\n                                                                              Supplier<T> generator,\n                                                                              Function<T, CompletableFuture<Boolean>> initializer,\n                                                                              Function<byte[], T> parser) {\n        return context.getByPath(file).thenCompose(opt -> {\n            if (opt.isPresent())\n                return opt.get().getInputStream(context.network, context.crypto, x -> {})\n                        .thenCompose(in -> Serialize.readFully(in, opt.get().getSize()))\n                        .thenApply(parser);\n            T val = generator.get();\n            byte[] raw = val.serialize();\n            String filename = file.getFileName().toString();\n            AsyncReader reader = AsyncReader.build(raw);\n            return initializer.apply(val).thenCompose(x -> context.getByPath(file.getParent()))\n                    .thenCompose(dopt -> dopt.get()\n                            .uploadAndReturnFile(filename, reader, raw.length, false, dopt.get().mirrorBatId(), context.network, context.crypto))\n                    .thenApply(x -> val);\n        });\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/Futures.java",
    "content": "package peergos.shared.util;\n\nimport jsinterop.annotations.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\n\npublic class Futures {\n    private static final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);\n\n    @JsMethod\n    public static final <T> CompletableFuture<T> of(T val) {\n        return CompletableFuture.completedFuture(val);\n    }\n\n    public static final <V> CompletableFuture<V> orTimeout(Supplier<CompletableFuture<V>> work, long millis) {\n        CompletableFuture<V> res = new CompletableFuture<>();\n        executor.schedule(() -> res.completeExceptionally(new TimeoutException()), millis, TimeUnit.MILLISECONDS);\n        ForkJoinPool.commonPool().execute(() -> work.get()\n                .thenApply(res::complete)\n                .exceptionally(res::completeExceptionally));\n        return res;\n    }\n\n    /**\n     *\n     * @param futures collection of independent futures whose results we want to combine\n     * @param <T> result type of each future\n     * @return\n     */\n    public static <T> CompletableFuture<Set<T>> combineAll(Collection<CompletableFuture<T>> futures) {\n        CompletableFuture<Set<T>> identity = CompletableFuture.completedFuture(Collections.emptySet());\n        return futures.stream().reduce(identity,\n                (a, b) -> b.thenCompose(opt ->\n                        a.thenApply(set -> Stream.concat(set.stream(), Stream.of(opt))\n                                .collect(Collectors.toSet()))),\n                (a, b) -> b.thenCompose(setb ->\n                        a.thenApply(seta -> Stream.concat(seta.stream(), setb.stream()).collect(Collectors.toSet()))));\n    }\n\n    /**\n     *\n     * @param futures collection of independent futures whose results we want to combine\n     * @param <T> result type of each future\n     * @return\n     */\n    public static <T> CompletableFuture<List<T>> combineAllInOrder(Collection<CompletableFuture<T>> futures) {\n        CompletableFuture<List<T>> identity = CompletableFuture.completedFuture(Collections.emptyList());\n        return futures.stream().reduce(identity,\n                (a, b) -> b.thenCompose(opt ->\n                        a.thenApply(set -> {\n                            ArrayList<T> combined = new ArrayList<>(set.size() + 1);\n                            combined.addAll(set);\n                            combined.add(opt);\n                            return combined;\n                        })),\n                (a, b) -> b.thenCompose(setb ->\n                        a.thenApply(seta -> Stream.concat(seta.stream(), setb.stream()).collect(Collectors.toList()))));\n    }\n\n    /*** Reduce a set of input values against an Identity where the composition step is asynchronous\n     *\n     * @param input the values to reduce\n     * @param identity the identity of the target type\n     * @param composer composes an input value with a target type value asynchronously\n     * @param combiner\n     * @param <T> target type\n     * @param <V> input type\n     * @return\n     */\n    public static <T, V> CompletableFuture<T> reduceAll(Collection<V> input,\n                                                        T identity,\n                                                        BiFunction<T, V, CompletableFuture<T>> composer,\n                                                        BiFunction<T, T, T> combiner) {\n        return reduceAll(input.stream(), identity, composer, combiner);\n    }\n\n    /*** Reduce a set of input values against an Identity where the composition step is asynchronous\n     *\n     * @param input the values to reduce\n     * @param identity the identity of the target type\n     * @param composer composes an input value with a target type value asynchronously\n     * @param combiner\n     * @param <T> target type\n     * @param <V> input type\n     * @return\n     */\n    public static <T, V> CompletableFuture<T> reduceAll(Stream<V> input,\n                                                        T identity,\n                                                        BiFunction<T, V, CompletableFuture<T>> composer,\n                                                        BiFunction<T, T, T> combiner) {\n        CompletableFuture<T> identityFut = CompletableFuture.completedFuture(identity);\n        return input.reduce(\n                identityFut,\n                (a, b) -> a.thenCompose(res -> composer.apply(res, b)),\n                (a, b) -> a.thenCompose(x -> b.thenApply(y -> combiner.apply(x, y)))\n        );\n    }\n\n    /** Efficiently apply an async function to an entire list\n     *\n     * @param input\n     * @param producer\n     * @param <X>\n     * @param <V>\n     * @return\n     */\n    public static <X, V> CompletableFuture<List<V>> map(List<X> input,\n                                                        Function<X, CompletableFuture<V>> producer) {\n        return combineAllInOrder(input.stream()\n                .parallel()\n                .map(producer)\n                .collect(Collectors.toList()));\n    }\n\n    /*** Asynchronously map a set of input values to output values until one matches a predicate\n     *\n     * @param input the values to reduce\n     * @param producer maps an input value to a completable future of the return type\n     * @param <X> input type\n     * @param <V> return type\n     * @return\n     */\n    public static <X, V> CompletableFuture<Optional<V>> findFirst(\n            Collection<X> input,\n            Function<X, CompletableFuture<Optional<V>>> producer) {\n        if (input.isEmpty())\n            return Futures.of(Optional.empty());\n        List<X> inList = new ArrayList<>(input);\n\n        return producer.apply(inList.get(0))\n                .thenCompose(optRes -> {\n                    if (optRes.isPresent())\n                        return Futures.of(optRes);\n                    return findFirst(inList.subList(1, inList.size()), producer);\n                });\n    }\n\n    public static <V> CompletableFuture<V> runAsync(Supplier<CompletableFuture<V>> work) {\n        return runAsync(work, ForkJoinPool.commonPool());\n    }\n\n    public static <V> CompletableFuture<V> runAsync(Supplier<CompletableFuture<V>> work, ExecutorService pool) {\n        CompletableFuture<V> res = new CompletableFuture<>();\n        pool.execute(() -> {\n            try {\n                work.get()\n                        .thenApply(res::complete)\n                        .exceptionally(res::completeExceptionally);\n            } catch (Throwable t) {\n                res.completeExceptionally(t);\n            }\n        });\n        return res;\n    }\n\n    public static <T> CompletableFuture<T> asyncExceptionally(Supplier<CompletableFuture<T>> normal,\n                                                              Function<Throwable, CompletableFuture<T>> exceptional) {\n        CompletableFuture<T> result = new CompletableFuture<>();\n        try {\n            normal.get()\n                    .thenApply(result::complete)\n                    .exceptionally(t -> {\n                        try {\n                            exceptional.apply(t)\n                                    .thenApply(result::complete)\n                                    .exceptionally(result::completeExceptionally);\n                        } catch (Throwable t2) {\n                            t2.printStackTrace();\n                            result.completeExceptionally(t);\n                        }\n                        return true;\n                    });\n        } catch (Throwable t) {\n            try {\n                exceptional.apply(t)\n                        .thenApply(result::complete)\n                        .exceptionally(result::completeExceptionally);\n            } catch (Throwable t2) {\n                t2.printStackTrace();\n                result.completeExceptionally(t);\n            }\n        }\n        return result;\n    }\n\n    public static <T> T logAndReturn(Throwable t, T result) {\n        t.printStackTrace();\n        return result;\n    }\n\n    public static <T> T logAndThrow(Throwable t) {\n        return logAndThrow(t, Optional.empty());\n    }\n\n    public static <T> T logAndThrow(Throwable t, Optional<String> message) {\n        if (message.isPresent())\n            System.out.println(message);\n        t.printStackTrace();\n        throw new RuntimeException(t.getMessage(), t);\n    }\n\n    public static <T> CompletableFuture<T> errored(Throwable t) {\n        CompletableFuture<T> err = new CompletableFuture<>();\n        err.completeExceptionally(t);\n        return err;\n    }\n\n    @JsMethod\n    public static <T>CompletableFuture<T> incomplete() {\n        return new CompletableFuture<>();\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/LRUCache.java",
    "content": "package peergos.shared.util;\n\nimport java.util.*;\n\npublic class LRUCache<K, V> extends LinkedHashMap<K, V> {\n    private final int cacheSize;\n\n    public LRUCache(int cacheSize) {\n        super(16, 0.75f, true);\n        this.cacheSize = cacheSize;\n    }\n\n    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {\n        return size() >= cacheSize;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/LongUtil.java",
    "content": "package peergos.shared.util;\n\nimport jsinterop.annotations.*;\n\npublic class LongUtil {\n\n    public static long intsToLong(int high, int low) {\n        return (low & 0xFFFFFFFFL) + ((high & 0xFFFFFFFFL) << 32);\n    }\n\n    @JsMethod\n    public static Long box(long in) {\n        return Long.valueOf(in);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/Pair.java",
    "content": "package peergos.shared.util;\n\nimport java.util.function.Function;\n\npublic class Pair<L,R> {\n    public final L left;\n    public final R right;\n\n    public Pair(L left, R right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    public <B,D> Pair<B, D> apply(Function<L, B> applyLeft, Function<R, D> applyRight) {\n        return new Pair<>(\n                applyLeft.apply(left),\n                applyRight.apply(right));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Pair<?, ?> pair = (Pair<?, ?>) o;\n\n        if (left != null ? !left.equals(pair.left) : pair.left != null) return false;\n        return right != null ? right.equals(pair.right) : pair.right == null;\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = left != null ? left.hashCode() : 0;\n        result = 31 * result + (right != null ? right.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"(\" + left.toString() + \", \" + right.toString() + \")\";\n    }\n\n    public static <E, F> Pair<E, F> of(E left, F right) {\n        return new Pair<>(left, right);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/PathUtil.java",
    "content": "package peergos.shared.util;\n\nimport java.nio.file.*;\nimport java.util.*;\n\npublic class PathUtil {\n\n    public static Path get(String in, String... rest) {\n        // tolerate windows path separators\n        in = in.trim().replaceAll(\"\\\\\\\\\", \"/\");\n        if (in.startsWith(\"/\"))\n            in = in.substring(1);\n        if (in.endsWith(\"/\"))\n            in = in.substring(0, in.length() - 1);\n        String[] split = in.split(\"/\");\n        if (split.length == 0 && rest.length == 0)\n            return Paths.get(\"\");\n        if (split.length == 1 && rest.length == 0)\n            return Paths.get(split[0]);\n        Path result = Paths.get(split[0], Arrays.copyOfRange(split, 1, split.length));\n        if (rest.length == 0)\n            return result;\n        return result.resolve(get(rest[0], Arrays.copyOfRange(rest, 1, rest.length)));\n    }\n\n    public static List<String> components(Path p) {\n        List<String> res = new ArrayList<>();\n        for (int i=0; i < p.getNameCount(); i++)\n            res.add(p.getName(i).toString());\n        return res;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/Plan.java",
    "content": "package peergos.shared.util;\n\nimport jsinterop.annotations.JsConstructor;\n\npublic class Plan {\n\n    public final long desiredQuota;\n    public final boolean annual;\n\n    @JsConstructor\n    public Plan(long desiredQuota, boolean annual) {\n        this.desiredQuota = desiredQuota;\n        this.annual = annual;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/ProgressConsumer.java",
    "content": "package peergos.shared.util;\n\nimport jsinterop.annotations.*;\n\n@FunctionalInterface\n@JsFunction\npublic interface ProgressConsumer<T> {\n\n    void accept(T t);\n}"
  },
  {
    "path": "src/peergos/shared/util/Serialize.java",
    "content": "package peergos.shared.util;\n\nimport jsinterop.annotations.*;\nimport peergos.shared.*;\nimport peergos.shared.cbor.*;\nimport peergos.shared.user.fs.*;\n\nimport java.io.*;\nimport java.util.Arrays;\nimport java.util.concurrent.*;\nimport java.util.function.*;\n\npublic class Serialize\n{\n\n    @JsMethod\n    public static byte[] newByteArray(int len) {\n        return new byte[len];\n    }\n\n    public static void serialize(byte[] b, DataOutput dout) throws IOException\n    {\n        dout.writeInt(b.length);\n        if (b.length > 0)\n            dout.write(b);\n    }\n\n    public static void serialize(String s, DataOutput dout) throws IOException\n    {\n        dout.writeInt(s.length());\n        dout.write(s.getBytes());\n    }\n\n    public static String deserializeString(DataInput din, int len) throws IOException\n    {\n        int l = din.readInt();\n        if (l < 0 || l > len)\n            throw new IOException(\"String size \"+ l + \" too long.\");\n        byte[] b = new byte[l];\n        din.readFully(b);\n        return new String(b);\n    }\n\n    public static byte[] deserializeByteArray(int length, DataInput din, int maxLength) throws IOException\n    {\n        if (length == 0)\n            return new byte[0];\n\n        byte[] b = getByteArray(length, maxLength);\n        din.readFully(b);\n        return b;\n    }\n\n    public static byte[] deserializeByteArray(DataInput din, int maxLength) throws IOException\n    {\n        int l = din.readInt();\n        if (l == 0)\n            return new byte[0];\n\n        byte[] b = getByteArray(l, maxLength);\n        din.readFully(b);\n        return b;\n    }\n\n    public static byte[] getByteArray(int len, int maxLength) throws IOException\n    {\n        if (len < 0 || len > maxLength)\n            throw new IOException(\"byte array of size \"+ len +\" too big.\");\n        return new byte[len];\n    }\n\n    public static byte[] readFully(InputStream in) throws IOException {\n        ByteArrayOutputStream bout =  new ByteArrayOutputStream();\n        byte[] b =  new  byte[0x1000];\n        int nRead;\n        while ((nRead = in.read(b, 0, b.length)) != -1 )\n            bout.write(b, 0, nRead);\n        in.close();\n        return bout.toByteArray();\n    }\n\n    public static byte[] readFully(InputStream in, int maxSize) throws IOException {\n        ByteArrayOutputStream bout =  new ByteArrayOutputStream();\n        byte[] b =  new  byte[0x1000];\n        int nRead;\n        int total = 0;\n        while ((nRead = in.read(b, 0, b.length)) != -1 ) {\n            total += nRead;\n            if (total > maxSize)\n                throw new IOException(\"Input exceeds maximum size of \" + maxSize + \" bytes\");\n            bout.write(b, 0, nRead);\n        }\n        in.close();\n        return bout.toByteArray();\n    }\n\n    public static byte[] read(InputStream in, int size) throws IOException {\n        byte[] res = new byte[size];\n        byte[] b =  new  byte[0x1000];\n        int nRead;\n        int offset = 0;\n        while (offset < size && (nRead = in.read(b, 0, b.length)) != -1 ) {\n            System.arraycopy(b, 0, res, offset, nRead);\n            offset += nRead;\n        }\n        return res;\n    }\n\n    public static CompletableFuture<byte[]> readFully(FileWrapper f, Crypto crypto, NetworkAccess network) {\n        long size = f.getSize();\n        return f.getInputStream(f.version.get(f.writer()), network, crypto, x -> {})\n                .thenCompose(stream -> readFully(stream, size));\n    }\n\n    public static CompletableFuture<byte[]> readFully(AsyncReader in, long size) {\n        byte[] res = new byte[(int)size];\n        return in.readIntoArray(res, 0, (int) size).thenApply(i -> res);\n    }\n\n    @JsMethod\n    public static <T> T parse(byte[] in, Function<Cborable, T> parser) {\n        return Cborable.parser(parser).apply(in);\n    }\n\n    public static <T> CompletableFuture<T> parse(FileWrapper f,\n                                                 Function<Cborable, T> parser,\n                                                 NetworkAccess network,\n                                                 Crypto crypto) {\n        byte[] res = new byte[(int)f.getSize()];\n        return f.getInputStream(f.version.get(f.writer()),network, crypto, x -> {})\n                .thenCompose(reader -> reader.readIntoArray(res, 0, (int) f.getSize()))\n                .thenApply(i -> Cborable.parser(parser).apply(res));\n    }\n\n    public static <T> CompletableFuture<T> parse(AsyncReader in, long size, Function<Cborable, T> parser) {\n        byte[] res = new byte[(int)size];\n        return in.readIntoArray(res, 0, (int) size)\n                .thenApply(i -> Cborable.parser(parser).apply(res));\n    }\n\n    public static CompletableFuture<Boolean> readFullArray(AsyncReader in, byte[] result) {\n        return in.readIntoArray(result, 0, result.length).thenApply(b -> true);\n    }\n\n    public static byte[] ensureSize(byte[] data, int  size) {\n        boolean iBigger = data.length < size;\n        return  iBigger ? Arrays.copyOf(data, size) : data;\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/TimeLimitedClient.java",
    "content": "package peergos.shared.util;\n\nimport peergos.shared.cbor.*;\nimport peergos.shared.crypto.asymmetric.*;\n\nimport java.util.*;\nimport java.util.concurrent.*;\n\npublic class TimeLimitedClient {\n\n    public static class SignedRequest implements Cborable {\n        public final String path;\n        public final long createdEpochMillis;\n\n        public SignedRequest(String path, long createdEpochMillis) {\n            this.path = path;\n            this.createdEpochMillis = createdEpochMillis;\n        }\n\n        @Override\n        public CborObject toCbor() {\n            SortedMap<String, Cborable> state = new TreeMap<>();\n            state.put(\"p\", new CborObject.CborString(path));\n            state.put(\"t\", new CborObject.CborLong(createdEpochMillis));\n            return CborObject.CborMap.build(state);\n        }\n\n        public CompletableFuture<byte[]> sign(SecretSigningKey signer) {\n            return signer.signMessage(serialize());\n        }\n\n        public static SignedRequest fromCbor(Cborable cbor) {\n            if (! (cbor instanceof CborObject.CborMap))\n                throw new IllegalStateException(\"Invalid cbor for SignedRequest! \" + cbor);\n            CborObject.CborMap m = (CborObject.CborMap) cbor;\n            String path = m.getString(\"p\");\n            long created = m.getLong(\"t\");\n            return new SignedRequest(path, created);\n        }\n    }\n\n    public static CompletableFuture<byte[]> signNow(SecretSigningKey signer) {\n        byte[] time = new CborObject.CborLong(System.currentTimeMillis()).serialize();\n        return signer.signMessage(time);\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/TriFunction.java",
    "content": "package peergos.shared.util;\n\nimport java.util.Objects;\nimport java.util.function.Function;\n\n@FunctionalInterface\npublic interface TriFunction<T, U, X, R> {\n\n    R apply(T t, U u, X x);\n\n    default <V> TriFunction<T, U, X, V> andThen(Function<? super R, ? extends V> after) {\n        Objects.requireNonNull(after);\n        return (T t, U u, X x) -> after.apply(apply(t, u, x));\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/Triple.java",
    "content": "package peergos.shared.util;\n\nimport java.util.function.*;\n\npublic class Triple<L,M, R> {\n    public final L left;\n    public final M middle;\n    public final R right;\n\n    public Triple(L left, M middle, R right) {\n        this.left = left;\n        this.middle = middle;\n        this.right = right;\n    }\n\n    public <B,C,D> Triple<B, C, D> apply(Function<L, B> applyLeft, Function<M, C> applyMiddle, Function<R, D> applyRight) {\n        return new Triple<>(\n                applyLeft.apply(left),\n                applyMiddle.apply(middle),\n                applyRight.apply(right));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Triple<?, ?, ?> triple = (Triple<?, ?, ?>) o;\n\n        if (left != null ? !left.equals(triple.left) : triple.left != null) return false;\n        if (middle != null ? !middle.equals(triple.middle) : triple.middle != null) return false;\n        return right != null ? right.equals(triple.right) : triple.right == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = left != null ? left.hashCode() : 0;\n        result = 31 * result + (middle != null ? middle.hashCode() : 0);\n        result = 31 * result + (right != null ? right.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"(\" + left.toString() + \", \" + middle.toString() + \", \" + right.toString() + \")\";\n    }\n}\n"
  },
  {
    "path": "src/peergos/shared/util/Version.java",
    "content": "package peergos.shared.util;\n\nimport jsinterop.annotations.JsType;\n\n@JsType\npublic class Version implements Comparable<Version> {\n\n    public final int major, minor, patch;\n    public final String suffix;\n\n    public Version(int major, int minor, int patch, String suffix) {\n        this.major = major;\n        this.minor = minor;\n        this.patch = patch;\n        this.suffix = suffix;\n    }\n\n    public String toString() {\n        return major + \".\" + minor + \".\" + patch + (suffix.length() > 0 ? \"-\" + suffix : \"\");\n    }\n\n    public boolean isBefore(Version other) {\n        return this.compareTo(other) < 0;\n    }\n\n    @Override\n    public int compareTo(Version other) {\n        int major = Integer.compare(this.major, other.major);\n        if (major != 0)\n            return major;\n        int minor = Integer.compare(this.minor, other.minor);\n        if (minor != 0)\n            return minor;\n        int patch = Integer.compare(this.patch, other.patch);\n        if (patch != 0)\n            return patch;\n        if (suffix.length() == 0 || other.suffix.length() == 0)\n            return other.suffix.length() - suffix.length();\n        return suffix.compareTo(other.suffix);\n    }\n\n    public static Version parse(String version) {\n        int first = version.indexOf(\".\");\n        int second = version.indexOf(\".\", first + 1);\n        int third = version.contains(\"-\") ? version.indexOf(\"-\") : version.length();\n\n        int major = Integer.parseInt(version.substring(0, first));\n        int minor = Integer.parseInt(version.substring(first + 1, second));\n        int patch = Integer.parseInt(version.substring(second + 1, third));\n        String suffix = third < version.length() ? version.substring(third + 1) : \"\";\n        return new Version(major, minor, patch, suffix);\n    }\n}"
  },
  {
    "path": "src/peergos/shared/zxing/BarcodeFormat.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * Enumerates barcode formats known to this package. Please keep alphabetized.\n *\n * @author Sean Owen\n */\npublic enum BarcodeFormat {\n\n  /** Aztec 2D barcode format. */\n  AZTEC,\n\n  /** CODABAR 1D format. */\n  CODABAR,\n\n  /** Code 39 1D format. */\n  CODE_39,\n\n  /** Code 93 1D format. */\n  CODE_93,\n\n  /** Code 128 1D format. */\n  CODE_128,\n\n  /** Data Matrix 2D barcode format. */\n  DATA_MATRIX,\n\n  /** EAN-8 1D format. */\n  EAN_8,\n\n  /** EAN-13 1D format. */\n  EAN_13,\n\n  /** ITF (Interleaved Two of Five) 1D format. */\n  ITF,\n\n  /** MaxiCode 2D barcode format. */\n  MAXICODE,\n\n  /** PDF417 format. */\n  PDF_417,\n\n  /** QR Code 2D barcode format. */\n  QR_CODE,\n\n  /** RSS 14 */\n  RSS_14,\n\n  /** RSS EXPANDED */\n  RSS_EXPANDED,\n\n  /** UPC-A 1D format. */\n  UPC_A,\n\n  /** UPC-E 1D format. */\n  UPC_E,\n\n  /** UPC/EAN extension format. Not a stand-alone format. */\n  UPC_EAN_EXTENSION\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/Binarizer.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\nimport peergos.shared.zxing.common.BitArray;\nimport peergos.shared.zxing.common.BitMatrix;\n\n/**\n * This class hierarchy provides a set of methods to convert luminance data to 1 bit data.\n * It allows the algorithm to vary polymorphically, for example allowing a very expensive\n * thresholding technique for servers and a fast one for mobile. It also permits the implementation\n * to vary, e.g. a JNI version for Android and a Java fallback version for other platforms.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic abstract class Binarizer {\n\n  private final LuminanceSource source;\n\n  protected Binarizer(LuminanceSource source) {\n    this.source = source;\n  }\n\n  public final LuminanceSource getLuminanceSource() {\n    return source;\n  }\n\n  /**\n   * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return\n   * cached data. Callers should assume this method is expensive and call it as seldom as possible.\n   * This method is intended for decoding 1D barcodes and may choose to apply sharpening.\n   * For callers which only examine one row of pixels at a time, the same BitArray should be reused\n   * and passed in with each call for performance. However it is legal to keep more than one row\n   * at a time if needed.\n   *\n   * @param y The row to fetch, which must be in [0, bitmap height)\n   * @param row An optional preallocated array. If null or too small, it will be ignored.\n   *            If used, the Binarizer will call BitArray.clear(). Always use the returned object.\n   * @return The array of bits for this row (true means black).\n   * @throws NotFoundException if row can't be binarized\n   */\n  public abstract BitArray getBlackRow(int y, BitArray row) throws NotFoundException;\n\n  /**\n   * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive\n   * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or\n   * may not apply sharpening. Therefore, a row from this matrix may not be identical to one\n   * fetched using getBlackRow(), so don't mix and match between them.\n   *\n   * @return The 2D array of bits for the image (true means black).\n   * @throws NotFoundException if image can't be binarized to make a matrix\n   */\n  public abstract BitMatrix getBlackMatrix() throws NotFoundException;\n\n  /**\n   * Creates a new object with the same type as this Binarizer implementation, but with pristine\n   * state. This is needed because Binarizer implementations may be stateful, e.g. keeping a cache\n   * of 1 bit data. See Effective Java for why we can't use Java's clone() method.\n   *\n   * @param source The LuminanceSource this Binarizer will operate on.\n   * @return A new concrete Binarizer implementation object.\n   */\n  public abstract Binarizer createBinarizer(LuminanceSource source);\n\n  public final int getWidth() {\n    return source.getWidth();\n  }\n\n  public final int getHeight() {\n    return source.getHeight();\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/BinaryBitmap.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\nimport peergos.shared.zxing.common.BitArray;\nimport peergos.shared.zxing.common.BitMatrix;\n\n/**\n * This class is the core bitmap class used by ZXing to represent 1 bit data. Reader objects\n * accept a BinaryBitmap and attempt to decode it.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic final class BinaryBitmap {\n\n  private final Binarizer binarizer;\n  private BitMatrix matrix;\n\n  public BinaryBitmap(Binarizer binarizer) {\n    if (binarizer == null) {\n      throw new IllegalArgumentException(\"Binarizer must be non-null.\");\n    }\n    this.binarizer = binarizer;\n  }\n\n  /**\n   * @return The width of the bitmap.\n   */\n  public int getWidth() {\n    return binarizer.getWidth();\n  }\n\n  /**\n   * @return The height of the bitmap.\n   */\n  public int getHeight() {\n    return binarizer.getHeight();\n  }\n\n  /**\n   * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return\n   * cached data. Callers should assume this method is expensive and call it as seldom as possible.\n   * This method is intended for decoding 1D barcodes and may choose to apply sharpening.\n   *\n   * @param y The row to fetch, which must be in [0, bitmap height)\n   * @param row An optional preallocated array. If null or too small, it will be ignored.\n   *            If used, the Binarizer will call BitArray.clear(). Always use the returned object.\n   * @return The array of bits for this row (true means black).\n   * @throws NotFoundException if row can't be binarized\n   */\n  public BitArray getBlackRow(int y, BitArray row) throws NotFoundException {\n    return binarizer.getBlackRow(y, row);\n  }\n\n  /**\n   * Converts a 2D array of luminance data to 1 bit. As above, assume this method is expensive\n   * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or\n   * may not apply sharpening. Therefore, a row from this matrix may not be identical to one\n   * fetched using getBlackRow(), so don't mix and match between them.\n   *\n   * @return The 2D array of bits for the image (true means black).\n   * @throws NotFoundException if image can't be binarized to make a matrix\n   */\n  public BitMatrix getBlackMatrix() throws NotFoundException {\n    // The matrix is created on demand the first time it is requested, then cached. There are two\n    // reasons for this:\n    // 1. This work will never be done if the caller only installs 1D Reader objects, or if a\n    //    1D Reader finds a barcode before the 2D Readers run.\n    // 2. This work will only be done once even if the caller installs multiple 2D Readers.\n    if (matrix == null) {\n      matrix = binarizer.getBlackMatrix();\n    }\n    return matrix;\n  }\n\n  /**\n   * @return Whether this bitmap can be cropped.\n   */\n  public boolean isCropSupported() {\n    return binarizer.getLuminanceSource().isCropSupported();\n  }\n\n  /**\n   * Returns a new object with cropped image data. Implementations may keep a reference to the\n   * original data rather than a copy. Only callable if isCropSupported() is true.\n   *\n   * @param left The left coordinate, which must be in [0,getWidth())\n   * @param top The top coordinate, which must be in [0,getHeight())\n   * @param width The width of the rectangle to crop.\n   * @param height The height of the rectangle to crop.\n   * @return A cropped version of this object.\n   */\n  public BinaryBitmap crop(int left, int top, int width, int height) {\n    LuminanceSource newSource = binarizer.getLuminanceSource().crop(left, top, width, height);\n    return new BinaryBitmap(binarizer.createBinarizer(newSource));\n  }\n\n  /**\n   * @return Whether this bitmap supports counter-clockwise rotation.\n   */\n  public boolean isRotateSupported() {\n    return binarizer.getLuminanceSource().isRotateSupported();\n  }\n\n  /**\n   * Returns a new object with rotated image data by 90 degrees counterclockwise.\n   * Only callable if {@link #isRotateSupported()} is true.\n   *\n   * @return A rotated version of this object.\n   */\n  public BinaryBitmap rotateCounterClockwise() {\n    LuminanceSource newSource = binarizer.getLuminanceSource().rotateCounterClockwise();\n    return new BinaryBitmap(binarizer.createBinarizer(newSource));\n  }\n\n  /**\n   * Returns a new object with rotated image data by 45 degrees counterclockwise.\n   * Only callable if {@link #isRotateSupported()} is true.\n   *\n   * @return A rotated version of this object.\n   */\n  public BinaryBitmap rotateCounterClockwise45() {\n    LuminanceSource newSource = binarizer.getLuminanceSource().rotateCounterClockwise45();\n    return new BinaryBitmap(binarizer.createBinarizer(newSource));\n  }\n\n  @Override\n  public String toString() {\n    try {\n      return getBlackMatrix().toString();\n    } catch (NotFoundException e) {\n      return \"\";\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/ChecksumException.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * Thrown when a barcode was successfully detected and decoded, but\n * was not returned because its checksum feature failed.\n *\n * @author Sean Owen\n */\npublic final class ChecksumException extends ReaderException {\n\n  private static final ChecksumException INSTANCE = new ChecksumException();\n  static {\n    INSTANCE.setStackTrace(NO_TRACE); // since it's meaningless\n  }\n\n  private ChecksumException() {\n    // do nothing\n  }\n\n  private ChecksumException(Throwable cause) {\n    super(cause);\n  }\n\n  public static ChecksumException getChecksumInstance() {\n    return isStackTrace ? new ChecksumException() : INSTANCE;\n  }\n\n  public static ChecksumException getChecksumInstance(Throwable cause) {\n    return isStackTrace ? new ChecksumException(cause) : INSTANCE;\n  }\n}"
  },
  {
    "path": "src/peergos/shared/zxing/DecodeHintType.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\nimport java.util.List;\n\n/**\n * Encapsulates a type of hint that a caller may pass to a barcode reader to help it\n * more quickly or accurately decode it. It is up to implementations to decide what,\n * if anything, to do with the information that is supplied.\n *\n * @author Sean Owen\n * @author dswitkin@google.com (Daniel Switkin)\n * @see Reader#decode(BinaryBitmap,java.util.Map)\n */\npublic enum DecodeHintType {\n\n  /**\n   * Unspecified, application-specific hint. Maps to an unspecified {@link Object}.\n   */\n  OTHER(Object.class),\n\n  /**\n   * Image is a pure monochrome image of a barcode. Doesn't matter what it maps to;\n   * use {@link Boolean#TRUE}.\n   */\n  PURE_BARCODE(Void.class),\n\n  /**\n   * Image is known to be of one of a few possible formats.\n   * Maps to a {@link List} of {@link BarcodeFormat}s.\n   */\n  POSSIBLE_FORMATS(List.class),\n\n  /**\n   * Spend more time to try to find a barcode; optimize for accuracy, not speed.\n   * Doesn't matter what it maps to; use {@link Boolean#TRUE}.\n   */\n  TRY_HARDER(Void.class),\n\n  /**\n   * Specifies what character encoding to use when decoding, where applicable (type String)\n   */\n  CHARACTER_SET(String.class),\n\n  /**\n   * Allowed lengths of encoded data -- reject anything else. Maps to an {@code int[]}.\n   */\n  ALLOWED_LENGTHS(int[].class),\n\n  /**\n   * Assume Code 39 codes employ a check digit. Doesn't matter what it maps to;\n   * use {@link Boolean#TRUE}.\n   */\n  ASSUME_CODE_39_CHECK_DIGIT(Void.class),\n\n  /**\n   * Assume the barcode is being processed as a GS1 barcode, and modify behavior as needed.\n   * For example this affects FNC1 handling for Code 128 (aka GS1-128). Doesn't matter what it maps to;\n   * use {@link Boolean#TRUE}.\n   */\n  ASSUME_GS1(Void.class),\n\n  /**\n   * If true, return the start and end digits in a Codabar barcode instead of stripping them. They\n   * are alpha, whereas the rest are numeric. By default, they are stripped, but this causes them\n   * to not be. Doesn't matter what it maps to; use {@link Boolean#TRUE}.\n   */\n  RETURN_CODABAR_START_END(Void.class),\n\n  /**\n   * The caller needs to be notified via callback when a possible {@link ResultPoint}\n   * is found. Maps to a {@link ResultPointCallback}.\n   */\n  NEED_RESULT_POINT_CALLBACK(ResultPointCallback.class),\n\n\n  /**\n   * Allowed extension lengths for EAN or UPC barcodes. Other formats will ignore this.\n   * Maps to an {@code int[]} of the allowed extension lengths, for example [2], [5], or [2, 5].\n   * If it is optional to have an extension, do not set this hint. If this is set,\n   * and a UPC or EAN barcode is found but an extension is not, then no result will be returned\n   * at all.\n   */\n  ALLOWED_EAN_EXTENSIONS(int[].class),\n\n  // End of enumeration values.\n  ;\n\n  /**\n   * Data type the hint is expecting.\n   * Among the possible values the {@link Void} stands out as being used for\n   * hints that do not expect a value to be supplied (flag hints). Such hints\n   * will possibly have their value ignored, or replaced by a\n   * {@link Boolean#TRUE}. Hint suppliers should probably use\n   * {@link Boolean#TRUE} as directed by the actual hint documentation.\n   */\n  private final Class<?> valueType;\n\n  DecodeHintType(Class<?> valueType) {\n    this.valueType = valueType;\n  }\n\n  public Class<?> getValueType() {\n    return valueType;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/Dimension.java",
    "content": "/*\n * Copyright 2012 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * Simply encapsulates a width and height.\n */\npublic final class Dimension {\n\n  private final int width;\n  private final int height;\n\n  public Dimension(int width, int height) {\n    if (width < 0 || height < 0) {\n      throw new IllegalArgumentException();\n    }\n    this.width = width;\n    this.height = height;\n  }\n\n  public int getWidth() {\n    return width;\n  }\n\n  public int getHeight() {\n    return height;\n  }\n\n  @Override\n  public boolean equals(Object other) {\n    if (other instanceof Dimension) {\n      Dimension d = (Dimension) other;\n      return width == d.width && height == d.height;\n    }\n    return false;\n  }\n\n  @Override\n  public int hashCode() {\n      return width * 32713 + height;\n  }\n\n  @Override\n  public String toString() {\n    return width + \"x\" + height;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/EncodeHintType.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * These are a set of hints that you may pass to Writers to specify their behavior.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic enum EncodeHintType {\n\n  /**\n   * Specifies what degree of error correction to use, for example in QR Codes.\n   * Type depends on the encoder. For example for QR codes it's type\n   * {@link peergos.shared.zxing.qrcode.decoder.ErrorCorrectionLevel ErrorCorrectionLevel}.\n   * For Aztec it is of type {@link Integer}, representing the minimal percentage of error correction words.\n   * For PDF417 it is of type {@link Integer}, valid values being 0 to 8.\n   * In all cases, it can also be a {@link String} representation of the desired value as well.\n   * Note: an Aztec symbol should have a minimum of 25% EC words.\n   */\n  ERROR_CORRECTION,\n\n  /**\n   * Specifies what character encoding to use where applicable (type {@link String})\n   */\n  CHARACTER_SET,\n\n  /**\n   * Specifies the matrix shape for Data Matrix (type {@link peergos.shared.zxing.datamatrix.encoder.SymbolShapeHint})\n   */\n  DATA_MATRIX_SHAPE,\n\n  /**\n   * Specifies a minimum barcode size (type {@link Dimension}). Only applicable to Data Matrix now.\n   *\n   * @deprecated use width/height params in\n   * {@link peergos.shared.zxing.datamatrix.DataMatrixWriter#encode(String, BarcodeFormat, int, int)}\n   */\n  @Deprecated\n  MIN_SIZE,\n\n  /**\n   * Specifies a maximum barcode size (type {@link Dimension}). Only applicable to Data Matrix now.\n   *\n   * @deprecated without replacement\n   */\n  @Deprecated\n  MAX_SIZE,\n\n  /**\n   * Specifies margin, in pixels, to use when generating the barcode. The meaning can vary\n   * by format; for example it controls margin before and after the barcode horizontally for\n   * most 1D formats. (Type {@link Integer}, or {@link String} representation of the integer value).\n   */\n  MARGIN,\n\n  /**\n   * Specifies whether to use compact mode for PDF417 (type {@link Boolean}, or \"true\" or \"false\"\n   * {@link String} value).\n   */\n  PDF417_COMPACT,\n\n  /**\n   * Specifies what compaction mode to use for PDF417 (type\n   * {@link peergos.shared.zxing.pdf417.encoder.Compaction Compaction} or {@link String} value of one of its\n   * enum values).\n   */\n  PDF417_COMPACTION,\n\n  /**\n   * Specifies the minimum and maximum number of rows and columns for PDF417 (type\n   * {@link peergos.shared.zxing.pdf417.encoder.Dimensions Dimensions}).\n   */\n  PDF417_DIMENSIONS,\n\n  /**\n   * Specifies the required number of layers for an Aztec code.\n   * A negative number (-1, -2, -3, -4) specifies a compact Aztec code.\n   * 0 indicates to use the minimum number of layers (the default).\n   * A positive number (1, 2, .. 32) specifies a normal (non-compact) Aztec code.\n   * (Type {@link Integer}, or {@link String} representation of the integer value).\n   */\n   AZTEC_LAYERS,\n\n   /**\n    * Specifies the exact version of QR code to be encoded.\n    * (Type {@link Integer}, or {@link String} representation of the integer value).\n    */\n   QR_VERSION,\n\n  /**\n   * Specifies the QR code mask pattern to be used. Allowed values are\n   * 0..QRCode.NUM_MASK_PATTERNS-1. By default the code will automatically select\n   * the optimal mask pattern.\n   * * (Type {@link Integer}, or {@link String} representation of the integer value).\n   */\n  QR_MASK_PATTERN,\n\n  /**\n   * Specifies whether the data should be encoded to the GS1 standard (type {@link Boolean}, or \"true\" or \"false\"\n   * {@link String } value).\n   */\n  GS1_FORMAT,\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/FormatException.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * Thrown when a barcode was successfully detected, but some aspect of\n * the content did not conform to the barcode's format rules. This could have\n * been due to a mis-detection.\n *\n * @author Sean Owen\n */\npublic final class FormatException extends ReaderException {\n\n  private static final FormatException INSTANCE = new FormatException();\n  static {\n    INSTANCE.setStackTrace(NO_TRACE); // since it's meaningless\n  }\n\n  private FormatException() {\n  }\n\n  private FormatException(Throwable cause) {\n    super(cause);\n  }\n\n  public static FormatException getFormatInstance() {\n    return isStackTrace ? new FormatException() : INSTANCE;\n  }\n\n  public static FormatException getFormatInstance(Throwable cause) {\n    return isStackTrace ? new FormatException(cause) : INSTANCE;\n  }\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/InvertedLuminanceSource.java",
    "content": "/*\n * Copyright 2013 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * A wrapper implementation of {@link LuminanceSource} which inverts the luminances it returns -- black becomes\n * white and vice versa, and each value becomes (255-value).\n *\n * @author Sean Owen\n */\npublic final class InvertedLuminanceSource extends LuminanceSource {\n\n  private final LuminanceSource delegate;\n\n  public InvertedLuminanceSource(LuminanceSource delegate) {\n    super(delegate.getWidth(), delegate.getHeight());\n    this.delegate = delegate;\n  }\n\n  @Override\n  public byte[] getRow(int y, byte[] row) {\n    row = delegate.getRow(y, row);\n    int width = getWidth();\n    for (int i = 0; i < width; i++) {\n      row[i] = (byte) (255 - (row[i] & 0xFF));\n    }\n    return row;\n  }\n\n  @Override\n  public byte[] getMatrix() {\n    byte[] matrix = delegate.getMatrix();\n    int length = getWidth() * getHeight();\n    byte[] invertedMatrix = new byte[length];\n    for (int i = 0; i < length; i++) {\n      invertedMatrix[i] = (byte) (255 - (matrix[i] & 0xFF));\n    }\n    return invertedMatrix;\n  }\n\n  @Override\n  public boolean isCropSupported() {\n    return delegate.isCropSupported();\n  }\n\n  @Override\n  public LuminanceSource crop(int left, int top, int width, int height) {\n    return new InvertedLuminanceSource(delegate.crop(left, top, width, height));\n  }\n\n  @Override\n  public boolean isRotateSupported() {\n    return delegate.isRotateSupported();\n  }\n\n  /**\n   * @return original delegate {@link LuminanceSource} since invert undoes itself\n   */\n  @Override\n  public LuminanceSource invert() {\n    return delegate;\n  }\n\n  @Override\n  public LuminanceSource rotateCounterClockwise() {\n    return new InvertedLuminanceSource(delegate.rotateCounterClockwise());\n  }\n\n  @Override\n  public LuminanceSource rotateCounterClockwise45() {\n    return new InvertedLuminanceSource(delegate.rotateCounterClockwise45());\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/LuminanceSource.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * The purpose of this class hierarchy is to abstract different bitmap implementations across\n * platforms into a standard interface for requesting greyscale luminance values. The interface\n * only provides immutable methods; therefore crop and rotation create copies. This is to ensure\n * that one Reader does not modify the original luminance source and leave it in an unknown state\n * for other Readers in the chain.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic abstract class LuminanceSource {\n\n  private final int width;\n  private final int height;\n\n  protected LuminanceSource(int width, int height) {\n    this.width = width;\n    this.height = height;\n  }\n\n  /**\n   * Fetches one row of luminance data from the underlying platform's bitmap. Values range from\n   * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have\n   * to bitwise and with 0xff for each value. It is preferable for implementations of this method\n   * to only fetch this row rather than the whole image, since no 2D Readers may be installed and\n   * getMatrix() may never be called.\n   *\n   * @param y The row to fetch, which must be in [0,getHeight())\n   * @param row An optional preallocated array. If null or too small, it will be ignored.\n   *            Always use the returned object, and ignore the .length of the array.\n   * @return An array containing the luminance data.\n   */\n  public abstract byte[] getRow(int y, byte[] row);\n\n  /**\n   * Fetches luminance data for the underlying bitmap. Values should be fetched using:\n   * {@code int luminance = array[y * width + x] & 0xff}\n   *\n   * @return A row-major 2D array of luminance values. Do not use result.length as it may be\n   *         larger than width * height bytes on some platforms. Do not modify the contents\n   *         of the result.\n   */\n  public abstract byte[] getMatrix();\n\n  /**\n   * @return The width of the bitmap.\n   */\n  public final int getWidth() {\n    return width;\n  }\n\n  /**\n   * @return The height of the bitmap.\n   */\n  public final int getHeight() {\n    return height;\n  }\n\n  /**\n   * @return Whether this subclass supports cropping.\n   */\n  public boolean isCropSupported() {\n    return false;\n  }\n\n  /**\n   * Returns a new object with cropped image data. Implementations may keep a reference to the\n   * original data rather than a copy. Only callable if isCropSupported() is true.\n   *\n   * @param left The left coordinate, which must be in [0,getWidth())\n   * @param top The top coordinate, which must be in [0,getHeight())\n   * @param width The width of the rectangle to crop.\n   * @param height The height of the rectangle to crop.\n   * @return A cropped version of this object.\n   */\n  public LuminanceSource crop(int left, int top, int width, int height) {\n    throw new UnsupportedOperationException(\"This luminance source does not support cropping.\");\n  }\n\n  /**\n   * @return Whether this subclass supports counter-clockwise rotation.\n   */\n  public boolean isRotateSupported() {\n    return false;\n  }\n\n  /**\n   * @return a wrapper of this {@code LuminanceSource} which inverts the luminances it returns -- black becomes\n   *  white and vice versa, and each value becomes (255-value).\n   */\n  public LuminanceSource invert() {\n    return new InvertedLuminanceSource(this);\n  }\n\n  /**\n   * Returns a new object with rotated image data by 90 degrees counterclockwise.\n   * Only callable if {@link #isRotateSupported()} is true.\n   *\n   * @return A rotated version of this object.\n   */\n  public LuminanceSource rotateCounterClockwise() {\n    throw new UnsupportedOperationException(\"This luminance source does not support rotation by 90 degrees.\");\n  }\n\n  /**\n   * Returns a new object with rotated image data by 45 degrees counterclockwise.\n   * Only callable if {@link #isRotateSupported()} is true.\n   *\n   * @return A rotated version of this object.\n   */\n  public LuminanceSource rotateCounterClockwise45() {\n    throw new UnsupportedOperationException(\"This luminance source does not support rotation by 45 degrees.\");\n  }\n\n  @Override\n  public final String toString() {\n    byte[] row = new byte[width];\n    StringBuilder result = new StringBuilder(height * (width + 1));\n    for (int y = 0; y < height; y++) {\n      row = getRow(y, row);\n      for (int x = 0; x < width; x++) {\n        int luminance = row[x] & 0xFF;\n        char c;\n        if (luminance < 0x40) {\n          c = '#';\n        } else if (luminance < 0x80) {\n          c = '+';\n        } else if (luminance < 0xC0) {\n          c = '.';\n        } else {\n          c = ' ';\n        }\n        result.append(c);\n      }\n      result.append('\\n');\n    }\n    return result.toString();\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/NotFoundException.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * Thrown when a barcode was not found in the image. It might have been\n * partially detected but could not be confirmed.\n *\n * @author Sean Owen\n */\npublic final class NotFoundException extends ReaderException {\n\n  private static final NotFoundException INSTANCE = new NotFoundException();\n  static {\n    INSTANCE.setStackTrace(NO_TRACE); // since it's meaningless\n  }\n\n  private NotFoundException() {\n    // do nothing\n  }\n\n  public static NotFoundException getNotFoundInstance() {\n    return INSTANCE;\n  }\n\n}"
  },
  {
    "path": "src/peergos/shared/zxing/PlanarYUVLuminanceSource.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * This object extends LuminanceSource around an array of YUV data returned from the camera driver,\n * with the option to crop to a rectangle within the full data. This can be used to exclude\n * superfluous pixels around the perimeter and speed up decoding.\n *\n * It works for any pixel format where the Y channel is planar and appears first, including\n * YCbCr_420_SP and YCbCr_422_SP.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic final class PlanarYUVLuminanceSource extends LuminanceSource {\n\n  private static final int THUMBNAIL_SCALE_FACTOR = 2;\n\n  private final byte[] yuvData;\n  private final int dataWidth;\n  private final int dataHeight;\n  private final int left;\n  private final int top;\n\n  public PlanarYUVLuminanceSource(byte[] yuvData,\n                                  int dataWidth,\n                                  int dataHeight,\n                                  int left,\n                                  int top,\n                                  int width,\n                                  int height,\n                                  boolean reverseHorizontal) {\n    super(width, height);\n\n    if (left + width > dataWidth || top + height > dataHeight) {\n      throw new IllegalArgumentException(\"Crop rectangle does not fit within image data.\");\n    }\n\n    this.yuvData = yuvData;\n    this.dataWidth = dataWidth;\n    this.dataHeight = dataHeight;\n    this.left = left;\n    this.top = top;\n    if (reverseHorizontal) {\n      reverseHorizontal(width, height);\n    }\n  }\n\n  @Override\n  public byte[] getRow(int y, byte[] row) {\n    if (y < 0 || y >= getHeight()) {\n      throw new IllegalArgumentException(\"Requested row is outside the image: \" + y);\n    }\n    int width = getWidth();\n    if (row == null || row.length < width) {\n      row = new byte[width];\n    }\n    int offset = (y + top) * dataWidth + left;\n    System.arraycopy(yuvData, offset, row, 0, width);\n    return row;\n  }\n\n  @Override\n  public byte[] getMatrix() {\n    int width = getWidth();\n    int height = getHeight();\n\n    // If the caller asks for the entire underlying image, save the copy and give them the\n    // original data. The docs specifically warn that result.length must be ignored.\n    if (width == dataWidth && height == dataHeight) {\n      return yuvData;\n    }\n\n    int area = width * height;\n    byte[] matrix = new byte[area];\n    int inputOffset = top * dataWidth + left;\n\n    // If the width matches the full width of the underlying data, perform a single copy.\n    if (width == dataWidth) {\n      System.arraycopy(yuvData, inputOffset, matrix, 0, area);\n      return matrix;\n    }\n\n    // Otherwise copy one cropped row at a time.\n    for (int y = 0; y < height; y++) {\n      int outputOffset = y * width;\n      System.arraycopy(yuvData, inputOffset, matrix, outputOffset, width);\n      inputOffset += dataWidth;\n    }\n    return matrix;\n  }\n\n  @Override\n  public boolean isCropSupported() {\n    return true;\n  }\n\n  @Override\n  public LuminanceSource crop(int left, int top, int width, int height) {\n    return new PlanarYUVLuminanceSource(yuvData,\n                                        dataWidth,\n                                        dataHeight,\n                                        this.left + left,\n                                        this.top + top,\n                                        width,\n                                        height,\n                                        false);\n  }\n\n  public int[] renderThumbnail() {\n    int width = getWidth() / THUMBNAIL_SCALE_FACTOR;\n    int height = getHeight() / THUMBNAIL_SCALE_FACTOR;\n    int[] pixels = new int[width * height];\n    byte[] yuv = yuvData;\n    int inputOffset = top * dataWidth + left;\n\n    for (int y = 0; y < height; y++) {\n      int outputOffset = y * width;\n      for (int x = 0; x < width; x++) {\n        int grey = yuv[inputOffset + x * THUMBNAIL_SCALE_FACTOR] & 0xff;\n        pixels[outputOffset + x] = 0xFF000000 | (grey * 0x00010101);\n      }\n      inputOffset += dataWidth * THUMBNAIL_SCALE_FACTOR;\n    }\n    return pixels;\n  }\n\n  /**\n   * @return width of image from {@link #renderThumbnail()}\n   */\n  public int getThumbnailWidth() {\n    return getWidth() / THUMBNAIL_SCALE_FACTOR;\n  }\n\n  /**\n   * @return height of image from {@link #renderThumbnail()}\n   */\n  public int getThumbnailHeight() {\n    return getHeight() / THUMBNAIL_SCALE_FACTOR;\n  }\n\n  private void reverseHorizontal(int width, int height) {\n    byte[] yuvData = this.yuvData;\n    for (int y = 0, rowStart = top * dataWidth + left; y < height; y++, rowStart += dataWidth) {\n      int middle = rowStart + width / 2;\n      for (int x1 = rowStart, x2 = rowStart + width - 1; x1 < middle; x1++, x2--) {\n        byte temp = yuvData[x1];\n        yuvData[x1] = yuvData[x2];\n        yuvData[x2] = temp;\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/RGBLuminanceSource.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * This class is used to help decode images from files which arrive as RGB data from\n * an ARGB pixel array. It does not support rotation.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n * @author Betaminos\n */\npublic final class RGBLuminanceSource extends LuminanceSource {\n\n  private final byte[] luminances;\n  private final int dataWidth;\n  private final int dataHeight;\n  private final int left;\n  private final int top;\n\n  public RGBLuminanceSource(int width, int height, int[] pixels) {\n    super(width, height);\n\n    dataWidth = width;\n    dataHeight = height;\n    left = 0;\n    top = 0;\n\n    // In order to measure pure decoding speed, we convert the entire image to a greyscale array\n    // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app.\n    //\n    // Total number of pixels suffices, can ignore shape\n    int size = width * height;\n    luminances = new byte[size];\n    for (int offset = 0; offset < size; offset++) {\n      int pixel = pixels[offset];\n      int r = (pixel >> 16) & 0xff; // red\n      int g2 = (pixel >> 7) & 0x1fe; // 2 * green\n      int b = pixel & 0xff; // blue\n      // Calculate green-favouring average cheaply\n      luminances[offset] = (byte) ((r + g2 + b) / 4);\n    }\n  }\n\n  private RGBLuminanceSource(byte[] pixels,\n                             int dataWidth,\n                             int dataHeight,\n                             int left,\n                             int top,\n                             int width,\n                             int height) {\n    super(width, height);\n    if (left + width > dataWidth || top + height > dataHeight) {\n      throw new IllegalArgumentException(\"Crop rectangle does not fit within image data.\");\n    }\n    this.luminances = pixels;\n    this.dataWidth = dataWidth;\n    this.dataHeight = dataHeight;\n    this.left = left;\n    this.top = top;\n  }\n\n  @Override\n  public byte[] getRow(int y, byte[] row) {\n    if (y < 0 || y >= getHeight()) {\n      throw new IllegalArgumentException(\"Requested row is outside the image: \" + y);\n    }\n    int width = getWidth();\n    if (row == null || row.length < width) {\n      row = new byte[width];\n    }\n    int offset = (y + top) * dataWidth + left;\n    System.arraycopy(luminances, offset, row, 0, width);\n    return row;\n  }\n\n  @Override\n  public byte[] getMatrix() {\n    int width = getWidth();\n    int height = getHeight();\n\n    // If the caller asks for the entire underlying image, save the copy and give them the\n    // original data. The docs specifically warn that result.length must be ignored.\n    if (width == dataWidth && height == dataHeight) {\n      return luminances;\n    }\n\n    int area = width * height;\n    byte[] matrix = new byte[area];\n    int inputOffset = top * dataWidth + left;\n\n    // If the width matches the full width of the underlying data, perform a single copy.\n    if (width == dataWidth) {\n      System.arraycopy(luminances, inputOffset, matrix, 0, area);\n      return matrix;\n    }\n\n    // Otherwise copy one cropped row at a time.\n    for (int y = 0; y < height; y++) {\n      int outputOffset = y * width;\n      System.arraycopy(luminances, inputOffset, matrix, outputOffset, width);\n      inputOffset += dataWidth;\n    }\n    return matrix;\n  }\n\n  @Override\n  public boolean isCropSupported() {\n    return true;\n  }\n\n  @Override\n  public LuminanceSource crop(int left, int top, int width, int height) {\n    return new RGBLuminanceSource(luminances,\n                                  dataWidth,\n                                  dataHeight,\n                                  this.left + left,\n                                  this.top + top,\n                                  width,\n                                  height);\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/Reader.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\nimport java.util.Map;\n\n/**\n * Implementations of this interface can decode an image of a barcode in some format into\n * the String it encodes. For example, {@link peergos.shared.zxing.qrcode.QRCodeReader} can\n * decode a QR code. The decoder may optionally receive hints from the caller which may help\n * it decode more quickly or accurately.\n *\n * See {@link MultiFormatReader}, which attempts to determine what barcode\n * format is present within the image as well, and then decodes it accordingly.\n *\n * @author Sean Owen\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic interface Reader {\n\n  /**\n   * Locates and decodes a barcode in some format within an image.\n   *\n   * @param image image of barcode to decode\n   * @return String which the barcode encodes\n   * @throws NotFoundException if no potential barcode is found\n   * @throws ChecksumException if a potential barcode is found but does not pass its checksum\n   * @throws FormatException if a potential barcode is found but format is invalid\n   */\n  Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException;\n\n  /**\n   * Locates and decodes a barcode in some format within an image. This method also accepts\n   * hints, each possibly associated to some data, which may help the implementation decode.\n   *\n   * @param image image of barcode to decode\n   * @param hints passed as a {@link Map} from {@link DecodeHintType}\n   * to arbitrary data. The\n   * meaning of the data depends upon the hint type. The implementation may or may not do\n   * anything with these hints.\n   * @return String which the barcode encodes\n   * @throws NotFoundException if no potential barcode is found\n   * @throws ChecksumException if a potential barcode is found but does not pass its checksum\n   * @throws FormatException if a potential barcode is found but format is invalid\n   */\n  Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints)\n      throws NotFoundException, ChecksumException, FormatException;\n\n  /**\n   * Resets any internal state the implementation has after a decode, to prepare it\n   * for reuse.\n   */\n  void reset();\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/ReaderException.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * The general exception class throw when something goes wrong during decoding of a barcode.\n * This includes, but is not limited to, failing checksums / error correction algorithms, being\n * unable to locate finder timing patterns, and so on.\n *\n * @author Sean Owen\n */\npublic abstract class ReaderException extends Exception {\n\n  protected static final boolean isStackTrace = true;\n  protected static final StackTraceElement[] NO_TRACE = new StackTraceElement[0];\n\n  ReaderException() {\n    // do nothing\n  }\n\n  ReaderException(Throwable cause) {\n    super(cause);\n  }\n\n  // Prevent stack traces from being taken\n  @Override\n  public final synchronized Throwable fillInStackTrace() {\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/Result.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\nimport java.util.EnumMap;\nimport java.util.Map;\n\n/**\n * <p>Encapsulates the result of decoding a barcode within an image.</p>\n *\n * @author Sean Owen\n */\npublic final class Result {\n\n  private final String text;\n  private final byte[] rawBytes;\n  private final int numBits;\n  private ResultPoint[] resultPoints;\n  private final BarcodeFormat format;\n  private Map<ResultMetadataType,Object> resultMetadata;\n  private final long timestamp;\n\n  public Result(String text,\n                byte[] rawBytes,\n                ResultPoint[] resultPoints,\n                BarcodeFormat format) {\n    this(text, rawBytes, resultPoints, format, System.currentTimeMillis());\n  }\n\n  public Result(String text,\n                byte[] rawBytes,\n                ResultPoint[] resultPoints,\n                BarcodeFormat format,\n                long timestamp) {\n    this(text, rawBytes, rawBytes == null ? 0 : 8 * rawBytes.length,\n         resultPoints, format, timestamp);\n  }\n\n  public Result(String text,\n                byte[] rawBytes,\n                int numBits,\n                ResultPoint[] resultPoints,\n                BarcodeFormat format,\n                long timestamp) {\n    this.text = text;\n    this.rawBytes = rawBytes;\n    this.numBits = numBits;\n    this.resultPoints = resultPoints;\n    this.format = format;\n    this.resultMetadata = null;\n    this.timestamp = timestamp;\n  }\n\n  /**\n   * @return raw text encoded by the barcode\n   */\n  public String getText() {\n    return text;\n  }\n\n  /**\n   * @return raw bytes encoded by the barcode, if applicable, otherwise {@code null}\n   */\n  public byte[] getRawBytes() {\n    return rawBytes;\n  }\n\n  /**\n   * @return how many bits of {@link #getRawBytes()} are valid; typically 8 times its length\n   * @since 3.3.0\n   */\n  public int getNumBits() {\n    return numBits;\n  }\n\n  /**\n   * @return points related to the barcode in the image. These are typically points\n   *         identifying finder patterns or the corners of the barcode. The exact meaning is\n   *         specific to the type of barcode that was decoded.\n   */\n  public ResultPoint[] getResultPoints() {\n    return resultPoints;\n  }\n\n  /**\n   * @return {@link BarcodeFormat} representing the format of the barcode that was decoded\n   */\n  public BarcodeFormat getBarcodeFormat() {\n    return format;\n  }\n\n  /**\n   * @return {@link Map} mapping {@link ResultMetadataType} keys to values. May be\n   *   {@code null}. This contains optional metadata about what was detected about the barcode,\n   *   like orientation.\n   */\n  public Map<ResultMetadataType,Object> getResultMetadata() {\n    return resultMetadata;\n  }\n\n  public void putMetadata(ResultMetadataType type, Object value) {\n    if (resultMetadata == null) {\n      resultMetadata = new EnumMap<>(ResultMetadataType.class);\n    }\n    resultMetadata.put(type, value);\n  }\n\n  public void putAllMetadata(Map<ResultMetadataType,Object> metadata) {\n    if (metadata != null) {\n      if (resultMetadata == null) {\n        resultMetadata = metadata;\n      } else {\n        resultMetadata.putAll(metadata);\n      }\n    }\n  }\n\n  public void addResultPoints(ResultPoint[] newPoints) {\n    ResultPoint[] oldPoints = resultPoints;\n    if (oldPoints == null) {\n      resultPoints = newPoints;\n    } else if (newPoints != null && newPoints.length > 0) {\n      ResultPoint[] allPoints = new ResultPoint[oldPoints.length + newPoints.length];\n      System.arraycopy(oldPoints, 0, allPoints, 0, oldPoints.length);\n      System.arraycopy(newPoints, 0, allPoints, oldPoints.length, newPoints.length);\n      resultPoints = allPoints;\n    }\n  }\n\n  public long getTimestamp() {\n    return timestamp;\n  }\n\n  @Override\n  public String toString() {\n    return text;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/ResultMetadataType.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * Represents some type of metadata about the result of the decoding that the decoder\n * wishes to communicate back to the caller.\n *\n * @author Sean Owen\n */\npublic enum ResultMetadataType {\n\n  /**\n   * Unspecified, application-specific metadata. Maps to an unspecified {@link Object}.\n   */\n  OTHER,\n\n  /**\n   * Denotes the likely approximate orientation of the barcode in the image. This value\n   * is given as degrees rotated clockwise from the normal, upright orientation.\n   * For example a 1D barcode which was found by reading top-to-bottom would be\n   * said to have orientation \"90\". This key maps to an {@link Integer} whose\n   * value is in the range [0,360).\n   */\n  ORIENTATION,\n\n  /**\n   * <p>2D barcode formats typically encode text, but allow for a sort of 'byte mode'\n   * which is sometimes used to encode binary data. While {@link Result} makes available\n   * the complete raw bytes in the barcode for these formats, it does not offer the bytes\n   * from the byte segments alone.</p>\n   *\n   * <p>This maps to a {@link java.util.List} of byte arrays corresponding to the\n   * raw bytes in the byte segments in the barcode, in order.</p>\n   */\n  BYTE_SEGMENTS,\n\n  /**\n   * Error correction level used, if applicable. The value type depends on the\n   * format, but is typically a String.\n   */\n  ERROR_CORRECTION_LEVEL,\n\n  /**\n   * For some periodicals, indicates the issue number as an {@link Integer}.\n   */\n  ISSUE_NUMBER,\n\n  /**\n   * For some products, indicates the suggested retail price in the barcode as a\n   * formatted {@link String}.\n   */\n  SUGGESTED_PRICE,\n\n  /**\n   * For some products, the possible country of manufacture as a {@link String} denoting the\n   * ISO country code. Some map to multiple possible countries, like \"US/CA\".\n   */\n  POSSIBLE_COUNTRY,\n\n  /**\n   * For some products, the extension text\n   */\n  UPC_EAN_EXTENSION,\n\n  /**\n   * PDF417-specific metadata\n   */\n  PDF417_EXTRA_METADATA,\n\n  /**\n   * If the code format supports structured append and the current scanned code is part of one then the\n   * sequence number is given with it.\n   */\n  STRUCTURED_APPEND_SEQUENCE,\n\n  /**\n   * If the code format supports structured append and the current scanned code is part of one then the\n   * parity is given with it.\n   */\n  STRUCTURED_APPEND_PARITY,\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/ResultPoint.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\nimport peergos.shared.zxing.common.detector.MathUtils;\n\n/**\n * <p>Encapsulates a point of interest in an image containing a barcode. Typically, this\n * would be the location of a finder pattern or the corner of the barcode, for example.</p>\n *\n * @author Sean Owen\n */\npublic class ResultPoint {\n\n  private final float x;\n  private final float y;\n\n  public ResultPoint(float x, float y) {\n    this.x = x;\n    this.y = y;\n  }\n\n  public final float getX() {\n    return x;\n  }\n\n  public final float getY() {\n    return y;\n  }\n\n  @Override\n  public final boolean equals(Object other) {\n    if (other instanceof ResultPoint) {\n      ResultPoint otherPoint = (ResultPoint) other;\n      return x == otherPoint.x && y == otherPoint.y;\n    }\n    return false;\n  }\n\n  @Override\n  public final int hashCode() {\n    return 31 * Float.floatToIntBits(x) + Float.floatToIntBits(y);\n  }\n\n  @Override\n  public final String toString() {\n    return \"(\" + x + ',' + y + ')';\n  }\n\n  /**\n   * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC\n   * and BC is less than AC, and the angle between BC and BA is less than 180 degrees.\n   *\n   * @param patterns array of three {@code ResultPoint} to order\n   */\n  public static void orderBestPatterns(ResultPoint[] patterns) {\n\n    // Find distances between pattern centers\n    float zeroOneDistance = distance(patterns[0], patterns[1]);\n    float oneTwoDistance = distance(patterns[1], patterns[2]);\n    float zeroTwoDistance = distance(patterns[0], patterns[2]);\n\n    ResultPoint pointA;\n    ResultPoint pointB;\n    ResultPoint pointC;\n    // Assume one closest to other two is B; A and C will just be guesses at first\n    if (oneTwoDistance >= zeroOneDistance && oneTwoDistance >= zeroTwoDistance) {\n      pointB = patterns[0];\n      pointA = patterns[1];\n      pointC = patterns[2];\n    } else if (zeroTwoDistance >= oneTwoDistance && zeroTwoDistance >= zeroOneDistance) {\n      pointB = patterns[1];\n      pointA = patterns[0];\n      pointC = patterns[2];\n    } else {\n      pointB = patterns[2];\n      pointA = patterns[0];\n      pointC = patterns[1];\n    }\n\n    // Use cross product to figure out whether A and C are correct or flipped.\n    // This asks whether BC x BA has a positive z component, which is the arrangement\n    // we want for A, B, C. If it's negative, then we've got it flipped around and\n    // should swap A and C.\n    if (crossProductZ(pointA, pointB, pointC) < 0.0f) {\n      ResultPoint temp = pointA;\n      pointA = pointC;\n      pointC = temp;\n    }\n\n    patterns[0] = pointA;\n    patterns[1] = pointB;\n    patterns[2] = pointC;\n  }\n\n  /**\n   * @param pattern1 first pattern\n   * @param pattern2 second pattern\n   * @return distance between two points\n   */\n  public static float distance(ResultPoint pattern1, ResultPoint pattern2) {\n    return MathUtils.distance(pattern1.x, pattern1.y, pattern2.x, pattern2.y);\n  }\n\n  /**\n   * Returns the z component of the cross product between vectors BC and BA.\n   */\n  private static float crossProductZ(ResultPoint pointA,\n                                     ResultPoint pointB,\n                                     ResultPoint pointC) {\n    float bX = pointB.x;\n    float bY = pointB.y;\n    return ((pointC.x - bX) * (pointA.y - bY)) - ((pointC.y - bY) * (pointA.x - bX));\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/ResultPointCallback.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * Callback which is invoked when a possible result point (significant\n * point in the barcode image such as a corner) is found.\n *\n * @see DecodeHintType#NEED_RESULT_POINT_CALLBACK\n */\npublic interface ResultPointCallback {\n\n  void foundPossibleResultPoint(ResultPoint point);\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/Writer.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\nimport peergos.shared.zxing.common.BitMatrix;\n\nimport java.util.Map;\n\n/**\n * The base class for all objects which encode/generate a barcode image.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic interface Writer {\n\n  /**\n   * Encode a barcode using the default settings.\n   *\n   * @param contents The contents to encode in the barcode\n   * @param format The barcode format to generate\n   * @param width The preferred width in pixels\n   * @param height The preferred height in pixels\n   * @return {@link BitMatrix} representing encoded barcode image\n   * @throws WriterException if contents cannot be encoded legally in a format\n   */\n  BitMatrix encode(String contents, BarcodeFormat format, int width, int height)\n      throws WriterException;\n\n  /**\n   * @param contents The contents to encode in the barcode\n   * @param format The barcode format to generate\n   * @param width The preferred width in pixels\n   * @param height The preferred height in pixels\n   * @param hints Additional parameters to supply to the encoder\n   * @return {@link BitMatrix} representing encoded barcode image\n   * @throws WriterException if contents cannot be encoded legally in a format\n   */\n  BitMatrix encode(String contents,\n                   BarcodeFormat format,\n                   int width,\n                   int height,\n                   Map<EncodeHintType,?> hints)\n      throws WriterException;\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/WriterException.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing;\n\n/**\n * A base class which covers the range of exceptions which may occur when encoding a barcode using\n * the Writer framework.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic final class WriterException extends Exception {\n\n  public WriterException() {\n  }\n\n  public WriterException(String message) {\n    super(message);\n  }\n \n  public WriterException(Throwable cause) {\n    super(cause);\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/BitArray.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport java.util.Arrays;\n\n/**\n * <p>A simple, fast array of bits, represented compactly by an array of ints internally.</p>\n *\n * @author Sean Owen\n */\npublic final class BitArray implements Cloneable {\n\n  private int[] bits;\n  private int size;\n\n  public BitArray() {\n    this.size = 0;\n    this.bits = new int[1];\n  }\n\n  public BitArray(int size) {\n    this.size = size;\n    this.bits = makeArray(size);\n  }\n\n  // For testing only\n  BitArray(int[] bits, int size) {\n    this.bits = bits;\n    this.size = size;\n  }\n\n  public int getSize() {\n    return size;\n  }\n\n  public int getSizeInBytes() {\n    return (size + 7) / 8;\n  }\n\n  private void ensureCapacity(int size) {\n    if (size > bits.length * 32) {\n      int[] newBits = makeArray(size);\n      System.arraycopy(bits, 0, newBits, 0, bits.length);\n      this.bits = newBits;\n    }\n  }\n\n  /**\n   * @param i bit to get\n   * @return true iff bit i is set\n   */\n  public boolean get(int i) {\n    return (bits[i / 32] & (1 << (i & 0x1F))) != 0;\n  }\n\n  /**\n   * Sets bit i.\n   *\n   * @param i bit to set\n   */\n  public void set(int i) {\n    bits[i / 32] |= 1 << (i & 0x1F);\n  }\n\n  /**\n   * Flips bit i.\n   *\n   * @param i bit to set\n   */\n  public void flip(int i) {\n    bits[i / 32] ^= 1 << (i & 0x1F);\n  }\n\n  /**\n   * @param from first bit to check\n   * @return index of first bit that is set, starting from the given index, or size if none are set\n   *  at or beyond this given index\n   * @see #getNextUnset(int)\n   */\n  public int getNextSet(int from) {\n    if (from >= size) {\n      return size;\n    }\n    int bitsOffset = from / 32;\n    int currentBits = bits[bitsOffset];\n    // mask off lesser bits first\n    currentBits &= -(1 << (from & 0x1F));\n    while (currentBits == 0) {\n      if (++bitsOffset == bits.length) {\n        return size;\n      }\n      currentBits = bits[bitsOffset];\n    }\n    int result = (bitsOffset * 32) + Integer.numberOfTrailingZeros(currentBits);\n    return result > size ? size : result;\n  }\n\n  /**\n   * @param from index to start looking for unset bit\n   * @return index of next unset bit, or {@code size} if none are unset until the end\n   * @see #getNextSet(int)\n   */\n  public int getNextUnset(int from) {\n    if (from >= size) {\n      return size;\n    }\n    int bitsOffset = from / 32;\n    int currentBits = ~bits[bitsOffset];\n    // mask off lesser bits first\n    currentBits &= -(1 << (from & 0x1F));\n    while (currentBits == 0) {\n      if (++bitsOffset == bits.length) {\n        return size;\n      }\n      currentBits = ~bits[bitsOffset];\n    }\n    int result = (bitsOffset * 32) + Integer.numberOfTrailingZeros(currentBits);\n    return result > size ? size : result;\n  }\n\n  /**\n   * Sets a block of 32 bits, starting at bit i.\n   *\n   * @param i first bit to set\n   * @param newBits the new value of the next 32 bits. Note again that the least-significant bit\n   * corresponds to bit i, the next-least-significant to i+1, and so on.\n   */\n  public void setBulk(int i, int newBits) {\n    bits[i / 32] = newBits;\n  }\n\n  /**\n   * Sets a range of bits.\n   *\n   * @param start start of range, inclusive.\n   * @param end end of range, exclusive\n   */\n  public void setRange(int start, int end) {\n    if (end < start || start < 0 || end > size) {\n      throw new IllegalArgumentException();\n    }\n    if (end == start) {\n      return;\n    }\n    end--; // will be easier to treat this as the last actually set bit -- inclusive\n    int firstInt = start / 32;\n    int lastInt = end / 32;\n    for (int i = firstInt; i <= lastInt; i++) {\n      int firstBit = i > firstInt ? 0 : start & 0x1F;\n      int lastBit = i < lastInt ? 31 : end & 0x1F;\n      // Ones from firstBit to lastBit, inclusive\n      int mask = (2 << lastBit) - (1 << firstBit);\n      bits[i] |= mask;\n    }\n  }\n\n  /**\n   * Clears all bits (sets to false).\n   */\n  public void clear() {\n    int max = bits.length;\n    for (int i = 0; i < max; i++) {\n      bits[i] = 0;\n    }\n  }\n\n  /**\n   * Efficient method to check if a range of bits is set, or not set.\n   *\n   * @param start start of range, inclusive.\n   * @param end end of range, exclusive\n   * @param value if true, checks that bits in range are set, otherwise checks that they are not set\n   * @return true iff all bits are set or not set in range, according to value argument\n   * @throws IllegalArgumentException if end is less than start or the range is not contained in the array\n   */\n  public boolean isRange(int start, int end, boolean value) {\n    if (end < start || start < 0 || end > size) {\n      throw new IllegalArgumentException();\n    }\n    if (end == start) {\n      return true; // empty range matches\n    }\n    end--; // will be easier to treat this as the last actually set bit -- inclusive\n    int firstInt = start / 32;\n    int lastInt = end / 32;\n    for (int i = firstInt; i <= lastInt; i++) {\n      int firstBit = i > firstInt ? 0 : start & 0x1F;\n      int lastBit = i < lastInt ? 31 : end & 0x1F;\n      // Ones from firstBit to lastBit, inclusive\n      int mask = (2 << lastBit) - (1 << firstBit);\n\n      // Return false if we're looking for 1s and the masked bits[i] isn't all 1s (that is,\n      // equals the mask, or we're looking for 0s and the masked portion is not all 0s\n      if ((bits[i] & mask) != (value ? mask : 0)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  public void appendBit(boolean bit) {\n    ensureCapacity(size + 1);\n    if (bit) {\n      bits[size / 32] |= 1 << (size & 0x1F);\n    }\n    size++;\n  }\n\n  /**\n   * Appends the least-significant bits, from value, in order from most-significant to\n   * least-significant. For example, appending 6 bits from 0x000001E will append the bits\n   * 0, 1, 1, 1, 1, 0 in that order.\n   *\n   * @param value {@code int} containing bits to append\n   * @param numBits bits from value to append\n   */\n  public void appendBits(int value, int numBits) {\n    if (numBits < 0 || numBits > 32) {\n      throw new IllegalArgumentException(\"Num bits must be between 0 and 32\");\n    }\n    ensureCapacity(size + numBits);\n    for (int numBitsLeft = numBits; numBitsLeft > 0; numBitsLeft--) {\n      appendBit(((value >> (numBitsLeft - 1)) & 0x01) == 1);\n    }\n  }\n\n  public void appendBitArray(BitArray other) {\n    int otherSize = other.size;\n    ensureCapacity(size + otherSize);\n    for (int i = 0; i < otherSize; i++) {\n      appendBit(other.get(i));\n    }\n  }\n\n  public void xor(BitArray other) {\n    if (size != other.size) {\n      throw new IllegalArgumentException(\"Sizes don't match\");\n    }\n    for (int i = 0; i < bits.length; i++) {\n      // The last int could be incomplete (i.e. not have 32 bits in\n      // it) but there is no problem since 0 XOR 0 == 0.\n      bits[i] ^= other.bits[i];\n    }\n  }\n\n  /**\n   *\n   * @param bitOffset first bit to start writing\n   * @param array array to write into. Bytes are written most-significant byte first. This is the opposite\n   *  of the internal representation, which is exposed by {@link #getBitArray()}\n   * @param offset position in array to start writing\n   * @param numBytes how many bytes to write\n   */\n  public void toBytes(int bitOffset, byte[] array, int offset, int numBytes) {\n    for (int i = 0; i < numBytes; i++) {\n      int theByte = 0;\n      for (int j = 0; j < 8; j++) {\n        if (get(bitOffset)) {\n          theByte |= 1 << (7 - j);\n        }\n        bitOffset++;\n      }\n      array[offset + i] = (byte) theByte;\n    }\n  }\n\n  /**\n   * @return underlying array of ints. The first element holds the first 32 bits, and the least\n   *         significant bit is bit 0.\n   */\n  public int[] getBitArray() {\n    return bits;\n  }\n\n  /**\n   * Reverses all bits in the array.\n   */\n  public void reverse() {\n    int[] newBits = new int[bits.length];\n    // reverse all int's first\n    int len = (size - 1) / 32;\n    int oldBitsLen = len + 1;\n    for (int i = 0; i < oldBitsLen; i++) {\n      long x = bits[i];\n      x = ((x >>  1) & 0x55555555L) | ((x & 0x55555555L) <<  1);\n      x = ((x >>  2) & 0x33333333L) | ((x & 0x33333333L) <<  2);\n      x = ((x >>  4) & 0x0f0f0f0fL) | ((x & 0x0f0f0f0fL) <<  4);\n      x = ((x >>  8) & 0x00ff00ffL) | ((x & 0x00ff00ffL) <<  8);\n      x = ((x >> 16) & 0x0000ffffL) | ((x & 0x0000ffffL) << 16);\n      newBits[len - i] = (int) x;\n    }\n    // now correct the int's if the bit size isn't a multiple of 32\n    if (size != oldBitsLen * 32) {\n      int leftOffset = oldBitsLen * 32 - size;\n      int currentInt = newBits[0] >>> leftOffset;\n      for (int i = 1; i < oldBitsLen; i++) {\n        int nextInt = newBits[i];\n        currentInt |= nextInt << (32 - leftOffset);\n        newBits[i - 1] = currentInt;\n        currentInt = nextInt >>> leftOffset;\n      }\n      newBits[oldBitsLen - 1] = currentInt;\n    }\n    bits = newBits;\n  }\n\n  private static int[] makeArray(int size) {\n    return new int[(size + 31) / 32];\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof BitArray)) {\n      return false;\n    }\n    BitArray other = (BitArray) o;\n    return size == other.size && Arrays.equals(bits, other.bits);\n  }\n\n  @Override\n  public int hashCode() {\n    return 31 * size + Arrays.hashCode(bits);\n  }\n\n  @Override\n  public String toString() {\n    StringBuilder result = new StringBuilder(size + (size / 8) + 1);\n    for (int i = 0; i < size; i++) {\n      if ((i & 0x07) == 0) {\n        result.append(' ');\n      }\n      result.append(get(i) ? 'X' : '.');\n    }\n    return result.toString();\n  }\n\n  public BitArray clone() {\n    return new BitArray(Arrays.copyOfRange(bits, 0, bits.length), size);\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/BitMatrix.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport java.util.Arrays;\n\n/**\n * <p>Represents a 2D matrix of bits. In function arguments below, and throughout the common\n * module, x is the column position, and y is the row position. The ordering is always x, y.\n * The origin is at the top-left.</p>\n *\n * <p>Internally the bits are represented in a 1-D array of 32-bit ints. However, each row begins\n * with a new int. This is done intentionally so that we can copy out a row into a BitArray very\n * efficiently.</p>\n *\n * <p>The ordering of bits is row-major. Within each int, the least significant bits are used first,\n * meaning they represent lower x values. This is compatible with BitArray's implementation.</p>\n *\n * @author Sean Owen\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic final class BitMatrix implements Cloneable {\n\n  private final int width;\n  private final int height;\n  private final int rowSize;\n  private final int[] bits;\n\n  /**\n   * Creates an empty square {@code BitMatrix}.\n   *\n   * @param dimension height and width\n   */\n  public BitMatrix(int dimension) {\n    this(dimension, dimension);\n  }\n\n  /**\n   * Creates an empty {@code BitMatrix}.\n   *\n   * @param width bit matrix width\n   * @param height bit matrix height\n   */\n  public BitMatrix(int width, int height) {\n    if (width < 1 || height < 1) {\n      throw new IllegalArgumentException(\"Both dimensions must be greater than 0\");\n    }\n    this.width = width;\n    this.height = height;\n    this.rowSize = (width + 31) / 32;\n    bits = new int[rowSize * height];\n  }\n\n  private BitMatrix(int width, int height, int rowSize, int[] bits) {\n    this.width = width;\n    this.height = height;\n    this.rowSize = rowSize;\n    this.bits = bits;\n  }\n\n  /**\n   * Interprets a 2D array of booleans as a {@code BitMatrix}, where \"true\" means an \"on\" bit.\n   *\n   * @param image bits of the image, as a row-major 2D array. Elements are arrays representing rows\n   * @return {@code BitMatrix} representation of image\n   */\n  public static BitMatrix parse(boolean[][] image) {\n    int height = image.length;\n    int width = image[0].length;\n    BitMatrix bits = new BitMatrix(width, height);\n    for (int i = 0; i < height; i++) {\n      boolean[] imageI = image[i];\n      for (int j = 0; j < width; j++) {\n        if (imageI[j]) {\n          bits.set(j, i);\n        }\n      }\n    }\n    return bits;\n  }\n\n  public static BitMatrix parse(String stringRepresentation, String setString, String unsetString) {\n    if (stringRepresentation == null) {\n      throw new IllegalArgumentException();\n    }\n\n    boolean[] bits = new boolean[stringRepresentation.length()];\n    int bitsPos = 0;\n    int rowStartPos = 0;\n    int rowLength = -1;\n    int nRows = 0;\n    int pos = 0;\n    while (pos < stringRepresentation.length()) {\n      if (stringRepresentation.charAt(pos) == '\\n' ||\n          stringRepresentation.charAt(pos) == '\\r') {\n        if (bitsPos > rowStartPos) {\n          if (rowLength == -1) {\n            rowLength = bitsPos - rowStartPos;\n          } else if (bitsPos - rowStartPos != rowLength) {\n            throw new IllegalArgumentException(\"row lengths do not match\");\n          }\n          rowStartPos = bitsPos;\n          nRows++;\n        }\n        pos++;\n      }  else if (stringRepresentation.substring(pos, pos + setString.length()).equals(setString)) {\n        pos += setString.length();\n        bits[bitsPos] = true;\n        bitsPos++;\n      } else if (stringRepresentation.substring(pos, pos + unsetString.length()).equals(unsetString)) {\n        pos += unsetString.length();\n        bits[bitsPos] = false;\n        bitsPos++;\n      } else {\n        throw new IllegalArgumentException(\n            \"illegal character encountered: \" + stringRepresentation.substring(pos));\n      }\n    }\n\n    // no EOL at end?\n    if (bitsPos > rowStartPos) {\n      if (rowLength == -1) {\n        rowLength = bitsPos - rowStartPos;\n      } else if (bitsPos - rowStartPos != rowLength) {\n        throw new IllegalArgumentException(\"row lengths do not match\");\n      }\n      nRows++;\n    }\n\n    BitMatrix matrix = new BitMatrix(rowLength, nRows);\n    for (int i = 0; i < bitsPos; i++) {\n      if (bits[i]) {\n        matrix.set(i % rowLength, i / rowLength);\n      }\n    }\n    return matrix;\n  }\n\n  /**\n   * <p>Gets the requested bit, where true means black.</p>\n   *\n   * @param x The horizontal component (i.e. which column)\n   * @param y The vertical component (i.e. which row)\n   * @return value of given bit in matrix\n   */\n  public boolean get(int x, int y) {\n    int offset = y * rowSize + (x / 32);\n    return ((bits[offset] >>> (x & 0x1f)) & 1) != 0;\n  }\n\n  /**\n   * <p>Sets the given bit to true.</p>\n   *\n   * @param x The horizontal component (i.e. which column)\n   * @param y The vertical component (i.e. which row)\n   */\n  public void set(int x, int y) {\n    int offset = y * rowSize + (x / 32);\n    bits[offset] |= 1 << (x & 0x1f);\n  }\n\n  public void unset(int x, int y) {\n    int offset = y * rowSize + (x / 32);\n    bits[offset] &= ~(1 << (x & 0x1f));\n  }\n\n  /**\n   * <p>Flips the given bit.</p>\n   *\n   * @param x The horizontal component (i.e. which column)\n   * @param y The vertical component (i.e. which row)\n   */\n  public void flip(int x, int y) {\n    int offset = y * rowSize + (x / 32);\n    bits[offset] ^= 1 << (x & 0x1f);\n  }\n\n  /**\n   * Exclusive-or (XOR): Flip the bit in this {@code BitMatrix} if the corresponding\n   * mask bit is set.\n   *\n   * @param mask XOR mask\n   */\n  public void xor(BitMatrix mask) {\n    if (width != mask.getWidth() || height != mask.getHeight()\n        || rowSize != mask.getRowSize()) {\n      throw new IllegalArgumentException(\"input matrix dimensions do not match\");\n    }\n    BitArray rowArray = new BitArray(width);\n    for (int y = 0; y < height; y++) {\n      int offset = y * rowSize;\n      int[] row = mask.getRow(y, rowArray).getBitArray();\n      for (int x = 0; x < rowSize; x++) {\n        bits[offset + x] ^= row[x];\n      }\n    }\n  }\n\n  /**\n   * Clears all bits (sets to false).\n   */\n  public void clear() {\n    int max = bits.length;\n    for (int i = 0; i < max; i++) {\n      bits[i] = 0;\n    }\n  }\n\n  /**\n   * <p>Sets a square region of the bit matrix to true.</p>\n   *\n   * @param left The horizontal position to begin at (inclusive)\n   * @param top The vertical position to begin at (inclusive)\n   * @param width The width of the region\n   * @param height The height of the region\n   */\n  public void setRegion(int left, int top, int width, int height) {\n    if (top < 0 || left < 0) {\n      throw new IllegalArgumentException(\"Left and top must be nonnegative\");\n    }\n    if (height < 1 || width < 1) {\n      throw new IllegalArgumentException(\"Height and width must be at least 1\");\n    }\n    int right = left + width;\n    int bottom = top + height;\n    if (bottom > this.height || right > this.width) {\n      throw new IllegalArgumentException(\"The region must fit inside the matrix\");\n    }\n    for (int y = top; y < bottom; y++) {\n      int offset = y * rowSize;\n      for (int x = left; x < right; x++) {\n        bits[offset + (x / 32)] |= 1 << (x & 0x1f);\n      }\n    }\n  }\n\n  /**\n   * A fast method to retrieve one row of data from the matrix as a BitArray.\n   *\n   * @param y The row to retrieve\n   * @param row An optional caller-allocated BitArray, will be allocated if null or too small\n   * @return The resulting BitArray - this reference should always be used even when passing\n   *         your own row\n   */\n  public BitArray getRow(int y, BitArray row) {\n    if (row == null || row.getSize() < width) {\n      row = new BitArray(width);\n    } else {\n      row.clear();\n    }\n    int offset = y * rowSize;\n    for (int x = 0; x < rowSize; x++) {\n      row.setBulk(x * 32, bits[offset + x]);\n    }\n    return row;\n  }\n\n  /**\n   * @param y row to set\n   * @param row {@link BitArray} to copy from\n   */\n  public void setRow(int y, BitArray row) {\n    System.arraycopy(row.getBitArray(), 0, bits, y * rowSize, rowSize);\n  }\n\n  /**\n   * Modifies this {@code BitMatrix} to represent the same but rotated 180 degrees\n   */\n  public void rotate180() {\n    int width = getWidth();\n    int height = getHeight();\n    BitArray topRow = new BitArray(width);\n    BitArray bottomRow = new BitArray(width);\n    for (int i = 0; i < (height + 1) / 2; i++) {\n      topRow = getRow(i, topRow);\n      bottomRow = getRow(height - 1 - i, bottomRow);\n      topRow.reverse();\n      bottomRow.reverse();\n      setRow(i, bottomRow);\n      setRow(height - 1 - i, topRow);\n    }\n  }\n\n  /**\n   * This is useful in detecting the enclosing rectangle of a 'pure' barcode.\n   *\n   * @return {@code left,top,width,height} enclosing rectangle of all 1 bits, or null if it is all white\n   */\n  public int[] getEnclosingRectangle() {\n    int left = width;\n    int top = height;\n    int right = -1;\n    int bottom = -1;\n\n    for (int y = 0; y < height; y++) {\n      for (int x32 = 0; x32 < rowSize; x32++) {\n        int theBits = bits[y * rowSize + x32];\n        if (theBits != 0) {\n          if (y < top) {\n            top = y;\n          }\n          if (y > bottom) {\n            bottom = y;\n          }\n          if (x32 * 32 < left) {\n            int bit = 0;\n            while ((theBits << (31 - bit)) == 0) {\n              bit++;\n            }\n            if ((x32 * 32 + bit) < left) {\n              left = x32 * 32 + bit;\n            }\n          }\n          if (x32 * 32 + 31 > right) {\n            int bit = 31;\n            while ((theBits >>> bit) == 0) {\n              bit--;\n            }\n            if ((x32 * 32 + bit) > right) {\n              right = x32 * 32 + bit;\n            }\n          }\n        }\n      }\n    }\n\n    if (right < left || bottom < top) {\n      return null;\n    }\n\n    return new int[] {left, top, right - left + 1, bottom - top + 1};\n  }\n\n  /**\n   * This is useful in detecting a corner of a 'pure' barcode.\n   *\n   * @return {@code x,y} coordinate of top-left-most 1 bit, or null if it is all white\n   */\n  public int[] getTopLeftOnBit() {\n    int bitsOffset = 0;\n    while (bitsOffset < bits.length && bits[bitsOffset] == 0) {\n      bitsOffset++;\n    }\n    if (bitsOffset == bits.length) {\n      return null;\n    }\n    int y = bitsOffset / rowSize;\n    int x = (bitsOffset % rowSize) * 32;\n\n    int theBits = bits[bitsOffset];\n    int bit = 0;\n    while ((theBits << (31 - bit)) == 0) {\n      bit++;\n    }\n    x += bit;\n    return new int[] {x, y};\n  }\n\n  public int[] getBottomRightOnBit() {\n    int bitsOffset = bits.length - 1;\n    while (bitsOffset >= 0 && bits[bitsOffset] == 0) {\n      bitsOffset--;\n    }\n    if (bitsOffset < 0) {\n      return null;\n    }\n\n    int y = bitsOffset / rowSize;\n    int x = (bitsOffset % rowSize) * 32;\n\n    int theBits = bits[bitsOffset];\n    int bit = 31;\n    while ((theBits >>> bit) == 0) {\n      bit--;\n    }\n    x += bit;\n\n    return new int[] {x, y};\n  }\n\n  /**\n   * @return The width of the matrix\n   */\n  public int getWidth() {\n    return width;\n  }\n\n  /**\n   * @return The height of the matrix\n   */\n  public int getHeight() {\n    return height;\n  }\n\n  /**\n   * @return The row size of the matrix\n   */\n  public int getRowSize() {\n    return rowSize;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof BitMatrix)) {\n      return false;\n    }\n    BitMatrix other = (BitMatrix) o;\n    return width == other.width && height == other.height && rowSize == other.rowSize &&\n    Arrays.equals(bits, other.bits);\n  }\n\n  @Override\n  public int hashCode() {\n    int hash = width;\n    hash = 31 * hash + width;\n    hash = 31 * hash + height;\n    hash = 31 * hash + rowSize;\n     hash = 31 * hash + Arrays.hashCode(bits);\n    return hash;\n  }\n\n  /**\n   * @return string representation using \"X\" for set and \" \" for unset bits\n   */\n  @Override\n  public String toString() {\n    return toString(\"X \", \"  \");\n  }\n\n  /**\n   * @param setString representation of a set bit\n   * @param unsetString representation of an unset bit\n   * @return string representation of entire matrix utilizing given strings\n   */\n  public String toString(String setString, String unsetString) {\n    return buildToString(setString, unsetString, \"\\n\");\n  }\n\n  /**\n   * @param setString representation of a set bit\n   * @param unsetString representation of an unset bit\n   * @param lineSeparator newline character in string representation\n   * @return string representation of entire matrix utilizing given strings and line separator\n   * @deprecated call {@link #toString(String,String)} only, which uses \\n line separator always\n   */\n  @Deprecated\n  public String toString(String setString, String unsetString, String lineSeparator) {\n    return buildToString(setString, unsetString, lineSeparator);\n  }\n\n  private String buildToString(String setString, String unsetString, String lineSeparator) {\n    StringBuilder result = new StringBuilder(height * (width + 1));\n    for (int y = 0; y < height; y++) {\n      for (int x = 0; x < width; x++) {\n        result.append(get(x, y) ? setString : unsetString);\n      }\n      result.append(lineSeparator);\n    }\n    return result.toString();\n  }\n\n  public BitMatrix clone() {\n    return new BitMatrix(width, height, rowSize, Arrays.copyOfRange(bits, 0, bits.length));\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/BitSource.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\n/**\n * <p>This provides an easy abstraction to read bits at a time from a sequence of bytes, where the\n * number of bits read is not often a multiple of 8.</p>\n *\n * <p>This class is thread-safe but not reentrant -- unless the caller modifies the bytes array\n * it passed in, in which case all bets are off.</p>\n *\n * @author Sean Owen\n */\npublic final class BitSource {\n\n  private final byte[] bytes;\n  private int byteOffset;\n  private int bitOffset;\n\n  /**\n   * @param bytes bytes from which this will read bits. Bits will be read from the first byte first.\n   * Bits are read within a byte from most-significant to least-significant bit.\n   */\n  public BitSource(byte[] bytes) {\n    this.bytes = bytes;\n  }\n\n  /**\n   * @return index of next bit in current byte which would be read by the next call to {@link #readBits(int)}.\n   */\n  public int getBitOffset() {\n    return bitOffset;\n  }\n\n  /**\n   * @return index of next byte in input byte array which would be read by the next call to {@link #readBits(int)}.\n   */\n  public int getByteOffset() {\n    return byteOffset;\n  }\n\n  /**\n   * @param numBits number of bits to read\n   * @return int representing the bits read. The bits will appear as the least-significant\n   *         bits of the int\n   * @throws IllegalArgumentException if numBits isn't in [1,32] or more than is available\n   */\n  public int readBits(int numBits) {\n    if (numBits < 1 || numBits > 32 || numBits > available()) {\n      throw new IllegalArgumentException(String.valueOf(numBits));\n    }\n\n    int result = 0;\n\n    // First, read remainder from current byte\n    if (bitOffset > 0) {\n      int bitsLeft = 8 - bitOffset;\n      int toRead = numBits < bitsLeft ? numBits : bitsLeft;\n      int bitsToNotRead = bitsLeft - toRead;\n      int mask = (0xFF >> (8 - toRead)) << bitsToNotRead;\n      result = (bytes[byteOffset] & mask) >> bitsToNotRead;\n      numBits -= toRead;\n      bitOffset += toRead;\n      if (bitOffset == 8) {\n        bitOffset = 0;\n        byteOffset++;\n      }\n    }\n\n    // Next read whole bytes\n    if (numBits > 0) {\n      while (numBits >= 8) {\n        result = (result << 8) | (bytes[byteOffset] & 0xFF);\n        byteOffset++;\n        numBits -= 8;\n      }\n\n      // Finally read a partial byte\n      if (numBits > 0) {\n        int bitsToNotRead = 8 - numBits;\n        int mask = (0xFF >> bitsToNotRead) << bitsToNotRead;\n        result = (result << numBits) | ((bytes[byteOffset] & mask) >> bitsToNotRead);\n        bitOffset += numBits;\n      }\n    }\n\n    return result;\n  }\n\n  /**\n   * @return number of bits that can be read successfully\n   */\n  public int available() {\n    return 8 * (bytes.length - byteOffset) - bitOffset;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/CharacterSetECI.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport peergos.shared.zxing.FormatException;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Encapsulates a Character Set ECI, according to \"Extended Channel Interpretations\" 5.3.1.1\n * of ISO 18004.\n *\n * @author Sean Owen\n */\npublic enum CharacterSetECI {\n\n  // Enum name is a Java encoding valid for java.lang and java.io\n  Cp437(new int[]{0,2}),\n  ISO8859_1(new int[]{1,3}, \"ISO-8859-1\"),\n  ISO8859_2(4, \"ISO-8859-2\"),\n  ISO8859_3(5, \"ISO-8859-3\"),\n  ISO8859_4(6, \"ISO-8859-4\"),\n  ISO8859_5(7, \"ISO-8859-5\"),\n  ISO8859_6(8, \"ISO-8859-6\"),\n  ISO8859_7(9, \"ISO-8859-7\"),\n  ISO8859_8(10, \"ISO-8859-8\"),\n  ISO8859_9(11, \"ISO-8859-9\"),\n  ISO8859_10(12, \"ISO-8859-10\"),\n  ISO8859_11(13, \"ISO-8859-11\"),\n  ISO8859_13(15, \"ISO-8859-13\"),\n  ISO8859_14(16, \"ISO-8859-14\"),\n  ISO8859_15(17, \"ISO-8859-15\"),\n  ISO8859_16(18, \"ISO-8859-16\"),\n  SJIS(20, \"Shift_JIS\"),\n  Cp1250(21, \"windows-1250\"),\n  Cp1251(22, \"windows-1251\"),\n  Cp1252(23, \"windows-1252\"),\n  Cp1256(24, \"windows-1256\"),\n  UnicodeBigUnmarked(25, \"UTF-16BE\", \"UnicodeBig\"),\n  UTF8(26, \"UTF-8\"),\n  ASCII(new int[] {27, 170}, \"US-ASCII\"),\n  Big5(28),\n  GB18030(29, \"GB2312\", \"EUC_CN\", \"GBK\"),\n  EUC_KR(30, \"EUC-KR\");\n\n  private static final Map<Integer,CharacterSetECI> VALUE_TO_ECI = new HashMap<>();\n  private static final Map<String,CharacterSetECI> NAME_TO_ECI = new HashMap<>();\n  static {\n    for (CharacterSetECI eci : values()) {\n      for (int value : eci.values) {\n        VALUE_TO_ECI.put(value, eci);\n      }\n      NAME_TO_ECI.put(eci.name(), eci);\n      for (String name : eci.otherEncodingNames) {\n        NAME_TO_ECI.put(name, eci);\n      }\n    }\n  }\n\n  private final int[] values;\n  private final String[] otherEncodingNames;\n\n  CharacterSetECI(int value) {\n    this(new int[] {value});\n  }\n\n  CharacterSetECI(int value, String... otherEncodingNames) {\n    this.values = new int[] {value};\n    this.otherEncodingNames = otherEncodingNames;\n  }\n\n  CharacterSetECI(int[] values, String... otherEncodingNames) {\n    this.values = values;\n    this.otherEncodingNames = otherEncodingNames;\n  }\n\n  public int getValue() {\n    return values[0];\n  }\n\n  /**\n   * @param value character set ECI value\n   * @return {@code CharacterSetECI} representing ECI of given value, or null if it is legal but\n   *   unsupported\n   * @throws FormatException if ECI value is invalid\n   */\n  public static CharacterSetECI getCharacterSetECIByValue(int value) throws FormatException {\n    if (value < 0 || value >= 900) {\n      throw FormatException.getFormatInstance();\n    }\n    return VALUE_TO_ECI.get(value);\n  }\n\n  /**\n   * @param name character set ECI encoding name\n   * @return CharacterSetECI representing ECI for character encoding, or null if it is legal\n   *   but unsupported\n   */\n  public static CharacterSetECI getCharacterSetECIByName(String name) {\n    return NAME_TO_ECI.get(name);\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/DecoderResult.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport java.util.List;\n\n/**\n * <p>Encapsulates the result of decoding a matrix of bits. This typically\n * applies to 2D barcode formats. For now it contains the raw bytes obtained,\n * as well as a String interpretation of those bytes, if applicable.</p>\n *\n * @author Sean Owen\n */\npublic final class DecoderResult {\n\n  private final byte[] rawBytes;\n  private int numBits;\n  private final String text;\n  private final List<byte[]> byteSegments;\n  private final String ecLevel;\n  private Integer errorsCorrected;\n  private Integer erasures;\n  private Object other;\n  private final int structuredAppendParity;\n  private final int structuredAppendSequenceNumber;\n\n  public DecoderResult(byte[] rawBytes,\n                       String text,\n                       List<byte[]> byteSegments,\n                       String ecLevel) {\n    this(rawBytes, text, byteSegments, ecLevel, -1, -1);\n  }\n\n  public DecoderResult(byte[] rawBytes,\n                       String text,\n                       List<byte[]> byteSegments,\n                       String ecLevel,\n                       int saSequence,\n                       int saParity) {\n    this.rawBytes = rawBytes;\n    this.numBits = rawBytes == null ? 0 : 8 * rawBytes.length;\n    this.text = text;\n    this.byteSegments = byteSegments;\n    this.ecLevel = ecLevel;\n    this.structuredAppendParity = saParity;\n    this.structuredAppendSequenceNumber = saSequence;\n  }\n\n  /**\n   * @return raw bytes representing the result, or {@code null} if not applicable\n   */\n  public byte[] getRawBytes() {\n    return rawBytes;\n  }\n\n  /**\n   * @return how many bits of {@link #getRawBytes()} are valid; typically 8 times its length\n   * @since 3.3.0\n   */\n  public int getNumBits() {\n    return numBits;\n  }\n\n  /**\n   * @param numBits overrides the number of bits that are valid in {@link #getRawBytes()}\n   * @since 3.3.0\n   */\n  public void setNumBits(int numBits) {\n    this.numBits = numBits;\n  }\n\n  /**\n   * @return text representation of the result\n   */\n  public String getText() {\n    return text;\n  }\n\n  /**\n   * @return list of byte segments in the result, or {@code null} if not applicable\n   */\n  public List<byte[]> getByteSegments() {\n    return byteSegments;\n  }\n\n  /**\n   * @return name of error correction level used, or {@code null} if not applicable\n   */\n  public String getECLevel() {\n    return ecLevel;\n  }\n\n  /**\n   * @return number of errors corrected, or {@code null} if not applicable\n   */\n  public Integer getErrorsCorrected() {\n    return errorsCorrected;\n  }\n\n  public void setErrorsCorrected(Integer errorsCorrected) {\n    this.errorsCorrected = errorsCorrected;\n  }\n\n  /**\n   * @return number of erasures corrected, or {@code null} if not applicable\n   */\n  public Integer getErasures() {\n    return erasures;\n  }\n\n  public void setErasures(Integer erasures) {\n    this.erasures = erasures;\n  }\n\n  /**\n   * @return arbitrary additional metadata\n   */\n  public Object getOther() {\n    return other;\n  }\n\n  public void setOther(Object other) {\n    this.other = other;\n  }\n\n  public boolean hasStructuredAppend() {\n    return structuredAppendParity >= 0 && structuredAppendSequenceNumber >= 0;\n  }\n\n  public int getStructuredAppendParity() {\n    return structuredAppendParity;\n  }\n\n  public int getStructuredAppendSequenceNumber() {\n    return structuredAppendSequenceNumber;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/DefaultGridSampler.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport peergos.shared.zxing.NotFoundException;\n\n/**\n * @author Sean Owen\n */\npublic final class DefaultGridSampler extends GridSampler {\n\n  @Override\n  public BitMatrix sampleGrid(BitMatrix image,\n                              int dimensionX,\n                              int dimensionY,\n                              float p1ToX, float p1ToY,\n                              float p2ToX, float p2ToY,\n                              float p3ToX, float p3ToY,\n                              float p4ToX, float p4ToY,\n                              float p1FromX, float p1FromY,\n                              float p2FromX, float p2FromY,\n                              float p3FromX, float p3FromY,\n                              float p4FromX, float p4FromY) throws NotFoundException {\n\n    PerspectiveTransform transform = PerspectiveTransform.quadrilateralToQuadrilateral(\n        p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY,\n        p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY);\n\n    return sampleGrid(image, dimensionX, dimensionY, transform);\n  }\n\n  @Override\n  public BitMatrix sampleGrid(BitMatrix image,\n                              int dimensionX,\n                              int dimensionY,\n                              PerspectiveTransform transform) throws NotFoundException {\n    if (dimensionX <= 0 || dimensionY <= 0) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n    BitMatrix bits = new BitMatrix(dimensionX, dimensionY);\n    float[] points = new float[2 * dimensionX];\n    for (int y = 0; y < dimensionY; y++) {\n      int max = points.length;\n      float iValue = y + 0.5f;\n      for (int x = 0; x < max; x += 2) {\n        points[x] = (float) (x / 2) + 0.5f;\n        points[x + 1] = iValue;\n      }\n      transform.transformPoints(points);\n      // Quick check to see if points transformed to something inside the image;\n      // sufficient to check the endpoints\n      checkAndNudgePoints(image, points);\n      try {\n        for (int x = 0; x < max; x += 2) {\n          if (image.get((int) points[x], (int) points[x + 1])) {\n            // Black(-ish) pixel\n            bits.set(x / 2, y);\n          }\n        }\n      } catch (ArrayIndexOutOfBoundsException aioobe) {\n        // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting\n        // transform gets \"twisted\" such that it maps a straight line of points to a set of points\n        // whose endpoints are in bounds, but others are not. There is probably some mathematical\n        // way to detect this about the transformation that I don't know yet.\n        // This results in an ugly runtime exception despite our clever checks above -- can't have\n        // that. We could check each point's coordinates but that feels duplicative. We settle for\n        // catching and wrapping ArrayIndexOutOfBoundsException.\n        throw NotFoundException.getNotFoundInstance();\n      }\n    }\n    return bits;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/DetectorResult.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport peergos.shared.zxing.ResultPoint;\n\n/**\n * <p>Encapsulates the result of detecting a barcode in an image. This includes the raw\n * matrix of black/white pixels corresponding to the barcode, and possibly points of interest\n * in the image, like the location of finder patterns or corners of the barcode in the image.</p>\n *\n * @author Sean Owen\n */\npublic class DetectorResult {\n\n  private final BitMatrix bits;\n  private final ResultPoint[] points;\n\n  public DetectorResult(BitMatrix bits, ResultPoint[] points) {\n    this.bits = bits;\n    this.points = points;\n  }\n\n  public final BitMatrix getBits() {\n    return bits;\n  }\n\n  public final ResultPoint[] getPoints() {\n    return points;\n  }\n\n}"
  },
  {
    "path": "src/peergos/shared/zxing/common/GlobalHistogramBinarizer.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport peergos.shared.zxing.Binarizer;\nimport peergos.shared.zxing.LuminanceSource;\nimport peergos.shared.zxing.NotFoundException;\n\n/**\n * This Binarizer implementation uses the old ZXing global histogram approach. It is suitable\n * for low-end mobile devices which don't have enough CPU or memory to use a local thresholding\n * algorithm. However, because it picks a global black point, it cannot handle difficult shadows\n * and gradients.\n *\n * Faster mobile devices and all desktop applications should probably use HybridBinarizer instead.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n * @author Sean Owen\n */\npublic class GlobalHistogramBinarizer extends Binarizer {\n\n  private static final int LUMINANCE_BITS = 5;\n  private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS;\n  private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS;\n  private static final byte[] EMPTY = new byte[0];\n\n  private byte[] luminances;\n  private final int[] buckets;\n\n  public GlobalHistogramBinarizer(LuminanceSource source) {\n    super(source);\n    luminances = EMPTY;\n    buckets = new int[LUMINANCE_BUCKETS];\n  }\n\n  // Applies simple sharpening to the row data to improve performance of the 1D Readers.\n  @Override\n  public BitArray getBlackRow(int y, BitArray row) throws NotFoundException {\n    LuminanceSource source = getLuminanceSource();\n    int width = source.getWidth();\n    if (row == null || row.getSize() < width) {\n      row = new BitArray(width);\n    } else {\n      row.clear();\n    }\n\n    initArrays(width);\n    byte[] localLuminances = source.getRow(y, luminances);\n    int[] localBuckets = buckets;\n    for (int x = 0; x < width; x++) {\n      localBuckets[(localLuminances[x] & 0xff) >> LUMINANCE_SHIFT]++;\n    }\n    int blackPoint = estimateBlackPoint(localBuckets);\n\n    if (width < 3) {\n      // Special case for very small images\n      for (int x = 0; x < width; x++) {\n        if ((localLuminances[x] & 0xff) < blackPoint) {\n          row.set(x);\n        }\n      }\n    } else {\n      int left = localLuminances[0] & 0xff;\n      int center = localLuminances[1] & 0xff;\n      for (int x = 1; x < width - 1; x++) {\n        int right = localLuminances[x + 1] & 0xff;\n        // A simple -1 4 -1 box filter with a weight of 2.\n        if (((center * 4) - left - right) / 2 < blackPoint) {\n          row.set(x);\n        }\n        left = center;\n        center = right;\n      }\n    }\n    return row;\n  }\n\n  // Does not sharpen the data, as this call is intended to only be used by 2D Readers.\n  @Override\n  public BitMatrix getBlackMatrix() throws NotFoundException {\n    LuminanceSource source = getLuminanceSource();\n    int width = source.getWidth();\n    int height = source.getHeight();\n    BitMatrix matrix = new BitMatrix(width, height);\n\n    // Quickly calculates the histogram by sampling four rows from the image. This proved to be\n    // more robust on the blackbox tests than sampling a diagonal as we used to do.\n    initArrays(width);\n    int[] localBuckets = buckets;\n    for (int y = 1; y < 5; y++) {\n      int row = height * y / 5;\n      byte[] localLuminances = source.getRow(row, luminances);\n      int right = (width * 4) / 5;\n      for (int x = width / 5; x < right; x++) {\n        int pixel = localLuminances[x] & 0xff;\n        localBuckets[pixel >> LUMINANCE_SHIFT]++;\n      }\n    }\n    int blackPoint = estimateBlackPoint(localBuckets);\n\n    // We delay reading the entire image luminance until the black point estimation succeeds.\n    // Although we end up reading four rows twice, it is consistent with our motto of\n    // \"fail quickly\" which is necessary for continuous scanning.\n    byte[] localLuminances = source.getMatrix();\n    for (int y = 0; y < height; y++) {\n      int offset = y * width;\n      for (int x = 0; x < width; x++) {\n        int pixel = localLuminances[offset + x] & 0xff;\n        if (pixel < blackPoint) {\n          matrix.set(x, y);\n        }\n      }\n    }\n\n    return matrix;\n  }\n\n  @Override\n  public Binarizer createBinarizer(LuminanceSource source) {\n    return new GlobalHistogramBinarizer(source);\n  }\n\n  private void initArrays(int luminanceSize) {\n    if (luminances.length < luminanceSize) {\n      luminances = new byte[luminanceSize];\n    }\n    for (int x = 0; x < LUMINANCE_BUCKETS; x++) {\n      buckets[x] = 0;\n    }\n  }\n\n  private static int estimateBlackPoint(int[] buckets) throws NotFoundException {\n    // Find the tallest peak in the histogram.\n    int numBuckets = buckets.length;\n    int maxBucketCount = 0;\n    int firstPeak = 0;\n    int firstPeakSize = 0;\n    for (int x = 0; x < numBuckets; x++) {\n      if (buckets[x] > firstPeakSize) {\n        firstPeak = x;\n        firstPeakSize = buckets[x];\n      }\n      if (buckets[x] > maxBucketCount) {\n        maxBucketCount = buckets[x];\n      }\n    }\n\n    // Find the second-tallest peak which is somewhat far from the tallest peak.\n    int secondPeak = 0;\n    int secondPeakScore = 0;\n    for (int x = 0; x < numBuckets; x++) {\n      int distanceToBiggest = x - firstPeak;\n      // Encourage more distant second peaks by multiplying by square of distance.\n      int score = buckets[x] * distanceToBiggest * distanceToBiggest;\n      if (score > secondPeakScore) {\n        secondPeak = x;\n        secondPeakScore = score;\n      }\n    }\n\n    // Make sure firstPeak corresponds to the black peak.\n    if (firstPeak > secondPeak) {\n      int temp = firstPeak;\n      firstPeak = secondPeak;\n      secondPeak = temp;\n    }\n\n    // If there is too little contrast in the image to pick a meaningful black point, throw rather\n    // than waste time trying to decode the image, and risk false positives.\n    if (secondPeak - firstPeak <= numBuckets / 16) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n\n    // Find a valley between them that is low and closer to the white peak.\n    int bestValley = secondPeak - 1;\n    int bestValleyScore = -1;\n    for (int x = secondPeak - 1; x > firstPeak; x--) {\n      int fromFirst = x - firstPeak;\n      int score = fromFirst * fromFirst * (secondPeak - x) * (maxBucketCount - buckets[x]);\n      if (score > bestValleyScore) {\n        bestValley = x;\n        bestValleyScore = score;\n      }\n    }\n\n    return bestValley << LUMINANCE_SHIFT;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/GridSampler.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport peergos.shared.zxing.NotFoundException;\n\n/**\n * Implementations of this class can, given locations of finder patterns for a QR code in an\n * image, sample the right points in the image to reconstruct the QR code, accounting for\n * perspective distortion. It is abstracted since it is relatively expensive and should be allowed\n * to take advantage of platform-specific optimized implementations, like Sun's Java Advanced\n * Imaging library, but which may not be available in other environments such as J2ME, and vice\n * versa.\n *\n * The implementation used can be controlled by calling {@link #setGridSampler(GridSampler)}\n * with an instance of a class which implements this interface.\n *\n * @author Sean Owen\n */\npublic abstract class GridSampler {\n\n  private static GridSampler gridSampler = new DefaultGridSampler();\n\n  /**\n   * Sets the implementation of GridSampler used by the library. One global\n   * instance is stored, which may sound problematic. But, the implementation provided\n   * ought to be appropriate for the entire platform, and all uses of this library\n   * in the whole lifetime of the JVM. For instance, an Android activity can swap in\n   * an implementation that takes advantage of native platform libraries.\n   *\n   * @param newGridSampler The platform-specific object to install.\n   */\n  public static void setGridSampler(GridSampler newGridSampler) {\n    gridSampler = newGridSampler;\n  }\n\n  /**\n   * @return the current implementation of GridSampler\n   */\n  public static GridSampler getInstance() {\n    return gridSampler;\n  }\n\n  /**\n   * Samples an image for a rectangular matrix of bits of the given dimension. The sampling\n   * transformation is determined by the coordinates of 4 points, in the original and transformed\n   * image space.\n   *\n   * @param image image to sample\n   * @param dimensionX width of {@link BitMatrix} to sample from image\n   * @param dimensionY height of {@link BitMatrix} to sample from image\n   * @param p1ToX point 1 preimage X\n   * @param p1ToY point 1 preimage Y\n   * @param p2ToX point 2 preimage X\n   * @param p2ToY point 2 preimage Y\n   * @param p3ToX point 3 preimage X\n   * @param p3ToY point 3 preimage Y\n   * @param p4ToX point 4 preimage X\n   * @param p4ToY point 4 preimage Y\n   * @param p1FromX point 1 image X\n   * @param p1FromY point 1 image Y\n   * @param p2FromX point 2 image X\n   * @param p2FromY point 2 image Y\n   * @param p3FromX point 3 image X\n   * @param p3FromY point 3 image Y\n   * @param p4FromX point 4 image X\n   * @param p4FromY point 4 image Y\n   * @return {@link BitMatrix} representing a grid of points sampled from the image within a region\n   *   defined by the \"from\" parameters\n   * @throws NotFoundException if image can't be sampled, for example, if the transformation defined\n   *   by the given points is invalid or results in sampling outside the image boundaries\n   */\n  public abstract BitMatrix sampleGrid(BitMatrix image,\n                                       int dimensionX,\n                                       int dimensionY,\n                                       float p1ToX, float p1ToY,\n                                       float p2ToX, float p2ToY,\n                                       float p3ToX, float p3ToY,\n                                       float p4ToX, float p4ToY,\n                                       float p1FromX, float p1FromY,\n                                       float p2FromX, float p2FromY,\n                                       float p3FromX, float p3FromY,\n                                       float p4FromX, float p4FromY) throws NotFoundException;\n\n  public abstract BitMatrix sampleGrid(BitMatrix image,\n                                       int dimensionX,\n                                       int dimensionY,\n                                       PerspectiveTransform transform) throws NotFoundException;\n\n  /**\n   * <p>Checks a set of points that have been transformed to sample points on an image against\n   * the image's dimensions to see if the point are even within the image.</p>\n   *\n   * <p>This method will actually \"nudge\" the endpoints back onto the image if they are found to be\n   * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder\n   * patterns in an image where the QR Code runs all the way to the image border.</p>\n   *\n   * <p>For efficiency, the method will check points from either end of the line until one is found\n   * to be within the image. Because the set of points are assumed to be linear, this is valid.</p>\n   *\n   * @param image image into which the points should map\n   * @param points actual points in x1,y1,...,xn,yn form\n   * @throws NotFoundException if an endpoint is lies outside the image boundaries\n   */\n  protected static void checkAndNudgePoints(BitMatrix image,\n                                            float[] points) throws NotFoundException {\n    int width = image.getWidth();\n    int height = image.getHeight();\n    // Check and nudge points from start until we see some that are OK:\n    boolean nudged = true;\n    int maxOffset = points.length - 1; // points.length must be even\n    for (int offset = 0; offset < maxOffset && nudged; offset += 2) {\n      int x = (int) points[offset];\n      int y = (int) points[offset + 1];\n      if (x < -1 || x > width || y < -1 || y > height) {\n        throw NotFoundException.getNotFoundInstance();\n      }\n      nudged = false;\n      if (x == -1) {\n        points[offset] = 0.0f;\n        nudged = true;\n      } else if (x == width) {\n        points[offset] = width - 1;\n        nudged = true;\n      }\n      if (y == -1) {\n        points[offset + 1] = 0.0f;\n        nudged = true;\n      } else if (y == height) {\n        points[offset + 1] = height - 1;\n        nudged = true;\n      }\n    }\n    // Check and nudge points from end:\n    nudged = true;\n    for (int offset = points.length - 2; offset >= 0 && nudged; offset -= 2) {\n      int x = (int) points[offset];\n      int y = (int) points[offset + 1];\n      if (x < -1 || x > width || y < -1 || y > height) {\n        throw NotFoundException.getNotFoundInstance();\n      }\n      nudged = false;\n      if (x == -1) {\n        points[offset] = 0.0f;\n        nudged = true;\n      } else if (x == width) {\n        points[offset] = width - 1;\n        nudged = true;\n      }\n      if (y == -1) {\n        points[offset + 1] = 0.0f;\n        nudged = true;\n      } else if (y == height) {\n        points[offset + 1] = height - 1;\n        nudged = true;\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/HybridBinarizer.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport peergos.shared.zxing.Binarizer;\nimport peergos.shared.zxing.LuminanceSource;\nimport peergos.shared.zxing.NotFoundException;\n\n/**\n * This class implements a local thresholding algorithm, which while slower than the\n * GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for\n * high frequency images of barcodes with black data on white backgrounds. For this application,\n * it does a much better job than a global blackpoint with severe shadows and gradients.\n * However it tends to produce artifacts on lower frequency images and is therefore not\n * a good general purpose binarizer for uses outside ZXing.\n *\n * This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers,\n * and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already\n * inherently local, and only fails for horizontal gradients. We can revisit that problem later,\n * but for now it was not a win to use local blocks for 1D.\n *\n * This Binarizer is the default for the unit tests and the recommended class for library users.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic final class HybridBinarizer extends GlobalHistogramBinarizer {\n\n  // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels.\n  // So this is the smallest dimension in each axis we can accept.\n  private static final int BLOCK_SIZE_POWER = 3;\n  private static final int BLOCK_SIZE = 1 << BLOCK_SIZE_POWER; // ...0100...00\n  private static final int BLOCK_SIZE_MASK = BLOCK_SIZE - 1;   // ...0011...11\n  private static final int MINIMUM_DIMENSION = BLOCK_SIZE * 5;\n  private static final int MIN_DYNAMIC_RANGE = 24;\n\n  private BitMatrix matrix;\n\n  public HybridBinarizer(LuminanceSource source) {\n    super(source);\n  }\n\n  /**\n   * Calculates the final BitMatrix once for all requests. This could be called once from the\n   * constructor instead, but there are some advantages to doing it lazily, such as making\n   * profiling easier, and not doing heavy lifting when callers don't expect it.\n   */\n  @Override\n  public BitMatrix getBlackMatrix() throws NotFoundException {\n    if (matrix != null) {\n      return matrix;\n    }\n    LuminanceSource source = getLuminanceSource();\n    int width = source.getWidth();\n    int height = source.getHeight();\n    if (width >= MINIMUM_DIMENSION && height >= MINIMUM_DIMENSION) {\n      byte[] luminances = source.getMatrix();\n      int subWidth = width >> BLOCK_SIZE_POWER;\n      if ((width & BLOCK_SIZE_MASK) != 0) {\n        subWidth++;\n      }\n      int subHeight = height >> BLOCK_SIZE_POWER;\n      if ((height & BLOCK_SIZE_MASK) != 0) {\n        subHeight++;\n      }\n      int[][] blackPoints = calculateBlackPoints(luminances, subWidth, subHeight, width, height);\n\n      BitMatrix newMatrix = new BitMatrix(width, height);\n      calculateThresholdForBlock(luminances, subWidth, subHeight, width, height, blackPoints, newMatrix);\n      matrix = newMatrix;\n    } else {\n      // If the image is too small, fall back to the global histogram approach.\n      matrix = super.getBlackMatrix();\n    }\n    return matrix;\n  }\n\n  @Override\n  public Binarizer createBinarizer(LuminanceSource source) {\n    return new HybridBinarizer(source);\n  }\n\n  /**\n   * For each block in the image, calculate the average black point using a 5x5 grid\n   * of the blocks around it. Also handles the corner cases (fractional blocks are computed based\n   * on the last pixels in the row/column which are also used in the previous block).\n   */\n  private static void calculateThresholdForBlock(byte[] luminances,\n                                                 int subWidth,\n                                                 int subHeight,\n                                                 int width,\n                                                 int height,\n                                                 int[][] blackPoints,\n                                                 BitMatrix matrix) {\n    int maxYOffset = height - BLOCK_SIZE;\n    int maxXOffset = width - BLOCK_SIZE;\n    for (int y = 0; y < subHeight; y++) {\n      int yoffset = y << BLOCK_SIZE_POWER;\n      if (yoffset > maxYOffset) {\n        yoffset = maxYOffset;\n      }\n      int top = cap(y, subHeight - 3);\n      for (int x = 0; x < subWidth; x++) {\n        int xoffset = x << BLOCK_SIZE_POWER;\n        if (xoffset > maxXOffset) {\n          xoffset = maxXOffset;\n        }\n        int left = cap(x, subWidth - 3);\n        int sum = 0;\n        for (int z = -2; z <= 2; z++) {\n          int[] blackRow = blackPoints[top + z];\n          sum += blackRow[left - 2] + blackRow[left - 1] + blackRow[left] + blackRow[left + 1] + blackRow[left + 2];\n        }\n        int average = sum / 25;\n        thresholdBlock(luminances, xoffset, yoffset, average, width, matrix);\n      }\n    }\n  }\n\n  private static int cap(int value, int max) {\n    return value < 2 ? 2 : value > max ? max : value;\n  }\n\n  /**\n   * Applies a single threshold to a block of pixels.\n   */\n  private static void thresholdBlock(byte[] luminances,\n                                     int xoffset,\n                                     int yoffset,\n                                     int threshold,\n                                     int stride,\n                                     BitMatrix matrix) {\n    for (int y = 0, offset = yoffset * stride + xoffset; y < BLOCK_SIZE; y++, offset += stride) {\n      for (int x = 0; x < BLOCK_SIZE; x++) {\n        // Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.\n        if ((luminances[offset + x] & 0xFF) <= threshold) {\n          matrix.set(xoffset + x, yoffset + y);\n        }\n      }\n    }\n  }\n\n  /**\n   * Calculates a single black point for each block of pixels and saves it away.\n   * See the following thread for a discussion of this algorithm:\n   *  http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0\n   */\n  private static int[][] calculateBlackPoints(byte[] luminances,\n                                              int subWidth,\n                                              int subHeight,\n                                              int width,\n                                              int height) {\n    int maxYOffset = height - BLOCK_SIZE;\n    int maxXOffset = width - BLOCK_SIZE;\n    int[][] blackPoints = new int[subHeight][subWidth];\n    for (int y = 0; y < subHeight; y++) {\n      int yoffset = y << BLOCK_SIZE_POWER;\n      if (yoffset > maxYOffset) {\n        yoffset = maxYOffset;\n      }\n      for (int x = 0; x < subWidth; x++) {\n        int xoffset = x << BLOCK_SIZE_POWER;\n        if (xoffset > maxXOffset) {\n          xoffset = maxXOffset;\n        }\n        int sum = 0;\n        int min = 0xFF;\n        int max = 0;\n        for (int yy = 0, offset = yoffset * width + xoffset; yy < BLOCK_SIZE; yy++, offset += width) {\n          for (int xx = 0; xx < BLOCK_SIZE; xx++) {\n            int pixel = luminances[offset + xx] & 0xFF;\n            sum += pixel;\n            // still looking for good contrast\n            if (pixel < min) {\n              min = pixel;\n            }\n            if (pixel > max) {\n              max = pixel;\n            }\n          }\n          // short-circuit min/max tests once dynamic range is met\n          if (max - min > MIN_DYNAMIC_RANGE) {\n            // finish the rest of the rows quickly\n            for (yy++, offset += width; yy < BLOCK_SIZE; yy++, offset += width) {\n              for (int xx = 0; xx < BLOCK_SIZE; xx++) {\n                sum += luminances[offset + xx] & 0xFF;\n              }\n            }\n          }\n        }\n\n        // The default estimate is the average of the values in the block.\n        int average = sum >> (BLOCK_SIZE_POWER * 2);\n        if (max - min <= MIN_DYNAMIC_RANGE) {\n          // If variation within the block is low, assume this is a block with only light or only\n          // dark pixels. In that case we do not want to use the average, as it would divide this\n          // low contrast area into black and white pixels, essentially creating data out of noise.\n          //\n          // The default assumption is that the block is light/background. Since no estimate for\n          // the level of dark pixels exists locally, use half the min for the block.\n          average = min / 2;\n\n          if (y > 0 && x > 0) {\n            // Correct the \"white background\" assumption for blocks that have neighbors by comparing\n            // the pixels in this block to the previously calculated black points. This is based on\n            // the fact that dark barcode symbology is always surrounded by some amount of light\n            // background for which reasonable black point estimates were made. The bp estimated at\n            // the boundaries is used for the interior.\n\n            // The (min < bp) is arbitrary but works better than other heuristics that were tried.\n            int averageNeighborBlackPoint =\n                (blackPoints[y - 1][x] + (2 * blackPoints[y][x - 1]) + blackPoints[y - 1][x - 1]) / 4;\n            if (min < averageNeighborBlackPoint) {\n              average = averageNeighborBlackPoint;\n            }\n          }\n        }\n        blackPoints[y][x] = average;\n      }\n    }\n    return blackPoints;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/PerspectiveTransform.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\n/**\n * <p>This class implements a perspective transform in two dimensions. Given four source and four\n * destination points, it will compute the transformation implied between them. The code is based\n * directly upon section 3.4.2 of George Wolberg's \"Digital Image Warping\"; see pages 54-56.</p>\n *\n * @author Sean Owen\n */\npublic final class PerspectiveTransform {\n\n  private final float a11;\n  private final float a12;\n  private final float a13;\n  private final float a21;\n  private final float a22;\n  private final float a23;\n  private final float a31;\n  private final float a32;\n  private final float a33;\n\n  private PerspectiveTransform(float a11, float a21, float a31,\n                               float a12, float a22, float a32,\n                               float a13, float a23, float a33) {\n    this.a11 = a11;\n    this.a12 = a12;\n    this.a13 = a13;\n    this.a21 = a21;\n    this.a22 = a22;\n    this.a23 = a23;\n    this.a31 = a31;\n    this.a32 = a32;\n    this.a33 = a33;\n  }\n\n  public static PerspectiveTransform quadrilateralToQuadrilateral(float x0, float y0,\n                                                                  float x1, float y1,\n                                                                  float x2, float y2,\n                                                                  float x3, float y3,\n                                                                  float x0p, float y0p,\n                                                                  float x1p, float y1p,\n                                                                  float x2p, float y2p,\n                                                                  float x3p, float y3p) {\n\n    PerspectiveTransform qToS = quadrilateralToSquare(x0, y0, x1, y1, x2, y2, x3, y3);\n    PerspectiveTransform sToQ = squareToQuadrilateral(x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p);\n    return sToQ.times(qToS);\n  }\n\n  public void transformPoints(float[] points) {\n    float a11 = this.a11;\n    float a12 = this.a12;\n    float a13 = this.a13;\n    float a21 = this.a21;\n    float a22 = this.a22;\n    float a23 = this.a23;\n    float a31 = this.a31;\n    float a32 = this.a32;\n    float a33 = this.a33;\n    int maxI = points.length - 1; // points.length must be even\n    for (int i = 0; i < maxI; i += 2) {\n      float x = points[i];\n      float y = points[i + 1];\n      float denominator = a13 * x + a23 * y + a33;\n      points[i] = (a11 * x + a21 * y + a31) / denominator;\n      points[i + 1] = (a12 * x + a22 * y + a32) / denominator;\n    }\n  }\n\n  public void transformPoints(float[] xValues, float[] yValues) {\n    int n = xValues.length;\n    for (int i = 0; i < n; i++) {\n      float x = xValues[i];\n      float y = yValues[i];\n      float denominator = a13 * x + a23 * y + a33;\n      xValues[i] = (a11 * x + a21 * y + a31) / denominator;\n      yValues[i] = (a12 * x + a22 * y + a32) / denominator;\n    }\n  }\n\n  public static PerspectiveTransform squareToQuadrilateral(float x0, float y0,\n                                                           float x1, float y1,\n                                                           float x2, float y2,\n                                                           float x3, float y3) {\n    float dx3 = x0 - x1 + x2 - x3;\n    float dy3 = y0 - y1 + y2 - y3;\n    if (dx3 == 0.0f && dy3 == 0.0f) {\n      // Affine\n      return new PerspectiveTransform(x1 - x0, x2 - x1, x0,\n                                      y1 - y0, y2 - y1, y0,\n                                      0.0f,    0.0f,    1.0f);\n    } else {\n      float dx1 = x1 - x2;\n      float dx2 = x3 - x2;\n      float dy1 = y1 - y2;\n      float dy2 = y3 - y2;\n      float denominator = dx1 * dy2 - dx2 * dy1;\n      float a13 = (dx3 * dy2 - dx2 * dy3) / denominator;\n      float a23 = (dx1 * dy3 - dx3 * dy1) / denominator;\n      return new PerspectiveTransform(x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0,\n                                      y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0,\n                                      a13,                a23,                1.0f);\n    }\n  }\n\n  public static PerspectiveTransform quadrilateralToSquare(float x0, float y0,\n                                                           float x1, float y1,\n                                                           float x2, float y2,\n                                                           float x3, float y3) {\n    // Here, the adjoint serves as the inverse:\n    return squareToQuadrilateral(x0, y0, x1, y1, x2, y2, x3, y3).buildAdjoint();\n  }\n\n  PerspectiveTransform buildAdjoint() {\n    // Adjoint is the transpose of the cofactor matrix:\n    return new PerspectiveTransform(a22 * a33 - a23 * a32,\n        a23 * a31 - a21 * a33,\n        a21 * a32 - a22 * a31,\n        a13 * a32 - a12 * a33,\n        a11 * a33 - a13 * a31,\n        a12 * a31 - a11 * a32,\n        a12 * a23 - a13 * a22,\n        a13 * a21 - a11 * a23,\n        a11 * a22 - a12 * a21);\n  }\n\n  PerspectiveTransform times(PerspectiveTransform other) {\n    return new PerspectiveTransform(a11 * other.a11 + a21 * other.a12 + a31 * other.a13,\n        a11 * other.a21 + a21 * other.a22 + a31 * other.a23,\n        a11 * other.a31 + a21 * other.a32 + a31 * other.a33,\n        a12 * other.a11 + a22 * other.a12 + a32 * other.a13,\n        a12 * other.a21 + a22 * other.a22 + a32 * other.a23,\n        a12 * other.a31 + a22 * other.a32 + a32 * other.a33,\n        a13 * other.a11 + a23 * other.a12 + a33 * other.a13,\n        a13 * other.a21 + a23 * other.a22 + a33 * other.a23,\n        a13 * other.a31 + a23 * other.a32 + a33 * other.a33);\n\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/StringUtils.java",
    "content": "/*\n * Copyright (C) 2010 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common;\n\nimport java.nio.charset.Charset;\nimport java.util.Map;\n\nimport peergos.shared.zxing.DecodeHintType;\n\n/**\n * Common string-related functions.\n *\n * @author Sean Owen\n * @author Alex Dupre\n */\npublic final class StringUtils {\n\n  private static final String PLATFORM_DEFAULT_ENCODING = \"UTF-8\";\n  public static final String SHIFT_JIS = \"SJIS\";\n  public static final String GB2312 = \"GB2312\";\n  private static final String EUC_JP = \"EUC_JP\";\n  private static final String UTF8 = \"UTF8\";\n  private static final String ISO88591 = \"ISO8859_1\";\n  private static final boolean ASSUME_SHIFT_JIS =\n      SHIFT_JIS.equalsIgnoreCase(PLATFORM_DEFAULT_ENCODING) ||\n      EUC_JP.equalsIgnoreCase(PLATFORM_DEFAULT_ENCODING);\n\n  private StringUtils() { }\n\n  /**\n   * @param bytes bytes encoding a string, whose encoding should be guessed\n   * @param hints decode hints if applicable\n   * @return name of guessed encoding; at the moment will only guess one of:\n   *  {@link #SHIFT_JIS}, {@link #UTF8}, {@link #ISO88591}, or the platform\n   *  default encoding if none of these can possibly be correct\n   */\n  public static String guessEncoding(byte[] bytes, Map<DecodeHintType,?> hints) {\n    if (hints != null && hints.containsKey(DecodeHintType.CHARACTER_SET)) {\n      return hints.get(DecodeHintType.CHARACTER_SET).toString();\n    }\n    // For now, merely tries to distinguish ISO-8859-1, UTF-8 and Shift_JIS,\n    // which should be by far the most common encodings.\n    int length = bytes.length;\n    boolean canBeISO88591 = true;\n    boolean canBeShiftJIS = true;\n    boolean canBeUTF8 = true;\n    int utf8BytesLeft = 0;\n    int utf2BytesChars = 0;\n    int utf3BytesChars = 0;\n    int utf4BytesChars = 0;\n    int sjisBytesLeft = 0;\n    int sjisKatakanaChars = 0;\n    int sjisCurKatakanaWordLength = 0;\n    int sjisCurDoubleBytesWordLength = 0;\n    int sjisMaxKatakanaWordLength = 0;\n    int sjisMaxDoubleBytesWordLength = 0;\n    int isoHighOther = 0;\n\n    boolean utf8bom = bytes.length > 3 &&\n        bytes[0] == (byte) 0xEF &&\n        bytes[1] == (byte) 0xBB &&\n        bytes[2] == (byte) 0xBF;\n\n    for (int i = 0;\n         i < length && (canBeISO88591 || canBeShiftJIS || canBeUTF8);\n         i++) {\n\n      int value = bytes[i] & 0xFF;\n\n      // UTF-8 stuff\n      if (canBeUTF8) {\n        if (utf8BytesLeft > 0) {\n          if ((value & 0x80) == 0) {\n            canBeUTF8 = false;\n          } else {\n            utf8BytesLeft--;\n          }\n        } else if ((value & 0x80) != 0) {\n          if ((value & 0x40) == 0) {\n            canBeUTF8 = false;\n          } else {\n            utf8BytesLeft++;\n            if ((value & 0x20) == 0) {\n              utf2BytesChars++;\n            } else {\n              utf8BytesLeft++;\n              if ((value & 0x10) == 0) {\n                utf3BytesChars++;\n              } else {\n                utf8BytesLeft++;\n                if ((value & 0x08) == 0) {\n                  utf4BytesChars++;\n                } else {\n                  canBeUTF8 = false;\n                }\n              }\n            }\n          }\n        }\n      }\n\n      // ISO-8859-1 stuff\n      if (canBeISO88591) {\n        if (value > 0x7F && value < 0xA0) {\n          canBeISO88591 = false;\n        } else if (value > 0x9F && (value < 0xC0 || value == 0xD7 || value == 0xF7)) {\n          isoHighOther++;\n        }\n      }\n\n      // Shift_JIS stuff\n      if (canBeShiftJIS) {\n        if (sjisBytesLeft > 0) {\n          if (value < 0x40 || value == 0x7F || value > 0xFC) {\n            canBeShiftJIS = false;\n          } else {\n            sjisBytesLeft--;\n          }\n        } else if (value == 0x80 || value == 0xA0 || value > 0xEF) {\n          canBeShiftJIS = false;\n        } else if (value > 0xA0 && value < 0xE0) {\n          sjisKatakanaChars++;\n          sjisCurDoubleBytesWordLength = 0;\n          sjisCurKatakanaWordLength++;\n          if (sjisCurKatakanaWordLength > sjisMaxKatakanaWordLength) {\n            sjisMaxKatakanaWordLength = sjisCurKatakanaWordLength;\n          }\n        } else if (value > 0x7F) {\n          sjisBytesLeft++;\n          //sjisDoubleBytesChars++;\n          sjisCurKatakanaWordLength = 0;\n          sjisCurDoubleBytesWordLength++;\n          if (sjisCurDoubleBytesWordLength > sjisMaxDoubleBytesWordLength) {\n            sjisMaxDoubleBytesWordLength = sjisCurDoubleBytesWordLength;\n          }\n        } else {\n          //sjisLowChars++;\n          sjisCurKatakanaWordLength = 0;\n          sjisCurDoubleBytesWordLength = 0;\n        }\n      }\n    }\n\n    if (canBeUTF8 && utf8BytesLeft > 0) {\n      canBeUTF8 = false;\n    }\n    if (canBeShiftJIS && sjisBytesLeft > 0) {\n      canBeShiftJIS = false;\n    }\n\n    // Easy -- if there is BOM or at least 1 valid not-single byte character (and no evidence it can't be UTF-8), done\n    if (canBeUTF8 && (utf8bom || utf2BytesChars + utf3BytesChars + utf4BytesChars > 0)) {\n      return UTF8;\n    }\n    // Easy -- if assuming Shift_JIS or >= 3 valid consecutive not-ascii characters (and no evidence it can't be), done\n    if (canBeShiftJIS && (ASSUME_SHIFT_JIS || sjisMaxKatakanaWordLength >= 3 || sjisMaxDoubleBytesWordLength >= 3)) {\n      return SHIFT_JIS;\n    }\n    // Distinguishing Shift_JIS and ISO-8859-1 can be a little tough for short words. The crude heuristic is:\n    // - If we saw\n    //   - only two consecutive katakana chars in the whole text, or\n    //   - at least 10% of bytes that could be \"upper\" not-alphanumeric Latin1,\n    // - then we conclude Shift_JIS, else ISO-8859-1\n    if (canBeISO88591 && canBeShiftJIS) {\n      return (sjisMaxKatakanaWordLength == 2 && sjisKatakanaChars == 2) || isoHighOther * 10 >= length\n          ? SHIFT_JIS : ISO88591;\n    }\n\n    // Otherwise, try in order ISO-8859-1, Shift JIS, UTF-8 and fall back to default platform encoding\n    if (canBeISO88591) {\n      return ISO88591;\n    }\n    if (canBeShiftJIS) {\n      return SHIFT_JIS;\n    }\n    if (canBeUTF8) {\n      return UTF8;\n    }\n    // Otherwise, we take a wild guess with platform encoding\n    return PLATFORM_DEFAULT_ENCODING;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/detector/MathUtils.java",
    "content": "/*\n * Copyright 2012 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.detector;\n\n/**\n * General math-related and numeric utility functions.\n */\npublic final class MathUtils {\n\n  private MathUtils() {\n  }\n\n  /**\n   * Ends up being a bit faster than {@link Math#round(float)}. This merely rounds its\n   * argument to the nearest int, where x.5 rounds up to x+1. Semantics of this shortcut\n   * differ slightly from {@link Math#round(float)} in that half rounds down for negative\n   * values. -2.5 rounds to -3, not -2. For purposes here it makes no difference.\n   *\n   * @param d real value to round\n   * @return nearest {@code int}\n   */\n  public static int round(float d) {\n    return (int) (d + (d < 0.0f ? -0.5f : 0.5f));\n  }\n\n  /**\n   * @param aX point A x coordinate\n   * @param aY point A y coordinate\n   * @param bX point B x coordinate\n   * @param bY point B y coordinate\n   * @return Euclidean distance between points A and B\n   */\n  public static float distance(float aX, float aY, float bX, float bY) {\n    double xDiff = aX - bX;\n    double yDiff = aY - bY;\n    return (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff);\n  }\n\n  /**\n   * @param aX point A x coordinate\n   * @param aY point A y coordinate\n   * @param bX point B x coordinate\n   * @param bY point B y coordinate\n   * @return Euclidean distance between points A and B\n   */\n  public static float distance(int aX, int aY, int bX, int bY) {\n    double xDiff = aX - bX;\n    double yDiff = aY - bY;\n    return (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff);\n  }\n\n  /**\n   * @param array values to sum\n   * @return sum of values in array\n   */\n  public static int sum(int[] array) {\n    int count = 0;\n    for (int a : array) {\n      count += a;\n    }\n    return count;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/detector/MonochromeRectangleDetector.java",
    "content": "/*\n * Copyright 2009 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.detector;\n\nimport peergos.shared.zxing.NotFoundException;\nimport peergos.shared.zxing.ResultPoint;\nimport peergos.shared.zxing.common.BitMatrix;\n\n/**\n * <p>A somewhat generic detector that looks for a barcode-like rectangular region within an image.\n * It looks within a mostly white region of an image for a region of black and white, but mostly\n * black. It returns the four corners of the region, as best it can determine.</p>\n *\n * @author Sean Owen\n * @deprecated without replacement since 3.3.0\n */\n@Deprecated\npublic final class MonochromeRectangleDetector {\n\n  private static final int MAX_MODULES = 32;\n\n  private final BitMatrix image;\n\n  public MonochromeRectangleDetector(BitMatrix image) {\n    this.image = image;\n  }\n\n  /**\n   * <p>Detects a rectangular region of black and white -- mostly black -- with a region of mostly\n   * white, in an image.</p>\n   *\n   * @return {@link ResultPoint}[] describing the corners of the rectangular region. The first and\n   *  last points are opposed on the diagonal, as are the second and third. The first point will be\n   *  the topmost point and the last, the bottommost. The second point will be leftmost and the\n   *  third, the rightmost\n   * @throws NotFoundException if no Data Matrix Code can be found\n   */\n  public ResultPoint[] detect() throws NotFoundException {\n    int height = image.getHeight();\n    int width = image.getWidth();\n    int halfHeight = height / 2;\n    int halfWidth = width / 2;\n    int deltaY = Math.max(1, height / (MAX_MODULES * 8));\n    int deltaX = Math.max(1, width / (MAX_MODULES * 8));\n\n    int top = 0;\n    int bottom = height;\n    int left = 0;\n    int right = width;\n    ResultPoint pointA = findCornerFromCenter(halfWidth, 0, left, right,\n        halfHeight, -deltaY, top, bottom, halfWidth / 2);\n    top = (int) pointA.getY() - 1;\n    ResultPoint pointB = findCornerFromCenter(halfWidth, -deltaX, left, right,\n        halfHeight, 0, top, bottom, halfHeight / 2);\n    left = (int) pointB.getX() - 1;\n    ResultPoint pointC = findCornerFromCenter(halfWidth, deltaX, left, right,\n        halfHeight, 0, top, bottom, halfHeight / 2);\n    right = (int) pointC.getX() + 1;\n    ResultPoint pointD = findCornerFromCenter(halfWidth, 0, left, right,\n        halfHeight, deltaY, top, bottom, halfWidth / 2);\n    bottom = (int) pointD.getY() + 1;\n\n    // Go try to find point A again with better information -- might have been off at first.\n    pointA = findCornerFromCenter(halfWidth, 0, left, right,\n        halfHeight, -deltaY, top, bottom, halfWidth / 4);\n\n    return new ResultPoint[] { pointA, pointB, pointC, pointD };\n  }\n\n  /**\n   * Attempts to locate a corner of the barcode by scanning up, down, left or right from a center\n   * point which should be within the barcode.\n   *\n   * @param centerX center's x component (horizontal)\n   * @param deltaX same as deltaY but change in x per step instead\n   * @param left minimum value of x\n   * @param right maximum value of x\n   * @param centerY center's y component (vertical)\n   * @param deltaY change in y per step. If scanning up this is negative; down, positive;\n   *  left or right, 0\n   * @param top minimum value of y to search through (meaningless when di == 0)\n   * @param bottom maximum value of y\n   * @param maxWhiteRun maximum run of white pixels that can still be considered to be within\n   *  the barcode\n   * @return a {@link ResultPoint} encapsulating the corner that was found\n   * @throws NotFoundException if such a point cannot be found\n   */\n  private ResultPoint findCornerFromCenter(int centerX,\n                                           int deltaX,\n                                           int left,\n                                           int right,\n                                           int centerY,\n                                           int deltaY,\n                                           int top,\n                                           int bottom,\n                                           int maxWhiteRun) throws NotFoundException {\n    int[] lastRange = null;\n    for (int y = centerY, x = centerX;\n         y < bottom && y >= top && x < right && x >= left;\n         y += deltaY, x += deltaX) {\n      int[] range;\n      if (deltaX == 0) {\n        // horizontal slices, up and down\n        range = blackWhiteRange(y, maxWhiteRun, left, right, true);\n      } else {\n        // vertical slices, left and right\n        range = blackWhiteRange(x, maxWhiteRun, top, bottom, false);\n      }\n      if (range == null) {\n        if (lastRange == null) {\n          throw NotFoundException.getNotFoundInstance();\n        }\n        // lastRange was found\n        if (deltaX == 0) {\n          int lastY = y - deltaY;\n          if (lastRange[0] < centerX) {\n            if (lastRange[1] > centerX) {\n              // straddle, choose one or the other based on direction\n              return new ResultPoint(lastRange[deltaY > 0 ? 0 : 1], lastY);\n            }\n            return new ResultPoint(lastRange[0], lastY);\n          } else {\n            return new ResultPoint(lastRange[1], lastY);\n          }\n        } else {\n          int lastX = x - deltaX;\n          if (lastRange[0] < centerY) {\n            if (lastRange[1] > centerY) {\n              return new ResultPoint(lastX, lastRange[deltaX < 0 ? 0 : 1]);\n            }\n            return new ResultPoint(lastX, lastRange[0]);\n          } else {\n            return new ResultPoint(lastX, lastRange[1]);\n          }\n        }\n      }\n      lastRange = range;\n    }\n    throw NotFoundException.getNotFoundInstance();\n  }\n\n  /**\n   * Computes the start and end of a region of pixels, either horizontally or vertically, that could\n   * be part of a Data Matrix barcode.\n   *\n   * @param fixedDimension if scanning horizontally, this is the row (the fixed vertical location)\n   *  where we are scanning. If scanning vertically it's the column, the fixed horizontal location\n   * @param maxWhiteRun largest run of white pixels that can still be considered part of the\n   *  barcode region\n   * @param minDim minimum pixel location, horizontally or vertically, to consider\n   * @param maxDim maximum pixel location, horizontally or vertically, to consider\n   * @param horizontal if true, we're scanning left-right, instead of up-down\n   * @return int[] with start and end of found range, or null if no such range is found\n   *  (e.g. only white was found)\n   */\n  private int[] blackWhiteRange(int fixedDimension, int maxWhiteRun, int minDim, int maxDim, boolean horizontal) {\n\n    int center = (minDim + maxDim) / 2;\n\n    // Scan left/up first\n    int start = center;\n    while (start >= minDim) {\n      if (horizontal ? image.get(start, fixedDimension) : image.get(fixedDimension, start)) {\n        start--;\n      } else {\n        int whiteRunStart = start;\n        do {\n          start--;\n        } while (start >= minDim && !(horizontal ? image.get(start, fixedDimension) :\n            image.get(fixedDimension, start)));\n        int whiteRunSize = whiteRunStart - start;\n        if (start < minDim || whiteRunSize > maxWhiteRun) {\n          start = whiteRunStart;\n          break;\n        }\n      }\n    }\n    start++;\n\n    // Then try right/down\n    int end = center;\n    while (end < maxDim) {\n      if (horizontal ? image.get(end, fixedDimension) : image.get(fixedDimension, end)) {\n        end++;\n      } else {\n        int whiteRunStart = end;\n        do {\n          end++;\n        } while (end < maxDim && !(horizontal ? image.get(end, fixedDimension) :\n            image.get(fixedDimension, end)));\n        int whiteRunSize = end - whiteRunStart;\n        if (end >= maxDim || whiteRunSize > maxWhiteRun) {\n          end = whiteRunStart;\n          break;\n        }\n      }\n    }\n    end--;\n\n    return end > start ? new int[]{start, end} : null;\n  }\n\n}"
  },
  {
    "path": "src/peergos/shared/zxing/common/detector/WhiteRectangleDetector.java",
    "content": "/*\n * Copyright 2010 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.detector;\n\nimport peergos.shared.zxing.NotFoundException;\nimport peergos.shared.zxing.ResultPoint;\nimport peergos.shared.zxing.common.BitMatrix;\n\n/**\n * <p>\n * Detects a candidate barcode-like rectangular region within an image. It\n * starts around the center of the image, increases the size of the candidate\n * region until it finds a white rectangular region. By keeping track of the\n * last black points it encountered, it determines the corners of the barcode.\n * </p>\n *\n * @author David Olivier\n */\npublic final class WhiteRectangleDetector {\n\n  private static final int INIT_SIZE = 10;\n  private static final int CORR = 1;\n\n  private final BitMatrix image;\n  private final int height;\n  private final int width;\n  private final int leftInit;\n  private final int rightInit;\n  private final int downInit;\n  private final int upInit;\n\n  public WhiteRectangleDetector(BitMatrix image) throws NotFoundException {\n    this(image, INIT_SIZE, image.getWidth() / 2, image.getHeight() / 2);\n  }\n\n  /**\n   * @param image barcode image to find a rectangle in\n   * @param initSize initial size of search area around center\n   * @param x x position of search center\n   * @param y y position of search center\n   * @throws NotFoundException if image is too small to accommodate {@code initSize}\n   */\n  public WhiteRectangleDetector(BitMatrix image, int initSize, int x, int y) throws NotFoundException {\n    this.image = image;\n    height = image.getHeight();\n    width = image.getWidth();\n    int halfsize = initSize / 2;\n    leftInit = x - halfsize;\n    rightInit = x + halfsize;\n    upInit = y - halfsize;\n    downInit = y + halfsize;\n    if (upInit < 0 || leftInit < 0 || downInit >= height || rightInit >= width) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n  }\n\n  /**\n   * <p>\n   * Detects a candidate barcode-like rectangular region within an image. It\n   * starts around the center of the image, increases the size of the candidate\n   * region until it finds a white rectangular region.\n   * </p>\n   *\n   * @return {@link ResultPoint}[] describing the corners of the rectangular\n   *         region. The first and last points are opposed on the diagonal, as\n   *         are the second and third. The first point will be the topmost\n   *         point and the last, the bottommost. The second point will be\n   *         leftmost and the third, the rightmost\n   * @throws NotFoundException if no Data Matrix Code can be found\n   */\n  public ResultPoint[] detect() throws NotFoundException {\n\n    int left = leftInit;\n    int right = rightInit;\n    int up = upInit;\n    int down = downInit;\n    boolean sizeExceeded = false;\n    boolean aBlackPointFoundOnBorder = true;\n\n    boolean atLeastOneBlackPointFoundOnRight = false;\n    boolean atLeastOneBlackPointFoundOnBottom = false;\n    boolean atLeastOneBlackPointFoundOnLeft = false;\n    boolean atLeastOneBlackPointFoundOnTop = false;\n\n    while (aBlackPointFoundOnBorder) {\n\n      aBlackPointFoundOnBorder = false;\n\n      // .....\n      // .   |\n      // .....\n      boolean rightBorderNotWhite = true;\n      while ((rightBorderNotWhite || !atLeastOneBlackPointFoundOnRight) && right < width) {\n        rightBorderNotWhite = containsBlackPoint(up, down, right, false);\n        if (rightBorderNotWhite) {\n          right++;\n          aBlackPointFoundOnBorder = true;\n          atLeastOneBlackPointFoundOnRight = true;\n        } else if (!atLeastOneBlackPointFoundOnRight) {\n          right++;\n        }\n      }\n\n      if (right >= width) {\n        sizeExceeded = true;\n        break;\n      }\n\n      // .....\n      // .   .\n      // .___.\n      boolean bottomBorderNotWhite = true;\n      while ((bottomBorderNotWhite || !atLeastOneBlackPointFoundOnBottom) && down < height) {\n        bottomBorderNotWhite = containsBlackPoint(left, right, down, true);\n        if (bottomBorderNotWhite) {\n          down++;\n          aBlackPointFoundOnBorder = true;\n          atLeastOneBlackPointFoundOnBottom = true;\n        } else if (!atLeastOneBlackPointFoundOnBottom) {\n          down++;\n        }\n      }\n\n      if (down >= height) {\n        sizeExceeded = true;\n        break;\n      }\n\n      // .....\n      // |   .\n      // .....\n      boolean leftBorderNotWhite = true;\n      while ((leftBorderNotWhite || !atLeastOneBlackPointFoundOnLeft) && left >= 0) {\n        leftBorderNotWhite = containsBlackPoint(up, down, left, false);\n        if (leftBorderNotWhite) {\n          left--;\n          aBlackPointFoundOnBorder = true;\n          atLeastOneBlackPointFoundOnLeft = true;\n        } else if (!atLeastOneBlackPointFoundOnLeft) {\n          left--;\n        }\n      }\n\n      if (left < 0) {\n        sizeExceeded = true;\n        break;\n      }\n\n      // .___.\n      // .   .\n      // .....\n      boolean topBorderNotWhite = true;\n      while ((topBorderNotWhite || !atLeastOneBlackPointFoundOnTop) && up >= 0) {\n        topBorderNotWhite = containsBlackPoint(left, right, up, true);\n        if (topBorderNotWhite) {\n          up--;\n          aBlackPointFoundOnBorder = true;\n          atLeastOneBlackPointFoundOnTop = true;\n        } else if (!atLeastOneBlackPointFoundOnTop) {\n          up--;\n        }\n      }\n\n      if (up < 0) {\n        sizeExceeded = true;\n        break;\n      }\n\n    }\n\n    if (!sizeExceeded) {\n\n      int maxSize = right - left;\n\n      ResultPoint z = null;\n      for (int i = 1; z == null && i < maxSize; i++) {\n        z = getBlackPointOnSegment(left, down - i, left + i, down);\n      }\n\n      if (z == null) {\n        throw NotFoundException.getNotFoundInstance();\n      }\n\n      ResultPoint t = null;\n      //go down right\n      for (int i = 1; t == null && i < maxSize; i++) {\n        t = getBlackPointOnSegment(left, up + i, left + i, up);\n      }\n\n      if (t == null) {\n        throw NotFoundException.getNotFoundInstance();\n      }\n\n      ResultPoint x = null;\n      //go down left\n      for (int i = 1; x == null && i < maxSize; i++) {\n        x = getBlackPointOnSegment(right, up + i, right - i, up);\n      }\n\n      if (x == null) {\n        throw NotFoundException.getNotFoundInstance();\n      }\n\n      ResultPoint y = null;\n      //go up left\n      for (int i = 1; y == null && i < maxSize; i++) {\n        y = getBlackPointOnSegment(right, down - i, right - i, down);\n      }\n\n      if (y == null) {\n        throw NotFoundException.getNotFoundInstance();\n      }\n\n      return centerEdges(y, z, x, t);\n\n    } else {\n      throw NotFoundException.getNotFoundInstance();\n    }\n  }\n\n  private ResultPoint getBlackPointOnSegment(float aX, float aY, float bX, float bY) {\n    int dist = MathUtils.round(MathUtils.distance(aX, aY, bX, bY));\n    float xStep = (bX - aX) / dist;\n    float yStep = (bY - aY) / dist;\n\n    for (int i = 0; i < dist; i++) {\n      int x = MathUtils.round(aX + i * xStep);\n      int y = MathUtils.round(aY + i * yStep);\n      if (image.get(x, y)) {\n        return new ResultPoint(x, y);\n      }\n    }\n    return null;\n  }\n\n  /**\n   * recenters the points of a constant distance towards the center\n   *\n   * @param y bottom most point\n   * @param z left most point\n   * @param x right most point\n   * @param t top most point\n   * @return {@link ResultPoint}[] describing the corners of the rectangular\n   *         region. The first and last points are opposed on the diagonal, as\n   *         are the second and third. The first point will be the topmost\n   *         point and the last, the bottommost. The second point will be\n   *         leftmost and the third, the rightmost\n   */\n  private ResultPoint[] centerEdges(ResultPoint y, ResultPoint z,\n                                    ResultPoint x, ResultPoint t) {\n\n    //\n    //       t            t\n    //  z                      x\n    //        x    OR    z\n    //   y                    y\n    //\n\n    float yi = y.getX();\n    float yj = y.getY();\n    float zi = z.getX();\n    float zj = z.getY();\n    float xi = x.getX();\n    float xj = x.getY();\n    float ti = t.getX();\n    float tj = t.getY();\n\n    if (yi < width / 2.0f) {\n      return new ResultPoint[]{\n          new ResultPoint(ti - CORR, tj + CORR),\n          new ResultPoint(zi + CORR, zj + CORR),\n          new ResultPoint(xi - CORR, xj - CORR),\n          new ResultPoint(yi + CORR, yj - CORR)};\n    } else {\n      return new ResultPoint[]{\n          new ResultPoint(ti + CORR, tj + CORR),\n          new ResultPoint(zi + CORR, zj - CORR),\n          new ResultPoint(xi - CORR, xj + CORR),\n          new ResultPoint(yi - CORR, yj - CORR)};\n    }\n  }\n\n  /**\n   * Determines whether a segment contains a black point\n   *\n   * @param a          min value of the scanned coordinate\n   * @param b          max value of the scanned coordinate\n   * @param fixed      value of fixed coordinate\n   * @param horizontal set to true if scan must be horizontal, false if vertical\n   * @return true if a black point has been found, else false.\n   */\n  private boolean containsBlackPoint(int a, int b, int fixed, boolean horizontal) {\n\n    if (horizontal) {\n      for (int x = a; x <= b; x++) {\n        if (image.get(x, fixed)) {\n          return true;\n        }\n      }\n    } else {\n      for (int y = a; y <= b; y++) {\n        if (image.get(fixed, y)) {\n          return true;\n        }\n      }\n    }\n\n    return false;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/reedsolomon/GenericGF.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.reedsolomon;\n\n/**\n * <p>This class contains utility methods for performing mathematical operations over\n * the Galois Fields. Operations use a given primitive polynomial in calculations.</p>\n *\n * <p>Throughout this package, elements of the GF are represented as an {@code int}\n * for convenience and speed (but at the cost of memory).\n * </p>\n *\n * @author Sean Owen\n * @author David Olivier\n */\npublic final class GenericGF {\n\n  public static final GenericGF AZTEC_DATA_12 = new GenericGF(0x1069, 4096, 1); // x^12 + x^6 + x^5 + x^3 + 1\n  public static final GenericGF AZTEC_DATA_10 = new GenericGF(0x409, 1024, 1); // x^10 + x^3 + 1\n  public static final GenericGF AZTEC_DATA_6 = new GenericGF(0x43, 64, 1); // x^6 + x + 1\n  public static final GenericGF AZTEC_PARAM = new GenericGF(0x13, 16, 1); // x^4 + x + 1\n  public static final GenericGF QR_CODE_FIELD_256 = new GenericGF(0x011D, 256, 0); // x^8 + x^4 + x^3 + x^2 + 1\n  public static final GenericGF DATA_MATRIX_FIELD_256 = new GenericGF(0x012D, 256, 1); // x^8 + x^5 + x^3 + x^2 + 1\n  public static final GenericGF AZTEC_DATA_8 = DATA_MATRIX_FIELD_256;\n  public static final GenericGF MAXICODE_FIELD_64 = AZTEC_DATA_6;\n\n  private final int[] expTable;\n  private final int[] logTable;\n  private final GenericGFPoly zero;\n  private final GenericGFPoly one;\n  private final int size;\n  private final int primitive;\n  private final int generatorBase;\n\n  /**\n   * Create a representation of GF(size) using the given primitive polynomial.\n   *\n   * @param primitive irreducible polynomial whose coefficients are represented by\n   *  the bits of an int, where the least-significant bit represents the constant\n   *  coefficient\n   * @param size the size of the field\n   * @param b the factor b in the generator polynomial can be 0- or 1-based\n   *  (g(x) = (x+a^b)(x+a^(b+1))...(x+a^(b+2t-1))).\n   *  In most cases it should be 1, but for QR code it is 0.\n   */\n  public GenericGF(int primitive, int size, int b) {\n    this.primitive = primitive;\n    this.size = size;\n    this.generatorBase = b;\n\n    expTable = new int[size];\n    logTable = new int[size];\n    int x = 1;\n    for (int i = 0; i < size; i++) {\n      expTable[i] = x;\n      x *= 2; // we're assuming the generator alpha is 2\n      if (x >= size) {\n        x ^= primitive;\n        x &= size - 1;\n      }\n    }\n    for (int i = 0; i < size - 1; i++) {\n      logTable[expTable[i]] = i;\n    }\n    // logTable[0] == 0 but this should never be used\n    zero = new GenericGFPoly(this, new int[]{0});\n    one = new GenericGFPoly(this, new int[]{1});\n  }\n\n  GenericGFPoly getZero() {\n    return zero;\n  }\n\n  GenericGFPoly getOne() {\n    return one;\n  }\n\n  /**\n   * @return the monomial representing coefficient * x^degree\n   */\n  GenericGFPoly buildMonomial(int degree, int coefficient) {\n    if (degree < 0) {\n      throw new IllegalArgumentException();\n    }\n    if (coefficient == 0) {\n      return zero;\n    }\n    int[] coefficients = new int[degree + 1];\n    coefficients[0] = coefficient;\n    return new GenericGFPoly(this, coefficients);\n  }\n\n  /**\n   * Implements both addition and subtraction -- they are the same in GF(size).\n   *\n   * @return sum/difference of a and b\n   */\n  static int addOrSubtract(int a, int b) {\n    return a ^ b;\n  }\n\n  /**\n   * @return 2 to the power of a in GF(size)\n   */\n  int exp(int a) {\n    return expTable[a];\n  }\n\n  /**\n   * @return base 2 log of a in GF(size)\n   */\n  int log(int a) {\n    if (a == 0) {\n      throw new IllegalArgumentException();\n    }\n    return logTable[a];\n  }\n\n  /**\n   * @return multiplicative inverse of a\n   */\n  int inverse(int a) {\n    if (a == 0) {\n      throw new ArithmeticException();\n    }\n    return expTable[size - logTable[a] - 1];\n  }\n\n  /**\n   * @return product of a and b in GF(size)\n   */\n  int multiply(int a, int b) {\n    if (a == 0 || b == 0) {\n      return 0;\n    }\n    return expTable[(logTable[a] + logTable[b]) % (size - 1)];\n  }\n\n  public int getSize() {\n    return size;\n  }\n\n  public int getGeneratorBase() {\n    return generatorBase;\n  }\n\n  @Override\n  public String toString() {\n    return \"GF(0x\" + Integer.toHexString(primitive) + ',' + size + ')';\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/reedsolomon/GenericGFPoly.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.reedsolomon;\n\n/**\n * <p>Represents a polynomial whose coefficients are elements of a GF.\n * Instances of this class are immutable.</p>\n *\n * <p>Much credit is due to William Rucklidge since portions of this code are an indirect\n * port of his C++ Reed-Solomon implementation.</p>\n *\n * @author Sean Owen\n */\nfinal class GenericGFPoly {\n\n  private final GenericGF field;\n  private final int[] coefficients;\n\n  /**\n   * @param field the {@link GenericGF} instance representing the field to use\n   * to perform computations\n   * @param coefficients coefficients as ints representing elements of GF(size), arranged\n   * from most significant (highest-power term) coefficient to least significant\n   * @throws IllegalArgumentException if argument is null or empty,\n   * or if leading coefficient is 0 and this is not a\n   * constant polynomial (that is, it is not the monomial \"0\")\n   */\n  GenericGFPoly(GenericGF field, int[] coefficients) {\n    if (coefficients.length == 0) {\n      throw new IllegalArgumentException();\n    }\n    this.field = field;\n    int coefficientsLength = coefficients.length;\n    if (coefficientsLength > 1 && coefficients[0] == 0) {\n      // Leading term must be non-zero for anything except the constant polynomial \"0\"\n      int firstNonZero = 1;\n      while (firstNonZero < coefficientsLength && coefficients[firstNonZero] == 0) {\n        firstNonZero++;\n      }\n      if (firstNonZero == coefficientsLength) {\n        this.coefficients = new int[]{0};\n      } else {\n        this.coefficients = new int[coefficientsLength - firstNonZero];\n        System.arraycopy(coefficients,\n            firstNonZero,\n            this.coefficients,\n            0,\n            this.coefficients.length);\n      }\n    } else {\n      this.coefficients = coefficients;\n    }\n  }\n\n  int[] getCoefficients() {\n    return coefficients;\n  }\n\n  /**\n   * @return degree of this polynomial\n   */\n  int getDegree() {\n    return coefficients.length - 1;\n  }\n\n  /**\n   * @return true iff this polynomial is the monomial \"0\"\n   */\n  boolean isZero() {\n    return coefficients[0] == 0;\n  }\n\n  /**\n   * @return coefficient of x^degree term in this polynomial\n   */\n  int getCoefficient(int degree) {\n    return coefficients[coefficients.length - 1 - degree];\n  }\n\n  /**\n   * @return evaluation of this polynomial at a given point\n   */\n  int evaluateAt(int a) {\n    if (a == 0) {\n      // Just return the x^0 coefficient\n      return getCoefficient(0);\n    }\n    if (a == 1) {\n      // Just the sum of the coefficients\n      int result = 0;\n      for (int coefficient : coefficients) {\n        result = GenericGF.addOrSubtract(result, coefficient);\n      }\n      return result;\n    }\n    int result = coefficients[0];\n    int size = coefficients.length;\n    for (int i = 1; i < size; i++) {\n      result = GenericGF.addOrSubtract(field.multiply(a, result), coefficients[i]);\n    }\n    return result;\n  }\n\n  GenericGFPoly addOrSubtract(GenericGFPoly other) {\n    if (!field.equals(other.field)) {\n      throw new IllegalArgumentException(\"GenericGFPolys do not have same GenericGF field\");\n    }\n    if (isZero()) {\n      return other;\n    }\n    if (other.isZero()) {\n      return this;\n    }\n\n    int[] smallerCoefficients = this.coefficients;\n    int[] largerCoefficients = other.coefficients;\n    if (smallerCoefficients.length > largerCoefficients.length) {\n      int[] temp = smallerCoefficients;\n      smallerCoefficients = largerCoefficients;\n      largerCoefficients = temp;\n    }\n    int[] sumDiff = new int[largerCoefficients.length];\n    int lengthDiff = largerCoefficients.length - smallerCoefficients.length;\n    // Copy high-order terms only found in higher-degree polynomial's coefficients\n    System.arraycopy(largerCoefficients, 0, sumDiff, 0, lengthDiff);\n\n    for (int i = lengthDiff; i < largerCoefficients.length; i++) {\n      sumDiff[i] = GenericGF.addOrSubtract(smallerCoefficients[i - lengthDiff], largerCoefficients[i]);\n    }\n\n    return new GenericGFPoly(field, sumDiff);\n  }\n\n  GenericGFPoly multiply(GenericGFPoly other) {\n    if (!field.equals(other.field)) {\n      throw new IllegalArgumentException(\"GenericGFPolys do not have same GenericGF field\");\n    }\n    if (isZero() || other.isZero()) {\n      return field.getZero();\n    }\n    int[] aCoefficients = this.coefficients;\n    int aLength = aCoefficients.length;\n    int[] bCoefficients = other.coefficients;\n    int bLength = bCoefficients.length;\n    int[] product = new int[aLength + bLength - 1];\n    for (int i = 0; i < aLength; i++) {\n      int aCoeff = aCoefficients[i];\n      for (int j = 0; j < bLength; j++) {\n        product[i + j] = GenericGF.addOrSubtract(product[i + j],\n            field.multiply(aCoeff, bCoefficients[j]));\n      }\n    }\n    return new GenericGFPoly(field, product);\n  }\n\n  GenericGFPoly multiply(int scalar) {\n    if (scalar == 0) {\n      return field.getZero();\n    }\n    if (scalar == 1) {\n      return this;\n    }\n    int size = coefficients.length;\n    int[] product = new int[size];\n    for (int i = 0; i < size; i++) {\n      product[i] = field.multiply(coefficients[i], scalar);\n    }\n    return new GenericGFPoly(field, product);\n  }\n\n  GenericGFPoly multiplyByMonomial(int degree, int coefficient) {\n    if (degree < 0) {\n      throw new IllegalArgumentException();\n    }\n    if (coefficient == 0) {\n      return field.getZero();\n    }\n    int size = coefficients.length;\n    int[] product = new int[size + degree];\n    for (int i = 0; i < size; i++) {\n      product[i] = field.multiply(coefficients[i], coefficient);\n    }\n    return new GenericGFPoly(field, product);\n  }\n\n  GenericGFPoly[] divide(GenericGFPoly other) {\n    if (!field.equals(other.field)) {\n      throw new IllegalArgumentException(\"GenericGFPolys do not have same GenericGF field\");\n    }\n    if (other.isZero()) {\n      throw new IllegalArgumentException(\"Divide by 0\");\n    }\n\n    GenericGFPoly quotient = field.getZero();\n    GenericGFPoly remainder = this;\n\n    int denominatorLeadingTerm = other.getCoefficient(other.getDegree());\n    int inverseDenominatorLeadingTerm = field.inverse(denominatorLeadingTerm);\n\n    while (remainder.getDegree() >= other.getDegree() && !remainder.isZero()) {\n      int degreeDifference = remainder.getDegree() - other.getDegree();\n      int scale = field.multiply(remainder.getCoefficient(remainder.getDegree()), inverseDenominatorLeadingTerm);\n      GenericGFPoly term = other.multiplyByMonomial(degreeDifference, scale);\n      GenericGFPoly iterationQuotient = field.buildMonomial(degreeDifference, scale);\n      quotient = quotient.addOrSubtract(iterationQuotient);\n      remainder = remainder.addOrSubtract(term);\n    }\n\n    return new GenericGFPoly[] { quotient, remainder };\n  }\n\n  @Override\n  public String toString() {\n    if (isZero()) {\n      return \"0\";\n    }\n    StringBuilder result = new StringBuilder(8 * getDegree());\n    for (int degree = getDegree(); degree >= 0; degree--) {\n      int coefficient = getCoefficient(degree);\n      if (coefficient != 0) {\n        if (coefficient < 0) {\n          if (degree == getDegree()) {\n            result.append(\"-\");\n          } else {\n            result.append(\" - \");\n          }\n          coefficient = -coefficient;\n        } else {\n          if (result.length() > 0) {\n            result.append(\" + \");\n          }\n        }\n        if (degree == 0 || coefficient != 1) {\n          int alphaPower = field.log(coefficient);\n          if (alphaPower == 0) {\n            result.append('1');\n          } else if (alphaPower == 1) {\n            result.append('a');\n          } else {\n            result.append(\"a^\");\n            result.append(alphaPower);\n          }\n        }\n        if (degree != 0) {\n          if (degree == 1) {\n            result.append('x');\n          } else {\n            result.append(\"x^\");\n            result.append(degree);\n          }\n        }\n      }\n    }\n    return result.toString();\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/reedsolomon/ReedSolomonDecoder.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.reedsolomon;\n\n/**\n * <p>Implements Reed-Solomon decoding, as the name implies.</p>\n *\n * <p>The algorithm will not be explained here, but the following references were helpful\n * in creating this implementation:</p>\n *\n * <ul>\n * <li>Bruce Maggs.\n * <a href=\"http://www.cs.cmu.edu/afs/cs.cmu.edu/project/pscico-guyb/realworld/www/rs_decode.ps\">\n * \"Decoding Reed-Solomon Codes\"</a> (see discussion of Forney's Formula)</li>\n * <li>J.I. Hall. <a href=\"www.mth.msu.edu/~jhall/classes/codenotes/GRS.pdf\">\n * \"Chapter 5. Generalized Reed-Solomon Codes\"</a>\n * (see discussion of Euclidean algorithm)</li>\n * </ul>\n *\n * <p>Much credit is due to William Rucklidge since portions of this code are an indirect\n * port of his C++ Reed-Solomon implementation.</p>\n *\n * @author Sean Owen\n * @author William Rucklidge\n * @author sanfordsquires\n */\npublic final class ReedSolomonDecoder {\n\n  private final GenericGF field;\n\n  public ReedSolomonDecoder(GenericGF field) {\n    this.field = field;\n  }\n\n  /**\n   * <p>Decodes given set of received codewords, which include both data and error-correction\n   * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place,\n   * in the input.</p>\n   *\n   * @param received data and error-correction codewords\n   * @param twoS number of error-correction codewords available\n   * @throws ReedSolomonException if decoding fails for any reason\n   */\n  public void decode(int[] received, int twoS) throws ReedSolomonException {\n    GenericGFPoly poly = new GenericGFPoly(field, received);\n    int[] syndromeCoefficients = new int[twoS];\n    boolean noError = true;\n    for (int i = 0; i < twoS; i++) {\n      int eval = poly.evaluateAt(field.exp(i + field.getGeneratorBase()));\n      syndromeCoefficients[syndromeCoefficients.length - 1 - i] = eval;\n      if (eval != 0) {\n        noError = false;\n      }\n    }\n    if (noError) {\n      return;\n    }\n    GenericGFPoly syndrome = new GenericGFPoly(field, syndromeCoefficients);\n    GenericGFPoly[] sigmaOmega =\n        runEuclideanAlgorithm(field.buildMonomial(twoS, 1), syndrome, twoS);\n    GenericGFPoly sigma = sigmaOmega[0];\n    GenericGFPoly omega = sigmaOmega[1];\n    int[] errorLocations = findErrorLocations(sigma);\n    int[] errorMagnitudes = findErrorMagnitudes(omega, errorLocations);\n    for (int i = 0; i < errorLocations.length; i++) {\n      int position = received.length - 1 - field.log(errorLocations[i]);\n      if (position < 0) {\n        throw new ReedSolomonException(\"Bad error location\");\n      }\n      received[position] = GenericGF.addOrSubtract(received[position], errorMagnitudes[i]);\n    }\n  }\n\n  private GenericGFPoly[] runEuclideanAlgorithm(GenericGFPoly a, GenericGFPoly b, int R)\n      throws ReedSolomonException {\n    // Assume a's degree is >= b's\n    if (a.getDegree() < b.getDegree()) {\n      GenericGFPoly temp = a;\n      a = b;\n      b = temp;\n    }\n\n    GenericGFPoly rLast = a;\n    GenericGFPoly r = b;\n    GenericGFPoly tLast = field.getZero();\n    GenericGFPoly t = field.getOne();\n\n    // Run Euclidean algorithm until r's degree is less than R/2\n    while (r.getDegree() >= R / 2) {\n      GenericGFPoly rLastLast = rLast;\n      GenericGFPoly tLastLast = tLast;\n      rLast = r;\n      tLast = t;\n\n      // Divide rLastLast by rLast, with quotient in q and remainder in r\n      if (rLast.isZero()) {\n        // Oops, Euclidean algorithm already terminated?\n        throw new ReedSolomonException(\"r_{i-1} was zero\");\n      }\n      r = rLastLast;\n      GenericGFPoly q = field.getZero();\n      int denominatorLeadingTerm = rLast.getCoefficient(rLast.getDegree());\n      int dltInverse = field.inverse(denominatorLeadingTerm);\n      while (r.getDegree() >= rLast.getDegree() && !r.isZero()) {\n        int degreeDiff = r.getDegree() - rLast.getDegree();\n        int scale = field.multiply(r.getCoefficient(r.getDegree()), dltInverse);\n        q = q.addOrSubtract(field.buildMonomial(degreeDiff, scale));\n        r = r.addOrSubtract(rLast.multiplyByMonomial(degreeDiff, scale));\n      }\n\n      t = q.multiply(tLast).addOrSubtract(tLastLast);\n\n      if (r.getDegree() >= rLast.getDegree()) {\n        throw new IllegalStateException(\"Division algorithm failed to reduce polynomial?\");\n      }\n    }\n\n    int sigmaTildeAtZero = t.getCoefficient(0);\n    if (sigmaTildeAtZero == 0) {\n      throw new ReedSolomonException(\"sigmaTilde(0) was zero\");\n    }\n\n    int inverse = field.inverse(sigmaTildeAtZero);\n    GenericGFPoly sigma = t.multiply(inverse);\n    GenericGFPoly omega = r.multiply(inverse);\n    return new GenericGFPoly[]{sigma, omega};\n  }\n\n  private int[] findErrorLocations(GenericGFPoly errorLocator) throws ReedSolomonException {\n    // This is a direct application of Chien's search\n    int numErrors = errorLocator.getDegree();\n    if (numErrors == 1) { // shortcut\n      return new int[] { errorLocator.getCoefficient(1) };\n    }\n    int[] result = new int[numErrors];\n    int e = 0;\n    for (int i = 1; i < field.getSize() && e < numErrors; i++) {\n      if (errorLocator.evaluateAt(i) == 0) {\n        result[e] = field.inverse(i);\n        e++;\n      }\n    }\n    if (e != numErrors) {\n      throw new ReedSolomonException(\"Error locator degree does not match number of roots\");\n    }\n    return result;\n  }\n\n  private int[] findErrorMagnitudes(GenericGFPoly errorEvaluator, int[] errorLocations) {\n    // This is directly applying Forney's Formula\n    int s = errorLocations.length;\n    int[] result = new int[s];\n    for (int i = 0; i < s; i++) {\n      int xiInverse = field.inverse(errorLocations[i]);\n      int denominator = 1;\n      for (int j = 0; j < s; j++) {\n        if (i != j) {\n          //denominator = field.multiply(denominator,\n          //    GenericGF.addOrSubtract(1, field.multiply(errorLocations[j], xiInverse)));\n          // Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug.\n          // Below is a funny-looking workaround from Steven Parkes\n          int term = field.multiply(errorLocations[j], xiInverse);\n          int termPlus1 = (term & 0x1) == 0 ? term | 1 : term & ~1;\n          denominator = field.multiply(denominator, termPlus1);\n        }\n      }\n      result[i] = field.multiply(errorEvaluator.evaluateAt(xiInverse),\n          field.inverse(denominator));\n      if (field.getGeneratorBase() != 0) {\n        result[i] = field.multiply(result[i], xiInverse);\n      }\n    }\n    return result;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/reedsolomon/ReedSolomonEncoder.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.reedsolomon;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * <p>Implements Reed-Solomon encoding, as the name implies.</p>\n *\n * @author Sean Owen\n * @author William Rucklidge\n */\npublic final class ReedSolomonEncoder {\n\n  private final GenericGF field;\n  private final List<GenericGFPoly> cachedGenerators;\n\n  public ReedSolomonEncoder(GenericGF field) {\n    this.field = field;\n    this.cachedGenerators = new ArrayList<>();\n    cachedGenerators.add(new GenericGFPoly(field, new int[]{1}));\n  }\n\n  private GenericGFPoly buildGenerator(int degree) {\n    if (degree >= cachedGenerators.size()) {\n      GenericGFPoly lastGenerator = cachedGenerators.get(cachedGenerators.size() - 1);\n      for (int d = cachedGenerators.size(); d <= degree; d++) {\n        GenericGFPoly nextGenerator = lastGenerator.multiply(\n            new GenericGFPoly(field, new int[] { 1, field.exp(d - 1 + field.getGeneratorBase()) }));\n        cachedGenerators.add(nextGenerator);\n        lastGenerator = nextGenerator;\n      }\n    }\n    return cachedGenerators.get(degree);\n  }\n\n  public void encode(int[] toEncode, int ecBytes) {\n    if (ecBytes == 0) {\n      throw new IllegalArgumentException(\"No error correction bytes\");\n    }\n    int dataBytes = toEncode.length - ecBytes;\n    if (dataBytes <= 0) {\n      throw new IllegalArgumentException(\"No data bytes provided\");\n    }\n    GenericGFPoly generator = buildGenerator(ecBytes);\n    int[] infoCoefficients = new int[dataBytes];\n    System.arraycopy(toEncode, 0, infoCoefficients, 0, dataBytes);\n    GenericGFPoly info = new GenericGFPoly(field, infoCoefficients);\n    info = info.multiplyByMonomial(ecBytes, 1);\n    GenericGFPoly remainder = info.divide(generator)[1];\n    int[] coefficients = remainder.getCoefficients();\n    int numZeroCoefficients = ecBytes - coefficients.length;\n    for (int i = 0; i < numZeroCoefficients; i++) {\n      toEncode[dataBytes + i] = 0;\n    }\n    System.arraycopy(coefficients, 0, toEncode, dataBytes + numZeroCoefficients, coefficients.length);\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/common/reedsolomon/ReedSolomonException.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.common.reedsolomon;\n\n/**\n * <p>Thrown when an exception occurs during Reed-Solomon decoding, such as when\n * there are too many errors to correct.</p>\n *\n * @author Sean Owen\n */\npublic final class ReedSolomonException extends Exception {\n\n  public ReedSolomonException(String message) {\n    super(message);\n  }\n\n}"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/QRCodeReader.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode;\n\nimport peergos.shared.zxing.BarcodeFormat;\nimport peergos.shared.zxing.BinaryBitmap;\nimport peergos.shared.zxing.ChecksumException;\nimport peergos.shared.zxing.DecodeHintType;\nimport peergos.shared.zxing.FormatException;\nimport peergos.shared.zxing.NotFoundException;\nimport peergos.shared.zxing.Reader;\nimport peergos.shared.zxing.Result;\nimport peergos.shared.zxing.ResultMetadataType;\nimport peergos.shared.zxing.ResultPoint;\nimport peergos.shared.zxing.common.BitMatrix;\nimport peergos.shared.zxing.common.DecoderResult;\nimport peergos.shared.zxing.common.DetectorResult;\nimport peergos.shared.zxing.qrcode.decoder.Decoder;\nimport peergos.shared.zxing.qrcode.decoder.QRCodeDecoderMetaData;\nimport peergos.shared.zxing.qrcode.detector.Detector;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * This implementation can detect and decode QR Codes in an image.\n *\n * @author Sean Owen\n */\npublic class QRCodeReader implements Reader {\n\n  private static final ResultPoint[] NO_POINTS = new ResultPoint[0];\n\n  private final Decoder decoder = new Decoder();\n\n  protected final Decoder getDecoder() {\n    return decoder;\n  }\n\n  /**\n   * Locates and decodes a QR code in an image.\n   *\n   * @return a String representing the content encoded by the QR code\n   * @throws NotFoundException if a QR code cannot be found\n   * @throws FormatException if a QR code cannot be decoded\n   * @throws ChecksumException if error correction fails\n   */\n  @Override\n  public Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException {\n    return decode(image, null);\n  }\n\n  @Override\n  public final Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints)\n      throws NotFoundException, ChecksumException, FormatException {\n    DecoderResult decoderResult;\n    ResultPoint[] points;\n    if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) {\n      BitMatrix bits = extractPureBits(image.getBlackMatrix());\n      decoderResult = decoder.decode(bits, hints);\n      points = NO_POINTS;\n    } else {\n      DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints);\n      decoderResult = decoder.decode(detectorResult.getBits(), hints);\n      points = detectorResult.getPoints();\n    }\n\n    // If the code was mirrored: swap the bottom-left and the top-right points.\n    if (decoderResult.getOther() instanceof QRCodeDecoderMetaData) {\n      ((QRCodeDecoderMetaData) decoderResult.getOther()).applyMirroredCorrection(points);\n    }\n\n    Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.QR_CODE);\n    List<byte[]> byteSegments = decoderResult.getByteSegments();\n    if (byteSegments != null) {\n      result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments);\n    }\n    String ecLevel = decoderResult.getECLevel();\n    if (ecLevel != null) {\n      result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel);\n    }\n    if (decoderResult.hasStructuredAppend()) {\n      result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE,\n                         decoderResult.getStructuredAppendSequenceNumber());\n      result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY,\n                         decoderResult.getStructuredAppendParity());\n    }\n    return result;\n  }\n\n  @Override\n  public void reset() {\n    // do nothing\n  }\n\n  /**\n   * This method detects a code in a \"pure\" image -- that is, pure monochrome image\n   * which contains only an unrotated, unskewed, image of a code, with some white border\n   * around it. This is a specialized method that works exceptionally fast in this special\n   * case.\n   */\n  private static BitMatrix extractPureBits(BitMatrix image) throws NotFoundException {\n\n    int[] leftTopBlack = image.getTopLeftOnBit();\n    int[] rightBottomBlack = image.getBottomRightOnBit();\n    if (leftTopBlack == null || rightBottomBlack == null) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n\n    float moduleSize = moduleSize(leftTopBlack, image);\n\n    int top = leftTopBlack[1];\n    int bottom = rightBottomBlack[1];\n    int left = leftTopBlack[0];\n    int right = rightBottomBlack[0];\n\n    // Sanity check!\n    if (left >= right || top >= bottom) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n\n    if (bottom - top != right - left) {\n      // Special case, where bottom-right module wasn't black so we found something else in the last row\n      // Assume it's a square, so use height as the width\n      right = left + (bottom - top);\n      if (right >= image.getWidth()) {\n        // Abort if that would not make sense -- off image\n        throw NotFoundException.getNotFoundInstance();\n      }\n    }\n\n    int matrixWidth = Math.round((right - left + 1) / moduleSize);\n    int matrixHeight = Math.round((bottom - top + 1) / moduleSize);\n    if (matrixWidth <= 0 || matrixHeight <= 0) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n    if (matrixHeight != matrixWidth) {\n      // Only possibly decode square regions\n      throw NotFoundException.getNotFoundInstance();\n    }\n\n    // Push in the \"border\" by half the module width so that we start\n    // sampling in the middle of the module. Just in case the image is a\n    // little off, this will help recover.\n    int nudge = (int) (moduleSize / 2.0f);\n    top += nudge;\n    left += nudge;\n\n    // But careful that this does not sample off the edge\n    // \"right\" is the farthest-right valid pixel location -- right+1 is not necessarily\n    // This is positive by how much the inner x loop below would be too large\n    int nudgedTooFarRight = left + (int) ((matrixWidth - 1) * moduleSize) - right;\n    if (nudgedTooFarRight > 0) {\n      if (nudgedTooFarRight > nudge) {\n        // Neither way fits; abort\n        throw NotFoundException.getNotFoundInstance();\n      }\n      left -= nudgedTooFarRight;\n    }\n    // See logic above\n    int nudgedTooFarDown = top + (int) ((matrixHeight - 1) * moduleSize) - bottom;\n    if (nudgedTooFarDown > 0) {\n      if (nudgedTooFarDown > nudge) {\n        // Neither way fits; abort\n        throw NotFoundException.getNotFoundInstance();\n      }\n      top -= nudgedTooFarDown;\n    }\n\n    // Now just read off the bits\n    BitMatrix bits = new BitMatrix(matrixWidth, matrixHeight);\n    for (int y = 0; y < matrixHeight; y++) {\n      int iOffset = top + (int) (y * moduleSize);\n      for (int x = 0; x < matrixWidth; x++) {\n        if (image.get(left + (int) (x * moduleSize), iOffset)) {\n          bits.set(x, y);\n        }\n      }\n    }\n    return bits;\n  }\n\n  private static float moduleSize(int[] leftTopBlack, BitMatrix image) throws NotFoundException {\n    int height = image.getHeight();\n    int width = image.getWidth();\n    int x = leftTopBlack[0];\n    int y = leftTopBlack[1];\n    boolean inBlack = true;\n    int transitions = 0;\n    while (x < width && y < height) {\n      if (inBlack != image.get(x, y)) {\n        if (++transitions == 5) {\n          break;\n        }\n        inBlack = !inBlack;\n      }\n      x++;\n      y++;\n    }\n    if (x == width || y == height) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n    return (x - leftTopBlack[0]) / 7.0f;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/QRCodeWriter.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode;\n\nimport peergos.shared.zxing.BarcodeFormat;\nimport peergos.shared.zxing.EncodeHintType;\nimport peergos.shared.zxing.Writer;\nimport peergos.shared.zxing.WriterException;\nimport peergos.shared.zxing.common.BitMatrix;\nimport peergos.shared.zxing.qrcode.encoder.ByteMatrix;\nimport peergos.shared.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport peergos.shared.zxing.qrcode.encoder.Encoder;\nimport peergos.shared.zxing.qrcode.encoder.QRCode;\n\nimport java.util.Map;\n\n/**\n * This object renders a QR Code as a BitMatrix 2D array of greyscale values.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic final class QRCodeWriter implements Writer {\n\n  private static final int QUIET_ZONE_SIZE = 4;\n\n  @Override\n  public BitMatrix encode(String contents, BarcodeFormat format, int width, int height)\n      throws WriterException {\n\n    return encode(contents, format, width, height, null);\n  }\n\n  @Override\n  public BitMatrix encode(String contents,\n                          BarcodeFormat format,\n                          int width,\n                          int height,\n                          Map<EncodeHintType,?> hints) throws WriterException {\n\n    if (contents.isEmpty()) {\n      throw new IllegalArgumentException(\"Found empty contents\");\n    }\n\n    if (format != BarcodeFormat.QR_CODE) {\n      throw new IllegalArgumentException(\"Can only encode QR_CODE, but got \" + format);\n    }\n\n    if (width < 0 || height < 0) {\n      throw new IllegalArgumentException(\"Requested dimensions are too small: \" + width + 'x' +\n          height);\n    }\n\n    ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L;\n    int quietZone = QUIET_ZONE_SIZE;\n    if (hints != null) {\n      if (hints.containsKey(EncodeHintType.ERROR_CORRECTION)) {\n        errorCorrectionLevel = ErrorCorrectionLevel.valueOf(hints.get(EncodeHintType.ERROR_CORRECTION).toString());\n      }\n      if (hints.containsKey(EncodeHintType.MARGIN)) {\n        quietZone = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString());\n      }\n    }\n\n    QRCode code = Encoder.encode(contents, errorCorrectionLevel, hints);\n    return renderResult(code, width, height, quietZone);\n  }\n\n  // Note that the input matrix uses 0 == white, 1 == black, while the output matrix uses\n  // 0 == black, 255 == white (i.e. an 8 bit greyscale bitmap).\n  private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) {\n    ByteMatrix input = code.getMatrix();\n    if (input == null) {\n      throw new IllegalStateException();\n    }\n    int inputWidth = input.getWidth();\n    int inputHeight = input.getHeight();\n    int qrWidth = inputWidth + (quietZone * 2);\n    int qrHeight = inputHeight + (quietZone * 2);\n    int outputWidth = Math.max(width, qrWidth);\n    int outputHeight = Math.max(height, qrHeight);\n\n    int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight);\n    // Padding includes both the quiet zone and the extra white pixels to accommodate the requested\n    // dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone.\n    // If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will\n    // handle all the padding from 100x100 (the actual QR) up to 200x160.\n    int leftPadding = (outputWidth - (inputWidth * multiple)) / 2;\n    int topPadding = (outputHeight - (inputHeight * multiple)) / 2;\n\n    BitMatrix output = new BitMatrix(outputWidth, outputHeight);\n\n    for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) {\n      // Write the contents of this row of the barcode\n      for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) {\n        if (input.get(inputX, inputY) == 1) {\n          output.setRegion(outputX, outputY, multiple, multiple);\n        }\n      }\n    }\n\n    return output;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/BitMatrixParser.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\nimport peergos.shared.zxing.FormatException;\nimport peergos.shared.zxing.common.BitMatrix;\n\n/**\n * @author Sean Owen\n */\nfinal class BitMatrixParser {\n\n  private final BitMatrix bitMatrix;\n  private Version parsedVersion;\n  private FormatInformation parsedFormatInfo;\n  private boolean mirror;\n\n  /**\n   * @param bitMatrix {@link BitMatrix} to parse\n   * @throws FormatException if dimension is not >= 21 and 1 mod 4\n   */\n  BitMatrixParser(BitMatrix bitMatrix) throws FormatException {\n    int dimension = bitMatrix.getHeight();\n    if (dimension < 21 || (dimension & 0x03) != 1) {\n      throw FormatException.getFormatInstance();\n    }\n    this.bitMatrix = bitMatrix;\n  }\n\n  /**\n   * <p>Reads format information from one of its two locations within the QR Code.</p>\n   *\n   * @return {@link FormatInformation} encapsulating the QR Code's format info\n   * @throws FormatException if both format information locations cannot be parsed as\n   * the valid encoding of format information\n   */\n  FormatInformation readFormatInformation() throws FormatException {\n\n    if (parsedFormatInfo != null) {\n      return parsedFormatInfo;\n    }\n\n    // Read top-left format info bits\n    int formatInfoBits1 = 0;\n    for (int i = 0; i < 6; i++) {\n      formatInfoBits1 = copyBit(i, 8, formatInfoBits1);\n    }\n    // .. and skip a bit in the timing pattern ...\n    formatInfoBits1 = copyBit(7, 8, formatInfoBits1);\n    formatInfoBits1 = copyBit(8, 8, formatInfoBits1);\n    formatInfoBits1 = copyBit(8, 7, formatInfoBits1);\n    // .. and skip a bit in the timing pattern ...\n    for (int j = 5; j >= 0; j--) {\n      formatInfoBits1 = copyBit(8, j, formatInfoBits1);\n    }\n\n    // Read the top-right/bottom-left pattern too\n    int dimension = bitMatrix.getHeight();\n    int formatInfoBits2 = 0;\n    int jMin = dimension - 7;\n    for (int j = dimension - 1; j >= jMin; j--) {\n      formatInfoBits2 = copyBit(8, j, formatInfoBits2);\n    }\n    for (int i = dimension - 8; i < dimension; i++) {\n      formatInfoBits2 = copyBit(i, 8, formatInfoBits2);\n    }\n\n    parsedFormatInfo = FormatInformation.decodeFormatInformation(formatInfoBits1, formatInfoBits2);\n    if (parsedFormatInfo != null) {\n      return parsedFormatInfo;\n    }\n    throw FormatException.getFormatInstance();\n  }\n\n  /**\n   * <p>Reads version information from one of its two locations within the QR Code.</p>\n   *\n   * @return {@link Version} encapsulating the QR Code's version\n   * @throws FormatException if both version information locations cannot be parsed as\n   * the valid encoding of version information\n   */\n  Version readVersion() throws FormatException {\n\n    if (parsedVersion != null) {\n      return parsedVersion;\n    }\n\n    int dimension = bitMatrix.getHeight();\n\n    int provisionalVersion = (dimension - 17) / 4;\n    if (provisionalVersion <= 6) {\n      return Version.getVersionForNumber(provisionalVersion);\n    }\n\n    // Read top-right version info: 3 wide by 6 tall\n    int versionBits = 0;\n    int ijMin = dimension - 11;\n    for (int j = 5; j >= 0; j--) {\n      for (int i = dimension - 9; i >= ijMin; i--) {\n        versionBits = copyBit(i, j, versionBits);\n      }\n    }\n\n    Version theParsedVersion = Version.decodeVersionInformation(versionBits);\n    if (theParsedVersion != null && theParsedVersion.getDimensionForVersion() == dimension) {\n      parsedVersion = theParsedVersion;\n      return theParsedVersion;\n    }\n\n    // Hmm, failed. Try bottom left: 6 wide by 3 tall\n    versionBits = 0;\n    for (int i = 5; i >= 0; i--) {\n      for (int j = dimension - 9; j >= ijMin; j--) {\n        versionBits = copyBit(i, j, versionBits);\n      }\n    }\n\n    theParsedVersion = Version.decodeVersionInformation(versionBits);\n    if (theParsedVersion != null && theParsedVersion.getDimensionForVersion() == dimension) {\n      parsedVersion = theParsedVersion;\n      return theParsedVersion;\n    }\n    throw FormatException.getFormatInstance();\n  }\n\n  private int copyBit(int i, int j, int versionBits) {\n    boolean bit = mirror ? bitMatrix.get(j, i) : bitMatrix.get(i, j);\n    return bit ? (versionBits << 1) | 0x1 : versionBits << 1;\n  }\n\n  /**\n   * <p>Reads the bits in the {@link BitMatrix} representing the finder pattern in the\n   * correct order in order to reconstruct the codewords bytes contained within the\n   * QR Code.</p>\n   *\n   * @return bytes encoded within the QR Code\n   * @throws FormatException if the exact number of bytes expected is not read\n   */\n  byte[] readCodewords() throws FormatException {\n\n    FormatInformation formatInfo = readFormatInformation();\n    Version version = readVersion();\n\n    // Get the data mask for the format used in this QR Code. This will exclude\n    // some bits from reading as we wind through the bit matrix.\n    DataMask dataMask = DataMask.values()[formatInfo.getDataMask()];\n    int dimension = bitMatrix.getHeight();\n    dataMask.unmaskBitMatrix(bitMatrix, dimension);\n\n    BitMatrix functionPattern = version.buildFunctionPattern();\n\n    boolean readingUp = true;\n    byte[] result = new byte[version.getTotalCodewords()];\n    int resultOffset = 0;\n    int currentByte = 0;\n    int bitsRead = 0;\n    // Read columns in pairs, from right to left\n    for (int j = dimension - 1; j > 0; j -= 2) {\n      if (j == 6) {\n        // Skip whole column with vertical alignment pattern;\n        // saves time and makes the other code proceed more cleanly\n        j--;\n      }\n      // Read alternatingly from bottom to top then top to bottom\n      for (int count = 0; count < dimension; count++) {\n        int i = readingUp ? dimension - 1 - count : count;\n        for (int col = 0; col < 2; col++) {\n          // Ignore bits covered by the function pattern\n          if (!functionPattern.get(j - col, i)) {\n            // Read a bit\n            bitsRead++;\n            currentByte <<= 1;\n            if (bitMatrix.get(j - col, i)) {\n              currentByte |= 1;\n            }\n            // If we've made a whole byte, save it off\n            if (bitsRead == 8) {\n              result[resultOffset++] = (byte) currentByte;\n              bitsRead = 0;\n              currentByte = 0;\n            }\n          }\n        }\n      }\n      readingUp ^= true; // readingUp = !readingUp; // switch directions\n    }\n    if (resultOffset != version.getTotalCodewords()) {\n      throw FormatException.getFormatInstance();\n    }\n    return result;\n  }\n\n  /**\n   * Revert the mask removal done while reading the code words. The bit matrix should revert to its original state.\n   */\n  void remask() {\n    if (parsedFormatInfo == null) {\n      return; // We have no format information, and have no data mask\n    }\n    DataMask dataMask = DataMask.values()[parsedFormatInfo.getDataMask()];\n    int dimension = bitMatrix.getHeight();\n    dataMask.unmaskBitMatrix(bitMatrix, dimension);\n  }\n\n  /**\n   * Prepare the parser for a mirrored operation.\n   * This flag has effect only on the {@link #readFormatInformation()} and the\n   * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the\n   * {@link #mirror()} method should be called.\n   *\n   * @param mirror Whether to read version and format information mirrored.\n   */\n  void setMirror(boolean mirror) {\n    parsedVersion = null;\n    parsedFormatInfo = null;\n    this.mirror = mirror;\n  }\n\n  /** Mirror the bit matrix in order to attempt a second reading. */\n  void mirror() {\n    for (int x = 0; x < bitMatrix.getWidth(); x++) {\n      for (int y = x + 1; y < bitMatrix.getHeight(); y++) {\n        if (bitMatrix.get(x, y) != bitMatrix.get(y, x)) {\n          bitMatrix.flip(y, x);\n          bitMatrix.flip(x, y);\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/DataBlock.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\n/**\n * <p>Encapsulates a block of data within a QR Code. QR Codes may split their data into\n * multiple blocks, each of which is a unit of data and error-correction codewords. Each\n * is represented by an instance of this class.</p>\n *\n * @author Sean Owen\n */\nfinal class DataBlock {\n\n  private final int numDataCodewords;\n  private final byte[] codewords;\n\n  private DataBlock(int numDataCodewords, byte[] codewords) {\n    this.numDataCodewords = numDataCodewords;\n    this.codewords = codewords;\n  }\n\n  /**\n   * <p>When QR Codes use multiple data blocks, they are actually interleaved.\n   * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This\n   * method will separate the data into original blocks.</p>\n   *\n   * @param rawCodewords bytes as read directly from the QR Code\n   * @param version version of the QR Code\n   * @param ecLevel error-correction level of the QR Code\n   * @return DataBlocks containing original bytes, \"de-interleaved\" from representation in the\n   *         QR Code\n   */\n  static DataBlock[] getDataBlocks(byte[] rawCodewords,\n                                   Version version,\n                                   ErrorCorrectionLevel ecLevel) {\n\n    if (rawCodewords.length != version.getTotalCodewords()) {\n      throw new IllegalArgumentException();\n    }\n\n    // Figure out the number and size of data blocks used by this version and\n    // error correction level\n    Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel);\n\n    // First count the total number of data blocks\n    int totalBlocks = 0;\n    Version.ECB[] ecBlockArray = ecBlocks.getECBlocks();\n    for (Version.ECB ecBlock : ecBlockArray) {\n      totalBlocks += ecBlock.getCount();\n    }\n\n    // Now establish DataBlocks of the appropriate size and number of data codewords\n    DataBlock[] result = new DataBlock[totalBlocks];\n    int numResultBlocks = 0;\n    for (Version.ECB ecBlock : ecBlockArray) {\n      for (int i = 0; i < ecBlock.getCount(); i++) {\n        int numDataCodewords = ecBlock.getDataCodewords();\n        int numBlockCodewords = ecBlocks.getECCodewordsPerBlock() + numDataCodewords;\n        result[numResultBlocks++] = new DataBlock(numDataCodewords, new byte[numBlockCodewords]);\n      }\n    }\n\n    // All blocks have the same amount of data, except that the last n\n    // (where n may be 0) have 1 more byte. Figure out where these start.\n    int shorterBlocksTotalCodewords = result[0].codewords.length;\n    int longerBlocksStartAt = result.length - 1;\n    while (longerBlocksStartAt >= 0) {\n      int numCodewords = result[longerBlocksStartAt].codewords.length;\n      if (numCodewords == shorterBlocksTotalCodewords) {\n        break;\n      }\n      longerBlocksStartAt--;\n    }\n    longerBlocksStartAt++;\n\n    int shorterBlocksNumDataCodewords = shorterBlocksTotalCodewords - ecBlocks.getECCodewordsPerBlock();\n    // The last elements of result may be 1 element longer;\n    // first fill out as many elements as all of them have\n    int rawCodewordsOffset = 0;\n    for (int i = 0; i < shorterBlocksNumDataCodewords; i++) {\n      for (int j = 0; j < numResultBlocks; j++) {\n        result[j].codewords[i] = rawCodewords[rawCodewordsOffset++];\n      }\n    }\n    // Fill out the last data block in the longer ones\n    for (int j = longerBlocksStartAt; j < numResultBlocks; j++) {\n      result[j].codewords[shorterBlocksNumDataCodewords] = rawCodewords[rawCodewordsOffset++];\n    }\n    // Now add in error correction blocks\n    int max = result[0].codewords.length;\n    for (int i = shorterBlocksNumDataCodewords; i < max; i++) {\n      for (int j = 0; j < numResultBlocks; j++) {\n        int iOffset = j < longerBlocksStartAt ? i : i + 1;\n        result[j].codewords[iOffset] = rawCodewords[rawCodewordsOffset++];\n      }\n    }\n    return result;\n  }\n\n  int getNumDataCodewords() {\n    return numDataCodewords;\n  }\n\n  byte[] getCodewords() {\n    return codewords;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/DataMask.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\nimport peergos.shared.zxing.common.BitMatrix;\n\n/**\n * <p>Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations\n * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix,\n * including areas used for finder patterns, timing patterns, etc. These areas should be unused\n * after the point they are unmasked anyway.</p>\n *\n * <p>Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position\n * and j is row position. In fact, as the text says, i is row position and j is column position.</p>\n *\n * @author Sean Owen\n */\nenum DataMask {\n\n  // See ISO 18004:2006 6.8.1\n\n  /**\n   * 000: mask bits for which (x + y) mod 2 == 0\n   */\n  DATA_MASK_000() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return ((i + j) & 0x01) == 0;\n    }\n  },\n\n  /**\n   * 001: mask bits for which x mod 2 == 0\n   */\n  DATA_MASK_001() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return (i & 0x01) == 0;\n    }\n  },\n\n  /**\n   * 010: mask bits for which y mod 3 == 0\n   */\n  DATA_MASK_010() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return j % 3 == 0;\n    }\n  },\n\n  /**\n   * 011: mask bits for which (x + y) mod 3 == 0\n   */\n  DATA_MASK_011() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return (i + j) % 3 == 0;\n    }\n  },\n\n  /**\n   * 100: mask bits for which (x/2 + y/3) mod 2 == 0\n   */\n  DATA_MASK_100() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return (((i / 2) + (j / 3)) & 0x01) == 0;\n    }\n  },\n\n  /**\n   * 101: mask bits for which xy mod 2 + xy mod 3 == 0\n   * equivalently, such that xy mod 6 == 0\n   */\n  DATA_MASK_101() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return (i * j) % 6 == 0;\n    }\n  },\n\n  /**\n   * 110: mask bits for which (xy mod 2 + xy mod 3) mod 2 == 0\n   * equivalently, such that xy mod 6 < 3\n   */\n  DATA_MASK_110() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return ((i * j) % 6) < 3;\n    }\n  },\n\n  /**\n   * 111: mask bits for which ((x+y)mod 2 + xy mod 3) mod 2 == 0\n   * equivalently, such that (x + y + xy mod 3) mod 2 == 0\n   */\n  DATA_MASK_111() {\n    @Override\n    boolean isMasked(int i, int j) {\n      return ((i + j + ((i * j) % 3)) & 0x01) == 0;\n    }\n  };\n\n  // End of enum constants.\n\n\n  /**\n   * <p>Implementations of this method reverse the data masking process applied to a QR Code and\n   * make its bits ready to read.</p>\n   *\n   * @param bits representation of QR Code bits\n   * @param dimension dimension of QR Code, represented by bits, being unmasked\n   */\n  final void unmaskBitMatrix(BitMatrix bits, int dimension) {\n    for (int i = 0; i < dimension; i++) {\n      for (int j = 0; j < dimension; j++) {\n        if (isMasked(i, j)) {\n          bits.flip(j, i);\n        }\n      }\n    }\n  }\n\n  abstract boolean isMasked(int i, int j);\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/DecodedBitStreamParser.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\nimport peergos.shared.zxing.DecodeHintType;\nimport peergos.shared.zxing.FormatException;\nimport peergos.shared.zxing.common.BitSource;\nimport peergos.shared.zxing.common.CharacterSetECI;\nimport peergos.shared.zxing.common.DecoderResult;\nimport peergos.shared.zxing.common.StringUtils;\n\nimport java.io.UnsupportedEncodingException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * <p>QR Codes can encode text as bits in one of several modes, and can use multiple modes\n * in one QR Code. This class decodes the bits back into text.</p>\n *\n * <p>See ISO 18004:2006, 6.4.3 - 6.4.7</p>\n *\n * @author Sean Owen\n */\nfinal class DecodedBitStreamParser {\n\n  /**\n   * See ISO 18004:2006, 6.4.4 Table 5\n   */\n  private static final char[] ALPHANUMERIC_CHARS =\n      \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:\".toCharArray();\n  private static final int GB2312_SUBSET = 1;\n\n  private DecodedBitStreamParser() {\n  }\n\n  static DecoderResult decode(byte[] bytes,\n                              Version version,\n                              ErrorCorrectionLevel ecLevel,\n                              Map<DecodeHintType,?> hints) throws FormatException {\n    BitSource bits = new BitSource(bytes);\n    StringBuilder result = new StringBuilder(50);\n    List<byte[]> byteSegments = new ArrayList<>(1);\n    int symbolSequence = -1;\n    int parityData = -1;\n\n    try {\n      CharacterSetECI currentCharacterSetECI = null;\n      boolean fc1InEffect = false;\n      Mode mode;\n      do {\n        // While still another segment to read...\n        if (bits.available() < 4) {\n          // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here\n          mode = Mode.TERMINATOR;\n        } else {\n          mode = Mode.forBits(bits.readBits(4)); // mode is encoded by 4 bits\n        }\n        switch (mode) {\n          case TERMINATOR:\n            break;\n          case FNC1_FIRST_POSITION:\n          case FNC1_SECOND_POSITION:\n            // We do little with FNC1 except alter the parsed result a bit according to the spec\n            fc1InEffect = true;\n            break;\n          case STRUCTURED_APPEND:\n            if (bits.available() < 16) {\n              throw FormatException.getFormatInstance();\n            }\n            // sequence number and parity is added later to the result metadata\n            // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue\n            symbolSequence = bits.readBits(8);\n            parityData = bits.readBits(8);\n            break;\n          case ECI:\n            // Count doesn't apply to ECI\n            int value = parseECIValue(bits);\n            currentCharacterSetECI = CharacterSetECI.getCharacterSetECIByValue(value);\n            if (currentCharacterSetECI == null) {\n              throw FormatException.getFormatInstance();\n            }\n            break;\n          case HANZI:\n            // First handle Hanzi mode which does not start with character count\n            // Chinese mode contains a sub set indicator right after mode indicator\n            int subset = bits.readBits(4);\n            int countHanzi = bits.readBits(mode.getCharacterCountBits(version));\n            if (subset == GB2312_SUBSET) {\n              decodeHanziSegment(bits, result, countHanzi);\n            }\n            break;\n          default:\n            // \"Normal\" QR code modes:\n            // How many characters will follow, encoded in this mode?\n            int count = bits.readBits(mode.getCharacterCountBits(version));\n            switch (mode) {\n              case NUMERIC:\n                decodeNumericSegment(bits, result, count);\n                break;\n              case ALPHANUMERIC:\n                decodeAlphanumericSegment(bits, result, count, fc1InEffect);\n                break;\n              case BYTE:\n                decodeByteSegment(bits, result, count, currentCharacterSetECI, byteSegments, hints);\n                break;\n              case KANJI:\n                decodeKanjiSegment(bits, result, count);\n                break;\n              default:\n                throw FormatException.getFormatInstance();\n            }\n            break;\n        }\n      } while (mode != Mode.TERMINATOR);\n    } catch (IllegalArgumentException iae) {\n      // from readBits() calls\n      throw FormatException.getFormatInstance();\n    }\n\n    return new DecoderResult(bytes,\n                             result.toString(),\n                             byteSegments.isEmpty() ? null : byteSegments,\n                             ecLevel == null ? null : ecLevel.toString(),\n                             symbolSequence,\n                             parityData);\n  }\n\n  /**\n   * See specification GBT 18284-2000\n   */\n  private static void decodeHanziSegment(BitSource bits,\n                                         StringBuilder result,\n                                         int count) throws FormatException {\n    // Don't crash trying to read more bits than we have available.\n    if (count * 13 > bits.available()) {\n      throw FormatException.getFormatInstance();\n    }\n\n    // Each character will require 2 bytes. Read the characters as 2-byte pairs\n    // and decode as GB2312 afterwards\n    byte[] buffer = new byte[2 * count];\n    int offset = 0;\n    while (count > 0) {\n      // Each 13 bits encodes a 2-byte character\n      int twoBytes = bits.readBits(13);\n      int assembledTwoBytes = ((twoBytes / 0x060) << 8) | (twoBytes % 0x060);\n      if (assembledTwoBytes < 0x00A00) {\n        // In the 0xA1A1 to 0xAAFE range\n        assembledTwoBytes += 0x0A1A1;\n      } else {\n        // In the 0xB0A1 to 0xFAFE range\n        assembledTwoBytes += 0x0A6A1;\n      }\n      buffer[offset] = (byte) ((assembledTwoBytes >> 8) & 0xFF);\n      buffer[offset + 1] = (byte) (assembledTwoBytes & 0xFF);\n      offset += 2;\n      count--;\n    }\n\n    try {\n      result.append(new String(buffer, StringUtils.GB2312));\n    } catch (UnsupportedEncodingException ignored) {\n      throw FormatException.getFormatInstance();\n    }\n  }\n\n  private static void decodeKanjiSegment(BitSource bits,\n                                         StringBuilder result,\n                                         int count) throws FormatException {\n    // Don't crash trying to read more bits than we have available.\n    if (count * 13 > bits.available()) {\n      throw FormatException.getFormatInstance();\n    }\n\n    // Each character will require 2 bytes. Read the characters as 2-byte pairs\n    // and decode as Shift_JIS afterwards\n    byte[] buffer = new byte[2 * count];\n    int offset = 0;\n    while (count > 0) {\n      // Each 13 bits encodes a 2-byte character\n      int twoBytes = bits.readBits(13);\n      int assembledTwoBytes = ((twoBytes / 0x0C0) << 8) | (twoBytes % 0x0C0);\n      if (assembledTwoBytes < 0x01F00) {\n        // In the 0x8140 to 0x9FFC range\n        assembledTwoBytes += 0x08140;\n      } else {\n        // In the 0xE040 to 0xEBBF range\n        assembledTwoBytes += 0x0C140;\n      }\n      buffer[offset] = (byte) (assembledTwoBytes >> 8);\n      buffer[offset + 1] = (byte) assembledTwoBytes;\n      offset += 2;\n      count--;\n    }\n    // Shift_JIS may not be supported in some environments:\n    try {\n      result.append(new String(buffer, StringUtils.SHIFT_JIS));\n    } catch (UnsupportedEncodingException ignored) {\n      throw FormatException.getFormatInstance();\n    }\n  }\n\n  private static void decodeByteSegment(BitSource bits,\n                                        StringBuilder result,\n                                        int count,\n                                        CharacterSetECI currentCharacterSetECI,\n                                        Collection<byte[]> byteSegments,\n                                        Map<DecodeHintType,?> hints) throws FormatException {\n    // Don't crash trying to read more bits than we have available.\n    if (8 * count > bits.available()) {\n      throw FormatException.getFormatInstance();\n    }\n\n    byte[] readBytes = new byte[count];\n    for (int i = 0; i < count; i++) {\n      readBytes[i] = (byte) bits.readBits(8);\n    }\n    String encoding = \"UTF-8\";\n    try {\n      result.append(new String(readBytes, encoding));\n    } catch (UnsupportedEncodingException ignored) {\n      throw FormatException.getFormatInstance();\n    }\n    byteSegments.add(readBytes);\n  }\n\n  private static char toAlphaNumericChar(int value) throws FormatException {\n    if (value >= ALPHANUMERIC_CHARS.length) {\n      throw FormatException.getFormatInstance();\n    }\n    return ALPHANUMERIC_CHARS[value];\n  }\n\n  private static void decodeAlphanumericSegment(BitSource bits,\n                                                StringBuilder result,\n                                                int count,\n                                                boolean fc1InEffect) throws FormatException {\n    // Read two characters at a time\n    int start = result.length();\n    while (count > 1) {\n      if (bits.available() < 11) {\n        throw FormatException.getFormatInstance();\n      }\n      int nextTwoCharsBits = bits.readBits(11);\n      result.append(toAlphaNumericChar(nextTwoCharsBits / 45));\n      result.append(toAlphaNumericChar(nextTwoCharsBits % 45));\n      count -= 2;\n    }\n    if (count == 1) {\n      // special case: one character left\n      if (bits.available() < 6) {\n        throw FormatException.getFormatInstance();\n      }\n      result.append(toAlphaNumericChar(bits.readBits(6)));\n    }\n    // See section 6.4.8.1, 6.4.8.2\n    if (fc1InEffect) {\n      // We need to massage the result a bit if in an FNC1 mode:\n      for (int i = start; i < result.length(); i++) {\n        if (result.charAt(i) == '%') {\n          if (i < result.length() - 1 && result.charAt(i + 1) == '%') {\n            // %% is rendered as %\n            result.deleteCharAt(i + 1);\n          } else {\n            // In alpha mode, % should be converted to FNC1 separator 0x1D\n            result.setCharAt(i, (char) 0x1D);\n          }\n        }\n      }\n    }\n  }\n\n  private static void decodeNumericSegment(BitSource bits,\n                                           StringBuilder result,\n                                           int count) throws FormatException {\n    // Read three digits at a time\n    while (count >= 3) {\n      // Each 10 bits encodes three digits\n      if (bits.available() < 10) {\n        throw FormatException.getFormatInstance();\n      }\n      int threeDigitsBits = bits.readBits(10);\n      if (threeDigitsBits >= 1000) {\n        throw FormatException.getFormatInstance();\n      }\n      result.append(toAlphaNumericChar(threeDigitsBits / 100));\n      result.append(toAlphaNumericChar((threeDigitsBits / 10) % 10));\n      result.append(toAlphaNumericChar(threeDigitsBits % 10));\n      count -= 3;\n    }\n    if (count == 2) {\n      // Two digits left over to read, encoded in 7 bits\n      if (bits.available() < 7) {\n        throw FormatException.getFormatInstance();\n      }\n      int twoDigitsBits = bits.readBits(7);\n      if (twoDigitsBits >= 100) {\n        throw FormatException.getFormatInstance();\n      }\n      result.append(toAlphaNumericChar(twoDigitsBits / 10));\n      result.append(toAlphaNumericChar(twoDigitsBits % 10));\n    } else if (count == 1) {\n      // One digit left over to read\n      if (bits.available() < 4) {\n        throw FormatException.getFormatInstance();\n      }\n      int digitBits = bits.readBits(4);\n      if (digitBits >= 10) {\n        throw FormatException.getFormatInstance();\n      }\n      result.append(toAlphaNumericChar(digitBits));\n    }\n  }\n\n  private static int parseECIValue(BitSource bits) throws FormatException {\n    int firstByte = bits.readBits(8);\n    if ((firstByte & 0x80) == 0) {\n      // just one byte\n      return firstByte & 0x7F;\n    }\n    if ((firstByte & 0xC0) == 0x80) {\n      // two bytes\n      int secondByte = bits.readBits(8);\n      return ((firstByte & 0x3F) << 8) | secondByte;\n    }\n    if ((firstByte & 0xE0) == 0xC0) {\n      // three bytes\n      int secondThirdBytes = bits.readBits(16);\n      return ((firstByte & 0x1F) << 16) | secondThirdBytes;\n    }\n    throw FormatException.getFormatInstance();\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/Decoder.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\nimport peergos.shared.zxing.ChecksumException;\nimport peergos.shared.zxing.DecodeHintType;\nimport peergos.shared.zxing.FormatException;\nimport peergos.shared.zxing.common.BitMatrix;\nimport peergos.shared.zxing.common.DecoderResult;\nimport peergos.shared.zxing.common.reedsolomon.GenericGF;\nimport peergos.shared.zxing.common.reedsolomon.ReedSolomonDecoder;\nimport peergos.shared.zxing.common.reedsolomon.ReedSolomonException;\n\nimport java.util.Map;\n\n/**\n * <p>The main class which implements QR Code decoding -- as opposed to locating and extracting\n * the QR Code from an image.</p>\n *\n * @author Sean Owen\n */\npublic final class Decoder {\n\n  private final ReedSolomonDecoder rsDecoder;\n\n  public Decoder() {\n    rsDecoder = new ReedSolomonDecoder(GenericGF.QR_CODE_FIELD_256);\n  }\n\n  public DecoderResult decode(boolean[][] image) throws ChecksumException, FormatException {\n    return decode(image, null);\n  }\n\n  /**\n   * <p>Convenience method that can decode a QR Code represented as a 2D array of booleans.\n   * \"true\" is taken to mean a black module.</p>\n   *\n   * @param image booleans representing white/black QR Code modules\n   * @param hints decoding hints that should be used to influence decoding\n   * @return text and bytes encoded within the QR Code\n   * @throws FormatException if the QR Code cannot be decoded\n   * @throws ChecksumException if error correction fails\n   */\n  public DecoderResult decode(boolean[][] image, Map<DecodeHintType,?> hints)\n      throws ChecksumException, FormatException {\n    return decode(BitMatrix.parse(image), hints);\n  }\n\n  public DecoderResult decode(BitMatrix bits) throws ChecksumException, FormatException {\n    return decode(bits, null);\n  }\n\n  /**\n   * <p>Decodes a QR Code represented as a {@link BitMatrix}. A 1 or \"true\" is taken to mean a black module.</p>\n   *\n   * @param bits booleans representing white/black QR Code modules\n   * @param hints decoding hints that should be used to influence decoding\n   * @return text and bytes encoded within the QR Code\n   * @throws FormatException if the QR Code cannot be decoded\n   * @throws ChecksumException if error correction fails\n   */\n  public DecoderResult decode(BitMatrix bits, Map<DecodeHintType,?> hints)\n      throws FormatException, ChecksumException {\n\n    // Construct a parser and read version, error-correction level\n    BitMatrixParser parser = new BitMatrixParser(bits);\n    FormatException fe = null;\n    ChecksumException ce = null;\n    try {\n      return decode(parser, hints);\n    } catch (FormatException e) {\n      fe = e;\n    } catch (ChecksumException e) {\n      ce = e;\n    }\n\n    try {\n\n      // Revert the bit matrix\n      parser.remask();\n\n      // Will be attempting a mirrored reading of the version and format info.\n      parser.setMirror(true);\n\n      // Preemptively read the version.\n      parser.readVersion();\n\n      // Preemptively read the format information.\n      parser.readFormatInformation();\n\n      /*\n       * Since we're here, this means we have successfully detected some kind\n       * of version and format information when mirrored. This is a good sign,\n       * that the QR code may be mirrored, and we should try once more with a\n       * mirrored content.\n       */\n      // Prepare for a mirrored reading.\n      parser.mirror();\n\n      DecoderResult result = decode(parser, hints);\n\n      // Success! Notify the caller that the code was mirrored.\n      result.setOther(new QRCodeDecoderMetaData(true));\n\n      return result;\n\n    } catch (FormatException | ChecksumException e) {\n      // Throw the exception from the original reading\n      if (fe != null) {\n        throw fe;\n      }\n      throw ce; // If fe is null, this can't be\n    }\n  }\n\n  private DecoderResult decode(BitMatrixParser parser, Map<DecodeHintType,?> hints)\n      throws FormatException, ChecksumException {\n    Version version = parser.readVersion();\n    ErrorCorrectionLevel ecLevel = parser.readFormatInformation().getErrorCorrectionLevel();\n\n    // Read codewords\n    byte[] codewords = parser.readCodewords();\n    // Separate into data blocks\n    DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel);\n\n    // Count total number of data bytes\n    int totalBytes = 0;\n    for (DataBlock dataBlock : dataBlocks) {\n      totalBytes += dataBlock.getNumDataCodewords();\n    }\n    byte[] resultBytes = new byte[totalBytes];\n    int resultOffset = 0;\n\n    // Error-correct and copy data blocks together into a stream of bytes\n    for (DataBlock dataBlock : dataBlocks) {\n      byte[] codewordBytes = dataBlock.getCodewords();\n      int numDataCodewords = dataBlock.getNumDataCodewords();\n      correctErrors(codewordBytes, numDataCodewords);\n      for (int i = 0; i < numDataCodewords; i++) {\n        resultBytes[resultOffset++] = codewordBytes[i];\n      }\n    }\n\n    // Decode the contents of that stream of bytes\n    return DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints);\n  }\n\n  /**\n   * <p>Given data and error-correction codewords received, possibly corrupted by errors, attempts to\n   * correct the errors in-place using Reed-Solomon error correction.</p>\n   *\n   * @param codewordBytes data and error correction codewords\n   * @param numDataCodewords number of codewords that are data bytes\n   * @throws ChecksumException if error correction fails\n   */\n  private void correctErrors(byte[] codewordBytes, int numDataCodewords) throws ChecksumException {\n    int numCodewords = codewordBytes.length;\n    // First read into an array of ints\n    int[] codewordsInts = new int[numCodewords];\n    for (int i = 0; i < numCodewords; i++) {\n      codewordsInts[i] = codewordBytes[i] & 0xFF;\n    }\n    try {\n      rsDecoder.decode(codewordsInts, codewordBytes.length - numDataCodewords);\n    } catch (ReedSolomonException ignored) {\n      throw ChecksumException.getChecksumInstance();\n    }\n    // Copy back into array of bytes -- only need to worry about the bytes that were data\n    // We don't care about errors in the error-correction codewords\n    for (int i = 0; i < numDataCodewords; i++) {\n      codewordBytes[i] = (byte) codewordsInts[i];\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/ErrorCorrectionLevel.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\n/**\n * <p>See ISO 18004:2006, 6.5.1. This enum encapsulates the four error correction levels\n * defined by the QR code standard.</p>\n *\n * @author Sean Owen\n */\npublic enum ErrorCorrectionLevel {\n\n  /** L = ~7% correction */\n  L(0x01),\n  /** M = ~15% correction */\n  M(0x00),\n  /** Q = ~25% correction */\n  Q(0x03),\n  /** H = ~30% correction */\n  H(0x02);\n\n  private static final ErrorCorrectionLevel[] FOR_BITS = {M, L, H, Q};\n\n  private final int bits;\n\n  ErrorCorrectionLevel(int bits) {\n    this.bits = bits;\n  }\n\n  public int getBits() {\n    return bits;\n  }\n\n  /**\n   * @param bits int containing the two bits encoding a QR Code's error correction level\n   * @return ErrorCorrectionLevel representing the encoded error correction level\n   */\n  public static ErrorCorrectionLevel forBits(int bits) {\n    if (bits < 0 || bits >= FOR_BITS.length) {\n      throw new IllegalArgumentException();\n    }\n    return FOR_BITS[bits];\n  }\n\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/FormatInformation.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\n/**\n * <p>Encapsulates a QR Code's format information, including the data mask used and\n * error correction level.</p>\n *\n * @author Sean Owen\n * @see DataMask\n * @see ErrorCorrectionLevel\n */\nfinal class FormatInformation {\n\n  private static final int FORMAT_INFO_MASK_QR = 0x5412;\n\n  /**\n   * See ISO 18004:2006, Annex C, Table C.1\n   */\n  private static final int[][] FORMAT_INFO_DECODE_LOOKUP = {\n      {0x5412, 0x00},\n      {0x5125, 0x01},\n      {0x5E7C, 0x02},\n      {0x5B4B, 0x03},\n      {0x45F9, 0x04},\n      {0x40CE, 0x05},\n      {0x4F97, 0x06},\n      {0x4AA0, 0x07},\n      {0x77C4, 0x08},\n      {0x72F3, 0x09},\n      {0x7DAA, 0x0A},\n      {0x789D, 0x0B},\n      {0x662F, 0x0C},\n      {0x6318, 0x0D},\n      {0x6C41, 0x0E},\n      {0x6976, 0x0F},\n      {0x1689, 0x10},\n      {0x13BE, 0x11},\n      {0x1CE7, 0x12},\n      {0x19D0, 0x13},\n      {0x0762, 0x14},\n      {0x0255, 0x15},\n      {0x0D0C, 0x16},\n      {0x083B, 0x17},\n      {0x355F, 0x18},\n      {0x3068, 0x19},\n      {0x3F31, 0x1A},\n      {0x3A06, 0x1B},\n      {0x24B4, 0x1C},\n      {0x2183, 0x1D},\n      {0x2EDA, 0x1E},\n      {0x2BED, 0x1F},\n  };\n\n  private final ErrorCorrectionLevel errorCorrectionLevel;\n  private final byte dataMask;\n\n  private FormatInformation(int formatInfo) {\n    // Bits 3,4\n    errorCorrectionLevel = ErrorCorrectionLevel.forBits((formatInfo >> 3) & 0x03);\n    // Bottom 3 bits\n    dataMask = (byte) (formatInfo & 0x07);\n  }\n\n  static int numBitsDiffering(int a, int b) {\n    return Integer.bitCount(a ^ b);\n  }\n\n  /**\n   * @param maskedFormatInfo1 format info indicator, with mask still applied\n   * @param maskedFormatInfo2 second copy of same info; both are checked at the same time\n   *  to establish best match\n   * @return information about the format it specifies, or {@code null}\n   *  if doesn't seem to match any known pattern\n   */\n  static FormatInformation decodeFormatInformation(int maskedFormatInfo1, int maskedFormatInfo2) {\n    FormatInformation formatInfo = doDecodeFormatInformation(maskedFormatInfo1, maskedFormatInfo2);\n    if (formatInfo != null) {\n      return formatInfo;\n    }\n    // Should return null, but, some QR codes apparently\n    // do not mask this info. Try again by actually masking the pattern\n    // first\n    return doDecodeFormatInformation(maskedFormatInfo1 ^ FORMAT_INFO_MASK_QR,\n                                     maskedFormatInfo2 ^ FORMAT_INFO_MASK_QR);\n  }\n\n  private static FormatInformation doDecodeFormatInformation(int maskedFormatInfo1, int maskedFormatInfo2) {\n    // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing\n    int bestDifference = Integer.MAX_VALUE;\n    int bestFormatInfo = 0;\n    for (int[] decodeInfo : FORMAT_INFO_DECODE_LOOKUP) {\n      int targetInfo = decodeInfo[0];\n      if (targetInfo == maskedFormatInfo1 || targetInfo == maskedFormatInfo2) {\n        // Found an exact match\n        return new FormatInformation(decodeInfo[1]);\n      }\n      int bitsDifference = numBitsDiffering(maskedFormatInfo1, targetInfo);\n      if (bitsDifference < bestDifference) {\n        bestFormatInfo = decodeInfo[1];\n        bestDifference = bitsDifference;\n      }\n      if (maskedFormatInfo1 != maskedFormatInfo2) {\n        // also try the other option\n        bitsDifference = numBitsDiffering(maskedFormatInfo2, targetInfo);\n        if (bitsDifference < bestDifference) {\n          bestFormatInfo = decodeInfo[1];\n          bestDifference = bitsDifference;\n        }\n      }\n    }\n    // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits\n    // differing means we found a match\n    if (bestDifference <= 3) {\n      return new FormatInformation(bestFormatInfo);\n    }\n    return null;\n  }\n\n  ErrorCorrectionLevel getErrorCorrectionLevel() {\n    return errorCorrectionLevel;\n  }\n\n  byte getDataMask() {\n    return dataMask;\n  }\n\n  @Override\n  public int hashCode() {\n    return (errorCorrectionLevel.ordinal() << 3) | dataMask;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof FormatInformation)) {\n      return false;\n    }\n    FormatInformation other = (FormatInformation) o;\n    return this.errorCorrectionLevel == other.errorCorrectionLevel &&\n        this.dataMask == other.dataMask;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/Mode.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\n/**\n * <p>See ISO 18004:2006, 6.4.1, Tables 2 and 3. This enum encapsulates the various modes in which\n * data can be encoded to bits in the QR code standard.</p>\n *\n * @author Sean Owen\n */\npublic enum Mode {\n\n  TERMINATOR(new int[]{0, 0, 0}, 0x00), // Not really a mode...\n  NUMERIC(new int[]{10, 12, 14}, 0x01),\n  ALPHANUMERIC(new int[]{9, 11, 13}, 0x02),\n  STRUCTURED_APPEND(new int[]{0, 0, 0}, 0x03), // Not supported\n  BYTE(new int[]{8, 16, 16}, 0x04),\n  ECI(new int[]{0, 0, 0}, 0x07), // character counts don't apply\n  KANJI(new int[]{8, 10, 12}, 0x08),\n  FNC1_FIRST_POSITION(new int[]{0, 0, 0}, 0x05),\n  FNC1_SECOND_POSITION(new int[]{0, 0, 0}, 0x09),\n  /** See GBT 18284-2000; \"Hanzi\" is a transliteration of this mode name. */\n  HANZI(new int[]{8, 10, 12}, 0x0D);\n\n  private final int[] characterCountBitsForVersions;\n  private final int bits;\n\n  Mode(int[] characterCountBitsForVersions, int bits) {\n    this.characterCountBitsForVersions = characterCountBitsForVersions;\n    this.bits = bits;\n  }\n\n  /**\n   * @param bits four bits encoding a QR Code data mode\n   * @return Mode encoded by these bits\n   * @throws IllegalArgumentException if bits do not correspond to a known mode\n   */\n  public static Mode forBits(int bits) {\n    switch (bits) {\n      case 0x0:\n        return TERMINATOR;\n      case 0x1:\n        return NUMERIC;\n      case 0x2:\n        return ALPHANUMERIC;\n      case 0x3:\n        return STRUCTURED_APPEND;\n      case 0x4:\n        return BYTE;\n      case 0x5:\n        return FNC1_FIRST_POSITION;\n      case 0x7:\n        return ECI;\n      case 0x8:\n        return KANJI;\n      case 0x9:\n        return FNC1_SECOND_POSITION;\n      case 0xD:\n        // 0xD is defined in GBT 18284-2000, may not be supported in foreign country\n        return HANZI;\n      default:\n        throw new IllegalArgumentException();\n    }\n  }\n\n  /**\n   * @param version version in question\n   * @return number of bits used, in this QR Code symbol {@link Version}, to encode the\n   *         count of characters that will follow encoded in this Mode\n   */\n  public int getCharacterCountBits(Version version) {\n    int number = version.getVersionNumber();\n    int offset;\n    if (number <= 9) {\n      offset = 0;\n    } else if (number <= 26) {\n      offset = 1;\n    } else {\n      offset = 2;\n    }\n    return characterCountBitsForVersions[offset];\n  }\n\n  public int getBits() {\n    return bits;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/QRCodeDecoderMetaData.java",
    "content": "/*\n * Copyright 2013 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\nimport peergos.shared.zxing.ResultPoint;\n\n/**\n * Meta-data container for QR Code decoding. Instances of this class may be used to convey information back to the\n * decoding caller. Callers are expected to process this.\n *\n * @see peergos.shared.zxing.common.DecoderResult#getOther()\n */\npublic final class QRCodeDecoderMetaData {\n\n  private final boolean mirrored;\n\n  QRCodeDecoderMetaData(boolean mirrored) {\n    this.mirrored = mirrored;\n  }\n\n  /**\n   * @return true if the QR Code was mirrored.\n   */\n  public boolean isMirrored() {\n    return mirrored;\n  }\n\n  /**\n   * Apply the result points' order correction due to mirroring.\n   *\n   * @param points Array of points to apply mirror correction to.\n   */\n  public void applyMirroredCorrection(ResultPoint[] points) {\n    if (!mirrored || points == null || points.length < 3) {\n      return;\n    }\n    ResultPoint bottomLeft = points[0];\n    points[0] = points[2];\n    points[2] = bottomLeft;\n    // No need to 'fix' top-left and alignment pattern.\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/decoder/Version.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.decoder;\n\nimport peergos.shared.zxing.FormatException;\nimport peergos.shared.zxing.common.BitMatrix;\n\n/**\n * See ISO 18004:2006 Annex D\n *\n * @author Sean Owen\n */\npublic final class Version {\n\n  /**\n   * See ISO 18004:2006 Annex D.\n   * Element i represents the raw version bits that specify version i + 7\n   */\n  private static final int[] VERSION_DECODE_INFO = {\n      0x07C94, 0x085BC, 0x09A99, 0x0A4D3, 0x0BBF6,\n      0x0C762, 0x0D847, 0x0E60D, 0x0F928, 0x10B78,\n      0x1145D, 0x12A17, 0x13532, 0x149A6, 0x15683,\n      0x168C9, 0x177EC, 0x18EC4, 0x191E1, 0x1AFAB,\n      0x1B08E, 0x1CC1A, 0x1D33F, 0x1ED75, 0x1F250,\n      0x209D5, 0x216F0, 0x228BA, 0x2379F, 0x24B0B,\n      0x2542E, 0x26A64, 0x27541, 0x28C69\n  };\n\n  private static final Version[] VERSIONS = buildVersions();\n\n  private final int versionNumber;\n  private final int[] alignmentPatternCenters;\n  private final ECBlocks[] ecBlocks;\n  private final int totalCodewords;\n\n  private Version(int versionNumber,\n                  int[] alignmentPatternCenters,\n                  ECBlocks... ecBlocks) {\n    this.versionNumber = versionNumber;\n    this.alignmentPatternCenters = alignmentPatternCenters;\n    this.ecBlocks = ecBlocks;\n    int total = 0;\n    int ecCodewords = ecBlocks[0].getECCodewordsPerBlock();\n    ECB[] ecbArray = ecBlocks[0].getECBlocks();\n    for (ECB ecBlock : ecbArray) {\n      total += ecBlock.getCount() * (ecBlock.getDataCodewords() + ecCodewords);\n    }\n    this.totalCodewords = total;\n  }\n\n  public int getVersionNumber() {\n    return versionNumber;\n  }\n\n  public int[] getAlignmentPatternCenters() {\n    return alignmentPatternCenters;\n  }\n\n  public int getTotalCodewords() {\n    return totalCodewords;\n  }\n\n  public int getDimensionForVersion() {\n    return 17 + 4 * versionNumber;\n  }\n\n  public ECBlocks getECBlocksForLevel(ErrorCorrectionLevel ecLevel) {\n    return ecBlocks[ecLevel.ordinal()];\n  }\n\n  /**\n   * <p>Deduces version information purely from QR Code dimensions.</p>\n   *\n   * @param dimension dimension in modules\n   * @return Version for a QR Code of that dimension\n   * @throws FormatException if dimension is not 1 mod 4\n   */\n  public static Version getProvisionalVersionForDimension(int dimension) throws FormatException {\n    if (dimension % 4 != 1) {\n      throw FormatException.getFormatInstance();\n    }\n    try {\n      return getVersionForNumber((dimension - 17) / 4);\n    } catch (IllegalArgumentException ignored) {\n      throw FormatException.getFormatInstance();\n    }\n  }\n\n  public static Version getVersionForNumber(int versionNumber) {\n    if (versionNumber < 1 || versionNumber > 40) {\n      throw new IllegalArgumentException();\n    }\n    return VERSIONS[versionNumber - 1];\n  }\n\n  static Version decodeVersionInformation(int versionBits) {\n    int bestDifference = Integer.MAX_VALUE;\n    int bestVersion = 0;\n    for (int i = 0; i < VERSION_DECODE_INFO.length; i++) {\n      int targetVersion = VERSION_DECODE_INFO[i];\n      // Do the version info bits match exactly? done.\n      if (targetVersion == versionBits) {\n        return getVersionForNumber(i + 7);\n      }\n      // Otherwise see if this is the closest to a real version info bit string\n      // we have seen so far\n      int bitsDifference = FormatInformation.numBitsDiffering(versionBits, targetVersion);\n      if (bitsDifference < bestDifference) {\n        bestVersion = i + 7;\n        bestDifference = bitsDifference;\n      }\n    }\n    // We can tolerate up to 3 bits of error since no two version info codewords will\n    // differ in less than 8 bits.\n    if (bestDifference <= 3) {\n      return getVersionForNumber(bestVersion);\n    }\n    // If we didn't find a close enough match, fail\n    return null;\n  }\n\n  /**\n   * See ISO 18004:2006 Annex E\n   */\n  BitMatrix buildFunctionPattern() {\n    int dimension = getDimensionForVersion();\n    BitMatrix bitMatrix = new BitMatrix(dimension);\n\n    // Top left finder pattern + separator + format\n    bitMatrix.setRegion(0, 0, 9, 9);\n    // Top right finder pattern + separator + format\n    bitMatrix.setRegion(dimension - 8, 0, 8, 9);\n    // Bottom left finder pattern + separator + format\n    bitMatrix.setRegion(0, dimension - 8, 9, 8);\n\n    // Alignment patterns\n    int max = alignmentPatternCenters.length;\n    for (int x = 0; x < max; x++) {\n      int i = alignmentPatternCenters[x] - 2;\n      for (int y = 0; y < max; y++) {\n        if ((x != 0 || (y != 0 && y != max - 1)) && (x != max - 1 || y != 0)) {\n          bitMatrix.setRegion(alignmentPatternCenters[y] - 2, i, 5, 5);\n        }\n        // else no o alignment patterns near the three finder patterns\n      }\n    }\n\n    // Vertical timing pattern\n    bitMatrix.setRegion(6, 9, 1, dimension - 17);\n    // Horizontal timing pattern\n    bitMatrix.setRegion(9, 6, dimension - 17, 1);\n\n    if (versionNumber > 6) {\n      // Version info, top right\n      bitMatrix.setRegion(dimension - 11, 0, 3, 6);\n      // Version info, bottom left\n      bitMatrix.setRegion(0, dimension - 11, 6, 3);\n    }\n\n    return bitMatrix;\n  }\n\n  /**\n   * <p>Encapsulates a set of error-correction blocks in one symbol version. Most versions will\n   * use blocks of differing sizes within one version, so, this encapsulates the parameters for\n   * each set of blocks. It also holds the number of error-correction codewords per block since it\n   * will be the same across all blocks within one version.</p>\n   */\n  public static final class ECBlocks {\n    private final int ecCodewordsPerBlock;\n    private final ECB[] ecBlocks;\n\n    ECBlocks(int ecCodewordsPerBlock, ECB... ecBlocks) {\n      this.ecCodewordsPerBlock = ecCodewordsPerBlock;\n      this.ecBlocks = ecBlocks;\n    }\n\n    public int getECCodewordsPerBlock() {\n      return ecCodewordsPerBlock;\n    }\n\n    public int getNumBlocks() {\n      int total = 0;\n      for (ECB ecBlock : ecBlocks) {\n        total += ecBlock.getCount();\n      }\n      return total;\n    }\n\n    public int getTotalECCodewords() {\n      return ecCodewordsPerBlock * getNumBlocks();\n    }\n\n    public ECB[] getECBlocks() {\n      return ecBlocks;\n    }\n  }\n\n  /**\n   * <p>Encapsulates the parameters for one error-correction block in one symbol version.\n   * This includes the number of data codewords, and the number of times a block with these\n   * parameters is used consecutively in the QR code version's format.</p>\n   */\n  public static final class ECB {\n    private final int count;\n    private final int dataCodewords;\n\n    ECB(int count, int dataCodewords) {\n      this.count = count;\n      this.dataCodewords = dataCodewords;\n    }\n\n    public int getCount() {\n      return count;\n    }\n\n    public int getDataCodewords() {\n      return dataCodewords;\n    }\n  }\n\n  @Override\n  public String toString() {\n    return String.valueOf(versionNumber);\n  }\n\n  /**\n   * See ISO 18004:2006 6.5.1 Table 9\n   */\n  private static Version[] buildVersions() {\n    return new Version[]{\n        new Version(1, new int[]{},\n            new ECBlocks(7, new ECB(1, 19)),\n            new ECBlocks(10, new ECB(1, 16)),\n            new ECBlocks(13, new ECB(1, 13)),\n            new ECBlocks(17, new ECB(1, 9))),\n        new Version(2, new int[]{6, 18},\n            new ECBlocks(10, new ECB(1, 34)),\n            new ECBlocks(16, new ECB(1, 28)),\n            new ECBlocks(22, new ECB(1, 22)),\n            new ECBlocks(28, new ECB(1, 16))),\n        new Version(3, new int[]{6, 22},\n            new ECBlocks(15, new ECB(1, 55)),\n            new ECBlocks(26, new ECB(1, 44)),\n            new ECBlocks(18, new ECB(2, 17)),\n            new ECBlocks(22, new ECB(2, 13))),\n        new Version(4, new int[]{6, 26},\n            new ECBlocks(20, new ECB(1, 80)),\n            new ECBlocks(18, new ECB(2, 32)),\n            new ECBlocks(26, new ECB(2, 24)),\n            new ECBlocks(16, new ECB(4, 9))),\n        new Version(5, new int[]{6, 30},\n            new ECBlocks(26, new ECB(1, 108)),\n            new ECBlocks(24, new ECB(2, 43)),\n            new ECBlocks(18, new ECB(2, 15),\n                new ECB(2, 16)),\n            new ECBlocks(22, new ECB(2, 11),\n                new ECB(2, 12))),\n        new Version(6, new int[]{6, 34},\n            new ECBlocks(18, new ECB(2, 68)),\n            new ECBlocks(16, new ECB(4, 27)),\n            new ECBlocks(24, new ECB(4, 19)),\n            new ECBlocks(28, new ECB(4, 15))),\n        new Version(7, new int[]{6, 22, 38},\n            new ECBlocks(20, new ECB(2, 78)),\n            new ECBlocks(18, new ECB(4, 31)),\n            new ECBlocks(18, new ECB(2, 14),\n                new ECB(4, 15)),\n            new ECBlocks(26, new ECB(4, 13),\n                new ECB(1, 14))),\n        new Version(8, new int[]{6, 24, 42},\n            new ECBlocks(24, new ECB(2, 97)),\n            new ECBlocks(22, new ECB(2, 38),\n                new ECB(2, 39)),\n            new ECBlocks(22, new ECB(4, 18),\n                new ECB(2, 19)),\n            new ECBlocks(26, new ECB(4, 14),\n                new ECB(2, 15))),\n        new Version(9, new int[]{6, 26, 46},\n            new ECBlocks(30, new ECB(2, 116)),\n            new ECBlocks(22, new ECB(3, 36),\n                new ECB(2, 37)),\n            new ECBlocks(20, new ECB(4, 16),\n                new ECB(4, 17)),\n            new ECBlocks(24, new ECB(4, 12),\n                new ECB(4, 13))),\n        new Version(10, new int[]{6, 28, 50},\n            new ECBlocks(18, new ECB(2, 68),\n                new ECB(2, 69)),\n            new ECBlocks(26, new ECB(4, 43),\n                new ECB(1, 44)),\n            new ECBlocks(24, new ECB(6, 19),\n                new ECB(2, 20)),\n            new ECBlocks(28, new ECB(6, 15),\n                new ECB(2, 16))),\n        new Version(11, new int[]{6, 30, 54},\n            new ECBlocks(20, new ECB(4, 81)),\n            new ECBlocks(30, new ECB(1, 50),\n                new ECB(4, 51)),\n            new ECBlocks(28, new ECB(4, 22),\n                new ECB(4, 23)),\n            new ECBlocks(24, new ECB(3, 12),\n                new ECB(8, 13))),\n        new Version(12, new int[]{6, 32, 58},\n            new ECBlocks(24, new ECB(2, 92),\n                new ECB(2, 93)),\n            new ECBlocks(22, new ECB(6, 36),\n                new ECB(2, 37)),\n            new ECBlocks(26, new ECB(4, 20),\n                new ECB(6, 21)),\n            new ECBlocks(28, new ECB(7, 14),\n                new ECB(4, 15))),\n        new Version(13, new int[]{6, 34, 62},\n            new ECBlocks(26, new ECB(4, 107)),\n            new ECBlocks(22, new ECB(8, 37),\n                new ECB(1, 38)),\n            new ECBlocks(24, new ECB(8, 20),\n                new ECB(4, 21)),\n            new ECBlocks(22, new ECB(12, 11),\n                new ECB(4, 12))),\n        new Version(14, new int[]{6, 26, 46, 66},\n            new ECBlocks(30, new ECB(3, 115),\n                new ECB(1, 116)),\n            new ECBlocks(24, new ECB(4, 40),\n                new ECB(5, 41)),\n            new ECBlocks(20, new ECB(11, 16),\n                new ECB(5, 17)),\n            new ECBlocks(24, new ECB(11, 12),\n                new ECB(5, 13))),\n        new Version(15, new int[]{6, 26, 48, 70},\n            new ECBlocks(22, new ECB(5, 87),\n                new ECB(1, 88)),\n            new ECBlocks(24, new ECB(5, 41),\n                new ECB(5, 42)),\n            new ECBlocks(30, new ECB(5, 24),\n                new ECB(7, 25)),\n            new ECBlocks(24, new ECB(11, 12),\n                new ECB(7, 13))),\n        new Version(16, new int[]{6, 26, 50, 74},\n            new ECBlocks(24, new ECB(5, 98),\n                new ECB(1, 99)),\n            new ECBlocks(28, new ECB(7, 45),\n                new ECB(3, 46)),\n            new ECBlocks(24, new ECB(15, 19),\n                new ECB(2, 20)),\n            new ECBlocks(30, new ECB(3, 15),\n                new ECB(13, 16))),\n        new Version(17, new int[]{6, 30, 54, 78},\n            new ECBlocks(28, new ECB(1, 107),\n                new ECB(5, 108)),\n            new ECBlocks(28, new ECB(10, 46),\n                new ECB(1, 47)),\n            new ECBlocks(28, new ECB(1, 22),\n                new ECB(15, 23)),\n            new ECBlocks(28, new ECB(2, 14),\n                new ECB(17, 15))),\n        new Version(18, new int[]{6, 30, 56, 82},\n            new ECBlocks(30, new ECB(5, 120),\n                new ECB(1, 121)),\n            new ECBlocks(26, new ECB(9, 43),\n                new ECB(4, 44)),\n            new ECBlocks(28, new ECB(17, 22),\n                new ECB(1, 23)),\n            new ECBlocks(28, new ECB(2, 14),\n                new ECB(19, 15))),\n        new Version(19, new int[]{6, 30, 58, 86},\n            new ECBlocks(28, new ECB(3, 113),\n                new ECB(4, 114)),\n            new ECBlocks(26, new ECB(3, 44),\n                new ECB(11, 45)),\n            new ECBlocks(26, new ECB(17, 21),\n                new ECB(4, 22)),\n            new ECBlocks(26, new ECB(9, 13),\n                new ECB(16, 14))),\n        new Version(20, new int[]{6, 34, 62, 90},\n            new ECBlocks(28, new ECB(3, 107),\n                new ECB(5, 108)),\n            new ECBlocks(26, new ECB(3, 41),\n                new ECB(13, 42)),\n            new ECBlocks(30, new ECB(15, 24),\n                new ECB(5, 25)),\n            new ECBlocks(28, new ECB(15, 15),\n                new ECB(10, 16))),\n        new Version(21, new int[]{6, 28, 50, 72, 94},\n            new ECBlocks(28, new ECB(4, 116),\n                new ECB(4, 117)),\n            new ECBlocks(26, new ECB(17, 42)),\n            new ECBlocks(28, new ECB(17, 22),\n                new ECB(6, 23)),\n            new ECBlocks(30, new ECB(19, 16),\n                new ECB(6, 17))),\n        new Version(22, new int[]{6, 26, 50, 74, 98},\n            new ECBlocks(28, new ECB(2, 111),\n                new ECB(7, 112)),\n            new ECBlocks(28, new ECB(17, 46)),\n            new ECBlocks(30, new ECB(7, 24),\n                new ECB(16, 25)),\n            new ECBlocks(24, new ECB(34, 13))),\n        new Version(23, new int[]{6, 30, 54, 78, 102},\n            new ECBlocks(30, new ECB(4, 121),\n                new ECB(5, 122)),\n            new ECBlocks(28, new ECB(4, 47),\n                new ECB(14, 48)),\n            new ECBlocks(30, new ECB(11, 24),\n                new ECB(14, 25)),\n            new ECBlocks(30, new ECB(16, 15),\n                new ECB(14, 16))),\n        new Version(24, new int[]{6, 28, 54, 80, 106},\n            new ECBlocks(30, new ECB(6, 117),\n                new ECB(4, 118)),\n            new ECBlocks(28, new ECB(6, 45),\n                new ECB(14, 46)),\n            new ECBlocks(30, new ECB(11, 24),\n                new ECB(16, 25)),\n            new ECBlocks(30, new ECB(30, 16),\n                new ECB(2, 17))),\n        new Version(25, new int[]{6, 32, 58, 84, 110},\n            new ECBlocks(26, new ECB(8, 106),\n                new ECB(4, 107)),\n            new ECBlocks(28, new ECB(8, 47),\n                new ECB(13, 48)),\n            new ECBlocks(30, new ECB(7, 24),\n                new ECB(22, 25)),\n            new ECBlocks(30, new ECB(22, 15),\n                new ECB(13, 16))),\n        new Version(26, new int[]{6, 30, 58, 86, 114},\n            new ECBlocks(28, new ECB(10, 114),\n                new ECB(2, 115)),\n            new ECBlocks(28, new ECB(19, 46),\n                new ECB(4, 47)),\n            new ECBlocks(28, new ECB(28, 22),\n                new ECB(6, 23)),\n            new ECBlocks(30, new ECB(33, 16),\n                new ECB(4, 17))),\n        new Version(27, new int[]{6, 34, 62, 90, 118},\n            new ECBlocks(30, new ECB(8, 122),\n                new ECB(4, 123)),\n            new ECBlocks(28, new ECB(22, 45),\n                new ECB(3, 46)),\n            new ECBlocks(30, new ECB(8, 23),\n                new ECB(26, 24)),\n            new ECBlocks(30, new ECB(12, 15),\n                new ECB(28, 16))),\n        new Version(28, new int[]{6, 26, 50, 74, 98, 122},\n            new ECBlocks(30, new ECB(3, 117),\n                new ECB(10, 118)),\n            new ECBlocks(28, new ECB(3, 45),\n                new ECB(23, 46)),\n            new ECBlocks(30, new ECB(4, 24),\n                new ECB(31, 25)),\n            new ECBlocks(30, new ECB(11, 15),\n                new ECB(31, 16))),\n        new Version(29, new int[]{6, 30, 54, 78, 102, 126},\n            new ECBlocks(30, new ECB(7, 116),\n                new ECB(7, 117)),\n            new ECBlocks(28, new ECB(21, 45),\n                new ECB(7, 46)),\n            new ECBlocks(30, new ECB(1, 23),\n                new ECB(37, 24)),\n            new ECBlocks(30, new ECB(19, 15),\n                new ECB(26, 16))),\n        new Version(30, new int[]{6, 26, 52, 78, 104, 130},\n            new ECBlocks(30, new ECB(5, 115),\n                new ECB(10, 116)),\n            new ECBlocks(28, new ECB(19, 47),\n                new ECB(10, 48)),\n            new ECBlocks(30, new ECB(15, 24),\n                new ECB(25, 25)),\n            new ECBlocks(30, new ECB(23, 15),\n                new ECB(25, 16))),\n        new Version(31, new int[]{6, 30, 56, 82, 108, 134},\n            new ECBlocks(30, new ECB(13, 115),\n                new ECB(3, 116)),\n            new ECBlocks(28, new ECB(2, 46),\n                new ECB(29, 47)),\n            new ECBlocks(30, new ECB(42, 24),\n                new ECB(1, 25)),\n            new ECBlocks(30, new ECB(23, 15),\n                new ECB(28, 16))),\n        new Version(32, new int[]{6, 34, 60, 86, 112, 138},\n            new ECBlocks(30, new ECB(17, 115)),\n            new ECBlocks(28, new ECB(10, 46),\n                new ECB(23, 47)),\n            new ECBlocks(30, new ECB(10, 24),\n                new ECB(35, 25)),\n            new ECBlocks(30, new ECB(19, 15),\n                new ECB(35, 16))),\n        new Version(33, new int[]{6, 30, 58, 86, 114, 142},\n            new ECBlocks(30, new ECB(17, 115),\n                new ECB(1, 116)),\n            new ECBlocks(28, new ECB(14, 46),\n                new ECB(21, 47)),\n            new ECBlocks(30, new ECB(29, 24),\n                new ECB(19, 25)),\n            new ECBlocks(30, new ECB(11, 15),\n                new ECB(46, 16))),\n        new Version(34, new int[]{6, 34, 62, 90, 118, 146},\n            new ECBlocks(30, new ECB(13, 115),\n                new ECB(6, 116)),\n            new ECBlocks(28, new ECB(14, 46),\n                new ECB(23, 47)),\n            new ECBlocks(30, new ECB(44, 24),\n                new ECB(7, 25)),\n            new ECBlocks(30, new ECB(59, 16),\n                new ECB(1, 17))),\n        new Version(35, new int[]{6, 30, 54, 78, 102, 126, 150},\n            new ECBlocks(30, new ECB(12, 121),\n                new ECB(7, 122)),\n            new ECBlocks(28, new ECB(12, 47),\n                new ECB(26, 48)),\n            new ECBlocks(30, new ECB(39, 24),\n                new ECB(14, 25)),\n            new ECBlocks(30, new ECB(22, 15),\n                new ECB(41, 16))),\n        new Version(36, new int[]{6, 24, 50, 76, 102, 128, 154},\n            new ECBlocks(30, new ECB(6, 121),\n                new ECB(14, 122)),\n            new ECBlocks(28, new ECB(6, 47),\n                new ECB(34, 48)),\n            new ECBlocks(30, new ECB(46, 24),\n                new ECB(10, 25)),\n            new ECBlocks(30, new ECB(2, 15),\n                new ECB(64, 16))),\n        new Version(37, new int[]{6, 28, 54, 80, 106, 132, 158},\n            new ECBlocks(30, new ECB(17, 122),\n                new ECB(4, 123)),\n            new ECBlocks(28, new ECB(29, 46),\n                new ECB(14, 47)),\n            new ECBlocks(30, new ECB(49, 24),\n                new ECB(10, 25)),\n            new ECBlocks(30, new ECB(24, 15),\n                new ECB(46, 16))),\n        new Version(38, new int[]{6, 32, 58, 84, 110, 136, 162},\n            new ECBlocks(30, new ECB(4, 122),\n                new ECB(18, 123)),\n            new ECBlocks(28, new ECB(13, 46),\n                new ECB(32, 47)),\n            new ECBlocks(30, new ECB(48, 24),\n                new ECB(14, 25)),\n            new ECBlocks(30, new ECB(42, 15),\n                new ECB(32, 16))),\n        new Version(39, new int[]{6, 26, 54, 82, 110, 138, 166},\n            new ECBlocks(30, new ECB(20, 117),\n                new ECB(4, 118)),\n            new ECBlocks(28, new ECB(40, 47),\n                new ECB(7, 48)),\n            new ECBlocks(30, new ECB(43, 24),\n                new ECB(22, 25)),\n            new ECBlocks(30, new ECB(10, 15),\n                new ECB(67, 16))),\n        new Version(40, new int[]{6, 30, 58, 86, 114, 142, 170},\n            new ECBlocks(30, new ECB(19, 118),\n                new ECB(6, 119)),\n            new ECBlocks(28, new ECB(18, 47),\n                new ECB(31, 48)),\n            new ECBlocks(30, new ECB(34, 24),\n                new ECB(34, 25)),\n            new ECBlocks(30, new ECB(20, 15),\n                new ECB(61, 16)))\n    };\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/detector/AlignmentPattern.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.detector;\n\nimport peergos.shared.zxing.ResultPoint;\n\n/**\n * <p>Encapsulates an alignment pattern, which are the smaller square patterns found in\n * all but the simplest QR Codes.</p>\n *\n * @author Sean Owen\n */\npublic final class AlignmentPattern extends ResultPoint {\n\n  private final float estimatedModuleSize;\n\n  AlignmentPattern(float posX, float posY, float estimatedModuleSize) {\n    super(posX, posY);\n    this.estimatedModuleSize = estimatedModuleSize;\n  }\n\n  /**\n   * <p>Determines if this alignment pattern \"about equals\" an alignment pattern at the stated\n   * position and size -- meaning, it is at nearly the same center with nearly the same size.</p>\n   */\n  boolean aboutEquals(float moduleSize, float i, float j) {\n    if (Math.abs(i - getY()) <= moduleSize && Math.abs(j - getX()) <= moduleSize) {\n      float moduleSizeDiff = Math.abs(moduleSize - estimatedModuleSize);\n      return moduleSizeDiff <= 1.0f || moduleSizeDiff <= estimatedModuleSize;\n    }\n    return false;\n  }\n\n  /**\n   * Combines this object's current estimate of a finder pattern position and module size\n   * with a new estimate. It returns a new {@code FinderPattern} containing an average of the two.\n   */\n  AlignmentPattern combineEstimate(float i, float j, float newModuleSize) {\n    float combinedX = (getX() + j) / 2.0f;\n    float combinedY = (getY() + i) / 2.0f;\n    float combinedModuleSize = (estimatedModuleSize + newModuleSize) / 2.0f;\n    return new AlignmentPattern(combinedX, combinedY, combinedModuleSize);\n  }\n\n}"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/detector/AlignmentPatternFinder.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.detector;\n\nimport peergos.shared.zxing.NotFoundException;\nimport peergos.shared.zxing.ResultPointCallback;\nimport peergos.shared.zxing.common.BitMatrix;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * <p>This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder\n * patterns but are smaller and appear at regular intervals throughout the image.</p>\n *\n * <p>At the moment this only looks for the bottom-right alignment pattern.</p>\n *\n * <p>This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied,\n * pasted and stripped down here for maximum performance but does unfortunately duplicate\n * some code.</p>\n *\n * <p>This class is thread-safe but not reentrant. Each thread must allocate its own object.</p>\n *\n * @author Sean Owen\n */\nfinal class AlignmentPatternFinder {\n\n  private final BitMatrix image;\n  private final List<AlignmentPattern> possibleCenters;\n  private final int startX;\n  private final int startY;\n  private final int width;\n  private final int height;\n  private final float moduleSize;\n  private final int[] crossCheckStateCount;\n  private final ResultPointCallback resultPointCallback;\n\n  /**\n   * <p>Creates a finder that will look in a portion of the whole image.</p>\n   *\n   * @param image image to search\n   * @param startX left column from which to start searching\n   * @param startY top row from which to start searching\n   * @param width width of region to search\n   * @param height height of region to search\n   * @param moduleSize estimated module size so far\n   */\n  AlignmentPatternFinder(BitMatrix image,\n                         int startX,\n                         int startY,\n                         int width,\n                         int height,\n                         float moduleSize,\n                         ResultPointCallback resultPointCallback) {\n    this.image = image;\n    this.possibleCenters = new ArrayList<>(5);\n    this.startX = startX;\n    this.startY = startY;\n    this.width = width;\n    this.height = height;\n    this.moduleSize = moduleSize;\n    this.crossCheckStateCount = new int[3];\n    this.resultPointCallback = resultPointCallback;\n  }\n\n  /**\n   * <p>This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since\n   * it's pretty performance-critical and so is written to be fast foremost.</p>\n   *\n   * @return {@link AlignmentPattern} if found\n   * @throws NotFoundException if not found\n   */\n  AlignmentPattern find() throws NotFoundException {\n    int startX = this.startX;\n    int height = this.height;\n    int maxJ = startX + width;\n    int middleI = startY + (height / 2);\n    // We are looking for black/white/black modules in 1:1:1 ratio;\n    // this tracks the number of black/white/black modules seen so far\n    int[] stateCount = new int[3];\n    for (int iGen = 0; iGen < height; iGen++) {\n      // Search from middle outwards\n      int i = middleI + ((iGen & 0x01) == 0 ? (iGen + 1) / 2 : -((iGen + 1) / 2));\n      stateCount[0] = 0;\n      stateCount[1] = 0;\n      stateCount[2] = 0;\n      int j = startX;\n      // Burn off leading white pixels before anything else; if we start in the middle of\n      // a white run, it doesn't make sense to count its length, since we don't know if the\n      // white run continued to the left of the start point\n      while (j < maxJ && !image.get(j, i)) {\n        j++;\n      }\n      int currentState = 0;\n      while (j < maxJ) {\n        if (image.get(j, i)) {\n          // Black pixel\n          if (currentState == 1) { // Counting black pixels\n            stateCount[1]++;\n          } else { // Counting white pixels\n            if (currentState == 2) { // A winner?\n              if (foundPatternCross(stateCount)) { // Yes\n                AlignmentPattern confirmed = handlePossibleCenter(stateCount, i, j);\n                if (confirmed != null) {\n                  return confirmed;\n                }\n              }\n              stateCount[0] = stateCount[2];\n              stateCount[1] = 1;\n              stateCount[2] = 0;\n              currentState = 1;\n            } else {\n              stateCount[++currentState]++;\n            }\n          }\n        } else { // White pixel\n          if (currentState == 1) { // Counting black pixels\n            currentState++;\n          }\n          stateCount[currentState]++;\n        }\n        j++;\n      }\n      if (foundPatternCross(stateCount)) {\n        AlignmentPattern confirmed = handlePossibleCenter(stateCount, i, maxJ);\n        if (confirmed != null) {\n          return confirmed;\n        }\n      }\n\n    }\n\n    // Hmm, nothing we saw was observed and confirmed twice. If we had\n    // any guess at all, return it.\n    if (!possibleCenters.isEmpty()) {\n      return possibleCenters.get(0);\n    }\n\n    throw NotFoundException.getNotFoundInstance();\n  }\n\n  /**\n   * Given a count of black/white/black pixels just seen and an end position,\n   * figures the location of the center of this black/white/black run.\n   */\n  private static float centerFromEnd(int[] stateCount, int end) {\n    return (end - stateCount[2]) - stateCount[1] / 2.0f;\n  }\n\n  /**\n   * @param stateCount count of black/white/black pixels just read\n   * @return true iff the proportions of the counts is close enough to the 1/1/1 ratios\n   *         used by alignment patterns to be considered a match\n   */\n  private boolean foundPatternCross(int[] stateCount) {\n    float moduleSize = this.moduleSize;\n    float maxVariance = moduleSize / 2.0f;\n    for (int i = 0; i < 3; i++) {\n      if (Math.abs(moduleSize - stateCount[i]) >= maxVariance) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * <p>After a horizontal scan finds a potential alignment pattern, this method\n   * \"cross-checks\" by scanning down vertically through the center of the possible\n   * alignment pattern to see if the same proportion is detected.</p>\n   *\n   * @param startI row where an alignment pattern was detected\n   * @param centerJ center of the section that appears to cross an alignment pattern\n   * @param maxCount maximum reasonable number of modules that should be\n   * observed in any reading state, based on the results of the horizontal scan\n   * @return vertical center of alignment pattern, or {@link Float#NaN} if not found\n   */\n  private float crossCheckVertical(int startI, int centerJ, int maxCount,\n      int originalStateCountTotal) {\n    BitMatrix image = this.image;\n\n    int maxI = image.getHeight();\n    int[] stateCount = crossCheckStateCount;\n    stateCount[0] = 0;\n    stateCount[1] = 0;\n    stateCount[2] = 0;\n\n    // Start counting up from center\n    int i = startI;\n    while (i >= 0 && image.get(centerJ, i) && stateCount[1] <= maxCount) {\n      stateCount[1]++;\n      i--;\n    }\n    // If already too many modules in this state or ran off the edge:\n    if (i < 0 || stateCount[1] > maxCount) {\n      return Float.NaN;\n    }\n    while (i >= 0 && !image.get(centerJ, i) && stateCount[0] <= maxCount) {\n      stateCount[0]++;\n      i--;\n    }\n    if (stateCount[0] > maxCount) {\n      return Float.NaN;\n    }\n\n    // Now also count down from center\n    i = startI + 1;\n    while (i < maxI && image.get(centerJ, i) && stateCount[1] <= maxCount) {\n      stateCount[1]++;\n      i++;\n    }\n    if (i == maxI || stateCount[1] > maxCount) {\n      return Float.NaN;\n    }\n    while (i < maxI && !image.get(centerJ, i) && stateCount[2] <= maxCount) {\n      stateCount[2]++;\n      i++;\n    }\n    if (stateCount[2] > maxCount) {\n      return Float.NaN;\n    }\n\n    int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2];\n    if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) {\n      return Float.NaN;\n    }\n\n    return foundPatternCross(stateCount) ? centerFromEnd(stateCount, i) : Float.NaN;\n  }\n\n  /**\n   * <p>This is called when a horizontal scan finds a possible alignment pattern. It will\n   * cross check with a vertical scan, and if successful, will see if this pattern had been\n   * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have\n   * found the alignment pattern.</p>\n   *\n   * @param stateCount reading state module counts from horizontal scan\n   * @param i row where alignment pattern may be found\n   * @param j end of possible alignment pattern in row\n   * @return {@link AlignmentPattern} if we have found the same pattern twice, or null if not\n   */\n  private AlignmentPattern handlePossibleCenter(int[] stateCount, int i, int j) {\n    int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2];\n    float centerJ = centerFromEnd(stateCount, j);\n    float centerI = crossCheckVertical(i, (int) centerJ, 2 * stateCount[1], stateCountTotal);\n    if (!Float.isNaN(centerI)) {\n      float estimatedModuleSize = (stateCount[0] + stateCount[1] + stateCount[2]) / 3.0f;\n      for (AlignmentPattern center : possibleCenters) {\n        // Look for about the same center and module size:\n        if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) {\n          return center.combineEstimate(centerI, centerJ, estimatedModuleSize);\n        }\n      }\n      // Hadn't found this before; save it\n      AlignmentPattern point = new AlignmentPattern(centerJ, centerI, estimatedModuleSize);\n      possibleCenters.add(point);\n      if (resultPointCallback != null) {\n        resultPointCallback.foundPossibleResultPoint(point);\n      }\n    }\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/detector/Detector.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.detector;\n\nimport peergos.shared.zxing.DecodeHintType;\nimport peergos.shared.zxing.FormatException;\nimport peergos.shared.zxing.NotFoundException;\nimport peergos.shared.zxing.ResultPoint;\nimport peergos.shared.zxing.ResultPointCallback;\nimport peergos.shared.zxing.common.BitMatrix;\nimport peergos.shared.zxing.common.DetectorResult;\nimport peergos.shared.zxing.common.GridSampler;\nimport peergos.shared.zxing.common.PerspectiveTransform;\nimport peergos.shared.zxing.common.detector.MathUtils;\nimport peergos.shared.zxing.qrcode.decoder.Version;\n\nimport java.util.Map;\n\n/**\n * <p>Encapsulates logic that can detect a QR Code in an image, even if the QR Code\n * is rotated or skewed, or partially obscured.</p>\n *\n * @author Sean Owen\n */\npublic class Detector {\n\n  private final BitMatrix image;\n  private ResultPointCallback resultPointCallback;\n\n  public Detector(BitMatrix image) {\n    this.image = image;\n  }\n\n  protected final BitMatrix getImage() {\n    return image;\n  }\n\n  protected final ResultPointCallback getResultPointCallback() {\n    return resultPointCallback;\n  }\n\n  /**\n   * <p>Detects a QR Code in an image.</p>\n   *\n   * @return {@link DetectorResult} encapsulating results of detecting a QR Code\n   * @throws NotFoundException if QR Code cannot be found\n   * @throws FormatException if a QR Code cannot be decoded\n   */\n  public DetectorResult detect() throws NotFoundException, FormatException {\n    return detect(null);\n  }\n\n  /**\n   * <p>Detects a QR Code in an image.</p>\n   *\n   * @param hints optional hints to detector\n   * @return {@link DetectorResult} encapsulating results of detecting a QR Code\n   * @throws NotFoundException if QR Code cannot be found\n   * @throws FormatException if a QR Code cannot be decoded\n   */\n  public final DetectorResult detect(Map<DecodeHintType,?> hints) throws NotFoundException, FormatException {\n\n    resultPointCallback = hints == null ? null :\n        (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);\n\n    FinderPatternFinder finder = new FinderPatternFinder(image, resultPointCallback);\n    FinderPatternInfo info = finder.find(hints);\n\n    return processFinderPatternInfo(info);\n  }\n\n  protected final DetectorResult processFinderPatternInfo(FinderPatternInfo info)\n      throws NotFoundException, FormatException {\n\n    FinderPattern topLeft = info.getTopLeft();\n    FinderPattern topRight = info.getTopRight();\n    FinderPattern bottomLeft = info.getBottomLeft();\n\n    float moduleSize = calculateModuleSize(topLeft, topRight, bottomLeft);\n    if (moduleSize < 1.0f) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n    int dimension = computeDimension(topLeft, topRight, bottomLeft, moduleSize);\n    Version provisionalVersion = Version.getProvisionalVersionForDimension(dimension);\n    int modulesBetweenFPCenters = provisionalVersion.getDimensionForVersion() - 7;\n\n    AlignmentPattern alignmentPattern = null;\n    // Anything above version 1 has an alignment pattern\n    if (provisionalVersion.getAlignmentPatternCenters().length > 0) {\n\n      // Guess where a \"bottom right\" finder pattern would have been\n      float bottomRightX = topRight.getX() - topLeft.getX() + bottomLeft.getX();\n      float bottomRightY = topRight.getY() - topLeft.getY() + bottomLeft.getY();\n\n      // Estimate that alignment pattern is closer by 3 modules\n      // from \"bottom right\" to known top left location\n      float correctionToTopLeft = 1.0f - 3.0f / modulesBetweenFPCenters;\n      int estAlignmentX = (int) (topLeft.getX() + correctionToTopLeft * (bottomRightX - topLeft.getX()));\n      int estAlignmentY = (int) (topLeft.getY() + correctionToTopLeft * (bottomRightY - topLeft.getY()));\n\n      // Kind of arbitrary -- expand search radius before giving up\n      for (int i = 4; i <= 16; i <<= 1) {\n        try {\n          alignmentPattern = findAlignmentInRegion(moduleSize,\n              estAlignmentX,\n              estAlignmentY,\n              i);\n          break;\n        } catch (NotFoundException re) {\n          // try next round\n        }\n      }\n      // If we didn't find alignment pattern... well try anyway without it\n    }\n\n    PerspectiveTransform transform =\n        createTransform(topLeft, topRight, bottomLeft, alignmentPattern, dimension);\n\n    BitMatrix bits = sampleGrid(image, transform, dimension);\n\n    ResultPoint[] points;\n    if (alignmentPattern == null) {\n      points = new ResultPoint[]{bottomLeft, topLeft, topRight};\n    } else {\n      points = new ResultPoint[]{bottomLeft, topLeft, topRight, alignmentPattern};\n    }\n    return new DetectorResult(bits, points);\n  }\n\n  private static PerspectiveTransform createTransform(ResultPoint topLeft,\n                                                      ResultPoint topRight,\n                                                      ResultPoint bottomLeft,\n                                                      ResultPoint alignmentPattern,\n                                                      int dimension) {\n    float dimMinusThree = dimension - 3.5f;\n    float bottomRightX;\n    float bottomRightY;\n    float sourceBottomRightX;\n    float sourceBottomRightY;\n    if (alignmentPattern != null) {\n      bottomRightX = alignmentPattern.getX();\n      bottomRightY = alignmentPattern.getY();\n      sourceBottomRightX = dimMinusThree - 3.0f;\n      sourceBottomRightY = sourceBottomRightX;\n    } else {\n      // Don't have an alignment pattern, just make up the bottom-right point\n      bottomRightX = (topRight.getX() - topLeft.getX()) + bottomLeft.getX();\n      bottomRightY = (topRight.getY() - topLeft.getY()) + bottomLeft.getY();\n      sourceBottomRightX = dimMinusThree;\n      sourceBottomRightY = dimMinusThree;\n    }\n\n    return PerspectiveTransform.quadrilateralToQuadrilateral(\n        3.5f,\n        3.5f,\n        dimMinusThree,\n        3.5f,\n        sourceBottomRightX,\n        sourceBottomRightY,\n        3.5f,\n        dimMinusThree,\n        topLeft.getX(),\n        topLeft.getY(),\n        topRight.getX(),\n        topRight.getY(),\n        bottomRightX,\n        bottomRightY,\n        bottomLeft.getX(),\n        bottomLeft.getY());\n  }\n\n  private static BitMatrix sampleGrid(BitMatrix image,\n                                      PerspectiveTransform transform,\n                                      int dimension) throws NotFoundException {\n\n    GridSampler sampler = GridSampler.getInstance();\n    return sampler.sampleGrid(image, dimension, dimension, transform);\n  }\n\n  /**\n   * <p>Computes the dimension (number of modules on a size) of the QR Code based on the position\n   * of the finder patterns and estimated module size.</p>\n   */\n  private static int computeDimension(ResultPoint topLeft,\n                                      ResultPoint topRight,\n                                      ResultPoint bottomLeft,\n                                      float moduleSize) throws NotFoundException {\n    int tltrCentersDimension = MathUtils.round(ResultPoint.distance(topLeft, topRight) / moduleSize);\n    int tlblCentersDimension = MathUtils.round(ResultPoint.distance(topLeft, bottomLeft) / moduleSize);\n    int dimension = ((tltrCentersDimension + tlblCentersDimension) / 2) + 7;\n    switch (dimension & 0x03) { // mod 4\n      case 0:\n        dimension++;\n        break;\n        // 1? do nothing\n      case 2:\n        dimension--;\n        break;\n      case 3:\n        throw NotFoundException.getNotFoundInstance();\n    }\n    return dimension;\n  }\n\n  /**\n   * <p>Computes an average estimated module size based on estimated derived from the positions\n   * of the three finder patterns.</p>\n   *\n   * @param topLeft detected top-left finder pattern center\n   * @param topRight detected top-right finder pattern center\n   * @param bottomLeft detected bottom-left finder pattern center\n   * @return estimated module size\n   */\n  protected final float calculateModuleSize(ResultPoint topLeft,\n                                            ResultPoint topRight,\n                                            ResultPoint bottomLeft) {\n    // Take the average\n    return (calculateModuleSizeOneWay(topLeft, topRight) +\n        calculateModuleSizeOneWay(topLeft, bottomLeft)) / 2.0f;\n  }\n\n  /**\n   * <p>Estimates module size based on two finder patterns -- it uses\n   * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the\n   * width of each, measuring along the axis between their centers.</p>\n   */\n  private float calculateModuleSizeOneWay(ResultPoint pattern, ResultPoint otherPattern) {\n    float moduleSizeEst1 = sizeOfBlackWhiteBlackRunBothWays((int) pattern.getX(),\n        (int) pattern.getY(),\n        (int) otherPattern.getX(),\n        (int) otherPattern.getY());\n    float moduleSizeEst2 = sizeOfBlackWhiteBlackRunBothWays((int) otherPattern.getX(),\n        (int) otherPattern.getY(),\n        (int) pattern.getX(),\n        (int) pattern.getY());\n    if (Float.isNaN(moduleSizeEst1)) {\n      return moduleSizeEst2 / 7.0f;\n    }\n    if (Float.isNaN(moduleSizeEst2)) {\n      return moduleSizeEst1 / 7.0f;\n    }\n    // Average them, and divide by 7 since we've counted the width of 3 black modules,\n    // and 1 white and 1 black module on either side. Ergo, divide sum by 14.\n    return (moduleSizeEst1 + moduleSizeEst2) / 14.0f;\n  }\n\n  /**\n   * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of\n   * a finder pattern by looking for a black-white-black run from the center in the direction\n   * of another point (another finder pattern center), and in the opposite direction too.\n   */\n  private float sizeOfBlackWhiteBlackRunBothWays(int fromX, int fromY, int toX, int toY) {\n\n    float result = sizeOfBlackWhiteBlackRun(fromX, fromY, toX, toY);\n\n    // Now count other way -- don't run off image though of course\n    float scale = 1.0f;\n    int otherToX = fromX - (toX - fromX);\n    if (otherToX < 0) {\n      scale = fromX / (float) (fromX - otherToX);\n      otherToX = 0;\n    } else if (otherToX >= image.getWidth()) {\n      scale = (image.getWidth() - 1 - fromX) / (float) (otherToX - fromX);\n      otherToX = image.getWidth() - 1;\n    }\n    int otherToY = (int) (fromY - (toY - fromY) * scale);\n\n    scale = 1.0f;\n    if (otherToY < 0) {\n      scale = fromY / (float) (fromY - otherToY);\n      otherToY = 0;\n    } else if (otherToY >= image.getHeight()) {\n      scale = (image.getHeight() - 1 - fromY) / (float) (otherToY - fromY);\n      otherToY = image.getHeight() - 1;\n    }\n    otherToX = (int) (fromX + (otherToX - fromX) * scale);\n\n    result += sizeOfBlackWhiteBlackRun(fromX, fromY, otherToX, otherToY);\n\n    // Middle pixel is double-counted this way; subtract 1\n    return result - 1.0f;\n  }\n\n  /**\n   * <p>This method traces a line from a point in the image, in the direction towards another point.\n   * It begins in a black region, and keeps going until it finds white, then black, then white again.\n   * It reports the distance from the start to this point.</p>\n   *\n   * <p>This is used when figuring out how wide a finder pattern is, when the finder pattern\n   * may be skewed or rotated.</p>\n   */\n  private float sizeOfBlackWhiteBlackRun(int fromX, int fromY, int toX, int toY) {\n    // Mild variant of Bresenham's algorithm;\n    // see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm\n    boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX);\n    if (steep) {\n      int temp = fromX;\n      fromX = fromY;\n      fromY = temp;\n      temp = toX;\n      toX = toY;\n      toY = temp;\n    }\n\n    int dx = Math.abs(toX - fromX);\n    int dy = Math.abs(toY - fromY);\n    int error = -dx / 2;\n    int xstep = fromX < toX ? 1 : -1;\n    int ystep = fromY < toY ? 1 : -1;\n\n    // In black pixels, looking for white, first or second time.\n    int state = 0;\n    // Loop up until x == toX, but not beyond\n    int xLimit = toX + xstep;\n    for (int x = fromX, y = fromY; x != xLimit; x += xstep) {\n      int realX = steep ? y : x;\n      int realY = steep ? x : y;\n\n      // Does current pixel mean we have moved white to black or vice versa?\n      // Scanning black in state 0,2 and white in state 1, so if we find the wrong\n      // color, advance to next state or end if we are in state 2 already\n      if ((state == 1) == image.get(realX, realY)) {\n        if (state == 2) {\n          return MathUtils.distance(x, y, fromX, fromY);\n        }\n        state++;\n      }\n\n      error += dy;\n      if (error > 0) {\n        if (y == toY) {\n          break;\n        }\n        y += ystep;\n        error -= dx;\n      }\n    }\n    // Found black-white-black; give the benefit of the doubt that the next pixel outside the image\n    // is \"white\" so this last point at (toX+xStep,toY) is the right ending. This is really a\n    // small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this.\n    if (state == 2) {\n      return MathUtils.distance(toX + xstep, toY, fromX, fromY);\n    }\n    // else we didn't find even black-white-black; no estimate is really possible\n    return Float.NaN;\n  }\n\n  /**\n   * <p>Attempts to locate an alignment pattern in a limited region of the image, which is\n   * guessed to contain it. This method uses {@link AlignmentPattern}.</p>\n   *\n   * @param overallEstModuleSize estimated module size so far\n   * @param estAlignmentX x coordinate of center of area probably containing alignment pattern\n   * @param estAlignmentY y coordinate of above\n   * @param allowanceFactor number of pixels in all directions to search from the center\n   * @return {@link AlignmentPattern} if found, or null otherwise\n   * @throws NotFoundException if an unexpected error occurs during detection\n   */\n  protected final AlignmentPattern findAlignmentInRegion(float overallEstModuleSize,\n                                                         int estAlignmentX,\n                                                         int estAlignmentY,\n                                                         float allowanceFactor)\n      throws NotFoundException {\n    // Look for an alignment pattern (3 modules in size) around where it\n    // should be\n    int allowance = (int) (allowanceFactor * overallEstModuleSize);\n    int alignmentAreaLeftX = Math.max(0, estAlignmentX - allowance);\n    int alignmentAreaRightX = Math.min(image.getWidth() - 1, estAlignmentX + allowance);\n    if (alignmentAreaRightX - alignmentAreaLeftX < overallEstModuleSize * 3) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n\n    int alignmentAreaTopY = Math.max(0, estAlignmentY - allowance);\n    int alignmentAreaBottomY = Math.min(image.getHeight() - 1, estAlignmentY + allowance);\n    if (alignmentAreaBottomY - alignmentAreaTopY < overallEstModuleSize * 3) {\n      throw NotFoundException.getNotFoundInstance();\n    }\n\n    AlignmentPatternFinder alignmentFinder =\n        new AlignmentPatternFinder(\n            image,\n            alignmentAreaLeftX,\n            alignmentAreaTopY,\n            alignmentAreaRightX - alignmentAreaLeftX,\n            alignmentAreaBottomY - alignmentAreaTopY,\n            overallEstModuleSize,\n            resultPointCallback);\n    return alignmentFinder.find();\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/detector/FinderPattern.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.detector;\n\nimport peergos.shared.zxing.ResultPoint;\n\n/**\n * <p>Encapsulates a finder pattern, which are the three square patterns found in\n * the corners of QR Codes. It also encapsulates a count of similar finder patterns,\n * as a convenience to the finder's bookkeeping.</p>\n *\n * @author Sean Owen\n */\npublic final class FinderPattern extends ResultPoint {\n\n  private final float estimatedModuleSize;\n  private final int count;\n\n  FinderPattern(float posX, float posY, float estimatedModuleSize) {\n    this(posX, posY, estimatedModuleSize, 1);\n  }\n\n  private FinderPattern(float posX, float posY, float estimatedModuleSize, int count) {\n    super(posX, posY);\n    this.estimatedModuleSize = estimatedModuleSize;\n    this.count = count;\n  }\n\n  public float getEstimatedModuleSize() {\n    return estimatedModuleSize;\n  }\n\n  int getCount() {\n    return count;\n  }\n\n  /*\n  void incrementCount() {\n    this.count++;\n  }\n   */\n\n  /**\n   * <p>Determines if this finder pattern \"about equals\" a finder pattern at the stated\n   * position and size -- meaning, it is at nearly the same center with nearly the same size.</p>\n   */\n  boolean aboutEquals(float moduleSize, float i, float j) {\n    if (Math.abs(i - getY()) <= moduleSize && Math.abs(j - getX()) <= moduleSize) {\n      float moduleSizeDiff = Math.abs(moduleSize - estimatedModuleSize);\n      return moduleSizeDiff <= 1.0f || moduleSizeDiff <= estimatedModuleSize;\n    }\n    return false;\n  }\n\n  /**\n   * Combines this object's current estimate of a finder pattern position and module size\n   * with a new estimate. It returns a new {@code FinderPattern} containing a weighted average\n   * based on count.\n   */\n  FinderPattern combineEstimate(float i, float j, float newModuleSize) {\n    int combinedCount = count + 1;\n    float combinedX = (count * getX() + j) / combinedCount;\n    float combinedY = (count * getY() + i) / combinedCount;\n    float combinedModuleSize = (count * estimatedModuleSize + newModuleSize) / combinedCount;\n    return new FinderPattern(combinedX, combinedY, combinedModuleSize, combinedCount);\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/detector/FinderPatternFinder.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.detector;\n\nimport peergos.shared.zxing.DecodeHintType;\nimport peergos.shared.zxing.NotFoundException;\nimport peergos.shared.zxing.ResultPoint;\nimport peergos.shared.zxing.ResultPointCallback;\nimport peergos.shared.zxing.common.BitMatrix;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * <p>This class attempts to find finder patterns in a QR Code. Finder patterns are the square\n * markers at three corners of a QR Code.</p>\n *\n * <p>This class is thread-safe but not reentrant. Each thread must allocate its own object.\n *\n * @author Sean Owen\n */\npublic class FinderPatternFinder {\n\n  private static final int CENTER_QUORUM = 2;\n  private static final EstimatedModuleComparator moduleComparator = new EstimatedModuleComparator();\n  protected static final int MIN_SKIP = 3; // 1 pixel/module times 3 modules/center\n  protected static final int MAX_MODULES = 97; // support up to version 20 for mobile clients\n\n  private final BitMatrix image;\n  private final List<FinderPattern> possibleCenters;\n  private boolean hasSkipped;\n  private final int[] crossCheckStateCount;\n  private final ResultPointCallback resultPointCallback;\n\n  /**\n   * <p>Creates a finder that will search the image for three finder patterns.</p>\n   *\n   * @param image image to search\n   */\n  public FinderPatternFinder(BitMatrix image) {\n    this(image, null);\n  }\n\n  public FinderPatternFinder(BitMatrix image, ResultPointCallback resultPointCallback) {\n    this.image = image;\n    this.possibleCenters = new ArrayList<>();\n    this.crossCheckStateCount = new int[5];\n    this.resultPointCallback = resultPointCallback;\n  }\n\n  protected final BitMatrix getImage() {\n    return image;\n  }\n\n  protected final List<FinderPattern> getPossibleCenters() {\n    return possibleCenters;\n  }\n\n  final FinderPatternInfo find(Map<DecodeHintType,?> hints) throws NotFoundException {\n    boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);\n    int maxI = image.getHeight();\n    int maxJ = image.getWidth();\n    // We are looking for black/white/black/white/black modules in\n    // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far\n\n    // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the\n    // image, and then account for the center being 3 modules in size. This gives the smallest\n    // number of pixels the center could be, so skip this often. When trying harder, look for all\n    // QR versions regardless of how dense they are.\n    int iSkip = (3 * maxI) / (4 * MAX_MODULES);\n    if (iSkip < MIN_SKIP || tryHarder) {\n      iSkip = MIN_SKIP;\n    }\n\n    boolean done = false;\n    int[] stateCount = new int[5];\n    for (int i = iSkip - 1; i < maxI && !done; i += iSkip) {\n      // Get a row of black/white values\n      clearCounts(stateCount);\n      int currentState = 0;\n      for (int j = 0; j < maxJ; j++) {\n        if (image.get(j, i)) {\n          // Black pixel\n          if ((currentState & 1) == 1) { // Counting white pixels\n            currentState++;\n          }\n          stateCount[currentState]++;\n        } else { // White pixel\n          if ((currentState & 1) == 0) { // Counting black pixels\n            if (currentState == 4) { // A winner?\n              if (foundPatternCross(stateCount)) { // Yes\n                boolean confirmed = handlePossibleCenter(stateCount, i, j);\n                if (confirmed) {\n                  // Start examining every other line. Checking each line turned out to be too\n                  // expensive and didn't improve performance.\n                  iSkip = 2;\n                  if (hasSkipped) {\n                    done = haveMultiplyConfirmedCenters();\n                  } else {\n                    int rowSkip = findRowSkip();\n                    if (rowSkip > stateCount[2]) {\n                      // Skip rows between row of lower confirmed center\n                      // and top of presumed third confirmed center\n                      // but back up a bit to get a full chance of detecting\n                      // it, entire width of center of finder pattern\n\n                      // Skip by rowSkip, but back off by stateCount[2] (size of last center\n                      // of pattern we saw) to be conservative, and also back off by iSkip which\n                      // is about to be re-added\n                      i += rowSkip - stateCount[2] - iSkip;\n                      j = maxJ - 1;\n                    }\n                  }\n                } else {\n                  shiftCounts2(stateCount);\n                  currentState = 3;\n                  continue;\n                }\n                // Clear state to start looking again\n                currentState = 0;\n                clearCounts(stateCount);\n              } else { // No, shift counts back by two\n                shiftCounts2(stateCount);\n                currentState = 3;\n              }\n            } else {\n              stateCount[++currentState]++;\n            }\n          } else { // Counting white pixels\n            stateCount[currentState]++;\n          }\n        }\n      }\n      if (foundPatternCross(stateCount)) {\n        boolean confirmed = handlePossibleCenter(stateCount, i, maxJ);\n        if (confirmed) {\n          iSkip = stateCount[0];\n          if (hasSkipped) {\n            // Found a third one\n            done = haveMultiplyConfirmedCenters();\n          }\n        }\n      }\n    }\n\n    FinderPattern[] patternInfo = selectBestPatterns();\n    ResultPoint.orderBestPatterns(patternInfo);\n\n    return new FinderPatternInfo(patternInfo);\n  }\n\n  /**\n   * Given a count of black/white/black/white/black pixels just seen and an end position,\n   * figures the location of the center of this run.\n   */\n  private static float centerFromEnd(int[] stateCount, int end) {\n    return (end - stateCount[4] - stateCount[3]) - stateCount[2] / 2.0f;\n  }\n\n  /**\n   * @param stateCount count of black/white/black/white/black pixels just read\n   * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios\n   *         used by finder patterns to be considered a match\n   */\n  protected static boolean foundPatternCross(int[] stateCount) {\n    int totalModuleSize = 0;\n    for (int i = 0; i < 5; i++) {\n      int count = stateCount[i];\n      if (count == 0) {\n        return false;\n      }\n      totalModuleSize += count;\n    }\n    if (totalModuleSize < 7) {\n      return false;\n    }\n    float moduleSize = totalModuleSize / 7.0f;\n    float maxVariance = moduleSize / 2.0f;\n    // Allow less than 50% variance from 1-1-3-1-1 proportions\n    return\n        Math.abs(moduleSize - stateCount[0]) < maxVariance &&\n        Math.abs(moduleSize - stateCount[1]) < maxVariance &&\n        Math.abs(3.0f * moduleSize - stateCount[2]) < 3 * maxVariance &&\n        Math.abs(moduleSize - stateCount[3]) < maxVariance &&\n        Math.abs(moduleSize - stateCount[4]) < maxVariance;\n  }\n\n  /**\n   * @param stateCount count of black/white/black/white/black pixels just read\n   * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios\n   *         used by finder patterns to be considered a match\n   */\n  protected static boolean foundPatternDiagonal(int[] stateCount) {\n    int totalModuleSize = 0;\n    for (int i = 0; i < 5; i++) {\n      int count = stateCount[i];\n      if (count == 0) {\n        return false;\n      }\n      totalModuleSize += count;\n    }\n    if (totalModuleSize < 7) {\n      return false;\n    }\n    float moduleSize = totalModuleSize / 7.0f;\n    float maxVariance = moduleSize / 1.333f;\n    // Allow less than 75% variance from 1-1-3-1-1 proportions\n    return\n            Math.abs(moduleSize - stateCount[0]) < maxVariance &&\n                    Math.abs(moduleSize - stateCount[1]) < maxVariance &&\n                    Math.abs(3.0f * moduleSize - stateCount[2]) < 3 * maxVariance &&\n                    Math.abs(moduleSize - stateCount[3]) < maxVariance &&\n                    Math.abs(moduleSize - stateCount[4]) < maxVariance;\n  }\n\n  private int[] getCrossCheckStateCount() {\n    clearCounts(crossCheckStateCount);\n    return crossCheckStateCount;\n  }\n\n  protected final void clearCounts(int[] counts) {\n    for (int x = 0; x < counts.length; x++) {\n      counts[x] = 0;\n    }\n  }\n\n  protected final void shiftCounts2(int[] stateCount) {\n    stateCount[0] = stateCount[2];\n    stateCount[1] = stateCount[3];\n    stateCount[2] = stateCount[4];\n    stateCount[3] = 1;\n    stateCount[4] = 0;\n  }\n\n  /**\n   * After a vertical and horizontal scan finds a potential finder pattern, this method\n   * \"cross-cross-cross-checks\" by scanning down diagonally through the center of the possible\n   * finder pattern to see if the same proportion is detected.\n   * \n   * @param centerI row where a finder pattern was detected\n   * @param centerJ center of the section that appears to cross a finder pattern\n   * @return true if proportions are withing expected limits\n   */\n  private boolean crossCheckDiagonal(int centerI, int centerJ) {\n    int[] stateCount = getCrossCheckStateCount();\n\n    // Start counting up, left from center finding black center mass\n    int i = 0;\n    while (centerI >= i && centerJ >= i && image.get(centerJ - i, centerI - i)) {\n      stateCount[2]++;\n      i++;\n    }\n    if (stateCount[2] == 0) {\n      return false;\n    }\n\n    // Continue up, left finding white space\n    while (centerI >= i && centerJ >= i && !image.get(centerJ - i, centerI - i)) {\n      stateCount[1]++;\n      i++;\n    }\n    if (stateCount[1] == 0) {\n      return false;\n    }\n\n    // Continue up, left finding black border\n    while (centerI >= i && centerJ >= i && image.get(centerJ - i, centerI - i)) {\n      stateCount[0]++;\n      i++;\n    }\n    if (stateCount[0] == 0) {\n      return false;\n    }\n\n    int maxI = image.getHeight();\n    int maxJ = image.getWidth();\n\n    // Now also count down, right from center\n    i = 1;\n    while (centerI + i < maxI && centerJ + i < maxJ && image.get(centerJ + i, centerI + i)) {\n      stateCount[2]++;\n      i++;\n    }\n\n    while (centerI + i < maxI && centerJ + i < maxJ && !image.get(centerJ + i, centerI + i)) {\n      stateCount[3]++;\n      i++;\n    }\n    if (stateCount[3] == 0) {\n      return false;\n    }\n\n    while (centerI + i < maxI && centerJ + i < maxJ && image.get(centerJ + i, centerI + i)) {\n      stateCount[4]++;\n      i++;\n    }\n    if (stateCount[4] == 0) {\n      return false;\n    }\n\n    return foundPatternDiagonal(stateCount);\n  }\n\n  /**\n   * <p>After a horizontal scan finds a potential finder pattern, this method\n   * \"cross-checks\" by scanning down vertically through the center of the possible\n   * finder pattern to see if the same proportion is detected.</p>\n   *\n   * @param startI row where a finder pattern was detected\n   * @param centerJ center of the section that appears to cross a finder pattern\n   * @param maxCount maximum reasonable number of modules that should be\n   * observed in any reading state, based on the results of the horizontal scan\n   * @return vertical center of finder pattern, or {@link Float#NaN} if not found\n   */\n  private float crossCheckVertical(int startI, int centerJ, int maxCount,\n      int originalStateCountTotal) {\n    BitMatrix image = this.image;\n\n    int maxI = image.getHeight();\n    int[] stateCount = getCrossCheckStateCount();\n\n    // Start counting up from center\n    int i = startI;\n    while (i >= 0 && image.get(centerJ, i)) {\n      stateCount[2]++;\n      i--;\n    }\n    if (i < 0) {\n      return Float.NaN;\n    }\n    while (i >= 0 && !image.get(centerJ, i) && stateCount[1] <= maxCount) {\n      stateCount[1]++;\n      i--;\n    }\n    // If already too many modules in this state or ran off the edge:\n    if (i < 0 || stateCount[1] > maxCount) {\n      return Float.NaN;\n    }\n    while (i >= 0 && image.get(centerJ, i) && stateCount[0] <= maxCount) {\n      stateCount[0]++;\n      i--;\n    }\n    if (stateCount[0] > maxCount) {\n      return Float.NaN;\n    }\n\n    // Now also count down from center\n    i = startI + 1;\n    while (i < maxI && image.get(centerJ, i)) {\n      stateCount[2]++;\n      i++;\n    }\n    if (i == maxI) {\n      return Float.NaN;\n    }\n    while (i < maxI && !image.get(centerJ, i) && stateCount[3] < maxCount) {\n      stateCount[3]++;\n      i++;\n    }\n    if (i == maxI || stateCount[3] >= maxCount) {\n      return Float.NaN;\n    }\n    while (i < maxI && image.get(centerJ, i) && stateCount[4] < maxCount) {\n      stateCount[4]++;\n      i++;\n    }\n    if (stateCount[4] >= maxCount) {\n      return Float.NaN;\n    }\n\n    // If we found a finder-pattern-like section, but its size is more than 40% different than\n    // the original, assume it's a false positive\n    int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] +\n        stateCount[4];\n    if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) {\n      return Float.NaN;\n    }\n\n    return foundPatternCross(stateCount) ? centerFromEnd(stateCount, i) : Float.NaN;\n  }\n\n  /**\n   * <p>Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical,\n   * except it reads horizontally instead of vertically. This is used to cross-cross\n   * check a vertical cross check and locate the real center of the alignment pattern.</p>\n   */\n  private float crossCheckHorizontal(int startJ, int centerI, int maxCount,\n      int originalStateCountTotal) {\n    BitMatrix image = this.image;\n\n    int maxJ = image.getWidth();\n    int[] stateCount = getCrossCheckStateCount();\n\n    int j = startJ;\n    while (j >= 0 && image.get(j, centerI)) {\n      stateCount[2]++;\n      j--;\n    }\n    if (j < 0) {\n      return Float.NaN;\n    }\n    while (j >= 0 && !image.get(j, centerI) && stateCount[1] <= maxCount) {\n      stateCount[1]++;\n      j--;\n    }\n    if (j < 0 || stateCount[1] > maxCount) {\n      return Float.NaN;\n    }\n    while (j >= 0 && image.get(j, centerI) && stateCount[0] <= maxCount) {\n      stateCount[0]++;\n      j--;\n    }\n    if (stateCount[0] > maxCount) {\n      return Float.NaN;\n    }\n\n    j = startJ + 1;\n    while (j < maxJ && image.get(j, centerI)) {\n      stateCount[2]++;\n      j++;\n    }\n    if (j == maxJ) {\n      return Float.NaN;\n    }\n    while (j < maxJ && !image.get(j, centerI) && stateCount[3] < maxCount) {\n      stateCount[3]++;\n      j++;\n    }\n    if (j == maxJ || stateCount[3] >= maxCount) {\n      return Float.NaN;\n    }\n    while (j < maxJ && image.get(j, centerI) && stateCount[4] < maxCount) {\n      stateCount[4]++;\n      j++;\n    }\n    if (stateCount[4] >= maxCount) {\n      return Float.NaN;\n    }\n\n    // If we found a finder-pattern-like section, but its size is significantly different than\n    // the original, assume it's a false positive\n    int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] +\n        stateCount[4];\n    if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= originalStateCountTotal) {\n      return Float.NaN;\n    }\n\n    return foundPatternCross(stateCount) ? centerFromEnd(stateCount, j) : Float.NaN;\n  }\n\n  /**\n   * @param stateCount reading state module counts from horizontal scan\n   * @param i row where finder pattern may be found\n   * @param j end of possible finder pattern in row\n   * @param pureBarcode ignored\n   * @return true if a finder pattern candidate was found this time\n   * @deprecated only exists for backwards compatibility\n   * @see #handlePossibleCenter(int[], int, int)\n   */\n  @Deprecated\n  protected final boolean handlePossibleCenter(int[] stateCount, int i, int j, boolean pureBarcode) {\n    return handlePossibleCenter(stateCount, i, j);\n  }\n\n  /**\n   * <p>This is called when a horizontal scan finds a possible alignment pattern. It will\n   * cross check with a vertical scan, and if successful, will, ah, cross-cross-check\n   * with another horizontal scan. This is needed primarily to locate the real horizontal\n   * center of the pattern in cases of extreme skew.\n   * And then we cross-cross-cross check with another diagonal scan.</p>\n   *\n   * <p>If that succeeds the finder pattern location is added to a list that tracks\n   * the number of times each location has been nearly-matched as a finder pattern.\n   * Each additional find is more evidence that the location is in fact a finder\n   * pattern center\n   *\n   * @param stateCount reading state module counts from horizontal scan\n   * @param i row where finder pattern may be found\n   * @param j end of possible finder pattern in row\n   * @return true if a finder pattern candidate was found this time\n   */\n  protected final boolean handlePossibleCenter(int[] stateCount, int i, int j) {\n    int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] +\n        stateCount[4];\n    float centerJ = centerFromEnd(stateCount, j);\n    float centerI = crossCheckVertical(i, (int) centerJ, stateCount[2], stateCountTotal);\n    if (!Float.isNaN(centerI)) {\n      // Re-cross check\n      centerJ = crossCheckHorizontal((int) centerJ, (int) centerI, stateCount[2], stateCountTotal);\n      if (!Float.isNaN(centerJ) && crossCheckDiagonal((int) centerI, (int) centerJ)) {\n        float estimatedModuleSize = stateCountTotal / 7.0f;\n        boolean found = false;\n        for (int index = 0; index < possibleCenters.size(); index++) {\n          FinderPattern center = possibleCenters.get(index);\n          // Look for about the same center and module size:\n          if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) {\n            possibleCenters.set(index, center.combineEstimate(centerI, centerJ, estimatedModuleSize));\n            found = true;\n            break;\n          }\n        }\n        if (!found) {\n          FinderPattern point = new FinderPattern(centerJ, centerI, estimatedModuleSize);\n          possibleCenters.add(point);\n          if (resultPointCallback != null) {\n            resultPointCallback.foundPossibleResultPoint(point);\n          }\n        }\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * @return number of rows we could safely skip during scanning, based on the first\n   *         two finder patterns that have been located. In some cases their position will\n   *         allow us to infer that the third pattern must lie below a certain point farther\n   *         down in the image.\n   */\n  private int findRowSkip() {\n    int max = possibleCenters.size();\n    if (max <= 1) {\n      return 0;\n    }\n    ResultPoint firstConfirmedCenter = null;\n    for (FinderPattern center : possibleCenters) {\n      if (center.getCount() >= CENTER_QUORUM) {\n        if (firstConfirmedCenter == null) {\n          firstConfirmedCenter = center;\n        } else {\n          // We have two confirmed centers\n          // How far down can we skip before resuming looking for the next\n          // pattern? In the worst case, only the difference between the\n          // difference in the x / y coordinates of the two centers.\n          // This is the case where you find top left last.\n          hasSkipped = true;\n          return (int) (Math.abs(firstConfirmedCenter.getX() - center.getX()) -\n              Math.abs(firstConfirmedCenter.getY() - center.getY())) / 2;\n        }\n      }\n    }\n    return 0;\n  }\n\n  /**\n   * @return true iff we have found at least 3 finder patterns that have been detected\n   *         at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the\n   *         candidates is \"pretty similar\"\n   */\n  private boolean haveMultiplyConfirmedCenters() {\n    int confirmedCount = 0;\n    float totalModuleSize = 0.0f;\n    int max = possibleCenters.size();\n    for (FinderPattern pattern : possibleCenters) {\n      if (pattern.getCount() >= CENTER_QUORUM) {\n        confirmedCount++;\n        totalModuleSize += pattern.getEstimatedModuleSize();\n      }\n    }\n    if (confirmedCount < 3) {\n      return false;\n    }\n    // OK, we have at least 3 confirmed centers, but, it's possible that one is a \"false positive\"\n    // and that we need to keep looking. We detect this by asking if the estimated module sizes\n    // vary too much. We arbitrarily say that when the total deviation from average exceeds\n    // 5% of the total module size estimates, it's too much.\n    float average = totalModuleSize / max;\n    float totalDeviation = 0.0f;\n    for (FinderPattern pattern : possibleCenters) {\n      totalDeviation += Math.abs(pattern.getEstimatedModuleSize() - average);\n    }\n    return totalDeviation <= 0.05f * totalModuleSize;\n  }\n\n  /**\n   * Get square of distance between a and b.\n   */\n  private static double squaredDistance(FinderPattern a, FinderPattern b) {\n    double x = a.getX() - b.getX();\n    double y = a.getY() - b.getY();\n    return x * x + y * y;\n  }\n\n  /**\n   * @return the 3 best {@link FinderPattern}s from our list of candidates. The \"best\" are\n   *         those have similar module size and form a shape closer to a isosceles right triangle.\n   * @throws NotFoundException if 3 such finder patterns do not exist\n   */\n  private FinderPattern[] selectBestPatterns() throws NotFoundException {\n\n    int startSize = possibleCenters.size();\n    if (startSize < 3) {\n      // Couldn't find enough finder patterns\n      throw NotFoundException.getNotFoundInstance();\n    }\n\n    possibleCenters.sort(moduleComparator);\n\n    double distortion = Double.MAX_VALUE;\n    double[] squares = new double[3];\n    FinderPattern[] bestPatterns = new FinderPattern[3];\n\n    for (int i = 0; i < possibleCenters.size() - 2; i++) {\n      FinderPattern fpi = possibleCenters.get(i);\n      float minModuleSize = fpi.getEstimatedModuleSize();\n\n      for (int j = i + 1; j < possibleCenters.size() - 1; j++) {\n        FinderPattern fpj = possibleCenters.get(j);\n        double squares0 = squaredDistance(fpi, fpj);\n\n        for (int k = j + 1; k < possibleCenters.size(); k++) {\n          FinderPattern fpk = possibleCenters.get(k);\n          float maxModuleSize = fpk.getEstimatedModuleSize();\n          if (maxModuleSize > minModuleSize * 1.4f) {\n            // module size is not similar\n            continue;\n          }\n\n          squares[0] = squares0;\n          squares[1] = squaredDistance(fpj, fpk);\n          squares[2] = squaredDistance(fpi, fpk);\n          Arrays.sort(squares);\n\n          // a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).\n          // Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,\n          // we need to check both two equal sides separately.\n          // The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity\n          // from isosceles right triangle.\n          double d = Math.abs(squares[2] - 2 * squares[1]) + Math.abs(squares[2] - 2 * squares[0]);\n          if (d < distortion) {\n            distortion = d;\n            bestPatterns[0] = fpi;\n            bestPatterns[1] = fpj;\n            bestPatterns[2] = fpk;\n          }\n        }\n      }\n    }\n\n    if (distortion == Double.MAX_VALUE) {\n        throw NotFoundException.getNotFoundInstance();\n    }\n\n    return bestPatterns;\n  }\n\n  /**\n   * <p>Orders by {@link FinderPattern#getEstimatedModuleSize()}</p>\n   */\n  private static final class EstimatedModuleComparator implements Comparator<FinderPattern>, Serializable {\n    @Override\n    public int compare(FinderPattern center1, FinderPattern center2) {\n      return Float.compare(center1.getEstimatedModuleSize(), center2.getEstimatedModuleSize());\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/detector/FinderPatternInfo.java",
    "content": "/*\n * Copyright 2007 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.detector;\n\n/**\n * <p>Encapsulates information about finder patterns in an image, including the location of\n * the three finder patterns, and their estimated module size.</p>\n *\n * @author Sean Owen\n */\npublic final class FinderPatternInfo {\n\n  private final FinderPattern bottomLeft;\n  private final FinderPattern topLeft;\n  private final FinderPattern topRight;\n\n  public FinderPatternInfo(FinderPattern[] patternCenters) {\n    this.bottomLeft = patternCenters[0];\n    this.topLeft = patternCenters[1];\n    this.topRight = patternCenters[2];\n  }\n\n  public FinderPattern getBottomLeft() {\n    return bottomLeft;\n  }\n\n  public FinderPattern getTopLeft() {\n    return topLeft;\n  }\n\n  public FinderPattern getTopRight() {\n    return topRight;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/encoder/BlockPair.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.encoder;\n\nfinal class BlockPair {\n\n  private final byte[] dataBytes;\n  private final byte[] errorCorrectionBytes;\n\n  BlockPair(byte[] data, byte[] errorCorrection) {\n    dataBytes = data;\n    errorCorrectionBytes = errorCorrection;\n  }\n\n  public byte[] getDataBytes() {\n    return dataBytes;\n  }\n\n  public byte[] getErrorCorrectionBytes() {\n    return errorCorrectionBytes;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/encoder/ByteMatrix.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.encoder;\n\nimport java.util.Arrays;\n\n/**\n * JAVAPORT: The original code was a 2D array of ints, but since it only ever gets assigned\n * -1, 0, and 1, I'm going to use less memory and go with bytes.\n *\n * @author dswitkin@google.com (Daniel Switkin)\n */\npublic final class ByteMatrix {\n\n  private final byte[][] bytes;\n  private final int width;\n  private final int height;\n\n  public ByteMatrix(int width, int height) {\n    bytes = new byte[height][width];\n    this.width = width;\n    this.height = height;\n  }\n\n  public int getHeight() {\n    return height;\n  }\n\n  public int getWidth() {\n    return width;\n  }\n\n  public byte get(int x, int y) {\n    return bytes[y][x];\n  }\n\n  /**\n   * @return an internal representation as bytes, in row-major order. array[y][x] represents point (x,y)\n   */\n  public byte[][] getArray() {\n    return bytes;\n  }\n\n  public void set(int x, int y, byte value) {\n    bytes[y][x] = value;\n  }\n\n  public void set(int x, int y, int value) {\n    bytes[y][x] = (byte) value;\n  }\n\n  public void set(int x, int y, boolean value) {\n    bytes[y][x] = (byte) (value ? 1 : 0);\n  }\n\n  public void clear(byte value) {\n    for (byte[] aByte : bytes) {\n      Arrays.fill(aByte, value);\n    }\n  }\n\n  @Override\n  public String toString() {\n    StringBuilder result = new StringBuilder(2 * width * height + 2);\n    for (int y = 0; y < height; ++y) {\n      byte[] bytesY = bytes[y];\n      for (int x = 0; x < width; ++x) {\n        switch (bytesY[x]) {\n          case 0:\n            result.append(\" 0\");\n            break;\n          case 1:\n            result.append(\" 1\");\n            break;\n          default:\n            result.append(\"  \");\n            break;\n        }\n      }\n      result.append('\\n');\n    }\n    return result.toString();\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/encoder/Encoder.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.encoder;\n\nimport peergos.shared.zxing.EncodeHintType;\nimport peergos.shared.zxing.WriterException;\nimport peergos.shared.zxing.common.BitArray;\nimport peergos.shared.zxing.common.CharacterSetECI;\nimport peergos.shared.zxing.common.reedsolomon.GenericGF;\nimport peergos.shared.zxing.common.reedsolomon.ReedSolomonEncoder;\nimport peergos.shared.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport peergos.shared.zxing.qrcode.decoder.Mode;\nimport peergos.shared.zxing.qrcode.decoder.Version;\n\nimport java.io.UnsupportedEncodingException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * @author satorux@google.com (Satoru Takabayashi) - creator\n * @author dswitkin@google.com (Daniel Switkin) - ported from C++\n */\npublic final class Encoder {\n\n  // The original table is defined in the table 5 of JISX0510:2004 (p.19).\n  private static final int[] ALPHANUMERIC_TABLE = {\n      -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x00-0x0f\n      -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x10-0x1f\n      36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43,  // 0x20-0x2f\n      0,   1,  2,  3,  4,  5,  6,  7,  8,  9, 44, -1, -1, -1, -1, -1,  // 0x30-0x3f\n      -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,  // 0x40-0x4f\n      25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1,  // 0x50-0x5f\n  };\n\n  static final String DEFAULT_BYTE_MODE_ENCODING = \"ISO-8859-1\";\n\n  private Encoder() {\n  }\n\n  // The mask penalty calculation is complicated.  See Table 21 of JISX0510:2004 (p.45) for details.\n  // Basically it applies four rules and summate all penalties.\n  private static int calculateMaskPenalty(ByteMatrix matrix) {\n    return MaskUtil.applyMaskPenaltyRule1(matrix)\n        + MaskUtil.applyMaskPenaltyRule2(matrix)\n        + MaskUtil.applyMaskPenaltyRule3(matrix)\n        + MaskUtil.applyMaskPenaltyRule4(matrix);\n  }\n\n  /**\n   * @param content text to encode\n   * @param ecLevel error correction level to use\n   * @return {@link QRCode} representing the encoded QR code\n   * @throws WriterException if encoding can't succeed, because of for example invalid content\n   *   or configuration\n   */\n  public static QRCode encode(String content, ErrorCorrectionLevel ecLevel) throws WriterException {\n    return encode(content, ecLevel, null);\n  }\n\n  public static QRCode encode(String content,\n                              ErrorCorrectionLevel ecLevel,\n                              Map<EncodeHintType,?> hints) throws WriterException {\n\n    // Determine what character encoding has been specified by the caller, if any\n    String encoding = DEFAULT_BYTE_MODE_ENCODING;\n    boolean hasEncodingHint = hints != null && hints.containsKey(EncodeHintType.CHARACTER_SET);\n    if (hasEncodingHint) {\n      encoding = hints.get(EncodeHintType.CHARACTER_SET).toString();\n    }\n\n    // Pick an encoding mode appropriate for the content. Note that this will not attempt to use\n    // multiple modes / segments even if that were more efficient. Twould be nice.\n    Mode mode = chooseMode(content, encoding);\n\n    // This will store the header information, like mode and\n    // length, as well as \"header\" segments like an ECI segment.\n    BitArray headerBits = new BitArray();\n\n    // Append ECI segment if applicable\n    if (mode == Mode.BYTE && hasEncodingHint) {\n      CharacterSetECI eci = CharacterSetECI.getCharacterSetECIByName(encoding);\n      if (eci != null) {\n        appendECI(eci, headerBits);\n      }\n    }\n\n    // Append the FNC1 mode header for GS1 formatted data if applicable\n    boolean hasGS1FormatHint = hints != null && hints.containsKey(EncodeHintType.GS1_FORMAT);\n    if (hasGS1FormatHint && Boolean.valueOf(hints.get(EncodeHintType.GS1_FORMAT).toString())) {\n      // GS1 formatted codes are prefixed with a FNC1 in first position mode header\n      appendModeInfo(Mode.FNC1_FIRST_POSITION, headerBits);\n    }\n\n    // (With ECI in place,) Write the mode marker\n    appendModeInfo(mode, headerBits);\n\n    // Collect data within the main segment, separately, to count its size if needed. Don't add it to\n    // main payload yet.\n    BitArray dataBits = new BitArray();\n    appendBytes(content, mode, dataBits, encoding);\n\n    Version version;\n    if (hints != null && hints.containsKey(EncodeHintType.QR_VERSION)) {\n      int versionNumber = Integer.parseInt(hints.get(EncodeHintType.QR_VERSION).toString());\n      version = Version.getVersionForNumber(versionNumber);\n      int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, version);\n      if (!willFit(bitsNeeded, version, ecLevel)) {\n        throw new WriterException(\"Data too big for requested version\");\n      }\n    } else {\n      version = recommendVersion(ecLevel, mode, headerBits, dataBits);\n    }\n\n    BitArray headerAndDataBits = new BitArray();\n    headerAndDataBits.appendBitArray(headerBits);\n    // Find \"length\" of main segment and write it\n    int numLetters = mode == Mode.BYTE ? dataBits.getSizeInBytes() : content.length();\n    appendLengthInfo(numLetters, version, mode, headerAndDataBits);\n    // Put data together into the overall payload\n    headerAndDataBits.appendBitArray(dataBits);\n\n    Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel);\n    int numDataBytes = version.getTotalCodewords() - ecBlocks.getTotalECCodewords();\n\n    // Terminate the bits properly.\n    terminateBits(numDataBytes, headerAndDataBits);\n\n    // Interleave data bits with error correction code.\n    BitArray finalBits = interleaveWithECBytes(headerAndDataBits,\n                                               version.getTotalCodewords(),\n                                               numDataBytes,\n                                               ecBlocks.getNumBlocks());\n\n    QRCode qrCode = new QRCode();\n\n    qrCode.setECLevel(ecLevel);\n    qrCode.setMode(mode);\n    qrCode.setVersion(version);\n\n    //  Choose the mask pattern and set to \"qrCode\".\n    int dimension = version.getDimensionForVersion();\n    ByteMatrix matrix = new ByteMatrix(dimension, dimension);\n\n    // Enable manual selection of the pattern to be used via hint\n    int maskPattern = -1;\n    if (hints != null && hints.containsKey(EncodeHintType.QR_MASK_PATTERN)) {\n      int hintMaskPattern = Integer.parseInt(hints.get(EncodeHintType.QR_MASK_PATTERN).toString());\n      maskPattern = QRCode.isValidMaskPattern(hintMaskPattern) ? hintMaskPattern : -1;\n    }\n\n    if (maskPattern == -1) {\n      maskPattern = chooseMaskPattern(finalBits, ecLevel, version, matrix);\n    }\n    qrCode.setMaskPattern(maskPattern);\n\n    // Build the matrix and set it to \"qrCode\".\n    MatrixUtil.buildMatrix(finalBits, ecLevel, version, maskPattern, matrix);\n    qrCode.setMatrix(matrix);\n\n    return qrCode;\n  }\n\n  /**\n   * Decides the smallest version of QR code that will contain all of the provided data.\n   *\n   * @throws WriterException if the data cannot fit in any version\n   */\n  private static Version recommendVersion(ErrorCorrectionLevel ecLevel,\n                                          Mode mode,\n                                          BitArray headerBits,\n                                          BitArray dataBits) throws WriterException {\n    // Hard part: need to know version to know how many bits length takes. But need to know how many\n    // bits it takes to know version. First we take a guess at version by assuming version will be\n    // the minimum, 1:\n    int provisionalBitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, Version.getVersionForNumber(1));\n    Version provisionalVersion = chooseVersion(provisionalBitsNeeded, ecLevel);\n\n    // Use that guess to calculate the right version. I am still not sure this works in 100% of cases.\n    int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, provisionalVersion);\n    return chooseVersion(bitsNeeded, ecLevel);\n  }\n\n  private static int calculateBitsNeeded(Mode mode,\n                                         BitArray headerBits,\n                                         BitArray dataBits,\n                                         Version version) {\n    return headerBits.getSize() + mode.getCharacterCountBits(version) + dataBits.getSize();\n  }\n\n  /**\n   * @return the code point of the table used in alphanumeric mode or\n   *  -1 if there is no corresponding code in the table.\n   */\n  static int getAlphanumericCode(int code) {\n    if (code < ALPHANUMERIC_TABLE.length) {\n      return ALPHANUMERIC_TABLE[code];\n    }\n    return -1;\n  }\n\n  public static Mode chooseMode(String content) {\n    return chooseMode(content, null);\n  }\n\n  /**\n   * Choose the best mode by examining the content. Note that 'encoding' is used as a hint;\n   * if it is Shift_JIS, and the input is only double-byte Kanji, then we return {@link Mode#KANJI}.\n   */\n  private static Mode chooseMode(String content, String encoding) {\n    if (\"Shift_JIS\".equals(encoding) && isOnlyDoubleByteKanji(content)) {\n      // Choose Kanji mode if all input are double-byte characters\n      return Mode.KANJI;\n    }\n    boolean hasNumeric = false;\n    boolean hasAlphanumeric = false;\n    for (int i = 0; i < content.length(); ++i) {\n      char c = content.charAt(i);\n      if (c >= '0' && c <= '9') {\n        hasNumeric = true;\n      } else if (getAlphanumericCode(c) != -1) {\n        hasAlphanumeric = true;\n      } else {\n        return Mode.BYTE;\n      }\n    }\n    if (hasAlphanumeric) {\n      return Mode.ALPHANUMERIC;\n    }\n    if (hasNumeric) {\n      return Mode.NUMERIC;\n    }\n    return Mode.BYTE;\n  }\n\n  private static boolean isOnlyDoubleByteKanji(String content) {\n    byte[] bytes;\n    try {\n      bytes = content.getBytes(\"Shift_JIS\");\n    } catch (UnsupportedEncodingException ignored) {\n      return false;\n    }\n    int length = bytes.length;\n    if (length % 2 != 0) {\n      return false;\n    }\n    for (int i = 0; i < length; i += 2) {\n      int byte1 = bytes[i] & 0xFF;\n      if ((byte1 < 0x81 || byte1 > 0x9F) && (byte1 < 0xE0 || byte1 > 0xEB)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private static int chooseMaskPattern(BitArray bits,\n                                       ErrorCorrectionLevel ecLevel,\n                                       Version version,\n                                       ByteMatrix matrix) throws WriterException {\n\n    int minPenalty = Integer.MAX_VALUE;  // Lower penalty is better.\n    int bestMaskPattern = -1;\n    // We try all mask patterns to choose the best one.\n    for (int maskPattern = 0; maskPattern < QRCode.NUM_MASK_PATTERNS; maskPattern++) {\n      MatrixUtil.buildMatrix(bits, ecLevel, version, maskPattern, matrix);\n      int penalty = calculateMaskPenalty(matrix);\n      if (penalty < minPenalty) {\n        minPenalty = penalty;\n        bestMaskPattern = maskPattern;\n      }\n    }\n    return bestMaskPattern;\n  }\n\n  private static Version chooseVersion(int numInputBits, ErrorCorrectionLevel ecLevel) throws WriterException {\n    for (int versionNum = 1; versionNum <= 40; versionNum++) {\n      Version version = Version.getVersionForNumber(versionNum);\n      if (willFit(numInputBits, version, ecLevel)) {\n        return version;\n      }\n    }\n    throw new WriterException(\"Data too big\");\n  }\n\n  /**\n   * @return true if the number of input bits will fit in a code with the specified version and\n   * error correction level.\n   */\n  private static boolean willFit(int numInputBits, Version version, ErrorCorrectionLevel ecLevel) {\n      // In the following comments, we use numbers of Version 7-H.\n      // numBytes = 196\n      int numBytes = version.getTotalCodewords();\n      // getNumECBytes = 130\n      Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel);\n      int numEcBytes = ecBlocks.getTotalECCodewords();\n      // getNumDataBytes = 196 - 130 = 66\n      int numDataBytes = numBytes - numEcBytes;\n      int totalInputBytes = (numInputBits + 7) / 8;\n      return numDataBytes >= totalInputBytes;\n  }\n\n  /**\n   * Terminate bits as described in 8.4.8 and 8.4.9 of JISX0510:2004 (p.24).\n   */\n  static void terminateBits(int numDataBytes, BitArray bits) throws WriterException {\n    int capacity = numDataBytes * 8;\n    if (bits.getSize() > capacity) {\n      throw new WriterException(\"data bits cannot fit in the QR Code\" + bits.getSize() + \" > \" +\n          capacity);\n    }\n    for (int i = 0; i < 4 && bits.getSize() < capacity; ++i) {\n      bits.appendBit(false);\n    }\n    // Append termination bits. See 8.4.8 of JISX0510:2004 (p.24) for details.\n    // If the last byte isn't 8-bit aligned, we'll add padding bits.\n    int numBitsInLastByte = bits.getSize() & 0x07;\n    if (numBitsInLastByte > 0) {\n      for (int i = numBitsInLastByte; i < 8; i++) {\n        bits.appendBit(false);\n      }\n    }\n    // If we have more space, we'll fill the space with padding patterns defined in 8.4.9 (p.24).\n    int numPaddingBytes = numDataBytes - bits.getSizeInBytes();\n    for (int i = 0; i < numPaddingBytes; ++i) {\n      bits.appendBits((i & 0x01) == 0 ? 0xEC : 0x11, 8);\n    }\n    if (bits.getSize() != capacity) {\n      throw new WriterException(\"Bits size does not equal capacity\");\n    }\n  }\n\n  /**\n   * Get number of data bytes and number of error correction bytes for block id \"blockID\". Store\n   * the result in \"numDataBytesInBlock\", and \"numECBytesInBlock\". See table 12 in 8.5.1 of\n   * JISX0510:2004 (p.30)\n   */\n  static void getNumDataBytesAndNumECBytesForBlockID(int numTotalBytes,\n                                                     int numDataBytes,\n                                                     int numRSBlocks,\n                                                     int blockID,\n                                                     int[] numDataBytesInBlock,\n                                                     int[] numECBytesInBlock) throws WriterException {\n    if (blockID >= numRSBlocks) {\n      throw new WriterException(\"Block ID too large\");\n    }\n    // numRsBlocksInGroup2 = 196 % 5 = 1\n    int numRsBlocksInGroup2 = numTotalBytes % numRSBlocks;\n    // numRsBlocksInGroup1 = 5 - 1 = 4\n    int numRsBlocksInGroup1 = numRSBlocks - numRsBlocksInGroup2;\n    // numTotalBytesInGroup1 = 196 / 5 = 39\n    int numTotalBytesInGroup1 = numTotalBytes / numRSBlocks;\n    // numTotalBytesInGroup2 = 39 + 1 = 40\n    int numTotalBytesInGroup2 = numTotalBytesInGroup1 + 1;\n    // numDataBytesInGroup1 = 66 / 5 = 13\n    int numDataBytesInGroup1 = numDataBytes / numRSBlocks;\n    // numDataBytesInGroup2 = 13 + 1 = 14\n    int numDataBytesInGroup2 = numDataBytesInGroup1 + 1;\n    // numEcBytesInGroup1 = 39 - 13 = 26\n    int numEcBytesInGroup1 = numTotalBytesInGroup1 - numDataBytesInGroup1;\n    // numEcBytesInGroup2 = 40 - 14 = 26\n    int numEcBytesInGroup2 = numTotalBytesInGroup2 - numDataBytesInGroup2;\n    // Sanity checks.\n    // 26 = 26\n    if (numEcBytesInGroup1 != numEcBytesInGroup2) {\n      throw new WriterException(\"EC bytes mismatch\");\n    }\n    // 5 = 4 + 1.\n    if (numRSBlocks != numRsBlocksInGroup1 + numRsBlocksInGroup2) {\n      throw new WriterException(\"RS blocks mismatch\");\n    }\n    // 196 = (13 + 26) * 4 + (14 + 26) * 1\n    if (numTotalBytes !=\n        ((numDataBytesInGroup1 + numEcBytesInGroup1) *\n            numRsBlocksInGroup1) +\n            ((numDataBytesInGroup2 + numEcBytesInGroup2) *\n                numRsBlocksInGroup2)) {\n      throw new WriterException(\"Total bytes mismatch\");\n    }\n\n    if (blockID < numRsBlocksInGroup1) {\n      numDataBytesInBlock[0] = numDataBytesInGroup1;\n      numECBytesInBlock[0] = numEcBytesInGroup1;\n    } else {\n      numDataBytesInBlock[0] = numDataBytesInGroup2;\n      numECBytesInBlock[0] = numEcBytesInGroup2;\n    }\n  }\n\n  /**\n   * Interleave \"bits\" with corresponding error correction bytes. On success, store the result in\n   * \"result\". The interleave rule is complicated. See 8.6 of JISX0510:2004 (p.37) for details.\n   */\n  static BitArray interleaveWithECBytes(BitArray bits,\n                                        int numTotalBytes,\n                                        int numDataBytes,\n                                        int numRSBlocks) throws WriterException {\n\n    // \"bits\" must have \"getNumDataBytes\" bytes of data.\n    if (bits.getSizeInBytes() != numDataBytes) {\n      throw new WriterException(\"Number of bits and data bytes does not match\");\n    }\n\n    // Step 1.  Divide data bytes into blocks and generate error correction bytes for them. We'll\n    // store the divided data bytes blocks and error correction bytes blocks into \"blocks\".\n    int dataBytesOffset = 0;\n    int maxNumDataBytes = 0;\n    int maxNumEcBytes = 0;\n\n    // Since, we know the number of reedsolmon blocks, we can initialize the vector with the number.\n    Collection<BlockPair> blocks = new ArrayList<>(numRSBlocks);\n\n    for (int i = 0; i < numRSBlocks; ++i) {\n      int[] numDataBytesInBlock = new int[1];\n      int[] numEcBytesInBlock = new int[1];\n      getNumDataBytesAndNumECBytesForBlockID(\n          numTotalBytes, numDataBytes, numRSBlocks, i,\n          numDataBytesInBlock, numEcBytesInBlock);\n\n      int size = numDataBytesInBlock[0];\n      byte[] dataBytes = new byte[size];\n      bits.toBytes(8 * dataBytesOffset, dataBytes, 0, size);\n      byte[] ecBytes = generateECBytes(dataBytes, numEcBytesInBlock[0]);\n      blocks.add(new BlockPair(dataBytes, ecBytes));\n\n      maxNumDataBytes = Math.max(maxNumDataBytes, size);\n      maxNumEcBytes = Math.max(maxNumEcBytes, ecBytes.length);\n      dataBytesOffset += numDataBytesInBlock[0];\n    }\n    if (numDataBytes != dataBytesOffset) {\n      throw new WriterException(\"Data bytes does not match offset\");\n    }\n\n    BitArray result = new BitArray();\n\n    // First, place data blocks.\n    for (int i = 0; i < maxNumDataBytes; ++i) {\n      for (BlockPair block : blocks) {\n        byte[] dataBytes = block.getDataBytes();\n        if (i < dataBytes.length) {\n          result.appendBits(dataBytes[i], 8);\n        }\n      }\n    }\n    // Then, place error correction blocks.\n    for (int i = 0; i < maxNumEcBytes; ++i) {\n      for (BlockPair block : blocks) {\n        byte[] ecBytes = block.getErrorCorrectionBytes();\n        if (i < ecBytes.length) {\n          result.appendBits(ecBytes[i], 8);\n        }\n      }\n    }\n    if (numTotalBytes != result.getSizeInBytes()) {  // Should be same.\n      throw new WriterException(\"Interleaving error: \" + numTotalBytes + \" and \" +\n          result.getSizeInBytes() + \" differ.\");\n    }\n\n    return result;\n  }\n\n  static byte[] generateECBytes(byte[] dataBytes, int numEcBytesInBlock) {\n    int numDataBytes = dataBytes.length;\n    int[] toEncode = new int[numDataBytes + numEcBytesInBlock];\n    for (int i = 0; i < numDataBytes; i++) {\n      toEncode[i] = dataBytes[i] & 0xFF;\n    }\n    new ReedSolomonEncoder(GenericGF.QR_CODE_FIELD_256).encode(toEncode, numEcBytesInBlock);\n\n    byte[] ecBytes = new byte[numEcBytesInBlock];\n    for (int i = 0; i < numEcBytesInBlock; i++) {\n      ecBytes[i] = (byte) toEncode[numDataBytes + i];\n    }\n    return ecBytes;\n  }\n\n  /**\n   * Append mode info. On success, store the result in \"bits\".\n   */\n  static void appendModeInfo(Mode mode, BitArray bits) {\n    bits.appendBits(mode.getBits(), 4);\n  }\n\n\n  /**\n   * Append length info. On success, store the result in \"bits\".\n   */\n  static void appendLengthInfo(int numLetters, Version version, Mode mode, BitArray bits) throws WriterException {\n    int numBits = mode.getCharacterCountBits(version);\n    if (numLetters >= (1 << numBits)) {\n      throw new WriterException(numLetters + \" is bigger than \" + ((1 << numBits) - 1));\n    }\n    bits.appendBits(numLetters, numBits);\n  }\n\n  /**\n   * Append \"bytes\" in \"mode\" mode (encoding) into \"bits\". On success, store the result in \"bits\".\n   */\n  static void appendBytes(String content,\n                          Mode mode,\n                          BitArray bits,\n                          String encoding) throws WriterException {\n    switch (mode) {\n      case NUMERIC:\n        appendNumericBytes(content, bits);\n        break;\n      case ALPHANUMERIC:\n        appendAlphanumericBytes(content, bits);\n        break;\n      case BYTE:\n        append8BitBytes(content, bits, encoding);\n        break;\n      case KANJI:\n        appendKanjiBytes(content, bits);\n        break;\n      default:\n        throw new WriterException(\"Invalid mode: \" + mode);\n    }\n  }\n\n  static void appendNumericBytes(CharSequence content, BitArray bits) {\n    int length = content.length();\n    int i = 0;\n    while (i < length) {\n      int num1 = content.charAt(i) - '0';\n      if (i + 2 < length) {\n        // Encode three numeric letters in ten bits.\n        int num2 = content.charAt(i + 1) - '0';\n        int num3 = content.charAt(i + 2) - '0';\n        bits.appendBits(num1 * 100 + num2 * 10 + num3, 10);\n        i += 3;\n      } else if (i + 1 < length) {\n        // Encode two numeric letters in seven bits.\n        int num2 = content.charAt(i + 1) - '0';\n        bits.appendBits(num1 * 10 + num2, 7);\n        i += 2;\n      } else {\n        // Encode one numeric letter in four bits.\n        bits.appendBits(num1, 4);\n        i++;\n      }\n    }\n  }\n\n  static void appendAlphanumericBytes(CharSequence content, BitArray bits) throws WriterException {\n    int length = content.length();\n    int i = 0;\n    while (i < length) {\n      int code1 = getAlphanumericCode(content.charAt(i));\n      if (code1 == -1) {\n        throw new WriterException();\n      }\n      if (i + 1 < length) {\n        int code2 = getAlphanumericCode(content.charAt(i + 1));\n        if (code2 == -1) {\n          throw new WriterException();\n        }\n        // Encode two alphanumeric letters in 11 bits.\n        bits.appendBits(code1 * 45 + code2, 11);\n        i += 2;\n      } else {\n        // Encode one alphanumeric letter in six bits.\n        bits.appendBits(code1, 6);\n        i++;\n      }\n    }\n  }\n\n  static void append8BitBytes(String content, BitArray bits, String encoding)\n      throws WriterException {\n    byte[] bytes;\n    try {\n      bytes = content.getBytes(encoding);\n    } catch (UnsupportedEncodingException uee) {\n      throw new WriterException(uee);\n    }\n    for (byte b : bytes) {\n      bits.appendBits(b, 8);\n    }\n  }\n\n  static void appendKanjiBytes(String content, BitArray bits) throws WriterException {\n    byte[] bytes;\n    try {\n      bytes = content.getBytes(\"Shift_JIS\");\n    } catch (UnsupportedEncodingException uee) {\n      throw new WriterException(uee);\n    }\n    if (bytes.length % 2 != 0) {\n      throw new WriterException(\"Kanji byte size not even\");\n    }\n    int maxI = bytes.length - 1; // bytes.length must be even\n    for (int i = 0; i < maxI; i += 2) {\n      int byte1 = bytes[i] & 0xFF;\n      int byte2 = bytes[i + 1] & 0xFF;\n      int code = (byte1 << 8) | byte2;\n      int subtracted = -1;\n      if (code >= 0x8140 && code <= 0x9ffc) {\n        subtracted = code - 0x8140;\n      } else if (code >= 0xe040 && code <= 0xebbf) {\n        subtracted = code - 0xc140;\n      }\n      if (subtracted == -1) {\n        throw new WriterException(\"Invalid byte sequence\");\n      }\n      int encoded = ((subtracted >> 8) * 0xc0) + (subtracted & 0xff);\n      bits.appendBits(encoded, 13);\n    }\n  }\n\n  private static void appendECI(CharacterSetECI eci, BitArray bits) {\n    bits.appendBits(Mode.ECI.getBits(), 4);\n    // This is correct for values up to 127, which is all we need now.\n    bits.appendBits(eci.getValue(), 8);\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/encoder/MaskUtil.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.encoder;\n\n/**\n * @author Satoru Takabayashi\n * @author Daniel Switkin\n * @author Sean Owen\n */\nfinal class MaskUtil {\n\n  // Penalty weights from section 6.8.2.1\n  private static final int N1 = 3;\n  private static final int N2 = 3;\n  private static final int N3 = 40;\n  private static final int N4 = 10;\n\n  private MaskUtil() {\n    // do nothing\n  }\n\n  /**\n   * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and\n   * give penalty to them. Example: 00000 or 11111.\n   */\n  static int applyMaskPenaltyRule1(ByteMatrix matrix) {\n    return applyMaskPenaltyRule1Internal(matrix, true) + applyMaskPenaltyRule1Internal(matrix, false);\n  }\n\n  /**\n   * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give\n   * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a\n   * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block.\n   */\n  static int applyMaskPenaltyRule2(ByteMatrix matrix) {\n    int penalty = 0;\n    byte[][] array = matrix.getArray();\n    int width = matrix.getWidth();\n    int height = matrix.getHeight();\n    for (int y = 0; y < height - 1; y++) {\n      byte[] arrayY = array[y];\n      for (int x = 0; x < width - 1; x++) {\n        int value = arrayY[x];\n        if (value == arrayY[x + 1] && value == array[y + 1][x] && value == array[y + 1][x + 1]) {\n          penalty++;\n        }\n      }\n    }\n    return N2 * penalty;\n  }\n\n  /**\n   * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4\n   * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them.  If we\n   * find patterns like 000010111010000, we give penalty once.\n   */\n  static int applyMaskPenaltyRule3(ByteMatrix matrix) {\n    int numPenalties = 0;\n    byte[][] array = matrix.getArray();\n    int width = matrix.getWidth();\n    int height = matrix.getHeight();\n    for (int y = 0; y < height; y++) {\n      for (int x = 0; x < width; x++) {\n        byte[] arrayY = array[y];  // We can at least optimize this access\n        if (x + 6 < width &&\n            arrayY[x] == 1 &&\n            arrayY[x + 1] == 0 &&\n            arrayY[x + 2] == 1 &&\n            arrayY[x + 3] == 1 &&\n            arrayY[x + 4] == 1 &&\n            arrayY[x + 5] == 0 &&\n            arrayY[x + 6] == 1 &&\n            (isWhiteHorizontal(arrayY, x - 4, x) || isWhiteHorizontal(arrayY, x + 7, x + 11))) {\n          numPenalties++;\n        }\n        if (y + 6 < height &&\n            array[y][x] == 1 &&\n            array[y + 1][x] == 0 &&\n            array[y + 2][x] == 1 &&\n            array[y + 3][x] == 1 &&\n            array[y + 4][x] == 1 &&\n            array[y + 5][x] == 0 &&\n            array[y + 6][x] == 1 &&\n            (isWhiteVertical(array, x, y - 4, y) || isWhiteVertical(array, x, y + 7, y + 11))) {\n          numPenalties++;\n        }\n      }\n    }\n    return numPenalties * N3;\n  }\n\n  private static boolean isWhiteHorizontal(byte[] rowArray, int from, int to) {\n    from = Math.max(from, 0);\n    to = Math.min(to, rowArray.length);\n    for (int i = from; i < to; i++) {\n      if (rowArray[i] == 1) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private static boolean isWhiteVertical(byte[][] array, int col, int from, int to) {\n    from = Math.max(from, 0);\n    to = Math.min(to, array.length);\n    for (int i = from; i < to; i++) {\n      if (array[i][col] == 1) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give\n   * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance.\n   */\n  static int applyMaskPenaltyRule4(ByteMatrix matrix) {\n    int numDarkCells = 0;\n    byte[][] array = matrix.getArray();\n    int width = matrix.getWidth();\n    int height = matrix.getHeight();\n    for (int y = 0; y < height; y++) {\n      byte[] arrayY = array[y];\n      for (int x = 0; x < width; x++) {\n        if (arrayY[x] == 1) {\n          numDarkCells++;\n        }\n      }\n    }\n    int numTotalCells = matrix.getHeight() * matrix.getWidth();\n    int fivePercentVariances = Math.abs(numDarkCells * 2 - numTotalCells) * 10 / numTotalCells;\n    return fivePercentVariances * N4;\n  }\n\n  /**\n   * Return the mask bit for \"getMaskPattern\" at \"x\" and \"y\". See 8.8 of JISX0510:2004 for mask\n   * pattern conditions.\n   */\n  static boolean getDataMaskBit(int maskPattern, int x, int y) {\n    int intermediate;\n    int temp;\n    switch (maskPattern) {\n      case 0:\n        intermediate = (y + x) & 0x1;\n        break;\n      case 1:\n        intermediate = y & 0x1;\n        break;\n      case 2:\n        intermediate = x % 3;\n        break;\n      case 3:\n        intermediate = (y + x) % 3;\n        break;\n      case 4:\n        intermediate = ((y / 2) + (x / 3)) & 0x1;\n        break;\n      case 5:\n        temp = y * x;\n        intermediate = (temp & 0x1) + (temp % 3);\n        break;\n      case 6:\n        temp = y * x;\n        intermediate = ((temp & 0x1) + (temp % 3)) & 0x1;\n        break;\n      case 7:\n        temp = y * x;\n        intermediate = ((temp % 3) + ((y + x) & 0x1)) & 0x1;\n        break;\n      default:\n        throw new IllegalArgumentException(\"Invalid mask pattern: \" + maskPattern);\n    }\n    return intermediate == 0;\n  }\n\n  /**\n   * Helper function for applyMaskPenaltyRule1. We need this for doing this calculation in both\n   * vertical and horizontal orders respectively.\n   */\n  private static int applyMaskPenaltyRule1Internal(ByteMatrix matrix, boolean isHorizontal) {\n    int penalty = 0;\n    int iLimit = isHorizontal ? matrix.getHeight() : matrix.getWidth();\n    int jLimit = isHorizontal ? matrix.getWidth() : matrix.getHeight();\n    byte[][] array = matrix.getArray();\n    for (int i = 0; i < iLimit; i++) {\n      int numSameBitCells = 0;\n      int prevBit = -1;\n      for (int j = 0; j < jLimit; j++) {\n        int bit = isHorizontal ? array[i][j] : array[j][i];\n        if (bit == prevBit) {\n          numSameBitCells++;\n        } else {\n          if (numSameBitCells >= 5) {\n            penalty += N1 + (numSameBitCells - 5);\n          }\n          numSameBitCells = 1;  // Include the cell itself.\n          prevBit = bit;\n        }\n      }\n      if (numSameBitCells >= 5) {\n        penalty += N1 + (numSameBitCells - 5);\n      }\n    }\n    return penalty;\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/encoder/MatrixUtil.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.encoder;\n\nimport peergos.shared.zxing.WriterException;\nimport peergos.shared.zxing.common.BitArray;\nimport peergos.shared.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport peergos.shared.zxing.qrcode.decoder.Version;\n\n/**\n * @author satorux@google.com (Satoru Takabayashi) - creator\n * @author dswitkin@google.com (Daniel Switkin) - ported from C++\n */\nfinal class MatrixUtil {\n\n  private static final int[][] POSITION_DETECTION_PATTERN = {\n      {1, 1, 1, 1, 1, 1, 1},\n      {1, 0, 0, 0, 0, 0, 1},\n      {1, 0, 1, 1, 1, 0, 1},\n      {1, 0, 1, 1, 1, 0, 1},\n      {1, 0, 1, 1, 1, 0, 1},\n      {1, 0, 0, 0, 0, 0, 1},\n      {1, 1, 1, 1, 1, 1, 1},\n  };\n\n  private static final int[][] POSITION_ADJUSTMENT_PATTERN = {\n      {1, 1, 1, 1, 1},\n      {1, 0, 0, 0, 1},\n      {1, 0, 1, 0, 1},\n      {1, 0, 0, 0, 1},\n      {1, 1, 1, 1, 1},\n  };\n\n  // From Appendix E. Table 1, JIS0510X:2004 (p 71). The table was double-checked by komatsu.\n  private static final int[][] POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = {\n      {-1, -1, -1, -1,  -1,  -1,  -1},  // Version 1\n      { 6, 18, -1, -1,  -1,  -1,  -1},  // Version 2\n      { 6, 22, -1, -1,  -1,  -1,  -1},  // Version 3\n      { 6, 26, -1, -1,  -1,  -1,  -1},  // Version 4\n      { 6, 30, -1, -1,  -1,  -1,  -1},  // Version 5\n      { 6, 34, -1, -1,  -1,  -1,  -1},  // Version 6\n      { 6, 22, 38, -1,  -1,  -1,  -1},  // Version 7\n      { 6, 24, 42, -1,  -1,  -1,  -1},  // Version 8\n      { 6, 26, 46, -1,  -1,  -1,  -1},  // Version 9\n      { 6, 28, 50, -1,  -1,  -1,  -1},  // Version 10\n      { 6, 30, 54, -1,  -1,  -1,  -1},  // Version 11\n      { 6, 32, 58, -1,  -1,  -1,  -1},  // Version 12\n      { 6, 34, 62, -1,  -1,  -1,  -1},  // Version 13\n      { 6, 26, 46, 66,  -1,  -1,  -1},  // Version 14\n      { 6, 26, 48, 70,  -1,  -1,  -1},  // Version 15\n      { 6, 26, 50, 74,  -1,  -1,  -1},  // Version 16\n      { 6, 30, 54, 78,  -1,  -1,  -1},  // Version 17\n      { 6, 30, 56, 82,  -1,  -1,  -1},  // Version 18\n      { 6, 30, 58, 86,  -1,  -1,  -1},  // Version 19\n      { 6, 34, 62, 90,  -1,  -1,  -1},  // Version 20\n      { 6, 28, 50, 72,  94,  -1,  -1},  // Version 21\n      { 6, 26, 50, 74,  98,  -1,  -1},  // Version 22\n      { 6, 30, 54, 78, 102,  -1,  -1},  // Version 23\n      { 6, 28, 54, 80, 106,  -1,  -1},  // Version 24\n      { 6, 32, 58, 84, 110,  -1,  -1},  // Version 25\n      { 6, 30, 58, 86, 114,  -1,  -1},  // Version 26\n      { 6, 34, 62, 90, 118,  -1,  -1},  // Version 27\n      { 6, 26, 50, 74,  98, 122,  -1},  // Version 28\n      { 6, 30, 54, 78, 102, 126,  -1},  // Version 29\n      { 6, 26, 52, 78, 104, 130,  -1},  // Version 30\n      { 6, 30, 56, 82, 108, 134,  -1},  // Version 31\n      { 6, 34, 60, 86, 112, 138,  -1},  // Version 32\n      { 6, 30, 58, 86, 114, 142,  -1},  // Version 33\n      { 6, 34, 62, 90, 118, 146,  -1},  // Version 34\n      { 6, 30, 54, 78, 102, 126, 150},  // Version 35\n      { 6, 24, 50, 76, 102, 128, 154},  // Version 36\n      { 6, 28, 54, 80, 106, 132, 158},  // Version 37\n      { 6, 32, 58, 84, 110, 136, 162},  // Version 38\n      { 6, 26, 54, 82, 110, 138, 166},  // Version 39\n      { 6, 30, 58, 86, 114, 142, 170},  // Version 40\n  };\n\n  // Type info cells at the left top corner.\n  private static final int[][] TYPE_INFO_COORDINATES = {\n      {8, 0},\n      {8, 1},\n      {8, 2},\n      {8, 3},\n      {8, 4},\n      {8, 5},\n      {8, 7},\n      {8, 8},\n      {7, 8},\n      {5, 8},\n      {4, 8},\n      {3, 8},\n      {2, 8},\n      {1, 8},\n      {0, 8},\n  };\n\n  // From Appendix D in JISX0510:2004 (p. 67)\n  private static final int VERSION_INFO_POLY = 0x1f25;  // 1 1111 0010 0101\n\n  // From Appendix C in JISX0510:2004 (p.65).\n  private static final int TYPE_INFO_POLY = 0x537;\n  private static final int TYPE_INFO_MASK_PATTERN = 0x5412;\n\n  private MatrixUtil() {\n    // do nothing\n  }\n  \n  // Set all cells to -1.  -1 means that the cell is empty (not set yet).\n  //\n  // JAVAPORT: We shouldn't need to do this at all. The code should be rewritten to begin encoding\n  // with the ByteMatrix initialized all to zero.\n  static void clearMatrix(ByteMatrix matrix) {\n    matrix.clear((byte) -1);\n  }\n\n  // Build 2D matrix of QR Code from \"dataBits\" with \"ecLevel\", \"version\" and \"getMaskPattern\". On\n  // success, store the result in \"matrix\" and return true.\n  static void buildMatrix(BitArray dataBits,\n                          ErrorCorrectionLevel ecLevel,\n                          Version version,\n                          int maskPattern,\n                          ByteMatrix matrix) throws WriterException {\n    clearMatrix(matrix);\n    embedBasicPatterns(version, matrix);\n    // Type information appear with any version.\n    embedTypeInfo(ecLevel, maskPattern, matrix);\n    // Version info appear if version >= 7.\n    maybeEmbedVersionInfo(version, matrix);\n    // Data should be embedded at end.\n    embedDataBits(dataBits, maskPattern, matrix);\n  }\n\n  // Embed basic patterns. On success, modify the matrix and return true.\n  // The basic patterns are:\n  // - Position detection patterns\n  // - Timing patterns\n  // - Dark dot at the left bottom corner\n  // - Position adjustment patterns, if need be\n  static void embedBasicPatterns(Version version, ByteMatrix matrix) throws WriterException {\n    // Let's get started with embedding big squares at corners.\n    embedPositionDetectionPatternsAndSeparators(matrix);\n    // Then, embed the dark dot at the left bottom corner.\n    embedDarkDotAtLeftBottomCorner(matrix);\n\n    // Position adjustment patterns appear if version >= 2.\n    maybeEmbedPositionAdjustmentPatterns(version, matrix);\n    // Timing patterns should be embedded after position adj. patterns.\n    embedTimingPatterns(matrix);\n  }\n\n  // Embed type information. On success, modify the matrix.\n  static void embedTypeInfo(ErrorCorrectionLevel ecLevel, int maskPattern, ByteMatrix matrix)\n      throws WriterException {\n    BitArray typeInfoBits = new BitArray();\n    makeTypeInfoBits(ecLevel, maskPattern, typeInfoBits);\n\n    for (int i = 0; i < typeInfoBits.getSize(); ++i) {\n      // Place bits in LSB to MSB order.  LSB (least significant bit) is the last value in\n      // \"typeInfoBits\".\n      boolean bit = typeInfoBits.get(typeInfoBits.getSize() - 1 - i);\n\n      // Type info bits at the left top corner. See 8.9 of JISX0510:2004 (p.46).\n      int[] coordinates = TYPE_INFO_COORDINATES[i];\n      int x1 = coordinates[0];\n      int y1 = coordinates[1];\n      matrix.set(x1, y1, bit);\n\n      if (i < 8) {\n        // Right top corner.\n        int x2 = matrix.getWidth() - i - 1;\n        int y2 = 8;\n        matrix.set(x2, y2, bit);\n      } else {\n        // Left bottom corner.\n        int x2 = 8;\n        int y2 = matrix.getHeight() - 7 + (i - 8);\n        matrix.set(x2, y2, bit);\n      }\n    }\n  }\n\n  // Embed version information if need be. On success, modify the matrix and return true.\n  // See 8.10 of JISX0510:2004 (p.47) for how to embed version information.\n  static void maybeEmbedVersionInfo(Version version, ByteMatrix matrix) throws WriterException {\n    if (version.getVersionNumber() < 7) {  // Version info is necessary if version >= 7.\n      return;  // Don't need version info.\n    }\n    BitArray versionInfoBits = new BitArray();\n    makeVersionInfoBits(version, versionInfoBits);\n\n    int bitIndex = 6 * 3 - 1;  // It will decrease from 17 to 0.\n    for (int i = 0; i < 6; ++i) {\n      for (int j = 0; j < 3; ++j) {\n        // Place bits in LSB (least significant bit) to MSB order.\n        boolean bit = versionInfoBits.get(bitIndex);\n        bitIndex--;\n        // Left bottom corner.\n        matrix.set(i, matrix.getHeight() - 11 + j, bit);\n        // Right bottom corner.\n        matrix.set(matrix.getHeight() - 11 + j, i, bit);\n      }\n    }\n  }\n\n  // Embed \"dataBits\" using \"getMaskPattern\". On success, modify the matrix and return true.\n  // For debugging purposes, it skips masking process if \"getMaskPattern\" is -1.\n  // See 8.7 of JISX0510:2004 (p.38) for how to embed data bits.\n  static void embedDataBits(BitArray dataBits, int maskPattern, ByteMatrix matrix)\n      throws WriterException {\n    int bitIndex = 0;\n    int direction = -1;\n    // Start from the right bottom cell.\n    int x = matrix.getWidth() - 1;\n    int y = matrix.getHeight() - 1;\n    while (x > 0) {\n      // Skip the vertical timing pattern.\n      if (x == 6) {\n        x -= 1;\n      }\n      while (y >= 0 && y < matrix.getHeight()) {\n        for (int i = 0; i < 2; ++i) {\n          int xx = x - i;\n          // Skip the cell if it's not empty.\n          if (!isEmpty(matrix.get(xx, y))) {\n            continue;\n          }\n          boolean bit;\n          if (bitIndex < dataBits.getSize()) {\n            bit = dataBits.get(bitIndex);\n            ++bitIndex;\n          } else {\n            // Padding bit. If there is no bit left, we'll fill the left cells with 0, as described\n            // in 8.4.9 of JISX0510:2004 (p. 24).\n            bit = false;\n          }\n\n          // Skip masking if mask_pattern is -1.\n          if (maskPattern != -1 && MaskUtil.getDataMaskBit(maskPattern, xx, y)) {\n            bit = !bit;\n          }\n          matrix.set(xx, y, bit);\n        }\n        y += direction;\n      }\n      direction = -direction;  // Reverse the direction.\n      y += direction;\n      x -= 2;  // Move to the left.\n    }\n    // All bits should be consumed.\n    if (bitIndex != dataBits.getSize()) {\n      throw new WriterException(\"Not all bits consumed: \" + bitIndex + '/' + dataBits.getSize());\n    }\n  }\n\n  // Return the position of the most significant bit set (to one) in the \"value\". The most\n  // significant bit is position 32. If there is no bit set, return 0. Examples:\n  // - findMSBSet(0) => 0\n  // - findMSBSet(1) => 1\n  // - findMSBSet(255) => 8\n  static int findMSBSet(int value) {\n    return 32 - Integer.numberOfLeadingZeros(value);\n  }\n\n  // Calculate BCH (Bose-Chaudhuri-Hocquenghem) code for \"value\" using polynomial \"poly\". The BCH\n  // code is used for encoding type information and version information.\n  // Example: Calculation of version information of 7.\n  // f(x) is created from 7.\n  //   - 7 = 000111 in 6 bits\n  //   - f(x) = x^2 + x^1 + x^0\n  // g(x) is given by the standard (p. 67)\n  //   - g(x) = x^12 + x^11 + x^10 + x^9 + x^8 + x^5 + x^2 + 1\n  // Multiply f(x) by x^(18 - 6)\n  //   - f'(x) = f(x) * x^(18 - 6)\n  //   - f'(x) = x^14 + x^13 + x^12\n  // Calculate the remainder of f'(x) / g(x)\n  //         x^2\n  //         __________________________________________________\n  //   g(x) )x^14 + x^13 + x^12\n  //         x^14 + x^13 + x^12 + x^11 + x^10 + x^7 + x^4 + x^2\n  //         --------------------------------------------------\n  //                              x^11 + x^10 + x^7 + x^4 + x^2\n  //\n  // The remainder is x^11 + x^10 + x^7 + x^4 + x^2\n  // Encode it in binary: 110010010100\n  // The return value is 0xc94 (1100 1001 0100)\n  //\n  // Since all coefficients in the polynomials are 1 or 0, we can do the calculation by bit\n  // operations. We don't care if coefficients are positive or negative.\n  static int calculateBCHCode(int value, int poly) {\n    if (poly == 0) {\n      throw new IllegalArgumentException(\"0 polynomial\");\n    }\n    // If poly is \"1 1111 0010 0101\" (version info poly), msbSetInPoly is 13. We'll subtract 1\n    // from 13 to make it 12.\n    int msbSetInPoly = findMSBSet(poly);\n    value <<= msbSetInPoly - 1;\n    // Do the division business using exclusive-or operations.\n    while (findMSBSet(value) >= msbSetInPoly) {\n      value ^= poly << (findMSBSet(value) - msbSetInPoly);\n    }\n    // Now the \"value\" is the remainder (i.e. the BCH code)\n    return value;\n  }\n\n  // Make bit vector of type information. On success, store the result in \"bits\" and return true.\n  // Encode error correction level and mask pattern. See 8.9 of\n  // JISX0510:2004 (p.45) for details.\n  static void makeTypeInfoBits(ErrorCorrectionLevel ecLevel, int maskPattern, BitArray bits)\n      throws WriterException {\n    if (!QRCode.isValidMaskPattern(maskPattern)) {\n      throw new WriterException(\"Invalid mask pattern\");\n    }\n    int typeInfo = (ecLevel.getBits() << 3) | maskPattern;\n    bits.appendBits(typeInfo, 5);\n\n    int bchCode = calculateBCHCode(typeInfo, TYPE_INFO_POLY);\n    bits.appendBits(bchCode, 10);\n\n    BitArray maskBits = new BitArray();\n    maskBits.appendBits(TYPE_INFO_MASK_PATTERN, 15);\n    bits.xor(maskBits);\n\n    if (bits.getSize() != 15) {  // Just in case.\n      throw new WriterException(\"should not happen but we got: \" + bits.getSize());\n    }\n  }\n\n  // Make bit vector of version information. On success, store the result in \"bits\" and return true.\n  // See 8.10 of JISX0510:2004 (p.45) for details.\n  static void makeVersionInfoBits(Version version, BitArray bits) throws WriterException {\n    bits.appendBits(version.getVersionNumber(), 6);\n    int bchCode = calculateBCHCode(version.getVersionNumber(), VERSION_INFO_POLY);\n    bits.appendBits(bchCode, 12);\n\n    if (bits.getSize() != 18) {  // Just in case.\n      throw new WriterException(\"should not happen but we got: \" + bits.getSize());\n    }\n  }\n\n  // Check if \"value\" is empty.\n  private static boolean isEmpty(int value) {\n    return value == -1;\n  }\n\n  private static void embedTimingPatterns(ByteMatrix matrix) {\n    // -8 is for skipping position detection patterns (size 7), and two horizontal/vertical\n    // separation patterns (size 1). Thus, 8 = 7 + 1.\n    for (int i = 8; i < matrix.getWidth() - 8; ++i) {\n      int bit = (i + 1) % 2;\n      // Horizontal line.\n      if (isEmpty(matrix.get(i, 6))) {\n        matrix.set(i, 6, bit);\n      }\n      // Vertical line.\n      if (isEmpty(matrix.get(6, i))) {\n        matrix.set(6, i, bit);\n      }\n    }\n  }\n\n  // Embed the lonely dark dot at left bottom corner. JISX0510:2004 (p.46)\n  private static void embedDarkDotAtLeftBottomCorner(ByteMatrix matrix) throws WriterException {\n    if (matrix.get(8, matrix.getHeight() - 8) == 0) {\n      throw new WriterException();\n    }\n    matrix.set(8, matrix.getHeight() - 8, 1);\n  }\n\n  private static void embedHorizontalSeparationPattern(int xStart,\n                                                       int yStart,\n                                                       ByteMatrix matrix) throws WriterException {\n    for (int x = 0; x < 8; ++x) {\n      if (!isEmpty(matrix.get(xStart + x, yStart))) {\n        throw new WriterException();\n      }\n      matrix.set(xStart + x, yStart, 0);\n    }\n  }\n\n  private static void embedVerticalSeparationPattern(int xStart,\n                                                     int yStart,\n                                                     ByteMatrix matrix) throws WriterException {\n    for (int y = 0; y < 7; ++y) {\n      if (!isEmpty(matrix.get(xStart, yStart + y))) {\n        throw new WriterException();\n      }\n      matrix.set(xStart, yStart + y, 0);\n    }\n  }\n\n  private static void embedPositionAdjustmentPattern(int xStart, int yStart, ByteMatrix matrix) {\n    for (int y = 0; y < 5; ++y) {\n      int[] patternY = POSITION_ADJUSTMENT_PATTERN[y];\n      for (int x = 0; x < 5; ++x) {\n        matrix.set(xStart + x, yStart + y, patternY[x]);\n      }\n    }\n  }\n\n  private static void embedPositionDetectionPattern(int xStart, int yStart, ByteMatrix matrix) {\n    for (int y = 0; y < 7; ++y) {\n      int[] patternY = POSITION_DETECTION_PATTERN[y];\n      for (int x = 0; x < 7; ++x) {\n        matrix.set(xStart + x, yStart + y, patternY[x]);\n      }\n    }\n  }\n\n  // Embed position detection patterns and surrounding vertical/horizontal separators.\n  private static void embedPositionDetectionPatternsAndSeparators(ByteMatrix matrix) throws WriterException {\n    // Embed three big squares at corners.\n    int pdpWidth = POSITION_DETECTION_PATTERN[0].length;\n    // Left top corner.\n    embedPositionDetectionPattern(0, 0, matrix);\n    // Right top corner.\n    embedPositionDetectionPattern(matrix.getWidth() - pdpWidth, 0, matrix);\n    // Left bottom corner.\n    embedPositionDetectionPattern(0, matrix.getWidth() - pdpWidth, matrix);\n\n    // Embed horizontal separation patterns around the squares.\n    int hspWidth = 8;\n    // Left top corner.\n    embedHorizontalSeparationPattern(0, hspWidth - 1, matrix);\n    // Right top corner.\n    embedHorizontalSeparationPattern(matrix.getWidth() - hspWidth,\n        hspWidth - 1, matrix);\n    // Left bottom corner.\n    embedHorizontalSeparationPattern(0, matrix.getWidth() - hspWidth, matrix);\n\n    // Embed vertical separation patterns around the squares.\n    int vspSize = 7;\n    // Left top corner.\n    embedVerticalSeparationPattern(vspSize, 0, matrix);\n    // Right top corner.\n    embedVerticalSeparationPattern(matrix.getHeight() - vspSize - 1, 0, matrix);\n    // Left bottom corner.\n    embedVerticalSeparationPattern(vspSize, matrix.getHeight() - vspSize,\n        matrix);\n  }\n\n  // Embed position adjustment patterns if need be.\n  private static void maybeEmbedPositionAdjustmentPatterns(Version version, ByteMatrix matrix) {\n    if (version.getVersionNumber() < 2) {  // The patterns appear if version >= 2\n      return;\n    }\n    int index = version.getVersionNumber() - 1;\n    int[] coordinates = POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[index];\n    for (int y : coordinates) {\n      if (y >= 0) {\n        for (int x : coordinates) {\n          if (x >= 0 && isEmpty(matrix.get(x, y))) {\n            // If the cell is unset, we embed the position adjustment pattern here.\n            // -2 is necessary since the x/y coordinates point to the center of the pattern, not the\n            // left top corner.\n            embedPositionAdjustmentPattern(x - 2, y - 2, matrix);\n          }\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/peergos/shared/zxing/qrcode/encoder/QRCode.java",
    "content": "/*\n * Copyright 2008 ZXing authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage peergos.shared.zxing.qrcode.encoder;\n\nimport peergos.shared.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport peergos.shared.zxing.qrcode.decoder.Mode;\nimport peergos.shared.zxing.qrcode.decoder.Version;\n\n/**\n * @author satorux@google.com (Satoru Takabayashi) - creator\n * @author dswitkin@google.com (Daniel Switkin) - ported from C++\n */\npublic final class QRCode {\n\n  public static final int NUM_MASK_PATTERNS = 8;\n\n  private Mode mode;\n  private ErrorCorrectionLevel ecLevel;\n  private Version version;\n  private int maskPattern;\n  private ByteMatrix matrix;\n\n  public QRCode() {\n    maskPattern = -1;\n  }\n\n  public Mode getMode() {\n    return mode;\n  }\n\n  public ErrorCorrectionLevel getECLevel() {\n    return ecLevel;\n  }\n\n  public Version getVersion() {\n    return version;\n  }\n\n  public int getMaskPattern() {\n    return maskPattern;\n  }\n\n  public ByteMatrix getMatrix() {\n    return matrix;\n  }\n\n  @Override\n  public String toString() {\n    StringBuilder result = new StringBuilder(200);\n    result.append(\"<<\\n\");\n    result.append(\" mode: \");\n    result.append(mode);\n    result.append(\"\\n ecLevel: \");\n    result.append(ecLevel);\n    result.append(\"\\n version: \");\n    result.append(version);\n    result.append(\"\\n maskPattern: \");\n    result.append(maskPattern);\n    if (matrix == null) {\n      result.append(\"\\n matrix: null\\n\");\n    } else {\n      result.append(\"\\n matrix:\\n\");\n      result.append(matrix);\n    }\n    result.append(\">>\\n\");\n    return result.toString();\n  }\n\n  public void setMode(Mode value) {\n    mode = value;\n  }\n\n  public void setECLevel(ErrorCorrectionLevel value) {\n    ecLevel = value;\n  }\n\n  public void setVersion(Version version) {\n    this.version = version;\n  }\n\n  public void setMaskPattern(int value) {\n    maskPattern = value;\n  }\n\n  public void setMatrix(ByteMatrix value) {\n    matrix = value;\n  }\n\n  // Check if \"mask_pattern\" is valid.\n  public static boolean isValidMaskPattern(int maskPattern) {\n    return maskPattern >= 0 && maskPattern < NUM_MASK_PATTERNS;\n  }\n\n}\n"
  },
  {
    "path": "test/resources/static_handler/something.txt",
    "content": "The thing!"
  },
  {
    "path": "test/resources/static_handler/test/hello.txt",
    "content": "Hello, Peergos!"
  }
]